parent
d9d46bc95d
commit
c96e2dd5e2
@ -0,0 +1,20 @@ |
|||||||
|
/node_modules/ |
||||||
|
.*.sw* |
||||||
|
.DS_Store |
||||||
|
*.sqlite3 |
||||||
|
*.sqlite3-wal |
||||||
|
*.sqlite3-shm |
||||||
|
debug/ |
||||||
|
secrets/* |
||||||
|
coverage/ |
||||||
|
.coverage |
||||||
|
media |
||||||
|
tests/fixtures |
||||||
|
rendered/wip |
||||||
|
junk/ |
||||||
|
__pycache__ |
||||||
|
.ipynb_checkpoints |
||||||
|
data_dump |
||||||
|
book |
||||||
|
server_configs |
||||||
|
notes |
@ -0,0 +1,43 @@ |
|||||||
|
import Fastify from "fastify"; |
||||||
|
import FastifyStatic from "@fastify/static"; |
||||||
|
import path from "path"; |
||||||
|
import { ToDo } from "../lib/models.js"; |
||||||
|
import fs from "fs"; |
||||||
|
import _ from "lodash"; |
||||||
|
|
||||||
|
const fastify = Fastify({ |
||||||
|
logger: true |
||||||
|
}); |
||||||
|
|
||||||
|
fastify.get("/todo", async (request, reply) => { |
||||||
|
const list = await ToDo.all({}); |
||||||
|
|
||||||
|
return list; |
||||||
|
}); |
||||||
|
|
||||||
|
fastify.get("/todo.html", async (req, rep) => { |
||||||
|
const list = await ToDo.all({}); |
||||||
|
|
||||||
|
const tmpl_src = fs.readFileSync("templates/todo.html"); |
||||||
|
const tmpl = _.template(tmpl_src, ); |
||||||
|
|
||||||
|
const result = tmpl({message: "Hello!"}); |
||||||
|
|
||||||
|
rep.code(200) |
||||||
|
.type("text/html") |
||||||
|
.send(result); |
||||||
|
}); |
||||||
|
|
||||||
|
fastify.register(FastifyStatic, { |
||||||
|
root: path.join(path.resolve("."), 'static'), |
||||||
|
prefix: '/', // optional: default '/'
|
||||||
|
constraints: {}, // optional: default {}
|
||||||
|
index: "index.html" |
||||||
|
}) |
||||||
|
|
||||||
|
try { |
||||||
|
await fastify.listen({port: 3000}); |
||||||
|
} catch(err) { |
||||||
|
fastify.log.error(err); |
||||||
|
process.exit(1); |
||||||
|
} |
@ -0,0 +1,45 @@ |
|||||||
|
// Update with your config settings. |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
|
||||||
|
development: { |
||||||
|
client: 'sqlite3', |
||||||
|
useNullAsDefault: true, |
||||||
|
connection: { |
||||||
|
filename: './dev.sqlite3' |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
staging: { |
||||||
|
client: 'postgresql', |
||||||
|
connection: { |
||||||
|
database: 'my_db', |
||||||
|
user: 'username', |
||||||
|
password: 'password' |
||||||
|
}, |
||||||
|
pool: { |
||||||
|
min: 2, |
||||||
|
max: 10 |
||||||
|
}, |
||||||
|
migrations: { |
||||||
|
tableName: 'knex_migrations' |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
production: { |
||||||
|
client: 'postgresql', |
||||||
|
connection: { |
||||||
|
database: 'my_db', |
||||||
|
user: 'username', |
||||||
|
password: 'password' |
||||||
|
}, |
||||||
|
pool: { |
||||||
|
min: 2, |
||||||
|
max: 10 |
||||||
|
}, |
||||||
|
migrations: { |
||||||
|
tableName: 'knex_migrations' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
import { knex, Model } from './ormish.js'; |
||||||
|
|
||||||
|
export class ToDo extends Model.from_table("todo") { |
||||||
|
|
||||||
|
} |
@ -0,0 +1,622 @@ |
|||||||
|
/* |
||||||
|
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'; |
||||||
|
import assert from 'assert'; |
||||||
|
import { attachPaginate } from 'knex-paginate'; |
||||||
|
|
||||||
|
/* |
||||||
|
A preconfigured knex.js driver using the config.development configuration |
||||||
|
by default. |
||||||
|
|
||||||
|
___TODO___: Need to make this configurable, even though I just use one config right now since I run sqlite3 all the time. |
||||||
|
*/ |
||||||
|
export const knex = knexConfig(config.development); |
||||||
|
|
||||||
|
/* |
||||||
|
run the PERF_TRICKS to configure sqlite3 when thing start, really need to make this |
||||||
|
a configuration and only do it with sqlite3, but for now just get this done |
||||||
|
*/ |
||||||
|
if(config.development.client === "sqlite3") { |
||||||
|
const PERF_TRICKS = [ |
||||||
|
"pragma journal_mode = WAL", // use a WAL journal to not block writers/readers
|
||||||
|
"pragma synchronous = normal", // set concurrency to normal now that we have WAL
|
||||||
|
// "pragma temp_store = memory", // use RAM to make the temporary indices
|
||||||
|
// "pragma mmap_size = 30000000000", // use 30GB of mmap to store DB, not useful in multi-process settings
|
||||||
|
// "pragma page_size = 32768", // improve performance only if you store large stuff
|
||||||
|
"pragma vacuum", // give it a vacuum, this could impact startup time significantly
|
||||||
|
"pragma optimize", // optimize it, but should be done periodically
|
||||||
|
]; |
||||||
|
|
||||||
|
for(let sql of PERF_TRICKS) { |
||||||
|
await knex.raw(sql); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
attachPaginate(); |
||||||
|
|
||||||
|
/* |
||||||
|
Filled in by `load_schema` to give access to the database scheme in the admin |
||||||
|
tool and generally through the API. |
||||||
|
*/ |
||||||
|
export const SCHEMA = {}; |
||||||
|
|
||||||
|
const load_schema = async () => { |
||||||
|
const raw = await knex("sqlite_schema").where({"type": "table"}).select(['type', 'name']); |
||||||
|
for(let table of raw) { |
||||||
|
table._columns = await knex(table.name).columnInfo(); |
||||||
|
SCHEMA[table.name] = table; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
await load_schema(); |
||||||
|
|
||||||
|
/* |
||||||
|
In some cases (like the generic admin) you need to get validation |
||||||
|
rules but you don't have a specific class to work with. This function |
||||||
|
is called by Model.validation and you can call it directly to get rules |
||||||
|
for a database table. |
||||||
|
|
||||||
|
|
||||||
|
___BUG___: validator doesn't do proper date formatting yet, so it won't support date types. |
||||||
|
|
||||||
|
1. `name string` - the table name. |
||||||
|
2. `rules Object` - default rules with empty "" for the rules you want filled in |
||||||
|
3. `all boolean` - set this to true if you want everything |
||||||
|
4. `no_id boolean` - defaults to true, set false if you also want the id |
||||||
|
5. `return Object` - the resulting rules to use with Validator |
||||||
|
*/ |
||||||
|
export const validation = (name, rules, all=false, no_id=true) => { |
||||||
|
assert(rules, "rules parameter is required and will be modified"); |
||||||
|
const schema = SCHEMA[name]; |
||||||
|
assert(schema, `There is no schema for table named ${name}. Did you forget to migrate:latest?`); |
||||||
|
|
||||||
|
for(let [key, opts] of Object.entries(schema._columns)) { |
||||||
|
// most of the time you don't want the id
|
||||||
|
if(no_id && key === "id") continue; |
||||||
|
|
||||||
|
if(all || rules[key] === "") { |
||||||
|
let required = opts.nullable || opts.defaultValue ? "nullable|" : "required|"; |
||||||
|
|
||||||
|
switch(opts.type) { |
||||||
|
case "varchar": // fallthrough
|
||||||
|
case "text": |
||||||
|
// some databases have an email type but we'll also look for ones named email
|
||||||
|
if(key === "email") { |
||||||
|
rules[key] = required + "email"; |
||||||
|
} else { |
||||||
|
rules[key] = required + "string"; |
||||||
|
} |
||||||
|
|
||||||
|
if(opts.maxLength) { |
||||||
|
rules[key] += `|min:0|max:${opts.maxLength}`; |
||||||
|
} |
||||||
|
break; |
||||||
|
case "boolean": |
||||||
|
rules[key] = required + "boolean"; |
||||||
|
break; |
||||||
|
case "integer": |
||||||
|
case "float": |
||||||
|
rules[key] = required + "numeric"; |
||||||
|
break; |
||||||
|
case "date": |
||||||
|
// rules[key] = required + "date";
|
||||||
|
break; |
||||||
|
case "email": |
||||||
|
// not all databases have this
|
||||||
|
rules[key] = required + "email"; |
||||||
|
break; |
||||||
|
default: |
||||||
|
rules[key] = required; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return rules; |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
The base class for all models found in `lib/models.js`. You use this by extending it with: |
||||||
|
|
||||||
|
```javascript
|
||||||
|
class User extends Model.from_table('user') { |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
This will create a `User` class that is automatically configured using the SCHEMA create from the `user` table in your database. You won't need to define the attributes on this class as it will be correctly populated from the database. |
||||||
|
|
||||||
|
The database is therefore the "source of truth" for all of the models. You can then add functions to extend what this class does. |
||||||
|
*/ |
||||||
|
export class Model { |
||||||
|
|
||||||
|
/* |
||||||
|
Allows you to build a new object of this Model with the given `attr` |
||||||
|
already set, but really you should use the `Model.from(attr)` method instead. |
||||||
|
This does _no_ object sanitization with `Model.clean(attr)` method, and if |
||||||
|
it doesn't match the underlying database it will throw an exception. |
||||||
|
|
||||||
|
- `attr Object` - the attributes for this model |
||||||
|
*/ |
||||||
|
constructor(attr) { |
||||||
|
assert(attr, "Must give attributes."); |
||||||
|
Object.assign(this, attr); |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
How to actually create a new instance of this model. This |
||||||
|
will do two things: |
||||||
|
|
||||||
|
1. Correctly use the schema for the subclass model. |
||||||
|
2. Sanitize the input to remove anything that shouldn't be in the database. |
||||||
|
|
||||||
|
The `also_remove` parameter is a list of additional keys to also scrub from the object. |
||||||
|
|
||||||
|
- `attr Object` -- The attributes this should start with. |
||||||
|
- `also_remove Array` -- list of additional attributes to remove. |
||||||
|
*/ |
||||||
|
static from(attr, also_remove=undefined) { |
||||||
|
return new this(this.clean(attr, also_remove)); |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
Returns an object representing the schema for this Model. Remember that this |
||||||
|
will reflect what's in the database schema, which is formatted however |
||||||
|
`knex.js` formats your database Schema. Might not be portable between |
||||||
|
databases and only tested with SQlite3. |
||||||
|
|
||||||
|
_This is an attribute accessor, so just do `obj.schema` rather than call it like a function._ |
||||||
|
|
||||||
|
- `return Object` - The schema for this model. |
||||||
|
*/ |
||||||
|
get schema() { |
||||||
|
return this.constructor.schema; |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
Uses the `this.schema` scrub out any attributes that are not valid for the |
||||||
|
schema. This is effectively a whitelist for the allowed attributes based on |
||||||
|
the database schema. You can use the `also_remove` parameter to list |
||||||
|
additional attributes to remove, which you should do to sanitize incoming |
||||||
|
objects for things like password fields. |
||||||
|
|
||||||
|
- `attr Object` - The attributes to clean. |
||||||
|
- `also_remove Array` - Additional attributes to remove. |
||||||
|
*/ |
||||||
|
static clean(attr, also_remove=undefined) { |
||||||
|
assert(attr, "Must give attributes to clean."); |
||||||
|
|
||||||
|
let clean_entries = Object.entries(attr) |
||||||
|
.filter(([k,v]) => k in this.schema); |
||||||
|
|
||||||
|
if(also_remove) also_remove.forEach(k => delete clean_entries[k]); |
||||||
|
|
||||||
|
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` |
||||||
|
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 "" |
||||||
|
will be filled in with a minimum rule to match the database schema. |
||||||
|
|
||||||
|
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. It actually just |
||||||
|
calls the `validation` module function with `validation(this.table_name, rules)`. |
||||||
|
|
||||||
|
- `param rules {Object}` - rules specifier |
||||||
|
*/ |
||||||
|
static validation(rules) { |
||||||
|
return validation(this.table_name, rules); |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
Delete this object from the database. We use the word "destroy" because |
||||||
|
`delete` is reserved. It uses this object's `.id` to determine which object |
||||||
|
to delete and uses this `knex` code: |
||||||
|
|
||||||
|
```javascript
|
||||||
|
await knex(obj.table_name).where({id: obj.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. |
||||||
|
|
||||||
|
+ `obj Model` -- the object to destroy, just have id |
||||||
|
*/ |
||||||
|
static async destroy(obj) { |
||||||
|
assert(this.table_name !== undefined, "You must set class variable table_name."); |
||||||
|
assert(obj.id !== undefined, "No id in object to destroy."); |
||||||
|
|
||||||
|
await knex(this.table_name). |
||||||
|
where({id: obj.id}). |
||||||
|
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. |
||||||
|
+ ___return___ `Model` subclass found, or `undefined` if nothing. |
||||||
|
*/ |
||||||
|
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", |
||||||
|
knex(through_table).select(`${model.table_name}_id as id`).where(`${this.table_name}_id`, "=", this.id)); |
||||||
|
|
||||||
|
let rows = await query; |
||||||
|
|
||||||
|
let results = []; |
||||||
|
|
||||||
|
for(let r of rows) { |
||||||
|
results.push(new model(await model.clean(r))); |
||||||
|
} |
||||||
|
|
||||||
|
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 |
||||||
|
+ ___return___ `number` |
||||||
|
*/ |
||||||
|
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:
|
||||||
|
const spec = { count: columns || ['id']}; |
||||||
|
let res = await knex(this.table_name).where(where).count(spec); |
||||||
|
if(res.length == 1) { |
||||||
|
// single result, just give the count
|
||||||
|
return res[0].count; |
||||||
|
} else { |
||||||
|
console.warn("Your call to count in", this.table_name, "using where=", where, "columns: ", columns, "returned a weird result:", res); |
||||||
|
return res; // weird result let them deal with it
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
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}`); |
||||||
|
|
||||||
|
let res = await knex(this.table_name).insert(attr).returning(['id']); |
||||||
|
assert(res, `Failed to get an id from the insert for ${this.table_name}`); |
||||||
|
|
||||||
|
attr.id = res[0].id; |
||||||
|
|
||||||
|
return new this(attr); |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
Implements an upsert (insert but update on conflict) for Postgres, MySQL, and SQLite3 only. |
||||||
|
|
||||||
|
___TODO___: allow specifying returns for databases that support it |
||||||
|
|
||||||
|
+ attr `Object` - The attributes to insert or update. |
||||||
|
+ conflict_key `string` - The key that can cause a conflict then update. |
||||||
|
+ merge `boolean` - Defaults to true and will change the record. false will ignore and not update on conflict. |
||||||
|
+ ___return___ `number` - id or undefined if ignored, no update |
||||||
|
*/ |
||||||
|
static async upsert(attr, conflict_key, merge=true) { |
||||||
|
assert(conflict_key !== undefined, `You forgot to set the conflict_key on upsert to table ${this.table_name}`); |
||||||
|
let result = undefined; |
||||||
|
|
||||||
|
if(merge) { |
||||||
|
result = await knex(this.table_name) |
||||||
|
.insert(attr) |
||||||
|
.returning(['id']) |
||||||
|
.onConflict(conflict_key) |
||||||
|
.merge(); |
||||||
|
} else { |
||||||
|
result = await knex(this.table_name).insert(attr).returning(['id']).onConflict(conflict_key).ignore(); |
||||||
|
} |
||||||
|
|
||||||
|
// BUG: is returning undefined for ignored and no result right?
|
||||||
|
if(result == undefined || result.length == 0) { |
||||||
|
return undefined; |
||||||
|
} else { |
||||||
|
attr.id = result[0].id; |
||||||
|
return attr; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
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. |
||||||
|
|
||||||
|
+ `where Object` -- The knex where options for a query. |
||||||
|
+ `what Model` -- The table/Model to use as the update. |
||||||
|
+ ___return___ `number` -- _WARNING_: I believe this returns the count updated but that might be database specific. |
||||||
|
*/ |
||||||
|
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; |
||||||
|
|
||||||
|
if(columns) { |
||||||
|
attr = await knex(this.table_name).column(columns).first().where(where); |
||||||
|
} else { |
||||||
|
attr = await knex(this.table_name).first().where(where); |
||||||
|
} |
||||||
|
|
||||||
|
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. |
||||||
|
+ ___return___ `number` -- Number deleted, but that might be database specific. |
||||||
|
*/ |
||||||
|
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. |
||||||
|
+ ___return___ `Array` of `Model` subclass. |
||||||
|
*/ |
||||||
|
static async all(where, columns) { |
||||||
|
assert(where, "You must give a where options."); |
||||||
|
let results = []; |
||||||
|
|
||||||
|
if(columns) { |
||||||
|
results = await knex(this.table_name).column(columns).where(where).select(); |
||||||
|
} else { |
||||||
|
results = await knex(this.table_name).where(where); |
||||||
|
} |
||||||
|
|
||||||
|
let final = results.map(r => new this(r)); |
||||||
|
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; |
||||||
|
|
||||||
|
assert(SCHEMA, "schema is not loaded!"); |
||||||
|
assert(SCHEMA[table_name], `table named ${table_name} not in SCHEMA: ${ Object.keys(SCHEMA) }`); |
||||||
|
|
||||||
|
m.schema = SCHEMA[table_name]._columns; |
||||||
|
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; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default { knex, SCHEMA, Model, validation }; |
@ -0,0 +1,14 @@ |
|||||||
|
|
||||||
|
exports.up = async (knex) => { |
||||||
|
await knex.raw('PRAGMA journal_mode=WAL;'); |
||||||
|
|
||||||
|
await knex.schema.createTable('todo', (table) => { |
||||||
|
table.timestamps(true, true); |
||||||
|
table.increments('id'); |
||||||
|
table.string("task").notNullable(); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
exports.down = async (knex) => { |
||||||
|
await knex.schema.dropTable("todo"); |
||||||
|
}; |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,19 @@ |
|||||||
|
{ |
||||||
|
"name": "bandolier2", |
||||||
|
"version": "0.1.0", |
||||||
|
"private": true, |
||||||
|
"type": "module", |
||||||
|
"bin": { |
||||||
|
"bando": "./bando.js" |
||||||
|
}, |
||||||
|
"scripts": {}, |
||||||
|
"dependencies": { |
||||||
|
"@fastify/static": "^7.0.0", |
||||||
|
"commander": "^12.0.0", |
||||||
|
"fastify": "^4.26.0", |
||||||
|
"knex": "^3.1.0", |
||||||
|
"knex-paginate": "^3.1.1", |
||||||
|
"lodash": "^4.17.21", |
||||||
|
"sqlite3": "^5.1.7" |
||||||
|
} |
||||||
|
} |
File diff suppressed because one or more lines are too long
@ -0,0 +1,37 @@ |
|||||||
|
<!doctype html> |
||||||
|
|
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<script src="/alpine.js"></script> |
||||||
|
<title>Bandolier2</title> |
||||||
|
|
||||||
|
<style> |
||||||
|
@import "/open-props.min.css"; |
||||||
|
|
||||||
|
blockquote { |
||||||
|
--_accent-1: var(--lime-5); |
||||||
|
--_accent-2: var(--lime-4); |
||||||
|
--_bg: var(--surface-2); |
||||||
|
--_ink: var(--text-1); |
||||||
|
|
||||||
|
color: var(--_ink); |
||||||
|
border-color: var(--_accent-2); |
||||||
|
background-color: var(--_bg); |
||||||
|
justify-self: flex-start; |
||||||
|
} |
||||||
|
</style> |
||||||
|
<head> |
||||||
|
<body> |
||||||
|
<h1>Test!</h1> |
||||||
|
<div x-data="{ open: false }"> |
||||||
|
<button @click="open = !open">Expand</button> |
||||||
|
<span x-show="open"> |
||||||
|
Content... |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<blockquote> |
||||||
|
One day I will learn how to run without falling. |
||||||
|
</blockquote> |
||||||
|
</body> |
||||||
|
</html> |
File diff suppressed because one or more lines are too long
@ -0,0 +1,29 @@ |
|||||||
|
<!doctype html> |
||||||
|
|
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<script src="/alpine.js"></script> |
||||||
|
<title>Bandolier2</title> |
||||||
|
|
||||||
|
<style> |
||||||
|
@import "/open-props.min.css"; |
||||||
|
|
||||||
|
blockquote { |
||||||
|
--_accent-1: var(--lime-5); |
||||||
|
--_accent-2: var(--lime-4); |
||||||
|
--_bg: var(--surface-2); |
||||||
|
--_ink: var(--text-1); |
||||||
|
|
||||||
|
color: var(--_ink); |
||||||
|
border-color: var(--_accent-2); |
||||||
|
background-color: var(--_bg); |
||||||
|
justify-self: flex-start; |
||||||
|
} |
||||||
|
</style> |
||||||
|
<head> |
||||||
|
<body> |
||||||
|
<h1><%= message %></h1> |
||||||
|
|
||||||
|
<p>This is a template!</p> |
||||||
|
</body> |
||||||
|
</html> |
Loading…
Reference in new issue