From 7fc75d2c14c2e1f71694b8586f33212e42d5cc34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mih=C3=A1ly=20T=C3=B3th?= Date: Sun, 23 Nov 2025 12:19:10 +0100 Subject: [PATCH 1/3] chore: registry schema fixes --- .changeset/busy-pianos-stop.md | 5 + .../registry/implementation/registry.ts | 8 +- .../feature-focused/registry/scaffold.ts | 2 - schemas/registry/registry-item.json | 121 ++++++++---------- schemas/registry/registry.json | 10 +- 5 files changed, 70 insertions(+), 76 deletions(-) create mode 100644 .changeset/busy-pianos-stop.md diff --git a/.changeset/busy-pianos-stop.md b/.changeset/busy-pianos-stop.md new file mode 100644 index 00000000..23067dff --- /dev/null +++ b/.changeset/busy-pianos-stop.md @@ -0,0 +1,5 @@ +--- +"@calycode/cli": patch +--- + +chore: cleanup registry schema, unify casing, adjust schema for future diff --git a/packages/cli/src/commands/registry/implementation/registry.ts b/packages/cli/src/commands/registry/implementation/registry.ts index 55812b59..2b21b98f 100644 --- a/packages/cli/src/commands/registry/implementation/registry.ts +++ b/packages/cli/src/commands/registry/implementation/registry.ts @@ -61,21 +61,21 @@ async function addToXano({ if (installResult.success) { results.installed.push({ component: componentName, - file: file.target || file.path, + file: file.path, response: installResult.body, }); } else if (installResult.body && isAlreadyExistsError(installResult.body)) { // Skipped due to already existing results.skipped.push({ component: componentName, - file: file.target || file.path, + file: file.path, error: installResult.body.message, }); } else { // Other failures results.failed.push({ component: componentName, - file: file.target || file.path, + file: file.path, error: installResult.error || 'Installation failed', response: installResult.body, }); @@ -107,7 +107,7 @@ async function installComponentToXano(file, resolvedContext, core) { // For types that require dynamic IDs, resolve them first if (file.type === 'registry:query') { const targetApiGroup = await getApiGroupByName( - file['api-group-name'], + file.apiGroupName, { instanceConfig, workspaceConfig, branchConfig }, core ); diff --git a/packages/cli/src/utils/feature-focused/registry/scaffold.ts b/packages/cli/src/utils/feature-focused/registry/scaffold.ts index c8aecc6d..f8e0050c 100644 --- a/packages/cli/src/utils/feature-focused/registry/scaffold.ts +++ b/packages/cli/src/utils/feature-focused/registry/scaffold.ts @@ -6,7 +6,6 @@ async function ensureDirForFile(filePath: string) { await mkdir(dir, { recursive: true }); } -// [ ] CLI async function scaffoldRegistry( { registryRoot }: { registryRoot?: string } = { registryRoot: 'registry', @@ -49,7 +48,6 @@ async function scaffoldRegistry( { path: `${componentsRoot}/functions/${functionFileName}`, type: 'registry:function', - target: `/caly/${functionName}`, }, ], }; diff --git a/schemas/registry/registry-item.json b/schemas/registry/registry-item.json index b08aa383..a0753db4 100644 --- a/schemas/registry/registry-item.json +++ b/schemas/registry/registry-item.json @@ -32,28 +32,9 @@ ], "description": "The type of the item. This is used to determine the type and target path of the item when resolved for a Xano instance." }, - "description": { - "type": "string", - "description": "The description of the item. This is used to provide a **brief** overview of the item." - }, - "title": { - "type": "string", - "description": "The human-readable title for your registry item. Keep it short and descriptive. This can be used for searching or selecting in the CLI." - }, - "author": { - "type": "string", - "description": "The author of the item. Recommended format: username \u003Curl\u003E" - }, - "registryDependencies": { - "type": "array", - "description": "An array of registry items that this item depends on. Use the name of the item to reference it. Referencing items in external registries is NOT supported.", - "items": { - "type": "string" - } - }, "files": { "type": "array", - "description": "The main payload of the registry item. This is an array of files that are part of the registry item. Each file is an object with a path, content, type, and target.", + "description": "The main payload of the registry item. This is an array of files that are part of the registry item. Each file is an object with a path, content, type.", "items": { "type": "object", "properties": { @@ -63,7 +44,7 @@ }, "content": { "type": "string", - "description": "The content of the file. Optional." + "description": "The content of the file. Optional, in case of Xano it can be the text representation of a .xs file." }, "type": { "type": "string", @@ -86,25 +67,25 @@ "registry:realtime/trigger", "registry:test", "registry:snippet", - "registry:file", "registry:item" ], "description": "The type of the file. This is used to determine the type of the file when resolved for a Xano instance and is also used to determine the required Metadata API path for resolution." }, - "target": { - "type": "string", - "description": "The target path of the file. This is the path to the file in the project." - }, - "api-group-name": { + "apiGroupName": { "type": "string", "description": "The name of the api group into which a specific registry:query should be added. Only required for registry:query." + }, + "meta": { + "type": "object", + "description": "Additional metadata for the registry item's component. This is an object with any key value pairs. Put here anything that Xano can meaningfully accept or that helps you identify the current item. e.g. updated_at or similar.", + "additionalProperties": true } }, "if": { "properties": { "type": { "enum": [ - "registry:file" + "registry:query" ] } } @@ -113,54 +94,60 @@ "required": [ "path", "type", - "target" + "apiGroupName" ] }, "else": { - "if": { - "properties": { - "type": { - "enum": [ - "registry:query" - ] - } - } - }, - "then": { - "required": [ - "path", - "type", - "api-group-name" - ] - }, - "else": { - "required": [ - "path", - "type" - ] - } + "required": [ + "path", + "type" + ] } - }, - "meta": { - "type": "object", - "description": "Additional metadata for the registry item. This is an object with any key value pairs. Put here anything that Xano can meaningfully accept or that helps you identify the current item. e.g. updated_at or similar.", - "additionalProperties": true - }, - "docs": { + } + }, + "description": { + "type": "string", + "description": "The description of the item. This is used to provide a **brief** overview of the item." + }, + "docs": { + "type": "string", + "description": "The documentation for the registry item. This is a markdown string. This can be a longer explanation of how to use the item, or can have embedded images / videos etc, in the registry preview, will be rendered with Docsify." + }, + "postInstallHint": { + "type": "string", + "description": "A short hint that will be presented in the console, after installing this component. Useful to take notes of variables or required customizations to make a registry item work smoothly." + }, + "title": { + "type": "string", + "description": "The human-readable title for your registry item. Keep it short and descriptive. This can be used for searching or selecting in the CLI." + }, + "author": { + "type": "string", + "description": "The author of the item. Recommended format: username \u003Curl\u003E" + }, + "registryDependencies": { + "type": "array", + "description": "An array of registry items that this item depends on. Use the name of the item to reference it. Referencing items in external registries is NOT supported.", + "items": { + "type": "string" + } + }, + "categories": { + "type": "array", + "items": { "type": "string", - "description": "The documentation for the registry item. This is a markdown string." - }, - "categories": { - "type": "array", - "items": { - "type": "string", - "description": "The categories of the registry item. This is an array of strings. Helps in search and categorisation." - } + "description": "The categories of the registry item. This is an array of strings. Helps in search and categorisation." } + }, + "meta": { + "type": "object", + "description": "Additional metadata for the registry item. This is an object with any key value pairs. Put here extra content, that you consider useful in making this registry item be easy to understand.", + "additionalProperties": true } }, "required": [ "name", - "type" + "type", + "files" ] } \ No newline at end of file diff --git a/schemas/registry/registry.json b/schemas/registry/registry.json index 71939208..c46fb589 100644 --- a/schemas/registry/registry.json +++ b/schemas/registry/registry.json @@ -7,10 +7,16 @@ "type": "string" }, "homepage": { + "type": "string", + "format": "uri" + }, + "description": { "type": "string" }, "items": { "type": "array", + "minItems": 1, + "uniqueItems": true, "items": { "$ref": "https://calycode.com/schemas/registry/registry-item.json" } @@ -20,7 +26,5 @@ "name", "homepage", "items" - ], - "uniqueItems": true, - "minItems": 1 + ] } \ No newline at end of file From 6260409725a560e7d4fe80bb92781bf59660615c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mih=C3=A1ly=20T=C3=B3th?= Date: Sun, 23 Nov 2025 13:03:53 +0100 Subject: [PATCH 2/3] fix: test command related fixes --- .changeset/fluffy-carpets-check.md | 6 ++ .../src/commands/test/implementation/test.ts | 93 +++++++++++++++---- packages/core/src/features/testing/index.ts | 2 - schemas/testing/config.json | 21 ++++- 4 files changed, 95 insertions(+), 27 deletions(-) create mode 100644 .changeset/fluffy-carpets-check.md diff --git a/.changeset/fluffy-carpets-check.md b/.changeset/fluffy-carpets-check.md new file mode 100644 index 00000000..84917c74 --- /dev/null +++ b/.changeset/fluffy-carpets-check.md @@ -0,0 +1,6 @@ +--- +"@calycode/core": minor +"@calycode/cli": minor +--- + +fix: fixes for test runner, improved test-config-schema, including warning count in the test summary output. diff --git a/packages/cli/src/commands/test/implementation/test.ts b/packages/cli/src/commands/test/implementation/test.ts index 66fac86b..f2263019 100644 --- a/packages/cli/src/commands/test/implementation/test.ts +++ b/packages/cli/src/commands/test/implementation/test.ts @@ -21,35 +21,88 @@ import { * - `duration` (number, optional): duration of the test in milliseconds */ function printTestSummary(results) { - const total = results.length; - const succeeded = results.filter((r) => r.success).length; - const failed = total - succeeded; - const totalDuration = results.reduce((sum, r) => sum + (r.duration || 0), 0); + // Collect all rows for sizing + const rows = results.map((r) => { + const status = r.success ? '✅' : '❌'; + const method = r.method || ''; + const path = r.path || ''; + const warningsCount = r.warnings && Array.isArray(r.warnings) ? r.warnings.length : 0; + const duration = (r.duration || 0).toString(); + return { status, method, path, warnings: warningsCount.toString(), duration }; + }); - // Table header - log.message( - `${'='.repeat(60)} - Test Results Summary - ${'-'.repeat(60)} - ${'Status'.padEnd(4)} | ${'Method'.padEnd(6)} | ${'Path'.padEnd(24)} | ${'Duration (ms)'} - ${'-'.repeat(60)}` + // Calculate max width for each column (including header) + const headers = { + status: 'Status', + method: 'Method', + path: 'Path', + warnings: 'Warnings', + duration: 'Duration (ms)', + }; + + const colWidths = { + status: Math.max(headers.status.length, ...rows.map((r) => r.status.length)), + method: Math.max(headers.method.length, ...rows.map((r) => r.method.length)), + path: Math.max(headers.path.length, ...rows.map((r) => r.path.length)), + warnings: Math.max(headers.warnings.length, ...rows.map((r) => r.warnings.length)), + duration: Math.max(headers.duration.length, ...rows.map((r) => r.duration.length)), + }; + + // Helper to pad cell + const pad = (str, len) => str.padEnd(len); + + const sepLine = '-'.repeat( + colWidths.status + + colWidths.method + + colWidths.path + + colWidths.warnings + + colWidths.duration + + 13 ); - // Table rows - for (const r of results) { - const status = r.success ? '✅' : '❌'; + // Header + log.message(`${'='.repeat(sepLine.length)} + Test Results Summary + ${sepLine} + ${pad(headers.status, colWidths.status)} | ${pad(headers.method, colWidths.method)} | ${pad( + headers.path, + colWidths.path + )} | ${pad(headers.warnings, colWidths.warnings)} | ${pad(headers.duration, colWidths.duration)} + ${sepLine}`); + + // Rows + for (const r of rows) { log.message( - `${status.padEnd(4)} | ${r.method.padEnd(6)} | ${r.path.padEnd(24)} | ${( - r.duration || 0 - ).toString()}` + `${pad(r.status, colWidths.status)} | ${pad(r.method, colWidths.method)} | ${pad( + r.path, + colWidths.path + )} | ${pad(r.warnings, colWidths.warnings)} | ${pad(r.duration, colWidths.duration)}` ); } + // Summary + const total = results.length; + const succeeded = results.filter((r) => r.success).length; + const failed = total - succeeded; + const totalDuration = results.reduce((sum, r) => sum + (r.duration || 0), 0); + log.message( - `${'-'.repeat(60)} + `${sepLine} Total: ${total} | Passed: ${succeeded} | Failed: ${failed} | Total Duration: ${totalDuration} ms - ${'-'.repeat(60)}` + ${sepLine}` ); + + // Print out the warnings list: + const testsWithWarnings = results.filter((r) => r.warnings && r.warnings.length > 0); + if (testsWithWarnings.length > 0) { + log.message('\nWarnings details:'); + for (const r of testsWithWarnings) { + log.message(`- ${r.method} ${r.path}:`); + for (const warn of r.warnings) { + log.message(` [${warn.key}] ${warn.message}`); + } + } + } } /** @@ -168,4 +221,4 @@ async function runTest({ } } -export { runTest }; \ No newline at end of file +export { runTest }; diff --git a/packages/core/src/features/testing/index.ts b/packages/core/src/features/testing/index.ts index e505bfff..349456e1 100644 --- a/packages/core/src/features/testing/index.ts +++ b/packages/core/src/features/testing/index.ts @@ -134,7 +134,6 @@ async function testRunner({ const customAssertKeys = customAsserts ? Object.keys(customAsserts) : []; if (customAssertKeys.length > 0) { - // Use only asserts provided in customAsserts (with their specified levels/fns) for (const key of customAssertKeys) { const assertOpt = customAsserts[key]; if (assertOpt && typeof assertOpt.fn === 'function' && assertOpt.level !== 'off') { @@ -145,7 +144,6 @@ async function testRunner({ } } } else { - // Use all available asserts (with their default levels/fns) for (const [key, assertOpt] of Object.entries(availableAsserts)) { if (assertOpt.level !== 'off') { assertsToRun.push({ diff --git a/schemas/testing/config.json b/schemas/testing/config.json index 54d17014..dff13e1d 100644 --- a/schemas/testing/config.json +++ b/schemas/testing/config.json @@ -4,7 +4,7 @@ "type": "array", "items": { "type": "object", - "required": ["path", "method", "headers", "queryParams", "requestBody", "customAsserts"], + "required": ["path", "method", "requestBody", "customAsserts"], "properties": { "path": { "type": "string" @@ -17,8 +17,15 @@ "additionalProperties": { "type": "string" } }, "queryParams": { - "type": "object", - "additionalProperties": { "type": "string" } + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "value": {} + }, + "required": ["name", "value"] + } }, "requestBody": {}, "store": { @@ -39,10 +46,14 @@ "required": ["level"], "properties": { "fn": {}, - "level": { "type": "string" } + "level": { + "type": "string", + "enum": ["error", "warn", "off"] + } } } } - } + }, + "additionalProperties": false } } From c67593e69c5e82d1a986d3102c27e4700ee3019b Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sun, 23 Nov 2025 12:05:53 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`dev?= =?UTF-8?q?`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @MihalyToth20. * https://github.com/calycode/xano-tools/pull/175#issuecomment-3567879704 The following files were modified: * `packages/cli/src/commands/registry/implementation/registry.ts` * `packages/cli/src/commands/test/implementation/test.ts` * `packages/cli/src/utils/feature-focused/registry/scaffold.ts` * `packages/core/src/features/testing/index.ts` --- .../registry/implementation/registry.ts | 18 +++++++++----- .../src/commands/test/implementation/test.ts | 24 ++++++++++++------- .../feature-focused/registry/scaffold.ts | 12 +++++++++- packages/core/src/features/testing/index.ts | 23 ++++++++++++++---- 4 files changed, 57 insertions(+), 20 deletions(-) diff --git a/packages/cli/src/commands/registry/implementation/registry.ts b/packages/cli/src/commands/registry/implementation/registry.ts index 2b21b98f..f7666ef6 100644 --- a/packages/cli/src/commands/registry/implementation/registry.ts +++ b/packages/cli/src/commands/registry/implementation/registry.ts @@ -24,6 +24,13 @@ function isAlreadyExistsError(errorObj: any): boolean { ); } +/** + * Adds one or more registry components to a Xano instance, attempting to install each component file and collecting success, skip, and failure outcomes. + * + * @param componentNames - Registry component names to install; if empty, the user will be prompted to select components. + * @param context - CLI context used to resolve instance, workspace, and branch configuration; defaults to an empty object. + * @returns An object with `installed` (entries with `component`, `file` path, and `response`), `failed` (entries with `component`, `file`, `error`, and optional `response`), and `skipped` (entries for items skipped because the resource already exists). + */ async function addToXano({ componentNames, context = {}, @@ -93,12 +100,11 @@ async function addToXano({ } /** - * Installs a component file to Xano. + * Install a single component file into the configured Xano instance. * - * @param {Object} file - The component file metadata. - * @param {Object} resolvedContext - The resolved context configs. - * @param {any} core - Core utilities. - * @returns {Promise<{ success: boolean, error?: string, body?: any }>} + * @param file - Component file metadata (e.g., `type`, `path`, `target`, and for query files `apiGroupName`) that identifies what to install and where. + * @param resolvedContext - Resolved configuration objects: `instanceConfig`, `workspaceConfig`, and `branchConfig`. + * @returns An object with `success: true` and the parsed response `body` on success; on failure `success: false` and `error` contains a human-readable message, `body` may include the raw response when available. */ async function installComponentToXano(file, resolvedContext, core) { const { instanceConfig, workspaceConfig, branchConfig } = resolvedContext; @@ -177,4 +183,4 @@ async function installComponentToXano(file, resolvedContext, core) { } } -export { addToXano, scaffoldRegistry }; +export { addToXano, scaffoldRegistry }; \ No newline at end of file diff --git a/packages/cli/src/commands/test/implementation/test.ts b/packages/cli/src/commands/test/implementation/test.ts index f2263019..4fe7a54d 100644 --- a/packages/cli/src/commands/test/implementation/test.ts +++ b/packages/cli/src/commands/test/implementation/test.ts @@ -10,15 +10,17 @@ import { } from '../../../utils/index'; /** - * Prints a formatted summary table of test outcomes to the log. + * Print a formatted table of test outcomes and an optional detailed warnings section to the log. * - * Logs a header, one row per result showing status, HTTP method, path, and duration, and a final summary line with totals and aggregate duration. + * The table includes columns for status, HTTP method, path, warnings count, and duration (ms), + * followed by an aggregate summary line with total, passed, failed, and total duration. * * @param results - Array of test result objects. Each object should include: - * - `success` (boolean): whether the test passed, - * - `method` (string): HTTP method used, - * - `path` (string): endpoint path, - * - `duration` (number, optional): duration of the test in milliseconds + * - `success`: whether the test passed + * - `method`: HTTP method used for the test + * - `path`: endpoint path exercised by the test + * - `duration` (optional): duration of the test in milliseconds + * - `warnings` (optional): array of warning objects; each warning should include `key` and `message` */ function printTestSummary(results) { // Collect all rows for sizing @@ -128,9 +130,13 @@ async function loadTestConfig(testConfigPath) { } /** - * Runs API tests for selected API groups using a provided test configuration and writes per-group results to disk. + * Run API tests for selected API groups, write per-group JSON results to disk, and print a formatted summary. * - * @param instance - Name or alias of the target instance + * Resolves the target instance/workspace/branch, selects API groups (optionally prompting), loads the test + * configuration, executes tests via the provided runtime `core`, writes each group's results to a timestamped + * JSON file under the configured output path, and prints a summary table and optional output directory path. + * + * @param instance - Target instance name or alias used to resolve configuration * @param workspace - Workspace name within the instance * @param branch - Branch label within the workspace * @param group - Specific API group name to run; when omitted and `isAll` is false the user may be prompted @@ -221,4 +227,4 @@ async function runTest({ } } -export { runTest }; +export { runTest }; \ No newline at end of file diff --git a/packages/cli/src/utils/feature-focused/registry/scaffold.ts b/packages/cli/src/utils/feature-focused/registry/scaffold.ts index f8e0050c..b547888e 100644 --- a/packages/cli/src/utils/feature-focused/registry/scaffold.ts +++ b/packages/cli/src/utils/feature-focused/registry/scaffold.ts @@ -1,11 +1,21 @@ import { dirname, join } from 'node:path'; import { mkdir, writeFile } from 'node:fs/promises'; +/** + * Ensures the parent directory for the provided file path exists, creating it recursively if needed. + * + * @param filePath - Path to the target file whose containing directory should exist + */ async function ensureDirForFile(filePath: string) { const dir = dirname(filePath); await mkdir(dir, { recursive: true }); } +/** + * Creates a sample registry scaffold (components, function implementation, function definition, and index) under the specified registry root. + * + * @param registryRoot - Root directory where registry files will be written. Defaults to `'registry'`. + */ async function scaffoldRegistry( { registryRoot }: { registryRoot?: string } = { registryRoot: 'registry', @@ -69,4 +79,4 @@ async function scaffoldRegistry( await writeFile(indexPath, JSON.stringify(sampleIndex, null, 2), 'utf8'); } -export { scaffoldRegistry }; +export { scaffoldRegistry }; \ No newline at end of file diff --git a/packages/core/src/features/testing/index.ts b/packages/core/src/features/testing/index.ts index 349456e1..5b03ec7e 100644 --- a/packages/core/src/features/testing/index.ts +++ b/packages/core/src/features/testing/index.ts @@ -42,9 +42,24 @@ function getByPath(obj, path) { } /** - * testConfig is actually an array of objects defining in what order and which - * endpoints to run, also optionally define custom asserts (either inline func, or predefined asserts) - * ApiGroupConfig allows for extra keys. In this case it should include an 'oas' key + * Execute the configured API tests across the provided API groups and return per-group results. + * + * For each group, ensures an OpenAPI spec is available (fetching and patching from the remote API if absent), + * runs the endpoints defined by `testConfig` in order, evaluates assertions (built-in or custom), + * optionally extracts runtime values from JSON responses into a shared runtime store, and records timing, + * successes, errors, and warnings for each endpoint. + * + * @param context - Execution context containing instance, workspace, and branch identifiers + * @param groups - Array of API group configurations; each group may include an `oas` property (OpenAPI) and a `canonical` identifier used to build request base URLs + * @param testConfig - Ordered array of endpoint test definitions. Each entry should include: + * - `path` and `method` for the request + * - `headers`, `queryParams`, and `requestBody` for request composition + * - optional `store` mappings [{ key, path }] to extract values from JSON responses into runtime variables + * - optional `customAsserts` to override or provide per-endpoint assertions + * + * @returns An array of objects, one per input group, each containing the original `group` and a `results` array. + * Each result includes `path`, `method`, `success` (true if no assertion errors), `errors` (or null), + * `warnings` (or null), and `duration` (milliseconds) */ async function testRunner({ context, @@ -244,4 +259,4 @@ async function testRunner({ return finalOutput; } -export { testRunner }; +export { testRunner }; \ No newline at end of file