Browse Source

update: models, tests and utils for the resource api option

pull/25761/head
sumeyye 3 months ago
parent
commit
b3c1e4369b
  1. 2
      npm/ng-packs/packages/schematics/src/commands/proxy-add/index.ts
  2. 5
      npm/ng-packs/packages/schematics/src/models/generate-proxy-schema.ts
  3. 1
      npm/ng-packs/packages/schematics/src/models/proxy-config.ts
  4. 1
      npm/ng-packs/packages/schematics/src/models/service.ts
  5. 145
      npm/ng-packs/packages/schematics/src/tests/proxy-service-template-render.spec.ts
  6. 16
      npm/ng-packs/packages/schematics/src/utils/service.ts
  7. 7
      npm/ng-packs/packages/schematics/src/utils/source.ts

2
npm/ng-packs/packages/schematics/src/commands/proxy-add/index.ts

@ -52,8 +52,6 @@ export default function (schema: GenerateProxySchema) {
const generateIndex = createProxyIndexGenerator(targetPath);
console.log('HELLO');
return chain([
mergeAndAllowDelete(host, clearProxy),
saveProxyConfig,

5
npm/ng-packs/packages/schematics/src/models/generate-proxy-schema.ts

@ -31,4 +31,9 @@ export interface GenerateProxySchema {
*/
entryPoint?: string;
serviceType?: eServiceType;
/**
* Generate Resource API helpers for read operations.
*/
resourceApi?: boolean;
}

1
npm/ng-packs/packages/schematics/src/models/proxy-config.ts

@ -2,4 +2,5 @@ import { ApiDefinition } from './api-definition';
export interface ProxyConfig extends ApiDefinition {
generated: string[];
resourceApi?: boolean;
}

1
npm/ng-packs/packages/schematics/src/models/service.ts

@ -10,6 +10,7 @@ export interface ServiceGeneratorParams {
apiName: string;
controllers: Controller[];
serviceImports: Record<string, string[]>;
resourceApi?: boolean;
}
export class Service {

145
npm/ng-packs/packages/schematics/src/tests/proxy-service-template-render.spec.ts

@ -36,14 +36,20 @@ function render(context: Record<string, unknown>): string {
return compiled(context);
}
function buildContext(body: Partial<MockBody>) {
function buildContext(body: Partial<MockBody>, resourceApi = false) {
return {
apiName: 'Default',
name: 'Sample',
namespace: 'app',
resourceApi,
imports: [
{ keyword: 'import', specifiers: ['RestService', 'Rest'], path: '@abp/ng.core' },
{ keyword: 'import', specifiers: ['Injectable', 'inject'], path: '@angular/core' },
...(resourceApi
? [
{ keyword: 'import', specifiers: ['Signal', 'computed', 'ResourceRef'], path: '@angular/core' },
]
: []),
],
methods: [
{
@ -100,6 +106,35 @@ describe('proxy service template — rendered output', () => {
expect(output).not.toContain('headers:');
});
test('resource api mode emits requestResource helper for GET methods', () => {
const ctx = buildContext({
responseType: 'MyDto',
responseTypeWithNamespace: 'My.Project.MyDto',
}, true);
ctx.methods[0].signature.parameters = [
{ name: 'input', type: 'GetSampleListInput' } as any,
{ name: 'config', type: 'Partial<Rest.Config>' } as any,
];
const output = render(ctx);
expect(output).toContain('Signal');
expect(output).toContain('computed(() => {');
expect(output).toContain('const { input } = params();');
expect(output).toContain('this.restService.requestResource(');
expect(output).toContain('{ apiName: this.apiName, ...config }');
expect(output).toContain('getSampleAsyncResource');
});
test('resource api mode does not emit helpers for non-GET methods', () => {
const output = render(buildContext({
method: 'POST',
responseType: 'MyDto',
responseTypeWithNamespace: 'My.Project.MyDto',
}, true));
expect(output).not.toContain('getSampleAsyncResource');
});
test('json httpResponseType emits Accept but no responseType (default is json)', () => {
const output = render(buildContext({
responseType: 'string',
@ -191,6 +226,109 @@ describe('proxy service template — rendered output', () => {
expect(output.match(/}/g)!.length).toBeGreaterThanOrEqual(3);
});
test('resource api rendered service compiles cleanly', () => {
const ts = require('typescript');
const ctx = buildContext({
responseType: 'string',
responseTypeWithNamespace: 'string',
httpResponseType: 'json',
acceptHeader: 'application/json',
}, true);
ctx.methods[0].signature.parameters = [
{ name: 'config', type: 'Partial<Rest.Config>' } as any,
];
const output = render(ctx);
const abpStub = `
declare module '@abp/ng.core' {
export namespace Rest {
export interface Config {
apiName?: string;
observe?: any;
skipHandleError?: boolean;
responseType?: string;
[key: string]: any;
}
export type Observe = any;
}
export class RestService {
request<TBody, TResponse>(req: any, config?: any): import('rxjs').Observable<TResponse>;
requestResource<TBody, TResponse>(request: any, config?: any, api?: string): any;
}
}
`;
const angularCoreStub = `
declare module '@angular/core' {
export function Injectable(opts?: any): ClassDecorator;
export function inject<T>(token: { new (...args: any[]): T }): T;
export function inject<T>(token: any): T;
export interface Signal<T> { (): T; }
export function computed<T>(fn: () => T): Signal<T>;
export interface ResourceRef<T> { value?: T; }
}
`;
const rxjsStub = `
declare module 'rxjs' {
export class Observable<T> { subscribe(...args: any[]): unknown; }
}
`;
const ambient = abpStub + angularCoreStub + rxjsStub;
const sources: Record<string, string> = {
'/proxy/sample.service.ts': output,
'/proxy/ambient.d.ts': ambient,
};
const compilerOptions: any = {
target: ts.ScriptTarget.ES2020,
module: ts.ModuleKind.ES2020,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
ignoreDeprecations: '6.0',
experimentalDecorators: true,
emitDecoratorMetadata: true,
strict: true,
noEmit: true,
skipLibCheck: true,
};
const baseHost = ts.createCompilerHost(compilerOptions, true);
const host: any = {
...baseHost,
getSourceFile: (fileName: string, languageVersion: any, onError: any) => {
if (sources[fileName]) {
return ts.createSourceFile(fileName, sources[fileName], languageVersion, true);
}
return baseHost.getSourceFile(fileName, languageVersion, onError);
},
fileExists: (fileName: string) =>
sources[fileName] != null || baseHost.fileExists(fileName),
readFile: (fileName: string) =>
sources[fileName] ?? baseHost.readFile(fileName),
};
const program = ts.createProgram(Object.keys(sources), compilerOptions, host);
const errors = ts
.getPreEmitDiagnostics(program)
.filter((d: any) => d.category === ts.DiagnosticCategory.Error && d.code !== 6053);
if (errors.length) {
const messages = errors
.map((d: any) => {
const where = d.file
? (() => {
const p = d.file.getLineAndCharacterOfPosition(d.start ?? 0);
const lineText = d.file.text.split('\n')[p.line];
return `${d.file.fileName}:${p.line + 1}:${p.character + 1}\n>>> ${lineText}\n>>> ${' '.repeat(p.character)}^`;
})()
: '(no file)';
return `[${where}] TS${d.code}: ${ts.flattenDiagnosticMessageText(d.messageText, '\n')}`;
})
.join('\n---\n');
throw new Error(`Resource proxy did not compile:\n${output}\n=== diagnostics ===\n${messages}`);
}
expect(errors).toHaveLength(0);
});
test.each([
{ name: 'string + json Accept', body: { responseType: 'string', responseTypeWithNamespace: 'string', httpResponseType: 'json', acceptHeader: 'application/problem+json' } },
{ name: 'string + text Accept', body: { responseType: 'string', responseTypeWithNamespace: 'string', httpResponseType: 'text', acceptHeader: 'text/csv' } },
@ -244,6 +382,7 @@ describe('proxy service template — rendered output', () => {
target: ts.ScriptTarget.ES2020,
module: ts.ModuleKind.ES2020,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
ignoreDeprecations: '6.0',
experimentalDecorators: true,
emitDecoratorMetadata: true,
strict: true,
@ -365,6 +504,7 @@ describe('proxy service template — rendered output', () => {
}
export class RestService {
request<TBody, TResponse>(req: any, config?: any): import('rxjs').Observable<TResponse>;
requestResource<TBody, TResponse>(request: any, config?: any, api?: string): any;
}
}
`;
@ -373,6 +513,8 @@ describe('proxy service template — rendered output', () => {
export function Injectable(opts?: any): ClassDecorator;
export function inject<T>(token: { new (...args: any[]): T }): T;
export function inject<T>(token: any): T;
export function computed<T>(fn: () => T): any;
export interface ResourceRef<T> { value?: T; }
}
`;
const rxjsStub = `
@ -397,6 +539,7 @@ describe('proxy service template — rendered output', () => {
target: ts.ScriptTarget.ES2020,
module: ts.ModuleKind.ES2020,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
ignoreDeprecations: '6.0',
experimentalDecorators: true,
emitDecoratorMetadata: true,
strict: true,

16
npm/ng-packs/packages/schematics/src/utils/service.ts

@ -35,6 +35,7 @@ export function createControllerToServiceMapper({
solution,
types,
apiName,
resourceApi,
}: ServiceGeneratorParams) {
const mapActionToMethod = createActionToMethodMapper();
@ -49,6 +50,11 @@ export function createControllerToServiceMapper({
);
imports.push(new Import({ path: '@abp/ng.core', specifiers: ['RestService', 'Rest'] }));
imports.push(new Import({ path: '@angular/core', specifiers: ['Injectable', 'inject'] }));
if (resourceApi) {
appendImportSpec(imports, '@angular/core', 'computed');
appendImportSpec(imports, '@angular/core', 'Signal');
appendImportSpec(imports, '@angular/core', 'ResourceRef');
}
sortImports(imports);
const methods = actions.map(mapActionToMethod);
sortMethods(methods);
@ -56,6 +62,16 @@ export function createControllerToServiceMapper({
};
}
function appendImportSpec(imports: Import[], path: string, specifier: string) {
const existing = imports.find(x => x.path === path);
if (!existing) {
imports.push(new Import({ path, specifiers: [specifier] }));
return;
}
existing.specifiers = [...new Set([...existing.specifiers, specifier])];
}
function getTypesWithoutIRemoteStreamContent(types: Record<string, Type>) {
const newType = { ...types };
VOLO_REMOTE_STREAM_CONTENT.forEach(fileType => {

7
npm/ng-packs/packages/schematics/src/utils/source.ts

@ -108,6 +108,13 @@ export function createProxyConfigReader(targetPath: string) {
};
}
export function resolveProxyResourceApi(
params: GenerateProxySchema,
previousConfig?: ProxyConfig,
) {
return params.resourceApi ?? previousConfig?.resourceApi ?? false;
}
export function createProxyClearer(targetPath: string) {
targetPath += PROXY_PATH;
const proxyIndexPath = `${targetPath}/index.ts`;

Loading…
Cancel
Save