Skip to content

Commit

Permalink
refactor: persist edge cases of type casting in integration tests (#202)
Browse files Browse the repository at this point in the history
throw error when encounter invalid integer or date
  • Loading branch information
cyjake authored Oct 19, 2021
1 parent ec223d6 commit 96cbf9e
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 30 deletions.
58 changes: 46 additions & 12 deletions src/data_types.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

const util = require('util');
const invokable = require('./utils/invokable');

/**
Expand Down Expand Up @@ -66,6 +67,15 @@ class DataType {
}
}

/**
* Check if params is instance of DataType or not
* @param {*} params
* @returns {boolean}
*/
static is(params) {
return params instanceof DataType;
}

/**
* cast raw data returned from data packet into js type
*/
Expand Down Expand Up @@ -181,7 +191,10 @@ class INTEGER extends DataType {
}

uncast(value) {
if (typeof value === 'string') return parseInt(value, 10);
const originValue = value;
if (value == null) return value;
if (typeof value === 'string') value = parseInt(value, 10);
if (isNaN(value)) throw new Error(util.format('invalid integer: %s', originValue));
return value;
}
}
Expand Down Expand Up @@ -227,27 +240,31 @@ class DATE extends DataType {
}

uncast(value) {
const originValue = value;

if (value == null) return value;
if (typeof value.toDate === 'function') {
value = value.toDate();
}

// @deprecated
// vaguely standard date formats such as 2021-10-15 15:50:02,548
if (typeof value === 'string' && rDateFormat.test(value)) {
value = new Date(`${value.replace(' ', 'T').replace(',', '.')}Z`);
}

// 1634611135776
// '2021-10-15T08:38:43.877Z'
if (!(value instanceof Date)) value = new Date(value);
if (isNaN(value)) throw new Error(util.format('invalid date: %s', originValue));

const { precision } = this;
if (value instanceof Date && precision < 3) {
if (precision < 3) {
const result = new Date(value);
result.setMilliseconds(result.getMilliseconds() % (10 ** precision));
return result;
}

if (typeof value === 'string') {
// vaguely standard date formats such as 2021-10-15 15:50:02,548
if (rDateFormat.test(value)) {
return new Date(`${value.replace(' ', 'T').replace(',', '.')}Z`);
}
// Date.parse('2021-10-15T08:38:43.877Z')
return new Date(value);
}
return value instanceof Date ? value : new Date(value);
return value;
}
}

Expand All @@ -261,12 +278,29 @@ class DATEONLY extends DataType {
return this.dataType.toUpperCase();
}

cast(value) {
if (value == null) return value;
if (value instanceof Date) return value;
return new Date(value);
}

uncast(value) {
const originValue = value;

if (value == null) return value;
if (typeof value.toDate === 'function') {
value = value.toDate();
}

// @deprecated
// vaguely standard date formats such as 2021-10-15 15:50:02,548
if (typeof value === 'string' && rDateFormat.test(value)) {
value = new Date(`${value.replace(' ', 'T').replace(',', '.')}Z`);
}

if (!(value instanceof Date)) value = new Date(value);
if (isNaN(value)) throw new Error(util.format('invalid date: %s', originValue));;

return new Date(value.getFullYear(), value.getMonth(), value.getDate());
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/drivers/abstract/attribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class Attribute {

// { foo: STRING }
// { foo: STRING(255) }
if (typeof params === 'function' || params instanceof DataTypes) {
if (typeof params === 'function' || DataTypes.is(params)) {
params = { type: params };
}
const type = createType(DataTypes, params);
Expand Down
2 changes: 1 addition & 1 deletion src/expr_formatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ function coerceLiteral(spell, ast) {

if (firstArg.type === 'id') {
const model = findModel(spell, firstArg.qualifiers);
const attribute = model && model.attributeMap[firstArg.value];
const attribute = model && model.attributes[firstArg.value];

if (attribute) {
for (const arg of args.slice(1)) {
Expand Down
143 changes: 134 additions & 9 deletions test/integration/suite/data_types.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
'use strict';

const assert = require('assert').strict;
const strftime = require('strftime');

const { Bone, DataTypes } = require('../../..');
const { INTEGER, STRING, DATE, TEXT, BOOLEAN, JSON, JSONB } = DataTypes;
const { INTEGER, STRING, DATE, DATEONLY, TEXT, BOOLEAN, JSON, JSONB } = DataTypes;


describe('=> Data types', () => {
Expand Down Expand Up @@ -79,11 +80,7 @@ describe('=> Data types - JSON', () => {
}
}

beforeEach(async () => {
Object.defineProperties(Note, {
columns: { value: [], configurable: true },
synchronized: { value: undefined, configurable: true }
});
before(async () => {
await Note.driver.dropTable('notes');
await Note.sync();
});
Expand All @@ -107,8 +104,20 @@ describe('=> Data types - JSON', () => {
});

it('=> type casting', async function() {
await Note.create({ meta: {} });
await Note.create({ meta: [] });
const note = await Note.create({ meta: {} });
assert.deepEqual(note.meta, {});
await note.reload();
assert.deepEqual(note.meta, {});

const note2 = await Note.create({ meta: [] });
assert.deepEqual(note2.meta, []);
await note2.reload();
assert.deepEqual(note2.meta, []);

const note3 = await Note.create({ meta: [ 1, 2, 3 ] });
assert.deepEqual(note3.meta, [ 1, 2, 3 ]);
await note3.reload();
assert.deepEqual(note3.meta, [ 1, 2, 3 ]);
});
});

Expand Down Expand Up @@ -146,7 +155,6 @@ describe('=> Data types - BINARY', () => {
});

it('=> init', async () => {

const metaData = Buffer.from('meta');
const metabData = Buffer.from('metab');
const metacData = Buffer.from('metac');
Expand Down Expand Up @@ -183,3 +191,120 @@ describe('=> Data types - BINARY', () => {
assert.equal(foundNote.metac.toString(), 'metac');
});
});

describe('=> Data Types - INTEGER', function() {
class Note extends Bone {
static attributes = {
word_count: INTEGER,
}
}

before(async function() {
await Note.driver.dropTable('notes');
await Note.sync();
});

it('type casting', async function() {
const note = await Note.create({ word_count: '800' });
await note.reload();
assert.equal(note.word_count, 800);

const note2 = await Note.findOne({ word_count: '800' });
assert.equal(note2.word_count, 800);

const result = await Note.where({ word_count: [ 800, null ] });
assert.equal(result.length, 1);
assert.deepEqual(result.toJSON(), [ note2.toJSON() ]);

await assert.rejects(async function() {
await Note.where({ word_count: [ 'foo' ] });
}, /invalid integer/i);
});
});

describe('=> Data types - DATE', function() {
class Note extends Bone {
static attributes = {
createdAt: DATE(6),
updatedAt: DATE(6),
}
}

before(async function() {
await Note.driver.dropTable('notes');
await Note.sync();
});

it('type casting', async function() {
const date = new Date('2021-10-15T08:38:43.877Z');
const note = await Note.create({ createdAt: date, updatedAt: date });
await note.reload();
assert.equal(note.createdAt.toISOString(), '2021-10-15T08:38:43.877Z');

await assert.doesNotReject(async function() {
const result = await Note.where({ createdAt: date });
assert.equal(result.length, 1);
});

await assert.doesNotReject(async function() {
const result = await Note.where({ createdAt: '2021-10-15 08:38:43,877' });
assert.equal(result.length, 1);
});

// MySQL throws on invalid date string in SELECT, others neglect.
await assert.rejects(async function() {
const result = await Note.where({ createdAt: 'invalid date' });
assert.equal(result.length, 0);
}, /invalid date/i);

// SQLite neglects invalid date string in INSERT, others throw.
await assert.rejects(async function() {
const note2 = await Note.create({ createdAt: 'invalid date' });
await note2.reload();
assert.equal(note2.createdAt, null);
}, /invalid date/i);
});
});

describe('=> Data types - DATEONLY', function() {
class Note extends Bone {
static attributes = {
createdAt: DATEONLY,
}
}

before(async function() {
await Note.driver.dropTable('notes');
await Note.sync();
});

it('type casting', async function() {
const date = new Date('2021-10-15T08:38:43.877Z');
const note = await Note.create({ createdAt: date });
await note.reload();
assert.equal(strftime('%Y-%m-%d', note.createdAt), '2021-10-15');

await assert.doesNotReject(async function() {
const result = await Note.where({ createdAt: date });
assert.equal(result.length, 1);
});

await assert.doesNotReject(async function() {
const result = await Note.where({ createdAt: '2021-10-15 08:38:43,877' });
assert.equal(result.length, 1);
});

// MySQL throws on invalid date string in SELECT, others neglect.
await assert.rejects(async function() {
const result = await Note.where({ createdAt: 'invalid date' });
assert.equal(result.length, 0);
}, /invalid date/i);

// SQLite neglects invalid date string in INSERT, others throw.
await assert.rejects(async function() {
const note2 = await Note.create({ createdAt: 'invalid date' });
await note2.reload();
assert.equal(note2.createdAt, null);
}, /invalid date/i);
});
});
8 changes: 4 additions & 4 deletions test/unit/query_object.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@ describe('=> parseObject', function() {
"`title` = 'Leah' OR `title` LIKE '%Leah%'"
);
assert.equal(
query({ createdAt: { $not: [ '2021-09-30', { $gte: '2021-10-07' } ] } }),
"NOT (`gmt_create` = '2021-09-30' AND `gmt_create` >= '2021-10-07')"
query({ title: { $not: [ 'Leah', { $like: '%Leah%' } ] } }),
"NOT (`title` = 'Leah' AND `title` LIKE '%Leah%')"
);
assert.equal(
query({ createdAt: { $not: { $gt: '2021-01-01', $lte: '2021-12-31' } } }),
"NOT (`gmt_create` > '2021-01-01' AND `gmt_create` <= '2021-12-31')"
query({ title: { $not: { $like: '%Leah%', $gt: 'L' } } }),
"NOT (`title` LIKE '%Leah%' AND `title` > 'L')"
);
});

Expand Down
6 changes: 3 additions & 3 deletions test/unit/spell.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,12 +202,12 @@ describe('=> Spell', function() {
authorId: 2,
},
{
title: { $in: ['sss', 'sss1' ] },
authorId: { $in: ['sss', 'sss1' ] },
title: { $in: [ 'sss', 'sss1' ] },
authorId: { $in: [ 1, 2 ] },
}
],
}).toSqlString(),
"SELECT * FROM `articles` WHERE (`title` LIKE '%Cain%' AND `author_id` = 1 OR `title` IN ('s1', '21') AND `author_id` = 2 OR `title` IN ('s1') AND `author_id` = 2 OR `title` IN ('sss', 'sss1') AND `author_id` IN ('sss', 'sss1')) AND `gmt_deleted` IS NULL"
"SELECT * FROM `articles` WHERE (`title` LIKE '%Cain%' AND `author_id` = 1 OR `title` IN ('s1', '21') AND `author_id` = 2 OR `title` IN ('s1') AND `author_id` = 2 OR `title` IN ('sss', 'sss1') AND `author_id` IN (1, 2)) AND `gmt_deleted` IS NULL"
);
});

Expand Down

0 comments on commit 96cbf9e

Please sign in to comment.