From d888aa0306503cad0acb100321d3638854856a20 Mon Sep 17 00:00:00 2001 From: Henry Fontanier Date: Fri, 17 Nov 2023 11:58:44 +0100 Subject: [PATCH] Revert "Revert "feat(structured data): remaining front API endpoints"" (#2581) * Revert "Revert "feat(structured data): remaining front API endpoints (#2574)" (#2579)" This reverts commit 68e99aa01399f2e0b4d6e2523ea8e6a0de87524a. * fix query params dyn routes --- front/lib/core_api.ts | 26 ++- .../[name]/databases/[dId]/index.ts | 104 +++++++++ .../[name]/databases/[dId]/query.ts | 135 +++++++++++ .../[name]/databases/[dId]/schema.ts | 108 +++++++++ .../[name]/databases/[dId]/tables/[tId].ts | 119 ++++++++++ .../[name]/databases/[dId]/tables/index.ts | 173 ++++++++++++++ .../databases/[dId]/tables/rows/[rId].ts | 135 +++++++++++ .../databases/[dId]/tables/rows/index.ts | 212 ++++++++++++++++++ 8 files changed, 1000 insertions(+), 12 deletions(-) create mode 100644 front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/index.ts create mode 100644 front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/query.ts create mode 100644 front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/schema.ts create mode 100644 front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/tables/[tId].ts create mode 100644 front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/tables/index.ts create mode 100644 front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/tables/rows/[rId].ts create mode 100644 front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/tables/rows/index.ts diff --git a/front/lib/core_api.ts b/front/lib/core_api.ts index bd684e38c002..6d19c14b750a 100644 --- a/front/lib/core_api.ts +++ b/front/lib/core_api.ts @@ -119,7 +119,7 @@ export type CoreAPIDatabase = { name: string; }; -type CoreAPIDatabaseTable = { +export type CoreAPIDatabaseTable = { created: number; database_id: string; table_id: string; @@ -127,13 +127,21 @@ type CoreAPIDatabaseTable = { description: string; }; -type CoreAPIDatabaseRow = { +export type CoreAPIDatabaseRow = { created: number; table_id: string; row_id: string; content: Record; }; +export type CoreAPIDatabaseSchema = Record< + string, + { + table: CoreAPIDatabaseTable; + schema: Record; + } +>; + export const CoreAPI = { async createProject(): Promise> { const response = await fetch(`${CORE_API}/projects`, { @@ -905,7 +913,7 @@ export const CoreAPI = { tableId: string; name: string; description: string; - }): Promise> { + }): Promise> { const response = await fetch( `${CORE_API}/projects/${projectId}/data_sources/${dataSourceName}/databases/${databaseId}/tables`, { @@ -1061,13 +1069,7 @@ export const CoreAPI = { databaseId: string; }): Promise< CoreAPIResponse<{ - schema: Record< - string, - { - table: CoreAPIDatabaseTable; - schema: Record; - } - >; + schema: CoreAPIDatabaseSchema; }> > { const response = await fetch( @@ -1092,8 +1094,8 @@ export const CoreAPI = { query: string; }): Promise< CoreAPIResponse<{ - schema: Record; - rows: Record[]; + schema: CoreAPIDatabaseSchema; + rows: CoreAPIDatabaseRow[]; }> > { const response = await fetch( diff --git a/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/index.ts b/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/index.ts new file mode 100644 index 000000000000..eff24b90f000 --- /dev/null +++ b/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/index.ts @@ -0,0 +1,104 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +import { getDataSource } from "@app/lib/api/data_sources"; +import { Authenticator, getAPIKey } from "@app/lib/auth"; +import { CoreAPI, CoreAPIDatabase } from "@app/lib/core_api"; +import { isDevelopmentOrDustWorkspace } from "@app/lib/development"; +import logger from "@app/logger/logger"; +import { apiError, withLogging } from "@app/logger/withlogging"; + +type GetDatabaseResponseBody = { + database: CoreAPIDatabase; +}; + +async function handler( + req: NextApiRequest, + res: NextApiResponse +): Promise { + const keyRes = await getAPIKey(req); + if (keyRes.isErr()) { + return apiError(req, res, keyRes.error); + } + + const { auth } = await Authenticator.fromKey( + keyRes.value, + req.query.wId as string + ); + + const owner = auth.workspace(); + const plan = auth.plan(); + if (!owner || !plan) { + return apiError(req, res, { + status_code: 404, + api_error: { + type: "workspace_not_found", + message: "The workspace you requested was not found.", + }, + }); + } + + if (!isDevelopmentOrDustWorkspace(owner)) { + res.status(404).end(); + return; + } + + const dataSource = await getDataSource(auth, req.query.name as string); + if (!dataSource) { + return apiError(req, res, { + status_code: 404, + api_error: { + type: "data_source_not_found", + message: "The data source you requested was not found.", + }, + }); + } + + const databaseId = req.query.dId; + if (!databaseId || typeof databaseId !== "string") { + return apiError(req, res, { + status_code: 400, + api_error: { + type: "invalid_request_error", + message: "The database id is missing.", + }, + }); + } + + switch (req.method) { + case "GET": + const databaseRes = await CoreAPI.getDatabase({ + projectId: dataSource.dustAPIProjectId, + dataSourceName: dataSource.name, + databaseId, + }); + if (databaseRes.isErr()) { + logger.error({ + dataSourcename: dataSource.name, + workspaceId: owner.id, + error: databaseRes.error, + }); + return apiError(req, res, { + status_code: 500, + api_error: { + type: "internal_server_error", + message: "Failed to get database.", + }, + }); + } + + const { database } = databaseRes.value; + + return res.status(200).json({ database }); + + default: + return apiError(req, res, { + status_code: 405, + api_error: { + type: "method_not_supported_error", + message: "The method passed is not supported, GET is expected.", + }, + }); + } +} + +export default withLogging(handler); diff --git a/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/query.ts b/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/query.ts new file mode 100644 index 000000000000..76bcd72d363e --- /dev/null +++ b/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/query.ts @@ -0,0 +1,135 @@ +import { isLeft } from "fp-ts/lib/Either"; +import * as t from "io-ts"; +import * as reporter from "io-ts-reporters"; +import { NextApiRequest, NextApiResponse } from "next"; + +import { getDataSource } from "@app/lib/api/data_sources"; +import { Authenticator, getAPIKey } from "@app/lib/auth"; +import { + CoreAPI, + CoreAPIDatabaseRow, + CoreAPIDatabaseSchema, +} from "@app/lib/core_api"; +import { isDevelopmentOrDustWorkspace } from "@app/lib/development"; +import logger from "@app/logger/logger"; +import { apiError, withLogging } from "@app/logger/withlogging"; + +const GetDatabaseSchemaReqBodySchema = t.type({ + query: t.string, +}); + +type QueryDatabaseSchemaResponseBody = { + schema: CoreAPIDatabaseSchema; + rows: CoreAPIDatabaseRow[]; +}; + +async function handler( + req: NextApiRequest, + res: NextApiResponse +): Promise { + const keyRes = await getAPIKey(req); + if (keyRes.isErr()) { + return apiError(req, res, keyRes.error); + } + + const { auth } = await Authenticator.fromKey( + keyRes.value, + req.query.wId as string + ); + + const owner = auth.workspace(); + const plan = auth.plan(); + if (!owner || !plan) { + return apiError(req, res, { + status_code: 404, + api_error: { + type: "workspace_not_found", + message: "The workspace you requested was not found.", + }, + }); + } + + if (!isDevelopmentOrDustWorkspace(owner)) { + res.status(404).end(); + return; + } + + const dataSource = await getDataSource(auth, req.query.name as string); + if (!dataSource) { + return apiError(req, res, { + status_code: 404, + api_error: { + type: "data_source_not_found", + message: "The data source you requested was not found.", + }, + }); + } + + const databaseId = req.query.dId; + if (!databaseId || typeof databaseId !== "string") { + return apiError(req, res, { + status_code: 400, + api_error: { + type: "invalid_request_error", + message: "Invalid request query: id is required.", + }, + }); + } + + switch (req.method) { + case "POST": + const bodyValidation = GetDatabaseSchemaReqBodySchema.decode(req.body); + if (isLeft(bodyValidation)) { + const pathError = reporter.formatValidationErrors(bodyValidation.left); + return apiError(req, res, { + status_code: 400, + api_error: { + type: "invalid_request_error", + message: `Invalid request body: ${pathError}`, + }, + }); + } + + const { query } = bodyValidation.right; + + const queryRes = await CoreAPI.queryDatabase({ + projectId: dataSource.dustAPIProjectId, + dataSourceName: dataSource.name, + databaseId, + query, + }); + if (queryRes.isErr()) { + logger.error( + { + dataSourceName: dataSource.name, + workspaceId: owner.id, + databaseId, + error: queryRes.error, + }, + "Failed to query database." + ); + return apiError(req, res, { + status_code: 500, + api_error: { + type: "internal_server_error", + message: "Failed to query database.", + }, + }); + } + + const { schema, rows } = queryRes.value; + + return res.status(200).json({ schema, rows }); + + default: + return apiError(req, res, { + status_code: 405, + api_error: { + type: "method_not_supported_error", + message: "The method passed is not supported, GET is expected.", + }, + }); + } +} + +export default withLogging(handler); diff --git a/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/schema.ts b/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/schema.ts new file mode 100644 index 000000000000..d5a5bd6a18b0 --- /dev/null +++ b/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/schema.ts @@ -0,0 +1,108 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +import { getDataSource } from "@app/lib/api/data_sources"; +import { Authenticator, getAPIKey } from "@app/lib/auth"; +import { CoreAPI, CoreAPIDatabaseSchema } from "@app/lib/core_api"; +import { isDevelopmentOrDustWorkspace } from "@app/lib/development"; +import logger from "@app/logger/logger"; +import { apiError, withLogging } from "@app/logger/withlogging"; + +type GetDatabaseSchemaResponseBody = { + schema: CoreAPIDatabaseSchema; +}; + +async function handler( + req: NextApiRequest, + res: NextApiResponse +): Promise { + const keyRes = await getAPIKey(req); + if (keyRes.isErr()) { + return apiError(req, res, keyRes.error); + } + + const { auth } = await Authenticator.fromKey( + keyRes.value, + req.query.wId as string + ); + + const owner = auth.workspace(); + const plan = auth.plan(); + if (!owner || !plan) { + return apiError(req, res, { + status_code: 404, + api_error: { + type: "workspace_not_found", + message: "The workspace you requested was not found.", + }, + }); + } + + if (!isDevelopmentOrDustWorkspace(owner)) { + res.status(404).end(); + return; + } + + const dataSource = await getDataSource(auth, req.query.name as string); + if (!dataSource) { + return apiError(req, res, { + status_code: 404, + api_error: { + type: "data_source_not_found", + message: "The data source you requested was not found.", + }, + }); + } + + const databaseId = req.query.dId; + if (!databaseId || typeof databaseId !== "string") { + return apiError(req, res, { + status_code: 400, + api_error: { + type: "invalid_request_error", + message: "Invalid request query: id is required.", + }, + }); + } + + switch (req.method) { + case "GET": + const schemaRes = await CoreAPI.getDatabaseSchema({ + projectId: dataSource.dustAPIProjectId, + dataSourceName: dataSource.name, + databaseId, + }); + if (schemaRes.isErr()) { + logger.error( + { + dataSourceName: dataSource.name, + workspaceId: owner.id, + databaseId, + error: schemaRes.error, + }, + "Failed to get database schema." + ); + return apiError(req, res, { + status_code: 500, + api_error: { + type: "internal_server_error", + message: "Failed to get database schema.", + }, + }); + } + + const { schema } = schemaRes.value; + + return res.status(200).json({ schema }); + + default: + return apiError(req, res, { + status_code: 405, + api_error: { + type: "method_not_supported_error", + message: "The method passed is not supported, GET is expected.", + }, + }); + } +} + +export default withLogging(handler); diff --git a/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/tables/[tId].ts b/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/tables/[tId].ts new file mode 100644 index 000000000000..1156923c01c4 --- /dev/null +++ b/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/tables/[tId].ts @@ -0,0 +1,119 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +import { getDataSource } from "@app/lib/api/data_sources"; +import { Authenticator, getAPIKey } from "@app/lib/auth"; +import { CoreAPI, CoreAPIDatabaseTable } from "@app/lib/core_api"; +import { isDevelopmentOrDustWorkspace } from "@app/lib/development"; +import logger from "@app/logger/logger"; +import { apiError, withLogging } from "@app/logger/withlogging"; + +type GetDatabaseTableResponseBody = { + table: CoreAPIDatabaseTable; +}; + +async function handler( + req: NextApiRequest, + res: NextApiResponse +): Promise { + const keyRes = await getAPIKey(req); + if (keyRes.isErr()) { + return apiError(req, res, keyRes.error); + } + + const { auth } = await Authenticator.fromKey( + keyRes.value, + req.query.wId as string + ); + + const owner = auth.workspace(); + const plan = auth.plan(); + if (!owner || !plan) { + return apiError(req, res, { + status_code: 404, + api_error: { + type: "workspace_not_found", + message: "The workspace you requested was not found.", + }, + }); + } + + if (!isDevelopmentOrDustWorkspace(owner)) { + res.status(404).end(); + return; + } + + const dataSource = await getDataSource(auth, req.query.name as string); + if (!dataSource) { + return apiError(req, res, { + status_code: 404, + api_error: { + type: "data_source_not_found", + message: "The data source you requested was not found.", + }, + }); + } + + const databaseId = req.query.dId; + if (!databaseId || typeof databaseId !== "string") { + return apiError(req, res, { + status_code: 400, + api_error: { + type: "invalid_request_error", + message: "The database id is missing.", + }, + }); + } + + const tableId = req.query.tId; + if (!tableId || typeof tableId !== "string") { + return apiError(req, res, { + status_code: 400, + api_error: { + type: "invalid_request_error", + message: "The table id is missing.", + }, + }); + } + + switch (req.method) { + case "GET": + const tableRes = await CoreAPI.getDatabaseTable({ + projectId: dataSource.dustAPIProjectId, + dataSourceName: dataSource.name, + databaseId, + tableId, + }); + if (tableRes.isErr()) { + logger.error( + { + dataSourcename: dataSource.name, + workspaceId: owner.id, + error: tableRes.error, + }, + "Failed to get database table." + ); + return apiError(req, res, { + status_code: 500, + api_error: { + type: "internal_server_error", + message: "Failed to get database.", + }, + }); + } + + const { table } = tableRes.value; + + return res.status(200).json({ table }); + + default: + return apiError(req, res, { + status_code: 405, + api_error: { + type: "method_not_supported_error", + message: "The method passed is not supported, GET is expected.", + }, + }); + } +} + +export default withLogging(handler); diff --git a/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/tables/index.ts b/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/tables/index.ts new file mode 100644 index 000000000000..044d53829e78 --- /dev/null +++ b/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/tables/index.ts @@ -0,0 +1,173 @@ +import { isLeft } from "fp-ts/lib/Either"; +import * as t from "io-ts"; +import * as reporter from "io-ts-reporters"; +import { NextApiRequest, NextApiResponse } from "next"; + +import { getDataSource } from "@app/lib/api/data_sources"; +import { Authenticator, getAPIKey } from "@app/lib/auth"; +import { CoreAPI, CoreAPIDatabaseTable } from "@app/lib/core_api"; +import { isDevelopmentOrDustWorkspace } from "@app/lib/development"; +import { generateModelSId } from "@app/lib/utils"; +import logger from "@app/logger/logger"; +import { apiError, withLogging } from "@app/logger/withlogging"; + +type ListDatabaseTablesResponseBody = { + tables: CoreAPIDatabaseTable[]; +}; + +const UpsertDatabaseTableRequestBodySchema = t.type({ + name: t.string, + description: t.string, +}); + +type UpsertDatabaseTableResponseBody = { + table: CoreAPIDatabaseTable; +}; + +async function handler( + req: NextApiRequest, + res: NextApiResponse< + ListDatabaseTablesResponseBody | UpsertDatabaseTableResponseBody + > +): Promise { + const keyRes = await getAPIKey(req); + if (keyRes.isErr()) { + return apiError(req, res, keyRes.error); + } + + const { auth } = await Authenticator.fromKey( + keyRes.value, + req.query.wId as string + ); + + const owner = auth.workspace(); + const plan = auth.plan(); + if (!owner || !plan) { + return apiError(req, res, { + status_code: 404, + api_error: { + type: "workspace_not_found", + message: "The workspace you requested was not found.", + }, + }); + } + + if (!isDevelopmentOrDustWorkspace(owner)) { + res.status(404).end(); + return; + } + + const dataSource = await getDataSource(auth, req.query.name as string); + if (!dataSource) { + return apiError(req, res, { + status_code: 404, + api_error: { + type: "data_source_not_found", + message: "The data source you requested was not found.", + }, + }); + } + + const databaseId = req.query.dId; + if (!databaseId || typeof databaseId !== "string") { + return apiError(req, res, { + status_code: 400, + api_error: { + type: "invalid_request_error", + message: "The database id is missing.", + }, + }); + } + + switch (req.method) { + case "GET": + const tablesRes = await CoreAPI.getDatabaseTables({ + projectId: dataSource.dustAPIProjectId, + dataSourceName: dataSource.name, + databaseId, + }); + if (tablesRes.isErr()) { + logger.error( + { + dataSourcename: dataSource.name, + workspaceId: owner.id, + error: tablesRes.error, + }, + "Failed to get database tables." + ); + return apiError(req, res, { + status_code: 500, + api_error: { + type: "internal_server_error", + message: "Failed to get database tables.", + }, + }); + } + + const { tables } = tablesRes.value; + + return res.status(200).json({ tables }); + + case "POST": + const bodyValidation = UpsertDatabaseTableRequestBodySchema.decode( + req.body + ); + if (isLeft(bodyValidation)) { + const pathError = reporter.formatValidationErrors(bodyValidation.left); + return apiError(req, res, { + api_error: { + type: "invalid_request_error", + message: `Invalid request body: ${pathError}`, + }, + status_code: 400, + }); + } + const { name, description } = bodyValidation.right; + const id = generateModelSId(); + + const upsertRes = await CoreAPI.upsertDatabaseTable({ + projectId: dataSource.dustAPIProjectId, + dataSourceName: dataSource.name, + databaseId, + tableId: id, + name, + description, + }); + + if (upsertRes.isErr()) { + logger.error( + { + dataSourceName: dataSource.name, + workspaceId: owner.id, + databaseName: name, + databaseId: id, + error: upsertRes.error, + }, + "Failed to upsert database table." + ); + + return apiError(req, res, { + status_code: 500, + api_error: { + type: "internal_server_error", + message: "Failed to upsert database table.", + }, + }); + } + + const { table } = upsertRes.value; + + return res.status(200).json({ table }); + + default: + return apiError(req, res, { + status_code: 405, + api_error: { + type: "method_not_supported_error", + message: "The method passed is not supported, GET, POST is expected.", + }, + }); + } +} + +export default withLogging(handler); diff --git a/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/tables/rows/[rId].ts b/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/tables/rows/[rId].ts new file mode 100644 index 000000000000..2762939c83b1 --- /dev/null +++ b/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/tables/rows/[rId].ts @@ -0,0 +1,135 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +import { getDataSource } from "@app/lib/api/data_sources"; +import { Authenticator, getAPIKey } from "@app/lib/auth"; +import { CoreAPI, CoreAPIDatabaseRow } from "@app/lib/core_api"; +import { isDevelopmentOrDustWorkspace } from "@app/lib/development"; +import logger from "@app/logger/logger"; +import { apiError, withLogging } from "@app/logger/withlogging"; + +type GetDatabaseRowsResponseBody = { + row: CoreAPIDatabaseRow; +}; + +async function handler( + req: NextApiRequest, + res: NextApiResponse +): Promise { + const keyRes = await getAPIKey(req); + if (keyRes.isErr()) { + return apiError(req, res, keyRes.error); + } + + const { auth } = await Authenticator.fromKey( + keyRes.value, + req.query.wId as string + ); + + const owner = auth.workspace(); + const plan = auth.plan(); + if (!owner || !plan) { + return apiError(req, res, { + status_code: 404, + api_error: { + type: "workspace_not_found", + message: "The workspace you requested was not found.", + }, + }); + } + + if (!isDevelopmentOrDustWorkspace(owner)) { + res.status(404).end(); + return; + } + + const dataSource = await getDataSource(auth, req.query.name as string); + if (!dataSource) { + return apiError(req, res, { + status_code: 404, + api_error: { + type: "data_source_not_found", + message: "The data source you requested was not found.", + }, + }); + } + + const databaseId = req.query.dId; + if (!databaseId || typeof databaseId !== "string") { + return apiError(req, res, { + status_code: 400, + api_error: { + type: "invalid_request_error", + message: "The database id is missing.", + }, + }); + } + + const tableId = req.query.tId; + if (!tableId || typeof tableId !== "string") { + return apiError(req, res, { + status_code: 400, + api_error: { + type: "invalid_request_error", + message: "The table id is missing.", + }, + }); + } + + const rowId = req.query.rId; + if (!rowId || typeof rowId !== "string") { + return apiError(req, res, { + status_code: 400, + api_error: { + type: "invalid_request_error", + message: "The row id is missing.", + }, + }); + } + + switch (req.method) { + case "GET": + const rowRes = await CoreAPI.getDatabaseRow({ + projectId: dataSource.dustAPIProjectId, + dataSourceName: dataSource.name, + databaseId, + tableId, + rowId, + }); + + if (rowRes.isErr()) { + logger.error( + { + dataSourceName: dataSource.name, + workspaceId: owner.id, + databaseName: name, + databaseId: databaseId, + tableId: tableId, + error: rowRes.error, + }, + "Failed to get database row." + ); + + return apiError(req, res, { + status_code: 500, + api_error: { + type: "internal_server_error", + message: "Failed to get database row.", + }, + }); + } + + const { row } = rowRes.value; + return res.status(200).json({ row }); + + default: + return apiError(req, res, { + status_code: 405, + api_error: { + type: "method_not_supported_error", + message: "The method passed is not supported, GET is expected.", + }, + }); + } +} + +export default withLogging(handler); diff --git a/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/tables/rows/index.ts b/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/tables/rows/index.ts new file mode 100644 index 000000000000..ca052cb56fe0 --- /dev/null +++ b/front/pages/api/v1/w/[wId]/data_sources/[name]/databases/[dId]/tables/rows/index.ts @@ -0,0 +1,212 @@ +import { isLeft } from "fp-ts/lib/Either"; +import * as t from "io-ts"; +import * as reporter from "io-ts-reporters"; +import { NextApiRequest, NextApiResponse } from "next"; + +import { getDataSource } from "@app/lib/api/data_sources"; +import { Authenticator, getAPIKey } from "@app/lib/auth"; +import { CoreAPI, CoreAPIDatabaseRow } from "@app/lib/core_api"; +import { isDevelopmentOrDustWorkspace } from "@app/lib/development"; +import logger from "@app/logger/logger"; +import { apiError, withLogging } from "@app/logger/withlogging"; + +const UpsertDatabaseRowsRequestBodySchema = t.type({ + contents: t.record( + t.string, + t.record(t.string, t.union([t.string, t.null, t.number, t.boolean])) + ), + truncate: t.union([t.boolean, t.undefined]), +}); + +type UpsertDatabaseRowsResponseBody = { + success: true; +}; + +const ListDatabaseRowsReqQuerySchema = t.type({ + offset: t.number, + limit: t.number, +}); + +type ListDatabaseRowsResponseBody = { + rows: CoreAPIDatabaseRow[]; + offset: number; + limit: number; + total: number; +}; + +async function handler( + req: NextApiRequest, + res: NextApiResponse< + UpsertDatabaseRowsResponseBody | ListDatabaseRowsResponseBody + > +): Promise { + const keyRes = await getAPIKey(req); + if (keyRes.isErr()) { + return apiError(req, res, keyRes.error); + } + + const { auth } = await Authenticator.fromKey( + keyRes.value, + req.query.wId as string + ); + + const owner = auth.workspace(); + const plan = auth.plan(); + if (!owner || !plan) { + return apiError(req, res, { + status_code: 404, + api_error: { + type: "workspace_not_found", + message: "The workspace you requested was not found.", + }, + }); + } + + if (!isDevelopmentOrDustWorkspace(owner)) { + res.status(404).end(); + return; + } + + const dataSource = await getDataSource(auth, req.query.name as string); + if (!dataSource) { + return apiError(req, res, { + status_code: 404, + api_error: { + type: "data_source_not_found", + message: "The data source you requested was not found.", + }, + }); + } + + const databaseId = req.query.dId; + if (!databaseId || typeof databaseId !== "string") { + return apiError(req, res, { + status_code: 400, + api_error: { + type: "invalid_request_error", + message: "The database id is missing.", + }, + }); + } + + const tableId = req.query.tId; + if (!tableId || typeof tableId !== "string") { + return apiError(req, res, { + status_code: 400, + api_error: { + type: "invalid_request_error", + message: "The table id is missing.", + }, + }); + } + + switch (req.method) { + case "GET": + const queryValidation = ListDatabaseRowsReqQuerySchema.decode(req.query); + if (isLeft(queryValidation)) { + const pathError = reporter.formatValidationErrors(queryValidation.left); + return apiError(req, res, { + api_error: { + type: "invalid_request_error", + message: `Invalid request query: ${pathError}`, + }, + status_code: 400, + }); + } + const { offset, limit } = queryValidation.right; + + const listRes = await CoreAPI.getDatabaseRows({ + projectId: dataSource.dustAPIProjectId, + dataSourceName: dataSource.name, + databaseId, + tableId, + offset, + limit, + }); + + if (listRes.isErr()) { + logger.error( + { + dataSourceName: dataSource.name, + workspaceId: owner.id, + databaseName: name, + databaseId: databaseId, + tableId: tableId, + error: listRes.error, + }, + "Failed to list database rows." + ); + + return apiError(req, res, { + status_code: 500, + api_error: { + type: "internal_server_error", + message: "Failed to list database rows.", + }, + }); + } + + const { rows, total } = listRes.value; + return res.status(200).json({ rows, offset, limit, total }); + + case "POST": + const bodyValidation = UpsertDatabaseRowsRequestBodySchema.decode( + req.body + ); + if (isLeft(bodyValidation)) { + const pathError = reporter.formatValidationErrors(bodyValidation.left); + return apiError(req, res, { + api_error: { + type: "invalid_request_error", + message: `Invalid request body: ${pathError}`, + }, + status_code: 400, + }); + } + const { contents, truncate } = bodyValidation.right; + + const upsertRes = await CoreAPI.upsertDatabaseRows({ + projectId: dataSource.dustAPIProjectId, + dataSourceName: dataSource.name, + databaseId, + tableId: tableId, + contents, + truncate, + }); + + if (upsertRes.isErr()) { + logger.error( + { + dataSourceName: dataSource.name, + workspaceId: owner.id, + databaseName: name, + databaseId: databaseId, + tableId: tableId, + error: upsertRes.error, + }, + "Failed to upsert database rows." + ); + + return apiError(req, res, { + status_code: 500, + api_error: { + type: "internal_server_error", + message: "Failed to upsert database rows.", + }, + }); + } + + return res.status(200).json({ success: true }); + + default: + return apiError(req, res, { + status_code: 405, + api_error: { + type: "method_not_supported_error", + message: "The method passed is not supported, GET, POST is expected.", + }, + }); + } +} + +export default withLogging(handler);