├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── helpers.js ├── index.js └── settings.js ├── package-lock.json ├── package.json └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | contexts.json 3 | *.log 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - "6.10" 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Dali Zheng (http://daliwa.li) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fortune Micro API Serializer 2 | 3 | [![Build Status](https://img.shields.io/travis/fortunejs/fortune-micro-api/master.svg?style=flat-square)](https://travis-ci.org/fortunejs/fortune-micro-api) 4 | [![npm Version](https://img.shields.io/npm/v/fortune-micro-api.svg?style=flat-square)](https://www.npmjs.com/package/fortune-micro-api) 5 | [![License](https://img.shields.io/npm/l/fortune-micro-api.svg?style=flat-square)](https://raw.githubusercontent.com/fortunejs/fortune-micro-api/master/LICENSE) 6 | 7 | This is a [Micro API](http://micro-api.org) serializer for [Fortune.js](http://fortunejs.com), which is compatible with the specification as of **2017-04-25**. 8 | 9 | ```sh 10 | $ npm install fortune fortune-http fortune-micro-api 11 | ``` 12 | 13 | 14 | ## Usage 15 | 16 | ```js 17 | const http = require('http') 18 | const fortune = require('fortune') 19 | const fortuneHTTP = require('fortune-http') 20 | const microApiSerializer = require('fortune-micro-api') 21 | 22 | const options = { 23 | entryPoint: 'http://example.com', 24 | externalContext: '/context.jsonld' 25 | } 26 | 27 | // `instance` is an instance of Fortune.js. 28 | const listener = fortuneHTTP(instance, { 29 | serializers: [ 30 | // The `options` object here is required. 31 | [ microApiSerializer, options ] 32 | ] 33 | }) 34 | 35 | // The listener function may be used as a standalone server, or 36 | // may be composed as part of a framework. 37 | const server = http.createServer((request, response) => 38 | // When an external context is set, it should be handled externally. 39 | (request.url.indexOf(options.externalContext) === 0 ? 40 | microApiSerializer.showExternalContext(response, options) : 41 | listener(request, response)) 42 | .catch(error => { /* error logging */ })) 43 | 44 | server.listen(8080) 45 | ``` 46 | 47 | 48 | The `options` object is as follows: 49 | 50 | - `entryPoint`: URI to the entry point. **Required**. 51 | - `externalContext`: refer to the `@context` instead of embedding. **Recommended**. This requires some additional setup, so it's disabled by default. This should be valued by a URI to the external context. 52 | - `inflectType`: convert record type name to *PascalCase* in the payload. Default: `true`. 53 | - `reverseFields`: An object keyed by field names, which should use the `@reverse` property. 54 | - `contexts`: An array valued by URIs to external contexts. 55 | 56 | **Inherited options**: 57 | 58 | - `bufferEncoding`: which encoding type to use for input buffer fields. 59 | - `maxLimit`: maximum number of records to show per page. 60 | - `includeLimit`: maximum depth of fields per include. 61 | - `uriBase64`: encode URIs in base64 to discourage clients from tampering with the URI. 62 | - `castId`: try to cast string IDs to numbers if possible. 63 | 64 | 65 | ## MessagePack 66 | 67 | Instead of using JSON as a serialization format, it can optionally use [MessagePack](http://msgpack.org) instead, with an unregistered media type `application/x-micro-api`. It has the advantage of serializing dates and buffers properly. 68 | 69 | ```js 70 | const microApiSerializer = require('fortune-micro-api') 71 | 72 | // Alternative serializer with unregistered media type. 73 | const microApiMsgPack = microApiSerializer.msgpack 74 | ``` 75 | 76 | 77 | ## License 78 | 79 | This software is licensed under the [MIT license](https://raw.githubusercontent.com/fortunejs/fortune-micro-api/master/LICENSE). 80 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const url = require('url') 4 | const path = require('path') 5 | const fs = require('fs') 6 | const http = require('http') 7 | const settings = require('./settings') 8 | const reservedKeys = settings.reservedKeys 9 | const contextURI = settings.contextURI 10 | 11 | const contextField = reservedKeys.context 12 | const idField = reservedKeys.id 13 | const hrefField = reservedKeys.href 14 | const typeField = reservedKeys.type 15 | const metaField = reservedKeys.meta 16 | 17 | const contextsPath = path.join(__dirname, '../contexts.json') 18 | const buffer = Buffer.from || Buffer 19 | 20 | // Memoize these results. 21 | let context 22 | let externalContext 23 | 24 | module.exports = { 25 | showContext, showExternalContext, mapRecord, showQueries, 26 | attachIncluded, parseBuffer, capitalize 27 | } 28 | 29 | 30 | function showContext (options) { 31 | if (context) return context 32 | 33 | const base = options.entryPoint 34 | 35 | context = options.contexts.concat(contextURI, { 36 | [reservedKeys.base]: base, 37 | [reservedKeys.vocabulary]: `${base}/#` 38 | }) 39 | 40 | return context 41 | } 42 | 43 | 44 | function showExternalContext (response, options) { 45 | // The external context is retrieved from remote servers once upon 46 | // initialization, and is cached afterwards. 47 | if (externalContext) return endExternalContext(response) 48 | 49 | let cachedContexts = [] 50 | 51 | try { 52 | cachedContexts = JSON.parse(fs.readFileSync(contextsPath)) 53 | } 54 | catch (error) { 55 | // Just ignore the error. 56 | } 57 | 58 | return Promise.all(options.contexts.concat(contextURI).map(uri => 59 | new Promise((resolve, reject) => 60 | http.get(Object.assign(url.parse(uri), { 61 | headers: { 62 | 'Accept': settings.linkedDataType, 63 | 'User-Agent': 'curl' 64 | } 65 | }), response => { 66 | const chunks = [] 67 | 68 | // If the server doesn't respond, just time out. 69 | const timeout = setTimeout(reject, 10 * 1000) 70 | 71 | response.on('error', reject) 72 | response.on('data', chunk => chunks.push(chunk)) 73 | response.on('end', () => { 74 | clearTimeout(timeout) 75 | resolve(JSON.parse( 76 | Buffer.concat(chunks).toString())[contextField]) 77 | }) 78 | }) 79 | ) 80 | )) 81 | .catch(() => { 82 | cachedContexts.isCached = true 83 | return cachedContexts 84 | }) 85 | .then(contexts => { 86 | if (!contexts.isCached) 87 | try { 88 | fs.writeFileSync(contextsPath, JSON.stringify(contexts)) 89 | } 90 | catch (error) { 91 | // Just ignore the error. 92 | } 93 | 94 | contexts.unshift({}) 95 | contexts.push({ 96 | [reservedKeys.base]: options.entryPoint, 97 | [reservedKeys.vocabulary]: `${options.entryPoint}/#` 98 | }) 99 | 100 | externalContext = buffer(JSON.stringify({ 101 | [contextField]: Object.assign.apply(null, contexts) 102 | }, null, options.jsonSpaces)) 103 | 104 | return endExternalContext(response) 105 | }) 106 | } 107 | 108 | function endExternalContext (response) { 109 | return new Promise((resolve, reject) => { 110 | response.setHeader('Content-Type', settings.linkedDataType) 111 | response.end(externalContext, error => error ? reject(error) : resolve()) 112 | }) 113 | } 114 | 115 | 116 | function mapRecord (type, record) { 117 | const keys = this.keys 118 | const recordTypes = this.recordTypes 119 | const encodeRoute = this.encodeRoute 120 | const options = this.options 121 | 122 | const entryPoint = options.entryPoint 123 | const inflectType = options.inflectType 124 | const uriBase64 = options.uriBase64 125 | 126 | const typeKey = inflectType ? capitalize(type) : type 127 | const fields = recordTypes[type] 128 | const id = record[keys.primary] 129 | const clone = {} 130 | 131 | clone[metaField] = { [contextField]: null } 132 | clone[typeField] = encodeURIComponent(typeKey) 133 | clone[hrefField] = encodeRoute(type, id, null, uriBase64) 134 | clone[idField] = id 135 | 136 | const unionFields = union(Object.keys(fields), Object.keys(record)) 137 | 138 | for (let i = 0, j = unionFields.length; i < j; i++) { 139 | const field = unionFields[i] 140 | 141 | if (field === keys.primary || field === typeField || 142 | field === hrefField || field === metaField) continue 143 | 144 | const fieldDefinition = fields[field] 145 | const hasField = field in record 146 | 147 | if (!hasField && !fieldDefinition[keys.link]) continue 148 | 149 | // Detect and expand relative links. 150 | if (record[field] && 151 | typeof record[field][hrefField] === 'string' && 152 | record[field][hrefField].charAt(0) === '/') 153 | record[field][hrefField] = 154 | `${entryPoint}${record[field][hrefField]}` 155 | 156 | // Handle undefined fields. 157 | if (!fieldDefinition) { 158 | if (field !== keys.primary) 159 | clone[metaField][field] = record[field] 160 | continue 161 | } 162 | 163 | // Rearrange order of typed fields. 164 | if (fieldDefinition[keys.type]) { 165 | clone[field] = record[field] 166 | continue 167 | } 168 | 169 | // Handle link fields. 170 | const fieldIsReverse = field in options.reverseFields 171 | const node = { 172 | [hrefField]: encodeRoute(type, id, field, uriBase64) 173 | } 174 | 175 | // If the field is on the returned record, show the ids, if any. 176 | if (hasField) node[idField] = record[field] 177 | 178 | if (!fieldIsReverse) clone[field] = node 179 | else { 180 | const field = fieldDefinition[keys.inverse] 181 | 182 | if (!(reservedKeys.reverse in clone)) 183 | clone[reservedKeys.reverse] = {} 184 | 185 | clone[reservedKeys.reverse][field] = node 186 | } 187 | } 188 | 189 | if (Object.keys(clone[metaField]).length === 1) delete clone[metaField] 190 | 191 | return clone 192 | } 193 | 194 | 195 | function showQueries (meta) { 196 | const include = meta.include 197 | const options = meta.options 198 | 199 | return { 200 | include: include ? include.map(path => path.join('.')) : [], 201 | offset: options.offset || 0, 202 | limit: options.limit || 0, 203 | match: options.match || {}, 204 | range: options.range || {}, 205 | exists: options.exists || {}, 206 | fields: options.fields || {}, 207 | sort: options.sort || {} 208 | } 209 | } 210 | 211 | 212 | function attachIncluded (record) { 213 | if (!(metaField in record)) 214 | record[metaField] = { [contextField]: null } 215 | 216 | record[metaField].included = true 217 | 218 | return record 219 | } 220 | 221 | 222 | function parseBuffer (payload) { 223 | const BadRequestError = this.errors.BadRequestError 224 | 225 | if (!Buffer.isBuffer(payload)) return payload 226 | 227 | try { 228 | return JSON.parse(payload.toString()) 229 | } 230 | catch (error) { 231 | throw new BadRequestError(`Invalid JSON: ${error.message}`) 232 | } 233 | } 234 | 235 | 236 | function capitalize (str) { 237 | return str.charAt(0).toUpperCase() + str.slice(1) 238 | } 239 | 240 | 241 | function union () { 242 | const result = [] 243 | const seen = {} 244 | let value 245 | let array 246 | 247 | for (let g = 0, h = arguments.length; g < h; g++) { 248 | array = arguments[g] 249 | 250 | for (let i = 0, j = array.length; i < j; i++) { 251 | value = array[i] 252 | if (!(value in seen)) { 253 | seen[value] = true 254 | result.push(value) 255 | } 256 | } 257 | } 258 | 259 | return result 260 | } 261 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const msgpack = require('msgpack-lite') 4 | 5 | const settings = require('./settings') 6 | const mediaType = settings.mediaType 7 | const unregisteredMediaType = settings.unregisteredMediaType 8 | const typeMappings = settings.typeMappings 9 | const reservedKeys = settings.reservedKeys 10 | const defaults = settings.defaults 11 | 12 | const helpers = require('./helpers') 13 | const capitalize = helpers.capitalize 14 | const showQueries = helpers.showQueries 15 | const showContext = helpers.showContext 16 | const mapRecord = helpers.mapRecord 17 | const attachIncluded = helpers.attachIncluded 18 | const parseBuffer = helpers.parseBuffer 19 | 20 | const contextField = reservedKeys.context 21 | const hrefField = reservedKeys.href 22 | const idField = reservedKeys.id 23 | const typeField = reservedKeys.type 24 | const metaField = reservedKeys.meta 25 | const queryField = reservedKeys.query 26 | const errorField = reservedKeys.error 27 | 28 | // Index fields. 29 | const definitionsField = reservedKeys.definitions 30 | const classField = reservedKeys['class'] 31 | const propertyOfField = reservedKeys.propertyOf 32 | const propertyTypeField = reservedKeys.propertyType 33 | const labelField = reservedKeys.label 34 | const commentField = reservedKeys.comment 35 | const isArrayField = reservedKeys.isArray 36 | const inverseField = reservedKeys.inverse 37 | 38 | 39 | module.exports = microApiSerializer 40 | 41 | // Expose MessagePack serializer. 42 | microApiSerializer.msgpack = HttpSerializer => 43 | microApiSerializer(HttpSerializer, true) 44 | 45 | // Expose external context function. 46 | microApiSerializer.showExternalContext = helpers.showExternalContext 47 | 48 | 49 | function microApiSerializer (HttpSerializer, isMsgPack) { 50 | class MicroApiSerializer extends HttpSerializer { 51 | constructor (dependencies) { 52 | super(dependencies) 53 | 54 | const options = this.options 55 | 56 | if (!('entryPoint' in options)) throw new Error( 57 | 'The "entryPoint" option is required, it must be a URL ' + 58 | 'to the entry point.') 59 | 60 | // Strip trailing slash. 61 | options.entryPoint = options.entryPoint.replace(/\/$/, '') 62 | 63 | // Set options. 64 | for (const key in defaults) 65 | if (!(key in options)) 66 | options[key] = defaults[key] 67 | 68 | // Override `encodeRoute` method by prefixing entry point. 69 | const encodeRoute = this.encodeRoute 70 | this.encodeRoute = (type, id, field, b64) => 71 | options.entryPoint + encodeRoute(type, id, field, b64) 72 | } 73 | 74 | 75 | processResponse (contextResponse, request, response) { 76 | const options = this.options 77 | let payload = contextResponse.payload 78 | 79 | if (!contextResponse.meta) contextResponse.meta = {} 80 | if (!contextResponse.meta.headers) contextResponse.meta.headers = {} 81 | 82 | if (payload && 'records' in payload) 83 | contextResponse = this.showResponse(contextResponse, 84 | request, payload.records, payload.include) 85 | if (contextResponse instanceof Error) { 86 | if (contextResponse.isMethodInvalid) return contextResponse 87 | if (contextResponse.isTypeUnspecified) 88 | this.showIndex(contextResponse, request, response) 89 | else this.showError(contextResponse) 90 | } 91 | 92 | payload = contextResponse.payload 93 | if (!payload) return contextResponse 94 | 95 | if (!isMsgPack) { 96 | contextResponse.payload = JSON.stringify(payload, (key, value) => { 97 | // Duck type checking for buffer stringification. 98 | if (value && value.type === 'Buffer' && 99 | Array.isArray(value.data) && 100 | Object.keys(value).length === 2) 101 | return new Buffer(value.data).toString(options.bufferEncoding) 102 | 103 | return value 104 | }, options.jsonSpaces) 105 | 106 | // Set UTF-8 charset. 107 | response.setHeader('Content-Type', `${mediaType}; charset=utf-8`) 108 | } 109 | else contextResponse.payload = msgpack.encode(payload) 110 | 111 | if (options.externalContext) 112 | response.setHeader('Link', 113 | `<${options.entryPoint}${options.externalContext}>; ` + 114 | 'rel="http://www.w3.org/ns/json-ld#context"; ' + 115 | 'type="application/ld+json"') 116 | 117 | return contextResponse 118 | } 119 | 120 | 121 | showIndex (contextResponse, request, response) { 122 | const keys = this.keys 123 | const recordTypes = this.recordTypes 124 | const encodeRoute = this.encodeRoute 125 | const options = this.options 126 | const defaultLanguage = this.message.defaultLanguage 127 | const documentation = this.documentation || {} 128 | const language = request.meta.language 129 | const uriBase64 = options.uriBase64 130 | const inflectType = options.inflectType 131 | const definitions = [] 132 | const fields = {} 133 | 134 | const payload = contextResponse.payload = {} 135 | 136 | if (!options.externalContext) 137 | contextResponse.payload[contextField] = showContext(options) 138 | 139 | payload[hrefField] = encodeRoute(null, null, null, uriBase64) 140 | payload[typeField] = reservedKeys.ontology 141 | payload[definitionsField] = definitions 142 | 143 | for (const type in recordTypes) { 144 | const key = inflectType ? capitalize(type) : type 145 | 146 | const doc = { 147 | [hrefField]: `#${encodeURIComponent(key)}`, 148 | [idField]: key, 149 | [typeField]: classField 150 | } 151 | 152 | if (type in documentation) { 153 | // If documentation is explicitly falsy, don't reveal it. 154 | if (!documentation[type]) continue 155 | 156 | doc[commentField] = typeof documentation[type] === 'object' ? 157 | (documentation[type][language] || 158 | documentation[type][defaultLanguage]) : 159 | documentation[type] 160 | } 161 | 162 | payload[key] = { 163 | [hrefField]: encodeRoute(type, null, null, uriBase64) 164 | } 165 | 166 | definitions.push(doc) 167 | 168 | const typeFields = Object.getOwnPropertyNames(recordTypes[type]) 169 | 170 | for (let i = 0, j = typeFields.length; i < j; i++) { 171 | const field = typeFields[i] 172 | const definition = recordTypes[type][field] 173 | const descriptor = 174 | Object.getOwnPropertyDescriptor(recordTypes[type], field) 175 | 176 | if (!descriptor.enumerable && !definition.inputOnly) continue 177 | 178 | if (!fields.hasOwnProperty(field)) { 179 | const doc = { 180 | [hrefField]: `#${encodeURIComponent(field)}`, 181 | [idField]: field, 182 | [propertyOfField]: [] 183 | } 184 | 185 | if (keys.type in definition) doc[propertyTypeField] = 186 | typeMappings[definition[keys.type].prototype.constructor.name] 187 | else if (keys.link in definition) { 188 | doc[propertyTypeField] = `#${encodeURIComponent(inflectType ? 189 | capitalize(definition[keys.link]) : definition[keys.link])}` 190 | 191 | if (keys.inverse in definition && 192 | definition.propertyIsEnumerable(keys.inverse)) 193 | doc[inverseField] = 194 | `#${encodeURIComponent(definition[keys.inverse])}` 195 | } 196 | 197 | if (keys.isArray in definition) doc[isArrayField] = true 198 | 199 | if (field in documentation) { 200 | // If documentation is explicitly falsy, don't reveal it. 201 | if (!documentation[field]) continue 202 | 203 | doc[commentField] = 204 | typeof documentation[field] === 'object' ? 205 | (documentation[field][language] || 206 | documentation[field][defaultLanguage]) : 207 | documentation[field] 208 | } 209 | 210 | fields[field] = doc 211 | } 212 | 213 | fields[field][propertyOfField].push(`#${encodeURIComponent(key)}`) 214 | } 215 | } 216 | 217 | for (const field in fields) 218 | definitions.push(fields[field]) 219 | 220 | response.statusCode = 200 221 | } 222 | 223 | 224 | showResponse (contextResponse, request, records, include) { 225 | const keys = this.keys 226 | const methods = this.methods 227 | const encodeRoute = this.encodeRoute 228 | const NotFoundError = this.errors.NotFoundError 229 | 230 | const options = this.options 231 | const uriBase64 = options.uriBase64 232 | 233 | const method = request.meta.method 234 | const type = request.meta.type 235 | const ids = request.meta.ids 236 | const originalType = request.meta.originalType 237 | const originalIds = request.meta.originalIds 238 | const relatedField = request.meta.relatedField 239 | const updateModified = contextResponse.meta.updateModified 240 | 241 | // Handle a not found error. 242 | if (ids && ids.length && method === methods.find && 243 | !relatedField && !records.length) 244 | return new NotFoundError('No records match the request.') 245 | 246 | // Delete and update requests may not respond with anything. 247 | if (method === methods.delete || 248 | (method === methods.update && !updateModified)) { 249 | delete contextResponse.payload 250 | return contextResponse 251 | } 252 | 253 | // Create method should include location header. 254 | if (method === methods.create) 255 | contextResponse.meta.headers['Location'] = 256 | encodeRoute(type, records.map(record => record[keys.primary]), 257 | null, uriBase64) 258 | 259 | const output = {} 260 | 261 | if (!options.externalContext) 262 | output[contextField] = showContext(options) 263 | 264 | output[hrefField] = encodeRoute( 265 | originalType || type, 266 | originalIds || ids, 267 | relatedField, uriBase64) 268 | output[metaField] = Object.assign({ 269 | [reservedKeys.context]: null, 270 | count: records.count 271 | }, contextResponse.meta) 272 | 273 | delete output[metaField].headers 274 | 275 | // For the find method, it may be helpful to show available queries. 276 | if (method === methods.find) 277 | output[queryField] = Object.assign({ 278 | [reservedKeys.context]: null 279 | }, showQueries(request.meta)) 280 | 281 | // Handle edge case when one record is expected. 282 | if ((method !== methods.find || (ids && ids.length === 1)) && 283 | records.length === 1 && !include) { 284 | delete output[queryField] 285 | Object.assign(output, mapRecord.call(this, type, records[0])) 286 | } 287 | 288 | // At least one type will be present. 289 | else output[reservedKeys.graph] = records.map(record => 290 | mapRecord.call(this, type, record)) 291 | 292 | if (include) 293 | for (const includeType in include) 294 | Array.prototype.push.apply(output[reservedKeys.graph], 295 | include[includeType].map(record => 296 | attachIncluded.call(this, 297 | mapRecord.call(this, includeType, record)))) 298 | 299 | contextResponse.payload = output 300 | 301 | return contextResponse 302 | } 303 | 304 | 305 | showError (error) { 306 | const options = this.options 307 | const errorObject = Object.assign({ 308 | [labelField]: error.name, 309 | [commentField]: error.message 310 | }, error) 311 | 312 | delete errorObject.meta 313 | delete errorObject.payload 314 | 315 | const payload = {} 316 | 317 | if (!options.externalContext) 318 | payload[contextField] = showContext(options) 319 | 320 | payload[errorField] = errorObject 321 | error.payload = payload 322 | } 323 | 324 | 325 | parsePayload (contextRequest) { 326 | switch (contextRequest.method) { 327 | case this.methods.create: 328 | return this.parseCreate(contextRequest) 329 | case this.methods.update: 330 | return this.parseUpdate(contextRequest) 331 | default: 332 | throw new Error('Method is invalid.') 333 | } 334 | } 335 | 336 | 337 | parseCreate (contextRequest) { 338 | contextRequest.payload = parseBuffer.call(this, contextRequest.payload) 339 | 340 | const keys = this.keys 341 | const castValue = this.castValue 342 | const castToNumber = this.castToNumber 343 | const options = this.options 344 | const recordTypes = this.recordTypes 345 | const MethodError = this.errors.MethodError 346 | const BadRequestError = this.errors.BadRequestError 347 | 348 | const payload = contextRequest.payload 349 | const type = contextRequest.type 350 | const ids = contextRequest.ids 351 | const relatedField = contextRequest.relatedField 352 | 353 | const fields = recordTypes[type] 354 | const cast = (type, options) => value => castValue(value, type, options) 355 | 356 | if (ids) throw new MethodError( 357 | 'Can not create with IDs in the route.') 358 | 359 | if (relatedField) throw new MethodError( 360 | 'Can not create related record.') 361 | 362 | return (payload[reservedKeys.graph] || [ payload ]).map(record => { 363 | for (const field in record) { 364 | const value = record[field] 365 | 366 | if (field === keys.primary) { 367 | record[keys.primary] = options.castId ? castToNumber(value) : value 368 | continue 369 | } 370 | 371 | const fieldDefinition = fields[field] || {} 372 | const fieldType = fieldDefinition[keys.type] 373 | const fieldIsArray = fieldDefinition[keys.isArray] 374 | const fieldLink = fieldDefinition[keys.link] 375 | 376 | if (fieldLink) { 377 | record[field] = value[idField] 378 | continue 379 | } 380 | 381 | record[field] = fieldIsArray ? 382 | value.map(cast(fieldType, options)) : 383 | castValue(value, fieldType, options) 384 | } 385 | 386 | for (const field in record[reservedKeys.reverse]) { 387 | const value = record[reservedKeys.reverse][field] 388 | let reverseField 389 | 390 | for (const f in fields) 391 | if (fields[f][keys.inverse] === field && 392 | f in options.reverseFields) { 393 | reverseField = f 394 | break 395 | } 396 | 397 | if (!reverseField) throw new BadRequestError( 398 | `Reverse field for "${field}" not found.`) 399 | 400 | record[reverseField] = value[idField] 401 | } 402 | 403 | delete record[reservedKeys.reverse] 404 | 405 | return record 406 | }) 407 | } 408 | 409 | 410 | parseUpdate (contextRequest) { 411 | contextRequest.payload = parseBuffer.call(this, contextRequest.payload) 412 | 413 | const keys = this.keys 414 | const castValue = this.castValue 415 | const castToNumber = this.castToNumber 416 | const options = this.options 417 | const recordTypes = this.recordTypes 418 | const BadRequestError = this.errors.BadRequestError 419 | 420 | const payload = contextRequest.payload 421 | const type = contextRequest.type 422 | const ids = contextRequest.ids 423 | 424 | const fields = recordTypes[type] 425 | const cast = (type, options) => value => castValue(value, type, options) 426 | 427 | return (payload[reservedKeys.graph] || [ payload ]).map(update => { 428 | const clone = {} 429 | 430 | let updateId = update[idField] 431 | if (!updateId && ids.length === 1) updateId = ids[0] 432 | 433 | const id = options.castId ? 434 | castToNumber(updateId) : updateId 435 | 436 | if (!id) throw new BadRequestError( 437 | `A value for "${idField}" is missing.`) 438 | 439 | if (ids && !ids.some(i => i === id)) 440 | throw new BadRequestError( 441 | `The requested ID "${id}" is not addressable.`) 442 | 443 | clone[keys.primary] = id 444 | 445 | const replace = {} 446 | 447 | for (const field in update) { 448 | const value = update[field] 449 | const fieldDefinition = fields[field] || {} 450 | const fieldType = fieldDefinition[keys.type] 451 | const fieldIsArray = fieldDefinition[keys.isArray] 452 | const fieldLink = fieldDefinition[keys.link] 453 | 454 | if (fieldLink) { 455 | if (!(idField in value)) 456 | throw new BadRequestError(`The field "${field}" must be an ` + 457 | `object containing at least the key "${idField}".`) 458 | 459 | replace[field] = value[idField] 460 | continue 461 | } 462 | 463 | replace[field] = fieldIsArray ? 464 | value.map(cast(fieldType, options)) : 465 | castValue(value, fieldType, options) 466 | } 467 | 468 | for (const field in update[reservedKeys.reverse]) { 469 | const value = update[reservedKeys.reverse][field] 470 | let reverseField 471 | 472 | for (const f in fields) 473 | if (fields[f][keys.inverse] === field && 474 | f in options.reverseFields) { 475 | reverseField = f 476 | break 477 | } 478 | 479 | if (!reverseField) throw new BadRequestError( 480 | `Reverse field for "${field}" not found.`) 481 | 482 | replace[reverseField] = value[idField] 483 | } 484 | 485 | clone.replace = replace 486 | 487 | const operate = update[reservedKeys.operate] 488 | 489 | if (operate) { 490 | castFields(operate.push, operate.pull) 491 | if ('push' in operate) clone.push = operate.push 492 | if ('pull' in operate) clone.pull = operate.pull 493 | } 494 | 495 | return clone 496 | }) 497 | 498 | function castFields () { 499 | for (const object of arguments) 500 | for (const field in object) { 501 | const value = object[field] 502 | const fieldDefinition = fields[field] || {} 503 | const fieldIsArray = fieldDefinition[keys.isArray] 504 | const fieldType = fieldDefinition[keys.type] 505 | 506 | object[field] = fieldIsArray ? 507 | value.map(cast(fieldType, options)) : 508 | castValue(value, fieldType, options) 509 | } 510 | } 511 | } 512 | } 513 | 514 | MicroApiSerializer.mediaType = isMsgPack ? 515 | unregisteredMediaType : mediaType 516 | 517 | return MicroApiSerializer 518 | } 519 | -------------------------------------------------------------------------------- /lib/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | // Registered media type. 5 | mediaType: 'application/vnd.micro+json', 6 | 7 | // Unregistered media type. 8 | unregisteredMediaType: 'application/x-micro-api', 9 | 10 | // Linked data type. 11 | linkedDataType: 'application/ld+json', 12 | 13 | // Micro API JSON-LD context. 14 | contextURI: 'http://micro-api.org/context.jsonld', 15 | 16 | // Reserved keys from the JSON-LD & Micro API specifications. 17 | reservedKeys: { 18 | // JSON LD 19 | context: '@context', 20 | vocabulary: '@vocab', 21 | base: '@base', 22 | href: 'href', 23 | reverse: 'reverse', 24 | type: 'type', 25 | graph: 'graph', 26 | 27 | // Micro API 28 | id: 'id', 29 | error: 'error', 30 | meta: 'meta', 31 | query: 'query', 32 | operate: 'operate', 33 | isArray: 'isArray', 34 | 35 | // Ontology. 36 | definitions: 'definitions', 37 | ontology: 'Ontology', 38 | 'class': 'Class', 39 | property: 'Property', 40 | propertyOf: 'propertyOf', 41 | propertyType: 'propertyType', 42 | label: 'label', 43 | comment: 'comment', 44 | inverse: 'inverse' 45 | }, 46 | 47 | typeMappings: { 48 | 'String': 'xsd:string', 49 | 'Number': 'xsd:float', 50 | 'Boolean': 'xsd:boolean', 51 | 'Date': 'xsd:dateTime', 52 | 'Buffer': 'xsd:base64Binary', 53 | 'Object': 'xsd:complexType' 54 | }, 55 | 56 | defaults: { 57 | // Inflect the record type name in the payload to be PascalCase. 58 | inflectType: true, 59 | 60 | // Maximum number of records to show per page. 61 | maxLimit: 1000, 62 | 63 | // Maximum number of fields per include. 64 | includeLimit: 3, 65 | 66 | // What encoding to use for input and output buffer fields. 67 | bufferEncoding: 'base64', 68 | 69 | // How many spaces to use for pretty printing JSON. 70 | jsonSpaces: 2, 71 | 72 | // Encode URIs using base64. Useful for discouraging clients from tampering 73 | // with the URI. 74 | uriBase64: false, 75 | 76 | // URI to the entry point. Required. 77 | entryPoint: null, 78 | 79 | // URI to an external `@context`. Recommended. 80 | externalContext: null, 81 | 82 | // Whether or not to try to cast string IDs to numbers. 83 | castId: false, 84 | 85 | // What additional contexts to provide, such as schema.org. 86 | contexts: [], 87 | 88 | // Which fields should be considered reverses. 89 | reverseFields: {}, 90 | 91 | // What HTTP methods may be allowed, ordered by appearance in URI template. 92 | allowLevel: [ 93 | [ 'GET' ], // Index 94 | [ 'GET', 'POST', 'PATCH', 'DELETE' ], // Collection 95 | [ 'GET', 'PATCH', 'DELETE' ], // Records 96 | [ 'GET', 'PATCH', 'DELETE' ] // Related records 97 | ] 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fortune-micro-api", 3 | "version": "3.1.6", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.0.0", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", 10 | "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", 11 | "dev": true, 12 | "requires": { 13 | "@babel/highlight": "^7.0.0" 14 | } 15 | }, 16 | "@babel/highlight": { 17 | "version": "7.0.0", 18 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", 19 | "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", 20 | "dev": true, 21 | "requires": { 22 | "chalk": "^2.0.0", 23 | "esutils": "^2.0.2", 24 | "js-tokens": "^4.0.0" 25 | } 26 | }, 27 | "acorn": { 28 | "version": "6.0.4", 29 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.4.tgz", 30 | "integrity": "sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg==", 31 | "dev": true 32 | }, 33 | "acorn-jsx": { 34 | "version": "5.0.1", 35 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz", 36 | "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==", 37 | "dev": true 38 | }, 39 | "ajv": { 40 | "version": "6.6.1", 41 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", 42 | "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", 43 | "dev": true, 44 | "requires": { 45 | "fast-deep-equal": "^2.0.1", 46 | "fast-json-stable-stringify": "^2.0.0", 47 | "json-schema-traverse": "^0.4.1", 48 | "uri-js": "^4.2.2" 49 | } 50 | }, 51 | "ansi-escapes": { 52 | "version": "3.1.0", 53 | "resolved": "http://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", 54 | "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", 55 | "dev": true 56 | }, 57 | "ansi-regex": { 58 | "version": "3.0.0", 59 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", 60 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", 61 | "dev": true 62 | }, 63 | "ansi-styles": { 64 | "version": "3.2.1", 65 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 66 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 67 | "dev": true, 68 | "requires": { 69 | "color-convert": "^1.9.0" 70 | } 71 | }, 72 | "argparse": { 73 | "version": "1.0.10", 74 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 75 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 76 | "dev": true, 77 | "requires": { 78 | "sprintf-js": "~1.0.2" 79 | } 80 | }, 81 | "astral-regex": { 82 | "version": "1.0.0", 83 | "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", 84 | "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", 85 | "dev": true 86 | }, 87 | "balanced-match": { 88 | "version": "1.0.0", 89 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 90 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 91 | "dev": true 92 | }, 93 | "bindings": { 94 | "version": "1.2.1", 95 | "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", 96 | "integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE=", 97 | "dev": true, 98 | "optional": true 99 | }, 100 | "boolbase": { 101 | "version": "1.0.0", 102 | "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 103 | "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", 104 | "dev": true 105 | }, 106 | "brace-expansion": { 107 | "version": "1.1.11", 108 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 109 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 110 | "dev": true, 111 | "requires": { 112 | "balanced-match": "^1.0.0", 113 | "concat-map": "0.0.1" 114 | } 115 | }, 116 | "busboy": { 117 | "version": "0.2.14", 118 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", 119 | "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", 120 | "dev": true, 121 | "requires": { 122 | "dicer": "0.2.5", 123 | "readable-stream": "1.1.x" 124 | } 125 | }, 126 | "caller-path": { 127 | "version": "0.1.0", 128 | "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", 129 | "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", 130 | "dev": true, 131 | "requires": { 132 | "callsites": "^0.2.0" 133 | } 134 | }, 135 | "callsites": { 136 | "version": "0.2.0", 137 | "resolved": "http://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", 138 | "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", 139 | "dev": true 140 | }, 141 | "chalk": { 142 | "version": "2.4.1", 143 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", 144 | "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", 145 | "dev": true, 146 | "requires": { 147 | "ansi-styles": "^3.2.1", 148 | "escape-string-regexp": "^1.0.5", 149 | "supports-color": "^5.3.0" 150 | } 151 | }, 152 | "chardet": { 153 | "version": "0.7.0", 154 | "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", 155 | "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", 156 | "dev": true 157 | }, 158 | "circular-json": { 159 | "version": "0.3.3", 160 | "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", 161 | "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", 162 | "dev": true 163 | }, 164 | "clean-css": { 165 | "version": "4.2.1", 166 | "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", 167 | "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==", 168 | "dev": true, 169 | "requires": { 170 | "source-map": "~0.6.0" 171 | } 172 | }, 173 | "cli-cursor": { 174 | "version": "2.1.0", 175 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", 176 | "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", 177 | "dev": true, 178 | "requires": { 179 | "restore-cursor": "^2.0.0" 180 | } 181 | }, 182 | "cli-width": { 183 | "version": "2.2.0", 184 | "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", 185 | "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", 186 | "dev": true 187 | }, 188 | "color-convert": { 189 | "version": "1.9.3", 190 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 191 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 192 | "dev": true, 193 | "requires": { 194 | "color-name": "1.1.3" 195 | } 196 | }, 197 | "color-name": { 198 | "version": "1.1.3", 199 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 200 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 201 | "dev": true 202 | }, 203 | "concat-map": { 204 | "version": "0.0.1", 205 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 206 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 207 | "dev": true 208 | }, 209 | "cookie": { 210 | "version": "0.3.1", 211 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 212 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", 213 | "dev": true 214 | }, 215 | "core-util-is": { 216 | "version": "1.0.2", 217 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 218 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", 219 | "dev": true 220 | }, 221 | "cross-spawn": { 222 | "version": "6.0.5", 223 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", 224 | "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", 225 | "dev": true, 226 | "requires": { 227 | "nice-try": "^1.0.4", 228 | "path-key": "^2.0.1", 229 | "semver": "^5.5.0", 230 | "shebang-command": "^1.2.0", 231 | "which": "^1.2.9" 232 | } 233 | }, 234 | "css-select": { 235 | "version": "2.0.2", 236 | "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.0.2.tgz", 237 | "integrity": "sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==", 238 | "dev": true, 239 | "requires": { 240 | "boolbase": "^1.0.0", 241 | "css-what": "^2.1.2", 242 | "domutils": "^1.7.0", 243 | "nth-check": "^1.0.2" 244 | } 245 | }, 246 | "css-what": { 247 | "version": "2.1.2", 248 | "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.2.tgz", 249 | "integrity": "sha512-wan8dMWQ0GUeF7DGEPVjhHemVW/vy6xUYmFzRY8RYqgA0JtXC9rJmbScBjqSu6dg9q0lwPQy6ZAmJVr3PPTvqQ==", 250 | "dev": true 251 | }, 252 | "debug": { 253 | "version": "4.1.0", 254 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", 255 | "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", 256 | "dev": true, 257 | "requires": { 258 | "ms": "^2.1.1" 259 | } 260 | }, 261 | "deep-is": { 262 | "version": "0.1.3", 263 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", 264 | "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", 265 | "dev": true 266 | }, 267 | "dicer": { 268 | "version": "0.2.5", 269 | "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", 270 | "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", 271 | "dev": true, 272 | "requires": { 273 | "readable-stream": "1.1.x", 274 | "streamsearch": "0.1.2" 275 | } 276 | }, 277 | "doctrine": { 278 | "version": "2.1.0", 279 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", 280 | "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", 281 | "dev": true, 282 | "requires": { 283 | "esutils": "^2.0.2" 284 | } 285 | }, 286 | "dom-serializer": { 287 | "version": "0.1.0", 288 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", 289 | "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", 290 | "dev": true, 291 | "requires": { 292 | "domelementtype": "~1.1.1", 293 | "entities": "~1.1.1" 294 | }, 295 | "dependencies": { 296 | "domelementtype": { 297 | "version": "1.1.3", 298 | "resolved": "http://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", 299 | "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", 300 | "dev": true 301 | } 302 | } 303 | }, 304 | "domelementtype": { 305 | "version": "1.2.1", 306 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.2.1.tgz", 307 | "integrity": "sha512-SQVCLFS2E7G5CRCMdn6K9bIhRj1bS6QBWZfF0TUPh4V/BbqrQ619IdSS3/izn0FZ+9l+uODzaZjb08fjOfablA==", 308 | "dev": true 309 | }, 310 | "domhandler": { 311 | "version": "2.4.2", 312 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", 313 | "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", 314 | "dev": true, 315 | "requires": { 316 | "domelementtype": "1" 317 | } 318 | }, 319 | "domutils": { 320 | "version": "1.7.0", 321 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", 322 | "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", 323 | "dev": true, 324 | "requires": { 325 | "dom-serializer": "0", 326 | "domelementtype": "1" 327 | } 328 | }, 329 | "entities": { 330 | "version": "1.1.2", 331 | "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", 332 | "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", 333 | "dev": true 334 | }, 335 | "error-class": { 336 | "version": "2.0.2", 337 | "resolved": "https://registry.npmjs.org/error-class/-/error-class-2.0.2.tgz", 338 | "integrity": "sha512-Saa2G4bWSnh/I4Pw9bKHUvNZaxZJqthSV49dQxgM0vKGIBl5ap54AmBqsc0ONQYG5gOrKv4kLvfsZuvxM9Z23Q==", 339 | "dev": true 340 | }, 341 | "escape-string-regexp": { 342 | "version": "1.0.5", 343 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 344 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 345 | "dev": true 346 | }, 347 | "eslint": { 348 | "version": "5.10.0", 349 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.10.0.tgz", 350 | "integrity": "sha512-HpqzC+BHULKlnPwWae9MaVZ5AXJKpkxCVXQHrFaRw3hbDj26V/9ArYM4Rr/SQ8pi6qUPLXSSXC4RBJlyq2Z2OQ==", 351 | "dev": true, 352 | "requires": { 353 | "@babel/code-frame": "^7.0.0", 354 | "ajv": "^6.5.3", 355 | "chalk": "^2.1.0", 356 | "cross-spawn": "^6.0.5", 357 | "debug": "^4.0.1", 358 | "doctrine": "^2.1.0", 359 | "eslint-scope": "^4.0.0", 360 | "eslint-utils": "^1.3.1", 361 | "eslint-visitor-keys": "^1.0.0", 362 | "espree": "^5.0.0", 363 | "esquery": "^1.0.1", 364 | "esutils": "^2.0.2", 365 | "file-entry-cache": "^2.0.0", 366 | "functional-red-black-tree": "^1.0.1", 367 | "glob": "^7.1.2", 368 | "globals": "^11.7.0", 369 | "ignore": "^4.0.6", 370 | "imurmurhash": "^0.1.4", 371 | "inquirer": "^6.1.0", 372 | "js-yaml": "^3.12.0", 373 | "json-stable-stringify-without-jsonify": "^1.0.1", 374 | "levn": "^0.3.0", 375 | "lodash": "^4.17.5", 376 | "minimatch": "^3.0.4", 377 | "mkdirp": "^0.5.1", 378 | "natural-compare": "^1.4.0", 379 | "optionator": "^0.8.2", 380 | "path-is-inside": "^1.0.2", 381 | "pluralize": "^7.0.0", 382 | "progress": "^2.0.0", 383 | "regexpp": "^2.0.1", 384 | "require-uncached": "^1.0.3", 385 | "semver": "^5.5.1", 386 | "strip-ansi": "^4.0.0", 387 | "strip-json-comments": "^2.0.1", 388 | "table": "^5.0.2", 389 | "text-table": "^0.2.0" 390 | } 391 | }, 392 | "eslint-config-boss": { 393 | "version": "1.0.6", 394 | "resolved": "https://registry.npmjs.org/eslint-config-boss/-/eslint-config-boss-1.0.6.tgz", 395 | "integrity": "sha512-Wty0JFe8x+ltLGyjvJ6I7me1I7F6EZliK/Pt5hxwkKYO/Dr2SpXQFLApAUDaWeyw2RPBIq8qh/Zd9Lz7tgdwmg==", 396 | "dev": true 397 | }, 398 | "eslint-scope": { 399 | "version": "4.0.0", 400 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.0.tgz", 401 | "integrity": "sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA==", 402 | "dev": true, 403 | "requires": { 404 | "esrecurse": "^4.1.0", 405 | "estraverse": "^4.1.1" 406 | } 407 | }, 408 | "eslint-utils": { 409 | "version": "1.3.1", 410 | "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz", 411 | "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==", 412 | "dev": true 413 | }, 414 | "eslint-visitor-keys": { 415 | "version": "1.0.0", 416 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", 417 | "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", 418 | "dev": true 419 | }, 420 | "espree": { 421 | "version": "5.0.0", 422 | "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.0.tgz", 423 | "integrity": "sha512-1MpUfwsdS9MMoN7ZXqAr9e9UKdVHDcvrJpyx7mm1WuQlx/ygErEQBzgi5Nh5qBHIoYweprhtMkTCb9GhcAIcsA==", 424 | "dev": true, 425 | "requires": { 426 | "acorn": "^6.0.2", 427 | "acorn-jsx": "^5.0.0", 428 | "eslint-visitor-keys": "^1.0.0" 429 | } 430 | }, 431 | "esprima": { 432 | "version": "4.0.1", 433 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 434 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 435 | "dev": true 436 | }, 437 | "esquery": { 438 | "version": "1.0.1", 439 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", 440 | "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", 441 | "dev": true, 442 | "requires": { 443 | "estraverse": "^4.0.0" 444 | } 445 | }, 446 | "esrecurse": { 447 | "version": "4.2.1", 448 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", 449 | "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", 450 | "dev": true, 451 | "requires": { 452 | "estraverse": "^4.1.0" 453 | } 454 | }, 455 | "estraverse": { 456 | "version": "4.2.0", 457 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", 458 | "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", 459 | "dev": true 460 | }, 461 | "esutils": { 462 | "version": "2.0.2", 463 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", 464 | "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", 465 | "dev": true 466 | }, 467 | "event-lite": { 468 | "version": "0.1.1", 469 | "resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.1.tgz", 470 | "integrity": "sha1-R88IqNN9C2lM23s7F7UfqsZXYIY=" 471 | }, 472 | "external-editor": { 473 | "version": "3.0.3", 474 | "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", 475 | "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", 476 | "dev": true, 477 | "requires": { 478 | "chardet": "^0.7.0", 479 | "iconv-lite": "^0.4.24", 480 | "tmp": "^0.0.33" 481 | } 482 | }, 483 | "fast-crc32c": { 484 | "version": "1.0.4", 485 | "resolved": "https://registry.npmjs.org/fast-crc32c/-/fast-crc32c-1.0.4.tgz", 486 | "integrity": "sha1-qLVm6aouI7a0EWzz2NB/X1ItVOM=", 487 | "dev": true, 488 | "requires": { 489 | "sse4_crc32": "^5.0.0" 490 | } 491 | }, 492 | "fast-deep-equal": { 493 | "version": "2.0.1", 494 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", 495 | "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", 496 | "dev": true 497 | }, 498 | "fast-json-stable-stringify": { 499 | "version": "2.0.0", 500 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 501 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", 502 | "dev": true 503 | }, 504 | "fast-levenshtein": { 505 | "version": "2.0.6", 506 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 507 | "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", 508 | "dev": true 509 | }, 510 | "figures": { 511 | "version": "2.0.0", 512 | "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", 513 | "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", 514 | "dev": true, 515 | "requires": { 516 | "escape-string-regexp": "^1.0.5" 517 | } 518 | }, 519 | "file-entry-cache": { 520 | "version": "2.0.0", 521 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", 522 | "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", 523 | "dev": true, 524 | "requires": { 525 | "flat-cache": "^1.2.1", 526 | "object-assign": "^4.0.1" 527 | } 528 | }, 529 | "flat-cache": { 530 | "version": "1.3.4", 531 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", 532 | "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", 533 | "dev": true, 534 | "requires": { 535 | "circular-json": "^0.3.1", 536 | "graceful-fs": "^4.1.2", 537 | "rimraf": "~2.6.2", 538 | "write": "^0.2.1" 539 | } 540 | }, 541 | "fortune": { 542 | "version": "5.5.15", 543 | "resolved": "https://registry.npmjs.org/fortune/-/fortune-5.5.15.tgz", 544 | "integrity": "sha512-/OTcAvfNmxxxt4WWA3gEC3+5DwG03Icvh8CcCjOc8zCMe2JatP+sY8mxHN3e1Jtp4rFKOppt/CX/BD6ziM8MJw==", 545 | "dev": true, 546 | "requires": { 547 | "error-class": "^2.0.2", 548 | "event-lite": "^0.1.2" 549 | }, 550 | "dependencies": { 551 | "event-lite": { 552 | "version": "0.1.2", 553 | "resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.2.tgz", 554 | "integrity": "sha512-HnSYx1BsJ87/p6swwzv+2v6B4X+uxUteoDfRxsAb1S1BePzQqOLevVmkdA15GHJVd9A9Ok6wygUR18Hu0YeV9g==", 555 | "dev": true 556 | } 557 | } 558 | }, 559 | "fortune-http": { 560 | "version": "1.2.25", 561 | "resolved": "https://registry.npmjs.org/fortune-http/-/fortune-http-1.2.25.tgz", 562 | "integrity": "sha512-OCK76Gimz+kjC7VhR3lmVkcuXYTCEqU4iE+0IaiG+8poq/OR9xrAwdlD0cvWkBhKWbRfSHsKnEEYhu42dRhwGA==", 563 | "dev": true, 564 | "requires": { 565 | "busboy": "^0.2.14", 566 | "clean-css": "^4.2.1", 567 | "cookie": "^0.3.1", 568 | "fast-crc32c": "^1.0.4", 569 | "negotiator": "^0.6.1", 570 | "simulacra": "^2.1.16" 571 | } 572 | }, 573 | "fs.realpath": { 574 | "version": "1.0.0", 575 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 576 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 577 | "dev": true 578 | }, 579 | "functional-red-black-tree": { 580 | "version": "1.0.1", 581 | "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", 582 | "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", 583 | "dev": true 584 | }, 585 | "glob": { 586 | "version": "7.1.3", 587 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", 588 | "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", 589 | "dev": true, 590 | "requires": { 591 | "fs.realpath": "^1.0.0", 592 | "inflight": "^1.0.4", 593 | "inherits": "2", 594 | "minimatch": "^3.0.4", 595 | "once": "^1.3.0", 596 | "path-is-absolute": "^1.0.0" 597 | } 598 | }, 599 | "globals": { 600 | "version": "11.9.0", 601 | "resolved": "https://registry.npmjs.org/globals/-/globals-11.9.0.tgz", 602 | "integrity": "sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg==", 603 | "dev": true 604 | }, 605 | "graceful-fs": { 606 | "version": "4.1.15", 607 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", 608 | "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", 609 | "dev": true 610 | }, 611 | "has-flag": { 612 | "version": "3.0.0", 613 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 614 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 615 | "dev": true 616 | }, 617 | "htmlparser2": { 618 | "version": "3.10.0", 619 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.0.tgz", 620 | "integrity": "sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ==", 621 | "dev": true, 622 | "requires": { 623 | "domelementtype": "^1.3.0", 624 | "domhandler": "^2.3.0", 625 | "domutils": "^1.5.1", 626 | "entities": "^1.1.1", 627 | "inherits": "^2.0.1", 628 | "readable-stream": "^3.0.6" 629 | }, 630 | "dependencies": { 631 | "domelementtype": { 632 | "version": "1.3.0", 633 | "resolved": "http://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", 634 | "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", 635 | "dev": true 636 | }, 637 | "readable-stream": { 638 | "version": "3.0.6", 639 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.0.6.tgz", 640 | "integrity": "sha512-9E1oLoOWfhSXHGv6QlwXJim7uNzd9EVlWK+21tCU9Ju/kR0/p2AZYPz4qSchgO8PlLIH4FpZYfzwS+rEksZjIg==", 641 | "dev": true, 642 | "requires": { 643 | "inherits": "^2.0.3", 644 | "string_decoder": "^1.1.1", 645 | "util-deprecate": "^1.0.1" 646 | } 647 | }, 648 | "string_decoder": { 649 | "version": "1.1.1", 650 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 651 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 652 | "dev": true, 653 | "requires": { 654 | "safe-buffer": "~5.1.0" 655 | } 656 | } 657 | } 658 | }, 659 | "iconv-lite": { 660 | "version": "0.4.24", 661 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 662 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 663 | "dev": true, 664 | "requires": { 665 | "safer-buffer": ">= 2.1.2 < 3" 666 | } 667 | }, 668 | "ieee754": { 669 | "version": "1.1.8", 670 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", 671 | "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" 672 | }, 673 | "ignore": { 674 | "version": "4.0.6", 675 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", 676 | "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", 677 | "dev": true 678 | }, 679 | "imurmurhash": { 680 | "version": "0.1.4", 681 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 682 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", 683 | "dev": true 684 | }, 685 | "inflight": { 686 | "version": "1.0.6", 687 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 688 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 689 | "dev": true, 690 | "requires": { 691 | "once": "^1.3.0", 692 | "wrappy": "1" 693 | } 694 | }, 695 | "inherits": { 696 | "version": "2.0.3", 697 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 698 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 699 | "dev": true 700 | }, 701 | "inquirer": { 702 | "version": "6.2.1", 703 | "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.1.tgz", 704 | "integrity": "sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg==", 705 | "dev": true, 706 | "requires": { 707 | "ansi-escapes": "^3.0.0", 708 | "chalk": "^2.0.0", 709 | "cli-cursor": "^2.1.0", 710 | "cli-width": "^2.0.0", 711 | "external-editor": "^3.0.0", 712 | "figures": "^2.0.0", 713 | "lodash": "^4.17.10", 714 | "mute-stream": "0.0.7", 715 | "run-async": "^2.2.0", 716 | "rxjs": "^6.1.0", 717 | "string-width": "^2.1.0", 718 | "strip-ansi": "^5.0.0", 719 | "through": "^2.3.6" 720 | }, 721 | "dependencies": { 722 | "ansi-regex": { 723 | "version": "4.0.0", 724 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.0.0.tgz", 725 | "integrity": "sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==", 726 | "dev": true 727 | }, 728 | "strip-ansi": { 729 | "version": "5.0.0", 730 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.0.0.tgz", 731 | "integrity": "sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==", 732 | "dev": true, 733 | "requires": { 734 | "ansi-regex": "^4.0.0" 735 | } 736 | } 737 | } 738 | }, 739 | "int64-buffer": { 740 | "version": "0.1.9", 741 | "resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-0.1.9.tgz", 742 | "integrity": "sha1-ngOdoEOyT3ixlrKD4EZT716ZD2E=" 743 | }, 744 | "is-fullwidth-code-point": { 745 | "version": "2.0.0", 746 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 747 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", 748 | "dev": true 749 | }, 750 | "is-promise": { 751 | "version": "2.1.0", 752 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", 753 | "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", 754 | "dev": true 755 | }, 756 | "isarray": { 757 | "version": "1.0.0", 758 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 759 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 760 | }, 761 | "isexe": { 762 | "version": "2.0.0", 763 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 764 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 765 | "dev": true 766 | }, 767 | "js-tokens": { 768 | "version": "4.0.0", 769 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 770 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 771 | "dev": true 772 | }, 773 | "js-yaml": { 774 | "version": "3.12.0", 775 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", 776 | "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", 777 | "dev": true, 778 | "requires": { 779 | "argparse": "^1.0.7", 780 | "esprima": "^4.0.0" 781 | } 782 | }, 783 | "json-schema-traverse": { 784 | "version": "0.4.1", 785 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 786 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 787 | "dev": true 788 | }, 789 | "json-stable-stringify-without-jsonify": { 790 | "version": "1.0.1", 791 | "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 792 | "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", 793 | "dev": true 794 | }, 795 | "levn": { 796 | "version": "0.3.0", 797 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", 798 | "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", 799 | "dev": true, 800 | "requires": { 801 | "prelude-ls": "~1.1.2", 802 | "type-check": "~0.3.2" 803 | } 804 | }, 805 | "lodash": { 806 | "version": "4.17.11", 807 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", 808 | "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", 809 | "dev": true 810 | }, 811 | "mimic-fn": { 812 | "version": "1.2.0", 813 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", 814 | "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", 815 | "dev": true 816 | }, 817 | "minimatch": { 818 | "version": "3.0.4", 819 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 820 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 821 | "dev": true, 822 | "requires": { 823 | "brace-expansion": "^1.1.7" 824 | } 825 | }, 826 | "minimist": { 827 | "version": "0.0.8", 828 | "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 829 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 830 | "dev": true 831 | }, 832 | "mkdirp": { 833 | "version": "0.5.1", 834 | "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 835 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 836 | "dev": true, 837 | "requires": { 838 | "minimist": "0.0.8" 839 | } 840 | }, 841 | "ms": { 842 | "version": "2.1.1", 843 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 844 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", 845 | "dev": true 846 | }, 847 | "msgpack-lite": { 848 | "version": "0.1.26", 849 | "resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz", 850 | "integrity": "sha1-3TxQsm8FnyXn7e42REGDWOKprYk=", 851 | "requires": { 852 | "event-lite": "^0.1.1", 853 | "ieee754": "^1.1.8", 854 | "int64-buffer": "^0.1.9", 855 | "isarray": "^1.0.0" 856 | } 857 | }, 858 | "mute-stream": { 859 | "version": "0.0.7", 860 | "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", 861 | "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", 862 | "dev": true 863 | }, 864 | "nan": { 865 | "version": "2.11.0", 866 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz", 867 | "integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw==", 868 | "dev": true, 869 | "optional": true 870 | }, 871 | "natural-compare": { 872 | "version": "1.4.0", 873 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 874 | "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", 875 | "dev": true 876 | }, 877 | "negotiator": { 878 | "version": "0.6.1", 879 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 880 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", 881 | "dev": true 882 | }, 883 | "nice-try": { 884 | "version": "1.0.5", 885 | "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", 886 | "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", 887 | "dev": true 888 | }, 889 | "nth-check": { 890 | "version": "1.0.2", 891 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", 892 | "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", 893 | "dev": true, 894 | "requires": { 895 | "boolbase": "~1.0.0" 896 | } 897 | }, 898 | "object-assign": { 899 | "version": "4.1.1", 900 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 901 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", 902 | "dev": true 903 | }, 904 | "once": { 905 | "version": "1.4.0", 906 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 907 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 908 | "dev": true, 909 | "requires": { 910 | "wrappy": "1" 911 | } 912 | }, 913 | "onetime": { 914 | "version": "2.0.1", 915 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", 916 | "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", 917 | "dev": true, 918 | "requires": { 919 | "mimic-fn": "^1.0.0" 920 | } 921 | }, 922 | "optionator": { 923 | "version": "0.8.2", 924 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", 925 | "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", 926 | "dev": true, 927 | "requires": { 928 | "deep-is": "~0.1.3", 929 | "fast-levenshtein": "~2.0.4", 930 | "levn": "~0.3.0", 931 | "prelude-ls": "~1.1.2", 932 | "type-check": "~0.3.2", 933 | "wordwrap": "~1.0.0" 934 | } 935 | }, 936 | "os-tmpdir": { 937 | "version": "1.0.2", 938 | "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 939 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", 940 | "dev": true 941 | }, 942 | "path-is-absolute": { 943 | "version": "1.0.1", 944 | "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 945 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 946 | "dev": true 947 | }, 948 | "path-is-inside": { 949 | "version": "1.0.2", 950 | "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", 951 | "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", 952 | "dev": true 953 | }, 954 | "path-key": { 955 | "version": "2.0.1", 956 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", 957 | "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", 958 | "dev": true 959 | }, 960 | "pluralize": { 961 | "version": "7.0.0", 962 | "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", 963 | "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", 964 | "dev": true 965 | }, 966 | "prelude-ls": { 967 | "version": "1.1.2", 968 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", 969 | "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", 970 | "dev": true 971 | }, 972 | "progress": { 973 | "version": "2.0.3", 974 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", 975 | "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", 976 | "dev": true 977 | }, 978 | "punycode": { 979 | "version": "2.1.1", 980 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 981 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 982 | "dev": true 983 | }, 984 | "readable-stream": { 985 | "version": "1.1.14", 986 | "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", 987 | "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", 988 | "dev": true, 989 | "requires": { 990 | "core-util-is": "~1.0.0", 991 | "inherits": "~2.0.1", 992 | "isarray": "0.0.1", 993 | "string_decoder": "~0.10.x" 994 | }, 995 | "dependencies": { 996 | "isarray": { 997 | "version": "0.0.1", 998 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 999 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", 1000 | "dev": true 1001 | } 1002 | } 1003 | }, 1004 | "regexpp": { 1005 | "version": "2.0.1", 1006 | "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", 1007 | "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", 1008 | "dev": true 1009 | }, 1010 | "require-uncached": { 1011 | "version": "1.0.3", 1012 | "resolved": "http://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", 1013 | "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", 1014 | "dev": true, 1015 | "requires": { 1016 | "caller-path": "^0.1.0", 1017 | "resolve-from": "^1.0.0" 1018 | } 1019 | }, 1020 | "resolve-from": { 1021 | "version": "1.0.1", 1022 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", 1023 | "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", 1024 | "dev": true 1025 | }, 1026 | "restore-cursor": { 1027 | "version": "2.0.0", 1028 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", 1029 | "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", 1030 | "dev": true, 1031 | "requires": { 1032 | "onetime": "^2.0.0", 1033 | "signal-exit": "^3.0.2" 1034 | } 1035 | }, 1036 | "rimraf": { 1037 | "version": "2.6.2", 1038 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", 1039 | "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", 1040 | "dev": true, 1041 | "requires": { 1042 | "glob": "^7.0.5" 1043 | } 1044 | }, 1045 | "run-async": { 1046 | "version": "2.3.0", 1047 | "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", 1048 | "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", 1049 | "dev": true, 1050 | "requires": { 1051 | "is-promise": "^2.1.0" 1052 | } 1053 | }, 1054 | "rxjs": { 1055 | "version": "6.3.3", 1056 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", 1057 | "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==", 1058 | "dev": true, 1059 | "requires": { 1060 | "tslib": "^1.9.0" 1061 | } 1062 | }, 1063 | "safe-buffer": { 1064 | "version": "5.1.2", 1065 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1066 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 1067 | "dev": true 1068 | }, 1069 | "safer-buffer": { 1070 | "version": "2.1.2", 1071 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1072 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 1073 | "dev": true 1074 | }, 1075 | "semver": { 1076 | "version": "5.6.0", 1077 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", 1078 | "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", 1079 | "dev": true 1080 | }, 1081 | "shebang-command": { 1082 | "version": "1.2.0", 1083 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", 1084 | "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", 1085 | "dev": true, 1086 | "requires": { 1087 | "shebang-regex": "^1.0.0" 1088 | } 1089 | }, 1090 | "shebang-regex": { 1091 | "version": "1.0.0", 1092 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", 1093 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", 1094 | "dev": true 1095 | }, 1096 | "signal-exit": { 1097 | "version": "3.0.2", 1098 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 1099 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", 1100 | "dev": true 1101 | }, 1102 | "simulacra": { 1103 | "version": "2.1.16", 1104 | "resolved": "https://registry.npmjs.org/simulacra/-/simulacra-2.1.16.tgz", 1105 | "integrity": "sha512-ovGo6OVBHrhA96HRj7WbIPwrHQ7ndoQoUS5NNlXXcDJZvam1kVl7fhPiwg6CyXMxffzkux75vlCPFkOV5Y58uQ==", 1106 | "dev": true, 1107 | "requires": { 1108 | "css-select": "^2.0.0", 1109 | "dom-serializer": "^0.1.0", 1110 | "htmlparser2": "^3.9.2" 1111 | } 1112 | }, 1113 | "slice-ansi": { 1114 | "version": "2.0.0", 1115 | "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.0.0.tgz", 1116 | "integrity": "sha512-4j2WTWjp3GsZ+AOagyzVbzp4vWGtZ0hEZ/gDY/uTvm6MTxUfTUIsnMIFb1bn8o0RuXiqUw15H1bue8f22Vw2oQ==", 1117 | "dev": true, 1118 | "requires": { 1119 | "ansi-styles": "^3.2.0", 1120 | "astral-regex": "^1.0.0", 1121 | "is-fullwidth-code-point": "^2.0.0" 1122 | } 1123 | }, 1124 | "source-map": { 1125 | "version": "0.6.1", 1126 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 1127 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 1128 | "dev": true 1129 | }, 1130 | "sprintf-js": { 1131 | "version": "1.0.3", 1132 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 1133 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 1134 | "dev": true 1135 | }, 1136 | "sse4_crc32": { 1137 | "version": "5.3.0", 1138 | "resolved": "https://registry.npmjs.org/sse4_crc32/-/sse4_crc32-5.3.0.tgz", 1139 | "integrity": "sha512-AgnPwYR3ABQHRz1VKir0Ftcvz831tEd7ggwXkuTh1ULcDS6GMAioICCCnMj6YBmVnYbaeuePInqM7jrGuTencA==", 1140 | "dev": true, 1141 | "optional": true, 1142 | "requires": { 1143 | "bindings": "~1.2.1", 1144 | "nan": "2.11.0" 1145 | } 1146 | }, 1147 | "streamsearch": { 1148 | "version": "0.1.2", 1149 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", 1150 | "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", 1151 | "dev": true 1152 | }, 1153 | "string-width": { 1154 | "version": "2.1.1", 1155 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", 1156 | "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", 1157 | "dev": true, 1158 | "requires": { 1159 | "is-fullwidth-code-point": "^2.0.0", 1160 | "strip-ansi": "^4.0.0" 1161 | } 1162 | }, 1163 | "string_decoder": { 1164 | "version": "0.10.31", 1165 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 1166 | "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", 1167 | "dev": true 1168 | }, 1169 | "strip-ansi": { 1170 | "version": "4.0.0", 1171 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", 1172 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", 1173 | "dev": true, 1174 | "requires": { 1175 | "ansi-regex": "^3.0.0" 1176 | } 1177 | }, 1178 | "strip-json-comments": { 1179 | "version": "2.0.1", 1180 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 1181 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", 1182 | "dev": true 1183 | }, 1184 | "supports-color": { 1185 | "version": "5.5.0", 1186 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 1187 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 1188 | "dev": true, 1189 | "requires": { 1190 | "has-flag": "^3.0.0" 1191 | } 1192 | }, 1193 | "table": { 1194 | "version": "5.1.1", 1195 | "resolved": "https://registry.npmjs.org/table/-/table-5.1.1.tgz", 1196 | "integrity": "sha512-NUjapYb/qd4PeFW03HnAuOJ7OMcBkJlqeClWxeNlQ0lXGSb52oZXGzkO0/I0ARegQ2eUT1g2VDJH0eUxDRcHmw==", 1197 | "dev": true, 1198 | "requires": { 1199 | "ajv": "^6.6.1", 1200 | "lodash": "^4.17.11", 1201 | "slice-ansi": "2.0.0", 1202 | "string-width": "^2.1.1" 1203 | } 1204 | }, 1205 | "tapdance": { 1206 | "version": "5.1.1", 1207 | "resolved": "https://registry.npmjs.org/tapdance/-/tapdance-5.1.1.tgz", 1208 | "integrity": "sha512-vL8aVPZseZbF7iCdGQZwKnn/B+ZEyw95sU3aBJ7ATOmMSYriBLzGgUev3wZKC4gBPa3X85jdlQnwPwjvzk/tAw==", 1209 | "dev": true 1210 | }, 1211 | "text-table": { 1212 | "version": "0.2.0", 1213 | "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", 1214 | "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", 1215 | "dev": true 1216 | }, 1217 | "through": { 1218 | "version": "2.3.8", 1219 | "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", 1220 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", 1221 | "dev": true 1222 | }, 1223 | "tmp": { 1224 | "version": "0.0.33", 1225 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", 1226 | "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", 1227 | "dev": true, 1228 | "requires": { 1229 | "os-tmpdir": "~1.0.2" 1230 | } 1231 | }, 1232 | "tslib": { 1233 | "version": "1.9.3", 1234 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", 1235 | "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", 1236 | "dev": true 1237 | }, 1238 | "type-check": { 1239 | "version": "0.3.2", 1240 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", 1241 | "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", 1242 | "dev": true, 1243 | "requires": { 1244 | "prelude-ls": "~1.1.2" 1245 | } 1246 | }, 1247 | "uri-js": { 1248 | "version": "4.2.2", 1249 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 1250 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 1251 | "dev": true, 1252 | "requires": { 1253 | "punycode": "^2.1.0" 1254 | } 1255 | }, 1256 | "util-deprecate": { 1257 | "version": "1.0.2", 1258 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1259 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", 1260 | "dev": true 1261 | }, 1262 | "which": { 1263 | "version": "1.3.1", 1264 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 1265 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 1266 | "dev": true, 1267 | "requires": { 1268 | "isexe": "^2.0.0" 1269 | } 1270 | }, 1271 | "wordwrap": { 1272 | "version": "1.0.0", 1273 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", 1274 | "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", 1275 | "dev": true 1276 | }, 1277 | "wrappy": { 1278 | "version": "1.0.2", 1279 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1280 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1281 | "dev": true 1282 | }, 1283 | "write": { 1284 | "version": "0.2.1", 1285 | "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", 1286 | "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", 1287 | "dev": true, 1288 | "requires": { 1289 | "mkdirp": "^0.5.1" 1290 | } 1291 | } 1292 | } 1293 | } 1294 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fortune-micro-api", 3 | "description": "Micro API serializer for Fortune.", 4 | "version": "3.1.7", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:fortunejs/fortune-micro-api.git" 9 | }, 10 | "bugs": "https://github.com/fortunejs/fortune-micro-api/issues", 11 | "scripts": { 12 | "postpublish": "npm run tag", 13 | "tag": "git tag `npm v fortune-micro-api version` && git push origin --tags", 14 | "test": "npm run lint && node test", 15 | "lint": "eslint lib test" 16 | }, 17 | "dependencies": { 18 | "msgpack-lite": "^0.1.26" 19 | }, 20 | "devDependencies": { 21 | "chalk": "^2.4.1", 22 | "eslint": "^5.10.0", 23 | "eslint-config-boss": "^1.0.6", 24 | "fortune": "^5.5.15", 25 | "fortune-http": "^1.2.25", 26 | "tapdance": "^5.1.1" 27 | }, 28 | "files": [ 29 | "lib/", 30 | "LICENSE" 31 | ], 32 | "main": "lib/index.js", 33 | "eslintConfig": { 34 | "extends": "boss", 35 | "rules": { 36 | "strict": 0 37 | } 38 | }, 39 | "engines": { 40 | "node": ">=6.10" 41 | }, 42 | "keywords": [ 43 | "micro", 44 | "api", 45 | "fortune", 46 | "http", 47 | "hypermedia", 48 | "rest", 49 | "serializer" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const deepEqual = require('fortune/lib/common/deep_equal') 2 | const qs = require('querystring') 3 | 4 | const run = require('tapdance') 5 | 6 | const httpTest = require('fortune-http/test/http_test') 7 | const microApiSerializer = require('../lib') 8 | const settings = require('../lib/settings') 9 | 10 | const reservedKeys = settings.reservedKeys 11 | const mediaType = settings.mediaType 12 | const unregisteredMediaType = settings.unregisteredMediaType 13 | 14 | const options = { 15 | entryPoint: 'http://example.com/', 16 | externalContext: '/context.jsonld', 17 | castId: true 18 | } 19 | 20 | const httpOptions = { 21 | serializers: [ 22 | [ microApiSerializer, options ], 23 | [ microApiSerializer.msgpack, options ] 24 | ] 25 | } 26 | 27 | function test (path, parameters, callback) { 28 | return httpTest(httpOptions, path, parameters, callback) 29 | } 30 | 31 | 32 | run((assert, comment) => { 33 | comment('use msgpack') 34 | return test('/', { 35 | headers: { 36 | 'Accept': unregisteredMediaType, 37 | 'Accept-Encoding': '*' 38 | } 39 | }, response => { 40 | assert(response.status === 200, 'status is correct') 41 | assert(response.headers['content-type'] === unregisteredMediaType, 42 | 'content type is correct') 43 | }) 44 | }) 45 | 46 | 47 | run((assert, comment) => { 48 | comment('show index') 49 | return test('/', null, response => { 50 | assert(response.status === 200, 'status is correct') 51 | assert(~response.headers['content-type'].indexOf(mediaType), 52 | 'content type is correct') 53 | assert('User' in response.body, 'user type in body') 54 | assert('Animal' in response.body, 'animal type in body') 55 | assert('☯' in response.body, 'mystery type in body') 56 | }) 57 | }) 58 | 59 | 60 | run((assert, comment) => { 61 | comment('show collection') 62 | return test('/user', null, response => { 63 | assert(response.status === 200, 'status is correct') 64 | assert(~response.headers['content-type'].indexOf(mediaType), 65 | 'content type is correct') 66 | assert(response.body.graph.length === 3, 67 | 'number of records correct') 68 | }) 69 | }) 70 | 71 | 72 | run((assert, comment) => { 73 | comment('show individual record with include') 74 | return test(`/user/1?${qs.stringify({ 75 | 'include': [ 'spouse', 'spouse.friends' ] 76 | })}`, null, response => { 77 | assert(response.status === 200, 'status is correct') 78 | assert(~response.headers['content-type'].indexOf(mediaType), 79 | 'content type is correct') 80 | assert(response.body.graph.length === 3, 'number of records correct') 81 | }) 82 | }) 83 | 84 | 85 | run((assert, comment) => { 86 | comment('show individual record with encoded ID') 87 | return test(`/animal/%2Fwtf?${qs.stringify({ 88 | 'fields': [ 'birthday', 'type' ] 89 | })}`, null, response => { 90 | assert(response.status === 200, 'status is correct') 91 | assert(~response.headers['content-type'].indexOf(mediaType), 92 | 'content type is correct') 93 | assert(!('graph' in response.body), 'single record') 94 | assert(Object.keys(response.body).length === 7, 95 | 'number of fields correct') 96 | }) 97 | }) 98 | 99 | 100 | run((assert, comment) => { 101 | comment('sort a collection and use sparse fields') 102 | return test( 103 | `/user?${qs.stringify({ 104 | 'sort': [ 'birthday', '-name' ], 105 | 'fields': [ 'name', 'birthday' ] 106 | })}`, null, response => { 107 | assert(response.status === 200, 'status is correct') 108 | assert(~response.headers['content-type'].indexOf(mediaType), 109 | 'content type is correct') 110 | assert(deepEqual( 111 | response.body.graph.map(record => record.name), 112 | [ 113 | 'John Doe', 'Microsoft Bob', 'Jane Doe' 114 | ]), 'sort order is correct') 115 | }) 116 | }) 117 | 118 | 119 | run((assert, comment) => { 120 | comment('match on a collection') 121 | return test(`/user?${qs.stringify({ 122 | 'match.name': [ 'John Doe', 'Jane Doe' ], 123 | 'match.birthday': '1992-12-07' 124 | })}`, null, response => { 125 | assert(response.status === 200, 'status is correct') 126 | assert(~response.headers['content-type'].indexOf(mediaType), 127 | 'content type is correct') 128 | assert(deepEqual( 129 | response.body.graph.map(record => record.name).sort(), 130 | [ 'John Doe' ]), 'match is correct') 131 | }) 132 | }) 133 | 134 | 135 | run((assert, comment) => { 136 | comment('show related records') 137 | return test('/user/2/ownedPets', null, response => { 138 | assert(response.status === 200, 'status is correct') 139 | assert(~response.headers['content-type'].indexOf(mediaType), 140 | 'content type is correct') 141 | assert(response.body.graph.length === 2, 142 | 'number of records correct') 143 | }) 144 | }) 145 | 146 | 147 | run((assert, comment) => { 148 | comment('find an empty collection') 149 | return test(encodeURI('/☯'), null, response => { 150 | assert(response.status === 200, 'status is correct') 151 | assert(~response.headers['content-type'].indexOf(mediaType), 152 | 'content type is correct') 153 | assert(Array.isArray(response.body.graph) && 154 | !response.body.graph.length, 'payload is empty array') 155 | }) 156 | }) 157 | 158 | 159 | run((assert, comment) => { 160 | comment('find a single non-existent record') 161 | return test('/user/4', null, response => { 162 | assert(response.status === 404, 'status is correct') 163 | assert(~response.headers['content-type'].indexOf(mediaType), 164 | 'content type is correct') 165 | assert('error' in response.body, 'error object exists') 166 | assert(response.body.error.label === 'NotFoundError', 167 | 'name is correct') 168 | assert(response.body.error.comment.length, 'message exists') 169 | }) 170 | }) 171 | 172 | 173 | run((assert, comment) => { 174 | comment('find a collection of non-existent related records') 175 | return test('/user/3/ownedPets', null, response => { 176 | assert(response.status === 200, 'status is correct') 177 | assert(~response.headers['content-type'].indexOf(mediaType), 178 | 'content type is correct') 179 | assert(Array.isArray(response.body.graph) && 180 | !response.body.graph.length, 'payload is empty array') 181 | }) 182 | }) 183 | 184 | 185 | run((assert, comment) => { 186 | comment('create record') 187 | return test('/animal', { 188 | method: 'post', 189 | headers: { 'Content-Type': mediaType }, 190 | body: { 191 | name: 'Rover', 192 | birthday: new Date().toJSON(), 193 | picture: new Buffer('This is a string.').toString('base64'), 194 | nicknames: [ 'Foo', 'Bar' ], 195 | owner: { 'id': 1 } 196 | } 197 | }, response => { 198 | assert(response.status === 201, 'status is correct') 199 | assert(~response.headers['content-type'].indexOf(mediaType), 200 | 'content type is correct') 201 | assert(response.headers['location'] === response.body.href, 202 | 'location header is correct') 203 | assert(response.body.type === 'Animal', 'type is correct') 204 | assert(response.body.owner.id === 1, 'link is correct') 205 | assert(new Buffer(response.body.picture, 'base64') 206 | .toString() === 'This is a string.', 'buffer is correct') 207 | assert(Date.now() - new Date(response.body.birthday) 208 | .getTime() < 60 * 1000, 'date is close enough') 209 | }, (change, methods) => { 210 | assert(change[methods.create].animal[0], 'created ID exists') 211 | }) 212 | }) 213 | 214 | 215 | run((assert, comment) => { 216 | comment('create record with existing ID should fail') 217 | return test('/user', { 218 | method: 'post', 219 | headers: { 'Content-Type': mediaType }, 220 | body: { 221 | id: 1 222 | } 223 | }, response => { 224 | assert(response.status === 409, 'status is correct') 225 | assert(~response.headers['content-type'].indexOf(mediaType), 226 | 'content type is correct') 227 | assert(response.body['error'], 'error exists') 228 | }) 229 | }) 230 | 231 | 232 | run((assert, comment) => { 233 | comment('create record on wrong route should fail') 234 | return test('/user/1', { 235 | method: 'post', 236 | headers: { 'Content-Type': mediaType }, 237 | body: {} 238 | }, response => { 239 | assert(response.status === 405, 'status is correct') 240 | assert(~response.headers['content-type'].indexOf(mediaType), 241 | 'content type is correct') 242 | assert(response.body.error, 'error exists') 243 | }) 244 | }) 245 | 246 | 247 | run((assert, comment) => { 248 | comment('update record #1') 249 | return test('/user/2', { 250 | method: 'patch', 251 | headers: { 'Content-Type': mediaType }, 252 | body: { 253 | id: 2, 254 | name: 'Jenny Death', 255 | spouse: { id: 3 }, 256 | enemies: { id: [ 3 ] }, 257 | friends: { id: [ 1, 3 ] } 258 | } 259 | }, response => { 260 | assert(response.status === 200, 'status is correct') 261 | assert(~response.headers['content-type'].indexOf(mediaType), 262 | 'content type is correct') 263 | assert(Math.abs(new Date(response.body.lastModifiedAt) 264 | .getTime() - Date.now()) < 5 * 1000, 'update modifier is correct') 265 | }) 266 | }) 267 | 268 | 269 | run((assert, comment) => { 270 | comment('update record #2') 271 | return test('/animal/2', { 272 | method: 'patch', 273 | headers: { 'Content-Type': mediaType }, 274 | body: { 275 | id: 2, 276 | nicknames: [ 'Baz', 'Qux' ] 277 | } 278 | }, response => { 279 | assert(response.status === 200, 'status is correct') 280 | assert(~response.headers['content-type'].indexOf(mediaType), 281 | 'content type is correct') 282 | assert(Math.abs(new Date(response.body.lastModifiedAt) 283 | .getTime() - Date.now()) < 5 * 1000, 'update modifier is correct') 284 | }) 285 | }) 286 | 287 | 288 | run((assert, comment) => { 289 | comment('delete a single record') 290 | return test('/animal/3', { method: 'delete' }, response => { 291 | assert(response.status === 204, 'status is correct') 292 | }) 293 | }) 294 | 295 | 296 | run((assert, comment) => { 297 | comment('respond to options: index') 298 | return test('/', { method: 'options' }, response => { 299 | assert(response.status === 204, 'status is correct') 300 | assert(response.headers['allow'] === 'GET', 'allow header is correct') 301 | }) 302 | }) 303 | 304 | 305 | run((assert, comment) => { 306 | comment('respond to options: collection') 307 | return test('/user', { method: 'options' }, response => { 308 | assert(response.status === 204, 'status is correct') 309 | assert(response.headers['allow'] === 'GET, POST, PATCH, DELETE', 310 | 'allow header is correct') 311 | }) 312 | }) 313 | 314 | 315 | run((assert, comment) => { 316 | comment('respond to options: IDs') 317 | return test('/user/3', { method: 'options' }, response => { 318 | assert(response.status === 204, 'status is correct') 319 | assert(response.headers['allow'] === 'GET, PATCH, DELETE', 320 | 'allow header is correct') 321 | }) 322 | }) 323 | 324 | 325 | run((assert, comment) => { 326 | comment('respond to options: related') 327 | return test('/user/3/ownedPets', { method: 'options' }, response => { 328 | assert(response.status === 204, 'status is correct') 329 | assert(response.headers['allow'] === 'GET, PATCH, DELETE', 330 | 'allow header is correct') 331 | }) 332 | }) 333 | 334 | 335 | run((assert, comment) => { 336 | comment('respond to options: fail') 337 | return test('/foo', { method: 'options' }, response => { 338 | assert(response.status === 404, 'status is correct') 339 | }) 340 | }) 341 | 342 | run((assert, comment) => { 343 | comment('show external context') 344 | 345 | let asserts = 0 346 | 347 | const response = { 348 | setHeader () { 349 | assert(true, 'header is set') 350 | asserts++ 351 | }, 352 | end (payload, callback) { 353 | const context = JSON.parse(payload.toString())[reservedKeys.context] 354 | assert(context[reservedKeys.base], 'base is set') 355 | assert(context[reservedKeys.vocabulary], 'vocab is set') 356 | asserts++ 357 | callback() 358 | } 359 | } 360 | 361 | const options = { 362 | entryPoint: 'http://example.com', 363 | contexts: [], 364 | jsonSpaces: 2 365 | } 366 | 367 | return microApiSerializer.showExternalContext(response, options) 368 | .then(() => assert(asserts === 2, 'all assertions ran')) 369 | .catch(() => assert(false, 'something went wrong')) 370 | }) 371 | --------------------------------------------------------------------------------