1 | 1 | "use strict"; |
2 | 1 | var _ = { |
3 | | clone: require("lodash.clone"), |
4 | | omit: require("lodash.omit") |
5 | | }; |
6 | 1 | var async = require("async"); |
7 | 1 | var debug = require("/home/pmcnr/work/jsonapi-store-mongodb/lib/./debugging"); |
8 | 1 | var mongodb = require("mongodb"); |
9 | | |
10 | | |
11 | 1 | var MongoStore = module.exports = function MongoStore(config) { |
12 | 5 | this._config = config; |
13 | | }; |
14 | | |
15 | | |
16 | | /** |
17 | | Handlers readiness status. This should be set to `true` once all handlers are ready to process requests. |
18 | | */ |
19 | 1 | MongoStore.prototype.ready = false; |
20 | | |
21 | | |
22 | 1 | MongoStore._mongoUuid = function(uuid) { |
23 | 123 | return new mongodb.Binary(uuid, mongodb.Binary.SUBTYPE_UUID); |
24 | | }; |
25 | | |
26 | | |
27 | 1 | MongoStore._isRelationshipAttribute = function(attribute) { |
28 | 24 | return attribute._settings && (attribute._settings.__one || attribute._settings.__many); |
29 | | }; |
30 | | |
31 | | |
32 | 1 | MongoStore._toMongoDocument = function(resource) { |
33 | 17 | var document = _.clone(resource, true); |
34 | 17 | document._id = MongoStore._mongoUuid(document.id); |
35 | 17 | return document; |
36 | | }; |
37 | | |
38 | | |
39 | 1 | MongoStore._getRelationshipAttributeNames = function(attributes) { |
40 | 5 | var attributeNames = Object.getOwnPropertyNames(attributes); |
41 | 5 | var relationshipAttributeNames = attributeNames.reduce(function(partialAttributeNames, name) { |
42 | 24 | var attribute = attributes[name]; |
43 | 24 | if (MongoStore._isRelationshipAttribute(attribute)) { |
44 | 11 | return partialAttributeNames.concat(name); |
45 | | } |
46 | 13 | return partialAttributeNames; |
47 | | }, []); |
48 | 5 | return relationshipAttributeNames; |
49 | | }; |
50 | | |
51 | | |
52 | 1 | MongoStore._getSearchCriteria = function(relationships) { |
53 | 44 | if (!relationships) return {}; |
54 | 12 | var relationshipNames = Object.getOwnPropertyNames(relationships); |
55 | 12 | var criteria = relationshipNames.reduce(function(partialCriteria, relationshipName) { |
56 | 12 | var relationshipId = relationships[relationshipName]; |
57 | 12 | partialCriteria[relationshipName + ".id"] = relationshipId; |
58 | 12 | return partialCriteria; |
59 | | }, {}); |
60 | 12 | return criteria; |
61 | | }; |
62 | | |
63 | | |
64 | 1 | MongoStore._notFoundError = function(type, id) { |
65 | 9 | return { |
66 | | status: "404", |
67 | | code: "ENOTFOUND", |
68 | | title: "Requested resource does not exist", |
69 | | detail: "There is no " + type + " with id " + id |
70 | | }; |
71 | | }; |
72 | | |
73 | | |
74 | 1 | MongoStore.prototype._createIndexesForRelationships = function(collection, relationshipAttributeNames) { |
75 | 5 | var index = relationshipAttributeNames.reduce(function(partialIndex, name) { |
76 | 11 | partialIndex[name + ".id"] = 1; |
77 | 11 | return partialIndex; |
78 | | }, {}); |
79 | 5 | collection.createIndex(index); |
80 | | }; |
81 | | |
82 | | |
83 | | /** |
84 | | Initialise gets invoked once for each resource that uses this handler. |
85 | | */ |
86 | 1 | MongoStore.prototype.initialise = function(resourceConfig) { |
87 | 5 | var self = this; |
88 | 5 | if (!self._config.url) { |
89 | 0 | return console.error("MongoDB url missing from configuration"); |
90 | | } |
91 | 5 | self.resourceConfig = resourceConfig; |
92 | 5 | self.relationshipAttributeNames = MongoStore._getRelationshipAttributeNames(resourceConfig.attributes); |
93 | 5 | mongodb.MongoClient.connect(self._config.url).then(function(db) { |
94 | 5 | self._db = db; |
95 | | }).catch(function(err) { |
96 | 0 | return console.error("error connecting to MongoDB:", err.message); |
97 | | }).then(function() { |
98 | 5 | var resourceName = resourceConfig.resource; |
99 | 5 | debug("initialising resource [" + resourceName + "]"); |
100 | 5 | var collection = self._db.collection(resourceName); |
101 | 5 | self._createIndexesForRelationships(collection, self.relationshipAttributeNames); |
102 | 5 | self.ready = true; |
103 | | }); |
104 | | }; |
105 | | |
106 | | |
107 | | /** |
108 | | Drops the database if it already exists and populates it with example documents. |
109 | | */ |
110 | 1 | MongoStore.prototype.populate = function(callback) { |
111 | 5 | var self = this; |
112 | 5 | self._db.dropDatabase(function(err) { |
113 | 5 | if (err) return console.error("error dropping database"); |
114 | 5 | async.each(self.resourceConfig.examples, function(document, cb) { |
115 | 16 | self.create({ params: {} }, document, cb); |
116 | | }, function(error) { |
117 | 5 | if (error) console.error("error creating example document:", error); |
118 | 5 | return callback(); |
119 | | }); |
120 | | }); |
121 | | }; |
122 | | |
123 | | |
124 | | /** |
125 | | Search for a list of resources, give a resource type. |
126 | | */ |
127 | 1 | MongoStore.prototype.search = function(request, callback) { |
128 | 28 | var collection = this._db.collection(request.params.type); |
129 | 28 | debug("relationships> " + JSON.stringify(request.params.relationships, null, 2)); |
130 | 28 | var criteria = MongoStore._getSearchCriteria(request.params.relationships); |
131 | 28 | debug("criteria> " + JSON.stringify(criteria, null, 2)); |
132 | 28 | collection.find(criteria, { _id: 0 }).toArray(callback); |
133 | | }; |
134 | | |
135 | | |
136 | | /** |
137 | | Find a specific resource, given a resource type and and id. |
138 | | */ |
139 | 1 | MongoStore.prototype.find = function(request, callback) { |
140 | 96 | var collection = this._db.collection(request.params.type); |
141 | 96 | var documentId = MongoStore._mongoUuid(request.params.id); |
142 | 96 | collection.findOne({ _id: documentId }, { _id: 0 }, function(err, result) { |
143 | 96 | if (err || !result) { |
144 | 6 | return callback(MongoStore._notFoundError(request.params.type, request.params.id)); |
145 | | } |
146 | 90 | return callback(null, result); |
147 | | }); |
148 | | }; |
149 | | |
150 | | |
151 | | /** |
152 | | Create (store) a new resource give a resource type and an object. |
153 | | */ |
154 | 1 | MongoStore.prototype.create = function(request, newResource, callback) { |
155 | 17 | var collection = this._db.collection(newResource.type); |
156 | 17 | var document = MongoStore._toMongoDocument(newResource); |
157 | 17 | collection.insertOne(document, function(err) { |
158 | 17 | if (err) return callback(err); |
159 | 17 | collection.findOne(document, { _id: 0 }, callback); |
160 | | }); |
161 | | }; |
162 | | |
163 | | |
164 | | /** |
165 | | Delete a resource, given a resource type and an id. |
166 | | */ |
167 | 1 | MongoStore.prototype.delete = function(request, callback) { |
168 | 2 | var collection = this._db.collection(request.params.type); |
169 | 2 | var documentId = MongoStore._mongoUuid(request.params.id); |
170 | 2 | collection.deleteOne({ _id: documentId }, function(err, result) { |
171 | 2 | if (err) return callback(err); |
172 | 2 | if (result.deletedCount === 0) { |
173 | 1 | return callback(MongoStore._notFoundError(request.params.type, request.params.id)); |
174 | | } |
175 | 1 | return callback(err, result); |
176 | | }); |
177 | | }; |
178 | | |
179 | | |
180 | | /** |
181 | | Update a resource, given a resource type and id, along with a partialResource. |
182 | | partialResource contains a subset of changes that need to be merged over the original. |
183 | | */ |
184 | 1 | MongoStore.prototype.update = function(request, partialResource, callback) { |
185 | 8 | var collection = this._db.collection(request.params.type); |
186 | 8 | var documentId = MongoStore._mongoUuid(request.params.id); |
187 | 48 | var partialDocument = _.omit(partialResource, function(value) { return value === undefined; }); |
188 | 8 | debug("partialDocument> " + JSON.stringify(partialDocument, null, 2)); |
189 | 8 | collection.findOneAndUpdate({ |
190 | | _id: documentId |
191 | | }, { |
192 | | $set: partialDocument |
193 | | }, { |
194 | | returnOriginal: false, |
195 | | projection: { _id: 0 } |
196 | | }, function(err, result) { |
197 | 8 | debug("err>", JSON.stringify(err, null, 2)); |
198 | 8 | debug("result>", JSON.stringify(result, null, 2)); |
199 | 8 | if (err) return callback(err); |
200 | 8 | if (!result || !result.value) { |
201 | 2 | return callback(MongoStore._notFoundError(request.params.type, request.params.id)); |
202 | | } |
203 | 6 | return callback(null, result.value); |
204 | | }); |
205 | | }; |
206 | | |