UNPKG

12.7 kBJavaScriptView Raw
1// Copyright IBM Corp. 2013,2019. All Rights Reserved.
2// Node module: loopback-datasource-juggler
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6// This test written in mocha+should.js
7'use strict';
8
9/* global getSchema:false */
10const should = require('./init.js');
11
12let db, Model, modelWithDecimalArray, dateArrayModel, numArrayModel;
13
14class NestedClass {
15 constructor(roleName) {
16 this.roleName = roleName;
17 }
18}
19
20describe('datatypes', function() {
21 before(function(done) {
22 db = getSchema();
23 const Nested = db.define('Nested', {});
24 const modelTableSchema = {
25 str: String,
26 date: Date,
27 num: Number,
28 bool: Boolean,
29 list: {type: [String]},
30 arr: Array,
31 nested: Nested,
32 nestedClass: NestedClass,
33 };
34 Model = db.define('Model', modelTableSchema);
35 // 'modelWithDecimalArray' is too long an identifier name for Oracle DB
36 modelWithDecimalArray = db.define('modelWithDecArr', {
37 randomReview: {
38 type: [String],
39 mongodb: {
40 dataType: 'Decimal128',
41 },
42 },
43 });
44 dateArrayModel = db.define('dateArrayModel', {
45 bunchOfDates: [Date],
46 bunchOfOtherDates: {
47 type: [Date],
48 },
49 });
50 numArrayModel = db.define('numArrayModel', {
51 bunchOfNums: [Number],
52 });
53 db.automigrate(['Model', 'modelWithDecArr', 'dateArrayModel', 'numArrayModel'], done);
54 });
55
56 it('should resolve top-level "type" property correctly', function() {
57 const Account = db.define('Account', {
58 type: String,
59 id: String,
60 });
61 Account.definition.properties.type.type.should.equal(String);
62 });
63
64 it('should resolve "type" sub-property correctly', function() {
65 const Account = db.define('Account', {
66 item: {type: {
67 itemname: {type: String},
68 type: {type: String},
69 }},
70 });
71 Account.definition.properties.item.type.should.not.equal(String);
72 });
73 it('should resolve array prop with connector specific metadata', function() {
74 const props = modelWithDecimalArray.definition.properties;
75 props.randomReview.type.should.deepEqual(Array(String));
76 props.randomReview.mongodb.should.deepEqual({dataType: 'Decimal128'});
77 });
78
79 it('should coerce array of dates from string', async () => {
80 const dateVal = new Date('2019-02-21T12:00:00').toISOString();
81 const created = await dateArrayModel.create({
82 bunchOfDates: [dateVal,
83 dateVal,
84 dateVal],
85 bunchOfOtherDates: [dateVal,
86 dateVal,
87 dateVal],
88 });
89 created.bunchOfDates[0].should.be.an.instanceOf(Date);
90 created.bunchOfDates[0].should.deepEqual(new Date(dateVal));
91 created.bunchOfOtherDates[0].should.be.an.instanceOf(Date);
92 created.bunchOfOtherDates[0].should.deepEqual(new Date(dateVal));
93 });
94
95 it('should coerce array of numbers from string', async () => {
96 const dateVal = new Date('2019-02-21T12:00:00').toISOString();
97 const created = await numArrayModel.create({
98 bunchOfNums: ['1',
99 '2',
100 '3'],
101 });
102 created.bunchOfNums[0].should.be.an.instanceOf(Number);
103 created.bunchOfNums[0].should.equal(1);
104 });
105
106 it('should return 400 when property of type array is set to string value',
107 function(done) {
108 const myModel = db.define('myModel', {
109 list: {type: ['object']},
110 });
111
112 myModel.create({list: 'This string will crash the server'}, function(err) {
113 (err.statusCode).should.equal(400);
114 done();
115 });
116 });
117
118 it('should return 400 when property of type array is set to object value',
119 function(done) {
120 const myModel = db.define('myModel', {
121 list: {type: ['object']},
122 });
123
124 myModel.create({list: {key: 'This string will crash the server'}}, function(err) {
125 (err.statusCode).should.equal(400);
126 done();
127 });
128 });
129
130 it('should keep types when get read data from db', function(done) {
131 const d = new Date('2015-01-01T12:00:00');
132 let id;
133
134 Model.create({
135 str: 'hello', date: d, num: '3', bool: 1, list: ['test'], arr: [1, 'str'],
136 }, function(err, m) {
137 should.not.exists(err);
138 should.exist(m && m.id);
139 m.str.should.be.type('string');
140 m.num.should.be.type('number');
141 m.bool.should.be.type('boolean');
142 m.list[0].should.be.equal('test');
143 m.arr[0].should.be.equal(1);
144 m.arr[1].should.be.equal('str');
145 id = m.id;
146 testFind(testAll);
147 });
148
149 function testFind(next) {
150 Model.findById(id, function(err, m) {
151 should.not.exist(err);
152 should.exist(m);
153 m.str.should.be.type('string');
154 m.num.should.be.type('number');
155 m.bool.should.be.type('boolean');
156 m.list[0].should.be.equal('test');
157 m.arr[0].should.be.equal(1);
158 m.arr[1].should.be.equal('str');
159 m.date.should.be.an.instanceOf(Date);
160 m.date.toString().should.equal(d.toString(), 'Time must match');
161 next();
162 });
163 }
164
165 function testAll() {
166 Model.findOne(function(err, m) {
167 should.not.exist(err);
168 should.exist(m);
169 m.str.should.be.type('string');
170 m.num.should.be.type('number');
171 m.bool.should.be.type('boolean');
172 m.date.should.be.an.instanceOf(Date);
173 m.date.toString().should.equal(d.toString(), 'Time must match');
174 done();
175 });
176 }
177 });
178
179 it('should create nested object defined by a class when reading data from db', async () => {
180 const d = new Date('2015-01-01T12:00:00');
181 let id;
182 const created = await Model.create({
183 date: d,
184 list: ['test'],
185 arr: [1, 'str'],
186 nestedClass: new NestedClass('admin'),
187 });
188 created.list.toJSON().should.deepEqual(['test']);
189 created.arr.toJSON().should.deepEqual([1, 'str']);
190 created.date.should.be.an.instanceOf(Date);
191 created.date.toString().should.equal(d.toString(), 'Time must match');
192 created.nestedClass.should.have.property('roleName', 'admin');
193
194 const found = await Model.findById(created.id);
195 should.exist(found);
196 found.list.toJSON().should.deepEqual(['test']);
197 found.arr.toJSON().should.deepEqual([1, 'str']);
198 found.date.should.be.an.instanceOf(Date);
199 found.date.toString().should.equal(d.toString(), 'Time must match');
200 found.nestedClass.should.have.property('roleName', 'admin');
201 });
202
203 it('should respect data types when updating attributes', function(done) {
204 const d = new Date;
205 let id;
206
207 Model.create({
208 str: 'hello', date: d, num: '3', bool: 1}, function(err, m) {
209 should.not.exist(err);
210 should.exist(m && m.id);
211
212 // sanity check initial types
213 m.str.should.be.type('string');
214 m.num.should.be.type('number');
215 m.bool.should.be.type('boolean');
216 id = m.id;
217 testDataInDB(function() {
218 testUpdate(function() {
219 testDataInDB(done);
220 });
221 });
222 });
223
224 function testUpdate(done) {
225 Model.findById(id, function(err, m) {
226 should.not.exist(err);
227 // update using updateAttributes
228 m.updateAttributes({
229 id: m.id, num: 10,
230 }, function(err, m) {
231 should.not.exist(err);
232 m.num.should.be.type('number');
233 done();
234 });
235 });
236 }
237
238 function testDataInDB(done) {
239 // verify that the value stored in the db is still an object
240 function cb(err, data) {
241 should.exist(data);
242 data.num.should.be.type('number');
243 done();
244 }
245
246 if (db.connector.find.length === 4) {
247 db.connector.find(Model.modelName, id, {}, cb);
248 } else {
249 db.connector.find(Model.modelName, id, cb);
250 }
251 }
252 });
253
254 it('should not coerce nested objects into ModelConstructor types', function() {
255 const coerced = Model._coerce({nested: {foo: 'bar'}});
256 coerced.nested.constructor.name.should.equal('Object');
257 });
258
259 it('rejects array value converted to NaN for a required property',
260 function(done) {
261 db = getSchema();
262 Model = db.define('RequiredNumber', {
263 num: {type: Number, required: true},
264 });
265 db.automigrate(['Model'], function() {
266 Model.create({num: [1, 2, 3]}, function(err, inst) {
267 should.exist(err);
268 err.should.have.property('name').equal('ValidationError');
269 done();
270 });
271 });
272 });
273
274 it('handles null data', (done) => {
275 db = getSchema();
276 Model = db.define('HandleNullModel', {
277 data: {type: 'string'},
278 });
279 db.automigrate(['HandleNullModel'], function() {
280 const a = new Model(null);
281 done();
282 });
283 });
284
285 describe('model option persistUndefinedAsNull', function() {
286 let TestModel, isStrict;
287 before(function(done) {
288 db = getSchema();
289 TestModel = db.define(
290 'TestModel',
291 {
292 name: {type: String, required: false},
293 desc: {type: String, required: false},
294 stars: {type: Number, required: false},
295 },
296 {
297 persistUndefinedAsNull: true,
298 },
299 );
300
301 isStrict = TestModel.definition.settings.strict;
302
303 db.automigrate(['TestModel'], done);
304 });
305
306 it('should set missing optional properties to null', function(done) {
307 const EXPECTED = {desc: null, stars: null};
308 TestModel.create({name: 'a-test-name'}, function(err, created) {
309 if (err) return done(err);
310 created.should.have.properties(EXPECTED);
311
312 TestModel.findById(created.id, function(err, found) {
313 if (err) return done(err);
314 found.should.have.properties(EXPECTED);
315 done();
316 });
317 });
318 });
319
320 it('should convert property value undefined to null', function(done) {
321 const EXPECTED = {desc: null, extra: null};
322 const data = {desc: undefined, extra: undefined};
323 if (isStrict) {
324 // SQL-based connectors don't support dynamic properties
325 delete EXPECTED.extra;
326 delete data.extra;
327 }
328 TestModel.create(data, function(err, created) {
329 if (err) return done(err);
330
331 created.should.have.properties(EXPECTED);
332
333 TestModel.findById(created.id, function(err, found) {
334 if (err) return done(err);
335 found.should.have.properties(EXPECTED);
336 done();
337 });
338 });
339 });
340
341 it('should convert undefined to null in the setter', function() {
342 const inst = new TestModel();
343 inst.desc = undefined;
344 inst.should.have.property('desc', null);
345 inst.toObject().should.have.property('desc', null);
346 });
347
348 it('should use null in unsetAttribute()', function() {
349 const inst = new TestModel();
350 inst.unsetAttribute('stars');
351 inst.should.have.property('stars', null);
352 inst.toObject().should.have.property('stars', null);
353 });
354
355 it('should convert undefined to null on save', function(done) {
356 const EXPECTED = {desc: null, stars: null, extra: null, dx: null};
357 if (isStrict) {
358 // SQL-based connectors don't support dynamic properties
359 delete EXPECTED.extra;
360 delete EXPECTED.dx;
361 }
362
363 TestModel.create({}, function(err, created) {
364 if (err) return done(err);
365 created.desc = undefined; // Note: this is may be a no-op
366 created.unsetAttribute('stars');
367 created.extra = undefined;
368 created.__data.dx = undefined;
369
370 created.save(function(err, saved) {
371 if (err) return done(err);
372
373 created.should.have.properties(EXPECTED);
374 saved.should.have.properties(EXPECTED);
375
376 function cb(err, found) {
377 if (err) return done(err);
378 should.exist(found[0]);
379 found[0].should.have.properties(EXPECTED);
380 done();
381 }
382
383 if (TestModel.dataSource.connector.all.length === 4) {
384 TestModel.dataSource.connector.all(
385 TestModel.modelName,
386 {where: {id: created.id}},
387 {},
388 cb,
389 );
390 } else {
391 TestModel.dataSource.connector.all(
392 TestModel.modelName,
393 {where: {id: created.id}},
394 cb,
395 );
396 }
397 });
398 });
399 });
400
401 it('should convert undefined to null in toObject()', function() {
402 const inst = new TestModel();
403 inst.desc = undefined; // Note: this may be a no-op
404 inst.unsetAttribute('stars');
405 inst.extra = undefined;
406 inst.__data.dx = undefined;
407
408 inst.toObject(false).should.have.properties({
409 desc: null, stars: null, extra: null, dx: null,
410 });
411 });
412 });
413});