Merge pull request #3783 from inaturalist/taxon-mapper

Taxon mapper
This commit is contained in:
Seth Peterson
2026-07-01 16:08:09 -05:00
committed by GitHub
2 changed files with 145 additions and 0 deletions

View File

@@ -160,6 +160,43 @@ class Taxon extends Realm.Object {
};
}
/**
* Inverse of mapApiToRealm: maps a live Realm Taxon into the plain,
* snake_case ApiTaxon shape the API and most display/search code expect.
* Live Realm objects expose camelCase mapTo accessors (preferredCommonName,
* defaultPhoto); we read those (falling back to the snake_case keys so a
* plain object passes through unchanged) and return a detached POJO. Use
* this whenever a live Taxon needs to leave the data layer (React/Zustand
* state, async boundaries, child components) so we aren't passing around an
* object Realm can invalidate underneath us.
*
* @param {object} taxon - a live Realm Taxon (or an already-plain object)
* @returns {object} plain ApiTaxon-shaped object
*/
static mapRealmToPojo( taxon ) {
if ( !taxon ) return taxon;
const photo = taxon.defaultPhoto || taxon.default_photo;
return {
id: taxon.id,
name: taxon.name,
rank: taxon.rank,
rank_level: taxon.rank_level,
iconic_taxon_name: taxon.iconic_taxon_name,
ancestor_ids: taxon.ancestor_ids
? Array.from( taxon.ancestor_ids )
: undefined,
preferred_common_name: taxon.preferredCommonName ?? taxon.preferred_common_name,
default_photo: photo?.url
? {
id: photo.id,
url: photo.url,
attribution: photo.attribution,
license_code: photo.license_code ?? photo.licenseCode,
}
: undefined,
};
}
static uri = item => ( item && item.default_photo ) && { uri: item.default_photo.url };
static schema = {

View File

@@ -0,0 +1,108 @@
import Taxon from "realmModels/Taxon";
import safeRealmWrite from "sharedHelpers/safeRealmWrite";
describe( "Taxon", ( ) => {
describe( "mapRealmToPojo", ( ) => {
it( "returns the value unchanged when given null or undefined", ( ) => {
expect( Taxon.mapRealmToPojo( null ) ).toBeNull( );
expect( Taxon.mapRealmToPojo( undefined ) ).toBeUndefined( );
} );
describe( "with an already-plain snake_case object", ( ) => {
const apiTaxon = {
id: 745,
name: "Silphium perfoliatum",
rank: "species",
rank_level: 10,
iconic_taxon_name: "Plantae",
ancestor_ids: [1, 2, 3],
preferred_common_name: "Cup Plant",
default_photo: {
id: 7,
url: "https://example.com/photo.jpg",
attribution: "(c) someone",
license_code: "cc-by",
},
};
it( "passes the snake_case fields through unchanged", ( ) => {
const result = Taxon.mapRealmToPojo( apiTaxon );
expect( result.id ).toBe( 745 );
expect( result.name ).toBe( "Silphium perfoliatum" );
expect( result.rank ).toBe( "species" );
expect( result.rank_level ).toBe( 10 );
expect( result.iconic_taxon_name ).toBe( "Plantae" );
expect( result.preferred_common_name ).toBe( "Cup Plant" );
} );
it( "maps the default_photo fields", ( ) => {
const result = Taxon.mapRealmToPojo( apiTaxon );
expect( result.default_photo ).toEqual( {
id: 7,
url: "https://example.com/photo.jpg",
attribution: "(c) someone",
license_code: "cc-by",
} );
} );
it( "returns a detached array copy of ancestor_ids", ( ) => {
const result = Taxon.mapRealmToPojo( apiTaxon );
expect( Array.isArray( result.ancestor_ids ) ).toBe( true );
expect( result.ancestor_ids ).toEqual( [1, 2, 3] );
// a copy, not the same reference we passed in
expect( result.ancestor_ids ).not.toBe( apiTaxon.ancestor_ids );
} );
} );
it( "leaves default_photo undefined when the photo has no url", ( ) => {
const result = Taxon.mapRealmToPojo( {
id: 1,
name: "Life",
default_photo: { id: 9, attribution: "(c) someone" },
} );
expect( result.default_photo ).toBeUndefined( );
} );
describe( "with a live Realm Taxon", ( ) => {
let realmTaxon;
beforeEach( ( ) => {
safeRealmWrite( global.realm, ( ) => {
realmTaxon = global.realm.create( "Taxon", {
id: 123,
name: "Danaus plexippus",
rank: "species",
rank_level: 10,
iconic_taxon_name: "Insecta",
ancestor_ids: [48460, 1, 47120],
// exercise the camelCase mapTo accessors
preferred_common_name: "Monarch",
default_photo: {
id: 555,
url: "https://example.com/monarch.jpg",
attribution: "(c) lepidopterist",
license_code: "cc-by-nc",
},
} );
}, "create Taxon for mapRealmToPojo test" );
} );
afterEach( ( ) => {
safeRealmWrite( global.realm, ( ) => {
global.realm.delete( realmTaxon );
}, "delete Taxon for mapRealmToPojo test" );
} );
it( "returns a detached plain object, not a live Realm object", ( ) => {
const result = Taxon.mapRealmToPojo( realmTaxon );
// a plain object literal, not a live Realm.Object the database can invalidate
expect( Object.getPrototypeOf( result ) ).toBe( Object.prototype );
expect( result.id ).toBe( 123 );
expect( result.name ).toBe( "Danaus plexippus" );
expect( result.rank ).toBe( "species" );
expect( result.rank_level ).toBe( 10 );
expect( result.iconic_taxon_name ).toBe( "Insecta" );
} );
} );
} );
} );