UNPKG

@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
#!/usr/bin/env node // 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); });