Skip to content

Commit 9de4b8b

Browse files
authored
Refactors configuration management (#4271)
* Adds flow types / Configuration interfaces * Lets call it options * Use a single interface to generate the configurations * Translates options to definitions only if comments are set * improves logic * Moves objects around * Fixes issue affecting logging of circular objects * fixes undefined env * Moves all defaults to defaults * Adds back CLI defaults * Restored defaults in commander.js * Merge provided defaults and platform defaults * Addresses visual nits * Improves Config.js code * Adds ability to pass the default value in trailing comments * Load platform defaults from the definitions file * proper default values on various options * Adds ParseServer.start and server.start(options) as quick startup methods * Moves creating liveQueryServer http into ParseServer.js * removes dead code * Adds tests to guarantee we can start a LQ Server from main module * Fixes incorrect code regading liveQuery init port * Start a http server for LQ if port is specified * ensure we dont fail if config.port is not set * Specify port * ignore other path skipped in tests * Adds test for custom middleware setting * Refactors new Config into Config.get - Hides AppCache from ParseServer.js, use Config.put which validates * Extracts controller creation into Controllers/index.js - This makes the ParseServer init way simpler * Move serverURL inference into ParseServer * review nits
1 parent d29a448 commit 9de4b8b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1462
-874
lines changed

resources/buildConfigDefinitions.js

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/**
2+
* Parse Server Configuration Builder
3+
*
4+
* This module builds the definitions file (src/Options/Definitions.js)
5+
* from the src/Options/index.js options interfaces.
6+
* The Definitions.js module is responsible for the default values as well
7+
* as the mappings for the CLI.
8+
*
9+
* To rebuild the definitions file, run
10+
* `$ node resources/buildConfigDefinitions.js`
11+
*/
12+
const parsers = require('../src/Options/parsers');
13+
14+
function last(array) {
15+
return array[array.length - 1];
16+
}
17+
18+
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
19+
function toENV(key) {
20+
let str = '';
21+
let previousIsUpper = false;
22+
for(let i = 0; i < key.length; i++) {
23+
const char = key[i];
24+
if (letters.indexOf(char) >= 0) {
25+
if (!previousIsUpper) {
26+
str += '_';
27+
previousIsUpper = true;
28+
}
29+
} else {
30+
previousIsUpper = false;
31+
}
32+
str += char;
33+
}
34+
return str.toUpperCase();
35+
}
36+
37+
function getCommentValue(comment) {
38+
if (!comment) { return }
39+
return comment.value.trim();
40+
}
41+
42+
function getENVPrefix(iface) {
43+
if (iface.id.name === 'ParseServerOptions') {
44+
return 'PARSE_SERVER_';
45+
}
46+
if (iface.id.name === 'CustomPagesOptions') {
47+
return 'PARSE_SERVER_CUSTOM_PAGES_';
48+
}
49+
if (iface.id.name === 'LiveQueryServerOptions') {
50+
return 'PARSE_SERVER_LIVE_QUERY_';
51+
}
52+
}
53+
54+
function processProperty(property, iface) {
55+
const firstComment = getCommentValue(last(property.leadingComments || []));
56+
const lastComment = getCommentValue((property.trailingComments || [])[0]);
57+
const name = property.key.name;
58+
const prefix = getENVPrefix(iface);
59+
60+
if (!firstComment) {
61+
return;
62+
}
63+
const components = firstComment.split(':ENV:').map((elt) => {
64+
return elt.trim();
65+
});
66+
let defaultValue;
67+
if (lastComment && lastComment.indexOf('=') >= 0) {
68+
const slice = lastComment.slice(lastComment.indexOf('=') + 1, lastComment.length).trim();
69+
defaultValue = slice;
70+
}
71+
const help = components[0];
72+
const env = components[1] || (prefix + toENV(name));
73+
let type = property.value.type;
74+
let isRequired = true;
75+
if (type == 'NullableTypeAnnotation') {
76+
isRequired = false;
77+
type = property.value.typeAnnotation.type;
78+
}
79+
return {
80+
name,
81+
env,
82+
help,
83+
type,
84+
defaultValue,
85+
types: property.value.types,
86+
typeAnnotation: property.value.typeAnnotation,
87+
required: isRequired
88+
};
89+
}
90+
91+
92+
function doInterface(iface) {
93+
return iface.body.properties
94+
.map((prop) => processProperty(prop, iface))
95+
.filter((e) => e !== undefined);
96+
}
97+
98+
function mapperFor(elt, t) {
99+
const p = t.identifier('parsers');
100+
const wrap = (identifier) => t.memberExpression(p, identifier);
101+
102+
if (t.isNumberTypeAnnotation(elt)) {
103+
return t.callExpression(wrap(t.identifier('numberParser')), [t.stringLiteral(elt.name)]);
104+
} else if (t.isArrayTypeAnnotation(elt)) {
105+
return wrap(t.identifier('arrayParser'));
106+
} else if (t.isAnyTypeAnnotation(elt)) {
107+
return wrap(t.identifier('objectParser'));
108+
} else if (t.isBooleanTypeAnnotation(elt)) {
109+
return wrap(t.identifier('booleanParser'));
110+
} else if (t.isGenericTypeAnnotation(elt)) {
111+
const type = elt.typeAnnotation.id.name;
112+
if (type == 'Adapter') {
113+
return wrap(t.identifier('moduleOrObjectParser'));
114+
}
115+
if (type == 'NumberOrBoolean') {
116+
return wrap(t.identifier('numberOrBooleanParser'));
117+
}
118+
return wrap(t.identifier('objectParser'));
119+
}
120+
}
121+
122+
function parseDefaultValue(elt, value, t) {
123+
let litteralValue;
124+
if (t.isStringTypeAnnotation(elt)) {
125+
if (value == '""' || value == "''") {
126+
litteralValue = t.stringLiteral('');
127+
} else {
128+
litteralValue = t.stringLiteral(value);
129+
}
130+
} else if (t.isNumberTypeAnnotation(elt)) {
131+
litteralValue = t.numericLiteral(parsers.numberOrBoolParser('')(value));
132+
} else if (t.isArrayTypeAnnotation(elt)) {
133+
const array = parsers.objectParser(value);
134+
litteralValue = t.arrayExpression(array.map((value) => {
135+
if (typeof value == 'string') {
136+
return t.stringLiteral(value);
137+
} else {
138+
throw new Error('Unable to parse array');
139+
}
140+
}));
141+
} else if (t.isAnyTypeAnnotation(elt)) {
142+
litteralValue = t.arrayExpression([]);
143+
} else if (t.isBooleanTypeAnnotation(elt)) {
144+
litteralValue = t.booleanLiteral(parsers.booleanParser(value));
145+
} else if (t.isGenericTypeAnnotation(elt)) {
146+
const type = elt.typeAnnotation.id.name;
147+
if (type == 'NumberOrBoolean') {
148+
litteralValue = t.numericLiteral(parsers.numberOrBoolParser('')(value));
149+
}
150+
if (type == 'CustomPagesOptions') {
151+
const object = parsers.objectParser(value);
152+
const props = Object.keys(object).map((key) => {
153+
return t.objectProperty(key, object[value]);
154+
});
155+
litteralValue = t.objectExpression(props);
156+
}
157+
}
158+
return litteralValue;
159+
}
160+
161+
function inject(t, list) {
162+
return list.map((elt) => {
163+
if (!elt.name) {
164+
return;
165+
}
166+
const props = ['env', 'help'].map((key) => {
167+
if (elt[key]) {
168+
return t.objectProperty(t.stringLiteral(key), t.stringLiteral(elt[key]));
169+
}
170+
}).filter((e) => e !== undefined);
171+
if (elt.required) {
172+
props.push(t.objectProperty(t.stringLiteral('required'), t.booleanLiteral(true)))
173+
}
174+
const action = mapperFor(elt, t);
175+
if (action) {
176+
props.push(t.objectProperty(t.stringLiteral('action'), action))
177+
}
178+
if (elt.defaultValue) {
179+
const parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
180+
if (parsedValue) {
181+
props.push(t.objectProperty(t.stringLiteral('default'), parsedValue));
182+
} else {
183+
throw new Error(`Unable to parse value for ${elt.name} `);
184+
}
185+
}
186+
const obj = t.objectExpression(props);
187+
return t.objectProperty(t.stringLiteral(elt.name), obj);
188+
}).filter((elt) => {
189+
return elt != undefined;
190+
});
191+
}
192+
193+
const makeRequire = function(variableName, module, t) {
194+
const decl = t.variableDeclarator(t.identifier(variableName), t.callExpression(t.identifier('require'), [t.stringLiteral(module)]));
195+
return t.variableDeclaration('var', [decl])
196+
}
197+
198+
const plugin = function (babel) {
199+
const t = babel.types;
200+
const moduleExports = t.memberExpression(t.identifier('module'), t.identifier('exports'));
201+
return {
202+
visitor: {
203+
Program: function(path) {
204+
// Inject the parser's loader
205+
path.unshiftContainer('body', makeRequire('parsers', './parsers', t));
206+
},
207+
ExportDeclaration: function(path) {
208+
// Export declaration on an interface
209+
if (path.node && path.node.declaration && path.node.declaration.type == 'InterfaceDeclaration') {
210+
const l = inject(t, doInterface(path.node.declaration));
211+
const id = path.node.declaration.id.name;
212+
const exports = t.memberExpression(moduleExports, t.identifier(id));
213+
path.replaceWith(
214+
t.assignmentExpression('=', exports, t.objectExpression(l))
215+
)
216+
}
217+
}
218+
}
219+
}
220+
};
221+
222+
const auxiliaryCommentBefore = `
223+
**** GENERATED CODE ****
224+
This code has been generated by resources/buildConfigDefinitions.js
225+
Do not edit manually, but update Options/index.js
226+
`
227+
228+
const babel = require("babel-core");
229+
const res = babel.transformFileSync('./src/Options/index.js', { plugins: [ plugin ], auxiliaryCommentBefore });
230+
require('fs').writeFileSync('./src/Options/Definitions.js', res.code + '\n');

spec/AccountLockoutPolicy.spec.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ describe("Account Lockout Policy: ", () => {
7171
publicServerURL: "https://my.public.server.com/1"
7272
})
7373
.then(() => {
74-
new Config('test');
74+
Config.get('test');
7575
fail('set duration to an invalid number test failed');
7676
done();
7777
})
@@ -95,7 +95,7 @@ describe("Account Lockout Policy: ", () => {
9595
publicServerURL: "https://my.public.server.com/1"
9696
})
9797
.then(() => {
98-
new Config('test');
98+
Config.get('test');
9999
fail('set threshold to an invalid number test failed');
100100
done();
101101
})
@@ -119,7 +119,7 @@ describe("Account Lockout Policy: ", () => {
119119
publicServerURL: "https://my.public.server.com/1"
120120
})
121121
.then(() => {
122-
new Config('test');
122+
Config.get('test');
123123
fail('threshold value < 1 is invalid test failed');
124124
done();
125125
})
@@ -143,7 +143,7 @@ describe("Account Lockout Policy: ", () => {
143143
publicServerURL: "https://my.public.server.com/1"
144144
})
145145
.then(() => {
146-
new Config('test');
146+
Config.get('test');
147147
fail('threshold value > 999 is invalid test failed');
148148
done();
149149
})
@@ -167,7 +167,7 @@ describe("Account Lockout Policy: ", () => {
167167
publicServerURL: "https://my.public.server.com/1"
168168
})
169169
.then(() => {
170-
new Config('test');
170+
Config.get('test');
171171
fail('duration value < 1 is invalid test failed');
172172
done();
173173
})
@@ -191,7 +191,7 @@ describe("Account Lockout Policy: ", () => {
191191
publicServerURL: "https://my.public.server.com/1"
192192
})
193193
.then(() => {
194-
new Config('test');
194+
Config.get('test');
195195
fail('duration value > 99999 is invalid test failed');
196196
done();
197197
})

spec/AdapterLoader.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ describe("AdapterLoader", ()=>{
135135
reconfigureServer({
136136
push: pushAdapterOptions,
137137
}).then(() => {
138-
const config = new Config(Parse.applicationId);
138+
const config = Config.get(Parse.applicationId);
139139
const pushAdapter = config.pushWorker.adapter;
140140
expect(pushAdapter.getValidPushTypes()).toEqual(['ios']);
141141
expect(pushAdapter.options).toEqual(pushAdapterOptions);

spec/AudienceRouter.spec.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ var AudiencesRouter = require('../src/Routers/AudiencesRouter').AudiencesRouter;
55

66
describe('AudiencesRouter', () => {
77
it('uses find condition from request.body', (done) => {
8-
var config = new Config('test');
8+
var config = Config.get('test');
99
var androidAudienceRequest = {
1010
'name': 'Android Users',
1111
'query': '{ "test": "android" }'
@@ -46,7 +46,7 @@ describe('AudiencesRouter', () => {
4646
});
4747

4848
it('uses find condition from request.query', (done) => {
49-
var config = new Config('test');
49+
var config = Config.get('test');
5050
var androidAudienceRequest = {
5151
'name': 'Android Users',
5252
'query': '{ "test": "android" }'
@@ -87,7 +87,7 @@ describe('AudiencesRouter', () => {
8787
});
8888

8989
it('query installations with limit = 0', (done) => {
90-
var config = new Config('test');
90+
var config = Config.get('test');
9191
var androidAudienceRequest = {
9292
'name': 'Android Users',
9393
'query': '{ "test": "android" }'
@@ -106,7 +106,7 @@ describe('AudiencesRouter', () => {
106106
info: {}
107107
};
108108

109-
new Config('test');
109+
Config.get('test');
110110
var router = new AudiencesRouter();
111111
rest.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
112112
.then(() => {
@@ -127,7 +127,7 @@ describe('AudiencesRouter', () => {
127127
});
128128

129129
it('query installations with count = 1', done => {
130-
var config = new Config('test');
130+
var config = Config.get('test');
131131
var androidAudienceRequest = {
132132
'name': 'Android Users',
133133
'query': '{ "test": "android" }'
@@ -163,7 +163,7 @@ describe('AudiencesRouter', () => {
163163
});
164164

165165
it('query installations with limit = 0 and count = 1', (done) => {
166-
var config = new Config('test');
166+
var config = Config.get('test');
167167
var androidAudienceRequest = {
168168
'name': 'Android Users',
169169
'query': '{ "test": "android" }'
@@ -286,7 +286,7 @@ describe('AudiencesRouter', () => {
286286
});
287287

288288
it_exclude_dbs(['postgres'])('should support legacy parse.com audience fields', (done) => {
289-
const database = (new Config(Parse.applicationId)).database.adapter.database;
289+
const database = (Config.get(Parse.applicationId)).database.adapter.database;
290290
const now = new Date();
291291
Parse._request('POST', 'push_audiences', { name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' })}, { useMasterKey: true })
292292
.then((audience) => {

spec/AuthenticationAdapters.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ describe('AuthenticationProviders', function() {
186186
ok(!provider.synchronizedExpiration,
187187
"Expiration should be cleared");
188188
// make sure the auth data is properly deleted
189-
var config = new Config(Parse.applicationId);
189+
var config = Config.get(Parse.applicationId);
190190
config.database.adapter.find('_User', {
191191
fields: Object.assign({}, defaultColumns._Default, defaultColumns._Installation),
192192
}, { objectId: model.id }, {})

0 commit comments

Comments
 (0)