@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.
- package/bin/builders/gql/index.js +27 -1
- package/bin/builders/gql/validate.js +135 -0
- package/package.json +3 -1
|
@@ -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
|
-
.
|
|
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-
|
|
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",
|