-
Notifications
You must be signed in to change notification settings - Fork 2
/
index.js
166 lines (136 loc) · 3.7 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
/**
* module dependencies
*/
var mongoose = require('mongoose');
/**
* select fields from an array of string mongoose style
*
* @param {Array} ref reference array
* @param {String} select field to filter from orignial array
* @return {Array} filtered list of fields
*
* @api private
*/
var selectFields = function (ref, select) {
var fields;
if (!select) return ref;
// select is of type -fied1 -field2
if (select[0] === '-') {
var removes = select.split(' ');
fields = ref.slice(0);
removes.forEach(function (remove) {
fields.splice(fields.indexOf(remove.slice(1)), 1);
});
return fields;
} else {
// select is of type fied1 field2
return select.split(' ');
}
};
/**
* @param {Schema} schema
* @param {Object} options
*
* @api public
*/
module.exports = function (schema, options) {
var refmodel = ('string' === typeof options.ref)
? mongoose.model(options.ref)
: options.ref,
modelName = refmodel.modelName,
refschema = refmodel.schema,
root, idPath, pos, sync, fields, rootEl, el = {};
// root path
root = options.path
? options.path + '.'
: '';
// id path
idPath = options.id
? options.id
: root + '_id';
// add virtual id path
if (options.path) {
schema.virtual(options.path + '.id').get(function () {
var _id = this.get(idPath);
if (_id) return _id.toString();
});
}
if (options.pos) pos = options.pos;
// select fields to copy
fields = selectFields(Object.keys(refschema.paths), options.select);
if (!options.id && fields.indexOf('_id') === -1) fields.push('_id');
// append fields to schema
if (options.path) {
rootEl = {};
rootEl[options.path] = el;
} else {
rootEl = el;
}
fields.forEach(function(name) {
var type = refschema.paths[name].options.type;
el[name] = {type: type};
if (name === '_id') el[name].ref = modelName;
});
schema.add(rootEl);
// fetch source and fill on save
schema.pre('save', function (next) {
var id = this.get(idPath),
_this = this;
if (!this.isModified(idPath)) return next();
if (!id) {
fields.forEach(function (field) {
_this.set(root + field, null);
});
return next();
}
refmodel
.findById(id)
.select(fields.join(' '))
.exec(function (err, model) {
if (err) return next(err);
if (!model) return next(new Error(modelName + ' (id:' + id + ') not found'));
fields.forEach(function (field) {
_this.set(root + field, model.get(field));
});
next();
});
});
// get fields to get in sync
sync = selectFields(fields, options.sync);
// update all denormalized references when source is updated
refschema.pre('save', function (next) {
var _this = this,
conditions = {},
updates = {};
// nothing to do for fresh doc
if (this.isNew) return next();
// get updated fields
var changed = sync.filter(function (field) {
return _this.isDirectModified(field);
});
// nothing we care has changed
if (!changed.length) return next();
// build update query
if (pos) {
conditions[pos.replace('.$', '') + '_id'] = this.id;
} else {
conditions[idPath] = this.id;
}
changed.forEach(function (field) {
if (pos) {
updates[pos + field] = _this.get(field);
} else {
updates[root + field] = _this.get(field);
}
});
// trigger updates
mongoose
.model(options.dest)
.update(conditions, updates, {multi: true})
.exec(function (err) {
schema.emit('fill', err, _this);
});
// call next, to save changes
next();
});
};