diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b06544..14e9cee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] jobs: lint: @@ -28,7 +28,13 @@ jobs: uses: actions/setup-node@v6 with: node-version: 24 - cache: 'pnpm' + cache: "pnpm" + + - name: Prepare embed directory + run: mkdir -p frontend/dist && touch frontend/dist/.keep + + - name: generate openapi schema + run: go run ./cmd/schema/main.go - name: Run go generate run: go generate ./... @@ -85,7 +91,7 @@ jobs: uses: actions/setup-node@v6 with: node-version: 24 - cache: 'pnpm' + cache: "pnpm" - name: Run go generate run: go generate ./... diff --git a/.gitignore b/.gitignore index 86c0c6a..cb7c644 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ TESTING_PLAN.md # this thiing changes too often frontend/openapi.json +frontend/src/client \ No newline at end of file diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 7140e3c..523da05 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -3,6 +3,7 @@ import globals from "globals"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; import tseslint from "typescript-eslint"; +import unusedImports from "eslint-plugin-unused-imports"; // 👈 add this import { defineConfig, globalIgnores } from "eslint/config"; export default defineConfig([ @@ -15,6 +16,12 @@ export default defineConfig([ reactHooks.configs.flat.recommended, reactRefresh.configs.vite, ], + plugins: { + "unused-imports": unusedImports, + }, + rules: { + "unused-imports/no-unused-imports": "error", + }, languageOptions: { ecmaVersion: 2020, globals: globals.browser, diff --git a/frontend/package.json b/frontend/package.json index 32d0858..0020f45 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,10 +5,10 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc -b && vite build", + "build": "vite build && tsc -b", "lint": "eslint .", "preview": "vite preview", - "generate:client": "openapi-ts --input http://localhost:8000/openapi.json --output src/client", + "generate:client": "openapi-ts --input ./openapi.json --output src/client", "format": "prettier --write .", "format:check": "prettier --check ." }, @@ -43,6 +43,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.17", + "vaul": "^1.1.2", "zod": "^4.2.0", "zustand": "^5.0.9" }, @@ -57,6 +58,7 @@ "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", + "eslint-plugin-unused-imports": "^4.4.1", "globals": "^16.5.0", "husky": "^9.1.7", "lint-staged": "^16.4.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 8edf822..cb64565 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -98,6 +98,9 @@ importers: tailwindcss: specifier: ^4.1.17 version: 4.1.17 + vaul: + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) zod: specifier: ^4.2.0 version: 4.2.0 @@ -135,6 +138,9 @@ importers: eslint-plugin-react-refresh: specifier: ^0.4.24 version: 0.4.24(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-unused-imports: + specifier: ^4.4.1 + version: 4.4.1(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) globals: specifier: ^16.5.0 version: 16.5.0 @@ -1933,6 +1939,15 @@ packages: peerDependencies: eslint: '>=8.40' + eslint-plugin-unused-imports@4.4.1: + resolution: {integrity: sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0 + eslint: ^10.0.0 || ^9.0.0 || ^8.0.0 + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2749,6 +2764,12 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vaul@1.1.2: + resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + vite@7.2.4: resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4581,6 +4602,12 @@ snapshots: dependencies: eslint: 9.39.1(jiti@2.6.1) + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): + dependencies: + eslint: 9.39.1(jiti@2.6.1) + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 @@ -5357,6 +5384,15 @@ snapshots: dependencies: react: 19.2.0 + vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3): dependencies: esbuild: 0.25.12 diff --git a/frontend/src/client/@tanstack/react-query.gen.ts b/frontend/src/client/@tanstack/react-query.gen.ts deleted file mode 100644 index 7ae6f47..0000000 --- a/frontend/src/client/@tanstack/react-query.gen.ts +++ /dev/null @@ -1,361 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/react-query'; - -import { client } from '../client.gen'; -import { createNewTable, deleteRow, deleteTable, insertOrUpdateRow, listColumns, listHistory, listRecentHistory, listRows, listTables, newTableFormFileds, type Options, rowInsertOrUpdateForm, runSqlQuery } from '../sdk.gen'; -import type { CreateNewTableData, CreateNewTableError, CreateNewTableResponse, DeleteRowData, DeleteRowError, DeleteRowResponse, DeleteTableData, DeleteTableError, DeleteTableResponse, InsertOrUpdateRowData, InsertOrUpdateRowError, InsertOrUpdateRowResponse, ListColumnsData, ListColumnsError, ListColumnsResponse, ListHistoryData, ListHistoryError, ListHistoryResponse, ListRecentHistoryData, ListRecentHistoryError, ListRecentHistoryResponse, ListRowsData, ListRowsError, ListRowsResponse2, ListTablesData, ListTablesError, ListTablesResponse, NewTableFormFiledsData, NewTableFormFiledsError, NewTableFormFiledsResponse, RowInsertOrUpdateFormData, RowInsertOrUpdateFormError, RowInsertOrUpdateFormResponse, RunSqlQueryData, RunSqlQueryError, RunSqlQueryResponse } from '../types.gen'; - -export type QueryKey = [ - Pick & { - _id: string; - _infinite?: boolean; - tags?: ReadonlyArray; - } -]; - -const createQueryKey = (id: string, options?: TOptions, infinite?: boolean, tags?: ReadonlyArray): [ - QueryKey[0] -] => { - const params: QueryKey[0] = { _id: id, baseUrl: options?.baseUrl || (options?.client ?? client).getConfig().baseUrl } as QueryKey[0]; - if (infinite) { - params._infinite = infinite; - } - if (tags) { - params.tags = tags; - } - if (options?.body) { - params.body = options.body; - } - if (options?.headers) { - params.headers = options.headers; - } - if (options?.path) { - params.path = options.path; - } - if (options?.query) { - params.query = options.query; - } - return [params]; -}; - -export const listHistoryQueryKey = (options?: Options) => createQueryKey('listHistory', options); - -/** - * List query history - */ -export const listHistoryOptions = (options?: Options) => queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await listHistory({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }); - return data; - }, - queryKey: listHistoryQueryKey(options) -}); - -const createInfiniteParams = [0], 'body' | 'headers' | 'path' | 'query'>>(queryKey: QueryKey, page: K) => { - const params = { ...queryKey[0] }; - if (page.body) { - params.body = { - ...queryKey[0].body as any, - ...page.body as any - }; - } - if (page.headers) { - params.headers = { - ...queryKey[0].headers, - ...page.headers - }; - } - if (page.path) { - params.path = { - ...queryKey[0].path as any, - ...page.path as any - }; - } - if (page.query) { - params.query = { - ...queryKey[0].query as any, - ...page.query as any - }; - } - return params as unknown as typeof page; -}; - -export const listHistoryInfiniteQueryKey = (options?: Options): QueryKey> => createQueryKey('listHistory', options, true); - -/** - * List query history - */ -export const listHistoryInfiniteOptions = (options?: Options) => infiniteQueryOptions, QueryKey>, number | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>( -// @ts-ignore -{ - queryFn: async ({ pageParam, queryKey, signal }) => { - // @ts-ignore - const page: Pick>[0], 'body' | 'headers' | 'path' | 'query'> = typeof pageParam === 'object' ? pageParam : { - query: { - page: pageParam - } - }; - const params = createInfiniteParams(queryKey, page); - const { data } = await listHistory({ - ...options, - ...params, - signal, - throwOnError: true - }); - return data; - }, - queryKey: listHistoryInfiniteQueryKey(options) -}); - -export const listRecentHistoryQueryKey = (options?: Options) => createQueryKey('listRecentHistory', options); - -/** - * List recent history - */ -export const listRecentHistoryOptions = (options?: Options) => queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await listRecentHistory({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }); - return data; - }, - queryKey: listRecentHistoryQueryKey(options) -}); - -/** - * Run SQL query - */ -export const runSqlQueryMutation = (options?: Partial>): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await runSqlQuery({ - ...options, - ...fnOptions, - throwOnError: true - }); - return data; - } - }; - return mutationOptions; -}; - -/** - * Delete a table - */ -export const deleteTableMutation = (options?: Partial>): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await deleteTable({ - ...options, - ...fnOptions, - throwOnError: true - }); - return data; - } - }; - return mutationOptions; -}; - -export const listTablesQueryKey = (options?: Options) => createQueryKey('listTables', options); - -/** - * List of all tables - */ -export const listTablesOptions = (options?: Options) => queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await listTables({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }); - return data; - }, - queryKey: listTablesQueryKey(options) -}); - -export const newTableFormFiledsQueryKey = (options?: Options) => createQueryKey('newTableFormFileds', options); - -/** - * Get data types for new table form - */ -export const newTableFormFiledsOptions = (options?: Options) => queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await newTableFormFileds({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }); - return data; - }, - queryKey: newTableFormFiledsQueryKey(options) -}); - -/** - * Create a new table - */ -export const createNewTableMutation = (options?: Partial>): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await createNewTable({ - ...options, - ...fnOptions, - throwOnError: true - }); - return data; - } - }; - return mutationOptions; -}; - -export const listRowsQueryKey = (options: Options) => createQueryKey('listRows', options); - -/** - * List rows of a table - */ -export const listRowsOptions = (options: Options) => queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await listRows({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }); - return data; - }, - queryKey: listRowsQueryKey(options) -}); - -export const listRowsInfiniteQueryKey = (options: Options): QueryKey> => createQueryKey('listRows', options, true); - -/** - * List rows of a table - */ -export const listRowsInfiniteOptions = (options: Options) => infiniteQueryOptions, QueryKey>, number | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>( -// @ts-ignore -{ - queryFn: async ({ pageParam, queryKey, signal }) => { - // @ts-ignore - const page: Pick>[0], 'body' | 'headers' | 'path' | 'query'> = typeof pageParam === 'object' ? pageParam : { - query: { - page: pageParam - } - }; - const params = createInfiniteParams(queryKey, page); - const { data } = await listRows({ - ...options, - ...params, - signal, - throwOnError: true - }); - return data; - }, - queryKey: listRowsInfiniteQueryKey(options) -}); - -export const listColumnsQueryKey = (options: Options) => createQueryKey('listColumns', options); - -/** - * List columns of a table - */ -export const listColumnsOptions = (options: Options) => queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await listColumns({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }); - return data; - }, - queryKey: listColumnsQueryKey(options) -}); - -export const rowInsertOrUpdateFormQueryKey = (options: Options) => createQueryKey('rowInsertOrUpdateForm', options); - -/** - * Get row insert/update form metadata - */ -export const rowInsertOrUpdateFormOptions = (options: Options) => queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await rowInsertOrUpdateForm({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }); - return data; - }, - queryKey: rowInsertOrUpdateFormQueryKey(options) -}); - -export const rowInsertOrUpdateFormInfiniteQueryKey = (options: Options): QueryKey> => createQueryKey('rowInsertOrUpdateForm', options, true); - -/** - * Get row insert/update form metadata - */ -export const rowInsertOrUpdateFormInfiniteOptions = (options: Options) => infiniteQueryOptions, QueryKey>, number | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>( -// @ts-ignore -{ - queryFn: async ({ pageParam, queryKey, signal }) => { - // @ts-ignore - const page: Pick>[0], 'body' | 'headers' | 'path' | 'query'> = typeof pageParam === 'object' ? pageParam : { - query: { - page: pageParam - } - }; - const params = createInfiniteParams(queryKey, page); - const { data } = await rowInsertOrUpdateForm({ - ...options, - ...params, - signal, - throwOnError: true - }); - return data; - }, - queryKey: rowInsertOrUpdateFormInfiniteQueryKey(options) -}); - -/** - * Insert or update a row - */ -export const insertOrUpdateRowMutation = (options?: Partial>): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await insertOrUpdateRow({ - ...options, - ...fnOptions, - throwOnError: true - }); - return data; - } - }; - return mutationOptions; -}; - -/** - * Delete a row - */ -export const deleteRowMutation = (options?: Partial>): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await deleteRow({ - ...options, - ...fnOptions, - throwOnError: true - }); - return data; - } - }; - return mutationOptions; -}; diff --git a/frontend/src/client/client.gen.ts b/frontend/src/client/client.gen.ts deleted file mode 100644 index cab3c70..0000000 --- a/frontend/src/client/client.gen.ts +++ /dev/null @@ -1,16 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { type ClientOptions, type Config, createClient, createConfig } from './client'; -import type { ClientOptions as ClientOptions2 } from './types.gen'; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = (override?: Config) => Config & T>; - -export const client = createClient(createConfig()); diff --git a/frontend/src/client/client/client.gen.ts b/frontend/src/client/client/client.gen.ts deleted file mode 100644 index 9ec9ad8..0000000 --- a/frontend/src/client/client/client.gen.ts +++ /dev/null @@ -1,298 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { createSseClient } from '../core/serverSentEvents.gen'; -import type { HttpMethod } from '../core/types.gen'; -import { getValidRequestBody } from '../core/utils.gen'; -import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils.gen'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors(); - - const beforeRequest = async < - TData = unknown, - TResponseStyle extends 'data' | 'fields' = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, - >( - options: RequestOptions, - ) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - serializedBody: undefined as string | undefined, - }; - - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); - } - - if (opts.requestValidator) { - await opts.requestValidator(opts); - } - - if (opts.body !== undefined && opts.bodySerializer) { - opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.serializedBody === '') { - opts.headers.delete('Content-Type'); - } - - const resolvedOpts = opts as typeof opts & - ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - - return { opts: resolvedOpts, url }; - }; - - const request: Client['request'] = async (options) => { - const { opts, url } = await beforeRequest(options); - const requestInit: ReqInit = { - redirect: 'follow', - ...opts, - body: getValidRequestBody(opts), - }; - - let request = new Request(url, requestInit); - - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - let response: Response; - - try { - response = await _fetch(request); - } catch (error) { - // Handle fetch exceptions (AbortError, network errors, etc.) - let finalError = error; - - for (const fn of interceptors.error.fns) { - if (fn) { - finalError = (await fn(error, undefined as any, request, opts)) as unknown; - } - } - - finalError = finalError || ({} as unknown); - - if (opts.throwOnError) { - throw finalError; - } - - // Return error response - return opts.responseStyle === 'data' - ? undefined - : { - error: finalError, - request, - response: undefined as any, - }; - } - - for (const fn of interceptors.response.fns) { - if (fn) { - response = await fn(response, request, opts); - } - } - - const result = { - request, - response, - }; - - if (response.ok) { - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - if (response.status === 204 || response.headers.get('Content-Length') === '0') { - let emptyData: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'text': - emptyData = await response[parseAs](); - break; - case 'formData': - emptyData = new FormData(); - break; - case 'stream': - emptyData = response.body; - break; - case 'json': - default: - emptyData = {}; - break; - } - return opts.responseStyle === 'data' - ? emptyData - : { - data: emptyData, - ...result, - }; - } - - let data: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'formData': - case 'text': - data = await response[parseAs](); - break; - case 'json': { - // Some servers return 200 with no Content-Length and empty body. - // response.json() would throw; read as text and parse if non-empty. - const text = await response.text(); - data = text ? JSON.parse(text) : {}; - break; - } - case 'stream': - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result, - }; - } - - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result, - }; - } - - const textError = await response.text(); - let jsonError: unknown; - - try { - jsonError = JSON.parse(textError); - } catch { - // noop - } - - const error = jsonError ?? textError; - let finalError = error; - - for (const fn of interceptors.error.fns) { - if (fn) { - finalError = (await fn(error, response, request, opts)) as string; - } - } - - finalError = finalError || ({} as string); - - if (opts.throwOnError) { - throw finalError; - } - - // TODO: we probably want to return error and improve types - return opts.responseStyle === 'data' - ? undefined - : { - error: finalError, - ...result, - }; - }; - - const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => - request({ ...options, method }); - - const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { - const { opts, url } = await beforeRequest(options); - return createSseClient({ - ...opts, - body: opts.body as BodyInit | null | undefined, - headers: opts.headers as unknown as Record, - method, - onRequest: async (url, init) => { - let request = new Request(url, init); - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - return request; - }, - serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, - url, - }); - }; - - const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); - - return { - buildUrl: _buildUrl, - connect: makeMethodFn('CONNECT'), - delete: makeMethodFn('DELETE'), - get: makeMethodFn('GET'), - getConfig, - head: makeMethodFn('HEAD'), - interceptors, - options: makeMethodFn('OPTIONS'), - patch: makeMethodFn('PATCH'), - post: makeMethodFn('POST'), - put: makeMethodFn('PUT'), - request, - setConfig, - sse: { - connect: makeSseFn('CONNECT'), - delete: makeSseFn('DELETE'), - get: makeSseFn('GET'), - head: makeSseFn('HEAD'), - options: makeSseFn('OPTIONS'), - patch: makeSseFn('PATCH'), - post: makeSseFn('POST'), - put: makeSseFn('PUT'), - trace: makeSseFn('TRACE'), - }, - trace: makeMethodFn('TRACE'), - } as Client; -}; diff --git a/frontend/src/client/client/index.ts b/frontend/src/client/client/index.ts deleted file mode 100644 index b295ede..0000000 --- a/frontend/src/client/client/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type { Auth } from '../core/auth.gen'; -export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -export { - formDataBodySerializer, - jsonBodySerializer, - urlSearchParamsBodySerializer, -} from '../core/bodySerializer.gen'; -export { buildClientParams } from '../core/params.gen'; -export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; -export { createClient } from './client.gen'; -export type { - Client, - ClientOptions, - Config, - CreateClientConfig, - Options, - RequestOptions, - RequestResult, - ResolvedRequestOptions, - ResponseStyle, - TDataShape, -} from './types.gen'; -export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/frontend/src/client/client/types.gen.ts b/frontend/src/client/client/types.gen.ts deleted file mode 100644 index 9813eea..0000000 --- a/frontend/src/client/client/types.gen.ts +++ /dev/null @@ -1,214 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth } from '../core/auth.gen'; -import type { - ServerSentEventsOptions, - ServerSentEventsResult, -} from '../core/serverSentEvents.gen'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; -import type { Middleware } from './utils.gen'; - -export type ResponseStyle = 'data' | 'fields'; - -export interface Config - extends Omit, CoreConfig { - /** - * Base URL for all requests made by this client. - */ - baseUrl?: T['baseUrl']; - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Please don't use the Fetch client for Next.js applications. The `next` - * options won't have any effect. - * - * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. - */ - next?: never; - /** - * Return the response data parsed in a specified format. By default, `auto` - * will infer the appropriate method from the `Content-Type` response header. - * You can override this behavior with any of the {@link Body} methods. - * Select `stream` if you don't want to parse response data at all. - * - * @default 'auto' - */ - parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; - /** - * Should we return only data or multiple fields (data, error, response, etc.)? - * - * @default 'fields' - */ - responseStyle?: ResponseStyle; - /** - * Throw an error instead of returning it in the response? - * - * @default false - */ - throwOnError?: T['throwOnError']; -} - -export interface RequestOptions< - TData = unknown, - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> - extends - Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }>, - Pick< - ServerSentEventsOptions, - | 'onRequest' - | 'onSseError' - | 'onSseEvent' - | 'sseDefaultRetryDelay' - | 'sseMaxRetryAttempts' - | 'sseMaxRetryDelay' - > { - /** - * Any body that you want to add to your request. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} - */ - body?: unknown; - path?: Record; - query?: Record; - /** - * Security mechanism(s) to use for the request. - */ - security?: ReadonlyArray; - url: Url; -} - -export interface ResolvedRequestOptions< - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends RequestOptions { - serializedBody?: string; -} - -export type RequestResult< - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = ThrowOnError extends true - ? Promise< - TResponseStyle extends 'data' - ? TData extends Record - ? TData[keyof TData] - : TData - : { - data: TData extends Record ? TData[keyof TData] : TData; - request: Request; - response: Response; - } - > - : Promise< - TResponseStyle extends 'data' - ? (TData extends Record ? TData[keyof TData] : TData) | undefined - : ( - | { - data: TData extends Record ? TData[keyof TData] : TData; - error: undefined; - } - | { - data: undefined; - error: TError extends Record ? TError[keyof TError] : TError; - } - ) & { - request: Request; - response: Response; - } - >; - -export interface ClientOptions { - baseUrl?: string; - responseStyle?: ResponseStyle; - throwOnError?: boolean; -} - -type MethodFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => RequestResult; - -type SseFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => Promise>; - -type RequestFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'> & - Pick>, 'method'>, -) => RequestResult; - -type BuildUrlFn = < - TData extends { - body?: unknown; - path?: Record; - query?: Record; - url: string; - }, ->( - options: TData & Options, -) => string; - -export type Client = CoreClient & { - interceptors: Middleware; -}; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = ( - override?: Config, -) => Config & T>; - -export interface TDataShape { - body?: unknown; - headers?: unknown; - path?: unknown; - query?: unknown; - url: string; -} - -type OmitKeys = Pick>; - -export type Options< - TData extends TDataShape = TDataShape, - ThrowOnError extends boolean = boolean, - TResponse = unknown, - TResponseStyle extends ResponseStyle = 'fields', -> = OmitKeys< - RequestOptions, - 'body' | 'path' | 'query' | 'url' -> & - ([TData] extends [never] ? unknown : Omit); diff --git a/frontend/src/client/client/utils.gen.ts b/frontend/src/client/client/utils.gen.ts deleted file mode 100644 index 5162192..0000000 --- a/frontend/src/client/client/utils.gen.ts +++ /dev/null @@ -1,316 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { getAuthToken } from '../core/auth.gen'; -import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -import { jsonBodySerializer } from '../core/bodySerializer.gen'; -import { - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from '../core/pathSerializer.gen'; -import { getUrl } from '../core/utils.gen'; -import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; - -export const createQuerySerializer = ({ - parameters = {}, - ...args -}: QuerySerializerOptions = {}) => { - const querySerializer = (queryParams: T) => { - const search: string[] = []; - if (queryParams && typeof queryParams === 'object') { - for (const name in queryParams) { - const value = queryParams[name]; - - if (value === undefined || value === null) { - continue; - } - - const options = parameters[name] || args; - - if (Array.isArray(value)) { - const serializedArray = serializeArrayParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: 'form', - value, - ...options.array, - }); - if (serializedArray) search.push(serializedArray); - } else if (typeof value === 'object') { - const serializedObject = serializeObjectParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: 'deepObject', - value: value as Record, - ...options.object, - }); - if (serializedObject) search.push(serializedObject); - } else { - const serializedPrimitive = serializePrimitiveParam({ - allowReserved: options.allowReserved, - name, - value: value as string, - }); - if (serializedPrimitive) search.push(serializedPrimitive); - } - } - } - return search.join('&'); - }; - return querySerializer; -}; - -/** - * Infers parseAs value from provided Content-Type header. - */ -export const getParseAs = (contentType: string | null): Exclude => { - if (!contentType) { - // If no Content-Type header is provided, the best we can do is return the raw response body, - // which is effectively the same as the 'stream' option. - return 'stream'; - } - - const cleanContent = contentType.split(';')[0]?.trim(); - - if (!cleanContent) { - return; - } - - if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { - return 'json'; - } - - if (cleanContent === 'multipart/form-data') { - return 'formData'; - } - - if ( - ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) - ) { - return 'blob'; - } - - if (cleanContent.startsWith('text/')) { - return 'text'; - } - - return; -}; - -const checkForExistence = ( - options: Pick & { - headers: Headers; - }, - name?: string, -): boolean => { - if (!name) { - return false; - } - if ( - options.headers.has(name) || - options.query?.[name] || - options.headers.get('Cookie')?.includes(`${name}=`) - ) { - return true; - } - return false; -}; - -export const setAuthParams = async ({ - security, - ...options -}: Pick, 'security'> & - Pick & { - headers: Headers; - }) => { - for (const auth of security) { - if (checkForExistence(options, auth.name)) { - continue; - } - - const token = await getAuthToken(auth, options.auth); - - if (!token) { - continue; - } - - const name = auth.name ?? 'Authorization'; - - switch (auth.in) { - case 'query': - if (!options.query) { - options.query = {}; - } - options.query[name] = token; - break; - case 'cookie': - options.headers.append('Cookie', `${name}=${token}`); - break; - case 'header': - default: - options.headers.set(name, token); - break; - } - } -}; - -export const buildUrl: Client['buildUrl'] = (options) => - getUrl({ - baseUrl: options.baseUrl as string, - path: options.path, - query: options.query, - querySerializer: - typeof options.querySerializer === 'function' - ? options.querySerializer - : createQuerySerializer(options.querySerializer), - url: options.url, - }); - -export const mergeConfigs = (a: Config, b: Config): Config => { - const config = { ...a, ...b }; - if (config.baseUrl?.endsWith('/')) { - config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); - } - config.headers = mergeHeaders(a.headers, b.headers); - return config; -}; - -const headersEntries = (headers: Headers): Array<[string, string]> => { - const entries: Array<[string, string]> = []; - headers.forEach((value, key) => { - entries.push([key, value]); - }); - return entries; -}; - -export const mergeHeaders = ( - ...headers: Array['headers'] | undefined> -): Headers => { - const mergedHeaders = new Headers(); - for (const header of headers) { - if (!header) { - continue; - } - - const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); - - for (const [key, value] of iterator) { - if (value === null) { - mergedHeaders.delete(key); - } else if (Array.isArray(value)) { - for (const v of value) { - mergedHeaders.append(key, v as string); - } - } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e., their - // content value in OpenAPI specification is 'application/json' - mergedHeaders.set( - key, - typeof value === 'object' ? JSON.stringify(value) : (value as string), - ); - } - } - } - return mergedHeaders; -}; - -type ErrInterceptor = ( - error: Err, - response: Res, - request: Req, - options: Options, -) => Err | Promise; - -type ReqInterceptor = (request: Req, options: Options) => Req | Promise; - -type ResInterceptor = ( - response: Res, - request: Req, - options: Options, -) => Res | Promise; - -class Interceptors { - fns: Array = []; - - clear(): void { - this.fns = []; - } - - eject(id: number | Interceptor): void { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = null; - } - } - - exists(id: number | Interceptor): boolean { - const index = this.getInterceptorIndex(id); - return Boolean(this.fns[index]); - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this.fns[id] ? id : -1; - } - return this.fns.indexOf(id); - } - - update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = fn; - return id; - } - return false; - } - - use(fn: Interceptor): number { - this.fns.push(fn); - return this.fns.length - 1; - } -} - -export interface Middleware { - error: Interceptors>; - request: Interceptors>; - response: Interceptors>; -} - -export const createInterceptors = (): Middleware< - Req, - Res, - Err, - Options -> => ({ - error: new Interceptors>(), - request: new Interceptors>(), - response: new Interceptors>(), -}); - -const defaultQuerySerializer = createQuerySerializer({ - allowReserved: false, - array: { - explode: true, - style: 'form', - }, - object: { - explode: true, - style: 'deepObject', - }, -}); - -const defaultHeaders = { - 'Content-Type': 'application/json', -}; - -export const createConfig = ( - override: Config & T> = {}, -): Config & T> => ({ - ...jsonBodySerializer, - headers: defaultHeaders, - parseAs: 'auto', - querySerializer: defaultQuerySerializer, - ...override, -}); diff --git a/frontend/src/client/core/auth.gen.ts b/frontend/src/client/core/auth.gen.ts deleted file mode 100644 index 3ebf994..0000000 --- a/frontend/src/client/core/auth.gen.ts +++ /dev/null @@ -1,41 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type AuthToken = string | undefined; - -export interface Auth { - /** - * Which part of the request do we use to send the auth? - * - * @default 'header' - */ - in?: 'header' | 'query' | 'cookie'; - /** - * Header or query parameter name. - * - * @default 'Authorization' - */ - name?: string; - scheme?: 'basic' | 'bearer'; - type: 'apiKey' | 'http'; -} - -export const getAuthToken = async ( - auth: Auth, - callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, -): Promise => { - const token = typeof callback === 'function' ? await callback(auth) : callback; - - if (!token) { - return; - } - - if (auth.scheme === 'bearer') { - return `Bearer ${token}`; - } - - if (auth.scheme === 'basic') { - return `Basic ${btoa(token)}`; - } - - return token; -}; diff --git a/frontend/src/client/core/bodySerializer.gen.ts b/frontend/src/client/core/bodySerializer.gen.ts deleted file mode 100644 index 67daca6..0000000 --- a/frontend/src/client/core/bodySerializer.gen.ts +++ /dev/null @@ -1,82 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; - -export type QuerySerializer = (query: Record) => string; - -export type BodySerializer = (body: unknown) => unknown; - -type QuerySerializerOptionsObject = { - allowReserved?: boolean; - array?: Partial>; - object?: Partial>; -}; - -export type QuerySerializerOptions = QuerySerializerOptionsObject & { - /** - * Per-parameter serialization overrides. When provided, these settings - * override the global array/object settings for specific parameter names. - */ - parameters?: Record; -}; - -const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { - if (typeof value === 'string' || value instanceof Blob) { - data.append(key, value); - } else if (value instanceof Date) { - data.append(key, value.toISOString()); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { - if (typeof value === 'string') { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -export const formDataBodySerializer = { - bodySerializer: (body: unknown): FormData => { - const data = new FormData(); - - Object.entries(body as Record).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeFormDataPair(data, key, v)); - } else { - serializeFormDataPair(data, key, value); - } - }); - - return data; - }, -}; - -export const jsonBodySerializer = { - bodySerializer: (body: unknown): string => - JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), -}; - -export const urlSearchParamsBodySerializer = { - bodySerializer: (body: unknown): string => { - const data = new URLSearchParams(); - - Object.entries(body as Record).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); - } else { - serializeUrlSearchParamsPair(data, key, value); - } - }); - - return data.toString(); - }, -}; diff --git a/frontend/src/client/core/params.gen.ts b/frontend/src/client/core/params.gen.ts deleted file mode 100644 index 7955601..0000000 --- a/frontend/src/client/core/params.gen.ts +++ /dev/null @@ -1,169 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -type Slot = 'body' | 'headers' | 'path' | 'query'; - -export type Field = - | { - in: Exclude; - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If omitted, we use the same value as `key`. - */ - map?: string; - } - | { - in: Extract; - /** - * Key isn't required for bodies. - */ - key?: string; - map?: string; - } - | { - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If `in` is omitted, `map` aliases `key` to the transport layer. - */ - map: Slot; - }; - -export interface Fields { - allowExtra?: Partial>; - args?: ReadonlyArray; -} - -export type FieldsConfig = ReadonlyArray; - -const extraPrefixesMap: Record = { - $body_: 'body', - $headers_: 'headers', - $path_: 'path', - $query_: 'query', -}; -const extraPrefixes = Object.entries(extraPrefixesMap); - -type KeyMap = Map< - string, - | { - in: Slot; - map?: string; - } - | { - in?: never; - map: Slot; - } ->; - -const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { - if (!map) { - map = new Map(); - } - - for (const config of fields) { - if ('in' in config) { - if (config.key) { - map.set(config.key, { - in: config.in, - map: config.map, - }); - } - } else if ('key' in config) { - map.set(config.key, { - map: config.map, - }); - } else if (config.args) { - buildKeyMap(config.args, map); - } - } - - return map; -}; - -interface Params { - body: unknown; - headers: Record; - path: Record; - query: Record; -} - -const stripEmptySlots = (params: Params) => { - for (const [slot, value] of Object.entries(params)) { - if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { - delete params[slot as Slot]; - } - } -}; - -export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { - const params: Params = { - body: {}, - headers: {}, - path: {}, - query: {}, - }; - - const map = buildKeyMap(fields); - - let config: FieldsConfig[number] | undefined; - - for (const [index, arg] of args.entries()) { - if (fields[index]) { - config = fields[index]; - } - - if (!config) { - continue; - } - - if ('in' in config) { - if (config.key) { - const field = map.get(config.key)!; - const name = field.map || config.key; - if (field.in) { - (params[field.in] as Record)[name] = arg; - } - } else { - params.body = arg; - } - } else { - for (const [key, value] of Object.entries(arg ?? {})) { - const field = map.get(key); - - if (field) { - if (field.in) { - const name = field.map || key; - (params[field.in] as Record)[name] = value; - } else { - params[field.map] = value; - } - } else { - const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); - - if (extra) { - const [prefix, slot] = extra; - (params[slot] as Record)[key.slice(prefix.length)] = value; - } else if ('allowExtra' in config && config.allowExtra) { - for (const [slot, allowed] of Object.entries(config.allowExtra)) { - if (allowed) { - (params[slot as Slot] as Record)[key] = value; - break; - } - } - } - } - } - } - } - - stripEmptySlots(params); - - return params; -}; diff --git a/frontend/src/client/core/pathSerializer.gen.ts b/frontend/src/client/core/pathSerializer.gen.ts deleted file mode 100644 index 994b284..0000000 --- a/frontend/src/client/core/pathSerializer.gen.ts +++ /dev/null @@ -1,171 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} - -interface SerializePrimitiveOptions { - allowReserved?: boolean; - name: string; -} - -export interface SerializerOptions { - /** - * @default true - */ - explode: boolean; - style: T; -} - -export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -export type ObjectStyle = 'form' | 'deepObject'; -type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; - -interface SerializePrimitiveParam extends SerializePrimitiveOptions { - value: string; -} - -export const separatorArrayExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'form': - return ','; - case 'pipeDelimited': - return '|'; - case 'spaceDelimited': - return '%20'; - default: - return ','; - } -}; - -export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const serializeArrayParam = ({ - allowReserved, - explode, - name, - style, - value, -}: SerializeOptions & { - value: unknown[]; -}) => { - if (!explode) { - const joinedValues = ( - allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) - ).join(separatorArrayNoExplode(style)); - switch (style) { - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - case 'simple': - return joinedValues; - default: - return `${name}=${joinedValues}`; - } - } - - const separator = separatorArrayExplode(style); - const joinedValues = value - .map((v) => { - if (style === 'label' || style === 'simple') { - return allowReserved ? v : encodeURIComponent(v as string); - } - - return serializePrimitiveParam({ - allowReserved, - name, - value: v as string, - }); - }) - .join(separator); - return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; -}; - -export const serializePrimitiveParam = ({ - allowReserved, - name, - value, -}: SerializePrimitiveParam) => { - if (value === undefined || value === null) { - return ''; - } - - if (typeof value === 'object') { - throw new Error( - 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', - ); - } - - return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; -}; - -export const serializeObjectParam = ({ - allowReserved, - explode, - name, - style, - value, - valueOnly, -}: SerializeOptions & { - value: Record | Date; - valueOnly?: boolean; -}) => { - if (value instanceof Date) { - return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; - } - - if (style !== 'deepObject' && !explode) { - let values: string[] = []; - Object.entries(value).forEach(([key, v]) => { - values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; - }); - const joinedValues = values.join(','); - switch (style) { - case 'form': - return `${name}=${joinedValues}`; - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - default: - return joinedValues; - } - } - - const separator = separatorObjectExplode(style); - const joinedValues = Object.entries(value) - .map(([key, v]) => - serializePrimitiveParam({ - allowReserved, - name: style === 'deepObject' ? `${name}[${key}]` : key, - value: v as string, - }), - ) - .join(separator); - return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; -}; diff --git a/frontend/src/client/core/queryKeySerializer.gen.ts b/frontend/src/client/core/queryKeySerializer.gen.ts deleted file mode 100644 index 5000df6..0000000 --- a/frontend/src/client/core/queryKeySerializer.gen.ts +++ /dev/null @@ -1,117 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -/** - * JSON-friendly union that mirrors what Pinia Colada can hash. - */ -export type JsonValue = - | null - | string - | number - | boolean - | JsonValue[] - | { [key: string]: JsonValue }; - -/** - * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. - */ -export const queryKeyJsonReplacer = (_key: string, value: unknown) => { - if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { - return undefined; - } - if (typeof value === 'bigint') { - return value.toString(); - } - if (value instanceof Date) { - return value.toISOString(); - } - return value; -}; - -/** - * Safely stringifies a value and parses it back into a JsonValue. - */ -export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { - try { - const json = JSON.stringify(input, queryKeyJsonReplacer); - if (json === undefined) { - return undefined; - } - return JSON.parse(json) as JsonValue; - } catch { - return undefined; - } -}; - -/** - * Detects plain objects (including objects with a null prototype). - */ -const isPlainObject = (value: unknown): value is Record => { - if (value === null || typeof value !== 'object') { - return false; - } - const prototype = Object.getPrototypeOf(value as object); - return prototype === Object.prototype || prototype === null; -}; - -/** - * Turns URLSearchParams into a sorted JSON object for deterministic keys. - */ -const serializeSearchParams = (params: URLSearchParams): JsonValue => { - const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); - const result: Record = {}; - - for (const [key, value] of entries) { - const existing = result[key]; - if (existing === undefined) { - result[key] = value; - continue; - } - - if (Array.isArray(existing)) { - (existing as string[]).push(value); - } else { - result[key] = [existing, value]; - } - } - - return result; -}; - -/** - * Normalizes any accepted value into a JSON-friendly shape for query keys. - */ -export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { - if (value === null) { - return null; - } - - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - return value; - } - - if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { - return undefined; - } - - if (typeof value === 'bigint') { - return value.toString(); - } - - if (value instanceof Date) { - return value.toISOString(); - } - - if (Array.isArray(value)) { - return stringifyToJsonValue(value); - } - - if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { - return serializeSearchParams(value); - } - - if (isPlainObject(value)) { - return stringifyToJsonValue(value); - } - - return undefined; -}; diff --git a/frontend/src/client/core/serverSentEvents.gen.ts b/frontend/src/client/core/serverSentEvents.gen.ts deleted file mode 100644 index ddf3c4d..0000000 --- a/frontend/src/client/core/serverSentEvents.gen.ts +++ /dev/null @@ -1,242 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Config } from './types.gen'; - -export type ServerSentEventsOptions = Omit & - Pick & { - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Implementing clients can call request interceptors inside this hook. - */ - onRequest?: (url: string, init: RequestInit) => Promise; - /** - * Callback invoked when a network or parsing error occurs during streaming. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param error The error that occurred. - */ - onSseError?: (error: unknown) => void; - /** - * Callback invoked when an event is streamed from the server. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param event Event streamed from the server. - * @returns Nothing (void). - */ - onSseEvent?: (event: StreamEvent) => void; - serializedBody?: RequestInit['body']; - /** - * Default retry delay in milliseconds. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 3000 - */ - sseDefaultRetryDelay?: number; - /** - * Maximum number of retry attempts before giving up. - */ - sseMaxRetryAttempts?: number; - /** - * Maximum retry delay in milliseconds. - * - * Applies only when exponential backoff is used. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 30000 - */ - sseMaxRetryDelay?: number; - /** - * Optional sleep function for retry backoff. - * - * Defaults to using `setTimeout`. - */ - sseSleepFn?: (ms: number) => Promise; - url: string; - }; - -export interface StreamEvent { - data: TData; - event?: string; - id?: string; - retry?: number; -} - -export type ServerSentEventsResult = { - stream: AsyncGenerator< - TData extends Record ? TData[keyof TData] : TData, - TReturn, - TNext - >; -}; - -export function createSseClient({ - onRequest, - onSseError, - onSseEvent, - responseTransformer, - responseValidator, - sseDefaultRetryDelay, - sseMaxRetryAttempts, - sseMaxRetryDelay, - sseSleepFn, - url, - ...options -}: ServerSentEventsOptions): ServerSentEventsResult { - let lastEventId: string | undefined; - - const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); - - const createStream = async function* () { - let retryDelay: number = sseDefaultRetryDelay ?? 3000; - let attempt = 0; - const signal = options.signal ?? new AbortController().signal; - - while (true) { - if (signal.aborted) break; - - attempt++; - - const headers = - options.headers instanceof Headers - ? options.headers - : new Headers(options.headers as Record | undefined); - - if (lastEventId !== undefined) { - headers.set('Last-Event-ID', lastEventId); - } - - try { - const requestInit: RequestInit = { - redirect: 'follow', - ...options, - body: options.serializedBody, - headers, - signal, - }; - let request = new Request(url, requestInit); - if (onRequest) { - request = await onRequest(url, requestInit); - } - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = options.fetch ?? globalThis.fetch; - const response = await _fetch(request); - - if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); - - if (!response.body) throw new Error('No body in SSE response'); - - const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); - - let buffer = ''; - - const abortHandler = () => { - try { - reader.cancel(); - } catch { - // noop - } - }; - - signal.addEventListener('abort', abortHandler); - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += value; - buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings - - const chunks = buffer.split('\n\n'); - buffer = chunks.pop() ?? ''; - - for (const chunk of chunks) { - const lines = chunk.split('\n'); - const dataLines: Array = []; - let eventName: string | undefined; - - for (const line of lines) { - if (line.startsWith('data:')) { - dataLines.push(line.replace(/^data:\s*/, '')); - } else if (line.startsWith('event:')) { - eventName = line.replace(/^event:\s*/, ''); - } else if (line.startsWith('id:')) { - lastEventId = line.replace(/^id:\s*/, ''); - } else if (line.startsWith('retry:')) { - const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); - if (!Number.isNaN(parsed)) { - retryDelay = parsed; - } - } - } - - let data: unknown; - let parsedJson = false; - - if (dataLines.length) { - const rawData = dataLines.join('\n'); - try { - data = JSON.parse(rawData); - parsedJson = true; - } catch { - data = rawData; - } - } - - if (parsedJson) { - if (responseValidator) { - await responseValidator(data); - } - - if (responseTransformer) { - data = await responseTransformer(data); - } - } - - onSseEvent?.({ - data, - event: eventName, - id: lastEventId, - retry: retryDelay, - }); - - if (dataLines.length) { - yield data as any; - } - } - } - } finally { - signal.removeEventListener('abort', abortHandler); - reader.releaseLock(); - } - - break; // exit loop on normal completion - } catch (error) { - // connection failed or aborted; retry after delay - onSseError?.(error); - - if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { - break; // stop after firing error - } - - // exponential backoff: double retry each attempt, cap at 30s - const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); - await sleep(backoff); - } - } - }; - - const stream = createStream(); - - return { stream }; -} diff --git a/frontend/src/client/core/types.gen.ts b/frontend/src/client/core/types.gen.ts deleted file mode 100644 index 9efe71d..0000000 --- a/frontend/src/client/core/types.gen.ts +++ /dev/null @@ -1,104 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth, AuthToken } from './auth.gen'; -import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; - -export type HttpMethod = - | 'connect' - | 'delete' - | 'get' - | 'head' - | 'options' - | 'patch' - | 'post' - | 'put' - | 'trace'; - -export type Client< - RequestFn = never, - Config = unknown, - MethodFn = never, - BuildUrlFn = never, - SseFn = never, -> = { - /** - * Returns the final request URL. - */ - buildUrl: BuildUrlFn; - getConfig: () => Config; - request: RequestFn; - setConfig: (config: Config) => Config; -} & { - [K in HttpMethod]: MethodFn; -} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); - -export interface Config { - /** - * Auth token or a function returning auth token. The resolved value will be - * added to the request payload as defined by its `security` array. - */ - auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; - /** - * A function for serializing request body parameter. By default, - * {@link JSON.stringify()} will be used. - */ - bodySerializer?: BodySerializer | null; - /** - * An object containing any HTTP headers that you want to pre-populate your - * `Headers` object with. - * - * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} - */ - headers?: - | RequestInit['headers'] - | Record< - string, - string | number | boolean | (string | number | boolean)[] | null | undefined | unknown - >; - /** - * The request method. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} - */ - method?: Uppercase; - /** - * A function for serializing request query parameters. By default, arrays - * will be exploded in form style, objects will be exploded in deepObject - * style, and reserved characters are percent-encoded. - * - * This method will have no effect if the native `paramsSerializer()` Axios - * API function is used. - * - * {@link https://swagger.io/docs/specification/serialization/#query View examples} - */ - querySerializer?: QuerySerializer | QuerySerializerOptions; - /** - * A function validating request data. This is useful if you want to ensure - * the request conforms to the desired shape, so it can be safely sent to - * the server. - */ - requestValidator?: (data: unknown) => Promise; - /** - * A function transforming response data before it's returned. This is useful - * for post-processing data, e.g., converting ISO strings into Date objects. - */ - responseTransformer?: (data: unknown) => Promise; - /** - * A function validating response data. This is useful if you want to ensure - * the response conforms to the desired shape, so it can be safely passed to - * the transformers and returned to the user. - */ - responseValidator?: (data: unknown) => Promise; -} - -type IsExactlyNeverOrNeverUndefined = [T] extends [never] - ? true - : [T] extends [never | undefined] - ? [undefined] extends [T] - ? false - : true - : false; - -export type OmitNever> = { - [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; -}; diff --git a/frontend/src/client/core/utils.gen.ts b/frontend/src/client/core/utils.gen.ts deleted file mode 100644 index 9a4fec7..0000000 --- a/frontend/src/client/core/utils.gen.ts +++ /dev/null @@ -1,140 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; -import { - type ArraySeparatorStyle, - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from './pathSerializer.gen'; - -export interface PathSerializer { - path: Record; - url: string; -} - -export const PATH_PARAM_RE = /\{[^{}]+\}/g; - -export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace(match, serializeArrayParam({ explode, name, style, value })); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; - -export function getValidRequestBody(options: { - body?: unknown; - bodySerializer?: BodySerializer | null; - serializedBody?: unknown; -}) { - const hasBody = options.body !== undefined; - const isSerializedBody = hasBody && options.bodySerializer; - - if (isSerializedBody) { - if ('serializedBody' in options) { - const hasSerializedBody = - options.serializedBody !== undefined && options.serializedBody !== ''; - - return hasSerializedBody ? options.serializedBody : null; - } - - // not all clients implement a serializedBody property (i.e., client-axios) - return options.body !== '' ? options.body : null; - } - - // plain/text body - if (hasBody) { - return options.body; - } - - // no body was provided - return undefined; -} diff --git a/frontend/src/client/index.ts b/frontend/src/client/index.ts deleted file mode 100644 index b576d03..0000000 --- a/frontend/src/client/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export { createNewTable, deleteRow, deleteTable, insertOrUpdateRow, listColumns, listHistory, listRecentHistory, listRows, listTables, newTableFormFileds, type Options, rowInsertOrUpdateForm, runSqlQuery } from './sdk.gen'; -export type { ClientOptions, ColType, ColValue, CreateNewTableData, CreateNewTableError, CreateNewTableErrors, CreateNewTableResponse, CreateNewTableResponses, CreeteNewTableInputBody, CreeteNewTableInputBodyWritable, DeleteRowData, DeleteRowError, DeleteRowErrors, DeleteRowResponse, DeleteRowResponses, DeleteTableData, DeleteTableError, DeleteTableErrors, DeleteTableRequest, DeleteTableRequestWritable, DeleteTableResponse, DeleteTableResponses, ErrorDetail, ErrorModel, ErrorModelWritable, FormDatatype, FormDatatypeWritable, History, InsertOrUpdateRowData, InsertOrUpdateRowError, InsertOrUpdateRowErrors, InsertOrUpdateRowResponse, InsertOrUpdateRowResponses, ListColumnsData, ListColumnsError, ListColumnsErrors, ListColumnsResponse, ListColumnsResponses, ListHistoryData, ListHistoryError, ListHistoryErrors, ListHistoryResponse, ListHistoryResponses, ListRecentHistoryData, ListRecentHistoryError, ListRecentHistoryErrors, ListRecentHistoryResponse, ListRecentHistoryResponses, ListRowsData, ListRowsError, ListRowsErrors, ListRowsResponse, ListRowsResponse2, ListRowsResponses, ListRowsResponseWritable, ListTablesData, ListTablesError, ListTablesErrors, ListTablesResponse, ListTablesResponses, ListTablesRow, NewTableFormFiledsData, NewTableFormFiledsError, NewTableFormFiledsErrors, NewTableFormFiledsResponse, NewTableFormFiledsResponses, RowInsertOrUpdateFormData, RowInsertOrUpdateFormError, RowInsertOrUpdateFormErrors, RowInsertOrUpdateFormOutputBody, RowInsertOrUpdateFormOutputBodyWritable, RowInsertOrUpdateFormResponse, RowInsertOrUpdateFormResponses, RowSet, RunSqlQueryData, RunSqlQueryError, RunSqlQueryErrors, RunSqlQueryInputBody, RunSqlQueryInputBodyWritable, RunSqlQueryOutputBody, RunSqlQueryOutputBodyWritable, RunSqlQueryResponse, RunSqlQueryResponses, VarianDataType } from './types.gen'; diff --git a/frontend/src/client/sdk.gen.ts b/frontend/src/client/sdk.gen.ts deleted file mode 100644 index d4c9917..0000000 --- a/frontend/src/client/sdk.gen.ts +++ /dev/null @@ -1,107 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Client, Options as Options2, TDataShape } from './client'; -import { client } from './client.gen'; -import type { CreateNewTableData, CreateNewTableErrors, CreateNewTableResponses, DeleteRowData, DeleteRowErrors, DeleteRowResponses, DeleteTableData, DeleteTableErrors, DeleteTableResponses, InsertOrUpdateRowData, InsertOrUpdateRowErrors, InsertOrUpdateRowResponses, ListColumnsData, ListColumnsErrors, ListColumnsResponses, ListHistoryData, ListHistoryErrors, ListHistoryResponses, ListRecentHistoryData, ListRecentHistoryErrors, ListRecentHistoryResponses, ListRowsData, ListRowsErrors, ListRowsResponses, ListTablesData, ListTablesErrors, ListTablesResponses, NewTableFormFiledsData, NewTableFormFiledsErrors, NewTableFormFiledsResponses, RowInsertOrUpdateFormData, RowInsertOrUpdateFormErrors, RowInsertOrUpdateFormResponses, RunSqlQueryData, RunSqlQueryErrors, RunSqlQueryResponses } from './types.gen'; - -export type Options = Options2 & { - /** - * You can provide a client instance returned by `createClient()` instead of - * individual options. This might be also useful if you want to implement a - * custom client. - */ - client?: Client; - /** - * You can pass arbitrary values through the `meta` object. This can be - * used to access values that aren't defined as part of the SDK function. - */ - meta?: Record; -}; - -/** - * List query history - */ -export const listHistory = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/history', ...options }); - -/** - * List recent history - */ -export const listRecentHistory = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/history/recent', ...options }); - -/** - * Run SQL query - */ -export const runSqlQuery = (options: Options) => (options.client ?? client).post({ - url: '/api/v1/run-sql', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - -/** - * Delete a table - */ -export const deleteTable = (options: Options) => (options.client ?? client).delete({ - url: '/api/v1/tables', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - -/** - * List of all tables - */ -export const listTables = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/tables', ...options }); - -/** - * Get data types for new table form - */ -export const newTableFormFileds = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/tables/form/new', ...options }); - -/** - * Create a new table - */ -export const createNewTable = (options: Options) => (options.client ?? client).post({ - url: '/api/v1/tables/form/new', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - -/** - * List rows of a table - */ -export const listRows = (options: Options) => (options.client ?? client).get({ url: '/api/v1/tables/{tableName}', ...options }); - -/** - * List columns of a table - */ -export const listColumns = (options: Options) => (options.client ?? client).get({ url: '/api/v1/tables/{tableName}/columns', ...options }); - -/** - * Get row insert/update form metadata - */ -export const rowInsertOrUpdateForm = (options: Options) => (options.client ?? client).get({ url: '/api/v1/tables/{tableName}/form', ...options }); - -/** - * Insert or update a row - */ -export const insertOrUpdateRow = (options: Options) => (options.client ?? client).post({ - url: '/api/v1/tables/{tableName}/form', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - -/** - * Delete a row - */ -export const deleteRow = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/tables/{tableName}/row/{hash}', ...options }); diff --git a/frontend/src/client/types.gen.ts b/frontend/src/client/types.gen.ts deleted file mode 100644 index 1c5428d..0000000 --- a/frontend/src/client/types.gen.ts +++ /dev/null @@ -1,576 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type ClientOptions = { - baseUrl: `${string}://${string}` | (string & {}); -}; - -export type ColType = { - dataType: string; - hasAutoIncrement: boolean; - hasDefault: boolean; - hasDigit?: boolean; - hasSize: boolean; - hasValues?: boolean; - inputType: 'text' | 'number' | 'checkbox' | 'textarea' | 'json' | 'select'; - isNull: boolean; - isPk: boolean; - isUnique: boolean; - [key: string]: unknown; -}; - -export type ColValue = { - columnName: string; - columnType: ColType; - defaultValue?: unknown; - size?: number; - value?: unknown; - [key: string]: unknown; -}; - -export type CreeteNewTableInputBody = { - /** - * A URL to the JSON Schema for this object. - */ - readonly $schema?: string; - inputs: Array | null; - tableName: string; - [key: string]: unknown; -}; - -export type DeleteTableRequest = { - /** - * A URL to the JSON Schema for this object. - */ - readonly $schema?: string; - tableName: string; - verificationQuery: string; - [key: string]: unknown; -}; - -export type ErrorDetail = { - /** - * Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id' - */ - location?: string; - /** - * Error message text - */ - message?: string; - /** - * The value at the given location - */ - value?: unknown; - [key: string]: unknown; -}; - -export type ErrorModel = { - /** - * A URL to the JSON Schema for this object. - */ - readonly $schema?: string; - /** - * A human-readable explanation specific to this occurrence of the problem. - */ - detail?: string; - /** - * Optional list of individual error details - */ - errors?: Array | null; - /** - * A URI reference that identifies the specific occurrence of the problem. - */ - instance?: string; - /** - * HTTP status code - */ - status?: number; - /** - * A short, human-readable summary of the problem type. This value should not change between occurrences of the error. - */ - title?: string; - /** - * A URI reference to human-readable documentation for the error. - */ - type?: string; - [key: string]: unknown; -}; - -export type FormDatatype = { - /** - * A URL to the JSON Schema for this object. - */ - readonly $schema?: string; - numericType: Array | null; - stringType: Array | null; - [key: string]: unknown; -}; - -export type History = { - id: number; - message: string; - time: string; - [key: string]: unknown; -}; - -export type ListRowsResponse = { - /** - * A URL to the JSON Schema for this object. - */ - readonly $schema?: string; - activeTable: string; - cols: Array | null; - hasNextPage: boolean; - page: number; - rowCount: number; - rows: Array | null; - totalPages: number; - [key: string]: unknown; -}; - -export type ListTablesRow = { - tableName: string; - tableSchema: string; - [key: string]: unknown; -}; - -export type RowInsertOrUpdateFormOutputBody = { - /** - * A URL to the JSON Schema for this object. - */ - readonly $schema?: string; - cols: Array | null; - [key: string]: unknown; -}; - -export type RowSet = { - columns: Array | null; - hash: string; - [key: string]: unknown; -}; - -export type RunSqlQueryInputBody = { - /** - * A URL to the JSON Schema for this object. - */ - readonly $schema?: string; - query: string; - [key: string]: unknown; -}; - -export type RunSqlQueryOutputBody = { - /** - * A URL to the JSON Schema for this object. - */ - readonly $schema?: string; - columns: Array | null; - rows: Array | null> | null; - [key: string]: unknown; -}; - -export type VarianDataType = { - hasAutoIncrement: boolean; - hasDigit: boolean; - hasSize: boolean; - hasValues: boolean; - isNumeric: boolean; - type: string; - [key: string]: unknown; -}; - -export type CreeteNewTableInputBodyWritable = { - inputs: Array | null; - tableName: string; - [key: string]: unknown; -}; - -export type DeleteTableRequestWritable = { - tableName: string; - verificationQuery: string; - [key: string]: unknown; -}; - -export type ErrorModelWritable = { - /** - * A human-readable explanation specific to this occurrence of the problem. - */ - detail?: string; - /** - * Optional list of individual error details - */ - errors?: Array | null; - /** - * A URI reference that identifies the specific occurrence of the problem. - */ - instance?: string; - /** - * HTTP status code - */ - status?: number; - /** - * A short, human-readable summary of the problem type. This value should not change between occurrences of the error. - */ - title?: string; - /** - * A URI reference to human-readable documentation for the error. - */ - type?: string; - [key: string]: unknown; -}; - -export type FormDatatypeWritable = { - numericType: Array | null; - stringType: Array | null; - [key: string]: unknown; -}; - -export type ListRowsResponseWritable = { - activeTable: string; - cols: Array | null; - hasNextPage: boolean; - page: number; - rowCount: number; - rows: Array | null; - totalPages: number; - [key: string]: unknown; -}; - -export type RowInsertOrUpdateFormOutputBodyWritable = { - cols: Array | null; - [key: string]: unknown; -}; - -export type RunSqlQueryInputBodyWritable = { - query: string; - [key: string]: unknown; -}; - -export type RunSqlQueryOutputBodyWritable = { - columns: Array | null; - rows: Array | null> | null; - [key: string]: unknown; -}; - -export type ListHistoryData = { - body?: never; - path?: never; - query?: { - page?: number; - }; - url: '/api/v1/history'; -}; - -export type ListHistoryErrors = { - /** - * Error - */ - default: ErrorModel; -}; - -export type ListHistoryError = ListHistoryErrors[keyof ListHistoryErrors]; - -export type ListHistoryResponses = { - /** - * OK - */ - 200: Array | null; -}; - -export type ListHistoryResponse = ListHistoryResponses[keyof ListHistoryResponses]; - -export type ListRecentHistoryData = { - body?: never; - path?: never; - query?: never; - url: '/api/v1/history/recent'; -}; - -export type ListRecentHistoryErrors = { - /** - * Error - */ - default: ErrorModel; -}; - -export type ListRecentHistoryError = ListRecentHistoryErrors[keyof ListRecentHistoryErrors]; - -export type ListRecentHistoryResponses = { - /** - * OK - */ - 200: Array | null; -}; - -export type ListRecentHistoryResponse = ListRecentHistoryResponses[keyof ListRecentHistoryResponses]; - -export type RunSqlQueryData = { - body: RunSqlQueryInputBodyWritable; - path?: never; - query?: never; - url: '/api/v1/run-sql'; -}; - -export type RunSqlQueryErrors = { - /** - * Error - */ - default: ErrorModel; -}; - -export type RunSqlQueryError = RunSqlQueryErrors[keyof RunSqlQueryErrors]; - -export type RunSqlQueryResponses = { - /** - * OK - */ - 200: RunSqlQueryOutputBody; -}; - -export type RunSqlQueryResponse = RunSqlQueryResponses[keyof RunSqlQueryResponses]; - -export type DeleteTableData = { - body: DeleteTableRequestWritable; - path?: never; - query?: never; - url: '/api/v1/tables'; -}; - -export type DeleteTableErrors = { - /** - * Error - */ - default: ErrorModel; -}; - -export type DeleteTableError = DeleteTableErrors[keyof DeleteTableErrors]; - -export type DeleteTableResponses = { - /** - * No Content - */ - 204: void; -}; - -export type DeleteTableResponse = DeleteTableResponses[keyof DeleteTableResponses]; - -export type ListTablesData = { - body?: never; - path?: never; - query?: never; - url: '/api/v1/tables'; -}; - -export type ListTablesErrors = { - /** - * Error - */ - default: ErrorModel; -}; - -export type ListTablesError = ListTablesErrors[keyof ListTablesErrors]; - -export type ListTablesResponses = { - /** - * OK - */ - 200: Array | null; -}; - -export type ListTablesResponse = ListTablesResponses[keyof ListTablesResponses]; - -export type NewTableFormFiledsData = { - body?: never; - path?: never; - query?: never; - url: '/api/v1/tables/form/new'; -}; - -export type NewTableFormFiledsErrors = { - /** - * Error - */ - default: ErrorModel; -}; - -export type NewTableFormFiledsError = NewTableFormFiledsErrors[keyof NewTableFormFiledsErrors]; - -export type NewTableFormFiledsResponses = { - /** - * OK - */ - 200: FormDatatype; -}; - -export type NewTableFormFiledsResponse = NewTableFormFiledsResponses[keyof NewTableFormFiledsResponses]; - -export type CreateNewTableData = { - body: CreeteNewTableInputBodyWritable; - path?: never; - query?: never; - url: '/api/v1/tables/form/new'; -}; - -export type CreateNewTableErrors = { - /** - * Error - */ - default: ErrorModel; -}; - -export type CreateNewTableError = CreateNewTableErrors[keyof CreateNewTableErrors]; - -export type CreateNewTableResponses = { - /** - * No Content - */ - 204: void; -}; - -export type CreateNewTableResponse = CreateNewTableResponses[keyof CreateNewTableResponses]; - -export type ListRowsData = { - body?: never; - path: { - tableName: string; - }; - query?: { - page?: number; - column?: string; - order?: 'ASC' | 'DESC'; - }; - url: '/api/v1/tables/{tableName}'; -}; - -export type ListRowsErrors = { - /** - * Error - */ - default: ErrorModel; -}; - -export type ListRowsError = ListRowsErrors[keyof ListRowsErrors]; - -export type ListRowsResponses = { - /** - * OK - */ - 200: ListRowsResponse; -}; - -export type ListRowsResponse2 = ListRowsResponses[keyof ListRowsResponses]; - -export type ListColumnsData = { - body?: never; - path: { - tableName: string; - }; - query?: never; - url: '/api/v1/tables/{tableName}/columns'; -}; - -export type ListColumnsErrors = { - /** - * Error - */ - default: ErrorModel; -}; - -export type ListColumnsError = ListColumnsErrors[keyof ListColumnsErrors]; - -export type ListColumnsResponses = { - /** - * OK - */ - 200: Array | null; -}; - -export type ListColumnsResponse = ListColumnsResponses[keyof ListColumnsResponses]; - -export type RowInsertOrUpdateFormData = { - body?: never; - path: { - tableName: string; - }; - query?: { - page?: number; - hash?: string; - }; - url: '/api/v1/tables/{tableName}/form'; -}; - -export type RowInsertOrUpdateFormErrors = { - /** - * Error - */ - default: ErrorModel; -}; - -export type RowInsertOrUpdateFormError = RowInsertOrUpdateFormErrors[keyof RowInsertOrUpdateFormErrors]; - -export type RowInsertOrUpdateFormResponses = { - /** - * OK - */ - 200: RowInsertOrUpdateFormOutputBody; -}; - -export type RowInsertOrUpdateFormResponse = RowInsertOrUpdateFormResponses[keyof RowInsertOrUpdateFormResponses]; - -export type InsertOrUpdateRowData = { - body: Array | null; - path: { - tableName: string; - }; - query?: { - hash?: string; - page?: number; - }; - url: '/api/v1/tables/{tableName}/form'; -}; - -export type InsertOrUpdateRowErrors = { - /** - * Error - */ - default: ErrorModel; -}; - -export type InsertOrUpdateRowError = InsertOrUpdateRowErrors[keyof InsertOrUpdateRowErrors]; - -export type InsertOrUpdateRowResponses = { - /** - * No Content - */ - 204: void; -}; - -export type InsertOrUpdateRowResponse = InsertOrUpdateRowResponses[keyof InsertOrUpdateRowResponses]; - -export type DeleteRowData = { - body?: never; - path: { - tableName: string; - hash: string; - }; - query?: { - page?: number; - }; - url: '/api/v1/tables/{tableName}/row/{hash}'; -}; - -export type DeleteRowErrors = { - /** - * Error - */ - default: ErrorModel; -}; - -export type DeleteRowError = DeleteRowErrors[keyof DeleteRowErrors]; - -export type DeleteRowResponses = { - /** - * No Content - */ - 204: void; -}; - -export type DeleteRowResponse = DeleteRowResponses[keyof DeleteRowResponses]; diff --git a/frontend/src/client/zod.gen.ts b/frontend/src/client/zod.gen.ts deleted file mode 100644 index 6fb5496..0000000 --- a/frontend/src/client/zod.gen.ts +++ /dev/null @@ -1,277 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import * as z from 'zod'; - -export const zColType = z.object({ - dataType: z.string(), - hasAutoIncrement: z.boolean(), - hasDefault: z.boolean(), - hasDigit: z.boolean().optional(), - hasSize: z.boolean(), - hasValues: z.boolean().optional(), - inputType: z.enum([ - 'text', - 'number', - 'checkbox', - 'textarea', - 'json', - 'select' - ]), - isNull: z.boolean(), - isPk: z.boolean(), - isUnique: z.boolean() -}); - -export const zColValue = z.object({ - columnName: z.string(), - columnType: zColType, - defaultValue: z.unknown().optional(), - size: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional(), - value: z.unknown().optional() -}); - -export const zCreeteNewTableInputBody = z.object({ - $schema: z.url().readonly().optional(), - inputs: z.array(zColValue).nullable(), - tableName: z.string() -}); - -export const zDeleteTableRequest = z.object({ - $schema: z.url().readonly().optional(), - tableName: z.string(), - verificationQuery: z.string() -}); - -export const zErrorDetail = z.object({ - location: z.string().optional(), - message: z.string().optional(), - value: z.unknown().optional() -}); - -export const zErrorModel = z.object({ - $schema: z.url().readonly().optional(), - detail: z.string().optional(), - errors: z.array(zErrorDetail).nullish(), - instance: z.url().optional(), - status: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional(), - title: z.string().optional(), - type: z.url().optional().default('about:blank') -}); - -export const zHistory = z.object({ - id: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }), - message: z.string(), - time: z.iso.datetime() -}); - -export const zListTablesRow = z.object({ - tableName: z.string(), - tableSchema: z.string() -}); - -export const zRowInsertOrUpdateFormOutputBody = z.object({ - $schema: z.url().readonly().optional(), - cols: z.array(zColValue).nullable() -}); - -export const zRowSet = z.object({ - columns: z.array(zColValue).nullable(), - hash: z.string() -}); - -export const zListRowsResponse = z.object({ - $schema: z.url().readonly().optional(), - activeTable: z.string(), - cols: z.array(zColValue).nullable(), - hasNextPage: z.boolean(), - page: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }), - rowCount: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }), - rows: z.array(zRowSet).nullable(), - totalPages: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }) -}); - -export const zRunSqlQueryInputBody = z.object({ - $schema: z.url().readonly().optional(), - query: z.string() -}); - -export const zRunSqlQueryOutputBody = z.object({ - $schema: z.url().readonly().optional(), - columns: z.array(z.string()).nullable(), - rows: z.array(z.array(z.unknown()).nullable()).nullable() -}); - -export const zVarianDataType = z.object({ - hasAutoIncrement: z.boolean(), - hasDigit: z.boolean(), - hasSize: z.boolean(), - hasValues: z.boolean(), - isNumeric: z.boolean(), - type: z.string() -}); - -export const zFormDatatype = z.object({ - $schema: z.url().readonly().optional(), - numericType: z.array(zVarianDataType).nullable(), - stringType: z.array(zVarianDataType).nullable() -}); - -export const zCreeteNewTableInputBodyWritable = z.object({ - inputs: z.array(zColValue).nullable(), - tableName: z.string() -}); - -export const zDeleteTableRequestWritable = z.object({ - tableName: z.string(), - verificationQuery: z.string() -}); - -export const zErrorModelWritable = z.object({ - detail: z.string().optional(), - errors: z.array(zErrorDetail).nullish(), - instance: z.url().optional(), - status: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional(), - title: z.string().optional(), - type: z.url().optional().default('about:blank') -}); - -export const zFormDatatypeWritable = z.object({ - numericType: z.array(zVarianDataType).nullable(), - stringType: z.array(zVarianDataType).nullable() -}); - -export const zListRowsResponseWritable = z.object({ - activeTable: z.string(), - cols: z.array(zColValue).nullable(), - hasNextPage: z.boolean(), - page: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }), - rowCount: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }), - rows: z.array(zRowSet).nullable(), - totalPages: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }) -}); - -export const zRowInsertOrUpdateFormOutputBodyWritable = z.object({ - cols: z.array(zColValue).nullable() -}); - -export const zRunSqlQueryInputBodyWritable = z.object({ - query: z.string() -}); - -export const zRunSqlQueryOutputBodyWritable = z.object({ - columns: z.array(z.string()).nullable(), - rows: z.array(z.array(z.unknown()).nullable()).nullable() -}); - -export const zListHistoryQuery = z.object({ - page: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional().default(BigInt(1)) -}); - -/** - * OK - */ -export const zListHistoryResponse = z.array(zHistory).nullable(); - -/** - * OK - */ -export const zListRecentHistoryResponse = z.array(zHistory).nullable(); - -export const zRunSqlQueryBody = zRunSqlQueryInputBodyWritable; - -/** - * OK - */ -export const zRunSqlQueryResponse = zRunSqlQueryOutputBody; - -export const zDeleteTableBody = zDeleteTableRequestWritable; - -/** - * No Content - */ -export const zDeleteTableResponse = z.void(); - -/** - * OK - */ -export const zListTablesResponse = z.array(zListTablesRow).nullable(); - -/** - * OK - */ -export const zNewTableFormFiledsResponse = zFormDatatype; - -export const zCreateNewTableBody = zCreeteNewTableInputBodyWritable; - -/** - * No Content - */ -export const zCreateNewTableResponse = z.void(); - -export const zListRowsPath = z.object({ - tableName: z.string() -}); - -export const zListRowsQuery = z.object({ - page: z.coerce.bigint().gte(BigInt(1)).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional().default(BigInt(1)), - column: z.string().optional(), - order: z.enum(['ASC', 'DESC']).optional() -}); - -/** - * OK - */ -export const zListRowsResponse2 = zListRowsResponse; - -export const zListColumnsPath = z.object({ - tableName: z.string() -}); - -/** - * OK - */ -export const zListColumnsResponse = z.array(zColValue).nullable(); - -export const zRowInsertOrUpdateFormPath = z.object({ - tableName: z.string() -}); - -export const zRowInsertOrUpdateFormQuery = z.object({ - page: z.coerce.bigint().gte(BigInt(1)).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional().default(BigInt(1)), - hash: z.string().optional() -}); - -/** - * OK - */ -export const zRowInsertOrUpdateFormResponse = zRowInsertOrUpdateFormOutputBody; - -export const zInsertOrUpdateRowBody = z.array(zColValue).nullable(); - -export const zInsertOrUpdateRowPath = z.object({ - tableName: z.string() -}); - -export const zInsertOrUpdateRowQuery = z.object({ - hash: z.string().optional(), - page: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional().default(BigInt(1)) -}); - -/** - * No Content - */ -export const zInsertOrUpdateRowResponse = z.void(); - -export const zDeleteRowPath = z.object({ - tableName: z.string(), - hash: z.string() -}); - -export const zDeleteRowQuery = z.object({ - page: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional().default(BigInt(1)) -}); - -/** - * No Content - */ -export const zDeleteRowResponse = z.void(); diff --git a/frontend/src/components/rows/RowUpsertForm.tsx b/frontend/src/components/rows/RowUpsertForm.tsx new file mode 100644 index 0000000..7b728d3 --- /dev/null +++ b/frontend/src/components/rows/RowUpsertForm.tsx @@ -0,0 +1,366 @@ +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { Controller, useForm, useFieldArray } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { Input } from "@/components/ui/input"; +import { toast } from "sonner"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Textarea } from "@/components/ui/textarea"; +import { + Field, + FieldError, + FieldGroup, + FieldLabel, +} from "@/components/ui/field"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { + insertOrUpdateRowMutation, + rowInsertOrUpdateFormOptions, +} from "@/client/@tanstack/react-query.gen"; +import type { ColValue, ErrorModel } from "@/client"; +import { zColValue } from "@/client/zod.gen"; + +const zColField = zColValue.extend({ + size: z.coerce.number().optional(), + value: z.union([z.string(), z.boolean(), z.number(), z.null()]).default(null), + useDefault: z.boolean().default(false), + useAutoIncrement: z.boolean().default(false), +}); + +const formSchema = z.object({ + cols: z.array(zColField).min(1, "At least one column required"), +}); + +type FormInput = z.input; +type FormSchema = z.output; +type ColField = z.infer; + +const colFieldValueSchema = z + .union([z.string(), z.boolean(), z.number(), z.null()]) + .catch(null); + +function buildDefaultCols(cols: ColValue[]): ColField[] { + return cols.map((col) => ({ + columnName: col.columnName, + columnType: col.columnType, + defaultValue: col.defaultValue, + size: col.size !== undefined ? Number(col.size) : undefined, + value: colFieldValueSchema.parse(col.value), + useDefault: false, + useAutoIncrement: false, + })); +} +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; + +import { Button } from "@/components/ui/button"; +import { Loader2, Plus, Save } from "lucide-react"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { useRowContext } from "@/hooks/useRows"; + +interface RowUpsertFormProps { + children?: React.ReactNode; + hash?: string; +} + +export const RowUpsertForm = ({ hash, children }: RowUpsertFormProps) => { + const { page, tableName } = useRowContext(); + const navigate = useNavigate(); + + const isEdit = !!hash; + + const { data, isPending } = useQuery( + rowInsertOrUpdateFormOptions({ + path: { tableName }, + query: { hash: hash || undefined, page }, + }), + ); + + const { mutateAsync: insertOrUpdateMutation } = useMutation( + insertOrUpdateRowMutation(), + ); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + cols: [], + }, + mode: "onChange", + }); + + const { fields, replace } = useFieldArray({ + control: form.control, + name: "cols", + }); + + useEffect(() => { + if (data?.cols) { + replace(buildDefaultCols(data.cols)); + } + }, [data, replace]); + + const onSubmit = async (formValues: FormSchema) => { + if (!tableName) return; + await insertOrUpdateMutation( + { + path: { + tableName: tableName, + }, + body: formValues.cols, + query: { + hash: hash || undefined, + page: page, + }, + }, + { + onError: (error: ErrorModel) => { + console.error( + "Mutation error:", + error.errors?.[0]?.message || error.detail, + ); + }, + onSuccess: () => { + toast.success("Row inserted/updated successfully"); + navigate(`/tables/${tableName}?page=${page}`); + }, + }, + ); + }; + return ( + + + {children || ( + + )} + + e.preventDefault()} + className="min-w-[90%] md:min-w-xl flex flex-col p-0" + > + {isPending ? ( +
+ +
+ ) : ( +
+ + + {isEdit ? "Update" : "Insert"} Row + + {isEdit + ? "Update the details of the existing record." + : "Fill in the fields below to add a new record to the table."} + + + + {form.formState.errors.cols?.message && ( +
+ {form.formState.errors.cols.message} +
+ )} + + +
+ {fields.length === 0 ? ( +
+ No columns found for this table. +
+ ) : ( + fields.map((field, index) => { + const { columnName, columnType } = field; + + return ( + { + const useDefault = form.watch( + `cols.${index}.useDefault`, + ); + const useAutoIncrement = form.watch( + `cols.${index}.useAutoIncrement`, + ); + const isDisabled = useDefault || useAutoIncrement; + + return ( + + + {columnName.replace(/_/g, " ")} + + ({columnType.dataType}) + + {columnType.isUnique && ( + + • Unique + + )} + + + {columnType.hasDefault && ( + ( + + )} + /> + )} + + {columnType.hasAutoIncrement && ( + ( + + )} + /> + )} + + {columnType.inputType === "checkbox" ? ( +
+ + inputField.onChange(c === true) + } + /> + + Enable {columnName.replace(/_/g, " ")} + +
+ ) : columnType.inputType === "textarea" || + columnType.inputType === "json" ? ( +