pure-orm 2.2.0 → 4.0.0-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 (135) hide show
  1. package/.eslintrc.json +2 -2
  2. package/README.md +169 -242
  3. package/examples/basic/bo/person.js +4 -3
  4. package/examples/basic/dao/person.js +1 -2
  5. package/examples/basic/db.js +6 -15
  6. package/examples/basic/orm.js +8 -0
  7. package/examples/blog/bo/article.js +5 -2
  8. package/examples/blog/bo/article_tag.js +5 -2
  9. package/examples/blog/bo/article_tags.js +4 -3
  10. package/examples/blog/bo/articles.js +4 -3
  11. package/examples/blog/bo/person.js +4 -2
  12. package/examples/blog/bo/tag.js +4 -2
  13. package/examples/blog/business-objects.js +1 -10
  14. package/examples/blog/orm.js +14 -0
  15. package/examples/order/bo/line-item.js +5 -2
  16. package/examples/order/bo/line-items.js +4 -3
  17. package/examples/order/bo/order.js +5 -2
  18. package/examples/order/bo/orders.js +4 -3
  19. package/examples/order/bo/product-variant.js +5 -2
  20. package/examples/order/bo/product-variants.js +4 -3
  21. package/examples/order/bo/product.js +5 -2
  22. package/examples/order/bo/products.js +4 -3
  23. package/examples/order/bo/utm-source.js +4 -2
  24. package/examples/order/orm.js +20 -0
  25. package/examples/order-more/bo/actual-product-variant.js +5 -2
  26. package/examples/order-more/bo/actual-product-variants.js +4 -3
  27. package/examples/order-more/bo/color.js +5 -2
  28. package/examples/order-more/bo/colors.js +4 -3
  29. package/examples/order-more/bo/customer.js +5 -2
  30. package/examples/order-more/bo/customers.js +4 -3
  31. package/examples/order-more/bo/gender.js +5 -2
  32. package/examples/order-more/bo/genders.js +4 -3
  33. package/examples/order-more/bo/inventory-level.js +5 -2
  34. package/examples/order-more/bo/inventory-levels.js +4 -3
  35. package/examples/order-more/bo/line-item.js +5 -2
  36. package/examples/order-more/bo/line-items.js +4 -3
  37. package/examples/order-more/bo/order.js +5 -2
  38. package/examples/order-more/bo/orders.js +4 -3
  39. package/examples/order-more/bo/parcel-event.js +5 -2
  40. package/examples/order-more/bo/parcel-events.js +4 -3
  41. package/examples/order-more/bo/parcel-line-item.js +5 -2
  42. package/examples/order-more/bo/parcel-line-items.js +4 -3
  43. package/examples/order-more/bo/parcel.js +5 -2
  44. package/examples/order-more/bo/parcels.js +4 -3
  45. package/examples/order-more/bo/physical-address.js +5 -2
  46. package/examples/order-more/bo/physical-addresses.js +4 -3
  47. package/examples/order-more/bo/product-variant-image.js +5 -2
  48. package/examples/order-more/bo/product-variant-images.js +4 -3
  49. package/examples/order-more/bo/product-variant.js +5 -2
  50. package/examples/order-more/bo/product-variants.js +4 -3
  51. package/examples/order-more/bo/product.js +5 -2
  52. package/examples/order-more/bo/products.js +4 -3
  53. package/examples/order-more/bo/refund.js +5 -2
  54. package/examples/order-more/bo/refunds.js +4 -3
  55. package/examples/order-more/bo/shipment-actual-product-variant.js +5 -2
  56. package/examples/order-more/bo/shipment-actual-product-variants.js +4 -3
  57. package/examples/order-more/bo/shipment.js +5 -2
  58. package/examples/order-more/bo/shipments.js +4 -3
  59. package/examples/order-more/bo/size.js +5 -2
  60. package/examples/order-more/bo/sizes.js +4 -3
  61. package/examples/order-more/bo/utm-medium.js +4 -2
  62. package/examples/order-more/bo/utm-source.js +4 -2
  63. package/examples/order-more/orm.js +49 -0
  64. package/package.json +4 -2
  65. package/src/bo.js +393 -0
  66. package/src/{bo/base-bo.spec.js → bo.spec.js} +73 -37
  67. package/src/factory.js +167 -0
  68. package/src/factory.spec.js +11 -0
  69. package/src/index.js +49 -6
  70. package/test-utils/five/bo/line-item.js +5 -2
  71. package/test-utils/five/bo/line-items.js +4 -3
  72. package/test-utils/five/bo/order.js +5 -2
  73. package/test-utils/five/bo/orders.js +4 -3
  74. package/test-utils/five/bo/parcel-event.js +5 -2
  75. package/test-utils/five/bo/parcel-events.js +4 -3
  76. package/test-utils/five/bo/parcel-line-item.js +5 -2
  77. package/test-utils/five/bo/parcel-line-items.js +4 -3
  78. package/test-utils/five/bo/parcel.js +5 -2
  79. package/test-utils/five/bo/parcels.js +4 -3
  80. package/test-utils/five/orm.js +19 -0
  81. package/test-utils/nine/bo/feature-switch.js +5 -2
  82. package/test-utils/nine/bo/feature-switches.js +4 -3
  83. package/test-utils/nine/orm.js +9 -0
  84. package/test-utils/six/bo/customer.js +5 -2
  85. package/test-utils/six/bo/customers.js +4 -3
  86. package/test-utils/six/bo/line-item.js +5 -2
  87. package/test-utils/six/bo/line-items.js +4 -3
  88. package/test-utils/six/bo/order.js +5 -2
  89. package/test-utils/six/bo/orders.js +4 -3
  90. package/test-utils/six/bo/parcel-line-item.js +5 -2
  91. package/test-utils/six/bo/parcel-line-items.js +4 -3
  92. package/test-utils/six/bo/parcel.js +5 -2
  93. package/test-utils/six/bo/parcels.js +4 -3
  94. package/test-utils/six/orm.js +19 -0
  95. package/test-utils/thirteen/bo/audience.js +21 -0
  96. package/test-utils/thirteen/bo/audiences.js +10 -0
  97. package/{examples/basic → test-utils/thirteen}/bo/base.js +0 -0
  98. package/test-utils/thirteen/bo/brand.js +21 -0
  99. package/test-utils/thirteen/bo/brands.js +10 -0
  100. package/test-utils/thirteen/bo/categories.js +13 -0
  101. package/test-utils/thirteen/bo/category.js +21 -0
  102. package/test-utils/thirteen/bo/member.js +21 -0
  103. package/test-utils/thirteen/bo/members.js +10 -0
  104. package/test-utils/thirteen/bo/passion.js +21 -0
  105. package/test-utils/thirteen/bo/passions.js +10 -0
  106. package/test-utils/thirteen/bo/product.js +22 -0
  107. package/test-utils/thirteen/bo/products.js +10 -0
  108. package/test-utils/thirteen/bo/recommendation-audience.js +27 -0
  109. package/test-utils/thirteen/bo/recommendation-audiences.js +10 -0
  110. package/test-utils/thirteen/bo/recommendation.js +33 -0
  111. package/test-utils/thirteen/bo/recommendations.js +10 -0
  112. package/test-utils/thirteen/orm.js +25 -0
  113. package/test-utils/thirteen/results.json +74 -0
  114. package/test-utils/twelve/bo/member.js +5 -2
  115. package/test-utils/twelve/bo/members.js +4 -3
  116. package/test-utils/twelve/bo/prompt.js +5 -2
  117. package/test-utils/twelve/bo/prompts.js +4 -3
  118. package/test-utils/twelve/orm.js +10 -0
  119. package/examples/basic/business-objects.js +0 -9
  120. package/examples/basic/dao/base.js +0 -9
  121. package/examples/blog/bo/base.js +0 -5
  122. package/examples/order/bo/base.js +0 -5
  123. package/examples/order/business-objects.js +0 -11
  124. package/examples/order-more/bo/base.js +0 -5
  125. package/examples/order-more/business-objects.js +0 -26
  126. package/src/bo/base-bo-collection.js +0 -15
  127. package/src/bo/base-bo.js +0 -379
  128. package/src/dao/base-dao.js +0 -146
  129. package/src/dao/base-dao.spec.js +0 -6
  130. package/src/util/helpers.js +0 -28
  131. package/src/util/helpers.spec.js +0 -6
  132. package/test-utils/five/business-objects.js +0 -11
  133. package/test-utils/nine/business-objects.js +0 -7
  134. package/test-utils/six/business-objects.js +0 -11
  135. package/test-utils/twelve/business-objects.js +0 -8
package/README.md CHANGED
@@ -25,9 +25,7 @@ The name _**pure**ORM_ reflects both of these points - that it is _pure_ ORM (th
25
25
 
26
26
  #### Philosophy
27
27
 
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.
29
- - Have _database-connected_ "data access objects" which allow the unobstructed writing of normal SQL.
30
- - Have the "data access objects" returning the pure business objects.
28
+ - Write _native_, _unobstructed_ SQL in a "data access layer" which returns _pure_ "business objects" to be used in the app's business logic.
31
29
 
32
30
  #### Concepts
33
31
 
@@ -45,41 +43,41 @@ A **Business Object Collection** (BO Collection) is a group of pure javascript o
45
43
  - If your query returns records for multiple business objects, a BO Collection will be created and returned.
46
44
  - 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).
47
45
 
48
- A **Data Access Object** (DAO) is a database-aware abstraction layer where native SQL is written.
46
+ A **Data Access Layer** (DAL) is a database-aware abstraction layer where native SQL is written.
49
47
 
50
48
  - This is not an "expresion language" or "query builder". There are not hundreds of methods mapping the complexity, expressiveness, and nuance of SQL to class objects.
51
49
  - Rather, is a data access layer in which native SQL is written, and which returns business objects (properly nested and structured).
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).
53
50
 
54
51
  ## Quick Example
55
52
 
56
53
  Using PureORM allows you to write code like this:
57
54
 
58
55
  ```javascript
59
- class PersonDAO extends BaseDAO {
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
- }
73
- }
74
- ```
75
-
76
- And use it like this:
56
+ // ./dal/person.js
57
+ const getPerson = ({ id }) => {
58
+ const query = `
59
+ SELECT
60
+ ${orm.tables.person.columns},
61
+ ${orm.tables.job.columns},
62
+ ${orm.tables.employer.columns}
63
+ FROM person
64
+ JOIN job on person.id = job.person_id
65
+ JOIN employer on job.employer_id = employer.id
66
+ WHERE id = $(id)
67
+ `;
68
+ return orm.one(query, { id });
69
+ };
77
70
 
78
- ```javascript
79
- const person = await personDAO.get({ id: 55 });
80
- console.log(person);
71
+ // ./controllers/rest/person.js
72
+ const { getPerson } = require('./dal/person');
73
+ const get = (req, res) => {
74
+ const person = await getPerson({ id: req.params.id });
75
+ res.json(person);
76
+ };
81
77
  ```
82
78
 
79
+ Which a GET to that controller would return
80
+
83
81
  ```javascript
84
82
  Person {
85
83
  id: 55,
@@ -117,62 +115,52 @@ Person {
117
115
 
118
116
  ### Things to Note:
119
117
 
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:
118
+ - Our DAL function returns a single Person business object which is properly structured from the many relational row records!
119
+ - Our query is executed with a `one` method. The ORM 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!
120
+ - Rather than manually specifying our columns in the sql select expression, we used the orm's getter for columns. 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 this convenience if you don't mind the tedium:
123
121
  ```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
- }
122
+ // ./dal/person.js
123
+ const getPerson = ({ id }) => {
124
+ // Example showing you can manually specify the select expression fields
125
+ // instead of using the orm's columns getter.
126
+ // Note: you must namespace the field with table name and hashtag.
127
+ const query = `
128
+ SELECT
129
+ person.id as "person#id",
130
+ person.name as "person#name",
131
+ job.id as "job#id",
132
+ job.person_id: "job#person_id",
133
+ job.employer_id: "job#employer_id",
134
+ job.start_date: "job#start_date",
135
+ job.end_date: "job#end_date",
136
+ employer.id as "employer#id",
137
+ employer.name as "employer#name"
138
+ FROM person
139
+ JOIN job on person.id = job.person_id
140
+ JOIN employer on job.employer_id = employer.id
141
+ WHERE id = $(id)
142
+ `;
143
+ return orm.one(query, { id });
144
+ };
148
145
  ```
149
146
 
150
147
  ## Usage
151
148
 
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.
149
+ The exports you'll use from pure-orm:
157
150
 
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.
151
+ - `create` is a factory to create your ORM. This factory function accepts an object with two properties.
152
+ - `getBusinessObjects` which is a function that returns an array of all your business object classes (where each business object must implement static `table` and `sqlColumnsData` fields.
153
+ - `db` which is an instance of the database driver.
166
154
 
167
155
  ## Practical Example
168
156
 
169
- Lets take a practical example to see all this in action. Lets fill in the backend for a tiny web page renderer.
157
+ Lets take a practical example to see all this in action. Lets fill in the backend for a tiny rest server for a person.
170
158
 
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.
159
+ Lets say we have a database with three tables: person, job, and employer. We want our rest server to return an payload like this for requests which the get method receive.
172
160
 
173
161
  ```javascript
174
- // ./controllers/pages/person.js
175
- const renderProfile = (req, res) => {
162
+ // ./controllers/rest/person.js
163
+ const get = (req, res) => {
176
164
  const person = {
177
165
  id: 55,
178
166
  name: 'John Doe',
@@ -203,7 +191,7 @@ const renderProfile = (req, res) => {
203
191
  ]
204
192
  }
205
193
  };
206
- res.render('profile.html', person);
194
+ res.json(person);
207
195
  };
208
196
  ```
209
197
 
@@ -212,7 +200,7 @@ Based on the tables, I know exactly how to query for this:
212
200
  ```sql
213
201
  SELECT *
214
202
  FROM person
215
- JOIN job on person.id = job.person_id
203
+ LEFT JOIN job on person.id = job.person_id
216
204
  JOIN employer on job.employer_id = employer.id
217
205
  WHERE id = 55;
218
206
  ```
@@ -221,7 +209,7 @@ I already know how to SQL, and don't want to spend the time mapping what I alrea
221
209
 
222
210
  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
211
 
224
- So, lets install PureOrm and the database driver and get started!
212
+ So, lets install PureORM and the database driver and get started!
225
213
 
226
214
  ### Step 1: Installing PureORM and the Database Driver
227
215
 
@@ -237,8 +225,8 @@ npm install --save pg-promise
237
225
  Lets remove our hardcoded example, and write our contoller code using functions we want to exist and will create.
238
226
 
239
227
  ```diff
240
- // ./controllers/pages/person.js
241
- +const personDAO = require('./dao/person');
228
+ // ./controllers/rest/person.js
229
+ +const { getPerson } = require('./dal/person');
242
230
  const renderProfile = (req, res) => {
243
231
  - const person = {
244
232
  - id: 55,
@@ -270,11 +258,11 @@ const renderProfile = (req, res) => {
270
258
  - ]
271
259
  - }
272
260
  - };
273
- + const person = personDAO.get({ id: req.params.id });
261
+ + const person = getPerson({ id: req.params.id });
274
262
  res.render('profile.html', person);
275
263
  ```
276
264
 
277
- This looks nice, now let's create the dao and bo necessary.
265
+ This looks nice, now let's create the necessary bo and function in the dal.
278
266
 
279
267
  ### Step 3: Creating the Business Objects
280
268
 
@@ -282,9 +270,7 @@ Let's create a `/bo` directory and the classes we want.
282
270
 
283
271
  ```javascript
284
272
  // ./bo/person.js
285
- const Base = require('./base');
286
-
287
- class Person extends Base {
273
+ class Person {
288
274
  static tableName = 'person';
289
275
  static sqlColumnsData = ['id', 'name'];
290
276
  // any other business methods...
@@ -294,11 +280,10 @@ module.exports = Person;
294
280
 
295
281
  ```javascript
296
282
  // ./bo/job.js
297
- const Base = require('./base');
298
283
  const Person = require('./person');
299
284
  const Employer = require('./employer');
300
285
 
301
- class Job extends Base {
286
+ class Job {
302
287
  static tableName = 'job';
303
288
  static sqlColumnsData = [
304
289
  'id',
@@ -314,9 +299,7 @@ module.exports = Job;
314
299
 
315
300
  ```javascript
316
301
  // ./bo/employer.js
317
- const Base = require('./base');
318
-
319
- class Employer extends Base {
302
+ class Employer {
320
303
  static tableName = 'employer';
321
304
  static sqlColumnsData = ['id', 'name'];
322
305
  // any other business methods...
@@ -329,99 +312,50 @@ We've not got our three business object classes. To review, each business object
329
312
  - A static `tableName` property to denote which table results this class is for.
330
313
  - A static `sqlColumnsData` property to enumerate the table columns.
331
314
 
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.)
315
+ ### Step 4: Creating our ORM
333
316
 
334
317
  ```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
- ]
318
+ // ./factories/orm.js
319
+ const db = require('./db');
320
+ const { create } = require('pure-orm');
321
+ const Person = require('./person');
322
+ const Job = require('./job');
323
+ const Employer = require('./employer');
324
+
325
+ const orm = create({
326
+ db,
327
+ getBusinessObjects: () => [Person, Job, Employer]
344
328
  });
345
329
  module.exports = BaseBO;
346
330
  ```
347
331
 
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.
332
+ ### Step 4: Creating the DAL function
357
333
 
358
334
  ```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
- }
377
- }
378
- module.exports = new PersonDAO();
379
- ```
380
-
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).
384
-
385
- **If you want to use PureORM in the more integrated way:**
335
+ // ./dal/person.js
336
+ const orm = require('../factories/orm');
386
337
 
387
- ```javascript
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
- }
338
+ const getPerson({ id }) {
339
+ const query = `
340
+ SELECT
341
+ ${Person.getSQLSelectClause()},
342
+ ${Job.getSQLSelectClause()},
343
+ ${Employer.getSQLSelectClause()}
344
+ FROM person
345
+ JOIN job on person.id = job.person_id
346
+ JOIN employer on job.employer_id = employer.id
347
+ WHERE id = $(id)
348
+ `;
349
+ return orm.one(query, { id });
406
350
  }
407
- module.exports = new PersonDAO();
351
+ module.exports = getPerson;
408
352
  ```
409
353
 
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!
411
-
412
- ### Step 5: Creating the BaseDAO
413
-
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
- ```
354
+ Notice that we're using `orm.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!
421
355
 
422
- ### Step 6: Creating Database Driver Instance
356
+ ### Step 6: Creating the Database Driver Instance
423
357
 
424
- The last step is creating the datebase driver. Let's create a `factories` directory, and add this there.
358
+ The last step is creating the datebase driver, that we had imported up in `./factories/orm`.
425
359
 
426
360
  ```javascript
427
361
  // ./factories/db.js
@@ -431,12 +365,12 @@ module.exports = pgp({
431
365
  host: process.env.DB_HOSTNAME,
432
366
  port: process.env.DB_PORT,
433
367
  database: process.env.DB_NAME,
434
- user: DB_USERNAME,
435
- password: DB_PASSWORD
368
+ user: process.env.DB_USERNAME,
369
+ password: process.env.DB_PASSWORD
436
370
  });
437
371
  ```
438
372
 
439
- That's it! Our example controller code now works! The DAO get method returns a properly structured business object as we desire.
373
+ That's it! Our example controller code now works! The `getPerson` function in our data access layer now returns a properly structured business object as we desire.
440
374
 
441
375
  ## FAQ
442
376
 
@@ -445,7 +379,7 @@ That's it! Our example controller code now works! The DAO get method returns a p
445
379
  ```javascript
446
380
  // ./bo/library.js
447
381
  const Libraries = require('./libraries');
448
- class Library extends Base {
382
+ class Library {
449
383
  get BoCollection() {
450
384
  return Libraries;
451
385
  }
@@ -465,12 +399,14 @@ class Library extends Base {
465
399
  { column: 'address', references: Address }
466
400
  ];
467
401
  }
402
+ aBussinessObjectMethod() {}
403
+ anotherBussinessObjectMethod() {}
468
404
  }
469
405
  ```
470
406
 
471
407
  ```javascript
472
408
  // ./bo/libraries.js
473
- class Library extends Base {
409
+ class Library {
474
410
  static get Bo() {
475
411
  return require('./person'); // eslint-disable-line
476
412
  }
@@ -486,26 +422,65 @@ class Library extends Base {
486
422
 
487
423
  ### If I use PureORM do I have to re-invent all the super basic CRUD methods?
488
424
 
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.
425
+ The goal of PureORM is to foster writing SQL and receiving pure business objects. That said, some SQL is so common that we preload the created ORM with with some basic CRUD operations.
426
+
427
+ For example, rather than every entity's DAL needing basic get, create, etc method, you can use:
428
+
429
+ ```javascript
430
+ // ./controllers/rest/person.js
431
+ const get = (req, res) => {
432
+ if (req.params.id) {
433
+ return res.json(await orm.get(new Person({ id })));
434
+ }
435
+ if (req.query.name) {
436
+ return res.json(
437
+ await orm.getAnyMatching(new Person({ name: req.query.name }))
438
+ );
439
+ }
440
+ res
441
+ .status(404)
442
+ .json({ error: 'Please specify an id or provide a name filter' });
443
+ };
444
+ ```
445
+
446
+ At any point you can ditch these built-ins and write some SQL in a DAL function.
447
+
448
+ ```javascript
449
+ // ./controllers/rest/person.js
450
+ const { getPerson, getPeopleWithName } = require('../../dal/person');
451
+ const get = (req, res) => {
452
+ if (req.params.id) {
453
+ return res.json(await getPerson(id));
454
+ }
455
+ if (req.query.name) {
456
+ return res.json(await getPeopleWithName(req.query.name));
457
+ }
458
+ res
459
+ .status(404)
460
+ .json({ error: 'Please specify an id or provide a name filter' });
461
+ };
462
+ ```
490
463
 
491
464
  ### Whare are the tradeoffs that PureORM makes in using SQL instead of a query builder API?
492
465
 
493
466
  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
467
 
495
- ### Will I then have dozens of similar DAO methods, since strings aren't as composable as stateful ORM builder builder APIs?
468
+ ### Will I then have dozens of similar DAL functions, since strings aren't as composable as stateful ORM builder builder APIs?
496
469
 
497
470
  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
471
 
499
472
  ### Does PureORM abstract away the database driver?
500
473
 
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.
474
+ No, the whole premise of PureORM is to offer a library to aid the use of writing SQL. The datebase driver is always available to you (at `orm.db`) if you wish to use it directly with no PureORM mappings, or just import it. In my experience, a small percentage of highly complex queries looking for sums or counts in my DAL use the database driver directly.
475
+
476
+ The only difference is how the SQL is invoked: `orm.one(query, {})` vs `orm.db.one(query, {})`
502
477
 
503
478
  ### Can I use aggregate functions while still using the PureORM mapping?
504
479
 
505
480
  Yes, if you'd like to get the mapping while also passing through some select expressions, use the special meta prefix. For example:
506
481
 
507
482
  ```javascript
508
- getBloggerPayout({id, startDate, endDate}) {
483
+ const getBloggerPayout = ({ id, startDate, endDate }) => {
509
484
  const query = `
510
485
  SELECT
511
486
  ${Person.getSQLSelectClause()},
@@ -522,8 +497,8 @@ getBloggerPayout({id, startDate, endDate}) {
522
497
  person.pay_frequency
523
498
  ORDER BY meta_amount DESC NULLS LAST;
524
499
  `;
525
- return this.one(query, { id, startDate, endDate });
526
- }
500
+ return orm.one(query, { id, startDate, endDate });
501
+ };
527
502
  ```
528
503
 
529
504
  ## Comparisons
@@ -545,13 +520,11 @@ PureORM
545
520
 
546
521
  ## API
547
522
 
548
- ### Classes
523
+ ### Interfaces
549
524
 
550
- #### `BaseBo`
525
+ #### `PureORMEntity`
551
526
 
552
- An abstract class which is the base class your BO classes to extend.
553
-
554
- **Abstract Methods** to be implemented
527
+ An interface which your business object classes need to implement.
555
528
 
556
529
  - `static get tableName(): string` - Returns the string table name which the business object associates with from the database.
557
530
  - `static get sqlColumnsData(): Array<string|ColumnData>` - Returns an array of the database column data. The type is either:
@@ -562,51 +535,30 @@ An abstract class which is the base class your BO classes to extend.
562
535
  - `primaryKey: boolean` - Is this column (part of) the primary key (defaults to false)
563
536
  - `string` - If a string, it is applied as the `column` value, with all others defaulted.
564
537
  - (Note: if there is no primary key, `id` is defaulted)
538
+ - `get BoCollection()?: BoCollection` - (Optional) returns the business object collection class constructor.
539
+ - `static get displayName()?: string` - (Optional) returns the string display name of the business object (defaults to camelcase of tableName)
565
540
 
566
- Optional
567
-
568
- - `get BoCollection(): BoCollection` - Returns the business object collection class constructor.
569
- - `static get displayName(): string` - Returns the string display name of the business object (defaults to camelcase of tableName)
570
-
571
- **Public Methods**
572
-
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!)
579
-
580
- #### `BaseBoCollection`
541
+ #### `PureORMCollection`
581
542
 
582
543
  An abstract class which is the base class your Bo Collection classes extend.
583
544
 
584
- **Abstract Methods** to be implemented
585
-
586
545
  - `static get Bo(): BO` - Returns the individual (singular) business object class constructor.
546
+ - `get displayName()?: BO` - (Optional) returns the string display name of the business object collection (defaults to bo displayName with an "s")
587
547
 
588
- Optional
589
-
590
- - `get displayName(): BO` - Returns the string display name of the business object collection (defaults to bo displayName with an "s")
591
-
592
- **Public Methods**
593
-
594
- - `filter(predicate)`
595
- - and some advanced methods
548
+ #### `create`
596
549
 
597
- #### `BaseDAO`
550
+ The factory function for creating your ORM.
598
551
 
599
- The base class your DAO classes extend.
600
-
601
- **Abstract Methods** to be implemented
552
+ **Parameters**
602
553
 
603
- - `get Bo(): BO` - Returns the business object class constructor.
554
+ - `getBusinessObjects: () => Array<BusinessObject>` - A function which returns an array of all the business objects.
555
+ - `db: <DataBaseDriverInstance>` - A database driver instance.
604
556
 
605
- **Public Methods**
557
+ **Return Value**
606
558
 
607
- - `constructor({ db }})`
559
+ Your ORM.
608
560
 
609
- Abstractions over `pg-promise`'s query methods:
561
+ It has the following query methods:
610
562
 
611
563
  - `one(query: string, params: object)` - executes a query and returns a Bo, or throws.
612
564
  - `oneOrNone(query: string, params: object)` - executes a query and returns a Bo or undefined, or throws.
@@ -614,9 +566,9 @@ Abstractions over `pg-promise`'s query methods:
614
566
  - `any(query: string, params: object)` - executes a query and returns a BoCollection.
615
567
  - `none(query: string, params: object)` - executes a query and returns null.
616
568
 
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.)
569
+ (Note these orm query 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.)
618
570
 
619
- Built-in "basic" / generic functions which your extending DAO class instance gets for free
571
+ Built-in "basic" / generic crud functions
620
572
 
621
573
  - `getMatching(bo: BaseBO)`
622
574
  - `getOneOrNoneMatching(bo: BaseBO)`
@@ -627,38 +579,13 @@ Built-in "basic" / generic functions which your extending DAO class instance get
627
579
  - `delete(bo: BaseBO)`
628
580
  - `deleteMatching(bo: BaseBO)`
629
581
 
630
- These are just provided because they are so common and straight-forward. However, the point of this library specifically contrasts against having a large surface area of pre-built functions to learn. The idea is to add a DAO class, and add your own custom functions with your own SQL.
631
-
632
- ### Methods
633
-
634
- #### `createBaseBO({ getBusinessObjects }): BaseBo`
635
-
636
- **Parameters**
637
-
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).
639
-
640
- **Return Value**
641
-
642
- - The BaseBo class to extend for your business objects.
643
-
644
- #### `createBaseDAO({ db, logError }): BaseDAO`
645
-
646
- **Parameters**
647
-
648
- - `logError: function`
649
- - `db: (database driver)`
650
-
651
- **Return Value**
652
-
653
- - The BaseDAO class to extend for your dao classes.
582
+ These are just provided because they are so common and straight-forward. While the goal of this library is foster writing SQL in your DAL (which returns pure business objects) some CRUD operations are so common they are included in the ORM. Feel free to completely disregard if you want to write these in your DAL yourself.
654
583
 
655
584
  ## Current Status
656
585
 
657
586
  #### Current Limitations (PRs welcome!)
658
587
 
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.
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
661
- - the query can start from some other table, and join a bunch of times to get there, though
588
+ - `pg-promise`/`node-postgres` is the only database driver supported out-of-the-box. 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.
662
589
  - 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).
663
590
  - the result of _the select_ must always be a non-circular tree (eg, (Bad): Article, Person, Group, GroupArticle, Article)
664
591
 
@@ -1,6 +1,7 @@
1
- const BaseBo = require('./base');
2
-
3
- class Person extends BaseBo {
1
+ class Person {
2
+ constructor(props) {
3
+ Object.assign(this, props);
4
+ }
4
5
  static get tableName() {
5
6
  return 'person';
6
7
  }