From 1e9adf90cdaa5b84f22931d836b9dfde260a4b68 Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 27 Jan 2022 07:34:13 +0000 Subject: [PATCH 1/4] Enable unsafe keep a json specification when compeller is invoked --- __tests__/compiler.test.ts | 16 ++++++++ __tests__/tmp/openapi.json | 34 ++++++++++++++++ _templates/compeller/new/compeller.ejs.t | 7 ++++ package.json | 1 + src/compeller.ts | 50 ++++++++++++++++++++++-- src/file-utils/write-specification.ts | 17 ++++++++ yarn.lock | 2 +- 7 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 __tests__/tmp/openapi.json create mode 100644 _templates/compeller/new/compeller.ejs.t create mode 100644 src/file-utils/write-specification.ts diff --git a/__tests__/compiler.test.ts b/__tests__/compiler.test.ts index d25a4e0..c2b5cd5 100644 --- a/__tests__/compiler.test.ts +++ b/__tests__/compiler.test.ts @@ -1,3 +1,4 @@ +import { join } from 'path'; import { compeller } from '../src'; const spec = { @@ -47,5 +48,20 @@ describe('API Compiler tests', () => { statusCode: '200', }); }); + + it('keeps a local specification json when true', () => { + const stuff = compeller(spec, { + jsonSpecFile: join(__dirname, 'tmp', 'openapi.json'), + }); + + const { response } = stuff('/test', 'get'); + + const resp = response('200', { name: 'Type-safe reply' }); + + expect(resp).toEqual({ + body: '{"name":"Type-safe reply"}', + statusCode: '200', + }); + }); }); }); diff --git a/__tests__/tmp/openapi.json b/__tests__/tmp/openapi.json new file mode 100644 index 0000000..d9aa47b --- /dev/null +++ b/__tests__/tmp/openapi.json @@ -0,0 +1,34 @@ +{ + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "openapi": "3.1.0", + "paths": { + "/test": { + "get": { + "responses": { + "200": { + "description": "Test response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "name" + ] + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/_templates/compeller/new/compeller.ejs.t b/_templates/compeller/new/compeller.ejs.t new file mode 100644 index 0000000..d5dd511 --- /dev/null +++ b/_templates/compeller/new/compeller.ejs.t @@ -0,0 +1,7 @@ +--- +to: <%= directory %>/openapi/compeller.ts +--- +import { compeller } from 'compeller' +import { OpenAPISpecification } from './spec' + +cont compelled = compeller(OpenAPISpecification) diff --git a/package.json b/package.json index d8ba59f..8a82822 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@swc/jest": "^0.2.17", "@tsconfig/node14": "^1.0.1", "@types/jest": "^27.4.0", + "@types/node": "^17.0.12", "embedme": "^1.22.0", "husky": "^7.0.0", "jest": "^27.4.7", diff --git a/src/compeller.ts b/src/compeller.ts index aaf5da5..59cfb5b 100644 --- a/src/compeller.ts +++ b/src/compeller.ts @@ -1,6 +1,40 @@ import Ajv, { JSONSchemaType } from 'ajv'; import { FromSchema } from 'json-schema-to-ts'; import { OpenAPIObject } from 'openapi3-ts'; +import { writeSpecification } from './file-utils/write-specification'; + +export interface ICompellerOptions { + /** + * The content type for the responses, currently only 'application/json' is + * supported + */ + contentType?: string; + /** + * If boolean the default file location will be used + * + * @default false + */ + jsonSpecFile?: string | boolean; + /** + * Bind the relative path where compeller is used, to enable storing the + * specification along side the compeller entity + */ + relativePath?: string; +} + +/** + * For now this is a mask on the OpenAPIObject, but later some fields will + * become mandatory. + * + * This is to enforce configuration, and remove the option for some fields to + * be any type, which is not the desired behavior for compeller. + */ +export interface ICompellerOpenAPIObject extends OpenAPIObject {} + +const DEFAULT_OPTIONS: ICompellerOptions = { + contentType: 'application/json', + jsonSpecFile: false, +}; /** * The open API Compiler will take in an OpenAPI specification and return type- @@ -10,17 +44,23 @@ import { OpenAPIObject } from 'openapi3-ts'; * classes that define the Components and Schema's of the paths. * * @param {OpenAPIObject} spec - The OpenAPI specification document - * @param {string} contentType - The content type of requests and responses - * @default 'application/json' + * @param {ICompellerOptions} options - Compeller options * @returns */ export const compeller = < - T extends OpenAPIObject, + T extends ICompellerOpenAPIObject, U extends string = 'application/json' >( spec: T, - contentType = 'application/json' + { + contentType = 'application/json', + jsonSpecFile = false, + }: ICompellerOptions = DEFAULT_OPTIONS ) => { + if (jsonSpecFile) { + writeSpecification(jsonSpecFile, spec); + } + return < P extends keyof T['paths'], M extends keyof T['paths'][P], @@ -32,6 +72,8 @@ export const compeller = < const path = route as string; /** + * Build a response object for the API with the required status and body + * format * * @param statusCode The response code that the API returns * @param body The JSON body for the API that is associated with that diff --git a/src/file-utils/write-specification.ts b/src/file-utils/write-specification.ts new file mode 100644 index 0000000..dda03b2 --- /dev/null +++ b/src/file-utils/write-specification.ts @@ -0,0 +1,17 @@ +import { writeFileSync } from 'fs'; +import { join } from 'path'; +import { ICompellerOpenAPIObject, ICompellerOptions } from '../compeller'; + +export const writeSpecification = ( + jsonSpecFile: ICompellerOptions['jsonSpecFile'], + spec: ICompellerOpenAPIObject +) => { + if (typeof jsonSpecFile === 'string') { + writeFileSync(jsonSpecFile, JSON.stringify(spec, undefined, 2)); + } else { + writeFileSync( + join(process.cwd(), 'openapi.json'), + JSON.stringify(spec, undefined, 2) + ); + } +}; diff --git a/yarn.lock b/yarn.lock index 2ace92f..b120ceb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -838,7 +838,7 @@ dependencies: "@types/node" "*" -"@types/node@*": +"@types/node@*", "@types/node@^17.0.12": version "17.0.12" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.12.tgz#f7aa331b27f08244888c47b7df126184bc2339c5" integrity sha512-4YpbAsnJXWYK/fpTVFlMIcUIho2AYCi4wg5aNPrG1ng7fn/1/RZfCIpRCiBX+12RVa34RluilnvCqD+g3KiSiA== From 0c94d782e96f63a03c051aa852fa970e8f677e54 Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 27 Jan 2022 08:11:33 +0000 Subject: [PATCH 2/4] Update to keep a specification document not throwing --- .gitignore | 2 ++ __tests__/tmp/openapi.json | 34 --------------------------- _templates/compeller/new/spec.ejs.t | 2 +- jest.config.js | 3 --- src/file-utils/write-specification.ts | 22 ++++++++++++----- 5 files changed, 19 insertions(+), 44 deletions(-) delete mode 100644 __tests__/tmp/openapi.json diff --git a/.gitignore b/.gitignore index f9e4ec2..39d3c18 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ node_modules dist *.tgz + +tmp diff --git a/__tests__/tmp/openapi.json b/__tests__/tmp/openapi.json deleted file mode 100644 index d9aa47b..0000000 --- a/__tests__/tmp/openapi.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "info": { - "title": "Test API", - "version": "1.0.0" - }, - "openapi": "3.1.0", - "paths": { - "/test": { - "get": { - "responses": { - "200": { - "description": "Test response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "name" - ] - } - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/_templates/compeller/new/spec.ejs.t b/_templates/compeller/new/spec.ejs.t index 5a57896..dfaa8bc 100644 --- a/_templates/compeller/new/spec.ejs.t +++ b/_templates/compeller/new/spec.ejs.t @@ -10,7 +10,7 @@ export const OpenAPISpecification = { }, openapi: '3.1.0', paths: { - 'v1//version': { + 'v1/version': { get: { responses: { '200': { diff --git a/jest.config.js b/jest.config.js index def4cb4..3ad91f8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,7 +5,4 @@ module.exports = { }, testEnvironment: 'node', modulePathIgnorePatterns: ['/.*/fixtures/', '/example/*'], - moduleNameMapper: { - '^compeller/(.*)$': '/src/$1', - }, }; diff --git a/src/file-utils/write-specification.ts b/src/file-utils/write-specification.ts index dda03b2..b931cac 100644 --- a/src/file-utils/write-specification.ts +++ b/src/file-utils/write-specification.ts @@ -6,12 +6,22 @@ export const writeSpecification = ( jsonSpecFile: ICompellerOptions['jsonSpecFile'], spec: ICompellerOpenAPIObject ) => { - if (typeof jsonSpecFile === 'string') { - writeFileSync(jsonSpecFile, JSON.stringify(spec, undefined, 2)); - } else { - writeFileSync( - join(process.cwd(), 'openapi.json'), - JSON.stringify(spec, undefined, 2) + let fileName; + + try { + fileName = + typeof jsonSpecFile === 'string' + ? jsonSpecFile + : join(process.cwd(), 'openapi.json'); + + writeFileSync(fileName, JSON.stringify(spec, undefined, 2)); + } catch (err) { + console.warn( + 'Compeller could not write your schema to a file and has rescued to prevent unwanted runtime side-effects', + { + fileName, + error: err, + } ); } }; From 7c6f2c8103189f605e84fa6fc8d8b34ff3cf48bf Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 27 Jan 2022 08:12:12 +0000 Subject: [PATCH 3/4] Add file utils tests --- __tests__/file-utils/write-specification.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 __tests__/file-utils/write-specification.test.ts diff --git a/__tests__/file-utils/write-specification.test.ts b/__tests__/file-utils/write-specification.test.ts new file mode 100644 index 0000000..c6ccb6e --- /dev/null +++ b/__tests__/file-utils/write-specification.test.ts @@ -0,0 +1,14 @@ +import { writeSpecification } from '../../src/file-utils/write-specification'; + +describe('writeSpecification', () => { + it('with directory and not file', () => { + writeSpecification('./tmp', { + info: { + title: 'spec', + version: '0.0.1', + }, + openapi: '3.1.0', + paths: {}, + }); + }); +}); From 6b558bae69cc010e1a5c72d4056a5bdf348eece9 Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 27 Jan 2022 08:17:57 +0000 Subject: [PATCH 4/4] Guard against production side effects in compeller --- src/file-utils/write-specification.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/file-utils/write-specification.ts b/src/file-utils/write-specification.ts index b931cac..4696f86 100644 --- a/src/file-utils/write-specification.ts +++ b/src/file-utils/write-specification.ts @@ -2,17 +2,27 @@ import { writeFileSync } from 'fs'; import { join } from 'path'; import { ICompellerOpenAPIObject, ICompellerOptions } from '../compeller'; +/** + * Write a JSON object to file if a valid path if provided + * + * @default `${__dirname}/openapi.json` + * + * @param jsonSpecFile If true a default path will be used as provided, other provide a fully qualified path + * @param spec The OpenAPI specification object + */ export const writeSpecification = ( jsonSpecFile: ICompellerOptions['jsonSpecFile'], spec: ICompellerOpenAPIObject ) => { let fileName; + if (['production', 'prod'].includes(process?.env?.NODE_ENV || '')) return; + try { fileName = typeof jsonSpecFile === 'string' ? jsonSpecFile - : join(process.cwd(), 'openapi.json'); + : join(__dirname, 'openapi.json'); writeFileSync(fileName, JSON.stringify(spec, undefined, 2)); } catch (err) {