-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmodel.js
275 lines (232 loc) · 6.13 KB
/
model.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
import reduce from "https://deno.land/x/lodash/reduce.js";
import merge from "https://deno.land/x/lodash/merge.js";
import each from "https://deno.land/x/lodash/forEach.js";
import flatten from "https://deno.land/x/lodash/flatten.js";
import Validations, { GenericValidation } from "./model/validations.js";
import Errors from "./model/errors.js";
import Relation from "./model/relation.js";
import { camelCase, snakeCase } from "https://deno.land/x/case/mod.ts";
// import "https://deno.land/x/humanizer.ts/vocabularies.ts";
/**
* Models are classes that construct the data model for your
* application, as well as perform any database-related business logic
* such as validations and data massaging or aggregation. They follow
* the active record pattern, and therefore encapsulate all logic
* related to the querying and persistence of a table in your database.
*/
export default class Model {
/**
* The app this model is a part of.
*/
static app = null
/**
* Name of the class, used in logging.
*/
static get name() {
return `${this}`.split(" ")[1];
}
/**
* Name used in parameters.
*/
static get paramName() {
return camelCase(this.name);
}
static get collectionName() {
return snakeCase(this.name) + "s";
}
/**
* All validations on this model.
*/
static validations = [];
/**
* All associations to other models.
*/
static associations = {
belongsTo: {},
hasMany: {},
hasOne: {},
};
/**
* Table name for this model.
*/
static table = this.tableName;
/**
* A macro for creating a new `Validation` object in the list of
* validations a model may run through. Call it with
* `YourModelName.validates`.
*
* @param string name - Name of the property
* @param Object validations - Validations to add
*/
static validates(name, validations = {}) {
each(validations, (options, name) => {
const Validation = Validations[name];
options = options === true ? {} : options;
this.validations.push(new Validation(options));
});
}
/**
* A macro for creating a new `GenericValidation` object, allowing the
* validation to consist of just running a method which may or may not
* add errors. Call it with `YourModelName.validate`.
*
* @param string method - Name of the method to call
*/
static validate(method) {
this.validations.push(new GenericValidation({ method }));
}
/**
* Create a new model record and save it to the database.
*/
static create(attributes = {}) {
const model = new this(attributes);
model.save();
return model;
}
/**
* Return a relation representing all records in the database.
*/
static get all() {
return new Relation(this);
}
/**
* Perform a query for matching models in the database.
*/
static where(query) {
return this.all.where(query);
}
/**
* Find an existing model record in the database by the given
* parameters.
*/
static findBy(query) {
return this.where(query).first;
}
/**
* Find an existing model record in the database by its ID.
*/
static find(id) {
return this.findBy({ id });
}
constructor(attributes = {}) {
this.attributes = attributes;
this.errors = new Errors();
this.associated = {};
this._buildAssociations();
this.initialize();
}
initialize() {}
/**
* All non-function properties of this object.
*/
get attributes() {
return reduce(
this,
(attrs, value, prop) => {
if (typeof value !== "function") {
attrs[prop] = value;
}
return attrs;
},
{},
);
}
/**
* Set attributes on this object by assigning properties directly to
* it.
*/
set attributes(attrs = {}) {
merge(this, attrs);
}
/**
* Flatten validators from their method calls.
*/
get validations() {
return flatten(this.constructor.validations);
}
/**
* Run all configured validators.
*/
get valid() {
this.validations.forEach((validation) => validation.valid(this));
return this.errors.any;
}
/**
* Persist the current information in this model to the database.
*/
save() {
if (!this.valid) {
return false;
}
const query = new Relation(this);
if (this.id) {
query.where("id", this.id).update(this.attributes);
} else {
query.insert(this.attributes);
}
query.run();
return true;
}
/**
* Set the given attributes on this model and persist.
*/
update(attributes = {}) {
this.attributes = attributes;
return this.save();
}
/**
* Remove this model from the database.
*/
destroy() {
const query = new Relation(this);
query.where("id", this.id).delete();
query.run();
return true;
}
/**
* Reload this model's information from the database.
*/
reload() {
const model = this.constructor.find(this.id);
merge(this, model.attributes);
return this;
}
/**
* Build model associations from the `.associations` static property
* when constructed.
*
* @private
*/
_buildAssociations() {
Object.entries(this.constructor.associations, (type, associations) => {
Object.entries(associations, (name, Model) => {
Object.defineProperty(this, name, {
get() {
if (typeof this.associated[name] !== "undefined") {
return this.associated[name];
}
const param = this.constructor.paramName;
const fk = `${param}ID`;
const id = this[`${name}ID`];
let value;
if (type === "hasMany") {
value = Model.where({ [fk]: this.id });
} else if (type === "hasOne") {
value = Model.where({ [fk]: this.id });
} else if (type === "belongsTo") {
value = Model.find(id);
} else {
throw new Error(`Invalid association type: "${type}"`);
}
this.associated[name] = value;
return value;
},
set(value) {
this.associated[name] = value;
this[`${name}ID`] = value.id;
},
});
});
});
}
}