@checkdigit/eslint-plugin 7.18.0-PR.143-c535 → 7.18.0-PR.143-1b97
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 +203 -79
- package/package.json +1 -1
- package/src/athena/api-matcher.ts +242 -105
|
@@ -1,103 +1,227 @@
|
|
|
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 isSplitPartUrl(node) {
|
|
64
|
+
const fn = rec(node);
|
|
65
|
+
if (fn?.["type"] !== "function") {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (getFunctionName(node) !== "split_part") {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const args = fn["args"]?.value;
|
|
72
|
+
if (!Array.isArray(args) || args[2] === void 0) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
if (getColumnName(args[0]) !== "url") {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return getStringValue(args[1]) === "/" && getNumberValue(args[2]) !== void 0;
|
|
79
|
+
}
|
|
80
|
+
function isCardinalitySplitUrl(node) {
|
|
81
|
+
const fn = rec(node);
|
|
82
|
+
if (fn?.["type"] !== "function") {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (getFunctionName(node) !== "cardinality") {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const outerArgs = fn["args"]?.value;
|
|
89
|
+
if (!Array.isArray(outerArgs) || outerArgs.length === 0) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const innerFn = rec(outerArgs[0]);
|
|
93
|
+
if (innerFn?.["type"] !== "function") {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (getFunctionName(outerArgs[0]) !== "split") {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const innerArgs = innerFn["args"]?.value;
|
|
100
|
+
if (!Array.isArray(innerArgs) || innerArgs.length < 2) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return getColumnName(innerArgs[0]) === "url" && getStringValue(innerArgs[1]) === "/";
|
|
43
104
|
}
|
|
44
|
-
function
|
|
45
|
-
|
|
105
|
+
function getConditionTableQualifier(left) {
|
|
106
|
+
const colName = getColumnName(left);
|
|
107
|
+
if (colName !== void 0) {
|
|
108
|
+
return getColumnTable(left);
|
|
109
|
+
}
|
|
110
|
+
if (isSplitUrlIndexed(left)) {
|
|
111
|
+
const args = rec(left)?.["args"]?.value;
|
|
112
|
+
return getColumnTable(args?.[0]) ?? null;
|
|
113
|
+
}
|
|
114
|
+
if (isSplitPartUrl(left)) {
|
|
115
|
+
const args = rec(left)?.["args"]?.value;
|
|
116
|
+
return getColumnTable(args?.[0]) ?? null;
|
|
117
|
+
}
|
|
118
|
+
if (isCardinalitySplitUrl(left)) {
|
|
119
|
+
const outerArgs = rec(left)?.["args"]?.value;
|
|
120
|
+
const splitFn = rec(outerArgs?.[0]);
|
|
121
|
+
const innerArgs = splitFn?.["args"]?.value;
|
|
122
|
+
return getColumnTable(innerArgs?.[0]) ?? null;
|
|
123
|
+
}
|
|
124
|
+
return void 0;
|
|
46
125
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
126
|
+
function buildLeafPredicate(node, tableAlias) {
|
|
127
|
+
if (node.operator !== "=") {
|
|
128
|
+
return void 0;
|
|
129
|
+
}
|
|
130
|
+
const { left, right } = node;
|
|
131
|
+
const conditionTable = getConditionTableQualifier(left);
|
|
132
|
+
if (conditionTable !== null && conditionTable !== void 0 && tableAlias !== null && conditionTable !== tableAlias) {
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
if (getColumnName(left) === "method") {
|
|
136
|
+
const value = getStringValue(right);
|
|
137
|
+
if (value !== void 0) {
|
|
138
|
+
return (_path, method) => method === value;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (getColumnName(left) === "responsestatus") {
|
|
142
|
+
const value = getStringValue(right);
|
|
143
|
+
if (value !== void 0) {
|
|
144
|
+
return (_path, _method, responseCode) => responseCode === value;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (isSplitUrlIndexed(left)) {
|
|
148
|
+
const index = left.array_index[0]?.index.value;
|
|
149
|
+
const value = getStringValue(right);
|
|
150
|
+
if (typeof index === "number" && value !== void 0) {
|
|
151
|
+
return (path) => {
|
|
152
|
+
const parts = path.split("/");
|
|
153
|
+
const part = parts[index - 1];
|
|
154
|
+
log(`checking path part`, { path, index, part, value });
|
|
155
|
+
return part?.startsWith(":") === true ? true : part === value;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (isSplitPartUrl(left)) {
|
|
160
|
+
const args = rec(left)?.["args"]?.value;
|
|
161
|
+
const index = getNumberValue(args?.[2]);
|
|
162
|
+
const value = getStringValue(right);
|
|
163
|
+
if (index !== void 0 && value !== void 0) {
|
|
164
|
+
return (path) => {
|
|
165
|
+
const parts = path.split("/");
|
|
166
|
+
const part = parts[index - 1];
|
|
167
|
+
log("checking split_part path part", { path, index, part, value });
|
|
168
|
+
return part?.startsWith(":") === true ? true : part === value;
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (isCardinalitySplitUrl(left)) {
|
|
173
|
+
const count = getNumberValue(right);
|
|
174
|
+
if (count !== void 0) {
|
|
175
|
+
return (path) => path.split("/").length === count;
|
|
176
|
+
}
|
|
54
177
|
}
|
|
55
178
|
return void 0;
|
|
56
179
|
}
|
|
57
|
-
function
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
180
|
+
function buildPredicate(expr, tableAlias) {
|
|
181
|
+
const node = rec(expr);
|
|
182
|
+
if (node?.["type"] !== "binary_expr") {
|
|
183
|
+
return ALWAYS_TRUE;
|
|
184
|
+
}
|
|
185
|
+
const binary = expr;
|
|
186
|
+
switch (binary.operator) {
|
|
187
|
+
case "AND": {
|
|
188
|
+
const leftPred = buildPredicate(binary.left, tableAlias);
|
|
189
|
+
const rightPred = buildPredicate(binary.right, tableAlias);
|
|
190
|
+
return (path, method, code) => leftPred(path, method, code) && rightPred(path, method, code);
|
|
191
|
+
}
|
|
192
|
+
case "OR": {
|
|
193
|
+
const leftPred = buildPredicate(binary.left, tableAlias);
|
|
194
|
+
const rightPred = buildPredicate(binary.right, tableAlias);
|
|
195
|
+
return (path, method, code) => leftPred(path, method, code) || rightPred(path, method, code);
|
|
196
|
+
}
|
|
197
|
+
case "NOT": {
|
|
198
|
+
const innerPred = buildPredicate(binary.left, tableAlias);
|
|
199
|
+
return (path, method, code) => !innerPred(path, method, code);
|
|
200
|
+
}
|
|
201
|
+
default: {
|
|
202
|
+
return buildLeafPredicate(binary, tableAlias) ?? ALWAYS_TRUE;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
63
205
|
}
|
|
64
206
|
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(
|
|
207
|
+
const tableAlias = tableAST.as ?? null;
|
|
208
|
+
const predicate = buildPredicate(selectAST.where ?? null, tableAlias);
|
|
209
|
+
const allOperations = apiSchemas.flatMap((apiSchema) => Object.entries(apiSchema.apis)).flatMap(
|
|
71
210
|
([path, operations]) => Object.entries(operations).map(([method, operationSchemas]) => ({
|
|
72
211
|
path,
|
|
73
212
|
method: method.toUpperCase(),
|
|
74
213
|
operationSchemas
|
|
75
214
|
}))
|
|
76
215
|
);
|
|
77
|
-
log("total operation schemas",
|
|
78
|
-
const
|
|
79
|
-
({ path, method }) =>
|
|
216
|
+
log("total operation schemas", allOperations.length);
|
|
217
|
+
const matchedApis = allOperations.flatMap(
|
|
218
|
+
({ path, method, operationSchemas }) => Object.entries(operationSchemas.responses).flatMap(
|
|
219
|
+
([responseCode, responseSchema]) => predicate(path, method, responseCode) ? [{ path, method, request: operationSchemas.request, response: responseSchema }] : []
|
|
220
|
+
)
|
|
80
221
|
);
|
|
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);
|
|
222
|
+
log("matched apis", matchedApis.length);
|
|
99
223
|
if (matchedApis.length === 0) {
|
|
100
|
-
log("no api
|
|
224
|
+
log("no matched api");
|
|
101
225
|
throw new Error("no matched api");
|
|
102
226
|
}
|
|
103
227
|
return matchedApis;
|
|
@@ -105,4 +229,4 @@ function matchApi(selectAST, tableAST, apiSchemas) {
|
|
|
105
229
|
export {
|
|
106
230
|
matchApi
|
|
107
231
|
};
|
|
108
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
232
|
+
//# 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-1b97","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,264 @@ 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;
|
|
73
|
+
}
|
|
74
|
+
|
|
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;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Matches: split_part(url, '/', N) — Presto-style, index in args[2] (1-based)
|
|
101
|
+
function isSplitPartUrl(node: unknown): node is SqlFunction {
|
|
102
|
+
const fn = rec(node);
|
|
103
|
+
if (fn?.['type'] !== 'function') {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (getFunctionName(node) !== 'split_part') {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const args = (fn['args'] as { value?: unknown[] } | undefined)?.value;
|
|
110
|
+
if (!Array.isArray(args) || args[2] === undefined) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
if (getColumnName(args[0]) !== 'url') {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
return getStringValue(args[1]) === '/' && getNumberValue(args[2]) !== undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Matches: cardinality(split(url, '/'))
|
|
120
|
+
function isCardinalitySplitUrl(node: unknown): node is SqlFunction {
|
|
121
|
+
const fn = rec(node);
|
|
122
|
+
if (fn?.['type'] !== 'function') {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
if (getFunctionName(node) !== 'cardinality') {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const outerArgs = (fn['args'] as { value?: unknown[] } | undefined)?.value;
|
|
129
|
+
if (!Array.isArray(outerArgs) || outerArgs.length === 0) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const innerFn = rec(outerArgs[0]);
|
|
133
|
+
if (innerFn?.['type'] !== 'function') {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
if (getFunctionName(outerArgs[0]) !== 'split') {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const innerArgs = (innerFn['args'] as { value?: unknown[] } | undefined)?.value;
|
|
140
|
+
if (!Array.isArray(innerArgs) || innerArgs.length < 2) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return getColumnName(innerArgs[0]) === 'url' && getStringValue(innerArgs[1]) === '/';
|
|
79
144
|
}
|
|
80
145
|
|
|
81
|
-
|
|
82
|
-
|
|
146
|
+
// Returns the table qualifier of the left-hand side of a matchable binary condition.
|
|
147
|
+
// null means unqualified (applies to every table); undefined means indeterminate.
|
|
148
|
+
function getConditionTableQualifier(left: unknown): string | null | undefined {
|
|
149
|
+
const colName = getColumnName(left);
|
|
150
|
+
if (colName !== undefined) {
|
|
151
|
+
return getColumnTable(left);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (isSplitUrlIndexed(left)) {
|
|
155
|
+
const args = (rec(left)?.['args'] as { value?: unknown[] } | undefined)?.value;
|
|
156
|
+
return getColumnTable(args?.[0]) ?? null;
|
|
157
|
+
}
|
|
158
|
+
if (isSplitPartUrl(left)) {
|
|
159
|
+
const args = (rec(left)?.['args'] as { value?: unknown[] } | undefined)?.value;
|
|
160
|
+
return getColumnTable(args?.[0]) ?? null;
|
|
161
|
+
}
|
|
162
|
+
if (isCardinalitySplitUrl(left)) {
|
|
163
|
+
const outerArgs = (rec(left)?.['args'] as { value?: unknown[] } | undefined)?.value;
|
|
164
|
+
const splitFn = rec(outerArgs?.[0]);
|
|
165
|
+
const innerArgs = (splitFn?.['args'] as { value?: unknown[] } | undefined)?.value;
|
|
166
|
+
return getColumnTable(innerArgs?.[0]) ?? null;
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
83
169
|
}
|
|
84
170
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
171
|
+
// --- Predicate builders ---
|
|
172
|
+
|
|
173
|
+
// tableAlias: the alias (or null if none) of the FROM-clause item we are currently matching.
|
|
174
|
+
// Conditions that explicitly reference a different alias are skipped (treated as ALWAYS_TRUE).
|
|
175
|
+
function buildLeafPredicate(node: Binary, tableAlias: string | null): OperationPredicate | undefined {
|
|
176
|
+
if (node.operator !== '=') {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
const { left, right } = node;
|
|
180
|
+
|
|
181
|
+
const conditionTable = getConditionTableQualifier(left);
|
|
182
|
+
// conditionTable === null → unqualified, applies to all tables
|
|
183
|
+
// conditionTable === string → qualified; skip if it names a different alias
|
|
184
|
+
if (conditionTable !== null && conditionTable !== undefined && tableAlias !== null && conditionTable !== tableAlias) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// method = 'GET'
|
|
189
|
+
if (getColumnName(left) === 'method') {
|
|
190
|
+
const value = getStringValue(right);
|
|
191
|
+
if (value !== undefined) {
|
|
192
|
+
return (_path, method) => method === value;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// responsestatus = '200'
|
|
197
|
+
if (getColumnName(left) === 'responsestatus') {
|
|
198
|
+
const value = getStringValue(right);
|
|
199
|
+
if (value !== undefined) {
|
|
200
|
+
return (_path, _method, responseCode) => responseCode === value;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// split(url, '/')[N] = 'value'
|
|
205
|
+
if (isSplitUrlIndexed(left)) {
|
|
206
|
+
const index = left.array_index[0]?.index.value;
|
|
207
|
+
const value = getStringValue(right);
|
|
208
|
+
if (typeof index === 'number' && value !== undefined) {
|
|
209
|
+
return (path) => {
|
|
210
|
+
const parts = path.split('/');
|
|
211
|
+
const part = parts[index - 1]; // athena index is 1-based
|
|
212
|
+
log(`checking path part`, { path, index, part, value });
|
|
213
|
+
return part?.startsWith(':') === true ? true : part === value;
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// split_part(url, '/', N) = 'value'
|
|
219
|
+
if (isSplitPartUrl(left)) {
|
|
220
|
+
const args = (rec(left)?.['args'] as { value?: unknown[] } | undefined)?.value;
|
|
221
|
+
const index = getNumberValue(args?.[2]);
|
|
222
|
+
const value = getStringValue(right);
|
|
223
|
+
if (index !== undefined && value !== undefined) {
|
|
224
|
+
return (path) => {
|
|
225
|
+
const parts = path.split('/');
|
|
226
|
+
const part = parts[index - 1]; // 1-based
|
|
227
|
+
log('checking split_part path part', { path, index, part, value });
|
|
228
|
+
return part?.startsWith(':') === true ? true : part === value;
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
91
232
|
|
|
92
|
-
|
|
93
|
-
|
|
233
|
+
// cardinality(split(url, '/')) = N
|
|
234
|
+
if (isCardinalitySplitUrl(left)) {
|
|
235
|
+
const count = getNumberValue(right);
|
|
236
|
+
if (count !== undefined) {
|
|
237
|
+
return (path) => path.split('/').length === count;
|
|
238
|
+
}
|
|
94
239
|
}
|
|
240
|
+
|
|
95
241
|
return undefined;
|
|
96
242
|
}
|
|
97
243
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
244
|
+
function buildPredicate(expr: unknown, tableAlias: string | null): OperationPredicate {
|
|
245
|
+
const node = rec(expr);
|
|
246
|
+
if (node?.['type'] !== 'binary_expr') {
|
|
247
|
+
return ALWAYS_TRUE;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const binary = expr as Binary;
|
|
104
251
|
|
|
105
|
-
|
|
252
|
+
switch (binary.operator) {
|
|
253
|
+
case 'AND': {
|
|
254
|
+
const leftPred = buildPredicate(binary.left, tableAlias);
|
|
255
|
+
const rightPred = buildPredicate(binary.right, tableAlias);
|
|
256
|
+
return (path, method, code) => leftPred(path, method, code) && rightPred(path, method, code);
|
|
257
|
+
}
|
|
258
|
+
case 'OR': {
|
|
259
|
+
const leftPred = buildPredicate(binary.left, tableAlias);
|
|
260
|
+
const rightPred = buildPredicate(binary.right, tableAlias);
|
|
261
|
+
return (path, method, code) => leftPred(path, method, code) || rightPred(path, method, code);
|
|
262
|
+
}
|
|
263
|
+
case 'NOT': {
|
|
264
|
+
const innerPred = buildPredicate(binary.left, tableAlias);
|
|
265
|
+
return (path, method, code) => !innerPred(path, method, code);
|
|
266
|
+
}
|
|
267
|
+
default: {
|
|
268
|
+
return buildLeafPredicate(binary, tableAlias) ?? ALWAYS_TRUE;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
106
271
|
}
|
|
107
272
|
|
|
108
|
-
// [TODO:] match only relevent table in case multiple tables are joined
|
|
109
273
|
export function matchApi(
|
|
110
274
|
selectAST: object,
|
|
111
275
|
tableAST: object,
|
|
112
276
|
apiSchemas: ApiSchemas[],
|
|
113
277
|
): MatchedOperation[] | undefined {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
]
|
|
119
|
-
.flat()
|
|
120
|
-
.filter<Matcher>((matcher) => matcher !== undefined);
|
|
121
|
-
|
|
122
|
-
const allOperationSchemas: OperationToMatch[] = apiSchemas
|
|
278
|
+
const tableAlias = (tableAST as { as?: string | null }).as ?? null;
|
|
279
|
+
const predicate = buildPredicate((selectAST as { where?: unknown }).where ?? null, tableAlias);
|
|
280
|
+
|
|
281
|
+
const allOperations: OperationToMatch[] = apiSchemas
|
|
123
282
|
.flatMap((apiSchema) => Object.entries(apiSchema.apis))
|
|
124
283
|
.flatMap(([path, operations]) =>
|
|
125
284
|
Object.entries(operations).map(([method, operationSchemas]) => ({
|
|
@@ -128,41 +287,19 @@ export function matchApi(
|
|
|
128
287
|
operationSchemas,
|
|
129
288
|
})),
|
|
130
289
|
);
|
|
131
|
-
log('total operation schemas',
|
|
290
|
+
log('total operation schemas', allOperations.length);
|
|
132
291
|
|
|
133
|
-
const
|
|
134
|
-
|
|
292
|
+
const matchedApis = allOperations.flatMap(({ path, method, operationSchemas }) =>
|
|
293
|
+
Object.entries(operationSchemas.responses).flatMap(([responseCode, responseSchema]) =>
|
|
294
|
+
predicate(path, method, responseCode)
|
|
295
|
+
? [{ path, method, request: operationSchemas.request, response: responseSchema }]
|
|
296
|
+
: [],
|
|
297
|
+
),
|
|
135
298
|
);
|
|
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);
|
|
299
|
+
log('matched apis', matchedApis.length);
|
|
300
|
+
|
|
164
301
|
if (matchedApis.length === 0) {
|
|
165
|
-
log('no api
|
|
302
|
+
log('no matched api');
|
|
166
303
|
throw new Error('no matched api');
|
|
167
304
|
}
|
|
168
305
|
return matchedApis;
|