Browse Source

Merge pull request #6177 from nocodb/develop

pull/6178/head 0.109.7
github-actions[bot] 1 year ago committed by GitHub
parent
commit
5d409b7c27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/nc-gui/components/dashboard/settings/app-store/AppInstall.vue
  2. 4
      packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue
  3. 4
      packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue
  4. 9
      packages/nc-gui/components/dlg/AirtableImport.vue
  5. 2
      packages/nc-gui/components/general/HelpAndSupport.vue
  6. 2
      packages/nc-gui/components/smartsheet/Form.vue
  7. 6
      packages/nc-gui/components/smartsheet/Pagination.vue
  8. 2
      packages/nc-gui/components/smartsheet/column/FormulaOptions.vue
  9. 4
      packages/nc-gui/components/smartsheet/header/Icon.vue
  10. 2
      packages/nc-gui/components/smartsheet/sidebar/toolbar/DebugMeta.vue
  11. 2
      packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue
  12. 2
      packages/nc-gui/components/tabs/Smartsheet.vue
  13. 7
      packages/nc-gui/components/virtual-cell/QrCode.vue
  14. 1
      packages/nc-gui/components/virtual-cell/components/ItemChip.vue
  15. 1
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  16. 1
      packages/nc-gui/components/virtual-cell/components/ListItems.vue
  17. 4
      packages/nc-gui/composables/useMultiSelect/index.ts
  18. 1
      packages/nc-gui/lang/en.json
  19. 52
      packages/nc-gui/package-lock.json
  20. 2
      packages/nc-gui/package.json
  21. 2
      packages/nc-gui/pages/[projectType]/[projectId]/index.vue
  22. 4
      packages/nc-gui/pages/[projectType]/form/[viewId]/index.vue
  23. 2
      packages/nc-gui/pages/[projectType]/form/[viewId]/index/index.vue
  24. 3
      packages/nc-gui/plugins/jobs.ts
  25. 7
      packages/nc-gui/plugins/tele.ts
  26. 2
      packages/nc-gui/store/project.ts
  27. 4
      packages/nocodb-sdk/package-lock.json
  28. 30
      packages/nocodb/package-lock.json
  29. 4
      packages/nocodb/package.json
  30. 1
      packages/nocodb/src/app.module.ts
  31. 19
      packages/nocodb/src/controllers/data-table.controller.spec.ts
  32. 189
      packages/nocodb/src/controllers/data-table.controller.ts
  33. 2
      packages/nocodb/src/controllers/test/TestResetService/index.ts
  34. 648
      packages/nocodb/src/db/BaseModelSqlv2.ts
  35. 41
      packages/nocodb/src/db/sql-client/lib/mssql/MssqlClient.ts
  36. 2
      packages/nocodb/src/db/sql-client/lib/oracle/OracleClient.ts
  37. 2
      packages/nocodb/src/db/sql-client/lib/pg/PgClient.ts
  38. 73
      packages/nocodb/src/db/sql-client/lib/snowflake/SnowflakeClient.ts
  39. 1
      packages/nocodb/src/db/sql-client/lib/sqlite/SqliteClient.ts
  40. 44
      packages/nocodb/src/db/sql-data-mapper/lib/BaseModel.ts
  41. 2
      packages/nocodb/src/db/sql-mgr/v2/SqlMgrv2Trans.ts
  42. 2
      packages/nocodb/src/db/sql-migrator/lib/KnexMigrator.ts
  43. 1
      packages/nocodb/src/db/sql-migrator/lib/KnexMigratorv2.ts
  44. 4
      packages/nocodb/src/filters/global-exception/global-exception.filter.ts
  45. 8
      packages/nocodb/src/gateways/socket.gateway.ts
  46. 23
      packages/nocodb/src/helpers/PagedResponse.ts
  47. 2
      packages/nocodb/src/helpers/apiMetrics.ts
  48. 8
      packages/nocodb/src/helpers/catchError.ts
  49. 39
      packages/nocodb/src/helpers/extractLimitAndOffset.ts
  50. 1
      packages/nocodb/src/helpers/index.ts
  51. 1
      packages/nocodb/src/interface/XcMetaMgr.ts
  52. 2
      packages/nocodb/src/meta/migrations/v1/nc_011_remove_old_ses_plugin.ts
  53. 1
      packages/nocodb/src/models/Model.ts
  54. 1
      packages/nocodb/src/models/View.ts
  55. 4
      packages/nocodb/src/modules/datas/datas.module.ts
  56. 2
      packages/nocodb/src/modules/event-emitter/nestjs-event-emitter.ts
  57. 9
      packages/nocodb/src/modules/jobs/jobs.gateway.ts
  58. 2
      packages/nocodb/src/modules/jobs/jobs/at-import/at-import.controller.ts
  59. 89
      packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts
  60. 1
      packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts
  61. 4
      packages/nocodb/src/services/auth.service.ts
  62. 19
      packages/nocodb/src/services/data-table.service.spec.ts
  63. 434
      packages/nocodb/src/services/data-table.service.ts
  64. 2
      packages/nocodb/src/services/datas.service.ts
  65. 8
      packages/nocodb/src/services/tables.service.ts
  66. 1
      packages/nocodb/src/utils/common/XcAudit.ts
  67. 4
      packages/nocodb/src/version-upgrader/v1-legacy/BaseApiBuilder.ts
  68. 4
      packages/nocodb/src/version-upgrader/v1-legacy/NcProjectBuilder.ts
  69. 8
      packages/nocodb/src/version-upgrader/v1-legacy/gql/GqlApiBuilder.ts
  70. 8
      packages/nocodb/src/version-upgrader/v1-legacy/rest/RestApiBuilder.ts
  71. 163
      packages/nocodb/tests/unit/factory/column.ts
  72. 35
      packages/nocodb/tests/unit/factory/row.ts
  73. 84
      packages/nocodb/tests/unit/factory/view.ts
  74. 7
      packages/nocodb/tests/unit/init/index.ts
  75. 2
      packages/nocodb/tests/unit/rest/index.test.ts
  76. 2867
      packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts
  77. 13
      tests/playwright/tests/db/columns/columnQrCode.spec.ts

2
packages/nc-gui/components/dashboard/settings/app-store/AppInstall.vue

@ -113,7 +113,7 @@ const readPluginDetails = async () => {
// and it has been changed to XcType.Checkbox, since 0.0.2
// hence, change the text value to boolean here
if ('secure' in parsedInput && typeof parsedInput.secure === 'string') {
parsedInput.secure = !!parsedInput.secure
parsedInput.secure = parsedInput.secure === 'true'
}
plugin = { ...res, formDetails, parsedInput }

4
packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue

@ -1,12 +1,10 @@
<script lang="ts" setup>
import { Form, Modal, message } from 'ant-design-vue'
import type { SelectHandler } from 'ant-design-vue/es/vc-select/Select'
import type { ProjectCreateForm } from '#imports'
import type { DefaultConnection, ProjectCreateForm, SQLiteConnection } from '#imports'
import {
CertTypes,
ClientType,
DefaultConnection,
SQLiteConnection,
SSLUsage,
clientTypes as _clientTypes,
computed,

4
packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue

@ -2,12 +2,10 @@
import type { BaseType } from 'nocodb-sdk'
import { Form, Modal, message } from 'ant-design-vue'
import type { SelectHandler } from 'ant-design-vue/es/vc-select/Select'
import type { ProjectCreateForm } from '#imports'
import type { DefaultConnection, ProjectCreateForm, SQLiteConnection } from '#imports'
import {
CertTypes,
ClientType,
DefaultConnection,
SQLiteConnection,
SSLUsage,
clientTypes,
computed,

9
packages/nc-gui/components/dlg/AirtableImport.vue

@ -64,6 +64,7 @@ const syncSource = ref({
syncLookup: true,
syncFormula: false,
syncAttachment: true,
syncUsers: true,
},
},
})
@ -174,6 +175,7 @@ async function loadSyncSrc() {
syncLookup: true,
syncFormula: false,
syncAttachment: true,
syncUsers: true,
},
},
}
@ -334,6 +336,13 @@ onMounted(async () => {
</a-checkbox>
</div>
<!-- Import Users Columns -->
<div class="my-2">
<a-checkbox v-model:checked="syncSource.details.options.syncUsers">
{{ $t('labels.importUsers') }}
</a-checkbox>
</div>
<!-- Import Formula Columns -->
<a-tooltip placement="top">
<template #title>

2
packages/nc-gui/components/general/HelpAndSupport.vue

@ -10,7 +10,7 @@ const { project } = storeToRefs(useProject())
const route = useRoute()
const openSwaggerLink = () => {
openLink(`/api/v1/db/meta/projects/${route.params.projectId}/swagger`, appInfo.value.ncSiteUrl)
openLink(`./api/v1/db/meta/projects/${route.params.projectId}/swagger`, appInfo.value.ncSiteUrl)
}
</script>

2
packages/nc-gui/components/smartsheet/Form.vue

@ -730,7 +730,7 @@ watch(view, (nextView) => {
<LazySmartsheetDivDataCell class="relative">
<LazySmartsheetCell
v-model="formState[element.title]"
class="nc-input"
class="nc-input truncate"
:class="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:data-testid="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:column="element"

6
packages/nc-gui/components/smartsheet/Pagination.vue

@ -2,9 +2,11 @@
import { ChangePageInj, PaginationDataInj, computed, iconMap, inject, isRtlLang, useI18n } from '#imports'
import type { Language } from '~/lib'
const props = defineProps<{
interface Props {
alignCountOnRight?: boolean
}>()
}
const { alignCountOnRight } = defineProps<Props>()
const { locale } = useI18n()

2
packages/nc-gui/components/smartsheet/column/FormulaOptions.vue

@ -78,7 +78,7 @@ const formulaRef = ref()
const sugListRef = ref()
const sugOptionsRef = ref<typeof AntListItem[]>([])
const sugOptionsRef = ref<(typeof AntListItem)[]>([])
const wordToComplete = ref<string | undefined>('')

4
packages/nc-gui/components/smartsheet/header/Icon.vue

@ -1,8 +1,8 @@
<script lang="ts" setup>
import { ColumnType, isVirtualCol } from 'nocodb-sdk'
import type { ColumnType } from 'nocodb-sdk'
import { isVirtualCol } from 'nocodb-sdk'
const { column } = defineProps<{ column: ColumnType }>()
</script>
<template>

2
packages/nc-gui/components/smartsheet/sidebar/toolbar/DebugMeta.vue

@ -10,7 +10,7 @@ const { metas } = $(useMetas())
const { tables } = useTable()
const localTables = computed(
() => tables.value.filter((t) => metas[t.id as string]) as (typeof tables.value[number] & { id: string })[],
() => tables.value.filter((t) => metas[t.id as string]) as ((typeof tables.value)[number] & { id: string })[],
)
</script>

2
packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue

@ -49,7 +49,7 @@ type QuickImportDialogType = 'csv' | 'excel' | 'json'
// TODO: add 'json' when it's ready
const quickImportDialogTypes: QuickImportDialogType[] = ['csv', 'excel']
const quickImportDialogs: Record<typeof quickImportDialogTypes[number], Ref<boolean>> = quickImportDialogTypes.reduce(
const quickImportDialogs: Record<(typeof quickImportDialogTypes)[number], Ref<boolean>> = quickImportDialogTypes.reduce(
(acc: any, curr) => {
acc[curr] = ref(false)
return acc

2
packages/nc-gui/components/tabs/Smartsheet.vue

@ -72,7 +72,7 @@ const onDrop = async (event: DragEvent) => {
event.preventDefault()
try {
// Access the dropped data
const data = JSON.parse(event.dataTransfer?.getData('text/json')!)
const data = JSON.parse(event.dataTransfer!.getData('text/json'))
// Do something with the received data
// if dragged item is not from the same base, return

7
packages/nc-gui/components/virtual-cell/QrCode.vue

@ -16,7 +16,6 @@ const showQrCode = computed(() => qrValue?.value?.length > 0 && !tooManyCharsFor
const qrCodeOptions: QRCode.QRCodeToDataURLOptions = {
errorCorrectionLevel: 'M',
margin: 1,
version: 4,
rendererOpts: {
quality: 1,
},
@ -55,11 +54,13 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning } = us
@ok="handleModalOkClick"
>
<template #footer>
<div class="mr-4" data-testid="nc-qr-code-large-value-label">{{ qrValue }}</div>
<div class="mr-4 overflow-scroll p-2" data-testid="nc-qr-code-large-value-label">
{{ qrValue }}
</div>
</template>
<img v-if="showQrCode" :src="qrCodeLarge" alt="QR Code" />
</a-modal>
<div v-if="tooManyCharsForQrCode" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
<div v-if="tooManyCharsForQrCode" class="text-left text-wrap mt-2 text-[#e65100] text-[10px]">
{{ $t('labels.qrCodeValueTooLong') }}
</div>
<img

1
packages/nc-gui/components/virtual-cell/components/ItemChip.vue

@ -9,7 +9,6 @@ import {
inject,
isAttachment,
ref,
renderValue,
useExpandedFormDetached,
useLTARStoreOrThrow,
} from '#imports'

1
packages/nc-gui/components/virtual-cell/components/ListChildItems.vue

@ -13,7 +13,6 @@ import {
iconMap,
inject,
ref,
renderValue,
useLTARStoreOrThrow,
useSmartsheetRowStoreOrThrow,
useVModel,

1
packages/nc-gui/components/virtual-cell/components/ListItems.vue

@ -12,7 +12,6 @@ import {
inject,
isDrawerExist,
ref,
renderValue,
useLTARStoreOrThrow,
useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow,

4
packages/nc-gui/composables/useMultiSelect/index.ts

@ -26,7 +26,6 @@ import {
useI18n,
useMetas,
useProject,
useUIPermission,
} from '#imports'
const MAIN_MOUSE_PRESSED = 0
@ -80,9 +79,6 @@ export function useMultiSelect(
() => !(activeCell.row === null || activeCell.col === null || isNaN(activeCell.row) || isNaN(activeCell.col)),
)
const { isUIAllowed } = useUIPermission()
const hasEditPermission = $computed(() => isUIAllowed('xcDatatableEditable'))
function makeActive(row: number, col: number) {
if (activeCell.row === row && activeCell.col === col) {
return

1
packages/nc-gui/lang/en.json

@ -312,6 +312,7 @@
"importLookupColumns": "Import Lookup Columns",
"importAttachmentColumns": "Import Attachment Columns",
"importFormulaColumns": "Import Formula Columns",
"importUsers": "Import Users (by email)",
"noData": "No Data",
"goToDashboard": "Go to Dashboard",
"importing": "Importing",

52
packages/nc-gui/package-lock.json generated

@ -30,7 +30,7 @@
"leaflet.markercluster": "^1.5.3",
"locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0",
"nocodb-sdk": "0.109.6",
"nocodb-sdk": "file:../nocodb-sdk",
"papaparse": "^5.3.2",
"pinia": "^2.0.33",
"qrcode": "^1.5.1",
@ -111,7 +111,6 @@
},
"../nocodb-sdk": {
"version": "0.109.6",
"extraneous": true,
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@ -8720,6 +8719,7 @@
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
"devOptional": true,
"funding": [
{
"type": "individual",
@ -12238,21 +12238,8 @@
}
},
"node_modules/nocodb-sdk": {
"version": "0.109.6",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.109.6.tgz",
"integrity": "sha512-Zh4MjkurCPYl/eJWt9ChvoikMpDujI/F6IaQYb9bJBss6Ns4grdhKizCdvywb2dRYaGqXquzwrGg2jmpw/Xyxg==",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
}
},
"node_modules/nocodb-sdk/node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"dependencies": {
"follow-redirects": "^1.14.0"
}
"resolved": "../nocodb-sdk",
"link": true
},
"node_modules/node-abi": {
"version": "3.23.0",
@ -24729,7 +24716,8 @@
"follow-redirects": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
"devOptional": true
},
"form-data": {
"version": "4.0.0",
@ -27279,22 +27267,22 @@
}
},
"nocodb-sdk": {
"version": "0.109.6",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.109.6.tgz",
"integrity": "sha512-Zh4MjkurCPYl/eJWt9ChvoikMpDujI/F6IaQYb9bJBss6Ns4grdhKizCdvywb2dRYaGqXquzwrGg2jmpw/Xyxg==",
"version": "file:../nocodb-sdk",
"requires": {
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"axios": "^0.21.1",
"jsep": "^1.3.6"
},
"dependencies": {
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"requires": {
"follow-redirects": "^1.14.0"
}
}
"cspell": "^4.1.0",
"eslint": "^7.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^4.0.0",
"jsep": "^1.3.6",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.1",
"typescript": "^4.0.2"
}
},
"node-abi": {

2
packages/nc-gui/package.json

@ -54,7 +54,7 @@
"leaflet.markercluster": "^1.5.3",
"locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0",
"nocodb-sdk": "0.109.6",
"nocodb-sdk": "file:../nocodb-sdk",
"papaparse": "^5.3.2",
"pinia": "^2.0.33",
"qrcode": "^1.5.1",

2
packages/nc-gui/pages/[projectType]/[projectId]/index.vue

@ -365,7 +365,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
v-if="isUIAllowed('apiDocs') && !isMobileMode"
v-e="['e:api-docs']"
class="nc-project-menu-item group"
@click.stop="openLink(`/api/v1/db/meta/projects/${route.params.projectId}/swagger`, appInfo.ncSiteUrl)"
@click.stop="openLink(`./api/v1/db/meta/projects/${route.params.projectId}/swagger`, appInfo.ncSiteUrl)"
>
<component :is="iconMap.json" class="group-hover:text-accent" />
{{ $t('activity.account.swagger') }}

4
packages/nc-gui/pages/[projectType]/form/[viewId]/index.vue

@ -120,10 +120,6 @@ p {
}
}
&.nc-cell-longtext {
@apply !p-0 pb-2px pr-2px;
}
textarea {
@apply px-4 py-2 rounded;

2
packages/nc-gui/pages/[projectType]/form/[viewId]/index/index.vue

@ -165,7 +165,7 @@ const onDecode = async (scannedCodeValue: string) => {
<LazySmartsheetCell
v-else
v-model="formState[field.title]"
class="nc-input"
class="nc-input truncate"
:data-testid="`nc-form-input-cell-${field.label || field.title}`"
:class="`nc-form-input-${field.title?.replaceAll(' ', '')}`"
:column="field"

3
packages/nc-gui/plugins/jobs.ts

@ -13,9 +13,12 @@ export default defineNuxtPlugin(async (nuxtApp) => {
if (socket) socket.disconnect()
const url = new URL(appInfo.ncSiteUrl, window.location.href.split(/[?#]/)[0])
let socketPath = url.pathname
socketPath += socketPath.endsWith('/') ? 'socket.io' : '/socket.io'
socket = io(`${url.href}jobs`, {
extraHeaders: { 'xc-auth': token },
path: socketPath,
})
socket.on('connect_error', (e) => {

7
packages/nc-gui/plugins/tele.ts

@ -16,10 +16,13 @@ export default defineNuxtPlugin(async (nuxtApp) => {
try {
if (socket) socket.disconnect()
const url = new URL(appInfo.ncSiteUrl, window.location.href.split(/[?#]/)[0]).href
const url = new URL(appInfo.ncSiteUrl, window.location.href.split(/[?#]/)[0])
let socketPath = url.pathname
socketPath += socketPath.endsWith('/') ? 'socket.io' : '/socket.io'
socket = io(url, {
socket = io(url.href, {
extraHeaders: { 'xc-auth': token },
path: socketPath,
})
socket.on('connect_error', () => {

2
packages/nc-gui/store/project.ts

@ -66,7 +66,7 @@ export const useProject = defineStore('projectStore', () => {
for (const base of bases.value) {
if (base.id) {
temp[base.id] = SqlUiFactory.create({ client: base.type }) as Exclude<
ReturnType<typeof SqlUiFactory['create']>,
ReturnType<(typeof SqlUiFactory)['create']>,
typeof OracleUi
>
}

4
packages/nocodb-sdk/package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "nocodb-sdk",
"version": "0.109.5",
"version": "0.109.6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb-sdk",
"version": "0.109.5",
"version": "0.109.6",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",

30
packages/nocodb/package-lock.json generated

@ -83,7 +83,7 @@
"nc-lib-gui": "0.109.6",
"nc-plugin": "^0.1.3",
"ncp": "^2.0.0",
"nocodb-sdk": "0.109.6",
"nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"object-sizeof": "^2.6.1",
@ -192,7 +192,6 @@
},
"../nocodb-sdk": {
"version": "0.109.6",
"extraneous": true,
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@ -13208,13 +13207,8 @@
}
},
"node_modules/nocodb-sdk": {
"version": "0.109.6",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.109.6.tgz",
"integrity": "sha512-Zh4MjkurCPYl/eJWt9ChvoikMpDujI/F6IaQYb9bJBss6Ns4grdhKizCdvywb2dRYaGqXquzwrGg2jmpw/Xyxg==",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
}
"resolved": "../nocodb-sdk",
"link": true
},
"node_modules/node-abort-controller": {
"version": "3.1.1",
@ -28517,12 +28511,22 @@
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="
},
"nocodb-sdk": {
"version": "0.109.6",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.109.6.tgz",
"integrity": "sha512-Zh4MjkurCPYl/eJWt9ChvoikMpDujI/F6IaQYb9bJBss6Ns4grdhKizCdvywb2dRYaGqXquzwrGg2jmpw/Xyxg==",
"version": "file:../nocodb-sdk",
"requires": {
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"axios": "^0.21.1",
"jsep": "^1.3.6"
"cspell": "^4.1.0",
"eslint": "^7.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^4.0.0",
"jsep": "^1.3.6",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.1",
"typescript": "^4.0.2"
}
},
"node-abort-controller": {

4
packages/nocodb/package.json

@ -116,7 +116,7 @@
"nc-lib-gui": "0.109.6",
"nc-plugin": "^0.1.3",
"ncp": "^2.0.0",
"nocodb-sdk": "0.109.6",
"nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"object-sizeof": "^2.6.1",
@ -204,4 +204,4 @@
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
}

1
packages/nocodb/src/app.module.ts

@ -29,7 +29,6 @@ import type { MiddlewareConsumer } from '@nestjs/common';
JobsModule,
NestJsEventEmitter.forRoot(),
],
controllers: [],
providers: [
AuthService,
{

19
packages/nocodb/src/controllers/data-table.controller.spec.ts

@ -0,0 +1,19 @@
import { Test } from '@nestjs/testing';
import { DataTableController } from './data-table.controller';
import type { TestingModule } from '@nestjs/testing';
describe('DataTableController', () => {
let controller: DataTableController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [DataTableController],
}).compile();
controller = module.get<DataTableController>(DataTableController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

189
packages/nocodb/src/controllers/data-table.controller.ts

@ -0,0 +1,189 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Patch,
Post,
Query,
Request,
Response,
UseGuards,
} from '@nestjs/common';
import { GlobalGuard } from '../guards/global/global.guard';
import { parseHrtimeToSeconds } from '../helpers';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { DataTableService } from '../services/data-table.service';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class DataTableController {
constructor(private readonly dataTableService: DataTableService) {}
// todo: Handle the error case where view doesnt belong to model
@Get('/api/v1/tables/:modelId/rows')
@Acl('dataList')
async dataList(
@Request() req,
@Response() res,
@Param('modelId') modelId: string,
@Query('viewId') viewId: string,
) {
const startTime = process.hrtime();
const responseData = await this.dataTableService.dataList({
query: req.query,
modelId: modelId,
viewId: viewId,
});
const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime));
res.setHeader('xc-db-response', elapsedSeconds);
res.json(responseData);
}
@Get(['/api/v1/tables/:modelId/rows/count'])
@Acl('dataCount')
async dataCount(
@Request() req,
@Response() res,
@Param('modelId') modelId: string,
@Query('viewId') viewId: string,
) {
const countResult = await this.dataTableService.dataCount({
query: req.query,
modelId,
viewId,
});
res.json(countResult);
}
@Post(['/api/v1/tables/:modelId/rows'])
@HttpCode(200)
@Acl('dataInsert')
async dataInsert(
@Request() req,
@Param('modelId') modelId: string,
@Query('viewId') viewId: string,
@Body() body: any,
) {
return await this.dataTableService.dataInsert({
modelId: modelId,
body: body,
viewId,
cookie: req,
});
}
@Patch(['/api/v1/tables/:modelId/rows'])
@Acl('dataUpdate')
async dataUpdate(
@Request() req,
@Param('modelId') modelId: string,
@Query('viewId') viewId: string,
@Param('rowId') rowId: string,
) {
return await this.dataTableService.dataUpdate({
modelId: modelId,
body: req.body,
cookie: req,
viewId,
});
}
@Delete(['/api/v1/tables/:modelId/rows'])
@Acl('dataDelete')
async dataDelete(
@Request() req,
@Param('modelId') modelId: string,
@Query('viewId') viewId: string,
@Param('rowId') rowId: string,
) {
return await this.dataTableService.dataDelete({
modelId: modelId,
cookie: req,
viewId,
body: req.body,
});
}
@Get(['/api/v1/tables/:modelId/rows/:rowId'])
@Acl('dataRead')
async dataRead(
@Request() req,
@Param('modelId') modelId: string,
@Query('viewId') viewId: string,
@Param('rowId') rowId: string,
) {
return await this.dataTableService.dataRead({
modelId,
rowId: rowId,
query: req.query,
viewId,
});
}
@Get(['/api/v1/tables/:modelId/links/:columnId/rows/:rowId'])
@Acl('nestedDataList')
async nestedDataList(
@Request() req,
@Param('modelId') modelId: string,
@Query('viewId') viewId: string,
@Param('columnId') columnId: string,
@Param('rowId') rowId: string,
) {
return await this.dataTableService.nestedDataList({
modelId,
rowId: rowId,
query: req.query,
viewId,
columnId,
});
}
@Post(['/api/v1/tables/:modelId/links/:columnId/rows/:rowId'])
@Acl('nestedDataLink')
async nestedLink(
@Request() req,
@Param('modelId') modelId: string,
@Query('viewId') viewId: string,
@Param('columnId') columnId: string,
@Param('rowId') rowId: string,
@Body() refRowIds: string | string[] | number | number[],
) {
return await this.dataTableService.nestedLink({
modelId,
rowId: rowId,
query: req.query,
viewId,
columnId,
refRowIds,
cookie: req,
});
}
@Delete(['/api/v1/tables/:modelId/links/:columnId/rows/:rowId'])
@Acl('nestedDataUnlink')
async nestedUnlink(
@Request() req,
@Param('modelId') modelId: string,
@Query('viewId') viewId: string,
@Param('columnId') columnId: string,
@Param('rowId') rowId: string,
@Body() refRowIds: string | string[] | number | number[],
) {
return await this.dataTableService.nestedUnlink({
modelId,
rowId: rowId,
query: req.query,
viewId,
columnId,
refRowIds,
cookie: req,
});
}
}

2
packages/nocodb/src/controllers/test/TestResetService/index.ts

@ -1,6 +1,6 @@
import axios from 'axios';
import Project from '../../../models/Project';
import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2';
// import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2';
import Noco from '../../../Noco';
import User from '../../../models/User';
import NocoCache from '../../../cache/NocoCache';

648
packages/nocodb/src/db/BaseModelSqlv2.ts

@ -18,6 +18,7 @@ import { customAlphabet } from 'nanoid';
import DOMPurify from 'isomorphic-dompurify';
import { v4 as uuidv4 } from 'uuid';
import { Knex } from 'knex';
import { extractLimitAndOffset } from '../helpers';
import { NcError } from '../helpers/catchError';
import getAst from '../helpers/getAst';
import {
@ -43,6 +44,7 @@ import genRollupSelectv2 from './genRollupSelectv2';
import conditionV2 from './conditionV2';
import sortV2 from './sortV2';
import { customValidators } from './util/customValidators';
import Transaction = Knex.Transaction;
import type { XKnex } from './CustomKnex';
import type {
XcFilter,
@ -58,7 +60,6 @@ import type {
SelectOption,
} from '../models';
import type { SortType } from 'nocodb-sdk';
import Transaction = Knex.Transaction;
dayjs.extend(utc);
dayjs.extend(timezone);
@ -1511,20 +1512,12 @@ class BaseModelSqlv2 {
}
_getListArgs(args: XcFilterWithAlias): XcFilter {
const obj: XcFilter = {};
obj.where = args.where || args.w || '';
const obj: XcFilter = extractLimitAndOffset(args);
obj.where = args.filter || args.where || args.w || '';
obj.having = args.having || args.h || '';
obj.shuffle = args.shuffle || args.r || '';
obj.condition = args.condition || args.c || {};
obj.conditionGraph = args.conditionGraph || {};
obj.limit = Math.max(
Math.min(
args.limit || args.l || this.config.limitDefault,
this.config.limitMax,
),
this.config.limitMin,
);
obj.offset = Math.max(+(args.offset || args.o) || 0, 0);
obj.fields = args.fields || args.f;
obj.sort = args.sort || args.s;
return obj;
@ -2244,12 +2237,14 @@ class BaseModelSqlv2 {
foreign_key_checks = true,
skip_hooks = false,
raw = false,
insertOneByOneAsFallback = false,
}: {
chunkSize?: number;
cookie?: any;
foreign_key_checks?: boolean;
skip_hooks?: boolean;
raw?: boolean;
insertOneByOneAsFallback?: boolean;
} = {},
) {
let trx;
@ -2403,12 +2398,28 @@ class BaseModelSqlv2 {
}
}
const response =
this.isPg || this.isMssql
? await trx
.batchInsert(this.tnPath, insertDatas, chunkSize)
.returning(this.model.primaryKey?.column_name)
: await trx.batchInsert(this.tnPath, insertDatas, chunkSize);
let response;
// insert one by one as fallback to get ids for sqlite and mysql
if (insertOneByOneAsFallback && (this.isSqlite || this.isMySQL)) {
// sqlite and mysql doesnt support returning, so insert one by one and return ids
response = [];
const aiPkCol = this.model.primaryKeys.find((pk) => pk.ai);
for (const insertData of insertDatas) {
const query = trx(this.tnPath).insert(insertData);
const id = (await query)[0];
response.push(aiPkCol ? { [aiPkCol.title]: id } : id);
}
} else {
response =
this.isPg || this.isMssql
? await trx
.batchInsert(this.tnPath, insertDatas, chunkSize)
.returning(this.model.primaryKey?.column_name)
: await trx.batchInsert(this.tnPath, insertDatas, chunkSize);
}
if (!foreign_key_checks) {
if (this.isPg) {
@ -2433,7 +2444,11 @@ class BaseModelSqlv2 {
async bulkUpdate(
datas: any[],
{ cookie, raw = false }: { cookie?: any; raw?: boolean } = {},
{
cookie,
raw = false,
throwExceptionIfNotExist = false,
}: { cookie?: any; raw?: boolean; throwExceptionIfNotExist?: boolean } = {},
) {
let transaction;
try {
@ -2476,9 +2491,12 @@ class BaseModelSqlv2 {
if (!raw) {
for (const pkValues of updatePkValues) {
newData.push(
await this.readByPk(pkValues, false, {}, { ignoreView: true }),
);
const oldRecord = await this.readByPk(pkValues);
if (!oldRecord && throwExceptionIfNotExist)
NcError.unprocessableEntity(
`Record with pk ${JSON.stringify(pkValues)} not found`,
);
newData.push(oldRecord);
}
}
@ -2553,7 +2571,13 @@ class BaseModelSqlv2 {
}
}
async bulkDelete(ids: any[], { cookie }: { cookie?: any } = {}) {
async bulkDelete(
ids: any[],
{
cookie,
throwExceptionIfNotExist = false,
}: { cookie?: any; throwExceptionIfNotExist?: boolean } = {},
) {
let transaction;
try {
const deleteIds = await Promise.all(
@ -2570,9 +2594,14 @@ class BaseModelSqlv2 {
// pk not specified - bypass
continue;
}
deleted.push(
await this.readByPk(pkValues, false, {}, { ignoreView: true }),
);
const oldRecord = await this.readByPk(pkValues);
if (!oldRecord && throwExceptionIfNotExist)
NcError.unprocessableEntity(
`Record with pk ${JSON.stringify(pkValues)} not found`,
);
deleted.push(oldRecord);
res.push(d);
}
@ -2635,12 +2664,6 @@ class BaseModelSqlv2 {
transaction = await this.dbDriver.transaction();
if (base.is_meta && execQueries.length > 0) {
for (const execQuery of execQueries) {
await execQuery(transaction, idsVals);
}
}
for (const d of res) {
await transaction(this.tnPath).del().where(d);
}
@ -2685,8 +2708,8 @@ class BaseModelSqlv2 {
qb,
this.dbDriver,
);
const execQueries: ((trx: Transaction, qb: any) => Promise<any>)[] = [];
// qb.del();
for (const column of this.model.columns) {
if (column.uidt !== UITypes.LinkToAnotherRecord) continue;
@ -2706,18 +2729,16 @@ class BaseModelSqlv2 {
await parentTable.getColumns();
const childTn = this.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable);
switch (colOptions.type) {
case 'mm':
{
const vChildCol = await colOptions.getMMChildColumn();
const vParentCol = await colOptions.getMMParentColumn();
const vTable = await colOptions.getMMModel();
const vTn = this.getTnPath(vTable);
execQueries.push((trx, qb) =>
execQueries.push(() =>
this.dbDriver(vTn)
.where({
[vChildCol.column_name]: this.dbDriver(childTn)
@ -2781,7 +2802,6 @@ class BaseModelSqlv2 {
return count;
} catch (e) {
if (trx) await trx.rollback();
throw e;
}
}
@ -2977,104 +2997,12 @@ class BaseModelSqlv2 {
modelId: this.model.id,
tnPath: this.tnPath,
});
/*
const view = await View.get(this.viewId);
// handle form view data submission
if (
(hookName === 'after.insert' || hookName === 'after.bulkInsert') &&
view.type === ViewTypes.FORM
) {
try {
const formView = await view.getView<FormView>();
const { columns } = await FormView.getWithInfo(formView.fk_view_id);
const allColumns = await this.model.getColumns();
const fieldById = columns.reduce(
(o: Record<string, any>, f: Record<string, any>) => ({
...o,
[f.fk_column_id]: f,
}),
{},
);
let order = 1;
const filteredColumns = allColumns
?.map((c: Record<string, any>) => ({
...c,
fk_column_id: c.id,
fk_view_id: formView.fk_view_id,
...(fieldById[c.id] ? fieldById[c.id] : {}),
order: (fieldById[c.id] && fieldById[c.id].order) || order++,
id: fieldById[c.id] && fieldById[c.id].id,
}))
.sort(
(a: Record<string, any>, b: Record<string, any>) =>
a.order - b.order,
)
.filter(
(f: Record<string, any>) =>
f.show &&
f.uidt !== UITypes.Rollup &&
f.uidt !== UITypes.Lookup &&
f.uidt !== UITypes.Formula &&
f.uidt !== UITypes.QrCode &&
f.uidt !== UITypes.Barcode &&
f.uidt !== UITypes.SpecificDBType,
)
.sort(
(a: Record<string, any>, b: Record<string, any>) =>
a.order - b.order,
)
.map((c: Record<string, any>) => ({
...c,
required: !!(c.required || 0),
}));
const emails = Object.entries(JSON.parse(formView?.email) || {})
.filter((a) => a[1])
.map((a) => a[0]);
if (emails?.length) {
const transformedData = _transformSubmittedFormDataForEmail(
newData,
formView,
filteredColumns,
);
(await NcPluginMgrv2.emailAdapter(false))?.mailSend({
to: emails.join(','),
subject: 'NocoDB Form',
html: ejs.render(formSubmissionEmailTemplate, {
data: transformedData,
tn: this.tnPath,
_tn: this.model.title,
}),
});
}
} catch (e) {
console.log(e);
}
}
try {
const [event, operation] = hookName.split('.');
const hooks = await Hook.list({
fk_model_id: this.model.id,
event,
operation,
});
for (const hook of hooks) {
if (hook.active) {
invokeWebhook(hook, this.model, view, prevData, newData, req?.user);
}
}
} catch (e) {
console.log('hooks :: error', hookName, e);
}*/
}
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async errorInsert(e, data, trx, cookie) {}
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async errorUpdate(e, data, trx, cookie) {}
// todo: handle composite primary key
@ -3088,7 +3016,7 @@ class BaseModelSqlv2 {
);
}
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async errorDelete(e, id, trx, cookie) {}
async validate(columns) {
@ -3809,6 +3737,464 @@ class BaseModelSqlv2 {
}
return data;
}
async addLinks({
cookie,
childIds,
colId,
rowId,
}: {
cookie: any;
childIds: (string | number)[];
colId: string;
rowId: string;
}) {
const columns = await this.model.getColumns();
const column = columns.find((c) => c.id === colId);
if (!column || column.uidt !== UITypes.LinkToAnotherRecord)
NcError.notFound(`Link column ${colId} not found`);
const row = await this.dbDriver(this.tnPath)
.where(await this._wherePk(rowId))
.first();
// validate rowId
if (!row) {
NcError.notFound(`Row with id '${rowId}' not found`);
}
if (!childIds.length) return;
const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>();
const childColumn = await colOptions.getChildColumn();
const parentColumn = await colOptions.getParentColumn();
const parentTable = await parentColumn.getModel();
const childTable = await childColumn.getModel();
await childTable.getColumns();
await parentTable.getColumns();
const childTn = this.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable);
switch (colOptions.type) {
case RelationTypes.MANY_TO_MANY:
{
const vChildCol = await colOptions.getMMChildColumn();
const vParentCol = await colOptions.getMMParentColumn();
const vTable = await colOptions.getMMModel();
const vTn = this.getTnPath(vTable);
let insertData: Record<string, any>[];
// validate Ids
{
const childRowsQb = this.dbDriver(parentTn)
.select(parentColumn.column_name)
.select(`${vTable.table_name}.${vChildCol.column_name}`)
.leftJoin(vTn, (qb) => {
qb.on(
`${vTable.table_name}.${vParentCol.column_name}`,
`${parentTable.table_name}.${parentColumn.column_name}`,
).andOn(
`${vTable.table_name}.${vChildCol.column_name}`,
row[childColumn.column_name],
);
});
// .where(_wherePk(parentTable.primaryKeys, childId))
if (parentTable.primaryKeys.length > 1) {
childRowsQb.where((qb) => {
for (const childId of childIds) {
qb.orWhere(_wherePk(parentTable.primaryKeys, childId));
}
});
} else {
childRowsQb.whereIn(parentTable.primaryKey.column_name, childIds);
}
if (parentTable.primaryKey.column_name !== parentColumn.column_name)
childRowsQb.select(parentTable.primaryKey.column_name);
const childRows = await childRowsQb;
if (childRows.length !== childIds.length) {
const missingIds = childIds.filter(
(id) =>
!childRows.find((r) => r[parentColumn.column_name] === id),
);
NcError.unprocessableEntity(
`Child record with id [${missingIds.join(', ')}] not found`,
);
}
insertData = childRows
// skip existing links
.filter((childRow) => !childRow[vChildCol.column_name])
// generate insert data for new links
.map((childRow) => ({
[vParentCol.column_name]: childRow[parentColumn.column_name],
[vChildCol.column_name]: row[childColumn.column_name],
}));
// if no new links, return true
if (!insertData.length) return true;
}
// if (this.isSnowflake) {
// const parentPK = this.dbDriver(parentTn)
// .select(parentColumn.column_name)
// // .where(_wherePk(parentTable.primaryKeys, childId))
// .whereIn(parentTable.primaryKey.column_name, childIds)
// .first();
//
// const childPK = this.dbDriver(childTn)
// .select(childColumn.column_name)
// .where(_wherePk(childTable.primaryKeys, rowId))
// .first();
//
// await this.dbDriver.raw(
// `INSERT INTO ?? (??, ??) SELECT (${parentPK.toQuery()}), (${childPK.toQuery()})`,
// [vTn, vParentCol.column_name, vChildCol.column_name],
// );
// } else {
// await this.dbDriver(vTn).insert({
// [vParentCol.column_name]: this.dbDriver(parentTn)
// .select(parentColumn.column_name)
// // .where(_wherePk(parentTable.primaryKeys, childId))
// .where(parentTable.primaryKey.column_name, childIds)
// .first(),
// [vChildCol.column_name]: this.dbDriver(childTn)
// .select(childColumn.column_name)
// .where(_wherePk(childTable.primaryKeys, rowId))
// .first(),
// });
// todo: use bulk insert
await this.dbDriver(vTn).insert(insertData);
// }
}
break;
case RelationTypes.HAS_MANY:
{
// validate Ids
{
const childRowsQb = this.dbDriver(childTn).select(
childTable.primaryKey.column_name,
);
if (childTable.primaryKeys.length > 1) {
childRowsQb.where((qb) => {
for (const childId of childIds) {
qb.orWhere(_wherePk(childTable.primaryKeys, childId));
}
});
} else {
childRowsQb.whereIn(parentTable.primaryKey.column_name, childIds);
}
const childRows = await childRowsQb;
if (childRows.length !== childIds.length) {
const missingIds = childIds.filter(
(id) =>
!childRows.find((r) => r[parentColumn.column_name] === id),
);
NcError.unprocessableEntity(
`Child record with id [${missingIds.join(', ')}] not found`,
);
}
}
await this.dbDriver(childTn)
.update({
[childColumn.column_name]: this.dbDriver.from(
this.dbDriver(parentTn)
.select(parentColumn.column_name)
.where(_wherePk(parentTable.primaryKeys, rowId))
.first()
.as('___cn_alias'),
),
})
// .where(_wherePk(childTable.primaryKeys, childId));
.whereIn(childTable.primaryKey.column_name, childIds);
}
break;
case RelationTypes.BELONGS_TO:
{
// validate Ids
{
const childRowsQb = this.dbDriver(parentTn)
.select(parentTable.primaryKey.column_name)
.where(_wherePk(parentTable.primaryKeys, childIds[0]))
.first();
const childRow = await childRowsQb;
if (!childRow) {
NcError.unprocessableEntity(
`Child record with id [${childIds[0]}] not found`,
);
}
}
await this.dbDriver(childTn)
.update({
[childColumn.column_name]: this.dbDriver.from(
this.dbDriver(parentTn)
.select(parentColumn.column_name)
.where(_wherePk(parentTable.primaryKeys, childIds[0]))
// .whereIn(parentTable.primaryKey.column_name, childIds)
.first()
.as('___cn_alias'),
),
})
.where(_wherePk(childTable.primaryKeys, rowId));
}
break;
}
// const response = await this.readByPk(rowId);
// await this.afterInsert(response, this.dbDriver, cookie);
// await this.afterAddChild(rowId, childId, cookie);
}
async removeLinks({
cookie,
childIds,
colId,
rowId,
}: {
cookie: any;
childIds: (string | number)[];
colId: string;
rowId: string;
}) {
const columns = await this.model.getColumns();
const column = columns.find((c) => c.id === colId);
if (!column || column.uidt !== UITypes.LinkToAnotherRecord)
NcError.notFound(`Link column ${colId} not found`);
const row = await this.dbDriver(this.tnPath)
.where(await this._wherePk(rowId))
.first();
// validate rowId
if (!row) {
NcError.notFound(`Row with id '${rowId}' not found`);
}
if (!childIds.length) return;
const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>();
const childColumn = await colOptions.getChildColumn();
const parentColumn = await colOptions.getParentColumn();
const parentTable = await parentColumn.getModel();
const childTable = await childColumn.getModel();
await childTable.getColumns();
await parentTable.getColumns();
const childTn = this.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable);
const prevData = await this.readByPk(rowId);
switch (colOptions.type) {
case RelationTypes.MANY_TO_MANY:
{
const vChildCol = await colOptions.getMMChildColumn();
const vParentCol = await colOptions.getMMParentColumn();
const vTable = await colOptions.getMMModel();
// validate Ids
{
const childRowsQb = this.dbDriver(parentTn)
.select(parentColumn.column_name)
// .where(_wherePk(parentTable.primaryKeys, childId))
.whereIn(parentTable.primaryKey.column_name, childIds);
if (parentTable.primaryKey.column_name !== parentColumn.column_name)
childRowsQb.select(parentTable.primaryKey.column_name);
const childRows = await childRowsQb;
if (childRows.length !== childIds.length) {
const missingIds = childIds.filter(
(id) =>
!childRows.find((r) => r[parentColumn.column_name] === id),
);
NcError.unprocessableEntity(
`Child record with id [${missingIds.join(', ')}] not found`,
);
}
}
const vTn = this.getTnPath(vTable);
await this.dbDriver(vTn)
.where({
[vChildCol.column_name]: this.dbDriver(childTn)
.select(childColumn.column_name)
.where(_wherePk(childTable.primaryKeys, rowId))
.first(),
})
.whereIn(
[vParentCol.column_name],
this.dbDriver(parentTn)
.select(parentColumn.column_name)
.whereIn(parentTable.primaryKey.column_name, childIds),
)
.delete();
}
break;
case RelationTypes.HAS_MANY:
{
// validate Ids
{
const childRowsQb = this.dbDriver(childTn)
.select(childTable.primaryKey.column_name)
.whereIn(childTable.primaryKey.column_name, childIds);
const childRows = await childRowsQb;
if (childRows.length !== childIds.length) {
const missingIds = childIds.filter(
(id) =>
!childRows.find((r) => r[parentColumn.column_name] === id),
);
NcError.unprocessableEntity(
`Child record with id [${missingIds.join(', ')}] not found`,
);
}
}
await this.dbDriver(childTn)
// .where({
// [childColumn.cn]: this.dbDriver(parentTable.tn)
// .select(parentColumn.cn)
// .where(parentTable.primaryKey.cn, rowId)
// .first()
// })
// .where(_wherePk(childTable.primaryKeys, childId))
.whereIn(childTable.primaryKey.column_name, childIds)
.update({ [childColumn.column_name]: null });
}
break;
case RelationTypes.BELONGS_TO:
{
// validate Ids
{
if (childIds.length > 1)
NcError.unprocessableEntity(
'Request must contain only one parent id',
);
const childRowsQb = this.dbDriver(parentTn)
.select(parentTable.primaryKey.column_name)
.where(_wherePk(parentTable.primaryKeys, childIds[0]))
.first();
const childRow = await childRowsQb;
if (!childRow) {
NcError.unprocessableEntity(
`Child record with id [${childIds[0]}] not found`,
);
}
}
await this.dbDriver(childTn)
// .where({
// [childColumn.cn]: this.dbDriver(parentTable.tn)
// .select(parentColumn.cn)
// .where(parentTable.primaryKey.cn, childId)
// .first()
// })
// .where(_wherePk(childTable.primaryKeys, rowId))
.where(childTable.primaryKey.column_name, rowId)
.update({ [childColumn.column_name]: null });
}
break;
}
// const newData = await this.readByPk(rowId);
// await this.afterUpdate(prevData, newData, this.dbDriver, cookie);
// await this.afterRemoveChild(rowId, childIds, cookie);
}
async btRead(
{ colId, id }: { colId; id },
args: { limit?; offset?; fieldSet?: Set<string> } = {},
) {
try {
const { where, sort } = this._getListArgs(args as any);
// todo: get only required fields
const relColumn = (await this.model.getColumns()).find(
(c) => c.id === colId,
);
const row = await this.dbDriver(this.tnPath)
.where(await this._wherePk(id))
.first();
// validate rowId
if (!row) {
NcError.notFound(`Row with id ${id} not found`);
}
const parentCol = await (
(await relColumn.getColOptions()) as LinkToAnotherRecordColumn
).getParentColumn();
const parentTable = await parentCol.getModel();
const chilCol = await (
(await relColumn.getColOptions()) as LinkToAnotherRecordColumn
).getChildColumn();
const childTable = await chilCol.getModel();
const parentModel = await Model.getBaseModelSQL({
model: parentTable,
dbDriver: this.dbDriver,
});
await childTable.getColumns();
const childTn = this.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable);
const qb = this.dbDriver(parentTn);
await this.applySortAndFilter({ table: parentTable, where, qb, sort });
qb.where(
parentCol.column_name,
this.dbDriver(childTn)
.select(chilCol.column_name)
// .where(parentTable.primaryKey.cn, p)
.where(_wherePk(childTable.primaryKeys, id)),
);
await parentModel.selectObject({ qb, fieldsSet: args.fieldSet });
const parent = (await this.execAndParse(qb, childTable))?.[0];
const proto = await parentModel.getProto();
if (parent) {
parent.__proto__ = proto;
}
return parent;
} catch (e) {
console.log(e);
throw e;
}
}
}
function extractSortsObject(

41
packages/nocodb/src/db/sql-client/lib/mssql/MssqlClient.ts

@ -2101,7 +2101,7 @@ class MssqlClient extends KnexClient {
table = table.onUpdate(relation.ur);
}
if (relation.dr) {
table = table.onDelete(relation.dr);
table.onDelete(relation.dr);
}
})
.toQuery());
@ -2192,28 +2192,31 @@ class MssqlClient extends KnexClient {
table = table.onUpdate(args.onUpdate);
}
if (args.onDelete) {
table = table.onDelete(args.onDelete);
table.onDelete(args.onDelete);
}
},
);
const upStatement =
this.querySeparator() +
(await this.sqlClient.schema
.table(this.getTnPath(args.childTable), function (table) {
table = table
.foreign(args.childColumn, foreignKeyName)
.references(args.parentColumn)
.on(self.getTnPath(args.parentTable));
const upQb = this.sqlClient.schema.table(
this.getTnPath(args.childTable),
function (table) {
table = table
.foreign(args.childColumn, foreignKeyName)
.references(args.parentColumn)
.on(self.getTnPath(args.parentTable));
if (args.onUpdate) {
table = table.onUpdate(args.onUpdate);
}
if (args.onDelete) {
table = table.onDelete(args.onDelete);
}
})
.toQuery());
if (args.onUpdate) {
table = table.onUpdate(args.onUpdate);
}
if (args.onDelete) {
table.onDelete(args.onDelete);
}
},
);
await upQb;
const upStatement = this.querySeparator() + upQb.toQuery();
this.emit(`Success : ${upStatement}`);
@ -2221,7 +2224,7 @@ class MssqlClient extends KnexClient {
this.querySeparator() +
this.sqlClient.schema
.table(this.getTnPath(args.childTable), function (table) {
table = table.dropForeign(args.childColumn, foreignKeyName);
table.dropForeign(args.childColumn, foreignKeyName);
})
.toQuery();

2
packages/nocodb/src/db/sql-client/lib/oracle/OracleClient.ts

@ -1914,7 +1914,7 @@ class OracleClient extends KnexClient {
* @returns {String} message
*/
async totalRecords(_args: any = {}): Promise<Result> {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const func = this.totalRecords.name;
throw new Error('Function not supported for oracle yet');
}

2
packages/nocodb/src/db/sql-client/lib/pg/PgClient.ts

@ -2359,7 +2359,7 @@ class PGClient extends KnexClient {
table = table.onUpdate(relation.ur);
}
if (relation.dr) {
table = table.onDelete(relation.dr);
table.onDelete(relation.dr);
}
})
.toQuery());

73
packages/nocodb/src/db/sql-client/lib/snowflake/SnowflakeClient.ts

@ -1970,23 +1970,24 @@ class SnowflakeClient extends KnexClient {
relationsList = relationsList.data.list;
for (const relation of relationsList) {
downQuery +=
this.querySeparator() +
(await this.sqlClient.schema
.table(relation.tn, function (table) {
table = table
.foreign(relation.cn, null)
.references(relation.rcn)
.on(relation.rtn);
if (relation.ur) {
table = table.onUpdate(relation.ur);
}
if (relation.dr) {
table = table.onDelete(relation.dr);
}
})
.toQuery());
const downQb = this.sqlClient.schema.table(
relation.tn,
function (table) {
table = table
.foreign(relation.cn, null)
.references(relation.rcn)
.on(relation.rtn);
if (relation.ur) {
table = table.onUpdate(relation.ur);
}
if (relation.dr) {
table.onDelete(relation.dr);
}
},
);
await downQb;
downQuery += this.querySeparator() + downQb.toQuery();
}
let indexList: any = await this.indexList(args);
@ -2060,8 +2061,6 @@ class SnowflakeClient extends KnexClient {
const foreignKeyName = args.foreignKeyName || null;
try {
// s = await this.sqlClient.schema.index(Object.keys(args.columns));
await this.sqlClient.schema.table(args.childTable, (table) => {
table = table
.foreign(args.childColumn, foreignKeyName)
@ -2072,27 +2071,27 @@ class SnowflakeClient extends KnexClient {
table = table.onUpdate(args.onUpdate);
}
if (args.onDelete) {
table = table.onDelete(args.onDelete);
table.onDelete(args.onDelete);
}
});
const upStatement =
this.querySeparator() +
(await this.sqlClient.schema
.table(args.childTable, (table) => {
table = table
.foreign(args.childColumn, foreignKeyName)
.references(args.parentColumn)
.on(this.getTnPath(args.parentTable));
const upQb = this.sqlClient.schema.table(args.childTable, (table) => {
table = table
.foreign(args.childColumn, foreignKeyName)
.references(args.parentColumn)
.on(this.getTnPath(args.parentTable));
if (args.onUpdate) {
table = table.onUpdate(args.onUpdate);
}
if (args.onDelete) {
table = table.onDelete(args.onDelete);
}
})
.toQuery());
if (args.onUpdate) {
table = table.onUpdate(args.onUpdate);
}
if (args.onDelete) {
table.onDelete(args.onDelete);
}
});
await upQb;
const upStatement = this.querySeparator() + upQb.toQuery();
this.emit(`Success : ${upStatement}`);
@ -2100,7 +2099,7 @@ class SnowflakeClient extends KnexClient {
this.querySeparator() +
this.sqlClient.schema
.table(args.childTable, (table) => {
table = table.dropForeign(args.childColumn, foreignKeyName);
table.dropForeign(args.childColumn, foreignKeyName);
})
.toQuery();

1
packages/nocodb/src/db/sql-client/lib/sqlite/SqliteClient.ts

@ -1866,6 +1866,7 @@ class SqliteClient extends KnexClient {
/* Filter relations for current table */
if (args.tn) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
relations = relations.filter(
(r) => r.tn === args.tn || r.rtn === args.tn,
);

44
packages/nocodb/src/db/sql-data-mapper/lib/BaseModel.ts

@ -321,7 +321,7 @@ abstract class BaseModel {
* @returns {Object} Table row data
*/
// @ts-ignore
async readByPk(id, { conditionGraph }) {
async readByPk(id) {
try {
return await this._run(
this.$db.select().where(this._wherePk(id)).first(),
@ -704,10 +704,7 @@ abstract class BaseModel {
*/
async exists(id, _) {
try {
return (
Object.keys(await this.readByPk(id, { conditionGraph: null }))
.length !== 0
);
return Object.keys(await this.readByPk(id)).length !== 0;
} catch (e) {
console.log(e);
throw e;
@ -1341,7 +1338,7 @@ abstract class BaseModel {
* @param {Object} data - insert data
* @param {Object} trx? - knex transaction reference
*/
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async beforeInsert(data, trx?: any, cookie?: {}) {}
/**
@ -1350,7 +1347,7 @@ abstract class BaseModel {
* @param {Object} response - inserted data
* @param {Object} trx? - knex transaction reference
*/
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async afterInsert(response, trx?: any, cookie?: {}) {}
/**
@ -1360,7 +1357,7 @@ abstract class BaseModel {
* @param {Object} data - insert data
* @param {Object} trx? - knex transaction reference
*/
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async errorInsert(err, data, trx?: any, cookie?: {}) {}
/**
@ -1369,7 +1366,7 @@ abstract class BaseModel {
* @param {Object} data - update data
* @param {Object} trx? - knex transaction reference
*/
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async beforeUpdate(data, trx?: any, cookie?: {}) {}
/**
@ -1378,7 +1375,7 @@ abstract class BaseModel {
* @param {Object} response - updated data
* @param {Object} trx? - knex transaction reference
*/
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async afterUpdate(response, trx?: any, cookie?: {}) {}
/**
@ -1388,7 +1385,7 @@ abstract class BaseModel {
* @param {Object} data - update data
* @param {Object} trx? - knex transaction reference
*/
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async errorUpdate(err, data, trx?: any, cookie?: {}) {}
/**
@ -1397,7 +1394,7 @@ abstract class BaseModel {
* @param {Object} data - delete data
* @param {Object} trx? - knex transaction reference
*/
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async beforeDelete(data, trx?: any, cookie?: {}) {}
/**
@ -1406,7 +1403,7 @@ abstract class BaseModel {
* @param {Object} response - Deleted data
* @param {Object} trx? - knex transaction reference
*/
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async afterDelete(response, trx?: any, cookie?: {}) {}
/**
@ -1416,7 +1413,7 @@ abstract class BaseModel {
* @param {Object} data - delete data
* @param {Object} trx? - knex transaction reference
*/
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async errorDelete(err, data, trx?: any, cookie?: {}) {}
/**
@ -1425,7 +1422,7 @@ abstract class BaseModel {
* @param {Object[]} data - insert data
* @param {Object} trx - knex transaction reference
*/
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async beforeInsertb(data, trx?: any) {}
/**
@ -1434,7 +1431,7 @@ abstract class BaseModel {
* @param {Object[]} response - inserted data
* @param {Object} trx - knex transaction reference
*/
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async afterInsertb(response, trx?: any) {}
/**
@ -1444,7 +1441,7 @@ abstract class BaseModel {
* @param {Object} data - delete data
* @param {Object} trx - knex transaction reference
*/
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async errorInsertb(err, data, trx?: any) {}
/**
@ -1453,7 +1450,7 @@ abstract class BaseModel {
* @param {Object[]} data - update data
* @param {Object} trx - knex transaction reference
*/
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async beforeUpdateb(data, trx?: any) {}
/**
@ -1462,7 +1459,7 @@ abstract class BaseModel {
* @param {Object[]} response - updated data
* @param {Object} trx - knex transaction reference
*/
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async afterUpdateb(response, trx?: any) {}
/**
@ -1472,7 +1469,7 @@ abstract class BaseModel {
* @param {Object[]} data - delete data
* @param {Object} trx - knex transaction reference
*/
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async errorUpdateb(err, data, trx?: any) {}
/**
@ -1481,7 +1478,7 @@ abstract class BaseModel {
* @param {Object[]} data - delete data
* @param {Object} trx - knex transaction reference
*/
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async beforeDeleteb(data, trx?: any) {}
/**
@ -1490,7 +1487,7 @@ abstract class BaseModel {
* @param {Object[]} response - deleted data
* @param {Object} trx - knex transaction reference
*/
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async afterDeleteb(response, trx?: any) {}
/**
@ -1500,12 +1497,13 @@ abstract class BaseModel {
* @param {Object[]} data - delete data
* @param {Object} trx - knex transaction reference
*/
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async errorDeleteb(err, data, trx?: any) {}
}
export interface XcFilter {
where?: string;
filter?: string;
having?: string;
condition?: any;
conditionGraph?: any;

2
packages/nocodb/src/db/sql-mgr/v2/SqlMgrv2Trans.ts

@ -1,7 +1,7 @@
import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2';
import KnexMigratorv2Tans from '../../sql-migrator/lib/KnexMigratorv2Tans';
import Base from '../../../models/Base';
import SqlMgrv2 from './SqlMgrv2';
import type Base from '../../../models/Base';
import type { Knex } from 'knex';
import type { XKnex } from '../../CustomKnex';

2
packages/nocodb/src/db/sql-migrator/lib/KnexMigrator.ts

@ -137,6 +137,7 @@ export default class KnexMigrator extends SqlMigrator {
),
);
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const dirStat = await promisify(fs.stat)(
path.join(
this.toolDir,
@ -232,6 +233,7 @@ export default class KnexMigrator extends SqlMigrator {
);
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const metaStat = await promisify(fs.stat)(
path.join(
this.toolDir,

1
packages/nocodb/src/db/sql-migrator/lib/KnexMigratorv2.ts

@ -114,6 +114,7 @@ export default class KnexMigratorv2 {
}*/
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async _initDbOnFs(base: Base) {
// this.emit(
// 'Creating folder: ',

4
packages/nocodb/src/filters/global-exception/global-exception.filter.ts

@ -8,6 +8,7 @@ import {
NotFound,
NotImplemented,
Unauthorized,
UnprocessableEntity,
} from '../../helpers/catchError';
import type { ArgumentsHost, ExceptionFilter } from '@nestjs/common';
import type { Response } from 'express';
@ -15,6 +16,7 @@ import type { Response } from 'express';
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
private logger = new Logger(GlobalExceptionFilter.name);
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
@ -58,6 +60,8 @@ export class GlobalExceptionFilter implements ExceptionFilter {
return response
.status(400)
.json({ msg: exception.message, errors: exception.errors });
} else if (exception instanceof UnprocessableEntity) {
return response.status(422).json({ msg: exception.message });
}
// handle different types of exceptions

8
packages/nocodb/src/gateways/socket.gateway.ts

@ -14,12 +14,20 @@ function getHash(str) {
return crypto.createHash('md5').update(str).digest('hex');
}
const url = new URL(
process.env.NC_PUBLIC_URL ||
`http://localhost:${process.env.PORT || '8080'}/`,
);
let namespace = url.pathname;
namespace += namespace.endsWith('/') ? '' : '/';
@WebSocketGateway({
cors: {
origin: '*',
allowedHeaders: ['xc-auth'],
credentials: true,
},
namespace,
})
@Injectable()
export class SocketGateway implements OnModuleInit {

23
packages/nocodb/src/helpers/PagedResponse.ts

@ -1,11 +1,6 @@
import { extractLimitAndOffset } from '.';
import type { PaginatedType } from 'nocodb-sdk';
const config: any = {
limitDefault: Math.max(+process.env.DB_QUERY_LIMIT_DEFAULT || 25, 1),
limitMin: Math.max(+process.env.DB_QUERY_LIMIT_MIN || 1, 1),
limitMax: Math.max(+process.env.DB_QUERY_LIMIT_MAX || 1000, 1),
};
export class PagedResponseImpl<T> {
constructor(
list: T[],
@ -17,12 +12,7 @@ export class PagedResponseImpl<T> {
o?: number;
} = {},
) {
const limit = Math.max(
Math.min(args.limit || args.l || config.limitDefault, config.limitMax),
config.limitMin,
);
const offset = Math.max(+(args.offset || args.o) || 0, 0);
const { offset, limit } = extractLimitAndOffset(args);
let count = args.count ?? null;
@ -40,8 +30,17 @@ export class PagedResponseImpl<T> {
this.pageInfo.page ===
(Math.ceil(this.pageInfo.totalRows / this.pageInfo.pageSize) || 1);
}
if (offset && offset >= count) {
this.errors = [
{
message: 'Offset is beyond the total number of rows',
},
];
}
}
list: Array<T>;
pageInfo: PaginatedType;
errors?: any[];
}

2
packages/nocodb/src/helpers/apiMetrics.ts

@ -3,7 +3,7 @@ import type { Request } from 'express';
const countMap = {};
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const metrics = async (req: Request, c = 150) => {
if (!req?.route?.path) return;
const event = `a:api:${req.route.path}:${req.method}`;

8
packages/nocodb/src/helpers/catchError.ts

@ -413,6 +413,8 @@ export default function (
return res.status(501).json({ msg: e.message });
} else if (e instanceof AjvError) {
return res.status(400).json({ msg: e.message, errors: e.errors });
} else if (e instanceof UnprocessableEntity) {
return res.status(422).json({ msg: e.message });
}
next(e);
}
@ -431,6 +433,8 @@ export class InternalServerError extends Error {}
export class NotImplemented extends Error {}
export class UnprocessableEntity extends Error {}
export class AjvError extends Error {
constructor(param: { message: string; errors: ErrorObject[] }) {
super(param.message);
@ -468,4 +472,8 @@ export class NcError {
static ajvValidationError(param: { message: string; errors: ErrorObject[] }) {
throw new AjvError(param);
}
static unprocessableEntity(message = 'Unprocessable entity') {
throw new UnprocessableEntity(message);
}
}

39
packages/nocodb/src/helpers/extractLimitAndOffset.ts

@ -0,0 +1,39 @@
const config = {
limitDefault: Math.max(+process.env.DB_QUERY_LIMIT_DEFAULT || 25, 1),
limitMin: Math.max(+process.env.DB_QUERY_LIMIT_MIN || 1, 1),
limitMax: Math.max(+process.env.DB_QUERY_LIMIT_MAX || 1000, 1),
};
export function extractLimitAndOffset(
args: {
limit?: number | string;
offset?: number | string;
l?: number | string;
o?: number | string;
} = {},
) {
const obj: {
limit?: number;
offset?: number;
} = {};
// use default value if invalid limit
// for example, if limit is not a number, it will be ignored
// if limit is less than 1, it will be ignored
const limit = +(args.limit || args.l);
obj.limit = Math.max(
Math.min(
limit && limit > 0 && Number.isInteger(limit)
? limit
: config.limitDefault,
config.limitMax,
),
config.limitMin,
);
// skip any invalid offset, ignore negative and non-integer values
const offset = +(args.offset || args.o) || 0;
obj.offset = Math.max(Number.isInteger(offset) ? offset : 0, 0);
return obj;
}

1
packages/nocodb/src/helpers/index.ts

@ -2,5 +2,6 @@ import { populateMeta } from './populateMeta';
export * from './columnHelpers';
export * from './apiHelpers';
export * from './cacheHelpers';
export * from './extractLimitAndOffset';
export { populateMeta };

1
packages/nocodb/src/interface/XcMetaMgr.ts

@ -1,2 +1 @@
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export default interface XcMetaMgr {}

2
packages/nocodb/src/meta/migrations/v1/nc_011_remove_old_ses_plugin.ts

@ -5,7 +5,7 @@ const up = async (knex: Knex) => {
await knex('nc_plugins').del().where({ title: 'SES' });
};
const down = async (knex: Knex) => {
const down = async (_: Knex) => {
// await knex('nc_plugins').insert([ses]);
};

1
packages/nocodb/src/models/Model.ts

@ -65,6 +65,7 @@ export default class Model implements TableType {
}
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async getViews(force = false, ncMeta = Noco.ncMeta): Promise<View[]> {
this.views = await View.listWithInfo(this.id, ncMeta);
return this.views;

1
packages/nocodb/src/models/View.ts

@ -1,4 +1,3 @@
import { title } from 'process';
import { isSystemColumn, UITypes, ViewTypes } from 'nocodb-sdk';
import Noco from '../Noco';
import {

4
packages/nocodb/src/modules/datas/datas.module.ts

@ -3,8 +3,10 @@ import { MulterModule } from '@nestjs/platform-express';
import multer from 'multer';
import { NC_ATTACHMENT_FIELD_SIZE } from '../../constants';
import { DataAliasController } from '../../controllers/data-alias.controller';
import { DataTableController } from '../../controllers/data-table.controller';
import { PublicDatasExportController } from '../../controllers/public-datas-export.controller';
import { PublicDatasController } from '../../controllers/public-datas.controller';
import { DataTableService } from '../../services/data-table.service';
import { DatasService } from '../../services/datas.service';
import { DatasController } from '../../controllers/datas.controller';
import { BulkDataAliasController } from '../../controllers/bulk-data-alias.controller';
@ -29,6 +31,7 @@ import { PublicDatasService } from '../../services/public-datas.service';
controllers: [
...(process.env.NC_WORKER_CONTAINER !== 'true'
? [
DataTableController,
DatasController,
BulkDataAliasController,
DataAliasController,
@ -41,6 +44,7 @@ import { PublicDatasService } from '../../services/public-datas.service';
: []),
],
providers: [
DataTableService,
DatasService,
BulkDataAliasService,
DataAliasNestedService,

2
packages/nocodb/src/modules/event-emitter/nestjs-event-emitter.ts

@ -1,4 +1,4 @@
import type { EventEmitter2 } from '@nestjs/event-emitter';
import { EventEmitter2 } from '@nestjs/event-emitter';
import type { IEventEmitter } from './event-emitter.interface';
export class NestjsEventEmitter implements IEventEmitter {

9
packages/nocodb/src/modules/jobs/jobs.gateway.ts

@ -14,13 +14,20 @@ import { JobEvents } from '../../interface/Jobs';
import type { OnModuleInit } from '@nestjs/common';
import type { JobStatus } from '../../interface/Jobs';
const url = new URL(
process.env.NC_PUBLIC_URL ||
`http://localhost:${process.env.PORT || '8080'}/`,
);
let namespace = url.pathname;
namespace += namespace.endsWith('/') ? 'jobs' : '/jobs';
@WebSocketGateway({
cors: {
origin: '*',
allowedHeaders: ['xc-auth'],
credentials: true,
},
namespace: 'jobs',
namespace,
})
export class JobsGateway implements OnModuleInit {
constructor(@Inject('JobsService') private readonly jobsService) {}

2
packages/nocodb/src/modules/jobs/jobs/at-import/at-import.controller.ts

@ -65,7 +65,7 @@ export class AtImportController {
@Post('/api/v1/db/meta/syncs/:syncId/abort')
@HttpCode(200)
async abortImport(@Request() req) {
async abortImport(@Request() _) {
return {};
}
}

89
packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts

@ -348,32 +348,6 @@ export class AtImportProcessor {
}
};
const nc_DumpTableSchema = async () => {
console.log('[');
// const ncTblList = await api.base.tableList(
// ncCreatedProjectSchema.id,
// syncDB.baseId
// );
const ncTblList = { list: [] };
ncTblList['list'] = await this.tablesService.getAccessibleTables({
projectId: ncCreatedProjectSchema.id,
baseId: syncDB.baseId,
roles: userRole,
});
for (let i = 0; i < ncTblList.list.length; i++) {
// const ncTbl = await api.dbTable.read(ncTblList.list[i].id);
const ncTbl = await this.tablesService.getTableWithAccessibleViews({
tableId: ncTblList.list[i].id,
user: syncDB.user,
});
console.log(JSON.stringify(ncTbl, null, 2));
console.log(',');
}
console.log(']');
};
// retrieve nc column schema from using aTbl field ID as reference
//
const nc_getColumnSchema = async (aTblFieldId) => {
@ -1563,45 +1537,6 @@ export class AtImportProcessor {
return rec;
};
const nocoReadDataSelected = async (projName, table, callback, fields) => {
return new Promise((resolve, reject) => {
base(table.title)
.select({
pageSize: 100,
// maxRecords: 100,
fields: fields,
})
.eachPage(
async function page(records, fetchNextPage) {
// This function (`page`) will get called for each page of records.
// records.forEach(record => callback(table, record));
logBasic(
`:: ${table.title} / ${fields} : ${
recordCnt + 1
} ~ ${(recordCnt += 100)}`,
);
await Promise.all(
records.map((r) => callback(projName, table, r, fields)),
);
// To fetch the next page of records, call `fetchNextPage`.
// If there are more records, `page` will get called again.
// If there are no more records, `done` will get called.
fetchNextPage();
},
function done(err) {
if (err) {
console.error(err);
reject(err);
}
resolve(null);
},
);
});
};
//////////
const nc_isLinkExists = (airtableFieldId) => {
return !!ncLinkMappingTable.find(
(x) => x.aTbl.typeOptions.symmetricColumnId === airtableFieldId,
@ -1879,9 +1814,7 @@ export class AtImportProcessor {
req: { user: syncDB.user, clientIp: '' },
})
.catch((e) =>
e.response?.data?.msg
? logBasic(`NOTICE: ${e.response.data.msg}`)
: console.log(e),
e.message ? logBasic(`NOTICE: ${e.message}`) : console.log(e),
),
);
recordPerfStats(_perfStart, 'auth.projectUserAdd');
@ -2294,7 +2227,6 @@ export class AtImportProcessor {
};
///////////////////////////////////////////////////////////////////////////////
let recordCnt = 0;
try {
logBasic('SDK initialized');
logDetailed('Project initialization started');
@ -2357,10 +2289,12 @@ export class AtImportProcessor {
await nocoSetPrimary(aTblSchema);
logDetailed('Configuring Display Value column completed');
logBasic('Configuring User(s)');
// add users
await nocoAddUsers(schema);
logDetailed('Adding users completed');
if (syncDB.options.syncUsers) {
logBasic('Configuring User(s)');
// add users
await nocoAddUsers(schema);
logDetailed('Adding users completed');
}
// hide-fields
// await nocoReconfigureFields(aTblSchema);
@ -2374,7 +2308,6 @@ export class AtImportProcessor {
if (syncDB.options.syncData) {
try {
// await nc_DumpTableSchema();
const _perfStart = recordPerfStart();
const ncTblList = { list: [] };
ncTblList['list'] = await this.tablesService.getAccessibleTables({
@ -2404,8 +2337,6 @@ export class AtImportProcessor {
});
recordPerfStats(_perfStart, 'dbTable.read');
recordCnt = 0;
recordsMap[ncTbl.id] = await importData({
projectName: syncDB.projectName,
table: ncTbl,
@ -2469,12 +2400,12 @@ export class AtImportProcessor {
await generateMigrationStats(aTblSchema);
}
} catch (e) {
if (e.response?.data?.msg) {
if (e.message) {
T.event({
event: 'a:airtable-import:error',
data: { error: e.response.data.msg },
data: { error: e.message },
});
throw new Error(e.response.data.msg);
throw new Error(e.message);
}
throw e;
}

1
packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts

@ -1214,6 +1214,7 @@ export class ImportService {
storageAdapter as any
).fileReadByStream(linkFile);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
handledLinks = await this.importLinkFromCsvStream({
idMap,
linkStream: linkReadStream,

4
packages/nocodb/src/services/auth.service.ts

@ -40,8 +40,8 @@ export class AuthService {
email: _email,
firstname,
lastname,
token,
ignore_subscribe,
// token,
// ignore_subscribe,
} = createUserDto as any;
let { password } = createUserDto;

19
packages/nocodb/src/services/data-table.service.spec.ts

@ -0,0 +1,19 @@
import { Test } from '@nestjs/testing';
import { DataTableService } from './data-table.service';
import type { TestingModule } from '@nestjs/testing';
describe('DataTableService', () => {
let service: DataTableService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DataTableService],
}).compile();
service = module.get<DataTableService>(DataTableService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

434
packages/nocodb/src/services/data-table.service.ts

@ -0,0 +1,434 @@
import { Injectable } from '@nestjs/common';
import { RelationTypes, UITypes } from 'nocodb-sdk';
import { nocoExecute } from 'nc-help';
import { NcError } from '../helpers/catchError';
import getAst from '../helpers/getAst';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import { Base, Column, Model, View } from '../models';
import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2';
import { DatasService } from './datas.service';
import type { LinkToAnotherRecordColumn } from '../models';
@Injectable()
export class DataTableService {
constructor(private datasService: DatasService) {}
async dataList(param: {
projectId?: string;
modelId: string;
query: any;
viewId?: string;
}) {
const { model, view } = await this.getModelAndView(param);
return await this.datasService.getDataList({
model,
view,
query: param.query,
});
}
async dataRead(param: {
projectId?: string;
modelId: string;
rowId: string;
viewId?: string;
query: any;
}) {
const { model, view } = await this.getModelAndView(param);
const base = await Base.get(model.base_id);
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view?.id,
dbDriver: await NcConnectionMgrv2.get(base),
});
const row = await baseModel.readByPk(param.rowId, false, param.query);
if (!row) {
NcError.notFound('Row not found');
}
return row;
}
async dataInsert(param: {
projectId?: string;
viewId?: string;
modelId: string;
body: any;
cookie: any;
}) {
const { model, view } = await this.getModelAndView(param);
const base = await Base.get(model.base_id);
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view?.id,
dbDriver: await NcConnectionMgrv2.get(base),
});
// if array then do bulk insert
const result = await baseModel.bulkInsert(
Array.isArray(param.body) ? param.body : [param.body],
{ cookie: param.cookie, insertOneByOneAsFallback: true },
);
return Array.isArray(param.body) ? result : result[0];
}
async dataUpdate(param: {
projectId?: string;
modelId: string;
viewId?: string;
// rowId: string;
body: any;
cookie: any;
}) {
const { model, view } = await this.getModelAndView(param);
await this.checkForDuplicateRow({ rows: param.body, model });
const base = await Base.get(model.base_id);
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view?.id,
dbDriver: await NcConnectionMgrv2.get(base),
});
const res = await baseModel.bulkUpdate(
Array.isArray(param.body) ? param.body : [param.body],
{ cookie: param.cookie, throwExceptionIfNotExist: true },
);
return this.extractIdObj({ body: param.body, model });
}
async dataDelete(param: {
projectId?: string;
modelId: string;
viewId?: string;
// rowId: string;
cookie: any;
body: any;
}) {
const { model, view } = await this.getModelAndView(param);
await this.checkForDuplicateRow({ rows: param.body, model });
const base = await Base.get(model.base_id);
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view?.id,
dbDriver: await NcConnectionMgrv2.get(base),
});
await baseModel.bulkDelete(
Array.isArray(param.body) ? param.body : [param.body],
{ cookie: param.cookie, throwExceptionIfNotExist: true },
);
return this.extractIdObj({ body: param.body, model });
}
async dataCount(param: {
projectId?: string;
viewId?: string;
modelId: string;
query: any;
}) {
const { model, view } = await this.getModelAndView(param);
const base = await Base.get(model.base_id);
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view?.id,
dbDriver: await NcConnectionMgrv2.get(base),
});
const countArgs: any = { ...param.query };
try {
countArgs.filterArr = JSON.parse(countArgs.filterArrJson);
} catch (e) {}
const count: number = await baseModel.count(countArgs);
return { count };
}
private async getModelAndView(param: {
projectId?: string;
viewId?: string;
modelId: string;
}) {
const model = await Model.get(param.modelId);
if (!model) {
NcError.notFound(`Table with id '${param.modelId}' not found`);
}
if (param.projectId && model.project_id !== param.projectId) {
throw new Error('Table not belong to project');
}
let view: View;
if (param.viewId) {
view = await View.get(param.viewId);
if (!view || (view.fk_model_id && view.fk_model_id !== param.modelId)) {
NcError.unprocessableEntity(`View with id '${param.viewId}' not found`);
}
}
return { model, view };
}
private async extractIdObj({
model,
body,
}: {
body: Record<string, any> | Record<string, any>[];
model: Model;
}) {
const pkColumns = await model
.getColumns()
.then((cols) => cols.filter((col) => col.pk));
const result = (Array.isArray(body) ? body : [body]).map((row) => {
return pkColumns.reduce((acc, col) => {
acc[col.title] = row[col.title];
return acc;
}, {});
});
return Array.isArray(body) ? result : result[0];
}
private async checkForDuplicateRow({
rows,
model,
}: {
rows: any[] | any;
model: Model;
}) {
if (!rows || !Array.isArray(rows) || rows.length === 1) {
return;
}
await model.getColumns();
const keys = new Set();
for (const row of rows) {
let pk;
// if only one primary key then extract the value
if (model.primaryKeys.length === 1) pk = row[model.primaryKey.title];
// if composite primary key then join the values with ___
else pk = model.primaryKeys.map((pk) => row[pk.title]).join('___');
// if duplicate then throw error
if (keys.has(pk)) {
NcError.unprocessableEntity('Duplicate row with id ' + pk);
}
keys.add(pk);
}
}
async nestedDataList(param: {
viewId: string;
modelId: string;
query: any;
rowId: string | string[] | number | number[];
columnId: string;
}) {
const { model, view } = await this.getModelAndView(param);
const base = await Base.get(model.base_id);
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view?.id,
dbDriver: await NcConnectionMgrv2.get(base),
});
if (!(await baseModel.exist(param.rowId))) {
NcError.notFound(`Row with id '${param.rowId}' not found`);
}
const column = await this.getColumn(param);
const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>();
const relatedModel = await colOptions.getRelatedTable();
const { ast, dependencyFields } = await getAst({
model: relatedModel,
query: param.query,
extractOnlyPrimaries: !(param.query?.f || param.query?.fields),
});
const listArgs: any = dependencyFields;
try {
listArgs.filterArr = JSON.parse(listArgs.filterArrJson);
} catch (e) {}
try {
listArgs.sortArr = JSON.parse(listArgs.sortArrJson);
} catch (e) {}
let data: any[];
let count: number;
if (colOptions.type === RelationTypes.MANY_TO_MANY) {
data = await baseModel.mmList(
{
colId: column.id,
parentId: param.rowId,
},
listArgs as any,
);
count = (await baseModel.mmListCount({
colId: column.id,
parentId: param.rowId,
})) as number;
} else if (colOptions.type === RelationTypes.HAS_MANY) {
data = await baseModel.hmList(
{
colId: column.id,
id: param.rowId,
},
listArgs as any,
);
count = (await baseModel.hmListCount({
colId: column.id,
id: param.rowId,
})) as number;
} else {
data = await baseModel.btRead(
{
colId: column.id,
id: param.rowId,
},
param.query as any,
);
}
data = await nocoExecute(ast, data, {}, listArgs);
if (colOptions.type === RelationTypes.BELONGS_TO) return data;
return new PagedResponseImpl(data, {
count,
...param.query,
});
}
private async getColumn(param: { modelId: string; columnId: string }) {
const column = await Column.get({ colId: param.columnId });
if (!column)
NcError.notFound(`Column with id '${param.columnId}' not found`);
if (column.fk_model_id !== param.modelId)
NcError.badRequest('Column not belong to model');
if (column.uidt !== UITypes.LinkToAnotherRecord)
NcError.badRequest('Column is not LTAR');
return column;
}
async nestedLink(param: {
cookie: any;
viewId: string;
modelId: string;
columnId: string;
query: any;
refRowIds: string | string[] | number | number[];
rowId: string;
}) {
this.validateIds(param.refRowIds);
const { model, view } = await this.getModelAndView(param);
const base = await Base.get(model.base_id);
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view?.id,
dbDriver: await NcConnectionMgrv2.get(base),
});
const column = await this.getColumn(param);
await baseModel.addLinks({
colId: column.id,
childIds: Array.isArray(param.refRowIds)
? param.refRowIds
: [param.refRowIds],
rowId: param.rowId,
cookie: param.cookie,
});
return true;
}
async nestedUnlink(param: {
cookie: any;
viewId: string;
modelId: string;
columnId: string;
query: any;
refRowIds: string | string[] | number | number[];
rowId: string;
}) {
this.validateIds(param.refRowIds);
const { model, view } = await this.getModelAndView(param);
if (!model)
NcError.notFound('Table with id ' + param.modelId + ' not found');
const base = await Base.get(model.base_id);
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view?.id,
dbDriver: await NcConnectionMgrv2.get(base),
});
const column = await this.getColumn(param);
await baseModel.removeLinks({
colId: column.id,
childIds: Array.isArray(param.refRowIds)
? param.refRowIds
: [param.refRowIds],
rowId: param.rowId,
cookie: param.cookie,
});
return true;
}
private validateIds(rowIds: any[] | any) {
if (Array.isArray(rowIds)) {
const map = new Map<string, boolean>();
const set = new Set<string>();
for (const rowId of rowIds) {
if (rowId === undefined || rowId === null)
NcError.unprocessableEntity('Invalid row id ' + rowId);
if (map.has(rowId)) {
set.add(rowId);
} else {
map.set(rowId, true);
}
}
if (set.size > 0)
NcError.unprocessableEntity(
'Child record with id [' + [...set].join(', ') + '] are duplicated',
);
} else if (rowIds === undefined || rowIds === null) {
NcError.unprocessableEntity('Invalid row id ' + rowIds);
}
}
}

2
packages/nocodb/src/services/datas.service.ts

@ -117,7 +117,7 @@ export class DatasService {
async getDataList(param: {
model: Model;
view: View;
view?: View;
query: any;
baseModel?: BaseModelSqlv2;
}) {

8
packages/nocodb/src/services/tables.service.ts

@ -14,13 +14,7 @@ import getColumnPropsFromUIDT from '../helpers/getColumnPropsFromUIDT';
import getColumnUiType from '../helpers/getColumnUiType';
import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName';
import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue';
import {
Audit,
Column,
Model,
ModelRoleVisibility,
Project,
} from '../models';
import { Audit, Column, Model, ModelRoleVisibility, Project } from '../models';
import Noco from '../Noco';
import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2';
import { validatePayload } from '../helpers';

1
packages/nocodb/src/utils/common/XcAudit.ts

@ -9,5 +9,6 @@ export default class XcAudit {
private static app: Noco;
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public static async log(data: { project }) {}
}

4
packages/nocodb/src/version-upgrader/v1-legacy/BaseApiBuilder.ts

@ -3,15 +3,15 @@ import { Router } from 'express';
import inflection from 'inflection';
import ModelXcMetaFactory from '../../db/sql-mgr/code/models/xc/ModelXcMetaFactory';
import NcConnectionMgr from '../../utils/common/NcConnectionMgr';
import { DbConfig, NcConfig } from '../../interface/config';
import ncModelsOrderUpgrader from './jobs/ncModelsOrderUpgrader';
import ncParentModelTitleUpgrader from './jobs/ncParentModelTitleUpgrader';
import ncRemoveDuplicatedRelationRows from './jobs/ncRemoveDuplicatedRelationRows';
import type { DbConfig, NcConfig } from '../../interface/config';
import NcProjectBuilder from './NcProjectBuilder';
import type { XKnex } from '../../db/CustomKnex';
import type { BaseModelSql } from '../../db/BaseModelSql';
import type { MetaService } from '../../meta/meta.service';
import type Noco from '../../Noco';
import type NcProjectBuilder from './NcProjectBuilder';
import type { MysqlClient, PgClient, SqlClient } from 'nc-help';
const log = debug('nc:api:base');

4
packages/nocodb/src/version-upgrader/v1-legacy/NcProjectBuilder.ts

@ -1,9 +1,9 @@
import { Router } from 'express';
import { SqlClientFactory } from '../../db/sql-client/lib/SqlClientFactory';
import Noco from '../../Noco';
import { NcConfig } from '../../interface/config';
import { GqlApiBuilder } from './gql/GqlApiBuilder';
import { RestApiBuilder } from './rest/RestApiBuilder';
import type Noco from '../../Noco';
import type { NcConfig } from '../../interface/config';
export default class NcProjectBuilder {
public readonly id: string;

8
packages/nocodb/src/version-upgrader/v1-legacy/gql/GqlApiBuilder.ts

@ -1,10 +1,10 @@
import { Router } from 'express';
import GqlXcSchemaFactory from '../../../db/sql-mgr/code/gql-schema/xc-ts/GqlXcSchemaFactory';
import BaseApiBuilder from '../BaseApiBuilder';
import type { MetaService } from '../../../meta/meta.service';
import type Noco from '../../../Noco';
import type NcProjectBuilder from '../NcProjectBuilder';
import type { DbConfig, NcConfig } from '../../../interface/config';
import { MetaService } from '../../../meta/meta.service';
import Noco from '../../../Noco';
import NcProjectBuilder from '../NcProjectBuilder';
import { DbConfig, NcConfig } from '../../../interface/config';
import type XcMetaMgr from '../../../interface/XcMetaMgr';
export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {

8
packages/nocodb/src/version-upgrader/v1-legacy/rest/RestApiBuilder.ts

@ -3,11 +3,11 @@ import SwaggerXc from '../../../db/sql-mgr/code/routers/xc-ts/SwaggerXc';
import ExpressXcTsRoutes from '../../../db/sql-mgr/code/routes/xc-ts/ExpressXcTsRoutes';
import NcHelp from '../../../utils/NcHelp';
import BaseApiBuilder from '../BaseApiBuilder';
import type { MetaService } from '../../../meta/meta.service';
import type Noco from '../../../Noco';
import { MetaService } from '../../../meta/meta.service';
import Noco from '../../../Noco';
import { DbConfig, NcConfig } from '../../../interface/config';
import NcProjectBuilder from '../NcProjectBuilder';
import type { Router } from 'express';
import type { DbConfig, NcConfig } from '../../../interface/config';
import type NcProjectBuilder from '../NcProjectBuilder';
export class RestApiBuilder extends BaseApiBuilder<Noco> {
public readonly type = 'rest';

163
packages/nocodb/tests/unit/factory/column.ts

@ -1,13 +1,13 @@
import { UITypes } from 'nocodb-sdk';
import request from 'supertest';
import Column from '../../../src/models/Column';
import FormViewColumn from '../../../src/models/FormViewColumn';
import GalleryViewColumn from '../../../src/models/GalleryViewColumn';
import GridViewColumn from '../../../src/models/GridViewColumn';
import Model from '../../../src/models/Model';
import Project from '../../../src/models/Project';
import View from '../../../src/models/View';
import { isSqlite, isPg } from '../init/db';
import { isPg, isSqlite } from '../init/db';
import type Column from '../../../src/models/Column';
import type FormViewColumn from '../../../src/models/FormViewColumn';
import type GalleryViewColumn from '../../../src/models/GalleryViewColumn';
import type GridViewColumn from '../../../src/models/GridViewColumn';
import type Project from '../../../src/models/Project';
import type View from '../../../src/models/View';
const defaultColumns = function (context) {
return [
@ -46,6 +46,122 @@ const defaultColumns = function (context) {
];
};
const customColumns = function (type: string, options: any = {}) {
switch (type) {
case 'textBased':
return [
{
column_name: 'Id',
title: 'Id',
uidt: UITypes.ID,
},
{
column_name: 'SingleLineText',
title: 'SingleLineText',
uidt: UITypes.SingleLineText,
},
{
column_name: 'MultiLineText',
title: 'MultiLineText',
uidt: UITypes.LongText,
},
{
column_name: 'Email',
title: 'Email',
uidt: UITypes.Email,
},
{
column_name: 'Phone',
title: 'Phone',
uidt: UITypes.PhoneNumber,
},
{
column_name: 'Url',
title: 'Url',
uidt: UITypes.URL,
},
];
case 'numberBased':
return [
{
column_name: 'Id',
title: 'Id',
uidt: UITypes.ID,
},
{
column_name: 'Number',
title: 'Number',
uidt: UITypes.Number,
},
{
column_name: 'Decimal',
title: 'Decimal',
uidt: UITypes.Decimal,
},
{
column_name: 'Currency',
title: 'Currency',
uidt: UITypes.Currency,
},
{
column_name: 'Percent',
title: 'Percent',
uidt: UITypes.Percent,
},
{
column_name: 'Duration',
title: 'Duration',
uidt: UITypes.Duration,
},
{
column_name: 'Rating',
title: 'Rating',
uidt: UITypes.Rating,
},
];
case 'dateBased':
return [
{
column_name: 'Id',
title: 'Id',
uidt: UITypes.ID,
},
{
column_name: 'Date',
title: 'Date',
uidt: UITypes.Date,
},
{
column_name: 'DateTime',
title: 'DateTime',
uidt: UITypes.DateTime,
},
];
case 'selectBased':
return [
{
column_name: 'Id',
title: 'Id',
uidt: UITypes.ID,
},
{
column_name: 'SingleSelect',
title: 'SingleSelect',
uidt: UITypes.SingleSelect,
dtxp: "'jan','feb','mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'",
},
{
column_name: 'MultiSelect',
title: 'MultiSelect',
uidt: UITypes.MultiSelect,
dtxp: "'jan','feb','mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'",
},
];
case 'custom':
return [{ title: 'Id', column_name: 'Id', uidt: UITypes.ID }, ...options];
}
};
const createColumn = async (context, table, columnAttr) => {
await request(context.app)
.post(`/api/v1/db/meta/tables/${table.id}/columns`)
@ -55,7 +171,7 @@ const createColumn = async (context, table, columnAttr) => {
});
const column: Column = (await table.getColumns()).find(
(column) => column.title === columnAttr.title
(column) => column.title === columnAttr.title,
);
return column;
};
@ -76,7 +192,7 @@ const createRollupColumn = async (
table: Model;
relatedTableName: string;
relatedTableColumnTitle: string;
}
},
) => {
const childBases = await project.getBases();
const childTable = await Model.getByIdOrName({
@ -86,13 +202,13 @@ const createRollupColumn = async (
});
const childTableColumns = await childTable.getColumns();
const childTableColumn = await childTableColumns.find(
(column) => column.title === relatedTableColumnTitle
(column) => column.title === relatedTableColumnTitle,
);
const ltarColumn = (await table.getColumns()).find(
(column) =>
column.uidt === UITypes.LinkToAnotherRecord &&
column.colOptions?.fk_related_model_id === childTable.id
column.colOptions?.fk_related_model_id === childTable.id,
);
const rollupColumn = await createColumn(context, table, {
@ -122,7 +238,7 @@ const createLookupColumn = async (
table: Model;
relatedTableName: string;
relatedTableColumnTitle: string;
}
},
) => {
const childBases = await project.getBases();
const childTable = await Model.getByIdOrName({
@ -132,19 +248,19 @@ const createLookupColumn = async (
});
const childTableColumns = await childTable.getColumns();
const childTableColumn = await childTableColumns.find(
(column) => column.title === relatedTableColumnTitle
(column) => column.title === relatedTableColumnTitle,
);
if (!childTableColumn) {
throw new Error(
`Could not find column ${relatedTableColumnTitle} in ${relatedTableName}`
`Could not find column ${relatedTableColumnTitle} in ${relatedTableName}`,
);
}
const ltarColumn = (await table.getColumns()).find(
(column) =>
column.uidt === UITypes.LinkToAnotherRecord &&
column.colOptions?.fk_related_model_id === childTable.id
column.colOptions?.fk_related_model_id === childTable.id,
);
const lookupColumn = await createColumn(context, table, {
title: title,
@ -168,15 +284,15 @@ const createQrCodeColumn = async (
title: string;
table: Model;
referencedQrValueTableColumnTitle: string;
}
},
) => {
const referencedQrValueTableColumnId = await table
.getColumns()
.then(
(cols) =>
cols.find(
(column) => column.title == referencedQrValueTableColumnTitle
)['id']
(column) => column.title == referencedQrValueTableColumnTitle,
)['id'],
);
const qrCodeColumn = await createColumn(context, table, {
@ -198,15 +314,15 @@ const createBarcodeColumn = async (
title: string;
table: Model;
referencedBarcodeValueTableColumnTitle: string;
}
},
) => {
const referencedBarcodeValueTableColumnId = await table
.getColumns()
.then(
(cols) =>
cols.find(
(column) => column.title == referencedBarcodeValueTableColumnTitle
)['id']
(column) => column.title == referencedBarcodeValueTableColumnTitle,
)['id'],
);
const barcodeColumn = await createColumn(context, table, {
@ -230,7 +346,7 @@ const createLtarColumn = async (
parentTable: Model;
childTable: Model;
type: string;
}
},
) => {
const ltarColumn = await createColumn(context, parentTable, {
title: title,
@ -246,7 +362,7 @@ const createLtarColumn = async (
const updateViewColumn = async (
context,
{ view, column, attr }: { column: Column; view: View; attr: any }
{ view, column, attr }: { column: Column; view: View; attr: any },
) => {
const res = await request(context.app)
.patch(`/api/v1/db/meta/views/${view.id}/columns/${column.id}`)
@ -263,6 +379,7 @@ const updateViewColumn = async (
};
export {
customColumns,
defaultColumns,
createColumn,
createQrCodeColumn,

35
packages/nocodb/tests/unit/factory/row.ts

@ -1,11 +1,12 @@
import { ColumnType, UITypes } from 'nocodb-sdk';
import { UITypes } from 'nocodb-sdk';
import request from 'supertest';
import Column from '../../../src/models/Column';
import Filter from '../../../src/models/Filter';
import Model from '../../../src/models/Model';
import Project from '../../../src/models/Project';
import Sort from '../../../src/models/Sort';
import NcConnectionMgrv2 from '../../../src/utils/common/NcConnectionMgrv2';
import type { ColumnType } from 'nocodb-sdk';
import type Column from '../../../src/models/Column';
import type Filter from '../../../src/models/Filter';
import type Project from '../../../src/models/Project';
import type Sort from '../../../src/models/Sort';
const rowValue = (column: ColumnType, index: number) => {
switch (column.uidt) {
@ -175,9 +176,17 @@ const rowMixedValue = (column: ColumnType, index: number) => {
case UITypes.Date:
// set startDate as 400 days before today
// eslint-disable-next-line no-case-declarations
const result = new Date();
result.setDate(result.getDate() - 400 + index);
return result.toISOString().slice(0, 10);
const d1 = new Date();
d1.setDate(d1.getDate() - 400 + index);
return d1.toISOString().slice(0, 10);
case UITypes.DateTime:
// set startDate as 400 days before today
// eslint-disable-next-line no-case-declarations
const d2 = new Date();
d2.setDate(d2.getDate() - 400 + index);
// set time to 12:00:00
d2.setHours(12, 0, 0, 0);
return d2.toISOString();
case UITypes.URL:
return urls[index % urls.length];
case UITypes.SingleSelect:
@ -228,7 +237,7 @@ const listRow = async ({
const getOneRow = async (
context,
{ project, table }: { project: Project; table: Model }
{ project, table }: { project: Project; table: Model },
) => {
const response = await request(context.app)
.get(`/api/v1/db/data/noco/${project.id}/${table.id}/find-one`)
@ -266,7 +275,7 @@ const createRow = async (
project: Project;
table: Model;
index?: number;
}
},
) => {
const columns = await table.getColumns();
const rowData = generateDefaultRowAttributes({ columns, index });
@ -289,7 +298,7 @@ const createBulkRows = async (
project: Project;
table: Model;
values: any[];
}
},
) => {
await request(context.app)
.post(`/api/v1/db/data/bulk/noco/${project.id}/${table.id}`)
@ -317,7 +326,7 @@ const createChildRow = async (
rowId?: string;
childRowId?: string;
type: string;
}
},
) => {
if (!rowId) {
const row = await createRow(context, { project, table });
@ -331,7 +340,7 @@ const createChildRow = async (
await request(context.app)
.post(
`/api/v1/db/data/noco/${project.id}/${table.id}/${rowId}/${type}/${column.title}/${childRowId}`
`/api/v1/db/data/noco/${project.id}/${table.id}/${rowId}/${type}/${column.title}/${childRowId}`,
)
.set('xc-auth', context.token);

84
packages/nocodb/tests/unit/factory/view.ts

@ -1,9 +1,20 @@
import { ViewTypes } from 'nocodb-sdk';
import request from 'supertest';
import Model from '../../../src/models/Model';
import View from '../../../src/models/View';
import type Model from '../../../src/models/Model';
const createView = async (context, {title, table, type}: {title: string, table: Model, type: ViewTypes}) => {
const createView = async (
context,
{
title,
table,
type,
}: {
title: string;
table: Model;
type: ViewTypes;
},
) => {
const viewTypeStr = (type) => {
switch (type) {
case ViewTypes.GALLERY:
@ -26,13 +37,70 @@ const createView = async (context, {title, table, type}: {title: string, table:
title,
type,
});
if(response.status !== 200) {
throw new Error('createView',response.body.message);
if (response.status !== 200) {
throw new Error('createView', response.body.message);
}
const view = await View.getByTitleOrId({fk_model_id: table.id, titleOrId:title}) as View;
const view = (await View.getByTitleOrId({
fk_model_id: table.id,
titleOrId: title,
})) as View;
return view;
};
return view
}
const updateView = async (
context,
{
table,
view,
filter = [],
sort = [],
field = [],
}: {
table: Model;
view: View;
filter?: any[];
sort?: any[];
field?: any[];
},
) => {
if (filter.length) {
for (let i = 0; i < filter.length; i++) {
await request(context.app)
.post(`/api/v1/db/meta/views/${view.id}/filters`)
.set('xc-auth', context.token)
.send(filter[i])
.expect(200);
}
}
if (sort.length) {
for (let i = 0; i < sort.length; i++) {
await request(context.app)
.post(`/api/v1/db/meta/views/${view.id}/sorts`)
.set('xc-auth', context.token)
.send(sort[i])
.expect(200);
}
}
if (field.length) {
for (let i = 0; i < field.length; i++) {
const columns = await table.getColumns();
const viewColumns = await view.getColumns();
const columnId = columns.find((c) => c.title === field[i]).id;
const viewColumnId = viewColumns.find(
(c) => c.fk_column_id === columnId,
).id;
// configure view to hide selected fields
await request(context.app)
.patch(`/api/v1/db/meta/views/${view.id}/columns/${viewColumnId}`)
.set('xc-auth', context.token)
.send({ show: false })
.expect(200);
}
}
};
export {createView}
export { createView, updateView };

7
packages/nocodb/tests/unit/init/index.ts

@ -25,7 +25,7 @@ const serverInit = async () => {
const isFirstTimeRun = () => !server;
export default async function () {
export default async function (isSakila = true) {
const { default: TestDbMngr } = await import('../TestDbMngr');
if (isFirstTimeRun()) {
@ -33,7 +33,10 @@ export default async function () {
server = await serverInit();
}
await cleanUpSakila();
if (isSakila) {
await cleanUpSakila();
}
await cleanupMeta();
const { token } = await createUser({ app: server }, { roles: 'editor' });

2
packages/nocodb/tests/unit/rest/index.test.ts

@ -8,6 +8,7 @@ import tableRowTests from './tests/tableRow.test';
import viewRowTests from './tests/viewRow.test';
import attachmentTests from './tests/attachment.test';
import filterTest from './tests/filter.test';
import newDataApisTest from './tests/newDataApis.test';
function restTests() {
authTests();
@ -19,6 +20,7 @@ function restTests() {
columnTypeSpecificTests();
attachmentTests();
filterTest();
newDataApisTest();
}
export default function () {

2867
packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts

File diff suppressed because it is too large Load Diff

13
tests/playwright/tests/db/columns/columnQrCode.spec.ts

@ -43,20 +43,17 @@ test.describe('Virtual Columns', () => {
{
referencedValue: 'A Corua (La Corua)',
base64EncodedSrc:
'data:image/png;base64,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',
// 'data:image/png;base64,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',
'data:image/png;base64,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',
},
{
referencedValue: 'Abha',
base64EncodedSrc:
// 'data:image/png;base64,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',
'data:image/png;base64,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',
'data:image/png;base64,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',
},
{
referencedValue: 'Abu Dhabi',
base64EncodedSrc:
// 'data:image/png;base64,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',
'data:image/png;base64,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',
'data:image/png;base64,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',
},
];
@ -87,7 +84,7 @@ test.describe('Virtual Columns', () => {
{
referencedValue: 'Hamburg',
base64EncodedSrc:
'data:image/png;base64,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',
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAYAAAA8AXHiAAAAAXNSR0IArs4c6QAAB0ZJREFUeF7tndtyIyEMBe3//+hsOXFcSW2AoTkw2O593RGXQyMJMZ5cPz4+Pi7+U4GwAlfBCitqc58KCJYgTFFAsKbIaqOCJQNTFBCsKbLaqGDJwBQFqmBdr9cpnc5uNFlBSWtQGlu6n9kaf7dfnE+t3PBqkyVipzUQrFstQo8V10CwBOur0BfeXII1QVQSioiNORZRjdmYYzHd9FgN3QRLsKACdbM4WMlwQ2Zcy32SeUxtnqUxEG3IfIhuxIaMDdexiHhkUiUbNFmQiAtW/QCjx4KHEcESrIdzMxQmY4NgCVaWp0drKO2gVzrmWOWdTLQhizeJo/+aJWOLJ+87VKqTC7tzjrWz1oJ135+kdEBszj7l1rwcyU2XnQp33kU1UQkkxEawGsdzQjfJCUg/hkKi9OWS1NpQaCgcOk0bChubmIQ1YmMoNBR+MuCpcJMC6asl7yRbSeZ4Nbh31tocq5FjCZYeq5mEknKDYAmWYJFd8MPGcgM8WOix6uQJlmB9EmLyPuCio7so/AbpqpoU0YBITvqxQAp3+A6lA7LggtVQgIhKbErDECxPhc1TIclJBEuwBIvEP8sNv1VDr8uavFfRIymEybvJe9OfbQ1Wc/ShB4jHCnXdbIaMjdg0BxJ6gIwtfgkdmkuzGTLZZqOhB8jYiE1ouM1myNgEqylr/wNoISr5HzmZ9o+6bIHm806/K0yKXWsLLYRgfUn6jLtIsJgCaKPosZjYeqy6g8E5Vn45ci1Gj80gRKEdDmpsOcV4S/E6Fh/KfEvBmq/xdw+CBd/hSnsfAv06TPp7EizB6qfmgIVgCdYBTPofESzB6qfmgIVgCdYBTPofQWD1d/O8FuQlQDLbswvLZMzExr9XeFdNsAg+ZRvBEqwsUd961q50pvS4aaN6rOzC6LH0WFmi9Fi/9dRjZfnCl9DkauJsm6x02e+8k7GR66ZaP9EvFKZfmyGTXWVDFm/VQpCxEd1WzUePRVa0kZetqlUJ1n0hDIUDFP9hKliClSXqwEmWeE1zLHjvl17d5EKQsemx9FiEm6bNS4JVmnXSBd/6SLZXa2tn70O0JvMhtTz0dkOyI3LMFay600pvlOR643ID2UUlmx1cOtnhzVjV8UByUW/dkvkkxyBYT1CTIptYsCac8JKikhyvw1E9Hk16Cz3WRvUYAiMBiKQDeqwJfy2L7OTkgtfaSt4kkDHvoM0Wp0LiFXYQr9djpA8jSS9HAEabi77d0Cv2yrifFq93roJ1uSw9FeqxWMFXj2WO9cmAHkuPNRQ1Td7LHthQOICWYEGwBjT/0zSZY5HC5aoQRXQjYyM2tbGR9Snmfyt/V0gGTmxIsktAJQAlxyZYjQp7+pY+uXhJeIi3uNmsCrnRTazH+lpuPRZ7I8JQuLAMQLwcCWvEhnhNsumW/sSeuFpiYyhkHjiqNQ2FyTs8kmOh+yvwae0dvA8ZA/EypJ94KBSs8jKQEJXUc4ucUY+VT94Fq3GlQxI94k4NhfX7xbSmpL1eG5y8J123YAnWA1zBMseqRjRzLHOs3jB35HkcCo80vtszSS9Lyh03G1IrIuMm10ClOZHShWBNoJ/kjMSGwJ2EdEoonLAe05skopJBEUiIjWCR1ZlgI1js9GkobMAoWII1wV8xUclASFgjNk8bClftcLJ4RNRnvUlI65Nsb9kvoZODpm2hnCD4x77T3ofqsMJOsBbmX4I14XeFK3ZJqw89Vkuh3P/rsfRYOZp+tCRYgiVYowoYCkcVPG4f91hk8Y4Pt/0keUuz3WrfE8lyTDrhT64P0Rp/uyE58L7l/HqaTJb086y1r+T6EK0Fa4A2PRb8KAghdWCdukx3GJtgCVYXtEcfFizBOspK13OCtRCspNi3VSav2K6yKVG46oSXTgfI2i0rN5DB1dzEKkhIP4Klx8IlCrJR9FgDl9DJHa7Hqqd2RGtSx0puongdiwxOsATroQDZRV1HrvvDpJ9VNuZY5ljmWAd2NYk2ngobwpJfKB9Yq8OPkNJBEoTa/SvK1+gfaSLh5rDKPx4k/SAhCu+8k7bIPAVrIPchggsWKxIn8z+yud7qVFgD21DIPlhS0lSw7soIlmDhE54eq/6Z7uTmeiuPlU6Qz87/SGGZ5LlIt3c6FSKBwLfhST/EmwoW2SYDp8+kJyGQEBvBGlhwwtczQiJYvt0wdPeZhF6PpceaAqNgCZZgHblyS58KSR5FbFblMeQ6Y4f5JMdANIjXsciEiI1gEdXqNlsXSPPT/btFwcorLVgTvt2QFJUseXqjJMdgKGwcLMjJi4iaXNRbW2ePgfRvjnWnQI+1ydsNZFeuskkWLkmIStskdVv2m0dabkhONt2WYJUVFawB2gRLsAbw6RcvHaJWAZwUSY81oOaqBV/Vz4AU/5kK1oCaqxZ8VT8DUgjWCvEMheyd99raoF9CJxfbtt5Lgbf6m9DvtbTnzlawztX/ZXsXrJdd2nMnJljn6v+yvQvWyy7tuRP7B8uMeuRlJZWqAAAAAElFTkSuQmCC',
},
...expectedQrCodeCellValues.slice(1),
];
@ -115,7 +112,7 @@ test.describe('Virtual Columns', () => {
{
referencedValue: 'Hamburg',
base64EncodedSrc:
'data:image/png;base64,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',
'data:image/png;base64,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',
},
]);

Loading…
Cancel
Save