/** * Parse Server Configuration Builder * * This module builds the definitions file (src/Options/Definitions.js) * from the src/Options/index.js options interfaces. * The Definitions.js module is responsible for the default values as well * as the mappings for the CLI. * * To rebuild the definitions file, run * `$ node resources/buildConfigDefinitions.js` */ const parsers = require('../src/Options/parsers'); /** The types of nested options. */ const nestedOptionTypes = [ 'CustomPagesOptions', 'DatabaseOptions', 'FileUploadOptions', 'IdempotencyOptions', 'Object', 'PagesCustomUrlsOptions', 'PagesOptions', 'PagesRoute', 'PasswordPolicyOptions', 'SecurityOptions', 'SchemaOptions', 'LogLevels', ]; /** The prefix of environment variables for nested options. */ const nestedOptionEnvPrefix = { AccountLockoutOptions: 'PARSE_SERVER_ACCOUNT_LOCKOUT_', CustomPagesOptions: 'PARSE_SERVER_CUSTOM_PAGES_', DatabaseOptions: 'PARSE_SERVER_DATABASE_', FileUploadOptions: 'PARSE_SERVER_FILE_UPLOAD_', IdempotencyOptions: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_', LiveQueryOptions: 'PARSE_SERVER_LIVEQUERY_', LiveQueryServerOptions: 'PARSE_LIVE_QUERY_SERVER_', PagesCustomUrlsOptions: 'PARSE_SERVER_PAGES_CUSTOM_URL_', PagesOptions: 'PARSE_SERVER_PAGES_', PagesRoute: 'PARSE_SERVER_PAGES_ROUTE_', ParseServerOptions: 'PARSE_SERVER_', PasswordPolicyOptions: 'PARSE_SERVER_PASSWORD_POLICY_', SecurityOptions: 'PARSE_SERVER_SECURITY_', SchemaOptions: 'PARSE_SERVER_SCHEMA_', LogLevels: 'PARSE_SERVER_LOG_LEVELS_', RateLimitOptions: 'PARSE_SERVER_RATE_LIMIT_', }; function last(array) { return array[array.length - 1]; } const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; function toENV(key) { let str = ''; let previousIsUpper = false; for (let i = 0; i < key.length; i++) { const char = key[i]; if (letters.indexOf(char) >= 0) { if (!previousIsUpper) { str += '_'; previousIsUpper = true; } } else { previousIsUpper = false; } str += char; } return str.toUpperCase(); } function getCommentValue(comment) { if (!comment) { return; } return comment.value.trim(); } function getENVPrefix(iface) { if (nestedOptionEnvPrefix[iface.id.name]) { return nestedOptionEnvPrefix[iface.id.name]; } } function processProperty(property, iface) { const firstComment = getCommentValue(last(property.leadingComments || [])); const name = property.key.name; const prefix = getENVPrefix(iface); if (!firstComment) { return; } const lines = firstComment.split('\n').map(line => line.trim()); let help = ''; let envLine; let defaultLine; lines.forEach(line => { if (line.indexOf(':ENV:') === 0) { envLine = line; } else if (line.indexOf(':DEFAULT:') === 0) { defaultLine = line; } else { help += line; } }); let env; if (envLine) { env = envLine.split(' ')[1]; } else { env = prefix + toENV(name); } let defaultValue; if (defaultLine) { const defaultArray = defaultLine.split(' '); defaultArray.shift(); defaultValue = defaultArray.join(' '); } let type = property.value.type; let isRequired = true; if (type == 'NullableTypeAnnotation') { isRequired = false; type = property.value.typeAnnotation.type; } return { name, env, help, type, defaultValue, types: property.value.types, typeAnnotation: property.value.typeAnnotation, required: isRequired, }; } function doInterface(iface) { return iface.body.properties .sort((a, b) => a.key.name.localeCompare(b.key.name)) .map(prop => processProperty(prop, iface)) .filter(e => e !== undefined); } function mapperFor(elt, t) { const p = t.identifier('parsers'); const wrap = identifier => t.memberExpression(p, identifier); if (t.isNumberTypeAnnotation(elt)) { return t.callExpression(wrap(t.identifier('numberParser')), [t.stringLiteral(elt.name)]); } else if (t.isArrayTypeAnnotation(elt)) { return wrap(t.identifier('arrayParser')); } else if (t.isAnyTypeAnnotation(elt)) { return wrap(t.identifier('objectParser')); } else if (t.isBooleanTypeAnnotation(elt)) { return wrap(t.identifier('booleanParser')); } else if (t.isGenericTypeAnnotation(elt)) { const type = elt.typeAnnotation.id.name; if (type == 'Adapter') { return wrap(t.identifier('moduleOrObjectParser')); } if (type == 'NumberOrBoolean') { return wrap(t.identifier('numberOrBooleanParser')); } if (type === 'StringOrStringArray') { return wrap(t.identifier('arrayParser')); } return wrap(t.identifier('objectParser')); } } function parseDefaultValue(elt, value, t) { let literalValue; if (t.isStringTypeAnnotation(elt)) { if (value == '""' || value == "''") { literalValue = t.stringLiteral(''); } else { literalValue = t.stringLiteral(value); } } else if (t.isNumberTypeAnnotation(elt)) { literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value)); } else if (t.isArrayTypeAnnotation(elt)) { const array = parsers.objectParser(value); literalValue = t.arrayExpression( array.map(value => { if (typeof value == 'string') { return t.stringLiteral(value); } else if (typeof value == 'number') { return t.numericLiteral(value); } else if (typeof value == 'object') { const object = parsers.objectParser(value); const props = Object.entries(object).map(([k, v]) => { if (typeof v == 'string') { return t.objectProperty(t.identifier(k), t.stringLiteral(v)); } else if (typeof v == 'number') { return t.objectProperty(t.identifier(k), t.numericLiteral(v)); } else if (typeof v == 'boolean') { return t.objectProperty(t.identifier(k), t.booleanLiteral(v)); } }); return t.objectExpression(props); } else { throw new Error('Unable to parse array'); } }) ); } else if (t.isAnyTypeAnnotation(elt)) { literalValue = t.arrayExpression([]); } else if (t.isBooleanTypeAnnotation(elt)) { literalValue = t.booleanLiteral(parsers.booleanParser(value)); } else if (t.isGenericTypeAnnotation(elt)) { const type = elt.typeAnnotation.id.name; if (type == 'NumberOrBoolean') { literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value)); } if (nestedOptionTypes.includes(type)) { const object = parsers.objectParser(value); const props = Object.keys(object).map(key => { return t.objectProperty(key, object[value]); }); literalValue = t.objectExpression(props); } if (type == 'ProtectedFields') { const prop = t.objectProperty( t.stringLiteral('_User'), t.objectPattern([ t.objectProperty(t.stringLiteral('*'), t.arrayExpression([t.stringLiteral('email')])), ]) ); literalValue = t.objectExpression([prop]); } } return literalValue; } function inject(t, list) { let comments = ''; const results = list .map(elt => { if (!elt.name) { return; } const props = ['env', 'help'] .map(key => { if (elt[key]) { return t.objectProperty(t.stringLiteral(key), t.stringLiteral(elt[key])); } }) .filter(e => e !== undefined); if (elt.required) { props.push(t.objectProperty(t.stringLiteral('required'), t.booleanLiteral(true))); } const action = mapperFor(elt, t); if (action) { props.push(t.objectProperty(t.stringLiteral('action'), action)); } if (elt.defaultValue) { let parsedValue = parseDefaultValue(elt, elt.defaultValue, t); if (!parsedValue) { for (const type of elt.typeAnnotation.types) { elt.type = type.type; parsedValue = parseDefaultValue(elt, elt.defaultValue, t); if (parsedValue) { break; } } } if (parsedValue) { props.push(t.objectProperty(t.stringLiteral('default'), parsedValue)); } else { throw new Error(`Unable to parse value for ${elt.name} `); } } let type = elt.type.replace('TypeAnnotation', ''); if (type === 'Generic') { type = elt.typeAnnotation.id.name; } if (type === 'Array') { type = elt.typeAnnotation.elementType.id ? `${elt.typeAnnotation.elementType.id.name}[]` : `${elt.typeAnnotation.elementType.type.replace('TypeAnnotation', '')}[]`; } if (type === 'NumberOrBoolean') { type = 'Number|Boolean'; } if (type === 'NumberOrString') { type = 'Number|String'; } if (type === 'Adapter') { const adapterType = elt.typeAnnotation.typeParameters.params[0].id.name; type = `Adapter<${adapterType}>`; } if (type === 'StringOrStringArray') { type = 'String|String[]'; } comments += ` * @property {${type}} ${elt.name} ${elt.help}\n`; const obj = t.objectExpression(props); return t.objectProperty(t.stringLiteral(elt.name), obj); }) .filter(elt => { return elt != undefined; }); return { results, comments }; } const makeRequire = function (variableName, module, t) { const decl = t.variableDeclarator( t.identifier(variableName), t.callExpression(t.identifier('require'), [t.stringLiteral(module)]) ); return t.variableDeclaration('var', [decl]); }; let docs = ``; const plugin = function (babel) { const t = babel.types; const moduleExports = t.memberExpression(t.identifier('module'), t.identifier('exports')); return { visitor: { ImportDeclaration: function (path) { path.remove(); }, Program: function (path) { // Inject the parser's loader path.unshiftContainer('body', makeRequire('parsers', './parsers', t)); }, ExportDeclaration: function (path) { // Export declaration on an interface if ( path.node && path.node.declaration && path.node.declaration.type == 'InterfaceDeclaration' ) { const { results, comments } = inject(t, doInterface(path.node.declaration)); const id = path.node.declaration.id.name; const exports = t.memberExpression(moduleExports, t.identifier(id)); docs += `/**\n * @interface ${id}\n${comments} */\n\n`; path.replaceWith(t.assignmentExpression('=', exports, t.objectExpression(results))); } }, }, }; }; const auxiliaryCommentBefore = ` **** GENERATED CODE **** This code has been generated by resources/buildConfigDefinitions.js Do not edit manually, but update Options/index.js `; const babel = require('@babel/core'); const res = babel.transformFileSync('./src/Options/index.js', { plugins: [plugin, '@babel/transform-flow-strip-types'], babelrc: false, auxiliaryCommentBefore, sourceMaps: false, }); require('fs').writeFileSync('./src/Options/Definitions.js', res.code + '\n'); require('fs').writeFileSync('./src/Options/docs.js', docs);