The normalize operation reshapes OpenAPI spec files in place so they can be consumed by stackql's relational analyzer. It is a pure client-side lowering step - upstream server behaviour is not changed, only the local spec representation.
stackql is a relational backend, so polymorphism (oneOf / anyOf) cannot be represented as distinct SQL columns, and opaque type: object schemas with no defined fields have nothing for DESCRIBE to project. The normalize operation applies four passes that collapse these shapes into something the analyzer can handle:
renameVariants(top-level scope only) - rewritesoneOf/anyOftoallOfat top-level component schemas, their direct properties, and request / response body schemas and their direct properties. Deeper polymorphism is left alone because the analyzer does not descend into it.stripMisplacedSchemaKeywords(whole-doc walk) - in anyproperties:map, deletes children whose name is a schema keyword (type,required,description,title,format,minItems,maxItems,minimum,maximum,default,nullable,readOnly,writeOnly,deprecated,additionalProperties,patternProperties) AND whose value is a scalar. These are upstream indentation bugs where e.g. the outer schema'stype: objectwas indented one level too deep and landed insidepropertiesas if it were a property named "type". Properties whose value is a proper schema object are preserved.convertOpaqueObjectsToStrings(whole-doc walk) - any schema withtype: objectand none of{properties, additionalProperties, patternProperties, allOf, oneOf, anyOf, $ref}is rewritten totype: stringwith(opaque JSON object)appended to the description. stackql then exposes it as a queryable JSON-blob column instead of failingDESCRIBEwith "No columns found".walkAllOf/flattenAllOf(whole-doc walk) - flattens everyallOfarray by merging members (resolving$refs, deep-cloning) into a single schema.requiredarrays are unioned;propertiesmerge without overwriting.$refcycles are handled via aseenRefsset.
The operation rewrites every .yaml / .yml file in the target directory in place.
async function normalize(options) {
// Implementation details
}| Parameter | Type | Required | Description |
|---|---|---|---|
apiDir |
string | Yes | Directory containing .yaml / .yml OpenAPI service files |
verbose |
boolean | No | Whether to log per-pass detail for every file (default: false) |
The function returns a Promise that resolves to an aggregate stats object across every processed file:
{
allOfFlattened: number, // count of allOf arrays merged
oneOfRenamed: number, // count of oneOf -> allOf rewrites
anyOfRenamed: number, // count of anyOf -> allOf rewrites
stripped: Array<string>, // JSON-pointer-ish paths of stripped keywords
opaqueConverted: Array<string>, // paths of object->string rewrites
filesProcessed: number // how many files were rewritten
}import { providerdev } from '@stackql/provider-utils';
async function normalizeExample() {
try {
const stats = await providerdev.normalize({
apiDir: './provider-dev/source',
verbose: true,
});
console.log(`Processed ${stats.filesProcessed} file(s).`);
console.log(`Flattened ${stats.allOfFlattened} allOf array(s).`);
console.log(`Renamed ${stats.oneOfRenamed} oneOf / ${stats.anyOfRenamed} anyOf to allOf.`);
console.log(`Stripped ${stats.stripped.length} misplaced schema keyword(s).`);
console.log(`Converted ${stats.opaqueConverted.length} opaque object schema(s) to string.`);
} catch (error) {
console.error('Error normalizing specs:', error);
}
}
normalizeExample();The operation is also exposed as a CLI via the provider-dev-utils bin entry:
npx provider-dev-utils normalize --api-dir ./provider-dev/source [--verbose]| Flag | Required | Description |
|---|---|---|
--api-dir DIR |
Yes | Directory containing .yaml / .yml files to rewrite in place |
--verbose |
No | Log per-pass detail for every file |
Run normalize on the output of providerdev.split (or on any hand-authored service spec directory) before providerdev.generate. Typical pipeline:
providerdev.split-> per-service YAML files inprovider-dev/source/providerdev.normalize-> rewrite those files in place for relational consumptionproviderdev.analyze-> produce mapping recommendationsproviderdev.generate-> build the stackql provider
- In-place rewrite: the original files are overwritten. Keep the raw spec under
provider-dev/downloaded/(or equivalent) so you can re-split if needed. - Lossy for opaque objects: pass 3 trades structural detail for a queryable column. The original shape is not recoverable from the normalized file.
- Shallow variant rename: pass 1 only rewrites
oneOf/anyOfat the sites the analyzer reads. Variants buried inside array items or nested properties are preserved on purpose - collapsing them would risk lossy merges for no analyzer benefit. - YAML output formatting: files are written with
js-yamlusing{ lineWidth: -1, noRefs: true }, which matches the existing provider-devflatten.mjsoutput but may reorder keys relative to the input.