rets-client
A RETS (Real Estate Transaction Standard) client for Node.js.
I haven't had time to work on this for a couple years now, and I no longer work on any projects which use this.
I think it's a good little project, and while there are things I wish I'd refactored, it has a lot to offer.
So if someone would like to start maintianing it, I'm open to figuring out how best to make that work.
Changes
6.0
- Forked from the original project by @zacronos that was left unmaintained
- Updated dependencies
- Improved type definitions
5.2
Added timeout
setting, and allow COMPACT
format.
5.1
Added TypeScript typings.
5.0
A significant amount of internal cleanup, resulting in more consistent code and API. There are some minor breaking
changes, but they're small enough that migrating to 5.0 shouldn't be much effort. The
Example Usage has been updated to show 5.x patterns.
- object stream queries now have events with a
type
field to make discrimination easier, with the following
possibilities:
dataStream
, for a stream containing an object's raw datalocation
, when no stream is available but a URL is available in the headerInfo
(as per the Location: 1
option)headerInfo
, with the headers for the outer multipart responseerror
, for an error corresponding to a single object rather than the stream as a whole
- search stream queries now return an object with a
retsStream
field rather than the bare stream - the
searchRets
method now returns an object with a rawStream
field rather than the bare stream - headerInfo is now available on every query made
- login and logout set
client.loginHeaderInfo
and client.logoutHeaderInfo
- every streaming query will include an event with
type: 'headerInfo'
- every buffered query (and most streaming queries) will return an object including a
headerInfo
field
- errors now consistently include response headers as well as the request options
- all calls made to the RETS server now obey
settings.method
for POST vs GET
4.X and earlier
See the changelog for earlier changes.
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 CoffeeScript, but may be used just as easily in a Node app using Javascript or
CoffeeScript. Promises in this module are provided by Bluebird.
The original module was developed against a server running RETS v1.7.2, so there may be incompatibilities with other
versions. However, we want this library to work against any RETS 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 RETS
are welcomed.
For more information about what all the parameters and return values and such mean, you might want to look at the
RETS 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 RETS data rather than requiring access to a real RETS server
- refactor streaming API to correctly respond to backpressure
Example Usage
Client Configuration
var clientSettings = {
loginUrl: retsLoginUrl,
username: retsUser,
password: retsPassword,
version: 'RETS/1.7.2',
userAgent: 'RETS node-client/4.x',
method: 'GET'
};
...
Client Configuration with UA Authorization
var clientSettings = {
version: 'RETS/1.7.2',
userAgent: userAgent,
userAgentPassword: userAgentPassword,
sessionId: sessionId
};
...
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 rets-client code
var rets = require('rets-client');
var fs = require('fs');
var photoSourceId = '12345';
rets.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(' ~~~~~~~~~ Header Info ~~~~~~~~~');
outputFields(data.headerInfo);
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(' ~~~~~~~~~ Header Info ~~~~~~~~~');
outputFields(data.headerInfo);
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(' ~~~~~~~~~ Header Info ~~~~~~~~~');
outputFields(data.headerInfo);
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(' ~~~~~~~~~ Header Info ~~~~~~~~~');
outputFields(searchData.headerInfo);
console.log(' ~~~~~~~~~~ Query Info ~~~~~~~~~');
outputFields(searchData, {exclude: ['results','headerInfo']});
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!");
}
});
}).then(function () {
return client.objects.getAllObjects("Property", "LargePhoto", photoSourceId, {alwaysGroupObjects: true, ObjectData: '*'})
}).then(function (photoResults) {
console.log("=================================");
console.log("======== Photo Results ========");
console.log("=================================");
console.log(' ~~~~~~~~~ Header Info ~~~~~~~~~');
outputFields(photoResults.headerInfo);
for (var i = 0; i < photoResults.objects.length; i++) {
console.log(" -------- Photo " + (i + 1) + " --------");
if (photoResults.objects[i].error) {
console.log(" Error: " + photoResults.objects[i].error);
} else {
outputFields(photoResults.objects[i].headerInfo);
fs.writeFileSync(
"/tmp/photo" + (i + 1) + "." + photoResults.objects[i].headerInfo.contentType.match(/\w+\/(\w+)/i)[1],
photoResults.objects[i].data);
}
}
});
}).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 rets = require('rets-client');
var through2 = require('through2');
var ;
function doAsyncProcessing(row, index, callback) {
console.log("-------- Result " + index + " --------");
outputFields(row);
callback();
}
rets.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 'headerInfo':
console.log(' ~~~~~~~~~ Header Info ~~~~~~~~~');
outputFields(event.payload);
callback();
break;
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 '));
});
Photo streaming example
var rets = require('rets-client');
var fs = require('fs');
rets.getAutoLogoutClient(clientSettings, function (client) {
var photoIds = {
'11111': [1,3],
'22222': '*',
'33333': '0'
};
return client.objects.stream.getObjects('Property', 'LargePhoto', photoIds, {alwaysGroupObjects: true, ObjectData: '*'})
.then(function (photoStream) {
console.log("========================================");
console.log("======== Photo Stream Results ========");
console.log("========================================");
return new Promise(function (resolve, reject) {
var i=0;
photoStream.objectStream.on('data', function (event) {
try {
if (event.type == 'headerInfo') {
console.log(' ~~~~~~~~~ Header Info ~~~~~~~~~');
outputFields(event.headerInfo);
return
}
console.log(" -------- Photo " + (i + 1) + " --------");
if (event.type == 'error') {
console.log(" Error: " + event.error);
} else if (event.type == 'dataStream') {
outputFields(event.headerInfo);
fileStream = fs.createWriteStream(
"/tmp/photo_" + event.headerInfo.contentId + "_" + event.headerInfo.objectId + "." + event.headerInfo.contentType.match(/\w+\/(\w+)/i)[1]);
event.dataStream.pipe(fileStream);
}
i++;
} catch (err) {
reject(err);
}
});
photoStream.objectStream.on('error', function (errorInfo) {
reject(errorInfo);
});
photoStream.objectStream.on('end', function () {
resolve();
});
});
}).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
Debugging
You can turn on all debug logging by adding rets-client:*
to your DEBUG
environment variable, as per the
debug module. Sub-loggers available:
rets-client:main
: basic logging of RETS call options and errorsrets-client:request
: logging of HTTP request/response headers and other related info, with output almost identical
to that provided by the request-debug module.rets-client:multipart
: logging of most multipart parser eventsrets-client:multipart:verbose
: logging of additional multipart parser events (too cluttered for most purposes)
If you want access to the request debugging data directly, you can use the requestDebugFunction
client setting. This
function will be set up as a debug handler as per the request-debug module.