Skip to content

Commit

Permalink
feat: [add-circuit-breaker-to-api] Allows addition of circuit breaker…
Browse files Browse the repository at this point in the history
… to api calls
  • Loading branch information
xadil committed May 5, 2024
1 parent 7f7a6b6 commit 7cc1727
Show file tree
Hide file tree
Showing 13 changed files with 22,624 additions and 7,729 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
node_modules
coverage
.eslintcache
.sdkmanrc
sample-apis/generated
56 changes: 41 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,31 @@ It's error-prone, slow and boring to create all those connectors manually. Most

## Usage
```
Usage: chimp-datasources-generator [options]
[command]
Commands:
create <directory> <api-location> [custom-data-source-import] Create the datasources for api, pass either a URL or a yaml/json file.
Use http/https when pointing to a URL and relative location when pointing to a file
Examples:
chimp-datasources-generator create ./generated/external-apis https://domain.com/v3/api-docs.yaml
chimp-datasources-generator create ./generated/external-apis ./api-docs.yaml
You can also specify your own custom data source import:
chimp-datasources-generator create ./generated/external-apis ./api-docs.yaml "@app/apis/DataSource#DataSource" "@app/apis/DataSource#DataSource" will use an import of:
import { DataSource } from "@app/apis/DataSource"
For a default import just use the path:
"@app/apis/BaseDataSource"
Usage: chimp-datasources-generator create [options] <directory> <api-location>
Generate RestDatasource from swagger specs
Examples:
chimp-datasources-generator create ./generated/external-apis https://domain.com/v3/api-docs.yaml --withCircuitBreaker
chimp-datasources-generator create ./generated/external-apis ./api-docs.yaml
chimp-datasources-generator create ./generated/external-apis ./api-docs.yaml "@app/apis/DataSource#DataSource"
Arguments:
directory Directory for the generated source files.
api-location Location/URL for the json/yaml swagger specs. Use http/https when pointing to a URL and relative location when pointing to a file.
Options:
-ds, --dataSourceImport <string> You can also specify your own custom data source import.
"@app/apis/DataSource#DataSource" will use an import of:
import { DataSource } from "@app/apis/DataSource"
For a default import just use the path:
"@app/apis/BaseDataSource"
-cb, --withCircuitBreaker <boolean> Add circuit breakers for api calls. (default: false)
-cbc, --circuitBreakerConfig <string> You can also specify your own custom circuit breaker import per service.
"@app/circuit-breakers/CircuitBreakerConfig#getServiceCircuitBreaker" will use an import of:
import { getServiceCircuitBreaker } from "@app/circuit-breakers/CircuitBreakerConfig"
-h, --help display help for command
```

In your code create dataSources.ts file like this one:
Expand Down Expand Up @@ -58,6 +68,22 @@ import { DataSources } from "@app/dataSources";
export type GqlContext = { dataSources: DataSources };
```

For External CircuitBreaker Config file add a function with signature with single arg e.g. (serviceName: string). At run time it allows to have circuit breaker config different for each service. The service name passed is the same as the spec file name.
```typescript
import { circuitBreaker, handleWhen, SamplingBreaker } from 'cockatiel'

export const getServiceCircuitBreaker = (serviceName: string) => {
const defaultBreakerPolicy = {
halfOpenAfter: 3000,
breaker: new SamplingBreaker({ threshold: 0.5, duration: 3 * 1000, minimumRps: 1 }),
}
return circuitBreaker(
handleWhen((err: any) => err.extensions.code.startsWith('5')),
defaultBreakerPolicy,
)
}
```

## How

We are using the swagger-codegen-cli with custom templates and a bit of extra scripting.
81 changes: 46 additions & 35 deletions lib/chimp-datasources-generator.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,48 @@
#!/usr/bin/env node

const shelljs = require("shelljs");
const program = require("commander");
const { Command } = require("commander");

const program = new Command();
const path = require("path");
const extractor = require("./helpers/customDataSourceExtractor");
const buildSpecGenCommandWithOptions = require("./helpers/buildSpecGenCommandWithOptions");

program
.command("create <directory> <api-location> [custom-data-source-import]")
.name("chimp-datasources-generator")
.command("create")
.argument("<directory>", "Directory for the generated source files.")
.argument(
"<api-location>",
"Location/URL for the json/yaml swagger specs. Use http/https when pointing to a URL and relative location when pointing to a file."
)
.option(
"-ds, --dataSourceImport <string>",
"You can also specify your own custom data source import. \n" +
' "@app/apis/DataSource#DataSource" will use an import of:\n' +
' import { DataSource } from "@app/apis/DataSource"\n' +
"For a default import just use the path:\n" +
' "@app/apis/BaseDataSource"'
)
.option(
"-cb, --withCircuitBreaker <boolean>",
"Add circuit breakers for api calls.",
false
)
.option(
"-cbc, --circuitBreakerConfig <string>",
"You can also specify your own custom circuit breaker import per service. \n" +
' "@app/circuit-breakers/CircuitBreakerConfig#getServiceCircuitBreaker" will use an import of:\n' +
' import { getServiceCircuitBreaker } from "@app/circuit-breakers/CircuitBreakerConfig"\n'
)
.description(
"Create the datasources for api, pass either a URL or a yaml/json file. \n" +
" Use http/https when pointing to a URL and relative location when pointing to a file \n" +
" Examples: \n" +
" chimp-datasources-generator create ./generated/external-apis https://domain.com/v3/api-docs.yaml\n" +
" chimp-datasources-generator create ./generated/external-apis ./api-docs.yaml\n" +
" You can also specify your own custom data source import:\n" +
'chimp-datasources-generator create ./generated/external-apis ./api-docs.yaml "@app/apis/DataSource#DataSource"\n' +
' "@app/apis/DataSource#DataSource" will use an import of:\n' +
' import { DataSource } from "@app/apis/DataSource"\n' +
" For a default import just use the path:\n" +
' "@app/apis/BaseDataSource"' +
""
"Generate RestDatasource from swagger specs\n" +
"Examples: \n" +
" chimp-datasources-generator create ./generated/external-apis https://domain.com/v3/api-docs.yaml --withCircuitBreaker\n" +
" chimp-datasources-generator create ./generated/external-apis ./api-docs.yaml\n" +
' chimp-datasources-generator create ./generated/external-apis ./api-docs.yaml "@app/apis/DataSource#DataSource"\n' +
' chimp-datasources-generator create ./generated/external-apis ./api-docs.yaml -cbc "@app/circuit-breakers/CircuitBreakerConfig#getServiceCircuitBreaker"\n'
)
.action(function (directory, location, dataSourceImport) {
.action((directory, location, options) => {
const API_DIR = directory;
const API_URL = location;

Expand All @@ -34,10 +55,17 @@ program
const pathToSwagger = path.join(__dirname, "./swagger-codegen-cli.jar");
const pathToTemplate = path.join(__dirname, "./typescript-fetch");
// // shelljs.rm("-rf", API_DIR);
const serviceName = path.parse(resolvedPath).base;
shelljs.mkdir("-p", resolvedPath);
shelljs.exec(
`java -jar ${pathToSwagger} generate -l typescript-fetch --template-dir ${pathToTemplate} -i ${resolvedAPI} -o ${resolvedPath}`
const shellCommand = buildSpecGenCommandWithOptions(
pathToSwagger,
pathToTemplate,
resolvedAPI,
resolvedPath,
serviceName,
options
);
shelljs.exec(shellCommand);

shelljs.sed("-i", /this.DELETE/, "this.delete", `${resolvedPath}/api.ts`);
shelljs.sed("-i", /this.GET/, "this.get", `${resolvedPath}/api.ts`);
Expand All @@ -51,23 +79,6 @@ program
"extends GenericObject",
`${resolvedPath}/api.ts`
);

if (dataSourceImport) {
const { dataSourceName, importString } = extractor(dataSourceImport);

shelljs.sed(
"-i",
/\/\/ {\$CustomDataSourcePlaceholder}/,
importString,
`${resolvedPath}/api.ts`
);
shelljs.sed(
"-i",
/extends RESTDataSource/,
`extends ${dataSourceName}`,
`${resolvedPath}/api.ts`
);
}
});

program.parse(process.argv);
Expand Down
52 changes: 52 additions & 0 deletions lib/helpers/buildSpecGenCommandWithOptions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const extractDataSource = require("./customDataSourceExtractor");
const extractCircuitBreakerConfig = require("./extractCircuitBreakerConfig");

const buildSpecGenCommandWithOptions = (
pathToSwaggerJar,
pathToTemplate,
pathToSpecFile,
codeOutputFolder,
serviceName,
options
) => {
const additionalProps = [];
let shellCommand = `java -jar ${pathToSwaggerJar} generate -l typescript-fetch --template-dir ${pathToTemplate} -i ${pathToSpecFile} -o ${codeOutputFolder}`;

additionalProps.push(`serviceName=${serviceName}`);

if (options.withCircuitBreaker) {
additionalProps.push("withCircuitBreaker=true");
}

if (options.circuitBreakerConfig) {
const config = extractCircuitBreakerConfig(options.circuitBreakerConfig);
if (config) {
const { circuitBreakerImportString, circuitBreakerConfigFn } = config;
additionalProps.push(
`circuitBreakerImportString=${circuitBreakerImportString}`
);
additionalProps.push(
`circuitBreakerConfigFn="${circuitBreakerConfigFn}"`
);
}
}

if (options.dataSourceImport) {
const { dataSourceName, importString } = extractDataSource(
options.dataSourceImport
);
additionalProps.push(`dataSourceName=${dataSourceName}`);
additionalProps.push(`dataSourceImportString="${importString}"`);
} else {
additionalProps.push("dataSourceName=RESTDataSource");
}

if (additionalProps.length > 0) {
shellCommand = shellCommand.concat(
` --additional-properties=${additionalProps.toString()}`
);
}
return shellCommand;
};

module.exports = buildSpecGenCommandWithOptions;
73 changes: 73 additions & 0 deletions lib/helpers/buildSpecGenCommandWithOptions.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const buildSpecGenCommandWithOptions = require("./buildSpecGenCommandWithOptions");

describe("Build spec gen command test", () => {
const pathToSpecFile = "/spec/input.json";
const codeOutputFolder = "/output";
const pathToTemplate = "/template";
const pathToSwaggerJar = "/swagger.jar";
const baseCommand =
"java -jar /swagger.jar generate -l typescript-fetch --template-dir /template -i /spec/input.json -o /output";
test("should create command for no options passed", () => {
const options = {};
const result = buildSpecGenCommandWithOptions(
pathToSwaggerJar,
pathToTemplate,
pathToSpecFile,
codeOutputFolder,
"test-service-name",
options
);
expect(result).toBe(
`${baseCommand} --additional-properties=serviceName=test-service-name,dataSourceName=RESTDataSource`
);
});

test("should create command for custom datasource passed", () => {
const options = {
dataSourceImport: "@app/apis/DataSource#CustomDataSource",
};
const result = buildSpecGenCommandWithOptions(
pathToSwaggerJar,
pathToTemplate,
pathToSpecFile,
codeOutputFolder,
"test-service-name",
options
);
expect(result).toBe(
`${baseCommand} --additional-properties=serviceName=test-service-name,dataSourceName=CustomDataSource,dataSourceImportString="@app/apis/DataSource"`
);
});

test("should create command for custom datasource passed", () => {
const options = {
dataSourceImport: "@app/apis/DataSource#CustomDataSource",
};
const result = buildSpecGenCommandWithOptions(
pathToSwaggerJar,
pathToTemplate,
pathToSpecFile,
codeOutputFolder,
"test-service-name",
options
);
expect(result).toBe(
`${baseCommand} --additional-properties=serviceName=test-service-name,dataSourceName=CustomDataSource,dataSourceImportString="@app/apis/DataSource"`
);
});

test("should create command for circuit breaker options", () => {
const options = { withCircuitBreaker: "--withCircuitBreaker" };
const result = buildSpecGenCommandWithOptions(
pathToSwaggerJar,
pathToTemplate,
pathToSpecFile,
codeOutputFolder,
"test-service-name",
options
);
expect(result).toBe(
`${baseCommand} --additional-properties=serviceName=test-service-name,withCircuitBreaker=true,dataSourceName=RESTDataSource`
);
});
});
8 changes: 5 additions & 3 deletions lib/helpers/customDataSourceExtractor.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
module.exports = (dataSourcePath) => {
const extractDataSource = (dataSourcePath) => {
const split = dataSourcePath.split("#");

if (split.length > 1) {
return {
importString: `import { ${split[1]} } from "${split[0]}"`,
importString: `${split[0]}`,
dataSourceName: split[1],
};
}
return {
importString: `import DataSource from "${split[0]}"`,
importString: `${split[0]}`,
dataSourceName: "DataSource",
};
};

module.exports = extractDataSource;
10 changes: 5 additions & 5 deletions lib/helpers/customDataSourceExtractor.spec.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
const extractor = require("./customDataSourceExtractor");
const extractDataSource = require("./customDataSourceExtractor");

test("named import", () => {
const dataSourcePath = "@app/apis/DataSource#CustomDataSource";
expect(extractor(dataSourcePath)).toEqual({
importString: 'import { CustomDataSource } from "@app/apis/DataSource"',
expect(extractDataSource(dataSourcePath)).toEqual({
importString: "@app/apis/DataSource",
dataSourceName: "CustomDataSource",
});
});

test("default import", () => {
const dataSourcePath = "@app/MyDataSource";
expect(extractor(dataSourcePath)).toEqual({
importString: 'import DataSource from "@app/MyDataSource"',
expect(extractDataSource(dataSourcePath)).toEqual({
importString: "@app/MyDataSource",
dataSourceName: "DataSource",
});
});
13 changes: 13 additions & 0 deletions lib/helpers/extractCircuitBreakerConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const extractCircuitBreakerConfig = (circuitBreakerConfig = "") => {
const split = circuitBreakerConfig.split("#");

if (split.length > 1) {
return {
circuitBreakerImportString: `${split[0]}`,
circuitBreakerConfigFn: split[1],
};
}
return undefined;
};

module.exports = extractCircuitBreakerConfig;
20 changes: 20 additions & 0 deletions lib/helpers/extractCircuitBreakerConfig.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const extractCircuitBreakerConfig = require("./extractCircuitBreakerConfig");

describe("extract circuit breaker config", () => {
test("should return imports for circuit breaker config", () => {
const dataSourcePath =
"@app/circuit-breakers/CircuitBreakerConfig#getServiceCircuitBreaker";
expect(extractCircuitBreakerConfig(dataSourcePath)).toEqual({
circuitBreakerImportString: "@app/circuit-breakers/CircuitBreakerConfig",
circuitBreakerConfigFn: "getServiceCircuitBreaker",
});
});

test("should return undefined", () => {
let dataSourcePath;
expect(extractCircuitBreakerConfig(dataSourcePath)).not.toBeDefined();

dataSourcePath = "undefined";
expect(extractCircuitBreakerConfig(dataSourcePath)).not.toBeDefined();
});
});
Loading

0 comments on commit 7cc1727

Please sign in to comment.