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.
- package/.eslintrc.json +2 -2
- package/README.md +169 -242
- package/examples/basic/bo/person.js +4 -3
- package/examples/basic/dao/person.js +1 -2
- package/examples/basic/db.js +6 -15
- package/examples/basic/orm.js +8 -0
- package/examples/blog/bo/article.js +5 -2
- package/examples/blog/bo/article_tag.js +5 -2
- package/examples/blog/bo/article_tags.js +4 -3
- package/examples/blog/bo/articles.js +4 -3
- package/examples/blog/bo/person.js +4 -2
- package/examples/blog/bo/tag.js +4 -2
- package/examples/blog/business-objects.js +1 -10
- package/examples/blog/orm.js +14 -0
- package/examples/order/bo/line-item.js +5 -2
- package/examples/order/bo/line-items.js +4 -3
- package/examples/order/bo/order.js +5 -2
- package/examples/order/bo/orders.js +4 -3
- package/examples/order/bo/product-variant.js +5 -2
- package/examples/order/bo/product-variants.js +4 -3
- package/examples/order/bo/product.js +5 -2
- package/examples/order/bo/products.js +4 -3
- package/examples/order/bo/utm-source.js +4 -2
- package/examples/order/orm.js +20 -0
- package/examples/order-more/bo/actual-product-variant.js +5 -2
- package/examples/order-more/bo/actual-product-variants.js +4 -3
- package/examples/order-more/bo/color.js +5 -2
- package/examples/order-more/bo/colors.js +4 -3
- package/examples/order-more/bo/customer.js +5 -2
- package/examples/order-more/bo/customers.js +4 -3
- package/examples/order-more/bo/gender.js +5 -2
- package/examples/order-more/bo/genders.js +4 -3
- package/examples/order-more/bo/inventory-level.js +5 -2
- package/examples/order-more/bo/inventory-levels.js +4 -3
- package/examples/order-more/bo/line-item.js +5 -2
- package/examples/order-more/bo/line-items.js +4 -3
- package/examples/order-more/bo/order.js +5 -2
- package/examples/order-more/bo/orders.js +4 -3
- package/examples/order-more/bo/parcel-event.js +5 -2
- package/examples/order-more/bo/parcel-events.js +4 -3
- package/examples/order-more/bo/parcel-line-item.js +5 -2
- package/examples/order-more/bo/parcel-line-items.js +4 -3
- package/examples/order-more/bo/parcel.js +5 -2
- package/examples/order-more/bo/parcels.js +4 -3
- package/examples/order-more/bo/physical-address.js +5 -2
- package/examples/order-more/bo/physical-addresses.js +4 -3
- package/examples/order-more/bo/product-variant-image.js +5 -2
- package/examples/order-more/bo/product-variant-images.js +4 -3
- package/examples/order-more/bo/product-variant.js +5 -2
- package/examples/order-more/bo/product-variants.js +4 -3
- package/examples/order-more/bo/product.js +5 -2
- package/examples/order-more/bo/products.js +4 -3
- package/examples/order-more/bo/refund.js +5 -2
- package/examples/order-more/bo/refunds.js +4 -3
- package/examples/order-more/bo/shipment-actual-product-variant.js +5 -2
- package/examples/order-more/bo/shipment-actual-product-variants.js +4 -3
- package/examples/order-more/bo/shipment.js +5 -2
- package/examples/order-more/bo/shipments.js +4 -3
- package/examples/order-more/bo/size.js +5 -2
- package/examples/order-more/bo/sizes.js +4 -3
- package/examples/order-more/bo/utm-medium.js +4 -2
- package/examples/order-more/bo/utm-source.js +4 -2
- package/examples/order-more/orm.js +49 -0
- package/package.json +4 -2
- package/src/bo.js +393 -0
- package/src/{bo/base-bo.spec.js → bo.spec.js} +73 -37
- package/src/factory.js +167 -0
- package/src/factory.spec.js +11 -0
- package/src/index.js +49 -6
- package/test-utils/five/bo/line-item.js +5 -2
- package/test-utils/five/bo/line-items.js +4 -3
- package/test-utils/five/bo/order.js +5 -2
- package/test-utils/five/bo/orders.js +4 -3
- package/test-utils/five/bo/parcel-event.js +5 -2
- package/test-utils/five/bo/parcel-events.js +4 -3
- package/test-utils/five/bo/parcel-line-item.js +5 -2
- package/test-utils/five/bo/parcel-line-items.js +4 -3
- package/test-utils/five/bo/parcel.js +5 -2
- package/test-utils/five/bo/parcels.js +4 -3
- package/test-utils/five/orm.js +19 -0
- package/test-utils/nine/bo/feature-switch.js +5 -2
- package/test-utils/nine/bo/feature-switches.js +4 -3
- package/test-utils/nine/orm.js +9 -0
- package/test-utils/six/bo/customer.js +5 -2
- package/test-utils/six/bo/customers.js +4 -3
- package/test-utils/six/bo/line-item.js +5 -2
- package/test-utils/six/bo/line-items.js +4 -3
- package/test-utils/six/bo/order.js +5 -2
- package/test-utils/six/bo/orders.js +4 -3
- package/test-utils/six/bo/parcel-line-item.js +5 -2
- package/test-utils/six/bo/parcel-line-items.js +4 -3
- package/test-utils/six/bo/parcel.js +5 -2
- package/test-utils/six/bo/parcels.js +4 -3
- package/test-utils/six/orm.js +19 -0
- package/test-utils/thirteen/bo/audience.js +21 -0
- package/test-utils/thirteen/bo/audiences.js +10 -0
- package/{examples/basic → test-utils/thirteen}/bo/base.js +0 -0
- package/test-utils/thirteen/bo/brand.js +21 -0
- package/test-utils/thirteen/bo/brands.js +10 -0
- package/test-utils/thirteen/bo/categories.js +13 -0
- package/test-utils/thirteen/bo/category.js +21 -0
- package/test-utils/thirteen/bo/member.js +21 -0
- package/test-utils/thirteen/bo/members.js +10 -0
- package/test-utils/thirteen/bo/passion.js +21 -0
- package/test-utils/thirteen/bo/passions.js +10 -0
- package/test-utils/thirteen/bo/product.js +22 -0
- package/test-utils/thirteen/bo/products.js +10 -0
- package/test-utils/thirteen/bo/recommendation-audience.js +27 -0
- package/test-utils/thirteen/bo/recommendation-audiences.js +10 -0
- package/test-utils/thirteen/bo/recommendation.js +33 -0
- package/test-utils/thirteen/bo/recommendations.js +10 -0
- package/test-utils/thirteen/orm.js +25 -0
- package/test-utils/thirteen/results.json +74 -0
- package/test-utils/twelve/bo/member.js +5 -2
- package/test-utils/twelve/bo/members.js +4 -3
- package/test-utils/twelve/bo/prompt.js +5 -2
- package/test-utils/twelve/bo/prompts.js +4 -3
- package/test-utils/twelve/orm.js +10 -0
- package/examples/basic/business-objects.js +0 -9
- package/examples/basic/dao/base.js +0 -9
- package/examples/blog/bo/base.js +0 -5
- package/examples/order/bo/base.js +0 -5
- package/examples/order/business-objects.js +0 -11
- package/examples/order-more/bo/base.js +0 -5
- package/examples/order-more/business-objects.js +0 -26
- package/src/bo/base-bo-collection.js +0 -15
- package/src/bo/base-bo.js +0 -379
- package/src/dao/base-dao.js +0 -146
- package/src/dao/base-dao.spec.js +0 -6
- package/src/util/helpers.js +0 -28
- package/src/util/helpers.spec.js +0 -6
- package/test-utils/five/business-objects.js +0 -11
- package/test-utils/nine/business-objects.js +0 -7
- package/test-utils/six/business-objects.js +0 -11
- 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
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
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
|
|
121
|
-
- Our query is executed with a `one` method. The
|
|
122
|
-
- Rather than manually specifying our columns in the sql select expression, we
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
- `
|
|
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
|
|
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
|
|
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/
|
|
175
|
-
const
|
|
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.
|
|
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
|
|
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/
|
|
241
|
-
+const
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
315
|
+
### Step 4: Creating our ORM
|
|
333
316
|
|
|
334
317
|
```javascript
|
|
335
|
-
// ./
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
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
|
-
// ./
|
|
360
|
-
const
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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 =
|
|
351
|
+
module.exports = getPerson;
|
|
408
352
|
```
|
|
409
353
|
|
|
410
|
-
Notice that we're using `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
###
|
|
523
|
+
### Interfaces
|
|
549
524
|
|
|
550
|
-
#### `
|
|
525
|
+
#### `PureORMEntity`
|
|
551
526
|
|
|
552
|
-
An
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
550
|
+
The factory function for creating your ORM.
|
|
598
551
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
**Abstract Methods** to be implemented
|
|
552
|
+
**Parameters**
|
|
602
553
|
|
|
603
|
-
- `
|
|
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
|
-
**
|
|
557
|
+
**Return Value**
|
|
606
558
|
|
|
607
|
-
|
|
559
|
+
Your ORM.
|
|
608
560
|
|
|
609
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|