@adobe-commerce/elsie 1.4.0-alpha2 → 1.4.0-beta2

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.
@@ -1,6 +1,7 @@
1
1
  const path = require('path');
2
2
  const createOrClearDirectory = require('./createOrClearDirectory');
3
3
  const getSchemaRef = require('./getSchemaRef');
4
+ const validate = require('./validate');
4
5
  require('dotenv').config();
5
6
 
6
7
  const generate = require('@graphql-codegen/cli').generate;
@@ -67,5 +68,30 @@ module.exports = async function generateResourceBuilder(yargs) {
67
68
  },
68
69
  });
69
70
  })
70
- .demandCommand(1, 1, 'choose a command: types or mocks');
71
+ .command(
72
+ 'validate',
73
+ 'Validate GraphQL operations',
74
+ async (yargs) => {
75
+ return yargs
76
+ .option('source', {
77
+ alias: 's',
78
+ describe: 'Path to the source code containing GraphQL operations',
79
+ type: 'array',
80
+ string: true,
81
+ demandOption: true,
82
+ })
83
+ .option('endpoints', {
84
+ alias: 'e',
85
+ describe: 'Path to GraphQL endpoints',
86
+ type: 'array',
87
+ string: true,
88
+ demandOption: true,
89
+ });
90
+ },
91
+ async (argv) => {
92
+ const { source, endpoints } = argv;
93
+ await validate(source, endpoints);
94
+ },
95
+ )
96
+ .demandCommand(1, 1, 'choose a command: types, mocks or validate');
71
97
  };
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ const fsPromises = require('node:fs/promises');
3
+ const path = require('node:path');
4
+ const parser = require('@babel/parser');
5
+ const traverse = require('@babel/traverse');
6
+ const { getIntrospectionQuery, buildClientSchema, parse, validate } = require('graphql');
7
+
8
+ async function walk(dir, collected = []) {
9
+ const dirents = await fsPromises.readdir(dir, { withFileTypes: true });
10
+
11
+ for (const d of dirents) {
12
+ const full = path.resolve(dir, d.name);
13
+
14
+ if (d.isDirectory()) {
15
+ // skip node_modules and “hidden” folders such as .git
16
+ if (d.name === 'node_modules' || d.name.startsWith('.')) continue;
17
+ await walk(full, collected);
18
+ } else if (/\.(c?m?js|ts|tsx)$/.test(d.name)) {
19
+ collected.push(full);
20
+ }
21
+ }
22
+ return collected;
23
+ }
24
+
25
+ function extractConstants(code) {
26
+ const ast = parser.parse(code, {
27
+ sourceType: 'unambiguous',
28
+ plugins: [
29
+ 'typescript',
30
+ 'jsx',
31
+ 'classProperties',
32
+ 'objectRestSpread',
33
+ 'dynamicImport',
34
+ 'optionalChaining',
35
+ 'nullishCoalescingOperator',
36
+ ],
37
+ });
38
+ const found = [];
39
+ traverse.default(ast, {
40
+ VariableDeclaration(path) {
41
+ if (path.node.kind !== 'const') return;
42
+ for (const decl of path.node.declarations) {
43
+ const { id, init } = decl;
44
+ if (!init || id.type !== 'Identifier') continue;
45
+ let text = null;
46
+ switch (init.type) {
47
+ case 'TemplateLiteral': {
48
+ // join all raw chunks; ignores embedded ${expr} for simplicity
49
+ text = init.quasis.map(q => q.value.cooked).join('');
50
+ break;
51
+ }
52
+ case 'StringLiteral':
53
+ text = init.value;
54
+ break;
55
+ }
56
+ if (text) {
57
+ const match = text.match(/\b(query|mutation|fragment)\b/i);
58
+ if (match) {
59
+ found.push(text.trim());
60
+ }
61
+ }
62
+ }
63
+ },
64
+ });
65
+
66
+ return found;
67
+ }
68
+
69
+ async function fetchSchema(endpoint) {
70
+ const body = JSON.stringify({ query: getIntrospectionQuery() });
71
+ const res = await fetch(endpoint, {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json' },
74
+ body
75
+ });
76
+ if (!res.ok) throw new Error(`Introspection query failed: ${res.statusText}`);
77
+ const { data, errors } = await res.json();
78
+ if (errors?.length) throw new Error(`Server returned errors: ${JSON.stringify(errors)}`);
79
+ return buildClientSchema(data);
80
+ }
81
+
82
+ async function validateGqlOperations(endpoint, operation) {
83
+ console.log(`\nValidating against endpoint: ${endpoint}`);
84
+ try {
85
+ const document = parse(operation);
86
+ const errors = validate(await fetchSchema(endpoint), document);
87
+ if (errors.length) {
88
+ console.error('❌ Operation is NOT valid for this schema:');
89
+ errors.forEach(e => console.error('-', e.message));
90
+ process.exitCode = 1;
91
+ } else {
92
+ console.log('✅ Operation is valid!');
93
+ }
94
+ } catch (e) {
95
+ console.error(e);
96
+ process.exitCode = 1;
97
+ }
98
+ }
99
+
100
+ async function getAllOperations(directories) {
101
+ let fullContent = '';
102
+ for (const directory of directories) {
103
+ const files = await walk(path.resolve(directory));
104
+ for (const f of files) {
105
+ const code = await fsPromises.readFile(f, 'utf8');
106
+
107
+ let extracted;
108
+ try {
109
+ extracted = extractConstants(code); // may throw on bad syntax
110
+ } catch (err) {
111
+ console.error(
112
+ `⚠️ Skipping ${path.relative(process.cwd(), f)}\n` +
113
+ ` ${err.message}`
114
+ );
115
+ continue;
116
+ }
117
+ fullContent += extracted;
118
+ }
119
+ }
120
+ return fullContent;
121
+ }
122
+
123
+
124
+
125
+ module.exports = async function main(sources, endpoints) {
126
+ for (const endpoint of endpoints) {
127
+ const operations = await getAllOperations(sources);
128
+ if (!operations) {
129
+ console.error('No GraphQL operations found in the specified directories.');
130
+ process.exitCode = 0;
131
+ return;
132
+ }
133
+ await validateGqlOperations(endpoint, operations);
134
+ }
135
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe-commerce/elsie",
3
- "version": "1.4.0-alpha2",
3
+ "version": "1.4.0-beta2",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "description": "Domain Package SDK",
6
6
  "engines": {
@@ -36,8 +36,10 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@babel/core": "^7.24.9",
39
+ "@babel/parser": "^7.24.0",
39
40
  "@babel/preset-env": "^7.24.8",
40
41
  "@babel/preset-typescript": "^7.24.7",
42
+ "@babel/traverse": "^7.24.0",
41
43
  "@chromatic-com/storybook": "^1",
42
44
  "@graphql-codegen/cli": "^5.0.0",
43
45
  "@graphql-codegen/client-preset": "^4.1.0",