@blogic-cz/agent-tools 0.14.10 → 0.14.12
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/package.json
CHANGED
package/src/db-tool/schema.ts
CHANGED
|
@@ -5,18 +5,39 @@
|
|
|
5
5
|
* This is a known limitation — callers must validate the table name
|
|
6
6
|
* via `isValidTableName()` before calling `getColumns()`.
|
|
7
7
|
*/
|
|
8
|
+
export const SYSTEM_SCHEMAS_SQL = "'pg_catalog', 'information_schema'";
|
|
9
|
+
|
|
10
|
+
export function parseTableReference(tableName: string): { schemaName?: string; tableName: string } {
|
|
11
|
+
const [schemaName, name, ...extra] = tableName.split(".");
|
|
12
|
+
|
|
13
|
+
if (name && extra.length === 0) {
|
|
14
|
+
return { schemaName, tableName: name };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return { tableName };
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
export const SCHEMA_QUERIES = {
|
|
9
21
|
tables: `
|
|
10
|
-
SELECT
|
|
22
|
+
SELECT
|
|
23
|
+
schemaname as schema,
|
|
24
|
+
tablename as name,
|
|
25
|
+
schemaname || '.' || tablename as qualified_name
|
|
11
26
|
FROM pg_tables
|
|
12
|
-
WHERE schemaname
|
|
13
|
-
ORDER BY tablename
|
|
27
|
+
WHERE schemaname NOT IN (${SYSTEM_SCHEMAS_SQL})
|
|
28
|
+
ORDER BY schemaname, tablename
|
|
14
29
|
`,
|
|
15
30
|
columns: (tableName: string) => {
|
|
16
|
-
const
|
|
31
|
+
const tableReference = parseTableReference(tableName);
|
|
32
|
+
const escapedTableName = tableReference.tableName.replaceAll("'", "''");
|
|
33
|
+
const schemaFilter = tableReference.schemaName
|
|
34
|
+
? `AND c.table_schema = '${tableReference.schemaName.replaceAll("'", "''")}'`
|
|
35
|
+
: `AND c.table_schema NOT IN (${SYSTEM_SCHEMAS_SQL})`;
|
|
17
36
|
|
|
18
37
|
return `
|
|
19
38
|
SELECT
|
|
39
|
+
c.table_schema as schema,
|
|
40
|
+
c.table_name as table,
|
|
20
41
|
c.column_name as name,
|
|
21
42
|
c.data_type as type,
|
|
22
43
|
c.is_nullable = 'YES' as nullable,
|
|
@@ -32,14 +53,16 @@ export const SCHEMA_QUERIES = {
|
|
|
32
53
|
) as is_primary_key
|
|
33
54
|
FROM information_schema.columns c
|
|
34
55
|
WHERE c.table_name = '${escapedTableName}'
|
|
35
|
-
|
|
36
|
-
ORDER BY c.ordinal_position
|
|
56
|
+
${schemaFilter}
|
|
57
|
+
ORDER BY c.table_schema, c.ordinal_position
|
|
37
58
|
`;
|
|
38
59
|
},
|
|
39
60
|
relationships: `
|
|
40
61
|
SELECT
|
|
62
|
+
tc.table_schema as from_schema,
|
|
41
63
|
tc.table_name as from_table,
|
|
42
64
|
kcu.column_name as from_column,
|
|
65
|
+
ccu.table_schema as to_schema,
|
|
43
66
|
ccu.table_name as to_table,
|
|
44
67
|
ccu.column_name as to_column,
|
|
45
68
|
tc.constraint_name
|
|
@@ -49,10 +72,10 @@ export const SCHEMA_QUERIES = {
|
|
|
49
72
|
AND tc.table_schema = kcu.table_schema
|
|
50
73
|
JOIN information_schema.constraint_column_usage ccu
|
|
51
74
|
ON ccu.constraint_name = tc.constraint_name
|
|
52
|
-
AND ccu.
|
|
75
|
+
AND ccu.constraint_schema = tc.constraint_schema
|
|
53
76
|
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
54
|
-
AND tc.table_schema
|
|
55
|
-
ORDER BY tc.table_name, kcu.column_name
|
|
77
|
+
AND tc.table_schema NOT IN (${SYSTEM_SCHEMAS_SQL})
|
|
78
|
+
ORDER BY tc.table_schema, tc.table_name, kcu.column_name
|
|
56
79
|
`,
|
|
57
80
|
};
|
|
58
81
|
|
package/src/db-tool/security.ts
CHANGED
|
@@ -10,7 +10,7 @@ const MUTATION_PATTERNS = [
|
|
|
10
10
|
/^\s*CREATE\s+/i,
|
|
11
11
|
];
|
|
12
12
|
|
|
13
|
-
const TABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]
|
|
13
|
+
const TABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$/;
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Strip SQL comments (block and line) while preserving string literals.
|
package/src/db-tool/service.ts
CHANGED
|
@@ -15,7 +15,13 @@ import {
|
|
|
15
15
|
DbTunnelError,
|
|
16
16
|
type DbError,
|
|
17
17
|
} from "./errors";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
getColumns,
|
|
20
|
+
getRelationships,
|
|
21
|
+
getTableNames,
|
|
22
|
+
parseTableReference,
|
|
23
|
+
SYSTEM_SCHEMAS_SQL,
|
|
24
|
+
} from "./schema";
|
|
19
25
|
import { detectSchemaError, isValidTableName, isMutationQuery } from "./security";
|
|
20
26
|
import { transformQueryResult } from "./transformers";
|
|
21
27
|
|
|
@@ -324,7 +330,7 @@ export class DbService extends Context.Service<
|
|
|
324
330
|
) {
|
|
325
331
|
const command = buildPsqlCommand(
|
|
326
332
|
config,
|
|
327
|
-
|
|
333
|
+
`SELECT schemaname || '.' || tablename FROM pg_tables WHERE schemaname NOT IN (${SYSTEM_SCHEMAS_SQL}) ORDER BY schemaname, tablename;`,
|
|
328
334
|
password,
|
|
329
335
|
true,
|
|
330
336
|
);
|
|
@@ -356,11 +362,16 @@ export class DbService extends Context.Service<
|
|
|
356
362
|
return [] as string[];
|
|
357
363
|
}
|
|
358
364
|
|
|
359
|
-
const
|
|
365
|
+
const tableReference = parseTableReference(tableName);
|
|
366
|
+
const escapedSchemaName = tableReference.schemaName?.replaceAll("'", "''");
|
|
367
|
+
const escapedTableName = tableReference.tableName.replaceAll("'", "''");
|
|
368
|
+
const schemaFilter = escapedSchemaName
|
|
369
|
+
? `AND table_schema = '${escapedSchemaName}'`
|
|
370
|
+
: `AND table_schema NOT IN (${SYSTEM_SCHEMAS_SQL})`;
|
|
360
371
|
|
|
361
372
|
const command = buildPsqlCommand(
|
|
362
373
|
config,
|
|
363
|
-
`SELECT column_name FROM information_schema.columns WHERE table_name = '${escapedTableName}'
|
|
374
|
+
`SELECT column_name FROM information_schema.columns WHERE table_name = '${escapedTableName}' ${schemaFilter} ORDER BY table_schema, ordinal_position;`,
|
|
364
375
|
password,
|
|
365
376
|
true,
|
|
366
377
|
);
|
|
@@ -516,21 +527,25 @@ export class DbService extends Context.Service<
|
|
|
516
527
|
}
|
|
517
528
|
|
|
518
529
|
const tables = tablesResult.data as {
|
|
530
|
+
schema?: string;
|
|
519
531
|
name: string;
|
|
532
|
+
qualified_name?: string;
|
|
520
533
|
}[];
|
|
521
534
|
const fullSchema: Record<string, unknown>[] = [];
|
|
522
535
|
|
|
523
536
|
for (const table of tables) {
|
|
524
537
|
const columnsResult = yield* executeSelectQuery(
|
|
525
538
|
config,
|
|
526
|
-
getColumns(table.name),
|
|
539
|
+
getColumns(table.qualified_name ?? table.name),
|
|
527
540
|
password,
|
|
528
541
|
startTimeMs,
|
|
529
542
|
).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
530
543
|
|
|
531
544
|
if (columnsResult && columnsResult.success && columnsResult.data) {
|
|
532
545
|
fullSchema.push({
|
|
546
|
+
schema: table.schema,
|
|
533
547
|
table: table.name,
|
|
548
|
+
qualified_name: table.qualified_name ?? table.name,
|
|
534
549
|
columns: columnsResult.data,
|
|
535
550
|
});
|
|
536
551
|
}
|
|
@@ -668,7 +683,7 @@ export class DbService extends Context.Service<
|
|
|
668
683
|
return {
|
|
669
684
|
success: false,
|
|
670
685
|
error:
|
|
671
|
-
"Invalid table name. Use only letters, numbers, and
|
|
686
|
+
"Invalid table name. Use only letters, numbers, underscores, and an optional schema prefix, and start each identifier with a letter or underscore.",
|
|
672
687
|
executionTimeMs: Number(endTime) - Number(startTimeMs),
|
|
673
688
|
};
|
|
674
689
|
}
|
|
@@ -12,28 +12,25 @@ import {
|
|
|
12
12
|
resolveConfig,
|
|
13
13
|
} from "./shared";
|
|
14
14
|
|
|
15
|
-
type LogLine
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
readonly body?: string;
|
|
26
|
-
readonly severity?: string;
|
|
27
|
-
readonly attributes?: Record<string, unknown>;
|
|
28
|
-
};
|
|
15
|
+
import type { LogLine, StructuredLogLine } from "./types";
|
|
16
|
+
|
|
17
|
+
function isStringRecord(value: unknown): value is Record<string, string> {
|
|
18
|
+
return (
|
|
19
|
+
typeof value === "object" &&
|
|
20
|
+
value !== null &&
|
|
21
|
+
!Array.isArray(value) &&
|
|
22
|
+
Object.values(value).every((item) => typeof item === "string")
|
|
23
|
+
);
|
|
24
|
+
}
|
|
29
25
|
|
|
30
26
|
function parseLabel(value: string | Record<string, string>): Record<string, string> {
|
|
31
|
-
if (
|
|
27
|
+
if (isStringRecord(value)) {
|
|
32
28
|
return value;
|
|
33
29
|
}
|
|
34
30
|
|
|
35
31
|
try {
|
|
36
|
-
|
|
32
|
+
const parsed = JSON.parse(value) as unknown;
|
|
33
|
+
return isStringRecord(parsed) ? parsed : {};
|
|
37
34
|
} catch {
|
|
38
35
|
return {};
|
|
39
36
|
}
|
|
@@ -64,7 +61,7 @@ export function extractLogsFromDsQuery(response: {
|
|
|
64
61
|
results: {
|
|
65
62
|
A: {
|
|
66
63
|
frames?: Array<{
|
|
67
|
-
schema: { fields: Array<{ name: string; type?: string }> };
|
|
64
|
+
schema: { fields: Array<{ name: string; type?: string; labels?: Record<string, string> }> };
|
|
68
65
|
data: { values: unknown[][] };
|
|
69
66
|
}>;
|
|
70
67
|
};
|
|
@@ -81,15 +78,14 @@ export function extractLogsFromDsQuery(response: {
|
|
|
81
78
|
const lineIndex = fields.findIndex(
|
|
82
79
|
(field) => field.name === "body" || field.name === "Line" || field.name === "line",
|
|
83
80
|
);
|
|
84
|
-
const labelsIndex = fields.findIndex(
|
|
85
|
-
(field) => field.name === "labels" || field.name === "labelTypes",
|
|
86
|
-
);
|
|
81
|
+
const labelsIndex = fields.findIndex((field) => field.name === "labels");
|
|
87
82
|
|
|
88
83
|
const timestamps = (timeIndex >= 0 ? values[timeIndex] : []) as Array<string | number>;
|
|
89
84
|
const lines = (lineIndex >= 0 ? values[lineIndex] : []) as string[];
|
|
90
85
|
const labelValues = (labelsIndex >= 0 ? values[labelsIndex] : []) as Array<
|
|
91
86
|
string | Record<string, string>
|
|
92
87
|
>;
|
|
88
|
+
const streamLabels = lineIndex >= 0 ? fields[lineIndex]?.labels : undefined;
|
|
93
89
|
|
|
94
90
|
for (const [index, line] of lines.entries()) {
|
|
95
91
|
const structured = parseStructuredLogLine(line);
|
|
@@ -97,7 +93,7 @@ export function extractLogsFromDsQuery(response: {
|
|
|
97
93
|
timestamp: String(timestamps[index] ?? ""),
|
|
98
94
|
line,
|
|
99
95
|
...structured,
|
|
100
|
-
labels: labelValues[index] ? parseLabel(labelValues[index]) : {},
|
|
96
|
+
labels: labelValues[index] ? parseLabel(labelValues[index]) : (streamLabels ?? {}),
|
|
101
97
|
});
|
|
102
98
|
}
|
|
103
99
|
}
|
|
@@ -163,7 +159,7 @@ const queryCommand = Command.make(
|
|
|
163
159
|
|
|
164
160
|
yield* Console.log(formatOutput(result, format));
|
|
165
161
|
}).pipe(
|
|
166
|
-
Effect.
|
|
162
|
+
Effect.catchTag("ObservabilityToolError", (error) =>
|
|
167
163
|
Effect.gen(function* () {
|
|
168
164
|
const result = {
|
|
169
165
|
success: false,
|
|
@@ -8,6 +8,21 @@ export type ObservabilityEnvConfig = {
|
|
|
8
8
|
tempoUid: string;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
export type LogLine = {
|
|
12
|
+
readonly timestamp: string;
|
|
13
|
+
readonly line: string;
|
|
14
|
+
readonly body?: string;
|
|
15
|
+
readonly severity?: string;
|
|
16
|
+
readonly attributes?: Record<string, unknown>;
|
|
17
|
+
readonly labels: Record<string, string>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type StructuredLogLine = {
|
|
21
|
+
readonly body?: string;
|
|
22
|
+
readonly severity?: string;
|
|
23
|
+
readonly attributes?: Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
|
|
11
26
|
export type GrafanaDatasource = {
|
|
12
27
|
uid: string;
|
|
13
28
|
name: string;
|