Skip to content

Commit

Permalink
[nan-1752] add enforce proxy configuration logic
Browse files Browse the repository at this point in the history
  • Loading branch information
khaliqgant committed Sep 19, 2024
1 parent cff3c83 commit 232332a
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 158 deletions.
149 changes: 45 additions & 104 deletions src/rules/enforce-proxy-configuration-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,111 +3,52 @@ import { describe, it } from 'vitest';
import enforceProxyConfigurationType from './enforce-proxy-configuration-type';

const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: { ecmaVersion: 2018, sourceType: 'module' },
});

describe('enforce-proxy-configuration-type', () => {
it('should pass valid cases and fail invalid cases', () => {
ruleTester.run('enforce-proxy-configuration-type', enforceProxyConfigurationType, {
valid: [
{
code: `
const config: ProxyConfiguration = {
endpoint: 'api.xro/2.0/Contacts',
headers: { 'xero-tenant-id': tenant_id },
params: { summarizeErrors: 'false' },
data: { Contacts: input.map(toXeroContact) }
};
const res = await nango.post(config);
`,
},
{
code: `
let config: ProxyConfiguration;
config = {
endpoint: 'api.xro/2.0/Contacts',
headers: { 'xero-tenant-id': tenant_id },
params: { summarizeErrors: 'false' },
data: { Contacts: input.map(toXeroContact) }
};
const res = await nango.get(config);
`,
},
{
code: `
const res = await nango.put({ endpoint: 'api.example.com', data: {} });
`,
},
],
invalid: [
{
code: `
const config = {
endpoint: 'api.xro/2.0/Contacts',
headers: { 'xero-tenant-id': tenant_id },
params: { summarizeErrors: 'false' },
data: { Contacts: input.map(toXeroContact) }
};
const res = await nango.post(config);
`,
errors: [{ message: 'Configuration object for Nango API calls should be typed as ProxyConfiguration' }],
output: `
const config: ProxyConfiguration = {
endpoint: 'api.xro/2.0/Contacts',
headers: { 'xero-tenant-id': tenant_id },
params: { summarizeErrors: 'false' },
data: { Contacts: input.map(toXeroContact) }
};
const res = await nango.post(config);
`,
},
{
code: `
let config = {
endpoint: 'api.xro/2.0/Contacts',
headers: { 'xero-tenant-id': tenant_id },
params: { summarizeErrors: 'false' },
data: { Contacts: input.map(toXeroContact) }
};
const res = await nango.get(config);
`,
errors: [{ message: 'Configuration object for Nango API calls should be typed as ProxyConfiguration' }],
output: `
let config: ProxyConfiguration = {
endpoint: 'api.xro/2.0/Contacts',
headers: { 'xero-tenant-id': tenant_id },
params: { summarizeErrors: 'false' },
data: { Contacts: input.map(toXeroContact) }
};
const res = await nango.get(config);
`,
},
{
code: `
var config = {
endpoint: 'api.xro/2.0/Contacts',
headers: { 'xero-tenant-id': tenant_id },
params: { summarizeErrors: 'false' },
data: { Contacts: input.map(toXeroContact) }
};
const res = await nango.proxy(config);
`,
errors: [{ message: 'Configuration object for Nango API calls should be typed as ProxyConfiguration' }],
output: `
var config: ProxyConfiguration = {
endpoint: 'api.xro/2.0/Contacts',
headers: { 'xero-tenant-id': tenant_id },
params: { summarizeErrors: 'false' },
data: { Contacts: input.map(toXeroContact) }
};
const res = await nango.proxy(config);
`,
},
],
describe('enforce-proxy-configuration-type-tests', () => {
it('should pass valid cases and fail invalid cases', () => {
ruleTester.run('enforce-proxy-configuration-type', enforceProxyConfigurationType, {
valid: [
{
code: `
import type { NangoSync, Account, ProxyConfiguration } from '../../models';
const config: ProxyConfiguration = {
endpoint: 'api.xro/2.0/Accounts',
headers: { 'xero-tenant-id': tenant_id },
params: { order: 'UpdatedDateUTC DESC' },
retries: 10
};
`,
},
],
invalid: [
{
code: `
import type { NangoSync, Account } from '../../models';
const config = {
endpoint: 'api.xro/2.0/Accounts',
headers: { 'xero-tenant-id': tenant_id },
params: { order: 'UpdatedDateUTC DESC' },
retries: 10
};
`,
errors: [
{ message: 'ProxyConfiguration type should be imported from ../../models' },
{ message: 'ProxyConfiguration type should be used for Nango API call configurations' },
],
output: `
import type { NangoSync, Account, ProxyConfiguration } from '../../models';
const config: ProxyConfiguration = {
endpoint: 'api.xro/2.0/Accounts',
headers: { 'xero-tenant-id': tenant_id },
params: { order: 'UpdatedDateUTC DESC' },
retries: 10
};
`,
},
],
});
});
});
});
113 changes: 59 additions & 54 deletions src/rules/enforce-proxy-configuration-type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Rule } from 'eslint';
import { Node, CallExpression, Identifier, VariableDeclarator } from 'estree';
import { ImportDeclaration, VariableDeclaration, VariableDeclarator, Identifier, ObjectExpression, Property } from 'estree';

const enforceProxyConfigurationType: Rule.RuleModule = {
meta: {
Expand All @@ -12,66 +12,71 @@ const enforceProxyConfigurationType: Rule.RuleModule = {
fixable: 'code',
schema: [],
},
create(context: Rule.RuleContext) {
const sourceCode = context.getSourceCode();
create(context) {
let hasProxyConfigurationImport = false;
let configVariableName: string | null = null;
let importNode: ImportDeclaration | null = null;
let configNode: VariableDeclaration | null = null;

return {
CallExpression(node: Node) {
if (isNangoApiCall(node)) {
const options = node.arguments[0];

if (options && options.type === 'Identifier') {
const scope = sourceCode.getScope(node);
const variable = scope.variables.find(v => v.name === options.name);
if (variable && variable.defs[0] && variable.defs[0].node.type === 'VariableDeclarator') {
const declarator = variable.defs[0].node;
if (!hasProxyConfigurationType(declarator, context)) {
context.report({
node: declarator,
message: 'Configuration object for Nango API calls should be typed as ProxyConfiguration',
fix(fixer) {
const declarationToken = sourceCode.getFirstToken(declarator.parent);
if (declarationToken && (declarationToken.value === 'const' || declarationToken.value === 'let' || declarationToken.value === 'var')) {
return fixer.insertTextAfter(declarator.id, ': ProxyConfiguration');
}
return null;
}
});
}
}
ImportDeclaration(node: ImportDeclaration) {
if (node.source.value === '../../models') {
importNode = node;
hasProxyConfigurationImport = node.specifiers.some(
(specifier) => specifier.type === 'ImportSpecifier' &&
'imported' in specifier &&
specifier.imported.type === 'Identifier' &&
specifier.imported.name === 'ProxyConfiguration'
);
}
},
VariableDeclaration(node: VariableDeclaration) {
const declarator = node.declarations[0] as VariableDeclarator;
if (declarator && declarator.type === 'VariableDeclarator' &&
declarator.id.type === 'Identifier' && declarator.init &&
declarator.init.type === 'ObjectExpression') {
const properties = declarator.init.properties;
if (properties.some((prop): prop is Property =>
prop.type === 'Property' &&
prop.key.type === 'Identifier' &&
prop.key.name === 'endpoint')) {
configVariableName = declarator.id.name;
configNode = node;
}
}
},
};
},
};

function isNangoApiCall(node: Node): node is CallExpression {
return (
node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'nango' &&
node.callee.property.type === 'Identifier' &&
['get', 'post', 'put', 'patch', 'delete', 'proxy'].includes(node.callee.property.name)
);
}
'Program:exit'() {
if (configVariableName && !hasProxyConfigurationImport && importNode && configNode) {
context.report({
node: context.getSourceCode().ast,
message: 'ProxyConfiguration type should be imported and used for Nango API call configurations',
fix(fixer) {
const fixes = [];

function hasProxyConfigurationType(node: VariableDeclarator, context: Rule.RuleContext): boolean {
const sourceCode = context.getSourceCode();
const idToken = sourceCode.getFirstToken(node.id);
if (!idToken) return false;
if (importNode && importNode.specifiers.length > 0) {
fixes.push(fixer.insertTextAfter(
importNode.specifiers[importNode.specifiers.length - 1],
', ProxyConfiguration'
));
}

const nextToken = sourceCode.getTokenAfter(idToken);
if (!nextToken) return false;
if (configNode && configNode.declarations.length > 0) {
const configDeclarator = configNode.declarations[0] as VariableDeclarator;
if (configDeclarator && configDeclarator.id.type === 'Identifier') {
fixes.push(fixer.insertTextAfter(
configDeclarator.id,
': ProxyConfiguration'
));
}
}

const tokenAfterColon = sourceCode.getTokenAfter(nextToken);

return (
nextToken.type === 'Punctuator' &&
nextToken.value === ':' &&
tokenAfterColon?.value === 'ProxyConfiguration'
);
}
return fixes;
},
});
}
},
};
},
};

export default enforceProxyConfigurationType;

0 comments on commit 232332a

Please sign in to comment.