-
Notifications
You must be signed in to change notification settings - Fork 2
/
Orm.js
340 lines (284 loc) · 9.4 KB
/
Orm.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
const knex = require('knex');
const {merge, isString} = require('lodash');
const KRedis = require('./kredis');
const Table = require('./Table');
const Scoper = require('./Scoper');
const Shape = require('./Shape');
const migrator = require('./migrator');
class Orm {
constructor(config={}) {
if ('db' in config) {
this.knex = knex(config.db);
} else {
throw new Error(`no 'db' config found`);
}
if ('redis' in config) {
this.cache = new KRedis(config.redis);
}
// tables definitions
this.tableClasses = new Map();
// tables instances
this.tables = new Map();
// tableColumns cache
this.tableColumns = new Map();
// migrator
this.migrator = migrator(this);
// exports which can be exported in place of orm instance
this.exports = {
orm: this,
table: (...args) => this.table(...args),
trx: (...args) => this.trx(...args),
raw: (...args) => this.raw(...args),
migrator: this.migrator,
cache: this.cache,
knex: this.knex,
scoper: (...args) => this.scoper(...args),
shape: (...args) => this.shape(...args)
};
}
// raw expr helper
raw(expr) {
return this.knex.raw(expr);
}
// transaction helper
transaction(promiseFn) {
return this.knex.transaction(promiseFn);
}
// transaction shorthand
// usage:
// return orm.trx((t) => {
// return orm('users', t).save([{}, {}, {}]);
// }).then((users) => {
// ...
// });
trx(promiseFn) {
let outerResult;
return this.transaction((t) => {
return promiseFn(t).then((result) => {
return t.commit().then(() => {
outerResult = result;
return result;
});
}).catch((e) => {
t.rollback().then(() => {
throw e;
});
});
}).then(() => outerResult);
}
// method to close the database
close() {
const promises = [this.knex.destroy()];
if (this.cache) {
promises.push(this.cache.disconnect());
}
return Promise.all(promises);
}
// here, we load the columns of all the tables that have been
// defined via the orm, and return a promise on completion
// cos, if people wanna do that before starting the server
// let em do that. we also call ioredis.connect if its available
load() {
const promises = Array.from(this.tables.keys).map((name) => this.table(name).load());
// if (this.cache) {
// promises.push(this.cache.connect());
// }
return Promise.all(promises);
}
// get a tableClass
tableClass(tableName) {
return this.tableClasses.get(tableName);
}
// get a table object
table(tableName, trx=null) {
if (!this.tables.has(tableName)) {
throw new Error(`trying to access invalid table ${tableName}`);
}
const tbl = this.tables.get(tableName).fork();
if (trx !== null) {
tbl.transacting(trx);
}
return tbl;
}
scoper(scopes) {
return new Scoper(scopes);
}
shape(checks) {
return new Shape(checks);
}
// shorthand for table
tbl(tableName, trx=null) {
return this.table(tableName, trx);
}
defineTable(params={}) {
const tableName = params.name;
if (!isString(tableName)) {
throw new Error(`Invalid table-name: ${tableName} supplied via key 'name'`);
}
if (this.tableClasses.has(tableName)) {
throw new Error(`Table '${tableName}' already defined`);
}
this.tableClasses.set(tableName, this.newTableClass(params));
this.instantitateTable(tableName, params);
return this;
}
extendTable(tableName, {scopes={}, joints={}, relations={}, methods={}}) {
if (!this.tableClasses.has(tableName)) {
throw new Error(`Table '${tableName}' not defined yet`);
}
const TableClass = this.tableClass(tableName);
const ExtendedTableClass = class extends TableClass {};
this.attachScopesToTableClass(ExtendedTableClass, scopes);
this.attachJointsToTableClass(ExtendedTableClass, joints);
this.attachRelationsToTableClass(ExtendedTableClass, relations);
this.attachMethodsToTableClass(ExtendedTableClass, methods);
this.tableClasses.set(tableName, ExtendedTableClass);
this.instantitateTable(tableName);
return this;
}
instantitateTable(tableName) {
const TableClass = this.tableClasses.get(tableName);
return this.tables.set(tableName, new TableClass(this));
}
newTableClass(params) {
return this.extendTableClass(Table, params);
}
extendTableClass(TableClass, params) {
const {name, props, processors, scopes, joints, relations, methods} = merge(
// the defaults
{
// the table's name, is required
name: null,
// table properties
props: {
key: 'id',
// default key column, can be ['user_id', 'post_id'] for composite keys
uuid: false,
// by default we don't assume that you use an auto generated uuid as db id
perPage: 25,
// standard batch size per page used by `forPage` method
// forPage method uses offset
// avoid that and use a keyset in prod (http://use-the-index-luke.com/no-offset)
timestamps: false
// set to `true` if you want auto timestamps or
// timestamps: ['created_at', 'updated_at'] (these are defaults when `true`)
// will be assigned in this order only
},
// predefined scopes on the table
scopes: {},
// predefined joints on the table
joints: {},
// relations definitions for the table
relations: {},
// table methods defintions
methods: {}
},
// supplied params which will override the defaults
params
);
// the extended table class whose objects will behave as needed
const ExtendedTableClass = class extends TableClass {};
// assign name to the table class
ExtendedTableClass.prototype.name = name;
// assign props to the table class
ExtendedTableClass.prototype.props = props;
// assign processors to the table class
ExtendedTableClass.prototype.processors = processors;
// store names of defined scopes, joints, relations, and methods
ExtendedTableClass.prototype.definedScopes = new Set();
ExtendedTableClass.prototype.definedJoints = new Set();
ExtendedTableClass.prototype.definedRelations = new Set();
ExtendedTableClass.prototype.definedMethods = new Set();
// attach scopes, joints, relations and methods to tables
// these are the only ones extendable after creation
this.attachScopesToTableClass(ExtendedTableClass, scopes);
this.attachJointsToTableClass(ExtendedTableClass, joints);
this.attachRelationsToTableClass(ExtendedTableClass, relations);
this.attachMethodsToTableClass(ExtendedTableClass, methods);
// return the extended table class
return ExtendedTableClass;
}
attachScopesToTableClass(TableClass, scopes) {
// keep a record of defined scopes
Object.keys(scopes).forEach((name) => {
TableClass.prototype.definedScopes.add(name);
});
// process and merge scopes with table class
merge(
TableClass.prototype,
Object.keys(scopes).reduce((processed, name) => {
return merge(processed, {
[name](...args) {
scopes[name].apply(this, args);
// set the label of the last pushed scope
this.scopeTrack.relabelLastScope(name);
return this;
}
});
}, {})
);
}
attachJointsToTableClass(TableClass, joints) {
// keep a record of defined joints
Object.keys(joints).forEach((name) => {
TableClass.prototype.definedJoints.add(name);
});
// process and merge joints with table class
merge(
TableClass.prototype,
Object.keys(joints).reduce((processed, name) => {
// predefined joints never take arguments
return merge(processed, {
[name]() {
if (this.scopeTrack.hasJoint(name)) {
return this;
} else {
joints[name].call(this);
// set the label of the last pushed scope
this.scopeTrack.relabelLastScope(name);
// ensure that the last scope is a joint
this.scopeTrack.convertLastScopeToJoint();
return this;
}
}
});
}, {})
);
}
attachRelationsToTableClass(TableClass, relations) {
// keep a record of defined relations
Object.keys(relations).forEach((name) => {
TableClass.prototype.definedRelations.add(name);
});
// process and merge relations with table class
merge(
TableClass.prototype,
Object.keys(relations).reduce((processed, name) => {
// const relation = relations[name];
return merge(processed, {
[name](model) {
if (model) {
return relations[name].bind(this)().setName(name).forModel(model);
} else {
return relations[name].bind(this)().setName(name);
}
}
});
}, {})
);
}
attachMethodsToTableClass(TableClass, methods) {
// keep a record of defined methods
Object.keys(methods).forEach((name) => {
TableClass.prototype.definedMethods.add(name);
});
// process and merge relations with table class
merge(
TableClass.prototype,
Object.keys(methods).reduce((processed, name) => {
return merge(processed, {[name]: methods[name]});
}, {})
);
}
}
module.exports = Orm;