@checkdigit/eslint-athena-plugin 1.0.0-PR.2-dcdf

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.
Files changed (66) hide show
  1. package/LICENSE.txt +21 -0
  2. package/README.md +17 -0
  3. package/SECURITY.md +13 -0
  4. package/dist-mjs/athena/api-locator.mjs +66 -0
  5. package/dist-mjs/athena/api-matcher.mjs +206 -0
  6. package/dist-mjs/athena/athena.mjs +165 -0
  7. package/dist-mjs/athena/column.mjs +1 -0
  8. package/dist-mjs/athena/context.mjs +21 -0
  9. package/dist-mjs/athena/index.mjs +1 -0
  10. package/dist-mjs/athena/service-table.mjs +45 -0
  11. package/dist-mjs/athena/sql-file.mjs +123 -0
  12. package/dist-mjs/athena/types.mjs +1 -0
  13. package/dist-mjs/athena/validate.mjs +619 -0
  14. package/dist-mjs/athena/visitor.mjs +291 -0
  15. package/dist-mjs/get-documentation-url.mjs +9 -0
  16. package/dist-mjs/index.mjs +56 -0
  17. package/dist-mjs/openapi/deref-schema.mjs +20 -0
  18. package/dist-mjs/openapi/generate-schema.mjs +375 -0
  19. package/dist-mjs/openapi/service-schema-generator.mjs +176 -0
  20. package/dist-mjs/peggy/athena-peggy.mjs +20700 -0
  21. package/dist-mjs/service.mjs +9 -0
  22. package/dist-mjs/sql-parser.mjs +28 -0
  23. package/dist-types/athena/api-locator.d.ts +2 -0
  24. package/dist-types/athena/api-matcher.d.ts +14 -0
  25. package/dist-types/athena/athena.d.ts +5 -0
  26. package/dist-types/athena/column.d.ts +1 -0
  27. package/dist-types/athena/context.d.ts +21 -0
  28. package/dist-types/athena/index.d.ts +8 -0
  29. package/dist-types/athena/service-table.d.ts +8 -0
  30. package/dist-types/athena/sql-file.d.ts +5 -0
  31. package/dist-types/athena/types.d.ts +493 -0
  32. package/dist-types/athena/validate.d.ts +14 -0
  33. package/dist-types/athena/visitor.d.ts +75 -0
  34. package/dist-types/get-documentation-url.d.ts +1 -0
  35. package/dist-types/index.d.ts +5 -0
  36. package/dist-types/openapi/deref-schema.d.ts +1 -0
  37. package/dist-types/openapi/generate-schema.d.ts +33 -0
  38. package/dist-types/openapi/service-schema-generator.d.ts +5 -0
  39. package/dist-types/peggy/athena-peggy.d.ts +13 -0
  40. package/dist-types/service.d.ts +2 -0
  41. package/dist-types/sql-parser.d.ts +25 -0
  42. package/package.json +1 -0
  43. package/src/api/v1/swagger.yml +619 -0
  44. package/src/api/v2/swagger.yml +477 -0
  45. package/src/athena/api-locator.ts +78 -0
  46. package/src/athena/api-matcher.ts +323 -0
  47. package/src/athena/athena.ts +224 -0
  48. package/src/athena/column.ts +4 -0
  49. package/src/athena/context.ts +47 -0
  50. package/src/athena/index.ts +13 -0
  51. package/src/athena/service-table.ts +78 -0
  52. package/src/athena/sql-file.ts +161 -0
  53. package/src/athena/types.ts +568 -0
  54. package/src/athena/validate.ts +902 -0
  55. package/src/athena/visitor.ts +406 -0
  56. package/src/get-documentation-url.ts +7 -0
  57. package/src/index.ts +67 -0
  58. package/src/openapi/deref-schema.ts +20 -0
  59. package/src/openapi/generate-schema.ts +553 -0
  60. package/src/openapi/service-schema-generator.ts +241 -0
  61. package/src/peggy/athena-peggy.ts +22149 -0
  62. package/src/peggy/athena.peggy +2971 -0
  63. package/src/service.ts +11 -0
  64. package/src/services/eslintAthenaPlugin/v1/swagger.schema.deref.json +1931 -0
  65. package/src/services/eslintAthenaPlugin/v2/swagger.schema.deref.json +978 -0
  66. package/src/sql-parser.ts +53 -0
@@ -0,0 +1,241 @@
1
+ // openapi/service-schema-generator.ts
2
+
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+
5
+ import debug from 'debug';
6
+
7
+ import { type ApiSchemas, buildApiSchemaFromYaml } from './generate-schema.ts';
8
+
9
+ const log = debug('eslint-athena-plugin:athena:service-schema-generator');
10
+
11
+ const ORGANIZATIONS = ['checkdigit'] as const;
12
+ const SWAGGER_SCHEMA_DEREF_FILENAME = 'swagger.schema.deref.json';
13
+
14
+ function errorMessageFromError(error: unknown): string {
15
+ return error instanceof Error ? error.message : String(error);
16
+ }
17
+
18
+ interface ServiceEndpoint {
19
+ path: string;
20
+ yamlContent: string;
21
+ }
22
+
23
+ interface ServiceSource {
24
+ organization: string;
25
+ serviceName: string;
26
+ endpoints: ServiceEndpoint[];
27
+ }
28
+
29
+ function derefApiSchemas(schemas: ApiSchemas): ApiSchemas {
30
+ const definitions = schemas.definitions ?? {};
31
+
32
+ function derefValue(
33
+ value: unknown,
34
+ resolving: Set<string> = new Set<string>(),
35
+ ): unknown {
36
+ if (value === null || typeof value !== 'object') {
37
+ return value;
38
+ }
39
+ if (Array.isArray(value)) {
40
+ return value.map((item) => derefValue(item, resolving));
41
+ }
42
+ const obj = value as Record<string, unknown>;
43
+ if (typeof obj['$ref'] === 'string') {
44
+ const refName = /\/definitions\/(?<name>[^/]+)$/u.exec(obj['$ref'])
45
+ ?.groups?.['name'];
46
+ if (refName !== undefined && !resolving.has(refName)) {
47
+ return derefValue(
48
+ definitions[refName],
49
+ new Set<string>([...resolving, refName]),
50
+ );
51
+ }
52
+ }
53
+ return Object.fromEntries(
54
+ Object.entries(obj).map(([key, val]) => [
55
+ key,
56
+ derefValue(val, resolving),
57
+ ]),
58
+ );
59
+ }
60
+
61
+ const resolvedApis = derefValue(schemas.apis) as ApiSchemas['apis'];
62
+ if (schemas.definitions !== undefined) {
63
+ return {
64
+ apis: resolvedApis,
65
+ definitions: derefValue(schemas.definitions) as NonNullable<
66
+ ApiSchemas['definitions']
67
+ >,
68
+ };
69
+ }
70
+ return { apis: resolvedApis };
71
+ }
72
+
73
+ function readServiceConfig(
74
+ packageJsonContent: string,
75
+ org: string,
76
+ serviceName: string,
77
+ ):
78
+ | {
79
+ organization: string;
80
+ serviceName: string;
81
+ apiRoot: string;
82
+ endpoints: string[];
83
+ }
84
+ | undefined {
85
+ const packageJson = JSON.parse(packageJsonContent) as {
86
+ name?: string;
87
+ service?: { api?: { root?: string; endpoints?: string[] } };
88
+ };
89
+ const apiRoot = packageJson.service?.api?.root;
90
+ const apiEndpoints = packageJson.service?.api?.endpoints;
91
+ if (
92
+ apiRoot === undefined ||
93
+ apiEndpoints === undefined ||
94
+ apiEndpoints.length === 0
95
+ ) {
96
+ return undefined;
97
+ }
98
+ const [pkgOrg = org, pkgServiceName = serviceName] =
99
+ packageJson.name?.slice(1).split('/') ?? [];
100
+ return {
101
+ organization: pkgOrg,
102
+ serviceName: pkgServiceName,
103
+ apiRoot,
104
+ endpoints: apiEndpoints,
105
+ };
106
+ }
107
+
108
+ function findServiceLocally(
109
+ serviceFolder: string,
110
+ org: string,
111
+ serviceName: string,
112
+ ): ServiceSource | undefined {
113
+ const config = readServiceConfig(
114
+ readFileSync(`${serviceFolder}/package.json`, 'utf-8'),
115
+ org,
116
+ serviceName,
117
+ );
118
+ if (config === undefined) {
119
+ return undefined;
120
+ }
121
+
122
+ const endpoints: ServiceEndpoint[] = [];
123
+ for (const endpoint of config.endpoints) {
124
+ const swaggerPath = `${serviceFolder}/${config.apiRoot}/${endpoint}/swagger.yml`;
125
+ if (existsSync(swaggerPath)) {
126
+ endpoints.push({
127
+ path: endpoint,
128
+ yamlContent: readFileSync(swaggerPath, 'utf-8'),
129
+ });
130
+ }
131
+ }
132
+
133
+ if (endpoints.length > 0) {
134
+ return {
135
+ organization: config.organization,
136
+ serviceName: config.serviceName,
137
+ endpoints,
138
+ };
139
+ }
140
+ return undefined;
141
+ }
142
+
143
+ function findServiceInProject(serviceName: string): ServiceSource | undefined {
144
+ const packageJson = JSON.parse(readFileSync('./package.json', 'utf-8')) as {
145
+ name?: string;
146
+ };
147
+ const [org, projectName] = (packageJson.name ?? '').slice(1).split('/');
148
+ if (org === undefined || projectName !== serviceName) {
149
+ return undefined;
150
+ }
151
+ log(
152
+ `[schema-generator] '${serviceName}' is the current project, reading local swagger files`,
153
+ );
154
+ return findServiceLocally('.', org, serviceName);
155
+ }
156
+
157
+ function findServiceInNodeModules(
158
+ serviceName: string,
159
+ ): ServiceSource | undefined {
160
+ for (const org of ORGANIZATIONS) {
161
+ const serviceFolder = `node_modules/@${org}/${serviceName}`;
162
+ if (!existsSync(serviceFolder)) {
163
+ log(
164
+ `[schema-generator] not found in node_modules: @${org}/${serviceName}`,
165
+ );
166
+ continue;
167
+ }
168
+ try {
169
+ log(
170
+ `[schema-generator] found in node_modules: @${org}/${serviceName}, reading swagger files`,
171
+ );
172
+ const source = findServiceLocally(serviceFolder, org, serviceName);
173
+ if (source !== undefined) {
174
+ return source;
175
+ }
176
+ log(
177
+ `[schema-generator] no swagger schema inside node_modules/@${org}/${serviceName}`,
178
+ );
179
+ } catch (error) {
180
+ log(
181
+ `[schema-generator] error reading node_modules/@${org}/${serviceName}: ${errorMessageFromError(error)}`,
182
+ );
183
+ }
184
+ }
185
+ return undefined;
186
+ }
187
+
188
+ export function generateSchemasForService(
189
+ serviceName: string,
190
+ outputDir?: string,
191
+ ): { schema: ApiSchemas; endpoint: string }[] {
192
+ log(`[schema-generator] locating service '${serviceName}'`);
193
+
194
+ const source =
195
+ findServiceInProject(serviceName) ?? findServiceInNodeModules(serviceName);
196
+ if (source === undefined) {
197
+ log(
198
+ `[schema-generator] '${serviceName}' not found — ensure it is listed as a devDependency or the repo is accessible via git`,
199
+ );
200
+ return [];
201
+ }
202
+
203
+ log(
204
+ `[schema-generator] found '${serviceName}' (${source.endpoints.length.toString()} endpoint(s))`,
205
+ );
206
+
207
+ const results: { schema: ApiSchemas; endpoint: string }[] = [];
208
+ for (const { path: endpoint, yamlContent } of source.endpoints) {
209
+ try {
210
+ const rawSchema = buildApiSchemaFromYaml(
211
+ yamlContent,
212
+ source.organization,
213
+ source.serviceName,
214
+ );
215
+ if (rawSchema === undefined) {
216
+ continue;
217
+ }
218
+ const schema = derefApiSchemas(rawSchema);
219
+ results.push({ schema, endpoint });
220
+
221
+ if (outputDir !== undefined) {
222
+ const versionFolder = endpoint.split('/').at(-1) ?? endpoint;
223
+ const dir = `${outputDir}/${versionFolder}`;
224
+ mkdirSync(dir, { recursive: true });
225
+ writeFileSync(
226
+ `${dir}/${SWAGGER_SCHEMA_DEREF_FILENAME}`,
227
+ JSON.stringify(schema, undefined, 2),
228
+ );
229
+ log(
230
+ `[schema-generator] cached schema to ${dir}/${SWAGGER_SCHEMA_DEREF_FILENAME}`,
231
+ );
232
+ }
233
+ } catch (error) {
234
+ log(
235
+ `[schema-generator] error processing endpoint ${endpoint}: ${errorMessageFromError(error)}`,
236
+ );
237
+ }
238
+ }
239
+
240
+ return results;
241
+ }