Comparing version 0.1.1 to 0.1.2
@@ -5,2 +5,9 @@ # Changelog | ||
## [0.1.2](https://gitlab.com/monstrous/monstrous/compare/v0.1.1...v0.1.2) (2023-02-19) | ||
### Bug Fixes | ||
* projection clones set #to_array ([4409912](https://gitlab.com/monstrous/monstrous/commit/4409912f37fcd557bb0268297a0ed6d7d94801d6)) | ||
## [0.1.1](https://gitlab.com/monstrous/monstrous/compare/v0.1.0...v0.1.1) (2023-02-17) | ||
@@ -7,0 +14,0 @@ |
@@ -205,3 +205,3 @@ import pgp from 'pg-promise'; | ||
clone() { | ||
const clone = new Projection(this.#key, this.#columns); | ||
const clone = new Projection(this.#key, this.#columns, this.#to_array); | ||
@@ -208,0 +208,0 @@ for (const [name, p] of Object.entries(this.#projections)) { |
{ | ||
"name": "monstrous", | ||
"version": "0.1.1", | ||
"version": "0.1.2", | ||
"description": "a lightweight SQL composer for Node.js and PostgreSQL", | ||
@@ -9,2 +9,3 @@ "main": "index.js", | ||
"snap": "c8 tap -R list --no-cov", | ||
"coverage": "c8 report --reporter=text-lcov | coveralls", | ||
"release": "commit-and-tag-version" | ||
@@ -74,2 +75,3 @@ }, | ||
"commit-and-tag-version": "^11.1.0", | ||
"coveralls": "^3.1.1", | ||
"tap": "^16.3.0" | ||
@@ -76,0 +78,0 @@ }, |
@@ -5,2 +5,4 @@ # monstrous | ||
[![Coverage Status](https://coveralls.io/repos/gitlab/monstrous/monstrous/badge.svg?branch=main)](https://coveralls.io/gitlab/monstrous/monstrous?branch=main) | ||
```javascript | ||
@@ -45,3 +47,3 @@ await db.select( | ||
## on composition and reuse | ||
## relations, composition, and reuse | ||
@@ -52,4 +54,6 @@ monstrous is additionally inspired by the Clojure library [Penkala](https://github.com/retro/penkala) and the ["Relations as First-Class Citizen" principle](https://try-alf.org/blog/2013-10-21-relations-as-first-class-citizen) elucidated in Ruby by alf/bmg (although, like Penkala, it sticks to SQL instead of implementing a full relational algebra). | ||
`db.select()` has one required argument: the bare minimum query is `db.select(db.libraries)`. What's inside those parentheses looks like a relation, has a `db.libraries.primary_key` and `qualified` columns, but in fact is -- when it matters -- a statement representing the retrieval of all records and all columns in `libraries`. All the detail of query _building_ happens on and to statements, with fluent functions like `join`, `filter`, `limit`, and `project`. Query builder functions can be used in any order and combination as long as references are preserved: you can't `filter` a column in `authors` before you `join(db.authors)` into your statement. | ||
`db.select()` has one required argument: the bare minimum query is `db.select(db.libraries)`. Inside those parentheses is a relation, with a `db.libraries.primary_key` and `qualified` columns, which can be transformed into (essentially) another relation, and another, and another, through fluent functions like `join`, `filter`, `limit`, and `project`. Each relation-builder function returns a _new_ relation -- intermediary relations can be used and built off of repeatedly. | ||
Relations' columns are dollar-prefixed properties: `db.libraries.$id`, `db.authors.$name`. Use these properties instead of strings -- while you can get away with un- or partially-qualified names much of the time, you also lose out on some of monstrous' automatic resolution on the fully-qualified and quoted name returned by `db.libraries.$id`, and risk ambiguous column names appearing in the final query. Keys as in criteria objects can be specified `{[db.libraries.$id]: 2}`. Spreading an object (`[...db.libraries]`) returns all columns, useful in projections. | ||
Fluent functions come in two flavors: | ||
@@ -61,3 +65,3 @@ | ||
- `join` will declare and attach a single new join relation | ||
- `join` will declare a single new join relation and its join conditions | ||
- `filter` will augment an existing or create a new `where` clause | ||
@@ -72,2 +76,4 @@ - `group` will append its field(s) to the `group by` clause | ||
Functions can be used in any order and combination as long as references are preserved: you can't `filter` a column in `authors` before you `join(db.authors)` into your statement. | ||
```javascript | ||
@@ -91,18 +97,18 @@ await db.select(db.libraries | ||
```javascript | ||
const books_near_me = db.libraries | ||
const ipsum = db.libraries | ||
.join(db.holdings) | ||
.join(db.books) | ||
.filter({[db.libraries.$postcode]: '12345'}); | ||
.join(db.authors) | ||
.filter({[`${db.authors.$name} ilike`]: 'lauren%ipsum'}); | ||
db.attach( // don't do this on an application hot path! | ||
books_near_me | ||
.join(db.authors) | ||
.filter({[`${db.authors.$name} ilike`]: 'lauren%ipsum'}) | ||
['saved_queries', 'ipsum'] | ||
// don't do this on an application hot path! | ||
db.attach(ipsum, ['saved_queries', 'ipsum']); | ||
// elsewhere.... | ||
const result = await db.select( | ||
db.saved_queries.ipsum.filter({[db.libraries.$postcode]: '12345'}) | ||
); | ||
await db.select(db.saved_queries.ipsum); | ||
``` | ||
Furthermore, other attached or ad-hoc queries can use `books_near_me` as a base without altering its future executions. Every fluent function returns an updated clone of its base statement. | ||
Furthermore, other attached or ad-hoc queries can use `ipsum` as a base without altering its future executions. Every fluent function returns an updated clone of its base statement. | ||
@@ -127,3 +133,6 @@ ## connect | ||
```javascript | ||
db.expr(`extract(years from justify_interval($1 - ${db.employees.hired_at}))`, [new Date()]) | ||
db.expr( | ||
`extract(years from justify_interval($1 - ${db.employees.hired_at}))`, | ||
[new Date()] | ||
).as('tenure') // alias only required in projection! | ||
``` | ||
@@ -170,3 +179,3 @@ | ||
The `on` criteria map the columns of the newly joined relation to, usually, fully qualified columns (exposed as dollar-prefixed properties) of any already-joined relation. There is no "handedness" to criteria, so the division into criteria keys and values is a matter of taste. Literals and `expr`s are also valid `on` criteria values only. | ||
The `on` criteria map the columns of the newly joined relation to, usually, fully qualified columns of any already-joined relation. There is no "handedness" to criteria, so the division into criteria keys and values is a matter of taste. Literals and `expr`s are also valid `on` criteria values only. | ||
@@ -191,2 +200,4 @@ Results are automatically projected as `new_relname: [record object, record object]` to the most recently joined relation referenced in the `on`, but you can also override with your own projection. | ||
Spreading a relation with `...` yields all columns: | ||
```javascript | ||
@@ -230,3 +241,3 @@ [...db.employees, db.libraries.$postcode] // add library postcode to all employee fields | ||
Libraries and patrons also use the `$columns` field to specify fields without renaming them. `$columns` is processed similarly to the array form of the projection. In the example, the `director` instead specifies only a subset of fields one by one, and `tenure` is a raw SQL expression which will be interpolated into the query. As always, raw SQL is a potential vector for SQL injection; use it carefully! | ||
`libraries` and `patrons` also use the `$columns` field to specify fields without renaming them. `$columns` is processed similarly to the array form of the projection. In the example, the `director` instead specifies only a subset of fields one by one, and `tenure` is a raw SQL expression which will be interpolated into the query. As always, raw SQL is a potential vector for SQL injection; use it carefully! | ||
@@ -432,2 +443,3 @@ `db.expr`s may be included in a `$columns` array, but must be aliased. In other words, the `director` could be constructed thus: | ||
- ad-hoc CTEs? | ||
- lateral join exprs? | ||
- set operations (`union [all]`, `intersect`, `except`) | ||
@@ -434,0 +446,0 @@ - wrapped tasks with connection settings? |
84688
447
5