Skip to content

dev #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,35 @@ yarn ts-openapi-gen generate
> Рекомендуется использовать типы как есть, и всегда выставлять в
> `override_policies` для `types` значение `override`. Однако хуки
> могут являться частью бизнес-логики, или требовать вспомогательных
> действий. Поэтому считайте, что фича с генерацией хуков предназначена для упрощения создания нового раздела, но не для полной автоматизации.
> действий. Поэтому считайте, что фича с генерацией хуков предназначена для упрощения создания нового раздела, но не для полной автоматизации.

# Example v2.0.0-beta

const generator =
new OpenApiGenerator(
new URLRequest('https://example.com/index.yaml')
);

export default generaotr;

or

const generator =
new OpenApiGenerator(
new FileRequest('/mnt/d/users/john/index.yaml')
);

export default generator;





# Middlewares

[load.document.before] - takes a URL/path to document as argument, expects parsed object as output
[load.document.after] - takes parsed object as argument, expects modified object as output

# Events

[load.progress] - takes a percentage of progress as argument, expects void. For printing or formatting.
Binary file added bun.lockb
Binary file not shown.
67 changes: 34 additions & 33 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,55 +45,56 @@
"license": "ISC",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^10.1.0",
"@inquirer/input": "^1.0.7",
"@inquirer/prompts": "^1.1.3",
"@oclif/core": "^2",
"@oclif/plugin-help": "^5",
"@inquirer/input": "^1.2.16",
"@inquirer/prompts": "^1.2.3",
"@oclif/core": "^2.16.0",
"@oclif/plugin-help": "^5.2.20",
"@oclif/plugin-plugins": "^2.4.7",
"@openapi-contrib/openapi-schema-to-json-schema": "^4.0.4",
"@openapitools/openapi-generator-cli": "^2.5.2",
"@stoplight/json-schema-ref-parser": "^9.2.4",
"@stoplight/yaml": "^4.2.3",
"@openapi-contrib/openapi-schema-to-json-schema": "^4.0.5",
"@openapitools/openapi-generator-cli": "^2.13.9",
"@stoplight/json-schema-ref-parser": "^9.2.7",
"@stoplight/yaml": "^4.3.0",
"add": "^2.0.6",
"case": "^1.6.3",
"code-block-writer": "^12.0.0",
"fastify": "^4.12.0",
"fastify": "^4.28.1",
"js-yaml": "^4.1.0",
"jsdom": "^22.0.0",
"jsdom": "^22.1.0",
"json-schema": "^0.4.0",
"json-schema-resolver": "^2.0.0",
"json-schema-to-typescript": "^12.0.0",
"kleur": "^4.1.5",
"lodash.keyby": "^4.6.0",
"node-fetch-commonjs": "^3.2.4",
"openapi-typescript": "^6.1.0",
"p-queue-compat": "^1.0.223",
"middleware-io": "^2.8.1",
"node-fetch-commonjs": "^3.3.2",
"openapi-typescript": "^6.7.6",
"p-queue-compat": "^1.0.226",
"prettier": "^2.8.8",
"ts-morph": "^18.0.0",
"yaml": "^2.1.1",
"yargs": "^17.6.2",
"yarn": "^1.22.19"
"yaml": "^2.5.1",
"yargs": "^17.7.2",
"yarn": "^1.22.22"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"@types/d3": "^7.4.0",
"@types/js-yaml": "^4.0.5",
"@types/jsdom": "^21.1.1",
"@types/lodash.keyby": "^4.6.7",
"@types/node": "^18.0.0",
"@types/yargs": "^17.0.22",
"@typescript-eslint/eslint-plugin": "^5.59.6",
"@typescript-eslint/parser": "^5.59.6",
"eslint": "^8.40.0",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/d3": "^7.4.3",
"@types/js-yaml": "^4.0.9",
"@types/jsdom": "^21.1.7",
"@types/lodash.keyby": "^4.6.9",
"@types/node": "^18.19.50",
"@types/yargs": "^17.0.33",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"eslint": "^8.57.1",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-oclif": "^4.0.0",
"eslint-config-oclif-typescript": "^1.0.3",
"eslint-config-prettier": "^8.8.0",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-unicorn": "^47.0.0",
"oclif": "^3",
"openapi-types": "^12.1.0",
"ts-node": "^10.9.1",
"tslib": "^2.5.0",
"typescript": "^5.0.4"
"oclif": "^3.17.2",
"openapi-types": "^12.1.3",
"ts-node": "^10.9.2",
"tslib": "^2.7.0",
"typescript": "^5.6.2"
}
}
}
63 changes: 48 additions & 15 deletions src/codeGen/ReactQueryHookGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,18 @@ export class ReactQueryHookGenerator {
this.typeFetcher = typeFetcher;
}

async checkFilePathForGroup(group: string) {
const folder = join(this.config.output_path, group);
async mkdirs(relativePath: string) {
const folder = join(this.config.output_path, relativePath);
await mkdir(folder, { recursive: true });
const filePath = `${folder}/index.ts`;

const isExisting = existsSync(filePath);

if (this.overridePolicy === 'skip' && isExisting) {
console.log(kleur.italic(kleur.bgWhite(kleur.black(group))), 'хуки react-query пропущены как существующие');
console.log(
kleur.italic(kleur.bgWhite(kleur.black(relativePath))),
'хуки react-query пропущены как существующие'
);
return null;
}

Expand Down Expand Up @@ -291,8 +294,20 @@ export class ReactQueryHookGenerator {
});
}

async generate(group: string, flatOperations: AugmentedOperation[]) {
const filePath = await this.checkFilePathForGroup(group);
async generate(operations: AugmentedOperation[]) {
const splitted: Record<string, AugmentedOperation[]> = {};

for (const operation of operations) {
if (!splitted[operation.storePath]) splitted[operation.storePath] = [];
splitted[operation.storePath].push(operation);
}

const keys = Object.keys(splitted);
await Promise.all(keys.map(path => this.generateSingle(path, splitted[path])));
}

private async generateSingle(relativePath: string, operations: AugmentedOperation[]) {
const filePath = await this.mkdirs(relativePath);
if (!filePath) return;

const project = new Project();
Expand All @@ -306,35 +321,53 @@ export class ReactQueryHookGenerator {

imports.push(...this.config['react-query'].imports);

this.queryKeysSchema = generateQueryKeysSchema(flatOperations);
this.queryKeysSchema = generateQueryKeysSchema(operations);
generateQueryKeysConstant(this.queryKeysSchema, sourceFile);

for (const operation of flatOperations) {
const enumsPath = join(this.config.output_path, relativePath, 'enums.ts');
const hasEnums = existsSync(enumsPath);

if (hasEnums) {
sourceFile.addExportDeclaration({
moduleSpecifier: './enums',
});
}

sourceFile.addExportDeclaration({
moduleSpecifier: './types',
});

for (const operation of operations) {
const queryParams = operation.queryParams;

if (queryParams.length > 0 && operation.isMutation) {
console.error('Mutations with queryParams are not supported yet: check operation', operation.path);
console.error(
'Mutations with queryParams are not supported yet: check operation',
operation.originalPath
);
continue;
}

const types = this.typeFetcher(operation);

// eslint-disable-next-line unicorn/consistent-function-scoping
const resolveImport = (path: string) => {
const fullPath = join(this.config.output_path, path).replaceAll('\\', '/');
const resolveImport = (importPath: string) => {
const fullPath = join(this.config.output_path, importPath).replaceAll('\\', '/');
const filePathNorm = filePath.replaceAll('\\', '/');

let relativePath = relative(dirname(filePathNorm), fullPath).replaceAll('\\', '/');
let relativeImportPath = relative(dirname(filePathNorm), fullPath).replaceAll('\\', '/');

if (relativePath[0] !== '.') {
relativePath = './' + relativePath;
if (relativeImportPath[0] !== '.') {
relativeImportPath = './' + relativeImportPath;
}

return relativePath;
return relativeImportPath;
};

if (!types.response) {
console.error('No response found in operation ' + operation.path + ', of group ' + operation.group);
console.error(
'No response found in operation ' + operation.originalPath + ', of group ' + operation.group
);
continue;
}

Expand Down
6 changes: 3 additions & 3 deletions src/codeGen/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export type QueryKeysSchema = {
items: Record<OperationId, AugmentedOperation & { hasPathParams: boolean; hasBody: boolean }>;
};

export function generateQueryKeysSchema(flatOperations: AugmentedOperation[]): QueryKeysSchema {
const searchOperations = flatOperations.filter(e => SEARCH_OPCODES.includes(parseOpcode(e)));
export function generateQueryKeysSchema(operations: AugmentedOperation[]): QueryKeysSchema {
const searchOperations = operations.filter(e => SEARCH_OPCODES.includes(parseOpcode(e)));

const items = searchOperations.reduce((acc, searchOperation) => {
const pathParameters =
Expand Down Expand Up @@ -93,7 +93,7 @@ export function createCallQueryKey(operation: AugmentedOperation, type: 'query'
const keyParts = operation.pathVariables.map(e => `${keyPrefix}${e}`).join(',');

// Always invalidate search of multiple entities.
if (type === 'mutation' && operation.path.endsWith(':search'))
if (type === 'mutation' && operation.originalPath.endsWith(':search'))
return `QueryKeys.${operation.original.operationId}()`;

if (operation.original.requestBody && isHavePathParams) {
Expand Down
21 changes: 9 additions & 12 deletions src/commands/generate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
import input from '@inquirer/input';
import { checkbox, select } from '@inquirer/prompts';
import { Args, Command } from '@oclif/core';
import { writeFile } from 'fs/promises';
import { OpenAPIV3 } from 'openapi-types';

import { ReactQueryHookGenerator } from '../../codeGen/ReactQueryHookGenerator';
import { ParsedSchema, SchemaParser } from '../../common/SchemaParser';
import { SchemaParser } from '../../common/SchemaParser';
import { runEslintAutoFix } from '../../common/helpers';
import { OverridePolicy } from '../../common/types';
import { Config, ConfigSchema, Target } from '../../config/Config';
Expand All @@ -29,7 +30,6 @@ export default class Generate extends Command {

private conf!: ConfigSchema;
private isSomePrompted = false;
private parsedSchema!: ParsedSchema;

private async applyArgsToConfig() {
const { args } = await this.parse(Generate);
Expand Down Expand Up @@ -140,8 +140,11 @@ export default class Generate extends Command {
await traverseAndModify(
indexDocument,
async ref => {
// TODO: apply load.document.before middlewares
const result = await loader.loadJson(ref.absolutePath);

// TODO: apply load.document.after, return result

if (!ref.target) return result;

if (result.components && ref.target.startsWith('components')) {
Expand Down Expand Up @@ -169,13 +172,11 @@ export default class Generate extends Command {
);

const schemaParser = new SchemaParser(this.conf, indexDocument);
this.parsedSchema = await schemaParser.parse();

const { groups, derefedPathGroupedOps } = this.parsedSchema;
const parsedSchema = await schemaParser.parse();

const typeRenderer = new TypeRenderer({
overridePolicy: this.conf.override_policies[Target.TYPES]!,
parsedSchema: this.parsedSchema,
parsedSchema: parsedSchema,
config: this.conf,
});

Expand All @@ -190,18 +191,14 @@ export default class Generate extends Command {
config: this.conf,
overridePolicy: override_policies[Target.REACT_QUERY]!,
typeFetcher: operation => {
const types = typeRenderer.getTypesForRequest(operation.path, operation.method as any)!;
const types = typeRenderer.getTypesForRequest(operation.originalPath, operation.method as any)!;
return types;
},
});

console.log('⏳ Генерируем хуки react-query...');

await Promise.all(
groups.map(group => {
return hookGen.generate(group, derefedPathGroupedOps[group]);
})
);
await hookGen.generate(parsedSchema.operations);

console.log('✔️ Хуки сгенерированы!');
}
Expand Down
Loading