Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/gentle-trees-tan.md
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions .changeset/tangy-windows-taste.md
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ async function updateOasWizard({
isAll = false,
printOutput = false,
core,
includeTables = false,
}: {
instance: string;
workspace: string;
Expand All @@ -23,6 +24,7 @@ async function updateOasWizard({
isAll: boolean;
printOutput: boolean;
core;
includeTables?: boolean;
}) {
attachCliEventHandlers('generate-oas', core, {
instance,
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/commands/generate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -118,6 +123,7 @@ function registerGenerateCommands(program, core) {
isAll: opts.all,
printOutput: opts.printOutputDir,
core: core,
includeTables: opts.includeTables,
});
})
);
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/features/oas/generate/methods/do-oas-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DoOasUpdateOutput> {
// 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[] = [
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Comment on lines +1 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Redundant logic in tag preservation.

The function has confusing and redundant logic:

  1. Lines 13-16 build a tags array from tagSet
  2. Line 19 references tags (just created) as if it were pre-existing input
  3. Lines 20-24 completely rebuild the tags array, making lines 13-16 unnecessary

The comment on line 18 mentions "preserve existing tags," but the function doesn't accept any existing tags as input. Either:

  • The function should accept an existingTags parameter to truly preserve them, or
  • Lines 18-24 should be removed as they just recreate what lines 13-16 already built

Apply this diff to simplify the function:

 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) => ({
+   // 2. Build the global tags array
+   const 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;
 }

If you truly need to preserve existing tags from the OpenAPI spec, modify the signature:

-function extractTagsToGlobal(paths) {
+function extractTagsToGlobal(paths, existingTags = []) {
    // 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);
+   // 2. Preserve existing tags and add discovered ones
+   const existingTagNames = existingTags.map((t) => t.name);
-   const allTags = Array.from(new Set([...existingTags, ...tagSet]));
-   tags = allTags.map((tag) => ({
+   const allTags = Array.from(new Set([...existingTagNames, ...tagSet]));
+   const tags = allTags.map((tag) => ({
       name: tag,
       description: `Auto-generated tag for ${tag}`,
    }));

    return tags;
 }
🤖 Prompt for AI Agents
In
packages/core/src/features/oas/generate/methods/extract-tags-to-global-level.ts
lines 1-27, the function builds a tags array from operations then immediately
recomputes it as if preserving pre-existing tags though no such input exists;
remove the redundant intermediate build and the "preserve existing tags" block
(lines that compute existingTags, allTags and rebuild tags) OR change the
function signature to accept an existingTags parameter and merge
operation-derived tags with that input (deduplicate) before returning the final
tags array — implement one of these two fixes so the function is no longer doing
confusing duplicate work.


export { extractTagsToGlobal };
56 changes: 50 additions & 6 deletions packages/core/src/features/oas/generate/methods/patch-oas-spec.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down Expand Up @@ -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 },
Expand All @@ -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 },
Expand All @@ -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 },
Expand All @@ -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 },
Expand All @@ -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 },
Expand All @@ -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 },
Expand All @@ -225,7 +270,6 @@ async function patchOasSpec({ oas, instanceConfig, workspaceConfig, storage }) {
bearerFormat: 'JWT',
}),
},

};

newOas.security = newOas.security || [{ bearerAuth: [] }];
Expand All @@ -235,4 +279,4 @@ async function patchOasSpec({ oas, instanceConfig, workspaceConfig, storage }) {
return oasWithPatchedResponseSchemas;
}

export { patchOasSpec }
export { patchOasSpec };
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ async function updateSpecForGroup({
workspaceConfig,
storage,
core,
includeTables = false,
}: {
group: any;
instanceConfig: any;
workspaceConfig: any;
branchConfig: any;
storage: any;
core: any;
includeTables: boolean;
}): Promise<{
oas: string;
generatedItems: GeneratedItem[];
Expand All @@ -36,6 +38,7 @@ async function updateSpecForGroup({
instanceConfig,
workspaceConfig,
storage,
includeTables,
});

// Optionally emit info for consumer
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/implementations/generate-oas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }[] }[]> {
Expand Down Expand Up @@ -42,6 +43,7 @@ async function updateOpenapiSpecImplementation(
branchConfig,
storage,
core,
includeTables: options.includeTables ?? false,
});

return { group: grp.name, oas, generatedItems };
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ export class Caly extends TypedEmitter<EventMap> {
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,
Expand All @@ -156,6 +157,7 @@ export class Caly extends TypedEmitter<EventMap> {
workspace,
branch,
groups,
includeTables,
},
startDir
);
Expand Down Expand Up @@ -386,7 +388,12 @@ export class Caly extends TypedEmitter<EventMap> {
* });
* ```
*/
async doOasUpdate({ inputOas, instanceConfig, workspaceConfig }): Promise<{
async doOasUpdate({
inputOas,
instanceConfig,
workspaceConfig,
includeTables = false,
}): Promise<{
oas: any;
generatedItems: {
path: string;
Expand All @@ -398,6 +405,7 @@ export class Caly extends TypedEmitter<EventMap> {
instanceConfig,
workspaceConfig,
storage: this.storage,
includeTables,
});
}

Expand Down