@ojiepermana/angular-sdk 22.0.44 → 22.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -5
- package/schematics/collection.json +20 -0
- package/schematics/init/index.js +90 -0
- package/schematics/init/schema.json +19 -0
- package/schematics/ng-add/index.js +134 -0
- package/schematics/ng-add/schema.json +14 -0
- package/schematics/sdk/index.js +76 -0
- package/schematics/sdk/schema.json +19 -0
- package/sdk.config.example.json +24 -0
- package/src/config/loader.js +41 -0
- package/src/config/schema.js +58 -0
- package/src/emit/client.js +248 -0
- package/src/emit/metadata.js +295 -0
- package/src/emit/models.js +106 -0
- package/src/emit/navigation.js +56 -0
- package/src/emit/operations.js +122 -0
- package/src/emit/public-api.js +54 -0
- package/src/emit/services.js +107 -0
- package/src/engine.js +65 -0
- package/src/layout/per-domain.js +359 -0
- package/src/parser/bundle.js +25 -0
- package/src/parser/ir.js +320 -0
- package/src/parser/types.js +7 -0
- package/src/render/template.js +58 -0
- package/src/writer/index.js +278 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.emitOperations = emitOperations;
|
|
4
|
+
const template_1 = require("../render/template");
|
|
5
|
+
const models_1 = require("./models");
|
|
6
|
+
/**
|
|
7
|
+
* Emit one tree-shakeable operation function per operationId at
|
|
8
|
+
* `fn/<tag-kebab>/<operation-kebab>.ts`.
|
|
9
|
+
*
|
|
10
|
+
* Each file exports:
|
|
11
|
+
* - `<operationId>$Params` interface
|
|
12
|
+
* - `<operationId>` function returning `Observable<StrictHttpResponse<R>>`
|
|
13
|
+
* - `<operationId>.PATH` constant
|
|
14
|
+
*/
|
|
15
|
+
function emitOperations(ir, target) {
|
|
16
|
+
return ir.operations.map((op) => ({
|
|
17
|
+
path: `fn/${(0, template_1.kebabCase)(op.tag)}/${(0, template_1.kebabCase)(op.operationId)}.ts`,
|
|
18
|
+
content: (0, template_1.finalize)(renderOperation(op, target)),
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
function renderOperation(op, target) {
|
|
22
|
+
const fnName = (0, template_1.camelCase)(op.operationId);
|
|
23
|
+
const paramsName = `${(0, template_1.pascalCase)(op.operationId)}$Params`;
|
|
24
|
+
const responseType = op.successRef ? (0, template_1.pascalCase)(op.successRef) : 'unknown';
|
|
25
|
+
const paramImports = collectParamImports(op);
|
|
26
|
+
const imports = [
|
|
27
|
+
`import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http';`,
|
|
28
|
+
`import { Observable } from 'rxjs';`,
|
|
29
|
+
`import { filter, map } from 'rxjs/operators';`,
|
|
30
|
+
``,
|
|
31
|
+
`import { RequestBuilder } from '../../request-builder';`,
|
|
32
|
+
`import { StrictHttpResponse } from '../../strict-http-response';`,
|
|
33
|
+
];
|
|
34
|
+
for (const name of paramImports) {
|
|
35
|
+
imports.push(`import type { ${(0, template_1.pascalCase)(name)} } from '../../models/${(0, template_1.kebabCase)(name)}';`);
|
|
36
|
+
}
|
|
37
|
+
const paramsInterface = renderParamsInterface(op, paramsName);
|
|
38
|
+
const hasAnyParams = paramsInterface.hasAnyField;
|
|
39
|
+
const paramsOptional = !paramsInterface.anyRequired;
|
|
40
|
+
const bodyCT = op.bodySchemaRef ? "'application/json'" : '';
|
|
41
|
+
const pathCalls = op.params
|
|
42
|
+
.filter((p) => p.in === 'path')
|
|
43
|
+
.map((p) => ` rb.path('${p.name}', params.${safeProp(p.name)});`);
|
|
44
|
+
const queryCalls = op.params
|
|
45
|
+
.filter((p) => p.in === 'query')
|
|
46
|
+
.map((p) => ` rb.query('${p.name}', params.${safeProp(p.name)});`);
|
|
47
|
+
const headerCalls = op.params
|
|
48
|
+
.filter((p) => p.in === 'header')
|
|
49
|
+
.map((p) => ` rb.header('${p.name}', params.${safeProp(p.name)});`);
|
|
50
|
+
const bodyCall = op.bodySchemaRef ? ` rb.body(params.body, ${bodyCT});` : '';
|
|
51
|
+
const populate = pathCalls.length || queryCalls.length || headerCalls.length || bodyCall
|
|
52
|
+
? ` if (params) {
|
|
53
|
+
${[...pathCalls, ...queryCalls, ...headerCalls, bodyCall].filter(Boolean).join('\n')}
|
|
54
|
+
}`
|
|
55
|
+
: '';
|
|
56
|
+
const paramsArg = hasAnyParams
|
|
57
|
+
? `params${paramsOptional ? '?' : ''}: ${paramsName}`
|
|
58
|
+
: `_params?: Record<string, never>`;
|
|
59
|
+
return `${target.banner}
|
|
60
|
+
|
|
61
|
+
${imports.join('\n')}
|
|
62
|
+
|
|
63
|
+
${paramsInterface.source}
|
|
64
|
+
export function ${fnName}(
|
|
65
|
+
http: HttpClient,
|
|
66
|
+
rootUrl: string,
|
|
67
|
+
${paramsArg},
|
|
68
|
+
context?: HttpContext,
|
|
69
|
+
): Observable<StrictHttpResponse<${responseType}>> {
|
|
70
|
+
const rb = new RequestBuilder(rootUrl, ${fnName}.PATH, '${op.method}');
|
|
71
|
+
${populate}
|
|
72
|
+
return http.request(rb.build({ responseType: 'json', accept: 'application/json', context })).pipe(
|
|
73
|
+
filter((r: unknown): r is HttpResponse<unknown> => r instanceof HttpResponse),
|
|
74
|
+
map((r) => r as StrictHttpResponse<${responseType}>),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
${fnName}.PATH = '${op.path}';
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
function collectParamImports(op) {
|
|
82
|
+
const set = new Set();
|
|
83
|
+
for (const p of op.params) {
|
|
84
|
+
if (p.rule.ref)
|
|
85
|
+
set.add(p.rule.ref);
|
|
86
|
+
if (p.rule.itemsRef)
|
|
87
|
+
set.add(p.rule.itemsRef);
|
|
88
|
+
}
|
|
89
|
+
if (op.bodySchemaRef)
|
|
90
|
+
set.add(op.bodySchemaRef);
|
|
91
|
+
if (op.successRef)
|
|
92
|
+
set.add(op.successRef);
|
|
93
|
+
return [...set].sort();
|
|
94
|
+
}
|
|
95
|
+
function renderParamsInterface(op, paramsName) {
|
|
96
|
+
const lines = [];
|
|
97
|
+
let anyRequired = false;
|
|
98
|
+
const pushField = (p) => {
|
|
99
|
+
const optional = p.required ? '' : '?';
|
|
100
|
+
if (p.required)
|
|
101
|
+
anyRequired = true;
|
|
102
|
+
const type = (0, models_1.renderFieldType)(p.rule);
|
|
103
|
+
const name = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(p.name) ? p.name : `'${p.name}'`;
|
|
104
|
+
lines.push(` ${name}${optional}: ${type};`);
|
|
105
|
+
};
|
|
106
|
+
for (const p of op.params)
|
|
107
|
+
pushField(p);
|
|
108
|
+
if (op.bodySchemaRef) {
|
|
109
|
+
const optional = op.bodyRequired ? '' : '?';
|
|
110
|
+
if (op.bodyRequired)
|
|
111
|
+
anyRequired = true;
|
|
112
|
+
lines.push(` body${optional}: ${(0, template_1.pascalCase)(op.bodySchemaRef)};`);
|
|
113
|
+
}
|
|
114
|
+
const hasAnyField = lines.length > 0;
|
|
115
|
+
const source = hasAnyField
|
|
116
|
+
? `export interface ${paramsName} {\n${lines.join('\n')}\n}\n`
|
|
117
|
+
: `export type ${paramsName} = Record<string, never>;\n`;
|
|
118
|
+
return { source, hasAnyField, anyRequired };
|
|
119
|
+
}
|
|
120
|
+
function safeProp(name) {
|
|
121
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : `['${name}']`;
|
|
122
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.emitPublicApi = emitPublicApi;
|
|
4
|
+
const template_1 = require("../render/template");
|
|
5
|
+
/**
|
|
6
|
+
* Emit `public-api.ts` — the barrel that re-exports every generated artifact
|
|
7
|
+
* based on the resolved feature flags. Mirrors the convention used by the
|
|
8
|
+
* reference output in `old/api/index.ts`.
|
|
9
|
+
*/
|
|
10
|
+
function emitPublicApi(ir, target) {
|
|
11
|
+
const lines = [target.banner, ''];
|
|
12
|
+
if (target.features.client) {
|
|
13
|
+
lines.push(`export { ApiConfiguration, provideApiConfiguration } from './api-configuration';`);
|
|
14
|
+
lines.push(`export { BaseService } from './base-service';`);
|
|
15
|
+
lines.push(`export { RequestBuilder } from './request-builder';`);
|
|
16
|
+
lines.push(`export type { StrictHttpResponse } from './strict-http-response';`);
|
|
17
|
+
lines.push(`export { ${target.clientName} } from './api';`);
|
|
18
|
+
lines.push('');
|
|
19
|
+
}
|
|
20
|
+
if (target.features.models) {
|
|
21
|
+
for (const schema of ir.schemas) {
|
|
22
|
+
lines.push(`export type { ${(0, template_1.pascalCase)(schema.name)} } from './models/${(0, template_1.kebabCase)(schema.name)}';`);
|
|
23
|
+
}
|
|
24
|
+
lines.push('');
|
|
25
|
+
}
|
|
26
|
+
if (target.features.services) {
|
|
27
|
+
const tags = Array.from(new Set(ir.operations.map((o) => o.tag))).sort();
|
|
28
|
+
for (const tag of tags) {
|
|
29
|
+
lines.push(`export { ${(0, template_1.pascalCase)(tag)}Service } from './services/${(0, template_1.kebabCase)(tag)}.service';`);
|
|
30
|
+
}
|
|
31
|
+
lines.push('');
|
|
32
|
+
}
|
|
33
|
+
if (target.features.operations) {
|
|
34
|
+
const sorted = [...ir.operations].sort((a, b) => a.operationId.localeCompare(b.operationId));
|
|
35
|
+
for (const op of sorted) {
|
|
36
|
+
lines.push(renderFnReexport(op));
|
|
37
|
+
}
|
|
38
|
+
lines.push('');
|
|
39
|
+
}
|
|
40
|
+
if (target.features.metadata) {
|
|
41
|
+
lines.push(`export * from './metadata';`);
|
|
42
|
+
lines.push(`export * from './openapi-helpers';`);
|
|
43
|
+
lines.push('');
|
|
44
|
+
}
|
|
45
|
+
if (target.features.navigation) {
|
|
46
|
+
lines.push(`export { ApiNavigation } from './api.navigation';`);
|
|
47
|
+
}
|
|
48
|
+
return [{ path: 'public-api.ts', content: (0, template_1.finalize)(lines.join('\n')) }];
|
|
49
|
+
}
|
|
50
|
+
function renderFnReexport(op) {
|
|
51
|
+
const fnName = (0, template_1.camelCase)(op.operationId);
|
|
52
|
+
const paramsName = `${(0, template_1.pascalCase)(op.operationId)}$Params`;
|
|
53
|
+
return `export { ${fnName}, type ${paramsName} } from './fn/${(0, template_1.kebabCase)(op.tag)}/${(0, template_1.kebabCase)(op.operationId)}';`;
|
|
54
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.emitServices = emitServices;
|
|
4
|
+
const template_1 = require("../render/template");
|
|
5
|
+
/**
|
|
6
|
+
* Emit one Injectable service per tag at `services/<tag-kebab>.service.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Each method returns `Observable<T>` (body-only) plus a `<method>$Response`
|
|
9
|
+
* variant that returns the full `StrictHttpResponse<T>`. Bodies are dispatched
|
|
10
|
+
* through the tree-shakeable fn modules emitted by {@link emitOperations}, so
|
|
11
|
+
* unused methods can be shaken out.
|
|
12
|
+
*/
|
|
13
|
+
function emitServices(ir, target) {
|
|
14
|
+
const byTag = groupByTag(ir.operations);
|
|
15
|
+
const files = [];
|
|
16
|
+
for (const [tag, ops] of byTag) {
|
|
17
|
+
files.push({
|
|
18
|
+
path: `services/${(0, template_1.kebabCase)(tag)}.service.ts`,
|
|
19
|
+
content: (0, template_1.finalize)(renderService(tag, ops, target)),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return files;
|
|
23
|
+
}
|
|
24
|
+
function groupByTag(operations) {
|
|
25
|
+
const map = new Map();
|
|
26
|
+
for (const op of operations) {
|
|
27
|
+
const bucket = map.get(op.tag) ?? [];
|
|
28
|
+
bucket.push(op);
|
|
29
|
+
map.set(op.tag, bucket);
|
|
30
|
+
}
|
|
31
|
+
return map;
|
|
32
|
+
}
|
|
33
|
+
function renderService(tag, ops, target) {
|
|
34
|
+
const className = `${(0, template_1.pascalCase)(tag)}Service`;
|
|
35
|
+
const sorted = [...ops].sort((a, b) => a.operationId.localeCompare(b.operationId));
|
|
36
|
+
const fnImports = [];
|
|
37
|
+
const modelImports = new Set();
|
|
38
|
+
for (const op of sorted) {
|
|
39
|
+
const fnName = (0, template_1.camelCase)(op.operationId);
|
|
40
|
+
const paramsName = `${(0, template_1.pascalCase)(op.operationId)}$Params`;
|
|
41
|
+
fnImports.push(`import { ${fnName}, type ${paramsName} } from '../fn/${(0, template_1.kebabCase)(tag)}/${(0, template_1.kebabCase)(op.operationId)}';`);
|
|
42
|
+
if (op.successRef)
|
|
43
|
+
modelImports.add(op.successRef);
|
|
44
|
+
}
|
|
45
|
+
const modelImportLines = [...modelImports]
|
|
46
|
+
.sort()
|
|
47
|
+
.map((ref) => `import type { ${(0, template_1.pascalCase)(ref)} } from '../models/${(0, template_1.kebabCase)(ref)}';`);
|
|
48
|
+
const methods = sorted.map((op) => renderMethod(op, target.features.resources)).join('\n\n');
|
|
49
|
+
return `${target.banner}
|
|
50
|
+
|
|
51
|
+
import { HttpContext, HttpResponse } from '@angular/common/http';
|
|
52
|
+
import { Injectable } from '@angular/core';
|
|
53
|
+
${target.features.resources ? "import { rxResource } from '@angular/core/rxjs-interop';\n" : ''}import { Observable } from 'rxjs';
|
|
54
|
+
import { filter, map } from 'rxjs/operators';
|
|
55
|
+
|
|
56
|
+
import { BaseService } from '../base-service';
|
|
57
|
+
import { StrictHttpResponse } from '../strict-http-response';
|
|
58
|
+
|
|
59
|
+
${fnImports.join('\n')}
|
|
60
|
+
${modelImportLines.length ? '\n' + modelImportLines.join('\n') : ''}
|
|
61
|
+
|
|
62
|
+
@Injectable({ providedIn: 'root' })
|
|
63
|
+
export class ${className} extends BaseService {
|
|
64
|
+
${methods}
|
|
65
|
+
}
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
function renderMethod(op, emitResources) {
|
|
69
|
+
const fnName = (0, template_1.camelCase)(op.operationId);
|
|
70
|
+
const paramsName = `${(0, template_1.pascalCase)(op.operationId)}$Params`;
|
|
71
|
+
const responseType = op.successRef ? (0, template_1.pascalCase)(op.successRef) : 'unknown';
|
|
72
|
+
const hasParams = op.params.length > 0 || Boolean(op.bodySchemaRef);
|
|
73
|
+
const anyRequired = op.params.some((p) => p.required) || (op.bodySchemaRef != null && op.bodyRequired);
|
|
74
|
+
const paramsSig = hasParams ? `params${anyRequired ? '' : '?'}: ${paramsName}` : `params?: ${paramsName}`;
|
|
75
|
+
let resourceMethod = '';
|
|
76
|
+
if (emitResources) {
|
|
77
|
+
if (hasParams) {
|
|
78
|
+
resourceMethod = `\n\n /** Resource wrapper for ${op.summary ?? op.operationId} */
|
|
79
|
+
${fnName}Resource(requestFn: () => ${paramsName}, context?: HttpContext) {
|
|
80
|
+
return rxResource({
|
|
81
|
+
request: requestFn,
|
|
82
|
+
loader: ({ request }) => this.${fnName}(request, context),
|
|
83
|
+
});
|
|
84
|
+
}`;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
resourceMethod = `\n\n /** Resource wrapper for ${op.summary ?? op.operationId} */
|
|
88
|
+
${fnName}Resource(context?: HttpContext) {
|
|
89
|
+
return rxResource({
|
|
90
|
+
loader: () => this.${fnName}(undefined, context),
|
|
91
|
+
});
|
|
92
|
+
}`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return ` /** ${op.summary ?? op.operationId} (${op.method.toUpperCase()} ${op.path}) */
|
|
96
|
+
${fnName}$Response(${paramsSig}, context?: HttpContext): Observable<StrictHttpResponse<${responseType}>> {
|
|
97
|
+
return ${fnName}(this.http, this.rootUrl, params as ${paramsName}, context).pipe(
|
|
98
|
+
filter((r: unknown): r is HttpResponse<unknown> => r instanceof HttpResponse),
|
|
99
|
+
map((r) => r as StrictHttpResponse<${responseType}>),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** ${op.summary ?? op.operationId} (${op.method.toUpperCase()} ${op.path}) */
|
|
104
|
+
${fnName}(${paramsSig}, context?: HttpContext): Observable<${responseType}> {
|
|
105
|
+
return this.${fnName}$Response(params as ${paramsName}, context).pipe(map((r) => r.body));
|
|
106
|
+
}${resourceMethod}`;
|
|
107
|
+
}
|
package/src/engine.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generate = generate;
|
|
4
|
+
const node_path_1 = require("node:path");
|
|
5
|
+
const client_1 = require("./emit/client");
|
|
6
|
+
const metadata_1 = require("./emit/metadata");
|
|
7
|
+
const models_1 = require("./emit/models");
|
|
8
|
+
const navigation_1 = require("./emit/navigation");
|
|
9
|
+
const operations_1 = require("./emit/operations");
|
|
10
|
+
const public_api_1 = require("./emit/public-api");
|
|
11
|
+
const services_1 = require("./emit/services");
|
|
12
|
+
const per_domain_1 = require("./layout/per-domain");
|
|
13
|
+
const ir_1 = require("./parser/ir");
|
|
14
|
+
const bundle_1 = require("./parser/bundle");
|
|
15
|
+
const index_1 = require("./writer/index");
|
|
16
|
+
/**
|
|
17
|
+
* Run the generator for a single resolved target. Pure in the sense that it
|
|
18
|
+
* does **not** touch the filesystem — the caller is expected to materialize
|
|
19
|
+
* `result.files` either into an Angular schematic `Tree` or onto disk.
|
|
20
|
+
*/
|
|
21
|
+
async function generate(target, workspaceRoot) {
|
|
22
|
+
const specPath = (0, node_path_1.isAbsolute)(target.input) ? target.input : (0, node_path_1.resolve)(workspaceRoot, target.input);
|
|
23
|
+
const doc = await (0, bundle_1.loadSpec)(specPath);
|
|
24
|
+
const ir = (0, ir_1.buildIR)(doc);
|
|
25
|
+
const files = [];
|
|
26
|
+
if (target.features.client)
|
|
27
|
+
files.push(...(0, client_1.emitClient)(target));
|
|
28
|
+
if (target.features.models)
|
|
29
|
+
files.push(...(0, models_1.emitModels)(ir, target));
|
|
30
|
+
if (target.features.operations)
|
|
31
|
+
files.push(...(0, operations_1.emitOperations)(ir, target));
|
|
32
|
+
if (target.features.services)
|
|
33
|
+
files.push(...(0, services_1.emitServices)(ir, target));
|
|
34
|
+
if (target.features.metadata)
|
|
35
|
+
files.push(...(0, metadata_1.emitMetadata)(ir, target));
|
|
36
|
+
if (target.features.navigation)
|
|
37
|
+
files.push(...(0, navigation_1.emitNavigation)(ir, target));
|
|
38
|
+
// public-api barrel is emitted last so it sees the final feature set.
|
|
39
|
+
files.push(...(0, public_api_1.emitPublicApi)(ir, target));
|
|
40
|
+
const laidOut = target.splitByDomain ? (0, per_domain_1.relayoutPerDomain)(files, ir, target) : files;
|
|
41
|
+
const outputDir = (0, node_path_1.isAbsolute)(target.output) ? target.output : (0, node_path_1.resolve)(workspaceRoot, target.output);
|
|
42
|
+
const wrapped = applyMode(laidOut, ir, target, workspaceRoot, outputDir);
|
|
43
|
+
return {
|
|
44
|
+
target,
|
|
45
|
+
outputDir,
|
|
46
|
+
files: wrapped,
|
|
47
|
+
stats: {
|
|
48
|
+
schemas: ir.schemas.length,
|
|
49
|
+
operations: ir.operations.length,
|
|
50
|
+
tags: new Set(ir.operations.map((o) => o.tag)).size,
|
|
51
|
+
files: wrapped.length,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function applyMode(files, ir, target, workspaceRoot, outputDir) {
|
|
56
|
+
switch (target.mode) {
|
|
57
|
+
case 'library':
|
|
58
|
+
return (0, index_1.writeLibrary)(files, ir, target, workspaceRoot, outputDir);
|
|
59
|
+
case 'secondary-entrypoint':
|
|
60
|
+
return (0, index_1.writeSecondaryEntrypoint)(files, ir, target, workspaceRoot, outputDir);
|
|
61
|
+
case 'standalone':
|
|
62
|
+
default:
|
|
63
|
+
return (0, index_1.writeStandalone)(files);
|
|
64
|
+
}
|
|
65
|
+
}
|