From f8f9c4da8efc6ec94e1dd52fe525fd9cbfdeb7af Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 25 Oct 2024 17:32:42 +0100 Subject: [PATCH] Work in progress - this is the scaffolding for views to be added to the public API. --- packages/server/specs/resources/view.ts | 193 ++++++++++++++++++ .../src/api/controllers/public/views.ts | 56 +++++ .../server/src/api/routes/public/views.ts | 165 +++++++++++++++ .../server/src/api/routes/utils/validators.ts | 4 + 4 files changed, 418 insertions(+) create mode 100644 packages/server/specs/resources/view.ts create mode 100644 packages/server/src/api/controllers/public/views.ts create mode 100644 packages/server/src/api/routes/public/views.ts diff --git a/packages/server/specs/resources/view.ts b/packages/server/specs/resources/view.ts new file mode 100644 index 0000000000..6458af15fc --- /dev/null +++ b/packages/server/specs/resources/view.ts @@ -0,0 +1,193 @@ +import { FieldType, FormulaType, RelationshipType } from "@budibase/types" +import { object } from "./utils" +import Resource from "./utils/Resource" + +const view = { + _id: "ta_5b1649e42a5b41dea4ef7742a36a7a70", + name: "People", + schema: { + name: { + type: "string", + name: "name", + }, + age: { + type: "number", + name: "age", + }, + relationship: { + type: "link", + name: "relationship", + tableId: "ta_...", + fieldName: "relatedColumn", + relationshipType: "many-to-many", + }, + }, +} + +const baseColumnDef = { + type: { + type: "string", + enum: Object.values(FieldType), + description: + "Defines the type of the column, most explain themselves, a link column is a relationship.", + }, + constraints: { + type: "object", + description: + "A constraint can be applied to the column which will be validated against when a row is saved.", + properties: { + type: { + type: "string", + enum: ["string", "number", "object", "boolean"], + }, + presence: { + type: "boolean", + description: "Defines whether the column is required or not.", + }, + }, + }, + name: { + type: "string", + description: "The name of the column.", + }, + autocolumn: { + type: "boolean", + description: "Defines whether the column is automatically generated.", + }, +} + +const viewSchema = { + description: "The table to be created/updated.", + type: "object", + required: ["name", "schema"], + properties: { + name: { + description: "The name of the table.", + type: "string", + }, + primaryDisplay: { + type: "string", + description: + "The name of the column which should be used in relationship tags when relating to this table.", + }, + schema: { + type: "object", + additionalProperties: { + oneOf: [ + // relationship + { + type: "object", + properties: { + ...baseColumnDef, + type: { + type: "string", + enum: [FieldType.LINK], + description: "A relationship column.", + }, + fieldName: { + type: "string", + description: + "The name of the column which a relationship column is related to in another table.", + }, + tableId: { + type: "string", + description: + "The ID of the table which a relationship column is related to.", + }, + relationshipType: { + type: "string", + enum: Object.values(RelationshipType), + description: + "Defines the type of relationship that this column will be used for.", + }, + through: { + type: "string", + description: + "When using a SQL table that contains many to many relationships this defines the table the relationships are linked through.", + }, + foreignKey: { + type: "string", + description: + "When using a SQL table that contains a one to many relationship this defines the foreign key.", + }, + throughFrom: { + type: "string", + description: + "When using a SQL table that utilises a through table, this defines the primary key in the through table for this table.", + }, + throughTo: { + type: "string", + description: + "When using a SQL table that utilises a through table, this defines the primary key in the through table for the related table.", + }, + }, + }, + { + type: "object", + properties: { + ...baseColumnDef, + type: { + type: "string", + enum: [FieldType.FORMULA], + description: "A formula column.", + }, + formula: { + type: "string", + description: + "Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format.", + }, + formulaType: { + type: "string", + enum: Object.values(FormulaType), + description: + "Defines whether this is a static or dynamic formula.", + }, + }, + }, + { + type: "object", + properties: baseColumnDef, + }, + ], + }, + }, + }, +} + +const viewOutputSchema = { + ...viewSchema, + properties: { + ...viewSchema.properties, + _id: { + description: "The ID of the view.", + type: "string", + }, + }, + required: [...viewSchema.required, "_id"], +} + +export default new Resource() + .setExamples({ + view: { + value: { + data: view, + }, + }, + views: { + value: { + data: [view], + }, + }, + }) + .setSchemas({ + view: viewSchema, + viewOutput: object({ + data: viewOutputSchema, + }), + viewSearch: object({ + data: { + type: "array", + items: viewOutputSchema, + }, + }), + }) diff --git a/packages/server/src/api/controllers/public/views.ts b/packages/server/src/api/controllers/public/views.ts new file mode 100644 index 0000000000..11fb4cc344 --- /dev/null +++ b/packages/server/src/api/controllers/public/views.ts @@ -0,0 +1,56 @@ +import { search as stringSearch } from "./utils" +import * as controller from "../view" +import { ViewV2, UserCtx } from "@budibase/types" +import { Next } from "koa" + +function fixView(view: ViewV2, params: any) { + if (!params || !view) { + return view + } + if (params.viewId) { + view.id = params.viewId + } + return view +} + +export async function search(ctx: UserCtx, next: Next) { + const { name } = ctx.request.body + // TODO: need a view search endpoint + // await controller.v2.fetch(ctx) + ctx.body = stringSearch(ctx.body, name) + await next() +} + +export async function create(ctx: UserCtx, next: Next) { + await controller.v2.create(ctx) + await next() +} + +export async function read(ctx: UserCtx, next: Next) { + await controller.v2.get(ctx) + await next() +} + +export async function update(ctx: UserCtx, next: Next) { + // TODO: this is more complex - no rev on views + // ctx.request.body = await addRev( + // fixView(ctx.request.body, ctx.params), + // ctx.params.tableId + // ) + await controller.v2.update(ctx) + await next() +} + +export async function destroy(ctx: UserCtx, next: Next) { + await controller.v2.remove(ctx) + ctx.body = ctx.table + await next() +} + +export default { + create, + read, + update, + destroy, + search, +} diff --git a/packages/server/src/api/routes/public/views.ts b/packages/server/src/api/routes/public/views.ts new file mode 100644 index 0000000000..7c182d105f --- /dev/null +++ b/packages/server/src/api/routes/public/views.ts @@ -0,0 +1,165 @@ +import controller from "../../controllers/public/views" +import Endpoint from "./utils/Endpoint" +import { viewValidator, nameValidator } from "../utils/validators" + +const read = [], + write = [] + +/** + * @openapi + * /views: + * post: + * operationId: viewCreate + * summary: Create a view + * description: Create a view, this can be against an internal or external table. + * tags: + * - views + * parameters: + * - $ref: '#/components/parameters/appId' + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/view' + * examples: + * view: + * $ref: '#/components/examples/view' + * responses: + * 200: + * description: Returns the created view, including the ID which has been generated for it. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/viewOutput' + * examples: + * view: + * $ref: '#/components/examples/view' + */ +write.push( + new Endpoint("post", "/views", controller.create).addMiddleware( + viewValidator() + ) +) + +/** + * @openapi + * /views/{viewId}: + * put: + * operationId: viewUpdate + * summary: Update a view + * description: Update a view, this can be against an internal or external table. + * tags: + * - views + * parameters: + * - $ref: '#/components/parameters/viewId' + * - $ref: '#/components/parameters/appId' + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/view' + * examples: + * view: + * $ref: '#/components/examples/view' + * responses: + * 200: + * description: Returns the updated view. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/viewOutput' + * examples: + * view: + * $ref: '#/components/examples/view' + */ +write.push( + new Endpoint("put", "/views/:viewId", controller.update).addMiddleware( + viewValidator() + ) +) + +/** + * @openapi + * /views/{viewId}: + * delete: + * operationId: viewDestroy + * summary: Delete a view + * description: Delete a view, this can be against an internal or external table. + * tags: + * - views + * parameters: + * - $ref: '#/components/parameters/viewId' + * - $ref: '#/components/parameters/appId' + * responses: + * 200: + * description: Returns the deleted view. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/viewOutput' + * examples: + * view: + * $ref: '#/components/examples/view' + */ +write.push(new Endpoint("delete", "/views/:viewId", controller.destroy)) + +/** + * @openapi + * /views/{viewId}: + * get: + * operationId: viewGetById + * summary: Retrieve a view + * description: Lookup a view, this could be internal or external. + * tags: + * - views + * parameters: + * - $ref: '#/components/parameters/viewId' + * - $ref: '#/components/parameters/appId' + * responses: + * 200: + * description: Returns the retrieved view. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/viewOutput' + * examples: + * view: + * $ref: '#/components/examples/view' + */ +read.push(new Endpoint("get", "/views/:viewId", controller.read)) + +/** + * @openapi + * /views/search: + * post: + * operationId: viewSearch + * summary: Search for views + * description: Based on view properties (currently only name) search for views. + * tags: + * - views + * parameters: + * - $ref: '#/components/parameters/appId' + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/viewSearch' + * responses: + * 200: + * description: Returns the found views, based on the search parameters. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/viewSearch' + * examples: + * views: + * $ref: '#/components/examples/views' + */ +read.push( + new Endpoint("post", "/views/search", controller.search).addMiddleware( + nameValidator() + ) +) + +export default { read, write } diff --git a/packages/server/src/api/routes/utils/validators.ts b/packages/server/src/api/routes/utils/validators.ts index 862d8c30c5..68ebd72c5e 100644 --- a/packages/server/src/api/routes/utils/validators.ts +++ b/packages/server/src/api/routes/utils/validators.ts @@ -66,6 +66,10 @@ export function tableValidator() { ) } +export function viewValidator() { + return auth.joiValidator.body(Joi.object()) +} + export function nameValidator() { return auth.joiValidator.body( Joi.object({