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. 2
      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 // and it has been changed to XcType.Checkbox, since 0.0.2
// hence, change the text value to boolean here // hence, change the text value to boolean here
if ('secure' in parsedInput && typeof parsedInput.secure === 'string') { if ('secure' in parsedInput && typeof parsedInput.secure === 'string') {
parsedInput.secure = !!parsedInput.secure parsedInput.secure = parsedInput.secure === 'true'
} }
plugin = { ...res, formDetails, parsedInput } plugin = { ...res, formDetails, parsedInput }

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

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

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

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

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

@ -64,6 +64,7 @@ const syncSource = ref({
syncLookup: true, syncLookup: true,
syncFormula: false, syncFormula: false,
syncAttachment: true, syncAttachment: true,
syncUsers: true,
}, },
}, },
}) })
@ -174,6 +175,7 @@ async function loadSyncSrc() {
syncLookup: true, syncLookup: true,
syncFormula: false, syncFormula: false,
syncAttachment: true, syncAttachment: true,
syncUsers: true,
}, },
}, },
} }
@ -334,6 +336,13 @@ onMounted(async () => {
</a-checkbox> </a-checkbox>
</div> </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 --> <!-- Import Formula Columns -->
<a-tooltip placement="top"> <a-tooltip placement="top">
<template #title> <template #title>

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

@ -10,7 +10,7 @@ const { project } = storeToRefs(useProject())
const route = useRoute() const route = useRoute()
const openSwaggerLink = () => { 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> </script>

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

@ -730,7 +730,7 @@ watch(view, (nextView) => {
<LazySmartsheetDivDataCell class="relative"> <LazySmartsheetDivDataCell class="relative">
<LazySmartsheetCell <LazySmartsheetCell
v-model="formState[element.title]" v-model="formState[element.title]"
class="nc-input" class="nc-input truncate"
:class="`nc-form-input-${element.title.replaceAll(' ', '')}`" :class="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:data-testid="`nc-form-input-${element.title.replaceAll(' ', '')}`" :data-testid="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:column="element" :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 { ChangePageInj, PaginationDataInj, computed, iconMap, inject, isRtlLang, useI18n } from '#imports'
import type { Language } from '~/lib' import type { Language } from '~/lib'
const props = defineProps<{ interface Props {
alignCountOnRight?: boolean alignCountOnRight?: boolean
}>() }
const { alignCountOnRight } = defineProps<Props>()
const { locale } = useI18n() const { locale } = useI18n()

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

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

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

@ -1,8 +1,8 @@
<script lang="ts" setup> <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 }>() const { column } = defineProps<{ column: ColumnType }>()
</script> </script>
<template> <template>

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

@ -10,7 +10,7 @@ const { metas } = $(useMetas())
const { tables } = useTable() const { tables } = useTable()
const localTables = computed( 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> </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 // TODO: add 'json' when it's ready
const quickImportDialogTypes: QuickImportDialogType[] = ['csv', 'excel'] 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: any, curr) => {
acc[curr] = ref(false) acc[curr] = ref(false)
return acc return acc

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

@ -72,7 +72,7 @@ const onDrop = async (event: DragEvent) => {
event.preventDefault() event.preventDefault()
try { try {
// Access the dropped data // 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 // Do something with the received data
// if dragged item is not from the same base, return // 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 = { const qrCodeOptions: QRCode.QRCodeToDataURLOptions = {
errorCorrectionLevel: 'M', errorCorrectionLevel: 'M',
margin: 1, margin: 1,
version: 4,
rendererOpts: { rendererOpts: {
quality: 1, quality: 1,
}, },
@ -55,11 +54,13 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning } = us
@ok="handleModalOkClick" @ok="handleModalOkClick"
> >
<template #footer> <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> </template>
<img v-if="showQrCode" :src="qrCodeLarge" alt="QR Code" /> <img v-if="showQrCode" :src="qrCodeLarge" alt="QR Code" />
</a-modal> </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') }} {{ $t('labels.qrCodeValueTooLong') }}
</div> </div>
<img <img

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

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

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

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

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

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

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

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

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

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

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

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

2
packages/nc-gui/package.json

@ -54,7 +54,7 @@
"leaflet.markercluster": "^1.5.3", "leaflet.markercluster": "^1.5.3",
"locale-codes": "^1.3.1", "locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"nocodb-sdk": "0.109.6", "nocodb-sdk": "file:../nocodb-sdk",
"papaparse": "^5.3.2", "papaparse": "^5.3.2",
"pinia": "^2.0.33", "pinia": "^2.0.33",
"qrcode": "^1.5.1", "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-if="isUIAllowed('apiDocs') && !isMobileMode"
v-e="['e:api-docs']" v-e="['e:api-docs']"
class="nc-project-menu-item group" 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" /> <component :is="iconMap.json" class="group-hover:text-accent" />
{{ $t('activity.account.swagger') }} {{ $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 { textarea {
@apply px-4 py-2 rounded; @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 <LazySmartsheetCell
v-else v-else
v-model="formState[field.title]" v-model="formState[field.title]"
class="nc-input" class="nc-input truncate"
:data-testid="`nc-form-input-cell-${field.label || field.title}`" :data-testid="`nc-form-input-cell-${field.label || field.title}`"
:class="`nc-form-input-${field.title?.replaceAll(' ', '')}`" :class="`nc-form-input-${field.title?.replaceAll(' ', '')}`"
:column="field" :column="field"

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

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

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

@ -16,10 +16,13 @@ export default defineNuxtPlugin(async (nuxtApp) => {
try { try {
if (socket) socket.disconnect() 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 }, extraHeaders: { 'xc-auth': token },
path: socketPath,
}) })
socket.on('connect_error', () => { 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) { for (const base of bases.value) {
if (base.id) { if (base.id) {
temp[base.id] = SqlUiFactory.create({ client: base.type }) as Exclude< temp[base.id] = SqlUiFactory.create({ client: base.type }) as Exclude<
ReturnType<typeof SqlUiFactory['create']>, ReturnType<(typeof SqlUiFactory)['create']>,
typeof OracleUi typeof OracleUi
> >
} }

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

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

30
packages/nocodb/package-lock.json generated

@ -83,7 +83,7 @@
"nc-lib-gui": "0.109.6", "nc-lib-gui": "0.109.6",
"nc-plugin": "^0.1.3", "nc-plugin": "^0.1.3",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nocodb-sdk": "0.109.6", "nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10", "nodemailer": "^6.4.10",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"object-sizeof": "^2.6.1", "object-sizeof": "^2.6.1",
@ -192,7 +192,6 @@
}, },
"../nocodb-sdk": { "../nocodb-sdk": {
"version": "0.109.6", "version": "0.109.6",
"extraneous": true,
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
@ -13208,13 +13207,8 @@
} }
}, },
"node_modules/nocodb-sdk": { "node_modules/nocodb-sdk": {
"version": "0.109.6", "resolved": "../nocodb-sdk",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.109.6.tgz", "link": true
"integrity": "sha512-Zh4MjkurCPYl/eJWt9ChvoikMpDujI/F6IaQYb9bJBss6Ns4grdhKizCdvywb2dRYaGqXquzwrGg2jmpw/Xyxg==",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
}
}, },
"node_modules/node-abort-controller": { "node_modules/node-abort-controller": {
"version": "3.1.1", "version": "3.1.1",
@ -28517,12 +28511,22 @@
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==" "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="
}, },
"nocodb-sdk": { "nocodb-sdk": {
"version": "0.109.6", "version": "file:../nocodb-sdk",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.109.6.tgz",
"integrity": "sha512-Zh4MjkurCPYl/eJWt9ChvoikMpDujI/F6IaQYb9bJBss6Ns4grdhKizCdvywb2dRYaGqXquzwrGg2jmpw/Xyxg==",
"requires": { "requires": {
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"axios": "^0.21.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": { "node-abort-controller": {

2
packages/nocodb/package.json

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

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

@ -29,7 +29,6 @@ import type { MiddlewareConsumer } from '@nestjs/common';
JobsModule, JobsModule,
NestJsEventEmitter.forRoot(), NestJsEventEmitter.forRoot(),
], ],
controllers: [],
providers: [ providers: [
AuthService, 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 axios from 'axios';
import Project from '../../../models/Project'; import Project from '../../../models/Project';
import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; // import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2';
import Noco from '../../../Noco'; import Noco from '../../../Noco';
import User from '../../../models/User'; import User from '../../../models/User';
import NocoCache from '../../../cache/NocoCache'; 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 DOMPurify from 'isomorphic-dompurify';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { extractLimitAndOffset } from '../helpers';
import { NcError } from '../helpers/catchError'; import { NcError } from '../helpers/catchError';
import getAst from '../helpers/getAst'; import getAst from '../helpers/getAst';
import { import {
@ -43,6 +44,7 @@ import genRollupSelectv2 from './genRollupSelectv2';
import conditionV2 from './conditionV2'; import conditionV2 from './conditionV2';
import sortV2 from './sortV2'; import sortV2 from './sortV2';
import { customValidators } from './util/customValidators'; import { customValidators } from './util/customValidators';
import Transaction = Knex.Transaction;
import type { XKnex } from './CustomKnex'; import type { XKnex } from './CustomKnex';
import type { import type {
XcFilter, XcFilter,
@ -58,7 +60,6 @@ import type {
SelectOption, SelectOption,
} from '../models'; } from '../models';
import type { SortType } from 'nocodb-sdk'; import type { SortType } from 'nocodb-sdk';
import Transaction = Knex.Transaction;
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
@ -1511,20 +1512,12 @@ class BaseModelSqlv2 {
} }
_getListArgs(args: XcFilterWithAlias): XcFilter { _getListArgs(args: XcFilterWithAlias): XcFilter {
const obj: XcFilter = {}; const obj: XcFilter = extractLimitAndOffset(args);
obj.where = args.where || args.w || ''; obj.where = args.filter || args.where || args.w || '';
obj.having = args.having || args.h || ''; obj.having = args.having || args.h || '';
obj.shuffle = args.shuffle || args.r || ''; obj.shuffle = args.shuffle || args.r || '';
obj.condition = args.condition || args.c || {}; obj.condition = args.condition || args.c || {};
obj.conditionGraph = args.conditionGraph || {}; 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.fields = args.fields || args.f;
obj.sort = args.sort || args.s; obj.sort = args.sort || args.s;
return obj; return obj;
@ -2244,12 +2237,14 @@ class BaseModelSqlv2 {
foreign_key_checks = true, foreign_key_checks = true,
skip_hooks = false, skip_hooks = false,
raw = false, raw = false,
insertOneByOneAsFallback = false,
}: { }: {
chunkSize?: number; chunkSize?: number;
cookie?: any; cookie?: any;
foreign_key_checks?: boolean; foreign_key_checks?: boolean;
skip_hooks?: boolean; skip_hooks?: boolean;
raw?: boolean; raw?: boolean;
insertOneByOneAsFallback?: boolean;
} = {}, } = {},
) { ) {
let trx; let trx;
@ -2403,12 +2398,28 @@ class BaseModelSqlv2 {
} }
} }
const response = let response;
this.isPg || this.isMssql
? await trx // insert one by one as fallback to get ids for sqlite and mysql
.batchInsert(this.tnPath, insertDatas, chunkSize) if (insertOneByOneAsFallback && (this.isSqlite || this.isMySQL)) {
.returning(this.model.primaryKey?.column_name) // sqlite and mysql doesnt support returning, so insert one by one and return ids
: await trx.batchInsert(this.tnPath, insertDatas, chunkSize); 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 (!foreign_key_checks) {
if (this.isPg) { if (this.isPg) {
@ -2433,7 +2444,11 @@ class BaseModelSqlv2 {
async bulkUpdate( async bulkUpdate(
datas: any[], datas: any[],
{ cookie, raw = false }: { cookie?: any; raw?: boolean } = {}, {
cookie,
raw = false,
throwExceptionIfNotExist = false,
}: { cookie?: any; raw?: boolean; throwExceptionIfNotExist?: boolean } = {},
) { ) {
let transaction; let transaction;
try { try {
@ -2476,9 +2491,12 @@ class BaseModelSqlv2 {
if (!raw) { if (!raw) {
for (const pkValues of updatePkValues) { for (const pkValues of updatePkValues) {
newData.push( const oldRecord = await this.readByPk(pkValues);
await this.readByPk(pkValues, false, {}, { ignoreView: true }), 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; let transaction;
try { try {
const deleteIds = await Promise.all( const deleteIds = await Promise.all(
@ -2570,9 +2594,14 @@ class BaseModelSqlv2 {
// pk not specified - bypass // pk not specified - bypass
continue; 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); res.push(d);
} }
@ -2635,12 +2664,6 @@ class BaseModelSqlv2 {
transaction = await this.dbDriver.transaction(); 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) { for (const d of res) {
await transaction(this.tnPath).del().where(d); await transaction(this.tnPath).del().where(d);
} }
@ -2685,8 +2708,8 @@ class BaseModelSqlv2 {
qb, qb,
this.dbDriver, this.dbDriver,
); );
const execQueries: ((trx: Transaction, qb: any) => Promise<any>)[] = []; const execQueries: ((trx: Transaction, qb: any) => Promise<any>)[] = [];
// qb.del();
for (const column of this.model.columns) { for (const column of this.model.columns) {
if (column.uidt !== UITypes.LinkToAnotherRecord) continue; if (column.uidt !== UITypes.LinkToAnotherRecord) continue;
@ -2706,18 +2729,16 @@ class BaseModelSqlv2 {
await parentTable.getColumns(); await parentTable.getColumns();
const childTn = this.getTnPath(childTable); const childTn = this.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable);
switch (colOptions.type) { switch (colOptions.type) {
case 'mm': case 'mm':
{ {
const vChildCol = await colOptions.getMMChildColumn(); const vChildCol = await colOptions.getMMChildColumn();
const vParentCol = await colOptions.getMMParentColumn();
const vTable = await colOptions.getMMModel(); const vTable = await colOptions.getMMModel();
const vTn = this.getTnPath(vTable); const vTn = this.getTnPath(vTable);
execQueries.push((trx, qb) => execQueries.push(() =>
this.dbDriver(vTn) this.dbDriver(vTn)
.where({ .where({
[vChildCol.column_name]: this.dbDriver(childTn) [vChildCol.column_name]: this.dbDriver(childTn)
@ -2781,7 +2802,6 @@ class BaseModelSqlv2 {
return count; return count;
} catch (e) { } catch (e) {
if (trx) await trx.rollback();
throw e; throw e;
} }
} }
@ -2977,104 +2997,12 @@ class BaseModelSqlv2 {
modelId: this.model.id, modelId: this.model.id,
tnPath: this.tnPath, 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) {} 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) {} protected async errorUpdate(e, data, trx, cookie) {}
// todo: handle composite primary key // 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) {} protected async errorDelete(e, id, trx, cookie) {}
async validate(columns) { async validate(columns) {
@ -3809,6 +3737,464 @@ class BaseModelSqlv2 {
} }
return data; 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( 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); table = table.onUpdate(relation.ur);
} }
if (relation.dr) { if (relation.dr) {
table = table.onDelete(relation.dr); table.onDelete(relation.dr);
} }
}) })
.toQuery()); .toQuery());
@ -2192,28 +2192,31 @@ class MssqlClient extends KnexClient {
table = table.onUpdate(args.onUpdate); table = table.onUpdate(args.onUpdate);
} }
if (args.onDelete) { if (args.onDelete) {
table = table.onDelete(args.onDelete); table.onDelete(args.onDelete);
} }
}, },
); );
const upStatement = const upQb = this.sqlClient.schema.table(
this.querySeparator() + this.getTnPath(args.childTable),
(await this.sqlClient.schema function (table) {
.table(this.getTnPath(args.childTable), function (table) { table = table
table = table .foreign(args.childColumn, foreignKeyName)
.foreign(args.childColumn, foreignKeyName) .references(args.parentColumn)
.references(args.parentColumn) .on(self.getTnPath(args.parentTable));
.on(self.getTnPath(args.parentTable));
if (args.onUpdate) { if (args.onUpdate) {
table = table.onUpdate(args.onUpdate); table = table.onUpdate(args.onUpdate);
} }
if (args.onDelete) { if (args.onDelete) {
table = table.onDelete(args.onDelete); table.onDelete(args.onDelete);
} }
}) },
.toQuery()); );
await upQb;
const upStatement = this.querySeparator() + upQb.toQuery();
this.emit(`Success : ${upStatement}`); this.emit(`Success : ${upStatement}`);
@ -2221,7 +2224,7 @@ class MssqlClient extends KnexClient {
this.querySeparator() + this.querySeparator() +
this.sqlClient.schema this.sqlClient.schema
.table(this.getTnPath(args.childTable), function (table) { .table(this.getTnPath(args.childTable), function (table) {
table = table.dropForeign(args.childColumn, foreignKeyName); table.dropForeign(args.childColumn, foreignKeyName);
}) })
.toQuery(); .toQuery();

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

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

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

@ -1970,23 +1970,24 @@ class SnowflakeClient extends KnexClient {
relationsList = relationsList.data.list; relationsList = relationsList.data.list;
for (const relation of relationsList) { for (const relation of relationsList) {
downQuery += const downQb = this.sqlClient.schema.table(
this.querySeparator() + relation.tn,
(await this.sqlClient.schema function (table) {
.table(relation.tn, function (table) { table = table
table = table .foreign(relation.cn, null)
.foreign(relation.cn, null) .references(relation.rcn)
.references(relation.rcn) .on(relation.rtn);
.on(relation.rtn);
if (relation.ur) {
if (relation.ur) { table = table.onUpdate(relation.ur);
table = table.onUpdate(relation.ur); }
} if (relation.dr) {
if (relation.dr) { table.onDelete(relation.dr);
table = table.onDelete(relation.dr); }
} },
}) );
.toQuery()); await downQb;
downQuery += this.querySeparator() + downQb.toQuery();
} }
let indexList: any = await this.indexList(args); let indexList: any = await this.indexList(args);
@ -2060,8 +2061,6 @@ class SnowflakeClient extends KnexClient {
const foreignKeyName = args.foreignKeyName || null; const foreignKeyName = args.foreignKeyName || null;
try { try {
// s = await this.sqlClient.schema.index(Object.keys(args.columns));
await this.sqlClient.schema.table(args.childTable, (table) => { await this.sqlClient.schema.table(args.childTable, (table) => {
table = table table = table
.foreign(args.childColumn, foreignKeyName) .foreign(args.childColumn, foreignKeyName)
@ -2072,27 +2071,27 @@ class SnowflakeClient extends KnexClient {
table = table.onUpdate(args.onUpdate); table = table.onUpdate(args.onUpdate);
} }
if (args.onDelete) { if (args.onDelete) {
table = table.onDelete(args.onDelete); table.onDelete(args.onDelete);
} }
}); });
const upStatement = const upQb = this.sqlClient.schema.table(args.childTable, (table) => {
this.querySeparator() + table = table
(await this.sqlClient.schema .foreign(args.childColumn, foreignKeyName)
.table(args.childTable, (table) => { .references(args.parentColumn)
table = table .on(this.getTnPath(args.parentTable));
.foreign(args.childColumn, foreignKeyName)
.references(args.parentColumn)
.on(this.getTnPath(args.parentTable));
if (args.onUpdate) { if (args.onUpdate) {
table = table.onUpdate(args.onUpdate); table = table.onUpdate(args.onUpdate);
} }
if (args.onDelete) { if (args.onDelete) {
table = table.onDelete(args.onDelete); table.onDelete(args.onDelete);
} }
}) });
.toQuery());
await upQb;
const upStatement = this.querySeparator() + upQb.toQuery();
this.emit(`Success : ${upStatement}`); this.emit(`Success : ${upStatement}`);
@ -2100,7 +2099,7 @@ class SnowflakeClient extends KnexClient {
this.querySeparator() + this.querySeparator() +
this.sqlClient.schema this.sqlClient.schema
.table(args.childTable, (table) => { .table(args.childTable, (table) => {
table = table.dropForeign(args.childColumn, foreignKeyName); table.dropForeign(args.childColumn, foreignKeyName);
}) })
.toQuery(); .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 */ /* Filter relations for current table */
if (args.tn) { if (args.tn) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
relations = relations.filter( relations = relations.filter(
(r) => r.tn === args.tn || r.rtn === args.tn, (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 * @returns {Object} Table row data
*/ */
// @ts-ignore // @ts-ignore
async readByPk(id, { conditionGraph }) { async readByPk(id) {
try { try {
return await this._run( return await this._run(
this.$db.select().where(this._wherePk(id)).first(), this.$db.select().where(this._wherePk(id)).first(),
@ -704,10 +704,7 @@ abstract class BaseModel {
*/ */
async exists(id, _) { async exists(id, _) {
try { try {
return ( return Object.keys(await this.readByPk(id)).length !== 0;
Object.keys(await this.readByPk(id, { conditionGraph: null }))
.length !== 0
);
} catch (e) { } catch (e) {
console.log(e); console.log(e);
throw e; throw e;
@ -1341,7 +1338,7 @@ abstract class BaseModel {
* @param {Object} data - insert data * @param {Object} data - insert data
* @param {Object} trx? - knex transaction reference * @param {Object} trx? - knex transaction reference
*/ */
// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars
async beforeInsert(data, trx?: any, cookie?: {}) {} async beforeInsert(data, trx?: any, cookie?: {}) {}
/** /**
@ -1350,7 +1347,7 @@ abstract class BaseModel {
* @param {Object} response - inserted data * @param {Object} response - inserted data
* @param {Object} trx? - knex transaction reference * @param {Object} trx? - knex transaction reference
*/ */
// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars
async afterInsert(response, trx?: any, cookie?: {}) {} async afterInsert(response, trx?: any, cookie?: {}) {}
/** /**
@ -1360,7 +1357,7 @@ abstract class BaseModel {
* @param {Object} data - insert data * @param {Object} data - insert data
* @param {Object} trx? - knex transaction reference * @param {Object} trx? - knex transaction reference
*/ */
// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars
async errorInsert(err, data, trx?: any, cookie?: {}) {} async errorInsert(err, data, trx?: any, cookie?: {}) {}
/** /**
@ -1369,7 +1366,7 @@ abstract class BaseModel {
* @param {Object} data - update data * @param {Object} data - update data
* @param {Object} trx? - knex transaction reference * @param {Object} trx? - knex transaction reference
*/ */
// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars
async beforeUpdate(data, trx?: any, cookie?: {}) {} async beforeUpdate(data, trx?: any, cookie?: {}) {}
/** /**
@ -1378,7 +1375,7 @@ abstract class BaseModel {
* @param {Object} response - updated data * @param {Object} response - updated data
* @param {Object} trx? - knex transaction reference * @param {Object} trx? - knex transaction reference
*/ */
// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars
async afterUpdate(response, trx?: any, cookie?: {}) {} async afterUpdate(response, trx?: any, cookie?: {}) {}
/** /**
@ -1388,7 +1385,7 @@ abstract class BaseModel {
* @param {Object} data - update data * @param {Object} data - update data
* @param {Object} trx? - knex transaction reference * @param {Object} trx? - knex transaction reference
*/ */
// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars
async errorUpdate(err, data, trx?: any, cookie?: {}) {} async errorUpdate(err, data, trx?: any, cookie?: {}) {}
/** /**
@ -1397,7 +1394,7 @@ abstract class BaseModel {
* @param {Object} data - delete data * @param {Object} data - delete data
* @param {Object} trx? - knex transaction reference * @param {Object} trx? - knex transaction reference
*/ */
// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars
async beforeDelete(data, trx?: any, cookie?: {}) {} async beforeDelete(data, trx?: any, cookie?: {}) {}
/** /**
@ -1406,7 +1403,7 @@ abstract class BaseModel {
* @param {Object} response - Deleted data * @param {Object} response - Deleted data
* @param {Object} trx? - knex transaction reference * @param {Object} trx? - knex transaction reference
*/ */
// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars
async afterDelete(response, trx?: any, cookie?: {}) {} async afterDelete(response, trx?: any, cookie?: {}) {}
/** /**
@ -1416,7 +1413,7 @@ abstract class BaseModel {
* @param {Object} data - delete data * @param {Object} data - delete data
* @param {Object} trx? - knex transaction reference * @param {Object} trx? - knex transaction reference
*/ */
// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars
async errorDelete(err, data, trx?: any, cookie?: {}) {} async errorDelete(err, data, trx?: any, cookie?: {}) {}
/** /**
@ -1425,7 +1422,7 @@ abstract class BaseModel {
* @param {Object[]} data - insert data * @param {Object[]} data - insert data
* @param {Object} trx - knex transaction reference * @param {Object} trx - knex transaction reference
*/ */
// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars
async beforeInsertb(data, trx?: any) {} async beforeInsertb(data, trx?: any) {}
/** /**
@ -1434,7 +1431,7 @@ abstract class BaseModel {
* @param {Object[]} response - inserted data * @param {Object[]} response - inserted data
* @param {Object} trx - knex transaction reference * @param {Object} trx - knex transaction reference
*/ */
// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars
async afterInsertb(response, trx?: any) {} async afterInsertb(response, trx?: any) {}
/** /**
@ -1444,7 +1441,7 @@ abstract class BaseModel {
* @param {Object} data - delete data * @param {Object} data - delete data
* @param {Object} trx - knex transaction reference * @param {Object} trx - knex transaction reference
*/ */
// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars
async errorInsertb(err, data, trx?: any) {} async errorInsertb(err, data, trx?: any) {}
/** /**
@ -1453,7 +1450,7 @@ abstract class BaseModel {
* @param {Object[]} data - update data * @param {Object[]} data - update data
* @param {Object} trx - knex transaction reference * @param {Object} trx - knex transaction reference
*/ */
// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars
async beforeUpdateb(data, trx?: any) {} async beforeUpdateb(data, trx?: any) {}
/** /**
@ -1462,7 +1459,7 @@ abstract class BaseModel {
* @param {Object[]} response - updated data * @param {Object[]} response - updated data
* @param {Object} trx - knex transaction reference * @param {Object} trx - knex transaction reference
*/ */
// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars
async afterUpdateb(response, trx?: any) {} async afterUpdateb(response, trx?: any) {}
/** /**
@ -1472,7 +1469,7 @@ abstract class BaseModel {
* @param {Object[]} data - delete data * @param {Object[]} data - delete data
* @param {Object} trx - knex transaction reference * @param {Object} trx - knex transaction reference
*/ */
// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars
async errorUpdateb(err, data, trx?: any) {} async errorUpdateb(err, data, trx?: any) {}
/** /**
@ -1481,7 +1478,7 @@ abstract class BaseModel {
* @param {Object[]} data - delete data * @param {Object[]} data - delete data
* @param {Object} trx - knex transaction reference * @param {Object} trx - knex transaction reference
*/ */
// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars
async beforeDeleteb(data, trx?: any) {} async beforeDeleteb(data, trx?: any) {}
/** /**
@ -1490,7 +1487,7 @@ abstract class BaseModel {
* @param {Object[]} response - deleted data * @param {Object[]} response - deleted data
* @param {Object} trx - knex transaction reference * @param {Object} trx - knex transaction reference
*/ */
// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars
async afterDeleteb(response, trx?: any) {} async afterDeleteb(response, trx?: any) {}
/** /**
@ -1500,12 +1497,13 @@ abstract class BaseModel {
* @param {Object[]} data - delete data * @param {Object[]} data - delete data
* @param {Object} trx - knex transaction reference * @param {Object} trx - knex transaction reference
*/ */
// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars
async errorDeleteb(err, data, trx?: any) {} async errorDeleteb(err, data, trx?: any) {}
} }
export interface XcFilter { export interface XcFilter {
where?: string; where?: string;
filter?: string;
having?: string; having?: string;
condition?: any; condition?: any;
conditionGraph?: any; conditionGraph?: any;

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

@ -1,7 +1,7 @@
import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2';
import KnexMigratorv2Tans from '../../sql-migrator/lib/KnexMigratorv2Tans'; import KnexMigratorv2Tans from '../../sql-migrator/lib/KnexMigratorv2Tans';
import Base from '../../../models/Base';
import SqlMgrv2 from './SqlMgrv2'; import SqlMgrv2 from './SqlMgrv2';
import type Base from '../../../models/Base';
import type { Knex } from 'knex'; import type { Knex } from 'knex';
import type { XKnex } from '../../CustomKnex'; 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 // @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const dirStat = await promisify(fs.stat)( const dirStat = await promisify(fs.stat)(
path.join( path.join(
this.toolDir, this.toolDir,
@ -232,6 +233,7 @@ export default class KnexMigrator extends SqlMigrator {
); );
// @ts-ignore // @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const metaStat = await promisify(fs.stat)( const metaStat = await promisify(fs.stat)(
path.join( path.join(
this.toolDir, this.toolDir,

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

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

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

@ -8,6 +8,7 @@ import {
NotFound, NotFound,
NotImplemented, NotImplemented,
Unauthorized, Unauthorized,
UnprocessableEntity,
} from '../../helpers/catchError'; } from '../../helpers/catchError';
import type { ArgumentsHost, ExceptionFilter } from '@nestjs/common'; import type { ArgumentsHost, ExceptionFilter } from '@nestjs/common';
import type { Response } from 'express'; import type { Response } from 'express';
@ -15,6 +16,7 @@ import type { Response } from 'express';
@Catch() @Catch()
export class GlobalExceptionFilter implements ExceptionFilter { export class GlobalExceptionFilter implements ExceptionFilter {
private logger = new Logger(GlobalExceptionFilter.name); private logger = new Logger(GlobalExceptionFilter.name);
catch(exception: any, host: ArgumentsHost) { catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp(); const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>(); const response = ctx.getResponse<Response>();
@ -58,6 +60,8 @@ export class GlobalExceptionFilter implements ExceptionFilter {
return response return response
.status(400) .status(400)
.json({ msg: exception.message, errors: exception.errors }); .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 // 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'); 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({ @WebSocketGateway({
cors: { cors: {
origin: '*', origin: '*',
allowedHeaders: ['xc-auth'], allowedHeaders: ['xc-auth'],
credentials: true, credentials: true,
}, },
namespace,
}) })
@Injectable() @Injectable()
export class SocketGateway implements OnModuleInit { 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'; 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> { export class PagedResponseImpl<T> {
constructor( constructor(
list: T[], list: T[],
@ -17,12 +12,7 @@ export class PagedResponseImpl<T> {
o?: number; o?: number;
} = {}, } = {},
) { ) {
const limit = Math.max( const { offset, limit } = extractLimitAndOffset(args);
Math.min(args.limit || args.l || config.limitDefault, config.limitMax),
config.limitMin,
);
const offset = Math.max(+(args.offset || args.o) || 0, 0);
let count = args.count ?? null; let count = args.count ?? null;
@ -40,8 +30,17 @@ export class PagedResponseImpl<T> {
this.pageInfo.page === this.pageInfo.page ===
(Math.ceil(this.pageInfo.totalRows / this.pageInfo.pageSize) || 1); (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>; list: Array<T>;
pageInfo: PaginatedType; pageInfo: PaginatedType;
errors?: any[];
} }

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

@ -3,7 +3,7 @@ import type { Request } from 'express';
const countMap = {}; const countMap = {};
// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars
const metrics = async (req: Request, c = 150) => { const metrics = async (req: Request, c = 150) => {
if (!req?.route?.path) return; if (!req?.route?.path) return;
const event = `a:api:${req.route.path}:${req.method}`; 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 }); return res.status(501).json({ msg: e.message });
} else if (e instanceof AjvError) { } else if (e instanceof AjvError) {
return res.status(400).json({ msg: e.message, errors: e.errors }); 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); next(e);
} }
@ -431,6 +433,8 @@ export class InternalServerError extends Error {}
export class NotImplemented extends Error {} export class NotImplemented extends Error {}
export class UnprocessableEntity extends Error {}
export class AjvError extends Error { export class AjvError extends Error {
constructor(param: { message: string; errors: ErrorObject[] }) { constructor(param: { message: string; errors: ErrorObject[] }) {
super(param.message); super(param.message);
@ -468,4 +472,8 @@ export class NcError {
static ajvValidationError(param: { message: string; errors: ErrorObject[] }) { static ajvValidationError(param: { message: string; errors: ErrorObject[] }) {
throw new AjvError(param); 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 './columnHelpers';
export * from './apiHelpers'; export * from './apiHelpers';
export * from './cacheHelpers'; export * from './cacheHelpers';
export * from './extractLimitAndOffset';
export { populateMeta }; 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 {} 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' }); await knex('nc_plugins').del().where({ title: 'SES' });
}; };
const down = async (knex: Knex) => { const down = async (_: Knex) => {
// await knex('nc_plugins').insert([ses]); // 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 // @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async getViews(force = false, ncMeta = Noco.ncMeta): Promise<View[]> { public async getViews(force = false, ncMeta = Noco.ncMeta): Promise<View[]> {
this.views = await View.listWithInfo(this.id, ncMeta); this.views = await View.listWithInfo(this.id, ncMeta);
return this.views; 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 { isSystemColumn, UITypes, ViewTypes } from 'nocodb-sdk';
import Noco from '../Noco'; import Noco from '../Noco';
import { 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 multer from 'multer';
import { NC_ATTACHMENT_FIELD_SIZE } from '../../constants'; import { NC_ATTACHMENT_FIELD_SIZE } from '../../constants';
import { DataAliasController } from '../../controllers/data-alias.controller'; import { DataAliasController } from '../../controllers/data-alias.controller';
import { DataTableController } from '../../controllers/data-table.controller';
import { PublicDatasExportController } from '../../controllers/public-datas-export.controller'; import { PublicDatasExportController } from '../../controllers/public-datas-export.controller';
import { PublicDatasController } from '../../controllers/public-datas.controller'; import { PublicDatasController } from '../../controllers/public-datas.controller';
import { DataTableService } from '../../services/data-table.service';
import { DatasService } from '../../services/datas.service'; import { DatasService } from '../../services/datas.service';
import { DatasController } from '../../controllers/datas.controller'; import { DatasController } from '../../controllers/datas.controller';
import { BulkDataAliasController } from '../../controllers/bulk-data-alias.controller'; import { BulkDataAliasController } from '../../controllers/bulk-data-alias.controller';
@ -29,6 +31,7 @@ import { PublicDatasService } from '../../services/public-datas.service';
controllers: [ controllers: [
...(process.env.NC_WORKER_CONTAINER !== 'true' ...(process.env.NC_WORKER_CONTAINER !== 'true'
? [ ? [
DataTableController,
DatasController, DatasController,
BulkDataAliasController, BulkDataAliasController,
DataAliasController, DataAliasController,
@ -41,6 +44,7 @@ import { PublicDatasService } from '../../services/public-datas.service';
: []), : []),
], ],
providers: [ providers: [
DataTableService,
DatasService, DatasService,
BulkDataAliasService, BulkDataAliasService,
DataAliasNestedService, 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'; import type { IEventEmitter } from './event-emitter.interface';
export class NestjsEventEmitter implements IEventEmitter { 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 { OnModuleInit } from '@nestjs/common';
import type { JobStatus } from '../../interface/Jobs'; 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({ @WebSocketGateway({
cors: { cors: {
origin: '*', origin: '*',
allowedHeaders: ['xc-auth'], allowedHeaders: ['xc-auth'],
credentials: true, credentials: true,
}, },
namespace: 'jobs', namespace,
}) })
export class JobsGateway implements OnModuleInit { export class JobsGateway implements OnModuleInit {
constructor(@Inject('JobsService') private readonly jobsService) {} 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') @Post('/api/v1/db/meta/syncs/:syncId/abort')
@HttpCode(200) @HttpCode(200)
async abortImport(@Request() req) { async abortImport(@Request() _) {
return {}; 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 // retrieve nc column schema from using aTbl field ID as reference
// //
const nc_getColumnSchema = async (aTblFieldId) => { const nc_getColumnSchema = async (aTblFieldId) => {
@ -1563,45 +1537,6 @@ export class AtImportProcessor {
return rec; 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) => { const nc_isLinkExists = (airtableFieldId) => {
return !!ncLinkMappingTable.find( return !!ncLinkMappingTable.find(
(x) => x.aTbl.typeOptions.symmetricColumnId === airtableFieldId, (x) => x.aTbl.typeOptions.symmetricColumnId === airtableFieldId,
@ -1879,9 +1814,7 @@ export class AtImportProcessor {
req: { user: syncDB.user, clientIp: '' }, req: { user: syncDB.user, clientIp: '' },
}) })
.catch((e) => .catch((e) =>
e.response?.data?.msg e.message ? logBasic(`NOTICE: ${e.message}`) : console.log(e),
? logBasic(`NOTICE: ${e.response.data.msg}`)
: console.log(e),
), ),
); );
recordPerfStats(_perfStart, 'auth.projectUserAdd'); recordPerfStats(_perfStart, 'auth.projectUserAdd');
@ -2294,7 +2227,6 @@ export class AtImportProcessor {
}; };
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
let recordCnt = 0;
try { try {
logBasic('SDK initialized'); logBasic('SDK initialized');
logDetailed('Project initialization started'); logDetailed('Project initialization started');
@ -2357,10 +2289,12 @@ export class AtImportProcessor {
await nocoSetPrimary(aTblSchema); await nocoSetPrimary(aTblSchema);
logDetailed('Configuring Display Value column completed'); logDetailed('Configuring Display Value column completed');
logBasic('Configuring User(s)'); if (syncDB.options.syncUsers) {
// add users logBasic('Configuring User(s)');
await nocoAddUsers(schema); // add users
logDetailed('Adding users completed'); await nocoAddUsers(schema);
logDetailed('Adding users completed');
}
// hide-fields // hide-fields
// await nocoReconfigureFields(aTblSchema); // await nocoReconfigureFields(aTblSchema);
@ -2374,7 +2308,6 @@ export class AtImportProcessor {
if (syncDB.options.syncData) { if (syncDB.options.syncData) {
try { try {
// await nc_DumpTableSchema();
const _perfStart = recordPerfStart(); const _perfStart = recordPerfStart();
const ncTblList = { list: [] }; const ncTblList = { list: [] };
ncTblList['list'] = await this.tablesService.getAccessibleTables({ ncTblList['list'] = await this.tablesService.getAccessibleTables({
@ -2404,8 +2337,6 @@ export class AtImportProcessor {
}); });
recordPerfStats(_perfStart, 'dbTable.read'); recordPerfStats(_perfStart, 'dbTable.read');
recordCnt = 0;
recordsMap[ncTbl.id] = await importData({ recordsMap[ncTbl.id] = await importData({
projectName: syncDB.projectName, projectName: syncDB.projectName,
table: ncTbl, table: ncTbl,
@ -2469,12 +2400,12 @@ export class AtImportProcessor {
await generateMigrationStats(aTblSchema); await generateMigrationStats(aTblSchema);
} }
} catch (e) { } catch (e) {
if (e.response?.data?.msg) { if (e.message) {
T.event({ T.event({
event: 'a:airtable-import:error', 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; throw e;
} }

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

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

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

@ -40,8 +40,8 @@ export class AuthService {
email: _email, email: _email,
firstname, firstname,
lastname, lastname,
token, // token,
ignore_subscribe, // ignore_subscribe,
} = createUserDto as any; } = createUserDto as any;
let { password } = createUserDto; 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: { async getDataList(param: {
model: Model; model: Model;
view: View; view?: View;
query: any; query: any;
baseModel?: BaseModelSqlv2; 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 getColumnUiType from '../helpers/getColumnUiType';
import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName'; import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName';
import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue'; import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue';
import { import { Audit, Column, Model, ModelRoleVisibility, Project } from '../models';
Audit,
Column,
Model,
ModelRoleVisibility,
Project,
} from '../models';
import Noco from '../Noco'; import Noco from '../Noco';
import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2';
import { validatePayload } from '../helpers'; import { validatePayload } from '../helpers';

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

@ -9,5 +9,6 @@ export default class XcAudit {
private static app: Noco; private static app: Noco;
// @ts-ignore // @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public static async log(data: { project }) {} 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 inflection from 'inflection';
import ModelXcMetaFactory from '../../db/sql-mgr/code/models/xc/ModelXcMetaFactory'; import ModelXcMetaFactory from '../../db/sql-mgr/code/models/xc/ModelXcMetaFactory';
import NcConnectionMgr from '../../utils/common/NcConnectionMgr'; import NcConnectionMgr from '../../utils/common/NcConnectionMgr';
import { DbConfig, NcConfig } from '../../interface/config';
import ncModelsOrderUpgrader from './jobs/ncModelsOrderUpgrader'; import ncModelsOrderUpgrader from './jobs/ncModelsOrderUpgrader';
import ncParentModelTitleUpgrader from './jobs/ncParentModelTitleUpgrader'; import ncParentModelTitleUpgrader from './jobs/ncParentModelTitleUpgrader';
import ncRemoveDuplicatedRelationRows from './jobs/ncRemoveDuplicatedRelationRows'; import ncRemoveDuplicatedRelationRows from './jobs/ncRemoveDuplicatedRelationRows';
import type { DbConfig, NcConfig } from '../../interface/config'; import NcProjectBuilder from './NcProjectBuilder';
import type { XKnex } from '../../db/CustomKnex'; import type { XKnex } from '../../db/CustomKnex';
import type { BaseModelSql } from '../../db/BaseModelSql'; import type { BaseModelSql } from '../../db/BaseModelSql';
import type { MetaService } from '../../meta/meta.service'; import type { MetaService } from '../../meta/meta.service';
import type Noco from '../../Noco'; import type Noco from '../../Noco';
import type NcProjectBuilder from './NcProjectBuilder';
import type { MysqlClient, PgClient, SqlClient } from 'nc-help'; import type { MysqlClient, PgClient, SqlClient } from 'nc-help';
const log = debug('nc:api:base'); 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 { Router } from 'express';
import { SqlClientFactory } from '../../db/sql-client/lib/SqlClientFactory'; import { SqlClientFactory } from '../../db/sql-client/lib/SqlClientFactory';
import Noco from '../../Noco';
import { NcConfig } from '../../interface/config';
import { GqlApiBuilder } from './gql/GqlApiBuilder'; import { GqlApiBuilder } from './gql/GqlApiBuilder';
import { RestApiBuilder } from './rest/RestApiBuilder'; import { RestApiBuilder } from './rest/RestApiBuilder';
import type Noco from '../../Noco';
import type { NcConfig } from '../../interface/config';
export default class NcProjectBuilder { export default class NcProjectBuilder {
public readonly id: string; public readonly id: string;

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

@ -1,10 +1,10 @@
import { Router } from 'express'; import { Router } from 'express';
import GqlXcSchemaFactory from '../../../db/sql-mgr/code/gql-schema/xc-ts/GqlXcSchemaFactory'; import GqlXcSchemaFactory from '../../../db/sql-mgr/code/gql-schema/xc-ts/GqlXcSchemaFactory';
import BaseApiBuilder from '../BaseApiBuilder'; import BaseApiBuilder from '../BaseApiBuilder';
import type { MetaService } from '../../../meta/meta.service'; import { MetaService } from '../../../meta/meta.service';
import type Noco from '../../../Noco'; import Noco from '../../../Noco';
import type NcProjectBuilder from '../NcProjectBuilder'; import NcProjectBuilder from '../NcProjectBuilder';
import type { DbConfig, NcConfig } from '../../../interface/config'; import { DbConfig, NcConfig } from '../../../interface/config';
import type XcMetaMgr from '../../../interface/XcMetaMgr'; import type XcMetaMgr from '../../../interface/XcMetaMgr';
export class GqlApiBuilder extends BaseApiBuilder<Noco> implements 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 ExpressXcTsRoutes from '../../../db/sql-mgr/code/routes/xc-ts/ExpressXcTsRoutes';
import NcHelp from '../../../utils/NcHelp'; import NcHelp from '../../../utils/NcHelp';
import BaseApiBuilder from '../BaseApiBuilder'; import BaseApiBuilder from '../BaseApiBuilder';
import type { MetaService } from '../../../meta/meta.service'; import { MetaService } from '../../../meta/meta.service';
import type Noco from '../../../Noco'; import Noco from '../../../Noco';
import { DbConfig, NcConfig } from '../../../interface/config';
import NcProjectBuilder from '../NcProjectBuilder';
import type { Router } from 'express'; import type { Router } from 'express';
import type { DbConfig, NcConfig } from '../../../interface/config';
import type NcProjectBuilder from '../NcProjectBuilder';
export class RestApiBuilder extends BaseApiBuilder<Noco> { export class RestApiBuilder extends BaseApiBuilder<Noco> {
public readonly type = 'rest'; public readonly type = 'rest';

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

@ -1,13 +1,13 @@
import { UITypes } from 'nocodb-sdk'; import { UITypes } from 'nocodb-sdk';
import request from 'supertest'; 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 Model from '../../../src/models/Model';
import Project from '../../../src/models/Project'; import { isPg, isSqlite } from '../init/db';
import View from '../../../src/models/View'; import type Column from '../../../src/models/Column';
import { isSqlite, isPg } from '../init/db'; 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) { const defaultColumns = function (context) {
return [ 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) => { const createColumn = async (context, table, columnAttr) => {
await request(context.app) await request(context.app)
.post(`/api/v1/db/meta/tables/${table.id}/columns`) .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( const column: Column = (await table.getColumns()).find(
(column) => column.title === columnAttr.title (column) => column.title === columnAttr.title,
); );
return column; return column;
}; };
@ -76,7 +192,7 @@ const createRollupColumn = async (
table: Model; table: Model;
relatedTableName: string; relatedTableName: string;
relatedTableColumnTitle: string; relatedTableColumnTitle: string;
} },
) => { ) => {
const childBases = await project.getBases(); const childBases = await project.getBases();
const childTable = await Model.getByIdOrName({ const childTable = await Model.getByIdOrName({
@ -86,13 +202,13 @@ const createRollupColumn = async (
}); });
const childTableColumns = await childTable.getColumns(); const childTableColumns = await childTable.getColumns();
const childTableColumn = await childTableColumns.find( const childTableColumn = await childTableColumns.find(
(column) => column.title === relatedTableColumnTitle (column) => column.title === relatedTableColumnTitle,
); );
const ltarColumn = (await table.getColumns()).find( const ltarColumn = (await table.getColumns()).find(
(column) => (column) =>
column.uidt === UITypes.LinkToAnotherRecord && 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, { const rollupColumn = await createColumn(context, table, {
@ -122,7 +238,7 @@ const createLookupColumn = async (
table: Model; table: Model;
relatedTableName: string; relatedTableName: string;
relatedTableColumnTitle: string; relatedTableColumnTitle: string;
} },
) => { ) => {
const childBases = await project.getBases(); const childBases = await project.getBases();
const childTable = await Model.getByIdOrName({ const childTable = await Model.getByIdOrName({
@ -132,19 +248,19 @@ const createLookupColumn = async (
}); });
const childTableColumns = await childTable.getColumns(); const childTableColumns = await childTable.getColumns();
const childTableColumn = await childTableColumns.find( const childTableColumn = await childTableColumns.find(
(column) => column.title === relatedTableColumnTitle (column) => column.title === relatedTableColumnTitle,
); );
if (!childTableColumn) { if (!childTableColumn) {
throw new Error( throw new Error(
`Could not find column ${relatedTableColumnTitle} in ${relatedTableName}` `Could not find column ${relatedTableColumnTitle} in ${relatedTableName}`,
); );
} }
const ltarColumn = (await table.getColumns()).find( const ltarColumn = (await table.getColumns()).find(
(column) => (column) =>
column.uidt === UITypes.LinkToAnotherRecord && 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, { const lookupColumn = await createColumn(context, table, {
title: title, title: title,
@ -168,15 +284,15 @@ const createQrCodeColumn = async (
title: string; title: string;
table: Model; table: Model;
referencedQrValueTableColumnTitle: string; referencedQrValueTableColumnTitle: string;
} },
) => { ) => {
const referencedQrValueTableColumnId = await table const referencedQrValueTableColumnId = await table
.getColumns() .getColumns()
.then( .then(
(cols) => (cols) =>
cols.find( cols.find(
(column) => column.title == referencedQrValueTableColumnTitle (column) => column.title == referencedQrValueTableColumnTitle,
)['id'] )['id'],
); );
const qrCodeColumn = await createColumn(context, table, { const qrCodeColumn = await createColumn(context, table, {
@ -198,15 +314,15 @@ const createBarcodeColumn = async (
title: string; title: string;
table: Model; table: Model;
referencedBarcodeValueTableColumnTitle: string; referencedBarcodeValueTableColumnTitle: string;
} },
) => { ) => {
const referencedBarcodeValueTableColumnId = await table const referencedBarcodeValueTableColumnId = await table
.getColumns() .getColumns()
.then( .then(
(cols) => (cols) =>
cols.find( cols.find(
(column) => column.title == referencedBarcodeValueTableColumnTitle (column) => column.title == referencedBarcodeValueTableColumnTitle,
)['id'] )['id'],
); );
const barcodeColumn = await createColumn(context, table, { const barcodeColumn = await createColumn(context, table, {
@ -230,7 +346,7 @@ const createLtarColumn = async (
parentTable: Model; parentTable: Model;
childTable: Model; childTable: Model;
type: string; type: string;
} },
) => { ) => {
const ltarColumn = await createColumn(context, parentTable, { const ltarColumn = await createColumn(context, parentTable, {
title: title, title: title,
@ -246,7 +362,7 @@ const createLtarColumn = async (
const updateViewColumn = async ( const updateViewColumn = async (
context, 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) const res = await request(context.app)
.patch(`/api/v1/db/meta/views/${view.id}/columns/${column.id}`) .patch(`/api/v1/db/meta/views/${view.id}/columns/${column.id}`)
@ -263,6 +379,7 @@ const updateViewColumn = async (
}; };
export { export {
customColumns,
defaultColumns, defaultColumns,
createColumn, createColumn,
createQrCodeColumn, 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 request from 'supertest';
import Column from '../../../src/models/Column';
import Filter from '../../../src/models/Filter';
import Model from '../../../src/models/Model'; 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 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) => { const rowValue = (column: ColumnType, index: number) => {
switch (column.uidt) { switch (column.uidt) {
@ -175,9 +176,17 @@ const rowMixedValue = (column: ColumnType, index: number) => {
case UITypes.Date: case UITypes.Date:
// set startDate as 400 days before today // set startDate as 400 days before today
// eslint-disable-next-line no-case-declarations // eslint-disable-next-line no-case-declarations
const result = new Date(); const d1 = new Date();
result.setDate(result.getDate() - 400 + index); d1.setDate(d1.getDate() - 400 + index);
return result.toISOString().slice(0, 10); 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: case UITypes.URL:
return urls[index % urls.length]; return urls[index % urls.length];
case UITypes.SingleSelect: case UITypes.SingleSelect:
@ -228,7 +237,7 @@ const listRow = async ({
const getOneRow = async ( const getOneRow = async (
context, context,
{ project, table }: { project: Project; table: Model } { project, table }: { project: Project; table: Model },
) => { ) => {
const response = await request(context.app) const response = await request(context.app)
.get(`/api/v1/db/data/noco/${project.id}/${table.id}/find-one`) .get(`/api/v1/db/data/noco/${project.id}/${table.id}/find-one`)
@ -266,7 +275,7 @@ const createRow = async (
project: Project; project: Project;
table: Model; table: Model;
index?: number; index?: number;
} },
) => { ) => {
const columns = await table.getColumns(); const columns = await table.getColumns();
const rowData = generateDefaultRowAttributes({ columns, index }); const rowData = generateDefaultRowAttributes({ columns, index });
@ -289,7 +298,7 @@ const createBulkRows = async (
project: Project; project: Project;
table: Model; table: Model;
values: any[]; values: any[];
} },
) => { ) => {
await request(context.app) await request(context.app)
.post(`/api/v1/db/data/bulk/noco/${project.id}/${table.id}`) .post(`/api/v1/db/data/bulk/noco/${project.id}/${table.id}`)
@ -317,7 +326,7 @@ const createChildRow = async (
rowId?: string; rowId?: string;
childRowId?: string; childRowId?: string;
type: string; type: string;
} },
) => { ) => {
if (!rowId) { if (!rowId) {
const row = await createRow(context, { project, table }); const row = await createRow(context, { project, table });
@ -331,7 +340,7 @@ const createChildRow = async (
await request(context.app) await request(context.app)
.post( .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); .set('xc-auth', context.token);

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

@ -1,9 +1,20 @@
import { ViewTypes } from 'nocodb-sdk'; import { ViewTypes } from 'nocodb-sdk';
import request from 'supertest'; import request from 'supertest';
import Model from '../../../src/models/Model';
import View from '../../../src/models/View'; 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) => { const viewTypeStr = (type) => {
switch (type) { switch (type) {
case ViewTypes.GALLERY: case ViewTypes.GALLERY:
@ -26,13 +37,70 @@ const createView = async (context, {title, table, type}: {title: string, table:
title, title,
type, type,
}); });
if(response.status !== 200) { if (response.status !== 200) {
throw new Error('createView',response.body.message); 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; const isFirstTimeRun = () => !server;
export default async function () { export default async function (isSakila = true) {
const { default: TestDbMngr } = await import('../TestDbMngr'); const { default: TestDbMngr } = await import('../TestDbMngr');
if (isFirstTimeRun()) { if (isFirstTimeRun()) {
@ -33,7 +33,10 @@ export default async function () {
server = await serverInit(); server = await serverInit();
} }
await cleanUpSakila(); if (isSakila) {
await cleanUpSakila();
}
await cleanupMeta(); await cleanupMeta();
const { token } = await createUser({ app: server }, { roles: 'editor' }); 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 viewRowTests from './tests/viewRow.test';
import attachmentTests from './tests/attachment.test'; import attachmentTests from './tests/attachment.test';
import filterTest from './tests/filter.test'; import filterTest from './tests/filter.test';
import newDataApisTest from './tests/newDataApis.test';
function restTests() { function restTests() {
authTests(); authTests();
@ -19,6 +20,7 @@ function restTests() {
columnTypeSpecificTests(); columnTypeSpecificTests();
attachmentTests(); attachmentTests();
filterTest(); filterTest();
newDataApisTest();
} }
export default function () { 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)', referencedValue: 'A Corua (La Corua)',
base64EncodedSrc: 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', referencedValue: 'Abha',
base64EncodedSrc: base64EncodedSrc:
// 'data:image/png;base64,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', 'data:image/png;base64,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',
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAYAAAA8AXHiAAAAAXNSR0IArs4c6QAACsxJREFUeF7tne1uGzsMRJP3f+hcpMAFkl0152hKOW4z/iuJpIbDD8nr9evb29vbSz9FYBiB1xJrGNGK+4VAiVUiHEGgxDoCa4WWWOXAEQSWxHp9fT2ijIROnCOutl9lrvZm5pDt13Ejc2K/V73P4rsS6+XlxZCgxPoagRuGq1Phs7B+15m/TiOXbGtIY+bs2mJkNmPtohrOnwC6xHqONkaVwgmHm16A9Jj+yOgh3pMdSWZMZJKd7+OUGY1eo4dwjUrhCeOmSEK2JWWdZJZYouUwPZYBepf1JdYdsYkgoFZg10+/m096WgoXyJlAQmDhELFy2I8n1gkATL+wcgaRwNg6ISPJBIne3TWr+QaT3Xu5kR7rhGElliuNJdYFJzq9lFgl1u0S0pSBEuuO0m72WQXf7jXA6kQ74b+WwqBZT0p/4iwiSYklnGeuGyYcSlkhuZNKHGyIZubQfui02oy1+N7PAE+nGXPMT8q2yTaJ/bv7KbEWj+YYh+46hyK8GcshSr7553osgqXEuv9WJmkxfhyxdjdMRDQZLCmnj1rTUjhUCkuszxmpxCqx8KHEJMuVWCVWiWUaYNO7fJwzdY/1iFK42tuuXtMwT+BsMtaur6LM+czPYyXOmAD2hN5EZkKAif0bvaTnqZ/HSpxBG05Aa8a6I0A4l1gL1pwgdCLTBAHd+E+U12OlMNlgsma3bzF3ThRZiYz3Nbu2Jj2leUiP7Ej8kKyJbt4TRckaAmkC6AkZJdbduyXW4nVglNUmss2EjDS7JkG+u6bEKrF2OaPml1glliLK7iRFrF2hp+YnJylaY3qs634edbIyeqlsn/LFrtynfj8WkcQ00YmzzJpdoM2R3egtsQaQL7H4WapT2fRP3deMJR6BPuU8ChxTtk/ZdoRYlG4JkNQoAinRSzKTEmVKMN3JrfQmtl7lJBgZf+3aFn2l813GJ3p3AVndFRkSTARjYmuJJcKCgC2xGMQEI5Z6/wqL1jRjLRAyzqFSR+MmC5Lz0jKeyKWgv2XO73oey2zOOJiO6FSiTOnbBdXsLdVr9kP6SYb5+gl1lFj3t9MRWQlUO06Bc+pUWGIF75unEmScVWLdX5C7m7WjHstG5J/Oo4g2fQpFZ1qS/nRvqV6zH7KNZLQULhBsxiJaiRfTil9QkZYoYyWMPpF93jdHRErKmrGV9NK4ybbkPJP1jAxT5ggT9XTDiVRJhhmgDaFJzwSIhtAl1gLpEov/3YGIQ+MmkEy2oUAyMiaCrRlL/FOxcRYRh8ZLrEUfswLFOOPjuiRKkqsC02OR7cZWkxmucxK9u9VkArOo9E9dkBJIxsEE/ARIiYwSiw9JI6WwGSvJT/c1FIwmCKjkGhlmN7t6ouuGEsu4gueUWKLhJRgJxFUdp9KY1H6yc3U3ZEoh9T4mGBM9tB8jk2SY+7JjpZCMK7G4FBoSGBx3D0nkuxJL/HVvAmLi8Eetof0YO0hGiVVi3Tjy1MQyVwVJj0E9FJ1ETKQ9ag45MPk6imSavRm95Iekl1WnwhKLXUgkMA6eCM6EJMkasrXEYs6oGSXW5bXg5ua9GYu5VWKVWMySYEaJNUAsg/uJxpucl15CUr+QyJ2QmeBs1pieiuSQL6Iei5SaU4SRYUowyTEONnPIGRRI5AhzV7Taq5G7ixHNX/n3hk/SYyWKd2+Mp0A0pDFzSqzPCBChm7HCS1UiI42b8poEsFlDQWJkfBuxyDgybFUaJtaY+6QJ4Gn/qpyIX8sQgU/tl3xxLGMRsGRYibX+hfZuL1diLZhI0WjKCTkizRwUOGacgsuQgjAyMpIMjbafat4JWDKsGesHZqwJUphT4kS2ORGNFDQ2KAwGpMv4gmQYO3b1RD2WUZKk6CsAJRZRYv+FaKZ9mLjqKbEWKJrAIZdTYK2yGsmccHiJJd/LsEuCE2nfOGtlp7GFyLa7f2PrBIGbsZqxbv9N/W3EoiiaMMyWCtOHkb0TMh7RH5psY/aSZDlT2j/aF2UsclSJxb8cfscocTCdcksswU7TgxggSdWEjGaszwg0Y8lDApGzxAqIZSLazNl1zsR8Y1dSkqjnoHHTL5n9J7aT3ORroFuJNl/pJM4xZYw2ODGe2G70EnFovMSSpcI40Dhseo6xK4l6Ig6Nl1gl1pLrRBwa/5HESrJGAiTpeVQ5pYx1yo5EL2VgGl9dcyT7Q9tXPRY53EQbKTY6kg0buXSCo7uiRMdqDWFkmmgK6ESG2R/aXmLxReUpgqNzHvRocrI/tL3EKrEeRixKryZVJsZSiZpI6xRppsyv+pRnKZ/GjgQD4/OPc9TNe2JIicV/QrDrLEPoEkugeuKEMxUkJGcisJKGv8QqsQQC9ylE6BJLwNqM9cOIlTTRJtKS8kFyjUwiMOl4d39y4JlYQwceexj5OM/sl/JC1LyXWHdYJ0hiHEqB8igZJZb4X+lmrM80MeQssUqsGwcmst63EQsVBw4nmatex6xJ5uxmOXJmckc1tV+ToXZL/bEei5z1zECT7SsSEPDPvN8SS3jcOFCIwSnNWJ9fZnu7Pzv1aDJ5xhDARJK5ECRbkvES65uIRaUhcaZZQ4Q1ZH2U7YmtZBuNm77MYES+ONZjmQ2Sccl44izKehNAr/aS2Eq40niJlbBqcQN+FWNIYpwTmvdpWYl1+YfVUzfv3+WsZqzPCJjgI1+1FC4Qasb6ujEnUv0qt1OnQlJGpyhzN7TSQXJpnOz+3TjJpXGzl6QvS/ZzwtYSK/GE+K3lCWeZxjvZzglbS6zEEyUWvoKpxCqxbiSh0+qqbbkdgKZ+/hX658tlyQbNGrqCeJbm3WCanOBof+bUT7YtMxYtetS4IUnSH5RYn39BZDDcJXCJFfRLU4FlAod07Tp8dQAosRYoG1B2nUOlguTZ8R9HrIkNW3A/zqPoM7WfbCcd9khPhKbxqXss0rPaL2E04Tt1KkwUJWvI6SUW/xrIZNsS68LOEqvEShIWX7oFr/WhE+DKUBPRuyUokWls27XDlvpdB97sSL4r3FVqAHqfk4B0lW0cuGs/lehdef/PJ1tNf5TYRuUyqQwlVsCCxHlGTYl1eR7LgEZzJqLCZkKyhcZLrP1qok6FJ4AtsdZ/zUvXL1TGKEhWPRa1IEmbUmIJT5wILNNE/7gei3oDc/mXZCwjl3gyYbspwYaMSfZJ7CdMTMbalpGcCpPNGePNnOT64OOaCdtLrDsCI6fCCec0Y/FlpyEwZRIzngQ0BXjUY5VYd3dNlLWkfBri0JwSa4EQgULjBLodJ1JMZWhrz1fzJmwlO/65jEWZI8m2BOLqOH5dU2KJ92kmzjGZw8zB2n75PjGRaYhEdpRYi7x4IuqNg80ccugJ2w3RJspLErDGNsIsCQLS+1eVQnIebfZ93DjP6Nkl8ITMlf1GriEOYberp8QSBwJz7KdsaxxDZC2xhLOSZtY4hyKvGctl7d3yecuKf9PNe4mVXaq2FMLrkkqsf5xYVG7MuCmFRs5uNCZPDJjymdiaBMoJW6g/NPd0I6UwAdEQIAHayP04p8S6e6/EEoymiC6xSixBo/uUEmsftm/LWPumZivMBnePwUS0laUnSrLJlAY1si25CzN6t3E31w2J4mRNicWolViM0W1GicWglViMUYkl/gFtuwTB0x3v8pL2YNuOZ36jX8DNLnkSBJ76xWtPglHNCBAosQLQuoQRKLEYo84IEPgP102OL11LUEwAAAAASUVORK5CYII=',
}, },
{ {
referencedValue: 'Abu Dhabi', referencedValue: 'Abu Dhabi',
base64EncodedSrc: 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', referencedValue: 'Hamburg',
base64EncodedSrc: base64EncodedSrc:
'data:image/png;base64,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', 'data:image/png;base64,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',
}, },
...expectedQrCodeCellValues.slice(1), ...expectedQrCodeCellValues.slice(1),
]; ];
@ -115,7 +112,7 @@ test.describe('Virtual Columns', () => {
{ {
referencedValue: 'Hamburg', referencedValue: 'Hamburg',
base64EncodedSrc: base64EncodedSrc:
'data:image/png;base64,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', 'data:image/png;base64,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',
}, },
]); ]);

Loading…
Cancel
Save