murmuration 1.1.13 → 2.0.3

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.
Files changed (2) hide show
  1. package/README.md +175 -90
  2. package/package.json +1 -1
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 will be migrated if needed each time the former is deployed.
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,15 +31,18 @@ Remember that it is the aforementioned specific packages that you should install
31
31
 
32
32
  ## Usage
33
33
 
34
- Aside from small differences in configuration and error handling the Functionality across the aforementioned specific packages is identical, and is therefore covered here.
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, in a similar vein to an ORM, in this case 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
41
 
42
- In the first example, a `SELECT` statement is generated that checks an `account` table and returns an `id` should the email address and password match:
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.
44
+
45
+ In the first example a row is selected should its email address and password match the given values:
43
46
 
44
47
  ```
45
48
  const using = require("../using");
@@ -69,21 +72,12 @@ function checkAccountOperation(connection, abort, proceed, complete, context) {
69
72
 
70
73
  There are several points worth noting:
71
74
 
72
- * As already mentioned, ideally statements should be executed within operations. Each operation provides a connection and a context as well as three callbacks.
73
- * The three callbacks 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
- * A local `using()` function has been employed rather the the function supplied by the package, because the `Statement` class it utilities has been extended for convenience. More on this later.
75
-
76
- The remainder of the points pertain to the statement itself. An exhaustive description of the various methods available is given at the end of this subsection.
77
-
78
- * The `selectFromAccount()` is defined in the application's own `Statement` class, again more on this later.
79
75
  * The `where()` method takes a plain old JavaScript object.
80
76
  * The `one()` method takes a handler that is called if one row is returned.
81
77
  * The `else()` method takes a handler that is called in all other cases.
82
78
  * The `catch()` method takes a handler that is called should execution fail.
83
79
 
84
- Note that the assumption with passing a plain old JavaScript object in order to generate a `WHERE` clause is that the property values should be equated. Note also that the `abort()` function is provided directly to the `catch()` method.
85
-
86
- 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:
87
81
 
88
82
  ```
89
83
  const using = require("../using");
@@ -108,10 +102,10 @@ function deleteExpiredResetCodesOperation(connection, abort, proceed, complete,
108
102
 
109
103
  The following points are worth noting:
110
104
 
111
- * The `where()` method takes a template literal this time.
105
+ * The `where()` method takes a template literal this time, see below for further details.
112
106
  * The `success()` method takes a callback that is called should hte execution succeed.
113
107
 
114
- In the following example, a row is inserted into a table for software packages:
108
+ In this example a row is inserted into a table for software packages:
115
109
 
116
110
  ```
117
111
  const using = require("../using");
@@ -154,32 +148,161 @@ Note:
154
148
 
155
149
  * The `set()` method takes a plain old JavaScript object.
156
150
 
157
- Generally the idea is to be able to generate the most commonly used kinds of statements are deal with the outcomes of their execution with the minimum of fuss. If passing plain old JavaScript objects will not suffice then template literals can be used instead.
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.
158
152
 
159
- ### Statement class specification
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.
160
154
 
161
- The following specification gives a complete and more detailed list of the methods available.
155
+ ### Operations and transactions
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.
158
+
159
+ In the example below, three operations have been provided and the context has some example properties that they will make use of:
160
+
161
+ ```
162
+ const configuration = ... ,
163
+ operations = [
164
+ checkUsernameOperation,
165
+ checkEmailAddressOperation,
166
+ addEmailOperation
167
+ ],
168
+ context = {
169
+ emailAddress,
170
+ username,
171
+ password
172
+ };
173
+
174
+ transaction(configuration, operations, (completed) => {
162
175
 
163
- * One of the `selectFrom()`, `insertInto()`, `deleteFrom()` or `delete()` methods must be called. Each takes a string for the name of a table or view.
176
+ ..
164
177
 
165
- It is recommended that these methods are not called directly, 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.
178
+ }, context);
179
+ ```
166
180
 
167
- If the `selectFrom()` method is called, then one of the following of the following methods must be called. Each takes a callback:
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.
168
182
 
169
- * The `none()` method for when no rows are returned.
170
- * The `one()` method for when exactly one row is returned.
171
- * The `first()` method for when one or more rows are returned.
172
- * The `many()` method for when any number of rows are returned, including none.
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.
173
184
 
174
- In all but the last case, you must also call the `else()` method with a callback.
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.
175
186
 
176
- * The `success()` method must accompany the `insertInto()`, `deleteFrom()` or `delete()` methods.
177
- * The `catch()` method must always be called with a callback for when an exception occurs.
178
- * Either the `getSQL()` or `execute()` methods must be called, usually the latter.
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
+ ```
218
+
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
255
+
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:
179
257
 
180
- If
258
+ ```
259
+ "use strict";
260
+
261
+ const { Statement: BaseStatement } = require("./murmuration-...");
262
+
263
+ const USER_TABLE = "user",
264
+ RELEASE_TABLE = "release";
265
+
266
+ class Statement extends BaseStatement {
267
+ updateUser() { return this.update(USER_TABLE); }
268
+ updateRelease() { return this.update(RELEASE_TABLE); }
269
+
270
+ insertIntoUser() { return this.insertInto(USER_TABLE); }
271
+ insertIntoRelease() { return this.insertInto(RELEASE_TABLE); }
272
+
273
+ deleteFromUser() { return this.deleteFrom(USER_TABLE); }
274
+ deleteFromRelease() { return this.deleteFrom(RELEASE_TABLE); }
275
+
276
+ selectFromUser() { return this.selectFrom(USER_TABLE); }
277
+ selectFromRelease() { return this.selectFrom(RELEASE_TABLE); }
278
+
279
+ static fromConnection(connection) { return BaseStatement.fromConnection(Statement, connection); }
280
+ }
281
+
282
+ module.exports = Statement;
283
+ ```
284
+
285
+ Here the ellipsis in the `require()` statement should be replaced as needed.
286
+
287
+ Now make use of this subclass in your own `using()` function...
288
+
289
+ ```
290
+ "use strict";
291
+
292
+ const Statement = require("./statement");
293
+
294
+ function using(connection) {
295
+ const statement = Statement.fromConnection(connection);
296
+
297
+ return statement;
298
+ }
299
+
300
+ module.exports = using;
301
+ ```
181
302
 
182
- ### Getting and releasing connections
303
+ ...or require and instantiate it directly. The `using()` function is purely for convenience.
304
+
305
+ ### Connections
183
306
 
184
307
  The static `fromConfiguration()` method of the `Connection` class takes a configuration and a callback argument:
185
308
 
@@ -218,70 +341,32 @@ In the event of an error, if a `log` property has been added to the `configurati
218
341
 
219
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.
220
343
 
221
- ### Using transactions
222
-
223
- 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.
224
-
225
- In the example below, four operations has been provided and the context has properties that they will make use of:
226
-
227
- ```
228
- const configuration = ... ,
229
- operations = [
230
- checkUsernameAvailable,
231
- checkEmailAddressAvailable,
232
- addEmailAddressUsernamePasswordAndStatus,
233
- retrieveUserIdentifier,
234
- ],
235
- context = {
236
- emailAddress,
237
- username,
238
- password
239
- };
240
-
241
- transaction(configuration, operations, (completed) => {
242
-
243
- ..
244
-
245
- }, context);
246
- ```
247
- The signature of the operations must be identical to the following example:
248
-
249
- ```
250
- function checkUsernameAvailable(connection, abort, proceed, complete, context) {
251
- const { username } = context;
252
-
253
- selectUsername(connection, username, (error, rows) => {
254
- if (error) {
255
- abort();
256
-
257
- return;
258
- }
259
-
260
- const rowsLength = rows.length;
261
-
262
- (rowsLength === 0) ?
263
- proceed() :
264
- complete();
265
- });
266
- }
267
- ```
268
- Note the provision of the `abort()`, `proceed()` and `complete()` callbacks, each of which is utilised. Their utility should be self evident. One important point to note is that there is an explicit `return` statement after the call to the `abort()` callback. It is easy to forget that invoking a callback does not prevent execution continuing in the current context.
344
+ ### Statement class specification
269
345
 
270
- It is entirely possible to conflate the first three of these operations into a single SQL statement and then to combine that with the last SQL statement that recovers an auto-incremented identifier. Both of these statements can then be placed in a single SQL file and run with a call to the `query()` function. There is nothing to be gained from such a approach, however, and there are good reasons not to take it:
346
+ The following specification gives a complete and more detailed list of the methods available.
271
347
 
272
- * 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 considered 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.
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.
273
349
 
274
- * 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.
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.
275
351
 
276
- * 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.
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.
277
358
 
278
- 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.
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.
279
364
 
280
- This approach leads to less SQL and more JavaScript, however, as already mentioned but well worth repeating, that JavaScript is easily readable and atomic. The downside is a small amount of boilerplate JavaScript wrapping each operation, but this is a small price to pay.
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.
281
366
 
282
- ### Making use of migrations
367
+ ## Migrations
283
368
 
284
- 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 MariaDB instance. It is essential that the prescriptions below are followed pretty much to the letter. Failing to do so will doubtless result in failure.
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.
285
370
 
286
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.
287
372
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "murmuration",
3
3
  "author": "James Smith",
4
- "version": "1.1.13",
4
+ "version": "2.0.3",
5
5
  "license": "MIT, Anti-996",
6
6
  "homepage": "https://github.com/djalbat/murmuration",
7
7
  "description": "Database statements, transactions and migrations.",