From a8624f4e585e5b2f890ad8181490d99c4a86e844 Mon Sep 17 00:00:00 2001 From: Patrick Leary Date: Tue, 19 Jul 2022 12:24:02 -0400 Subject: [PATCH] add more info to some errors #331; start documenting obs POST/PUT schema #332 --- lib/inaturalist_api_v2.js | 73 ++++++++++++------- openapi/schema/request/observations_create.js | 52 +++++++++---- 2 files changed, 85 insertions(+), 40 deletions(-) diff --git a/lib/inaturalist_api_v2.js b/lib/inaturalist_api_v2.js index 1fa24000..991c012e 100644 --- a/lib/inaturalist_api_v2.js +++ b/lib/inaturalist_api_v2.js @@ -59,7 +59,7 @@ const InaturalistAPIV2 = class InaturalistAPIV2 { from: "errorMiddleware" }] } ); - } ); + } ).catch( ( ) => InaturalistAPIV2.renderDefaultError( res, err, status ) ); return; } // console.log( "Error trace from errorMiddleware:" ); @@ -67,7 +67,10 @@ const InaturalistAPIV2 = class InaturalistAPIV2 { // console.log( `[DEBUG] Error from request for ${req.path}` ); // } // console.trace( err ); + InaturalistAPIV2.renderDefaultError( res, err, status ); + }; + static renderDefaultError = ( res, err, status ) => { res.status( status || 500 ).jsonp( err instanceof Error ? { status: ( status || 500 ).toString( ), @@ -488,7 +491,7 @@ const InaturalistAPIV2 = class InaturalistAPIV2 { if ( req.userSession ) { const defaults = await User.localeDefaults( req.userSession.user_id ); if ( defaults ) { - req.userSession = Object.assign( { }, req.userSession, defaults ); + req.userSession = { ...req.userSession, ...defaults }; } if ( req.userSession.isSuspended ) { const error = new Error( "Unauthorized" ); @@ -497,7 +500,7 @@ const InaturalistAPIV2 = class InaturalistAPIV2 { } } next( ); - } + }; static userJwtValidate = ( req, required ) => { if ( !req.headers.authorization ) { @@ -507,15 +510,16 @@ const InaturalistAPIV2 = class InaturalistAPIV2 { return Promise.resolve( true ); } const token = _.last( req.headers.authorization.split( /\s+/ ) ); - return jwt.verify( token, config.jwtSecret || "secret", - { algorithms: ["HS512"] }, ( err, payload ) => { - if ( required === "required" && err ) throw jwtInvalidError; - if ( payload && payload.user_id ) { - req.userSession = payload; - } - return Promise.resolve( true ); - } ); - } + return jwt.verify( token, config.jwtSecret || "secret", { + algorithms: ["HS512"] + }, ( err, payload ) => { + if ( required === "required" && err ) throw jwtInvalidError; + if ( payload && payload.user_id ) { + req.userSession = payload; + } + return Promise.resolve( true ); + } ); + }; static applicationJwtValidate = ( req, required ) => { if ( !req.headers.authorization ) { @@ -525,15 +529,16 @@ const InaturalistAPIV2 = class InaturalistAPIV2 { return Promise.resolve( true ); } const token = _.last( req.headers.authorization.split( /\s+/ ) ); - return jwt.verify( token, config.jwtApplicationSecret || "application_secret", - { algorithms: ["HS512"] }, ( err, payload ) => { - if ( required === "required" && err ) throw jwtInvalidError; - if ( payload && payload.application ) { - req.applicationSession = payload; - } - return Promise.resolve( true ); - } ); - } + return jwt.verify( token, config.jwtApplicationSecret || "application_secret", { + algorithms: ["HS512"] + }, ( err, payload ) => { + if ( required === "required" && err ) throw jwtInvalidError; + if ( payload && payload.application ) { + req.applicationSession = payload; + } + return Promise.resolve( true ); + } ); + }; static initializeOpenapi = inaturalistAPIExpressApp => { // method override middleware must be defined before initializing openapi @@ -592,6 +597,22 @@ const InaturalistAPIV2 = class InaturalistAPIV2 { // TODO: custom coercion for JSON bodies? "multipart/form-data": InaturalistAPIV2.multipartMiddleware }, + errorTransformer: ( openapiError, ajvError ) => { + console.log( openapiError ); + console.log( ajvError ); + const returnedError = { + message: openapiError.message + }; + if ( ajvError && ajvError ) { + if ( ajvError.instancePath ) { + returnedError.instancePath = ajvError.instancePath; + } + if ( ajvError.params ) { + returnedError.params = ajvError.params; + } + } + return returnedError; + }, errorMiddleware: InaturalistAPIV2.errorMiddleware } ); @@ -613,19 +634,21 @@ const InaturalistAPIV2 = class InaturalistAPIV2 { // "unsupported," or provide some kind of param that shows these private and // unsupported endpoints to people who are interested, like developers on // staff. ~~kueda 20200813 - const swaggerApiDoc = Object.assign( {}, initializedOpenapi.apiDoc, { + const swaggerApiDoc = { + ...initializedOpenapi.apiDoc, paths: _.omitBy( initializedOpenapi.apiDoc.paths, ( pathConfig, path ) => path.match( /computervision/ ) ), tags: _.sortBy( _.omitBy( initializedOpenapi.apiDoc.tags, tag => tag.name.match( /Computer Vision/ ) ), tag => tag.name ), - components: Object.assign( {}, initializedOpenapi.apiDoc.components, { + components: { + ...initializedOpenapi.apiDoc.components, schemas: _.omitBy( initializedOpenapi.apiDoc.components.schemas, ( schema, schemaName ) => schemaName.match( /vision/i ) ) - } ) - } ); + } + }; const swaggerUIMiddleware = swaggerUi.setup( swaggerApiDoc, swaggerOptions ); // temporary fix for swaggerUI loading issues // See https://github.com/scottie1984/swagger-ui-express/issues/178 diff --git a/openapi/schema/request/observations_create.js b/openapi/schema/request/observations_create.js index 18bb2a0f..173a7ca9 100644 --- a/openapi/schema/request/observations_create.js +++ b/openapi/schema/request/observations_create.js @@ -2,18 +2,40 @@ const Joi = require( "joi" ); module.exports = Joi.object( ).keys( { fields: Joi.any( ), - observation: Joi.object( ).unknown( true ) - // observation: Joi.object( ).keys( { - // species_guess: Joi.string( ) - // .description( ` - // The name of the organism observed. If the taxon ID is absent, iNat will - // try to choose a single taxon based on this string, but it may fail if - // there's some taxonomic amgiguity." - // ` ), - // taxon_uuid: Joi.string( ).guid( { version: "uuidv4" } ) - // .description( ` - // UUID of the taxon to associate with this observation. An identification - // for this taxon will automatically be added for the user. - // ` ) - // } ) -} ); + observation: Joi.object( ).keys( { + captive_flag: Joi.boolean( ), + coordinate_system: Joi.string( ), + description: Joi.string( ), + geo_x: Joi.number( ), + geo_y: Joi.number( ), + geoprivacy: Joi.string( ), + latitude: Joi.number( ), + license: Joi.string( ), + location_is_exact: Joi.boolean( ), + longitude: Joi.number( ), + make_license_default: Joi.boolean( ), + make_licenses_same: Joi.boolean( ), + map_scale: Joi.number( ).integer( ), + observation_field_values_attributes: Joi.object( ).keys( { + observation_field_id: Joi.number( ).integer( ).required( ), + value: Joi.any( ).required( ) + } ).unknown( false ), + observed_on_string: Joi.string( ), + owners_identification_from_vision: Joi.boolean( ), + place_guess: Joi.string( ), + positional_accuracy: Joi.number( ), + positioning_device: Joi.string( ), + positioning_method: Joi.string( ), + project_id: Joi.number( ).integer( ), + prefers_community_taxon: Joi.boolean( ), + site_id: Joi.number( ).integer( ), + species_guess: Joi.string( ) + .description( "The name of the organism observed. If the taxon ID is absent, iNat will " + + "try to choose a single taxon based on this string, but it may fail if " + + "there's some taxonomic amgiguity." ), + tag_list: Joi.string( ), + taxon_id: Joi.number( ), + taxon_name: Joi.number( ), + time_zone: Joi.string( ) + } ).unknown( false ) +} ).unknown( false );