@neo4j-cypher/react-codemirror 2.0.0-next.15 → 2.0.0-next.17
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/CHANGELOG.md +24 -0
- package/dist/CypherEditor.d.ts +5 -1
- package/dist/CypherEditor.js +20 -0
- package/dist/CypherEditor.js.map +1 -1
- package/dist/e2e_tests/autoCompletion.spec.js +82 -37
- package/dist/e2e_tests/autoCompletion.spec.js.map +1 -1
- package/dist/e2e_tests/debounce.spec.js +2 -1
- package/dist/e2e_tests/debounce.spec.js.map +1 -1
- package/dist/e2e_tests/e2eUtils.d.ts +1 -0
- package/dist/e2e_tests/e2eUtils.js +13 -2
- package/dist/e2e_tests/e2eUtils.js.map +1 -1
- package/dist/e2e_tests/performanceTest.spec.js +1 -1
- package/dist/e2e_tests/performanceTest.spec.js.map +1 -1
- package/dist/e2e_tests/signatureHelp.spec.js +59 -13
- package/dist/e2e_tests/signatureHelp.spec.js.map +1 -1
- package/dist/e2e_tests/snippets.spec.js +2 -2
- package/dist/e2e_tests/snippets.spec.js.map +1 -1
- package/dist/e2e_tests/syntaxValidation.spec.js +33 -4
- package/dist/e2e_tests/syntaxValidation.spec.js.map +1 -1
- package/dist/lang-cypher/autocomplete.js +10 -5
- package/dist/lang-cypher/autocomplete.js.map +1 -1
- package/dist/lang-cypher/constants.d.ts +2 -0
- package/dist/lang-cypher/constants.js +4 -0
- package/dist/lang-cypher/constants.js.map +1 -1
- package/dist/lang-cypher/createCypherTheme.js +1 -1
- package/dist/lang-cypher/createCypherTheme.js.map +1 -1
- package/dist/lang-cypher/langCypher.d.ts +1 -0
- package/dist/lang-cypher/langCypher.js +1 -8
- package/dist/lang-cypher/langCypher.js.map +1 -1
- package/dist/lang-cypher/lintWorker.d.ts +8 -4
- package/dist/lang-cypher/lintWorker.js +12 -2
- package/dist/lang-cypher/lintWorker.js.map +1 -1
- package/dist/lang-cypher/syntaxValidation.d.ts +0 -1
- package/dist/lang-cypher/syntaxValidation.js +15 -31
- package/dist/lang-cypher/syntaxValidation.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/CypherEditor.tsx +32 -4
- package/src/e2e_tests/autoCompletion.spec.tsx +121 -56
- package/src/e2e_tests/debounce.spec.tsx +37 -32
- package/src/e2e_tests/e2eUtils.ts +14 -2
- package/src/e2e_tests/performanceTest.spec.tsx +1 -1
- package/src/e2e_tests/signatureHelp.spec.tsx +74 -13
- package/src/e2e_tests/snippets.spec.tsx +2 -2
- package/src/e2e_tests/syntaxValidation.spec.tsx +76 -4
- package/src/lang-cypher/autocomplete.ts +14 -8
- package/src/lang-cypher/constants.ts +4 -0
- package/src/lang-cypher/createCypherTheme.ts +1 -1
- package/src/lang-cypher/langCypher.ts +3 -12
- package/src/lang-cypher/lintWorker.ts +24 -7
- package/src/lang-cypher/syntaxValidation.ts +15 -45
|
@@ -13,7 +13,7 @@ test('Prop lint set to false disables syntax validation', async ({
|
|
|
13
13
|
await mount(<CypherEditor value={query} lint={false} />);
|
|
14
14
|
|
|
15
15
|
await expect(page.locator('.cm-lintRange-error').last()).not.toBeVisible({
|
|
16
|
-
timeout:
|
|
16
|
+
timeout: 10000,
|
|
17
17
|
});
|
|
18
18
|
});
|
|
19
19
|
|
|
@@ -24,7 +24,7 @@ test('Can turn linting back on', async ({ page, mount }) => {
|
|
|
24
24
|
const component = await mount(<CypherEditor value={query} lint={false} />);
|
|
25
25
|
|
|
26
26
|
await expect(page.locator('.cm-lintRange-error').last()).not.toBeVisible({
|
|
27
|
-
timeout:
|
|
27
|
+
timeout: 10000,
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
await component.update(<CypherEditor value={query} lint={true} />);
|
|
@@ -33,7 +33,7 @@ test('Can turn linting back on', async ({ page, mount }) => {
|
|
|
33
33
|
|
|
34
34
|
await editorPage.checkErrorMessage(
|
|
35
35
|
'METCH',
|
|
36
|
-
'
|
|
36
|
+
`Invalid input 'METCH': expected 'FOREACH', 'ALTER', 'ORDER BY', 'CALL', 'USING PERIODIC COMMIT', 'CREATE', 'LOAD CSV', 'START DATABASE', 'STOP DATABASE', 'DEALLOCATE', 'DELETE', 'DENY', 'DETACH', 'DROP', 'DRYRUN', 'FINISH', 'GRANT', 'INSERT', 'LIMIT', 'MATCH', 'MERGE', 'NODETACH', 'OFFSET', 'OPTIONAL', 'REALLOCATE', 'REMOVE', 'RENAME', 'RETURN', 'REVOKE', 'ENABLE SERVER', 'SET', 'SHOW', 'SKIP', 'TERMINATE', 'UNWIND', 'USE' or 'WITH'`,
|
|
37
37
|
);
|
|
38
38
|
});
|
|
39
39
|
|
|
@@ -45,10 +45,22 @@ test('Syntactic errors are surfaced', async ({ page, mount }) => {
|
|
|
45
45
|
|
|
46
46
|
await editorPage.checkErrorMessage(
|
|
47
47
|
'METCH',
|
|
48
|
-
'
|
|
48
|
+
`Invalid input 'METCH': expected 'FOREACH', 'ALTER', 'ORDER BY', 'CALL', 'USING PERIODIC COMMIT', 'CREATE', 'LOAD CSV', 'START DATABASE', 'STOP DATABASE', 'DEALLOCATE', 'DELETE', 'DENY', 'DETACH', 'DROP', 'DRYRUN', 'FINISH', 'GRANT', 'INSERT', 'LIMIT', 'MATCH', 'MERGE', 'NODETACH', 'OFFSET', 'OPTIONAL', 'REALLOCATE', 'REMOVE', 'RENAME', 'RETURN', 'REVOKE', 'ENABLE SERVER', 'SET', 'SHOW', 'SKIP', 'TERMINATE', 'UNWIND', 'USE' or 'WITH'`,
|
|
49
49
|
);
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
test('Does not trigger syntax errors for backticked parameters in parameter creation', async ({
|
|
53
|
+
page,
|
|
54
|
+
mount,
|
|
55
|
+
}) => {
|
|
56
|
+
const editorPage = new CypherEditorPage(page);
|
|
57
|
+
|
|
58
|
+
const query = ':param x => "abc"';
|
|
59
|
+
await mount(<CypherEditor value={query} />);
|
|
60
|
+
|
|
61
|
+
await editorPage.checkNoNotificationMessage('error');
|
|
62
|
+
});
|
|
63
|
+
|
|
52
64
|
test('Errors for undefined labels are surfaced', async ({ page, mount }) => {
|
|
53
65
|
const editorPage = new CypherEditorPage(page);
|
|
54
66
|
const query = 'MATCH (n: Person) RETURN n';
|
|
@@ -169,3 +181,63 @@ test('Validation errors are correctly overlapped', async ({ page, mount }) => {
|
|
|
169
181
|
"Invalid input. '-1' is not a valid value. Must be a positive integer",
|
|
170
182
|
);
|
|
171
183
|
});
|
|
184
|
+
|
|
185
|
+
test('Strikethroughs are shown for deprecated functions', async ({
|
|
186
|
+
page,
|
|
187
|
+
mount,
|
|
188
|
+
}) => {
|
|
189
|
+
const editorPage = new CypherEditorPage(page);
|
|
190
|
+
const query = `RETURN id()`;
|
|
191
|
+
|
|
192
|
+
await mount(<CypherEditor value={query} schema={testData.mockSchema} />);
|
|
193
|
+
await expect(
|
|
194
|
+
editorPage.page.locator('.cm-deprecated-element').last(),
|
|
195
|
+
).toBeVisible({ timeout: 10000 });
|
|
196
|
+
await editorPage.checkWarningMessage('id', 'Function id is deprecated.');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('Strikethroughs are shown for deprecated procedures', async ({
|
|
200
|
+
page,
|
|
201
|
+
mount,
|
|
202
|
+
}) => {
|
|
203
|
+
const editorPage = new CypherEditorPage(page);
|
|
204
|
+
const query = `CALL apoc.create.uuids()`;
|
|
205
|
+
|
|
206
|
+
await mount(<CypherEditor value={query} schema={testData.mockSchema} />);
|
|
207
|
+
await expect(
|
|
208
|
+
editorPage.page.locator('.cm-deprecated-element').last(),
|
|
209
|
+
).toBeVisible({ timeout: 10000 });
|
|
210
|
+
|
|
211
|
+
await editorPage.checkWarningMessage(
|
|
212
|
+
'apoc.create.uuids',
|
|
213
|
+
'Procedure apoc.create.uuids is deprecated.',
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('Syntax validation depends on the Cypher version', async ({
|
|
218
|
+
page,
|
|
219
|
+
mount,
|
|
220
|
+
}) => {
|
|
221
|
+
await mount(
|
|
222
|
+
<CypherEditor
|
|
223
|
+
schema={testData.mockSchema}
|
|
224
|
+
featureFlags={{ cypher25: true }}
|
|
225
|
+
/>,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const editorPage = new CypherEditorPage(page);
|
|
229
|
+
const textField = page.getByRole('textbox');
|
|
230
|
+
await textField.fill('CYPHER 5 CALL apoc.create.uuids(5)');
|
|
231
|
+
|
|
232
|
+
await editorPage.checkWarningMessage(
|
|
233
|
+
'apoc.create.uuids',
|
|
234
|
+
'Procedure apoc.create.uuids is deprecated.',
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
await textField.fill('CYPHER 25 CALL apoc.create.uuids(5)');
|
|
238
|
+
|
|
239
|
+
await editorPage.checkErrorMessage(
|
|
240
|
+
'apoc.create.uuids',
|
|
241
|
+
'Procedure apoc.create.uuids is not present in the database.',
|
|
242
|
+
);
|
|
243
|
+
});
|
|
@@ -3,7 +3,10 @@ import {
|
|
|
3
3
|
CompletionSource,
|
|
4
4
|
snippet,
|
|
5
5
|
} from '@codemirror/autocomplete';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
autocomplete,
|
|
8
|
+
shouldAutoCompleteYield,
|
|
9
|
+
} from '@neo4j-cypher/language-support';
|
|
7
10
|
import {
|
|
8
11
|
CompletionItemKind,
|
|
9
12
|
CompletionItemTag,
|
|
@@ -49,7 +52,7 @@ export const completionStyles: (
|
|
|
49
52
|
completion: Completion & { deprecated?: boolean },
|
|
50
53
|
) => string = (completion) => {
|
|
51
54
|
if (completion.deprecated) {
|
|
52
|
-
return 'cm-deprecated-
|
|
55
|
+
return 'cm-deprecated-element';
|
|
53
56
|
} else {
|
|
54
57
|
return null;
|
|
55
58
|
}
|
|
@@ -58,15 +61,18 @@ export const completionStyles: (
|
|
|
58
61
|
export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
|
|
59
62
|
(config) => (context) => {
|
|
60
63
|
const documentText = context.state.doc.toString();
|
|
61
|
-
|
|
64
|
+
const offset = context.pos;
|
|
62
65
|
const triggerCharacters = ['.', ':', '{', '$', ')'];
|
|
63
|
-
const lastCharacter = documentText.at(
|
|
64
|
-
|
|
66
|
+
const lastCharacter = documentText.at(offset - 1);
|
|
67
|
+
const yieldTriggered = shouldAutoCompleteYield(documentText, offset);
|
|
65
68
|
const lastWord = context.matchBefore(/\w*/);
|
|
66
69
|
const inWord = lastWord.from !== lastWord.to;
|
|
67
70
|
|
|
68
71
|
const shouldTriggerCompletion =
|
|
69
|
-
inWord ||
|
|
72
|
+
inWord ||
|
|
73
|
+
context.explicit ||
|
|
74
|
+
triggerCharacters.includes(lastCharacter) ||
|
|
75
|
+
yieldTriggered;
|
|
70
76
|
|
|
71
77
|
if (config.useLightVersion && !context.explicit) {
|
|
72
78
|
return null;
|
|
@@ -78,9 +84,9 @@ export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
|
|
|
78
84
|
|
|
79
85
|
const options = autocomplete(
|
|
80
86
|
// TODO This is a temporary hack because completions are not working well
|
|
81
|
-
documentText.slice(0,
|
|
87
|
+
documentText.slice(0, offset),
|
|
82
88
|
config.schema ?? {},
|
|
83
|
-
|
|
89
|
+
offset,
|
|
84
90
|
context.explicit,
|
|
85
91
|
);
|
|
86
92
|
|
|
@@ -40,6 +40,8 @@ export const cypherTokenTypeToNode = (facet: Facet<unknown>) => ({
|
|
|
40
40
|
relationship: NodeType.define({ id: 26, name: 'label' }),
|
|
41
41
|
boolean: NodeType.define({ id: 27, name: 'booleanLiteral' }),
|
|
42
42
|
number: NodeType.define({ id: 28, name: 'numberLiteral' }),
|
|
43
|
+
setting: NodeType.define({ id: 29, name: 'setting' }),
|
|
44
|
+
settingValue: NodeType.define({ id: 30, name: 'settingValue' }),
|
|
43
45
|
});
|
|
44
46
|
|
|
45
47
|
export type PrismSpecificTokenType =
|
|
@@ -78,6 +80,8 @@ export const tokenTypeToStyleTag: Record<HighlightedCypherTokenTypes, Tag> = {
|
|
|
78
80
|
punctuation: tags.punctuation,
|
|
79
81
|
separator: tags.separator,
|
|
80
82
|
consoleCommand: tags.macroName,
|
|
83
|
+
setting: tags.attributeName,
|
|
84
|
+
settingValue: tags.attributeValue,
|
|
81
85
|
};
|
|
82
86
|
|
|
83
87
|
export const parserAdapterNodeSet = (nodes: Record<string, NodeType>) =>
|
|
@@ -4,14 +4,11 @@ import {
|
|
|
4
4
|
Language,
|
|
5
5
|
LanguageSupport,
|
|
6
6
|
} from '@codemirror/language';
|
|
7
|
-
import {
|
|
8
|
-
_internalFeatureFlags,
|
|
9
|
-
type DbSchema,
|
|
10
|
-
} from '@neo4j-cypher/language-support';
|
|
7
|
+
import { type DbSchema } from '@neo4j-cypher/language-support';
|
|
11
8
|
import { completionStyles, cypherAutocomplete } from './autocomplete';
|
|
12
9
|
import { ParserAdapter } from './parser-adapter';
|
|
13
10
|
import { signatureHelpTooltip } from './signatureHelp';
|
|
14
|
-
import { cypherLinter
|
|
11
|
+
import { cypherLinter } from './syntaxValidation';
|
|
15
12
|
|
|
16
13
|
const facet = defineLanguageFacet({
|
|
17
14
|
commentTokens: { block: { open: '/*', close: '*/' }, line: '//' },
|
|
@@ -23,6 +20,7 @@ export type CypherConfig = {
|
|
|
23
20
|
showSignatureTooltipBelow?: boolean;
|
|
24
21
|
featureFlags?: {
|
|
25
22
|
consoleCommands?: boolean;
|
|
23
|
+
cypher25?: boolean;
|
|
26
24
|
};
|
|
27
25
|
schema?: DbSchema;
|
|
28
26
|
useLightVersion: boolean;
|
|
@@ -30,12 +28,6 @@ export type CypherConfig = {
|
|
|
30
28
|
};
|
|
31
29
|
|
|
32
30
|
export function cypher(config: CypherConfig) {
|
|
33
|
-
const featureFlags = config.featureFlags;
|
|
34
|
-
// We allow to override the consoleCommands feature flag
|
|
35
|
-
if (featureFlags.consoleCommands !== undefined) {
|
|
36
|
-
_internalFeatureFlags.consoleCommands = featureFlags.consoleCommands;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
31
|
const parserAdapter = new ParserAdapter(facet, config);
|
|
40
32
|
|
|
41
33
|
const cypherLanguage = new Language(facet, parserAdapter, [], 'cypher');
|
|
@@ -46,7 +38,6 @@ export function cypher(config: CypherConfig) {
|
|
|
46
38
|
optionClass: completionStyles,
|
|
47
39
|
}),
|
|
48
40
|
cypherLinter(config),
|
|
49
|
-
semanticAnalysisLinter(config),
|
|
50
41
|
signatureHelpTooltip(config),
|
|
51
42
|
]);
|
|
52
43
|
}
|
|
@@ -1,14 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
DbSchema,
|
|
3
|
+
lintCypherQuery as _lintCypherQuery,
|
|
4
|
+
_internalFeatureFlags,
|
|
5
|
+
} from '@neo4j-cypher/language-support';
|
|
2
6
|
import workerpool from 'workerpool';
|
|
3
7
|
|
|
4
|
-
|
|
8
|
+
function lintCypherQuery(
|
|
9
|
+
query: string,
|
|
10
|
+
dbSchema: DbSchema,
|
|
11
|
+
featureFlags: { consoleCommands?: boolean; cypher25?: boolean } = {},
|
|
12
|
+
) {
|
|
13
|
+
// We allow to override the consoleCommands feature flag
|
|
14
|
+
if (featureFlags.consoleCommands !== undefined) {
|
|
15
|
+
_internalFeatureFlags.consoleCommands = featureFlags.consoleCommands;
|
|
16
|
+
}
|
|
17
|
+
if (featureFlags.cypher25 !== undefined) {
|
|
18
|
+
_internalFeatureFlags.cypher25 = featureFlags.cypher25;
|
|
19
|
+
}
|
|
20
|
+
return _lintCypherQuery(query, dbSchema);
|
|
21
|
+
}
|
|
5
22
|
|
|
6
|
-
|
|
23
|
+
workerpool.worker({ lintCypherQuery });
|
|
7
24
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
25
|
+
type LinterArgs = Parameters<typeof lintCypherQuery>;
|
|
26
|
+
|
|
27
|
+
export type LinterTask = workerpool.Promise<ReturnType<typeof lintCypherQuery>>;
|
|
11
28
|
|
|
12
29
|
export type LintWorker = {
|
|
13
|
-
|
|
30
|
+
lintCypherQuery: (...args: LinterArgs) => LinterTask;
|
|
14
31
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Diagnostic, linter } from '@codemirror/lint';
|
|
2
2
|
import { Extension } from '@codemirror/state';
|
|
3
|
-
import {
|
|
4
|
-
import { DiagnosticSeverity } from 'vscode-languageserver-types';
|
|
3
|
+
import { DiagnosticSeverity, DiagnosticTag } from 'vscode-languageserver-types';
|
|
5
4
|
import workerpool from 'workerpool';
|
|
6
5
|
import type { CypherConfig } from './langCypher';
|
|
7
6
|
import type { LinterTask, LintWorker } from './lintWorker';
|
|
@@ -16,73 +15,44 @@ const pool = workerpool.pool(WorkerURL, {
|
|
|
16
15
|
let lastSemanticJob: LinterTask | undefined;
|
|
17
16
|
|
|
18
17
|
export const cypherLinter: (config: CypherConfig) => Extension = (config) =>
|
|
19
|
-
linter((view) => {
|
|
20
|
-
if (!config.lint) {
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const query = view.state.doc.toString();
|
|
25
|
-
const syntaxErrors = validateSyntax(query, config.schema ?? {});
|
|
26
|
-
|
|
27
|
-
return syntaxErrors.map(
|
|
28
|
-
(diagnostic): Diagnostic => ({
|
|
29
|
-
from: diagnostic.offsets.start,
|
|
30
|
-
to: diagnostic.offsets.end,
|
|
31
|
-
severity:
|
|
32
|
-
diagnostic.severity === DiagnosticSeverity.Error
|
|
33
|
-
? 'error'
|
|
34
|
-
: 'warning',
|
|
35
|
-
message: diagnostic.message,
|
|
36
|
-
}),
|
|
37
|
-
);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
export const semanticAnalysisLinter: (config: CypherConfig) => Extension = (
|
|
41
|
-
config,
|
|
42
|
-
) =>
|
|
43
18
|
linter(async (view) => {
|
|
44
19
|
if (!config.lint) {
|
|
45
20
|
return [];
|
|
46
21
|
}
|
|
47
|
-
|
|
48
22
|
const query = view.state.doc.toString();
|
|
49
23
|
if (query.length === 0) {
|
|
50
24
|
return [];
|
|
51
25
|
}
|
|
52
26
|
|
|
53
|
-
// we want to avoid the ANTLR4 reparse in the worker thread, this should hit our main thread cache
|
|
54
|
-
const parse = parserWrapper.parse(query);
|
|
55
|
-
const statements = parse.statementsParsing;
|
|
56
|
-
|
|
57
|
-
const anySyntacticError = statements.some(
|
|
58
|
-
(statement) => statement.syntaxErrors.length !== 0,
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
if (anySyntacticError) {
|
|
62
|
-
return [];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
27
|
try {
|
|
66
28
|
if (lastSemanticJob !== undefined && !lastSemanticJob.resolved) {
|
|
67
29
|
void lastSemanticJob.cancel();
|
|
68
30
|
}
|
|
69
31
|
|
|
70
32
|
const proxyWorker = (await pool.proxy()) as unknown as LintWorker;
|
|
71
|
-
lastSemanticJob = proxyWorker.
|
|
33
|
+
lastSemanticJob = proxyWorker.lintCypherQuery(
|
|
72
34
|
query,
|
|
73
35
|
config.schema ?? {},
|
|
36
|
+
config.featureFlags ?? {},
|
|
74
37
|
);
|
|
75
38
|
const result = await lastSemanticJob;
|
|
76
39
|
|
|
77
|
-
|
|
40
|
+
const a: Diagnostic[] = result.map((diagnostic) => {
|
|
78
41
|
return {
|
|
79
|
-
from:
|
|
80
|
-
to:
|
|
42
|
+
from: diagnostic.offsets.start,
|
|
43
|
+
to: diagnostic.offsets.end,
|
|
81
44
|
severity:
|
|
82
|
-
|
|
83
|
-
|
|
45
|
+
diagnostic.severity === DiagnosticSeverity.Error
|
|
46
|
+
? 'error'
|
|
47
|
+
: 'warning',
|
|
48
|
+
message: diagnostic.message,
|
|
49
|
+
...(diagnostic.tags !== undefined &&
|
|
50
|
+
diagnostic.tags.includes(DiagnosticTag.Deprecated)
|
|
51
|
+
? { markClass: 'cm-deprecated-element' }
|
|
52
|
+
: {}),
|
|
84
53
|
};
|
|
85
54
|
});
|
|
55
|
+
return a;
|
|
86
56
|
} catch (err) {
|
|
87
57
|
if (!(err instanceof workerpool.Promise.CancellationError)) {
|
|
88
58
|
console.error(String(err) + ' ' + query);
|