@webpieces/nx-webpieces-rules 0.4.779 β 0.4.780
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/executors/generate/executor.d.ts +2 -1
- package/src/executors/generate/executor.js +16 -4
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/validate-architecture-unchanged/executor.d.ts +17 -0
- package/src/executors/validate-architecture-unchanged/executor.js +32 -25
- package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
- package/src/executors/validate-runtime-architecture/executor.js +32 -8
- package/src/executors/validate-runtime-architecture/executor.js.map +1 -1
- package/src/executors/visualize/executor.js +3 -1
- package/src/executors/visualize/executor.js.map +1 -1
- package/src/lib/api-contract-files.d.ts +54 -0
- package/src/lib/api-contract-files.js +138 -0
- package/src/lib/api-contract-files.js.map +1 -0
- package/src/lib/api-usage/api-contract-errors.d.ts +1 -1
- package/src/lib/api-usage/api-contract-errors.js +4 -4
- package/src/lib/api-usage/api-contract-errors.js.map +1 -1
- package/src/lib/api-usage/api-relations.d.ts +6 -5
- package/src/lib/api-usage/api-relations.js +1 -1
- package/src/lib/api-usage/api-relations.js.map +1 -1
- package/src/lib/api-usage/api-scanner.d.ts +2 -1
- package/src/lib/api-usage/api-scanner.js +2 -1
- package/src/lib/api-usage/api-scanner.js.map +1 -1
- package/src/lib/graph-loader.d.ts +18 -22
- package/src/lib/graph-loader.js +49 -31
- package/src/lib/graph-loader.js.map +1 -1
- package/src/lib/runtime-graph.js +4 -4
- package/src/lib/runtime-graph.js.map +1 -1
- package/src/lib/runtime-visualizer.js +1 -1
- package/src/lib/runtime-visualizer.js.map +1 -1
- package/src/plugin.js +1 -0
- package/src/plugin.js.map +1 -1
- package/src/runtime-targets.js +1 -0
- package/src/runtime-targets.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/visualize/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/visualize/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;AAgBH,8BA2CC;AAxDD,yDAA0D;AAC1D,iEAA6D;AAC7D,0DAAgF;AAChF,2CAAwC;AAUzB,KAAK,UAAU,WAAW,CACrC,OAAiC,EACjC,OAAwB;IAExB,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;IACpC,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAEnC,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;IAEjD,8DAA8D;IAC9D,IAAI,CAAC;QACD,uBAAuB;QACvB,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;QACzC,MAAM,SAAS,GAAG,IAAA,+BAAgB,EAAC,aAAa,EAAE,SAAS,CAAC,CAAC;QAE7D,IAAI,CAAC,SAAS,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,0DAA0D,CAAC,CAAC;YAC1E,OAAO,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;YAC5D,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAC9B,CAAC;QACD,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC;QAEjC,yBAAyB;QACzB,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;QAC9C,MAAM,UAAU,GAAG,IAAI,kCAAe,EAAE,CAAC;QACzC,MAAM,QAAQ,GAAG,UAAU,CAAC,kBAAkB,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;QACrE,OAAO,CAAC,GAAG,CAAC,gBAAgB,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;QAEjD,yBAAyB;QACzB,OAAO,CAAC,GAAG,CAAC,0CAA0C,CAAC,CAAC;QACxD,IAAI,UAAU,CAAC,iBAAiB,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAClD,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QACpC,CAAC;aAAM,CAAC;YACJ,OAAO,CAAC,GAAG,CAAC,2CAA2C,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;QAChF,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC7B,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,iBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,MAAM,QAAQ,GAAG,KAAK,YAAY,4BAAa,CAAC,CAAC,CAAC,IAAA,qCAAsB,EAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;QAChG,OAAO,CAAC,KAAK,CAAC,yBAAyB,EAAE,QAAQ,CAAC,CAAC;QACnD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC9B,CAAC;AACL,CAAC","sourcesContent":["/**\n * Visualize Executor\n *\n * Generates visual representations of the architecture graph (DOT + HTML)\n * and opens the visualization in a browser.\n *\n * Usage:\n * nx run architecture:visualize\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { loadBlessedGraph } from '../../lib/graph-loader';\nimport { GraphVisualizer } from '../../lib/graph-visualizer';\nimport { RuleFailError, renderRuleFailForHuman } from '@webpieces/rules-config';\nimport { toError } from '../../toError';\n\nexport interface VisualizeExecutorOptions {\n graphPath?: string;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\nexport default async function runExecutor(\n options: VisualizeExecutorOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const graphPath = options.graphPath;\n const workspaceRoot = context.root;\n\n console.log('\\nπ¨ Architecture Visualization\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // Load the saved graph\n console.log('π Loading saved graph...');\n const graphFile = loadBlessedGraph(workspaceRoot, graphPath);\n\n if (!graphFile) {\n console.error('β No saved graph found at architecture/dependencies.json');\n console.error(' Run: nx run architecture:generate first');\n return { success: false };\n }\n const graph = graphFile.projects;\n\n // Generate visualization\n console.log('π¨ Generating visualization...');\n const visualizer = new GraphVisualizer();\n const vizPaths = visualizer.writeVisualization(graph, workspaceRoot);\n console.log(`β
Generated: ${vizPaths.htmlPath}`);\n\n // Try to open in browser\n console.log('\\nπ Opening visualization in browser...');\n if (visualizer.openVisualization(vizPaths.htmlPath)) {\n console.log('β
Browser opened');\n } else {\n console.log(`β οΈ Could not auto-open. Open manually: ${vizPaths.htmlPath}`);\n }\n\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n const rendered = error instanceof RuleFailError ? renderRuleFailForHuman(error) : error.message;\n console.error('β Visualization failed:', rendered);\n return { success: false };\n }\n}\n"]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-API contract files: `architecture/apis/<ApiName>.json`.
|
|
3
|
+
*
|
|
4
|
+
* WHY these exist (issue #949): the full endpoint contract used to live inside
|
|
5
|
+
* architecture/dependencies.json under `apiContracts`, so adding ONE `@Endpoint` rewrote the project
|
|
6
|
+
* dependency graph and failed validate-architecture-unchanged until `architecture:generate` was rerun.
|
|
7
|
+
* dependencies.json is meant to change only when PROJECT dependencies change. Each API's contract now
|
|
8
|
+
* lives in its own generated file, and dependencies.json only LINKS to it (`apiContractFiles`), so it
|
|
9
|
+
* changes when an API is added or removed β never when an endpoint is.
|
|
10
|
+
*
|
|
11
|
+
* These files are written by `architecture:generate` and deliberately NOT drift-gated: nothing
|
|
12
|
+
* compares them to source. The runtime graph (generate, validate-runtime-architecture) reads its
|
|
13
|
+
* queue/trigger data from them.
|
|
14
|
+
*/
|
|
15
|
+
import type { ApiContracts } from './api-usage/api-relations';
|
|
16
|
+
/** The directory, relative to dependencies.json's own directory, holding one file per API. */
|
|
17
|
+
export declare const API_CONTRACTS_DIR = "apis";
|
|
18
|
+
/** The rule name every contract-file failure is reported under. */
|
|
19
|
+
export declare const API_CONTRACT_FILES_RULE = "validate-architecture-unchanged";
|
|
20
|
+
/** apiClassName -> path of its contract file, RELATIVE to dependencies.json's directory. */
|
|
21
|
+
export type ApiContractFileRefs = Record<string, string>;
|
|
22
|
+
/**
|
|
23
|
+
* What one `architecture:generate` pass did to the `apis/` directory, so the executor can say so.
|
|
24
|
+
*/
|
|
25
|
+
export declare class ApiContractFilesWriteResult {
|
|
26
|
+
readonly written: string[];
|
|
27
|
+
readonly deleted: string[];
|
|
28
|
+
constructor(written: string[], deleted: string[]);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Reads and writes the per-API contract files. All paths are resolved against the directory that
|
|
32
|
+
* holds dependencies.json (`graphPath`), so a non-default graphPath keeps its contracts beside it.
|
|
33
|
+
*/
|
|
34
|
+
export declare class ApiContractFiles {
|
|
35
|
+
/** The link dependencies.json carries for one API: `apis/<ApiName>.json`. */
|
|
36
|
+
refFor(api: string): string;
|
|
37
|
+
/** The link table for every contract, in the contracts' (already sorted) order. */
|
|
38
|
+
refsFor(contracts: ApiContracts): ApiContractFileRefs;
|
|
39
|
+
/**
|
|
40
|
+
* Write one file per contract and DELETE every `apis/*.json` whose API no longer exists, so a
|
|
41
|
+
* removed API never leaves a stale contract behind for the runtime graph to read.
|
|
42
|
+
*/
|
|
43
|
+
write(workspaceRoot: string, graphPath: string, contracts: ApiContracts): ApiContractFilesWriteResult;
|
|
44
|
+
/**
|
|
45
|
+
* Load every contract dependencies.json links to. A missing or unreadable file FAILS: the runtime
|
|
46
|
+
* graph would otherwise silently lose that API's queues and triggers.
|
|
47
|
+
*/
|
|
48
|
+
load(workspaceRoot: string, graphPath: string, refs: ApiContractFileRefs): ApiContracts;
|
|
49
|
+
private loadOne;
|
|
50
|
+
/** Pretty, deterministic JSON: the api name first, then the contract in its declared field order. */
|
|
51
|
+
private format;
|
|
52
|
+
private dirFor;
|
|
53
|
+
private deleteStale;
|
|
54
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Per-API contract files: `architecture/apis/<ApiName>.json`.
|
|
4
|
+
*
|
|
5
|
+
* WHY these exist (issue #949): the full endpoint contract used to live inside
|
|
6
|
+
* architecture/dependencies.json under `apiContracts`, so adding ONE `@Endpoint` rewrote the project
|
|
7
|
+
* dependency graph and failed validate-architecture-unchanged until `architecture:generate` was rerun.
|
|
8
|
+
* dependencies.json is meant to change only when PROJECT dependencies change. Each API's contract now
|
|
9
|
+
* lives in its own generated file, and dependencies.json only LINKS to it (`apiContractFiles`), so it
|
|
10
|
+
* changes when an API is added or removed β never when an endpoint is.
|
|
11
|
+
*
|
|
12
|
+
* These files are written by `architecture:generate` and deliberately NOT drift-gated: nothing
|
|
13
|
+
* compares them to source. The runtime graph (generate, validate-runtime-architecture) reads its
|
|
14
|
+
* queue/trigger data from them.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.ApiContractFiles = exports.ApiContractFilesWriteResult = exports.API_CONTRACT_FILES_RULE = exports.API_CONTRACTS_DIR = void 0;
|
|
18
|
+
const tslib_1 = require("tslib");
|
|
19
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
20
|
+
const path = tslib_1.__importStar(require("path"));
|
|
21
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
22
|
+
const toError_1 = require("../toError");
|
|
23
|
+
/** The directory, relative to dependencies.json's own directory, holding one file per API. */
|
|
24
|
+
exports.API_CONTRACTS_DIR = 'apis';
|
|
25
|
+
/** The rule name every contract-file failure is reported under. */
|
|
26
|
+
exports.API_CONTRACT_FILES_RULE = 'validate-architecture-unchanged';
|
|
27
|
+
/**
|
|
28
|
+
* What one `architecture:generate` pass did to the `apis/` directory, so the executor can say so.
|
|
29
|
+
*/
|
|
30
|
+
class ApiContractFilesWriteResult {
|
|
31
|
+
written;
|
|
32
|
+
deleted;
|
|
33
|
+
constructor(written, deleted) {
|
|
34
|
+
this.written = written;
|
|
35
|
+
this.deleted = deleted;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.ApiContractFilesWriteResult = ApiContractFilesWriteResult;
|
|
39
|
+
/**
|
|
40
|
+
* Reads and writes the per-API contract files. All paths are resolved against the directory that
|
|
41
|
+
* holds dependencies.json (`graphPath`), so a non-default graphPath keeps its contracts beside it.
|
|
42
|
+
*/
|
|
43
|
+
class ApiContractFiles {
|
|
44
|
+
/** The link dependencies.json carries for one API: `apis/<ApiName>.json`. */
|
|
45
|
+
refFor(api) {
|
|
46
|
+
return `${exports.API_CONTRACTS_DIR}/${api}.json`;
|
|
47
|
+
}
|
|
48
|
+
/** The link table for every contract, in the contracts' (already sorted) order. */
|
|
49
|
+
refsFor(contracts) {
|
|
50
|
+
const refs = {};
|
|
51
|
+
for (const api of Object.keys(contracts).sort())
|
|
52
|
+
refs[api] = this.refFor(api);
|
|
53
|
+
return refs;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Write one file per contract and DELETE every `apis/*.json` whose API no longer exists, so a
|
|
57
|
+
* removed API never leaves a stale contract behind for the runtime graph to read.
|
|
58
|
+
*/
|
|
59
|
+
write(workspaceRoot, graphPath, contracts) {
|
|
60
|
+
const dir = this.dirFor(workspaceRoot, graphPath);
|
|
61
|
+
const written = [];
|
|
62
|
+
const apis = Object.keys(contracts).sort();
|
|
63
|
+
if (apis.length > 0)
|
|
64
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
65
|
+
for (const api of apis) {
|
|
66
|
+
const fullPath = path.join(dir, `${api}.json`);
|
|
67
|
+
fs.writeFileSync(fullPath, this.format(api, contracts[api]), 'utf-8');
|
|
68
|
+
written.push(fullPath);
|
|
69
|
+
}
|
|
70
|
+
const deleted = this.deleteStale(dir, new Set(apis));
|
|
71
|
+
return new ApiContractFilesWriteResult(written, deleted);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Load every contract dependencies.json links to. A missing or unreadable file FAILS: the runtime
|
|
75
|
+
* graph would otherwise silently lose that API's queues and triggers.
|
|
76
|
+
*/
|
|
77
|
+
load(workspaceRoot, graphPath, refs) {
|
|
78
|
+
const baseDir = path.dirname(path.join(workspaceRoot, graphPath));
|
|
79
|
+
const contracts = {};
|
|
80
|
+
for (const api of Object.keys(refs).sort()) {
|
|
81
|
+
contracts[api] = this.loadOne(api, path.join(baseDir, refs[api]));
|
|
82
|
+
}
|
|
83
|
+
return contracts;
|
|
84
|
+
}
|
|
85
|
+
loadOne(api, fullPath) {
|
|
86
|
+
if (!fs.existsSync(fullPath)) {
|
|
87
|
+
throw new rules_config_1.RuleFailError(exports.API_CONTRACT_FILES_RULE, `dependencies.json links ${api} to ${fullPath}, but that file does not exist.`, undefined, undefined, [new rules_config_1.Option('Regenerate the architecture files: pnpm nx run architecture:generate', true)]);
|
|
88
|
+
}
|
|
89
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
|
|
92
|
+
const contract = {
|
|
93
|
+
owner: parsed.owner,
|
|
94
|
+
apiKind: parsed.apiKind,
|
|
95
|
+
basePath: parsed.basePath,
|
|
96
|
+
methods: parsed.methods,
|
|
97
|
+
};
|
|
98
|
+
return contract;
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
const error = (0, toError_1.toError)(err);
|
|
102
|
+
throw new rules_config_1.RuleFailError(exports.API_CONTRACT_FILES_RULE, `Could not read the ${api} contract file ${fullPath}: ${error.message}`, undefined, undefined, [new rules_config_1.Option('Regenerate the architecture files: pnpm nx run architecture:generate', true)], undefined, error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Pretty, deterministic JSON: the api name first, then the contract in its declared field order. */
|
|
106
|
+
format(api, contract) {
|
|
107
|
+
const file = {
|
|
108
|
+
api,
|
|
109
|
+
owner: contract.owner,
|
|
110
|
+
apiKind: contract.apiKind,
|
|
111
|
+
basePath: contract.basePath,
|
|
112
|
+
methods: contract.methods,
|
|
113
|
+
};
|
|
114
|
+
return JSON.stringify(file, null, 4) + '\n';
|
|
115
|
+
}
|
|
116
|
+
dirFor(workspaceRoot, graphPath) {
|
|
117
|
+
return path.join(path.dirname(path.join(workspaceRoot, graphPath)), exports.API_CONTRACTS_DIR);
|
|
118
|
+
}
|
|
119
|
+
deleteStale(dir, keep) {
|
|
120
|
+
if (!fs.existsSync(dir))
|
|
121
|
+
return [];
|
|
122
|
+
const deleted = [];
|
|
123
|
+
for (const name of fs.readdirSync(dir).sort()) {
|
|
124
|
+
if (!name.endsWith('.json'))
|
|
125
|
+
continue;
|
|
126
|
+
if (keep.has(name.slice(0, -'.json'.length)))
|
|
127
|
+
continue;
|
|
128
|
+
const fullPath = path.join(dir, name);
|
|
129
|
+
fs.rmSync(fullPath);
|
|
130
|
+
deleted.push(fullPath);
|
|
131
|
+
}
|
|
132
|
+
if (fs.readdirSync(dir).length === 0)
|
|
133
|
+
fs.rmdirSync(dir);
|
|
134
|
+
return deleted;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
exports.ApiContractFiles = ApiContractFiles;
|
|
138
|
+
//# sourceMappingURL=api-contract-files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-contract-files.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-contract-files.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Per-API contract files: `architecture/apis/<ApiName>.json`.\n *\n * WHY these exist (issue #949): the full endpoint contract used to live inside\n * architecture/dependencies.json under `apiContracts`, so adding ONE `@Endpoint` rewrote the project\n * dependency graph and failed validate-architecture-unchanged until `architecture:generate` was rerun.\n * dependencies.json is meant to change only when PROJECT dependencies change. Each API's contract now\n * lives in its own generated file, and dependencies.json only LINKS to it (`apiContractFiles`), so it\n * changes when an API is added or removed β never when an endpoint is.\n *\n * These files are written by `architecture:generate` and deliberately NOT drift-gated: nothing\n * compares them to source. The runtime graph (generate, validate-runtime-architecture) reads its\n * queue/trigger data from them.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { RuleFailError, Option } from '@webpieces/rules-config';\nimport type { ApiContract, ApiContracts } from './api-usage/api-relations';\nimport { toError } from '../toError';\n\n/** The directory, relative to dependencies.json's own directory, holding one file per API. */\nexport const API_CONTRACTS_DIR = 'apis';\n\n/** The rule name every contract-file failure is reported under. */\nexport const API_CONTRACT_FILES_RULE = 'validate-architecture-unchanged';\n\n/** apiClassName -> path of its contract file, RELATIVE to dependencies.json's directory. */\nexport type ApiContractFileRefs = Record<string, string>;\n\n/**\n * What one `architecture:generate` pass did to the `apis/` directory, so the executor can say so.\n */\nexport class ApiContractFilesWriteResult {\n constructor(\n public readonly written: string[],\n public readonly deleted: string[],\n ) {}\n}\n\n/**\n * Reads and writes the per-API contract files. All paths are resolved against the directory that\n * holds dependencies.json (`graphPath`), so a non-default graphPath keeps its contracts beside it.\n */\nexport class ApiContractFiles {\n /** The link dependencies.json carries for one API: `apis/<ApiName>.json`. */\n refFor(api: string): string {\n return `${API_CONTRACTS_DIR}/${api}.json`;\n }\n\n /** The link table for every contract, in the contracts' (already sorted) order. */\n refsFor(contracts: ApiContracts): ApiContractFileRefs {\n const refs: ApiContractFileRefs = {};\n for (const api of Object.keys(contracts).sort()) refs[api] = this.refFor(api);\n return refs;\n }\n\n /**\n * Write one file per contract and DELETE every `apis/*.json` whose API no longer exists, so a\n * removed API never leaves a stale contract behind for the runtime graph to read.\n */\n write(workspaceRoot: string, graphPath: string, contracts: ApiContracts): ApiContractFilesWriteResult {\n const dir = this.dirFor(workspaceRoot, graphPath);\n const written: string[] = [];\n const apis = Object.keys(contracts).sort();\n if (apis.length > 0) fs.mkdirSync(dir, { recursive: true });\n for (const api of apis) {\n const fullPath = path.join(dir, `${api}.json`);\n fs.writeFileSync(fullPath, this.format(api, contracts[api]), 'utf-8');\n written.push(fullPath);\n }\n const deleted = this.deleteStale(dir, new Set(apis));\n return new ApiContractFilesWriteResult(written, deleted);\n }\n\n /**\n * Load every contract dependencies.json links to. A missing or unreadable file FAILS: the runtime\n * graph would otherwise silently lose that API's queues and triggers.\n */\n load(workspaceRoot: string, graphPath: string, refs: ApiContractFileRefs): ApiContracts {\n const baseDir = path.dirname(path.join(workspaceRoot, graphPath));\n const contracts: ApiContracts = {};\n for (const api of Object.keys(refs).sort()) {\n contracts[api] = this.loadOne(api, path.join(baseDir, refs[api]));\n }\n return contracts;\n }\n\n private loadOne(api: string, fullPath: string): ApiContract {\n if (!fs.existsSync(fullPath)) {\n throw new RuleFailError(\n API_CONTRACT_FILES_RULE,\n `dependencies.json links ${api} to ${fullPath}, but that file does not exist.`,\n undefined,\n undefined,\n [new Option('Regenerate the architecture files: pnpm nx run architecture:generate', true)],\n );\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const parsed = JSON.parse(fs.readFileSync(fullPath, 'utf-8')) as ApiContractFileJson;\n const contract: ApiContract = {\n owner: parsed.owner,\n apiKind: parsed.apiKind,\n basePath: parsed.basePath,\n methods: parsed.methods,\n };\n return contract;\n } catch (err: unknown) {\n const error = toError(err);\n throw new RuleFailError(\n API_CONTRACT_FILES_RULE,\n `Could not read the ${api} contract file ${fullPath}: ${error.message}`,\n undefined,\n undefined,\n [new Option('Regenerate the architecture files: pnpm nx run architecture:generate', true)],\n undefined,\n error,\n );\n }\n }\n\n /** Pretty, deterministic JSON: the api name first, then the contract in its declared field order. */\n private format(api: string, contract: ApiContract): string {\n const file: ApiContractFileJson = {\n api,\n owner: contract.owner,\n apiKind: contract.apiKind,\n basePath: contract.basePath,\n methods: contract.methods,\n };\n return JSON.stringify(file, null, 4) + '\\n';\n }\n\n private dirFor(workspaceRoot: string, graphPath: string): string {\n return path.join(path.dirname(path.join(workspaceRoot, graphPath)), API_CONTRACTS_DIR);\n }\n\n private deleteStale(dir: string, keep: Set<string>): string[] {\n if (!fs.existsSync(dir)) return [];\n const deleted: string[] = [];\n for (const name of fs.readdirSync(dir).sort()) {\n if (!name.endsWith('.json')) continue;\n if (keep.has(name.slice(0, -'.json'.length))) continue;\n const fullPath = path.join(dir, name);\n fs.rmSync(fullPath);\n deleted.push(fullPath);\n }\n if (fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);\n return deleted;\n }\n}\n\n/** The on-disk shape of one `apis/<ApiName>.json`. Describes foreign JSON we narrow on read. */\ninterface ApiContractFileJson extends ApiContract {\n api: string;\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The three ways `buildApiContracts` refuses to emit a green, wrong
|
|
2
|
+
* The three ways `buildApiContracts` refuses to emit a green, wrong api contract table.
|
|
3
3
|
*
|
|
4
4
|
* All three share one rule: an entry that is PRESENT but incomplete is worse than an absent one.
|
|
5
5
|
* Every other entry in the table is complete, so a consumer has no reason to suspect the one that
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* The three ways `buildApiContracts` refuses to emit a green, wrong
|
|
3
|
+
* The three ways `buildApiContracts` refuses to emit a green, wrong api contract table.
|
|
4
4
|
*
|
|
5
5
|
* All three share one rule: an entry that is PRESENT but incomplete is worse than an absent one.
|
|
6
6
|
* Every other entry in the table is complete, so a consumer has no reason to suspect the one that
|
|
@@ -22,7 +22,7 @@ class MissingBasePathError extends Error {
|
|
|
22
22
|
constructor(contracts) {
|
|
23
23
|
super(`${contracts.length} API contract(s) have @Endpoint methods but no readable @ApiPath basePath:\n` +
|
|
24
24
|
contracts.map((c) => ` β’ ${c}`).join('\n') +
|
|
25
|
-
`\n basePath is REQUIRED in
|
|
25
|
+
`\n basePath is REQUIRED in every api contract β an entry without it makes every consumer\n` +
|
|
26
26
|
` compute basePath + path as just path, silently. Inline the @ApiPath string literal,\n` +
|
|
27
27
|
` or move the constant into the same module as the contract class.`);
|
|
28
28
|
this.contracts = contracts;
|
|
@@ -44,7 +44,7 @@ class UnresolvedEndpointPathError extends Error {
|
|
|
44
44
|
paths
|
|
45
45
|
.map((p) => ` β’ ${p.api}.${p.method} β @Endpoint(${p.argument}, ...) at ${p.at}`)
|
|
46
46
|
.join('\n') +
|
|
47
|
-
`\n path is REQUIRED in
|
|
47
|
+
`\n path is REQUIRED in every api contract β every consumer builds its request URL as\n` +
|
|
48
48
|
` basePath + path, so an unreadable path is MISSING ROUTING, not cosmetic metadata,\n` +
|
|
49
49
|
` and a class whose every path is unreadable drops out of the graph entirely.\n` +
|
|
50
50
|
` Inline the @Endpoint string literal, or move the constant into the SAME module as\n` +
|
|
@@ -99,7 +99,7 @@ class EmptiedApiContractError extends Error {
|
|
|
99
99
|
contracts
|
|
100
100
|
.map((c) => ` β’ ${c.api} β ${c.declared} @Endpoint method(s) declared, 0 usable, at ${c.at}`)
|
|
101
101
|
.join('\n') +
|
|
102
|
-
`\n A contract with zero usable methods is DROPPED from
|
|
102
|
+
`\n A contract with zero usable methods is DROPPED from the api contracts, so the class,\n` +
|
|
103
103
|
` its queues and its triggers disappear from the architecture graph with no error.\n` +
|
|
104
104
|
` Both @Endpoint arguments must be readable: the path as a string literal or a\n` +
|
|
105
105
|
` SAME-module const, and the kind as a literal 'rpc' | 'cloudtasks' | 'cron' |\n` +
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-contract-errors.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-contract-errors.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;AAIH;;;GAGG;AACH,MAAa,oBAAqB,SAAQ,KAAK;IACf;IAA5B,YAA4B,SAA4B;QACpD,KAAK,CACD,GAAG,SAAS,CAAC,MAAM,8EAA8E;YAC7F,SAAS,CAAC,GAAG,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;YACtD,
|
|
1
|
+
{"version":3,"file":"api-contract-errors.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-contract-errors.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;AAIH;;;GAGG;AACH,MAAa,oBAAqB,SAAQ,KAAK;IACf;IAA5B,YAA4B,SAA4B;QACpD,KAAK,CACD,GAAG,SAAS,CAAC,MAAM,8EAA8E;YAC7F,SAAS,CAAC,GAAG,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;YACtD,8FAA8F;YAC9F,0FAA0F;YAC1F,qEAAqE,CAC5E,CAAC;QAPsB,cAAS,GAAT,SAAS,CAAmB;QAQpD,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACvC,CAAC;CACJ;AAXD,oDAWC;AAED;;;;;;GAMG;AACH,MAAa,2BAA4B,SAAQ,KAAK;IACtB;IAA5B,YAA4B,KAAwC;QAChE,KAAK,CACD,GAAG,KAAK,CAAC,MAAM,qDAAqD;YAChE,KAAK;iBACA,GAAG,CACA,CAAC,CAAyB,EAAE,EAAE,CAC1B,UAAU,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,gBAAgB,CAAC,CAAC,QAAQ,aAAa,CAAC,CAAC,EAAE,EAAE,CAC/E;iBACA,IAAI,CAAC,IAAI,CAAC;YACf,0FAA0F;YAC1F,wFAAwF;YACxF,kFAAkF;YAClF,wFAAwF;YACxF,sFAAsF;YACtF,yFAAyF;YACzF,6BAA6B,CACpC,CAAC;QAhBsB,UAAK,GAAL,KAAK,CAAmC;QAiBhE,IAAI,CAAC,IAAI,GAAG,6BAA6B,CAAC;IAC9C,CAAC;CACJ;AApBD,kEAoBC;AAED;;;;;;;;;GASG;AACH,MAAa,6BAA8B,SAAQ,KAAK;IACxB;IAA5B,YAA4B,OAA4C;QACpE,KAAK,CACD,GAAG,OAAO,CAAC,MAAM,2DAA2D;YACxE,OAAO;iBACF,GAAG,CACA,CAAC,CAA2B,EAAE,EAAE,CAC5B,UAAU,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,MAAM,CAAC,CAAC,QAAQ,OAAO,CAAC,CAAC,EAAE,EAAE,CAC/D;iBACA,IAAI,CAAC,IAAI,CAAC;YACf,wFAAwF;YACxF,uEAAuE;YACvE,+DAA+D;YAC/D,yFAAyF;YACzF,sEAAsE;YACtE,0FAA0F;YAC1F,qFAAqF,CAC5F,CAAC;QAhBsB,YAAO,GAAP,OAAO,CAAqC;QAiBpE,IAAI,CAAC,IAAI,GAAG,+BAA+B,CAAC;IAChD,CAAC;CACJ;AApBD,sEAoBC;AAED;;;;;;GAMG;AACH,MAAa,uBAAwB,SAAQ,KAAK;IAClB;IAA5B,YAA4B,SAAwC;QAChE,KAAK,CACD,GAAG,SAAS,CAAC,MAAM,4EAA4E;YAC3F,SAAS;iBACJ,GAAG,CACA,CAAC,CAAqB,EAAE,EAAE,CACtB,UAAU,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,QAAQ,+CAA+C,CAAC,CAAC,EAAE,EAAE,CAC3F;iBACA,IAAI,CAAC,IAAI,CAAC;YACf,6FAA6F;YAC7F,uFAAuF;YACvF,mFAAmF;YACnF,mFAAmF;YACnF,qFAAqF;YACrF,mCAAmC,CAC1C,CAAC;QAfsB,cAAS,GAAT,SAAS,CAA+B;QAgBhE,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IAC1C,CAAC;CACJ;AAnBD,0DAmBC","sourcesContent":["/**\n * The three ways `buildApiContracts` refuses to emit a green, wrong api contract table.\n *\n * All three share one rule: an entry that is PRESENT but incomplete is worse than an absent one.\n * Every other entry in the table is complete, so a consumer has no reason to suspect the one that\n * lost a field β it just computes a confidently wrong URL, or draws a service with no queues.\n *\n * Each aggregates EVERY offender into one message rather than throwing on the first: an author who\n * moved a constants module broke five decorators at once and wants all five named in one run.\n *\n * Split out of api-scanner.ts, which owns the scan itself and is at its file-size limit.\n */\n\nimport { EmptiedApiContract, UndeclaredExternalCaller, UnresolvedEndpointPath } from './api-relations';\n\n/**\n * A routed contract whose `@ApiPath` argument the scan could not read. Fatal on purpose: shipping the\n * entry without its basePath is what made `/whatsapp/test` render as `/test` in a downstream runbook.\n */\nexport class MissingBasePathError extends Error {\n constructor(public readonly contracts: readonly string[]) {\n super(\n `${contracts.length} API contract(s) have @Endpoint methods but no readable @ApiPath basePath:\\n` +\n contracts.map((c: string) => ` β’ ${c}`).join('\\n') +\n `\\n basePath is REQUIRED in every api contract β an entry without it makes every consumer\\n` +\n ` compute basePath + path as just path, silently. Inline the @ApiPath string literal,\\n` +\n ` or move the constant into the same module as the contract class.`,\n );\n this.name = 'MissingBasePathError';\n }\n}\n\n/**\n * `@Endpoint` paths the scan could not read. Fatal for the same reason MissingBasePathError is: the\n * two arguments are the two halves of ONE url. An http client builds its request as\n * `basePath + path`, so a contract shipped without a method's path is missing routing information,\n * and the consumer computes a confidently wrong URL. Skipping the method instead was worse still β\n * a class whose every path was an unreadable constant lost every method and vanished from the graph.\n */\nexport class UnresolvedEndpointPathError extends Error {\n constructor(public readonly paths: readonly UnresolvedEndpointPath[]) {\n super(\n `${paths.length} @Endpoint path(s) could not be read as a string:\\n` +\n paths\n .map(\n (p: UnresolvedEndpointPath) =>\n ` β’ ${p.api}.${p.method} β @Endpoint(${p.argument}, ...) at ${p.at}`,\n )\n .join('\\n') +\n `\\n path is REQUIRED in every api contract β every consumer builds its request URL as\\n` +\n ` basePath + path, so an unreadable path is MISSING ROUTING, not cosmetic metadata,\\n` +\n ` and a class whose every path is unreadable drops out of the graph entirely.\\n` +\n ` Inline the @Endpoint string literal, or move the constant into the SAME module as\\n` +\n ` the contract class β a same-module const IS resolved, one imported from another\\n` +\n ` module is NOT (this scan is parser-only by design: module resolution can land on a\\n` +\n ` decorator-erased .d.ts).`,\n );\n this.name = 'UnresolvedEndpointPathError';\n }\n}\n\n/**\n * `external` endpoints whose CALLER the scan could not read. Fatal, like the two above, because the\n * alternative is a diagram that lies by omission: the inbound box exists solely to name the system\n * calling us from outside, and with nothing to name it falls back to restating our own contract\n * name β which the reader already sees on the service box the arrow points at.\n *\n * `@Endpoint`'s TS overloads make `calledBy` a compile error to omit, so a scan reaching here saw a\n * JS caller, an `as any`, a cross-module constant this parser-only pass cannot fold, or a\n * `callerKind` that is not one of the declared kinds.\n */\nexport class UndeclaredExternalCallerError extends Error {\n constructor(public readonly callers: readonly UndeclaredExternalCaller[]) {\n super(\n `${callers.length} 'external' @Endpoint(s) do not declare WHO calls them:\\n` +\n callers\n .map(\n (c: UndeclaredExternalCaller) =>\n ` β’ ${c.api}.${c.method} β ${c.argument} at ${c.at}`,\n )\n .join('\\n') +\n `\\n An 'external' endpoint is driven by a system OUTSIDE this repo, and the runtime\\n` +\n ` architecture graph draws that system as an inbound box. Name it:\\n` +\n ` @Endpoint('/hook', 'external', { calledBy: 'twilio' })\\n` +\n ` Add callerKind for anything that is not a vendor SaaS β database | cache | queue |\\n` +\n ` storage | saas | system β e.g. a GCP Pub/Sub push subscription:\\n` +\n ` @Endpoint('/push', 'external', { calledBy: 'pubsub-push', callerKind: 'system' })\\n` +\n ` Use a string LITERAL or a SAME-module const: this scan is parser-only by design.`,\n );\n this.name = 'UndeclaredExternalCallerError';\n }\n}\n\n/**\n * Contract classes that declared `@Endpoint` methods and kept none of them. Fatal because the\n * alternative is the silent drop the api scan exists to close: buildApiContracts legitimately skips\n * a zero-method class (a vendor seam has no routes), and a class gutted by unreadable decorator\n * arguments used the very same exit β which is how a service lost two real Cloud Tasks queues and an\n * inbound webhook without a single line of output.\n */\nexport class EmptiedApiContractError extends Error {\n constructor(public readonly contracts: readonly EmptiedApiContract[]) {\n super(\n `${contracts.length} API contract class(es) declare @Endpoint methods but kept NONE of them:\\n` +\n contracts\n .map(\n (c: EmptiedApiContract) =>\n ` β’ ${c.api} β ${c.declared} @Endpoint method(s) declared, 0 usable, at ${c.at}`,\n )\n .join('\\n') +\n `\\n A contract with zero usable methods is DROPPED from the api contracts, so the class,\\n` +\n ` its queues and its triggers disappear from the architecture graph with no error.\\n` +\n ` Both @Endpoint arguments must be readable: the path as a string literal or a\\n` +\n ` SAME-module const, and the kind as a literal 'rpc' | 'cloudtasks' | 'cron' |\\n` +\n ` 'external'. Fix the arguments above, or remove the @Endpoint decorators if the\\n` +\n ` class is genuinely not routed.`,\n );\n this.name = 'EmptiedApiContractError';\n }\n}\n"]}
|
|
@@ -190,10 +190,11 @@ export interface ExternalSystemDeclaration {
|
|
|
190
190
|
label: string;
|
|
191
191
|
}
|
|
192
192
|
/**
|
|
193
|
-
* The committed, per-contract view written to `architecture/
|
|
193
|
+
* The committed, per-contract view written to `architecture/apis/<ApiName>.json` (one file per API;
|
|
194
|
+
* dependencies.json only links to it under `apiContractFiles` β see ApiContractFiles).
|
|
194
195
|
*
|
|
195
|
-
* The runtime graph is derived
|
|
196
|
-
* diverge β which means anything the runtime graph needs must be COMMITTED
|
|
196
|
+
* The runtime graph is derived from the committed files so generate and validate can never
|
|
197
|
+
* diverge β which means anything the runtime graph needs must be COMMITTED, not re-scanned.
|
|
197
198
|
* Per-method trigger kinds and queue names are exactly that: without this table the derivation
|
|
198
199
|
* cannot tell a queued endpoint from a cron sweep, and cannot name the queue between two services.
|
|
199
200
|
*/
|
|
@@ -211,7 +212,7 @@ export interface ApiContract {
|
|
|
211
212
|
basePath: string;
|
|
212
213
|
methods: ApiMethodMeta[];
|
|
213
214
|
}
|
|
214
|
-
/** apiClassName -> its committed contract. Serialized as
|
|
215
|
+
/** apiClassName -> its committed contract. Serialized as one `architecture/apis/<ApiName>.json` each. */
|
|
215
216
|
export type ApiContracts = Record<string, ApiContract>;
|
|
216
217
|
/**
|
|
217
218
|
* ONE decorator argument the scan saw but could not reduce to a string β `@ApiPath(SOME_CONST)`
|
|
@@ -252,7 +253,7 @@ export declare class NonLiteralDecoratorArg {
|
|
|
252
253
|
* this one is FATAL. Upstream components need the URL: an http client builds its request as
|
|
253
254
|
* `basePath + path`, so an unreadable path is missing ROUTING, not missing metadata β the same
|
|
254
255
|
* reasoning that already makes basePath required. Skipping the method instead used to delete it, and
|
|
255
|
-
* a class whose every path was a constant lost every method and vanished from
|
|
256
|
+
* a class whose every path was a constant lost every method and vanished from the contract table with
|
|
256
257
|
* nothing printed anywhere.
|
|
257
258
|
*/
|
|
258
259
|
export declare class UnresolvedEndpointPath {
|
|
@@ -104,7 +104,7 @@ exports.NonLiteralDecoratorArg = NonLiteralDecoratorArg;
|
|
|
104
104
|
* this one is FATAL. Upstream components need the URL: an http client builds its request as
|
|
105
105
|
* `basePath + path`, so an unreadable path is missing ROUTING, not missing metadata β the same
|
|
106
106
|
* reasoning that already makes basePath required. Skipping the method instead used to delete it, and
|
|
107
|
-
* a class whose every path was a constant lost every method and vanished from
|
|
107
|
+
* a class whose every path was a constant lost every method and vanished from the contract table with
|
|
108
108
|
* nothing printed anywhere.
|
|
109
109
|
*/
|
|
110
110
|
class UnresolvedEndpointPath {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-relations.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-relations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Relations model\n *\n * The typed classification of a compile-time dependency edge P -> apiLib in\n * architecture/dependencies.json. Where the flat `dependsOn` only says \"P depends\n * on apiLib\", `apiRelations[apiLib]` says WHY: which API contracts P IMPLEMENTS\n * (serves, `class Ctrl extends XxxApi`) and which it USES (calls as a client,\n * `factory.createRpcClient(XxxApi, ...)` / `createPubSubClient(...)`), each tagged\n * with its transport.\n *\n * Interfaces + object literals here mirror the sibling runtime-graph.ts model β\n * these are serialization DTOs written verbatim into the committed JSON, and\n * `implements`/`uses` are legal interface property names (they are reserved words\n * only as binding identifiers, not as member names).\n */\n\n/**\n * Transport of an API contract:\n * - `rpc` β synchronous request/response over HTTP\n * - `pubsub` β fire-and-forget, delivered later through a Cloud Tasks queue\n * - `external` β a contract for a system OUTSIDE this repo (firestore, gmail, ...). Nothing in-repo\n * implements it, so it never becomes a serviceβservice edge; it terminates the graph\n * at a dashed vendor node. Detected from `runtime-architecture.externalApiPaths`\n * rather than from a decorator, because a vendor contract is a plain interface bound\n * to a Symbol token, not an `abstract class` carrying @ApiPath.\n */\nexport type ApiTransport = 'rpc' | 'pubsub' | 'external';\n\n/**\n * The kinds an external system can be DECLARED as. Each draws its own shape in the runtime viz, so\n * a datastore stops looking like an HTTP service.\n *\n * Lives here rather than beside the runtime graph model because that model already imports from this\n * file; putting it there and importing back would close a module cycle.\n */\nexport const EXTERNAL_SYSTEM_KINDS = [\n 'database',\n 'cache',\n 'queue',\n 'storage',\n 'saas',\n 'system',\n /**\n * A destination whose ADDRESS is supplied at runtime β a URL a partner registered, an OAuth\n * callback, a per-tenant host. Unlike every other kind it does not name one vendor: it names the\n * PLACE in our own system where somebody else's address is dialled, which is the fact a security\n * review is looking for.\n *\n * Declared like every other kind, on the CONTRACT: `@externalSystem runtime partner-webhooks`.\n * On the contract rather than at a `createRpcClient` call site deliberately β \"the far end of\n * this contract is outside our estate\" is a property of the CONTRACT, true for every caller of\n * it, so putting it there means one declaration however many services deliver over it, and\n * nothing to keep in step when a second one appears. It also means this kind rides the exact\n * same declare β resolve β draw pipeline `saas` and `database` already ride, rather than a\n * second mechanism that reads construction sites and can disagree with the first.\n */\n 'runtime',\n] as const;\n\nexport type ExternalSystemKind = (typeof EXTERNAL_SYSTEM_KINDS)[number];\n\n/** True for a string that names one of {@link EXTERNAL_SYSTEM_KINDS}. */\n// webpieces-disable no-function-outside-class -- type guard beside the type it guards, matching this file's DTO style\nexport function isExternalSystemKind(value: string): value is ExternalSystemKind {\n return (EXTERNAL_SYSTEM_KINDS as readonly string[]).includes(value);\n}\n\n/**\n * ONE declared external system, keyed in {@link ExternalSystemDecls} by its IDENTITY.\n *\n * Identity, not display text: two projects each tagged `external:database:postgres` name the same\n * `postgres` node and converge on it with one arrow apiece, instead of drawing a database each.\n *\n * The two arrays are the two declaration sites, and a system may legitimately have both β a repo can\n * wrap a datastore behind a contract in one service and open it directly in another.\n */\nexport interface ExternalSystemDecl {\n kind: ExternalSystemKind;\n label: string;\n /** Contracts declaring it with an `@externalSystem` JSDoc tag; every user of one gets an arrow. */\n apis: string[];\n /** Projects declaring it with an `external:<kind>:<identity>` nx tag; each gets its OWN arrow. */\n projects: string[];\n}\n\n/** identity -> its declaration. Serialized as the `externalSystems` key of dependencies.json. */\nexport type ExternalSystemDecls = Record<string, ExternalSystemDecl>;\n\n/**\n * How a project relates to ONE api-lib it depends on:\n * - `implements` β it serves the api (a controller extends it)\n * - `uses` β it calls the api (generates a client)\n * - `uses-implements` β it does BOTH (implements some of the api-lib's contracts,\n * uses others)\n */\nexport type ApiRelationKind = 'implements' | 'uses' | 'uses-implements';\n\n/** One API class a project implements or uses, with its transport. */\nexport interface ApiRef {\n api: string;\n type: ApiTransport;\n /**\n * ONLY on a `uses` ref: the service the call site aims at, read from the client config literal\n * (`createRpcClient(XxxApi, new ClientConfig('helper-fsdb'))` β `helper-fsdb`). It is matched\n * against a project's DECLARED `serviceName` to pick the ONE runtime edge target, instead of\n * fanning the edge out to every implementer of the api β which is catastrophically wrong for a\n * company-wide contract registered in a shared library and therefore implemented by every server.\n *\n * Absent when the config argument is not a `new <Xxx>ClientConfig('<literal>')` (a variable, a\n * computed name, ...). Absent means \"unknown target\", NOT \"no target\" β the runtime graph then\n * falls back to the old fan-out and says so out loud.\n */\n targetService?: string;\n /**\n * ONLY on a `pubsub` uses ref. True means \"this producer was attributed to EVERY cloudtasks\n * method of the contract, not to the methods it actually enqueues\".\n *\n * A producer builds one client for the whole contract (`createPubSubClient(EmailTaskApi, cfg)`)\n * and enqueues through a proxy (`emailTasks.send(req)`) somewhere else entirely β often after\n * the client has been stored in a DI binding β so WHICH methods it enqueues is not statically\n * recoverable. The consumer side IS exact (addRoutes + the contract's method table). Recording\n * the difference keeps a producer-side queue from being read as proof that queue is used.\n */\n methodsInferred?: boolean;\n}\n\n/**\n * Identity of a ref for de-duplication: an api used twice against DIFFERENT services is two distinct\n * relations (two distinct runtime edges), so the api name alone is not the key.\n */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function apiRefKey(ref: ApiRef): string {\n return `${ref.api} ${ref.targetService ?? ''}`;\n}\n\n/**\n * A project's relationship to ONE api-lib it depends on. Serialized verbatim into\n * architecture/dependencies.json under `apiRelations[apiLibProjectName]`.\n */\nexport interface ApiRelation {\n kind: ApiRelationKind;\n implements: ApiRef[];\n uses: ApiRef[];\n}\n\n/** apiLibProjectName -> relation. Attached to a GraphEntry as `apiRelations`. */\nexport type ProjectApiRelations = Record<string, ApiRelation>;\n\n/**\n * What triggers ONE endpoint, mirroring core-util's `EndpointKind`. Duplicated as a string union\n * rather than imported: nx-webpieces-rules is build tooling and must not take a runtime dependency\n * on the framework it inspects (it reads decorators as TEXT, from projects that may be on a\n * different @webpieces version than the tooling itself).\n */\nexport type EndpointKind = 'rpc' | 'cloudtasks' | 'cron' | 'external';\n\n/** HTTP verbs understood by the generated contract runtime. */\nexport type ContractHttpMethod = 'GET' | 'POST';\n\n/** One path/query/body mapping emitted into architecture/dependencies.json. */\nexport interface ApiParameterMeta {\n index: number;\n source: 'path' | 'query' | 'body';\n /** Path/query wire name; a JSON/form body occupies the whole entity and has no key. */\n wireName?: string;\n}\n\n/**\n * One method on an API contract, as written in source: what triggers it, where it is mounted, and\n * (for a queued method) which Cloud Tasks queue delivers it.\n */\nexport interface ApiMethodMeta {\n name: string;\n /** The @Endpoint path, relative to the class's @ApiPath basePath. */\n path: string;\n kind: EndpointKind;\n /** The actual incoming/outgoing verb; POST when @Endpoint omits httpMethod. */\n httpMethod?: ContractHttpMethod;\n /** Explicit parameter mappings; absent only when the method has none. */\n parameters?: ApiParameterMeta[];\n /** Present when callers receive the transport-neutral full response. */\n responseType?: 'full';\n /**\n * `@Queue(...)` override, else `${ApiClassName}-${methodName}`.\n *\n * ONLY on a `cloudtasks` or `cron` method β those are the kinds actually delivered through a\n * named queue or schedule, and Terraform matches on this string. A synchronous `rpc` (or an\n * inbound `external`) endpoint has no queue and needs none; emitting a plausible-looking name for\n * one put every synchronous endpoint one naive `methods.map(m => m.queueName)` away from being\n * provisioned as a queue.\n */\n queueName?: string;\n /**\n * WHO outside this repo drives this endpoint, from `@Endpoint(p, 'external', { calledBy })`.\n *\n * ONLY on an `external` method, the same way `queueName` is only on the kinds that HAVE a queue.\n *\n * Deliberately the SAME {@link ExternalSystemDeclaration} the OUTBOUND `@externalSystem` tag\n * resolves to, not a parallel inbound-only type: an inbound `saas twilio` and an outbound\n * `saas twilio` are the same vendor, so sharing the type makes them share an IDENTITY and\n * converge on ONE node instead of drawing twilio twice facing opposite directions.\n *\n * Optional in the TYPE only for graphs generated before the caller was required β generation\n * FAILS on an `external` method whose caller cannot be read (UndeclaredExternalCallerError).\n */\n caller?: ExternalSystemDeclaration;\n}\n\n/**\n * A discovered API contract class: its name, the api-lib project that owns it, its transport, and\n * its per-method trigger table.\n */\nexport interface ApiClassInfo {\n api: string;\n owner: string;\n type: ApiTransport;\n /** The class's @ApiPath basePath; absent for an external (vendor) contract, which has no route. */\n basePath?: string;\n /**\n * Every @Endpoint method, in declaration order. Empty for an external contract (a vendor\n * interface has no endpoints β it is called through a vendor SDK, not mounted).\n */\n methods: ApiMethodMeta[];\n /**\n * Set when the contract carries an `@externalSystem <kind> [label]` JSDoc tag β a vendor seam\n * declaring WHAT it is a seam to. JSDoc rather than a decorator because these seams are plain TS\n * `interface`s, which cannot carry one.\n */\n externalSystem?: ExternalSystemDeclaration;\n}\n\n/** The `(kind, label)` pair a single declaration resolves to. */\nexport interface ExternalSystemDeclaration {\n kind: ExternalSystemKind;\n label: string;\n}\n\n/**\n * The committed, per-contract view written to `architecture/dependencies.json` under `apiContracts`.\n *\n * The runtime graph is derived SOLELY from dependencies.json so generate and validate can never\n * diverge β which means anything the runtime graph needs must be COMMITTED there, not re-scanned.\n * Per-method trigger kinds and queue names are exactly that: without this table the derivation\n * cannot tell a queued endpoint from a cron sweep, and cannot name the queue between two services.\n */\nexport interface ApiContract {\n owner: string;\n /** 'rpc' | 'pubsub' for an in-repo contract, 'external' for a vendor seam. */\n apiKind: ApiTransport;\n /**\n * REQUIRED. Every routed contract carries `@ApiPath`, so every entry in this table must carry the\n * base path its methods hang off. Optional was worse than absent: a consumer joining\n * `basePath + path` for the ONE entry that lost it computed `/test` where the real route was\n * `/whatsapp/test`, and had no reason to suspect it β every other entry had the field. Generation\n * now FAILS instead of shipping an entry that computes a confidently wrong URL.\n */\n basePath: string;\n methods: ApiMethodMeta[];\n}\n\n/** apiClassName -> its committed contract. Serialized as the `apiContracts` key. */\nexport type ApiContracts = Record<string, ApiContract>;\n\n/**\n * ONE decorator argument the scan saw but could not reduce to a string β `@ApiPath(SOME_CONST)`\n * where SOME_CONST is imported from another module, a computed expression, an enum member, ...\n *\n * Recorded rather than dropped. Before this existed, an unresolvable argument cost the contract its\n * basePath, or a method, or (when EVERY method's path was one) the whole class β with nothing\n * printed anywhere. Same-module constants now resolve, so what remains here is the genuinely\n * unresolvable, which the author can fix by inlining the literal or moving the constant in-module.\n */\nexport class NonLiteralDecoratorArg {\n constructor(\n /** The contract class the argument was written on. */\n public readonly api: string,\n /** `ApiPath` | `Endpoint` | `Queue`. */\n public readonly decorator: string,\n /** The method name for a member decorator, null for a class decorator. */\n public readonly method: string | null,\n /** The argument exactly as written, e.g. `WHATSAPP_API_PATH`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * ONE `@Endpoint(path, kind)` whose PATH argument was present but could not be reduced to a string.\n *\n * Split out of NonLiteralDecoratorArg (which stays a warning, covering @Queue and the rest) because\n * this one is FATAL. Upstream components need the URL: an http client builds its request as\n * `basePath + path`, so an unreadable path is missing ROUTING, not missing metadata β the same\n * reasoning that already makes basePath required. Skipping the method instead used to delete it, and\n * a class whose every path was a constant lost every method and vanished from `apiContracts` with\n * nothing printed anywhere.\n */\nexport class UnresolvedEndpointPath {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** The path argument exactly as written, e.g. `PROCESS_PATH`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * ONE `external` `@Endpoint` whose CALLER could not be read from the source.\n *\n * Fatal for the same reason {@link UnresolvedEndpointPath} is. The inbound box exists to say who is\n * calling us from outside; with no caller it can only restate our own contract name, which is the\n * exact bug this diagnostic exists to make impossible to reintroduce. `@Endpoint`'s TS overloads\n * already require `calledBy`, so anything reaching here is a JS caller, an `as any`, a cross-module\n * constant the parser-only scan cannot fold, or an unknown `callerKind`.\n */\nexport class UndeclaredExternalCaller {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** What was wrong, as written β `<missing>`, `SOME_CONST`, `callerKind: 'vendor'`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * A contract class that DECLARED `@Endpoint` methods and kept none of them.\n *\n * The backstop for the mechanism that hid an entire service: `buildApiContracts` skips a zero-method\n * class (correctly β a vendor seam has no routes), so a class gutted by unreadable decorator\n * arguments left through the same door as a legitimately routeless one. A class that declared\n * endpoints and produced none is never legitimate, so it is named instead.\n */\nexport class EmptiedApiContract {\n constructor(\n /** The contract class name. */\n public readonly api: string,\n /** How many `@Endpoint` decorators were written on it. */\n public readonly declared: number,\n /** `path/to/file.ts:LINE` of the class, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** Derive the relation kind from the (possibly empty) implements/uses ref lists. */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function deriveApiRelationKind(\n implementsRefs: ApiRef[],\n usesRefs: ApiRef[],\n): ApiRelationKind {\n if (implementsRefs.length > 0 && usesRefs.length > 0) return 'uses-implements';\n if (implementsRefs.length > 0) return 'implements';\n return 'uses';\n}\n\n/**\n * Stable-sort a ref list by api name, then by target service, so the committed JSON is\n * deterministic even when one api is used against two different services.\n */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function sortApiRefs(refs: ApiRef[]): ApiRef[] {\n return [...refs].sort(\n (a: ApiRef, b: ApiRef) =>\n a.api.localeCompare(b.api) ||\n (a.targetService ?? '').localeCompare(b.targetService ?? ''),\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"api-relations.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-relations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Relations model\n *\n * The typed classification of a compile-time dependency edge P -> apiLib in\n * architecture/dependencies.json. Where the flat `dependsOn` only says \"P depends\n * on apiLib\", `apiRelations[apiLib]` says WHY: which API contracts P IMPLEMENTS\n * (serves, `class Ctrl extends XxxApi`) and which it USES (calls as a client,\n * `factory.createRpcClient(XxxApi, ...)` / `createPubSubClient(...)`), each tagged\n * with its transport.\n *\n * Interfaces + object literals here mirror the sibling runtime-graph.ts model β\n * these are serialization DTOs written verbatim into the committed JSON, and\n * `implements`/`uses` are legal interface property names (they are reserved words\n * only as binding identifiers, not as member names).\n */\n\n/**\n * Transport of an API contract:\n * - `rpc` β synchronous request/response over HTTP\n * - `pubsub` β fire-and-forget, delivered later through a Cloud Tasks queue\n * - `external` β a contract for a system OUTSIDE this repo (firestore, gmail, ...). Nothing in-repo\n * implements it, so it never becomes a serviceβservice edge; it terminates the graph\n * at a dashed vendor node. Detected from `runtime-architecture.externalApiPaths`\n * rather than from a decorator, because a vendor contract is a plain interface bound\n * to a Symbol token, not an `abstract class` carrying @ApiPath.\n */\nexport type ApiTransport = 'rpc' | 'pubsub' | 'external';\n\n/**\n * The kinds an external system can be DECLARED as. Each draws its own shape in the runtime viz, so\n * a datastore stops looking like an HTTP service.\n *\n * Lives here rather than beside the runtime graph model because that model already imports from this\n * file; putting it there and importing back would close a module cycle.\n */\nexport const EXTERNAL_SYSTEM_KINDS = [\n 'database',\n 'cache',\n 'queue',\n 'storage',\n 'saas',\n 'system',\n /**\n * A destination whose ADDRESS is supplied at runtime β a URL a partner registered, an OAuth\n * callback, a per-tenant host. Unlike every other kind it does not name one vendor: it names the\n * PLACE in our own system where somebody else's address is dialled, which is the fact a security\n * review is looking for.\n *\n * Declared like every other kind, on the CONTRACT: `@externalSystem runtime partner-webhooks`.\n * On the contract rather than at a `createRpcClient` call site deliberately β \"the far end of\n * this contract is outside our estate\" is a property of the CONTRACT, true for every caller of\n * it, so putting it there means one declaration however many services deliver over it, and\n * nothing to keep in step when a second one appears. It also means this kind rides the exact\n * same declare β resolve β draw pipeline `saas` and `database` already ride, rather than a\n * second mechanism that reads construction sites and can disagree with the first.\n */\n 'runtime',\n] as const;\n\nexport type ExternalSystemKind = (typeof EXTERNAL_SYSTEM_KINDS)[number];\n\n/** True for a string that names one of {@link EXTERNAL_SYSTEM_KINDS}. */\n// webpieces-disable no-function-outside-class -- type guard beside the type it guards, matching this file's DTO style\nexport function isExternalSystemKind(value: string): value is ExternalSystemKind {\n return (EXTERNAL_SYSTEM_KINDS as readonly string[]).includes(value);\n}\n\n/**\n * ONE declared external system, keyed in {@link ExternalSystemDecls} by its IDENTITY.\n *\n * Identity, not display text: two projects each tagged `external:database:postgres` name the same\n * `postgres` node and converge on it with one arrow apiece, instead of drawing a database each.\n *\n * The two arrays are the two declaration sites, and a system may legitimately have both β a repo can\n * wrap a datastore behind a contract in one service and open it directly in another.\n */\nexport interface ExternalSystemDecl {\n kind: ExternalSystemKind;\n label: string;\n /** Contracts declaring it with an `@externalSystem` JSDoc tag; every user of one gets an arrow. */\n apis: string[];\n /** Projects declaring it with an `external:<kind>:<identity>` nx tag; each gets its OWN arrow. */\n projects: string[];\n}\n\n/** identity -> its declaration. Serialized as the `externalSystems` key of dependencies.json. */\nexport type ExternalSystemDecls = Record<string, ExternalSystemDecl>;\n\n/**\n * How a project relates to ONE api-lib it depends on:\n * - `implements` β it serves the api (a controller extends it)\n * - `uses` β it calls the api (generates a client)\n * - `uses-implements` β it does BOTH (implements some of the api-lib's contracts,\n * uses others)\n */\nexport type ApiRelationKind = 'implements' | 'uses' | 'uses-implements';\n\n/** One API class a project implements or uses, with its transport. */\nexport interface ApiRef {\n api: string;\n type: ApiTransport;\n /**\n * ONLY on a `uses` ref: the service the call site aims at, read from the client config literal\n * (`createRpcClient(XxxApi, new ClientConfig('helper-fsdb'))` β `helper-fsdb`). It is matched\n * against a project's DECLARED `serviceName` to pick the ONE runtime edge target, instead of\n * fanning the edge out to every implementer of the api β which is catastrophically wrong for a\n * company-wide contract registered in a shared library and therefore implemented by every server.\n *\n * Absent when the config argument is not a `new <Xxx>ClientConfig('<literal>')` (a variable, a\n * computed name, ...). Absent means \"unknown target\", NOT \"no target\" β the runtime graph then\n * falls back to the old fan-out and says so out loud.\n */\n targetService?: string;\n /**\n * ONLY on a `pubsub` uses ref. True means \"this producer was attributed to EVERY cloudtasks\n * method of the contract, not to the methods it actually enqueues\".\n *\n * A producer builds one client for the whole contract (`createPubSubClient(EmailTaskApi, cfg)`)\n * and enqueues through a proxy (`emailTasks.send(req)`) somewhere else entirely β often after\n * the client has been stored in a DI binding β so WHICH methods it enqueues is not statically\n * recoverable. The consumer side IS exact (addRoutes + the contract's method table). Recording\n * the difference keeps a producer-side queue from being read as proof that queue is used.\n */\n methodsInferred?: boolean;\n}\n\n/**\n * Identity of a ref for de-duplication: an api used twice against DIFFERENT services is two distinct\n * relations (two distinct runtime edges), so the api name alone is not the key.\n */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function apiRefKey(ref: ApiRef): string {\n return `${ref.api} ${ref.targetService ?? ''}`;\n}\n\n/**\n * A project's relationship to ONE api-lib it depends on. Serialized verbatim into\n * architecture/dependencies.json under `apiRelations[apiLibProjectName]`.\n */\nexport interface ApiRelation {\n kind: ApiRelationKind;\n implements: ApiRef[];\n uses: ApiRef[];\n}\n\n/** apiLibProjectName -> relation. Attached to a GraphEntry as `apiRelations`. */\nexport type ProjectApiRelations = Record<string, ApiRelation>;\n\n/**\n * What triggers ONE endpoint, mirroring core-util's `EndpointKind`. Duplicated as a string union\n * rather than imported: nx-webpieces-rules is build tooling and must not take a runtime dependency\n * on the framework it inspects (it reads decorators as TEXT, from projects that may be on a\n * different @webpieces version than the tooling itself).\n */\nexport type EndpointKind = 'rpc' | 'cloudtasks' | 'cron' | 'external';\n\n/** HTTP verbs understood by the generated contract runtime. */\nexport type ContractHttpMethod = 'GET' | 'POST';\n\n/** One path/query/body mapping emitted into architecture/dependencies.json. */\nexport interface ApiParameterMeta {\n index: number;\n source: 'path' | 'query' | 'body';\n /** Path/query wire name; a JSON/form body occupies the whole entity and has no key. */\n wireName?: string;\n}\n\n/**\n * One method on an API contract, as written in source: what triggers it, where it is mounted, and\n * (for a queued method) which Cloud Tasks queue delivers it.\n */\nexport interface ApiMethodMeta {\n name: string;\n /** The @Endpoint path, relative to the class's @ApiPath basePath. */\n path: string;\n kind: EndpointKind;\n /** The actual incoming/outgoing verb; POST when @Endpoint omits httpMethod. */\n httpMethod?: ContractHttpMethod;\n /** Explicit parameter mappings; absent only when the method has none. */\n parameters?: ApiParameterMeta[];\n /** Present when callers receive the transport-neutral full response. */\n responseType?: 'full';\n /**\n * `@Queue(...)` override, else `${ApiClassName}-${methodName}`.\n *\n * ONLY on a `cloudtasks` or `cron` method β those are the kinds actually delivered through a\n * named queue or schedule, and Terraform matches on this string. A synchronous `rpc` (or an\n * inbound `external`) endpoint has no queue and needs none; emitting a plausible-looking name for\n * one put every synchronous endpoint one naive `methods.map(m => m.queueName)` away from being\n * provisioned as a queue.\n */\n queueName?: string;\n /**\n * WHO outside this repo drives this endpoint, from `@Endpoint(p, 'external', { calledBy })`.\n *\n * ONLY on an `external` method, the same way `queueName` is only on the kinds that HAVE a queue.\n *\n * Deliberately the SAME {@link ExternalSystemDeclaration} the OUTBOUND `@externalSystem` tag\n * resolves to, not a parallel inbound-only type: an inbound `saas twilio` and an outbound\n * `saas twilio` are the same vendor, so sharing the type makes them share an IDENTITY and\n * converge on ONE node instead of drawing twilio twice facing opposite directions.\n *\n * Optional in the TYPE only for graphs generated before the caller was required β generation\n * FAILS on an `external` method whose caller cannot be read (UndeclaredExternalCallerError).\n */\n caller?: ExternalSystemDeclaration;\n}\n\n/**\n * A discovered API contract class: its name, the api-lib project that owns it, its transport, and\n * its per-method trigger table.\n */\nexport interface ApiClassInfo {\n api: string;\n owner: string;\n type: ApiTransport;\n /** The class's @ApiPath basePath; absent for an external (vendor) contract, which has no route. */\n basePath?: string;\n /**\n * Every @Endpoint method, in declaration order. Empty for an external contract (a vendor\n * interface has no endpoints β it is called through a vendor SDK, not mounted).\n */\n methods: ApiMethodMeta[];\n /**\n * Set when the contract carries an `@externalSystem <kind> [label]` JSDoc tag β a vendor seam\n * declaring WHAT it is a seam to. JSDoc rather than a decorator because these seams are plain TS\n * `interface`s, which cannot carry one.\n */\n externalSystem?: ExternalSystemDeclaration;\n}\n\n/** The `(kind, label)` pair a single declaration resolves to. */\nexport interface ExternalSystemDeclaration {\n kind: ExternalSystemKind;\n label: string;\n}\n\n/**\n * The committed, per-contract view written to `architecture/apis/<ApiName>.json` (one file per API;\n * dependencies.json only links to it under `apiContractFiles` β see ApiContractFiles).\n *\n * The runtime graph is derived from the committed files so generate and validate can never\n * diverge β which means anything the runtime graph needs must be COMMITTED, not re-scanned.\n * Per-method trigger kinds and queue names are exactly that: without this table the derivation\n * cannot tell a queued endpoint from a cron sweep, and cannot name the queue between two services.\n */\nexport interface ApiContract {\n owner: string;\n /** 'rpc' | 'pubsub' for an in-repo contract, 'external' for a vendor seam. */\n apiKind: ApiTransport;\n /**\n * REQUIRED. Every routed contract carries `@ApiPath`, so every entry in this table must carry the\n * base path its methods hang off. Optional was worse than absent: a consumer joining\n * `basePath + path` for the ONE entry that lost it computed `/test` where the real route was\n * `/whatsapp/test`, and had no reason to suspect it β every other entry had the field. Generation\n * now FAILS instead of shipping an entry that computes a confidently wrong URL.\n */\n basePath: string;\n methods: ApiMethodMeta[];\n}\n\n/** apiClassName -> its committed contract. Serialized as one `architecture/apis/<ApiName>.json` each. */\nexport type ApiContracts = Record<string, ApiContract>;\n\n/**\n * ONE decorator argument the scan saw but could not reduce to a string β `@ApiPath(SOME_CONST)`\n * where SOME_CONST is imported from another module, a computed expression, an enum member, ...\n *\n * Recorded rather than dropped. Before this existed, an unresolvable argument cost the contract its\n * basePath, or a method, or (when EVERY method's path was one) the whole class β with nothing\n * printed anywhere. Same-module constants now resolve, so what remains here is the genuinely\n * unresolvable, which the author can fix by inlining the literal or moving the constant in-module.\n */\nexport class NonLiteralDecoratorArg {\n constructor(\n /** The contract class the argument was written on. */\n public readonly api: string,\n /** `ApiPath` | `Endpoint` | `Queue`. */\n public readonly decorator: string,\n /** The method name for a member decorator, null for a class decorator. */\n public readonly method: string | null,\n /** The argument exactly as written, e.g. `WHATSAPP_API_PATH`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * ONE `@Endpoint(path, kind)` whose PATH argument was present but could not be reduced to a string.\n *\n * Split out of NonLiteralDecoratorArg (which stays a warning, covering @Queue and the rest) because\n * this one is FATAL. Upstream components need the URL: an http client builds its request as\n * `basePath + path`, so an unreadable path is missing ROUTING, not missing metadata β the same\n * reasoning that already makes basePath required. Skipping the method instead used to delete it, and\n * a class whose every path was a constant lost every method and vanished from the contract table with\n * nothing printed anywhere.\n */\nexport class UnresolvedEndpointPath {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** The path argument exactly as written, e.g. `PROCESS_PATH`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * ONE `external` `@Endpoint` whose CALLER could not be read from the source.\n *\n * Fatal for the same reason {@link UnresolvedEndpointPath} is. The inbound box exists to say who is\n * calling us from outside; with no caller it can only restate our own contract name, which is the\n * exact bug this diagnostic exists to make impossible to reintroduce. `@Endpoint`'s TS overloads\n * already require `calledBy`, so anything reaching here is a JS caller, an `as any`, a cross-module\n * constant the parser-only scan cannot fold, or an unknown `callerKind`.\n */\nexport class UndeclaredExternalCaller {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** What was wrong, as written β `<missing>`, `SOME_CONST`, `callerKind: 'vendor'`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * A contract class that DECLARED `@Endpoint` methods and kept none of them.\n *\n * The backstop for the mechanism that hid an entire service: `buildApiContracts` skips a zero-method\n * class (correctly β a vendor seam has no routes), so a class gutted by unreadable decorator\n * arguments left through the same door as a legitimately routeless one. A class that declared\n * endpoints and produced none is never legitimate, so it is named instead.\n */\nexport class EmptiedApiContract {\n constructor(\n /** The contract class name. */\n public readonly api: string,\n /** How many `@Endpoint` decorators were written on it. */\n public readonly declared: number,\n /** `path/to/file.ts:LINE` of the class, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** Derive the relation kind from the (possibly empty) implements/uses ref lists. */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function deriveApiRelationKind(\n implementsRefs: ApiRef[],\n usesRefs: ApiRef[],\n): ApiRelationKind {\n if (implementsRefs.length > 0 && usesRefs.length > 0) return 'uses-implements';\n if (implementsRefs.length > 0) return 'implements';\n return 'uses';\n}\n\n/**\n * Stable-sort a ref list by api name, then by target service, so the committed JSON is\n * deterministic even when one api is used against two different services.\n */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function sortApiRefs(refs: ApiRef[]): ApiRef[] {\n return [...refs].sort(\n (a: ApiRef, b: ApiRef) =>\n a.api.localeCompare(b.api) ||\n (a.targetService ?? '').localeCompare(b.targetService ?? ''),\n );\n}\n"]}
|
|
@@ -161,7 +161,8 @@ export declare class ApiUsageScanner {
|
|
|
161
161
|
*/
|
|
162
162
|
export declare function scanAndAttachApiRelations(workspaceRoot: string, graph: EnhancedGraph, projectInfos: Map<string, ProjectInfo>, externalApiPaths?: readonly string[]): ApiScanResult;
|
|
163
163
|
/**
|
|
164
|
-
* The committed
|
|
164
|
+
* The committed api contract table (one `architecture/apis/<ApiName>.json` per entry), from a
|
|
165
|
+
* completed scan.
|
|
165
166
|
*
|
|
166
167
|
* Only contracts with β₯1 endpoint are emitted: a vendor seam has no routes, so a table entry for it
|
|
167
168
|
* would be an empty shell, and its identity is already carried by the `external` refs in
|
|
@@ -414,7 +414,8 @@ function scanAndAttachApiRelations(workspaceRoot, graph, projectInfos, externalA
|
|
|
414
414
|
return result;
|
|
415
415
|
}
|
|
416
416
|
/**
|
|
417
|
-
* The committed
|
|
417
|
+
* The committed api contract table (one `architecture/apis/<ApiName>.json` per entry), from a
|
|
418
|
+
* completed scan.
|
|
418
419
|
*
|
|
419
420
|
* Only contracts with β₯1 endpoint are emitted: a vendor seam has no routes, so a table entry for it
|
|
420
421
|
* would be an empty shell, and its identity is already carried by the `external` refs in
|