infrawise 0.13.2 → 0.15.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 CHANGED
@@ -12,6 +12,8 @@ Infrawise gives AI coding assistants deterministic infrastructure awareness.
12
12
 
13
13
  It statically analyzes your codebase, cloud infrastructure, and database schemas, then exposes that context through MCP so tools like Claude Code can understand your actual tables, indexes, query patterns, and service relationships instead of guessing from source files alone.
14
14
 
15
+ ![infrawise start --claude, then Claude Code answers an SQS handler question with the exact event shape and queue risks pulled live from infrawise](https://raw.githubusercontent.com/Sidd27/infrawise/main/docs/demo/gifs/mcp.gif)
16
+
15
17
  ---
16
18
 
17
19
  ## Why this exists
@@ -72,7 +74,7 @@ That's it. Infrawise will:
72
74
  1. Probe your environment and generate `infrawise.yaml` (first time only — asks which AWS profile to use only if you have several)
73
75
  2. Scan your AWS services, databases, and codebase
74
76
  3. Write `.mcp.json` so your editor auto-connects on every future launch
75
- 4. Open Claude Code with all 16 MCP tools ready
77
+ 4. Open Claude Code with all 21 MCP tools ready
76
78
 
77
79
  **Every time after:**
78
80
 
@@ -115,7 +117,7 @@ Writes `.mcp.json` to your project root and opens Claude Code. Claude Code reads
115
117
  infrawise start --cursor
116
118
  ```
117
119
 
118
- Writes `.cursor/mcp.json` and opens Cursor. All 16 infrawise tools are available in Cursor's MCP panel.
120
+ Writes `.cursor/mcp.json` and opens Cursor. All 21 infrawise tools are available in Cursor's MCP panel.
119
121
 
120
122
  ### Any editor (no flag)
121
123
 
@@ -151,6 +153,7 @@ Add to your editor's MCP config:
151
153
  | ---------------------------- | ----------------------------------------------------------------------------------------------------------- |
152
154
  | `get_infra_overview` | Complete snapshot — services, counts, high-severity findings, analysis `freshness` (age + stale flag), `configured` flag |
153
155
  | `get_graph_summary` | Full infrastructure graph — all nodes, edges, and findings |
156
+ | `get_table_schema` | Column-level schema for named tables/collections — types, PKs, FKs, indexes, DynamoDB keys (no row data) |
154
157
  | `analyze_function` | Issues in a specific function — scans, missing indexes, N+1, trigger event shapes, missing IAM permissions |
155
158
  | `suggest_gsi` | Exact GSI config for a DynamoDB table + attribute |
156
159
  | `postgres_index_suggestions` | Exact `CREATE INDEX` SQL for your actual table |
@@ -165,6 +168,10 @@ Add to your editor's MCP config:
165
168
  | `get_eventbridge_details` | EventBridge rules — name, state, schedule/event pattern, target functions |
166
169
  | `get_s3_overview` | S3 buckets — versioning, encryption, public access, event notifications |
167
170
  | `get_log_errors` | CloudWatch error patterns and counts (no raw log messages) |
171
+ | `get_stack_outputs` | Stack outputs and cross-stack exports parsed from local IaC files (Terraform outputs, CFN/CDK Outputs) |
172
+ | `get_cognito_overview` | Cognito user pools — MFA config, app client auth flows, OAuth settings, token validity (secrets never included) |
173
+ | `get_stream_details` | Kinesis streams (shards, retention, capacity mode) and MSK clusters (state, Kafka version, brokers) |
174
+ | `get_cache_overview` | ElastiCache clusters — engine, encryption in transit/at rest, replication group, failover (data never read) |
168
175
 
169
176
  ---
170
177
 
@@ -301,6 +308,22 @@ s3:
301
308
  apiGateway:
302
309
  enabled: false
303
310
 
311
+ cognito:
312
+ enabled: false
313
+
314
+ kinesis:
315
+ enabled: false
316
+
317
+ msk:
318
+ enabled: false
319
+
320
+ elasticache:
321
+ enabled: false
322
+
323
+ runtimeSignals:
324
+ enabled: false # Lambda throttles/errors + queue age via CloudWatch metrics
325
+ windowHours: 24
326
+
304
327
  cloudwatchLogs:
305
328
  enabled: false
306
329
  logGroupPrefixes: []
@@ -374,7 +397,11 @@ Works from AWS APIs, database schema introspection, and IaC files — no depende
374
397
  | API Gateway | REST, HTTP, and WebSocket APIs — routes, methods, Lambda integrations |
375
398
  | RDS | Publicly accessible, no backups, unencrypted, no deletion protection, single-AZ |
376
399
  | CloudWatch Logs | Log groups with no retention policy |
377
- | Terraform / CloudFormation / CDK | IaC drift vs deployed state |
400
+ | Cognito | User pools and app client config auth flows, OAuth settings, token validity, client secret presence |
401
+ | Kinesis / MSK | Streams (shards, retention, capacity mode) and MSK clusters (state, Kafka version, brokers) |
402
+ | ElastiCache | Missing in-transit encryption, single-node clusters with no replication |
403
+ | Runtime signals (opt-in) | Lambda throttling/errors and stale queue messages from CloudWatch metrics |
404
+ | Terraform / CloudFormation / CDK | IaC drift vs deployed state; stack outputs and cross-stack exports |
378
405
 
379
406
  ### Code correlation analysis (TypeScript / JavaScript)
380
407
 
@@ -446,7 +473,7 @@ src/
446
473
  aws/ DynamoDB, S3, Lambda, SQS/SNS/SSM/Secrets/EventBridge/RDS/APIGateway, CloudWatch
447
474
  db/ PostgreSQL, MySQL, MongoDB
448
475
  iac/ Terraform, CDK, CloudFormation (local file parsing)
449
- analyzers/ 29 rule-based analyzers
476
+ analyzers/ 34 rule-based analyzers
450
477
  context/ Repository scanner (ts-morph AST)
451
478
  server/ Fastify MCP server (@modelcontextprotocol/sdk, Streamable HTTP)
452
479
  cli/ CLI commands (Commander.js)
@@ -473,6 +500,8 @@ Feature roadmap is tracked in the [GitHub Project](https://github.com/users/Sidd
473
500
 
474
501
  The `demo/localstack/` directory runs infrawise against real AWS APIs emulated locally via [LocalStack](https://localstack.cloud) — an open-source tool that spins up a full AWS environment in Docker so you can test AWS integrations at zero cost, with no real AWS account needed. See [`demo/localstack/README.md`](demo/localstack/README.md) for setup instructions.
475
502
 
503
+ ![infrawise analyze running against the LocalStack demo and reporting the high-severity findings](https://raw.githubusercontent.com/Sidd27/infrawise/main/docs/demo/gifs/analyze.gif)
504
+
476
505
  ---
477
506
 
478
507
  ## Contributing
@@ -2,3 +2,4 @@ export * from './services.js';
2
2
  export * from './dynamodb.js';
3
3
  export * from './logs.js';
4
4
  export * from './s3.js';
5
+ export * from './metrics.js';
@@ -0,0 +1,94 @@
1
+ import { CloudWatchClient, GetMetricDataCommand } from '@aws-sdk/client-cloudwatch';
2
+ import { clientConfig } from './services.js';
3
+ import { logger } from '../../core/index.js';
4
+ export async function extractRuntimeSignals(cfg, lambdaNames, queueNames, windowHours = 24) {
5
+ const lambdas = new Map();
6
+ const queues = new Map();
7
+ if (lambdaNames.length === 0 && queueNames.length === 0)
8
+ return { lambdas, queues };
9
+ const client = new CloudWatchClient(clientConfig(cfg));
10
+ const end = new Date();
11
+ const start = new Date(end.getTime() - windowHours * 3600 * 1000);
12
+ const period = windowHours * 3600;
13
+ // Metric names live in Dimensions; query Ids are positional because function/queue
14
+ // names may contain characters GetMetricData Ids do not allow.
15
+ const queries = [];
16
+ lambdaNames.forEach((name, i) => {
17
+ queries.push({
18
+ Id: `lt${i}`,
19
+ MetricStat: {
20
+ Metric: {
21
+ Namespace: 'AWS/Lambda',
22
+ MetricName: 'Throttles',
23
+ Dimensions: [{ Name: 'FunctionName', Value: name }],
24
+ },
25
+ Period: period,
26
+ Stat: 'Sum',
27
+ },
28
+ }, {
29
+ Id: `le${i}`,
30
+ MetricStat: {
31
+ Metric: {
32
+ Namespace: 'AWS/Lambda',
33
+ MetricName: 'Errors',
34
+ Dimensions: [{ Name: 'FunctionName', Value: name }],
35
+ },
36
+ Period: period,
37
+ Stat: 'Sum',
38
+ },
39
+ });
40
+ });
41
+ queueNames.forEach((name, i) => {
42
+ queries.push({
43
+ Id: `qa${i}`,
44
+ MetricStat: {
45
+ Metric: {
46
+ Namespace: 'AWS/SQS',
47
+ MetricName: 'ApproximateAgeOfOldestMessage',
48
+ Dimensions: [{ Name: 'QueueName', Value: name }],
49
+ },
50
+ Period: period,
51
+ Stat: 'Maximum',
52
+ },
53
+ });
54
+ });
55
+ try {
56
+ for (let i = 0; i < queries.length; i += 500) {
57
+ const chunk = queries.slice(i, i + 500);
58
+ let nextToken;
59
+ do {
60
+ const res = await client.send(new GetMetricDataCommand({
61
+ StartTime: start,
62
+ EndTime: end,
63
+ MetricDataQueries: chunk,
64
+ NextToken: nextToken,
65
+ }));
66
+ for (const r of res.MetricDataResults ?? []) {
67
+ const id = r.Id ?? '';
68
+ const value = r.Values?.[0] ?? 0;
69
+ const idx = parseInt(id.slice(2), 10);
70
+ if (Number.isNaN(idx))
71
+ continue;
72
+ if (id.startsWith('lt')) {
73
+ const e = lambdas.get(lambdaNames[idx]) ?? { throttles: 0, errors: 0 };
74
+ e.throttles = value;
75
+ lambdas.set(lambdaNames[idx], e);
76
+ }
77
+ else if (id.startsWith('le')) {
78
+ const e = lambdas.get(lambdaNames[idx]) ?? { throttles: 0, errors: 0 };
79
+ e.errors = value;
80
+ lambdas.set(lambdaNames[idx], e);
81
+ }
82
+ else if (id.startsWith('qa')) {
83
+ queues.set(queueNames[idx], { oldestMessageAgeSec: value });
84
+ }
85
+ }
86
+ nextToken = res.NextToken;
87
+ } while (nextToken);
88
+ }
89
+ }
90
+ catch (err) {
91
+ logger.warn(`Runtime signals fetch failed: ${err instanceof Error ? err.message : String(err)}`);
92
+ }
93
+ return { lambdas, queues };
94
+ }
@@ -7,10 +7,14 @@ import { SecretsManagerClient, ListSecretsCommand } from '@aws-sdk/client-secret
7
7
  import { LambdaClient, ListFunctionsCommand, ListEventSourceMappingsCommand, } from '@aws-sdk/client-lambda';
8
8
  import { EventBridgeClient, ListRulesCommand, ListTargetsByRuleCommand, } from '@aws-sdk/client-eventbridge';
9
9
  import { RDSClient, DescribeDBInstancesCommand } from '@aws-sdk/client-rds';
10
+ import { KinesisClient, ListStreamsCommand, DescribeStreamSummaryCommand, } from '@aws-sdk/client-kinesis';
11
+ import { KafkaClient, ListClustersV2Command } from '@aws-sdk/client-kafka';
12
+ import { ElastiCacheClient, DescribeCacheClustersCommand, DescribeReplicationGroupsCommand, } from '@aws-sdk/client-elasticache';
13
+ import { CognitoIdentityProviderClient, ListUserPoolsCommand, ListUserPoolClientsCommand, DescribeUserPoolClientCommand, DescribeUserPoolCommand, } from '@aws-sdk/client-cognito-identity-provider';
10
14
  import { IAMClient, ListAttachedRolePoliciesCommand, GetPolicyCommand, GetPolicyVersionCommand, ListRolePoliciesCommand, GetRolePolicyCommand, } from '@aws-sdk/client-iam';
11
15
  import { fromIni } from '@aws-sdk/credential-providers';
12
16
  import { logger } from '../../core/index.js';
13
- function clientConfig(cfg) {
17
+ export function clientConfig(cfg) {
14
18
  const region = cfg.region ?? 'us-east-1';
15
19
  const base = { region };
16
20
  if (cfg.profile)
@@ -420,6 +424,198 @@ export async function extractLambdaMetadata(cfg = {}, includeFunctions) {
420
424
  export async function validateLambdaAccess(cfg = {}) {
421
425
  await new LambdaClient(clientConfig(cfg)).send(new ListFunctionsCommand({ MaxItems: 1 }));
422
426
  }
427
+ // ─── Kinesis ─────────────────────────────────────────────────────────────────
428
+ export async function extractKinesisMetadata(cfg = {}) {
429
+ const client = new KinesisClient(clientConfig(cfg));
430
+ const streams = [];
431
+ try {
432
+ let nextToken;
433
+ const names = [];
434
+ do {
435
+ const res = await client.send(new ListStreamsCommand({ NextToken: nextToken }));
436
+ names.push(...(res.StreamNames ?? []));
437
+ nextToken = res.NextToken;
438
+ } while (nextToken);
439
+ for (const name of names) {
440
+ try {
441
+ const res = await client.send(new DescribeStreamSummaryCommand({ StreamName: name }));
442
+ const d = res.StreamDescriptionSummary;
443
+ if (!d)
444
+ continue;
445
+ streams.push({
446
+ name,
447
+ arn: d.StreamARN ?? '',
448
+ status: d.StreamStatus ?? 'UNKNOWN',
449
+ shardCount: d.OpenShardCount,
450
+ retentionHours: d.RetentionPeriodHours,
451
+ encrypted: d.EncryptionType === 'KMS',
452
+ mode: d.StreamModeDetails?.StreamMode ?? 'PROVISIONED',
453
+ });
454
+ }
455
+ catch (err) {
456
+ logger.warn(`Kinesis describe failed for ${name}: ${err instanceof Error ? err.message : String(err)}`);
457
+ }
458
+ }
459
+ }
460
+ catch (err) {
461
+ logger.warn(`Kinesis list failed: ${err instanceof Error ? err.message : String(err)}`);
462
+ }
463
+ return streams;
464
+ }
465
+ // ─── MSK ─────────────────────────────────────────────────────────────────────
466
+ export async function extractMSKMetadata(cfg = {}) {
467
+ const client = new KafkaClient(clientConfig(cfg));
468
+ const clusters = [];
469
+ try {
470
+ let nextToken;
471
+ do {
472
+ const res = await client.send(new ListClustersV2Command({ NextToken: nextToken }));
473
+ for (const c of res.ClusterInfoList ?? []) {
474
+ if (!c.ClusterName)
475
+ continue;
476
+ clusters.push({
477
+ name: c.ClusterName,
478
+ arn: c.ClusterArn ?? '',
479
+ state: c.State ?? 'UNKNOWN',
480
+ clusterType: c.ClusterType ?? 'PROVISIONED',
481
+ kafkaVersion: c.Provisioned?.CurrentBrokerSoftwareInfo?.KafkaVersion,
482
+ brokerNodes: c.Provisioned?.NumberOfBrokerNodes,
483
+ });
484
+ }
485
+ nextToken = res.NextToken;
486
+ } while (nextToken);
487
+ }
488
+ catch (err) {
489
+ logger.warn(`MSK list failed: ${err instanceof Error ? err.message : String(err)}`);
490
+ }
491
+ return clusters;
492
+ }
493
+ // ─── ElastiCache ─────────────────────────────────────────────────────────────
494
+ export async function extractElastiCacheMetadata(cfg = {}) {
495
+ const client = new ElastiCacheClient(clientConfig(cfg));
496
+ const clusters = [];
497
+ try {
498
+ const failoverByGroup = new Map();
499
+ try {
500
+ let marker;
501
+ do {
502
+ const res = await client.send(new DescribeReplicationGroupsCommand({ Marker: marker }));
503
+ for (const g of res.ReplicationGroups ?? []) {
504
+ if (g.ReplicationGroupId) {
505
+ failoverByGroup.set(g.ReplicationGroupId, g.AutomaticFailover ?? 'disabled');
506
+ }
507
+ }
508
+ marker = res.Marker;
509
+ } while (marker);
510
+ }
511
+ catch {
512
+ /* replication groups are optional context */
513
+ }
514
+ let marker;
515
+ do {
516
+ const res = await client.send(new DescribeCacheClustersCommand({ Marker: marker }));
517
+ for (const c of res.CacheClusters ?? []) {
518
+ if (!c.CacheClusterId)
519
+ continue;
520
+ clusters.push({
521
+ id: c.CacheClusterId,
522
+ engine: c.Engine ?? 'unknown',
523
+ engineVersion: c.EngineVersion ?? '',
524
+ nodeType: c.CacheNodeType ?? '',
525
+ numNodes: c.NumCacheNodes ?? 0,
526
+ transitEncryption: c.TransitEncryptionEnabled ?? false,
527
+ atRestEncryption: c.AtRestEncryptionEnabled ?? false,
528
+ replicationGroupId: c.ReplicationGroupId,
529
+ automaticFailover: c.ReplicationGroupId
530
+ ? failoverByGroup.get(c.ReplicationGroupId)
531
+ : undefined,
532
+ });
533
+ }
534
+ marker = res.Marker;
535
+ } while (marker);
536
+ }
537
+ catch (err) {
538
+ logger.warn(`ElastiCache list failed: ${err instanceof Error ? err.message : String(err)}`);
539
+ }
540
+ return clusters;
541
+ }
542
+ // ─── Cognito ─────────────────────────────────────────────────────────────────
543
+ export async function extractCognitoMetadata(cfg = {}) {
544
+ const client = new CognitoIdentityProviderClient(clientConfig(cfg));
545
+ const pools = [];
546
+ try {
547
+ let nextToken;
548
+ const poolRefs = [];
549
+ do {
550
+ const res = await client.send(new ListUserPoolsCommand({ MaxResults: 60, NextToken: nextToken }));
551
+ for (const p of res.UserPools ?? []) {
552
+ if (p.Id && p.Name)
553
+ poolRefs.push({ id: p.Id, name: p.Name });
554
+ }
555
+ nextToken = res.NextToken;
556
+ } while (nextToken);
557
+ for (const ref of poolRefs) {
558
+ try {
559
+ const poolRes = await client.send(new DescribeUserPoolCommand({ UserPoolId: ref.id }));
560
+ const clients = [];
561
+ let clientToken;
562
+ do {
563
+ const clientsRes = await client.send(new ListUserPoolClientsCommand({
564
+ UserPoolId: ref.id,
565
+ MaxResults: 60,
566
+ NextToken: clientToken,
567
+ }));
568
+ for (const c of clientsRes.UserPoolClients ?? []) {
569
+ if (!c.ClientId)
570
+ continue;
571
+ try {
572
+ const detail = await client.send(new DescribeUserPoolClientCommand({ UserPoolId: ref.id, ClientId: c.ClientId }));
573
+ const d = detail.UserPoolClient;
574
+ if (!d)
575
+ continue;
576
+ clients.push({
577
+ clientName: d.ClientName ?? c.ClientName ?? '',
578
+ clientId: c.ClientId,
579
+ authFlows: d.ExplicitAuthFlows ?? [],
580
+ oauthFlows: d.AllowedOAuthFlows ?? [],
581
+ oauthScopes: d.AllowedOAuthScopes ?? [],
582
+ callbackUrls: d.CallbackURLs ?? [],
583
+ generatesSecret: !!d.ClientSecret,
584
+ accessTokenValidity: d.AccessTokenValidity,
585
+ idTokenValidity: d.IdTokenValidity,
586
+ refreshTokenValidity: d.RefreshTokenValidity,
587
+ tokenValidityUnits: d.TokenValidityUnits
588
+ ? {
589
+ accessToken: d.TokenValidityUnits.AccessToken,
590
+ idToken: d.TokenValidityUnits.IdToken,
591
+ refreshToken: d.TokenValidityUnits.RefreshToken,
592
+ }
593
+ : undefined,
594
+ });
595
+ }
596
+ catch {
597
+ /* skip client on describe failure */
598
+ }
599
+ }
600
+ clientToken = clientsRes.NextToken;
601
+ } while (clientToken);
602
+ pools.push({
603
+ name: ref.name,
604
+ id: ref.id,
605
+ mfaConfiguration: poolRes.UserPool?.MfaConfiguration,
606
+ clients,
607
+ });
608
+ }
609
+ catch (err) {
610
+ logger.warn(`Cognito describe failed for ${ref.name}: ${err instanceof Error ? err.message : String(err)}`);
611
+ }
612
+ }
613
+ }
614
+ catch (err) {
615
+ logger.warn(`Cognito list failed: ${err instanceof Error ? err.message : String(err)}`);
616
+ }
617
+ return pools;
618
+ }
423
619
  // ─── RDS ─────────────────────────────────────────────────────────────────────
424
620
  export async function extractRDSMetadata(cfg = {}) {
425
621
  const client = new RDSClient(clientConfig(cfg));
@@ -28,10 +28,19 @@ export async function extractMySQLMetadata(connectionString) {
28
28
  logger.debug(`Found ${tableRows.length} MySQL table(s)`);
29
29
  // Get all columns
30
30
  const [columnRows] = await connection.execute(`
31
- SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME
31
+ SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE
32
32
  FROM information_schema.columns
33
33
  WHERE TABLE_SCHEMA NOT IN (${[...SYSTEM_SCHEMAS].map(() => '?').join(', ')})
34
34
  ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION
35
+ `, [...SYSTEM_SCHEMAS]);
36
+ // Get foreign keys
37
+ const [fkRows] = await connection.execute(`
38
+ SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME,
39
+ REFERENCED_TABLE_SCHEMA, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
40
+ FROM information_schema.key_column_usage
41
+ WHERE REFERENCED_TABLE_NAME IS NOT NULL
42
+ AND TABLE_SCHEMA NOT IN (${[...SYSTEM_SCHEMAS].map(() => '?').join(', ')})
43
+ ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION
35
44
  `, [...SYSTEM_SCHEMAS]);
36
45
  // Get all indexes
37
46
  const [indexRows] = await connection.execute(`
@@ -51,6 +60,7 @@ export async function extractMySQLMetadata(connectionString) {
51
60
  `, [...SYSTEM_SCHEMAS]);
52
61
  // Build lookup maps
53
62
  const columnMap = new Map();
63
+ const columnDetailMap = new Map();
54
64
  for (const row of columnRows) {
55
65
  const key = `${row['TABLE_SCHEMA']}.${row['TABLE_NAME']}`;
56
66
  let cols = columnMap.get(key);
@@ -59,6 +69,30 @@ export async function extractMySQLMetadata(connectionString) {
59
69
  columnMap.set(key, cols);
60
70
  }
61
71
  cols.push(row['COLUMN_NAME']);
72
+ let details = columnDetailMap.get(key);
73
+ if (!details) {
74
+ details = [];
75
+ columnDetailMap.set(key, details);
76
+ }
77
+ details.push({
78
+ name: row['COLUMN_NAME'],
79
+ dataType: row['DATA_TYPE'],
80
+ nullable: row['IS_NULLABLE'] === 'YES',
81
+ });
82
+ }
83
+ const fkMap = new Map();
84
+ for (const row of fkRows) {
85
+ const key = `${row['TABLE_SCHEMA']}.${row['TABLE_NAME']}`;
86
+ let fks = fkMap.get(key);
87
+ if (!fks) {
88
+ fks = [];
89
+ fkMap.set(key, fks);
90
+ }
91
+ fks.push({
92
+ column: row['COLUMN_NAME'],
93
+ referencesTable: `${row['REFERENCED_TABLE_SCHEMA']}.${row['REFERENCED_TABLE_NAME']}`,
94
+ referencesColumn: row['REFERENCED_COLUMN_NAME'],
95
+ });
62
96
  }
63
97
  const indexMap = new Map();
64
98
  for (const row of indexRows) {
@@ -96,6 +130,8 @@ export async function extractMySQLMetadata(connectionString) {
96
130
  indexes: indexMap.get(key) ?? [],
97
131
  primaryKeys: pkMap.get(key) ?? [],
98
132
  engine,
133
+ columnDetails: columnDetailMap.get(key) ?? [],
134
+ foreignKeys: fkMap.get(key) ?? [],
99
135
  });
100
136
  }
101
137
  return results;
@@ -22,7 +22,7 @@ export async function extractPostgresMetadata(connectionString) {
22
22
  logger.debug(`Found ${tablesResult.rows.length} PostgreSQL table(s)`);
23
23
  // Get all columns
24
24
  const columnsResult = await client.query(`
25
- SELECT table_schema, table_name, column_name
25
+ SELECT table_schema, table_name, column_name, data_type, is_nullable
26
26
  FROM information_schema.columns
27
27
  WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
28
28
  ORDER BY table_schema, table_name, ordinal_position
@@ -48,8 +48,29 @@ export async function extractPostgresMetadata(connectionString) {
48
48
  AND tc.table_schema NOT IN ('pg_catalog', 'information_schema')
49
49
  ORDER BY tc.table_schema, tc.table_name
50
50
  `);
51
- // Build maps for columns, indexes, primary keys
51
+ // Get foreign keys
52
+ const foreignKeysResult = await client.query(`
53
+ SELECT
54
+ tc.table_schema,
55
+ tc.table_name,
56
+ kcu.column_name,
57
+ ccu.table_schema AS ref_schema,
58
+ ccu.table_name AS ref_table,
59
+ ccu.column_name AS ref_column
60
+ FROM information_schema.table_constraints tc
61
+ JOIN information_schema.key_column_usage kcu
62
+ ON tc.constraint_name = kcu.constraint_name
63
+ AND tc.table_schema = kcu.table_schema
64
+ JOIN information_schema.constraint_column_usage ccu
65
+ ON tc.constraint_name = ccu.constraint_name
66
+ AND tc.table_schema = ccu.table_schema
67
+ WHERE tc.constraint_type = 'FOREIGN KEY'
68
+ AND tc.table_schema NOT IN ('pg_catalog', 'information_schema')
69
+ ORDER BY tc.table_schema, tc.table_name
70
+ `);
71
+ // Build maps for columns, indexes, primary keys, foreign keys
52
72
  const columnMap = new Map();
73
+ const columnDetailMap = new Map();
53
74
  for (const row of columnsResult.rows) {
54
75
  const key = `${row.table_schema}.${row.table_name}`;
55
76
  let cols = columnMap.get(key);
@@ -58,6 +79,30 @@ export async function extractPostgresMetadata(connectionString) {
58
79
  columnMap.set(key, cols);
59
80
  }
60
81
  cols.push(row.column_name);
82
+ let details = columnDetailMap.get(key);
83
+ if (!details) {
84
+ details = [];
85
+ columnDetailMap.set(key, details);
86
+ }
87
+ details.push({
88
+ name: row.column_name,
89
+ dataType: row.data_type,
90
+ nullable: row.is_nullable === 'YES',
91
+ });
92
+ }
93
+ const fkMap = new Map();
94
+ for (const row of foreignKeysResult.rows) {
95
+ const key = `${row.table_schema}.${row.table_name}`;
96
+ let fks = fkMap.get(key);
97
+ if (!fks) {
98
+ fks = [];
99
+ fkMap.set(key, fks);
100
+ }
101
+ fks.push({
102
+ column: row.column_name,
103
+ referencesTable: `${row.ref_schema}.${row.ref_table}`,
104
+ referencesColumn: row.ref_column,
105
+ });
61
106
  }
62
107
  const indexMap = new Map();
63
108
  for (const row of indexesResult.rows) {
@@ -89,6 +134,8 @@ export async function extractPostgresMetadata(connectionString) {
89
134
  columns: columnMap.get(key) ?? [],
90
135
  indexes: indexMap.get(key) ?? [],
91
136
  primaryKeys: pkMap.get(key) ?? [],
137
+ columnDetails: columnDetailMap.get(key) ?? [],
138
+ foreignKeys: fkMap.get(key) ?? [],
92
139
  });
93
140
  }
94
141
  return results;