@hitchy/plugin-odem-rest 0.5.6 → 0.7.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/api/services/odem-rest/cors.js +5 -5
- package/index.js +128 -83
- package/package.json +13 -14
- package/readme.md +101 -12
- package/api/services/odem-rest/schema.js +0 -208
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module.exports = function() {
|
|
4
4
|
const Config = this.config;
|
|
5
|
-
const Services = this.
|
|
5
|
+
const Services = this.services;
|
|
6
6
|
|
|
7
7
|
const CommonlyAcceptedHeaders = [
|
|
8
8
|
"Accept", "Accept-Language", "Authorization", "Content-Language",
|
|
@@ -28,7 +28,7 @@ module.exports = function() {
|
|
|
28
28
|
/**
|
|
29
29
|
* Handles CORS-related behaviours.
|
|
30
30
|
*
|
|
31
|
-
* @name api.
|
|
31
|
+
* @name api.services.OdemRestCors
|
|
32
32
|
*/
|
|
33
33
|
class OdemRestCors {
|
|
34
34
|
/**
|
|
@@ -84,7 +84,7 @@ module.exports = function() {
|
|
|
84
84
|
static getRequestFilterForModel( model ) { // eslint-disable-line no-unused-vars
|
|
85
85
|
return ( req, res, next ) => {
|
|
86
86
|
if ( !res.headersSent ) {
|
|
87
|
-
if ( Services.
|
|
87
|
+
if ( Services.OdemSchema.mayBeExposed( req, model ) ) {
|
|
88
88
|
this.handleMethods( model, null, req, res, Accepted.model.methods );
|
|
89
89
|
|
|
90
90
|
if ( res.hasHeader( "Access-Control-Allow-Origin" ) ) {
|
|
@@ -112,7 +112,7 @@ module.exports = function() {
|
|
|
112
112
|
static getRequestFilterForModelSchema( model ) { // eslint-disable-line no-unused-vars
|
|
113
113
|
return ( req, res, next ) => {
|
|
114
114
|
if ( !res.headersSent ) {
|
|
115
|
-
if ( Services.
|
|
115
|
+
if ( Services.OdemSchema.mayBeExposed( req, model ) ) {
|
|
116
116
|
this.handleMethods( model, null, req, res, Accepted.schema.methods );
|
|
117
117
|
|
|
118
118
|
if ( res.hasHeader( "Access-Control-Allow-Origin" ) ) {
|
|
@@ -140,7 +140,7 @@ module.exports = function() {
|
|
|
140
140
|
static getRequestFilterForModelItem( model ) { // eslint-disable-line no-unused-vars
|
|
141
141
|
return ( req, res, next ) => {
|
|
142
142
|
if ( !res.headersSent && req.params.uuid !== ".schema" ) {
|
|
143
|
-
if ( Services.
|
|
143
|
+
if ( Services.OdemSchema.mayBeExposed( req, model ) ) {
|
|
144
144
|
this.handleMethods( model, req.params.uuid, req, res, Accepted.item.methods );
|
|
145
145
|
|
|
146
146
|
if ( res.hasHeader( "Access-Control-Allow-Origin" ) ) {
|
package/index.js
CHANGED
|
@@ -4,7 +4,7 @@ const { posix: { resolve } } = require( "path" );
|
|
|
4
4
|
|
|
5
5
|
module.exports = function() {
|
|
6
6
|
const api = this;
|
|
7
|
-
const {
|
|
7
|
+
const { services: Services, models: Models, utility: { case: Case } } = api;
|
|
8
8
|
|
|
9
9
|
const logDebug = api.log( "hitchy:odem:rest:debug" );
|
|
10
10
|
const logError = api.log( "hitchy:odem:rest:error" );
|
|
@@ -70,7 +70,7 @@ module.exports = function() {
|
|
|
70
70
|
const routeName = Case.pascalToKebab( name );
|
|
71
71
|
const model = Models[name] || {};
|
|
72
72
|
|
|
73
|
-
addRoutesOnModel( routes, urlPrefix, routeName, model, convenience );
|
|
73
|
+
addRoutesOnModel( routes, urlPrefix, routeName, name, model, convenience );
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
routes.set( "HEAD " + resolve( urlPrefix, ":model" ), ( _, res ) => res.status( 404 ).send() );
|
|
@@ -80,6 +80,20 @@ module.exports = function() {
|
|
|
80
80
|
},
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Generates JSON response indicating rejection of access.
|
|
85
|
+
*
|
|
86
|
+
* @param {Hitchy.Core.ServerResponse} res response manager
|
|
87
|
+
* @returns {void}
|
|
88
|
+
*/
|
|
89
|
+
function resAccessForbidden( res ) {
|
|
90
|
+
res
|
|
91
|
+
.status( 403 )
|
|
92
|
+
.json( {
|
|
93
|
+
error: "access forbidden",
|
|
94
|
+
} );
|
|
95
|
+
}
|
|
96
|
+
|
|
83
97
|
/**
|
|
84
98
|
* Adds routes handling common requests not related to particular model.
|
|
85
99
|
*
|
|
@@ -90,7 +104,7 @@ module.exports = function() {
|
|
|
90
104
|
* @returns {void}
|
|
91
105
|
*/
|
|
92
106
|
function addGlobalRoutes( routes, urlPrefix, models ) {
|
|
93
|
-
const {
|
|
107
|
+
const { services: { Model: BaseModel, OdemSchema }, utility: { case: { pascalToKebab } } } = api;
|
|
94
108
|
|
|
95
109
|
routes.set( `GET ${resolve( urlPrefix, ".schema" )}`, reqFetchSchemata );
|
|
96
110
|
|
|
@@ -101,7 +115,12 @@ module.exports = function() {
|
|
|
101
115
|
* @param {Hitchy.Core.ServerResponse} res response manager
|
|
102
116
|
* @returns {void}
|
|
103
117
|
*/
|
|
104
|
-
function reqFetchSchemata( req, res ) {
|
|
118
|
+
async function reqFetchSchemata( req, res ) {
|
|
119
|
+
if ( api.plugins.authentication && !await Services.Authorization.mayAccess( req.user, "@hitchy.odem.schema" ) ) {
|
|
120
|
+
resAccessForbidden( res );
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
105
124
|
const modelKeys = Object.keys( models );
|
|
106
125
|
const numModels = modelKeys.length;
|
|
107
126
|
|
|
@@ -111,11 +130,11 @@ module.exports = function() {
|
|
|
111
130
|
const model = models[modelKeys[i]];
|
|
112
131
|
|
|
113
132
|
if ( model.prototype instanceof BaseModel &&
|
|
114
|
-
|
|
115
|
-
|
|
133
|
+
OdemSchema.mayBeExposed( req, model ) &&
|
|
134
|
+
OdemSchema.mayBePromoted( req, model ) ) {
|
|
116
135
|
const slug = pascalToKebab( model.name );
|
|
117
136
|
|
|
118
|
-
result[slug] =
|
|
137
|
+
result[slug] = OdemSchema.extractPublicData( model );
|
|
119
138
|
}
|
|
120
139
|
}
|
|
121
140
|
|
|
@@ -130,12 +149,13 @@ module.exports = function() {
|
|
|
130
149
|
* route patterns into function handling requests matching that pattern
|
|
131
150
|
* @param {string} urlPrefix common prefix to use on every route regarding any model-related processing
|
|
132
151
|
* @param {string} routeName name of model to be used in path name of request
|
|
152
|
+
* @param {string} modelName name of model derived from name extracted from path name of request in `routeName`
|
|
133
153
|
* @param {class<Model>} Model model class
|
|
134
154
|
* @param {boolean} includeConvenienceRoutes set true to include additional set of routes for controlling all action via GET-requests
|
|
135
155
|
* @returns {void}
|
|
136
156
|
*/
|
|
137
|
-
function addRoutesOnModel( routes, urlPrefix, routeName, Model, includeConvenienceRoutes ) {
|
|
138
|
-
const { Model: BaseModel,
|
|
157
|
+
function addRoutesOnModel( routes, urlPrefix, routeName, modelName, Model, includeConvenienceRoutes ) {
|
|
158
|
+
const { Model: BaseModel, OdemSchema: Schema, OdemUtilityUuid: { ptnUuid } } = Services;
|
|
139
159
|
|
|
140
160
|
const modelUrl = resolve( urlPrefix, routeName );
|
|
141
161
|
|
|
@@ -182,7 +202,7 @@ module.exports = function() {
|
|
|
182
202
|
* @returns {void}
|
|
183
203
|
*/
|
|
184
204
|
function reqSuccess( req, res ) {
|
|
185
|
-
if ( Services.
|
|
205
|
+
if ( Services.OdemSchema.mayBeExposed( req, Model ) ) {
|
|
186
206
|
res.status( 200 ).send();
|
|
187
207
|
} else {
|
|
188
208
|
res.status( 403 ).send();
|
|
@@ -209,10 +229,15 @@ module.exports = function() {
|
|
|
209
229
|
* @param {Hitchy.Core.ServerResponse} res API for creating response
|
|
210
230
|
* @returns {void}
|
|
211
231
|
*/
|
|
212
|
-
function reqFetchSchema( req, res ) {
|
|
232
|
+
async function reqFetchSchema( req, res ) {
|
|
213
233
|
logDebug( "got request fetching schema" );
|
|
214
234
|
|
|
215
|
-
if ( !Services.
|
|
235
|
+
if ( api.plugins.authentication && !await Services.Authorization.mayAccess( req.user, `@hitchy.odem.model.${modelName}.schema` ) ) {
|
|
236
|
+
resAccessForbidden( res );
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if ( !Services.OdemSchema.mayBeExposed( req, Model ) ) {
|
|
216
241
|
res.status( 403 ).json( { error: "access forbidden by model" } );
|
|
217
242
|
return;
|
|
218
243
|
}
|
|
@@ -228,23 +253,28 @@ module.exports = function() {
|
|
|
228
253
|
* @param {Hitchy.Core.ServerResponse} res API for creating response
|
|
229
254
|
* @returns {Promise|undefined} promises request processed successfully
|
|
230
255
|
*/
|
|
231
|
-
function reqCheckItem( req, res ) {
|
|
256
|
+
async function reqCheckItem( req, res ) {
|
|
232
257
|
logDebug( "got request checking if some item exists" );
|
|
233
258
|
|
|
259
|
+
if ( api.plugins.authentication && !await Services.Authorization.mayAccess( req.user, `@hitchy.odem.model.${modelName}.check` ) ) {
|
|
260
|
+
resAccessForbidden( res );
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
234
264
|
if ( !Schema.mayBeExposed( req, Model ) ) {
|
|
235
265
|
res.status( 403 ).json( { error: "access forbidden by model" } );
|
|
236
|
-
return
|
|
266
|
+
return;
|
|
237
267
|
}
|
|
238
268
|
|
|
239
269
|
const { uuid } = req.params;
|
|
240
270
|
if ( !ptnUuid.test( uuid ) ) {
|
|
241
271
|
res.status( 400 ).send();
|
|
242
|
-
return
|
|
272
|
+
return;
|
|
243
273
|
}
|
|
244
274
|
|
|
245
275
|
const item = new Model( uuid ); // eslint-disable-line new-cap
|
|
246
276
|
|
|
247
|
-
|
|
277
|
+
await item.$exists
|
|
248
278
|
.then( exists => {
|
|
249
279
|
res.status( exists ? 200 : 404 ).send();
|
|
250
280
|
} )
|
|
@@ -259,26 +289,31 @@ module.exports = function() {
|
|
|
259
289
|
*
|
|
260
290
|
* @param {Hitchy.Core.IncomingMessage} req description of request
|
|
261
291
|
* @param {Hitchy.Core.ServerResponse} res API for creating response
|
|
262
|
-
* @returns {Promise} promises request processed successfully
|
|
292
|
+
* @returns {Promise<void>} promises request processed successfully
|
|
263
293
|
*/
|
|
264
|
-
function reqFetchItem( req, res ) {
|
|
294
|
+
async function reqFetchItem( req, res ) {
|
|
265
295
|
logDebug( "got request fetching some item" );
|
|
266
296
|
|
|
297
|
+
if ( api.plugins.authentication && !await Services.Authorization.mayAccess( req.user, `@hitchy.odem.model.${modelName}.read` ) ) {
|
|
298
|
+
resAccessForbidden( res );
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
267
302
|
if ( !Schema.mayBeExposed( req, Model ) ) {
|
|
268
303
|
res.status( 403 ).json( { error: "access forbidden by model" } );
|
|
269
|
-
return
|
|
304
|
+
return;
|
|
270
305
|
}
|
|
271
306
|
|
|
272
307
|
const { uuid } = req.params;
|
|
273
308
|
if ( !ptnUuid.test( uuid ) ) {
|
|
274
309
|
res.status( 400 ).json( { error: "invalid UUID" } );
|
|
275
|
-
return
|
|
310
|
+
return;
|
|
276
311
|
}
|
|
277
312
|
|
|
278
313
|
const item = new Model( uuid ); // eslint-disable-line new-cap
|
|
279
314
|
|
|
280
|
-
|
|
281
|
-
.then( loaded => res.json( loaded.toObject( { serialized: true } ) ) )
|
|
315
|
+
await item.load()
|
|
316
|
+
.then( loaded => res.json( Schema.filterItem( loaded.toObject( { serialized: true } ), req, Model, "read" ) ) )
|
|
282
317
|
.catch( error => {
|
|
283
318
|
logError( "fetching %s:", routeName, error );
|
|
284
319
|
|
|
@@ -287,6 +322,7 @@ module.exports = function() {
|
|
|
287
322
|
res.status( 404 ).json( { error: "selected item not found" } );
|
|
288
323
|
break;
|
|
289
324
|
}
|
|
325
|
+
|
|
290
326
|
default : {
|
|
291
327
|
res.status( 500 ).json( { error: error.message } );
|
|
292
328
|
}
|
|
@@ -302,17 +338,22 @@ module.exports = function() {
|
|
|
302
338
|
* @param {Hitchy.Core.ServerResponse} res response controller
|
|
303
339
|
* @returns {Promise} promises response sent
|
|
304
340
|
*/
|
|
305
|
-
function reqFetchItems( req, res ) {
|
|
341
|
+
async function reqFetchItems( req, res ) {
|
|
306
342
|
logDebug( "got request fetching items" );
|
|
307
343
|
|
|
344
|
+
if ( api.plugins.authentication && !await Services.Authorization.mayAccess( req.user, `@hitchy.odem.model.${modelName}.list` ) ) {
|
|
345
|
+
resAccessForbidden( res );
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
308
349
|
if ( !Schema.mayBeExposed( req, Model ) ) {
|
|
309
350
|
res.status( 403 ).json( { error: "access forbidden by model" } );
|
|
310
|
-
return
|
|
351
|
+
return;
|
|
311
352
|
}
|
|
312
353
|
|
|
313
354
|
if ( req.headers["x-list-as-array"] ) {
|
|
314
355
|
res.status( 400 ).json( { error: "fetching items as array is deprecated for security reasons" } );
|
|
315
|
-
return
|
|
356
|
+
return;
|
|
316
357
|
}
|
|
317
358
|
|
|
318
359
|
let handler = reqListAll;
|
|
@@ -321,7 +362,7 @@ module.exports = function() {
|
|
|
321
362
|
handler = reqListMatches;
|
|
322
363
|
}
|
|
323
364
|
|
|
324
|
-
|
|
365
|
+
await handler.call( this, req, res );
|
|
325
366
|
}
|
|
326
367
|
|
|
327
368
|
/**
|
|
@@ -332,7 +373,6 @@ module.exports = function() {
|
|
|
332
373
|
*/
|
|
333
374
|
function parseQuery( query ) {
|
|
334
375
|
const simpleTernary = /^([^:\s]+):between:([^:]+):([^:]+)$/i.exec( query );
|
|
335
|
-
|
|
336
376
|
if ( simpleTernary ) {
|
|
337
377
|
const [ , name, lower, upper ] = simpleTernary;
|
|
338
378
|
|
|
@@ -422,13 +462,13 @@ module.exports = function() {
|
|
|
422
462
|
|
|
423
463
|
const meta = count || req.headers["x-count"] ? {} : null;
|
|
424
464
|
|
|
425
|
-
await Model.find( parsedQuery, { offset, limit, sortBy, sortAscendingly: !descending }, {
|
|
465
|
+
await Model.find( Schema.checkQuery( parsedQuery, req, Model ), { offset, limit, sortBy, sortAscendingly: !descending }, {
|
|
426
466
|
metaCollector: meta,
|
|
427
467
|
loadRecords
|
|
428
468
|
} )
|
|
429
469
|
.then( matches => {
|
|
430
470
|
const result = {
|
|
431
|
-
items: matches.map(
|
|
471
|
+
items: matches.map( item => Schema.filterItem( item.toObject( { serialized: true } ), req, Model, "list" ) ),
|
|
432
472
|
};
|
|
433
473
|
|
|
434
474
|
if ( meta ) {
|
|
@@ -453,17 +493,18 @@ module.exports = function() {
|
|
|
453
493
|
* @param {Hitchy.Core.ServerResponse} res API for creating response
|
|
454
494
|
* @returns {Promise|undefined} promises request processed successfully
|
|
455
495
|
*/
|
|
456
|
-
function reqListAll( req, res ) {
|
|
496
|
+
async function reqListAll( req, res ) {
|
|
457
497
|
logDebug( "got request listing all items" );
|
|
458
498
|
|
|
459
499
|
if ( !Schema.mayBeExposed( req, Model ) ) {
|
|
460
500
|
res.status( 403 ).json( { error: "access forbidden by model" } );
|
|
461
|
-
return
|
|
501
|
+
return;
|
|
462
502
|
}
|
|
463
503
|
|
|
464
504
|
const { offset = 0, limit = Infinity, sortBy = null, descending = false, loadRecords = true, count = false } = req.query;
|
|
465
505
|
const meta = count || req.headers["x-count"] ? {} : null;
|
|
466
|
-
|
|
506
|
+
|
|
507
|
+
await Model.list( {
|
|
467
508
|
offset,
|
|
468
509
|
limit,
|
|
469
510
|
sortBy,
|
|
@@ -471,7 +512,7 @@ module.exports = function() {
|
|
|
471
512
|
}, { loadRecords, metaCollector: meta } )
|
|
472
513
|
.then( matches => {
|
|
473
514
|
const result = {
|
|
474
|
-
items: matches.map(
|
|
515
|
+
items: matches.map( item => Schema.filterItem( item.toObject( { serialized: true } ), req, Model, "list" ) ),
|
|
475
516
|
};
|
|
476
517
|
|
|
477
518
|
if ( meta ) {
|
|
@@ -495,38 +536,37 @@ module.exports = function() {
|
|
|
495
536
|
* @param {Hitchy.Core.ServerResponse} res API for creating response
|
|
496
537
|
* @returns {Promise|undefined} promises request processed successfully
|
|
497
538
|
*/
|
|
498
|
-
function reqCreateItem( req, res ) {
|
|
539
|
+
async function reqCreateItem( req, res ) {
|
|
499
540
|
logDebug( "got request creating item" );
|
|
500
541
|
|
|
542
|
+
if ( api.plugins.authentication && !await Services.Authorization.mayAccess( req.user, `@hitchy.odem.model.${modelName}.create` ) ) {
|
|
543
|
+
resAccessForbidden( res );
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
501
547
|
if ( !Schema.mayBeExposed( req, Model ) ) {
|
|
502
548
|
res.status( 403 ).json( { error: "access forbidden by model" } );
|
|
503
|
-
return
|
|
549
|
+
return;
|
|
504
550
|
}
|
|
505
551
|
|
|
506
552
|
const item = new Model(); // eslint-disable-line new-cap
|
|
507
553
|
|
|
508
|
-
|
|
554
|
+
await ( req.method === "GET" ? Promise.resolve( req.query ) : req.fetchBody() )
|
|
509
555
|
.then( record => {
|
|
510
|
-
if ( record.uuid ) {
|
|
511
|
-
logDebug( "creating %s:", routeName, "new entry can not be created with uuid" );
|
|
512
|
-
res.status( 400 ).json( { error: "new entry can not be created with uuid" } );
|
|
513
|
-
return undefined;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
556
|
if ( record ) {
|
|
517
|
-
|
|
518
|
-
|
|
557
|
+
if ( record.uuid ) {
|
|
558
|
+
logDebug( "creating %s:", routeName, "new entry can not be created with uuid" );
|
|
559
|
+
res.status( 400 ).json( { error: "new entry can not be created with uuid" } );
|
|
560
|
+
return undefined;
|
|
561
|
+
}
|
|
519
562
|
|
|
563
|
+
const filtered = Schema.filterItem( record, req, Model, "create" );
|
|
520
564
|
const definedProps = Model.schema.props;
|
|
521
565
|
const definedComputed = Model.schema.computed;
|
|
522
566
|
|
|
523
|
-
for (
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
if ( definedProps[name] ) {
|
|
527
|
-
item.$properties[name] = record[name];
|
|
528
|
-
} else if ( definedComputed[name] ) {
|
|
529
|
-
item[name] = record[name];
|
|
567
|
+
for ( const [ key, value ] of Object.entries( filtered ) ) {
|
|
568
|
+
if ( definedProps[key]?.$type || definedComputed[key] ) {
|
|
569
|
+
item[key] = value;
|
|
530
570
|
}
|
|
531
571
|
}
|
|
532
572
|
}
|
|
@@ -549,23 +589,28 @@ module.exports = function() {
|
|
|
549
589
|
* @param {Hitchy.Core.ServerResponse} res API for creating response
|
|
550
590
|
* @returns {Promise|undefined} promises request processed successfully
|
|
551
591
|
*/
|
|
552
|
-
function reqModifyItem( req, res ) {
|
|
592
|
+
async function reqModifyItem( req, res ) {
|
|
553
593
|
logDebug( "got request to modify some item" );
|
|
554
594
|
|
|
595
|
+
if ( api.plugins.authentication && !await Services.Authorization.mayAccess( req.user, `@hitchy.odem.model.${modelName}.write` ) ) {
|
|
596
|
+
resAccessForbidden( res );
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
555
600
|
if ( !Schema.mayBeExposed( req, Model ) ) {
|
|
556
601
|
res.status( 403 ).json( { error: "access forbidden by model" } );
|
|
557
|
-
return
|
|
602
|
+
return;
|
|
558
603
|
}
|
|
559
604
|
|
|
560
605
|
const { uuid } = req.params;
|
|
561
606
|
if ( !ptnUuid.test( uuid ) ) {
|
|
562
607
|
res.status( 400 ).json( { error: "invalid UUID" } );
|
|
563
|
-
return
|
|
608
|
+
return;
|
|
564
609
|
}
|
|
565
610
|
|
|
566
611
|
const item = new Model( uuid ); // eslint-disable-line new-cap
|
|
567
612
|
|
|
568
|
-
|
|
613
|
+
await item.$exists
|
|
569
614
|
.then( exists => {
|
|
570
615
|
if ( !exists ) {
|
|
571
616
|
res.status( 404 ).json( { error: "selected item not found" } );
|
|
@@ -578,27 +623,20 @@ module.exports = function() {
|
|
|
578
623
|
] )
|
|
579
624
|
.then( ( [ loaded, record ] ) => {
|
|
580
625
|
if ( record ) {
|
|
581
|
-
|
|
582
|
-
const names = Object.keys( record );
|
|
583
|
-
const numNames = names.length;
|
|
584
|
-
|
|
626
|
+
const filtered = Schema.filterItem( record, req, Model, "write" );
|
|
585
627
|
const definedProps = Model.schema.props;
|
|
586
628
|
const definedComputed = Model.schema.computed;
|
|
587
629
|
|
|
588
|
-
for (
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if ( definedProps[name] ) {
|
|
592
|
-
loaded.$properties[name] = record[name]; // eslint-disable-line no-param-reassign
|
|
593
|
-
} else if ( definedComputed[name] ) {
|
|
594
|
-
loaded[name] = record[name]; // eslint-disable-line no-param-reassign
|
|
630
|
+
for ( const [ key, value ] of Object.entries( filtered ) ) {
|
|
631
|
+
if ( definedProps[key]?.$type || definedComputed[key] ) {
|
|
632
|
+
loaded[key] = value; // eslint-disable-line no-param-reassign
|
|
595
633
|
}
|
|
596
634
|
}
|
|
597
635
|
}
|
|
598
636
|
|
|
599
637
|
return loaded.save()
|
|
600
638
|
.then( saved => {
|
|
601
|
-
res.json( saved.toObject( { serialized: true } ) );
|
|
639
|
+
res.json( Schema.filterItem( saved.toObject( { serialized: true } ), req, Model, "read" ) );
|
|
602
640
|
} );
|
|
603
641
|
} );
|
|
604
642
|
} )
|
|
@@ -616,23 +654,28 @@ module.exports = function() {
|
|
|
616
654
|
* @param {Hitchy.Core.ServerResponse} res API for creating response
|
|
617
655
|
* @returns {Promise|undefined} promises request processed successfully
|
|
618
656
|
*/
|
|
619
|
-
function reqReplaceItem( req, res ) {
|
|
657
|
+
async function reqReplaceItem( req, res ) {
|
|
620
658
|
logDebug( "got request replacing some item" );
|
|
621
659
|
|
|
660
|
+
if ( api.plugins.authentication && !await Services.Authorization.mayAccess( req.user, `@hitchy.odem.model.${modelName}.write` ) ) {
|
|
661
|
+
resAccessForbidden( res );
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
622
665
|
if ( !Schema.mayBeExposed( req, Model ) ) {
|
|
623
666
|
res.status( 403 ).json( { error: "access forbidden by model" } );
|
|
624
|
-
return
|
|
667
|
+
return;
|
|
625
668
|
}
|
|
626
669
|
|
|
627
670
|
const { uuid } = req.params;
|
|
628
671
|
if ( !ptnUuid.test( uuid ) ) {
|
|
629
672
|
res.status( 400 ).json( { error: "invalid UUID" } );
|
|
630
|
-
return
|
|
673
|
+
return;
|
|
631
674
|
}
|
|
632
675
|
|
|
633
676
|
const item = new Model( uuid, { onUnsaved: false } ); // eslint-disable-line new-cap
|
|
634
677
|
|
|
635
|
-
|
|
678
|
+
await Promise.all( [ item.$exists, req.method === "GET" ? Promise.resolve( req.query ) : req.fetchBody() ] )
|
|
636
679
|
.then( ( [ exists, record ] ) => {
|
|
637
680
|
return ( exists ? item.load() : Promise.resolve() ).then( () => {
|
|
638
681
|
const propNames = Object.keys( Model.schema.props );
|
|
@@ -640,30 +683,27 @@ module.exports = function() {
|
|
|
640
683
|
|
|
641
684
|
const computedNames = Object.keys( Model.schema.computed );
|
|
642
685
|
const numComputedNames = computedNames.length;
|
|
686
|
+
const filtered = Schema.filterItem( record, req, Model, "write" );
|
|
643
687
|
|
|
644
688
|
// update properties, drop those missing in provided record
|
|
645
689
|
for ( let i = 0; i < numPropNames; i++ ) {
|
|
646
690
|
const propName = propNames[i];
|
|
647
|
-
const value =
|
|
691
|
+
const value = filtered[propName];
|
|
648
692
|
|
|
649
|
-
|
|
650
|
-
item.$properties[propName] = null;
|
|
651
|
-
} else {
|
|
652
|
-
item.$properties[propName] = value;
|
|
653
|
-
}
|
|
693
|
+
item[propName] = value ?? null;
|
|
654
694
|
}
|
|
655
695
|
|
|
656
696
|
// assign all posted computed properties
|
|
657
697
|
for ( let i = 0; i < numComputedNames; i++ ) {
|
|
658
698
|
const computedName = computedNames[i];
|
|
659
|
-
item[computedName] =
|
|
699
|
+
item[computedName] = filtered[computedName];
|
|
660
700
|
}
|
|
661
701
|
|
|
662
702
|
return item.save( { ignoreUnloaded: !exists } );
|
|
663
703
|
} );
|
|
664
704
|
} )
|
|
665
705
|
.then( saved => {
|
|
666
|
-
res.json( {
|
|
706
|
+
res.json( Schema.filterItem( saved.toObject( { serialized: true } ), req, Model, "read" ) );
|
|
667
707
|
} )
|
|
668
708
|
.catch( error => {
|
|
669
709
|
logError( "updating %s:", routeName, error );
|
|
@@ -678,23 +718,28 @@ module.exports = function() {
|
|
|
678
718
|
* @param {Hitchy.Core.ServerResponse} res API for creating response
|
|
679
719
|
* @returns {Promise|undefined} promises request processed successfully
|
|
680
720
|
*/
|
|
681
|
-
function reqRemoveItem( req, res ) {
|
|
721
|
+
async function reqRemoveItem( req, res ) {
|
|
682
722
|
logDebug( "got request removing some item" );
|
|
683
723
|
|
|
724
|
+
if ( api.plugins.authentication && !await Services.Authorization.mayAccess( req.user, `@hitchy.odem.model.${modelName}.remove` ) ) {
|
|
725
|
+
resAccessForbidden( res );
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
684
729
|
if ( !Schema.mayBeExposed( req, Model ) ) {
|
|
685
730
|
res.status( 403 ).json( { error: "access forbidden by model" } );
|
|
686
|
-
return
|
|
731
|
+
return;
|
|
687
732
|
}
|
|
688
733
|
|
|
689
734
|
const { uuid } = req.params;
|
|
690
735
|
if ( !ptnUuid.test( uuid ) ) {
|
|
691
736
|
res.status( 400 ).json( { error: "invalid UUID" } );
|
|
692
|
-
return
|
|
737
|
+
return;
|
|
693
738
|
}
|
|
694
739
|
|
|
695
740
|
const item = new Model( uuid ); // eslint-disable-line new-cap
|
|
696
741
|
|
|
697
|
-
|
|
742
|
+
await item.$exists
|
|
698
743
|
.then( exists => {
|
|
699
744
|
if ( exists ) {
|
|
700
745
|
return item.remove()
|
package/package.json
CHANGED
|
@@ -1,34 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hitchy/plugin-odem-rest",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "HTTP REST API for Hitchy's
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "HTTP REST API for Hitchy's document-oriented database",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"lint": "eslint .",
|
|
8
|
-
"test": "hitchy-pm odem --exec mocha --ui=tdd 'test/scripts/**/*.js'"
|
|
9
|
-
"coverage": "hitchy-pm odem --exec c8 mocha --ui=tdd 'test/scripts/**/*.js'"
|
|
8
|
+
"test": "hitchy-pm odem auth --exec c8 mocha --ui=tdd 'test/scripts/**/*.js'"
|
|
10
9
|
},
|
|
11
10
|
"repository": "https://gitlab.com/hitchy/plugin-odem-rest.git",
|
|
12
11
|
"keywords": [
|
|
13
|
-
"hitchy"
|
|
14
|
-
"ODM"
|
|
12
|
+
"hitchy"
|
|
15
13
|
],
|
|
16
14
|
"author": "Thomas Urban",
|
|
17
15
|
"license": "MIT",
|
|
18
16
|
"bugs": "https://gitlab.com/hitchy/plugin-odem-rest/-/issues",
|
|
19
17
|
"homepage": "https://gitlab.com/hitchy/plugin-odem-rest#plugin-odem-rest",
|
|
20
18
|
"peerDependencies": {
|
|
21
|
-
"@hitchy/core": "
|
|
22
|
-
"@hitchy/plugin-odem": "
|
|
19
|
+
"@hitchy/core": "0.8.x",
|
|
20
|
+
"@hitchy/plugin-odem": "0.9.x",
|
|
21
|
+
"@hitchy/plugin-auth": "0.4.x"
|
|
23
22
|
},
|
|
24
23
|
"devDependencies": {
|
|
25
24
|
"@hitchy/types": "^0.1.3",
|
|
26
|
-
"@hitchy/server-dev-tools": "^0.4.
|
|
27
|
-
"c8": "^
|
|
28
|
-
"eslint": "^8.
|
|
29
|
-
"eslint-config-cepharum": "^1.0.
|
|
30
|
-
"eslint-plugin-promise": "^6.
|
|
31
|
-
"mocha": "^10.
|
|
25
|
+
"@hitchy/server-dev-tools": "^0.4.9",
|
|
26
|
+
"c8": "^10.1.2",
|
|
27
|
+
"eslint": "^8.57.0",
|
|
28
|
+
"eslint-config-cepharum": "^1.0.14",
|
|
29
|
+
"eslint-plugin-promise": "^6.6.0",
|
|
30
|
+
"mocha": "^10.7.3",
|
|
32
31
|
"should": "^13.2.3",
|
|
33
32
|
"should-http": "^0.1.1"
|
|
34
33
|
}
|
package/readme.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# plugin-odem-rest [](https://gitlab.com/hitchy/plugin-odem-rest/-/commits/master)
|
|
2
2
|
|
|
3
|
-
HTTP REST API for [Hitchy's](https://core.hitchy.org/) [
|
|
3
|
+
HTTP REST API for [Hitchy's](https://core.hitchy.org/) [document-oriented database](https://odem.hitchy.org/)
|
|
4
4
|
|
|
5
|
-
[Hitchy](http://core.hitchy.org/) is a server-side framework for developing web applications with [Node.js](https://nodejs.org/). [Odem](https://www.npmjs.com/package/@hitchy/plugin-odem) is plugin for Hitchy implementing
|
|
5
|
+
[Hitchy](http://core.hitchy.org/) is a server-side framework for developing web applications with [Node.js](https://nodejs.org/). [Odem](https://www.npmjs.com/package/@hitchy/plugin-odem) is a plugin for Hitchy implementing a document-oriented database using data backends like regular file systems, LevelDBs and temporary in-memory databases.
|
|
6
6
|
|
|
7
|
-
This plugin is defining blueprint routes for accessing data managed in
|
|
7
|
+
This plugin is defining blueprint routes for accessing data managed in document-oriented database using REST API.
|
|
8
8
|
|
|
9
9
|
## License
|
|
10
10
|
|
|
@@ -21,15 +21,34 @@ npm i @hitchy/plugin-odem-rest @hitchy/plugin-odem
|
|
|
21
21
|
The command is installing this plugin and the additionally required [@hitchy/plugin-odem](https://www.npmjs.com/package/@hitchy/plugin-odem).
|
|
22
22
|
|
|
23
23
|
:::warning Compatibility
|
|
24
|
-
Starting with version 0.4.0 the latter plugin must be installed
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
Starting with version 0.4.0 the latter plugin must be installed explicitly.
|
|
25
|
+
:::
|
|
26
|
+
|
|
27
|
+
:::warning Compatibility
|
|
28
|
+
Starting with version 0.6.0 authorization is tightened as soon as [@hitchy/plugin-auth](https://auth.hitchy.org) is discovered in current project. You need an [additional configuration](#authorization) to keep the REST API as open as before.
|
|
29
|
+
:::
|
|
27
30
|
|
|
28
31
|
## Usage
|
|
29
32
|
|
|
30
33
|
This package depends on [@hitchy/plugin-odem](https://odem.hitchy.org/) and its preparation of model definitions discovered by [Hitchy's core](https://core.hitchy.org/). See the linked manuals for additional information.
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
In addition, [@hitchy/plugin-auth](https://auth.hitchy.org) is supported resulting in a tightened access control. When using that plugin in a project, a file **config/auth.js** with following content is necessary to test-drive this plugin's REST API:
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
exports.auth = {
|
|
39
|
+
authorizations: {
|
|
40
|
+
"@hitchy.odem": "*"
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> **Do not use this in a production setup!**
|
|
46
|
+
>
|
|
47
|
+
> This example is granting permissions to create, adjust and remove items of your document-oriented database without any authentication. In addition, all _protected_ models and properties get exposed to everyone.
|
|
48
|
+
>
|
|
49
|
+
> See the [section on authorization](#authorization) for additional information!
|
|
50
|
+
|
|
51
|
+
Now, for a quick start example create a file **api/models/local-employee.js** in your Hitchy-based application with the following content:
|
|
33
52
|
|
|
34
53
|
```javascript
|
|
35
54
|
module.exports = {
|
|
@@ -60,7 +79,7 @@ module.exports = {
|
|
|
60
79
|
};
|
|
61
80
|
```
|
|
62
81
|
|
|
63
|
-
When starting your Hitchy-based application it will discover a model named **LocalEmployee** and expose it via REST API using
|
|
82
|
+
When starting your Hitchy-based application it will discover a model named **LocalEmployee** and expose it via REST API using base URL `/api/local-employee` just because of this package and its dependencies mentioned before.
|
|
64
83
|
|
|
65
84
|
#### Models of Hitchy plugins
|
|
66
85
|
|
|
@@ -69,7 +88,7 @@ Due to the way Hitchy is discovering plugins and compiling [components](https://
|
|
|
69
88
|
|
|
70
89
|
### How it works
|
|
71
90
|
|
|
72
|
-
This plugin is defining a set of [blueprint routes](https://core.hitchy.org/internals/routing-basics.html#focusing-on-routes) implementing REST API for every model defined in file system as described before.
|
|
91
|
+
This plugin is defining a set of [blueprint routes](https://core.hitchy.org/internals/routing-basics.html#focusing-on-routes) implementing a REST API for every model defined in file system as described before.
|
|
73
92
|
|
|
74
93
|
Those routes comply with this pattern:
|
|
75
94
|
|
|
@@ -86,7 +105,7 @@ exports.model = {
|
|
|
86
105
|
|
|
87
106
|
The model's segment in URL `<model>` is derived as the kebab-case version of model's name which is given in PascalCase. Thus, a model definition in a file named **api/models/my-fancy-model.js** is assumed to describe a model named **MyFancyModel** by default, resulting in model's URL segment to be **my-fancy-model** again. So the URL path for the collection of items is `/api/my-fancy-model`.
|
|
88
107
|
|
|
89
|
-
In Hitchy's
|
|
108
|
+
In Hitchy's document-oriented database all model instances or items are uniquely addressable via UUIDs. By appending an item's UUID to the given URL path of a collection you get the URL path of that item, e.g. `/api/my-fancy-model/01234567-1234-1234-1234-56789abcdef0`.
|
|
90
109
|
|
|
91
110
|
|
|
92
111
|
### The REST API
|
|
@@ -201,9 +220,9 @@ For example, a GET-request for `/api/localEmployee?q=salary:between:2000:4000` w
|
|
|
201
220
|
|
|
202
221
|
##### Complex tests <Bade type=info text=v0.5.2+></Badge>
|
|
203
222
|
|
|
204
|
-
Hitchy
|
|
223
|
+
Hitchy's document-oriented database supports more complex queries that can't be encoded as such simple queries as described above. Thus, a different way of querying has been added.
|
|
205
224
|
|
|
206
|
-
When using parameter `query` instead of `q`, its value is assumed to be a JSON-encoded query complying with query syntax supported by [Model.find() method of Hitchy
|
|
225
|
+
When using parameter `query` instead of `q`, its value is assumed to be a JSON-encoded query complying with query syntax supported by [Model.find() method of Hitchy's document-oriented database](https://odem.hitchy.org/api/model.html#model-find).
|
|
207
226
|
|
|
208
227
|
```http request
|
|
209
228
|
GET /api/user?query={"in":{"name":["john","jane","jason"]}}
|
|
@@ -231,3 +250,73 @@ Using query parameter `sortBy=lastName` a fetched list of items is sorted by val
|
|
|
231
250
|
Query parameter `limit=n` is requesting to fetch at most **n** items. Parameter `offset=n` is requesting to skip **n** items before starting retrieval. Slicing is applied after sorting items.
|
|
232
251
|
|
|
233
252
|
When slicing this way only a subset of basically available items is fetched by intention. If you need to know the total number of available items when requesting a slice you can either set custom field `x-count` in request header or query parameter `count` to `1` or any other truthy value. This will have a slight negative impact on request performance, but causes delivery of the total number of matching items in a separate property `count` of response body as well as in response header named `x-count`.
|
|
253
|
+
|
|
254
|
+
#### Authorization
|
|
255
|
+
|
|
256
|
+
As soon as [@hitchy/plugin-auth](https://auth.hitchy.org) is included with your project, **@hitchy/plugin-odem-rest** is using it to check a requesting user's authorization by declaring named [resources](https://auth.hitchy.org/introduction.html#resources) available for setting up authorization rules in [database](https://auth.hitchy.org/api/model/authorization-rule.html) or in [configuration](https://auth.hitchy.org/api/config.html#config-auth-authorizations).
|
|
257
|
+
|
|
258
|
+
##### Resource namespace
|
|
259
|
+
|
|
260
|
+
All declared resources share common prefix `@hitchy.odem`.
|
|
261
|
+
|
|
262
|
+
Despite the generic name, the authorization control described here does not apply to **@hitchy/plugin-odem** in general. Custom server-side code needs to stay capable of interacting with models and their private or protected properties. Instead, authorization control affects the REST API implemented by this plugin, only. Other plugins may implement APIs for accessing the same models by different means. Those plugins may share the resource naming pattern described here so that all APIs featuring remote interaction with the data can be controlled in the same way. Thus, resource names omit to refer to the REST API explicitly.
|
|
263
|
+
|
|
264
|
+
##### Per-model resources
|
|
265
|
+
|
|
266
|
+
Resources are declared to control authorizations for basically interacting with either model:
|
|
267
|
+
|
|
268
|
+
* When finding or listing items, accessing the resource `@hitchy.odem.model.<ModelName>.list` must be granted.
|
|
269
|
+
* When checking an item, accessing the resource `@hitchy.odem.model.<ModelName>.check` must be granted.
|
|
270
|
+
* When reading an item, accessing the resource `@hitchy.odem.model.<ModelName>.read` must be granted.
|
|
271
|
+
* When patching or replacing an item, accessing the resource `@hitchy.odem.model.<ModelName>.write` must be granted.
|
|
272
|
+
* When creating a new item, accessing the resource `@hitchy.odem.model.<ModelName>.create` must be granted.
|
|
273
|
+
* When removing an item, accessing the resource `@hitchy.odem.model.<ModelName>.remove` must be granted.
|
|
274
|
+
* Accessing a model's schema requires the resource `@hitchy.odem.model.<ModelName>.schema` to be granted.
|
|
275
|
+
* Accessing the collection of all models' schemata requires the resource `@hitchy.odem.schema` to be granted. This implicitly causes models with their [promote option](https://odem.hitchy.org/guides/defining-models.html#options) being `protected` to be exposed in responses to that request unless resource `@hitchy.odem.model.<ModelName>.promote` isn't revoked from the user.
|
|
276
|
+
|
|
277
|
+
> In these examples, replace `<ModelName>` with a model's name in PascalCase.
|
|
278
|
+
|
|
279
|
+
##### Per-property resources
|
|
280
|
+
|
|
281
|
+
In addition, resources are declared to control access on a model's protected properties. By default, protected properties are hidden from a user's REST request.
|
|
282
|
+
|
|
283
|
+
> Prior to supporting **@hitchy/plugin-auth** for authoriation control in v0.6.0, protected properties have been available to any authenticated user. The same behavior still applies if **@hitchy/plugin-auth** isn't available in a project.
|
|
284
|
+
|
|
285
|
+
* When finding or listing items, accessing the resource `@hitchy.odem.model.<ModelName>.property.<propertyName>.list` must be granted for the protected property to be supported in filter queries and to include it in resulting set of items.
|
|
286
|
+
* When reading an item, accessing the resource `@hitchy.odem.model.<ModelName>.property.<propertyName>.read` must be granted to include the property in the resulting record.
|
|
287
|
+
* When creating an item, accessing the resource `@hitchy.odem.model.<ModelName>.property.<propertyName>.create` must be granted to include the property in the resulting record.
|
|
288
|
+
* When patching or replacing an item, accessing the resource `@hitchy.odem.model.<ModelName>.property.<propertyName>.write` must be granted to consider an update for the property and to include it in the resulting record.
|
|
289
|
+
|
|
290
|
+
> In these examples, replace `<ModelName>` with a model's name in PascalCase and `<propertyName>` with its name as defined in the model.
|
|
291
|
+
|
|
292
|
+
Per-property authorization control is not considered for public and private properties. This is meant to limit performance penalties on processing requests.
|
|
293
|
+
|
|
294
|
+
* Public properties are always available via REST API as soon as the according [per-model authorization](#per-model-resources) is given.
|
|
295
|
+
* Private properties are never available.
|
|
296
|
+
|
|
297
|
+
##### Examples
|
|
298
|
+
|
|
299
|
+
The following example for a file **config/auth.js** is effectively disabling all authorization control in context of **@hitchy/plugin-odem-rest** and thus should be used for evaluation purposes, only:
|
|
300
|
+
|
|
301
|
+
```javascript
|
|
302
|
+
exports.auth = {
|
|
303
|
+
authorizations: {
|
|
304
|
+
"@hitchy.odem": "*"
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
A slightly more sophisticated example could look like this:
|
|
310
|
+
|
|
311
|
+
```javascript
|
|
312
|
+
exports.auth = {
|
|
313
|
+
authorizations: {
|
|
314
|
+
"@hitchy.odem": "*",
|
|
315
|
+
"@hitchy.odem.model.User": [ "-*", "@admin" ],
|
|
316
|
+
"@hitchy.odem.model.Role": [ "-*", "@admin" ],
|
|
317
|
+
"@hitchy.odem.model.User.property.password": [ "-*" ],
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
This example is granting access to all features of **@hitchy/plugin-odem-rest** unless they address either model `User` or `Role`. Access on those models is limited to users with role `admin`. However, even those users must not access protected property `password`, though you might want to declare it as private anyway in which case last rule in given example does not have any effect.
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const PtnAcceptsValue = /^\s*(?:function\s*(?:\s\S+\s*)?)?\(\s*[^\s)]/;
|
|
4
|
-
|
|
5
|
-
module.exports = function() {
|
|
6
|
-
/**
|
|
7
|
-
* Handles model schemata in context of REST-API plugin.
|
|
8
|
-
*
|
|
9
|
-
* @name api.runtime.services.OdemRestSchema
|
|
10
|
-
*/
|
|
11
|
-
class OdemRestSchema {
|
|
12
|
-
/**
|
|
13
|
-
* Detects if selected model's schema may be promoted to clients or not.
|
|
14
|
-
*
|
|
15
|
-
* @param {HitchyIncomingMessage} request request descriptor
|
|
16
|
-
* @param {class<Model>} model class of model to check
|
|
17
|
-
* @returns {boolean} true if model's schema may be promoted to clients, false otherwise
|
|
18
|
-
*/
|
|
19
|
-
static mayBePromoted( request, model ) {
|
|
20
|
-
const { schema: { options } } = model;
|
|
21
|
-
|
|
22
|
-
const scope = String( options.promote || options.expose || "public" ).trim().toLowerCase();
|
|
23
|
-
|
|
24
|
-
switch ( scope ) {
|
|
25
|
-
case "public" :
|
|
26
|
-
return true;
|
|
27
|
-
|
|
28
|
-
case "protected" :
|
|
29
|
-
return Boolean( request.user );
|
|
30
|
-
|
|
31
|
-
default :
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Detects if selected model may be exposed to clients or not.
|
|
38
|
-
*
|
|
39
|
-
* @param {HitchyIncomingMessage} request request descriptor
|
|
40
|
-
* @param {class<Model>} model class of model to check
|
|
41
|
-
* @returns {boolean} true if model may be exposed to clients, false otherwise
|
|
42
|
-
*/
|
|
43
|
-
static mayBeExposed( request, model ) {
|
|
44
|
-
const { schema: { options } } = model;
|
|
45
|
-
|
|
46
|
-
const scope = String( options.expose || "public" ).trim().toLowerCase();
|
|
47
|
-
|
|
48
|
-
switch ( scope ) {
|
|
49
|
-
case "public" :
|
|
50
|
-
return true;
|
|
51
|
-
|
|
52
|
-
case "protected" :
|
|
53
|
-
return Boolean( request.user );
|
|
54
|
-
|
|
55
|
-
default :
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Extracts information on selected model's schema for publishing.
|
|
62
|
-
*
|
|
63
|
-
* @param {class<Model>} model model to process
|
|
64
|
-
* @param {boolean} omitComputed set true to omit computed properties
|
|
65
|
-
* @returns {object} extracted public information on selected model's schema
|
|
66
|
-
*/
|
|
67
|
-
static extractPublicData( model, { omitComputed = false } = {} ) {
|
|
68
|
-
const { schema: { props, computed, options } } = model;
|
|
69
|
-
|
|
70
|
-
const extracted = {
|
|
71
|
-
name: model.name,
|
|
72
|
-
props: {},
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const propNames = Object.keys( props );
|
|
76
|
-
const numProps = propNames.length;
|
|
77
|
-
|
|
78
|
-
for ( let i = 0; i < numProps; i++ ) {
|
|
79
|
-
const name = propNames[i];
|
|
80
|
-
const prop = props[name];
|
|
81
|
-
|
|
82
|
-
const copy = extracted.props[name] = {
|
|
83
|
-
type: prop.type || "string",
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const optionNames = Object.keys( prop );
|
|
87
|
-
const numOptions = optionNames.length;
|
|
88
|
-
|
|
89
|
-
for ( let j = 0; j < numOptions; j++ ) {
|
|
90
|
-
const optionName = optionNames[j];
|
|
91
|
-
|
|
92
|
-
switch ( optionName.toLowerCase() ) {
|
|
93
|
-
case "type" :
|
|
94
|
-
case "indexes" :
|
|
95
|
-
case "indices" :
|
|
96
|
-
case "index" :
|
|
97
|
-
break;
|
|
98
|
-
|
|
99
|
-
default : {
|
|
100
|
-
const option = prop[optionName];
|
|
101
|
-
|
|
102
|
-
if ( option != null && typeof option !== "function" ) {
|
|
103
|
-
copy[optionName] = this.cloneSerializable( option );
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if ( !omitComputed ) {
|
|
111
|
-
const computedNames = Object.keys( props );
|
|
112
|
-
const numComputed = computedNames.length;
|
|
113
|
-
extracted.computed = {};
|
|
114
|
-
|
|
115
|
-
for ( let i = 0; i < numComputed; i++ ) {
|
|
116
|
-
const name = computedNames[i];
|
|
117
|
-
const fn = computed[name];
|
|
118
|
-
|
|
119
|
-
if ( typeof fn === "function" ) {
|
|
120
|
-
extracted.computed[name] = PtnAcceptsValue.test( fn );
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const optionsNames = Object.keys( options );
|
|
126
|
-
const numOptions = optionsNames.length;
|
|
127
|
-
|
|
128
|
-
for ( let i = 0; i < numOptions; i++ ) {
|
|
129
|
-
const name = optionsNames[i];
|
|
130
|
-
|
|
131
|
-
switch ( name ) {
|
|
132
|
-
case "onUnsaved" :
|
|
133
|
-
case "expose" :
|
|
134
|
-
case "promote" :
|
|
135
|
-
break;
|
|
136
|
-
|
|
137
|
-
default : {
|
|
138
|
-
if ( !extracted.options ) {
|
|
139
|
-
extracted.options = {};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const option = options[name];
|
|
143
|
-
|
|
144
|
-
if ( option != null && typeof option !== "function" ) {
|
|
145
|
-
extracted.options[name] = this.cloneSerializable( option );
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return extracted;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Creates deep clone of provided value omitting any data can't be or
|
|
156
|
-
* mustn't be serialized.
|
|
157
|
-
*
|
|
158
|
-
* @param {*} input data to be cloned
|
|
159
|
-
* @returns {*} (partial) clone of provided data
|
|
160
|
-
*/
|
|
161
|
-
static cloneSerializable( input ) {
|
|
162
|
-
switch ( typeof input ) {
|
|
163
|
-
case "object" : {
|
|
164
|
-
if ( Array.isArray( input ) ) {
|
|
165
|
-
const numItems = input.length;
|
|
166
|
-
const clone = new Array( numItems );
|
|
167
|
-
let write = 0;
|
|
168
|
-
|
|
169
|
-
for ( let read = 0; read < numItems; read++ ) {
|
|
170
|
-
const value = this.cloneSerializable( input[read] );
|
|
171
|
-
|
|
172
|
-
if ( value != null ) {
|
|
173
|
-
clone[write++] = value;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
clone.splice( write );
|
|
178
|
-
|
|
179
|
-
return clone;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const names = Object.keys( input );
|
|
183
|
-
const numNames = names.length;
|
|
184
|
-
const clone = {};
|
|
185
|
-
|
|
186
|
-
for ( let i = 0; i < numNames; i++ ) {
|
|
187
|
-
const name = names[i];
|
|
188
|
-
const value = this.cloneSerializable( input[name] );
|
|
189
|
-
|
|
190
|
-
if ( value != null ) {
|
|
191
|
-
clone[name] = value;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return clone;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
case "function" :
|
|
199
|
-
return null;
|
|
200
|
-
|
|
201
|
-
default :
|
|
202
|
-
return input;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return OdemRestSchema;
|
|
208
|
-
};
|