Browse Source

Merge pull request #25761 from abpframework/issue/25678

Angular - Supporting the resource api functionalities for schematics
pull/25870/head
Fahri Gedik 2 months ago
committed by GitHub
parent
commit
7e03693a6d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 18
      docs/en/cli/index.md
  2. 5
      docs/en/framework/ui/angular/service-proxies.md
  3. 15
      npm/ng-packs/packages/core/src/lib/services/rest.service.ts
  4. 16
      npm/ng-packs/packages/core/src/lib/tests/rest.service.spec.ts
  5. 1
      npm/ng-packs/packages/generators/src/generators/generate-proxy/schema.d.ts
  6. 5
      npm/ng-packs/packages/generators/src/generators/generate-proxy/schema.json
  7. 42
      npm/ng-packs/packages/schematics/src/commands/api/files-service/proxy/__namespace@dir__/__name@kebab__.service.ts.template
  8. 5
      npm/ng-packs/packages/schematics/src/commands/api/index.ts
  9. 5
      npm/ng-packs/packages/schematics/src/commands/api/schema.json
  10. 11
      npm/ng-packs/packages/schematics/src/commands/proxy-add/index.ts
  11. 5
      npm/ng-packs/packages/schematics/src/commands/proxy-add/schema.json
  12. 9
      npm/ng-packs/packages/schematics/src/commands/proxy-refresh/index.ts
  13. 5
      npm/ng-packs/packages/schematics/src/commands/proxy-refresh/schema.json
  14. 9
      npm/ng-packs/packages/schematics/src/commands/proxy-remove/index.ts
  15. 5
      npm/ng-packs/packages/schematics/src/commands/proxy-remove/schema.json
  16. 5
      npm/ng-packs/packages/schematics/src/models/generate-proxy-schema.ts
  17. 1
      npm/ng-packs/packages/schematics/src/models/proxy-config.ts
  18. 1
      npm/ng-packs/packages/schematics/src/models/service.ts
  19. 148
      npm/ng-packs/packages/schematics/src/tests/proxy-service-template-render.spec.ts
  20. 16
      npm/ng-packs/packages/schematics/src/utils/service.ts
  21. 7
      npm/ng-packs/packages/schematics/src/utils/source.ts

18
docs/en/cli/index.md

@ -809,14 +809,15 @@ abp generate-proxy -t csharp -url https://localhost:44302/
- `csharp`: C#, work in the `*.HttpApi.Client` project directory. There are some additional options for this client: - `csharp`: C#, work in the `*.HttpApi.Client` project directory. There are some additional options for this client:
- `--without-contracts`: Avoid generating the application service interface, class, enum and dto types. - `--without-contracts`: Avoid generating the application service interface, class, enum and dto types.
- `--folder`: Folder name to place generated CSharp code in. Default value: `ClientProxies`. - `--folder`: Folder name to place generated CSharp code in. Default value: `ClientProxies`.
- `ng`: Angular. There are some additional options for this client: - `ng`: Angular. There are some additional options for this client:
- `--api-name` or `-a`: The name of the API endpoint defined in the `/src/environments/environment.ts`. Default value: `default`. - `--api-name` or `-a`: The name of the API endpoint defined in the `/src/environments/environment.ts`. Default value: `default`.
- `--source` or `-s`: Specifies the Angular project name to resolve the root namespace & API definition URL from. Default value: `defaultProject`. - `--source` or `-s`: Specifies the Angular project name to resolve the root namespace & API definition URL from. Default value: `defaultProject`.
- `--target`: Specifies the Angular project name to place generated code in. Default value: `defaultProject`. - `--target`: Specifies the Angular project name to place generated code in. Default value: `defaultProject`.
- `--module`: Backend module name. Default value: `app`. - `--module`: Backend module name. Default value: `app`.
- `--entry-point`: Targets the Angular project to place the generated code. - `--entry-point`: Targets the Angular project to place the generated code.
- `--url`: Specifies api definition url. Default value is API Name's url in environment file. - `--url`: Specifies api definition url. Default value is API Name's url in environment file.
- `--prompt` or `-p`: Asks the options from the command line prompt (for the unspecified options). - `--resource-api`: Adds optional Resource API helpers for `GET` endpoints while keeping the generated Observable services. This parameter requires Angular v22 or later.
- `--prompt` or `-p`: Asks the options from the command line prompt (for the unspecified options).
- `js`: JavaScript. work in the `*.Web` project directory. There are some additional options for this client: - `js`: JavaScript. work in the `*.Web` project directory. There are some additional options for this client:
- `--output` or `-o`: JavaScript file path or folder to place generated code in. - `--output` or `-o`: JavaScript file path or folder to place generated code in.
- `--module` or `-m`: Specifies the name of the backend module you wish to generate proxies for. Default value: `app`. - `--module` or `-m`: Specifies the name of the backend module you wish to generate proxies for. Default value: `app`.
@ -1314,4 +1315,3 @@ var tokenResponse = await httpClient.RequestClientCredentialsTokenAsync(
- [Examples for the new command](./new-command-samples.md) - [Examples for the new command](./new-command-samples.md)
- [Video tutorial](https://abp.io/video-courses/essentials/abp-cli) - [Video tutorial](https://abp.io/video-courses/essentials/abp-cli)

5
docs/en/framework/ui/angular/service-proxies.md

@ -88,12 +88,15 @@ export const environment: Config.Environment = {
- **target:** Target for the Angular project to place the generated code. For example, if it's `permission-management`, it'll look like this (npm/ng-packs/packages/*permission-management*). - **target:** Target for the Angular project to place the generated code. For example, if it's `permission-management`, it'll look like this (npm/ng-packs/packages/*permission-management*).
- **entryPoint:** To create the generated proxy folder in the target. The directory is `permission-management/proxy/src/lib/proxy` and the `permission-management` is the value of target. If you want to create a folder for the generated proxy, there are two options, you should either set the value `proxy` as the entryPoint or go to project.json and change the `sourceRoot` from `packages/permission-management/src` to `packages/permission-management/proxy/src`. No need to change the sourceRoot of project with the property. if you keep it empty, the proxy will be generated into the folder defined in the sourceRoot property. - **entryPoint:** To create the generated proxy folder in the target. The directory is `permission-management/proxy/src/lib/proxy` and the `permission-management` is the value of target. If you want to create a folder for the generated proxy, there are two options, you should either set the value `proxy` as the entryPoint or go to project.json and change the `sourceRoot` from `packages/permission-management/src` to `packages/permission-management/proxy/src`. No need to change the sourceRoot of project with the property. if you keep it empty, the proxy will be generated into the folder defined in the sourceRoot property.
- **serviceType:** The service type of the generated proxy. The options are `application`, `integration` and `all`. The default value is `application`. A developer can mark a service "integration service". If you want to skip proxy generation for the service, then this is the correct setting. More info about [Integration Services](../../api-development/integration-services.md) - **serviceType:** The service type of the generated proxy. The options are `application`, `integration` and `all`. The default value is `application`. A developer can mark a service "integration service". If you want to skip proxy generation for the service, then this is the correct setting. More info about [Integration Services](../../api-development/integration-services.md)
- **resourceApi:** Generates optional `rxResource` helpers for `GET` endpoints. This is off by default and keeps the current Observable-based services unchanged. This parameter requires Angular v22 or later
### Services ### Services
The `generate-proxy` command generates one service per back-end controller and a method (property with a function value actually) for each action in the controller. These methods call backend APIs via [RestService](./http-requests#restservice). The `generate-proxy` command generates one service per back-end controller and a method (property with a function value actually) for each action in the controller. These methods call backend APIs via [RestService](./http-requests#restservice).
If you pass `--resource-api`, the generator keeps those methods and adds matching `rxResource` helpers for read operations.
A variable named `apiName` (available as of v2.4) is defined in each service. `apiName` matches the module's `RemoteServiceName`. This variable passes to the `RestService` as a parameter at each request. If there is no microservice API defined in the environment, `RestService` uses the default. See [getting a specific API endpoint from application config](./http-requests#how-to-get-a-specific-api-endpoint-from-application-config) A variable named `apiName` (available as of v2.4) is defined in each service. `apiName` matches the module's `RemoteServiceName`. This variable passes to the `RestService` as a parameter at each request. If there is no microservice API defined in the environment, `RestService` uses the default. See [getting a specific API endpoint from application config](./http-requests#how-to-get-a-specific-api-endpoint-from-application-config)
The `providedIn` property of the services is defined as `'root'`. Therefore there is no need to provide them in a module. You can use them directly by injecting as shown below: The `providedIn` property of the services is defined as `'root'`. Therefore there is no need to provide them in a module. You can use them directly by injecting as shown below:
@ -188,4 +191,4 @@ When you run a project on Visual Studio using IIS Express as the web server, the
## See Also ## See Also
* [Video tutorial](https://abp.io/video-courses/essentials/generating-client-proxies) * [Video tutorial](https://abp.io/video-courses/essentials/generating-client-proxies)

15
npm/ng-packs/packages/core/src/lib/services/rest.service.ts

@ -1,5 +1,6 @@
import { HttpClient, HttpHeaders, HttpParameterCodec, HttpParams, HttpRequest } from '@angular/common/http'; import { HttpClient, HttpHeaders, HttpParameterCodec, HttpParams, HttpRequest } from '@angular/common/http';
import { Injectable, inject } from '@angular/core'; import { Injectable, Signal, inject } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { Observable, from, of, throwError } from 'rxjs'; import { Observable, from, of, throwError } from 'rxjs';
import { catchError, map, switchMap } from 'rxjs/operators'; import { catchError, map, switchMap } from 'rxjs/operators';
import { ExternalHttpClient } from '../clients/http.client'; import { ExternalHttpClient } from '../clients/http.client';
@ -66,6 +67,18 @@ export class RestService {
}), }),
); );
} }
requestResource<T, R>(
request: Signal<Rest.Request<T>>,
config?: Rest.Config,
api?: string,
) {
return rxResource({
params: () => request(),
stream: ({ params }) => this.request<T, R>(params, config, api),
});
}
private getHttpClient(isExternal: boolean) { private getHttpClient(isExternal: boolean) {
return isExternal ? this.externalHttp : this.http; return isExternal ? this.externalHttp : this.http;
} }

16
npm/ng-packs/packages/core/src/lib/tests/rest.service.spec.ts

@ -1,4 +1,6 @@
import { HttpErrorResponse, HttpHeaders } from '@angular/common/http'; import { HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { effect, signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { createHttpFactory, HttpMethod, SpectatorHttp, SpyObject } from '@ngneat/spectator/vitest'; import { createHttpFactory, HttpMethod, SpectatorHttp, SpyObject } from '@ngneat/spectator/vitest';
import { OAuthService } from 'angular-oauth2-oidc'; import { OAuthService } from 'angular-oauth2-oidc';
import { of, throwError } from 'rxjs'; import { of, throwError } from 'rxjs';
@ -92,6 +94,20 @@ describe('HttpClient testing', () => {
await completionPromise; await completionPromise;
}); });
test('should create a resource-based request that still uses the ABP request pipeline', () => {
const resource = TestBed.runInInjectionContext(() => {
const resource = spectator.service.requestResource(signal({ method: HttpMethod.GET, url: '/test' }));
effect(() => {
resource.value();
});
return resource;
});
expect(typeof resource.reload).toBe('function');
expect(typeof resource.value).toBe('function');
expect(typeof resource.hasValue).toBe('function');
});
test('should handle the error', () => { test('should handle the error', () => {
const spy = vi.spyOn(httpErrorReporter, 'reportError'); const spy = vi.spyOn(httpErrorReporter, 'reportError');

1
npm/ng-packs/packages/generators/src/generators/generate-proxy/schema.d.ts

@ -5,5 +5,6 @@ export interface GenerateProxyGeneratorSchema {
target: string; target: string;
url: string; url: string;
serviceType: string; serviceType: string;
resourceApi: boolean;
entryPoint: string; entryPoint: string;
} }

5
npm/ng-packs/packages/generators/src/generators/generate-proxy/schema.json

@ -80,6 +80,11 @@
] ]
} }
}, },
"resourceApi": {
"description": "Generate Resource API helpers for read endpoints",
"type": "boolean",
"default": false
},
"entryPoint": { "entryPoint": {
"description": "Target Angular project to place the generated code", "description": "Target Angular project to place the generated code",
"type": "string", "type": "string",

42
npm/ng-packs/packages/schematics/src/commands/api/files-service/proxy/__namespace@dir__/__name@kebab__.service.ts.template

@ -9,12 +9,45 @@ export class <%= name %>Service {
apiName = '<%= apiName %>';<% apiName = '<%= apiName %>';<%
for (let {body, signature} of methods) { %> for (let {body, signature} of methods) { %>
<% <%
const isBlob = body.isBlobMethod() ; const isBlob = body.isBlobMethod();
const responseType = isBlob ? "Blob":body.responseType; const responseType = isBlob ? 'Blob' : body.responseType;
const httpResponseType = body.httpResponseType; const httpResponseType = body.httpResponseType;
const acceptHeader = body.acceptHeader; const acceptHeader = body.acceptHeader;
const resourceParameters = signature.parameters.filter(p => p.name !== 'config');
const resourceType = resourceParameters.length
? `{ ${resourceParameters.map(p => `${p.name}${p.optional}: ${p.type}`).join('; ')} }`
: '';
const resourceArgs = resourceParameters.map(p => `params.${p.name}`).join(', ');
%> %>
<% if (resourceApi && body.method === 'GET') { %>
<%= camel(signature.name) %> = (<%
if (resourceParameters.length) { %>
params: Signal<<%= resourceType %>>,<% } %>
config?: Partial<Rest.Config>
): ResourceRef<<%= responseType %> | undefined> => this.restService.requestResource(
computed(() => {<%
if (resourceParameters.length) { %>
const { <%= resourceParameters.map(p => p.name).join(', ') %> } = params();<% } %>
return ({
method: '<%= body.method %>',<%
if (httpResponseType && httpResponseType !== 'json') { %>
responseType: '<%= httpResponseType %>',<% } %><%
if (acceptHeader) { %>
headers: { Accept: '<%= acceptHeader %>' },<% } %>
url: <%= body.url %>,<%
if (body.dictParamVar && !body.params.length) { %>
params: <%= body.dictParamVar %>,<% } %><%
if (body.dictParamVar && body.params.length) { %>
params: { ...<%= body.dictParamVar %>, <%= body.params.join(', ') %> },<% } %><%
if (!body.dictParamVar && body.params.length) { %>
params: { <%= body.params.join(', ') %> },<% }
if (body.body) { %>
body: <%= body.body %>,<% } %>
});
}),
{ apiName: this.apiName, ...config },
);<% } else { %>
<%= camel(signature.name) %> = (<%= serializeParameters(signature.parameters) %>) => <%= camel(signature.name) %> = (<%= serializeParameters(signature.parameters) %>) =>
this.restService.request<<%= body.requestType %>, <%= responseType %>>({ this.restService.request<<%= body.requestType %>, <%= responseType %>>({
method: '<%= body.method %>',<% method: '<%= body.method %>',<%
@ -32,5 +65,6 @@ export class <%= name %>Service {
if (body.body) { %> if (body.body) { %>
body: <%= body.body %>,<% } %> body: <%= body.body %>,<% } %>
}, },
{ apiName: this.apiName,...config });<% } %> { apiName: this.apiName,...config });<% } %><%
} } %>
}

5
npm/ng-packs/packages/schematics/src/commands/api/index.ts

@ -56,6 +56,7 @@ export default function (schema: GenerateProxySchema) {
const types = data.types; const types = data.types;
const modules = data.modules; const modules = data.modules;
const serviceType = schema.serviceType || defaultEServiceType; const serviceType = schema.serviceType || defaultEServiceType;
const resourceApi = schema.resourceApi ?? false;
if (!types || !modules) { if (!types || !modules) {
throw new SchematicsException(Exception.InvalidApiDefinition); throw new SchematicsException(Exception.InvalidApiDefinition);
@ -82,6 +83,7 @@ export default function (schema: GenerateProxySchema) {
apiName, apiName,
controllers, controllers,
serviceImports, serviceImports,
resourceApi,
}); });
const modelImports: Record<string, string[]> = {}; const modelImports: Record<string, string[]> = {};
@ -177,7 +179,7 @@ function createModelGenerator(params: ModelGeneratorParams) {
} }
function createServiceGenerator(params: ServiceGeneratorParams) { function createServiceGenerator(params: ServiceGeneratorParams) {
const { targetPath, controllers, serviceImports } = params; const { targetPath, controllers, serviceImports, resourceApi } = params;
const mapControllerToService = createControllerToServiceMapper(params); const mapControllerToService = createControllerToServiceMapper(params);
return chain( return chain(
@ -195,6 +197,7 @@ function createServiceGenerator(params: ServiceGeneratorParams) {
applyTemplates({ applyTemplates({
...cases, ...cases,
serializeParameters, serializeParameters,
resourceApi,
...service, ...service,
}), }),
move(normalize(targetPath)), move(normalize(targetPath)),

5
npm/ng-packs/packages/schematics/src/commands/api/schema.json

@ -79,6 +79,11 @@
} }
] ]
} }
},
"resourceApi": {
"description": "Generate Resource API helpers for read endpoints",
"type": "boolean",
"default": false
} }
}, },

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

@ -11,6 +11,7 @@ import {
createProxyWarningSaver, createProxyWarningSaver,
mergeAndAllowDelete, mergeAndAllowDelete,
removeDefaultPlaceholders, removeDefaultPlaceholders,
resolveProxyResourceApi,
resolveProject, resolveProject,
} from '../../utils'; } from '../../utils';
@ -24,17 +25,21 @@ export default function (schema: GenerateProxySchema) {
const targetPath = buildTargetPath(target.definition, params.entryPoint); const targetPath = buildTargetPath(target.definition, params.entryPoint);
const readProxyConfig = createProxyConfigReader(targetPath); const readProxyConfig = createProxyConfigReader(targetPath);
let generated: string[] = []; let generated: string[] = [];
let previousResourceApi = false;
try { try {
generated = readProxyConfig(host).generated; const previousConfig = readProxyConfig(host);
generated = previousConfig.generated;
previousResourceApi = resolveProxyResourceApi(params, previousConfig);
const index = generated.findIndex(m => m === moduleName); const index = generated.findIndex(m => m === moduleName);
if (index < 0) generated.push(moduleName); if (index < 0) generated.push(moduleName);
} catch (_) { } catch (_) {
generated.push(moduleName); generated.push(moduleName);
previousResourceApi = resolveProxyResourceApi(params);
} }
const getApiDefinition = createApiDefinitionGetter(params); const getApiDefinition = createApiDefinitionGetter(params);
const data = { generated, ...(await getApiDefinition(host)) }; const data = { generated, resourceApi: previousResourceApi, ...(await getApiDefinition(host)) };
data.generated = []; data.generated = [];
const clearProxy = createProxyClearer(targetPath); const clearProxy = createProxyClearer(targetPath);
@ -43,7 +48,7 @@ export default function (schema: GenerateProxySchema) {
const saveProxyWarning = createProxyWarningSaver(targetPath); const saveProxyWarning = createProxyWarningSaver(targetPath);
const generateApis = createApisGenerator(schema, generated); const generateApis = createApisGenerator({ ...schema, resourceApi: previousResourceApi }, generated);
const generateIndex = createProxyIndexGenerator(targetPath); const generateIndex = createProxyIndexGenerator(targetPath);

5
npm/ng-packs/packages/schematics/src/commands/proxy-add/schema.json

@ -80,6 +80,11 @@
] ]
} }
}, },
"resourceApi": {
"description": "Generate Resource API helpers for read endpoints",
"type": "boolean",
"default": false
},
"entryPoint": { "entryPoint": {
"description": "Target Angular project to place the generated code", "description": "Target Angular project to place the generated code",
"type": "string", "type": "string",

9
npm/ng-packs/packages/schematics/src/commands/proxy-refresh/index.ts

@ -10,6 +10,7 @@ import {
createProxyIndexGenerator, createProxyIndexGenerator,
mergeAndAllowDelete, mergeAndAllowDelete,
removeDefaultPlaceholders, removeDefaultPlaceholders,
resolveProxyResourceApi,
resolveProject, resolveProject,
} from '../../utils'; } from '../../utils';
@ -21,17 +22,19 @@ export default function (schema: GenerateProxySchema) {
const targetPath = buildTargetPath(target.definition, params.entryPoint); const targetPath = buildTargetPath(target.definition, params.entryPoint);
const readProxyConfig = createProxyConfigReader(targetPath); const readProxyConfig = createProxyConfigReader(targetPath);
const { generated } = readProxyConfig(host); const previousConfig = readProxyConfig(host);
const { generated } = previousConfig;
const resourceApi = resolveProxyResourceApi(params, previousConfig);
const getApiDefinition = createApiDefinitionGetter(params); const getApiDefinition = createApiDefinitionGetter(params);
const data = { generated, ...(await getApiDefinition(host)) }; const data = { generated, resourceApi, ...(await getApiDefinition(host)) };
data.generated = []; data.generated = [];
const clearProxy = createProxyClearer(targetPath); const clearProxy = createProxyClearer(targetPath);
const saveProxyConfig = createProxyConfigSaver(data, targetPath); const saveProxyConfig = createProxyConfigSaver(data, targetPath);
const generateApis = createApisGenerator(schema, generated); const generateApis = createApisGenerator({ ...schema, resourceApi }, generated);
const generateIndex = createProxyIndexGenerator(targetPath); const generateIndex = createProxyIndexGenerator(targetPath);

5
npm/ng-packs/packages/schematics/src/commands/proxy-refresh/schema.json

@ -49,6 +49,11 @@
}, },
"x-prompt": "Please enter URL for api definition (default: API Name's url in environment file)" "x-prompt": "Please enter URL for api definition (default: API Name's url in environment file)"
}, },
"resourceApi": {
"description": "Generate Resource API helpers for read endpoints",
"type": "boolean",
"default": false
},
"entryPoint": { "entryPoint": {
"description": "Target Angular project to place the generated code", "description": "Target Angular project to place the generated code",
"type": "string", "type": "string",

9
npm/ng-packs/packages/schematics/src/commands/proxy-remove/index.ts

@ -10,6 +10,7 @@ import {
createProxyIndexGenerator, createProxyIndexGenerator,
mergeAndAllowDelete, mergeAndAllowDelete,
removeDefaultPlaceholders, removeDefaultPlaceholders,
resolveProxyResourceApi,
resolveProject, resolveProject,
} from '../../utils'; } from '../../utils';
@ -23,21 +24,23 @@ export default function (schema: GenerateProxySchema) {
const targetPath = buildTargetPath(target.definition, params.entryPoint); const targetPath = buildTargetPath(target.definition, params.entryPoint);
const readProxyConfig = createProxyConfigReader(targetPath); const readProxyConfig = createProxyConfigReader(targetPath);
const { generated } = readProxyConfig(host); const previousConfig = readProxyConfig(host);
const { generated } = previousConfig;
const resourceApi = resolveProxyResourceApi(params, previousConfig);
const index = generated.findIndex(m => m === moduleName); const index = generated.findIndex(m => m === moduleName);
if (index < 0) return host; if (index < 0) return host;
generated.splice(index, 1); generated.splice(index, 1);
const getApiDefinition = createApiDefinitionGetter(params); const getApiDefinition = createApiDefinitionGetter(params);
const data = { generated, ...(await getApiDefinition(host)) }; const data = { generated, resourceApi, ...(await getApiDefinition(host)) };
data.generated = []; data.generated = [];
const clearProxy = createProxyClearer(targetPath); const clearProxy = createProxyClearer(targetPath);
const saveProxyConfig = createProxyConfigSaver(data, targetPath); const saveProxyConfig = createProxyConfigSaver(data, targetPath);
const generateApis = createApisGenerator(schema, generated); const generateApis = createApisGenerator({ ...schema, resourceApi }, generated);
const generateIndex = createProxyIndexGenerator(targetPath); const generateIndex = createProxyIndexGenerator(targetPath);

5
npm/ng-packs/packages/schematics/src/commands/proxy-remove/schema.json

@ -49,6 +49,11 @@
}, },
"x-prompt": "Please enter URL for API definition (default: API Name's url in environment file)" "x-prompt": "Please enter URL for API definition (default: API Name's url in environment file)"
}, },
"resourceApi": {
"description": "Generate Resource API helpers for read endpoints",
"type": "boolean",
"default": false
},
"entryPoint": { "entryPoint": {
"description": "Target Angular project to place the generated code", "description": "Target Angular project to place the generated code",
"type": "string", "type": "string",

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

@ -31,4 +31,9 @@ export interface GenerateProxySchema {
*/ */
entryPoint?: string; entryPoint?: string;
serviceType?: eServiceType; 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 { export interface ProxyConfig extends ApiDefinition {
generated: string[]; generated: string[];
resourceApi?: boolean;
} }

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

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

148
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); return compiled(context);
} }
function buildContext(body: Partial<MockBody>) { function buildContext(body: Partial<MockBody>, resourceApi = false) {
return { return {
apiName: 'Default', apiName: 'Default',
name: 'Sample', name: 'Sample',
namespace: 'app', namespace: 'app',
resourceApi,
imports: [ imports: [
{ keyword: 'import', specifiers: ['RestService', 'Rest'], path: '@abp/ng.core' }, { keyword: 'import', specifiers: ['RestService', 'Rest'], path: '@abp/ng.core' },
{ keyword: 'import', specifiers: ['Injectable', 'inject'], path: '@angular/core' }, { keyword: 'import', specifiers: ['Injectable', 'inject'], path: '@angular/core' },
...(resourceApi
? [
{ keyword: 'import', specifiers: ['Signal', 'computed', 'ResourceRef'], path: '@angular/core' },
]
: []),
], ],
methods: [ methods: [
{ {
@ -100,6 +106,38 @@ describe('proxy service template — rendered output', () => {
expect(output).not.toContain('headers:'); 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('getSampleAsync = (');
expect(output).not.toContain('getSampleAsyncResource');
expect(output).not.toContain('this.restService.request<any, MyDto>');
});
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).toContain('getSampleAsync = (');
expect(output).not.toContain('requestResource(');
});
test('json httpResponseType emits Accept but no responseType (default is json)', () => { test('json httpResponseType emits Accept but no responseType (default is json)', () => {
const output = render(buildContext({ const output = render(buildContext({
responseType: 'string', responseType: 'string',
@ -191,6 +229,109 @@ describe('proxy service template — rendered output', () => {
expect(output.match(/}/g)!.length).toBeGreaterThanOrEqual(3); 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([ test.each([
{ name: 'string + json Accept', body: { responseType: 'string', responseTypeWithNamespace: 'string', httpResponseType: 'json', acceptHeader: 'application/problem+json' } }, { 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' } }, { name: 'string + text Accept', body: { responseType: 'string', responseTypeWithNamespace: 'string', httpResponseType: 'text', acceptHeader: 'text/csv' } },
@ -244,6 +385,7 @@ describe('proxy service template — rendered output', () => {
target: ts.ScriptTarget.ES2020, target: ts.ScriptTarget.ES2020,
module: ts.ModuleKind.ES2020, module: ts.ModuleKind.ES2020,
moduleResolution: ts.ModuleResolutionKind.NodeJs, moduleResolution: ts.ModuleResolutionKind.NodeJs,
ignoreDeprecations: '6.0',
experimentalDecorators: true, experimentalDecorators: true,
emitDecoratorMetadata: true, emitDecoratorMetadata: true,
strict: true, strict: true,
@ -365,6 +507,7 @@ describe('proxy service template — rendered output', () => {
} }
export class RestService { export class RestService {
request<TBody, TResponse>(req: any, config?: any): import('rxjs').Observable<TResponse>; request<TBody, TResponse>(req: any, config?: any): import('rxjs').Observable<TResponse>;
requestResource<TBody, TResponse>(request: any, config?: any, api?: string): any;
} }
} }
`; `;
@ -373,6 +516,8 @@ describe('proxy service template — rendered output', () => {
export function Injectable(opts?: any): ClassDecorator; export function Injectable(opts?: any): ClassDecorator;
export function inject<T>(token: { new (...args: any[]): T }): T; export function inject<T>(token: { new (...args: any[]): T }): T;
export function inject<T>(token: any): T; export function inject<T>(token: any): T;
export function computed<T>(fn: () => T): any;
export interface ResourceRef<T> { value?: T; }
} }
`; `;
const rxjsStub = ` const rxjsStub = `
@ -397,6 +542,7 @@ describe('proxy service template — rendered output', () => {
target: ts.ScriptTarget.ES2020, target: ts.ScriptTarget.ES2020,
module: ts.ModuleKind.ES2020, module: ts.ModuleKind.ES2020,
moduleResolution: ts.ModuleResolutionKind.NodeJs, moduleResolution: ts.ModuleResolutionKind.NodeJs,
ignoreDeprecations: '6.0',
experimentalDecorators: true, experimentalDecorators: true,
emitDecoratorMetadata: true, emitDecoratorMetadata: true,
strict: true, strict: true,

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

@ -35,6 +35,7 @@ export function createControllerToServiceMapper({
solution, solution,
types, types,
apiName, apiName,
resourceApi,
}: ServiceGeneratorParams) { }: ServiceGeneratorParams) {
const mapActionToMethod = createActionToMethodMapper(); 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: '@abp/ng.core', specifiers: ['RestService', 'Rest'] }));
imports.push(new Import({ path: '@angular/core', specifiers: ['Injectable', 'inject'] })); 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); sortImports(imports);
const methods = actions.map(mapActionToMethod); const methods = actions.map(mapActionToMethod);
sortMethods(methods); 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>) { function getTypesWithoutIRemoteStreamContent(types: Record<string, Type>) {
const newType = { ...types }; const newType = { ...types };
VOLO_REMOTE_STREAM_CONTENT.forEach(fileType => { 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) { export function createProxyClearer(targetPath: string) {
targetPath += PROXY_PATH; targetPath += PROXY_PATH;
const proxyIndexPath = `${targetPath}/index.ts`; const proxyIndexPath = `${targetPath}/index.ts`;

Loading…
Cancel
Save