@general-dexterity/cube-records-codegen
Version:
CLI tool for generating Cube Record type definitions from a CubeJS server
372 lines (363 loc) • 11.3 kB
JavaScript
// src/index.ts
import { Console as Console3 } from "console";
import { readFileSync } from "fs";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
import { Command } from "commander";
// src/code-generator.ts
import { Console as Console2 } from "console";
// src/utils.ts
import ts from "typescript";
var dimensionTypeToTsType = (type) => {
switch (type) {
case "number":
return ts.SyntaxKind.NumberKeyword;
case "string":
case "time":
return ts.SyntaxKind.StringKeyword;
case "boolean":
return ts.SyntaxKind.BooleanKeyword;
default:
throw new Error(`Unknown dimension type: ${type}`);
}
};
var cubeMeasureToPropertyName = (measure) => {
return measure.split(".")[1];
};
var isNil = (value) => {
return value === null || value === void 0;
};
// src/definition-retriever.ts
var DefinitionRetriever = class {
baseUrl;
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async retrieveDefinitions() {
const url = this.baseUrl.endsWith("/") ? `${this.baseUrl}v1/meta` : `${this.baseUrl}/v1/meta`;
const response = await fetch(url);
const data = await response.json();
const cubes = data.cubes;
const byRelation = this.groupByRelation(cubes);
const cubesWithRelations = cubes.map((cube) => {
const relationKey = cube.connectedComponent?.toString() ?? "";
const relations = byRelation[relationKey] ?? [];
return {
...cube,
joins: relations.map((c) => c.name).filter((c) => c !== cube.name)
};
});
return cubesWithRelations;
}
groupByRelation(defs) {
return defs.reduce(
(acc, def) => {
if (isNil(def.connectedComponent)) {
return acc;
}
const key = def.connectedComponent.toString();
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(def);
return acc;
},
{}
);
}
};
// src/output-writer.ts
import { Console } from "console";
import { writeFile } from "fs/promises";
import ts2 from "typescript";
var errorLogger = new Console(process.stderr);
var OutputWriter = class {
async writeNodes(nodes, path) {
const content = this.printNodes(nodes);
await this.writeOutput(content, path);
}
async writeOutput(content, path) {
if (path === "-") {
console.log(content);
} else {
try {
await writeFile(path, content, "utf8");
} catch (err) {
errorLogger.error("An error occurred while writing to file:");
errorLogger.error(err);
throw err;
}
}
}
printNodes(nodes) {
const sourceFile = ts2.createSourceFile(
"output.ts",
"",
ts2.ScriptTarget.Latest
);
const printer = ts2.createPrinter({ newLine: ts2.NewLineKind.LineFeed });
return printer.printList(
ts2.ListFormat.MultiLine,
ts2.factory.createNodeArray(nodes),
sourceFile
);
}
};
// src/type-generator.ts
import ts3 from "typescript";
var TypeGenerator = class {
generateTypes(definitions) {
return this.createModuleAugmentation(definitions);
}
createModuleAugmentation(definitions) {
const cubeRecordMap = ts3.factory.createInterfaceDeclaration(
void 0,
// No modifiers
ts3.factory.createIdentifier("CubeRecordMap"),
[],
void 0,
definitions.map((definition) => {
const cubeNameLowercase = definition.name.toLowerCase();
return ts3.factory.createPropertySignature(
void 0,
ts3.factory.createIdentifier(cubeNameLowercase),
void 0,
ts3.factory.createTypeLiteralNode([
ts3.factory.createPropertySignature(
void 0,
ts3.factory.createIdentifier("measures"),
void 0,
this.createMeasuresType(definition.measures)
),
ts3.factory.createPropertySignature(
void 0,
ts3.factory.createIdentifier("dimensions"),
void 0,
this.createDimensionsType(definition.dimensions)
),
ts3.factory.createPropertySignature(
void 0,
ts3.factory.createIdentifier("joins"),
ts3.factory.createToken(ts3.SyntaxKind.QuestionToken),
this.createJoinsType(definition.joins)
)
])
);
})
);
const importDeclaration = ts3.factory.createImportDeclaration(
void 0,
void 0,
ts3.factory.createStringLiteral("@general-dexterity/cube-records"),
void 0
);
const emptyExport = ts3.factory.createExportDeclaration(
void 0,
false,
ts3.factory.createNamedExports([]),
void 0,
void 0
);
const moduleAugmentation = ts3.factory.createModuleDeclaration(
[ts3.factory.createToken(ts3.SyntaxKind.DeclareKeyword)],
ts3.factory.createStringLiteral("@general-dexterity/cube-records"),
ts3.factory.createModuleBlock([cubeRecordMap]),
ts3.NodeFlags.None
);
return [importDeclaration, moduleAugmentation, emptyExport];
}
// Helper method to create measures type from cube definition
createMeasuresType(measures) {
return ts3.factory.createTypeLiteralNode(
measures.map((measure) => {
const propertyName = cubeMeasureToPropertyName(measure.name);
const properties = [
ts3.factory.createPropertySignature(
void 0,
ts3.factory.createIdentifier("type"),
void 0,
ts3.factory.createKeywordTypeNode(
dimensionTypeToTsType(measure.type)
)
)
];
if (measure.type) {
properties.push(
ts3.factory.createPropertySignature(
void 0,
ts3.factory.createIdentifier("__cubetype"),
ts3.factory.createToken(ts3.SyntaxKind.QuestionToken),
ts3.factory.createLiteralTypeNode(
ts3.factory.createStringLiteral(measure.type)
)
)
);
}
return ts3.factory.createPropertySignature(
void 0,
ts3.factory.createIdentifier(propertyName),
void 0,
ts3.factory.createTypeLiteralNode(properties)
);
})
);
}
// Helper method to create dimensions type from cube definition
createDimensionsType(dimensions) {
return ts3.factory.createTypeLiteralNode(
dimensions.map((dimension) => {
const propertyName = cubeMeasureToPropertyName(dimension.name);
const properties = [
ts3.factory.createPropertySignature(
void 0,
ts3.factory.createIdentifier("type"),
void 0,
ts3.factory.createKeywordTypeNode(
dimensionTypeToTsType(dimension.type)
)
)
];
if (dimension.type) {
properties.push(
ts3.factory.createPropertySignature(
void 0,
ts3.factory.createIdentifier("__cubetype"),
void 0,
// Not optional for dimensions, as we need it for time filtering
ts3.factory.createLiteralTypeNode(
ts3.factory.createStringLiteral(dimension.type)
)
)
);
}
return ts3.factory.createPropertySignature(
void 0,
ts3.factory.createIdentifier(propertyName),
void 0,
ts3.factory.createTypeLiteralNode(properties)
);
})
);
}
// Helper method to create joins type from cube definition
createJoinsType(joins) {
if (!joins || joins.length === 0) {
return ts3.factory.createTupleTypeNode([]);
}
return ts3.factory.createTupleTypeNode(
joins.map(
(join2) => ts3.factory.createLiteralTypeNode(
ts3.factory.createStringLiteral(join2.toLowerCase())
)
)
);
}
};
// src/code-generator.ts
var debugLogger = new Console2(process.stderr);
var CodeGenerator = class _CodeGenerator {
options;
typeGenerator;
outputWriter;
constructor(options) {
this.options = options;
this.typeGenerator = new TypeGenerator();
this.outputWriter = new OutputWriter();
}
static async run(options) {
const generator = new _CodeGenerator(options);
await generator.run();
}
async run() {
const retriever = new DefinitionRetriever(this.options.baseUrl);
let shouldStop = false;
while (!shouldStop) {
shouldStop = !this.options.watch;
const allDefinitions = await retriever.retrieveDefinitions();
const excludedDefinitions = this.options.exclude;
const definitions = allDefinitions.filter(
(definition) => !excludedDefinitions.includes(definition.name)
);
debugLogger.debug("Generating types...");
const declarations = this.typeGenerator.generateTypes(definitions);
await this.outputWriter.writeNodes(declarations, this.options.output);
if (shouldStop) {
break;
}
debugLogger.debug("Sleeping for %d ms...", this.options.watchDelay);
await new Promise(
(resolve) => setTimeout(resolve, this.options.watchDelay)
);
}
}
};
// src/index.ts
var __dirname2 = dirname(fileURLToPath(import.meta.url));
var packageJson = JSON.parse(
readFileSync(join(__dirname2, "../package.json"), "utf8")
);
var errorLogger2 = new Console3(process.stderr);
var program = new Command();
var defaults = {
baseUrl: "http://localhost:4000/cubejs-api",
watch: false,
delay: 5e3,
exclude: "",
viewsOnly: false,
output: "-"
};
program.name("cube-record-gen").version(packageJson.version).description("Generate Cube Record type definitions from a CubeJS server.").option(
"-b, --baseUrl [value]",
"Set the CubeJS server's base URL",
defaults.baseUrl
).option(
"-w, --watch",
"Watch for changes in the meta endpoint and regenerate the type definitions",
defaults.watch
).option(
"-d, --duration [value]ms",
"Set the how often we should check for changes in the meta endpoint",
Number,
defaults.delay
).option(
"-o, --output [path]",
"The path of the file where the types should be generated. Use '-' to output to stdout",
defaults.output
).parse(process.argv);
var args = program.opts();
if (process.env.DEBUG) {
errorLogger2.debug("cube-record-gen: Debug mode enabled");
errorLogger2.debug(
`cube-record-gen: Arguments: ${JSON.stringify(args, null, 2)}`
);
}
async function main() {
try {
const output = args.output;
if (isNil(output)) {
errorLogger2.error(
"cube-type-sync: Missing required option: output. Use --help for more information."
);
process.exit(1);
}
const options = {
...args,
output,
baseUrl: args.baseUrl ?? defaults.baseUrl,
exclude: args.exclude?.split(",") ?? [],
watchDelay: args.delay ?? defaults.delay,
watch: args.watch ?? defaults.watch
};
await CodeGenerator.run(options);
} catch (error) {
errorLogger2.error("An error occurred while generating types: ");
errorLogger2.error(error);
errorLogger2.error("CLI options: ");
errorLogger2.table(args);
process.exit(1);
}
}
main().catch((_error) => {
process.exit(1);
});