Browse Source

Merge branch 'develop' into feat/ajv-validation-followup

pull/5222/head
Wing-Kam Wong 2 years ago
parent
commit
a0997adfe8
  1. 120
      packages/nc-gui/lang/da.json
  2. 3
      packages/noco-docs/content/en/setup-and-usages/table-operations.md
  3. 1
      packages/nocodb-sdk/src/lib/Api.ts
  4. 6
      packages/nocodb-sdk/src/lib/sqlUi/OracleUi.ts
  5. 11
      packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts
  6. 14
      packages/nocodb/package-lock.json
  7. 2
      packages/nocodb/package.json
  8. 10
      packages/nocodb/src/lib/Noco.ts
  9. 49
      packages/nocodb/src/lib/controllers/apiTokenController.ts
  10. 127
      packages/nocodb/src/lib/controllers/attachmentController.ts
  11. 39
      packages/nocodb/src/lib/controllers/auditController.ts
  12. 91
      packages/nocodb/src/lib/controllers/baseController.ts
  13. 8
      packages/nocodb/src/lib/controllers/cacheController.ts
  14. 77
      packages/nocodb/src/lib/controllers/columnController.ts
  15. 92
      packages/nocodb/src/lib/controllers/dataControllers/bulkDataAliasController.ts
  16. 259
      packages/nocodb/src/lib/controllers/dataControllers/dataAliasController.ts
  17. 10
      packages/nocodb/src/lib/controllers/dataControllers/dataAliasExportController.ts
  18. 137
      packages/nocodb/src/lib/controllers/dataControllers/dataAliasNestedController.ts
  19. 192
      packages/nocodb/src/lib/controllers/dataControllers/dataController.ts
  20. 270
      packages/nocodb/src/lib/controllers/dataControllers/helpers.ts
  21. 15
      packages/nocodb/src/lib/controllers/dataControllers/index.ts
  22. 18
      packages/nocodb/src/lib/controllers/dataControllers/oldDataController.ts
  23. 6
      packages/nocodb/src/lib/controllers/exportController.ts
  24. 115
      packages/nocodb/src/lib/controllers/filterController.ts
  25. 21
      packages/nocodb/src/lib/controllers/formViewColumnController.ts
  26. 48
      packages/nocodb/src/lib/controllers/formViewController.ts
  27. 49
      packages/nocodb/src/lib/controllers/galleryViewController.ts
  28. 23
      packages/nocodb/src/lib/controllers/gridViewColumnController.ts
  29. 34
      packages/nocodb/src/lib/controllers/gridViewController.ts
  30. 98
      packages/nocodb/src/lib/controllers/hookController.ts
  31. 90
      packages/nocodb/src/lib/controllers/hookFilterController.ts
  32. 19
      packages/nocodb/src/lib/controllers/kanbanViewController.ts
  33. 31
      packages/nocodb/src/lib/controllers/mapViewController.ts
  34. 54
      packages/nocodb/src/lib/controllers/metaDiffController.ts
  35. 34
      packages/nocodb/src/lib/controllers/modelVisibilityController.ts
  36. 17
      packages/nocodb/src/lib/controllers/orgLicenseController.ts
  37. 63
      packages/nocodb/src/lib/controllers/orgTokenController.ts
  38. 154
      packages/nocodb/src/lib/controllers/orgUserController.ts
  39. 32
      packages/nocodb/src/lib/controllers/pluginController.ts
  40. 169
      packages/nocodb/src/lib/controllers/projectController.ts
  41. 85
      packages/nocodb/src/lib/controllers/projectUserController.ts
  42. 9
      packages/nocodb/src/lib/controllers/publicControllers/index.ts
  43. 105
      packages/nocodb/src/lib/controllers/publicControllers/publicDataController.ts
  44. 20
      packages/nocodb/src/lib/controllers/publicControllers/publicDataExportController.ts
  45. 31
      packages/nocodb/src/lib/controllers/publicControllers/publicMetaController.ts
  46. 61
      packages/nocodb/src/lib/controllers/sharedBaseController.ts
  47. 80
      packages/nocodb/src/lib/controllers/sortController.ts
  48. 36
      packages/nocodb/src/lib/controllers/swaggerController/index.ts
  49. 0
      packages/nocodb/src/lib/controllers/swaggerController/redocHtml.ts
  50. 0
      packages/nocodb/src/lib/controllers/swaggerController/swaggerHtml.ts
  51. 20
      packages/nocodb/src/lib/controllers/syncController/importApis.ts
  52. 48
      packages/nocodb/src/lib/controllers/syncController/index.ts
  53. 111
      packages/nocodb/src/lib/controllers/tableController.ts
  54. 2
      packages/nocodb/src/lib/controllers/testController.ts
  55. 445
      packages/nocodb/src/lib/controllers/userController/index.ts
  56. 16
      packages/nocodb/src/lib/controllers/userController/initStrategies.ts
  57. 0
      packages/nocodb/src/lib/controllers/userController/ui/auth/emailVerify.ts
  58. 0
      packages/nocodb/src/lib/controllers/userController/ui/auth/resetPassword.ts
  59. 0
      packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/forgotPassword.ts
  60. 0
      packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/invite.ts
  61. 0
      packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/verify.ts
  62. 303
      packages/nocodb/src/lib/controllers/userController/userApis.ts
  63. 59
      packages/nocodb/src/lib/controllers/utilController.ts
  64. 38
      packages/nocodb/src/lib/controllers/viewColumnController.ts
  65. 133
      packages/nocodb/src/lib/controllers/viewController.ts
  66. 4
      packages/nocodb/src/lib/db/sql-client/lib/KnexClient.ts
  67. 4
      packages/nocodb/src/lib/db/sql-mgr/SqlMgr.ts
  68. 48
      packages/nocodb/src/lib/meta/NcMetaMgr.ts
  69. 8
      packages/nocodb/src/lib/meta/NcMetaMgrEE.ts
  70. 53
      packages/nocodb/src/lib/meta/api/apiTokenApis.ts
  71. 225
      packages/nocodb/src/lib/meta/api/attachmentApis.ts
  72. 129
      packages/nocodb/src/lib/meta/api/baseApis.ts
  73. 101
      packages/nocodb/src/lib/meta/api/dataApis/bulkDataAliasApis.ts
  74. 432
      packages/nocodb/src/lib/meta/api/dataApis/dataAliasApis.ts
  75. 291
      packages/nocodb/src/lib/meta/api/dataApis/dataAliasNestedApis.ts
  76. 612
      packages/nocodb/src/lib/meta/api/dataApis/dataApis.ts
  77. 15
      packages/nocodb/src/lib/meta/api/dataApis/index.ts
  78. 22
      packages/nocodb/src/lib/meta/api/ee/orgTokenApis.ts
  79. 175
      packages/nocodb/src/lib/meta/api/filterApis.ts
  80. 66
      packages/nocodb/src/lib/meta/api/formViewApis.ts
  81. 20
      packages/nocodb/src/lib/meta/api/formViewColumnApis.ts
  82. 47
      packages/nocodb/src/lib/meta/api/galleryViewApis.ts
  83. 47
      packages/nocodb/src/lib/meta/api/gridViewApis.ts
  84. 22
      packages/nocodb/src/lib/meta/api/helpers/apiHelpers.ts
  85. 4
      packages/nocodb/src/lib/meta/api/helpers/columnHelpers.ts
  86. 2
      packages/nocodb/src/lib/meta/api/helpers/populateMeta.ts
  87. 113
      packages/nocodb/src/lib/meta/api/hookApis.ts
  88. 145
      packages/nocodb/src/lib/meta/api/hookFilterApis.ts
  89. 194
      packages/nocodb/src/lib/meta/api/index.ts
  90. 129
      packages/nocodb/src/lib/meta/api/modelVisibilityApis.ts
  91. 83
      packages/nocodb/src/lib/meta/api/orgTokenApis.ts
  92. 337
      packages/nocodb/src/lib/meta/api/orgUserApis.ts
  93. 287
      packages/nocodb/src/lib/meta/api/projectApis.ts
  94. 333
      packages/nocodb/src/lib/meta/api/projectUserApis.ts
  95. 5
      packages/nocodb/src/lib/meta/api/publicApis/index.ts
  96. 472
      packages/nocodb/src/lib/meta/api/publicApis/publicDataApis.ts
  97. 112
      packages/nocodb/src/lib/meta/api/sharedBaseApis.ts
  98. 81
      packages/nocodb/src/lib/meta/api/sortApis.ts
  99. 61
      packages/nocodb/src/lib/meta/api/swagger/swaggerApis.ts
  100. 447
      packages/nocodb/src/lib/meta/api/tableApis.ts
  101. Some files were not shown because too many files have changed in this diff Show More

120
packages/nc-gui/lang/da.json

@ -1,31 +1,31 @@
{ {
"general": { "general": {
"home": "Hjem", "home": "Forside",
"load": "belastning", "load": "Indlæs",
"open": "Åben", "open": "Åbn",
"close": "Tæt", "close": "Luk",
"yes": "Ja", "yes": "Ja",
"no": "Ingen", "no": "Nej",
"ok": "Okay", "ok": "Okay",
"and": "Og", "and": "Og",
"or": "Eller", "or": "Eller",
"add": "Tilføje", "add": "Tilføj",
"edit": "Redigere", "edit": "Redigér",
"remove": "Fjerne", "remove": "Fjern",
"save": "Gemme", "save": "Gem",
"cancel": "Afbestille", "cancel": "Fortryd",
"submit": "Indsend", "submit": "Indsend",
"create": "skab", "create": "Opret",
"duplicate": "Duplikat", "duplicate": "Duplikat",
"insert": "Indsættes", "insert": "Indsæt",
"delete": "Delete.", "delete": "Slet",
"update": "UPDATE.", "update": "Opdatér",
"rename": "Omdøb", "rename": "Omdøb",
"reload": "Genindlæsning", "reload": "Genindlæs",
"reset": "Nulstil", "reset": "Nulstil",
"install": "Installere", "install": "Installer",
"show": "At vise", "show": "Vis",
"hide": "Skjule", "hide": "Skjul",
"showAll": "Vis alt", "showAll": "Vis alt",
"hideAll": "Gem alt", "hideAll": "Gem alt",
"showMore": "Vis mere", "showMore": "Vis mere",
@ -75,7 +75,7 @@
"hideField": "Skjul felt", "hideField": "Skjul felt",
"sortAsc": "Sortere stigende", "sortAsc": "Sortere stigende",
"sortDesc": "Sortere nedadgående", "sortDesc": "Sortere nedadgående",
"geoDataField": "GeoData Field" "geoDataField": "GeoData-felt"
}, },
"objects": { "objects": {
"project": "Projekt", "project": "Projekt",
@ -92,15 +92,15 @@
"records": "Optegnelser.", "records": "Optegnelser.",
"webhook": "WebHook.", "webhook": "WebHook.",
"webhooks": "Webhooks.", "webhooks": "Webhooks.",
"view": "Udsigt", "view": "Visning",
"views": "Visninger.", "views": "Visninger",
"viewType": { "viewType": {
"grid": "Grid.", "grid": "Grid",
"gallery": "Galleri", "gallery": "Galleri",
"form": "Formular", "form": "Formular",
"kanban": "Kanban.", "kanban": "Kanban.",
"calendar": "Kalender", "calendar": "Kalender",
"map": "Map" "map": "Kort"
}, },
"user": "Bruger", "user": "Bruger",
"users": "Brugere", "users": "Brugere",
@ -209,7 +209,7 @@
"advancedSettings": "Avancerede indstillinger", "advancedSettings": "Avancerede indstillinger",
"codeSnippet": "Kodeuddrag", "codeSnippet": "Kodeuddrag",
"keyboardShortcut": "Tastaturgenveje", "keyboardShortcut": "Tastaturgenveje",
"generateRandomName": "Generate Random Name" "generateRandomName": "Generér Tilfældigt Navn"
}, },
"labels": { "labels": {
"createdBy": "Oprettet af", "createdBy": "Oprettet af",
@ -217,7 +217,7 @@
"projName": "Projekt navn", "projName": "Projekt navn",
"tableName": "Tabelnavn.", "tableName": "Tabelnavn.",
"viewName": "Se navn", "viewName": "Se navn",
"viewLink": "Se Link.", "viewLink": "Vis Link",
"columnName": "Kolonne navn", "columnName": "Kolonne navn",
"columnType": "Kolonne type", "columnType": "Kolonne type",
"roleName": "Rolle navn", "roleName": "Rolle navn",
@ -235,7 +235,7 @@
"action": "Handling", "action": "Handling",
"actions": "Handlinger", "actions": "Handlinger",
"operation": "Operation", "operation": "Operation",
"operationSub": "Sub Operation", "operationSub": "Underordnet operation",
"operationType": "Driftstype", "operationType": "Driftstype",
"operationSubType": "Drift Undertype", "operationSubType": "Drift Undertype",
"description": "Beskrivelse", "description": "Beskrivelse",
@ -257,7 +257,7 @@
"barcodeFormat": "Stregkodeformat", "barcodeFormat": "Stregkodeformat",
"qrCodeValueTooLong": "For mange tegn til en QR-kode", "qrCodeValueTooLong": "For mange tegn til en QR-kode",
"barcodeValueTooLong": "For mange tegn til en stregkode", "barcodeValueTooLong": "For mange tegn til en stregkode",
"yourLocation": "Your Location", "yourLocation": "Din Placering",
"lng": "Lng", "lng": "Lng",
"lat": "Lat", "lat": "Lat",
"aggregateFunction": "Aggregate Function.", "aggregateFunction": "Aggregate Function.",
@ -283,8 +283,8 @@
}, },
"docReference": "Dokumentreference.", "docReference": "Dokumentreference.",
"selectUserRole": "Vælg brugerrolle", "selectUserRole": "Vælg brugerrolle",
"childTable": "Børnebord", "childTable": "Undertabel",
"childColumn": "Barn kolonne", "childColumn": "Underkolonner",
"linkToAnotherRecord": "Link til en anden post", "linkToAnotherRecord": "Link til en anden post",
"onUpdate": "På opdatering", "onUpdate": "På opdatering",
"onDelete": "På Delete.", "onDelete": "På Delete.",
@ -379,36 +379,36 @@
"reloadRoles": "Genindlæs roller", "reloadRoles": "Genindlæs roller",
"nextPage": "Næste side", "nextPage": "Næste side",
"prevPage": "Forrige side", "prevPage": "Forrige side",
"nextRecord": "Næste rekord.", "nextRecord": "Næste post",
"previousRecord": "Tidligere rekord.", "previousRecord": "Forrige post",
"copyApiURL": "COPY API URL.", "copyApiURL": "COPY API URL.",
"createTable": "Tabel Create.", "createTable": "Opret tabel",
"refreshTable": "Tabeller opdatere", "refreshTable": "Genopfrisk Tabeller",
"renameTable": "Bord omdøb", "renameTable": "Omdøb Tabel",
"deleteTable": "TABEL DELETE.", "deleteTable": "Slet Tabel",
"addField": "Tilføj nyt felt til denne tabel", "addField": "Tilføj nyt felt til denne tabel",
"setDisplay": "Set as Display value", "setDisplay": "Sæt som visningsværdi",
"addRow": "Tilføj ny række", "addRow": "Tilføj ny post",
"saveRow": "Gem ro", "saveRow": "Gem række",
"saveAndExit": "Gem og afslutning", "saveAndExit": "Gem og afslutning",
"saveAndStay": "Gem og bliv", "saveAndStay": "Gem og bliv",
"insertRow": "Indsæt ny række", "insertRow": "Indsæt ny række",
"deleteRow": "DELETE ROW.", "deleteRow": "Slet Række",
"duplicateRow": "Duplicate Row", "duplicateRow": "Dupliker Række",
"deleteSelectedRow": "Slet de valgte rækker", "deleteSelectedRow": "Slet de valgte rækker",
"importExcel": "Import Excel.", "importExcel": "Import Excel.",
"importCSV": "Import CSV.", "importCSV": "Import CSV.",
"downloadCSV": "Download som CSV.", "downloadCSV": "Download som CSV.",
"downloadExcel": "Download som XLSX", "downloadExcel": "Download som XLSX",
"uploadCSV": "Upload CSV.", "uploadCSV": "Upload CSV.",
"import": "Importere", "import": "Import",
"importMetadata": "Import metadata.", "importMetadata": "Import metadata.",
"exportMetadata": "Eksport metadata.", "exportMetadata": "Eksport metadata.",
"clearMetadata": "Klare metadata.", "clearMetadata": "Klare metadata.",
"exportToFile": "Eksporter til filer", "exportToFile": "Eksporter til filer",
"changePwd": "Skift kodeord", "changePwd": "Skift kodeord",
"createView": "Opret en visning", "createView": "Opret en visning",
"shareView": "Del View", "shareView": "Del Visning",
"listSharedView": "Shared View List.", "listSharedView": "Shared View List.",
"ListView": "Visninger List", "ListView": "Visninger List",
"copyView": "Kopi visning", "copyView": "Kopi visning",
@ -429,23 +429,23 @@
"importZip": "Import Zip.", "importZip": "Import Zip.",
"metaSync": "Synkroniser nu", "metaSync": "Synkroniser nu",
"settings": "Indstillinger.", "settings": "Indstillinger.",
"previewAs": "Forhåndsvisning så", "previewAs": "Forhåndsvisning som",
"resetReview": "Nulstil preview.", "resetReview": "Nulstil forhåndsvisning",
"testDbConn": "Test Database Connection.", "testDbConn": "Test Database Forbindelse",
"removeDbFromEnv": "Fjern databasen fra miljøet", "removeDbFromEnv": "Fjern databasen fra miljøet",
"editConnJson": "Rediger forbindelse JSON", "editConnJson": "Rediger forbindelse JSON",
"sponsorUs": "Sponsor os", "sponsorUs": "Sponsorer os",
"sendEmail": "SEND E-MAIL", "sendEmail": "SEND E-MAIL",
"addUserToProject": "Tilføj bruger til projekt", "addUserToProject": "Tilføj bruger til projekt",
"getApiSnippet": "Hent API-snippet", "getApiSnippet": "Hent API-snippet",
"clearCell": "Klar celle", "clearCell": "Ryd celle",
"addFilterGroup": "Tilføj filtergruppe", "addFilterGroup": "Tilføj filtergruppe",
"linkRecord": "Link record", "linkRecord": "Link record",
"addNewRecord": "Tilføj ny post", "addNewRecord": "Tilføj ny post",
"useConnectionUrl": "Brug forbindelses-URL", "useConnectionUrl": "Brug forbindelses-URL",
"toggleCommentsDraw": "Toggle kommentarer tegne", "toggleCommentsDraw": "Toggle kommentarer tegne",
"expandRecord": "Udvid optegnelse", "expandRecord": "Udvid optegnelse",
"deleteRecord": "Slet registrering", "deleteRecord": "Slet Post",
"erd": { "erd": {
"showColumns": "Vis kolonner", "showColumns": "Vis kolonner",
"showPkAndFk": "Vis primære og fremmede nøgler", "showPkAndFk": "Vis primære og fremmede nøgler",
@ -461,8 +461,8 @@
"addOrEditStack": "Tilføj / Rediger stak" "addOrEditStack": "Tilføj / Rediger stak"
}, },
"map": { "map": {
"mappedBy": "Mapped By", "mappedBy": "Kortlagt af",
"chooseMappingField": "Choose a Mapping Field" "chooseMappingField": "Vælg et kortlægningsfelt"
} }
}, },
"tooltip": { "tooltip": {
@ -530,9 +530,9 @@
"orgViewer": "Seeren har ikke lov til at oprette nye projekter, men kan få adgang til alle inviterede projekter." "orgViewer": "Seeren har ikke lov til at oprette nye projekter, men kan få adgang til alle inviterede projekter."
}, },
"map": { "map": {
"overLimit": "You're over the limit.", "overLimit": "Du er over grænsen.",
"closeLimit": "You're getting close to the limit.", "closeLimit": "Du nærmer dig grænsen.",
"limitNumber": "The limit of markers shown in a Map View is 1000 records." "limitNumber": "Grænsen for antallet af markeringer, der vises i en kortvisning, er 1000 poster."
}, },
"footerInfo": "Rækker per side", "footerInfo": "Rækker per side",
"upload": "Vælg fil for at uploade", "upload": "Vælg fil for at uploade",
@ -616,7 +616,7 @@
"gallery": "Tilføj Gallery View.", "gallery": "Tilføj Gallery View.",
"form": "Tilføj formularvisning", "form": "Tilføj formularvisning",
"kanban": "Tilføj Kanban View.", "kanban": "Tilføj Kanban View.",
"map": "Add Map View", "map": "Tilføj Kortvisning",
"calendar": "Tilføj kalendervisning" "calendar": "Tilføj kalendervisning"
}, },
"tablesMetadataInSync": "Tabeller Metadata er synkroniseret", "tablesMetadataInSync": "Tabeller Metadata er synkroniseret",
@ -648,11 +648,11 @@
"deleteViewConfirmation": "Er du sikker på, at du vil slette denne visning?", "deleteViewConfirmation": "Er du sikker på, at du vil slette denne visning?",
"deleteTableConfirmation": "Ønsker du at slette tabellen", "deleteTableConfirmation": "Ønsker du at slette tabellen",
"showM2mTables": "Vis M2M-tabeller", "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.", "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": "Show NULL in Cells", "showNullInCells": "Vis NULL i celler",
"showNullInCellsDesc": "Display 'NULL' tag in cells holding NULL value. This helps differentiate against cells holding EMPTY string.", "showNullInCellsDesc": "Display 'NULL' tag in cells holding NULL value. This helps differentiate against cells holding EMPTY string.",
"showNullAndEmptyInFilter": "Show NULL and EMPTY in Filter", "showNullAndEmptyInFilter": "Vis NULL og EMPTY i Filter",
"showNullAndEmptyInFilterDesc": "Enable 'additional' filters to differentiate fields containing NULL & Empty Strings. Default support for Blank treats both NULL & Empty strings alike.", "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\".", "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", "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.", "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 _", "nameShouldStartWithAnAlphabetOr_": "Navnet skal starte med et alfabet eller _",
"followingCharactersAreNotAllowed": "Følgende tegn er ikke tilladt", "followingCharactersAreNotAllowed": "Følgende tegn er ikke tilladt",
"columnNameRequired": "Kolonnens navn er påkrævet", "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", "projectNameExceeds50Characters": "Projektnavnet overstiger 50 tegn",
"projectNameCannotStartWithSpace": "Projektnavnet kan ikke begynde med et mellemrum", "projectNameCannotStartWithSpace": "Projektnavnet kan ikke begynde med et mellemrum",
"requiredField": "Obligatorisk felt", "requiredField": "Obligatorisk felt",
@ -740,7 +740,7 @@
}, },
"success": { "success": {
"columnDuplicated": "Kolonne duplikeret med succes", "columnDuplicated": "Kolonne duplikeret med succes",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)", "rowDuplicatedWithoutSavedYet": "Række duplikeret (ikke gemt)",
"updatedUIACL": "Opdateret UI ACL for tabeller med succes", "updatedUIACL": "Opdateret UI ACL for tabeller med succes",
"pluginUninstalled": "Plugin afinstalleret med succes", "pluginUninstalled": "Plugin afinstalleret med succes",
"pluginSettingsSaved": "Plugin-indstillingerne er gemt med succes", "pluginSettingsSaved": "Plugin-indstillingerne er gemt med succes",

3
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. You can also click `Show more` for additional menu options.
<img width="445" alt="image" src="https://user-images.githubusercontent.com/35857179/189075678-d18b799f-df13-4f78-a5a5-813e8d3277ae.png"> ![Screenshot 2023-03-03 at 8 13 07 PM](https://user-images.githubusercontent.com/86527202/222749857-0e793db2-a5d2-4b54-8d23-2a0cbbec8f5d.png)
<!-- <img width="445" alt="image" src="https://user-images.githubusercontent.com/35857179/189075678-d18b799f-df13-4f78-a5a5-813e8d3277ae.png"> -->
Click `Save` button to create the new column. Click `Save` button to create the new column.

1
packages/nocodb-sdk/src/lib/Api.ts

@ -2159,6 +2159,7 @@ export interface ViewColumnReqType {
* Model for Visibility Rule Request * Model for Visibility Rule Request
*/ */
export type VisibilityRuleReqType = { export type VisibilityRuleReqType = {
id?: string | null;
disabled?: { disabled?: {
/** Model for Bool */ /** Model for Bool */
commenter?: BoolType; commenter?: BoolType;

6
packages/nocodb-sdk/src/lib/sqlUi/OracleUi.ts

@ -1,3 +1,4 @@
import { NormalColumnRequestType } from '../Api'
import UITypes from '../UITypes'; import UITypes from '../UITypes';
import { IDType } from './index'; 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 = {}; const colProp: any = {};
switch (col.uidt) { switch (col.uidt) {
case 'ID': case 'ID':

11
packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts

@ -1,3 +1,4 @@
import { BoolType } from '../Api'
import UITypes from '../UITypes'; import UITypes from '../UITypes';
import { MssqlUi } from './MssqlUi'; import { MssqlUi } from './MssqlUi';
@ -56,12 +57,12 @@ export type SqlUIColumn = {
dt?: string; dt?: string;
dtx?: string; dtx?: string;
ct?: string; ct?: string;
nrqd?: boolean; nrqd?: BoolType;
rqd?: boolean; rqd?: BoolType;
ck?: string; ck?: string;
pk?: boolean; pk?: BoolType;
un?: boolean; un?: BoolType;
ai?: boolean; ai?: BoolType;
cdf?: string | any; cdf?: string | any;
clen?: number | any; clen?: number | any;
np?: string; np?: string;

14
packages/nocodb/package-lock.json generated

@ -66,7 +66,7 @@
"multer": "^1.4.2", "multer": "^1.4.2",
"mysql2": "^2.2.5", "mysql2": "^2.2.5",
"nanoid": "^3.1.20", "nanoid": "^3.1.20",
"nc-help": "0.2.85", "nc-help": "0.2.87",
"nc-lib-gui": "0.105.3", "nc-lib-gui": "0.105.3",
"nc-plugin": "0.1.2", "nc-plugin": "0.1.2",
"ncp": "^2.0.0", "ncp": "^2.0.0",
@ -11311,9 +11311,9 @@
} }
}, },
"node_modules/nc-help": { "node_modules/nc-help": {
"version": "0.2.85", "version": "0.2.87",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.85.tgz", "resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.87.tgz",
"integrity": "sha512-EOyrc2PuRUJzv73jHNHmUR6YhC0TlJG0DTY/sug7BF4MJAVPJgyavJnrqkRC7g0NS4xociki9gs5MbLRjlRwtQ==", "integrity": "sha512-Zlg06ialvylBEE1qtvjlNKxZrPShzXwvy3WG7nfw+8GngOkQBCTlKguejT2Kq4Gfb5378WPX1APXtsetMKBrRA==",
"dependencies": { "dependencies": {
"@rudderstack/rudder-sdk-node": "^1.1.3", "@rudderstack/rudder-sdk-node": "^1.1.3",
"axios": "^0.21.1", "axios": "^0.21.1",
@ -28029,9 +28029,9 @@
"integrity": "sha512-3AryS9uwa5NfISLxMciUonrH7YfXp+nlahB9T7girXIsLQrmwX4MdnuKs32akduCOGpKmjTJSWmATULbuMkbfw==" "integrity": "sha512-3AryS9uwa5NfISLxMciUonrH7YfXp+nlahB9T7girXIsLQrmwX4MdnuKs32akduCOGpKmjTJSWmATULbuMkbfw=="
}, },
"nc-help": { "nc-help": {
"version": "0.2.85", "version": "0.2.87",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.85.tgz", "resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.87.tgz",
"integrity": "sha512-EOyrc2PuRUJzv73jHNHmUR6YhC0TlJG0DTY/sug7BF4MJAVPJgyavJnrqkRC7g0NS4xociki9gs5MbLRjlRwtQ==", "integrity": "sha512-Zlg06ialvylBEE1qtvjlNKxZrPShzXwvy3WG7nfw+8GngOkQBCTlKguejT2Kq4Gfb5378WPX1APXtsetMKBrRA==",
"requires": { "requires": {
"@rudderstack/rudder-sdk-node": "^1.1.3", "@rudderstack/rudder-sdk-node": "^1.1.3",
"axios": "^0.21.1", "axios": "^0.21.1",

2
packages/nocodb/package.json

@ -107,7 +107,7 @@
"multer": "^1.4.2", "multer": "^1.4.2",
"mysql2": "^2.2.5", "mysql2": "^2.2.5",
"nanoid": "^3.1.20", "nanoid": "^3.1.20",
"nc-help": "0.2.85", "nc-help": "0.2.87",
"nc-lib-gui": "0.105.3", "nc-lib-gui": "0.105.3",
"nc-plugin": "0.1.2", "nc-plugin": "0.1.2",
"ncp": "^2.0.0", "ncp": "^2.0.0",

10
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 Migrator from './db/sql-migrator/lib/KnexMigrator';
import Store from './models/Store'; import Store from './models/Store';
import NcConfigFactory from './utils/NcConfigFactory'; import NcConfigFactory from './utils/NcConfigFactory';
import { Tele } from 'nc-help'; import { T } from 'nc-help';
import NcProjectBuilderCE from './v1-legacy/NcProjectBuilder'; import NcProjectBuilderCE from './v1-legacy/NcProjectBuilder';
import NcProjectBuilderEE from './v1-legacy/NcProjectBuilderEE'; import NcProjectBuilderEE from './v1-legacy/NcProjectBuilderEE';
@ -45,7 +45,7 @@ import User from './models/User';
import * as http from 'http'; import * as http from 'http';
import weAreHiring from './utils/weAreHiring'; import weAreHiring from './utils/weAreHiring';
import getInstance from './utils/getInstance'; import getInstance from './utils/getInstance';
import initAdminFromEnv from './meta/api/userApi/initAdminFromEnv'; import initAdminFromEnv from './services/userService/initAdminFromEnv';
const log = debug('nc:app'); const log = debug('nc:app');
require('dotenv').config(); require('dotenv').config();
@ -275,10 +275,10 @@ export default class Noco {
} }
next(); next();
}); });
Tele.init({ T.init({
instance: getInstance, 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}`); console.log(`App started successfully.\nVisit -> ${Noco.dashboardUrl}`);
weAreHiring(); weAreHiring();
return this.router; return this.router;
@ -531,7 +531,7 @@ export default class Noco {
if (!serverId) { if (!serverId) {
await Noco._ncMeta.metaInsert('', '', 'nc_store', { await Noco._ncMeta.metaInsert('', '', 'nc_store', {
key: 'nc_server_id', key: 'nc_server_id',
value: (serverId = Tele.id), value: (serverId = T.id),
}); });
} }
process.env.NC_SERVER_UUID = serverId; process.env.NC_SERVER_UUID = serverId;

49
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;

127
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;

39
packages/nocodb/src/lib/meta/api/auditApis.ts → packages/nocodb/src/lib/controllers/auditController.ts

@ -1,39 +1,24 @@
import { Request, Response, Router } from 'express'; import { Request, Response, Router } from 'express';
import Audit from '../../models/Audit'; import Audit from '../models/Audit';
import { AuditOperationSubTypes, AuditOperationTypes } from 'nocodb-sdk'; import { PagedResponseImpl } from '../meta/helpers/PagedResponse';
import Model from '../../models/Model'; import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { PagedResponseImpl } from '../helpers/PagedResponse'; import { auditService } from '../services';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import DOMPurify from 'isomorphic-dompurify';
import { getAjvValidatorMw } from './helpers';
export async function commentRow(req: Request<any, any>, res) { export async function commentRow(req: Request<any, any>, res) {
res.json( res.json(
await Audit.insert({ await auditService.commentRow({
...req.body, rowId: req.params.rowId,
user: (req as any).user?.email, user: (req as any).user,
op_type: AuditOperationTypes.COMMENT, body: req.body,
}) })
); );
} }
export async function auditRowUpdate(req: Request<any, any>, res) { export async function auditRowUpdate(req: Request<any, any>, res) {
const model = await Model.getByIdOrName({ id: req.body.fk_model_id });
res.json( res.json(
await Audit.insert({ await auditService.auditRowUpdate({
fk_model_id: req.body.fk_model_id, rowId: req.params.rowId,
row_id: req.params.rowId, body: req.body,
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(`<span class="">${req.body.column_name}</span>
: <span class="text-decoration-line-through red px-2 lighten-4 black--text">${req.body.prev_value}</span>
<span class="black--text green lighten-4 px-2">${req.body.value}</span>`),
ip: (req as any).clientIp,
user: (req as any).user?.email,
}) })
); );
} }
@ -70,12 +55,10 @@ router.get(
); );
router.post( router.post(
'/api/v1/db/meta/audits/comments', '/api/v1/db/meta/audits/comments',
getAjvValidatorMw('swagger.json#/components/schemas/CommentReq'),
ncMetaAclMw(commentRow, 'commentRow') ncMetaAclMw(commentRow, 'commentRow')
); );
router.post( router.post(
'/api/v1/db/meta/audits/rows/:rowId/update', '/api/v1/db/meta/audits/rows/:rowId/update',
getAjvValidatorMw('swagger.json#/components/schemas/AuditRowUpdateReq'),
ncMetaAclMw(auditRowUpdate, 'auditRowUpdate') ncMetaAclMw(auditRowUpdate, 'auditRowUpdate')
); );
router.get( router.get(

91
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<any>, res: Response<Base>) {
const base = await baseService.baseGetWithConfig({
baseId: req.params.baseId,
});
res.json(base);
}
async function baseUpdate(req: Request<any, any, any>, res: Response<any>) {
const base = await baseService.baseUpdate({
baseId: req.params.baseId,
base: req.body,
projectId: req.params.projectId,
});
res.json(base);
}
async function baseList(
req: Request<any, any, any>,
res: Response<BaseListType>
) {
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<any, any, any>,
res: Response<any>
) {
const result = await baseService.baseDelete({
baseId: req.params.baseId,
});
res.json(result);
}
async function baseCreate(req: Request<any, any>, 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;

8
packages/nocodb/src/lib/meta/api/cacheApis.ts → packages/nocodb/src/lib/controllers/cacheController.ts

@ -1,9 +1,9 @@
import catchError from '../helpers/catchError'; import catchError from '../meta/helpers/catchError';
import NocoCache from '../../cache/NocoCache';
import { Router } from 'express'; import { Router } from 'express';
import { cacheService } from '../services';
export async function cacheGet(_, res) { export async function cacheGet(_, res) {
const data = await NocoCache.export(); const data = await cacheService.cacheGet();
res.set({ res.set({
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename="cache-export.json"`, 'Content-Disposition': `attachment; filename="cache-export.json"`,
@ -12,7 +12,7 @@ export async function cacheGet(_, res) {
} }
export async function cacheDelete(_, res) { export async function cacheDelete(_, res) {
return res.json(await NocoCache.destroy()); return res.json(await cacheService.cacheDelete());
} }
const router = Router(); const router = Router();

77
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<any, any, ColumnReqType & { uidt: UITypes }>,
res: Response<TableType>
) {
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<TableType>) {
res.json(
await columnService.columnUpdate({
columnId: req.params.columnId,
column: req.body,
req,
})
);
}
export async function columnDelete(req: Request, res: Response<TableType>) {
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;

92
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;

259
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;

10
packages/nocodb/src/lib/meta/api/dataApis/dataAliasExportApis.ts → packages/nocodb/src/lib/controllers/dataControllers/dataAliasExportController.ts

@ -1,13 +1,13 @@
import { Request, Response, Router } from 'express'; import { Request, Response, Router } from 'express';
import * as XLSX from 'xlsx'; 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 { import {
extractCsvData, extractCsvData,
extractXlsxData, extractXlsxData,
getViewAndModelFromRequestByAliasOrId, } from '../../services/dataService/helpers';
} from './helpers'; import { getViewAndModelFromRequestByAliasOrId } from './helpers';
import apiMetrics from '../../helpers/apiMetrics';
import View from '../../../models/View';
async function excelDataExport(req: Request, res: Response) { async function excelDataExport(req: Request, res: Response) {
const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); const { model, view } = await getViewAndModelFromRequestByAliasOrId(req);

137
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;

192
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;

270
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<LookupColumn>();
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<LinkToAnotherRecordColumn>();
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;
}

15
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,
};

18
packages/nocodb/src/lib/meta/api/dataApis/oldDataApis.ts → packages/nocodb/src/lib/controllers/dataControllers/oldDataController.ts

@ -1,14 +1,14 @@
import { Request, Response, Router } from 'express'; import { Request, Response, Router } from 'express';
import Model from '../../../models/Model'; import Model from '../../models/Model';
import { nocoExecute } from 'nc-help'; import { nocoExecute } from 'nc-help';
import Base from '../../../models/Base'; import Base from '../../models/Base';
import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2';
import View from '../../../models/View'; import View from '../../models/View';
import ncMetaAclMw from '../../helpers/ncMetaAclMw'; import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw';
import Project from '../../../models/Project'; import Project from '../../models/Project';
import { NcError } from '../../helpers/catchError'; import { NcError } from '../../meta/helpers/catchError';
import apiMetrics from '../../helpers/apiMetrics'; import apiMetrics from '../../meta/helpers/apiMetrics';
import getAst from '../../../db/sql-data-mapper/lib/sql/helpers/getAst'; import getAst from '../../db/sql-data-mapper/lib/sql/helpers/getAst';
export async function dataList(req: Request, res: Response) { export async function dataList(req: Request, res: Response) {
const { model, view } = await getViewAndModelFromRequest(req); const { model, view } = await getViewAndModelFromRequest(req);

6
packages/nocodb/src/lib/meta/api/exportApis.ts → packages/nocodb/src/lib/controllers/exportController.ts

@ -1,7 +1,7 @@
import { Request, Response, Router } from 'express'; import { Request, Response, Router } from 'express';
import View from '../../models/View'; import View from '../models/View';
import ncMetaAclMw from '../helpers/ncMetaAclMw'; import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { extractCsvData } from './dataApis/helpers'; import { extractCsvData } from './dataControllers/helpers';
async function exportCsv(req: Request, res: Response) { async function exportCsv(req: Request, res: Response) {
const view = await View.get(req.params.viewId); const view = await View.get(req.params.viewId);

115
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<any, any, FilterReqType>, 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<any, any, FilterReqType>,
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;

21
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;

48
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<FormType>) {
const formViewData = await formViewService.formViewGet({
formViewId: req.params.formViewId,
});
res.json(formViewData);
}
export async function formViewCreate(req: Request<any, any>, 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;

49
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<GalleryType>) {
res.json(
await galleryViewService.galleryViewGet({
galleryViewId: req.params.galleryViewId,
})
);
}
export async function galleryViewCreate(req: Request<any, any>, 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;

23
packages/nocodb/src/lib/meta/api/gridViewColumnApis.ts → packages/nocodb/src/lib/controllers/gridViewColumnController.ts

@ -1,17 +1,23 @@
import { Request, Response, Router } from 'express'; import { Request, Response, Router } from 'express';
import GridViewColumn from '../../models/GridViewColumn'; import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { Tele } from 'nc-help'; import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import ncMetaAclMw from '../helpers/ncMetaAclMw'; import { gridViewColumnService } from '../services';
import { metaApiMetrics } from '../helpers/apiMetrics';
import { getAjvValidatorMw } from './helpers';
export async function columnList(req: Request, res: Response) { 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) { export async function gridColumnUpdate(req: Request, res: Response) {
Tele.emit('evt', { evt_type: 'gridViewColumn:updated' }); res.json(
res.json(await GridViewColumn.update(req.params.gridViewColumnId, req.body)); await gridViewColumnService.gridColumnUpdate({
gridViewColumnId: req.params.gridViewColumnId,
grid: req.body,
})
);
} }
const router = Router({ mergeParams: true }); const router = Router({ mergeParams: true });
@ -23,7 +29,6 @@ router.get(
router.patch( router.patch(
'/api/v1/db/meta/grid-columns/:gridViewColumnId', '/api/v1/db/meta/grid-columns/:gridViewColumnId',
metaApiMetrics, metaApiMetrics,
getAjvValidatorMw('swagger.json#/components/schemas/GridColumnReq'),
ncMetaAclMw(gridColumnUpdate, 'gridColumnUpdate') ncMetaAclMw(gridColumnUpdate, 'gridColumnUpdate')
); );
export default router; export default router;

34
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<any>, 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;

98
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<any, any, any>,
res: Response<HookListType>
) {
// todo: pagination
res.json(
new PagedResponseImpl(
await hookService.hookList({ tableId: req.params.tableId })
)
);
}
export async function hookCreate(
req: Request<any, HookType>,
res: Response<HookType>
) {
const hook = await hookService.hookCreate({
hook: req.body,
tableId: req.params.tableId,
});
res.json(hook);
}
export async function hookDelete(
req: Request<any, HookType>,
res: Response<any>
) {
res.json(await hookService.hookDelete({ hookId: req.params.hookId }));
}
export async function hookUpdate(
req: Request<any, HookType>,
res: Response<HookType>
) {
res.json(
await hookService.hookUpdate({ hookId: req.params.hookId, hook: req.body })
);
}
export async function hookTest(req: Request<any, any>, 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;

90
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<any, any>, 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;

19
packages/nocodb/src/lib/meta/api/kanbanViewApis.ts → packages/nocodb/src/lib/controllers/kanbanViewController.ts

@ -1,18 +1,19 @@
import { Request, Response, Router } from 'express'; import { Request, Response, Router } from 'express';
import { KanbanType, ViewTypes } from 'nocodb-sdk'; import { KanbanType, ViewTypes } from 'nocodb-sdk';
import View from '../../models/View'; import View from '../models/View';
import KanbanView from '../../models/KanbanView'; import KanbanView from '../models/KanbanView';
import { Tele } from 'nc-help'; import { T } from 'nc-help';
import ncMetaAclMw from '../helpers/ncMetaAclMw'; import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { metaApiMetrics } from '../helpers/apiMetrics'; import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import { getAjvValidatorMw } from './helpers';
// todo: map to service
export async function kanbanViewGet(req: Request, res: Response<KanbanType>) { export async function kanbanViewGet(req: Request, res: Response<KanbanType>) {
res.json(await KanbanView.get(req.params.kanbanViewId)); res.json(await KanbanView.get(req.params.kanbanViewId));
} }
export async function kanbanViewCreate(req: Request<any, any>, res) { export async function kanbanViewCreate(req: Request<any, any>, 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({ const view = await View.insert({
...req.body, ...req.body,
// todo: sanitize // todo: sanitize
@ -23,7 +24,7 @@ export async function kanbanViewCreate(req: Request<any, any>, res) {
} }
export async function kanbanViewUpdate(req, 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)); res.json(await KanbanView.update(req.params.kanbanViewId, req.body));
} }
@ -32,13 +33,11 @@ const router = Router({ mergeParams: true });
router.post( router.post(
'/api/v1/db/meta/tables/:tableId/kanbans', '/api/v1/db/meta/tables/:tableId/kanbans',
metaApiMetrics, metaApiMetrics,
getAjvValidatorMw('swagger.json#/components/schemas/KanbanReq'),
ncMetaAclMw(kanbanViewCreate, 'kanbanViewCreate') ncMetaAclMw(kanbanViewCreate, 'kanbanViewCreate')
); );
router.patch( router.patch(
'/api/v1/db/meta/kanbans/:kanbanViewId', '/api/v1/db/meta/kanbans/:kanbanViewId',
metaApiMetrics, metaApiMetrics,
getAjvValidatorMw('swagger.json#/components/schemas/KanbanUpdateReq'),
ncMetaAclMw(kanbanViewUpdate, 'kanbanViewUpdate') ncMetaAclMw(kanbanViewUpdate, 'kanbanViewUpdate')
); );
router.get( router.get(

31
packages/nocodb/src/lib/meta/api/mapViewApis.ts → packages/nocodb/src/lib/controllers/mapViewController.ts

@ -1,29 +1,30 @@
import { Request, Response, Router } from 'express'; import { Request, Response, Router } from 'express';
import { MapType, ViewTypes } from 'nocodb-sdk'; import { MapType } from 'nocodb-sdk';
import View from '../../models/View'; import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import ncMetaAclMw from '../helpers/ncMetaAclMw'; import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import { Tele } from 'nc-help'; import { mapViewService } from '../services';
import { metaApiMetrics } from '../helpers/apiMetrics';
import MapView from '../../models/MapView';
export async function mapViewGet(req: Request, res: Response<MapType>) { export async function mapViewGet(req: Request, res: Response<MapType>) {
res.json(await MapView.get(req.params.mapViewId)); res.json(
await mapViewService.mapViewGet({ mapViewId: req.params.mapViewId })
);
} }
export async function mapViewCreate(req: Request<any, any>, res) { export async function mapViewCreate(req: Request<any, any>, res) {
Tele.emit('evt', { evt_type: 'vtable:created', show_as: 'map' }); const view = await mapViewService.mapViewCreate({
const view = await View.insert({ tableId: req.params.tableId,
...req.body, map: req.body,
// todo: sanitize
fk_model_id: req.params.tableId,
type: ViewTypes.MAP,
}); });
res.json(view); res.json(view);
} }
export async function mapViewUpdate(req, res) { export async function mapViewUpdate(req, res) {
Tele.emit('evt', { evt_type: 'view:updated', type: 'map' }); res.json(
res.json(await MapView.update(req.params.mapViewId, req.body)); await mapViewService.mapViewUpdate({
mapViewId: req.params.mapViewId,
map: req.body,
})
);
} }
const router = Router({ mergeParams: true }); const router = Router({ mergeParams: true });

54
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;

34
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;

17
packages/nocodb/src/lib/meta/api/orgLicenseApis.ts → packages/nocodb/src/lib/controllers/orgLicenseController.ts

@ -1,21 +1,15 @@
import { Router } from 'express'; import { Router } from 'express';
import { OrgUserRoles } from 'nocodb-sdk'; import { OrgUserRoles } from 'nocodb-sdk';
import { NC_LICENSE_KEY } from '../../constants'; import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import Store from '../../models/Store'; import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import Noco from '../../Noco'; import { orgLicenseService } from '../services';
import { metaApiMetrics } from '../helpers/apiMetrics';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { getAjvValidatorMw } from './helpers';
async function licenseGet(_req, res) { async function licenseGet(_req, res) {
const license = await Store.get(NC_LICENSE_KEY); res.json(await orgLicenseService.licenseGet());
res.json({ key: license?.value });
} }
async function licenseSet(req, res) { async function licenseSet(req, res) {
await Store.saveOrUpdate({ value: req.body.key, key: NC_LICENSE_KEY }); await orgLicenseService.licenseSet({ key: req.body.key });
await Noco.loadEEState();
res.json({ msg: 'License key saved' }); res.json({ msg: 'License key saved' });
} }
@ -31,7 +25,6 @@ router.get(
router.post( router.post(
'/api/v1/license', '/api/v1/license',
metaApiMetrics, metaApiMetrics,
getAjvValidatorMw('swagger.json#/components/schemas/LicenseReq'),
ncMetaAclMw(licenseSet, 'licenseSet', { ncMetaAclMw(licenseSet, 'licenseSet', {
allowedRoles: [OrgUserRoles.SUPER_ADMIN], allowedRoles: [OrgUserRoles.SUPER_ADMIN],
blockApiTokenAccess: true, blockApiTokenAccess: true,

63
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;

154
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<any> {
await orgUserService.userSettings({});
res.json({});
}
async function userInviteResend(req, res): Promise<any> {
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;

32
packages/nocodb/src/lib/meta/api/pluginApis.ts → packages/nocodb/src/lib/controllers/pluginController.ts

@ -1,38 +1,35 @@
import { Request, Response, Router } from 'express'; import { Request, Response, Router } from 'express';
import { Tele } from 'nc-help'; import { PagedResponseImpl } from '../meta/helpers/PagedResponse';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import Plugin from '../../models/Plugin';
import { PluginType } from 'nocodb-sdk'; import { PluginType } from 'nocodb-sdk';
import NcPluginMgrv2 from '../helpers/NcPluginMgrv2'; import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import ncMetaAclMw from '../helpers/ncMetaAclMw'; import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import { metaApiMetrics } from '../helpers/apiMetrics'; import { pluginService } from '../services';
import { getAjvValidatorMw } from './helpers';
export async function pluginList(_req: Request, res: Response) { 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<any, any>, res: Response) { export async function pluginTest(req: Request<any, any>, res: Response) {
Tele.emit('evt', { evt_type: 'plugin:tested' }); res.json(await pluginService.pluginTest({ body: req.body }));
res.json(await NcPluginMgrv2.test(req.body));
} }
export async function pluginRead(req: Request, res: Response) { 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( export async function pluginUpdate(
req: Request<any, any, PluginType>, req: Request<any, any, PluginType>,
res: Response res: Response
) { ) {
const plugin = await Plugin.update(req.params.pluginId, req.body); const plugin = await pluginService.pluginUpdate({
Tele.emit('evt', { pluginId: req.params.pluginId,
evt_type: plugin.active ? 'plugin:installed' : 'plugin:uninstalled', plugin: req.body,
title: plugin.title,
}); });
res.json(plugin); res.json(plugin);
} }
export async function isPluginActive(req: Request, res: Response) { 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 }); const router = Router({ mergeParams: true });
@ -44,8 +41,6 @@ router.get(
router.post( router.post(
'/api/v1/db/meta/plugins/test', '/api/v1/db/meta/plugins/test',
metaApiMetrics, metaApiMetrics,
getAjvValidatorMw('swagger.json#/components/schemas/PluginTestReq'),
ncMetaAclMw(pluginTest, 'pluginTest') ncMetaAclMw(pluginTest, 'pluginTest')
); );
router.get( router.get(
@ -56,7 +51,6 @@ router.get(
router.patch( router.patch(
'/api/v1/db/meta/plugins/:pluginId', '/api/v1/db/meta/plugins/:pluginId',
metaApiMetrics, metaApiMetrics,
getAjvValidatorMw('swagger.json#/components/schemas/PluginReq'),
ncMetaAclMw(pluginUpdate, 'pluginUpdate') ncMetaAclMw(pluginUpdate, 'pluginUpdate')
); );
router.get( router.get(

169
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<any, any, any>,
res: Response<Project>
) {
const project = await projectService.getProjectWithInfo({
projectId: req.params.projectId,
});
projectService.sanitizeProject(project);
res.json(project);
}
export async function projectUpdate(
req: Request<any, any, any>,
res: Response<ProjectListType>
) {
const project = await projectService.projectUpdate({
projectId: req.params.projectId,
project: req.body,
});
res.json(project);
}
export async function projectList(
req: Request<any> & { user: { id: string; roles: string } },
res: Response<ProjectListType>
) {
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<any>, res: Response<boolean>) {
const deleted = await projectService.projectSoftDelete({
projectId: req.params.projectId,
});
res.json(deleted);
}
async function projectCreate(req: Request<any, any>, 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')
);
};

85
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<any> {
res.json(
await projectUserService.userInvite({
projectId: req.params.projectId,
projectUser: req.body,
req,
})
);
}
// @ts-ignore
async function projectUserUpdate(req, res, next): Promise<any> {
res.json(
await projectUserService.projectUserUpdate({
projectUser: req.body,
projectId: req.params.projectId,
userId: req.params.userId,
req,
})
);
}
async function projectUserDelete(req, res): Promise<any> {
await projectUserService.projectUserDelete({
projectId: req.params.projectId,
userId: req.params.userId,
req,
});
res.json({
msg: 'success',
});
}
async function projectUserInviteResend(req, res): Promise<any> {
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;

9
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,
};

105
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;

20
packages/nocodb/src/lib/meta/api/publicApis/publicDataExportApis.ts → packages/nocodb/src/lib/controllers/publicControllers/publicDataExportController.ts

@ -1,17 +1,19 @@
import { Request, Response, Router } from 'express'; import { Request, Response, Router } from 'express';
import * as XLSX from 'xlsx'; 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 { nocoExecute } from 'nc-help';
import papaparse from 'papaparse'; import papaparse from 'papaparse';
import { ErrorMessages, isSystemColumn, UITypes, ViewTypes } from 'nocodb-sdk'; import { ErrorMessages, isSystemColumn, UITypes, ViewTypes } from 'nocodb-sdk';
import Column from '../../../models/Column'; import getAst from '../../db/sql-data-mapper/lib/sql/helpers/getAst';
import LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn'; import catchError, { NcError } from '../../meta/helpers/catchError';
import LookupColumn from '../../../models/LookupColumn'; import {
import catchError, { NcError } from '../../helpers/catchError'; Base,
import getAst from '../../../db/sql-data-mapper/lib/sql/helpers/getAst'; Column,
LinkToAnotherRecordColumn,
LookupColumn,
Model,
View,
} from '../../models';
import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2';
async function exportExcel(req: Request, res: Response) { async function exportExcel(req: Request, res: Response) {
const view = await View.getByUUID(req.params.publicDataUuid); const view = await View.getByUUID(req.params.publicDataUuid);

31
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<any> {
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;

61
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<any> {
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<any> {
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<any> {
const sharedBase = await sharedBaseService.disableSharedBaseLink({
projectId: req.params.projectId,
});
res.json(sharedBase);
}
async function getSharedBaseLink(req, res): Promise<any> {
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;

80
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<any, any, any>,
res: Response<SortListType>
) {
const sortList = await sortService.sortList({
viewId: req.params.viewId,
});
res.json({
sorts: new PagedResponseImpl(sortList),
});
}
// @ts-ignore
export async function sortCreate(req: Request<any, any, SortReqType>, 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;

36
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;

0
packages/nocodb/src/lib/meta/api/swagger/redocHtml.ts → packages/nocodb/src/lib/controllers/swaggerController/redocHtml.ts

0
packages/nocodb/src/lib/meta/api/swagger/swaggerHtml.ts → packages/nocodb/src/lib/controllers/swaggerController/swaggerHtml.ts

20
packages/nocodb/src/lib/meta/api/sync/importApis.ts → packages/nocodb/src/lib/controllers/syncController/importApis.ts

@ -1,13 +1,14 @@
import { Request, Router } from 'express'; import { Request, Router } from 'express';
// import { Queue } from 'bullmq'; // import { Queue } from 'bullmq';
// import axios from 'axios'; // import axios from 'axios';
import catchError, { NcError } from '../../helpers/catchError'; import catchError, { NcError } from '../../meta/helpers/catchError';
import { Server } from 'socket.io'; import { Server } from 'socket.io';
import NocoJobs from '../../../jobs/NocoJobs'; import NocoJobs from '../../jobs/NocoJobs';
import job, { AirtableSyncConfig } from './helpers/job'; import { SyncSource } from '../../models';
import SyncSource from '../../../models/SyncSource'; import Noco from '../../Noco';
import Noco from '../../../Noco'; import { syncService, userService } from '../../services';
import { genJwt } from '../userApi/helpers'; import { AirtableSyncConfig } from '../../services/syncService/helpers/job';
const AIRTABLE_IMPORT_JOB = 'AIRTABLE_IMPORT_JOB'; const AIRTABLE_IMPORT_JOB = 'AIRTABLE_IMPORT_JOB';
const AIRTABLE_PROGRESS_JOB = 'AIRTABLE_PROGRESS_JOB'; const AIRTABLE_PROGRESS_JOB = 'AIRTABLE_PROGRESS_JOB';
@ -23,7 +24,10 @@ export default (
jobs: { [id: string]: { last_message: any } } jobs: { [id: string]: { last_message: any } }
) => { ) => {
// add importer job handler and progress notification job handler // 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( NocoJobs.jobsMgr.addJobWorker(
AIRTABLE_PROGRESS_JOB, AIRTABLE_PROGRESS_JOB,
({ payload, progress }) => { ({ payload, progress }) => {
@ -94,7 +98,7 @@ export default (
const syncSource = await SyncSource.get(req.params.syncId); const syncSource = await SyncSource.get(req.params.syncId);
const user = await syncSource.getUser(); 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 // Treat default baseUrl as siteUrl from req object
let baseURL = (req as any).ncSiteUrl; let baseURL = (req as any).ncSiteUrl;

48
packages/nocodb/src/lib/meta/api/sync/syncSourceApis.ts → packages/nocodb/src/lib/controllers/syncController/index.ts

@ -1,42 +1,42 @@
import { Request, Response, Router } from 'express'; import { Request, Response, Router } from 'express';
import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw';
import SyncSource from '../../../models/SyncSource'; import { syncService } from '../../services';
import { Tele } from 'nc-help';
import { PagedResponseImpl } from '../../helpers/PagedResponse';
import ncMetaAclMw from '../../helpers/ncMetaAclMw';
import Project from '../../../models/Project';
export async function syncSourceList(req: Request, res: Response) { export async function syncSourceList(req: Request, res: Response) {
// todo: pagination // todo: pagination
res.json( res.json(
new PagedResponseImpl( syncService.syncSourceList({
await SyncSource.list(req.params.projectId, req.params.baseId) projectId: req.params.projectId,
) })
); );
} }
export async function syncCreate(req: Request, res: Response) { export async function syncCreate(req: Request, res: Response) {
Tele.emit('evt', { evt_type: 'webhooks:created' }); res.json(
const project = await Project.getWithInfo(req.params.projectId); await syncService.syncCreate({
projectId: req.params.projectId,
const sync = await SyncSource.insert({ baseId: req.params.baseId,
...req.body, userId: (req as any).user.id,
fk_user_id: (req as any).user.id, syncPayload: req.body,
base_id: req.params.baseId ? req.params.baseId : project.bases[0].id, })
project_id: req.params.projectId, );
});
res.json(sync);
} }
export async function syncDelete(req: Request, res: Response<any>) { export async function syncDelete(req: Request, res: Response<any>) {
Tele.emit('evt', { evt_type: 'webhooks:deleted' }); res.json(
res.json(await SyncSource.delete(req.params.syncId)); await syncService.syncDelete({
syncId: req.params.syncId,
})
);
} }
export async function syncUpdate(req: Request, res: Response) { export async function syncUpdate(req: Request, res: Response) {
Tele.emit('evt', { evt_type: 'webhooks:updated' }); res.json(
await syncService.syncUpdate({
res.json(await SyncSource.update(req.params.syncId, req.body)); syncId: req.params.syncId,
syncPayload: req.body,
})
);
} }
const router = Router({ mergeParams: true }); const router = Router({ mergeParams: true });

111
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<TableListType>) {
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<any, any, TableReqType>, 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<TableType>) {
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<any, any>, 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;

2
packages/nocodb/src/lib/meta/api/testApis.ts → packages/nocodb/src/lib/controllers/testController.ts

@ -1,5 +1,5 @@
import { Request, Router } from 'express'; import { Request, Router } from 'express';
import { TestResetService } from '../../services/test/TestResetService'; import { TestResetService } from '../services/test/TestResetService';
export async function reset(req: Request<any, any>, res) { export async function reset(req: Request<any, any>, res) {
const service = new TestResetService({ const service = new TestResetService({

445
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<TableType>) {
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<any> =>
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<any> =>
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<any> {
res.json(req?.session?.passport?.user ?? {});
}
async function passwordChange(req: Request<any, any>, res): Promise<any> {
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<any, any>, res): Promise<any> {
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<any> {
await userService.tokenValidate({
token: req.params.tokenId,
});
res.json(true);
}
async function passwordReset(req, res): Promise<any> {
await userService.passwordReset({
token: req.params.tokenId,
body: req.body,
req,
});
res.json({ msg: 'Password reset successful' });
}
async function emailVerification(req, res): Promise<any> {
await userService.emailVerification({
token: req.params.tokenId,
req,
});
res.json({ msg: 'Email verified successfully' });
}
async function refreshToken(req, res): Promise<any> {
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<any> {
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 };

16
packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts → packages/nocodb/src/lib/controllers/userController/initStrategies.ts

@ -1,6 +1,4 @@
import { OrgUserRoles } from 'nocodb-sdk'; import { OrgUserRoles } from 'nocodb-sdk';
import User from '../../../models/User';
import ProjectUser from '../../../models/ProjectUser';
import { promisify } from 'util'; import { promisify } from 'util';
import { Strategy as CustomStrategy } from 'passport-custom'; import { Strategy as CustomStrategy } from 'passport-custom';
import passport from 'passport'; import passport from 'passport';
@ -17,13 +15,11 @@ const jwtOptions = {
}; };
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import Project from '../../../models/Project'; import NocoCache from '../../cache/NocoCache';
import NocoCache from '../../../cache/NocoCache'; import { ApiToken, Plugin, Project, ProjectUser, User } from '../../models';
import { CacheGetType, CacheScope } from '../../../utils/globals'; import Noco from '../../Noco';
import ApiToken from '../../../models/ApiToken'; import { CacheGetType, CacheScope } from '../../utils/globals';
import Noco from '../../../Noco'; import { userService } from '../../services';
import Plugin from '../../../models/Plugin';
import { registerNewUserIfAllowed } from './userApis';
export function initStrategies(router): void { export function initStrategies(router): void {
passport.use( passport.use(
@ -311,7 +307,7 @@ export function initStrategies(router): void {
// or return error // or return error
} else { } else {
const salt = await promisify(bcrypt.genSalt)(10); const salt = await promisify(bcrypt.genSalt)(10);
const user = await registerNewUserIfAllowed({ const user = await userService.registerNewUserIfAllowed({
firstname: null, firstname: null,
lastname: null, lastname: null,
email_verification_token: null, email_verification_token: null,

0
packages/nocodb/src/lib/meta/api/userApi/ui/auth/emailVerify.ts → packages/nocodb/src/lib/controllers/userController/ui/auth/emailVerify.ts

0
packages/nocodb/src/lib/meta/api/userApi/ui/auth/resetPassword.ts → packages/nocodb/src/lib/controllers/userController/ui/auth/resetPassword.ts

0
packages/nocodb/src/lib/meta/api/userApi/ui/emailTemplates/forgotPassword.ts → packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/forgotPassword.ts

0
packages/nocodb/src/lib/meta/api/userApi/ui/emailTemplates/invite.ts → packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/invite.ts

0
packages/nocodb/src/lib/meta/api/userApi/ui/emailTemplates/verify.ts → packages/nocodb/src/lib/controllers/userController/ui/emailTemplates/verify.ts

303
packages/nocodb/src/lib/meta/api/userApi/userApis.ts → packages/nocodb/src/lib/controllers/userController/userApis.ts

@ -1,82 +1,24 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { TableType, validatePassword } from 'nocodb-sdk'; import { TableType, validatePassword } from 'nocodb-sdk';
import { OrgUserRoles } from 'nocodb-sdk'; import { T } from 'nc-help';
import { NC_APP_SETTINGS } from '../../../constants';
import Store from '../../../models/Store';
import { Tele } from 'nc-help';
import catchError, { NcError } from '../../helpers/catchError';
const { isEmail } = require('validator'); const { isEmail } = require('validator');
import * as ejs from 'ejs'; import * as ejs from 'ejs';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { promisify } from 'util'; import { promisify } from 'util';
import User from '../../../models/User';
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
import Audit from '../../../models/Audit';
import NcPluginMgrv2 from '../../helpers/NcPluginMgrv2';
import passport from 'passport'; import passport from 'passport';
import extractProjectIdAndAuthenticate from '../../helpers/extractProjectIdAndAuthenticate'; import { getAjvValidatorMw } from '../../meta/api/helpers';
import ncMetaAclMw from '../../helpers/ncMetaAclMw'; import catchError, { NcError } from '../../meta/helpers/catchError';
import { MetaTable } from '../../../utils/globals'; import extractProjectIdAndAuthenticate from '../../meta/helpers/extractProjectIdAndAuthenticate';
import Noco from '../../../Noco'; import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw';
import { getAjvValidatorMw } from '../helpers'; import NcPluginMgrv2 from '../../meta/helpers/NcPluginMgrv2';
import { genJwt } from './helpers'; import { Audit, User } from '../../models';
import { randomTokenString } from '../../helpers/stringHelpers'; import Noco from '../../Noco';
import { userService } from '../../services';
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,
});
}
export async function signup(req: Request, res: Response<TableType>) { export async function signup(req: Request, res: Response<TableType>) {
const { const {
@ -122,7 +64,7 @@ export async function signup(req: Request, res: Response<TableType>) {
const email_verification_token = uuidv4(); const email_verification_token = uuidv4();
if (!ignore_subscribe) { if (!ignore_subscribe) {
Tele.emit('evt_subscribe', email); T.emit('evt_subscribe', email);
} }
if (user) { if (user) {
@ -141,7 +83,7 @@ export async function signup(req: Request, res: Response<TableType>) {
NcError.badRequest('User already exist'); NcError.badRequest('User already exist');
} }
} else { } else {
await registerNewUserIfAllowed({ await userService.registerNewUserIfAllowed({
firstname, firstname,
lastname, lastname,
email, email,
@ -171,7 +113,7 @@ export async function signup(req: Request, res: Response<TableType>) {
); );
} }
await promisify((req as any).login.bind(req))(user); await promisify((req as any).login.bind(req))(user);
const refreshToken = randomTokenString(); const refreshToken = userService.randomTokenString();
await User.update(user.id, { await User.update(user.id, {
refresh_token: refreshToken, refresh_token: refreshToken,
email: user.email, email: user.email,
@ -190,7 +132,7 @@ export async function signup(req: Request, res: Response<TableType>) {
}); });
res.json({ res.json({
token: genJwt(user, Noco.getConfig()), token: userService.genJwt(user, Noco.getConfig()),
} as any); } as any);
} }
@ -214,10 +156,10 @@ async function successfulSignIn({
} }
await promisify((req as any).login.bind(req))(user); await promisify((req as any).login.bind(req))(user);
const refreshToken = randomTokenString(); const refreshToken = userService.randomTokenString();
if (!user.token_version) { if (!user.token_version) {
user.token_version = randomTokenString(); user.token_version = userService.randomTokenString();
} }
await User.update(user.id, { await User.update(user.id, {
@ -236,7 +178,7 @@ async function successfulSignIn({
}); });
res.json({ res.json({
token: genJwt(user, Noco.getConfig()), token: userService.genJwt(user, Noco.getConfig()),
} as any); } as any);
} catch (e) { } catch (e) {
console.log(e); console.log(e);
@ -279,15 +221,13 @@ async function googleSignin(req, res, next) {
)(req, res, next); )(req, res, next);
} }
const REFRESH_TOKEN_COOKIE_KEY = 'refresh_token'; function setTokenCookie(res: Response, token): void {
function setTokenCookie(res, token): void {
// create http only cookie with refresh token that expires in 7 days // create http only cookie with refresh token that expires in 7 days
const cookieOptions = { const cookieOptions = {
httpOnly: true, httpOnly: true,
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), 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<any> { async function me(req, res): Promise<any> {
@ -298,184 +238,47 @@ async function passwordChange(req: Request<any, any>, res): Promise<any> {
if (!(req as any).isAuthenticated()) { if (!(req as any).isAuthenticated()) {
NcError.forbidden('Not allowed'); 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({ await userService.passwordChange({
op_type: 'AUTHENTICATION', user: req['user'],
op_sub_type: 'PASSWORD_CHANGE', req,
user: user.email, body: req.body,
description: `changed password `,
ip: (req as any).clientIp,
}); });
res.json({ msg: 'Password updated successfully' }); res.json({ msg: 'Password updated successfully' });
} }
async function passwordForgot(req: Request<any, any>, res): Promise<any> { async function passwordForgot(req: Request<any, any>, res): Promise<any> {
const _email = req.body.email; await userService.passwordForgot({
if (!_email) { siteUrl: (req as any).ncSiteUrl,
NcError.badRequest('Please enter your email address.'); body: req.body,
} req,
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 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' }); res.json({ msg: 'Please check your email to reset the password' });
} }
async function tokenValidate(req, res): Promise<any> { async function tokenValidate(req, res): Promise<any> {
const token = req.params.tokenId; await userService.tokenValidate({
token: req.params.tokenId,
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');
}
res.json(true); res.json(true);
} }
async function passwordReset(req, res): Promise<any> { async function passwordReset(req, res): Promise<any> {
const token = req.params.tokenId; await userService.passwordReset({
token: req.params.tokenId,
const user = await Noco.ncMeta.metaGet(null, null, MetaTable.USERS, { body: req.body,
reset_password_token: token, req,
});
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,
}); });
res.json({ msg: 'Password reset successful' }); res.json({ msg: 'Password reset successful' });
} }
async function emailVerification(req, res): Promise<any> { async function emailVerification(req, res): Promise<any> {
const token = req.params.tokenId; await userService.emailVerification({
token: req.params.tokenId,
const user = await Noco.ncMeta.metaGet(null, null, MetaTable.USERS, { req,
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,
}); });
res.json({ msg: 'Email verified successfully' }); res.json({ msg: 'Email verified successfully' });
@ -487,15 +290,13 @@ async function refreshToken(req, res): Promise<any> {
return res.status(400).json({ msg: 'Missing refresh token' }); return res.status(400).json({ msg: 'Missing refresh token' });
} }
const user = await User.getByRefreshToken( const user = await User.getByRefreshToken(req.cookies.refresh_token);
req.cookies[REFRESH_TOKEN_COOKIE_KEY]
);
if (!user) { if (!user) {
return res.status(400).json({ msg: 'Invalid refresh token' }); return res.status(400).json({ msg: 'Invalid refresh token' });
} }
const refreshToken = randomTokenString(); const refreshToken = userService.randomTokenString();
await User.update(user.id, { await User.update(user.id, {
email: user.email, email: user.email,
@ -505,7 +306,7 @@ async function refreshToken(req, res): Promise<any> {
setTokenCookie(res, refreshToken); setTokenCookie(res, refreshToken);
res.json({ res.json({
token: genJwt(user, Noco.getConfig()), token: userService.genJwt(user, Noco.getConfig()),
} as any); } as any);
} catch (e) { } catch (e) {
return res.status(400).json({ msg: e.message }); return res.status(400).json({ msg: e.message });
@ -526,30 +327,6 @@ async function renderPasswordReset(req, res): Promise<any> {
} }
} }
// clear refresh token cookie and update user refresh token to null
const signout = async (req, res): Promise<any> => {
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) => { const mapRoutes = (router) => {
// todo: old api - /auth/signup?tool=1 // todo: old api - /auth/signup?tool=1
router.post( router.post(
@ -562,7 +339,6 @@ const mapRoutes = (router) => {
getAjvValidatorMw('swagger.json#/components/schemas/SignInReq'), getAjvValidatorMw('swagger.json#/components/schemas/SignInReq'),
catchError(signin) catchError(signin)
); );
router.post('/auth/user/signout', catchError(signout));
router.get('/auth/user/me', extractProjectIdAndAuthenticate, catchError(me)); router.get('/auth/user/me', extractProjectIdAndAuthenticate, catchError(me));
router.post( router.post(
'/auth/password/forgot', '/auth/password/forgot',
@ -651,7 +427,6 @@ const mapRoutes = (router) => {
getAjvValidatorMw('swagger.json#/components/schemas/SignInReq'), getAjvValidatorMw('swagger.json#/components/schemas/SignInReq'),
catchError(signin) catchError(signin)
); );
router.post('/api/v1/auth/user/signout', catchError(signout));
router.get( router.get(
'/api/v1/auth/user/me', '/api/v1/auth/user/me',
extractProjectIdAndAuthenticate, extractProjectIdAndAuthenticate,
@ -683,4 +458,4 @@ const mapRoutes = (router) => {
// respond with password reset page // respond with password reset page
router.get('/auth/password/reset/:tokenId', catchError(renderPasswordReset)); router.get('/auth/password/reset/:tokenId', catchError(renderPasswordReset));
}; };
export { mapRoutes as userApis }; export { mapRoutes as userController };

59
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')
);
};

38
packages/nocodb/src/lib/meta/api/viewColumnApis.ts → packages/nocodb/src/lib/controllers/viewColumnController.ts

@ -1,34 +1,30 @@
import { Request, Response, Router } from 'express'; import { Request, Response, Router } from 'express';
import View from '../../models/View'; import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { Tele } from 'nc-help'; import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import ncMetaAclMw from '../helpers/ncMetaAclMw'; import { viewColumnService } from '../services';
import { metaApiMetrics } from '../helpers/apiMetrics';
import { getAjvValidatorMw } from './helpers';
export async function columnList(req: Request, res: Response) { 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) { export async function columnAdd(req: Request, res: Response) {
const viewColumn = await View.insertOrUpdateColumn( const viewColumn = await viewColumnService.columnAdd({
req.params.viewId, viewId: req.params.viewId,
req.body.fk_column_id, columnId: req.body.fk_column_id,
{ column: {
...req.body, ...req.body,
view_id: req.params.viewId, view_id: req.params.viewId,
} },
); });
Tele.emit('evt', { evt_type: 'viewColumn:inserted' });
res.json(viewColumn); res.json(viewColumn);
} }
export async function columnUpdate(req: Request, res: Response) { export async function columnUpdate(req: Request, res: Response) {
const result = await View.updateColumn( const result = await viewColumnService.columnUpdate({
req.params.viewId, viewId: req.params.viewId,
req.params.columnId, columnId: req.params.columnId,
req.body column: req.body,
); });
Tele.emit('evt', { evt_type: 'viewColumn:updated' });
res.json(result); res.json(result);
} }
@ -41,13 +37,11 @@ router.get(
router.post( router.post(
'/api/v1/db/meta/views/:viewId/columns/', '/api/v1/db/meta/views/:viewId/columns/',
metaApiMetrics, metaApiMetrics,
getAjvValidatorMw('swagger.json#/components/schemas/ViewColumnReq'),
ncMetaAclMw(columnAdd, 'columnAdd') ncMetaAclMw(columnAdd, 'columnAdd')
); );
router.patch( router.patch(
'/api/v1/db/meta/views/:viewId/columns/:columnId', '/api/v1/db/meta/views/:viewId/columns/:columnId',
metaApiMetrics, metaApiMetrics,
getAjvValidatorMw('swagger.json#/components/schemas/ViewColumnUpdateReq'),
ncMetaAclMw(columnUpdate, 'viewColumnUpdate') ncMetaAclMw(columnUpdate, 'viewColumnUpdate')
); );
export default router; export default router;

133
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<any, any, any>, 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<any, any, any>,
res: Response<View>
) {
res.json(await viewService.shareView({ viewId: req.params.viewId }));
}
// @ts-ignore
export async function viewCreate(req: Request<any, any>, 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<any, any>, res) {
res.json(
await viewService.shareViewUpdate({
viewId: req.params.viewId,
sharedView: req.body,
})
);
}
async function shareViewDelete(req: Request<any, any>, res) {
res.json(await viewService.shareViewDelete({ viewId: req.params.viewId }));
}
async function showAllColumns(req: Request<any, any>, res) {
res.json(
await viewService.showAllColumns({
viewId: req.params.viewId,
ignoreIds: <string[]>(req.query?.ignoreIds || []),
})
);
}
async function hideAllColumns(req: Request<any, any>, res) {
res.json(
await viewService.hideAllColumns({
viewId: req.params.viewId,
ignoreIds: <string[]>(req.query?.ignoreIds || []),
})
);
}
async function shareViewList(req: Request<any, any>, 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;

4
packages/nocodb/src/lib/db/sql-client/lib/KnexClient.ts

@ -1,6 +1,6 @@
/* eslint-disable no-constant-condition */ /* eslint-disable no-constant-condition */
import { knex, Knex } from 'knex'; import { knex, Knex } from 'knex';
import { Tele } from 'nc-help'; import { T } from 'nc-help';
import Debug from '../../util/Debug'; import Debug from '../../util/Debug';
import Emit from '../../util/emit'; import Emit from '../../util/emit';
import Result from '../../util/Result'; import Result from '../../util/Result';
@ -620,7 +620,7 @@ class KnexClient extends SqlClient {
KnexClient.___ext = await this._validateInput(); KnexClient.___ext = await this._validateInput();
} }
if (!KnexClient.___ext) { if (!KnexClient.___ext) {
Tele.emit('evt', { T.emit('evt', {
evt_type: 'project:external', evt_type: 'project:external',
payload: null, payload: null,
check: true, check: true,

4
packages/nocodb/src/lib/db/sql-mgr/SqlMgr.ts

@ -7,7 +7,7 @@ import fsExtra from 'fs-extra';
import importFresh from 'import-fresh'; import importFresh from 'import-fresh';
import inflection from 'inflection'; import inflection from 'inflection';
import slash from 'slash'; import slash from 'slash';
import { Tele } from 'nc-help'; import { T } from 'nc-help';
import SqlClientFactory from '../sql-client/lib/SqlClientFactory'; import SqlClientFactory from '../sql-client/lib/SqlClientFactory';
// import debug from 'debug'; // import debug from 'debug';
@ -1047,7 +1047,7 @@ export default class SqlMgr {
// t = process.hrtime(); // t = process.hrtime();
const data = await require('axios')(...apiMeta); 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; return data.data;
} }

48
packages/nocodb/src/lib/meta/NcMetaMgr.ts

@ -40,7 +40,7 @@ import NcTemplateParser from '../v1-legacy/templates/NcTemplateParser';
import { defaultConnectionConfig } from '../utils/NcConfigFactory'; import { defaultConnectionConfig } from '../utils/NcConfigFactory';
import xcMetaDiff from './handlers/xcMetaDiff'; import xcMetaDiff from './handlers/xcMetaDiff';
import { UITypes } from 'nocodb-sdk'; import { UITypes } from 'nocodb-sdk';
import { Tele } from 'nc-help'; import { T } from 'nc-help';
import { NC_ATTACHMENT_FIELD_SIZE } from '../constants'; import { NC_ATTACHMENT_FIELD_SIZE } from '../constants';
const randomID = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 10); const randomID = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 10);
const XC_PLUGIN_DET = 'XC_PLUGIN_DET'; const XC_PLUGIN_DET = 'XC_PLUGIN_DET';
@ -953,7 +953,7 @@ export default class NcMetaMgr {
ip: req.clientIp, ip: req.clientIp,
}); });
Tele.emit('evt', { evt_type: 'webhooks:deleted' }); T.emit('evt', { evt_type: 'webhooks:deleted' });
} catch (e) { } catch (e) {
throw e; throw e;
} }
@ -988,7 +988,7 @@ export default class NcMetaMgr {
ip: req.clientIp, ip: req.clientIp,
}); });
Tele.emit('evt', { evt_type: 'webhooks:updated' }); T.emit('evt', { evt_type: 'webhooks:updated' });
} else { } else {
const res = await this.xcMeta.metaInsert( const res = await this.xcMeta.metaInsert(
projectId, 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} `, 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, ip: req.clientIp,
}); });
Tele.emit('evt', { evt_type: 'webhooks:created' }); T.emit('evt', { evt_type: 'webhooks:created' });
return res; return res;
} }
@ -1269,7 +1269,7 @@ export default class NcMetaMgr {
} catch (e) { } catch (e) {
throw e; throw e;
} finally { } 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, ip: req.clientIp,
}); });
Tele.emit('evt', { evt_type: 'project:created' }); T.emit('evt', { evt_type: 'project:created' });
break; break;
case 'projectUpdateByWeb': case 'projectUpdateByWeb':
@ -1603,7 +1603,7 @@ export default class NcMetaMgr {
this.getProjectId(args), this.getProjectId(args),
args.args.projectJson args.args.projectJson
); );
Tele.emit('evt', { evt_type: 'project:updated' }); T.emit('evt', { evt_type: 'project:updated' });
break; break;
case 'projectCreateByOneClick': case 'projectCreateByOneClick':
{ {
@ -1635,7 +1635,7 @@ export default class NcMetaMgr {
description: `created project ${config.title}(${result.id}) `, description: `created project ${config.title}(${result.id}) `,
ip: req.clientIp, ip: req.clientIp,
}); });
Tele.emit('evt', { evt_type: 'project:created', oneClick: true }); T.emit('evt', { evt_type: 'project:created', oneClick: true });
} }
break; break;
case 'projectCreateByWebWithXCDB': { case 'projectCreateByWebWithXCDB': {
@ -1689,10 +1689,10 @@ export default class NcMetaMgr {
ip: req.clientIp, ip: req.clientIp,
}); });
Tele.emit('evt', { evt_type: 'project:created', xcdb: true }); T.emit('evt', { evt_type: 'project:created', xcdb: true });
postListenerCb = async () => { postListenerCb = async () => {
if (args?.args?.template) { if (args?.args?.template) {
Tele.emit('evt', { T.emit('evt', {
evt_type: args.args?.quickImport evt_type: args.args?.quickImport
? 'project:created:fromExcel' ? 'project:created:fromExcel'
: 'project:created:fromTemplate', : 'project:created:fromTemplate',
@ -1730,7 +1730,7 @@ export default class NcMetaMgr {
case 'projectDelete': case 'projectDelete':
case 'projectRestart': case 'projectRestart':
case 'projectStart': case 'projectStart':
Tele.emit('evt', { evt_type: 'project:' + args.api }); T.emit('evt', { evt_type: 'project:' + args.api });
result = null; result = null;
break; break;
@ -2145,7 +2145,7 @@ export default class NcMetaMgr {
ip: req.clientIp, ip: req.clientIp,
}); });
Tele.emit('evt', { evt_type: 'acl:updated' }); T.emit('evt', { evt_type: 'acl:updated' });
return res; return res;
} catch (e) { } catch (e) {
@ -3485,7 +3485,7 @@ export default class NcMetaMgr {
['id', 'view_id', 'view_type'] ['id', 'view_id', 'view_type']
); );
res.url = `${req.ncSiteUrl}${this.config.dashboardPath}#/nc/view/${res.view_id}`; 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; return res;
} catch (e) { } catch (e) {
console.log(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}`; 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; return sharedBase;
} catch (e) { } catch (e) {
console.log(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', { // await this.xcMeta.metaUpdate(this.getProjectId(args), this.getDbAlias(args), 'nc_shared_views', {
// password: args.args?.password // password: args.args?.password
// }, args.args.id); // }, args.args.id);
// Tele.emit('evt', {evt_type: 'sharedView:password-updated'}) // T.emit('evt', {evt_type: 'sharedView:password-updated'})
// return {msg: 'Success'}; // return {msg: 'Success'};
// } catch (e) { // } catch (e) {
// console.log(e) // console.log(e)
@ -3623,7 +3623,7 @@ export default class NcMetaMgr {
'nc_shared_views', 'nc_shared_views',
args.args.id args.args.id
); );
Tele.emit('evt', { evt_type: 'sharedView:deleted' }); T.emit('evt', { evt_type: 'sharedView:deleted' });
return { msg: 'Success' }; return { msg: 'Success' };
} catch (e) { } catch (e) {
console.log(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; return result;
} }
@ -5071,7 +5071,7 @@ export default class NcMetaMgr {
} catch (e) { } catch (e) {
throw e; throw e;
} finally { } finally {
Tele.emit('evt', { T.emit('evt', {
evt_type: 'plugin:installed', evt_type: 'plugin:installed',
title: args.args.title, title: args.args.title,
}); });
@ -5153,7 +5153,7 @@ export default class NcMetaMgr {
ip: req.clientIp, ip: req.clientIp,
}); });
Tele.emit('evt', { T.emit('evt', {
evt_type: 'vtable:created', evt_type: 'vtable:created',
show_as: args.args.show_as, show_as: args.args.show_as,
}); });
@ -5270,7 +5270,7 @@ export default class NcMetaMgr {
}); });
await RestAuthCtrl.instance.loadLatestApiTokens(); await RestAuthCtrl.instance.loadLatestApiTokens();
Tele.emit('evt', { evt_type: 'apiToken:created' }); T.emit('evt', { evt_type: 'apiToken:created' });
return { return {
description: args.args.description, description: args.args.description,
token, token,
@ -5282,7 +5282,7 @@ export default class NcMetaMgr {
} }
protected async xcApiTokenDelete(args): Promise<any> { protected async xcApiTokenDelete(args): Promise<any> {
Tele.emit('evt', { evt_type: 'apiToken:deleted' }); T.emit('evt', { evt_type: 'apiToken:deleted' });
const res = await this.xcMeta.metaDelete( const res = await this.xcMeta.metaDelete(
null, null,
null, null,
@ -5327,7 +5327,7 @@ export default class NcMetaMgr {
ip: req.clientIp, ip: req.clientIp,
}); });
Tele.emit('evt', { T.emit('evt', {
evt_type: 'vtable:renamed', evt_type: 'vtable:renamed',
show_as: args.args.show_as, show_as: args.args.show_as,
}); });
@ -5335,7 +5335,7 @@ export default class NcMetaMgr {
} }
protected async xcVirtualTableUpdate(args): Promise<any> { protected async xcVirtualTableUpdate(args): Promise<any> {
// 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( return this.xcMeta.metaUpdate(
this.getProjectId(args), this.getProjectId(args),
this.getDbAlias(args), this.getDbAlias(args),
@ -5482,7 +5482,7 @@ export default class NcMetaMgr {
ip: req.clientIp, ip: req.clientIp,
}); });
Tele.emit('evt', { evt_type: 'vtable:deleted' }); T.emit('evt', { evt_type: 'vtable:deleted' });
return res; return res;
} }

8
packages/nocodb/src/lib/meta/NcMetaMgrEE.ts

@ -1,5 +1,5 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { Tele } from 'nc-help'; import { T } from 'nc-help';
import NcMetaMgr from './NcMetaMgr'; import NcMetaMgr from './NcMetaMgr';
@ -104,7 +104,7 @@ export default class NcMetaMgrEE extends NcMetaMgr {
ip: req.clientIp, ip: req.clientIp,
}); });
Tele.emit('evt', { evt_type: 'acl:updated' }); T.emit('evt', { evt_type: 'acl:updated' });
return res; return res;
} catch (e) { } catch (e) {
@ -277,7 +277,7 @@ export default class NcMetaMgrEE extends NcMetaMgr {
sharedView.url = `${req.ncSiteUrl}${this.config.dashboardPath}#/nc/view/${sharedView.view_id}`; 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; return sharedView;
} catch (e) { } catch (e) {
console.log(e); console.log(e);
@ -295,7 +295,7 @@ export default class NcMetaMgrEE extends NcMetaMgr {
}, },
args.args.id args.args.id
); );
Tele.emit('evt', { evt_type: 'sharedView:password-updated' }); T.emit('evt', { evt_type: 'sharedView:password-updated' });
return { msg: 'Success' }; return { msg: 'Success' };
} catch (e) { } catch (e) {
console.log(e); console.log(e);

53
packages/nocodb/src/lib/meta/api/apiTokenApis.ts

@ -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;

225
packages/nocodb/src/lib/meta/api/attachmentApis.ts

@ -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;

129
packages/nocodb/src/lib/meta/api/baseApis.ts

@ -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<any, any, any>,
res: Response<Base>
) {
const base = await Base.get(req.params.baseId);
base.config = base.getConnectionConfig();
res.json(base);
}
export async function baseUpdate(
req: Request<any, any, any>,
res: Response<any>
) {
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<any, any, any>,
res: Response<BaseListType>,
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<any, any, any>,
res: Response<any>
) {
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<any, any>, 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')
);
};

101
packages/nocodb/src/lib/meta/api/dataApis/bulkDataAliasApis.ts

@ -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<T extends BulkOperation>(
req: Request,
res: Response,
operation: T,
options: Parameters<typeof BaseModelSqlv2.prototype[T]>
) {
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;

432
packages/nocodb/src/lib/meta/api/dataApis/dataAliasApis.ts

@ -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;

291
packages/nocodb/src/lib/meta/api/dataApis/dataAliasNestedApis.ts

@ -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;

612
packages/nocodb/src/lib/meta/api/dataApis/dataApis.ts

@ -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;

15
packages/nocodb/src/lib/meta/api/dataApis/index.ts

@ -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,
};

22
packages/nocodb/src/lib/meta/api/ee/orgTokenApis.ts

@ -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({}),
}
)
);
}

175
packages/nocodb/src/lib/meta/api/filterApis.ts

@ -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<any, any, any, TableListParams>,
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<any, any, any, TableListParams>,
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<any, any, TableReq>,
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<any, any, any, TableListParams>,
res: Response
) {
const filter = await Filter.rootFilterListByHook({
hookId: req.params.hookId,
});
res.json(filter);
}
export async function hookFilterCreate(req: Request<any, any, TableReq>, 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;

66
packages/nocodb/src/lib/meta/api/formViewApis.ts

@ -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<FormType>) {
const formViewData = await FormView.getWithInfo(req.params.formViewId);
res.json(formViewData);
}
export async function formViewCreate(req: Request<any, any>, 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;

20
packages/nocodb/src/lib/meta/api/formViewColumnApis.ts

@ -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;

47
packages/nocodb/src/lib/meta/api/galleryViewApis.ts

@ -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<GalleryType>) {
res.json(await GalleryView.get(req.params.galleryViewId));
}
export async function galleryViewCreate(req: Request<any, any>, 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;

47
packages/nocodb/src/lib/meta/api/gridViewApis.ts

@ -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<any, any>, 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;

22
packages/nocodb/src/lib/meta/api/helpers/apiHelpers.ts

@ -3,6 +3,7 @@ import Ajv, { ErrorObject } from 'ajv';
import addFormats from 'ajv-formats'; import addFormats from 'ajv-formats';
// @ts-ignore // @ts-ignore
import swagger from '../../../../schema/swagger.json'; import swagger from '../../../../schema/swagger.json';
import { NcError } from '../../helpers/catchError';
export function parseHrtimeToSeconds(hrtime) { export function parseHrtimeToSeconds(hrtime) {
const seconds = (hrtime[0] + hrtime[1] / 1e6).toFixed(3); 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 // If the request body is invalid, send a response with an error message
res.status(400).json({ res.status(400).json({
status: 'error',
message: 'Invalid request body', message: 'Invalid request body',
errors, 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,
});
}
};

4
packages/nocodb/src/lib/meta/api/helpers/columnHelpers.ts

@ -1,9 +1,9 @@
import { customAlphabet } from 'nanoid'; import { customAlphabet } from 'nanoid';
import { import {
BoolType,
ColumnReqType, ColumnReqType,
LinkToAnotherRecordType, LinkToAnotherRecordType,
LookupColumnReqType, LookupColumnReqType,
BoolType,
RelationTypes, RelationTypes,
RollupColumnReqType, RollupColumnReqType,
TableType, TableType,
@ -77,7 +77,7 @@ export async function createHmAndBtColumn(
} }
} }
export async function validateRollupPayload(payload: ColumnReqType) { export async function validateRollupPayload(payload: ColumnReqType | Column) {
validateParams( validateParams(
[ [
'title', 'title',

2
packages/nocodb/src/lib/meta/api/helpers/populateMeta.ts

@ -11,7 +11,7 @@ import getTableNameAlias, {
import LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn'; import LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn';
import getColumnUiType from '../../helpers/getColumnUiType'; import getColumnUiType from '../../helpers/getColumnUiType';
import mapDefaultDisplayValue from '../../helpers/mapDefaultDisplayValue'; import mapDefaultDisplayValue from '../../helpers/mapDefaultDisplayValue';
import { extractAndGenerateManyToManyRelations } from '../metaDiffApis'; import { extractAndGenerateManyToManyRelations } from '../../../services/metaDiffService';
import { ModelTypes, UITypes, ViewTypes } from 'nocodb-sdk'; import { ModelTypes, UITypes, ViewTypes } from 'nocodb-sdk';
import { IGNORE_TABLES } from '../../../utils/common/BaseApiBuilder'; import { IGNORE_TABLES } from '../../../utils/common/BaseApiBuilder';

113
packages/nocodb/src/lib/meta/api/hookApis.ts

@ -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<any, any, any>,
res: Response<HookListType>
) {
res.json(
new PagedResponseImpl(await Hook.list({ fk_model_id: req.params.tableId }))
);
}
export async function hookCreate(
req: Request<any, HookType>,
res: Response<HookType>
) {
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<any, HookType>,
res: Response<any>
) {
Tele.emit('evt', { evt_type: 'webhooks:deleted' });
res.json(await Hook.delete(req.params.hookId));
}
export async function hookUpdate(
req: Request<any, HookType>,
res: Response<HookType>
) {
Tele.emit('evt', { evt_type: 'webhooks:updated' });
res.json(await Hook.update(req.params.hookId, req.body));
}
export async function hookTest(req: Request<any, any>, 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;

145
packages/nocodb/src/lib/meta/api/hookFilterApis.ts

@ -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<any, any, any, TableListParams>,
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<any, any, any, TableListParams>,
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<any, any, TableReq>,
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;

194
packages/nocodb/src/lib/meta/api/index.ts

@ -1,114 +1,114 @@
import { Tele } from 'nc-help'; import { T } from 'nc-help';
import orgLicenseApis from './orgLicenseApis'; import orgLicenseController from '../../controllers/orgLicenseController';
import orgTokenApis from './orgTokenApis'; import orgTokenController from '../../controllers/orgTokenController';
import orgUserApis from './orgUserApis'; import orgUserController from '../../controllers/orgUserController';
import projectApis from './projectApis'; import projectController from '../../controllers/projectController';
import baseApis from './baseApis'; import baseController from '../../controllers/baseController';
import tableApis from './tableApis'; import tableController from '../../controllers/tableController';
import columnApis from './columnApis'; import columnController from '../../controllers/columnController';
import { Router } from 'express'; import { Router } from 'express';
import sortApis from './sortApis'; import sortController from '../../controllers/sortController';
import filterApis from './filterApis'; import filterController from '../../controllers/filterController';
import viewColumnApis from './viewColumnApis'; import viewColumnController from '../../controllers/viewColumnController';
import gridViewApis from './gridViewApis'; import gridViewController from '../../controllers/gridViewController';
import viewApis from './viewApis'; import viewController from '../../controllers/viewController';
import galleryViewApis from './galleryViewApis'; import galleryViewController from '../../controllers/galleryViewController';
import formViewApis from './formViewApis'; import formViewController from '../../controllers/formViewController';
import formViewColumnApis from './formViewColumnApis'; import formViewColumnController from '../../controllers/formViewColumnController';
import attachmentApis from './attachmentApis'; import attachmentController from '../../controllers/attachmentController';
import exportApis from './exportApis'; import exportController from '../../controllers/exportController';
import auditApis from './auditApis'; import auditController from '../../controllers/auditController';
import hookApis from './hookApis'; import hookController from '../../controllers/hookController';
import pluginApis from './pluginApis'; import pluginController from '../../controllers/pluginController';
import gridViewColumnApis from './gridViewColumnApis'; import gridViewColumnController from '../../controllers/gridViewColumnController';
import kanbanViewApis from './kanbanViewApis'; import kanbanViewController from '../../controllers/kanbanViewController';
import { userApis } from './userApi'; import { userController } from '../../controllers/userController';
// import extractProjectIdAndAuthenticate from './helpers/extractProjectIdAndAuthenticate'; // import extractProjectIdAndAuthenticate from './helpers/extractProjectIdAndAuthenticate';
import utilApis from './utilApis'; import utilController from '../../controllers/utilController';
import projectUserApis from './projectUserApis'; import projectUserController from '../../controllers/projectUserController';
import sharedBaseApis from './sharedBaseApis'; import sharedBaseController from '../../controllers/sharedBaseController';
import { initStrategies } from './userApi/initStrategies'; import { initStrategies } from '../../controllers/userController/initStrategies';
import modelVisibilityApis from './modelVisibilityApis'; import modelVisibilityController from '../../controllers/modelVisibilityController';
import metaDiffApis from './metaDiffApis'; import metaDiffController from '../../controllers/metaDiffController';
import cacheApis from './cacheApis'; import cacheController from '../../controllers/cacheController';
import apiTokenApis from './apiTokenApis'; import apiTokenController from '../../controllers/apiTokenController';
import hookFilterApis from './hookFilterApis'; import hookFilterController from '../../controllers/hookFilterController';
import testApis from './testApis'; import testController from '../../controllers/testController';
import { import {
bulkDataAliasApis, bulkDataAliasController,
dataAliasApis, dataAliasController,
dataAliasExportApis, dataAliasExportController,
dataAliasNestedApis, dataAliasNestedController,
dataApis, dataController,
oldDataApis, oldDataController,
} from './dataApis'; } from '../../controllers/dataControllers';
import { import {
publicDataApis, publicDataController,
publicDataExportApis, publicDataExportController,
publicMetaApis, publicMetaController,
} from './publicApis'; } from '../../controllers/publicControllers';
import { Server, Socket } from 'socket.io'; import { Server, Socket } from 'socket.io';
import passport from 'passport'; import passport from 'passport';
import crypto from 'crypto'; import crypto from 'crypto';
import swaggerApis from './swagger/swaggerApis'; import swaggerController from '../../controllers/swaggerController';
import importApis from './sync/importApis'; import importController from '../../controllers/syncController/importApis';
import syncSourceApis from './sync/syncSourceApis'; import syncSourceController from '../../controllers/syncController';
import mapViewApis from './mapViewApis'; import mapViewController from '../../controllers/mapViewController';
const clients: { [id: string]: Socket } = {}; const clients: { [id: string]: Socket } = {};
const jobs: { [id: string]: { last_message: any } } = {}; const jobs: { [id: string]: { last_message: any } } = {};
export default function (router: Router, server) { export default function (router: Router, server) {
initStrategies(router); initStrategies(router);
projectApis(router); projectController(router);
baseApis(router); baseController(router);
utilApis(router); utilController(router);
if (process.env['PLAYWRIGHT_TEST'] === 'true') { if (process.env['PLAYWRIGHT_TEST'] === 'true') {
router.use(testApis); router.use(testController);
} }
router.use(columnApis); router.use(columnController);
router.use(exportApis); router.use(exportController);
router.use(dataApis); router.use(dataController);
router.use(bulkDataAliasApis); router.use(bulkDataAliasController);
router.use(dataAliasApis); router.use(dataAliasController);
router.use(dataAliasNestedApis); router.use(dataAliasNestedController);
router.use(dataAliasExportApis); router.use(dataAliasExportController);
router.use(oldDataApis); router.use(oldDataController);
router.use(sortApis); router.use(sortController);
router.use(filterApis); router.use(filterController);
router.use(viewColumnApis); router.use(viewColumnController);
router.use(gridViewApis); router.use(gridViewController);
router.use(formViewColumnApis); router.use(formViewColumnController);
router.use(publicDataApis); router.use(publicDataController);
router.use(publicDataExportApis); router.use(publicDataExportController);
router.use(publicMetaApis); router.use(publicMetaController);
router.use(gridViewColumnApis); router.use(gridViewColumnController);
router.use(tableApis); router.use(tableController);
router.use(galleryViewApis); router.use(galleryViewController);
router.use(formViewApis); router.use(formViewController);
router.use(viewApis); router.use(viewController);
router.use(attachmentApis); router.use(attachmentController);
router.use(auditApis); router.use(auditController);
router.use(hookApis); router.use(hookController);
router.use(pluginApis); router.use(pluginController);
router.use(projectUserApis); router.use(projectUserController);
router.use(orgUserApis); router.use(orgUserController);
router.use(orgTokenApis); router.use(orgTokenController);
router.use(orgLicenseApis); router.use(orgLicenseController);
router.use(sharedBaseApis); router.use(sharedBaseController);
router.use(modelVisibilityApis); router.use(modelVisibilityController);
router.use(metaDiffApis); router.use(metaDiffController);
router.use(cacheApis); router.use(cacheController);
router.use(apiTokenApis); router.use(apiTokenController);
router.use(hookFilterApis); router.use(hookFilterController);
router.use(swaggerApis); router.use(swaggerController);
router.use(syncSourceApis); router.use(syncSourceController);
router.use(kanbanViewApis); router.use(kanbanViewController);
router.use(mapViewApis); router.use(mapViewController);
userApis(router); userController(router);
const io = new Server(server, { const io = new Server(server, {
cors: { cors: {
@ -133,15 +133,15 @@ export default function (router: Router, server) {
}).on('connection', (socket) => { }).on('connection', (socket) => {
clients[socket.id] = socket; clients[socket.id] = socket;
const id = getHash( const id = getHash(
(process.env.NC_SERVER_UUID || Tele.id) + (process.env.NC_SERVER_UUID || T.id) +
(socket?.handshake as any)?.user?.id (socket?.handshake as any)?.user?.id
); );
socket.on('page', (args) => { socket.on('page', (args) => {
Tele.page({ ...args, id }); T.page({ ...args, id });
}); });
socket.on('event', (args) => { socket.on('event', (args) => {
Tele.event({ ...args, id }); T.event({ ...args, id });
}); });
socket.on('subscribe', (room) => { socket.on('subscribe', (room) => {
if (room in jobs) { 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) { function getHash(str) {

129
packages/nocodb/src/lib/meta/api/modelVisibilityApis.ts

@ -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;

83
packages/nocodb/src/lib/meta/api/orgTokenApis.ts

@ -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;

337
packages/nocodb/src/lib/meta/api/orgUserApis.ts

@ -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<any> {
NcError.notImplemented();
}
async function userInviteResend(req, res): Promise<any> {
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;

287
packages/nocodb/src/lib/meta/api/projectApis.ts

@ -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<any, any, any>,
res: Response<Project>
) {
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<any, any, any>,
res: Response<ProjectListType>
) {
const project = await Project.getWithInfo(req.params.projectId);
const data: Partial<Project> = 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<any> & { user: { id: string; roles: string } },
res: Response<ProjectListType>,
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<any>,
res: Response<ProjectListType>
) {
const result = await Project.softDelete(req.params.projectId);
Tele.emit('evt', { evt_type: 'project:deleted' });
res.json(result);
}
//
//
async function projectCreate(req: Request<any, any>, 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_<random_id>.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')
);
};

333
packages/nocodb/src/lib/meta/api/projectUserApis.ts

@ -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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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;

5
packages/nocodb/src/lib/meta/api/publicApis/index.ts

@ -1,5 +0,0 @@
import publicDataApis from './publicDataApis';
import publicDataExportApis from './publicDataExportApis';
import publicMetaApis from './publicMetaApis';
export { publicDataApis, publicDataExportApis, publicMetaApis };

472
packages/nocodb/src/lib/meta/api/publicApis/publicDataApis.ts

@ -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<LinkToAnotherRecordColumn>();
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;

112
packages/nocodb/src/lib/meta/api/sharedBaseApis.ts

@ -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<any> {
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<any> {
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<any> {
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<any> {
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;

81
packages/nocodb/src/lib/meta/api/sortApis.ts

@ -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<TableType>) {}
// @ts-ignore
export async function sortList(
req: Request<any, any, any>,
res: Response<SortListType>
) {
const sortList = await Sort.list({ viewId: req.params.viewId });
res.json({
sorts: new PagedResponseImpl(sortList),
});
}
// @ts-ignore
export async function sortCreate(req: Request<any, any, SortType>, 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;

61
packages/nocodb/src/lib/meta/api/swagger/swaggerApis.ts

@ -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;

447
packages/nocodb/src/lib/meta/api/tableApis.ts

@ -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<TableType>) {
const table = await Model.getWithInfo({
id: req.params.tableId,
});
// todo: optimise
const viewList = <View[]>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<TableListType>) {
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<any, any, TableReqType>, 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<Column, 'column_name' | 'title'> & {
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<any, any>, 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<LinkToAnotherRecordColumn>()
.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;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save