fix: scope and batch sensitive suggestions
This commit is contained in:
@@ -14,7 +14,10 @@ import { MetadataGenerationModelUnavailableError } from "../catalog/metadata-gen
|
||||
import { ModelCompletionProviderError } from "../catalog/model-completer.js";
|
||||
import {
|
||||
SensitiveDataSuggester,
|
||||
SensitiveDataSuggestionDuplicateTargetIdsError,
|
||||
SensitiveDataSuggestionInvalidResponseError,
|
||||
SensitiveDataSuggestionNoEligibleColumnsError,
|
||||
SensitiveDataSuggestionPayloadTooLargeError,
|
||||
SensitiveDataSuggestionTargetNotFoundError,
|
||||
} from "../catalog/sensitive-data-suggester.js";
|
||||
import {
|
||||
@@ -28,8 +31,20 @@ import {
|
||||
|
||||
const idSchema = z.uuid();
|
||||
const modelIdSchema = z.string().regex(/^[a-z][a-z0-9._-]{0,63}$/);
|
||||
const suggestionSchema = z.object({ modelId: modelIdSchema }).strict();
|
||||
const selectedTargetIdsSchema = z.array(idSchema).min(1);
|
||||
const suggestionSchema = z.discriminatedUnion("scope", [
|
||||
z.object({ modelId: modelIdSchema, scope: z.literal("all") }).strict(),
|
||||
z.object({
|
||||
modelId: modelIdSchema,
|
||||
scope: z.literal("selected_tables"),
|
||||
targetIds: selectedTargetIdsSchema,
|
||||
}).strict(),
|
||||
z.object({
|
||||
modelId: modelIdSchema,
|
||||
scope: z.literal("selected_columns"),
|
||||
targetIds: selectedTargetIdsSchema,
|
||||
}).strict(),
|
||||
]);
|
||||
const startSchema = z.discriminatedUnion("scope", [
|
||||
z.object({
|
||||
modelId: modelIdSchema,
|
||||
@@ -123,19 +138,6 @@ function safeError(reply: FastifyReply, error: unknown) {
|
||||
message: "The selected metadata-generation model is unavailable.",
|
||||
});
|
||||
}
|
||||
if (error instanceof SensitiveDataSuggestionTargetNotFoundError) {
|
||||
return reply.code(404).send({
|
||||
code: "database_not_found",
|
||||
message: "Database configuration was not found.",
|
||||
});
|
||||
}
|
||||
if (error instanceof SensitiveDataSuggestionInvalidResponseError
|
||||
|| error instanceof ModelCompletionProviderError) {
|
||||
return reply.code(502).send({
|
||||
code: "sensitive_data_suggestion_failed",
|
||||
message: "Sensitive-data suggestions could not be prepared.",
|
||||
});
|
||||
}
|
||||
if (error instanceof DescriptionGenerationDuplicateTargetIdsError) {
|
||||
return reply.code(400).send({
|
||||
code: "description_generation_target_ids_duplicate",
|
||||
@@ -190,6 +192,74 @@ function safeError(reply: FastifyReply, error: unknown) {
|
||||
});
|
||||
}
|
||||
|
||||
function safeSuggestionError(reply: FastifyReply, error: unknown) {
|
||||
if (error instanceof CatalogUnavailableError) {
|
||||
return reply.code(503).send({
|
||||
code: "catalog_unavailable",
|
||||
message: "The database catalog is unavailable, so no sensitive-field suggestions were prepared.",
|
||||
});
|
||||
}
|
||||
if (error instanceof MetadataGenerationModelUnavailableError) {
|
||||
return reply.code(409).send({
|
||||
code: "metadata_generation_model_unavailable",
|
||||
message: "The selected metadata-generation model is unavailable.",
|
||||
});
|
||||
}
|
||||
if (error instanceof SensitiveDataSuggestionTargetNotFoundError) {
|
||||
const code = error.target === "database"
|
||||
? "database_not_found"
|
||||
: error.target === "table"
|
||||
? "catalog_table_not_found"
|
||||
: "catalog_column_not_found";
|
||||
const message = error.target === "database"
|
||||
? "The database configuration was not found."
|
||||
: error.target === "table"
|
||||
? "One or more selected Catalog Tables were not found in this database."
|
||||
: "One or more selected Catalog Columns were not found in this database.";
|
||||
return reply.code(404).send({ code, message });
|
||||
}
|
||||
if (error instanceof SensitiveDataSuggestionDuplicateTargetIdsError) {
|
||||
return reply.code(400).send({
|
||||
code: "sensitive_data_suggestion_target_ids_duplicate",
|
||||
message: "Each selected table or column must appear only once.",
|
||||
});
|
||||
}
|
||||
if (error instanceof SensitiveDataSuggestionNoEligibleColumnsError) {
|
||||
return reply.code(409).send({
|
||||
code: "sensitive_data_suggestion_no_columns",
|
||||
message: "The selected scope contains no Catalog Columns to classify.",
|
||||
});
|
||||
}
|
||||
if (error instanceof SensitiveDataSuggestionPayloadTooLargeError) {
|
||||
return reply.code(413).send({
|
||||
code: "sensitive_data_suggestion_payload_too_large",
|
||||
message: "The selected structural metadata cannot be divided into safe LLM requests.",
|
||||
});
|
||||
}
|
||||
if (error instanceof SensitiveDataSuggestionInvalidResponseError) {
|
||||
return reply.code(502).send({
|
||||
code: "sensitive_data_suggestion_invalid_response",
|
||||
message: "The LLM returned an incomplete or invalid classification. No suggestions were applied.",
|
||||
});
|
||||
}
|
||||
if (error instanceof ModelCompletionProviderError) {
|
||||
return reply.code(502).send({
|
||||
code: "sensitive_data_suggestion_provider_unavailable",
|
||||
message: "The selected LLM service could not complete the request. No suggestions were applied.",
|
||||
});
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
return reply.code(400).send({
|
||||
code: "sensitive_data_suggestion_request_invalid",
|
||||
message: "Choose a database, one or more tables, or one or more columns to classify.",
|
||||
});
|
||||
}
|
||||
return reply.code(500).send({
|
||||
code: "sensitive_data_suggestion_failed",
|
||||
message: "Sensitive-field suggestions failed before review. No changes were applied.",
|
||||
});
|
||||
}
|
||||
|
||||
export function catalogDescriptionGenerationRoutes(
|
||||
app: FastifyInstance,
|
||||
deps: {
|
||||
@@ -206,11 +276,13 @@ export function catalogDescriptionGenerationRoutes(
|
||||
const suggestions = await deps.sensitiveDataSuggester.suggest(
|
||||
databaseId,
|
||||
input.modelId,
|
||||
input.scope,
|
||||
"targetIds" in input ? input.targetIds : [],
|
||||
new AbortController().signal,
|
||||
);
|
||||
return { suggestions };
|
||||
} catch (error) {
|
||||
return safeError(reply, error);
|
||||
return safeSuggestionError(reply, error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -16,10 +16,12 @@ import {
|
||||
const idSchema = z.uuid();
|
||||
const metadataSchema = z.object({
|
||||
version: z.number().int().positive(),
|
||||
description: z.string().max(20_000).nullable(),
|
||||
generatedDescription: z.string().max(20_000).nullable(),
|
||||
description: z.string().max(20_000).nullable().optional(),
|
||||
generatedDescription: z.string().max(20_000).nullable().optional(),
|
||||
sensitive: z.boolean().optional(),
|
||||
}).strict();
|
||||
}).strict().refine((value) => (
|
||||
"description" in value || "generatedDescription" in value || "sensitive" in value
|
||||
));
|
||||
const createRunSchema = z.object({
|
||||
version: z.number().int().positive(),
|
||||
scope: z.enum(["tables", "columns", "relationships", "all"]),
|
||||
@@ -116,8 +118,10 @@ export function catalogSchemaRoutes(
|
||||
tableId,
|
||||
columnId,
|
||||
input.version,
|
||||
normalized(input.description),
|
||||
normalized(input.generatedDescription),
|
||||
"description" in input ? normalized(input.description ?? null) : current.description,
|
||||
"generatedDescription" in input
|
||||
? normalized(input.generatedDescription ?? null)
|
||||
: current.generatedDescription,
|
||||
input.sensitive,
|
||||
);
|
||||
if (!updated) return reply.code(409).send({ code: "column_stale", message: "Column metadata changed. Reload and try again." });
|
||||
|
||||
Reference in New Issue
Block a user