@everystack/mcp 0.2.0
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/README.md +100 -0
- package/package.json +39 -0
- package/src/index.ts +58 -0
- package/src/prompts/add-feature.ts +163 -0
- package/src/prompts/debug.ts +136 -0
- package/src/prompts/deploy.ts +131 -0
- package/src/prompts/design-schema.ts +104 -0
- package/src/prompts/index.ts +16 -0
- package/src/prompts/new-app.ts +211 -0
- package/src/prompts/secure.ts +231 -0
- package/src/resources/adding-database.md +169 -0
- package/src/resources/admin.md +81 -0
- package/src/resources/auth.md +115 -0
- package/src/resources/aws-setup.md +173 -0
- package/src/resources/cli.md +108 -0
- package/src/resources/client-api.md +145 -0
- package/src/resources/core.md +196 -0
- package/src/resources/deployment.md +146 -0
- package/src/resources/events.md +87 -0
- package/src/resources/first-run.md +100 -0
- package/src/resources/getting-started.md +75 -0
- package/src/resources/handler-options.md +114 -0
- package/src/resources/images.md +73 -0
- package/src/resources/index.ts +224 -0
- package/src/resources/jobs.md +97 -0
- package/src/resources/logging.md +91 -0
- package/src/resources/plugins.md +68 -0
- package/src/resources/project-claude-md.md +127 -0
- package/src/resources/query-protocol.md +129 -0
- package/src/resources/schema-patterns.md +167 -0
- package/src/resources/security-device.md +99 -0
- package/src/resources/security.md +270 -0
- package/src/resources/ssr.md +82 -0
- package/src/resources/storage.md +63 -0
- package/src/resources/testing.md +118 -0
- package/src/tools/check-environment.ts +319 -0
- package/src/tools/index.ts +58 -0
- package/src/tools/project-status.ts +183 -0
- package/src/tools/project-validate.ts +369 -0
- package/src/tools/schema-analyze.ts +410 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
interface ColumnInfo {
|
|
5
|
+
name: string;
|
|
6
|
+
dbName: string;
|
|
7
|
+
type: string;
|
|
8
|
+
modifiers: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface TableInfo {
|
|
12
|
+
name: string;
|
|
13
|
+
exportName: string;
|
|
14
|
+
columns: ColumnInfo[];
|
|
15
|
+
file: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface RelationInfo {
|
|
19
|
+
name: string;
|
|
20
|
+
table: string;
|
|
21
|
+
type: 'one' | 'many';
|
|
22
|
+
fields: string[];
|
|
23
|
+
references: string[];
|
|
24
|
+
relationName?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface HandlerConfig {
|
|
28
|
+
exposedTables: string[];
|
|
29
|
+
hiddenColumns: Record<string, string[]>;
|
|
30
|
+
protectedFields: Record<string, string[]>;
|
|
31
|
+
rowOwnership: Record<string, { column: string; userField: string }>;
|
|
32
|
+
softDelete: { tables: string[] };
|
|
33
|
+
pgSettings: boolean;
|
|
34
|
+
relations: Record<string, Record<string, unknown>>;
|
|
35
|
+
publicRoutes: string[];
|
|
36
|
+
publicRpc: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface SchemaAnalysis {
|
|
40
|
+
tables: TableInfo[];
|
|
41
|
+
relations: RelationInfo[];
|
|
42
|
+
handlerConfig: Partial<HandlerConfig> | null;
|
|
43
|
+
issues: string[];
|
|
44
|
+
suggestions: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract a brace-balanced block starting from an opening brace.
|
|
49
|
+
*/
|
|
50
|
+
function extractBraceBlock(source: string, openIndex: number): string {
|
|
51
|
+
let depth = 0;
|
|
52
|
+
for (let i = openIndex; i < source.length; i++) {
|
|
53
|
+
if (source[i] === '{') depth++;
|
|
54
|
+
if (source[i] === '}') depth--;
|
|
55
|
+
if (depth === 0) return source.slice(openIndex + 1, i);
|
|
56
|
+
}
|
|
57
|
+
return source.slice(openIndex + 1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse pgTable declarations from a TypeScript source file.
|
|
62
|
+
* Uses regex + brace-depth extraction to handle nested objects (e.g., { withTimezone: true }).
|
|
63
|
+
*/
|
|
64
|
+
function parseTables(source: string, file: string): TableInfo[] {
|
|
65
|
+
const tables: TableInfo[] = [];
|
|
66
|
+
|
|
67
|
+
// Find each pgTable declaration
|
|
68
|
+
const headerRegex = /export\s+const\s+(\w+)\s*=\s*pgTable\(\s*['"]([^'"]+)['"]\s*,\s*\{/g;
|
|
69
|
+
let match: RegExpExecArray | null;
|
|
70
|
+
|
|
71
|
+
while ((match = headerRegex.exec(source)) !== null) {
|
|
72
|
+
const exportName = match[1];
|
|
73
|
+
const dbName = match[2];
|
|
74
|
+
// The opening brace position is at the end of the match minus 1
|
|
75
|
+
const openBrace = match.index + match[0].length - 1;
|
|
76
|
+
const columnsBlock = extractBraceBlock(source, openBrace);
|
|
77
|
+
|
|
78
|
+
const columns = parseColumns(columnsBlock);
|
|
79
|
+
tables.push({ name: dbName, exportName, columns, file });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return tables;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseColumns(block: string): ColumnInfo[] {
|
|
86
|
+
const columns: ColumnInfo[] = [];
|
|
87
|
+
// Match: columnName: type('db_name', ...optional args...)...chain...
|
|
88
|
+
// The type call may have extra args after the string (e.g., timestamp('x', { withTimezone: true }))
|
|
89
|
+
const colRegex = /(\w+)\s*:\s*(\w+)\(\s*['"]([^'"]+)['"][^)]*\)([\s\S]*?)(?=\n\s*\w+\s*:|$)/g;
|
|
90
|
+
let match: RegExpExecArray | null;
|
|
91
|
+
|
|
92
|
+
while ((match = colRegex.exec(block)) !== null) {
|
|
93
|
+
const name = match[1];
|
|
94
|
+
const type = match[2];
|
|
95
|
+
const dbName = match[3];
|
|
96
|
+
const rest = match[4];
|
|
97
|
+
|
|
98
|
+
const modifiers: string[] = [];
|
|
99
|
+
if (rest.includes('.primaryKey()')) modifiers.push('primaryKey');
|
|
100
|
+
if (rest.includes('.notNull()')) modifiers.push('notNull');
|
|
101
|
+
if (rest.includes('.unique()')) modifiers.push('unique');
|
|
102
|
+
if (rest.includes('.defaultNow()')) modifiers.push('defaultNow');
|
|
103
|
+
if (rest.includes('.defaultRandom()')) modifiers.push('defaultRandom');
|
|
104
|
+
if (rest.includes('.default(')) modifiers.push('default');
|
|
105
|
+
if (rest.includes('.references(')) modifiers.push('references');
|
|
106
|
+
|
|
107
|
+
columns.push({ name, dbName, type, modifiers });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return columns;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Parse relations() declarations from source.
|
|
115
|
+
*/
|
|
116
|
+
function parseRelations(source: string): RelationInfo[] {
|
|
117
|
+
const relations: RelationInfo[] = [];
|
|
118
|
+
|
|
119
|
+
// Match: export const nameRelations = relations(tableName, ({ one, many }) => ({
|
|
120
|
+
const relBlockRegex = /export\s+const\s+\w+\s*=\s*relations\(\s*(\w+)\s*,\s*\(\s*\{[^}]*\}\s*\)\s*=>\s*\(\{([\s\S]*?)\}\)\s*\)/g;
|
|
121
|
+
let blockMatch: RegExpExecArray | null;
|
|
122
|
+
|
|
123
|
+
while ((blockMatch = relBlockRegex.exec(source)) !== null) {
|
|
124
|
+
const table = blockMatch[1];
|
|
125
|
+
const body = blockMatch[2];
|
|
126
|
+
|
|
127
|
+
// Match individual relations: name: one(target, { fields: [...], references: [...] })
|
|
128
|
+
const relRegex = /(\w+)\s*:\s*(one|many)\(\s*(\w+)(?:\s*,\s*\{([^}]*)\})?\s*\)/g;
|
|
129
|
+
let relMatch: RegExpExecArray | null;
|
|
130
|
+
|
|
131
|
+
while ((relMatch = relRegex.exec(body)) !== null) {
|
|
132
|
+
const name = relMatch[1];
|
|
133
|
+
const type = relMatch[2] as 'one' | 'many';
|
|
134
|
+
const target = relMatch[3];
|
|
135
|
+
const opts = relMatch[4] || '';
|
|
136
|
+
|
|
137
|
+
const fields: string[] = [];
|
|
138
|
+
const references: string[] = [];
|
|
139
|
+
|
|
140
|
+
// Parse fields: [table.column]
|
|
141
|
+
const fieldsMatch = opts.match(/fields:\s*\[([^\]]+)\]/);
|
|
142
|
+
if (fieldsMatch) {
|
|
143
|
+
fields.push(...fieldsMatch[1].split(',').map((f) => f.trim().replace(/.*\./, '')));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const refsMatch = opts.match(/references:\s*\[([^\]]+)\]/);
|
|
147
|
+
if (refsMatch) {
|
|
148
|
+
references.push(...refsMatch[1].split(',').map((r) => r.trim().replace(/.*\./, '')));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const nameMatch = opts.match(/relationName:\s*['"]([^'"]+)['"]/);
|
|
152
|
+
|
|
153
|
+
relations.push({
|
|
154
|
+
name,
|
|
155
|
+
table: target,
|
|
156
|
+
type,
|
|
157
|
+
fields,
|
|
158
|
+
references,
|
|
159
|
+
...(nameMatch ? { relationName: nameMatch[1] } : {}),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return relations;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Parse handler configuration from the server handler/plugin file.
|
|
169
|
+
*/
|
|
170
|
+
function parseHandlerConfig(source: string): Partial<HandlerConfig> {
|
|
171
|
+
const config: Partial<HandlerConfig> = {};
|
|
172
|
+
|
|
173
|
+
// exposedTables
|
|
174
|
+
const exposedMatch = source.match(/exposedTables:\s*\[([\s\S]*?)\]/);
|
|
175
|
+
if (exposedMatch) {
|
|
176
|
+
config.exposedTables = exposedMatch[1]
|
|
177
|
+
.match(/['"]([^'"]+)['"]/g)
|
|
178
|
+
?.map((s) => s.replace(/['"]/g, '')) ?? [];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// hiddenColumns
|
|
182
|
+
const hiddenMatch = source.match(/hiddenColumns:\s*\{([\s\S]*?)\}/);
|
|
183
|
+
if (hiddenMatch) {
|
|
184
|
+
config.hiddenColumns = {};
|
|
185
|
+
const entries = hiddenMatch[1].matchAll(/(\w+):\s*\[([\s\S]*?)\]/g);
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
config.hiddenColumns[entry[1]] = entry[2]
|
|
188
|
+
.match(/['"]([^'"]+)['"]/g)
|
|
189
|
+
?.map((s) => s.replace(/['"]/g, '')) ?? [];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// protectedFields
|
|
194
|
+
const protectedMatch = source.match(/protectedFields:\s*\{([\s\S]*?)\}/);
|
|
195
|
+
if (protectedMatch) {
|
|
196
|
+
config.protectedFields = {};
|
|
197
|
+
const entries = protectedMatch[1].matchAll(/(\w+):\s*\[([\s\S]*?)\]/g);
|
|
198
|
+
for (const entry of entries) {
|
|
199
|
+
config.protectedFields[entry[1]] = entry[2]
|
|
200
|
+
.match(/['"]([^'"]+)['"]/g)
|
|
201
|
+
?.map((s) => s.replace(/['"]/g, '')) ?? [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// rowOwnership — scan for entries after the rowOwnership key
|
|
206
|
+
if (/rowOwnership\s*:/.test(source)) {
|
|
207
|
+
config.rowOwnership = {};
|
|
208
|
+
// Extract everything from rowOwnership: { to the matching closing }
|
|
209
|
+
const startIdx = source.indexOf('rowOwnership');
|
|
210
|
+
if (startIdx !== -1) {
|
|
211
|
+
// Find the opening brace
|
|
212
|
+
const braceStart = source.indexOf('{', startIdx);
|
|
213
|
+
if (braceStart !== -1) {
|
|
214
|
+
// Find the matching closing brace (handle one level of nesting)
|
|
215
|
+
let depth = 0;
|
|
216
|
+
let braceEnd = braceStart;
|
|
217
|
+
for (let i = braceStart; i < source.length; i++) {
|
|
218
|
+
if (source[i] === '{') depth++;
|
|
219
|
+
if (source[i] === '}') depth--;
|
|
220
|
+
if (depth === 0) { braceEnd = i; break; }
|
|
221
|
+
}
|
|
222
|
+
const block = source.slice(braceStart + 1, braceEnd);
|
|
223
|
+
const entryRegex = /(\w+):\s*\{\s*column:\s*['"]([^'"]+)['"]\s*,\s*userField:\s*['"]([^'"]+)['"]\s*\}/g;
|
|
224
|
+
const entries = block.matchAll(entryRegex);
|
|
225
|
+
for (const entry of entries) {
|
|
226
|
+
config.rowOwnership[entry[1]] = { column: entry[2], userField: entry[3] };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// softDelete
|
|
233
|
+
const softDeleteMatch = source.match(/softDelete:\s*\{[\s\S]*?tables:\s*\[([\s\S]*?)\]/);
|
|
234
|
+
if (softDeleteMatch) {
|
|
235
|
+
config.softDelete = {
|
|
236
|
+
tables: softDeleteMatch[1]
|
|
237
|
+
.match(/['"]([^'"]+)['"]/g)
|
|
238
|
+
?.map((s) => s.replace(/['"]/g, '')) ?? [],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// pgSettings
|
|
243
|
+
config.pgSettings = /pgSettings\s*:/.test(source);
|
|
244
|
+
|
|
245
|
+
// publicRoutes
|
|
246
|
+
const publicRoutesMatch = source.match(/publicRoutes:\s*\[([\s\S]*?)\]/);
|
|
247
|
+
if (publicRoutesMatch) {
|
|
248
|
+
config.publicRoutes = publicRoutesMatch[1]
|
|
249
|
+
.match(/['"]([^'"]+)['"]/g)
|
|
250
|
+
?.map((s) => s.replace(/['"]/g, '')) ?? [];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// publicRpc
|
|
254
|
+
const publicRpcMatch = source.match(/publicRpc:\s*\[([\s\S]*?)\]/);
|
|
255
|
+
if (publicRpcMatch) {
|
|
256
|
+
config.publicRpc = publicRpcMatch[1]
|
|
257
|
+
.match(/['"]([^'"]+)['"]/g)
|
|
258
|
+
?.map((s) => s.replace(/['"]/g, '')) ?? [];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return config;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function analyzeSchema(projectPath: string): SchemaAnalysis {
|
|
265
|
+
const issues: string[] = [];
|
|
266
|
+
const suggestions: string[] = [];
|
|
267
|
+
const allTables: TableInfo[] = [];
|
|
268
|
+
const allRelations: RelationInfo[] = [];
|
|
269
|
+
|
|
270
|
+
// Find schema files
|
|
271
|
+
const dbDir = join(projectPath, 'db');
|
|
272
|
+
const schemaFiles: string[] = [];
|
|
273
|
+
|
|
274
|
+
if (existsSync(dbDir)) {
|
|
275
|
+
try {
|
|
276
|
+
const files = readdirSync(dbDir);
|
|
277
|
+
for (const f of files) {
|
|
278
|
+
if (f.endsWith('.ts') && !f.endsWith('.test.ts') && !f.startsWith('seed')) {
|
|
279
|
+
schemaFiles.push(join(dbDir, f));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
// ignore read errors
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (schemaFiles.length === 0) {
|
|
288
|
+
return {
|
|
289
|
+
tables: [],
|
|
290
|
+
relations: [],
|
|
291
|
+
handlerConfig: null,
|
|
292
|
+
issues: ['No schema files found in db/ directory'],
|
|
293
|
+
suggestions: ['Create db/schema.ts with your Drizzle pgTable definitions'],
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Parse each schema file
|
|
298
|
+
for (const file of schemaFiles) {
|
|
299
|
+
try {
|
|
300
|
+
const source = readFileSync(file, 'utf-8');
|
|
301
|
+
const relativePath = file.replace(projectPath + '/', '');
|
|
302
|
+
allTables.push(...parseTables(source, relativePath));
|
|
303
|
+
allRelations.push(...parseRelations(source));
|
|
304
|
+
} catch {
|
|
305
|
+
issues.push(`Failed to read ${file}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Find and parse handler config
|
|
310
|
+
let handlerConfig: Partial<HandlerConfig> | null = null;
|
|
311
|
+
const handlerCandidates = [
|
|
312
|
+
'server/plugins/api.ts',
|
|
313
|
+
'server/api.ts',
|
|
314
|
+
'server/handler.ts',
|
|
315
|
+
'server/index.ts',
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
for (const candidate of handlerCandidates) {
|
|
319
|
+
const fullPath = join(projectPath, candidate);
|
|
320
|
+
if (existsSync(fullPath)) {
|
|
321
|
+
try {
|
|
322
|
+
const source = readFileSync(fullPath, 'utf-8');
|
|
323
|
+
if (source.includes('createHandler')) {
|
|
324
|
+
handlerConfig = parseHandlerConfig(source);
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
// continue to next candidate
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Cross-reference analysis
|
|
334
|
+
if (handlerConfig?.exposedTables) {
|
|
335
|
+
const tableNames = allTables.map((t) => t.name);
|
|
336
|
+
for (const exposed of handlerConfig.exposedTables) {
|
|
337
|
+
// Check both db name and export name
|
|
338
|
+
const found = allTables.some((t) => t.name === exposed || t.exportName === exposed);
|
|
339
|
+
if (!found) {
|
|
340
|
+
issues.push(`exposedTables includes '${exposed}' but no matching pgTable definition found in schema`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Tables defined but not exposed
|
|
345
|
+
for (const table of allTables) {
|
|
346
|
+
if (!handlerConfig.exposedTables.includes(table.name) &&
|
|
347
|
+
!handlerConfig.exposedTables.includes(table.exportName)) {
|
|
348
|
+
suggestions.push(`Table '${table.name}' is defined in schema but not in exposedTables — intentional?`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Check for soft delete columns
|
|
354
|
+
if (handlerConfig?.softDelete?.tables) {
|
|
355
|
+
for (const tableName of handlerConfig.softDelete.tables) {
|
|
356
|
+
const table = allTables.find((t) => t.name === tableName || t.exportName === tableName);
|
|
357
|
+
if (table) {
|
|
358
|
+
const hasDeletedAt = table.columns.some((c) => c.name === 'deletedAt' || c.dbName === 'deleted_at');
|
|
359
|
+
if (!hasDeletedAt) {
|
|
360
|
+
issues.push(`Table '${tableName}' is in softDelete.tables but has no deletedAt column`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Check for rowOwnership references
|
|
367
|
+
if (handlerConfig?.rowOwnership) {
|
|
368
|
+
for (const [tableName, ownership] of Object.entries(handlerConfig.rowOwnership)) {
|
|
369
|
+
const table = allTables.find((t) => t.name === tableName || t.exportName === tableName);
|
|
370
|
+
if (table) {
|
|
371
|
+
const hasCol = table.columns.some(
|
|
372
|
+
(c) => c.name === ownership.column || c.dbName === ownership.column,
|
|
373
|
+
);
|
|
374
|
+
if (!hasCol) {
|
|
375
|
+
issues.push(`rowOwnership for '${tableName}' references column '${ownership.column}' which doesn't exist`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// pgSettings check
|
|
382
|
+
if (!handlerConfig?.pgSettings && allTables.length > 0) {
|
|
383
|
+
suggestions.push('No pgSettings configured — RLS policies cannot read JWT claims without it');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Tables without primary keys
|
|
387
|
+
for (const table of allTables) {
|
|
388
|
+
const hasPk = table.columns.some((c) => c.modifiers.includes('primaryKey'));
|
|
389
|
+
if (!hasPk) {
|
|
390
|
+
// Could be a composite primary key (defined in table config, not column)
|
|
391
|
+
suggestions.push(`Table '${table.name}' — no single-column primaryKey() detected (may use composite key)`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Tables with timestamps
|
|
396
|
+
for (const table of allTables) {
|
|
397
|
+
const hasCreatedAt = table.columns.some((c) => c.dbName === 'created_at');
|
|
398
|
+
if (!hasCreatedAt) {
|
|
399
|
+
suggestions.push(`Table '${table.name}' has no created_at column — recommended for audit`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
tables: allTables,
|
|
405
|
+
relations: allRelations,
|
|
406
|
+
handlerConfig,
|
|
407
|
+
issues,
|
|
408
|
+
suggestions,
|
|
409
|
+
};
|
|
410
|
+
}
|