|
|
@ -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 config from '../knexfile.cjs'; |
|
|
|
import knexConfig from 'knex'; |
|
|
|
import knexConfig from 'knex'; |
|
|
@ -192,12 +207,24 @@ export class Model { |
|
|
|
return Object.fromEntries(clean_entries); |
|
|
|
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() { |
|
|
|
get table_name() { |
|
|
|
return this.constructor.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 |
|
|
|
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 |
|
|
|
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 "" |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
- `param rules {Object}` - rules specifier |
|
|
|
*/ |
|
|
|
*/ |
|
|
@ -213,6 +241,18 @@ export class Model { |
|
|
|
return validation(this.table_name, rules); |
|
|
|
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() { |
|
|
|
async destroy() { |
|
|
|
assert(this.table_name !== undefined, "You must set class variable table_name."); |
|
|
|
assert(this.table_name !== undefined, "You must set class variable table_name."); |
|
|
|
|
|
|
|
|
|
|
@ -221,15 +261,107 @@ export class Model { |
|
|
|
del(); |
|
|
|
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) { |
|
|
|
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)}`); |
|
|
|
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); |
|
|
|
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) { |
|
|
|
async has_many(model, where, columns) { |
|
|
|
return await model.all(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) { |
|
|
|
async many_to_many(model, through_table) { |
|
|
|
// SECURITY: doing string interpolation which might allow injecting SQL
|
|
|
|
// SECURITY: doing string interpolation which might allow injecting SQL
|
|
|
|
let query = knex(model.table_name).where("id", "in", |
|
|
|
let query = knex(model.table_name).where("id", "in", |
|
|
@ -246,6 +378,15 @@ export class Model { |
|
|
|
return results; |
|
|
|
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) { |
|
|
|
static async count(where, columns) { |
|
|
|
// the knex count api returns a DB specific result, so we need
|
|
|
|
// the knex count api returns a DB specific result, so we need
|
|
|
|
// to specify what we want, which is count:
|
|
|
|
// 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) { |
|
|
|
static async insert(attr) { |
|
|
|
assert(this.table_name !== undefined, "You must set class variable table_name."); |
|
|
|
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}`); |
|
|
|
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; |
|
|
|
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) { |
|
|
|
static async update(where, what) { |
|
|
|
assert(where, "You must give a where options."); |
|
|
|
assert(where, "You must give a where options."); |
|
|
|
return knex(this.table_name).where(where).update(what); |
|
|
|
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) { |
|
|
|
static async first(where, columns) { |
|
|
|
assert(where, "You must give a where options."); |
|
|
|
assert(where, "You must give a where options."); |
|
|
|
let attr = undefined; |
|
|
|
let attr = undefined; |
|
|
@ -313,11 +497,37 @@ export class Model { |
|
|
|
return attr !== undefined ? new this(attr) : attr; |
|
|
|
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) { |
|
|
|
static async delete(where) { |
|
|
|
assert(where, "You must give a where options."); |
|
|
|
assert(where, "You must give a where options."); |
|
|
|
return knex(this.table_name).where(where).del(); |
|
|
|
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) { |
|
|
|
static async all(where, columns) { |
|
|
|
assert(where, "You must give a where options."); |
|
|
|
assert(where, "You must give a where options."); |
|
|
|
let results = []; |
|
|
|
let results = []; |
|
|
@ -332,6 +542,29 @@ export class Model { |
|
|
|
return final; |
|
|
|
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) { |
|
|
|
static from_table(table_name) { |
|
|
|
let m = class extends Model { }; |
|
|
|
let m = class extends Model { }; |
|
|
|
m.table_name = table_name; |
|
|
|
m.table_name = table_name; |
|
|
@ -343,6 +576,19 @@ export class Model { |
|
|
|
return m; |
|
|
|
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) { |
|
|
|
static async exists(where) { |
|
|
|
let res = await knex(this.table_name).select('id').where(where).limit(1).first(); |
|
|
|
let res = await knex(this.table_name).select('id').where(where).limit(1).first(); |
|
|
|
return res ? res.id : false; |
|
|
|
return res ? res.id : false; |
|
|
|