modelier v0.0.0
Modelier
NOTE: work in progress! this is not a thing yet!
This is an attempt to define a somewhat standard interface to create the model
part of a system. In a sense it is kind of an abstract of the
active record married
with Promise
and decoupled from the actual persistence layer.
The idea here is to define an standard, which then can be reimplemented with different persistence layers underneath it. The point of all this exercise is to decouple the application level code from the persistence layer; and bring balance to the galaxy.
Why Active Record
Active record, as any other, has its drawbacks. But, regardless of those it is almost unbeatably good at one thing: representing tabular data.
In the end you don't have to build your app logic on top of active record. In many cases it is simple enough to do so, in some it isn't. But, regardless to the case, it is much easier to talk to an active record than to a database directly.
Why Promises?
Don't fight it. You can't win this one....
Modeling
In modelier
there is an idea of I call schema
. This thing is basically your
mapping to the persistence layer. It is not supposed to have any actual business
logic of your application. A schema simply outlines units persisted properties
and relationships between each other.
import { Schema } from "modelier-mongodb";
const Model = new Schema({ url: "mongodb://localhost..." });
export const User = new Model("User", {
email: String,
username: String,
password: String,
createdAt: Date,
updatedAt: Date
});
Model.index(User, "username");
export const Post = new Model("Post", {
urlSlug: String,
title: String,
body: String,
author: User, // <- direct association
createdAt: Date,
updatedAt: Date
});
Model.index(Post, "urlSlug");
Model.index(Post, "createdAt");
export const Comment = new Model("Comment", {
post: Post,
author: User,
text: String,
createdAt: Date
});
Model.index(Comment, "post");
Model.index(Comment, "createdAt");
There are a few important moments to consider. Firstly, primary ids are implied.
Same for relationship references, they should be done on a model to model basis
and actual references should be consistently auto-generated into authorId
and
such by the engine.
Indexes
The generic indexes interface should look somewhat like this:
const Provider = new Schema(....);
Provider.index(Model, "field");
Provider.index(Model, ["field1", "field2"]);
Some specific providers might add some extra options that are related to the databases they manage.
Automatic Timestamps
If a model has a createdAt
and/or updatedAt
properties defined in a schema,
those properties will be automatically populated.
CRUD Operations
All models will have a standard CRUD operations interface regardless of the actual
persistence layer provider. All those methods will return an instance of Promise
and are supposed to be used with the ES7 async/await functions.
var user = await new User({username: "nikolay"}).save();
await user.update({password: "NikolayR0k5!"});
await user.delete();
There are also a set of similar operations that can be performed on a whole table
await User.update({admin: true}); // make _everyone_ an admin
await User.delete(); // delete everything
NOTE: the table level operations are combinable with the query filters, see below.
Querying
Querying in modelier
consists of basically filters and resolvers
// to find a record by an ID
const user = await User.find("12345"); // NOTE: rejects into NotFound
const admins = await User.filter({admin: true}); // or #.all();
const admin = await User.filter({admin: true}).first(); // also #last();
The #filter()
method returns an extended Promise
which has a bunch of
chained methods to aggregate data:
const admins = await User.filter({admin: true}).count();
const names = await User.filter({admin: true}).pluck("username");
NOTE the promise that was returned only trigger the actual query once the
then
method is called. Before that happens you can chain it as much as you like.
The #filter()
method can take the following parameters:
User.filter({
username: "nikolay", // direct match
username: /nikolay/, // regexp match
username: ["nikolay", "andrew"], // one of the optins
username: null // checks missing properties
name: { // querying the nested attributes
first: "Nikolay",
last: "Rocks"
}
});
You also can use the implicit schema references between the models with the
#filter()
method:
const user = await User.find("12345");
const posts = await Post.filter({author: user});
This will automatically resolve the external key references and build a correct query to the database.
Ordering, Grouping, Aggregation
The query language also has several methods to describe various ordering and aggregation queries:
const latest = await Post.sort("createdAt").slice(0, 10);
const counts = await Post.group("author").count(); // #avg("rating")...
Most of the method names are derived from the Array
unit in javascript, but
actually are lazy methods for the querying language.
Custom scopes/filters
TODO
Lifecycle Hooks
TODO
Validation
TODO
Copyright & License
All code in this repository is released under the terms of the MIT license
Copyright (C) 2016 Nikolay Nemshilov
9 years ago