/
model.js
236 lines (205 loc) · 7.35 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
import * as collection from "./collection";
import { NotFoundError, NotImplementedError } from "../base";
import { verify } from "../util";
const MEMORY_STORAGE = {};
const TYPE_DEFAULTS = {
bytes: null,
unicode: null,
int: null,
float: null,
bool: false,
list: () => [],
dict: () => ({}),
object: () => ({})
};
export class Model {
constructor(options = {}) {
const fill = options.fill === undefined ? true : options.fill;
if (fill) this.constructor.fill(this);
}
static niw() {
return new this();
}
/**
* Fills the current model with the proper values so that
* no values are unset as this would violate the model definition
* integrity. This is required when retrieving an object(s) from
* the data source (as some of them may be incomplete).
*
* @param {Object} model The model that is going to have its unset
* attributes filled with "default" data, in case none is provided
* all of the attributes will be filled with "default" data.
* @param {Boolean} safe If the safe mode should be used for the fill
* operation meaning that under some conditions no unit fill
* operation is going to be applied (eg: retrieval operations).
*/
static async fill(model = {}, safe = false) {
for (const [name, field] of Object.entries(this.schema)) {
if (model[name] !== undefined) continue;
if (["_id"].includes(model[name])) continue;
const _private = field.private === undefined ? false : field.private;
const increment = field.increment === undefined ? false : field.increment;
if (_private && safe) continue;
if (increment) continue;
if (field.initial !== undefined) {
const initial = field.initial;
model[name] = initial;
} else {
const type = field.type || null;
let _default = typeD(type, null);
_default = type._default === undefined ? _default : type._default();
model[name] = _default;
}
}
}
static get adapter() {
return process.env.ADAPTER || "mongo";
}
async validate() {
const errors = [...this._validate()];
if (errors.length) {
throw new Error(`Invalid model: ${errors.map(err => String(err)).join(", ")}`);
}
}
async apply(model) {
await this.wrap(model);
return this;
}
async wrap(model) {
await this._wrap(model);
return this;
}
get model() {
return this;
}
get jsonV() {
return this.model;
}
get string() {
return JSON.stringify(this.model);
}
/**
* Wraps the provided model object around the current instance, making
* sure that all of the elements are compliant with the schema.
*
* It should be possible to override the `_wrap` operation to implement
* a custom "way" of setting data into a model.
*
* @param {Object} model The model structure that is going to be used
* to wrap the current model object, meaning that all of its elements
* are going to be stored in the current object.
*/
async _wrap(model) {
for (const key of Object.keys(this.constructor.schema)) {
if (model[key] === undefined) continue;
this[key] = model[key];
}
if (model._id !== undefined) this._id = model._id;
}
* _validate() {}
}
export class ModelStore extends Model {
static async find(options = {}) {
const found = await this.collection.find(options);
const models = await Promise.all(found.map(v => new this().wrap(v)));
return models;
}
static async get(id) {
const options = {};
options[this.idName] = id;
const model = await this.collection.findOne(options);
if (!model) {
throw new NotFoundError();
}
const instance = new this();
return instance.wrap(model);
}
static get schema() {
throw new NotImplementedError();
}
static get collection() {
if (this._collection) return this._collection;
const adapter = this.adapter[0].toUpperCase() + this.adapter.slice(1);
this._collection = new collection[adapter + "Collection"](this.dataOptions);
return this._collection;
}
static get idName() {
return "id";
}
static get dataOptions() {
return {
name: this.name,
schema: this.schema
};
}
async save(validate = true) {
let model;
if (validate) await this.validate();
await this.verify();
const isNew = this._id === undefined;
const conditions = {};
conditions[this.constructor.idName] = this.identifier;
if (isNew) model = await this.constructor.collection.create(this.model);
else model = await this.constructor.collection.findOneAndUpdate(conditions, this.model);
this.wrap(model);
return this;
}
async delete() {
const conditions = {};
conditions[this.constructor.idName] = this.identifier;
await this.constructor.collection.findOneAndDelete(conditions);
return this;
}
async verify() {
verify(
this.identifier !== undefined && this.identifier !== null,
"The identifier must be defined before saving"
);
for (const [name, field] of Object.entries(this.constructor.schema)) {
verify(
!field.required || ![undefined, null].includes(this[name]),
`No value provided for mandatory field '${name}'`
);
}
}
get identifier() {
return this[this.constructor.idName];
}
}
export class ModelMemory extends ModelStore {
static get adapter() {
return "memory";
}
static get dataOptions() {
return Object.assign(super.dataOptions, { storage: this.storage });
}
static get storage() {
return MEMORY_STORAGE[this.name];
}
}
/**
* Retrieves the default (initial) value for the a certain
* provided data type falling back to the provided default
* value in case it's not possible to retrieve a new valid
* default for value for the type.
*
* The process of retrieval of the default value to a certain
* type may include the calling of a lambda function to obtain
* a new instance of the default value, this avoid the usage
* of global shared structures for the default values, that
* could cause extremely confusing situations.
*
* @param {Type} type The data type object for which to retrieve its
* default value.
* @param {Object} _default The default value to be returned in case it's
* not possible to retrieve a better one.
* @returns {Object} The "final" default value for the data type according
* to the best possible strategy.
*/
export const typeD = function(type, _default = null) {
if (TYPE_DEFAULTS[type] === undefined) return _default;
_default = TYPE_DEFAULTS[type];
if (typeof _default !== "function") return _default;
return _default();
};
export default Model;