diff --git a/.github/workflows/build-publish.yml b/.github/workflows/build-publish.yml index 5a8413c..c312b4c 100644 --- a/.github/workflows/build-publish.yml +++ b/.github/workflows/build-publish.yml @@ -2,6 +2,9 @@ name: Build, Test, and Publish on: workflow_dispatch: + repository_dispatch: + types: + - cda-prod-release permissions: pages: write @@ -15,6 +18,12 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Show trigger context + run: | + echo "Event: ${{ github.event_name }}" + echo "Release tag: ${{ github.event.client_payload.release_tag || 'manual' }}" + echo "Source repository: ${{ github.event.client_payload.source_repository || github.repository }}" + - name: Set up Node.js uses: actions/setup-node@v4 with: diff --git a/README.md b/README.md index b1824c0..5aaeeb3 100644 --- a/README.md +++ b/README.md @@ -50,9 +50,9 @@ Throughout CDA, "time series" is arbitrarily referred to in both a one-word ("ti ## Developers ### Versioning In order to accommodate changes both to the generator and to CDA itself, cwmsjs is versioned in the following format: -`[generator SemVer]-[generation date]` +`[generator SemVer]-[CDA schema version]` -CDA is expected to at some point expose a CalVer for the latest update. When this is available, the generation date will be replaced with the current CDA CalVer. +The generator now uses the live OpenAPI `info.version` published by CDA. If that field is unavailable, it falls back to the current date. ### Publishing Contributors with authorization can publish a new version of cwmsjs by manually running the "Build, Test, and Publish" GitHub Action. @@ -63,6 +63,7 @@ The workflow will build an updated cwmsjs library using the current generator an - Clone this repository - Install dependencies with: `npm install` +- Optionally set `CWMS_SCHEMA_URL` if you need to build from a non-default CDA schema endpoint - Run the generator with: `npm run build` diff --git a/openapitools.json b/openapitools.json index 5571688..9cbc6d5 100644 --- a/openapitools.json +++ b/openapitools.json @@ -2,6 +2,6 @@ "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { - "version": "7.4.0" + "version": "5.4.0" } } diff --git a/package.json b/package.json index 591da05..ee64887 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cwmsjs-generator", - "version": "2.3.2", + "version": "2.4.0", "description": "OpenAPI generator for building the cwmsjs JavaScript/TypeScript library for CWMS Data API", "author": "USACE,HEC,CWMS,OpenApi Contributors", "repository": { @@ -30,15 +30,16 @@ "typings": "./dist/index.d.ts", "scripts": { "build": "npm run buildApi && npm run buildDocs", - "buildApi": "npm run buildSpec && npm run openapi && npm run modPackage && cd cwmsjs && npm run build", + "buildApi": "npm run buildSpec && npm run openapi && npm run modPackage && npm run postGenerate && cd cwmsjs && npm install && npm run build", "buildDocs": "npm run docs && npm run examples", "buildSpec": "npm run getSpec && npm run modSpec", "clean": "shx mkdir -p ./cwmsjs && shx rm -r ./cwmsjs", - "docs": "cd cwmsjs && npx typedoc src/index.ts", + "docs": "node ./scripts/buildTypedoc.js", "examples": "node ./scripts/tests2exampledocs.js ", - "getSpec": "wget https://cwms-data.usace.army.mil/cwms-data/swagger-docs -O cwms-swagger-raw.json", - "modPackage": "./scripts/package-updates/modPackage.sh", - "modSpec": "./scripts/spec-updates/modSpec.sh", + "getSpec": "node ./scripts/getSpec.js", + "modPackage": "node ./scripts/package-updates/modPackage.js", + "modSpec": "node ./scripts/spec-updates/modSpec.js", + "postGenerate": "node ./scripts/postGenerate.js", "link": "cd cwmsjs && npm link && cd ../tests && npm link cwmsjs", "openapi": "npx @openapitools/openapi-generator-cli generate -g typescript-fetch -o ./cwmsjs -i cwms-swagger-mod.json -c openapi.config.json" }, diff --git a/scripts/buildTypedoc.js b/scripts/buildTypedoc.js new file mode 100644 index 0000000..e0183df --- /dev/null +++ b/scripts/buildTypedoc.js @@ -0,0 +1,107 @@ +const { spawnSync } = require("node:child_process"); +const fs = require("node:fs"); +const path = require("node:path"); + +const rootDir = path.resolve(__dirname, ".."); +const cwmsjsDir = path.join(rootDir, "cwmsjs"); +const docsDir = path.join(cwmsjsDir, "docs"); +const tempDocsDir = path.join(cwmsjsDir, "docs-typedoc"); +const packageJson = JSON.parse( + fs.readFileSync(path.join(cwmsjsDir, "package.json"), "utf8"), +); + +removePath(tempDocsDir, { strict: true }); + +const result = spawnSync( + `npx typedoc src/index.ts --name "cwmsjs v${packageJson.version}" --out docs-typedoc`, + { + cwd: cwmsjsDir, + stdio: "inherit", + shell: true, + }, +); + +if (result.error) { + console.error(result.error.message); + process.exit(1); +} + +if (result.status !== 0) { + process.exit(result.status ?? 1); +} + +fs.mkdirSync(docsDir, { recursive: true }); + +for (const entry of fs.readdirSync(tempDocsDir, { withFileTypes: true })) { + const sourcePath = path.join(tempDocsDir, entry.name); + const destinationPath = path.join(docsDir, entry.name); + + removePath(destinationPath); + copyPath(sourcePath, destinationPath); +} + +removePath(tempDocsDir, { strict: true }); + +function removePath(targetPath, { strict = false } = {}) { + if (!fs.existsSync(targetPath)) { + return; + } + + const targetStats = fs.lstatSync(targetPath); + + try { + fs.rmSync(targetPath, { recursive: true, force: true }); + } catch (error) { + if (process.platform !== "win32") { + throw error; + } + + const cleanupCommand = targetStats.isDirectory() + ? `if exist "${targetPath}" rmdir /s /q "${targetPath}"` + : `if exist "${targetPath}" del /f /q "${targetPath}"`; + const cleanup = spawnSync( + "cmd.exe", + ["/d", "/s", "/c", cleanupCommand], + { + stdio: "inherit", + }, + ); + + if (cleanup.status !== 0 && fs.existsSync(targetPath)) { + throw error; + } + } + + if (fs.existsSync(targetPath)) { + if (strict) { + throw new Error(`Unable to remove ${targetPath}`); + } + console.warn(`Skipping cleanup for locked path: ${targetPath}`); + } +} + +function copyPath(sourcePath, destinationPath) { + const sourceStats = fs.statSync(sourcePath); + + if (sourceStats.isDirectory()) { + fs.mkdirSync(destinationPath, { recursive: true }); + + for (const entry of fs.readdirSync(sourcePath, { withFileTypes: true })) { + copyPath( + path.join(sourcePath, entry.name), + path.join(destinationPath, entry.name), + ); + } + return; + } + + try { + fs.copyFileSync(sourcePath, destinationPath); + } catch (error) { + if (error && error.code === "EPERM") { + console.warn(`Skipping locked file: ${destinationPath}`); + return; + } + throw error; + } +} diff --git a/scripts/docker/buildAll.cmd b/scripts/docker/buildAll.cmd index eb33a6c..a12ba52 100644 --- a/scripts/docker/buildAll.cmd +++ b/scripts/docker/buildAll.cmd @@ -1,4 +1,4 @@ @REM runs all scripts start to finish, using docker where windows might be an issue echo Fetching and Manipulating spec... -.\scripts\docker\buildSpec.cmd && npm run openapi && .\scripts\docker\runPkg.cmd && cd cwmsjs && npm install && npm run build && cd .. +npm.cmd run buildSpec && npm.cmd run openapi && .\scripts\docker\runPkg.cmd && cd cwmsjs && npm.cmd install && npm.cmd run build && cd .. echo Done. diff --git a/scripts/docker/buildSpec.cmd b/scripts/docker/buildSpec.cmd index 14c6afe..5155201 100644 --- a/scripts/docker/buildSpec.cmd +++ b/scripts/docker/buildSpec.cmd @@ -1,2 +1,2 @@ -@REM Run scripts from windows -docker run --rm -v %cd%:/scripts -w /scripts node:lts bash -c "npm install -g node-jq && npm run buildSpec" \ No newline at end of file +@REM Build the live spec from Windows without docker +npm.cmd run buildSpec diff --git a/scripts/exampletemplate.html b/scripts/exampletemplate.html index 525fffe..c99504e 100644 --- a/scripts/exampletemplate.html +++ b/scripts/exampletemplate.html @@ -7,7 +7,7 @@ - ${docName} Example + ${docName} Example | cwmsjs v${packageVersion} @@ -47,7 +47,7 @@ HOME - cwmsjs - v1.15.0 + HOME - cwmsjs - v${packageVersion} @@ -59,6 +59,7 @@
  • ${docName}
  • Example: ${docName}

    +

    cwmsjs v${packageVersion}

    @@ -92,4 +93,4 @@

    Groundwork-Water + React

    - \ No newline at end of file + diff --git a/scripts/getSpec.js b/scripts/getSpec.js new file mode 100644 index 0000000..6b0b471 --- /dev/null +++ b/scripts/getSpec.js @@ -0,0 +1,37 @@ +const fs = require("node:fs/promises"); + +const DEFAULT_SCHEMA_URL = + "https://cwms-data.usace.army.mil/cwms-data/swagger-docs"; +const OUTPUT_PATH = "cwms-swagger-raw.json"; + +async function main() { + const schemaUrl = process.env.CWMS_SCHEMA_URL || DEFAULT_SCHEMA_URL; + const response = await fetch(schemaUrl, { + headers: { + Accept: "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`Failed to download schema: ${response.status} ${response.statusText}`); + } + + const body = await response.text(); + let parsed; + + try { + parsed = JSON.parse(body); + } catch (error) { + throw new Error(`Schema response was not valid JSON: ${error.message}`); + } + + await fs.writeFile(OUTPUT_PATH, `${JSON.stringify(parsed)}\n`, "utf8"); + + const schemaVersion = parsed?.info?.version || "unknown"; + console.log(`Saved ${schemaUrl} to ${OUTPUT_PATH} (schema version ${schemaVersion})`); +} + +main().catch((error) => { + console.error(error.message); + process.exit(1); +}); diff --git a/scripts/package-updates/modPackage.js b/scripts/package-updates/modPackage.js new file mode 100644 index 0000000..d62f467 --- /dev/null +++ b/scripts/package-updates/modPackage.js @@ -0,0 +1,45 @@ +const fs = require("node:fs"); +const path = require("node:path"); + +const rootDir = path.resolve(__dirname, "..", ".."); + +function readJson(relativePath) { + return JSON.parse(fs.readFileSync(path.join(rootDir, relativePath), "utf8")); +} + +function writeJson(relativePath, value) { + fs.writeFileSync( + path.join(rootDir, relativePath), + `${JSON.stringify(value, null, 2)}\n`, + "utf8", + ); +} + +function getVersionSuffix() { + const rawSpec = readJson("cwms-swagger-raw.json"); + return rawSpec?.info?.version || new Date().toISOString().slice(0, 10).replace(/-/g, "."); +} + +function main() { + const rootPackage = readJson("package.json"); + const generatedPackage = readJson("cwmsjs/package.json"); + const updates = readJson("scripts/package-updates/updates.json"); + const versionSuffix = getVersionSuffix(); + + const nextPackage = { + ...generatedPackage, + ...updates, + author: rootPackage.author, + generatorVersion: rootPackage.version, + keywords: rootPackage.keywords, + repository: rootPackage.repository, + version: `${rootPackage.version}-${versionSuffix}`, + }; + + delete nextPackage.publishConfig; + + writeJson("cwmsjs/package.json", nextPackage); + fs.copyFileSync(path.join(rootDir, "README.md"), path.join(rootDir, "cwmsjs", "README.md")); +} + +main(); diff --git a/scripts/postGenerate.js b/scripts/postGenerate.js new file mode 100644 index 0000000..dafde77 --- /dev/null +++ b/scripts/postGenerate.js @@ -0,0 +1,60 @@ +const fs = require("node:fs"); +const path = require("node:path"); + +const rootDir = __dirname ? path.resolve(__dirname, "..") : process.cwd(); + +function replaceInFile(relativePath, replacements) { + const fullPath = path.join(rootDir, relativePath); + if (!fs.existsSync(fullPath)) { + return; + } + + let content = fs.readFileSync(fullPath, "utf8"); + for (const [from, to] of replacements) { + if (!content.includes(from)) { + continue; + } + content = content.replace(from, to); + } + fs.writeFileSync(fullPath, content, "utf8"); +} + +function patchTsConfig(relativePath) { + const fullPath = path.join(rootDir, relativePath); + if (!fs.existsSync(fullPath)) { + return; + } + + const tsconfig = JSON.parse(fs.readFileSync(fullPath, "utf8")); + tsconfig.compilerOptions ??= {}; + tsconfig.compilerOptions.lib = ["es2018", "dom"]; + fs.writeFileSync(fullPath, `${JSON.stringify(tsconfig, null, 2)}\n`, "utf8"); +} + +replaceInFile("cwmsjs/src/models/AbstractRatingMetadata.ts", [ + [ + " return TransitionalRatingToJSON(value);", + " return TransitionalRatingToJSON(value as TransitionalRating);", + ], + [ + " return VirtualRatingToJSON(value);", + " return VirtualRatingToJSON(value as VirtualRating);", + ], +]); + +replaceInFile("cwmsjs/src/models/LocationLevel.ts", [ + [ + " return { ...ConstantLocationLevelFromJSONTyped(json, true), ...SeasonalLocationLevelFromJSONTyped(json, true), ...TimeSeriesLocationLevelFromJSONTyped(json, true), ...VirtualLocationLevelFromJSONTyped(json, true) };", + " return { ...ConstantLocationLevelFromJSONTyped(json, true), ...SeasonalLocationLevelFromJSONTyped(json, true), ...TimeSeriesLocationLevelFromJSONTyped(json, true), ...VirtualLocationLevelFromJSONTyped(json, true) } as LocationLevel;", + ], + [ + " return { ...ConstantLocationLevelToJSON(value), ...SeasonalLocationLevelToJSON(value), ...TimeSeriesLocationLevelToJSON(value), ...VirtualLocationLevelToJSON(value) };", + " return { ...ConstantLocationLevelToJSON(value as any), ...SeasonalLocationLevelToJSON(value as any), ...TimeSeriesLocationLevelToJSON(value as any), ...VirtualLocationLevelToJSON(value as any) };", + ], +]); + +replaceInFile("cwmsjs/src/runtime.ts", [ + ["export type FetchAPI = GlobalFetch['fetch'];", "export type FetchAPI = typeof fetch;"], +]); + +patchTsConfig("cwmsjs/tsconfig.json"); diff --git a/scripts/spec-updates/modSpec.js b/scripts/spec-updates/modSpec.js new file mode 100644 index 0000000..db60dd9 --- /dev/null +++ b/scripts/spec-updates/modSpec.js @@ -0,0 +1,136 @@ +const fs = require("node:fs"); +const path = require("node:path"); + +const rootDir = path.resolve(__dirname, "..", ".."); + +function readJson(relativePath) { + return JSON.parse(fs.readFileSync(path.join(rootDir, relativePath), "utf8")); +} + +function writeJson(relativePath, value) { + fs.writeFileSync( + path.join(rootDir, relativePath), + `${JSON.stringify(value, null, 2)}\n`, + "utf8", + ); +} + +function getVersionSuffix(rawSpec) { + return rawSpec?.info?.version || new Date().toISOString().slice(0, 10).replace(/-/g, "."); +} + +function removeUniqueItems(value) { + if (Array.isArray(value)) { + return value.map(removeUniqueItems); + } + + if (value && typeof value === "object") { + const result = {}; + for (const [key, childValue] of Object.entries(value)) { + if (key === "uniqueItems") { + continue; + } + result[key] = removeUniqueItems(childValue); + } + return result; + } + + return value; +} + +function replaceInString(value) { + return value + .replaceAll("timeseries", "time-series") + .replaceAll("Timeseries", "TimeSeries") + .replaceAll( + "#/components/schemas/AbstractRatingMetadata", + "#/components/schemas/BaseRatingMetadata", + ); +} + +function normalizeStrings(value) { + if (Array.isArray(value)) { + return value.map(normalizeStrings); + } + + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value).map(([key, childValue]) => [ + replaceInString(key), + normalizeStrings(childValue), + ]), + ); + } + + return typeof value === "string" ? replaceInString(value) : value; +} + +function stripCwmsDataPrefix(spec) { + const paths = {}; + for (const [route, routeConfig] of Object.entries(spec.paths || {})) { + const normalizedRoute = route.replace(/^\/cwms-data(?=\/|$)/, "") || "/"; + paths[normalizedRoute] = routeConfig; + } + spec.paths = paths; + spec.servers = (spec.servers || []).map((server) => ({ + ...server, + url: server.url.replace(/\/cwms-data\/?$/, ""), + })); +} + +function normalizeOperationIds(spec) { + for (const pathItem of Object.values(spec.paths || {})) { + if (!pathItem || typeof pathItem !== "object") { + continue; + } + + for (const operation of Object.values(pathItem)) { + if (!operation || typeof operation !== "object" || !operation.operationId) { + continue; + } + + operation.operationId = operation.operationId.replace( + /^(get|post|patch|put|delete)CwmsData(\w*)$/, + "$1$2", + ); + } + } +} + +function main() { + const rawSpec = readJson("cwms-swagger-raw.json"); + const rootPackage = readJson("package.json"); + const servers = readJson("scripts/spec-updates/servers.json"); + const baseRatingMetadata = readJson("scripts/spec-updates/BaseRatingMetadata.json"); + const tsArrayItems = readJson("scripts/spec-updates/tsArrayItems.json"); + + const schemaVersion = getVersionSuffix(rawSpec); + const spec = structuredClone(rawSpec); + + spec.servers = servers.servers; + spec.info.version = `${rootPackage.version}-${schemaVersion}`; + spec.components ??= {}; + spec.components.schemas ??= {}; + spec.components.schemas.BaseRatingMetadata = baseRatingMetadata.BaseRatingMetadata; + + if ( + spec.components.schemas.TimeSeries?.properties?.values?.items && + spec.components.schemas.TimeSeries.properties.values.items.type === "array" + ) { + spec.components.schemas.TimeSeries.properties.values.items.items = tsArrayItems.items; + } + + if (spec.paths?.["/cwms-data/offices"]?.get?.responses?.["200"]?.content?.["application/json"]) { + spec.paths["/cwms-data/offices"].get.responses["200"].content[""] = + spec.paths["/cwms-data/offices"].get.responses["200"].content["application/json"]; + } + + const cleaned = removeUniqueItems(spec); + const normalized = normalizeStrings(cleaned); + normalizeOperationIds(normalized); + stripCwmsDataPrefix(normalized); + + writeJson("cwms-swagger-mod.json", normalized); +} + +main(); diff --git a/scripts/tests2exampledocs.js b/scripts/tests2exampledocs.js index 24f5fb3..7cc8cee 100644 --- a/scripts/tests2exampledocs.js +++ b/scripts/tests2exampledocs.js @@ -18,6 +18,9 @@ const testDirectory = "./tests/endpoints"; const genDirectory = "./tests/generator"; const outputDirectory = "./cwmsjs/docs/examples"; const templatePath = "./scripts/exampletemplate.html"; // Path to your HTML template +const packageVersion = JSON.parse( + fs.readFileSync("./cwmsjs/package.json", "utf8") +).version; // Ensure the output directory exists if (!fs.existsSync(outputDirectory)) { @@ -29,10 +32,11 @@ function getFiles(directories) { directories.forEach((d) => { all_files = fs .readdirSync(d) + .filter((fileName) => fileName.endsWith(".test.js")) .map((fileName) => path.join(d, fileName)) .concat(all_files); }); - return all_files; + return all_files.sort(); } // Read the HTML template fs.readFile(templatePath, "utf8", (err, template) => { @@ -82,6 +86,7 @@ fs.readFile(templatePath, "utf8", (err, template) => { // Replace placeholder in template const filledTemplate = template .replaceAll("${docName}", docFullName) + .replaceAll("${packageVersion}", packageVersion) .replaceAll( "${pageBody}", ` @@ -148,6 +153,7 @@ ${formattedBlock.replaceAll("new ", "new cwmsjs.")}\n`) + .replaceAll("Example:", "") // .replaceAll(":", "") .replaceAll("${docName}", "Home") + .replaceAll("${packageVersion}", packageVersion) .replaceAll(" ", "") .replaceAll( "${pageBody}", @@ -163,36 +169,40 @@ ${formattedBlock.replaceAll("new ", "new cwmsjs.")}\n`) + // Update the modules file and index file to have links to the examples const indexPath = "./cwmsjs/docs/index.html"; const modulesPath = "./cwmsjs/docs/modules.html"; - fs.readFile(modulesPath, "utf8", (err, content) => { - if (err) throw err; - if (content.indexOf("Examples") >= 0) return; - const updatedContent = content.replace( - "", - ` + if (fs.existsSync(modulesPath)) { + fs.readFile(modulesPath, "utf8", (err, content) => { + if (err) throw err; + if (content.indexOf("Examples") >= 0) return; + const updatedContent = content.replace( + "", + `

    Examples Home

    ${exampleLinks} ` - ); - fs.writeFile(modulesPath, updatedContent, (err) => { - if (err) throw err; - console.log(`Updated modules file: ${modulesPath}`); + ); + fs.writeFile(modulesPath, updatedContent, (err) => { + if (err) throw err; + console.log(`Updated modules file: ${modulesPath}`); + }); }); - }); - fs.readFile(indexPath, "utf8", (err, content) => { - if (err) throw err; - if (content.indexOf("Examples") >= 0) return; - const updatedContent = content.replace( - "", - ` + } + if (fs.existsSync(indexPath)) { + fs.readFile(indexPath, "utf8", (err, content) => { + if (err) throw err; + if (content.indexOf("Examples") >= 0) return; + const updatedContent = content.replace( + "", + `

    Examples Home

    ${exampleLinks} ` - ); - fs.writeFile(indexPath, updatedContent, (err) => { - if (err) throw err; - console.log(`Updated modules file: ${indexPath}`); + ); + fs.writeFile(indexPath, updatedContent, (err) => { + if (err) throw err; + console.log(`Updated modules file: ${indexPath}`); + }); }); - }); + } }); function escapeHtml(unsafe) { diff --git a/tests/endpoints/Projects.test.js b/tests/endpoints/Projects.test.js new file mode 100644 index 0000000..a37f82f --- /dev/null +++ b/tests/endpoints/Projects.test.js @@ -0,0 +1,56 @@ +import { Configuration, ProjectsApi } from "cwmsjs"; +import fetch from "node-fetch"; +global.fetch = fetch; + +test("Test Projects", async () => { + const config = new Configuration({ + basePath: "https://cwms-data.usace.army.mil/cwms-data", + }); + const projects_api = new ProjectsApi(config); + + await projects_api + .getProjects({ + office: "SWT", + idMask: "KEYS*", + pageSize: 5, + }) + .then(async (data) => { + expect(data?.projects).toBeDefined(); + expect(Array.isArray(data?.projects)).toBe(true); + + const firstProject = data?.projects?.[0]; + if (firstProject?.location?.name) { + const project = await projects_api.getProjectsWithName({ + office: "SWT", + name: firstProject.location.name, + }); + expect(project?.location?.name).toBeDefined(); + } + + console.log(`Returned ${data?.projects?.length ?? 0} projects`); + }) + .catch(async (e) => { + if (e.response) { + const error_msg = await e.response.json(); + e.message = `${e.response.url}\n${e.message}\n${JSON.stringify( + error_msg, + null, + 2 + )}`; + console.error(e); + throw e; + } else { + throw e; + } + }); + + await projects_api + .getProjectsLocations({ + office: "SWT", + projectLike: "KEYS*", + }) + .then((data) => { + expect(Array.isArray(data)).toBe(true); + console.log(`Returned ${data.length} project child-location groups`); + }); +}, 30000); diff --git a/tests/endpoints/Stream-Locations.test.js b/tests/endpoints/Stream-Locations.test.js new file mode 100644 index 0000000..966994e --- /dev/null +++ b/tests/endpoints/Stream-Locations.test.js @@ -0,0 +1,48 @@ +import { Configuration, StreamLocationsApi } from "cwmsjs"; +import fetch from "node-fetch"; +global.fetch = fetch; + +test("Test Stream Locations", async () => { + const config = new Configuration({ + basePath: "https://cwms-data.usace.army.mil/cwms-data", + }); + const stream_locations_api = new StreamLocationsApi(config); + + await stream_locations_api + .getStreamLocations({ + officeMask: "SWT", + nameMask: "KEYS*", + }) + .then(async (data) => { + expect(Array.isArray(data)).toBe(true); + + const firstLocation = data?.[0]; + if ( + firstLocation?.streamLocationNode?.id?.name && + firstLocation?.streamLocationNode?.streamNode?.streamId?.name + ) { + const detail = await stream_locations_api.getStreamLocationsWithName({ + office: firstLocation.streamLocationNode.id.officeId, + name: firstLocation.streamLocationNode.id.name, + streamId: firstLocation.streamLocationNode.streamNode.streamId.name, + }); + expect(Array.isArray(detail)).toBe(true); + } + + console.log(`Returned ${data.length} stream locations`); + }) + .catch(async (e) => { + if (e.response) { + const error_msg = await e.response.json(); + e.message = `${e.response.url}\n${e.message}\n${JSON.stringify( + error_msg, + null, + 2 + )}`; + console.error(e); + throw e; + } else { + throw e; + } + }); +}, 30000); diff --git a/tests/package.json b/tests/package.json index 6dd28e5..6296b4e 100644 --- a/tests/package.json +++ b/tests/package.json @@ -16,8 +16,10 @@ "test": "npm run jest-modules --", "test-cicd": "npm run jest-modules -- --runInBand --ci --detectOpenHandles", "coverage": "npm run test --coverage", + "docs": "node ../scripts/tests2exampledocs.js", + "smoke": "node smoke.js", "test:ts": "npm run jest-modules Timeseries.v2.test.js", - "link": "cd ../src && npm link && cd ../tests && npm link cwmsjs" + "link": "cd ../cwmsjs && npm link && cd ../tests && npm link cwmsjs" }, "jest": { "clearMocks": true, diff --git a/tests/smoke.js b/tests/smoke.js new file mode 100644 index 0000000..4e3b701 --- /dev/null +++ b/tests/smoke.js @@ -0,0 +1,90 @@ +import { readFile } from "node:fs/promises"; +import { + CatalogApi, + Configuration, + OfficesApi, + ProjectsApi, +} from "../cwmsjs/dist/index.js"; + +if (!global.fetch) { + throw new Error("This smoke test expects a Node.js runtime with native fetch support."); +} + +async function runStep(name, fn) { + process.stdout.write(`${name}... `); + const result = await fn(); + console.log("ok"); + return result; +} + +async function main() { + const packageJson = JSON.parse( + await readFile(new URL("../cwmsjs/package.json", import.meta.url), "utf8") + ); + const rootPackageJson = JSON.parse( + await readFile(new URL("../package.json", import.meta.url), "utf8") + ); + + let expectedVersion = process.env.EXPECTED_CWMSJS_VERSION; + try { + const rawSpec = JSON.parse( + await readFile(new URL("../cwms-swagger-raw.json", import.meta.url), "utf8") + ); + expectedVersion = `${rootPackageJson.version}-${rawSpec?.info?.version}`; + } catch {} + + if (!expectedVersion) { + throw new Error( + "Unable to determine expected cwmsjs version. Run buildApi first or set EXPECTED_CWMSJS_VERSION." + ); + } + + if (packageJson.version !== expectedVersion) { + throw new Error( + `cwmsjs version mismatch. Expected ${expectedVersion}, found ${packageJson.version}.` + ); + } + + console.log(`cwmsjs version: ${packageJson.version}`); + + const config = new Configuration({ + basePath: "https://cwms-data.usace.army.mil/cwms-data", + }); + const officesApi = new OfficesApi(config); + const catalogApi = new CatalogApi(config); + const projectsApi = new ProjectsApi(config); + + await runStep("offices", async () => { + const offices = await officesApi.getOffices(); + console.log(` offices returned: ${offices?.offices?.length ?? offices?.length ?? 0}`); + }); + + await runStep("catalog", async () => { + const catalog = await catalogApi.getCatalogWithDataset({ + office: "SWT", + like: "KEYS..*.Inst.1Hour.0.Ccp-Rev", + dataset: "TIMESERIES", + }); + console.log(` catalog entries returned: ${catalog?.entries?.length ?? 0}`); + }); + + await runStep("projects", async () => { + const projects = await projectsApi.getProjects({ + office: "SWT", + idMask: "KEYS*", + pageSize: 5, + }); + console.log(` projects returned: ${projects?.projects?.length ?? 0}`); + }); +} + +main().catch(async (error) => { + if (error?.response) { + const body = await error.response.text(); + console.error(error.response.url); + console.error(body); + } else { + console.error(error); + } + process.exit(1); +});