codesearch-mcp 0.2.2-alpha → 0.2.3-alpha
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 +34 -0
- package/README.md +15 -8
- package/dist/app.js +45 -7
- package/dist/src/commands/EvalCommand.d.ts +10 -0
- package/dist/src/commands/EvalCommand.js +231 -0
- package/dist/src/commands/IndexCommand.js +79 -53
- package/dist/src/db/index.d.ts +1 -1
- package/dist/src/db/index.js +1 -2
- package/dist/src/db/schema.d.ts +5 -29
- package/dist/src/db/schema.js +2 -8
- package/dist/src/dependency-traversal.d.ts +16 -0
- package/dist/src/dependency-traversal.js +45 -0
- package/dist/src/enriched-prompt.d.ts +3 -2
- package/dist/src/enriched-prompt.js +3 -2
- package/dist/src/eval-results.d.ts +12 -0
- package/dist/src/eval-results.js +114 -0
- package/dist/src/file-utils.d.ts +15 -0
- package/dist/src/file-utils.js +24 -1
- package/dist/src/help.js +4 -1
- package/dist/src/interfaces/IAstParser.d.ts +13 -1
- package/dist/src/interfaces/IDatabase.d.ts +0 -14
- package/dist/src/interfaces/IRetrievalEvaluator.d.ts +94 -0
- package/dist/src/interfaces/IRetrievalEvaluator.js +2 -0
- package/dist/src/interfaces/ISearcher.d.ts +11 -16
- package/dist/src/interfaces/index.d.ts +1 -0
- package/dist/src/project-scope.d.ts +19 -0
- package/dist/src/project-scope.js +49 -0
- package/dist/src/project-selector.d.ts +6 -0
- package/dist/src/project-selector.js +18 -0
- package/dist/src/retrieval-eval.d.ts +16 -0
- package/dist/src/retrieval-eval.js +214 -0
- package/dist/src/services/AstParserService.d.ts +66 -1
- package/dist/src/services/AstParserService.js +430 -25
- package/dist/src/services/DatabaseService.d.ts +0 -12
- package/dist/src/services/DatabaseService.js +2 -52
- package/dist/src/services/IndexerService.d.ts +8 -0
- package/dist/src/services/IndexerService.js +24 -11
- package/dist/src/services/McpServer.d.ts +7 -0
- package/dist/src/services/McpServer.js +42 -38
- package/dist/src/services/OpenAICompatibleEmbedder.js +2 -1
- package/dist/src/services/SearcherService.d.ts +189 -25
- package/dist/src/services/SearcherService.js +505 -203
- package/dist/src/symbol-rows.d.ts +14 -9
- package/dist/src/symbol-rows.js +17 -11
- package/dist/src/tools/FindSymbolTool.js +14 -5
- package/dist/src/tools/GetCallersTool.d.ts +3 -1
- package/dist/src/tools/GetCallersTool.js +16 -6
- package/dist/src/tools/GetDependenciesTool.d.ts +3 -1
- package/dist/src/tools/GetDependenciesTool.js +15 -8
- package/dist/src/tools/GetDependentsTool.d.ts +3 -1
- package/dist/src/tools/GetDependentsTool.js +31 -16
- package/dist/src/tools/ListFilesTool.d.ts +3 -1
- package/dist/src/tools/ListFilesTool.js +14 -12
- package/dist/src/tools/ListProjectsTool.js +5 -1
- package/dist/src/tools/ReadFileRangeTool.d.ts +3 -1
- package/dist/src/tools/ReadFileRangeTool.js +12 -11
- package/dist/src/tools/ReadFileTool.d.ts +3 -1
- package/dist/src/tools/ReadFileTool.js +12 -10
- package/dist/src/tools/SearchCodebaseTool.js +3 -1
- package/dist/src/tools/format-helpers.d.ts +16 -1
- package/dist/src/tools/format-helpers.js +64 -0
- package/package.json +9 -8
|
@@ -38,6 +38,12 @@ const path = __importStar(require("path"));
|
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
39
|
const ts_morph_1 = require("ts-morph");
|
|
40
40
|
const LoggerService_1 = require("./LoggerService");
|
|
41
|
+
const GRAPHQL_MODULE = '@nestjs/graphql';
|
|
42
|
+
const GRAPHQL_TYPE_DECORATORS = new Set(['ObjectType', 'InputType', 'InterfaceType']);
|
|
43
|
+
const GRAPHQL_OPERATION_KIND = {
|
|
44
|
+
Query: 'graphql_query', Mutation: 'graphql_mutation', Subscription: 'graphql_subscription',
|
|
45
|
+
};
|
|
46
|
+
const GRAPHQL_NAME_RE = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
|
41
47
|
const FALLBACK_OPTS = {
|
|
42
48
|
skipAddingFilesFromTsConfig: true,
|
|
43
49
|
skipFileDependencyResolution: false,
|
|
@@ -47,9 +53,11 @@ const MORPH_CACHE_MAX = 10;
|
|
|
47
53
|
const VALID_SEGMENT = /^#?[\w$]+$/;
|
|
48
54
|
class AstParserService {
|
|
49
55
|
logger;
|
|
56
|
+
deps;
|
|
50
57
|
morphProjects = new Map();
|
|
51
|
-
constructor(logger = LoggerService_1.LoggerService.silent()) {
|
|
58
|
+
constructor(logger = LoggerService_1.LoggerService.silent(), deps = {}) {
|
|
52
59
|
this.logger = logger;
|
|
60
|
+
this.deps = deps;
|
|
53
61
|
}
|
|
54
62
|
// ── public interface ────────────────────────────────────────────────────────
|
|
55
63
|
async analyzeProject(filePaths, serviceName, projectRoot = process.cwd(), opts = {}) {
|
|
@@ -61,7 +69,7 @@ class AstParserService {
|
|
|
61
69
|
const morphProject = this.createMorphProject(projectRoot, skipDependencyResolution, rootTsConfigPath);
|
|
62
70
|
for (const filePath of filePaths) {
|
|
63
71
|
if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
|
|
64
|
-
analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [] });
|
|
72
|
+
analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] });
|
|
65
73
|
continue;
|
|
66
74
|
}
|
|
67
75
|
try {
|
|
@@ -69,7 +77,7 @@ class AstParserService {
|
|
|
69
77
|
}
|
|
70
78
|
catch (err) {
|
|
71
79
|
this.logger.warn('AstParser: failed to add source file', { filePath, reason: err.message });
|
|
72
|
-
analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [] });
|
|
80
|
+
analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] });
|
|
73
81
|
}
|
|
74
82
|
}
|
|
75
83
|
for (const sourceFile of morphProject.getSourceFiles()) {
|
|
@@ -101,7 +109,7 @@ class AstParserService {
|
|
|
101
109
|
this.logger.debug('AstParser: analyzeFile start', { filePath, serviceName, projectRoot });
|
|
102
110
|
if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
|
|
103
111
|
this.logger.debug('AstParser: analyzeFile skip (non-TS)', { filePath, ms: Date.now() - t0 });
|
|
104
|
-
return { edges: [], symbols: [], calls: [], members: [] };
|
|
112
|
+
return { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] };
|
|
105
113
|
}
|
|
106
114
|
const morphProject = this.getMorphProject(projectRoot);
|
|
107
115
|
const existing = morphProject.getSourceFile(filePath);
|
|
@@ -113,7 +121,7 @@ class AstParserService {
|
|
|
113
121
|
}
|
|
114
122
|
catch (err) {
|
|
115
123
|
this.logger.warn('AstParser: analyzeFile failed to add source file', { filePath, reason: err.message, ms: Date.now() - t0 });
|
|
116
|
-
return { edges: [], symbols: [], calls: [], members: [] };
|
|
124
|
+
return { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] };
|
|
117
125
|
}
|
|
118
126
|
const result = this.analyzeSourceFile(sourceFile, filePath, serviceName);
|
|
119
127
|
morphProject.removeSourceFile(sourceFile);
|
|
@@ -148,11 +156,24 @@ class AstParserService {
|
|
|
148
156
|
const members = memberCandidates.map(c => c.entry);
|
|
149
157
|
const calls = this.extractCalls(sourceFile, filePath, serviceName);
|
|
150
158
|
const ownerRanges = this.buildOwnerRanges([...symbolCandidates, ...memberCandidates]);
|
|
151
|
-
|
|
159
|
+
// GraphQL-named rows are isolated like members: a bug here must never
|
|
160
|
+
// change a file's symbols, members, chunks, ownerPath or call graph.
|
|
161
|
+
// Runs after member extraction (decision 6 of `graphql-symbols`) — a
|
|
162
|
+
// GraphQL row is written only when the decorated declaration's own row
|
|
163
|
+
// was written to code_symbol.
|
|
164
|
+
let graphqlNames = [];
|
|
165
|
+
try {
|
|
166
|
+
graphqlNames = this.extractGraphqlNames(sourceFile, filePath, serviceName, [...symbolCandidates, ...memberCandidates]);
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
this.logger.warn('AstParser: GraphQL extraction failed', { filePath, reason: err.message });
|
|
170
|
+
graphqlNames = [];
|
|
171
|
+
}
|
|
172
|
+
return { edges, symbols, members, calls, ownerRanges, graphqlNames };
|
|
152
173
|
}
|
|
153
174
|
catch (err) {
|
|
154
175
|
this.logger.warn('AstParser: failed to analyze file', { filePath, reason: err.message });
|
|
155
|
-
return { edges: [], symbols: [], calls: [], members: [] };
|
|
176
|
+
return { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] };
|
|
156
177
|
}
|
|
157
178
|
}
|
|
158
179
|
// ── private: MorphProject cache (LRU, max MORPH_CACHE_MAX entries) ──────────
|
|
@@ -230,8 +251,19 @@ class AstParserService {
|
|
|
230
251
|
const edges = [];
|
|
231
252
|
const usageMap = this.buildUsageMap(sourceFile);
|
|
232
253
|
for (const decl of sourceFile.getImportDeclarations()) {
|
|
254
|
+
const namedImports = decl.getNamedImports();
|
|
255
|
+
const defaultImport = decl.getDefaultImport();
|
|
256
|
+
const namespaceImport = decl.getNamespaceImport();
|
|
257
|
+
const hasClause = Boolean(defaultImport || namespaceImport || namedImports.length > 0);
|
|
233
258
|
const resolved = decl.getModuleSpecifierSourceFile();
|
|
234
|
-
|
|
259
|
+
let toFile = resolved ? resolved.getFilePath() : null;
|
|
260
|
+
// A bindingless specifier never resolves through ts-morph's bound symbol
|
|
261
|
+
// (nothing is bound), even for a real project file — fall back to
|
|
262
|
+
// TypeScript's own resolver and map the result back onto the project.
|
|
263
|
+
if (!toFile && !hasClause) {
|
|
264
|
+
toFile = this.resolveSideEffectModuleSpecifier(decl, sourceFile, filePath);
|
|
265
|
+
}
|
|
266
|
+
if (!toFile) {
|
|
235
267
|
const specifier = decl.getModuleSpecifierValue();
|
|
236
268
|
const isCodeImport = !/\.(json|css|scss|less|svg|png|jpg|gif|ico|woff2?|ttf|eot)(\?.*)?$/.test(specifier);
|
|
237
269
|
if (specifier.startsWith('.') && isCodeImport) {
|
|
@@ -239,34 +271,37 @@ class AstParserService {
|
|
|
239
271
|
}
|
|
240
272
|
continue;
|
|
241
273
|
}
|
|
242
|
-
const toFile = resolved.getFilePath();
|
|
243
274
|
if (toFile.includes('/node_modules/'))
|
|
244
275
|
continue;
|
|
245
|
-
|
|
246
|
-
const defaultImport = decl.getDefaultImport();
|
|
247
|
-
const namespaceImport = decl.getNamespaceImport();
|
|
248
|
-
if (namedImports.length > 0) {
|
|
249
|
-
for (const named of namedImports) {
|
|
250
|
-
const localName = named.getAliasNode()?.getText() ?? named.getName();
|
|
251
|
-
const fromSymbol = this.resolveFromSymbol(localName, usageMap);
|
|
252
|
-
edges.push({
|
|
253
|
-
project: serviceName, fromFile: filePath, fromSymbol,
|
|
254
|
-
toFile, toSymbol: named.getName(), depType: 'import',
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
else if (defaultImport) {
|
|
276
|
+
if (!hasClause) {
|
|
259
277
|
edges.push({
|
|
260
278
|
project: serviceName, fromFile: filePath, fromSymbol: null,
|
|
279
|
+
toFile, toSymbol: null, depType: 'import-side-effect',
|
|
280
|
+
});
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (defaultImport) {
|
|
284
|
+
const fromSymbol = this.resolveFromSymbol(defaultImport.getText(), usageMap);
|
|
285
|
+
edges.push({
|
|
286
|
+
project: serviceName, fromFile: filePath, fromSymbol,
|
|
261
287
|
toFile, toSymbol: null, depType: 'import-default',
|
|
262
288
|
});
|
|
263
289
|
}
|
|
264
|
-
|
|
290
|
+
if (namespaceImport) {
|
|
291
|
+
const fromSymbol = this.resolveFromSymbol(namespaceImport.getText(), usageMap);
|
|
265
292
|
edges.push({
|
|
266
|
-
project: serviceName, fromFile: filePath, fromSymbol
|
|
293
|
+
project: serviceName, fromFile: filePath, fromSymbol,
|
|
267
294
|
toFile, toSymbol: null, depType: 'import-namespace',
|
|
268
295
|
});
|
|
269
296
|
}
|
|
297
|
+
for (const named of namedImports) {
|
|
298
|
+
const localName = named.getAliasNode()?.getText() ?? named.getName();
|
|
299
|
+
const fromSymbol = this.resolveFromSymbol(localName, usageMap);
|
|
300
|
+
edges.push({
|
|
301
|
+
project: serviceName, fromFile: filePath, fromSymbol,
|
|
302
|
+
toFile, toSymbol: named.getName(), depType: 'import',
|
|
303
|
+
});
|
|
304
|
+
}
|
|
270
305
|
}
|
|
271
306
|
for (const decl of sourceFile.getExportDeclarations()) {
|
|
272
307
|
const resolved = decl.getModuleSpecifierSourceFile();
|
|
@@ -293,6 +328,18 @@ class AstParserService {
|
|
|
293
328
|
}
|
|
294
329
|
return edges;
|
|
295
330
|
}
|
|
331
|
+
/** Manual fallback for a bindingless import specifier (design.md decision 2) —
|
|
332
|
+
* ts-morph's `getModuleSpecifierSourceFile()` is `null` for these even when
|
|
333
|
+
* the target is a real project file, since nothing is bound to resolve through. */
|
|
334
|
+
resolveSideEffectModuleSpecifier(decl, sourceFile, filePath) {
|
|
335
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
336
|
+
const compilerOptions = sourceFile.getProject().getCompilerOptions();
|
|
337
|
+
const result = ts_morph_1.ts.resolveModuleName(specifier, filePath, compilerOptions, ts_morph_1.ts.sys);
|
|
338
|
+
const resolvedFileName = result.resolvedModule?.resolvedFileName;
|
|
339
|
+
if (!resolvedFileName)
|
|
340
|
+
return null;
|
|
341
|
+
return sourceFile.getProject().getSourceFile(resolvedFileName)?.getFilePath() ?? null;
|
|
342
|
+
}
|
|
296
343
|
resolveFromSymbol(localName, usageMap) {
|
|
297
344
|
const users = usageMap.get(localName);
|
|
298
345
|
return users?.length === 1 ? users[0] : null;
|
|
@@ -559,6 +606,364 @@ class AstParserService {
|
|
|
559
606
|
endLine: c.declNode.getEndLineNumber(),
|
|
560
607
|
}));
|
|
561
608
|
}
|
|
609
|
+
// ── private: GraphQL-named rows (graphql-symbols) ───────────────────────────
|
|
610
|
+
/** Local name → the name exported by `@nestjs/graphql`, for every named import
|
|
611
|
+
* of that module specifier (aliases followed). Recognition is import-based,
|
|
612
|
+
* not type-checker-based, so `skipFileDependencyResolution` batches agree. */
|
|
613
|
+
buildGraphqlImportMap(sourceFile) {
|
|
614
|
+
const map = new Map();
|
|
615
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
616
|
+
if (decl.getModuleSpecifierValue() !== GRAPHQL_MODULE)
|
|
617
|
+
continue;
|
|
618
|
+
for (const named of decl.getNamedImports()) {
|
|
619
|
+
const exportedName = named.getName();
|
|
620
|
+
const localName = named.getAliasNode()?.getText() ?? exportedName;
|
|
621
|
+
map.set(localName, exportedName);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return map;
|
|
625
|
+
}
|
|
626
|
+
/** The identifier naming a decorator's expression (`@Foo` or the callee of `@Foo(...)`). */
|
|
627
|
+
static decoratorIdentifier(dec) {
|
|
628
|
+
const expr = dec.getExpression();
|
|
629
|
+
if (ts_morph_1.Node.isCallExpression(expr)) {
|
|
630
|
+
const callee = expr.getExpression();
|
|
631
|
+
return ts_morph_1.Node.isIdentifier(callee) ? callee : undefined;
|
|
632
|
+
}
|
|
633
|
+
return ts_morph_1.Node.isIdentifier(expr) ? expr : undefined;
|
|
634
|
+
}
|
|
635
|
+
/** `dec` recognised against `importMap` — its exported `@nestjs/graphql` name and its call arguments (`[]` for a bare `@Foo`). */
|
|
636
|
+
recognizedDecorator(dec, importMap) {
|
|
637
|
+
const idNode = AstParserService.decoratorIdentifier(dec);
|
|
638
|
+
if (!idNode)
|
|
639
|
+
return null;
|
|
640
|
+
const exported = importMap.get(idNode.getText());
|
|
641
|
+
if (!exported)
|
|
642
|
+
return null;
|
|
643
|
+
const expr = dec.getExpression();
|
|
644
|
+
const args = ts_morph_1.Node.isCallExpression(expr) ? expr.getArguments() : [];
|
|
645
|
+
return { name: exported, args };
|
|
646
|
+
}
|
|
647
|
+
static literalStringValue(node) {
|
|
648
|
+
if (ts_morph_1.Node.isStringLiteral(node) || ts_morph_1.Node.isNoSubstitutionTemplateLiteral(node))
|
|
649
|
+
return node.getLiteralValue();
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
static checkGraphqlGrammar(name) {
|
|
653
|
+
return GRAPHQL_NAME_RE.test(name) ? { ok: true, name } : { ok: false, reason: 'invalid-name', value: name };
|
|
654
|
+
}
|
|
655
|
+
/** `true` when any decorator argument is an (unwrapped) options object
|
|
656
|
+
* literal with `isAbstract: true` — an abstract base type (`PaginatedType`
|
|
657
|
+
* pattern) that NestJS never registers in the actual GraphQL schema. */
|
|
658
|
+
static isAbstractGraphqlType(args) {
|
|
659
|
+
return args.some((arg) => {
|
|
660
|
+
if (!ts_morph_1.Node.isObjectLiteralExpression(arg))
|
|
661
|
+
return false;
|
|
662
|
+
const prop = arg.getProperties().find((p) => {
|
|
663
|
+
if (!ts_morph_1.Node.isPropertyAssignment(p))
|
|
664
|
+
return false;
|
|
665
|
+
const nameNode = p.getNameNode();
|
|
666
|
+
if (ts_morph_1.Node.isIdentifier(nameNode))
|
|
667
|
+
return nameNode.getText() === 'isAbstract';
|
|
668
|
+
if (ts_morph_1.Node.isStringLiteral(nameNode))
|
|
669
|
+
return nameNode.getLiteralValue() === 'isAbstract';
|
|
670
|
+
return false;
|
|
671
|
+
});
|
|
672
|
+
const init = prop?.getInitializer();
|
|
673
|
+
return init !== undefined && ts_morph_1.Node.isTrueLiteral(init);
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
/** `obj`'s own `name` property (`PropertyAssignment` or `ShorthandPropertyAssignment`), if any. */
|
|
677
|
+
static findNameProperty(obj) {
|
|
678
|
+
return obj.getProperties().find((p) => {
|
|
679
|
+
if (!ts_morph_1.Node.isPropertyAssignment(p) && !ts_morph_1.Node.isShorthandPropertyAssignment(p))
|
|
680
|
+
return false;
|
|
681
|
+
const nameNode = p.getNameNode();
|
|
682
|
+
if (ts_morph_1.Node.isIdentifier(nameNode))
|
|
683
|
+
return nameNode.getText() === 'name';
|
|
684
|
+
if (ts_morph_1.Node.isStringLiteral(nameNode))
|
|
685
|
+
return nameNode.getLiteralValue() === 'name';
|
|
686
|
+
return false;
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
/** Strips parens/`as`/`satisfies`/non-null wrappers (any nesting) to find the
|
|
690
|
+
* underlying node — used only to classify an argument's *shape* (type
|
|
691
|
+
* function vs. options object vs. literal vs. other), never to read a name
|
|
692
|
+
* value through the wrapper (decision 4 of `graphql-symbols` forbids that). */
|
|
693
|
+
static peekThroughWrapper(node) {
|
|
694
|
+
let current = node;
|
|
695
|
+
let wrapped = false;
|
|
696
|
+
while (true) {
|
|
697
|
+
if (ts_morph_1.Node.isParenthesizedExpression(current) || ts_morph_1.Node.isAsExpression(current) || ts_morph_1.Node.isSatisfiesExpression(current) || ts_morph_1.Node.isNonNullExpression(current)) {
|
|
698
|
+
current = current.getExpression();
|
|
699
|
+
wrapped = true;
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
return { inner: current, wrapped };
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* GraphQL name from a decorator's arguments (design decision 4 of
|
|
708
|
+
* `graphql-symbols`) — syntactic, no wrapper looked through at any step.
|
|
709
|
+
* `allowOptionsName` gates step 2 (the `name` option — `Query`/`Mutation`/
|
|
710
|
+
* `Subscription`/`ResolveField` only); `fallbackName` is the TS name used at
|
|
711
|
+
* step 3.
|
|
712
|
+
*/
|
|
713
|
+
deriveGraphqlName(args, allowOptionsName, fallbackName) {
|
|
714
|
+
if (args.length > 0) {
|
|
715
|
+
const first = args[0];
|
|
716
|
+
const { inner, wrapped } = AstParserService.peekThroughWrapper(first);
|
|
717
|
+
const literal = AstParserService.literalStringValue(inner);
|
|
718
|
+
if (literal !== null) {
|
|
719
|
+
// A wrapped literal is still "a wrapped ... argument", an anything-else
|
|
720
|
+
// node (decision 4) — reading its value through the wrapper is exactly
|
|
721
|
+
// what "no wrapper is looked through" forbids.
|
|
722
|
+
if (wrapped)
|
|
723
|
+
return { ok: false, reason: 'non-literal-name' };
|
|
724
|
+
return AstParserService.checkGraphqlGrammar(literal);
|
|
725
|
+
}
|
|
726
|
+
const isTypeFnOrOptions = ts_morph_1.Node.isArrowFunction(inner) || ts_morph_1.Node.isFunctionExpression(inner) || ts_morph_1.Node.isObjectLiteralExpression(inner);
|
|
727
|
+
if (!isTypeFnOrOptions)
|
|
728
|
+
return { ok: false, reason: 'non-literal-name' };
|
|
729
|
+
// Type function / options object, wrapped or not — this is a routing
|
|
730
|
+
// decision only (go to step 2/3); nothing is read from it here, so its
|
|
731
|
+
// wrapper doesn't matter yet.
|
|
732
|
+
}
|
|
733
|
+
if (allowOptionsName) {
|
|
734
|
+
for (const arg of args) {
|
|
735
|
+
const { inner, wrapped } = AstParserService.peekThroughWrapper(arg);
|
|
736
|
+
if (!ts_morph_1.Node.isObjectLiteralExpression(inner))
|
|
737
|
+
continue;
|
|
738
|
+
// A wrapped options object cannot be trusted without looking through
|
|
739
|
+
// the wrapper, which decision 4 forbids — treat it as non-literal
|
|
740
|
+
// rather than silently skipping to the next argument or to step 3.
|
|
741
|
+
if (wrapped)
|
|
742
|
+
return { ok: false, reason: 'non-literal-name' };
|
|
743
|
+
const nameProp = AstParserService.findNameProperty(inner);
|
|
744
|
+
if (!nameProp)
|
|
745
|
+
continue;
|
|
746
|
+
if (ts_morph_1.Node.isShorthandPropertyAssignment(nameProp))
|
|
747
|
+
return { ok: false, reason: 'non-literal-name' };
|
|
748
|
+
const init = nameProp.getInitializer();
|
|
749
|
+
const value = init ? AstParserService.literalStringValue(init) : null;
|
|
750
|
+
if (value !== null)
|
|
751
|
+
return AstParserService.checkGraphqlGrammar(value);
|
|
752
|
+
return { ok: false, reason: 'non-literal-name' };
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return AstParserService.checkGraphqlGrammar(fallbackName);
|
|
756
|
+
}
|
|
757
|
+
logGraphqlSkip(filePath, declaration, decorator, reason, value) {
|
|
758
|
+
const fields = { filePath, declaration, decorator, reason };
|
|
759
|
+
if (value !== undefined)
|
|
760
|
+
fields.value = value;
|
|
761
|
+
this.logger.debug('AstParser: graphql row skipped', fields);
|
|
762
|
+
}
|
|
763
|
+
/** `declNode`'s qualified TypeScript name for diagnostics, independent of
|
|
764
|
+
* whether its own `code_symbol` row was actually written. */
|
|
765
|
+
declarationNameFor(node, fallback) {
|
|
766
|
+
const described = this.describeIndexedNode(node);
|
|
767
|
+
return described ? this.qualifiedName(described) : fallback;
|
|
768
|
+
}
|
|
769
|
+
/** The identifier named by a `@Resolver` argument: directly, or as an arrow
|
|
770
|
+
* function's expression body; or the sole identifier of a single-element
|
|
771
|
+
* array literal in either position (list-returning resolver). */
|
|
772
|
+
static extractResolverIdentifier(argNode) {
|
|
773
|
+
let target = argNode;
|
|
774
|
+
if (ts_morph_1.Node.isArrowFunction(target)) {
|
|
775
|
+
const body = target.getBody();
|
|
776
|
+
if (ts_morph_1.Node.isBlock(body))
|
|
777
|
+
return null;
|
|
778
|
+
target = body;
|
|
779
|
+
}
|
|
780
|
+
if (ts_morph_1.Node.isIdentifier(target))
|
|
781
|
+
return target;
|
|
782
|
+
if (ts_morph_1.Node.isArrayLiteralExpression(target)) {
|
|
783
|
+
const elements = target.getElements();
|
|
784
|
+
if (elements.length === 1 && ts_morph_1.Node.isIdentifier(elements[0]))
|
|
785
|
+
return elements[0];
|
|
786
|
+
}
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
defaultResolveDeclaration(identifier) {
|
|
790
|
+
const sym = identifier.getSymbol();
|
|
791
|
+
if (!sym)
|
|
792
|
+
return undefined;
|
|
793
|
+
const firstDecl = sym.getDeclarations()?.[0];
|
|
794
|
+
const resolved = firstDecl && ts_morph_1.Node.isImportSpecifier(firstDecl) ? (sym.getAliasedSymbol() ?? sym) : sym;
|
|
795
|
+
return resolved.getDeclarations()?.[0];
|
|
796
|
+
}
|
|
797
|
+
/** The GraphQL type name of a `@ResolveField`'s resolved parent class
|
|
798
|
+
* (design decision 5, second half): its recognised `@ObjectType` (else
|
|
799
|
+
* `@InterfaceType`) decorator, or its class name when it carries no
|
|
800
|
+
* decorator spelled `ObjectType`/`InterfaceType` at all, recognised or not. */
|
|
801
|
+
classGraphqlTypeName(declClass) {
|
|
802
|
+
const declImportMap = this.buildGraphqlImportMap(declClass.getSourceFile());
|
|
803
|
+
const decorators = declClass.getDecorators();
|
|
804
|
+
const hasAnyTypeSpelledDecorator = decorators.some((d) => {
|
|
805
|
+
const idNode = AstParserService.decoratorIdentifier(d);
|
|
806
|
+
const text = idNode?.getText();
|
|
807
|
+
return text === 'ObjectType' || text === 'InterfaceType';
|
|
808
|
+
});
|
|
809
|
+
const recognized = decorators
|
|
810
|
+
.map(d => this.recognizedDecorator(d, declImportMap))
|
|
811
|
+
.find(r => r?.name === 'ObjectType')
|
|
812
|
+
?? decorators.map(d => this.recognizedDecorator(d, declImportMap)).find(r => r?.name === 'InterfaceType');
|
|
813
|
+
if (recognized) {
|
|
814
|
+
const fallbackName = declClass.getName() ?? (declClass.isDefaultExport() ? 'default' : '');
|
|
815
|
+
const nameResult = this.deriveGraphqlName(recognized.args, false, fallbackName);
|
|
816
|
+
return nameResult.ok ? { ok: true, name: nameResult.name } : { ok: false };
|
|
817
|
+
}
|
|
818
|
+
// Carries a type-spelled decorator that isn't recognised (e.g. imported
|
|
819
|
+
// through a barrel) — its GraphQL name is unreadable; the class-name
|
|
820
|
+
// fallback never applies to it (it may differ from the unreadable name).
|
|
821
|
+
if (hasAnyTypeSpelledDecorator)
|
|
822
|
+
return { ok: false };
|
|
823
|
+
const name = declClass.getName();
|
|
824
|
+
if (!name)
|
|
825
|
+
return { ok: false };
|
|
826
|
+
return GRAPHQL_NAME_RE.test(name) ? { ok: true, name } : { ok: false, value: name };
|
|
827
|
+
}
|
|
828
|
+
/** The parent type of a `@ResolveField` — the enclosing class's recognised
|
|
829
|
+
* `@Resolver` argument (design decision 5, first half). */
|
|
830
|
+
resolveResolverParentType(cls, importMap) {
|
|
831
|
+
const resolverDec = cls.getDecorators()
|
|
832
|
+
.map(d => this.recognizedDecorator(d, importMap))
|
|
833
|
+
.find(r => r?.name === 'Resolver');
|
|
834
|
+
if (!resolverDec || resolverDec.args.length === 0)
|
|
835
|
+
return { ok: false };
|
|
836
|
+
const argNode = resolverDec.args[0];
|
|
837
|
+
const literal = AstParserService.literalStringValue(argNode);
|
|
838
|
+
if (literal !== null) {
|
|
839
|
+
return GRAPHQL_NAME_RE.test(literal) ? { ok: true, name: literal } : { ok: false, value: literal };
|
|
840
|
+
}
|
|
841
|
+
const identifier = AstParserService.extractResolverIdentifier(argNode);
|
|
842
|
+
if (!identifier)
|
|
843
|
+
return { ok: false };
|
|
844
|
+
const resolve = this.deps.resolveDeclaration ?? this.defaultResolveDeclaration.bind(this);
|
|
845
|
+
const declaration = resolve(identifier);
|
|
846
|
+
if (!declaration || !ts_morph_1.Node.isClassDeclaration(declaration))
|
|
847
|
+
return { ok: false };
|
|
848
|
+
return this.classGraphqlTypeName(declaration);
|
|
849
|
+
}
|
|
850
|
+
extractClassTypeRows(cls, importMap, ownByDeclNode, filePath, serviceName, entries) {
|
|
851
|
+
const own = ownByDeclNode.get(cls);
|
|
852
|
+
const declaration = this.declarationNameFor(cls, own?.entry.name ?? cls.getName() ?? 'default');
|
|
853
|
+
const seenNames = new Set();
|
|
854
|
+
for (const dec of cls.getDecorators()) {
|
|
855
|
+
const rec = this.recognizedDecorator(dec, importMap);
|
|
856
|
+
if (!rec || !GRAPHQL_TYPE_DECORATORS.has(rec.name))
|
|
857
|
+
continue;
|
|
858
|
+
if (AstParserService.isAbstractGraphqlType(rec.args)) {
|
|
859
|
+
this.logGraphqlSkip(filePath, declaration, rec.name, 'abstract-type');
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
if (!own) {
|
|
863
|
+
this.logGraphqlSkip(filePath, declaration, rec.name, 'no-symbol-row');
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
const nameResult = this.deriveGraphqlName(rec.args, false, own.entry.name);
|
|
867
|
+
if (!nameResult.ok) {
|
|
868
|
+
this.logGraphqlSkip(filePath, declaration, rec.name, nameResult.reason, nameResult.reason === 'invalid-name' ? nameResult.value : undefined);
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
if (seenNames.has(nameResult.name)) {
|
|
872
|
+
this.logGraphqlSkip(filePath, declaration, rec.name, 'duplicate', own.entry.name);
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
seenNames.add(nameResult.name);
|
|
876
|
+
entries.push({
|
|
877
|
+
project: serviceName, filePath, name: nameResult.name, kind: 'graphql_type',
|
|
878
|
+
signature: own.entry.signature, line: own.entry.line, endLine: own.entry.endLine, col: own.entry.col,
|
|
879
|
+
container: null, isStatic: false, target: declaration,
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
extractResolveFieldRow(method, rec, own, declaration, importMap, filePath, serviceName, entries) {
|
|
884
|
+
if (!own) {
|
|
885
|
+
this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'no-symbol-row');
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const cls = method.getParent();
|
|
889
|
+
if (!cls || !ts_morph_1.Node.isClassDeclaration(cls)) {
|
|
890
|
+
this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'unresolvable-parent');
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
const parent = this.resolveResolverParentType(cls, importMap);
|
|
894
|
+
if (!parent.ok) {
|
|
895
|
+
this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'unresolvable-parent', parent.value);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const nameResult = this.deriveGraphqlName(rec.args, true, own.entry.name);
|
|
899
|
+
if (!nameResult.ok) {
|
|
900
|
+
this.logGraphqlSkip(filePath, declaration, 'ResolveField', nameResult.reason, nameResult.reason === 'invalid-name' ? nameResult.value : undefined);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
entries.push({
|
|
904
|
+
project: serviceName, filePath, name: nameResult.name, kind: 'graphql_field',
|
|
905
|
+
signature: own.entry.signature, line: own.entry.line, endLine: own.entry.endLine, col: own.entry.col,
|
|
906
|
+
container: parent.name, isStatic: own.entry.isStatic, target: declaration,
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
extractMethodOperationRows(method, importMap, ownByDeclNode, filePath, serviceName, entries) {
|
|
910
|
+
const own = ownByDeclNode.get(method);
|
|
911
|
+
const declaration = this.declarationNameFor(method, own?.entry.name ?? AstParserService.memberName(method.getNameNode()) ?? '<anonymous>');
|
|
912
|
+
for (const dec of method.getDecorators()) {
|
|
913
|
+
const rec = this.recognizedDecorator(dec, importMap);
|
|
914
|
+
if (!rec)
|
|
915
|
+
continue;
|
|
916
|
+
if (rec.name === 'ResolveField') {
|
|
917
|
+
// Isolated per field: a throw resolving this field's parent (e.g. the
|
|
918
|
+
// injected test seam) degrades to "no row for that field" and never
|
|
919
|
+
// trips the file-level catch in `analyzeSourceFile`.
|
|
920
|
+
try {
|
|
921
|
+
this.extractResolveFieldRow(method, rec, own, declaration, importMap, filePath, serviceName, entries);
|
|
922
|
+
}
|
|
923
|
+
catch (err) {
|
|
924
|
+
this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'unresolvable-parent');
|
|
925
|
+
}
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
const kind = GRAPHQL_OPERATION_KIND[rec.name];
|
|
929
|
+
if (!kind)
|
|
930
|
+
continue; // recognised but not an operation decorator (wrong declaration kind) — ignored, no log
|
|
931
|
+
if (!own) {
|
|
932
|
+
this.logGraphqlSkip(filePath, declaration, rec.name, 'no-symbol-row');
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
const nameResult = this.deriveGraphqlName(rec.args, true, own.entry.name);
|
|
936
|
+
if (!nameResult.ok) {
|
|
937
|
+
this.logGraphqlSkip(filePath, declaration, rec.name, nameResult.reason, nameResult.reason === 'invalid-name' ? nameResult.value : undefined);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
entries.push({
|
|
941
|
+
project: serviceName, filePath, name: nameResult.name, kind,
|
|
942
|
+
signature: own.entry.signature, line: own.entry.line, endLine: own.entry.endLine, col: own.entry.col,
|
|
943
|
+
container: rec.name, isStatic: own.entry.isStatic, target: declaration,
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
/** GraphQL-named rows for every recognised `@nestjs/graphql` decorator in
|
|
948
|
+
* the file (`graphql-symbols`). `ownRows` is `symbols ∪ members` — the
|
|
949
|
+
* declarations' own `code_symbol` candidates, checked so a GraphQL row is
|
|
950
|
+
* never written without its decorated declaration's own row. */
|
|
951
|
+
extractGraphqlNames(sourceFile, filePath, serviceName, ownRows) {
|
|
952
|
+
const importMap = this.buildGraphqlImportMap(sourceFile);
|
|
953
|
+
if (importMap.size === 0)
|
|
954
|
+
return [];
|
|
955
|
+
const ownByDeclNode = new Map();
|
|
956
|
+
for (const c of ownRows)
|
|
957
|
+
ownByDeclNode.set(c.declNode, c);
|
|
958
|
+
const entries = [];
|
|
959
|
+
for (const cls of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.ClassDeclaration)) {
|
|
960
|
+
this.extractClassTypeRows(cls, importMap, ownByDeclNode, filePath, serviceName, entries);
|
|
961
|
+
}
|
|
962
|
+
for (const method of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.MethodDeclaration)) {
|
|
963
|
+
this.extractMethodOperationRows(method, importMap, ownByDeclNode, filePath, serviceName, entries);
|
|
964
|
+
}
|
|
965
|
+
return entries;
|
|
966
|
+
}
|
|
562
967
|
// ── private: shared container resolution ────────────────────────────────────
|
|
563
968
|
static isValidSegment(s) {
|
|
564
969
|
return VALID_SEGMENT.test(s);
|
|
@@ -6,7 +6,6 @@ import * as schema from '../db/schema';
|
|
|
6
6
|
type Schema = typeof schema;
|
|
7
7
|
export type DrizzleDb = NodePgDatabase<Schema>;
|
|
8
8
|
export type DrizzleTx = NodePgTransaction<Schema, ExtractTablesWithRelations<Schema>>;
|
|
9
|
-
export declare const CURRENT_SYMBOL_INDEX_VERSION = 2;
|
|
10
9
|
export declare class DatabaseService implements IDatabase {
|
|
11
10
|
private config;
|
|
12
11
|
private pool;
|
|
@@ -32,17 +31,6 @@ export declare class DatabaseService implements IDatabase {
|
|
|
32
31
|
deleteFiles(project: string, filePaths: string[]): Promise<void>;
|
|
33
32
|
listIndexedProjects(): Promise<string[]>;
|
|
34
33
|
deleteProject(name: string): Promise<void>;
|
|
35
|
-
listProjectsWithIndexRows(): Promise<string[]>;
|
|
36
|
-
/**
|
|
37
|
-
* Among `projectNames` (or every project with rows when null), those with at
|
|
38
|
-
* least one row in `code_chunk`/`code_symbol` and a `symbol_index_version`
|
|
39
|
-
* below `CURRENT_SYMBOL_INDEX_VERSION` (a missing row counts as version 1).
|
|
40
|
-
* A missing `symbol_index_version` table (un-migrated DB, `rag-mcp`/`rag-web`
|
|
41
|
-
* bootstrapped with `initDb`) counts as "no marker rows" — every project with
|
|
42
|
-
* rows is then outdated. Any other database error propagates.
|
|
43
|
-
*/
|
|
44
|
-
listOutdatedProjects(projectNames: string[] | null): Promise<string[]>;
|
|
45
|
-
upsertSymbolIndexVersion(project: string): Promise<void>;
|
|
46
34
|
/** Guard against SQL injection — the dimension is interpolated into DDL. */
|
|
47
35
|
private assertDim;
|
|
48
36
|
private assertMinPostgresVersion;
|
|
@@ -33,14 +33,13 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.DatabaseService =
|
|
36
|
+
exports.DatabaseService = void 0;
|
|
37
37
|
const node_postgres_1 = require("drizzle-orm/node-postgres");
|
|
38
38
|
const pg_1 = require("pg");
|
|
39
39
|
const drizzle_orm_1 = require("drizzle-orm");
|
|
40
40
|
const schema = __importStar(require("../db/schema"));
|
|
41
41
|
const db_1 = require("../db");
|
|
42
42
|
const MIN_PG_VERSION_NUM = 150000;
|
|
43
|
-
exports.CURRENT_SYMBOL_INDEX_VERSION = 2;
|
|
44
43
|
class DatabaseService {
|
|
45
44
|
config;
|
|
46
45
|
pool;
|
|
@@ -140,6 +139,7 @@ class DatabaseService {
|
|
|
140
139
|
await this.query(`CREATE INDEX IF NOT EXISTS idx_symbol_file ON code_symbol (project, "filePath")`);
|
|
141
140
|
await this.query(`ALTER TABLE code_symbol ADD COLUMN IF NOT EXISTS container TEXT`);
|
|
142
141
|
await this.query(`ALTER TABLE code_symbol ADD COLUMN IF NOT EXISTS "isStatic" BOOLEAN NOT NULL DEFAULT false`);
|
|
142
|
+
await this.query(`ALTER TABLE code_symbol ADD COLUMN IF NOT EXISTS "target" TEXT`);
|
|
143
143
|
// DDL is transactional in PostgreSQL — either both statements apply or
|
|
144
144
|
// neither does, so a failure here never leaves code_symbol without a
|
|
145
145
|
// unique index (the old idx_symbol_unique is dropped only once the new
|
|
@@ -149,12 +149,6 @@ class DatabaseService {
|
|
|
149
149
|
await tx.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_symbol_unique_v2 ' +
|
|
150
150
|
'ON code_symbol (project, "filePath", container, name, kind, line) NULLS NOT DISTINCT');
|
|
151
151
|
});
|
|
152
|
-
await this.query(`
|
|
153
|
-
CREATE TABLE IF NOT EXISTS symbol_index_version (
|
|
154
|
-
project TEXT PRIMARY KEY,
|
|
155
|
-
version INT NOT NULL
|
|
156
|
-
)
|
|
157
|
-
`);
|
|
158
152
|
// code_call table
|
|
159
153
|
await this.query(`
|
|
160
154
|
CREATE TABLE IF NOT EXISTS code_call (
|
|
@@ -268,50 +262,6 @@ class DatabaseService {
|
|
|
268
262
|
await tx.delete(db_1.codeDependency).where((0, drizzle_orm_1.eq)(db_1.codeDependency.project, name));
|
|
269
263
|
});
|
|
270
264
|
}
|
|
271
|
-
async listProjectsWithIndexRows() {
|
|
272
|
-
const rows = await this.query(`SELECT DISTINCT name FROM (
|
|
273
|
-
SELECT "serviceName" AS name FROM code_chunk
|
|
274
|
-
UNION
|
|
275
|
-
SELECT project AS name FROM code_symbol
|
|
276
|
-
) t`);
|
|
277
|
-
return rows.map(r => r.name);
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Among `projectNames` (or every project with rows when null), those with at
|
|
281
|
-
* least one row in `code_chunk`/`code_symbol` and a `symbol_index_version`
|
|
282
|
-
* below `CURRENT_SYMBOL_INDEX_VERSION` (a missing row counts as version 1).
|
|
283
|
-
* A missing `symbol_index_version` table (un-migrated DB, `rag-mcp`/`rag-web`
|
|
284
|
-
* bootstrapped with `initDb`) counts as "no marker rows" — every project with
|
|
285
|
-
* rows is then outdated. Any other database error propagates.
|
|
286
|
-
*/
|
|
287
|
-
async listOutdatedProjects(projectNames) {
|
|
288
|
-
const rowsUnion = `
|
|
289
|
-
SELECT "serviceName" AS name FROM code_chunk
|
|
290
|
-
UNION
|
|
291
|
-
SELECT project AS name FROM code_symbol
|
|
292
|
-
`;
|
|
293
|
-
const filterSql = projectNames ? 'AND t.name = ANY($1)' : '';
|
|
294
|
-
const params = projectNames ? [projectNames] : [];
|
|
295
|
-
try {
|
|
296
|
-
const rows = await this.query(`SELECT DISTINCT t.name FROM (${rowsUnion}) t
|
|
297
|
-
WHERE NOT EXISTS (
|
|
298
|
-
SELECT 1 FROM symbol_index_version v
|
|
299
|
-
WHERE v.project = t.name AND v.version >= ${exports.CURRENT_SYMBOL_INDEX_VERSION}
|
|
300
|
-
)
|
|
301
|
-
${filterSql}`, params);
|
|
302
|
-
return rows.map(r => r.name);
|
|
303
|
-
}
|
|
304
|
-
catch (err) {
|
|
305
|
-
if (!/relation "symbol_index_version" does not exist/i.test(err.message))
|
|
306
|
-
throw err;
|
|
307
|
-
const rows = await this.query(`SELECT DISTINCT t.name FROM (${rowsUnion}) t ${projectNames ? 'WHERE t.name = ANY($1)' : ''}`, params);
|
|
308
|
-
return rows.map(r => r.name);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
async upsertSymbolIndexVersion(project) {
|
|
312
|
-
await this.query(`INSERT INTO symbol_index_version (project, version) VALUES ($1, $2)
|
|
313
|
-
ON CONFLICT (project) DO UPDATE SET version = EXCLUDED.version`, [project, exports.CURRENT_SYMBOL_INDEX_VERSION]);
|
|
314
|
-
}
|
|
315
265
|
/** Guard against SQL injection — the dimension is interpolated into DDL. */
|
|
316
266
|
assertDim(dimensions) {
|
|
317
267
|
if (!Number.isInteger(dimensions) || dimensions <= 0 || dimensions > 2000) {
|