murmuration 1.1.7 → 1.1.13

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 +149 -79
  2. package/bin/statement.js +81 -43
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,19 +1,17 @@
1
1
  # Murmuration
2
2
 
3
- Database connections, transactions and migrations.
3
+ Database statements, transactions and migrations.
4
4
 
5
5
  There are two specific packages that you should make use of instead this one:
6
6
 
7
7
  * [Murmuration-MariaDB](https://github.com/djalbat/murmuration-mariadb)
8
8
  * [Murmuration-PostGreSQL](https://github.com/djalbat/murmuration-postgresql)
9
9
 
10
- This readme file largely pertains to both, although there are also specific instructions given in readme file for each.
10
+ This readme file pertains to both, although there are specific instructions for each of the above in their respective readme files.
11
11
 
12
- Murmuration is meant to be used as alternative to a database [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping). Aside from migrations, 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 run commands, optionally in the context of transactions.
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
- The migration functionality, if used correctly, will guarantee that a Node application's codebase remains in line with the database it relies on, updating the latter each time the former is deployed.
15
-
16
- The prescriptions given below are an essential part of the package. They show how to write database utility functions at scale; how to employ them in the context of transactions; and they outline what needs to be done in order to guarantee the success of migrations.
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.
17
15
 
18
16
  ## Installation
19
17
 
@@ -33,120 +31,192 @@ Remember that it is the aforementioned specific packages that you should install
33
31
 
34
32
  ## Usage
35
33
 
36
- Functionality across the specific packages is identical, aside from small differences in configuration and error handling, and is therefore covered here.
34
+ Aside from small differences in configuration and error handling the Functionality across the aforementioned specific packages is identical, and is therefore covered here.
37
35
 
38
- ### Getting and releasing connections
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.
39
37
 
40
- The static `fromConfiguration()` method of the `Connection` class takes a configuration and a callback argument:
38
+ ### Generating statements
41
39
 
42
- ```
43
- Connection.fromConfiguration(configuration, (error, connection) => {
40
+ Statements are generated dynamically, in a similar vein to an ORM, in this case with a simple, promise-like syntax.
44
41
 
45
- ...
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:
46
43
 
47
- connection.release();
48
- };
44
+ ```
45
+ const using = require("../using");
46
+
47
+ function checkAccountOperation(connection, abort, proceed, complete, context) {
48
+ const { emailAddress, password } = context;
49
+
50
+ using(connection)
51
+ .selectFromAccont()
52
+ .where({ emailAddress, password })
53
+ .one(({ id }) => {
54
+ Object.assign(context, {
55
+ id
56
+ });
57
+
58
+ proceed();
59
+ })
60
+ .else(() => {
61
+ ...
62
+
63
+ complete()
64
+ })
65
+ .catch(abort)
66
+ .execute();
67
+ }
49
68
  ```
50
69
 
51
- If successful, the `error` argument of the callback will be null and a `connection` object will be returned, otherwise the `error` argument will be truthy. Details of the format of the `configuration` object can be found in the configuration subsections of the specific package readme files.
70
+ There are several points worth noting:
52
71
 
53
- ### Logging
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.
54
75
 
55
- Ideally you should add a `log` property to the `configuration` object that references an object of the following form:
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
+ * The `where()` method takes a plain old JavaScript object.
80
+ * The `one()` method takes a handler that is called if one row is returned.
81
+ * The `else()` method takes a handler that is called in all other cases.
82
+ * The `catch()` method takes a handler that is called should execution fail.
83
+
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.
56
87
 
57
88
  ```
58
- const log = {
59
- trace: () => { ... },
60
- debug: () => { ... },
61
- info: () => { ... },
62
- warning: () => { ... },
63
- error: () => { ... },
64
- fatal: () => { ... },
65
- });
89
+ const using = require("../using");
90
+
91
+ const { unauthorized } = require("../utilities/states");
92
+
93
+ function deleteExpiredResetCodesOperation(connection, abort, proceed, complete, context) {
94
+ const { emailAddress, password } = context;
95
+
96
+ using(connection)
97
+ .deleteFromResetCode()
98
+ .where`
99
+
100
+ expires < NOW()
101
+
102
+ `
103
+ .success(proceed)
104
+ .catch(abort)
105
+ .execute();
106
+ }
66
107
  ```
67
- Currently only the `error()`, `info()` and `debug()` functions are made use of, but in future others may be utilised.
68
108
 
69
- If you do not provide a `log` object, all logging is suppressed.
109
+ The following points are worth noting:
70
110
 
71
- ### Error handling
111
+ * The `where()` method takes a template literal this time.
112
+ * The `success()` method takes a callback that is called should hte execution succeed.
72
113
 
73
- In the event of an error, if a `log` property has been added to the `configuration` object then the `log.error()` function will be called with a message containing a reasonable stab at the cause of the error. Details can be found in the subsections of the same name in the specific package readme files.
114
+ In the following example, a row is inserted into a table for software packages:
74
115
 
75
- 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.
116
+ ```
117
+ const using = require("../using");
76
118
 
77
- ### Running queries
119
+ function createReleaseOperation(connection, abort, proceed, complete, context) {
120
+ const { name, entriesBlob, versionNumber } = context;
121
+
122
+ using(connection)
123
+ .insertIntoRelease()
124
+ .values({ name, entriesBlob, versionNumber })
125
+ .success(proceed)
126
+ .catch(abort)
127
+ .execute();
128
+ }
129
+ ```
78
130
 
79
- Two functions are provided, namely `query()` and `execute()`. The former returns an error and an array of rows returned by the query by way of a callback, the latter only an error by way of a callback. Otherwise their signatures are the same:
131
+ Note the following:
132
+
133
+ * The `values()` method takes a plain old JavaScript object with the values to be inserted.
134
+
135
+ Finally, the following operation updates a user profile:
80
136
 
81
137
  ```
82
- const sql = ... ;
138
+ const using = require("../using");
139
+
140
+ function editProfileOperation(connection, abort, proceed, complete, context) {
141
+ const { name, notes, userIdentifier } = context,
142
+ identifier = userIdentifier; ///
143
+
144
+ using(connection)
145
+ .updateUser()
146
+ .set({ name, notes })
147
+ .where({ identifier })
148
+ .success(proceed)
149
+ .execute();
150
+ }
151
+ ```
83
152
 
84
- query(connection, sql, ...parameters, (error, rows) => {
153
+ Note:
85
154
 
86
- ...
155
+ * The `set()` method takes a plain old JavaScript object.
87
156
 
88
- });
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.
89
158
 
90
- execute(connection, sql, ...parameters, status, (error) => {
159
+ ### Statement class specification
91
160
 
92
- ...
161
+ The following specification gives a complete and more detailed list of the methods available.
93
162
 
94
- });
95
- ```
96
- In both cases, a variable length list of parameters can be passed between the `sql` and `callback` arguments. These replace the placeholders in the SQL you provide. For more information, see the specific packages.
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.
97
164
 
98
- To make use of these functions, it is recommended that you create a file corresponding to each table or view, naming the functions therein to reflect the SQL statements and parameters. The SQL they employ can be read from files, the names of which exactly match the function names. For example:
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.
99
166
 
100
- ```
101
- const SELECT_USERNAME_FILE_NAME = "table/user/selectUsername.sql",
102
- SELECT_IDENTIFIER_FILE_NAME = "table/user/selectIdentifier.sql",
103
- SELECT_EMAIL_ADDRESS_FILE_NAME = "table/user/selectEmailAddress.sql",
104
- UPDATE_NAME_IDENTIFIER_FILE_NAME = "table/user/updateNameIdentifier.sql",
105
- ...
106
- ;
167
+ If the `selectFrom()` method is called, then one of the following of the following methods must be called. Each takes a callback:
107
168
 
108
- function selectUsername(connection, username, callback) { ... }
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.
109
173
 
110
- function selectIdentifier(connection, identifier, callback) { ... }
174
+ In all but the last case, you must also call the `else()` method with a callback.
111
175
 
112
- function selectEmailAddress(connection, emailAddress, callback) { ... }
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.
113
179
 
114
- function updateNameIdentifier(connection, name, identifier, callback) { ... }
180
+ If
115
181
 
116
- ...
117
- ```
118
- The body of each of the function should be identical bar the parameters and the use of the `query()` versus the `execute()` function:
182
+ ### Getting and releasing connections
183
+
184
+ The static `fromConfiguration()` method of the `Connection` class takes a configuration and a callback argument:
119
185
 
120
186
  ```
121
- function selectEmailAddress(connection, emailAddress, callback) {
122
- const filePath = `${SQL_DIRECTORY_PATH}/${SELECT_EMAIL_ADDRESS_FILE_NAME}`,
123
- sql = sqlFromFilePath(filePath);
187
+ Connection.fromConfiguration(configuration, (error, connection) => {
124
188
 
125
- query(connection, sql, emailAddress, (error, rows) => {
126
- if (error) {
127
- log.error("selectEmailAddress() failed.");
128
- }
189
+ ...
129
190
 
130
- callback(error, rows);
131
- });
132
- }
191
+ connection.release();
192
+ };
193
+ ```
133
194
 
134
- function updateNameIdentifier(connection, name, identifier, callback) {
135
- const filePath = `${SQL_DIRECTORY_PATH}/${UPDATE_NAME_IDENTIFIER_FILE_NAME}`,
136
- sql = sqlFromFilePath(filePath);
195
+ If successful, the `error` argument of the callback will be null and a `connection` object will be returned, otherwise the `error` argument will be truthy. Details of the format of the `configuration` object can be found in the configuration subsections of the specific package readme files.
137
196
 
138
- execute(connection, sql, name, identifier, (error) => {
139
- if (error) {
140
- log.error("updateNameIdentifier() failed.");
141
- }
197
+ ### Logging
142
198
 
143
- callback(error);
144
- });
145
- }
199
+ Ideally you should add a `log` property to the `configuration` object that references an object of the following form:
200
+
201
+ ```
202
+ const log = {
203
+ trace: () => { ... },
204
+ debug: () => { ... },
205
+ info: () => { ... },
206
+ warning: () => { ... },
207
+ error: () => { ... },
208
+ fatal: () => { ... },
209
+ });
146
210
  ```
147
- Note that the parameters, as well as matching the function names precisely, are passed directly to the `query()` or `execute()` functions. Essentially the only purpose of these functions is to retrieve the SQL, pass it to the requisite murmuration function and log an error if it occurs.
211
+ Currently only the `error()`, `info()` and `debug()` functions are made use of, but in future others may be utilised.
212
+
213
+ If you do not provide a `log` object, all logging is suppressed.
148
214
 
149
- Lastly, it is recommended that you avoid complex queries that span more than one table and always employ views instead. For information on views, see the MariaDB documentation [here](https://mariadb.com/kb/en/views/).
215
+ ### Error handling
216
+
217
+ In the event of an error, if a `log` property has been added to the `configuration` object then the `log.error()` function will be called with a message containing a reasonable stab at the cause of the error. Details can be found in the subsections of the same name in the specific package readme files.
218
+
219
+ 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.
150
220
 
151
221
  ### Using transactions
152
222
 
@@ -213,7 +283,7 @@ This approach leads to less SQL and more JavaScript, however, as already mention
213
283
 
214
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.
215
285
 
216
- The `migrate()` function takes the usual `configuration` argument followed by `migrationsDirectoryPath` argument and a `callback` argument. The callback is invoked with the usual `error` argument, which is truthy if the migrations have succeeded and falsey otherwise.
286
+ 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.
217
287
 
218
288
  ```
219
289
  const configuration = ... ,
package/bin/statement.js CHANGED
@@ -76,6 +76,10 @@ class Statement {
76
76
  this.query = query;
77
77
  }
78
78
 
79
+ setParameters(parameters) {
80
+ this.parameters = parameters;
81
+ }
82
+
79
83
  one(oneHandler) {
80
84
  this.oneHandler = oneHandler;
81
85
 
@@ -118,8 +122,21 @@ class Statement {
118
122
  return this;
119
123
  }
120
124
 
121
- set(object) {
122
- const assignments = this.assignmentsFromObject(object); ///
125
+ set(objectOrArray, ...parameters) {
126
+ let assignments;
127
+
128
+ const objectOrArrayIsArray = Array.isArray(objectOrArray);
129
+
130
+ if (objectOrArrayIsArray) {
131
+ const array = objectOrArray, ///
132
+ strings = array; ///
133
+
134
+ assignments = this.assignmentsFromStringsAndParameters(strings, parameters);
135
+ } else {
136
+ const object = objectOrArray; ///
137
+
138
+ assignments = this.assignmentsFromObject(object); ///
139
+ }
123
140
 
124
141
  this.sql = ` ${this.sql} SET ${assignments}`;
125
142
 
@@ -132,7 +149,7 @@ class Statement {
132
149
  const objectOrArrayIsArray = Array.isArray(objectOrArray);
133
150
 
134
151
  if (objectOrArrayIsArray) {
135
- const array = objectOrArray,
152
+ const array = objectOrArray, ///
136
153
  strings = array; ///
137
154
 
138
155
  clause = this.clauseFromStringsAndParameters(strings, parameters);
@@ -150,20 +167,21 @@ class Statement {
150
167
  values(objectOrArray, ...parameters) {
151
168
  const objectOrArrayIsArray = Array.isArray(objectOrArray);
152
169
 
170
+ let columnsAndValues;
171
+
153
172
  if (objectOrArrayIsArray) {
154
- const array = objectOrArray,
155
- strings = array, ///
156
- columnsAndValues = this.columnsAndValuesFromStringsAndParameters(strings, parameters);
173
+ const array = objectOrArray, ///
174
+ strings = array; ///
157
175
 
158
- this.sql = `${this.sql} ${columnsAndValues}`;
176
+ columnsAndValues = this.columnsAndValuesFromStringsAndParameters(strings, parameters);
159
177
  } else {
160
- const object = objectOrArray, ///
161
- values = this.valuesFromObject(object),
162
- columns = this.columnsFromObject(object)
178
+ const object = objectOrArray; ///
163
179
 
164
- this.sql = `${this.sql} (${columns}) VALUES (${values})`;
180
+ columnsAndValues = this.columnsAndValuesFromObject(object);
165
181
  }
166
182
 
183
+ this.sql = `${this.sql} ${columnsAndValues}`;
184
+
167
185
  return this;
168
186
  }
169
187
 
@@ -182,18 +200,19 @@ class Statement {
182
200
  return object;
183
201
  }
184
202
 
185
- valuesFromObject(object) {
186
- const columns = this.columnsFromObject(object),
203
+ clauseFromObject(object) {
204
+ const keys = Object.keys(object),
205
+ columns = keys.map((key) => this.columnFromKey(key)),
187
206
  parameters = Object.values(object), ///
188
207
  firstIndex = 0,
189
- values = columns.reduce((values, column, index) => {
208
+ clause = columns.reduce((clause, column, index) => {
190
209
  const placeholder = this.placeholder();
191
210
 
192
- values = (index === firstIndex) ?
193
- `${placeholder}` :
194
- ` ${values}, ${placeholder}`;
211
+ clause = (index === firstIndex) ?
212
+ `${column}=${placeholder}` :
213
+ ` ${clause} AND ${column}=${placeholder}`;
195
214
 
196
- return values;
215
+ return clause;
197
216
  }, EMPTY_STRING);
198
217
 
199
218
  this.parameters = [
@@ -201,21 +220,22 @@ class Statement {
201
220
  ...parameters
202
221
  ];
203
222
 
204
- return values;
223
+ return clause;
205
224
  }
206
225
 
207
- clauseFromObject(object) {
208
- const columns = this.columnsFromObject(object),
226
+ assignmentsFromObject(object) {
227
+ const keys = Object.keys(object),
228
+ columns = keys.map((key) => this.columnFromKey(key)),
209
229
  parameters = Object.values(object), ///
210
230
  firstIndex = 0,
211
- clause = columns.reduce((clause, column, index) => {
231
+ assignments = columns.reduce((assignments, column, index) => {
212
232
  const placeholder = this.placeholder();
213
233
 
214
- clause = (index === firstIndex) ?
215
- `${column}=${placeholder}` :
216
- ` ${clause} AND ${column}=${placeholder}`;
234
+ assignments = (index === firstIndex) ?
235
+ `${column}=${placeholder}` :
236
+ ` ${assignments}, ${column}=${placeholder}`;
217
237
 
218
- return clause;
238
+ return assignments;
219
239
  }, EMPTY_STRING);
220
240
 
221
241
  this.parameters = [
@@ -223,36 +243,31 @@ class Statement {
223
243
  ...parameters
224
244
  ];
225
245
 
226
- return clause;
246
+ return assignments;
227
247
  }
228
248
 
229
- columnsFromObject(object) {
249
+ columnsAndValuesFromObject(object) {
230
250
  const keys = Object.keys(object),
231
- columns = keys.map((key) => this.columnFromKey(key));
232
-
233
- return columns;
234
- }
235
-
236
- assignmentsFromObject(object) {
237
- const columns = this.columnsFromObject(object),
251
+ columns = keys.map((key) => this.columnFromKey(key)),
238
252
  parameters = Object.values(object), ///
239
253
  firstIndex = 0,
240
- assignments = columns.reduce((assignments, column, index) => {
254
+ values = columns.reduce((values, column, index) => {
241
255
  const placeholder = this.placeholder();
242
256
 
243
- assignments = (index === firstIndex) ?
244
- `${column}=${placeholder}` :
245
- ` ${assignments}, ${column}=${placeholder}`;
257
+ values = (index === firstIndex) ?
258
+ `${placeholder}` :
259
+ ` ${values}, ${placeholder}`;
246
260
 
247
- return assignments;
248
- }, EMPTY_STRING);
261
+ return values;
262
+ }, EMPTY_STRING),
263
+ columnsAndValues = `(${columns}) VALUES (${values})`;
249
264
 
250
265
  this.parameters = [
251
266
  ...this.parameters,
252
267
  ...parameters
253
268
  ];
254
269
 
255
- return assignments;
270
+ return columnsAndValues;
256
271
  }
257
272
 
258
273
  clauseFromStringsAndParameters(strings, parameters) {
@@ -272,12 +287,35 @@ class Statement {
272
287
 
273
288
  this.parameters = [
274
289
  ...this.parameters,
275
- ...parameters
290
+ ...parameters
276
291
  ];
277
292
 
278
293
  return clause;
279
294
  }
280
295
 
296
+ assignmentsFromStringsAndParameters(strings, parameters) {
297
+ const stringsLength = strings.length,
298
+ lastIndex = stringsLength - 1,
299
+ assignments = strings.reduce((assignments, string, index) => {
300
+ if (index < lastIndex) {
301
+ const placeholder = this.placeholder();
302
+
303
+ assignments = `${assignments}${string}${placeholder}`
304
+ } else {
305
+ assignments = `${assignments}${string}`;
306
+ }
307
+
308
+ return assignments;
309
+ }, EMPTY_STRING);
310
+
311
+ this.parameters = [
312
+ ...this.parameters,
313
+ ...parameters
314
+ ];
315
+
316
+ return assignments;
317
+ }
318
+
281
319
  columnsAndValuesFromStringsAndParameters(strings, parameters) {
282
320
  const stringsLength = strings.length,
283
321
  lastIndex = stringsLength - 1,
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "murmuration",
3
3
  "author": "James Smith",
4
- "version": "1.1.7",
4
+ "version": "1.1.13",
5
5
  "license": "MIT, Anti-996",
6
6
  "homepage": "https://github.com/djalbat/murmuration",
7
- "description": "Database connections, transactions and migrations.",
7
+ "description": "Database statements, transactions and migrations.",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/djalbat/murmuration"