diff --git a/packages/nc-gui/lang/da.json b/packages/nc-gui/lang/da.json index e67b4258f8..ff663368a2 100644 --- a/packages/nc-gui/lang/da.json +++ b/packages/nc-gui/lang/da.json @@ -1,31 +1,31 @@ { "general": { - "home": "Hjem", - "load": "belastning", - "open": "Åben", - "close": "Tæt", + "home": "Forside", + "load": "Indlæs", + "open": "Åbn", + "close": "Luk", "yes": "Ja", - "no": "Ingen", + "no": "Nej", "ok": "Okay", "and": "Og", "or": "Eller", - "add": "Tilføje", - "edit": "Redigere", - "remove": "Fjerne", - "save": "Gemme", - "cancel": "Afbestille", + "add": "Tilføj", + "edit": "Redigér", + "remove": "Fjern", + "save": "Gem", + "cancel": "Fortryd", "submit": "Indsend", - "create": "skab", + "create": "Opret", "duplicate": "Duplikat", - "insert": "Indsættes", - "delete": "Delete.", - "update": "UPDATE.", + "insert": "Indsæt", + "delete": "Slet", + "update": "Opdatér", "rename": "Omdøb", - "reload": "Genindlæsning", + "reload": "Genindlæs", "reset": "Nulstil", - "install": "Installere", - "show": "At vise", - "hide": "Skjule", + "install": "Installer", + "show": "Vis", + "hide": "Skjul", "showAll": "Vis alt", "hideAll": "Gem alt", "showMore": "Vis mere", @@ -75,7 +75,7 @@ "hideField": "Skjul felt", "sortAsc": "Sortere stigende", "sortDesc": "Sortere nedadgående", - "geoDataField": "GeoData Field" + "geoDataField": "GeoData-felt" }, "objects": { "project": "Projekt", @@ -92,15 +92,15 @@ "records": "Optegnelser.", "webhook": "WebHook.", "webhooks": "Webhooks.", - "view": "Udsigt", - "views": "Visninger.", + "view": "Visning", + "views": "Visninger", "viewType": { - "grid": "Grid.", + "grid": "Grid", "gallery": "Galleri", "form": "Formular", "kanban": "Kanban.", "calendar": "Kalender", - "map": "Map" + "map": "Kort" }, "user": "Bruger", "users": "Brugere", @@ -209,7 +209,7 @@ "advancedSettings": "Avancerede indstillinger", "codeSnippet": "Kodeuddrag", "keyboardShortcut": "Tastaturgenveje", - "generateRandomName": "Generate Random Name" + "generateRandomName": "Generér Tilfældigt Navn" }, "labels": { "createdBy": "Oprettet af", @@ -217,7 +217,7 @@ "projName": "Projekt navn", "tableName": "Tabelnavn.", "viewName": "Se navn", - "viewLink": "Se Link.", + "viewLink": "Vis Link", "columnName": "Kolonne navn", "columnType": "Kolonne type", "roleName": "Rolle navn", @@ -235,7 +235,7 @@ "action": "Handling", "actions": "Handlinger", "operation": "Operation", - "operationSub": "Sub Operation", + "operationSub": "Underordnet operation", "operationType": "Driftstype", "operationSubType": "Drift Undertype", "description": "Beskrivelse", @@ -257,7 +257,7 @@ "barcodeFormat": "Stregkodeformat", "qrCodeValueTooLong": "For mange tegn til en QR-kode", "barcodeValueTooLong": "For mange tegn til en stregkode", - "yourLocation": "Your Location", + "yourLocation": "Din Placering", "lng": "Lng", "lat": "Lat", "aggregateFunction": "Aggregate Function.", @@ -283,8 +283,8 @@ }, "docReference": "Dokumentreference.", "selectUserRole": "Vælg brugerrolle", - "childTable": "Børnebord", - "childColumn": "Barn kolonne", + "childTable": "Undertabel", + "childColumn": "Underkolonner", "linkToAnotherRecord": "Link til en anden post", "onUpdate": "På opdatering", "onDelete": "På Delete.", @@ -379,36 +379,36 @@ "reloadRoles": "Genindlæs roller", "nextPage": "Næste side", "prevPage": "Forrige side", - "nextRecord": "Næste rekord.", - "previousRecord": "Tidligere rekord.", + "nextRecord": "Næste post", + "previousRecord": "Forrige post", "copyApiURL": "COPY API URL.", - "createTable": "Tabel Create.", - "refreshTable": "Tabeller opdatere", - "renameTable": "Bord omdøb", - "deleteTable": "TABEL DELETE.", + "createTable": "Opret tabel", + "refreshTable": "Genopfrisk Tabeller", + "renameTable": "Omdøb Tabel", + "deleteTable": "Slet Tabel", "addField": "Tilføj nyt felt til denne tabel", - "setDisplay": "Set as Display value", - "addRow": "Tilføj ny række", - "saveRow": "Gem ro", + "setDisplay": "Sæt som visningsværdi", + "addRow": "Tilføj ny post", + "saveRow": "Gem række", "saveAndExit": "Gem og afslutning", "saveAndStay": "Gem og bliv", "insertRow": "Indsæt ny række", - "deleteRow": "DELETE ROW.", - "duplicateRow": "Duplicate Row", + "deleteRow": "Slet Række", + "duplicateRow": "Dupliker Række", "deleteSelectedRow": "Slet de valgte rækker", "importExcel": "Import Excel.", "importCSV": "Import CSV.", "downloadCSV": "Download som CSV.", "downloadExcel": "Download som XLSX", "uploadCSV": "Upload CSV.", - "import": "Importere", + "import": "Import", "importMetadata": "Import metadata.", "exportMetadata": "Eksport metadata.", "clearMetadata": "Klare metadata.", "exportToFile": "Eksporter til filer", "changePwd": "Skift kodeord", "createView": "Opret en visning", - "shareView": "Del View", + "shareView": "Del Visning", "listSharedView": "Shared View List.", "ListView": "Visninger List", "copyView": "Kopi visning", @@ -429,23 +429,23 @@ "importZip": "Import Zip.", "metaSync": "Synkroniser nu", "settings": "Indstillinger.", - "previewAs": "Forhåndsvisning så", - "resetReview": "Nulstil preview.", - "testDbConn": "Test Database Connection.", + "previewAs": "Forhåndsvisning som", + "resetReview": "Nulstil forhåndsvisning", + "testDbConn": "Test Database Forbindelse", "removeDbFromEnv": "Fjern databasen fra miljøet", "editConnJson": "Rediger forbindelse JSON", - "sponsorUs": "Sponsor os", + "sponsorUs": "Sponsorer os", "sendEmail": "SEND E-MAIL", "addUserToProject": "Tilføj bruger til projekt", "getApiSnippet": "Hent API-snippet", - "clearCell": "Klar celle", + "clearCell": "Ryd celle", "addFilterGroup": "Tilføj filtergruppe", "linkRecord": "Link record", "addNewRecord": "Tilføj ny post", "useConnectionUrl": "Brug forbindelses-URL", "toggleCommentsDraw": "Toggle kommentarer tegne", "expandRecord": "Udvid optegnelse", - "deleteRecord": "Slet registrering", + "deleteRecord": "Slet Post", "erd": { "showColumns": "Vis kolonner", "showPkAndFk": "Vis primære og fremmede nøgler", @@ -461,8 +461,8 @@ "addOrEditStack": "Tilføj / Rediger stak" }, "map": { - "mappedBy": "Mapped By", - "chooseMappingField": "Choose a Mapping Field" + "mappedBy": "Kortlagt af", + "chooseMappingField": "Vælg et kortlægningsfelt" } }, "tooltip": { @@ -530,9 +530,9 @@ "orgViewer": "Seeren har ikke lov til at oprette nye projekter, men kan få adgang til alle inviterede projekter." }, "map": { - "overLimit": "You're over the limit.", - "closeLimit": "You're getting close to the limit.", - "limitNumber": "The limit of markers shown in a Map View is 1000 records." + "overLimit": "Du er over grænsen.", + "closeLimit": "Du nærmer dig grænsen.", + "limitNumber": "Grænsen for antallet af markeringer, der vises i en kortvisning, er 1000 poster." }, "footerInfo": "Rækker per side", "upload": "Vælg fil for at uploade", @@ -616,7 +616,7 @@ "gallery": "Tilføj Gallery View.", "form": "Tilføj formularvisning", "kanban": "Tilføj Kanban View.", - "map": "Add Map View", + "map": "Tilføj Kortvisning", "calendar": "Tilføj kalendervisning" }, "tablesMetadataInSync": "Tabeller Metadata er synkroniseret", @@ -648,11 +648,11 @@ "deleteViewConfirmation": "Er du sikker på, at du vil slette denne visning?", "deleteTableConfirmation": "Ønsker du at slette tabellen", "showM2mTables": "Vis M2M-tabeller", - "showM2mTablesDesc": "Many-to-many relation is supported via a junction table & is hidden by default. Enable this option to list all such tables along with existing tables.", - "showNullInCells": "Show NULL in Cells", + "showM2mTablesDesc": "Mange-til-mange-relationer understøttes via en sammenknytnings-tabel (junction table) og er skjult som standard. Aktiver denne indstilling for at få vist alle sådanne tabeller sammen med eksisterende tabeller.", + "showNullInCells": "Vis NULL i celler", "showNullInCellsDesc": "Display 'NULL' tag in cells holding NULL value. This helps differentiate against cells holding EMPTY string.", - "showNullAndEmptyInFilter": "Show NULL and EMPTY in Filter", - "showNullAndEmptyInFilterDesc": "Enable 'additional' filters to differentiate fields containing NULL & Empty Strings. Default support for Blank treats both NULL & Empty strings alike.", + "showNullAndEmptyInFilter": "Vis NULL og EMPTY i Filter", + "showNullAndEmptyInFilterDesc": "Aktiver \"yderligere\" filtre for at skelne mellem felter, der indeholder NULL og tomme strenge. Standardunderstøttelse for Blank behandler både NULL- og tomme strenge ens.", "deleteKanbanStackConfirmation": "Hvis du sletter denne stak, fjernes også valgmuligheden `{stackToBeDeleted}` fra `{groupingField}`. Posterne vil blive flyttet til stakken \"uncategorized\".", "computedFieldEditWarning": "Beregnet felt: indholdet er skrivebeskyttet. Brug kolonne-redigeringsmenuen til at omkonfigurere", "computedFieldDeleteWarning": "Beregnet felt: indholdet er skrivebeskyttet. Det er ikke muligt at slette indholdet.", @@ -707,7 +707,7 @@ "nameShouldStartWithAnAlphabetOr_": "Navnet skal starte med et alfabet eller _", "followingCharactersAreNotAllowed": "Følgende tegn er ikke tilladt", "columnNameRequired": "Kolonnens navn er påkrævet", - "columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters", + "columnNameExceedsCharacters": "Længden af kolonnenavnet overstiger maks. {value} tegn", "projectNameExceeds50Characters": "Projektnavnet overstiger 50 tegn", "projectNameCannotStartWithSpace": "Projektnavnet kan ikke begynde med et mellemrum", "requiredField": "Obligatorisk felt", @@ -740,7 +740,7 @@ }, "success": { "columnDuplicated": "Kolonne duplikeret med succes", - "rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)", + "rowDuplicatedWithoutSavedYet": "Række duplikeret (ikke gemt)", "updatedUIACL": "Opdateret UI ACL for tabeller med succes", "pluginUninstalled": "Plugin afinstalleret med succes", "pluginSettingsSaved": "Plugin-indstillingerne er gemt med succes", diff --git a/packages/noco-docs/content/en/setup-and-usages/table-operations.md b/packages/noco-docs/content/en/setup-and-usages/table-operations.md index 2aeeae8c43..f2b4b8ac61 100644 --- a/packages/noco-docs/content/en/setup-and-usages/table-operations.md +++ b/packages/noco-docs/content/en/setup-and-usages/table-operations.md @@ -70,7 +70,8 @@ After the click, it will show a menu and you can enter the column name and choos You can also click `Show more` for additional menu options. -image +![Screenshot 2023-03-03 at 8 13 07 PM](https://user-images.githubusercontent.com/86527202/222749857-0e793db2-a5d2-4b54-8d23-2a0cbbec8f5d.png) + Click `Save` button to create the new column. diff --git a/packages/nocodb-sdk/src/lib/Api.ts b/packages/nocodb-sdk/src/lib/Api.ts index 865e4c994f..b421ae426d 100644 --- a/packages/nocodb-sdk/src/lib/Api.ts +++ b/packages/nocodb-sdk/src/lib/Api.ts @@ -2159,6 +2159,7 @@ export interface ViewColumnReqType { * Model for Visibility Rule Request */ export type VisibilityRuleReqType = { + id?: string | null; disabled?: { /** Model for Bool */ commenter?: BoolType; diff --git a/packages/nocodb-sdk/src/lib/sqlUi/OracleUi.ts b/packages/nocodb-sdk/src/lib/sqlUi/OracleUi.ts index 9dafe83191..3498a0835e 100644 --- a/packages/nocodb-sdk/src/lib/sqlUi/OracleUi.ts +++ b/packages/nocodb-sdk/src/lib/sqlUi/OracleUi.ts @@ -1,3 +1,4 @@ +import { NormalColumnRequestType } from '../Api' import UITypes from '../UITypes'; import { IDType } from './index'; @@ -794,7 +795,10 @@ export class OracleUi { } } - static getDataTypeForUiType(col: { uidt?: UITypes }, idType?: IDType) { + static getDataTypeForUiType( + col: { uidt: UITypes | NormalColumnRequestType['uidt'] }, + idType?: IDType + ) { const colProp: any = {}; switch (col.uidt) { case 'ID': diff --git a/packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts b/packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts index 6e79066cad..3eef12622a 100644 --- a/packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts +++ b/packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts @@ -1,3 +1,4 @@ +import { BoolType } from '../Api' import UITypes from '../UITypes'; import { MssqlUi } from './MssqlUi'; @@ -56,12 +57,12 @@ export type SqlUIColumn = { dt?: string; dtx?: string; ct?: string; - nrqd?: boolean; - rqd?: boolean; + nrqd?: BoolType; + rqd?: BoolType; ck?: string; - pk?: boolean; - un?: boolean; - ai?: boolean; + pk?: BoolType; + un?: BoolType; + ai?: BoolType; cdf?: string | any; clen?: number | any; np?: string; diff --git a/packages/nocodb/package-lock.json b/packages/nocodb/package-lock.json index 94179fd694..8783f36d99 100644 --- a/packages/nocodb/package-lock.json +++ b/packages/nocodb/package-lock.json @@ -66,7 +66,7 @@ "multer": "^1.4.2", "mysql2": "^2.2.5", "nanoid": "^3.1.20", - "nc-help": "0.2.85", + "nc-help": "0.2.87", "nc-lib-gui": "0.105.3", "nc-plugin": "0.1.2", "ncp": "^2.0.0", @@ -11311,9 +11311,9 @@ } }, "node_modules/nc-help": { - "version": "0.2.85", - "resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.85.tgz", - "integrity": "sha512-EOyrc2PuRUJzv73jHNHmUR6YhC0TlJG0DTY/sug7BF4MJAVPJgyavJnrqkRC7g0NS4xociki9gs5MbLRjlRwtQ==", + "version": "0.2.87", + "resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.87.tgz", + "integrity": "sha512-Zlg06ialvylBEE1qtvjlNKxZrPShzXwvy3WG7nfw+8GngOkQBCTlKguejT2Kq4Gfb5378WPX1APXtsetMKBrRA==", "dependencies": { "@rudderstack/rudder-sdk-node": "^1.1.3", "axios": "^0.21.1", @@ -28029,9 +28029,9 @@ "integrity": "sha512-3AryS9uwa5NfISLxMciUonrH7YfXp+nlahB9T7girXIsLQrmwX4MdnuKs32akduCOGpKmjTJSWmATULbuMkbfw==" }, "nc-help": { - "version": "0.2.85", - "resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.85.tgz", - "integrity": "sha512-EOyrc2PuRUJzv73jHNHmUR6YhC0TlJG0DTY/sug7BF4MJAVPJgyavJnrqkRC7g0NS4xociki9gs5MbLRjlRwtQ==", + "version": "0.2.87", + "resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.87.tgz", + "integrity": "sha512-Zlg06ialvylBEE1qtvjlNKxZrPShzXwvy3WG7nfw+8GngOkQBCTlKguejT2Kq4Gfb5378WPX1APXtsetMKBrRA==", "requires": { "@rudderstack/rudder-sdk-node": "^1.1.3", "axios": "^0.21.1", diff --git a/packages/nocodb/package.json b/packages/nocodb/package.json index 9dee15b3f9..617f60d5b4 100644 --- a/packages/nocodb/package.json +++ b/packages/nocodb/package.json @@ -107,7 +107,7 @@ "multer": "^1.4.2", "mysql2": "^2.2.5", "nanoid": "^3.1.20", - "nc-help": "0.2.85", + "nc-help": "0.2.87", "nc-lib-gui": "0.105.3", "nc-plugin": "0.1.2", "ncp": "^2.0.0", diff --git a/packages/nocodb/src/lib/Noco.ts b/packages/nocodb/src/lib/Noco.ts index 9099de28b4..151f211c65 100644 --- a/packages/nocodb/src/lib/Noco.ts +++ b/packages/nocodb/src/lib/Noco.ts @@ -21,7 +21,7 @@ import { NC_LICENSE_KEY } from './constants'; import Migrator from './db/sql-migrator/lib/KnexMigrator'; import Store from './models/Store'; import NcConfigFactory from './utils/NcConfigFactory'; -import { Tele } from 'nc-help'; +import { T } from 'nc-help'; import NcProjectBuilderCE from './v1-legacy/NcProjectBuilder'; import NcProjectBuilderEE from './v1-legacy/NcProjectBuilderEE'; @@ -45,7 +45,7 @@ import User from './models/User'; import * as http from 'http'; import weAreHiring from './utils/weAreHiring'; import getInstance from './utils/getInstance'; -import initAdminFromEnv from './meta/api/userApi/initAdminFromEnv'; +import initAdminFromEnv from './services/userService/initAdminFromEnv'; const log = debug('nc:app'); require('dotenv').config(); @@ -275,10 +275,10 @@ export default class Noco { } next(); }); - Tele.init({ + T.init({ instance: getInstance, }); - Tele.emit('evt_app_started', await User.count()); + T.emit('evt_app_started', await User.count()); console.log(`App started successfully.\nVisit -> ${Noco.dashboardUrl}`); weAreHiring(); return this.router; @@ -531,7 +531,7 @@ export default class Noco { if (!serverId) { await Noco._ncMeta.metaInsert('', '', 'nc_store', { key: 'nc_server_id', - value: (serverId = Tele.id), + value: (serverId = T.id), }); } process.env.NC_SERVER_UUID = serverId; diff --git a/packages/nocodb/src/lib/controllers/apiTokenController.ts b/packages/nocodb/src/lib/controllers/apiTokenController.ts new file mode 100644 index 0000000000..d472c9a800 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/apiTokenController.ts @@ -0,0 +1,49 @@ +import { Request, Response, Router } from 'express'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import { apiTokenService } from '../services'; + +export async function apiTokenList(req: Request, res: Response) { + res.json(await apiTokenService.apiTokenList({ userId: req['user'].id })); +} + +export async function apiTokenCreate(req: Request, res: Response) { + res.json( + await apiTokenService.apiTokenCreate({ + tokenBody: req.body, + userId: req['user'].id, + }) + ); +} + +export async function apiTokenDelete(req: Request, res: Response) { + res.json( + await apiTokenService.apiTokenDelete({ + token: req.params.token, + user: req['user'], + }) + ); +} + +// todo: add reset token api to regenerate token + +// deprecated apis +const router = Router({ mergeParams: true }); + +router.get( + '/api/v1/db/meta/projects/:projectId/api-tokens', + metaApiMetrics, + ncMetaAclMw(apiTokenList, 'apiTokenList') +); +router.post( + '/api/v1/db/meta/projects/:projectId/api-tokens', + metaApiMetrics, + ncMetaAclMw(apiTokenCreate, 'apiTokenCreate') +); +router.delete( + '/api/v1/db/meta/projects/:projectId/api-tokens/:token', + metaApiMetrics, + ncMetaAclMw(apiTokenDelete, 'apiTokenDelete') +); + +export default router; diff --git a/packages/nocodb/src/lib/controllers/attachmentController.ts b/packages/nocodb/src/lib/controllers/attachmentController.ts new file mode 100644 index 0000000000..8b9bd0eff7 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/attachmentController.ts @@ -0,0 +1,127 @@ +import { Request, Response, Router } from 'express'; +import multer from 'multer'; +import { OrgUserRoles, ProjectRoles } from 'nocodb-sdk'; +import path from 'path'; +import Noco from '../Noco'; +import { MetaTable } from '../utils/globals'; +import extractProjectIdAndAuthenticate from '../meta/helpers/extractProjectIdAndAuthenticate'; +import catchError, { NcError } from '../meta/helpers/catchError'; +import { NC_ATTACHMENT_FIELD_SIZE } from '../constants'; +import { getCacheMiddleware } from '../meta/api/helpers'; +import { attachmentService } from '../services'; + +const isUploadAllowedMw = async (req: Request, _res: Response, next: any) => { + if (!req['user']?.id) { + if (!req['user']?.isPublicBase) { + NcError.unauthorized('Unauthorized'); + } + } + + try { + // check user is super admin or creator + if ( + req['user'].roles?.includes(OrgUserRoles.SUPER_ADMIN) || + req['user'].roles?.includes(OrgUserRoles.CREATOR) || + req['user'].roles?.includes(ProjectRoles.EDITOR) || + // if viewer then check at-least one project have editor or higher role + // todo: cache + !!(await Noco.ncMeta + .knex(MetaTable.PROJECT_USERS) + .where(function () { + this.where('roles', ProjectRoles.OWNER); + this.orWhere('roles', ProjectRoles.CREATOR); + this.orWhere('roles', ProjectRoles.EDITOR); + }) + .andWhere('fk_user_id', req['user'].id) + .first()) + ) + return next(); + } catch {} + NcError.badRequest('Upload not allowed'); +}; + +export async function upload(req: Request, res: Response) { + const attachments = await attachmentService.upload({ + files: (req as any).files, + path: req.query?.path as string, + }); + + res.json(attachments); +} + +export async function uploadViaURL(req: Request, res: Response) { + const attachments = await attachmentService.uploadViaURL({ + urls: req.body, + path: req.query?.path as string, + }); + + res.json(attachments); +} + +export async function fileRead(req, res) { + try { + const { img, type } = await attachmentService.fileRead({ + path: path.join('nc', 'uploads', req.params?.[0]), + }); + + res.writeHead(200, { 'Content-Type': type }); + res.end(img, 'binary'); + } catch (e) { + console.log(e); + res.status(404).send('Not found'); + } +} + +const router = Router({ mergeParams: true }); + +router.get( + /^\/dl\/([^/]+)\/([^/]+)\/(.+)$/, + getCacheMiddleware(), + async (req, res) => { + try { + const { img, type } = await attachmentService.fileRead({ + path: path.join( + 'nc', + req.params[0], + req.params[1], + 'uploads', + ...req.params[2].split('/') + ), + }); + + res.writeHead(200, { 'Content-Type': type }); + res.end(img, 'binary'); + } catch (e) { + res.status(404).send('Not found'); + } + } +); + +router.post( + '/api/v1/db/storage/upload', + multer({ + storage: multer.diskStorage({}), + limits: { + fieldSize: NC_ATTACHMENT_FIELD_SIZE, + }, + }).any(), + [ + extractProjectIdAndAuthenticate, + catchError(isUploadAllowedMw), + catchError(upload), + ] +); + +router.post( + '/api/v1/db/storage/upload-by-url', + + [ + extractProjectIdAndAuthenticate, + catchError(isUploadAllowedMw), + catchError(uploadViaURL), + ] +); + +router.get(/^\/download\/(.+)$/, getCacheMiddleware(), catchError(fileRead)); + +export default router; diff --git a/packages/nocodb/src/lib/meta/api/auditApis.ts b/packages/nocodb/src/lib/controllers/auditController.ts similarity index 50% rename from packages/nocodb/src/lib/meta/api/auditApis.ts rename to packages/nocodb/src/lib/controllers/auditController.ts index fdf9f70daf..73ab7dafa4 100644 --- a/packages/nocodb/src/lib/meta/api/auditApis.ts +++ b/packages/nocodb/src/lib/controllers/auditController.ts @@ -1,39 +1,24 @@ import { Request, Response, Router } from 'express'; -import Audit from '../../models/Audit'; -import { AuditOperationSubTypes, AuditOperationTypes } from 'nocodb-sdk'; -import Model from '../../models/Model'; -import { PagedResponseImpl } from '../helpers/PagedResponse'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; - -import DOMPurify from 'isomorphic-dompurify'; -import { getAjvValidatorMw } from './helpers'; +import Audit from '../models/Audit'; +import { PagedResponseImpl } from '../meta/helpers/PagedResponse'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { auditService } from '../services'; export async function commentRow(req: Request, res) { res.json( - await Audit.insert({ - ...req.body, - user: (req as any).user?.email, - op_type: AuditOperationTypes.COMMENT, + await auditService.commentRow({ + rowId: req.params.rowId, + user: (req as any).user, + body: req.body, }) ); } export async function auditRowUpdate(req: Request, res) { - const model = await Model.getByIdOrName({ id: req.body.fk_model_id }); res.json( - await Audit.insert({ - fk_model_id: req.body.fk_model_id, - row_id: req.params.rowId, - op_type: AuditOperationTypes.DATA, - op_sub_type: AuditOperationSubTypes.UPDATE, - description: DOMPurify.sanitize( - `Table ${model.table_name} : field ${req.body.column_name} got changed from ${req.body.prev_value} to ${req.body.value}` - ), - details: DOMPurify.sanitize(`${req.body.column_name} - : ${req.body.prev_value} - ${req.body.value}`), - ip: (req as any).clientIp, - user: (req as any).user?.email, + await auditService.auditRowUpdate({ + rowId: req.params.rowId, + body: req.body, }) ); } @@ -70,12 +55,10 @@ router.get( ); router.post( '/api/v1/db/meta/audits/comments', - getAjvValidatorMw('swagger.json#/components/schemas/CommentReq'), ncMetaAclMw(commentRow, 'commentRow') ); router.post( '/api/v1/db/meta/audits/rows/:rowId/update', - getAjvValidatorMw('swagger.json#/components/schemas/AuditRowUpdateReq'), ncMetaAclMw(auditRowUpdate, 'auditRowUpdate') ); router.get( diff --git a/packages/nocodb/src/lib/controllers/baseController.ts b/packages/nocodb/src/lib/controllers/baseController.ts new file mode 100644 index 0000000000..f35deb5069 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/baseController.ts @@ -0,0 +1,91 @@ +import { Request, Response } from 'express'; +import { BaseListType } from 'nocodb-sdk'; +import { PagedResponseImpl } from '../meta/helpers/PagedResponse'; +import Base from '../models/Base'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; + +import { baseService } from '../services'; + +async function baseGet(req: Request, res: Response) { + const base = await baseService.baseGetWithConfig({ + baseId: req.params.baseId, + }); + + res.json(base); +} + +async function baseUpdate(req: Request, res: Response) { + const base = await baseService.baseUpdate({ + baseId: req.params.baseId, + base: req.body, + projectId: req.params.projectId, + }); + res.json(base); +} + +async function baseList( + req: Request, + res: Response +) { + const bases = await baseService.baseList({ + projectId: req.params.projectId, + }); + + res // todo: pagination + .json({ + bases: new PagedResponseImpl(bases, { + count: bases.length, + limit: bases.length, + }), + }); +} + +export async function baseDelete( + req: Request, + res: Response +) { + const result = await baseService.baseDelete({ + baseId: req.params.baseId, + }); + res.json(result); +} + +async function baseCreate(req: Request, res) { + const base = await baseService.baseCreate({ + projectId: req.params.projectId, + base: req.body, + }); + + res.json(base); +} + +const initRoutes = (router) => { + router.get( + '/api/v1/db/meta/projects/:projectId/bases/:baseId', + metaApiMetrics, + ncMetaAclMw(baseGet, 'baseGet') + ); + router.patch( + '/api/v1/db/meta/projects/:projectId/bases/:baseId', + metaApiMetrics, + ncMetaAclMw(baseUpdate, 'baseUpdate') + ); + router.delete( + '/api/v1/db/meta/projects/:projectId/bases/:baseId', + metaApiMetrics, + ncMetaAclMw(baseDelete, 'baseDelete') + ); + router.post( + '/api/v1/db/meta/projects/:projectId/bases', + metaApiMetrics, + ncMetaAclMw(baseCreate, 'baseCreate') + ); + router.get( + '/api/v1/db/meta/projects/:projectId/bases', + metaApiMetrics, + ncMetaAclMw(baseList, 'baseList') + ); +}; + +export default initRoutes; diff --git a/packages/nocodb/src/lib/meta/api/cacheApis.ts b/packages/nocodb/src/lib/controllers/cacheController.ts similarity index 70% rename from packages/nocodb/src/lib/meta/api/cacheApis.ts rename to packages/nocodb/src/lib/controllers/cacheController.ts index 8c5af7fa0d..751c26178c 100644 --- a/packages/nocodb/src/lib/meta/api/cacheApis.ts +++ b/packages/nocodb/src/lib/controllers/cacheController.ts @@ -1,9 +1,9 @@ -import catchError from '../helpers/catchError'; -import NocoCache from '../../cache/NocoCache'; +import catchError from '../meta/helpers/catchError'; import { Router } from 'express'; +import { cacheService } from '../services'; export async function cacheGet(_, res) { - const data = await NocoCache.export(); + const data = await cacheService.cacheGet(); res.set({ 'Content-Type': 'application/json', 'Content-Disposition': `attachment; filename="cache-export.json"`, @@ -12,7 +12,7 @@ export async function cacheGet(_, res) { } export async function cacheDelete(_, res) { - return res.json(await NocoCache.destroy()); + return res.json(await cacheService.cacheDelete()); } const router = Router(); diff --git a/packages/nocodb/src/lib/controllers/columnController.ts b/packages/nocodb/src/lib/controllers/columnController.ts new file mode 100644 index 0000000000..a6a230cfe1 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/columnController.ts @@ -0,0 +1,77 @@ +import { Request, Response, Router } from 'express'; +import { ColumnReqType, TableType, UITypes } from 'nocodb-sdk'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { columnService } from '../services'; + +export async function columnGet(req: Request, res: Response) { + res.json(await columnService.columnGet({ columnId: req.params.columnId })); +} + +export async function columnAdd( + req: Request, + res: Response +) { + res.json( + await columnService.columnAdd({ + tableId: req.params.tableId, + column: req.body, + req, + }) + ); +} + +export async function columnSetAsPrimary(req: Request, res: Response) { + res.json( + await columnService.columnSetAsPrimary({ columnId: req.params.columnId }) + ); +} + +export async function columnUpdate(req: Request, res: Response) { + res.json( + await columnService.columnUpdate({ + columnId: req.params.columnId, + column: req.body, + req, + }) + ); +} + +export async function columnDelete(req: Request, res: Response) { + res.json( + await columnService.columnDelete({ columnId: req.params.columnId, req }) + ); +} + +const router = Router({ mergeParams: true }); + +router.post( + '/api/v1/db/meta/tables/:tableId/columns/', + metaApiMetrics, + ncMetaAclMw(columnAdd, 'columnAdd') +); + +router.patch( + '/api/v1/db/meta/columns/:columnId', + metaApiMetrics, + ncMetaAclMw(columnUpdate, 'columnUpdate') +); + +router.delete( + '/api/v1/db/meta/columns/:columnId', + metaApiMetrics, + ncMetaAclMw(columnDelete, 'columnDelete') +); + +router.get( + '/api/v1/db/meta/columns/:columnId', + metaApiMetrics, + ncMetaAclMw(columnGet, 'columnGet') +); + +router.post( + '/api/v1/db/meta/columns/:columnId/primary', + metaApiMetrics, + ncMetaAclMw(columnSetAsPrimary, 'columnSetAsPrimary') +); +export default router; diff --git a/packages/nocodb/src/lib/controllers/dataControllers/bulkDataAliasController.ts b/packages/nocodb/src/lib/controllers/dataControllers/bulkDataAliasController.ts new file mode 100644 index 0000000000..7aa016ef2e --- /dev/null +++ b/packages/nocodb/src/lib/controllers/dataControllers/bulkDataAliasController.ts @@ -0,0 +1,92 @@ +import { Request, Response, Router } from 'express'; +import { bulkDataService } from '../../services'; +import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw'; +import apiMetrics from '../../meta/helpers/apiMetrics'; + +async function bulkDataInsert(req: Request, res: Response) { + res.json( + await bulkDataService.bulkDataInsert({ + body: req.body, + cookie: req, + projectName: req.params.projectName, + tableName: req.params.tableName, + }) + ); +} + +async function bulkDataUpdate(req: Request, res: Response) { + res.json( + await bulkDataService.bulkDataUpdate({ + body: req.body, + cookie: req, + projectName: req.params.projectName, + tableName: req.params.tableName, + }) + ); +} + +// todo: Integrate with filterArrJson bulkDataUpdateAll +async function bulkDataUpdateAll(req: Request, res: Response) { + res.json( + await bulkDataService.bulkDataUpdateAll({ + body: req.body, + cookie: req, + projectName: req.params.projectName, + tableName: req.params.tableName, + query: req.query, + }) + ); +} + +async function bulkDataDelete(req: Request, res: Response) { + res.json( + await bulkDataService.bulkDataDelete({ + body: req.body, + cookie: req, + projectName: req.params.projectName, + tableName: req.params.tableName, + }) + ); +} + +// todo: Integrate with filterArrJson bulkDataDeleteAll +async function bulkDataDeleteAll(req: Request, res: Response) { + res.json( + await bulkDataService.bulkDataDeleteAll({ + // cookie: req, + projectName: req.params.projectName, + tableName: req.params.tableName, + query: req.query, + }) + ); +} + +const router = Router({ mergeParams: true }); + +router.post( + '/api/v1/db/data/bulk/:orgs/:projectName/:tableName', + apiMetrics, + ncMetaAclMw(bulkDataInsert, 'bulkDataInsert') +); +router.patch( + '/api/v1/db/data/bulk/:orgs/:projectName/:tableName', + apiMetrics, + ncMetaAclMw(bulkDataUpdate, 'bulkDataUpdate') +); +router.patch( + '/api/v1/db/data/bulk/:orgs/:projectName/:tableName/all', + apiMetrics, + ncMetaAclMw(bulkDataUpdateAll, 'bulkDataUpdateAll') +); +router.delete( + '/api/v1/db/data/bulk/:orgs/:projectName/:tableName', + apiMetrics, + ncMetaAclMw(bulkDataDelete, 'bulkDataDelete') +); +router.delete( + '/api/v1/db/data/bulk/:orgs/:projectName/:tableName/all', + apiMetrics, + ncMetaAclMw(bulkDataDeleteAll, 'bulkDataDeleteAll') +); + +export default router; diff --git a/packages/nocodb/src/lib/controllers/dataControllers/dataAliasController.ts b/packages/nocodb/src/lib/controllers/dataControllers/dataAliasController.ts new file mode 100644 index 0000000000..b5eda23c74 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/dataControllers/dataAliasController.ts @@ -0,0 +1,259 @@ +import { Request, Response, Router } from 'express'; +import { dataService } from '../../services'; +import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw'; +import apiMetrics from '../../meta/helpers/apiMetrics'; +import { parseHrtimeToSeconds } from '../../meta/api/helpers'; + +// todo: Handle the error case where view doesnt belong to model +async function dataList(req: Request, res: Response) { + const startTime = process.hrtime(); + const responseData = await dataService.dataList({ + query: req.query, + projectName: req.params.projectName, + tableName: req.params.tableName, + viewName: req.params.viewName, + }); + const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime)); + res.setHeader('xc-db-response', elapsedSeconds); + res.json(responseData); +} + +async function dataFindOne(req: Request, res: Response) { + res.json( + await dataService.dataFindOne({ + query: req.query, + projectName: req.params.projectName, + tableName: req.params.tableName, + viewName: req.params.viewName, + }) + ); +} + +async function dataGroupBy(req: Request, res: Response) { + res.json( + await dataService.dataGroupBy({ + query: req.query, + projectName: req.params.projectName, + tableName: req.params.tableName, + viewName: req.params.viewName, + }) + ); +} + +async function dataCount(req: Request, res: Response) { + const countResult = await dataService.dataCount({ + query: req.query, + projectName: req.params.projectName, + tableName: req.params.tableName, + viewName: req.params.viewName, + }); + + res.json(countResult); +} + +async function dataInsert(req: Request, res: Response) { + res.json( + await dataService.dataInsert({ + projectName: req.params.projectName, + tableName: req.params.tableName, + viewName: req.params.viewName, + body: req.body, + cookie: req, + }) + ); +} + +async function dataUpdate(req: Request, res: Response) { + res.json( + await dataService.dataUpdate({ + projectName: req.params.projectName, + tableName: req.params.tableName, + viewName: req.params.viewName, + body: req.body, + cookie: req, + rowId: req.params.rowId, + }) + ); +} + +async function dataDelete(req: Request, res: Response) { + res.json( + await dataService.dataDelete({ + projectName: req.params.projectName, + tableName: req.params.tableName, + viewName: req.params.viewName, + cookie: req, + rowId: req.params.rowId, + }) + ); +} + +async function dataRead(req: Request, res: Response) { + res.json( + await dataService.dataRead({ + projectName: req.params.projectName, + tableName: req.params.tableName, + viewName: req.params.viewName, + rowId: req.params.rowId, + query: req.query, + }) + ); +} + +async function dataExist(req: Request, res: Response) { + res.json( + await dataService.dataExist({ + projectName: req.params.projectName, + tableName: req.params.tableName, + viewName: req.params.viewName, + rowId: req.params.rowId, + query: req.query, + }) + ); +} + +// todo: Handle the error case where view doesnt belong to model +async function groupedDataList(req: Request, res: Response) { + const startTime = process.hrtime(); + const groupedData = await dataService.groupedDataList({ + projectName: req.params.projectName, + tableName: req.params.tableName, + viewName: req.params.viewName, + query: req.query, + columnId: req.params.columnId, + }); + const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime)); + res.setHeader('xc-db-response', elapsedSeconds); + res.json(groupedData); +} +const router = Router({ mergeParams: true }); + +// table data crud apis +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName', + apiMetrics, + ncMetaAclMw(dataList, 'dataList') +); + +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName/find-one', + apiMetrics, + ncMetaAclMw(dataFindOne, 'dataFindOne') +); + +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName/groupby', + apiMetrics, + ncMetaAclMw(dataGroupBy, 'dataGroupBy') +); + +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName/group/:columnId', + apiMetrics, + ncMetaAclMw(groupedDataList, 'groupedDataList') +); + +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/exist', + apiMetrics, + ncMetaAclMw(dataExist, 'dataExist') +); + +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName/count', + apiMetrics, + ncMetaAclMw(dataCount, 'dataCount') +); + +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/count', + apiMetrics, + ncMetaAclMw(dataCount, 'dataCount') +); + +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId', + apiMetrics, + ncMetaAclMw(dataRead, 'dataRead') +); + +router.patch( + '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId', + apiMetrics, + ncMetaAclMw(dataUpdate, 'dataUpdate') +); + +router.delete( + '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId', + apiMetrics, + ncMetaAclMw(dataDelete, 'dataDelete') +); + +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName', + apiMetrics, + ncMetaAclMw(dataList, 'dataList') +); + +// table view data crud apis +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName', + apiMetrics, + ncMetaAclMw(dataList, 'dataList') +); + +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/find-one', + apiMetrics, + ncMetaAclMw(dataFindOne, 'dataFindOne') +); + +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/groupby', + apiMetrics, + ncMetaAclMw(dataGroupBy, 'dataGroupBy') +); + +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/group/:columnId', + apiMetrics, + ncMetaAclMw(groupedDataList, 'groupedDataList') +); + +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/:rowId/exist', + apiMetrics, + ncMetaAclMw(dataExist, 'dataExist') +); + +router.post( + '/api/v1/db/data/:orgs/:projectName/:tableName', + apiMetrics, + ncMetaAclMw(dataInsert, 'dataInsert') +); + +router.post( + '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName', + apiMetrics, + ncMetaAclMw(dataInsert, 'dataInsert') +); + +router.patch( + '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/:rowId', + apiMetrics, + ncMetaAclMw(dataUpdate, 'dataUpdate') +); + +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/:rowId', + apiMetrics, + ncMetaAclMw(dataRead, 'dataRead') +); + +router.delete( + '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/:rowId', + apiMetrics, + ncMetaAclMw(dataDelete, 'dataDelete') +); + +export default router; diff --git a/packages/nocodb/src/lib/meta/api/dataApis/dataAliasExportApis.ts b/packages/nocodb/src/lib/controllers/dataControllers/dataAliasExportController.ts similarity index 88% rename from packages/nocodb/src/lib/meta/api/dataApis/dataAliasExportApis.ts rename to packages/nocodb/src/lib/controllers/dataControllers/dataAliasExportController.ts index 8c6cf38bea..0b85b87801 100644 --- a/packages/nocodb/src/lib/meta/api/dataApis/dataAliasExportApis.ts +++ b/packages/nocodb/src/lib/controllers/dataControllers/dataAliasExportController.ts @@ -1,13 +1,13 @@ import { Request, Response, Router } from 'express'; import * as XLSX from 'xlsx'; -import ncMetaAclMw from '../../helpers/ncMetaAclMw'; +import apiMetrics from '../../meta/helpers/apiMetrics'; +import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw'; +import { View } from '../../models'; import { extractCsvData, extractXlsxData, - getViewAndModelFromRequestByAliasOrId, -} from './helpers'; -import apiMetrics from '../../helpers/apiMetrics'; -import View from '../../../models/View'; +} from '../../services/dataService/helpers'; +import { getViewAndModelFromRequestByAliasOrId } from './helpers'; async function excelDataExport(req: Request, res: Response) { const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); diff --git a/packages/nocodb/src/lib/controllers/dataControllers/dataAliasNestedController.ts b/packages/nocodb/src/lib/controllers/dataControllers/dataAliasNestedController.ts new file mode 100644 index 0000000000..9fc5169ee3 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/dataControllers/dataAliasNestedController.ts @@ -0,0 +1,137 @@ +import { Request, Response, Router } from 'express'; +import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw'; +import apiMetrics from '../../meta/helpers/apiMetrics'; +import { dataAliasNestedService } from '../../services'; + +// todo: handle case where the given column is not ltar +export async function mmList(req: Request, res: Response) { + res.json( + await dataAliasNestedService.mmList({ + query: req.query, + columnName: req.params.columnName, + rowId: req.params.rowId, + projectName: req.params.projectName, + tableName: req.params.tableName, + }) + ); +} + +export async function mmExcludedList(req: Request, res: Response) { + res.json( + await dataAliasNestedService.mmExcludedList({ + query: req.query, + columnName: req.params.columnName, + rowId: req.params.rowId, + projectName: req.params.projectName, + tableName: req.params.tableName, + }) + ); +} + +export async function hmExcludedList(req: Request, res: Response) { + res.json( + await dataAliasNestedService.hmExcludedList({ + query: req.query, + columnName: req.params.columnName, + rowId: req.params.rowId, + projectName: req.params.projectName, + tableName: req.params.tableName, + }) + ); +} + +export async function btExcludedList(req: Request, res: Response) { + res.json( + await dataAliasNestedService.btExcludedList({ + query: req.query, + columnName: req.params.columnName, + rowId: req.params.rowId, + projectName: req.params.projectName, + tableName: req.params.tableName, + }) + ); +} + +// todo: handle case where the given column is not ltar +export async function hmList(req: Request, res: Response) { + res.json( + await dataAliasNestedService.hmList({ + query: req.query, + columnName: req.params.columnName, + rowId: req.params.rowId, + projectName: req.params.projectName, + tableName: req.params.tableName, + }) + ); +} + +//@ts-ignore +async function relationDataRemove(req, res) { + await dataAliasNestedService.relationDataRemove({ + columnName: req.params.columnName, + rowId: req.params.rowId, + projectName: req.params.projectName, + tableName: req.params.tableName, + cookie: req, + refRowId: req.params.refRowId, + }); + + res.json({ msg: 'success' }); +} + +//@ts-ignore +// todo: Give proper error message when reference row is already related and handle duplicate ref row id in hm +async function relationDataAdd(req, res) { + await dataAliasNestedService.relationDataAdd({ + columnName: req.params.columnName, + rowId: req.params.rowId, + projectName: req.params.projectName, + tableName: req.params.tableName, + cookie: req, + refRowId: req.params.refRowId, + }); + + res.json({ msg: 'success' }); +} + +const router = Router({ mergeParams: true }); + +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/mm/:columnName/exclude', + apiMetrics, + ncMetaAclMw(mmExcludedList, 'mmExcludedList') +); +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/hm/:columnName/exclude', + apiMetrics, + ncMetaAclMw(hmExcludedList, 'hmExcludedList') +); +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/bt/:columnName/exclude', + apiMetrics, + ncMetaAclMw(btExcludedList, 'btExcludedList') +); + +router.post( + '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/:relationType/:columnName/:refRowId', + apiMetrics, + ncMetaAclMw(relationDataAdd, 'relationDataAdd') +); +router.delete( + '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/:relationType/:columnName/:refRowId', + apiMetrics, + ncMetaAclMw(relationDataRemove, 'relationDataRemove') +); + +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/mm/:columnName', + apiMetrics, + ncMetaAclMw(mmList, 'mmList') +); +router.get( + '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/hm/:columnName', + apiMetrics, + ncMetaAclMw(hmList, 'hmList') +); + +export default router; diff --git a/packages/nocodb/src/lib/controllers/dataControllers/dataController.ts b/packages/nocodb/src/lib/controllers/dataControllers/dataController.ts new file mode 100644 index 0000000000..fb5c35ab17 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/dataControllers/dataController.ts @@ -0,0 +1,192 @@ +import { Request, Response, Router } from 'express'; +import { dataService } from '../../services'; +import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw'; +import apiMetrics from '../../meta/helpers/apiMetrics'; + +export async function dataList(req: Request, res: Response) { + res.json( + await dataService.dataListByViewId({ + viewId: req.params.viewId, + query: req.query, + }) + ); +} + +export async function mmList(req: Request, res: Response) { + res.json( + await dataService.mmList({ + viewId: req.params.viewId, + colId: req.params.colId, + rowId: req.params.rowId, + query: req.query, + }) + ); +} + +export async function mmExcludedList(req: Request, res: Response) { + res.json( + await dataService.mmExcludedList({ + viewId: req.params.viewId, + colId: req.params.colId, + rowId: req.params.rowId, + query: req.query, + }) + ); +} + +export async function hmExcludedList(req: Request, res: Response) { + res.json( + await dataService.hmExcludedList({ + viewId: req.params.viewId, + colId: req.params.colId, + rowId: req.params.rowId, + query: req.query, + }) + ); +} + +export async function btExcludedList(req: Request, res: Response) { + res.json( + await dataService.btExcludedList({ + viewId: req.params.viewId, + colId: req.params.colId, + rowId: req.params.rowId, + query: req.query, + }) + ); +} + +export async function hmList(req: Request, res: Response) { + res.json( + await dataService.hmList({ + viewId: req.params.viewId, + colId: req.params.colId, + rowId: req.params.rowId, + query: req.query, + }) + ); +} + +async function dataRead(req: Request, res: Response) { + res.json( + await dataService.dataReadByViewId({ + viewId: req.params.viewId, + rowId: req.params.rowId, + query: req.query, + }) + ); +} + +async function dataInsert(req: Request, res: Response) { + res.json( + await dataService.dataInsertByViewId({ + viewId: req.params.viewId, + body: req.body, + cookie: req, + }) + ); +} + +async function dataUpdate(req: Request, res: Response) { + res.json( + await dataService.dataUpdateByViewId({ + viewId: req.params.viewId, + rowId: req.params.rowId, + body: req.body, + cookie: req, + }) + ); +} + +async function dataDelete(req: Request, res: Response) { + res.json( + await dataService.dataDeleteByViewId({ + viewId: req.params.viewId, + rowId: req.params.rowId, + cookie: req, + }) + ); +} + +async function relationDataDelete(req, res) { + await dataService.relationDataDelete({ + viewId: req.params.viewId, + colId: req.params.colId, + childId: req.params.childId, + rowId: req.params.rowId, + cookie: req, + }); + + res.json({ msg: 'success' }); +} + +//@ts-ignore +async function relationDataAdd(req, res) { + await dataService.relationDataAdd({ + viewId: req.params.viewId, + colId: req.params.colId, + childId: req.params.childId, + rowId: req.params.rowId, + cookie: req, + }); + + res.json({ msg: 'success' }); +} + +const router = Router({ mergeParams: true }); + +router.get('/data/:viewId/', apiMetrics, ncMetaAclMw(dataList, 'dataList')); +router.post( + '/data/:viewId/', + apiMetrics, + ncMetaAclMw(dataInsert, 'dataInsert') +); +router.get( + '/data/:viewId/:rowId', + apiMetrics, + ncMetaAclMw(dataRead, 'dataRead') +); +router.patch( + '/data/:viewId/:rowId', + apiMetrics, + ncMetaAclMw(dataUpdate, 'dataUpdate') +); +router.delete( + '/data/:viewId/:rowId', + apiMetrics, + ncMetaAclMw(dataDelete, 'dataDelete') +); + +router.get( + '/data/:viewId/:rowId/mm/:colId', + apiMetrics, + ncMetaAclMw(mmList, 'mmList') +); +router.get( + '/data/:viewId/:rowId/hm/:colId', + apiMetrics, + ncMetaAclMw(hmList, 'hmList') +); + +router.get( + '/data/:viewId/:rowId/mm/:colId/exclude', + ncMetaAclMw(mmExcludedList, 'mmExcludedList') +); +router.get( + '/data/:viewId/:rowId/hm/:colId/exclude', + ncMetaAclMw(hmExcludedList, 'hmExcludedList') +); +router.get( + '/data/:viewId/:rowId/bt/:colId/exclude', + ncMetaAclMw(btExcludedList, 'btExcludedList') +); + +router.post( + '/data/:viewId/:rowId/:relationType/:colId/:childId', + ncMetaAclMw(relationDataAdd, 'relationDataAdd') +); +router.delete( + '/data/:viewId/:rowId/:relationType/:colId/:childId', + ncMetaAclMw(relationDataDelete, 'relationDataDelete') +); +export default router; diff --git a/packages/nocodb/src/lib/controllers/dataControllers/helpers.ts b/packages/nocodb/src/lib/controllers/dataControllers/helpers.ts new file mode 100644 index 0000000000..2ca5cd17e1 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/dataControllers/helpers.ts @@ -0,0 +1,270 @@ +import { NcError } from '../../meta/helpers/catchError'; +import Project from '../../models/Project'; +import Model from '../../models/Model'; +import View from '../../models/View'; +import { Request } from 'express'; +import Base from '../../models/Base'; +import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; +import { isSystemColumn, UITypes } from 'nocodb-sdk'; + +import * as XLSX from 'xlsx'; +import Column from '../../models/Column'; +import LookupColumn from '../../models/LookupColumn'; +import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn'; + +import papaparse from 'papaparse'; +import { dataService } from '../../services'; +export async function getViewAndModelFromRequestByAliasOrId( + req: + | Request<{ projectName: string; tableName: string; viewName?: string }> + | Request +) { + const project = await Project.getWithInfoByTitleOrId(req.params.projectName); + + const model = await Model.getByAliasOrId({ + project_id: project.id, + aliasOrId: req.params.tableName, + }); + const view = + req.params.viewName && + (await View.getByTitleOrId({ + titleOrId: req.params.viewName, + fk_model_id: model.id, + })); + if (!model) NcError.notFound('Table not found'); + return { model, view }; +} + +export async function extractXlsxData(param: { + view: View; + query: any; + siteUrl: string; +}) { + const { view, query, siteUrl } = param; + const base = await Base.get(view.base_id); + + await view.getModelWithInfo(); + await view.getColumns(); + + view.model.columns = view.columns + .filter((c) => c.show) + .map( + (c) => + new Column({ ...c, ...view.model.columnsById[c.fk_column_id] } as any) + ) + .filter((column) => !isSystemColumn(column) || view.show_system_fields); + + const baseModel = await Model.getBaseModelSQL({ + id: view.model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const { offset, dbRows, elapsed } = await dataService.getDbRows({ + baseModel, + view, + query, + siteUrl, + }); + + const fields = query.fields as string[]; + + const data = XLSX.utils.json_to_sheet(dbRows, { header: fields }); + + return { offset, dbRows, elapsed, data }; +} + +export async function extractCsvData(view: View, req: Request) { + const base = await Base.get(view.base_id); + const fields = req.query.fields; + + await view.getModelWithInfo(); + await view.getColumns(); + + view.model.columns = view.columns + .filter((c) => c.show) + .map( + (c) => + new Column({ ...c, ...view.model.columnsById[c.fk_column_id] } as any) + ) + .filter((column) => !isSystemColumn(column) || view.show_system_fields); + + const baseModel = await Model.getBaseModelSQL({ + id: view.model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const { offset, dbRows, elapsed } = await dataService.getDbRows({ + baseModel, + view, + query: req.query, + siteUrl: (req as any).ncSiteUrl, + }); + + const data = papaparse.unparse( + { + fields: view.model.columns + .sort((c1, c2) => + Array.isArray(fields) + ? fields.indexOf(c1.title as any) - fields.indexOf(c2.title as any) + : 0 + ) + .filter( + (c) => + !fields || !Array.isArray(fields) || fields.includes(c.title as any) + ) + .map((c) => c.title), + data: dbRows, + }, + { + escapeFormulae: true, + } + ); + + return { offset, dbRows, elapsed, data }; +} +// +// async function getDbRows(baseModel, view: View, req: Request) { +// let offset = +req.query.offset || 0; +// const limit = 100; +// // const size = +process.env.NC_EXPORT_MAX_SIZE || 1024; +// const timeout = +process.env.NC_EXPORT_MAX_TIMEOUT || 5000; +// const dbRows = []; +// const startTime = process.hrtime(); +// let elapsed, temp; +// +// const listArgs: any = { ...req.query }; +// try { +// listArgs.filterArr = JSON.parse(listArgs.filterArrJson); +// } catch (e) {} +// try { +// listArgs.sortArr = JSON.parse(listArgs.sortArrJson); +// } catch (e) {} +// +// for ( +// elapsed = 0; +// elapsed < timeout; +// offset += limit, +// temp = process.hrtime(startTime), +// elapsed = temp[0] * 1000 + temp[1] / 1000000 +// ) { +// const rows = await nocoExecute( +// await getAst({ +// query: req.query, +// includePkByDefault: false, +// model: view.model, +// view, +// }), +// await baseModel.list({ ...listArgs, offset, limit }), +// {}, +// req.query +// ); +// +// if (!rows?.length) { +// offset = -1; +// break; +// } +// +// for (const row of rows) { +// const dbRow = { ...row }; +// +// for (const column of view.model.columns) { +// if (isSystemColumn(column) && !view.show_system_fields) continue; +// dbRow[column.title] = await serializeCellValue({ +// value: row[column.title], +// column, +// siteUrl: req['ncSiteUrl'], +// }); +// } +// dbRows.push(dbRow); +// } +// } +// return { offset, dbRows, elapsed }; +// } + +export async function serializeCellValue({ + value, + column, + siteUrl, +}: { + column?: Column; + value: any; + siteUrl: string; +}) { + if (!column) { + return value; + } + + if (!value) return value; + + switch (column?.uidt) { + case UITypes.Attachment: { + let data = value; + try { + if (typeof value === 'string') { + data = JSON.parse(value); + } + } catch {} + + return (data || []).map( + (attachment) => + `${encodeURI(attachment.title)}(${encodeURI( + attachment.path ? `${siteUrl}/${attachment.path}` : attachment.url + )})` + ); + } + case UITypes.Lookup: + { + const colOptions = await column.getColOptions(); + const lookupColumn = await colOptions.getLookupColumn(); + return ( + await Promise.all( + [...(Array.isArray(value) ? value : [value])].map(async (v) => + serializeCellValue({ + value: v, + column: lookupColumn, + siteUrl, + }) + ) + ) + ).join(', '); + } + break; + case UITypes.LinkToAnotherRecord: + { + const colOptions = + await column.getColOptions(); + const relatedModel = await colOptions.getRelatedTable(); + await relatedModel.getColumns(); + return [...(Array.isArray(value) ? value : [value])] + .map((v) => { + return v[relatedModel.displayValue?.title]; + }) + .join(', '); + } + break; + default: + if (value && typeof value === 'object') { + return JSON.stringify(value); + } + return value; + } +} + +export async function getColumnByIdOrName( + columnNameOrId: string, + model: Model +) { + const column = (await model.getColumns()).find( + (c) => + c.title === columnNameOrId || + c.id === columnNameOrId || + c.column_name === columnNameOrId + ); + + if (!column) + NcError.notFound(`Column with id/name '${columnNameOrId}' is not found`); + + return column; +} diff --git a/packages/nocodb/src/lib/controllers/dataControllers/index.ts b/packages/nocodb/src/lib/controllers/dataControllers/index.ts new file mode 100644 index 0000000000..122ec7fac2 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/dataControllers/index.ts @@ -0,0 +1,15 @@ +import dataController from './dataController'; +import oldDataController from './oldDataController'; +import dataAliasController from './dataAliasController'; +import bulkDataAliasController from './bulkDataAliasController'; +import dataAliasNestedController from './dataAliasNestedController'; +import dataAliasExportController from './dataAliasExportController'; + +export { + dataController, + oldDataController, + dataAliasController, + bulkDataAliasController, + dataAliasNestedController, + dataAliasExportController, +}; diff --git a/packages/nocodb/src/lib/meta/api/dataApis/oldDataApis.ts b/packages/nocodb/src/lib/controllers/dataControllers/oldDataController.ts similarity index 89% rename from packages/nocodb/src/lib/meta/api/dataApis/oldDataApis.ts rename to packages/nocodb/src/lib/controllers/dataControllers/oldDataController.ts index 5df008eb8a..3492ff7361 100644 --- a/packages/nocodb/src/lib/meta/api/dataApis/oldDataApis.ts +++ b/packages/nocodb/src/lib/controllers/dataControllers/oldDataController.ts @@ -1,14 +1,14 @@ import { Request, Response, Router } from 'express'; -import Model from '../../../models/Model'; +import Model from '../../models/Model'; import { nocoExecute } from 'nc-help'; -import Base from '../../../models/Base'; -import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; -import View from '../../../models/View'; -import ncMetaAclMw from '../../helpers/ncMetaAclMw'; -import Project from '../../../models/Project'; -import { NcError } from '../../helpers/catchError'; -import apiMetrics from '../../helpers/apiMetrics'; -import getAst from '../../../db/sql-data-mapper/lib/sql/helpers/getAst'; +import Base from '../../models/Base'; +import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; +import View from '../../models/View'; +import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw'; +import Project from '../../models/Project'; +import { NcError } from '../../meta/helpers/catchError'; +import apiMetrics from '../../meta/helpers/apiMetrics'; +import getAst from '../../db/sql-data-mapper/lib/sql/helpers/getAst'; export async function dataList(req: Request, res: Response) { const { model, view } = await getViewAndModelFromRequest(req); diff --git a/packages/nocodb/src/lib/meta/api/exportApis.ts b/packages/nocodb/src/lib/controllers/exportController.ts similarity index 81% rename from packages/nocodb/src/lib/meta/api/exportApis.ts rename to packages/nocodb/src/lib/controllers/exportController.ts index 7c0a5e013e..38ae156cb9 100644 --- a/packages/nocodb/src/lib/meta/api/exportApis.ts +++ b/packages/nocodb/src/lib/controllers/exportController.ts @@ -1,7 +1,7 @@ import { Request, Response, Router } from 'express'; -import View from '../../models/View'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { extractCsvData } from './dataApis/helpers'; +import View from '../models/View'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { extractCsvData } from './dataControllers/helpers'; async function exportCsv(req: Request, res: Response) { const view = await View.get(req.params.viewId); diff --git a/packages/nocodb/src/lib/controllers/filterController.ts b/packages/nocodb/src/lib/controllers/filterController.ts new file mode 100644 index 0000000000..bf48e30765 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/filterController.ts @@ -0,0 +1,115 @@ +import { Request, Response, Router } from 'express'; +import { FilterReqType } from 'nocodb-sdk'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; + +import { filterService } from '../services'; + +// @ts-ignore +export async function filterGet(req: Request, res: Response) { + res.json(await filterService.filterGet({ filterId: req.params.filterId })); +} + +// @ts-ignore +export async function filterList(req: Request, res: Response) { + res.json( + await filterService.filterList({ + viewId: req.params.viewId, + }) + ); +} + +// @ts-ignore +export async function filterChildrenRead(req: Request, res: Response) { + const filter = await filterService.filterChildrenList({ + filterId: req.params.filterParentId, + }); + + res.json(filter); +} + +export async function filterCreate(req: Request, res) { + const filter = await filterService.filterCreate({ + filter: req.body, + viewId: req.params.viewId, + }); + res.json(filter); +} + +export async function filterUpdate(req, res) { + const filter = await filterService.filterUpdate({ + filterId: req.params.filterId, + filter: req.body, + }); + res.json(filter); +} + +export async function filterDelete(req: Request, res: Response) { + const filter = await filterService.filterDelete({ + filterId: req.params.filterId, + }); + res.json(filter); +} + +export async function hookFilterList(req: Request, res: Response) { + res.json( + await filterService.hookFilterList({ + hookId: req.params.hookId, + }) + ); +} + +export async function hookFilterCreate( + req: Request, + res +) { + const filter = await filterService.hookFilterCreate({ + filter: req.body, + hookId: req.params.hookId, + }); + res.json(filter); +} + +const router = Router({ mergeParams: true }); +router.get( + '/api/v1/db/meta/views/:viewId/filters', + metaApiMetrics, + ncMetaAclMw(filterList, 'filterList') +); +router.post( + '/api/v1/db/meta/views/:viewId/filters', + metaApiMetrics, + ncMetaAclMw(filterCreate, 'filterCreate') +); + +router.get( + '/api/v1/db/meta/hooks/:hookId/filters', + ncMetaAclMw(hookFilterList, 'filterList') +); +router.post( + '/api/v1/db/meta/hooks/:hookId/filters', + metaApiMetrics, + ncMetaAclMw(hookFilterCreate, 'filterCreate') +); + +router.get( + '/api/v1/db/meta/filters/:filterId', + metaApiMetrics, + ncMetaAclMw(filterGet, 'filterGet') +); +router.patch( + '/api/v1/db/meta/filters/:filterId', + metaApiMetrics, + ncMetaAclMw(filterUpdate, 'filterUpdate') +); +router.delete( + '/api/v1/db/meta/filters/:filterId', + metaApiMetrics, + ncMetaAclMw(filterDelete, 'filterDelete') +); +router.get( + '/api/v1/db/meta/filters/:filterParentId/children', + metaApiMetrics, + ncMetaAclMw(filterChildrenRead, 'filterChildrenRead') +); +export default router; diff --git a/packages/nocodb/src/lib/controllers/formViewColumnController.ts b/packages/nocodb/src/lib/controllers/formViewColumnController.ts new file mode 100644 index 0000000000..8b13309411 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/formViewColumnController.ts @@ -0,0 +1,21 @@ +import { Request, Response, Router } from 'express'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import { formViewColumnService } from '../services'; + +export async function columnUpdate(req: Request, res: Response) { + res.json( + await formViewColumnService.columnUpdate({ + formViewColumnId: req.params.formViewColumnId, + formViewColumn: req.body, + }) + ); +} + +const router = Router({ mergeParams: true }); +router.patch( + '/api/v1/db/meta/form-columns/:formViewColumnId', + metaApiMetrics, + ncMetaAclMw(columnUpdate, 'columnUpdate') +); +export default router; diff --git a/packages/nocodb/src/lib/controllers/formViewController.ts b/packages/nocodb/src/lib/controllers/formViewController.ts new file mode 100644 index 0000000000..2bc61fd58d --- /dev/null +++ b/packages/nocodb/src/lib/controllers/formViewController.ts @@ -0,0 +1,48 @@ +import { Request, Response, Router } from 'express'; +import { FormType } from 'nocodb-sdk'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import { formViewService } from '../services'; + +export async function formViewGet(req: Request, res: Response) { + const formViewData = await formViewService.formViewGet({ + formViewId: req.params.formViewId, + }); + res.json(formViewData); +} + +export async function formViewCreate(req: Request, res) { + const view = await formViewService.formViewCreate({ + body: req.body, + tableId: req.params.tableId, + }); + res.json(view); +} + +export async function formViewUpdate(req, res) { + res.json( + await formViewService.formViewUpdate({ + formViewId: req.params.formViewId, + body: req.body, + }) + ); +} + +const router = Router({ mergeParams: true }); +router.post( + '/api/v1/db/meta/tables/:tableId/forms', + metaApiMetrics, + ncMetaAclMw(formViewCreate, 'formViewCreate') +); +router.get( + '/api/v1/db/meta/forms/:formViewId', + metaApiMetrics, + ncMetaAclMw(formViewGet, 'formViewGet') +); +router.patch( + '/api/v1/db/meta/forms/:formViewId', + metaApiMetrics, + ncMetaAclMw(formViewUpdate, 'formViewUpdate') +); + +export default router; diff --git a/packages/nocodb/src/lib/controllers/galleryViewController.ts b/packages/nocodb/src/lib/controllers/galleryViewController.ts new file mode 100644 index 0000000000..f54dc901cd --- /dev/null +++ b/packages/nocodb/src/lib/controllers/galleryViewController.ts @@ -0,0 +1,49 @@ +import { Request, Response, Router } from 'express'; +import { GalleryType } from 'nocodb-sdk'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import { galleryViewService } from '../services'; + +export async function galleryViewGet(req: Request, res: Response) { + res.json( + await galleryViewService.galleryViewGet({ + galleryViewId: req.params.galleryViewId, + }) + ); +} + +export async function galleryViewCreate(req: Request, res) { + const view = await galleryViewService.galleryViewCreate({ + gallery: req.body, + // todo: sanitize + tableId: req.params.tableId, + }); + res.json(view); +} + +export async function galleryViewUpdate(req, res) { + res.json( + await galleryViewService.galleryViewUpdate({ + galleryViewId: req.params.galleryViewId, + gallery: req.body, + }) + ); +} + +const router = Router({ mergeParams: true }); +router.post( + '/api/v1/db/meta/tables/:tableId/galleries', + metaApiMetrics, + ncMetaAclMw(galleryViewCreate, 'galleryViewCreate') +); +router.patch( + '/api/v1/db/meta/galleries/:galleryViewId', + metaApiMetrics, + ncMetaAclMw(galleryViewUpdate, 'galleryViewUpdate') +); +router.get( + '/api/v1/db/meta/galleries/:galleryViewId', + metaApiMetrics, + ncMetaAclMw(galleryViewGet, 'galleryViewGet') +); +export default router; diff --git a/packages/nocodb/src/lib/meta/api/gridViewColumnApis.ts b/packages/nocodb/src/lib/controllers/gridViewColumnController.ts similarity index 50% rename from packages/nocodb/src/lib/meta/api/gridViewColumnApis.ts rename to packages/nocodb/src/lib/controllers/gridViewColumnController.ts index 28669a6a2f..1ce0a41be7 100644 --- a/packages/nocodb/src/lib/meta/api/gridViewColumnApis.ts +++ b/packages/nocodb/src/lib/controllers/gridViewColumnController.ts @@ -1,17 +1,23 @@ import { Request, Response, Router } from 'express'; -import GridViewColumn from '../../models/GridViewColumn'; -import { Tele } from 'nc-help'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import { getAjvValidatorMw } from './helpers'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import { gridViewColumnService } from '../services'; export async function columnList(req: Request, res: Response) { - res.json(await GridViewColumn.list(req.params.gridViewId)); + res.json( + await gridViewColumnService.columnList({ + gridViewId: req.params.gridViewId, + }) + ); } export async function gridColumnUpdate(req: Request, res: Response) { - Tele.emit('evt', { evt_type: 'gridViewColumn:updated' }); - res.json(await GridViewColumn.update(req.params.gridViewColumnId, req.body)); + res.json( + await gridViewColumnService.gridColumnUpdate({ + gridViewColumnId: req.params.gridViewColumnId, + grid: req.body, + }) + ); } const router = Router({ mergeParams: true }); @@ -23,7 +29,6 @@ router.get( router.patch( '/api/v1/db/meta/grid-columns/:gridViewColumnId', metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/GridColumnReq'), ncMetaAclMw(gridColumnUpdate, 'gridColumnUpdate') ); export default router; diff --git a/packages/nocodb/src/lib/controllers/gridViewController.ts b/packages/nocodb/src/lib/controllers/gridViewController.ts new file mode 100644 index 0000000000..865ccecdb6 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/gridViewController.ts @@ -0,0 +1,34 @@ +import { Request, Router } from 'express'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import { gridViewService } from '../services'; + +export async function gridViewCreate(req: Request, res) { + const view = await gridViewService.gridViewCreate({ + grid: req.body, + tableId: req.params.tableId, + }); + res.json(view); +} + +export async function gridViewUpdate(req, res) { + res.json( + await gridViewService.gridViewUpdate({ + viewId: req.params.viewId, + grid: req.body, + }) + ); +} + +const router = Router({ mergeParams: true }); +router.post( + '/api/v1/db/meta/tables/:tableId/grids/', + metaApiMetrics, + ncMetaAclMw(gridViewCreate, 'gridViewCreate') +); +router.patch( + '/api/v1/db/meta/grids/:viewId', + metaApiMetrics, + ncMetaAclMw(gridViewUpdate, 'gridViewUpdate') +); +export default router; diff --git a/packages/nocodb/src/lib/controllers/hookController.ts b/packages/nocodb/src/lib/controllers/hookController.ts new file mode 100644 index 0000000000..c75e2b82d1 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/hookController.ts @@ -0,0 +1,98 @@ +import catchError from '../meta/helpers/catchError'; +import { Request, Response, Router } from 'express'; +import { HookListType, HookType } from 'nocodb-sdk'; +import { PagedResponseImpl } from '../meta/helpers/PagedResponse'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import { hookService } from '../services'; + +export async function hookList( + req: Request, + res: Response +) { + // todo: pagination + res.json( + new PagedResponseImpl( + await hookService.hookList({ tableId: req.params.tableId }) + ) + ); +} + +export async function hookCreate( + req: Request, + res: Response +) { + const hook = await hookService.hookCreate({ + hook: req.body, + tableId: req.params.tableId, + }); + res.json(hook); +} + +export async function hookDelete( + req: Request, + res: Response +) { + res.json(await hookService.hookDelete({ hookId: req.params.hookId })); +} + +export async function hookUpdate( + req: Request, + res: Response +) { + res.json( + await hookService.hookUpdate({ hookId: req.params.hookId, hook: req.body }) + ); +} + +export async function hookTest(req: Request, res: Response) { + await hookService.hookTest({ + hookTest: req.body, + tableId: req.params.tableId, + }); + res.json({ msg: 'Success' }); +} + +export async function tableSampleData(req: Request, res: Response) { + res // todo: pagination + .json( + await hookService.tableSampleData({ + tableId: req.params.tableId, + // todo: replace any with type + operation: req.params.operation as any, + }) + ); +} + +const router = Router({ mergeParams: true }); +router.get( + '/api/v1/db/meta/tables/:tableId/hooks', + metaApiMetrics, + ncMetaAclMw(hookList, 'hookList') +); +router.post( + '/api/v1/db/meta/tables/:tableId/hooks/test', + metaApiMetrics, + ncMetaAclMw(hookTest, 'hookTest') +); +router.post( + '/api/v1/db/meta/tables/:tableId/hooks', + metaApiMetrics, + ncMetaAclMw(hookCreate, 'hookCreate') +); +router.delete( + '/api/v1/db/meta/hooks/:hookId', + metaApiMetrics, + ncMetaAclMw(hookDelete, 'hookDelete') +); +router.patch( + '/api/v1/db/meta/hooks/:hookId', + metaApiMetrics, + ncMetaAclMw(hookUpdate, 'hookUpdate') +); +router.get( + '/api/v1/db/meta/tables/:tableId/hooks/samplePayload/:operation', + metaApiMetrics, + catchError(tableSampleData) +); +export default router; diff --git a/packages/nocodb/src/lib/controllers/hookFilterController.ts b/packages/nocodb/src/lib/controllers/hookFilterController.ts new file mode 100644 index 0000000000..c83a754d05 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/hookFilterController.ts @@ -0,0 +1,90 @@ +import { Request, Response, Router } from 'express'; +import { T } from 'nc-help'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import { hookFilterService } from '../services'; + +export async function filterGet(req: Request, res: Response) { + const filter = await hookFilterService.filterGet({ + hookId: req.params.hookId, + }); + + res.json(filter); +} + +export async function filterList(req: Request, res: Response) { + const filter = await hookFilterService.filterList({ + hookId: req.params.hookId, + }); + + res.json(filter); +} + +export async function filterChildrenRead(req: Request, res: Response) { + const filter = await hookFilterService.filterChildrenRead({ + hookId: req.params.hookId, + filterParentId: req.params.filterParentId, + }); + + res.json(filter); +} + +export async function filterCreate(req: Request, res) { + const filter = await hookFilterService.filterCreate({ + filter: req.body, + hookId: req.params.hookId, + }); + + res.json(filter); +} + +export async function filterUpdate(req, res) { + const filter = await hookFilterService.filterUpdate({ + filterId: req.params.filterId, + filter: req.body, + hookId: req.params.hookId, + }); + + res.json(filter); +} + +export async function filterDelete(req: Request, res: Response) { + const filter = await hookFilterService.filterDelete({ + filterId: req.params.filterId, + }); + T.emit('evt', { evt_type: 'hookFilter:deleted' }); + res.json(filter); +} + +const router = Router({ mergeParams: true }); +router.get( + '/hooks/:hookId/filters/', + metaApiMetrics, + ncMetaAclMw(filterList, 'filterList') +); +router.post( + '/hooks/:hookId/filters/', + metaApiMetrics, + ncMetaAclMw(filterCreate, 'filterCreate') +); +router.get( + '/hooks/:hookId/filters/:filterId', + metaApiMetrics, + ncMetaAclMw(filterGet, 'filterGet') +); +router.patch( + '/hooks/:hookId/filters/:filterId', + metaApiMetrics, + ncMetaAclMw(filterUpdate, 'filterUpdate') +); +router.delete( + '/hooks/:hookId/filters/:filterId', + metaApiMetrics, + ncMetaAclMw(filterDelete, 'filterDelete') +); +router.get( + '/hooks/:hookId/filters/:filterParentId/children', + metaApiMetrics, + ncMetaAclMw(filterChildrenRead, 'filterChildrenRead') +); +export default router; diff --git a/packages/nocodb/src/lib/meta/api/kanbanViewApis.ts b/packages/nocodb/src/lib/controllers/kanbanViewController.ts similarity index 65% rename from packages/nocodb/src/lib/meta/api/kanbanViewApis.ts rename to packages/nocodb/src/lib/controllers/kanbanViewController.ts index c8d864bf82..bf36b43b90 100644 --- a/packages/nocodb/src/lib/meta/api/kanbanViewApis.ts +++ b/packages/nocodb/src/lib/controllers/kanbanViewController.ts @@ -1,18 +1,19 @@ import { Request, Response, Router } from 'express'; import { KanbanType, ViewTypes } from 'nocodb-sdk'; -import View from '../../models/View'; -import KanbanView from '../../models/KanbanView'; -import { Tele } from 'nc-help'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import { getAjvValidatorMw } from './helpers'; +import View from '../models/View'; +import KanbanView from '../models/KanbanView'; +import { T } from 'nc-help'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; + +// todo: map to service export async function kanbanViewGet(req: Request, res: Response) { res.json(await KanbanView.get(req.params.kanbanViewId)); } export async function kanbanViewCreate(req: Request, res) { - Tele.emit('evt', { evt_type: 'vtable:created', show_as: 'kanban' }); + T.emit('evt', { evt_type: 'vtable:created', show_as: 'kanban' }); const view = await View.insert({ ...req.body, // todo: sanitize @@ -23,7 +24,7 @@ export async function kanbanViewCreate(req: Request, res) { } export async function kanbanViewUpdate(req, res) { - Tele.emit('evt', { evt_type: 'view:updated', type: 'kanban' }); + T.emit('evt', { evt_type: 'view:updated', type: 'kanban' }); res.json(await KanbanView.update(req.params.kanbanViewId, req.body)); } @@ -32,13 +33,11 @@ const router = Router({ mergeParams: true }); router.post( '/api/v1/db/meta/tables/:tableId/kanbans', metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/KanbanReq'), ncMetaAclMw(kanbanViewCreate, 'kanbanViewCreate') ); router.patch( '/api/v1/db/meta/kanbans/:kanbanViewId', metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/KanbanUpdateReq'), ncMetaAclMw(kanbanViewUpdate, 'kanbanViewUpdate') ); router.get( diff --git a/packages/nocodb/src/lib/meta/api/mapViewApis.ts b/packages/nocodb/src/lib/controllers/mapViewController.ts similarity index 53% rename from packages/nocodb/src/lib/meta/api/mapViewApis.ts rename to packages/nocodb/src/lib/controllers/mapViewController.ts index 5771284da4..3a982d1b03 100644 --- a/packages/nocodb/src/lib/meta/api/mapViewApis.ts +++ b/packages/nocodb/src/lib/controllers/mapViewController.ts @@ -1,29 +1,30 @@ import { Request, Response, Router } from 'express'; -import { MapType, ViewTypes } from 'nocodb-sdk'; -import View from '../../models/View'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { Tele } from 'nc-help'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import MapView from '../../models/MapView'; +import { MapType } from 'nocodb-sdk'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import { mapViewService } from '../services'; export async function mapViewGet(req: Request, res: Response) { - res.json(await MapView.get(req.params.mapViewId)); + res.json( + await mapViewService.mapViewGet({ mapViewId: req.params.mapViewId }) + ); } export async function mapViewCreate(req: Request, res) { - Tele.emit('evt', { evt_type: 'vtable:created', show_as: 'map' }); - const view = await View.insert({ - ...req.body, - // todo: sanitize - fk_model_id: req.params.tableId, - type: ViewTypes.MAP, + const view = await mapViewService.mapViewCreate({ + tableId: req.params.tableId, + map: req.body, }); res.json(view); } export async function mapViewUpdate(req, res) { - Tele.emit('evt', { evt_type: 'view:updated', type: 'map' }); - res.json(await MapView.update(req.params.mapViewId, req.body)); + res.json( + await mapViewService.mapViewUpdate({ + mapViewId: req.params.mapViewId, + map: req.body, + }) + ); } const router = Router({ mergeParams: true }); diff --git a/packages/nocodb/src/lib/controllers/metaDiffController.ts b/packages/nocodb/src/lib/controllers/metaDiffController.ts new file mode 100644 index 0000000000..45ac703a0d --- /dev/null +++ b/packages/nocodb/src/lib/controllers/metaDiffController.ts @@ -0,0 +1,54 @@ +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { Router } from 'express'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import { metaDiffService } from '../services'; + +export async function metaDiff(req, res) { + res.json(await metaDiffService.metaDiff({ projectId: req.params.projectId })); +} + +export async function baseMetaDiff(req, res) { + res.json( + await metaDiffService.baseMetaDiff({ + baseId: req.params.baseId, + projectId: req.params.projectId, + }) + ); +} + +export async function metaDiffSync(req, res) { + await metaDiffService.metaDiffSync({ projectId: req.params.projectId }); + res.json({ msg: 'success' }); +} + +export async function baseMetaDiffSync(req, res) { + await metaDiffService.baseMetaDiffSync({ + projectId: req.params.projectId, + baseId: req.params.baseId, + }); + + res.json({ msg: 'success' }); +} + +const router = Router(); +router.get( + '/api/v1/db/meta/projects/:projectId/meta-diff', + metaApiMetrics, + ncMetaAclMw(metaDiff, 'metaDiff') +); +router.post( + '/api/v1/db/meta/projects/:projectId/meta-diff', + metaApiMetrics, + ncMetaAclMw(metaDiffSync, 'metaDiffSync') +); +router.get( + '/api/v1/db/meta/projects/:projectId/meta-diff/:baseId', + metaApiMetrics, + ncMetaAclMw(baseMetaDiff, 'baseMetaDiff') +); +router.post( + '/api/v1/db/meta/projects/:projectId/meta-diff/:baseId', + metaApiMetrics, + ncMetaAclMw(baseMetaDiffSync, 'baseMetaDiffSync') +); +export default router; diff --git a/packages/nocodb/src/lib/controllers/modelVisibilityController.ts b/packages/nocodb/src/lib/controllers/modelVisibilityController.ts new file mode 100644 index 0000000000..6af9a71506 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/modelVisibilityController.ts @@ -0,0 +1,34 @@ +import { Router } from 'express'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import { modelVisibilityService } from '../services'; + +async function xcVisibilityMetaSetAll(req, res) { + await modelVisibilityService.xcVisibilityMetaSetAll({ + visibilityRule: req.body, + projectId: req.params.projectId, + }); + + res.json({ msg: 'success' }); +} + +const router = Router({ mergeParams: true }); +router.get( + '/api/v1/db/meta/projects/:projectId/visibility-rules', + metaApiMetrics, + ncMetaAclMw(async (req, res) => { + res.json( + await modelVisibilityService.xcVisibilityMetaGet({ + projectId: req.params.projectId, + includeM2M: + req.query.includeM2M === true || req.query.includeM2M === 'true', + }) + ); + }, 'modelVisibilityList') +); +router.post( + '/api/v1/db/meta/projects/:projectId/visibility-rules', + metaApiMetrics, + ncMetaAclMw(xcVisibilityMetaSetAll, 'modelVisibilitySet') +); +export default router; diff --git a/packages/nocodb/src/lib/meta/api/orgLicenseApis.ts b/packages/nocodb/src/lib/controllers/orgLicenseController.ts similarity index 54% rename from packages/nocodb/src/lib/meta/api/orgLicenseApis.ts rename to packages/nocodb/src/lib/controllers/orgLicenseController.ts index ec13f4f9e7..23e5948ba1 100644 --- a/packages/nocodb/src/lib/meta/api/orgLicenseApis.ts +++ b/packages/nocodb/src/lib/controllers/orgLicenseController.ts @@ -1,21 +1,15 @@ import { Router } from 'express'; import { OrgUserRoles } from 'nocodb-sdk'; -import { NC_LICENSE_KEY } from '../../constants'; -import Store from '../../models/Store'; -import Noco from '../../Noco'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { getAjvValidatorMw } from './helpers'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { orgLicenseService } from '../services'; async function licenseGet(_req, res) { - const license = await Store.get(NC_LICENSE_KEY); - - res.json({ key: license?.value }); + res.json(await orgLicenseService.licenseGet()); } async function licenseSet(req, res) { - await Store.saveOrUpdate({ value: req.body.key, key: NC_LICENSE_KEY }); - await Noco.loadEEState(); + await orgLicenseService.licenseSet({ key: req.body.key }); res.json({ msg: 'License key saved' }); } @@ -31,7 +25,6 @@ router.get( router.post( '/api/v1/license', metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/LicenseReq'), ncMetaAclMw(licenseSet, 'licenseSet', { allowedRoles: [OrgUserRoles.SUPER_ADMIN], blockApiTokenAccess: true, diff --git a/packages/nocodb/src/lib/controllers/orgTokenController.ts b/packages/nocodb/src/lib/controllers/orgTokenController.ts new file mode 100644 index 0000000000..75e406fc42 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/orgTokenController.ts @@ -0,0 +1,63 @@ +import { Request, Response, Router } from 'express'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import { getConditionalHandler } from '../meta/helpers/getHandler'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { orgTokenService, orgTokenServiceEE } from '../services'; + +async function apiTokenList(req, res) { + res.json( + await getConditionalHandler( + orgTokenService.apiTokenList, + orgTokenServiceEE.apiTokenListEE + )({ + query: req.query, + user: req['user'], + }) + ); +} + +export async function apiTokenCreate(req: Request, res: Response) { + res.json( + await orgTokenService.apiTokenCreate({ + apiToken: req.body, + user: req['user'], + }) + ); +} + +export async function apiTokenDelete(req: Request, res: Response) { + res.json( + await orgTokenService.apiTokenDelete({ + token: req.params.token, + user: req['user'], + }) + ); +} + +const router = Router({ mergeParams: true }); + +router.get( + '/api/v1/tokens', + metaApiMetrics, + ncMetaAclMw(apiTokenList, 'apiTokenList', { + // allowedRoles: [OrgUserRoles.SUPER], + blockApiTokenAccess: true, + }) +); +router.post( + '/api/v1/tokens', + metaApiMetrics, + ncMetaAclMw(apiTokenCreate, 'apiTokenCreate', { + // allowedRoles: [OrgUserRoles.SUPER], + blockApiTokenAccess: true, + }) +); +router.delete( + '/api/v1/tokens/:token', + metaApiMetrics, + ncMetaAclMw(apiTokenDelete, 'apiTokenDelete', { + // allowedRoles: [OrgUserRoles.SUPER], + blockApiTokenAccess: true, + }) +); +export default router; diff --git a/packages/nocodb/src/lib/controllers/orgUserController.ts b/packages/nocodb/src/lib/controllers/orgUserController.ts new file mode 100644 index 0000000000..e2e6df488a --- /dev/null +++ b/packages/nocodb/src/lib/controllers/orgUserController.ts @@ -0,0 +1,154 @@ +import { Router } from 'express'; +import { OrgUserRoles } from 'nocodb-sdk'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { orgUserService } from '../services'; + +async function userList(req, res) { + res.json( + await orgUserService.userList({ + query: req.query, + }) + ); +} + +async function userUpdate(req, res) { + res.json( + await orgUserService.userUpdate({ + user: req.body, + userId: req.params.userId, + }) + ); +} + +async function userDelete(req, res) { + await orgUserService.userDelete({ + userId: req.params.userId, + }); + res.json({ msg: 'success' }); +} + +async function userAdd(req, res) { + const result = await orgUserService.userAdd({ + user: req.body, + req, + projectId: req.params.projectId, + }); + + res.json(result); +} + +async function userSettings(_req, res): Promise { + await orgUserService.userSettings({}); + res.json({}); +} + +async function userInviteResend(req, res): Promise { + await orgUserService.userInviteResend({ + userId: req.params.userId, + req, + }); + + res.json({ msg: 'success' }); +} + +async function generateResetUrl(req, res) { + const result = await orgUserService.generateResetUrl({ + siteUrl: req.ncSiteUrl, + userId: req.params.userId, + }); + + res.json(result); +} + +async function appSettingsGet(_req, res) { + const settings = await orgUserService.appSettingsGet(); + res.json(settings); +} + +async function appSettingsSet(req, res) { + await orgUserService.appSettingsSet({ + settings: req.body, + }); + + res.json({ msg: 'Settings saved' }); +} + +const router = Router({ mergeParams: true }); +router.get( + '/api/v1/users', + metaApiMetrics, + ncMetaAclMw(userList, 'userList', { + allowedRoles: [OrgUserRoles.SUPER_ADMIN], + blockApiTokenAccess: true, + }) +); +router.patch( + '/api/v1/users/:userId', + metaApiMetrics, + ncMetaAclMw(userUpdate, 'userUpdate', { + allowedRoles: [OrgUserRoles.SUPER_ADMIN], + blockApiTokenAccess: true, + }) +); +router.delete( + '/api/v1/users/:userId', + metaApiMetrics, + ncMetaAclMw(userDelete, 'userDelete', { + allowedRoles: [OrgUserRoles.SUPER_ADMIN], + blockApiTokenAccess: true, + }) +); +router.post( + '/api/v1/users', + metaApiMetrics, + ncMetaAclMw(userAdd, 'userAdd', { + allowedRoles: [OrgUserRoles.SUPER_ADMIN], + blockApiTokenAccess: true, + }) +); +router.post( + '/api/v1/users/settings', + metaApiMetrics, + ncMetaAclMw(userSettings, 'userSettings', { + allowedRoles: [OrgUserRoles.SUPER_ADMIN], + blockApiTokenAccess: true, + }) +); +router.post( + '/api/v1/users/:userId/resend-invite', + metaApiMetrics, + ncMetaAclMw(userInviteResend, 'userInviteResend', { + allowedRoles: [OrgUserRoles.SUPER_ADMIN], + blockApiTokenAccess: true, + }) +); + +router.post( + '/api/v1/users/:userId/generate-reset-url', + metaApiMetrics, + ncMetaAclMw(generateResetUrl, 'generateResetUrl', { + allowedRoles: [OrgUserRoles.SUPER_ADMIN], + blockApiTokenAccess: true, + }) +); + +router.get( + '/api/v1/app-settings', + metaApiMetrics, + ncMetaAclMw(appSettingsGet, 'appSettingsGet', { + allowedRoles: [OrgUserRoles.SUPER_ADMIN], + blockApiTokenAccess: true, + }) +); + +router.post( + '/api/v1/app-settings', + metaApiMetrics, + ncMetaAclMw(appSettingsSet, 'appSettingsSet', { + allowedRoles: [OrgUserRoles.SUPER_ADMIN], + blockApiTokenAccess: true, + }) +); + +export default router; diff --git a/packages/nocodb/src/lib/meta/api/pluginApis.ts b/packages/nocodb/src/lib/controllers/pluginController.ts similarity index 54% rename from packages/nocodb/src/lib/meta/api/pluginApis.ts rename to packages/nocodb/src/lib/controllers/pluginController.ts index 6934e17b41..8d3e1b6943 100644 --- a/packages/nocodb/src/lib/meta/api/pluginApis.ts +++ b/packages/nocodb/src/lib/controllers/pluginController.ts @@ -1,38 +1,35 @@ import { Request, Response, Router } from 'express'; -import { Tele } from 'nc-help'; -import { PagedResponseImpl } from '../helpers/PagedResponse'; -import Plugin from '../../models/Plugin'; +import { PagedResponseImpl } from '../meta/helpers/PagedResponse'; import { PluginType } from 'nocodb-sdk'; -import NcPluginMgrv2 from '../helpers/NcPluginMgrv2'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import { getAjvValidatorMw } from './helpers'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import { pluginService } from '../services'; export async function pluginList(_req: Request, res: Response) { - res.json(new PagedResponseImpl(await Plugin.list())); + res.json(new PagedResponseImpl(await pluginService.pluginList())); } export async function pluginTest(req: Request, res: Response) { - Tele.emit('evt', { evt_type: 'plugin:tested' }); - res.json(await NcPluginMgrv2.test(req.body)); + res.json(await pluginService.pluginTest({ body: req.body })); } export async function pluginRead(req: Request, res: Response) { - res.json(await Plugin.get(req.params.pluginId)); + res.json(await pluginService.pluginRead({ pluginId: req.params.pluginId })); } export async function pluginUpdate( req: Request, res: Response ) { - const plugin = await Plugin.update(req.params.pluginId, req.body); - Tele.emit('evt', { - evt_type: plugin.active ? 'plugin:installed' : 'plugin:uninstalled', - title: plugin.title, + const plugin = await pluginService.pluginUpdate({ + pluginId: req.params.pluginId, + plugin: req.body, }); res.json(plugin); } export async function isPluginActive(req: Request, res: Response) { - res.json(await Plugin.isPluginActive(req.params.pluginTitle)); + res.json( + await pluginService.isPluginActive({ pluginTitle: req.params.pluginTitle }) + ); } const router = Router({ mergeParams: true }); @@ -44,8 +41,6 @@ router.get( router.post( '/api/v1/db/meta/plugins/test', metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/PluginTestReq'), - ncMetaAclMw(pluginTest, 'pluginTest') ); router.get( @@ -56,7 +51,6 @@ router.get( router.patch( '/api/v1/db/meta/plugins/:pluginId', metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/PluginReq'), ncMetaAclMw(pluginUpdate, 'pluginUpdate') ); router.get( diff --git a/packages/nocodb/src/lib/controllers/projectController.ts b/packages/nocodb/src/lib/controllers/projectController.ts new file mode 100644 index 0000000000..18d761efb1 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/projectController.ts @@ -0,0 +1,169 @@ +import { Request, Response } from 'express'; +import { ProjectType } from 'nocodb-sdk'; +import Project from '../models/Project'; +import { ProjectListType } from 'nocodb-sdk'; +import { packageVersion } from '../utils/packageVersion'; +import { T } from 'nc-help'; +import { PagedResponseImpl } from '../meta/helpers/PagedResponse'; +import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import ProjectUser from '../models/ProjectUser'; +import Noco from '../Noco'; +import isDocker from 'is-docker'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import Filter from '../models/Filter'; + +import { projectService } from '../services'; + +// // Project CRUD + +export async function projectGet( + req: Request, + res: Response +) { + const project = await projectService.getProjectWithInfo({ + projectId: req.params.projectId, + }); + + projectService.sanitizeProject(project); + + res.json(project); +} + +export async function projectUpdate( + req: Request, + res: Response +) { + const project = await projectService.projectUpdate({ + projectId: req.params.projectId, + project: req.body, + }); + + res.json(project); +} + +export async function projectList( + req: Request & { user: { id: string; roles: string } }, + res: Response +) { + const projects = await projectService.projectList({ + user: req.user, + query: req.query, + }); + + res.json( + new PagedResponseImpl(projects as ProjectType[], { + count: projects.length, + limit: projects.length, + }) + ); +} + +export async function projectDelete(req: Request, res: Response) { + const deleted = await projectService.projectSoftDelete({ + projectId: req.params.projectId, + }); + + res.json(deleted); +} + +async function projectCreate(req: Request, res) { + const project = await projectService.projectCreate({ + project: req.body, + user: req['user'], + }); + + res.json(project); +} + +export async function projectInfoGet(_req, res) { + res.json({ + Node: process.version, + Arch: process.arch, + Platform: process.platform, + Docker: isDocker(), + RootDB: Noco.getConfig()?.meta?.db?.client, + PackageVersion: packageVersion, + }); +} + +export async function projectCost(req, res) { + let cost = 0; + const project = await Project.getWithInfo(req.params.projectId); + + for (const base of project.bases) { + const sqlClient = await NcConnectionMgrv2.getSqlClient(base); + const userCount = await ProjectUser.getUsersCount(req.query); + const recordCount = (await sqlClient.totalRecords())?.data.TotalRecords; + + if (recordCount > 100000) { + // 36,000 or $79/user/month + cost = Math.max(36000, 948 * userCount); + } else if (recordCount > 50000) { + // $36,000 or $50/user/month + cost = Math.max(36000, 600 * userCount); + } else if (recordCount > 10000) { + // $240/user/yr + cost = Math.min(240 * userCount, 36000); + } else if (recordCount > 1000) { + // $120/user/yr + cost = Math.min(120 * userCount, 36000); + } + } + + T.event({ + event: 'a:project:cost', + data: { + cost, + }, + }); + + res.json({ cost }); +} + +export async function hasEmptyOrNullFilters(req, res) { + res.json(await Filter.hasEmptyOrNullFilters(req.params.projectId)); +} + +export default (router) => { + router.get( + '/api/v1/db/meta/projects/:projectId/info', + metaApiMetrics, + ncMetaAclMw(projectInfoGet, 'projectInfoGet') + ); + router.get( + '/api/v1/db/meta/projects/:projectId', + metaApiMetrics, + ncMetaAclMw(projectGet, 'projectGet') + ); + router.patch( + '/api/v1/db/meta/projects/:projectId', + metaApiMetrics, + ncMetaAclMw(projectUpdate, 'projectUpdate') + ); + router.get( + '/api/v1/db/meta/projects/:projectId/cost', + metaApiMetrics, + ncMetaAclMw(projectCost, 'projectCost') + ); + router.delete( + '/api/v1/db/meta/projects/:projectId', + metaApiMetrics, + ncMetaAclMw(projectDelete, 'projectDelete') + ); + router.post( + '/api/v1/db/meta/projects', + metaApiMetrics, + ncMetaAclMw(projectCreate, 'projectCreate') + ); + router.get( + '/api/v1/db/meta/projects', + metaApiMetrics, + ncMetaAclMw(projectList, 'projectList') + ); + router.get( + '/api/v1/db/meta/projects/:projectId/has-empty-or-null-filters', + metaApiMetrics, + ncMetaAclMw(hasEmptyOrNullFilters, 'hasEmptyOrNullFilters') + ); +}; diff --git a/packages/nocodb/src/lib/controllers/projectUserController.ts b/packages/nocodb/src/lib/controllers/projectUserController.ts new file mode 100644 index 0000000000..f2bace15a4 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/projectUserController.ts @@ -0,0 +1,85 @@ +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { Router } from 'express'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import { projectUserService } from '../services'; + +async function userList(req, res) { + res.json({ + users: await projectUserService.userList({ + projectId: req.params.projectId, + query: req.query, + }), + }); +} + +async function userInvite(req, res): Promise { + res.json( + await projectUserService.userInvite({ + projectId: req.params.projectId, + projectUser: req.body, + req, + }) + ); +} + +// @ts-ignore +async function projectUserUpdate(req, res, next): Promise { + res.json( + await projectUserService.projectUserUpdate({ + projectUser: req.body, + projectId: req.params.projectId, + userId: req.params.userId, + req, + }) + ); +} + +async function projectUserDelete(req, res): Promise { + await projectUserService.projectUserDelete({ + projectId: req.params.projectId, + userId: req.params.userId, + req, + }); + res.json({ + msg: 'success', + }); +} + +async function projectUserInviteResend(req, res): Promise { + res.json( + await projectUserService.projectUserInviteResend({ + projectId: req.params.projectId, + userId: req.params.userId, + projectUser: req.body, + req, + }) + ); +} + +const router = Router({ mergeParams: true }); +router.get( + '/api/v1/db/meta/projects/:projectId/users', + metaApiMetrics, + ncMetaAclMw(userList, 'userList') +); +router.post( + '/api/v1/db/meta/projects/:projectId/users', + metaApiMetrics, + ncMetaAclMw(userInvite, 'userInvite') +); +router.patch( + '/api/v1/db/meta/projects/:projectId/users/:userId', + metaApiMetrics, + ncMetaAclMw(projectUserUpdate, 'projectUserUpdate') +); +router.delete( + '/api/v1/db/meta/projects/:projectId/users/:userId', + metaApiMetrics, + ncMetaAclMw(projectUserDelete, 'projectUserDelete') +); +router.post( + '/api/v1/db/meta/projects/:projectId/users/:userId/resend-invite', + metaApiMetrics, + ncMetaAclMw(projectUserInviteResend, 'projectUserInviteResend') +); +export default router; diff --git a/packages/nocodb/src/lib/controllers/publicControllers/index.ts b/packages/nocodb/src/lib/controllers/publicControllers/index.ts new file mode 100644 index 0000000000..1d63952955 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/publicControllers/index.ts @@ -0,0 +1,9 @@ +import publicDataController from './publicDataController'; +import publicDataExportController from './publicDataExportController'; +import publicMetaController from './publicMetaController'; + +export { + publicDataController, + publicDataExportController, + publicMetaController, +}; diff --git a/packages/nocodb/src/lib/controllers/publicControllers/publicDataController.ts b/packages/nocodb/src/lib/controllers/publicControllers/publicDataController.ts new file mode 100644 index 0000000000..46a73a0f2a --- /dev/null +++ b/packages/nocodb/src/lib/controllers/publicControllers/publicDataController.ts @@ -0,0 +1,105 @@ +import { Request, Response, Router } from 'express'; +import multer from 'multer'; +import { NC_ATTACHMENT_FIELD_SIZE } from '../../constants'; +import catchError from '../../meta/helpers/catchError'; +import { publicDataService } from '../../services'; + +export async function dataList(req: Request, res: Response) { + const pagedResponse = await publicDataService.dataList({ + query: req.query, + password: req.headers?.['xc-password'] as string, + sharedViewUuid: req.params.sharedViewUuid, + }); + res.json({ data: pagedResponse }); +} + +// todo: Handle the error case where view doesnt belong to model +async function groupedDataList(req: Request, res: Response) { + const groupedData = await publicDataService.groupedDataList({ + query: req.query, + password: req.headers?.['xc-password'] as string, + sharedViewUuid: req.params.sharedViewUuid, + groupColumnId: req.params.columnId, + }); + res.json(groupedData); +} + +async function dataInsert(req: Request & { files: any[] }, res: Response) { + const insertResult = await publicDataService.dataInsert({ + sharedViewUuid: req.params.sharedViewUuid, + password: req.headers?.['xc-password'] as string, + body: req.body?.data, + siteUrl: (req as any).ncSiteUrl, + files: req.files, + }); + + res.json(insertResult); +} + +async function relDataList(req, res) { + const pagedResponse = await publicDataService.relDataList({ + query: req.query, + password: req.headers?.['xc-password'] as string, + sharedViewUuid: req.params.sharedViewUuid, + columnId: req.params.columnId, + }); + + res.json(pagedResponse); +} + +export async function publicMmList(req: Request, res: Response) { + const paginatedResponse = await publicDataService.publicMmList({ + query: req.query, + password: req.headers?.['xc-password'] as string, + sharedViewUuid: req.params.sharedViewUuid, + columnId: req.params.columnId, + rowId: req.params.rowId, + }); + res.json(paginatedResponse); +} + +export async function publicHmList(req: Request, res: Response) { + const paginatedResponse = await publicDataService.publicHmList({ + query: req.query, + password: req.headers?.['xc-password'] as string, + sharedViewUuid: req.params.sharedViewUuid, + columnId: req.params.columnId, + rowId: req.params.rowId, + }); + res.json(paginatedResponse); +} + +const router = Router({ mergeParams: true }); +router.get( + '/api/v1/db/public/shared-view/:sharedViewUuid/rows', + catchError(dataList) +); +router.get( + '/api/v1/db/public/shared-view/:sharedViewUuid/group/:columnId', + catchError(groupedDataList) +); +router.get( + '/api/v1/db/public/shared-view/:sharedViewUuid/nested/:columnId', + catchError(relDataList) +); +router.post( + '/api/v1/db/public/shared-view/:sharedViewUuid/rows', + multer({ + storage: multer.diskStorage({}), + limits: { + fieldSize: NC_ATTACHMENT_FIELD_SIZE, + }, + }).any(), + catchError(dataInsert) +); + +router.get( + '/api/v1/db/public/shared-view/:sharedViewUuid/rows/:rowId/mm/:colId', + catchError(publicMmList) +); +router.get( + '/api/v1/db/public/shared-view/:sharedViewUuid/rows/:rowId/hm/:colId', + catchError(publicHmList) +); + +export default router; diff --git a/packages/nocodb/src/lib/meta/api/publicApis/publicDataExportApis.ts b/packages/nocodb/src/lib/controllers/publicControllers/publicDataExportController.ts similarity index 92% rename from packages/nocodb/src/lib/meta/api/publicApis/publicDataExportApis.ts rename to packages/nocodb/src/lib/controllers/publicControllers/publicDataExportController.ts index 936cc68c9b..cc677d6ab6 100644 --- a/packages/nocodb/src/lib/meta/api/publicApis/publicDataExportApis.ts +++ b/packages/nocodb/src/lib/controllers/publicControllers/publicDataExportController.ts @@ -1,17 +1,19 @@ import { Request, Response, Router } from 'express'; import * as XLSX from 'xlsx'; -import View from '../../../models/View'; -import Model from '../../../models/Model'; -import Base from '../../../models/Base'; -import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; import { nocoExecute } from 'nc-help'; import papaparse from 'papaparse'; import { ErrorMessages, isSystemColumn, UITypes, ViewTypes } from 'nocodb-sdk'; -import Column from '../../../models/Column'; -import LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn'; -import LookupColumn from '../../../models/LookupColumn'; -import catchError, { NcError } from '../../helpers/catchError'; -import getAst from '../../../db/sql-data-mapper/lib/sql/helpers/getAst'; +import getAst from '../../db/sql-data-mapper/lib/sql/helpers/getAst'; +import catchError, { NcError } from '../../meta/helpers/catchError'; +import { + Base, + Column, + LinkToAnotherRecordColumn, + LookupColumn, + Model, + View, +} from '../../models'; +import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; async function exportExcel(req: Request, res: Response) { const view = await View.getByUUID(req.params.publicDataUuid); diff --git a/packages/nocodb/src/lib/controllers/publicControllers/publicMetaController.ts b/packages/nocodb/src/lib/controllers/publicControllers/publicMetaController.ts new file mode 100644 index 0000000000..238e3223ae --- /dev/null +++ b/packages/nocodb/src/lib/controllers/publicControllers/publicMetaController.ts @@ -0,0 +1,31 @@ +import { Request, Response, Router } from 'express'; +import catchError from '../../meta/helpers/catchError'; +import { publicMetaService } from '../../services'; + +export async function viewMetaGet(req: Request, res: Response) { + res.json( + await publicMetaService.viewMetaGet({ + password: req.headers?.['xc-password'] as string, + sharedViewUuid: req.params.sharedViewUuid, + }) + ); +} +async function publicSharedBaseGet(req, res): Promise { + res.json( + await publicMetaService.publicSharedBaseGet({ + sharedBaseUuid: req.params.sharedBaseUuid, + }) + ); +} + +const router = Router({ mergeParams: true }); +router.get( + '/api/v1/db/public/shared-view/:sharedViewUuid/meta', + catchError(viewMetaGet) +); + +router.get( + '/api/v1/db/public/shared-base/:sharedBaseUuid/meta', + catchError(publicSharedBaseGet) +); +export default router; diff --git a/packages/nocodb/src/lib/controllers/sharedBaseController.ts b/packages/nocodb/src/lib/controllers/sharedBaseController.ts new file mode 100644 index 0000000000..1fd9fb27cd --- /dev/null +++ b/packages/nocodb/src/lib/controllers/sharedBaseController.ts @@ -0,0 +1,61 @@ +import { Router } from 'express'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { sharedBaseService } from '../services'; + +async function createSharedBaseLink(req, res): Promise { + const sharedBase = await sharedBaseService.createSharedBaseLink({ + projectId: req.params.projectId, + roles: req.body?.roles, + password: req.body?.password, + siteUrl: req.ncSiteUrl, + }); + + res.json(sharedBase); +} + +async function updateSharedBaseLink(req, res): Promise { + const sharedBase = await sharedBaseService.updateSharedBaseLink({ + projectId: req.params.projectId, + roles: req.body?.roles, + password: req.body?.password, + siteUrl: req.ncSiteUrl, + }); + + res.json(sharedBase); +} + +async function disableSharedBaseLink(req, res): Promise { + const sharedBase = await sharedBaseService.disableSharedBaseLink({ + projectId: req.params.projectId, + }); + + res.json(sharedBase); +} + +async function getSharedBaseLink(req, res): Promise { + const sharedBase = await sharedBaseService.getSharedBaseLink({ + projectId: req.params.projectId, + siteUrl: req.ncSiteUrl, + }); + + res.json(sharedBase); +} + +const router = Router({ mergeParams: true }); +router.get( + '/api/v1/db/meta/projects/:projectId/shared', + ncMetaAclMw(getSharedBaseLink, 'getSharedBaseLink') +); +router.post( + '/api/v1/db/meta/projects/:projectId/shared', + ncMetaAclMw(createSharedBaseLink, 'createSharedBaseLink') +); +router.patch( + '/api/v1/db/meta/projects/:projectId/shared', + ncMetaAclMw(updateSharedBaseLink, 'updateSharedBaseLink') +); +router.delete( + '/api/v1/db/meta/projects/:projectId/shared', + ncMetaAclMw(disableSharedBaseLink, 'disableSharedBaseLink') +); +export default router; diff --git a/packages/nocodb/src/lib/controllers/sortController.ts b/packages/nocodb/src/lib/controllers/sortController.ts new file mode 100644 index 0000000000..332e31e419 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/sortController.ts @@ -0,0 +1,80 @@ +import { Request, Response, Router } from 'express'; +import { PagedResponseImpl } from '../meta/helpers/PagedResponse'; +import { SortListType, SortReqType } from 'nocodb-sdk'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; + +import { sortService } from '../services'; + +// @ts-ignore +export async function sortList( + req: Request, + res: Response +) { + const sortList = await sortService.sortList({ + viewId: req.params.viewId, + }); + res.json({ + sorts: new PagedResponseImpl(sortList), + }); +} + +// @ts-ignore +export async function sortCreate(req: Request, res) { + const sort = await sortService.sortCreate({ + sort: req.body, + viewId: req.params.viewId, + }); + res.json(sort); +} + +export async function sortUpdate(req, res) { + const sort = await sortService.sortUpdate({ + sortId: req.params.sortId, + sort: req.body, + }); + res.json(sort); +} + +export async function sortDelete(req: Request, res: Response) { + const sort = await sortService.sortDelete({ + sortId: req.params.sortId, + }); + res.json(sort); +} +export async function sortGet(req: Request, res: Response) { + const sort = await sortService.sortGet({ + sortId: req.params.sortId, + }); + res.json(sort); +} + +const router = Router({ mergeParams: true }); +router.get( + '/api/v1/db/meta/views/:viewId/sorts/', + metaApiMetrics, + ncMetaAclMw(sortList, 'sortList') +); +router.post( + '/api/v1/db/meta/views/:viewId/sorts/', + metaApiMetrics, + ncMetaAclMw(sortCreate, 'sortCreate') +); + +router.get( + '/api/v1/db/meta/sorts/:sortId', + metaApiMetrics, + ncMetaAclMw(sortGet, 'sortGet') +); + +router.patch( + '/api/v1/db/meta/sorts/:sortId', + metaApiMetrics, + ncMetaAclMw(sortUpdate, 'sortUpdate') +); +router.delete( + '/api/v1/db/meta/sorts/:sortId', + metaApiMetrics, + ncMetaAclMw(sortDelete, 'sortDelete') +); +export default router; diff --git a/packages/nocodb/src/lib/controllers/swaggerController/index.ts b/packages/nocodb/src/lib/controllers/swaggerController/index.ts new file mode 100644 index 0000000000..1853e4c7ff --- /dev/null +++ b/packages/nocodb/src/lib/controllers/swaggerController/index.ts @@ -0,0 +1,36 @@ +import { Router } from 'express'; +import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw'; +import getSwaggerHtml from './swaggerHtml'; +import getRedocHtml from './redocHtml'; +import { swaggerService } from '../../services'; + +async function swaggerJson(req, res) { + const swagger = await swaggerService.swaggerJson({ + projectId: req.params.projectId, + siteUrl: req.ncSiteUrl, + }); + + res.json(swagger); +} + +function swaggerHtml(_, res) { + res.send(getSwaggerHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' })); +} + +function redocHtml(_, res) { + res.send(getRedocHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' })); +} + +const router = Router({ mergeParams: true }); + +// todo: auth +router.get( + '/api/v1/db/meta/projects/:projectId/swagger.json', + ncMetaAclMw(swaggerJson, 'swaggerJson') +); + +router.get('/api/v1/db/meta/projects/:projectId/swagger', swaggerHtml); + +router.get('/api/v1/db/meta/projects/:projectId/redoc', redocHtml); + +export default router; diff --git a/packages/nocodb/src/lib/meta/api/swagger/redocHtml.ts b/packages/nocodb/src/lib/controllers/swaggerController/redocHtml.ts similarity index 100% rename from packages/nocodb/src/lib/meta/api/swagger/redocHtml.ts rename to packages/nocodb/src/lib/controllers/swaggerController/redocHtml.ts diff --git a/packages/nocodb/src/lib/meta/api/swagger/swaggerHtml.ts b/packages/nocodb/src/lib/controllers/swaggerController/swaggerHtml.ts similarity index 100% rename from packages/nocodb/src/lib/meta/api/swagger/swaggerHtml.ts rename to packages/nocodb/src/lib/controllers/swaggerController/swaggerHtml.ts diff --git a/packages/nocodb/src/lib/meta/api/sync/importApis.ts b/packages/nocodb/src/lib/controllers/syncController/importApis.ts similarity index 87% rename from packages/nocodb/src/lib/meta/api/sync/importApis.ts rename to packages/nocodb/src/lib/controllers/syncController/importApis.ts index 92ab241117..f161a40ada 100644 --- a/packages/nocodb/src/lib/meta/api/sync/importApis.ts +++ b/packages/nocodb/src/lib/controllers/syncController/importApis.ts @@ -1,13 +1,14 @@ import { Request, Router } from 'express'; // import { Queue } from 'bullmq'; // import axios from 'axios'; -import catchError, { NcError } from '../../helpers/catchError'; +import catchError, { NcError } from '../../meta/helpers/catchError'; import { Server } from 'socket.io'; -import NocoJobs from '../../../jobs/NocoJobs'; -import job, { AirtableSyncConfig } from './helpers/job'; -import SyncSource from '../../../models/SyncSource'; -import Noco from '../../../Noco'; -import { genJwt } from '../userApi/helpers'; +import NocoJobs from '../../jobs/NocoJobs'; +import { SyncSource } from '../../models'; +import Noco from '../../Noco'; +import { syncService, userService } from '../../services'; +import { AirtableSyncConfig } from '../../services/syncService/helpers/job'; + const AIRTABLE_IMPORT_JOB = 'AIRTABLE_IMPORT_JOB'; const AIRTABLE_PROGRESS_JOB = 'AIRTABLE_PROGRESS_JOB'; @@ -23,7 +24,10 @@ export default ( jobs: { [id: string]: { last_message: any } } ) => { // add importer job handler and progress notification job handler - NocoJobs.jobsMgr.addJobWorker(AIRTABLE_IMPORT_JOB, job); + NocoJobs.jobsMgr.addJobWorker( + AIRTABLE_IMPORT_JOB, + syncService.airtableImportJob + ); NocoJobs.jobsMgr.addJobWorker( AIRTABLE_PROGRESS_JOB, ({ payload, progress }) => { @@ -94,7 +98,7 @@ export default ( const syncSource = await SyncSource.get(req.params.syncId); const user = await syncSource.getUser(); - const token = genJwt(user, Noco.getConfig()); + const token = userService.genJwt(user, Noco.getConfig()); // Treat default baseUrl as siteUrl from req object let baseURL = (req as any).ncSiteUrl; diff --git a/packages/nocodb/src/lib/meta/api/sync/syncSourceApis.ts b/packages/nocodb/src/lib/controllers/syncController/index.ts similarity index 53% rename from packages/nocodb/src/lib/meta/api/sync/syncSourceApis.ts rename to packages/nocodb/src/lib/controllers/syncController/index.ts index f9e761ff71..7dbfac49b5 100644 --- a/packages/nocodb/src/lib/meta/api/sync/syncSourceApis.ts +++ b/packages/nocodb/src/lib/controllers/syncController/index.ts @@ -1,42 +1,42 @@ import { Request, Response, Router } from 'express'; - -import SyncSource from '../../../models/SyncSource'; -import { Tele } from 'nc-help'; -import { PagedResponseImpl } from '../../helpers/PagedResponse'; -import ncMetaAclMw from '../../helpers/ncMetaAclMw'; -import Project from '../../../models/Project'; +import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw'; +import { syncService } from '../../services'; export async function syncSourceList(req: Request, res: Response) { // todo: pagination res.json( - new PagedResponseImpl( - await SyncSource.list(req.params.projectId, req.params.baseId) - ) + syncService.syncSourceList({ + projectId: req.params.projectId, + }) ); } export async function syncCreate(req: Request, res: Response) { - Tele.emit('evt', { evt_type: 'webhooks:created' }); - const project = await Project.getWithInfo(req.params.projectId); - - const sync = await SyncSource.insert({ - ...req.body, - fk_user_id: (req as any).user.id, - base_id: req.params.baseId ? req.params.baseId : project.bases[0].id, - project_id: req.params.projectId, - }); - res.json(sync); + res.json( + await syncService.syncCreate({ + projectId: req.params.projectId, + baseId: req.params.baseId, + userId: (req as any).user.id, + syncPayload: req.body, + }) + ); } export async function syncDelete(req: Request, res: Response) { - Tele.emit('evt', { evt_type: 'webhooks:deleted' }); - res.json(await SyncSource.delete(req.params.syncId)); + res.json( + await syncService.syncDelete({ + syncId: req.params.syncId, + }) + ); } export async function syncUpdate(req: Request, res: Response) { - Tele.emit('evt', { evt_type: 'webhooks:updated' }); - - res.json(await SyncSource.update(req.params.syncId, req.body)); + res.json( + await syncService.syncUpdate({ + syncId: req.params.syncId, + syncPayload: req.body, + }) + ); } const router = Router({ mergeParams: true }); diff --git a/packages/nocodb/src/lib/controllers/tableController.ts b/packages/nocodb/src/lib/controllers/tableController.ts new file mode 100644 index 0000000000..c59c8d4a13 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/tableController.ts @@ -0,0 +1,111 @@ +import { Request, Response, Router } from 'express'; +import { TableListType, TableReqType, TableType } from 'nocodb-sdk'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { PagedResponseImpl } from '../meta/helpers/PagedResponse'; +import { tableService } from '../services'; + +export async function tableList(req: Request, res: Response) { + res.json( + new PagedResponseImpl( + await tableService.getAccessibleTables({ + projectId: req.params.projectId, + baseId: req.params.baseId, + includeM2M: req.query?.includeM2M === 'true', + roles: (req as any).session?.passport?.user?.roles, + }) + ) + ); +} + +export async function tableCreate(req: Request, res) { + const result = await tableService.tableCreate({ + projectId: req.params.projectId, + baseId: req.params.baseId, + table: req.body, + user: (req as any).session?.passport?.user, + }); + + res.json(result); +} + +export async function tableGet(req: Request, res: Response) { + const table = await tableService.getTableWithAccessibleViews({ + tableId: req.params.tableId, + user: (req as any).session?.passport?.user, + }); + + res.json(table); +} + +export async function tableDelete(req: Request, res: Response) { + const result = await tableService.tableDelete({ + tableId: req.params.tableId, + user: (req as any).session?.passport?.user, + req, + }); + + res.json(result); +} + +export async function tableReorder(req: Request, res: Response) { + res.json( + await tableService.reorderTable({ + tableId: req.params.tableId, + order: req.body.order, + }) + ); +} + +// todo: move to table service +export async function tableUpdate(req: Request, res) { + await tableService.tableUpdate({ + tableId: req.params.tableId, + table: req.body, + projectId: (req as any).ncProjectId, + }); + res.json({ msg: 'success' }); +} + +const router = Router({ mergeParams: true }); +router.get( + '/api/v1/db/meta/projects/:projectId/tables', + metaApiMetrics, + ncMetaAclMw(tableList, 'tableList') +); +router.get( + '/api/v1/db/meta/projects/:projectId/:baseId/tables', + metaApiMetrics, + ncMetaAclMw(tableList, 'tableList') +); +router.post( + '/api/v1/db/meta/projects/:projectId/tables', + metaApiMetrics, + ncMetaAclMw(tableCreate, 'tableCreate') +); +router.post( + '/api/v1/db/meta/projects/:projectId/:baseId/tables', + metaApiMetrics, + ncMetaAclMw(tableCreate, 'tableCreate') +); +router.get( + '/api/v1/db/meta/tables/:tableId', + metaApiMetrics, + ncMetaAclMw(tableGet, 'tableGet') +); +router.patch( + '/api/v1/db/meta/tables/:tableId', + metaApiMetrics, + ncMetaAclMw(tableUpdate, 'tableUpdate') +); +router.delete( + '/api/v1/db/meta/tables/:tableId', + metaApiMetrics, + ncMetaAclMw(tableDelete, 'tableDelete') +); +router.post( + '/api/v1/db/meta/tables/:tableId/reorder', + metaApiMetrics, + ncMetaAclMw(tableReorder, 'tableReorder') +); +export default router; diff --git a/packages/nocodb/src/lib/meta/api/testApis.ts b/packages/nocodb/src/lib/controllers/testController.ts similarity index 85% rename from packages/nocodb/src/lib/meta/api/testApis.ts rename to packages/nocodb/src/lib/controllers/testController.ts index 506f9d4474..08ea1e780e 100644 --- a/packages/nocodb/src/lib/meta/api/testApis.ts +++ b/packages/nocodb/src/lib/controllers/testController.ts @@ -1,5 +1,5 @@ import { Request, Router } from 'express'; -import { TestResetService } from '../../services/test/TestResetService'; +import { TestResetService } from '../services/test/TestResetService'; export async function reset(req: Request, res) { const service = new TestResetService({ diff --git a/packages/nocodb/src/lib/controllers/userController/index.ts b/packages/nocodb/src/lib/controllers/userController/index.ts new file mode 100644 index 0000000000..8843b2e5ea --- /dev/null +++ b/packages/nocodb/src/lib/controllers/userController/index.ts @@ -0,0 +1,445 @@ +import { Request, Response } from 'express'; +import { TableType, validatePassword } from 'nocodb-sdk'; +import { T } from 'nc-help'; + +const { isEmail } = require('validator'); +import * as ejs from 'ejs'; + +import bcrypt from 'bcryptjs'; +import { promisify } from 'util'; + +const { v4: uuidv4 } = require('uuid'); + +import passport from 'passport'; +import { getAjvValidatorMw } from '../../meta/api/helpers'; +import catchError, { NcError } from '../../meta/helpers/catchError'; +import extractProjectIdAndAuthenticate from '../../meta/helpers/extractProjectIdAndAuthenticate'; +import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw'; +import NcPluginMgrv2 from '../../meta/helpers/NcPluginMgrv2'; +import { Audit, User } from '../../models'; +import Noco from '../../Noco'; +import { userService } from '../../services'; + +export async function signup(req: Request, res: Response) { + const { + email: _email, + firstname, + lastname, + token, + ignore_subscribe, + } = req.body; + let { password } = req.body; + + // validate password and throw error if password is satisfying the conditions + const { valid, error } = validatePassword(password); + if (!valid) { + NcError.badRequest(`Password : ${error}`); + } + + if (!isEmail(_email)) { + NcError.badRequest(`Invalid email`); + } + + const email = _email.toLowerCase(); + + let user = await User.getByEmail(email); + + if (user) { + if (token) { + if (token !== user.invite_token) { + NcError.badRequest(`Invalid invite url`); + } else if (user.invite_token_expires < new Date()) { + NcError.badRequest( + 'Expired invite url, Please contact super admin to get a new invite url' + ); + } + } else { + // todo : opening up signup for timebeing + // return next(new Error(`Email '${email}' already registered`)); + } + } + + const salt = await promisify(bcrypt.genSalt)(10); + password = await promisify(bcrypt.hash)(password, salt); + const email_verification_token = uuidv4(); + + if (!ignore_subscribe) { + T.emit('evt_subscribe', email); + } + + if (user) { + if (token) { + await User.update(user.id, { + firstname, + lastname, + salt, + password, + email_verification_token, + invite_token: null, + invite_token_expires: null, + email: user.email, + }); + } else { + NcError.badRequest('User already exist'); + } + } else { + await userService.registerNewUserIfAllowed({ + firstname, + lastname, + email, + salt, + password, + email_verification_token, + }); + } + user = await User.getByEmail(email); + + try { + const template = (await import('./ui/emailTemplates/verify')).default; + await ( + await NcPluginMgrv2.emailAdapter() + ).mailSend({ + to: email, + subject: 'Verify email', + html: ejs.render(template, { + verifyLink: + (req as any).ncSiteUrl + + `/email/verify/${user.email_verification_token}`, + }), + }); + } catch (e) { + console.log( + 'Warning : `mailSend` failed, Please configure emailClient configuration.' + ); + } + await promisify((req as any).login.bind(req))(user); + const refreshToken = userService.randomTokenString(); + await User.update(user.id, { + refresh_token: refreshToken, + email: user.email, + }); + + setTokenCookie(res, refreshToken); + + user = (req as any).user; + + await Audit.insert({ + op_type: 'AUTHENTICATION', + op_sub_type: 'SIGNUP', + user: user.email, + description: `signed up `, + ip: (req as any).clientIp, + }); + + res.json({ + token: userService.genJwt(user, Noco.getConfig()), + } as any); +} + +async function successfulSignIn({ + user, + err, + info, + req, + res, + auditDescription, +}) { + try { + if (!user || !user.email) { + if (err) { + return res.status(400).send(err); + } + if (info) { + return res.status(400).send(info); + } + return res.status(400).send({ msg: 'Your signin has failed' }); + } + + await promisify((req as any).login.bind(req))(user); + const refreshToken = userService.randomTokenString(); + + if (!user.token_version) { + user.token_version = userService.randomTokenString(); + } + + await User.update(user.id, { + refresh_token: refreshToken, + email: user.email, + token_version: user.token_version, + }); + setTokenCookie(res, refreshToken); + + await Audit.insert({ + op_type: 'AUTHENTICATION', + op_sub_type: 'SIGNIN', + user: user.email, + ip: req.clientIp, + description: auditDescription, + }); + + res.json({ + token: userService.genJwt(user, Noco.getConfig()), + } as any); + } catch (e) { + console.log(e); + throw e; + } +} + +async function signin(req, res, next) { + passport.authenticate( + 'local', + { session: false }, + async (err, user, info): Promise => + await successfulSignIn({ + user, + err, + info, + req, + res, + auditDescription: 'signed in', + }) + )(req, res, next); +} + +async function googleSignin(req, res, next) { + passport.authenticate( + 'google', + { + session: false, + callbackURL: req.ncSiteUrl + Noco.getConfig().dashboardPath, + }, + async (err, user, info): Promise => + await successfulSignIn({ + user, + err, + info, + req, + res, + auditDescription: 'signed in using Google Auth', + }) + )(req, res, next); +} + +function setTokenCookie(res: Response, token): void { + // create http only cookie with refresh token that expires in 7 days + const cookieOptions = { + httpOnly: true, + expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + }; + res.cookie('refresh_token', token, cookieOptions); +} + +async function me(req, res): Promise { + res.json(req?.session?.passport?.user ?? {}); +} + +async function passwordChange(req: Request, res): Promise { + if (!(req as any).isAuthenticated()) { + NcError.forbidden('Not allowed'); + } + + await userService.passwordChange({ + user: req['user'], + req, + body: req.body, + }); + + res.json({ msg: 'Password updated successfully' }); +} + +async function passwordForgot(req: Request, res): Promise { + await userService.passwordForgot({ + siteUrl: (req as any).ncSiteUrl, + body: req.body, + req, + }); + + res.json({ msg: 'Please check your email to reset the password' }); +} + +async function tokenValidate(req, res): Promise { + await userService.tokenValidate({ + token: req.params.tokenId, + }); + res.json(true); +} + +async function passwordReset(req, res): Promise { + await userService.passwordReset({ + token: req.params.tokenId, + body: req.body, + req, + }); + + res.json({ msg: 'Password reset successful' }); +} + +async function emailVerification(req, res): Promise { + await userService.emailVerification({ + token: req.params.tokenId, + req, + }); + + res.json({ msg: 'Email verified successfully' }); +} + +async function refreshToken(req, res): Promise { + try { + if (!req?.cookies?.refresh_token) { + return res.status(400).json({ msg: 'Missing refresh token' }); + } + + const user = await User.getByRefreshToken(req.cookies.refresh_token); + + if (!user) { + return res.status(400).json({ msg: 'Invalid refresh token' }); + } + + const refreshToken = userService.randomTokenString(); + + await User.update(user.id, { + email: user.email, + refresh_token: refreshToken, + }); + + setTokenCookie(res, refreshToken); + + res.json({ + token: userService.genJwt(user, Noco.getConfig()), + } as any); + } catch (e) { + return res.status(400).json({ msg: e.message }); + } +} + +async function renderPasswordReset(req, res): Promise { + try { + res.send( + ejs.render((await import('./ui/auth/resetPassword')).default, { + ncPublicUrl: process.env.NC_PUBLIC_URL || '', + token: JSON.stringify(req.params.tokenId), + baseUrl: `/`, + }) + ); + } catch (e) { + return res.status(400).json({ msg: e.message }); + } +} + +const mapRoutes = (router) => { + // todo: old api - /auth/signup?tool=1 + router.post( + '/auth/user/signup', + getAjvValidatorMw('swagger.json#/components/schemas/SignUpReq'), + catchError(signup) + ); + router.post( + '/auth/user/signin', + getAjvValidatorMw('swagger.json#/components/schemas/SignInReq'), + catchError(signin) + ); + router.get('/auth/user/me', extractProjectIdAndAuthenticate, catchError(me)); + router.post('/auth/password/forgot', catchError(passwordForgot)); + router.post('/auth/token/validate/:tokenId', catchError(tokenValidate)); + router.post( + '/auth/password/reset/:tokenId', + getAjvValidatorMw('swagger.json#/components/schemas/PasswordResetReq'), + catchError(passwordReset) + ); + router.post('/auth/email/validate/:tokenId', catchError(emailVerification)); + router.post( + '/user/password/change', + ncMetaAclMw(passwordChange, 'passwordChange') + ); + router.post('/auth/token/refresh', catchError(refreshToken)); + + /* Google auth apis */ + + router.post(`/auth/google/genTokenByCode`, catchError(googleSignin)); + + router.get('/auth/google', (req: any, res, next) => + passport.authenticate('google', { + scope: ['profile', 'email'], + state: req.query.state, + callbackURL: req.ncSiteUrl + Noco.getConfig().dashboardPath, + })(req, res, next) + ); + + // deprecated APIs + router.post( + '/api/v1/db/auth/user/signup', + getAjvValidatorMw('swagger.json#/components/schemas/SignUpReq'), + catchError(signup) + ); + router.post( + '/api/v1/db/auth/user/signin', + getAjvValidatorMw('swagger.json#/components/schemas/SignInReq'), + catchError(signin) + ); + router.get( + '/api/v1/db/auth/user/me', + extractProjectIdAndAuthenticate, + catchError(me) + ); + router.post('/api/v1/db/auth/password/forgot', catchError(passwordForgot)); + router.post( + '/api/v1/db/auth/token/validate/:tokenId', + catchError(tokenValidate) + ); + router.post( + '/api/v1/db/auth/password/reset/:tokenId', + catchError(passwordReset) + ); + router.post( + '/api/v1/db/auth/email/validate/:tokenId', + catchError(emailVerification) + ); + router.post( + '/api/v1/db/auth/password/change', + ncMetaAclMw(passwordChange, 'passwordChange') + ); + router.post('/api/v1/db/auth/token/refresh', catchError(refreshToken)); + router.get( + '/api/v1/db/auth/password/reset/:tokenId', + catchError(renderPasswordReset) + ); + + // new API + router.post( + '/api/v1/auth/user/signup', + getAjvValidatorMw('swagger.json#/components/schemas/SignUpReq'), + catchError(signup) + ); + router.post( + '/api/v1/auth/user/signin', + getAjvValidatorMw('swagger.json#/components/schemas/SignInReq'), + catchError(signin) + ); + router.get( + '/api/v1/auth/user/me', + extractProjectIdAndAuthenticate, + catchError(me) + ); + router.post('/api/v1/auth/password/forgot', catchError(passwordForgot)); + router.post( + '/api/v1/auth/token/validate/:tokenId', + catchError(tokenValidate) + ); + router.post( + '/api/v1/auth/password/reset/:tokenId', + catchError(passwordReset) + ); + router.post( + '/api/v1/auth/email/validate/:tokenId', + catchError(emailVerification) + ); + router.post( + '/api/v1/auth/password/change', + ncMetaAclMw(passwordChange, 'passwordChange') + ); + router.post('/api/v1/auth/token/refresh', catchError(refreshToken)); + // respond with password reset page + router.get('/auth/password/reset/:tokenId', catchError(renderPasswordReset)); +}; +export { mapRoutes as userController }; diff --git a/packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts b/packages/nocodb/src/lib/controllers/userController/initStrategies.ts similarity index 95% rename from packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts rename to packages/nocodb/src/lib/controllers/userController/initStrategies.ts index b40f2c58d1..3b6b99c9aa 100644 --- a/packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts +++ b/packages/nocodb/src/lib/controllers/userController/initStrategies.ts @@ -1,6 +1,4 @@ import { OrgUserRoles } from 'nocodb-sdk'; -import User from '../../../models/User'; -import ProjectUser from '../../../models/ProjectUser'; import { promisify } from 'util'; import { Strategy as CustomStrategy } from 'passport-custom'; import passport from 'passport'; @@ -17,13 +15,11 @@ const jwtOptions = { }; import bcrypt from 'bcryptjs'; -import Project from '../../../models/Project'; -import NocoCache from '../../../cache/NocoCache'; -import { CacheGetType, CacheScope } from '../../../utils/globals'; -import ApiToken from '../../../models/ApiToken'; -import Noco from '../../../Noco'; -import Plugin from '../../../models/Plugin'; -import { registerNewUserIfAllowed } from './userApis'; +import NocoCache from '../../cache/NocoCache'; +import { ApiToken, Plugin, Project, ProjectUser, User } from '../../models'; +import Noco from '../../Noco'; +import { CacheGetType, CacheScope } from '../../utils/globals'; +import { userService } from '../../services'; export function initStrategies(router): void { passport.use( @@ -311,7 +307,7 @@ export function initStrategies(router): void { // or return error } else { const salt = await promisify(bcrypt.genSalt)(10); - const user = await registerNewUserIfAllowed({ + const user = await userService.registerNewUserIfAllowed({ firstname: null, lastname: null, email_verification_token: null, diff --git a/packages/nocodb/src/lib/meta/api/userApi/ui/auth/emailVerify.ts b/packages/nocodb/src/lib/controllers/userController/ui/auth/emailVerify.ts similarity index 100% rename from packages/nocodb/src/lib/meta/api/userApi/ui/auth/emailVerify.ts rename to packages/nocodb/src/lib/controllers/userController/ui/auth/emailVerify.ts diff --git a/packages/nocodb/src/lib/meta/api/userApi/ui/auth/resetPassword.ts b/packages/nocodb/src/lib/controllers/userController/ui/auth/resetPassword.ts similarity index 100% rename from packages/nocodb/src/lib/meta/api/userApi/ui/auth/resetPassword.ts rename to packages/nocodb/src/lib/controllers/userController/ui/auth/resetPassword.ts diff --git a/packages/nocodb/src/lib/meta/api/userApi/ui/emailTemplates/forgotPassword.ts b/packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/forgotPassword.ts similarity index 100% rename from packages/nocodb/src/lib/meta/api/userApi/ui/emailTemplates/forgotPassword.ts rename to packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/forgotPassword.ts diff --git a/packages/nocodb/src/lib/meta/api/userApi/ui/emailTemplates/invite.ts b/packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/invite.ts similarity index 100% rename from packages/nocodb/src/lib/meta/api/userApi/ui/emailTemplates/invite.ts rename to packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/invite.ts diff --git a/packages/nocodb/src/lib/meta/api/userApi/ui/emailTemplates/verify.ts b/packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/verify.ts similarity index 100% rename from packages/nocodb/src/lib/meta/api/userApi/ui/emailTemplates/verify.ts rename to packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/verify.ts diff --git a/packages/nocodb/src/lib/meta/api/userApi/userApis.ts b/packages/nocodb/src/lib/controllers/userController/userApis.ts similarity index 57% rename from packages/nocodb/src/lib/meta/api/userApi/userApis.ts rename to packages/nocodb/src/lib/controllers/userController/userApis.ts index 809b96a6c1..70d17b40ef 100644 --- a/packages/nocodb/src/lib/meta/api/userApi/userApis.ts +++ b/packages/nocodb/src/lib/controllers/userController/userApis.ts @@ -1,82 +1,24 @@ import { Request, Response } from 'express'; import { TableType, validatePassword } from 'nocodb-sdk'; -import { OrgUserRoles } from 'nocodb-sdk'; -import { NC_APP_SETTINGS } from '../../../constants'; -import Store from '../../../models/Store'; -import { Tele } from 'nc-help'; -import catchError, { NcError } from '../../helpers/catchError'; +import { T } from 'nc-help'; const { isEmail } = require('validator'); import * as ejs from 'ejs'; import bcrypt from 'bcryptjs'; import { promisify } from 'util'; -import User from '../../../models/User'; const { v4: uuidv4 } = require('uuid'); -import Audit from '../../../models/Audit'; -import NcPluginMgrv2 from '../../helpers/NcPluginMgrv2'; import passport from 'passport'; -import extractProjectIdAndAuthenticate from '../../helpers/extractProjectIdAndAuthenticate'; -import ncMetaAclMw from '../../helpers/ncMetaAclMw'; -import { MetaTable } from '../../../utils/globals'; -import Noco from '../../../Noco'; -import { getAjvValidatorMw } from '../helpers'; -import { genJwt } from './helpers'; -import { randomTokenString } from '../../helpers/stringHelpers'; - -export async function registerNewUserIfAllowed({ - firstname, - lastname, - email, - salt, - password, - email_verification_token, -}: { - firstname; - lastname; - email: string; - salt: any; - password; - email_verification_token; -}) { - let roles: string = OrgUserRoles.CREATOR; - - if (await User.isFirst()) { - roles = `${OrgUserRoles.CREATOR},${OrgUserRoles.SUPER_ADMIN}`; - // todo: update in nc_store - // roles = 'owner,creator,editor' - Tele.emit('evt', { - evt_type: 'project:invite', - count: 1, - }); - } else { - let settings: { invite_only_signup?: boolean } = {}; - try { - settings = JSON.parse((await Store.get(NC_APP_SETTINGS))?.value); - } catch {} - - if (settings?.invite_only_signup) { - NcError.badRequest('Not allowed to signup, contact super admin.'); - } else { - roles = OrgUserRoles.VIEWER; - } - } - - const token_version = randomTokenString(); - - return await User.insert({ - firstname, - lastname, - email, - salt, - password, - email_verification_token, - roles, - token_version, - }); -} +import { getAjvValidatorMw } from '../../meta/api/helpers'; +import catchError, { NcError } from '../../meta/helpers/catchError'; +import extractProjectIdAndAuthenticate from '../../meta/helpers/extractProjectIdAndAuthenticate'; +import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw'; +import NcPluginMgrv2 from '../../meta/helpers/NcPluginMgrv2'; +import { Audit, User } from '../../models'; +import Noco from '../../Noco'; +import { userService } from '../../services'; export async function signup(req: Request, res: Response) { const { @@ -122,7 +64,7 @@ export async function signup(req: Request, res: Response) { const email_verification_token = uuidv4(); if (!ignore_subscribe) { - Tele.emit('evt_subscribe', email); + T.emit('evt_subscribe', email); } if (user) { @@ -141,7 +83,7 @@ export async function signup(req: Request, res: Response) { NcError.badRequest('User already exist'); } } else { - await registerNewUserIfAllowed({ + await userService.registerNewUserIfAllowed({ firstname, lastname, email, @@ -171,7 +113,7 @@ export async function signup(req: Request, res: Response) { ); } await promisify((req as any).login.bind(req))(user); - const refreshToken = randomTokenString(); + const refreshToken = userService.randomTokenString(); await User.update(user.id, { refresh_token: refreshToken, email: user.email, @@ -190,7 +132,7 @@ export async function signup(req: Request, res: Response) { }); res.json({ - token: genJwt(user, Noco.getConfig()), + token: userService.genJwt(user, Noco.getConfig()), } as any); } @@ -214,10 +156,10 @@ async function successfulSignIn({ } await promisify((req as any).login.bind(req))(user); - const refreshToken = randomTokenString(); + const refreshToken = userService.randomTokenString(); if (!user.token_version) { - user.token_version = randomTokenString(); + user.token_version = userService.randomTokenString(); } await User.update(user.id, { @@ -236,7 +178,7 @@ async function successfulSignIn({ }); res.json({ - token: genJwt(user, Noco.getConfig()), + token: userService.genJwt(user, Noco.getConfig()), } as any); } catch (e) { console.log(e); @@ -279,15 +221,13 @@ async function googleSignin(req, res, next) { )(req, res, next); } -const REFRESH_TOKEN_COOKIE_KEY = 'refresh_token'; - -function setTokenCookie(res, token): void { +function setTokenCookie(res: Response, token): void { // create http only cookie with refresh token that expires in 7 days const cookieOptions = { httpOnly: true, expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), }; - res.cookie(REFRESH_TOKEN_COOKIE_KEY, token, cookieOptions); + res.cookie('refresh_token', token, cookieOptions); } async function me(req, res): Promise { @@ -298,184 +238,47 @@ async function passwordChange(req: Request, res): Promise { if (!(req as any).isAuthenticated()) { NcError.forbidden('Not allowed'); } - const { currentPassword, newPassword } = req.body; - if (!currentPassword || !newPassword) { - return NcError.badRequest('Missing new/old password'); - } - - // validate password and throw error if password is satisfying the conditions - const { valid, error } = validatePassword(newPassword); - if (!valid) { - NcError.badRequest(`Password : ${error}`); - } - - const user = await User.getByEmail((req as any).user.email); - const hashedPassword = await promisify(bcrypt.hash)( - currentPassword, - user.salt - ); - if (hashedPassword !== user.password) { - return NcError.badRequest('Current password is wrong'); - } - - const salt = await promisify(bcrypt.genSalt)(10); - const password = await promisify(bcrypt.hash)(newPassword, salt); - await User.update(user.id, { - salt, - password, - email: user.email, - token_version: null, - }); - - await Audit.insert({ - op_type: 'AUTHENTICATION', - op_sub_type: 'PASSWORD_CHANGE', - user: user.email, - description: `changed password `, - ip: (req as any).clientIp, + await userService.passwordChange({ + user: req['user'], + req, + body: req.body, }); res.json({ msg: 'Password updated successfully' }); } async function passwordForgot(req: Request, res): Promise { - const _email = req.body.email; - if (!_email) { - NcError.badRequest('Please enter your email address.'); - } - - const email = _email.toLowerCase(); - const user = await User.getByEmail(email); - - if (user) { - const token = uuidv4(); - await User.update(user.id, { - email: user.email, - reset_password_token: token, - reset_password_expires: new Date(Date.now() + 60 * 60 * 1000), - token_version: null, - }); - try { - const template = (await import('./ui/emailTemplates/forgotPassword')) - .default; - await NcPluginMgrv2.emailAdapter().then((adapter) => - adapter.mailSend({ - to: user.email, - subject: 'Password Reset Link', - text: `Visit following link to update your password : ${ - (req as any).ncSiteUrl - }/auth/password/reset/${token}.`, - html: ejs.render(template, { - resetLink: (req as any).ncSiteUrl + `/auth/password/reset/${token}`, - }), - }) - ); - } catch (e) { - console.log(e); - return NcError.badRequest( - 'Email Plugin is not found. Please contact administrators to configure it in App Store first.' - ); - } + await userService.passwordForgot({ + siteUrl: (req as any).ncSiteUrl, + body: req.body, + req, + }); - await Audit.insert({ - op_type: 'AUTHENTICATION', - op_sub_type: 'PASSWORD_FORGOT', - user: user.email, - description: `requested for password reset `, - ip: (req as any).clientIp, - }); - } else { - return NcError.badRequest('Your email has not been registered.'); - } res.json({ msg: 'Please check your email to reset the password' }); } async function tokenValidate(req, res): Promise { - const token = req.params.tokenId; - - const user = await Noco.ncMeta.metaGet(null, null, MetaTable.USERS, { - reset_password_token: token, + await userService.tokenValidate({ + token: req.params.tokenId, }); - - if (!user || !user.email) { - NcError.badRequest('Invalid reset url'); - } - if (new Date(user.reset_password_expires) < new Date()) { - NcError.badRequest('Password reset url expired'); - } res.json(true); } async function passwordReset(req, res): Promise { - const token = req.params.tokenId; - - const user = await Noco.ncMeta.metaGet(null, null, MetaTable.USERS, { - reset_password_token: token, - }); - - if (!user) { - NcError.badRequest('Invalid reset url'); - } - if (user.reset_password_expires < new Date()) { - NcError.badRequest('Password reset url expired'); - } - if (user.provider && user.provider !== 'local') { - NcError.badRequest('Email registered via social account'); - } - - // validate password and throw error if password is satisfying the conditions - const { valid, error } = validatePassword(req.body.password); - if (!valid) { - NcError.badRequest(`Password : ${error}`); - } - - const salt = await promisify(bcrypt.genSalt)(10); - const password = await promisify(bcrypt.hash)(req.body.password, salt); - - await User.update(user.id, { - salt, - password, - email: user.email, - reset_password_expires: null, - reset_password_token: '', - token_version: null, - }); - - await Audit.insert({ - op_type: 'AUTHENTICATION', - op_sub_type: 'PASSWORD_RESET', - user: user.email, - description: `did reset password `, - ip: req.clientIp, + await userService.passwordReset({ + token: req.params.tokenId, + body: req.body, + req, }); res.json({ msg: 'Password reset successful' }); } async function emailVerification(req, res): Promise { - const token = req.params.tokenId; - - const user = await Noco.ncMeta.metaGet(null, null, MetaTable.USERS, { - email_verification_token: token, - }); - - if (!user) { - NcError.badRequest('Invalid verification url'); - } - - await User.update(user.id, { - email: user.email, - email_verification_token: '', - email_verified: true, - }); - - await Audit.insert({ - op_type: 'AUTHENTICATION', - op_sub_type: 'EMAIL_VERIFICATION', - user: user.email, - description: `verified email `, - ip: req.clientIp, + await userService.emailVerification({ + token: req.params.tokenId, + req, }); res.json({ msg: 'Email verified successfully' }); @@ -487,15 +290,13 @@ async function refreshToken(req, res): Promise { return res.status(400).json({ msg: 'Missing refresh token' }); } - const user = await User.getByRefreshToken( - req.cookies[REFRESH_TOKEN_COOKIE_KEY] - ); + const user = await User.getByRefreshToken(req.cookies.refresh_token); if (!user) { return res.status(400).json({ msg: 'Invalid refresh token' }); } - const refreshToken = randomTokenString(); + const refreshToken = userService.randomTokenString(); await User.update(user.id, { email: user.email, @@ -505,7 +306,7 @@ async function refreshToken(req, res): Promise { setTokenCookie(res, refreshToken); res.json({ - token: genJwt(user, Noco.getConfig()), + token: userService.genJwt(user, Noco.getConfig()), } as any); } catch (e) { return res.status(400).json({ msg: e.message }); @@ -526,30 +327,6 @@ async function renderPasswordReset(req, res): Promise { } } -// clear refresh token cookie and update user refresh token to null -const signout = async (req, res): Promise => { - const resBody = { msg: 'Success' }; - if (!req.cookies[REFRESH_TOKEN_COOKIE_KEY]) { - return res.json(resBody); - } - - const user = await User.getByRefreshToken( - req.cookies[REFRESH_TOKEN_COOKIE_KEY] - ); - - if (!user) { - return res.json(resBody); - } - - res.clearCookie(REFRESH_TOKEN_COOKIE_KEY); - - await User.update(user.id, { - refresh_token: null, - }); - - res.json(resBody); -}; - const mapRoutes = (router) => { // todo: old api - /auth/signup?tool=1 router.post( @@ -562,7 +339,6 @@ const mapRoutes = (router) => { getAjvValidatorMw('swagger.json#/components/schemas/SignInReq'), catchError(signin) ); - router.post('/auth/user/signout', catchError(signout)); router.get('/auth/user/me', extractProjectIdAndAuthenticate, catchError(me)); router.post( '/auth/password/forgot', @@ -651,7 +427,6 @@ const mapRoutes = (router) => { getAjvValidatorMw('swagger.json#/components/schemas/SignInReq'), catchError(signin) ); - router.post('/api/v1/auth/user/signout', catchError(signout)); router.get( '/api/v1/auth/user/me', extractProjectIdAndAuthenticate, @@ -683,4 +458,4 @@ const mapRoutes = (router) => { // respond with password reset page router.get('/auth/password/reset/:tokenId', catchError(renderPasswordReset)); }; -export { mapRoutes as userApis }; +export { mapRoutes as userController }; diff --git a/packages/nocodb/src/lib/controllers/utilController.ts b/packages/nocodb/src/lib/controllers/utilController.ts new file mode 100644 index 0000000000..e5e2b504b6 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/utilController.ts @@ -0,0 +1,59 @@ +// // Project CRUD +import { Request, Response } from 'express'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import catchError from '../meta/helpers/catchError'; +import { utilService } from '../services'; + +export async function testConnection(req: Request, res: Response) { + res.json(await utilService.testConnection({ body: req.body })); +} + +export async function appInfo(req: Request, res: Response) { + res.json( + await utilService.appInfo({ + req: { + ncSiteUrl: (req as any).ncSiteUrl, + }, + }) + ); +} + +export async function versionInfo(_req: Request, res: Response) { + res.json(await utilService.versionInfo()); +} + +export async function appHealth(_: Request, res: Response) { + res.json(await utilService.appHealth()); +} + +export async function axiosRequestMake(req: Request, res: Response) { + res.json(await utilService.axiosRequestMake({ body: req.body })); +} + +export async function aggregatedMetaInfo(_req: Request, res: Response) { + res.json(await utilService.aggregatedMetaInfo()); +} + +export async function urlToDbConfig(req: Request, res: Response) { + res.json( + await utilService.urlToDbConfig({ + body: req.body, + }) + ); +} + +export default (router) => { + router.post( + '/api/v1/db/meta/connection/test', + ncMetaAclMw(testConnection, 'testConnection') + ); + router.get('/api/v1/db/meta/nocodb/info', catchError(appInfo)); + router.post('/api/v1/db/meta/axiosRequestMake', catchError(axiosRequestMake)); + router.get('/api/v1/version', catchError(versionInfo)); + router.get('/api/v1/health', catchError(appHealth)); + router.post('/api/v1/url_to_config', catchError(urlToDbConfig)); + router.get( + '/api/v1/aggregated-meta-info', + ncMetaAclMw(aggregatedMetaInfo, 'aggregatedMetaInfo') + ); +}; diff --git a/packages/nocodb/src/lib/meta/api/viewColumnApis.ts b/packages/nocodb/src/lib/controllers/viewColumnController.ts similarity index 50% rename from packages/nocodb/src/lib/meta/api/viewColumnApis.ts rename to packages/nocodb/src/lib/controllers/viewColumnController.ts index 9da4a8c51d..aece1ca06e 100644 --- a/packages/nocodb/src/lib/meta/api/viewColumnApis.ts +++ b/packages/nocodb/src/lib/controllers/viewColumnController.ts @@ -1,34 +1,30 @@ import { Request, Response, Router } from 'express'; -import View from '../../models/View'; -import { Tele } from 'nc-help'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import { getAjvValidatorMw } from './helpers'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import { viewColumnService } from '../services'; export async function columnList(req: Request, res: Response) { - res.json(await View.getColumns(req.params.viewId)); + res.json(await viewColumnService.columnList({ viewId: req.params.viewId })); } + export async function columnAdd(req: Request, res: Response) { - const viewColumn = await View.insertOrUpdateColumn( - req.params.viewId, - req.body.fk_column_id, - { + const viewColumn = await viewColumnService.columnAdd({ + viewId: req.params.viewId, + columnId: req.body.fk_column_id, + column: { ...req.body, view_id: req.params.viewId, - } - ); - Tele.emit('evt', { evt_type: 'viewColumn:inserted' }); - + }, + }); res.json(viewColumn); } export async function columnUpdate(req: Request, res: Response) { - const result = await View.updateColumn( - req.params.viewId, - req.params.columnId, - req.body - ); - Tele.emit('evt', { evt_type: 'viewColumn:updated' }); + const result = await viewColumnService.columnUpdate({ + viewId: req.params.viewId, + columnId: req.params.columnId, + column: req.body, + }); res.json(result); } @@ -41,13 +37,11 @@ router.get( router.post( '/api/v1/db/meta/views/:viewId/columns/', metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/ViewColumnReq'), ncMetaAclMw(columnAdd, 'columnAdd') ); router.patch( '/api/v1/db/meta/views/:viewId/columns/:columnId', metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/ViewColumnUpdateReq'), ncMetaAclMw(columnUpdate, 'viewColumnUpdate') ); export default router; diff --git a/packages/nocodb/src/lib/controllers/viewController.ts b/packages/nocodb/src/lib/controllers/viewController.ts new file mode 100644 index 0000000000..edbc2ed736 --- /dev/null +++ b/packages/nocodb/src/lib/controllers/viewController.ts @@ -0,0 +1,133 @@ +import { Request, Response, Router } from 'express'; +import { PagedResponseImpl } from '../meta/helpers/PagedResponse'; +import { View } from '../models'; +import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; +import { metaApiMetrics } from '../meta/helpers/apiMetrics'; +import { viewService } from '../services'; + +// @ts-ignore +export async function viewGet(req: Request, res: Response) {} + +// @ts-ignore +export async function viewList(req: Request, res: Response) { + const filteredViewList = await viewService.viewList({ + tableId: req.params.tableId, + user: (req as any).session?.passport?.user, + }); + + res.json(new PagedResponseImpl(filteredViewList)); +} + +// @ts-ignore +export async function shareView( + req: Request, + res: Response +) { + res.json(await viewService.shareView({ viewId: req.params.viewId })); +} + +// @ts-ignore +export async function viewCreate(req: Request, res, next) {} + +// @ts-ignore +export async function viewUpdate(req, res) { + const result = await viewService.viewUpdate({ + viewId: req.params.viewId, + view: req.body, + }); + res.json(result); +} + +// @ts-ignore +export async function viewDelete(req: Request, res: Response, next) { + const result = await viewService.viewDelete({ viewId: req.params.viewId }); + res.json(result); +} + +async function shareViewUpdate(req: Request, res) { + res.json( + await viewService.shareViewUpdate({ + viewId: req.params.viewId, + sharedView: req.body, + }) + ); +} + +async function shareViewDelete(req: Request, res) { + res.json(await viewService.shareViewDelete({ viewId: req.params.viewId })); +} + +async function showAllColumns(req: Request, res) { + res.json( + await viewService.showAllColumns({ + viewId: req.params.viewId, + ignoreIds: (req.query?.ignoreIds || []), + }) + ); +} + +async function hideAllColumns(req: Request, res) { + res.json( + await viewService.hideAllColumns({ + viewId: req.params.viewId, + ignoreIds: (req.query?.ignoreIds || []), + }) + ); +} + +async function shareViewList(req: Request, res) { + res.json( + await viewService.shareViewList({ + tableId: req.params.tableId, + }) + ); +} + +const router = Router({ mergeParams: true }); +router.get( + '/api/v1/db/meta/tables/:tableId/views', + metaApiMetrics, + ncMetaAclMw(viewList, 'viewList') +); +router.patch( + '/api/v1/db/meta/views/:viewId', + metaApiMetrics, + ncMetaAclMw(viewUpdate, 'viewUpdate') +); +router.delete( + '/api/v1/db/meta/views/:viewId', + metaApiMetrics, + ncMetaAclMw(viewDelete, 'viewDelete') +); +router.post( + '/api/v1/db/meta/views/:viewId/show-all', + metaApiMetrics, + ncMetaAclMw(showAllColumns, 'showAllColumns') +); +router.post( + '/api/v1/db/meta/views/:viewId/hide-all', + metaApiMetrics, + ncMetaAclMw(hideAllColumns, 'hideAllColumns') +); + +router.get( + '/api/v1/db/meta/tables/:tableId/share', + metaApiMetrics, + ncMetaAclMw(shareViewList, 'shareViewList') +); +router.post( + '/api/v1/db/meta/views/:viewId/share', + ncMetaAclMw(shareView, 'shareView') +); +router.patch( + '/api/v1/db/meta/views/:viewId/share', + metaApiMetrics, + ncMetaAclMw(shareViewUpdate, 'shareViewUpdate') +); +router.delete( + '/api/v1/db/meta/views/:viewId/share', + metaApiMetrics, + ncMetaAclMw(shareViewDelete, 'shareViewDelete') +); + +export default router; diff --git a/packages/nocodb/src/lib/db/sql-client/lib/KnexClient.ts b/packages/nocodb/src/lib/db/sql-client/lib/KnexClient.ts index 9c47ab34ac..7643c92c9b 100644 --- a/packages/nocodb/src/lib/db/sql-client/lib/KnexClient.ts +++ b/packages/nocodb/src/lib/db/sql-client/lib/KnexClient.ts @@ -1,6 +1,6 @@ /* eslint-disable no-constant-condition */ import { knex, Knex } from 'knex'; -import { Tele } from 'nc-help'; +import { T } from 'nc-help'; import Debug from '../../util/Debug'; import Emit from '../../util/emit'; import Result from '../../util/Result'; @@ -620,7 +620,7 @@ class KnexClient extends SqlClient { KnexClient.___ext = await this._validateInput(); } if (!KnexClient.___ext) { - Tele.emit('evt', { + T.emit('evt', { evt_type: 'project:external', payload: null, check: true, diff --git a/packages/nocodb/src/lib/db/sql-mgr/SqlMgr.ts b/packages/nocodb/src/lib/db/sql-mgr/SqlMgr.ts index 3f6a85b9c2..55ea1559d7 100644 --- a/packages/nocodb/src/lib/db/sql-mgr/SqlMgr.ts +++ b/packages/nocodb/src/lib/db/sql-mgr/SqlMgr.ts @@ -7,7 +7,7 @@ import fsExtra from 'fs-extra'; import importFresh from 'import-fresh'; import inflection from 'inflection'; import slash from 'slash'; -import { Tele } from 'nc-help'; +import { T } from 'nc-help'; import SqlClientFactory from '../sql-client/lib/SqlClientFactory'; // import debug from 'debug'; @@ -1047,7 +1047,7 @@ export default class SqlMgr { // t = process.hrtime(); const data = await require('axios')(...apiMeta); - Tele.emit('evt', { evt_type: 'import:excel:url' }); + T.emit('evt', { evt_type: 'import:excel:url' }); return data.data; } diff --git a/packages/nocodb/src/lib/meta/NcMetaMgr.ts b/packages/nocodb/src/lib/meta/NcMetaMgr.ts index aecaff7189..da98f2c5e9 100644 --- a/packages/nocodb/src/lib/meta/NcMetaMgr.ts +++ b/packages/nocodb/src/lib/meta/NcMetaMgr.ts @@ -40,7 +40,7 @@ import NcTemplateParser from '../v1-legacy/templates/NcTemplateParser'; import { defaultConnectionConfig } from '../utils/NcConfigFactory'; import xcMetaDiff from './handlers/xcMetaDiff'; import { UITypes } from 'nocodb-sdk'; -import { Tele } from 'nc-help'; +import { T } from 'nc-help'; import { NC_ATTACHMENT_FIELD_SIZE } from '../constants'; const randomID = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 10); const XC_PLUGIN_DET = 'XC_PLUGIN_DET'; @@ -953,7 +953,7 @@ export default class NcMetaMgr { ip: req.clientIp, }); - Tele.emit('evt', { evt_type: 'webhooks:deleted' }); + T.emit('evt', { evt_type: 'webhooks:deleted' }); } catch (e) { throw e; } @@ -988,7 +988,7 @@ export default class NcMetaMgr { ip: req.clientIp, }); - Tele.emit('evt', { evt_type: 'webhooks:updated' }); + T.emit('evt', { evt_type: 'webhooks:updated' }); } else { const res = await this.xcMeta.metaInsert( projectId, @@ -1009,7 +1009,7 @@ export default class NcMetaMgr { description: `created webhook ${args.args.data.title} - ${args.args.data.event} ${args.args.data.operation} - ${args.args.data.notification?.type} - of table ${args.args.tn} `, ip: req.clientIp, }); - Tele.emit('evt', { evt_type: 'webhooks:created' }); + T.emit('evt', { evt_type: 'webhooks:created' }); return res; } @@ -1269,7 +1269,7 @@ export default class NcMetaMgr { } catch (e) { throw e; } finally { - Tele.emit('evt', { evt_type: 'image:uploaded' }); + T.emit('evt', { evt_type: 'image:uploaded' }); } } @@ -1595,7 +1595,7 @@ export default class NcMetaMgr { ip: req.clientIp, }); - Tele.emit('evt', { evt_type: 'project:created' }); + T.emit('evt', { evt_type: 'project:created' }); break; case 'projectUpdateByWeb': @@ -1603,7 +1603,7 @@ export default class NcMetaMgr { this.getProjectId(args), args.args.projectJson ); - Tele.emit('evt', { evt_type: 'project:updated' }); + T.emit('evt', { evt_type: 'project:updated' }); break; case 'projectCreateByOneClick': { @@ -1635,7 +1635,7 @@ export default class NcMetaMgr { description: `created project ${config.title}(${result.id}) `, ip: req.clientIp, }); - Tele.emit('evt', { evt_type: 'project:created', oneClick: true }); + T.emit('evt', { evt_type: 'project:created', oneClick: true }); } break; case 'projectCreateByWebWithXCDB': { @@ -1689,10 +1689,10 @@ export default class NcMetaMgr { ip: req.clientIp, }); - Tele.emit('evt', { evt_type: 'project:created', xcdb: true }); + T.emit('evt', { evt_type: 'project:created', xcdb: true }); postListenerCb = async () => { if (args?.args?.template) { - Tele.emit('evt', { + T.emit('evt', { evt_type: args.args?.quickImport ? 'project:created:fromExcel' : 'project:created:fromTemplate', @@ -1730,7 +1730,7 @@ export default class NcMetaMgr { case 'projectDelete': case 'projectRestart': case 'projectStart': - Tele.emit('evt', { evt_type: 'project:' + args.api }); + T.emit('evt', { evt_type: 'project:' + args.api }); result = null; break; @@ -2145,7 +2145,7 @@ export default class NcMetaMgr { ip: req.clientIp, }); - Tele.emit('evt', { evt_type: 'acl:updated' }); + T.emit('evt', { evt_type: 'acl:updated' }); return res; } catch (e) { @@ -3485,7 +3485,7 @@ export default class NcMetaMgr { ['id', 'view_id', 'view_type'] ); res.url = `${req.ncSiteUrl}${this.config.dashboardPath}#/nc/view/${res.view_id}`; - Tele.emit('evt', { evt_type: 'sharedView:generated-link' }); + T.emit('evt', { evt_type: 'sharedView:generated-link' }); return res; } catch (e) { console.log(e); @@ -3549,7 +3549,7 @@ export default class NcMetaMgr { sharedBase.url = `${req.ncSiteUrl}${this.config.dashboardPath}#/nc/base/${sharedBase.shared_base_id}`; - Tele.emit('evt', { evt_type: 'sharedBase:generated-link' }); + T.emit('evt', { evt_type: 'sharedBase:generated-link' }); return sharedBase; } catch (e) { console.log(e); @@ -3606,7 +3606,7 @@ export default class NcMetaMgr { // await this.xcMeta.metaUpdate(this.getProjectId(args), this.getDbAlias(args), 'nc_shared_views', { // password: args.args?.password // }, args.args.id); - // Tele.emit('evt', {evt_type: 'sharedView:password-updated'}) + // T.emit('evt', {evt_type: 'sharedView:password-updated'}) // return {msg: 'Success'}; // } catch (e) { // console.log(e) @@ -3623,7 +3623,7 @@ export default class NcMetaMgr { 'nc_shared_views', args.args.id ); - Tele.emit('evt', { evt_type: 'sharedView:deleted' }); + T.emit('evt', { evt_type: 'sharedView:deleted' }); return { msg: 'Success' }; } catch (e) { console.log(e); @@ -4477,7 +4477,7 @@ export default class NcMetaMgr { }); } - Tele.emit('evt', { evt_type: 'template:imported' }); + T.emit('evt', { evt_type: 'template:imported' }); return result; } @@ -5071,7 +5071,7 @@ export default class NcMetaMgr { } catch (e) { throw e; } finally { - Tele.emit('evt', { + T.emit('evt', { evt_type: 'plugin:installed', title: args.args.title, }); @@ -5153,7 +5153,7 @@ export default class NcMetaMgr { ip: req.clientIp, }); - Tele.emit('evt', { + T.emit('evt', { evt_type: 'vtable:created', show_as: args.args.show_as, }); @@ -5270,7 +5270,7 @@ export default class NcMetaMgr { }); await RestAuthCtrl.instance.loadLatestApiTokens(); - Tele.emit('evt', { evt_type: 'apiToken:created' }); + T.emit('evt', { evt_type: 'apiToken:created' }); return { description: args.args.description, token, @@ -5282,7 +5282,7 @@ export default class NcMetaMgr { } protected async xcApiTokenDelete(args): Promise { - Tele.emit('evt', { evt_type: 'apiToken:deleted' }); + T.emit('evt', { evt_type: 'apiToken:deleted' }); const res = await this.xcMeta.metaDelete( null, null, @@ -5327,7 +5327,7 @@ export default class NcMetaMgr { ip: req.clientIp, }); - Tele.emit('evt', { + T.emit('evt', { evt_type: 'vtable:renamed', show_as: args.args.show_as, }); @@ -5335,7 +5335,7 @@ export default class NcMetaMgr { } protected async xcVirtualTableUpdate(args): Promise { - // Tele.emit('evt', {evt_type: 'vtable:updated',show_as: args.args.show_as}) + // T.emit('evt', {evt_type: 'vtable:updated',show_as: args.args.show_as}) return this.xcMeta.metaUpdate( this.getProjectId(args), this.getDbAlias(args), @@ -5482,7 +5482,7 @@ export default class NcMetaMgr { ip: req.clientIp, }); - Tele.emit('evt', { evt_type: 'vtable:deleted' }); + T.emit('evt', { evt_type: 'vtable:deleted' }); return res; } diff --git a/packages/nocodb/src/lib/meta/NcMetaMgrEE.ts b/packages/nocodb/src/lib/meta/NcMetaMgrEE.ts index dfa8dff104..c0e10ec8ab 100644 --- a/packages/nocodb/src/lib/meta/NcMetaMgrEE.ts +++ b/packages/nocodb/src/lib/meta/NcMetaMgrEE.ts @@ -1,5 +1,5 @@ import { v4 as uuidv4 } from 'uuid'; -import { Tele } from 'nc-help'; +import { T } from 'nc-help'; import NcMetaMgr from './NcMetaMgr'; @@ -104,7 +104,7 @@ export default class NcMetaMgrEE extends NcMetaMgr { ip: req.clientIp, }); - Tele.emit('evt', { evt_type: 'acl:updated' }); + T.emit('evt', { evt_type: 'acl:updated' }); return res; } catch (e) { @@ -277,7 +277,7 @@ export default class NcMetaMgrEE extends NcMetaMgr { sharedView.url = `${req.ncSiteUrl}${this.config.dashboardPath}#/nc/view/${sharedView.view_id}`; } - Tele.emit('evt', { evt_type: 'sharedView:generated-link' }); + T.emit('evt', { evt_type: 'sharedView:generated-link' }); return sharedView; } catch (e) { console.log(e); @@ -295,7 +295,7 @@ export default class NcMetaMgrEE extends NcMetaMgr { }, args.args.id ); - Tele.emit('evt', { evt_type: 'sharedView:password-updated' }); + T.emit('evt', { evt_type: 'sharedView:password-updated' }); return { msg: 'Success' }; } catch (e) { console.log(e); diff --git a/packages/nocodb/src/lib/meta/api/apiTokenApis.ts b/packages/nocodb/src/lib/meta/api/apiTokenApis.ts deleted file mode 100644 index 5ed4c08094..0000000000 --- a/packages/nocodb/src/lib/meta/api/apiTokenApis.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Request, Response, Router } from 'express'; -import { OrgUserRoles } from 'nocodb-sdk'; -import { Tele } from 'nc-help'; -import { NcError } from '../helpers/catchError'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import ApiToken from '../../models/ApiToken'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import { getAjvValidatorMw } from './helpers'; - -export async function apiTokenList(req: Request, res: Response) { - res.json(await ApiToken.list(req['user'].id)); -} -export async function apiTokenCreate(req: Request, res: Response) { - Tele.emit('evt', { evt_type: 'apiToken:created' }); - res.json(await ApiToken.insert({ ...req.body, fk_user_id: req['user'].id })); -} -export async function apiTokenDelete(req: Request, res: Response) { - const apiToken = await ApiToken.getByToken(req.params.token); - if ( - !req['user'].roles.includes(OrgUserRoles.SUPER_ADMIN) && - apiToken.fk_user_id !== req['user'].id - ) { - NcError.notFound('Token not found'); - } - Tele.emit('evt', { evt_type: 'apiToken:deleted' }); - - // todo: verify token belongs to the user - res.json(await ApiToken.delete(req.params.token)); -} - -// todo: add reset token api to regenerate token - -// deprecated apis -const router = Router({ mergeParams: true }); - -router.get( - '/api/v1/db/meta/projects/:projectId/api-tokens', - metaApiMetrics, - ncMetaAclMw(apiTokenList, 'apiTokenList') -); -router.post( - '/api/v1/db/meta/projects/:projectId/api-tokens', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/ApiTokenReq'), - ncMetaAclMw(apiTokenCreate, 'apiTokenCreate') -); -router.delete( - '/api/v1/db/meta/projects/:projectId/api-tokens/:token', - metaApiMetrics, - ncMetaAclMw(apiTokenDelete, 'apiTokenDelete') -); - -export default router; diff --git a/packages/nocodb/src/lib/meta/api/attachmentApis.ts b/packages/nocodb/src/lib/meta/api/attachmentApis.ts deleted file mode 100644 index bfef1cd85f..0000000000 --- a/packages/nocodb/src/lib/meta/api/attachmentApis.ts +++ /dev/null @@ -1,225 +0,0 @@ -// @ts-ignore -import { Request, Response, Router } from 'express'; -import multer from 'multer'; -import { nanoid } from 'nanoid'; -import { OrgUserRoles, ProjectRoles } from 'nocodb-sdk'; -import path from 'path'; -import slash from 'slash'; -import Noco from '../../Noco'; -import { MetaTable } from '../../utils/globals'; -import mimetypes, { mimeIcons } from '../../utils/mimeTypes'; -import { Tele } from 'nc-help'; -import extractProjectIdAndAuthenticate from '../helpers/extractProjectIdAndAuthenticate'; -import catchError, { NcError } from '../helpers/catchError'; -import NcPluginMgrv2 from '../helpers/NcPluginMgrv2'; -import Local from '../../v1-legacy/plugins/adapters/storage/Local'; -import { NC_ATTACHMENT_FIELD_SIZE } from '../../constants'; -import { getCacheMiddleware, getAjvValidatorMw } from './helpers'; - -const isUploadAllowed = async (req: Request, _res: Response, next: any) => { - if (!req['user']?.id) { - if (!req['user']?.isPublicBase) { - NcError.unauthorized('Unauthorized'); - } - } - - try { - // check user is super admin or creator - if ( - req['user'].roles?.includes(OrgUserRoles.SUPER_ADMIN) || - req['user'].roles?.includes(OrgUserRoles.CREATOR) || - req['user'].roles?.includes(ProjectRoles.EDITOR) || - // if viewer then check at-least one project have editor or higher role - // todo: cache - !!(await Noco.ncMeta - .knex(MetaTable.PROJECT_USERS) - .where(function () { - this.where('roles', ProjectRoles.OWNER); - this.orWhere('roles', ProjectRoles.CREATOR); - this.orWhere('roles', ProjectRoles.EDITOR); - }) - .andWhere('fk_user_id', req['user'].id) - .first()) - ) - return next(); - } catch {} - NcError.badRequest('Upload not allowed'); -}; - -export async function upload(req: Request, res: Response) { - const filePath = sanitizeUrlPath( - req.query?.path?.toString()?.split('/') || [''] - ); - const destPath = path.join('nc', 'uploads', ...filePath); - - const storageAdapter = await NcPluginMgrv2.storageAdapter(); - - const attachments = await Promise.all( - (req as any).files?.map(async (file) => { - const fileName = `${nanoid(18)}${path.extname(file.originalname)}`; - - const url = await storageAdapter.fileCreate( - slash(path.join(destPath, fileName)), - file - ); - - let attachmentPath; - - // if `url` is null, then it is local attachment - if (!url) { - // then store the attachement path only - // url will be constructued in `useAttachmentCell` - attachmentPath = `download/${filePath.join('/')}/${fileName}`; - } - - return { - ...(url ? { url } : {}), - ...(attachmentPath ? { path: attachmentPath } : {}), - title: file.originalname, - mimetype: file.mimetype, - size: file.size, - icon: mimeIcons[path.extname(file.originalname).slice(1)] || undefined, - }; - }) - ); - - Tele.emit('evt', { evt_type: 'image:uploaded' }); - - res.json(attachments); -} - -export async function uploadViaURL(req: Request, res: Response) { - const filePath = sanitizeUrlPath( - req.query?.path?.toString()?.split('/') || [''] - ); - const destPath = path.join('nc', 'uploads', ...filePath); - - const storageAdapter = await NcPluginMgrv2.storageAdapter(); - - const attachments = await Promise.all( - req.body?.map?.(async (urlMeta) => { - const { url, fileName: _fileName } = urlMeta; - - const fileName = `${nanoid(18)}${_fileName || url.split('/').pop()}`; - - const attachmentUrl = await (storageAdapter as any).fileCreateByUrl( - slash(path.join(destPath, fileName)), - url - ); - - let attachmentPath; - - // if `attachmentUrl` is null, then it is local attachment - if (!attachmentUrl) { - // then store the attachement path only - // url will be constructued in `useAttachmentCell` - attachmentPath = `download/${filePath.join('/')}/${fileName}`; - } - - return { - ...(attachmentUrl ? { url: attachmentUrl } : {}), - ...(attachmentPath ? { path: attachmentPath } : {}), - title: fileName, - mimetype: urlMeta.mimetype, - size: urlMeta.size, - icon: mimeIcons[path.extname(fileName).slice(1)] || undefined, - }; - }) - ); - - Tele.emit('evt', { evt_type: 'image:uploaded' }); - - res.json(attachments); -} - -export async function fileRead(req, res) { - try { - // get the local storage adapter to display local attachments - const storageAdapter = new Local(); - const type = - mimetypes[path.extname(req.params?.[0]).split('/').pop().slice(1)] || - 'text/plain'; - - const img = await storageAdapter.fileRead( - slash( - path.join( - 'nc', - 'uploads', - req.params?.[0] - ?.split('/') - .filter((p) => p !== '..') - .join('/') - ) - ) - ); - res.writeHead(200, { 'Content-Type': type }); - res.end(img, 'binary'); - } catch (e) { - console.log(e); - res.status(404).send('Not found'); - } -} - -const router = Router({ mergeParams: true }); - -router.get( - /^\/dl\/([^/]+)\/([^/]+)\/(.+)$/, - getCacheMiddleware(), - async (req, res) => { - try { - // const type = mimetypes[path.extname(req.params.fileName).slice(1)] || 'text/plain'; - const type = - mimetypes[path.extname(req.params[2]).split('/').pop().slice(1)] || - 'text/plain'; - - const storageAdapter = await NcPluginMgrv2.storageAdapter(); - // const img = await this.storageAdapter.fileRead(slash(path.join('nc', req.params.projectId, req.params.dbAlias, 'uploads', req.params.fileName))); - const img = await storageAdapter.fileRead( - slash( - path.join( - 'nc', - req.params[0], - req.params[1], - 'uploads', - ...req.params[2].split('/') - ) - ) - ); - res.writeHead(200, { 'Content-Type': type }); - res.end(img, 'binary'); - } catch (e) { - res.status(404).send('Not found'); - } - } -); - -export function sanitizeUrlPath(paths) { - return paths.map((url) => url.replace(/[/.?#]+/g, '_')); -} - -router.post( - '/api/v1/db/storage/upload', - multer({ - storage: multer.diskStorage({}), - limits: { - fieldSize: NC_ATTACHMENT_FIELD_SIZE, - }, - }).any(), - [ - getAjvValidatorMw('swagger.json#/components/schemas/AttachmentReq'), - extractProjectIdAndAuthenticate, - catchError(isUploadAllowed), - catchError(upload), - ] -); - -router.post('/api/v1/db/storage/upload-by-url', [ - getAjvValidatorMw('swagger.json#/components/schemas/AttachmentReq'), - extractProjectIdAndAuthenticate, - catchError(isUploadAllowed), - catchError(uploadViaURL), -]); - -router.get(/^\/download\/(.+)$/, getCacheMiddleware(), catchError(fileRead)); - -export default router; diff --git a/packages/nocodb/src/lib/meta/api/baseApis.ts b/packages/nocodb/src/lib/meta/api/baseApis.ts deleted file mode 100644 index c13bbd18d3..0000000000 --- a/packages/nocodb/src/lib/meta/api/baseApis.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { Request, Response } from 'express'; -import Project from '../../models/Project'; -import { BaseListType } from 'nocodb-sdk'; -import { PagedResponseImpl } from '../helpers/PagedResponse'; -import { syncBaseMigration } from '../helpers/syncMigration'; -import Base from '../../models/Base'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { Tele } from 'nc-help'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import { getAjvValidatorMw, populateMeta } from './helpers'; - -export async function baseGet( - req: Request, - res: Response -) { - const base = await Base.get(req.params.baseId); - - base.config = base.getConnectionConfig(); - - res.json(base); -} - -export async function baseUpdate( - req: Request, - res: Response -) { - const baseBody = req.body; - const project = await Project.getWithInfo(req.params.projectId); - const base = await Base.updateBase(req.params.baseId, { - ...baseBody, - type: baseBody.config?.client, - projectId: project.id, - id: req.params.baseId, - }); - - delete base.config; - - Tele.emit('evt', { - evt_type: 'base:updated', - }); - - res.json(base); -} - -export async function baseList( - req: Request, - res: Response, - next -) { - try { - const bases = await Base.list({ projectId: req.params.projectId }); - - res // todo: pagination - .json({ - bases: new PagedResponseImpl(bases, { - count: bases.length, - limit: bases.length, - }), - }); - } catch (e) { - console.log(e); - next(e); - } -} - -export async function baseDelete( - req: Request, - res: Response -) { - const base = await Base.get(req.params.baseId); - const result = await base.delete(); - Tele.emit('evt', { evt_type: 'base:deleted' }); - res.json(result); -} - -async function baseCreate(req: Request, res) { - // type | base | projectId - const baseBody = req.body; - const project = await Project.getWithInfo(req.params.projectId); - const base = await Base.createBase({ - ...baseBody, - type: baseBody.config?.client, - projectId: project.id, - }); - - await syncBaseMigration(project, base); - - const info = await populateMeta(base, project); - - Tele.emit('evt_api_created', info); - - delete base.config; - - Tele.emit('evt', { - evt_type: 'base:created', - }); - - res.json(base); -} - -export default (router) => { - router.get( - '/api/v1/db/meta/projects/:projectId/bases/:baseId', - metaApiMetrics, - ncMetaAclMw(baseGet, 'baseGet') - ); - router.patch( - '/api/v1/db/meta/projects/:projectId/bases/:baseId', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/BaseReq'), - ncMetaAclMw(baseUpdate, 'baseUpdate') - ); - router.delete( - '/api/v1/db/meta/projects/:projectId/bases/:baseId', - metaApiMetrics, - ncMetaAclMw(baseDelete, 'baseDelete') - ); - router.post( - '/api/v1/db/meta/projects/:projectId/bases', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/BaseReq'), - ncMetaAclMw(baseCreate, 'baseCreate') - ); - router.get( - '/api/v1/db/meta/projects/:projectId/bases', - metaApiMetrics, - ncMetaAclMw(baseList, 'baseList') - ); -}; diff --git a/packages/nocodb/src/lib/meta/api/dataApis/bulkDataAliasApis.ts b/packages/nocodb/src/lib/meta/api/dataApis/bulkDataAliasApis.ts deleted file mode 100644 index 739585a1c5..0000000000 --- a/packages/nocodb/src/lib/meta/api/dataApis/bulkDataAliasApis.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { Request, Response, Router } from 'express'; -import { BaseModelSqlv2 } from '../../../db/sql-data-mapper/lib/sql/BaseModelSqlv2'; -import Model from '../../../models/Model'; -import Base from '../../../models/Base'; -import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; -import ncMetaAclMw from '../../helpers/ncMetaAclMw'; -import { getViewAndModelFromRequestByAliasOrId } from './helpers'; -import apiMetrics from '../../helpers/apiMetrics'; - -type BulkOperation = - | 'bulkInsert' - | 'bulkUpdate' - | 'bulkUpdateAll' - | 'bulkDelete' - | 'bulkDeleteAll'; - -async function getModelViewBase(req: Request) { - const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); - - const base = await Base.get(model.base_id); - return { model, view, base }; -} - -async function executeBulkOperation( - req: Request, - res: Response, - operation: T, - options: Parameters -) { - const { model, view, base } = await getModelViewBase(req); - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - res.json(await baseModel[operation].apply(null, options)); -} - -async function bulkDataInsert(req: Request, res: Response) { - await executeBulkOperation(req, res, 'bulkInsert', [ - req.body, - { cookie: req }, - ]); -} - -async function bulkDataUpdate(req: Request, res: Response) { - await executeBulkOperation(req, res, 'bulkUpdate', [ - req.body, - { cookie: req }, - ]); -} - -// todo: Integrate with filterArrJson bulkDataUpdateAll -async function bulkDataUpdateAll(req: Request, res: Response) { - await executeBulkOperation(req, res, 'bulkUpdateAll', [ - req.query, - req.body, - { cookie: req }, - ]); -} - -async function bulkDataDelete(req: Request, res: Response) { - await executeBulkOperation(req, res, 'bulkDelete', [ - req.body, - { cookie: req }, - ]); -} - -// todo: Integrate with filterArrJson bulkDataDeleteAll -async function bulkDataDeleteAll(req: Request, res: Response) { - await executeBulkOperation(req, res, 'bulkDeleteAll', [req.query]); -} -const router = Router({ mergeParams: true }); - -router.post( - '/api/v1/db/data/bulk/:orgs/:projectName/:tableName', - apiMetrics, - ncMetaAclMw(bulkDataInsert, 'bulkDataInsert') -); -router.patch( - '/api/v1/db/data/bulk/:orgs/:projectName/:tableName', - apiMetrics, - ncMetaAclMw(bulkDataUpdate, 'bulkDataUpdate') -); -router.patch( - '/api/v1/db/data/bulk/:orgs/:projectName/:tableName/all', - apiMetrics, - ncMetaAclMw(bulkDataUpdateAll, 'bulkDataUpdateAll') -); -router.delete( - '/api/v1/db/data/bulk/:orgs/:projectName/:tableName', - apiMetrics, - ncMetaAclMw(bulkDataDelete, 'bulkDataDelete') -); -router.delete( - '/api/v1/db/data/bulk/:orgs/:projectName/:tableName/all', - apiMetrics, - ncMetaAclMw(bulkDataDeleteAll, 'bulkDataDeleteAll') -); - -export default router; diff --git a/packages/nocodb/src/lib/meta/api/dataApis/dataAliasApis.ts b/packages/nocodb/src/lib/meta/api/dataApis/dataAliasApis.ts deleted file mode 100644 index 4ef4b7ce5e..0000000000 --- a/packages/nocodb/src/lib/meta/api/dataApis/dataAliasApis.ts +++ /dev/null @@ -1,432 +0,0 @@ -import { Request, Response, Router } from 'express'; -import Model from '../../../models/Model'; -import { nocoExecute } from 'nc-help'; -import Base from '../../../models/Base'; -import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; -import { NcError } from '../../helpers/catchError'; -import { PagedResponseImpl } from '../../helpers/PagedResponse'; -import View from '../../../models/View'; -import ncMetaAclMw from '../../helpers/ncMetaAclMw'; -import { getViewAndModelFromRequestByAliasOrId } from './helpers'; -import apiMetrics from '../../helpers/apiMetrics'; -import getAst from '../../../db/sql-data-mapper/lib/sql/helpers/getAst'; -import { parseHrtimeToSeconds } from '../helpers'; - -// todo: Handle the error case where view doesnt belong to model -async function dataList(req: Request, res: Response) { - const startTime = process.hrtime(); - const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); - const responseData = await getDataList(model, view, req); - const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime)); - res.setHeader('xc-db-response', elapsedSeconds); - res.json(responseData); -} - -async function dataFindOne(req: Request, res: Response) { - const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); - res.json(await getFindOne(model, view, req)); -} - -async function dataGroupBy(req: Request, res: Response) { - const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); - res.json(await getDataGroupBy(model, view, req)); -} - -async function dataCount(req: Request, res: Response) { - const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const countArgs: any = { ...req.query }; - try { - countArgs.filterArr = JSON.parse(countArgs.filterArrJson); - } catch (e) {} - - const count = await baseModel.count(countArgs); - - res.json({ count }); -} - -// todo: Handle the error case where view doesnt belong to model -async function dataInsert(req: Request, res: Response) { - const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - res.json(await baseModel.insert(req.body, null, req)); -} - -async function dataUpdate(req: Request, res: Response) { - const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - res.json(await baseModel.updateByPk(req.params.rowId, req.body, null, req)); -} - -async function dataDelete(req: Request, res: Response) { - const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); - const base = await Base.get(model.base_id); - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - // todo: Should have error http status code - const message = await baseModel.hasLTARData(req.params.rowId, model); - if (message.length) { - res.json({ message }); - return; - } - res.json(await baseModel.delByPk(req.params.rowId, null, req)); -} - -async function getDataList(model, view: View, req) { - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const requestObj = await getAst({ model, query: req.query, view }); - - const listArgs: any = { ...req.query }; - try { - listArgs.filterArr = JSON.parse(listArgs.filterArrJson); - } catch (e) {} - try { - listArgs.sortArr = JSON.parse(listArgs.sortArrJson); - } catch (e) {} - - let data = []; - let count = 0; - try { - data = await nocoExecute( - requestObj, - await baseModel.list(listArgs), - {}, - listArgs - ); - count = await baseModel.count(listArgs); - } catch (e) { - console.log(e); - NcError.internalServerError( - 'Internal Server Error, check server log for more details' - ); - } - - return new PagedResponseImpl(data, { - ...req.query, - count, - }); -} - -async function getFindOne(model, view: View, req) { - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const args: any = { ...req.query }; - try { - args.filterArr = JSON.parse(args.filterArrJson); - } catch (e) {} - try { - args.sortArr = JSON.parse(args.sortArrJson); - } catch (e) {} - - const data = await baseModel.findOne(args); - return data - ? await nocoExecute( - await getAst({ model, query: args, view }), - data, - {}, - {} - ) - : {}; -} - -async function getDataGroupBy(model, view: View, req) { - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const listArgs: any = { ...req.query }; - const data = await baseModel.groupBy({ ...req.query }); - const count = await baseModel.count(listArgs); - - return new PagedResponseImpl(data, { - ...req.query, - count, - }); -} - -async function dataRead(req: Request, res: Response) { - const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const row = await baseModel.readByPk(req.params.rowId); - - if (!row) { - NcError.notFound(); - } - - res.json( - await nocoExecute( - await getAst({ model, query: req.query, view }), - row, - {}, - req.query - ) - ); -} - -async function dataExist(req: Request, res: Response) { - const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - res.json(await baseModel.exist(req.params.rowId)); -} - -// todo: Handle the error case where view doesnt belong to model -async function groupedDataList(req: Request, res: Response) { - const startTime = process.hrtime(); - const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); - const groupedData = await getGroupedDataList(model, view, req); - const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime)); - res.setHeader('xc-db-response', elapsedSeconds); - res.json(groupedData); -} - -async function getGroupedDataList(model, view: View, req) { - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const requestObj = await getAst({ model, query: req.query, view }); - - const listArgs: any = { ...req.query }; - try { - listArgs.filterArr = JSON.parse(listArgs.filterArrJson); - } catch (e) {} - try { - listArgs.sortArr = JSON.parse(listArgs.sortArrJson); - } catch (e) {} - try { - listArgs.options = JSON.parse(listArgs.optionsArrJson); - } catch (e) {} - - let data = []; - // let count = 0 - try { - const groupedData = await baseModel.groupedList({ - ...listArgs, - groupColumnId: req.params.columnId, - }); - data = await nocoExecute( - { key: 1, value: requestObj }, - groupedData, - {}, - listArgs - ); - const countArr = await baseModel.groupedListCount({ - ...listArgs, - groupColumnId: req.params.columnId, - }); - data = data.map((item) => { - // todo: use map to avoid loop - const count = - countArr.find((countItem: any) => countItem.key === item.key)?.count ?? - 0; - - item.value = new PagedResponseImpl(item.value, { - ...req.query, - count: count, - }); - return item; - }); - } catch (e) { - console.log(e); - NcError.internalServerError( - 'Internal Server Error, check server log for more details' - ); - } - return data; -} - -const router = Router({ mergeParams: true }); - -// table data crud apis -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName', - apiMetrics, - ncMetaAclMw(dataList, 'dataList') -); - -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName/find-one', - apiMetrics, - ncMetaAclMw(dataFindOne, 'dataFindOne') -); - -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName/groupby', - apiMetrics, - ncMetaAclMw(dataGroupBy, 'dataGroupBy') -); - -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName/group/:columnId', - apiMetrics, - ncMetaAclMw(groupedDataList, 'groupedDataList') -); - -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/exist', - apiMetrics, - ncMetaAclMw(dataExist, 'dataExist') -); - -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName/count', - apiMetrics, - ncMetaAclMw(dataCount, 'dataCount') -); - -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/count', - apiMetrics, - ncMetaAclMw(dataCount, 'dataCount') -); - -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId', - apiMetrics, - ncMetaAclMw(dataRead, 'dataRead') -); - -router.patch( - '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId', - apiMetrics, - ncMetaAclMw(dataUpdate, 'dataUpdate') -); - -router.delete( - '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId', - apiMetrics, - ncMetaAclMw(dataDelete, 'dataDelete') -); - -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName', - apiMetrics, - ncMetaAclMw(dataList, 'dataList') -); - -// table view data crud apis -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName', - apiMetrics, - ncMetaAclMw(dataList, 'dataList') -); - -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/find-one', - apiMetrics, - ncMetaAclMw(dataFindOne, 'dataFindOne') -); - -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/groupby', - apiMetrics, - ncMetaAclMw(dataGroupBy, 'dataGroupBy') -); - -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/group/:columnId', - apiMetrics, - ncMetaAclMw(groupedDataList, 'groupedDataList') -); - -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/:rowId/exist', - apiMetrics, - ncMetaAclMw(dataExist, 'dataExist') -); - -router.post( - '/api/v1/db/data/:orgs/:projectName/:tableName', - apiMetrics, - ncMetaAclMw(dataInsert, 'dataInsert') -); - -router.post( - '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName', - apiMetrics, - ncMetaAclMw(dataInsert, 'dataInsert') -); - -router.patch( - '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/:rowId', - apiMetrics, - ncMetaAclMw(dataUpdate, 'dataUpdate') -); - -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/:rowId', - apiMetrics, - ncMetaAclMw(dataRead, 'dataRead') -); - -router.delete( - '/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/:rowId', - apiMetrics, - ncMetaAclMw(dataDelete, 'dataDelete') -); - -export default router; diff --git a/packages/nocodb/src/lib/meta/api/dataApis/dataAliasNestedApis.ts b/packages/nocodb/src/lib/meta/api/dataApis/dataAliasNestedApis.ts deleted file mode 100644 index d260694fe5..0000000000 --- a/packages/nocodb/src/lib/meta/api/dataApis/dataAliasNestedApis.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { Request, Response, Router } from 'express'; -import Model from '../../../models/Model'; -import Base from '../../../models/Base'; -import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; -import { PagedResponseImpl } from '../../helpers/PagedResponse'; -import ncMetaAclMw from '../../helpers/ncMetaAclMw'; -import { - getColumnByIdOrName, - getViewAndModelFromRequestByAliasOrId, -} from './helpers'; -import { NcError } from '../../helpers/catchError'; -import apiMetrics from '../../helpers/apiMetrics'; - -// todo: handle case where the given column is not ltar -export async function mmList(req: Request, res: Response, next) { - const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); - - if (!model) return next(new Error('Table not found')); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const column = await getColumnByIdOrName(req.params.columnName, model); - - const data = await baseModel.mmList( - { - colId: column.id, - parentId: req.params.rowId, - }, - req.query as any - ); - const count: any = await baseModel.mmListCount({ - colId: column.id, - parentId: req.params.rowId, - }); - - res.json( - new PagedResponseImpl(data, { - count, - ...req.query, - }) - ); -} - -export async function mmExcludedList(req: Request, res: Response, next) { - const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); - if (!model) return next(new Error('Table not found')); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - const column = await getColumnByIdOrName(req.params.columnName, model); - - const data = await baseModel.getMmChildrenExcludedList( - { - colId: column.id, - pid: req.params.rowId, - }, - req.query - ); - - const count = await baseModel.getMmChildrenExcludedListCount( - { - colId: column.id, - pid: req.params.rowId, - }, - req.query - ); - - res.json( - new PagedResponseImpl(data, { - count, - ...req.query, - }) - ); -} - -export async function hmExcludedList(req: Request, res: Response, next) { - const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); - - if (!model) return next(new Error('Table not found')); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const column = await getColumnByIdOrName(req.params.columnName, model); - - const data = await baseModel.getHmChildrenExcludedList( - { - colId: column.id, - pid: req.params.rowId, - }, - req.query - ); - - const count = await baseModel.getHmChildrenExcludedListCount( - { - colId: column.id, - pid: req.params.rowId, - }, - req.query - ); - - res.json( - new PagedResponseImpl(data, { - count, - ...req.query, - }) - ); -} - -export async function btExcludedList(req: Request, res: Response, next) { - const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); - if (!model) return next(new Error('Table not found')); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const column = await getColumnByIdOrName(req.params.columnName, model); - - const data = await baseModel.getBtChildrenExcludedList( - { - colId: column.id, - cid: req.params.rowId, - }, - req.query - ); - - const count = await baseModel.getBtChildrenExcludedListCount( - { - colId: column.id, - cid: req.params.rowId, - }, - req.query - ); - - res.json( - new PagedResponseImpl(data, { - count, - ...req.query, - }) - ); -} - -// todo: handle case where the given column is not ltar -export async function hmList(req: Request, res: Response, next) { - const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); - if (!model) return next(new Error('Table not found')); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const column = await getColumnByIdOrName(req.params.columnName, model); - - const data = await baseModel.hmList( - { - colId: column.id, - id: req.params.rowId, - }, - req.query - ); - - const count = await baseModel.hmListCount({ - colId: column.id, - id: req.params.rowId, - }); - - res.json( - new PagedResponseImpl(data, { - count, - ...req.query, - } as any) - ); -} - -//@ts-ignore -async function relationDataRemove(req, res) { - const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); - - if (!model) NcError.notFound('Table not found'); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const column = await getColumnByIdOrName(req.params.columnName, model); - - await baseModel.removeChild({ - colId: column.id, - childId: req.params.refRowId, - rowId: req.params.rowId, - cookie: req, - }); - - res.json({ msg: 'success' }); -} - -//@ts-ignore -// todo: Give proper error message when reference row is already related and handle duplicate ref row id in hm -async function relationDataAdd(req, res) { - const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); - if (!model) NcError.notFound('Table not found'); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const column = await getColumnByIdOrName(req.params.columnName, model); - await baseModel.addChild({ - colId: column.id, - childId: req.params.refRowId, - rowId: req.params.rowId, - cookie: req, - }); - - res.json({ msg: 'success' }); -} - -const router = Router({ mergeParams: true }); - -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/mm/:columnName/exclude', - apiMetrics, - ncMetaAclMw(mmExcludedList, 'mmExcludedList') -); -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/hm/:columnName/exclude', - apiMetrics, - ncMetaAclMw(hmExcludedList, 'hmExcludedList') -); -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/bt/:columnName/exclude', - apiMetrics, - ncMetaAclMw(btExcludedList, 'btExcludedList') -); - -router.post( - '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/:relationType/:columnName/:refRowId', - apiMetrics, - ncMetaAclMw(relationDataAdd, 'relationDataAdd') -); -router.delete( - '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/:relationType/:columnName/:refRowId', - apiMetrics, - ncMetaAclMw(relationDataRemove, 'relationDataRemove') -); - -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/mm/:columnName', - apiMetrics, - ncMetaAclMw(mmList, 'mmList') -); -router.get( - '/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/hm/:columnName', - apiMetrics, - ncMetaAclMw(hmList, 'hmList') -); - -export default router; diff --git a/packages/nocodb/src/lib/meta/api/dataApis/dataApis.ts b/packages/nocodb/src/lib/meta/api/dataApis/dataApis.ts deleted file mode 100644 index b8a8899b5f..0000000000 --- a/packages/nocodb/src/lib/meta/api/dataApis/dataApis.ts +++ /dev/null @@ -1,612 +0,0 @@ -import { Request, Response, Router } from 'express'; -import Model from '../../../models/Model'; -import { nocoExecute } from 'nc-help'; -import Base from '../../../models/Base'; -import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; -import { PagedResponseImpl } from '../../helpers/PagedResponse'; -import View from '../../../models/View'; -import ncMetaAclMw from '../../helpers/ncMetaAclMw'; -import { NcError } from '../../helpers/catchError'; -import apiMetrics from '../../helpers/apiMetrics'; -import getAst from '../../../db/sql-data-mapper/lib/sql/helpers/getAst'; - -export async function dataList(req: Request, res: Response, next) { - const view = await View.get(req.params.viewId); - - const model = await Model.getByIdOrName({ - id: view?.fk_model_id || req.params.viewId, - }); - - if (!model) return next(new Error('Table not found')); - - res.json(await getDataList(model, view, req)); -} - -export async function mmList(req: Request, res: Response, next) { - const view = await View.get(req.params.viewId); - - const model = await Model.getByIdOrName({ - id: view?.fk_model_id || req.params.viewId, - }); - - if (!model) return next(new Error('Table not found')); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const key = `${model.title}List`; - const requestObj: any = { - [key]: 1, - }; - - const data = ( - await nocoExecute( - requestObj, - { - [key]: async (args) => { - return await baseModel.mmList( - { - colId: req.params.colId, - parentId: req.params.rowId, - }, - args - ); - }, - }, - {}, - - { nested: { [key]: req.query } } - ) - )?.[key]; - - const count: any = await baseModel.mmListCount({ - colId: req.params.colId, - parentId: req.params.rowId, - }); - - res.json( - new PagedResponseImpl(data, { - count, - ...req.query, - }) - ); -} - -export async function mmExcludedList(req: Request, res: Response, next) { - const view = await View.get(req.params.viewId); - - const model = await Model.getByIdOrName({ - id: view?.fk_model_id || req.params.viewId, - }); - - if (!model) return next(new Error('Table not found')); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const key = 'List'; - const requestObj: any = { - [key]: 1, - }; - - const data = ( - await nocoExecute( - requestObj, - { - [key]: async (args) => { - return await baseModel.getMmChildrenExcludedList( - { - colId: req.params.colId, - pid: req.params.rowId, - }, - args - ); - }, - }, - {}, - - { nested: { [key]: req.query } } - ) - )?.[key]; - - const count = await baseModel.getMmChildrenExcludedListCount( - { - colId: req.params.colId, - pid: req.params.rowId, - }, - req.query - ); - - res.json( - new PagedResponseImpl(data, { - count, - ...req.query, - }) - ); -} - -export async function hmExcludedList(req: Request, res: Response, next) { - const view = await View.get(req.params.viewId); - - const model = await Model.getByIdOrName({ - id: view?.fk_model_id || req.params.viewId, - }); - - if (!model) return next(new Error('Table not found')); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const key = 'List'; - const requestObj: any = { - [key]: 1, - }; - - const data = ( - await nocoExecute( - requestObj, - { - [key]: async (args) => { - return await baseModel.getHmChildrenExcludedList( - { - colId: req.params.colId, - pid: req.params.rowId, - }, - args - ); - }, - }, - {}, - - { nested: { [key]: req.query } } - ) - )?.[key]; - - const count = await baseModel.getHmChildrenExcludedListCount( - { - colId: req.params.colId, - pid: req.params.rowId, - }, - req.query - ); - - res.json( - new PagedResponseImpl(data, { - count, - ...req.query, - }) - ); -} - -export async function btExcludedList(req: Request, res: Response, next) { - const view = await View.get(req.params.viewId); - - const model = await Model.getByIdOrName({ - id: view?.fk_model_id || req.params.viewId, - }); - - if (!model) return next(new Error('Table not found')); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const key = 'List'; - const requestObj: any = { - [key]: 1, - }; - - const data = ( - await nocoExecute( - requestObj, - { - [key]: async (args) => { - return await baseModel.getBtChildrenExcludedList( - { - colId: req.params.colId, - cid: req.params.rowId, - }, - args - ); - }, - }, - {}, - - { nested: { [key]: req.query } } - ) - )?.[key]; - - const count = await baseModel.getBtChildrenExcludedListCount( - { - colId: req.params.colId, - cid: req.params.rowId, - }, - req.query - ); - - res.json( - new PagedResponseImpl(data, { - count, - ...req.query, - }) - ); -} - -export async function hmList(req: Request, res: Response, next) { - const view = await View.get(req.params.viewId); - - const model = await Model.getByIdOrName({ - id: view?.fk_model_id || req.params.viewId, - }); - - if (!model) return next(new Error('Table not found')); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const key = `${model.title}List`; - const requestObj: any = { - [key]: 1, - }; - - const data = ( - await nocoExecute( - requestObj, - { - [key]: async (args) => { - return await baseModel.hmList( - { - colId: req.params.colId, - id: req.params.rowId, - }, - args - ); - }, - }, - {}, - { nested: { [key]: req.query } } - ) - )?.[key]; - - const count = await baseModel.hmListCount({ - colId: req.params.colId, - id: req.params.rowId, - }); - - res.json( - new PagedResponseImpl(data, { - totalRows: count, - } as any) - ); -} - -async function dataRead(req: Request, res: Response, next) { - try { - const model = await Model.getByIdOrName({ - id: req.params.viewId, - }); - if (!model) return next(new Error('Table not found')); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - res.json( - await nocoExecute( - await getAst({ model, query: req.query }), - await baseModel.readByPk(req.params.rowId), - {}, - {} - ) - ); - } catch (e) { - console.log(e); - NcError.internalServerError( - 'Internal Server Error, check server log for more details' - ); - } -} - -async function dataInsert(req: Request, res: Response, next) { - try { - const model = await Model.getByIdOrName({ - id: req.params.viewId, - }); - if (!model) return next(new Error('Table not found')); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - res.json(await baseModel.insert(req.body, null, req)); - } catch (e) { - console.log(e); - res.status(500).json({ msg: e.message }); - } -} - -// async function dataInsertNew(req: Request, res: Response) { -// const { model, view } = await getViewAndModelFromRequest(req); -// -// const base = await Base.get(model.base_id); -// -// const baseModel = await Model.getBaseModelSQL({ -// id: model.id, -// viewId: view?.id, -// dbDriver: NcConnectionMgrv2.get(base) -// }); -// -// res.json(await baseModel.insert(req.body, null, req)); -// } - -// async function dataUpdateNew(req: Request, res: Response) { -// const { model, view } = await getViewAndModelFromRequest(req); -// const base = await Base.get(model.base_id); -// -// const baseModel = await Model.getBaseModelSQL({ -// id: model.id, -// viewId: view.id, -// dbDriver: NcConnectionMgrv2.get(base) -// }); -// -// res.json(await baseModel.updateByPk(req.params.rowId, req.body, null, req)); -// } -async function dataUpdate(req: Request, res: Response, next) { - try { - const model = await Model.getByIdOrName({ - id: req.params.viewId, - }); - if (!model) return next(new Error('Table not found')); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - res.json(await baseModel.updateByPk(req.params.rowId, req.body, null, req)); - } catch (e) { - console.log(e); - res.status(500).json({ msg: e.message }); - } -} -// -// async function dataDeleteNew(req: Request, res: Response) { -// const { model, view } = await getViewAndModelFromRequest(req); -// const base = await Base.get(model.base_id); -// const baseModel = await Model.getBaseModelSQL({ -// id: model.id, -// viewId: view.id, -// dbDriver: NcConnectionMgrv2.get(base) -// }); -// -// res.json(await baseModel.delByPk(req.params.rowId, null, req)); -// } - -async function dataDelete(req: Request, res: Response, next) { - try { - const model = await Model.getByIdOrName({ - id: req.params.viewId, - }); - if (!model) return next(new Error('Table not found')); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - res.json(await baseModel.delByPk(req.params.rowId, null, req)); - } catch (e) { - console.log(e); - res.status(500).json({ msg: e.message }); - } -} - -async function getDataList(model, view: View, req) { - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const requestObj = await getAst({ query: req.query, model, view }); - - const listArgs: any = { ...req.query }; - try { - listArgs.filterArr = JSON.parse(listArgs.filterArrJson); - } catch (e) {} - try { - listArgs.sortArr = JSON.parse(listArgs.sortArrJson); - } catch (e) {} - - let data = []; - let count = 0; - try { - data = await nocoExecute( - requestObj, - await baseModel.list(listArgs), - {}, - listArgs - ); - count = await baseModel.count(listArgs); - } catch (e) { - // show empty result instead of throwing error here - // e.g. search some text in a numeric field - console.log(e); - NcError.internalServerError( - 'Internal Server Error, check server log for more details' - ); - } - - return new PagedResponseImpl(data, { - count, - ...req.query, - }); -} -//@ts-ignore -async function relationDataDelete(req, res) { - const view = await View.get(req.params.viewId); - - const model = await Model.getByIdOrName({ - id: view?.fk_model_id || req.params.viewId, - }); - - if (!model) NcError.notFound('Table not found'); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - await baseModel.removeChild({ - colId: req.params.colId, - childId: req.params.childId, - rowId: req.params.rowId, - cookie: req, - }); - - res.json({ msg: 'success' }); -} - -//@ts-ignore -async function relationDataAdd(req, res) { - const view = await View.get(req.params.viewId); - - const model = await Model.getByIdOrName({ - id: view?.fk_model_id || req.params.viewId, - }); - - if (!model) NcError.notFound('Table not found'); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - await baseModel.addChild({ - colId: req.params.colId, - childId: req.params.childId, - rowId: req.params.rowId, - cookie: req, - }); - - res.json({ msg: 'success' }); -} - -const router = Router({ mergeParams: true }); - -// router.get('/data/:orgs/:projectName/:tableName',apiMetrics,ncMetaAclMw(dataListNew)); -// router.get( -// '/data/:orgs/:projectName/:tableName/views/:viewName', -// ncMetaAclMw(dataListNew) -// ); -// -// router.post( -// '/data/:orgs/:projectName/:tableName/views/:viewName', -// ncMetaAclMw(dataInsertNew) -// ); -// router.patch( -// '/data/:orgs/:projectName/:tableName/views/:viewName/:rowId', -// ncMetaAclMw(dataUpdateNew) -// ); -// router.delete( -// '/data/:orgs/:projectName/:tableName/views/:viewName/:rowId', -// ncMetaAclMw(dataDeleteNew) -// ); - -router.get('/data/:viewId/', apiMetrics, ncMetaAclMw(dataList, 'dataList')); -router.post( - '/data/:viewId/', - apiMetrics, - ncMetaAclMw(dataInsert, 'dataInsert') -); -router.get( - '/data/:viewId/:rowId', - apiMetrics, - ncMetaAclMw(dataRead, 'dataRead') -); -router.patch( - '/data/:viewId/:rowId', - apiMetrics, - ncMetaAclMw(dataUpdate, 'dataUpdate') -); -router.delete( - '/data/:viewId/:rowId', - apiMetrics, - ncMetaAclMw(dataDelete, 'dataDelete') -); - -router.get( - '/data/:viewId/:rowId/mm/:colId', - apiMetrics, - ncMetaAclMw(mmList, 'mmList') -); -router.get( - '/data/:viewId/:rowId/hm/:colId', - apiMetrics, - ncMetaAclMw(hmList, 'hmList') -); - -router.get( - '/data/:viewId/:rowId/mm/:colId/exclude', - ncMetaAclMw(mmExcludedList, 'mmExcludedList') -); -router.get( - '/data/:viewId/:rowId/hm/:colId/exclude', - ncMetaAclMw(hmExcludedList, 'hmExcludedList') -); -router.get( - '/data/:viewId/:rowId/bt/:colId/exclude', - ncMetaAclMw(btExcludedList, 'btExcludedList') -); - -router.post( - '/data/:viewId/:rowId/:relationType/:colId/:childId', - ncMetaAclMw(relationDataAdd, 'relationDataAdd') -); -router.delete( - '/data/:viewId/:rowId/:relationType/:colId/:childId', - ncMetaAclMw(relationDataDelete, 'relationDataDelete') -); -export default router; diff --git a/packages/nocodb/src/lib/meta/api/dataApis/index.ts b/packages/nocodb/src/lib/meta/api/dataApis/index.ts deleted file mode 100644 index 34bb194ee0..0000000000 --- a/packages/nocodb/src/lib/meta/api/dataApis/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import dataApis from './dataApis'; -import oldDataApis from './oldDataApis'; -import dataAliasApis from './dataAliasApis'; -import bulkDataAliasApis from './bulkDataAliasApis'; -import dataAliasNestedApis from './dataAliasNestedApis'; -import dataAliasExportApis from './dataAliasExportApis'; - -export { - dataApis, - oldDataApis, - dataAliasApis, - bulkDataAliasApis, - dataAliasNestedApis, - dataAliasExportApis, -}; diff --git a/packages/nocodb/src/lib/meta/api/ee/orgTokenApis.ts b/packages/nocodb/src/lib/meta/api/ee/orgTokenApis.ts deleted file mode 100644 index 410d040c11..0000000000 --- a/packages/nocodb/src/lib/meta/api/ee/orgTokenApis.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { OrgUserRoles } from 'nocodb-sdk'; -import ApiToken from '../../../models/ApiToken'; -import { PagedResponseImpl } from '../../helpers/PagedResponse'; - -export async function apiTokenListEE(req, res) { - let fk_user_id = req.user.id; - - // if super admin get all tokens - if (req.user.roles.includes(OrgUserRoles.SUPER_ADMIN)) { - fk_user_id = undefined; - } - - res.json( - new PagedResponseImpl( - await ApiToken.listWithCreatedBy({ ...req.query, fk_user_id }), - { - ...req.query, - count: await ApiToken.count({}), - } - ) - ); -} diff --git a/packages/nocodb/src/lib/meta/api/filterApis.ts b/packages/nocodb/src/lib/meta/api/filterApis.ts deleted file mode 100644 index 1be850b7d8..0000000000 --- a/packages/nocodb/src/lib/meta/api/filterApis.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { Request, Response, Router } from 'express'; -// @ts-ignore -import Model from '../../models/Model'; -import { Tele } from 'nc-help'; -// @ts-ignore -import { PagedResponseImpl } from '../helpers/PagedResponse'; -// @ts-ignore -import { Table, TableList, TableListParams, TableReq } from 'nocodb-sdk'; -// @ts-ignore -import ProjectMgrv2 from '../../db/sql-mgr/v2/ProjectMgrv2'; -// @ts-ignore -import Project from '../../models/Project'; -import Filter from '../../models/Filter'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import { getAjvValidatorMw } from './helpers'; - -// @ts-ignore -export async function filterGet(req: Request, res: Response, next) { - try { - const filter = await Filter.get(req.params.filterId); - - res.json(filter); - } catch (e) { - console.log(e); - next(e); - } -} - -// @ts-ignore -export async function filterList( - req: Request, - res: Response, - next -) { - try { - const filter = await Filter.rootFilterList({ viewId: req.params.viewId }); - - res.json(filter); - } catch (e) { - console.log(e); - next(e); - } -} -// @ts-ignore -export async function filterChildrenRead( - req: Request, - res: Response, - next -) { - try { - const filter = await Filter.parentFilterList({ - parentId: req.params.filterParentId, - }); - - res.json(filter); - } catch (e) { - console.log(e); - next(e); - } -} - -export async function filterCreate( - req: Request, - res, - next -) { - try { - const filter = await Filter.insert({ - ...req.body, - fk_view_id: req.params.viewId, - }); - - Tele.emit('evt', { evt_type: 'filter:created' }); - res.json(filter); - } catch (e) { - console.log(e); - next(e); - } -} - -// @ts-ignore -export async function filterUpdate(req, res, next) { - try { - const filter = await Filter.update(req.params.filterId, { - ...req.body, - fk_view_id: req.params.viewId, - }); - Tele.emit('evt', { evt_type: 'filter:updated' }); - res.json(filter); - } catch (e) { - console.log(e); - next(e); - } -} - -// @ts-ignore -export async function filterDelete(req: Request, res: Response, next) { - try { - const filter = await Filter.delete(req.params.filterId); - Tele.emit('evt', { evt_type: 'filter:deleted' }); - res.json(filter); - } catch (e) { - console.log(e); - next(e); - } -} - -export async function hookFilterList( - req: Request, - res: Response -) { - const filter = await Filter.rootFilterListByHook({ - hookId: req.params.hookId, - }); - - res.json(filter); -} - -export async function hookFilterCreate(req: Request, res) { - const filter = await Filter.insert({ - ...req.body, - fk_hook_id: req.params.hookId, - }); - - Tele.emit('evt', { evt_type: 'hookFilter:created' }); - res.json(filter); -} - -const router = Router({ mergeParams: true }); -router.get( - '/api/v1/db/meta/views/:viewId/filters', - metaApiMetrics, - ncMetaAclMw(filterList, 'filterList') -); -router.post( - '/api/v1/db/meta/views/:viewId/filters', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/FilterReq'), - ncMetaAclMw(filterCreate, 'filterCreate') -); - -router.get( - '/api/v1/db/meta/hooks/:hookId/filters', - ncMetaAclMw(hookFilterList, 'filterList') -); -router.post( - '/api/v1/db/meta/hooks/:hookId/filters', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/FilterReq'), - ncMetaAclMw(hookFilterCreate, 'filterCreate') -); - -router.get( - '/api/v1/db/meta/filters/:filterId', - metaApiMetrics, - ncMetaAclMw(filterGet, 'filterGet') -); -router.patch( - '/api/v1/db/meta/filters/:filterId', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/FilterReq'), - ncMetaAclMw(filterUpdate, 'filterUpdate') -); -router.delete( - '/api/v1/db/meta/filters/:filterId', - metaApiMetrics, - ncMetaAclMw(filterDelete, 'filterDelete') -); -router.get( - '/api/v1/db/meta/filters/:filterParentId/children', - metaApiMetrics, - ncMetaAclMw(filterChildrenRead, 'filterChildrenRead') -); -export default router; diff --git a/packages/nocodb/src/lib/meta/api/formViewApis.ts b/packages/nocodb/src/lib/meta/api/formViewApis.ts deleted file mode 100644 index beadf3cded..0000000000 --- a/packages/nocodb/src/lib/meta/api/formViewApis.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Request, Response, Router } from 'express'; -// @ts-ignore -import Model from '../../models/Model'; -import { Tele } from 'nc-help'; -// @ts-ignore -import { PagedResponseImpl } from '../helpers/PagedResponse'; -import { FormType, ViewTypes } from 'nocodb-sdk'; -// @ts-ignore -import ProjectMgrv2 from '../../db/sql-mgr/v2/ProjectMgrv2'; -// @ts-ignore -import Project from '../../models/Project'; -import View from '../../models/View'; -import FormView from '../../models/FormView'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import { getAjvValidatorMw } from './helpers'; - -// @ts-ignore -export async function formViewGet(req: Request, res: Response) { - const formViewData = await FormView.getWithInfo(req.params.formViewId); - res.json(formViewData); -} - -export async function formViewCreate(req: Request, res) { - Tele.emit('evt', { evt_type: 'vtable:created', show_as: 'form' }); - const view = await View.insert({ - ...req.body, - // todo: sanitize - fk_model_id: req.params.tableId, - type: ViewTypes.FORM, - }); - res.json(view); -} -// @ts-ignore -export async function formViewUpdate(req, res) { - Tele.emit('evt', { evt_type: 'view:updated', type: 'grid' }); - res.json(await FormView.update(req.params.formViewId, req.body)); -} - -// @ts-ignore -export async function formViewDelete(req: Request, res: Response, next) {} - -const router = Router({ mergeParams: true }); -router.post( - '/api/v1/db/meta/tables/:tableId/forms', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/FormCreateReq'), - ncMetaAclMw(formViewCreate, 'formViewCreate') -); -router.get( - '/api/v1/db/meta/forms/:formViewId', - metaApiMetrics, - ncMetaAclMw(formViewGet, 'formViewGet') -); -router.patch( - '/api/v1/db/meta/forms/:formViewId', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/FormReq'), - ncMetaAclMw(formViewUpdate, 'formViewUpdate') -); -router.delete( - '/api/v1/db/meta/forms/:formViewId', - metaApiMetrics, - ncMetaAclMw(formViewDelete, 'formViewDelete') -); -export default router; diff --git a/packages/nocodb/src/lib/meta/api/formViewColumnApis.ts b/packages/nocodb/src/lib/meta/api/formViewColumnApis.ts deleted file mode 100644 index bad2f91b0a..0000000000 --- a/packages/nocodb/src/lib/meta/api/formViewColumnApis.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Request, Response, Router } from 'express'; -import FormViewColumn from '../../models/FormViewColumn'; -import { Tele } from 'nc-help'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import { getAjvValidatorMw } from './helpers'; - -export async function columnUpdate(req: Request, res: Response) { - Tele.emit('evt', { evt_type: 'formViewColumn:updated' }); - res.json(await FormViewColumn.update(req.params.formViewColumnId, req.body)); -} - -const router = Router({ mergeParams: true }); -router.patch( - '/api/v1/db/meta/form-columns/:formViewColumnId', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/FormColumnReq'), - ncMetaAclMw(columnUpdate, 'columnUpdate') -); -export default router; diff --git a/packages/nocodb/src/lib/meta/api/galleryViewApis.ts b/packages/nocodb/src/lib/meta/api/galleryViewApis.ts deleted file mode 100644 index 2c6c79db9e..0000000000 --- a/packages/nocodb/src/lib/meta/api/galleryViewApis.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Request, Response, Router } from 'express'; -import { GalleryType, ViewTypes } from 'nocodb-sdk'; -import View from '../../models/View'; -import GalleryView from '../../models/GalleryView'; -import { Tele } from 'nc-help'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import { getAjvValidatorMw } from './helpers'; -export async function galleryViewGet(req: Request, res: Response) { - res.json(await GalleryView.get(req.params.galleryViewId)); -} - -export async function galleryViewCreate(req: Request, res) { - Tele.emit('evt', { evt_type: 'vtable:created', show_as: 'gallery' }); - const view = await View.insert({ - ...req.body, - // todo: sanitize - fk_model_id: req.params.tableId, - type: ViewTypes.GALLERY, - }); - res.json(view); -} - -export async function galleryViewUpdate(req, res) { - Tele.emit('evt', { evt_type: 'view:updated', type: 'gallery' }); - res.json(await GalleryView.update(req.params.galleryViewId, req.body)); -} - -const router = Router({ mergeParams: true }); -router.post( - '/api/v1/db/meta/tables/:tableId/galleries', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/GalleryReq'), - ncMetaAclMw(galleryViewCreate, 'galleryViewCreate') -); -router.patch( - '/api/v1/db/meta/galleries/:galleryViewId', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/GalleryReq'), - ncMetaAclMw(galleryViewUpdate, 'galleryViewUpdate') -); -router.get( - '/api/v1/db/meta/galleries/:galleryViewId', - metaApiMetrics, - ncMetaAclMw(galleryViewGet, 'galleryViewGet') -); -export default router; diff --git a/packages/nocodb/src/lib/meta/api/gridViewApis.ts b/packages/nocodb/src/lib/meta/api/gridViewApis.ts deleted file mode 100644 index 96880398cb..0000000000 --- a/packages/nocodb/src/lib/meta/api/gridViewApis.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Request, Router } from 'express'; -// @ts-ignore -import Model from '../../models/Model'; -import { Tele } from 'nc-help'; -// @ts-ignore -import { PagedResponseImpl } from '../helpers/PagedResponse'; -import { ViewTypes } from 'nocodb-sdk'; -// @ts-ignore -import ProjectMgrv2 from '../../db/sql-mgr/v2/ProjectMgrv2'; -// @ts-ignore -import Project from '../../models/Project'; -import View from '../../models/View'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import GridView from '../../models/GridView'; -import { getAjvValidatorMw } from './helpers'; - -// @ts-ignore -export async function gridViewCreate(req: Request, res) { - const view = await View.insert({ - ...req.body, - // todo: sanitize - fk_model_id: req.params.tableId, - type: ViewTypes.GRID, - }); - Tele.emit('evt', { evt_type: 'vtable:created', show_as: 'grid' }); - res.json(view); -} - -export async function gridViewUpdate(req, res) { - Tele.emit('evt', { evt_type: 'view:updated', type: 'grid' }); - res.json(await GridView.update(req.params.viewId, req.body)); -} - -const router = Router({ mergeParams: true }); -router.post( - '/api/v1/db/meta/tables/:tableId/grids/', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/GridReq'), - ncMetaAclMw(gridViewCreate, 'gridViewCreate') -); -router.patch( - '/api/v1/db/meta/grids/:viewId', - metaApiMetrics, - ncMetaAclMw(gridViewUpdate, 'gridViewUpdate') -); -export default router; diff --git a/packages/nocodb/src/lib/meta/api/helpers/apiHelpers.ts b/packages/nocodb/src/lib/meta/api/helpers/apiHelpers.ts index 08512fb1f3..ec3fdfcf9b 100644 --- a/packages/nocodb/src/lib/meta/api/helpers/apiHelpers.ts +++ b/packages/nocodb/src/lib/meta/api/helpers/apiHelpers.ts @@ -3,6 +3,7 @@ import Ajv, { ErrorObject } from 'ajv'; import addFormats from 'ajv-formats'; // @ts-ignore import swagger from '../../../../schema/swagger.json'; +import { NcError } from '../../helpers/catchError'; export function parseHrtimeToSeconds(hrtime) { const seconds = (hrtime[0] + hrtime[1] / 1e6).toFixed(3); @@ -31,10 +32,29 @@ export const getAjvValidatorMw = (schema) => { // If the request body is invalid, send a response with an error message res.status(400).json({ - status: 'error', message: 'Invalid request body', errors, }); } }; }; + +// a function to validate the payload against the schema +export const validatePayload = (schema, payload) => { + // Validate the request body against the schema + const valid = ajv.validate( + typeof schema === 'string' ? { $ref: schema } : schema, + payload + ); + + // If the request body is not valid, throw error + if (!valid) { + const errors: ErrorObject[] | null | undefined = ajv.errors; + + // If the request body is invalid, throw error with error message and errors + NcError.ajvValidationError({ + message: 'Invalid request body', + errors, + }); + } +}; diff --git a/packages/nocodb/src/lib/meta/api/helpers/columnHelpers.ts b/packages/nocodb/src/lib/meta/api/helpers/columnHelpers.ts index b8a9d57b2c..6c73c3dec9 100644 --- a/packages/nocodb/src/lib/meta/api/helpers/columnHelpers.ts +++ b/packages/nocodb/src/lib/meta/api/helpers/columnHelpers.ts @@ -1,9 +1,9 @@ import { customAlphabet } from 'nanoid'; import { + BoolType, ColumnReqType, LinkToAnotherRecordType, LookupColumnReqType, - BoolType, RelationTypes, RollupColumnReqType, TableType, @@ -77,7 +77,7 @@ export async function createHmAndBtColumn( } } -export async function validateRollupPayload(payload: ColumnReqType) { +export async function validateRollupPayload(payload: ColumnReqType | Column) { validateParams( [ 'title', diff --git a/packages/nocodb/src/lib/meta/api/helpers/populateMeta.ts b/packages/nocodb/src/lib/meta/api/helpers/populateMeta.ts index ffe246a131..1f71c96bde 100644 --- a/packages/nocodb/src/lib/meta/api/helpers/populateMeta.ts +++ b/packages/nocodb/src/lib/meta/api/helpers/populateMeta.ts @@ -11,7 +11,7 @@ import getTableNameAlias, { import LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn'; import getColumnUiType from '../../helpers/getColumnUiType'; import mapDefaultDisplayValue from '../../helpers/mapDefaultDisplayValue'; -import { extractAndGenerateManyToManyRelations } from '../metaDiffApis'; +import { extractAndGenerateManyToManyRelations } from '../../../services/metaDiffService'; import { ModelTypes, UITypes, ViewTypes } from 'nocodb-sdk'; import { IGNORE_TABLES } from '../../../utils/common/BaseApiBuilder'; diff --git a/packages/nocodb/src/lib/meta/api/hookApis.ts b/packages/nocodb/src/lib/meta/api/hookApis.ts deleted file mode 100644 index 403b969aec..0000000000 --- a/packages/nocodb/src/lib/meta/api/hookApis.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Tele } from 'nc-help'; -import catchError from '../helpers/catchError'; -import { Request, Response, Router } from 'express'; -import Hook from '../../models/Hook'; -import { HookListType, HookType } from 'nocodb-sdk'; -import { PagedResponseImpl } from '../helpers/PagedResponse'; -import { invokeWebhook } from '../helpers/webhookHelpers'; -import Model from '../../models/Model'; -import populateSamplePayload from '../helpers/populateSamplePayload'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import { getAjvValidatorMw } from './helpers'; - -export async function hookList( - req: Request, - res: Response -) { - res.json( - new PagedResponseImpl(await Hook.list({ fk_model_id: req.params.tableId })) - ); -} - -export async function hookCreate( - req: Request, - res: Response -) { - Tele.emit('evt', { evt_type: 'webhooks:created' }); - const hook = await Hook.insert({ - ...req.body, - fk_model_id: req.params.tableId, - }); - res.json(hook); -} - -export async function hookDelete( - req: Request, - res: Response -) { - Tele.emit('evt', { evt_type: 'webhooks:deleted' }); - res.json(await Hook.delete(req.params.hookId)); -} - -export async function hookUpdate( - req: Request, - res: Response -) { - Tele.emit('evt', { evt_type: 'webhooks:updated' }); - - res.json(await Hook.update(req.params.hookId, req.body)); -} - -export async function hookTest(req: Request, res: Response) { - const model = await Model.getByIdOrName({ id: req.params.tableId }); - - const { - hook, - payload: { data, user }, - } = req.body; - await invokeWebhook( - new Hook(hook), - model, - data, - user, - (hook as any)?.filters, - true - ); - - Tele.emit('evt', { evt_type: 'webhooks:tested' }); - - res.json({ msg: 'Success' }); -} -export async function tableSampleData(req: Request, res: Response) { - const model = await Model.getByIdOrName({ id: req.params.tableId }); - - res // todo: pagination - .json(await populateSamplePayload(model, false, req.params.operation)); -} - -const router = Router({ mergeParams: true }); -router.get( - '/api/v1/db/meta/tables/:tableId/hooks', - metaApiMetrics, - ncMetaAclMw(hookList, 'hookList') -); -router.post( - '/api/v1/db/meta/tables/:tableId/hooks/test', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/HookTestReq'), - ncMetaAclMw(hookTest, 'hookTest') -); -router.post( - '/api/v1/db/meta/tables/:tableId/hooks', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/HookReq'), - ncMetaAclMw(hookCreate, 'hookCreate') -); -router.delete( - '/api/v1/db/meta/hooks/:hookId', - metaApiMetrics, - ncMetaAclMw(hookDelete, 'hookDelete') -); -router.patch( - '/api/v1/db/meta/hooks/:hookId', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/HookReq'), - ncMetaAclMw(hookUpdate, 'hookUpdate') -); -router.get( - '/api/v1/db/meta/tables/:tableId/hooks/samplePayload/:operation', - metaApiMetrics, - catchError(tableSampleData) -); -export default router; diff --git a/packages/nocodb/src/lib/meta/api/hookFilterApis.ts b/packages/nocodb/src/lib/meta/api/hookFilterApis.ts deleted file mode 100644 index 7b182fcab4..0000000000 --- a/packages/nocodb/src/lib/meta/api/hookFilterApis.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { Request, Response, Router } from 'express'; -// @ts-ignore -import Model from '../../models/Model'; -import { Tele } from 'nc-help'; -// @ts-ignore -import { PagedResponseImpl } from '../helpers/PagedResponse'; -// @ts-ignore -import { Table, TableList, TableListParams, TableReq } from 'nocodb-sdk'; -// @ts-ignore -import ProjectMgrv2 from '../../db/sql-mgr/v2/ProjectMgrv2'; -// @ts-ignore -import Project from '../../models/Project'; -import Filter from '../../models/Filter'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import { getAjvValidatorMw } from './helpers'; - -// @ts-ignore -export async function filterGet(req: Request, res: Response, next) { - try { - const filter = await Filter.getFilterObject({ hookId: req.params.hookId }); - - res.json(filter); - } catch (e) { - console.log(e); - next(e); - } -} - -// @ts-ignore -export async function filterList( - req: Request, - res: Response, - next -) { - try { - const filter = await Filter.rootFilterListByHook({ - hookId: req.params.hookId, - }); - - res.json(filter); - } catch (e) { - console.log(e); - next(e); - } -} -// @ts-ignore -export async function filterChildrenRead( - req: Request, - res: Response, - next -) { - try { - const filter = await Filter.parentFilterListByHook({ - hookId: req.params.hookId, - parentId: req.params.filterParentId, - }); - - res.json(filter); - } catch (e) { - console.log(e); - next(e); - } -} - -export async function filterCreate( - req: Request, - res, - next -) { - try { - const filter = await Filter.insert({ - ...req.body, - fk_hook_id: req.params.hookId, - }); - - Tele.emit('evt', { evt_type: 'hookFilter:created' }); - res.json(filter); - } catch (e) { - console.log(e); - next(e); - } -} - -// @ts-ignore -export async function filterUpdate(req, res, next) { - try { - const filter = await Filter.update(req.params.filterId, { - ...req.body, - fk_hook_id: req.params.hookId, - }); - Tele.emit('evt', { evt_type: 'hookFilter:updated' }); - res.json(filter); - } catch (e) { - console.log(e); - next(e); - } -} - -// @ts-ignore -export async function filterDelete(req: Request, res: Response, next) { - try { - const filter = await Filter.delete(req.params.filterId); - Tele.emit('evt', { evt_type: 'hookFilter:deleted' }); - res.json(filter); - } catch (e) { - console.log(e); - next(e); - } -} - -const router = Router({ mergeParams: true }); -router.get( - '/hooks/:hookId/filters/', - metaApiMetrics, - ncMetaAclMw(filterList, 'filterList') -); -router.post( - '/hooks/:hookId/filters/', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/FilterReq'), - ncMetaAclMw(filterCreate, 'filterCreate') -); -router.get( - '/hooks/:hookId/filters/:filterId', - metaApiMetrics, - ncMetaAclMw(filterGet, 'filterGet') -); -router.patch( - '/hooks/:hookId/filters/:filterId', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/FilterReq'), - ncMetaAclMw(filterUpdate, 'filterUpdate') -); -router.delete( - '/hooks/:hookId/filters/:filterId', - metaApiMetrics, - ncMetaAclMw(filterDelete, 'filterDelete') -); -router.get( - '/hooks/:hookId/filters/:filterParentId/children', - metaApiMetrics, - ncMetaAclMw(filterChildrenRead, 'filterChildrenRead') -); -export default router; diff --git a/packages/nocodb/src/lib/meta/api/index.ts b/packages/nocodb/src/lib/meta/api/index.ts index 67e6203407..a785e5ad77 100644 --- a/packages/nocodb/src/lib/meta/api/index.ts +++ b/packages/nocodb/src/lib/meta/api/index.ts @@ -1,114 +1,114 @@ -import { Tele } from 'nc-help'; -import orgLicenseApis from './orgLicenseApis'; -import orgTokenApis from './orgTokenApis'; -import orgUserApis from './orgUserApis'; -import projectApis from './projectApis'; -import baseApis from './baseApis'; -import tableApis from './tableApis'; -import columnApis from './columnApis'; +import { T } from 'nc-help'; +import orgLicenseController from '../../controllers/orgLicenseController'; +import orgTokenController from '../../controllers/orgTokenController'; +import orgUserController from '../../controllers/orgUserController'; +import projectController from '../../controllers/projectController'; +import baseController from '../../controllers/baseController'; +import tableController from '../../controllers/tableController'; +import columnController from '../../controllers/columnController'; import { Router } from 'express'; -import sortApis from './sortApis'; -import filterApis from './filterApis'; -import viewColumnApis from './viewColumnApis'; -import gridViewApis from './gridViewApis'; -import viewApis from './viewApis'; -import galleryViewApis from './galleryViewApis'; -import formViewApis from './formViewApis'; -import formViewColumnApis from './formViewColumnApis'; -import attachmentApis from './attachmentApis'; -import exportApis from './exportApis'; -import auditApis from './auditApis'; -import hookApis from './hookApis'; -import pluginApis from './pluginApis'; -import gridViewColumnApis from './gridViewColumnApis'; -import kanbanViewApis from './kanbanViewApis'; -import { userApis } from './userApi'; +import sortController from '../../controllers/sortController'; +import filterController from '../../controllers/filterController'; +import viewColumnController from '../../controllers/viewColumnController'; +import gridViewController from '../../controllers/gridViewController'; +import viewController from '../../controllers/viewController'; +import galleryViewController from '../../controllers/galleryViewController'; +import formViewController from '../../controllers/formViewController'; +import formViewColumnController from '../../controllers/formViewColumnController'; +import attachmentController from '../../controllers/attachmentController'; +import exportController from '../../controllers/exportController'; +import auditController from '../../controllers/auditController'; +import hookController from '../../controllers/hookController'; +import pluginController from '../../controllers/pluginController'; +import gridViewColumnController from '../../controllers/gridViewColumnController'; +import kanbanViewController from '../../controllers/kanbanViewController'; +import { userController } from '../../controllers/userController'; // import extractProjectIdAndAuthenticate from './helpers/extractProjectIdAndAuthenticate'; -import utilApis from './utilApis'; -import projectUserApis from './projectUserApis'; -import sharedBaseApis from './sharedBaseApis'; -import { initStrategies } from './userApi/initStrategies'; -import modelVisibilityApis from './modelVisibilityApis'; -import metaDiffApis from './metaDiffApis'; -import cacheApis from './cacheApis'; -import apiTokenApis from './apiTokenApis'; -import hookFilterApis from './hookFilterApis'; -import testApis from './testApis'; +import utilController from '../../controllers/utilController'; +import projectUserController from '../../controllers/projectUserController'; +import sharedBaseController from '../../controllers/sharedBaseController'; +import { initStrategies } from '../../controllers/userController/initStrategies'; +import modelVisibilityController from '../../controllers/modelVisibilityController'; +import metaDiffController from '../../controllers/metaDiffController'; +import cacheController from '../../controllers/cacheController'; +import apiTokenController from '../../controllers/apiTokenController'; +import hookFilterController from '../../controllers/hookFilterController'; +import testController from '../../controllers/testController'; import { - bulkDataAliasApis, - dataAliasApis, - dataAliasExportApis, - dataAliasNestedApis, - dataApis, - oldDataApis, -} from './dataApis'; + bulkDataAliasController, + dataAliasController, + dataAliasExportController, + dataAliasNestedController, + dataController, + oldDataController, +} from '../../controllers/dataControllers'; import { - publicDataApis, - publicDataExportApis, - publicMetaApis, -} from './publicApis'; + publicDataController, + publicDataExportController, + publicMetaController, +} from '../../controllers/publicControllers'; import { Server, Socket } from 'socket.io'; import passport from 'passport'; import crypto from 'crypto'; -import swaggerApis from './swagger/swaggerApis'; -import importApis from './sync/importApis'; -import syncSourceApis from './sync/syncSourceApis'; -import mapViewApis from './mapViewApis'; +import swaggerController from '../../controllers/swaggerController'; +import importController from '../../controllers/syncController/importApis'; +import syncSourceController from '../../controllers/syncController'; +import mapViewController from '../../controllers/mapViewController'; const clients: { [id: string]: Socket } = {}; const jobs: { [id: string]: { last_message: any } } = {}; export default function (router: Router, server) { initStrategies(router); - projectApis(router); - baseApis(router); - utilApis(router); + projectController(router); + baseController(router); + utilController(router); if (process.env['PLAYWRIGHT_TEST'] === 'true') { - router.use(testApis); + router.use(testController); } - router.use(columnApis); - router.use(exportApis); - router.use(dataApis); - router.use(bulkDataAliasApis); - router.use(dataAliasApis); - router.use(dataAliasNestedApis); - router.use(dataAliasExportApis); - router.use(oldDataApis); - router.use(sortApis); - router.use(filterApis); - router.use(viewColumnApis); - router.use(gridViewApis); - router.use(formViewColumnApis); - router.use(publicDataApis); - router.use(publicDataExportApis); - router.use(publicMetaApis); - router.use(gridViewColumnApis); - router.use(tableApis); - router.use(galleryViewApis); - router.use(formViewApis); - router.use(viewApis); - router.use(attachmentApis); - router.use(auditApis); - router.use(hookApis); - router.use(pluginApis); - router.use(projectUserApis); - router.use(orgUserApis); - router.use(orgTokenApis); - router.use(orgLicenseApis); - router.use(sharedBaseApis); - router.use(modelVisibilityApis); - router.use(metaDiffApis); - router.use(cacheApis); - router.use(apiTokenApis); - router.use(hookFilterApis); - router.use(swaggerApis); - router.use(syncSourceApis); - router.use(kanbanViewApis); - router.use(mapViewApis); + router.use(columnController); + router.use(exportController); + router.use(dataController); + router.use(bulkDataAliasController); + router.use(dataAliasController); + router.use(dataAliasNestedController); + router.use(dataAliasExportController); + router.use(oldDataController); + router.use(sortController); + router.use(filterController); + router.use(viewColumnController); + router.use(gridViewController); + router.use(formViewColumnController); + router.use(publicDataController); + router.use(publicDataExportController); + router.use(publicMetaController); + router.use(gridViewColumnController); + router.use(tableController); + router.use(galleryViewController); + router.use(formViewController); + router.use(viewController); + router.use(attachmentController); + router.use(auditController); + router.use(hookController); + router.use(pluginController); + router.use(projectUserController); + router.use(orgUserController); + router.use(orgTokenController); + router.use(orgLicenseController); + router.use(sharedBaseController); + router.use(modelVisibilityController); + router.use(metaDiffController); + router.use(cacheController); + router.use(apiTokenController); + router.use(hookFilterController); + router.use(swaggerController); + router.use(syncSourceController); + router.use(kanbanViewController); + router.use(mapViewController); - userApis(router); + userController(router); const io = new Server(server, { cors: { @@ -133,15 +133,15 @@ export default function (router: Router, server) { }).on('connection', (socket) => { clients[socket.id] = socket; const id = getHash( - (process.env.NC_SERVER_UUID || Tele.id) + + (process.env.NC_SERVER_UUID || T.id) + (socket?.handshake as any)?.user?.id ); socket.on('page', (args) => { - Tele.page({ ...args, id }); + T.page({ ...args, id }); }); socket.on('event', (args) => { - Tele.event({ ...args, id }); + T.event({ ...args, id }); }); socket.on('subscribe', (room) => { if (room in jobs) { @@ -152,7 +152,7 @@ export default function (router: Router, server) { }); }); - importApis(router, io, jobs); + importController(router, io, jobs); } function getHash(str) { diff --git a/packages/nocodb/src/lib/meta/api/modelVisibilityApis.ts b/packages/nocodb/src/lib/meta/api/modelVisibilityApis.ts deleted file mode 100644 index 26dc965bb1..0000000000 --- a/packages/nocodb/src/lib/meta/api/modelVisibilityApis.ts +++ /dev/null @@ -1,129 +0,0 @@ -import Model from '../../models/Model'; -import ModelRoleVisibility from '../../models/ModelRoleVisibility'; -import { Router } from 'express'; -import { Tele } from 'nc-help'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import { getAjvValidatorMw } from './helpers'; -async function xcVisibilityMetaSetAll(req, res) { - Tele.emit('evt', { evt_type: 'uiAcl:updated' }); - for (const d of req.body) { - for (const role of Object.keys(d.disabled)) { - const dataInDb = await ModelRoleVisibility.get({ - role, - // fk_model_id: d.fk_model_id, - fk_view_id: d.id, - }); - if (dataInDb) { - if (d.disabled[role]) { - if (!dataInDb.disabled) { - await ModelRoleVisibility.update(d.id, role, { - disabled: d.disabled[role], - }); - } - } else { - await dataInDb.delete(); - } - } else if (d.disabled[role]) { - await ModelRoleVisibility.insert({ - fk_view_id: d.id, - disabled: d.disabled[role], - role, - }); - } - } - } - Tele.emit('evt', { evt_type: 'uiAcl:updated' }); - - res.json({ msg: 'success' }); -} - -// @ts-ignore -export async function xcVisibilityMetaGet( - projectId, - _models: Model[] = null, - includeM2M = true - // type: 'table' | 'tableAndViews' | 'views' = 'table' -) { - // todo: move to - const roles = ['owner', 'creator', 'viewer', 'editor', 'commenter', 'guest']; - - const defaultDisabled = roles.reduce((o, r) => ({ ...o, [r]: false }), {}); - - let models = - _models || - (await Model.list({ - project_id: projectId, - base_id: undefined, - })); - - models = includeM2M ? models : (models.filter((t) => !t.mm) as Model[]); - - const result = await models.reduce(async (_obj, model) => { - const obj = await _obj; - // obj[model.id] = { - // tn: model.tn, - // _tn: model._tn, - // order: model.order, - // fk_model_id: model.id, - // id: model.id, - // type: model.type, - // disabled: { ...defaultDisabled } - // }; - // if (type === 'tableAndViews') { - const views = await model.getViews(); - for (const view of views) { - obj[view.id] = { - ptn: model.table_name, - _ptn: model.title, - ptype: model.type, - tn: view.title, - _tn: view.title, - table_meta: model.meta, - ...view, - disabled: { ...defaultDisabled }, - }; - // } - } - - return obj; - }, Promise.resolve({})); - - const disabledList = await ModelRoleVisibility.list(projectId); - - for (const d of disabledList) { - // if (d.fk_model_id) result[d.fk_model_id].disabled[d.role] = !!d.disabled; - // else if (type === 'tableAndViews' && d.fk_view_id) - if (result[d.fk_view_id]) - result[d.fk_view_id].disabled[d.role] = !!d.disabled; - } - - return Object.values(result); - // ?.sort( - // (a: any, b: any) => - // (a.order || 0) - (b.order || 0) || - // (a?._tn || a?.tn)?.localeCompare(b?._tn || b?.tn) - // ); -} - -const router = Router({ mergeParams: true }); -router.get( - '/api/v1/db/meta/projects/:projectId/visibility-rules', - metaApiMetrics, - ncMetaAclMw(async (req, res) => { - res.json( - await xcVisibilityMetaGet( - req.params.projectId, - null, - req.query.includeM2M === true || req.query.includeM2M === 'true' - ) - ); - }, 'modelVisibilityList') -); -router.post( - '/api/v1/db/meta/projects/:projectId/visibility-rules', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/VisibilityRuleReq'), - ncMetaAclMw(xcVisibilityMetaSetAll, 'modelVisibilitySet') -); -export default router; diff --git a/packages/nocodb/src/lib/meta/api/orgTokenApis.ts b/packages/nocodb/src/lib/meta/api/orgTokenApis.ts deleted file mode 100644 index 300a804c4e..0000000000 --- a/packages/nocodb/src/lib/meta/api/orgTokenApis.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Request, Response, Router } from 'express'; -import { OrgUserRoles } from 'nocodb-sdk'; -import ApiToken from '../../models/ApiToken'; -import { Tele } from 'nc-help'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import { NcError } from '../helpers/catchError'; -import getHandler from '../helpers/getHandler'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { PagedResponseImpl } from '../helpers/PagedResponse'; -import { apiTokenListEE } from './ee/orgTokenApis'; -import { getAjvValidatorMw } from './helpers'; - -async function apiTokenList(req, res) { - const fk_user_id = req.user.id; - let includeUnmappedToken = false; - if (req['user'].roles.includes(OrgUserRoles.SUPER_ADMIN)) { - includeUnmappedToken = true; - } - - res.json( - new PagedResponseImpl( - await ApiToken.listWithCreatedBy({ - ...req.query, - fk_user_id, - includeUnmappedToken, - }), - { - ...req.query, - count: await ApiToken.count({ - includeUnmappedToken, - fk_user_id, - }), - } - ) - ); -} - -export async function apiTokenCreate(req: Request, res: Response) { - Tele.emit('evt', { evt_type: 'org:apiToken:created' }); - res.json(await ApiToken.insert({ ...req.body, fk_user_id: req['user'].id })); -} - -export async function apiTokenDelete(req: Request, res: Response) { - const fk_user_id = req['user'].id; - const apiToken = await ApiToken.getByToken(req.params.token); - if ( - !req['user'].roles.includes(OrgUserRoles.SUPER_ADMIN) && - apiToken.fk_user_id !== fk_user_id - ) { - NcError.notFound('Token not found'); - } - Tele.emit('evt', { evt_type: 'org:apiToken:deleted' }); - res.json(await ApiToken.delete(req.params.token)); -} - -const router = Router({ mergeParams: true }); - -router.get( - '/api/v1/tokens', - metaApiMetrics, - ncMetaAclMw(getHandler(apiTokenList, apiTokenListEE), 'apiTokenList', { - // allowedRoles: [OrgUserRoles.SUPER], - blockApiTokenAccess: true, - }) -); -router.post( - '/api/v1/tokens', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/ApiTokenReq'), - ncMetaAclMw(apiTokenCreate, 'apiTokenCreate', { - // allowedRoles: [OrgUserRoles.SUPER], - blockApiTokenAccess: true, - }) -); -router.delete( - '/api/v1/tokens/:token', - metaApiMetrics, - ncMetaAclMw(apiTokenDelete, 'apiTokenDelete', { - // allowedRoles: [OrgUserRoles.SUPER], - blockApiTokenAccess: true, - }) -); -export default router; diff --git a/packages/nocodb/src/lib/meta/api/orgUserApis.ts b/packages/nocodb/src/lib/meta/api/orgUserApis.ts deleted file mode 100644 index 62c3cdf2aa..0000000000 --- a/packages/nocodb/src/lib/meta/api/orgUserApis.ts +++ /dev/null @@ -1,337 +0,0 @@ -import { Router } from 'express'; -import { - AuditOperationSubTypes, - AuditOperationTypes, - PluginCategory, -} from 'nocodb-sdk'; -import { v4 as uuidv4 } from 'uuid'; -import validator from 'validator'; -import { OrgUserRoles } from 'nocodb-sdk'; -import { NC_APP_SETTINGS } from '../../constants'; -import Audit from '../../models/Audit'; -import ProjectUser from '../../models/ProjectUser'; -import Store from '../../models/Store'; -import SyncSource from '../../models/SyncSource'; -import User from '../../models/User'; -import Noco from '../../Noco'; -import { MetaTable } from '../../utils/globals'; -import { Tele } from 'nc-help'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import { NcError } from '../helpers/catchError'; -import { extractProps } from '../helpers/extractProps'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { PagedResponseImpl } from '../helpers/PagedResponse'; -import { randomTokenString } from '../helpers/stringHelpers'; -import { getAjvValidatorMw } from './helpers'; -import { sendInviteEmail } from './projectUserApis'; - -async function userList(req, res) { - res.json( - new PagedResponseImpl(await User.list(req.query), { - ...req.query, - count: await User.count(req.query), - }) - ); -} - -async function userUpdate(req, res) { - const updateBody = extractProps(req.body, ['roles']); - - const user = await User.get(req.params.userId); - - if (user.roles.includes(OrgUserRoles.SUPER_ADMIN)) { - NcError.badRequest('Cannot update super admin roles'); - } - - res.json( - await User.update(req.params.userId, { - ...updateBody, - token_version: null, - }) - ); -} - -async function userDelete(req, res) { - const ncMeta = await Noco.ncMeta.startTransaction(); - try { - const user = await User.get(req.params.userId, ncMeta); - - if (user.roles.includes(OrgUserRoles.SUPER_ADMIN)) { - NcError.badRequest('Cannot delete super admin'); - } - - // delete project user entry and assign to super admin - const projectUsers = await ProjectUser.getProjectsIdList( - req.params.userId, - ncMeta - ); - - // todo: clear cache - - // TODO: assign super admin as project owner - for (const projectUser of projectUsers) { - await ProjectUser.delete( - projectUser.project_id, - projectUser.fk_user_id, - ncMeta - ); - } - - // delete sync source entry - await SyncSource.deleteByUserId(req.params.userId, ncMeta); - - // delete user - await User.delete(req.params.userId, ncMeta); - await ncMeta.commit(); - } catch (e) { - await ncMeta.rollback(e); - throw e; - } - - res.json({ msg: 'success' }); -} - -async function userAdd(req, res, next) { - // allow only viewer or creator role - if ( - req.body.roles && - ![OrgUserRoles.VIEWER, OrgUserRoles.CREATOR].includes(req.body.roles) - ) { - NcError.badRequest('Invalid role'); - } - - // extract emails from request body - const emails = (req.body.email || '') - .toLowerCase() - .split(/\s*,\s*/) - .map((v) => v.trim()); - - // check for invalid emails - const invalidEmails = emails.filter((v) => !validator.isEmail(v)); - - if (!emails.length) { - return NcError.badRequest('Invalid email address'); - } - if (invalidEmails.length) { - NcError.badRequest('Invalid email address : ' + invalidEmails.join(', ')); - } - - const invite_token = uuidv4(); - const error = []; - - for (const email of emails) { - // add user to project if user already exist - const user = await User.getByEmail(email); - - if (user) { - NcError.badRequest('User already exist'); - } else { - try { - // create new user with invite token - await User.insert({ - invite_token, - invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000), - email, - roles: req.body.roles || OrgUserRoles.VIEWER, - token_version: randomTokenString(), - }); - - const count = await User.count(); - Tele.emit('evt', { evt_type: 'org:user:invite', count }); - - await Audit.insert({ - op_type: AuditOperationTypes.ORG_USER, - op_sub_type: AuditOperationSubTypes.INVITE, - user: req.user.email, - description: `invited ${email} to ${req.params.projectId} project `, - ip: req.clientIp, - }); - // in case of single user check for smtp failure - // and send back token if failed - if ( - emails.length === 1 && - !(await sendInviteEmail(email, invite_token, req)) - ) { - return res.json({ invite_token, email }); - } else { - sendInviteEmail(email, invite_token, req); - } - } catch (e) { - console.log(e); - if (emails.length === 1) { - return next(e); - } else { - error.push({ email, error: e.message }); - } - } - } - } - - if (emails.length === 1) { - res.json({ - msg: 'success', - }); - } else { - return res.json({ invite_token, emails, error }); - } -} - -async function userSettings(_req, _res): Promise { - NcError.notImplemented(); -} - -async function userInviteResend(req, res): Promise { - const user = await User.get(req.params.userId); - - if (!user) { - NcError.badRequest(`User with id '${req.params.userId}' not found`); - } - - const invite_token = uuidv4(); - - await User.update(user.id, { - invite_token, - invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000), - }); - - const pluginData = await Noco.ncMeta.metaGet2(null, null, MetaTable.PLUGIN, { - category: PluginCategory.EMAIL, - active: true, - }); - - if (!pluginData) { - NcError.badRequest( - `No Email Plugin is found. Please go to App Store to configure first or copy the invitation URL to users instead.` - ); - } - - await sendInviteEmail(user.email, invite_token, req); - - await Audit.insert({ - op_type: AuditOperationTypes.ORG_USER, - op_sub_type: AuditOperationSubTypes.RESEND_INVITE, - user: user.email, - description: `resent a invite to ${user.email} `, - ip: req.clientIp, - }); - - res.json({ msg: 'success' }); -} - -async function generateResetUrl(req, res) { - const user = await User.get(req.params.userId); - - if (!user) { - NcError.badRequest(`User with id '${req.params.userId}' not found`); - } - const token = uuidv4(); - await User.update(user.id, { - email: user.email, - reset_password_token: token, - reset_password_expires: new Date(Date.now() + 60 * 60 * 1000), - token_version: null, - }); - - res.json({ - reset_password_token: token, - reset_password_url: req.ncSiteUrl + `/auth/password/reset/${token}`, - }); -} - -async function appSettingsGet(_req, res) { - let settings = {}; - try { - settings = JSON.parse((await Store.get(NC_APP_SETTINGS))?.value); - } catch {} - res.json(settings); -} - -async function appSettingsSet(req, res) { - await Store.saveOrUpdate({ - value: JSON.stringify(req.body), - key: NC_APP_SETTINGS, - }); - - res.json({ msg: 'Settings saved' }); -} - -const router = Router({ mergeParams: true }); -router.get( - '/api/v1/users', - metaApiMetrics, - ncMetaAclMw(userList, 'userList', { - allowedRoles: [OrgUserRoles.SUPER_ADMIN], - blockApiTokenAccess: true, - }) -); -router.patch( - '/api/v1/users/:userId', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/OrgUserReq'), - ncMetaAclMw(userUpdate, 'userUpdate', { - allowedRoles: [OrgUserRoles.SUPER_ADMIN], - blockApiTokenAccess: true, - }) -); -router.delete( - '/api/v1/users/:userId', - metaApiMetrics, - ncMetaAclMw(userDelete, 'userDelete', { - allowedRoles: [OrgUserRoles.SUPER_ADMIN], - blockApiTokenAccess: true, - }) -); -router.post( - '/api/v1/users', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/OrgUserReq'), - ncMetaAclMw(userAdd, 'userAdd', { - allowedRoles: [OrgUserRoles.SUPER_ADMIN], - blockApiTokenAccess: true, - }) -); -router.post( - '/api/v1/users/settings', - metaApiMetrics, - ncMetaAclMw(userSettings, 'userSettings', { - allowedRoles: [OrgUserRoles.SUPER_ADMIN], - blockApiTokenAccess: true, - }) -); -router.post( - '/api/v1/users/:userId/resend-invite', - metaApiMetrics, - ncMetaAclMw(userInviteResend, 'userInviteResend', { - allowedRoles: [OrgUserRoles.SUPER_ADMIN], - blockApiTokenAccess: true, - }) -); - -router.post( - '/api/v1/users/:userId/generate-reset-url', - metaApiMetrics, - ncMetaAclMw(generateResetUrl, 'generateResetUrl', { - allowedRoles: [OrgUserRoles.SUPER_ADMIN], - blockApiTokenAccess: true, - }) -); - -router.get( - '/api/v1/app-settings', - metaApiMetrics, - ncMetaAclMw(appSettingsGet, 'appSettingsGet', { - allowedRoles: [OrgUserRoles.SUPER_ADMIN], - blockApiTokenAccess: true, - }) -); - -router.post( - '/api/v1/app-settings', - metaApiMetrics, - ncMetaAclMw(appSettingsSet, 'appSettingsSet', { - allowedRoles: [OrgUserRoles.SUPER_ADMIN], - blockApiTokenAccess: true, - }) -); - -export default router; diff --git a/packages/nocodb/src/lib/meta/api/projectApis.ts b/packages/nocodb/src/lib/meta/api/projectApis.ts deleted file mode 100644 index 5f45bbb3da..0000000000 --- a/packages/nocodb/src/lib/meta/api/projectApis.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { Request, Response } from 'express'; -import { OrgUserRoles, ProjectType } from 'nocodb-sdk'; -import Project from '../../models/Project'; -import { ProjectListType } from 'nocodb-sdk'; -import DOMPurify from 'isomorphic-dompurify'; -import { packageVersion } from '../../utils/packageVersion'; -import { Tele } from 'nc-help'; -import { PagedResponseImpl } from '../helpers/PagedResponse'; -import syncMigration from '../helpers/syncMigration'; -import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import ProjectUser from '../../models/ProjectUser'; -import { customAlphabet } from 'nanoid'; -import Noco from '../../Noco'; -import isDocker from 'is-docker'; -import { NcError } from '../helpers/catchError'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import { extractPropsAndSanitize } from '../helpers/extractProps'; -import NcConfigFactory from '../../utils/NcConfigFactory'; -import { promisify } from 'util'; -import { getAjvValidatorMw, populateMeta } from './helpers'; -import Filter from '../../models/Filter'; - -const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 4); - -// // Project CRUD - -export async function projectGet( - req: Request, - res: Response -) { - const project = await Project.getWithInfo(req.params.projectId); - - // delete datasource connection details - project.bases?.forEach((b) => { - ['config'].forEach((k) => delete b[k]); - }); - - res.json(project); -} - -export async function projectUpdate( - req: Request, - res: Response -) { - const project = await Project.getWithInfo(req.params.projectId); - - const data: Partial = extractPropsAndSanitize(req?.body, [ - 'title', - 'meta', - 'color', - ]); - - if ( - data?.title && - project.title !== data.title && - (await Project.getByTitle(data.title)) - ) { - NcError.badRequest('Project title already in use'); - } - - const result = await Project.update(req.params.projectId, data); - Tele.emit('evt', { evt_type: 'project:update' }); - res.json(result); -} - -export async function projectList( - req: Request & { user: { id: string; roles: string } }, - res: Response, - next -) { - try { - const projects = req.user?.roles?.includes(OrgUserRoles.SUPER_ADMIN) - ? await Project.list(req.query) - : await ProjectUser.getProjectsList(req.user.id, req.query); - - res // todo: pagination - .json( - new PagedResponseImpl(projects as ProjectType[], { - count: projects.length, - limit: projects.length, - }) - ); - } catch (e) { - console.log(e); - next(e); - } -} - -export async function projectDelete( - req: Request, - res: Response -) { - const result = await Project.softDelete(req.params.projectId); - Tele.emit('evt', { evt_type: 'project:deleted' }); - res.json(result); -} - -// -// - -async function projectCreate(req: Request, res) { - const projectBody = req.body; - if (!projectBody.external) { - const ranId = nanoid(); - projectBody.prefix = `nc_${ranId}__`; - projectBody.is_meta = true; - if (process.env.NC_MINIMAL_DBS) { - // if env variable NC_MINIMAL_DBS is set, then create a SQLite file/connection for each project - // each file will be named as nc_.db - const fs = require('fs'); - const toolDir = NcConfigFactory.getToolDir(); - const nanoidv2 = customAlphabet( - '1234567890abcdefghijklmnopqrstuvwxyz', - 14 - ); - if (!(await promisify(fs.exists)(`${toolDir}/nc_minimal_dbs`))) { - await promisify(fs.mkdir)(`${toolDir}/nc_minimal_dbs`); - } - const dbId = nanoidv2(); - const projectTitle = DOMPurify.sanitize(projectBody.title); - projectBody.prefix = ''; - projectBody.bases = [ - { - type: 'sqlite3', - config: { - client: 'sqlite3', - connection: { - client: 'sqlite3', - database: projectTitle, - connection: { - filename: `${toolDir}/nc_minimal_dbs/${projectTitle}_${dbId}.db`, - }, - }, - }, - inflection_column: 'camelize', - inflection_table: 'camelize', - }, - ]; - } else { - const db = Noco.getConfig().meta?.db; - projectBody.bases = [ - { - type: db?.client, - config: null, - is_meta: true, - inflection_column: 'camelize', - inflection_table: 'camelize', - }, - ]; - } - } else { - if (process.env.NC_CONNECT_TO_EXTERNAL_DB_DISABLED) { - NcError.badRequest('Connecting to external db is disabled'); - } - projectBody.is_meta = false; - } - - if (projectBody?.title.length > 50) { - NcError.badRequest('Project title exceeds 50 characters'); - } - - if (await Project.getByTitle(projectBody?.title)) { - NcError.badRequest('Project title already in use'); - } - - projectBody.title = DOMPurify.sanitize(projectBody.title); - - const project = await Project.createProject(projectBody); - await ProjectUser.insert({ - fk_user_id: (req as any).user.id, - project_id: project.id, - roles: 'owner', - }); - - await syncMigration(project); - - // populate metadata if existing table - for (const base of await project.getBases()) { - const info = await populateMeta(base, project); - - Tele.emit('evt_api_created', info); - delete base.config; - } - - Tele.emit('evt', { - evt_type: 'project:created', - xcdb: !projectBody.external, - }); - - Tele.emit('evt', { evt_type: 'project:rest' }); - - res.json(project); -} - -export async function projectInfoGet(_req, res) { - res.json({ - Node: process.version, - Arch: process.arch, - Platform: process.platform, - Docker: isDocker(), - RootDB: Noco.getConfig()?.meta?.db?.client, - PackageVersion: packageVersion, - }); -} - -export async function projectCost(req, res) { - let cost = 0; - const project = await Project.getWithInfo(req.params.projectId); - - for (const base of project.bases) { - const sqlClient = await NcConnectionMgrv2.getSqlClient(base); - const userCount = await ProjectUser.getUsersCount(req.query); - const recordCount = (await sqlClient.totalRecords())?.data.TotalRecords; - - if (recordCount > 100000) { - // 36,000 or $79/user/month - cost = Math.max(36000, 948 * userCount); - } else if (recordCount > 50000) { - // $36,000 or $50/user/month - cost = Math.max(36000, 600 * userCount); - } else if (recordCount > 10000) { - // $240/user/yr - cost = Math.min(240 * userCount, 36000); - } else if (recordCount > 1000) { - // $120/user/yr - cost = Math.min(120 * userCount, 36000); - } - } - - Tele.event({ - event: 'a:project:cost', - data: { - cost, - }, - }); - - res.json({ cost }); -} - -export async function hasEmptyOrNullFilters(req, res) { - res.json(await Filter.hasEmptyOrNullFilters(req.params.projectId)); -} - -export default (router) => { - router.get( - '/api/v1/db/meta/projects/:projectId/info', - metaApiMetrics, - ncMetaAclMw(projectInfoGet, 'projectInfoGet') - ); - router.get( - '/api/v1/db/meta/projects/:projectId', - metaApiMetrics, - ncMetaAclMw(projectGet, 'projectGet') - ); - router.patch( - '/api/v1/db/meta/projects/:projectId', - metaApiMetrics, - ncMetaAclMw(projectUpdate, 'projectUpdate') - ); - router.get( - '/api/v1/db/meta/projects/:projectId/cost', - metaApiMetrics, - ncMetaAclMw(projectCost, 'projectCost') - ); - router.delete( - '/api/v1/db/meta/projects/:projectId', - metaApiMetrics, - ncMetaAclMw(projectDelete, 'projectDelete') - ); - router.post( - '/api/v1/db/meta/projects', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/ProjectReq'), - ncMetaAclMw(projectCreate, 'projectCreate') - ); - router.get( - '/api/v1/db/meta/projects', - metaApiMetrics, - ncMetaAclMw(projectList, 'projectList') - ); - router.get( - '/api/v1/db/meta/projects/:projectId/has-empty-or-null-filters', - metaApiMetrics, - ncMetaAclMw(hasEmptyOrNullFilters, 'hasEmptyOrNullFilters') - ); -}; diff --git a/packages/nocodb/src/lib/meta/api/projectUserApis.ts b/packages/nocodb/src/lib/meta/api/projectUserApis.ts deleted file mode 100644 index 3f0610fee6..0000000000 --- a/packages/nocodb/src/lib/meta/api/projectUserApis.ts +++ /dev/null @@ -1,333 +0,0 @@ -import { OrgUserRoles } from 'nocodb-sdk'; -import { Tele } from 'nc-help'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { Router } from 'express'; -import { PagedResponseImpl } from '../helpers/PagedResponse'; -import ProjectUser from '../../models/ProjectUser'; -import validator from 'validator'; -import { NcError } from '../helpers/catchError'; -import { v4 as uuidv4 } from 'uuid'; -import User from '../../models/User'; -import Audit from '../../models/Audit'; -import NocoCache from '../../cache/NocoCache'; -import { CacheGetType, CacheScope, MetaTable } from '../../utils/globals'; -import * as ejs from 'ejs'; -import NcPluginMgrv2 from '../helpers/NcPluginMgrv2'; -import Noco from '../../Noco'; -import { PluginCategory } from 'nocodb-sdk'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import { randomTokenString } from '../helpers/stringHelpers'; -import { getAjvValidatorMw } from './helpers'; - -async function userList(req, res) { - res.json({ - users: new PagedResponseImpl( - await ProjectUser.getUsersList({ - ...req.query, - project_id: req.params.projectId, - }), - { - ...req.query, - count: await ProjectUser.getUsersCount(req.query), - } - ), - }); -} - -async function userInvite(req, res, next): Promise { - const emails = (req.body.email || '') - .toLowerCase() - .split(/\s*,\s*/) - .map((v) => v.trim()); - - // check for invalid emails - const invalidEmails = emails.filter((v) => !validator.isEmail(v)); - if (!emails.length) { - return NcError.badRequest('Invalid email address'); - } - if (invalidEmails.length) { - NcError.badRequest('Invalid email address : ' + invalidEmails.join(', ')); - } - - const invite_token = uuidv4(); - const error = []; - - for (const email of emails) { - // add user to project if user already exist - const user = await User.getByEmail(email); - - if (user) { - // check if this user has been added to this project - const projectUser = await ProjectUser.get(req.params.projectId, user.id); - if (projectUser) { - NcError.badRequest( - `${user.email} with role ${projectUser.roles} already exists in this project` - ); - } - - await ProjectUser.insert({ - project_id: req.params.projectId, - fk_user_id: user.id, - roles: req.body.roles || 'editor', - }); - - const cachedUser = await NocoCache.get( - `${CacheScope.USER}:${email}___${req.params.projectId}`, - CacheGetType.TYPE_OBJECT - ); - - if (cachedUser) { - cachedUser.roles = req.body.roles || 'editor'; - await NocoCache.set( - `${CacheScope.USER}:${email}___${req.params.projectId}`, - cachedUser - ); - } - - await Audit.insert({ - project_id: req.params.projectId, - op_type: 'AUTHENTICATION', - op_sub_type: 'INVITE', - user: req.user.email, - description: `invited ${email} to ${req.params.projectId} project `, - ip: req.clientIp, - }); - } else { - try { - // create new user with invite token - const { id } = await User.insert({ - invite_token, - invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000), - email, - roles: OrgUserRoles.VIEWER, - token_version: randomTokenString(), - }); - - // add user to project - await ProjectUser.insert({ - project_id: req.params.projectId, - fk_user_id: id, - roles: req.body.roles, - }); - - const count = await User.count(); - Tele.emit('evt', { evt_type: 'project:invite', count }); - - await Audit.insert({ - project_id: req.params.projectId, - op_type: 'AUTHENTICATION', - op_sub_type: 'INVITE', - user: req.user.email, - description: `invited ${email} to ${req.params.projectId} project `, - ip: req.clientIp, - }); - // in case of single user check for smtp failure - // and send back token if failed - if ( - emails.length === 1 && - !(await sendInviteEmail(email, invite_token, req)) - ) { - return res.json({ invite_token, email }); - } else { - sendInviteEmail(email, invite_token, req); - } - } catch (e) { - console.log(e); - if (emails.length === 1) { - return next(e); - } else { - error.push({ email, error: e.message }); - } - } - } - } - - if (emails.length === 1) { - res.json({ - msg: 'success', - }); - } else { - return res.json({ invite_token, emails, error }); - } -} - -// @ts-ignore -async function projectUserUpdate(req, res, next): Promise { - if (!req?.body?.project_id) { - return next(new Error('Missing project id in request body.')); - } - - if ( - req.session?.passport?.user?.roles?.owner && - req.session?.passport?.user?.id === req.params.userId && - req.body.roles.indexOf('owner') === -1 - ) { - NcError.badRequest("Super admin can't remove Super role themselves"); - } - try { - const user = await User.get(req.params.userId); - - if (!user) { - NcError.badRequest(`User with id '${req.params.userId}' doesn't exist`); - } - - // todo: handle roles which contains super - if ( - !req.session?.passport?.user?.roles?.owner && - req.body.roles.indexOf('owner') > -1 - ) { - NcError.forbidden('Insufficient privilege to add super admin role.'); - } - - await ProjectUser.update( - req.params.projectId, - req.params.userId, - req.body.roles - ); - - await Audit.insert({ - op_type: 'AUTHENTICATION', - op_sub_type: 'ROLES_MANAGEMENT', - user: req.user.email, - description: `updated roles for ${user.email} with ${req.body.roles} `, - ip: req.clientIp, - }); - - res.json({ - msg: 'User details updated successfully', - }); - } catch (e) { - next(e); - } -} - -async function projectUserDelete(req, res): Promise { - const project_id = req.params.projectId; - - if (req.session?.passport?.user?.id === req.params.userId) { - NcError.badRequest("Admin can't delete themselves!"); - } - - if (!req.session?.passport?.user?.roles?.owner) { - const user = await User.get(req.params.userId); - if (user.roles?.split(',').includes('super')) - NcError.forbidden('Insufficient privilege to delete a super admin user.'); - - const projectUser = await ProjectUser.get(project_id, req.params.userId); - if (projectUser?.roles?.split(',').includes('super')) - NcError.forbidden('Insufficient privilege to delete a owner user.'); - } - - await ProjectUser.delete(project_id, req.params.userId); - res.json({ - msg: 'success', - }); -} - -async function projectUserInviteResend(req, res): Promise { - const user = await User.get(req.params.userId); - - if (!user) { - NcError.badRequest(`User with id '${req.params.userId}' not found`); - } - - req.body.roles = user.roles; - const invite_token = uuidv4(); - - await User.update(user.id, { - invite_token, - invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000), - }); - - const pluginData = await Noco.ncMeta.metaGet2(null, null, MetaTable.PLUGIN, { - category: PluginCategory.EMAIL, - active: true, - }); - - if (!pluginData) { - NcError.badRequest( - `No Email Plugin is found. Please go to App Store to configure first or copy the invitation URL to users instead.` - ); - } - - await sendInviteEmail(user.email, invite_token, req); - - await Audit.insert({ - op_type: 'AUTHENTICATION', - op_sub_type: 'RESEND_INVITE', - user: user.email, - description: `resent a invite to ${user.email} `, - ip: req.clientIp, - project_id: req.params.projectId, - }); - - res.json({ msg: 'success' }); -} - -export async function sendInviteEmail( - email: string, - token: string, - req: any -): Promise { - try { - const template = (await import('./userApi/ui/emailTemplates/invite')) - .default; - - const emailAdapter = await NcPluginMgrv2.emailAdapter(); - - if (emailAdapter) { - await emailAdapter.mailSend({ - to: email, - subject: 'Verify email', - html: ejs.render(template, { - signupLink: `${req.ncSiteUrl}${ - Noco.getConfig()?.dashboardPath - }#/signup/${token}`, - projectName: req.body?.projectName, - roles: (req.body?.roles || '') - .split(',') - .map((r) => r.replace(/^./, (m) => m.toUpperCase())) - .join(', '), - adminEmail: req.session?.passport?.user?.email, - }), - }); - return true; - } - } catch (e) { - console.log( - 'Warning : `mailSend` failed, Please configure emailClient configuration.', - e.message - ); - throw e; - } -} - -const router = Router({ mergeParams: true }); -router.get( - '/api/v1/db/meta/projects/:projectId/users', - metaApiMetrics, - ncMetaAclMw(userList, 'userList') -); -router.post( - '/api/v1/db/meta/projects/:projectId/users', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/ProjectUserReq'), - ncMetaAclMw(userInvite, 'userInvite') -); -router.patch( - '/api/v1/db/meta/projects/:projectId/users/:userId', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/ProjectUserReq'), - ncMetaAclMw(projectUserUpdate, 'projectUserUpdate') -); -router.delete( - '/api/v1/db/meta/projects/:projectId/users/:userId', - metaApiMetrics, - ncMetaAclMw(projectUserDelete, 'projectUserDelete') -); -router.post( - '/api/v1/db/meta/projects/:projectId/users/:userId/resend-invite', - metaApiMetrics, - ncMetaAclMw(projectUserInviteResend, 'projectUserInviteResend') -); -export default router; diff --git a/packages/nocodb/src/lib/meta/api/publicApis/index.ts b/packages/nocodb/src/lib/meta/api/publicApis/index.ts deleted file mode 100644 index 60028a421f..0000000000 --- a/packages/nocodb/src/lib/meta/api/publicApis/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import publicDataApis from './publicDataApis'; -import publicDataExportApis from './publicDataExportApis'; -import publicMetaApis from './publicMetaApis'; - -export { publicDataApis, publicDataExportApis, publicMetaApis }; diff --git a/packages/nocodb/src/lib/meta/api/publicApis/publicDataApis.ts b/packages/nocodb/src/lib/meta/api/publicApis/publicDataApis.ts deleted file mode 100644 index bb678b10f3..0000000000 --- a/packages/nocodb/src/lib/meta/api/publicApis/publicDataApis.ts +++ /dev/null @@ -1,472 +0,0 @@ -import { Request, Response, Router } from 'express'; -import Model from '../../../models/Model'; -import { nocoExecute } from 'nc-help'; -import Base from '../../../models/Base'; -import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; -import { PagedResponseImpl } from '../../helpers/PagedResponse'; -import View from '../../../models/View'; -import catchError, { NcError } from '../../helpers/catchError'; -import multer from 'multer'; -import { ErrorMessages, UITypes, ViewTypes } from 'nocodb-sdk'; -import Column from '../../../models/Column'; -import LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn'; -import NcPluginMgrv2 from '../../helpers/NcPluginMgrv2'; -import path from 'path'; -import { nanoid } from 'nanoid'; -import { mimeIcons } from '../../../utils/mimeTypes'; -import slash from 'slash'; -import { sanitizeUrlPath } from '../attachmentApis'; -import getAst from '../../../db/sql-data-mapper/lib/sql/helpers/getAst'; -import { getColumnByIdOrName } from '../dataApis/helpers'; -import { NC_ATTACHMENT_FIELD_SIZE } from '../../../constants'; - -export async function dataList(req: Request, res: Response) { - try { - const view = await View.getByUUID(req.params.sharedViewUuid); - - if (!view) NcError.notFound('Not found'); - if ( - view.type !== ViewTypes.GRID && - view.type !== ViewTypes.KANBAN && - view.type !== ViewTypes.GALLERY && - view.type !== ViewTypes.MAP - ) { - NcError.notFound('Not found'); - } - - if (view.password && view.password !== req.headers?.['xc-password']) { - return NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); - } - - const model = await Model.getByIdOrName({ - id: view?.fk_model_id, - }); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const listArgs: any = { ...req.query }; - try { - listArgs.filterArr = JSON.parse(listArgs.filterArrJson); - } catch (e) {} - try { - listArgs.sortArr = JSON.parse(listArgs.sortArrJson); - } catch (e) {} - - let data = []; - let count = 0; - - try { - data = await nocoExecute( - await getAst({ - query: req.query, - model, - view, - }), - await baseModel.list(listArgs), - {}, - listArgs - ); - count = await baseModel.count(listArgs); - } catch (e) { - // show empty result instead of throwing error here - // e.g. search some text in a numeric field - } - - res.json({ - data: new PagedResponseImpl(data, { ...req.query, count }), - }); - } catch (e) { - console.log(e); - res.status(500).json({ msg: e.message }); - } -} - -// todo: Handle the error case where view doesnt belong to model -async function groupedDataList(req: Request, res: Response) { - try { - const view = await View.getByUUID(req.params.sharedViewUuid); - - if (!view) NcError.notFound('Not found'); - - if ( - view.type !== ViewTypes.GRID && - view.type !== ViewTypes.KANBAN && - view.type !== ViewTypes.GALLERY - ) { - NcError.notFound('Not found'); - } - - if (view.password && view.password !== req.headers?.['xc-password']) { - return NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); - } - - const model = await Model.getByIdOrName({ - id: view?.fk_model_id, - }); - - res.json(await getGroupedDataList(model, view, req)); - } catch (e) { - console.log(e); - res.status(500).json({ msg: e.message }); - } -} - -async function getGroupedDataList(model, view: View, req) { - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const requestObj = await getAst({ model, query: req.query, view }); - - const listArgs: any = { ...req.query }; - try { - listArgs.filterArr = JSON.parse(listArgs.filterArrJson); - } catch (e) {} - try { - listArgs.sortArr = JSON.parse(listArgs.sortArrJson); - } catch (e) {} - try { - listArgs.options = JSON.parse(listArgs.optionsArrJson); - } catch (e) {} - - let data = []; - - try { - const groupedData = await baseModel.groupedList({ - ...listArgs, - groupColumnId: req.params.columnId, - }); - data = await nocoExecute( - { key: 1, value: requestObj }, - groupedData, - {}, - listArgs - ); - const countArr = await baseModel.groupedListCount({ - ...listArgs, - groupColumnId: req.params.columnId, - }); - data = data.map((item) => { - // todo: use map to avoid loop - const count = - countArr.find((countItem: any) => countItem.key === item.key)?.count ?? - 0; - - item.value = new PagedResponseImpl(item.value, { - ...req.query, - count: count, - }); - return item; - }); - } catch (e) { - // show empty result instead of throwing error here - // e.g. search some text in a numeric field - } - return data; -} - -async function dataInsert( - req: Request & { files: any[] }, - res: Response, - next -) { - const view = await View.getByUUID(req.params.sharedViewUuid); - - if (!view) return next(new Error('Not found')); - if (view.type !== ViewTypes.FORM) return next(new Error('Not found')); - - if (view.password && view.password !== req.headers?.['xc-password']) { - return res.status(403).json(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); - } - - const model = await Model.getByIdOrName({ - id: view?.fk_model_id, - }); - const base = await Base.get(model.base_id); - const project = await base.getProject(); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - await view.getViewWithInfo(); - await view.getColumns(); - await view.getModelWithInfo(); - await view.model.getColumns(); - - const fields = (view.model.columns = view.columns - .filter((c) => c.show) - .reduce((o, c) => { - o[view.model.columnsById[c.fk_column_id].title] = new Column({ - ...c, - ...view.model.columnsById[c.fk_column_id], - } as any); - return o; - }, {}) as any); - - let body = req.body?.data; - - if (typeof body === 'string') body = JSON.parse(body); - - const insertObject = Object.entries(body).reduce((obj, [key, val]) => { - if (key in fields) { - obj[key] = val; - } - return obj; - }, {}); - - const attachments = {}; - const storageAdapter = await NcPluginMgrv2.storageAdapter(); - - for (const file of req.files || []) { - // remove `_` prefix and `[]` suffix - const fieldName = file?.fieldname?.replace(/^_|\[\d*]$/g, ''); - - const filePath = sanitizeUrlPath([ - 'v1', - project.title, - model.title, - fieldName, - ]); - - if (fieldName in fields && fields[fieldName].uidt === UITypes.Attachment) { - attachments[fieldName] = attachments[fieldName] || []; - const fileName = `${nanoid(6)}_${file.originalname}`; - let url = await storageAdapter.fileCreate( - slash(path.join('nc', 'uploads', ...filePath, fileName)), - file - ); - - if (!url) { - url = `${(req as any).ncSiteUrl}/download/${filePath.join( - '/' - )}/${fileName}`; - } - - attachments[fieldName].push({ - url, - title: file.originalname, - mimetype: file.mimetype, - size: file.size, - icon: mimeIcons[path.extname(file.originalname).slice(1)] || undefined, - }); - } - } - - for (const [column, data] of Object.entries(attachments)) { - insertObject[column] = JSON.stringify(data); - } - - res.json(await baseModel.nestedInsert(insertObject, null)); -} - -async function relDataList(req, res) { - const view = await View.getByUUID(req.params.sharedViewUuid); - - if (!view) NcError.notFound('Not found'); - if (view.type !== ViewTypes.FORM) NcError.notFound('Not found'); - - if (view.password && view.password !== req.headers?.['xc-password']) { - NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); - } - - const column = await Column.get({ colId: req.params.columnId }); - const colOptions = await column.getColOptions(); - - const model = await colOptions.getRelatedTable(); - - const base = await Base.get(model.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: model.id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const requestObj = await getAst({ - query: req.query, - model, - extractOnlyPrimaries: true, - }); - - let data = []; - let count = 0; - try { - data = data = await nocoExecute( - requestObj, - await baseModel.list(req.query), - {}, - req.query - ); - count = await baseModel.count(req.query); - } catch (e) { - // show empty result instead of throwing error here - // e.g. search some text in a numeric field - } - - res.json(new PagedResponseImpl(data, { ...req.query, count })); -} - -export async function publicMmList(req: Request, res: Response) { - const view = await View.getByUUID(req.params.sharedViewUuid); - - if (!view) NcError.notFound('Not found'); - if (view.type !== ViewTypes.GRID) NcError.notFound('Not found'); - - if (view.password && view.password !== req.headers?.['xc-password']) { - NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); - } - - const column = await getColumnByIdOrName( - req.params.colId, - await view.getModel() - ); - - if (column.fk_model_id !== view.fk_model_id) - NcError.badRequest("Column doesn't belongs to the model"); - - const base = await Base.get(view.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: view.fk_model_id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const key = `List`; - const requestObj: any = { - [key]: 1, - }; - - const data = ( - await nocoExecute( - requestObj, - { - [key]: async (args) => { - return await baseModel.mmList( - { - colId: req.params.colId, - parentId: req.params.rowId, - }, - args - ); - }, - }, - {}, - - { nested: { [key]: req.query } } - ) - )?.[key]; - - const count: any = await baseModel.mmListCount({ - colId: req.params.colId, - parentId: req.params.rowId, - }); - - res.json(new PagedResponseImpl(data, { ...req.query, count })); -} - -export async function publicHmList(req: Request, res: Response) { - const view = await View.getByUUID(req.params.sharedViewUuid); - - if (!view) NcError.notFound('Not found'); - if (view.type !== ViewTypes.GRID) NcError.notFound('Not found'); - - if (view.password && view.password !== req.headers?.['xc-password']) { - NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); - } - - const column = await getColumnByIdOrName( - req.params.colId, - await view.getModel() - ); - - if (column.fk_model_id !== view.fk_model_id) - NcError.badRequest("Column doesn't belongs to the model"); - - const base = await Base.get(view.base_id); - - const baseModel = await Model.getBaseModelSQL({ - id: view.fk_model_id, - viewId: view?.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - const key = `List`; - const requestObj: any = { - [key]: 1, - }; - - const data = ( - await nocoExecute( - requestObj, - { - [key]: async (args) => { - return await baseModel.hmList( - { - colId: req.params.colId, - id: req.params.rowId, - }, - args - ); - }, - }, - {}, - { nested: { [key]: req.query } } - ) - )?.[key]; - - const count = await baseModel.hmListCount({ - colId: req.params.colId, - id: req.params.rowId, - }); - - res.json(new PagedResponseImpl(data, { ...req.query, count })); -} - -const router = Router({ mergeParams: true }); -router.get( - '/api/v1/db/public/shared-view/:sharedViewUuid/rows', - catchError(dataList) -); -router.get( - '/api/v1/db/public/shared-view/:sharedViewUuid/group/:columnId', - catchError(groupedDataList) -); -router.get( - '/api/v1/db/public/shared-view/:sharedViewUuid/nested/:columnId', - catchError(relDataList) -); -router.post( - '/api/v1/db/public/shared-view/:sharedViewUuid/rows', - multer({ - storage: multer.diskStorage({}), - limits: { - fieldSize: NC_ATTACHMENT_FIELD_SIZE, - }, - }).any(), - catchError(dataInsert) -); - -router.get( - '/api/v1/db/public/shared-view/:sharedViewUuid/rows/:rowId/mm/:colId', - catchError(publicMmList) -); -router.get( - '/api/v1/db/public/shared-view/:sharedViewUuid/rows/:rowId/hm/:colId', - catchError(publicHmList) -); - -export default router; diff --git a/packages/nocodb/src/lib/meta/api/sharedBaseApis.ts b/packages/nocodb/src/lib/meta/api/sharedBaseApis.ts deleted file mode 100644 index c232327aa8..0000000000 --- a/packages/nocodb/src/lib/meta/api/sharedBaseApis.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { Router } from 'express'; -import { Tele } from 'nc-help'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { v4 as uuidv4 } from 'uuid'; -import Project from '../../models/Project'; -import { NcError } from '../helpers/catchError'; -import { getAjvValidatorMw } from './helpers'; -// todo: load from config -const config = { - dashboardPath: '/nc', -}; - -async function createSharedBaseLink(req, res): Promise { - const project = await Project.get(req.params.projectId); - - let roles = req.body?.roles; - if (!roles || (roles !== 'editor' && roles !== 'viewer')) { - roles = 'viewer'; - } - - if (!project) { - NcError.badRequest('Invalid project id'); - } - const data: any = { - uuid: uuidv4(), - password: req.body?.password, - roles, - }; - - await Project.update(project.id, data); - - data.url = `${req.ncSiteUrl}${config.dashboardPath}#/nc/base/${data.uuid}`; - delete data.password; - Tele.emit('evt', { evt_type: 'sharedBase:generated-link' }); - res.json(data); -} -async function updateSharedBaseLink(req, res): Promise { - const project = await Project.get(req.params.projectId); - - let roles = req.body?.roles; - if (!roles || (roles !== 'editor' && roles !== 'viewer')) { - roles = 'viewer'; - } - - if (!project) { - NcError.badRequest('Invalid project id'); - } - const data: any = { - uuid: project.uuid || uuidv4(), - password: req.body?.password, - roles, - }; - - await Project.update(project.id, data); - - data.url = `${req.ncSiteUrl}${config.dashboardPath}#/nc/base/${data.uuid}`; - delete data.password; - Tele.emit('evt', { evt_type: 'sharedBase:generated-link' }); - res.json(data); -} - -async function disableSharedBaseLink(req, res): Promise { - const project = await Project.get(req.params.projectId); - - if (!project) { - NcError.badRequest('Invalid project id'); - } - const data: any = { - uuid: null, - }; - - await Project.update(project.id, data); - Tele.emit('evt', { evt_type: 'sharedBase:disable-link' }); - res.json({ uuid: null }); -} - -async function getSharedBaseLink(req, res): Promise { - const project = await Project.get(req.params.projectId); - - if (!project) { - NcError.badRequest('Invalid project id'); - } - const data: any = { - uuid: project.uuid, - roles: project.roles, - }; - if (data.uuid) - data.url = `${req.ncSiteUrl}${config.dashboardPath}#/nc/base/${data.shared_base_id}`; - - res.json(data); -} - -const router = Router({ mergeParams: true }); -router.get( - '/api/v1/db/meta/projects/:projectId/shared', - ncMetaAclMw(getSharedBaseLink, 'getSharedBaseLink') -); -router.post( - '/api/v1/db/meta/projects/:projectId/shared', - getAjvValidatorMw('swagger.json#/components/schemas/SharedBaseReq'), - ncMetaAclMw(createSharedBaseLink, 'createSharedBaseLink') -); -router.patch( - '/api/v1/db/meta/projects/:projectId/shared', - getAjvValidatorMw('swagger.json#/components/schemas/SharedBaseReq'), - ncMetaAclMw(updateSharedBaseLink, 'updateSharedBaseLink') -); -router.delete( - '/api/v1/db/meta/projects/:projectId/shared', - ncMetaAclMw(disableSharedBaseLink, 'disableSharedBaseLink') -); -export default router; diff --git a/packages/nocodb/src/lib/meta/api/sortApis.ts b/packages/nocodb/src/lib/meta/api/sortApis.ts deleted file mode 100644 index 5341d07cba..0000000000 --- a/packages/nocodb/src/lib/meta/api/sortApis.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Request, Response, Router } from 'express'; -// @ts-ignore -import Model from '../../models/Model'; -import { Tele } from 'nc-help'; -// @ts-ignore -import { PagedResponseImpl } from '../helpers/PagedResponse'; -import { SortListType, SortType, TableType } from 'nocodb-sdk'; -// @ts-ignore -import ProjectMgrv2 from '../../db/sql-mgr/v2/ProjectMgrv2'; -// @ts-ignore -import Project from '../../models/Project'; -import Sort from '../../models/Sort'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import { getAjvValidatorMw } from './helpers'; - -// @ts-ignore -export async function sortGet(req: Request, res: Response) {} - -// @ts-ignore -export async function sortList( - req: Request, - res: Response -) { - const sortList = await Sort.list({ viewId: req.params.viewId }); - res.json({ - sorts: new PagedResponseImpl(sortList), - }); -} - -// @ts-ignore -export async function sortCreate(req: Request, res) { - const sort = await Sort.insert({ - ...req.body, - fk_view_id: req.params.viewId, - } as Sort); - Tele.emit('evt', { evt_type: 'sort:created' }); - res.json(sort); -} - -export async function sortUpdate(req, res) { - const sort = await Sort.update(req.params.sortId, req.body); - Tele.emit('evt', { evt_type: 'sort:updated' }); - res.json(sort); -} - -export async function sortDelete(req: Request, res: Response) { - Tele.emit('evt', { evt_type: 'sort:deleted' }); - const sort = await Sort.delete(req.params.sortId); - res.json(sort); -} - -const router = Router({ mergeParams: true }); -router.get( - '/api/v1/db/meta/views/:viewId/sorts/', - metaApiMetrics, - ncMetaAclMw(sortList, 'sortList') -); -router.post( - '/api/v1/db/meta/views/:viewId/sorts/', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/SortReq'), - ncMetaAclMw(sortCreate, 'sortCreate') -); -router.get( - '/api/v1/db/meta/sorts/:sortId', - metaApiMetrics, - ncMetaAclMw(sortGet, 'sortGet') -); -router.patch( - '/api/v1/db/meta/sorts/:sortId', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/SortReq'), - ncMetaAclMw(sortUpdate, 'sortUpdate') -); -router.delete( - '/api/v1/db/meta/sorts/:sortId', - metaApiMetrics, - ncMetaAclMw(sortDelete, 'sortDelete') -); -export default router; diff --git a/packages/nocodb/src/lib/meta/api/swagger/swaggerApis.ts b/packages/nocodb/src/lib/meta/api/swagger/swaggerApis.ts deleted file mode 100644 index c8cb637ba1..0000000000 --- a/packages/nocodb/src/lib/meta/api/swagger/swaggerApis.ts +++ /dev/null @@ -1,61 +0,0 @@ -// @ts-ignore -import catchError, { NcError } from '../../helpers/catchError'; -import { Router } from 'express'; -import Model from '../../../models/Model'; -import ncMetaAclMw from '../../helpers/ncMetaAclMw'; -import getSwaggerJSON from './helpers/getSwaggerJSON'; -import Project from '../../../models/Project'; -import getSwaggerHtml from './swaggerHtml'; -import getRedocHtml from './redocHtml'; - -async function swaggerJson(req, res) { - const project = await Project.get(req.params.projectId); - - if (!project) NcError.notFound(); - - const models = await Model.list({ - project_id: req.params.projectId, - base_id: null, - }); - - const swagger = await getSwaggerJSON(project, models); - - swagger.servers = [ - { - url: req.ncSiteUrl, - }, - { - url: '{customUrl}', - variables: { - customUrl: { - default: req.ncSiteUrl, - description: 'Provide custom nocodb app base url', - }, - }, - }, - ] as any; - - res.json(swagger); -} - -function swaggerHtml(_, res) { - res.send(getSwaggerHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' })); -} - -function redocHtml(_, res) { - res.send(getRedocHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' })); -} - -const router = Router({ mergeParams: true }); - -// todo: auth -router.get( - '/api/v1/db/meta/projects/:projectId/swagger.json', - ncMetaAclMw(swaggerJson, 'swaggerJson') -); - -router.get('/api/v1/db/meta/projects/:projectId/swagger', swaggerHtml); - -router.get('/api/v1/db/meta/projects/:projectId/redoc', redocHtml); - -export default router; diff --git a/packages/nocodb/src/lib/meta/api/tableApis.ts b/packages/nocodb/src/lib/meta/api/tableApis.ts deleted file mode 100644 index 4c5d6856a5..0000000000 --- a/packages/nocodb/src/lib/meta/api/tableApis.ts +++ /dev/null @@ -1,447 +0,0 @@ -import { Request, Response, Router } from 'express'; -import Model from '../../models/Model'; -import { Tele } from 'nc-help'; -import { PagedResponseImpl } from '../helpers/PagedResponse'; -import DOMPurify from 'isomorphic-dompurify'; -import { - AuditOperationSubTypes, - AuditOperationTypes, - isVirtualCol, - ModelTypes, - NormalColumnRequestType, - TableListType, - TableReqType, - TableType, - UITypes, -} from 'nocodb-sdk'; -import ProjectMgrv2 from '../../db/sql-mgr/v2/ProjectMgrv2'; -import Project from '../../models/Project'; -import Audit from '../../models/Audit'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { getAjvValidatorMw } from './helpers'; -import { xcVisibilityMetaGet } from './modelVisibilityApis'; -import View from '../../models/View'; -import getColumnPropsFromUIDT from '../helpers/getColumnPropsFromUIDT'; -import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue'; -import { NcError } from '../helpers/catchError'; -import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName'; -import Column from '../../models/Column'; -import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; -import getColumnUiType from '../helpers/getColumnUiType'; -import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn'; -import { metaApiMetrics } from '../helpers/apiMetrics'; - -export async function tableGet(req: Request, res: Response) { - const table = await Model.getWithInfo({ - id: req.params.tableId, - }); - - // todo: optimise - const viewList = await xcVisibilityMetaGet(table.project_id, [table]); - - //await View.list(req.params.tableId) - table.views = viewList.filter((table: any) => { - return Object.keys((req as any).session?.passport?.user?.roles).some( - (role) => - (req as any)?.session?.passport?.user?.roles[role] && - !table.disabled[role] - ); - }); - - res.json(table); -} - -export async function tableReorder(req: Request, res: Response) { - res.json(Model.updateOrder(req.params.tableId, req.body.order)); -} - -export async function tableList(req: Request, res: Response) { - const viewList = await xcVisibilityMetaGet(req.params.projectId); - - // todo: optimise - const tableViewMapping = viewList.reduce((o, view: any) => { - o[view.fk_model_id] = o[view.fk_model_id] || 0; - if ( - Object.keys((req as any).session?.passport?.user?.roles).some( - (role) => - (req as any)?.session?.passport?.user?.roles[role] && - !view.disabled[role] - ) - ) { - o[view.fk_model_id]++; - } - return o; - }, {}); - - const tableList = ( - await Model.list({ - project_id: req.params.projectId, - base_id: req.params.baseId, - }) - ).filter((t) => tableViewMapping[t.id]); - - res.json( - new PagedResponseImpl( - req.query?.includeM2M === 'true' - ? tableList - : (tableList.filter((t) => !t.mm) as Model[]) - ) - ); -} - -export async function tableCreate(req: Request, res) { - const project = await Project.getWithInfo(req.params.projectId); - let base = project.bases[0]; - - if (req.params.baseId) { - base = project.bases.find((b) => b.id === req.params.baseId); - } - - if ( - !req.body.table_name || - (project.prefix && project.prefix === req.body.table_name) - ) { - NcError.badRequest( - 'Missing table name `table_name` property in request body' - ); - } - - if (base.is_meta && project.prefix) { - if (!req.body.table_name.startsWith(project.prefix)) { - req.body.table_name = `${project.prefix}_${req.body.table_name}`; - } - } - - req.body.table_name = DOMPurify.sanitize(req.body.table_name); - - // validate table name - if (/^\s+|\s+$/.test(req.body.table_name)) { - NcError.badRequest( - 'Leading or trailing whitespace not allowed in table names' - ); - } - - if ( - !(await Model.checkTitleAvailable({ - table_name: req.body.table_name, - project_id: project.id, - base_id: base.id, - })) - ) { - NcError.badRequest('Duplicate table name'); - } - - if (!req.body.title) { - req.body.title = getTableNameAlias( - req.body.table_name, - project.prefix, - base - ); - } - - if ( - !(await Model.checkAliasAvailable({ - title: req.body.title, - project_id: project.id, - base_id: base.id, - })) - ) { - NcError.badRequest('Duplicate table alias'); - } - - const sqlMgr = await ProjectMgrv2.getSqlMgr(project); - - const sqlClient = await NcConnectionMgrv2.getSqlClient(base); - - let tableNameLengthLimit = 255; - const sqlClientType = sqlClient.knex.clientType(); - if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') { - tableNameLengthLimit = 64; - } else if (sqlClientType === 'pg') { - tableNameLengthLimit = 63; - } else if (sqlClientType === 'mssql') { - tableNameLengthLimit = 128; - } - - if (req.body.table_name.length > tableNameLengthLimit) { - NcError.badRequest(`Table name exceeds ${tableNameLengthLimit} characters`); - } - - const mxColumnLength = Column.getMaxColumnNameLength(sqlClientType); - - for (const column of req.body.columns) { - if (column.column_name.length > mxColumnLength) { - NcError.badRequest( - `Column name ${column.column_name} exceeds ${mxColumnLength} characters` - ); - } - } - - req.body.columns = req.body.columns?.map((c) => ({ - ...getColumnPropsFromUIDT(c as any, base), - cn: c.column_name, - })); - await sqlMgr.sqlOpPlus(base, 'tableCreate', { - ...req.body, - tn: req.body.table_name, - }); - - const columns: Array< - Omit & { - cn: string; - system?: boolean; - } - > = (await sqlClient.columnList({ tn: req.body.table_name }))?.data?.list; - - const tables = await Model.list({ - project_id: project.id, - base_id: base.id, - }); - - await Audit.insert({ - project_id: project.id, - base_id: base.id, - op_type: AuditOperationTypes.TABLE, - op_sub_type: AuditOperationSubTypes.CREATED, - user: (req as any)?.user?.email, - description: `created table ${req.body.table_name} with alias ${req.body.title} `, - ip: (req as any).clientIp, - }).then(() => {}); - - mapDefaultDisplayValue(req.body.columns); - - Tele.emit('evt', { evt_type: 'table:created' }); - - res.json( - await Model.insert(project.id, base.id, { - ...req.body, - columns: columns.map((c, i) => { - const colMetaFromReq = req.body?.columns?.find( - (c1) => c.cn === c1.column_name - ) as NormalColumnRequestType; - return { - ...colMetaFromReq, - uidt: - (colMetaFromReq?.uidt as string) || - c.uidt || - getColumnUiType(base, c), - ...c, - dtxp: [UITypes.MultiSelect, UITypes.SingleSelect].includes( - colMetaFromReq.uidt as any - ) - ? colMetaFromReq.dtxp - : c.dtxp, - title: colMetaFromReq?.title || getColumnNameAlias(c.cn, base), - column_name: c.cn, - order: i + 1, - } as NormalColumnRequestType; - }), - order: +(tables?.pop()?.order ?? 0) + 1, - }) - ); -} - -export async function tableUpdate(req: Request, res) { - const model = await Model.get(req.params.tableId); - - const project = await Project.getWithInfo( - req.body.project_id || (req as any).ncProjectId - ); - const base = project.bases.find((b) => b.id === model.base_id); - - if (model.project_id !== project.id) { - NcError.badRequest('Model does not belong to project'); - } - - // if meta present update meta and return - // todo: allow user to update meta and other prop in single api call - if ('meta' in req.body) { - await Model.updateMeta(req.params.tableId, req.body.meta); - - return res.json({ msg: 'success' }); - } - - if (!req.body.table_name) { - NcError.badRequest( - 'Missing table name `table_name` property in request body' - ); - } - - if (base.is_meta && project.prefix) { - if (!req.body.table_name.startsWith(project.prefix)) { - req.body.table_name = `${project.prefix}${req.body.table_name}`; - } - } - - req.body.table_name = DOMPurify.sanitize(req.body.table_name); - - // validate table name - if (/^\s+|\s+$/.test(req.body.table_name)) { - NcError.badRequest( - 'Leading or trailing whitespace not allowed in table names' - ); - } - - if ( - !(await Model.checkTitleAvailable({ - table_name: req.body.table_name, - project_id: project.id, - base_id: base.id, - })) - ) { - NcError.badRequest('Duplicate table name'); - } - - if (!req.body.title) { - req.body.title = getTableNameAlias( - req.body.table_name, - project.prefix, - base - ); - } - - if ( - !(await Model.checkAliasAvailable({ - title: req.body.title, - project_id: project.id, - base_id: base.id, - })) - ) { - NcError.badRequest('Duplicate table alias'); - } - - const sqlMgr = await ProjectMgrv2.getSqlMgr(project); - const sqlClient = await NcConnectionMgrv2.getSqlClient(base); - - let tableNameLengthLimit = 255; - const sqlClientType = sqlClient.knex.clientType(); - if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') { - tableNameLengthLimit = 64; - } else if (sqlClientType === 'pg') { - tableNameLengthLimit = 63; - } else if (sqlClientType === 'mssql') { - tableNameLengthLimit = 128; - } - - if (req.body.table_name.length > tableNameLengthLimit) { - NcError.badRequest(`Table name exceeds ${tableNameLengthLimit} characters`); - } - - await Model.updateAliasAndTableName( - req.params.tableId, - req.body.title, - req.body.table_name - ); - - await sqlMgr.sqlOpPlus(base, 'tableRename', { - ...req.body, - tn: req.body.table_name, - tn_old: model.table_name, - }); - - Tele.emit('evt', { evt_type: 'table:updated' }); - - res.json({ msg: 'success' }); -} - -export async function tableDelete(req: Request, res: Response) { - const table = await Model.getByIdOrName({ id: req.params.tableId }); - await table.getColumns(); - - const relationColumns = table.columns.filter( - (c) => c.uidt === UITypes.LinkToAnotherRecord - ); - - if (relationColumns?.length) { - const referredTables = await Promise.all( - relationColumns.map(async (c) => - c - .getColOptions() - .then((opt) => opt.getRelatedTable()) - .then() - ) - ); - NcError.badRequest( - `Table can't be deleted since Table is being referred in following tables : ${referredTables.join( - ', ' - )}. Delete LinkToAnotherRecord columns and try again.` - ); - } - - const project = await Project.getWithInfo(table.project_id); - const base = project.bases.find((b) => b.id === table.base_id); - const sqlMgr = await ProjectMgrv2.getSqlMgr(project); - (table as any).tn = table.table_name; - table.columns = table.columns.filter((c) => !isVirtualCol(c)); - table.columns.forEach((c) => { - (c as any).cn = c.column_name; - }); - - if (table.type === ModelTypes.TABLE) { - await sqlMgr.sqlOpPlus(base, 'tableDelete', table); - } else if (table.type === ModelTypes.VIEW) { - await sqlMgr.sqlOpPlus(base, 'viewDelete', { - ...table, - view_name: table.table_name, - }); - } - - await Audit.insert({ - project_id: project.id, - base_id: base.id, - op_type: AuditOperationTypes.TABLE, - op_sub_type: AuditOperationSubTypes.DELETED, - user: (req as any)?.user?.email, - description: `Deleted ${table.type} ${table.table_name} with alias ${table.title} `, - ip: (req as any).clientIp, - }).then(() => {}); - - Tele.emit('evt', { evt_type: 'table:deleted' }); - - res.json(await table.delete()); -} - -const router = Router({ mergeParams: true }); -router.get( - '/api/v1/db/meta/projects/:projectId/tables', - metaApiMetrics, - ncMetaAclMw(tableList, 'tableList') -); -router.get( - '/api/v1/db/meta/projects/:projectId/:baseId/tables', - metaApiMetrics, - ncMetaAclMw(tableList, 'tableList') -); -router.post( - '/api/v1/db/meta/projects/:projectId/tables', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/TableReq'), - ncMetaAclMw(tableCreate, 'tableCreate') -); -router.post( - '/api/v1/db/meta/projects/:projectId/:baseId/tables', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/TableReq'), - ncMetaAclMw(tableCreate, 'tableCreate') -); -router.get( - '/api/v1/db/meta/tables/:tableId', - metaApiMetrics, - ncMetaAclMw(tableGet, 'tableGet') -); -router.patch( - '/api/v1/db/meta/tables/:tableId', - metaApiMetrics, - ncMetaAclMw(tableUpdate, 'tableUpdate') -); -router.delete( - '/api/v1/db/meta/tables/:tableId', - metaApiMetrics, - ncMetaAclMw(tableDelete, 'tableDelete') -); -router.post( - '/api/v1/db/meta/tables/:tableId/reorder', - metaApiMetrics, - ncMetaAclMw(tableReorder, 'tableReorder') -); -export default router; diff --git a/packages/nocodb/src/lib/meta/api/userApi/index.ts b/packages/nocodb/src/lib/meta/api/userApi/index.ts deleted file mode 100644 index c4a5003430..0000000000 --- a/packages/nocodb/src/lib/meta/api/userApi/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './userApis'; diff --git a/packages/nocodb/src/lib/meta/api/viewApis.ts b/packages/nocodb/src/lib/meta/api/viewApis.ts deleted file mode 100644 index 6078e43fae..0000000000 --- a/packages/nocodb/src/lib/meta/api/viewApis.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { Request, Response, Router } from 'express'; -// @ts-ignore -import Model from '../../models/Model'; -import { Tele } from 'nc-help'; -// @ts-ignore -import { PagedResponseImpl } from '../helpers/PagedResponse'; -// @ts-ignore -import { Table, TableReq, ViewList } from 'nocodb-sdk'; -// @ts-ignore -import ProjectMgrv2 from '../../db/sql-mgr/v2/ProjectMgrv2'; -// @ts-ignore -import Project from '../../models/Project'; -import View from '../../models/View'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { xcVisibilityMetaGet } from './modelVisibilityApis'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import { getAjvValidatorMw } from './helpers'; -// @ts-ignore -export async function viewGet(req: Request, res: Response) {} - -// @ts-ignore -export async function viewList( - req: Request, - res: Response -) { - const model = await Model.get(req.params.tableId); - - const viewList = await xcVisibilityMetaGet( - // req.params.projectId, - // req.params.baseId, - model.project_id, - [model] - ); - - //await View.list(req.params.tableId) - const filteredViewList = viewList.filter((view: any) => { - return Object.keys((req as any).session?.passport?.user?.roles).some( - (role) => - (req as any)?.session?.passport?.user?.roles[role] && - !view.disabled[role] - ); - }); - - res.json(new PagedResponseImpl(filteredViewList)); -} - -// @ts-ignore -export async function shareView( - req: Request, - res: Response -) { - Tele.emit('evt', { evt_type: 'sharedView:generated-link' }); - res.json(await View.share(req.params.viewId)); -} - -// @ts-ignore -export async function viewCreate(req: Request, res, next) {} - -// @ts-ignore -export async function viewUpdate(req, res) { - const result = await View.update(req.params.viewId, req.body); - Tele.emit('evt', { evt_type: 'vtable:updated', show_as: result.type }); - res.json(result); -} - -// @ts-ignore -export async function viewDelete(req: Request, res: Response, next) { - const result = await View.delete(req.params.viewId); - Tele.emit('evt', { evt_type: 'vtable:deleted' }); - res.json(result); -} - -async function shareViewUpdate(req: Request, res) { - Tele.emit('evt', { evt_type: 'sharedView:updated' }); - res.json(await View.update(req.params.viewId, req.body)); -} - -async function shareViewDelete(req: Request, res) { - Tele.emit('evt', { evt_type: 'sharedView:deleted' }); - res.json(await View.sharedViewDelete(req.params.viewId)); -} - -async function showAllColumns(req: Request, res) { - res.json( - await View.showAllColumns( - req.params.viewId, - (req.query?.ignoreIds || []) - ) - ); -} - -async function hideAllColumns(req: Request, res) { - res.json( - await View.hideAllColumns( - req.params.viewId, - (req.query?.ignoreIds || []) - ) - ); -} - -async function shareViewList(req: Request, res) { - res.json(await View.shareViewList(req.params.tableId)); -} - -const router = Router({ mergeParams: true }); -router.get( - '/api/v1/db/meta/tables/:tableId/views', - metaApiMetrics, - ncMetaAclMw(viewList, 'viewList') -); -router.patch( - '/api/v1/db/meta/views/:viewId', - getAjvValidatorMw('swagger.json#/components/schemas/ViewReq'), - metaApiMetrics, - ncMetaAclMw(viewUpdate, 'viewUpdate') -); -router.delete( - '/api/v1/db/meta/views/:viewId', - metaApiMetrics, - ncMetaAclMw(viewDelete, 'viewDelete') -); -router.post( - '/api/v1/db/meta/views/:viewId/show-all', - metaApiMetrics, - ncMetaAclMw(showAllColumns, 'showAllColumns') -); -router.post( - '/api/v1/db/meta/views/:viewId/hide-all', - metaApiMetrics, - ncMetaAclMw(hideAllColumns, 'hideAllColumns') -); - -router.get( - '/api/v1/db/meta/tables/:tableId/share', - metaApiMetrics, - ncMetaAclMw(shareViewList, 'shareViewList') -); -router.post( - '/api/v1/db/meta/views/:viewId/share', - ncMetaAclMw(shareView, 'shareView') -); -router.patch( - '/api/v1/db/meta/views/:viewId/share', - getAjvValidatorMw('swagger.json#/components/schemas/SharedViewReq'), - metaApiMetrics, - ncMetaAclMw(shareViewUpdate, 'shareViewUpdate') -); -router.delete( - '/api/v1/db/meta/views/:viewId/share', - metaApiMetrics, - ncMetaAclMw(shareViewDelete, 'shareViewDelete') -); - -export default router; diff --git a/packages/nocodb/src/lib/meta/helpers/apiMetrics.ts b/packages/nocodb/src/lib/meta/helpers/apiMetrics.ts index 82c6a0c32b..e276c99ef9 100644 --- a/packages/nocodb/src/lib/meta/helpers/apiMetrics.ts +++ b/packages/nocodb/src/lib/meta/helpers/apiMetrics.ts @@ -1,5 +1,5 @@ import { Request } from 'express'; -import { Tele } from 'nc-help'; +import { T } from 'nc-help'; const countMap = {}; @@ -9,7 +9,7 @@ const metrics = async (req: Request, c = 150) => { const event = `a:api:${req.route.path}:${req.method}`; countMap[event] = (countMap[event] || 0) + 1; if (countMap[event] >= c) { - Tele.event({ event }); + T.event({ event }); countMap[event] = 0; } }; diff --git a/packages/nocodb/src/lib/meta/helpers/catchError.ts b/packages/nocodb/src/lib/meta/helpers/catchError.ts index 1c3393ed0d..4c6da5d46d 100644 --- a/packages/nocodb/src/lib/meta/helpers/catchError.ts +++ b/packages/nocodb/src/lib/meta/helpers/catchError.ts @@ -1,3 +1,5 @@ +import { ErrorObject } from 'ajv'; + enum DBError { TABLE_EXIST = 'TABLE_EXIST', TABLE_NOT_EXIST = 'TABLE_NOT_EXIST', @@ -5,6 +7,7 @@ enum DBError { COLUMN_NOT_EXIST = 'COLUMN_NOT_EXIST', CONSTRAINT_EXIST = 'CONSTRAINT_EXIST', CONSTRAINT_NOT_EXIST = 'CONSTRAINT_NOT_EXIST', + COLUMN_NOT_NULL = 'COLUMN_NOT_NULL', } // extract db errors using database error code @@ -20,6 +23,7 @@ function extractDBError(error): { let extra: Record; let type: DBError; + // todo: handle not null constraint error for all databases switch (error.code) { // sqlite errors case 'SQLITE_BUSY': @@ -172,6 +176,19 @@ function extractDBError(error): { break; case 'ER_BAD_NULL_ERROR': message = 'A null value is not allowed for this field.'; + { + const extractColNameMatch = error.message.match( + /Column '(\w+)' cannot be null/i + ); + if (extractColNameMatch && extractColNameMatch[1]) { + message = `The column '${extractColNameMatch[1]}' cannot be null.`; + type = DBError.COLUMN_NOT_NULL; + extra = { + column: extractColNameMatch[1], + }; + } + } + break; case 'ER_DATA_TOO_LONG': message = 'The data entered is too long for this field.'; @@ -394,6 +411,8 @@ export default function ( return res.status(500).json({ msg: e.message }); } else if (e instanceof NotImplemented) { return res.status(501).json({ msg: e.message }); + } else if (e instanceof AjvError) { + return res.status(400).json({ msg: e.message, errors: e.errors }); } next(e); } @@ -412,6 +431,15 @@ class InternalServerError extends Error {} class NotImplemented extends Error {} +class AjvError extends Error { + constructor(param: { message: string; errors: ErrorObject[] }) { + super(param.message); + this.errors = param.errors; + } + + errors: ErrorObject[]; +} + export class NcError { static notFound(message = 'Not found') { throw new NotFound(message); @@ -436,4 +464,8 @@ export class NcError { static notImplemented(message = 'Not implemented') { throw new NotImplemented(message); } + + static ajvValidationError(param: { message: string; errors: ErrorObject[] }) { + throw new AjvError(param); + } } diff --git a/packages/nocodb/src/lib/meta/helpers/getHandler.ts b/packages/nocodb/src/lib/meta/helpers/getHandler.ts index 1f40f45b2f..07c57a08eb 100644 --- a/packages/nocodb/src/lib/meta/helpers/getHandler.ts +++ b/packages/nocodb/src/lib/meta/helpers/getHandler.ts @@ -12,3 +12,20 @@ export default function getHandler( return eeHandler(...args); }; } + +export function getConditionalHandler< + T extends (...args: any[]) => any, + U extends (...args: any[]) => any +>( + defaultHandler: T, + eeHandler: U +): ( + ...args: Parameters | Parameters +) => Promise | ReturnType> { + return async (...args: Parameters | Parameters) => { + if (Noco.isEE()) { + return defaultHandler(...args); + } + return eeHandler(...args); + }; +} diff --git a/packages/nocodb/src/lib/models/FormView.ts b/packages/nocodb/src/lib/models/FormView.ts index 8ec5047d5d..9dbe572f88 100644 --- a/packages/nocodb/src/lib/models/FormView.ts +++ b/packages/nocodb/src/lib/models/FormView.ts @@ -1,6 +1,6 @@ import Noco from '../Noco'; import { CacheGetType, CacheScope, MetaTable } from '../utils/globals'; -import { FormType } from 'nocodb-sdk'; +import { BoolType, FormType } from 'nocodb-sdk'; import { deserializeJSON, serializeJSON } from '../utils/serialize'; import FormViewColumn from './FormViewColumn'; import View from './View'; @@ -8,6 +8,9 @@ import NocoCache from '../cache/NocoCache'; import { extractProps } from '../meta/helpers/extractProps'; export default class FormView implements FormType { + show: BoolType; + is_default: BoolType; + order: number; title?: string; heading?: string; subheading?: string; @@ -17,8 +20,8 @@ export default class FormView implements FormType { email?: string; banner_image_url?: string; logo_url?: string; - submit_another_form?: boolean; - show_blank_form?: boolean; + submit_another_form?: BoolType; + show_blank_form?: BoolType; fk_view_id: string; columns?: FormViewColumn[]; diff --git a/packages/nocodb/src/lib/models/GalleryView.ts b/packages/nocodb/src/lib/models/GalleryView.ts index 2cf2f6ce8f..8f530d2154 100644 --- a/packages/nocodb/src/lib/models/GalleryView.ts +++ b/packages/nocodb/src/lib/models/GalleryView.ts @@ -1,24 +1,24 @@ import Noco from '../Noco'; import { CacheGetType, CacheScope, MetaTable } from '../utils/globals'; -import { GalleryColumnType, GalleryType, UITypes } from 'nocodb-sdk'; +import { BoolType, GalleryColumnType, GalleryType, UITypes } from 'nocodb-sdk'; import View from './View'; import NocoCache from '../cache/NocoCache'; import { extractProps } from '../meta/helpers/extractProps'; export default class GalleryView implements GalleryType { fk_view_id?: string; - deleted?: boolean; + deleted?: BoolType; order?: number; - next_enabled?: boolean; - prev_enabled?: boolean; + next_enabled?: BoolType; + prev_enabled?: BoolType; cover_image_idx?: number; cover_image?: string; restrict_types?: string; restrict_size?: string; restrict_number?: string; - public?: boolean; + public?: BoolType; password?: string; - show_all_fields?: boolean; + show_all_fields?: BoolType; fk_cover_image_col_id?: string; project_id?: string; diff --git a/packages/nocodb/src/lib/models/Hook.ts b/packages/nocodb/src/lib/models/Hook.ts index 031521926e..bf1c64611b 100644 --- a/packages/nocodb/src/lib/models/Hook.ts +++ b/packages/nocodb/src/lib/models/Hook.ts @@ -1,4 +1,4 @@ -import { HookType } from 'nocodb-sdk'; +import { BoolType, HookReqType, HookType } from 'nocodb-sdk'; import { CacheDelDirection, CacheGetType, @@ -21,21 +21,21 @@ export default class Hook implements HookType { type?: string; event?: 'after' | 'before'; operation?: 'insert' | 'delete' | 'update'; - async?: boolean; + async?: BoolType; payload?: string; url?: string; headers?: string; - condition?: boolean; + condition?: BoolType; notification?: string | object; retries?: number; retry_interval?: number; timeout?: number; - active?: boolean; + active?: BoolType; project_id?: string; base_id?: string; - constructor(hook: Partial) { + constructor(hook: Partial) { Object.assign(this, hook); } diff --git a/packages/nocodb/src/lib/models/KanbanView.ts b/packages/nocodb/src/lib/models/KanbanView.ts index 6ac1b65d09..9bff3fbed4 100644 --- a/packages/nocodb/src/lib/models/KanbanView.ts +++ b/packages/nocodb/src/lib/models/KanbanView.ts @@ -1,5 +1,5 @@ import Noco from '../Noco'; -import { KanbanType, UITypes } from 'nocodb-sdk'; +import { BoolType, KanbanType, UITypes } from 'nocodb-sdk'; import { CacheGetType, CacheScope, MetaTable } from '../utils/globals'; import View from './View'; import NocoCache from '../cache/NocoCache'; @@ -16,12 +16,12 @@ export default class KanbanView implements KanbanType { // below fields are not in use at this moment // keep them for time being - show?: boolean; + show?: BoolType; order?: number; uuid?: string; - public?: boolean; + public?: BoolType; password?: string; - show_all_fields?: boolean; + show_all_fields?: BoolType; constructor(data: KanbanView) { Object.assign(this, data); diff --git a/packages/nocodb/src/lib/models/Model.ts b/packages/nocodb/src/lib/models/Model.ts index 5fdf81219c..b90517b84c 100644 --- a/packages/nocodb/src/lib/models/Model.ts +++ b/packages/nocodb/src/lib/models/Model.ts @@ -27,7 +27,6 @@ import { extractProps } from '../meta/helpers/extractProps'; export default class Model implements TableType { copy_enabled: BoolType; - created_at: Date | number | string; base_id: 'db' | string; deleted: BoolType; enabled: BoolType; @@ -42,7 +41,6 @@ export default class Model implements TableType { show_all_fields: boolean; tags: string; type: ModelTypes; - updated_at: Date | number | string; table_name: string; title: string; diff --git a/packages/nocodb/src/lib/models/ProjectUser.ts b/packages/nocodb/src/lib/models/ProjectUser.ts index 8b25ff6af1..14e208cb60 100644 --- a/packages/nocodb/src/lib/models/ProjectUser.ts +++ b/packages/nocodb/src/lib/models/ProjectUser.ts @@ -217,7 +217,7 @@ export default class ProjectUser { static async getProjectsList( userId: string, - _params: any, + _params?: any, ncMeta = Noco.ncMeta ): Promise { // todo: pagination diff --git a/packages/nocodb/src/lib/models/View.ts b/packages/nocodb/src/lib/models/View.ts index 41e17d6d67..e453da37ea 100644 --- a/packages/nocodb/src/lib/models/View.ts +++ b/packages/nocodb/src/lib/models/View.ts @@ -15,6 +15,7 @@ import GridViewColumn from './GridViewColumn'; import Sort from './Sort'; import Filter from './Filter'; import { + BoolType, ColumnReqType, isSystemColumn, UITypes, @@ -942,7 +943,7 @@ export default class View implements ViewType { body: { title?: string; order?: number; - show_system_fields?: boolean; + show_system_fields?: BoolType; lock_type?: string; password?: string; uuid?: string; diff --git a/packages/nocodb/src/lib/models/index.ts b/packages/nocodb/src/lib/models/index.ts new file mode 100644 index 0000000000..534c8ce025 --- /dev/null +++ b/packages/nocodb/src/lib/models/index.ts @@ -0,0 +1,36 @@ +export { default as ApiToken } from './ApiToken'; +export { default as Audit } from './Audit'; +export { default as BarcodeColumn } from './BarcodeColumn'; +export { default as Base } from './Base'; +export { default as Column } from './Column'; +export { default as Filter } from './Filter'; +export { default as FormulaColumn } from './FormulaColumn'; +export { default as FormView } from './FormView'; +export { default as FormViewColumn } from './FormViewColumn'; +export { default as GalleryView } from './GalleryView'; +export { default as GalleryViewColumn } from './GalleryViewColumn'; +export { default as GridView } from './GridView'; +export { default as GridViewColumn } from './GridViewColumn'; +export { default as Hook } from './Hook'; +export { default as HookFilter } from './HookFilter'; +export { default as HookLog } from './HookLog'; +export { default as KanbanView } from './KanbanView'; +export { default as KanbanViewColumn } from './KanbanViewColumn'; +export { default as LinkToAnotherRecordColumn } from './LinkToAnotherRecordColumn'; +export { default as LookupColumn } from './LookupColumn'; +export { default as MapView } from './MapView'; +export { default as MapViewColumn } from './MapViewColumn'; +export { default as Model } from './Model'; +export { default as ModelRoleVisibility } from './ModelRoleVisibility'; +export { default as Plugin } from './Plugin'; +export { default as Project } from './Project'; +export { default as ProjectUser } from './ProjectUser'; +export { default as QrCodeColumn } from './QrCodeColumn'; +export { default as RollupColumn } from './RollupColumn'; +export { default as SelectOption } from './SelectOption'; +export { default as Sort } from './Sort'; +export { default as Store } from './Store'; +export { default as SyncLogs } from './SyncLogs'; +export { default as SyncSource } from './SyncSource'; +export { default as User } from './User'; +export { default as View } from './View'; diff --git a/packages/nocodb/src/lib/services/apiTokenService.ts b/packages/nocodb/src/lib/services/apiTokenService.ts new file mode 100644 index 0000000000..329bcb305c --- /dev/null +++ b/packages/nocodb/src/lib/services/apiTokenService.ts @@ -0,0 +1,36 @@ +import { ApiTokenReqType, OrgUserRoles } from 'nocodb-sdk'; +import { T } from 'nc-help'; +import { validatePayload } from '../meta/api/helpers'; +import { NcError } from '../meta/helpers/catchError'; +import ApiToken from '../models/ApiToken'; +import User from '../models/User'; + +export async function apiTokenList(param: { userId: string }) { + return ApiToken.list(param.userId); +} +export async function apiTokenCreate(param: { + userId: string; + tokenBody: ApiTokenReqType; +}) { + await validatePayload( + 'swagger.json#/components/schemas/ApiTokenReq', + param.tokenBody + ); + + T.emit('evt', { evt_type: 'apiToken:created' }); + return ApiToken.insert({ ...param.tokenBody, fk_user_id: param.userId }); +} + +export async function apiTokenDelete(param: { token; user: User }) { + const apiToken = await ApiToken.getByToken(param.token); + if ( + !param.user.roles.includes(OrgUserRoles.SUPER_ADMIN) && + apiToken.fk_user_id !== param.user.id + ) { + NcError.notFound('Token not found'); + } + T.emit('evt', { evt_type: 'apiToken:deleted' }); + + // todo: verify token belongs to the user + return await ApiToken.delete(param.token); +} diff --git a/packages/nocodb/src/lib/services/attachmentService.ts b/packages/nocodb/src/lib/services/attachmentService.ts new file mode 100644 index 0000000000..d65134e75b --- /dev/null +++ b/packages/nocodb/src/lib/services/attachmentService.ts @@ -0,0 +1,116 @@ +import { nanoid } from 'nanoid'; +import path from 'path'; +import slash from 'slash'; +import mimetypes, { mimeIcons } from '../utils/mimeTypes'; +import { T } from 'nc-help'; +import NcPluginMgrv2 from '../meta/helpers/NcPluginMgrv2'; +import Local from '../v1-legacy/plugins/adapters/storage/Local'; + +export async function upload(param: { + path?: string; + // todo: proper type + files: unknown[]; +}) { + const filePath = sanitizeUrlPath(param.path?.toString()?.split('/') || ['']); + const destPath = path.join('nc', 'uploads', ...filePath); + + const storageAdapter = await NcPluginMgrv2.storageAdapter(); + + const attachments = await Promise.all( + param.files?.map(async (file: any) => { + const fileName = `${nanoid(18)}${path.extname(file.originalname)}`; + + const url = await storageAdapter.fileCreate( + slash(path.join(destPath, fileName)), + file + ); + + let attachmentPath; + + // if `url` is null, then it is local attachment + if (!url) { + // then store the attachement path only + // url will be constructued in `useAttachmentCell` + attachmentPath = `download/${filePath.join('/')}/${fileName}`; + } + + return { + ...(url ? { url } : {}), + ...(attachmentPath ? { path: attachmentPath } : {}), + title: file.originalname, + mimetype: file.mimetype, + size: file.size, + icon: mimeIcons[path.extname(file.originalname).slice(1)] || undefined, + }; + }) + ); + + T.emit('evt', { evt_type: 'image:uploaded' }); + + return attachments; +} + +export async function uploadViaURL(param: { + path?: string; + urls: { + url: string; + fileName: string; + mimetype?: string; + size?: string | number; + }[]; +}) { + const filePath = sanitizeUrlPath(param?.path?.toString()?.split('/') || ['']); + const destPath = path.join('nc', 'uploads', ...filePath); + + const storageAdapter = await NcPluginMgrv2.storageAdapter(); + + const attachments = await Promise.all( + param.urls?.map?.(async (urlMeta) => { + const { url, fileName: _fileName } = urlMeta; + + const fileName = `${nanoid(18)}${_fileName || url.split('/').pop()}`; + + const attachmentUrl = await (storageAdapter as any).fileCreateByUrl( + slash(path.join(destPath, fileName)), + url + ); + + let attachmentPath; + + // if `attachmentUrl` is null, then it is local attachment + if (!attachmentUrl) { + // then store the attachement path only + // url will be constructued in `useAttachmentCell` + attachmentPath = `download/${filePath.join('/')}/${fileName}`; + } + + return { + ...(attachmentUrl ? { url: attachmentUrl } : {}), + ...(attachmentPath ? { path: attachmentPath } : {}), + title: fileName, + mimetype: urlMeta.mimetype, + size: urlMeta.size, + icon: mimeIcons[path.extname(fileName).slice(1)] || undefined, + }; + }) + ); + + T.emit('evt', { evt_type: 'image:uploaded' }); + + return attachments; +} + +export async function fileRead(param: { path: string }) { + // get the local storage adapter to display local attachments + const storageAdapter = new Local(); + const type = + mimetypes[path.extname(param.path).split('/').pop().slice(1)] || + 'text/plain'; + + const img = await storageAdapter.fileRead(slash(param.path)); + return { img, type }; +} + +export function sanitizeUrlPath(paths) { + return paths.map((url) => url.replace(/[/.?#]+/g, '_')); +} diff --git a/packages/nocodb/src/lib/services/auditService.ts b/packages/nocodb/src/lib/services/auditService.ts new file mode 100644 index 0000000000..886934cc84 --- /dev/null +++ b/packages/nocodb/src/lib/services/auditService.ts @@ -0,0 +1,78 @@ +import { validatePayload } from '../meta/api/helpers'; +import Audit from '../models/Audit'; +import { + AuditOperationSubTypes, + AuditOperationTypes, + AuditRowUpdateReqType, +} from 'nocodb-sdk'; +import Model from '../models/Model'; +import { PagedResponseImpl } from '../meta/helpers/PagedResponse'; + +import DOMPurify from 'isomorphic-dompurify'; + +export async function commentRow(param: { + rowId: string; + body: AuditRowUpdateReqType; + user: any; +}) { + await validatePayload( + 'swagger.json#/components/schemas/CommentReq', + param.body + ); + + return await Audit.insert({ + ...param.body, + user: param.user?.email, + op_type: AuditOperationTypes.COMMENT, + }); +} + +export async function auditRowUpdate(param: { + rowId: string; + body: AuditRowUpdateReqType; +}) { + await validatePayload( + 'swagger.json#/components/schemas/AuditRowUpdateReq', + param.body + ); + + const model = await Model.getByIdOrName({ id: param.body.fk_model_id }); + return await Audit.insert({ + fk_model_id: param.body.fk_model_id, + row_id: param.rowId, + op_type: AuditOperationTypes.DATA, + op_sub_type: AuditOperationSubTypes.UPDATE, + description: DOMPurify.sanitize( + `Table ${model.table_name} : field ${param.body.column_name} got changed from ${param.body.prev_value} to ${param.body.value}` + ), + details: DOMPurify.sanitize(`${param.body.column_name} + : ${param.body.prev_value} + ${param.body.value}`), + ip: (param as any).clientIp, + user: (param as any).user?.email, + }); +} + +export async function commentList(param: { query: any }) { + return await Audit.commentsList(param.query); +} + +export async function auditList(param: { query: any; projectId: string }) { + return new PagedResponseImpl( + await Audit.projectAuditList(param.projectId, param.query), + { + count: await Audit.projectAuditCount(param.projectId), + ...param.query, + } + ); +} + +export async function commentsCount(param: { + fk_model_id: string; + ids: string[]; +}) { + return await Audit.commentsCount({ + fk_model_id: param.fk_model_id as string, + ids: param.ids as string[], + }); +} diff --git a/packages/nocodb/src/lib/services/baseService.ts b/packages/nocodb/src/lib/services/baseService.ts new file mode 100644 index 0000000000..21adb99ac8 --- /dev/null +++ b/packages/nocodb/src/lib/services/baseService.ts @@ -0,0 +1,82 @@ +import Project from '../models/Project'; +import { BaseReqType } from 'nocodb-sdk'; +import { syncBaseMigration } from '../meta/helpers/syncMigration'; +import Base from '../models/Base'; +import { T } from 'nc-help'; +import { populateMeta, validatePayload } from '../meta/api/helpers'; + +export async function baseGetWithConfig(param: { baseId: any }) { + const base = await Base.get(param.baseId); + + base.config = base.getConnectionConfig(); + + return base; +} + +export async function baseUpdate(param: { + baseId: string; + base: BaseReqType; + projectId: string; +}) { + validatePayload('swagger.json#/components/schemas/BaseReq', param.base); + + const baseBody = param.base; + const project = await Project.getWithInfo(param.projectId); + const base = await Base.updateBase(param.baseId, { + ...baseBody, + type: baseBody.config?.client, + projectId: project.id, + id: param.baseId, + }); + + delete base.config; + + T.emit('evt', { + evt_type: 'base:updated', + }); + + return base; +} + +export async function baseList(param: { projectId: string }) { + const bases = await Base.list({ projectId: param.projectId }); + + return bases; +} + +export async function baseDelete(param: { baseId: string }) { + const base = await Base.get(param.baseId); + await base.delete(); + T.emit('evt', { evt_type: 'base:deleted' }); + return true; +} + +export async function baseCreate(param: { + projectId: string; + base: BaseReqType; +}) { + validatePayload('swagger.json#/components/schemas/BaseReq', param.base); + + // type | base | projectId + const baseBody = param.base; + const project = await Project.getWithInfo(param.projectId); + const base = await Base.createBase({ + ...baseBody, + type: baseBody.config?.client, + projectId: project.id, + }); + + await syncBaseMigration(project, base); + + const info = await populateMeta(base, project); + + T.emit('evt_api_created', info); + + delete base.config; + + T.emit('evt', { + evt_type: 'base:created', + }); + + return base; +} diff --git a/packages/nocodb/src/lib/services/cacheService.ts b/packages/nocodb/src/lib/services/cacheService.ts new file mode 100644 index 0000000000..968da7c33f --- /dev/null +++ b/packages/nocodb/src/lib/services/cacheService.ts @@ -0,0 +1,10 @@ +import NocoCache from '../cache/NocoCache'; + +export async function cacheGet() { + return await NocoCache.export(); +} + +export async function cacheDelete() { + await NocoCache.destroy(); + return true; +} diff --git a/packages/nocodb/src/lib/meta/api/columnApis.ts b/packages/nocodb/src/lib/services/columnService.ts similarity index 76% rename from packages/nocodb/src/lib/meta/api/columnApis.ts rename to packages/nocodb/src/lib/services/columnService.ts index 593438f6d7..1609f02fe8 100644 --- a/packages/nocodb/src/lib/meta/api/columnApis.ts +++ b/packages/nocodb/src/lib/services/columnService.ts @@ -1,16 +1,3 @@ -import { Request, Response, Router } from 'express'; -import Model from '../../models/Model'; -import ProjectMgrv2 from '../../db/sql-mgr/v2/ProjectMgrv2'; -import Base from '../../models/Base'; -import Column from '../../models/Column'; -import { Tele } from 'nc-help'; -import validateParams from '../helpers/validateParams'; - -import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn'; -import { - getUniqueColumnAliasName, - getUniqueColumnName, -} from '../helpers/getUniqueName'; import { AuditOperationSubTypes, AuditOperationTypes, @@ -21,32 +8,42 @@ import { RelationTypes, substituteColumnAliasWithIdInFormula, substituteColumnIdWithAliasInFormula, - TableType, UITypes, } from 'nocodb-sdk'; -import Audit from '../../models/Audit'; -import SqlMgrv2 from '../../db/sql-mgr/v2/SqlMgrv2'; -import Noco from '../../Noco'; -import NcMetaIO from '../NcMetaIO'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import { NcError } from '../helpers/catchError'; -import getColumnPropsFromUIDT from '../helpers/getColumnPropsFromUIDT'; -import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue'; -import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; -import { metaApiMetrics } from '../helpers/apiMetrics'; -import FormulaColumn from '../../models/FormulaColumn'; -import KanbanView from '../../models/KanbanView'; -import { MetaTable } from '../../utils/globals'; -import formulaQueryBuilderv2 from '../../db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2'; +import formulaQueryBuilderv2 from '../db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2'; +import ProjectMgrv2 from '../db/sql-mgr/v2/ProjectMgrv2'; +import SqlMgrv2 from '../db/sql-mgr/v2/SqlMgrv2'; import { createHmAndBtColumn, generateFkName, - getAjvValidatorMw, randomID, validateLookupPayload, + validatePayload, validateRequiredField, validateRollupPayload, -} from './helpers'; +} from '../meta/api/helpers'; +import { NcError } from '../meta/helpers/catchError'; +import getColumnPropsFromUIDT from '../meta/helpers/getColumnPropsFromUIDT'; +import { + getUniqueColumnAliasName, + getUniqueColumnName, +} from '../meta/helpers/getUniqueName'; +import mapDefaultDisplayValue from '../meta/helpers/mapDefaultDisplayValue'; +import validateParams from '../meta/helpers/validateParams'; +import NcMetaIO from '../meta/NcMetaIO'; +import Audit from '../models/Audit'; +import Base from '../models/Base'; +import Column from '../models/Column'; +import FormulaColumn from '../models/FormulaColumn'; +import KanbanView from '../models/KanbanView'; +import LinkToAnotherRecordColumn from '../models/LinkToAnotherRecordColumn'; +import Model from '../models/Model'; +import Project from '../models/Project'; +import Noco from '../Noco'; +import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; + +import { T } from 'nc-help'; +import { MetaTable } from '../utils/globals'; export enum Altered { NEW_COLUMN = 1, @@ -54,1369 +51,1076 @@ export enum Altered { UPDATE_COLUMN = 8, } -export async function columnGet(req: Request, res: Response) { - res.json(await Column.get({ colId: req.params.columnId })); -} +export async function columnUpdate(param: { + req?: any; + columnId: string; + column: ColumnReqType & { colOptions?: any }; + cookie?: any; +}) { + const { cookie } = param; + const column = await Column.get({ colId: param.columnId }); -export async function columnAdd( - req: Request, - res: Response -) { const table = await Model.getWithInfo({ - id: req.params.tableId, + id: column.fk_model_id, }); const base = await Base.get(table.base_id); - const project = await base.getProject(); - - if (req.body.title || req.body.column_name) { - const dbDriver = NcConnectionMgrv2.get(base); + const sqlClient = await NcConnectionMgrv2.getSqlClient(base); - const sqlClientType = dbDriver.clientType(); + const sqlClientType = sqlClient.knex.clientType(); - const mxColumnLength = Column.getMaxColumnNameLength(sqlClientType); + const mxColumnLength = Column.getMaxColumnNameLength(sqlClientType); - if ((req.body.title || req.body.column_name).length > mxColumnLength) { - NcError.badRequest( - `Column name ${ - req.body.title || req.body.column_name - } exceeds ${mxColumnLength} characters` - ); - } + if (param.column.column_name.length > mxColumnLength) { + NcError.badRequest( + `Column name ${param.column.column_name} exceeds ${mxColumnLength} characters` + ); } if ( - !isVirtualCol(req.body) && + !isVirtualCol(param.column) && !(await Column.checkTitleAvailable({ - column_name: req.body.column_name, - fk_model_id: req.params.tableId, + column_name: param.column.column_name, + fk_model_id: column.fk_model_id, + exclude_id: param.columnId, })) ) { NcError.badRequest('Duplicate column name'); } if ( !(await Column.checkAliasAvailable({ - title: req.body.title || req.body.column_name, - fk_model_id: req.params.tableId, + title: param.column.title, + fk_model_id: column.fk_model_id, + exclude_id: param.columnId, })) ) { NcError.badRequest('Duplicate column alias'); } - let colBody: any = req.body; - switch (colBody.uidt) { - case UITypes.Rollup: - { - await validateRollupPayload(req.body); - - await Column.insert({ + let colBody = { ...param.column } as Column & { + formula?: string; + formula_raw?: string; + }; + if ( + [ + UITypes.Lookup, + UITypes.Rollup, + UITypes.LinkToAnotherRecord, + UITypes.Formula, + UITypes.QrCode, + UITypes.Barcode, + UITypes.ForeignKey, + ].includes(column.uidt) + ) { + if (column.uidt === colBody.uidt) { + if ([UITypes.QrCode, UITypes.Barcode].includes(column.uidt)) { + await Column.update(column.id, { + ...column, ...colBody, - fk_model_id: table.id, - }); - } - break; - case UITypes.Lookup: - { - await validateLookupPayload(req.body); + } as Column); + } else if (column.uidt === UITypes.Formula) { + colBody.formula = await substituteColumnAliasWithIdInFormula( + colBody.formula_raw || colBody.formula, + table.columns + ); - await Column.insert({ + try { + // test the query to see if it is valid in db level + const dbDriver = NcConnectionMgrv2.get(base); + await formulaQueryBuilderv2(colBody.formula, null, dbDriver, table); + } catch (e) { + console.error(e); + NcError.badRequest('Invalid Formula'); + } + + await Column.update(column.id, { + // title: colBody.title, + ...column, ...colBody, - fk_model_id: table.id, + }); + } else if (colBody.title !== column.title) { + await Column.updateAlias(param.columnId, { + title: colBody.title, }); } - break; + await updateRollupOrLookup(colBody, column); + } else { + NcError.notImplemented( + `Updating ${colBody.uidt} => ${colBody.uidt} is not implemented` + ); + } + } else if ( + [ + UITypes.Lookup, + UITypes.Rollup, + UITypes.LinkToAnotherRecord, + UITypes.Formula, + UITypes.QrCode, + UITypes.Barcode, + UITypes.ForeignKey, + ].includes(colBody.uidt) + ) { + NcError.notImplemented( + `Updating ${colBody.uidt} => ${colBody.uidt} is not implemented` + ); + } else if ( + [UITypes.SingleSelect, UITypes.MultiSelect].includes(colBody.uidt) + ) { + colBody = getColumnPropsFromUIDT(colBody, base); - case UITypes.LinkToAnotherRecord: - { - validateParams(['parentId', 'childId', 'type'], req.body); + const baseModel = await Model.getBaseModelSQL({ + id: table.id, + dbDriver: NcConnectionMgrv2.get(base), + }); - // get parent and child models - const parent = await Model.getWithInfo({ - id: (req.body as LinkToAnotherColumnReqType).parentId, - }); - const child = await Model.getWithInfo({ - id: (req.body as LinkToAnotherColumnReqType).childId, - }); - let childColumn: Column; + if (colBody.colOptions?.options) { + const supportedDrivers = ['mysql', 'mysql2', 'pg', 'mssql', 'sqlite3']; + const dbDriver = NcConnectionMgrv2.get(base); + const driverType = dbDriver.clientType(); - const sqlMgr = await ProjectMgrv2.getSqlMgr({ - id: base.project_id, - }); - if ( - (req.body as LinkToAnotherColumnReqType).type === 'hm' || - (req.body as LinkToAnotherColumnReqType).type === 'bt' - ) { - // populate fk column name - const fkColName = getUniqueColumnName( - await child.getColumns(), - `${parent.table_name}_id` + // MultiSelect to SingleSelect + if ( + column.uidt === UITypes.MultiSelect && + colBody.uidt === UITypes.SingleSelect + ) { + if (driverType === 'mysql' || driverType === 'mysql2') { + await dbDriver.raw( + `UPDATE ?? SET ?? = SUBSTRING_INDEX(??, ',', 1) WHERE ?? LIKE '%,%';`, + [ + table.table_name, + column.column_name, + column.column_name, + column.column_name, + ] ); - - let foreignKeyName; - { - // create foreign key - const newColumn = { - cn: fkColName, - - title: fkColName, - column_name: fkColName, - rqd: false, - pk: false, - ai: false, - cdf: null, - dt: parent.primaryKey.dt, - dtxp: parent.primaryKey.dtxp, - dtxs: parent.primaryKey.dtxs, - un: parent.primaryKey.un, - altered: Altered.NEW_COLUMN, - }; - const tableUpdateBody = { - ...child, - tn: child.table_name, - originalColumns: child.columns.map((c) => ({ - ...c, - cn: c.column_name, - })), - columns: [ - ...child.columns.map((c) => ({ - ...c, - cn: c.column_name, - })), - newColumn, - ], - }; - - await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody); - - const { id } = await Column.insert({ - ...newColumn, - uidt: UITypes.ForeignKey, - fk_model_id: child.id, - }); - - childColumn = await Column.get({ colId: id }); - - // ignore relation creation if virtual - if (!(req.body as LinkToAnotherColumnReqType).virtual) { - foreignKeyName = generateFkName(parent, child); - // create relation - await sqlMgr.sqlOpPlus(base, 'relationCreate', { - childColumn: fkColName, - childTable: child.table_name, - parentTable: parent.table_name, - onDelete: 'NO ACTION', - onUpdate: 'NO ACTION', - type: 'real', - parentColumn: parent.primaryKey.column_name, - foreignKeyName, - }); - } - - // todo: create index for virtual relations as well - // create index for foreign key in pg - if (base.type === 'pg') { - await createColumnIndex({ - column: new Column({ - ...newColumn, - fk_model_id: child.id, - }), - base, - sqlMgr, - }); - } - } - await createHmAndBtColumn( - child, - parent, - childColumn, - (req.body as LinkToAnotherColumnReqType).type as RelationTypes, - (req.body as LinkToAnotherColumnReqType).title, - foreignKeyName, - (req.body as LinkToAnotherColumnReqType).virtual + } else if (driverType === 'pg') { + await dbDriver.raw(`UPDATE ?? SET ?? = split_part(??, ',', 1);`, [ + table.table_name, + column.column_name, + column.column_name, + ]); + } else if (driverType === 'mssql') { + await dbDriver.raw( + `UPDATE ?? SET ?? = LEFT(cast(?? as varchar(max)), CHARINDEX(',', ??) - 1) WHERE CHARINDEX(',', ??) > 0;`, + [ + table.table_name, + column.column_name, + column.column_name, + column.column_name, + column.column_name, + ] ); - } else if ((req.body as LinkToAnotherColumnReqType).type === 'mm') { - const aTn = `${project?.prefix ?? ''}_nc_m2m_${randomID()}`; - const aTnAlias = aTn; - - const parentPK = parent.primaryKey; - const childPK = child.primaryKey; - - const associateTableCols = []; - - const parentCn = 'table1_id'; - const childCn = 'table2_id'; - - associateTableCols.push( - { - cn: childCn, - column_name: childCn, - title: childCn, - rqd: true, - pk: true, - ai: false, - cdf: null, - dt: childPK.dt, - dtxp: childPK.dtxp, - dtxs: childPK.dtxs, - un: childPK.un, - altered: 1, - uidt: UITypes.ForeignKey, - }, - { - cn: parentCn, - column_name: parentCn, - title: parentCn, - rqd: true, - pk: true, - ai: false, - cdf: null, - dt: parentPK.dt, - dtxp: parentPK.dtxp, - dtxs: parentPK.dtxs, - un: parentPK.un, - altered: 1, - uidt: UITypes.ForeignKey, - } + } else if (driverType === 'sqlite3') { + await dbDriver.raw( + `UPDATE ?? SET ?? = substr(??, 1, instr(??, ',') - 1) WHERE ?? LIKE '%,%';`, + [ + table.table_name, + column.column_name, + column.column_name, + column.column_name, + column.column_name, + ] ); + } + } - await sqlMgr.sqlOpPlus(base, 'tableCreate', { - tn: aTn, - _tn: aTnAlias, - columns: associateTableCols, - }); - - const assocModel = await Model.insert(project.id, base.id, { - table_name: aTn, - title: aTnAlias, - // todo: sanitize - mm: true, - columns: associateTableCols, - }); - - let foreignKeyName1; - let foreignKeyName2; - - if (!(req.body as LinkToAnotherColumnReqType).virtual) { - foreignKeyName1 = generateFkName(parent, child); - foreignKeyName2 = generateFkName(parent, child); - - const rel1Args = { - ...req.body, - childTable: aTn, - childColumn: parentCn, - parentTable: parent.table_name, - parentColumn: parentPK.column_name, - type: 'real', - foreignKeyName: foreignKeyName1, - }; - const rel2Args = { - ...req.body, - childTable: aTn, - childColumn: childCn, - parentTable: child.table_name, - parentColumn: childPK.column_name, - type: 'real', - foreignKeyName: foreignKeyName2, - }; + // Handle migrations + if (column.colOptions?.options) { + for (const op of column.colOptions.options.filter( + (el) => el.order === null + )) { + op.title = op.title.replace(/^'/, '').replace(/'$/, ''); + } + } - await sqlMgr.sqlOpPlus(base, 'relationCreate', rel1Args); - await sqlMgr.sqlOpPlus(base, 'relationCreate', rel2Args); + // Handle default values + const optionTitles = colBody.colOptions.options.map((el) => + el.title.replace(/'/g, "''") + ); + if (colBody.cdf) { + if (colBody.uidt === UITypes.SingleSelect) { + if (!optionTitles.includes(colBody.cdf.replace(/'/g, "''"))) { + NcError.badRequest( + `Default value '${colBody.cdf}' is not a select option.` + ); } - const parentCol = (await assocModel.getColumns())?.find( - (c) => c.column_name === parentCn - ); - const childCol = (await assocModel.getColumns())?.find( - (c) => c.column_name === childCn - ); - - await createHmAndBtColumn( - assocModel, - child, - childCol, - null, - null, - foreignKeyName1, - (req.body as LinkToAnotherColumnReqType).virtual, - true - ); - await createHmAndBtColumn( - assocModel, - parent, - parentCol, - null, - null, - foreignKeyName2, - (req.body as LinkToAnotherColumnReqType).virtual, - true - ); - - await Column.insert({ - title: getUniqueColumnAliasName( - await child.getColumns(), - `${parent.title} List` - ), - uidt: UITypes.LinkToAnotherRecord, - type: 'mm', - - // ref_db_alias - fk_model_id: child.id, - // db_type: - - fk_child_column_id: childPK.id, - fk_parent_column_id: parentPK.id, - - fk_mm_model_id: assocModel.id, - fk_mm_child_column_id: childCol.id, - fk_mm_parent_column_id: parentCol.id, - fk_related_model_id: parent.id, - }); - await Column.insert({ - title: getUniqueColumnAliasName( - await parent.getColumns(), - req.body.title ?? `${child.title} List` - ), - - uidt: UITypes.LinkToAnotherRecord, - type: 'mm', - - fk_model_id: parent.id, - - fk_child_column_id: parentPK.id, - fk_parent_column_id: childPK.id, + } else { + for (const cdf of colBody.cdf.split(',')) { + if (!optionTitles.includes(cdf.replace(/'/g, "''"))) { + NcError.badRequest( + `Default value '${cdf}' is not a select option.` + ); + } + } + } - fk_mm_model_id: assocModel.id, - fk_mm_child_column_id: parentCol.id, - fk_mm_parent_column_id: childCol.id, - fk_related_model_id: child.id, - }); + // handle single quote for default value + if (driverType === 'mysql' || driverType === 'mysql2') { + colBody.cdf = colBody.cdf.replace(/'/g, "'"); + } else { + colBody.cdf = colBody.cdf.replace(/'/g, "''"); + } - // todo: create index for virtual relations as well - // create index for foreign key in pg - if (base.type === 'pg') { - await createColumnIndex({ - column: new Column({ - ...associateTableCols[0], - fk_model_id: assocModel.id, - }), - base, - sqlMgr, - }); - await createColumnIndex({ - column: new Column({ - ...associateTableCols[1], - fk_model_id: assocModel.id, - }), - base, - sqlMgr, - }); - } + if (driverType === 'pg') { + colBody.cdf = `'${colBody.cdf}'`; } } - Tele.emit('evt', { evt_type: 'relation:created' }); - break; - case UITypes.QrCode: - await Column.insert({ - ...colBody, - fk_model_id: table.id, - }); - break; - case UITypes.Barcode: - await Column.insert({ - ...colBody, - fk_model_id: table.id, - }); - break; - case UITypes.Formula: - colBody.formula = await substituteColumnAliasWithIdInFormula( - colBody.formula_raw || colBody.formula, - table.columns - ); - - try { - // test the query to see if it is valid in db level - const dbDriver = NcConnectionMgrv2.get(base); - await formulaQueryBuilderv2(colBody.formula, null, dbDriver, table); - } catch (e) { - console.error(e); - NcError.badRequest('Invalid Formula'); + // Restrict duplicates + const titles = colBody.colOptions.options.map((el) => el.title); + if ( + titles.some(function (item) { + return titles.indexOf(item) !== titles.lastIndexOf(item); + }) + ) { + NcError.badRequest('Duplicates are not allowed!'); } - await Column.insert({ - ...colBody, - fk_model_id: table.id, - }); + // Restrict empty options + if ( + titles.some(function (item) { + return item === ''; + }) + ) { + NcError.badRequest('Empty options are not allowed!'); + } - break; - default: - { - colBody = getColumnPropsFromUIDT(colBody, base); - if (colBody.uidt === UITypes.Duration) { - colBody.dtxp = '20'; - // by default, colBody.dtxs is 2 - // Duration column needs more that that - colBody.dtxs = '4'; + // Trim end of enum/set + if (colBody.dt === 'enum' || colBody.dt === 'set') { + for (const opt of colBody.colOptions.options) { + opt.title = opt.title.trimEnd(); } + } + + if (colBody.uidt === UITypes.SingleSelect) { + colBody.dtxp = colBody.colOptions?.options.length + ? `${colBody.colOptions.options + .map((o) => `'${o.title.replace(/'/gi, "''")}'`) + .join(',')}` + : ''; + } else if (colBody.uidt === UITypes.MultiSelect) { + colBody.dtxp = colBody.colOptions?.options.length + ? `${colBody.colOptions.options + .map((o) => { + if (o.title.includes(',')) { + NcError.badRequest("Illegal char(',') for MultiSelect"); + } + return `'${o.title.replace(/'/gi, "''")}'`; + }) + .join(',')}` + : ''; + } + // Handle empty enum/set for mysql (we restrict empty user options beforehand) + if (driverType === 'mysql' || driverType === 'mysql2') { if ( - [UITypes.SingleSelect, UITypes.MultiSelect].includes(colBody.uidt) + !colBody.colOptions.options.length && + (!colBody.dtxp || colBody.dtxp === '') ) { - const dbDriver = NcConnectionMgrv2.get(base); - const driverType = dbDriver.clientType(); - const optionTitles = colBody.colOptions.options.map((el) => - el.title.replace(/'/g, "''") - ); + colBody.dtxp = "''"; + } - // this is not used for select columns and cause issue for MySQL - colBody.dtxs = ''; - // Handle default values - if (colBody.cdf) { - if (colBody.uidt === UITypes.SingleSelect) { - if (!optionTitles.includes(colBody.cdf.replace(/'/g, "''"))) { - NcError.badRequest( - `Default value '${colBody.cdf}' is not a select option.` - ); - } - } else { - for (const cdf of colBody.cdf.split(',')) { - if (!optionTitles.includes(cdf.replace(/'/g, "''"))) { - NcError.badRequest( - `Default value '${cdf}' is not a select option.` - ); - } - } - } + if (colBody.dt === 'set') { + if (colBody.colOptions?.options.length > 64) { + colBody.dt = 'text'; + } + } + } - // handle single quote for default value - if (driverType === 'mysql' || driverType === 'mysql2') { - colBody.cdf = colBody.cdf.replace(/'/g, "'"); + // Handle option delete + if (column.colOptions?.options) { + for (const option of column.colOptions.options.filter((oldOp) => + colBody.colOptions.options.find((newOp) => newOp.id === oldOp.id) + ? false + : true + )) { + if ( + !supportedDrivers.includes(driverType) && + column.uidt === UITypes.MultiSelect + ) { + NcError.badRequest( + 'Your database not yet supported for this operation. Please remove option from records manually before dropping.' + ); + } + if (column.uidt === UITypes.SingleSelect) { + if (driverType === 'mssql') { + await dbDriver.raw(`UPDATE ?? SET ?? = NULL WHERE ?? LIKE ?`, [ + table.table_name, + column.column_name, + column.column_name, + option.title, + ]); } else { - colBody.cdf = colBody.cdf.replace(/'/g, "''"); + await baseModel.bulkUpdateAll( + { where: `(${column.title},eq,${option.title})` }, + { [column.column_name]: null }, + { cookie } + ); } - - if (driverType === 'pg') { - colBody.cdf = `'${colBody.cdf}'`; + } else if (column.uidt === UITypes.MultiSelect) { + if (driverType === 'mysql' || driverType === 'mysql2') { + if (colBody.dt === 'set') { + await dbDriver.raw( + `UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), ',')) WHERE FIND_IN_SET(?, ??)`, + [ + table.table_name, + column.column_name, + column.column_name, + option.title, + option.title, + column.column_name, + ] + ); + } else { + await dbDriver.raw( + `UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), ','))`, + [ + table.table_name, + column.column_name, + column.column_name, + option.title, + ] + ); + } + } else if (driverType === 'pg') { + await dbDriver.raw( + `UPDATE ?? SET ?? = array_to_string(array_remove(string_to_array(??, ','), ?), ',')`, + [ + table.table_name, + column.column_name, + column.column_name, + option.title, + ] + ); + } else if (driverType === 'mssql') { + await dbDriver.raw( + `UPDATE ?? SET ?? = substring(replace(concat(',', ??, ','), concat(',', ?, ','), ','), 2, len(replace(concat(',', ??, ','), concat(',', ?, ','), ',')) - 2)`, + [ + table.table_name, + column.column_name, + column.column_name, + option.title, + column.column_name, + option.title, + ] + ); + } else if (driverType === 'sqlite3') { + await dbDriver.raw( + `UPDATE ?? SET ?? = TRIM(REPLACE(',' || ?? || ',', ',' || ? || ',', ','), ',')`, + [ + table.table_name, + column.column_name, + column.column_name, + option.title, + ] + ); } } + } + } - // Restrict duplicates - const titles = colBody.colOptions.options.map((el) => el.title); - if ( - titles.some(function (item) { - return titles.indexOf(item) !== titles.lastIndexOf(item); - }) - ) { - NcError.badRequest('Duplicates are not allowed!'); - } + const interchange = []; - // Restrict empty options + // Handle option update + if (column.colOptions?.options) { + const old_titles = column.colOptions.options.map((el) => el.title); + for (const option of column.colOptions.options.filter((oldOp) => + colBody.colOptions.options.find( + (newOp) => newOp.id === oldOp.id && newOp.title !== oldOp.title + ) + )) { if ( - titles.some(function (item) { - return item === ''; - }) + !supportedDrivers.includes(driverType) && + column.uidt === UITypes.MultiSelect ) { - NcError.badRequest('Empty options are not allowed!'); + NcError.badRequest( + 'Your database not yet supported for this operation. Please remove option from records manually before updating.' + ); } - // Trim end of enum/set - if (colBody.dt === 'enum' || colBody.dt === 'set') { - for (const opt of colBody.colOptions.options) { - opt.title = opt.title.trimEnd(); + const newOp = { + ...colBody.colOptions.options.find((el) => option.id === el.id), + }; + if (old_titles.includes(newOp.title)) { + const def_option = { ...newOp }; + let title_counter = 1; + while (old_titles.includes(newOp.title)) { + newOp.title = `${def_option.title}_${title_counter++}`; } + interchange.push({ + def_option, + temp_title: newOp.title, + }); } - if (colBody.uidt === UITypes.SingleSelect) { - colBody.dtxp = colBody.colOptions?.options.length - ? `${colBody.colOptions.options - .map((o) => `'${o.title.replace(/'/gi, "''")}'`) - .join(',')}` - : ''; - } else if (colBody.uidt === UITypes.MultiSelect) { - colBody.dtxp = colBody.colOptions?.options.length - ? `${colBody.colOptions.options - .map((o) => { - if (o.title.includes(',')) { - NcError.badRequest("Illegal char(',') for MultiSelect"); - } - return `'${o.title.replace(/'/gi, "''")}'`; - }) - .join(',')}` - : ''; - } - - // Handle empty enum/set for mysql (we restrict empty user options beforehand) - if (driverType === 'mysql' || driverType === 'mysql2') { - if ( - !colBody.colOptions.options.length && - (!colBody.dtxp || colBody.dtxp === '') - ) { - colBody.dtxp = "''"; - } - - if (colBody.dt === 'set') { - if (colBody.colOptions?.options.length > 64) { - colBody.dt = 'text'; - } - } - } - } - - const tableUpdateBody = { - ...table, - tn: table.table_name, - originalColumns: table.columns.map((c) => ({ - ...c, - cn: c.column_name, - })), - columns: [ - ...table.columns.map((c) => ({ ...c, cn: c.column_name })), - { - ...colBody, - cn: colBody.column_name, - altered: Altered.NEW_COLUMN, - }, - ], - }; - - const sqlClient = await NcConnectionMgrv2.getSqlClient(base); - const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: base.project_id }); - await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody); - - const columns: Array< - Omit & { - cn: string; - system?: boolean; - } - > = (await sqlClient.columnList({ tn: table.table_name }))?.data?.list; - - const insertedColumnMeta = - columns.find((c) => c.cn === colBody.column_name) || ({} as any); - - await Column.insert({ - ...colBody, - ...insertedColumnMeta, - dtxp: [UITypes.MultiSelect, UITypes.SingleSelect].includes( - colBody.uidt as any - ) - ? colBody.dtxp - : insertedColumnMeta.dtxp, - fk_model_id: table.id, - }); - } - break; - } - - await table.getColumns(); - - await Audit.insert({ - project_id: base.project_id, - op_type: AuditOperationTypes.TABLE_COLUMN, - op_sub_type: AuditOperationSubTypes.CREATED, - user: (req as any)?.user?.email, - description: `created column ${colBody.column_name} with alias ${colBody.title} from table ${table.table_name}`, - ip: (req as any).clientIp, - }).then(() => {}); - - Tele.emit('evt', { evt_type: 'column:created' }); - - res.json(table); -} - -export async function columnSetAsPrimary(req: Request, res: Response) { - const column = await Column.get({ colId: req.params.columnId }); - res.json(await Model.updatePrimaryColumn(column.fk_model_id, column.id)); -} - -async function updateRollupOrLookup(colBody: any, column: Column) { - if ( - UITypes.Lookup === column.uidt && - validateRequiredField(colBody, [ - 'fk_lookup_column_id', - 'fk_relation_column_id', - ]) - ) { - await validateLookupPayload(colBody, column.id); - await Column.update(column.id, colBody); - } else if ( - UITypes.Rollup === column.uidt && - validateRequiredField(colBody, [ - 'fk_relation_column_id', - 'fk_rollup_column_id', - 'rollup_function', - ]) - ) { - await validateRollupPayload(colBody); - await Column.update(column.id, colBody); - } -} - -export async function columnUpdate(req: Request, res: Response) { - const column = await Column.get({ colId: req.params.columnId }); - - const table = await Model.getWithInfo({ - id: column.fk_model_id, - }); - - const base = await Base.get(table.base_id); - - const sqlClient = await NcConnectionMgrv2.getSqlClient(base); - - const sqlClientType = sqlClient.knex.clientType(); - - const mxColumnLength = Column.getMaxColumnNameLength(sqlClientType); - - if (req.body.column_name.length > mxColumnLength) { - NcError.badRequest( - `Column name ${req.body.column_name} exceeds ${mxColumnLength} characters` - ); - } - - if ( - !isVirtualCol(req.body) && - !(await Column.checkTitleAvailable({ - column_name: req.body.column_name, - fk_model_id: column.fk_model_id, - exclude_id: req.params.columnId, - })) - ) { - NcError.badRequest('Duplicate column name'); - } - if ( - !(await Column.checkAliasAvailable({ - title: req.body.title, - fk_model_id: column.fk_model_id, - exclude_id: req.params.columnId, - })) - ) { - NcError.badRequest('Duplicate column alias'); - } - - let colBody = req.body; - if ( - [ - UITypes.Lookup, - UITypes.Rollup, - UITypes.LinkToAnotherRecord, - UITypes.Formula, - UITypes.QrCode, - UITypes.Barcode, - UITypes.ForeignKey, - ].includes(column.uidt) - ) { - if (column.uidt === colBody.uidt) { - if ([UITypes.QrCode, UITypes.Barcode].includes(column.uidt)) { - await Column.update(column.id, { - ...column, - ...colBody, - }); - } else if (column.uidt === UITypes.Formula) { - colBody.formula = await substituteColumnAliasWithIdInFormula( - colBody.formula_raw || colBody.formula, - table.columns - ); - - try { - // test the query to see if it is valid in db level - const dbDriver = NcConnectionMgrv2.get(base); - await formulaQueryBuilderv2(colBody.formula, null, dbDriver, table); - } catch (e) { - console.error(e); - NcError.badRequest('Invalid Formula'); - } - - await Column.update(column.id, { - // title: colBody.title, - ...column, - ...colBody, - }); - } else if (colBody.title !== column.title) { - await Column.updateAlias(req.params.columnId, { - title: colBody.title, - }); - } - await updateRollupOrLookup(colBody, column); - } else { - NcError.notImplemented( - `Updating ${colBody.uidt} => ${colBody.uidt} is not implemented` - ); - } - } else if ( - [ - UITypes.Lookup, - UITypes.Rollup, - UITypes.LinkToAnotherRecord, - UITypes.Formula, - UITypes.QrCode, - UITypes.Barcode, - UITypes.ForeignKey, - ].includes(colBody.uidt) - ) { - NcError.notImplemented( - `Updating ${colBody.uidt} => ${colBody.uidt} is not implemented` - ); - } else if ( - [UITypes.SingleSelect, UITypes.MultiSelect].includes(colBody.uidt) - ) { - colBody = getColumnPropsFromUIDT(colBody, base); - - const baseModel = await Model.getBaseModelSQL({ - id: table.id, - dbDriver: NcConnectionMgrv2.get(base), - }); - - if (colBody.colOptions?.options) { - const supportedDrivers = ['mysql', 'mysql2', 'pg', 'mssql', 'sqlite3']; - const dbDriver = NcConnectionMgrv2.get(base); - const driverType = dbDriver.clientType(); - - // MultiSelect to SingleSelect - if ( - column.uidt === UITypes.MultiSelect && - colBody.uidt === UITypes.SingleSelect - ) { - if (driverType === 'mysql' || driverType === 'mysql2') { - await dbDriver.raw( - `UPDATE ?? SET ?? = SUBSTRING_INDEX(??, ',', 1) WHERE ?? LIKE '%,%';`, - [ - table.table_name, - column.column_name, - column.column_name, - column.column_name, - ] - ); - } else if (driverType === 'pg') { - await dbDriver.raw(`UPDATE ?? SET ?? = split_part(??, ',', 1);`, [ - table.table_name, - column.column_name, - column.column_name, - ]); - } else if (driverType === 'mssql') { - await dbDriver.raw( - `UPDATE ?? SET ?? = LEFT(cast(?? as varchar(max)), CHARINDEX(',', ??) - 1) WHERE CHARINDEX(',', ??) > 0;`, - [ - table.table_name, - column.column_name, - column.column_name, - column.column_name, - column.column_name, - ] - ); - } else if (driverType === 'sqlite3') { - await dbDriver.raw( - `UPDATE ?? SET ?? = substr(??, 1, instr(??, ',') - 1) WHERE ?? LIKE '%,%';`, - [ - table.table_name, - column.column_name, - column.column_name, - column.column_name, - column.column_name, - ] - ); - } - } - - // Handle migrations - if (column.colOptions?.options) { - for (const op of column.colOptions.options.filter( - (el) => el.order === null - )) { - op.title = op.title.replace(/^'/, '').replace(/'$/, ''); - } - } - - // Handle default values - const optionTitles = colBody.colOptions.options.map((el) => - el.title.replace(/'/g, "''") - ); - if (colBody.cdf) { - if (colBody.uidt === UITypes.SingleSelect) { - if (!optionTitles.includes(colBody.cdf.replace(/'/g, "''"))) { - NcError.badRequest( - `Default value '${colBody.cdf}' is not a select option.` - ); - } - } else { - for (const cdf of colBody.cdf.split(',')) { - if (!optionTitles.includes(cdf.replace(/'/g, "''"))) { - NcError.badRequest( - `Default value '${cdf}' is not a select option.` - ); - } - } - } - - // handle single quote for default value - if (driverType === 'mysql' || driverType === 'mysql2') { - colBody.cdf = colBody.cdf.replace(/'/g, "'"); - } else { - colBody.cdf = colBody.cdf.replace(/'/g, "''"); - } - - if (driverType === 'pg') { - colBody.cdf = `'${colBody.cdf}'`; - } - } - - // Restrict duplicates - const titles = colBody.colOptions.options.map((el) => el.title); - if ( - titles.some(function (item) { - return titles.indexOf(item) !== titles.lastIndexOf(item); - }) - ) { - NcError.badRequest('Duplicates are not allowed!'); - } - - // Restrict empty options - if ( - titles.some(function (item) { - return item === ''; - }) - ) { - NcError.badRequest('Empty options are not allowed!'); - } + // Append new option before editing + if ( + (driverType === 'mysql' || driverType === 'mysql2') && + (column.dt === 'enum' || column.dt === 'set') + ) { + column.colOptions.options.push({ title: newOp.title }); - // Trim end of enum/set - if (colBody.dt === 'enum' || colBody.dt === 'set') { - for (const opt of colBody.colOptions.options) { - opt.title = opt.title.trimEnd(); - } - } + let temp_dtxp = ''; + if (column.uidt === UITypes.SingleSelect) { + temp_dtxp = column.colOptions.options.length + ? `${column.colOptions.options + .map((o) => `'${o.title.replace(/'/gi, "''")}'`) + .join(',')}` + : ''; + } else if (column.uidt === UITypes.MultiSelect) { + temp_dtxp = column.colOptions.options.length + ? `${column.colOptions.options + .map((o) => { + if (o.title.includes(',')) { + NcError.badRequest("Illegal char(',') for MultiSelect"); + throw new Error(''); + } + return `'${o.title.replace(/'/gi, "''")}'`; + }) + .join(',')}` + : ''; + } - if (colBody.uidt === UITypes.SingleSelect) { - colBody.dtxp = colBody.colOptions?.options.length - ? `${colBody.colOptions.options - .map((o) => `'${o.title.replace(/'/gi, "''")}'`) - .join(',')}` - : ''; - } else if (colBody.uidt === UITypes.MultiSelect) { - colBody.dtxp = colBody.colOptions?.options.length - ? `${colBody.colOptions.options - .map((o) => { - if (o.title.includes(',')) { - NcError.badRequest("Illegal char(',') for MultiSelect"); - } - return `'${o.title.replace(/'/gi, "''")}'`; - }) - .join(',')}` - : ''; - } + const tableUpdateBody = { + ...table, + tn: table.table_name, + originalColumns: table.columns.map((c) => ({ + ...c, + cn: c.column_name, + cno: c.column_name, + })), + columns: await Promise.all( + table.columns.map(async (c) => { + if (c.id === param.columnId) { + const res = { + ...c, + ...column, + cn: column.column_name, + cno: c.column_name, + dtxp: temp_dtxp, + altered: Altered.UPDATE_COLUMN, + }; + return Promise.resolve(res); + } else { + (c as any).cn = c.column_name; + } + return Promise.resolve(c); + }) + ), + }; - // Handle empty enum/set for mysql (we restrict empty user options beforehand) - if (driverType === 'mysql' || driverType === 'mysql2') { - if ( - !colBody.colOptions.options.length && - (!colBody.dtxp || colBody.dtxp === '') - ) { - colBody.dtxp = "''"; - } + const sqlMgr = await ProjectMgrv2.getSqlMgr({ + id: base.project_id, + }); + await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody); - if (colBody.dt === 'set') { - if (colBody.colOptions?.options.length > 64) { - colBody.dt = 'text'; + await Column.update(param.columnId, { + ...column, + }); } - } - } - // Handle option delete - if (column.colOptions?.options) { - for (const option of column.colOptions.options.filter((oldOp) => - colBody.colOptions.options.find((newOp) => newOp.id === oldOp.id) - ? false - : true - )) { - if ( - !supportedDrivers.includes(driverType) && - column.uidt === UITypes.MultiSelect - ) { - NcError.badRequest( - 'Your database not yet supported for this operation. Please remove option from records manually before dropping.' - ); - } if (column.uidt === UITypes.SingleSelect) { if (driverType === 'mssql') { - await dbDriver.raw(`UPDATE ?? SET ?? = NULL WHERE ?? LIKE ?`, [ + await dbDriver.raw(`UPDATE ?? SET ?? = ? WHERE ?? LIKE ?`, [ table.table_name, column.column_name, + newOp.title, column.column_name, option.title, ]); } else { await baseModel.bulkUpdateAll( { where: `(${column.title},eq,${option.title})` }, - { [column.column_name]: null }, - { cookie: req } + { [column.column_name]: newOp.title }, + { cookie } ); } } else if (column.uidt === UITypes.MultiSelect) { if (driverType === 'mysql' || driverType === 'mysql2') { if (colBody.dt === 'set') { await dbDriver.raw( - `UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), ',')) WHERE FIND_IN_SET(?, ??)`, + `UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), CONCAT(',', ?, ','))) WHERE FIND_IN_SET(?, ??)`, [ table.table_name, column.column_name, column.column_name, option.title, + newOp.title, option.title, column.column_name, ] ); } else { await dbDriver.raw( - `UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), ','))`, + `UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), CONCAT(',', ?, ',')))`, [ table.table_name, column.column_name, column.column_name, option.title, + newOp.title, ] ); } } else if (driverType === 'pg') { await dbDriver.raw( - `UPDATE ?? SET ?? = array_to_string(array_remove(string_to_array(??, ','), ?), ',')`, + `UPDATE ?? SET ?? = array_to_string(array_replace(string_to_array(??, ','), ?, ?), ',')`, [ table.table_name, column.column_name, column.column_name, option.title, + newOp.title, ] ); } else if (driverType === 'mssql') { await dbDriver.raw( - `UPDATE ?? SET ?? = substring(replace(concat(',', ??, ','), concat(',', ?, ','), ','), 2, len(replace(concat(',', ??, ','), concat(',', ?, ','), ',')) - 2)`, + `UPDATE ?? SET ?? = substring(replace(concat(',', ??, ','), concat(',', ?, ','), concat(',', ?, ',')), 2, len(replace(concat(',', ??, ','), concat(',', ?, ','), concat(',', ?, ','))) - 2)`, [ table.table_name, column.column_name, column.column_name, option.title, + newOp.title, column.column_name, option.title, + newOp.title, ] ); } else if (driverType === 'sqlite3') { await dbDriver.raw( - `UPDATE ?? SET ?? = TRIM(REPLACE(',' || ?? || ',', ',' || ? || ',', ','), ',')`, + `UPDATE ?? SET ?? = TRIM(REPLACE(',' || ?? || ',', ',' || ? || ',', ',' || ? || ','), ',')`, [ table.table_name, column.column_name, column.column_name, option.title, + newOp.title, + ] + ); + } + } + } + } + + for (const ch of interchange) { + const newOp = ch.def_option; + if (column.uidt === UITypes.SingleSelect) { + if (driverType === 'mssql') { + await dbDriver.raw(`UPDATE ?? SET ?? = ? WHERE ?? LIKE ?`, [ + table.table_name, + column.column_name, + newOp.title, + column.column_name, + ch.temp_title, + ]); + } else { + await baseModel.bulkUpdateAll( + { where: `(${column.title},eq,${ch.temp_title})` }, + { [column.column_name]: newOp.title }, + { cookie } + ); + } + } else if (column.uidt === UITypes.MultiSelect) { + if (driverType === 'mysql' || driverType === 'mysql2') { + if (colBody.dt === 'set') { + await dbDriver.raw( + `UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), CONCAT(',', ?, ','))) WHERE FIND_IN_SET(?, ??)`, + [ + table.table_name, + column.column_name, + column.column_name, + ch.temp_title, + newOp.title, + ch.temp_title, + column.column_name, + ] + ); + } else { + await dbDriver.raw( + `UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), CONCAT(',', ?, ',')))`, + [ + table.table_name, + column.column_name, + column.column_name, + ch.temp_title, + newOp.title, + ch.temp_title, + column.column_name, ] ); } + } else if (driverType === 'pg') { + await dbDriver.raw( + `UPDATE ?? SET ?? = array_to_string(array_replace(string_to_array(??, ','), ?, ?), ',')`, + [ + table.table_name, + column.column_name, + column.column_name, + ch.temp_title, + newOp.title, + ] + ); + } else if (driverType === 'mssql') { + await dbDriver.raw( + `UPDATE ?? SET ?? = substring(replace(concat(',', ??, ','), concat(',', ?, ','), concat(',', ?, ',')), 2, len(replace(concat(',', ??, ','), concat(',', ?, ','), concat(',', ?, ','))) - 2)`, + [ + table.table_name, + column.column_name, + column.column_name, + ch.temp_title, + newOp.title, + column.column_name, + ch.temp_title, + newOp.title, + ] + ); + } else if (driverType === 'sqlite3') { + await dbDriver.raw( + `UPDATE ?? SET ?? = TRIM(REPLACE(',' || ?? || ',', ',' || ? || ',', ',' || ? || ','), ',')`, + [ + table.table_name, + column.column_name, + column.column_name, + ch.temp_title, + newOp.title, + ] + ); + } + } + } + } + + const tableUpdateBody = { + ...table, + tn: table.table_name, + originalColumns: table.columns.map((c) => ({ + ...c, + cn: c.column_name, + cno: c.column_name, + })), + columns: await Promise.all( + table.columns.map(async (c) => { + if (c.id === param.columnId) { + const res = { + ...c, + ...colBody, + cn: colBody.column_name, + cno: c.column_name, + altered: Altered.UPDATE_COLUMN, + }; + + // update formula with new column name + if (c.column_name != colBody.column_name) { + const formulas = await Noco.ncMeta + .knex(MetaTable.COL_FORMULA) + .where('formula', 'like', `%${c.id}%`); + if (formulas) { + const new_column = c; + new_column.column_name = colBody.column_name; + new_column.title = colBody.title; + for (const f of formulas) { + // the formula with column IDs only + const formula = f.formula; + // replace column IDs with alias to get the new formula_raw + const new_formula_raw = substituteColumnIdWithAliasInFormula( + formula, + [new_column] + ); + await FormulaColumn.update(c.id, { + formula_raw: new_formula_raw, + }); + } + } + } + return Promise.resolve(res); + } else { + (c as any).cn = c.column_name; } - } - } + return Promise.resolve(c); + }) + ), + }; - const interchange = []; + const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: base.project_id }); + await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody); - // Handle option update - if (column.colOptions?.options) { - const old_titles = column.colOptions.options.map((el) => el.title); - for (const option of column.colOptions.options.filter((oldOp) => - colBody.colOptions.options.find( - (newOp) => newOp.id === oldOp.id && newOp.title !== oldOp.title - ) - )) { - if ( - !supportedDrivers.includes(driverType) && - column.uidt === UITypes.MultiSelect - ) { - NcError.badRequest( - 'Your database not yet supported for this operation. Please remove option from records manually before updating.' - ); - } + await Column.update(param.columnId, { + ...colBody, + }); + } else { + colBody = getColumnPropsFromUIDT(colBody, base); + const tableUpdateBody = { + ...table, + tn: table.table_name, + originalColumns: table.columns.map((c) => ({ + ...c, + cn: c.column_name, + cno: c.column_name, + })), + columns: await Promise.all( + table.columns.map(async (c) => { + if (c.id === param.columnId) { + const res = { + ...c, + ...colBody, + cn: colBody.column_name, + cno: c.column_name, + altered: Altered.UPDATE_COLUMN, + }; - const newOp = { - ...colBody.colOptions.options.find((el) => option.id === el.id), - }; - if (old_titles.includes(newOp.title)) { - const def_option = { ...newOp }; - let title_counter = 1; - while (old_titles.includes(newOp.title)) { - newOp.title = `${def_option.title}_${title_counter++}`; + // update formula with new column name + if (c.column_name != colBody.column_name) { + const formulas = await Noco.ncMeta + .knex(MetaTable.COL_FORMULA) + .where('formula', 'like', `%${c.id}%`); + if (formulas) { + const new_column = c; + new_column.column_name = colBody.column_name; + new_column.title = colBody.title; + for (const f of formulas) { + // the formula with column IDs only + const formula = f.formula; + // replace column IDs with alias to get the new formula_raw + const new_formula_raw = substituteColumnIdWithAliasInFormula( + formula, + [new_column] + ); + await FormulaColumn.update(c.id, { + formula_raw: new_formula_raw, + }); + } + } } - interchange.push({ - def_option, - temp_title: newOp.title, - }); + return Promise.resolve(res); + } else { + (c as any).cn = c.column_name; } + return Promise.resolve(c); + }) + ), + }; - // Append new option before editing - if ( - (driverType === 'mysql' || driverType === 'mysql2') && - (column.dt === 'enum' || column.dt === 'set') - ) { - column.colOptions.options.push({ title: newOp.title }); + const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: base.project_id }); + await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody); - let temp_dtxp = ''; - if (column.uidt === UITypes.SingleSelect) { - temp_dtxp = column.colOptions.options.length - ? `${column.colOptions.options - .map((o) => `'${o.title.replace(/'/gi, "''")}'`) - .join(',')}` - : ''; - } else if (column.uidt === UITypes.MultiSelect) { - temp_dtxp = column.colOptions.options.length - ? `${column.colOptions.options - .map((o) => { - if (o.title.includes(',')) { - NcError.badRequest("Illegal char(',') for MultiSelect"); - throw new Error(''); - } - return `'${o.title.replace(/'/gi, "''")}'`; - }) - .join(',')}` - : ''; - } + await Column.update(param.columnId, { + ...colBody, + }); + } + await Audit.insert({ + project_id: base.project_id, + op_type: AuditOperationTypes.TABLE_COLUMN, + op_sub_type: AuditOperationSubTypes.UPDATED, + user: param.req?.user?.email, + description: `updated column ${column.column_name} with alias ${column.title} from table ${table.table_name}`, + ip: param.req?.clientIp, + }).then(() => {}); - const tableUpdateBody = { - ...table, - tn: table.table_name, - originalColumns: table.columns.map((c) => ({ - ...c, - cn: c.column_name, - cno: c.column_name, - })), - columns: await Promise.all( - table.columns.map(async (c) => { - if (c.id === req.params.columnId) { - const res = { - ...c, - ...column, - cn: column.column_name, - cno: c.column_name, - dtxp: temp_dtxp, - altered: Altered.UPDATE_COLUMN, - }; - return Promise.resolve(res); - } else { - (c as any).cn = c.column_name; - } - return Promise.resolve(c); - }) - ), - }; + await table.getColumns(); + T.emit('evt', { evt_type: 'column:updated' }); + + return table; +} + +export async function columnGet(param: { columnId: string }) { + return Column.get({ colId: param.columnId }); +} + +export async function columnSetAsPrimary(param: { columnId: string }) { + const column = await Column.get({ colId: param.columnId }); + return Model.updatePrimaryColumn(column.fk_model_id, column.id); +} + +export async function columnAdd(param: { + req?: any; + tableId: string; + column: ColumnReqType; +}) { + validatePayload('swagger.json#/components/schemas/ColumnReq', param.column); + + const table = await Model.getWithInfo({ + id: param.tableId, + }); + + const base = await Base.get(table.base_id); + + const project = await base.getProject(); + + if (param.column.title || param.column.column_name) { + const dbDriver = NcConnectionMgrv2.get(base); + + const sqlClientType = dbDriver.clientType(); + + const mxColumnLength = Column.getMaxColumnNameLength(sqlClientType); + + if ( + (param.column.title || param.column.column_name).length > mxColumnLength + ) { + NcError.badRequest( + `Column name ${ + param.column.title || param.column.column_name + } exceeds ${mxColumnLength} characters` + ); + } + } + + if ( + !isVirtualCol(param.column) && + !(await Column.checkTitleAvailable({ + column_name: param.column.column_name, + fk_model_id: param.tableId, + })) + ) { + NcError.badRequest('Duplicate column name'); + } + if ( + !(await Column.checkAliasAvailable({ + title: param.column.title || param.column.column_name, + fk_model_id: param.tableId, + })) + ) { + NcError.badRequest('Duplicate column alias'); + } + + let colBody: any = param.column; + switch (colBody.uidt) { + case UITypes.Rollup: + { + await validateRollupPayload(param.column); + + await Column.insert({ + ...colBody, + fk_model_id: table.id, + }); + } + break; + case UITypes.Lookup: + { + await validateLookupPayload(param.column); + + await Column.insert({ + ...colBody, + fk_model_id: table.id, + }); + } + break; + + case UITypes.LinkToAnotherRecord: + await createLTARColumn({ ...param, base, project }); + T.emit('evt', { evt_type: 'relation:created' }); + break; + + case UITypes.QrCode: + await Column.insert({ + ...colBody, + fk_model_id: table.id, + }); + break; + case UITypes.Barcode: + await Column.insert({ + ...colBody, + fk_model_id: table.id, + }); + break; + case UITypes.Formula: + colBody.formula = await substituteColumnAliasWithIdInFormula( + colBody.formula_raw || colBody.formula, + table.columns + ); + + try { + // test the query to see if it is valid in db level + const dbDriver = NcConnectionMgrv2.get(base); + await formulaQueryBuilderv2(colBody.formula, null, dbDriver, table); + } catch (e) { + console.error(e); + NcError.badRequest('Invalid Formula'); + } + + await Column.insert({ + ...colBody, + fk_model_id: table.id, + }); - const sqlMgr = await ProjectMgrv2.getSqlMgr({ - id: base.project_id, - }); - await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody); + break; + default: + { + colBody = getColumnPropsFromUIDT(colBody, base); + if (colBody.uidt === UITypes.Duration) { + colBody.dtxp = '20'; + // by default, colBody.dtxs is 2 + // Duration column needs more that that + colBody.dtxs = '4'; + } - await Column.update(req.params.columnId, { - ...column, - }); - } + if ( + [UITypes.SingleSelect, UITypes.MultiSelect].includes(colBody.uidt) + ) { + const dbDriver = NcConnectionMgrv2.get(base); + const driverType = dbDriver.clientType(); + const optionTitles = colBody.colOptions.options.map((el) => + el.title.replace(/'/g, "''") + ); - if (column.uidt === UITypes.SingleSelect) { - if (driverType === 'mssql') { - await dbDriver.raw(`UPDATE ?? SET ?? = ? WHERE ?? LIKE ?`, [ - table.table_name, - column.column_name, - newOp.title, - column.column_name, - option.title, - ]); + // this is not used for select columns and cause issue for MySQL + colBody.dtxs = ''; + // Handle default values + if (colBody.cdf) { + if (colBody.uidt === UITypes.SingleSelect) { + if (!optionTitles.includes(colBody.cdf.replace(/'/g, "''"))) { + NcError.badRequest( + `Default value '${colBody.cdf}' is not a select option.` + ); + } } else { - await baseModel.bulkUpdateAll( - { where: `(${column.title},eq,${option.title})` }, - { [column.column_name]: newOp.title }, - { cookie: req } - ); + for (const cdf of colBody.cdf.split(',')) { + if (!optionTitles.includes(cdf.replace(/'/g, "''"))) { + NcError.badRequest( + `Default value '${cdf}' is not a select option.` + ); + } + } } - } else if (column.uidt === UITypes.MultiSelect) { + + // handle single quote for default value if (driverType === 'mysql' || driverType === 'mysql2') { - if (colBody.dt === 'set') { - await dbDriver.raw( - `UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), CONCAT(',', ?, ','))) WHERE FIND_IN_SET(?, ??)`, - [ - table.table_name, - column.column_name, - column.column_name, - option.title, - newOp.title, - option.title, - column.column_name, - ] - ); - } else { - await dbDriver.raw( - `UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), CONCAT(',', ?, ',')))`, - [ - table.table_name, - column.column_name, - column.column_name, - option.title, - newOp.title, - ] - ); - } - } else if (driverType === 'pg') { - await dbDriver.raw( - `UPDATE ?? SET ?? = array_to_string(array_replace(string_to_array(??, ','), ?, ?), ',')`, - [ - table.table_name, - column.column_name, - column.column_name, - option.title, - newOp.title, - ] - ); - } else if (driverType === 'mssql') { - await dbDriver.raw( - `UPDATE ?? SET ?? = substring(replace(concat(',', ??, ','), concat(',', ?, ','), concat(',', ?, ',')), 2, len(replace(concat(',', ??, ','), concat(',', ?, ','), concat(',', ?, ','))) - 2)`, - [ - table.table_name, - column.column_name, - column.column_name, - option.title, - newOp.title, - column.column_name, - option.title, - newOp.title, - ] - ); - } else if (driverType === 'sqlite3') { - await dbDriver.raw( - `UPDATE ?? SET ?? = TRIM(REPLACE(',' || ?? || ',', ',' || ? || ',', ',' || ? || ','), ',')`, - [ - table.table_name, - column.column_name, - column.column_name, - option.title, - newOp.title, - ] - ); + colBody.cdf = colBody.cdf.replace(/'/g, "'"); + } else { + colBody.cdf = colBody.cdf.replace(/'/g, "''"); + } + + if (driverType === 'pg') { + colBody.cdf = `'${colBody.cdf}'`; } } - } - } - for (const ch of interchange) { - const newOp = ch.def_option; - if (column.uidt === UITypes.SingleSelect) { - if (driverType === 'mssql') { - await dbDriver.raw(`UPDATE ?? SET ?? = ? WHERE ?? LIKE ?`, [ - table.table_name, - column.column_name, - newOp.title, - column.column_name, - ch.temp_title, - ]); - } else { - await baseModel.bulkUpdateAll( - { where: `(${column.title},eq,${ch.temp_title})` }, - { [column.column_name]: newOp.title }, - { cookie: req } - ); + // Restrict duplicates + const titles = colBody.colOptions.options.map((el) => el.title); + if ( + titles.some(function (item) { + return titles.indexOf(item) !== titles.lastIndexOf(item); + }) + ) { + NcError.badRequest('Duplicates are not allowed!'); } - } else if (column.uidt === UITypes.MultiSelect) { - if (driverType === 'mysql' || driverType === 'mysql2') { - if (colBody.dt === 'set') { - await dbDriver.raw( - `UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), CONCAT(',', ?, ','))) WHERE FIND_IN_SET(?, ??)`, - [ - table.table_name, - column.column_name, - column.column_name, - ch.temp_title, - newOp.title, - ch.temp_title, - column.column_name, - ] - ); - } else { - await dbDriver.raw( - `UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), CONCAT(',', ?, ',')))`, - [ - table.table_name, - column.column_name, - column.column_name, - ch.temp_title, - newOp.title, - ch.temp_title, - column.column_name, - ] - ); + + // Restrict empty options + if ( + titles.some(function (item) { + return item === ''; + }) + ) { + NcError.badRequest('Empty options are not allowed!'); + } + + // Trim end of enum/set + if (colBody.dt === 'enum' || colBody.dt === 'set') { + for (const opt of colBody.colOptions.options) { + opt.title = opt.title.trimEnd(); } - } else if (driverType === 'pg') { - await dbDriver.raw( - `UPDATE ?? SET ?? = array_to_string(array_replace(string_to_array(??, ','), ?, ?), ',')`, - [ - table.table_name, - column.column_name, - column.column_name, - ch.temp_title, - newOp.title, - ] - ); - } else if (driverType === 'mssql') { - await dbDriver.raw( - `UPDATE ?? SET ?? = substring(replace(concat(',', ??, ','), concat(',', ?, ','), concat(',', ?, ',')), 2, len(replace(concat(',', ??, ','), concat(',', ?, ','), concat(',', ?, ','))) - 2)`, - [ - table.table_name, - column.column_name, - column.column_name, - ch.temp_title, - newOp.title, - column.column_name, - ch.temp_title, - newOp.title, - ] - ); - } else if (driverType === 'sqlite3') { - await dbDriver.raw( - `UPDATE ?? SET ?? = TRIM(REPLACE(',' || ?? || ',', ',' || ? || ',', ',' || ? || ','), ',')`, - [ - table.table_name, - column.column_name, - column.column_name, - ch.temp_title, - newOp.title, - ] - ); } - } - } - } - const tableUpdateBody = { - ...table, - tn: table.table_name, - originalColumns: table.columns.map((c) => ({ - ...c, - cn: c.column_name, - cno: c.column_name, - })), - columns: await Promise.all( - table.columns.map(async (c) => { - if (c.id === req.params.columnId) { - const res = { - ...c, - ...colBody, - cn: colBody.column_name, - cno: c.column_name, - altered: Altered.UPDATE_COLUMN, - }; + if (colBody.uidt === UITypes.SingleSelect) { + colBody.dtxp = colBody.colOptions?.options.length + ? `${colBody.colOptions.options + .map((o) => `'${o.title.replace(/'/gi, "''")}'`) + .join(',')}` + : ''; + } else if (colBody.uidt === UITypes.MultiSelect) { + colBody.dtxp = colBody.colOptions?.options.length + ? `${colBody.colOptions.options + .map((o) => { + if (o.title.includes(',')) { + NcError.badRequest("Illegal char(',') for MultiSelect"); + } + return `'${o.title.replace(/'/gi, "''")}'`; + }) + .join(',')}` + : ''; + } + + // Handle empty enum/set for mysql (we restrict empty user options beforehand) + if (driverType === 'mysql' || driverType === 'mysql2') { + if ( + !colBody.colOptions.options.length && + (!colBody.dtxp || colBody.dtxp === '') + ) { + colBody.dtxp = "''"; + } - // update formula with new column name - if (c.column_name != colBody.column_name) { - const formulas = await Noco.ncMeta - .knex(MetaTable.COL_FORMULA) - .where('formula', 'like', `%${c.id}%`); - if (formulas) { - const new_column = c; - new_column.column_name = colBody.column_name; - new_column.title = colBody.title; - for (const f of formulas) { - // the formula with column IDs only - const formula = f.formula; - // replace column IDs with alias to get the new formula_raw - const new_formula_raw = substituteColumnIdWithAliasInFormula( - formula, - [new_column] - ); - await FormulaColumn.update(c.id, { - formula_raw: new_formula_raw, - }); - } + if (colBody.dt === 'set') { + if (colBody.colOptions?.options.length > 64) { + colBody.dt = 'text'; } } - return Promise.resolve(res); - } else { - (c as any).cn = c.column_name; } - return Promise.resolve(c); - }) - ), - }; - - const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: base.project_id }); - await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody); + } - await Column.update(req.params.columnId, { - ...colBody, - }); - } else { - colBody = getColumnPropsFromUIDT(colBody, base); - const tableUpdateBody = { - ...table, - tn: table.table_name, - originalColumns: table.columns.map((c) => ({ - ...c, - cn: c.column_name, - cno: c.column_name, - })), - columns: await Promise.all( - table.columns.map(async (c) => { - if (c.id === req.params.columnId) { - const res = { - ...c, + const tableUpdateBody = { + ...table, + tn: table.table_name, + originalColumns: table.columns.map((c) => ({ + ...c, + cn: c.column_name, + })), + columns: [ + ...table.columns.map((c) => ({ ...c, cn: c.column_name })), + { ...colBody, cn: colBody.column_name, - cno: c.column_name, - altered: Altered.UPDATE_COLUMN, - }; + altered: Altered.NEW_COLUMN, + }, + ], + }; - // update formula with new column name - if (c.column_name != colBody.column_name) { - const formulas = await Noco.ncMeta - .knex(MetaTable.COL_FORMULA) - .where('formula', 'like', `%${c.id}%`); - if (formulas) { - const new_column = c; - new_column.column_name = colBody.column_name; - new_column.title = colBody.title; - for (const f of formulas) { - // the formula with column IDs only - const formula = f.formula; - // replace column IDs with alias to get the new formula_raw - const new_formula_raw = substituteColumnIdWithAliasInFormula( - formula, - [new_column] - ); - await FormulaColumn.update(c.id, { - formula_raw: new_formula_raw, - }); - } - } - } - return Promise.resolve(res); - } else { - (c as any).cn = c.column_name; + const sqlClient = await NcConnectionMgrv2.getSqlClient(base); + const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: base.project_id }); + await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody); + + const columns: Array< + Omit & { + cn: string; + system?: boolean; } - return Promise.resolve(c); - }) - ), - }; + > = (await sqlClient.columnList({ tn: table.table_name }))?.data?.list; - const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: base.project_id }); - await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody); + const insertedColumnMeta = + columns.find((c) => c.cn === colBody.column_name) || ({} as any); - await Column.update(req.params.columnId, { - ...colBody, - }); + await Column.insert({ + ...colBody, + ...insertedColumnMeta, + dtxp: [UITypes.MultiSelect, UITypes.SingleSelect].includes( + colBody.uidt as any + ) + ? colBody.dtxp + : insertedColumnMeta.dtxp, + fk_model_id: table.id, + }); + } + break; } + + await table.getColumns(); + await Audit.insert({ project_id: base.project_id, op_type: AuditOperationTypes.TABLE_COLUMN, - op_sub_type: AuditOperationSubTypes.UPDATED, - user: (req as any)?.user?.email, - description: `updated column ${column.column_name} with alias ${column.title} from table ${table.table_name}`, - ip: (req as any).clientIp, + op_sub_type: AuditOperationSubTypes.CREATED, + user: param?.req?.user?.email, + description: `created column ${colBody.column_name} with alias ${colBody.title} from table ${table.table_name}`, + ip: param?.req.clientIp, }).then(() => {}); - await table.getColumns(); - Tele.emit('evt', { evt_type: 'column:updated' }); + T.emit('evt', { evt_type: 'column:created' }); - res.json(table); + return table; } -export async function columnDelete(req: Request, res: Response) { - const column = await Column.get({ colId: req.params.columnId }); +export async function columnDelete(param: { req?: any; columnId: string }) { + const column = await Column.get({ colId: param.columnId }); const table = await Model.getWithInfo({ id: column.fk_model_id, }); @@ -1437,7 +1141,7 @@ export async function columnDelete(req: Request, res: Response) { case UITypes.QrCode: case UITypes.Barcode: case UITypes.Formula: - await Column.delete(req.params.columnId); + await Column.delete(param.columnId); break; case UITypes.LinkToAnotherRecord: { @@ -1566,7 +1270,7 @@ export async function columnDelete(req: Request, res: Response) { break; } } - Tele.emit('evt', { evt_type: 'raltion:deleted' }); + T.emit('evt', { evt_type: 'raltion:deleted' }); break; case UITypes.ForeignKey: { NcError.notImplemented(); @@ -1593,7 +1297,7 @@ export async function columnDelete(req: Request, res: Response) { cno: c.column_name, })), columns: table.columns.map((c) => { - if (c.id === req.params.columnId) { + if (c.id === param.columnId) { return { ...c, cn: c.column_name, @@ -1609,7 +1313,7 @@ export async function columnDelete(req: Request, res: Response) { await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody); - await Column.delete(req.params.columnId); + await Column.delete(param.columnId); } } @@ -1617,9 +1321,9 @@ export async function columnDelete(req: Request, res: Response) { project_id: base.project_id, op_type: AuditOperationTypes.TABLE_COLUMN, op_sub_type: AuditOperationSubTypes.DELETED, - user: (req as any)?.user?.email, + user: param?.req?.user?.email, description: `deleted column ${column.column_name} with alias ${column.title} from table ${table.table_name}`, - ip: (req as any).clientIp, + ip: param?.req.clientIp, }).then(() => {}); await table.getColumns(); @@ -1632,16 +1336,9 @@ export async function columnDelete(req: Request, res: Response) { ); } - // await ncMeta.commit(); - // await sql-mgr.commit(); - Tele.emit('evt', { evt_type: 'column:deleted' }); + T.emit('evt', { evt_type: 'column:deleted' }); - res.json(table); - // } catch (e) { - // sql-mgr.rollback(); - // ncMeta.rollback(); - // throw e; - // } + return table; } const deleteHmOrBtRelation = async ( @@ -1758,7 +1455,298 @@ const deleteHmOrBtRelation = async ( await Column.delete(childColumn.id, ncMeta); }; -async function createColumnIndex({ +async function createLTARColumn(param: { + tableId: string; + column: ColumnReqType; + base: Base; + project: Project; +}) { + validateParams(['parentId', 'childId', 'type'], param.column); + + // get parent and child models + const parent = await Model.getWithInfo({ + id: (param.column as LinkToAnotherColumnReqType).parentId, + }); + const child = await Model.getWithInfo({ + id: (param.column as LinkToAnotherColumnReqType).childId, + }); + let childColumn: Column; + + const sqlMgr = await ProjectMgrv2.getSqlMgr({ + id: param.base.project_id, + }); + if ( + (param.column as LinkToAnotherColumnReqType).type === 'hm' || + (param.column as LinkToAnotherColumnReqType).type === 'bt' + ) { + // populate fk column name + const fkColName = getUniqueColumnName( + await child.getColumns(), + `${parent.table_name}_id` + ); + + let foreignKeyName; + { + // create foreign key + const newColumn = { + cn: fkColName, + + title: fkColName, + column_name: fkColName, + rqd: false, + pk: false, + ai: false, + cdf: null, + dt: parent.primaryKey.dt, + dtxp: parent.primaryKey.dtxp, + dtxs: parent.primaryKey.dtxs, + un: parent.primaryKey.un, + altered: Altered.NEW_COLUMN, + }; + const tableUpdateBody = { + ...child, + tn: child.table_name, + originalColumns: child.columns.map((c) => ({ + ...c, + cn: c.column_name, + })), + columns: [ + ...child.columns.map((c) => ({ + ...c, + cn: c.column_name, + })), + newColumn, + ], + }; + + await sqlMgr.sqlOpPlus(param.base, 'tableUpdate', tableUpdateBody); + + const { id } = await Column.insert({ + ...newColumn, + uidt: UITypes.ForeignKey, + fk_model_id: child.id, + }); + + childColumn = await Column.get({ colId: id }); + + // ignore relation creation if virtual + if (!(param.column as LinkToAnotherColumnReqType).virtual) { + foreignKeyName = generateFkName(parent, child); + // create relation + await sqlMgr.sqlOpPlus(param.base, 'relationCreate', { + childColumn: fkColName, + childTable: child.table_name, + parentTable: parent.table_name, + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + type: 'real', + parentColumn: parent.primaryKey.column_name, + foreignKeyName, + }); + } + + // todo: create index for virtual relations as well + // create index for foreign key in pg + if (param.base.type === 'pg') { + await createColumnIndex({ + column: new Column({ + ...newColumn, + fk_model_id: child.id, + }), + base: param.base, + sqlMgr, + }); + } + } + await createHmAndBtColumn( + child, + parent, + childColumn, + (param.column as LinkToAnotherColumnReqType).type as RelationTypes, + (param.column as LinkToAnotherColumnReqType).title, + foreignKeyName, + (param.column as LinkToAnotherColumnReqType).virtual + ); + } else if ((param.column as LinkToAnotherColumnReqType).type === 'mm') { + const aTn = `${param.project?.prefix ?? ''}_nc_m2m_${randomID()}`; + const aTnAlias = aTn; + + const parentPK = parent.primaryKey; + const childPK = child.primaryKey; + + const associateTableCols = []; + + const parentCn = 'table1_id'; + const childCn = 'table2_id'; + + associateTableCols.push( + { + cn: childCn, + column_name: childCn, + title: childCn, + rqd: true, + pk: true, + ai: false, + cdf: null, + dt: childPK.dt, + dtxp: childPK.dtxp, + dtxs: childPK.dtxs, + un: childPK.un, + altered: 1, + uidt: UITypes.ForeignKey, + }, + { + cn: parentCn, + column_name: parentCn, + title: parentCn, + rqd: true, + pk: true, + ai: false, + cdf: null, + dt: parentPK.dt, + dtxp: parentPK.dtxp, + dtxs: parentPK.dtxs, + un: parentPK.un, + altered: 1, + uidt: UITypes.ForeignKey, + } + ); + + await sqlMgr.sqlOpPlus(param.base, 'tableCreate', { + tn: aTn, + _tn: aTnAlias, + columns: associateTableCols, + }); + + const assocModel = await Model.insert(param.project.id, param.base.id, { + table_name: aTn, + title: aTnAlias, + // todo: sanitize + mm: true, + columns: associateTableCols, + }); + + let foreignKeyName1; + let foreignKeyName2; + + if (!(param.column as LinkToAnotherColumnReqType).virtual) { + foreignKeyName1 = generateFkName(parent, child); + foreignKeyName2 = generateFkName(parent, child); + + const rel1Args = { + ...param.column, + childTable: aTn, + childColumn: parentCn, + parentTable: parent.table_name, + parentColumn: parentPK.column_name, + type: 'real', + foreignKeyName: foreignKeyName1, + }; + const rel2Args = { + ...param.column, + childTable: aTn, + childColumn: childCn, + parentTable: child.table_name, + parentColumn: childPK.column_name, + type: 'real', + foreignKeyName: foreignKeyName2, + }; + + await sqlMgr.sqlOpPlus(param.base, 'relationCreate', rel1Args); + await sqlMgr.sqlOpPlus(param.base, 'relationCreate', rel2Args); + } + const parentCol = (await assocModel.getColumns())?.find( + (c) => c.column_name === parentCn + ); + const childCol = (await assocModel.getColumns())?.find( + (c) => c.column_name === childCn + ); + + await createHmAndBtColumn( + assocModel, + child, + childCol, + null, + null, + foreignKeyName1, + (param.column as LinkToAnotherColumnReqType).virtual, + true + ); + await createHmAndBtColumn( + assocModel, + parent, + parentCol, + null, + null, + foreignKeyName2, + (param.column as LinkToAnotherColumnReqType).virtual, + true + ); + + await Column.insert({ + title: getUniqueColumnAliasName( + await child.getColumns(), + `${parent.title} List` + ), + uidt: UITypes.LinkToAnotherRecord, + type: 'mm', + + // ref_db_alias + fk_model_id: child.id, + // db_type: + + fk_child_column_id: childPK.id, + fk_parent_column_id: parentPK.id, + + fk_mm_model_id: assocModel.id, + fk_mm_child_column_id: childCol.id, + fk_mm_parent_column_id: parentCol.id, + fk_related_model_id: parent.id, + }); + await Column.insert({ + title: getUniqueColumnAliasName( + await parent.getColumns(), + param.column.title ?? `${child.title} List` + ), + + uidt: UITypes.LinkToAnotherRecord, + type: 'mm', + + fk_model_id: parent.id, + + fk_child_column_id: parentPK.id, + fk_parent_column_id: childPK.id, + + fk_mm_model_id: assocModel.id, + fk_mm_child_column_id: parentCol.id, + fk_mm_parent_column_id: childCol.id, + fk_related_model_id: child.id, + }); + + // todo: create index for virtual relations as well + // create index for foreign key in pg + if (param.base.type === 'pg') { + await createColumnIndex({ + column: new Column({ + ...associateTableCols[0], + fk_model_id: assocModel.id, + }), + base: param.base, + sqlMgr, + }); + await createColumnIndex({ + column: new Column({ + ...associateTableCols[1], + fk_model_id: assocModel.id, + }), + base: param.base, + sqlMgr, + }); + } + } +} + +export async function createColumnIndex({ column, sqlMgr, base, @@ -1781,36 +1769,25 @@ async function createColumnIndex({ sqlMgr.sqlOpPlus(base, 'indexCreate', indexArgs); } -const router = Router({ mergeParams: true }); - -router.post( - '/api/v1/db/meta/tables/:tableId/columns/', - metaApiMetrics, - getAjvValidatorMw('swagger.json#/components/schemas/ColumnReq'), - ncMetaAclMw(columnAdd, 'columnAdd') -); - -router.patch( - '/api/v1/db/meta/columns/:columnId', - metaApiMetrics, - ncMetaAclMw(columnUpdate, 'columnUpdate') -); - -router.delete( - '/api/v1/db/meta/columns/:columnId', - metaApiMetrics, - ncMetaAclMw(columnDelete, 'columnDelete') -); - -router.get( - '/api/v1/db/meta/columns/:columnId', - metaApiMetrics, - ncMetaAclMw(columnGet, 'columnGet') -); - -router.post( - '/api/v1/db/meta/columns/:columnId/primary', - metaApiMetrics, - ncMetaAclMw(columnSetAsPrimary, 'columnSetAsPrimary') -); -export default router; +async function updateRollupOrLookup(colBody: any, column: Column) { + if ( + UITypes.Lookup === column.uidt && + validateRequiredField(colBody, [ + 'fk_lookup_column_id', + 'fk_relation_column_id', + ]) + ) { + await validateLookupPayload(colBody, column.id); + await Column.update(column.id, colBody); + } else if ( + UITypes.Rollup === column.uidt && + validateRequiredField(colBody, [ + 'fk_relation_column_id', + 'fk_rollup_column_id', + 'rollup_function', + ]) + ) { + await validateRollupPayload(colBody); + await Column.update(column.id, colBody); + } +} diff --git a/packages/nocodb/src/lib/services/dataService/bulkData.ts b/packages/nocodb/src/lib/services/dataService/bulkData.ts new file mode 100644 index 0000000000..2ac6b5a20b --- /dev/null +++ b/packages/nocodb/src/lib/services/dataService/bulkData.ts @@ -0,0 +1,102 @@ +import { BaseModelSqlv2 } from '../../db/sql-data-mapper/lib/sql/BaseModelSqlv2'; +import { Base, Model } from '../../models'; +import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; +import { getViewAndModelByAliasOrId, PathParams } from './helpers'; + +type BulkOperation = + | 'bulkInsert' + | 'bulkUpdate' + | 'bulkUpdateAll' + | 'bulkDelete' + | 'bulkDeleteAll'; + +export async function getModelViewBase(param: PathParams) { + const { model, view } = await getViewAndModelByAliasOrId(param); + + const base = await Base.get(model.base_id); + return { model, view, base }; +} + +export async function executeBulkOperation( + param: PathParams & { + operation: T; + options: Parameters; + } +) { + const { model, view, base } = await getModelViewBase(param); + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + return await baseModel[param.operation].apply(null, param.options); +} + +// todo: Integrate with filterArrJson bulkDataUpdateAll +export async function bulkDataInsert( + param: PathParams & { + body: any; + cookie: any; + } +) { + return await executeBulkOperation({ + ...param, + operation: 'bulkInsert', + options: [param.body, { cookie: param.cookie }], + }); +} + +// todo: Integrate with filterArrJson bulkDataUpdateAll +export async function bulkDataUpdate( + param: PathParams & { + body: any; + cookie: any; + } +) { + return await executeBulkOperation({ + ...param, + operation: 'bulkUpdate', + options: [param.body, { cookie: param.cookie }], + }); +} + +// todo: Integrate with filterArrJson bulkDataUpdateAll +export async function bulkDataUpdateAll( + param: PathParams & { + body: any; + cookie: any; + query: any; + } +) { + return await executeBulkOperation({ + ...param, + operation: 'bulkUpdateAll', + options: [param.query, param.body, { cookie: param.cookie }], + }); +} + +export async function bulkDataDelete( + param: PathParams & { + body: any; + cookie: any; + } +) { + return await executeBulkOperation({ + ...param, + operation: 'bulkDelete', + options: [param.body, { cookie: param.cookie }], + }); +} + +// todo: Integrate with filterArrJson bulkDataDeleteAll +export async function bulkDataDeleteAll( + param: PathParams & { + query: any; + } +) { + return await executeBulkOperation({ + ...param, + operation: 'bulkDeleteAll', + options: [param.query], + }); +} diff --git a/packages/nocodb/src/lib/services/dataService/dataAliasNestedService.ts b/packages/nocodb/src/lib/services/dataService/dataAliasNestedService.ts new file mode 100644 index 0000000000..20959d15fa --- /dev/null +++ b/packages/nocodb/src/lib/services/dataService/dataAliasNestedService.ts @@ -0,0 +1,280 @@ +import { PagedResponseImpl } from '../../meta/helpers/PagedResponse'; +import { Base, Model } from '../../models'; +import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; +import { + getColumnByIdOrName, + getViewAndModelByAliasOrId, + PathParams, +} from './helpers'; +import { NcError } from '../../meta/helpers/catchError'; + +// todo: handle case where the given column is not ltar +export async function mmList( + param: PathParams & { + query: any; + columnName: string; + rowId: string; + } +) { + const { model, view } = await getViewAndModelByAliasOrId(param); + + if (!model) NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const column = await getColumnByIdOrName(param.columnName, model); + + const data = await baseModel.mmList( + { + colId: column.id, + parentId: param.rowId, + }, + param.query as any + ); + const count: any = await baseModel.mmListCount({ + colId: column.id, + parentId: param.rowId, + }); + + return new PagedResponseImpl(data, { + count, + ...param.query, + }); +} + +export async function mmExcludedList( + param: PathParams & { + query: any; + columnName: string; + rowId: string; + } +) { + const { model, view } = await getViewAndModelByAliasOrId(param); + if (!model) NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + const column = await getColumnByIdOrName(param.columnName, model); + + const data = await baseModel.getMmChildrenExcludedList( + { + colId: column.id, + pid: param.rowId, + }, + param.query + ); + + const count = await baseModel.getMmChildrenExcludedListCount( + { + colId: column.id, + pid: param.rowId, + }, + param.query + ); + + return new PagedResponseImpl(data, { + count, + ...param.query, + }); +} + +export async function hmExcludedList( + param: PathParams & { + query: any; + columnName: string; + rowId: string; + } +) { + const { model, view } = await getViewAndModelByAliasOrId(param); + + if (!model) NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const column = await getColumnByIdOrName(param.columnName, model); + + const data = await baseModel.getHmChildrenExcludedList( + { + colId: column.id, + pid: param.rowId, + }, + param.query + ); + + const count = await baseModel.getHmChildrenExcludedListCount( + { + colId: column.id, + pid: param.rowId, + }, + param.query + ); + + return new PagedResponseImpl(data, { + count, + ...param.query, + }); +} + +export async function btExcludedList( + param: PathParams & { + query: any; + columnName: string; + rowId: string; + } +) { + const { model, view } = await getViewAndModelByAliasOrId(param); + if (!model) NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const column = await getColumnByIdOrName(param.columnName, model); + + const data = await baseModel.getBtChildrenExcludedList( + { + colId: column.id, + cid: param.rowId, + }, + param.query + ); + + const count = await baseModel.getBtChildrenExcludedListCount( + { + colId: column.id, + cid: param.rowId, + }, + param.query + ); + + return new PagedResponseImpl(data, { + count, + ...param.query, + }); +} + +// todo: handle case where the given column is not ltar +export async function hmList( + param: PathParams & { + query: any; + columnName: string; + rowId: string; + } +) { + const { model, view } = await getViewAndModelByAliasOrId(param); + + if (!model) NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const column = await getColumnByIdOrName(param.columnName, model); + + const data = await baseModel.hmList( + { + colId: column.id, + id: param.rowId, + }, + param.query + ); + + const count = await baseModel.hmListCount({ + colId: column.id, + id: param.rowId, + }); + + return new PagedResponseImpl(data, { + count, + ...param.query, + } as any); +} + +//@ts-ignore +export async function relationDataRemove( + param: PathParams & { + columnName: string; + rowId: string; + refRowId: string; + cookie: any; + } +) { + const { model, view } = await getViewAndModelByAliasOrId(param); + if (!model) NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const column = await getColumnByIdOrName(param.columnName, model); + + await baseModel.removeChild({ + colId: column.id, + childId: param.refRowId, + rowId: param.rowId, + cookie: param.cookie, + }); + + return true; +} + +//@ts-ignore +// todo: Give proper error message when reference row is already related and handle duplicate ref row id in hm +export async function relationDataAdd( + param: PathParams & { + columnName: string; + rowId: string; + refRowId: string; + cookie: any; + } +) { + const { model, view } = await getViewAndModelByAliasOrId(param); + if (!model) NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const column = await getColumnByIdOrName(param.columnName, model); + await baseModel.addChild({ + colId: column.id, + childId: param.refRowId, + rowId: param.rowId, + cookie: param.cookie, + }); + + return true; +} diff --git a/packages/nocodb/src/lib/services/dataService/export.ts b/packages/nocodb/src/lib/services/dataService/export.ts new file mode 100644 index 0000000000..fa222f61ce --- /dev/null +++ b/packages/nocodb/src/lib/services/dataService/export.ts @@ -0,0 +1,50 @@ +import { getViewAndModelByAliasOrId, PathParams } from './helpers'; +import { View } from '../../models'; + +// Todo: bring logic from controller +export async function excelDataExport( + param: PathParams & { + query: any; + } +) { + const { model, view } = await getViewAndModelByAliasOrId(param); + let targetView = view; + if (!targetView) { + targetView = await View.getDefaultView(model.id); + } + // const { offset, elapsed, data } = await extractXlsxData({ + // view: targetView, + // query: param.query, + // }); + // const wb = XLSX.utils.book_new(); + // XLSX.utils.book_append_sheet(wb, data, targetView.title); + // const buf = XLSX.write(wb, { type: 'base64', bookType: 'xlsx' }); + // res.set({ + // 'Access-Control-Expose-Headers': 'nc-export-offset', + // 'nc-export-offset': offset, + // 'nc-export-elapsed-time': elapsed, + // 'Content-Disposition': `attachment; filename="${encodeURI( + // targetView.title + // )}-export.xlsx"`, + // }); + // res.end(buf); +} + +// async function csvDataExport(req: Request, res: Response) { +// const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); +// let targetView = view; +// if (!targetView) { +// targetView = await View.getDefaultView(model.id); +// } +// const { offset, elapsed, data } = await extractCsvData(targetView, req); +// +// res.set({ +// 'Access-Control-Expose-Headers': 'nc-export-offset', +// 'nc-export-offset': offset, +// 'nc-export-elapsed-time': elapsed, +// 'Content-Disposition': `attachment; filename="${encodeURI( +// targetView.title +// )}-export.csv"`, +// }); +// res.send(data); +// } diff --git a/packages/nocodb/src/lib/meta/api/dataApis/helpers.ts b/packages/nocodb/src/lib/services/dataService/helpers.ts similarity index 78% rename from packages/nocodb/src/lib/meta/api/dataApis/helpers.ts rename to packages/nocodb/src/lib/services/dataService/helpers.ts index aec0e4216e..9654fc3dfc 100644 --- a/packages/nocodb/src/lib/meta/api/dataApis/helpers.ts +++ b/packages/nocodb/src/lib/services/dataService/helpers.ts @@ -1,35 +1,40 @@ -import Project from '../../../models/Project'; -import Model from '../../../models/Model'; -import View from '../../../models/View'; -import { NcError } from '../../helpers/catchError'; import { Request } from 'express'; -import Base from '../../../models/Base'; -import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; -import { isSystemColumn, UITypes } from 'nocodb-sdk'; - import { nocoExecute } from 'nc-help'; +import { isSystemColumn, UITypes } from 'nocodb-sdk'; import * as XLSX from 'xlsx'; -import Column from '../../../models/Column'; -import LookupColumn from '../../../models/LookupColumn'; -import LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn'; - +import { BaseModelSqlv2 } from '../../db/sql-data-mapper/lib/sql/BaseModelSqlv2'; +import getAst from '../../db/sql-data-mapper/lib/sql/helpers/getAst'; +import { NcError } from '../../meta/helpers/catchError'; +import { Model, View } from '../../models'; +import Base from '../../models/Base'; +import Column from '../../models/Column'; +import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn'; +import LookupColumn from '../../models/LookupColumn'; +import Project from '../../models/Project'; +import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; import papaparse from 'papaparse'; -import getAst from '../../../db/sql-data-mapper/lib/sql/helpers/getAst'; -export async function getViewAndModelFromRequestByAliasOrId( - req: - | Request<{ projectName: string; tableName: string; viewName?: string }> - | Request -) { - const project = await Project.getWithInfoByTitleOrId(req.params.projectName); + +export interface PathParams { + projectName: string; + tableName: string; + viewName?: string; +} + +export async function getViewAndModelByAliasOrId(param: { + projectName: string; + tableName: string; + viewName?: string; +}) { + const project = await Project.getWithInfoByTitleOrId(param.projectName); const model = await Model.getByAliasOrId({ project_id: project.id, - aliasOrId: req.params.tableName, + aliasOrId: param.tableName, }); const view = - req.params.viewName && + param.viewName && (await View.getByTitleOrId({ - titleOrId: req.params.viewName, + titleOrId: param.viewName, fk_model_id: model.id, })); if (!model) NcError.notFound('Table not found'); @@ -56,7 +61,12 @@ export async function extractXlsxData(view: View, req: Request) { dbDriver: NcConnectionMgrv2.get(base), }); - const { offset, dbRows, elapsed } = await getDbRows(baseModel, view, req); + const { offset, dbRows, elapsed } = await getDbRows({ + baseModel, + view, + siteUrl: (req as any).ncSiteUrl, + query: req.query, + }); const fields = req.query.fields as string[]; @@ -86,7 +96,12 @@ export async function extractCsvData(view: View, req: Request) { dbDriver: NcConnectionMgrv2.get(base), }); - const { offset, dbRows, elapsed } = await getDbRows(baseModel, view, req); + const { offset, dbRows, elapsed } = await getDbRows({ + baseModel, + view, + query: req.query, + siteUrl: (req as any).ncSiteUrl, + }); const data = papaparse.unparse( { @@ -111,64 +126,6 @@ export async function extractCsvData(view: View, req: Request) { return { offset, dbRows, elapsed, data }; } -async function getDbRows(baseModel, view: View, req: Request) { - let offset = +req.query.offset || 0; - const limit = 100; - // const size = +process.env.NC_EXPORT_MAX_SIZE || 1024; - const timeout = +process.env.NC_EXPORT_MAX_TIMEOUT || 5000; - const dbRows = []; - const startTime = process.hrtime(); - let elapsed, temp; - - const listArgs: any = { ...req.query }; - try { - listArgs.filterArr = JSON.parse(listArgs.filterArrJson); - } catch (e) {} - try { - listArgs.sortArr = JSON.parse(listArgs.sortArrJson); - } catch (e) {} - - for ( - elapsed = 0; - elapsed < timeout; - offset += limit, - temp = process.hrtime(startTime), - elapsed = temp[0] * 1000 + temp[1] / 1000000 - ) { - const rows = await nocoExecute( - await getAst({ - query: req.query, - includePkByDefault: false, - model: view.model, - view, - }), - await baseModel.list({ ...listArgs, offset, limit }), - {}, - req.query - ); - - if (!rows?.length) { - offset = -1; - break; - } - - for (const row of rows) { - const dbRow = { ...row }; - - for (const column of view.model.columns) { - if (isSystemColumn(column) && !view.show_system_fields) continue; - dbRow[column.title] = await serializeCellValue({ - value: row[column.title], - column, - siteUrl: req['ncSiteUrl'], - }); - } - dbRows.push(dbRow); - } - } - return { offset, dbRows, elapsed }; -} - export async function serializeCellValue({ value, column, @@ -254,3 +211,67 @@ export async function getColumnByIdOrName( return column; } + +export async function getDbRows(param: { + baseModel: BaseModelSqlv2; + view: View; + query: any; + siteUrl: string; +}) { + const { baseModel, view, query = {}, siteUrl } = param; + let offset = +query.offset || 0; + const limit = 100; + // const size = +process.env.NC_EXPORT_MAX_SIZE || 1024; + const timeout = +process.env.NC_EXPORT_MAX_TIMEOUT || 5000; + const dbRows = []; + const startTime = process.hrtime(); + let elapsed, temp; + + const listArgs: any = { ...query }; + try { + listArgs.filterArr = JSON.parse(listArgs.filterArrJson); + } catch (e) {} + try { + listArgs.sortArr = JSON.parse(listArgs.sortArrJson); + } catch (e) {} + + for ( + elapsed = 0; + elapsed < timeout; + offset += limit, + temp = process.hrtime(startTime), + elapsed = temp[0] * 1000 + temp[1] / 1000000 + ) { + const rows = await nocoExecute( + await getAst({ + query: query, + includePkByDefault: false, + model: view.model, + view, + }), + await baseModel.list({ ...listArgs, offset, limit }), + {}, + query + ); + + if (!rows?.length) { + offset = -1; + break; + } + + for (const row of rows) { + const dbRow = { ...row }; + + for (const column of view.model.columns) { + if (isSystemColumn(column) && !view.show_system_fields) continue; + dbRow[column.title] = await serializeCellValue({ + value: row[column.title], + column, + siteUrl, + }); + } + dbRows.push(dbRow); + } + } + return { offset, dbRows, elapsed }; +} diff --git a/packages/nocodb/src/lib/services/dataService/index.ts b/packages/nocodb/src/lib/services/dataService/index.ts new file mode 100644 index 0000000000..c125530352 --- /dev/null +++ b/packages/nocodb/src/lib/services/dataService/index.ts @@ -0,0 +1,798 @@ +import { nocoExecute } from 'nc-help'; +import getAst from '../../db/sql-data-mapper/lib/sql/helpers/getAst'; +import { NcError } from '../../meta/helpers/catchError'; +import { PagedResponseImpl } from '../../meta/helpers/PagedResponse'; +import { Base, Model, View } from '../../models'; +import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; +import { getViewAndModelByAliasOrId, PathParams } from './helpers'; + +export async function dataList(param: PathParams & { query: any }) { + const { model, view } = await getViewAndModelByAliasOrId(param); + const responseData = await getDataList({ model, view, query: param.query }); + return responseData; +} + +export async function dataFindOne(param: PathParams & { query: any }) { + const { model, view } = await getViewAndModelByAliasOrId(param); + return await getFindOne({ model, view, query: param.query }); +} + +export async function dataGroupBy(param: PathParams & { query: any }) { + const { model, view } = await getViewAndModelByAliasOrId(param); + return await getDataGroupBy({ model, view, query: param.query }); +} + +export async function dataCount(param: PathParams & { query: any }) { + const { model, view } = await getViewAndModelByAliasOrId(param); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const countArgs: any = { ...param.query }; + try { + countArgs.filterArr = JSON.parse(countArgs.filterArrJson); + } catch (e) {} + + const count: number = await baseModel.count(countArgs); + + return { count }; +} + +export async function dataInsert( + param: PathParams & { body: unknown; cookie: any } +) { + const { model, view } = await getViewAndModelByAliasOrId(param); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + return await baseModel.insert(param.body, null, param.cookie); +} + +export async function dataUpdate( + param: PathParams & { body: unknown; cookie: any; rowId: string } +) { + const { model, view } = await getViewAndModelByAliasOrId(param); + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + return await baseModel.updateByPk( + param.rowId, + param.body, + null, + param.cookie + ); +} + +export async function dataDelete( + param: PathParams & { rowId: string; cookie: any } +) { + const { model, view } = await getViewAndModelByAliasOrId(param); + const base = await Base.get(model.base_id); + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + // todo: Should have error http status code + const message = await baseModel.hasLTARData(param.rowId, model); + if (message.length) { + return { message }; + } + return await baseModel.delByPk(param.rowId, null, param.cookie); +} + +export async function getDataList(param: { + model: Model; + view: View; + query: any; +}) { + const { model, view, query = {} } = param; + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const requestObj = await getAst({ model, query, view }); + + const listArgs: any = { ...query }; + try { + listArgs.filterArr = JSON.parse(listArgs.filterArrJson); + } catch (e) {} + try { + listArgs.sortArr = JSON.parse(listArgs.sortArrJson); + } catch (e) {} + + let data = []; + let count = 0; + try { + data = await nocoExecute( + requestObj, + await baseModel.list(listArgs), + {}, + listArgs + ); + count = await baseModel.count(listArgs); + } catch (e) { + console.log(e); + NcError.internalServerError( + 'Internal Server Error, check server log for more details' + ); + } + + return new PagedResponseImpl(data, { + ...query, + count, + }); +} + +export async function getFindOne(param: { + model: Model; + view: View; + query: any; +}) { + const { model, view, query = {} } = param; + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const args: any = { ...query }; + try { + args.filterArr = JSON.parse(args.filterArrJson); + } catch (e) {} + try { + args.sortArr = JSON.parse(args.sortArrJson); + } catch (e) {} + + const data = await baseModel.findOne(args); + return data + ? await nocoExecute( + await getAst({ model, query: args, view }), + data, + {}, + {} + ) + : {}; +} + +export async function getDataGroupBy(param: { + model: Model; + view: View; + query?: any; +}) { + const { model, view, query = {} } = param; + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const listArgs: any = { ...query }; + const data = await baseModel.groupBy({ ...query }); + const count = await baseModel.count(listArgs); + + return new PagedResponseImpl(data, { + ...query, + count, + }); +} + +export async function dataRead( + param: PathParams & { query: any; rowId: string } +) { + const { model, view } = await getViewAndModelByAliasOrId(param); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const row = await baseModel.readByPk(param.rowId); + + if (!row) { + NcError.notFound('Row not found'); + } + + return await nocoExecute( + await getAst({ model, query: param.query, view }), + row, + {}, + param.query + ); +} + +export async function dataExist( + param: PathParams & { rowId: string; query: any } +) { + const { model, view } = await getViewAndModelByAliasOrId(param); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + return await baseModel.exist(param.rowId); +} + +// todo: Handle the error case where view doesnt belong to model +export async function groupedDataList( + param: PathParams & { query: any; columnId: string } +) { + const { model, view } = await getViewAndModelByAliasOrId(param); + const groupedData = await getGroupedDataList({ + model, + view, + query: param.query, + columnId: param.columnId, + }); + return groupedData; +} + +export async function getGroupedDataList(param: { + model; + view: View; + query: any; + columnId: string; +}) { + const { model, view, query = {} } = param; + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const requestObj = await getAst({ model, query, view }); + + const listArgs: any = { ...query }; + try { + listArgs.filterArr = JSON.parse(listArgs.filterArrJson); + } catch (e) {} + try { + listArgs.sortArr = JSON.parse(listArgs.sortArrJson); + } catch (e) {} + try { + listArgs.options = JSON.parse(listArgs.optionsArrJson); + } catch (e) {} + + let data = []; + + const groupedData = await baseModel.groupedList({ + ...listArgs, + groupColumnId: param.columnId, + }); + data = await nocoExecute( + { key: 1, value: requestObj }, + groupedData, + {}, + listArgs + ); + const countArr = await baseModel.groupedListCount({ + ...listArgs, + groupColumnId: param.columnId, + }); + data = data.map((item) => { + // todo: use map to avoid loop + const count = + countArr.find((countItem: any) => countItem.key === item.key)?.count ?? 0; + + item.value = new PagedResponseImpl(item.value, { + ...query, + count: count, + }); + return item; + }); + + return data; +} + +export async function dataListByViewId(param: { viewId: string; query: any }) { + const view = await View.get(param.viewId); + + const model = await Model.getByIdOrName({ + id: view?.fk_model_id || param.viewId, + }); + + if (!model) NcError.notFound('Table not found'); + + return await getDataList({ model, view, query: param.query }); +} + +export async function mmList(param: { + viewId: string; + colId: string; + query: any; + rowId: string; +}) { + const view = await View.get(param.viewId); + + const model = await Model.getByIdOrName({ + id: view?.fk_model_id || param.viewId, + }); + + if (!model) NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const key = `${model.title}List`; + const requestObj: any = { + [key]: 1, + }; + + const data = ( + await nocoExecute( + requestObj, + { + [key]: async (args) => { + return await baseModel.mmList( + { + colId: param.colId, + parentId: param.rowId, + }, + args + ); + }, + }, + {}, + + { nested: { [key]: param.query } } + ) + )?.[key]; + + const count: any = await baseModel.mmListCount({ + colId: param.colId, + parentId: param.rowId, + }); + + return new PagedResponseImpl(data, { + count, + ...param.query, + }); +} + +export async function mmExcludedList(param: { + viewId: string; + colId: string; + query: any; + rowId: string; +}) { + const view = await View.get(param.viewId); + + const model = await Model.getByIdOrName({ + id: view?.fk_model_id || param.viewId, + }); + + if (!model) NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const key = 'List'; + const requestObj: any = { + [key]: 1, + }; + + const data = ( + await nocoExecute( + requestObj, + { + [key]: async (args) => { + return await baseModel.getMmChildrenExcludedList( + { + colId: param.colId, + pid: param.rowId, + }, + args + ); + }, + }, + {}, + + { nested: { [key]: param.query } } + ) + )?.[key]; + + const count = await baseModel.getMmChildrenExcludedListCount( + { + colId: param.colId, + pid: param.rowId, + }, + param.query + ); + + return new PagedResponseImpl(data, { + count, + ...param.query, + }); +} + +export async function hmExcludedList(param: { + viewId: string; + colId: string; + query: any; + rowId: string; +}) { + const view = await View.get(param.viewId); + + const model = await Model.getByIdOrName({ + id: view?.fk_model_id || param.viewId, + }); + + if (!model) NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const key = 'List'; + const requestObj: any = { + [key]: 1, + }; + + const data = ( + await nocoExecute( + requestObj, + { + [key]: async (args) => { + return await baseModel.getHmChildrenExcludedList( + { + colId: param.colId, + pid: param.rowId, + }, + args + ); + }, + }, + {}, + + { nested: { [key]: param.query } } + ) + )?.[key]; + + const count = await baseModel.getHmChildrenExcludedListCount( + { + colId: param.colId, + pid: param.rowId, + }, + param.query + ); + + return new PagedResponseImpl(data, { + count, + ...param.query, + }); +} + +export async function btExcludedList(param: { + viewId: string; + colId: string; + query: any; + rowId: string; +}) { + const view = await View.get(param.viewId); + + const model = await Model.getByIdOrName({ + id: view?.fk_model_id || param.viewId, + }); + + if (!model) return NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const key = 'List'; + const requestObj: any = { + [key]: 1, + }; + + const data = ( + await nocoExecute( + requestObj, + { + [key]: async (args) => { + return await baseModel.getBtChildrenExcludedList( + { + colId: param.colId, + cid: param.rowId, + }, + args + ); + }, + }, + {}, + + { nested: { [key]: param.query } } + ) + )?.[key]; + + const count = await baseModel.getBtChildrenExcludedListCount( + { + colId: param.colId, + cid: param.rowId, + }, + param.query + ); + + return new PagedResponseImpl(data, { + count, + ...param.query, + }); +} + +export async function hmList(param: { + viewId: string; + colId: string; + query: any; + rowId: string; +}) { + const view = await View.get(param.viewId); + + const model = await Model.getByIdOrName({ + id: view?.fk_model_id || param.viewId, + }); + + if (!model) NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const key = `${model.title}List`; + const requestObj: any = { + [key]: 1, + }; + + const data = ( + await nocoExecute( + requestObj, + { + [key]: async (args) => { + return await baseModel.hmList( + { + colId: param.colId, + id: param.rowId, + }, + args + ); + }, + }, + {}, + { nested: { [key]: param.query } } + ) + )?.[key]; + + const count = await baseModel.hmListCount({ + colId: param.colId, + id: param.rowId, + }); + + return new PagedResponseImpl(data, { + totalRows: count, + } as any); +} + +export async function dataReadByViewId(param: { + viewId: string; + rowId: string; + query: any; +}) { + try { + const model = await Model.getByIdOrName({ + id: param.viewId, + }); + if (!model) NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + return await nocoExecute( + await getAst({ model, query: param.query }), + await baseModel.readByPk(param.rowId), + {}, + {} + ); + } catch (e) { + console.log(e); + NcError.internalServerError( + 'Internal Server Error, check server log for more details' + ); + } +} + +export async function dataInsertByViewId(param: { + viewId: string; + body: any; + cookie: any; +}) { + const model = await Model.getByIdOrName({ + id: param.viewId, + }); + if (!model) return NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + return await baseModel.insert(param.body, null, param.cookie); +} + +export async function dataUpdateByViewId(param: { + viewId: string; + rowId: string; + body: any; + cookie: any; +}) { + const model = await Model.getByIdOrName({ + id: param.viewId, + }); + if (!model) NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + return await baseModel.updateByPk( + param.rowId, + param.body, + null, + param.cookie + ); +} + +export async function dataDeleteByViewId(param: { + viewId: string; + rowId: string; + cookie: any; +}) { + const model = await Model.getByIdOrName({ + id: param.viewId, + }); + if (!model) NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + return await baseModel.delByPk(param.rowId, null, param.cookie); +} + +export async function relationDataDelete(param: { + viewId: string; + colId: string; + childId: string; + rowId: string; + cookie: any; +}) { + const view = await View.get(param.viewId); + + const model = await Model.getByIdOrName({ + id: view?.fk_model_id || param.viewId, + }); + + if (!model) NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + await baseModel.removeChild({ + colId: param.colId, + childId: param.childId, + rowId: param.rowId, + cookie: param.cookie, + }); + + return true; +} + +export async function relationDataAdd(param: { + viewId: string; + colId: string; + childId: string; + rowId: string; + cookie: any; +}) { + const view = await View.get(param.viewId); + + const model = await Model.getByIdOrName({ + id: view?.fk_model_id || param.viewId, + }); + + if (!model) NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + await baseModel.addChild({ + colId: param.colId, + childId: param.childId, + rowId: param.rowId, + cookie: param.cookie, + }); + + return true; +} + +export * from './helpers'; diff --git a/packages/nocodb/src/lib/services/ee/orgTokenService.ts b/packages/nocodb/src/lib/services/ee/orgTokenService.ts new file mode 100644 index 0000000000..9dd491144c --- /dev/null +++ b/packages/nocodb/src/lib/services/ee/orgTokenService.ts @@ -0,0 +1,20 @@ +import { OrgUserRoles, UserType } from 'nocodb-sdk'; +import { PagedResponseImpl } from '../../meta/helpers/PagedResponse'; +import { ApiToken } from '../../models'; + +export async function apiTokenListEE(param: { user: UserType; query: any }) { + let fk_user_id = param.user.id; + + // if super admin get all tokens + if (param.user.roles.includes(OrgUserRoles.SUPER_ADMIN)) { + fk_user_id = undefined; + } + + return new PagedResponseImpl( + await ApiToken.listWithCreatedBy({ ...param.query, fk_user_id }), + { + ...(param.query || {}), + count: await ApiToken.count({}), + } + ); +} diff --git a/packages/nocodb/src/lib/services/filterService.ts b/packages/nocodb/src/lib/services/filterService.ts new file mode 100644 index 0000000000..351d312378 --- /dev/null +++ b/packages/nocodb/src/lib/services/filterService.ts @@ -0,0 +1,74 @@ +import { FilterReqType } from 'nocodb-sdk'; +import { validatePayload } from '../meta/api/helpers'; +import Filter from '../models/Filter'; +import { T } from 'nc-help'; + +export async function hookFilterCreate(param: { + filter: FilterReqType; + hookId: any; +}) { + validatePayload('swagger.json#/components/schemas/FilterReq', param.filter); + + const filter = await Filter.insert({ + ...param.filter, + fk_hook_id: param.hookId, + }); + + T.emit('evt', { evt_type: 'hookFilter:created' }); + return filter; +} + +export async function hookFilterList(param: { hookId: any }) { + return Filter.rootFilterListByHook({ hookId: param.hookId }); +} + +export async function filterDelete(param: { filterId: string }) { + await Filter.delete(param.filterId); + T.emit('evt', { evt_type: 'filter:deleted' }); + return true; +} + +export async function filterCreate(param: { + filter: FilterReqType; + viewId: string; +}) { + validatePayload('swagger.json#/components/schemas/FilterReq', param.filter); + + const filter = await Filter.insert({ + ...param.filter, + fk_view_id: param.viewId, + }); + + T.emit('evt', { evt_type: 'filter:created' }); + + return filter; +} +export async function filterUpdate(param: { + filter: FilterReqType; + filterId: string; +}) { + validatePayload('swagger.json#/components/schemas/FilterReq', param.filter); + + // todo: type correction + const filter = await Filter.update(param.filterId, param.filter as Filter); + + T.emit('evt', { evt_type: 'filter:updated' }); + + return filter; +} + +export async function filterChildrenList(param: { filterId: any }) { + return Filter.parentFilterList({ + parentId: param.filterId, + }); +} + +export async function filterGet(param: { filterId: string }) { + const filter = await Filter.get(param.filterId); + return filter; +} + +export async function filterList(param: { viewId: string }) { + const filter = await Filter.rootFilterList({ viewId: param.viewId }); + return filter; +} diff --git a/packages/nocodb/src/lib/services/formViewColumnService.ts b/packages/nocodb/src/lib/services/formViewColumnService.ts new file mode 100644 index 0000000000..ecceec1f81 --- /dev/null +++ b/packages/nocodb/src/lib/services/formViewColumnService.ts @@ -0,0 +1,19 @@ +import { validatePayload } from '../meta/api/helpers'; +import { FormViewColumn } from '../models'; +import { T } from 'nc-help'; +export async function columnUpdate(param: { + formViewColumnId: string; + // todo: replace with FormColumnReq + formViewColumn: FormViewColumn; +}) { + validatePayload( + 'swagger.json#/components/schemas/FormColumnReq', + param.formViewColumn + ); + + T.emit('evt', { evt_type: 'formViewColumn:updated' }); + return await FormViewColumn.update( + param.formViewColumnId, + param.formViewColumn + ); +} diff --git a/packages/nocodb/src/lib/services/formViewService.ts b/packages/nocodb/src/lib/services/formViewService.ts new file mode 100644 index 0000000000..94c6f5c9a5 --- /dev/null +++ b/packages/nocodb/src/lib/services/formViewService.ts @@ -0,0 +1,36 @@ +import { T } from 'nc-help'; +import { FormReqType, ViewTypes } from 'nocodb-sdk'; +import { validatePayload } from '../meta/api/helpers'; +import { FormView, View } from '../models'; + +export async function formViewGet(param: { formViewId: string }) { + const formViewData = await FormView.getWithInfo(param.formViewId); + return formViewData; +} + +export async function formViewCreate(param: { + tableId: string; + body: FormReqType; +}) { + validatePayload('swagger.json#/components/schemas/FormCreateReq', param.body); + + T.emit('evt', { evt_type: 'vtable:created', show_as: 'form' }); + const view = await View.insert({ + ...param.body, + // todo: sanitize + fk_model_id: param.tableId, + type: ViewTypes.FORM, + }); + return view; +} + +// @ts-ignore +export async function formViewUpdate(param: { + formViewId: string; + body: FormReqType; +}) { + validatePayload('swagger.json#/components/schemas/FormReq', param.body); + + T.emit('evt', { evt_type: 'view:updated', type: 'grid' }); + await FormView.update(param.formViewId, param.body); +} diff --git a/packages/nocodb/src/lib/services/galleryViewService.ts b/packages/nocodb/src/lib/services/galleryViewService.ts new file mode 100644 index 0000000000..8c564fa74d --- /dev/null +++ b/packages/nocodb/src/lib/services/galleryViewService.ts @@ -0,0 +1,34 @@ +import { GalleryReqType, ViewTypes } from 'nocodb-sdk'; +import { T } from 'nc-help'; +import { validatePayload } from '../meta/api/helpers'; +import { GalleryView, View } from '../models'; + +export async function galleryViewGet(param: { galleryViewId: string }) { + return await GalleryView.get(param.galleryViewId); +} + +export async function galleryViewCreate(param: { + tableId: string; + gallery: GalleryReqType; +}) { + validatePayload('swagger.json#/components/schemas/GalleryReq', param.gallery); + + T.emit('evt', { evt_type: 'vtable:created', show_as: 'gallery' }); + const view = await View.insert({ + ...param.gallery, + // todo: sanitize + fk_model_id: param.tableId, + type: ViewTypes.GALLERY, + }); + return view; +} + +export async function galleryViewUpdate(param: { + galleryViewId: string; + gallery: GalleryReqType; +}) { + validatePayload('swagger.json#/components/schemas/GalleryReq', param.gallery); + + T.emit('evt', { evt_type: 'view:updated', type: 'gallery' }); + await GalleryView.update(param.galleryViewId, param.gallery); +} diff --git a/packages/nocodb/src/lib/services/gridViewColumnService.ts b/packages/nocodb/src/lib/services/gridViewColumnService.ts new file mode 100644 index 0000000000..70d5d3df76 --- /dev/null +++ b/packages/nocodb/src/lib/services/gridViewColumnService.ts @@ -0,0 +1,18 @@ +import { GridColumnReqType } from 'nocodb-sdk'; +import { validatePayload } from '../meta/api/helpers'; +import GridViewColumn from '../models/GridViewColumn'; +import { T } from 'nc-help'; + +export async function columnList(param: { gridViewId: string }) { + return await GridViewColumn.list(param.gridViewId); +} + +export async function gridColumnUpdate(param: { + gridViewColumnId: string; + grid: GridColumnReqType; +}) { + validatePayload('swagger.json#/components/schemas/GridColumnReq', param.grid); + + T.emit('evt', { evt_type: 'gridViewColumn:updated' }); + return await GridViewColumn.update(param.gridViewColumnId, param.grid); +} diff --git a/packages/nocodb/src/lib/services/gridViewService.ts b/packages/nocodb/src/lib/services/gridViewService.ts new file mode 100644 index 0000000000..92d01ced57 --- /dev/null +++ b/packages/nocodb/src/lib/services/gridViewService.ts @@ -0,0 +1,30 @@ +import { T } from 'nc-help'; +import { GridReqType, ViewTypes } from 'nocodb-sdk'; +import { validatePayload } from '../meta/api/helpers'; +import { View } from '../models'; +import { GridView } from '../models'; + +export async function gridViewCreate(param: { + tableId: string; + grid: GridReqType; +}) { + validatePayload('swagger.json#/components/schemas/GridReq', param.grid); + + const view = await View.insert({ + ...param.grid, + // todo: sanitize + fk_model_id: param.tableId, + type: ViewTypes.GRID, + }); + T.emit('evt', { evt_type: 'vtable:created', show_as: 'grid' }); + return view; +} + +// todo: json schema validation +export async function gridViewUpdate(param: { + viewId: string; + grid: GridReqType; +}) { + T.emit('evt', { evt_type: 'view:updated', type: 'grid' }); + return await GridView.update(param.viewId, param.grid); +} diff --git a/packages/nocodb/src/lib/services/hookFilterService.ts b/packages/nocodb/src/lib/services/hookFilterService.ts new file mode 100644 index 0000000000..b8b1e04a42 --- /dev/null +++ b/packages/nocodb/src/lib/services/hookFilterService.ts @@ -0,0 +1,66 @@ +import { T } from 'nc-help'; +import { FilterReqType } from 'nocodb-sdk'; +import { validatePayload } from '../meta/api/helpers'; +import Filter from '../models/Filter'; + +export async function filterGet(param: { hookId: string }) { + const filter = await Filter.getFilterObject({ hookId: param.hookId }); + + return filter; +} + +export async function filterList(param: { hookId: string }) { + const filters = await Filter.rootFilterListByHook({ + hookId: param.hookId, + }); + + return filters; +} + +export async function filterChildrenRead(param: { + hookId: string; + filterParentId: string; +}) { + const filter = await Filter.parentFilterListByHook({ + hookId: param.hookId, + parentId: param.filterParentId, + }); + + return filter; +} + +export async function filterCreate(param: { + hookId: string; + filter: FilterReqType; +}) { + validatePayload('swagger.json#/components/schemas/FilterReq', param.filter); + + const filter = await Filter.insert({ + ...param.filter, + fk_hook_id: param.hookId, + }); + + T.emit('evt', { evt_type: 'hookFilter:created' }); + return filter; +} + +export async function filterUpdate(param: { + hookId: string; + filterId: string; + filter: FilterReqType; +}) { + validatePayload('swagger.json#/components/schemas/FilterReq', param.filter); + + const filter = await Filter.update(param.filterId, { + ...param.filter, + fk_hook_id: param.hookId, + } as Filter); + T.emit('evt', { evt_type: 'hookFilter:updated' }); + return filter; +} + +export async function filterDelete(param: { filterId: string }) { + await Filter.delete(param.filterId); + T.emit('evt', { evt_type: 'hookFilter:deleted' }); + return true; +} diff --git a/packages/nocodb/src/lib/services/hookService.ts b/packages/nocodb/src/lib/services/hookService.ts new file mode 100644 index 0000000000..9acf6d4b48 --- /dev/null +++ b/packages/nocodb/src/lib/services/hookService.ts @@ -0,0 +1,79 @@ +import { T } from 'nc-help'; +import { validatePayload } from '../meta/api/helpers'; +import { Hook, Model } from '../models'; +import { HookReqType, HookTestReqType } from 'nocodb-sdk'; + +import { invokeWebhook } from '../meta/helpers/webhookHelpers'; +import populateSamplePayload from '../meta/helpers/populateSamplePayload'; + +export async function hookList(param: { tableId: string }) { + // todo: pagination + return await Hook.list({ fk_model_id: param.tableId }); +} + +export async function hookCreate(param: { + tableId: string; + hook: HookReqType; +}) { + validatePayload('swagger.json#/components/schemas/HookReq', param.hook); + + T.emit('evt', { evt_type: 'webhooks:created' }); + // todo: type correction + const hook = await Hook.insert({ + ...param.hook, + fk_model_id: param.tableId, + } as any); + return hook; +} + +export async function hookDelete(param: { hookId: string }) { + T.emit('evt', { evt_type: 'webhooks:deleted' }); + await Hook.delete(param.hookId); + return true; +} + +export async function hookUpdate(param: { hookId: string; hook: HookReqType }) { + validatePayload('swagger.json#/components/schemas/HookReq', param.hook); + + T.emit('evt', { evt_type: 'webhooks:updated' }); + + // todo: correction in swagger + return await Hook.update(param.hookId, param.hook as any); +} + +export async function hookTest(param: { + tableId: string; + hookTest: HookTestReqType; +}) { + validatePayload( + 'swagger.json#/components/schemas/HookTestReq', + param.hookTest + ); + + const model = await Model.getByIdOrName({ id: param.tableId }); + + const { + hook, + payload: { data, user }, + } = param.hookTest; + await invokeWebhook( + new Hook(hook), + model, + data, + user, + (hook as any)?.filters, + true + ); + + T.emit('evt', { evt_type: 'webhooks:tested' }); + + return true; +} +export async function tableSampleData(param: { + tableId: string; + operation: 'insert' | 'update'; +}) { + const model = await Model.getByIdOrName({ id: param.tableId }); + + return await populateSamplePayload(model, false, param.operation); +} diff --git a/packages/nocodb/src/lib/services/index.ts b/packages/nocodb/src/lib/services/index.ts new file mode 100644 index 0000000000..c58049aa62 --- /dev/null +++ b/packages/nocodb/src/lib/services/index.ts @@ -0,0 +1,38 @@ +export * as projectService from './projectService'; +export * as tableService from './tableService'; +export * as columnService from './columnService'; +export * as filterService from './filterService'; +export * as sortService from './sortService'; +export * as baseService from './baseService'; +export * as apiTokenService from './apiTokenService'; +export * as viewService from './viewService'; +export * as hookService from './hookService'; +export * as pluginService from './pluginService'; +export * as utilService from './utilService'; +export * as formViewService from './formViewService'; +export * as formViewColumnService from './formViewColumnService'; +export * as gridViewService from './gridViewService'; +export * as galleryViewService from './galleryViewService'; +export * as kanbanViewService from './kanbanViewService'; +export * as gridViewColumnService from './gridViewColumnService'; +export * as viewColumnService from './viewColumnService'; +export * as metaDiffService from './metaDiffService'; +export * as mapViewService from './mapViewService'; +export * as modelVisibilityService from './modelVisibilityService'; +export * as sharedBaseService from './sharedBaseService'; +export * as orgUserService from './orgUserService'; +export * as orgLicenseService from './orgLicenseService'; +export * as projectUserService from './projectUserService'; +export * as attachmentService from './attachmentService'; +export * as hookFilterService from './hookFilterService'; +export * as dataService from './dataService'; +export * as bulkDataService from './dataService/bulkData'; +export * as dataAliasNestedService from './dataService/dataAliasNestedService'; +export * as cacheService from './cacheService'; +export * as auditService from './auditService'; +export * as swaggerService from './swaggerService'; +export * as userService from './userService'; +export * as syncService from './syncService'; +export * from './public'; +export * as orgTokenService from './orgTokenService'; +export * as orgTokenServiceEE from './ee/orgTokenService'; diff --git a/packages/nocodb/src/lib/services/kanbanViewService.ts b/packages/nocodb/src/lib/services/kanbanViewService.ts new file mode 100644 index 0000000000..e6561ae323 --- /dev/null +++ b/packages/nocodb/src/lib/services/kanbanViewService.ts @@ -0,0 +1,35 @@ +import { KanbanReqType, ViewTypes } from 'nocodb-sdk'; +import { validatePayload } from '../meta/api/helpers'; +import { KanbanView, View } from '../models'; +import { T } from 'nc-help'; + +export async function kanbanViewGet(param: { kanbanViewId: string }) { + return await KanbanView.get(param.kanbanViewId); +} + +export async function kanbanViewCreate(param: { + tableId: string; + kanban: KanbanReqType; +}) { + validatePayload('swagger.json#/components/schemas/KanbanReq', param.kanban), + T.emit('evt', { evt_type: 'vtable:created', show_as: 'kanban' }); + const view = await View.insert({ + ...param.kanban, + // todo: sanitize + fk_model_id: param.tableId, + type: ViewTypes.KANBAN, + }); + return view; +} + +export async function kanbanViewUpdate(param: { + kanbanViewId: string; + kanban: KanbanReqType; +}) { + validatePayload( + 'swagger.json#/components/schemas/KanbanUpdateReq', + param.kanban + ), + T.emit('evt', { evt_type: 'view:updated', type: 'kanban' }); + return await KanbanView.update(param.kanbanViewId, param.kanban); +} diff --git a/packages/nocodb/src/lib/services/mapViewService.ts b/packages/nocodb/src/lib/services/mapViewService.ts new file mode 100644 index 0000000000..199baa9d13 --- /dev/null +++ b/packages/nocodb/src/lib/services/mapViewService.ts @@ -0,0 +1,33 @@ +import { MapType, ViewTypes } from 'nocodb-sdk'; +import View from '../models/View'; +import { T } from 'nc-help'; +import MapView from '../models/MapView'; + +export async function mapViewGet(param: { mapViewId: string }) { + return await MapView.get(param.mapViewId); +} + +export async function mapViewCreate(param: { + tableId: string; + // todo: add MapReq in schema + map: MapType; +}) { + T.emit('evt', { evt_type: 'vtable:created', show_as: 'map' }); + const view = await View.insert({ + ...param.map, + // todo: sanitize + fk_model_id: param.tableId, + type: ViewTypes.MAP, + }); + return view; +} + +export async function mapViewUpdate(param: { + mapViewId: string; + // todo: add MapReq in schema + map: MapType; +}) { + T.emit('evt', { evt_type: 'view:updated', type: 'map' }); + // todo: type correction + return await MapView.update(param.mapViewId, param.map as any); +} diff --git a/packages/nocodb/src/lib/meta/api/metaDiffApis.ts b/packages/nocodb/src/lib/services/metaDiffService.ts similarity index 93% rename from packages/nocodb/src/lib/meta/api/metaDiffApis.ts rename to packages/nocodb/src/lib/services/metaDiffService.ts index 35f8118e21..274ba8e705 100644 --- a/packages/nocodb/src/lib/meta/api/metaDiffApis.ts +++ b/packages/nocodb/src/lib/services/metaDiffService.ts @@ -1,22 +1,23 @@ // // Project CRUD -import { Tele } from 'nc-help'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import Model from '../../models/Model'; -import Project from '../../models/Project'; -import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; +import { T } from 'nc-help'; +import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; import { isVirtualCol, ModelTypes, RelationTypes, UITypes } from 'nocodb-sdk'; -import { Router } from 'express'; -import Base from '../../models/Base'; -import ModelXcMetaFactory from '../../db/sql-mgr/code/models/xc/ModelXcMetaFactory'; -import Column from '../../models/Column'; -import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn'; -import { getUniqueColumnAliasName } from '../helpers/getUniqueName'; -import NcHelp from '../../utils/NcHelp'; -import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName'; -import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue'; -import getColumnUiType from '../helpers/getColumnUiType'; -import { metaApiMetrics } from '../helpers/apiMetrics'; +import { + Base, + Column, + LinkToAnotherRecordColumn, + Model, + Project, +} from '../models'; +import ModelXcMetaFactory from '../db/sql-mgr/code/models/xc/ModelXcMetaFactory'; +import { getUniqueColumnAliasName } from '../meta/helpers/getUniqueName'; +import NcHelp from '../utils/NcHelp'; +import getTableNameAlias, { + getColumnNameAlias, +} from '../meta/helpers/getTableName'; +import mapDefaultDisplayValue from '../meta/helpers/mapDefaultDisplayValue'; +import getColumnUiType from '../meta/helpers/getColumnUiType'; export enum MetaDiffType { TABLE_NEW = 'TABLE_NEW', @@ -545,8 +546,8 @@ async function getMetaDiff( return changes; } -export async function metaDiff(req, res) { - const project = await Project.getWithInfo(req.params.projectId); +export async function metaDiff(param: { projectId: string }) { + const project = await Project.getWithInfo(param.projectId); let changes = []; for (const base of project.bases) { try { @@ -558,22 +559,25 @@ export async function metaDiff(req, res) { } } - res.json(changes); + return changes; } -export async function baseMetaDiff(req, res) { - const project = await Project.getWithInfo(req.params.projectId); - const base = await Base.get(req.params.baseId); +export async function baseMetaDiff(param: { + projectId: string; + baseId: string; +}) { + const project = await Project.getWithInfo(param.projectId); + const base = await Base.get(param.baseId); let changes = []; const sqlClient = await NcConnectionMgrv2.getSqlClient(base); changes = await getMetaDiff(sqlClient, project, base); - res.json(changes); + return changes; } -export async function metaDiffSync(req, res) { - const project = await Project.getWithInfo(req.params.projectId); +export async function metaDiffSync(param: { projectId: string }) { + const project = await Project.getWithInfo(param.projectId); for (const base of project.bases) { const virtualColumnInsert: Array<() => Promise> = []; @@ -771,14 +775,17 @@ export async function metaDiffSync(req, res) { await extractAndGenerateManyToManyRelations(await base.getModels()); } - Tele.emit('evt', { evt_type: 'metaDiff:synced' }); + T.emit('evt', { evt_type: 'metaDiff:synced' }); - res.json({ msg: 'success' }); + return true; } -export async function baseMetaDiffSync(req, res) { - const project = await Project.getWithInfo(req.params.projectId); - const base = await Base.get(req.params.baseId); +export async function baseMetaDiffSync(param: { + projectId: string; + baseId: string; +}) { + const project = await Project.getWithInfo(param.projectId); + const base = await Base.get(param.baseId); const virtualColumnInsert: Array<() => Promise> = []; @@ -960,9 +967,9 @@ export async function baseMetaDiffSync(req, res) { // populate m2m relations await extractAndGenerateManyToManyRelations(await base.getModels()); - Tele.emit('evt', { evt_type: 'baseMetaDiff:synced' }); + T.emit('evt', { evt_type: 'baseMetaDiff:synced' }); - res.json({ msg: 'success' }); + return true; } async function isMMRelationExist( @@ -1094,26 +1101,3 @@ export async function extractAndGenerateManyToManyRelations( } } } - -const router = Router(); -router.get( - '/api/v1/db/meta/projects/:projectId/meta-diff', - metaApiMetrics, - ncMetaAclMw(metaDiff, 'metaDiff') -); -router.post( - '/api/v1/db/meta/projects/:projectId/meta-diff', - metaApiMetrics, - ncMetaAclMw(metaDiffSync, 'metaDiffSync') -); -router.get( - '/api/v1/db/meta/projects/:projectId/meta-diff/:baseId', - metaApiMetrics, - ncMetaAclMw(baseMetaDiff, 'baseMetaDiff') -); -router.post( - '/api/v1/db/meta/projects/:projectId/meta-diff/:baseId', - metaApiMetrics, - ncMetaAclMw(baseMetaDiffSync, 'baseMetaDiffSync') -); -export default router; diff --git a/packages/nocodb/src/lib/services/modelVisibilityService.ts b/packages/nocodb/src/lib/services/modelVisibilityService.ts new file mode 100644 index 0000000000..719d3de487 --- /dev/null +++ b/packages/nocodb/src/lib/services/modelVisibilityService.ts @@ -0,0 +1,102 @@ +import { VisibilityRuleReqType } from 'nocodb-sdk'; +import { validatePayload } from '../meta/api/helpers'; +import { NcError } from '../meta/helpers/catchError'; +import ModelRoleVisibility from '../models/ModelRoleVisibility'; +import { T } from 'nc-help'; +import { Model, View } from '../models'; + +export async function xcVisibilityMetaSetAll(param: { + visibilityRule: VisibilityRuleReqType; + projectId: string; +}) { + validatePayload( + 'swagger.json#/components/schemas/VisibilityRuleReq', + param.visibilityRule + ), + T.emit('evt', { evt_type: 'uiAcl:updated' }); + for (const d of param.visibilityRule) { + for (const role of Object.keys(d.disabled)) { + const view = await View.get(d.id); + + if (view.project_id !== param.projectId) { + NcError.badRequest('View does not belong to the project'); + } + + const dataInDb = await ModelRoleVisibility.get({ + role, + fk_view_id: d.id, + }); + if (dataInDb) { + if (d.disabled[role]) { + if (!dataInDb.disabled) { + await ModelRoleVisibility.update(d.id, role, { + disabled: d.disabled[role], + }); + } + } else { + await dataInDb.delete(); + } + } else if (d.disabled[role]) { + await ModelRoleVisibility.insert({ + fk_view_id: d.id, + disabled: d.disabled[role], + role, + }); + } + } + } + T.emit('evt', { evt_type: 'uiAcl:updated' }); + + return true; +} + +export async function xcVisibilityMetaGet(param: { + projectId: string; + includeM2M?: boolean; + models?: Model[]; +}) { + const { includeM2M = true, projectId, models: _models } = param ?? {}; + + // todo: move to + const roles = ['owner', 'creator', 'viewer', 'editor', 'commenter', 'guest']; + + const defaultDisabled = roles.reduce((o, r) => ({ ...o, [r]: false }), {}); + + let models = + _models || + (await Model.list({ + project_id: projectId, + base_id: undefined, + })); + + models = includeM2M ? models : (models.filter((t) => !t.mm) as Model[]); + + const result = await models.reduce(async (_obj, model) => { + const obj = await _obj; + + const views = await model.getViews(); + for (const view of views) { + obj[view.id] = { + ptn: model.table_name, + _ptn: model.title, + ptype: model.type, + tn: view.title, + _tn: view.title, + table_meta: model.meta, + ...view, + disabled: { ...defaultDisabled }, + }; + } + + return obj; + }, Promise.resolve({})); + + const disabledList = await ModelRoleVisibility.list(projectId); + + for (const d of disabledList) { + if (result[d.fk_view_id]) + result[d.fk_view_id].disabled[d.role] = !!d.disabled; + } + + return Object.values(result); +} diff --git a/packages/nocodb/src/lib/services/orgLicenseService.ts b/packages/nocodb/src/lib/services/orgLicenseService.ts new file mode 100644 index 0000000000..d6f8033575 --- /dev/null +++ b/packages/nocodb/src/lib/services/orgLicenseService.ts @@ -0,0 +1,18 @@ +import { NC_LICENSE_KEY } from '../constants'; +import { validatePayload } from '../meta/api/helpers'; +import Store from '../models/Store'; +import Noco from '../Noco'; + +export async function licenseGet() { + const license = await Store.get(NC_LICENSE_KEY); + + return { key: license?.value }; +} + +export async function licenseSet(param: { key: string }) { + validatePayload('swagger.json#/components/schemas/LicenseReq', param); + + await Store.saveOrUpdate({ value: param.key, key: NC_LICENSE_KEY }); + await Noco.loadEEState(); + return true; +} diff --git a/packages/nocodb/src/lib/services/orgTokenService.ts b/packages/nocodb/src/lib/services/orgTokenService.ts new file mode 100644 index 0000000000..fdee8e5fbe --- /dev/null +++ b/packages/nocodb/src/lib/services/orgTokenService.ts @@ -0,0 +1,59 @@ +import { ApiTokenReqType, OrgUserRoles } from 'nocodb-sdk'; +import { validatePayload } from '../meta/api/helpers'; +import { User } from '../models'; +import ApiToken from '../models/ApiToken'; +import { T } from 'nc-help'; +import { NcError } from '../meta/helpers/catchError'; +import { PagedResponseImpl } from '../meta/helpers/PagedResponse'; + +export async function apiTokenList(param: { user: User; query: any }) { + const fk_user_id = param.user.id; + let includeUnmappedToken = false; + if (param.user.roles.includes(OrgUserRoles.SUPER_ADMIN)) { + includeUnmappedToken = true; + } + + return new PagedResponseImpl( + await ApiToken.listWithCreatedBy({ + ...param.query, + fk_user_id, + includeUnmappedToken, + }), + { + ...param.query, + count: await ApiToken.count({ + includeUnmappedToken, + fk_user_id, + }), + } + ); +} + +export async function apiTokenCreate(param: { + user: User; + apiToken: ApiTokenReqType; +}) { + validatePayload( + 'swagger.json#/components/schemas/ApiTokenReq', + param.apiToken + ); + + T.emit('evt', { evt_type: 'org:apiToken:created' }); + return await ApiToken.insert({ + ...param.apiToken, + fk_user_id: param['user'].id, + }); +} + +export async function apiTokenDelete(param: { user: User; token: string }) { + const fk_user_id = param.user.id; + const apiToken = await ApiToken.getByToken(param.token); + if ( + !param.user.roles.includes(OrgUserRoles.SUPER_ADMIN) && + apiToken.fk_user_id !== fk_user_id + ) { + NcError.notFound('Token not found'); + } + T.emit('evt', { evt_type: 'org:apiToken:deleted' }); + return await ApiToken.delete(param.token); +} diff --git a/packages/nocodb/src/lib/services/orgUserService.ts b/packages/nocodb/src/lib/services/orgUserService.ts new file mode 100644 index 0000000000..b4e2d2adac --- /dev/null +++ b/packages/nocodb/src/lib/services/orgUserService.ts @@ -0,0 +1,271 @@ +import { + AuditOperationSubTypes, + AuditOperationTypes, + PluginCategory, + UserType, +} from 'nocodb-sdk'; +import { v4 as uuidv4 } from 'uuid'; +import validator from 'validator'; +import { OrgUserRoles } from 'nocodb-sdk'; +import { NC_APP_SETTINGS } from '../constants'; +import { validatePayload } from '../meta/api/helpers'; +import { Audit, ProjectUser, Store, SyncSource, User } from '../models'; +import Noco from '../Noco'; +import { MetaTable } from '../utils/globals'; +import { T } from 'nc-help'; +import { NcError } from '../meta/helpers/catchError'; +import { extractProps } from '../meta/helpers/extractProps'; +import { PagedResponseImpl } from '../meta/helpers/PagedResponse'; +import { randomTokenString } from '../meta/helpers/stringHelpers'; +import { sendInviteEmail } from './projectUserService'; + +export async function userList(param: { + // todo: add better typing + query: Record; +}) { + const { query = {} } = param; + + return new PagedResponseImpl(await User.list(query), { + ...query, + count: await User.count(query), + }); +} + +export async function userUpdate(param: { + // todo: better typing + user: Partial; + userId: string; +}) { + validatePayload('swagger.json#/components/schemas/OrgUserReq', param.user); + + const updateBody = extractProps(param.user, ['roles']); + + const user = await User.get(param.userId); + + if (user.roles.includes(OrgUserRoles.SUPER_ADMIN)) { + NcError.badRequest('Cannot update super admin roles'); + } + + return await User.update(param.userId, { + ...updateBody, + token_version: null, + }); +} + +export async function userDelete(param: { userId: string }) { + const ncMeta = await Noco.ncMeta.startTransaction(); + try { + const user = await User.get(param.userId, ncMeta); + + if (user.roles.includes(OrgUserRoles.SUPER_ADMIN)) { + NcError.badRequest('Cannot delete super admin'); + } + + // delete project user entry and assign to super admin + const projectUsers = await ProjectUser.getProjectsIdList( + param.userId, + ncMeta + ); + + // todo: clear cache + + // TODO: assign super admin as project owner + for (const projectUser of projectUsers) { + await ProjectUser.delete( + projectUser.project_id, + projectUser.fk_user_id, + ncMeta + ); + } + + // delete sync source entry + await SyncSource.deleteByUserId(param.userId, ncMeta); + + // delete user + await User.delete(param.userId, ncMeta); + await ncMeta.commit(); + } catch (e) { + await ncMeta.rollback(e); + throw e; + } + + return true; +} + +export async function userAdd(param: { + user: UserType; + projectId: string; + // todo: refactor + req: any; +}) { + validatePayload('swagger.json#/components/schemas/OrgUserReq', param.user); + + // allow only viewer or creator role + if ( + param.user.roles && + ![OrgUserRoles.VIEWER, OrgUserRoles.CREATOR].includes( + param.user.roles as OrgUserRoles + ) + ) { + NcError.badRequest('Invalid role'); + } + + // extract emails from request body + const emails = (param.user.email || '') + .toLowerCase() + .split(/\s*,\s*/) + .map((v) => v.trim()); + + // check for invalid emails + const invalidEmails = emails.filter((v) => !validator.isEmail(v)); + + if (!emails.length) { + return NcError.badRequest('Invalid email address'); + } + if (invalidEmails.length) { + NcError.badRequest('Invalid email address : ' + invalidEmails.join(', ')); + } + + const invite_token = uuidv4(); + const error = []; + + for (const email of emails) { + // add user to project if user already exist + const user = await User.getByEmail(email); + + if (user) { + NcError.badRequest('User already exist'); + } else { + try { + // create new user with invite token + await User.insert({ + invite_token, + invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000), + email, + roles: param.user.roles || OrgUserRoles.VIEWER, + token_version: randomTokenString(), + }); + + const count = await User.count(); + T.emit('evt', { evt_type: 'org:user:invite', count }); + + await Audit.insert({ + op_type: AuditOperationTypes.ORG_USER, + op_sub_type: AuditOperationSubTypes.INVITE, + user: param.req.user.email, + description: `invited ${email} to ${param.projectId} project `, + ip: param.req.clientIp, + }); + // in case of single user check for smtp failure + // and send back token if failed + if ( + emails.length === 1 && + !(await sendInviteEmail(email, invite_token, param.req)) + ) { + return { invite_token, email }; + } else { + sendInviteEmail(email, invite_token, param.req); + } + } catch (e) { + console.log(e); + if (emails.length === 1) { + throw e; + } else { + error.push({ email, error: e.message }); + } + } + } + } + + if (emails.length === 1) { + return { + msg: 'success', + }; + } else { + return { invite_token, emails, error }; + } +} + +export async function userSettings(_param): Promise { + NcError.notImplemented(); +} + +export async function userInviteResend(param: { + userId: string; + req: any; +}): Promise { + const user = await User.get(param.userId); + + if (!user) { + NcError.badRequest(`User with id '${param.userId}' not found`); + } + + const invite_token = uuidv4(); + + await User.update(user.id, { + invite_token, + invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000), + }); + + const pluginData = await Noco.ncMeta.metaGet2(null, null, MetaTable.PLUGIN, { + category: PluginCategory.EMAIL, + active: true, + }); + + if (!pluginData) { + NcError.badRequest( + `No Email Plugin is found. Please go to App Store to configure first or copy the invitation URL to users instead.` + ); + } + + await sendInviteEmail(user.email, invite_token, param.req); + + await Audit.insert({ + op_type: AuditOperationTypes.ORG_USER, + op_sub_type: AuditOperationSubTypes.RESEND_INVITE, + user: user.email, + description: `resent a invite to ${user.email} `, + ip: param.req.clientIp, + }); + + return true; +} + +export async function generateResetUrl(param: { + userId: string; + siteUrl: string; +}) { + const user = await User.get(param.userId); + + if (!user) { + NcError.badRequest(`User with id '${param.userId}' not found`); + } + const token = uuidv4(); + await User.update(user.id, { + email: user.email, + reset_password_token: token, + reset_password_expires: new Date(Date.now() + 60 * 60 * 1000), + token_version: null, + }); + + return { + reset_password_token: token, + reset_password_url: param.siteUrl + `/auth/password/reset/${token}`, + }; +} + +export async function appSettingsGet() { + let settings = {}; + try { + settings = JSON.parse((await Store.get(NC_APP_SETTINGS))?.value); + } catch {} + return settings; +} + +export async function appSettingsSet(param: { settings: any }) { + await Store.saveOrUpdate({ + value: JSON.stringify(param.settings), + key: NC_APP_SETTINGS, + }); + return true; +} diff --git a/packages/nocodb/src/lib/services/pluginService.ts b/packages/nocodb/src/lib/services/pluginService.ts new file mode 100644 index 0000000000..67302d8ce7 --- /dev/null +++ b/packages/nocodb/src/lib/services/pluginService.ts @@ -0,0 +1,36 @@ +import { T } from 'nc-help'; +import { validatePayload } from '../meta/api/helpers'; +import { Plugin } from '../models'; +import { PluginTestReqType, PluginType } from 'nocodb-sdk'; +import NcPluginMgrv2 from '../meta/helpers/NcPluginMgrv2'; + +export async function pluginList() { + return await Plugin.list(); +} + +export async function pluginTest(param: { body: PluginTestReqType }) { + validatePayload('swagger.json#/components/schemas/PluginTestReq', param.body); + + T.emit('evt', { evt_type: 'plugin:tested' }); + return await NcPluginMgrv2.test(param.body); +} + +export async function pluginRead(param: { pluginId: string }) { + return await Plugin.get(param.pluginId); +} +export async function pluginUpdate(param: { + pluginId: string; + plugin: PluginType; +}) { + validatePayload('swagger.json#/components/schemas/PluginReq', param.plugin); + + const plugin = await Plugin.update(param.pluginId, param.plugin); + T.emit('evt', { + evt_type: plugin.active ? 'plugin:installed' : 'plugin:uninstalled', + title: plugin.title, + }); + return plugin; +} +export async function isPluginActive(param: { pluginTitle: string }) { + return await Plugin.isPluginActive(param.pluginTitle); +} diff --git a/packages/nocodb/src/lib/services/projectService.ts b/packages/nocodb/src/lib/services/projectService.ts new file mode 100644 index 0000000000..08a848f7a5 --- /dev/null +++ b/packages/nocodb/src/lib/services/projectService.ts @@ -0,0 +1,171 @@ +import DOMPurify from 'isomorphic-dompurify'; +import { OrgUserRoles, ProjectReqType } from 'nocodb-sdk'; +import { promisify } from 'util'; +import { populateMeta, validatePayload } from '../meta/api/helpers'; +import { extractPropsAndSanitize } from '../meta/helpers/extractProps'; +import syncMigration from '../meta/helpers/syncMigration'; +import Project from '../models/Project'; +import ProjectUser from '../models/ProjectUser'; +import { customAlphabet } from 'nanoid'; +import Noco from '../Noco'; +import NcConfigFactory from '../utils/NcConfigFactory'; +import { NcError } from '../meta/helpers/catchError'; + +export async function projectCreate(param: { + project: ProjectReqType; + user: any; +}) { + validatePayload('swagger.json#/components/schemas/ProjectReq', param.project); + + const projectBody: ProjectReqType & Record = param.project; + if (!projectBody.external) { + const ranId = nanoid(); + projectBody.prefix = `nc_${ranId}__`; + projectBody.is_meta = true; + if (process.env.NC_MINIMAL_DBS) { + // if env variable NC_MINIMAL_DBS is set, then create a SQLite file/connection for each project + // each file will be named as nc_.db + const fs = require('fs'); + const toolDir = NcConfigFactory.getToolDir(); + const nanoidv2 = customAlphabet( + '1234567890abcdefghijklmnopqrstuvwxyz', + 14 + ); + if (!(await promisify(fs.exists)(`${toolDir}/nc_minimal_dbs`))) { + await promisify(fs.mkdir)(`${toolDir}/nc_minimal_dbs`); + } + const dbId = nanoidv2(); + const projectTitle = DOMPurify.sanitize(projectBody.title); + projectBody.prefix = ''; + projectBody.bases = [ + { + type: 'sqlite3', + config: { + client: 'sqlite3', + connection: { + client: 'sqlite3', + database: projectTitle, + connection: { + filename: `${toolDir}/nc_minimal_dbs/${projectTitle}_${dbId}.db`, + }, + }, + }, + inflection_column: 'camelize', + inflection_table: 'camelize', + }, + ]; + } else { + const db = Noco.getConfig().meta?.db; + projectBody.bases = [ + { + type: db?.client, + config: null, + is_meta: true, + inflection_column: 'camelize', + inflection_table: 'camelize', + }, + ]; + } + } else { + if (process.env.NC_CONNECT_TO_EXTERNAL_DB_DISABLED) { + NcError.badRequest('Connecting to external db is disabled'); + } + projectBody.is_meta = false; + } + + if (projectBody?.title.length > 50) { + NcError.badRequest('Project title exceeds 50 characters'); + } + + if (await Project.getByTitle(projectBody?.title)) { + NcError.badRequest('Project title already in use'); + } + + projectBody.title = DOMPurify.sanitize(projectBody.title); + projectBody.slug = projectBody.title; + + const project = await Project.createProject(projectBody); + await ProjectUser.insert({ + fk_user_id: (param as any).user.id, + project_id: project.id, + roles: 'owner', + }); + + await syncMigration(project); + + // populate metadata if existing table + for (const base of await project.getBases()) { + const info = await populateMeta(base, project); + + T.emit('evt_api_created', info); + delete base.config; + } + + T.emit('evt', { + evt_type: 'project:created', + xcdb: !projectBody.external, + }); + + T.emit('evt', { evt_type: 'project:rest' }); + + return project; +} + +const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 4); + +import { T } from 'nc-help'; + +export async function getProjectWithInfo(param: { projectId: string }) { + const project = await Project.getWithInfo(param.projectId); + return project; +} + +export async function projectSoftDelete(param: { projectId: any }) { + await Project.softDelete(param.projectId); + T.emit('evt', { evt_type: 'project:deleted' }); + return true; +} + +export function sanitizeProject(project: any) { + const sanitizedProject = { ...project }; + sanitizedProject.bases?.forEach((b: any) => { + ['config'].forEach((k) => delete b[k]); + }); + return sanitizedProject; +} + +export async function projectUpdate(param: { + projectId: string; + project: ProjectReqType; +}) { + const project = await Project.getWithInfo(param.projectId); + + const data: Partial = extractPropsAndSanitize( + param?.project as Project, + ['title', 'meta', 'color'] + ); + + if ( + data?.title && + project.title !== data.title && + (await Project.getByTitle(data.title)) + ) { + NcError.badRequest('Project title already in use'); + } + + const result = await Project.update(param.projectId, data); + T.emit('evt', { evt_type: 'project:update' }); + + return result; +} + +export async function projectList(param: { + user: { id: string; roles: string }; + query?: any; +}) { + const projects = param.user?.roles?.includes(OrgUserRoles.SUPER_ADMIN) + ? await Project.list(param.query) + : await ProjectUser.getProjectsList(param.user.id, param.query); + + return projects; +} diff --git a/packages/nocodb/src/lib/services/projectUserService.ts b/packages/nocodb/src/lib/services/projectUserService.ts new file mode 100644 index 0000000000..675ba744a6 --- /dev/null +++ b/packages/nocodb/src/lib/services/projectUserService.ts @@ -0,0 +1,325 @@ +import { OrgUserRoles, ProjectUserReqType } from 'nocodb-sdk'; +import { T } from 'nc-help'; +import { validatePayload } from '../meta/api/helpers'; +import { PagedResponseImpl } from '../meta/helpers/PagedResponse'; +import ProjectUser from '../models/ProjectUser'; +import validator from 'validator'; +import { NcError } from '../meta/helpers/catchError'; +import { v4 as uuidv4 } from 'uuid'; +import User from '../models/User'; +import Audit from '../models/Audit'; +import NocoCache from '../cache/NocoCache'; +import { CacheGetType, CacheScope, MetaTable } from '../utils/globals'; +import * as ejs from 'ejs'; +import NcPluginMgrv2 from '../meta/helpers/NcPluginMgrv2'; +import Noco from '../Noco'; +import { PluginCategory } from 'nocodb-sdk'; +import { randomTokenString } from '../meta/helpers/stringHelpers'; + +export async function userList(param: { projectId: string; query: any }) { + return new PagedResponseImpl( + await ProjectUser.getUsersList({ + ...param.query, + project_id: param.projectId, + }), + { + ...param.query, + count: await ProjectUser.getUsersCount(param.query), + } + ); +} + +export async function userInvite(param: { + projectId: string; + projectUser: ProjectUserReqType; + req: any; +}): Promise { + validatePayload( + 'swagger.json#/components/schemas/ProjectUserReq', + param.projectUser + ); + + const emails = (param.projectUser.email || '') + .toLowerCase() + .split(/\s*,\s*/) + .map((v) => v.trim()); + + // check for invalid emails + const invalidEmails = emails.filter((v) => !validator.isEmail(v)); + if (!emails.length) { + return NcError.badRequest('Invalid email address'); + } + if (invalidEmails.length) { + NcError.badRequest('Invalid email address : ' + invalidEmails.join(', ')); + } + + const invite_token = uuidv4(); + const error = []; + + for (const email of emails) { + // add user to project if user already exist + const user = await User.getByEmail(email); + + if (user) { + // check if this user has been added to this project + const projectUser = await ProjectUser.get(param.projectId, user.id); + if (projectUser) { + NcError.badRequest( + `${user.email} with role ${projectUser.roles} already exists in this project` + ); + } + + await ProjectUser.insert({ + project_id: param.projectId, + fk_user_id: user.id, + roles: param.projectUser.roles || 'editor', + }); + + const cachedUser = await NocoCache.get( + `${CacheScope.USER}:${email}___${param.projectId}`, + CacheGetType.TYPE_OBJECT + ); + + if (cachedUser) { + cachedUser.roles = param.projectUser.roles || 'editor'; + await NocoCache.set( + `${CacheScope.USER}:${email}___${param.projectId}`, + cachedUser + ); + } + + await Audit.insert({ + project_id: param.projectId, + op_type: 'AUTHENTICATION', + op_sub_type: 'INVITE', + user: param.req.user.email, + description: `invited ${email} to ${param.projectId} project `, + ip: param.req.clientIp, + }); + } else { + try { + // create new user with invite token + const { id } = await User.insert({ + invite_token, + invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000), + email, + roles: OrgUserRoles.VIEWER, + token_version: randomTokenString(), + }); + + // add user to project + await ProjectUser.insert({ + project_id: param.projectId, + fk_user_id: id, + roles: param.projectUser.roles, + }); + + const count = await User.count(); + T.emit('evt', { evt_type: 'project:invite', count }); + + await Audit.insert({ + project_id: param.projectId, + op_type: 'AUTHENTICATION', + op_sub_type: 'INVITE', + user: param.req.user.email, + description: `invited ${email} to ${param.projectId} project `, + ip: param.req.clientIp, + }); + // in case of single user check for smtp failure + // and send back token if failed + if ( + emails.length === 1 && + !(await sendInviteEmail(email, invite_token, param.req)) + ) { + return { invite_token, email }; + } else { + sendInviteEmail(email, invite_token, param.req); + } + } catch (e) { + console.log(e); + if (emails.length === 1) { + throw e; + } else { + error.push({ email, error: e.message }); + } + } + } + } + + if (emails.length === 1) { + return { + msg: 'success', + }; + } else { + return { invite_token, emails, error }; + } +} + +export async function projectUserUpdate(param: { + userId: string; + // todo: update swagger + projectUser: ProjectUserReqType & { project_id: string }; + // todo: refactor + req: any; + projectId: string; +}): Promise { + validatePayload( + 'swagger.json#/components/schemas/ProjectUserReq', + param.projectUser + ); + + // todo: use param.projectId + if (!param.projectUser?.project_id) { + NcError.badRequest('Missing project id in request body.'); + } + + if ( + param.req.session?.passport?.user?.roles?.owner && + param.req.session?.passport?.user?.id === param.userId && + param.projectUser.roles.indexOf('owner') === -1 + ) { + NcError.badRequest("Super admin can't remove Super role themselves"); + } + const user = await User.get(param.userId); + + if (!user) { + NcError.badRequest(`User with id '${param.userId}' doesn't exist`); + } + + // todo: handle roles which contains super + if ( + !param.req.session?.passport?.user?.roles?.owner && + param.projectUser.roles.indexOf('owner') > -1 + ) { + NcError.forbidden('Insufficient privilege to add super admin role.'); + } + + await ProjectUser.update( + param.projectId, + param.userId, + param.projectUser.roles + ); + + await Audit.insert({ + op_type: 'AUTHENTICATION', + op_sub_type: 'ROLES_MANAGEMENT', + user: param.req.user.email, + description: `updated roles for ${user.email} with ${param.projectUser.roles} `, + ip: param.req.clientIp, + }); + + return { + msg: 'User details updated successfully', + }; +} + +export async function projectUserDelete(param: { + projectId: string; + userId: string; + // todo: refactor + req: any; +}): Promise { + const project_id = param.projectId; + + if (param.req.session?.passport?.user?.id === param.userId) { + NcError.badRequest("Admin can't delete themselves!"); + } + + if (!param.req.session?.passport?.user?.roles?.owner) { + const user = await User.get(param.userId); + if (user.roles?.split(',').includes('super')) + NcError.forbidden('Insufficient privilege to delete a super admin user.'); + + const projectUser = await ProjectUser.get(project_id, param.userId); + if (projectUser?.roles?.split(',').includes('super')) + NcError.forbidden('Insufficient privilege to delete a owner user.'); + } + + await ProjectUser.delete(project_id, param.userId); + return true; +} + +export async function projectUserInviteResend(param: { + userId: string; + projectUser: ProjectUserReqType; + projectId: string; + // todo: refactor + req: any; +}): Promise { + const user = await User.get(param.userId); + + if (!user) { + NcError.badRequest(`User with id '${param.userId}' not found`); + } + + param.projectUser.roles = user.roles; + const invite_token = uuidv4(); + + await User.update(user.id, { + invite_token, + invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000), + }); + + const pluginData = await Noco.ncMeta.metaGet2(null, null, MetaTable.PLUGIN, { + category: PluginCategory.EMAIL, + active: true, + }); + + if (!pluginData) { + NcError.badRequest( + `No Email Plugin is found. Please go to App Store to configure first or copy the invitation URL to users instead.` + ); + } + + await sendInviteEmail(user.email, invite_token, param.req); + + await Audit.insert({ + op_type: 'AUTHENTICATION', + op_sub_type: 'RESEND_INVITE', + user: user.email, + description: `resent a invite to ${user.email} `, + ip: param.req.clientIp, + project_id: param.projectId, + }); + + return true; +} + +// todo: refactor the whole function +export async function sendInviteEmail( + email: string, + token: string, + req: any +): Promise { + try { + const template = (await import('./userService/ui/emailTemplates/invite')) + .default; + + const emailAdapter = await NcPluginMgrv2.emailAdapter(); + + if (emailAdapter) { + await emailAdapter.mailSend({ + to: email, + subject: 'Verify email', + html: ejs.render(template, { + signupLink: `${req.ncSiteUrl}${ + Noco.getConfig()?.dashboardPath + }#/signup/${token}`, + projectName: req.body?.projectName, + roles: (req.body?.roles || '') + .split(',') + .map((r) => r.replace(/^./, (m) => m.toUpperCase())) + .join(', '), + adminEmail: req.session?.passport?.user?.email, + }), + }); + return true; + } + } catch (e) { + console.log( + 'Warning : `mailSend` failed, Please configure emailClient configuration.', + e.message + ); + throw e; + } +} diff --git a/packages/nocodb/src/lib/services/public/index.ts b/packages/nocodb/src/lib/services/public/index.ts new file mode 100644 index 0000000000..b24c8caab5 --- /dev/null +++ b/packages/nocodb/src/lib/services/public/index.ts @@ -0,0 +1,5 @@ +import * as publicDataService from './publicDataService'; +import * as publicDataExportApis from './publicDataExportService'; +import * as publicMetaService from './publicMetaservice'; + +export { publicDataService, publicDataExportApis, publicMetaService }; diff --git a/packages/nocodb/src/lib/services/public/publicDataExportService.ts b/packages/nocodb/src/lib/services/public/publicDataExportService.ts new file mode 100644 index 0000000000..900dd3ed88 --- /dev/null +++ b/packages/nocodb/src/lib/services/public/publicDataExportService.ts @@ -0,0 +1,162 @@ +import { nocoExecute } from 'nc-help'; +import { isSystemColumn, UITypes } from 'nocodb-sdk'; +import getAst from '../../db/sql-data-mapper/lib/sql/helpers/getAst'; +import { NcError } from '../../meta/helpers/catchError'; +import { + Base, + Column, + LinkToAnotherRecordColumn, + LookupColumn, + Model, + View, +} from '../../models'; +import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; + +export async function getDbRows(param: { + model; + view: View; + query: any; + offset?: number; +}) { + param.view.model.columns = param.view.columns + .filter((c) => c.show) + .map( + (c) => + new Column({ + ...c, + ...param.view.model.columnsById[c.fk_column_id], + } as any) + ) + .filter( + (column) => !isSystemColumn(column) || param.view.show_system_fields + ); + + if (!param.model) NcError.notFound('Table not found'); + + const listArgs: any = { ...param.query }; + try { + listArgs.filterArr = JSON.parse(listArgs.filterArrJson); + } catch (e) {} + try { + listArgs.sortArr = JSON.parse(listArgs.sortArrJson); + } catch (e) {} + + const base = await Base.get(param.model.base_id); + const baseModel = await Model.getBaseModelSQL({ + id: param.model.id, + viewId: param.view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const requestObj = await getAst({ + query: param.query, + model: param.model, + view: param.view, + includePkByDefault: false, + }); + + let offset = +param.offset || 0; + const limit = 100; + // const size = +process.env.NC_EXPORT_MAX_SIZE || 1024; + const timeout = +process.env.NC_EXPORT_MAX_TIMEOUT || 5000; + const dbRows = []; + const startTime = process.hrtime(); + let elapsed, temp; + + for ( + elapsed = 0; + elapsed < timeout; + offset += limit, + temp = process.hrtime(startTime), + elapsed = temp[0] * 1000 + temp[1] / 1000000 + ) { + const rows = await nocoExecute( + requestObj, + await baseModel.list({ ...listArgs, offset, limit }), + {}, + listArgs + ); + + if (!rows?.length) { + offset = -1; + break; + } + + for (const row of rows) { + const dbRow = { ...row }; + + for (const column of param.view.model.columns) { + dbRow[column.title] = await serializeCellValue({ + value: row[column.title], + column, + }); + } + dbRows.push(dbRow); + } + } + return { offset, dbRows, elapsed }; +} + +export async function serializeCellValue({ + value, + column, +}: { + column?: Column; + value: any; +}) { + if (!column) { + return value; + } + + if (!value) return value; + + switch (column?.uidt) { + case UITypes.Attachment: { + let data = value; + try { + if (typeof value === 'string') { + data = JSON.parse(value); + } + } catch {} + + return (data || []).map( + (attachment) => + `${encodeURI(attachment.title)}(${encodeURI(attachment.url)})` + ); + } + case UITypes.Lookup: + { + const colOptions = await column.getColOptions(); + const lookupColumn = await colOptions.getLookupColumn(); + return ( + await Promise.all( + [...(Array.isArray(value) ? value : [value])].map(async (v) => + serializeCellValue({ + value: v, + column: lookupColumn, + }) + ) + ) + ).join(', '); + } + break; + case UITypes.LinkToAnotherRecord: + { + const colOptions = + await column.getColOptions(); + const relatedModel = await colOptions.getRelatedTable(); + await relatedModel.getColumns(); + return [...(Array.isArray(value) ? value : [value])] + .map((v) => { + return v[relatedModel.displayValue?.title]; + }) + .join(', '); + } + break; + default: + if (value && typeof value === 'object') { + return JSON.stringify(value); + } + return value; + } +} diff --git a/packages/nocodb/src/lib/services/public/publicDataService.ts b/packages/nocodb/src/lib/services/public/publicDataService.ts new file mode 100644 index 0000000000..238f5e5825 --- /dev/null +++ b/packages/nocodb/src/lib/services/public/publicDataService.ts @@ -0,0 +1,463 @@ +import { nocoExecute } from 'nc-help'; +import { ErrorMessages, UITypes, ViewTypes } from 'nocodb-sdk'; +import path from 'path'; +import { nanoid } from 'nanoid'; +import slash from 'slash'; +import getAst from '../../db/sql-data-mapper/lib/sql/helpers/getAst'; +import { NcError } from '../../meta/helpers/catchError'; +import NcPluginMgrv2 from '../../meta/helpers/NcPluginMgrv2'; +import { PagedResponseImpl } from '../../meta/helpers/PagedResponse'; +import { + Base, + Column, + LinkToAnotherRecordColumn, + Model, + View, +} from '../../models'; +import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; +import { mimeIcons } from '../../utils/mimeTypes'; +import { sanitizeUrlPath } from '../attachmentService'; +import { getColumnByIdOrName } from '../dataService/helpers'; + +export async function dataList(param: { + sharedViewUuid: string; + password?: string; + query: any; +}) { + const view = await View.getByUUID(param.sharedViewUuid); + + if (!view) NcError.notFound('Not found'); + if ( + view.type !== ViewTypes.GRID && + view.type !== ViewTypes.KANBAN && + view.type !== ViewTypes.GALLERY && + view.type !== ViewTypes.MAP + ) { + NcError.notFound('Not found'); + } + + if (view.password && view.password !== param.password) { + return NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); + } + + const model = await Model.getByIdOrName({ + id: view?.fk_model_id, + }); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const listArgs: any = { ...param.query }; + try { + listArgs.filterArr = JSON.parse(listArgs.filterArrJson); + } catch (e) {} + try { + listArgs.sortArr = JSON.parse(listArgs.sortArrJson); + } catch (e) {} + + let data = []; + let count = 0; + + try { + data = await nocoExecute( + await getAst({ + query: param.query, + model, + view, + }), + await baseModel.list(listArgs), + {}, + listArgs + ); + count = await baseModel.count(listArgs); + } catch (e) { + // show empty result instead of throwing error here + // e.g. search some text in a numeric field + } + + return new PagedResponseImpl(data, { ...param.query, count }); +} + +// todo: Handle the error case where view doesnt belong to model +export async function groupedDataList(param: { + sharedViewUuid: string; + password?: string; + query: any; + groupColumnId: string; +}) { + const view = await View.getByUUID(param.sharedViewUuid); + + if (!view) NcError.notFound('Not found'); + + if ( + view.type !== ViewTypes.GRID && + view.type !== ViewTypes.KANBAN && + view.type !== ViewTypes.GALLERY + ) { + NcError.notFound('Not found'); + } + + if (view.password && view.password !== param.password) { + return NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); + } + + const model = await Model.getByIdOrName({ + id: view?.fk_model_id, + }); + + return await getGroupedDataList({ + model, + view, + query: param.query, + groupColumnId: param.groupColumnId, + }); +} + +async function getGroupedDataList(param: { + model: Model; + view: View; + query: any; + groupColumnId: string; +}) { + const { model, view, query = {}, groupColumnId } = param; + const base = await Base.get(param.model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const requestObj = await getAst({ model, query: param.query, view }); + + const listArgs: any = { ...query }; + try { + listArgs.filterArr = JSON.parse(listArgs.filterArrJson); + } catch (e) {} + try { + listArgs.sortArr = JSON.parse(listArgs.sortArrJson); + } catch (e) {} + try { + listArgs.options = JSON.parse(listArgs.optionsArrJson); + } catch (e) {} + + let data = []; + + try { + const groupedData = await baseModel.groupedList({ + ...listArgs, + groupColumnId, + }); + data = await nocoExecute( + { key: 1, value: requestObj }, + groupedData, + {}, + listArgs + ); + const countArr = await baseModel.groupedListCount({ + ...listArgs, + groupColumnId, + }); + data = data.map((item) => { + // todo: use map to avoid loop + const count = + countArr.find((countItem: any) => countItem.key === item.key)?.count ?? + 0; + + item.value = new PagedResponseImpl(item.value, { + ...query, + count: count, + }); + return item; + }); + } catch (e) { + console.log(e); + NcError.internalServerError('Internal Server Error'); + } + return data; +} + +export async function dataInsert(param: { + sharedViewUuid: string; + password?: string; + body: any; + files: any[]; + siteUrl: string; +}) { + const view = await View.getByUUID(param.sharedViewUuid); + + if (!view) NcError.notFound(); + if (view.type !== ViewTypes.FORM) NcError.notFound(); + + if (view.password && view.password !== param.password) { + return NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); + } + + const model = await Model.getByIdOrName({ + id: view?.fk_model_id, + }); + + const base = await Base.get(model.base_id); + const project = await base.getProject(); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + await view.getViewWithInfo(); + await view.getColumns(); + await view.getModelWithInfo(); + await view.model.getColumns(); + + const fields = (view.model.columns = view.columns + .filter((c) => c.show) + .reduce((o, c) => { + o[view.model.columnsById[c.fk_column_id].title] = new Column({ + ...c, + ...view.model.columnsById[c.fk_column_id], + } as any); + return o; + }, {}) as any); + + let body = param?.body; + + if (typeof body === 'string') body = JSON.parse(body); + + const insertObject = Object.entries(body).reduce((obj, [key, val]) => { + if (key in fields) { + obj[key] = val; + } + return obj; + }, {}); + + const attachments = {}; + const storageAdapter = await NcPluginMgrv2.storageAdapter(); + + for (const file of param.files || []) { + // remove `_` prefix and `[]` suffix + const fieldName = file?.fieldname?.replace(/^_|\[\d*]$/g, ''); + + const filePath = sanitizeUrlPath([ + 'v1', + project.title, + model.title, + fieldName, + ]); + + if (fieldName in fields && fields[fieldName].uidt === UITypes.Attachment) { + attachments[fieldName] = attachments[fieldName] || []; + const fileName = `${nanoid(6)}_${file.originalname}`; + let url = await storageAdapter.fileCreate( + slash(path.join('nc', 'uploads', ...filePath, fileName)), + file + ); + + if (!url) { + url = `${param.siteUrl}/download/${filePath.join('/')}/${fileName}`; + } + + attachments[fieldName].push({ + url, + title: file.originalname, + mimetype: file.mimetype, + size: file.size, + icon: mimeIcons[path.extname(file.originalname).slice(1)] || undefined, + }); + } + } + + for (const [column, data] of Object.entries(attachments)) { + insertObject[column] = JSON.stringify(data); + } + + return await baseModel.nestedInsert(insertObject, null); +} + +export async function relDataList(param: { + query: any; + sharedViewUuid: string; + password?: string; + columnId: string; +}) { + const view = await View.getByUUID(param.sharedViewUuid); + + if (!view) NcError.notFound('Not found'); + + if (view.type !== ViewTypes.FORM) NcError.notFound('Not found'); + + if (view.password && view.password !== param.password) { + NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); + } + + const column = await Column.get({ colId: param.columnId }); + const colOptions = await column.getColOptions(); + + const model = await colOptions.getRelatedTable(); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const requestObj = await getAst({ + query: param.query, + model, + extractOnlyPrimaries: true, + }); + + let data = []; + let count = 0; + try { + data = data = await nocoExecute( + requestObj, + await baseModel.list(param.query), + {}, + param.query + ); + count = await baseModel.count(param.query); + } catch (e) { + // show empty result instead of throwing error here + // e.g. search some text in a numeric field + } + + return new PagedResponseImpl(data, { ...param.query, count }); +} + +export async function publicMmList(param: { + query: any; + sharedViewUuid: string; + password?: string; + columnId: string; + rowId: string; +}) { + const view = await View.getByUUID(param.sharedViewUuid); + + if (!view) NcError.notFound('Not found'); + if (view.type !== ViewTypes.GRID) NcError.notFound('Not found'); + + if (view.password && view.password !== param.password) { + NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); + } + + const column = await getColumnByIdOrName( + param.columnId, + await view.getModel() + ); + + if (column.fk_model_id !== view.fk_model_id) + NcError.badRequest("Column doesn't belongs to the model"); + + const base = await Base.get(view.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: view.fk_model_id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const key = `List`; + const requestObj: any = { + [key]: 1, + }; + + const data = ( + await nocoExecute( + requestObj, + { + [key]: async (args) => { + return await baseModel.mmList( + { + colId: param.columnId, + parentId: param.rowId, + }, + args + ); + }, + }, + {}, + + { nested: { [key]: param.query } } + ) + )?.[key]; + + const count: any = await baseModel.mmListCount({ + colId: param.columnId, + parentId: param.rowId, + }); + + return new PagedResponseImpl(data, { ...param.query, count }); +} + +export async function publicHmList(param: { + query: any; + rowId: string; + sharedViewUuid: string; + password?: string; + columnId: string; +}) { + const view = await View.getByUUID(param.sharedViewUuid); + + if (!view) NcError.notFound('Not found'); + if (view.type !== ViewTypes.GRID) NcError.notFound('Not found'); + + if (view.password && view.password !== param.password) { + NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); + } + + const column = await getColumnByIdOrName( + param.columnId, + await view.getModel() + ); + + if (column.fk_model_id !== view.fk_model_id) + NcError.badRequest("Column doesn't belongs to the model"); + + const base = await Base.get(view.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: view.fk_model_id, + viewId: view?.id, + dbDriver: NcConnectionMgrv2.get(base), + }); + + const key = `List`; + const requestObj: any = { + [key]: 1, + }; + + const data = ( + await nocoExecute( + requestObj, + { + [key]: async (args) => { + return await baseModel.hmList( + { + colId: param.columnId, + id: param.rowId, + }, + args + ); + }, + }, + {}, + { nested: { [key]: param.query } } + ) + )?.[key]; + + const count = await baseModel.hmListCount({ + colId: param.columnId, + id: param.rowId, + }); + + return new PagedResponseImpl(data, { ...param.query, count }); +} diff --git a/packages/nocodb/src/lib/meta/api/publicApis/publicMetaApis.ts b/packages/nocodb/src/lib/services/public/publicMetaservice.ts similarity index 65% rename from packages/nocodb/src/lib/meta/api/publicApis/publicMetaApis.ts rename to packages/nocodb/src/lib/services/public/publicMetaservice.ts index 9bb4aaf946..940b03e877 100644 --- a/packages/nocodb/src/lib/meta/api/publicApis/publicMetaApis.ts +++ b/packages/nocodb/src/lib/services/public/publicMetaservice.ts @@ -1,27 +1,31 @@ -import { Request, Response, Router } from 'express'; -import catchError, { NcError } from '../../helpers/catchError'; -import View from '../../../models/View'; -import Model from '../../../models/Model'; import { ErrorMessages, LinkToAnotherRecordType, RelationTypes, UITypes, } from 'nocodb-sdk'; -import Column from '../../../models/Column'; -import Base from '../../../models/Base'; -import Project from '../../../models/Project'; -import LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn'; - -export async function viewMetaGet(req: Request, res: Response) { +import { NcError } from '../../meta/helpers/catchError'; +import { + Base, + Column, + LinkToAnotherRecordColumn, + Model, + Project, + View, +} from '../../models'; + +export async function viewMetaGet(param: { + sharedViewUuid: string; + password: string; +}) { const view: View & { relatedMetas?: { [ket: string]: Model }; client?: string; - } = await View.getByUUID(req.params.sharedViewUuid); + } = await View.getByUUID(param.sharedViewUuid); if (!view) NcError.notFound('Not found'); - if (view.password && view.password !== req.headers?.['xc-password']) { + if (view.password && view.password !== param.password) { NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); } @@ -81,26 +85,16 @@ export async function viewMetaGet(req: Request, res: Response) { view.relatedMetas = relatedMetas; - res.json(view); + return view; } -async function publicSharedBaseGet(req, res): Promise { - const project = await Project.getByUuid(req.params.sharedBaseUuid); +export async function publicSharedBaseGet(param: { + sharedBaseUuid: string; +}): Promise { + const project = await Project.getByUuid(param.sharedBaseUuid); if (!project) { NcError.notFound(); } - res.json({ project_id: project.id }); + return { project_id: project.id }; } - -const router = Router({ mergeParams: true }); -router.get( - '/api/v1/db/public/shared-view/:sharedViewUuid/meta', - catchError(viewMetaGet) -); - -router.get( - '/api/v1/db/public/shared-base/:sharedBaseUuid/meta', - catchError(publicSharedBaseGet) -); -export default router; diff --git a/packages/nocodb/src/lib/services/sharedBaseService.ts b/packages/nocodb/src/lib/services/sharedBaseService.ts new file mode 100644 index 0000000000..a3549781f4 --- /dev/null +++ b/packages/nocodb/src/lib/services/sharedBaseService.ts @@ -0,0 +1,112 @@ +import { T } from 'nc-help'; +import { v4 as uuidv4 } from 'uuid'; +import { validatePayload } from '../meta/api/helpers'; +import Project from '../models/Project'; +import { NcError } from '../meta/helpers/catchError'; +// todo: load from config +const config = { + dashboardPath: '/nc', +}; + +export async function createSharedBaseLink(param: { + projectId: string; + roles: string; + password: string; + siteUrl: string; +}): Promise { + validatePayload('swagger.json#/components/schemas/SharedBaseReq', param); + + const project = await Project.get(param.projectId); + + let roles = param?.roles; + if (!roles || (roles !== 'editor' && roles !== 'viewer')) { + roles = 'viewer'; + } + + if (!project) { + NcError.badRequest('Invalid project id'); + } + + const data: any = { + uuid: uuidv4(), + password: param?.password, + roles, + }; + + await Project.update(project.id, data); + + data.url = `${param.siteUrl}${config.dashboardPath}#/nc/base/${data.uuid}`; + delete data.password; + T.emit('evt', { evt_type: 'sharedBase:generated-link' }); + return data; +} + +export async function updateSharedBaseLink(param: { + projectId: string; + roles: string; + password: string; + siteUrl: string; +}): Promise { + validatePayload('swagger.json#/components/schemas/SharedBaseReq', param); + + const project = await Project.get(param.projectId); + + let roles = param.roles; + if (!roles || (roles !== 'editor' && roles !== 'viewer')) { + roles = 'viewer'; + } + + if (!project) { + NcError.badRequest('Invalid project id'); + } + const data: any = { + uuid: project.uuid || uuidv4(), + password: param.password, + roles, + }; + + await Project.update(project.id, data); + + data.url = `${param.siteUrl}${config.dashboardPath}#/nc/base/${data.uuid}`; + delete data.password; + T.emit('evt', { evt_type: 'sharedBase:generated-link' }); + return data; +} + +export async function disableSharedBaseLink(param: { + projectId: string; +}): Promise { + const project = await Project.get(param.projectId); + + if (!project) { + NcError.badRequest('Invalid project id'); + } + const data: any = { + uuid: null, + }; + + await Project.update(project.id, data); + + T.emit('evt', { evt_type: 'sharedBase:disable-link' }); + + return { uuid: null }; +} + +export async function getSharedBaseLink(param: { + projectId: string; + siteUrl: string; +}): Promise { + const project = await Project.get(param.projectId); + + if (!project) { + NcError.badRequest('Invalid project id'); + } + const data: any = { + uuid: project.uuid, + roles: project.roles, + }; + if (data.uuid) + data.url = `${param.siteUrl}${config.dashboardPath}#/nc/base/${data.shared_base_id}`; + + return data; +} diff --git a/packages/nocodb/src/lib/services/sortService.ts b/packages/nocodb/src/lib/services/sortService.ts new file mode 100644 index 0000000000..c2d05948c1 --- /dev/null +++ b/packages/nocodb/src/lib/services/sortService.ts @@ -0,0 +1,37 @@ +import { SortReqType } from 'nocodb-sdk'; +import { validatePayload } from '../meta/api/helpers'; +import Sort from '../models/Sort'; +import { T } from 'nc-help'; + +export async function sortGet(param: { sortId: string }) { + return Sort.get(param.sortId); +} + +export async function sortDelete(param: { sortId: string }) { + await Sort.delete(param.sortId); + T.emit('evt', { evt_type: 'sort:deleted' }); + return true; +} + +export async function sortUpdate(param: { sortId: any; sort: SortReqType }) { + validatePayload('swagger.json#/components/schemas/SortReq', param.sort); + + const sort = await Sort.update(param.sortId, param.sort); + T.emit('evt', { evt_type: 'sort:updated' }); + return sort; +} + +export async function sortCreate(param: { viewId: any; sort: SortReqType }) { + validatePayload('swagger.json#/components/schemas/SortReq', param.sort); + + const sort = await Sort.insert({ + ...param.sort, + fk_view_id: param.viewId, + } as Sort); + T.emit('evt', { evt_type: 'sort:created' }); + return sort; +} + +export async function sortList(param: { viewId: string }) { + return Sort.list({ viewId: param.viewId }); +} diff --git a/packages/nocodb/src/lib/meta/api/swagger/helpers/getPaths.ts b/packages/nocodb/src/lib/services/swaggerService/getPaths.ts similarity index 88% rename from packages/nocodb/src/lib/meta/api/swagger/helpers/getPaths.ts rename to packages/nocodb/src/lib/services/swaggerService/getPaths.ts index a1d3d0b8f8..042ef515d6 100644 --- a/packages/nocodb/src/lib/meta/api/swagger/helpers/getPaths.ts +++ b/packages/nocodb/src/lib/services/swaggerService/getPaths.ts @@ -1,6 +1,6 @@ -import Noco from '../../../../Noco'; -import Model from '../../../../models/Model'; -import Project from '../../../../models/Project'; +import Noco from '../../Noco'; +import Model from '../../models/Model'; +import Project from '../../models/Project'; import { getModelPaths, getViewPaths } from './templates/paths'; import { SwaggerColumn } from './getSwaggerColumnMetas'; import { SwaggerView } from './getSwaggerJSON'; diff --git a/packages/nocodb/src/lib/meta/api/swagger/helpers/getSchemas.ts b/packages/nocodb/src/lib/services/swaggerService/getSchemas.ts similarity index 88% rename from packages/nocodb/src/lib/meta/api/swagger/helpers/getSchemas.ts rename to packages/nocodb/src/lib/services/swaggerService/getSchemas.ts index a62d19b220..5470c19fa9 100644 --- a/packages/nocodb/src/lib/meta/api/swagger/helpers/getSchemas.ts +++ b/packages/nocodb/src/lib/services/swaggerService/getSchemas.ts @@ -1,6 +1,6 @@ -import Noco from '../../../../Noco'; -import Model from '../../../../models/Model'; -import Project from '../../../../models/Project'; +import Noco from '../../Noco'; +import Model from '../../models/Model'; +import Project from '../../models/Project'; import { getModelSchemas, getViewSchemas } from './templates/schemas'; import { SwaggerColumn } from './getSwaggerColumnMetas'; import { SwaggerView } from './getSwaggerJSON'; diff --git a/packages/nocodb/src/lib/meta/api/swagger/helpers/getSwaggerColumnMetas.ts b/packages/nocodb/src/lib/services/swaggerService/getSwaggerColumnMetas.ts similarity index 83% rename from packages/nocodb/src/lib/meta/api/swagger/helpers/getSwaggerColumnMetas.ts rename to packages/nocodb/src/lib/services/swaggerService/getSwaggerColumnMetas.ts index 6ea1b312d6..45b76e4280 100644 --- a/packages/nocodb/src/lib/meta/api/swagger/helpers/getSwaggerColumnMetas.ts +++ b/packages/nocodb/src/lib/services/swaggerService/getSwaggerColumnMetas.ts @@ -1,9 +1,9 @@ import { UITypes } from 'nocodb-sdk'; -import LinkToAnotherRecordColumn from '../../../../models/LinkToAnotherRecordColumn'; -import SwaggerTypes from '../../../../db/sql-mgr/code/routers/xc-ts/SwaggerTypes'; -import Column from '../../../../models/Column'; -import Noco from '../../../../Noco'; -import Project from '../../../../models/Project'; +import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn'; +import SwaggerTypes from '../../db/sql-mgr/code/routers/xc-ts/SwaggerTypes'; +import Column from '../../models/Column'; +import Noco from '../../Noco'; +import Project from '../../models/Project'; export default async ( columns: Column[], diff --git a/packages/nocodb/src/lib/meta/api/swagger/helpers/getSwaggerJSON.ts b/packages/nocodb/src/lib/services/swaggerService/getSwaggerJSON.ts similarity index 80% rename from packages/nocodb/src/lib/meta/api/swagger/helpers/getSwaggerJSON.ts rename to packages/nocodb/src/lib/services/swaggerService/getSwaggerJSON.ts index 19c09a5514..8d4e81f478 100644 --- a/packages/nocodb/src/lib/meta/api/swagger/helpers/getSwaggerJSON.ts +++ b/packages/nocodb/src/lib/services/swaggerService/getSwaggerJSON.ts @@ -1,15 +1,13 @@ -import FormViewColumn from '../../../../models/FormViewColumn'; -import GalleryViewColumn from '../../../../models/GalleryViewColumn'; -import Noco from '../../../../Noco'; -import Model from '../../../../models/Model'; +import { Model, Project, View } from '../../models'; +import FormViewColumn from '../../models/FormViewColumn'; +import GalleryViewColumn from '../../models/GalleryViewColumn'; +import Noco from '../../Noco'; import swaggerBase from './swagger-base.json'; import getPaths from './getPaths'; import getSchemas from './getSchemas'; -import Project from '../../../../models/Project'; import getSwaggerColumnMetas from './getSwaggerColumnMetas'; import { ViewTypes } from 'nocodb-sdk'; -import GridViewColumn from '../../../../models/GridViewColumn'; -import View from '../../../../models/View'; +import GridViewColumn from '../../models/GridViewColumn'; export default async function getSwaggerJSON( project: Project, diff --git a/packages/nocodb/src/lib/services/swaggerService/index.ts b/packages/nocodb/src/lib/services/swaggerService/index.ts new file mode 100644 index 0000000000..4b46830bf4 --- /dev/null +++ b/packages/nocodb/src/lib/services/swaggerService/index.ts @@ -0,0 +1,37 @@ +import { NcError } from '../../meta/helpers/catchError'; +import Model from '../../models/Model'; +import Project from '../../models/Project'; +import getSwaggerJSON from './getSwaggerJSON'; + +export async function swaggerJson(param: { + projectId: string; + siteUrl: string; +}) { + const project = await Project.get(param.projectId); + + if (!project) NcError.notFound(); + + const models = await Model.list({ + project_id: param.projectId, + base_id: null, + }); + + const swagger = await getSwaggerJSON(project, models); + + swagger.servers = [ + { + url: param.siteUrl, + }, + { + url: '{customUrl}', + variables: { + customUrl: { + default: param.siteUrl, + description: 'Provide custom nocodb app base url', + }, + }, + }, + ] as any; + + return swagger; +} diff --git a/packages/nocodb/src/lib/meta/api/swagger/helpers/swagger-base.json b/packages/nocodb/src/lib/services/swaggerService/swagger-base.json similarity index 100% rename from packages/nocodb/src/lib/meta/api/swagger/helpers/swagger-base.json rename to packages/nocodb/src/lib/services/swaggerService/swagger-base.json diff --git a/packages/nocodb/src/lib/meta/api/swagger/helpers/templates/headers.ts b/packages/nocodb/src/lib/services/swaggerService/templates/headers.ts similarity index 100% rename from packages/nocodb/src/lib/meta/api/swagger/helpers/templates/headers.ts rename to packages/nocodb/src/lib/services/swaggerService/templates/headers.ts diff --git a/packages/nocodb/src/lib/meta/api/swagger/helpers/templates/params.ts b/packages/nocodb/src/lib/services/swaggerService/templates/params.ts similarity index 98% rename from packages/nocodb/src/lib/meta/api/swagger/helpers/templates/params.ts rename to packages/nocodb/src/lib/services/swaggerService/templates/params.ts index 891ecfd10f..698dbc5eb5 100644 --- a/packages/nocodb/src/lib/meta/api/swagger/helpers/templates/params.ts +++ b/packages/nocodb/src/lib/services/swaggerService/templates/params.ts @@ -1,6 +1,6 @@ import { SwaggerColumn } from '../getSwaggerColumnMetas'; import { RelationTypes, UITypes } from 'nocodb-sdk'; -import LinkToAnotherRecordColumn from '../../../../../models/LinkToAnotherRecordColumn'; +import LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn'; export const rowIdParam = { schema: { diff --git a/packages/nocodb/src/lib/meta/api/swagger/helpers/templates/paths.ts b/packages/nocodb/src/lib/services/swaggerService/templates/paths.ts similarity index 100% rename from packages/nocodb/src/lib/meta/api/swagger/helpers/templates/paths.ts rename to packages/nocodb/src/lib/services/swaggerService/templates/paths.ts diff --git a/packages/nocodb/src/lib/meta/api/swagger/helpers/templates/schemas.ts b/packages/nocodb/src/lib/services/swaggerService/templates/schemas.ts similarity index 100% rename from packages/nocodb/src/lib/meta/api/swagger/helpers/templates/schemas.ts rename to packages/nocodb/src/lib/services/swaggerService/templates/schemas.ts diff --git a/packages/nocodb/src/lib/meta/api/sync/helpers/EntityMap.ts b/packages/nocodb/src/lib/services/syncService/helpers/EntityMap.ts similarity index 100% rename from packages/nocodb/src/lib/meta/api/sync/helpers/EntityMap.ts rename to packages/nocodb/src/lib/services/syncService/helpers/EntityMap.ts diff --git a/packages/nocodb/src/lib/meta/api/sync/helpers/NocoSyncDestAdapter.ts b/packages/nocodb/src/lib/services/syncService/helpers/NocoSyncDestAdapter.ts similarity index 100% rename from packages/nocodb/src/lib/meta/api/sync/helpers/NocoSyncDestAdapter.ts rename to packages/nocodb/src/lib/services/syncService/helpers/NocoSyncDestAdapter.ts diff --git a/packages/nocodb/src/lib/meta/api/sync/helpers/NocoSyncSourceAdapter.ts b/packages/nocodb/src/lib/services/syncService/helpers/NocoSyncSourceAdapter.ts similarity index 100% rename from packages/nocodb/src/lib/meta/api/sync/helpers/NocoSyncSourceAdapter.ts rename to packages/nocodb/src/lib/services/syncService/helpers/NocoSyncSourceAdapter.ts diff --git a/packages/nocodb/src/lib/meta/api/sync/helpers/fetchAT.ts b/packages/nocodb/src/lib/services/syncService/helpers/fetchAT.ts similarity index 100% rename from packages/nocodb/src/lib/meta/api/sync/helpers/fetchAT.ts rename to packages/nocodb/src/lib/services/syncService/helpers/fetchAT.ts diff --git a/packages/nocodb/src/lib/meta/api/sync/helpers/job.ts b/packages/nocodb/src/lib/services/syncService/helpers/job.ts similarity index 98% rename from packages/nocodb/src/lib/meta/api/sync/helpers/job.ts rename to packages/nocodb/src/lib/services/syncService/helpers/job.ts index 84ea23d1db..3f09d188ad 100644 --- a/packages/nocodb/src/lib/meta/api/sync/helpers/job.ts +++ b/packages/nocodb/src/lib/services/syncService/helpers/job.ts @@ -1,4 +1,4 @@ -import { Tele } from 'nc-help'; +import { T } from 'nc-help'; import FetchAT from './fetchAT'; import { UITypes } from 'nocodb-sdk'; // import * as sMap from './syncMap'; @@ -11,6 +11,7 @@ import hash from 'object-hash'; import { promisify } from 'util'; import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; import tinycolor from 'tinycolor2'; import { importData, importLTARData } from './readAndProcessData'; @@ -18,6 +19,8 @@ import EntityMap from './EntityMap'; const writeJsonFileAsync = promisify(jsonfile.writeFile); +dayjs.extend(utc); + const selectColors = { // normal blue: '#cfdfff', @@ -909,6 +912,8 @@ export default async ( aTblLinkColumns[i].name + suffix, ncTbl.id ); + + // console.log(res.columns.find(x => x.title === aTblLinkColumns[i].name)) } } } @@ -1497,6 +1502,8 @@ export default async ( }) .eachPage( async function page(records, fetchNextPage) { + // console.log(JSON.stringify(records, null, 2)); + // This function (`page`) will get called for each page of records. // records.forEach(record => callback(table, record)); logBasic( @@ -1924,7 +1931,7 @@ export default async ( }); } - Tele.event({ + T.event({ event: 'a:airtable-import:success', data: { stats: { @@ -1965,7 +1972,6 @@ export default async ( '>=': 'gte', isEmpty: 'empty', isNotEmpty: 'notempty', - isWithin: 'isWithin', contains: 'like', doesNotContain: 'nlike', isAnyOf: 'anyof', @@ -1994,29 +2000,16 @@ export default async ( const datatype = colSchema.uidt; const ncFilters = []; + // console.log(filter) if (datatype === UITypes.Date || datatype === UITypes.DateTime) { - let comparison_op = null; - let comparison_sub_op = null; - let value = null; - if (['isEmpty', 'isNotEmpty'].includes(filter.operator)) { - comparison_op = filter.operator === 'isEmpty' ? 'blank' : 'notblank'; - } else { - if ('numberOfDays' in filter.value) { - value = filter.value['numberOfDays']; - } else if ('exactDate' in filter.value) { - value = filter.value['exactDate']; - } - comparison_op = filterMap[filter.operator]; - comparison_sub_op = filter.value.mode; - } - const fx = { - fk_column_id: columnId, - logical_op: f.conjunction, - comparison_op, - comparison_sub_op, - value, - }; - ncFilters.push(fx); + // skip filters over data datatype + updateMigrationSkipLog( + await sMap.getNcNameFromAtId(viewId), + colSchema.title, + colSchema.uidt, + `filter config skipped; filter over date datatype not supported` + ); + continue; } // single-select & multi-select @@ -2391,7 +2384,7 @@ export default async ( } } catch (e) { if (e.response?.data?.msg) { - Tele.event({ + T.event({ event: 'a:airtable-import:error', data: { error: e.response.data.msg }, }); diff --git a/packages/nocodb/src/lib/meta/api/sync/helpers/readAndProcessData.ts b/packages/nocodb/src/lib/services/syncService/helpers/readAndProcessData.ts similarity index 100% rename from packages/nocodb/src/lib/meta/api/sync/helpers/readAndProcessData.ts rename to packages/nocodb/src/lib/services/syncService/helpers/readAndProcessData.ts diff --git a/packages/nocodb/src/lib/meta/api/sync/helpers/syncMap.ts b/packages/nocodb/src/lib/services/syncService/helpers/syncMap.ts similarity index 100% rename from packages/nocodb/src/lib/meta/api/sync/helpers/syncMap.ts rename to packages/nocodb/src/lib/services/syncService/helpers/syncMap.ts diff --git a/packages/nocodb/src/lib/services/syncService/index.ts b/packages/nocodb/src/lib/services/syncService/index.ts new file mode 100644 index 0000000000..1c1f17b177 --- /dev/null +++ b/packages/nocodb/src/lib/services/syncService/index.ts @@ -0,0 +1,47 @@ +import { T } from 'nc-help'; +import { PagedResponseImpl } from '../../meta/helpers/PagedResponse'; +import { Project, SyncSource } from '../../models'; + +export async function syncSourceList(param: { + projectId: string; + baseId?: string; +}) { + return new PagedResponseImpl( + await SyncSource.list(param.projectId, param.baseId) + ); +} + +export async function syncCreate(param: { + projectId: string; + baseId?: string; + userId: string; + // todo: define type + syncPayload: Partial; +}) { + T.emit('evt', { evt_type: 'webhooks:created' }); + const project = await Project.getWithInfo(param.projectId); + + const sync = await SyncSource.insert({ + ...param.syncPayload, + fk_user_id: param.userId, + base_id: param.baseId ? param.baseId : project.bases[0].id, + project_id: param.projectId, + }); + return sync; +} + +export async function syncDelete(param: { syncId: string }) { + T.emit('evt', { evt_type: 'webhooks:deleted' }); + return await SyncSource.delete(param.syncId); +} + +export async function syncUpdate(param: { + syncId: string; + syncPayload: Partial; +}) { + T.emit('evt', { evt_type: 'webhooks:updated' }); + + return await SyncSource.update(param.syncId, param.syncPayload); +} + +export { default as airtableImportJob } from './helpers/job'; diff --git a/packages/nocodb/src/lib/services/tableService.ts b/packages/nocodb/src/lib/services/tableService.ts new file mode 100644 index 0000000000..747f2d57b8 --- /dev/null +++ b/packages/nocodb/src/lib/services/tableService.ts @@ -0,0 +1,464 @@ +import DOMPurify from 'isomorphic-dompurify'; +import { + AuditOperationSubTypes, + AuditOperationTypes, + isVirtualCol, + ModelTypes, + NormalColumnRequestType, + TableReqType, + UITypes, +} from 'nocodb-sdk'; +import ProjectMgrv2 from '../db/sql-mgr/v2/ProjectMgrv2'; +import { validatePayload } from '../meta/api/helpers'; +import { NcError } from '../meta/helpers/catchError'; +import getColumnPropsFromUIDT from '../meta/helpers/getColumnPropsFromUIDT'; +import getColumnUiType from '../meta/helpers/getColumnUiType'; +import getTableNameAlias, { + getColumnNameAlias, +} from '../meta/helpers/getTableName'; +import mapDefaultDisplayValue from '../meta/helpers/mapDefaultDisplayValue'; +import { + Audit, + Column, + LinkToAnotherRecordColumn, + Model, + ModelRoleVisibility, + Project, + User, + View, +} from '../models'; +import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; +import { T } from 'nc-help'; + +export async function tableUpdate(param: { + tableId: any; + table: TableReqType & { project_id?: string }; + projectId?: string; +}) { + const model = await Model.get(param.tableId); + + const project = await Project.getWithInfo( + param.table.project_id || param.projectId + ); + const base = project.bases.find((b) => b.id === model.base_id); + + if (model.project_id !== project.id) { + NcError.badRequest('Model does not belong to project'); + } + + // if meta present update meta and return + // todo: allow user to update meta and other prop in single api call + if ('meta' in param.table) { + await Model.updateMeta(param.tableId, param.table.meta); + + return true; + } + + if (!param.table.table_name) { + NcError.badRequest( + 'Missing table name `table_name` property in request body' + ); + } + + if (base.is_meta && project.prefix) { + if (!param.table.table_name.startsWith(project.prefix)) { + param.table.table_name = `${project.prefix}${param.table.table_name}`; + } + } + + param.table.table_name = DOMPurify.sanitize(param.table.table_name); + + // validate table name + if (/^\s+|\s+$/.test(param.table.table_name)) { + NcError.badRequest( + 'Leading or trailing whitespace not allowed in table names' + ); + } + + if ( + !(await Model.checkTitleAvailable({ + table_name: param.table.table_name, + project_id: project.id, + base_id: base.id, + })) + ) { + NcError.badRequest('Duplicate table name'); + } + + if (!param.table.title) { + param.table.title = getTableNameAlias( + param.table.table_name, + project.prefix, + base + ); + } + + if ( + !(await Model.checkAliasAvailable({ + title: param.table.title, + project_id: project.id, + base_id: base.id, + })) + ) { + NcError.badRequest('Duplicate table alias'); + } + + const sqlMgr = await ProjectMgrv2.getSqlMgr(project); + const sqlClient = await NcConnectionMgrv2.getSqlClient(base); + + let tableNameLengthLimit = 255; + const sqlClientType = sqlClient.knex.clientType(); + if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') { + tableNameLengthLimit = 64; + } else if (sqlClientType === 'pg') { + tableNameLengthLimit = 63; + } else if (sqlClientType === 'mssql') { + tableNameLengthLimit = 128; + } + + if (param.table.table_name.length > tableNameLengthLimit) { + NcError.badRequest(`Table name exceeds ${tableNameLengthLimit} characters`); + } + + await Model.updateAliasAndTableName( + param.tableId, + param.table.title, + param.table.table_name + ); + + await sqlMgr.sqlOpPlus(base, 'tableRename', { + ...param.table, + tn: param.table.table_name, + tn_old: model.table_name, + }); + + T.emit('evt', { evt_type: 'table:updated' }); + return true; +} + +export function reorderTable(param: { tableId: string; order: any }) { + return Model.updateOrder(param.tableId, param.order); +} + +export async function tableDelete(param: { + tableId: string; + user: User; + req?: any; +}) { + const table = await Model.getByIdOrName({ id: param.tableId }); + await table.getColumns(); + + const relationColumns = table.columns.filter( + (c) => c.uidt === UITypes.LinkToAnotherRecord + ); + + if (relationColumns?.length) { + const referredTables = await Promise.all( + relationColumns.map(async (c) => + c + .getColOptions() + .then((opt) => opt.getRelatedTable()) + .then() + ) + ); + NcError.badRequest( + `Table can't be deleted since Table is being referred in following tables : ${referredTables.join( + ', ' + )}. Delete LinkToAnotherRecord columns and try again.` + ); + } + + const project = await Project.getWithInfo(table.project_id); + const base = project.bases.find((b) => b.id === table.base_id); + const sqlMgr = await ProjectMgrv2.getSqlMgr(project); + (table as any).tn = table.table_name; + table.columns = table.columns.filter((c) => !isVirtualCol(c)); + table.columns.forEach((c) => { + (c as any).cn = c.column_name; + }); + + if (table.type === ModelTypes.TABLE) { + await sqlMgr.sqlOpPlus(base, 'tableDelete', table); + } else if (table.type === ModelTypes.VIEW) { + await sqlMgr.sqlOpPlus(base, 'viewDelete', { + ...table, + view_name: table.table_name, + }); + } + + await Audit.insert({ + project_id: project.id, + base_id: base.id, + op_type: AuditOperationTypes.TABLE, + op_sub_type: AuditOperationSubTypes.DELETED, + user: param.user?.email, + description: `Deleted ${table.type} ${table.table_name} with alias ${table.title} `, + ip: param.req?.clientIp, + }).then(() => {}); + + T.emit('evt', { evt_type: 'table:deleted' }); + + return table.delete(); +} + +export async function getTableWithAccessibleViews(param: { + tableId: string; + user: User; +}) { + const table = await Model.getWithInfo({ + id: param.tableId, + }); + + // todo: optimise + const viewList = await xcVisibilityMetaGet(table.project_id, [table]); + + //await View.list(param.tableId) + table.views = viewList.filter((table: any) => { + return Object.keys(param.user?.roles).some( + (role) => param.user?.roles[role] && !table.disabled[role] + ); + }); + + return table; +} + +export async function xcVisibilityMetaGet( + projectId, + _models: Model[] = null, + includeM2M = true + // type: 'table' | 'tableAndViews' | 'views' = 'table' +) { + // todo: move to + const roles = ['owner', 'creator', 'viewer', 'editor', 'commenter', 'guest']; + + const defaultDisabled = roles.reduce((o, r) => ({ ...o, [r]: false }), {}); + + let models = + _models || + (await Model.list({ + project_id: projectId, + base_id: undefined, + })); + + models = includeM2M ? models : (models.filter((t) => !t.mm) as Model[]); + + const result = await models.reduce(async (_obj, model) => { + const obj = await _obj; + + const views = await model.getViews(); + for (const view of views) { + obj[view.id] = { + ptn: model.table_name, + _ptn: model.title, + ptype: model.type, + tn: view.title, + _tn: view.title, + table_meta: model.meta, + ...view, + disabled: { ...defaultDisabled }, + }; + } + + return obj; + }, Promise.resolve({})); + + const disabledList = await ModelRoleVisibility.list(projectId); + + for (const d of disabledList) { + if (result[d.fk_view_id]) + result[d.fk_view_id].disabled[d.role] = !!d.disabled; + } + + return Object.values(result); +} + +export async function getAccessibleTables(param: { + projectId: string; + baseId: string; + includeM2M?: boolean; + roles: Record; +}) { + const viewList = await xcVisibilityMetaGet(param.projectId); + + // todo: optimise + const tableViewMapping = viewList.reduce((o, view: any) => { + o[view.fk_model_id] = o[view.fk_model_id] || 0; + if ( + Object.keys(param.roles).some( + (role) => param.roles[role] && !view.disabled[role] + ) + ) { + o[view.fk_model_id]++; + } + return o; + }, {}); + + const tableList = ( + await Model.list({ + project_id: param.projectId, + base_id: param.baseId, + }) + ).filter((t) => tableViewMapping[t.id]); + + return param.includeM2M + ? tableList + : (tableList.filter((t) => !t.mm) as Model[]); +} + +export async function tableCreate(param: { + projectId: string; + baseId?: string; + table: TableReqType; + user: User; + req?: any; +}) { + validatePayload('swagger.json#/components/schemas/TableReq', param.table); + + const project = await Project.getWithInfo(param.projectId); + let base = project.bases[0]; + + if (param.baseId) { + base = project.bases.find((b) => b.id === param.baseId); + } + + if ( + !param.table.table_name || + (project.prefix && project.prefix === param.table.table_name) + ) { + NcError.badRequest( + 'Missing table name `table_name` property in request body' + ); + } + + if (base.is_meta && project.prefix) { + if (!param.table.table_name.startsWith(project.prefix)) { + param.table.table_name = `${project.prefix}_${param.table.table_name}`; + } + } + + param.table.table_name = DOMPurify.sanitize(param.table.table_name); + + // validate table name + if (/^\s+|\s+$/.test(param.table.table_name)) { + NcError.badRequest( + 'Leading or trailing whitespace not allowed in table names' + ); + } + + if ( + !(await Model.checkTitleAvailable({ + table_name: param.table.table_name, + project_id: project.id, + base_id: base.id, + })) + ) { + NcError.badRequest('Duplicate table name'); + } + + if (!param.table.title) { + param.table.title = getTableNameAlias( + param.table.table_name, + project.prefix, + base + ); + } + + if ( + !(await Model.checkAliasAvailable({ + title: param.table.title, + project_id: project.id, + base_id: base.id, + })) + ) { + NcError.badRequest('Duplicate table alias'); + } + + const sqlMgr = await ProjectMgrv2.getSqlMgr(project); + + const sqlClient = await NcConnectionMgrv2.getSqlClient(base); + + let tableNameLengthLimit = 255; + const sqlClientType = sqlClient.knex.clientType(); + if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') { + tableNameLengthLimit = 64; + } else if (sqlClientType === 'pg') { + tableNameLengthLimit = 63; + } else if (sqlClientType === 'mssql') { + tableNameLengthLimit = 128; + } + + if (param.table.table_name.length > tableNameLengthLimit) { + NcError.badRequest(`Table name exceeds ${tableNameLengthLimit} characters`); + } + + const mxColumnLength = Column.getMaxColumnNameLength(sqlClientType); + + for (const column of param.table.columns) { + if (column.column_name.length > mxColumnLength) { + NcError.badRequest( + `Column name ${column.column_name} exceeds ${mxColumnLength} characters` + ); + } + } + + param.table.columns = param.table.columns?.map((c) => ({ + ...getColumnPropsFromUIDT(c as any, base), + cn: c.column_name, + })); + await sqlMgr.sqlOpPlus(base, 'tableCreate', { + ...param.table, + tn: param.table.table_name, + }); + + const columns: Array< + Omit & { + cn: string; + system?: boolean; + } + > = (await sqlClient.columnList({ tn: param.table.table_name }))?.data?.list; + + const tables = await Model.list({ + project_id: project.id, + base_id: base.id, + }); + + await Audit.insert({ + project_id: project.id, + base_id: base.id, + op_type: AuditOperationTypes.TABLE, + op_sub_type: AuditOperationSubTypes.CREATED, + user: param.user?.email, + description: `created table ${param.table.table_name} with alias ${param.table.title} `, + ip: param.req?.clientIp, + }).then(() => {}); + + mapDefaultDisplayValue(param.table.columns); + + T.emit('evt', { evt_type: 'table:created' }); + + // todo: type correction + const result = await Model.insert(project.id, base.id, { + ...param.table, + columns: columns.map((c, i) => { + const colMetaFromReq = param.table?.columns?.find( + (c1) => c.cn === c1.column_name + ); + return { + ...colMetaFromReq, + uidt: colMetaFromReq?.uidt || c.uidt || getColumnUiType(base, c), + ...c, + dtxp: [UITypes.MultiSelect, UITypes.SingleSelect].includes( + colMetaFromReq.uidt as any + ) + ? colMetaFromReq.dtxp + : c.dtxp, + title: colMetaFromReq?.title || getColumnNameAlias(c.cn, base), + column_name: c.cn, + order: i + 1, + } as NormalColumnRequestType; + }), + order: +(tables?.pop()?.order ?? 0) + 1, + } as any); + + return result; +} diff --git a/packages/nocodb/src/lib/meta/api/userApi/helpers.ts b/packages/nocodb/src/lib/services/userService/helpers.ts similarity index 83% rename from packages/nocodb/src/lib/meta/api/userApi/helpers.ts rename to packages/nocodb/src/lib/services/userService/helpers.ts index e7686b2a81..387b51170e 100644 --- a/packages/nocodb/src/lib/meta/api/userApi/helpers.ts +++ b/packages/nocodb/src/lib/services/userService/helpers.ts @@ -1,7 +1,7 @@ import * as jwt from 'jsonwebtoken'; import crypto from 'crypto'; -import User from '../../../models/User'; -import { NcConfig } from '../../../../interface/config'; +import User from '../../models/User'; +import { NcConfig } from '../../../interface/config'; export function genJwt(user: User, config: NcConfig) { return jwt.sign( diff --git a/packages/nocodb/src/lib/services/userService/index.ts b/packages/nocodb/src/lib/services/userService/index.ts new file mode 100644 index 0000000000..d9e42bd651 --- /dev/null +++ b/packages/nocodb/src/lib/services/userService/index.ts @@ -0,0 +1,300 @@ +import { + PasswordChangeReqType, + PasswordForgotReqType, + PasswordResetReqType, + UserType, + validatePassword, +} from 'nocodb-sdk'; +import { OrgUserRoles } from 'nocodb-sdk'; +import { T } from 'nc-help'; + +import * as ejs from 'ejs'; + +import bcrypt from 'bcryptjs'; +import { promisify } from 'util'; +import { NC_APP_SETTINGS } from '../../constants'; +import { validatePayload } from '../../meta/api/helpers'; +import { NcError } from '../../meta/helpers/catchError'; +import NcPluginMgrv2 from '../../meta/helpers/NcPluginMgrv2'; +import { Audit, Store, User } from '../../models'; +import Noco from '../../Noco'; +import { MetaTable } from '../../utils/globals'; +import { randomTokenString } from './helpers'; + +const { v4: uuidv4 } = require('uuid'); + +export async function registerNewUserIfAllowed({ + firstname, + lastname, + email, + salt, + password, + email_verification_token, +}: { + firstname; + lastname; + email: string; + salt: any; + password; + email_verification_token; +}) { + let roles: string = OrgUserRoles.CREATOR; + + if (await User.isFirst()) { + roles = `${OrgUserRoles.CREATOR},${OrgUserRoles.SUPER_ADMIN}`; + // todo: update in nc_store + // roles = 'owner,creator,editor' + T.emit('evt', { + evt_type: 'project:invite', + count: 1, + }); + } else { + let settings: { invite_only_signup?: boolean } = {}; + try { + settings = JSON.parse((await Store.get(NC_APP_SETTINGS))?.value); + } catch {} + + if (settings?.invite_only_signup) { + NcError.badRequest('Not allowed to signup, contact super admin.'); + } else { + roles = OrgUserRoles.VIEWER; + } + } + + const token_version = randomTokenString(); + + return await User.insert({ + firstname, + lastname, + email, + salt, + password, + email_verification_token, + roles, + token_version, + }); +} + +export async function passwordChange(param: { + body: PasswordChangeReqType; + user: UserType; + req: any; +}): Promise { + validatePayload( + 'swagger.json#/components/schemas/PasswordChangeReq', + param.body + ); + + const { currentPassword, newPassword } = param.body; + + if (!currentPassword || !newPassword) { + return NcError.badRequest('Missing new/old password'); + } + + // validate password and throw error if password is satisfying the conditions + const { valid, error } = validatePassword(newPassword); + + if (!valid) { + NcError.badRequest(`Password : ${error}`); + } + + const user = await User.getByEmail(param.user.email); + + const hashedPassword = await promisify(bcrypt.hash)( + currentPassword, + user.salt + ); + + if (hashedPassword !== user.password) { + return NcError.badRequest('Current password is wrong'); + } + + const salt = await promisify(bcrypt.genSalt)(10); + const password = await promisify(bcrypt.hash)(newPassword, salt); + + await User.update(user.id, { + salt, + password, + email: user.email, + token_version: null, + }); + + await Audit.insert({ + op_type: 'AUTHENTICATION', + op_sub_type: 'PASSWORD_CHANGE', + user: user.email, + description: `changed password `, + ip: param.req?.clientIp, + }); + + return true; +} + +export async function passwordForgot(param: { + body: PasswordForgotReqType; + siteUrl: string; + req: any; +}): Promise { + validatePayload( + 'swagger.json#/components/schemas/ForgotPasswordReq', + param.body + ); + + const _email = param.body.email; + + if (!_email) { + NcError.badRequest('Please enter your email address.'); + } + + const email = _email.toLowerCase(); + const user = await User.getByEmail(email); + + if (user) { + const token = uuidv4(); + await User.update(user.id, { + email: user.email, + reset_password_token: token, + reset_password_expires: new Date(Date.now() + 60 * 60 * 1000), + token_version: null, + }); + try { + const template = (await import('./ui/emailTemplates/forgotPassword')) + .default; + await NcPluginMgrv2.emailAdapter().then((adapter) => + adapter.mailSend({ + to: user.email, + subject: 'Password Reset Link', + text: `Visit following link to update your password : ${param.siteUrl}/auth/password/reset/${token}.`, + html: ejs.render(template, { + resetLink: param.siteUrl + `/auth/password/reset/${token}`, + }), + }) + ); + } catch (e) { + console.log(e); + return NcError.badRequest( + 'Email Plugin is not found. Please contact administrators to configure it in App Store first.' + ); + } + + await Audit.insert({ + op_type: 'AUTHENTICATION', + op_sub_type: 'PASSWORD_FORGOT', + user: user.email, + description: `requested for password reset `, + ip: param.req?.clientIp, + }); + } else { + return NcError.badRequest('Your email has not been registered.'); + } + + return true; +} + +export async function tokenValidate(param: { token: string }): Promise { + const token = param.token; + + const user = await Noco.ncMeta.metaGet(null, null, MetaTable.USERS, { + reset_password_token: token, + }); + + if (!user || !user.email) { + NcError.badRequest('Invalid reset url'); + } + if (new Date(user.reset_password_expires) < new Date()) { + NcError.badRequest('Password reset url expired'); + } + + return true; +} + +export async function passwordReset(param: { + body: PasswordResetReqType; + token: string; + // todo: exclude + req: any; +}): Promise { + validatePayload( + 'swagger.json#/components/schemas/PasswordResetReq', + param.body + ); + + const { token, body, req } = param; + + const user = await Noco.ncMeta.metaGet(null, null, MetaTable.USERS, { + reset_password_token: token, + }); + + if (!user) { + NcError.badRequest('Invalid reset url'); + } + if (user.reset_password_expires < new Date()) { + NcError.badRequest('Password reset url expired'); + } + if (user.provider && user.provider !== 'local') { + NcError.badRequest('Email registered via social account'); + } + + // validate password and throw error if password is satisfying the conditions + const { valid, error } = validatePassword(body.password); + if (!valid) { + NcError.badRequest(`Password : ${error}`); + } + + const salt = await promisify(bcrypt.genSalt)(10); + const password = await promisify(bcrypt.hash)(body.password, salt); + + await User.update(user.id, { + salt, + password, + email: user.email, + reset_password_expires: null, + reset_password_token: '', + token_version: null, + }); + + await Audit.insert({ + op_type: 'AUTHENTICATION', + op_sub_type: 'PASSWORD_RESET', + user: user.email, + description: `did reset password `, + ip: req.clientIp, + }); + + return true; +} + +export async function emailVerification(param: { + token: string; + // todo: exclude + req: any; +}): Promise { + const { token, req } = param; + + const user = await Noco.ncMeta.metaGet(null, null, MetaTable.USERS, { + email_verification_token: token, + }); + + if (!user) { + NcError.badRequest('Invalid verification url'); + } + + await User.update(user.id, { + email: user.email, + email_verification_token: '', + email_verified: true, + }); + + await Audit.insert({ + op_type: 'AUTHENTICATION', + op_sub_type: 'EMAIL_VERIFICATION', + user: user.email, + description: `verified email `, + ip: req.clientIp, + }); + + return true; +} + +export * from './helpers'; +export { default as initAdminFromEnv } from './initAdminFromEnv'; diff --git a/packages/nocodb/src/lib/meta/api/userApi/initAdminFromEnv.ts b/packages/nocodb/src/lib/services/userService/initAdminFromEnv.ts similarity index 96% rename from packages/nocodb/src/lib/meta/api/userApi/initAdminFromEnv.ts rename to packages/nocodb/src/lib/services/userService/initAdminFromEnv.ts index 8f9bfdce74..1a15b94649 100644 --- a/packages/nocodb/src/lib/meta/api/userApi/initAdminFromEnv.ts +++ b/packages/nocodb/src/lib/services/userService/initAdminFromEnv.ts @@ -1,15 +1,14 @@ -import User from '../../../models/User'; import { v4 as uuidv4 } from 'uuid'; import { promisify } from 'util'; import bcrypt from 'bcryptjs'; -import Noco from '../../../Noco'; -import { CacheScope, MetaTable } from '../../../utils/globals'; -import ProjectUser from '../../../models/ProjectUser'; import { validatePassword } from 'nocodb-sdk'; import boxen from 'boxen'; -import NocoCache from '../../../cache/NocoCache'; -import { Tele } from 'nc-help'; +import { T } from 'nc-help'; +import NocoCache from '../../cache/NocoCache'; +import { ProjectUser, User } from '../../models'; +import Noco from '../../Noco'; +import { CacheScope, MetaTable } from '../../utils/globals'; const { isEmail } = require('validator'); const rolesLevel = { owner: 0, creator: 1, editor: 2, commenter: 3, viewer: 4 }; @@ -68,7 +67,7 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) { // if super admin not present if (await User.isFirst(ncMeta)) { // roles = 'owner,creator,editor' - Tele.emit('evt', { + T.emit('evt', { evt_type: 'project:invite', count: 1, }); @@ -126,7 +125,7 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) { ncMeta ); } else { - Tele.emit('evt', { + T.emit('evt', { evt_type: 'project:invite', count: 1, }); diff --git a/packages/nocodb/src/lib/services/userService/ui/auth/emailVerify.ts b/packages/nocodb/src/lib/services/userService/ui/auth/emailVerify.ts new file mode 100644 index 0000000000..f7412b12ba --- /dev/null +++ b/packages/nocodb/src/lib/services/userService/ui/auth/emailVerify.ts @@ -0,0 +1,70 @@ +export default ` + + + NocoDB - Verify Email + + + + + + + +
+ + + + + + Email verified successfully! + + + {{errMsg}} + + + + + + + +
+ + + + + +`; diff --git a/packages/nocodb/src/lib/services/userService/ui/auth/resetPassword.ts b/packages/nocodb/src/lib/services/userService/ui/auth/resetPassword.ts new file mode 100644 index 0000000000..514fc6d739 --- /dev/null +++ b/packages/nocodb/src/lib/services/userService/ui/auth/resetPassword.ts @@ -0,0 +1,108 @@ +export default ` + + + NocoDB - Reset Password + + + + + + + +
+ + + + + + Password reset successful! + + + + + + +
+ + + + + +`; diff --git a/packages/nocodb/src/lib/services/userService/ui/emailTemplates/forgotPassword.ts b/packages/nocodb/src/lib/services/userService/ui/emailTemplates/forgotPassword.ts new file mode 100644 index 0000000000..afb2f5849a --- /dev/null +++ b/packages/nocodb/src/lib/services/userService/ui/emailTemplates/forgotPassword.ts @@ -0,0 +1,171 @@ +export default ` + + + + + Simple Transactional Email + + + + +
+ + + + + + + + +`; diff --git a/packages/nocodb/src/lib/services/userService/ui/emailTemplates/invite.ts b/packages/nocodb/src/lib/services/userService/ui/emailTemplates/invite.ts new file mode 100644 index 0000000000..fc81f9409e --- /dev/null +++ b/packages/nocodb/src/lib/services/userService/ui/emailTemplates/invite.ts @@ -0,0 +1,208 @@ +export default ` + + + + + Simple Transactional Email + + + + + + + + + + + + + +`; diff --git a/packages/nocodb/src/lib/services/userService/ui/emailTemplates/verify.ts b/packages/nocodb/src/lib/services/userService/ui/emailTemplates/verify.ts new file mode 100644 index 0000000000..11702cc659 --- /dev/null +++ b/packages/nocodb/src/lib/services/userService/ui/emailTemplates/verify.ts @@ -0,0 +1,207 @@ +export default ` + + + + + Simple Transactional Email + + + + + + + + + + + + + +`; diff --git a/packages/nocodb/src/lib/meta/api/utilApis.ts b/packages/nocodb/src/lib/services/utilService.ts similarity index 81% rename from packages/nocodb/src/lib/meta/api/utilApis.ts rename to packages/nocodb/src/lib/services/utilService.ts index 8bafe86ce4..b19eca2cf5 100644 --- a/packages/nocodb/src/lib/meta/api/utilApis.ts +++ b/packages/nocodb/src/lib/services/utilService.ts @@ -1,33 +1,30 @@ -// // Project CRUD -import { Request, Response } from 'express'; import { compareVersions, validate } from 'compare-versions'; import { ViewTypes } from 'nocodb-sdk'; -import Project from '../../models/Project'; -import Noco from '../../Noco'; -import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; -import { MetaTable } from '../../utils/globals'; -import { packageVersion } from '../../utils/packageVersion'; -import ncMetaAclMw from '../helpers/ncMetaAclMw'; -import SqlMgrv2 from '../../db/sql-mgr/v2/SqlMgrv2'; +import { Project } from '../models'; +import { NcError } from '../meta/helpers/catchError'; +import Noco from '../Noco'; +import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; +import { MetaTable } from '../utils/globals'; +import { packageVersion } from '../utils/packageVersion'; +import SqlMgrv2 from '../db/sql-mgr/v2/SqlMgrv2'; import NcConfigFactory, { defaultConnectionConfig, -} from '../../utils/NcConfigFactory'; -import User from '../../models/User'; -import catchError from '../helpers/catchError'; +} from '../utils/NcConfigFactory'; +import { User } from '../models'; import axios from 'axios'; -import { NC_ATTACHMENT_FIELD_SIZE } from '../../constants'; +import { NC_ATTACHMENT_FIELD_SIZE } from '../constants'; const versionCache = { releaseVersion: null, lastFetched: null, }; -export async function testConnection(req: Request, res: Response) { - res.json(await SqlMgrv2.testConnection(req.body)); +export async function testConnection(param: { body: any }) { + return await SqlMgrv2.testConnection(param.body); } -export async function appInfo(req: Request, res: Response) { +export async function appInfo(param: { req: { ncSiteUrl: string } }) { const projectHasAdmin = !(await User.isFirst()); const result = { authType: 'jwt', @@ -55,16 +52,16 @@ export async function appInfo(req: Request, res: Response) { ncMin: !!process.env.NC_MIN, teleEnabled: process.env.NC_DISABLE_TELE === 'true' ? false : true, auditEnabled: process.env.NC_DISABLE_AUDIT === 'true' ? false : true, - ncSiteUrl: (req as any).ncSiteUrl, + ncSiteUrl: (param.req as any).ncSiteUrl, ee: Noco.isEE(), ncAttachmentFieldSize: NC_ATTACHMENT_FIELD_SIZE, ncMaxAttachmentsAllowed: +(process.env.NC_MAX_ATTACHMENTS_ALLOWED || 10), }; - res.json(result); + return result; } -export async function versionInfo(_req: Request, res: Response) { +export async function versionInfo() { if ( !versionCache.lastFetched || (versionCache.lastFetched && @@ -94,19 +91,23 @@ export async function versionInfo(_req: Request, res: Response) { releaseVersion: versionCache.releaseVersion, }; - res.json(response); + return response; } -export async function appHealth(_: Request, res: Response) { - res.json({ +export async function appHealth() { + return { message: 'OK', timestamp: Date.now(), uptime: process.uptime(), - }); + }; } -async function _axiosRequestMake(req: Request, res: Response) { - const { apiMeta } = req.body; +async function _axiosRequestMake(param: { + body: { + apiMeta: any; + }; +}) { + const { apiMeta } = param.body; if (apiMeta?.body) { try { @@ -149,13 +150,17 @@ async function _axiosRequestMake(req: Request, res: Response) { withCredentials: true, }; const data = await require('axios')(_req); - return res.json(data?.data); + return data?.data; } -export async function axiosRequestMake(req: Request, res: Response) { +export async function axiosRequestMake(param: { + body: { + apiMeta: any; + }; +}) { const { apiMeta: { url }, - } = req.body; + } = param.body; const isExcelImport = /.*\.(xls|xlsx|xlsm|ods|ots)/; const isCSVImport = /.*\.(csv)/; const ipBlockList = @@ -164,21 +169,27 @@ export async function axiosRequestMake(req: Request, res: Response) { ipBlockList.test(url) || (!isCSVImport.test(url) && !isExcelImport.test(url)) ) { - return res.json({}); + return {}; } if (isCSVImport || isExcelImport) { - req.body.apiMeta.responseType = 'arraybuffer'; + param.body.apiMeta.responseType = 'arraybuffer'; } - return await _axiosRequestMake(req, res); + return await _axiosRequestMake({ + body: param.body, + }); } -export async function urlToDbConfig(req: Request, res: Response) { - const { url } = req.body; +export async function urlToDbConfig(param: { + body: { + url: string; + }; +}) { + const { url } = param.body; try { const connectionConfig = NcConfigFactory.extractXcUrlFromJdbc(url, true); - return res.json(connectionConfig); + return connectionConfig; } catch (error) { - return res.sendStatus(500); + return NcError.internalServerError(); } } @@ -218,7 +229,7 @@ interface AllMeta { sharedBaseCount: number; } -export async function aggregatedMetaInfo(_req: Request, res: Response) { +export async function aggregatedMetaInfo() { const [projects, userCount] = await Promise.all([ Project.list({}), Noco.ncMeta.metaCount(null, null, MetaTable.USERS), @@ -354,7 +365,7 @@ export async function aggregatedMetaInfo(_req: Request, res: Response) { ) ); - res.json(result); + return result; } const extractResultOrNull = (results: PromiseSettledResult[]) => { @@ -366,19 +377,3 @@ const extractResultOrNull = (results: PromiseSettledResult[]) => { return null; }); }; - -export default (router) => { - router.post( - '/api/v1/db/meta/connection/test', - ncMetaAclMw(testConnection, 'testConnection') - ); - router.get('/api/v1/db/meta/nocodb/info', catchError(appInfo)); - router.post('/api/v1/db/meta/axiosRequestMake', catchError(axiosRequestMake)); - router.get('/api/v1/version', catchError(versionInfo)); - router.get('/api/v1/health', catchError(appHealth)); - router.post('/api/v1/url_to_config', catchError(urlToDbConfig)); - router.get( - '/api/v1/aggregated-meta-info', - ncMetaAclMw(aggregatedMetaInfo, 'aggregatedMetaInfo') - ); -}; diff --git a/packages/nocodb/src/lib/services/viewColumnService.ts b/packages/nocodb/src/lib/services/viewColumnService.ts new file mode 100644 index 0000000000..73d2041d46 --- /dev/null +++ b/packages/nocodb/src/lib/services/viewColumnService.ts @@ -0,0 +1,39 @@ +import { T } from 'nc-help'; +import { View } from '../models'; + +export async function columnList(param: { viewId: string }) { + return await View.getColumns(param.viewId); +} +export async function columnAdd(param: { + viewId: string; + columnId: string; + // todo: add proper type for grid column in swagger + column: any; +}) { + const viewColumn = await View.insertOrUpdateColumn( + param.viewId, + param.columnId, + { + ...param.column, + view_id: param.viewId, + } + ); + T.emit('evt', { evt_type: 'viewColumn:inserted' }); + + return viewColumn; +} + +export async function columnUpdate(param: { + viewId: string; + columnId: string; + // todo: add proper type for grid column in swagger + column: any; +}) { + const result = await View.updateColumn( + param.viewId, + param.columnId, + param.column + ); + T.emit('evt', { evt_type: 'viewColumn:updated' }); + return result; +} diff --git a/packages/nocodb/src/lib/services/viewService.ts b/packages/nocodb/src/lib/services/viewService.ts new file mode 100644 index 0000000000..f328d5bab8 --- /dev/null +++ b/packages/nocodb/src/lib/services/viewService.ts @@ -0,0 +1,82 @@ +import { SharedViewType, ViewType } from 'nocodb-sdk'; +import { Model, View } from '../models'; +import { T } from 'nc-help'; +import { xcVisibilityMetaGet } from './modelVisibilityService'; + +export async function viewList(param: { + tableId: string; + user: { + roles: Record; + }; +}) { + const model = await Model.get(param.tableId); + + const viewList = await xcVisibilityMetaGet({ + projectId: model.project_id, + models: [model], + }); + + // todo: user roles + //await View.list(param.tableId) + const filteredViewList = viewList.filter((view: any) => { + return Object.keys(param?.user?.roles).some( + (role) => param?.user?.roles[role] && !view.disabled[role] + ); + }); + + return filteredViewList; +} + +// @ts-ignore +export async function shareView(param: { viewId: string }) { + T.emit('evt', { evt_type: 'sharedView:generated-link' }); + return await View.share(param.viewId); +} + +// todo: type correctly +export async function viewUpdate(param: { viewId: string; view: ViewType }) { + const result = await View.update(param.viewId, param.view); + T.emit('evt', { evt_type: 'vtable:updated', show_as: result.type }); + return result; +} + +export async function viewDelete(param: { viewId: string }) { + await View.delete(param.viewId); + T.emit('evt', { evt_type: 'vtable:deleted' }); + return true; +} + +export async function shareViewUpdate(param: { + viewId: string; + // todo: type correctly + sharedView: SharedViewType; +}) { + T.emit('evt', { evt_type: 'sharedView:updated' }); + return await View.update(param.viewId, param.sharedView); +} + +export async function shareViewDelete(param: { viewId: string }) { + T.emit('evt', { evt_type: 'sharedView:deleted' }); + await View.sharedViewDelete(param.viewId); + return true; +} + +export async function showAllColumns(param: { + viewId: string; + ignoreIds?: string[]; +}) { + await View.showAllColumns(param.viewId, param.ignoreIds || []); + return true; +} + +export async function hideAllColumns(param: { + viewId: string; + ignoreIds?: string[]; +}) { + await View.hideAllColumns(param.viewId, param.ignoreIds || []); + return true; +} + +export async function shareViewList(param: { tableId: string }) { + return await View.shareViewList(param.tableId); +} diff --git a/packages/nocodb/src/lib/utils/common/BaseApiBuilder.ts b/packages/nocodb/src/lib/utils/common/BaseApiBuilder.ts index 69294bdfc3..5b1f36e8a2 100644 --- a/packages/nocodb/src/lib/utils/common/BaseApiBuilder.ts +++ b/packages/nocodb/src/lib/utils/common/BaseApiBuilder.ts @@ -21,7 +21,7 @@ import NcProjectBuilder from '../../v1-legacy/NcProjectBuilder'; import Noco from '../../Noco'; import NcMetaIO from '../../meta/NcMetaIO'; import XcCache from '../../v1-legacy/plugins/adapters/cache/XcCache'; -import { Tele } from 'nc-help'; +import { T } from 'nc-help'; import BaseModel from './BaseModel'; import { XcCron } from './XcCron'; @@ -324,7 +324,7 @@ export default abstract class BaseApiBuilder } ); } - Tele.emit('evt', { evt_type: 'relation:created' }); + T.emit('evt', { evt_type: 'relation:created' }); } public async onRelationDelete( @@ -2979,7 +2979,7 @@ export default abstract class BaseApiBuilder } public async onTableCreate(_tn: string, _args?: any) { - Tele.emit('evt', { evt_type: 'table:created' }); + T.emit('evt', { evt_type: 'table:created' }); } public onVirtualTableUpdate(args: any) { diff --git a/packages/nocodb/src/lib/v1-legacy/NcProjectBuilder.ts b/packages/nocodb/src/lib/v1-legacy/NcProjectBuilder.ts index e155f40c15..15a63e4a6a 100644 --- a/packages/nocodb/src/lib/v1-legacy/NcProjectBuilder.ts +++ b/packages/nocodb/src/lib/v1-legacy/NcProjectBuilder.ts @@ -10,7 +10,7 @@ import SqlClientFactory from '../db/sql-client/lib/SqlClientFactory'; import Migrator from '../db/sql-migrator/lib/KnexMigrator'; import Noco from '../Noco'; -import { Tele } from 'nc-help'; +import { T } from 'nc-help'; import { GqlApiBuilder } from './gql/GqlApiBuilder'; import { XCEeError } from '../meta/NcMetaMgr'; import { RestApiBuilder } from './rest/RestApiBuilder'; @@ -840,7 +840,7 @@ export default class NcProjectBuilder { this.apiInfInfoList.push(info); this.aggregatedApiInfo = aggregatedInfo; if (isFirstTime) { - Tele.emit('evt_api_created', info); + T.emit('evt_api_created', info); } } diff --git a/packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrl.ts b/packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrl.ts index f78c17cd5c..1c8f61268c 100644 --- a/packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrl.ts +++ b/packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrl.ts @@ -29,7 +29,7 @@ const { isEmail } = require('validator'); import axios from 'axios'; import IEmailAdapter from '../../../interface/IEmailAdapter'; -import { Tele } from 'nc-help'; +import { T } from 'nc-help'; import XcCache from '../plugins/adapters/cache/XcCache'; passport.serializeUser(function ( @@ -130,7 +130,7 @@ export default class RestAuthCtrl { await this.createAuthTableIfNotExists(); await this.initStrategies(); - Tele.emit('evt_app_started', await this.users.count('id as count').first()); + T.emit('evt_app_started', await this.users.count('id as count').first()); this.app.router.use(passport.initialize()); const jwtMiddleware = passport.authenticate('jwt', { session: false }); @@ -406,7 +406,7 @@ export default class RestAuthCtrl { const token = req.query.state; if (token) { - Tele.emit('evt_subscribe', email); + T.emit('evt_subscribe', email); await this.users .update({ // firstname, lastname, @@ -431,7 +431,7 @@ export default class RestAuthCtrl { return cb({ msg: `Account not found!` }); } - Tele.emit('evt_subscribe', email); + T.emit('evt_subscribe', email); const salt = await promisify(bcrypt.genSalt)(10); user = await this.users.insert({ email: profile.emails[0].value, @@ -513,7 +513,7 @@ export default class RestAuthCtrl { const token = req.query?.state?.replace('github|', ''); if (token) { - Tele.emit('evt_subscribe', email); + T.emit('evt_subscribe', email); await this.users .update({ // firstname, lastname, @@ -538,7 +538,7 @@ export default class RestAuthCtrl { return cb({ msg: `Account not found!` }); } - Tele.emit('evt_subscribe', email); + T.emit('evt_subscribe', email); const salt = await promisify(bcrypt.genSalt)(10); user = await this.users.insert({ email: profile.emails[0].value, @@ -900,7 +900,7 @@ export default class RestAuthCtrl { const email_verification_token = uuidv4(); if (!ignore_subscribe) { - Tele.emit('evt_subscribe', email); + T.emit('evt_subscribe', email); } if (user) { @@ -928,7 +928,7 @@ export default class RestAuthCtrl { if (!(await this.users.first())) { // todo: update in nc_store // roles = 'owner,creator,editor' - Tele.emit('evt', { evt_type: 'project:invite', count: 1 }); + T.emit('evt', { evt_type: 'project:invite', count: 1 }); } else { if (process.env.NC_INVITE_ONLY_SIGNUP) { return next( @@ -1289,7 +1289,7 @@ export default class RestAuthCtrl { } } - Tele.emit('evt', { evt_type: 'project:invite', count: count?.count }); + T.emit('evt', { evt_type: 'project:invite', count: count?.count }); this.xcMeta.audit(req.body.project_id, null, 'nc_audit', { op_type: 'AUTHENTICATION', op_sub_type: 'INVITE', diff --git a/packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrlEE.ts b/packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrlEE.ts index a91eeb5558..efeae0b567 100644 --- a/packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrlEE.ts +++ b/packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrlEE.ts @@ -2,7 +2,7 @@ import passport from 'passport'; import { Strategy } from 'passport-jwt'; import { v4 as uuidv4 } from 'uuid'; import validator from 'validator'; -import { Tele } from 'nc-help'; +import { T } from 'nc-help'; import XcCache from '../plugins/adapters/cache/XcCache'; @@ -85,7 +85,7 @@ export default class RestAuthCtrlEE extends RestAuthCtrl { req.body.roles ); - Tele.emit('evt', { evt_type: 'project:invite', count: count?.count }); + T.emit('evt', { evt_type: 'project:invite', count: count?.count }); this.xcMeta.audit(req.body.project_id, null, 'nc_audit', { op_type: 'AUTHENTICATION', diff --git a/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts b/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts index e4354301eb..7a1c945f22 100644 --- a/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts +++ b/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts @@ -2,7 +2,7 @@ import { NcConfig } from '../../interface/config'; import debug from 'debug'; import NcMetaIO from '../meta/NcMetaIO'; -import { Tele } from 'nc-help'; +import { T } from 'nc-help'; import ncProjectEnvUpgrader from './ncProjectEnvUpgrader'; import ncProjectEnvUpgrader0011045 from './ncProjectEnvUpgrader0011045'; import ncProjectUpgraderV2_0090000 from './ncProjectUpgraderV2_0090000'; @@ -107,14 +107,14 @@ export default class NcUpgrader { } } await ctx.ncMeta.commit(); - Tele.emit('evt', { + T.emit('evt', { evt_type: 'appMigration:upgraded', from: oldVersion, to: process.env.NC_VERSION, }); } catch (e) { await ctx.ncMeta.rollback(e); - Tele.emit('evt', { + T.emit('evt', { evt_type: 'appMigration:failed', from: oldVersion, to: process.env.NC_VERSION, diff --git a/packages/nocodb/src/schema/swagger.json b/packages/nocodb/src/schema/swagger.json index 5b60b7e4ce..8f58513d76 100644 --- a/packages/nocodb/src/schema/swagger.json +++ b/packages/nocodb/src/schema/swagger.json @@ -14089,6 +14089,16 @@ ], "items": { "properties": { + "id": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "disabled": { "properties": { "commenter": { diff --git a/packages/nocodb/tests/unit/rest/tests/tableRow.test.ts b/packages/nocodb/tests/unit/rest/tests/tableRow.test.ts index 3f2b75e7e0..9fdc944bd2 100644 --- a/packages/nocodb/tests/unit/rest/tests/tableRow.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/tableRow.test.ts @@ -2125,9 +2125,13 @@ function tableTest() { .set('xc-auth', context.token) .expect(400); + // todo: only keep generic error message once updated in noco catchError middleware if ( - !response.body.msg.includes("Column 'customer_id' cannot be null") && - !response.body.msg.includes('Cannot add or update a child row') + !response.body.message?.includes("The column 'customer_id' cannot be null") && + !response.body.message?.includes("Column 'customer_id' cannot be null") && + !response.body.message?.includes('Cannot add or update a child row') && + !response.body.msg?.includes("Column 'customer_id' cannot be null") && + !response.body.msg?.includes('Cannot add or update a child row') ) { console.log( 'Delete list hm with existing ref row id with non nullable clause', diff --git a/tests/playwright/tests/filters.spec.ts b/tests/playwright/tests/filters.spec.ts index 2f5b64a43f..e399d85f80 100644 --- a/tests/playwright/tests/filters.spec.ts +++ b/tests/playwright/tests/filters.spec.ts @@ -86,8 +86,6 @@ async function verifyFilter(param: { return; } - console.log(`Verifying filter: ${param.opType} ${param.opSubType}`); - await toolbar.clickFilter(); await toolbar.filter.add({ columnTitle: param.column, diff --git a/tests/playwright/tests/keyboardShortcuts.spec.ts b/tests/playwright/tests/keyboardShortcuts.spec.ts index 5026ccbb5d..2c027f542d 100644 --- a/tests/playwright/tests/keyboardShortcuts.spec.ts +++ b/tests/playwright/tests/keyboardShortcuts.spec.ts @@ -232,8 +232,7 @@ test.describe('Verify shortcuts', () => { }); await api.dbTableRow.bulkCreate('noco', context.project.id, table.id, [record]); - const records = await api.dbTableRow.list('noco', context.project.id, table.id, { limit: 1 }); - console.log(records); + await api.dbTableRow.list('noco', context.project.id, table.id, { limit: 1 }); } catch (e) { console.error(e); }