@checkdigit/eslint-plugin 7.18.0-PR.143-b47c → 7.18.0-PR.143-cf9d
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-locator.mjs +6 -9
- package/dist-mjs/athena/api-matcher.mjs +37 -77
- package/dist-mjs/athena/validate.mjs +130 -161
- package/dist-mjs/athena/visitor.mjs +32 -52
- package/dist-mjs/openapi/generate-schema.mjs +17 -23
- package/dist-mjs/openapi/service-schema-generator.mjs +7 -9
- package/dist-types/athena/visitor.d.ts +5 -3
- package/package.json +1 -1
- package/src/athena/api-locator.ts +5 -11
- package/src/athena/api-matcher.ts +45 -79
- package/src/athena/validate.ts +175 -170
- package/src/athena/visitor.ts +38 -71
- package/src/openapi/generate-schema.ts +18 -34
- package/src/openapi/service-schema-generator.ts +7 -8
|
@@ -42,21 +42,18 @@ function getFunctionName(node: unknown): string | undefined {
|
|
|
42
42
|
return firstName === undefined ? undefined : firstName.value.toLowerCase();
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function
|
|
45
|
+
function getColumnRef(node: unknown): ColumnRefItem | undefined {
|
|
46
46
|
const col = rec(node);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
return col?.['type'] === 'column_ref' ? (node as ColumnRefItem) : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getColumnName(node: unknown): string | undefined {
|
|
51
|
+
const column = getColumnRef(node)?.column;
|
|
51
52
|
return typeof column === 'string' ? column.toLowerCase() : undefined;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
function getColumnTable(node: unknown): string | null | undefined {
|
|
55
|
-
|
|
56
|
-
if (col?.['type'] !== 'column_ref') {
|
|
57
|
-
return undefined;
|
|
58
|
-
}
|
|
59
|
-
return (node as ColumnRefItem).table;
|
|
56
|
+
return getColumnRef(node)?.table;
|
|
60
57
|
}
|
|
61
58
|
|
|
62
59
|
function getStringValue(node: unknown): string | undefined {
|
|
@@ -72,75 +69,49 @@ function getNumberValue(node: unknown): number | undefined {
|
|
|
72
69
|
return val?.['type'] === 'number' && typeof val['value'] === 'number' ? val['value'] : undefined;
|
|
73
70
|
}
|
|
74
71
|
|
|
72
|
+
// Returns the function args when the node is split(url, '/') or split_part(url, '/'), else undefined.
|
|
73
|
+
function getSplitUrlFunctionArgs(node: unknown, functionName: 'split' | 'split_part'): unknown[] | undefined {
|
|
74
|
+
if (getFunctionName(node) !== functionName) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const args = (rec(node)?.['args'] as { value?: unknown[] } | undefined)?.value;
|
|
78
|
+
if (!Array.isArray(args) || args.length < 2) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
if (getColumnName(args[0]) !== 'url' || getStringValue(args[1]) !== '/') {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return args;
|
|
85
|
+
}
|
|
86
|
+
|
|
75
87
|
// Matches: split(url, '/')[N] — a Function node carrying an array_index extension
|
|
76
88
|
interface SplitUrlIndexed extends SqlFunction {
|
|
77
89
|
array_index: { brackets: true; index: { type: string; value: unknown } }[];
|
|
78
90
|
}
|
|
79
91
|
function isSplitUrlIndexed(node: unknown): node is SplitUrlIndexed {
|
|
80
|
-
|
|
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) {
|
|
92
|
+
if (getSplitUrlFunctionArgs(node, 'split') === undefined) {
|
|
89
93
|
return false;
|
|
90
94
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
if (getStringValue(args[1]) !== '/') {
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
return Array.isArray(fn['array_index']) && (fn['array_index'] as unknown[]).length > 0;
|
|
95
|
+
const fn = rec(node);
|
|
96
|
+
return Array.isArray(fn?.['array_index']) && (fn['array_index'] as unknown[]).length > 0;
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
// Matches: split_part(url, '/', N) — Presto-style, index in args[2] (1-based)
|
|
101
100
|
function isSplitPartUrl(node: unknown): node is SqlFunction {
|
|
102
|
-
const
|
|
103
|
-
|
|
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;
|
|
101
|
+
const args = getSplitUrlFunctionArgs(node, 'split_part');
|
|
102
|
+
return getNumberValue(args?.[2]) !== undefined;
|
|
117
103
|
}
|
|
118
104
|
|
|
119
105
|
// Matches: cardinality(split(url, '/'))
|
|
120
106
|
function isCardinalitySplitUrl(node: unknown): node is SqlFunction {
|
|
121
|
-
const fn = rec(node);
|
|
122
|
-
if (fn?.['type'] !== 'function') {
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
107
|
if (getFunctionName(node) !== 'cardinality') {
|
|
126
108
|
return false;
|
|
127
109
|
}
|
|
128
|
-
const outerArgs = (
|
|
110
|
+
const outerArgs = (rec(node)?.['args'] as { value?: unknown[] } | undefined)?.value;
|
|
129
111
|
if (!Array.isArray(outerArgs) || outerArgs.length === 0) {
|
|
130
112
|
return false;
|
|
131
113
|
}
|
|
132
|
-
|
|
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]) === '/';
|
|
114
|
+
return getSplitUrlFunctionArgs(outerArgs[0], 'split') !== undefined;
|
|
144
115
|
}
|
|
145
116
|
|
|
146
117
|
// Returns the table qualifier of the left-hand side of a matchable binary condition.
|
|
@@ -151,13 +122,9 @@ function getConditionTableQualifier(left: unknown): string | null | undefined {
|
|
|
151
122
|
return getColumnTable(left);
|
|
152
123
|
}
|
|
153
124
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return getColumnTable(
|
|
157
|
-
}
|
|
158
|
-
if (isSplitPartUrl(left)) {
|
|
159
|
-
const args = (rec(left)?.['args'] as { value?: unknown[] } | undefined)?.value;
|
|
160
|
-
return getColumnTable(args?.[0]) ?? null;
|
|
125
|
+
const splitArgs = getSplitUrlFunctionArgs(left, 'split') ?? getSplitUrlFunctionArgs(left, 'split_part');
|
|
126
|
+
if (splitArgs !== undefined) {
|
|
127
|
+
return getColumnTable(splitArgs[0]) ?? null;
|
|
161
128
|
}
|
|
162
129
|
if (isCardinalitySplitUrl(left)) {
|
|
163
130
|
const outerArgs = (rec(left)?.['args'] as { value?: unknown[] } | undefined)?.value;
|
|
@@ -170,6 +137,16 @@ function getConditionTableQualifier(left: unknown): string | null | undefined {
|
|
|
170
137
|
|
|
171
138
|
// --- Predicate builders ---
|
|
172
139
|
|
|
140
|
+
// Builds a predicate that checks whether the Nth path segment (1-based) equals a fixed value.
|
|
141
|
+
// Path parameters (`:param`) are treated as wildcards and always match.
|
|
142
|
+
function buildPathSegmentPredicate(index: number, value: string): OperationPredicate {
|
|
143
|
+
return (path) => {
|
|
144
|
+
const part = path.split('/')[index - 1];
|
|
145
|
+
log('checking path segment', { path, index, part, value });
|
|
146
|
+
return part?.startsWith(':') === true ? true : part === value;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
173
150
|
// tableAlias: the alias (or null if none) of the FROM-clause item we are currently matching.
|
|
174
151
|
// Conditions that explicitly reference a different alias are skipped (treated as ALWAYS_TRUE).
|
|
175
152
|
function buildLeafPredicate(node: Binary, tableAlias: string | null): OperationPredicate | undefined {
|
|
@@ -206,27 +183,16 @@ function buildLeafPredicate(node: Binary, tableAlias: string | null): OperationP
|
|
|
206
183
|
const index = left.array_index[0]?.index.value;
|
|
207
184
|
const value = getStringValue(right);
|
|
208
185
|
if (typeof index === 'number' && value !== undefined) {
|
|
209
|
-
return (
|
|
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
|
-
};
|
|
186
|
+
return buildPathSegmentPredicate(index, value);
|
|
215
187
|
}
|
|
216
188
|
}
|
|
217
189
|
|
|
218
190
|
// split_part(url, '/', N) = 'value'
|
|
219
191
|
if (isSplitPartUrl(left)) {
|
|
220
|
-
const
|
|
221
|
-
const index = getNumberValue(args?.[2]);
|
|
192
|
+
const index = getNumberValue(getSplitUrlFunctionArgs(left, 'split_part')?.[2]);
|
|
222
193
|
const value = getStringValue(right);
|
|
223
194
|
if (index !== undefined && value !== undefined) {
|
|
224
|
-
return (
|
|
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
|
-
};
|
|
195
|
+
return buildPathSegmentPredicate(index, value);
|
|
230
196
|
}
|
|
231
197
|
}
|
|
232
198
|
|