-
+
diff --git a/packages/schema-builder/src/core/context.tsx b/packages/schema-builder/src/core/context.tsx
index b7aeb7c..ee87035 100644
--- a/packages/schema-builder/src/core/context.tsx
+++ b/packages/schema-builder/src/core/context.tsx
@@ -52,17 +52,14 @@ export interface SchemaBuilderProviderProps extends SchemaBuilderHostOptions {
children: ReactNode;
}
-export function SchemaBuilderProvider({
- children,
- tabs = EMPTY_TABS,
- ...host
-}: SchemaBuilderProviderProps) {
+export function SchemaBuilderProvider(props: SchemaBuilderProviderProps) {
+ const { children, tabs = EMPTY_TABS } = props;
const [store] = useState(() =>
createSchemaBuilderStore(
- host.scope,
- host.preferences,
- host.activeTab,
- host.selectedTableId ?? null
+ props.scope,
+ props.preferences,
+ props.activeTab,
+ props.selectedTableId ?? null
)
);
@@ -72,22 +69,22 @@ export function SchemaBuilderProvider({
}, [tabs]);
useEffect(() => {
- store.getState().replaceScope(host.scope);
- }, [host.scope, store]);
+ store.getState().replaceScope(props.scope);
+ }, [props.scope, store]);
useEffect(() => {
- store.getState().replacePreferences(host.preferences);
- }, [host.preferences, store]);
+ store.getState().replacePreferences(props.preferences);
+ }, [props.preferences, store]);
useEffect(() => {
- store.getState().replaceActiveTab(host.activeTab);
- }, [host.activeTab, store]);
+ store.getState().replaceActiveTab(props.activeTab);
+ }, [props.activeTab, store]);
useEffect(() => {
- if (host.selectedTableId !== undefined) {
- store.getState().setSelectedTableId(host.selectedTableId);
+ if (props.selectedTableId !== undefined) {
+ store.getState().setSelectedTableId(props.selectedTableId);
}
- }, [host.scope, host.selectedTableId, store]);
+ }, [props.scope, props.selectedTableId, store]);
const setActiveTab = useCallback(
(tabId: string) => {
@@ -97,9 +94,9 @@ export function SchemaBuilderProvider({
}
void extension?.preload?.();
store.getState().replaceActiveTab(tabId);
- host.onActiveTabChange(tabId);
+ props.onActiveTabChange(tabId);
},
- [host.onActiveTabChange, store, validatedTabs]
+ [props.onActiveTabChange, store, validatedTabs]
);
const setPreferences = useCallback(
@@ -111,17 +108,17 @@ export function SchemaBuilderProvider({
const current = store.getState().preferences;
const next = typeof update === 'function' ? update(current) : update;
store.getState().replacePreferences(next);
- host.onPreferencesChange(next);
+ props.onPreferencesChange(next);
},
- [host.onPreferencesChange, store]
+ [props.onPreferencesChange, store]
);
const selectTable = useCallback(
(tableId: string | null, tableName?: string | null) => {
store.getState().setSelectedTableId(tableId);
- host.onSelectedTableChange?.({ tableId, tableName: tableName ?? null });
+ props.onSelectedTableChange?.({ tableId, tableName: tableName ?? null });
},
- [host.onSelectedTableChange, store]
+ [props.onSelectedTableChange, store]
);
const selectField = useCallback(
(fieldId: string | null) => store.getState().setSelectedFieldId(fieldId),
@@ -130,7 +127,17 @@ export function SchemaBuilderProvider({
const value = useMemo
(
() => ({
- ...host,
+ adapter: props.adapter,
+ scope: props.scope,
+ colorMode: props.colorMode,
+ preferences: props.preferences,
+ onPreferencesChange: props.onPreferencesChange,
+ activeTab: props.activeTab,
+ onActiveTabChange: props.onActiveTabChange,
+ selectedTableId: props.selectedTableId,
+ onSelectedTableChange: props.onSelectedTableChange,
+ onNavigate: props.onNavigate,
+ onInvalidate: props.onInvalidate,
tabs: validatedTabs,
store,
setActiveTab,
@@ -138,7 +145,25 @@ export function SchemaBuilderProvider({
selectTable,
selectField
}),
- [host, selectField, selectTable, setActiveTab, setPreferences, store, validatedTabs]
+ [
+ props.activeTab,
+ props.adapter,
+ props.colorMode,
+ props.onActiveTabChange,
+ props.onInvalidate,
+ props.onNavigate,
+ props.onPreferencesChange,
+ props.onSelectedTableChange,
+ props.preferences,
+ props.scope,
+ props.selectedTableId,
+ selectField,
+ selectTable,
+ setActiveTab,
+ setPreferences,
+ store,
+ validatedTabs
+ ]
);
return {children} ;
diff --git a/packages/schema-builder/src/index.ts b/packages/schema-builder/src/index.ts
index f894754..dc7b7e0 100644
--- a/packages/schema-builder/src/index.ts
+++ b/packages/schema-builder/src/index.ts
@@ -51,3 +51,4 @@ export type {
SchemaBuilderTablesCapabilities,
SchemaBuilderVariables
} from './types';
+export type { SchemaBuilderDataState } from './schema/schema-builder-core/lib/gql/hooks/schema-builder';
diff --git a/packages/schema-builder/src/schema/schema-builder-core/components/schemas/schema-state-display.tsx b/packages/schema-builder/src/schema/schema-builder-core/components/schemas/schema-state-display.tsx
index 2ac49bf..4095aaa 100644
--- a/packages/schema-builder/src/schema/schema-builder-core/components/schemas/schema-state-display.tsx
+++ b/packages/schema-builder/src/schema/schema-builder-core/components/schemas/schema-state-display.tsx
@@ -4,7 +4,6 @@ import { memo } from 'react';
import { RiDatabase2Line } from '@remixicon/react';
import { cn } from '@/lib/utils';
-import type { DataError } from '../../lib/data';
import { type BaseStateConfig, ErrorBanner, InfoBanner } from '../shared/base-state-display';
import {
SchemaBuilderSkeleton,
@@ -23,7 +22,7 @@ export interface SchemaStateConfig {
message?: string;
onRetry?: () => void;
/** Original error object for auth error detection */
- error?: Error | DataError | null;
+ error?: Error | null;
}
// ============================================================================
diff --git a/packages/schema-builder/src/schema/schema-builder-core/components/shared/base-state-display.tsx b/packages/schema-builder/src/schema/schema-builder-core/components/shared/base-state-display.tsx
index a9f53a8..e4e1978 100644
--- a/packages/schema-builder/src/schema/schema-builder-core/components/shared/base-state-display.tsx
+++ b/packages/schema-builder/src/schema/schema-builder-core/components/shared/base-state-display.tsx
@@ -5,8 +5,7 @@ import { RiAlertLine, RiDatabase2Line, RiRefreshLine, RiShieldLine } from '@remi
import { cn } from '@/lib/utils';
import { AuthErrorBanner, isAuthError } from '../../lib/gql/auth-error-handler';
-import type { DataError } from '../../lib/data';
-import { Button } from '@constructive-io/ui/button';
+import { Button, buttonVariants } from '@constructive-io/ui/button';
import { useSchemaBuilderRuntime } from '@/blocks/schema/schema-builder-core/context/block-config';
// ============================================================================
@@ -35,7 +34,7 @@ export interface BaseStateConfig {
message?: string;
onRetry?: () => void;
/** Original error object for auth error detection */
- error?: Error | DataError | null;
+ error?: Error | null;
}
// ============================================================================
@@ -245,9 +244,12 @@ export function NoDatabaseBanner({
{linkText}
) : (
-
- {linkText}
-
+
+ {linkText}
+
);
return (
diff --git a/packages/schema-builder/src/schema/schema-builder-core/lib/data/error-handler.ts b/packages/schema-builder/src/schema/schema-builder-core/lib/data/error-handler.ts
deleted file mode 100644
index b3a60ab..0000000
--- a/packages/schema-builder/src/schema/schema-builder-core/lib/data/error-handler.ts
+++ /dev/null
@@ -1,649 +0,0 @@
-// Vendored from @constructive-io/data — trimmed to the surface the schema-builder blocks use. Do not edit to track upstream.
-
-/**
- * Centralized error handling for the data layer
- * Provides consistent error types, messages, and handling patterns
- */
-
-// ============================================================================
-// Error Types (const map instead of enum per codebase conventions)
-// ============================================================================
-
-export const DataErrorType = {
- // Network/Connection errors
- NETWORK_ERROR: 'NETWORK_ERROR',
- TIMEOUT_ERROR: 'TIMEOUT_ERROR',
-
- // Validation errors
- VALIDATION_FAILED: 'VALIDATION_FAILED',
- REQUIRED_FIELD_MISSING: 'REQUIRED_FIELD_MISSING',
- INVALID_MUTATION_DATA: 'INVALID_MUTATION_DATA',
-
- // Query errors
- QUERY_GENERATION_FAILED: 'QUERY_GENERATION_FAILED',
- QUERY_EXECUTION_FAILED: 'QUERY_EXECUTION_FAILED',
-
- // Permission errors
- UNAUTHORIZED: 'UNAUTHORIZED',
- FORBIDDEN: 'FORBIDDEN',
-
- // Schema errors
- TABLE_NOT_FOUND: 'TABLE_NOT_FOUND',
-
- // Request errors
- BAD_REQUEST: 'BAD_REQUEST',
- NOT_FOUND: 'NOT_FOUND',
-
- // GraphQL-specific errors
- GRAPHQL_ERROR: 'GRAPHQL_ERROR',
-
- // PostgreSQL constraint errors (surfaced via PostGraphile)
- UNIQUE_VIOLATION: 'UNIQUE_VIOLATION',
- FOREIGN_KEY_VIOLATION: 'FOREIGN_KEY_VIOLATION',
- NOT_NULL_VIOLATION: 'NOT_NULL_VIOLATION',
- CHECK_VIOLATION: 'CHECK_VIOLATION',
- EXCLUSION_VIOLATION: 'EXCLUSION_VIOLATION',
-
- // Generic errors
- UNKNOWN_ERROR: 'UNKNOWN_ERROR',
-} as const;
-
-export type DataErrorType = (typeof DataErrorType)[keyof typeof DataErrorType];
-
-// ============================================================================
-// DataError Class
-// ============================================================================
-
-export interface DataErrorOptions {
- /** Optional table name for data-layer operations */
- tableName?: string;
- /** Optional field name for data-layer operations */
- fieldName?: string;
- /** Optional constraint name for PostgreSQL constraint violations */
- constraint?: string;
- originalError?: Error;
- code?: string;
- context?: Record;
-}
-
-type CaptureStackTraceFn = (targetObject: object, constructorOpt?: Function) => void;
-
-const errorWithCaptureStackTrace = Error as ErrorConstructor & {
- captureStackTrace?: CaptureStackTraceFn;
-};
-
-/**
- * Standard data layer error class
- */
-export class DataError extends Error {
- public readonly type: DataErrorType;
- public readonly code?: string;
- public readonly originalError?: Error;
- public readonly context?: Record;
- public readonly tableName?: string;
- public readonly fieldName?: string;
- public readonly constraint?: string;
-
- constructor(type: DataErrorType, message: string, options: DataErrorOptions = {}) {
- super(message);
- this.name = 'DataError';
- this.type = type;
- this.code = options.code;
- this.originalError = options.originalError;
- this.context = options.context;
- this.tableName = options.tableName;
- this.fieldName = options.fieldName;
- this.constraint = options.constraint;
-
- if (errorWithCaptureStackTrace.captureStackTrace) {
- errorWithCaptureStackTrace.captureStackTrace(this, DataError);
- }
- }
-
- /**
- * Get a user-friendly error message.
- * Checks constraint-specific messages first, then falls back to generic messages.
- */
- getUserMessage(): string {
- // Check constraint-specific message first (for constraint violations)
- if (this.constraint) {
- const constraintMsg = getConstraintMessage(this.constraint);
- if (constraintMsg) return constraintMsg;
- }
-
- // Fall back to generic messages by error type
- switch (this.type) {
- case DataErrorType.NETWORK_ERROR:
- return 'Network error. Please check your connection and try again.';
- case DataErrorType.TIMEOUT_ERROR:
- return 'Request timed out. Please try again.';
- case DataErrorType.UNAUTHORIZED:
- return 'You are not authorized. Please log in and try again.';
- case DataErrorType.FORBIDDEN:
- return 'You do not have permission to access this resource.';
- case DataErrorType.VALIDATION_FAILED:
- return 'Validation failed. Please check your input and try again.';
- case DataErrorType.REQUIRED_FIELD_MISSING:
- return this.fieldName
- ? `The field "${this.fieldName}" is required.`
- : 'A required field is missing. Please check your input.';
- case DataErrorType.INVALID_MUTATION_DATA:
- return 'Invalid input. Please check your data and try again.';
- case DataErrorType.QUERY_GENERATION_FAILED:
- return 'Query validation failed. Please check your request.';
- case DataErrorType.QUERY_EXECUTION_FAILED:
- return 'Query execution failed. Please try again.';
- case DataErrorType.TABLE_NOT_FOUND:
- return 'The requested table was not found.';
- case DataErrorType.BAD_REQUEST:
- return this.message || 'Invalid request.';
- case DataErrorType.NOT_FOUND:
- return 'The requested resource was not found.';
- case DataErrorType.UNIQUE_VIOLATION:
- return this.fieldName
- ? `A record with this ${this.fieldName} already exists.`
- : 'A record with this value already exists.';
- case DataErrorType.FOREIGN_KEY_VIOLATION:
- return 'This record cannot be saved because it references a record that does not exist.';
- case DataErrorType.NOT_NULL_VIOLATION:
- return this.fieldName
- ? `The field "${this.fieldName}" cannot be empty.`
- : 'A required field cannot be empty.';
- case DataErrorType.CHECK_VIOLATION:
- return this.fieldName
- ? `The value for "${this.fieldName}" is not valid.`
- : 'The value does not meet the required constraints.';
- case DataErrorType.EXCLUSION_VIOLATION:
- return 'This record conflicts with an existing record.';
- default:
- return this.message || 'An unexpected error occurred.';
- }
- }
-
- /**
- * Check if this error is retryable
- */
- isRetryable(): boolean {
- return this.type === DataErrorType.NETWORK_ERROR || this.type === DataErrorType.TIMEOUT_ERROR;
- }
-
- /**
- * Get error details for logging
- */
- getLogDetails(): Record {
- return {
- type: this.type,
- message: this.message,
- code: this.code,
- tableName: this.tableName,
- fieldName: this.fieldName,
- context: this.context,
- stack: this.stack,
- originalError: this.originalError
- ? {
- name: this.originalError.name,
- message: this.originalError.message,
- }
- : undefined,
- };
- }
-}
-
-// ============================================================================
-// GraphQL Types
-// ============================================================================
-
-export interface GraphQLErrorLocation {
- line: number;
- column: number;
-}
-
-export type GraphQLErrorPath = Array;
-
-export interface GraphQLError {
- message: string;
- extensions?: { code?: string } & Record;
- locations?: GraphQLErrorLocation[];
- path?: GraphQLErrorPath;
-}
-
-// ============================================================================
-// Error Factory
-// ============================================================================
-
-export const createDataError = {
- networkError: (originalError?: Error, tableName?: string) =>
- new DataError(DataErrorType.NETWORK_ERROR, 'Network error occurred', { originalError, tableName }),
-
- timeoutError: (originalError?: Error, tableName?: string) =>
- new DataError(DataErrorType.TIMEOUT_ERROR, 'Request timed out', { originalError, tableName }),
-
- validationFailed: (tableName: string | undefined, validationErrors: string[]) =>
- new DataError(
- DataErrorType.VALIDATION_FAILED,
- `Validation failed: ${validationErrors.join(', ')}`,
- { tableName, context: { validationErrors } },
- ),
-
- requiredFieldMissing: (fieldName: string, tableName?: string) =>
- new DataError(DataErrorType.REQUIRED_FIELD_MISSING, `Required field ${fieldName} is missing`, {
- tableName,
- fieldName,
- }),
-
- unauthorized: (message = 'Authentication required', tableName?: string) =>
- new DataError(DataErrorType.UNAUTHORIZED, message, { tableName }),
-
- forbidden: (tableName?: string) =>
- new DataError(DataErrorType.FORBIDDEN, 'Access forbidden', { tableName }),
-
- queryGenerationFailed: (message: string, tableName?: string, code?: string) =>
- new DataError(DataErrorType.QUERY_GENERATION_FAILED, message, { tableName, code }),
-
- queryExecutionFailed: (message: string, tableName?: string, code?: string) =>
- new DataError(DataErrorType.QUERY_EXECUTION_FAILED, message, { tableName, code }),
-
- tableNotFound: (message = 'Table not found', tableName?: string, code?: string) =>
- new DataError(DataErrorType.TABLE_NOT_FOUND, message, { tableName, code }),
-
- invalidMutationData: (message: string, tableName?: string, code?: string, context?: Record) =>
- new DataError(DataErrorType.INVALID_MUTATION_DATA, message, { tableName, code, context }),
-
- // PostgreSQL constraint violations
- uniqueViolation: (message: string, tableName?: string, fieldName?: string, constraint?: string) =>
- new DataError(DataErrorType.UNIQUE_VIOLATION, message, { tableName, fieldName, constraint, code: '23505' }),
-
- foreignKeyViolation: (message: string, tableName?: string, fieldName?: string, constraint?: string) =>
- new DataError(DataErrorType.FOREIGN_KEY_VIOLATION, message, { tableName, fieldName, constraint, code: '23503' }),
-
- notNullViolation: (message: string, tableName?: string, fieldName?: string, constraint?: string) =>
- new DataError(DataErrorType.NOT_NULL_VIOLATION, message, { tableName, fieldName, constraint, code: '23502' }),
-
- checkViolation: (message: string, tableName?: string, fieldName?: string, constraint?: string) =>
- new DataError(DataErrorType.CHECK_VIOLATION, message, { tableName, fieldName, constraint, code: '23514' }),
-
- exclusionViolation: (message: string, tableName?: string, constraint?: string) =>
- new DataError(DataErrorType.EXCLUSION_VIOLATION, message, { tableName, constraint, code: '23P01' }),
-
- unknown: (originalError: Error, tableName?: string) =>
- new DataError(DataErrorType.UNKNOWN_ERROR, originalError.message, { originalError, tableName }),
-};
-
-// ============================================================================
-// Error Parsing
-// ============================================================================
-
-/**
- * PostgreSQL SQLSTATE error codes
- * https://www.postgresql.org/docs/current/errcodes-appendix.html
- */
-export const PG_ERROR_CODES = {
- // Class 23 - Integrity Constraint Violation
- UNIQUE_VIOLATION: '23505',
- FOREIGN_KEY_VIOLATION: '23503',
- NOT_NULL_VIOLATION: '23502',
- CHECK_VIOLATION: '23514',
- EXCLUSION_VIOLATION: '23P01',
-
- // Class 22 - Data Exception
- NUMERIC_VALUE_OUT_OF_RANGE: '22003',
- STRING_DATA_RIGHT_TRUNCATION: '22001',
- INVALID_TEXT_REPRESENTATION: '22P02',
- DATETIME_FIELD_OVERFLOW: '22008',
-
- // Class 42 - Syntax Error or Access Rule Violation
- UNDEFINED_TABLE: '42P01',
- UNDEFINED_COLUMN: '42703',
- INSUFFICIENT_PRIVILEGE: '42501',
-
- // Class 53 - Insufficient Resources
- DISK_FULL: '53100',
- OUT_OF_MEMORY: '53200',
- TOO_MANY_CONNECTIONS: '53300',
-} as const;
-
-export function parseGraphQLErrorCode(code: string | undefined): DataErrorType {
- if (!code) return DataErrorType.UNKNOWN_ERROR;
- const normalized = code.toUpperCase();
-
- // GraphQL standard error codes
- if (normalized === 'UNAUTHENTICATED') return DataErrorType.UNAUTHORIZED;
- if (normalized === 'BAD_TOKEN_DEFINITION') return DataErrorType.UNAUTHORIZED;
- if (normalized === 'FORBIDDEN') return DataErrorType.FORBIDDEN;
- if (normalized === 'GRAPHQL_VALIDATION_FAILED') return DataErrorType.QUERY_GENERATION_FAILED;
- if (normalized === 'INTERNAL_ERROR' || normalized === 'INTERNAL_SERVER_ERROR') return DataErrorType.QUERY_EXECUTION_FAILED;
- if (normalized === 'NOT_FOUND') return DataErrorType.TABLE_NOT_FOUND;
- if (normalized === 'INVALID_INPUT') return DataErrorType.INVALID_MUTATION_DATA;
-
- // PostgreSQL SQLSTATE codes (surfaced via PostGraphile)
- if (code === PG_ERROR_CODES.UNIQUE_VIOLATION) return DataErrorType.UNIQUE_VIOLATION;
- if (code === PG_ERROR_CODES.FOREIGN_KEY_VIOLATION) return DataErrorType.FOREIGN_KEY_VIOLATION;
- if (code === PG_ERROR_CODES.NOT_NULL_VIOLATION) return DataErrorType.NOT_NULL_VIOLATION;
- if (code === PG_ERROR_CODES.CHECK_VIOLATION) return DataErrorType.CHECK_VIOLATION;
- if (code === PG_ERROR_CODES.EXCLUSION_VIOLATION) return DataErrorType.EXCLUSION_VIOLATION;
- if (code === PG_ERROR_CODES.INSUFFICIENT_PRIVILEGE) return DataErrorType.FORBIDDEN;
- if (code === PG_ERROR_CODES.UNDEFINED_TABLE) return DataErrorType.TABLE_NOT_FOUND;
- if (code === PG_ERROR_CODES.UNDEFINED_COLUMN) return DataErrorType.INVALID_MUTATION_DATA;
-
- // Data exception codes -> validation failed
- if (code === PG_ERROR_CODES.NUMERIC_VALUE_OUT_OF_RANGE) return DataErrorType.VALIDATION_FAILED;
- if (code === PG_ERROR_CODES.STRING_DATA_RIGHT_TRUNCATION) return DataErrorType.VALIDATION_FAILED;
- if (code === PG_ERROR_CODES.INVALID_TEXT_REPRESENTATION) return DataErrorType.VALIDATION_FAILED;
- if (code === PG_ERROR_CODES.DATETIME_FIELD_OVERFLOW) return DataErrorType.VALIDATION_FAILED;
-
- return DataErrorType.UNKNOWN_ERROR;
-}
-
-/**
- * Generic error matching API.
- * Works with any DataErrorType — no need for per-type helper functions.
- */
-export const Errors = {
- /** Map a raw extensions.code to its DataErrorType */
- parse(code: string | undefined): DataErrorType {
- return parseGraphQLErrorCode(code);
- },
- /** Check if a raw extensions.code matches a DataErrorType */
- is(code: unknown, type: DataErrorType): boolean {
- if (typeof code !== 'string') return false;
- return parseGraphQLErrorCode(code) === type;
- },
- /** Check if any error object matches a DataErrorType */
- match(error: unknown, type: DataErrorType): boolean {
- if (error instanceof DataError) return error.type === type;
- return parseGraphQLError(error).type === type;
- },
- /** Parse any error into a classified DataError */
- from(error: unknown, tableName?: string): DataError {
- return parseGraphQLError(error, tableName);
- },
-} as const;
-
-function isGraphQLRequestErrorLike(
- error: unknown,
-): error is Error & { errors: Array<{ message: string }> } {
- return (
- error instanceof Error &&
- error.name === 'GraphQLRequestError' &&
- Array.isArray((error as any).errors)
- );
-}
-
-function isGraphQLErrorLike(value: unknown): value is GraphQLError {
- return (
- !!value &&
- typeof value === 'object' &&
- 'message' in value &&
- typeof (value as { message?: unknown }).message === 'string' &&
- !(value instanceof Error)
- );
-}
-
-function extractCodeFromMessage(message: string): string | undefined {
- const match = message.match(/\(\s*Code:\s*([A-Za-z0-9_]+)\s*\)/);
- return match?.[1];
-}
-
-function classifyByMessage(message: string): DataErrorType {
- const lower = message.toLowerCase();
- if (lower.includes('timeout') || lower.includes('timed out') || lower.includes('aborted')) {
- return DataErrorType.TIMEOUT_ERROR;
- }
- if (lower.includes('network') || lower.includes('fetch') || lower.includes('failed to fetch')) {
- return DataErrorType.NETWORK_ERROR;
- }
- if (lower.includes('unauthenticated') || lower.includes('unauthorized') || lower.includes('not authorized') || lower.includes('authentication')) {
- return DataErrorType.UNAUTHORIZED;
- }
- if (lower.includes('forbidden') || lower.includes('access denied') || lower.includes('permission')) {
- return DataErrorType.FORBIDDEN;
- }
- if (lower.includes('validation')) {
- return DataErrorType.VALIDATION_FAILED;
- }
- // PostgreSQL constraint violation patterns in messages
- if (lower.includes('duplicate key') || lower.includes('already exists') || lower.includes('unique constraint')) {
- return DataErrorType.UNIQUE_VIOLATION;
- }
- if (lower.includes('foreign key constraint') || lower.includes('violates foreign key')) {
- return DataErrorType.FOREIGN_KEY_VIOLATION;
- }
- if (lower.includes('not-null constraint') || lower.includes('null value in column')) {
- return DataErrorType.NOT_NULL_VIOLATION;
- }
- if (lower.includes('check constraint')) {
- return DataErrorType.CHECK_VIOLATION;
- }
- return DataErrorType.UNKNOWN_ERROR;
-}
-
-/**
- * Extract PostgreSQL-specific details from error extensions.
- * PostGraphile surfaces these in the error.extensions object.
- */
-interface PostgresErrorDetails {
- constraint?: string;
- column?: string;
- table?: string;
- detail?: string;
-}
-
-function extractPostgresDetails(extensions: Record | undefined): PostgresErrorDetails {
- if (!extensions) return {};
- return {
- constraint: typeof extensions.constraint === 'string' ? extensions.constraint : undefined,
- column: typeof extensions.column === 'string' ? extensions.column : undefined,
- table: typeof extensions.table === 'string' ? extensions.table : undefined,
- detail: typeof extensions.detail === 'string' ? extensions.detail : undefined,
- };
-}
-
-/**
- * Try to extract the field name from a PostgreSQL error message or constraint name.
- */
-function extractFieldFromError(message: string, constraint?: string, column?: string): string | undefined {
- // If column is provided directly, use it
- if (column) return column;
-
- // Try to extract from "column X" pattern
- const columnMatch = message.match(/column\s+"?([a-z_][a-z0-9_]*)"?/i);
- if (columnMatch) return columnMatch[1];
-
- // Try to extract from constraint name (often formatted as table_field_key or table_field_fkey)
- if (constraint) {
- const constraintMatch = constraint.match(/_([a-z_][a-z0-9_]*)_(?:key|fkey|check|pkey)$/i);
- if (constraintMatch) return constraintMatch[1];
- }
-
- // Try to extract from "Key (field)=" pattern in duplicate key errors
- const keyMatch = message.match(/Key\s+\(([a-z_][a-z0-9_]*)\)/i);
- if (keyMatch) return keyMatch[1];
-
- return undefined;
-}
-
-/**
- * Parse any error into a DataError.
- * Optional tableName adds context for data-layer operations.
- */
-export function parseGraphQLError(error: unknown, tableName?: string): DataError {
- if (error instanceof DataError) {
- return error;
- }
-
- // SDK throws GraphQLRequestError (extends Error) wrapping an array of GraphQL errors.
- // Extract the first error and re-parse it as a plain GraphQL error object,
- // which the isGraphQLErrorLike branch below handles (reads extensions.code, etc.)
- if (isGraphQLRequestErrorLike(error) && error.errors.length > 0) {
- return parseGraphQLError(error.errors[0], tableName);
- }
-
- if (isGraphQLErrorLike(error)) {
- const extCode = error.extensions?.code;
- const mappedFromExt = parseGraphQLErrorCode(extCode);
-
- // Extract PostgreSQL-specific details
- const pgDetails = extractPostgresDetails(error.extensions);
- const effectiveTable = tableName || pgDetails.table;
- const fieldName = extractFieldFromError(error.message, pgDetails.constraint, pgDetails.column);
-
- if (mappedFromExt !== DataErrorType.UNKNOWN_ERROR) {
- switch (mappedFromExt) {
- case DataErrorType.UNAUTHORIZED:
- return createDataError.unauthorized(error.message, effectiveTable);
- case DataErrorType.FORBIDDEN:
- return createDataError.forbidden(effectiveTable);
- case DataErrorType.QUERY_GENERATION_FAILED:
- return createDataError.queryGenerationFailed(error.message, effectiveTable, extCode);
- case DataErrorType.QUERY_EXECUTION_FAILED:
- return createDataError.queryExecutionFailed(error.message, effectiveTable, extCode);
- case DataErrorType.TABLE_NOT_FOUND:
- return createDataError.tableNotFound(error.message, effectiveTable, extCode);
- case DataErrorType.INVALID_MUTATION_DATA:
- return createDataError.invalidMutationData(error.message, effectiveTable, extCode, error.extensions);
- // PostgreSQL constraint violations
- case DataErrorType.UNIQUE_VIOLATION:
- return createDataError.uniqueViolation(error.message, effectiveTable, fieldName, pgDetails.constraint);
- case DataErrorType.FOREIGN_KEY_VIOLATION:
- return createDataError.foreignKeyViolation(error.message, effectiveTable, fieldName, pgDetails.constraint);
- case DataErrorType.NOT_NULL_VIOLATION:
- return createDataError.notNullViolation(error.message, effectiveTable, fieldName, pgDetails.constraint);
- case DataErrorType.CHECK_VIOLATION:
- return createDataError.checkViolation(error.message, effectiveTable, fieldName, pgDetails.constraint);
- case DataErrorType.EXCLUSION_VIOLATION:
- return createDataError.exclusionViolation(error.message, effectiveTable, pgDetails.constraint);
- case DataErrorType.VALIDATION_FAILED:
- return createDataError.validationFailed(effectiveTable, [error.message]);
- default:
- break;
- }
- }
-
- // Fallback: classify by message content
- const fallbackType = classifyByMessage(error.message);
- switch (fallbackType) {
- case DataErrorType.VALIDATION_FAILED:
- return createDataError.validationFailed(effectiveTable, [error.message]);
- case DataErrorType.FORBIDDEN:
- return createDataError.forbidden(effectiveTable);
- case DataErrorType.UNAUTHORIZED:
- return createDataError.unauthorized('Authentication required', effectiveTable);
- case DataErrorType.TIMEOUT_ERROR:
- return createDataError.timeoutError(new Error(error.message), effectiveTable);
- case DataErrorType.NETWORK_ERROR:
- return createDataError.networkError(new Error(error.message), effectiveTable);
- case DataErrorType.UNIQUE_VIOLATION:
- return createDataError.uniqueViolation(error.message, effectiveTable, fieldName, pgDetails.constraint);
- case DataErrorType.FOREIGN_KEY_VIOLATION:
- return createDataError.foreignKeyViolation(error.message, effectiveTable, fieldName, pgDetails.constraint);
- case DataErrorType.NOT_NULL_VIOLATION:
- return createDataError.notNullViolation(error.message, effectiveTable, fieldName, pgDetails.constraint);
- case DataErrorType.CHECK_VIOLATION:
- return createDataError.checkViolation(error.message, effectiveTable, fieldName, pgDetails.constraint);
- default:
- return createDataError.unknown(new Error(error.message), effectiveTable);
- }
- }
-
- if (error instanceof Error) {
- const embeddedCode = extractCodeFromMessage(error.message);
- const mappedFromMessageCode = parseGraphQLErrorCode(embeddedCode);
- if (mappedFromMessageCode !== DataErrorType.UNKNOWN_ERROR) {
- switch (mappedFromMessageCode) {
- case DataErrorType.UNAUTHORIZED:
- return createDataError.unauthorized('Authentication required. Please log in again.', tableName);
- case DataErrorType.FORBIDDEN:
- return createDataError.forbidden(tableName);
- default:
- return new DataError(mappedFromMessageCode, error.message, { tableName, code: embeddedCode, originalError: error });
- }
- }
-
- const type = classifyByMessage(error.message);
- const fieldName = extractFieldFromError(error.message);
- switch (type) {
- case DataErrorType.NETWORK_ERROR:
- return createDataError.networkError(error, tableName);
- case DataErrorType.TIMEOUT_ERROR:
- return createDataError.timeoutError(error, tableName);
- case DataErrorType.UNAUTHORIZED:
- return createDataError.unauthorized(error.message, tableName);
- case DataErrorType.FORBIDDEN:
- return createDataError.forbidden(tableName);
- case DataErrorType.VALIDATION_FAILED:
- return createDataError.validationFailed(tableName, [error.message]);
- case DataErrorType.UNIQUE_VIOLATION:
- return createDataError.uniqueViolation(error.message, tableName, fieldName);
- case DataErrorType.FOREIGN_KEY_VIOLATION:
- return createDataError.foreignKeyViolation(error.message, tableName, fieldName);
- case DataErrorType.NOT_NULL_VIOLATION:
- return createDataError.notNullViolation(error.message, tableName, fieldName);
- case DataErrorType.CHECK_VIOLATION:
- return createDataError.checkViolation(error.message, tableName, fieldName);
- default:
- return createDataError.unknown(error, tableName);
- }
- }
-
- const errorMessage = typeof error === 'string' ? error : 'Unknown error occurred';
- return createDataError.unknown(new Error(errorMessage), tableName);
-}
-
-export function parseError(error: unknown): DataError {
- return parseGraphQLError(error);
-}
-
-// ============================================================================
-// Constraint Message Registry
-// ============================================================================
-
-/**
- * Constraint-specific human-friendly messages.
- * Keys can be:
- * - Exact constraint name: "database_schema_hash_key"
- * - Pattern with wildcard prefix: "*_name_key" (matches any table's name unique constraint)
- */
-export const CONSTRAINT_MESSAGES: Record = {
- // Database provisioning
- database_schema_hash_key: 'This database name is already taken. Please choose a different name.',
- database_name_key: 'This database name is already taken. Please choose a different name.',
- database_provision_module_name_length_min: 'Database name must be at least 3 characters.',
- database_provision_module_name_length_max: 'Database name must be 63 characters or less.',
- database_provision_module_name_format: 'Database name must start with a letter and use only letters, numbers, underscores, or hyphens.',
-
- // Domain constraints
- domain_subdomain_domain_key: 'This subdomain is already in use for this domain.',
-
- // API constraints
- api_name_database_id_key: 'An API with this name already exists in this database.',
-
- // Generic patterns (checked after exact matches)
- '*_email_key': 'This email address is already registered.',
- '*_name_key': 'This name is already taken.',
- '*_slug_key': 'This URL slug is already in use.',
-};
-
-/**
- * Get human-friendly message for a constraint violation.
- * Checks exact match first, then pattern matches with * prefix.
- */
-export function getConstraintMessage(constraint: string | undefined): string | undefined {
- if (!constraint) return undefined;
-
- // Exact match first
- if (CONSTRAINT_MESSAGES[constraint]) {
- return CONSTRAINT_MESSAGES[constraint];
- }
-
- // Pattern match (keys starting with * are suffix patterns)
- for (const [pattern, message] of Object.entries(CONSTRAINT_MESSAGES)) {
- if (pattern.startsWith('*')) {
- const suffix = pattern.slice(1);
- if (constraint.endsWith(suffix)) {
- return message;
- }
- }
- }
-
- return undefined;
-}
diff --git a/packages/schema-builder/src/schema/schema-builder-core/lib/data/index.ts b/packages/schema-builder/src/schema/schema-builder-core/lib/data/index.ts
index 97c9f88..4fe309e 100644
--- a/packages/schema-builder/src/schema/schema-builder-core/lib/data/index.ts
+++ b/packages/schema-builder/src/schema/schema-builder-core/lib/data/index.ts
@@ -1,13 +1,11 @@
export { asFieldIds, stripEmpty } from './mutation-input';
export {
- DataError,
- DataErrorType,
- Errors,
- createDataError,
+ ConstructiveError,
+ createError,
+ isAuthenticationError,
parseError,
parseGraphQLError,
- parseGraphQLErrorCode,
-} from './error-handler';
+} from '@constructive-io/data';
export {
buildNodeData,
buildNodeDataForDataNodeType,
diff --git a/packages/schema-builder/src/schema/schema-builder-core/lib/gql/auth-error-handler.tsx b/packages/schema-builder/src/schema/schema-builder-core/lib/gql/auth-error-handler.tsx
index c174ee4..92fdce7 100644
--- a/packages/schema-builder/src/schema/schema-builder-core/lib/gql/auth-error-handler.tsx
+++ b/packages/schema-builder/src/schema/schema-builder-core/lib/gql/auth-error-handler.tsx
@@ -6,7 +6,7 @@ import { RiLockLine } from '@remixicon/react';
import { cn } from '@/lib/utils';
-import { DataError, DataErrorType, Errors, parseError } from '../data';
+import { ConstructiveError, isAuthenticationError, parseError } from '../data';
// ============================================================================
// Auth Error Detection
@@ -16,7 +16,7 @@ import { DataError, DataErrorType, Errors, parseError } from '../data';
* Check if an error is an authentication error (expired/missing session).
*/
export function isAuthError(error: unknown): boolean {
- return Errors.match(error, DataErrorType.UNAUTHORIZED);
+ return isAuthenticationError(error);
}
// ============================================================================
@@ -24,7 +24,7 @@ export function isAuthError(error: unknown): boolean {
// ============================================================================
interface AuthErrorBannerProps {
- error: Error | DataError;
+ error: Error;
/** Custom message override; defaults to the parsed error's user message. */
message?: string;
className?: string;
@@ -42,8 +42,8 @@ export function AuthErrorBanner({ error, message, className }: AuthErrorBannerPr
queryClient.clear();
}, [queryClient]);
- const dataError = error instanceof DataError ? error : parseError(error);
- const displayMessage = message || dataError.getUserMessage();
+ const normalizedError = error instanceof ConstructiveError ? error : parseError(error);
+ const displayMessage = message || normalizedError.message;
return (
0,
staleTime: 30 * 1000, // 30 seconds - shorter for fresher data
refetchOnMount: true, // Respect staleTime (NOT 'always' which ignores cache)
- // Keep previous data during refetch to prevent UI flicker
- // This prevents databases array from resetting during background refetch
- placeholderData: keepPreviousData,
});
// Extract the data from the response (data is undefined when query is disabled or loading)
diff --git a/packages/schema-builder/src/schema/schema-builder-core/lib/gql/hooks/schema-builder/use-database-constraints.ts b/packages/schema-builder/src/schema/schema-builder-core/lib/gql/hooks/schema-builder/use-database-constraints.ts
index 5b698ad..652f4bb 100644
--- a/packages/schema-builder/src/schema/schema-builder-core/lib/gql/hooks/schema-builder/use-database-constraints.ts
+++ b/packages/schema-builder/src/schema/schema-builder-core/lib/gql/hooks/schema-builder/use-database-constraints.ts
@@ -17,7 +17,7 @@
* - Constraints: 5min staleTime (changes rarely)
* - Databases: 30s staleTime (see use-accessible-databases.ts)
*/
-import { keepPreviousData, useQuery } from '@tanstack/react-query';
+import { useQuery } from '@tanstack/react-query';
import {
schemaBuilderQueryKey,
useSchemaBuilderSdkClient,
@@ -241,9 +241,6 @@ export function useDatabaseConstraints(
enabled,
staleTime: 5 * 60 * 1000, // 5 minutes - constraints change rarely
refetchOnMount: true, // Respect staleTime (NOT 'always' which ignores cache)
- // Keep previous data during refetch to prevent UI flicker
- // This prevents constraint arrays from resetting to [] during background refetch
- placeholderData: keepPreviousData,
});
return {
diff --git a/packages/schema-builder/src/schema/schema-builder-core/lib/gql/hooks/schema-builder/use-schema-builder-selectors.ts b/packages/schema-builder/src/schema/schema-builder-core/lib/gql/hooks/schema-builder/use-schema-builder-selectors.ts
index e478315..5a4527b 100644
--- a/packages/schema-builder/src/schema/schema-builder-core/lib/gql/hooks/schema-builder/use-schema-builder-selectors.ts
+++ b/packages/schema-builder/src/schema/schema-builder-core/lib/gql/hooks/schema-builder/use-schema-builder-selectors.ts
@@ -89,7 +89,20 @@ export interface SchemaBuilderDataState {
const SchemaBuilderDataContext = createContext
(null);
-export function SchemaBuilderDataProvider({ children }: { children: ReactNode }) {
+export interface SchemaBuilderDataProviderProps {
+ children: ReactNode;
+ value?: SchemaBuilderDataState;
+}
+
+export function SchemaBuilderDataProvider({ children, value }: SchemaBuilderDataProviderProps) {
+ if (value) {
+ return createElement(SchemaBuilderDataContext.Provider, { value }, children);
+ }
+
+ return createElement(SchemaBuilderQueryDataProvider, null, children);
+}
+
+function SchemaBuilderQueryDataProvider({ children }: { children: ReactNode }) {
const { databaseId, orgId } = useSchemaBuilderConfig();
const shouldLoadFullSchemaData = Boolean(databaseId);
diff --git a/packages/schema-builder/src/schema/schema-builder-fields/components/table-editor/table-editor.tsx b/packages/schema-builder/src/schema/schema-builder-fields/components/table-editor/table-editor.tsx
index 163c299..0cb059e 100644
--- a/packages/schema-builder/src/schema/schema-builder-fields/components/table-editor/table-editor.tsx
+++ b/packages/schema-builder/src/schema/schema-builder-fields/components/table-editor/table-editor.tsx
@@ -113,13 +113,16 @@ export function TableEditor() {
>
{/* Main content area - add right padding to make room for collapsed panel */}