Merge pull request #5222 from abpframework/feat/5202

Introduced separate commands for adding, removing, and refreshing proxies
pull/5240/head
Halil İbrahim Kalkan 5 years ago committed by GitHub
commit 0d27c0f785
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,9 +1,19 @@
{
"schematics": {
"proxy": {
"description": "ABP Proxy Generator Schematics",
"factory": "./commands/proxy",
"schema": "./commands/proxy/schema.json"
"proxy-add": {
"description": "ABP Proxy Generator Add Schematics",
"factory": "./commands/proxy-add",
"schema": "./commands/proxy-add/schema.json"
},
"proxy-refresh": {
"description": "ABP Proxy Generator Refresh Schematics",
"factory": "./commands/proxy-refresh",
"schema": "./commands/proxy-refresh/schema.json"
},
"proxy-remove": {
"description": "ABP Proxy Generator Remove Schematics",
"factory": "./commands/proxy-remove",
"schema": "./commands/proxy-remove/schema.json"
},
"api": {
"description": "ABP API Generator Schematics",

@ -10,15 +10,17 @@ import {
url,
} from '@angular-devkit/schematics';
import { Exception } from '../../enums';
import { ServiceGeneratorParams } from '../../models';
import { GenerateProxySchema, ServiceGeneratorParams } from '../../models';
import {
applyWithOverwrite,
buildDefaultPath,
createApiDefinitionReader,
createControllerToServiceMapper,
createImportRefsToModelReducer,
createImportRefToEnumMapper,
createProxyConfigReader,
createProxyConfigWriterCreator,
EnumGeneratorParams,
generateProxyConfigJson,
getEnumNamesFromImports,
getRootNamespace,
interpolate,
@ -28,7 +30,6 @@ import {
serializeParameters,
} from '../../utils';
import * as cases from '../../utils/text';
import { Schema as GenerateProxySchema } from './schema';
export default function(schema: GenerateProxySchema) {
const params = removeDefaultPlaceholders(schema);
@ -40,9 +41,9 @@ export default function(schema: GenerateProxySchema) {
const target = await resolveProject(tree, params.target!);
const solution = getRootNamespace(tree, source, moduleName);
const targetPath = buildDefaultPath(target.definition);
const definitionPath = `${targetPath}/shared/api-definition.json`;
const readApiDefinition = createApiDefinitionReader(definitionPath);
const data = readApiDefinition(tree);
const readProxyConfig = createProxyConfigReader(targetPath);
const createProxyConfigWriter = createProxyConfigWriterCreator(targetPath);
const data = readProxyConfig(tree);
const types = data.types;
const modules = data.modules;
if (!types || !modules) throw new SchematicsException(Exception.InvalidApiDefinition);
@ -80,7 +81,14 @@ export default function(schema: GenerateProxySchema) {
modelImports,
});
return branchAndMerge(chain([generateServices, generateModels, generateEnums]));
if (!data.generated.includes(moduleName)) data.generated.push(moduleName);
data.generated.sort();
const json = generateProxyConfigJson(data);
const overwriteProxyConfig = createProxyConfigWriter('overwrite', json);
return branchAndMerge(
chain([generateServices, generateModels, generateEnums, overwriteProxyConfig]),
);
},
]);
}

@ -1,16 +0,0 @@
export interface Schema {
/**
* Backend module to generate code for
*/
module?: string;
/**
* Angular project to resolve root namespace & API definition URL from
*/
source?: string;
/**
* Angular project to generate code in
*/
target?: string;
}

@ -0,0 +1,51 @@
import { strings } from '@angular-devkit/core';
import { chain, SchematicContext, Tree } from '@angular-devkit/schematics';
import { GenerateProxySchema } from '../../models';
import {
buildDefaultPath,
chainAndMerge,
createApiDefinitionGetter,
createApisGenerator,
createProxyClearer,
createProxyConfigReader,
createProxyConfigSaver,
createProxyWarningSaver,
removeDefaultPlaceholders,
resolveProject,
} from '../../utils';
export default function(schema: GenerateProxySchema) {
const params = removeDefaultPlaceholders(schema);
const moduleName = strings.camelize(params.module || 'app');
return chain([
async (host: Tree, _context: SchematicContext) => {
const target = await resolveProject(host, params.target!);
const targetPath = buildDefaultPath(target.definition);
const readProxyConfig = createProxyConfigReader(targetPath);
let generated: string[] = [];
try {
generated = readProxyConfig(host).generated;
const index = generated.findIndex(m => m === moduleName);
if (index < 0) generated.push(moduleName);
} catch (_) {
generated.push(moduleName);
}
const getApiDefinition = createApiDefinitionGetter(params);
const data = { generated, ...(await getApiDefinition(host)) };
data.generated = [];
const clearProxy = createProxyClearer(targetPath);
const saveProxyConfig = createProxyConfigSaver(data, targetPath);
const saveProxyWarning = createProxyWarningSaver(targetPath);
const generateApis = createApisGenerator(schema, generated);
return chainAndMerge([clearProxy, saveProxyConfig, saveProxyWarning, generateApis])(host);
},
]);
}

@ -6,33 +6,33 @@
"properties": {
"module": {
"alias": "m",
"description": "Backend module to generate code for",
"description": "Backend module name",
"type": "string",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "Please enter name of the backend module you wish to generate proxies for. (default: \"app\")"
"x-prompt": "Please enter backend module name. (default: \"app\")"
},
"source": {
"alias": "s",
"description": "Angular project to resolve root namespace & API definition URL from",
"description": "Source Angular project for API definition URL & root namespace resolution",
"type": "string",
"$default": {
"$source": "argv",
"index": 1
},
"x-prompt": "Plese enter Angular project name to resolve root namespace & API definition URL from. (default: workspace \"defaultProject\")"
"x-prompt": "Plese enter source Angular project for API definition URL & root namespace resolution. (default: workspace \"defaultProject\")"
},
"target": {
"alias": "t",
"description": "Angular project to generate code in",
"description": "Target Angular project to place the generated code",
"type": "string",
"$default": {
"$source": "argv",
"index": 2
},
"x-prompt": "Plese enter Angular project name to place generated code in. (default: workspace \"defaultProject\")"
"x-prompt": "Plese enter target Angular project to place the generated code. (default: workspace \"defaultProject\")"
}
},
"required": []

@ -0,0 +1,37 @@
import { SchematicContext, Tree } from '@angular-devkit/schematics';
import { GenerateProxySchema } from '../../models';
import {
buildDefaultPath,
chainAndMerge,
createApiDefinitionGetter,
createApisGenerator,
createProxyClearer,
createProxyConfigReader,
createProxyConfigSaver,
removeDefaultPlaceholders,
resolveProject,
} from '../../utils';
export default function(schema: GenerateProxySchema) {
const params = removeDefaultPlaceholders(schema);
return async (host: Tree, _context: SchematicContext) => {
const target = await resolveProject(host, params.target!);
const targetPath = buildDefaultPath(target.definition);
const readProxyConfig = createProxyConfigReader(targetPath);
const { generated } = readProxyConfig(host);
const getApiDefinition = createApiDefinitionGetter(params);
const data = { generated, ...(await getApiDefinition(host)) };
data.generated = [];
const clearProxy = createProxyClearer(targetPath);
const saveProxyConfig = createProxyConfigSaver(data, targetPath);
const generateApis = createApisGenerator(schema, generated);
return chainAndMerge([clearProxy, saveProxyConfig, generateApis])(host);
};
}

@ -0,0 +1,39 @@
{
"$schema": "http://json-schema.org/schema",
"id": "SchematicsAbpGenerateProxy",
"title": "ABP Generate Proxy Schema",
"type": "object",
"properties": {
"module": {
"alias": "m",
"description": "Backend module name",
"type": "string",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "Please enter backend module name. (default: \"app\")"
},
"source": {
"alias": "s",
"description": "Source Angular project for API definition URL & root namespace resolution",
"type": "string",
"$default": {
"$source": "argv",
"index": 1
},
"x-prompt": "Plese enter source Angular project for API definition URL & root namespace resolution. (default: workspace \"defaultProject\")"
},
"target": {
"alias": "t",
"description": "Target Angular project to place the generated code",
"type": "string",
"$default": {
"$source": "argv",
"index": 2
},
"x-prompt": "Plese enter target Angular project to place the generated code. (default: workspace \"defaultProject\")"
}
},
"required": []
}

@ -0,0 +1,43 @@
import { strings } from '@angular-devkit/core';
import { SchematicContext, Tree } from '@angular-devkit/schematics';
import { GenerateProxySchema } from '../../models';
import {
buildDefaultPath,
chainAndMerge,
createApiDefinitionGetter,
createApisGenerator,
createProxyClearer,
createProxyConfigReader,
createProxyConfigSaver,
removeDefaultPlaceholders,
resolveProject,
} from '../../utils';
export default function(schema: GenerateProxySchema) {
const params = removeDefaultPlaceholders(schema);
const moduleName = strings.camelize(params.module || 'app');
return async (host: Tree, _context: SchematicContext) => {
const target = await resolveProject(host, params.target!);
const targetPath = buildDefaultPath(target.definition);
const readProxyConfig = createProxyConfigReader(targetPath);
const { generated } = readProxyConfig(host);
const index = generated.findIndex(m => m === moduleName);
if (index < 0) return host;
generated.splice(index, 1);
const getApiDefinition = createApiDefinitionGetter(params);
const data = { generated, ...(await getApiDefinition(host)) };
data.generated = [];
const clearProxy = createProxyClearer(targetPath);
const saveProxyConfig = createProxyConfigSaver(data, targetPath);
const generateApis = createApisGenerator(schema, generated);
return chainAndMerge([clearProxy, saveProxyConfig, generateApis])(host);
};
}

@ -0,0 +1,39 @@
{
"$schema": "http://json-schema.org/schema",
"id": "SchematicsAbpGenerateProxy",
"title": "ABP Generate Proxy Schema",
"type": "object",
"properties": {
"module": {
"alias": "m",
"description": "Backend module name",
"type": "string",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "Please enter backend module name. (default: \"app\")"
},
"source": {
"alias": "s",
"description": "Source Angular project for API definition URL & root namespace resolution",
"type": "string",
"$default": {
"$source": "argv",
"index": 1
},
"x-prompt": "Plese enter source Angular project for API definition URL & root namespace resolution. (default: workspace \"defaultProject\")"
},
"target": {
"alias": "t",
"description": "Target Angular project to place the generated code",
"type": "string",
"$default": {
"$source": "argv",
"index": 2
},
"x-prompt": "Plese enter target Angular project to place the generated code. (default: workspace \"defaultProject\")"
}
},
"required": []
}

@ -1,42 +0,0 @@
import { strings } from '@angular-devkit/core';
import {
branchAndMerge,
chain,
schematic,
SchematicContext,
Tree,
} from '@angular-devkit/schematics';
import { API_DEFINITION_ENDPOINT } from '../../constants';
import { ApiDefinition } from '../../models';
import {
buildDefaultPath,
createApiDefinitionSaver,
getApiDefinition,
getSourceUrl,
removeDefaultPlaceholders,
resolveProject,
} from '../../utils';
import { Schema as GenerateProxySchema } from './schema';
export default function(schema: GenerateProxySchema) {
const params = removeDefaultPlaceholders(schema);
const moduleName = strings.camelize(params.module || 'app');
return chain([
async (tree: Tree, _context: SchematicContext) => {
const source = await resolveProject(tree, params.source!);
const target = await resolveProject(tree, params.target!);
const sourceUrl = getSourceUrl(tree, source, moduleName);
const targetPath = buildDefaultPath(target.definition);
const data: ApiDefinition = await getApiDefinition(sourceUrl + API_DEFINITION_ENDPOINT);
const saveApiDefinition = createApiDefinitionSaver(
data,
`${targetPath}/shared/api-definition.json`,
);
const createApi = schematic('api', schema);
return branchAndMerge(chain([saveApiDefinition, createApi]));
},
]);
}

@ -1,16 +0,0 @@
export interface Schema {
/**
* Backend module to generate code for
*/
module?: string;
/**
* Angular project to resolve root namespace & API definition URL from
*/
source?: string;
/**
* Angular project to generate code in
*/
target?: string;
}

@ -1,3 +1,4 @@
export * from './api';
export * from './proxy';
export * from './system-types';
export * from './volo';

@ -0,0 +1,18 @@
export const PROXY_PATH = '/proxy';
export const PROXY_CONFIG_PATH = `${PROXY_PATH}/generate-proxy.json`;
export const PROXY_WARNING_PATH = `${PROXY_PATH}/README.md`;
export const PROXY_WARNING = `# Proxy Generation Output
This directory includes the output of the latest proxy generation.
The \`services\`, \`models\`, and \`enums\` folders will be overwritten when proxy generation is run again.
Therefore, please do not place your own content in those folders.
In addition, \`generate-proxy.json\` works like a lock file.
It includes information used by the proxy generator, so please do not delete or modify it.
Finally, the name of this folder should not be changed for two reasons:
- Proxy generator will keep creating this folder and you will have multiple copies of the same content.
- ABP Suite generates imports from this folder and uses the path \`/proxy\` when doing so.
`;

@ -1,11 +1,13 @@
export const enum Exception {
DirRemoveFailed = '[Directory Remove Failed] Cannot remove "{0}".',
FileNotFound = '[File Not Found] There is no file at "{0}" path.',
FileWriteFailed = '[File Write Failed] Cannot write file at "{0}".',
InvalidModule = '[Invalid Module] Backend module "{0}" does not exist in API definition.',
InvalidApiDefinition = '[Invalid API Definition] The provided API definition is invalid.',
InvalidWorkspace = '[Invalid Workspace] The angular.json should be a valid JSON file.',
NoApi = '[API Not Available] Please double-check the URL in the source project environment and make sure your application is up and running.',
NoApiDefinition = '[API Definition Not Found] There is no API definition file at "{0}".',
NoProject = '[Project Not Found] Either define a default project in your workspace or specify the project name in schematics options.',
NoProxyConfig = '[Proxy Config Not Found] There is no JSON file at "{0}".',
NoTypeDefinition = '[Type Definition Not Found] There is no type definition for "{0}".',
NoWorkspace = '[Workspace Not Found] Make sure you are running schematics at the root directory of your workspace and it has an angular.json file.',
NoEnvironment = '[Environment Not Found] An environment file cannot be located in "{0}" project.',

@ -0,0 +1,16 @@
export interface GenerateProxySchema {
/**
* Backend module name
*/
module?: string;
/**
* Source Angular project for API definition URL & root namespace resolution
*/
source?: string;
/**
* Target Angular project to place the generated code
*/
target?: string;
}

@ -1,7 +1,10 @@
export * from './api-definition';
export * from './generate-proxy-schema';
export * from './import';
export * from './method';
export * from './model';
export * from './project';
export * from './proxy-config';
export * from './service';
export * from './tree';
export * from './util';

@ -0,0 +1,5 @@
import { ApiDefinition } from './api-definition';
export interface ProxyConfig extends ApiDefinition {
generated: string[];
}

@ -0,0 +1 @@
export type WriteOp = 'create' | 'overwrite';

@ -0,0 +1,11 @@
import { chain, schematic } from '@angular-devkit/schematics';
import { GenerateProxySchema } from '../models';
export function createApisGenerator(schema: GenerateProxySchema, generated: string[]) {
return chain(
generated.map(m => {
const apiSchema = { ...schema, source: schema.target, module: m };
return schematic('api', apiSchema);
}),
);
}

@ -1,4 +1,5 @@
export * from './angular';
export * from './api';
export * from './ast';
export * from './common';
export * from './enum';

@ -1,6 +1,8 @@
import {
apply,
chain,
forEach,
MergeStrategy,
mergeWith,
Rule,
SchematicContext,
@ -16,6 +18,14 @@ export function applyWithOverwrite(source: Source, rules: Rule[]): Rule {
};
}
export function chainAndMerge(rules: Rule[]) {
return (host: Tree) => async (tree: Tree, context: SchematicContext) =>
host.merge(
(await (chain(rules)(tree, context) as any).toPromise()) as Tree,
MergeStrategy.AllowDeleteConflict,
);
}
export function overwriteFileIfExists(tree: Tree): Rule {
return forEach(fileEntry => {
if (!tree.exists(fileEntry.path)) return fileEntry;

@ -1,13 +1,32 @@
import { strings } from '@angular-devkit/core';
import { SchematicsException, Tree } from '@angular-devkit/schematics';
import got from 'got';
import {
API_DEFINITION_ENDPOINT,
PROXY_CONFIG_PATH,
PROXY_PATH,
PROXY_WARNING,
PROXY_WARNING_PATH,
} from '../constants';
import { Exception } from '../enums';
import { ApiDefinition, Project } from '../models';
import { ApiDefinition, GenerateProxySchema, Project, ProxyConfig, WriteOp } from '../models';
import { getAssignedPropertyFromObjectliteral } from './ast';
import { interpolate } from './common';
import { readEnvironment } from './workspace';
import { readEnvironment, resolveProject } from './workspace';
export async function getApiDefinition(url: string) {
let body: any;
export function createApiDefinitionGetter(params: GenerateProxySchema) {
const moduleName = strings.camelize(params.module || 'app');
return async (host: Tree) => {
const source = await resolveProject(host, params.source!);
const sourceUrl = getSourceUrl(host, source, moduleName);
return await getApiDefinition(sourceUrl);
};
}
async function getApiDefinition(sourceUrl: string) {
const url = sourceUrl + API_DEFINITION_ENDPOINT;
let body: ApiDefinition;
try {
({ body } = await got(url, {
@ -17,9 +36,10 @@ export async function getApiDefinition(url: string) {
}));
} catch ({ response }) {
// handle redirects
if (response?.body && response.statusCode < 400) return response.body;
if (response.statusCode >= 400 || !response?.body)
throw new SchematicsException(Exception.NoApi);
throw new SchematicsException(Exception.NoApi);
body = response.body;
}
return body;
@ -71,23 +91,97 @@ export function getSourceUrl(tree: Tree, project: Project, moduleName: string) {
return assignment.replace(/[`'"]/g, '');
}
export function createApiDefinitionReader(targetPath: string) {
export function createProxyConfigReader(targetPath: string) {
targetPath += PROXY_CONFIG_PATH;
return (tree: Tree) => {
try {
const buffer = tree.read(targetPath);
const apiDefinition: ApiDefinition = JSON.parse(buffer!.toString());
return apiDefinition;
return JSON.parse(buffer!.toString()) as ProxyConfig;
} catch (_) {}
throw new SchematicsException(interpolate(Exception.NoApiDefinition, targetPath));
throw new SchematicsException(interpolate(Exception.NoProxyConfig, targetPath));
};
}
export function createApiDefinitionSaver(apiDefinition: ApiDefinition, targetPath: string) {
export function createProxyClearer(targetPath: string) {
targetPath += PROXY_PATH;
return (tree: Tree) => {
tree[tree.exists(targetPath) ? 'overwrite' : 'create'](
targetPath,
JSON.stringify(apiDefinition, null, 2),
);
try {
tree.getDir(targetPath).subdirs.forEach(dirName => {
if (!['enums', 'models', 'services'].includes(dirName)) return;
tree.delete(`${targetPath}/${dirName}`);
});
return tree;
} catch (_) {
throw new SchematicsException(interpolate(Exception.DirRemoveFailed, targetPath));
}
};
}
export function createProxyWarningSaver(targetPath: string) {
targetPath += PROXY_WARNING_PATH;
const createFileWriter = createFileWriterCreator(targetPath);
return (tree: Tree) => {
const op = tree.exists(targetPath) ? 'overwrite' : 'create';
const writeWarningMD = createFileWriter(op, PROXY_WARNING);
writeWarningMD(tree);
return tree;
};
}
export function createProxyConfigSaver(apiDefinition: ApiDefinition, targetPath: string) {
const createProxyConfigJson = createProxyConfigJsonCreator(apiDefinition);
const readPreviousConfig = createProxyConfigReader(targetPath);
const createProxyConfigWriter = createProxyConfigWriterCreator(targetPath);
targetPath += PROXY_CONFIG_PATH;
return (tree: Tree) => {
const generated: string[] = [];
let op: WriteOp = 'create';
if (tree.exists(targetPath)) {
op = 'overwrite';
try {
readPreviousConfig(tree).generated.forEach(m => generated.push(m));
} catch (_) {}
}
const json = createProxyConfigJson(generated);
const writeProxyConfig = createProxyConfigWriter(op, json);
writeProxyConfig(tree);
return tree;
};
}
export function createProxyConfigWriterCreator(targetPath: string) {
targetPath += PROXY_CONFIG_PATH;
return createFileWriterCreator(targetPath);
}
export function createFileWriterCreator(targetPath: string) {
return (op: WriteOp, data: string) => (tree: Tree) => {
try {
tree[op](targetPath, data);
return tree;
} catch (_) {}
throw new SchematicsException(interpolate(Exception.FileWriteFailed, targetPath));
};
}
export function createProxyConfigJsonCreator(apiDefinition: ApiDefinition) {
return (generated: string[]) => generateProxyConfigJson({ generated, ...apiDefinition });
}
export function generateProxyConfigJson(proxyConfig: ProxyConfig) {
return JSON.stringify(proxyConfig, null, 2);
}

@ -20,7 +20,9 @@ class FileCopy {
const PACKAGE_TO_BUILD = 'schematics';
const FILES_TO_COPY_AFTER_BUILD: (FileCopy | string)[] = [
{ src: 'src/commands/proxy/schema.json', dest: 'commands/proxy/schema.json' },
{ src: 'src/commands/proxy-add/schema.json', dest: 'commands/proxy-add/schema.json' },
{ src: 'src/commands/proxy-refresh/schema.json', dest: 'commands/proxy-refresh/schema.json' },
{ src: 'src/commands/proxy-remove/schema.json', dest: 'commands/proxy-remove/schema.json' },
{ src: 'src/commands/api/files-enum', dest: 'commands/api/files-enum' },
{ src: 'src/commands/api/files-model', dest: 'commands/api/files-model' },
{ src: 'src/commands/api/files-service', dest: 'commands/api/files-service' },

Loading…
Cancel
Save