pure-orm 1.3.1 → 2.2.0

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 (41) hide show
  1. package/.travis.yml +0 -6
  2. package/README.md +456 -185
  3. package/examples/order-more/bo/customer.js +18 -0
  4. package/examples/order-more/bo/customers.js +9 -0
  5. package/examples/order-more/bo/line-item.js +33 -0
  6. package/examples/order-more/bo/line-items.js +9 -0
  7. package/examples/order-more/bo/order.js +49 -0
  8. package/examples/order-more/bo/orders.js +9 -0
  9. package/examples/order-more/bo/parcel-event.js +19 -0
  10. package/examples/order-more/bo/parcel-events.js +9 -0
  11. package/examples/order-more/bo/parcel-line-item.js +24 -0
  12. package/examples/order-more/bo/parcel-line-items.js +9 -0
  13. package/examples/order-more/bo/parcel.js +18 -0
  14. package/examples/order-more/bo/parcels.js +9 -0
  15. package/examples/order-more/bo/physical-address.js +30 -0
  16. package/examples/order-more/bo/physical-addresses.js +12 -0
  17. package/examples/order-more/bo/refund.js +28 -0
  18. package/examples/order-more/bo/refunds.js +9 -0
  19. package/examples/order-more/bo/shipment-actual-product-variant.js +26 -0
  20. package/examples/order-more/bo/shipment-actual-product-variants.js +9 -0
  21. package/examples/order-more/bo/utm-medium.js +13 -0
  22. package/examples/order-more/bo/utm-source.js +13 -0
  23. package/examples/order-more/business-objects.js +12 -1
  24. package/package.json +1 -1
  25. package/src/bo/base-bo.js +51 -27
  26. package/src/bo/base-bo.spec.js +259 -0
  27. package/test-utils/eight/results.json +128 -0
  28. package/test-utils/eleven/results.json +818 -0
  29. package/test-utils/nine/bo/base.js +5 -0
  30. package/test-utils/nine/bo/feature-switch.js +18 -0
  31. package/test-utils/nine/bo/feature-switches.js +12 -0
  32. package/test-utils/nine/business-objects.js +7 -0
  33. package/test-utils/nine/results.json +12 -0
  34. package/test-utils/ten/results.json +899 -0
  35. package/test-utils/twelve/bo/base.js +5 -0
  36. package/test-utils/twelve/bo/member.js +16 -0
  37. package/test-utils/twelve/bo/members.js +9 -0
  38. package/test-utils/twelve/bo/prompt.js +20 -0
  39. package/test-utils/twelve/bo/prompts.js +9 -0
  40. package/test-utils/twelve/business-objects.js +8 -0
  41. package/test-utils/twelve/results.json +8 -0
package/README.md CHANGED
@@ -8,15 +8,24 @@ npm install --save pure-orm
8
8
 
9
9
  ## What is PureORM?
10
10
 
11
- PureORM is a pure ORM sql toolkit library for node (on top of `pg-promise`). It allows you to write regular native SQL and receive back properly structured (nested) pure business objects.
11
+ PureORM is a lightweight ORM for mapping the relational result rows of a database driver query to properly structured (nested) pure instances of your business object classes.
12
12
 
13
- This contrasts against traditional ("stateful") ORMs which use query builders (rather than raw SQL) to return database-aware (rather than pure) objects.
13
+ It's purpose - and guiding principle - is to allow you to write regular native SQL (not niche library-specific ORM wrapper APIs) and receive back properly structured/nested pure business objects (not database-connected stateful objects).
14
14
 
15
- The name _**pure**ORM_ reflects both that it is _pure_ ORM (there is no query builder dimension) as well as the _purity_ of the mapped Objects.
15
+ PureORM is the top layer for interfacing with your database. You bring your own database driver (eg [node-postgres](https://github.com/brianc/node-postgres)/[pg-promise](https://github.com/vitaly-t/pg-promise), [mysql](https://github.com/mysqljs/mysql), [node-sqlite3](https://github.com/mapbox/node-sqlite3), [node-mssql](https://github.com/tediousjs/node-mssql), [node-oracledb](https://github.com/oracle/node-oracledb), etc), and PureORM works on top of it to perform the Object-Relational Mapping (ORM).
16
+
17
+ PureORM contrasts with tradtional ORMs in two ways:
18
+
19
+ 1. PureORM is purely the "orm" (object-relational mapping) - it has the small scope of "owning" the mapping of database driver relational result rows to properly structured business objects. This contrasts against "ORM"s as typically exist - where they have grown to intertwine a huge query builder API with the ORM layer, where mapped objects are database-connected and serve as the query builder.
20
+ 2. PureORM yields pure (not database-connected) business objects. Queries are written in SQL (not niche library-specific ORM wrapper APIs), and the results are pure businesses objects.
21
+
22
+ Thus PureORM contrasts against traditional ORMs which use query builders (rather than raw SQL) to return database-connected (rather than pure) objects.
23
+
24
+ The name _**pure**ORM_ reflects both of these points - that it is _pure_ ORM (there is no query builder dimension) as well as the _purity_ of the mapped objects.
16
25
 
17
26
  #### Philosophy
18
27
 
19
- - Write _native_, _unobstructed_ SQL in a "data access layer" which returns _pure_ "business objects" to be used in the app's business logic.
28
+ - Write _native_, _unobstructed_ SQL in a "data access object" layer which returns _pure_ "business objects" to be used in the app's business logic.
20
29
  - Have _database-connected_ "data access objects" which allow the unobstructed writing of normal SQL.
21
30
  - Have the "data access objects" returning the pure business objects.
22
31
 
@@ -29,6 +38,12 @@ A **Business Object** (BO) is a pure javascript object corresponding to a table.
29
38
  - They are the subject of the app's business logic.
30
39
  - They will be full of userland business logic methods.
31
40
  - Their purity allows them to be easy to test/use.
41
+ - These are also referred to as "models".
42
+
43
+ A **Business Object Collection** (BO Collection) is a group of pure javascript objects.
44
+
45
+ - If your query returns records for multiple business objects, a BO Collection will be created and returned.
46
+ - You can create a BO Collection class for your business objects (in cases where it is useful to have business methods on the collection entity, not just each model entity).
32
47
 
33
48
  A **Data Access Object** (DAO) is a database-aware abstraction layer where native SQL is written.
34
49
 
@@ -36,167 +51,465 @@ A **Data Access Object** (DAO) is a database-aware abstraction layer where nativ
36
51
  - Rather, is a data access layer in which native SQL is written, and which returns business objects (properly nested and structured).
37
52
  - By convention, they may also accept business objects as inputs (to get, create, or update records) - but this is just a convention (necessary input data can be passed as separate arguments, or however).
38
53
 
39
- ---
40
-
41
- ## Examples
54
+ ## Quick Example
42
55
 
43
- ### Data Access Object
44
-
45
- Our data access layer where SQL is written.
56
+ Using PureORM allows you to write code like this:
46
57
 
47
58
  ```javascript
48
59
  class PersonDAO extends BaseDAO {
49
- Bo = Person;
50
- // example code from below...
60
+ get({ id }) {
61
+ const query = `
62
+ SELECT
63
+ ${Person.getSQLSelectClause()},
64
+ ${Job.getSQLSelectClause()},
65
+ ${Employer.getSQLSelectClause()}
66
+ FROM person
67
+ JOIN job on person.id = job.person_id
68
+ JOIN employer on job.employer_id = employer.id
69
+ WHERE id = $(id)
70
+ `;
71
+ return this.one(query, { id });
72
+ }
51
73
  }
52
74
  ```
53
75
 
54
- Lets start with a basic example which just uses the
55
- **`BaseBO.createOneFromDatabase`** method to map the column names to our desired
56
- javascript properties.
76
+ And use it like this:
57
77
 
58
78
  ```javascript
59
- getRandom() {
60
- const query = `
61
- SELECT person.id, person.first_name, person.last_name, person.created_date, person.employer_id
62
- FROM person
63
- ORDER BY random()
64
- LIMIT 1;
65
- `;
66
- return db.one(query).then(Person.createOneFromDatabase)
79
+ const person = await personDAO.get({ id: 55 });
80
+ console.log(person);
81
+ ```
82
+
83
+ ```javascript
84
+ Person {
85
+ id: 55,
86
+ name: 'John Doe',
87
+ jobs: JobsCollection {
88
+ models: [
89
+ Job {
90
+ id: 277,
91
+ personId: 55,
92
+ employerId: 17,
93
+ startDate: '2020-01-01',
94
+ endDate: '2020-12-31',
95
+ employer: Employer {
96
+ id: 17,
97
+ name: 'Good Corp',
98
+ }
99
+ },
100
+ Job {
101
+ id: 278,
102
+ personId: 55,
103
+ employerId: 26,
104
+ startDate: '2021-01-01',
105
+ endDate: '2021-12-31',
106
+ employer: Employer {
107
+ id: 26,
108
+ name: 'Better Corp',
109
+ }
110
+ }
111
+ ]
112
+ }
67
113
  }
68
- // OUTPUT: Person {id, firstName, lastName, createdDate, employerId}
69
114
  ```
70
115
 
71
- We can use **`BaseDAO.one`** to create our business object for us.
116
+ > This is a quick showcase. To see how to wire up this code, see the full [Practical Example](#practical-example) below.
117
+
118
+ ### Things to Note:
119
+
120
+ - Our DAO returns a single Person business object which is properly structured from the many relational row records!
121
+ - Our query is executed with a `one` method. The DAO methods for `one`, `oneOrNone`, `many`, `any` ensure their count against the number of generated top level business objects - not the number of relational row records the sql expression returns!
122
+ - Rather than manually specifying our columns in the sql select expression, we use the business object's `getSQLSelectClause`. This is purely a convenience method which namespaces each column with the table name prefix to ensure column names don't collide (for example, the person, job, and employer `id`s would collide if not namespaced, as would person and employer `name`s). You are welcome to do this by hand instead of using the convenience methods (as were used above), if you don't mind the tedium:
123
+ ```javascript
124
+ class PersonDAO extends BaseDAO {
125
+ get({ id }) {
126
+ // Example showing you can manually specific the select expression fields
127
+ // instead of using a business object's `getSQLSelectClause` convenience
128
+ // method. Note: you must namespace the field with table name and hashtag.
129
+ const query = `
130
+ SELECT
131
+ person.id as "person#id",
132
+ person.name as "person#name",
133
+ job.id as "job#id",
134
+ job.person_id: "job#person_id",
135
+ job.employer_id: "job#employer_id",
136
+ job.start_date: "job#start_date",
137
+ job.end_date: "job#end_date",
138
+ employer.id as "employer#id",
139
+ employer.name as "employer#name"
140
+ FROM person
141
+ JOIN job on person.id = job.person_id
142
+ JOIN employer on job.employer_id = employer.id
143
+ WHERE id = $(id)
144
+ `;
145
+ return this.one(query, { id });
146
+ }
147
+ }
148
+ ```
149
+
150
+ ## Usage
151
+
152
+ ### Ways of Using PureORM
153
+
154
+ PureORM can work with no integration with your database driver. This is the strictest usage of PureORM. Going this route, the only export you use from PureORM is the factory to create your base bo constructor (`createBaseBO`) and optionally a base bo collection constructor (`BaseBoCollection`).
155
+
156
+ PureORM also can wrap your database driver to make the API slightly less redundant. In this case you'll use the export which is a factory to create a base dao constructor (`createBaseDAO`). When you use this PureORM in this way, your DAO also inherits a handful of convenience methods for basic CRUD operations.
157
+
158
+ Overview of the API (full [API details](#api) further down):
159
+
160
+ - `createBaseBO` is a factory to create a base bo constructor. This factory method accepts a single argument: `getBusinessObjects` which is a function that returns an array of all your business object classes. You extend this class for all your business objects, and for each of these you must provide static `table` and `sqlColumnsData` fields. There are a number of other methods you can override from base to accomodate more advanced usage (including a `BoCollection` field to reference a custom class you want used for the collection).
161
+ - `BaseBoCollection` can be used as the base class for any bo collection classes you make. Sometimes it can be useful to have business methods on the collection entity (not just each model entity) and so you can create such a class by extending `BaseBoCollection` and providing a `BoCollection` field on the business object model class.
162
+
163
+ If you would like PureORM to wrap your database driver, you'll also use:
164
+
165
+ - `createBaseBO` is a factory to create a base dao constructor. This factory method accepts two arguments: `db` which is your database driver, and optionally `logError` which logs any database errors. You extend this class for all your dao objects, and for each of these you must provide a `Bo` field in the class which references the busines object this class is used for.
166
+
167
+ ## Practical Example
168
+
169
+ Lets take a practical example to see all this in action. Lets fill in the backend for a tiny web page renderer.
170
+
171
+ Lets say we have a database with three tables: person, job, and employer. We have a profile.html template set up so that if I hard code data like below, it renders our page.
172
+
173
+ ```javascript
174
+ // ./controllers/pages/person.js
175
+ const renderProfile = (req, res) => {
176
+ const person = {
177
+ id: 55,
178
+ name: 'John Doe',
179
+ jobs: {
180
+ models: [
181
+ {
182
+ id: 277,
183
+ personId: 55,
184
+ employerId: 17,
185
+ startDate: '2020-01-01',
186
+ endDate: '2020-12-31',
187
+ employer: {
188
+ id: 17,
189
+ name: 'Good Corp'
190
+ }
191
+ },
192
+ {
193
+ id: 278,
194
+ personId: 55,
195
+ employerId: 26,
196
+ startDate: '2021-01-01',
197
+ endDate: '2021-12-31',
198
+ employer: {
199
+ id: 26,
200
+ name: 'Better Corp'
201
+ }
202
+ }
203
+ ]
204
+ }
205
+ };
206
+ res.render('profile.html', person);
207
+ };
208
+ ```
209
+
210
+ Based on the tables, I know exactly how to query for this:
211
+
212
+ ```sql
213
+ SELECT *
214
+ FROM person
215
+ JOIN job on person.id = job.person_id
216
+ JOIN employer on job.employer_id = employer.id
217
+ WHERE id = 55;
218
+ ```
219
+
220
+ I already know how to SQL, and don't want to spend the time mapping what I already know how to do onto a huge, niche, library-specific API.
221
+
222
+ However, using this query with a database driver would give me a bunch of flat result records, not one object that is properly structed/nested like I want in my code, and with collided fields (id from all three tables, name from person and employer, etc).
223
+
224
+ So, lets install PureOrm and the database driver and get started!
225
+
226
+ ### Step 1: Installing PureORM and the Database Driver
227
+
228
+ For our example, we'll assume postgres database so we'll use the incredible [pg-promise](https://github.com/vitaly-t/pg-promise) database driver.
229
+
230
+ ```bash
231
+ npm install --save pure-orm
232
+ npm install --save pg-promise
233
+ ```
234
+
235
+ ### Step 2: Writing the Controller Code
236
+
237
+ Lets remove our hardcoded example, and write our contoller code using functions we want to exist and will create.
72
238
 
73
239
  ```diff
74
- getRandom() {
75
- const query = `
76
- SELECT person.id, person.first_name, person.last_name, person.created_date, person.employer_id
77
- FROM person
78
- ORDER BY random()
79
- LIMIT 1;
80
- `;
81
- - return db.one(query).then(Person.createOneFromDatabase)
82
- + return this.one(query);
240
+ // ./controllers/pages/person.js
241
+ +const personDAO = require('./dao/person');
242
+ const renderProfile = (req, res) => {
243
+ - const person = {
244
+ - id: 55,
245
+ - name: 'John Doe',
246
+ - jobs: {
247
+ - models: [
248
+ - {
249
+ - id: 277,
250
+ - personId: 55,
251
+ - employerId: 17,
252
+ - startDate: '2020-01-01',
253
+ - endDate: '2020-12-31',
254
+ - employer: {
255
+ - id: 17,
256
+ - name: 'Good Corp',
257
+ - }
258
+ - },
259
+ - {
260
+ - id: 278,
261
+ - personId: 55,
262
+ - employerId: 26,
263
+ - startDate: '2021-01-01',
264
+ - endDate: '2021-12-31',
265
+ - employer: {
266
+ - id: 26,
267
+ - name: 'Better Corp',
268
+ - }
269
+ - }
270
+ - ]
271
+ - }
272
+ - };
273
+ + const person = personDAO.get({ id: req.params.id });
274
+ res.render('profile.html', person);
275
+ ```
276
+
277
+ This looks nice, now let's create the dao and bo necessary.
278
+
279
+ ### Step 3: Creating the Business Objects
280
+
281
+ Let's create a `/bo` directory and the classes we want.
282
+
283
+ ```javascript
284
+ // ./bo/person.js
285
+ const Base = require('./base');
286
+
287
+ class Person extends Base {
288
+ static tableName = 'person';
289
+ static sqlColumnsData = ['id', 'name'];
290
+ // any other business methods...
83
291
  }
84
- // OUTPUT: Person {id, firstName, lastName, createdDate, employerId}
292
+ module.exports = Person;
85
293
  ```
86
294
 
87
- Specifying all the columns is tedious; lets use
88
- **`BaseBo.getSQLSelectClause()`** to get them for free.
295
+ ```javascript
296
+ // ./bo/job.js
297
+ const Base = require('./base');
298
+ const Person = require('./person');
299
+ const Employer = require('./employer');
300
+
301
+ class Job extends Base {
302
+ static tableName = 'job';
303
+ static sqlColumnsData = [
304
+ 'id',
305
+ { column: 'person_id', references: Person },
306
+ { column: 'employer_id', references: Employer },
307
+ 'start_date',
308
+ 'end_date'
309
+ ];
310
+ // any other business methods...
311
+ }
312
+ module.exports = Job;
313
+ ```
89
314
 
90
- ```diff
91
- getRandom() {
92
- const query = `
93
- - SELECT person.id, person.first_name, person.last_name, person.created_date, person.employer_id
94
- + SELECT ${Person.getSQLSelectClause()}
95
- FROM person
96
- ORDER BY random()
97
- LIMIT 1;
98
- `;
99
- return this.one(query);
315
+ ```javascript
316
+ // ./bo/employer.js
317
+ const Base = require('./base');
318
+
319
+ class Employer extends Base {
320
+ static tableName = 'employer';
321
+ static sqlColumnsData = ['id', 'name'];
322
+ // any other business methods...
100
323
  }
101
- // OUTPUT: Person {id, firstName, lastName, createdDate, employerId}
324
+ module.exports = Employer;
102
325
  ```
103
326
 
104
- More important than saving the tedium, though, is how
105
- **`BaseBo.getSQLSelectClause()`** namespaces each select expression name
106
- under the hood, and which **`BaseBo.createOneFromDatabase`** knows how to handle.
107
- This means that when joining, not only is the select expression easy,
108
- select expression names won't collide:
327
+ We've not got our three business object classes. To review, each business object class has:
109
328
 
110
- ```diff
111
- getRandom() {
112
- const query = `
113
- - SELECT ${Person.getSQLSelectClause()}
114
- + SELECT ${Person.getSQLSelectClause()}, ${Employer.getSQLSelectClause()}
115
- FROM person
116
- + JOIN employer on person.employer_id = employer.id
117
- ORDER BY random()
118
- LIMIT 1;
119
- `;
120
- return this.one(query);
329
+ - A static `tableName` property to denote which table results this class is for.
330
+ - A static `sqlColumnsData` property to enumerate the table columns.
331
+
332
+ The last business object we have to make is the `Base` one all of our above model classes extend. In order for any business model to properly structure/nest any other business object, each business object needs to know about every other business object. We do this by providing a `getBusinessObjects` method to our `createBaseBO` factory. (There is of course a circular dependency between the base class needing all classes that will end up being created which extend it, and using a function allows us to get around this circular dependency.)
333
+
334
+ ```javascript
335
+ // ./bo/base.js
336
+ const { createBaseBO } = require('pure-orm');
337
+
338
+ const BaseBO = createBaseBO({
339
+ getBusinessObjects: () => [
340
+ require('./person'), // eslint-disable-line
341
+ require('./job'), // eslint-disable-line
342
+ require('./employer') // eslint-disable-line
343
+ ]
344
+ });
345
+ module.exports = BaseBO;
346
+ ```
347
+
348
+ ### Step 4: Creating the DAO Object
349
+
350
+ At this point, the path diverges for if you want to PureORM in the strictest/narrowest way, or in the more integrated way.
351
+
352
+ Either way lets create a `./dao` directory and a `person.js` file.
353
+
354
+ **If you want to use it in the narrowest way:**
355
+
356
+ We'll import the database driver (`db`) and use it directly, and then call our ORM method on the results.
357
+
358
+ ```javascript
359
+ // ./dao/person.js
360
+ const { db } = require('../factories/db');
361
+ const Person = require('../business-objects/person');
362
+ class PersonDAO {
363
+ async get({ id }) {
364
+ const query = `
365
+ SELECT
366
+ ${Person.getSQLSelectClause()},
367
+ ${Job.getSQLSelectClause()},
368
+ ${Employer.getSQLSelectClause()}
369
+ FROM person
370
+ JOIN job on person.id = job.person_id
371
+ JOIN employer on job.employer_id = employer.id
372
+ WHERE id = $(id)
373
+ `;
374
+ const rawResultRows = await db.many(query, { id });
375
+ return Person.createOneFromDatabase(rawResultRows);
376
+ }
121
377
  }
122
- // OUTPUT: Person {id, firstName, lastName, createdDate, employer: Employer}
378
+ module.exports = new PersonDAO();
123
379
  ```
124
380
 
125
- Rather than being flat, with the employer id and createdDate colliding with
126
- person's id and createDate, the result is a nice Person BO with a nested
127
- Employer BO.
381
+ Notice we are using the _one_ method `createOneFromDatabase` which is what we want since we are creating _one_ person, even though we use _many_ for the database driver. From the database driver's perspective there are many relational result row; however, from our perspective, these compose _one_ properly structured person. The create from database methods (`createOneFromDatabase`, `createOneOrNoneFromDatabase`, `createManyFromDatabase`, `createFromDatabase` for any) ensure their count against the number of generated top level business objects - not the number of rows the sql expression returns!
382
+
383
+ If you are opting for this strict/narrow usage of PureORM, you're done! The DAO get method returns the relational result rows properly structured as we needed them to be. (Step 6 just shows us creating the database driver since we import a database driver - but it has nothing specific to PureORM).
128
384
 
129
- Lets move to a different example to show off another aspect of
130
- **`BaseBo.createOneFromDatabase`**: how it handles flattening data. Lets say
131
- there are three tags for article being retrieved, rather than the data
132
- being an array of 3 results with article repeated, the result is
133
- a nice Article BO with the tags nested in it.
385
+ **If you want to use PureORM in the more integrated way:**
134
386
 
135
387
  ```javascript
136
- getBySlug(slug) {
137
- const query = `
138
- SELECT
139
- ${Article.getSQLSelectClause()},
140
- ${Person.getSQLSelectClause()},
141
- ${ArticleTag.getSQLSelectClause()},
142
- ${Tag.getSQLSelectClause()}
143
- FROM article
144
- JOIN person
145
- ON article.author_id = person.id
146
- LEFT JOIN article_tags
147
- ON article.id = article_tags.article_id
148
- LEFT JOIN tag
149
- ON article_tags.tag_id = tag.id
150
- WHERE article.slug = $(slug);
151
- `;
152
- return this.one(query, { slug });
388
+ // ./dao/person.js
389
+ const BaseDAO = require('../dao/base');
390
+ const Person = require('../business-objects/person');
391
+ class PersonDAO extends BaseDAO {
392
+ Bo = Person;
393
+ get({ id }) {
394
+ const query = `
395
+ SELECT
396
+ ${Person.getSQLSelectClause()},
397
+ ${Job.getSQLSelectClause()},
398
+ ${Employer.getSQLSelectClause()}
399
+ FROM person
400
+ JOIN job on person.id = job.person_id
401
+ JOIN employer on job.employer_id = employer.id
402
+ WHERE id = $(id)
403
+ `;
404
+ return this.one(query, { id });
405
+ }
153
406
  }
154
- // OUTPUT: Article { person: Person, articleTags: Array<ArticleTag> }
407
+ module.exports = new PersonDAO();
155
408
  ```
156
409
 
157
410
  Notice that we're using `this.one`, which is what we want. The DAO methods for `one`, `oneOrNone`, `many`, `any` ensure their count against the number of generated top level business objects - not the number of rows the sql expression returns!
158
411
 
159
- Lets say we want to get more than one article. We can make slug an array, and
160
- **`BaseBo.createFromDatabase`** handles it seemlessly, giving us an Articles
161
- collections
412
+ ### Step 5: Creating the BaseDAO
162
413
 
163
- ```diff
164
- -getBySlug(slug) {
165
- +getBySlugs(slugs) {
166
- const query = `
167
- SELECT
168
- ${Article.getSQLSelectClause()},
169
- ${Person.getSQLSelectClause()},
170
- ${ArticleTag.getSQLSelectClause()},
171
- ${Tag.getSQLSelectClause()}
172
- FROM article
173
- JOIN person
174
- ON article.author_id = person.id
175
- LEFT JOIN article_tags
176
- ON article.id = article_tags.article_id
177
- LEFT JOIN tag
178
- ON article_tags.tag_id = tag.id
179
- - WHERE article.slug = $(slug);
180
- + WHERE article.slug in ($(slugs:csv));
181
- `;
182
- - return this.one(query, { slugs });
183
- + return this.many(query, { slugs });
414
+ Our PersonDAO extends BaseDAO, so lets create that by passing our database driver and optioanlly an error logger.
415
+
416
+ ```javascript
417
+ // ./dao/base.js
418
+ const { db } = require('../factories/db');
419
+ const constructor = createBaseDAO({ db, logError: console.log.bind(console) });
420
+ ```
421
+
422
+ ### Step 6: Creating Database Driver Instance
423
+
424
+ The last step is creating the datebase driver. Let's create a `factories` directory, and add this there.
425
+
426
+ ```javascript
427
+ // ./factories/db.js
428
+ const pgPromise = require('pg-promise');
429
+ const pgp = pgPromise();
430
+ module.exports = pgp({
431
+ host: process.env.DB_HOSTNAME,
432
+ port: process.env.DB_PORT,
433
+ database: process.env.DB_NAME,
434
+ user: DB_USERNAME,
435
+ password: DB_PASSWORD
436
+ });
437
+ ```
438
+
439
+ That's it! Our example controller code now works! The DAO get method returns a properly structured business object as we desire.
440
+
441
+ ## FAQ
442
+
443
+ ### Can you show a more complex business object and collection?
444
+
445
+ ```javascript
446
+ // ./bo/library.js
447
+ const Libraries = require('./libraries');
448
+ class Library extends Base {
449
+ get BoCollection() {
450
+ return Libraries;
451
+ }
452
+ static get tableName() {
453
+ return 'library_v2';
454
+ }
455
+ static get displayName() {
456
+ // If we didn't provide this static field, in javascript an object
457
+ // referencing a library bo would use `libraryV2` as the property name.
458
+ return 'library';
459
+ }
460
+ static get sqlColumnsData() {
461
+ return [
462
+ 'id',
463
+ 'name',
464
+ { column: 'is_ala_member', property: 'isALAMember' },
465
+ { column: 'address', references: Address }
466
+ ];
467
+ }
184
468
  }
185
- -// OUTPUT: Article { person: Person, articleTags: Array<ArticleTag> }
186
- +// OUTPUT: Articles[
187
- +// Article { person: Person, articleTags: Array<ArticleTag> }
188
- +// Article { person: Person, articleTags: Array<ArticleTag> }
189
- +// ]
190
469
  ```
191
470
 
192
- Lastly, lets switch gears one more time to see how meta data can be intertwined. Prefix the value as `meta_` and it will be passed through to the business object.
471
+ ```javascript
472
+ // ./bo/libraries.js
473
+ class Library extends Base {
474
+ static get Bo() {
475
+ return require('./person'); // eslint-disable-line
476
+ }
477
+ static get displayName() {
478
+ // If we didn't provide this static field, in javascript an object
479
+ // referencing this collection would use `librarys` as the property name.
480
+ return 'libraries';
481
+ }
482
+ aCollectionMethod() {}
483
+ anotherCollectionMethod() {}
484
+ }
485
+ ```
486
+
487
+ ### If I use PureORM do I have to re-invent all the super basic CRUD methods?
488
+
489
+ If you're using PureORM in the strict usage, then yes - it's scope is limited to a business object's mapping relational result rows to pure objects. If you are using PureORM in the more integrated usage extending the BaseDAO class, then you receive a handful of convenience methods for basic CRUD operations. At any point, you could override these convenience methods.
490
+
491
+ ### Whare are the tradeoffs that PureORM makes in using SQL instead of a query builder API?
492
+
493
+ Traditional/stateful ORMs offer a dialetic-generic, chainable object api for expressing underlying SQL - thus solving for database "lock-in" as well the inability of string queries compose easily. PureORM takes the approach that the tradeoff of developers having to learn the huge surface area of of a query builder, and having to map the complexity and nuance of SQL to it, are simply not worth the cost, and so is premised on not using a query building library. PureORM sees writing straight SQL heaviliy as a feature, not a defect needing solved, and not eclipsed by the composibility of a query builder.
494
+
495
+ ### Will I then have dozens of similar DAO methods, since strings aren't as composable as stateful ORM builder builder APIs?
496
+
497
+ There is still a lot of composibility possible with functions returning strings (someone create an Issue if you want to see examples used in the Kujo codebase), but in general yes, there is more repitition. Most of this remaining repitition is not something I think is a defect (though those obsessed with DRY would disagree). The only "defect" of this repitition is that there may be more than one similiar method (for example a "get" that does certain joins vs others), and differentiating the large query in a function name can be lengthy/annoying. In these cases where composing functions doesn't bring the number of similar functions methods to only one, rather than distilling these large queries into the function name (eg, getPersonWithJobsAndEmployers), I usually just opt for a small arbitrary hash at the end of the short name (eg, getXTW instead of getPersonWithJobsAndEmployers, getRJF instead of getPersonWithFriendsLocatedNearANewFriendRequest, etc).
498
+
499
+ ### Does PureORM abstract away the database driver?
500
+
501
+ No, the whole premise of PureORM is to offer a library to aid the use of writing SQL. There are [two ways](#ways-of-using-pureorm) to use PureORM. One abstracts over the database driver for convenience, but the datebase driver is always available to you (at `this.db`) if you wish to use it directly with no PureORM mappings. The other way of using PureORM doesn't abstract over the database driver at all (using PureORM to map the results is "opt-in") and the database driver is thus always used directly.
502
+
503
+ ### Can I use aggregate functions while still using the PureORM mapping?
504
+
505
+ Yes, if you'd like to get the mapping while also passing through some select expressions, use the special meta prefix. For example:
193
506
 
194
507
  ```javascript
195
- getBloggerPayout(id, startDate, endDate) {
508
+ getBloggerPayout({id, startDate, endDate}) {
196
509
  const query = `
197
510
  SELECT
198
511
  ${Person.getSQLSelectClause()},
199
- COALESCE(SUM(article.blogger_payout), 0) as meta_amount,
512
+ COALESCE(SUM(article.blogger_payout), 0) as meta_amount
200
513
  FROM
201
514
  person
202
515
  LEFT JOIN article
@@ -213,63 +526,22 @@ getBloggerPayout(id, startDate, endDate) {
213
526
  }
214
527
  ```
215
528
 
216
- ### Business Object Usage
217
-
218
- Now lets look at our business logic layer where we use the DAO to get/persist pure data. (This example uses the few included common DAO methods in order to show something. However, in practice you'll mainly be using your own custom functions with your own SQL to do your own interesting things; vs this contrived and basic example.)
219
-
220
- ```javascript
221
- let raw = new Person({
222
- email: 'foobar@gmail.com',
223
- firstName: 'craig',
224
- lastName: 'martin'
225
- });
226
-
227
- const personDAO = new PersonDAO({ db });
228
-
229
- // Returns a person business object with the persisted data
230
- let person = await personDAO.create(raw);
231
-
232
- person.email = 'craigmartin@gmail.com';
233
-
234
- // Returns a person business object with the updated persisted data
235
- person = await personDAO.update(person);
236
-
237
- // Gets or creates a person business object
238
- same = await personDAO.getOrCreate(raw);
239
- same.id === person.id; // true
240
-
241
- // Returns the person business object which matches this data
242
- same = await personDAO.getMatching(
243
- new Person({ email: 'craigmartin@gmail.com' })
244
- );
245
- same.id === person.id; // true
246
-
247
- // Deletes the person data form the database
248
- await personDAO.delete(person);
249
- ```
250
-
251
- To see everything in action, check out [the examples directory](https://github.com/craigmichaelmartin/pure-orm/tree/master/examples) and the [tests](https://github.com/craigmichaelmartin/pure-orm/blob/master/src/bo/base-bo.spec.js).
252
-
253
- ---
254
-
255
529
  ## Comparisons
256
530
 
257
531
  Low Level Abstractions
258
532
 
259
- - **Database Drivers** (eg [node-postgres](https://github.com/brianc/node-postgres), [mysql](https://github.com/mysqljs/mysql), [node-sqlite3](https://github.com/mapbox/node-sqlite3)) - These are powerful low level libraries that handle connecting to a database, executing raw SQL, and returning raw rows. All the higher level abstractions are built on these. `PureORM` like "stateful ORMs" are built on these.
533
+ - **Database Drivers** (eg [node-postgres](https://github.com/brianc/node-postgres), [mysql](https://github.com/mysqljs/mysql), [node-sqlite3](https://github.com/mapbox/node-sqlite3), [node-mssql](https://github.com/tediousjs/node-mssql), [node-oracledb](https://github.com/oracle/node-oracledb)) - These are powerful low level libraries that handle connecting to a database, executing raw SQL, and returning raw rows. All the higher level abstractions are built on these. `PureORM` like "stateful ORMs" are built on these.
260
534
 
261
535
  Stateful ORMs (comprised of two portions)
262
536
 
263
- - **Query Builders** (eg [knex](https://github.com/tgriesser/knex)) - These (built on database drivers) offer a dialetic-generic, chainable object api for expressing underlying SQL - thus solving for database "lock-in" as well the inability to compose SQL queriers as strings. `pure-orm` takes the approach that the tradeoff of developers having to learn the huge surface area of dialetic-generic api, and having to map the complexity and nuance of SQL to it, are simply not worth the cost, and so does not use a query building library. With `pure-orm` you just write SQL. The tradeoff on `pure-orms` side that is indeed being tied to a sql dialect and in the inability to compose sql expressions (strings don't compose nicely). Yet all this considered, `pure-orm` sees writing straight SQL heaviliy as a feature, not a defect needing solved, and not eclipsed by the composibility of a query builder.
537
+ - **Query Builders** (eg [knex](https://github.com/tgriesser/knex)) - These (built on database drivers) offer a dialetic-generic, chainable object api for expressing underlying SQL - thus solving for database "lock-in" as well the inability to compose SQL queriers as strings. PureORM takes the approach that the tradeoff of developers having to learn the huge surface area of dialetic-generic api, and having to map the complexity and nuance of SQL to it, are simply not worth the cost, and so does not use a query building library. With PureORM you just write SQL. The tradeoff on PureORM side that is indeed being tied to a sql dialect and in the inability to compose sql expressions (strings don't compose nicely). Yet all this considered, PureORM sees writing straight SQL heaviliy as a feature, not a defect needing solved, and not eclipsed by the composibility of a query builder.
264
538
 
265
- - **Stateful, Database Aware Objects** (eg [sequelize](https://github.com/sequelize/sequelize), [waterline](https://github.com/balderdashy/waterline), [bookshelf](https://github.com/bookshelf/bookshelf), [typeorm](https://github.com/typeorm/typeorm)) - These stateful, database-aware object libraries are the full embrace of "Stateful ORMs". Contrary to this these is `pure-orm` which yields pure, un-attached, structured objects.
539
+ - **Stateful, Database Aware Objects** (eg [sequelize](https://github.com/sequelize/sequelize), [waterline](https://github.com/balderdashy/waterline), [bookshelf](https://github.com/bookshelf/bookshelf), [typeorm](https://github.com/typeorm/typeorm)) - These stateful, database-aware object libraries are the full embrace of "Stateful ORMs". Contrary to this these is PureORM which yields pure, un-attached, structured objects.
266
540
 
267
541
  PureORM
268
542
 
269
- - `pure-orm` is more than just the preference against the query builder portion of Stateful ORMs
270
- - `pure-orm` is the preference against stateful, db-connected objects: `pure-orm` resolves result rows to _pure_ business objects. This purity in business objects fosters a clean layer of the business layer from the data access layer, as well as ensuring the very best in performance (eg, the [N+1 problem](https://docs.sqlalchemy.org/en/13/glossary.html#term-n-plus-one-problem) can't exist with pure objects).
271
-
272
- ---
543
+ - PureORM is more than just the preference against the query builder portion of Stateful ORMs
544
+ - PureORM is the preference against stateful, db-connected objects: PureORM resolves result rows to _pure_ business objects. This purity in business objects fosters a clean layer of the business layer from the data access layer, as well as ensuring the very best in performance (eg, the [N+1 problem](https://docs.sqlalchemy.org/en/13/glossary.html#term-n-plus-one-problem) can't exist with pure objects).
273
545
 
274
546
  ## API
275
547
 
@@ -281,26 +553,29 @@ An abstract class which is the base class your BO classes to extend.
281
553
 
282
554
  **Abstract Methods** to be implemented
283
555
 
284
- - `get BoCollection(): BoCollection` - Returns the business object collection class constructor.
285
556
  - `static get tableName(): string` - Returns the string table name which the business object associates with from the database.
286
557
  - `static get sqlColumnsData(): Array<string|ColumnData>` - Returns an array of the database column data. The type is either:
287
- - `ColumnData {column, property?, references?, primaryKey?, transform?}`
558
+ - `ColumnData {column, property?, references?, primaryKey?}`
288
559
  - `column: string` - The sql column name
289
560
  - `propery: string` - The javascript property name for this column (defaults to camelCase of `column`)
290
561
  - `references: Bo` - The relationship to another Bo (defaults to null)
291
562
  - `primaryKey: boolean` - Is this column (part of) the primary key (defaults to false)
292
- - `transform: fn` - When this data is pulled, a transform that runs on it; eg, creating a momentjs object for dates (defaults to `() => {}`)
293
563
  - `string` - If a string, it is applied as the `column` value, with all others defaulted.
294
564
  - (Note: if there is no primary key, `id` is defaulted)
295
565
 
296
566
  Optional
297
567
 
568
+ - `get BoCollection(): BoCollection` - Returns the business object collection class constructor.
298
569
  - `static get displayName(): string` - Returns the string display name of the business object (defaults to camelcase of tableName)
299
570
 
300
571
  **Public Methods**
301
572
 
302
- - `constructor(props: object)`
303
- - and more
573
+ - `createOneFromDatabase(relationalResultRows)` - Returns the properly structured Bo object (asserts one).
574
+ - `createOneOrNoneFromDatabase(relationalResultRows)` - Returns the properly structured Bo object if results (asserts one or none).
575
+ - `createManyFromDatabase(relationalResultRows)` - Returns the properly structured Bo objects (asserts many).
576
+ - `createFromDatabase(relationalResultRows)` - Returns any properly structured Bo objects (no assertion on count).
577
+
578
+ (Note these create from database methods ensure their count against the number of generated top level business objects - not the number of relational rows used!)
304
579
 
305
580
  #### `BaseBoCollection`
306
581
 
@@ -316,8 +591,8 @@ Optional
316
591
 
317
592
  **Public Methods**
318
593
 
319
- - `constructor(props: object)`
320
- - and more
594
+ - `filter(predicate)`
595
+ - and some advanced methods
321
596
 
322
597
  #### `BaseDAO`
323
598
 
@@ -326,7 +601,6 @@ The base class your DAO classes extend.
326
601
  **Abstract Methods** to be implemented
327
602
 
328
603
  - `get Bo(): BO` - Returns the business object class constructor.
329
- - `get BoCollection(): BO` - Returns the collection business object class constructor.
330
604
 
331
605
  **Public Methods**
332
606
 
@@ -340,7 +614,7 @@ Abstractions over `pg-promise`'s query methods:
340
614
  - `any(query: string, params: object)` - executes a query and returns a BoCollection.
341
615
  - `none(query: string, params: object)` - executes a query and returns null.
342
616
 
343
- (Note, these methods assert the correct number on the created BO's - not the raw postgres sql result. Thus, for example, `one` understands that there may be multiple result rows (which pg-promise's `one` would throw at) but which could correctly nest into one BO.)
617
+ (Note these create from database methods ensure their count against the number of generated top level business objects are created - not the number of relational rows returned from the database driver! Thus, for example, `one` understands that there may be multiple result rows (which a database driver's `one` query method would throw at) but which correctly nest into one BO.)
344
618
 
345
619
  Built-in "basic" / generic functions which your extending DAO class instance gets for free
346
620
 
@@ -361,7 +635,7 @@ These are just provided because they are so common and straight-forward. However
361
635
 
362
636
  **Parameters**
363
637
 
364
- - `getBusinessObjects: () => Array<BusinessObject>` - A function which returns an array of all the business objects, used to construct joined row data in the business object.
638
+ - `getBusinessObjects: () => Array<BusinessObject>` - A function which returns an array of all the business objects. In order for a business model to properly structure/nest data which could include any other business object, each business object needs to know about every other business object. We accomplish this by accepting this function which returns an array of all the business objects. (There is of course a circular dependency with the base class needing all classes that will end up extending it, but using this function handles this gracefully).
365
639
 
366
640
  **Return Value**
367
641
 
@@ -372,19 +646,17 @@ These are just provided because they are so common and straight-forward. However
372
646
  **Parameters**
373
647
 
374
648
  - `logError: function`
375
- - `db: pg-promise database`
649
+ - `db: (database driver)`
376
650
 
377
651
  **Return Value**
378
652
 
379
- - The BaseDAO class to extend for your business objects.
380
-
381
- ---
653
+ - The BaseDAO class to extend for your dao classes.
382
654
 
383
655
  ## Current Status
384
656
 
385
657
  #### Current Limitations (PRs welcome!)
386
658
 
387
- - `pg-promise`/`node-postgres` is the only database driver supported. There is not technical reason for this, other than that the project I'm using has a postgres database and so I only had `node-postgres` in mind. It would be great if `pure-orm` was database driver agnostic.
659
+ - `pg-promise`/`node-postgres` is the only database driver supported out-of-the-box for the integrated DAO usage path. There is not technical reason for this, other than that the project I'm using has a postgres database and so I only had `pg-promise` in mind. We could support more database drivers out of the box.
388
660
  - the dao you are writing your sql in must always be in the "select" and must be the one you want as your root(s) return objects
389
661
  - the query can start from some other table, and join a bunch of times to get there, though
390
662
  - there must be a clear path in the "select" to your leaf joined-to-entities (eg, (Good): Article, ArticleTag, Tag, TagModerator, Moderator; not (Bad): Article, Moderator).
@@ -396,7 +668,6 @@ These are just provided because they are so common and straight-forward. However
396
668
  - Add more tests
397
669
  - Known Bug: if a table references the same table twice, the first one is found as the nodePointingToIt and so ends up throwing.
398
670
  - ideally the fix to this will change the behavior of when a table points to another table by another name (author_id -> person)
399
- - Think about how to handle the none case of oneOrNone, any, and none
400
671
 
401
672
  #### Is it production ready?
402
673