scripts/fix_swagger_internal_types.mjs
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env node
/**
* Post-processes swagger output files to produce clean schema names:
*
* 1. Replaces github_com_flexprice_flexprice_internal_types. with types. in all swagger files
* (swagger.json, swagger.yaml, docs.go, swagger-3-0.json) so schema names are clean.
* 2. Strips the "dto." prefix from all schema/definition names in swagger.json, swagger.yaml,
* docs.go, and swagger-3-0.json so generated SDKs get clean type names natively.
* 3. In swagger-3-0.json only: adds x-speakeasy-name-override for each components.schemas key
* that starts with "types." or "errors." so Speakeasy-generated SDKs use clean names,
* FeatureType instead of TypesFeatureType.
* 4. In swagger-3-0.json only: patches string fields with timestamp-like names (created_at, updated_at,
* etc.) with format: date-time so the spec has ≥150 date-time fields for SDK generation.
*
* Usage: node scripts/fix_swagger_internal_types.mjs [--spec path/to/swagger-3-0.json]
*/
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, '..');
// All known github.com/flexprice/flexprice/internal/<pkg> prefixes to strip
const INTERNAL_PREFIXES = [
{ prefix: 'github_com_flexprice_flexprice_internal_types.', replacement: 'types.' },
{ prefix: 'github_com_flexprice_flexprice_internal_errors.', replacement: 'errors.' },
];
// Keep legacy aliases for the types prefix used in the step-1 loop below
const PREFIX = INTERNAL_PREFIXES[0].prefix;
const REPLACEMENT = INTERNAL_PREFIXES[0].replacement;
const FILES = [
'docs/swagger/swagger.json',
'docs/swagger/swagger.yaml',
'docs/swagger/docs.go',
'docs/swagger/swagger-3-0.json',
];
function main() {
const args = process.argv.slice(2);
let specPath = resolve(repoRoot, 'docs/swagger/swagger-3-0.json');
for (let i = 0; i < args.length; i++) {
if (args[i] === '--spec' && args[i + 1]) {
specPath = resolve(args[i + 1]);
break;
}
}
// 1. String replace all internal package prefixes in all files
for (const rel of FILES) {
const path = resolve(repoRoot, rel);
if (!existsSync(path)) continue;
let s = readFileSync(path, 'utf8');
const before = s;
for (const { prefix, replacement } of INTERNAL_PREFIXES) {
s = s.split(prefix).join(replacement);
}
if (s !== before) {
writeFileSync(path, s, 'utf8');
console.log('Updated', rel);
}
}
// 1b. Strip github_com_flexprice_flexprice_internal_domain_<pkg>. prefixes in all files.
// Transforms e.g. "github_com_flexprice_flexprice_internal_domain_addon.Addon" → "Addon".
const domainPrefixRe = /github_com_flexprice_flexprice_internal_domain_[a-z_]+\./g;
for (const rel of FILES) {
const path = resolve(repoRoot, rel);
if (!existsSync(path)) continue;
let s = readFileSync(path, 'utf8');
const before = s;
s = s.replace(domainPrefixRe, '');
if (s !== before) {
writeFileSync(path, s, 'utf8');
console.log(`Stripped domain package prefixes in ${rel}`);
}
}
// 2. Strip "dto." prefix from all schema/definition names in all files.
// Uses a regex to avoid mangling "webhookDto." or similar compound prefixes.
const dtoPrefixRe = /(?<![A-Za-z0-9_])dto\./g;
for (const rel of FILES) {
const path = resolve(repoRoot, rel);
if (!existsSync(path)) continue;
let s = readFileSync(path, 'utf8');
const before = s;
s = s.replace(dtoPrefixRe, '');
if (s !== before) {
writeFileSync(path, s, 'utf8');
console.log(`Stripped dto. prefix in ${rel}`);
}
}
// 3. Add Speakeasy overrides in swagger-3-0.json only
if (!existsSync(specPath)) {
console.log('swagger-3-0.json not found; skipping x-speakeasy-name-override.');
return;
}
const spec = JSON.parse(readFileSync(specPath, 'utf8'));
const schemas = spec.components?.schemas;
if (!schemas || typeof schemas !== 'object') {
console.log('No components.schemas; skipping x-speakeasy-name-override.');
return;
}
// Prefixes to strip so Speakeasy generates clean SDK type names
const STRIP_PREFIXES = ['types.', 'errors.'];
let count = 0;
for (const key of Object.keys(schemas)) {
for (const prefix of STRIP_PREFIXES) {
if (key.startsWith(prefix)) {
const override = key.slice(prefix.length);
if (schemas[key] && typeof schemas[key] === 'object') {
schemas[key]['x-speakeasy-name-override'] = override;
count++;
}
break;
}
}
}
// 4. Patch timestamp fields with format: date-time.
// Matches the same heuristic used by generate_overlay.py so the spec itself
// contains the format info (required for the ≥150 validation criterion) in
// addition to the overlay patches that Speakeasy applies at generation time.
// Note: do not use _period — e.g. billing_period is types.BillingPeriod (MONTHLY), not a timestamp.
const TIMESTAMP_RE = /(_at|_date|_start|_end|_time|_anchor|expires_at|expiry|due_date|close_time|archived_at|applied_at|executed_at|failed_at|finalized_at|completed_at|last_used_at|balance_updated_at|_due_lte|_at_gte|_at_lte)$/;
function getProperties(schema) {
const props = Object.assign({}, schema.properties || {});
for (const combiner of ['allOf', 'anyOf', 'oneOf']) {
for (const sub of (schema[combiner] || [])) {
Object.assign(props, sub.properties || {});
}
}
return props;
}
let dtCount = 0;
for (const schema of Object.values(schemas)) {
if (!schema || typeof schema !== 'object') continue;
const props = getProperties(schema);
for (const [propName, prop] of Object.entries(props)) {
if (prop && prop.type === 'string' && prop.format !== 'date-time' && TIMESTAMP_RE.test(propName)) {
prop.format = 'date-time';
dtCount++;
}
}
}
// 4b. Remove mistaken date-time (legacy: _period suffix used to match billing_period).
const NOT_DATE_TIME_STRING_FIELDS = new Set(['billing_period']);
let stripped = 0;
for (const schema of Object.values(schemas)) {
if (!schema || typeof schema !== 'object') continue;
const props = getProperties(schema);
for (const [propName, prop] of Object.entries(props)) {
if (
NOT_DATE_TIME_STRING_FIELDS.has(propName) &&
prop &&
prop.type === 'string' &&
prop.format === 'date-time'
) {
delete prop.format;
stripped++;
}
}
}
writeFileSync(specPath, JSON.stringify(spec, null, 2) + '\n', 'utf8');
console.log(`Added x-speakeasy-name-override to ${count} prefixed schemas in swagger-3-0.json.`);
console.log(`Patched ${dtCount} timestamp fields with format: date-time in swagger-3-0.json.`);
if (stripped > 0) {
console.log(`Stripped incorrect format: date-time from ${stripped} billing_period field(s).`);
}
}
main();