webapi-client
A RETS (Real Estate Transaction Standard) client for Node.js based on the webapi standards.
this library implements webapi the same way as the rets-client
This allows you to support both libraries will little changes.
It is not a fully implemented library and does not support all features of the rets-client
Implementation Notes
This interface uses promises, and an optional stream-based interface for better performance with large search results.
Future development will include an optional stream-based interface for object downloads, and an improved API for the
non-streaming object methods.
This library is written primarily in Typescript. Promises in this module are provided by Bluebird.
The original module was developed against a server running webapi v1.0.3, so there may be incompatibilities with other
versions. However, we want this library to work against any webapi servers that are in current use, so issue tickets
describing problems or (even better) pull requests that fix interactions with servers running other versions of webapi
are welcomed.
For more information about what all the parameters and return values and such mean, you might want to look at the
WEBAPI Specifications
Contributions
Issue tickets and pull requests are welcome. Pull requests must be backward-compatible to be considered, and ideally
should match existing code style.
TODO
- create unit tests -- specifically ones that run off example WEBAPI data rather than requiring access to a real WEBAPI server
- refactor streaming API to correctly respond to backpressure
Example Usage
Client Configuration
var clientSettings = {
apiUrl: webApiUrl,
auth: {
loginUrl: webApiLoginUrl,
username: webApiUser,
password: webApiPassword,
strategy: 'oauth2',
scope: 'api',
grant_type: 'client_credentials'
}
};
...
Output helper used in many examples below
function outputFields(obj, opts) {
if (!obj) {
console.log(" "+JSON.stringify(obj))
} else {
if (!opts) opts = {};
var excludeFields;
var loopFields;
if (opts.exclude) {
excludeFields = opts.exclude;
loopFields = Object.keys(obj);
} else if (opts.fields) {
loopFields = opts.fields;
excludeFields = [];
} else {
loopFields = Object.keys(obj);
excludeFields = [];
}
for (var i = 0; i < loopFields.length; i++) {
if (excludeFields.indexOf(loopFields[i]) != -1) {
continue;
}
if (typeof(obj[loopFields[i]]) == 'object') {
console.log(" " + loopFields[i] + ": " + JSON.stringify(obj[loopFields[i]], null, 2).replace(/\n/g, '\n '));
} else {
console.log(" " + loopFields[i] + ": " + JSON.stringify(obj[loopFields[i]]));
}
}
}
console.log("");
}
Example webapi-client code
var rets = require('webapi-client');
var fs = require('fs');
var photoSourceId = '12345';
webapi.getAutoLogoutClient(clientSettings, function (client) {
console.log("===================================");
console.log("======== System Metadata ========");
console.log("===================================");
console.log(' ~~~~~~~~~ Header Info ~~~~~~~~~');
outputFields(client.loginHeaderInfo);
console.log(' ~~~~~~~~~ System Data ~~~~~~~~~');
outputFields(client.systemData);
return client.metadata.getResources()
.then(function (data) {
console.log("======================================");
console.log("======== Resources Metadata ========");
console.log("======================================");
console.log(' ~~~~~~ Resources Metadata ~~~~~');
outputFields(data.results[0].info);
for (var dataItem = 0; dataItem < data.results[0].metadata.length; dataItem++) {
console.log(" -------- Resource " + dataItem + " --------");
outputFields(data.results[0].metadata[dataItem], {fields: ['ResourceID', 'StandardName', 'VisibleName', 'ObjectVersion']});
}
}).then(function () {
return client.metadata.getClass("Property");
}).then(function (data) {
console.log("===========================================================");
console.log("======== Class Metadata (from Property Resource) ========");
console.log("===========================================================");
console.log(' ~~~~~~~~ Class Metadata ~~~~~~~');
outputFields(data.results[0].info);
for (var classItem = 0; classItem < data.results[0].metadata.length; classItem++) {
console.log(" -------- Table " + classItem + " --------");
outputFields(data.results[0].metadata[classItem], {fields: ['ClassName', 'StandardName', 'VisibleName', 'TableVersion']});
}
}).then(function () {
return client.metadata.getTable("Property", "RESIDENTIAL");
}).then(function (data) {
console.log("==============================================");
console.log("======== Residential Table Metadata ========");
console.log("===============================================");
console.log(' ~~~~~~~~ Table Metadata ~~~~~~~');
outputFields(data.results[0].info);
for (var tableItem = 0; tableItem < data.results[0].metadata.length; tableItem++) {
console.log(" -------- Field " + tableItem + " --------");
outputFields(data.results[0].metadata[tableItem], {fields: ['MetadataEntryID', 'SystemName', 'ShortName', 'LongName', 'DataType']});
}
return data.results[0].metadata
}).then(function (fieldsData) {
var plucked = [];
for (var fieldItem = 0; fieldItem < fieldsData.length; fieldItem++) {
plucked.push(fieldsData[fieldItem].SystemName);
}
return plucked;
}).then(function (fields) {
return client.search.query("Property", "RESIDENTIAL", "(RecordModDate=2016-06-20+),(ActiveYN=1)", {limit:100, offset:10})
.then(function (searchData) {
console.log("=============================================");
console.log("======== Residential Query Results ========");
console.log("=============================================");
console.log(' ~~~~~~~~~~ Query Info ~~~~~~~~~');
for (var dataItem = 0; dataItem < searchData.results.length; dataItem++) {
console.log(" -------- Result " + dataItem + " --------");
outputFields(searchData.results[dataItem], {fields: fields});
}
if (searchData.maxRowsExceeded) {
console.log(" -------- More rows available!");
}
});
});
}).catch(function (errorInfo) {
var error = errorInfo.error || errorInfo;
console.log(" ERROR: issue encountered:");
outputFields(error);
console.log(' '+(error.stack||error).replace(/\n/g, '\n '));
});
Simple streaming example
var webapi = require('webapi-client');
var through2 = require('through2');
var Promise = require('bluebird');
function doAsyncProcessing(row, index, callback) {
console.log("-------- Result " + index + " --------");
outputFields(row);
callback();
}
webapi.getAutoLogoutClient(clientSettings, function (client) {
return new Promise(function (resolve, reject) {
console.log("====================================");
console.log("======== Streamed Results ========");
console.log("====================================");
var count = 0;
var streamResult = client.search.stream.query("Property", "RES", "(LastChangeTimestamp=2016-06-20+)", {limit:10, offset:4});
var processorStream = through2.obj(function (event, encoding, callback) {
switch (event.type) {
case 'data':
count++;
doAsyncProcessing(event.payload, count, callback);
break;
case 'done':
resolve(event.payload.rowsReceived);
callback();
break;
case 'error':
console.log('Error streaming RETS results: '+event.payload);
streamResult.retsStream.unpipe(processorStream);
processorStream.end();
reject(event.payload);
callback();
break;
default:
callback();
}
});
streamResult.retsStream.pipe(processorStream);
});
}).catch(function (errorInfo) {
var error = errorInfo.error || errorInfo;
console.log(" ERROR: issue encountered:");
outputFields(error);
console.log(' '+(error.stack||error).replace(/\n/g, '\n '));
});
Errors
There are 6 error classes exposed by this module:
RetsError
: A parent class for all the errors below, to make it more convenient to catch errors from this library.
I've made somewhat of an effort to catch any errors thrown by dependencies of this library and re-throw them as instances
of RetsError, so that any error generated by a call to this library can be detected the same way; if you find an error
coming through that didn't get this treatment, please open a ticket (or better, a PR!) to let me know.RetsParamError
: Used when a required function parameter is missing or has an invalid valueRetsServerError
: Used when the HTTP response indicates an error, such as a "401 Unauthorized" responseRetsReplyError
: Used when the HTTP response is valid, but the XML RETS response indicates an errorRetsProcessingError
: Used when a problem is encountered processing the response from the RETS serverRetsPermissionError
: Used when RETS login is successful, but the account does not have the full permissions expected