forked from balderdashy/sails-adapter-boilerplate
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
261 lines (223 loc) · 7.73 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
/**
* Module Dependencies
*/
var util = require('util');
var ldap = require('ldapjs');
var _ = require('lodash');
var Promise = require('bluebird');
/**
* Sails Boilerplate Adapter
*
* Most of the methods below are optional.
*
* If you don't need / can't get to every method, just implement
* what you have time for. The other methods will only fail if
* you try to call them!
*
* For many adapters, this file is all you need. For very complex adapters, you may need more flexiblity.
* In any case, it's probably a good idea to start with one file and refactor only if necessary.
* If you do go that route, it's conventional in Node to create a `./lib` directory for your private submodules
* and load them at the top of the file with other dependencies. e.g. var update = `require('./lib/update')`;
*/
module.exports = (function () {
// You'll want to maintain a reference to each collection
// (aka model) that gets registered with this adapter.
var _collectionReferences = {};
var _connections = {};
var _generateFilter = function (where) {
var filters = _.map(where, _parseFilter);
var filter = undefined
if (filters.length === 1) {
filter = filters.pop();
}
else {
filter = util.format('(&%s)', filters.join(''));
}
return filter;
}
var _parseFilter = function (value, key) {
if (_.isArray(value)) {
return util.format('(|%s)', value.map(function (i) {
return _parseFilter(i, key);
}).join(''));
}
if (key == 'or') {
return util.format('(|%s)', _generateFilter(value));
}
if (_.isObject(value)) {
//parse modifier !/not/like/contains/startsWith/endsWith
var filters = _.map(value, function (v, k) {
if (k == '!' || k == 'not') {
return util.format('(!%s)', _parseFilter(v, key));
}
else if (k == 'like') {
return _parseFilter(v.replace(/%/g, '*'), key);
}
else if (k == 'contains') {
return _parseFilter(util.format('*%s*', v), key);
}
else if (k == 'startsWith') {
return _parseFilter(util.format('%s*', v), key);
}
else if (k == 'endsWith') {
return _parseFilter(util.format('*%s', v), key);
}
});
if (filters.length === 1) {
return filters.pop();
}
return util.format('(&%s)', filters.join(''));
}
return util.format('(%s=%s)', key, value);
}
//TODO: Write This Methods since the connection pool doesn't work w/paging
var _LDAPConnect = function (opts) {
return new Promise(function (resolve, reject) {
try {
var client = ldap.createClient(opts.client);
client.on('connect', function () {
if (opts.bind) {
//TODO: Find a nice way to pass through bind.controls
// (without breaking if bind.controls is undefined)
client.bind(opts.bind.dn, opts.bind.password, function (e) {
if (e) return reject(e);
resolve(client);
});
}
else {
resolve(client);
}
});
client.on('connectError', function (e) {
e.message = e.message + '. There was a problem with the LDAP adapter config.';
reject(e);
});
}
catch (e) {
//TODO: this doesn't catch everything because LDAP throws something asynchronously somewhere slightly unusual
e.message = e.message + '. There was a problem with the LDAP adapter config.';
return reject(e);
}
});
};
var _LDAPUnbind = function (opts) {
return new Promise(function (resolve, reject) {
opts.unbind(function (e) {
if (e) return reject(e);
resolve();
});
})
}
var _LDAPSearch = function (opts) {
return new Promise(function (resolve, reject) {
var client = opts.client;
var base = opts.base;
var options = opts.options;
var page = opts.page;
var entries = [];
client.search(base, options, function (err, res) {
if (err) return reject(err);
res.on('searchEntry', function (entry) {
return entries.push(entry);
});
res.on('error', function (err) {
//We don't care about size limits.
if (err.message = 'Size Limit Exceeded') {
return resolve(entries);
}
//in this situation usually the
//'end' event isn't going to fire.
return reject(err);
});
res.on('end', function (result) {
if (result.errorMessage) return reject(result.errorMessage);
return resolve(entries);
});
});
});
};
var adapter = {
// Set to true if this adapter supports (or requires) things like data types, validations, keys, etc.
// If true, the schema for models using this adapter will be automatically synced when the server starts.
// Not terribly relevant if your data store is not SQL/schemaful.
syncable: false,
// Default configuration for collections
// (same effect as if these properties were included at the top level of the model definitions)
defaults: {
client: {
url: 'ldap://my.ldap.server',
maxConnections: 1
}
},
/**
*
* This method runs when a model is initially registered
* at server-start-time. This is the only required method.
*
* @param {[type]} collection [description]
* @param {Function} cb [description]
* @return {[type]} [description]
*/
registerConnection: function(connection, collections, cb) {
// Keep a reference to this collection
if (!connection.identity) return cb(Errors.IdentityMissing);
if (_connections[connection.identity]) return cb(Errors.IdentityDuplicate);
_collectionReferences = collections;
//We can't use connection pools because of LDAP doesn't
//support paging multiple queries on the same connection
_connections[connection.identity] = connection;
return cb();
},
/**
* Fired when a model is unregistered, typically when the server
* is killed. Useful for tearing-down remaining open connections,
* etc.
*
* @param {Function} cb [description]
* @return {[type]} [description]
*/
teardown: function(cb) {
cb && cb();
},
find: function(connection, collectionName, options, cb) {
// If you need to access your private data for this collection:
var collection = _collectionReferences[collectionName];
// Options object is normalized for you:
//
// options.where
// options.limit
// options.skip
// options.sort
// Filter, paginate, and sort records from the datastore.
// You should end up w/ an array of objects as a result.
// If no matches were found, this will be an empty array.
//TODO: should we force people to specify scope sub/base/one in their models ???
_LDAPConnect(_connections[connection]).then(function (connection) {
return Promise.all([
connection,
_LDAPSearch({
client: connection,
base: collection.base,
options: {
scope: 'sub',
filter: _generateFilter(options.where),
attributes: options.select,
paged: true,
sizeLimit: options.limit
}
})
]);
}).spread(function (connection, result) {
return Promise.all([
result,
_LDAPUnbind(connection)
]);
}).spread(function (result) {
//TODO: Support Binary Fields Here Somehow...
cb(null, _.pluck(result, 'object'));
}).catch(cb);
}
};
// Expose adapter definition
return adapter;
})();