Skip to content

Commit 80a7917

Browse files
committed
Revert "Revert "Force UTC at process level""
1 parent cf0835c commit 80a7917

19 files changed

+583
-40
lines changed

Gruntfile.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,8 @@ var _ = require('lodash'),
176176
ui: 'bdd',
177177
reporter: grunt.option('reporter') || 'spec',
178178
timeout: '15000',
179-
save: grunt.option('reporter-output')
179+
save: grunt.option('reporter-output'),
180+
require: ['core/server/overrides']
180181
},
181182

182183
// #### All Unit tests

core/server/data/db/connection.js

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
var knex = require('knex'),
2-
config = require('../../config'),
1+
var knex = require('knex'),
2+
config = require('../../config'),
33
dbConfig = config.database,
44
knexInstance;
55

66
function configure(dbConfig) {
77
var client = dbConfig.client,
88
pg;
99

10-
if (client === 'pg' || client === 'postgres' || client === 'postgresql') {
10+
dbConfig.isPostgreSQL = function () {
11+
return client === 'pg' || client === 'postgres' || client === 'postgresql';
12+
};
13+
14+
if (dbConfig.isPostgreSQL()) {
1115
try {
1216
pg = require('pg');
1317
} catch (e) {
@@ -20,12 +24,26 @@ function configure(dbConfig) {
2024
pg.types.setTypeParser(20, function (val) {
2125
return val === null ? null : parseInt(val, 10);
2226
});
27+
28+
// https://github.com/tgriesser/knex/issues/97
29+
// this sets the timezone to UTC only for the connection!
30+
dbConfig.pool = {
31+
afterCreate: function (connection, callback) {
32+
connection.query('set timezone=\'UTC\'', function (err) {
33+
callback(err, connection);
34+
});
35+
}
36+
};
2337
}
2438

2539
if (client === 'sqlite3') {
2640
dbConfig.useNullAsDefault = dbConfig.useNullAsDefault || false;
2741
}
2842

43+
if (client === 'mysql') {
44+
dbConfig.connection.timezone = 'UTC';
45+
}
46+
2947
return dbConfig;
3048
}
3149

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = [];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
var config = require('../../../../config'),
2+
models = require(config.paths.corePath + '/server/models'),
3+
sequence = require(config.paths.corePath + '/server/utils/sequence'),
4+
moment = require('moment'),
5+
_ = require('lodash'),
6+
Promise = require('bluebird'),
7+
messagePrefix = 'Transforming dates to UTC: ',
8+
settingsKey = '006/01',
9+
_private = {};
10+
11+
_private.getTZOffset = function getTZOffset(date) {
12+
return date.getTimezoneOffset();
13+
};
14+
15+
_private.getTZOffsetMax = function getTZOffsetMax() {
16+
return Math.max(Math.abs(new Date('2015-07-01').getTimezoneOffset()), Math.abs(new Date('2015-01-01').getTimezoneOffset()));
17+
};
18+
19+
_private.addOffset = function addOffset(date) {
20+
if (_private.noOffset) {
21+
return moment(date).toDate();
22+
}
23+
24+
return moment(date).add(_private.getTZOffset(date), 'minutes').toDate();
25+
};
26+
27+
/**
28+
* postgres: stores dates with offset, so it's enough to force timezone UTC in the db connection (see data/db/connection.js)
29+
* sqlite: stores UTC timestamps, but we will normalize the format to YYYY-MM-DD HH:mm:ss
30+
*/
31+
module.exports = function transformDatesIntoUTC(options, logger) {
32+
var ServerTimezoneOffset = _private.getTZOffsetMax(),
33+
settingsMigrations = null;
34+
35+
return models.Base.transaction(function (transaction) {
36+
options.transacting = transaction;
37+
38+
// will ensure updated_at fields will not be updated, we take them from the original models
39+
options.importing = true;
40+
options.context = {internal: true};
41+
42+
return sequence([
43+
function databaseCheck() {
44+
if (ServerTimezoneOffset === 0) {
45+
return Promise.reject(new Error('skip'));
46+
}
47+
48+
if (config.database.isPostgreSQL()) {
49+
return Promise.reject(new Error('skip'));
50+
}
51+
52+
if (config.database.client === 'sqlite3') {
53+
_private.noOffset = true;
54+
} else {
55+
_private.noOffset = false;
56+
}
57+
58+
logger.info(messagePrefix + '(could take a while)...');
59+
return Promise.resolve();
60+
},
61+
function checkIfMigrationAlreadyRan() {
62+
return models.Settings.findOne({key: 'migrations'}, options)
63+
.then(function (result) {
64+
try {
65+
settingsMigrations = JSON.parse(result.attributes.value) || {};
66+
} catch (err) {
67+
return Promise.reject(err);
68+
}
69+
70+
// CASE: migration ran already
71+
if (settingsMigrations.hasOwnProperty(settingsKey)) {
72+
return Promise.reject(new Error('skip'));
73+
}
74+
75+
return Promise.resolve();
76+
});
77+
},
78+
function updatePosts() {
79+
return models.Post.findAll(options).then(function (result) {
80+
if (result.models.length === 0) {
81+
logger.warn(messagePrefix + 'No Posts found');
82+
return;
83+
}
84+
85+
return Promise.mapSeries(result.models, function mapper(post) {
86+
if (post.get('published_at')) {
87+
post.set('published_at', _private.addOffset(post.get('published_at')));
88+
}
89+
90+
if (post.get('updated_at')) {
91+
post.set('updated_at', _private.addOffset(post.get('updated_at')));
92+
}
93+
94+
post.set('created_at', _private.addOffset(post.get('created_at')));
95+
return models.Post.edit(post.toJSON(), _.merge({}, options, {id: post.get('id')}));
96+
}).then(function () {
97+
logger.info(messagePrefix + 'Updated datetime fields for Posts');
98+
});
99+
});
100+
},
101+
function updateUsers() {
102+
return models.User.findAll(options).then(function (result) {
103+
if (result.models.length === 0) {
104+
logger.warn(messagePrefix + 'No Users found');
105+
return;
106+
}
107+
108+
return Promise.mapSeries(result.models, function mapper(user) {
109+
if (user.get('last_login')) {
110+
user.set('last_login', _private.addOffset(user.get('last_login')));
111+
}
112+
113+
if (user.get('updated_at')) {
114+
user.set('updated_at', _private.addOffset(user.get('updated_at')));
115+
}
116+
117+
user.set('created_at', _private.addOffset(user.get('created_at')));
118+
return models.User.edit(user.toJSON(), _.merge({}, options, {id: user.get('id')}));
119+
}).then(function () {
120+
logger.info(messagePrefix + 'Updated datetime fields for Users');
121+
});
122+
});
123+
},
124+
function updateSubscribers() {
125+
return models.Subscriber.findAll(options).then(function (result) {
126+
if (result.models.length === 0) {
127+
logger.warn(messagePrefix + 'No Subscribers found');
128+
return;
129+
}
130+
131+
return Promise.mapSeries(result.models, function mapper(subscriber) {
132+
if (subscriber.get('unsubscribed_at')) {
133+
subscriber.set('unsubscribed_at', _private.addOffset(subscriber.get('unsubscribed_at')));
134+
}
135+
136+
if (subscriber.get('updated_at')) {
137+
subscriber.set('updated_at', _private.addOffset(subscriber.get('updated_at')));
138+
}
139+
140+
subscriber.set('created_at', _private.addOffset(subscriber.get('created_at')));
141+
return models.Subscriber.edit(subscriber.toJSON(), _.merge({}, options, {id: subscriber.get('id')}));
142+
}).then(function () {
143+
logger.info(messagePrefix + 'Updated datetime fields for Subscribers');
144+
});
145+
});
146+
},
147+
function updateSettings() {
148+
return models.Settings.findAll(options).then(function (result) {
149+
if (result.models.length === 0) {
150+
logger.warn(messagePrefix + 'No Settings found');
151+
return;
152+
}
153+
154+
return Promise.mapSeries(result.models, function mapper(settings) {
155+
// migrations was new created, so it already is in UTC
156+
if (settings.get('key') === 'migrations') {
157+
return Promise.resolve();
158+
}
159+
160+
if (settings.get('updated_at')) {
161+
settings.set('updated_at', _private.addOffset(settings.get('updated_at')));
162+
}
163+
164+
settings.set('created_at', _private.addOffset(settings.get('created_at')));
165+
return models.Settings.edit(settings.toJSON(), _.merge({}, options, {id: settings.get('id')}));
166+
}).then(function () {
167+
logger.info(messagePrefix + 'Updated datetime fields for Settings');
168+
});
169+
});
170+
},
171+
function updateAllOtherModels() {
172+
return Promise.mapSeries(['Role', 'Permission', 'Tag', 'App', 'AppSetting', 'AppField', 'Client'], function (model) {
173+
return models[model].findAll(options).then(function (result) {
174+
if (result.models.length === 0) {
175+
logger.warn(messagePrefix + 'No {model} found'.replace('{model}', model));
176+
return;
177+
}
178+
179+
return Promise.mapSeries(result.models, function mapper(object) {
180+
object.set('created_at', _private.addOffset(object.get('created_at')));
181+
182+
if (object.get('updated_at')) {
183+
object.set('updated_at', _private.addOffset(object.get('updated_at')));
184+
}
185+
186+
return models[model].edit(object.toJSON(), _.merge({}, options, {id: object.get('id')}));
187+
}).then(function () {
188+
logger.info(messagePrefix + 'Updated datetime fields for {model}'.replace('{model}', model));
189+
});
190+
});
191+
});
192+
},
193+
function addMigrationSettingsEntry() {
194+
settingsMigrations[settingsKey] = moment().format();
195+
return models.Settings.edit({
196+
key: 'migrations',
197+
value: JSON.stringify(settingsMigrations)
198+
}, options);
199+
}]
200+
).catch(function (err) {
201+
if (err.message === 'skip') {
202+
logger.warn(messagePrefix + 'Your databases uses UTC datetimes, skip!');
203+
return Promise.resolve();
204+
}
205+
206+
return Promise.reject(err);
207+
});
208+
});
209+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = [
2+
require('./01-transform-dates-into-utc')
3+
];

core/server/data/schema/default-settings.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"core": {
33
"databaseVersion": {
4-
"defaultValue": "005"
4+
"defaultValue": "006"
55
},
66
"dbHash": {
77
"defaultValue": null
@@ -11,6 +11,9 @@
1111
},
1212
"displayUpdateNotification": {
1313
"defaultValue": null
14+
},
15+
"migrations": {
16+
"defaultValue": "{}"
1417
}
1518
},
1619
"blog": {

core/server/data/validation/index.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@ var schema = require('../schema').tables,
55
Promise = require('bluebird'),
66
errors = require('../../errors'),
77
config = require('../../config'),
8-
readThemes = require('../../utils/read-themes'),
8+
readThemes = require('../../utils/read-themes'),
99
i18n = require('../../i18n'),
10-
toString = require('lodash.tostring'),
1110

1211
validateSchema,
1312
validateSettings,
@@ -54,7 +53,7 @@ validateSchema = function validateSchema(tableName, model) {
5453

5554
_.each(columns, function each(columnKey) {
5655
var message = '',
57-
strVal = toString(model[columnKey]);
56+
strVal = _.toString(model[columnKey]);
5857

5958
// check nullable
6059
if (model.hasOwnProperty(columnKey) && schema[tableName][columnKey].hasOwnProperty('nullable')
@@ -166,7 +165,7 @@ validateActiveTheme = function validateActiveTheme(themeName) {
166165
// available validators: https://github.com/chriso/validator.js#validators
167166
validate = function validate(value, key, validations) {
168167
var validationErrors = [];
169-
value = toString(value);
168+
value = _.toString(value);
170169

171170
_.each(validations, function each(validationOptions, validationName) {
172171
var goodResult = true;

0 commit comments

Comments
 (0)