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.
Files changed (3) hide show
  1. package/README.md +125 -67
  2. package/license.txt +1 -1
  3. 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 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,13 +31,16 @@ 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 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
- ### 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.
153
154
 
154
- The following specification gives a complete and more detailed list of the methods available.
155
+ ### Operations and transactions
155
156
 
156
- * One of the `selectFrom()`, `insertInto()`, `deleteFrom()` or `delete()` methods must be called. Each takes a string for the name of a table or view.
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
- 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.
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
- * The `success()` method must accompany the `insertInto()`, `deleteFrom()` or `delete()` methods.
161
- * 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:
162
- * The `none()` method for when no rows are returned.
163
- * The `one()` method for when exactly one row is returned.
164
- * The `first()` method for when one or more rows are returned.
165
- * The `many()` method for when any number of rows are returned, including none.
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
- * The `catch()` method must always be called with a callback for when an exception occurs.
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
- 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.
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
- ### Extending the Statement class
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
- ### Getting and releasing connections
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
- ### Using transactions
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
- }, context);
297
- ```
346
+ The following specification gives a complete and more detailed list of the methods available.
298
347
 
299
- The signatures of the operations are given in the usage section. The bodies of the operations are immaterial, again see the usage section for idees. What follows are general prescriptions for how to make use of transactions.
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
- * 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.
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
- * 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.
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
- * 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.
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
- 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.
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
- ### Making use of migrations
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 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.
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
@@ -1,6 +1,6 @@
1
1
  MIT And Anti-996 Licenses
2
2
 
3
- Copyright (c) 2019 James Smith
3
+ Copyright (c) 2023 James Smith
4
4
 
5
5
  These licenses shall be included in all copies or substantial portions of
6
6
  this software and associated documentation files (the "Software").
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "murmuration",
3
3
  "author": "James Smith",
4
- "version": "2.0.1",
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.1.3"
13
+ "necessary": "^11.7.6"
14
14
  },
15
15
  "devDependencies": {},
16
16
  "scripts": {}