1 |
|
2 | (function() {
|
3 | "use strict";
|
4 |
|
5 | var async, getKeys, massage, massageCore, massageOne, massaged, preprocFilter, propagate, tools, _,
|
6 | __slice = [].slice;
|
7 |
|
8 | async = require('async');
|
9 |
|
10 | _ = require('underscore');
|
11 |
|
12 | tools = require('./manikin-tools');
|
13 |
|
14 | preprocFilter = function(filter) {
|
15 | var x;
|
16 | x = _.extend({}, filter);
|
17 | if (x.id) {
|
18 | x._id = x.id;
|
19 | }
|
20 | delete x.id;
|
21 | return x;
|
22 | };
|
23 |
|
24 | massageOne = function(x) {
|
25 | if (!(x != null)) {
|
26 | return x;
|
27 | }
|
28 | x.id = x._id;
|
29 | delete x._id;
|
30 | return x;
|
31 | };
|
32 |
|
33 | massageCore = function(r2) {
|
34 | if (Array.isArray(r2)) {
|
35 | return r2.map(massageOne);
|
36 | } else {
|
37 | return massageOne(r2);
|
38 | }
|
39 | };
|
40 |
|
41 | massage = function(r2) {
|
42 | return massageCore(JSON.parse(JSON.stringify(r2)));
|
43 | };
|
44 |
|
45 | massaged = function(f) {
|
46 | return function(err, data) {
|
47 | if (err) {
|
48 | return f(err);
|
49 | } else {
|
50 | return f(null, massage(data));
|
51 | }
|
52 | };
|
53 | };
|
54 |
|
55 | propagate = function(callback, f) {
|
56 | return function() {
|
57 | var args, err;
|
58 | err = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
|
59 | if (err) {
|
60 | return callback(err);
|
61 | } else {
|
62 | return f.apply(this, args);
|
63 | }
|
64 | };
|
65 | };
|
66 |
|
67 | getKeys = function(data, target, prefix) {
|
68 | var valids;
|
69 | if (target == null) {
|
70 | target = [];
|
71 | }
|
72 | if (prefix == null) {
|
73 | prefix = '';
|
74 | }
|
75 | valids = ['Array', 'String', 'Boolean', 'Date', 'Number', 'Null'];
|
76 | Object.keys(data).forEach(function(key) {
|
77 | if (valids.some(function(x) {
|
78 | return _(data[key])['is' + x]();
|
79 | })) {
|
80 | return target.push(prefix + key);
|
81 | } else {
|
82 | return getKeys(data[key], target, prefix + key + '.');
|
83 | }
|
84 | });
|
85 | return target;
|
86 | };
|
87 |
|
88 | exports.create = function() {
|
89 | var Mixed, ObjectID, ObjectId, Schema, api, defModels, getMeta, insertOps, internalListSub, key, makeModel, metaData, models, mongoose, nullablesValidation, preRemoveCascadeNonNullable, preRemoveCascadeNullable, specTransform, specmodels;
|
90 | if (process.env.NODE_ENV !== 'production') {
|
91 | for (key in require.cache) {
|
92 | delete require.cache[key];
|
93 | }
|
94 | }
|
95 | mongoose = require('mongoose');
|
96 | Schema = mongoose.Schema;
|
97 | Mixed = mongoose.Schema.Types.Mixed;
|
98 | ObjectID = mongoose.mongo.ObjectID;
|
99 | ObjectId = mongoose.Schema.ObjectId;
|
100 | api = {};
|
101 | models = {};
|
102 | specmodels = {};
|
103 | metaData = null;
|
104 | makeModel = function(connection, name, schema) {
|
105 | var ss;
|
106 | ss = new Schema(schema, {
|
107 | strict: true
|
108 | });
|
109 | ss.set('versionKey', false);
|
110 | return connection.model(name, ss, name);
|
111 | };
|
112 | getMeta = function(modelName) {
|
113 | return metaData[modelName];
|
114 | };
|
115 | nullablesValidation = function(schema) {
|
116 | return function(next) {
|
117 | var nonNullOuters, outers, paths, self;
|
118 | self = this;
|
119 | paths = schema.paths;
|
120 | outers = Object.keys(paths).filter(function(x) {
|
121 | return paths[x].options.type === ObjectId && typeof paths[x].options.ref === 'string' && !paths[x].options['x-owner'];
|
122 | }).map(function(x) {
|
123 | return {
|
124 | plur: paths[x].options.ref,
|
125 | sing: x,
|
126 | validation: paths[x].options['x-validation']
|
127 | };
|
128 | });
|
129 | nonNullOuters = outers.filter(function(x) {
|
130 | return self[x.sing] != null;
|
131 | });
|
132 | return async.forEach(nonNullOuters, function(o, callback) {
|
133 | return api.getOne(o.plur, {
|
134 | id: self[o.sing]
|
135 | }, function(err, data) {
|
136 | if (err || !data) {
|
137 | return callback(new Error("Invalid pointer"));
|
138 | } else if (o.validation) {
|
139 | return o.validation(self, data, function(err) {
|
140 | return callback(err ? new Error(err) : void 0);
|
141 | });
|
142 | } else {
|
143 | return callback();
|
144 | }
|
145 | });
|
146 | }, next);
|
147 | };
|
148 | };
|
149 | internalListSub = function(model, outer, id, filter, callback) {
|
150 | var finalFilter;
|
151 | if ((filter[outer] != null) && filter[outer].toString() !== id.toString()) {
|
152 | callback(new Error('No such id'));
|
153 | return;
|
154 | }
|
155 | filter = preprocFilter(filter);
|
156 | finalFilter = _.extend({}, filter, _.object([[outer, id]]));
|
157 | return models[model].find(finalFilter, callback);
|
158 | };
|
159 | preRemoveCascadeNonNullable = function(owner, id, next) {
|
160 | var manys;
|
161 | manys = getMeta(owner.modelName).manyToMany;
|
162 | return async.forEach(manys, function(many, callback) {
|
163 | var obj;
|
164 | obj = _.object([[many.inverseName, id]]);
|
165 | return models[many.ref].update(obj, {
|
166 | $pull: obj
|
167 | }, callback);
|
168 | }, function(err) {
|
169 | var flattenedModels;
|
170 | if (err) {
|
171 | return next(err);
|
172 | }
|
173 | flattenedModels = getMeta(owner.modelName).owns;
|
174 | return async.forEach(flattenedModels, function(mod, callback) {
|
175 | return internalListSub(mod.name, mod.field, id, {}, propagate(callback, function(data) {
|
176 | return async.forEach(data, function(item, callback) {
|
177 | return item.remove(callback);
|
178 | }, callback);
|
179 | }));
|
180 | }, next);
|
181 | });
|
182 | };
|
183 | preRemoveCascadeNullable = function(owner, id, next) {
|
184 | var flattenedModels, ownedModels;
|
185 | ownedModels = Object.keys(models).map(function(modelName) {
|
186 | var paths;
|
187 | paths = models[modelName].schema.paths;
|
188 | return Object.keys(paths).filter(function(x) {
|
189 | return paths[x].options.type === ObjectId && paths[x].options.ref === owner.modelName && !paths[x].options['x-owner'];
|
190 | }).map(function(x) {
|
191 | return {
|
192 | name: modelName,
|
193 | field: x
|
194 | };
|
195 | });
|
196 | });
|
197 | flattenedModels = _.flatten(ownedModels);
|
198 | return async.forEach(flattenedModels, function(mod, callback) {
|
199 | return internalListSub(mod.name, mod.field, id, {}, propagate(callback, function(data) {
|
200 | return async.forEach(data, function(item, callback) {
|
201 | item[mod.field] = null;
|
202 | item.save();
|
203 | return callback();
|
204 | }, callback);
|
205 | }));
|
206 | }, next);
|
207 | };
|
208 | specTransform = function(allspec, modelName, tgt, src, keys) {
|
209 | return keys.forEach(function(key) {
|
210 | if (src[key].type === 'mixed') {
|
211 | return tgt[key] = {
|
212 | type: Mixed
|
213 | };
|
214 | } else if (src[key].type === 'nested') {
|
215 | tgt[key] = {};
|
216 | return specTransform(allspec, modelName, tgt[key], src[key], _.without(Object.keys(src[key]), 'type'));
|
217 | } else if (src[key].type === 'string') {
|
218 | tgt[key] = _.extend({}, src[key], {
|
219 | type: String
|
220 | });
|
221 | if (src[key].validate != null) {
|
222 | return tgt[key].validate = function(value, callback) {
|
223 | return src[key].validate(api, value, callback);
|
224 | };
|
225 | }
|
226 | } else if (src[key].type === 'number') {
|
227 | return tgt[key] = _.extend({}, src[key], {
|
228 | type: Number
|
229 | });
|
230 | } else if (src[key].type === 'date') {
|
231 | return tgt[key] = _.extend({}, src[key], {
|
232 | type: Date
|
233 | });
|
234 | } else if (src[key].type === 'boolean') {
|
235 | return tgt[key] = _.extend({}, src[key], {
|
236 | type: Boolean
|
237 | });
|
238 | } else if (src[key].type === 'hasOne') {
|
239 | return tgt[key] = {
|
240 | ref: src[key].model,
|
241 | 'x-validation': src[key].validation
|
242 | };
|
243 | } else if (src[key].type === 'hasMany') {
|
244 | tgt[key] = [
|
245 | {
|
246 | type: ObjectId,
|
247 | ref: src[key].model,
|
248 | inverseName: src[key].inverseName
|
249 | }
|
250 | ];
|
251 | return allspec[src[key].model][src[key].inverseName] = [
|
252 | {
|
253 | type: ObjectId,
|
254 | ref: modelName,
|
255 | inverseName: key
|
256 | }
|
257 | ];
|
258 | }
|
259 | });
|
260 | };
|
261 | defModels = function(models) {
|
262 | var allspec, newrest, toDef;
|
263 | specmodels = tools.desugar(models);
|
264 | toDef = [];
|
265 | newrest = {};
|
266 | allspec = {};
|
267 | Object.keys(specmodels).forEach(function(modelName) {
|
268 | return allspec[modelName] = {};
|
269 | });
|
270 | Object.keys(specmodels).forEach(function(modelName) {
|
271 | var inspec, owners, spec;
|
272 | spec = allspec[modelName];
|
273 | owners = specmodels[modelName].owners || {};
|
274 | inspec = specmodels[modelName].fields || {};
|
275 | specTransform(allspec, modelName, spec, inspec, Object.keys(inspec));
|
276 | return newrest[modelName] = _.extend({}, specmodels[modelName], {
|
277 | fields: spec
|
278 | });
|
279 | });
|
280 | Object.keys(newrest).forEach(function(modelName) {
|
281 | var conf;
|
282 | conf = newrest[modelName];
|
283 | Object.keys(conf.owners).forEach(function(ownerName) {
|
284 | return conf.fields[ownerName] = {
|
285 | type: ObjectId,
|
286 | ref: conf.owners[ownerName],
|
287 | required: true,
|
288 | 'x-owner': true
|
289 | };
|
290 | });
|
291 | Object.keys(conf.indirectOwners).forEach(function(p) {
|
292 | return conf.fields[p] = {
|
293 | type: ObjectId,
|
294 | ref: conf.indirectOwners[p],
|
295 | required: true,
|
296 | 'x-indirect-owner': true
|
297 | };
|
298 | });
|
299 | Object.keys(conf.fields).forEach(function(fieldName) {
|
300 | if (conf.fields[fieldName].ref != null) {
|
301 | return conf.fields[fieldName].type = ObjectId;
|
302 | }
|
303 | });
|
304 | return toDef.push([modelName, newrest[modelName]]);
|
305 | });
|
306 | metaData = tools.getMeta(specmodels);
|
307 | return toDef;
|
308 | };
|
309 | (function() {
|
310 | var connection;
|
311 | connection = null;
|
312 | api.connect = function(databaseUrl, inputModels, callback) {
|
313 | try {
|
314 | connection = mongoose.createConnection(databaseUrl);
|
315 | defModels(inputModels).forEach(function(_arg) {
|
316 | var name, v;
|
317 | name = _arg[0], v = _arg[1];
|
318 | models[name] = makeModel(connection, name, v.fields);
|
319 | models[name].schema.pre('save', nullablesValidation(models[name].schema));
|
320 | models[name].schema.pre('remove', function(next) {
|
321 | return preRemoveCascadeNonNullable(models[name], this._id.toString(), next);
|
322 | });
|
323 | return models[name].schema.pre('remove', function(next) {
|
324 | return preRemoveCascadeNullable(models[name], this._id.toString(), next);
|
325 | });
|
326 | });
|
327 | } catch (ex) {
|
328 | callback(ex);
|
329 | return;
|
330 | }
|
331 | return callback();
|
332 | };
|
333 | return api.close = function(callback) {
|
334 | connection.close();
|
335 | return callback();
|
336 | };
|
337 | })();
|
338 | api.post = function(model, indata, callback) {
|
339 | var owners, ownersOwners, ownersRaw, saveFunc;
|
340 | saveFunc = function(data) {
|
341 | return new models[model](data).save(function(err) {
|
342 | var fieldMatch, valueMatch;
|
343 | if (err && err.code === 11000) {
|
344 | fieldMatch = err.err.match(/([a-zA-Z]+)_1/);
|
345 | valueMatch = err.err.match(/"([a-zA-Z]+)"/);
|
346 | if (fieldMatch && valueMatch) {
|
347 | return callback(new Error("Duplicate value '" + valueMatch[1] + "' for " + fieldMatch[1]));
|
348 | } else {
|
349 | return callback(new Error("Unique constraint violated"));
|
350 | }
|
351 | } else {
|
352 | return massaged(callback).apply(this, arguments);
|
353 | }
|
354 | });
|
355 | };
|
356 | ownersRaw = getMeta(model).owners;
|
357 | owners = _(ownersRaw).pluck('plur');
|
358 | ownersOwners = _.flatten(owners.map(function(x) {
|
359 | return getMeta(x).owners;
|
360 | }));
|
361 | if (ownersOwners.length === 0) {
|
362 | return saveFunc(indata);
|
363 | } else {
|
364 | return api.getOne(owners[0], {
|
365 | filter: {
|
366 | id: indata[ownersRaw[0].sing]
|
367 | }
|
368 | }, propagate(callback, function(ownerdata) {
|
369 | var metaFields, paths;
|
370 | paths = models[owners[0]].schema.paths;
|
371 | metaFields = Object.keys(paths).filter(function(key) {
|
372 | return !!paths[key].options['x-owner'] || !!paths[key].options['x-indirect-owner'];
|
373 | });
|
374 | metaFields.forEach(function(key) {
|
375 | return indata[key] = ownerdata[key];
|
376 | });
|
377 | return saveFunc(indata);
|
378 | }));
|
379 | }
|
380 | };
|
381 | api.list = function(model, filter, callback) {
|
382 | var defaultSort, rr;
|
383 | filter = preprocFilter(filter);
|
384 | defaultSort = specmodels[model].defaultSort;
|
385 | rr = models[model].find(filter);
|
386 | if (defaultSort != null) {
|
387 | rr = rr.sort(_.object([[defaultSort, 'asc']]));
|
388 | }
|
389 | return rr.exec(massaged(callback));
|
390 | };
|
391 | api.getOne = function(model, config, callback) {
|
392 | var filter;
|
393 | filter = preprocFilter(config.filter || {});
|
394 | return models[model].findOne(filter, function(err, data) {
|
395 | if (err) {
|
396 | if (err.toString() === 'Error: Invalid ObjectId') {
|
397 | callback(new Error('No such id'));
|
398 | } else {
|
399 | callback(err);
|
400 | }
|
401 | } else if (!(data != null)) {
|
402 | return callback(new Error('No match'));
|
403 | } else {
|
404 | return callback(null, massage(data));
|
405 | }
|
406 | });
|
407 | };
|
408 | api.delOne = function(model, filter, callback) {
|
409 | filter = preprocFilter(filter);
|
410 | return models[model].findOne(filter, function(err, d) {
|
411 | if (err) {
|
412 | if (err.toString() === 'Error: Invalid ObjectId') {
|
413 | return callback(new Error('No such id'));
|
414 | } else {
|
415 | return callback(err);
|
416 | }
|
417 | } else if (!(d != null)) {
|
418 | return callback(new Error('No such id'));
|
419 | } else {
|
420 | return d.remove(function(err) {
|
421 | return callback(err, !err ? massage(d) : void 0);
|
422 | });
|
423 | }
|
424 | });
|
425 | };
|
426 | api.putOne = function(modelName, data, filter, callback) {
|
427 | var inputFields, inputFieldsValid, invalidFields, model, validField;
|
428 | filter = preprocFilter(filter);
|
429 | model = models[modelName];
|
430 | inputFieldsValid = getKeys(data);
|
431 | inputFields = Object.keys(data);
|
432 | validField = Object.keys(model.schema.paths);
|
433 | invalidFields = _.difference(inputFieldsValid, validField);
|
434 | if (invalidFields.length > 0) {
|
435 | callback(new Error("Invalid fields: " + invalidFields.join(', ')));
|
436 | return;
|
437 | }
|
438 | return model.findOne(filter, function(err, d) {
|
439 | if (err != null) {
|
440 | if (err.message === 'Invalid ObjectId') {
|
441 | callback(new Error("No such id"));
|
442 | } else {
|
443 | callback(err);
|
444 | }
|
445 | return;
|
446 | }
|
447 | if (!(d != null)) {
|
448 | callback(new Error("No such id"));
|
449 | return;
|
450 | }
|
451 | inputFields.forEach(function(key) {
|
452 | return d[key] = data[key];
|
453 | });
|
454 | return d.save(function(err) {
|
455 | return callback(err, err ? null : massage(d));
|
456 | });
|
457 | });
|
458 | };
|
459 | api.delMany = function(primaryModel, primaryId, propertyName, secondaryId, callback) {
|
460 | var inverseName, mm, secondaryModel;
|
461 | mm = getMeta(primaryModel).manyToMany.filter(function(x) {
|
462 | return x.name === propertyName;
|
463 | })[0];
|
464 | if (!(mm != null)) {
|
465 | callback(new Error('Invalid many-to-many property'));
|
466 | return;
|
467 | }
|
468 | secondaryModel = mm.ref;
|
469 | inverseName = mm.inverseName;
|
470 | return async.forEach([
|
471 | {
|
472 | model: primaryModel,
|
473 | id: primaryId,
|
474 | property: propertyName,
|
475 | secondaryId: secondaryId
|
476 | }, {
|
477 | model: secondaryModel,
|
478 | id: secondaryId,
|
479 | property: inverseName,
|
480 | secondaryId: primaryId
|
481 | }
|
482 | ], function(item, callback) {
|
483 | return models[item.model].findById(item.id, propagate(callback, function(data) {
|
484 | var conditions, options, update;
|
485 | conditions = {
|
486 | _id: item.id
|
487 | };
|
488 | update = {
|
489 | $pull: _.object([[item.property, item.secondaryId]])
|
490 | };
|
491 | options = {};
|
492 | return models[item.model].update(conditions, update, options, function(err, numAffected) {
|
493 | return callback(err);
|
494 | });
|
495 | }));
|
496 | }, callback);
|
497 | };
|
498 | insertOps = [];
|
499 | api.postMany = function(primaryModel, primaryId, propertyName, secondaryId, callback) {
|
500 | var hasAlready, insertOpMatch, insertOpNow, inverseName, mm, secondaryModel;
|
501 | mm = getMeta(primaryModel).manyToMany.filter(function(x) {
|
502 | return x.name === propertyName;
|
503 | })[0];
|
504 | if (!(mm != null)) {
|
505 | callback(new Error('Invalid many-to-many property'));
|
506 | return;
|
507 | }
|
508 | secondaryModel = mm.ref;
|
509 | inverseName = mm.inverseName;
|
510 | insertOpNow = [
|
511 | {
|
512 | primaryModel: primaryModel,
|
513 | primaryId: primaryId,
|
514 | propertyName: propertyName,
|
515 | secondaryId: secondaryId
|
516 | }, {
|
517 | primaryModel: secondaryModel,
|
518 | primaryId: secondaryId,
|
519 | propertyName: inverseName,
|
520 | secondaryId: primaryId
|
521 | }
|
522 | ];
|
523 | insertOpMatch = function(x1, x2) {
|
524 | return x1.primaryModel === x2.primaryModel && x1.primaryId === x2.primaryId && x1.propertyName === x2.propertyName && x1.secondaryId === x2.secondaryId;
|
525 | };
|
526 | hasAlready = insertOps.some(function(x) {
|
527 | return insertOpNow.some(function(y) {
|
528 | return insertOpMatch(x, y);
|
529 | });
|
530 | });
|
531 | if (hasAlready) {
|
532 | callback(null, {
|
533 | status: 'insert already in progress'
|
534 | });
|
535 | return;
|
536 | }
|
537 | insertOpNow.forEach(function(op) {
|
538 | return insertOps.push(op);
|
539 | });
|
540 | return async.map(insertOpNow, function(item, callback) {
|
541 | return models[item.primaryModel].findById(item.primaryId, callback);
|
542 | }, propagate(callback, function(datas) {
|
543 | var updated;
|
544 | updated = [false, false];
|
545 | insertOpNow.forEach(function(conf, i) {
|
546 | if (-1 === datas[i][conf.propertyName].indexOf(conf.secondaryId)) {
|
547 | datas[i][conf.propertyName].push(conf.secondaryId);
|
548 | return updated[i] = true;
|
549 | }
|
550 | });
|
551 | return async.forEach([0, 1], function(index, callback) {
|
552 | if (updated[index]) {
|
553 | return datas[index].save(callback);
|
554 | } else {
|
555 | return callback();
|
556 | }
|
557 | }, function(err) {
|
558 | if (err) {
|
559 | return callback(err);
|
560 | }
|
561 | insertOps = insertOps.filter(function(x) {
|
562 | return !_(insertOpNow).contains(x);
|
563 | });
|
564 | return callback(null, {
|
565 | status: (updated.some(function(x) {
|
566 | return x;
|
567 | }) ? 'inserted' : 'already inserted')
|
568 | });
|
569 | });
|
570 | }));
|
571 | };
|
572 | api.getMany = function(primaryModel, primaryId, propertyName, callback) {
|
573 | return models[primaryModel].findOne({
|
574 | _id: primaryId
|
575 | }).populate(propertyName).exec(function(err, story) {
|
576 | return callback(err, massage(story[propertyName]));
|
577 | });
|
578 | };
|
579 | return api;
|
580 | };
|
581 |
|
582 | }).call(this);
|