@checkdigit/eslint-plugin 7.17.1 → 7.18.0-PR.143-9946
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 +30 -0
- package/dist-mjs/athena/api-matcher.mjs +108 -0
- package/dist-mjs/athena/athena.mjs +328 -0
- package/dist-mjs/athena/column.mjs +1 -0
- package/dist-mjs/athena/context.mjs +21 -0
- package/dist-mjs/athena/index.mjs +1 -0
- package/dist-mjs/athena/service-table.mjs +32 -0
- package/dist-mjs/athena/types.mjs +1 -0
- package/dist-mjs/athena/visitor.mjs +258 -0
- package/dist-mjs/index.mjs +8 -4
- package/dist-mjs/no-status-code-assert.mjs +1 -1
- package/dist-mjs/openapi/deref-schema.mjs +14 -0
- package/dist-mjs/openapi/generate-schema.mjs +273 -0
- package/dist-mjs/openapi/service-schema-generator.mjs +147 -0
- package/dist-mjs/peggy/athena-peggy.mjs +20629 -0
- package/dist-types/athena/api-locator.d.ts +2 -0
- package/dist-types/athena/api-matcher.d.ts +14 -0
- package/dist-types/athena/athena.d.ts +6 -0
- package/dist-types/athena/column.d.ts +1 -0
- package/dist-types/athena/context.d.ts +21 -0
- package/dist-types/athena/index.d.ts +8 -0
- package/dist-types/athena/service-table.d.ts +8 -0
- package/dist-types/athena/types.d.ts +474 -0
- package/dist-types/athena/visitor.d.ts +63 -0
- package/dist-types/no-status-code-assert.d.ts +1 -1
- package/dist-types/openapi/deref-schema.d.ts +1 -0
- package/dist-types/openapi/generate-schema.d.ts +33 -0
- package/dist-types/openapi/service-schema-generator.d.ts +5 -0
- package/dist-types/peggy/athena-peggy.d.ts +13 -0
- package/package.json +1 -96
- package/src/athena/ATHENA.md +387 -0
- package/src/athena/PLAN.md +355 -0
- package/src/athena/api-locator.ts +39 -0
- package/src/athena/api-matcher.ts +169 -0
- package/src/athena/athena.ts +488 -0
- package/src/athena/column.ts +2 -0
- package/src/athena/context.ts +47 -0
- package/src/athena/index.ts +11 -0
- package/src/athena/service-table.ts +55 -0
- package/src/athena/types.ts +526 -0
- package/src/athena/visitor.ts +365 -0
- package/src/index.ts +4 -0
- package/src/no-side-effects.ts +1 -1
- package/src/no-status-code-assert.ts +2 -2
- package/src/openapi/deref-schema.ts +14 -0
- package/src/openapi/generate-schema.ts +422 -0
- package/src/openapi/service-schema-generator.ts +189 -0
- package/src/peggy/athena-chat.peggy +608 -0
- package/src/peggy/athena-peggy.ts +22078 -0
- package/src/peggy/athena.peggy +2967 -0
- package/src/require-service-call-response-declaration.ts +2 -2
- package/src/services/interchange/v1/swagger.schema.deref.json +849 -0
- package/src/services/interchange/v1/swagger.schema.json +473 -0
- package/src/services/interchange/v1/swagger.yml +414 -0
- package/src/services/ledger/v1/swagger.schema.deref.json +6694 -0
- package/src/services/ledger/v1/swagger.schema.json +1820 -0
- package/src/services/ledger/v1/swagger.yml +1094 -0
- package/src/services/link/v1/swagger.schema.deref.json +648 -0
- package/src/services/link/v1/swagger.schema.json +444 -0
- package/src/services/link/v1/swagger.yml +343 -0
- package/src/services/message/v1/swagger.schema.deref.json +22049 -0
- package/src/services/message/v1/swagger.schema.json +3470 -0
- package/src/services/message/v1/swagger.yml +2798 -0
- package/src/services/message/v2/swagger.schema.deref.json +72221 -0
- package/src/services/message/v2/swagger.schema.json +3558 -0
- package/src/services/message/v2/swagger.yml +3009 -0
- package/src/services/paymentCard/v1/swagger.schema.deref.json +4346 -0
- package/src/services/paymentCard/v1/swagger.schema.json +2181 -0
- package/src/services/paymentCard/v1/swagger.yml +1161 -0
- package/src/services/paymentCard/v2/swagger.schema.deref.json +4336 -0
- package/src/services/paymentCard/v2/swagger.schema.json +2155 -0
- package/src/services/paymentCard/v2/swagger.yml +1149 -0
- package/src/services/person/v1/swagger.schema.deref.json +6786 -0
- package/src/services/person/v1/swagger.schema.json +1445 -0
- package/src/services/person/v1/swagger.yml +1157 -0
- package/src/services/teampayApproval/v1/swagger.schema.deref.json +9898 -0
- package/src/services/teampayCardManagement/v1/swagger.schema.deref.json +6187 -0
- package/src/services/teampayClientManagement/v1/swagger.schema.deref.json +4914 -0
- package/src/services/teampayClientManagement/v1/swagger.schema.json +1964 -0
- package/src/services/teampayClientManagement/v1/swagger.yml +1376 -0
package/package.json
CHANGED
|
@@ -1,96 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@checkdigit/eslint-plugin",
|
|
3
|
-
"version": "7.17.1",
|
|
4
|
-
"description": "Check Digit eslint plugins",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"eslint",
|
|
7
|
-
"eslintplugin"
|
|
8
|
-
],
|
|
9
|
-
"homepage": "https://github.com/checkdigit/eslint-plugin#readme",
|
|
10
|
-
"bugs": {
|
|
11
|
-
"url": "https://github.com/checkdigit/eslint-plugin/issues"
|
|
12
|
-
},
|
|
13
|
-
"repository": {
|
|
14
|
-
"type": "git",
|
|
15
|
-
"url": "https://github.com/checkdigit/eslint-plugin"
|
|
16
|
-
},
|
|
17
|
-
"license": "MIT",
|
|
18
|
-
"author": "Check Digit, LLC",
|
|
19
|
-
"sideEffects": false,
|
|
20
|
-
"type": "module",
|
|
21
|
-
"exports": {
|
|
22
|
-
".": {
|
|
23
|
-
"types": "./dist-types/index.d.ts",
|
|
24
|
-
"import": "./dist-mjs/index.mjs",
|
|
25
|
-
"default": "./dist-mjs/index.mjs"
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
"files": [
|
|
29
|
-
"src",
|
|
30
|
-
"dist-types",
|
|
31
|
-
"dist-mjs",
|
|
32
|
-
"!src/**/test/**",
|
|
33
|
-
"!src/**/*.test.ts",
|
|
34
|
-
"!src/**/*.spec.ts",
|
|
35
|
-
"!dist-types/**/test/**",
|
|
36
|
-
"!dist-types/**/*.test.d.ts",
|
|
37
|
-
"!dist-types/**/*.spec.d.ts",
|
|
38
|
-
"!dist-mjs/**/test/**",
|
|
39
|
-
"!dist-mjs/**/*.test.mjs",
|
|
40
|
-
"!dist-mjs/**/*.spec.mjs",
|
|
41
|
-
"SECURITY.md"
|
|
42
|
-
],
|
|
43
|
-
"scripts": {
|
|
44
|
-
"build:dist-mjs": "rimraf dist-mjs && npx builder --type=module --sourceMap --outDir=dist-mjs && node dist-mjs/index.mjs",
|
|
45
|
-
"build:dist-types": "rimraf dist-types && npx builder --type=types --outDir=dist-types",
|
|
46
|
-
"ci:compile": "tsc --noEmit",
|
|
47
|
-
"ci:coverage": "NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=true",
|
|
48
|
-
"ci:lint": "npm run lint",
|
|
49
|
-
"ci:style": "npm run prettier",
|
|
50
|
-
"ci:test": "NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=false",
|
|
51
|
-
"lint": "eslint --max-warnings 0 .",
|
|
52
|
-
"lint:fix": "eslint --max-warnings 0 --fix .",
|
|
53
|
-
"prepare": "",
|
|
54
|
-
"prepublishOnly": "npm run build:dist-types && npm run build:dist-mjs",
|
|
55
|
-
"prettier": "prettier --ignore-path .gitignore --list-different .",
|
|
56
|
-
"prettier:fix": "prettier --ignore-path .gitignore --write .",
|
|
57
|
-
"test": "npm run ci:compile && npm run ci:test && npm run ci:lint && npm run ci:style"
|
|
58
|
-
},
|
|
59
|
-
"prettier": "@checkdigit/prettier-config",
|
|
60
|
-
"jest": {
|
|
61
|
-
"preset": "@checkdigit/jest-config"
|
|
62
|
-
},
|
|
63
|
-
"dependencies": {
|
|
64
|
-
"@typescript-eslint/type-utils": "^8.46.0",
|
|
65
|
-
"@typescript-eslint/utils": "^8.46.0",
|
|
66
|
-
"http-status-codes": "^2.3.0",
|
|
67
|
-
"ts-api-utils": "^2.1.0"
|
|
68
|
-
},
|
|
69
|
-
"devDependencies": {
|
|
70
|
-
"@checkdigit/jest-config": "^6.0.2",
|
|
71
|
-
"@checkdigit/prettier-config": "^6.1.0",
|
|
72
|
-
"@checkdigit/typescript-config": "^9.3.2",
|
|
73
|
-
"@eslint/js": "^9.37.0",
|
|
74
|
-
"@types/eslint": "^9.6.1",
|
|
75
|
-
"@types/eslint-config-prettier": "^6.11.3",
|
|
76
|
-
"@typescript-eslint/parser": "^8.46.0",
|
|
77
|
-
"@typescript-eslint/rule-tester": "^8.46.0",
|
|
78
|
-
"eslint": "^9.37.0",
|
|
79
|
-
"eslint-config-prettier": "^10.1.8",
|
|
80
|
-
"eslint-import-resolver-typescript": "^4.4.4",
|
|
81
|
-
"eslint-plugin-eslint-plugin": "^6.4.0",
|
|
82
|
-
"eslint-plugin-import": "^2.32.0",
|
|
83
|
-
"eslint-plugin-no-only-tests": "^3.3.0",
|
|
84
|
-
"eslint-plugin-no-secrets": "^2.2.1",
|
|
85
|
-
"eslint-plugin-node": "^11.1.0",
|
|
86
|
-
"eslint-plugin-sonarjs": "1.0.4",
|
|
87
|
-
"rimraf": "^6.0.1",
|
|
88
|
-
"typescript-eslint": "^8.46.0"
|
|
89
|
-
},
|
|
90
|
-
"peerDependencies": {
|
|
91
|
-
"eslint": ">=9 <10"
|
|
92
|
-
},
|
|
93
|
-
"engines": {
|
|
94
|
-
"node": ">=22.18"
|
|
95
|
-
}
|
|
96
|
-
}
|
|
1
|
+
{"name":"@checkdigit/eslint-plugin","version":"7.18.0-PR.143-9946","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"}}}}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# Athena Rule — Design & Implementation Reference
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
The `athena` ESLint rule validates AWS Athena SQL strings embedded in TypeScript source code (as template literals or string literals beginning with `SELECT` or `WITH`) against OpenAPI schemas at lint time. It catches references to non-existent response/request body properties before the query is ever run in production.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Trigger Conditions
|
|
10
|
+
|
|
11
|
+
The rule is activated on:
|
|
12
|
+
|
|
13
|
+
- **Template literals** — every `` `...` `` in the file is inspected; the quasis (the static parts between `${...}` interpolations) are joined and trimmed.
|
|
14
|
+
- **String literals** — plain `'...'` or `"..."` strings are inspected if the value is a string type.
|
|
15
|
+
|
|
16
|
+
In both cases the rule is a no-op unless the resulting string starts with `SELECT ` or `WITH ` (case-insensitive).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## File Structure
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
src/athena/
|
|
24
|
+
athena.ts Entry point: ESLint rule, SQL parse, checkAthenaAst
|
|
25
|
+
visitor.ts VisitorMap interface, walk() dispatcher, type guards, extractors
|
|
26
|
+
context.ts VisitContext, ResolvedTable, ResolvedColumn, context factories
|
|
27
|
+
service-table.ts buildServiceTables() — maps MatchedOperation[] → ResolvedTable[]
|
|
28
|
+
api-locator.ts locateApi() — finds swagger.schema.deref.json on disk
|
|
29
|
+
api-matcher.ts matchApi() — filters OpenAPI operations by WHERE-clause conditions
|
|
30
|
+
types.ts TypeScript types for the PEG parser AST (based on node-sql-parser)
|
|
31
|
+
athena.spec.ts Integration tests via RuleTester
|
|
32
|
+
zzz.spec.ts Unit tests for walk() and the extractor helpers
|
|
33
|
+
json.spec.ts Dev scratch tests for JSONPath exploration (all skipped)
|
|
34
|
+
PLAN.md Architecture planning document
|
|
35
|
+
ATHENA.md This file
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## End-to-End Data Flow
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
TypeScript source
|
|
44
|
+
│
|
|
45
|
+
│ TemplateLiteral / Literal ESLint visitor
|
|
46
|
+
▼
|
|
47
|
+
sql: string (quasis joined, interpolation placeholders stripped)
|
|
48
|
+
│
|
|
49
|
+
│ parse(sql, { includeLocations: true }) ← src/peggy/athena-peggy.ts
|
|
50
|
+
▼
|
|
51
|
+
AST: Select | With[]...
|
|
52
|
+
│
|
|
53
|
+
│ checkAthenaAst(ast, ctx)
|
|
54
|
+
▼
|
|
55
|
+
WITH items processed first (each CTE → checkSelect → ctx.tables)
|
|
56
|
+
│
|
|
57
|
+
│ checkSelect(select, ctx)
|
|
58
|
+
▼
|
|
59
|
+
┌────────────────────────────────────────────┐
|
|
60
|
+
│ Pass 1 — resolveFromClause │
|
|
61
|
+
│ BaseFrom nodes → locateApi + matchApi │
|
|
62
|
+
│ → buildServiceTables → ctx.tables │
|
|
63
|
+
│ │
|
|
64
|
+
│ UNNEST pre-pass — applyUnnestPre │
|
|
65
|
+
│ source is a service-table column │
|
|
66
|
+
│ → synthetic ctx.tables[name:<unnested>] │
|
|
67
|
+
│ │
|
|
68
|
+
│ Pass 2 — resolveSelectColumns │
|
|
69
|
+
│ for each SELECT column: │
|
|
70
|
+
│ extractColumnRefs → find ref │
|
|
71
|
+
│ → lookup in ctx.tables │
|
|
72
|
+
│ → extractJsonExtractPath / bracket │
|
|
73
|
+
│ → JSONPath into schema │
|
|
74
|
+
│ │
|
|
75
|
+
│ UNNEST post-pass — applyUnnestPost │
|
|
76
|
+
│ source is a computed SELECT column │
|
|
77
|
+
│ │
|
|
78
|
+
│ _next (UNION ALL) → recurse checkSelect │
|
|
79
|
+
│ │
|
|
80
|
+
│ register result in ctx.tables[cteTableName]│
|
|
81
|
+
└────────────────────────────────────────────┘
|
|
82
|
+
│
|
|
83
|
+
AthenaError thrown → context.report(AthenaError)
|
|
84
|
+
parse error thrown → context.report(SyntextError)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## PEG Grammar
|
|
90
|
+
|
|
91
|
+
The SQL is parsed by `src/peggy/athena.peggy`, a hand-written PEG grammar compiled to `src/peggy/athena-peggy.ts` via `npm run peggy` (`peggy --format es`). The grammar is an extension of the `node-sql-parser` grammar with Athena/Trino-specific syntax (MAP, ARRAY, TRY_CAST, UNNEST, bracket array access, etc.).
|
|
92
|
+
|
|
93
|
+
The second grammar `src/peggy/athena-chat.peggy` is an experimental rewrite with a cleaner AST format; it is **not** used by the rule.
|
|
94
|
+
|
|
95
|
+
The compiled parser's `parse(sql, opts)` returns `{ ast: AST | AST[] }`. When the input is a single statement the result is a single `Select` node; when there are multiple (e.g. `;`-separated) it is an array.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## AST Node Shapes (relevant subset)
|
|
100
|
+
|
|
101
|
+
All types are in `src/athena/types.ts`.
|
|
102
|
+
|
|
103
|
+
| Shape | Key fields |
|
|
104
|
+
| ------------------------------ | -------------------------------------------------------------------------- |
|
|
105
|
+
| `Select` | `type:'select'`, `with`, `columns`, `from`, `where`, `_next` (UNION chain) |
|
|
106
|
+
| `With` | `name.value` (CTE name), `stmt.ast` (inner Select) |
|
|
107
|
+
| `BaseFrom` | `table` (string), `as` (alias or null) |
|
|
108
|
+
| `Join` extends `BaseFrom` | `join` (join type string), `on` |
|
|
109
|
+
| `TableExpr` | `expr.ast` (sub-Select), `as` |
|
|
110
|
+
| `UnnestFrom` (not in types.ts) | `type:'unnest'`, `expr` (ColumnRefItem), `as` (func_call alias) |
|
|
111
|
+
| `Column` | `type:'expr'`, `expr` (ExpressionValue), `as` (alias) |
|
|
112
|
+
| `ColumnRefItem` | `type:'column_ref'`, `table`, `column`, optionally `array_index` |
|
|
113
|
+
| `Function` | `type:'function'`, `name.name[0].value` (fn name), `args.value[]` |
|
|
114
|
+
| `Binary` | `type:'binary_expr'`, `operator`, `left`, `right` |
|
|
115
|
+
| `Cast` | `type:'cast'`, `expr`, `target.dataType` |
|
|
116
|
+
| `Case` | `type:'case'`, `args[]` (when/else arms) |
|
|
117
|
+
|
|
118
|
+
`ColumnRefItem.array_index` is added by the parser for bracket access (`col['key']`); it is not in the TypeScript type definition and is accessed via the `ColumnRefWithIndex` interface in `visitor.ts`.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Visitor Pattern (`visitor.ts`)
|
|
123
|
+
|
|
124
|
+
### `VisitorMap`
|
|
125
|
+
|
|
126
|
+
An interface with optional typed hooks, one per AST node kind:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
interface VisitorMap {
|
|
130
|
+
visitSelect?(node: Select): void;
|
|
131
|
+
visitWith?(node: With): void;
|
|
132
|
+
visitBaseFrom?(node: BaseFrom): void;
|
|
133
|
+
visitJoin?(node: Join): void;
|
|
134
|
+
visitTableExpr?(node: TableExpr): void;
|
|
135
|
+
visitUnnest?(node: UnnestFrom): void;
|
|
136
|
+
visitColumn?(node: Column): void;
|
|
137
|
+
visitColumnRef?(node: ColumnRefItem): void;
|
|
138
|
+
visitFunction?(node: Function): void;
|
|
139
|
+
visitBinary?(node: Binary): void;
|
|
140
|
+
visitAggrFunc?(node: AggrFunc): void;
|
|
141
|
+
visitCast?(node: Cast): void;
|
|
142
|
+
visitCase?(node: Case): void;
|
|
143
|
+
visitExprList?(node: ExprList): void;
|
|
144
|
+
visitValue?(node: ExpressionValue): void;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### `walk(node, visitor)`
|
|
149
|
+
|
|
150
|
+
The main entry point. For a `Select` node it:
|
|
151
|
+
|
|
152
|
+
1. Calls `visitSelect`
|
|
153
|
+
2. Walks `with[]` items (calls `visitWith`, recurses into CTE body)
|
|
154
|
+
3. Walks `from[]` items via `walkFrom`
|
|
155
|
+
4. Walks `columns[]` items via `walkExpr`
|
|
156
|
+
5. Walks `where` via `walkExpr`
|
|
157
|
+
6. Walks `_next` (UNION) recursively via `walk`
|
|
158
|
+
|
|
159
|
+
`walkFrom` dispatches to `visitUnnest`, `visitTableExpr` (+ recurses into subquery), `visitJoin`, or `visitBaseFrom` based on type guards.
|
|
160
|
+
|
|
161
|
+
`walkExpr` handles expression-level nodes: `binary_expr`, `column_ref`, `function`, `aggr_func`, `cast`, `case`, `expr_list`, `expr` (column wrapper), or `visitValue` for literal/value nodes.
|
|
162
|
+
|
|
163
|
+
### Type Guards
|
|
164
|
+
|
|
165
|
+
All accept `unknown` input so callers don't need intermediate casts:
|
|
166
|
+
|
|
167
|
+
| Guard | Identifies |
|
|
168
|
+
| --------------------- | ------------------------------------------------- |
|
|
169
|
+
| `isUnnestFrom(node)` | `type === 'unnest'` |
|
|
170
|
+
| `isDual(node)` | `type === 'dual'` |
|
|
171
|
+
| `isTableExpr(node)` | has `expr.ast` sub-select |
|
|
172
|
+
| `isJoin(node)` | has `join` property, is not unnest/dual/tableExpr |
|
|
173
|
+
| `isBaseFrom(node)` | has `table` property, is none of the above |
|
|
174
|
+
| `hasArrayIndex(node)` | `ColumnRefItem` with `array_index[]` |
|
|
175
|
+
|
|
176
|
+
### Extractor Helpers
|
|
177
|
+
|
|
178
|
+
These replace the ad-hoc JSONPath queries that appeared in the original monolith:
|
|
179
|
+
|
|
180
|
+
| Helper | Returns |
|
|
181
|
+
| ---------------------------------- | ------------------------------------------------------------- |
|
|
182
|
+
| `extractColumnRefs(expr)` | All `ColumnRefItem` nodes in the subtree |
|
|
183
|
+
| `extractJsonExtractPath(expr)` | Path arg of first `json_extract_scalar` / `json_extract` call |
|
|
184
|
+
| `extractBracketAccessorPath(expr)` | `$["key"]` string from first bracket accessor (`col['key']`) |
|
|
185
|
+
| `hasFunctionCalls(expr)` | `true` if any `function` or `aggr_func` node is present |
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Resolution Context (`context.ts`)
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
interface VisitContext {
|
|
193
|
+
tables: Map<string, ResolvedTable[]>; // name → one entry per matched API operation
|
|
194
|
+
aliases: Map<string, string>; // alias → canonical table name
|
|
195
|
+
apiSchemas: Map<string, ApiSchemas[]>; // disk-read cache, shared across the whole query
|
|
196
|
+
parent?: VisitContext; // parent scope (currently stored but not queried)
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**`createRootContext()`** — creates an empty context for the top-level query.
|
|
201
|
+
|
|
202
|
+
**`createChildContext(parent)`** — creates a child context that pre-populates `tables` from the parent (so CTE results defined in the `WITH` clause are visible to subsequent CTEs and the final `SELECT`). The `aliases` map is fresh (each SELECT has its own alias scope). The `apiSchemas` map is shared by reference.
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
interface ResolvedTable {
|
|
206
|
+
name?: string;
|
|
207
|
+
columns: Map<string, ResolvedColumn[]>; // column name → one per matched operation
|
|
208
|
+
apiOperation?: MatchedOperation[];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
interface ResolvedColumn {
|
|
212
|
+
name: string;
|
|
213
|
+
schema: v3.SchemaObject;
|
|
214
|
+
ast?: object; // originating AST node (for error location, future use)
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Service Table Columns (`service-table.ts`)
|
|
221
|
+
|
|
222
|
+
Every Athena service table (Kinesis stream) exposes eleven fixed top-level columns:
|
|
223
|
+
|
|
224
|
+
| Column | Schema |
|
|
225
|
+
| ----------------- | ------------------------------- |
|
|
226
|
+
| `method` | `{ type: 'string' }` |
|
|
227
|
+
| `started` | `{ type: 'string' }` |
|
|
228
|
+
| `ended` | `{ type: 'string' }` |
|
|
229
|
+
| `url` | `{ type: 'string' }` |
|
|
230
|
+
| `requestbody` | OpenAPI request body schema |
|
|
231
|
+
| `requestheaders` | OpenAPI request headers schema |
|
|
232
|
+
| `responsestatus` | `{ type: 'string' }` |
|
|
233
|
+
| `responsemessage` | `{ type: 'string' }` |
|
|
234
|
+
| `responsetype` | `{ type: 'string' }` |
|
|
235
|
+
| `responsebody` | OpenAPI response body schema |
|
|
236
|
+
| `responseheaders` | OpenAPI response headers schema |
|
|
237
|
+
|
|
238
|
+
`requestbody` / `requestheaders` come from `operation.request['properties'].body/headers`; `responsebody` / `responseheaders` from `operation.response['properties'].body/headers`. Both fall back to `{ type: 'object' }` if the field is absent.
|
|
239
|
+
|
|
240
|
+
`buildServiceTables(tableName, operations)` returns one `ResolvedTable` per `MatchedOperation` (multiple operations may match, e.g. GET and POST on the same path).
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## API Location (`api-locator.ts`)
|
|
245
|
+
|
|
246
|
+
`locateApi(serviceName)` converts a kebab-case service name (the SQL table name) to camelCase and globs for `src/services/<camelCase>/*/swagger.schema.deref.json`. All matched files are parsed and returned as `ApiSchemas[]`.
|
|
247
|
+
|
|
248
|
+
The deref'd schema is used (not the raw `swagger.schema.json`) so `$ref` pointers are already inlined.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## API Matching (`api-matcher.ts`)
|
|
253
|
+
|
|
254
|
+
`matchApi(selectAST, tableAST, apiSchemas)` builds a set of `Matcher` functions from WHERE-clause conditions then filters all operations in `apiSchemas` against them.
|
|
255
|
+
|
|
256
|
+
Current matchers (each is optional — if the WHERE clause doesn't contain the relevant condition, the matcher is skipped):
|
|
257
|
+
|
|
258
|
+
| Matcher | Condition pattern in WHERE |
|
|
259
|
+
| --------------- | ---------------------------------------------------------------- |
|
|
260
|
+
| Method | `method = 'GET'` / `'POST'` / etc. |
|
|
261
|
+
| Path part count | `cardinality(split(url, '/')) = N` |
|
|
262
|
+
| Path part value | `split(url, '/')[N] = 'value'` |
|
|
263
|
+
| Response status | `responsestatus = '200'` (applied post-filter on response codes) |
|
|
264
|
+
|
|
265
|
+
The function throws an `Error` if no operations match (not `AthenaError` — this propagates as an unexpected error through `checkAthenaAst` and is caught as a non-`AthenaError`, reported with the raw message).
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Two-Pass Column Resolution
|
|
270
|
+
|
|
271
|
+
### Pass 1 — `resolveFromClause`
|
|
272
|
+
|
|
273
|
+
Iterates `select.from[]`. For each `BaseFrom` item:
|
|
274
|
+
|
|
275
|
+
- Registers the alias (if any) in `ctx.aliases`
|
|
276
|
+
- Skips if the table name is already in `ctx.tables` (CTE reference or duplicate in comma-join)
|
|
277
|
+
- Otherwise calls `locateApi` + `matchApi` + `buildServiceTables` and stores the result in `ctx.tables`
|
|
278
|
+
|
|
279
|
+
JOINs (`isJoin`), UNNEST items, subqueries (`isTableExpr`), and DUAL are skipped in pass 1.
|
|
280
|
+
|
|
281
|
+
### UNNEST Pre-Pass — `applyUnnestPre`
|
|
282
|
+
|
|
283
|
+
`extractUnnestMappings` extracts `{ fromColumn, toColumn }` pairs from UNNEST items in `from[]`.
|
|
284
|
+
|
|
285
|
+
`applyUnnestPre` then tries to resolve each mapping immediately: if the `fromColumn` exists in a service table currently in `ctx.tables`, it verifies the schema is `array`, unwraps the `.items` schema, and registers a synthetic transient table `"<ownerTable>:<unnested>"` in `ctx.tables` with a single column `toColumn` holding the item schema.
|
|
286
|
+
|
|
287
|
+
Any mappings where `fromColumn` is not yet found (because it will be a computed SELECT column) are returned as `deferred`.
|
|
288
|
+
|
|
289
|
+
### Pass 2 — `resolveSelectColumns`
|
|
290
|
+
|
|
291
|
+
Iterates `select.columns[]`. For each column:
|
|
292
|
+
|
|
293
|
+
1. **Count column refs** (`extractColumnRefs`). If 0 or > 1, fall back to `{ type: 'string' }` under the alias or `_colN` name.
|
|
294
|
+
2. **Look up the single ref** in `ctx.tables` (using `ctx.aliases` to resolve table qualifiers).
|
|
295
|
+
3. **Wildcard (`*`)** — expand all columns from the referenced tables into `columns`.
|
|
296
|
+
4. **Resolve the column name** in each candidate table's `columns` map. Throw `AthenaError` if not found.
|
|
297
|
+
5. **Check for property access**: first try `extractJsonExtractPath` (json_extract_scalar/json_extract), then `extractBracketAccessorPath` (bracket accessor). If neither, use the resolved schema directly.
|
|
298
|
+
6. **Navigate into the JSON schema** using the accessor path. The path is rewritten to use double-dot (`$..`) to handle intermediate `allOf`/`anyOf`/`oneOf` wrappers. Throw `AthenaError` if the path resolves to nothing.
|
|
299
|
+
|
|
300
|
+
The column name used in the result map is: alias > bare column name (no function) > `_colN` (function present, no alias).
|
|
301
|
+
|
|
302
|
+
### UNNEST Post-Pass — `applyUnnestPost`
|
|
303
|
+
|
|
304
|
+
Resolves the deferred UNNEST mappings against the `columns` map built in pass 2. The `fromColumn` is now a computed column; its schema must be `array`.
|
|
305
|
+
|
|
306
|
+
### UNION ALL
|
|
307
|
+
|
|
308
|
+
`select._next` chains the next SELECT in a UNION. After resolving the current SELECT, `checkSelect` recurses on `_next` passing the same parent context and the same `withTableName`. The column schemas from UNION branches are not currently compared against each other (this is a known gap — see PLAN.md).
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## CTE / WITH Handling
|
|
313
|
+
|
|
314
|
+
`checkAthenaAst` iterates `select.with[]` before calling `checkSelect` on the main query. For each WITH item it calls `checkSelect(withItem.stmt.ast, ctx, withItem.name.value)`. At the end of `checkSelect`, if `withTableName` is set, the resolved columns are registered as a `ResolvedTable` in `ctx.tables` under that name.
|
|
315
|
+
|
|
316
|
+
`createChildContext(parent)` copies `parent.tables` so each nested CTE can see tables defined by earlier CTEs in the same WITH clause.
|
|
317
|
+
|
|
318
|
+
After processing all CTEs, `checkAthenaAst` sets `select.with = null` (mutates the AST) to avoid double-processing in the subsequent `checkSelect` call on the outer select.
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Property Accessor Schema Navigation
|
|
323
|
+
|
|
324
|
+
When a column expression contains a JSON path accessor, the path is converted to a JSONPath expression that navigates into the OpenAPI schema:
|
|
325
|
+
|
|
326
|
+
```
|
|
327
|
+
Input: $.foo.bar
|
|
328
|
+
Rewrite: $...properties.foo..properties.bar
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
The double-dot (`..`) at each segment boundary lets JSONPath skip through intermediate `allOf`, `anyOf`, `oneOf`, or `properties` wrappers that OpenAPI schemas commonly use.
|
|
332
|
+
|
|
333
|
+
`JSONPath({ json: column.schema, path: adjustedPath })` is called against the resolved column's schema. If no values are returned, `AthenaError` is thrown with `property not found <colRef> - <accessor>`.
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Error Reporting
|
|
338
|
+
|
|
339
|
+
Two message IDs are registered on the ESLint rule:
|
|
340
|
+
|
|
341
|
+
| ID | Cause |
|
|
342
|
+
| -------------- | --------------------------------------------- |
|
|
343
|
+
| `SyntextError` | PEG parser threw (SQL syntax error) |
|
|
344
|
+
| `AthenaError` | `AthenaError` thrown during schema resolution |
|
|
345
|
+
|
|
346
|
+
Any other `Error` (e.g. thrown by `matchApi`, `locateApi`, `assert.ok`) is caught and reported as `AthenaError` with the error's string representation, plus a `console.error` to stderr. This is a catch-all for unexpected runtime failures.
|
|
347
|
+
|
|
348
|
+
Errors are thrown at the first failure encountered (not collected). Multiple errors in one query are reported as a single lint diagnostic for the template literal node.
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Schema Files
|
|
353
|
+
|
|
354
|
+
`src/services/<camelCaseServiceName>/<version>/swagger.schema.deref.json`
|
|
355
|
+
|
|
356
|
+
Generated by:
|
|
357
|
+
|
|
358
|
+
1. `src/openapi/generate-schema.ts` — reads `swagger.yml` and produces `swagger.schema.json` with typed request/response schemas keyed by `apis[path][method]`
|
|
359
|
+
2. `src/openapi/deref-schema.ts` — resolves `$ref` pointers inline to produce `swagger.schema.deref.json`
|
|
360
|
+
|
|
361
|
+
The `ApiSchemas` type (from `generate-schema.ts`) has shape:
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
{
|
|
365
|
+
apis: Record<string, Record<string, OperationSchemas>>;
|
|
366
|
+
// apis['/path/v1']['GET'] → { request: SchemaObject, responses: { '200': SchemaObject, ... } }
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Debug Logging
|
|
373
|
+
|
|
374
|
+
Set `DEBUG=eslint-plugin:athena` (or `eslint-plugin:athena:*`) to see verbose resolution logs. Uses the `debug` npm package.
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Known Limitations (as of current implementation)
|
|
379
|
+
|
|
380
|
+
- Only `BaseFrom` tables are resolved in pass 1; `JOIN ... ON`, subqueries (`TableExpr`), and `DUAL` are silently ignored.
|
|
381
|
+
- Multiple column refs in one SELECT column fall back to `{ type: 'string' }` without error.
|
|
382
|
+
- `CAST` type information is not propagated — a `CAST(col AS ARRAY<VARCHAR>)` is still treated as the underlying column's schema.
|
|
383
|
+
- `CASE` expressions always fall back to `{ type: 'string' }`.
|
|
384
|
+
- UNION branches are not checked for column-count or type compatibility.
|
|
385
|
+
- Multiple matched API operations for the same table produce multiple `ResolvedTable` entries but conflicts are not reported.
|
|
386
|
+
- `OR` conditions in WHERE are not used by `api-matcher` for path/method matching — only AND chains are examined.
|
|
387
|
+
- `ast.json` is written to disk on every linted SQL string (debug artifact, should be removed for production use).
|