diff --git a/packages/server/src/api/controllers/view/index.ts b/packages/server/src/api/controllers/view/index.ts index 95b4b2e143..0d66969dd6 100644 --- a/packages/server/src/api/controllers/view/index.ts +++ b/packages/server/src/api/controllers/view/index.ts @@ -1,2 +1,2 @@ -export * as v1 from "./legacyViews" -export * as v2 from "./views" +export * as v1 from "./views" +export * as v2 from "./viewsV2" diff --git a/packages/server/src/api/controllers/view/legacyViews.ts b/packages/server/src/api/controllers/view/legacyViews.ts deleted file mode 100644 index 99c4224c62..0000000000 --- a/packages/server/src/api/controllers/view/legacyViews.ts +++ /dev/null @@ -1,195 +0,0 @@ -import viewTemplate from "./viewBuilder" -import { apiFileReturn } from "../../../utilities/fileSystem" -import { csv, json, jsonWithSchema, Format, isFormat } from "./exporters" -import { deleteView, getView, getViews, saveView } from "./utils" -import { fetchView } from "../row" -import { context, events } from "@budibase/backend-core" -import { DocumentType } from "../../../db/utils" -import sdk from "../../../sdk" -import { FieldTypes } from "../../../constants" -import { - Ctx, - Row, - Table, - TableExportFormat, - TableSchema, - View, -} from "@budibase/types" -import { builderSocket } from "../../../websockets" - -const { cloneDeep, isEqual } = require("lodash") - -export async function fetch(ctx: Ctx) { - ctx.body = await getViews() -} - -export async function save(ctx: Ctx) { - const db = context.getAppDB() - const { originalName, ...viewToSave } = ctx.request.body - - const existingTable = await sdk.tables.getTable(ctx.request.body.tableId) - existingTable.views ??= {} - const table = cloneDeep(existingTable) - - const groupByField: any = Object.values(table.schema).find( - (field: any) => field.name == viewToSave.groupBy - ) - - const view = viewTemplate(viewToSave, groupByField?.type === FieldTypes.ARRAY) - const viewName = viewToSave.name - - if (!viewName) { - ctx.throw(400, "Cannot create view without a name") - } - - await saveView(originalName, viewName, view) - - // add views to table document - if (!table.views) table.views = {} - if (!view.meta.schema) { - view.meta.schema = table.schema - } - table.views[viewName] = { ...view.meta, name: viewName } - if (originalName) { - delete table.views[originalName] - existingTable.views[viewName] = existingTable.views[originalName] - } - await db.put(table) - await handleViewEvents(existingTable.views[viewName], table.views[viewName]) - - ctx.body = table.views[viewName] - builderSocket?.emitTableUpdate(ctx, table) -} - -export async function calculationEvents(existingView: View, newView: View) { - const existingCalculation = existingView && existingView.calculation - const newCalculation = newView && newView.calculation - - if (existingCalculation && !newCalculation) { - await events.view.calculationDeleted(existingView) - } - - if (!existingCalculation && newCalculation) { - await events.view.calculationCreated(newView) - } - - if ( - existingCalculation && - newCalculation && - existingCalculation !== newCalculation - ) { - await events.view.calculationUpdated(newView) - } -} - -export async function filterEvents(existingView: View, newView: View) { - const hasExistingFilters = !!( - existingView && - existingView.filters && - existingView.filters.length - ) - const hasNewFilters = !!(newView && newView.filters && newView.filters.length) - - if (hasExistingFilters && !hasNewFilters) { - await events.view.filterDeleted(newView) - } - - if (!hasExistingFilters && hasNewFilters) { - await events.view.filterCreated(newView) - } - - if ( - hasExistingFilters && - hasNewFilters && - !isEqual(existingView.filters, newView.filters) - ) { - await events.view.filterUpdated(newView) - } -} - -async function handleViewEvents(existingView: View, newView: View) { - if (!existingView) { - await events.view.created(newView) - } else { - await events.view.updated(newView) - } - await calculationEvents(existingView, newView) - await filterEvents(existingView, newView) -} - -export async function destroy(ctx: Ctx) { - const db = context.getAppDB() - const viewName = decodeURIComponent(ctx.params.viewName) - const view = await deleteView(viewName) - const table = await sdk.tables.getTable(view.meta.tableId) - delete table.views![viewName] - await db.put(table) - await events.view.deleted(view) - - ctx.body = view - builderSocket?.emitTableUpdate(ctx, table) -} - -export async function exportView(ctx: Ctx) { - const viewName = decodeURIComponent(ctx.query.view as string) - const view = await getView(viewName) - - const format = ctx.query.format as unknown - - if (!isFormat(format)) { - ctx.throw( - 400, - "Format must be specified, either csv, json or jsonWithSchema" - ) - } - - if (view) { - ctx.params.viewName = viewName - // Fetch view rows - ctx.query = { - group: view.meta.groupBy, - calculation: view.meta.calculation, - // @ts-ignore - stats: !!view.meta.field, - field: view.meta.field, - } - } else { - // table all_ view - /* istanbul ignore next */ - ctx.params.viewName = viewName - } - - await fetchView(ctx) - let rows = ctx.body as Row[] - - let schema: TableSchema = view && view.meta && view.meta.schema - const tableId = - ctx.params.tableId || - view?.meta?.tableId || - (viewName.startsWith(DocumentType.TABLE) && viewName) - const table: Table = await sdk.tables.getTable(tableId) - if (!schema) { - schema = table.schema - } - - let exportRows = sdk.rows.utils.cleanExportRows(rows, schema, format, []) - - if (format === Format.CSV) { - ctx.attachment(`${viewName}.csv`) - ctx.body = apiFileReturn(csv(Object.keys(schema), exportRows)) - } else if (format === Format.JSON) { - ctx.attachment(`${viewName}.json`) - ctx.body = apiFileReturn(json(exportRows)) - } else if (format === Format.JSON_WITH_SCHEMA) { - ctx.attachment(`${viewName}.json`) - ctx.body = apiFileReturn(jsonWithSchema(schema, exportRows)) - } else { - throw "Format not recognised" - } - - if (viewName.startsWith(DocumentType.TABLE)) { - await events.table.exported(table, format as TableExportFormat) - } else { - await events.view.exported(table, format as TableExportFormat) - } -} diff --git a/packages/server/src/api/controllers/view/views.ts b/packages/server/src/api/controllers/view/views.ts index 47f9d645a0..99c4224c62 100644 --- a/packages/server/src/api/controllers/view/views.ts +++ b/packages/server/src/api/controllers/view/views.ts @@ -1,40 +1,195 @@ +import viewTemplate from "./viewBuilder" +import { apiFileReturn } from "../../../utilities/fileSystem" +import { csv, json, jsonWithSchema, Format, isFormat } from "./exporters" +import { deleteView, getView, getViews, saveView } from "./utils" +import { fetchView } from "../row" +import { context, events } from "@budibase/backend-core" +import { DocumentType } from "../../../db/utils" import sdk from "../../../sdk" -import { Ctx, ViewV2 } from "@budibase/types" +import { FieldTypes } from "../../../constants" +import { + Ctx, + Row, + Table, + TableExportFormat, + TableSchema, + View, +} from "@budibase/types" +import { builderSocket } from "../../../websockets" + +const { cloneDeep, isEqual } = require("lodash") export async function fetch(ctx: Ctx) { - const { tableId } = ctx.query + ctx.body = await getViews() +} - if (tableId && typeof tableId !== "string") { - ctx.throw(400, "tableId type is not valid") +export async function save(ctx: Ctx) { + const db = context.getAppDB() + const { originalName, ...viewToSave } = ctx.request.body + + const existingTable = await sdk.tables.getTable(ctx.request.body.tableId) + existingTable.views ??= {} + const table = cloneDeep(existingTable) + + const groupByField: any = Object.values(table.schema).find( + (field: any) => field.name == viewToSave.groupBy + ) + + const view = viewTemplate(viewToSave, groupByField?.type === FieldTypes.ARRAY) + const viewName = viewToSave.name + + if (!viewName) { + ctx.throw(400, "Cannot create view without a name") } - const views = tableId - ? await sdk.views.findByTable(tableId) - : await sdk.views.fetch() + await saveView(originalName, viewName, view) - ctx.body = { views } + // add views to table document + if (!table.views) table.views = {} + if (!view.meta.schema) { + view.meta.schema = table.schema + } + table.views[viewName] = { ...view.meta, name: viewName } + if (originalName) { + delete table.views[originalName] + existingTable.views[viewName] = existingTable.views[originalName] + } + await db.put(table) + await handleViewEvents(existingTable.views[viewName], table.views[viewName]) + + ctx.body = table.views[viewName] + builderSocket?.emitTableUpdate(ctx, table) } -export async function find(ctx: Ctx) { - const { viewId } = ctx.params +export async function calculationEvents(existingView: View, newView: View) { + const existingCalculation = existingView && existingView.calculation + const newCalculation = newView && newView.calculation - const result = await sdk.views.get(viewId) - ctx.body = result -} + if (existingCalculation && !newCalculation) { + await events.view.calculationDeleted(existingView) + } -export async function save(ctx: Ctx) { - const view = ctx.request.body - const result = await sdk.views.save(view) - ctx.body = { - ...view, - ...result, + if (!existingCalculation && newCalculation) { + await events.view.calculationCreated(newView) + } + + if ( + existingCalculation && + newCalculation && + existingCalculation !== newCalculation + ) { + await events.view.calculationUpdated(newView) } } -export async function remove(ctx: Ctx) { - const { viewId } = ctx.params - const { _rev } = await sdk.views.get(viewId) +export async function filterEvents(existingView: View, newView: View) { + const hasExistingFilters = !!( + existingView && + existingView.filters && + existingView.filters.length + ) + const hasNewFilters = !!(newView && newView.filters && newView.filters.length) - await sdk.views.remove(viewId, _rev) - ctx.status = 204 + if (hasExistingFilters && !hasNewFilters) { + await events.view.filterDeleted(newView) + } + + if (!hasExistingFilters && hasNewFilters) { + await events.view.filterCreated(newView) + } + + if ( + hasExistingFilters && + hasNewFilters && + !isEqual(existingView.filters, newView.filters) + ) { + await events.view.filterUpdated(newView) + } +} + +async function handleViewEvents(existingView: View, newView: View) { + if (!existingView) { + await events.view.created(newView) + } else { + await events.view.updated(newView) + } + await calculationEvents(existingView, newView) + await filterEvents(existingView, newView) +} + +export async function destroy(ctx: Ctx) { + const db = context.getAppDB() + const viewName = decodeURIComponent(ctx.params.viewName) + const view = await deleteView(viewName) + const table = await sdk.tables.getTable(view.meta.tableId) + delete table.views![viewName] + await db.put(table) + await events.view.deleted(view) + + ctx.body = view + builderSocket?.emitTableUpdate(ctx, table) +} + +export async function exportView(ctx: Ctx) { + const viewName = decodeURIComponent(ctx.query.view as string) + const view = await getView(viewName) + + const format = ctx.query.format as unknown + + if (!isFormat(format)) { + ctx.throw( + 400, + "Format must be specified, either csv, json or jsonWithSchema" + ) + } + + if (view) { + ctx.params.viewName = viewName + // Fetch view rows + ctx.query = { + group: view.meta.groupBy, + calculation: view.meta.calculation, + // @ts-ignore + stats: !!view.meta.field, + field: view.meta.field, + } + } else { + // table all_ view + /* istanbul ignore next */ + ctx.params.viewName = viewName + } + + await fetchView(ctx) + let rows = ctx.body as Row[] + + let schema: TableSchema = view && view.meta && view.meta.schema + const tableId = + ctx.params.tableId || + view?.meta?.tableId || + (viewName.startsWith(DocumentType.TABLE) && viewName) + const table: Table = await sdk.tables.getTable(tableId) + if (!schema) { + schema = table.schema + } + + let exportRows = sdk.rows.utils.cleanExportRows(rows, schema, format, []) + + if (format === Format.CSV) { + ctx.attachment(`${viewName}.csv`) + ctx.body = apiFileReturn(csv(Object.keys(schema), exportRows)) + } else if (format === Format.JSON) { + ctx.attachment(`${viewName}.json`) + ctx.body = apiFileReturn(json(exportRows)) + } else if (format === Format.JSON_WITH_SCHEMA) { + ctx.attachment(`${viewName}.json`) + ctx.body = apiFileReturn(jsonWithSchema(schema, exportRows)) + } else { + throw "Format not recognised" + } + + if (viewName.startsWith(DocumentType.TABLE)) { + await events.table.exported(table, format as TableExportFormat) + } else { + await events.view.exported(table, format as TableExportFormat) + } } diff --git a/packages/server/src/api/controllers/view/viewsV2.ts b/packages/server/src/api/controllers/view/viewsV2.ts new file mode 100644 index 0000000000..47f9d645a0 --- /dev/null +++ b/packages/server/src/api/controllers/view/viewsV2.ts @@ -0,0 +1,40 @@ +import sdk from "../../../sdk" +import { Ctx, ViewV2 } from "@budibase/types" + +export async function fetch(ctx: Ctx) { + const { tableId } = ctx.query + + if (tableId && typeof tableId !== "string") { + ctx.throw(400, "tableId type is not valid") + } + + const views = tableId + ? await sdk.views.findByTable(tableId) + : await sdk.views.fetch() + + ctx.body = { views } +} + +export async function find(ctx: Ctx) { + const { viewId } = ctx.params + + const result = await sdk.views.get(viewId) + ctx.body = result +} + +export async function save(ctx: Ctx) { + const view = ctx.request.body + const result = await sdk.views.save(view) + ctx.body = { + ...view, + ...result, + } +} + +export async function remove(ctx: Ctx) { + const { viewId } = ctx.params + const { _rev } = await sdk.views.get(viewId) + + await sdk.views.remove(viewId, _rev) + ctx.status = 204 +} diff --git a/packages/server/src/api/routes/tests/__snapshots__/legacyView.spec.js.snap b/packages/server/src/api/routes/tests/__snapshots__/view.spec.js.snap similarity index 100% rename from packages/server/src/api/routes/tests/__snapshots__/legacyView.spec.js.snap rename to packages/server/src/api/routes/tests/__snapshots__/view.spec.js.snap diff --git a/packages/server/src/api/routes/tests/legacyView.spec.js b/packages/server/src/api/routes/tests/view.spec.js similarity index 100% rename from packages/server/src/api/routes/tests/legacyView.spec.js rename to packages/server/src/api/routes/tests/view.spec.js diff --git a/packages/server/src/api/routes/tests/view.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts similarity index 100% rename from packages/server/src/api/routes/tests/view.spec.ts rename to packages/server/src/api/routes/tests/viewV2.spec.ts