Browse Source

Merge pull request #5621 from nocodb/develop

pull/5622/head 0.107.0-beta.1
github-actions[bot] 2 years ago committed by GitHub
parent
commit
1283338ceb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      packages/nc-gui/components/cell/Currency.vue
  2. 60
      packages/nc-gui/components/dashboard/TreeView.vue
  3. 109
      packages/nc-gui/components/dlg/AirtableImport.vue
  4. 84
      packages/nc-gui/components/dlg/ProjectDuplicate.vue
  5. 84
      packages/nc-gui/components/dlg/TableDuplicate.vue
  6. 11
      packages/nc-gui/components/smartsheet/header/Menu.vue
  7. 10
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  8. 78
      packages/nc-gui/lang/pl.json
  9. 16
      packages/nc-gui/lang/ru.json
  10. 10
      packages/nc-gui/lib/enums.ts
  11. 16
      packages/nc-gui/nuxt-shim.d.ts
  12. 52
      packages/nc-gui/package-lock.json
  13. 2
      packages/nc-gui/package.json
  14. 76
      packages/nc-gui/pages/index/index/index.vue
  15. 100
      packages/nc-gui/plugins/jobs.ts
  16. 13086
      packages/nc-plugin/package-lock.json
  17. 1
      packages/nc-plugin/package.json
  18. 16
      packages/nc-plugin/src/lib/IStorageAdapterV2.ts
  19. 1
      packages/nocodb-legacy/.gitignore
  20. 105
      packages/nocodb-legacy/package-lock.json
  21. 2
      packages/nocodb-legacy/package.json
  22. 19
      packages/nocodb-legacy/src/lib/controllers/exportImport/export.ctl.ts
  23. 39
      packages/nocodb-legacy/src/lib/controllers/exportImport/import.ctl.ts
  24. 7
      packages/nocodb-legacy/src/lib/controllers/exportImport/index.ts
  25. 87
      packages/nocodb-legacy/src/lib/db/sql-client/lib/sqlite/SqliteClient.ts
  26. 35
      packages/nocodb-legacy/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
  27. 3
      packages/nocodb-legacy/src/lib/meta/api/index.ts
  28. 20
      packages/nocodb-legacy/src/lib/plugins/backblaze/Backblaze.ts
  29. 18
      packages/nocodb-legacy/src/lib/plugins/gcs/Gcs.ts
  30. 20
      packages/nocodb-legacy/src/lib/plugins/linode/LinodeObjectStorage.ts
  31. 22
      packages/nocodb-legacy/src/lib/plugins/mino/Minio.ts
  32. 20
      packages/nocodb-legacy/src/lib/plugins/ovhCloud/OvhCloud.ts
  33. 18
      packages/nocodb-legacy/src/lib/plugins/s3/S3.ts
  34. 16
      packages/nocodb-legacy/src/lib/plugins/scaleway/ScalewayObjectStorage.ts
  35. 20
      packages/nocodb-legacy/src/lib/plugins/spaces/Spaces.ts
  36. 18
      packages/nocodb-legacy/src/lib/plugins/upcloud/UpoCloud.ts
  37. 18
      packages/nocodb-legacy/src/lib/plugins/vultr/Vultr.ts
  38. 5
      packages/nocodb-legacy/src/lib/services/dbData/bulkData.ts
  39. 493
      packages/nocodb-legacy/src/lib/services/exportImport/export.svc.ts
  40. 844
      packages/nocodb-legacy/src/lib/services/exportImport/import.svc.ts
  41. 2
      packages/nocodb-legacy/src/lib/services/index.ts
  42. 2
      packages/nocodb-legacy/src/lib/services/metaDiff.svc.ts
  43. 27
      packages/nocodb-legacy/src/lib/v1-legacy/plugins/adapters/storage/Local.ts
  44. 4
      packages/nocodb-sdk/package-lock.json
  45. 146
      packages/nocodb-sdk/src/lib/Api.ts
  46. 4
      packages/nocodb-sdk/src/lib/globals.ts
  47. 6
      packages/nocodb/.gitignore
  48. 316
      packages/nocodb/package-lock.json
  49. 11
      packages/nocodb/package.json
  50. 6
      packages/nocodb/src/Noco.ts
  51. 21
      packages/nocodb/src/app.module.ts
  52. 6
      packages/nocodb/src/controllers/imports/helpers/NocoSyncDestAdapter.ts
  53. 7
      packages/nocodb/src/controllers/imports/helpers/NocoSyncSourceAdapter.ts
  54. 2480
      packages/nocodb/src/controllers/imports/helpers/job.ts
  55. 21
      packages/nocodb/src/controllers/imports/import.controller.spec.ts
  56. 148
      packages/nocodb/src/controllers/imports/import.controller.ts
  57. 2
      packages/nocodb/src/controllers/tables.controller.ts
  58. 30
      packages/nocodb/src/controllers/test/TestResetService/index.ts
  59. 87
      packages/nocodb/src/db/BaseModelSqlv2.ts
  60. 44
      packages/nocodb/src/db/sql-client/lib/KnexClient.ts
  61. 76
      packages/nocodb/src/db/sql-client/lib/mssql/MssqlClient.ts
  62. 58
      packages/nocodb/src/db/sql-client/lib/mysql/MysqlClient.ts
  63. 75
      packages/nocodb/src/db/sql-client/lib/pg/PgClient.ts
  64. 71
      packages/nocodb/src/db/sql-client/lib/snowflake/SnowflakeClient.ts
  65. 163
      packages/nocodb/src/db/sql-client/lib/sqlite/SqliteClient.ts
  66. 12
      packages/nocodb/src/gateways/socket.gateway.spec.ts
  67. 43
      packages/nocodb/src/gateways/socket.gateway.ts
  68. 82
      packages/nocodb/src/helpers/exportImportHelpers.ts
  69. 6
      packages/nocodb/src/helpers/populateMeta.ts
  70. 22
      packages/nocodb/src/interface/Jobs.ts
  71. 35
      packages/nocodb/src/jobs/EmitteryJobsMgr.ts
  72. 67
      packages/nocodb/src/jobs/JobsMgr.ts
  73. 20
      packages/nocodb/src/jobs/NocoJobs.ts
  74. 56
      packages/nocodb/src/jobs/RedisJobsMgr.ts
  75. 8
      packages/nocodb/src/models/Model.ts
  76. 1
      packages/nocodb/src/models/Project.ts
  77. 6
      packages/nocodb/src/modules/event-emitter/event-emitter.interface.ts
  78. 16
      packages/nocodb/src/modules/event-emitter/event-emitter.module.ts
  79. 27
      packages/nocodb/src/modules/event-emitter/fallback-event-emitter.ts
  80. 23
      packages/nocodb/src/modules/event-emitter/nestjs-event-emitter.ts
  81. 6
      packages/nocodb/src/modules/global/global.module.ts
  82. 65
      packages/nocodb/src/modules/jobs/at-import/at-import.controller.ts
  83. 2516
      packages/nocodb/src/modules/jobs/at-import/at-import.processor.ts
  84. 0
      packages/nocodb/src/modules/jobs/at-import/helpers/EntityMap.ts
  85. 0
      packages/nocodb/src/modules/jobs/at-import/helpers/fetchAT.ts
  86. 5
      packages/nocodb/src/modules/jobs/at-import/helpers/readAndProcessData.ts
  87. 0
      packages/nocodb/src/modules/jobs/at-import/helpers/syncMap.ts
  88. 136
      packages/nocodb/src/modules/jobs/export-import/duplicate.controller.ts
  89. 408
      packages/nocodb/src/modules/jobs/export-import/duplicate.processor.ts
  90. 721
      packages/nocodb/src/modules/jobs/export-import/export.service.ts
  91. 1472
      packages/nocodb/src/modules/jobs/export-import/import.service.ts
  92. 136
      packages/nocodb/src/modules/jobs/fallback-queue.service.ts
  93. 23
      packages/nocodb/src/modules/jobs/helpers.ts
  94. 69
      packages/nocodb/src/modules/jobs/jobs-event.service.ts
  95. 121
      packages/nocodb/src/modules/jobs/jobs.gateway.ts
  96. 39
      packages/nocodb/src/modules/jobs/jobs.module.ts
  97. 59
      packages/nocodb/src/modules/jobs/jobs.service.ts
  98. 22
      packages/nocodb/src/modules/metas/metas.module.ts
  99. 16
      packages/nocodb/src/plugins/backblaze/Backblaze.ts
  100. 16
      packages/nocodb/src/plugins/gcs/Gcs.ts
  101. Some files were not shown because too many files have changed in this diff Show More

12
packages/nc-gui/components/cell/Currency.vue

@ -41,9 +41,10 @@ const currencyMeta = computed(() => {
const currency = computed(() => {
try {
return !vModel.value || isNaN(vModel.value)
? vModel.value
: new Intl.NumberFormat(currencyMeta.value.currency_locale || 'en-US', {
if (vModel.value === null || vModel.value === undefined || isNaN(vModel.value)) {
return vModel.value
}
return new Intl.NumberFormat(currencyMeta.value.currency_locale || 'en-US', {
style: 'currency',
currency: currencyMeta.value.currency_code || 'USD',
}).format(vModel.value)
@ -74,6 +75,7 @@ onMounted(() => {
v-if="editEnabled"
:ref="focus"
v-model="vModel"
type="number"
class="w-full h-full border-none outline-none px-2"
@blur="submitCurrency"
@keydown.down.stop
@ -88,7 +90,9 @@ onMounted(() => {
<span v-else-if="vModel === null && showNull" class="nc-null">NULL</span>
<span v-else-if="vModel">{{ currency }}</span>
<!-- only show the numeric value as previously string value was accepted -->
<span v-else-if="!isNaN(vModel)">{{ currency }}</span>
<!-- possibly unexpected string / null with showNull == false -->
<span v-else />
</template>

60
packages/nc-gui/components/dashboard/TreeView.vue

@ -9,6 +9,7 @@ import type { VNodeRef } from '#imports'
import {
ClientType,
Empty,
JobStatus,
TabType,
computed,
extractSdkResponseErrorMsg,
@ -37,7 +38,7 @@ const { isMobileMode } = useGlobal()
const { addTab, updateTab } = useTabs()
const { $api, $e } = useNuxtApp()
const { $api, $e, $jobs } = useNuxtApp()
const projectStore = useProject()
@ -389,6 +390,38 @@ const setIcon = async (icon: string, table: TableType) => {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const duplicateTable = async (table: TableType) => {
if (!table || !table.id || !table.project_id) return
const isOpen = ref(true)
const { close } = useDialog(resolveComponent('DlgTableDuplicate'), {
'modelValue': isOpen,
'table': table,
'onOk': async (jobData: { name: string; id: string }) => {
$jobs.subscribe({ name: jobData.name, id: jobData.id }, undefined, async (status: string, data?: any) => {
if (status === JobStatus.COMPLETED) {
await loadTables()
const newTable = tables.value.find((el) => el.id === data?.result?.id)
if (newTable) addTableTab(newTable)
} else if (status === JobStatus.FAILED) {
message.error('Failed to duplicate table')
await loadTables()
}
})
$e('a:table:duplicate')
},
'onUpdate:modelValue': closeDialog,
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
</script>
<template>
@ -734,6 +767,16 @@ const setIcon = async (icon: string, table: TableType) => {
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('table-duplicate') && !table.mm"
v-e="['c:table:duplicate']"
@click="duplicateTable(table)"
>
<div class="nc-project-menu-item">
{{ $t('general.duplicate') }}
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('table-delete')"
:data-testid="`sidebar-table-delete-${table.title}`"
@ -1050,6 +1093,12 @@ const setIcon = async (icon: string, table: TableType) => {
</div>
</a-menu-item>
<a-menu-item v-if="isUIAllowed('table-duplicate') && !table.mm" @click="duplicateTable(table)">
<div class="nc-project-menu-item">
{{ $t('general.duplicate') }}
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('table-delete')"
:data-testid="`sidebar-table-delete-${table.title}`"
@ -1085,6 +1134,15 @@ const setIcon = async (icon: string, table: TableType) => {
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('table-duplicate') && !contextMenuTarget.value.mm"
@click="duplicateTable(contextMenuTarget.value)"
>
<div class="nc-project-menu-item">
{{ $t('general.duplicate') }}
</div>
</a-menu-item>
<a-menu-item v-if="isUIAllowed('table-delete')" @click="deleteTable(contextMenuTarget.value)">
<div class="nc-project-menu-item">
{{ $t('general.delete') }}

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

@ -1,16 +1,14 @@
<script setup lang="ts">
import type { Socket } from 'socket.io-client'
import io from 'socket.io-client'
import type { Card as AntCard } from 'ant-design-vue'
import {
Form,
JobStatus,
computed,
extractSdkResponseErrorMsg,
fieldRequiredValidator,
iconMap,
message,
nextTick,
onBeforeUnmount,
onMounted,
ref,
storeToRefs,
@ -31,7 +29,7 @@ const { appInfo } = $(useGlobal())
const baseURL = appInfo.ncSiteUrl
const { $state } = useNuxtApp()
const { $state, $jobs } = useNuxtApp()
const projectStore = useProject()
@ -49,8 +47,6 @@ const logRef = ref<typeof AntCard>()
const enableAbort = ref(false)
let socket: Socket | null
const syncSource = ref({
id: '',
type: 'Airtable',
@ -72,6 +68,35 @@ const syncSource = ref({
},
})
const pushProgress = async (message: string, status: JobStatus | 'progress') => {
progress.value.push({ msg: message, status })
await nextTick(() => {
const container: HTMLDivElement = logRef.value?.$el?.firstElementChild
if (!container) return
container.scrollTop = container.scrollHeight
})
}
const onSubscribe = () => {
step.value = 2
}
const onStatus = async (status: JobStatus, data?: any) => {
if (status === JobStatus.COMPLETED) {
showGoToDashboardButton.value = true
await loadTables()
pushProgress('Done!', status)
// TODO: add tab of the first table
} else if (status === JobStatus.FAILED) {
pushProgress(data.error.message, status)
}
}
const onLog = (data: { message: string }) => {
pushProgress(data.message, 'progress')
}
const validators = computed(() => ({
'details.apiKey': [fieldRequiredValidator()],
'details.syncSourceUrlOrId': [fieldRequiredValidator()],
@ -130,7 +155,7 @@ async function loadSyncSrc() {
srcs[0].details = srcs[0].details || {}
syncSource.value = migrateSync(srcs[0])
syncSource.value.details.syncSourceUrlOrId = srcs[0].details.shareId
socket?.emit('subscribe', syncSource.value.id)
$jobs.subscribe({ syncId: syncSource.value.id }, onSubscribe, onStatus, onLog)
} else {
syncSource.value = {
id: '',
@ -161,11 +186,8 @@ async function sync() {
baseURL,
method: 'POST',
headers: { 'xc-auth': $state.token.value as string },
params: {
id: socket?.id,
},
})
socket?.emit('subscribe', syncSource.value.id)
$jobs.subscribe({ syncId: syncSource.value.id }, onSubscribe, onStatus, onLog)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -183,9 +205,6 @@ async function abort() {
baseURL,
method: 'POST',
headers: { 'xc-auth': $state.token.value as string },
params: {
id: socket?.id,
},
})
step.value = 1
} catch (e: any) {
@ -223,67 +242,12 @@ watch(
)
onMounted(async () => {
socket = io(new URL(baseURL, window.location.href.split(/[?#]/)[0]).href, {
extraHeaders: { 'xc-auth': $state.token.value as string },
})
socket.on('progress', async (d: Record<string, any>) => {
progress.value.push(d)
await nextTick(() => {
const container: HTMLDivElement = logRef.value?.$el?.firstElementChild
if (!container) return
container.scrollTop = container.scrollHeight
})
if (d.status === 'COMPLETED') {
showGoToDashboardButton.value = true
await loadTables()
// TODO: add tab of the first table
}
})
socket.on('disconnect', () => {
console.log('socket disconnected')
const rcInterval = setInterval(() => {
if (socket?.connected) {
clearInterval(rcInterval)
socket?.emit('subscribe', syncSource.value.id)
} else {
socket?.connect()
}
}, 2000)
})
socket.on('job', () => {
step.value = 2
})
// connect event does not provide data
socket.on('connect', () => {
console.log('socket connected')
if (syncSource.value.id) {
socket?.emit('subscribe', syncSource.value.id)
$jobs.subscribe({ syncId: syncSource.value.id }, onSubscribe, onStatus, onLog)
}
})
socket?.io.on('reconnect', () => {
console.log('socket reconnected')
if (syncSource.value.id) {
socket?.emit('subscribe', syncSource.value.id)
}
})
await loadSyncSrc()
})
onBeforeUnmount(() => {
if (socket) {
socket.off('disconnect')
socket.disconnect()
socket.removeAllListeners()
}
})
</script>
<template>
@ -407,7 +371,7 @@ onBeforeUnmount(() => {
<a-card ref="logRef" :body-style="{ backgroundColor: '#000000', height: '400px', overflow: 'auto' }">
<div v-for="({ msg, status }, i) in progress" :key="i">
<div v-if="status === 'FAILED'" class="flex items-center">
<div v-if="status === JobStatus.FAILED" class="flex items-center">
<component :is="iconMap.closeCircle" class="text-red-500" />
<span class="text-red-500 ml-2">{{ msg }}</span>
@ -424,7 +388,8 @@ onBeforeUnmount(() => {
v-if="
!progress ||
!progress.length ||
(progress[progress.length - 1].status !== 'COMPLETED' && progress[progress.length - 1].status !== 'FAILED')
(progress[progress.length - 1].status !== JobStatus.COMPLETED &&
progress[progress.length - 1].status !== JobStatus.FAILED)
"
class="flex items-center"
>

84
packages/nc-gui/components/dlg/ProjectDuplicate.vue

@ -0,0 +1,84 @@
<script setup lang="ts">
import type { ProjectType } from 'nocodb-sdk'
import { useVModel } from '#imports'
const props = defineProps<{
modelValue: boolean
project: ProjectType
onOk: (jobData: { name: string; id: string }) => Promise<void>
}>()
const emit = defineEmits(['update:modelValue'])
const { api } = useApi()
const dialogShow = useVModel(props, 'modelValue', emit)
const options = ref({
includeData: true,
includeViews: true,
includeHooks: true,
})
const optionsToExclude = computed(() => {
const { includeData, includeViews, includeHooks } = options.value
return {
excludeData: !includeData,
excludeViews: !includeViews,
excludeHooks: !includeHooks,
}
})
const isLoading = ref(false)
const _duplicate = async () => {
isLoading.value = true
try {
const jobData = await api.project.duplicate(props.project.id as string, optionsToExclude.value)
props.onOk(jobData as any)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
isLoading.value = false
dialogShow.value = false
}
const isEaster = ref(false)
</script>
<template>
<a-modal
v-model:visible="dialogShow"
:class="{ active: dialogShow }"
width="max(30vw, 600px)"
centered
wrap-class-name="nc-modal-project-duplicate"
@keydown.esc="dialogShow = false"
>
<template #footer>
<a-button key="back" size="large" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
<a-button key="submit" size="large" type="primary" :loading="isLoading" @click="_duplicate"
>{{ $t('general.confirm') }}
</a-button>
</template>
<div class="pl-10 pr-10 pt-5">
<div class="prose-xl font-bold self-center my-4" @dblclick="isEaster = !isEaster">{{ $t('general.duplicate') }}</div>
<div class="mb-2">Are you sure you want to duplicate the `{{ project.title }}` project?</div>
<div class="prose-md self-center text-gray-500 mt-4">{{ $t('title.advancedSettings') }}</div>
<a-divider class="!m-0 !p-0 !my-2" />
<div class="text-xs p-2">
<a-checkbox v-model:checked="options.includeData">Include data</a-checkbox>
<a-checkbox v-model:checked="options.includeViews">Include views</a-checkbox>
<a-checkbox v-show="isEaster" v-model:checked="options.includeHooks">Include webhooks</a-checkbox>
</div>
</div>
</a-modal>
</template>
<style scoped lang="scss"></style>

84
packages/nc-gui/components/dlg/TableDuplicate.vue

@ -0,0 +1,84 @@
<script setup lang="ts">
import type { TableType } from 'nocodb-sdk'
import { useVModel } from '#imports'
const props = defineProps<{
modelValue: boolean
table: TableType
onOk: (jobData: { name: string; id: string }) => Promise<void>
}>()
const emit = defineEmits(['update:modelValue'])
const { api } = useApi()
const dialogShow = useVModel(props, 'modelValue', emit)
const options = ref({
includeData: true,
includeViews: true,
includeHooks: true,
})
const optionsToExclude = computed(() => {
const { includeData, includeViews, includeHooks } = options.value
return {
excludeData: !includeData,
excludeViews: !includeViews,
excludeHooks: !includeHooks,
}
})
const isLoading = ref(false)
const _duplicate = async () => {
isLoading.value = true
try {
const jobData = await api.dbTable.duplicate(props.table.project_id!, props.table.id!, optionsToExclude.value)
props.onOk(jobData as any)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
isLoading.value = false
dialogShow.value = false
}
const isEaster = ref(false)
</script>
<template>
<a-modal
v-model:visible="dialogShow"
:class="{ active: dialogShow }"
width="max(30vw, 600px)"
centered
wrap-class-name="nc-modal-table-duplicate"
@keydown.esc="dialogShow = false"
>
<template #footer>
<a-button key="back" size="large" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
<a-button key="submit" size="large" type="primary" :loading="isLoading" @click="_duplicate"
>{{ $t('general.confirm') }}
</a-button>
</template>
<div class="pl-10 pr-10 pt-5">
<div class="prose-xl font-bold self-center my-4" @dblclick="isEaster = !isEaster">{{ $t('general.duplicate') }}</div>
<div class="mb-2">Are you sure you want to duplicate the `{{ table.title }}` table?</div>
<div class="prose-md self-center text-gray-500 mt-4">{{ $t('title.advancedSettings') }}</div>
<a-divider class="!m-0 !p-0 !my-2" />
<div class="text-xs p-2">
<a-checkbox v-model:checked="options.includeData">Include data</a-checkbox>
<a-checkbox v-model:checked="options.includeViews">Include views</a-checkbox>
<a-checkbox v-show="isEaster" v-model:checked="options.includeHooks">Include hooks</a-checkbox>
</div>
</div>
</a-modal>
</template>
<style scoped lang="scss"></style>

11
packages/nc-gui/components/smartsheet/header/Menu.vue

@ -14,9 +14,11 @@ import {
iconMap,
inject,
message,
useGlobal,
useI18n,
useMetas,
useNuxtApp,
useProject,
useSmartsheetStoreOrThrow,
useUndoRedo,
} from '#imports'
@ -28,6 +30,10 @@ const emit = defineEmits(['edit', 'addColumn'])
const { eventBus } = useSmartsheetStoreOrThrow()
const { includeM2M } = useGlobal()
const { loadTables } = useProject()
const column = inject(ColumnInj)
const reloadDataHook = inject(ReloadViewDataHookInj)
@ -62,6 +68,11 @@ const deleteColumn = () =>
/** force-reload related table meta if deleted column is a LTAR and not linked to same table */
if (column?.value?.uidt === UITypes.LinkToAnotherRecord && column.value?.colOptions) {
await getMeta((column.value?.colOptions as LinkToAnotherRecordType).fk_related_model_id!, true)
// reload tables if deleted column is mm and include m2m is true
if (includeM2M.value && (column.value?.colOptions as LinkToAnotherRecordType).type === RelationTypes.MANY_TO_MANY) {
loadTables()
}
}
$e('a:column:delete')

10
packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue

@ -89,4 +89,14 @@ if (!localValue.value) {
.ant-select-selection-search-input {
box-shadow: none !important;
}
::-webkit-scrollbar {
-webkit-appearance: none;
width: 7px;
}
::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.5);
box-shadow: 0 0 1px rgba(255, 255, 255, 0.5);
}
</style>

78
packages/nc-gui/lang/pl.json

@ -39,7 +39,7 @@
"signIn": "Zaloguj",
"signOut": "Wyloguj",
"required": "Wymagany",
"enableScanner": "Enable Scanner for filling",
"enableScanner": "Włącz skaner do wypełnienia",
"preferred": "Preferowany",
"mandatory": "Obowiązkowy",
"loading": "Ładowanie ...",
@ -76,7 +76,7 @@
"hideField": "Ukryj pole",
"sortAsc": "Sortowanie rosnące",
"sortDesc": "Sortuj malejąco",
"geoDataField": "GeoData Field"
"geoDataField": "Pole GeoData"
},
"objects": {
"project": "Projekt",
@ -101,7 +101,7 @@
"form": "Formularz",
"kanban": "Kanban",
"calendar": "Kalendarz",
"map": "Map"
"map": "Mapa"
},
"user": "Użytkownik",
"users": "Użytkownicy",
@ -210,8 +210,8 @@
"advancedSettings": "Ustawienia zaawansowane",
"codeSnippet": "Snippet",
"keyboardShortcut": "Skróty klawiaturowe",
"generateRandomName": "Generate Random Name",
"findRowByScanningCode": "Find row by scanning a QR or Barcode"
"generateRandomName": "Wygeneruj losową nazwę",
"findRowByScanningCode": "Znajdź wiersz poprzez zeskanowanie kodu QR lub kodu kreskowego"
},
"labels": {
"createdBy": "Stworzony przez",
@ -221,7 +221,7 @@
"viewName": "Nazwa widoku",
"viewLink": "Zobacz link",
"columnName": "Nazwa kolumny",
"columnToScanFor": "Column to scan",
"columnToScanFor": "Kolumna do skanowania",
"columnType": "Typ kolumny",
"roleName": "Nazwa roli",
"roleDescription": "Opisy roli",
@ -238,7 +238,7 @@
"action": "Akcja",
"actions": "Akcje",
"operation": "Operacja",
"operationSub": "Sub Operation",
"operationSub": "Podoperacja",
"operationType": "Typ operacji",
"operationSubType": "Podtypowy akcji",
"description": "Opis",
@ -260,9 +260,9 @@
"barcodeFormat": "Format kodu kreskowego",
"qrCodeValueTooLong": "Zbyt wiele znaków dla kodu QR",
"barcodeValueTooLong": "Zbyt wiele znaków dla kodu kreskowego",
"currentLocation": "Current Location",
"lng": "Lng",
"lat": "Lat",
"currentLocation": "Bieżąca lokalizacja",
"lng": "Długość",
"lat": "Szerokość",
"aggregateFunction": "Funkcja agregacji",
"dbCreateIfNotExists": "Baza danych: Utwórz, jeśli nie istnieje",
"clientKey": "Klucz klienta",
@ -390,13 +390,13 @@
"renameTable": "Zmień nazwę tabeli.",
"deleteTable": "Usuń tabelę",
"addField": "Dodaj nowe pole do tej tabeli",
"setDisplay": "Set as Display value",
"setDisplay": "Ustaw jako wartość wyświetlana",
"addRow": "Dodaj nowy rząd",
"saveRow": "Zapisz wiersz",
"saveAndExit": "Zapisz i wyjdź",
"saveAndStay": "Oszczędzaj i zostań",
"insertRow": "Wstaw nowy rząd",
"duplicateRow": "Duplicate Row",
"duplicateRow": "Duplikuj wiersz",
"deleteRow": "Usuń rząd",
"deleteSelectedRow": "Usuń wybrane wiersze",
"importExcel": "Importuj Excel.",
@ -412,8 +412,8 @@
"changePwd": "Zmień hasło",
"createView": "Utwórz widok",
"shareView": "Widok udostępniania",
"findRowByCodeScan": "Find row by scan",
"fillByCodeScan": "Fill by scan",
"findRowByCodeScan": "Znajdź rząd przez skanowanie",
"fillByCodeScan": "Wypełnij przez skanowanie",
"listSharedView": "Wspólna lista widoków",
"ListView": "Lista widoków",
"copyView": "Widok kopiowania",
@ -429,10 +429,10 @@
"openTab": "Otwórz nową kartę",
"iFrame": "Skopiuj wbudowany kod HTML",
"addWebhook": "Dodaj nowy webhook.",
"enableWebhook": "Enable Webhook",
"testWebhook": "Test Webhook",
"copyWebhook": "Copy Webhook",
"deleteWebhook": "Delete Webhook",
"enableWebhook": "Włącz Webhook",
"testWebhook": "Testuj Webhook",
"copyWebhook": "Skopiuj Webhook",
"deleteWebhook": "Usuń Webhook",
"newToken": "Dodaj nowy token.",
"exportZip": "Eksportuj Zip.",
"importZip": "Importuj Zip.",
@ -470,12 +470,12 @@
"addOrEditStack": "Dodaj / Edytuj stos"
},
"map": {
"mappedBy": "Mapped By",
"chooseMappingField": "Choose a Mapping Field",
"openInGoogleMaps": "Google Maps",
"mappedBy": "Mapowany przez",
"chooseMappingField": "Wybierz pole mapowania",
"openInGoogleMaps": "Mapy Google",
"openInOpenStreetMap": "OSM"
},
"toggleMobileMode": "Toggle Mobile Mode"
"toggleMobileMode": "Przełącz tryb mobilny"
},
"tooltip": {
"saveChanges": "Zapisz zmiany",
@ -542,15 +542,15 @@
"orgViewer": "Widz nie może tworzyć nowych projektów, ale może mieć dostęp do każdego zaproszonego projektu."
},
"codeScanner": {
"loadingScanner": "Loading the scanner...",
"selectColumn": "Select a column (QR code or Barcode) that you want to use for finding a row by scanning.",
"moreThanOneRowFoundForCode": "More than one row found for this code. Currently only unique codes are supported.",
"noRowFoundForCode": "No row found for this code for the selected column"
"loadingScanner": "Ładowanie skanera...",
"selectColumn": "Wybierz kolumnę (kod QR lub kod kreskowy), której chcesz użyć do znalezienia wiersza przez skanowanie.",
"moreThanOneRowFoundForCode": "Znaleziono więcej niż jeden wiersz dla tego kodu. Obecnie obsługiwane są tylko unikalne kody.",
"noRowFoundForCode": "Nie znaleziono wiersza dla tego kodu dla wybranej kolumny"
},
"map": {
"overLimit": "You're over the limit.",
"closeLimit": "You're getting close to the limit.",
"limitNumber": "The limit of markers shown in a Map View is 1000 records."
"overLimit": "Przekroczono limit.",
"closeLimit": "Zbliżasz się do limitu.",
"limitNumber": "Limit markerów pokazywanych w widoku mapy wynosi 1000 rekordów."
},
"footerInfo": "Wiersze na stronę",
"upload": "Wybierz plik do przesłania",
@ -634,7 +634,7 @@
"gallery": "Dodaj widok galerii.",
"form": "Dodaj widok formy",
"kanban": "Dodaj widok Kanban.",
"map": "Add Map View",
"map": "Dodaj widok mapy",
"calendar": "Dodaj widok kalendarza"
},
"tablesMetadataInSync": "Tabele Metadane są synchronizowane",
@ -666,11 +666,11 @@
"deleteViewConfirmation": "Czy na pewno chcesz usunąć ten widok?",
"deleteTableConfirmation": "Czy chcesz usunąć tabelę",
"showM2mTables": "Pokaż tabele M2M",
"showM2mTablesDesc": "Many-to-many relation is supported via a junction table & is hidden by default. Enable this option to list all such tables along with existing tables.",
"showNullInCells": "Show NULL in Cells",
"showNullInCellsDesc": "Display 'NULL' tag in cells holding NULL value. This helps differentiate against cells holding EMPTY string.",
"showNullAndEmptyInFilter": "Show NULL and EMPTY in Filter",
"showNullAndEmptyInFilterDesc": "Enable 'additional' filters to differentiate fields containing NULL & Empty Strings. Default support for Blank treats both NULL & Empty strings alike.",
"showM2mTablesDesc": "Relacja wiele do wielu jest obsługiwana przez tabelę łączącą i jest domyślnie ukryta. Proszę włączyć tę opcję, aby wyświetlić wszystkie takie tabele wraz z istniejącymi.",
"showNullInCells": "Pokaż NULL w komórkach",
"showNullInCellsDesc": "Wyświetlanie znacznika 'NULL' w komórkach posiadających wartość NULL. Pomaga to odróżnić się od komórek posiadających ciąg EMPTY.",
"showNullAndEmptyInFilter": "Pokaż NULL i EMPTY w filtrze",
"showNullAndEmptyInFilterDesc": "Włącz filtry 'dodatkowe' do różnicowania pól zawierających NULL i puste ciągi. Domyślna obsługa dla pustych kodów przetwarza zarówno NULL, jak i puste ciągi.",
"deleteKanbanStackConfirmation": "Usunięcie tego stosu spowoduje również usunięcie wybranej opcji `{stackToBeDeleted}` z `{groupingField}`. Rekordy przeniosą się do nieskategoryzowanego stosu.",
"computedFieldEditWarning": "Pole obliczeniowe: zawartość jest tylko do odczytu. Do rekonfiguracji należy użyć menu edycji kolumny",
"computedFieldDeleteWarning": "Pole obliczeniowe: zawartość jest tylko do odczytu. Nie można wyczyścić zawartości.",
@ -698,7 +698,7 @@
"allowedSpecialCharList": "Dozwolona lista znaków specjalnych"
},
"invalidURL": "Nieprawidłowy adres URL",
"invalidEmail": "Invalid Email",
"invalidEmail": "Nieprawidłowy e-mail",
"internalError": "Wystąpił błąd wewnętrzny",
"templateGeneratorNotFound": "Nie można znaleźć generatora szablonów!",
"fileUploadFailed": "Nie udało się przesłać pliku",
@ -726,7 +726,7 @@
"nameShouldStartWithAnAlphabetOr_": "Nazwa powinna zaczynać się od alfabetu lub od _",
"followingCharactersAreNotAllowed": "Następujące znaki są niedozwolone",
"columnNameRequired": "Nazwa kolumny jest wymagana",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"columnNameExceedsCharacters": "Nazwa kolumny przekracza maksymalną długość {value} znaków",
"projectNameExceeds50Characters": "Nazwa projektu przekracza 50 znaków",
"projectNameCannotStartWithSpace": "Nazwa projektu nie może zaczynać się od spacji",
"requiredField": "Pole wymagane",
@ -759,7 +759,7 @@
},
"success": {
"columnDuplicated": "Kolumna powielona z powodzeniem",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"rowDuplicatedWithoutSavedYet": "Wiersz zduplikowany (niezapisany)",
"updatedUIACL": "Pomyślnie zaktualizowano UI ACL dla tabel",
"pluginUninstalled": "Wtyczka odinstalowana pomyślnie",
"pluginSettingsSaved": "Ustawienia wtyczki zapisane pomyślnie",
@ -779,7 +779,7 @@
"userDeletedFromProject": "Użytkownik usunięty z projektu",
"inviteEmailSent": "E-mail został wysłany pomyślnie",
"inviteURLCopied": "Link z zaproszeniem skopiowany do schowka",
"commentCopied": "Comment copied to clipboard",
"commentCopied": "Komentarz skopiowany do schowka",
"passwordResetURLCopied": "URL resetowania hasła skopiowany do schowka",
"shareableURLCopied": "Skopiowano adres URL do schowka!",
"embeddableHTMLCodeCopied": "Skopiowano kod HTML!",

16
packages/nc-gui/lang/ru.json

@ -221,7 +221,7 @@
"viewName": "Название представления",
"viewLink": "Ссылка на представление",
"columnName": "Название столбца",
"columnToScanFor": "Column to scan",
"columnToScanFor": "Колонка для сканирования",
"columnType": "Тип столбца",
"roleName": "Имя роли",
"roleDescription": "Описание роли",
@ -390,7 +390,7 @@
"renameTable": "Переименовать таблицу",
"deleteTable": "Удалить таблицу",
"addField": "Добавить новое поле в эту таблицу",
"setDisplay": "Set as Display value",
"setDisplay": "Установить как значение отображения",
"addRow": "Добавить новую строку",
"saveRow": "Сохранить строку",
"saveAndExit": "Сохранить и выйти",
@ -413,7 +413,7 @@
"createView": "Создать представление",
"shareView": "Поделиться представлением",
"findRowByCodeScan": "Найти строку путем сканирования",
"fillByCodeScan": "Fill by scan",
"fillByCodeScan": "Заполнить сканированием",
"listSharedView": "Общий список представлений",
"ListView": "Список представлений",
"copyView": "Скопировать представление",
@ -543,9 +543,9 @@
},
"codeScanner": {
"loadingScanner": "Загрузка сканера...",
"selectColumn": "Select a column (QR code or Barcode) that you want to use for finding a row by scanning.",
"moreThanOneRowFoundForCode": "More than one row found for this code. Currently only unique codes are supported.",
"noRowFoundForCode": "No row found for this code for the selected column"
"selectColumn": "Выберите столбец (QR-код или штрих-код), который вы хотите использовать для поиска ряда путем сканирования.",
"moreThanOneRowFoundForCode": "Для этого кода найдено более одной строки. В настоящее время поддерживаются только уникальные коды.",
"noRowFoundForCode": "Не найдена строка для этого кода для выбранного столбца"
},
"map": {
"overLimit": "Вы превысили лимит.",
@ -670,7 +670,7 @@
"showNullInCells": "Показывать NULL в ячейках",
"showNullInCellsDesc": "Отображайте тег 'NULL' в ячейках, содержащих значение NULL. Это помогает отличить ячейки, содержащие строку EMPTY.",
"showNullAndEmptyInFilter": "Показать NULL и EMPTY в фильтре",
"showNullAndEmptyInFilterDesc": "Enable 'additional' filters to differentiate fields containing NULL & Empty Strings. Default support for Blank treats both NULL & Empty strings alike.",
"showNullAndEmptyInFilterDesc": "Включите \"дополнительные\" фильтры для различения полей, содержащих NULL и пустые строки. По умолчанию поддержка Blank одинаково обрабатывает строки NULL и Empty.",
"deleteKanbanStackConfirmation": "Удаление этого стека также удалит опцию выбора `{stackToBeDeleted}` из `{groupingField}`. Записи переместятся в стек без категории.",
"computedFieldEditWarning": "Вычисляемое поле: содержимое доступно только для чтения. Используйте меню редактирования столбцов для изменения конфигурации",
"computedFieldDeleteWarning": "Вычисляемое поле: содержимое доступно только для чтения. Невозможно очистить содержимое.",
@ -726,7 +726,7 @@
"nameShouldStartWithAnAlphabetOr_": "Имя должно начинаться с алфавита или _",
"followingCharactersAreNotAllowed": "Нельзя использовать следующие символы",
"columnNameRequired": "Требуется название столбца",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"columnNameExceedsCharacters": "Длина имени колонки превышает максимальную {value} символов",
"projectNameExceeds50Characters": "Название проекта превышает 50 символов",
"projectNameCannotStartWithSpace": "Название проекта не может начинаться с пробела",
"requiredField": "Обязательное поле",

10
packages/nc-gui/lib/enums.ts

@ -105,3 +105,13 @@ export enum AutomationLogLevel {
ERROR = 'ERROR',
ALL = 'ALL',
}
export enum JobStatus {
COMPLETED = 'completed',
WAITING = 'waiting',
ACTIVE = 'active',
DELAYED = 'delayed',
FAILED = 'failed',
PAUSED = 'paused',
REFRESH = 'refresh',
}

16
packages/nc-gui/nuxt-shim.d.ts vendored

@ -1,6 +1,6 @@
import type { Api as BaseAPI } from 'nocodb-sdk'
import type { UseGlobalReturn } from './composables/useGlobal/types'
import type { NocoI18n } from './lib'
import type { JobStatus, NocoI18n } from './lib'
import type { TabType } from './composables'
declare module '#app/nuxt' {
@ -13,6 +13,20 @@ declare module '#app/nuxt' {
/** {@link import('./plugins/tele') Telemetry} Emit telemetry event */
$e: (event: string, data?: any) => void
$state: UseGlobalReturn
$jobs: {
subscribe(
job:
| {
id: string
name: string
}
| any,
subscribedCb?: () => void,
statusCb?: ((status: JobStatus, error?: any) => void) | undefined,
logCb?: ((data: { message: string }) => void) | undefined,
): void
getStatus(name: string, id: string): Promise<string>
}
}
}

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

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

2
packages/nc-gui/package.json

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

76
packages/nc-gui/pages/index/index/index.vue

@ -1,9 +1,11 @@
<script lang="ts" setup>
import { ProjectStatus } from 'nocodb-sdk'
import type { ProjectType } from 'nocodb-sdk'
import tinycolor from 'tinycolor2'
import { breakpointsTailwind } from '@vueuse/core'
import {
Empty,
JobStatus,
Modal,
computed,
definePageMeta,
@ -27,7 +29,7 @@ definePageMeta({
title: 'title.myProject',
})
const { $api, $e } = useNuxtApp()
const { $api, $e, $jobs } = useNuxtApp()
const { api, isLoading } = useApi()
@ -39,9 +41,17 @@ const filterQuery = ref('')
const projects = ref<ProjectType[]>()
const activePage = ref(1)
const pageChange = (p: number) => {
activePage.value = p
}
const loadProjects = async () => {
const lastActivePage = activePage.value
const response = await api.project.list({})
projects.value = response.list
activePage.value = lastActivePage
}
const filteredProjects = computed(
@ -74,6 +84,36 @@ const deleteProject = (project: ProjectType) => {
})
}
const duplicateProject = (project: ProjectType) => {
const isOpen = ref(true)
const { close } = useDialog(resolveComponent('DlgProjectDuplicate'), {
'modelValue': isOpen,
'project': project,
'onOk': async (jobData: { name: string; id: string }) => {
await loadProjects()
$jobs.subscribe({ name: jobData.name, id: jobData.id }, undefined, async (status: string) => {
if (status === JobStatus.COMPLETED) {
await loadProjects()
} else if (status === JobStatus.FAILED) {
message.error('Failed to duplicate project')
await loadProjects()
}
})
$e('a:project:duplicate')
},
'onUpdate:modelValue': closeDialog,
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
const handleProjectColor = async (projectId: string, color: string) => {
const tcolor = tinycolor(color)
@ -122,7 +162,7 @@ const getProjectPrimary = (project: ProjectType) => {
const customRow = (record: ProjectType) => ({
onClick: async () => {
await navigateTo(`/nc/${record.id}`)
if (record.status !== ProjectStatus.JOB) await navigateTo(`/nc/${record.id}`)
$e('a:project:open')
},
@ -196,7 +236,7 @@ const copyProjectMeta = async () => {
v-else
:custom-row="customRow"
:data-source="filteredProjects"
:pagination="{ position: ['bottomCenter'] }"
:pagination="{ 'position': ['bottomCenter'], 'current': activePage, 'onUpdate:current': pageChange }"
:table-layout="md ? 'auto' : 'fixed'"
>
<template #emptyText>
@ -249,8 +289,13 @@ const copyProjectMeta = async () => {
</a-menu>
</div>
<div
class="capitalize color-transition group-hover:text-primary !w-[400px] h-full overflow-hidden overflow-ellipsis whitespace-nowrap pl-2"
class="flex capitalize color-transition group-hover:text-primary !w-[400px] h-full overflow-hidden overflow-ellipsis whitespace-nowrap pl-2"
>
<component
:is="iconMap.reload"
v-if="record.status === ProjectStatus.JOB"
:class="{ 'animate-infinite animate-spin text-gray-500': record.status === ProjectStatus.JOB }"
/>
{{ text }}
</div>
</div>
@ -260,7 +305,7 @@ const copyProjectMeta = async () => {
<a-table-column key="id" :title="$t('labels.actions')" data-index="id">
<template #default="{ text, record }">
<div class="flex items-center gap-2">
<div v-if="record.status !== ProjectStatus.JOB" class="flex items-center gap-2">
<component
:is="iconMap.edit"
v-e="['c:project:edit:rename']"
@ -274,6 +319,25 @@ const copyProjectMeta = async () => {
:data-testid="`delete-project-${record.title}`"
@click.stop="deleteProject(record)"
/>
<a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-import-menu" @click.stop>
<GeneralIcon
icon="threeDotVertical"
class="nc-import-menu outline-0"
:data-testid="`p-three-dot-${record.title}`"
/>
<template #overlay>
<a-menu class="!py-0 rounded text-sm">
<a-menu-item key="duplicate" v-e="['c:project:duplicate']" @click.stop="duplicateProject(record)">
<div class="color-transition nc-project-menu-item group" :data-testid="`dupe-project-${record.title}`">
<GeneralIcon icon="copy" class="group-hover:text-accent" />
Duplicate
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</template>
</a-table-column>
@ -282,7 +346,7 @@ const copyProjectMeta = async () => {
</div>
</template>
<style scoped>
<style lang="scss" scoped>
.nc-action-btn {
@apply text-gray-500 group-hover:text-accent active:(ring ring-accent ring-opacity-100) cursor-pointer p-2 w-[30px] h-[30px] hover:bg-gray-300/50 rounded-full;
}

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

@ -0,0 +1,100 @@
import type { Socket } from 'socket.io-client'
import io from 'socket.io-client'
import { JobStatus, defineNuxtPlugin, useGlobal, watch } from '#imports'
export default defineNuxtPlugin(async (nuxtApp) => {
const { appInfo } = $(useGlobal())
let socket: Socket | null = null
let messageIndex = 0
const init = async (token: string) => {
try {
if (socket) socket.disconnect()
const url = new URL(appInfo.ncSiteUrl, window.location.href.split(/[?#]/)[0])
socket = io(`${url.href}jobs`, {
extraHeaders: { 'xc-auth': token },
})
socket.on('connect_error', (e) => {
console.error(e)
socket?.disconnect()
})
} catch {}
}
if (nuxtApp.$state.signedIn.value) {
await init(nuxtApp.$state.token.value)
}
const send = (name: string, data: any) => {
if (socket) {
const _id = messageIndex++
socket.emit(name, { _id, data })
return _id
}
}
const jobs = {
subscribe(
job: { id: string; name: string } | any,
subscribedCb?: () => void,
statusCb?: (status: JobStatus, data?: any) => void,
logCb?: (data: { message: string }) => void,
) {
const logFn = (data: { id: string; name: string; data: { message: string } }) => {
if (data.id === job.id) {
if (logCb) logCb(data.data)
}
}
const statusFn = (data: any) => {
if (data.id === job.id) {
if (statusCb) statusCb(data.status, data.data)
if (data.status === JobStatus.COMPLETED || data.status === JobStatus.FAILED) {
socket?.off('status', statusFn)
socket?.off('log', logFn)
}
}
}
const _id = send('subscribe', job)
const subscribeFn = (data: { _id: number; name: string; id: string }) => {
if (data._id === _id) {
if (data.id !== job.id || data.name !== job.name) {
job.id = data.id
job.name = data.name
}
if (subscribedCb) subscribedCb()
socket?.on('log', logFn)
socket?.on('status', statusFn)
socket?.off('subscribed', subscribeFn)
}
}
socket?.on('subscribed', subscribeFn)
},
getStatus(name: string, id: string): Promise<string> {
return new Promise((resolve) => {
if (socket) {
const _id = send('status', { name, id })
const tempFn = (data: any) => {
if (data._id === _id) {
resolve(data.status)
socket?.off('status', tempFn)
}
}
socket.on('status', tempFn)
}
})
},
}
watch((nuxtApp.$state as ReturnType<typeof useGlobal>).token, (newToken, oldToken) => {
if (newToken && newToken !== oldToken) init(newToken)
else if (!newToken) socket?.disconnect()
})
nuxtApp.provide('jobs', jobs)
})

13086
packages/nc-plugin/package-lock.json generated

File diff suppressed because it is too large Load Diff

1
packages/nc-plugin/package.json

@ -58,6 +58,7 @@
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@types/node": "^18.11.9",
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"ava": "^5.2.0",

16
packages/nc-plugin/src/lib/IStorageAdapterV2.ts

@ -1,11 +1,27 @@
import { Readable } from 'stream';
import IStorageAdapter from './IStorageAdapter';
/*
#ref: https://github.com/nocodb/nocodb/pull/5608
fileCreateByStream: write file from a readable stream to the storage
fileReadByStream: read file from the storage to a readable stream
getDirectoryList: get files available in a directory
These methods are added to support export/import without buffering all the data in memory.
The methods are only available for Local storage adapter for now, and will be implemented for other adapters later.
The methods are not used in current codebase, but will be used in future.
*/
export default interface IStorageAdapterV2 extends IStorageAdapter {
fileCreateByUrl(
destPath: string,
url: string,
fileMeta?: FileMeta
): Promise<any>;
fileCreateByStream(destPath: string, readStream: Readable): Promise<void>;
fileReadByStream(key: string): Promise<Readable>;
getDirectoryList(path: string): Promise<string[]>;
}
interface FileMeta {

1
packages/nocodb-legacy/.gitignore vendored

@ -20,3 +20,4 @@ test_meta.db
test_sakila.db
test_sakila_*.db
.env
export/**

105
packages/nocodb-legacy/package-lock.json generated

@ -68,7 +68,7 @@
"nanoid": "^3.1.20",
"nc-help": "0.2.87",
"nc-lib-gui": "0.106.1",
"nc-plugin": "0.1.2",
"nc-plugin": "file:../nc-plugin",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",
@ -155,8 +155,42 @@
"vuedraggable": "^2.24.3"
}
},
"../nc-plugin": {
"version": "0.1.3",
"license": "AGPL-3.0-or-later",
"dependencies": {
"nc-common": "0.0.6"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@types/node": "^18.11.9",
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"ava": "^5.2.0",
"codecov": "^3.5.0",
"cspell": "^4.1.0",
"cz-conventional-changelog": "^3.3.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",
"gh-pages": "^3.1.0",
"npm-run-all": "^4.1.5",
"nyc": "^15.1.0",
"open-cli": "^7.1.0",
"prettier": "^2.1.1",
"standard-version": "^9.0.0",
"ts-node": "^9.0.0",
"typedoc": "^0.19.0",
"typescript": "^4.0.2"
},
"engines": {
"node": ">=10"
}
},
"../nocodb-sdk": {
"version": "0.106.1",
"version": "0.107.0-beta.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@ -614,14 +648,6 @@
"node": ">=6.9.0"
}
},
"node_modules/@bitauth/libauth": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/@bitauth/libauth/-/libauth-1.19.1.tgz",
"integrity": "sha512-R524tD5VwOt3QRHr7N518nqTVR/HKgfWL4LypekcGuNQN8R4PWScvuRcRzrY39A28kLztMv+TJdiKuMNbkU1ug==",
"engines": {
"node": ">=8.9"
}
},
"node_modules/@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@ -11302,14 +11328,6 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
"node_modules/nc-common": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/nc-common/-/nc-common-0.0.6.tgz",
"integrity": "sha512-3AryS9uwa5NfISLxMciUonrH7YfXp+nlahB9T7girXIsLQrmwX4MdnuKs32akduCOGpKmjTJSWmATULbuMkbfw==",
"engines": {
"node": ">=10"
}
},
"node_modules/nc-help": {
"version": "0.2.87",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.87.tgz",
@ -11338,16 +11356,8 @@
}
},
"node_modules/nc-plugin": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/nc-plugin/-/nc-plugin-0.1.2.tgz",
"integrity": "sha512-9NZJPIgD6r3GnCNJelfZFga+RQPDuHZCoFus2qaZsNdbJEP7LSTOeZ1Pw8l89fvVbp0VFLO3Bcg8ggJppKt8JQ==",
"dependencies": {
"@bitauth/libauth": "^1.17.1",
"nc-common": "0.0.6"
},
"engines": {
"node": ">=10"
}
"resolved": "../nc-plugin",
"link": true
},
"node_modules/ncp": {
"version": "2.0.0",
@ -19575,11 +19585,6 @@
"regenerator-runtime": "^0.13.11"
}
},
"@bitauth/libauth": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/@bitauth/libauth/-/libauth-1.19.1.tgz",
"integrity": "sha512-R524tD5VwOt3QRHr7N518nqTVR/HKgfWL4LypekcGuNQN8R4PWScvuRcRzrY39A28kLztMv+TJdiKuMNbkU1ug=="
},
"@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@ -28023,11 +28028,6 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
"nc-common": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/nc-common/-/nc-common-0.0.6.tgz",
"integrity": "sha512-3AryS9uwa5NfISLxMciUonrH7YfXp+nlahB9T7girXIsLQrmwX4MdnuKs32akduCOGpKmjTJSWmATULbuMkbfw=="
},
"nc-help": {
"version": "0.2.87",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.87.tgz",
@ -28050,12 +28050,31 @@
}
},
"nc-plugin": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/nc-plugin/-/nc-plugin-0.1.2.tgz",
"integrity": "sha512-9NZJPIgD6r3GnCNJelfZFga+RQPDuHZCoFus2qaZsNdbJEP7LSTOeZ1Pw8l89fvVbp0VFLO3Bcg8ggJppKt8JQ==",
"version": "file:../nc-plugin",
"requires": {
"@bitauth/libauth": "^1.17.1",
"nc-common": "0.0.6"
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@types/node": "^18.11.9",
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"ava": "^5.2.0",
"codecov": "^3.5.0",
"cspell": "^4.1.0",
"cz-conventional-changelog": "^3.3.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",
"gh-pages": "^3.1.0",
"nc-common": "0.0.6",
"npm-run-all": "^4.1.5",
"nyc": "^15.1.0",
"open-cli": "^7.1.0",
"prettier": "^2.1.1",
"standard-version": "^9.0.0",
"ts-node": "^9.0.0",
"typedoc": "^0.19.0",
"typescript": "^4.0.2"
}
},
"ncp": {

2
packages/nocodb-legacy/package.json

@ -110,7 +110,7 @@
"nanoid": "^3.1.20",
"nc-help": "0.2.87",
"nc-lib-gui": "0.106.1",
"nc-plugin": "0.1.2",
"nc-plugin": "file:../nc-plugin",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",

19
packages/nocodb-legacy/src/lib/controllers/exportImport/export.ctl.ts

@ -0,0 +1,19 @@
import { Router } from 'express';
import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw';
import { exportService } from '../../services';
import type { Request, Response } from 'express';
export async function exportBase(req: Request, res: Response) {
res.json(
await exportService.exportBase({ baseId: req.params.baseId, path: req.body.path })
);
}
const router = Router({ mergeParams: true });
router.post(
'/api/v1/db/meta/export/:projectId/:baseId',
ncMetaAclMw(exportBase, 'exportBase')
);
export default router;

39
packages/nocodb-legacy/src/lib/controllers/exportImport/import.ctl.ts

@ -0,0 +1,39 @@
import { Router } from 'express';
import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw';
import { importService } from '../../services';
import type { Request, Response } from 'express';
export async function importModels(req: Request, res: Response) {
const { body, ...rest } = req;
res.json(
await importService.importModels({
user: (req as any).user,
projectId: req.params.projectId,
baseId: req.params.baseId,
data: Array.isArray(body) ? body : body.models,
req: rest,
})
);
}
export async function importBase(req: Request, res: Response) {
const { body, ...rest } = req;
res.json(
await importService.importBase({
user: (req as any).user,
projectId: req.params.projectId,
baseId: req.params.baseId,
src: body.src,
req: rest,
})
);
}
const router = Router({ mergeParams: true });
router.post(
'/api/v1/db/meta/import/:projectId/:baseId',
ncMetaAclMw(importBase, 'importBase')
);
export default router;

7
packages/nocodb-legacy/src/lib/controllers/exportImport/index.ts

@ -0,0 +1,7 @@
import exportController from './export.ctl';
import importController from './import.ctl';
export default {
exportController,
importController,
};

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

@ -200,7 +200,7 @@ class SqliteClient extends KnexClient {
table.integer('status').nullable();
table.dateTime('created');
table.timestamps();
}
},
);
log.debug('Table created:', `${args.tn}`, data);
} else {
@ -295,7 +295,7 @@ class SqliteClient extends KnexClient {
try {
const response = await this.sqlClient.raw(
`SELECT name as tn FROM sqlite_master where type = 'table'`
`SELECT name as tn FROM sqlite_master where type = 'table'`,
);
result.data.list = [];
@ -359,7 +359,7 @@ class SqliteClient extends KnexClient {
try {
const response = await this.sqlClient.raw(
`PRAGMA table_info("${args.tn}")`
`PRAGMA table_info("${args.tn}")`,
);
const triggerList = (await this.triggerList(args)).data.list;
@ -409,7 +409,7 @@ class SqliteClient extends KnexClient {
response[i].not_nullable = response[i].notnull === 1;
response[i].rqd = response[i].notnull === 1;
response[i].cdf = response[i].dflt_value;
response[i].pk = response[i].pk === 1;
response[i].pk = response[i].pk > 0;
response[i].cop = response[i].cid;
// https://stackoverflow.com/a/7906029
@ -420,7 +420,8 @@ class SqliteClient extends KnexClient {
response[i].dtxs = '';
response[i].au = !!triggerList.find(
({ trigger }) => trigger === `xc_trigger_${args.tn}_${response[i].cn}`
({ trigger }) =>
trigger === `xc_trigger_${args.tn}_${response[i].cn}`,
);
}
@ -466,7 +467,7 @@ class SqliteClient extends KnexClient {
// PRAGMA index_xinfo('idx_fk_original_language_id');
const response = await this.sqlClient.raw(
`PRAGMA index_list("${args.tn}")`
`PRAGMA index_list("${args.tn}")`,
);
const rows = [];
@ -478,7 +479,7 @@ class SqliteClient extends KnexClient {
response[i].unique = response[i].unique === 1 ? 1 : 0;
const colsInIndex = await this.sqlClient.raw(
`PRAGMA index_info('${response[i].key_name}')`
`PRAGMA index_info('${response[i].key_name}')`,
);
if (colsInIndex.length === 1) {
@ -531,7 +532,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`PRAGMA foreign_key_list('${args.tn}')`
`PRAGMA foreign_key_list('${args.tn}')`,
);
for (let i = 0; i < response.length; ++i) {
@ -582,7 +583,7 @@ class SqliteClient extends KnexClient {
for (let i = 0; i < tables.length; ++i) {
const response = await this.sqlClient.raw(
`PRAGMA foreign_key_list('${tables[i].tn}')`
`PRAGMA foreign_key_list('${tables[i].tn}')`,
);
for (let j = 0; j < response.length; ++j) {
@ -633,7 +634,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`select *, name as trigger_name from sqlite_master where type = 'trigger' and tbl_name='${args.tn}';`
`select *, name as trigger_name from sqlite_master where type = 'trigger' and tbl_name='${args.tn}';`,
);
for (let i = 0; i < response.length; ++i) {
@ -676,7 +677,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`show function status where db='${args.databaseName}'`
`show function status where db='${args.databaseName}'`,
);
if (response.length === 2) {
@ -730,7 +731,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`show procedure status where db='${args.databaseName}'`
`show procedure status where db='${args.databaseName}'`,
);
if (response.length === 2) {
@ -775,7 +776,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`SELECT * FROM sqlite_master WHERE type = 'view'`
`SELECT * FROM sqlite_master WHERE type = 'view'`,
);
for (let i = 0; i < response.length; ++i) {
@ -813,7 +814,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`SHOW CREATE FUNCTION ${args.function_name};`
`SHOW CREATE FUNCTION ${args.function_name};`,
);
if (response.length === 2) {
@ -865,7 +866,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`show create procedure ${args.procedure_name};`
`show create procedure ${args.procedure_name};`,
);
if (response.length === 2) {
@ -911,7 +912,7 @@ class SqliteClient extends KnexClient {
try {
const response = await this.sqlClient.raw(
`SELECT * FROM sqlite_master WHERE type = 'view' AND name = '${args.view_name}'`
`SELECT * FROM sqlite_master WHERE type = 'view' AND name = '${args.view_name}'`,
);
for (let i = 0; i < response.length; ++i) {
@ -938,7 +939,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`SHOW FULL TABLES IN ${args.databaseName} WHERE TABLE_TYPE LIKE 'VIEW';`
`SHOW FULL TABLES IN ${args.databaseName} WHERE TABLE_TYPE LIKE 'VIEW';`,
);
if (response.length === 2) {
@ -970,7 +971,7 @@ class SqliteClient extends KnexClient {
log.api(`${_func}:args:`, args);
const rows = await this.sqlClient.raw(
`create database ${args.database_name}`
`create database ${args.database_name}`,
);
return rows;
}
@ -981,7 +982,7 @@ class SqliteClient extends KnexClient {
log.api(`${_func}:args:`, args);
const rows = await this.sqlClient.raw(
`drop database ${args.database_name}`
`drop database ${args.database_name}`,
);
return rows;
}
@ -1011,7 +1012,7 @@ class SqliteClient extends KnexClient {
log.api(`${_func}:args:`, args);
const rows = await this.sqlClient.raw(
`DROP FUNCTION IF EXISTS ${args.function_name}`
`DROP FUNCTION IF EXISTS ${args.function_name}`,
);
return rows;
}
@ -1022,7 +1023,7 @@ class SqliteClient extends KnexClient {
log.api(`${_func}:args:`, args);
const rows = await this.sqlClient.raw(
`DROP PROCEDURE IF EXISTS ${args.procedure_name}`
`DROP PROCEDURE IF EXISTS ${args.procedure_name}`,
);
return rows;
}
@ -1042,7 +1043,7 @@ class SqliteClient extends KnexClient {
this._version = result.data.object;
log.debug(
`Version was empty for ${args.func}: population version for database as`,
this._version
this._version,
);
}
@ -1073,7 +1074,7 @@ class SqliteClient extends KnexClient {
log.api(`${func}:args:`, args);
try {
const rows = await this.sqlClient.raw(
`CREATE TRIGGER \`${args.function_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`
`CREATE TRIGGER \`${args.function_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`,
);
result.data.list = rows;
} catch (e) {
@ -1101,7 +1102,7 @@ class SqliteClient extends KnexClient {
try {
await this.sqlClient.raw(`DROP TRIGGER ${args.function_name}`);
const rows = await this.sqlClient.raw(
`CREATE TRIGGER \`${args.function_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`
`CREATE TRIGGER \`${args.function_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`,
);
result.data.list = rows;
} catch (e) {
@ -1128,7 +1129,7 @@ class SqliteClient extends KnexClient {
log.api(`${func}:args:`, args);
try {
const rows = await this.sqlClient.raw(
`CREATE TRIGGER \`${args.procedure_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`
`CREATE TRIGGER \`${args.procedure_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`,
);
result.data.list = rows;
} catch (e) {
@ -1156,7 +1157,7 @@ class SqliteClient extends KnexClient {
try {
await this.sqlClient.raw(`DROP TRIGGER ${args.procedure_name}`);
const rows = await this.sqlClient.raw(
`CREATE TRIGGER \`${args.procedure_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`
`CREATE TRIGGER \`${args.procedure_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`,
);
result.data.list = rows;
} catch (e) {
@ -1216,7 +1217,7 @@ class SqliteClient extends KnexClient {
try {
await this.sqlClient.raw(`DROP TRIGGER ${args.trigger_name}`);
await this.sqlClient.raw(
`CREATE TRIGGER \`${args.trigger_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`
`CREATE TRIGGER \`${args.trigger_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`,
);
const upQuery = `DROP TRIGGER ${args.trigger_name};\nCREATE TRIGGER \`${args.trigger_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`;
@ -1507,13 +1508,13 @@ class SqliteClient extends KnexClient {
args.table,
args.columns[i],
oldColumn,
upQuery
upQuery,
);
downQuery += this.alterTableAddColumn(
args.table,
oldColumn,
args.columns[i],
downQuery
downQuery,
);
} else if (args.columns[i].altered & 2 || args.columns[i].altered & 8) {
// col edit
@ -1521,7 +1522,7 @@ class SqliteClient extends KnexClient {
args.table,
args.columns[i],
oldColumn,
upQuery
upQuery,
);
downQuery += ';';
// downQuery += this.alterTableChangeColumn(
@ -1537,7 +1538,7 @@ class SqliteClient extends KnexClient {
args.table,
args.columns[i],
oldColumn,
upQuery
upQuery,
);
downQuery += ';';
// downQuery += alterTableRemoveColumn(
@ -1553,7 +1554,7 @@ class SqliteClient extends KnexClient {
const pkQuery = this.alterTablePK(
args.columns,
args.originalColumns,
upQuery
upQuery,
);
await this.sqlClient.raw('PRAGMA foreign_keys = OFF;');
@ -1572,7 +1573,7 @@ class SqliteClient extends KnexClient {
if (pkQuery) {
await trx.schema.alterTable(args.table, (table) => {
for (const pk of pkQuery.oldPks.filter(
(el) => !pkQuery.newPks.includes(el)
(el) => !pkQuery.newPks.includes(el),
)) {
table.dropPrimary(pk);
}
@ -1858,7 +1859,7 @@ class SqliteClient extends KnexClient {
/* Filter relations for current table */
if (args.tn) {
relations = relations.filter(
(r) => r.tn === args.tn || r.rtn === args.tn
(r) => r.tn === args.tn || r.rtn === args.tn,
);
}
@ -1867,7 +1868,7 @@ class SqliteClient extends KnexClient {
let columns: any = await this.columnList({ tn: tables[i].tn });
columns = columns.data.list;
console.log(
`Sequelize model created: ${tables[i].tn}(${columns.length})\n`
`Sequelize model created: ${tables[i].tn}(${columns.length})\n`,
);
// let SqliteSequelizeRender = require('./SqliteSequelizeRender');
@ -1967,7 +1968,7 @@ class SqliteClient extends KnexClient {
query += this.genQuery(
`ALTER TABLE ?? DROP COLUMN ??`,
[t, n.cn],
shouldSanitize
shouldSanitize,
);
return query;
}
@ -2009,14 +2010,14 @@ class SqliteClient extends KnexClient {
const backupOldColumnQuery = this.genQuery(
`ALTER TABLE ?? RENAME COLUMN ?? TO ??;`,
[t, o.cn, `${o.cno}_nc_${suffix}`],
shouldSanitize
shouldSanitize,
);
let addNewColumnQuery = '';
addNewColumnQuery += this.genQuery(
` ADD ?? ${n.dt}`,
[n.cn],
shouldSanitize
shouldSanitize,
);
addNewColumnQuery += n.dtxp && n.dt !== 'text' ? `(${n.dtxp})` : '';
addNewColumnQuery += n.cdf
@ -2028,19 +2029,19 @@ class SqliteClient extends KnexClient {
addNewColumnQuery = this.genQuery(
`ALTER TABLE ?? ${addNewColumnQuery};`,
[t],
shouldSanitize
shouldSanitize,
);
const updateNewColumnQuery = this.genQuery(
`UPDATE ?? SET ?? = ??;`,
[t, n.cn, `${o.cno}_nc_${suffix}`],
shouldSanitize
shouldSanitize,
);
const dropOldColumnQuery = this.genQuery(
`ALTER TABLE ?? DROP COLUMN ??;`,
[t, `${o.cno}_nc_${suffix}`],
shouldSanitize
shouldSanitize,
);
query = `${backupOldColumnQuery}${addNewColumnQuery}${updateNewColumnQuery}${dropOldColumnQuery}`;
@ -2095,12 +2096,12 @@ class SqliteClient extends KnexClient {
try {
const tables = await this.sqlClient.raw(
`SELECT name FROM sqlite_master WHERE type='table';`
`SELECT name FROM sqlite_master WHERE type='table';`,
);
let count = 0;
for (const tb of tables) {
const tmp = await this.sqlClient.raw(
`SELECT COUNT(*) as ct FROM '${tb.name}';`
`SELECT COUNT(*) as ct FROM '${tb.name}';`,
);
if (tmp && tmp.length) {
count += tmp[0].ct;

35
packages/nocodb-legacy/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts

@ -2066,13 +2066,18 @@ class BaseModelSqlv2 {
{
chunkSize: _chunkSize = 100,
cookie,
foreign_key_checks = true,
raw = false,
}: {
chunkSize?: number;
cookie?: any;
foreign_key_checks?: boolean;
raw?: boolean;
} = {}
) {
try {
const insertDatas = await Promise.all(
// TODO: ag column handling for raw bulk insert
const insertDatas = raw ? datas : await Promise.all(
datas.map(async (d) => {
await populatePk(this.model, d);
return this.model.mapAliasToColumn(d);
@ -2081,27 +2086,49 @@ class BaseModelSqlv2 {
// await this.beforeInsertb(insertDatas, null);
if (!raw) {
for (const data of datas) {
await this.validate(data);
}
}
// fallbacks to `10` if database client is sqlite
// to avoid `too many SQL variables` error
// refer : https://www.sqlite.org/limits.html
const chunkSize = this.isSqlite ? 10 : _chunkSize;
const trx = await this.dbDriver.transaction();
if (!foreign_key_checks) {
if (this.isPg) {
await trx.raw('set session_replication_role to replica;');
} else if (this.isMySQL) {
await trx.raw('SET foreign_key_checks = 0;');
}
}
const response =
this.isPg || this.isMssql
? await this.dbDriver
? await trx
.batchInsert(this.tnPath, insertDatas, chunkSize)
.returning(this.model.primaryKey?.column_name)
: await this.dbDriver.batchInsert(
: await trx.batchInsert(
this.tnPath,
insertDatas,
chunkSize
);
await this.afterBulkInsert(insertDatas, this.dbDriver, cookie);
if (!foreign_key_checks) {
if (this.isPg) {
await trx.raw('set session_replication_role to origin;');
} else if (this.isMySQL) {
await trx.raw('SET foreign_key_checks = 1;');
}
}
await trx.commit();
if (!raw) await this.afterBulkInsert(insertDatas, this.dbDriver, cookie);
return response;
} catch (e) {

3
packages/nocodb-legacy/src/lib/meta/api/index.ts

@ -50,6 +50,7 @@ import {
import swaggerController from '../../controllers/apiDocs';
import { importController, syncSourceController } from '../../controllers/sync';
import mapViewController from '../../controllers/views/mapView.ctl';
import exportImportController from '../../controllers/exportImport'
import type { Socket } from 'socket.io';
import type { Router } from 'express';
@ -103,6 +104,8 @@ export default function (router: Router, server) {
router.use(syncSourceController);
router.use(kanbanViewController);
router.use(mapViewController);
router.use(exportImportController.exportController);
router.use(exportImportController.importController);
userController(router);

20
packages/nocodb-legacy/src/lib/plugins/backblaze/Backblaze.ts

@ -7,6 +7,7 @@ import {
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
export default class Backblaze implements IStorageAdapterV2 {
private s3Client: AWS.S3;
@ -73,11 +74,26 @@ export default class Backblaze implements IStorageAdapterV2 {
resolve(data.Location);
}
});
}
},
);
});
}
// TODO - implement
fileCreateByStream(_key: string, _stream: Readable): Promise<void> {
return Promise.resolve(undefined);
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
patchRegion(region: string): string {
// in v0.0.1, we constructed the endpoint with `region = s3.us-west-001`
// in v0.0.2, `region` would be `us-west-001`
@ -116,7 +132,7 @@ export default class Backblaze implements IStorageAdapterV2 {
s3Options.secretAccessKey = this.input.access_secret;
s3Options.endpoint = new AWS.Endpoint(
`s3.${s3Options.region}.backblazeb2.com`
`s3.${s3Options.region}.backblazeb2.com`,
);
this.s3Client = new AWS.S3(s3Options);

18
packages/nocodb-legacy/src/lib/plugins/gcs/Gcs.ts

@ -7,6 +7,7 @@ import {
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
import type { StorageOptions } from '@google-cloud/storage';
export default class Gcs implements IStorageAdapterV2 {
@ -122,8 +123,23 @@ export default class Gcs implements IStorageAdapterV2 {
.save(body)
.then((res) => resolve(res))
.catch(reject);
}
},
);
});
}
// TODO - implement
fileCreateByStream(_key: string, _stream: Readable): Promise<void> {
return Promise.resolve(undefined);
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
}

20
packages/nocodb-legacy/src/lib/plugins/linode/LinodeObjectStorage.ts

@ -7,6 +7,7 @@ import {
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
export default class LinodeObjectStorage implements IStorageAdapterV2 {
private s3Client: AWS.S3;
@ -73,11 +74,26 @@ export default class LinodeObjectStorage implements IStorageAdapterV2 {
resolve(data.Location);
}
});
}
},
);
});
}
// TODO - implement
fileCreateByStream(_key: string, _stream: Readable): Promise<void> {
return Promise.resolve(undefined);
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
@ -106,7 +122,7 @@ export default class LinodeObjectStorage implements IStorageAdapterV2 {
s3Options.secretAccessKey = this.input.access_secret;
s3Options.endpoint = new AWS.Endpoint(
`${this.input.region}.linodeobjects.com`
`${this.input.region}.linodeobjects.com`,
);
this.s3Client = new AWS.S3(s3Options);

22
packages/nocodb-legacy/src/lib/plugins/mino/Minio.ts

@ -7,6 +7,7 @@ import {
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
export default class Minio implements IStorageAdapterV2 {
private minioClient: MinioClient;
@ -39,7 +40,7 @@ export default class Minio implements IStorageAdapterV2 {
resolve(
`http${this.input.useSSL ? 's' : ''}://${this.input.endPoint}:${
this.input.port
}/${this.input.bucket}/${key}`
}/${this.input.bucket}/${key}`,
);
})
.catch(reject);
@ -123,12 +124,27 @@ export default class Minio implements IStorageAdapterV2 {
resolve(
`http${this.input.useSSL ? 's' : ''}://${this.input.endPoint}:${
this.input.port
}/${this.input.bucket}/${key}`
}/${this.input.bucket}/${key}`,
);
})
.catch(reject);
}
},
);
});
}
// TODO - implement
fileCreateByStream(_key: string, _stream: Readable): Promise<void> {
return Promise.resolve(undefined);
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
}

20
packages/nocodb-legacy/src/lib/plugins/ovhCloud/OvhCloud.ts

@ -7,6 +7,7 @@ import {
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
export default class OvhCloud implements IStorageAdapterV2 {
private s3Client: AWS.S3;
@ -73,11 +74,26 @@ export default class OvhCloud implements IStorageAdapterV2 {
resolve(data.Location);
}
});
}
},
);
});
}
// TODO - implement
fileCreateByStream(_key: string, _stream: Readable): Promise<void> {
return Promise.resolve(undefined);
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
@ -106,7 +122,7 @@ export default class OvhCloud implements IStorageAdapterV2 {
s3Options.secretAccessKey = this.input.access_secret;
s3Options.endpoint = new AWS.Endpoint(
`s3.${this.input.region}.cloud.ovh.net`
`s3.${this.input.region}.cloud.ovh.net`,
);
this.s3Client = new AWS.S3(s3Options);

18
packages/nocodb-legacy/src/lib/plugins/s3/S3.ts

@ -7,6 +7,7 @@ import {
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
export default class S3 implements IStorageAdapterV2 {
private s3Client: AWS.S3;
@ -72,11 +73,26 @@ export default class S3 implements IStorageAdapterV2 {
resolve(data.Location);
}
});
}
},
);
});
}
// TODO - implement
fileCreateByStream(_key: string, _stream: Readable): Promise<void> {
return Promise.resolve(undefined);
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}

16
packages/nocodb-legacy/src/lib/plugins/scaleway/ScalewayObjectStorage.ts

@ -7,6 +7,7 @@ import {
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import { Readable } from 'stream';
export default class ScalewayObjectStorage implements IStorageAdapterV2 {
private s3Client: AWS.S3;
@ -127,4 +128,19 @@ export default class ScalewayObjectStorage implements IStorageAdapterV2 {
);
});
}
// TODO - implement
fileCreateByStream(_key: string, _stream: Readable): Promise<void> {
return Promise.resolve(undefined);
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
}

20
packages/nocodb-legacy/src/lib/plugins/spaces/Spaces.ts

@ -7,6 +7,7 @@ import {
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
export default class Spaces implements IStorageAdapterV2 {
private s3Client: AWS.S3;
@ -73,11 +74,26 @@ export default class Spaces implements IStorageAdapterV2 {
resolve(data.Location);
}
});
}
},
);
});
}
// TODO - implement
fileCreateByStream(_key: string, _stream: Readable): Promise<void> {
return Promise.resolve(undefined);
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
@ -114,7 +130,7 @@ export default class Spaces implements IStorageAdapterV2 {
s3Options.secretAccessKey = this.input.access_secret;
s3Options.endpoint = new AWS.Endpoint(
`${this.input.region || 'nyc3'}.digitaloceanspaces.com`
`${this.input.region || 'nyc3'}.digitaloceanspaces.com`,
);
this.s3Client = new AWS.S3(s3Options);

18
packages/nocodb-legacy/src/lib/plugins/upcloud/UpoCloud.ts

@ -7,6 +7,7 @@ import {
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
export default class UpoCloud implements IStorageAdapterV2 {
private s3Client: AWS.S3;
@ -73,11 +74,26 @@ export default class UpoCloud implements IStorageAdapterV2 {
resolve(data.Location);
}
});
}
},
);
});
}
// TODO - implement
fileCreateByStream(_key: string, _stream: Readable): Promise<void> {
return Promise.resolve(undefined);
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}

18
packages/nocodb-legacy/src/lib/plugins/vultr/Vultr.ts

@ -7,6 +7,7 @@ import {
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
export default class Vultr implements IStorageAdapterV2 {
private s3Client: AWS.S3;
@ -73,11 +74,26 @@ export default class Vultr implements IStorageAdapterV2 {
resolve(data.Location);
}
});
}
},
);
});
}
// TODO - implement
fileCreateByStream(_key: string, _stream: Readable): Promise<void> {
return Promise.resolve(undefined);
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}

5
packages/nocodb-legacy/src/lib/services/dbData/bulkData.ts

@ -38,12 +38,15 @@ export async function bulkDataInsert(
param: PathParams & {
body: any;
cookie: any;
chunkSize?: number;
foreign_key_checks?: boolean;
raw?: boolean;
}
) {
return await executeBulkOperation({
...param,
operation: 'bulkInsert',
options: [param.body, { cookie: param.cookie }],
options: [param.body, { cookie: param.cookie, foreign_key_checks: param.foreign_key_checks, chunkSize: param.chunkSize, raw: param.raw }],
});
}

493
packages/nocodb-legacy/src/lib/services/exportImport/export.svc.ts

@ -0,0 +1,493 @@
import { NcError } from './../../meta/helpers/catchError';
import { UITypes, ViewTypes } from 'nocodb-sdk';
import { Project, Base, Model, View, LinkToAnotherRecordColumn } from '../../models';
import { dataService } from '..';
import { getViewAndModelByAliasOrId } from '../dbData/helpers';
import { Readable } from 'stream';
import NcPluginMgrv2 from '../../meta/helpers/NcPluginMgrv2';
import { unparse } from 'papaparse';
import { IStorageAdapterV2 } from 'nc-plugin';
/*
{
"entity": "project",
"bases": [
### current scope
{
"entity": "base",
"models": [
{
"entity": "model",
"model": {},
"views": []
}
]
}
### end current scope
]
}
*/
async function generateBaseIdMap(base: Base, idMap: Map<string, string>) {
idMap.set(base.project_id, base.project_id);
idMap.set(base.id, `${base.project_id}::${base.id}`);
const models = await base.getModels();
for (const md of models) {
idMap.set(md.id, `${base.project_id}::${base.id}::${md.id}`);
await md.getColumns();
for (const column of md.columns) {
idMap.set(column.id, `${idMap.get(md.id)}::${column.id}`);
}
}
return models;
}
async function serializeModels(param: { modelId: string[] }) {
const serializedModels = [];
// db id to structured id
const idMap = new Map<string, string>();
const projects: Project[] = []
const bases: Base[] = []
const modelsMap = new Map<string, Model[]>();
for (const modelId of param.modelId) {
const model = await Model.get(modelId);
if (!model) return NcError.badRequest(`Model not found for id '${modelId}'`);
const fndProject = projects.find(p => p.id === model.project_id)
const project = fndProject || await Project.get(model.project_id);
const fndBase = bases.find(b => b.id === model.base_id)
const base = fndBase || await Base.get(model.base_id);
if (!fndProject) projects.push(project);
if (!fndBase) bases.push(base);
if (!modelsMap.has(base.id)) {
modelsMap.set(base.id, await generateBaseIdMap(base, idMap));
}
await model.getColumns();
await model.getViews();
for (const column of model.columns) {
await column.getColOptions();
if (column.colOptions) {
for (const [k, v] of Object.entries(column.colOptions)) {
switch (k) {
case 'fk_mm_child_column_id':
case 'fk_mm_parent_column_id':
case 'fk_mm_model_id':
case 'fk_parent_column_id':
case 'fk_child_column_id':
case 'fk_related_model_id':
case 'fk_relation_column_id':
case 'fk_lookup_column_id':
case 'fk_rollup_column_id':
column.colOptions[k] = idMap.get(v as string);
break;
case 'options':
for (const o of column.colOptions['options']) {
delete o.id;
delete o.fk_column_id;
}
break;
case 'formula':
column.colOptions[k] = column.colOptions[k].replace(/(?<=\{\{).*?(?=\}\})/gm, (match) => idMap.get(match));
break;
case 'id':
case 'created_at':
case 'updated_at':
case 'fk_column_id':
delete column.colOptions[k];
break;
}
}
}
}
for (const view of model.views) {
idMap.set(view.id, `${idMap.get(model.id)}::${view.id}`);
await view.getColumns();
await view.getFilters();
await view.getSorts();
if (view.filter) {
const export_filters = []
for (const fl of view.filter.children) {
const tempFl = {
id: `${idMap.get(view.id)}::${fl.id}`,
fk_column_id: idMap.get(fl.fk_column_id),
fk_parent_id: fl.fk_parent_id,
is_group: fl.is_group,
logical_op: fl.logical_op,
comparison_op: fl.comparison_op,
comparison_sub_op: fl.comparison_sub_op,
value: fl.value,
}
if (tempFl.is_group) {
delete tempFl.comparison_op;
delete tempFl.comparison_sub_op;
delete tempFl.value;
}
export_filters.push(tempFl)
}
view.filter.children = export_filters;
}
if (view.sorts) {
const export_sorts = []
for (const sr of view.sorts) {
const tempSr = {
fk_column_id: idMap.get(sr.fk_column_id),
direction: sr.direction,
}
export_sorts.push(tempSr)
}
view.sorts = export_sorts;
}
if (view.view) {
for (const [k, v] of Object.entries(view.view)) {
switch (k) {
case 'fk_column_id':
case 'fk_cover_image_col_id':
case 'fk_grp_col_id':
view.view[k] = idMap.get(v as string);
break;
case 'meta':
if (view.type === ViewTypes.KANBAN) {
const meta = JSON.parse(view.view.meta as string) as Record<string, any>;
for (const [k, v] of Object.entries(meta)) {
const colId = idMap.get(k as string);
for (const op of v) {
op.fk_column_id = idMap.get(op.fk_column_id);
delete op.id;
}
meta[colId] = v;
delete meta[k];
}
view.view.meta = meta;
}
break;
case 'created_at':
case 'updated_at':
case 'fk_view_id':
case 'project_id':
case 'base_id':
case 'uuid':
delete view.view[k];
break;
}
}
}
}
serializedModels.push({
entity: 'model',
model: {
id: idMap.get(model.id),
prefix: project.prefix,
title: model.title,
table_name: clearPrefix(model.table_name, project.prefix),
meta: model.meta,
columns: model.columns.map((column) => ({
id: idMap.get(column.id),
ai: column.ai,
column_name: column.column_name,
cc: column.cc,
cdf: column.cdf,
meta: column.meta,
pk: column.pk,
order: column.order,
rqd: column.rqd,
system: column.system,
uidt: column.uidt,
title: column.title,
un: column.un,
unique: column.unique,
colOptions: column.colOptions,
})),
},
views: model.views.map((view) => ({
id: idMap.get(view.id),
is_default: view.is_default,
type: view.type,
meta: view.meta,
order: view.order,
title: view.title,
show: view.show,
show_system_fields: view.show_system_fields,
filter: view.filter,
sorts: view.sorts,
lock_type: view.lock_type,
columns: view.columns.map((column) => {
const {
id,
fk_view_id,
fk_column_id,
project_id,
base_id,
created_at,
updated_at,
uuid,
...rest
} = column as any;
return {
fk_column_id: idMap.get(fk_column_id),
...rest,
};
}),
view: view.view,
})),
});
}
return serializedModels;
}
async function exportModelData(param: {
storageAdapter: IStorageAdapterV2;
path: string;
projectId: string;
modelId: string;
viewId?: string;
}) {
const { model, view } = await getViewAndModelByAliasOrId({
projectName: param.projectId,
tableName: param.modelId,
viewName: param.viewId,
});
await model.getColumns();
const hasLink = model.columns.some((c) => c.uidt === UITypes.LinkToAnotherRecord && c.colOptions?.type === 'mm');
const pkMap = new Map<string, string>();
for (const column of model.columns.filter((c) => c.uidt === UITypes.LinkToAnotherRecord && c.colOptions?.type !== 'hm')) {
const relatedTable = await (
(await column.getColOptions()) as LinkToAnotherRecordColumn
).getRelatedTable();
await relatedTable.getColumns();
pkMap.set(column.id, relatedTable.primaryKey.title);
}
const readableStream = new Readable({
read() {},
});
const readableLinkStream = new Readable({
read() {},
});
readableStream.setEncoding('utf8');
readableLinkStream.setEncoding('utf8');
const storageAdapter = param.storageAdapter;
const uploadPromise = storageAdapter.fileCreateByStream(
`${param.path}/${model.id}.csv`,
readableStream
);
const uploadLinkPromise = hasLink
? storageAdapter.fileCreateByStream(
`${param.path}/${model.id}_links.csv`,
readableLinkStream
)
: Promise.resolve();
const limit = 100;
let offset = 0;
const primaryKey = model.columns.find((c) => c.pk);
const formatData = (data: any) => {
const linkData = [];
for (const row of data) {
const pkValue = primaryKey ? row[primaryKey.title] : undefined;
const linkRow = {};
for (const [k, v] of Object.entries(row)) {
const col = model.columns.find((c) => c.title === k);
if (col) {
if (col.pk) linkRow['pk'] = pkValue;
const colId = `${col.project_id}::${col.base_id}::${col.fk_model_id}::${col.id}`;
switch(col.uidt) {
case UITypes.LinkToAnotherRecord:
if (col.system || col.colOptions.type === 'hm') break;
const pkList = [];
const links = Array.isArray(v) ? v : [v];
for (const link of links) {
if (link) {
for (const [k, val] of Object.entries(link)) {
if (k === pkMap.get(col.id)) {
pkList.push(val);
}
}
}
}
if (col.colOptions.type === 'mm') {
linkRow[colId] = pkList.join(',');
} else {
row[colId] = pkList[0];
}
break;
case UITypes.Attachment:
try {
row[colId] = JSON.stringify(v);
} catch (e) {
row[colId] = v;
}
break;
case UITypes.ForeignKey:
case UITypes.Formula:
case UITypes.Lookup:
case UITypes.Rollup:
case UITypes.Rating:
case UITypes.Barcode:
// skip these types
break;
default:
row[colId] = v;
break;
}
delete row[k];
}
}
linkData.push(linkRow);
}
return { data, linkData };
}
try {
await recursiveRead(formatData, readableStream, readableLinkStream, model, view, offset, limit, true);
await uploadPromise;
await uploadLinkPromise;
} catch (e) {
await storageAdapter.fileDelete(`${param.path}/${model.id}.csv`);
await storageAdapter.fileDelete(`${param.path}/${model.id}_links.csv`);
console.error(e);
throw e;
}
return true;
}
async function recursiveRead(
formatter: Function,
stream: Readable,
linkStream: Readable,
model: Model,
view: View,
offset: number,
limit: number,
header = false
): Promise<void> {
return new Promise((resolve, reject) => {
dataService
.getDataList({ model, view, query: { limit, offset } })
.then((result) => {
try {
if (!header) {
stream.push('\r\n');
linkStream.push('\r\n');
}
const { data, linkData } = formatter(result.list);
stream.push(unparse(data, { header }));
linkStream.push(unparse(linkData, { header }));
if (result.pageInfo.isLastPage) {
stream.push(null);
linkStream.push(null);
resolve();
} else {
recursiveRead(formatter, stream, linkStream, model, view, offset + limit, limit).then(resolve);
}
} catch (e) {
reject(e);
}
});
});
}
function clearPrefix(text: string, prefix?: string) {
if (!prefix || prefix.length === 0) return text;
return text.replace(new RegExp(`^${prefix}_?`), '');
}
export async function exportBaseSchema(param: { baseId: string }) {
const base = await Base.get(param.baseId);
if (!base) return NcError.badRequest(`Base not found for id '${param.baseId}'`);
const project = await Project.get(base.project_id);
const models = (await base.getModels()).filter((m) => !m.mm && m.type === 'table');
const exportedModels = await serializeModels({ modelId: models.map(m => m.id) });
const exportData = { id: `${project.id}::${base.id}`, entity: 'base', models: exportedModels };
return exportData;
}
export async function exportBase(param: { path: string; baseId: string }) {
const base = await Base.get(param.baseId);
if (!base) return NcError.badRequest(`Base not found for id '${param.baseId}'`);
const project = await Project.get(base.project_id);
const models = (await base.getModels()).filter((m) => !m.mm && m.type === 'table');
const exportedModels = await serializeModels({ modelId: models.map(m => m.id) });
const exportData = { id: `${project.id}::${base.id}`, entity: 'base', models: exportedModels };
const storageAdapter = await NcPluginMgrv2.storageAdapter();
const destPath = `export/${project.id}/${base.id}/${param.path}/schema.json`;
try {
const readableStream = new Readable({
read() {},
});
readableStream.setEncoding('utf8');
readableStream.push(JSON.stringify(exportData));
readableStream.push(null);
await storageAdapter.fileCreateByStream(
destPath,
readableStream
);
for (const model of models) {
await exportModelData({
storageAdapter,
path: `export/${project.id}/${base.id}/${param.path}/data`,
projectId: project.id,
modelId: model.id,
});
}
} catch (e) {
console.error(e);
return NcError.internalServerError('Error while exporting base');
}
return true;
}

844
packages/nocodb-legacy/src/lib/services/exportImport/import.svc.ts

@ -0,0 +1,844 @@
import type { ViewCreateReqType } from 'nocodb-sdk';
import { UITypes, ViewTypes } from 'nocodb-sdk';
import { tableService, gridViewService, filterService, viewColumnService, gridViewColumnService, sortService, formViewService, galleryViewService, kanbanViewService, formViewColumnService, columnService, bulkDataService } from '..';
import { NcError } from '../../meta/helpers/catchError';
import { Project, Base, User, View, Model, Column, LinkToAnotherRecordColumn } from '../../models';
import NcPluginMgrv2 from '../../meta/helpers/NcPluginMgrv2';
import papaparse from 'papaparse';
export async function importModels(param: {
user: User;
projectId: string;
baseId: string;
data: { models: { model: any; views: any[] }[] } | { model: any; views: any[] }[];
req: any;
}) {
// structured id to db id
const idMap = new Map<string, string>();
const project = await Project.get(param.projectId);
if (!project) return NcError.badRequest(`Project not found for id '${param.projectId}'`);
const base = await Base.get(param.baseId);
if (!base) return NcError.badRequest(`Base not found for id '${param.baseId}'`);
const tableReferences = new Map<string, Model>();
const linkMap = new Map<string, string>();
param.data = Array.isArray(param.data) ? param.data : param.data.models;
// create tables with static columns
for (const data of param.data) {
const modelData = data.model;
const reducedColumnSet = modelData.columns.filter(
(a) =>
a.uidt !== UITypes.LinkToAnotherRecord &&
a.uidt !== UITypes.Lookup &&
a.uidt !== UITypes.Rollup &&
a.uidt !== UITypes.Formula &&
a.uidt !== UITypes.ForeignKey
);
// create table with static columns
const table = await tableService.tableCreate({
projectId: project.id,
baseId: base.id,
user: param.user,
table: withoutId({
...modelData,
columns: reducedColumnSet.map((a) => withoutId(a)),
}),
});
idMap.set(modelData.id, table.id);
// map column id's with new created column id's
for (const col of table.columns) {
const colRef = modelData.columns.find(
(a) => a.column_name === col.column_name
);
idMap.set(colRef.id, col.id);
}
tableReferences.set(modelData.id, table);
}
const referencedColumnSet = []
// create columns with reference to other columns
for (const data of param.data) {
const modelData = data.model;
const table = tableReferences.get(modelData.id);
const linkedColumnSet = modelData.columns.filter(
(a) => a.uidt === UITypes.LinkToAnotherRecord
);
// create columns with reference to other columns
for (const col of linkedColumnSet) {
if (col.colOptions) {
const colOptions = col.colOptions;
if (col.uidt === UITypes.LinkToAnotherRecord && idMap.has(colOptions.fk_related_model_id)) {
if (colOptions.type === 'mm') {
if (!linkMap.has(colOptions.fk_mm_model_id)) {
// delete col.column_name as it is not required and will cause ajv error (null for LTAR)
delete col.column_name;
const freshModelData = await columnService.columnAdd({
tableId: table.id,
column: withoutId({
...col,
...{
parentId: idMap.get(getParentIdentifier(colOptions.fk_child_column_id)),
childId: idMap.get(getParentIdentifier(colOptions.fk_parent_column_id)),
type: colOptions.type,
virtual: colOptions.virtual,
ur: colOptions.ur,
dr: colOptions.dr,
},
}),
req: param.req,
});
for (const nColumn of freshModelData.columns) {
if (nColumn.title === col.title) {
idMap.set(col.id, nColumn.id);
linkMap.set(colOptions.fk_mm_model_id, nColumn.colOptions.fk_mm_model_id);
break;
}
}
const childModel = getParentIdentifier(colOptions.fk_parent_column_id) === modelData.id ? freshModelData : await Model.get(idMap.get(getParentIdentifier(colOptions.fk_parent_column_id)));
if (getParentIdentifier(colOptions.fk_parent_column_id) !== modelData.id) await childModel.getColumns();
const childColumn = param.data.find(a => a.model.id === getParentIdentifier(colOptions.fk_parent_column_id)).model.columns.find(a => a.colOptions?.fk_mm_model_id === colOptions.fk_mm_model_id && a.id !== col.id);
for (const nColumn of childModel.columns) {
if (nColumn?.colOptions?.fk_mm_model_id === linkMap.get(colOptions.fk_mm_model_id) && nColumn.id !== idMap.get(col.id)) {
idMap.set(childColumn.id, nColumn.id);
if (nColumn.title !== childColumn.title) {
await columnService.columnUpdate({
columnId: nColumn.id,
column: {
...nColumn,
column_name: childColumn.title,
title: childColumn.title,
},
});
}
break;
}
}
}
} else if (colOptions.type === 'hm') {
// delete col.column_name as it is not required and will cause ajv error (null for LTAR)
delete col.column_name;
const freshModelData = await columnService.columnAdd({
tableId: table.id,
column: withoutId({
...col,
...{
parentId: idMap.get(getParentIdentifier(colOptions.fk_parent_column_id)),
childId: idMap.get(getParentIdentifier(colOptions.fk_child_column_id)),
type: colOptions.type,
virtual: colOptions.virtual,
ur: colOptions.ur,
dr: colOptions.dr,
},
}),
req: param.req,
});
for (const nColumn of freshModelData.columns) {
if (nColumn.title === col.title) {
idMap.set(col.id, nColumn.id);
idMap.set(colOptions.fk_parent_column_id, nColumn.colOptions.fk_parent_column_id);
idMap.set(colOptions.fk_child_column_id, nColumn.colOptions.fk_child_column_id);
break;
}
}
const childModel = colOptions.fk_related_model_id === modelData.id ? freshModelData : await Model.get(idMap.get(colOptions.fk_related_model_id));
if (colOptions.fk_related_model_id !== modelData.id) await childModel.getColumns();
const childColumn = param.data
.find((a) => a.model.id === colOptions.fk_related_model_id)
.model.columns.find(
(a) =>
a.colOptions?.fk_parent_column_id ===
colOptions.fk_parent_column_id &&
a.colOptions?.fk_child_column_id ===
colOptions.fk_child_column_id &&
a.id !== col.id
);
for (const nColumn of childModel.columns) {
if (nColumn.id !== idMap.get(col.id) && nColumn.colOptions?.fk_parent_column_id === idMap.get(colOptions.fk_parent_column_id) && nColumn.colOptions?.fk_child_column_id === idMap.get(colOptions.fk_child_column_id)) {
idMap.set(childColumn.id, nColumn.id);
if (nColumn.title !== childColumn.title) {
await columnService.columnUpdate({
columnId: nColumn.id,
column: {
...nColumn,
column_name: childColumn.title,
title: childColumn.title,
},
});
}
break;
}
}
}
}
}
}
referencedColumnSet.push(...modelData.columns.filter(
(a) =>
a.uidt === UITypes.Lookup ||
a.uidt === UITypes.Rollup ||
a.uidt === UITypes.Formula
));
}
const sortedReferencedColumnSet = [];
// sort referenced columns to avoid referencing before creation
for (const col of referencedColumnSet) {
const relatedColIds = [];
if (col.colOptions?.fk_lookup_column_id) {
relatedColIds.push(col.colOptions.fk_lookup_column_id);
}
if (col.colOptions?.fk_rollup_column_id) {
relatedColIds.push(col.colOptions.fk_rollup_column_id);
}
if (col.colOptions?.formula) {
relatedColIds.push(...col.colOptions.formula.match(/(?<=\{\{).*?(?=\}\})/gm));
}
// find the last related column in the sorted array
let fnd = undefined;
for (let i = sortedReferencedColumnSet.length - 1; i >= 0; i--) {
if (relatedColIds.includes(sortedReferencedColumnSet[i].id)) {
fnd = sortedReferencedColumnSet[i];
break;
}
}
if (!fnd) {
sortedReferencedColumnSet.unshift(col);
} else {
sortedReferencedColumnSet.splice(sortedReferencedColumnSet.indexOf(fnd) + 1, 0, col);
}
}
// create referenced columns
for (const col of sortedReferencedColumnSet) {
const { colOptions, ...flatCol } = col;
if (col.uidt === UITypes.Lookup) {
if (!idMap.get(colOptions.fk_relation_column_id)) continue;
const freshModelData = await columnService.columnAdd({
tableId: idMap.get(getParentIdentifier(col.id)),
column: withoutId({
...flatCol,
...{
fk_lookup_column_id: idMap.get(colOptions.fk_lookup_column_id),
fk_relation_column_id: idMap.get(colOptions.fk_relation_column_id),
},
}),
req: param.req,
});
for (const nColumn of freshModelData.columns) {
if (nColumn.title === col.title) {
idMap.set(col.id, nColumn.id);
break;
}
}
} else if (col.uidt === UITypes.Rollup) {
if (!idMap.get(colOptions.fk_relation_column_id)) continue;
const freshModelData = await columnService.columnAdd({
tableId: idMap.get(getParentIdentifier(col.id)),
column: withoutId({
...flatCol,
...{
fk_rollup_column_id: idMap.get(colOptions.fk_rollup_column_id),
fk_relation_column_id: idMap.get(colOptions.fk_relation_column_id),
rollup_function: colOptions.rollup_function,
},
}),
req: param.req,
});
for (const nColumn of freshModelData.columns) {
if (nColumn.title === col.title) {
idMap.set(col.id, nColumn.id);
break;
}
}
} else if (col.uidt === UITypes.Formula) {
const freshModelData = await columnService.columnAdd({
tableId: idMap.get(getParentIdentifier(col.id)),
column: withoutId({
...flatCol,
...{
formula_raw: colOptions.formula_raw,
},
}),
req: param.req,
});
for (const nColumn of freshModelData.columns) {
if (nColumn.title === col.title) {
idMap.set(col.id, nColumn.id);
break;
}
}
}
}
// create views
for (const data of param.data) {
const modelData = data.model;
const viewsData = data.views;
const table = tableReferences.get(modelData.id);
// get default view
await table.getViews();
for (const view of viewsData) {
const viewData = withoutId({
...view,
});
const vw = await createView(idMap, table, viewData, table.views);
if (!vw) continue;
idMap.set(view.id, vw.id);
// create filters
const filters = view.filter.children;
for (const fl of filters) {
const fg = await filterService.filterCreate({
viewId: vw.id,
filter: withoutId({
...fl,
fk_column_id: idMap.get(fl.fk_column_id),
fk_parent_id: idMap.get(fl.fk_parent_id),
}),
});
idMap.set(fl.id, fg.id);
}
// create sorts
for (const sr of view.sorts) {
await sortService.sortCreate({
viewId: vw.id,
sort: withoutId({
...sr,
fk_column_id: idMap.get(sr.fk_column_id),
}),
})
}
// update view columns
const vwColumns = await viewColumnService.columnList({ viewId: vw.id })
for (const cl of vwColumns) {
const fcl = view.columns.find(a => a.fk_column_id === reverseGet(idMap, cl.fk_column_id))
if (!fcl) continue;
await viewColumnService.columnUpdate({
viewId: vw.id,
columnId: cl.id,
column: {
show: fcl.show,
order: fcl.order,
},
})
}
switch (vw.type) {
case ViewTypes.GRID:
for (const cl of vwColumns) {
const fcl = view.columns.find(a => a.fk_column_id === reverseGet(idMap, cl.fk_column_id))
if (!fcl) continue;
const { fk_column_id, ...rest } = fcl;
await gridViewColumnService.gridColumnUpdate({
gridViewColumnId: cl.id,
grid: {
...withoutNull(rest),
},
})
}
break;
case ViewTypes.FORM:
for (const cl of vwColumns) {
const fcl = view.columns.find(a => a.fk_column_id === reverseGet(idMap, cl.fk_column_id))
if (!fcl) continue;
const { fk_column_id, ...rest } = fcl;
await formViewColumnService.columnUpdate({
formViewColumnId: cl.id,
formViewColumn: {
...withoutNull(rest),
},
})
}
break;
case ViewTypes.GALLERY:
case ViewTypes.KANBAN:
break;
}
}
}
return idMap;
}
async function createView(idMap: Map<string, string>, md: Model, vw: Partial<View>, views: View[]): Promise<View> {
if (vw.is_default) {
const view = views.find((a) => a.is_default);
if (view) {
const gridData = withoutNull(vw.view);
if (gridData) {
await gridViewService.gridViewUpdate({
viewId: view.id,
grid: gridData,
});
}
}
return view;
}
switch (vw.type) {
case ViewTypes.GRID:
const gview = await gridViewService.gridViewCreate({
tableId: md.id,
grid: vw as ViewCreateReqType,
});
const gridData = withoutNull(vw.view);
if (gridData) {
await gridViewService.gridViewUpdate({
viewId: gview.id,
grid: gridData,
});
}
return gview;
case ViewTypes.FORM:
const fview = await formViewService.formViewCreate({
tableId: md.id,
body: vw as ViewCreateReqType,
});
const formData = withoutNull(vw.view);
if (formData) {
await formViewService.formViewUpdate({
formViewId: fview.id,
form: formData,
});
}
return fview;
case ViewTypes.GALLERY:
const glview = await galleryViewService.galleryViewCreate({
tableId: md.id,
gallery: vw as ViewCreateReqType,
});
const galleryData = withoutNull(vw.view);
if (galleryData) {
for (const [k, v] of Object.entries(galleryData)) {
switch (k) {
case 'fk_cover_image_col_id':
galleryData[k] = idMap.get(v as string);
break;
}
}
await galleryViewService.galleryViewUpdate({
galleryViewId: glview.id,
gallery: galleryData,
});
}
return glview;
case ViewTypes.KANBAN:
const kview = await kanbanViewService.kanbanViewCreate({
tableId: md.id,
kanban: vw as ViewCreateReqType,
});
const kanbanData = withoutNull(vw.view);
if (kanbanData) {
for (const [k, v] of Object.entries(kanbanData)) {
switch (k) {
case 'fk_grp_col_id':
case 'fk_cover_image_col_id':
kanbanData[k] = idMap.get(v as string);
break;
case 'meta':
const meta = {};
for (const [mk, mv] of Object.entries(v as any)) {
const tempVal = [];
for (const vl of mv as any) {
if (vl.fk_column_id) {
tempVal.push({
...vl,
fk_column_id: idMap.get(vl.fk_column_id),
});
} else {
delete vl.fk_column_id;
tempVal.push({
...vl,
id: "uncategorized",
});
}
}
meta[idMap.get(mk)] = tempVal;
}
kanbanData[k] = meta;
break;
}
}
await kanbanViewService.kanbanViewUpdate({
kanbanViewId: kview.id,
kanban: kanbanData,
});
}
return kview;
}
return null
}
function withoutNull(obj: any) {
const newObj = {};
let found = false;
for (const [key, value] of Object.entries(obj)) {
if (value !== null) {
newObj[key] = value;
found = true;
}
}
if (!found) return null;
return newObj;
}
function reverseGet(map: Map<string, string>, vl: string) {
for (const [key, value] of map.entries()) {
if (vl === value) {
return key;
}
}
return undefined
}
function withoutId(obj: any) {
const { id, ...rest } = obj;
return rest;
}
function getParentIdentifier(id: string) {
const arr = id.split('::');
arr.pop();
return arr.join('::');
}
function getEntityIdentifier(id: string) {
const arr = id.split('::');
return arr.pop();
}
function findWithIdentifier(map: Map<string, any>, id: string) {
for (const key of map.keys()) {
if (getEntityIdentifier(key) === id) {
return map.get(key);
}
}
return undefined;
}
export async function importBase(param: {
user: User;
projectId: string;
baseId: string;
src: { type: 'local' | 'url' | 'file'; path?: string; url?: string; file?: any };
req: any;
}) {
const { user, projectId, baseId, src, req } = param;
const debug = req.params.debug === 'true';
const debugLog = (...args: any[]) => {
if (!debug) return;
console.log(...args);
}
let start = process.hrtime();
let elapsedTime = function(label?: string){
const elapsedS = (process.hrtime(start)[0]).toFixed(3);
const elapsedMs = process.hrtime(start)[1] / 1000000;
if (label) debugLog(`${label}: ${elapsedS}s ${elapsedMs}ms`);
start = process.hrtime();
}
switch (src.type) {
case 'local':
const path = src.path.replace(/\/$/, '');
const storageAdapter = await NcPluginMgrv2.storageAdapter();
try {
const schema = JSON.parse(await storageAdapter.fileRead(`${path}/schema.json`));
elapsedTime('read schema');
// store fk_mm_model_id (mm) to link once
const handledLinks = [];
const idMap = await importModels({
user,
projectId,
baseId,
data: schema,
req,
});
elapsedTime('import models');
if (idMap) {
const files = await storageAdapter.getDirectoryList(`${path}/data`);
const dataFiles = files.filter((file) => !file.match(/_links\.csv$/));
const linkFiles = files.filter((file) => file.match(/_links\.csv$/));
for (const file of dataFiles) {
const readStream = await storageAdapter.fileReadByStream(
`${path}/data/${file}`
);
const headers: string[] = [];
let chunk = [];
const modelId = findWithIdentifier(
idMap,
file.replace(/\.csv$/, '')
);
const model = await Model.get(modelId);
debugLog(`Importing ${model.title}...`);
await new Promise(async (resolve) => {
papaparse.parse(readStream, {
newline: '\r\n',
step: async function (results, parser) {
if (!headers.length) {
parser.pause();
for (const header of results.data) {
const id = idMap.get(header);
if (id) {
const col = await Column.get({
base_id: baseId,
colId: id,
});
if (col.colOptions?.type === 'bt') {
const childCol = await Column.get({
base_id: baseId,
colId: col.colOptions.fk_child_column_id,
});
headers.push(childCol.column_name);
} else {
headers.push(col.column_name);
}
} else {
debugLog(header);
}
}
parser.resume();
} else {
if (results.errors.length === 0) {
const row = {};
for (let i = 0; i < headers.length; i++) {
if (results.data[i] !== '') {
row[headers[i]] = results.data[i];
}
}
chunk.push(row);
if (chunk.length > 100) {
parser.pause();
elapsedTime('before import chunk');
try {
await bulkDataService.bulkDataInsert({
projectName: projectId,
tableName: modelId,
body: chunk,
cookie: null,
chunkSize: chunk.length + 1,
foreign_key_checks: false,
raw: true,
});
} catch (e) {
debugLog(`${model.title} import throwed an error!`);
console.log(e);
}
chunk = [];
elapsedTime('after import chunk');
parser.resume();
}
}
}
},
complete: async function () {
if (chunk.length > 0) {
elapsedTime('before import chunk');
try {
await bulkDataService.bulkDataInsert({
projectName: projectId,
tableName: modelId,
body: chunk,
cookie: null,
chunkSize: chunk.length + 1,
foreign_key_checks: false,
raw: true,
});
} catch (e) {
debugLog(chunk);
console.log(e);
}
chunk = [];
elapsedTime('after import chunk');
}
resolve(null);
},
});
});
}
// reset timer
elapsedTime();
for (const file of linkFiles) {
const readStream = await storageAdapter.fileReadByStream(
`${path}/data/${file}`
);
const headers: string[] = [];
const mmParentChild: any = {};
let chunk: Record<string, any[]> = {}; // colId: { rowId, childId }[]
const modelId = findWithIdentifier(
idMap,
file.replace(/_links\.csv$/, '')
);
const model = await Model.get(modelId);
let pkIndex = -1;
debugLog(`Linking ${model.title}...`);
await new Promise(async (resolve) => {
papaparse.parse(readStream, {
newline: '\r\n',
step: async function (results, parser) {
if (!headers.length) {
parser.pause();
for (const header of results.data) {
if (header === 'pk') {
headers.push(null);
pkIndex = headers.length - 1;
continue;
}
const id = idMap.get(header);
if (id) {
const col = await Column.get({
base_id: baseId,
colId: id,
});
if (
col.uidt === UITypes.LinkToAnotherRecord &&
col.colOptions.fk_mm_model_id &&
handledLinks.includes(col.colOptions.fk_mm_model_id)
) {
headers.push(null);
} else {
if (
col.uidt === UITypes.LinkToAnotherRecord &&
col.colOptions.fk_mm_model_id &&
!handledLinks.includes(
col.colOptions.fk_mm_model_id
)
) {
const colOptions = await col.getColOptions<LinkToAnotherRecordColumn>();
const vChildCol = await colOptions.getMMChildColumn();
const vParentCol = await colOptions.getMMParentColumn();
mmParentChild[col.colOptions.fk_mm_model_id] = {
parent: vParentCol.column_name,
child: vChildCol.column_name,
}
handledLinks.push(col.colOptions.fk_mm_model_id);
}
headers.push(col.colOptions.fk_mm_model_id);
chunk[col.colOptions.fk_mm_model_id] = []
}
}
}
parser.resume();
} else {
if (results.errors.length === 0) {
for (let i = 0; i < headers.length; i++) {
if (!headers[i]) continue;
const mm = mmParentChild[headers[i]];
for (const rel of results.data[i].split(',')) {
if (rel.trim() === '') continue;
chunk[headers[i]].push({ [mm.parent]: rel, [mm.child]: results.data[pkIndex] });
}
}
}
}
},
complete: async function () {
for (const [k, v] of Object.entries(chunk)) {
try {
elapsedTime('prepare link chunk');
await bulkDataService.bulkDataInsert({
projectName: projectId,
tableName: k,
body: v,
cookie: null,
chunkSize: 1000,
foreign_key_checks: false,
raw: true,
});
elapsedTime('insert link chunk');
} catch (e) {
console.log(e);
}
}
resolve(null);
},
});
});
}
}
} catch (e) {
throw new Error(e);
}
break;
case 'url':
break;
case 'file':
break;
}
}

2
packages/nocodb-legacy/src/lib/services/index.ts

@ -36,3 +36,5 @@ export * as syncService from './sync';
export * from './public';
export * as orgTokenService from './orgToken.svc';
export * as orgTokenServiceEE from './ee/orgToken.svc';
export * as exportService from './exportImport/export.svc';
export * as importService from './exportImport/import.svc';

2
packages/nocodb-legacy/src/lib/services/metaDiff.svc.ts

@ -1010,7 +1010,7 @@ export async function extractAndGenerateManyToManyRelations(
}
// todo: impl better method to identify m2m relation
if (belongsToCols?.length === 2 && normalColumns.length < 5) {
if (belongsToCols?.length === 2 && normalColumns.length < 5 && assocModel.primaryKeys.length === 2) {
const modelA = await belongsToCols[0].colOptions.getRelatedTable();
const modelB = await belongsToCols[1].colOptions.getRelatedTable();

27
packages/nocodb-legacy/src/lib/v1-legacy/plugins/adapters/storage/Local.ts

@ -4,6 +4,7 @@ import { promisify } from 'util';
import mkdirp from 'mkdirp';
import axios from 'axios';
import NcConfigFactory from '../../../../utils/NcConfigFactory';
import type { Readable } from 'stream';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
export default class Local implements IStorageAdapterV2 {
@ -65,6 +66,32 @@ export default class Local implements IStorageAdapterV2 {
});
}
public async fileCreateByStream(key: string, stream: Readable): Promise<void> {
return new Promise(async (resolve, reject) => {
const destPath = path.join(NcConfigFactory.getToolDir(), ...key.split('/'));
try {
await mkdirp(path.dirname(destPath));
const writableStream = fs.createWriteStream(destPath);
writableStream.on('finish', () => resolve());
writableStream.on('error', (err) => reject(err));
stream.pipe(writableStream);
} catch (e) {
throw e;
}
});
}
public async fileReadByStream(key: string): Promise<Readable> {
const srcPath = path.join(NcConfigFactory.getToolDir(), ...key.split('/'));
return fs.createReadStream(srcPath, { encoding: 'utf8' });
}
public async getDirectoryList(key: string): Promise<string[]> {
const destDir = path.join(NcConfigFactory.getToolDir(), ...key.split('/'));
return fs.promises.readdir(destDir);
}
// todo: implement
fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);

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

@ -1,12 +1,12 @@
{
"name": "nocodb-sdk",
"version": "0.106.1",
"version": "0.107.0-beta.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb-sdk",
"version": "0.106.1",
"version": "0.107.0-beta.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",

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

@ -1892,6 +1892,11 @@ export interface ProjectReqType {
* @example My Project
*/
title: string;
/**
* Project Status
* @example locked
*/
status?: StringOrNullType;
}
/**
@ -1910,6 +1915,11 @@ export interface ProjectUpdateReqType {
* @example My Project
*/
title?: string;
/**
* Project Status
* @example locked
*/
status?: StringOrNullType;
}
/**
@ -4001,6 +4011,97 @@ export class Api<
...params,
}),
/**
* @description Duplicate a project
*
* @tags Project
* @name BaseDuplicate
* @summary Duplicate Project Base
* @request POST:/api/v1/db/meta/duplicate/{projectId}/{baseId}
* @response `200` `{
name?: string,
id?: string,
}` OK
* @response `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\
msg: string,
}`
*/
baseDuplicate: (
projectId: IdType,
data: {
excludeData?: boolean;
excludeViews?: boolean;
excludeHooks?: boolean;
},
baseId?: IdType,
params: RequestParams = {}
) =>
this.request<
{
name?: string;
id?: string;
},
{
/** @example BadRequest [Error]: <ERROR MESSAGE> */
msg: string;
}
>({
path: `/api/v1/db/meta/duplicate/${projectId}/${baseId}`,
method: 'POST',
body: data,
type: ContentType.Json,
format: 'json',
...params,
}),
/**
* @description Duplicate a project
*
* @tags Project
* @name Duplicate
* @summary Duplicate Project
* @request POST:/api/v1/db/meta/duplicate/{projectId}
* @response `200` `{
name?: string,
id?: string,
}` OK
* @response `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\
msg: string,
}`
*/
duplicate: (
projectId: IdType,
data: {
excludeData?: boolean;
excludeViews?: boolean;
excludeHooks?: boolean;
},
params: RequestParams = {}
) =>
this.request<
{
name?: string;
id?: string;
},
{
/** @example BadRequest [Error]: <ERROR MESSAGE> */
msg: string;
}
>({
path: `/api/v1/db/meta/duplicate/${projectId}`,
method: 'POST',
body: data,
type: ContentType.Json,
format: 'json',
...params,
}),
/**
* @description Get the info of a given project
*
@ -4995,6 +5096,51 @@ export class Api<
...params,
}),
/**
* @description Duplicate a table
*
* @tags DB Table
* @name Duplicate
* @summary Duplicate Table
* @request POST:/api/v1/db/meta/duplicate/{projectId}/table/{tableId}
* @response `200` `{
name?: string,
id?: string,
}` OK
* @response `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\
msg: string,
}`
*/
duplicate: (
projectId: IdType,
tableId: IdType,
data: {
excludeData?: boolean;
excludeViews?: boolean;
},
params: RequestParams = {}
) =>
this.request<
{
name?: string;
id?: string;
},
{
/** @example BadRequest [Error]: <ERROR MESSAGE> */
msg: string;
}
>({
path: `/api/v1/db/meta/duplicate/${projectId}/table/${tableId}`,
method: 'POST',
body: data,
type: ContentType.Json,
format: 'json',
...params,
}),
/**
* @description Update the order of the given Table
*

4
packages/nocodb-sdk/src/lib/globals.ts

@ -72,3 +72,7 @@ export enum ModelTypes {
TABLE = 'table',
VIEW = 'view',
}
export enum ProjectStatus {
JOB = 'job',
}

6
packages/nocodb/.gitignore vendored

@ -35,4 +35,10 @@ lerna-debug.log*
!.vscode/extensions.json
/noco.db
# export
export/**
# test dbs
test_sakila_?.db
/docker/main.js

316
packages/nocodb/package-lock.json generated

@ -11,13 +11,17 @@
"dependencies": {
"@google-cloud/storage": "^5.7.2",
"@graphql-tools/merge": "^6.0.12",
"@nestjs/bull": "^0.6.3",
"@nestjs/common": "^9.4.0",
"@nestjs/core": "^9.4.0",
"@nestjs/event-emitter": "^1.4.1",
"@nestjs/jwt": "^10.0.3",
"@nestjs/mapped-types": "*",
"@nestjs/passport": "^9.0.3",
"@nestjs/platform-express": "^9.4.0",
"@nestjs/platform-socket.io": "^9.4.0",
"@nestjs/serve-static": "^3.0.1",
"@nestjs/websockets": "^9.4.0",
"@sentry/node": "^6.3.5",
"@types/chai": "^4.2.12",
"airtable": "^0.11.3",
@ -30,6 +34,7 @@
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.0",
"boxen": "^5.0.0",
"bull": "^4.10.4",
"bullmq": "^1.81.1",
"clear": "^0.1.0",
"colors": "^1.4.0",
@ -76,12 +81,13 @@
"nanoid": "^3.1.20",
"nc-help": "^0.2.87",
"nc-lib-gui": "0.107.0-beta.0",
"nc-plugin": "0.1.2",
"nc-plugin": "^0.1.3",
"ncp": "^2.0.0",
"nocodb-sdk": "0.107.0-beta.0",
"nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"os-locale": "^6.0.2",
"p-queue": "^6.6.2",
"papaparse": "^5.3.1",
"parse-database-url": "^0.3.0",
"passport": "^0.4.1",
@ -114,6 +120,7 @@
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@nestjsplus/dyn-schematics": "^1.0.12",
"@types/bull": "^4.10.0",
"@types/express": "^4.17.13",
"@types/jest": "^29.5.0",
"@types/mocha": "^10.0.1",
@ -147,9 +154,43 @@
"webpack-cli": "^5.0.1"
}
},
"../nc-plugin": {
"version": "0.1.3",
"extraneous": true,
"license": "AGPL-3.0-or-later",
"dependencies": {
"nc-common": "0.0.6"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@types/node": "^18.11.9",
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"ava": "^5.2.0",
"codecov": "^3.5.0",
"cspell": "^4.1.0",
"cz-conventional-changelog": "^3.3.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",
"gh-pages": "^3.1.0",
"npm-run-all": "^4.1.5",
"nyc": "^15.1.0",
"open-cli": "^7.1.0",
"prettier": "^2.1.1",
"standard-version": "^9.0.0",
"ts-node": "^9.0.0",
"typedoc": "^0.19.0",
"typescript": "^4.0.2"
},
"engines": {
"node": ">=10"
}
},
"../nocodb-sdk": {
"version": "0.107.0-beta.0",
"extraneous": true,
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@ -2300,6 +2341,32 @@
"win32"
]
},
"node_modules/@nestjs/bull": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/@nestjs/bull/-/bull-0.6.3.tgz",
"integrity": "sha512-CckH9O3t9qSiO4RCzdYvtFSaaMfIhTXMYagV/rtmVvI1SX5XNnxEaQXvtjxDBXF9DB1JE/5AejIl6ICym+MJIw==",
"dependencies": {
"@nestjs/bull-shared": "^0.1.3",
"tslib": "2.5.0"
},
"peerDependencies": {
"@nestjs/common": "^6.10.11 || ^7.0.0 || ^8.0.0 || ^9.0.0",
"@nestjs/core": "^6.10.11 || ^7.0.0 || ^8.0.0 || ^9.0.0",
"bull": "^3.3 || ^4.0.0"
}
},
"node_modules/@nestjs/bull-shared": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-0.1.3.tgz",
"integrity": "sha512-K0a1ERpnl/ZnTmm0UtYSSClDlDkQwNNwJYM6PogzpeflD64oqwVIn8Pj8rdS+BOYUxqdDy55q3p67ytO5oaVDA==",
"dependencies": {
"tslib": "2.5.0"
},
"peerDependencies": {
"@nestjs/common": "^6.10.11 || ^7.0.0 || ^8.0.0 || ^9.0.0",
"@nestjs/core": "^6.10.11 || ^7.0.0 || ^8.0.0 || ^9.0.0"
}
},
"node_modules/@nestjs/cli": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.3.0.tgz",
@ -2475,6 +2542,19 @@
}
}
},
"node_modules/@nestjs/event-emitter": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-1.4.1.tgz",
"integrity": "sha512-PmLpzMYgEKJNxOUrRjb6kNSm2PC6J+BeLTuF/bkYViGM/mVGvYOgU5jq8DQnXmiSmDmyWN+tO2cHSnR7odJJRA==",
"dependencies": {
"eventemitter2": "6.4.9"
},
"peerDependencies": {
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0",
"@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0",
"reflect-metadata": "^0.1.12"
}
},
"node_modules/@nestjs/jwt": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.0.3.tgz",
@ -2582,6 +2662,24 @@
"node": ">=10.0.0"
}
},
"node_modules/@nestjs/platform-socket.io": {
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-9.4.0.tgz",
"integrity": "sha512-pk5uWItnsrFKzvQrFcAmyfcb8cpGgoj4yR4+vbA5H/MLcv+8vGqruQO8riN8jAYGNPN9Y02ihBKbIvQqn92M5g==",
"dependencies": {
"socket.io": "4.6.1",
"tslib": "2.5.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/nest"
},
"peerDependencies": {
"@nestjs/common": "^9.0.0",
"@nestjs/websockets": "^9.0.0",
"rxjs": "^7.1.0"
}
},
"node_modules/@nestjs/schematics": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-9.1.0.tgz",
@ -2655,6 +2753,28 @@
}
}
},
"node_modules/@nestjs/websockets": {
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-9.4.0.tgz",
"integrity": "sha512-RATR9C0cKhXp3mTQAg75iItyUuRhVwU39Xe/kl0XLpvAhWzhnGrn6CxSTRRzBfp3F68DOKvs7/ODDY51f+rdXw==",
"dependencies": {
"iterare": "1.2.1",
"object-hash": "3.0.0",
"tslib": "2.5.0"
},
"peerDependencies": {
"@nestjs/common": "^9.0.0",
"@nestjs/core": "^9.0.0",
"@nestjs/platform-socket.io": "^9.0.0",
"reflect-metadata": "^0.1.12",
"rxjs": "^7.1.0"
},
"peerDependenciesMeta": {
"@nestjs/platform-socket.io": {
"optional": true
}
}
},
"node_modules/@nestjsplus/dyn-schematics": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@nestjsplus/dyn-schematics/-/dyn-schematics-1.0.12.tgz",
@ -3176,6 +3296,16 @@
"@types/node": "*"
}
},
"node_modules/@types/bull": {
"version": "4.10.0",
"resolved": "https://registry.npmjs.org/@types/bull/-/bull-4.10.0.tgz",
"integrity": "sha512-RkYW8K2H3J76HT6twmHYbzJ0GtLDDotpLP9ah9gtiA7zfF6peBH1l5fEiK0oeIZ3/642M7Jcb9sPmor8Vf4w6g==",
"deprecated": "This is a stub types definition. bull provides its own type definitions, so you do not need this installed.",
"dev": true,
"dependencies": {
"bull": "*"
}
},
"node_modules/@types/chai": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.4.tgz",
@ -7192,9 +7322,9 @@
}
},
"node_modules/engine.io": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.1.tgz",
"integrity": "sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz",
"integrity": "sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==",
"dependencies": {
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
@ -8054,6 +8184,16 @@
"node": ">=6"
}
},
"node_modules/eventemitter2": {
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
"integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg=="
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@ -13025,9 +13165,9 @@
}
},
"node_modules/nc-plugin": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/nc-plugin/-/nc-plugin-0.1.2.tgz",
"integrity": "sha512-9NZJPIgD6r3GnCNJelfZFga+RQPDuHZCoFus2qaZsNdbJEP7LSTOeZ1Pw8l89fvVbp0VFLO3Bcg8ggJppKt8JQ==",
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/nc-plugin/-/nc-plugin-0.1.3.tgz",
"integrity": "sha512-QosW02G6fk/32wc7jKqxCy0sm43cS2cvfkedP2fa8VVeLP6Q6tvhpm/k1boxbCRaLNbQx59F/PNBhogXFRO23g==",
"dependencies": {
"@bitauth/libauth": "^1.17.1",
"nc-common": "0.0.6"
@ -13066,13 +13206,8 @@
}
},
"node_modules/nocodb-sdk": {
"version": "0.107.0-beta.0",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.107.0-beta.0.tgz",
"integrity": "sha512-WMWl5HiSbc4e0Nr/VYk1hbZUdjKiIT0TFdT8UxUqpH++W5K+rpzBEn+i3FtLE2BvrsP6pfFxYUgtB9xv4MtnBQ==",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
}
"resolved": "../nocodb-sdk",
"link": true
},
"node_modules/node-abort-controller": {
"version": "3.1.1",
@ -13649,6 +13784,14 @@
"node": ">=0.10.0"
}
},
"node_modules/p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
"engines": {
"node": ">=4"
}
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@ -13693,6 +13836,32 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-queue": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
"dependencies": {
"eventemitter3": "^4.0.4",
"p-timeout": "^3.2.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-timeout": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
"dependencies": {
"p-finally": "^1.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
@ -19954,6 +20123,23 @@
"integrity": "sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==",
"optional": true
},
"@nestjs/bull": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/@nestjs/bull/-/bull-0.6.3.tgz",
"integrity": "sha512-CckH9O3t9qSiO4RCzdYvtFSaaMfIhTXMYagV/rtmVvI1SX5XNnxEaQXvtjxDBXF9DB1JE/5AejIl6ICym+MJIw==",
"requires": {
"@nestjs/bull-shared": "^0.1.3",
"tslib": "2.5.0"
}
},
"@nestjs/bull-shared": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-0.1.3.tgz",
"integrity": "sha512-K0a1ERpnl/ZnTmm0UtYSSClDlDkQwNNwJYM6PogzpeflD64oqwVIn8Pj8rdS+BOYUxqdDy55q3p67ytO5oaVDA==",
"requires": {
"tslib": "2.5.0"
}
},
"@nestjs/cli": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.3.0.tgz",
@ -20058,6 +20244,14 @@
"uid": "2.0.2"
}
},
"@nestjs/event-emitter": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-1.4.1.tgz",
"integrity": "sha512-PmLpzMYgEKJNxOUrRjb6kNSm2PC6J+BeLTuF/bkYViGM/mVGvYOgU5jq8DQnXmiSmDmyWN+tO2cHSnR7odJJRA==",
"requires": {
"eventemitter2": "6.4.9"
}
},
"@nestjs/jwt": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.0.3.tgz",
@ -20128,6 +20322,15 @@
}
}
},
"@nestjs/platform-socket.io": {
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-9.4.0.tgz",
"integrity": "sha512-pk5uWItnsrFKzvQrFcAmyfcb8cpGgoj4yR4+vbA5H/MLcv+8vGqruQO8riN8jAYGNPN9Y02ihBKbIvQqn92M5g==",
"requires": {
"socket.io": "4.6.1",
"tslib": "2.5.0"
}
},
"@nestjs/schematics": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-9.1.0.tgz",
@ -20164,6 +20367,16 @@
"tslib": "2.5.0"
}
},
"@nestjs/websockets": {
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-9.4.0.tgz",
"integrity": "sha512-RATR9C0cKhXp3mTQAg75iItyUuRhVwU39Xe/kl0XLpvAhWzhnGrn6CxSTRRzBfp3F68DOKvs7/ODDY51f+rdXw==",
"requires": {
"iterare": "1.2.1",
"object-hash": "3.0.0",
"tslib": "2.5.0"
}
},
"@nestjsplus/dyn-schematics": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@nestjsplus/dyn-schematics/-/dyn-schematics-1.0.12.tgz",
@ -20615,6 +20828,15 @@
"@types/node": "*"
}
},
"@types/bull": {
"version": "4.10.0",
"resolved": "https://registry.npmjs.org/@types/bull/-/bull-4.10.0.tgz",
"integrity": "sha512-RkYW8K2H3J76HT6twmHYbzJ0GtLDDotpLP9ah9gtiA7zfF6peBH1l5fEiK0oeIZ3/642M7Jcb9sPmor8Vf4w6g==",
"dev": true,
"requires": {
"bull": "*"
}
},
"@types/chai": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.4.tgz",
@ -23779,9 +24001,9 @@
}
},
"engine.io": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.1.tgz",
"integrity": "sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz",
"integrity": "sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==",
"requires": {
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
@ -24415,6 +24637,16 @@
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
},
"eventemitter2": {
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
"integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg=="
},
"eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
"events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@ -28218,9 +28450,9 @@
}
},
"nc-plugin": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/nc-plugin/-/nc-plugin-0.1.2.tgz",
"integrity": "sha512-9NZJPIgD6r3GnCNJelfZFga+RQPDuHZCoFus2qaZsNdbJEP7LSTOeZ1Pw8l89fvVbp0VFLO3Bcg8ggJppKt8JQ==",
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/nc-plugin/-/nc-plugin-0.1.3.tgz",
"integrity": "sha512-QosW02G6fk/32wc7jKqxCy0sm43cS2cvfkedP2fa8VVeLP6Q6tvhpm/k1boxbCRaLNbQx59F/PNBhogXFRO23g==",
"requires": {
"@bitauth/libauth": "^1.17.1",
"nc-common": "0.0.6"
@ -28247,12 +28479,22 @@
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="
},
"nocodb-sdk": {
"version": "0.107.0-beta.0",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.107.0-beta.0.tgz",
"integrity": "sha512-WMWl5HiSbc4e0Nr/VYk1hbZUdjKiIT0TFdT8UxUqpH++W5K+rpzBEn+i3FtLE2BvrsP6pfFxYUgtB9xv4MtnBQ==",
"version": "file:../nocodb-sdk",
"requires": {
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"axios": "^0.21.1",
"jsep": "^1.3.6"
"cspell": "^4.1.0",
"eslint": "^7.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^4.0.0",
"jsep": "^1.3.6",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.1",
"typescript": "^4.0.2"
}
},
"node-abort-controller": {
@ -28672,6 +28914,11 @@
"minimist": "^1.1.0"
}
},
"p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="
},
"p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@ -28698,6 +28945,23 @@
"aggregate-error": "^3.0.0"
}
},
"p-queue": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
"requires": {
"eventemitter3": "^4.0.4",
"p-timeout": "^3.2.0"
}
},
"p-timeout": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
"requires": {
"p-finally": "^1.0.0"
}
},
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",

11
packages/nocodb/package.json

@ -42,13 +42,17 @@
"dependencies": {
"@google-cloud/storage": "^5.7.2",
"@graphql-tools/merge": "^6.0.12",
"@nestjs/bull": "^0.6.3",
"@nestjs/common": "^9.4.0",
"@nestjs/core": "^9.4.0",
"@nestjs/event-emitter": "^1.4.1",
"@nestjs/jwt": "^10.0.3",
"@nestjs/mapped-types": "*",
"@nestjs/passport": "^9.0.3",
"@nestjs/platform-express": "^9.4.0",
"@nestjs/platform-socket.io": "^9.4.0",
"@nestjs/serve-static": "^3.0.1",
"@nestjs/websockets": "^9.4.0",
"@sentry/node": "^6.3.5",
"@types/chai": "^4.2.12",
"airtable": "^0.11.3",
@ -61,6 +65,7 @@
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.0",
"boxen": "^5.0.0",
"bull": "^4.10.4",
"bullmq": "^1.81.1",
"clear": "^0.1.0",
"colors": "^1.4.0",
@ -107,12 +112,13 @@
"nanoid": "^3.1.20",
"nc-help": "^0.2.87",
"nc-lib-gui": "0.107.0-beta.0",
"nc-plugin": "0.1.2",
"nc-plugin": "^0.1.3",
"ncp": "^2.0.0",
"nocodb-sdk": "0.107.0-beta.0",
"nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"os-locale": "^6.0.2",
"p-queue": "^6.6.2",
"papaparse": "^5.3.1",
"parse-database-url": "^0.3.0",
"passport": "^0.4.1",
@ -145,6 +151,7 @@
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@nestjsplus/dyn-schematics": "^1.0.12",
"@types/bull": "^4.10.0",
"@types/express": "^4.17.13",
"@types/jest": "^29.5.0",
"@types/mocha": "^10.0.1",

6
packages/nocodb/src/Noco.ts

@ -3,12 +3,14 @@ import { NestFactory } from '@nestjs/core';
import clear from 'clear';
import * as express from 'express';
import NcToolGui from 'nc-lib-gui';
import { IoAdapter } from '@nestjs/platform-socket.io';
import { AppModule } from './app.module';
import { NC_LICENSE_KEY } from './constants';
import Store from './models/Store';
import type { Express } from 'express';
import type * as http from 'http';
import { IEventEmitter } from './modules/event-emitter/event-emitter.interface'
export default class Noco {
private static _this: Noco;
@ -30,6 +32,7 @@ export default class Noco {
}
public static config: any;
public static eventEmitter: IEventEmitter;
public readonly router: express.Router;
public readonly projectRouter: express.Router;
public static _ncMeta: any;
@ -96,6 +99,9 @@ export default class Noco {
this._server = server;
const nestApp = await NestFactory.create(AppModule);
nestApp.useWebSocketAdapter(new IoAdapter(httpServer));
nestApp.use(
express.json({ limit: process.env.NC_REQUEST_BODY_SIZE || '50mb' }),
);

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

@ -1,5 +1,7 @@
import { Module, RequestMethod } from '@nestjs/common';
import { Inject, Module, RequestMethod } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { BullModule } from '@nestjs/bull';
import { EventEmitterModule as NestJsEventEmitter } from '@nestjs/event-emitter';
import { Connection } from './connection/connection';
import { GlobalExceptionFilter } from './filters/global-exception/global-exception.filter';
import NcPluginMgrv2 from './helpers/NcPluginMgrv2';
@ -7,12 +9,15 @@ import { GlobalMiddleware } from './middlewares/global/global.middleware';
import { GuiMiddleware } from './middlewares/gui/gui.middleware';
import { PublicMiddleware } from './middlewares/public/public.middleware';
import { DatasModule } from './modules/datas/datas.module';
import { IEventEmitter } from './modules/event-emitter/event-emitter.interface';
import { EventEmitterModule } from './modules/event-emitter/event-emitter.module';
import { AuthService } from './services/auth.service';
import { UsersModule } from './modules/users/users.module';
import { MetaService } from './meta/meta.service';
import Noco from './Noco';
import { TestModule } from './modules/test/test.module';
import { GlobalModule } from './modules/global/global.module';
import { HookHandlerService } from './services/hook-handler.service';
import { LocalStrategy } from './strategies/local.strategy';
import { AuthTokenStrategy } from './strategies/authtoken.strategy/authtoken.strategy';
import { BaseViewStrategy } from './strategies/base-view.strategy/base-view.strategy';
@ -20,6 +25,7 @@ import NcConfigFactory from './utils/NcConfigFactory';
import NcUpgrader from './version-upgrader/NcUpgrader';
import { MetasModule } from './modules/metas/metas.module';
import NocoCache from './cache/NocoCache';
import { JobsModule } from './modules/jobs/jobs.module';
import type {
MiddlewareConsumer,
OnApplicationBootstrap,
@ -32,6 +38,16 @@ import type {
...(process.env['PLAYWRIGHT_TEST'] === 'true' ? [TestModule] : []),
MetasModule,
DatasModule,
EventEmitterModule,
JobsModule,
NestJsEventEmitter.forRoot(),
...(process.env['NC_REDIS_URL']
? [
BullModule.forRoot({
redis: process.env.NC_REDIS_URL,
}),
]
: []),
],
controllers: [],
providers: [
@ -43,12 +59,14 @@ import type {
LocalStrategy,
AuthTokenStrategy,
BaseViewStrategy,
HookHandlerService,
],
})
export class AppModule implements OnApplicationBootstrap {
constructor(
private readonly connection: Connection,
private readonly metaService: MetaService,
@Inject('IEventEmitter') private readonly eventEmitter: IEventEmitter,
) {}
// Global Middleware
@ -78,6 +96,7 @@ export class AppModule implements OnApplicationBootstrap {
// temporary hack
Noco._ncMeta = this.metaService;
Noco.config = this.connection.config;
Noco.eventEmitter = this.eventEmitter;
// init plugin manager
await NcPluginMgrv2.init(Noco.ncMeta);

6
packages/nocodb/src/controllers/imports/helpers/NocoSyncDestAdapter.ts

@ -1,6 +0,0 @@
export abstract class NocoSyncSourceAdapter {
public abstract init(): Promise<void>;
public abstract destProjectWrite(): Promise<any>;
public abstract destSchemaWrite(): Promise<any>;
public abstract destDataWrite(): Promise<any>;
}

7
packages/nocodb/src/controllers/imports/helpers/NocoSyncSourceAdapter.ts

@ -1,7 +0,0 @@
export abstract class NocoSyncSourceAdapter {
public abstract init(): Promise<void>;
public abstract srcSchemaGet(): Promise<any>;
public abstract srcDataLoad(): Promise<any>;
public abstract srcDataListen(): Promise<any>;
public abstract srcDataPoll(): Promise<any>;
}

2480
packages/nocodb/src/controllers/imports/helpers/job.ts

File diff suppressed because it is too large Load Diff

21
packages/nocodb/src/controllers/imports/import.controller.spec.ts

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

148
packages/nocodb/src/controllers/imports/import.controller.ts

@ -1,148 +0,0 @@
import { Controller, HttpCode, Post, Request, UseGuards } from '@nestjs/common';
import { forwardRef, Inject } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { GlobalGuard } from '../../guards/global/global.guard';
import { NcError } from '../../helpers/catchError';
import { ExtractProjectIdMiddleware } from '../../middlewares/extract-project-id/extract-project-id.middleware';
import { SyncSource } from '../../models';
import NocoJobs from '../../jobs/NocoJobs';
import { SocketService } from '../../services/socket.service';
import airtableSyncJob from './helpers/job';
import type { AirtableSyncConfig } from './helpers/job';
import type { Server } from 'socket.io';
const AIRTABLE_IMPORT_JOB = 'AIRTABLE_IMPORT_JOB';
const AIRTABLE_PROGRESS_JOB = 'AIRTABLE_PROGRESS_JOB';
enum SyncStatus {
PROGRESS = 'PROGRESS',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
}
const initJob = (sv: Server, jobs: { [p: string]: { last_message: any } }) => {
// add importer job handler and progress notification job handler
NocoJobs.jobsMgr.addJobWorker(AIRTABLE_IMPORT_JOB, airtableSyncJob);
NocoJobs.jobsMgr.addJobWorker(
AIRTABLE_PROGRESS_JOB,
({ payload, progress }) => {
sv.to(payload?.id).emit('progress', {
msg: progress?.msg,
level: progress?.level,
status: progress?.status,
});
if (payload?.id in jobs) {
jobs[payload?.id].last_message = {
msg: progress?.msg,
level: progress?.level,
status: progress?.status,
};
}
},
);
NocoJobs.jobsMgr.addProgressCbk(AIRTABLE_IMPORT_JOB, (payload, progress) => {
NocoJobs.jobsMgr.add(AIRTABLE_PROGRESS_JOB, {
payload,
progress: {
msg: progress?.msg,
level: progress?.level,
status: progress?.status,
},
});
});
NocoJobs.jobsMgr.addSuccessCbk(AIRTABLE_IMPORT_JOB, (payload) => {
NocoJobs.jobsMgr.add(AIRTABLE_PROGRESS_JOB, {
payload,
progress: {
msg: 'Complete!',
status: SyncStatus.COMPLETED,
},
});
delete jobs[payload?.id];
});
NocoJobs.jobsMgr.addFailureCbk(AIRTABLE_IMPORT_JOB, (payload, error: any) => {
NocoJobs.jobsMgr.add(AIRTABLE_PROGRESS_JOB, {
payload,
progress: {
msg: error?.message || 'Failed due to some internal error',
status: SyncStatus.FAILED,
},
});
delete jobs[payload?.id];
});
};
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class ImportController {
constructor(
private readonly socketService: SocketService,
@Inject(forwardRef(() => ModuleRef)) private readonly moduleRef: ModuleRef,
) {}
@Post('/api/v1/db/meta/import/airtable')
@HttpCode(200)
importAirtable(@Request() req) {
NocoJobs.jobsMgr.add(AIRTABLE_IMPORT_JOB, {
id: req.query.id,
...req.body,
});
return {};
}
@Post('/api/v1/db/meta/syncs/:syncId/trigger')
@HttpCode(200)
async triggerSync(@Request() req) {
if (req.params.syncId in this.socketService.jobs) {
NcError.badRequest('Sync already in progress');
}
const syncSource = await SyncSource.get(req.params.syncId);
const user = await syncSource.getUser();
// Treat default baseUrl as siteUrl from req object
let baseURL = (req as any).ncSiteUrl;
// if environment value avail use it
// or if it's docker construct using `PORT`
if (process.env.NC_DOCKER) {
baseURL = `http://localhost:${process.env.PORT || 8080}`;
}
setTimeout(() => {
NocoJobs.jobsMgr.add<AirtableSyncConfig>(AIRTABLE_IMPORT_JOB, {
id: req.params.syncId,
...(syncSource?.details || {}),
projectId: syncSource.project_id,
baseId: syncSource.base_id,
authToken: '',
baseURL,
user: user,
moduleRef: this.moduleRef,
});
}, 1000);
this.socketService.jobs[req.params.syncId] = {
last_message: {
msg: 'Sync started',
},
};
return {};
}
@Post('/api/v1/db/meta/syncs/:syncId/abort')
@HttpCode(200)
async abortImport(@Request() req) {
if (req.params.syncId in this.socketService.jobs) {
delete this.socketService.jobs[req.params.syncId];
}
return {};
}
async onModuleInit() {
initJob(this.socketService.io, this.socketService.jobs);
}
}

2
packages/nocodb/src/controllers/tables.controller.ts

@ -99,7 +99,7 @@ export class TablesController {
await this.tablesService.tableUpdate({
tableId: tableId,
table: body,
projectId: req.user,
projectId: req.ncProjectId,
});
return { msg: 'The table has been updated successfully' };
}

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

@ -4,11 +4,16 @@ import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2';
import Noco from '../../../Noco';
import User from '../../../models/User';
import NocoCache from '../../../cache/NocoCache';
import { CacheScope } from '../../../utils/globals';
import {
CacheDelDirection,
CacheScope,
MetaTable,
} from '../../../utils/globals';
import ProjectUser from '../../../models/ProjectUser';
import resetPgSakilaProject from './resetPgSakilaProject';
import resetMysqlSakilaProject from './resetMysqlSakilaProject';
import resetMetaSakilaSqliteProject from './resetMetaSakilaSqliteProject';
import type ApiToken from '../../../models/ApiToken';
const workerStatus = {};
@ -80,6 +85,7 @@ export class TestResetService {
try {
await removeAllProjectCreatedByTheTest(this.parallelId);
await removeAllPrefixedUsersExceptSuper(this.parallelId);
await removeAllTokensCreatedByTheTest(this.parallelId);
} catch (e) {
console.log(`Error in cleaning up project: ${this.parallelId}`, e);
}
@ -174,6 +180,28 @@ const removeAllPrefixedUsersExceptSuper = async (parallelId: string) => {
}
};
const removeAllTokensCreatedByTheTest = async (parallelId: string) => {
const tokens: ApiToken[] = await Noco.ncMeta.metaList(
null,
null,
MetaTable.API_TOKENS,
);
for (const token of tokens) {
if (token.description.startsWith(`nc_test_${parallelId}`)) {
await NocoCache.deepDel(
CacheScope.API_TOKEN,
`${CacheScope.API_TOKEN}:${token.token}`,
CacheDelDirection.CHILD_TO_PARENT,
);
await Noco.ncMeta.metaDelete(null, null, MetaTable.API_TOKENS, {
token: token.token,
});
}
}
};
// todo: Remove this once user deletion improvement PR is merged
const removeProjectUsersFromCache = async (project: Project) => {
const projectUsers: ProjectUser[] = await ProjectUser.getUsersList({

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

@ -11,24 +11,17 @@ import {
UITypes,
ViewTypes,
} from 'nocodb-sdk';
import ejs from 'ejs';
import Validator from 'validator';
import { customAlphabet } from 'nanoid';
import DOMPurify from 'isomorphic-dompurify';
import { v4 as uuidv4 } from 'uuid';
import { NcError } from '../helpers/catchError';
import getAst from '../helpers/getAst';
import NcPluginMgrv2 from '../helpers/NcPluginMgrv2';
import {
_transformSubmittedFormDataForEmail,
invokeWebhook,
} from '../helpers/webhookHelpers';
import {
Audit,
Column,
Filter,
FormView,
Hook,
Model,
Project,
Sort,
@ -40,7 +33,8 @@ import {
COMPARISON_SUB_OPS,
IS_WITHIN_COMPARISON_SUB_OPS,
} from '../models/Filter';
import formSubmissionEmailTemplate from '../utils/common/formSubmissionEmailTemplate';
import Noco from '../Noco'
import { HANDLE_WEBHOOK } from '../services/hook-handler.service'
import formulaQueryBuilderv2 from './formulav2/formulaQueryBuilderv2';
import genRollupSelectv2 from './genRollupSelectv2';
import conditionV2 from './conditionV2';
@ -2097,13 +2091,20 @@ class BaseModelSqlv2 {
{
chunkSize: _chunkSize = 100,
cookie,
foreign_key_checks = true,
raw = false,
}: {
chunkSize?: number;
cookie?: any;
foreign_key_checks?: boolean;
raw?: boolean;
} = {},
) {
try {
const insertDatas = await Promise.all(
// TODO: ag column handling for raw bulk insert
const insertDatas = raw
? datas
: await Promise.all(
datas.map(async (d) => {
await populatePk(this.model, d);
return this.model.mapAliasToColumn(d);
@ -2112,27 +2113,45 @@ class BaseModelSqlv2 {
// await this.beforeInsertb(insertDatas, null);
if (!raw) {
for (const data of datas) {
await this.validate(data);
}
}
// fallbacks to `10` if database client is sqlite
// to avoid `too many SQL variables` error
// refer : https://www.sqlite.org/limits.html
const chunkSize = this.isSqlite ? 10 : _chunkSize;
const trx = await this.dbDriver.transaction();
if (!foreign_key_checks) {
if (this.isPg) {
await trx.raw('set session_replication_role to replica;');
} else if (this.isMySQL) {
await trx.raw('SET foreign_key_checks = 0;');
}
}
const response =
this.isPg || this.isMssql
? await this.dbDriver
? await trx
.batchInsert(this.tnPath, insertDatas, chunkSize)
.returning(this.model.primaryKey?.column_name)
: await this.dbDriver.batchInsert(
this.tnPath,
insertDatas,
chunkSize,
);
: await trx.batchInsert(this.tnPath, insertDatas, chunkSize);
if (!foreign_key_checks) {
if (this.isPg) {
await trx.raw('set session_replication_role to origin;');
} else if (this.isMySQL) {
await trx.raw('SET foreign_key_checks = 1;');
}
}
await trx.commit();
await this.afterBulkInsert(insertDatas, this.dbDriver, cookie);
if (!raw) await this.afterBulkInsert(insertDatas, this.dbDriver, cookie);
return response;
} catch (e) {
@ -2141,12 +2160,17 @@ class BaseModelSqlv2 {
}
}
async bulkUpdate(datas: any[], { cookie }: { cookie?: any } = {}) {
async bulkUpdate(
datas: any[],
{ cookie, raw = false }: { cookie?: any; raw?: boolean } = {},
) {
let transaction;
try {
const updateDatas = await Promise.all(
datas.map((d) => this.model.mapAliasToColumn(d)),
);
if (raw) await this.model.getColumns();
const updateDatas = raw
? datas
: await Promise.all(datas.map((d) => this.model.mapAliasToColumn(d)));
const prevData = [];
const newData = [];
@ -2154,13 +2178,13 @@ class BaseModelSqlv2 {
const toBeUpdated = [];
const res = [];
for (const d of updateDatas) {
await this.validate(d);
if (!raw) await this.validate(d);
const pkValues = await this._extractPksValues(d);
if (!pkValues) {
// pk not specified - bypass
continue;
}
prevData.push(await this.readByPk(pkValues));
if (!raw) prevData.push(await this.readByPk(pkValues));
const wherePk = await this._wherePk(pkValues);
res.push(wherePk);
toBeUpdated.push({ d, wherePk });
@ -2175,10 +2199,13 @@ class BaseModelSqlv2 {
await transaction.commit();
if (!raw) {
for (const pkValues of updatePkValues) {
newData.push(await this.readByPk(pkValues));
}
}
if (!raw)
await this.afterBulkUpdate(prevData, newData, this.dbDriver, cookie);
return res;
@ -2496,6 +2523,18 @@ class BaseModelSqlv2 {
}
private async handleHooks(hookName, prevData, newData, req): Promise<void> {
Noco.eventEmitter.emit(HANDLE_WEBHOOK, {
hookName,
prevData,
newData,
user: req?.user,
viewId: this.viewId,
modelId: this.model.id,
tnPath: this.tnPath,
})
/*
const view = await View.get(this.viewId);
// handle form view data submission
@ -2585,7 +2624,7 @@ class BaseModelSqlv2 {
}
} catch (e) {
console.log('hooks :: error', hookName, e);
}
}*/
}
// @ts-ignore

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

@ -2966,6 +2966,50 @@ class KnexClient extends SqlClient {
unsanitize(str) {
return str.replace(/\\[?]/g, '?');
}
sanitiseDataType(dt: string) {
// allow only alphanumeric and space
// eg: varchar, int, bigint, text, character varying, etc
if (/^[\w -]+(?:\(\d+(?:\s?,\s?\d+)?\))?$/.test(dt)) return dt;
throw new Error(`Invalid data type: ${dt}`);
}
// todo: add support to complex default values with functions and expressions
sanitiseDefaultValue(value: string | number | boolean) {
if (value === null || value === undefined) return undefined;
if (typeof value === 'string') {
// if value is null/true/false return as is
if (['NULL', 'null', 'TRUE', 'true', 'FALSE', 'false'].includes(value))
return value;
// if value is a number, return as is
if (/^\d+(\.\d+)?$/.test(value)) return value;
// if value is a function, return as is
// for example: CURRENT_TIMESTAMP(), NOW(), UUID(), etc
if (/^\w+\(\)$/.test(value)) return value;
// if value is a CURRENT_TIMESTAMP, return as is
if (/^CURRENT_TIMESTAMP[\w ]*$/.test(value.toUpperCase())) return value;
// if value wrapped in single/double quotes, then extract value and sanitise
const m = value.match(/^(['"])(.*)\1$/);
if (m) {
return this.genQuery('?', [
// escape for single/double quotes no longer needed remove it
m[2].replace(m[1] === '"' ? /\\"/g : /\\'/g, m[1]),
]);
}
// if any other type of string, just sanitise and return
return this.genQuery('?', [value]);
} else {
// if any other type of value, just sanitise and return
return this.genQuery('?', [value]);
}
}
}
// expose class

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

@ -2577,7 +2577,7 @@ class MssqlClient extends KnexClient {
alterTableColumn(t, n, o, existingQuery, change = 2) {
let query = '';
const defaultValue = getDefaultValue(n);
const defaultValue = this.sanitiseDefaultValue(n.cdf);
const shouldSanitize = true;
const scaleAndPrecision =
!getDefaultLengthIsDisabled(n.dt) && n.dtxp
@ -2586,7 +2586,11 @@ class MssqlClient extends KnexClient {
if (change === 0) {
query = existingQuery ? ',' : '';
query += this.genQuery(`?? ${n.dt}`, [n.cn], shouldSanitize);
query += this.genQuery(
`?? ${this.sanitiseDataType(n.dt)}`,
[n.cn],
shouldSanitize,
);
query += scaleAndPrecision;
query += n.rqd ? ' NOT NULL' : ' NULL';
query += n.ai ? ' IDENTITY(1,1)' : ' ';
@ -2601,7 +2605,11 @@ class MssqlClient extends KnexClient {
n.default_constraint_name = `DF_${t}_${n.cn}`;
}
} else if (change === 1) {
query += this.genQuery(` ADD ?? ${n.dt}`, [n.cn], shouldSanitize);
query += this.genQuery(
` ADD ?? ${this.sanitiseDataType(n.dt)}`,
[n.cn],
shouldSanitize,
);
query += scaleAndPrecision;
query += n.rqd ? ' NOT NULL' : ' NULL';
query += n.ai ? ' IDENTITY(1,1)' : ' ';
@ -2639,7 +2647,9 @@ class MssqlClient extends KnexClient {
n.rqd !== o.rqd
) {
query += this.genQuery(
`\nALTER TABLE ?? ALTER COLUMN ?? ${n.dt}${scaleAndPrecision}`,
`\nALTER TABLE ?? ALTER COLUMN ?? ${this.sanitiseDataType(
n.dt,
)}${scaleAndPrecision}`,
[this.getTnPath(t), n.cn],
shouldSanitize,
);
@ -2655,7 +2665,9 @@ class MssqlClient extends KnexClient {
);
if (n.cdf) {
query += this.genQuery(
`\nALTER TABLE ?? ADD CONSTRAINT ?? DEFAULT ${n.cdf} FOR ??;`,
`\nALTER TABLE ?? ADD CONSTRAINT ?? DEFAULT ${this.sanitiseDefaultValue(
n.cdf,
)} FOR ??;`,
[this.getTnPath(t), `DF_${n.tn}_${n.cn}`, n.cn],
shouldSanitize,
);
@ -2708,60 +2720,6 @@ class MssqlClient extends KnexClient {
}
}
function getDefaultValue(n) {
if (n.cdf === undefined || n.cdf === null) return n.cdf;
switch (n.dt) {
case 'boolean':
case 'bool':
case 'tinyint':
case 'int':
case 'samllint':
case 'bigint':
case 'integer':
case 'smallint':
case 'mediumint':
case 'int2':
case 'int4':
case 'int8':
case 'long':
case 'serial':
case 'bigserial':
case 'smallserial':
case 'number':
case 'float':
case 'double':
case 'decimal':
case 'numeric':
case 'real':
case 'double precision':
case 'money':
case 'smallmoney':
case 'dec':
return n.cdf;
break;
case 'datetime':
case 'timestamp':
case 'date':
case 'time':
if (
n.cdf.toLowerCase().indexOf('getdate') > -1 ||
/\(([\d\w'", ]*)\)$/.test(n.cdf)
) {
return n.cdf;
}
return JSON.stringify(n.cdf);
break;
case 'text':
case 'ntext':
return `'${n.cdf}'`;
break;
default:
return JSON.stringify(n.cdf);
break;
}
}
function getDefaultLengthIsDisabled(type) {
switch (type) {
case 'datetimeoffset':

58
packages/nocodb/src/db/sql-client/lib/mysql/MysqlClient.ts

@ -2463,18 +2463,18 @@ class MysqlClient extends KnexClient {
query += this.genQuery(
`
CHANGE
COLUMN ?? ?? ${n.dt}`,
COLUMN ?? ?? ${this.sanitiseDataType(n.dt)}`,
[o.cn, n.cn],
);
} else if (change === 1) {
query += this.genQuery(
`
ADD
COLUMN ?? ${n.dt}`,
COLUMN ?? ${this.sanitiseDataType(n.dt)}`,
[n.cn],
);
} else {
query += this.genQuery(` ?? ${n.dt}`, [n.cn]);
query += this.genQuery(` ?? ${this.sanitiseDataType(n.dt)}`, [n.cn]);
}
if (!n.dt.endsWith('text')) {
query += n.dtxp && n.dtxp !== ' ' ? `(${n.dtxp}` : '';
@ -2484,7 +2484,7 @@ class MysqlClient extends KnexClient {
query += n.un ? ' UNSIGNED' : '';
query += n.rqd ? ' NOT NULL' : ' NULL';
query += n.ai ? ' auto_increment' : '';
const defaultValue = getDefaultValue(n);
const defaultValue = this.sanitiseDefaultValue(n.cdf);
query += defaultValue
? `
DEFAULT ${defaultValue}`
@ -2576,54 +2576,4 @@ class MysqlClient extends KnexClient {
}
}
function getDefaultValue(n) {
if (n.cdf === undefined || n.cdf === null) return n.cdf;
switch (n.dt) {
case 'boolean':
case 'bool':
case 'tinyint':
case 'int':
case 'samllint':
case 'bigint':
case 'integer':
case 'smallint':
case 'mediumint':
case 'int2':
case 'int4':
case 'int8':
case 'long':
case 'serial':
case 'bigserial':
case 'smallserial':
case 'number':
case 'float':
case 'double':
case 'decimal':
case 'numeric':
case 'real':
case 'double precision':
case 'money':
case 'smallmoney':
case 'dec':
return n.cdf;
break;
case 'datetime':
case 'timestamp':
case 'date':
case 'time':
if (
n.cdf.indexOf('CURRENT_TIMESTAMP') > -1 ||
/\(([\d\w'", ]*)\)$/.test(n.cdf)
) {
return n.cdf;
}
return JSON.stringify(n.cdf);
break;
default:
return JSON.stringify(n.cdf);
break;
}
}
export default MysqlClient;

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

@ -2685,7 +2685,7 @@ class PGClient extends KnexClient {
alterTableColumn(t, n, o, existingQuery, change = 2) {
let query = '';
const defaultValue = getDefaultValue(n);
const defaultValue = this.sanitiseDefaultValue(n.cdf);
const shouldSanitize = true;
if (change === 0) {
@ -2699,12 +2699,20 @@ class PGClient extends KnexClient {
query += this.genQuery(` ?? serial`, [n.cn], shouldSanitize);
}
} else {
query += this.genQuery(` ?? ${n.dt}`, [n.cn], shouldSanitize);
query += this.genQuery(
` ?? ${this.sanitiseDataType(n.dt)}`,
[n.cn],
shouldSanitize,
);
query += n.rqd ? ' NOT NULL' : ' NULL';
query += defaultValue ? ` DEFAULT ${defaultValue}` : '';
}
} else if (change === 1) {
query += this.genQuery(` ADD ?? ${n.dt}`, [n.cn], shouldSanitize);
query += this.genQuery(
` ADD ?? ${this.sanitiseDataType(n.dt)}`,
[n.cn],
shouldSanitize,
);
query += n.rqd ? ' NOT NULL' : ' NULL';
query += defaultValue ? ` DEFAULT ${defaultValue}` : '';
query = this.genQuery(`ALTER TABLE ?? ${query};`, [t], shouldSanitize);
@ -2719,7 +2727,9 @@ class PGClient extends KnexClient {
if (n.dt !== o.dt) {
query += this.genQuery(
`\nALTER TABLE ?? ALTER COLUMN ?? TYPE ${n.dt} USING ??::${n.dt};\n`,
`\nALTER TABLE ?? ALTER COLUMN ?? TYPE ${this.sanitiseDataType(
n.dt,
)} USING ??::${this.sanitiseDataType(n.dt)};\n`,
[t, n.cn, n.cn],
shouldSanitize,
);
@ -2740,7 +2750,9 @@ class PGClient extends KnexClient {
[t, n.cn],
shouldSanitize,
);
query += n.cdf ? ` SET DEFAULT ${n.cdf};\n` : ` DROP DEFAULT;\n`;
query += n.cdf
? ` SET DEFAULT ${this.sanitiseDefaultValue(n.cdf)};\n`
: ` DROP DEFAULT;\n`;
}
}
return query;
@ -2782,57 +2794,4 @@ class PGClient extends KnexClient {
return result;
}
}
function getDefaultValue(n) {
if (n.cdf === undefined || n.cdf === null) return n.cdf;
switch (n.dt) {
case 'serial':
case 'bigserial':
case 'smallserial':
return '';
break;
case 'boolean':
case 'bool':
case 'tinyint':
case 'int':
case 'samllint':
case 'bigint':
case 'integer':
case 'mediumint':
case 'int2':
case 'int4':
case 'int8':
case 'long':
case 'number':
case 'float':
case 'double':
case 'decimal':
case 'numeric':
case 'real':
case 'double precision':
case 'money':
case 'smallmoney':
case 'dec':
return n.cdf;
break;
case 'datetime':
case 'timestamp':
case 'date':
case 'time':
if (
n.cdf.indexOf('CURRENT_TIMESTAMP') > -1 ||
/\(([\d\w'", ]*)\)$/.test(n.cdf)
) {
return n.cdf;
}
// return JSON.stringify(n.cdf);
break;
default:
// return JSON.stringify(n.cdf);
break;
}
return n.cdf;
}
export default PGClient;

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

@ -2474,7 +2474,7 @@ class SnowflakeClient extends KnexClient {
alterTableColumn(t, n, o, existingQuery, change = 2) {
let query = '';
const defaultValue = getDefaultValue(n);
const defaultValue = this.sanitiseDefaultValue(n);
const shouldSanitize = true;
if (change === 0) {
@ -2486,13 +2486,21 @@ class SnowflakeClient extends KnexClient {
shouldSanitize,
);
} else {
query += this.genQuery(` ?? ${n.dt}`, [n.cn], shouldSanitize);
query += this.genQuery(
` ?? ${this.sanitiseDataType(n.dt)}`,
[n.cn],
shouldSanitize,
);
query += n.dtxp && n.dt !== 'text' ? `(${n.dtxp})` : '';
query += n.rqd ? ' NOT NULL' : ' NULL';
query += defaultValue ? ` DEFAULT ${defaultValue}` : '';
}
} else if (change === 1) {
query += this.genQuery(` ADD ?? ${n.dt}`, [n.cn], shouldSanitize);
query += this.genQuery(
` ADD ?? ${this.sanitiseDataType(n.dt)}`,
[n.cn],
shouldSanitize,
);
query += n.dtxp && n.dt !== 'text' ? `(${n.dtxp})` : '';
query += n.rqd ? ' NOT NULL' : ' NULL';
query += defaultValue ? ` DEFAULT ${defaultValue}` : '';
@ -2512,7 +2520,9 @@ class SnowflakeClient extends KnexClient {
if (n.dt !== o.dt) {
query += this.genQuery(
`\nALTER TABLE ?? ALTER COLUMN ?? SET DATA TYPE ${n.dt}`,
`\nALTER TABLE ?? ALTER COLUMN ?? SET DATA TYPE ${this.sanitiseDataType(
n.dt,
)}`,
[this.getTnPath(t), n.cn],
shouldSanitize,
);
@ -2534,7 +2544,9 @@ class SnowflakeClient extends KnexClient {
[this.getTnPath(t), n.cn],
shouldSanitize,
);
query += n.cdf ? ` SET DEFAULT ${n.cdf};\n` : ` DROP DEFAULT;\n`;
query += n.cdf
? ` SET DEFAULT ${this.sanitiseDefaultValue(n.cdf)};\n`
: ` DROP DEFAULT;\n`;
}
}
return query;
@ -2606,53 +2618,4 @@ class SnowflakeClient extends KnexClient {
}
}
function getDefaultValue(n) {
if (n.cdf === undefined || n.cdf === null) return n.cdf;
switch (n.dt) {
case 'serial':
case 'bigserial':
case 'smallserial':
return '';
break;
case 'boolean':
case 'bool':
case 'tinyint':
case 'int':
case 'samllint':
case 'bigint':
case 'integer':
case 'mediumint':
case 'int2':
case 'int4':
case 'int8':
case 'long':
case 'number':
case 'float':
case 'double':
case 'decimal':
case 'numeric':
case 'real':
case 'double precision':
case 'money':
case 'smallmoney':
case 'dec':
return n.cdf;
break;
case 'datetime':
case 'timestamp':
case 'date':
case 'time':
if (
n.cdf.indexOf('CURRENT_TIMESTAMP') > -1 ||
/\(([\d\w'", ]*)\)$/.test(n.cdf)
) {
return n.cdf;
}
break;
default:
break;
}
return n.cdf;
}
export default SnowflakeClient;

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

@ -200,7 +200,7 @@ class SqliteClient extends KnexClient {
table.integer('status').nullable();
table.dateTime('created');
table.timestamps();
},
}
);
log.debug('Table created:', `${args.tn}`, data);
} else {
@ -295,7 +295,7 @@ class SqliteClient extends KnexClient {
try {
const response = await this.sqlClient.raw(
`SELECT name as tn FROM sqlite_master where type = 'table'`,
`SELECT name as tn FROM sqlite_master where type = 'table'`
);
result.data.list = [];
@ -359,7 +359,7 @@ class SqliteClient extends KnexClient {
try {
const response = await this.sqlClient.raw(
`PRAGMA table_info("${args.tn}")`,
`PRAGMA table_info("${args.tn}")`
);
const triggerList = (await this.triggerList(args)).data.list;
@ -409,7 +409,7 @@ class SqliteClient extends KnexClient {
response[i].not_nullable = response[i].notnull === 1;
response[i].rqd = response[i].notnull === 1;
response[i].cdf = response[i].dflt_value;
response[i].pk = response[i].pk === 1;
response[i].pk = response[i].pk > 0;
response[i].cop = response[i].cid;
// https://stackoverflow.com/a/7906029
@ -420,8 +420,7 @@ class SqliteClient extends KnexClient {
response[i].dtxs = '';
response[i].au = !!triggerList.find(
({ trigger }) =>
trigger === `xc_trigger_${args.tn}_${response[i].cn}`,
({ trigger }) => trigger === `xc_trigger_${args.tn}_${response[i].cn}`
);
}
@ -467,7 +466,7 @@ class SqliteClient extends KnexClient {
// PRAGMA index_xinfo('idx_fk_original_language_id');
const response = await this.sqlClient.raw(
`PRAGMA index_list("${args.tn}")`,
`PRAGMA index_list("${args.tn}")`
);
const rows = [];
@ -479,7 +478,7 @@ class SqliteClient extends KnexClient {
response[i].unique = response[i].unique === 1 ? 1 : 0;
const colsInIndex = await this.sqlClient.raw(
`PRAGMA index_info('${response[i].key_name}')`,
`PRAGMA index_info('${response[i].key_name}')`
);
if (colsInIndex.length === 1) {
@ -532,7 +531,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`PRAGMA foreign_key_list('${args.tn}')`,
`PRAGMA foreign_key_list('${args.tn}')`
);
for (let i = 0; i < response.length; ++i) {
@ -583,7 +582,7 @@ class SqliteClient extends KnexClient {
for (let i = 0; i < tables.length; ++i) {
const response = await this.sqlClient.raw(
`PRAGMA foreign_key_list('${tables[i].tn}')`,
`PRAGMA foreign_key_list('${tables[i].tn}')`
);
for (let j = 0; j < response.length; ++j) {
@ -634,7 +633,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`select *, name as trigger_name from sqlite_master where type = 'trigger' and tbl_name='${args.tn}';`,
`select *, name as trigger_name from sqlite_master where type = 'trigger' and tbl_name='${args.tn}';`
);
for (let i = 0; i < response.length; ++i) {
@ -677,7 +676,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`show function status where db='${args.databaseName}'`,
`show function status where db='${args.databaseName}'`
);
if (response.length === 2) {
@ -731,7 +730,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`show procedure status where db='${args.databaseName}'`,
`show procedure status where db='${args.databaseName}'`
);
if (response.length === 2) {
@ -776,7 +775,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`SELECT * FROM sqlite_master WHERE type = 'view'`,
`SELECT * FROM sqlite_master WHERE type = 'view'`
);
for (let i = 0; i < response.length; ++i) {
@ -814,7 +813,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`SHOW CREATE FUNCTION ${args.function_name};`,
`SHOW CREATE FUNCTION ${args.function_name};`
);
if (response.length === 2) {
@ -866,7 +865,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`show create procedure ${args.procedure_name};`,
`show create procedure ${args.procedure_name};`
);
if (response.length === 2) {
@ -912,7 +911,7 @@ class SqliteClient extends KnexClient {
try {
const response = await this.sqlClient.raw(
`SELECT * FROM sqlite_master WHERE type = 'view' AND name = '${args.view_name}'`,
`SELECT * FROM sqlite_master WHERE type = 'view' AND name = '${args.view_name}'`
);
for (let i = 0; i < response.length; ++i) {
@ -939,7 +938,7 @@ class SqliteClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`SHOW FULL TABLES IN ${args.databaseName} WHERE TABLE_TYPE LIKE 'VIEW';`,
`SHOW FULL TABLES IN ${args.databaseName} WHERE TABLE_TYPE LIKE 'VIEW';`
);
if (response.length === 2) {
@ -971,7 +970,7 @@ class SqliteClient extends KnexClient {
log.api(`${_func}:args:`, args);
const rows = await this.sqlClient.raw(
`create database ${args.database_name}`,
`create database ${args.database_name}`
);
return rows;
}
@ -982,7 +981,7 @@ class SqliteClient extends KnexClient {
log.api(`${_func}:args:`, args);
const rows = await this.sqlClient.raw(
`drop database ${args.database_name}`,
`drop database ${args.database_name}`
);
return rows;
}
@ -1012,7 +1011,7 @@ class SqliteClient extends KnexClient {
log.api(`${_func}:args:`, args);
const rows = await this.sqlClient.raw(
`DROP FUNCTION IF EXISTS ${args.function_name}`,
`DROP FUNCTION IF EXISTS ${args.function_name}`
);
return rows;
}
@ -1023,7 +1022,7 @@ class SqliteClient extends KnexClient {
log.api(`${_func}:args:`, args);
const rows = await this.sqlClient.raw(
`DROP PROCEDURE IF EXISTS ${args.procedure_name}`,
`DROP PROCEDURE IF EXISTS ${args.procedure_name}`
);
return rows;
}
@ -1043,7 +1042,7 @@ class SqliteClient extends KnexClient {
this._version = result.data.object;
log.debug(
`Version was empty for ${args.func}: population version for database as`,
this._version,
this._version
);
}
@ -1074,7 +1073,7 @@ class SqliteClient extends KnexClient {
log.api(`${func}:args:`, args);
try {
const rows = await this.sqlClient.raw(
`CREATE TRIGGER \`${args.function_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`,
`CREATE TRIGGER \`${args.function_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`
);
result.data.list = rows;
} catch (e) {
@ -1102,7 +1101,7 @@ class SqliteClient extends KnexClient {
try {
await this.sqlClient.raw(`DROP TRIGGER ${args.function_name}`);
const rows = await this.sqlClient.raw(
`CREATE TRIGGER \`${args.function_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`,
`CREATE TRIGGER \`${args.function_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`
);
result.data.list = rows;
} catch (e) {
@ -1129,7 +1128,7 @@ class SqliteClient extends KnexClient {
log.api(`${func}:args:`, args);
try {
const rows = await this.sqlClient.raw(
`CREATE TRIGGER \`${args.procedure_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`,
`CREATE TRIGGER \`${args.procedure_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`
);
result.data.list = rows;
} catch (e) {
@ -1157,7 +1156,7 @@ class SqliteClient extends KnexClient {
try {
await this.sqlClient.raw(`DROP TRIGGER ${args.procedure_name}`);
const rows = await this.sqlClient.raw(
`CREATE TRIGGER \`${args.procedure_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`,
`CREATE TRIGGER \`${args.procedure_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`
);
result.data.list = rows;
} catch (e) {
@ -1217,7 +1216,7 @@ class SqliteClient extends KnexClient {
try {
await this.sqlClient.raw(`DROP TRIGGER ${args.trigger_name}`);
await this.sqlClient.raw(
`CREATE TRIGGER \`${args.trigger_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`,
`CREATE TRIGGER \`${args.trigger_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`
);
const upQuery = `DROP TRIGGER ${args.trigger_name};\nCREATE TRIGGER \`${args.trigger_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`;
@ -1508,13 +1507,13 @@ class SqliteClient extends KnexClient {
args.table,
args.columns[i],
oldColumn,
upQuery,
upQuery
);
downQuery += this.alterTableAddColumn(
args.table,
oldColumn,
args.columns[i],
downQuery,
downQuery
);
} else if (args.columns[i].altered & 2 || args.columns[i].altered & 8) {
// col edit
@ -1522,7 +1521,7 @@ class SqliteClient extends KnexClient {
args.table,
args.columns[i],
oldColumn,
upQuery,
upQuery
);
downQuery += ';';
// downQuery += this.alterTableChangeColumn(
@ -1538,7 +1537,7 @@ class SqliteClient extends KnexClient {
args.table,
args.columns[i],
oldColumn,
upQuery,
upQuery
);
downQuery += ';';
// downQuery += alterTableRemoveColumn(
@ -1554,7 +1553,7 @@ class SqliteClient extends KnexClient {
const pkQuery = this.alterTablePK(
args.columns,
args.originalColumns,
upQuery,
upQuery
);
await this.sqlClient.raw('PRAGMA foreign_keys = OFF;');
@ -1573,7 +1572,7 @@ class SqliteClient extends KnexClient {
if (pkQuery) {
await trx.schema.alterTable(args.table, (table) => {
for (const pk of pkQuery.oldPks.filter(
(el) => !pkQuery.newPks.includes(el),
(el) => !pkQuery.newPks.includes(el)
)) {
table.dropPrimary(pk);
}
@ -1859,7 +1858,7 @@ class SqliteClient extends KnexClient {
/* Filter relations for current table */
if (args.tn) {
relations = relations.filter(
(r) => r.tn === args.tn || r.rtn === args.tn,
(r) => r.tn === args.tn || r.rtn === args.tn
);
}
@ -1868,7 +1867,7 @@ class SqliteClient extends KnexClient {
let columns: any = await this.columnList({ tn: tables[i].tn });
columns = columns.data.list;
console.log(
`Sequelize model created: ${tables[i].tn}(${columns.length})\n`,
`Sequelize model created: ${tables[i].tn}(${columns.length})\n`
);
// let SqliteSequelizeRender = require('./SqliteSequelizeRender');
@ -1968,7 +1967,7 @@ class SqliteClient extends KnexClient {
query += this.genQuery(
`ALTER TABLE ?? DROP COLUMN ??`,
[t, n.cn],
shouldSanitize,
shouldSanitize
);
return query;
}
@ -2001,8 +2000,6 @@ class SqliteClient extends KnexClient {
alterTableColumn(t, n, o, existingQuery, change = 2) {
let query = '';
// @ts-ignore
const defaultValue = getDefaultValue(n);
let shouldSanitize = true;
if (change === 2) {
const suffix = nanoid();
@ -2010,18 +2007,18 @@ class SqliteClient extends KnexClient {
const backupOldColumnQuery = this.genQuery(
`ALTER TABLE ?? RENAME COLUMN ?? TO ??;`,
[t, o.cn, `${o.cno}_nc_${suffix}`],
shouldSanitize,
shouldSanitize
);
let addNewColumnQuery = '';
addNewColumnQuery += this.genQuery(
` ADD ?? ${n.dt}`,
` ADD ?? ${this.sanitiseDataType(n.dt)}`,
[n.cn],
shouldSanitize,
shouldSanitize
);
addNewColumnQuery += n.dtxp && n.dt !== 'text' ? `(${n.dtxp})` : '';
addNewColumnQuery += n.cdf
? ` DEFAULT ${n.cdf}`
? ` DEFAULT ${this.sanitiseDefaultValue(n.cdf)}`
: !n.rqd
? ' '
: ` DEFAULT ''`;
@ -2029,33 +2026,45 @@ class SqliteClient extends KnexClient {
addNewColumnQuery = this.genQuery(
`ALTER TABLE ?? ${addNewColumnQuery};`,
[t],
shouldSanitize,
shouldSanitize
);
const updateNewColumnQuery = this.genQuery(
`UPDATE ?? SET ?? = ??;`,
[t, n.cn, `${o.cno}_nc_${suffix}`],
shouldSanitize,
shouldSanitize
);
const dropOldColumnQuery = this.genQuery(
`ALTER TABLE ?? DROP COLUMN ??;`,
[t, `${o.cno}_nc_${suffix}`],
shouldSanitize,
shouldSanitize
);
query = `${backupOldColumnQuery}${addNewColumnQuery}${updateNewColumnQuery}${dropOldColumnQuery}`;
} else if (change === 0) {
query = existingQuery ? ',' : '';
query += this.genQuery(`?? ${n.dt}`, [n.cn], shouldSanitize);
query += this.genQuery(
`?? ${this.sanitiseDataType(n.dt)}`,
[n.cn],
shouldSanitize,
);
query += n.dtxp && n.dt !== 'text' ? `(${n.dtxp})` : '';
query += n.cdf ? ` DEFAULT ${n.cdf}` : ' ';
query += n.cdf ? ` DEFAULT ${this.sanitiseDefaultValue(n.cdf)}` : ' ';
query += n.rqd ? ` NOT NULL` : ' ';
} else if (change === 1) {
shouldSanitize = true;
query += this.genQuery(` ADD ?? ${n.dt}`, [n.cn], shouldSanitize);
query += this.genQuery(
` ADD ?? ${this.sanitiseDataType(n.dt)}`,
[n.cn],
shouldSanitize,
);
query += n.dtxp && n.dt !== 'text' ? `(${n.dtxp})` : '';
query += n.cdf ? ` DEFAULT ${n.cdf}` : !n.rqd ? ' ' : ` DEFAULT ''`;
query += n.cdf
? ` DEFAULT ${this.sanitiseDefaultValue(n.cdf)}`
: !n.rqd
? ' '
: ` DEFAULT ''`;
query += n.rqd ? ` NOT NULL` : ' ';
query = this.genQuery(`ALTER TABLE ?? ${query};`, [t], shouldSanitize);
} else {
@ -2096,12 +2105,12 @@ class SqliteClient extends KnexClient {
try {
const tables = await this.sqlClient.raw(
`SELECT name FROM sqlite_master WHERE type='table';`,
`SELECT name FROM sqlite_master WHERE type='table';`
);
let count = 0;
for (const tb of tables) {
const tmp = await this.sqlClient.raw(
`SELECT COUNT(*) as ct FROM '${tb.name}';`,
`SELECT COUNT(*) as ct FROM '${tb.name}';`
);
if (tmp && tmp.length) {
count += tmp[0].ct;
@ -2119,54 +2128,4 @@ class SqliteClient extends KnexClient {
}
}
function getDefaultValue(n) {
if (n.cdf === undefined || n.cdf === null) return n.cdf;
switch (n.dt) {
case 'boolean':
case 'bool':
case 'tinyint':
case 'int':
case 'samllint':
case 'bigint':
case 'integer':
case 'smallint':
case 'mediumint':
case 'int2':
case 'int4':
case 'int8':
case 'long':
case 'serial':
case 'bigserial':
case 'smallserial':
case 'number':
case 'float':
case 'double':
case 'decimal':
case 'numeric':
case 'real':
case 'double precision':
case 'money':
case 'smallmoney':
case 'dec':
return n.cdf;
break;
case 'datetime':
case 'timestamp':
case 'date':
case 'time':
if (
n.cdf.indexOf('CURRENT_TIMESTAMP') > -1 ||
/\(([\d\w'", ]*)\)$/.test(n.cdf)
) {
return n.cdf;
}
return JSON.stringify(n.cdf);
break;
default:
return JSON.stringify(n.cdf);
break;
}
}
export default SqliteClient;

12
packages/nocodb/src/services/socket.service.spec.ts → packages/nocodb/src/gateways/socket.gateway.spec.ts

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

43
packages/nocodb/src/services/socket.service.ts → packages/nocodb/src/gateways/socket.gateway.ts

@ -1,11 +1,11 @@
import crypto from 'crypto';
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
import { Inject, Injectable } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import { T } from 'nc-help';
import { Server } from 'socket.io';
import { AuthGuard } from '@nestjs/passport';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
import Noco from '../Noco';
import { JwtStrategy } from '../strategies/jwt.strategy';
import type { OnModuleInit } from '@nestjs/common';
import type { Socket } from 'socket.io';
@ -14,30 +14,28 @@ function getHash(str) {
return crypto.createHash('md5').update(str).digest('hex');
}
@WebSocketGateway({
cors: {
origin: '*',
allowedHeaders: ['xc-auth'],
credentials: true,
},
})
@Injectable()
export class SocketService implements OnModuleInit {
export class SocketGateway implements OnModuleInit {
// private server: HttpServer;
private clients: { [id: string]: Socket } = {};
private _jobs: { [id: string]: { last_message: any } } = {};
private _io: Server;
constructor(
private jwtStrategy: JwtStrategy,
@Inject(HttpAdapterHost) private httpAdapterHost: HttpAdapterHost,
) {}
@WebSocketServer()
server: Server;
async onModuleInit() {
this._io = new Server(
Noco.httpServer ?? this.httpAdapterHost.httpAdapter.getHttpServer(),
{
cors: {
origin: '*',
allowedHeaders: ['xc-auth'],
credentials: true,
},
},
);
this.io
this.server
.use(async (socket, next) => {
try {
const context = new ExecutionContextHost([socket.handshake as any]);
@ -60,21 +58,10 @@ export class SocketService implements OnModuleInit {
socket.on('event', (args) => {
T.event({ ...args, id });
});
socket.on('subscribe', (room) => {
if (room in this.jobs) {
socket.join(room);
socket.emit('job');
socket.emit('progress', this.jobs[room].last_message);
}
});
});
}
public get io() {
return this._io;
}
public get jobs() {
return this._jobs;
return this.server;
}
}

82
packages/nocodb/src/helpers/exportImportHelpers.ts

@ -0,0 +1,82 @@
import type { Base } from '../models';
export async function generateBaseIdMap(
base: Base,
idMap: Map<string, string>,
) {
idMap.set(base.project_id, base.project_id);
idMap.set(base.id, `${base.project_id}::${base.id}`);
const models = await base.getModels();
for (const md of models) {
idMap.set(md.id, `${base.project_id}::${base.id}::${md.id}`);
await md.getColumns();
for (const column of md.columns) {
idMap.set(column.id, `${idMap.get(md.id)}::${column.id}`);
}
}
return models;
}
export function clearPrefix(text: string, prefix?: string) {
if (!prefix || prefix.length === 0) return text;
return text.replace(new RegExp(`^${prefix}_?`), '');
}
export function withoutNull(obj: any) {
const newObj = {};
let found = false;
for (const [key, value] of Object.entries(obj)) {
if (value !== null) {
newObj[key] = value;
found = true;
}
}
if (!found) return null;
return newObj;
}
export function reverseGet(map: Map<string, string>, vl: string) {
for (const [key, value] of map.entries()) {
if (vl === value) {
return key;
}
}
return undefined;
}
export function withoutId(obj: any) {
const { id, ...rest } = obj;
return rest;
}
export function getParentIdentifier(id: string) {
const arr = id.split('::');
arr.pop();
return arr.join('::');
}
export function getEntityIdentifier(id: string) {
const arr = id.split('::');
return arr.pop();
}
export function findWithIdentifier(map: Map<string, any>, id: string) {
for (const key of map.keys()) {
if (getEntityIdentifier(key) === id) {
return map.get(key);
}
}
return undefined;
}
export function generateUniqueName(name: string, names: string[]) {
let newName = name;
let i = 1;
while (names.includes(newName)) {
newName = `${name}_${i}`;
i++;
}
return newName;
}

6
packages/nocodb/src/helpers/populateMeta.ts

@ -83,7 +83,11 @@ export async function extractAndGenerateManyToManyRelations(
}
// todo: impl better method to identify m2m relation
if (belongsToCols?.length === 2 && normalColumns.length < 5) {
if (
belongsToCols?.length === 2 &&
normalColumns.length < 5 &&
assocModel.primaryKeys.length === 2
) {
const modelA = await belongsToCols[0].colOptions.getRelatedTable();
const modelB = await belongsToCols[1].colOptions.getRelatedTable();

22
packages/nocodb/src/interface/Jobs.ts

@ -0,0 +1,22 @@
export const JOBS_QUEUE = 'jobs';
export enum JobTypes {
DuplicateBase = 'duplicate-base',
DuplicateModel = 'duplicate-model',
AtImport = 'at-import',
}
export enum JobStatus {
COMPLETED = 'completed',
WAITING = 'waiting',
ACTIVE = 'active',
DELAYED = 'delayed',
FAILED = 'failed',
PAUSED = 'paused',
REFRESH = 'refresh',
}
export enum JobEvents {
STATUS = 'job.status',
LOG = 'job.log',
}

35
packages/nocodb/src/jobs/EmitteryJobsMgr.ts

@ -1,35 +0,0 @@
import Emittery from 'emittery';
import JobsMgr from './JobsMgr';
export default class EmitteryJobsMgr extends JobsMgr {
emitter: Emittery;
constructor() {
super();
this.emitter = new Emittery();
}
add(jobName: string, payload: any): Promise<any> {
return this.emitter.emit(jobName, payload);
}
addJobWorker(
jobName: string,
workerFn: (
payload: any,
progressCbk?: (payload: any, msg?: string) => void,
) => void,
) {
this.emitter.on(jobName, async (payload) => {
try {
await workerFn(payload, (msg) =>
this.invokeProgressCbks(jobName, payload, msg),
);
await this.invokeSuccessCbks(jobName, payload);
} catch (e) {
console.log(e);
await this.invokeFailureCbks(jobName, payload, e);
}
});
}
}

67
packages/nocodb/src/jobs/JobsMgr.ts

@ -1,67 +0,0 @@
export default abstract class JobsMgr {
protected successCbks: Array<{
[jobName: string]: (payload: any) => void;
}> = [];
protected failureCbks: Array<{
[jobName: string]: (payload: any, error: Error) => void;
}> = [];
protected progressCbks: Array<{
[jobName: string]: (payload: any, msg?: string) => void;
}> = [];
public abstract add<T>(jobName: string, payload: T): Promise<any>;
public abstract addJobWorker(
jobName: string,
workerFn: (
payload: any,
progressCbk?: (payload: any, msg?: string) => void,
) => void,
options?: {
onSuccess?: (payload: any) => void;
onFailure?: (payload: any, errorData: any) => void;
onProgress?: (payload: any, progressData: any) => void;
},
);
addSuccessCbk(jobName: string, cbk: (payload: any) => void) {
this.successCbks[jobName] = this.successCbks[jobName] || [];
this.successCbks[jobName].push(cbk);
}
addFailureCbk(jobName: string, cbk: (payload: any, errorData: any) => void) {
this.failureCbks[jobName] = this.failureCbks[jobName] || [];
this.failureCbks[jobName].push(cbk);
}
addProgressCbk(
jobName: string,
cbk: (payload: any, progressData: any) => void,
) {
this.progressCbks[jobName] = this.progressCbks[jobName] || [];
this.progressCbks[jobName].push(cbk);
}
protected async invokeSuccessCbks(jobName: string, payload: any) {
await Promise.all(
this.successCbks?.[jobName]?.map((cb) => cb(payload)) || [],
);
}
protected async invokeFailureCbks(
jobName: string,
payload: any,
error?: Error,
) {
await Promise.all(
this.failureCbks?.[jobName]?.map((cb) => cb(payload, error)) || [],
);
}
protected async invokeProgressCbks(
jobName: string,
payload: any,
data?: any,
) {
await Promise.all(
this.progressCbks?.[jobName]?.map((cb) => cb(payload, data)) || [],
);
}
}

20
packages/nocodb/src/jobs/NocoJobs.ts

@ -1,20 +0,0 @@
import EmitteryJobsMgr from './EmitteryJobsMgr';
import RedisJobsMgr from './RedisJobsMgr';
import type JobsMgr from './JobsMgr';
export default class NocoJobs {
private static client: JobsMgr;
private static init() {
if (process.env.NC_REDIS_URL) {
this.client = new RedisJobsMgr(process.env.NC_REDIS_URL);
} else {
this.client = new EmitteryJobsMgr();
}
}
public static get jobsMgr(): JobsMgr {
if (!this.client) this.init();
return this.client;
}
}

56
packages/nocodb/src/jobs/RedisJobsMgr.ts

@ -1,56 +0,0 @@
import { Queue, Worker } from 'bullmq';
import Redis from 'ioredis';
import JobsMgr from './JobsMgr';
export default class RedisJobsMgr extends JobsMgr {
queue: { [jobName: string]: Queue };
workers: { [jobName: string]: Worker };
connection: Redis;
constructor(config: any) {
super();
this.queue = {};
this.workers = {};
this.connection = new Redis(config, {
maxRetriesPerRequest: null,
});
}
async add(
jobName: string,
payload: any,
// options?: {
// onSuccess?: (payload: any) => void;
// onFailure?: (payload: any, msg: string) => void;
// onProgress?: (payload: any, msgOrData: any) => void;
// }
): Promise<any> {
this.queue[jobName] =
this.queue[jobName] ||
new Queue(jobName, { connection: this.connection });
this.queue[jobName].add(jobName, payload);
}
addJobWorker(
jobName: string,
workerFn: (
payload: any,
progressCbk?: (payload: any, msg?: string) => void,
) => void,
) {
this.workers[jobName] = new Worker(
jobName,
async (payload) => {
try {
await workerFn(payload.data, (...args) =>
this.invokeProgressCbks(jobName, ...args),
);
await this.invokeFailureCbks(jobName, payload.data);
} catch (e) {
await this.invokeFailureCbks(jobName, payload.data);
}
},
{ connection: this.connection },
);
}
}

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

@ -78,10 +78,18 @@ export default class Model implements TableType {
return this.columns?.filter((c) => c.pk);
}
// If there is no column marked as display value,
// we are getting the immediate next column to pk as display value
// or the first column(if pk is the last column).
public get displayValue(): Column {
if (!this.columns) return null;
const pCol = this.columns?.find((c) => c.pv);
if (pCol) return pCol;
if (this.mm) {
// by default, there is no default value in m2m table
// take the first column instead
return this.columns[0];
}
const pkIndex = this.columns.indexOf(this.primaryKey);
if (pkIndex < this.columns.length - 1) return this.columns[pkIndex + 1];
return this.columns[0];

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

@ -43,6 +43,7 @@ export default class Project implements ProjectType {
'prefix',
'description',
'is_meta',
'status',
]);
const { id: projectId } = await ncMeta.metaInsert2(

6
packages/nocodb/src/modules/event-emitter/event-emitter.interface.ts

@ -0,0 +1,6 @@
export interface IEventEmitter {
emit(event: string, arg: any): void;
on(event: string, listener: (arg: any) => void): () => void;
removeListener(event: string, listener: (arg: any) => void): void;
removeAllListeners(event?: string): void;
}

16
packages/nocodb/src/modules/event-emitter/event-emitter.module.ts

@ -0,0 +1,16 @@
import { Global, Module } from '@nestjs/common';
import { FallbackEventEmitter } from './fallback-event-emitter';
@Global()
@Module({
providers: [
{
provide: 'IEventEmitter',
useFactory: () => {
return new FallbackEventEmitter();
},
},
],
exports: ['IEventEmitter'],
})
export class EventEmitterModule {}

27
packages/nocodb/src/modules/event-emitter/fallback-event-emitter.ts

@ -0,0 +1,27 @@
import Emittery from 'emittery';
import { IEventEmitter } from './event-emitter.interface';
export class FallbackEventEmitter implements IEventEmitter {
private readonly emitter: Emittery;
constructor() {
this.emitter = new Emittery();
}
emit(event: string, data: any): void {
this.emitter.emit(event, data);
}
on(event: string, listener: (...args: any[]) => void) {
this.emitter.on(event, listener);
return () => this.emitter.off(event, listener);
}
removeListener(event: string, listener: (...args: any[]) => void): void {
this.emitter.off(event, listener);
}
removeAllListeners(event?: string): void {
this.emitter.clearListeners(event);
}
}

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

@ -0,0 +1,23 @@
import { EventEmitter2 } from '@nestjs/event-emitter';
import { IEventEmitter } from './event-emitter.interface';
export class NestjsEventEmitter implements IEventEmitter {
constructor(private readonly eventEmitter: EventEmitter2) {}
emit(event: string, data: any): void {
this.eventEmitter.emit(event, data);
}
on(event: string, listener: (...args: any[]) => void) {
this.eventEmitter.on(event, listener);
return () => this.eventEmitter.removeListener(event, listener);
}
removeListener(event: string, listener: (...args: any[]) => void): void {
this.eventEmitter.removeListener(event, listener);
}
removeAllListeners(event?: string): void {
this.eventEmitter.removeAllListeners(event);
}
}

6
packages/nocodb/src/modules/global/global.module.ts

@ -1,10 +1,10 @@
import { Global, Module } from '@nestjs/common';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { ExtractJwt } from 'passport-jwt';
import { SocketGateway } from '../../gateways/socket.gateway';
import { Connection } from '../../connection/connection';
import { GlobalGuard } from '../../guards/global/global.guard';
import { MetaService } from '../../meta/meta.service';
import { SocketService } from '../../services/socket.service';
import { JwtStrategy } from '../../strategies/jwt.strategy';
import NcConfigFactory from '../../utils/NcConfigFactory';
import { UsersService } from '../../services/users/users.service';
@ -38,7 +38,7 @@ export const JwtStrategyProvider: Provider = {
UsersService,
JwtStrategyProvider,
GlobalGuard,
SocketService,
SocketGateway,
],
exports: [
Connection,
@ -46,7 +46,7 @@ export const JwtStrategyProvider: Provider = {
JwtStrategyProvider,
UsersService,
GlobalGuard,
SocketService,
SocketGateway,
],
})
export class GlobalModule {}

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

@ -0,0 +1,65 @@
import { Controller, HttpCode, Post, Request, UseGuards } from '@nestjs/common';
import { GlobalGuard } from '../../../guards/global/global.guard';
import { ExtractProjectIdMiddleware } from '../../../middlewares/extract-project-id/extract-project-id.middleware';
import { SyncSource } from '../../../models';
import { NcError } from '../../../helpers/catchError';
import { JobsService } from '../jobs.service';
import { JobTypes } from '../../../interface/Jobs';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class AtImportController {
constructor(private readonly jobsService: JobsService) {}
@Post('/api/v1/db/meta/import/airtable')
@HttpCode(200)
async importAirtable(@Request() req) {
const job = await this.jobsService.activeQueue.add(JobTypes.AtImport, {
...req.body,
});
return { id: job.id, name: job.name };
}
@Post('/api/v1/db/meta/syncs/:syncId/trigger')
@HttpCode(200)
async triggerSync(@Request() req) {
const jobs = await this.jobsService.jobList(JobTypes.AtImport);
const fnd = jobs.find((j) => j.data.syncId === req.params.syncId);
if (fnd) {
NcError.badRequest('Sync already in progress');
}
const syncSource = await SyncSource.get(req.params.syncId);
const user = await syncSource.getUser();
// Treat default baseUrl as siteUrl from req object
let baseURL = (req as any).ncSiteUrl;
// if environment value avail use it
// or if it's docker construct using `PORT`
if (process.env.NC_DOCKER) {
baseURL = `http://localhost:${process.env.PORT || 8080}`;
}
const job = await this.jobsService.activeQueue.add(JobTypes.AtImport, {
syncId: req.params.syncId,
...(syncSource?.details || {}),
projectId: syncSource.project_id,
baseId: syncSource.base_id,
authToken: '',
baseURL,
user: user,
});
return { id: job.id, name: job.name };
}
@Post('/api/v1/db/meta/syncs/:syncId/abort')
@HttpCode(200)
async abortImport(@Request() req) {
return {};
}
}

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

File diff suppressed because it is too large Load Diff

0
packages/nocodb/src/controllers/imports/helpers/EntityMap.ts → packages/nocodb/src/modules/jobs/at-import/helpers/EntityMap.ts

0
packages/nocodb/src/controllers/imports/helpers/fetchAT.ts → packages/nocodb/src/modules/jobs/at-import/helpers/fetchAT.ts

5
packages/nocodb/src/controllers/imports/helpers/readAndProcessData.ts → packages/nocodb/src/modules/jobs/at-import/helpers/readAndProcessData.ts

@ -1,7 +1,8 @@
/* eslint-disable no-async-promise-executor */
import { RelationTypes, UITypes } from 'nocodb-sdk';
import EntityMap from './EntityMap';
import type { BulkDataAliasService } from '../../../services/bulk-data-alias.service';
import type { TablesService } from '../../../services/tables.service';
import type { BulkDataAliasService } from '../../../../services/bulk-data-alias.service';
import type { TablesService } from '../../../../services/tables.service';
// @ts-ignore
import type { AirtableBase } from 'airtable/lib/airtable_base';
import type { TableType } from 'nocodb-sdk';

0
packages/nocodb/src/controllers/imports/helpers/syncMap.ts → packages/nocodb/src/modules/jobs/at-import/helpers/syncMap.ts

136
packages/nocodb/src/modules/jobs/export-import/duplicate.controller.ts

@ -0,0 +1,136 @@
import {
Body,
Controller,
HttpCode,
Param,
Post,
Request,
UseGuards,
} from '@nestjs/common';
import { ProjectStatus } from 'nocodb-sdk';
import { GlobalGuard } from '../../../guards/global/global.guard';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../../../middlewares/extract-project-id/extract-project-id.middleware';
import { ProjectsService } from '../../../services/projects.service';
import { Base, Model, Project } from '../../../models';
import { generateUniqueName } from '../../../helpers/exportImportHelpers';
import { JobsService } from '../jobs.service';
import { JobTypes } from '../../../interface/Jobs';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class DuplicateController {
constructor(
private readonly jobsService: JobsService,
private readonly projectsService: ProjectsService,
) {}
@Post('/api/v1/db/meta/duplicate/:projectId/:baseId?')
@HttpCode(200)
@Acl('duplicateBase')
async duplicateBase(
@Request() req,
@Param('projectId') projectId: string,
@Param('baseId') baseId?: string,
@Body()
options?: {
excludeData?: boolean;
excludeViews?: boolean;
excludeHooks?: boolean;
},
) {
const project = await Project.get(projectId);
if (!project) {
throw new Error(`Project not found for id '${projectId}'`);
}
const base = baseId
? await Base.get(baseId)
: (await project.getBases())[0];
if (!base) {
throw new Error(`Base not found!`);
}
const projects = await Project.list({});
const uniqueTitle = generateUniqueName(
`${project.title} copy`,
projects.map((p) => p.title),
);
const dupProject = await this.projectsService.projectCreate({
project: { title: uniqueTitle, status: ProjectStatus.JOB },
user: { id: req.user.id },
});
const job = await this.jobsService.activeQueue.add(JobTypes.DuplicateBase, {
projectId: project.id,
baseId: base.id,
dupProjectId: dupProject.id,
options,
req: {
user: req.user,
clientIp: req.clientIp,
},
});
return { id: job.id, name: job.name };
}
@Post('/api/v1/db/meta/duplicate/:projectId/table/:modelId')
@HttpCode(200)
@Acl('duplicateModel')
async duplicateModel(
@Request() req,
@Param('projectId') projectId: string,
@Param('modelId') modelId?: string,
@Body()
options?: {
excludeData?: boolean;
excludeViews?: boolean;
excludeHooks?: boolean;
},
) {
const project = await Project.get(projectId);
if (!project) {
throw new Error(`Project not found for id '${projectId}'`);
}
const model = await Model.get(modelId);
if (!model) {
throw new Error(`Model not found!`);
}
const base = await Base.get(model.base_id);
const models = await base.getModels();
const uniqueTitle = generateUniqueName(
`${model.title} copy`,
models.map((p) => p.title),
);
const job = await this.jobsService.activeQueue.add(
JobTypes.DuplicateModel,
{
projectId: project.id,
baseId: base.id,
modelId: model.id,
title: uniqueTitle,
options,
req: {
user: req.user,
clientIp: req.clientIp,
},
},
);
return { id: job.id, name: job.name };
}
}

408
packages/nocodb/src/modules/jobs/export-import/duplicate.processor.ts

@ -0,0 +1,408 @@
import { Readable } from 'stream';
import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';
import papaparse from 'papaparse';
import { UITypes } from 'nocodb-sdk';
import { Logger } from '@nestjs/common';
import { Base, Column, Model, Project } from '../../../models';
import { ProjectsService } from '../../../services/projects.service';
import { findWithIdentifier } from '../../../helpers/exportImportHelpers';
import { BulkDataAliasService } from '../../../services/bulk-data-alias.service';
import { JOBS_QUEUE, JobTypes } from '../../../interface/Jobs';
import { elapsedTime, initTime } from '../helpers';
import { ExportService } from './export.service';
import { ImportService } from './import.service';
@Processor(JOBS_QUEUE)
export class DuplicateProcessor {
private readonly logger = new Logger(
`${JOBS_QUEUE}:${DuplicateProcessor.name}`,
);
constructor(
private readonly exportService: ExportService,
private readonly importService: ImportService,
private readonly projectsService: ProjectsService,
private readonly bulkDataService: BulkDataAliasService,
) {}
@Process(JobTypes.DuplicateBase)
async duplicateBase(job: Job) {
const hrTime = initTime();
const { projectId, baseId, dupProjectId, req, options } = job.data;
const excludeData = options?.excludeData || false;
const excludeHooks = options?.excludeHooks || false;
const excludeViews = options?.excludeViews || false;
const project = await Project.get(projectId);
const dupProject = await Project.get(dupProjectId);
const base = await Base.get(baseId);
try {
if (!project || !dupProject || !base) {
throw new Error(`Project or base not found!`);
}
const user = (req as any).user;
const models = (await base.getModels()).filter(
// TODO revert this when issue with cache is fixed
(m) => m.base_id === base.id && !m.mm && m.type === 'table',
);
const exportedModels = await this.exportService.serializeModels({
modelIds: models.map((m) => m.id),
excludeViews,
excludeHooks,
});
elapsedTime(
hrTime,
`serialize models schema for ${base.project_id}::${base.id}`,
'duplicateBase',
);
if (!exportedModels) {
throw new Error(`Export failed for base '${base.id}'`);
}
await dupProject.getBases();
const dupBase = dupProject.bases[0];
const idMap = await this.importService.importModels({
user,
projectId: dupProject.id,
baseId: dupBase.id,
data: exportedModels,
req: req,
});
elapsedTime(hrTime, `import models schema`, 'duplicateBase');
if (!idMap) {
throw new Error(`Import failed for base '${base.id}'`);
}
if (!excludeData) {
await this.importModelsData({
idMap,
sourceProject: project,
sourceModels: models,
destProject: dupProject,
destBase: dupBase,
hrTime,
});
}
await this.projectsService.projectUpdate({
projectId: dupProject.id,
project: {
status: null,
},
});
} catch (e) {
if (dupProject?.id) {
await this.projectsService.projectSoftDelete({
projectId: dupProject.id,
});
}
throw e;
}
}
@Process(JobTypes.DuplicateModel)
async duplicateModel(job: Job) {
const hrTime = initTime();
const { projectId, baseId, modelId, title, req, options } = job.data;
const excludeData = options?.excludeData || false;
const excludeHooks = options?.excludeHooks || false;
const excludeViews = options?.excludeViews || false;
const project = await Project.get(projectId);
const base = await Base.get(baseId);
const user = (req as any).user;
const models = (await base.getModels()).filter(
(m) => !m.mm && m.type === 'table',
);
const sourceModel = models.find((m) => m.id === modelId);
await sourceModel.getColumns();
const relatedModelIds = sourceModel.columns
.filter((col) => col.uidt === UITypes.LinkToAnotherRecord)
.map((col) => col.colOptions.fk_related_model_id)
.filter((id) => id);
const relatedModels = models.filter((m) => relatedModelIds.includes(m.id));
const exportedModel = (
await this.exportService.serializeModels({
modelIds: [modelId],
excludeViews,
excludeHooks,
})
)[0];
elapsedTime(
hrTime,
`serialize model schema for ${modelId}`,
'duplicateModel',
);
if (!exportedModel) {
throw new Error(`Export failed for base '${base.id}'`);
}
exportedModel.model.title = title;
exportedModel.model.table_name = title.toLowerCase().replace(/ /g, '_');
const idMap = await this.importService.importModels({
projectId,
baseId,
data: [exportedModel],
user,
req,
externalModels: relatedModels,
});
elapsedTime(hrTime, 'import model schema', 'duplicateModel');
if (!idMap) {
throw new Error(`Import failed for model '${modelId}'`);
}
if (!excludeData) {
const fields: Record<string, string[]> = {};
for (const md of relatedModels) {
const bts = md.columns
.filter(
(c) =>
c.uidt === UITypes.LinkToAnotherRecord &&
c.colOptions.type === 'bt' &&
c.colOptions.fk_related_model_id === modelId,
)
.map((c) => c.id);
if (bts.length > 0) {
fields[md.id] = [md.primaryKey.id];
fields[md.id].push(...bts);
}
}
await this.importModelsData({
idMap,
sourceProject: project,
sourceModels: [sourceModel],
destProject: project,
destBase: base,
hrTime,
modelFieldIds: fields,
externalModels: relatedModels,
});
elapsedTime(hrTime, 'import model data', 'duplicateModel');
}
return await Model.get(findWithIdentifier(idMap, sourceModel.id));
}
async importModelsData(param: {
idMap: Map<string, string>;
sourceProject: Project;
sourceModels: Model[];
destProject: Project;
destBase: Base;
hrTime: { hrTime: [number, number] };
modelFieldIds?: Record<string, string[]>;
externalModels?: Model[];
}) {
const {
idMap,
sourceProject,
sourceModels,
destProject,
destBase,
hrTime,
modelFieldIds,
externalModels,
} = param;
let handledLinks = [];
for (const sourceModel of sourceModels) {
const dataStream = new Readable({
read() {},
});
const linkStream = new Readable({
read() {},
});
this.exportService.streamModelDataAsCsv({
dataStream,
linkStream,
projectId: sourceProject.id,
modelId: sourceModel.id,
handledMmList: handledLinks,
});
const model = await Model.get(findWithIdentifier(idMap, sourceModel.id));
await this.importService.importDataFromCsvStream({
idMap,
dataStream,
destProject,
destBase,
destModel: model,
});
handledLinks = await this.importService.importLinkFromCsvStream({
idMap,
linkStream,
destProject,
destBase,
handledLinks,
});
elapsedTime(
hrTime,
`import data and links for ${model.title}`,
'importModelsData',
);
}
// update external models (has bt to this model)
if (externalModels) {
for (const sourceModel of externalModels) {
const fields = modelFieldIds?.[sourceModel.id];
if (!fields) continue;
const dataStream = new Readable({
read() {},
});
const linkStream = new Readable({
read() {},
});
this.exportService.streamModelDataAsCsv({
dataStream,
linkStream,
projectId: sourceProject.id,
modelId: sourceModel.id,
handledMmList: handledLinks,
_fieldIds: fields,
});
const headers: string[] = [];
let chunk = [];
const model = await Model.get(sourceModel.id);
await new Promise((resolve) => {
papaparse.parse(dataStream, {
newline: '\r\n',
step: async (results, parser) => {
if (!headers.length) {
parser.pause();
for (const header of results.data) {
const id = idMap.get(header);
if (id) {
const col = await Column.get({
base_id: destBase.id,
colId: id,
});
if (col) {
if (col.colOptions?.type === 'bt') {
const childCol = await Column.get({
base_id: destBase.id,
colId: col.colOptions.fk_child_column_id,
});
if (childCol) {
headers.push(childCol.column_name);
} else {
headers.push(null);
this.logger.error(`child column not found (${id})`);
}
} else {
headers.push(col.column_name);
}
} else {
headers.push(null);
this.logger.error(`column not found (${id})`);
}
} else {
headers.push(null);
this.logger.error(`id not found (${header})`);
}
}
parser.resume();
} else {
if (results.errors.length === 0) {
const row = {};
for (let i = 0; i < headers.length; i++) {
if (headers[i]) {
if (results.data[i] !== '') {
row[headers[i]] = results.data[i];
}
}
}
chunk.push(row);
if (chunk.length > 1000) {
parser.pause();
try {
await this.bulkDataService.bulkDataUpdate({
projectName: destProject.id,
tableName: model.id,
body: chunk,
cookie: null,
raw: true,
});
} catch (e) {
this.logger.error(e);
}
chunk = [];
parser.resume();
}
}
}
},
complete: async () => {
if (chunk.length > 0) {
try {
await this.bulkDataService.bulkDataUpdate({
projectName: destProject.id,
tableName: model.id,
body: chunk,
cookie: null,
raw: true,
});
} catch (e) {
this.logger.error(e);
}
chunk = [];
}
resolve(null);
},
});
});
elapsedTime(
hrTime,
`map existing links to ${model.title}`,
'importModelsData',
);
}
}
}
}

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

@ -0,0 +1,721 @@
import { Readable } from 'stream';
import { UITypes, ViewTypes } from 'nocodb-sdk';
import { unparse } from 'papaparse';
import { Injectable, Logger } from '@nestjs/common';
import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2';
import { getViewAndModelByAliasOrId } from '../../../modules/datas/helpers';
import {
clearPrefix,
generateBaseIdMap,
} from '../../../helpers/exportImportHelpers';
import NcPluginMgrv2 from '../../../helpers/NcPluginMgrv2';
import { NcError } from '../../../helpers/catchError';
import { Base, Hook, Model, Project } from '../../../models';
import { DatasService } from '../../../services/datas.service';
import { elapsedTime, initTime } from '../helpers';
import type { BaseModelSqlv2 } from '../../../db/BaseModelSqlv2';
import type { View } from '../../../models';
@Injectable()
export class ExportService {
private readonly logger = new Logger(ExportService.name);
constructor(private datasService: DatasService) {}
async serializeModels(param: {
modelIds: string[];
excludeViews?: boolean;
excludeHooks?: boolean;
}) {
const { modelIds } = param;
const excludeViews = param?.excludeViews || false;
const excludeHooks = param?.excludeHooks || false;
const serializedModels = [];
// db id to structured id
const idMap = new Map<string, string>();
const projects: Project[] = [];
const bases: Base[] = [];
const modelsMap = new Map<string, Model[]>();
for (const modelId of modelIds) {
const model = await Model.get(modelId);
if (!model)
return NcError.badRequest(`Model not found for id '${modelId}'`);
const fndProject = projects.find((p) => p.id === model.project_id);
const project = fndProject || (await Project.get(model.project_id));
const fndBase = bases.find((b) => b.id === model.base_id);
const base = fndBase || (await Base.get(model.base_id));
if (!fndProject) projects.push(project);
if (!fndBase) bases.push(base);
if (!modelsMap.has(base.id)) {
modelsMap.set(base.id, await generateBaseIdMap(base, idMap));
}
await model.getColumns();
await model.getViews();
// if views are excluded, filter all views except default
if (excludeViews) {
model.views = model.views.filter((v) => v.is_default);
}
for (const column of model.columns) {
await column.getColOptions();
if (column.colOptions) {
for (const [k, v] of Object.entries(column.colOptions)) {
switch (k) {
case 'fk_mm_child_column_id':
case 'fk_mm_parent_column_id':
case 'fk_mm_model_id':
case 'fk_parent_column_id':
case 'fk_child_column_id':
case 'fk_related_model_id':
case 'fk_relation_column_id':
case 'fk_lookup_column_id':
case 'fk_rollup_column_id':
column.colOptions[k] = idMap.get(v as string);
break;
case 'options':
for (const o of column.colOptions['options']) {
delete o.id;
delete o.fk_column_id;
}
break;
case 'formula':
column.colOptions[k] = column.colOptions[k].replace(
/(?<=\{\{).*?(?=\}\})/gm,
(match) => idMap.get(match),
);
break;
case 'id':
case 'created_at':
case 'updated_at':
case 'fk_column_id':
delete column.colOptions[k];
break;
}
}
}
}
for (const view of model.views) {
idMap.set(view.id, `${idMap.get(model.id)}::${view.id}`);
await view.getColumns();
await view.getFilters();
await view.getSorts();
if (view.filter) {
const export_filters = [];
for (const fl of view.filter.children) {
const tempFl = {
id: `${idMap.get(view.id)}::${fl.id}`,
fk_column_id: idMap.get(fl.fk_column_id),
fk_parent_id: fl.fk_parent_id,
is_group: fl.is_group,
logical_op: fl.logical_op,
comparison_op: fl.comparison_op,
comparison_sub_op: fl.comparison_sub_op,
value: fl.value,
};
if (tempFl.is_group) {
delete tempFl.comparison_op;
delete tempFl.comparison_sub_op;
delete tempFl.value;
}
export_filters.push(tempFl);
}
view.filter.children = export_filters;
}
if (view.sorts) {
const export_sorts = [];
for (const sr of view.sorts) {
const tempSr = {
fk_column_id: idMap.get(sr.fk_column_id),
direction: sr.direction,
};
export_sorts.push(tempSr);
}
view.sorts = export_sorts;
}
if (view.view) {
for (const [k, v] of Object.entries(view.view)) {
switch (k) {
case 'fk_column_id':
case 'fk_cover_image_col_id':
case 'fk_grp_col_id':
view.view[k] = idMap.get(v as string);
break;
case 'meta':
if (view.type === ViewTypes.KANBAN) {
const meta = JSON.parse(view.view.meta as string) as Record<
string,
any
>;
for (const [k, v] of Object.entries(meta)) {
const colId = idMap.get(k as string);
for (const op of v) {
op.fk_column_id = idMap.get(op.fk_column_id);
delete op.id;
}
meta[colId] = v;
delete meta[k];
}
view.view.meta = meta;
}
break;
case 'created_at':
case 'updated_at':
case 'fk_view_id':
case 'project_id':
case 'base_id':
case 'uuid':
delete view.view[k];
break;
}
}
}
}
const serializedHooks = [];
if (!excludeHooks) {
const hooks = await Hook.list({ fk_model_id: model.id });
for (const hook of hooks) {
idMap.set(hook.id, `${idMap.get(hook.fk_model_id)}::${hook.id}`);
const hookFilters = await hook.getFilters();
const export_filters = [];
if (hookFilters) {
for (const fl of hookFilters) {
const tempFl = {
id: `${idMap.get(hook.id)}::${fl.id}`,
fk_column_id: idMap.get(fl.fk_column_id),
fk_parent_id: fl.fk_parent_id,
is_group: fl.is_group,
logical_op: fl.logical_op,
comparison_op: fl.comparison_op,
comparison_sub_op: fl.comparison_sub_op,
value: fl.value,
};
if (tempFl.is_group) {
delete tempFl.comparison_op;
delete tempFl.comparison_sub_op;
delete tempFl.value;
}
export_filters.push(tempFl);
}
}
serializedHooks.push({
id: idMap.get(hook.id),
title: hook.title,
active: hook.active,
condition: hook.condition,
event: hook.event,
operation: hook.operation,
notification: hook.notification,
version: hook.version,
filters: export_filters,
});
}
}
serializedModels.push({
model: {
id: idMap.get(model.id),
prefix: project.prefix,
title: model.title,
table_name: clearPrefix(model.table_name, project.prefix),
meta: model.meta,
columns: model.columns.map((column) => ({
id: idMap.get(column.id),
ai: column.ai,
column_name: column.column_name,
cc: column.cc,
cdf: column.cdf,
meta: column.meta,
pk: column.pk,
pv: column.pv,
order: column.order,
rqd: column.rqd,
system: column.system,
uidt: column.uidt,
title: column.title,
un: column.un,
unique: column.unique,
colOptions: column.colOptions,
})),
},
views: model.views.map((view) => ({
id: idMap.get(view.id),
is_default: view.is_default,
type: view.type,
meta: view.meta,
order: view.order,
title: view.title,
show: view.show,
show_system_fields: view.show_system_fields,
filter: view.filter,
sorts: view.sorts,
lock_type: view.lock_type,
columns: view.columns.map((column) => {
const {
id,
fk_view_id,
fk_column_id,
project_id,
base_id,
created_at,
updated_at,
uuid,
...rest
} = column as any;
return {
fk_column_id: idMap.get(fk_column_id),
...rest,
};
}),
view: view.view,
})),
hooks: serializedHooks,
});
}
return serializedModels;
}
async streamModelDataAsCsv(param: {
dataStream: Readable;
linkStream: Readable;
projectId: string;
modelId: string;
viewId?: string;
handledMmList?: string[];
_fieldIds?: string[];
}) {
const { dataStream, linkStream, handledMmList } = param;
const { model, view } = await getViewAndModelByAliasOrId({
projectName: param.projectId,
tableName: param.modelId,
viewName: param.viewId,
});
const base = await Base.get(model.base_id);
await model.getColumns();
const btMap = new Map<string, string>();
for (const column of model.columns.filter(
(col) =>
col.uidt === UITypes.LinkToAnotherRecord &&
col.colOptions?.type === 'bt',
)) {
await column.getColOptions();
const fkCol = model.columns.find(
(c) => c.id === column.colOptions?.fk_child_column_id,
);
if (fkCol) {
// replace bt column with fk column if it is in _fieldIds
if (param._fieldIds && param._fieldIds.includes(column.id)) {
param._fieldIds.push(fkCol.id);
const btIndex = param._fieldIds.indexOf(column.id);
param._fieldIds.splice(btIndex, 1);
}
btMap.set(
fkCol.id,
`${column.project_id}::${column.base_id}::${column.fk_model_id}::${column.id}`,
);
}
}
const fields = param._fieldIds
? model.columns
.filter((c) => param._fieldIds?.includes(c.id))
.map((c) => c.title)
.join(',')
: model.columns
.filter((c) => c.uidt !== UITypes.LinkToAnotherRecord)
.map((c) => c.title)
.join(',');
const mmColumns = model.columns.filter(
(col) =>
col.uidt === UITypes.LinkToAnotherRecord &&
col.colOptions?.type === 'mm',
);
const hasLink = mmColumns.length > 0;
dataStream.setEncoding('utf8');
const formatData = (data: any) => {
for (const row of data) {
for (const [k, v] of Object.entries(row)) {
const col = model.columns.find((c) => c.title === k);
if (col) {
const colId = `${col.project_id}::${col.base_id}::${col.fk_model_id}::${col.id}`;
switch (col.uidt) {
case UITypes.ForeignKey:
{
if (btMap.has(col.id)) {
row[btMap.get(col.id)] = v;
delete row[k];
}
}
break;
case UITypes.Attachment:
try {
row[colId] = JSON.stringify(v);
} catch (e) {
row[colId] = v;
}
break;
case UITypes.Formula:
case UITypes.Lookup:
case UITypes.Rollup:
case UITypes.Barcode:
case UITypes.QrCode:
// skip these types
break;
default:
row[colId] = v;
break;
}
delete row[k];
}
}
}
return { data };
};
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view?.id,
dbDriver: await NcConnectionMgrv2.get(base),
});
const limit = 200;
const offset = 0;
try {
await this.recursiveRead(
formatData,
baseModel,
dataStream,
model,
view,
offset,
limit,
fields,
true,
);
} catch (e) {
this.logger.error(e);
throw e;
}
if (hasLink) {
linkStream.setEncoding('utf8');
for (const mm of mmColumns) {
if (handledMmList.includes(mm.colOptions?.fk_mm_model_id)) continue;
const mmModel = await Model.get(mm.colOptions?.fk_mm_model_id);
await mmModel.getColumns();
const childColumn = mmModel.columns.find(
(col) => col.id === mm.colOptions?.fk_mm_child_column_id,
);
const parentColumn = mmModel.columns.find(
(col) => col.id === mm.colOptions?.fk_mm_parent_column_id,
);
const childColumnTitle = childColumn.title;
const parentColumnTitle = parentColumn.title;
const mmFields = mmModel.columns
.filter((c) => c.uidt === UITypes.ForeignKey)
.map((c) => c.title)
.join(',');
const mmFormatData = (data: any) => {
data.map((d) => {
d.column = mm.id;
d.child = d[childColumnTitle];
d.parent = d[parentColumnTitle];
delete d[childColumnTitle];
delete d[parentColumnTitle];
return d;
});
return { data };
};
const mmLimit = 200;
const mmOffset = 0;
const mmBase =
mmModel.base_id === base.id ? base : await Base.get(mmModel.base_id);
const mmBaseModel = await Model.getBaseModelSQL({
id: mmModel.id,
dbDriver: await NcConnectionMgrv2.get(mmBase),
});
try {
await this.recursiveLinkRead(
mmFormatData,
mmBaseModel,
linkStream,
mmModel,
undefined,
mmOffset,
mmLimit,
mmFields,
true,
);
} catch (e) {
this.logger.error(e);
throw e;
}
handledMmList.push(mm.colOptions?.fk_mm_model_id);
}
linkStream.push(null);
} else {
linkStream.push(null);
}
}
async recursiveRead(
formatter: (data: any) => { data: any },
baseModel: BaseModelSqlv2,
stream: Readable,
model: Model,
view: View,
offset: number,
limit: number,
fields: string,
header = false,
): Promise<void> {
return new Promise((resolve, reject) => {
this.datasService
.getDataList({
model,
view,
query: { limit, offset, fields },
baseModel,
})
.then((result) => {
try {
if (!header) {
stream.push('\r\n');
}
const { data } = formatter(result.list);
stream.push(unparse(data, { header }));
if (result.pageInfo.isLastPage) {
stream.push(null);
resolve();
} else {
this.recursiveRead(
formatter,
baseModel,
stream,
model,
view,
offset + limit,
limit,
fields,
).then(resolve);
}
} catch (e) {
reject(e);
}
});
});
}
async recursiveLinkRead(
formatter: (data: any) => { data: any },
baseModel: BaseModelSqlv2,
linkStream: Readable,
model: Model,
view: View,
offset: number,
limit: number,
fields: string,
header = false,
): Promise<void> {
return new Promise((resolve, reject) => {
this.datasService
.getDataList({
model,
view,
query: { limit, offset, fields },
baseModel,
})
.then((result) => {
try {
if (!header) {
linkStream.push('\r\n');
}
const { data } = formatter(result.list);
if (data) linkStream.push(unparse(data, { header }));
if (result.pageInfo.isLastPage) {
resolve();
} else {
this.recursiveLinkRead(
formatter,
baseModel,
linkStream,
model,
view,
offset + limit,
limit,
fields,
).then(resolve);
}
} catch (e) {
reject(e);
}
});
});
}
async exportBase(param: { path: string; baseId: string }) {
const hrTime = initTime();
const base = await Base.get(param.baseId);
if (!base)
throw NcError.badRequest(`Base not found for id '${param.baseId}'`);
const project = await Project.get(base.project_id);
const models = (await base.getModels()).filter(
// TODO revert this when issue with cache is fixed
(m) => m.base_id === base.id && !m.mm && m.type === 'table',
);
const exportedModels = await this.serializeModels({
modelIds: models.map((m) => m.id),
});
elapsedTime(
hrTime,
`serialize models for ${base.project_id}::${base.id}`,
'exportBase',
);
const exportData = {
id: `${project.id}::${base.id}`,
models: exportedModels,
};
const storageAdapter = await NcPluginMgrv2.storageAdapter();
const destPath = `export/${project.id}/${base.id}/${param.path}`;
try {
const readableStream = new Readable({
read() {},
});
readableStream.setEncoding('utf8');
readableStream.push(JSON.stringify(exportData));
readableStream.push(null);
await (storageAdapter as any).fileCreateByStream(
`${destPath}/schema.json`,
readableStream,
);
const handledMmList: string[] = [];
const combinedLinkStream = new Readable({
read() {},
});
const uploadLinkPromise = (storageAdapter as any).fileCreateByStream(
`${destPath}/data/links.csv`,
combinedLinkStream,
);
for (const model of models) {
const dataStream = new Readable({
read() {},
});
const linkStream = new Readable({
read() {},
});
const linkPromise = new Promise((resolve) => {
linkStream.on('data', (chunk) => {
combinedLinkStream.push(chunk);
});
linkStream.on('end', () => {
combinedLinkStream.push('\r\n');
resolve(null);
});
linkStream.on('error', (e) => {
this.logger.error(e);
resolve(null);
});
});
const uploadPromise = (storageAdapter as any).fileCreateByStream(
`${destPath}/data/${model.id}.csv`,
dataStream,
);
this.streamModelDataAsCsv({
dataStream,
linkStream,
projectId: project.id,
modelId: model.id,
handledMmList,
});
await Promise.all([uploadPromise, linkPromise]);
}
combinedLinkStream.push(null);
await uploadLinkPromise;
elapsedTime(
hrTime,
`export base ${base.project_id}::${base.id}`,
'exportBase',
);
} catch (e) {
throw NcError.badRequest(e);
}
return {
path: destPath,
};
}
}

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

File diff suppressed because it is too large Load Diff

136
packages/nocodb/src/modules/jobs/fallback-queue.service.ts

@ -0,0 +1,136 @@
import { Injectable } from '@nestjs/common';
import PQueue from 'p-queue';
import Emittery from 'emittery';
import { JobStatus, JobTypes } from '../../interface/Jobs';
import { DuplicateProcessor } from './export-import/duplicate.processor';
import { JobsEventService } from './jobs-event.service';
import { AtImportProcessor } from './at-import/at-import.processor';
interface Job {
id: string;
name: string;
status: string;
data: any;
}
@Injectable()
export class QueueService {
static queue = new PQueue({ concurrency: 1 });
static queueIdCounter = 1;
static processed = 0;
static queueMemory: Job[] = [];
static emitter = new Emittery();
constructor(
private readonly jobsEventService: JobsEventService,
private readonly duplicateProcessor: DuplicateProcessor,
private readonly atImportProcessor: AtImportProcessor,
) {
this.emitter.on(JobStatus.ACTIVE, (data: { job: Job }) => {
const job = this.queueMemory.find(
(job) => job.id === data.job.id && job.name === data.job.name,
);
job.status = JobStatus.ACTIVE;
this.jobsEventService.onActive.apply(this.jobsEventService, [job as any]);
});
this.emitter.on(JobStatus.COMPLETED, (data: { job: Job; result: any }) => {
const job = this.queueMemory.find(
(job) => job.id === data.job.id && job.name === data.job.name,
);
job.status = JobStatus.COMPLETED;
this.jobsEventService.onCompleted.apply(this.jobsEventService, [
job,
data.result,
]);
// clear job from memory
this.removeJob(job);
});
this.emitter.on(JobStatus.FAILED, (data: { job: Job; error: Error }) => {
const job = this.queueMemory.find(
(job) => job.id === data.job.id && job.name === data.job.name,
);
job.status = JobStatus.FAILED;
this.jobsEventService.onFailed.apply(this.jobsEventService, [
job,
data.error,
]);
// clear job from memory
this.removeJob(job);
});
}
jobMap = {
[JobTypes.DuplicateBase]: {
this: this.duplicateProcessor,
fn: this.duplicateProcessor.duplicateBase,
},
[JobTypes.DuplicateModel]: {
this: this.duplicateProcessor,
fn: this.duplicateProcessor.duplicateModel,
},
[JobTypes.AtImport]: {
this: this.atImportProcessor,
fn: this.atImportProcessor.job,
},
};
async jobWrapper(job: Job) {
this.emitter.emit(JobStatus.ACTIVE, { job });
try {
const result = await this.jobMap[job.name].fn.apply(
this.jobMap[job.name].this,
[job],
);
this.emitter.emit(JobStatus.COMPLETED, { job, result });
} catch (error) {
this.emitter.emit(JobStatus.FAILED, { job, error });
}
}
get emitter() {
return QueueService.emitter;
}
get queue() {
return QueueService.queue;
}
get queueMemory() {
return QueueService.queueMemory;
}
get queueIndex() {
return QueueService.queueIdCounter;
}
set queueIndex(index: number) {
QueueService.queueIdCounter = index;
}
add(name: string, data: any) {
const id = `${this.queueIndex++}`;
const job = { id: `${id}`, name, status: JobStatus.WAITING, data };
this.queueMemory.push(job);
this.queue.add(() => this.jobWrapper(job));
return { id, name };
}
getJobs(types: string[] | string) {
types = Array.isArray(types) ? types : [types];
return this.queueMemory.filter((q) => types.includes(q.status));
}
getJob(id: string) {
return this.queueMemory.find((q) => q.id === id);
}
// remove job from memory
private removeJob(job: Job) {
const fIndex = this.queueMemory.findIndex(
(q) => q.id === job.id && q.name === job.name,
);
if (fIndex) {
this.queueMemory.splice(fIndex, 1);
}
}
}

23
packages/nocodb/src/modules/jobs/helpers.ts

@ -0,0 +1,23 @@
import { Logger } from '@nestjs/common';
import { JOBS_QUEUE } from '../../interface/Jobs';
export const initTime = function () {
return {
hrTime: process.hrtime(),
};
};
export const elapsedTime = function (
time: { hrTime: [number, number] },
label?: string,
context?: string,
) {
const elapsedS = process.hrtime(time.hrTime)[0].toFixed(3);
const elapsedMs = process.hrtime(time.hrTime)[1] / 1000000;
if (label)
Logger.debug(
`${label}: ${elapsedS}s ${elapsedMs}ms`,
`${JOBS_QUEUE}${context ? `:${context}` : ''}`,
);
time.hrTime = process.hrtime();
};

69
packages/nocodb/src/modules/jobs/jobs-event.service.ts

@ -0,0 +1,69 @@
import {
OnQueueActive,
OnQueueCompleted,
OnQueueFailed,
Processor,
} from '@nestjs/bull';
import { Job } from 'bull';
import boxen from 'boxen';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { JobEvents, JOBS_QUEUE, JobStatus } from '../../interface/Jobs';
@Processor(JOBS_QUEUE)
export class JobsEventService {
constructor(private eventEmitter: EventEmitter2) {}
@OnQueueActive()
onActive(job: Job) {
this.eventEmitter.emit(JobEvents.STATUS, {
name: job.name,
id: job.id.toString(),
status: JobStatus.ACTIVE,
});
}
@OnQueueFailed()
onFailed(job: Job, error: Error) {
console.error(
boxen(
`---- !! JOB FAILED !! ----\nname: ${job.name}\nid:${job.id}\nerror:${error.name} (${error.message})\n\nstack: ${error.stack}`,
{
padding: 1,
borderStyle: 'double',
borderColor: 'yellow',
},
),
);
this.eventEmitter.emit(JobEvents.STATUS, {
name: job.name,
id: job.id.toString(),
status: JobStatus.FAILED,
data: {
error: {
message: error?.message,
},
},
});
}
@OnQueueCompleted()
onCompleted(job: Job, data: any) {
this.eventEmitter.emit(JobEvents.STATUS, {
name: job.name,
id: job.id.toString(),
status: JobStatus.COMPLETED,
data: {
result: data,
},
});
}
sendLog(job: Job, data: { message: string }) {
this.eventEmitter.emit(JobEvents.LOG, {
name: job.name,
id: job.id.toString(),
data,
});
}
}

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

@ -0,0 +1,121 @@
import {
ConnectedSocket,
MessageBody,
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
import { AuthGuard } from '@nestjs/passport';
import { OnEvent } from '@nestjs/event-emitter';
import { JobEvents } from '../../interface/Jobs';
import { JobsService } from './jobs.service';
import type { JobStatus } from '../../interface/Jobs';
import type { OnModuleInit } from '@nestjs/common';
@WebSocketGateway({
cors: {
origin: '*',
allowedHeaders: ['xc-auth'],
credentials: true,
},
namespace: 'jobs',
})
export class JobsGateway implements OnModuleInit {
constructor(private readonly jobsService: JobsService) {}
@WebSocketServer()
server: Server;
async onModuleInit() {
this.server.use(async (socket, next) => {
try {
const context = new ExecutionContextHost([socket.handshake as any]);
const guard = new (AuthGuard('jwt'))(context);
await guard.canActivate(context);
} catch {}
next();
});
}
@SubscribeMessage('subscribe')
async subscribe(
@MessageBody()
body: { _id: number; data: { id: string; name: string } | any },
@ConnectedSocket() client: Socket,
): Promise<void> {
const { _id, data } = body;
if (
Object.keys(data).every((k) => ['name', 'id'].includes(k)) &&
data?.name &&
data?.id
) {
const rooms = (await this.jobsService.jobList(data.name)).map(
(j) => `${j.name}-${j.id}`,
);
const room = rooms.find((r) => r === `${data.name}-${data.id}`);
if (room) {
client.join(`${data.name}-${data.id}`);
client.emit('subscribed', {
_id,
name: data.name,
id: data.id,
});
}
} else {
const job = await this.jobsService.getJobWithData(data);
if (job) {
client.join(`${job.name}-${job.id}`);
client.emit('subscribed', {
_id,
name: job.name,
id: job.id,
});
}
}
}
@SubscribeMessage('status')
async status(
@MessageBody() body: { _id: number; data: { id: string; name: string } },
@ConnectedSocket() client: Socket,
): Promise<void> {
const { _id, data } = body;
client.emit('status', {
_id,
id: data.id,
name: data.name,
status: await this.jobsService.jobStatus(data.id),
});
}
@OnEvent(JobEvents.STATUS)
async sendJobStatus(data: {
name: string;
id: string;
status: JobStatus;
data?: any;
}): Promise<void> {
this.server.to(`${data.name}-${data.id}`).emit('status', {
id: data.id,
name: data.name,
status: data.status,
data: data.data,
});
}
@OnEvent(JobEvents.LOG)
async sendJobLog(data: {
name: string;
id: string;
data: { message: string };
}): Promise<void> {
this.server.to(`${data.name}-${data.id}`).emit('log', {
id: data.id,
name: data.name,
data: data.data,
});
}
}

39
packages/nocodb/src/modules/jobs/jobs.module.ts

@ -0,0 +1,39 @@
import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bull';
import { GlobalModule } from '../global/global.module';
import { DatasModule } from '../datas/datas.module';
import { MetasModule } from '../metas/metas.module';
import { JOBS_QUEUE } from '../../interface/Jobs';
import { JobsService } from './jobs.service';
import { ExportService } from './export-import/export.service';
import { ImportService } from './export-import/import.service';
import { DuplicateController } from './export-import/duplicate.controller';
import { DuplicateProcessor } from './export-import/duplicate.processor';
import { JobsGateway } from './jobs.gateway';
import { QueueService } from './fallback-queue.service';
import { JobsEventService } from './jobs-event.service';
import { AtImportController } from './at-import/at-import.controller';
import { AtImportProcessor } from './at-import/at-import.processor';
@Module({
imports: [
GlobalModule,
DatasModule,
MetasModule,
BullModule.registerQueue({
name: JOBS_QUEUE,
}),
],
controllers: [DuplicateController, AtImportController],
providers: [
QueueService,
JobsGateway,
JobsService,
JobsEventService,
DuplicateProcessor,
ExportService,
ImportService,
AtImportProcessor,
],
})
export class JobsModule {}

59
packages/nocodb/src/modules/jobs/jobs.service.ts

@ -0,0 +1,59 @@
import { InjectQueue } from '@nestjs/bull';
import { Injectable } from '@nestjs/common';
import { Queue } from 'bull';
import { JOBS_QUEUE, JobStatus } from '../../interface/Jobs';
import { QueueService } from './fallback-queue.service';
@Injectable()
export class JobsService {
public activeQueue;
constructor(
@InjectQueue(JOBS_QUEUE) private readonly jobsQueue: Queue,
private readonly fallbackQueueService: QueueService,
) {
this.activeQueue = this.fallbackQueueService;
/* process.env.NC_REDIS_URL
? this.jobsQueue
: this.fallbackQueueService;
*/
}
async jobStatus(jobId: string) {
return await (await this.activeQueue.getJob(jobId)).getState();
}
async jobList(jobType: string) {
return (
await this.activeQueue.getJobs([
JobStatus.ACTIVE,
JobStatus.WAITING,
JobStatus.DELAYED,
JobStatus.PAUSED,
])
).filter((j) => j.name === jobType);
}
async getJobWithData(data: any) {
const jobs = await this.activeQueue.getJobs([
// 'completed',
JobStatus.WAITING,
JobStatus.ACTIVE,
JobStatus.DELAYED,
// 'failed',
JobStatus.PAUSED,
]);
const job = jobs.find((j) => {
for (const key in data) {
if (j.data[key]) {
if (j.data[key] !== data[key]) return false;
} else {
return false;
}
}
return true;
});
return job;
}
}

22
packages/nocodb/src/modules/metas/metas.module.ts

@ -16,7 +16,6 @@ import { GalleriesController } from '../../controllers/galleries.controller';
import { GridColumnsController } from '../../controllers/grid-columns.controller';
import { GridsController } from '../../controllers/grids.controller';
import { HooksController } from '../../controllers/hooks.controller';
import { ImportController } from '../../controllers/imports/import.controller';
import { KanbansController } from '../../controllers/kanbans.controller';
import { MapsController } from '../../controllers/maps.controller';
import { MetaDiffsController } from '../../controllers/meta-diffs.controller';
@ -68,10 +67,10 @@ import { UtilsService } from '../../services/utils.service';
import { ViewColumnsService } from '../../services/view-columns.service';
import { ViewsService } from '../../services/views.service';
import { ApiDocsService } from '../../services/api-docs/api-docs.service';
import { EventEmitterModule } from '../event-emitter/event-emitter.module'
import { GlobalModule } from '../global/global.module';
import { ProjectUsersController } from '../../controllers/project-users.controller';
import { ProjectUsersService } from '../../services/project-users/project-users.service';
import { DatasModule } from '../datas/datas.module';
@Module({
imports: [
@ -98,7 +97,6 @@ import { DatasModule } from '../datas/datas.module';
GridColumnsController,
GridsController,
HooksController,
ImportController,
KanbansController,
MapsController,
MetaDiffsController,
@ -156,5 +154,23 @@ import { DatasModule } from '../datas/datas.module';
SharedBasesService,
BulkDataAliasService,
],
exports: [
TablesService,
ColumnsService,
FiltersService,
SortsService,
ViewsService,
ViewColumnsService,
GridsService,
GridColumnsService,
FormsService,
FormColumnsService,
GalleriesService,
KanbansService,
ProjectsService,
AttachmentsService,
ProjectUsersService,
HooksService,
],
})
export class MetasModule {}

16
packages/nocodb/src/plugins/backblaze/Backblaze.ts

@ -7,6 +7,7 @@ import {
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
export default class Backblaze implements IStorageAdapterV2 {
private s3Client: AWS.S3;
@ -78,6 +79,21 @@ export default class Backblaze implements IStorageAdapterV2 {
});
}
// TODO - implement
fileCreateByStream(_key: string, _stream: Readable): Promise<void> {
return Promise.resolve(undefined);
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
patchRegion(region: string): string {
// in v0.0.1, we constructed the endpoint with `region = s3.us-west-001`
// in v0.0.2, `region` would be `us-west-001`

16
packages/nocodb/src/plugins/gcs/Gcs.ts

@ -7,6 +7,7 @@ import {
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { Readable } from 'stream';
import type { StorageOptions } from '@google-cloud/storage';
export default class Gcs implements IStorageAdapterV2 {
@ -126,4 +127,19 @@ export default class Gcs implements IStorageAdapterV2 {
);
});
}
// TODO - implement
fileCreateByStream(_key: string, _stream: Readable): Promise<void> {
return Promise.resolve(undefined);
}
// TODO - implement
fileReadByStream(_key: string): Promise<Readable> {
return Promise.resolve(undefined);
}
// TODO - implement
getDirectoryList(_path: string): Promise<string[]> {
return Promise.resolve(undefined);
}
}

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

Loading…
Cancel
Save