@checkdigit/eslint-plugin 7.18.0-PR.143-a26e → 7.18.0-PR.143-bebb
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/validate.mjs +44 -12
- package/dist-mjs/peggy/athena-peggy.mjs +712 -642
- package/package.json +1 -1
- package/src/athena/validate.ts +52 -11
- package/src/peggy/athena-peggy.ts +711 -641
- package/src/peggy/athena.peggy +3 -3
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-bebb","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":">=24.14.1"},"service":{"api":{"root":"src","endpoints":["api/v1"]}}}
|
package/src/athena/validate.ts
CHANGED
|
@@ -127,6 +127,23 @@ function getFunctionAliasName(as: { name: { name: { value: string }[] } } | null
|
|
|
127
127
|
return as?.name.name[0]?.value;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
// Extracts a column alias name from an alias-arg node. Two cases arise:
|
|
131
|
+
// 1. column_ref (the normal case): { type: 'column_ref', column: 'dates' } → 'dates'
|
|
132
|
+
// 2. zero-arg keyword function: SQL type keywords like `date` are parsed as DATE() function
|
|
133
|
+
// calls; recover the column name by lowercasing the function name.
|
|
134
|
+
function extractColumnAliasName(arg: unknown): string | undefined {
|
|
135
|
+
const colRef = arg as { column?: unknown };
|
|
136
|
+
if (typeof colRef.column === 'string') {
|
|
137
|
+
return colRef.column;
|
|
138
|
+
}
|
|
139
|
+
const fnNode = arg as { type?: unknown; name?: { name?: { value?: string }[] }; args?: { value?: unknown[] } };
|
|
140
|
+
if (fnNode.type === 'function' && fnNode.args?.value?.length === 0) {
|
|
141
|
+
const functionName = fnNode.name?.name?.[0]?.value;
|
|
142
|
+
return functionName !== undefined ? functionName.toLowerCase() : undefined;
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
130
147
|
// UNNEST whose argument is a function call (e.g. SEQUENCE), not a column reference.
|
|
131
148
|
function isStandaloneUnnest(node: unknown): node is UnnestFrom {
|
|
132
149
|
return isUnnestFrom(node) && typeof (node.expr as { column?: unknown }).column !== 'string';
|
|
@@ -190,7 +207,7 @@ function resolveFromClause(select: Select, ctx: VisitContext): void {
|
|
|
190
207
|
if (tableAlias !== undefined) {
|
|
191
208
|
const columns = new Map(
|
|
192
209
|
(unknownItem.as?.args.value ?? []).map((columnRef) => {
|
|
193
|
-
const colName = (columnRef
|
|
210
|
+
const colName = extractColumnAliasName(columnRef) ?? '';
|
|
194
211
|
return [colName, [resolvedCol(colName, {})]];
|
|
195
212
|
}),
|
|
196
213
|
);
|
|
@@ -223,6 +240,7 @@ function resolveFromClause(select: Select, ctx: VisitContext): void {
|
|
|
223
240
|
interface UnnestMapping {
|
|
224
241
|
fromColumn: string;
|
|
225
242
|
toColumns: string[];
|
|
243
|
+
tableAlias?: string;
|
|
226
244
|
ast: object;
|
|
227
245
|
}
|
|
228
246
|
|
|
@@ -246,13 +264,20 @@ function extractUnnestMappings(select: Select): UnnestMapping[] {
|
|
|
246
264
|
const aliasArgs = item.as?.args.value ?? [];
|
|
247
265
|
const toColumns: string[] = [];
|
|
248
266
|
for (const col of aliasArgs) {
|
|
249
|
-
|
|
250
|
-
|
|
267
|
+
const colName = extractColumnAliasName(col);
|
|
268
|
+
if (colName !== undefined) {
|
|
269
|
+
toColumns.push(colName);
|
|
251
270
|
}
|
|
252
271
|
}
|
|
253
272
|
assert.ok(toColumns.length > 0, 'UNNEST alias must have at least one column name');
|
|
254
273
|
|
|
255
|
-
|
|
274
|
+
const tableAlias = getFunctionAliasName(item.as);
|
|
275
|
+
mappings.push({
|
|
276
|
+
fromColumn,
|
|
277
|
+
toColumns,
|
|
278
|
+
...(tableAlias !== undefined ? { tableAlias } : {}),
|
|
279
|
+
ast: item.expr,
|
|
280
|
+
});
|
|
256
281
|
}
|
|
257
282
|
|
|
258
283
|
return mappings;
|
|
@@ -261,11 +286,11 @@ function extractUnnestMappings(select: Select): UnnestMapping[] {
|
|
|
261
286
|
function applyUnnestPre(mappings: UnnestMapping[], ctx: VisitContext): UnnestMapping[] {
|
|
262
287
|
const deferred: UnnestMapping[] = [];
|
|
263
288
|
|
|
264
|
-
for (const { fromColumn, toColumns, ast } of mappings) {
|
|
289
|
+
for (const { fromColumn, toColumns, tableAlias, ast } of mappings) {
|
|
265
290
|
const ownerTable = [...ctx.tables.values()].flat().find((table) => table.columns.has(fromColumn));
|
|
266
291
|
|
|
267
292
|
if (ownerTable === undefined) {
|
|
268
|
-
deferred.push({ fromColumn, toColumns, ast });
|
|
293
|
+
deferred.push({ fromColumn, toColumns, ...(tableAlias !== undefined ? { tableAlias } : {}), ast });
|
|
269
294
|
continue;
|
|
270
295
|
}
|
|
271
296
|
|
|
@@ -274,16 +299,17 @@ function applyUnnestPre(mappings: UnnestMapping[], ctx: VisitContext): UnnestMap
|
|
|
274
299
|
const unnestTableName = `${ownerTable.name ?? ANONYMOUS_TABLE}:<unnested>`;
|
|
275
300
|
const apiOperation = ownerTable.apiOperation !== undefined ? { apiOperation: ownerTable.apiOperation } : {};
|
|
276
301
|
|
|
302
|
+
let unnestEntry: ResolvedTable[];
|
|
277
303
|
if (sourceSchema?.type === 'array') {
|
|
278
304
|
const [toColumn] = toColumns;
|
|
279
305
|
assert.ok(toColumn !== undefined);
|
|
280
|
-
|
|
306
|
+
unnestEntry = [
|
|
281
307
|
{
|
|
282
308
|
name: unnestTableName,
|
|
283
309
|
...apiOperation,
|
|
284
310
|
columns: new Map([[toColumn, [resolvedCol(toColumn, sourceSchema.items, sourceColumns[0]?.ast)]]]),
|
|
285
311
|
},
|
|
286
|
-
]
|
|
312
|
+
];
|
|
287
313
|
} else if (sourceSchema?.type === 'object') {
|
|
288
314
|
const [keyColumn, valueColumn] = toColumns;
|
|
289
315
|
assert.ok(
|
|
@@ -293,7 +319,7 @@ function applyUnnestPre(mappings: UnnestMapping[], ctx: VisitContext): UnnestMap
|
|
|
293
319
|
const addlProps = (sourceSchema as SchemaObject)['additionalProperties'] as unknown;
|
|
294
320
|
const valueSchema: SchemaObject =
|
|
295
321
|
typeof addlProps === 'object' && addlProps !== null ? addlProps : { type: 'string' };
|
|
296
|
-
|
|
322
|
+
unnestEntry = [
|
|
297
323
|
{
|
|
298
324
|
name: unnestTableName,
|
|
299
325
|
...apiOperation,
|
|
@@ -302,13 +328,28 @@ function applyUnnestPre(mappings: UnnestMapping[], ctx: VisitContext): UnnestMap
|
|
|
302
328
|
[valueColumn, [resolvedCol(valueColumn, valueSchema, sourceColumns[0]?.ast)]],
|
|
303
329
|
]),
|
|
304
330
|
},
|
|
305
|
-
]
|
|
306
|
-
} else {
|
|
331
|
+
];
|
|
332
|
+
} else if (ownerTable.apiOperation !== undefined) {
|
|
333
|
+
// Only raise an error when the column schema was derived from a known API spec — for
|
|
334
|
+
// computed columns (subqueries, CTEs, functions) the inferred schema may be imprecise.
|
|
307
335
|
throw new AthenaError(
|
|
308
336
|
ATHENA_ERROR,
|
|
309
337
|
`UNNEST source column '${fromColumn}' must resolve to an array or map schema`,
|
|
310
338
|
ast,
|
|
311
339
|
);
|
|
340
|
+
} else {
|
|
341
|
+
// Non-API source: schema is an estimate; register target columns with unknown schema.
|
|
342
|
+
unnestEntry = [
|
|
343
|
+
{
|
|
344
|
+
name: unnestTableName,
|
|
345
|
+
...apiOperation,
|
|
346
|
+
columns: new Map(toColumns.map((toColumn) => [toColumn, [resolvedCol(toColumn, {})]])),
|
|
347
|
+
},
|
|
348
|
+
];
|
|
349
|
+
}
|
|
350
|
+
ctx.tables.set(unnestTableName, unnestEntry);
|
|
351
|
+
if (tableAlias !== undefined) {
|
|
352
|
+
ctx.tables.set(tableAlias, unnestEntry);
|
|
312
353
|
}
|
|
313
354
|
}
|
|
314
355
|
|