mirror of https://github.com/nocodb/nocodb
Browse Source
* 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
56 changed files with 3575 additions and 96 deletions
@ -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> |
@ -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); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -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)) |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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 } |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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 |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -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>; |
||||||
|
} |
@ -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>; |
||||||
|
} |
@ -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 |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -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; |
||||||
|
}; |
@ -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({}); |
||||||
|
}) |
||||||
|
); |
||||||
|
}; |
@ -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; |
@ -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/>. |
||||||
|
* |
||||||
|
*/ |
Loading…
Reference in new issue