mirror of https://github.com/buster-so/buster.git
Merge pull request #334 from buster-so/big-nate/bus-1174-make-zod-or-valibot-types-for-api-protocol-types
Big nate/bus 1174 make zod or valibot types for api protocol types
This commit is contained in:
commit
49adaa1f7e
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "buster_server"
|
||||
version = "0.1.12"
|
||||
version = "0.1.13"
|
||||
edition = "2021"
|
||||
default-run = "buster_server"
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "buster-cli"
|
||||
version = "0.1.12"
|
||||
version = "0.1.13"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"api_tag": "api/v0.1.12", "api_version": "0.1.12"
|
||||
"api_tag": "api/v0.1.13", "api_version": "0.1.13"
|
||||
,
|
||||
"web_tag": "web/v0.1.12", "web_version": "0.1.12"
|
||||
"web_tag": "web/v0.1.13", "web_version": "0.1.13"
|
||||
,
|
||||
"cli_tag": "cli/v0.1.12", "cli_version": "0.1.12"
|
||||
"cli_tag": "cli/v0.1.13", "cli_version": "0.1.13"
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ You are a TypeScript expert with deep knowledge of the language's features and b
|
|||
- Use TypeScript for all new code
|
||||
- Prefer functional components and hooks over class components
|
||||
- Use proper TypeScript types for all variables and functions
|
||||
- When importing types, ALWAYS append the type with the keyword type in the import (in order to minimize build size)
|
||||
|
||||
## Best Practices
|
||||
- Try to use components defined in my @/components/ui folder.
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "web",
|
||||
"version": "0.1.12",
|
||||
"version": "0.1.13",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbo",
|
||||
|
@ -25,30 +25,30 @@
|
|||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@faker-js/faker": "^9.7.0",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@radix-ui/react-avatar": "^1.1.9",
|
||||
"@radix-ui/react-checkbox": "^1.3.1",
|
||||
"@radix-ui/react-collapsible": "^1.1.10",
|
||||
"@radix-ui/react-context-menu": "^2.2.14",
|
||||
"@radix-ui/react-dialog": "^1.1.13",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||
"@radix-ui/react-label": "^2.1.6",
|
||||
"@radix-ui/react-popover": "^1.1.13",
|
||||
"@radix-ui/react-scroll-area": "^1.2.8",
|
||||
"@radix-ui/react-select": "^2.2.4",
|
||||
"@radix-ui/react-separator": "^1.1.6",
|
||||
"@radix-ui/react-slider": "^1.3.4",
|
||||
"@radix-ui/react-switch": "^1.2.4",
|
||||
"@radix-ui/react-tabs": "^1.1.11",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-context-menu": "^2.2.15",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@radix-ui/react-tooltip": "^1.2.6",
|
||||
"@supabase/ssr": "^0.6.1",
|
||||
"@supabase/supabase-js": "^2.49.4",
|
||||
"@tanstack/query-sync-storage-persister": "^5.76.1",
|
||||
"@tanstack/react-form": "^1.11.1",
|
||||
"@tanstack/react-query": "^5.76.1",
|
||||
"@tanstack/react-query-devtools": "^5.76.1",
|
||||
"@tanstack/react-query-persist-client": "^5.76.1",
|
||||
"@supabase/supabase-js": "^2.49.8",
|
||||
"@tanstack/query-sync-storage-persister": "^5.77.2",
|
||||
"@tanstack/react-form": "^1.12.0",
|
||||
"@tanstack/react-query": "^5.77.2",
|
||||
"@tanstack/react-query-devtools": "^5.77.2",
|
||||
"@tanstack/react-query-persist-client": "^5.77.2",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.8",
|
||||
"@tanstack/react-virtual": "^3.13.9",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/prettier": "^2.7.3",
|
||||
"@types/react-color": "^3.0.13",
|
||||
|
@ -65,7 +65,7 @@
|
|||
"dom-to-image": "^2.6.0",
|
||||
"email-validator": "^2.0.4",
|
||||
"font-color-contrast": "^11.1.0",
|
||||
"framer-motion": "^12.11.3",
|
||||
"framer-motion": "^12.15.0",
|
||||
"intersection-observer": "^0.12.2",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
|
@ -73,14 +73,14 @@
|
|||
"js-yaml": "^4.1.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"monaco-sql-languages": "^0.14.0",
|
||||
"monaco-sql-languages": "^0.15.0",
|
||||
"monaco-yaml": "^5.4.0",
|
||||
"mutative": "^1.1.0",
|
||||
"mutative": "^1.2.0",
|
||||
"next": "14.2.28",
|
||||
"next-themes": "^0.4.6",
|
||||
"papaparse": "^5.5.2",
|
||||
"papaparse": "^5.5.3",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "^1.242.1",
|
||||
"posthog-js": "^1.248.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"react": "^18",
|
||||
|
@ -95,12 +95,12 @@
|
|||
"remark-gfm": "^4.0.1",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"ts-jest": "^29.3.3",
|
||||
"ts-jest": "^29.3.4",
|
||||
"use-context-selector": "^2.0.0",
|
||||
"utility-types": "^3.11.0",
|
||||
"valibot": "^1.1.0",
|
||||
"virtua": "^0.41.0",
|
||||
"zustand": "^5.0.4"
|
||||
"virtua": "^0.41.2",
|
||||
"zod": "^3.25.32",
|
||||
"zustand": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^3.2.6",
|
||||
|
@ -108,19 +108,19 @@
|
|||
"@next/bundle-analyzer": "^15.3.2",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@storybook/addon-controls": "^8.6.12",
|
||||
"@storybook/addon-essentials": "^8.6.12",
|
||||
"@storybook/addon-interactions": "^8.6.12",
|
||||
"@storybook/blocks": "^8.6.12",
|
||||
"@storybook/nextjs": "^8.6.12",
|
||||
"@storybook/react": "^8.6.12",
|
||||
"@storybook/test": "^8.6.12",
|
||||
"@tailwindcss/postcss": "4.1.6",
|
||||
"@storybook/addon-essentials": "^8.6.14",
|
||||
"@storybook/addon-interactions": "^8.6.14",
|
||||
"@storybook/blocks": "^8.6.14",
|
||||
"@storybook/nextjs": "^8.6.14",
|
||||
"@storybook/react": "^8.6.14",
|
||||
"@storybook/test": "^8.6.14",
|
||||
"@tailwindcss/postcss": "4.1.8",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/dotenv": "^8.2.3",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/lodash": "^4.17.17",
|
||||
"@types/node": "^20",
|
||||
"@types/papaparse": "^5.3.16",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
|
@ -134,9 +134,9 @@
|
|||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-storybook": "^0.12.0",
|
||||
"msw-storybook-addon": "^2.0.4",
|
||||
"sass": "^1.88.0",
|
||||
"sass": "^1.89.0",
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"tailwindcss": "4.1.6",
|
||||
"tailwindcss": "4.1.8",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5"
|
||||
},
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
export interface BusterApiKeyListItem {
|
||||
id: string;
|
||||
owner_id: string;
|
||||
owner_email: string;
|
||||
created_at: string;
|
||||
}
|
||||
import { z } from 'zod/v4-mini';
|
||||
|
||||
export const BusterApiKeyListItemSchema = z.object({
|
||||
id: z.string(),
|
||||
owner_id: z.string('Owner ID is required'),
|
||||
owner_email: z.string('Owner email is required'),
|
||||
created_at: z.string('Created at is required')
|
||||
});
|
||||
|
||||
export type BusterApiKeyListItem = z.infer<typeof BusterApiKeyListItemSchema>;
|
||||
|
||||
// API Response schemas
|
||||
export const GetApiKeysResponseSchema = z.object({
|
||||
api_keys: z.array(BusterApiKeyListItemSchema)
|
||||
});
|
||||
|
||||
export type GetApiKeysResponse = z.infer<typeof GetApiKeysResponseSchema>;
|
||||
|
||||
export const CreateApiKeyResponseSchema = z.object({
|
||||
api_key: z.string('API Key is required')
|
||||
});
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import * as v from 'valibot';
|
||||
import {
|
||||
DataSourceSchema,
|
||||
DataSourceTypes,
|
||||
|
@ -11,20 +10,18 @@ import {
|
|||
SQLServerCredentialsSchema,
|
||||
SnowflakeCredentials
|
||||
} from './interfaces';
|
||||
import { z } from 'zod/v4-mini';
|
||||
|
||||
// Helper function to test validation
|
||||
const testValidation = (
|
||||
schema: any,
|
||||
schema: z.ZodMiniObject<any>,
|
||||
value: any
|
||||
): { success: boolean; issues?: v.ValiError<any>['issues'] } => {
|
||||
try {
|
||||
v.parse(schema, value);
|
||||
): { success: boolean; issues?: z.core.$ZodIssue[] } => {
|
||||
const result = schema.safeParse(value);
|
||||
if (result.success) {
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (error instanceof v.ValiError) {
|
||||
return { success: false, issues: error.issues };
|
||||
}
|
||||
throw error;
|
||||
} else {
|
||||
return { success: false, issues: result.error.issues };
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -134,7 +131,10 @@ describe('DataSourceSchema', () => {
|
|||
const result = testValidation(DataSourceSchema, invalidPortDataSource);
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.issues).toBeDefined();
|
||||
expect(result.issues![0].message).toBe('Port must be greater than 0');
|
||||
expect(result.issues![0].message).toBe('Invalid input');
|
||||
|
||||
const testMessage = result.issues![0] as z.core.$ZodIssueInvalidUnion;
|
||||
expect(testMessage.errors[0][0].message).toBe('Port must be greater than 0');
|
||||
});
|
||||
|
||||
test('should fail validation with missing required fields', () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as v from 'valibot';
|
||||
import { z } from 'zod/v4-mini';
|
||||
|
||||
export enum DataSourceStatus {
|
||||
ACTIVE = 'active',
|
||||
|
@ -56,145 +56,148 @@ export enum DataSourceEnvironment {
|
|||
development = 'development'
|
||||
}
|
||||
|
||||
export const PostgresCredentialsSchema = v.object({
|
||||
name: v.pipe(v.string(), v.minLength(3, 'Name must be at least 3 characters')),
|
||||
type: v.union([v.literal('postgres'), v.literal('supabase')]),
|
||||
host: v.pipe(v.string(), v.minLength(1, 'Host must not be empty')),
|
||||
port: v.pipe(
|
||||
v.number(),
|
||||
v.minValue(1, 'Port must be greater than 0'),
|
||||
v.maxValue(65535, 'Port must be less than or equal to 65535')
|
||||
),
|
||||
username: v.pipe(v.string(), v.minLength(1, 'Username must not be empty')),
|
||||
password: v.pipe(v.string(), v.minLength(1, 'Password must not be empty')),
|
||||
default_database: v.pipe(v.string(), v.minLength(1, 'Database must not be empty')), // postgres
|
||||
default_schema: v.pipe(v.string(), v.minLength(1, 'Schema must not be empty')) // public
|
||||
export const PostgresCredentialsSchema = z.object({
|
||||
name: z.string().check(z.minLength(3, 'Name must be at least 3 characters')),
|
||||
type: z.union([z.literal('postgres'), z.literal('supabase')]),
|
||||
host: z.string().check(z.minLength(1, 'Host must not be empty')),
|
||||
port: z
|
||||
.number()
|
||||
.check(
|
||||
z.gte(1, 'Port must be greater than 0'),
|
||||
z.lte(65535, 'Port must be less than or equal to 65535')
|
||||
),
|
||||
username: z.string().check(z.minLength(1, 'Username must not be empty')),
|
||||
password: z.string().check(z.minLength(1, 'Password must not be empty')),
|
||||
default_database: z.string().check(z.minLength(1, 'Database must not be empty')), // postgres
|
||||
default_schema: z.string().check(z.minLength(1, 'Schema must not be empty')) // public
|
||||
});
|
||||
|
||||
export type PostgresCredentials = v.InferOutput<typeof PostgresCredentialsSchema>;
|
||||
export type PostgresCredentials = z.infer<typeof PostgresCredentialsSchema>;
|
||||
|
||||
export const MySQLCredentialsSchema = v.object({
|
||||
name: v.pipe(v.string(), v.minLength(1, 'Name must not be empty')),
|
||||
type: v.union([v.literal('mysql'), v.literal('mariadb')]),
|
||||
host: v.pipe(v.string(), v.minLength(1, 'Host must not be empty')),
|
||||
port: v.pipe(
|
||||
v.number(),
|
||||
v.minValue(1, 'Port must be greater than 0'),
|
||||
v.maxValue(65535, 'Port must be less than or equal to 65535')
|
||||
),
|
||||
username: v.pipe(v.string(), v.minLength(1, 'Username must not be empty')),
|
||||
password: v.pipe(v.string(), v.minLength(1, 'Password must not be empty')),
|
||||
default_database: v.pipe(v.string(), v.minLength(1, 'Database must not be empty'))
|
||||
export const MySQLCredentialsSchema = z.object({
|
||||
name: z.string().check(z.minLength(1, 'Name must not be empty')),
|
||||
type: z.union([z.literal('mysql'), z.literal('mariadb')]),
|
||||
host: z.string().check(z.minLength(1, 'Host must not be empty')),
|
||||
port: z
|
||||
.number()
|
||||
.check(
|
||||
z.gte(1, 'Port must be greater than 0'),
|
||||
z.lte(65535, 'Port must be less than or equal to 65535')
|
||||
),
|
||||
username: z.string().check(z.minLength(1, 'Username must not be empty')),
|
||||
password: z.string().check(z.minLength(1, 'Password must not be empty')),
|
||||
default_database: z.string().check(z.minLength(1, 'Database must not be empty'))
|
||||
});
|
||||
|
||||
export type MySQLCredentials = v.InferOutput<typeof MySQLCredentialsSchema>;
|
||||
export type MySQLCredentials = z.infer<typeof MySQLCredentialsSchema>;
|
||||
|
||||
export const BigQueryCredentialsSchema = v.object({
|
||||
name: v.pipe(v.string(), v.minLength(1, 'Name must not be empty')),
|
||||
type: v.literal('bigquery'),
|
||||
service_role_key: v.pipe(v.string(), v.minLength(1, 'Service role key must not be empty')),
|
||||
default_project_id: v.pipe(v.string(), v.minLength(1, 'Project ID must not be empty')),
|
||||
default_dataset_id: v.pipe(v.string(), v.minLength(1, 'Dataset ID must not be empty'))
|
||||
export const BigQueryCredentialsSchema = z.object({
|
||||
name: z.string().check(z.minLength(1, 'Name must not be empty')),
|
||||
type: z.literal('bigquery'),
|
||||
service_role_key: z.string().check(z.minLength(1, 'Service role key must not be empty')),
|
||||
default_project_id: z.string().check(z.minLength(1, 'Project ID must not be empty')),
|
||||
default_dataset_id: z.string().check(z.minLength(1, 'Dataset ID must not be empty'))
|
||||
});
|
||||
|
||||
export type BigQueryCredentials = v.InferOutput<typeof BigQueryCredentialsSchema>;
|
||||
export type BigQueryCredentials = z.infer<typeof BigQueryCredentialsSchema>;
|
||||
|
||||
export const RedshiftCredentialsSchema = v.object({
|
||||
name: v.pipe(v.string(), v.minLength(1, 'Name must not be empty')),
|
||||
type: v.literal('redshift'),
|
||||
host: v.pipe(v.string(), v.minLength(1, 'Host must not be empty')),
|
||||
port: v.pipe(
|
||||
v.number(),
|
||||
v.minValue(1, 'Port must be greater than 0'),
|
||||
v.maxValue(65535, 'Port must be less than or equal to 65535')
|
||||
),
|
||||
username: v.pipe(v.string(), v.minLength(1, 'Username must not be empty')),
|
||||
password: v.pipe(v.string(), v.minLength(1, 'Password must not be empty')),
|
||||
default_database: v.pipe(v.string(), v.minLength(1, 'Database must not be empty')),
|
||||
default_schema: v.pipe(v.string(), v.minLength(1, 'Schema must not be empty'))
|
||||
export const RedshiftCredentialsSchema = z.object({
|
||||
name: z.string().check(z.minLength(1, 'Name must not be empty')),
|
||||
type: z.literal('redshift'),
|
||||
host: z.string().check(z.minLength(1, 'Host must not be empty')),
|
||||
port: z
|
||||
.number()
|
||||
.check(
|
||||
z.gte(1, 'Port must be greater than 0'),
|
||||
z.lte(65535, 'Port must be less than or equal to 65535')
|
||||
),
|
||||
username: z.string().check(z.minLength(1, 'Username must not be empty')),
|
||||
password: z.string().check(z.minLength(1, 'Password must not be empty')),
|
||||
default_database: z.string().check(z.minLength(1, 'Database must not be empty')),
|
||||
default_schema: z.string().check(z.minLength(1, 'Schema must not be empty'))
|
||||
});
|
||||
|
||||
export type RedshiftCredentials = v.InferOutput<typeof RedshiftCredentialsSchema>;
|
||||
export type RedshiftCredentials = z.infer<typeof RedshiftCredentialsSchema>;
|
||||
|
||||
export const SnowflakeCredentialsSchema = v.object({
|
||||
name: v.pipe(v.string(), v.minLength(1, 'Name must not be empty')),
|
||||
type: v.literal('snowflake'),
|
||||
account_id: v.pipe(v.string(), v.minLength(1, 'Account ID must not be empty')),
|
||||
warehouse_id: v.pipe(v.string(), v.minLength(1, 'Warehouse ID must not be empty')),
|
||||
username: v.pipe(v.string(), v.minLength(1, 'Username must not be empty')),
|
||||
password: v.pipe(v.string(), v.minLength(1, 'Password must not be empty')),
|
||||
role: v.nullable(v.string()),
|
||||
default_database: v.pipe(v.string(), v.minLength(1, 'Database must not be empty')),
|
||||
default_schema: v.pipe(
|
||||
v.string(),
|
||||
v.minLength(1, 'Schema must not be empty'),
|
||||
v.toUpperCase(),
|
||||
v.custom((value) => value === String(value).toUpperCase(), 'Must be all uppercase')
|
||||
export const SnowflakeCredentialsSchema = z.object({
|
||||
name: z.string().check(z.minLength(1, 'Name must not be empty')),
|
||||
type: z.literal('snowflake'),
|
||||
account_id: z.string().check(z.minLength(1, 'Account ID must not be empty')),
|
||||
warehouse_id: z.string().check(z.minLength(1, 'Warehouse ID must not be empty')),
|
||||
username: z.string().check(z.minLength(1, 'Username must not be empty')),
|
||||
password: z.string().check(z.minLength(1, 'Password must not be empty')),
|
||||
role: z.nullable(z.string()),
|
||||
default_database: z.string().check(z.minLength(1, 'Database must not be empty')),
|
||||
default_schema: z.string().check(
|
||||
z.minLength(1, 'Schema must not be empty'),
|
||||
z.toUpperCase(),
|
||||
z.refine((val) => val === val.toUpperCase(), 'Must be all uppercase')
|
||||
)
|
||||
});
|
||||
|
||||
export type SnowflakeCredentials = v.InferOutput<typeof SnowflakeCredentialsSchema>;
|
||||
export type SnowflakeCredentials = z.infer<typeof SnowflakeCredentialsSchema>;
|
||||
|
||||
export const DatabricksCredentialsSchema = v.object({
|
||||
name: v.string(),
|
||||
type: v.literal('databricks'),
|
||||
host: v.string(),
|
||||
api_key: v.string('API key is required'),
|
||||
warehouse_id: v.string('Warehouse ID is required'),
|
||||
default_catalog: v.string(),
|
||||
default_schema: v.string()
|
||||
export const DatabricksCredentialsSchema = z.object({
|
||||
name: z.string(),
|
||||
type: z.literal('databricks'),
|
||||
host: z.string(),
|
||||
api_key: z.string().check(z.refine((val) => val.length > 0, 'API key is required')),
|
||||
warehouse_id: z.string().check(z.refine((val) => val.length > 0, 'Warehouse ID is required')),
|
||||
default_catalog: z.string(),
|
||||
default_schema: z.string()
|
||||
});
|
||||
|
||||
export type DatabricksCredentials = v.InferOutput<typeof DatabricksCredentialsSchema>;
|
||||
export type DatabricksCredentials = z.infer<typeof DatabricksCredentialsSchema>;
|
||||
|
||||
export const SQLServerCredentialsSchema = v.object({
|
||||
name: v.string(),
|
||||
type: v.literal('sqlserver'),
|
||||
host: v.string(),
|
||||
port: v.pipe(
|
||||
v.number(),
|
||||
v.minValue(1, 'Port must be greater than 0'),
|
||||
v.maxValue(65535, 'Port must be less than or equal to 65535')
|
||||
),
|
||||
username: v.pipe(v.string(), v.minLength(1, 'Username must not be empty')),
|
||||
password: v.pipe(v.string(), v.minLength(1, 'Password must not be empty')),
|
||||
default_database: v.string(),
|
||||
default_schema: v.string()
|
||||
export const SQLServerCredentialsSchema = z.object({
|
||||
name: z.string().check(z.minLength(1, 'Name must not be empty')),
|
||||
type: z.literal('sqlserver'),
|
||||
host: z.string().check(z.minLength(1, 'Host must not be empty')),
|
||||
port: z
|
||||
.number()
|
||||
.check(
|
||||
z.gte(1, 'Port must be greater than 0'),
|
||||
z.lte(65535, 'Port must be less than or equal to 65535')
|
||||
),
|
||||
username: z.string().check(z.minLength(1, 'Username must not be empty')),
|
||||
password: z.string().check(z.minLength(1, 'Password must not be empty')),
|
||||
default_database: z.string().check(z.minLength(1, 'Database must not be empty')),
|
||||
default_schema: z.string().check(z.minLength(1, 'Schema must not be empty'))
|
||||
});
|
||||
|
||||
export type SQLServerCredentials = v.InferOutput<typeof SQLServerCredentialsSchema>;
|
||||
export type SQLServerCredentials = z.infer<typeof SQLServerCredentialsSchema>;
|
||||
|
||||
export const DataSetSchema = v.object({
|
||||
id: v.string('Dataset ID is required'),
|
||||
name: v.string('Dataset name is required')
|
||||
export const DataSetSchema = z.object({
|
||||
id: z.string().check(z.refine((val) => val.length > 0, 'Dataset ID is required')),
|
||||
name: z.string().check(z.refine((val) => val.length > 0, 'Dataset name is required'))
|
||||
});
|
||||
|
||||
export const CreatedBySchema = v.object({
|
||||
email: v.string('Email is required'),
|
||||
id: v.string('User ID is required'),
|
||||
name: v.string('User name is required')
|
||||
export const CreatedBySchema = z.object({
|
||||
email: z.string().check(z.refine((val) => val.length > 0, 'Email is required')),
|
||||
id: z.string().check(z.refine((val) => val.length > 0, 'User ID is required')),
|
||||
name: z.string().check(z.refine((val) => val.length > 0, 'User name is required'))
|
||||
});
|
||||
|
||||
export const DataSourceSchema = v.object({
|
||||
created_at: v.pipe(v.string()),
|
||||
export const DataSourceSchema = z.object({
|
||||
created_at: z.string(),
|
||||
created_by: CreatedBySchema,
|
||||
credentials: v.union([
|
||||
v.omit(PostgresCredentialsSchema, ['name']),
|
||||
v.omit(MySQLCredentialsSchema, ['name']),
|
||||
v.omit(BigQueryCredentialsSchema, ['name']),
|
||||
v.omit(RedshiftCredentialsSchema, ['name']),
|
||||
v.omit(SnowflakeCredentialsSchema, ['name']),
|
||||
v.omit(DatabricksCredentialsSchema, ['name']),
|
||||
v.omit(SQLServerCredentialsSchema, ['name'])
|
||||
credentials: z.union([
|
||||
z.omit(PostgresCredentialsSchema, { name: true }),
|
||||
z.omit(MySQLCredentialsSchema, { name: true }),
|
||||
z.omit(BigQueryCredentialsSchema, { name: true }),
|
||||
z.omit(RedshiftCredentialsSchema, { name: true }),
|
||||
z.omit(SnowflakeCredentialsSchema, { name: true }),
|
||||
z.omit(DatabricksCredentialsSchema, { name: true }),
|
||||
z.omit(SQLServerCredentialsSchema, { name: true })
|
||||
]),
|
||||
data_sets: v.array(DataSetSchema),
|
||||
id: v.string('Data source ID is required'),
|
||||
name: v.string('Data source name is required'),
|
||||
type: v.enum(DataSourceTypes),
|
||||
updated_at: v.pipe(v.string())
|
||||
data_sets: z.array(DataSetSchema),
|
||||
id: z.string().check(z.refine((val) => val.length > 0, 'Data source ID is required')),
|
||||
name: z.string().check(z.refine((val) => val.length > 0, 'Data source name is required')),
|
||||
type: z.enum(DataSourceTypes),
|
||||
updated_at: z.string()
|
||||
});
|
||||
|
||||
export type DataSource = v.InferOutput<typeof DataSourceSchema>;
|
||||
export type DataSource = z.infer<typeof DataSourceSchema>;
|
||||
|
||||
export interface DataSourceListItem {
|
||||
name: string;
|
||||
|
|
|
@ -1,18 +1,58 @@
|
|||
import mainApi from '@/api/buster_rest/instances';
|
||||
import type { BusterApiKeyListItem } from '@/api/asset_interfaces/api_keys';
|
||||
import {
|
||||
BusterApiKeyListItemSchema,
|
||||
GetApiKeysResponseSchema,
|
||||
CreateApiKeyResponseSchema
|
||||
} from '@/api/asset_interfaces/api_keys';
|
||||
|
||||
// Get API Keys
|
||||
export const getApiKeys = async () => {
|
||||
return mainApi.get<{ api_keys: BusterApiKeyListItem[] }>(`/api_keys`).then((res) => res.data);
|
||||
const response = await mainApi.get(`/api_keys`);
|
||||
const result = GetApiKeysResponseSchema.safeParse(response.data);
|
||||
|
||||
if (!result.success) {
|
||||
console.error('API Keys validation error:', result.error.issues);
|
||||
throw new Error(
|
||||
`Invalid API response format for getApiKeys: ${result.error.issues.map((e) => e.message).join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
// Create API Key
|
||||
|
||||
export const createApiKey = async (name: string) => {
|
||||
return mainApi.post<{ api_key: string }>(`/api_keys`, { name }).then((res) => res.data);
|
||||
const response = await mainApi.post(`/api_keys`, { name });
|
||||
const result = CreateApiKeyResponseSchema.safeParse(response.data);
|
||||
|
||||
if (!result.success) {
|
||||
console.error('Create API Key validation error:', result.error.issues);
|
||||
throw new Error(
|
||||
`Invalid API response format for createApiKey: ${result.error.issues.map((e) => e.message).join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
// Delete API Key
|
||||
export const deleteApiKey = async (id: string) => {
|
||||
return mainApi.delete(`/api_keys/${id}`).then((res) => res.data);
|
||||
const response = await mainApi.delete(`/api_keys/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Get Single API Key
|
||||
export const getApiKey = async (id: string) => {
|
||||
return mainApi.get<BusterApiKeyListItem>(`/api_keys/${id}`).then((res) => res.data);
|
||||
const response = await mainApi.get(`/api_keys/${id}`);
|
||||
const result = BusterApiKeyListItemSchema.safeParse(response.data);
|
||||
|
||||
if (!result.success) {
|
||||
console.error('Get API Key validation error:', result.error.issues);
|
||||
throw new Error(
|
||||
`Invalid API response format for getApiKey: ${result.error.issues.map((e) => e.message).join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import * as v from 'valibot';
|
||||
import { DataSource, DataSourceSchema, DataSourceTypes } from '@/api/asset_interfaces/datasources';
|
||||
import { getDatasource } from './requests';
|
||||
import mainApi from '../instances';
|
||||
|
@ -11,9 +10,12 @@ jest.mock('../instances', () => ({
|
|||
}
|
||||
}));
|
||||
|
||||
jest.mock('valibot', () => ({
|
||||
...jest.requireActual('valibot'),
|
||||
parse: jest.fn().mockImplementation((schema, data) => data)
|
||||
// Mock the DataSourceSchema
|
||||
jest.mock('@/api/asset_interfaces/datasources', () => ({
|
||||
...jest.requireActual('@/api/asset_interfaces/datasources'),
|
||||
DataSourceSchema: {
|
||||
parse: jest.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
describe('data_source requests', () => {
|
||||
|
@ -51,11 +53,13 @@ describe('data_source requests', () => {
|
|||
data: mockDataSource
|
||||
});
|
||||
|
||||
// Setup schema validation mock
|
||||
(DataSourceSchema.parse as jest.Mock).mockReturnValue(mockDataSource);
|
||||
|
||||
const result = await getDatasource('test-id');
|
||||
|
||||
expect(true).toBe(true);
|
||||
expect(mainApi.get).toHaveBeenCalledWith('/data_sources/test-id');
|
||||
expect(v.parse).toHaveBeenCalledWith(DataSourceSchema, mockDataSource);
|
||||
expect(DataSourceSchema.parse).toHaveBeenCalledWith(mockDataSource);
|
||||
expect(result).toEqual(mockDataSource);
|
||||
});
|
||||
|
||||
|
@ -77,14 +81,14 @@ describe('data_source requests', () => {
|
|||
|
||||
// Setup validation error
|
||||
const validationError = new Error('Validation failed');
|
||||
jest.spyOn(v, 'parse').mockImplementation(() => {
|
||||
(DataSourceSchema.parse as jest.Mock).mockImplementation(() => {
|
||||
throw validationError;
|
||||
});
|
||||
|
||||
// Call and expect error
|
||||
await expect(getDatasource('test-id')).rejects.toThrow('Validation failed');
|
||||
expect(mainApi.get).toHaveBeenCalledWith('/data_sources/test-id');
|
||||
expect(v.parse).toHaveBeenCalled();
|
||||
expect(DataSourceSchema.parse).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,7 +10,6 @@ import type {
|
|||
SQLServerCredentials
|
||||
} from '@/api/asset_interfaces/datasources';
|
||||
import { DataSourceSchema } from '@/api/asset_interfaces/datasources';
|
||||
import * as v from 'valibot';
|
||||
import mainApi from '../instances';
|
||||
import { serverFetch } from '@/api/createServerInstance';
|
||||
|
||||
|
@ -21,13 +20,13 @@ export const listDatasources = async () => {
|
|||
export const getDatasource = async (id: string) => {
|
||||
return await mainApi
|
||||
.get<DataSource>(`/data_sources/${id}`)
|
||||
.then((res) => v.parse(DataSourceSchema, res.data));
|
||||
.then((res) => DataSourceSchema.parse(res.data));
|
||||
};
|
||||
|
||||
export const getDatasource_server = async (id: string) => {
|
||||
const response = await serverFetch<DataSource>(`/data_sources/${id}`);
|
||||
// Validate response with DataSourceSchema
|
||||
return v.parse(DataSourceSchema, response);
|
||||
return DataSourceSchema.parse(response);
|
||||
};
|
||||
|
||||
export const deleteDatasource = async (id: string) => {
|
||||
|
|
Loading…
Reference in New Issue