nodester 0.0.1
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/LICENSE +21 -0
- package/Readme.md +125 -0
- package/docs/App.md +13 -0
- package/docs/Queries.md +61 -0
- package/docs/Readme.md +2 -0
- package/docs/Routing.md +34 -0
- package/examples/goal/index.js +23 -0
- package/examples/rest/index.js +25 -0
- package/examples/rest/node_modules/.package-lock.json +40 -0
- package/examples/rest/package-lock.json +72 -0
- package/examples/rest/package.json +14 -0
- package/lib/application/MiddlewareStack.js +125 -0
- package/lib/application/http/request.js +462 -0
- package/lib/application/http/response.js +1107 -0
- package/lib/application/http/utils.js +254 -0
- package/lib/application/index.js +292 -0
- package/lib/constants/ConstantsEnum.js +13 -0
- package/lib/constants/ResponseFormats.js +7 -0
- package/lib/controllers/Controller.js +474 -0
- package/lib/controllers/JWTController.js +240 -0
- package/lib/controllers/ServiceController.js +109 -0
- package/lib/controllers/WebController.js +75 -0
- package/lib/facades/Facade.js +388 -0
- package/lib/facades/FacadeParams.js +11 -0
- package/lib/facades/ServiceFacade.js +17 -0
- package/lib/facades/jwt.facade.js +273 -0
- package/lib/factories/errors/CustomError.js +22 -0
- package/lib/factories/errors/index.js +9 -0
- package/lib/factories/responses/api.js +90 -0
- package/lib/factories/responses/html.js +55 -0
- package/lib/logger/console.js +24 -0
- package/lib/models/DisabledRefreshToken.js +68 -0
- package/lib/models/Extractor.js +320 -0
- package/lib/models/define.js +62 -0
- package/lib/models/mixins.js +369 -0
- package/lib/policies/Role.js +77 -0
- package/lib/policies/RoleExtracting.js +97 -0
- package/lib/preprocessors/BodyPreprocessor.js +61 -0
- package/lib/preprocessors/IncludesPreprocessor.js +55 -0
- package/lib/preprocessors/QueryPreprocessor.js +64 -0
- package/lib/routers/Default/index.js +143 -0
- package/lib/routers/Default/layer.js +50 -0
- package/lib/routers/Main/index.js +10 -0
- package/lib/routers/Roles/index.js +81 -0
- package/lib/services/includes.service.js +79 -0
- package/lib/services/jwt.service.js +147 -0
- package/lib/tools/sql.tool.js +82 -0
- package/lib/utils/dates.util.js +23 -0
- package/lib/utils/forms.util.js +22 -0
- package/lib/utils/json.util.js +49 -0
- package/lib/utils/mappers/Routes/index.js +100 -0
- package/lib/utils/mappers/Routes/utils.js +20 -0
- package/lib/utils/modelAssociations.util.js +44 -0
- package/lib/utils/objects.util.js +69 -0
- package/lib/utils/params.util.js +19 -0
- package/lib/utils/path.util.js +26 -0
- package/lib/utils/queries.util.js +240 -0
- package/lib/utils/sanitizations.util.js +111 -0
- package/lib/utils/sql.util.js +78 -0
- package/lib/utils/strings.util.js +43 -0
- package/lib/utils/types.util.js +26 -0
- package/package.json +63 -0
- package/tests/index.test.js +35 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ModelDataExtractor (/models/Extractor.js)
|
|
3
|
+
* Reads data from Express's request and filters it,
|
|
4
|
+
* based on model parameters and always-present query parameters (limit, skip, etc.).
|
|
5
|
+
*/
|
|
6
|
+
// Constants:
|
|
7
|
+
const { DataTypes } = require('sequelize');
|
|
8
|
+
const IGNORED_KEYS = [
|
|
9
|
+
// Sequilize keys:
|
|
10
|
+
'createdAt',
|
|
11
|
+
'updatedAt',
|
|
12
|
+
'deletedAt',
|
|
13
|
+
|
|
14
|
+
'created_at',
|
|
15
|
+
'updated_at',
|
|
16
|
+
'deleted_at',
|
|
17
|
+
// Procedural keys.
|
|
18
|
+
'should_delete',
|
|
19
|
+
];
|
|
20
|
+
const PROCEDURAL_KEYS = {
|
|
21
|
+
'should_delete': new DataTypes.BOOLEAN()
|
|
22
|
+
};
|
|
23
|
+
// Utils:
|
|
24
|
+
const sntz = require('nodester/utils/sanitizations.util');
|
|
25
|
+
const Params = require('nodester/facades/FacadeParams');
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
module.exports = class ModelDataExtractor {
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
modelDefinition,
|
|
32
|
+
options,
|
|
33
|
+
) {
|
|
34
|
+
if (!modelDefinition) {
|
|
35
|
+
throw new Error('"modelDefinition" attribute is invalid.');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
withFiles
|
|
40
|
+
} = Params(options, {
|
|
41
|
+
withFiles: false
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
this.model = modelDefinition;
|
|
45
|
+
this.attributes = modelDefinition.tableAttributes;
|
|
46
|
+
this.options = {
|
|
47
|
+
withFiles: !!withFiles,
|
|
48
|
+
fileInstanceName: 'file',
|
|
49
|
+
filesArrayName: 'files',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
extractData(
|
|
54
|
+
modelDefinition,
|
|
55
|
+
parentModelDefinition=null,
|
|
56
|
+
holder={},
|
|
57
|
+
includes=[],
|
|
58
|
+
skipIdValidation=false,
|
|
59
|
+
skipValidation=false,
|
|
60
|
+
) {
|
|
61
|
+
if (!modelDefinition) {
|
|
62
|
+
const err = new Error('"modelDefinition" attribute is invalid.');
|
|
63
|
+
err.name = 'InternalValidationError';
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const data = {};
|
|
68
|
+
const errors = {};
|
|
69
|
+
|
|
70
|
+
const attributes = modelDefinition.tableAttributes;
|
|
71
|
+
|
|
72
|
+
// Table attributes minus "special" keys.
|
|
73
|
+
const keysToCheck = Object.keys(attributes)
|
|
74
|
+
.filter(key => !IGNORED_KEYS.includes(key));
|
|
75
|
+
|
|
76
|
+
keysToCheck.forEach((key) => {
|
|
77
|
+
const {
|
|
78
|
+
type,
|
|
79
|
+
allowNull,
|
|
80
|
+
defaultValue,
|
|
81
|
+
primaryKey,
|
|
82
|
+
references
|
|
83
|
+
} = attributes[key];
|
|
84
|
+
|
|
85
|
+
// Extract raw value from passed object.
|
|
86
|
+
const dataValue = holder[key];
|
|
87
|
+
|
|
88
|
+
if (!!primaryKey && !!skipIdValidation) {
|
|
89
|
+
// Just put id to data object:
|
|
90
|
+
if (!!dataValue)
|
|
91
|
+
data[key] = dataValue;
|
|
92
|
+
|
|
93
|
+
// Skip further validation.
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// If this field references some other model:
|
|
98
|
+
if (!!references) {
|
|
99
|
+
const modelReferenced = references.model;
|
|
100
|
+
|
|
101
|
+
// If referenced model is the same as parentModel,
|
|
102
|
+
// skip (Sequilize will handle it):
|
|
103
|
+
if (modelReferenced === parentModelDefinition?.tableName) {
|
|
104
|
+
// Skip further validation.
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// If value is undefined, and null is allowed skip:
|
|
110
|
+
if (dataValue === undefined && allowNull) {
|
|
111
|
+
// Skip further validation.
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// If value is undefined, and null is allowed,
|
|
116
|
+
// set and skip:
|
|
117
|
+
if (dataValue === null && allowNull) {
|
|
118
|
+
data[key] = null;
|
|
119
|
+
// Skip further validation.
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (allowNull === false && (dataValue === undefined || dataValue === null)) {
|
|
124
|
+
|
|
125
|
+
// If default value can be set,
|
|
126
|
+
// or we're allowed to skip validation,
|
|
127
|
+
// skip:
|
|
128
|
+
if (defaultValue !== undefined || (!primaryKey && skipValidation === true)) {
|
|
129
|
+
// Skip further validation.
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return errors[key] = { message: `Field "${key}" can not be null.` };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
data[key] = _sanitizeValue(dataValue, type);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Check procedural keys:
|
|
140
|
+
for (const [ key, dataType ] of Object.entries(PROCEDURAL_KEYS)) {
|
|
141
|
+
// Extract raw value from passed object.
|
|
142
|
+
const dataValue = holder[key];
|
|
143
|
+
|
|
144
|
+
// If value is defined:
|
|
145
|
+
if (dataValue !== undefined) {
|
|
146
|
+
data[key] = _sanitizeValue(dataValue, dataType);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// If model has associations:
|
|
151
|
+
if (Object.keys(modelDefinition?.associations)?.length > 0) {
|
|
152
|
+
|
|
153
|
+
// Go through association entries:
|
|
154
|
+
const associationEntries = Object.entries( modelDefinition.associations );
|
|
155
|
+
associationEntries.forEach(([
|
|
156
|
+
associationName,
|
|
157
|
+
associationDefinition
|
|
158
|
+
]) => {
|
|
159
|
+
|
|
160
|
+
// If data of this association is present:
|
|
161
|
+
if (!!holder[associationName]) {
|
|
162
|
+
|
|
163
|
+
const associatedModel = associationDefinition.target;
|
|
164
|
+
|
|
165
|
+
const isSingleInstance = Array.isArray( holder[associationName] ) === false;
|
|
166
|
+
|
|
167
|
+
// If single instance of associated model:
|
|
168
|
+
if (isSingleInstance) {
|
|
169
|
+
data[associationName] = this.extractData(
|
|
170
|
+
associatedModel,
|
|
171
|
+
modelDefinition,
|
|
172
|
+
holder[associationName],
|
|
173
|
+
[], // Includes
|
|
174
|
+
skipIdValidation,
|
|
175
|
+
skipValidation
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
// If multiple instances of associated model:
|
|
179
|
+
else {
|
|
180
|
+
data[associationName] = holder[associationName].map((associationData) => {
|
|
181
|
+
return this.extractData(
|
|
182
|
+
associatedModel,
|
|
183
|
+
modelDefinition,
|
|
184
|
+
associationData,
|
|
185
|
+
[], // Includes
|
|
186
|
+
skipIdValidation,
|
|
187
|
+
skipValidation
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// If options for "withFiles" is true,
|
|
196
|
+
// also check files in object.
|
|
197
|
+
if (this.options.withFiles === true) {
|
|
198
|
+
// Check for single file & array of files.
|
|
199
|
+
const keysToCheck = [
|
|
200
|
+
this.options.fileInstanceName,
|
|
201
|
+
this.options.filesArrayName,
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
keysToCheck.filter(key => holder[key] !== undefined)
|
|
205
|
+
.forEach(key => data[key] = holder[key]);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// If errors were set, throw ValidationError:
|
|
209
|
+
if (Object.keys(errors).length > 0) {
|
|
210
|
+
const err = new Error('');
|
|
211
|
+
err.name = 'ValidationError';
|
|
212
|
+
err.details = { ...errors };
|
|
213
|
+
throw err;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return data;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
extractInstanceDataFromObject(
|
|
220
|
+
holder={},
|
|
221
|
+
includes,
|
|
222
|
+
options,
|
|
223
|
+
) {
|
|
224
|
+
const {
|
|
225
|
+
skipIdValidation,
|
|
226
|
+
skipValidation
|
|
227
|
+
} = Params(options, {
|
|
228
|
+
skipIdValidation: false,
|
|
229
|
+
skipValidation: false,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return this.extractData(
|
|
233
|
+
this.model,
|
|
234
|
+
null,
|
|
235
|
+
holder,
|
|
236
|
+
includes,
|
|
237
|
+
skipIdValidation,
|
|
238
|
+
skipValidation
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* ! Warning !
|
|
243
|
+
* Not finished method
|
|
244
|
+
* Do not use!
|
|
245
|
+
*/
|
|
246
|
+
extractArrayDataFromObject(
|
|
247
|
+
holder={},
|
|
248
|
+
includes,
|
|
249
|
+
options,
|
|
250
|
+
) {
|
|
251
|
+
const {
|
|
252
|
+
skipIdValidation,
|
|
253
|
+
skipValidation
|
|
254
|
+
} = Params(options, {
|
|
255
|
+
skipIdValidation: false,
|
|
256
|
+
skipValidation: false,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const { instances } = holder;
|
|
260
|
+
|
|
261
|
+
// All model instances must be in an array by key "instances".
|
|
262
|
+
// If "instances" is not array, throw error:
|
|
263
|
+
if (Array.isArray(instances) === false) {
|
|
264
|
+
const err = new Error('');
|
|
265
|
+
err.name = 'ValidationError';
|
|
266
|
+
err.details = { message: 'Field "instances" must be an array' };
|
|
267
|
+
throw err;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const results = instances.map((instance) =>
|
|
271
|
+
this.extractData(
|
|
272
|
+
this.model,
|
|
273
|
+
null,
|
|
274
|
+
instance,
|
|
275
|
+
skipIdValidation,
|
|
276
|
+
skipValidation
|
|
277
|
+
)
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
return results;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function _sanitizeValue(
|
|
285
|
+
value,
|
|
286
|
+
dataType,
|
|
287
|
+
fallback=null,
|
|
288
|
+
) {
|
|
289
|
+
let result = null;
|
|
290
|
+
|
|
291
|
+
if (dataType instanceof DataTypes.INTEGER) {
|
|
292
|
+
result = sntz.INT(value, { fallback });
|
|
293
|
+
}
|
|
294
|
+
else if (dataType instanceof DataTypes.DECIMAL) {
|
|
295
|
+
result = sntz.NUMBER(value, { fallback });
|
|
296
|
+
}
|
|
297
|
+
else if (dataType instanceof DataTypes.FLOAT) {
|
|
298
|
+
result = sntz.NUMBER(value, { fallback });
|
|
299
|
+
}
|
|
300
|
+
else if (dataType instanceof DataTypes.STRING) {
|
|
301
|
+
result = sntz.STRING(value, { fallback });
|
|
302
|
+
}
|
|
303
|
+
else if (dataType instanceof DataTypes.TEXT) {
|
|
304
|
+
result = sntz.STRING(value, { fallback });
|
|
305
|
+
}
|
|
306
|
+
else if (dataType instanceof DataTypes.ENUM) {
|
|
307
|
+
result = sntz.STRING(value, { fallback });
|
|
308
|
+
}
|
|
309
|
+
else if (dataType instanceof DataTypes.JSON) {
|
|
310
|
+
result = sntz.JSON(value, { fallback });
|
|
311
|
+
}
|
|
312
|
+
else if (dataType instanceof DataTypes.BOOLEAN) {
|
|
313
|
+
result = sntz.BOOLEAN(value, { fallback });
|
|
314
|
+
}
|
|
315
|
+
else if (dataType instanceof DataTypes.DATE || dataType instanceof DataTypes.DATEONLY) {
|
|
316
|
+
result = sntz.DATE(value, { fallback });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// CRUD mixins.
|
|
2
|
+
const { implementsCRUD } = require('./mixins');
|
|
3
|
+
// ORM.
|
|
4
|
+
const { DataTypes } = require('sequelize');
|
|
5
|
+
|
|
6
|
+
// Utils.
|
|
7
|
+
// const {
|
|
8
|
+
// pluralize,
|
|
9
|
+
// underscore
|
|
10
|
+
// } = require('../utils/strings.util');
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
module.exports = _defineModel;
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* params:
|
|
17
|
+
* - databaseConnection (Instance of Sequilize)
|
|
18
|
+
* - modelName (String)
|
|
19
|
+
* - definition (function)
|
|
20
|
+
* - options (Object)
|
|
21
|
+
* - ... Sequilize model options
|
|
22
|
+
* - noCRUD (Bool)
|
|
23
|
+
*/
|
|
24
|
+
function _defineModel(
|
|
25
|
+
databaseConnection,
|
|
26
|
+
modelName='',
|
|
27
|
+
definition=()=>{},
|
|
28
|
+
options={}
|
|
29
|
+
) {
|
|
30
|
+
const definitionObject = definition( DataTypes );
|
|
31
|
+
const _options = {
|
|
32
|
+
// Set snake-cased table name.
|
|
33
|
+
// tableName: underscore( pluralize(modelName) ),
|
|
34
|
+
// Set snake-case.
|
|
35
|
+
underscored: true,
|
|
36
|
+
// Enable automatic 'created_at' and 'updated_at' fields.
|
|
37
|
+
timestamps: true,
|
|
38
|
+
|
|
39
|
+
// The only way to get snake-cased timestamps (issue: https://github.com/sequelize/sequelize/issues/10857)
|
|
40
|
+
createdAt: 'created_at',
|
|
41
|
+
updatedAt: 'updated_at',
|
|
42
|
+
deletedAt: 'deleted_at',
|
|
43
|
+
|
|
44
|
+
// Add user-defined options (they can override upper ones).
|
|
45
|
+
...options
|
|
46
|
+
};
|
|
47
|
+
const model = databaseConnection.define(modelName, definitionObject, _options);
|
|
48
|
+
|
|
49
|
+
if (options.noCRUD !== true) {
|
|
50
|
+
// Add createWithIncludes, findById, updateById, deleteById, etc.
|
|
51
|
+
implementsCRUD(model);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Instance methods:
|
|
55
|
+
model.prototype.toJSON = function() {
|
|
56
|
+
const values = { ...this.get() };
|
|
57
|
+
return values;
|
|
58
|
+
}
|
|
59
|
+
// Instance methods\
|
|
60
|
+
|
|
61
|
+
return model;
|
|
62
|
+
}
|