11'use strict' ;
22const JSONAPISerializer = require ( 'jsonapi-serializer' ) . Serializer ;
3+ const crypto = require ( 'crypto' ) ;
4+
5+ /**
6+ * Converts a string to the `dasherized-key` format.
7+ *
8+ * @private
9+ * @function dasherize
10+ * @param {String } str - String to convert.
11+ * @return {String }
12+ */
13+ function dasherize ( str ) {
14+ let newStr = str . substr ( 0 , 1 ) . toLowerCase ( ) + str . substr ( 1 ) ;
15+ newStr = newStr . replace ( / ( [ A - Z ] ) / g, '-$1' ) . toLowerCase ( ) ;
16+ return newStr ;
17+ }
18+
19+ /**
20+ * Creates a unique ID for a given object, using a SHA-256 hash.
21+ *
22+ * @private
23+ * @function generateFauxId
24+ * @param {Object } data
25+ * @return {String }
26+ */
27+ function generateFauxId ( data ) {
28+ const hash = crypto . createHash ( 'sha256' ) ;
29+ hash . update ( JSON . stringify ( data ) ) ;
30+ return hash . digest ( 'hex' ) ;
31+ }
32+
33+ /**
34+ * Checks if Sequelize objects are available. If they're not, `serializePlainObject`
35+ * will be used to serialize the result. No relationships will be available.
36+ *
37+ * @private
38+ * @function mustParseAsSequelize
39+ * @param {Hook } hook
40+ * @return {Boolean }
41+ */
42+ function mustParseAsSequelize ( hook ) {
43+ return hook . service . Model && hook . result . $options ;
44+ }
345
446/**
547 * Creates a filter helper to check if a given attribute name must be excluded or not.
@@ -203,34 +245,86 @@ function createPagination(hook) {
203245 }
204246}
205247
248+ /**
249+ * Converts a plain Object instance into a JSON-API-compliant object.
250+ *
251+ * @private
252+ * @function serializePlainObject
253+ * @param {Object } item
254+ * @param {Object } options
255+ * @param {Hook } hook
256+ * @return {Object }
257+ */
258+ function serializePlainObject ( item , options , hook ) {
259+ const newItem = { } ;
260+
261+ if ( options . identifierKey ) {
262+ newItem . id = item [ options . identifierKey ] ;
263+ } else {
264+ newItem . id = generateFauxId ( item ) ;
265+ }
266+ if ( options . typeKey ) {
267+ newItem . type = item [ options . typeKey ] ;
268+ } else {
269+ newItem . type = hook . service . options . name ;
270+ }
271+
272+ newItem . attributes = { } ;
273+ Object . keys ( item ) . filter ( function ( key ) {
274+ return key !== options . identifierKey && key !== options . typeKey ;
275+ } ) . forEach ( function ( key ) {
276+ newItem . attributes [ dasherize ( key ) ] = item [ key ] ;
277+ } ) ;
278+
279+ newItem . links = {
280+ self : '/' + hook . path + '/' + newItem . id
281+ } ;
282+
283+ return newItem ;
284+ }
285+
206286/**
207287 * Creates a JSON API document with multiple records.
208288 *
209289 * @private
210290 * @method jsonapifyViaFind
211291 * @param {Hook } hook
212292 */
213- function jsonapifyViaFind ( hook ) {
293+ function jsonapifyViaFind ( hook , options ) {
214294 let serialized = { } ;
215- hook . result . included = [ ] ;
216- hook . result . data . forEach ( function ( data , index ) {
217- const jsonItem = data . toJSON ( ) ;
218- serialized = jsonapify ( jsonItem , hook . service . Model , hook . path + '/' + jsonItem . id , data . $options ) ;
219- hook . result . data [ index ] = serialized . document ;
220- if ( serialized . related ) {
221- hook . result . included = hook . result . included . concat ( serialized . related ) ;
222- }
223- if ( serialized . links ) {
224- hook . result . data [ index ] . links = serialized . links ;
225- createPagination ( hook ) ;
295+ if ( mustParseAsSequelize ( hook ) ) {
296+ hook . result . included = [ ] ;
297+ hook . result . data . forEach ( function ( data , index ) {
298+ const jsonItem = data . toJSON ( ) ;
299+ serialized = jsonapify ( jsonItem , hook . service . Model , hook . path + '/' + jsonItem . id , data . $options ) ;
300+ hook . result . data [ index ] = serialized . document ;
301+ if ( serialized . related ) {
302+ hook . result . included = hook . result . included . concat ( serialized . related ) ;
303+ }
304+ if ( serialized . links ) {
305+ hook . result . data [ index ] . links = serialized . links ;
306+ createPagination ( hook ) ;
307+ }
308+ } ) ;
309+ if ( ! hook . result . included . length ) {
310+ delete hook . result . included ;
311+ } else {
312+ hook . result . included = removeDuplicateRecords ( hook . result . included ) ;
226313 }
227- } ) ;
228- if ( ! hook . result . included . length ) {
229- delete hook . result . included ;
314+ createMetadata ( hook ) ;
230315 } else {
231- hook . result . included = removeDuplicateRecords ( hook . result . included ) ;
316+ const newResult = { } ;
317+ if ( Array . isArray ( hook . result ) && hook . result . length > 1 ) {
318+ newResult . data = hook . result . map ( function ( item ) {
319+ return serializePlainObject ( item , options , hook ) ;
320+ } ) ;
321+ } else if ( ! Array . isArray ( hook . result ) && Object . keys ( hook . result ) . length ) {
322+ newResult . data = [ serializePlainObject ( hook . result , options , hook ) ] ;
323+ } else {
324+ newResult . data = [ ] ;
325+ }
326+ hook . result = newResult ;
232327 }
233- createMetadata ( hook ) ;
234328}
235329
236330/**
@@ -240,19 +334,23 @@ function jsonapifyViaFind(hook) {
240334 * @method jsonapifyViaGet
241335 * @param {Hook } hook
242336 */
243- function jsonapifyViaGet ( hook ) {
337+ function jsonapifyViaGet ( hook , options ) {
244338 let serialized = { } ;
245- const jsonItem = hook . result . toJSON ( ) ;
246- serialized = jsonapify ( jsonItem , hook . service . Model , hook . path + '/' + jsonItem . id , hook . result . $options ) ;
247- hook . result = { data : serialized . document , included : serialized . related } ;
248- if ( hook . result . included && ! hook . result . included . length ) {
249- delete hook . result . included ;
250- }
251- if ( serialized . links ) {
252- hook . result . data . links = serialized . links ;
253- hook . result . data . links . parent = '/' + hook . service . Model . name ;
339+ if ( mustParseAsSequelize ( hook ) ) {
340+ const jsonItem = hook . result . toJSON ( ) ;
341+ serialized = jsonapify ( jsonItem , hook . service . Model , hook . path + '/' + jsonItem . id , hook . result . $options ) ;
342+ hook . result = { data : serialized . document , included : serialized . related } ;
343+ if ( hook . result . included && ! hook . result . included . length ) {
344+ delete hook . result . included ;
345+ }
346+ if ( serialized . links ) {
347+ hook . result . data . links = serialized . links ;
348+ hook . result . data . links . parent = '/' + hook . service . Model . name ;
349+ }
350+ createMetadata ( hook ) ;
351+ } else {
352+ hook . result . data = serializePlainObject ( hook . result , options , hook ) ;
254353 }
255- createMetadata ( hook ) ;
256354}
257355
258356/**
@@ -268,11 +366,17 @@ const entrypoints = { find: jsonapifyViaFind, get: jsonapifyViaGet };
268366 * It is used as an `after` hook. Bindable with `find` and `get` hooks.
269367 *
270368 * @function jsonapify
369+ * @param {Object } options - Define settings for the JSONAPIficiation process.
370+ * Available options:
371+ * - identifierKey: (String) Used by `serializePlainObject` to determine
372+ * which key will be used as `id`.
373+ * - typeKey: (String) Used by `serializePlainObject` to determine
374+ * which key will be used as `type`.
271375 */
272376module . exports = function ( options = { } ) { // eslint-disable-line no-unused-vars
273377 return function ( hook ) {
274378 if ( hook . method === 'find' || hook . method === 'get' ) {
275- entrypoints [ hook . method ] ( hook ) ;
379+ entrypoints [ hook . method ] ( hook , options ) ;
276380 }
277381 return Promise . resolve ( hook ) ;
278382 } ;
0 commit comments