@webpieces/nx-webpieces-rules 0.4.794 → 0.4.796
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 +6 -6
- package/src/lib/api-usage/api-ast.d.ts +16 -80
- package/src/lib/api-usage/api-ast.js +75 -98
- package/src/lib/api-usage/api-ast.js.map +1 -1
- package/src/lib/api-usage/api-contract-errors.d.ts +8 -3
- package/src/lib/api-usage/api-contract-errors.js +24 -8
- package/src/lib/api-usage/api-contract-errors.js.map +1 -1
- package/src/lib/api-usage/api-relations.d.ts +26 -2
- package/src/lib/api-usage/api-relations.js +23 -1
- package/src/lib/api-usage/api-relations.js.map +1 -1
- package/src/lib/api-usage/api-scanner.d.ts +8 -5
- package/src/lib/api-usage/api-scanner.js +17 -7
- package/src/lib/api-usage/api-scanner.js.map +1 -1
- package/src/lib/runtime-graph-model.d.ts +1 -1
- package/src/lib/runtime-graph-model.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/nx-webpieces-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.796",
|
|
4
4
|
"description": "Nx-specific webpieces validation rules and graph tooling. Bundles all @webpieces rule packages with Nx graph validators and an inference plugin.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@webpieces/ai-hook-rules": "0.4.
|
|
22
|
-
"@webpieces/code-rules": "0.4.
|
|
23
|
-
"@webpieces/eslint-rules": "0.4.
|
|
24
|
-
"@webpieces/pr-gate": "0.4.
|
|
25
|
-
"@webpieces/rules-config": "0.4.
|
|
21
|
+
"@webpieces/ai-hook-rules": "0.4.796",
|
|
22
|
+
"@webpieces/code-rules": "0.4.796",
|
|
23
|
+
"@webpieces/eslint-rules": "0.4.796",
|
|
24
|
+
"@webpieces/pr-gate": "0.4.796",
|
|
25
|
+
"@webpieces/rules-config": "0.4.796",
|
|
26
26
|
"madge": "8.0.0"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
@@ -1,30 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* API contract AST accessors
|
|
3
|
-
*
|
|
4
|
-
* The pure, stateless half of the api scan: given a TypeScript node, what contract / endpoint /
|
|
5
|
-
* injected type does it describe? Split out of api-scanner.ts, which owns the STATEFUL walk (project
|
|
6
|
-
* programs, the source index, relation accumulation) and had grown past the file-size limit.
|
|
7
|
-
*
|
|
8
|
-
* Everything here is parser-level on purpose. Decorators must be read exactly as written, and a
|
|
9
|
-
* plain parse cannot be diverted to a decorator-erased `.d.ts` by module resolution — the bug
|
|
10
|
-
* api-scanner's source pre-pass exists to guard against.
|
|
11
|
-
*/
|
|
1
|
+
/** Parser-only API contract AST accessors; deliberately avoids module resolution and erased d.ts. */
|
|
12
2
|
import * as ts from 'typescript';
|
|
13
|
-
import { ApiClassInfo, ApiMethodMeta, ApiParameterMeta, ContractHttpMethod, ApiTransport, EmptiedApiContract, ExternalSystemDeclaration, NonLiteralDecoratorArg, UndeclaredExternalCaller, UnresolvedEndpointPath } from './api-relations';
|
|
14
|
-
/**
|
|
15
|
-
* The module-scope `const NAME = '<string literal>'` bindings of ONE source file.
|
|
16
|
-
*
|
|
17
|
-
* A contract that hoists its route to a constant (`@ApiPath(WHATSAPP_API_PATH)`) is good practice —
|
|
18
|
-
* it lets a sibling contract and its callers share the symbol — but a decorator argument is read as
|
|
19
|
-
* TEXT here, with no checker to constant-fold it. Without this table such an argument resolved to
|
|
20
|
-
* nothing: the class lost its basePath, and a class whose every @Endpoint path was a constant
|
|
21
|
-
* resolved to zero methods and was dropped from the graph entirely.
|
|
22
|
-
*
|
|
23
|
-
* Deliberately SAME-MODULE only. Following an import would mean resolving modules, which is exactly
|
|
24
|
-
* what the source pre-pass avoids (it can be diverted to a decorator-erased `.d.ts`). A cross-module
|
|
25
|
-
* constant is therefore still unresolvable — and is REPORTED rather than silently dropped, see
|
|
26
|
-
* DecoratorArgDiagnostics.
|
|
27
|
-
*/
|
|
3
|
+
import { ApiClassInfo, ApiMethodMeta, ApiParameterMeta, ContractHttpMethod, ApiTransport, EmptiedApiContract, EndpointOperation, ExternalSystemDeclaration, NonLiteralDecoratorArg, UndeclaredExternalCaller, UndeclaredEndpointOperation, UnresolvedEndpointPath } from './api-relations';
|
|
4
|
+
/** Same-module string constants that parser-only decorator scanning can safely resolve. */
|
|
28
5
|
export declare class ModuleStringConstants {
|
|
29
6
|
private readonly byName;
|
|
30
7
|
constructor(byName: Map<string, string>);
|
|
@@ -34,14 +11,7 @@ export declare class ModuleStringConstants {
|
|
|
34
11
|
export declare function stringConstantsOf(sourceFile: ts.SourceFile): ModuleStringConstants;
|
|
35
12
|
/** The string an initializer denotes, unwrapping `as const` / parentheses, else null. */
|
|
36
13
|
export declare function stringValueOf(expr: ts.Expression | undefined): string | null;
|
|
37
|
-
/**
|
|
38
|
-
* ONE decorator argument that had to be a string, and what came of it.
|
|
39
|
-
*
|
|
40
|
-
* `value` is the string when it was a literal or resolved through a same-module constant.
|
|
41
|
-
* `unresolvedName` is the argument as written (`WHATSAPP_API_PATH`) when it is present but could not
|
|
42
|
-
* be reduced — the case that must be reported, never silently dropped. Both are null when the
|
|
43
|
-
* argument is simply absent.
|
|
44
|
-
*/
|
|
14
|
+
/** A resolved decorator string, or the source spelling that could not be reduced. */
|
|
45
15
|
export declare class DecoratorArgValue {
|
|
46
16
|
readonly value: string | null;
|
|
47
17
|
readonly unresolvedName: string | null;
|
|
@@ -49,26 +19,14 @@ export declare class DecoratorArgValue {
|
|
|
49
19
|
}
|
|
50
20
|
/** Read one decorator argument as a string, resolving same-module constants. */
|
|
51
21
|
export declare function decoratorArgValue(expr: ts.Expression | undefined, constants: ModuleStringConstants): DecoratorArgValue;
|
|
52
|
-
/**
|
|
53
|
-
* Collects everything this parser-only pass had to drop: decorator arguments it could not reduce to
|
|
54
|
-
* a string, plus the two of those that are FATAL rather than merely lossy.
|
|
55
|
-
*
|
|
56
|
-
* A same-module constant now resolves, but a cross-module one (`import { PATH } from './paths'`)
|
|
57
|
-
* genuinely cannot — the source pre-pass has no checker by design. That gap used to be invisible:
|
|
58
|
-
* the contract simply came out with no basePath, or with fewer methods, or not at all. Recording it
|
|
59
|
-
* turns a silent drop into a named one, pointing at the exact file, line and identifier.
|
|
60
|
-
*
|
|
61
|
-
* Three sinks, because the consequences differ. `record` is the warning stream (a @Queue name falls
|
|
62
|
-
* back to a derived one, so the graph is degraded, not wrong). `recordUnresolvedPath` and
|
|
63
|
-
* `recordEmptiedContract` are collected so generation can FAIL — one aggregated error naming every
|
|
64
|
-
* offender, because an author fixing five constants wants all five in one run.
|
|
65
|
-
*/
|
|
22
|
+
/** Aggregates parser-only scan losses so generation can fail loudly instead of dropping routes. */
|
|
66
23
|
export declare class DecoratorArgDiagnostics {
|
|
67
24
|
private readonly workspaceRoot;
|
|
68
25
|
private readonly found;
|
|
69
26
|
private readonly unresolvedPaths;
|
|
70
27
|
private readonly emptied;
|
|
71
28
|
private readonly undeclaredCallers;
|
|
29
|
+
private readonly undeclaredOperations;
|
|
72
30
|
constructor(workspaceRoot: string);
|
|
73
31
|
/** Record `argument` (as written) as unresolvable at `node`'s location. */
|
|
74
32
|
record(api: string, decorator: string, method: string | null, argument: string, node: ts.Node): void;
|
|
@@ -78,43 +36,29 @@ export declare class DecoratorArgDiagnostics {
|
|
|
78
36
|
recordEmptiedContract(api: string, declared: number, node: ts.Node): void;
|
|
79
37
|
/** Record an `external` `@Endpoint` whose caller is unreadable — fatal, see UndeclaredExternalCallerError. */
|
|
80
38
|
recordUndeclaredCaller(api: string, method: string, argument: string, node: ts.Node): void;
|
|
39
|
+
/** Record an `@Endpoint` whose required operation is missing or unreadable. */
|
|
40
|
+
recordUndeclaredOperation(api: string, method: string, argument: string, node: ts.Node): void;
|
|
81
41
|
all(): NonLiteralDecoratorArg[];
|
|
82
42
|
unresolvedEndpointPaths(): UnresolvedEndpointPath[];
|
|
83
43
|
emptiedContracts(): EmptiedApiContract[];
|
|
84
44
|
undeclaredExternalCallers(): UndeclaredExternalCaller[];
|
|
45
|
+
undeclaredEndpointOperations(): UndeclaredEndpointOperation[];
|
|
85
46
|
private locate;
|
|
86
47
|
}
|
|
87
48
|
/** {api, owner: `project`, type} when `cls` is an `abstract class` carrying `@ApiPath`, else null. */
|
|
88
49
|
export declare function apiClassInfoFrom(cls: ts.ClassDeclaration, project: string, diagnostics?: DecoratorArgDiagnostics | null): ApiClassInfo | null;
|
|
89
50
|
export declare function apiTransport(cls: ts.ClassDeclaration): ApiTransport;
|
|
90
|
-
/**
|
|
91
|
-
* Every `@Endpoint(path, kind)` method on a contract class, in declaration order.
|
|
92
|
-
*
|
|
93
|
-
* `kind` is a REQUIRED argument of the decorator, so a missing/non-literal second argument means the
|
|
94
|
-
* source does not compile (or is mid-edit) — we skip the method rather than defaulting it. Defaulting
|
|
95
|
-
* would put an undeclared cron or webhook into the graph as an ordinary rpc call, which is precisely
|
|
96
|
-
* the blindness the required argument exists to remove.
|
|
97
|
-
*
|
|
98
|
-
* `path` is NOT skippable. It may be a same-module constant; an argument that is present but still
|
|
99
|
-
* cannot be reduced is recorded on `diagnostics` as an UnresolvedEndpointPath, which FAILS generation
|
|
100
|
-
* later. Upstream components need the URL — a client computes its request as `basePath + path` — so
|
|
101
|
-
* dropping the method here shipped a contract missing routing information, and a class whose every
|
|
102
|
-
* path was a constant lost every method and disappeared from the graph entirely.
|
|
103
|
-
*
|
|
104
|
-
* A class that declared endpoints and kept NONE of them is recorded too: `buildApiContracts` skips
|
|
105
|
-
* zero-method classes, which is the door a gutted contract used to leave through unannounced.
|
|
106
|
-
*/
|
|
51
|
+
/** Reads required `(method, path, operation, kind, options?)` endpoint declarations in order. */
|
|
107
52
|
export declare function endpointMethodsOf(cls: ts.ClassDeclaration, api: string, constants?: ModuleStringConstants, diagnostics?: DecoratorArgDiagnostics | null): ApiMethodMeta[];
|
|
108
|
-
/**
|
|
109
|
-
export declare function
|
|
53
|
+
/** Required side-effect declaration; no inference from GET/POST is permitted. */
|
|
54
|
+
export declare function endpointOperationOf(operationArgument: ts.Expression | undefined, constants: ModuleStringConstants, diagnostics: DecoratorArgDiagnostics | null, api: string, method: string, node: ts.Node): EndpointOperation | null;
|
|
55
|
+
/** Required first positional HTTP method. */
|
|
56
|
+
export declare function httpMethodOf(methodArgument: ts.Expression | undefined, constants: ModuleStringConstants, diagnostics: DecoratorArgDiagnostics | null, api: string, method: string, node: ts.Node): ContractHttpMethod | null;
|
|
110
57
|
/** Only the non-default full-response marker needs an architecture field. */
|
|
111
58
|
export declare function endpointResponseTypeOf(options: ts.Expression | undefined, constants: ModuleStringConstants): 'body' | 'full';
|
|
112
59
|
/** Explicit `@PathParam` / `@QueryParam` mappings in source declaration order. */
|
|
113
60
|
export declare function httpParametersOf(member: ts.MethodDeclaration, httpMethod: ContractHttpMethod, constants: ModuleStringConstants, diagnostics: DecoratorArgDiagnostics | null, api: string, method: string): ApiParameterMeta[];
|
|
114
|
-
/**
|
|
115
|
-
* The outcome of reading `@Endpoint(path, 'external', { calledBy, callerKind })`'s third argument:
|
|
116
|
-
* either the resolved declaration, or the reason it could not be resolved (never both).
|
|
117
|
-
*/
|
|
61
|
+
/** Resolved EXTERNAL caller declaration, or the source problem that prevented resolution. */
|
|
118
62
|
export declare class ExternalCallerRead {
|
|
119
63
|
readonly declaration: ExternalSystemDeclaration | null;
|
|
120
64
|
/** What was wrong, as written, for the diagnostic. Null exactly when `declaration` is set. */
|
|
@@ -123,15 +67,7 @@ export declare class ExternalCallerRead {
|
|
|
123
67
|
/** What was wrong, as written, for the diagnostic. Null exactly when `declaration` is set. */
|
|
124
68
|
problem: string | null);
|
|
125
69
|
}
|
|
126
|
-
/**
|
|
127
|
-
* Read the declared caller out of the @Endpoint OPTIONS OBJECT LITERAL — `args[2]`, not a positional
|
|
128
|
-
* argument, because that is where `formPost` already lives and one options bag beats two.
|
|
129
|
-
*
|
|
130
|
-
* Everything unreadable is a PROBLEM, never a default: an unknown `callerKind` draws the wrong shape
|
|
131
|
-
* (which teaches the reader something false), and a missing `calledBy` puts us back at a box that can
|
|
132
|
-
* only name our own contract. The kind default applies ONLY to the case the API deliberately allows —
|
|
133
|
-
* `calledBy` present, `callerKind` absent.
|
|
134
|
-
*/
|
|
70
|
+
/** Reads `calledBy` and optional `callerKind` from the fifth positional options argument. */
|
|
135
71
|
export declare function externalCallerOf(arg: ts.Expression | undefined, constants: ModuleStringConstants): ExternalCallerRead;
|
|
136
72
|
/** One property of an object literal, read as a string through the same constant folding as an argument. */
|
|
137
73
|
export declare function objectPropertyValue(literal: ts.ObjectLiteralExpression, name: string, constants: ModuleStringConstants): DecoratorArgValue;
|
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* API contract AST accessors
|
|
4
|
-
*
|
|
5
|
-
* The pure, stateless half of the api scan: given a TypeScript node, what contract / endpoint /
|
|
6
|
-
* injected type does it describe? Split out of api-scanner.ts, which owns the STATEFUL walk (project
|
|
7
|
-
* programs, the source index, relation accumulation) and had grown past the file-size limit.
|
|
8
|
-
*
|
|
9
|
-
* Everything here is parser-level on purpose. Decorators must be read exactly as written, and a
|
|
10
|
-
* plain parse cannot be diverted to a decorator-erased `.d.ts` by module resolution — the bug
|
|
11
|
-
* api-scanner's source pre-pass exists to guard against.
|
|
12
|
-
*/
|
|
2
|
+
/** Parser-only API contract AST accessors; deliberately avoids module resolution and erased d.ts. */
|
|
13
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
4
|
exports.ExternalCallerRead = exports.DecoratorArgDiagnostics = exports.DecoratorArgValue = exports.ModuleStringConstants = void 0;
|
|
15
5
|
exports.stringConstantsOf = stringConstantsOf;
|
|
@@ -18,6 +8,7 @@ exports.decoratorArgValue = decoratorArgValue;
|
|
|
18
8
|
exports.apiClassInfoFrom = apiClassInfoFrom;
|
|
19
9
|
exports.apiTransport = apiTransport;
|
|
20
10
|
exports.endpointMethodsOf = endpointMethodsOf;
|
|
11
|
+
exports.endpointOperationOf = endpointOperationOf;
|
|
21
12
|
exports.httpMethodOf = httpMethodOf;
|
|
22
13
|
exports.endpointResponseTypeOf = endpointResponseTypeOf;
|
|
23
14
|
exports.httpParametersOf = httpParametersOf;
|
|
@@ -48,8 +39,20 @@ const fs = tslib_1.__importStar(require("fs"));
|
|
|
48
39
|
const path = tslib_1.__importStar(require("path"));
|
|
49
40
|
const bindings_1 = require("../di-graph/bindings");
|
|
50
41
|
const api_relations_1 = require("./api-relations");
|
|
51
|
-
/** Legal `@Endpoint
|
|
42
|
+
/** Legal enum-backed `@Endpoint` symbols; tooling reads source without importing application code. */
|
|
52
43
|
const ENDPOINT_KINDS = ['rpc', 'cloudtasks', 'cron', 'external'];
|
|
44
|
+
const ENDPOINT_OPERATIONS = ['read', 'write-idempotent', 'write'];
|
|
45
|
+
const ENDPOINT_SYMBOLS = {
|
|
46
|
+
GET: 'GET',
|
|
47
|
+
POST: 'POST',
|
|
48
|
+
READ: 'read',
|
|
49
|
+
WRITE_IDEMPOTENT: 'write-idempotent',
|
|
50
|
+
WRITE: 'write',
|
|
51
|
+
RPC: 'rpc',
|
|
52
|
+
CLOUDTASKS: 'cloudtasks',
|
|
53
|
+
CRON: 'cron',
|
|
54
|
+
EXTERNAL: 'external',
|
|
55
|
+
};
|
|
53
56
|
/**
|
|
54
57
|
* Name suffix that marks an exported type in an `externalApiPaths` project as a vendor CONTRACT
|
|
55
58
|
* (`GmailApi`, `StorageApi`) rather than one of the DTOs, configs or clients sitting beside it.
|
|
@@ -64,20 +67,7 @@ const EXTERNAL_SYSTEM_TAG = 'externalSystem';
|
|
|
64
67
|
* `svcName` first, and a consumer's own `XxxClientConfig` follows the same shape.
|
|
65
68
|
*/
|
|
66
69
|
const CLIENT_CONFIG_SUFFIX = 'ClientConfig';
|
|
67
|
-
/**
|
|
68
|
-
* The module-scope `const NAME = '<string literal>'` bindings of ONE source file.
|
|
69
|
-
*
|
|
70
|
-
* A contract that hoists its route to a constant (`@ApiPath(WHATSAPP_API_PATH)`) is good practice —
|
|
71
|
-
* it lets a sibling contract and its callers share the symbol — but a decorator argument is read as
|
|
72
|
-
* TEXT here, with no checker to constant-fold it. Without this table such an argument resolved to
|
|
73
|
-
* nothing: the class lost its basePath, and a class whose every @Endpoint path was a constant
|
|
74
|
-
* resolved to zero methods and was dropped from the graph entirely.
|
|
75
|
-
*
|
|
76
|
-
* Deliberately SAME-MODULE only. Following an import would mean resolving modules, which is exactly
|
|
77
|
-
* what the source pre-pass avoids (it can be diverted to a decorator-erased `.d.ts`). A cross-module
|
|
78
|
-
* constant is therefore still unresolvable — and is REPORTED rather than silently dropped, see
|
|
79
|
-
* DecoratorArgDiagnostics.
|
|
80
|
-
*/
|
|
70
|
+
/** Same-module string constants that parser-only decorator scanning can safely resolve. */
|
|
81
71
|
class ModuleStringConstants {
|
|
82
72
|
byName;
|
|
83
73
|
constructor(byName) {
|
|
@@ -125,14 +115,7 @@ function stringValueOf(expr) {
|
|
|
125
115
|
return stringValueOf(expr.expression);
|
|
126
116
|
return null;
|
|
127
117
|
}
|
|
128
|
-
/**
|
|
129
|
-
* ONE decorator argument that had to be a string, and what came of it.
|
|
130
|
-
*
|
|
131
|
-
* `value` is the string when it was a literal or resolved through a same-module constant.
|
|
132
|
-
* `unresolvedName` is the argument as written (`WHATSAPP_API_PATH`) when it is present but could not
|
|
133
|
-
* be reduced — the case that must be reported, never silently dropped. Both are null when the
|
|
134
|
-
* argument is simply absent.
|
|
135
|
-
*/
|
|
118
|
+
/** A resolved decorator string, or the source spelling that could not be reduced. */
|
|
136
119
|
class DecoratorArgValue {
|
|
137
120
|
value;
|
|
138
121
|
unresolvedName;
|
|
@@ -158,26 +141,24 @@ function decoratorArgValue(expr, constants) {
|
|
|
158
141
|
}
|
|
159
142
|
return new DecoratorArgValue(null, expr.getText());
|
|
160
143
|
}
|
|
161
|
-
/**
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
* `recordEmptiedContract` are collected so generation can FAIL — one aggregated error naming every
|
|
173
|
-
* offender, because an author fixing five constants wants all five in one run.
|
|
174
|
-
*/
|
|
144
|
+
/** Resolve the enum-backed constants intentionally allowed in @Endpoint positional arguments. */
|
|
145
|
+
// webpieces-disable no-function-outside-class -- pure AST accessor for decorator source
|
|
146
|
+
function endpointSymbolArgValue(expr, constants) {
|
|
147
|
+
if (expr && ts.isIdentifier(expr)) {
|
|
148
|
+
const builtin = ENDPOINT_SYMBOLS[expr.text];
|
|
149
|
+
if (builtin !== undefined)
|
|
150
|
+
return new DecoratorArgValue(builtin, null);
|
|
151
|
+
}
|
|
152
|
+
return decoratorArgValue(expr, constants);
|
|
153
|
+
}
|
|
154
|
+
/** Aggregates parser-only scan losses so generation can fail loudly instead of dropping routes. */
|
|
175
155
|
class DecoratorArgDiagnostics {
|
|
176
156
|
workspaceRoot;
|
|
177
157
|
found = [];
|
|
178
158
|
unresolvedPaths = [];
|
|
179
159
|
emptied = [];
|
|
180
160
|
undeclaredCallers = [];
|
|
161
|
+
undeclaredOperations = [];
|
|
181
162
|
constructor(workspaceRoot) {
|
|
182
163
|
this.workspaceRoot = workspaceRoot;
|
|
183
164
|
}
|
|
@@ -197,6 +178,10 @@ class DecoratorArgDiagnostics {
|
|
|
197
178
|
recordUndeclaredCaller(api, method, argument, node) {
|
|
198
179
|
this.undeclaredCallers.push(new api_relations_1.UndeclaredExternalCaller(api, method, argument, this.locate(node)));
|
|
199
180
|
}
|
|
181
|
+
/** Record an `@Endpoint` whose required operation is missing or unreadable. */
|
|
182
|
+
recordUndeclaredOperation(api, method, argument, node) {
|
|
183
|
+
this.undeclaredOperations.push(new api_relations_1.UndeclaredEndpointOperation(api, method, argument, this.locate(node)));
|
|
184
|
+
}
|
|
200
185
|
all() {
|
|
201
186
|
return this.found;
|
|
202
187
|
}
|
|
@@ -209,6 +194,9 @@ class DecoratorArgDiagnostics {
|
|
|
209
194
|
undeclaredExternalCallers() {
|
|
210
195
|
return this.undeclaredCallers;
|
|
211
196
|
}
|
|
197
|
+
undeclaredEndpointOperations() {
|
|
198
|
+
return this.undeclaredOperations;
|
|
199
|
+
}
|
|
212
200
|
locate(node) {
|
|
213
201
|
const sourceFile = node.getSourceFile();
|
|
214
202
|
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
@@ -240,23 +228,7 @@ function apiTransport(cls) {
|
|
|
240
228
|
}
|
|
241
229
|
/** The @Endpoint kinds that are actually DELIVERED through a named queue or schedule. */
|
|
242
230
|
const QUEUED_KINDS = ['cloudtasks', 'cron'];
|
|
243
|
-
/**
|
|
244
|
-
* Every `@Endpoint(path, kind)` method on a contract class, in declaration order.
|
|
245
|
-
*
|
|
246
|
-
* `kind` is a REQUIRED argument of the decorator, so a missing/non-literal second argument means the
|
|
247
|
-
* source does not compile (or is mid-edit) — we skip the method rather than defaulting it. Defaulting
|
|
248
|
-
* would put an undeclared cron or webhook into the graph as an ordinary rpc call, which is precisely
|
|
249
|
-
* the blindness the required argument exists to remove.
|
|
250
|
-
*
|
|
251
|
-
* `path` is NOT skippable. It may be a same-module constant; an argument that is present but still
|
|
252
|
-
* cannot be reduced is recorded on `diagnostics` as an UnresolvedEndpointPath, which FAILS generation
|
|
253
|
-
* later. Upstream components need the URL — a client computes its request as `basePath + path` — so
|
|
254
|
-
* dropping the method here shipped a contract missing routing information, and a class whose every
|
|
255
|
-
* path was a constant lost every method and disappeared from the graph entirely.
|
|
256
|
-
*
|
|
257
|
-
* A class that declared endpoints and kept NONE of them is recorded too: `buildApiContracts` skips
|
|
258
|
-
* zero-method classes, which is the door a gutted contract used to leave through unannounced.
|
|
259
|
-
*/
|
|
231
|
+
/** Reads required `(method, path, operation, kind, options?)` endpoint declarations in order. */
|
|
260
232
|
// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts
|
|
261
233
|
function endpointMethodsOf(cls, api, constants = new ModuleStringConstants(new Map()), diagnostics = null) {
|
|
262
234
|
const methods = [];
|
|
@@ -270,41 +242,43 @@ function endpointMethodsOf(cls, api, constants = new ModuleStringConstants(new M
|
|
|
270
242
|
declared++;
|
|
271
243
|
const name = member.name.text;
|
|
272
244
|
const args = decoratorArgs(endpoint);
|
|
273
|
-
const
|
|
274
|
-
const
|
|
245
|
+
const methodArg = endpointSymbolArgValue(args[0], constants);
|
|
246
|
+
const pathArg = decoratorArgValue(args[1], constants);
|
|
247
|
+
const operationArg = endpointSymbolArgValue(args[2], constants);
|
|
248
|
+
const kindArg = endpointSymbolArgValue(args[3], constants);
|
|
249
|
+
reportUnresolved(diagnostics, api, 'Endpoint', name, methodArg, endpoint);
|
|
275
250
|
reportUnresolved(diagnostics, api, 'Endpoint', name, pathArg, endpoint);
|
|
251
|
+
reportUnresolved(diagnostics, api, 'Endpoint', name, operationArg, endpoint);
|
|
276
252
|
reportUnresolved(diagnostics, api, 'Endpoint', name, kindArg, endpoint);
|
|
277
253
|
if (diagnostics !== null && pathArg.unresolvedName !== null) {
|
|
278
254
|
diagnostics.recordUnresolvedPath(api, name, pathArg.unresolvedName, endpoint);
|
|
279
255
|
}
|
|
256
|
+
const httpMethod = methodArg.value;
|
|
257
|
+
const operation = endpointOperationOf(args[2], constants, diagnostics, api, name, endpoint);
|
|
280
258
|
const kind = kindArg.value;
|
|
281
|
-
if (
|
|
259
|
+
if ((httpMethod !== 'GET' && httpMethod !== 'POST') ||
|
|
260
|
+
pathArg.value === null ||
|
|
261
|
+
operation === null ||
|
|
282
262
|
kind === null ||
|
|
283
263
|
!ENDPOINT_KINDS.includes(kind))
|
|
284
264
|
continue;
|
|
285
|
-
const httpMethod = httpMethodOf(args[2], constants, diagnostics, api, name, endpoint);
|
|
286
265
|
const method = {
|
|
287
266
|
name,
|
|
288
267
|
path: pathArg.value,
|
|
289
268
|
kind: kind,
|
|
269
|
+
operation,
|
|
290
270
|
httpMethod,
|
|
291
271
|
};
|
|
292
272
|
const parameters = httpParametersOf(member, httpMethod, constants, diagnostics, api, name);
|
|
293
273
|
if (parameters.length > 0)
|
|
294
274
|
method.parameters = parameters;
|
|
295
|
-
if (endpointResponseTypeOf(args[
|
|
275
|
+
if (endpointResponseTypeOf(args[4], constants) === 'full')
|
|
296
276
|
method.responseType = 'full';
|
|
297
|
-
// Only a queued or scheduled endpoint HAS a queue. Naming one for a synchronous rpc invited a
|
|
298
|
-
// tool to read `methods.map(m => m.queueName)` as a provisioning list and create queues that
|
|
299
|
-
// nothing will ever deliver to.
|
|
300
277
|
if (QUEUED_KINDS.includes(method.kind)) {
|
|
301
278
|
method.queueName = queueNameOf(member, api, name, constants, diagnostics);
|
|
302
279
|
}
|
|
303
|
-
// Only an `external` endpoint HAS an outside caller, mirroring the queue rule above. A
|
|
304
|
-
// caller recorded on an rpc method would be a fact about nothing, and would put a vendor
|
|
305
|
-
// box on the graph beside an endpoint no vendor calls.
|
|
306
280
|
if (method.kind === 'external') {
|
|
307
|
-
const caller = externalCallerOf(args[
|
|
281
|
+
const caller = externalCallerOf(args[4], constants);
|
|
308
282
|
if (caller.declaration !== null)
|
|
309
283
|
method.caller = caller.declaration;
|
|
310
284
|
else if (diagnostics !== null)
|
|
@@ -312,21 +286,35 @@ function endpointMethodsOf(cls, api, constants = new ModuleStringConstants(new M
|
|
|
312
286
|
}
|
|
313
287
|
methods.push(method);
|
|
314
288
|
}
|
|
315
|
-
// Declared endpoints, kept none: the class is about to be skipped as "zero methods" and would
|
|
316
|
-
// leave no trace. Never legitimate — a routeless contract declares no @Endpoint at all.
|
|
317
289
|
if (diagnostics !== null && declared > 0 && methods.length === 0) {
|
|
318
290
|
diagnostics.recordEmptiedContract(api, declared, cls);
|
|
319
291
|
}
|
|
320
292
|
return methods;
|
|
321
293
|
}
|
|
322
|
-
/**
|
|
294
|
+
/** Required side-effect declaration; no inference from GET/POST is permitted. */
|
|
323
295
|
// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers
|
|
324
|
-
function
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
296
|
+
function endpointOperationOf(operationArgument, constants, diagnostics, api, method, node) {
|
|
297
|
+
const declared = endpointSymbolArgValue(operationArgument, constants);
|
|
298
|
+
if (declared.value === null) {
|
|
299
|
+
diagnostics?.recordUndeclaredOperation(api, method, declared.unresolvedName ?? '<missing>', node);
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
reportUnresolved(diagnostics, api, 'Endpoint.operation', method, declared, node);
|
|
303
|
+
if (declared.value !== null &&
|
|
304
|
+
ENDPOINT_OPERATIONS.includes(declared.value)) {
|
|
305
|
+
return declared.value;
|
|
306
|
+
}
|
|
307
|
+
diagnostics?.recordUndeclaredOperation(api, method, declared.unresolvedName ?? declared.value ?? '<missing>', node);
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
/** Required first positional HTTP method. */
|
|
311
|
+
// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers
|
|
312
|
+
function httpMethodOf(methodArgument, constants, diagnostics, api, method, node) {
|
|
313
|
+
const declared = endpointSymbolArgValue(methodArgument, constants);
|
|
328
314
|
reportUnresolved(diagnostics, api, 'Endpoint.httpMethod', method, declared, node);
|
|
329
|
-
|
|
315
|
+
if (declared.value === 'GET' || declared.value === 'POST')
|
|
316
|
+
return declared.value;
|
|
317
|
+
return null;
|
|
330
318
|
}
|
|
331
319
|
/** Only the non-default full-response marker needs an architecture field. */
|
|
332
320
|
// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers
|
|
@@ -362,10 +350,7 @@ function httpParametersOf(member, httpMethod, constants, diagnostics, api, metho
|
|
|
362
350
|
}
|
|
363
351
|
/** Default `callerKind` when an `external` endpoint declares `calledBy` alone — mirrors core-util. */
|
|
364
352
|
const DEFAULT_CALLER_KIND = 'saas';
|
|
365
|
-
/**
|
|
366
|
-
* The outcome of reading `@Endpoint(path, 'external', { calledBy, callerKind })`'s third argument:
|
|
367
|
-
* either the resolved declaration, or the reason it could not be resolved (never both).
|
|
368
|
-
*/
|
|
353
|
+
/** Resolved EXTERNAL caller declaration, or the source problem that prevented resolution. */
|
|
369
354
|
class ExternalCallerRead {
|
|
370
355
|
declaration;
|
|
371
356
|
problem;
|
|
@@ -377,15 +362,7 @@ class ExternalCallerRead {
|
|
|
377
362
|
}
|
|
378
363
|
}
|
|
379
364
|
exports.ExternalCallerRead = ExternalCallerRead;
|
|
380
|
-
/**
|
|
381
|
-
* Read the declared caller out of the @Endpoint OPTIONS OBJECT LITERAL — `args[2]`, not a positional
|
|
382
|
-
* argument, because that is where `formPost` already lives and one options bag beats two.
|
|
383
|
-
*
|
|
384
|
-
* Everything unreadable is a PROBLEM, never a default: an unknown `callerKind` draws the wrong shape
|
|
385
|
-
* (which teaches the reader something false), and a missing `calledBy` puts us back at a box that can
|
|
386
|
-
* only name our own contract. The kind default applies ONLY to the case the API deliberately allows —
|
|
387
|
-
* `calledBy` present, `callerKind` absent.
|
|
388
|
-
*/
|
|
365
|
+
/** Reads `calledBy` and optional `callerKind` from the fifth positional options argument. */
|
|
389
366
|
// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts
|
|
390
367
|
function externalCallerOf(arg, constants) {
|
|
391
368
|
if (arg === undefined)
|