reso-upi-v2

Run Code Checks   CodeQL

Universal Property Identifier (UPI) Version 2

The UPI is a way to take a set of well-known Property identifiers and encode them in a way such that if two parties both have the same data, they can create the identifiers and match on them. This is useful for deduplicating data.

UPIs can also easily be searched on by parts and be used with URIs as part of a browser location or API URL, since they are friendly with existing URI standards.

Uniform Resource Names (URNs)

Uniform Resource Names are a type of Uniform Resource Identifier (URI) that allow globally unique identifiers to be created using a namespace.

They are a general Internet standard from IANA and IETF, which are in many enterprise-scale applications such as AWS and LinkedIn.

RESO has a reserved URN which includes v1 UPI identifiers. See section 3.4.2.

URN Encoding Scheme

In order to avoid collisions with special characters and preserve data passed in the URN, UPIv2 takes the following approach:

The delimiters are the components of the UPI!

What does this mean, in practice?

The UPI includes Country as part of constituent data. This means it has a component called :country: which is also a delimiter. This avoids the need for special delimiters.

Examples

Decode a UPI Payload from an Encoded UPI

Assume we have the following UPI:

urn:reso:upi:2.0:country:US:stateorprovince:CA:county:06037:subcounty::propertytype:Residential:subpropertytype::parcelnumber: [abc] 1-2 :: 3:456 :subparcelnumber:

Let’s look at the individual parts:

This UPI can be decoded into a RESO Common Format payload using the decode function.

// Assume we're calling from the node REPL 
// and in the root of the project directory
> const { decode } = require(".");

> const upi = 'urn:reso:upi:2.0:country:US:stateorprovince:CA:county:06037:subcounty::propertytype:Residential:subpropertytype::parcelnumber: [abc] 1-2 ::   3:456 :subparcelnumber:';

> const upiData = decode({ upi });

> upiData
{
  '@reso.context': 'urn:reso:metadata:2.0:resource:property',
  Country: 'US',
  StateOrProvince: 'CA',
  County: '06037',
  SubCounty: null,
  PropertyType: 'Residential',
  SubPropertyType: null,
  ParcelNumber: ' [abc] 1-2 ::   3:456 ',
  SubParcelNumber: null
}

Note that any item without data in it is null, and that all special characters are preserved in the ParcelNumber. This would be true if special characters were used in any of the values, not just ParcelNumber.

Encode a UPI from a RESO Common Format Payload

Now let’s say we have some standardized RESO data and we want to create a UPI from it.

We can do so using the encode function:

// Assume we're calling from the node REPL 
// and in the root of the project directory
> const { encode } = require(".");

> const upiData = {
  '@reso.context': 'urn:reso:metadata:2.0:resource:property',
  Country: 'US',
  StateOrProvince: 'CA',
  County: '06037',
  SubCounty: null,
  PropertyType: 'Residential',
  SubPropertyType: null,
  ParcelNumber: ' [abc] 1-2 ::   3:456 ',
  SubParcelNumber: null
};

> const upi = encode({upiData});

> upi
'urn:reso:upi:2.0:country:US:stateorprovince:CA:county:06037:subcounty::propertytype:Residential:subpropertytype::parcelnumber: [abc] 1-2 ::   3:456 :subparcelnumber:'

Other Benefits of URNs

UPI Hashes

In the U.S., Parcel Numbers are a matter of public record. However, in other countries / scenarios, there may be some data that cannot be conveyed due to intellectual property concerns or for other reasons.

The matching and deduplication aspects of the UPI still work even when hashed since if the same components and data were used between two records, their hashes would be the same.

As for choice of hashes, since we’re dealing with particularly sensitive data that others wouldn’t want shared, one-way hashing (i.e. cryptographic hashing) is a natural choice since it sufficiently obscures the source data. They’re also NIST and global standards used in large-scale production environments like GitHub, Blockchain and Ethereum, and have support out of the box in programming languages and frameworks.

One-way hashes also offer collision-resistance, which is important for the universality of the UPI.

Example

Let’s assume we have the UPI created in earlier examples:

urn:reso:upi:2.0:country:US:stateorprovince:CA:county:06037:subcounty::propertytype:Residential:subpropertytype::parcelnumber: [abc] 1-2 ::   3:456 :subparcelnumber:

To create a UPI hash from this value, use the hash function:

// Assume we're calling from the node REPL 
// and in the root of the project directory
> const { hash } = require('.'),
  upi = 'urn:reso:upi:2.0:country:US:stateorprovince:CA:county:06037:subcounty::propertytype:Residential:subpropertytype::parcelnumber: [abc] 1-2 ::   3:456 :subparcelnumber:'

> const upiHash = hash(upi);

> upiHash

'urn:reso:upi:2.0:sha3-256-hash:427c883322af677b76d72d43d9a00c3bedd6a1ede20e43c614f710abf85549a9'

Note that the component representing the UPI hash also includes the method that was used for hashing. This seems practical, and offers the ability to support different kinds of hashing, should the need arise.

Installation

As an npm package

To install from GitHub:

npm i RESOStandards/reso-upi-v2

Installing and Running Locally

If you would like to run the project locally, install Node and git if you don’t have them already.

> mkdir [PROJECT_LOCATION]
> cd [PROJECT_LOCATION]
> git clone https://github.com/RESOStandards/reso-upi-v2.git
> cd reso-upi-v2
> npm install
> npm install mocha

To check the installation, run:

> npm run test

For examples of how to use this library, see ./test.

Contributing

If you would like to suggest changes, please open a ticket.

If you have changes to contribute, fork the repo, clone locally, make changes, then make a PR against this repo.