You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

192 lines
7.8 KiB
Markdown

10 months ago
# dlayer-knex
Small utility for adding database support to your dlayer schema, using Knex, with minimum viable functionality that doesn't get in your way.
- Currently supports SQLite and PostgreSQL only (as this library needs to do a level of introspection that Knex does not have implementations for)
- Deliberately very limited featureset and API surface; you are expected to manually specify *complex* queries in your graph (and dlayer's extension system makes this possible)
- Current status: __beta__. Should work fine, but if it breaks, you get to keep both halves.
- Documentation may be incomplete.
## Realistic-ish example
```js
const knex = require("knex");
const dlayer = require("dlayer");
const dlayerKnex = require("dlayer-knex");
10 months ago
let db = knex(require("./knexfile").development);
let knexUsers = await dlayerKnex.generateModule(db, "users");
let knexThreads = await dlayerKnex.generateModule(db, "threads", {
user_id: [ "user", "threads" ]
});
let knexPosts = await dlayerKnex.generateModule(db, "posts", {
user_id: [ "user", "posts" ],
thread_id: [ "thread", "posts" ]
});
let api = dlayer({
makeContext: () => ({}),
modules: [
knexUsers.module,
knexPosts.module,
knexThreads.module
],
schema: {
users: knexUsers.rootQueries,
threads: knexThreads.rootQueries,
posts: knexPosts.rootQueries
}
});
let { moreThan, lessThan, not } = dlayerKnex;
let testResult = await api.query({
users: {
list: {
$arguments: {
filter: { id: not(lessThan(2)) }
},
id: true,
username: true,
threads: {
id: true,
subject: true,
user: {
id: true,
username: true
}
}
}
}
});
/*
testResult = {
users: {
list: [{
id: 2,
username: 'joepie91',
threads: [{
id: 2,
subject: 'Test 2',
user: {
id: 2,
username: 'joepie91'
}
}, {
id: 3,
subject: 'Test 3',
user: {
id: 2,
username: 'joepie91'
}
}]
}, {
id: 3,
username: 'bar',
threads: [{
id: 1,
subject: 'Test 1',
user: {
id: 3,
username: 'bar'
}
}]
}, {
id: 4,
username: null,
threads: []
}]
}
}
*/
```
## API
### generateModule(knexInstance, tableName, relations)
Relations are always defined on the table which stores the referenced ID in a column; the inverse relationship will be automatically created on the foreign table.
The foreign key already needs to be set up in the database schema - `dlayer-knex` reads out the table schema to set up the relations! The values you specify here are only to indicate on which fields of the `dlayer-knex`-generated objects you want the relations to be available.
So assuming that in the schema, `threads.user_id` points to `users.id`, then with this definition:
```js
let knexThreads = await dlayerKnex.generateModule(db, "threads", {
user_id: [ "user", "threads" ]
});
```
... that means that the following fields are created:
- A `user` field on any `threads` objects created by `dlayer-knex`, referencing the corresponding object from `users` where `users.id = threads.user_id`
- A `threads` field on any `users` objects created by `dlayer-knex`, with a list of all records from the `threads` table that have `threads.user_id` set to the `users` object's `id`
(This is equivalent to a set of `belongsTo` and `hasMany` relations in many ORMs, you just only specify the destination field names in `dlayer-knex`'s config.)
Through-relations, ie. using many-to-many tables, are not *explicitly* supported in `dlayer-knex`, but they don't need to be; you can simply treat the intermediate table as its own type with its own definition and relations, and do two steps of relation-resolving in your `dlayer` query.
For example, if you have a `communities` and `users` table, then the many-to-many table that links them together might be called `memberships`, and you might end up with a query like `users[0].memberships.community` to obtain that user's communities. (This is pseudocode, dlayer does not currently use that sort of syntax, but you hopefully get the idea!)
### lessThan(value), moreThan(value)
Pretty much what it says on the tin; can be used in query arguments in place of exact values.
### anyOf(arrayOfValues)
Can be used in query arguments in place of exact values. Will filter for the value being *one of* the specified values, rather than just a single one. Cannot currently be wrapped around `lessThan`, `moreThan`, or `not` - that might be added in the future, if people have a good reason for it.
### not(value)
Negates the match. Can be wrapped around a `lessThan`/`moreThan`/`anyOf` wrapper, but not around *another* `not` wrapper. Can also be used with exact values.
## dlayer module schema
The `generateModule` method generates a dlayer module and a number of root queries, that you can use in your dlayer schema. The module defines the following things:
- A type representing records from the specified table; it will be named `dlayer-knex.TABLE_NAME`, where `TABLE_NAME` is the name of the table in the database. This is the name you should use when extending the type, eg. to add complex queries.
- Extensions for any other `dlayer-knex` types, that are necessary to generate inverse relations. You don't generally have to care about these.
- A `dlayerKnexTable` value in the module's context, which is a DataLoader that fetches items from the table by its primary key. You don't generally have to care about this, *unless* you need to work around an API limitation; in that case, you can use dlayer's `$getModuleContext` utility function to access it, but keep in mind that this context value is not included in the semver guarantees for this library, and may break between releases.
Separately, a set of root queries is also generated, which you can insert into your root schema where it fits your usecase. Often this will be in a top-level property with the same name as the table, but it doesn't *need* to be there. These root queries are:
### list
Equivalent to a `SELECT` query, retrieves existing records from a table. Possible arguments:
- __filter:__ an object of predicates to match records against, optionally using `moreThan`/`lessThan`/`anyOf`/`not`.
- __orderBy:__ a column/field name to order the results by; ascending by default, prefix it with a `-` to sort descendingly.
- __skip:__ the amount of records to skip at the start; also known as an 'offset' or 'start'.
- __limit:__ the amount of records to retrieve, counting from the first non-skipped record.
- __first:__ when set to `true`, only return the first result (or `undefined`), rather than a list of results.
This query produces a list of records according to your criteria (or a single one, if `first` has been used).
### delete
Equivalent to a `DELETE` query, deletes records from a table. Possible arguments:
- __filter:__ an object of predicates to match records against, same as for `list`. Only these records will be deleted.
Produces an object `{ count: Number }` to tell you how many records were deleted.
### change
Equivalent to an `UPDATE` query, changes existing records in a table, based on some predicate. Possible arguments:
- __filter:__ an object of predicates to match records against, same as for `list`. Only these records will be changed.
- __values:__ an object of new values to set for the matched records. Relation fields are ignored; you need to set their underlying ID references instead. Only literal values can be specified here.
This query produces a list of the matched records, *after* having been updated with the specified new values.
### create
Equivalent to an `INSERT` query, creates new records in a table. Possible arguments:
- __values:__ an array of objects, that should be created in the table. Here, again, relation fields are ignored, and you need to set their underlying ID references. Only literal values accepted.
Produces a list of the newly created records, *after* having been inserted into the database; ie. including their new automatically-assigned ID.