Skip to content

Commit 69ea056

Browse files
committed
feat(*): basic setup for handlers and wrapping config logic
1 parent f557ded commit 69ea056

14 files changed

+459
-29
lines changed

Diff for: index.js

+1-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
const createHandlers = require('./src/createHandlers');
2-
3-
module.exports = {
4-
createHandlers,
5-
};
1+
module.exports = require('./lib');

Diff for: jest.config.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ module.exports = {
88
// The test environment that will be used for testing
99
testEnvironment: 'node',
1010

11-
// Source files roots (limit to `src/`)
12-
roots: ['<rootDir>/src'],
11+
// Source files roots (limit to `lib/`)
12+
roots: ['<rootDir>/lib'],
1313

1414
// Looks for tests in the __tests__ folder or alongside js files with the .(test|spec).js
1515
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.js$',

Diff for: lib/config.js

+190
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
const { has } = require('ramda');
2+
const convict = require('./convict');
3+
const { createDebug } = require('./debug');
4+
5+
const DEBUG_KEY = 'config';
6+
7+
const debug = createDebug(DEBUG_KEY);
8+
9+
const keyForValidatedConfigs = Symbol('validatedConfig');
10+
11+
const definition = {
12+
env: {
13+
doc: 'The environment the application is running in.',
14+
format: ['production', 'development', 'test'],
15+
default: 'development',
16+
env: 'NODE_ENV',
17+
},
18+
origin: {
19+
doc: 'The HTTP origin of the host of the netlify-cms admin panel using this OAuth provider. ' +
20+
'Multiple origin domains can be provided as an array of strings or a single comma-separated string. ' +
21+
'You can provide only the domain part (`\'example.com\'`) which implies any protocol on any port or you can explicitly ' +
22+
'specify a protocol and/or port (`\'https://example.com\'` or `\'http://example.com:8080\'`)',
23+
format: 'origin-list',
24+
default: null,
25+
allowEmpty: false,
26+
env: 'ORIGIN',
27+
},
28+
completeUri: {
29+
doc: 'The URI (specified during the OAuth 2.0 authorization flow) that the `complete` handler is hosted at.',
30+
default: null,
31+
format: String,
32+
env: 'OAUTH_REDIRECT_URI',
33+
},
34+
oauthProvider: {
35+
doc: 'The Git service / OAuth provider to use',
36+
default: 'github',
37+
format: ['github', 'gitlab'],
38+
env: 'OAUTH_PROVIDER',
39+
},
40+
oauthClientID: {
41+
doc: 'The OAuth 2.0 Client ID received from the OAuth provider.',
42+
default: null,
43+
format: String,
44+
env: 'OAUTH_CLIENT_ID',
45+
},
46+
oauthClientSecret: {
47+
doc: 'The OAuth 2.0 Client secret received from the OAuth provider.',
48+
default: null,
49+
format: String,
50+
env: 'OAUTH_CLIENT_SECRET',
51+
},
52+
oauthTokenHost: {
53+
doc: 'The OAuth 2.0 token host URI for the OAuth provider. ' +
54+
'If not provided, this will be guessed based on the provider. ' +
55+
'You must provide this for GitHub enterprise.',
56+
default: '',
57+
format: String,
58+
env: 'OAUTH_TOKEN_HOST',
59+
},
60+
oauthTokenPath: {
61+
doc: 'The relative URI to the OAuth 2.0 token endpoint for the OAuth provider. ' +
62+
'If not provided, this will be guessed based on the provider.',
63+
default: '',
64+
format: String,
65+
env: 'OAUTH_TOKEN_PATH',
66+
},
67+
oauthAuthorizePath: {
68+
doc: 'The relative URI to the OAuth 2.0 authorization endpoint for the OAuth provider. ' +
69+
'If not provided, this will be guessed based on the provider.',
70+
default: '',
71+
format: String,
72+
env: 'OAUTH_AUTHORIZE_PATH',
73+
},
74+
oauthScopes: {
75+
doc: 'The scopes to claim during the OAuth 2.0 authorization request with the OAuth provider. ' +
76+
'If not provided, this will be guessed based on the provider with the goal to ensure the user has ' +
77+
'read/write access to repositories.',
78+
default: '',
79+
format: String,
80+
env: 'OAUTH_SCOPES',
81+
},
82+
};
83+
84+
/**
85+
* Mutates the provided object, marking it as a validated config so we can check for it later.
86+
* @param {object} config
87+
*/
88+
function markConfigAsValidated(config) {
89+
config[keyForValidatedConfigs] = true;
90+
}
91+
92+
/**
93+
* Determine if a given value is a validated and authentic config.
94+
*
95+
* @param {object} config
96+
* @return {boolean}
97+
*/
98+
function isConfigValidated(config) {
99+
return !!(
100+
config &&
101+
typeof config === 'object' &&
102+
has(keyForValidatedConfigs, config) &&
103+
config[keyForValidatedConfigs]
104+
)
105+
}
106+
107+
/**
108+
* @typedef {{
109+
* skipAlreadyValidatedCheck?: boolean,
110+
* useEnv?: boolean,
111+
* useArgs?: boolean,
112+
* extraConvictOptions?: {},
113+
* extraValidateOptions?: {}
114+
* }} CreateConfigOptions
115+
*/
116+
117+
/**
118+
* @typedef {{get: function(string): *}} ConvictConfig
119+
*/
120+
121+
/**
122+
* Create and validate a convict configuration instance for this package.
123+
*
124+
* @param {{}=} userConfig
125+
* @param {boolean=} skipAlreadyValidatedCheck Set to true to always try to reload and revalidate the provided config
126+
* @param {boolean=} useEnv Set to true to try to extract config values from environment variables
127+
* @param {boolean=} useArgs Set to true to try to extract config values from command line arguments
128+
* @param {{}=} extraConvictOptions Additional options to pass directly to convict
129+
* @param {{}=} extraValidateOptions Additional options to pass directly to convict's validate function.
130+
* @return {{}} The convict config instance (with i.e. a `get` method)
131+
*/
132+
function createConfig(
133+
userConfig = {},
134+
{
135+
skipAlreadyValidatedCheck = false,
136+
useEnv = false,
137+
useArgs = false,
138+
extraConvictOptions = {},
139+
extraValidateOptions = {},
140+
} = {},
141+
) {
142+
// If the config provided is already a validated config, we can just straight up return it.
143+
if (!skipAlreadyValidatedCheck && isConfigValidated(userConfig)) {
144+
return userConfig;
145+
}
146+
147+
// Build out convict options
148+
const convictOptions = {};
149+
if (!useEnv) {
150+
convictOptions.env = {};
151+
}
152+
if (!useArgs) {
153+
convictOptions.args = [];
154+
}
155+
156+
// Merge together options
157+
const processedOptions = {
158+
...convictOptions,
159+
...extraConvictOptions,
160+
};
161+
162+
// Build the config based on our definition and options
163+
const config = convict(definition, processedOptions);
164+
165+
// Merge in the user config
166+
config.load(userConfig);
167+
168+
// Validate the config; this throws if the config is invalid
169+
config.validate({
170+
allowed: 'warn',
171+
output: debug,
172+
...extraValidateOptions,
173+
});
174+
175+
// Mark the config as authentic and validated
176+
markConfigAsValidated(config);
177+
178+
// Return it
179+
return config;
180+
}
181+
182+
module.exports = {
183+
debug,
184+
DEBUG_KEY,
185+
definition,
186+
markConfigAsValidated,
187+
keyForValidatedConfigs,
188+
isConfigValidated,
189+
createConfig,
190+
};

Diff for: lib/convict.js

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
const { format } = require('util');
2+
const convict = require('convict');
3+
4+
function isInvalidDueToEmptiness(finalVal, schema) {
5+
return schema.allowEmpty === false && finalVal.length === 0;
6+
}
7+
8+
/**
9+
* A format for convict that coerces comma-separated strings into arrays.
10+
*
11+
* @type {{name: string, coerce: function, validate: function}}
12+
*/
13+
const listFormat = (() => {
14+
const coerce = (val, schema) => {
15+
if (!Array.isArray(val) && typeof val === 'object') {
16+
return null;
17+
}
18+
19+
// First, coerce the value into an array by treating it as a comma-separated string if it isn't already an array.
20+
const processedVal = !Array.isArray(val)
21+
? `${val || ''}`.trim().split(',')
22+
: val;
23+
24+
// We don't use map or reduce here so we can bail early if we stumble upon a bad value.
25+
const finalVal = [];
26+
27+
for (const item of processedVal) {
28+
if (
29+
typeof item === 'object' ||
30+
typeof item === 'undefined' ||
31+
typeof item === 'symbol' ||
32+
typeof item === 'function'
33+
) {
34+
return null;
35+
}
36+
finalVal.push(item);
37+
}
38+
39+
return finalVal;
40+
};
41+
42+
const validate = (val, schema) => {
43+
const finalVal = coerce(val);
44+
if (finalVal === null || isInvalidDueToEmptiness(finalVal, schema)) {
45+
throw new Error(format('Expected array or string of comma-separated values, received:', val));
46+
}
47+
};
48+
49+
return {
50+
name: 'list',
51+
validate,
52+
coerce,
53+
};
54+
})();
55+
56+
const originListFormat = (() => {
57+
const coerce = (...restArgs) => {
58+
const listVal = listFormat.coerce(...restArgs);
59+
if (listVal === null) {
60+
return listVal;
61+
}
62+
63+
// We don't use map or reduce here so we can bail early if we stumble upon a bad value.
64+
const finalVal = [];
65+
66+
for (const item of listVal) {
67+
const processedItem = `${item}`.trim().toLowerCase();
68+
if (!processedItem) {
69+
return null;
70+
}
71+
finalVal.push(processedItem);
72+
}
73+
74+
return finalVal;
75+
};
76+
77+
const validate = (val, schema) => {
78+
const finalVal = coerce(val);
79+
if(finalVal === null || isInvalidDueToEmptiness(finalVal, schema)) {
80+
throw new Error(format('Expected array or string of comma-separated HTTP origins, received:', val));
81+
}
82+
};
83+
84+
return {
85+
name: 'origin-list',
86+
coerce,
87+
validate,
88+
};
89+
})();
90+
91+
convict.addFormat(listFormat);
92+
93+
convict.addFormat(originListFormat);
94+
95+
module.exports = convict;

Diff for: lib/debug.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const debug = require('debug');
2+
const { flatten } = require('ramda');
3+
4+
const DEBUG_ROOT_KEY = 'netlify-cms-oauth-provider-node';
5+
6+
const DEBUG_KEY_SEPARATOR = ':';
7+
8+
/**
9+
* Create a debug logger function that's pre-namespaced to this package.
10+
*
11+
* @param {(string|string[])...} keys
12+
* @return {function}
13+
*/
14+
function createDebug(...keys) {
15+
const processedKeys = flatten(keys);
16+
return debug(
17+
processedKeys.length
18+
? processedKeys.join(DEBUG_KEY_SEPARATOR)
19+
: DEBUG_ROOT_KEY
20+
);
21+
}
22+
23+
module.exports = {
24+
DEBUG_ROOT_KEY,
25+
DEBUG_KEY_SEPARATOR,
26+
debug: debug(DEBUG_ROOT_KEY),
27+
createDebug,
28+
};

Diff for: lib/handlers/generic.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const { receivesUserConfig } = require('./utils');
2+
3+
const createBeginHandler = receivesUserConfig((config) => {
4+
// TODO
5+
return () => true;
6+
});
7+
8+
const createCompleteHandler = receivesUserConfig((config) => {
9+
// TODO
10+
return () => true;
11+
});
12+
13+
const createHandlers = receivesUserConfig((config) => {
14+
return {
15+
begin: createBeginHandler(config),
16+
complete: createCompleteHandler(config),
17+
};
18+
});
19+
20+
module.exports = {
21+
createHandlers,
22+
createBeginHandler,
23+
createCompleteHandler,
24+
};

Diff for: lib/handlers/generic.test.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const { createHandlers } = require('./generic');
2+
3+
const okayConfig = {
4+
origin: 'localhost',
5+
completeUri: 'https://localhost/complete',
6+
oauthClientID: 'abc123',
7+
oauthClientSecret: 'def456',
8+
};
9+
10+
test('createHandlers works', () => {
11+
const handlers = createHandlers(okayConfig);
12+
expect(typeof handlers).toBe('object');
13+
expect(handlers).toHaveProperty('begin');
14+
expect(handlers).toHaveProperty('complete');
15+
expect(typeof handlers.begin).toBe('function');
16+
expect(typeof handlers.complete).toBe('function');
17+
});

Diff for: lib/handlers/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const generic = require('./generic');
2+
3+
module.exports = {
4+
generic,
5+
};

0 commit comments

Comments
 (0)