First module fully documented is lib/ormish.js

main
Zed A. Shaw 1 year ago
parent ff28e3febc
commit 6f49bc2455
  1. 15
      admin/pages/DocsBrowser.svelte
  2. 252
      lib/ormish.js

@ -29,6 +29,14 @@
index = status === 200 ? data : {};
}
const type_to_syntax = {
callexpression: "()",
objectexpression: "{}",
"function": "()",
"class": "{}",
"method": "()"
}
onMount(async () => {
await load_index();
});
@ -38,6 +46,7 @@
$: if(params.wild && params.wild !== url) {
load_docs(params.wild);
}
</script>
<style>
@ -76,7 +85,7 @@
}
export.class-def {
background-color: var(--value6);
background-color: var(--value7);
}
export > heading {
@ -156,7 +165,7 @@
{#each exp.methods as member}
<export class="member">
<heading>
<h4>{ member.name }</h4>
<h4>.{member.name}{ type_to_syntax[member.isa] || "" }</h4>
<meta-data>
{docs_data.source}:{ member.line_start }
<em>{ member.isa } of { exp.name }</em>
@ -178,7 +187,7 @@
{:else}
<export>
<heading>
<h4>{exp.name}</h4>
<h4>{exp.name}{ type_to_syntax[exp.isa] || "" }</h4>
<meta-data>
{docs_data.source}:{ exp.line_start }
<em>{ exp.isa }</em>

@ -1,5 +1,20 @@
/*
The ORMish object-relational-mapping-ish library for knex. It's just enough ORM added to knex to be useful, and anything else you need can be done with knex.
The ORMish object-relational-mapping-ish library for knex. It's just enough
ORM added to knex to be useful, and anything else you need can be done with
knex. If there's any situation where you can't do something in ORMish it's
expected that you will access the `knex` variable in thie module and simply use
knex directly. Here's how you'd do that:
```javascript
import { knex } from './lib/ormish.js';
// do your own update using a model's table name
knex(somemodel.table_name).where(where).update(what);
```
This makes it easy to use ORMish, but still break out when you need to create
custom queries or speed things up. The only advice here is to put any special
operations in a `class Model` subclass as a member function.
*/
import config from '../knexfile.cjs';
import knexConfig from 'knex';
@ -192,12 +207,24 @@ export class Model {
return Object.fromEntries(clean_entries);
}
/*
Returns the name of this model's current table. Used mostly internally but
useful otherwise. This is an attribute accessor (get) so you can just do:
```javascript
const name = obj.table_name;
```
Instead of calling it like a function. The main reason to use this is in
tools like the admin table browser found in `admin/pages/Table.svelte` and
`api/admin/table.js` so check those files out to see what's going on.
*/
get table_name() {
return this.constructor.table_name;
}
/*
Returns an object of basic rules meant for lib/api.js:validate
Returns an object of basic rules meant for `lib/api.js:validate`
based on what's in the database. It's meant to be an easy to
pass in starter which you can augment. It expects a set of rules
with keys you want configured. Any key that's set to an empty string ""
@ -205,7 +232,8 @@ export class Model {
It's designed to be called once at the top of an api/ handler to get
a basic set of rules. You could also run it to print out the rules then
simply write the rules directly where you need them.
simply write the rules directly where you need them. It actually just
calls the `validation` module function with `validation(this.table_name, rules)`.
- `param rules {Object}` - rules specifier
*/
@ -213,6 +241,18 @@ export class Model {
return validation(this.table_name, rules);
}
/*
Delete this object from the database. It uses this object's `.id` to determine
which object to delete and uses this `knex` code:
```javascript
await knex(this.table_name).where({id: this.id}).del();
```
As with all of ORMish it doesn't handle any relations or references when it does
the delete so if you have constraints it will fail. Use `knex` directly in that
case.
*/
async destroy() {
assert(this.table_name !== undefined, "You must set class variable table_name.");
@ -221,15 +261,107 @@ export class Model {
del();
}
/*
Does very basic 1-to-1 (1:1) relation, for use inside a custom function that returns
additional records. 1:1 mappings don't show up too often, as I think most modern database
designs would rather combine them into one giant table, but it does happen when you need to
add to a database without changing a "sacred" table.
This is really just a call to `model.first(where, columns)` and mostly acts as a kind of
documentation.
+ `model Model` - The other model to query.
+ `where Object` - The knex style selection criteria passed to `Model.first`.
+ `columns` - Passed to `Model.first` to restrict the columns returned.
*/
async has_one(model, where, columns) {
assert(where.id !== undefined, `where must at least have id for has_one ${model.table_name} you have ${JSON.stringify(where)}`);
return await model.first(where, columns);
}
/*
Maps this model to another with a 1-to-many (1:M) relational mapping. It queries
the other `model` based on the given { where }, which is usually an `id` in the other
table.
For example, if I have a `user` table and a `payment` table, I can have a 1 `user` -> M `payment`
like this:
```javascript
export class User extends Model.from_table('user') {
async payments() {
return await this.has_many(Payment, { user_id: this.id });
}
}
```
In this situation my `User` model is querying the `Payment` model for any `payment`
records that have `user_id=this.id`. That means if the `user.id` is 1, then it will
find any `payment` records with `user_id=1`.
*/
async has_many(model, where, columns) {
return await model.all(where, columns);
}
/*
Implements a simple many-to-many (M:M) using an intermediary table which
maps two table's IDs using two columns. For example, if you have a `User`
and `Payment` model, and you decide that `Payment` can have multiple users
then you'd do this:
```javascript
await paid1.many_to_many(User, "payment_user");
```
This is translated into `knex` as:
```javascript
await knex("user").where("id", "in",
knex("payment_user") // subquery in payment_user
.select(`user_id as id`)
.where(`payment_id`, "=", this.id)
);
```
The inverse operation would be:
```javascript
await user1.many_to_many(Payment, "payment_user");
```
Which is translated into `knex` as:
```javascript
await knex("payment").where("id", "in",
knex("payment_user") // subquery in payment_user
.select(`payment_id as id`)
.where(`user_id`, "=", this.id)
);
```
### Performance
This will fail if you have a massive many-to-many since it uses a subquery to get
a set of IDs, but in many cases it actually might outperform a more direct complicated
query. You should use this, then resort to raw `knex` code to craft a better one as
needed.
### Cleaning
Everything returned is first ran though `.clean()` to and turned into the
target model so you can use it directly. This ensures that if a database
driver infects the returned data with garbage it will be cleaned and you
get pure models. In the above example you'd get a `User` or `Payment`
with only what's in the schema.
### Attributed Relations
If your relation table has extra attributes--a super useful trick--then this
will not pick them up. In our example above, if you have another field
`payment_user.transaction_date` in addition to `user_id` and `payment_id` then
you won't get the `transaction_date`. In that case--you guessed it--use `knex`
directly to query for those.
*/
async many_to_many(model, through_table) {
// SECURITY: doing string interpolation which might allow injecting SQL
let query = knex(model.table_name).where("id", "in",
@ -246,6 +378,15 @@ export class Model {
return results;
}
/*
Counts the number of records matching the `where` specification.
It uses a direct SQL query using the `knex` operation `count`, but
`knex` returns a "database specific" result. This will try to
extract the count result, but will warn you when it can't do that.
+ `where Object` - knex where specification.
+ `columns Array` - columns to return
*/
static async count(where, columns) {
// the knex count api returns a DB specific result, so we need
// to specify what we want, which is count:
@ -260,6 +401,23 @@ export class Model {
}
}
/*
Your generic insert for this model's table. The `attr` is checked against the
database schema by `knex`, so you have to remove anything that doesn't belong.
Use the `Model.clean` function to do that easily. Otherwise it works exactly
like in knex with:
```javascript
await knex(this.table_name).insert(attr);
```
This function expects a return value that has one result with the `id` of
the inserted row, which might be database specific. These days if a database
doesn't support this it's a trash database that shouldn't be used.
+ `attr Object` - The attributes to store.
+ `return Number` - returns the id of the inserted object
*/
static async insert(attr) {
assert(this.table_name !== undefined, "You must set class variable table_name.");
assert(attr, `You must give some attr to insert into ${this.table_name}`);
@ -295,11 +453,37 @@ export class Model {
return result !== undefined ? result[0] : undefined;
}
/*
Performs an UPDATE operation on the object `what` found by `where`. This
translates into the following `knex.js` code:
```javascript
return knex(this.table_name).where(where).update(what);
```
So it's mostly just syntactic sugar over `knex.js` (like everything in ORMish). If you
need a more complex `update` then just use `knex()` directly.
*/
static async update(where, what) {
assert(where, "You must give a where options.");
return knex(this.table_name).where(where).update(what);
}
/*
Returns one record from the table based on the `knex` where specification,
and can limit the columns (attributes) that are returned. This is a `static`
method so you use it like this:
```javascript
const person = User.first({id: 1});
```
The `where` can be anything that `knex` understands as query as well.
+ `where Object` -- The `knex` where specification.
+ `columns Array` -- List of columns to include in the returned object.
+ `return Object`
*/
static async first(where, columns) {
assert(where, "You must give a where options.");
let attr = undefined;
@ -313,11 +497,37 @@ export class Model {
return attr !== undefined ? new this(attr) : attr;
}
/*
Delete a given record--or batch of records--based on the `where` specification.
It really just calls this `knex` operation:
```javascript
return knex(this.table_name).where(where).del();
```
+ `where Object` -- The where specification, doesn't have to be an Object.
*/
static async delete(where) {
assert(where, "You must give a where options.");
return knex(this.table_name).where(where).del();
}
/*
Returns all records matching the `where` specification, and also
reduces the returned columns based on the `columns` list. This is
the most common operation in `knex`, and when you do both `columns`
and `where` it's just doing this:
```javascript
results = await knex(this.table_name).column(columns).where(where).select();
```
As usual, if this isn't efficient or complex enough for you then you can
just do it directly in `knex`.
+ `where Object` -- The usual `knex` where specification, and can be anything `knex` likes.
+ `columns Array` -- The list of columns to return.
*/
static async all(where, columns) {
assert(where, "You must give a where options.");
let results = [];
@ -332,6 +542,29 @@ export class Model {
return final;
}
/*
This is how you create your own models based on `Model`. It's a neat
trick as well, which allows you to specify a table to use to populate
a new class. First, you use it like this:
```javascript
class User extends Model.from_table('user') {
}
```
How this works:
+ `from_table` crafts an empty class with `let m = class extends Model {}`.
+ Since JavaScript is a scripting language you can modify this class, and return it.
+ `from_table` then adds a `table_name` and the `schema` to this class.
+ After that it returns the new empy class, which you then extend and now you have a class pre-configured with the schema and table_name already set.
Obviously this only works with ES6 style classes. After this setup you just add your own
methods, use `super` like normal, and everything else. The functions in `Model` will all
work because `schema` and `table_name` are set.
+ `table_name` -- the name of the base table in the database to use. Must be in `SCHEMA`.
*/
static from_table(table_name) {
let m = class extends Model { };
m.table_name = table_name;
@ -343,6 +576,19 @@ export class Model {
return m;
}
/*
Determines if at least one record exists for the `where` specification.
This is doing a select for only an `id` column with a limit of 1, and if
it gets a result then it returns true. Probably not the most efficient
but it is portable. Here's what `knex` it's doing:
```javascript
let res = await knex(this.table_name).select('id').where(where).limit(1).first();
```
+ `where Object` -- the query specification for `knex`.
+ `return boolean` -- Whether it exists or not.
*/
static async exists(where) {
let res = await knex(this.table_name).select('id').where(where).limit(1).first();
return res ? res.id : false;

Loading…
Cancel
Save