diff --git a/.changeset/gentle-trees-tan.md b/.changeset/gentle-trees-tan.md new file mode 100644 index 0000000..f693e38 --- /dev/null +++ b/.changeset/gentle-trees-tan.md @@ -0,0 +1,6 @@ +--- +"@calycode/core": minor +"@calycode/cli": minor +--- + +chore: by default do not generate table schemas into the OAS spec, but allow that via flag diff --git a/.changeset/tangy-windows-taste.md b/.changeset/tangy-windows-taste.md new file mode 100644 index 0000000..3ae76e0 --- /dev/null +++ b/.changeset/tangy-windows-taste.md @@ -0,0 +1,8 @@ +--- +'@calycode/core': patch +'@calycode/cli': patch +--- + +chore: extract the tags from the paths to the global to comply better with OAS +chore: set maxLength to strings on the error codes (owasp:api4:2019-string-limit) +chore: set type to strings (owasp:api4:2019-string-restricted) diff --git a/packages/cli/src/commands/generate/implementation/oas-spec.ts b/packages/cli/src/commands/generate/implementation/oas-spec.ts index 6ff1d67..1e8f8bc 100644 --- a/packages/cli/src/commands/generate/implementation/oas-spec.ts +++ b/packages/cli/src/commands/generate/implementation/oas-spec.ts @@ -15,6 +15,7 @@ async function updateOasWizard({ isAll = false, printOutput = false, core, + includeTables = false, }: { instance: string; workspace: string; @@ -23,6 +24,7 @@ async function updateOasWizard({ isAll: boolean; printOutput: boolean; core; + includeTables?: boolean; }) { attachCliEventHandlers('generate-oas', core, { instance, @@ -60,7 +62,8 @@ async function updateOasWizard({ workspaceConfig.name, branchConfig.label, groups, - startDir + startDir, + includeTables ); for (const { group, generatedItems } of allGroupResults) { const apiGroupNameNorm = normalizeApiGroupName(group); diff --git a/packages/cli/src/commands/generate/index.ts b/packages/cli/src/commands/generate/index.ts index d1cbb43..4f153c8 100644 --- a/packages/cli/src/commands/generate/index.ts +++ b/packages/cli/src/commands/generate/index.ts @@ -108,6 +108,11 @@ function registerGenerateCommands(program, core) { addApiGroupOptions(specGenCommand); addPrintOutputFlag(specGenCommand); + specGenCommand.option( + '--include-tables', + 'Requests table schema fetching and inclusion into the generate spec. By default tables are not included.' + ); + specGenCommand.action( withErrorHandler(async (opts) => { await updateOasWizard({ @@ -118,6 +123,7 @@ function registerGenerateCommands(program, core) { isAll: opts.all, printOutput: opts.printOutputDir, core: core, + includeTables: opts.includeTables, }); }) ); diff --git a/packages/core/src/features/oas/generate/methods/do-oas-update.ts b/packages/core/src/features/oas/generate/methods/do-oas-update.ts index e8610d6..4aeb38e 100644 --- a/packages/core/src/features/oas/generate/methods/do-oas-update.ts +++ b/packages/core/src/features/oas/generate/methods/do-oas-update.ts @@ -15,15 +15,23 @@ async function doOasUpdate({ inputOas, instanceConfig, workspaceConfig, - storage, // Used for meta lookups, not FS + storage, + includeTables = false, }: { inputOas: any; instanceConfig: any; workspaceConfig: any; storage: any; + includeTables: boolean; }): Promise { // Patch and enrich OAS - const oas = await patchOasSpec({ oas: inputOas, instanceConfig, workspaceConfig, storage }); + const oas = await patchOasSpec({ + oas: inputOas, + instanceConfig, + workspaceConfig, + storage, + includeTables, + }); // Prepare output artifacts (relative paths) const generatedItems: GeneratedItem[] = [ diff --git a/packages/core/src/features/oas/generate/methods/extract-tags-to-global-level.ts b/packages/core/src/features/oas/generate/methods/extract-tags-to-global-level.ts new file mode 100644 index 0000000..15f8435 --- /dev/null +++ b/packages/core/src/features/oas/generate/methods/extract-tags-to-global-level.ts @@ -0,0 +1,29 @@ +function extractTagsToGlobal(paths) { + // 1. Collect all tags used in operations + const tagSet = new Set(); + for (const [path, methods] of Object.entries(paths || {})) { + for (const [method, operation] of Object.entries(methods)) { + if (operation.tags && Array.isArray(operation.tags)) { + operation.tags.forEach((tag) => tagSet.add(tag)); + } + } + } + + // 2. Build the global tags array if not present + let tags = Array.from(tagSet).map((tag) => ({ + name: tag, + description: `Auto-generated tag for ${tag}`, + })); + + // (Optional) If you want to preserve existing tags and only add missing ones: + const existingTags = (tags || []).map((t) => t.name); + const allTags = Array.from(new Set([...existingTags, ...tagSet])); + tags = allTags.map((tag) => ({ + name: tag, + description: `Auto-generated tag for ${tag}`, + })); + + return tags; +} + +export { extractTagsToGlobal }; diff --git a/packages/core/src/features/oas/generate/methods/patch-oas-spec.ts b/packages/core/src/features/oas/generate/methods/patch-oas-spec.ts index da75997..3c0b377 100644 --- a/packages/core/src/features/oas/generate/methods/patch-oas-spec.ts +++ b/packages/core/src/features/oas/generate/methods/patch-oas-spec.ts @@ -1,15 +1,24 @@ import { cleanupResponseSchemas } from './cleanup-response-schemas'; +import { extractTagsToGlobal } from './extract-tags-to-global-level'; import { generateTableSchemas } from '..'; -async function patchOasSpec({ oas, instanceConfig, workspaceConfig, storage }) { - +async function patchOasSpec({ + oas, + instanceConfig, + workspaceConfig, + storage, + includeTables = false, +}) { const newOas = { ...oas }; - const tableSchemas = await generateTableSchemas({ instanceConfig, workspaceConfig, storage }); + const tableSchemas = includeTables + ? await generateTableSchemas({ instanceConfig, workspaceConfig, storage }) + : {}; newOas.openapi = '3.1.1'; - newOas.components = { + newOas.tags = extractTagsToGlobal(newOas.paths); + newOas.components = { ...(oas.components ?? {}), responses: { @@ -83,16 +92,22 @@ async function patchOasSpec({ oas, instanceConfig, workspaceConfig, storage }) { properties: { code: { type: 'string', + format: 'const', + maxLength: 64, example: 'ERROR_CODE_ACCESS_DENIED', }, message: { type: 'string', + format: 'const', + maxLength: 256, example: 'Forbidden access.', }, payload: { anyOf: [ { type: 'string', + format: 'const', + maxLength: 1024, }, { type: 'null' }, { type: 'object', properties: {}, additionalProperties: true }, @@ -106,16 +121,22 @@ async function patchOasSpec({ oas, instanceConfig, workspaceConfig, storage }) { properties: { code: { type: 'string', + format: 'const', + maxLength: 64, example: 'ERROR_CODE_UNAUTHORIZED', }, message: { type: 'string', + format: 'const', + maxLength: 256, example: 'Authentication required.', }, payload: { anyOf: [ { type: 'string', + format: 'const', + maxLength: 1024, }, { type: 'null' }, { type: 'object', properties: {}, additionalProperties: true }, @@ -129,16 +150,22 @@ async function patchOasSpec({ oas, instanceConfig, workspaceConfig, storage }) { properties: { code: { type: 'string', + format: 'const', + maxLength: 64, example: 'ERROR_FATAL', }, message: { type: 'string', + format: 'const', + maxLength: 256, example: 'Something went wrong.', }, payload: { anyOf: [ { type: 'string', + format: 'const', + maxLength: 1024, }, { type: 'null' }, { type: 'object', properties: {}, additionalProperties: true }, @@ -152,16 +179,22 @@ async function patchOasSpec({ oas, instanceConfig, workspaceConfig, storage }) { properties: { code: { type: 'string', + format: 'const', + maxLength: 64, example: 'ERROR_CODE_TOO_MANY_REQUESTS', }, message: { type: 'string', + format: 'const', + maxLength: 256, example: 'Hit quota limits.', }, payload: { anyOf: [ { type: 'string', + format: 'const', + maxLength: 1024, }, { type: 'null' }, { type: 'object', properties: {}, additionalProperties: true }, @@ -175,16 +208,22 @@ async function patchOasSpec({ oas, instanceConfig, workspaceConfig, storage }) { properties: { code: { type: 'string', + format: 'const', + maxLength: 64, example: 'ERROR_CODE_NOT_FOUND', }, message: { type: 'string', + format: 'const', + maxLength: 256, example: 'The requested resource cannot be found.', }, payload: { anyOf: [ { type: 'string', + format: 'const', + maxLength: 1024, }, { type: 'null' }, { type: 'object', properties: {}, additionalProperties: true }, @@ -198,16 +237,22 @@ async function patchOasSpec({ oas, instanceConfig, workspaceConfig, storage }) { properties: { code: { type: 'string', + format: 'const', + maxLength: 64, example: 'ERROR_CODE_BAD_REQUEST', }, message: { type: 'string', + format: 'const', + maxLength: 256, example: 'The provided inputs are not correct.', }, payload: { anyOf: [ { type: 'string', + format: 'const', + maxLength: 1024, }, { type: 'null' }, { type: 'object', properties: {}, additionalProperties: true }, @@ -225,7 +270,6 @@ async function patchOasSpec({ oas, instanceConfig, workspaceConfig, storage }) { bearerFormat: 'JWT', }), }, - }; newOas.security = newOas.security || [{ bearerAuth: [] }]; @@ -235,4 +279,4 @@ async function patchOasSpec({ oas, instanceConfig, workspaceConfig, storage }) { return oasWithPatchedResponseSchemas; } -export { patchOasSpec } \ No newline at end of file +export { patchOasSpec }; diff --git a/packages/core/src/features/oas/generate/methods/update-spec-for-group.ts b/packages/core/src/features/oas/generate/methods/update-spec-for-group.ts index 912a5d3..fa54e12 100644 --- a/packages/core/src/features/oas/generate/methods/update-spec-for-group.ts +++ b/packages/core/src/features/oas/generate/methods/update-spec-for-group.ts @@ -12,6 +12,7 @@ async function updateSpecForGroup({ workspaceConfig, storage, core, + includeTables = false, }: { group: any; instanceConfig: any; @@ -19,6 +20,7 @@ async function updateSpecForGroup({ branchConfig: any; storage: any; core: any; + includeTables: boolean; }): Promise<{ oas: string; generatedItems: GeneratedItem[]; @@ -36,6 +38,7 @@ async function updateSpecForGroup({ instanceConfig, workspaceConfig, storage, + includeTables, }); // Optionally emit info for consumer diff --git a/packages/core/src/implementations/generate-oas.ts b/packages/core/src/implementations/generate-oas.ts index 0bcb8fc..61df9ca 100644 --- a/packages/core/src/implementations/generate-oas.ts +++ b/packages/core/src/implementations/generate-oas.ts @@ -10,6 +10,7 @@ async function updateOpenapiSpecImplementation( workspace: string; branch: string; groups: ApiGroup[]; + includeTables?: boolean; }, startDir: string ): Promise<{ group: string; oas: any; generatedItems: { path: string; content: string }[] }[]> { @@ -42,6 +43,7 @@ async function updateOpenapiSpecImplementation( branchConfig, storage, core, + includeTables: options.includeTables ?? false, }); return { group: grp.name, oas, generatedItems }; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e038fa9..607c70f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -146,7 +146,8 @@ export class Caly extends TypedEmitter { workspace: string, branch: string, groups: any, - startDir: string + startDir: string, + includeTables?: boolean ): Promise<{ group: string; oas: any; generatedItems: { path: string; content: string }[] }[]> { return updateOpenapiSpecImplementation( this.storage, @@ -156,6 +157,7 @@ export class Caly extends TypedEmitter { workspace, branch, groups, + includeTables, }, startDir ); @@ -386,7 +388,12 @@ export class Caly extends TypedEmitter { * }); * ``` */ - async doOasUpdate({ inputOas, instanceConfig, workspaceConfig }): Promise<{ + async doOasUpdate({ + inputOas, + instanceConfig, + workspaceConfig, + includeTables = false, + }): Promise<{ oas: any; generatedItems: { path: string; @@ -398,6 +405,7 @@ export class Caly extends TypedEmitter { instanceConfig, workspaceConfig, storage: this.storage, + includeTables, }); }