@checkdigit/eslint-plugin 7.18.0-PR.143-c535 → 7.18.0-PR.143-0de2
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/dist-mjs/athena/api-matcher.mjs +169 -79
- package/package.json +1 -1
- package/src/athena/api-matcher.ts +204 -105
|
@@ -1,103 +1,193 @@
|
|
|
1
1
|
// src/athena/api-matcher.ts
|
|
2
2
|
import debug from "debug";
|
|
3
|
-
import { JSONPath } from "jsonpath-plus";
|
|
4
3
|
var log = debug("eslint-plugin:athena:api-matcher");
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
var ALWAYS_TRUE = () => true;
|
|
5
|
+
function rec(node) {
|
|
6
|
+
return typeof node === "object" && node !== null ? node : void 0;
|
|
7
7
|
}
|
|
8
|
-
function
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
});
|
|
13
|
-
if (pathPartCondition !== void 0) {
|
|
14
|
-
const [pathPartIndex] = JSONPath({
|
|
15
|
-
json: pathPartCondition,
|
|
16
|
-
path: "$.left.array_index[0].index.value"
|
|
17
|
-
});
|
|
18
|
-
const [pathPartMatch] = JSONPath({
|
|
19
|
-
json: pathPartCondition,
|
|
20
|
-
path: "$.right.value"
|
|
21
|
-
});
|
|
22
|
-
return (path, _method) => {
|
|
23
|
-
const parts = path.split("/");
|
|
24
|
-
const part = parts[pathPartIndex - 1];
|
|
25
|
-
log(`checking path part`, { path, pathPartIndex, part, pathPartMatch });
|
|
26
|
-
return part?.startsWith(":") === true ? true : parts[pathPartIndex - 1] === pathPartMatch;
|
|
27
|
-
};
|
|
8
|
+
function getFunctionName(node) {
|
|
9
|
+
const fn = rec(node);
|
|
10
|
+
if (fn?.["type"] !== "function") {
|
|
11
|
+
return void 0;
|
|
28
12
|
}
|
|
29
|
-
|
|
13
|
+
const name = fn["name"];
|
|
14
|
+
const firstName = name?.name[0];
|
|
15
|
+
return firstName === void 0 ? void 0 : firstName.value.toLowerCase();
|
|
30
16
|
}
|
|
31
|
-
function
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
});
|
|
36
|
-
if (pathPartCount !== void 0) {
|
|
37
|
-
return (path, _method) => {
|
|
38
|
-
const parts = path.split("/");
|
|
39
|
-
return parts.length === pathPartCount;
|
|
40
|
-
};
|
|
17
|
+
function getColumnName(node) {
|
|
18
|
+
const col = rec(node);
|
|
19
|
+
if (col?.["type"] !== "column_ref") {
|
|
20
|
+
return void 0;
|
|
41
21
|
}
|
|
42
|
-
|
|
22
|
+
const column = node.column;
|
|
23
|
+
return typeof column === "string" ? column.toLowerCase() : void 0;
|
|
24
|
+
}
|
|
25
|
+
function getColumnTable(node) {
|
|
26
|
+
const col = rec(node);
|
|
27
|
+
if (col?.["type"] !== "column_ref") {
|
|
28
|
+
return void 0;
|
|
29
|
+
}
|
|
30
|
+
return node.table;
|
|
31
|
+
}
|
|
32
|
+
function getStringValue(node) {
|
|
33
|
+
const val = rec(node);
|
|
34
|
+
if (val?.["type"] !== "single_quote_string" && val?.["type"] !== "string") {
|
|
35
|
+
return void 0;
|
|
36
|
+
}
|
|
37
|
+
return typeof val["value"] === "string" ? val["value"] : void 0;
|
|
38
|
+
}
|
|
39
|
+
function getNumberValue(node) {
|
|
40
|
+
const val = rec(node);
|
|
41
|
+
return val?.["type"] === "number" && typeof val["value"] === "number" ? val["value"] : void 0;
|
|
42
|
+
}
|
|
43
|
+
function isSplitUrlIndexed(node) {
|
|
44
|
+
const fn = rec(node);
|
|
45
|
+
if (fn?.["type"] !== "function") {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (getFunctionName(node) !== "split") {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const args = fn["args"]?.value;
|
|
52
|
+
if (!Array.isArray(args) || args.length < 2) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (getColumnName(args[0]) !== "url") {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (getStringValue(args[1]) !== "/") {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return Array.isArray(fn["array_index"]) && fn["array_index"].length > 0;
|
|
62
|
+
}
|
|
63
|
+
function isCardinalitySplitUrl(node) {
|
|
64
|
+
const fn = rec(node);
|
|
65
|
+
if (fn?.["type"] !== "function") {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (getFunctionName(node) !== "cardinality") {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const outerArgs = fn["args"]?.value;
|
|
72
|
+
if (!Array.isArray(outerArgs) || outerArgs.length === 0) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const innerFn = rec(outerArgs[0]);
|
|
76
|
+
if (innerFn?.["type"] !== "function") {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (getFunctionName(outerArgs[0]) !== "split") {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
const innerArgs = innerFn["args"]?.value;
|
|
83
|
+
if (!Array.isArray(innerArgs) || innerArgs.length < 2) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return getColumnName(innerArgs[0]) === "url" && getStringValue(innerArgs[1]) === "/";
|
|
43
87
|
}
|
|
44
|
-
function
|
|
45
|
-
|
|
88
|
+
function getConditionTableQualifier(left) {
|
|
89
|
+
const colName = getColumnName(left);
|
|
90
|
+
if (colName !== void 0) {
|
|
91
|
+
return getColumnTable(left);
|
|
92
|
+
}
|
|
93
|
+
if (isSplitUrlIndexed(left)) {
|
|
94
|
+
const args = rec(left)?.["args"]?.value;
|
|
95
|
+
return getColumnTable(args?.[0]) ?? null;
|
|
96
|
+
}
|
|
97
|
+
if (isCardinalitySplitUrl(left)) {
|
|
98
|
+
const outerArgs = rec(left)?.["args"]?.value;
|
|
99
|
+
const splitFn = rec(outerArgs?.[0]);
|
|
100
|
+
const innerArgs = splitFn?.["args"]?.value;
|
|
101
|
+
return getColumnTable(innerArgs?.[0]) ?? null;
|
|
102
|
+
}
|
|
103
|
+
return void 0;
|
|
46
104
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
105
|
+
function buildLeafPredicate(node, tableAlias) {
|
|
106
|
+
if (node.operator !== "=") {
|
|
107
|
+
return void 0;
|
|
108
|
+
}
|
|
109
|
+
const { left, right } = node;
|
|
110
|
+
const conditionTable = getConditionTableQualifier(left);
|
|
111
|
+
if (conditionTable !== null && conditionTable !== void 0 && tableAlias !== null && conditionTable !== tableAlias) {
|
|
112
|
+
return void 0;
|
|
113
|
+
}
|
|
114
|
+
if (getColumnName(left) === "method") {
|
|
115
|
+
const value = getStringValue(right);
|
|
116
|
+
if (value !== void 0) {
|
|
117
|
+
return (_path, method) => method === value;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (getColumnName(left) === "responsestatus") {
|
|
121
|
+
const value = getStringValue(right);
|
|
122
|
+
if (value !== void 0) {
|
|
123
|
+
return (_path, _method, responseCode) => responseCode === value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (isSplitUrlIndexed(left)) {
|
|
127
|
+
const index = left.array_index[0]?.index.value;
|
|
128
|
+
const value = getStringValue(right);
|
|
129
|
+
if (typeof index === "number" && value !== void 0) {
|
|
130
|
+
return (path) => {
|
|
131
|
+
const parts = path.split("/");
|
|
132
|
+
const part = parts[index - 1];
|
|
133
|
+
log(`checking path part`, { path, index, part, value });
|
|
134
|
+
return part?.startsWith(":") === true ? true : part === value;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (isCardinalitySplitUrl(left)) {
|
|
139
|
+
const count = getNumberValue(right);
|
|
140
|
+
if (count !== void 0) {
|
|
141
|
+
return (path) => path.split("/").length === count;
|
|
142
|
+
}
|
|
54
143
|
}
|
|
55
144
|
return void 0;
|
|
56
145
|
}
|
|
57
|
-
function
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
146
|
+
function buildPredicate(expr, tableAlias) {
|
|
147
|
+
const node = rec(expr);
|
|
148
|
+
if (node?.["type"] !== "binary_expr") {
|
|
149
|
+
return ALWAYS_TRUE;
|
|
150
|
+
}
|
|
151
|
+
const binary = expr;
|
|
152
|
+
switch (binary.operator) {
|
|
153
|
+
case "AND": {
|
|
154
|
+
const leftPred = buildPredicate(binary.left, tableAlias);
|
|
155
|
+
const rightPred = buildPredicate(binary.right, tableAlias);
|
|
156
|
+
return (path, method, code) => leftPred(path, method, code) && rightPred(path, method, code);
|
|
157
|
+
}
|
|
158
|
+
case "OR": {
|
|
159
|
+
const leftPred = buildPredicate(binary.left, tableAlias);
|
|
160
|
+
const rightPred = buildPredicate(binary.right, tableAlias);
|
|
161
|
+
return (path, method, code) => leftPred(path, method, code) || rightPred(path, method, code);
|
|
162
|
+
}
|
|
163
|
+
case "NOT": {
|
|
164
|
+
const innerPred = buildPredicate(binary.left, tableAlias);
|
|
165
|
+
return (path, method, code) => !innerPred(path, method, code);
|
|
166
|
+
}
|
|
167
|
+
default: {
|
|
168
|
+
return buildLeafPredicate(binary, tableAlias) ?? ALWAYS_TRUE;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
63
171
|
}
|
|
64
172
|
function matchApi(selectAST, tableAST, apiSchemas) {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
getMethodMatcher(selectAST, tableAST)
|
|
69
|
-
].flat().filter((matcher) => matcher !== void 0);
|
|
70
|
-
const allOperationSchemas = apiSchemas.flatMap((apiSchema) => Object.entries(apiSchema.apis)).flatMap(
|
|
173
|
+
const tableAlias = tableAST.as ?? null;
|
|
174
|
+
const predicate = buildPredicate(selectAST.where ?? null, tableAlias);
|
|
175
|
+
const allOperations = apiSchemas.flatMap((apiSchema) => Object.entries(apiSchema.apis)).flatMap(
|
|
71
176
|
([path, operations]) => Object.entries(operations).map(([method, operationSchemas]) => ({
|
|
72
177
|
path,
|
|
73
178
|
method: method.toUpperCase(),
|
|
74
179
|
operationSchemas
|
|
75
180
|
}))
|
|
76
181
|
);
|
|
77
|
-
log("total operation schemas",
|
|
78
|
-
const
|
|
79
|
-
({ path, method }) =>
|
|
182
|
+
log("total operation schemas", allOperations.length);
|
|
183
|
+
const matchedApis = allOperations.flatMap(
|
|
184
|
+
({ path, method, operationSchemas }) => Object.entries(operationSchemas.responses).flatMap(
|
|
185
|
+
([responseCode, responseSchema]) => predicate(path, method, responseCode) ? [{ path, method, request: operationSchemas.request, response: responseSchema }] : []
|
|
186
|
+
)
|
|
80
187
|
);
|
|
81
|
-
log("matched
|
|
82
|
-
if (matchedOperationSchemas.length === 0) {
|
|
83
|
-
log("no matched operation schema");
|
|
84
|
-
throw new Error("no matched operation schema");
|
|
85
|
-
}
|
|
86
|
-
const matchedResponseStatus = getResponseStatusToMatch(selectAST, tableAST);
|
|
87
|
-
log("matchedResponseStatus", matchedResponseStatus);
|
|
88
|
-
const matchedApis = matchedOperationSchemas.flatMap(
|
|
89
|
-
(operation) => Object.entries(operation.operationSchemas.responses).map(([responseCode, responseSchema]) => {
|
|
90
|
-
const matchedResponseSchema = matchedResponseStatus === void 0 || responseCode === matchedResponseStatus ? responseSchema : void 0;
|
|
91
|
-
return matchedResponseSchema === void 0 ? void 0 : {
|
|
92
|
-
path: operation.path,
|
|
93
|
-
method: operation.method,
|
|
94
|
-
request: operation.operationSchemas.request,
|
|
95
|
-
response: matchedResponseSchema
|
|
96
|
-
};
|
|
97
|
-
})
|
|
98
|
-
).filter((api) => api !== void 0);
|
|
188
|
+
log("matched apis", matchedApis.length);
|
|
99
189
|
if (matchedApis.length === 0) {
|
|
100
|
-
log("no api
|
|
190
|
+
log("no matched api");
|
|
101
191
|
throw new Error("no matched api");
|
|
102
192
|
}
|
|
103
193
|
return matchedApis;
|
|
@@ -105,4 +195,4 @@ function matchApi(selectAST, tableAST, apiSchemas) {
|
|
|
105
195
|
export {
|
|
106
196
|
matchApi
|
|
107
197
|
};
|
|
108
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
198
|
+
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi4vLi4vc3JjL2F0aGVuYS9hcGktbWF0Y2hlci50cyJdLAogICJtYXBwaW5ncyI6ICI7QUFFQSxPQUFPLFdBQVc7QUFNbEIsSUFBTSxNQUFNLE1BQU0sa0NBQWtDO0FBa0JwRCxJQUFNLGNBQWtDLE1BQU07QUFJOUMsU0FBUyxJQUFJLE1BQW9EO0FBQy9ELFNBQU8sT0FBTyxTQUFTLFlBQVksU0FBUyxPQUFRLE9BQW1DO0FBQ3pGO0FBRUEsU0FBUyxnQkFBZ0IsTUFBbUM7QUFDMUQsUUFBTSxLQUFLLElBQUksSUFBSTtBQUNuQixNQUFJLEtBQUssTUFBTSxNQUFNLFlBQVk7QUFDL0IsV0FBTztBQUFBLEVBQ1Q7QUFDQSxRQUFNLE9BQU8sR0FBRyxNQUFNO0FBQ3RCLFFBQU0sWUFBWSxNQUFNLEtBQUssQ0FBQztBQUM5QixTQUFPLGNBQWMsU0FBWSxTQUFZLFVBQVUsTUFBTSxZQUFZO0FBQzNFO0FBRUEsU0FBUyxjQUFjLE1BQW1DO0FBQ3hELFFBQU0sTUFBTSxJQUFJLElBQUk7QUFDcEIsTUFBSSxNQUFNLE1BQU0sTUFBTSxjQUFjO0FBQ2xDLFdBQU87QUFBQSxFQUNUO0FBQ0EsUUFBTSxTQUFVLEtBQXVCO0FBQ3ZDLFNBQU8sT0FBTyxXQUFXLFdBQVcsT0FBTyxZQUFZLElBQUk7QUFDN0Q7QUFFQSxTQUFTLGVBQWUsTUFBMEM7QUFDaEUsUUFBTSxNQUFNLElBQUksSUFBSTtBQUNwQixNQUFJLE1BQU0sTUFBTSxNQUFNLGNBQWM7QUFDbEMsV0FBTztBQUFBLEVBQ1Q7QUFDQSxTQUFRLEtBQXVCO0FBQ2pDO0FBRUEsU0FBUyxlQUFlLE1BQW1DO0FBQ3pELFFBQU0sTUFBTSxJQUFJLElBQUk7QUFDcEIsTUFBSSxNQUFNLE1BQU0sTUFBTSx5QkFBeUIsTUFBTSxNQUFNLE1BQU0sVUFBVTtBQUN6RSxXQUFPO0FBQUEsRUFDVDtBQUNBLFNBQU8sT0FBTyxJQUFJLE9BQU8sTUFBTSxXQUFXLElBQUksT0FBTyxJQUFJO0FBQzNEO0FBRUEsU0FBUyxl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
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@checkdigit/eslint-plugin","version":"7.18.0-PR.143-
|
|
1
|
+
{"name":"@checkdigit/eslint-plugin","version":"7.18.0-PR.143-0de2","description":"Check Digit eslint plugins","keywords":["eslint","eslintplugin"],"homepage":"https://github.com/checkdigit/eslint-plugin#readme","bugs":{"url":"https://github.com/checkdigit/eslint-plugin/issues"},"repository":{"type":"git","url":"https://github.com/checkdigit/eslint-plugin"},"license":"MIT","author":"Check Digit, LLC","sideEffects":false,"type":"module","exports":{".":{"types":"./dist-types/index.d.ts","import":"./dist-mjs/index.mjs","default":"./dist-mjs/index.mjs"}},"files":["src","dist-types","dist-mjs","!src/**/test/**","!src/**/*.test.ts","!src/**/*.spec.ts","!dist-types/**/test/**","!dist-types/**/*.test.d.ts","!dist-types/**/*.spec.d.ts","!dist-mjs/**/test/**","!dist-mjs/**/*.test.mjs","!dist-mjs/**/*.spec.mjs","SECURITY.md"],"scripts":{"build:dist-mjs":"rimraf dist-mjs && npx builder --type=module --sourceMap --outDir=dist-mjs && node dist-mjs/index.mjs","build:dist-types":"rimraf dist-types && npx builder --type=types --outDir=dist-types","ci:compile":"tsc --noEmit","ci:coverage":"NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=true","ci:lint":"npm run lint","ci:style":"npm run prettier","ci:test":"NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=false","lint":"eslint --max-warnings 0 .","lint:fix":"eslint --max-warnings 0 --fix .","peggy":"for file in ./src/peggy/*.peggy; do peggy \"$file\" --format es --output \"${file%.peggy}-peggy.ts\"; done","peggy-watch":"for file in ./src/peggy/*.peggy; do peggy \"$file\" --format=es --watch --output=\"${file%.peggy}-peggy.ts\"; done","prepare":"","prepublishOnly":"npm run build:dist-types && npm run build:dist-mjs","prettier":"prettier --ignore-path .gitignore --list-different .","prettier:fix":"prettier --ignore-path .gitignore --write .","test":"npm run ci:compile && npm run ci:test && npm run ci:lint && npm run ci:style"},"prettier":"@checkdigit/prettier-config","jest":{"preset":"@checkdigit/jest-config"},"dependencies":{"@apidevtools/json-schema-ref-parser":"^15.3.5","@typescript-eslint/type-utils":"^8.60.1","@typescript-eslint/utils":"^8.60.1","ajv":"^8.20.0","debug":"^4.4.3","glob":"^13.0.6","http-status-codes":"^2.3.0","js-yaml":"^4.2.0","json-pointer":"^0.6.2","jsonpath-plus":"^10.4.0","ts-api-utils":"^2.5.0"},"devDependencies":{"@checkdigit/jest-config":"^6.0.2","@checkdigit/prettier-config":"^6.1.0","@checkdigit/typescript-config":"10.0.0","@eslint/js":"^9.37.0","@types/debug":"^4.1.13","@types/eslint":"^9.6.1","@types/eslint-config-prettier":"^6.11.3","@types/js-yaml":"^4.0.9","@types/json-pointer":"^1.0.34","@typescript-eslint/parser":"^8.60.1","@typescript-eslint/rule-tester":"^8.60.1","eslint":"^9.37.0","eslint-config-prettier":"^10.1.8","eslint-import-resolver-typescript":"^4.4.5","eslint-plugin-eslint-plugin":"^6.4.0","eslint-plugin-import":"^2.32.0","eslint-plugin-no-only-tests":"^3.4.0","eslint-plugin-no-secrets":"^2.3.3","eslint-plugin-node":"^11.1.0","eslint-plugin-sonarjs":"^1.0.4","openapi-types":"^12.1.3","peggy":"^4.2.0","rimraf":"^6.1.3","typescript-eslint":"^8.60.1"},"peerDependencies":{"eslint":">=9 <10"},"engines":{"node":">=22.18"},"service":{"api":{"root":"src","endpoints":["api/v1"]}},"wallaby":{"env":{"params":{"runner":"--experimental-vm-modules"}}}}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// athena/api-matcher.ts
|
|
2
2
|
|
|
3
3
|
import debug from 'debug';
|
|
4
|
-
import { JSONPath } from 'jsonpath-plus';
|
|
5
4
|
import type { SchemaObject } from 'ajv/dist/2020';
|
|
6
5
|
|
|
7
6
|
import type { ApiSchemas, OperationSchemas } from '../openapi/generate-schema';
|
|
7
|
+
import type { Binary, ColumnRefItem, Function as SqlFunction } from './types';
|
|
8
8
|
|
|
9
9
|
const log = debug('eslint-plugin:athena:api-matcher');
|
|
10
10
|
|
|
@@ -21,105 +21,226 @@ export interface MatchedOperation {
|
|
|
21
21
|
response: SchemaObject;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// A predicate over an API operation candidate: (url path, HTTP method, HTTP response code)
|
|
25
|
+
type OperationPredicate = (path: string, method: string, responseCode: string) => boolean;
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const ALWAYS_TRUE: OperationPredicate = () => true;
|
|
28
|
+
|
|
29
|
+
// --- AST accessor helpers (use Record<string,unknown> to avoid TypeScript narrowing conflicts) ---
|
|
30
|
+
|
|
31
|
+
function rec(node: unknown): Record<string, unknown> | undefined {
|
|
32
|
+
return typeof node === 'object' && node !== null ? (node as Record<string, unknown>) : undefined;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
path: "$.where..[?(@ && @.type === 'binary_expr' && @.operator === '=' && @.left && @.left.type === 'function' && @.left.name && @.left.name.name && @.left.name.name[0] && @.left.name.name[0].value === 'split' && @.left.args && @.left.args.value && @.left.args.value[0] && @.left.args.value[0].type === 'column_ref' && @.left.args.value[0].column === 'url' && @.left.args.value[1] && @.left.args.value[1].type === 'single_quote_string' && @.left.args.value[1].value === '/' && @.left.array_index && @.left.array_index[0] && @.left.array_index[0].brackets === true && @.left.array_index[0].index && @.left.array_index[0].index.type === 'number')]",
|
|
36
|
-
}); /*?*/
|
|
37
|
-
// log('pathPartCondition', pathPartCondition);
|
|
38
|
-
|
|
39
|
-
if (pathPartCondition !== undefined) {
|
|
40
|
-
const [pathPartIndex]: [number] = JSONPath({
|
|
41
|
-
json: pathPartCondition,
|
|
42
|
-
path: '$.left.array_index[0].index.value',
|
|
43
|
-
}); /*?*/
|
|
44
|
-
const [pathPartMatch]: [string] = JSONPath({
|
|
45
|
-
json: pathPartCondition,
|
|
46
|
-
path: '$.right.value',
|
|
47
|
-
}); /*?*/
|
|
48
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
49
|
-
return (path: string, _method: string) => {
|
|
50
|
-
const parts = path.split('/'); /*?*/
|
|
51
|
-
const part = parts[pathPartIndex - 1]; //athena index is larger than js index by one /*?*/
|
|
52
|
-
log(`checking path part`, { path, pathPartIndex, part, pathPartMatch });
|
|
53
|
-
return part?.startsWith(':') === true
|
|
54
|
-
? true // ignore path part if it presents a dynamic input parameter
|
|
55
|
-
: parts[pathPartIndex - 1] === pathPartMatch; // try to match with static path part
|
|
56
|
-
};
|
|
35
|
+
function getFunctionName(node: unknown): string | undefined {
|
|
36
|
+
const fn = rec(node);
|
|
37
|
+
if (fn?.['type'] !== 'function') {
|
|
38
|
+
return undefined;
|
|
57
39
|
}
|
|
40
|
+
const name = fn['name'] as SqlFunction['name'] | undefined;
|
|
41
|
+
const firstName = name?.name[0];
|
|
42
|
+
return firstName === undefined ? undefined : firstName.value.toLowerCase();
|
|
43
|
+
}
|
|
58
44
|
|
|
59
|
-
|
|
45
|
+
function getColumnName(node: unknown): string | undefined {
|
|
46
|
+
const col = rec(node);
|
|
47
|
+
if (col?.['type'] !== 'column_ref') {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const column = (node as ColumnRefItem).column;
|
|
51
|
+
return typeof column === 'string' ? column.toLowerCase() : undefined;
|
|
60
52
|
}
|
|
61
53
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
54
|
+
function getColumnTable(node: unknown): string | null | undefined {
|
|
55
|
+
const col = rec(node);
|
|
56
|
+
if (col?.['type'] !== 'column_ref') {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
return (node as ColumnRefItem).table;
|
|
60
|
+
}
|
|
69
61
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return parts.length === pathPartCount;
|
|
75
|
-
};
|
|
62
|
+
function getStringValue(node: unknown): string | undefined {
|
|
63
|
+
const val = rec(node);
|
|
64
|
+
if (val?.['type'] !== 'single_quote_string' && val?.['type'] !== 'string') {
|
|
65
|
+
return undefined;
|
|
76
66
|
}
|
|
67
|
+
return typeof val['value'] === 'string' ? val['value'] : undefined;
|
|
68
|
+
}
|
|
77
69
|
|
|
78
|
-
|
|
70
|
+
function getNumberValue(node: unknown): number | undefined {
|
|
71
|
+
const val = rec(node);
|
|
72
|
+
return val?.['type'] === 'number' && typeof val['value'] === 'number' ? val['value'] : undefined;
|
|
79
73
|
}
|
|
80
74
|
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
// Matches: split(url, '/')[N] — a Function node carrying an array_index extension
|
|
76
|
+
interface SplitUrlIndexed extends SqlFunction {
|
|
77
|
+
array_index: { brackets: true; index: { type: string; value: unknown } }[];
|
|
78
|
+
}
|
|
79
|
+
function isSplitUrlIndexed(node: unknown): node is SplitUrlIndexed {
|
|
80
|
+
const fn = rec(node);
|
|
81
|
+
if (fn?.['type'] !== 'function') {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (getFunctionName(node) !== 'split') {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const args = (fn['args'] as { value?: unknown[] } | undefined)?.value;
|
|
88
|
+
if (!Array.isArray(args) || args.length < 2) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (getColumnName(args[0]) !== 'url') {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (getStringValue(args[1]) !== '/') {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return Array.isArray(fn['array_index']) && (fn['array_index'] as unknown[]).length > 0;
|
|
83
98
|
}
|
|
84
99
|
|
|
85
|
-
//
|
|
86
|
-
function
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
100
|
+
// Matches: cardinality(split(url, '/'))
|
|
101
|
+
function isCardinalitySplitUrl(node: unknown): node is SqlFunction {
|
|
102
|
+
const fn = rec(node);
|
|
103
|
+
if (fn?.['type'] !== 'function') {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (getFunctionName(node) !== 'cardinality') {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const outerArgs = (fn['args'] as { value?: unknown[] } | undefined)?.value;
|
|
110
|
+
if (!Array.isArray(outerArgs) || outerArgs.length === 0) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
const innerFn = rec(outerArgs[0]);
|
|
114
|
+
if (innerFn?.['type'] !== 'function') {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
if (getFunctionName(outerArgs[0]) !== 'split') {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
const innerArgs = (innerFn['args'] as { value?: unknown[] } | undefined)?.value;
|
|
121
|
+
if (!Array.isArray(innerArgs) || innerArgs.length < 2) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return getColumnName(innerArgs[0]) === 'url' && getStringValue(innerArgs[1]) === '/';
|
|
125
|
+
}
|
|
91
126
|
|
|
92
|
-
|
|
93
|
-
|
|
127
|
+
// Returns the table qualifier of the left-hand side of a matchable binary condition.
|
|
128
|
+
// null means unqualified (applies to every table); undefined means indeterminate.
|
|
129
|
+
function getConditionTableQualifier(left: unknown): string | null | undefined {
|
|
130
|
+
const colName = getColumnName(left);
|
|
131
|
+
if (colName !== undefined) {
|
|
132
|
+
return getColumnTable(left);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (isSplitUrlIndexed(left)) {
|
|
136
|
+
const args = (rec(left)?.['args'] as { value?: unknown[] } | undefined)?.value;
|
|
137
|
+
return getColumnTable(args?.[0]) ?? null;
|
|
138
|
+
}
|
|
139
|
+
if (isCardinalitySplitUrl(left)) {
|
|
140
|
+
const outerArgs = (rec(left)?.['args'] as { value?: unknown[] } | undefined)?.value;
|
|
141
|
+
const splitFn = rec(outerArgs?.[0]);
|
|
142
|
+
const innerArgs = (splitFn?.['args'] as { value?: unknown[] } | undefined)?.value;
|
|
143
|
+
return getColumnTable(innerArgs?.[0]) ?? null;
|
|
94
144
|
}
|
|
95
145
|
return undefined;
|
|
96
146
|
}
|
|
97
147
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
148
|
+
// --- Predicate builders ---
|
|
149
|
+
|
|
150
|
+
// tableAlias: the alias (or null if none) of the FROM-clause item we are currently matching.
|
|
151
|
+
// Conditions that explicitly reference a different alias are skipped (treated as ALWAYS_TRUE).
|
|
152
|
+
function buildLeafPredicate(node: Binary, tableAlias: string | null): OperationPredicate | undefined {
|
|
153
|
+
if (node.operator !== '=') {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
const { left, right } = node;
|
|
157
|
+
|
|
158
|
+
const conditionTable = getConditionTableQualifier(left);
|
|
159
|
+
// conditionTable === null → unqualified, applies to all tables
|
|
160
|
+
// conditionTable === string → qualified; skip if it names a different alias
|
|
161
|
+
if (conditionTable !== null && conditionTable !== undefined && tableAlias !== null && conditionTable !== tableAlias) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// method = 'GET'
|
|
166
|
+
if (getColumnName(left) === 'method') {
|
|
167
|
+
const value = getStringValue(right);
|
|
168
|
+
if (value !== undefined) {
|
|
169
|
+
return (_path, method) => method === value;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// responsestatus = '200'
|
|
174
|
+
if (getColumnName(left) === 'responsestatus') {
|
|
175
|
+
const value = getStringValue(right);
|
|
176
|
+
if (value !== undefined) {
|
|
177
|
+
return (_path, _method, responseCode) => responseCode === value;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// split(url, '/')[N] = 'value'
|
|
182
|
+
if (isSplitUrlIndexed(left)) {
|
|
183
|
+
const index = left.array_index[0]?.index.value;
|
|
184
|
+
const value = getStringValue(right);
|
|
185
|
+
if (typeof index === 'number' && value !== undefined) {
|
|
186
|
+
return (path) => {
|
|
187
|
+
const parts = path.split('/');
|
|
188
|
+
const part = parts[index - 1]; // athena index is 1-based
|
|
189
|
+
log(`checking path part`, { path, index, part, value });
|
|
190
|
+
return part?.startsWith(':') === true ? true : part === value;
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
104
194
|
|
|
105
|
-
|
|
195
|
+
// cardinality(split(url, '/')) = N
|
|
196
|
+
if (isCardinalitySplitUrl(left)) {
|
|
197
|
+
const count = getNumberValue(right);
|
|
198
|
+
if (count !== undefined) {
|
|
199
|
+
return (path) => path.split('/').length === count;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildPredicate(expr: unknown, tableAlias: string | null): OperationPredicate {
|
|
207
|
+
const node = rec(expr);
|
|
208
|
+
if (node?.['type'] !== 'binary_expr') {
|
|
209
|
+
return ALWAYS_TRUE;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const binary = expr as Binary;
|
|
213
|
+
|
|
214
|
+
switch (binary.operator) {
|
|
215
|
+
case 'AND': {
|
|
216
|
+
const leftPred = buildPredicate(binary.left, tableAlias);
|
|
217
|
+
const rightPred = buildPredicate(binary.right, tableAlias);
|
|
218
|
+
return (path, method, code) => leftPred(path, method, code) && rightPred(path, method, code);
|
|
219
|
+
}
|
|
220
|
+
case 'OR': {
|
|
221
|
+
const leftPred = buildPredicate(binary.left, tableAlias);
|
|
222
|
+
const rightPred = buildPredicate(binary.right, tableAlias);
|
|
223
|
+
return (path, method, code) => leftPred(path, method, code) || rightPred(path, method, code);
|
|
224
|
+
}
|
|
225
|
+
case 'NOT': {
|
|
226
|
+
const innerPred = buildPredicate(binary.left, tableAlias);
|
|
227
|
+
return (path, method, code) => !innerPred(path, method, code);
|
|
228
|
+
}
|
|
229
|
+
default: {
|
|
230
|
+
return buildLeafPredicate(binary, tableAlias) ?? ALWAYS_TRUE;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
106
233
|
}
|
|
107
234
|
|
|
108
|
-
// [TODO:] match only relevent table in case multiple tables are joined
|
|
109
235
|
export function matchApi(
|
|
110
236
|
selectAST: object,
|
|
111
237
|
tableAST: object,
|
|
112
238
|
apiSchemas: ApiSchemas[],
|
|
113
239
|
): MatchedOperation[] | undefined {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
]
|
|
119
|
-
.flat()
|
|
120
|
-
.filter<Matcher>((matcher) => matcher !== undefined);
|
|
121
|
-
|
|
122
|
-
const allOperationSchemas: OperationToMatch[] = apiSchemas
|
|
240
|
+
const tableAlias = (tableAST as { as?: string | null }).as ?? null;
|
|
241
|
+
const predicate = buildPredicate((selectAST as { where?: unknown }).where ?? null, tableAlias);
|
|
242
|
+
|
|
243
|
+
const allOperations: OperationToMatch[] = apiSchemas
|
|
123
244
|
.flatMap((apiSchema) => Object.entries(apiSchema.apis))
|
|
124
245
|
.flatMap(([path, operations]) =>
|
|
125
246
|
Object.entries(operations).map(([method, operationSchemas]) => ({
|
|
@@ -128,41 +249,19 @@ export function matchApi(
|
|
|
128
249
|
operationSchemas,
|
|
129
250
|
})),
|
|
130
251
|
);
|
|
131
|
-
log('total operation schemas',
|
|
252
|
+
log('total operation schemas', allOperations.length);
|
|
132
253
|
|
|
133
|
-
const
|
|
134
|
-
|
|
254
|
+
const matchedApis = allOperations.flatMap(({ path, method, operationSchemas }) =>
|
|
255
|
+
Object.entries(operationSchemas.responses).flatMap(([responseCode, responseSchema]) =>
|
|
256
|
+
predicate(path, method, responseCode)
|
|
257
|
+
? [{ path, method, request: operationSchemas.request, response: responseSchema }]
|
|
258
|
+
: [],
|
|
259
|
+
),
|
|
135
260
|
);
|
|
136
|
-
log('matched
|
|
137
|
-
|
|
138
|
-
if (matchedOperationSchemas.length === 0) {
|
|
139
|
-
log('no matched operation schema');
|
|
140
|
-
throw new Error('no matched operation schema');
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const matchedResponseStatus = getResponseStatusToMatch(selectAST, tableAST);
|
|
144
|
-
// [TODO:] should we allow multiple response status?
|
|
145
|
-
// assert.ok(matchedResponseStatus !== undefined);
|
|
146
|
-
log('matchedResponseStatus', matchedResponseStatus);
|
|
147
|
-
|
|
148
|
-
const matchedApis = matchedOperationSchemas
|
|
149
|
-
.flatMap((operation) =>
|
|
150
|
-
Object.entries(operation.operationSchemas.responses).map(([responseCode, responseSchema]) => {
|
|
151
|
-
const matchedResponseSchema =
|
|
152
|
-
matchedResponseStatus === undefined || responseCode === matchedResponseStatus ? responseSchema : undefined;
|
|
153
|
-
return matchedResponseSchema === undefined
|
|
154
|
-
? undefined
|
|
155
|
-
: {
|
|
156
|
-
path: operation.path,
|
|
157
|
-
method: operation.method,
|
|
158
|
-
request: operation.operationSchemas.request,
|
|
159
|
-
response: matchedResponseSchema,
|
|
160
|
-
};
|
|
161
|
-
}),
|
|
162
|
-
)
|
|
163
|
-
.filter((api) => api !== undefined);
|
|
261
|
+
log('matched apis', matchedApis.length);
|
|
262
|
+
|
|
164
263
|
if (matchedApis.length === 0) {
|
|
165
|
-
log('no api
|
|
264
|
+
log('no matched api');
|
|
166
265
|
throw new Error('no matched api');
|
|
167
266
|
}
|
|
168
267
|
return matchedApis;
|