front-orm
TypeScript icon, indicating that this package has built-in type declarations

0.3.0-beta.7 • Public • Published

front-orm

The package is designed for easy-to-use receipt, storage, refreshing and updating of data received from the server

Installation

npm install front-orm

Quick Start

npm install front-orm

Importing

import {EntityManager} from "front-orm";

Create entity manager

const em = new EntityManager(/* setReactivity */)

You can add object tracking or a reactivity creation function. For example, use watch-object

npm install watch-object
import {watch} from "watch-object";

const setReactivity = (data) => {
  if (!Array.isArray(data)) {
    watch(data, 'name', (newVal, oldVal) => {
      console.log('obj name set', newVal, ' oldVal:', oldVal)
    })
  }
  return data
}

Create models

// You can also import fields classes
const PrimaryKey = em.defaultClasses.fields.PrimaryKey
const StringField = em.defaultClasses.fields.StringField
const NumberField = em.defaultClasses.fields.NumberField
const CollectionField = em.defaultClasses.fields.CollectionField
const EntityField = em.defaultClasses.fields.EntityField

function Author(em) {
  return {
    id: new PrimaryKey(em),
    name: new StringField(em),
    age: new NumberField(em),
    books: new CollectionField(em, 'Book')
  };
}

function Book(em) {
  return {
    id: new PrimaryKey(em),
    name: new StringField(em),
    author: new EntityField(em, Author.name),
  };
}

Add a model to the Entity manager

// You can also import classes
const Collection = em.defaultClasses.types.Collection
const Entity = em.defaultClasses.types.Entity

em.setModel(Author, {
  find: new Entity(em, (value) => {
    // Your request to the server
    return responseData
  }),
})
em.setModel(Book, {
  findSomeThing: new Collection(em, (value) => {
    // Your request to the server
    return responseData
  }),
})

Add hooks that will be used for all models

em.setHooks({
  get(model, pk) {
    /*
     Your request to the server
     return the data corresponding to the model  
    */
  },
  create(modelData, value) {
    /*
     Your request to the server
     return the primary key
    */
  },
  update(modelData, value) {
    /*
     Your request to the server
     return the primary key
    */
  },
  delete(modelData, pk) {
    /*
     Your request to the server
     return the primary key
    */
  },
})

Use package api

const author = await em.repositories.Author.find(authorParams)
const booksCollection = await em.repositories.Book.findSome(bookParams)
const book = booksCollection[0]

const newAuthor = em.post(someNewValue, em.models.Author)
em.put(someUpdatingValue, book)
em.flush()

Demo

Codesanbox example

or

git clone https://github.com/bd2051/front-orm-quick-example.git
cd front-orm-quick-example
npm install
npm run serve

Overview

The main goal of the library is to quickly create a data processing system from a server with Restful API. The library is suitable for small applications and prototypes.

The data storage uses an ORM design. When calling the missing property, data is requested from the server.

The library implies the use of data reactivity but does not contain it.

For full implementation, signals from the server about updating data are required. Use websockets, sse or long/short polling.

The scheme of the library is shown in the figure

plot

Model

To initialize the model, you need to create a model function that returns a set of model fields

function Book(em) {
  return {
    id: new PrimaryKey(em),
    name: new StringField(em),
    page: new NumberField(em),
    stories: new CollectionField(em, 'Story', (value) => value.id),
    author: new EntityField(em, 'Author', (value) => value.id)
  };
}

The function name is used as the model name.

Fields classes

For all fields, the first parameter is an instance of the entity manager.

PrimaryKey is a required field. Used for identification and requests.

StringField is used for a regular string field.

NumberField is used for a regular number field. Currently, no different from StringField.

BooleanField is used for a boolean field. Currently, no different from StringField. See Why beta

CollectionField is used to associate OneToMany or ManyToMany with another entity.

The second parameter is the name of the associated entity.

The third parameter is a function for bringing an entity object to the primary key. By default

(pk) => pk

Example:

Data received from the server for the entity Book

const book = {
  id: 1,
  stories: [{id: 1}, {id: 10}, {id: 11}],
  ...otherFields  
}

Then the function should look like this

const convertValueToPk = (value) => value.id

If the data from the server looks like this

const book = {
  id: 1,
  stories: [1, 10, 11],
  ...otherFields  
}

you can use the default function

(pk) => pk

EntityField is used to associate OneToOne or ManyToOne with another entity. The parameters are similar to CollectionField.

Model initializing

To initialize the model, there is a setModel method

em.setModel(Book, {
  find: new Entity(em, (value) => {
    // Your request to the server
    return responseData
  }),
  findAll: new Collection(em, (value) => {
    // Your request to the server
    return responseData
  }),
})

The first parameter is a model function

The second parameter used to create a model repository. It is an object with the Entity and Collection classes

Entity and Collection are classes for determining the response from the server.

For both classes, the first parameter is an instance of the entity manager, and the second is a callback with custom requests to the server. The callback parameter can be any.

Callback response:

  • for Entity, an object with model fields must be returned
  • for Collection, an array of objects with model fields must be returned

Repository

The Repository stores all methods of getting data from the server added during initialization of the model.

Getting access to the Repository

const repository = em.repositories[modelName]

or

const repository = em.getRepository(modelName)

Public properties and methods:

  • $em - instance of the entity manager
  • $model - instance of Model
  • $refreshCollection - a method for getting refreshed data from the server

Data storage

The main interaction with data occurs through the ModelView and the ModelView collection.

The structure of the data storage object is shown in the figure.

plot

The BaseModel contains methods used at higher levels.

  • $em is an instance of the entity manager
  • $getName is a function that returns the model name
  • $getPkName is a function that returns the name of the primary key
  • $refresh is a function for refreshing data from the server
  • $get, $create, $update, $delete calls the corresponding entity manager hook

The Model object is created when initializing the model function based on the prototype BaseModel. Contains the same fields as the model function.

The ModelData object is the main data storage element. Based on the prototype Model. Filled with data from the server based on the model fields. A link to the corresponding ModelData is placed in the relationship field. If there is no data, an instance of the class Empty is assigned to the field.

ModelView is based on the Proxy class with the ModelData target object. When accessing properties, either the value of the target object field or the ModelView of the associated object is returned. If the value of the target object field is equal to an instance of the Empty class, the 'get' hook is called. The assignment operation does not work, use the put method of the entity manager to change the data.

ModelView collection is an array of ModelView objects. Using the methods 'push', 'pop', 'shift', 'unshift' will cause an error. To change the data, the put method of the entity manager is used

Garbage collection

Memory is freed automatically by the garbage collector and does not require additional actions from the programmer. To do this, most of the internal relationships between data are implemented on the basis of WeakSet, WeakMap and WeakRef. Thus, the only live references to the data are ModelView and ModelView collection obtained using the entity manager. Excluding them from memory is a condition for starting the garbage collector for this data.

Note: The js garbage collector has a complex logic of operation, so even without live references, the object can continue to be in memory for some unspecified time.

Hooks

list of hooks used:

  • preFlush
  • get
  • create
  • update
  • delete

All hooks except 'get' are triggered during the execution of the 'flush' method. 'Get' is run in all cases when the Data Storage did not have the necessary data.

preFlush is designed for manipulating commits before launching the rest of the hooks. Read more about commits below.

  • The input parameter is an array of commits.
  • Waiting for the return of an array of commits or an empty array

get is used to get missing data in ModelData

  • Input parameters:
    • Model
    • primary key
  • Waits for the return of an object with fields similar to the Model

create is designed to send information about the creation of new data.

  • Input parameters:
    • ModelData
    • generalized value obtained from the commits for a given modelData
  • Waiting for the primary key to be returned

update is designed to send information about the updating of data. Similar to create

delete is designed to send information about data deletion

  • Input parameters:
    • ModelData
    • primary key
  • Waiting for the primary key to be returned

Commits

All up-to-date information about changes in model data is stored in commits. This allows you to roll back changes within certain limits. Updating of the model data with server data occurs by the 'flush' method. At the same time, all commits are overwritten. During execution, heuristic logic determines the use of appropriate hooks for commits. Depending on the specifics of data processing by the server, you may need to change the logic. There is a preFlash hook for this. It can also be used to create batch requests. Returning an empty array will result in the remaining hooks not being started.

Entity Manager

The main element of the api library is the entity manager

Creating:

const em = new EntityManager(setReactivity);

The input parameter is optional and is intended to create ModelData reactivity. Read more about reactivity below.

Public properties of the EntityManager:

  • models
  • repositories
  • pending
  • removed
  • defaultClasses

models contains instances of all available Models. Needed for some methods

repositories contains all the methods added during initialization of the Model. It has a tree-like structure. At the first level, the names of the models. On the second methods

The pending value is substituted for the waiting time for a response from the server. By default, null.

The removed value is substituted if the ModelData has been removed. By default, undefined.

defaultClasses contains all public classes available in the library. These classes can be imported from the library.

Public methods of the EntityManager:

  • setHooks
  • setModel
  • getModel
  • getRepository
  • onAddModelData
  • onAddCollection
  • put
  • post
  • remove
  • flush
  • revert
  • revertAll
  • checkModelDataByPk

setHooks is designed to add all entity manager hooks. Read more here

setModel is designed to add models to the entity manager. Read more here

getModel and getRepository return the Model and repository of the Model, respectively. Input parameter model name.

onAddModelData and onAddCollection are handlers designed to organize messaging with the server. Read more here

put is the main method for changing model data

  • Input parameters:
    • an object with properties that need to be changed
    • ModelView or Model (To create the input parameter Model)
  • ModelView is returned

Example:

const book = em.repositories.Book.find(1)

console.log(book.name) // 'Old book name'
em.put({
  name: 'New book name'
}, book)

console.log(book.name) // 'New book name'

post wrapper over the put method. Intended only for creating.

  • Input parameters:
    • an object with properties similar to the Model
    • Model
  • ModelView is returned

Example:

const newBook = em.post({
  name: 'Created book',
  ...otherProperties
}, em.models.Book)

console.log(newBook.name) // 'Created book'

remove wrapper over the put method. Intended only for deleting.

  • The only input parameter is ModelView
  • ModelView is returned

flush starts the processes for sending data to the server. Read more here

revert rolls back model data changes. The only input parameter is the number of commits being rolled back. By default, one. Read more here

revertAll this wrapper over the revert method. Designed to roll back all changes.

checkModelDataByPk is a method that checks ORM data exists by pk.

  • Input parameters:
    • Model
    • Primary key
  • Boolean is returned

Reactivity

The library implies the use of reactivity, but it does not have it itself. To add reactivity as an input parameter when creating an entity manager, add the function. The function will be called every time ModelData or ModelView collection is created

const setReactivity = (data) => {
  // do something to make the data reactive
  return reactiveData
}

const em = new EntityManager(setReactivity);

At the moment, reactivity works exactly when using Vue 3. See Why beta

Example with Vue 3

import { EntityManager } from 'front-orm';
import { ref } from 'vue';

const em = new EntityManager((modelData) => ref(modelData).value);

Real-time update

For a full real-time update, you need to establish negative feedback with the server. This is especially true for collections. You can use websockets, server events, or long/short polling for this.

Example based on websockets

const ws = new WebSocket(URL)
const em = new EntityManager()

em.onAddModelData = (model, pk) => {
  ws.send(JSON.stringify({
    model: model.$getName(),
    id: pk
  }))
}

const collectionsMap = new Map()

em.onAddCollection = (repository, weakCollection) => {
  ws.send(JSON.stringify({
    model: repository.$model.$getName(),
    isCollection: true
  }))
  if (typeof collectionsMap.get(name) === 'undefined') {
    collectionsMap.set(name, []);
  }
  collectionsMap.get(name).push({weakCollection, repository});
}
  
ws.addEventListener('message', (message) => {
  const { model: modelName, id } = JSON.parse(wsMessage.data);
  const updatingCollections = collectionsMap.get(modelName) || [];
  updatingCollections.forEach(({weakCollection, repository}) => {
    const collection = weakCollection.deref();
    if (typeof collection === 'undefined') {
      return;
    }
    repository?.$refreshCollection(collection);
  });
  em.getModel(modelName).$refresh(id)
})

onAddModelData is called when ModelData is created

  • Input parameters:
    • Model
    • primary key

ModelData is not passed so as not to break the logic of garbage collection. Read more here

onAddCollection is called when collection is created

  • Input parameters:
    • Repository. Read more here
    • WeakCollection. It is WeakRef with collection target

Use the 'deref' method to interact with the collection.

When receiving a message from the server about data changes, use the $refresh method of the Model and $refreshCollection of the Repository

Why beta version

Even though the idea of the library is simple, the implementation revealed certain difficulties. For the stable version, there is not enough experience in the practical use of the library.

Using the library with frameworks other than vue 3 may result in structure changes. Some solutions applied are not final. For example, the StringField, Number Field, and BooleanField classes all look the same. Perhaps they will be replaced by a single field.

I intend to support the project and hope for feedback from using the library.

Package Sidebar

Install

npm i front-orm

Weekly Downloads

8

Version

0.3.0-beta.7

License

ISC

Unpacked Size

211 kB

Total Files

109

Last publish

Collaborators

  • bd2051