Skip to content

Commit 3b29950

Browse files
authored
Add data and extra data to all functions in a schema (#27) (#28)
Add data and extra data to all functions in a schema (#27)
1 parent ef11bb4 commit 3b29950

File tree

3 files changed

+111
-51
lines changed

3 files changed

+111
-51
lines changed

Diff for: README.md

+28-15
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,23 @@ Serializer.register(type, options);
2828
- **id** (optional): The key to use as the reference. Default = 'id'.
2929
- **blacklist** (optional): An array of blacklisted attributes. Default = [].
3030
- **whitelist** (optional): An array of whitelisted attributes. Default = [].
31-
- **links** (optional): An *object* or a *function* that describes the links inside data. (If it is an object values can be string or function).
32-
- **topLevelMeta** (optional): An *object* or a *function* that describes the top-level meta. (If it is an object values can be string or function).
33-
- **topLevelLinks** (optional): An *object* or a *function* that describes the top-level links. (If it is an object values can be string or function).
31+
- **links** (optional): Describes the links inside data. It can be:
32+
- An *object* (values can be string or function).
33+
- A *function* with one argument `function(data) { ... }` or with two arguments `function(data, extraData) { ... }`
34+
- **topLevelMeta** (optional): Describes the top-level meta. It can be:
35+
- An *object* (values can be string or function).
36+
- A *function* with one argument `function(extraData) { ... }` or with two arguments `function(data, extraData) { ... }`
37+
- **topLevelLinks** (optional): Describes the top-level links. It can be:
38+
- An *object* (values can be string or function).
39+
- A *function* with one argument `function(extraData) { ... }` or with two arguments `function(data, extraData) { ... }`
3440
- **relationships** (optional): An object defining some relationships
3541
- relationship: The property in data to use as a relationship
3642
- **type**: The type to use for serializing the relationship (type need to be register)
3743
- **alternativeKey** (optional): An alternative key to use if relationship key not exist (example: 'author_id' as an alternative key for 'author' relationship). See [issue #12](https://github.com/danivek/json-api-serializer/issues/12).
3844
- **schema** (optional): A custom schema for serializing the relationship. If no schema define, it use the default one.
39-
- **links** (optional): An *object* or a *function* that describes the links for the relationship. (If it is an object values can be string or function).
45+
- **links** (optional): Describes the links for the relationship. It can be:
46+
- An *object* (values can be string or function).
47+
- A *function* with one argument `function(data) { ... }` or with two arguments `function(data, extraData) { ... }`
4048
- **convertCase** (optional): Case conversion for serializing data. Value can be : `kebab-case`, `snake_case`, `camelCase`
4149

4250
**Deserialization options:**
@@ -53,7 +61,7 @@ To avoid repeating the same options for each type, it's possible to add global o
5361
var JSONAPISerializer = require('json-api-serializer');
5462
var Serializer = new JSONAPISerializer({
5563
convertCase: 'kebab-case',
56-
unconvertCase: 'caseCase'
64+
unconvertCase: 'camelCase'
5765
});
5866
```
5967

@@ -130,14 +138,14 @@ Serializer.register('article', {
130138
schema: 'only-body' // A custom schema
131139
}
132140
},
133-
topLevelMeta: function(extraOptions) { // An object or a function that describes top level meta.
141+
topLevelMeta: function(data, extraData) { // An object or a function that describes top level meta.
134142
return {
135-
count: extraOptions.count,
136-
total: extraOptions.total
143+
count: extraData.count,
144+
total: data.length
137145
}
138146
},
139147
topLevelLinks: { // An object or a function that describes top level links.
140-
self: '/articles' // Can be a function (with extra options argument) or a string value
148+
self: '/articles' // Can be a function (with extra data argument) or a string value
141149
}
142150
});
143151

@@ -169,7 +177,7 @@ Serializer.register('comment', 'only-body', {
169177

170178
### Serialize
171179

172-
Serialize it with the corresponding resource type, data and optional extra options :
180+
Serialize it with the corresponding resource type, data and optional extra data :
173181

174182
```javascript
175183
// Synchronously (blocking)
@@ -189,7 +197,8 @@ The output data will be :
189197
"version": "1.0"
190198
},
191199
"meta": {
192-
"count": 2
200+
"count": 2,
201+
"total": 1
193202
},
194203
"links": {
195204
"self": "/articles"
@@ -360,8 +369,8 @@ Example :
360369
```javascript
361370
relationships: {
362371
comments: {
363-
type: 'comment'
364-
schema: 'customSchema'
372+
type: 'comment'
373+
schema: 'customSchema'
365374
}
366375
}
367376
```
@@ -371,8 +380,12 @@ relationships: {
371380
If your data contains one or multiple objects of varying types, it's possible to define a configuration object instead of the type-string as the first argument of ```serialize``` and ```serializeAsync``` with these options:
372381

373382
- **type** (required): A *string* for the path to the key to use to determine type or a *function* deriving a type-string from each data-item.
374-
- **topLevelMeta** (optional): An *object* or a *function* that describes the top-level meta. (If it is an object values can be string or function).
375-
- **topLevelLinks** (optional): An *object* or a *function* that describes the top-level links. (If it is an object values can be string or function).
383+
- **topLevelMeta** (optional): Describes the top-level meta. It can be:
384+
- An *object* (values can be string or function).
385+
- A *function* with one argument `function(extraData) { ... }` or with two arguments `function(data, extraData) { ... }`
386+
- **topLevelLinks** (optional): Describes the top-level links. It can be:
387+
- An *object* (values can be string or function).
388+
- A *function* with one argument `function(extraData) { ... }` or with two arguments `function(data, extraData) { ... }`
376389

377390
Example :
378391
```javascript

Diff for: lib/JSONAPISerializer.js

+47-33
Original file line numberDiff line numberDiff line change
@@ -116,20 +116,20 @@ module.exports = class JSONAPISerializer {
116116
* @param {string|Object} type resource's type as string or a dynamic type options as object.
117117
* @param {Object|Object[]} data input data.
118118
* @param {string} [schema=default] resource's schema name.
119-
* @param {Object} [extraOptions] additional data that can be used in topLevelMeta options.
119+
* @param {Object} [extraData] additional data that can be used in topLevelMeta options.
120120
* @return {Object} serialized data.
121121
*/
122-
serialize(type, data, schema, extraOptions) {
122+
serialize(type, data, schema, extraData) {
123123
// Support optional arguments (schema)
124124
if (arguments.length === 3) {
125125
if (_.isPlainObject(schema)) {
126-
extraOptions = schema;
126+
extraData = schema;
127127
schema = 'default';
128128
}
129129
}
130130

131131
schema = schema || 'default';
132-
extraOptions = extraOptions || {};
132+
extraData = extraData || {};
133133

134134
const included = [];
135135
let serializedData;
@@ -139,7 +139,7 @@ module.exports = class JSONAPISerializer {
139139
const typeOption = this.validateDynamicTypeOptions(type);
140140
// Override top level data
141141
topLevelOption = { topLevelMeta: typeOption.topLevelMeta, topLevelLinks: typeOption.topLevelLinks };
142-
serializedData = this.serializeMixedData(typeOption, data, included);
142+
serializedData = this.serializeMixedData(typeOption, data, included, extraData);
143143
} else { // Serialize data with the defined type
144144
if (!this.schemas[type]) {
145145
throw new Error(`No type registered for ${type}`);
@@ -151,16 +151,16 @@ module.exports = class JSONAPISerializer {
151151

152152
const resourceOpts = this.schemas[type][schema];
153153
topLevelOption = { topLevelMeta: resourceOpts.topLevelMeta, topLevelLinks: resourceOpts.topLevelLinks };
154-
serializedData = this.serializeData(type, data, resourceOpts, included);
154+
serializedData = this.serializeData(type, data, resourceOpts, included, extraData);
155155
}
156156

157157

158158
return {
159159
jsonapi: {
160160
version: '1.0',
161161
},
162-
meta: this.processOptionsValues(extraOptions, topLevelOption.topLevelMeta),
163-
links: this.processOptionsValues(extraOptions, topLevelOption.topLevelLinks),
162+
meta: this.processOptionsValues(data, extraData, topLevelOption.topLevelMeta, 'extraData'),
163+
links: this.processOptionsValues(data, extraData, topLevelOption.topLevelLinks, 'extraData'),
164164
data: serializedData,
165165
included: this.serializeIncluded(included),
166166
};
@@ -175,20 +175,20 @@ module.exports = class JSONAPISerializer {
175175
* @param {string} type resource's type.
176176
* @param {Object|Object[]} data input data.
177177
* @param {string} [schema=default] resource's schema name.
178-
* @param {Object} [extraOptions] additional data that can be used in topLevelMeta options.
178+
* @param {Object} [extraData] additional data that can be used in topLevelMeta options.
179179
* @return {Promise} resolves with serialized data.
180180
*/
181-
serializeAsync(type, data, schema, extraOptions) {
181+
serializeAsync(type, data, schema, extraData) {
182182
// Support optional arguments (schema)
183183
if (arguments.length === 3) {
184184
if (_.isPlainObject(schema)) {
185-
extraOptions = schema;
185+
extraData = schema;
186186
schema = 'default';
187187
}
188188
}
189189

190190
schema = schema || 'default';
191-
extraOptions = extraOptions || {};
191+
extraData = extraData || {};
192192

193193
const included = [];
194194
const isDataArray = Array.isArray(data);
@@ -223,8 +223,8 @@ module.exports = class JSONAPISerializer {
223223
try {
224224
// Serialize a single item of the data-array.
225225
const serializedItem = isDynamicType
226-
? this.serializeMixedData(type, item, included)
227-
: this.serializeData(type, item, resourceOpts, included);
226+
? this.serializeMixedData(type, item, included, extraData)
227+
: this.serializeData(type, item, resourceOpts, included, extraData);
228228

229229
// If the serialized item is null, we won't push it to the stream,
230230
// as pushing a null-value causes streams to end.
@@ -252,8 +252,8 @@ module.exports = class JSONAPISerializer {
252252
jsonapi: {
253253
version: '1.0',
254254
},
255-
meta: this.processOptionsValues(extraOptions, topLevelOption.topLevelMeta),
256-
links: this.processOptionsValues(extraOptions, topLevelOption.topLevelLinks),
255+
meta: this.processOptionsValues(extraData, data, topLevelOption.topLevelMeta),
256+
links: this.processOptionsValues(extraData, data, topLevelOption.topLevelLinks),
257257
// If the source data was an array, we just pass the serialized data array.
258258
// Otherwise we try to take the first (and only) item of it or pass null.
259259
data: isDataArray ? serializedData : (serializedData[0] || null),
@@ -373,9 +373,10 @@ module.exports = class JSONAPISerializer {
373373
* @param {Object|Object[]} data input data.
374374
* @param {options} options resource's configuration options.
375375
* @param {Object[]} included.
376+
* @param {Object} extraData additional data.
376377
* @return {Object|Object[]} serialized data.
377378
*/
378-
serializeData(type, data, options, included) {
379+
serializeData(type, data, options, included, extraData) {
379380
// Empty data
380381
if (_.isEmpty(data)) {
381382
// Return [] or null
@@ -384,16 +385,16 @@ module.exports = class JSONAPISerializer {
384385

385386
// Array data
386387
if (Array.isArray(data)) {
387-
return data.map(d => this.serializeData(type, d, options, included));
388+
return data.map(d => this.serializeData(type, d, options, included, extraData));
388389
}
389390

390391
// Single data
391392
return {
392393
type,
393394
id: data[options.id].toString(),
394395
attributes: this.serializeAttributes(data, options),
395-
relationships: this.serializeRelationships(data, options, included),
396-
links: this.processOptionsValues(data, options.links),
396+
relationships: this.serializeRelationships(data, options, included, extraData),
397+
links: this.processOptionsValues(data, extraData, options.links),
397398
};
398399
}
399400

@@ -406,9 +407,10 @@ module.exports = class JSONAPISerializer {
406407
* @param {Object} typeOption a dynamic type options.
407408
* @param {Object|Object[]} data input data.
408409
* @param {Object[]} included.
410+
* @param {Object} extraData additional data.
409411
* @return {Object|Object[]} serialized data.
410412
*/
411-
serializeMixedData(typeOption, data, included) {
413+
serializeMixedData(typeOption, data, included, extraData) {
412414
// Empty data
413415
if (_.isEmpty(data)) {
414416
// Return [] or null
@@ -417,7 +419,7 @@ module.exports = class JSONAPISerializer {
417419

418420
// Array data
419421
if (Array.isArray(data)) {
420-
return data.map(d => this.serializeMixedData(typeOption, d, included));
422+
return data.map(d => this.serializeMixedData(typeOption, d, included, extraData));
421423
}
422424

423425
// Single data
@@ -434,7 +436,7 @@ module.exports = class JSONAPISerializer {
434436
throw new Error(`No type registered for ${type}`);
435437
}
436438

437-
return this.serializeData(type, data, this.schemas[type].default, included);
439+
return this.serializeData(type, data, this.schemas[type].default, included, extraData);
438440
}
439441

440442
/**
@@ -519,9 +521,10 @@ module.exports = class JSONAPISerializer {
519521
* @param {Object|Object[]} data input data.
520522
* @param {Object} options resource's configuration options.
521523
* @param {Object[]} included.
524+
* @param {Object} extraData additional data.
522525
* @return {Object} serialized relationships.
523526
*/
524-
serializeRelationships(data, options, included) {
527+
serializeRelationships(data, options, included, extraData) {
525528
const serializedRelationships = {};
526529

527530
Object.keys(options.relationships).forEach((relationship) => {
@@ -543,8 +546,8 @@ module.exports = class JSONAPISerializer {
543546
}
544547

545548
const serializeRelationship = {
546-
links: this.processOptionsValues(data, rOptions.links),
547-
data: this.serializeRelationship(rOptions.type, data[relationshipKey], this.schemas[rOptions.type][schema], included),
549+
links: this.processOptionsValues(data, extraData, rOptions.links),
550+
data: this.serializeRelationship(rOptions.type, data[relationshipKey], this.schemas[rOptions.type][schema], included, extraData),
548551
};
549552

550553
relationship = (options.convertCase) ? this._convertCase(relationship, options.convertCase) : relationship;
@@ -565,9 +568,10 @@ module.exports = class JSONAPISerializer {
565568
* @param {Object|Object[]} rData relationship's data.
566569
* @param {Object} rOptions relationship's configuration options.
567570
* @param {Object[]} included.
571+
* @param {Object} extraData additional data.
568572
* @return {Object|Object[]} serialized relationship data.
569573
*/
570-
serializeRelationship(rType, rData, rOptions, included) {
574+
serializeRelationship(rType, rData, rOptions, included, extraData) {
571575
// No relationship data
572576
if (rData === undefined) {
573577
return undefined;
@@ -581,7 +585,7 @@ module.exports = class JSONAPISerializer {
581585

582586
// To-many relationships
583587
if (Array.isArray(rData)) {
584-
return rData.map(d => this.serializeRelationship(rType, d, rOptions, included));
588+
return rData.map(d => this.serializeRelationship(rType, d, rOptions, included, extraData));
585589
}
586590

587591
// To-one relationship
@@ -598,28 +602,38 @@ module.exports = class JSONAPISerializer {
598602
} else {
599603
// Relationship has been populated
600604
serializedRelationship.id = rData[rOptions.id].toString();
601-
included.push(this.serializeData(rType, rData, rOptions, included));
605+
included.push(this.serializeData(rType, rData, rOptions, included, extraData));
602606
}
603607
return serializedRelationship;
604608
}
605609

606610
/**
607611
* Process options values.
608-
* Allows options to be an object or a function.
612+
* Allows options to be an object or a function with 1 or 2 arguments
609613
*
610614
* @method JSONAPISerializer#processOptionsValues
611615
* @private
612616
* @param {Object} data data passed to functions options
617+
* @param {Object} extraData additional data passed to functions options
613618
* @param {Object} options configuration options.
619+
* @param {string} fallbackModeIfOneArg fallback mode if only one argument is passed to function.
620+
* Avoid breaking changes with issue https://github.com/danivek/json-api-serializer/issues/27.
614621
* @return {Object}
615622
*/
616-
processOptionsValues(data, options) {
623+
processOptionsValues(data, extraData, options, fallbackModeIfOneArg) {
617624
let processedOptions = {};
618625
if (options && typeof options === 'function') {
619-
processedOptions = options(data);
626+
// Backward compatible with functions with one 'extraData' argument
627+
processedOptions = (fallbackModeIfOneArg === 'extraData' && options.length === 1) ? options(extraData) : options(data, extraData);
620628
} else {
621629
Object.keys(options).forEach((key) => {
622-
const processedValue = options[key] && typeof options[key] === 'function' ? options[key](data) : options[key];
630+
let processedValue = {};
631+
if (options[key] && typeof options[key] === 'function') {
632+
// Backward compatible with functions with one 'extraData' argument
633+
processedValue = (fallbackModeIfOneArg === 'extraData' && options[key].length === 1) ? options[key](extraData) : options[key](data, extraData);
634+
} else {
635+
processedValue = options[key];
636+
}
623637
Object.assign(processedOptions, { [key]: processedValue });
624638
});
625639
}

0 commit comments

Comments
 (0)