feat(mongodb): show shell command diagnostics while editing
This commit is contained in:
1 parent
9444f0439e
commit
28c219ff03
4 files changed
+331
-7
No files matched your search
@@ -219,6 +219,7 @@ import {
|
||||
} from "@/lib/sql/semantic/diagnostics";
|
||||
import { resolveSqlDialectId, sqlReferenceAnalysisDialectFor } from "@/lib/sql/semantic/dialect";
|
||||
import { buildRedisSyntaxDiagnostics, shouldRunRedisDiagnostics } from "@/lib/redis/redisSyntaxDiagnostics";
|
||||
import { buildMongoSyntaxDiagnostics } from "@/lib/mongo/mongoSyntaxDiagnostics";
|
||||
import { buildRedisCompletionItemsFromContext, getRedisCompletionContext, getRedisCompletionResultValidFor, shouldAutoOpenRedisCompletion, takesKeyArgument, type RedisCompletionItem } from "@/lib/redis/redisCompletion";
|
||||
import type { SqlCompletionColumn, SqlCompletionContext, SqlCompletionForeignKey, SqlCompletionItem, SqlCompletionObject, SqlCompletionReferencedTable, SqlCompletionTable } from "@/lib/sql/sqlCompletion";
|
||||
import type { CompletionAssistantObjectKind, ColumnInfo, DatabaseType, IndexInfo, SqlReferenceAnalysis, SqlServerCompletionContext, SqlTableReference, SqlTextSpan } from "@/types/database";
|
||||
@@ -3680,7 +3681,7 @@ function invalidateSemanticDiagnosticsForDocumentChange() {
|
||||
}
|
||||
|
||||
function shouldSkipSqlSemanticDiagnostics() {
|
||||
return props.databaseType === "victoriametrics" || (props.databaseType !== "redis" && !settingsStore.editorSettings.sqlSemanticDiagnosticsEnabled);
|
||||
return props.databaseType === "victoriametrics" || (props.databaseType !== "redis" && props.databaseType !== "mongodb" && !settingsStore.editorSettings.sqlSemanticDiagnosticsEnabled);
|
||||
}
|
||||
|
||||
function rangesOverlap(left: { from: number; to: number }, right: { from: number; to: number }): boolean {
|
||||
@@ -3822,10 +3823,16 @@ async function refreshSemanticDiagnostics(options: { preserveOutsideRanges?: boo
|
||||
setSemanticDiagnostics([]);
|
||||
return;
|
||||
}
|
||||
if (props.databaseType === "mongodb" || props.databaseType === "elasticsearch" || props.databaseType === "easysearch" || props.databaseType === "meilisearch" || props.databaseType === "victoriametrics") {
|
||||
if (props.databaseType === "elasticsearch" || props.databaseType === "easysearch" || props.databaseType === "meilisearch" || props.databaseType === "victoriametrics") {
|
||||
setSemanticDiagnostics([]);
|
||||
return;
|
||||
}
|
||||
if (props.databaseType === "mongodb") {
|
||||
// Shell commands have no SQL semantics; surface the parser's own diagnosis instead of waiting for Run.
|
||||
const cursor = currentView.state.selection.main.head;
|
||||
setSemanticDiagnostics(buildMongoSyntaxDiagnostics(sql, cursor));
|
||||
return;
|
||||
}
|
||||
if (props.databaseType === "redis") {
|
||||
// Redis has no SQL semantics; run command-name / arity / quote / danger checks instead.
|
||||
if (!shouldRunRedisDiagnostics(sql, currentView.state.selection.main.head)) {
|
||||
@@ -7830,7 +7837,7 @@ watch(
|
||||
watch(
|
||||
() => settingsStore.editorSettings.sqlSemanticDiagnosticsEnabled,
|
||||
(enabled) => {
|
||||
if (props.databaseType === "redis" || props.databaseType === "victoriametrics") return;
|
||||
if (props.databaseType === "redis" || props.databaseType === "mongodb" || props.databaseType === "victoriametrics") return;
|
||||
if (!shouldSkipSqlSemanticDiagnostics() && enabled) {
|
||||
scheduleSemanticDiagnostics(0);
|
||||
return;
|
||||
|
||||
@@ -1342,13 +1342,13 @@ function parseMethodArgs(source: string, methodCallIndex: number): string[] | nu
|
||||
return splitTopLevel(source.slice(openIndex + 1, closeIndex));
|
||||
}
|
||||
|
||||
interface MongoTextRange {
|
||||
export interface MongoTextRange {
|
||||
from: number;
|
||||
to: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
function splitMongoCommandTextRanges(input: string): MongoTextRange[] {
|
||||
export function splitMongoCommandTextRanges(input: string): MongoTextRange[] {
|
||||
const commands: MongoTextRange[] = [];
|
||||
for (const segment of splitMongoSemicolonSeparatedSegments(input)) {
|
||||
const parsed = parseMongoCommand(segment.text);
|
||||
@@ -1856,7 +1856,7 @@ export function validateBulkWriteOptions(optionsJson: string): string | null {
|
||||
}
|
||||
|
||||
/** Filters of every non-insert operation, for the safety checks. */
|
||||
function bulkWriteFilters(operationsJson: string): string[] {
|
||||
export function bulkWriteFilters(operationsJson: string): string[] {
|
||||
try {
|
||||
const entries = JSON.parse(operationsJson) as Array<Record<string, { filter?: unknown }>>;
|
||||
return entries.flatMap((entry) => Object.values(entry)).flatMap((spec) => (isDocument(spec?.filter) ? [JSON.stringify(spec.filter)] : []));
|
||||
@@ -1879,7 +1879,7 @@ function mongoWriteFilter(command: MongoWriteCommand): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
function mongoFilterIsEffectivelyUnbounded(json: string): boolean {
|
||||
export function mongoFilterIsEffectivelyUnbounded(json: string): boolean {
|
||||
const parsed = parseNormalizedJson(json);
|
||||
return !isRecord(parsed) || mongoFilterContainsOpaqueLogic(parsed) || mongoFilterObjectIsUnbounded(parsed);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* MongoDB shell command diagnostics for the query editor.
|
||||
*
|
||||
* Mirrors `redisSyntaxDiagnostics.ts` so the editor's diagnostic pipeline
|
||||
* (StateField + Decoration.mark) renders them with no extra wiring. The source is
|
||||
* split into commands with the same splitter the executor uses, so an underline
|
||||
* always corresponds to a statement that would fail or warrant care when run.
|
||||
*/
|
||||
import type { SqlTextSpan } from "@/types/database";
|
||||
import type { SqlSemanticDiagnostic } from "@/lib/sql/semantic/diagnostics";
|
||||
import { findMatchingParen, hasUnclosedMongoDelimiters, trimMongoOuterComments } from "@dbx-app/mongo-shell";
|
||||
import { bulkWriteFilters, describeMongoCommandParseFailure, mongoFilterIsEffectivelyUnbounded, parseMongoCommand, splitMongoCommandTextRanges, type MongoCommand, type MongoTextRange } from "@/lib/mongo/mongoShellCommand";
|
||||
|
||||
const UNCLOSED_MESSAGE = "MongoDB command has unclosed parentheses, brackets, braces, or strings.";
|
||||
|
||||
/**
|
||||
* `db.<collection>.<method>` / `db["…"].<method>` / `db.getCollection("…").<method>` /
|
||||
* `db.getSiblingDB("…").<collection>.<method>` / `db.<method>` — the part worth underlining by default.
|
||||
* Call forms come first so `.getCollection("x")` is consumed whole instead of stopping at the name.
|
||||
*/
|
||||
const COMMAND_HEAD = /^\s*db\s*(?:\.\s*(?:getCollection|getSiblingDB)\s*\([^)]*\)|\.\s*[A-Za-z_$][\w$]*|\[[^\]]*\])*(?:\s*\.\s*[A-Za-z_$][\w$]*)?/;
|
||||
|
||||
export function buildMongoSyntaxDiagnostics(source: string, cursor = -1): SqlSemanticDiagnostic[] {
|
||||
const diagnostics: SqlSemanticDiagnostic[] = [];
|
||||
const segments = splitMongoCommandTextRanges(source);
|
||||
const current = segmentAtCursor(segments, cursor, source.length);
|
||||
|
||||
for (const segment of segments) {
|
||||
const text = trimMongoOuterComments(segment.text).trim().replace(/;$/, "").trim();
|
||||
if (!text) continue;
|
||||
|
||||
if (hasUnclosedMongoDelimiters(text)) {
|
||||
// A command still being typed under the cursor is not an error yet.
|
||||
if (segment === current) continue;
|
||||
diagnostics.push({ span: spanFor(source, segment.from, segment.to), message: UNCLOSED_MESSAGE, severity: "error" });
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsed = parseMongoCommand(segment.text);
|
||||
if (!parsed) {
|
||||
const message = describeMongoCommandParseFailure(segment.text);
|
||||
diagnostics.push({ span: errorSpan(source, segment, message), severity: "error", message });
|
||||
continue;
|
||||
}
|
||||
|
||||
const warning = describeMongoWriteRisk(parsed.command);
|
||||
if (warning) diagnostics.push({ span: headSpan(source, segment), severity: "warning", message: warning });
|
||||
}
|
||||
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hold off while the cursor sits inside a command whose delimiters are still
|
||||
* open — the user is mid-expression and everything after that point is noise.
|
||||
* Mirrors the intent of `shouldRunRedisDiagnostics`.
|
||||
*/
|
||||
export function shouldRunMongoDiagnostics(source: string, cursor: number): boolean {
|
||||
if (!source.trim()) return false;
|
||||
const current = segmentAtCursor(splitMongoCommandTextRanges(source), cursor, source.length);
|
||||
if (!current) return true;
|
||||
const text = trimMongoOuterComments(current.text).trim();
|
||||
return !text || !hasUnclosedMongoDelimiters(text);
|
||||
}
|
||||
|
||||
/** The command the cursor is in, counting the whitespace after it up to the next command. */
|
||||
function segmentAtCursor(segments: readonly MongoTextRange[], cursor: number, sourceLength: number): MongoTextRange | undefined {
|
||||
if (cursor < 0) return undefined;
|
||||
return segments.find((segment, index) => cursor >= segment.from && cursor <= (segments[index + 1]?.from ?? sourceLength));
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Warnings for commands that parse but deserve a second look
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
function describeMongoWriteRisk(command: MongoCommand): string | null {
|
||||
switch (command.kind) {
|
||||
case "update":
|
||||
case "delete": {
|
||||
if (!mongoFilterIsEffectivelyUnbounded(command.filter)) return null;
|
||||
const verb = command.kind === "update" ? "update" : "delete";
|
||||
return command.many ? `This ${verb} has no effective filter and will ${verb} every document in ${command.collection}.` : `This ${verb} has no effective filter and will ${verb} an arbitrary document in ${command.collection}.`;
|
||||
}
|
||||
case "replace":
|
||||
return mongoFilterIsEffectivelyUnbounded(command.filter) ? `This replace has no effective filter and will replace an arbitrary document in ${command.collection}.` : null;
|
||||
case "findOneAndUpdate":
|
||||
case "findOneAndReplace":
|
||||
case "findOneAndDelete": {
|
||||
if (!mongoFilterIsEffectivelyUnbounded(command.filter)) return null;
|
||||
const verb = command.kind === "findOneAndUpdate" ? "update" : command.kind === "findOneAndReplace" ? "replace" : "delete";
|
||||
return `This ${command.kind} has no effective filter and will ${verb} an arbitrary document in ${command.collection}.`;
|
||||
}
|
||||
case "bulkWrite":
|
||||
return bulkWriteFilters(command.operations).some(mongoFilterIsEffectivelyUnbounded) ? `This bulkWrite contains an operation with no effective filter, which can affect every document in ${command.collection}.` : null;
|
||||
case "dropCollection":
|
||||
return `This drops the ${command.collection} collection and all of its documents.`;
|
||||
case "inDatabase":
|
||||
// db.getSiblingDB("x").coll.deleteMany({}) is just as destructive as the unwrapped form.
|
||||
return describeMongoWriteRisk(command.command);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Span placement
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
/** Underline the part the message is about when it can be found in the text; otherwise the command head. */
|
||||
function errorSpan(source: string, segment: MongoTextRange, message: string): SqlTextSpan {
|
||||
const text = segment.text;
|
||||
|
||||
const constructor = /Unsupported value (?:new )?([A-Za-z_$][\w$]*)\(\.\.\.\)/.exec(message);
|
||||
if (constructor) {
|
||||
const callIndex = indexOutsideStrings(text, new RegExp(`\\b${escapeRegExp(constructor[1]!)}\\s*\\(`));
|
||||
if (callIndex >= 0) {
|
||||
const openIndex = text.indexOf("(", callIndex);
|
||||
const closeIndex = findMatchingParen(text, openIndex);
|
||||
return spanFor(source, segment.from + callIndex, segment.from + (closeIndex >= 0 ? closeIndex + 1 : openIndex + 1));
|
||||
}
|
||||
}
|
||||
|
||||
const operator = /must not contain update operators such as (\$[A-Za-z]+)/.exec(message);
|
||||
if (operator) {
|
||||
const keyIndex = indexOutsideStrings(text, new RegExp(escapeRegExp(operator[1]!)));
|
||||
if (keyIndex >= 0) return spanFor(source, segment.from + keyIndex, segment.from + keyIndex + operator[1]!.length);
|
||||
}
|
||||
|
||||
const trailing = /Unexpected text after [^:]+: "(.+)"\.$/.exec(message);
|
||||
if (trailing) {
|
||||
const tailIndex = text.lastIndexOf(trailing[1]!.replace(/…$/, ""));
|
||||
if (tailIndex >= 0) return spanFor(source, segment.from + tailIndex, segment.to);
|
||||
}
|
||||
|
||||
return headSpan(source, segment);
|
||||
}
|
||||
|
||||
function headSpan(source: string, segment: MongoTextRange): SqlTextSpan {
|
||||
const text = segment.text;
|
||||
const leading = text.length - text.trimStart().length;
|
||||
const head = COMMAND_HEAD.exec(text);
|
||||
const headEnd = head && head[0].trim() ? head[0].length : text.indexOf("\n", leading) === -1 ? text.length : text.indexOf("\n", leading);
|
||||
return spanFor(source, segment.from + leading, segment.from + Math.max(headEnd, leading + 1));
|
||||
}
|
||||
|
||||
/** 1-based line/column span for the absolute offsets [from, to). `end_column` is inclusive. */
|
||||
function spanFor(source: string, from: number, to: number): SqlTextSpan {
|
||||
const start = lineColumnAt(source, from);
|
||||
const end = lineColumnAt(source, Math.max(to - 1, from));
|
||||
return { start_line: start.line, start_column: start.column, end_line: end.line, end_column: end.column };
|
||||
}
|
||||
|
||||
function lineColumnAt(source: string, offset: number): { line: number; column: number } {
|
||||
let line = 1;
|
||||
let lineStart = 0;
|
||||
for (let index = 0; index < offset && index < source.length; index += 1) {
|
||||
if (source[index] === "\n") {
|
||||
line += 1;
|
||||
lineStart = index + 1;
|
||||
}
|
||||
}
|
||||
return { line, column: offset - lineStart + 1 };
|
||||
}
|
||||
|
||||
function indexOutsideStrings(text: string, pattern: RegExp): number {
|
||||
let quote: string | null = null;
|
||||
let escaped = false;
|
||||
for (let index = 0; index < text.length; index += 1) {
|
||||
const char = text[index]!;
|
||||
if (quote) {
|
||||
if (escaped) escaped = false;
|
||||
else if (char === "\\") escaped = true;
|
||||
else if (char === quote) quote = null;
|
||||
continue;
|
||||
}
|
||||
if (char === '"' || char === "'") {
|
||||
quote = char;
|
||||
continue;
|
||||
}
|
||||
if (pattern.test(text.slice(index)) && text.slice(index).search(pattern) === 0) return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { test } from "vitest";
|
||||
import { buildMongoSyntaxDiagnostics, shouldRunMongoDiagnostics } from "../../apps/desktop/src/lib/mongo/mongoSyntaxDiagnostics";
|
||||
|
||||
/** The exact text a diagnostic underlines, so span placement is asserted and not just line numbers. */
|
||||
function underlined(source: string, cursor = -1): Array<{ severity: string; text: string; message: string }> {
|
||||
const lines = source.split("\n");
|
||||
return buildMongoSyntaxDiagnostics(source, cursor).map((diagnostic) => {
|
||||
const { span } = diagnostic;
|
||||
const text =
|
||||
span.start_line === span.end_line ? lines[span.start_line - 1]!.slice(span.start_column - 1, span.end_column) : [lines[span.start_line - 1]!.slice(span.start_column - 1), ...lines.slice(span.start_line, span.end_line - 1), lines[span.end_line - 1]!.slice(0, span.end_column)].join("\n");
|
||||
return { severity: diagnostic.severity, text, message: diagnostic.message };
|
||||
});
|
||||
}
|
||||
|
||||
test("valid commands and comments produce no diagnostics", () => {
|
||||
for (const source of ["db.reports.find({a: 1})", 'db["my-coll"].updateOne({a: 1}, {$set: {b: 2}}, {upsert: true})', "// just a note", "db.a.find({});\ndb.b.countDocuments({})", ""]) {
|
||||
assert.deepEqual(buildMongoSyntaxDiagnostics(source), [], source);
|
||||
}
|
||||
});
|
||||
|
||||
test("underlines the offending value constructor and names the argument", () => {
|
||||
const [single] = underlined('db.reports.find({id: Foo("x")})');
|
||||
assert.equal(single?.severity, "error");
|
||||
assert.equal(single?.text, 'Foo("x")');
|
||||
assert.match(single!.message, /Unsupported value Foo\(\.\.\.\) in the filter argument of find\(\)/);
|
||||
|
||||
// Works across lines and through `new`, and finds the call inside a nested document.
|
||||
const [nested] = underlined("db.reports.updateOne(\n { phone: '+84' },\n { $set: { t: new Bar(1) } }\n)");
|
||||
assert.equal(nested?.text, "Bar(1)");
|
||||
assert.match(nested!.message, /update argument of updateOne\(\)/);
|
||||
});
|
||||
|
||||
test("underlines the update operator in a replaceOne replacement", () => {
|
||||
const [diagnostic] = underlined("db.reports.replaceOne({a: 1}, {$set: {b: 2}})");
|
||||
assert.equal(diagnostic?.text, "$set");
|
||||
assert.match(diagnostic!.message, /use updateOne\(\)/);
|
||||
});
|
||||
|
||||
test("underlines the command head for unsupported methods and wrong argument shapes", () => {
|
||||
const [unsupported] = underlined('db.reports.mapReduce("x")');
|
||||
assert.equal(unsupported?.text, "db.reports.mapReduce");
|
||||
assert.match(unsupported!.message, /mapReduce\(\) is not supported/);
|
||||
|
||||
const [shape] = underlined("db.reports.insertOne({a: 1}, {writeConcern: {w: 1}})");
|
||||
assert.equal(shape?.text, "db.reports.insertOne");
|
||||
assert.equal(shape?.message, "insertOne() expects one document.");
|
||||
|
||||
const [dbLevel] = underlined("db.adminCommand({ping: 1})");
|
||||
assert.equal(dbLevel?.text, "db.adminCommand");
|
||||
});
|
||||
|
||||
test("warns about writes with no effective filter and about dropping a collection", () => {
|
||||
const [every] = underlined("db.reports.deleteMany({})");
|
||||
assert.equal(every?.severity, "warning");
|
||||
assert.equal(every?.text, "db.reports.deleteMany");
|
||||
assert.match(every!.message, /every document in reports/);
|
||||
|
||||
const [arbitrary] = underlined("db.reports.updateOne({}, {$set: {a: 1}})");
|
||||
assert.match(arbitrary!.message, /an arbitrary document in reports/);
|
||||
assert.match(underlined("db.reports.replaceOne({_id: {$exists: true}}, {a: 1})")[0]!.message, /replace .* arbitrary document/);
|
||||
assert.match(underlined("db.reports.drop()")[0]!.message, /drops the reports collection/);
|
||||
assert.equal(underlined('db.getCollection("my-coll").drop()')[0]?.text, 'db.getCollection("my-coll").drop');
|
||||
|
||||
assert.match(underlined("db.reports.bulkWrite([{ insertOne: { document: { a: 1 } } }, { deleteMany: { filter: {} } }])")[0]!.message, /bulkWrite contains an operation with no effective filter/);
|
||||
|
||||
// Wrapping the command in getSiblingDB() does not hide it.
|
||||
const [sibling] = underlined('db.getSiblingDB("archive").reports.deleteMany({})');
|
||||
assert.equal(sibling?.severity, "warning");
|
||||
assert.equal(sibling?.text, 'db.getSiblingDB("archive").reports.deleteMany');
|
||||
assert.match(sibling!.message, /every document in reports/);
|
||||
|
||||
// A bounded write is not a warning.
|
||||
assert.deepEqual(buildMongoSyntaxDiagnostics("db.reports.deleteMany({status: 'stale'})"), []);
|
||||
assert.deepEqual(buildMongoSyntaxDiagnostics("db.reports.bulkWrite([{ deleteMany: { filter: { status: 'stale' } } }])"), []);
|
||||
assert.deepEqual(buildMongoSyntaxDiagnostics('db.getSiblingDB("archive").reports.deleteMany({status: "stale"})'), []);
|
||||
});
|
||||
|
||||
test("warns about find-and-modify commands with no effective filter", () => {
|
||||
for (const [source, action] of [
|
||||
["db.reports.findOneAndUpdate({}, {$set: {status: 'archived'}})", "update"],
|
||||
["db.reports.findOneAndReplace({}, {status: 'archived'})", "replace"],
|
||||
["db.reports.findOneAndDelete({})", "delete"],
|
||||
] as const) {
|
||||
const [diagnostic] = underlined(source);
|
||||
assert.equal(diagnostic?.severity, "warning", source);
|
||||
assert.match(diagnostic!.message, new RegExp(`${action} an arbitrary document in reports`), source);
|
||||
}
|
||||
|
||||
for (const source of ["db.reports.findOneAndUpdate({_id: 1}, {$set: {status: 'archived'}})", "db.reports.findOneAndReplace({_id: 1}, {status: 'archived'})", "db.reports.findOneAndDelete({_id: 1})"]) {
|
||||
assert.deepEqual(buildMongoSyntaxDiagnostics(source), [], source);
|
||||
}
|
||||
});
|
||||
|
||||
test("reports each statement separately", () => {
|
||||
const diagnostics = underlined("db.a.find({});\ndb.b.foo();\ndb.c.find({})");
|
||||
assert.equal(diagnostics.length, 1);
|
||||
assert.equal(diagnostics[0]?.text, "db.b.foo");
|
||||
});
|
||||
|
||||
test("does not flag a command the cursor is still typing", () => {
|
||||
const typing = "db.reports.find({a: 1";
|
||||
assert.deepEqual(buildMongoSyntaxDiagnostics(typing, typing.length), []);
|
||||
assert.deepEqual(buildMongoSyntaxDiagnostics(`${typing}\n`, typing.length + 1), []);
|
||||
assert.equal(shouldRunMongoDiagnostics(typing, typing.length), false);
|
||||
|
||||
// Once the cursor has left it, an unclosed command is an error.
|
||||
assert.equal(shouldRunMongoDiagnostics("db.reports.find({a: 1})", 23), true);
|
||||
const [left] = underlined(typing, -1);
|
||||
assert.equal(left?.severity, "error");
|
||||
assert.match(left!.message, /unclosed/i);
|
||||
assert.equal(shouldRunMongoDiagnostics("", 0), false);
|
||||
});
|
||||
|
||||
test("keeps completed diagnostics while the cursor is in an unfinished command", () => {
|
||||
const source = "db.a.deleteMany({});\ndb.b.find({";
|
||||
const [diagnostic] = underlined(source, source.length);
|
||||
assert.equal(diagnostic?.severity, "warning");
|
||||
assert.equal(diagnostic?.text, "db.a.deleteMany");
|
||||
assert.match(diagnostic!.message, /every document in a/);
|
||||
|
||||
const unfinished = underlined(source, 0).find((diagnostic) => diagnostic.severity === "error");
|
||||
assert.match(unfinished!.message, /unclosed/i);
|
||||
|
||||
const queryEditorSource = readFileSync("apps/desktop/src/components/editor/QueryEditor.vue", "utf8");
|
||||
const mongoBranch = queryEditorSource.slice(queryEditorSource.indexOf('if (props.databaseType === "mongodb")'), queryEditorSource.indexOf('if (props.databaseType === "redis")'));
|
||||
assert.doesNotMatch(mongoBranch, /shouldRunMongoDiagnostics/);
|
||||
assert.match(mongoBranch, /setSemanticDiagnostics\(buildMongoSyntaxDiagnostics\(sql, cursor\)\)/);
|
||||
});
|
||||
Reference in new issue
Block a user