diff --git a/packages/api/src/cli/codegen/language.ts b/packages/api/src/cli/codegen/language.ts index bc3f667d..314334ac 100644 --- a/packages/api/src/cli/codegen/language.ts +++ b/packages/api/src/cli/codegen/language.ts @@ -25,11 +25,12 @@ export default abstract class CodeGeneratorLanguage { userAgent: string; - requiredPackages: Record; + requiredPackages!: Record; constructor(spec: Oas, specPath: string, identifier: string) { this.spec = spec; this.specPath = specPath; + this.identifier = identifier; // User agents should be contextual to the spec in question and the version of `api` that was // used to generate the SDK. For example, this'll look like `petstore/1.0.0 (api/4.2.0)` for diff --git a/packages/api/src/cli/codegen/languages/typescript.ts b/packages/api/src/cli/codegen/languages/typescript.ts index c3f1ea1e..194596f1 100644 --- a/packages/api/src/cli/codegen/languages/typescript.ts +++ b/packages/api/src/cli/codegen/languages/typescript.ts @@ -3,6 +3,7 @@ import type { InstallerOptions } from '../language'; import type Oas from 'oas'; import type { Operation } from 'oas'; import type { HttpMethods, SchemaObject } from 'oas/dist/rmoas.types'; +import type { SemVer } from 'semver'; import type { ClassDeclaration, JSDocStructure, @@ -53,9 +54,9 @@ export default class TSGenerator extends CodeGeneratorLanguage { types: Map; - files: Record; + files?: Record; - sdk: ClassDeclaration; + sdk!: ClassDeclaration; schemas: Record< string, @@ -138,7 +139,7 @@ export default class TSGenerator extends CodeGeneratorLanguage { if (!pkgVersion) { // If the version that's in `info.version` isn't compatible with semver NPM won't be able to // handle it properly so we need to fallback to something it can. - pkgVersion = semver.coerce('0.0.0'); + pkgVersion = semver.coerce('0.0.0') as SemVer; } const pkg: PackageJson = { @@ -221,7 +222,7 @@ export default class TSGenerator extends CodeGeneratorLanguage { sdkSource .getImportDeclarations() .find(id => id.getText() === "import type * as types from './types';") - .remove(); + ?.remove(); } // If this SDK doesn't use the `HTTPMethodRange` interface for handling `2XX` response status @@ -230,7 +231,7 @@ export default class TSGenerator extends CodeGeneratorLanguage { sdkSource .getImportDeclarations() .find(id => id.getText().includes('HTTPMethodRange')) - .replaceWithText("import type { ConfigOptions, FetchResponse } from '@api/core'"); + ?.replaceWithText("import type { ConfigOptions, FetchResponse } from '@api/core'"); } if (this.outputJS) { @@ -566,10 +567,10 @@ sdk.server('https://eu.api.example.com/v14');`), let hasOptionalBody = false; let hasOptionalMetadata = false; - const parameters: { - body?: OptionalKind; - metadata?: OptionalKind; - } = {}; + const parameters = {} as { + body: OptionalKind; + metadata: OptionalKind; + }; if (paramTypes) { // If an operation has a request body payload it will only ever have `body` or `formData`, @@ -656,7 +657,7 @@ sdk.server('https://eu.api.example.com/v14');`), // we should only add a docblock to the first overload we create because IDE Intellisense will // always use that and adding a docblock to all three will bloat the SDK with unused and // unsurfaced method documentation. - docs: shouldAddAltTypedOverloads ? null : Object.keys(docblock).length ? [docblock] : null, + docs: shouldAddAltTypedOverloads ? undefined : Object.keys(docblock).length ? [docblock] : undefined, statements: writer => { /** * @example return this.core.fetch('/pet/findByStatus', 'get', body, metadata); @@ -698,7 +699,7 @@ sdk.server('https://eu.api.example.com/v14');`), { ...parameters.metadata, hasQuestionToken: false }, ], returnType, - docs: Object.keys(docblock).length ? [docblock] : null, + docs: Object.keys(docblock).length ? [docblock] : undefined, }); // Create an overload that just has a single `metadata` parameter. @@ -739,13 +740,13 @@ sdk.server('https://eu.api.example.com/v14');`), */ loadOperationsAndMethods() { const operations: Record = {}; - const methods = new Set(); + const methods = new Set(); // Prepare all of the schemas that we need to process for every operation within this API // definition. Object.entries(this.spec.getPaths()).forEach(([, ops]) => { - Object.entries(ops).forEach(([method, operation]: [HttpMethods, Operation]) => { - methods.add(method); + Object.entries(ops).forEach(([method, operation]: [string, Operation]) => { + methods.add(method as HttpMethods); const operationId = operation.getOperationId({ // This `camelCase` option will clean up any weird characters that might be present in @@ -786,7 +787,7 @@ sdk.server('https://eu.api.example.com/v14');`), transformer: (s: SchemaObject) => { // As our schemas are dereferenced in the `oas` library we don't want to pollute our // codegen'd schemas file with duplicate schemas. - if ('x-readme-ref-name' in s) { + if ('x-readme-ref-name' in s && typeof s['x-readme-ref-name'] !== 'undefined') { const typeName = generateTypeName(s['x-readme-ref-name']); this.addSchemaToExport(s, typeName, typeName); @@ -844,7 +845,7 @@ sdk.server('https://eu.api.example.com/v14');`), transformer: (s: SchemaObject) => { // As our schemas are dereferenced in the `oas` library we don't want to pollute our // codegen'd schemas file with duplicate schemas. - if ('x-readme-ref-name' in s) { + if ('x-readme-ref-name' in s && typeof s['x-readme-ref-name'] !== 'undefined') { const typeName = generateTypeName(s['x-readme-ref-name']); this.addSchemaToExport(s, typeName, `${typeName}`); diff --git a/packages/api/src/cli/fetcher.ts b/packages/api/src/cli/fetcher.ts index 1f189c3b..cd5d6040 100644 --- a/packages/api/src/cli/fetcher.ts +++ b/packages/api/src/cli/fetcher.ts @@ -51,7 +51,7 @@ export default class Fetcher { return undefined; } - return matches.groups.project; + return matches.groups?.project; } async load() { diff --git a/packages/api/src/cli/storage.ts b/packages/api/src/cli/storage.ts index 55f0a882..d2b11b85 100644 --- a/packages/api/src/cli/storage.ts +++ b/packages/api/src/cli/storage.ts @@ -22,7 +22,7 @@ export default class Storage { */ source: string; - identifier: string; + identifier!: string; fetcher: Fetcher; @@ -32,7 +32,9 @@ export default class Storage { this.fetcher = new Fetcher(source); this.source = source; - this.identifier = identifier; + if (identifier) { + this.identifier = identifier; + } // This should default to false so we have awareness if we've looked at the lockfile yet. Storage.lockfile = false; @@ -117,7 +119,9 @@ export default class Storage { if (!isValidForNPM.validForNewPackages) { // `prompts` doesn't support surfacing multiple errors in a `validate` call so we can only // surface the first to the user. - throw new Error(`Identifier cannot be used for an NPM package: ${isValidForNPM.errors[0]}`); + throw new Error( + `Identifier cannot be used for an NPM package: ${isValidForNPM?.errors?.[0] || '[error unavailable]'}`, + ); } return true; diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index c04ee9df..62023664 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -6,7 +6,9 @@ "esModuleInterop": true, "lib": ["DOM", "DOM.Iterable", "ES2020"], "noImplicitAny": true, - "outDir": "dist/" + "outDir": "dist/", + "strict": true, + "useUnknownInCatchVariables": false }, "include": ["./src/**/*"] } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 30d15a53..e7dd1429 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -37,24 +37,24 @@ export type HTTPMethodRange = Exclude; } = false; private config: ConfigOptions = {}; - private userAgent: string; + private userAgent!: string; constructor(spec?: Oas, userAgent?: string) { - this.spec = spec; - this.userAgent = userAgent; + if (spec) this.spec = spec; + if (userAgent) this.userAgent = userAgent; } setSpec(spec: Oas) { diff --git a/packages/core/src/lib/getJSONSchemaDefaults.ts b/packages/core/src/lib/getJSONSchemaDefaults.ts index 0c7cd5d4..cffc20dc 100644 --- a/packages/core/src/lib/getJSONSchemaDefaults.ts +++ b/packages/core/src/lib/getJSONSchemaDefaults.ts @@ -23,16 +23,16 @@ export default function getJSONSchemaDefaults(jsonSchemas: SchemaWrapper[]) { schema: SchemaObject, pointer: string, rootSchema: SchemaObject, - parentPointer: string, - parentKeyword: string, - parentSchema: SchemaObject, - indexProperty: string, + parentPointer?: string, + parentKeyword?: string, + parentSchema?: SchemaObject, + indexProperty?: string | number, ) => { if (!pointer.startsWith('/properties/')) { return; } - if (Array.isArray(parentSchema?.required) && parentSchema.required.includes(indexProperty)) { + if (Array.isArray(parentSchema?.required) && parentSchema?.required.includes(String(indexProperty))) { if (schema.type === 'object' && indexProperty) { defaults[indexProperty] = {}; } diff --git a/packages/core/src/lib/prepareAuth.ts b/packages/core/src/lib/prepareAuth.ts index 5f00ca66..6843e896 100644 --- a/packages/core/src/lib/prepareAuth.ts +++ b/packages/core/src/lib/prepareAuth.ts @@ -1,5 +1,6 @@ /* eslint-disable no-underscore-dangle */ import type { Operation } from 'oas'; +import type { KeyedSecuritySchemeObject } from 'oas/dist/rmoas.types'; export default function prepareAuth(authKey: (number | string)[], operation: Operation) { if (authKey.length === 0) { @@ -58,7 +59,7 @@ export default function prepareAuth(authKey: (number | string)[], operation: Ope ); } - const scheme = schemes.shift(); + const scheme = schemes.shift() as KeyedSecuritySchemeObject; preparedAuth[scheme._key] = { user: authKey[0], pass: authKey.length === 2 ? authKey[1] : '', @@ -76,7 +77,7 @@ export default function prepareAuth(authKey: (number | string)[], operation: Ope .map(([, ps]) => ps.filter(s => usableScheme === s._key)) .reduce((prev, next) => prev.concat(next), []); - const scheme = schemes.shift(); + const scheme = schemes.shift() as KeyedSecuritySchemeObject; switch (scheme.type) { case 'http': if (scheme.scheme === 'basic') { diff --git a/packages/core/src/lib/prepareParams.ts b/packages/core/src/lib/prepareParams.ts index 13f54fad..c4ef105b 100644 --- a/packages/core/src/lib/prepareParams.ts +++ b/packages/core/src/lib/prepareParams.ts @@ -74,9 +74,9 @@ function merge(src: any, target: any) { * */ function processFile( - paramName: string, + paramName: string | undefined, file: string | ReadStream, -): Promise<{ base64: string; buffer: Buffer; filename: string; paramName: string }> { +): Promise<{ base64?: string; buffer?: Buffer; filename: string; paramName?: string } | undefined> { if (typeof file === 'string') { // In order to support relative pathed files, we need to attempt to resolve them. const resolvedFile = path.resolve(file); @@ -103,7 +103,7 @@ function processFile( return resolve({ paramName, - base64: fileMetadata.content.replace(';base64', `;name=${payloadFilename};base64`), + base64: fileMetadata?.content?.replace(';base64', `;name=${payloadFilename};base64`), filename: payloadFilename, buffer: fileMetadata.buffer, }); @@ -118,7 +118,7 @@ function processFile( return { paramName, - base64: base64.replace(';base64', `;name=${payloadFilename};base64`), + base64: base64?.replace(';base64', `;name=${payloadFilename};base64`), filename: payloadFilename, buffer, }; @@ -228,7 +228,7 @@ export default async function prepareParams(operation: Operation, body?: unknown } }); - const intersection = Object.keys(body).filter(value => { + const intersection = Object.keys(body as NonNullable).filter(value => { if (Object.keys(digestedParameters).includes(value)) { return true; } else if (headerParams.has(value)) { @@ -238,10 +238,10 @@ export default async function prepareParams(operation: Operation, body?: unknown return false; }).length; - if (intersection && intersection / Object.keys(body).length > 0.25) { + // If more than 25% of the body intersects with the parameters that we've got on hand, then + // we should treat it as a metadata object and organize into parameters. + if (intersection && intersection / Object.keys(body as NonNullable).length > 0.25) { /* eslint-disable no-param-reassign */ - // If more than 25% of the body intersects with the parameters that we've got on hand, - // then we should treat it as a metadata object and organize into parameters. metadataIntersected = true; metadata = merge(params.body, body) as Record; body = undefined; @@ -304,7 +304,9 @@ export default async function prepareParams(operation: Operation, body?: unknown params.body = fileMetadata.base64; } - params.files[fileMetadata.filename] = fileMetadata.buffer; + if (fileMetadata.buffer && params?.files) { + params.files[fileMetadata.filename] = fileMetadata.buffer; + } }); }); } @@ -336,7 +338,7 @@ export default async function prepareParams(operation: Operation, body?: unknown } else if (param.in === 'header') { // Headers are sent case-insensitive so we need to make sure that we're properly // matching them when detecting what our incoming payload looks like. - metadataHeaderParam = Object.keys(metadata).find(k => k.toLowerCase() === paramName.toLowerCase()); + metadataHeaderParam = Object.keys(metadata).find(k => k.toLowerCase() === paramName.toLowerCase()) || ''; value = metadata[metadataHeaderParam]; } } @@ -348,20 +350,20 @@ export default async function prepareParams(operation: Operation, body?: unknown /* eslint-disable no-param-reassign */ switch (param.in) { case 'path': - params.path[paramName] = value; - delete metadata[paramName]; + (params.path as NonNullable)[paramName] = value; + if (metadata?.[paramName]) delete metadata[paramName]; break; case 'query': - params.query[paramName] = value; - delete metadata[paramName]; + (params.query as NonNullable)[paramName] = value; + if (metadata?.[paramName]) delete metadata[paramName]; break; case 'header': - params.header[paramName.toLowerCase()] = value; - delete metadata[metadataHeaderParam]; + (params.header as NonNullable)[paramName.toLowerCase()] = value; + if (metadataHeaderParam && metadata?.[metadataHeaderParam]) delete metadata[metadataHeaderParam]; break; case 'cookie': - params.cookie[paramName] = value; - delete metadata[paramName]; + (params.cookie as NonNullable)[paramName] = value; + if (metadata?.[paramName]) delete metadata[paramName]; break; default: // no-op } @@ -387,11 +389,17 @@ export default async function prepareParams(operation: Operation, body?: unknown // or specify a custom auth header (maybe we can't handle their auth case right) this is the // only way with this library that they can do that. specialHeaders.forEach(headerName => { - const headerParam = Object.keys(metadata).find(m => m.toLowerCase() === headerName); + const headerParam = Object.keys(metadata || {}).find(m => m.toLowerCase() === headerName); if (headerParam) { - params.header[headerName] = metadata[headerParam] as string; - // eslint-disable-next-line no-param-reassign - delete metadata[headerParam]; + // this if-statement below is a typeguard + if (typeof metadata === 'object') { + // this if-statement below is a typeguard + if (typeof params.header === 'object') { + params.header[headerName] = metadata[headerParam] as string; + } + // eslint-disable-next-line no-param-reassign + delete metadata[headerParam]; + } } }); } @@ -405,7 +413,7 @@ export default async function prepareParams(operation: Operation, body?: unknown } } - ['body', 'cookie', 'files', 'formData', 'header', 'path', 'query'].forEach((type: keyof typeof params) => { + (['body', 'cookie', 'files', 'formData', 'header', 'path', 'query'] as const).forEach((type: keyof typeof params) => { if (type in params && isEmpty(params[type])) { delete params[type]; } diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 5acf6120..4f40fdab 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -7,7 +7,7 @@ "lib": ["DOM", "DOM.Iterable", "ES2020"], "noImplicitAny": true, "outDir": "dist/", - "strict": false + "strict": true }, "include": ["./src/**/*"] } diff --git a/packages/httpsnippet-client-api/src/index.ts b/packages/httpsnippet-client-api/src/index.ts index b5ea2fd3..d42c35a3 100644 --- a/packages/httpsnippet-client-api/src/index.ts +++ b/packages/httpsnippet-client-api/src/index.ts @@ -10,9 +10,6 @@ import stringifyObject from 'stringify-object'; const { matchesMimeType } = utils; -// This should really be an exported type in `oas`. -type SecurityType = 'Basic' | 'Bearer' | 'Query' | 'Header' | 'Cookie' | 'OAuth2' | 'http' | 'apiKey'; - function stringify(obj: any, opts = {}) { return stringifyObject(obj, { indent: ' ', ...opts }); } @@ -47,9 +44,8 @@ function getAuthSources(operation: Operation) { return matchers; } - const security = operation.prepareSecurity(); - Object.keys(security).forEach((id: SecurityType) => { - security[id].forEach(scheme => { + Object.entries(operation.prepareSecurity()).forEach(([, schemes]) => { + schemes.forEach(scheme => { if (scheme.type === 'http') { if (scheme.scheme === 'basic') { matchers.header.authorization = 'Basic'; @@ -95,7 +91,7 @@ const client: Client = { convert: ({ cookiesObj, headersObj, postData, queryObj, url, ...source }, options) => { const opts = { ...options, - }; + } as APIOptions; if (!('apiDefinitionUri' in opts)) { throw new Error('This HTTP Snippet client must have an `apiDefinitionUri` option supplied to it.'); diff --git a/packages/httpsnippet-client-api/tsconfig.json b/packages/httpsnippet-client-api/tsconfig.json index b785b2bb..c2f1e0a4 100644 --- a/packages/httpsnippet-client-api/tsconfig.json +++ b/packages/httpsnippet-client-api/tsconfig.json @@ -6,7 +6,8 @@ "esModuleInterop": true, "lib": ["DOM", "ES2020"], "noImplicitAny": true, - "outDir": "dist/" + "outDir": "dist/", + "strict": true }, "include": ["./src/**/*"] } diff --git a/packages/test-utils/tsconfig.json b/packages/test-utils/tsconfig.json index a0ae87dd..c1d411a7 100644 --- a/packages/test-utils/tsconfig.json +++ b/packages/test-utils/tsconfig.json @@ -5,7 +5,8 @@ "declaration": true, "esModuleInterop": true, "noEmit": true, - "noImplicitAny": true + "noImplicitAny": true, + "strict": true }, "include": ["./**/*"] }