Skip to content

Commit

Permalink
feat(config): support using config file instead of cli args
Browse files Browse the repository at this point in the history
  • Loading branch information
uladkasach committed Feb 14, 2023
1 parent ccb8153 commit 1312bb0
Show file tree
Hide file tree
Showing 17 changed files with 343 additions and 68 deletions.
77 changes: 47 additions & 30 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@
"listr": "0.14.3",
"oclif": "3.3.1",
"type-fns": "0.7.0",
"uuid": "9.0.0"
"uuid": "9.0.0",
"yaml": "2.2.1"
},
"devDependencies": {
"@commitlint/cli": "13.1.0",
Expand Down
6 changes: 6 additions & 0 deletions src/contract/__test_assets__/codegen.sql.schema.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
language: postgres
dialect: 10.7
declarations: domain.ts
generates:
sql:
to: generated
6 changes: 2 additions & 4 deletions src/contract/commands/generate.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import Generate from './generate';
describe('command', () => {
it('should be able to generate schema for valid entities declaration', async () => {
await Generate.run([
'-d',
`${__dirname}/../__test_assets__/domain.ts`,
'-t',
`${__dirname}/../__test_assets__/generated`,
'-c',
`${__dirname}/../__test_assets__/codegen.sql.schema.yml`,
]);
});
});
24 changes: 8 additions & 16 deletions src/contract/commands/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,21 @@ export default class Generate extends Command {

public static flags = {
help: Flags.help({ char: 'h' }),
declarations: Flags.string({
char: 'd',
description: 'path to config file, containing entity definitions',
required: true,
default: 'declarations.ts',
}),
target: Flags.string({
char: 't',
description: 'target directory to record generated schema into',
required: true,
default: 'generated',
config: Flags.string({
char: 'c',
description: 'path to config file',
required: false,
default: 'codegen.sql.schema.yml',
}),
};

public async run() {
const { flags } = await this.parse(Generate);
const config = flags.declarations!;
const target = flags.target!;
const config = flags.config;

// get and display the plans
const configPath =
config.slice(0, 1) === '/' ? config : `${process.cwd()}/${config}`; // if starts with /, consider it as an absolute path
const targetDir =
target.slice(0, 1) === '/' ? target : `${process.cwd()}/${target}`; // if starts with /, consider it as an absolute path
await generateSchema({ configPath, targetDirPath: targetDir });
await generateSchema({ configPath });
}
}
29 changes: 29 additions & 0 deletions src/domain/objects/GeneratorConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { DomainObject } from 'domain-objects';
import Joi from 'joi';

export enum DatabaseLanguage {
MYSQL = 'mysql',
POSTGRES = 'postgres',
}

const schema = Joi.object().keys({
rootDir: Joi.string().required(), // dir of config file, to which all config paths are relative
language: Joi.string().valid(...Object.values(DatabaseLanguage)),
dialect: Joi.string().required(),
declarationsPath: Joi.string().required(),
targetDirPath: Joi.string().required(),
});

export interface GeneratorConfig {
rootDir: string;
language: DatabaseLanguage;
dialect: string;
declarationsPath: string;
targetDirPath: string;
}
export class GeneratorConfig
extends DomainObject<GeneratorConfig>
implements GeneratorConfig
{
public static schema = schema;
}
25 changes: 14 additions & 11 deletions src/logic/compose/generateSchema/generateSchema.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { generateAndRecordEntitySchema } from './generateAndRecordEntitySchema';
import { generateSchema } from './generateSchema';
import { normalizeDeclarationContents } from './normalizeDeclarationContents';
import { readConfig } from './readConfig';
import { readDeclarationFile } from './readDeclarationFile';

// we ignore this line since we can't define a type declaration for a relative module
jest.mock('./readConfig');
const readConfigMock = readConfig as jest.Mock;
readConfigMock.mockReturnValue({
declarationsPath: '__DECLARATIONS_PATH__',
targetDirPath: '__TARGET_DIR_PATH__',
});

jest.mock('./readDeclarationFile');
const readDeclarationFileMock = readDeclarationFile as jest.Mock;
readDeclarationFileMock.mockResolvedValue('__CONFIG_CONTENTS__');
readDeclarationFileMock.mockResolvedValue('__DECLARATION_CONTENTS__');

jest.mock('./normalizeDeclarationContents');
const normalizeDeclarationContentsMock =
Expand All @@ -22,39 +28,36 @@ const generateAndRecordEntitySchemaMock =

describe('generateSchema', () => {
beforeEach(() => jest.clearAllMocks());
it('should read the config file', async () => {
it('should read the declaration file', async () => {
await generateSchema({
configPath: '__CONFIG_PATH__',
targetDirPath: '__TARGET_DIR_PATH__',
});
expect(readDeclarationFileMock.mock.calls.length).toEqual(1);
expect(readDeclarationFileMock.mock.calls[0][0]).toMatchObject({
configPath: '__CONFIG_PATH__',
declarationsPath: '__DECLARATIONS_PATH__',
});
});
it('should extract entities from the contents of the config file', async () => {
it('should extract entities from the contents of the declaration file', async () => {
await generateSchema({
configPath: '__CONFIG_PATH__',
targetDirPath: '__TARGET_DIR_PATH__',
});
expect(normalizeDeclarationContentsMock.mock.calls.length).toEqual(1);
expect(normalizeDeclarationContentsMock.mock.calls[0][0]).toMatchObject({
contents: '__CONFIG_CONTENTS__',
contents: '__DECLARATION_CONTENTS__',
});
});
it('should generateAndRecordEntitySchema for each entity defined in the config', async () => {
await generateSchema({
configPath: '__CONFIG_PATH__',
targetDirPath: '__TARGET_DIR_PATH__',
});
expect(generateAndRecordEntitySchemaMock.mock.calls.length).toEqual(2);
expect(generateAndRecordEntitySchemaMock.mock.calls[0][0]).toMatchObject({
targetDirPath: '__TARGET_DIR_PATH__',
entity: { name: '__ENTITY_ONE__' },
targetDirPath: '__TARGET_DIR_PATH__',
});
expect(generateAndRecordEntitySchemaMock.mock.calls[1][0]).toMatchObject({
targetDirPath: '__TARGET_DIR_PATH__',
entity: { name: '__ENTITY_TWO__' },
targetDirPath: '__TARGET_DIR_PATH__',
});
});
});
8 changes: 5 additions & 3 deletions src/logic/compose/generateSchema/generateSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Listr from 'listr';

import { generateAndRecordEntitySchema } from './generateAndRecordEntitySchema';
import { normalizeDeclarationContents } from './normalizeDeclarationContents';
import { readConfig } from './readConfig';
import { readDeclarationFile } from './readDeclarationFile';

/*
Expand All @@ -12,13 +13,14 @@ import { readDeclarationFile } from './readDeclarationFile';
*/
export const generateSchema = async ({
configPath,
targetDirPath,
}: {
configPath: string;
targetDirPath: string;
}) => {
// 0. read the config file
const { declarationsPath, targetDirPath } = await readConfig({ configPath });

// 1. read the entities from source file
const contents = await readDeclarationFile({ configPath });
const contents = await readDeclarationFile({ declarationsPath });
const { entities } = await normalizeDeclarationContents({ contents });

// 2. for each entity: generate and record resources
Expand Down
62 changes: 62 additions & 0 deletions src/logic/compose/generateSchema/readConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
DatabaseLanguage,
GeneratorConfig,
} from '../../../domain/objects/GeneratorConfig';
import { UserInputError } from '../../../utils/errors/UserInputError';
import { readYmlFile } from '../../../utils/fileio/readYmlFile';
import { getDirOfPath } from '../../../utils/filepaths/getDirOfPath';

/*
1. read the config
2. validate the config
*/
export const readConfig = async ({
configPath,
}: {
configPath: string;
}): Promise<GeneratorConfig> => {
const configDir = getDirOfPath(configPath);
const getAbsolutePathFromRelativeToConfigPath = (relpath: string) =>
`${configDir}/${relpath}`;

// get the yml
const contents = await readYmlFile({ filePath: configPath });

// get the language and dialect
if (!contents.language)
throw new UserInputError({ reason: 'config.language must be defined' });
const language = contents.language;
if (contents.language && contents.language !== DatabaseLanguage.POSTGRES)
throw new UserInputError({
reason:
'dao generator only supports postgres. please update the `language` option in your config to `postgres` to continue',
});
if (!contents.dialect)
throw new UserInputError({ reason: 'config.dialect must be defined' });
const dialect = `${contents.dialect}`; // ensure that we read it as a string, as it could be a number

// validate the output config
if (!contents.declarations)
throw new UserInputError({
reason:
'config.declarations must specify path to the file containing declarations',
});
if (!contents.generates.sql?.to)
throw new UserInputError({
reason:
'config.generates.sql.to must specify where to output the generated sql',
});

// return the results
return new GeneratorConfig({
language,
dialect,
rootDir: configDir,
declarationsPath: getAbsolutePathFromRelativeToConfigPath(
contents.declarations,
),
targetDirPath: getAbsolutePathFromRelativeToConfigPath(
contents.generates.sql.to,
),
});
};
Loading

0 comments on commit 1312bb0

Please sign in to comment.