Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
147 lines
4.3 KiB
TypeScript
147 lines
4.3 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
type OperationUse = {
|
|
method: string;
|
|
path: string;
|
|
status: string;
|
|
};
|
|
|
|
type ScanResult = {
|
|
file: string;
|
|
componentCount: number;
|
|
operationCount: number;
|
|
components: Array<{
|
|
name: string;
|
|
operationCount: number;
|
|
examples: OperationUse[];
|
|
}>;
|
|
inlineUntypedDataResponses: OperationUse[];
|
|
};
|
|
|
|
function readJson(filePath: string): any {
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
return JSON.parse(content);
|
|
}
|
|
|
|
function scanSpec(filePath: string): ScanResult {
|
|
const spec = readJson(filePath);
|
|
|
|
const componentNames = new Set<string>();
|
|
const schemas = spec?.components?.schemas ?? {};
|
|
for (const [name, schema] of Object.entries<any>(schemas)) {
|
|
if ((schema?.properties ?? {}).data && JSON.stringify(schema.properties.data) === '{}') {
|
|
componentNames.add(name);
|
|
}
|
|
}
|
|
|
|
const usesByComponent = new Map<string, OperationUse[]>();
|
|
const inlineUntypedDataResponses: OperationUse[] = [];
|
|
|
|
for (const [routePath, methods] of Object.entries<any>(spec?.paths ?? {})) {
|
|
for (const [method, operation] of Object.entries<any>(methods ?? {})) {
|
|
if (!operation || typeof operation !== 'object') continue;
|
|
for (const [status, response] of Object.entries<any>(operation.responses ?? {})) {
|
|
const schema = response?.content?.['application/json']?.schema;
|
|
if (!schema || typeof schema !== 'object') continue;
|
|
|
|
if (schema.$ref && typeof schema.$ref === 'string') {
|
|
const name = schema.$ref.split('/').at(-1) ?? '';
|
|
if (componentNames.has(name)) {
|
|
const entries = usesByComponent.get(name) ?? [];
|
|
entries.push({ method: method.toUpperCase(), path: routePath, status });
|
|
usesByComponent.set(name, entries);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const dataSchema = schema?.properties?.data;
|
|
if (dataSchema && JSON.stringify(dataSchema) === '{}') {
|
|
inlineUntypedDataResponses.push({ method: method.toUpperCase(), path: routePath, status });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const components = Array.from(componentNames)
|
|
.sort()
|
|
.map((name) => {
|
|
const uses = usesByComponent.get(name) ?? [];
|
|
return {
|
|
name,
|
|
operationCount: uses.length,
|
|
examples: uses.slice(0, 5),
|
|
};
|
|
});
|
|
|
|
return {
|
|
file: filePath,
|
|
componentCount: componentNames.size,
|
|
operationCount: components.reduce((sum, item) => sum + item.operationCount, 0),
|
|
components,
|
|
inlineUntypedDataResponses,
|
|
};
|
|
}
|
|
|
|
function resolveInputPaths(args: string[]): string[] {
|
|
if (args.length > 0) {
|
|
return args.map((arg) => path.resolve(process.cwd(), arg));
|
|
}
|
|
|
|
return [
|
|
path.resolve(process.cwd(), 'docs/openapi/alga-openapi.ce.json'),
|
|
path.resolve(process.cwd(), 'docs/openapi/alga-openapi.ee.json'),
|
|
];
|
|
}
|
|
|
|
function printResult(result: ScanResult) {
|
|
console.log(`\n# ${result.file}`);
|
|
console.log(`components_with_untyped_data=${result.componentCount}`);
|
|
console.log(`operations_using_untyped_components=${result.operationCount}`);
|
|
|
|
if (result.components.length === 0 && result.inlineUntypedDataResponses.length === 0) {
|
|
console.log('no_untyped_success_data_found=true');
|
|
return;
|
|
}
|
|
|
|
for (const component of result.components) {
|
|
console.log(`- component=${component.name} operations=${component.operationCount}`);
|
|
for (const sample of component.examples) {
|
|
console.log(` example=${sample.method} ${sample.path} (${sample.status})`);
|
|
}
|
|
}
|
|
|
|
if (result.inlineUntypedDataResponses.length > 0) {
|
|
console.log(`inline_untyped_data_responses=${result.inlineUntypedDataResponses.length}`);
|
|
for (const sample of result.inlineUntypedDataResponses.slice(0, 10)) {
|
|
console.log(` inline_example=${sample.method} ${sample.path} (${sample.status})`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function main() {
|
|
const inputPaths = resolveInputPaths(process.argv.slice(2));
|
|
let hasFindings = false;
|
|
|
|
for (const filePath of inputPaths) {
|
|
if (!fs.existsSync(filePath)) {
|
|
console.error(`missing_spec_file=${filePath}`);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
const result = scanSpec(filePath);
|
|
printResult(result);
|
|
|
|
if (result.componentCount > 0 || result.inlineUntypedDataResponses.length > 0) {
|
|
hasFindings = true;
|
|
}
|
|
}
|
|
|
|
if (hasFindings) {
|
|
process.exitCode = 2;
|
|
}
|
|
}
|
|
|
|
main();
|