UNPKG

3.02 kBJavaScriptView Raw
1const fs = require('fs-extra');
2
3const JSDocFunctionNameRegex = /@name (\w+)/;
4const JSDocParamRegex = /@param (?:{(\w+)}\s+)?(\$?\w+)(.*)/g;
5const SectionRegex = /(\/\*\*.*?\*\/)\n*(.*?)(?=(\/\*)|$)/sg;
6const SQLParamRegex = / \${(\w+)}/gi;
7
8function difference(set1, set2) {
9 const s1 = new Set(set1);
10 const s2 = new Set(set2);
11
12 const result = new Set();
13 for (const value of s1) {
14 if (!s2.has(value)) {
15 result.add(value);
16 }
17 }
18 return result;
19}
20
21function anonymize(sqlStatement) {
22 let counter = 0;
23 const sortedParams = [];
24 const namedToPositions = sqlStatement.replace(SQLParamRegex, (match, name) => {
25 sortedParams.push(name);
26 counter += 1;
27 return ` $${counter}`;
28 });
29 return {
30 sortedParams,
31 query: namedToPositions,
32 };
33}
34
35const extractSQLParams = (statement) => {
36 const matches = [...statement.matchAll(SQLParamRegex)];
37 return matches.map(({ 1: name }) => name);
38};
39
40const extractJSDocParams = (comment) => {
41 const matches = [...comment.matchAll(JSDocParamRegex)];
42 return matches
43 .map(({ 1: type = 'string', 2: name }) => ({ [name]: type }))
44 .reduce((stored, current) => ({ ...stored, ...current }), []);
45};
46
47function checkParameters(statement, comment) {
48 const SQLParams = extractSQLParams(statement);
49 const JSDocParams = extractJSDocParams(comment);
50
51 const SQLvsJSDoc = difference(SQLParams, Object.keys(JSDocParams));
52 if (SQLvsJSDoc.size !== 0) {
53 throw new SyntaxError(`
54"${[...SQLvsJSDoc]}" params in the SQL statement:
55${statement}
56but not in the JSDoc section:
57${comment}`);
58 }
59
60 const JSDocvsSQL = difference(Object.keys(JSDocParams), SQLParams);
61 if (JSDocvsSQL.size !== 0) {
62 throw new SyntaxError(`
63"${[...JSDocvsSQL]}" params in JSDoc section:
64${comment}
65but not in the SQL statement:
66${statement}`);
67 }
68
69 return Object.entries(JSDocParams);
70}
71
72function* parseContent(fileContent) {
73 for (const { 1: docstring, 2: statement } of fileContent.matchAll(SectionRegex)) {
74 if (!docstring.includes('@private')) {
75 const functionBlock = JSDocFunctionNameRegex.exec(docstring);
76 if (functionBlock == null) {
77 throw new SyntaxError(`Missing @name in the JSDoc comment ${docstring}`);
78 }
79 const [jsDocLine, name] = functionBlock;
80
81 const params = checkParameters(statement, docstring);
82 const { query, sortedParams } = anonymize(statement);
83
84 yield {
85 name,
86 params,
87 sortedParams,
88 query: query.trim(),
89 docstring: docstring.replace(jsDocLine, ''),
90 };
91 }
92 }
93}
94
95const compile = async filepath => {
96 const fileContent = await fs.readFile(filepath, 'utf8');
97
98 const sections = parseContent(fileContent);
99
100 let output = '';
101 for (const { name, params, query, sortedParams } of sections) {
102 output += `export const ${name} = (${params.map(([param, type]) => `${param}: ${type.toLowerCase()}`).join(', ')}) => ({
103 text: '${query}',
104 values: [${sortedParams}]
105});\n\n`;
106 }
107
108 return output;
109};
110
111module.exports = { compile };