Browse Source

Feat - Airtable import (#2048)

* wip: add job manager

Signed-off-by: Pranav C <pranavxc@gmail.com>

* wip: job manager impl

Signed-off-by: Pranav C <pranavxc@gmail.com>

* wip: migrations

Signed-off-by: Pranav C <pranavxc@gmail.com>

* wip

Signed-off-by: Pranav C <pranavxc@gmail.com>

* wip: data sync

Signed-off-by: Pranav C <pranavxc@gmail.com>

* feat: sync source creation

Signed-off-by: Pranav C <pranavxc@gmail.com>

* feat: api integration

Signed-off-by: Pranav C <pranavxc@gmail.com>

* chore: remove unused files

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: update to work with project id, handle exceptions

Signed-off-by: Pranav C <pranavxc@gmail.com>

* feat: show more progress details

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: extract id from source creation api response

Signed-off-by: Pranav C <pranavxc@gmail.com>

* feat: bring latest changes

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: column creation and data sync

Signed-off-by: Pranav C <pranavxc@gmail.com>

* refactor: ui improvements

Signed-off-by: Pranav C <pranavxc@gmail.com>

* refactor:  bring changes from sync branch

Signed-off-by: Pranav C <pranavxc@gmail.com>

* refactor: avoid opening additional socket

Signed-off-by: Pranav C <pranavxc@gmail.com>

* chore: update package-lock

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: wait until data and LTAR insertion completes

Signed-off-by: Pranav C <pranavxc@gmail.com>

* feat: load table list after sync completes

Signed-off-by: Pranav C <pranavxc@gmail.com>

* enhancement: add navigation back to dashboard

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: column order as in base, column visibility clean-up

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* refactor: ui updates

Signed-off-by: Pranav C <pranavxc@gmail.com>

* enhancement: load first table after sync

Signed-off-by: Pranav C <pranavxc@gmail.com>

* refactor: common routine to sanitize column names

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* enhancement: accept both hared base id / url

Signed-off-by: Pranav C <pranavxc@gmail.com>

* refactor: sanitize table name

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* sync: fetch improvements

Signed-off-by: mertmit <mertmit99@gmail.com>

* sync: fix nestedLookup

Signed-off-by: mertmit <mertmit99@gmail.com>

* fix: dateTime datatype support

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* enhancement: add validation for credential form

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: migrate default values if configured

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* fix: include support for created time, modified time

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* fix: rollup column function map

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* feat: log column not migrated info

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* feat: fetch template

Signed-off-by: mertmit <mertmit99@gmail.com>

* fix: move record id & hash to bottom of list

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>

* refactor: add import from airtable under project tabs menu

Signed-off-by: Pranav C <pranavxc@gmail.com>

Co-authored-by: Raju Udava <86527202+dstala@users.noreply.github.com>
Co-authored-by: mertmit <mertmit99@gmail.com>
pull/2051/head
Pranav C 3 years ago committed by GitHub
parent
commit
194b205a92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      packages/nc-gui/components/ProjectTreeView.vue
  2. 281
      packages/nc-gui/components/import/importFromAirtable.vue
  3. 1
      packages/nc-gui/components/project/spreadsheet/public/xcKanban.vue
  4. 27
      packages/nc-gui/components/projectTabs.vue
  5. 6
      packages/nc-gui/components/settings/settingsModal.vue
  6. 3
      packages/nc-gui/lang/da.json
  7. 3
      packages/nc-gui/lang/de.json
  8. 3
      packages/nc-gui/lang/en.json
  9. 3
      packages/nc-gui/lang/es.json
  10. 3
      packages/nc-gui/lang/fa.json
  11. 3
      packages/nc-gui/lang/fi.json
  12. 3
      packages/nc-gui/lang/fr.json
  13. 3
      packages/nc-gui/lang/hr.json
  14. 3
      packages/nc-gui/lang/id.json
  15. 3
      packages/nc-gui/lang/it_IT.json
  16. 3
      packages/nc-gui/lang/iw.json
  17. 3
      packages/nc-gui/lang/ja.json
  18. 3
      packages/nc-gui/lang/ko.json
  19. 3
      packages/nc-gui/lang/lv.json
  20. 3
      packages/nc-gui/lang/nl.json
  21. 3
      packages/nc-gui/lang/no.json
  22. 3
      packages/nc-gui/lang/pl.json
  23. 3
      packages/nc-gui/lang/pt.json
  24. 3
      packages/nc-gui/lang/pt_BR.json
  25. 3
      packages/nc-gui/lang/ru.json
  26. 3
      packages/nc-gui/lang/sl.json
  27. 3
      packages/nc-gui/lang/sv.json
  28. 3
      packages/nc-gui/lang/th.json
  29. 3
      packages/nc-gui/lang/tr.json
  30. 3
      packages/nc-gui/lang/uk.json
  31. 3
      packages/nc-gui/lang/vi.json
  32. 3
      packages/nc-gui/lang/zh_CN.json
  33. 3
      packages/nc-gui/lang/zh_HK.json
  34. 3
      packages/nc-gui/lang/zh_TW.json
  35. 14
      packages/nc-gui/store/tabs.js
  36. 339
      packages/nocodb/package-lock.json
  37. 3
      packages/nocodb/package.json
  38. 35
      packages/nocodb/src/lib/noco-jobs/EmitteryJobsMgr.ts
  39. 65
      packages/nocodb/src/lib/noco-jobs/JobsMgr.ts
  40. 20
      packages/nocodb/src/lib/noco-jobs/NocoJobs.ts
  41. 54
      packages/nocodb/src/lib/noco-jobs/RedisJobsMgr.ts
  42. 4
      packages/nocodb/src/lib/noco-models/Project.ts
  43. 57
      packages/nocodb/src/lib/noco-models/SyncLogs.ts
  44. 135
      packages/nocodb/src/lib/noco-models/SyncSource.ts
  45. 9
      packages/nocodb/src/lib/noco/common/XcMigrationSourcev2.ts
  46. 10
      packages/nocodb/src/lib/noco/meta/api/index.ts
  47. 6
      packages/nocodb/src/lib/noco/meta/api/sync/helpers/NocoSyncDestAdapter.ts
  48. 7
      packages/nocodb/src/lib/noco/meta/api/sync/helpers/NocoSyncSourceAdapter.ts
  49. 170
      packages/nocodb/src/lib/noco/meta/api/sync/helpers/fetchAT.ts
  50. 2007
      packages/nocodb/src/lib/noco/meta/api/sync/helpers/job.ts
  51. 31
      packages/nocodb/src/lib/noco/meta/api/sync/helpers/syncMap.ts
  52. 79
      packages/nocodb/src/lib/noco/meta/api/sync/importApis.ts
  53. 53
      packages/nocodb/src/lib/noco/meta/api/sync/syncSourceApis.ts
  54. 1
      packages/nocodb/src/lib/noco/meta/helpers/extractProps.ts
  55. 72
      packages/nocodb/src/lib/noco/migrationsv2/nc_013_sync_source.ts
  56. 4
      packages/nocodb/src/lib/utils/globals.ts

18
packages/nc-gui/components/ProjectTreeView.vue

@ -17,7 +17,7 @@
>
{{ $store.getters["project/GtrProjectName"] }}
</h3>
<github-star-btn v-else/>
<github-star-btn v-else />
</div>
<v-navigation-drawer
ref="drawer"
@ -230,7 +230,7 @@
</template>
</v-list-item-title>
<v-spacer/>
<v-spacer />
<v-tooltip bottom>
<template #activator="{ on }">
@ -348,7 +348,7 @@
<span v-else class="caption">{{ child.name }}</span>
</v-list-item-title>
<template v-if="child.type === 'table'">
<v-spacer/>
<v-spacer />
<div class="action d-flex" @click.stop>
<v-menu>
<template #activator="{ on }">
@ -483,7 +483,7 @@
/>
</div>
<div class="pr-3 advance-menu d-none" :class="{ 'pl-3': !mini }">
<v-divider v-if="_isUIAllowed('treeViewProjectSettings')"/>
<v-divider v-if="_isUIAllowed('treeViewProjectSettings')" />
<v-list
v-if="_isUIAllowed('treeViewProjectSettings')"
@ -621,7 +621,7 @@
</v-tooltip>
</template>
</v-list>
<v-divider/>
<v-divider />
<v-list v-if="_isUIAllowed('previewAs') || previewAs" dense>
<v-list-item>
@ -678,9 +678,7 @@
</v-list>
</div>
<v-divider/>
<v-divider />
<div
v-t="['e:api-docs']"
class="caption pointer nc-docs pb-2 pl-5 pr-3 pt-2 d-flex align-center"
@ -692,7 +690,6 @@
{{ $t('title.apiDocs') }}
</div>
<template v-if="_isUIAllowed('settings')">
<div class="pl-5 pr-3 d-flex align-center pb-2">
<settings-modal>
@ -712,9 +709,8 @@
</div>
</template>
<!-- <v-divider/>-->
<!-- <extras class="pl-1"/>-->
<!-- <extras class="pl-1"/>-->
</div>
</v-navigation-drawer>

281
packages/nc-gui/components/import/importFromAirtable.vue

@ -0,0 +1,281 @@
<template>
<v-dialog v-model="airtableModal" max-width="min(900px, 90%)">
<v-card class="nc-import-card">
<v-toolbar class="elevation-0" height="68">
<h3 class="mt-2 grey--text">
{{ $t('title.importFromAirtable') }} :
<span v-if="step === 1" @dblclick="$set(syncSource.details,'syncViews',true)">Credentials<span
v-if="syncSource && syncSource.details && syncSource.details.syncViews"
>.</span></span>
<span v-else-if="step === 2">Logs</span>
</h3>
<v-spacer />
</v-toolbar>
<div class="h-100" style="width: 100%">
<div>
<v-card v-if="step === 1" class="py-6">
<v-form v-model="valid">
<div v-if="syncSource" class="px-10 mt-1 mx-auto" style="max-width: 400px">
<v-text-field
v-model="syncSource.details.apiKey"
outlined
dense
label="Api Key"
class="caption"
:rules="[v=> !!v || 'Api Key is required']"
/>
<v-text-field
v-model="syncSourceUrlOrId"
outlined
dense
label="Shared Base ID / URL"
class="caption"
:rules="[(v) => !!v || 'Shared Base ID / URL is required']"
/>
</div>
</v-form> <v-card-actions class="justify-center pb-6">
<v-btn
v-t="['c:sync-airtable:save-and-sync']"
:disabled="!valid"
large
color="primary"
@click="saveAndSync"
>
Save & Sync
</v-btn>
</v-card-actions>
</v-card>
<v-card
v-if="step === 2"
class="py-4 mt-4"
>
<v-card
ref="log"
dark
class="mt-2 mx-4 px-2 elevation-0 green--text"
height="500"
style="overflow-y: auto"
>
<div v-for="({msg , status}, i) in progress" :key="i">
<v-icon v-if="status==='FAILED'" color="red" size="15">
mdi-close-circle-outline
</v-icon>
<v-icon v-else color="green" size="15">
mdi-currency-usd
</v-icon>
<span class="caption nc-text">{{ msg }}</span>
</div>
<div
v-if="!progress || !progress.length || progress[progress.length-1].status !== 'COMPLETED' && progress[progress.length-1].status !== 'FAILED'"
class=""
>
<v-icon color="green" size="15">
mdi-loading mdi-spin
</v-icon>
<span class="caption nc-text">Syncing
</span>
<!-- <div class="nc-progress" />-->
</div>
</v-card>
<div
v-if="progress && progress.length && progress[progress.length-1].status === 'COMPLETED'"
class="pa-4 pt-8 text-center"
>
<v-btn large color="primary" @click="airtableModal=false">
Go to dashboard
</v-btn>
</div>
</v-card>
</div>
</div>
</v-card>
</v-dialog>
</template>
<script>
import io from 'socket.io-client'
export default {
name: 'ImportFromAirtable',
props: {
value: Boolean
},
data: () => ({
valid: false,
socket: null,
step: 1,
progress: [],
syncSource: null,
syncSourceUrlOrId: ''
}),
computed: {
airtableModal: {
set(v) {
this.$emit('input', v)
},
get() {
return this.value
}
}
},
watch: {
syncSourceUrlOrId(v) {
if (this.syncSource && this.syncSource.details) {
const m = v && v.match(/(exp|shr).{14}/g)
this.syncSource.details.shareId = m ? m[0] : null
}
}
},
created() {
this.socket = io(new URL(this.$axios.defaults.baseURL, window.location.href.split(/[?#]/)[0]).href, {
extraHeaders: { 'xc-auth': this.$store.state.users.token }
})
this.socket.on('connect_error', () => {
this.socket.disconnect()
this.socket = null
})
const socket = this.socket
socket.on('connect', function(data) {
console.log(socket.id)
console.log('socket connected', data)
})
socket.on('progress', (d) => {
this.progress.push(d)
this.$nextTick(() => {
if (this.$refs.log) {
const el = this.$refs.log.$el
el.scrollTop = el.scrollHeight
}
})
if (d.status === 'COMPLETED') {
this.$store.dispatch('project/_loadTables', {
dbKey: '0.projectJson.envs._noco.db.0',
key: '0.projectJson.envs._noco.db.0.tables',
_nodes: {
dbAlias: 'db',
env: '_noco',
type: 'tableDir'
}
}).then(() => this.$store.dispatch('tabs/loadFirstTableTab'))
}
})
this.loadSyncSrc()
},
beforeDestroy() {
if (this.socket) {
this.socket.disconnect()
}
},
methods: {
async saveAndSync() {
await this.createOrUpdate()
this.sync()
},
sync() {
this.step = 2
this.$axios.post(`/api/v1/db/meta/syncs/${this.syncSource.id}/trigger`, this.payload, {
params: {
id: this.socket.id
}
})
},
async loadSyncSrc() {
const { data: { list: srcs } } = await this.$axios.get(`/api/v1/db/meta/projects/${this.projectId}/syncs`)
if (srcs && srcs[0]) {
srcs[0].details = srcs[0].details || {}
this.syncSource = srcs[0]
this.syncSourceUrlOrId = srcs[0].details.shareId
} else {
this.syncSource = {
type: 'Airtable',
details: {
syncInterval: '15mins',
syncDirection: 'Airtable to NocoDB',
syncRetryCount: 1,
syncViews: false,
apiKey: '',
shareId: ''
}
}
}
},
async createOrUpdate() {
try {
const { id, ...payload } = this.syncSource
if (id) {
await this.$axios.patch(`/api/v1/db/meta/syncs/${id}`, payload)
} else {
this.syncSource = (await this.$axios.post(`/api/v1/db/meta/projects/${this.projectId}/syncs`, payload)).data
}
} catch (e) {
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000)
}
}
}
}
</script>
<style scoped>
.nc-progress {
margin-left: 12px;
position: relative;
width: 5px;
height: 5px;
border-radius: 5px;
background-color: #9880ff;
color: #9880ff;
animation: dotFlashing 1s infinite linear alternate;
animation-delay: .5s;
}
.nc-progress::before, .nc-progress::after {
content: '';
display: inline-block;
position: absolute;
top: 0;
}
.nc-progress::before {
left: -7.5px;
width: 5px;
height: 5px;
border-radius: 5px;
background-color: #9880ff;
color: #9880ff;
animation: dotFlashing 1s infinite alternate;
animation-delay: 0s;
}
.nc-progress::after {
left: 7.5px;
width: 5px;
height: 5px;
border-radius: 5px;
background-color: var(--v-primary-base);
color: var(--v-primary-base);
animation: dotFlashing 1s infinite alternate;
animation-delay: 1s;
}
@keyframes dotFlashing {
0% {
background-color: var(--v-primary-base);
}
50%,
100% {
background-color: var(--v-backgroundColor-base);
}
}
</style>

1
packages/nc-gui/components/project/spreadsheet/public/xcKanban.vue

@ -102,6 +102,7 @@ export default {
selectedExpandRowIndex: null,
selectedExpandRowMeta: null,
meta: null,
navDrawer: true,
selected: {
row: null,

27
packages/nc-gui/components/projectTabs.vue

@ -244,7 +244,7 @@
</v-tabs-items>
<!-- Add / Import -->
<v-menu offset-y v-if="_isUIAllowed('addOrImport')">
<v-menu v-if="_isUIAllowed('addOrImport')" offset-y>
<template #activator="{ on }">
<v-btn
color="primary"
@ -314,6 +314,21 @@
</span>
</v-list-item-title>
</v-list-item>
<v-list-item
v-if="_isUIAllowed('airtableImport')"
v-t="['a:actions:import-airtable']"
@click="airtableImportModal = true"
>
<v-list-item-title>
<v-icon small>
mdi-table-large
</v-icon>
<span class="caption">
<!-- TODO: i18n -->
Airtable
</span>
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-tabs>
@ -334,6 +349,8 @@
hide-label
@closeModal="quickImportModal = false"
/>
<import-from-airtable v-if="airtableImportModal" v-model="airtableImportModal" />
</v-container>
</template>
@ -366,9 +383,11 @@ import GrpcClient from '@/components/project/grpcClient'
import GlobalAcl from '@/components/globalAcl'
import AuditTab from '~/components/project/auditTab'
import QuickImport from '@/components/import/quickImport'
import ImportFromAirtable from '~/components/import/importFromAirtable'
export default {
components: {
ImportFromAirtable,
SwaggerClient,
// Screensaver,
DlgTableCreate,
@ -407,7 +426,8 @@ export default {
hideLogWindows: false,
showScreensaver: false,
quickImportModal: false,
quickImportType: ''
quickImportType: '',
airtableImportModal: false
}
},
methods: {
@ -489,6 +509,9 @@ export default {
onImportFromExcelOrCSV(quickImportType) {
this.quickImportModal = true
this.quickImportType = quickImportType
},
onAirtableImport() {
this.airtableImportModal = true
}
},
computed: {

6
packages/nc-gui/components/settings/settingsModal.vue

@ -177,13 +177,15 @@ import DisableOrEnableModels from '~/components/project/projectMetadata/disableO
import AuthTab from '~/components/authTab'
import XcMeta from '~/components/project/settings/xcMeta'
import AuditTab from '~/components/project/auditTab'
import ImportFromAirtable from '~/components/import/importFromAirtable'
export default {
name: 'SettingsModal',
components: { AuditTab, XcMeta, AuthTab, DisableOrEnableModels, AppStore },
components: { ImportFromAirtable, AuditTab, XcMeta, AuthTab, DisableOrEnableModels, AppStore },
data: () => ({
settingsModal: false,
activePage: 'role'
activePage: 'role',
improtFromAirtableModal: false
})
}
</script>

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

@ -171,7 +171,8 @@
"headLogin": "Log ind | Nocodb.",
"resetPassword": "Nulstil din adgangskode",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Bemærk Via.",

3
packages/nc-gui/lang/de.json

@ -171,7 +171,8 @@
"headLogin": "Anmelden | NocoDB",
"resetPassword": "Passwort zurücksetzen",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Benachrichtigen mit",

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

@ -171,7 +171,8 @@
"headLogin": "Log In | NocoDB",
"resetPassword": "Reset your password",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Notify Via",

3
packages/nc-gui/lang/es.json

@ -171,7 +171,8 @@
"headLogin": "Acceder | NocoDB",
"resetPassword": "Cambiar contraseña",
"teamAndSettings": "Equipo y configuración",
"apiDocs": "Documentación de la API"
"apiDocs": "Documentación de la API",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Notificar a través de",

3
packages/nc-gui/lang/fa.json

@ -171,7 +171,8 @@
"headLogin": "ورود | NocoDB",
"resetPassword": "بازنشانی کلمه عبور شما",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "اعلان از طریق",

3
packages/nc-gui/lang/fi.json

@ -171,7 +171,8 @@
"headLogin": "Kirjaudu sisään | Nokodb",
"resetPassword": "Nollaa salasana",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Ilmoittaa kautta",

3
packages/nc-gui/lang/fr.json

@ -171,7 +171,8 @@
"headLogin": "Connexion | Nocodb",
"resetPassword": "Réinitialiser le mot de passe",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Notifier via",

3
packages/nc-gui/lang/hr.json

@ -171,7 +171,8 @@
"headLogin": "Prijavite se | Nocodb",
"resetPassword": "Vraćanje izvorne lozinke",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Obavijestiti putem",

3
packages/nc-gui/lang/id.json

@ -171,7 +171,8 @@
"headLogin": "Masuk | Nocodb.",
"resetPassword": "Mereset password Anda",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Beri tahu VIA.",

3
packages/nc-gui/lang/it_IT.json

@ -171,7 +171,8 @@
"headLogin": "Accedi | NocoDB",
"resetPassword": "Reimposta la tua password",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Notifica via.",

3
packages/nc-gui/lang/iw.json

@ -171,7 +171,8 @@
"headLogin": "התחבר | נוקודב",
"resetPassword": "לאפס את הסיסמה שלך",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "תודיע דרך",

3
packages/nc-gui/lang/ja.json

@ -171,7 +171,8 @@
"headLogin": "ログイン | NocoDB",
"resetPassword": "パスワードをリセットする",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "次で通知する",

3
packages/nc-gui/lang/ko.json

@ -171,7 +171,8 @@
"headLogin": "로그인 | NocoDB.",
"resetPassword": "비밀번호를 재설정",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "통지를 통지합니다",

3
packages/nc-gui/lang/lv.json

@ -171,7 +171,8 @@
"headLogin": "Pieslēgties | NocoDB",
"resetPassword": "Atjaunot paroli",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Paziņot izmantojot",

3
packages/nc-gui/lang/nl.json

@ -171,7 +171,8 @@
"headLogin": "Log in | Nocodb",
"resetPassword": "Stel je wachtwoord opnieuw in",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Melding via",

3
packages/nc-gui/lang/no.json

@ -171,7 +171,8 @@
"headLogin": "Logg inn | NocoDB.",
"resetPassword": "Tilbakestill passordet ditt",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Varsle Via.",

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

@ -171,7 +171,8 @@
"headLogin": "Zaloguj się | NOCODB.",
"resetPassword": "Zresetuj swoje hasło",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Powiadomić VIA.",

3
packages/nc-gui/lang/pt.json

@ -171,7 +171,8 @@
"headLogin": "Autentique-se | Noco",
"resetPassword": "Redefina a sua palavra-passe",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Notificar via.",

3
packages/nc-gui/lang/pt_BR.json

@ -171,7 +171,8 @@
"headLogin": "Autentique-se | NocoDB",
"resetPassword": "Redefina a sua senha",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Notificar via.",

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

@ -171,7 +171,8 @@
"headLogin": "Войти |. NOCODB",
"resetPassword": "Сбросить пароль",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Уведомлять через",

3
packages/nc-gui/lang/sl.json

@ -171,7 +171,8 @@
"headLogin": "Prijava | Nocodb.",
"resetPassword": "Ponastavi geslo",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Obvestite VIO.",

3
packages/nc-gui/lang/sv.json

@ -171,7 +171,8 @@
"headLogin": "Logga in | Nocodb",
"resetPassword": "Återställ ditt lösenord",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Meddela via",

3
packages/nc-gui/lang/th.json

@ -171,7 +171,8 @@
"headLogin": "เขาสระบบ nocodb",
"resetPassword": "รเซตรหสผานของคณ",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "แจงเตอนผาน",

3
packages/nc-gui/lang/tr.json

@ -171,7 +171,8 @@
"headLogin": "Giriş | Nocodb",
"resetPassword": "Şifrenizi sıfırlayın",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Aracılığıyla Bildir",

3
packages/nc-gui/lang/uk.json

@ -171,7 +171,8 @@
"headLogin": "Вхід | Нокодб",
"resetPassword": "Скинути пароль",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Повідомити через",

3
packages/nc-gui/lang/vi.json

@ -171,7 +171,8 @@
"headLogin": "Đăng nhập |. NOCODB.",
"resetPassword": "Đặt lại mật khẩu của bạn",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Thông báo qua",

3
packages/nc-gui/lang/zh_CN.json

@ -171,7 +171,8 @@
"headLogin": "登录 | NocoDB",
"resetPassword": "重置密码",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "通知Via",

3
packages/nc-gui/lang/zh_HK.json

@ -171,7 +171,8 @@
"headLogin": "登入 | NocoDB",
"resetPassword": "reset你嗰密碼",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "通知Via",

3
packages/nc-gui/lang/zh_TW.json

@ -171,7 +171,8 @@
"headLogin": "登入|NocoDB",
"resetPassword": "重設密碼",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs"
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "通知Via",

14
packages/nc-gui/store/tabs.js

@ -290,6 +290,20 @@ export const actions = {
}
commit('list', tabs)
},
async loadFirstTableTab({ commit, state, rootGetters, dispatch, rootState }, load) {
const tabs = []
const nodes = rootState.project
.list[0] // project
.children[0] // environment
.children[0] // db
.children.find(n => n.type === 'tableDir') // parent node
.children
if (nodes && nodes[0]) {
tabs.push(nodes[0])
}
if (tabs.length) { commit('list', tabs) }
},
removeTableTab({ commit, state }, nodes) {
const tabs = JSON.parse(JSON.stringify(state.list))

339
packages/nocodb/package-lock.json generated

@ -12,6 +12,7 @@
"@google-cloud/storage": "^5.7.2",
"@graphql-tools/merge": "^6.0.12",
"@sentry/node": "^6.3.5",
"airtable": "^0.11.3",
"archiver": "^5.0.2",
"auto-bind": "^4.0.0",
"aws-sdk": "^2.829.0",
@ -19,6 +20,7 @@
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.0",
"boxen": "^5.0.0",
"bullmq": "^1.81.1",
"clear": "^0.1.0",
"cli-table3": "^0.6.0",
"cluster": "^0.7.7",
@ -74,6 +76,7 @@
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"ora": "^4.0.4",
"os-locale": "^5.0.0",
"papaparse": "^5.3.1",
@ -2367,6 +2370,11 @@
"node": ">=6.5"
}
},
"node_modules/abortcontroller-polyfill": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz",
"integrity": "sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q=="
},
"node_modules/accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
@ -2498,6 +2506,26 @@
"node": ">=8"
}
},
"node_modules/airtable": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/airtable/-/airtable-0.11.3.tgz",
"integrity": "sha512-fPT0SdipmJU1eQNe+sJshvnb87HcZ2rPS7gJuryDAF9xDbfbxB8AmYQSuC1f0PIbLyPPpSWBuEewf4UJ9i3rJw==",
"dependencies": {
"@types/node": ">=8.0.0 <15",
"abort-controller": "^3.0.0",
"abortcontroller-polyfill": "^1.4.0",
"lodash": "^4.17.21",
"node-fetch": "^2.6.7"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/airtable/node_modules/@types/node": {
"version": "14.18.16",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.16.tgz",
"integrity": "sha512-X3bUMdK/VmvrWdoTkz+VCn6nwKwrKCFTHtqwBIaQJNx4RUIBBUFXM00bqPz/DsDd+Icjmzm6/tyYZzeGVqb6/Q=="
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -4257,6 +4285,27 @@
"integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
"dev": true
},
"node_modules/bullmq": {
"version": "1.81.4",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-1.81.4.tgz",
"integrity": "sha512-sUEWOMKZnWlh1/XNqYAoSwXW6P8nZN7uJiHKZ8XlZCiIxWlEGjFtlugkkiCZ0lsTI2nNRHdxfpn78x9K3L1utQ==",
"dependencies": {
"cron-parser": "^4.2.1",
"get-port": "^5.1.1",
"glob": "^7.2.0",
"ioredis": "^4.28.5",
"lodash": "^4.17.21",
"msgpackr": "^1.4.6",
"semver": "^6.3.0",
"tslib": "^1.14.1",
"uuid": "^8.3.2"
}
},
"node_modules/bullmq/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
@ -7324,6 +7373,17 @@
"moment-timezone": "^0.5.x"
}
},
"node_modules/cron-parser": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.4.0.tgz",
"integrity": "sha512-TrE5Un4rtJaKgmzPewh67yrER5uKM0qI9hGLDBfWb8GGRe9pn/SDkhVrdHa4z7h0SeyeNxnQnogws/H+AQANQA==",
"dependencies": {
"luxon": "^1.28.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/cross-argv": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/cross-argv/-/cross-argv-1.0.0.tgz",
@ -10760,7 +10820,6 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz",
"integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==",
"dev": true,
"engines": {
"node": ">=8"
},
@ -14520,6 +14579,14 @@
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
"dev": true
},
"node_modules/luxon": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz",
"integrity": "sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==",
"engines": {
"node": "*"
}
},
"node_modules/mailersend": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/mailersend/-/mailersend-1.3.1.tgz",
@ -15728,6 +15795,104 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/msgpackr": {
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.5.7.tgz",
"integrity": "sha512-Hsa80i8W4BiObSMHslfnwC+CC1CYHZzoXJZn0+3EvoCEOgt3c5QlXhdcjgFk2aZxMgpV8aUFZqJyQUCIp4UrzA==",
"optionalDependencies": {
"msgpackr-extract": "^1.1.4"
}
},
"node_modules/msgpackr-extract": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-1.1.4.tgz",
"integrity": "sha512-WQbHvsThprXh+EqZYy+SQFEs7z6bNM7a0vgirwUfwUcphWGT2mdPcpyLCNiRsN6w5q5VKJUMblHY+tNEyceb9Q==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"node-gyp-build-optional-packages": "^4.3.2"
},
"optionalDependencies": {
"msgpackr-extract-darwin-arm64": "1.1.0",
"msgpackr-extract-darwin-x64": "1.1.0",
"msgpackr-extract-linux-arm": "1.1.0",
"msgpackr-extract-linux-arm64": "1.1.0",
"msgpackr-extract-linux-x64": "1.1.0",
"msgpackr-extract-win32-x64": "1.1.0"
}
},
"node_modules/msgpackr-extract-darwin-arm64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-1.1.0.tgz",
"integrity": "sha512-s1kHoT12tS2cCQOv+Wl3I+/cYNJXBPtwQqGA+dPYoXmchhXiE0Nso+BIfvQ5PxbmAyjj54Q5o7PnLTqVquNfZA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
]
},
"node_modules/msgpackr-extract-darwin-x64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-1.1.0.tgz",
"integrity": "sha512-yx/H/i12IKg4eWGu/eKdKzJD4jaYvvujQSaVmeOMCesbSQnWo5X6YR9TFjoiNoU9Aexk1KufzL9gW+1DozG1yw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
]
},
"node_modules/msgpackr-extract-linux-arm": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-1.1.0.tgz",
"integrity": "sha512-0VvSCqi12xpavxl14gMrauwIzHqHbmSChUijy/uo3mpjB1Pk4vlisKpZsaOZvNJyNKj0ACi5jYtbWnnOd7hYGw==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/msgpackr-extract-linux-arm64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-1.1.0.tgz",
"integrity": "sha512-AxFle3fHNwz2V4CYDIGFxI6o/ZuI0lBKg0uHI8EcCMUmDE5mVAUWYge5WXmORVvb8sVWyVgFlmi3MTu4Ve6tNQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/msgpackr-extract-linux-x64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-1.1.0.tgz",
"integrity": "sha512-O+XoyNFWpdB8oQL6O/YyzffPpmG5rTNrr1nKLW70HD2ENJUhcITzbV7eZimHPzkn8LAGls1tBaMTHQezTBpFOw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/msgpackr-extract-win32-x64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-1.1.0.tgz",
"integrity": "sha512-6AJdM5rNsL4yrskRfhujVSPEd6IBpgvsnIT/TPowKNLQ62iIdryizPY2PJNFiW3AJcY249AHEiDBXS1cTDPxzA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
]
},
"node_modules/mssql": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/mssql/-/mssql-6.4.0.tgz",
@ -16150,14 +16315,22 @@
"integrity": "sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA=="
},
"node_modules/node-fetch": {
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
"integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-forge": {
@ -16194,6 +16367,17 @@
"node": ">= 0.8.0"
}
},
"node_modules/node-gyp-build-optional-packages": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-4.3.2.tgz",
"integrity": "sha512-P5Ep3ISdmwcCkZIaBaQamQtWAG0facC89phWZgi5Z3hBU//J6S48OIvyZWSPPf6yQMklLZiqoosWAZUj7N+esA==",
"optional": true,
"bin": {
"node-gyp-build-optional": "optional.js",
"node-gyp-build-optional-packages": "bin.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/node-gyp/node_modules/nopt": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
@ -17257,6 +17441,14 @@
"node": ">=0.10.0"
}
},
"node_modules/object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"engines": {
"node": ">= 6"
}
},
"node_modules/object-inspect": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
@ -26673,6 +26865,11 @@
"event-target-shim": "^5.0.0"
}
},
"abortcontroller-polyfill": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz",
"integrity": "sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q=="
},
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
@ -26781,6 +26978,25 @@
}
}
},
"airtable": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/airtable/-/airtable-0.11.3.tgz",
"integrity": "sha512-fPT0SdipmJU1eQNe+sJshvnb87HcZ2rPS7gJuryDAF9xDbfbxB8AmYQSuC1f0PIbLyPPpSWBuEewf4UJ9i3rJw==",
"requires": {
"@types/node": ">=8.0.0 <15",
"abort-controller": "^3.0.0",
"abortcontroller-polyfill": "^1.4.0",
"lodash": "^4.17.21",
"node-fetch": "^2.6.7"
},
"dependencies": {
"@types/node": {
"version": "14.18.16",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.16.tgz",
"integrity": "sha512-X3bUMdK/VmvrWdoTkz+VCn6nwKwrKCFTHtqwBIaQJNx4RUIBBUFXM00bqPz/DsDd+Icjmzm6/tyYZzeGVqb6/Q=="
}
}
},
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -28213,6 +28429,29 @@
"integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
"dev": true
},
"bullmq": {
"version": "1.81.4",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-1.81.4.tgz",
"integrity": "sha512-sUEWOMKZnWlh1/XNqYAoSwXW6P8nZN7uJiHKZ8XlZCiIxWlEGjFtlugkkiCZ0lsTI2nNRHdxfpn78x9K3L1utQ==",
"requires": {
"cron-parser": "^4.2.1",
"get-port": "^5.1.1",
"glob": "^7.2.0",
"ioredis": "^4.28.5",
"lodash": "^4.17.21",
"msgpackr": "^1.4.6",
"semver": "^6.3.0",
"tslib": "^1.14.1",
"uuid": "^8.3.2"
},
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
@ -30607,6 +30846,14 @@
"moment-timezone": "^0.5.x"
}
},
"cron-parser": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.4.0.tgz",
"integrity": "sha512-TrE5Un4rtJaKgmzPewh67yrER5uKM0qI9hGLDBfWb8GGRe9pn/SDkhVrdHa4z7h0SeyeNxnQnogws/H+AQANQA==",
"requires": {
"luxon": "^1.28.0"
}
},
"cross-argv": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/cross-argv/-/cross-argv-1.0.0.tgz",
@ -33382,8 +33629,7 @@
"get-port": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz",
"integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==",
"dev": true
"integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ=="
},
"get-stdin": {
"version": "6.0.0",
@ -36211,6 +36457,11 @@
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
"dev": true
},
"luxon": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz",
"integrity": "sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ=="
},
"mailersend": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/mailersend/-/mailersend-1.3.1.tgz",
@ -37175,6 +37426,65 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"msgpackr": {
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.5.7.tgz",
"integrity": "sha512-Hsa80i8W4BiObSMHslfnwC+CC1CYHZzoXJZn0+3EvoCEOgt3c5QlXhdcjgFk2aZxMgpV8aUFZqJyQUCIp4UrzA==",
"requires": {
"msgpackr-extract": "^1.1.4"
}
},
"msgpackr-extract": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-1.1.4.tgz",
"integrity": "sha512-WQbHvsThprXh+EqZYy+SQFEs7z6bNM7a0vgirwUfwUcphWGT2mdPcpyLCNiRsN6w5q5VKJUMblHY+tNEyceb9Q==",
"optional": true,
"requires": {
"msgpackr-extract-darwin-arm64": "1.1.0",
"msgpackr-extract-darwin-x64": "1.1.0",
"msgpackr-extract-linux-arm": "1.1.0",
"msgpackr-extract-linux-arm64": "1.1.0",
"msgpackr-extract-linux-x64": "1.1.0",
"msgpackr-extract-win32-x64": "1.1.0",
"node-gyp-build-optional-packages": "^4.3.2"
}
},
"msgpackr-extract-darwin-arm64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-1.1.0.tgz",
"integrity": "sha512-s1kHoT12tS2cCQOv+Wl3I+/cYNJXBPtwQqGA+dPYoXmchhXiE0Nso+BIfvQ5PxbmAyjj54Q5o7PnLTqVquNfZA==",
"optional": true
},
"msgpackr-extract-darwin-x64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-1.1.0.tgz",
"integrity": "sha512-yx/H/i12IKg4eWGu/eKdKzJD4jaYvvujQSaVmeOMCesbSQnWo5X6YR9TFjoiNoU9Aexk1KufzL9gW+1DozG1yw==",
"optional": true
},
"msgpackr-extract-linux-arm": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-1.1.0.tgz",
"integrity": "sha512-0VvSCqi12xpavxl14gMrauwIzHqHbmSChUijy/uo3mpjB1Pk4vlisKpZsaOZvNJyNKj0ACi5jYtbWnnOd7hYGw==",
"optional": true
},
"msgpackr-extract-linux-arm64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-1.1.0.tgz",
"integrity": "sha512-AxFle3fHNwz2V4CYDIGFxI6o/ZuI0lBKg0uHI8EcCMUmDE5mVAUWYge5WXmORVvb8sVWyVgFlmi3MTu4Ve6tNQ==",
"optional": true
},
"msgpackr-extract-linux-x64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-1.1.0.tgz",
"integrity": "sha512-O+XoyNFWpdB8oQL6O/YyzffPpmG5rTNrr1nKLW70HD2ENJUhcITzbV7eZimHPzkn8LAGls1tBaMTHQezTBpFOw==",
"optional": true
},
"msgpackr-extract-win32-x64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-1.1.0.tgz",
"integrity": "sha512-6AJdM5rNsL4yrskRfhujVSPEd6IBpgvsnIT/TPowKNLQ62iIdryizPY2PJNFiW3AJcY249AHEiDBXS1cTDPxzA==",
"optional": true
},
"mssql": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/mssql/-/mssql-6.4.0.tgz",
@ -37556,9 +37866,9 @@
"integrity": "sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA=="
},
"node-fetch": {
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
"integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"requires": {
"whatwg-url": "^5.0.0"
}
@ -37605,6 +37915,12 @@
}
}
},
"node-gyp-build-optional-packages": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-4.3.2.tgz",
"integrity": "sha512-P5Ep3ISdmwcCkZIaBaQamQtWAG0facC89phWZgi5Z3hBU//J6S48OIvyZWSPPf6yQMklLZiqoosWAZUj7N+esA==",
"optional": true
},
"node-libs-browser": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
@ -38454,6 +38770,11 @@
}
}
},
"object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="
},
"object-inspect": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",

3
packages/nocodb/package.json

@ -94,6 +94,7 @@
"@google-cloud/storage": "^5.7.2",
"@graphql-tools/merge": "^6.0.12",
"@sentry/node": "^6.3.5",
"airtable": "^0.11.3",
"archiver": "^5.0.2",
"auto-bind": "^4.0.0",
"aws-sdk": "^2.829.0",
@ -101,6 +102,7 @@
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.0",
"boxen": "^5.0.0",
"bullmq": "^1.81.1",
"clear": "^0.1.0",
"cli-table3": "^0.6.0",
"cluster": "^0.7.7",
@ -156,6 +158,7 @@
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"ora": "^4.0.4",
"os-locale": "^5.0.0",
"papaparse": "^5.3.1",

35
packages/nocodb/src/lib/noco-jobs/EmitteryJobsMgr.ts

@ -0,0 +1,35 @@
import JobsMgr from './JobsMgr';
import Emittery from 'emittery';
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);
}
});
}
}

65
packages/nocodb/src/lib/noco-jobs/JobsMgr.ts

@ -0,0 +1,65 @@
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, msg: string) => void;
onProgress?: (payload: any, msgOrData: 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, msg: string) => void) {
this.failureCbks[jobName] = this.failureCbks[jobName] || [];
this.failureCbks[jobName].push(cbk);
}
addProgressCbk(
jobName: string,
cbk: (payload: any, progress: string) => 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,
msg?: string
) {
await Promise.all(
this.progressCbks?.[jobName]?.map(cb => cb(payload, msg))
);
}
}

20
packages/nocodb/src/lib/noco-jobs/NocoJobs.ts

@ -0,0 +1,20 @@
import JobsMgr from './JobsMgr';
import EmitteryJobsMgr from './EmitteryJobsMgr';
import RedisJobsMgr from './RedisJobsMgr';
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;
}
}

54
packages/nocodb/src/lib/noco-jobs/RedisJobsMgr.ts

@ -0,0 +1,54 @@
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);
}
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 }
);
}
}

4
packages/nocodb/src/lib/noco-models/Project.ts

@ -152,14 +152,14 @@ export default class Project implements ProjectType {
deleted: false
});
await NocoCache.set(`${CacheScope.PROJECT}:${projectId}`, projectData);
if (projectData.uuid) {
if (projectData?.uuid) {
await NocoCache.set(
`${CacheScope.PROJECT}:${projectData.uuid}`,
projectId
);
}
} else {
if (projectData.deleted) {
if (projectData?.deleted) {
projectData = null;
}
}

57
packages/nocodb/src/lib/noco-models/SyncLogs.ts

@ -0,0 +1,57 @@
import Noco from '../noco/Noco';
import { MetaTable } from '../utils/globals';
export default class SyncLogs {
id?: string;
project_id?: string;
fk_sync_source_id?: string;
time_taken?: string;
status?: string;
status_details?: string;
constructor(syncLog: Partial<SyncLogs>) {
Object.assign(this, syncLog);
}
static async list(projectId: string, ncMeta = Noco.ncMeta) {
const syncLogs = await ncMeta.metaList(null, null, MetaTable.SYNC_LOGS, {
condition: {
project_id: projectId
},
orderBy: {
created_at: 'asc'
}
});
return syncLogs?.map(h => new SyncLogs(h));
}
public static async insert(
syncLog: Partial<
SyncLogs & {
created_at?;
updated_at?;
}
>,
ncMeta = Noco.ncMeta
) {
const insertObj = {
project_id: syncLog?.project_id,
fk_sync_source_id: syncLog?.fk_sync_source_id,
time_taken: syncLog?.time_taken,
status: syncLog?.status,
status_details: syncLog?.status_details
};
const { id } = await ncMeta.metaInsert2(
null,
null,
MetaTable.SYNC_LOGS,
insertObj
);
return new SyncLogs({ ...insertObj, id });
}
static async delete(syncLogId: any, ncMeta = Noco.ncMeta) {
return await ncMeta.metaDelete(null, null, MetaTable.SYNC_LOGS, syncLogId);
}
}

135
packages/nocodb/src/lib/noco-models/SyncSource.ts

@ -0,0 +1,135 @@
import Noco from '../noco/Noco';
import { MetaTable } from '../utils/globals';
import extractProps from '../noco/meta/helpers/extractProps';
import User from './User';
export default class SyncSource {
id?: string;
title?: string;
type?: string;
details?: any;
deleted?: boolean;
order?: number;
project_id?: string;
fk_user_id?: string;
constructor(syncSource: Partial<SyncSource>) {
Object.assign(this, syncSource);
}
public getUser(ncMeta = Noco.ncMeta) {
return User.get(this.fk_user_id, ncMeta);
}
public static async get(syncSourceId: string, ncMeta = Noco.ncMeta) {
const syncSource = await ncMeta.metaGet2(
null,
null,
MetaTable.SYNC_SOURCE,
syncSourceId
);
if (syncSource.details && typeof syncSource.details === 'string') {
try {
syncSource.details = JSON.parse(syncSource.details);
} catch {}
}
return syncSource && new SyncSource(syncSource);
}
static async list(projectId: string, ncMeta = Noco.ncMeta) {
const syncSources = await ncMeta.metaList(
null,
null,
MetaTable.SYNC_SOURCE,
{
condition: {
project_id: projectId
},
orderBy: {
created_at: 'asc'
}
}
);
for (const syncSource of syncSources) {
if (syncSource.details && typeof syncSource.details === 'string') {
try {
syncSource.details = JSON.parse(syncSource.details);
} catch {}
}
}
return syncSources?.map(h => new SyncSource(h));
}
public static async insert(
syncSource: Partial<
SyncSource & {
created_at?;
updated_at?;
}
>,
ncMeta = Noco.ncMeta
) {
const insertObj = {
id: syncSource?.id,
title: syncSource?.title,
type: syncSource?.type,
details: syncSource?.details,
project_id: syncSource?.project_id,
fk_user_id: syncSource?.fk_user_id
};
if (insertObj.details && typeof insertObj.details === 'object') {
insertObj.details = JSON.stringify(insertObj.details);
}
const { id } = await ncMeta.metaInsert2(
null,
null,
MetaTable.SYNC_SOURCE,
insertObj
);
return this.get(id, ncMeta);
}
public static async update(
syncSourceId: string,
syncSource: Partial<SyncSource>,
ncMeta = Noco.ncMeta
) {
const updateObj = extractProps(syncSource, [
'id',
'title',
'type',
'details',
'deleted',
'order',
'project_id'
]);
if (updateObj.details && typeof updateObj.details === 'object') {
updateObj.details = JSON.stringify(updateObj.details);
}
// set meta
await ncMeta.metaUpdate(
null,
null,
MetaTable.SYNC_SOURCE,
updateObj,
syncSourceId
);
return this.get(syncSourceId, ncMeta);
}
static async delete(syncSourceId: any, ncMeta = Noco.ncMeta) {
return await ncMeta.metaDelete(
null,
null,
MetaTable.SYNC_SOURCE,
syncSourceId
);
}
}

9
packages/nocodb/src/lib/noco/common/XcMigrationSourcev2.ts

@ -1,5 +1,6 @@
import * as nc_011 from '../migrationsv2/nc_011';
import * as nc_012_alter_column_data_types from '../migrationsv2/nc_012_alter_column_data_types';
import * as nc_013_sync_source from '../migrationsv2/nc_013_sync_source';
// Create a custom migration source class
export default class XcMigrationSourcev2 {
@ -8,7 +9,11 @@ export default class XcMigrationSourcev2 {
// arguments to getMigrationName and getMigration
public getMigrations(): Promise<any> {
// In this example we are just returning migration names
return Promise.resolve(['nc_011', 'nc_012_alter_column_data_types']);
return Promise.resolve([
'nc_011',
'nc_012_alter_column_data_types',
'nc_013_sync_source'
]);
}
public getMigrationName(migration): string {
@ -21,6 +26,8 @@ export default class XcMigrationSourcev2 {
return nc_011;
case 'nc_012_alter_column_data_types':
return nc_012_alter_column_data_types;
case 'nc_013_sync_source':
return nc_013_sync_source;
}
}
}

10
packages/nocodb/src/lib/noco/meta/api/index.ts

@ -41,11 +41,15 @@ import {
publicMetaApis
} from './publicApis';
import { Tele } from 'nc-help';
import { Server } from 'socket.io';
import { Server, Socket } from 'socket.io';
import passport from 'passport';
import crypto from 'crypto';
import swaggerApis from './swagger/swaggerApis';
import importApis from './sync/importApis';
import syncSourceApis from './sync/syncSourceApis';
const clients: { [id: string]: Socket } = {};
export default function(router: Router, server) {
initStrategies(router);
@ -85,6 +89,7 @@ export default function(router: Router, server) {
router.use(apiTokenApis);
router.use(hookFilterApis);
router.use(swaggerApis);
router.use(syncSourceApis);
userApis(router);
@ -109,6 +114,7 @@ export default function(router: Router, server) {
}
)(socket.handshake, {}, next);
}).on('connection', socket => {
clients[socket.id] = socket;
const id = getHash(
(process.env.NC_SERVER_UUID || Tele.id) +
(socket?.handshake as any)?.user?.id
@ -121,6 +127,8 @@ export default function(router: Router, server) {
Tele.event({ ...args, id });
});
});
importApis(router, clients);
}
function getHash(str) {

6
packages/nocodb/src/lib/noco/meta/api/sync/helpers/NocoSyncDestAdapter.ts

@ -0,0 +1,6 @@
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/lib/noco/meta/api/sync/helpers/NocoSyncSourceAdapter.ts

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

170
packages/nocodb/src/lib/noco/meta/api/sync/helpers/fetchAT.ts

@ -0,0 +1,170 @@
const axios = require('axios').default;
var info : any = {
initialized: false
};
async function initialize(shareId) {
info.cookie = "";
let url = `https://airtable.com/${shareId}`
try {
const hreq = await axios.get(url, {
"headers": {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"accept-language": "en-US,en;q=0.9",
"sec-ch-ua": "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"100\", \"Google Chrome\";v=\"100\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Linux\"",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36"
},
"referrerPolicy": "strict-origin-when-cross-origin",
"body": null,
"method": "GET"
})
.then(response => {
for(const ck of response.headers['set-cookie']) {
info.cookie += ck.split(';')[0] + '; '
}
return response.data
})
info.headers = JSON.parse(hreq.match(/(?<=var headers =)(.*)(?=;)/g)[0].trim())
info.link = unicodeToChar(hreq.match(/(?<=fetch\(")(.*)(?=")/g)[0].trim())
info.baseInfo = decodeURIComponent(info.link).match(/{(.*)}/g)[0].split('&').reduce((result, el) => {
try {
return Object.assign(result, JSON.parse((el.includes('=')?el.split('=')[1]:el)))
} catch(e) {
if(el.includes('=')) {
return Object.assign(result, { [el.split('=')[0]]: el.split('=')[1]})
}
}
}, {})
info.baseId = info.baseInfo.applicationId;
info.initialized = true;
} catch (e) {
console.log(e);
info.initialized = false;
}
}
async function read() {
if (info.initialized) {
const resreq = await axios("https://airtable.com" + info.link, {
"headers": {
"accept": "*/*",
"accept-language": "en-US,en;q=0.9",
"sec-ch-ua": "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"100\", \"Google Chrome\";v=\"100\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Linux\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36",
"x-time-zone": "Europe/Berlin",
"cookie": info.cookie,
...info.headers
},
"referrerPolicy": "no-referrer",
"body": null,
"method": "GET"
})
.then(response => {
return response.data
})
.catch(() => {
throw "Error while fetching"
})
return {schema: resreq.data, baseId: info.baseId, baseInfo: info.baseInfo}
} else {
throw "Please initialize first!"
}
}
async function readView(viewId) {
if (info.initialized) {
const resreq = await axios(`https://airtable.com/v0.3/view/${viewId}/readData?` + `stringifiedObjectParams=${
encodeURIComponent('{}')}&requestId=${info.baseInfo.requestId}&accessPolicy=${encodeURIComponent(JSON.stringify({allowedActions: info.baseInfo.allowedActions, shareId: info.baseInfo.shareId, applicationId: info.baseInfo.applicationId, generationNumber: info.baseInfo.generationNumber, expires: info.baseInfo.expires, signature: info.baseInfo.signature}))}`, {
"headers": {
"accept": "*/*",
"accept-language": "en-US,en;q=0.9",
"sec-ch-ua": "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"100\", \"Google Chrome\";v=\"100\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Linux\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36",
"x-time-zone": "Europe/Berlin",
"cookie": info.cookie,
...info.headers
},
"referrerPolicy": "no-referrer",
"body": null,
"method": "GET"
})
.then(response => {
return response.data
})
.catch(() => {
throw "Error while fetching"
})
return {view: resreq.data}
} else {
throw "Please initialize first!"
}
}
async function readTemplate(templateId) {
if (!info.initialized) {
await initialize('shrO8aYf3ybwSdDKn');
}
const resreq = await axios(`https://www.airtable.com/v0.3/exploreApplications/${templateId}`, {
"headers": {
"accept": "*/*",
"accept-language": "en-US,en;q=0.9",
"sec-ch-ua": "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"100\", \"Google Chrome\";v=\"100\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Linux\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36",
"x-time-zone": "Europe/Berlin",
"cookie": info.cookie,
...info.headers
},
"referrer": "https://www.airtable.com/",
"referrerPolicy": "same-origin",
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "include"
})
.then(response => {
return response.data
})
.catch(() => {
throw "Error while fetching"
})
return {template: resreq}
}
function unicodeToChar(text) {
return text.replace(/\\u[\dA-F]{4}/gi, function(match) {
return String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16));
});
}
export default {
initialize,
read,
readView,
readTemplate
}

2007
packages/nocodb/src/lib/noco/meta/api/sync/helpers/job.ts

File diff suppressed because it is too large Load Diff

31
packages/nocodb/src/lib/noco/meta/api/sync/helpers/syncMap.ts

@ -0,0 +1,31 @@
export const mapTbl = {};
// static mapping records between aTblId && ncId
export const addToMappingTbl = function addToMappingTbl(
aTblId,
ncId,
ncName,
parent?
) {
mapTbl[aTblId] = {
ncId: ncId,
ncParent: parent,
// name added to assist in quick debug
ncName: ncName
};
};
// get NcID from airtable ID
export const getNcIdFromAtId = function getNcIdFromAtId(aId) {
return mapTbl[aId]?.ncId;
};
// get nc Parent from airtable ID
export const getNcParentFromAtId = function getNcParentFromAtId(aId) {
return mapTbl[aId]?.ncParent;
};
// get nc-title from airtable ID
export const getNcNameFromAtId = function getNcNameFromAtId(aId) {
return mapTbl[aId]?.ncName;
};

79
packages/nocodb/src/lib/noco/meta/api/sync/importApis.ts

@ -0,0 +1,79 @@
import { Request, Router } from 'express';
// import { Queue } from 'bullmq';
// import axios from 'axios';
import catchError from '../../helpers/catchError';
import { Socket } from 'socket.io';
import NocoJobs from '../../../../noco-jobs/NocoJobs';
import job, { AirtableSyncConfig } from './helpers/job';
import SyncSource from '../../../../noco-models/SyncSource';
import Noco from '../../../Noco';
import * as jwt from 'jsonwebtoken';
const AIRTABLE_IMPORT_JOB = 'AIRTABLE_IMPORT_JOB';
enum SyncStatus {
PROGRESS = 'PROGRESS',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED'
}
export default (router: Router, clients: { [id: string]: Socket }) => {
NocoJobs.jobsMgr.addJobWorker(AIRTABLE_IMPORT_JOB, job);
NocoJobs.jobsMgr.addProgressCbk(AIRTABLE_IMPORT_JOB, (payload, progress) => {
clients?.[payload?.id]?.emit('progress', {
msg: progress,
status: SyncStatus.PROGRESS
});
});
NocoJobs.jobsMgr.addSuccessCbk(AIRTABLE_IMPORT_JOB, payload => {
clients?.[payload?.id]?.emit('progress', {
msg: 'completed',
status: SyncStatus.COMPLETED
});
});
NocoJobs.jobsMgr.addFailureCbk(AIRTABLE_IMPORT_JOB, (payload, error: any) => {
clients?.[payload?.id]?.emit('progress', {
msg: error?.message || 'Failed due to some internal error',
status: SyncStatus.FAILED
});
});
router.post(
'/api/v1/db/meta/import/airtable',
catchError((req, res) => {
NocoJobs.jobsMgr.add(AIRTABLE_IMPORT_JOB, {
id: req.query.id,
...req.body
});
res.json({});
})
);
router.post(
'/api/v1/db/meta/syncs/:syncId/trigger',
catchError(async (req: Request, res) => {
const syncSource = await SyncSource.get(req.params.syncId);
const user = await syncSource.getUser();
const token = jwt.sign(
{
email: user.email,
firstname: user.firstname,
lastname: user.lastname,
id: user.id,
roles: user.roles
},
Noco.getConfig().auth.jwt.secret,
Noco.getConfig().auth.jwt.options
);
NocoJobs.jobsMgr.add<AirtableSyncConfig>(AIRTABLE_IMPORT_JOB, {
id: req.query.id,
...(syncSource?.details || {}),
projectId: syncSource.project_id,
authToken: token,
baseURL: (req as any).ncSiteUrl
});
res.json({});
})
);
};

53
packages/nocodb/src/lib/noco/meta/api/sync/syncSourceApis.ts

@ -0,0 +1,53 @@
import { Request, Response, Router } from 'express';
import { Tele } from 'nc-help';
import SyncSource from '../../../../noco-models/SyncSource';
import { PagedResponseImpl } from '../../helpers/PagedResponse';
import ncMetaAclMw from '../../helpers/ncMetaAclMw';
export async function syncSourceList(req: Request, res: Response) {
// todo: pagination
res.json(new PagedResponseImpl(await SyncSource.list(req.params.projectId)));
}
export async function syncCreate(req: Request, res: Response) {
Tele.emit('evt', { evt_type: 'webhooks:created' });
const sync = await SyncSource.insert({
...req.body,
fk_user_id: (req as any).user.id,
project_id: req.params.projectId
});
res.json(sync);
}
export async function syncDelete(req: Request, res: Response<any>) {
Tele.emit('evt', { evt_type: 'webhooks:deleted' });
res.json(await SyncSource.delete(req.params.syncId));
}
export async function syncUpdate(req: Request, res: Response) {
Tele.emit('evt', { evt_type: 'webhooks:updated' });
res.json(await SyncSource.update(req.params.syncId, req.body));
}
const router = Router({ mergeParams: true });
router.get(
'/api/v1/db/meta/projects/:projectId/syncs',
ncMetaAclMw(syncSourceList, 'syncSourceList')
);
router.post(
'/api/v1/db/meta/projects/:projectId/syncs',
ncMetaAclMw(syncCreate, 'syncSourceCreate')
);
router.delete(
'/api/v1/db/meta/syncs/:syncId',
ncMetaAclMw(syncDelete, 'syncSourceDelete')
);
router.patch(
'/api/v1/db/meta/syncs/:syncId',
ncMetaAclMw(syncUpdate, 'syncSourceUpdate')
);
export default router;

1
packages/nocodb/src/lib/noco/meta/helpers/extractProps.ts

@ -1,4 +1,5 @@
export default function extractProps<T>(body: T, props: string[]): Partial<T> {
// todo: throw error if no props found
return props.reduce((o, key) => {
if (key in body) o[key] = body[key];
return o;

72
packages/nocodb/src/lib/noco/migrationsv2/nc_013_sync_source.ts

@ -0,0 +1,72 @@
import Knex from 'knex';
import { MetaTable } from '../../utils/globals';
const up = async (knex: Knex) => {
await knex.schema.createTable(MetaTable.SYNC_SOURCE, table => {
table
.string('id', 20)
.primary()
.notNullable();
table.string('title');
table.string('type');
table.text('details');
table.boolean('deleted');
table.boolean('enabled').defaultTo(true);
table.float('order');
table.string('project_id', 128);
table.foreign('project_id').references(`${MetaTable.PROJECT}.id`);
table.string('fk_user_id', 128);
table.foreign('fk_user_id').references(`${MetaTable.USERS}.id`);
table.timestamps(true, true);
});
await knex.schema.createTable(MetaTable.SYNC_LOGS, table => {
table
.string('id', 20)
.primary()
.notNullable();
table.string('project_id', 128);
table.string('fk_sync_source_id', 20);
// table
// .foreign('fk_sync_source_id')
// .references(`${MetaTable.SYNC_SOURCE}.id`);
table.integer('time_taken');
table.string('status');
table.text('status_details');
table.timestamps(true, true);
});
};
const down = async knex => {
await knex.schema.dropTable(MetaTable.SYNC_SOURCE);
};
export { up, down };
/**
* @copyright Copyright (c) 2022, Xgene Cloud Ltd
*
* @author Wing-Kam Wong <wingkwong.code@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

4
packages/nocodb/src/lib/utils/globals.ts

@ -34,7 +34,9 @@ export enum MetaTable {
PLUGIN = 'nc_plugins_v2',
PROJECT_USERS = 'nc_project_users_v2',
MODEL_ROLE_VISIBILITY = 'nc_disabled_models_for_role_v2',
API_TOKENS = 'nc_api_tokens'
API_TOKENS = 'nc_api_tokens',
SYNC_SOURCE = 'nc_sync_source_v2',
SYNC_LOGS = 'nc_sync_logs_v2'
}
export enum CacheScope {

Loading…
Cancel
Save