mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
feat(scan): add constraint discovery warning helper
This commit is contained in:
parent
2d9ba2de98
commit
483d0d6c15
3 changed files with 115 additions and 1 deletions
70
packages/cli/src/context/scan/constraint-discovery.test.ts
Normal file
70
packages/cli/src/context/scan/constraint-discovery.test.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { constraintDiscoveryWarning, tryConstraintQuery } from './constraint-discovery.js';
|
||||
|
||||
describe('tryConstraintQuery', () => {
|
||||
it('returns the query value when the query succeeds', async () => {
|
||||
await expect(
|
||||
tryConstraintQuery(
|
||||
{
|
||||
schema: 'public',
|
||||
kind: 'primary_key',
|
||||
isDeniedError: () => false,
|
||||
},
|
||||
async () => ['id'],
|
||||
),
|
||||
).resolves.toEqual({ ok: true, value: ['id'] });
|
||||
});
|
||||
|
||||
it('returns a recoverable warning when the classifier recognizes denial', async () => {
|
||||
const error = Object.assign(new Error('permission denied'), { code: '42501' });
|
||||
|
||||
await expect(
|
||||
tryConstraintQuery(
|
||||
{
|
||||
schema: 'analytics',
|
||||
kind: 'foreign_key',
|
||||
isDeniedError: (candidate) => candidate === error,
|
||||
},
|
||||
async () => {
|
||||
throw error;
|
||||
},
|
||||
),
|
||||
).resolves.toEqual({
|
||||
ok: false,
|
||||
warning: {
|
||||
code: 'constraint_discovery_unauthorized',
|
||||
message: 'Skipped foreign-key discovery in analytics (insufficient grants on system catalogs)',
|
||||
recoverable: true,
|
||||
metadata: { schema: 'analytics', kind: 'foreign_key' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('rethrows non-denial errors unchanged', async () => {
|
||||
const error = Object.assign(new Error('connection reset'), { code: 'ECONNRESET' });
|
||||
|
||||
await expect(
|
||||
tryConstraintQuery(
|
||||
{
|
||||
schema: 'public',
|
||||
kind: 'primary_key',
|
||||
isDeniedError: () => false,
|
||||
},
|
||||
async () => {
|
||||
throw error;
|
||||
},
|
||||
),
|
||||
).rejects.toBe(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('constraintDiscoveryWarning', () => {
|
||||
it('formats stable primary-key warning text and metadata', () => {
|
||||
expect(constraintDiscoveryWarning({ schema: 'public', kind: 'primary_key' })).toEqual({
|
||||
code: 'constraint_discovery_unauthorized',
|
||||
message: 'Skipped primary-key discovery in public (insufficient grants on system catalogs)',
|
||||
recoverable: true,
|
||||
metadata: { schema: 'public', kind: 'primary_key' },
|
||||
});
|
||||
});
|
||||
});
|
||||
42
packages/cli/src/context/scan/constraint-discovery.ts
Normal file
42
packages/cli/src/context/scan/constraint-discovery.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import type { KtxScanWarning } from './types.js';
|
||||
|
||||
export type ConstraintDiscoveryKind = 'primary_key' | 'foreign_key';
|
||||
|
||||
export interface ConstraintQueryContext {
|
||||
schema: string;
|
||||
kind: ConstraintDiscoveryKind;
|
||||
isDeniedError: (error: unknown) => boolean;
|
||||
}
|
||||
|
||||
export type ConstraintQueryOutcome<T> = { ok: true; value: T } | { ok: false; warning: KtxScanWarning };
|
||||
|
||||
export function constraintDiscoveryWarning(input: {
|
||||
schema: string;
|
||||
kind: ConstraintDiscoveryKind;
|
||||
}): KtxScanWarning {
|
||||
return {
|
||||
code: 'constraint_discovery_unauthorized',
|
||||
message:
|
||||
`Skipped ${input.kind === 'primary_key' ? 'primary-key' : 'foreign-key'} ` +
|
||||
`discovery in ${input.schema} (insufficient grants on system catalogs)`,
|
||||
recoverable: true,
|
||||
metadata: { schema: input.schema, kind: input.kind },
|
||||
};
|
||||
}
|
||||
|
||||
export async function tryConstraintQuery<T>(
|
||||
ctx: ConstraintQueryContext,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<ConstraintQueryOutcome<T>> {
|
||||
try {
|
||||
return { ok: true, value: await fn() };
|
||||
} catch (error) {
|
||||
if (!ctx.isDeniedError(error)) {
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
warning: constraintDiscoveryWarning({ schema: ctx.schema, kind: ctx.kind }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -91,6 +91,7 @@ export interface KtxSchemaSnapshot {
|
|||
scope: KtxSchemaScope;
|
||||
tables: KtxSchemaTable[];
|
||||
metadata: Record<string, unknown>;
|
||||
warnings?: KtxScanWarning[];
|
||||
}
|
||||
|
||||
interface KtxCredentialEnvReference {
|
||||
|
|
@ -365,7 +366,8 @@ type KtxScanWarningCode =
|
|||
| 'relationship_llm_proposal_failed'
|
||||
| 'credential_redacted'
|
||||
| 'enrichment_failed'
|
||||
| 'description_fallback_used';
|
||||
| 'description_fallback_used'
|
||||
| 'constraint_discovery_unauthorized';
|
||||
|
||||
export interface KtxScanWarning {
|
||||
code: KtxScanWarningCode;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue