murmuration 2.0.1 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -67
- package/license.txt +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ This readme file pertains to both, although there are specific instructions for
|
|
|
11
11
|
|
|
12
12
|
Murmuration can be used as alternative to a database [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping). It is deliberately simple and low level in the sense that it provides no more than the bare minimum functionality needed to connect to a database and generate statements to be executed ideally in the context of transactions.
|
|
13
13
|
|
|
14
|
-
There is also some adjunct migration functionality that may or may not suit your use case. If used as prescribed then it guarantees that an application remains in line with its database, as the latter
|
|
14
|
+
There is also some adjunct migration functionality that may or may not suit your use case. If used as prescribed then it guarantees that an application remains in line with its database, as the latter can be migrated if needed each time the former is deployed.
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
@@ -31,13 +31,16 @@ Remember that it is the aforementioned specific packages that you should install
|
|
|
31
31
|
|
|
32
32
|
## Usage
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
Statements are covered first up, but ideally they should be executed in the context of transactions and therefore the remainder of this section cannot be overlooked. In particular, the example statements that follow are executed within operations, which are covered in the later subsection on transactions.
|
|
34
|
+
Statements are covered first up, but ideally they should be executed in the context of transactions and therefore the remainder of this section cannot be overlooked. In particular, the example statements that follow are executed inside operations, which are themselves covered in the later subsection. If you do not want to use operations then you can in fact easily wrap statements in promises, again see the relevant subsection later on.
|
|
37
35
|
|
|
38
36
|
### Generating statements
|
|
39
37
|
|
|
40
|
-
Statements are generated dynamically with a simple, promise-like syntax.
|
|
38
|
+
Statements are generated dynamically with a simple, promise-like syntax. Again it is worth noting that they can be executed inside either operations or promises. In the examples below operations are used. Wrapping statement in promises is covered later on.
|
|
39
|
+
|
|
40
|
+
Note that the following points apply to all of the examples:
|
|
41
|
+
|
|
42
|
+
* A local `using()` function has been employed rather the function supplied by the package, because the `Statement` class it utilities has been extended. See the relevant subsection below for more details.
|
|
43
|
+
* Each operation provides a connection and a context as well as the `abort`, `proceed` and `compoete` callbacks. These allow the result of the statement's execution to determine whether the application should abort, proceed to the next operation or complete, respectively. Again see the relevant section on operations later on.
|
|
41
44
|
|
|
42
45
|
In the first example a row is selected should its email address and password match the given values:
|
|
43
46
|
|
|
@@ -69,14 +72,12 @@ function checkAccountOperation(connection, abort, proceed, complete, context) {
|
|
|
69
72
|
|
|
70
73
|
There are several points worth noting:
|
|
71
74
|
|
|
72
|
-
* A local `using()` function has been employed rather the function supplied by the package, because the `Statement` class it utilities has been extended for convenience. For example, `selectFromAccount()` is defined in the application's own `Statement` class.
|
|
73
|
-
* Each operation provides a connection and a context as well as three callbacks. These allow the result of the execution to determine whether the application should proceed to the next operation or if the transaction should be aborted or completed.
|
|
74
75
|
* The `where()` method takes a plain old JavaScript object.
|
|
75
76
|
* The `one()` method takes a handler that is called if one row is returned.
|
|
76
77
|
* The `else()` method takes a handler that is called in all other cases.
|
|
77
78
|
* The `catch()` method takes a handler that is called should execution fail.
|
|
78
79
|
|
|
79
|
-
In the following example rows in a table holding temporary reset codes are deleted once they expire
|
|
80
|
+
In the following example rows in a table holding temporary reset codes are deleted once they expire:
|
|
80
81
|
|
|
81
82
|
```
|
|
82
83
|
const using = require("../using");
|
|
@@ -101,7 +102,7 @@ function deleteExpiredResetCodesOperation(connection, abort, proceed, complete,
|
|
|
101
102
|
|
|
102
103
|
The following points are worth noting:
|
|
103
104
|
|
|
104
|
-
* The `where()` method takes a template literal this time.
|
|
105
|
+
* The `where()` method takes a template literal this time, see below for further details.
|
|
105
106
|
* The `success()` method takes a callback that is called should hte execution succeed.
|
|
106
107
|
|
|
107
108
|
In this example a row is inserted into a table for software packages:
|
|
@@ -149,30 +150,108 @@ Note:
|
|
|
149
150
|
|
|
150
151
|
In general, the assumption with passing plain old JavaScript objects is that clauses and so forth can easily be constructed from them. The `set()`, `where()` and `values()` methods can also take appended template literals, however, so that you can define parts of the SQL with more freedom. More detail is given towards the end of the next subsection.
|
|
151
152
|
|
|
152
|
-
|
|
153
|
+
All of the methods that can be called against the instances of statements returned from the `using()` function are described in the statement specification subsection further below.
|
|
153
154
|
|
|
154
|
-
|
|
155
|
+
### Operations and transactions
|
|
155
156
|
|
|
156
|
-
|
|
157
|
+
Ideally, all statements should be executed in the context of a transaction. Murmuration provides a `transaction()` function that allows you to do this. It takes `configuration`, `operations`, `callback` and `context` arguments. The callback provided will have a `completed` argument while the context is mandatory and must be a plain old JavaScript object. The `transaction()` function makes use of the `context` object itself, in fact, with the object's `connection`, `operations` and `completed` properties being reserved.
|
|
157
158
|
|
|
158
|
-
|
|
159
|
+
In the example below, three operations have been provided and the context has some example properties that they will make use of:
|
|
159
160
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
161
|
+
```
|
|
162
|
+
const configuration = ... ,
|
|
163
|
+
operations = [
|
|
164
|
+
checkUsernameOperation,
|
|
165
|
+
checkEmailAddressOperation,
|
|
166
|
+
addEmailOperation
|
|
167
|
+
],
|
|
168
|
+
context = {
|
|
169
|
+
emailAddress,
|
|
170
|
+
username,
|
|
171
|
+
password
|
|
172
|
+
};
|
|
166
173
|
|
|
167
|
-
|
|
168
|
-
* The `set()` method must be called along with the `update()` method.
|
|
169
|
-
* The `where()` method can be called with all but the `insertInto()` method.
|
|
170
|
-
* The `values()` method must be called along with the `insertInto()` method.
|
|
171
|
-
* Either the `getSQL()` or `execute()` methods must be called, usually the latter.
|
|
174
|
+
transaction(configuration, operations, (completed) => {
|
|
172
175
|
|
|
173
|
-
|
|
176
|
+
..
|
|
177
|
+
|
|
178
|
+
}, context);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The signatures of the operations have already been demonstrated in the examples above. In fact the bodies of the operations are immaterial, they do not have to execute statements and can contain any application logic. However, if you do want to execute statement inside an operation then again see the examples for guidance on how to integration the statement methods with the operation callbacks.
|
|
182
|
+
|
|
183
|
+
What follows are general prescriptions for how to make use of transactions. They are meant to persuade that the small amount of boilerplate necessary to execute any statement inside a transaction is always worth the effort.
|
|
184
|
+
|
|
185
|
+
* You can try to insert values into a table and test whether they are unique by whether or not the insert fails. However, the database will throw an error that is indistinguishable from errors that occur because of, say, syntax errors in the SQL. It could be argued that it is bad practice to knowingly run a query that may result in an error and use the presence of such as an indication of whether or not an otherwise correct query has been successful. Therefore a better approach is to precede the insert with a select statement and execute both in the context of a transaction.
|
|
186
|
+
|
|
187
|
+
* Often conflating operations means that application logic that is far more suited to, in this case, JavaScript must be added to the SQL itself. Or worse, the application logic is simply assumed to be implicit in the SQL. It is far better to implement this kind of logic explicitly in JavaScript than complicate SQL statements with it. In short, executing multiple SQL statements threaded together with SQL variables and the like is always best avoided.
|
|
188
|
+
|
|
189
|
+
* As well as conditional branching, for example, often functionality needs to be implemented in the context of a transaction that cannot simply be added to an SQL statement. Unzipping a stored binary, for example, or checking some program variable dependent upon a prior query. Furthermore, a shared context means that even though several parts of the application logic might be related, they can still effectively communication with one another of the course of the transaction.
|
|
190
|
+
|
|
191
|
+
The example above demonstrates the crux of the approach taken here, therefore. The application logic is to be found in easily readable, atomic form within the body of each operation. On the other hand the SQL statements are considered to be dumb in the sense that they do nothing but slavishly place or retrieve information into or from the database.
|
|
192
|
+
|
|
193
|
+
### Supporting promises and the `async`/`await` syntax
|
|
194
|
+
|
|
195
|
+
Murmuration's promise-like syntax for generating and executing statements is easily and elegantly adaptable to promises. Consider the following asynchronous function to return a user from a database:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
async function getUser(connection, identifier) {
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
using(connection)
|
|
201
|
+
.selectFromUsers()
|
|
202
|
+
.where({ identifier })
|
|
203
|
+
.else(() => {
|
|
204
|
+
const user = null;
|
|
205
|
+
|
|
206
|
+
resolve(user);
|
|
207
|
+
})
|
|
208
|
+
.one(({ jsonString }) => {
|
|
209
|
+
const user = User.fromJSONString(jsonString);
|
|
210
|
+
|
|
211
|
+
resolve(user);
|
|
212
|
+
})
|
|
213
|
+
.catch(reject)
|
|
214
|
+
.execute();
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
```
|
|
174
218
|
|
|
175
|
-
|
|
219
|
+
Now, rather then the `one()`, `else()` and `catch()` methods calling the callbacks passed by way of an enclosing operation, they call the `resolve` and `reject` callbacks of the promise. The function can be thus be utilised as follows:
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
try {
|
|
223
|
+
const user = await getUser(connection, identifier); ;
|
|
224
|
+
|
|
225
|
+
/// Make use of the user
|
|
226
|
+
} catch (error) {
|
|
227
|
+
/// Handle any error
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Note that both the `one()` and `else()` methods call the `resolve` callback whereas the `catch()` method calls the `reject` callback, essentially passing the `error` argument on to body of the outermost `catch` block.
|
|
232
|
+
|
|
233
|
+
The following example is even more succinct:
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
async function destroyUser(connection, identifier) {
|
|
237
|
+
return new Promise((resolve, reject) => {
|
|
238
|
+
using(connection)
|
|
239
|
+
.deleteFromUsers()
|
|
240
|
+
.where({ identifier })
|
|
241
|
+
.success(resolve)
|
|
242
|
+
.catch(reject)
|
|
243
|
+
.execute();
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
If you want to interrupt the program flow for debugging purposes then replace the direct references to the `resolve` and `reject` callbacks with arrow functions that call them, as in the `one()` and `else()` methods in the first example.
|
|
249
|
+
|
|
250
|
+
Of course both of these functions can be utilised using promises directly, that is calling the `then()` and `catch()` methods of the returned promises directly.
|
|
251
|
+
|
|
252
|
+
Finally, note that it is easy to encapsulate the instantiation of promises into a function called `promisify()` or such like, but nothing is provided by this package.
|
|
253
|
+
|
|
254
|
+
### Extending the `Statement` class
|
|
176
255
|
|
|
177
256
|
This is recommended for no other reason than to avoid repetitively passing table or view names to `selectFrom()` methods and the like. A simple exapmle will amply demonstrate:
|
|
178
257
|
|
|
@@ -182,28 +261,20 @@ This is recommended for no other reason than to avoid repetitively passing table
|
|
|
182
261
|
const { Statement: BaseStatement } = require("./murmuration-...");
|
|
183
262
|
|
|
184
263
|
const USER_TABLE = "user",
|
|
185
|
-
RELEASE_TABLE = "release"
|
|
186
|
-
DISTRIBUTION_TABLE = "attribution",
|
|
187
|
-
LATEST_RELEASE_VIEW = "latest_release";
|
|
264
|
+
RELEASE_TABLE = "release";
|
|
188
265
|
|
|
189
266
|
class Statement extends BaseStatement {
|
|
190
267
|
updateUser() { return this.update(USER_TABLE); }
|
|
191
268
|
updateRelease() { return this.update(RELEASE_TABLE); }
|
|
192
|
-
updateDistribution() { return this.update(DISTRIBUTION_TABLE); }
|
|
193
269
|
|
|
194
270
|
insertIntoUser() { return this.insertInto(USER_TABLE); }
|
|
195
271
|
insertIntoRelease() { return this.insertInto(RELEASE_TABLE); }
|
|
196
|
-
insertIntoDistribution() { return this.insertInto(DISTRIBUTION_TABLE); }
|
|
197
272
|
|
|
198
273
|
deleteFromUser() { return this.deleteFrom(USER_TABLE); }
|
|
199
274
|
deleteFromRelease() { return this.deleteFrom(RELEASE_TABLE); }
|
|
200
|
-
deleteFromDistribution() { return this.deleteFrom(DISTRIBUTION_TABLE); }
|
|
201
275
|
|
|
202
276
|
selectFromUser() { return this.selectFrom(USER_TABLE); }
|
|
203
277
|
selectFromRelease() { return this.selectFrom(RELEASE_TABLE); }
|
|
204
|
-
selectFromDistribution() { return this.selectFrom(DISTRIBUTION_TABLE); }
|
|
205
|
-
|
|
206
|
-
selectFromLatestRelease() { return this.selectFrom(LATEST_RELEASE_VIEW); }
|
|
207
278
|
|
|
208
279
|
static fromConnection(connection) { return BaseStatement.fromConnection(Statement, connection); }
|
|
209
280
|
}
|
|
@@ -231,7 +302,7 @@ module.exports = using;
|
|
|
231
302
|
|
|
232
303
|
...or require and instantiate it directly. The `using()` function is purely for convenience.
|
|
233
304
|
|
|
234
|
-
###
|
|
305
|
+
### Connections
|
|
235
306
|
|
|
236
307
|
The static `fromConfiguration()` method of the `Connection` class takes a configuration and a callback argument:
|
|
237
308
|
|
|
@@ -270,45 +341,32 @@ In the event of an error, if a `log` property has been added to the `configurati
|
|
|
270
341
|
|
|
271
342
|
These messages are meant to help with debugging simple mistakes such as providing incorrect configuration. If you do not find them helpful, do not provide a `log` object and be assured that the errors are always returned by way of callback function arguments for you to deal with as you see fit.
|
|
272
343
|
|
|
273
|
-
###
|
|
274
|
-
|
|
275
|
-
Ideally, all database operations should be run in the context of transactions. There is a single `transaction()` function that allows you to do this. It takes `configuration`, `operations`, `callback` and `context` arguments. The callback provided will have a `completed` argument while the context is mandatory and must be a plain old JavaScript object. The `transaction()` function makes use of the `context` object itself, in fact, with the object's `connection`, `operations` and `completed` properties being reserved.
|
|
276
|
-
|
|
277
|
-
In the example below, three operations has been provided and the context has properties that they will make use of:
|
|
278
|
-
|
|
279
|
-
```
|
|
280
|
-
const configuration = ... ,
|
|
281
|
-
operations = [
|
|
282
|
-
checkUsernameOperation,
|
|
283
|
-
checkEmailAddressOperation,
|
|
284
|
-
addEmailOperation
|
|
285
|
-
],
|
|
286
|
-
context = {
|
|
287
|
-
emailAddress,
|
|
288
|
-
username,
|
|
289
|
-
password
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
transaction(configuration, operations, (completed) => {
|
|
293
|
-
|
|
294
|
-
..
|
|
344
|
+
### Statement class specification
|
|
295
345
|
|
|
296
|
-
|
|
297
|
-
```
|
|
346
|
+
The following specification gives a complete and more detailed list of the methods available.
|
|
298
347
|
|
|
299
|
-
|
|
348
|
+
* One of the `selectFrom()`, `insertInto()`, `deleteFrom()` or `delete()` methods must be called. Each takes a string for the name of a table or view.
|
|
300
349
|
|
|
301
|
-
|
|
350
|
+
It is recommended that these methods are not called directly, by the way, rather methods that call them indirectly are created on a subclass of the `Statement` class, in order to avoid repetition. This is covered in the later subsection on extending the `Statement` class.
|
|
302
351
|
|
|
303
|
-
*
|
|
352
|
+
* The `success()` method must accompany the `insertInto()`, `deleteFrom()` or `delete()` methods.
|
|
353
|
+
* If the `selectFrom()` method is called, then one of the following of the following methods must also be called. Each takes a callback. In all but the last case, you must also call the `else()` method with a callback:
|
|
354
|
+
* The `none()` method for when no rows are returned.
|
|
355
|
+
* The `one()` method for when exactly one row is returned.
|
|
356
|
+
* The `first()` method for when one or more rows are returned.
|
|
357
|
+
* The `many()` method for when any number of rows are returned, including none.
|
|
304
358
|
|
|
305
|
-
*
|
|
359
|
+
* The `catch()` method must always be called with a callback for when an exception occurs.
|
|
360
|
+
* The `set()` method must be called along with the `update()` method.
|
|
361
|
+
* The `where()` method can be called with all but the `insertInto()` method.
|
|
362
|
+
* The `values()` method must be called along with the `insertInto()` method.
|
|
363
|
+
* Either the `getSQL()` or `execute()` methods must be called, usually the latter.
|
|
306
364
|
|
|
307
|
-
|
|
365
|
+
Each of the `set()`, `where()` and `values()` methods can take a plain old JavaScript object or an appended template literal. You cannot pass a string as an argument because there is a danger that it might contain an un-escaped value. By forcing you to pass an appended template literal, the method in question is able to pass the array of arguments it receives directly on to the underlying database package, thereby guaranteeing that they will be correctly cast and escaped.
|
|
308
366
|
|
|
309
|
-
|
|
367
|
+
## Migrations
|
|
310
368
|
|
|
311
|
-
The migration functionality will definitely not suit every use case, however it can provide surety for small applications running on multiple Node instances connecting to a single
|
|
369
|
+
The migration functionality will definitely not suit every use case, however it can provide surety for small applications running on multiple Node instances connecting to a single database instance. It is essential that the prescriptions below are followed pretty much to the letter. Failing to do so will doubtless result in failure.
|
|
312
370
|
|
|
313
371
|
The `migrate()` function takes the usual `configuration` argument followed by `migrationsDirectoryPath` argument and a `callback` argument. The callback is called with the usual `error` argument, which is truthy if the migrations have succeeded and falsey otherwise.
|
|
314
372
|
|
package/license.txt
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "murmuration",
|
|
3
3
|
"author": "James Smith",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.4",
|
|
5
5
|
"license": "MIT, Anti-996",
|
|
6
6
|
"homepage": "https://github.com/djalbat/murmuration",
|
|
7
7
|
"description": "Database statements, transactions and migrations.",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"url": "https://github.com/djalbat/murmuration"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"necessary": "^11.
|
|
13
|
+
"necessary": "^11.7.6"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {},
|
|
16
16
|
"scripts": {}
|