diff --git a/markdown/readme/languages/README.md b/markdown/readme/languages/README.md
index 2fd8db92b8..b5ab39b22a 100644
--- a/markdown/readme/languages/README.md
+++ b/markdown/readme/languages/README.md
@@ -9,7 +9,7 @@ Supported Translations:
-
+
diff --git a/packages/nc-gui/components/api-client/Params.vue b/packages/nc-gui/components/api-client/Params.vue
index 816fb0fe26..b1e45e2eba 100644
--- a/packages/nc-gui/components/api-client/Params.vue
+++ b/packages/nc-gui/components/api-client/Params.vue
@@ -66,7 +66,7 @@ const deleteParamRow = (i: number) => vModel.value.splice(i, 1)
-
+
diff --git a/packages/nc-gui/components/cell/ClampedText.vue b/packages/nc-gui/components/cell/ClampedText.vue
index 515a95bed3..4a541268e2 100644
--- a/packages/nc-gui/components/cell/ClampedText.vue
+++ b/packages/nc-gui/components/cell/ClampedText.vue
@@ -3,35 +3,18 @@ const props = defineProps<{
value?: string | number | null
lines?: number
}>()
-
-const wrapper = ref()
-
-const key = ref(0)
-
-const debouncedRefresh = useDebounceFn(() => {
- key.value++
-}, 500)
-
-onMounted(() => {
- const observer = new ResizeObserver(() => {
- debouncedRefresh()
- })
-
- observer.observe(wrapper.value)
-})
-
-
-
+
+ {{ props.value || '' }}
diff --git a/packages/nc-gui/components/cell/MultiSelect.vue b/packages/nc-gui/components/cell/MultiSelect.vue
index ebef31bd75..f9a1bc59e2 100644
--- a/packages/nc-gui/components/cell/MultiSelect.vue
+++ b/packages/nc-gui/components/cell/MultiSelect.vue
@@ -313,11 +313,40 @@ const handleClose = (e: MouseEvent) => {
}
useEventListener(document, 'click', handleClose, true)
+
+const selectedOpts = computed(() => {
+ return options.value.reduce<(SelectOptionType & { index: number })[]>((selectedOptions, option) => {
+ const index = vModel.value.indexOf(option.value!)
+ if (index !== -1) {
+ selectedOptions.push({ ...option, index })
+ }
+ return selectedOptions
+ }, [])
+})
+
+
+
+
+ {{ selectedOpt.title }}
+
+
+
+
+
{
}
useEventListener(document, 'click', handleClose, true)
+
+const selectedOpt = computed(() => {
+ return options.value.find((o) => o.value === vModel.value)
+})
+
+
+
+ {{ selectedOpt.title }}
+
+
+
+
[]): void
}
-const { modelValue, rowIndex } = defineProps()
+const { modelValue } = defineProps()
const emits = defineEmits()
-const isGallery = inject(IsGalleryInj, ref(false))
-
-const isKanban = inject(IsKanbanInj, ref(false))
-
const dropZoneInjection = inject(DropZoneRef, ref())
const attachmentCellRef = ref()
const sortableRef = ref()
-const currentCellRef = ref(dropZoneInjection.value)
+const currentCellRef = inject(CurrentCellInj, dropZoneInjection.value)
-const { cellRefs, isSharedForm } = useSmartsheetStoreOrThrow()!
+const { isSharedForm } = useSmartsheetStoreOrThrow()!
const { getPossibleAttachmentSrc, openAttachment } = useAttachment()
@@ -65,32 +59,6 @@ const {
storedFiles,
} = useProvideAttachmentCell(updateModelValue)
-watch(
- [() => rowIndex, isForm, attachmentCellRef],
- () => {
- if (dropZoneInjection?.value) return
-
- if (!rowIndex && (isForm.value || isGallery.value || isKanban.value)) {
- currentCellRef.value = attachmentCellRef.value
- } else {
- nextTick(() => {
- const nextCell = cellRefs.value.reduceRight((cell, curr) => {
- if (!cell && curr.dataset.key === `${rowIndex}${column.value!.id}`) cell = curr
-
- return cell
- }, undefined as HTMLTableDataCellElement | undefined)
-
- if (!nextCell) {
- currentCellRef.value = attachmentCellRef.value
- } else {
- currentCellRef.value = nextCell
- }
- })
- }
- },
- { immediate: true, flush: 'post' },
-)
-
const { dragging } = useSortable(sortableRef, visibleItems, updateModelValue, isReadonly)
const { state: rowState } = useSmartsheetRowStoreOrThrow()
diff --git a/packages/nc-gui/components/dashboard/settings/AuditTab.vue b/packages/nc-gui/components/dashboard/settings/AuditTab.vue
index c747be082c..d90fbc5931 100644
--- a/packages/nc-gui/components/dashboard/settings/AuditTab.vue
+++ b/packages/nc-gui/components/dashboard/settings/AuditTab.vue
@@ -102,7 +102,7 @@ const columns = [
{
e.stopPropagation()
}
}
+
+// Todo: move intersection logic to a separate component or a vue directive
+const intersected = ref(false)
+
+let intersectionObserver = $ref()
+
+const elementToObserve = $ref()
+
+// load the cell only when it is in the viewport
+function initIntersectionObserver() {
+ intersectionObserver = new IntersectionObserver((entries) => {
+ entries.forEach((entry) => {
+ // if the cell is in the viewport, load the cell and disconnect the observer
+ if (entry.isIntersecting) {
+ intersected.value = true
+ intersectionObserver?.disconnect()
+ intersectionObserver = undefined
+ }
+ })
+ })
+}
+
+// observe the cell when it is mounted
+onMounted(() => {
+ initIntersectionObserver()
+ intersectionObserver?.observe(elementToObserve!)
+})
+
+// disconnect the observer when the cell is unmounted
+onUnmounted(() => {
+ intersectionObserver?.disconnect()
+})
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/nc-gui/components/smartsheet/Pagination.vue b/packages/nc-gui/components/smartsheet/Pagination.vue
index 559601f47b..89e3774cf5 100644
--- a/packages/nc-gui/components/smartsheet/Pagination.vue
+++ b/packages/nc-gui/components/smartsheet/Pagination.vue
@@ -28,10 +28,10 @@ const page = computed({
diff --git a/packages/nc-gui/components/smartsheet/TableDataCell.vue b/packages/nc-gui/components/smartsheet/TableDataCell.vue
index 932c61519b..b40604fd6b 100644
--- a/packages/nc-gui/components/smartsheet/TableDataCell.vue
+++ b/packages/nc-gui/components/smartsheet/TableDataCell.vue
@@ -1,7 +1,5 @@
diff --git a/packages/nc-gui/components/smartsheet/VirtualCell.vue b/packages/nc-gui/components/smartsheet/VirtualCell.vue
index 48322fb175..26d820b37c 100644
--- a/packages/nc-gui/components/smartsheet/VirtualCell.vue
+++ b/packages/nc-gui/components/smartsheet/VirtualCell.vue
@@ -52,23 +52,58 @@ function onNavigate(dir: NavigateDir, e: KeyboardEvent) {
if (!isForm.value) e.stopImmediatePropagation()
}
+
+// Todo: move intersection logic to a separate component or a vue directive
+const intersected = ref(false)
+
+let intersectionObserver = $ref()
+
+const elementToObserve = $ref()
+
+// load the cell only when it is in the viewport
+function initIntersectionObserver() {
+ intersectionObserver = new IntersectionObserver((entries) => {
+ entries.forEach((entry) => {
+ // if the cell is in the viewport, load the cell and disconnect the observer
+ if (entry.isIntersecting) {
+ intersected.value = true
+ intersectionObserver?.disconnect()
+ intersectionObserver = undefined
+ }
+ })
+ })
+}
+
+// observe the cell when it is mounted
+onMounted(() => {
+ initIntersectionObserver()
+ intersectionObserver?.observe(elementToObserve!)
+})
+
+// disconnect the observer when the cell is unmounted
+onUnmounted(() => {
+ intersectionObserver?.disconnect()
+})
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/nc-gui/components/smartsheet/expanded-form/index.vue b/packages/nc-gui/components/smartsheet/expanded-form/index.vue
index 6b9357b7a7..0c1e5285c7 100644
--- a/packages/nc-gui/components/smartsheet/expanded-form/index.vue
+++ b/packages/nc-gui/components/smartsheet/expanded-form/index.vue
@@ -11,7 +11,6 @@ import {
ReloadRowDataHookInj,
computedInject,
createEventHook,
- iconMap,
inject,
message,
provide,
diff --git a/packages/nc-gui/components/smartsheet/toolbar/AddRow.vue b/packages/nc-gui/components/smartsheet/toolbar/AddRow.vue
index 36896d4289..b8463e7118 100644
--- a/packages/nc-gui/components/smartsheet/toolbar/AddRow.vue
+++ b/packages/nc-gui/components/smartsheet/toolbar/AddRow.vue
@@ -13,7 +13,6 @@ const onClick = () => {
{{ $t('activity.addRow') }}
-
{
+import type { HookLogType, HookType } from 'nocodb-sdk'
+import { AutomationLogLevel, extractSdkResponseErrorMsg, onBeforeMount, parseProp, timeAgo, useApi, useGlobal } from '#imports'
+
+interface Props {
+ hook: HookType
+}
+
+const props = defineProps()
+
+const { api, isLoading } = useApi()
+
+const hookLogs = ref([])
+
+const activeKey = ref()
+
+const { appInfo } = useGlobal()
+
+let totalRows = $ref(0)
+
+const currentPage = $ref(1)
+
+const currentLimit = $ref(10)
+
+const showLogs = computed(
+ () =>
+ !(
+ appInfo.value.automationLogLevel === AutomationLogLevel.OFF ||
+ (appInfo.value.automationLogLevel === AutomationLogLevel.ALL && !appInfo.value.ee)
+ ),
+)
+
+async function loadHookLogs(page = currentPage, limit = currentLimit) {
+ try {
+ // cater empty records
+ page = page || 1
+ const { list, pageInfo } = await api.dbTableWebhookLogs.list(props.hook.id!, {
+ offset: limit * (page - 1),
+ limit,
+ })
+ hookLogs.value = parseHookLog(list)
+ totalRows = pageInfo.totalRows ?? 0
+ } catch (e: any) {
+ message.error(await extractSdkResponseErrorMsg(e))
+ }
+}
+
+function parseHookLog(hookLogs: any) {
+ for (const hookLog of hookLogs) {
+ if (hookLog?.response) {
+ hookLog.response = parseProp(hookLog.response)
+ }
+ if (hookLog?.response?.config?.data) {
+ hookLog.response.config.data = parseProp(hookLog.response.config.data)
+ }
+ if (hookLog?.payload) {
+ hookLog.payload = parseProp(hookLog.payload)
+ }
+ if (hookLog?.notification) {
+ hookLog.notification = parseProp(hookLog.notification)
+ }
+ }
+ return hookLogs
+}
+
+onBeforeMount(async () => {
+ if (showLogs.value) {
+ await loadHookLogs(currentPage, currentLimit)
+ }
+})
+
+
+
+
+
+
+
+ The NC_AUTOMATION_LOG_LEVEL is set to “OFF”, no logs will be displayed.
+
+
+ The NC_AUTOMATION_LOG_LEVEL is set to “ERROR”, only error logs will be displayed.
+
+
+
+ The NC_AUTOMATION_LOG_LEVEL is set to “ALL”, both error and success logs will be displayed.
+
+ Upgrade to Enterprise Edition to show all the logs.
+
+
+ For additional configuration options, please refer the documentation
+ here.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ hookLog.type }}: records.{{ hookLog.event }}.{{ hookLog.operation }} ({{ timeAgo(hookLog.created_at) }})
+
+
+
+
+ {{ hookLog.response?.status }}
+ {{ hookLog.response?.statusText || (hookLog.response?.status === 200 ? 'OK' : 'ERROR') }}
+
+
+
+
+ {{ hookLog.payload.method }}
+
+ {{ hookLog.payload.path }}
+
+
+
+
+
+ {{ hookLog.error_message }}
+
+
+
+
+ Request
+ {{ hookLog.response.config.headers }}
+
+
+
+ Response
+ {{ hookLog.response.headers }}
+
+
+
+ Payload
+ {{ hookLog.response.config.data }}
+
+
+
+
+ Payload
+ {{ hookLog.payload }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/nc-gui/components/webhook/ChannelMultiSelect.vue b/packages/nc-gui/components/webhook/ChannelMultiSelect.vue
index d9c07bc594..d90e3c9df7 100644
--- a/packages/nc-gui/components/webhook/ChannelMultiSelect.vue
+++ b/packages/nc-gui/components/webhook/ChannelMultiSelect.vue
@@ -1,5 +1,5 @@
diff --git a/packages/nc-gui/components/webhook/Editor.vue b/packages/nc-gui/components/webhook/Editor.vue
index d7719a50a7..fe652c5dda 100644
--- a/packages/nc-gui/components/webhook/Editor.vue
+++ b/packages/nc-gui/components/webhook/Editor.vue
@@ -39,16 +39,18 @@ const { appInfo } = $(useGlobal())
const meta = inject(MetaInj, ref())
+const hookTabKey = ref('hook-edit')
+
const useForm = Form.useForm
const hook = reactive<
- Omit & { notification: Record; eventOperation: string; condition: boolean }
+ Omit & { notification: Record; eventOperation?: string; condition: boolean }
>({
id: '',
title: '',
event: undefined,
operation: undefined,
- eventOperation: '',
+ eventOperation: undefined,
notification: {
type: 'URL',
payload: {
@@ -56,13 +58,15 @@ const hook = reactive<
body: '{{ json data }}',
headers: [{}],
parameters: [{}],
+ path: '',
},
},
condition: false,
active: true,
+ version: 'v2',
})
-const urlTabKey = ref('body')
+const urlTabKey = ref('params')
const apps: Record = ref()
@@ -172,11 +176,14 @@ const formInput = ref({
],
})
-const eventList = [
+const eventList = ref[]>([
{ text: ['After', 'Insert'], value: ['after', 'insert'] },
{ text: ['After', 'Update'], value: ['after', 'update'] },
{ text: ['After', 'Delete'], value: ['after', 'delete'] },
-]
+ { text: ['After', 'Bulk Insert'], value: ['after', 'bulkInsert'] },
+ { text: ['After', 'Bulk Update'], value: ['after', 'bulkUpdate'] },
+ { text: ['After', 'Bulk Delete'], value: ['after', 'bulkDelete'] },
+])
const notificationList = computed(() => {
return appInfo.isCloud
@@ -216,10 +223,7 @@ const validators = computed(() => {
'notification.payload.subject': [fieldRequiredValidator()],
'notification.payload.body': [fieldRequiredValidator()],
}),
- ...((hook.notification.type === 'Slack' ||
- hook.notification.type === 'Microsoft Teams' ||
- hook.notification.type === 'Discord' ||
- hook.notification.type === 'Mattermost') && {
+ ...(['Slack', 'Microsoft Teams', 'Discord', 'Mattermost'].includes(hook.notification.type) && {
'notification.payload.channels': [fieldRequiredValidator()],
'notification.payload.body': [fieldRequiredValidator()],
}),
@@ -231,9 +235,13 @@ const validators = computed(() => {
})
const { validate, validateInfos } = useForm(hook, validators)
-function onNotTypeChange(reset = false) {
+function onNotificationTypeChange(reset = false) {
if (reset) {
hook.notification.payload = {} as Record
+ if (['Slack', 'Microsoft Teams', 'Discord', 'Mattermost'].includes(hook.notification.type)) {
+ hook.notification.payload.channels = []
+ hook.notification.payload.body = ''
+ }
}
if (hook.notification.type === 'Slack') {
@@ -270,9 +278,17 @@ function setHook(newHook: HookType) {
payload: notification.payload,
},
})
+ if (hook.version === 'v1') {
+ urlTabKey.value = 'body'
+ eventList.value = [
+ { text: ['After', 'Insert'], value: ['after', 'insert'] },
+ { text: ['After', 'Update'], value: ['after', 'update'] },
+ { text: ['After', 'Delete'], value: ['after', 'delete'] },
+ ]
+ }
}
-async function onEventChange() {
+function onEventChange() {
const { notification: { payload = {}, type = {} } = {} } = hook
Object.assign(hook, {
@@ -285,20 +301,20 @@ async function onEventChange() {
hook.notification.payload = payload
- let channels: Ref[] | null> = ref(null)
+ const channels: Ref[] | null> = ref(null)
switch (hook.notification.type) {
case 'Slack':
- channels = slackChannels
+ channels.value = slackChannels.value
break
case 'Microsoft Teams':
- channels = teamsChannels
+ channels.value = teamsChannels.value
break
case 'Discord':
- channels = discordChannels
+ channels.value = discordChannels.value
break
case 'Mattermost':
- channels = mattermostChannels
+ channels.value = mattermostChannels.value
break
}
@@ -429,7 +445,7 @@ onMounted(async () => {
hook.eventOperation = `${hook.event} ${hook.operation}`
}
- onNotTypeChange()
+ onNotificationTypeChange()
})
@@ -462,271 +478,288 @@ onMounted(async () => {
-
-
-
-
-
-
- {{ $t('activity.enableWebhook') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ event.text.join(' ') }}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ Edit
+
+
+
+
+
+
+
+
+ {{ $t('activity.enableWebhook') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ event.text.join(' ') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ notificationOption.type }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ method.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ On Condition
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ Use context variable data to refer the record under consideration
+
+
+
+ data : Row data
+
+
+
+
- {{ notificationOption.type }}
+
-
-
-
-
-
-
-
-
-
-
- {{ method.title }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- On Condition
-
-
-
-
-
-
-
-
-
-
-
- Use context variable data to refer the record under consideration
-
-
-
- data : Row data
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Call Log
+
+
+
+
+
diff --git a/packages/nc-gui/components/webhook/List.vue b/packages/nc-gui/components/webhook/List.vue
index f1063e062d..59b2f3f63f 100644
--- a/packages/nc-gui/components/webhook/List.vue
+++ b/packages/nc-gui/components/webhook/List.vue
@@ -125,17 +125,18 @@ onMounted(() => {
- {{ item.event }} {{ item.operation }}
+ {{ item.event }} {{ item.operation.replace(/[A-Z]/g, ' $&') }}
-
+
+ ({{ item.version }})
{{ item.title }}
-
+
-
+
@@ -150,7 +151,7 @@ onMounted(() => {
{{ $t('labels.notifyVia') }} : {{ item?.notification?.type }}
-
+
{{ $t('activity.copyWebhook') }}
diff --git a/packages/nc-gui/components/webhook/Test.vue b/packages/nc-gui/components/webhook/Test.vue
index 2f7ad98cd9..88cc1c20ce 100644
--- a/packages/nc-gui/components/webhook/Test.vue
+++ b/packages/nc-gui/components/webhook/Test.vue
@@ -14,10 +14,7 @@ const { $api } = useNuxtApp()
const meta = inject(MetaInj, ref())
-const sampleData = ref({
- data: {},
-})
-const activeKey = ref(0)
+const sampleData = ref()
watch(
() => hook?.operation,
@@ -27,9 +24,11 @@ watch(
)
async function loadSampleData() {
- sampleData.value = {
- data: await $api.dbTableWebhook.samplePayloadGet(meta?.value?.id as string, hook?.operation || 'insert'),
- }
+ sampleData.value = await $api.dbTableWebhook.samplePayloadGet(
+ meta?.value?.id as string,
+ hook?.operation || 'insert',
+ hook.version!,
+ )
}
async function testWebhook() {
@@ -59,9 +58,6 @@ onMounted(async () => {
-
-
-
-
-
+ Sample Payload
+
diff --git a/packages/nc-gui/composables/useGlobal/state.ts b/packages/nc-gui/composables/useGlobal/state.ts
index c04597652a..0567148619 100644
--- a/packages/nc-gui/composables/useGlobal/state.ts
+++ b/packages/nc-gui/composables/useGlobal/state.ts
@@ -101,6 +101,7 @@ export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {
ncAttachmentFieldSize: 20,
ncMaxAttachmentsAllowed: 10,
isCloud: false,
+ automationLogLevel: 'OFF',
})
/** reactive token payload */
diff --git a/packages/nc-gui/composables/useGlobal/types.ts b/packages/nc-gui/composables/useGlobal/types.ts
index adf615070c..5ddc0aec34 100644
--- a/packages/nc-gui/composables/useGlobal/types.ts
+++ b/packages/nc-gui/composables/useGlobal/types.ts
@@ -23,6 +23,7 @@ export interface AppInfo {
ncAttachmentFieldSize: number
ncMaxAttachmentsAllowed: number
isCloud: boolean
+ automationLogLevel: 'OFF' | 'ERROR' | 'ALL'
}
export interface StoredState {
diff --git a/packages/nc-gui/composables/useSmartsheetStore.ts b/packages/nc-gui/composables/useSmartsheetStore.ts
index c2340e9ae4..9e74aab11f 100644
--- a/packages/nc-gui/composables/useSmartsheetStore.ts
+++ b/packages/nc-gui/composables/useSmartsheetStore.ts
@@ -32,8 +32,6 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
(meta.value as TableType)?.base_id ? sqlUis.value[(meta.value as TableType).base_id!] : Object.values(sqlUis.value)[0],
)
- const cellRefs = ref([])
-
const { search } = useFieldQuery()
const eventBus = useEventBus(Symbol('SmartsheetStore'))
@@ -78,7 +76,6 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
isGallery,
isKanban,
isMap,
- cellRefs,
isSharedForm,
sorts,
nestedFilters,
diff --git a/packages/nc-gui/composables/useViewData.ts b/packages/nc-gui/composables/useViewData.ts
index e3841dc7ee..cddb3e50dd 100644
--- a/packages/nc-gui/composables/useViewData.ts
+++ b/packages/nc-gui/composables/useViewData.ts
@@ -213,7 +213,7 @@ export function useViewData(
}
if (viewMeta.value?.type === ViewTypes.GRID) {
- await loadAggCommentsCount()
+ loadAggCommentsCount()
}
}
diff --git a/packages/nc-gui/context/index.ts b/packages/nc-gui/context/index.ts
index cb9c99f15c..ff359ea77f 100644
--- a/packages/nc-gui/context/index.ts
+++ b/packages/nc-gui/context/index.ts
@@ -36,3 +36,4 @@ export const DropZoneRef: InjectionKey[> = Symbol('drop-
export const ToggleDialogInj: InjectionKey = Symbol('toggle-dialog-injection')
export const CellClickHookInj: InjectionKey | undefined> = Symbol('cell-click-injection')
export const SaveRowInj: InjectionKey<(() => void) | undefined> = Symbol('save-row-injection')
+export const CurrentCellInj: InjectionKey][> = Symbol('current-cell-injection')
diff --git a/packages/nc-gui/lang/fr.json b/packages/nc-gui/lang/fr.json
index 2d4928614d..4cce07a0d9 100644
--- a/packages/nc-gui/lang/fr.json
+++ b/packages/nc-gui/lang/fr.json
@@ -101,7 +101,7 @@
"form": "Formulaire",
"kanban": "Kanban",
"calendar": "Calendrier",
- "map": "Map"
+ "map": "Carte"
},
"user": "Utilisateur",
"users": "Utilisateurs",
@@ -210,7 +210,7 @@
"advancedSettings": "Paramètres avancés",
"codeSnippet": "Extrait de code",
"keyboardShortcut": "Raccourcis clavier",
- "generateRandomName": "Generate Random Name",
+ "generateRandomName": "Générer un nom aléatoire",
"findRowByScanningCode": "Find row by scanning a QR or Barcode"
},
"labels": {
@@ -221,7 +221,7 @@
"viewName": "Vue",
"viewLink": "Lien de vue",
"columnName": "Nom de la colonne",
- "columnToScanFor": "Column to scan",
+ "columnToScanFor": "Colonne à scanner",
"columnType": "Type de colonne",
"roleName": "Nom de rôle",
"roleDescription": "Description du rôle",
@@ -396,7 +396,7 @@
"saveAndExit": "Enregistrer et quitter",
"saveAndStay": "Enregistrer et rester",
"insertRow": "Insérer une nouvelle ligne",
- "duplicateRow": "Duplicate Row",
+ "duplicateRow": "Dupliquer la ligne",
"deleteRow": "Supprimer la ligne",
"deleteSelectedRow": "Supprimer les lignes sélectionnées",
"importExcel": "Importer depuis Excel",
@@ -548,7 +548,7 @@
"noRowFoundForCode": "No row found for this code for the selected column"
},
"map": {
- "overLimit": "You're over the limit.",
+ "overLimit": "Vous avez dépassé la limite.",
"closeLimit": "You're getting close to the limit.",
"limitNumber": "The limit of markers shown in a Map View is 1000 records."
},
@@ -634,7 +634,7 @@
"gallery": "Ajouter une vue Galerie",
"form": "Ajouter une vue Formulaire",
"kanban": "Ajouter une vue Kanban",
- "map": "Add Map View",
+ "map": "Ajouter la vue Carte",
"calendar": "Ajouter une vue Calendrier"
},
"tablesMetadataInSync": "Les métadonnées de tables sont en synchronisation",
@@ -698,7 +698,7 @@
"allowedSpecialCharList": "Liste des caractères spéciaux autorisés"
},
"invalidURL": "URL invalide",
- "invalidEmail": "Invalid Email",
+ "invalidEmail": "Email invalide",
"internalError": "Une erreur interne est survenue",
"templateGeneratorNotFound": "Le générateur de modèles est introuvable !",
"fileUploadFailed": "Échec du téléversement du fichier",
@@ -779,7 +779,7 @@
"userDeletedFromProject": "Suppression réussie de l'utilisateur du projet",
"inviteEmailSent": "Email d'invitation envoyé avec succès",
"inviteURLCopied": "URL de l'invitation copiée dans le presse-papiers",
- "commentCopied": "Comment copied to clipboard",
+ "commentCopied": "Commentaire copié dans le presse-papier",
"passwordResetURLCopied": "URL de réinitialisation du mot de passe copiée dans le presse-papiers",
"shareableURLCopied": "Copie de l'URL de la base partageable dans le presse-papiers !",
"embeddableHTMLCodeCopied": "Copie du code HTML intégrable !",
diff --git a/packages/nc-gui/lang/pt_BR.json b/packages/nc-gui/lang/pt_BR.json
index fa649565d8..5e9ee29b93 100644
--- a/packages/nc-gui/lang/pt_BR.json
+++ b/packages/nc-gui/lang/pt_BR.json
@@ -101,7 +101,7 @@
"form": "Formulário",
"kanban": "Kanban",
"calendar": "Calendário",
- "map": "Map"
+ "map": "Mapa"
},
"user": "Do utilizador",
"users": "Comercial",
@@ -210,7 +210,7 @@
"advancedSettings": "Configurações avançadas",
"codeSnippet": "Código Snippet",
"keyboardShortcut": "Atalhos de teclado",
- "generateRandomName": "Generate Random Name",
+ "generateRandomName": "Gerar um código aleatório",
"findRowByScanningCode": "Find row by scanning a QR or Barcode"
},
"labels": {
@@ -472,7 +472,7 @@
"map": {
"mappedBy": "Mapped By",
"chooseMappingField": "Choose a Mapping Field",
- "openInGoogleMaps": "Google Maps",
+ "openInGoogleMaps": "Google Mapas",
"openInOpenStreetMap": "OSM"
},
"toggleMobileMode": "Toggle Mobile Mode"
diff --git a/packages/nc-gui/lang/uk.json b/packages/nc-gui/lang/uk.json
index 9ec647c0ba..5c73d3af62 100644
--- a/packages/nc-gui/lang/uk.json
+++ b/packages/nc-gui/lang/uk.json
@@ -22,12 +22,12 @@
"update": "Оновити",
"rename": "Перейменувати",
"reload": "Перезавантажити",
- "reset": "Скинути",
+ "reset": "Відновити",
"install": "Встановити",
"show": "Показати",
- "hide": "Приховати",
+ "hide": "Сховати",
"showAll": "Показати все",
- "hideAll": "Приховати все",
+ "hideAll": "Сховати все",
"showMore": "Показати більше",
"showOptions": "Показати опції",
"hideOptions": "Сховати опції",
@@ -52,7 +52,7 @@
"event": "Подія",
"condition": "Стан",
"after": "Після",
- "before": "Раніше",
+ "before": "До",
"search": "Пошук",
"notification": "Сповіщення",
"reference": "Посилання",
@@ -133,7 +133,7 @@
"Time": "Час",
"PhoneNumber": "Номер телефону",
"Email": "Пошта",
- "URL": "URL",
+ "URL": "URL-посилання",
"Number": "Число",
"Decimal": "Дробове",
"Currency": "Валюта",
@@ -148,7 +148,7 @@
"DateTime": "Дата і час",
"CreateTime": "Час створення",
"LastModifiedTime": "Час останньої зміни",
- "AutoNumber": "Автоматичне число",
+ "AutoNumber": "Автоматичне прирощування",
"Barcode": "Штрих-код",
"Button": "Кнопка",
"Password": "Пароль",
@@ -182,10 +182,10 @@
"teamAndAuth": "Команда та автор",
"rolesUserMgmt": "Ролі та управління користувачами",
"userMgmt": "Керування користувачами",
- "apiTokenMgmt": "Управління токенами API",
+ "apiTokenMgmt": "Керування токенами API",
"rolesMgmt": "Керування ролями",
"projMeta": "Метадані проєкту",
- "metaMgmt": "Метаменеджмент",
+ "metaMgmt": "Керування метаданими",
"metadata": "Метадані",
"exportImportMeta": "Експорт/Імпорт метаданих",
"uiACL": "Контроль доступу до інтерфейсу",
@@ -240,7 +240,7 @@
"operation": "Операція",
"operationSub": "Субоперація",
"operationType": "Тип операції",
- "operationSubType": "Підтип операції",
+ "operationSubType": "Тип субоперації",
"description": "Опис",
"authentication": "Автентифікація",
"token": "Токен",
@@ -272,11 +272,11 @@
"requriedIdentity": "Потрібна ідентифікація",
"inflection": {
"tableName": "Перехрестя - Назва таблиці",
- "columnName": "Перехрестя - Назва стовпця"
+ "columnName": "Перехрестя - назва стовпця"
},
"community": {
"starUs1": "Оцінити",
- "starUs2": "ми на GitHub",
+ "starUs2": "нас на GitHub",
"bookDemo": "Забронюйте безкоштовну демонстрацію",
"getAnswered": "Отримайте відповіді на запитання",
"joinDiscord": "Ми є в Discord",
@@ -338,30 +338,30 @@
"deleteProject": "Видалити проєкт",
"refreshProject": "Оновити проєкти",
"saveProject": "Зберегти проєкт",
- "deleteKanbanStack": "Видалити стек?",
+ "deleteKanbanStack": "Видалити табличку?",
"createProjectExtended": {
- "extDB": "Створити підключення ] до зовнішньої бази даних",
+ "extDB": "Створити підключення до зовнішньої бази даних",
"excel": "Створити проєкт з Excel",
"template": "Створити проєкт з шаблону"
},
- "OkSaveProject": "Підтвердити & Зберегти",
+ "OkSaveProject": "Зберегти",
"upgrade": {
"available": "Доступне оновлення",
"releaseNote": "Список змін",
- "howTo": "Як оновитися?"
+ "howTo": "Як оновитись?"
},
- "translate": "Допоможіть з перекладом",
+ "translate": "Допомогти з перекладом",
"account": {
- "authToken": "Копіювати токен авторизації",
+ "authToken": "Скопіювати токен",
"swagger": "Swagger: REST API",
- "projInfo": "Скопіювати інформацію про проект",
+ "projInfo": "Скопіювати інформацію про проєкт",
"themes": "Теми"
},
"sort": "Сортувати",
"addSort": "Додати параметри сортування",
"filter": "Фільтр",
"addFilter": "Додати фільтр",
- "share": "Поділитися",
+ "share": "Поділитись",
"shareBase": {
"disable": "Вимкнути спільну базу",
"enable": "Будь-хто, хто має посилання",
@@ -374,10 +374,10 @@
"inviteToken": "Токен запрошення",
"newUser": "Новий користувач",
"editUser": "Редагувати користувача",
- "deleteUser": "Видалити користувача з проєкту",
- "resendInvite": "Повторно надіслати запрошення на пошту",
- "copyInviteURL": "Скопіювати URL-адресу запрошення",
- "copyPasswordResetURL": "Скопіювати URL-адресу для оновлення паролю",
+ "deleteUser": "Видалити користувача",
+ "resendInvite": "Повторно надіслати запрошення",
+ "copyInviteURL": "Скопіювати URL-посилання для запрошення",
+ "copyPasswordResetURL": "Скопіювати URL-посилання для відновлення паролю",
"newRole": "Нова роль",
"reloadRoles": "Перезавантажити ролі",
"nextPage": "Наступна сторінка",
@@ -390,7 +390,7 @@
"renameTable": "Перейменувати таблицю",
"deleteTable": "Видалити таблицю",
"addField": "Додати нове поле до цієї таблиці",
- "setDisplay": "Встановити як значення для відображення",
+ "setDisplay": "Встановити як значення для показу",
"addRow": "Додати новий рядок",
"saveRow": "Зберегти рядок",
"saveAndExit": "Зберегти та вийти",
@@ -416,20 +416,20 @@
"fillByCodeScan": "Заповнити за допомогою сканування",
"listSharedView": "Список спільних виглядів",
"ListView": "Список виглядів",
- "copyView": "Копіювати вигляд",
+ "copyView": "Скопіювати вигляд",
"renameView": "Перейменувати вигляд",
"deleteView": "Видалити вигляд",
"createGrid": "Створіть вигляд сітки",
"createGallery": "Створити вигляд галереї",
"createCalendar": "Створити вигляд календаря",
- "createKanban": "Створити Kanban вигляд",
+ "createKanban": "Створити табличний вигляд",
"createForm": "Створити вигляд форми",
"showSystemFields": "Показати системні поля",
"copyUrl": "Копіювати URL-адресу",
"openTab": "Відкрийте нову вкладку",
"iFrame": "Копіювати вбудований HTML-код",
- "addWebhook": "Додати новий Webhook",
- "enableWebhook": "Увімкнути вебхук",
+ "addWebhook": "Додати новий вебхук",
+ "enableWebhook": "Ввімкнути вебхук",
"testWebhook": "Перевірити вебхук",
"copyWebhook": "Скопіювати вебхук",
"deleteWebhook": "Видалити вебхук",
@@ -439,8 +439,8 @@
"metaSync": "Синхронізувати",
"settings": "Налаштування",
"previewAs": "Попередній перегляд як",
- "resetReview": "Скинути попередній перегляд",
- "testDbConn": "Тестове з'єднання з базою даних",
+ "resetReview": "Відновити попередній перегляд",
+ "testDbConn": "Тестове зʼєднання з базою даних",
"removeDbFromEnv": "Видалити базу даних з середовища",
"editConnJson": "Редагувати підключення JSON",
"sponsorUs": "Спонсорувати нас",
@@ -460,38 +460,38 @@
"showPkAndFk": "Показати первинні та зовнішні ключі",
"showSqlViews": "Показати SQL вигляд",
"showMMTables": "Показати таблиці \"багато-до-багатьох\"",
- "showJunctionTableNames": "Показати ім'я таблиці для з'єднання"
+ "showJunctionTableNames": "Показати імʼя таблиці для зʼєднання"
},
"kanban": {
"collapseStack": "Згорнути стек",
"deleteStack": "Видалити стек",
"stackedBy": "Групувати по",
"chooseGroupingField": "Виберіть поле групування",
- "addOrEditStack": "Додати / Редагувати стек"
+ "addOrEditStack": "Додати/Редагувати табличку"
},
"map": {
- "mappedBy": "Mapped By",
+ "mappedBy": "Мапувати по",
"chooseMappingField": "Виберіть поле для мапування",
"openInGoogleMaps": "Google Maps",
- "openInOpenStreetMap": "OSM"
+ "openInOpenStreetMap": "Відкрити в Google Maps"
},
- "toggleMobileMode": "Увімкнути мобільний режим"
+ "toggleMobileMode": "Ввімкнути мобільний режим"
},
"tooltip": {
"saveChanges": "Зберегти зміни",
"xcDB": "Створити новий проєкт",
"extDB": "Підтримує MySQL, PostgreSQL, SQL Server та SQLite",
"apiRest": "Доступно через REST API",
- "apiGQL": "Доступно через API GraphQL",
+ "apiGQL": "Доступно через GraphQL API",
"theme": {
"dark": "Він доступний у чорному кольорі (^⇧B)",
"light": "Чи є він у чорному кольорі? (^⇧B)"
},
"addTable": "Додати нову таблицю",
"inviteMore": "Запросіть більше користувачів",
- "toggleNavDraw": "Ввімвкнути висувне меню",
+ "toggleNavDraw": "Ввімкнути висувне меню",
"reloadApiToken": "Перезавантажити API токен",
- "generateNewApiToken": "Створити новий API токен",
+ "generateNewApiToken": "Згенерувати API токен",
"addRole": "Додати нову роль",
"reloadList": "Перезавантажити список",
"metaSync": "Синхронізувати метаданні",
@@ -538,13 +538,13 @@
"info": {
"pasteNotSupported": "Операція вставки не підтримується в активній комірці",
"roles": {
- "orgCreator": "Розробник може створювати нові проєкти та мати доступ до будь-якого відкритого проєкту.",
- "orgViewer": "Глядач не може створювати нові проєкти, але він може отримати доступ до будь-якого відкритого проєкту."
+ "orgCreator": "Розробник може створювати нові проєкти та мати доступ до будь-якого публічного проєкту.",
+ "orgViewer": "Глядач не може створювати нові проєкти, але він може отримати доступ до будь-якого публічного проєкту."
},
"codeScanner": {
"loadingScanner": "Завантаження сканера...",
"selectColumn": "Виберіть стовпець, QR-код або штрих-код, який ви хочете використовувати для пошуку рядка за допомогою сканування.",
- "moreThanOneRowFoundForCode": "Для цього коду знайдено більше одного рядка. Наразі підтримуються лише унікальні коди.",
+ "moreThanOneRowFoundForCode": "Для цього коду знайдено декілька рядків. Наразі підтримуються лише унікальні коди",
"noRowFoundForCode": "Для цього коду не знайдено жодного рядка у вибраному стовпчику"
},
"map": {
@@ -558,19 +558,19 @@
"excelSupport": "Підтримуються: .xls, .xlsx, .xlsm, .ods, .ots",
"excelURL": "Введіть посилання до файлу Excel",
"csvURL": "Введіть посилання до файлу CSV",
- "footMsg": "# рядків, які потрібно проаналізувати, щоб вивести тип даних",
+ "footMsg": "К-сть рядків для аналізу, щоб визначити тип даних",
"excelImport": "аркуш(і) які доступні для імпорту",
"exportMetadata": "Ви хочете експортувати метадані з мета-таблиць?",
"importMetadata": "Ви хочете імпортувати метадані з мета-таблиць?",
"clearMetadata": "Ви хочете очистити метадані з мета-таблиць?",
- "projectEmptyMessage": "Почніть зі створення нового проекту",
+ "projectEmptyMessage": "Почніть зі створення нового проєкту",
"stopProject": "Ви хочете зупинити проєкт?",
"startProject": "Ви хочете розпочати проєкт?",
"restartProject": "Ви хочете перезапустити проєкт?",
"deleteProject": "Ви хочете видалити проєкт?",
"shareBasePrivate": "Створіть загальнодоступну базу, доступну лише для читання",
"shareBasePublic": "Будь-хто в Інтернеті може переглядати за допомогою цього посилання",
- "userInviteNoSMTP": "Схоже, ви ще не налаштували SMTP! Будь ласка, скопіюйте посилання на запрошення та надішліть його на адресу",
+ "userInviteNoSMTP": "Схоже, ви ще не налаштували SMTP! Будь ласка, скопіюйте посилання на запрошення та надішліть його на пошту.",
"dragDropHide": "Перетягніть поля сюди, щоб приховати",
"formInput": "Введіть назву форми для введення",
"formHelpText": "Додайте допоміжний текст",
@@ -578,7 +578,7 @@
"formDesc": "Додати опис форми",
"beforeEnablePwd": "Обмежити доступ за допомогою пароля",
"afterEnablePwd": "Доступ обмежено паролем",
- "privateLink": "Цим переглядом поділилися через приватне посилання",
+ "privateLink": "Цей перегляд доступний за приватним посиланням",
"privateLinkAdditionalInfo": "Люди з приватним посиланням можуть бачити лише клітини, видимі в цьому вигляді",
"afterFormSubmitted": "Після надсилання форми",
"apiOptions": "Доступ до проєкту через",
@@ -593,9 +593,9 @@
"collabView": "Учасники з дозволом на редагування або вище можуть змінювати конфігурацію перегляду.",
"lockedView": "Ніхто не може відредагувати конфігурацію перегляду, доки вона не розблокована.",
"personalView": "Тільки ви можете відредагувати конфігурацію. Іншим учасникам прихована зміна переглядів за замовчуванням.",
- "ownerDesc": "Може додавати/видаляти редакторів. Також може повністю редагувати структури та поля бази даних.",
+ "ownerDesc": "Може додавати або видаляти редакторів. Також може повністю редагувати структури та поля бази даних.",
"creatorDesc": "Може повністю редагувати структуру бази даних та значення.",
- "editorDesc": "Може редагувати записи, але не може змінити структуру бази даних/полів.",
+ "editorDesc": "Може редагувати записи, але не може змінити структуру бази даних або полів.",
"commenterDesc": "Може переглядати та коментувати записи, але нічого не може редагувати",
"viewerDesc": "Може переглядати записи, але нічого не може редагувати",
"addUser": "Додати нового користувача",
@@ -606,14 +606,14 @@
"metaNoChange": "Не виявлено жодних змін",
"sqlMigration": "Схема міграції буде створена автоматично. Створіть таблицю та оновіть цю сторінку.",
"dbConnectionStatus": "Середовище перевірено",
- "dbConnected": "З'єднання успішне",
+ "dbConnected": "Зʼєднання успішне",
"notifications": {
"no_new": "Немає нових повідомлень",
"clear": "Очистити"
},
"sponsor": {
"header": "Ви можете допомогти нам!",
- "message": "Ми - невелика команда, яка працює на повну ставку, щоб зробити NocoDB відкритим. Ми віримо, що такий інструмент, як NocoDB, повинен бути доступним безкоштовно для кожного розв'язувача проблем в Інтернеті."
+ "message": "Ми - невелика команда, яка працює на повну ставку, щоб зробити NocoDB відкритим. Ми віримо, що такий інструмент, як NocoDB, повинен бути доступним безкоштовно для кожного розвʼязувача проблем в Інтернеті."
},
"loginMsg": "Увійти до NocoDB",
"passwordRecovery": {
@@ -623,22 +623,22 @@
},
"signUp": {
"superAdmin": "Ви будете \"Власником\"",
- "alreadyHaveAccount": "Вже є обліковий запис?",
+ "alreadyHaveAccount": "Вже зареєстровані?",
"workEmail": "Введіть робочу адресу електронної пошти",
"enterPassword": "Введіть ваш пароль",
- "forgotPassword": "Забули власний пароль?",
+ "forgotPassword": "Забули пароль?",
"dontHaveAccount": "Не маєте облікового запису?"
},
"addView": {
"grid": "Додати вигляд сітки",
"gallery": "Додати вигляд галереї",
"form": "Додати вигляд форми",
- "kanban": "Додати вигляд Kanban",
+ "kanban": "Додати вигляд таблички",
"map": "Додати вид мапи",
"calendar": "Додати вигляд календаря"
},
"tablesMetadataInSync": "Таблиці метаданих синхронізуються",
- "addMultipleUsers": "Ви можете додати кілька електронних адрес, розділених комами(,)",
+ "addMultipleUsers": "Ви можете додати кілька електронних адрес через кому",
"enterTableName": "Введіть назву таблиці",
"addDefaultColumns": "Додайте стовпці за замовчуванням",
"tableNameInDb": "Назва таблиці, збережена в базі даних",
@@ -648,7 +648,7 @@
"import": {
"clickOrDrag": "Клацніть або перетягніть файл у цю область, щоб завантажити його"
},
- "metaDataRecreated": "Метадані таблиці успішно відтворено",
+ "metaDataRecreated": "Метадані таблиці відтворено",
"invalidCredentials": "Недійсні облікові дані",
"downloadingMoreFiles": "Завантажте інші файли",
"copiedToClipboard": "Скопійовано в буфер обміну",
@@ -658,7 +658,7 @@
"editingPKnotSupported": "Редагування первинного ключа не підтримується",
"deletedCache": "Кеш успішно видалено",
"cacheEmpty": "Кеш порожній",
- "exportedCache": "Кеш успішно експортовано",
+ "exportedCache": "Кеш експортовано",
"valueAlreadyInList": "Це значення вже є у списку",
"noColumnsToUpdate": "Немає стовпців для оновлення",
"tableDeleted": "Таблицю успішно видалено",
@@ -668,26 +668,26 @@
"showM2mTables": "Показати M2M таблиці",
"showM2mTablesDesc": "Звʼязок \"багато-до-багатьох\" підтримується через таблицю зʼєднань і за замовчуванням прихований. Увімкніть цю опцію, щоб перерахувати всі такі таблиці разом з існуючими.",
"showNullInCells": "Показати NULL в комірках",
- "showNullInCellsDesc": "Відображати тег 'NULL' у клітинках, що містять NULL-значення. Це допомагає відрізнити клітинки, що містять ПУСТИЙ рядок.",
+ "showNullInCellsDesc": "Відображати тег \"NULL\" у клітинках, що містять NULL-значення. Це допомагає відрізнити клітинки, що містять пустий рядок.",
"showNullAndEmptyInFilter": "Показувати NULL та EMPTY у фільтрі",
- "showNullAndEmptyInFilterDesc": "Увімкніть \"додаткові\" фільтри для розрізнення полів, що містять NULL та порожні рядки. За замовчуванням підтримка пропусків однаково обробляє як NULL, так і порожні рядки.",
- "deleteKanbanStackConfirmation": "Видалення цього стека також вилучить опцію вибору `{stackToBeDeleted}` зі стека `{groupingField}`. Записи буде переміщено до не категоризованого стека.",
+ "showNullAndEmptyInFilterDesc": "Ввімкніть \"додаткові\" фільтри для розрізнення полів, що містять NULL та порожні рядки. За замовчуванням підтримка пропусків однаково обробляє як NULL, так і порожні рядки.",
+ "deleteKanbanStackConfirmation": "Видалення цієї таблички також вилучить опцію вибору `{stackToBeDeleted}` з таблички `{groupingField}`. Записи буде переміщено до не категоризованої таблички.",
"computedFieldEditWarning": "Обчислюване поле: вміст доступний лише для читання. Використовуйте меню редагування стовпця для зміни конфігурації",
"computedFieldDeleteWarning": "Обчислюване поле: вміст доступний лише для читання. Не вдалося очистити вміст.",
"noMoreRecords": "Більше записів немає"
},
"error": {
"searchProject": "За вашим запитом {search} не знайдено жодного результату",
- "invalidChar": "Недійсний символ у шляху до теки.",
+ "invalidChar": "Недійсний символ у шляху.",
"invalidDbCredentials": "Неправильні облікові дані бази даних.",
- "unableToConnectToDb": "Неможливо під'єднатися до бази даних, будь ласка, перевірте, чи працює ваша база даних.",
+ "unableToConnectToDb": "Неможливо підʼєднатися до бази даних, будь ласка, перевірте, чи працює ваша база даних.",
"userDoesntHaveSufficientPermission": "Користувач не існує або не має достатніх прав для створення конфігурації.",
"dbConnectionStatus": "Неправильні параметри бази даних",
"dbConnectionFailed": "Помилка з’єднання:",
"signUpRules": {
- "emailReqd": "E-mail є обов'язковим полем",
- "emailInvalid": "E-mail має невірний формат",
- "passwdRequired": "Пароль є обов'язковим",
+ "emailReqd": "Пошта є обовʼязковим полем",
+ "emailInvalid": "Пошта має невірний формат",
+ "passwdRequired": "Пароль є обовʼязковим",
"passwdLength": "Пароль має складатися хоча б з 8 символів",
"passwdMismatch": "Паролі не збігаються",
"completeRuleSet": "Щонайменше 8 символів з однією великою літерою, однією цифрою та одним спеціальним символом",
@@ -704,7 +704,7 @@
"fileUploadFailed": "Не вдалося завантажити файл",
"primaryColumnUpdateFailed": "Не вдалося оновити основний стовпець",
"formDescriptionTooLong": "Занадто довгі дані для опису форми",
- "columnsRequired": "Обов'язковими є наступні стовпці",
+ "columnsRequired": "Обовʼязковими є наступні стовпці",
"selectAtleastOneColumn": "Принаймні один стовпець має бути вибраний",
"columnDescriptionNotFound": "Не вдається знайти стовпець призначення для",
"duplicateMappingFound": "Знайдено дублікат схеми, будь ласка, видаліть одну зі схем",
@@ -723,7 +723,7 @@
"setFormDataFailed": "Не вдалося встановити дані форми",
"formViewUpdateFailed": "Не вдалося оновити вигляд форми",
"tableNameRequired": "Ім'я таблиці є обов'язковим",
- "nameShouldStartWithAnAlphabetOr_": "Ім'я повинно починатися з літери або _",
+ "nameShouldStartWithAnAlphabetOr_": "Імʼя повинно починатися з літери або _",
"followingCharactersAreNotAllowed": "Наступні символи не допускаються",
"columnNameRequired": "Ім'я стовпця є обов'язковим",
"columnNameExceedsCharacters": "Довжина назви стовпця перевищує максимальну кількість в {value} символів",
@@ -741,52 +741,52 @@
"copyToClipboardError": "Не вдалося скопіювати в буфер обміну"
},
"toast": {
- "exportMetadata": "Метадані проєкту успішно експортовано",
- "importMetadata": "Метадані проєкту успішно імпортовано",
- "clearMetadata": "Метадані проєкту успішно очищено",
- "stopProject": "Проєкт успішно зупинений",
- "startProject": "Проєкт успішно запущений",
- "restartProject": "Проєкт успішно перезапущений",
- "deleteProject": "Проєкт успішно видалений",
+ "exportMetadata": "Метадані проєкту експортовано",
+ "importMetadata": "Метадані проєкту імпортовано",
+ "clearMetadata": "Метадані проєкту очищено",
+ "stopProject": "Проєкт зупинений",
+ "startProject": "Проєкт запущений",
+ "restartProject": "Проєкт перезапущений",
+ "deleteProject": "Проєкт видалений",
"authToken": "Токен авторизації скопійовано в буфер обміну",
"projInfo": "Інформацію про проєкт скопійовано до буфера обміну",
"inviteUrlCopy": "Скопійовано запрошення URL в буфер обміну",
- "createView": "Вигляд створено успішно",
- "formEmailSMTP": "Будь ласка, активуйте плагін SMTP в Магазині додатків, щоб увімкнути повідомлення електронної пошти",
- "collabView": "Успішно переведено в режим спільної роботи",
- "lockedView": "Успішно перемкнуто на заблокований вигляд",
+ "createView": "Вигляд створено",
+ "formEmailSMTP": "Будь ласка, активуйте плагін SMTP в магазині додатків, щоб ввімкнути повідомлення на електронну пошту",
+ "collabView": "Переведено в режим спільної роботи",
+ "lockedView": "Переведено в заблокований режим",
"futureRelease": "Незабаром!"
},
"success": {
"columnDuplicated": "Стовпець успішно продубльовано",
- "rowDuplicatedWithoutSavedYet": "Рядок продубльовано (не збережено)",
+ "rowDuplicatedWithoutSavedYet": "Рядок продубльовано, але не збережено",
"updatedUIACL": "Успішно оновлено UI ACL для таблиць",
"pluginUninstalled": "Плагін успішно видалено",
- "pluginSettingsSaved": "Налаштування плагіну успішно збережено",
+ "pluginSettingsSaved": "Налаштування плагіну збережено",
"pluginTested": "Успішно протестовані налаштування плагіну",
"tableRenamed": "Таблицю успішно перейменовано",
- "viewDeleted": "Вигляд успішно видалено",
+ "viewDeleted": "Вигляд видалено",
"primaryColumnUpdated": "Успішно оновлено як основний стовпець",
"tableDataExported": "Успішно експортовано всі дані таблиці",
"updated": "Успішно оновлено",
- "sharedViewDeleted": "Успішно видалено спільний перегляд",
+ "sharedViewDeleted": "Спільний перегляд видалено",
"userDeleted": "Користувача успішно видалено",
- "viewRenamed": "Вигляд успішно перейменовано",
+ "viewRenamed": "Вигляд перейменовано",
"tokenGenerated": "Токен успішно згенеровано",
"tokenDeleted": "Токен успішно видалено",
- "userAddedToProject": "Користувача успішно додано до проєкту",
- "userAdded": "Користувача успішно додано",
- "userDeletedFromProject": "Користувача успішно видалено з проєкту",
- "inviteEmailSent": "Лист запрошення успішно відправлено на електронну пошту",
- "inviteURLCopied": "URL запрошення скопійоване в буфер обміну",
- "commentCopied": "Коментар скопійовано до буфера обміну",
+ "userAddedToProject": "Користувача додано до проєкту",
+ "userAdded": "Користувача додано",
+ "userDeletedFromProject": "Користувача видалено з проєкту",
+ "inviteEmailSent": "Лист запрошення відправлено на електронну пошту",
+ "inviteURLCopied": "URL-посилання для запрошення скопійовано",
+ "commentCopied": "Коментар скопійовано в буфер обміну",
"passwordResetURLCopied": "URL-адресу скидання пароля скопійовано в буфер обміну",
- "shareableURLCopied": "URL адресу спільної бази скопійовано в буфер обміну!",
+ "shareableURLCopied": "URL-посилання спільної бази скопійовано!",
"embeddableHTMLCodeCopied": "Скопійовано вбудований HTML-код!",
- "userDetailsUpdated": "Дані користувача успішно оновлено",
- "tableDataImported": "Дані таблиці успішно імпортовано",
- "webhookUpdated": "Дані Webhook успішно оновлено",
- "webhookDeleted": "Hook успішно видалено",
+ "userDetailsUpdated": "Дані користувача оновлено",
+ "tableDataImported": "Дані таблиці імпортовано",
+ "webhookUpdated": "Дані вебхуку оновлено",
+ "webhookDeleted": "Вебхук видалено",
"webhookTested": "Webhook успішно протестовано",
"columnUpdated": "Стовпець оновлено",
"columnCreated": "Стовпець створено",
diff --git a/packages/nc-gui/lang/zh-Hans.json b/packages/nc-gui/lang/zh-Hans.json
index 93c45580b2..c8aec7fc76 100644
--- a/packages/nc-gui/lang/zh-Hans.json
+++ b/packages/nc-gui/lang/zh-Hans.json
@@ -238,7 +238,7 @@
"action": "操作",
"actions": "操作",
"operation": "操作",
- "operationSub": "Sub Operation",
+ "operationSub": "子操作",
"operationType": "操作类型",
"operationSubType": "子操作类型",
"description": "描述",
@@ -471,11 +471,11 @@
},
"map": {
"mappedBy": "Mapped By",
- "chooseMappingField": "Choose a Mapping Field",
+ "chooseMappingField": "选择映射字段",
"openInGoogleMaps": "谷歌地图",
"openInOpenStreetMap": "OSM"
},
- "toggleMobileMode": "Toggle Mobile Mode"
+ "toggleMobileMode": "切换移动模式"
},
"tooltip": {
"saveChanges": "保存更改",
@@ -779,7 +779,7 @@
"userDeletedFromProject": "踢出用户成功",
"inviteEmailSent": "邀请邮件发送成功",
"inviteURLCopied": "邀请URL已复制到剪贴板",
- "commentCopied": "Comment copied to clipboard",
+ "commentCopied": "已复制评论到剪贴板",
"passwordResetURLCopied": "密码重置网址已复制到剪贴板",
"shareableURLCopied": "已将可共享的基础URL复制到剪贴板!",
"embeddableHTMLCodeCopied": "已复制可嵌入的 HTML 代码!",
diff --git a/packages/nc-gui/lib/enums.ts b/packages/nc-gui/lib/enums.ts
index ab6e8b03df..b4aadc53c6 100644
--- a/packages/nc-gui/lib/enums.ts
+++ b/packages/nc-gui/lib/enums.ts
@@ -99,3 +99,9 @@ export enum DataSourcesSubTab {
Misc = 'Misc',
Edit = 'Edit',
}
+
+export enum AutomationLogLevel {
+ OFF = 'OFF',
+ ERROR = 'ERROR',
+ ALL = 'ALL',
+}
diff --git a/packages/nc-gui/package-lock.json b/packages/nc-gui/package-lock.json
index de3f3961c1..1014db0a01 100644
--- a/packages/nc-gui/package-lock.json
+++ b/packages/nc-gui/package-lock.json
@@ -30,7 +30,7 @@
"leaflet.markercluster": "^1.5.3",
"locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0",
- "nocodb-sdk": "0.106.0-beta.0",
+ "nocodb-sdk": "file:../nocodb-sdk",
"papaparse": "^5.3.2",
"pinia": "^2.0.33",
"qrcode": "^1.5.1",
@@ -111,7 +111,6 @@
},
"../nocodb-sdk": {
"version": "0.106.0-beta.0",
- "extraneous": true,
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@@ -8776,6 +8775,7 @@
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
+ "devOptional": true,
"funding": [
{
"type": "individual",
@@ -12294,21 +12294,8 @@
}
},
"node_modules/nocodb-sdk": {
- "version": "0.106.0-beta.0",
- "resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.106.0-beta.0.tgz",
- "integrity": "sha512-XoooU7D34nM5Hl9gEbyUmA0UzbWL+7E73PGm9b9Kmt3TCOnGtYL6AVaSqF0ve2xHhyTwNyIU1xB7neEDlZJRwA==",
- "dependencies": {
- "axios": "^0.21.1",
- "jsep": "^1.3.6"
- }
- },
- "node_modules/nocodb-sdk/node_modules/axios": {
- "version": "0.21.4",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
- "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
- "dependencies": {
- "follow-redirects": "^1.14.0"
- }
+ "resolved": "../nocodb-sdk",
+ "link": true
},
"node_modules/node-abi": {
"version": "3.23.0",
@@ -24810,7 +24797,8 @@
"follow-redirects": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
- "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
+ "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
+ "devOptional": true
},
"form-data": {
"version": "4.0.0",
@@ -27360,22 +27348,22 @@
}
},
"nocodb-sdk": {
- "version": "0.106.0-beta.0",
- "resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.106.0-beta.0.tgz",
- "integrity": "sha512-XoooU7D34nM5Hl9gEbyUmA0UzbWL+7E73PGm9b9Kmt3TCOnGtYL6AVaSqF0ve2xHhyTwNyIU1xB7neEDlZJRwA==",
+ "version": "file:../nocodb-sdk",
"requires": {
+ "@typescript-eslint/eslint-plugin": "^4.0.1",
+ "@typescript-eslint/parser": "^4.0.1",
"axios": "^0.21.1",
- "jsep": "^1.3.6"
- },
- "dependencies": {
- "axios": {
- "version": "0.21.4",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
- "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
- "requires": {
- "follow-redirects": "^1.14.0"
- }
- }
+ "cspell": "^4.1.0",
+ "eslint": "^7.8.0",
+ "eslint-config-prettier": "^6.11.0",
+ "eslint-plugin-eslint-comments": "^3.2.0",
+ "eslint-plugin-functional": "^3.0.2",
+ "eslint-plugin-import": "^2.22.0",
+ "eslint-plugin-prettier": "^4.0.0",
+ "jsep": "^1.3.6",
+ "npm-run-all": "^4.1.5",
+ "prettier": "^2.1.1",
+ "typescript": "^4.0.2"
}
},
"node-abi": {
diff --git a/packages/nc-gui/package.json b/packages/nc-gui/package.json
index 9415b5f527..847017b499 100644
--- a/packages/nc-gui/package.json
+++ b/packages/nc-gui/package.json
@@ -54,7 +54,7 @@
"leaflet.markercluster": "^1.5.3",
"locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0",
- "nocodb-sdk": "0.106.0-beta.0",
+ "nocodb-sdk": "file:../nocodb-sdk",
"papaparse": "^5.3.2",
"pinia": "^2.0.33",
"qrcode": "^1.5.1",
diff --git a/packages/noco-docs/content/en/developer-resources/webhooks.md b/packages/noco-docs/content/en/developer-resources/webhooks.md
index 820acfeaba..39ebb726b6 100644
--- a/packages/noco-docs/content/en/developer-resources/webhooks.md
+++ b/packages/noco-docs/content/en/developer-resources/webhooks.md
@@ -19,6 +19,7 @@ Some types of notifications can be triggered by a webhook after a particular eve
### Configure Webhook
+
- General configurations
- Webhook Name
- Webhook Trigger
@@ -27,11 +28,22 @@ Some types of notifications can be triggered by a webhook after a particular eve
- Webhook Conditional Trigger
- Only records meeting the criteria will trigger webhook
-![Screenshot 2022-09-14 at 10 35 39 AM](https://user-images.githubusercontent.com/86527202/190064668-37245025-81f6-491c-b639-83c8fd131bc3.png)
+
+![Screenshot 2023-04-06 at 11 39 49 AM](https://user-images.githubusercontent.com/86527202/230288581-c613e591-1c32-4151-a2d1-df2bbf1367fd.png)
+## Call Log
+
+Call Log allows user to check the call history of the hook. By default, it has been disabled. However, it can be configured by using environment variable `NC_AUTOMATION_LOG_LEVEL`.
+
+- `NC_AUTOMATION_LOG_LEVEL=OFF`: No logs will be displayed and no history will be inserted to meta database.
+- `NC_AUTOMATION_LOG_LEVEL=ERROR`: only error logs will be displayed and history of error logs will be inserted to meta database.
+- `NC_AUTOMATION_LOG_LEVEL=ALL`: Both error and success logs will be displayed and history of both types of logs will be inserted to meta database. **This option is only available for Enterprise Edition.**
+
+![image](https://user-images.githubusercontent.com/35857179/228790148-1e3f21c7-9385-413a-843f-b93073ca6bea.png)
+
## Triggers
@@ -59,6 +71,11 @@ The triggers will trigger asynchronously without blocking the actual operation.
## Accessing Data: Handlebars
+
+
+You can accessing data using handlebars for v1 webhooks only.
+
+
The current row data and other details will be available in the hooks payload so the user can use [handlebar syntax](https://handlebarsjs.com/guide/#simple-expressions) to use data.
> We are using [Handlebars](https://handlebarsjs.com/) library to parse the payload internally.
@@ -136,6 +153,7 @@ Detailed procedure for discord webhook described [here](https://support.discord.
## Slack
+
### 1. Create WebHook
- Details to create slack webhook are captured [here](https://api.slack.com/messaging/webhooks)
@@ -196,3 +214,13 @@ Detailed procedure for discord webhook described [here](https://support.discord.
- **Body**: Message to be posted over Teams channel, via webhooks on trigger of configured event.
- Body can contain plain text &
- Handlebars {{ }}
+
+## Webhook V2
+
+Webhook v2 is available after v0.106.0. Here's the differences.
+
+- Response Payload has been predefined and cannot configure in Body using Handlebars. The payload can be referenced under `Sample Payload` in Hook detail page.
+- Support the following bulk operations:
+ - AFTER BULK INSERT
+ - AFTER BULK UPDATE
+ - AFTER BULK DELETE
diff --git a/packages/noco-docs/content/en/getting-started/environment-variables.md b/packages/noco-docs/content/en/getting-started/environment-variables.md
index b3ac5d4d0f..2c277d41d4 100644
--- a/packages/noco-docs/content/en/getting-started/environment-variables.md
+++ b/packages/noco-docs/content/en/getting-started/environment-variables.md
@@ -61,4 +61,5 @@ For production usecases, it is **recommended** to configure
| NC_ADMIN_PASSWORD | For updating/creating super admin with provided email and password. Your password should have at least 8 letters with one uppercase, one number and one special letter(Allowed special chars $&+,:;=?@#\|'.^*()%!_-" ) | | |
| NODE_OPTIONS | For passing Node.js [options](https://nodejs.org/api/cli.html#node_optionsoptions) to instance | | |
| NC_MINIMAL_DBS | Create a new SQLite file for each project. All the db files are stored in `nc_minimal_dbs` folder in current working directory. (This option restricts project creation on external sources) | | |
-| NC_DISABLE_AUDIT | Disable Audit Log | `false` | |
\ No newline at end of file
+| NC_DISABLE_AUDIT | Disable Audit Log | `false` | |
+| NC_AUTOMATION_LOG_LEVEL | Possible Values: `OFF`, `ERROR`, `ALL`. See [Webhooks](/developer-resources/webhooks#call-log) for details. | `OFF` | |
\ No newline at end of file
diff --git a/packages/nocodb-sdk/package-lock.json b/packages/nocodb-sdk/package-lock.json
index 8c1bda86b4..56193280e4 100644
--- a/packages/nocodb-sdk/package-lock.json
+++ b/packages/nocodb-sdk/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "nocodb-sdk",
- "version": "0.105.3",
+ "version": "0.106.0-beta.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb-sdk",
- "version": "0.105.3",
+ "version": "0.106.0-beta.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
diff --git a/packages/nocodb-sdk/src/lib/Api.ts b/packages/nocodb-sdk/src/lib/Api.ts
index ec3a596669..b3b12a773e 100644
--- a/packages/nocodb-sdk/src/lib/Api.ts
+++ b/packages/nocodb-sdk/src/lib/Api.ts
@@ -622,6 +622,16 @@ export interface FilterListType {
pageInfo: PaginatedType;
}
+/**
+ * Model for Filter Log List
+ */
+export interface FilterLogListType {
+ /** List of filter objects */
+ list: FilterType[];
+ /** Model for Paginated */
+ pageInfo: PaginatedType;
+}
+
/**
* Model for Filter Request
*/
@@ -983,6 +993,29 @@ export interface GridType {
columns?: GridColumnType[];
}
+/**
+ * Model for Grid
+ */
+export interface GridCopyType {
+ /** Unique ID */
+ id?: IdType;
+ /** Project ID */
+ project_id?: IdType;
+ /** Base ID */
+ base_id?: IdType;
+ /** Foreign Key to View */
+ fk_view_id?: IdType;
+ /**
+ * Row Height
+ * @example 1
+ */
+ row_height?: number;
+ /** Meta info for Grid Model */
+ meta?: MetaType;
+ /** Grid View Columns */
+ columns?: GridColumnType[];
+}
+
/**
* Model for Grid Column
*/
@@ -1082,7 +1115,13 @@ export interface HookType {
* Hook Operation
* @example insert
*/
- operation?: 'delete' | 'insert' | 'update';
+ operation?:
+ | 'insert'
+ | 'update'
+ | 'delete'
+ | 'bulkInsert'
+ | 'bulkUpdate'
+ | 'bulkDelete';
/**
* Retry Count
* @example 10
@@ -1105,6 +1144,11 @@ export interface HookType {
title?: string;
/** Hook Type */
type?: string;
+ /**
+ * Hook Version
+ * @example v2
+ */
+ version?: 'v1' | 'v2';
}
/**
@@ -1140,7 +1184,13 @@ export interface HookReqType {
* Hook Operation
* @example insert
*/
- operation: 'delete' | 'insert' | 'update';
+ operation:
+ | 'insert'
+ | 'update'
+ | 'delete'
+ | 'bulkInsert'
+ | 'bulkUpdate'
+ | 'bulkDelete';
/**
* Retry Count
* @example 10
@@ -1181,28 +1231,79 @@ export interface HookListType {
* Model for Hook Log
*/
export interface HookLogType {
+ /**
+ * Unique Base ID
+ * @example ds_jxuewivwbxeum2
+ */
base_id?: string;
+ /** Hook Conditions */
conditions?: string;
- error?: string;
- error_code?: string;
- error_message?: string;
- event?: string;
+ /** Error */
+ error?: StringOrNullType;
+ /** Error Code */
+ error_code?: StringOrNullType;
+ /** Error Message */
+ error_message?: StringOrNullType;
+ /**
+ * Hook Event
+ * @example after
+ */
+ event?: 'after' | 'before';
+ /**
+ * Execution Time in milliseconds
+ * @example 98
+ */
execution_time?: string;
- /** Model for StringOrNull */
+ /** Foreign Key to Hook */
fk_hook_id?: StringOrNullType;
/** Unique ID */
- id?: IdType;
+ id?: StringOrNullType;
+ /** Hook Notification */
notifications?: string;
- operation?: string;
- payload?: any;
+ /**
+ * Hook Operation
+ * @example insert
+ */
+ operation?:
+ | 'insert'
+ | 'update'
+ | 'delete'
+ | 'bulkInsert'
+ | 'bulkUpdate'
+ | 'bulkDelete';
+ /**
+ * Hook Payload
+ * @example {"method":"POST","body":"{{ json data }}","headers":[{}],"parameters":[{}],"auth":"","path":"https://webhook.site/6eb45ce5-b611-4be1-8b96-c2965755662b"}
+ */
+ payload?: string;
+ /**
+ * Project ID
+ * @example p_tbhl1hnycvhe5l
+ */
project_id?: string;
- response?: string;
- /** Model for Bool */
+ /** Hook Response */
+ response?: StringOrNullType;
+ /** Is this testing hook call? */
test_call?: BoolType;
- triggered_by?: string;
+ /** Who triggered the hook? */
+ triggered_by?: StringOrNullType;
+ /**
+ * Hook Type
+ * @example URL
+ */
type?: string;
}
+/**
+ * Model for Hook Log List
+ */
+export interface HookLogListType {
+ /** List of hook objects */
+ list: HookLogType[];
+ /** Model for Paginated */
+ pageInfo: PaginatedType;
+}
+
/**
* Model for Hook Test Request
*/
@@ -6399,6 +6500,45 @@ export class Api<
...params,
}),
};
+ dbTableWebhookLogs = {
+ /**
+ * @description List the log data in a given Hook
+ *
+ * @tags DB Table Webhook Logs
+ * @name List
+ * @summary List Hook Logs
+ * @request GET:/api/v1/db/meta/hooks/{hookId}/logs
+ * @response `200` `HookLogListType` OK
+ * @response `400` `{
+ \** @example BadRequest [Error]: *\
+ msg: string,
+
+}`
+ */
+ list: (
+ hookId: IdType,
+ query?: {
+ /** @min 1 */
+ limit?: number;
+ /** @min 0 */
+ offset?: number;
+ },
+ params: RequestParams = {}
+ ) =>
+ this.request<
+ HookLogListType,
+ {
+ /** @example BadRequest [Error]: */
+ msg: string;
+ }
+ >({
+ path: `/api/v1/db/meta/hooks/${hookId}/logs`,
+ method: 'GET',
+ query: query,
+ format: 'json',
+ ...params,
+ }),
+ };
dbTableRow = {
/**
* @description List all table rows in a given table and project
@@ -8409,6 +8549,8 @@ export class Api<
ncAttachmentFieldSize?: number,
ncMaxAttachmentsAllowed?: number,
isCloud?: boolean,
+ \** @example OFF *\
+ automationLogLevel?: "OFF" | "ERROR" | "ALL",
}` OK
* @response `400` `{
@@ -8438,6 +8580,8 @@ export class Api<
ncAttachmentFieldSize?: number;
ncMaxAttachmentsAllowed?: number;
isCloud?: boolean;
+ /** @example OFF */
+ automationLogLevel?: 'OFF' | 'ERROR' | 'ALL';
},
{
/** @example BadRequest [Error]: */
@@ -8860,7 +9004,7 @@ export class Api<
* @tags DB Table Webhook
* @name SamplePayloadGet
* @summary Get Sample Hook Payload
- * @request GET:/api/v1/db/meta/tables/{tableId}/hooks/samplePayload/{operation}
+ * @request GET:/api/v1/db/meta/tables/{tableId}/hooks/samplePayload/{operation}/{version}
* @response `200` `{
\** Sample Payload Data *\
data?: object,
@@ -8874,7 +9018,14 @@ export class Api<
*/
samplePayloadGet: (
tableId: IdType,
- operation: 'update' | 'delete' | 'insert',
+ operation:
+ | 'insert'
+ | 'update'
+ | 'delete'
+ | 'bulkInsert'
+ | 'bulkUpdate'
+ | 'bulkDelete',
+ version: 'v1' | 'v2',
params: RequestParams = {}
) =>
this.request<
@@ -8887,7 +9038,7 @@ export class Api<
msg: string;
}
>({
- path: `/api/v1/db/meta/tables/${tableId}/hooks/samplePayload/${operation}`,
+ path: `/api/v1/db/meta/tables/${tableId}/hooks/samplePayload/${operation}/${version}`,
method: 'GET',
format: 'json',
...params,
diff --git a/packages/nocodb/Dockerfile.local b/packages/nocodb/Dockerfile.local
new file mode 100644
index 0000000000..bb3e9a5fcd
--- /dev/null
+++ b/packages/nocodb/Dockerfile.local
@@ -0,0 +1,55 @@
+###########
+# Builder
+###########
+FROM node:16.17.0-alpine3.15 as builder
+WORKDIR /usr/src/app
+
+# install node-gyp dependencies
+RUN apk add --no-cache python3 make g++
+
+# Copy application dependency manifests to the container image.
+# A wildcard is used to ensure both package.json AND package-lock.json are copied.
+# Copying this separately prevents re-running npm ci on every code change.
+COPY ./package*.json ./
+COPY ./docker/nc-gui/ ./docker/nc-gui/
+COPY ./docker/main.js ./docker/index.js
+COPY ./docker/start-local.sh /usr/src/appEntry/start.sh
+COPY ./src/lib/public/css/*.css ./docker/public/css/
+COPY ./src/lib/public/js/*.js ./docker/public/js/
+COPY ./src/lib/public/favicon.ico ./docker/public/
+
+# install production dependencies,
+# reduce node_module size with modclean & removing sqlite deps,
+# package built code into app.tar.gz & add execute permission to start.sh
+RUN npm ci --omit=dev --quiet \
+ && npx modclean --patterns="default:*" --ignore="nc-lib-gui/**,dayjs/**,express-status-monitor/**,@azure/msal-node/dist/**" --run \
+ && rm -rf ./node_modules/sqlite3/deps \
+ && tar -czf ../appEntry/app.tar.gz ./* \
+ && chmod +x /usr/src/appEntry/start.sh
+
+##########
+# Runner
+##########
+FROM alpine:3.15
+WORKDIR /usr/src/app
+
+ENV NC_DOCKER 0.6
+ENV NODE_ENV production
+ENV PORT 8080
+ENV NC_TOOL_DIR=/usr/app/data/
+
+RUN apk --update --no-cache add \
+ nodejs \
+ tar \
+ dumb-init \
+ curl \
+ jq
+
+# Copy packaged production code & main entry file
+COPY --from=builder /usr/src/appEntry/ /usr/src/appEntry/
+
+EXPOSE 8080
+ENTRYPOINT ["/usr/bin/dumb-init", "--"]
+
+# Start Nocodb
+CMD ["/usr/src/appEntry/start.sh"]
diff --git a/packages/nocodb/docker/start-local.sh b/packages/nocodb/docker/start-local.sh
new file mode 100644
index 0000000000..619e2f58fd
--- /dev/null
+++ b/packages/nocodb/docker/start-local.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+FILE="/usr/src/app/package.json"
+
+if [ ! -z "${NC_TOOL_DIR}" ]; then
+ mkdir -p $NC_TOOL_DIR
+fi
+
+if [ ! -f "$FILE" ]
+then
+ tar -xzf /usr/src/appEntry/app.tar.gz -C /usr/src/app/
+fi
+
+node docker/index.js
diff --git a/packages/nocodb/package-lock.json b/packages/nocodb/package-lock.json
index 72b3374266..6403f31d8c 100644
--- a/packages/nocodb/package-lock.json
+++ b/packages/nocodb/package-lock.json
@@ -70,7 +70,7 @@
"nc-lib-gui": "0.106.0-beta.0",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
- "nocodb-sdk": "0.106.0-beta.0",
+ "nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"os-locale": "^5.0.0",
@@ -157,7 +157,6 @@
},
"../nocodb-sdk": {
"version": "0.106.0-beta.0",
- "extraneous": true,
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@@ -11392,13 +11391,8 @@
"dev": true
},
"node_modules/nocodb-sdk": {
- "version": "0.106.0-beta.0",
- "resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.106.0-beta.0.tgz",
- "integrity": "sha512-XoooU7D34nM5Hl9gEbyUmA0UzbWL+7E73PGm9b9Kmt3TCOnGtYL6AVaSqF0ve2xHhyTwNyIU1xB7neEDlZJRwA==",
- "dependencies": {
- "axios": "^0.21.1",
- "jsep": "^1.3.6"
- }
+ "resolved": "../nocodb-sdk",
+ "link": true
},
"node_modules/node-abort-controller": {
"version": "3.0.1",
@@ -28097,12 +28091,22 @@
"dev": true
},
"nocodb-sdk": {
- "version": "0.106.0-beta.0",
- "resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.106.0-beta.0.tgz",
- "integrity": "sha512-XoooU7D34nM5Hl9gEbyUmA0UzbWL+7E73PGm9b9Kmt3TCOnGtYL6AVaSqF0ve2xHhyTwNyIU1xB7neEDlZJRwA==",
+ "version": "file:../nocodb-sdk",
"requires": {
+ "@typescript-eslint/eslint-plugin": "^4.0.1",
+ "@typescript-eslint/parser": "^4.0.1",
"axios": "^0.21.1",
- "jsep": "^1.3.6"
+ "cspell": "^4.1.0",
+ "eslint": "^7.8.0",
+ "eslint-config-prettier": "^6.11.0",
+ "eslint-plugin-eslint-comments": "^3.2.0",
+ "eslint-plugin-functional": "^3.0.2",
+ "eslint-plugin-import": "^2.22.0",
+ "eslint-plugin-prettier": "^4.0.0",
+ "jsep": "^1.3.6",
+ "npm-run-all": "^4.1.5",
+ "prettier": "^2.1.1",
+ "typescript": "^4.0.2"
}
},
"node-abort-controller": {
diff --git a/packages/nocodb/package.json b/packages/nocodb/package.json
index 493120154f..a548e478f6 100644
--- a/packages/nocodb/package.json
+++ b/packages/nocodb/package.json
@@ -112,7 +112,7 @@
"nc-lib-gui": "0.106.0-beta.0",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
- "nocodb-sdk": "0.106.0-beta.0",
+ "nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"os-locale": "^5.0.0",
@@ -188,4 +188,4 @@
"prettier": {
"singleQuote": true
}
-}
\ No newline at end of file
+}
diff --git a/packages/nocodb/src/lib/Noco.ts b/packages/nocodb/src/lib/Noco.ts
index 85c6ff8b61..8e3110b503 100644
--- a/packages/nocodb/src/lib/Noco.ts
+++ b/packages/nocodb/src/lib/Noco.ts
@@ -102,7 +102,7 @@ export default class Noco {
constructor() {
process.env.PORT = process.env.PORT || '8080';
// todo: move
- process.env.NC_VERSION = '0105003';
+ process.env.NC_VERSION = '0105004';
// if env variable NC_MINIMAL_DBS is set, then disable project creation with external sources
if (process.env.NC_MINIMAL_DBS) {
diff --git a/packages/nocodb/src/lib/controllers/hook.ctl.ts b/packages/nocodb/src/lib/controllers/hook.ctl.ts
index 7e671d2ec1..0dc90be12b 100644
--- a/packages/nocodb/src/lib/controllers/hook.ctl.ts
+++ b/packages/nocodb/src/lib/controllers/hook.ctl.ts
@@ -4,7 +4,7 @@ import { PagedResponseImpl } from '../meta/helpers/PagedResponse';
import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import { hookService } from '../services';
-import type { HookListType, HookType } from 'nocodb-sdk';
+import type { HookListType, HookLogListType, HookType } from 'nocodb-sdk';
import type { Request, Response } from 'express';
export async function hookList(
@@ -46,29 +46,62 @@ export async function hookUpdate(
}
export async function hookTest(req: Request, res: Response) {
- await hookService.hookTest({
- hookTest: req.body,
- tableId: req.params.tableId,
- });
- res.json({ msg: 'The hook has been tested successfully' });
+ try {
+ await hookService.hookTest({
+ hookTest: {
+ ...req.body,
+ payload: {
+ ...req.body.payload,
+ user: (req as any)?.user,
+ },
+ },
+ tableId: req.params.tableId,
+ });
+ res.json({ msg: 'The hook has been tested successfully' });
+ } catch (e) {
+ console.error(e);
+ throw e;
+ }
}
export async function tableSampleData(req: Request, res: Response) {
res.json(
await hookService.tableSampleData({
tableId: req.params.tableId,
- // todo: replace any with type
- operation: req.params.operation as any,
+ operation: req.params.operation as HookType['operation'],
+ version: req.params.version as HookType['version'],
})
);
}
+export async function hookLogList(
+ req: Request,
+ res: Response
+) {
+ res.json(
+ new PagedResponseImpl(
+ await hookService.hookLogList({
+ query: req.query,
+ hookId: req.params.hookId,
+ }),
+ {
+ ...req.query,
+ count: await hookService.hookLogCount({
+ hookId: req.params.hookId,
+ }),
+ }
+ )
+ );
+}
+
const router = Router({ mergeParams: true });
+
router.get(
'/api/v1/db/meta/tables/:tableId/hooks',
metaApiMetrics,
ncMetaAclMw(hookList, 'hookList')
);
+
router.post(
'/api/v1/db/meta/tables/:tableId/hooks/test',
metaApiMetrics,
@@ -90,8 +123,15 @@ router.patch(
ncMetaAclMw(hookUpdate, 'hookUpdate')
);
router.get(
- '/api/v1/db/meta/tables/:tableId/hooks/samplePayload/:operation',
+ '/api/v1/db/meta/tables/:tableId/hooks/samplePayload/:operation/:version',
metaApiMetrics,
catchError(tableSampleData)
);
+
+router.get(
+ '/api/v1/db/meta/hooks/:hookId/logs',
+ metaApiMetrics,
+ ncMetaAclMw(hookLogList, 'hookLogList')
+);
+
export default router;
diff --git a/packages/nocodb/src/lib/db/sql-client/lib/SqlClientFactory.ts b/packages/nocodb/src/lib/db/sql-client/lib/SqlClientFactory.ts
index 3781b1976d..9e9ca73383 100644
--- a/packages/nocodb/src/lib/db/sql-client/lib/SqlClientFactory.ts
+++ b/packages/nocodb/src/lib/db/sql-client/lib/SqlClientFactory.ts
@@ -48,19 +48,28 @@ export default class {
typeof connectionConfig.connection.ssl === 'object'
) {
if (connectionConfig.connection.ssl.caFilePath) {
- connectionConfig.connection.ssl.ca = await promisify(fs.readFile)(
- connectionConfig.connection.ssl.caFilePath
+ connectionConfig.connection.ssl.ca = (
+ await promisify(fs.readFile)(
+ connectionConfig.connection.ssl.caFilePath
+ )
).toString();
+ delete connectionConfig.connection.ssl.caFilePath;
}
if (connectionConfig.connection.ssl.keyFilePath) {
- connectionConfig.connection.ssl.key = await promisify(fs.readFile)(
- connectionConfig.connection.ssl.keyFilePath
+ connectionConfig.connection.ssl.key = (
+ await promisify(fs.readFile)(
+ connectionConfig.connection.ssl.keyFilePath
+ )
).toString();
+ delete connectionConfig.connection.ssl.keyFilePath;
}
if (connectionConfig.connection.ssl.certFilePath) {
- connectionConfig.connection.ssl.cert = await promisify(fs.readFile)(
- connectionConfig.connection.ssl.certFilePath
+ connectionConfig.connection.ssl.cert = (
+ await promisify(fs.readFile)(
+ connectionConfig.connection.ssl.certFilePath
+ )
).toString();
+ delete connectionConfig.connection.ssl.certFilePath;
}
}
diff --git a/packages/nocodb/src/lib/db/sql-client/lib/pg/PgClient.ts b/packages/nocodb/src/lib/db/sql-client/lib/pg/PgClient.ts
index aa26cbe8d8..19de9865a5 100644
--- a/packages/nocodb/src/lib/db/sql-client/lib/pg/PgClient.ts
+++ b/packages/nocodb/src/lib/db/sql-client/lib/pg/PgClient.ts
@@ -474,17 +474,22 @@ class PGClient extends KnexClient {
]);
}
- // if (this.connectionConfig.searchPath && this.connectionConfig.searchPath[0]) {
- await this.sqlClient.raw(
- ` CREATE SCHEMA IF NOT EXISTS ?? AUTHORIZATION ?? `,
- [
- (this.connectionConfig.searchPath &&
- this.connectionConfig.searchPath[0]) ||
- 'public',
- this.connectionConfig.connection.user,
- ]
- );
- // }
+ const schemaName = this.connectionConfig.searchPath?.[0] || 'public';
+
+ // Check schemaExists because `CREATE SCHEMA IF NOT EXISTS` requires permissions of `CREATE ON DATABASE`
+ const schemaExists = !!(
+ await this.sqlClient.raw(
+ `SELECT schema_name FROM information_schema.schemata WHERE schema_name = ?`,
+ [schemaName]
+ )
+ ).rows?.[0];
+
+ if (!schemaExists) {
+ await this.sqlClient.raw(
+ `CREATE SCHEMA IF NOT EXISTS ?? AUTHORIZATION ?? `,
+ [schemaName, this.connectionConfig.connection.user]
+ );
+ }
// this.sqlClient = knex(this.connectionConfig);
} catch (e) {
diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
index c4b1f6a1ee..14dd6bbe0d 100644
--- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
+++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
@@ -1,6 +1,7 @@
import autoBind from 'auto-bind';
import groupBy from 'lodash/groupBy';
import DataLoader from 'dataloader';
+import { nocoExecute } from 'nc-help';
import {
AuditOperationSubTypes,
AuditOperationTypes,
@@ -14,8 +15,10 @@ import ejs from 'ejs';
import Validator from 'validator';
import { customAlphabet } from 'nanoid';
import DOMPurify from 'isomorphic-dompurify';
+import { getViewAndModelByAliasOrId } from '../../../../services/dbData/helpers';
import Model from '../../../../models/Model';
import Column from '../../../../models/Column';
+import Project from '../../../../models/Project';
import Filter, {
COMPARISON_OPS,
COMPARISON_SUB_OPS,
@@ -33,6 +36,7 @@ import {
invokeWebhook,
} from '../../../../meta/helpers/webhookHelpers';
import { NcError } from '../../../../meta/helpers/catchError';
+import getAst from './helpers/getAst';
import { customValidators } from './customValidators';
import formulaQueryBuilderv2 from './formulav2/formulaQueryBuilderv2';
import genRollupSelectv2 from './genRollupSelectv2';
@@ -136,6 +140,15 @@ class BaseModelSqlv2 {
const proto = await this.getProto();
data.__proto__ = proto;
}
+
+ // retrieve virtual column data as well
+ const project = await Project.get(this.model.project_id);
+ const { model, view } = await getViewAndModelByAliasOrId({
+ projectName: project.title,
+ tableName: this.model.title,
+ });
+ const { ast } = await getAst({ model, view });
+ data = await nocoExecute(ast, data, {});
return data;
}
@@ -1283,7 +1296,8 @@ class BaseModelSqlv2 {
private async getSelectQueryBuilderForFormula(
column: Column,
tableAlias?: string,
- validateFormula = false
+ validateFormula = false,
+ aliasToColumnBuilder = {}
) {
const formula = await column.getColOptions();
if (formula.error) throw new Error(`Formula error: ${formula.error}`);
@@ -1293,7 +1307,7 @@ class BaseModelSqlv2 {
this.dbDriver,
this.model,
column,
- {},
+ aliasToColumnBuilder,
tableAlias,
validateFormula
);
@@ -1517,7 +1531,7 @@ class BaseModelSqlv2 {
validateFormula,
}: {
fieldsSet?: Set;
- qb: Knex.QueryBuilder;
+ qb: Knex.QueryBuilder & Knex.QueryInterface;
columns?: Column[];
fields?: string[] | string;
extractPkAndPv?: boolean;
@@ -1525,6 +1539,8 @@ class BaseModelSqlv2 {
alias?: string;
validateFormula?: boolean;
}): Promise {
+ // keep a common object for all columns to share across all columns
+ const aliasToColumnBuilder = {};
let viewOrTableColumns: Column[] | { fk_column_id?: string }[];
const res = {};
@@ -1588,7 +1604,8 @@ class BaseModelSqlv2 {
const selectQb = await this.getSelectQueryBuilderForFormula(
qrValueColumn,
alias,
- validateFormula
+ validateFormula,
+ aliasToColumnBuilder
);
qb.select({
[column.column_name]: selectQb.builder,
@@ -1622,7 +1639,8 @@ class BaseModelSqlv2 {
const selectQb = await this.getSelectQueryBuilderForFormula(
barcodeValueColumn,
alias,
- validateFormula
+ validateFormula,
+ aliasToColumnBuilder
);
qb.select({
[column.column_name]: selectQb.builder,
@@ -1647,7 +1665,8 @@ class BaseModelSqlv2 {
const selectQb = await this.getSelectQueryBuilderForFormula(
column,
alias,
- validateFormula
+ validateFormula,
+ aliasToColumnBuilder
);
qb.select(
this.dbDriver.raw(`?? as ??`, [
@@ -1700,9 +1719,6 @@ class BaseModelSqlv2 {
await this.beforeInsert(insertObj, trx, cookie);
}
- // if ('beforeInsert' in this) {
- // await this.beforeInsert(insertObj, trx, cookie);
- // }
await this.model.getColumns();
let response;
// const driver = trx ? trx : this.dbDriver;
@@ -1774,7 +1790,7 @@ class BaseModelSqlv2 {
async delByPk(id, trx?, cookie?) {
try {
- // retrieve data for handling paramas in hook
+ // retrieve data for handling params in hook
const data = await this.readByPk(id);
await this.beforeDelete(id, trx, cookie);
const response = await this.dbDriver(this.tnPath)
@@ -1839,15 +1855,17 @@ class BaseModelSqlv2 {
await this.beforeUpdate(data, trx, cookie);
+ const prevData = await this.readByPk(id);
+
const query = this.dbDriver(this.tnPath)
.update(updateObj)
.where(await this._wherePk(id));
await this.execAndParse(query);
- const response = await this.readByPk(id);
- await this.afterUpdate(response, trx, cookie);
- return response;
+ const newData = await this.readByPk(id);
+ await this.afterUpdate(prevData, newData, trx, cookie);
+ return newData;
} catch (e) {
console.log(e);
await this.errorUpdate(e, data, trx, cookie);
@@ -2031,21 +2049,14 @@ class BaseModelSqlv2 {
rowId =
response[this.model.primaryKey.title] ||
response[this.model.primaryKey.column_name];
- await Promise.all(postInsertOps.map((f) => f()));
- // if (!trx) {
- // await driver.commit();
- // }
+ await Promise.all(postInsertOps.map((f) => f()));
await this.afterInsert(response, this.dbDriver, cookie);
return response;
} catch (e) {
console.log(e);
- // await this.errorInsert(e, data, trx, cookie);
- // if (!trx) {
- // await driver.rollback(e);
- // }
throw e;
}
}
@@ -2073,11 +2084,6 @@ class BaseModelSqlv2 {
for (const data of datas) {
await this.validate(data);
}
- // let chunkSize = 50;
- //
- // if (this.isSqlite && datas[0]) {
- // chunkSize = Math.max(1, Math.floor(999 / Object.keys(datas[0]).length));
- // }
// fallbacks to `10` if database client is sqlite
// to avoid `too many SQL variables` error
@@ -2114,6 +2120,9 @@ class BaseModelSqlv2 {
transaction = await this.dbDriver.transaction();
// await this.beforeUpdateb(updateDatas, transaction);
+ const prevData = [];
+ const newData = [];
+ const updatePkValues = [];
const res = [];
for (const d of updateDatas) {
await this.validate(d);
@@ -2122,21 +2131,24 @@ class BaseModelSqlv2 {
// pk not specified - bypass
continue;
}
+ prevData.push(await this.readByPk(pkValues));
const wherePk = await this._wherePk(pkValues);
- const response = await transaction(this.tnPath)
- .update(d)
- .where(wherePk);
- res.push(response);
+ await transaction(this.tnPath).update(d).where(wherePk);
+ res.push(wherePk);
+ updatePkValues.push(pkValues);
}
- await this.afterBulkUpdate(updateDatas.length, this.dbDriver, cookie);
- transaction.commit();
+ await transaction.commit();
+
+ for (const pkValues of updatePkValues) {
+ newData.push(await this.readByPk(pkValues));
+ }
+
+ await this.afterBulkUpdate(prevData, newData, this.dbDriver, cookie);
return res;
} catch (e) {
- if (transaction) transaction.rollback();
- // console.log(e);
- // await this.errorUpdateb(e, data, null);
+ if (transaction) await transaction.rollback();
throw e;
}
}
@@ -2146,8 +2158,8 @@ class BaseModelSqlv2 {
data,
{ cookie }: { cookie?: any } = {}
) {
- let queryResponse;
try {
+ let count = 0;
const updateData = await this.model.mapAliasToColumn(data);
await this.validate(updateData);
const pkValues = await this._extractPksValues(updateData);
@@ -2178,11 +2190,11 @@ class BaseModelSqlv2 {
);
qb.update(updateData);
- queryResponse = (await qb) as any;
+
+ count = (await qb) as any;
}
- const count = queryResponse ?? 0;
- await this.afterBulkUpdate(count, this.dbDriver, cookie);
+ await this.afterBulkUpdate(null, count, this.dbDriver, cookie, true);
return count;
} catch (e) {
@@ -2197,27 +2209,32 @@ class BaseModelSqlv2 {
ids.map((d) => this.model.mapAliasToColumn(d))
);
- transaction = await this.dbDriver.transaction();
- // await this.beforeDeleteb(ids, transaction);
-
+ const deleted = [];
const res = [];
for (const d of deleteIds) {
- if (Object.keys(d).length) {
- const response = await transaction(this.tnPath).del().where(d);
- res.push(response);
+ const pkValues = await this._extractPksValues(d);
+ if (!pkValues) {
+ // pk not specified - bypass
+ continue;
}
+ deleted.push(await this.readByPk(pkValues));
+ res.push(d);
}
- // await this.afterDeleteb(res, transaction);
- transaction.commit();
+ transaction = await this.dbDriver.transaction();
+
+ for (const d of res) {
+ await transaction(this.tnPath).del().where(d);
+ }
+
+ await transaction.commit();
- await this.afterBulkDelete(ids.length, this.dbDriver, cookie);
+ await this.afterBulkDelete(deleted, this.dbDriver, cookie);
return res;
} catch (e) {
- if (transaction) transaction.rollback();
+ if (transaction) await transaction.rollback();
console.log(e);
- // await this.errorDeleteb(e, ids);
throw e;
}
}
@@ -2249,10 +2266,12 @@ class BaseModelSqlv2 {
qb,
this.dbDriver
);
+
qb.del();
+
const count = (await qb) as any;
- await this.afterBulkDelete(count, this.dbDriver, cookie);
+ await this.afterBulkDelete(count, this.dbDriver, cookie, true);
return count;
} catch (e) {
@@ -2265,12 +2284,11 @@ class BaseModelSqlv2 {
* */
public async beforeInsert(data: any, _trx: any, req): Promise {
- await this.handleHooks('Before.insert', data, req);
+ await this.handleHooks('before.insert', null, data, req);
}
public async afterInsert(data: any, _trx: any, req): Promise {
- await this.handleHooks('After.insert', data, req);
- // if (req?.headers?.['xc-gui']) {
+ await this.handleHooks('after.insert', null, data, req);
const id = this._extractPksValues(data);
await Audit.insert({
fk_model_id: this.model.id,
@@ -2284,16 +2302,27 @@ class BaseModelSqlv2 {
ip: req?.clientIp,
user: req?.user?.email,
});
- // }
}
- public async afterBulkUpdate(count: number, _trx: any, req): Promise {
+ public async afterBulkUpdate(
+ prevData: any,
+ newData: any,
+ _trx: any,
+ req,
+ isBulkAllOperation = false
+ ): Promise {
+ let noOfUpdatedRecords = newData;
+ if (!isBulkAllOperation) {
+ noOfUpdatedRecords = newData.length;
+ await this.handleHooks('after.bulkUpdate', prevData, newData, req);
+ }
+
await Audit.insert({
fk_model_id: this.model.id,
op_type: AuditOperationTypes.DATA,
op_sub_type: AuditOperationSubTypes.BULK_UPDATE,
description: DOMPurify.sanitize(
- `${count} records bulk updated in ${this.model.title}`
+ `${noOfUpdatedRecords} records bulk updated in ${this.model.title}`
),
// details: JSON.stringify(data),
ip: req?.clientIp,
@@ -2301,13 +2330,24 @@ class BaseModelSqlv2 {
});
}
- public async afterBulkDelete(count: number, _trx: any, req): Promise {
+ public async afterBulkDelete(
+ data: any,
+ _trx: any,
+ req,
+ isBulkAllOperation = false
+ ): Promise {
+ let noOfDeletedRecords = data;
+ if (!isBulkAllOperation) {
+ noOfDeletedRecords = data.length;
+ await this.handleHooks('after.bulkDelete', null, data, req);
+ }
+
await Audit.insert({
fk_model_id: this.model.id,
op_type: AuditOperationTypes.DATA,
op_sub_type: AuditOperationSubTypes.BULK_DELETE,
description: DOMPurify.sanitize(
- `${count} records bulk deleted in ${this.model.title}`
+ `${noOfDeletedRecords} records bulk deleted in ${this.model.title}`
),
// details: JSON.stringify(data),
ip: req?.clientIp,
@@ -2316,6 +2356,8 @@ class BaseModelSqlv2 {
}
public async afterBulkInsert(data: any[], _trx: any, req): Promise {
+ await this.handleHooks('after.bulkInsert', null, data, req);
+
await Audit.insert({
fk_model_id: this.model.id,
op_type: AuditOperationTypes.DATA,
@@ -2337,12 +2379,18 @@ class BaseModelSqlv2 {
}
}
if (ignoreWebhook === undefined || ignoreWebhook === 'false') {
- await this.handleHooks('Before.update', data, req);
+ await this.handleHooks('before.update', null, data, req);
}
}
- public async afterUpdate(data: any, _trx: any, req): Promise {
- const id = this._extractPksValues(data);
+ public async afterUpdate(
+ prevData: any,
+ newData: any,
+ _trx: any,
+ req
+ ): Promise {
+ const id = this._extractPksValues(newData);
+
await Audit.insert({
fk_model_id: this.model.id,
row_id: id,
@@ -2361,16 +2409,15 @@ class BaseModelSqlv2 {
}
}
if (ignoreWebhook === undefined || ignoreWebhook === 'false') {
- await this.handleHooks('After.update', data, req);
+ await this.handleHooks('after.update', prevData, newData, req);
}
}
public async beforeDelete(data: any, _trx: any, req): Promise {
- await this.handleHooks('Before.delete', data, req);
+ await this.handleHooks('before.delete', null, data, req);
}
public async afterDelete(data: any, _trx: any, req): Promise {
- // if (req?.headers?.['xc-gui']) {
const id = req?.params?.id;
await Audit.insert({
fk_model_id: this.model.id,
@@ -2382,15 +2429,17 @@ class BaseModelSqlv2 {
ip: req?.clientIp,
user: req?.user?.email,
});
- // }
- await this.handleHooks('After.delete', data, req);
+ await this.handleHooks('after.delete', null, data, req);
}
- private async handleHooks(hookName, data, req): Promise {
+ private async handleHooks(hookName, prevData, newData, req): Promise {
const view = await View.get(this.viewId);
// handle form view data submission
- if (hookName === 'After.insert' && view.type === ViewTypes.FORM) {
+ if (
+ (hookName === 'after.insert' || hookName === 'after.bulkInsert') &&
+ view.type === ViewTypes.FORM
+ ) {
try {
const formView = await view.getView();
const { columns } = await FormView.getWithInfo(formView.fk_view_id);
@@ -2440,11 +2489,11 @@ class BaseModelSqlv2 {
.map((a) => a[0]);
if (emails?.length) {
const transformedData = _transformSubmittedFormDataForEmail(
- data,
+ newData,
formView,
filteredColumns
);
- (await NcPluginMgrv2.emailAdapter())?.mailSend({
+ (await NcPluginMgrv2.emailAdapter(false))?.mailSend({
to: emails.join(','),
subject: 'NocoDB Form',
html: ejs.render(formSubmissionEmailTemplate, {
@@ -2468,7 +2517,7 @@ class BaseModelSqlv2 {
});
for (const hook of hooks) {
if (hook.active) {
- invokeWebhook(hook, this.model, data, req?.user);
+ invokeWebhook(hook, this.model, view, prevData, newData, req?.user);
}
}
} catch (e) {
@@ -2634,6 +2683,8 @@ class BaseModelSqlv2 {
break;
}
+ const response = await this.readByPk(rowId);
+ await this.afterInsert(response, this.dbDriver, cookie);
await this.afterAddChild(rowId, childId, cookie);
}
@@ -2681,6 +2732,8 @@ class BaseModelSqlv2 {
const childTn = this.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable);
+ const prevData = await this.readByPk(rowId);
+
switch (colOptions.type) {
case RelationTypes.MANY_TO_MANY:
{
@@ -2732,6 +2785,8 @@ class BaseModelSqlv2 {
break;
}
+ const newData = await this.readByPk(rowId);
+ await this.afterUpdate(prevData, newData, this.dbDriver, cookie);
await this.afterRemoveChild(rowId, childId, cookie);
}
diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts
index 3eb91300d9..f6a93ea247 100644
--- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts
+++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts
@@ -53,7 +53,7 @@ async function _formulaQueryBuilder(
alias,
knex: XKnex,
model: Model,
- aliasToColumn = {},
+ aliasToColumn: Record Promise<{ builder: any }>> = {},
tableAlias?: string
) {
// formula may include double curly brackets in previous version
@@ -69,21 +69,25 @@ async function _formulaQueryBuilder(
switch (col.uidt) {
case UITypes.Formula:
{
- const formulOption = await col.getColOptions();
- const { builder } = await _formulaQueryBuilder(
- formulOption.formula,
- alias,
- knex,
- model,
- { ...aliasToColumn, [col.id]: null },
- tableAlias
- );
- builder.sql = '(' + builder.sql + ')';
- aliasToColumn[col.id] = builder;
+ aliasToColumn[col.id] = async () => {
+ const formulOption = await col.getColOptions();
+ const { builder } = await _formulaQueryBuilder(
+ formulOption.formula,
+ alias,
+ knex,
+ model,
+ { ...aliasToColumn, [col.id]: null },
+ tableAlias
+ );
+ builder.sql = '(' + builder.sql + ')';
+ return {
+ builder,
+ };
+ };
}
break;
case UITypes.Lookup:
- {
+ aliasToColumn[col.id] = async (): Promise => {
let aliasCount = 0;
let selectQb;
let isMany = false;
@@ -398,25 +402,27 @@ async function _formulaQueryBuilder(
}
if (selectQb)
- aliasToColumn[col.id] =
- typeof selectQb === 'function'
- ? selectQb
- : knex.raw(selectQb as any).wrap('(', ')');
+ return {
+ builder:
+ typeof selectQb === 'function'
+ ? selectQb
+ : knex.raw(selectQb as any).wrap('(', ')'),
+ };
}
- }
+ };
break;
case UITypes.Rollup:
- {
+ aliasToColumn[col.id] = async (): Promise => {
const qb = await genRollupSelectv2({
knex,
columnOptions: (await col.getColOptions()) as RollupColumn,
alias: tableAlias,
});
- aliasToColumn[col.id] = knex.raw(qb.builder).wrap('(', ')');
- }
+ return { builder: knex.raw(qb.builder).wrap('(', ')') };
+ };
break;
case UITypes.LinkToAnotherRecord:
- {
+ aliasToColumn[col.id] = async (): Promise => {
const alias = `__nc_formula_ll`;
const relation = await col.getColOptions();
// if (relation.type !== 'bt') continue;
@@ -520,19 +526,22 @@ async function _formulaQueryBuilder(
.wrap('(', ')');
}
if (selectQb)
- aliasToColumn[col.id] =
- typeof selectQb === 'function'
- ? selectQb
- : knex.raw(selectQb as any).wrap('(', ')');
- }
+ return {
+ builder:
+ typeof selectQb === 'function'
+ ? selectQb
+ : knex.raw(selectQb as any).wrap('(', ')'),
+ };
+ };
break;
default:
- aliasToColumn[col.id] = col.column_name;
+ aliasToColumn[col.id] = () =>
+ Promise.resolve({ builder: col.column_name });
break;
}
}
- const fn = (pt, a?, prevBinaryOp?) => {
+ const fn = async (pt, a?, prevBinaryOp?) => {
const colAlias = a ? ` as ${a}` : '';
pt.arguments?.forEach?.((arg) => {
if (arg.fnName) return;
@@ -558,18 +567,6 @@ async function _formulaQueryBuilder(
return fn(pt.arguments[0], a, prevBinaryOp);
}
break;
- // case 'AVG':
- // if (pt.arguments.length > 1) {
- // return fn({
- // type: 'BinaryExpression',
- // operator: '/',
- // left: {...pt, callee: {name: 'SUM'}},
- // right: {type: 'Literal', value: pt.arguments.length}
- // }, a, prevBinaryOp)
- // } else {
- // return fn(pt.arguments[0], a, prevBinaryOp)
- // }
- // break;
case 'CONCAT':
if (knex.clientType() === 'sqlite3') {
if (pt.arguments.length > 1) {
@@ -616,7 +613,7 @@ async function _formulaQueryBuilder(
break;
default:
{
- const res = mapFunctionName({
+ const res = await mapFunctionName({
pt,
knex,
alias,
@@ -631,32 +628,37 @@ async function _formulaQueryBuilder(
break;
}
- return knex.raw(
- `${pt.callee.name}(${pt.arguments
- .map((arg) => {
- const query = fn(arg).toQuery();
- if (pt.callee.name === 'CONCAT') {
- if (knex.clientType() === 'mysql2') {
- // mysql2: CONCAT() returns NULL if any argument is NULL.
- // adding IFNULL to convert NULL values to empty strings
- return `IFNULL(${query}, '')`;
- } else {
- // do nothing
- // pg / mssql: Concatenate all arguments. NULL arguments are ignored.
- // sqlite3: special handling - See BinaryExpression
- }
- }
- return query;
- })
- .join()})${colAlias}`.replace(/\?/g, '\\?')
- );
+ return {
+ builder: knex.raw(
+ `${pt.callee.name}(${(
+ await Promise.all(
+ pt.arguments.map(async (arg) => {
+ const query = (await fn(arg)).builder.toQuery();
+ if (pt.callee.name === 'CONCAT') {
+ if (knex.clientType() === 'mysql2') {
+ // mysql2: CONCAT() returns NULL if any argument is NULL.
+ // adding IFNULL to convert NULL values to empty strings
+ return `IFNULL(${query}, '')`;
+ } else {
+ // do nothing
+ // pg / mssql: Concatenate all arguments. NULL arguments are ignored.
+ // sqlite3: special handling - See BinaryExpression
+ }
+ }
+ return query;
+ })
+ )
+ ).join()})${colAlias}`.replace(/\?/g, '\\?')
+ ),
+ };
} else if (pt.type === 'Literal') {
- return knex.raw(`?${colAlias}`, [pt.value]);
+ return { builder: knex.raw(`?${colAlias}`, [pt.value]) };
} else if (pt.type === 'Identifier') {
- if (typeof aliasToColumn?.[pt.name] === 'function') {
- return knex.raw(`??${colAlias}`, aliasToColumn?.[pt.name](pt.fnName));
+ const { builder } = await aliasToColumn?.[pt.name]?.();
+ if (typeof builder === 'function') {
+ return { builder: knex.raw(`??${colAlias}`, builder(pt.fnName)) };
}
- return knex.raw(`??${colAlias}`, [aliasToColumn?.[pt.name] || pt.name]);
+ return { builder: knex.raw(`??${colAlias}`, [builder || pt.name]) };
} else if (pt.type === 'BinaryExpression') {
if (pt.operator === '==') {
pt.operator = '=';
@@ -677,8 +679,8 @@ async function _formulaQueryBuilder(
pt.left.fnName = pt.left.fnName || 'ARITH';
pt.right.fnName = pt.right.fnName || 'ARITH';
- const left = fn(pt.left, null, pt.operator).toQuery();
- const right = fn(pt.right, null, pt.operator).toQuery();
+ const left = (await fn(pt.left, null, pt.operator)).builder.toQuery();
+ const right = (await fn(pt.right, null, pt.operator)).builder.toQuery();
let sql = `${left} ${pt.operator} ${right}${colAlias}`;
// comparing a date with empty string would throw
@@ -772,7 +774,7 @@ async function _formulaQueryBuilder(
if (prevBinaryOp && pt.operator !== prevBinaryOp) {
query.wrap('(', ')');
}
- return query;
+ return { builder: query };
} else if (pt.type === 'UnaryExpression') {
const query = knex.raw(
`${pt.operator}${fn(
@@ -784,10 +786,12 @@ async function _formulaQueryBuilder(
if (prevBinaryOp && pt.operator !== prevBinaryOp) {
query.wrap('(', ')');
}
- return query;
+ return { builder: query };
}
};
- return { builder: fn(tree, alias) };
+ const builder = (await fn(tree, alias)).builder;
+
+ return { builder };
}
function getTnPath(tb: Model, knex, tableAlias?: string) {
@@ -842,7 +846,7 @@ export default async function formulaQueryBuilderv2(
// dry run qb.builder to see if it will break the grid view or not
// if so, set formula error and show empty selectQb instead
await knex(getTnPath(model, knex, tableAlias))
- .select(qb.builder)
+ .select(knex.raw(`?? as ??`, [qb.builder, '__dry_run_alias']))
.as('dry-run-only');
// if column is provided, i.e. formula has been created
diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/commonFns.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/commonFns.ts
index 2b65d2cace..22eb5ce1f0 100644
--- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/commonFns.ts
+++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/commonFns.ts
@@ -2,72 +2,96 @@ import type { MapFnArgs } from '../mapFunctionName';
export default {
// todo: handle default case
- SWITCH: (args: MapFnArgs) => {
+ SWITCH: async (args: MapFnArgs) => {
const count = Math.floor((args.pt.arguments.length - 1) / 2);
let query = '';
- const switchVal = args.fn(args.pt.arguments[0]).toQuery();
+ const switchVal = (await args.fn(args.pt.arguments[0])).builder.toQuery();
for (let i = 0; i < count; i++) {
query += args.knex
.raw(
- `\n\tWHEN ${args
- .fn(args.pt.arguments[i * 2 + 1])
- .toQuery()} THEN ${args.fn(args.pt.arguments[i * 2 + 2]).toQuery()}`
+ `\n\tWHEN ${(
+ await args.fn(args.pt.arguments[i * 2 + 1])
+ ).builder.toQuery()} THEN ${(
+ await args.fn(args.pt.arguments[i * 2 + 2])
+ ).builder.toQuery()}`
)
.toQuery();
}
if (args.pt.arguments.length % 2 === 0) {
query += args.knex
.raw(
- `\n\tELSE ${args
- .fn(args.pt.arguments[args.pt.arguments.length - 1])
- .toQuery()}`
+ `\n\tELSE ${(
+ await args.fn(args.pt.arguments[args.pt.arguments.length - 1])
+ ).builder.toQuery()}`
)
.toQuery();
}
- return args.knex.raw(`CASE ${switchVal} ${query}\n END${args.colAlias}`);
+ return {
+ builder: args.knex.raw(
+ `CASE ${switchVal} ${query}\n END${args.colAlias}`
+ ),
+ };
},
- IF: (args: MapFnArgs) => {
+ IF: async (args: MapFnArgs) => {
let query = args.knex
.raw(
- `\n\tWHEN ${args.fn(args.pt.arguments[0]).toQuery()} THEN ${args
- .fn(args.pt.arguments[1])
- .toQuery()}`
+ `\n\tWHEN ${(
+ await args.fn(args.pt.arguments[0])
+ ).builder.toQuery()} THEN ${(
+ await args.fn(args.pt.arguments[1])
+ ).builder.toQuery()}`
)
.toQuery();
if (args.pt.arguments[2]) {
query += args.knex
- .raw(`\n\tELSE ${args.fn(args.pt.arguments[2]).toQuery()}`)
+ .raw(
+ `\n\tELSE ${(await args.fn(args.pt.arguments[2])).builder.toQuery()}`
+ )
.toQuery();
}
- return args.knex.raw(`CASE ${query}\n END${args.colAlias}`);
+ return { builder: args.knex.raw(`CASE ${query}\n END${args.colAlias}`) };
},
- TRUE: (_args) => 1,
- FALSE: (_args) => 0,
- AND: (args: MapFnArgs) => {
- return args.knex.raw(
- `${args.knex
- .raw(
- `${args.pt.arguments
- .map((ar) => args.fn(ar).toQuery())
- .join(' AND ')}`
- )
- .wrap('(', ')')
- .toQuery()}${args.colAlias}`
- );
+ TRUE: 1,
+ FALSE: 0,
+ AND: async (args: MapFnArgs) => {
+ return {
+ builder: args.knex.raw(
+ `${args.knex
+ .raw(
+ `${(
+ await Promise.all(
+ args.pt.arguments.map(async (ar) =>
+ (await args.fn(ar)).builder.toQuery()
+ )
+ )
+ ).join(' AND ')}`
+ )
+ .wrap('(', ')')
+ .toQuery()}${args.colAlias}`
+ ),
+ };
},
- OR: (args: MapFnArgs) => {
- return args.knex.raw(
- `${args.knex
- .raw(
- `${args.pt.arguments.map((ar) => args.fn(ar).toQuery()).join(' OR ')}`
- )
- .wrap('(', ')')
- .toQuery()}${args.colAlias}`
- );
+ OR: async (args: MapFnArgs) => {
+ return {
+ builder: args.knex.raw(
+ `${args.knex
+ .raw(
+ `${(
+ await Promise.all(
+ args.pt.arguments.map(async (ar) =>
+ (await args.fn(ar)).builder.toQuery()
+ )
+ )
+ ).join(' OR ')}`
+ )
+ .wrap('(', ')')
+ .toQuery()}${args.colAlias}`
+ ),
+ };
},
- AVG: (args: MapFnArgs) => {
+ AVG: async (args: MapFnArgs) => {
if (args.pt.arguments.length > 1) {
return args.fn(
{
@@ -83,7 +107,9 @@ export default {
return args.fn(args.pt.arguments[0], args.a, args.prevBinaryOp);
}
},
- FLOAT: (args: MapFnArgs) => {
- return args.fn(args.pt?.arguments?.[0]).wrap('(', ')');
+ FLOAT: async (args: MapFnArgs) => {
+ return {
+ builder: (await args.fn(args.pt?.arguments?.[0])).builder.wrap('(', ')'),
+ };
},
};
diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mssql.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mssql.ts
index 105311cade..6eef4c5cad 100644
--- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mssql.ts
+++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mssql.ts
@@ -6,62 +6,78 @@ import type { MapFnArgs } from '../mapFunctionName';
const mssql = {
...commonFns,
- MIN: (args: MapFnArgs) => {
+ MIN: async (args: MapFnArgs) => {
if (args.pt.arguments.length === 1) {
return args.fn(args.pt.arguments[0]);
}
let query = '';
for (const [i, arg] of Object.entries(args.pt.arguments)) {
if (+i === args.pt.arguments.length - 1) {
- query += args.knex.raw(`\n\tElse ${args.fn(arg).toQuery()}`).toQuery();
+ query += args.knex
+ .raw(`\n\tElse ${(await args.fn(arg)).builder.toQuery()}`)
+ .toQuery();
} else {
query += args.knex
.raw(
- `\n\tWhen ${args.pt.arguments
- .filter((_, j) => +i !== j)
- .map(
- (arg1) =>
- `${args.fn(arg).toQuery()} < ${args.fn(arg1).toQuery()}`
+ `\n\tWhen ${(
+ await Promise.all(
+ args.pt.arguments
+ .filter((_, j) => +i !== j)
+ .map(
+ async (arg1) =>
+ `${(await args.fn(arg)).builder.toQuery()} < ${(
+ await args.fn(arg1)
+ ).builder.toQuery()}`
+ )
)
- .join(' And ')} Then ${args.fn(arg).toQuery()}`
+ ).join(' And ')} Then ${(await args.fn(arg)).builder.toQuery()}`
)
.toQuery();
}
}
- return args.knex.raw(`Case ${query}\n End${args.colAlias}`);
+ return { builder: args.knex.raw(`Case ${query}\n End${args.colAlias}`) };
},
- MAX: (args: MapFnArgs) => {
+ MAX: async (args: MapFnArgs) => {
if (args.pt.arguments.length === 1) {
return args.fn(args.pt.arguments[0]);
}
let query = '';
for (const [i, arg] of Object.entries(args.pt.arguments)) {
if (+i === args.pt.arguments.length - 1) {
- query += args.knex.raw(`\nElse ${args.fn(arg).toQuery()}`).toQuery();
+ query += args.knex
+ .raw(`\nElse ${(await args.fn(arg)).builder.toQuery()}`)
+ .toQuery();
} else {
query += args.knex
.raw(
`\nWhen ${args.pt.arguments
.filter((_, j) => +i !== j)
.map(
- (arg1) =>
- `${args.fn(arg).toQuery()} > ${args.fn(arg1).toQuery()}`
+ async (arg1) =>
+ `${(await args.fn(arg)).builder.toQuery()} > ${(
+ await args.fn(arg1)
+ ).builder.toQuery()}`
)
- .join(' And ')} Then ${args.fn(arg).toQuery()}`
+ .join(' And ')} Then ${(await args.fn(arg)).builder.toQuery()}`
)
.toQuery();
}
}
- return args.knex.raw(`Case ${query}\n End${args.colAlias}`);
+ return { builder: args.knex.raw(`Case ${query}\n End${args.colAlias}`) };
},
- LOG: (args: MapFnArgs) => {
- return args.knex.raw(
- `LOG(${args.pt.arguments
- .reverse()
- .map((ar) => args.fn(ar).toQuery())
- .join(',')})${args.colAlias}`
- );
+ LOG: async (args: MapFnArgs) => {
+ return {
+ builder: args.knex.raw(
+ `LOG(${(
+ await Promise.all(
+ args.pt.arguments
+ .reverse()
+ .map(async (ar) => (await args.fn(ar)).builder.toQuery())
+ )
+ ).join(',')})${args.colAlias}`
+ ),
+ };
},
MOD: (pt) => {
Object.assign(pt, {
@@ -73,91 +89,125 @@ const mssql = {
},
REPEAT: 'REPLICATE',
NOW: 'getdate',
- SEARCH: (args: MapFnArgs) => {
+ SEARCH: async (args: MapFnArgs) => {
args.pt.callee.name = 'CHARINDEX';
const temp = args.pt.arguments[0];
args.pt.arguments[0] = args.pt.arguments[1];
args.pt.arguments[1] = temp;
},
- INT: (args: MapFnArgs) => {
- return args.knex.raw(
- `CASE WHEN ISNUMERIC(${args
- .fn(args.pt.arguments[0])
- .toQuery()}) = 1 THEN FLOOR(${args
- .fn(args.pt.arguments[0])
- .toQuery()}) ELSE 0 END${args.colAlias}`
- );
+ INT: async (args: MapFnArgs) => {
+ return {
+ builder: args.knex.raw(
+ `CASE WHEN ISNUMERIC(${(
+ await args.fn(args.pt.arguments[0])
+ ).builder.toQuery()}) = 1 THEN FLOOR(${(
+ await args.fn(args.pt.arguments[0])
+ ).builder.toQuery()}) ELSE 0 END${args.colAlias}`
+ ),
+ };
},
MID: 'SUBSTR',
- FLOAT: (args: MapFnArgs) => {
- return args.knex
- .raw(`CAST(${args.fn(args.pt.arguments[0])} as FLOAT)${args.colAlias}`)
- .wrap('(', ')');
+ FLOAT: async (args: MapFnArgs) => {
+ return {
+ builder: args.knex
+ .raw(
+ `CAST(${(await args.fn(args.pt.arguments[0])).builder} as FLOAT)${
+ args.colAlias
+ }`
+ )
+ .wrap('(', ')'),
+ };
},
- DATEADD: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
- const dateIN = fn(pt.arguments[1]);
- return knex.raw(
- `CASE
- WHEN ${fn(pt.arguments[0])} LIKE '%:%' THEN
- FORMAT(DATEADD(${String(fn(pt.arguments[2])).replace(/["']/g, '')},
- ${dateIN > 0 ? '+' : ''}${fn(pt.arguments[1])}, ${fn(
- pt.arguments[0]
- )}), 'yyyy-MM-dd HH:mm')
+ DATEADD: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
+ const dateIN = (await fn(pt.arguments[1])).builder;
+ return {
+ builder: knex.raw(
+ `CASE
+ WHEN ${(await fn(pt.arguments[0])).builder} LIKE '%:%' THEN
+ FORMAT(DATEADD(${String((await fn(pt.arguments[2])).builder).replace(
+ /["']/g,
+ ''
+ )},
+ ${dateIN > 0 ? '+' : ''}${(await fn(pt.arguments[1])).builder}, ${
+ (await fn(pt.arguments[0])).builder
+ }), 'yyyy-MM-dd HH:mm')
ELSE
- FORMAT(DATEADD(${String(fn(pt.arguments[2])).replace(/["']/g, '')},
- ${dateIN > 0 ? '+' : ''}${fn(pt.arguments[1])}, ${fn(
- pt.arguments[0]
- )}), 'yyyy-MM-dd')
+ FORMAT(DATEADD(${String((await fn(pt.arguments[2])).builder).replace(
+ /["']/g,
+ ''
+ )},
+ ${dateIN > 0 ? '+' : ''}${(await fn(pt.arguments[1])).builder}, ${fn(
+ pt.arguments[0]
+ )}), 'yyyy-MM-dd')
END${colAlias}`
- );
+ ),
+ };
},
- DATETIME_DIFF: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
- const datetime_expr1 = fn(pt.arguments[0]);
- const datetime_expr2 = fn(pt.arguments[1]);
+ DATETIME_DIFF: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
+ const datetime_expr1 = (await fn(pt.arguments[0])).builder;
+ const datetime_expr2 = (await fn(pt.arguments[1])).builder;
const rawUnit = pt.arguments[2]
- ? fn(pt.arguments[2]).bindings[0]
+ ? (await fn(pt.arguments[2])).builder.bindings[0]
: 'seconds';
const unit = convertUnits(rawUnit, 'mssql');
- return knex.raw(
- `DATEDIFF(${unit}, ${datetime_expr2}, ${datetime_expr1}) ${colAlias}`
- );
+ return {
+ builder: knex.raw(
+ `DATEDIFF(${unit}, ${datetime_expr2}, ${datetime_expr1}) ${colAlias}`
+ ),
+ };
},
- WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
+ WEEKDAY: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
// DATEPART(WEEKDAY, DATE): sunday = 1, monday = 2, ..., saturday = 7
// WEEKDAY() returns an index from 0 to 6 for Monday to Sunday
- return knex.raw(
- `(DATEPART(WEEKDAY, ${
- pt.arguments[0].type === 'Literal'
- ? `'${dayjs(fn(pt.arguments[0])).format('YYYY-MM-DD')}'`
- : fn(pt.arguments[0])
- }) - 2 - ${getWeekdayByText(
- pt?.arguments[1]?.value
- )} % 7 + 7) % 7 ${colAlias}`
- );
+ return {
+ builder: knex.raw(
+ `(DATEPART(WEEKDAY, ${
+ pt.arguments[0].type === 'Literal'
+ ? `'${dayjs((await fn(pt.arguments[0])).builder).format(
+ 'YYYY-MM-DD'
+ )}'`
+ : fn(pt.arguments[0])
+ }) - 2 - ${getWeekdayByText(
+ pt?.arguments[1]?.value
+ )} % 7 + 7) % 7 ${colAlias}`
+ ),
+ };
},
- AND: (args: MapFnArgs) => {
- return args.knex.raw(
- `CASE WHEN ${args.knex
- .raw(
- `${args.pt.arguments
- .map((ar) => args.fn(ar, '', 'AND').toQuery())
- .join(' AND ')}`
- )
- .wrap('(', ')')
- .toQuery()} THEN 1 ELSE 0 END ${args.colAlias}`
- );
+ AND: async (args: MapFnArgs) => {
+ return {
+ builder: args.knex.raw(
+ `CASE WHEN ${args.knex
+ .raw(
+ `${(
+ await Promise.all(
+ args.pt.arguments.map(async (ar) =>
+ (await args.fn(ar, '', 'AND')).builder.toQuery()
+ )
+ )
+ ).join(' AND ')}`
+ )
+ .wrap('(', ')')
+ .toQuery()} THEN 1 ELSE 0 END ${args.colAlias}`
+ ),
+ };
},
- OR: (args: MapFnArgs) => {
- return args.knex.raw(
- `CASE WHEN ${args.knex
- .raw(
- `${args.pt.arguments
- .map((ar) => args.fn(ar, '', 'OR').toQuery())
- .join(' OR ')}`
- )
- .wrap('(', ')')
- .toQuery()} THEN 1 ELSE 0 END ${args.colAlias}`
- );
+ OR: async (args: MapFnArgs) => {
+ return {
+ builder: args.knex.raw(
+ `CASE WHEN ${args.knex
+ .raw(
+ `${(
+ await Promise.all(
+ args.pt.arguments.map(async (ar) =>
+ (await args.fn(ar, '', 'OR')).builder.toQuery()
+ )
+ )
+ ).join(' OR ')}`
+ )
+ .wrap('(', ')')
+ .toQuery()} THEN 1 ELSE 0 END ${args.colAlias}`
+ ),
+ };
},
};
diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mysql.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mysql.ts
index eae5c0f3f1..7844af1d05 100644
--- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mysql.ts
+++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mysql.ts
@@ -9,90 +9,110 @@ const mysql2 = {
LEN: 'CHAR_LENGTH',
MIN: 'LEAST',
MAX: 'GREATEST',
- SEARCH: (args: MapFnArgs) => {
+ SEARCH: async (args: MapFnArgs) => {
args.pt.callee.name = 'LOCATE';
const temp = args.pt.arguments[0];
args.pt.arguments[0] = args.pt.arguments[1];
args.pt.arguments[1] = temp;
},
- INT: (args: MapFnArgs) => {
- return args.knex.raw(
- `CAST(${args.fn(args.pt.arguments[0])} as SIGNED)${args.colAlias}`
- );
+ INT: async (args: MapFnArgs) => {
+ return {
+ builder: args.knex.raw(
+ `CAST(${(await args.fn(args.pt.arguments[0])).builder} as SIGNED)${
+ args.colAlias
+ }`
+ ),
+ };
},
- LEFT: (args: MapFnArgs) => {
- return args.knex.raw(
- `SUBSTR(${args.fn(args.pt.arguments[0])},1,${args.fn(
- args.pt.arguments[1]
- )})${args.colAlias}`
- );
+ LEFT: async (args: MapFnArgs) => {
+ return {
+ builder: args.knex.raw(
+ `SUBSTR(${(await args.fn(args.pt.arguments[0])).builder},1,${
+ (await args.fn(args.pt.arguments[1])).builder
+ })${args.colAlias}`
+ ),
+ };
},
- RIGHT: (args: MapFnArgs) => {
- return args.knex.raw(
- `SUBSTR(${args.fn(args.pt.arguments[0])}, -(${args.fn(
- args.pt.arguments[1]
- )}))${args.colAlias}`
- );
+ RIGHT: async (args: MapFnArgs) => {
+ return {
+ builder: args.knex.raw(
+ `SUBSTR(${(await args.fn(args.pt.arguments[0])).builder}, -(${
+ (await args.fn(args.pt.arguments[1])).builder
+ }))${args.colAlias}`
+ ),
+ };
},
MID: 'SUBSTR',
- FLOAT: (args: MapFnArgs) => {
- return args.knex
- .raw(
- `CAST(CAST(${args.fn(args.pt.arguments[0])} as CHAR) AS DOUBLE)${
- args.colAlias
- }`
- )
- .wrap('(', ')');
+ FLOAT: async (args: MapFnArgs) => {
+ return {
+ builder: args.knex
+ .raw(
+ `CAST(CAST(${
+ (await args.fn(args.pt.arguments[0])).builder
+ } as CHAR) AS DOUBLE)${args.colAlias}`
+ )
+ .wrap('(', ')'),
+ };
},
- DATEADD: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
- return knex.raw(
- `CASE
- WHEN ${fn(pt.arguments[0])} LIKE '%:%' THEN
- DATE_FORMAT(DATE_ADD(${fn(pt.arguments[0])}, INTERVAL
- ${fn(pt.arguments[1])} ${String(fn(pt.arguments[2])).replace(
- /["']/g,
- ''
- )}), '%Y-%m-%d %H:%i')
+ DATEADD: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
+ return {
+ builder: knex.raw(
+ `CASE
+ WHEN ${(await fn(pt.arguments[0])).builder} LIKE '%:%' THEN
+ DATE_FORMAT(DATE_ADD(${(await fn(pt.arguments[0])).builder}, INTERVAL
+ ${(await fn(pt.arguments[1])).builder} ${String(
+ (await fn(pt.arguments[2])).builder
+ ).replace(/["']/g, '')}), '%Y-%m-%d %H:%i')
ELSE
- DATE(DATE_ADD(${fn(pt.arguments[0])}, INTERVAL
- ${fn(pt.arguments[1])} ${String(fn(pt.arguments[2])).replace(
- /["']/g,
- ''
- )}))
+ DATE(DATE_ADD(${(await fn(pt.arguments[0])).builder}, INTERVAL
+ ${(await fn(pt.arguments[1])).builder} ${String(
+ (await fn(pt.arguments[2])).builder
+ ).replace(/["']/g, '')}))
END${colAlias}`
- );
+ ),
+ };
},
- DATETIME_DIFF: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
- const datetime_expr1 = fn(pt.arguments[0]);
- const datetime_expr2 = fn(pt.arguments[1]);
+ DATETIME_DIFF: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
+ const datetime_expr1 = (await fn(pt.arguments[0])).builder;
+ const datetime_expr2 = (await fn(pt.arguments[1])).builder;
const unit = convertUnits(
- pt.arguments[2] ? fn(pt.arguments[2]).bindings[0] : 'seconds',
+ pt.arguments[2]
+ ? (await fn(pt.arguments[2])).builder.bindings[0]
+ : 'seconds',
'mysql'
);
if (unit === 'MICROSECOND') {
// MySQL doesn't support millisecond
// hence change from MICROSECOND to millisecond manually
- return knex.raw(
- `TIMESTAMPDIFF(${unit}, ${datetime_expr2}, ${datetime_expr1}) div 1000 ${colAlias}`
- );
+ return {
+ builder: knex.raw(
+ `TIMESTAMPDIFF(${unit}, ${datetime_expr2}, ${datetime_expr1}) div 1000 ${colAlias}`
+ ),
+ };
}
- return knex.raw(
- `TIMESTAMPDIFF(${unit}, ${datetime_expr2}, ${datetime_expr1}) ${colAlias}`
- );
+ return {
+ builder: knex.raw(
+ `TIMESTAMPDIFF(${unit}, ${datetime_expr2}, ${datetime_expr1}) ${colAlias}`
+ ),
+ };
},
- WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
+ WEEKDAY: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
// WEEKDAY() returns an index from 0 to 6 for Monday to Sunday
- return knex.raw(
- `(WEEKDAY(${
- pt.arguments[0].type === 'Literal'
- ? `'${dayjs(fn(pt.arguments[0])).format('YYYY-MM-DD')}'`
- : fn(pt.arguments[0])
- }) - ${getWeekdayByText(
- pt?.arguments[1]?.value
- )} % 7 + 7) % 7 ${colAlias}`
- );
+ return {
+ builder: knex.raw(
+ `(WEEKDAY(${
+ pt.arguments[0].type === 'Literal'
+ ? `'${dayjs((await fn(pt.arguments[0])).builder).format(
+ 'YYYY-MM-DD'
+ )}'`
+ : (await fn(pt.arguments[0])).builder
+ }) - ${getWeekdayByText(
+ pt?.arguments[1]?.value
+ )} % 7 + 7) % 7 ${colAlias}`
+ ),
+ };
},
};
diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/pg.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/pg.ts
index 0efcfc9cb3..222b0751bc 100644
--- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/pg.ts
+++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/pg.ts
@@ -12,50 +12,66 @@ const pg = {
CEILING: 'ceil',
POWER: 'pow',
SQRT: 'sqrt',
- SEARCH: (args: MapFnArgs) => {
- return args.knex.raw(
- `POSITION(${args.knex.raw(
- args.fn(args.pt.arguments[1]).toQuery()
- )} in ${args.knex.raw(args.fn(args.pt.arguments[0]).toQuery())})${
- args.colAlias
- }`
- );
+ SEARCH: async (args: MapFnArgs) => {
+ return {
+ builder: args.knex.raw(
+ `POSITION(${args.knex.raw(
+ (await args.fn(args.pt.arguments[1])).builder.toQuery()
+ )} in ${args.knex
+ .raw((await args.fn(args.pt.arguments[0])).builder)
+ .toQuery()})${args.colAlias}`
+ ),
+ };
},
INT(args: MapFnArgs) {
// todo: correction
- return args.knex.raw(
- `REGEXP_REPLACE(COALESCE(${args.fn(
- args.pt.arguments[0]
- )}::character varying, '0'), '[^0-9]+|\\.[0-9]+' ,'')${args.colAlias}`
- );
+ return {
+ builder: args.knex.raw(
+ `REGEXP_REPLACE(COALESCE(${args.fn(
+ args.pt.arguments[0]
+ )}::character varying, '0'), '[^0-9]+|\\.[0-9]+' ,'')${args.colAlias}`
+ ),
+ };
},
MID: 'SUBSTR',
- FLOAT: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
- return knex
- .raw(`CAST(${fn(pt.arguments[0])} as DOUBLE PRECISION)${colAlias}`)
- .wrap('(', ')');
+ FLOAT: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
+ return {
+ builder: knex
+ .raw(
+ `CAST(${
+ (await fn(pt.arguments[0])).builder
+ } as DOUBLE PRECISION)${colAlias}`
+ )
+ .wrap('(', ')'),
+ };
},
- ROUND: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
- return knex.raw(
- `ROUND((${fn(pt.arguments[0])})::numeric, ${
- pt?.arguments[1] ? fn(pt.arguments[1]) : 0
- }) ${colAlias}`
- );
+ ROUND: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
+ return {
+ builder: knex.raw(
+ `ROUND((${(await fn(pt.arguments[0])).builder})::numeric, ${
+ pt?.arguments[1] ? (await fn(pt.arguments[1])).builder : 0
+ }) ${colAlias}`
+ ),
+ };
},
- DATEADD: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
- return knex.raw(
- `${fn(pt.arguments[0])} + (${fn(pt.arguments[1])} ||
- '${String(fn(pt.arguments[2])).replace(
+ DATEADD: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
+ return {
+ builder: knex.raw(
+ `${(await fn(pt.arguments[0])).builder} + (${
+ (await fn(pt.arguments[1])).builder
+ } ||
+ '${String((await fn(pt.arguments[2])).builder).replace(
/["']/g,
''
)}')::interval${colAlias}`
- );
+ ),
+ };
},
- DATETIME_DIFF: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
- const datetime_expr1 = fn(pt.arguments[0]);
- const datetime_expr2 = fn(pt.arguments[1]);
+ DATETIME_DIFF: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
+ const datetime_expr1 = (await fn(pt.arguments[0])).builder;
+ const datetime_expr2 = (await fn(pt.arguments[1])).builder;
const rawUnit = pt.arguments[2]
- ? fn(pt.arguments[2]).bindings[0]
+ ? (await fn(pt.arguments[2])).builder.bindings[0]
: 'seconds';
let sql;
const unit = convertUnits(rawUnit, 'pg');
@@ -99,59 +115,77 @@ const pg = {
default:
sql = '';
}
- return knex.raw(`${sql} ${colAlias}`);
+ return { builder: knex.raw(`${sql} ${colAlias}`) };
},
- WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
+ WEEKDAY: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
// isodow: the day of the week as Monday (1) to Sunday (7)
// WEEKDAY() returns an index from 0 to 6 for Monday to Sunday
- return knex.raw(
- `(EXTRACT(ISODOW FROM ${
- pt.arguments[0].type === 'Literal'
- ? `date '${dayjs(fn(pt.arguments[0])).format('YYYY-MM-DD')}'`
- : fn(pt.arguments[0])
- }) - 1 - ${getWeekdayByText(
- pt?.arguments[1]?.value
- )} % 7 + 7) ::INTEGER % 7 ${colAlias}`
- );
+ return {
+ builder: knex.raw(
+ `(EXTRACT(ISODOW FROM ${
+ pt.arguments[0].type === 'Literal'
+ ? `date '${dayjs((await fn(pt.arguments[0])).builder).format(
+ 'YYYY-MM-DD'
+ )}'`
+ : (await fn(pt.arguments[0])).builder
+ }) - 1 - ${getWeekdayByText(
+ pt?.arguments[1]?.value
+ )} % 7 + 7) ::INTEGER % 7 ${colAlias}`
+ ),
+ };
},
- AND: (args: MapFnArgs) => {
- return args.knex.raw(
- `CASE WHEN ${args.knex
- .raw(
- `${args.pt.arguments
- .map((ar) => args.fn(ar, '', 'AND').toQuery())
- .join(' AND ')}`
- )
- .wrap('(', ')')
- .toQuery()} THEN TRUE ELSE FALSE END ${args.colAlias}`
- );
+ AND: async (args: MapFnArgs) => {
+ return {
+ builder: args.knex.raw(
+ `CASE WHEN ${args.knex
+ .raw(
+ `${(
+ await Promise.all(
+ args.pt.arguments.map(async (ar) =>
+ (await args.fn(ar, '', 'AND')).builder.toQuery()
+ )
+ )
+ ).join(' AND ')}`
+ )
+ .wrap('(', ')')
+ .toQuery()} THEN TRUE ELSE FALSE END ${args.colAlias}`
+ ),
+ };
},
- OR: (args: MapFnArgs) => {
- return args.knex.raw(
- `CASE WHEN ${args.knex
- .raw(
- `${args.pt.arguments
- .map((ar) => args.fn(ar, '', 'OR').toQuery())
- .join(' OR ')}`
- )
- .wrap('(', ')')
- .toQuery()} THEN TRUE ELSE FALSE END ${args.colAlias}`
- );
+ OR: async (args: MapFnArgs) => {
+ return {
+ builder: args.knex.raw(
+ `CASE WHEN ${args.knex
+ .raw(
+ `${args.pt.arguments
+ .map(async (ar) =>
+ (await args.fn(ar, '', 'OR')).builder.toQuery()
+ )
+ .join(' OR ')}`
+ )
+ .wrap('(', ')')
+ .toQuery()} THEN TRUE ELSE FALSE END ${args.colAlias}`
+ ),
+ };
},
- SUBSTR: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
- const str = fn(pt.arguments[0]);
- const positionFrom = fn(pt.arguments[1] ?? 1);
- const numberOfCharacters = fn(pt.arguments[2] ?? '');
- return knex.raw(
- `SUBSTR(${str}::TEXT, ${positionFrom}${
- numberOfCharacters ? ', ' + numberOfCharacters : ''
- })${colAlias}`
- );
+ SUBSTR: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
+ const str = (await fn(pt.arguments[0])).builder;
+ const positionFrom = (await fn(pt.arguments[1] ?? 1)).builder;
+ const numberOfCharacters = (await fn(pt.arguments[2] ?? '')).builder;
+ return {
+ builder: knex.raw(
+ `SUBSTR(${str}::TEXT, ${positionFrom}${
+ numberOfCharacters ? ', ' + numberOfCharacters : ''
+ })${colAlias}`
+ ),
+ };
},
- MOD: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
- const x = fn(pt.arguments[0]);
- const y = fn(pt.arguments[1]);
- return knex.raw(`MOD((${x})::NUMERIC, (${y})::NUMERIC) ${colAlias}`);
+ MOD: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
+ const x = (await fn(pt.arguments[0])).builder;
+ const y = (await fn(pt.arguments[1])).builder;
+ return {
+ builder: knex.raw(`MOD((${x})::NUMERIC, (${y})::NUMERIC) ${colAlias}`),
+ };
},
};
diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/sqlite.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/sqlite.ts
index d4bfb56d82..f3c219efe5 100644
--- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/sqlite.ts
+++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/sqlite.ts
@@ -11,17 +11,25 @@ import type { MapFnArgs } from '../mapFunctionName';
const sqlite3 = {
...commonFns,
LEN: 'LENGTH',
- CEILING(args) {
- return args.knex.raw(
- `round(${args.fn(args.pt.arguments[0])} + 0.5)${args.colAlias}`
- );
+ async CEILING(args) {
+ return {
+ builder: args.knex.raw(
+ `round(${(await args.fn(args.pt.arguments[0])).builder} + 0.5)${
+ args.colAlias
+ }`
+ ),
+ };
},
- FLOOR(args) {
- return args.knex.raw(
- `round(${args.fn(args.pt.arguments[0])} - 0.5)${args.colAlias}`
- );
+ async FLOOR(args) {
+ return {
+ builder: args.knex.raw(
+ `round(${(await args.fn(args.pt.arguments[0])).builder} - 0.5)${
+ args.colAlias
+ }`
+ ),
+ };
},
- MOD: (args: MapFnArgs) => {
+ MOD: async (args: MapFnArgs) => {
return args.fn({
type: 'BinaryExpression',
operator: '%',
@@ -29,62 +37,88 @@ const sqlite3 = {
right: args.pt.arguments[1],
});
},
- REPEAT(args: MapFnArgs) {
- return args.knex.raw(
- `replace(printf('%.' || ${args.fn(
- args.pt.arguments[1]
- )} || 'c', '/'),'/',${args.fn(args.pt.arguments[0])})${args.colAlias}`
- );
+ async REPEAT(args: MapFnArgs) {
+ return {
+ builder: args.knex.raw(
+ `replace(printf('%.' || ${
+ (await args.fn(args.pt.arguments[1])).builder
+ } || 'c', '/'),'/',${(await args.fn(args.pt.arguments[0])).builder})${
+ args.colAlias
+ }`
+ ),
+ };
},
NOW: 'DATE',
SEARCH: 'INSTR',
- INT(args: MapFnArgs) {
- return args.knex.raw(
- `CAST(${args.fn(args.pt.arguments[0])} as INTEGER)${args.colAlias}`
- );
+ async INT(args: MapFnArgs) {
+ return {
+ builder: args.knex.raw(
+ `CAST(${(await args.fn(args.pt.arguments[0])).builder} as INTEGER)${
+ args.colAlias
+ }`
+ ),
+ };
},
- LEFT: (args: MapFnArgs) => {
- return args.knex.raw(
- `SUBSTR(${args.fn(args.pt.arguments[0])},1,${args.fn(
- args.pt.arguments[1]
- )})${args.colAlias}`
- );
+ LEFT: async (args: MapFnArgs) => {
+ return {
+ builder: args.knex.raw(
+ `SUBSTR(${(await args.fn(args.pt.arguments[0])).builder},1,${
+ (await args.fn(args.pt.arguments[1])).builder
+ })${args.colAlias}`
+ ),
+ };
},
- RIGHT: (args: MapFnArgs) => {
- return args.knex.raw(
- `SUBSTR(${args.fn(args.pt.arguments[0])},-(${args.fn(
- args.pt.arguments[1]
- )}))${args.colAlias}`
- );
+ RIGHT: async (args: MapFnArgs) => {
+ return {
+ builder: args.knex.raw(
+ `SUBSTR(${(await args.fn(args.pt.arguments[0])).builder},-(${
+ (await args.fn(args.pt.arguments[1])).builder
+ }))${args.colAlias}`
+ ),
+ };
},
MID: 'SUBSTR',
- FLOAT: (args: MapFnArgs) => {
- return args.knex
- .raw(`CAST(${args.fn(args.pt.arguments[0])} as FLOAT)${args.colAlias}`)
- .wrap('(', ')');
+ FLOAT: async (args: MapFnArgs) => {
+ return {
+ builder: args.knex
+ .raw(
+ `CAST(${(await args.fn(args.pt.arguments[0])).builder} as FLOAT)${
+ args.colAlias
+ }`
+ )
+ .wrap('(', ')'),
+ };
},
- DATEADD: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
- const dateIN = fn(pt.arguments[1]);
- return knex.raw(
- `CASE
- WHEN ${fn(pt.arguments[0])} LIKE '%:%' THEN
+ DATEADD: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
+ const dateIN = (await fn(pt.arguments[1])).builder;
+ return {
+ builder: knex.raw(
+ `CASE
+ WHEN ${(await fn(pt.arguments[0])).builder} LIKE '%:%' THEN
STRFTIME('%Y-%m-%d %H:%M', DATETIME(DATETIME(${fn(
pt.arguments[0]
- )}, 'localtime'),
- ${dateIN > 0 ? '+' : ''}${fn(pt.arguments[1])} || ' ${String(
- fn(pt.arguments[2])
- ).replace(/["']/g, '')}'))
- ELSE
- DATE(DATETIME(${fn(pt.arguments[0])}, 'localtime'),
- ${dateIN > 0 ? '+' : ''}${fn(pt.arguments[1])} || ' ${String(
- fn(pt.arguments[2])
- ).replace(/["']/g, '')}')
+ )}, 'localtime'),
+ ${dateIN > 0 ? '+' : ''}${
+ (await fn(pt.arguments[1])).builder
+ } || ' ${String((await fn(pt.arguments[2])).builder).replace(
+ /["']/g,
+ ''
+ )}'))
+ ELSE
+ DATE(DATETIME(${(await fn(pt.arguments[0])).builder}, 'localtime'),
+ ${dateIN > 0 ? '+' : ''}${
+ (await fn(pt.arguments[1])).builder
+ } || ' ${String((await fn(pt.arguments[2])).builder).replace(
+ /["']/g,
+ ''
+ )}')
END${colAlias}`
- );
+ ),
+ };
},
- DATETIME_DIFF: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
- let datetime_expr1 = fn(pt.arguments[0]);
- let datetime_expr2 = fn(pt.arguments[1]);
+ DATETIME_DIFF: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
+ let datetime_expr1 = (await fn(pt.arguments[0])).builder;
+ let datetime_expr2 = (await fn(pt.arguments[1])).builder;
// JULIANDAY takes YYYY-MM-DD
if (datetime_expr1.sql === '?' && datetime_expr1.bindings?.[0]) {
datetime_expr1 = `'${convertToTargetFormat(
@@ -103,7 +137,7 @@ const sqlite3 = {
}
const rawUnit = pt.arguments[2]
- ? fn(pt.arguments[2]).bindings[0]
+ ? (await fn(pt.arguments[2])).builder.bindings[0]
: 'seconds';
let sql;
const unit = convertUnits(rawUnit, 'sqlite');
@@ -130,15 +164,15 @@ const sqlite3 = {
sql = `(strftime('%Y', ${datetime_expr1}) - strftime('%Y', ${datetime_expr2})) * 4 + (strftime('%m', ${datetime_expr1}) - strftime('%m', ${datetime_expr2})) / 3`;
break;
case 'years':
- sql = `CASE
- WHEN (${datetime_expr2} < ${datetime_expr1}) THEN
+ sql = `CASE
+ WHEN (${datetime_expr2} < ${datetime_expr1}) THEN
(
(strftime('%Y', ${datetime_expr1}) - strftime('%Y', ${datetime_expr2}))
- (strftime('%m', ${datetime_expr1}) < strftime('%m', ${datetime_expr2})
OR (strftime('%m', ${datetime_expr1}) = strftime('%m', ${datetime_expr2})
AND strftime('%d', ${datetime_expr1}) < strftime('%d', ${datetime_expr2})))
)
- WHEN (${datetime_expr2} > ${datetime_expr1}) THEN
+ WHEN (${datetime_expr2} > ${datetime_expr1}) THEN
-1 * (
(strftime('%Y', ${datetime_expr2}) - strftime('%Y', ${datetime_expr1}))
- (strftime('%m', ${datetime_expr2}) < strftime('%m', ${datetime_expr1})
@@ -154,44 +188,60 @@ const sqlite3 = {
default:
sql = '';
}
- return knex.raw(`${sql} ${colAlias}`);
+ return { builder: knex.raw(`${sql} ${colAlias}`) };
},
- WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
+ WEEKDAY: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
// strftime('%w', date) - day of week 0 - 6 with Sunday == 0
// WEEKDAY() returns an index from 0 to 6 for Monday to Sunday
- return knex.raw(
- `(strftime('%w', ${
- pt.arguments[0].type === 'Literal'
- ? `'${dayjs(fn(pt.arguments[0])).format('YYYY-MM-DD')}'`
- : fn(pt.arguments[0])
- }) - 1 - ${getWeekdayByText(
- pt?.arguments[1]?.value
- )} % 7 + 7) % 7 ${colAlias}`
- );
+ return {
+ builder: knex.raw(
+ `(strftime('%w', ${
+ pt.arguments[0].type === 'Literal'
+ ? `'${dayjs((await fn(pt.arguments[0])).builder).format(
+ 'YYYY-MM-DD'
+ )}'`
+ : (await fn(pt.arguments[0])).builder
+ }) - 1 - ${getWeekdayByText(
+ pt?.arguments[1]?.value
+ )} % 7 + 7) % 7 ${colAlias}`
+ ),
+ };
},
- AND: (args: MapFnArgs) => {
- return args.knex.raw(
- `CASE WHEN ${args.knex
- .raw(
- `${args.pt.arguments
- .map((ar) => args.fn(ar, '', 'AND').toQuery())
- .join(' AND ')}`
- )
- .wrap('(', ')')
- .toQuery()} THEN 1 ELSE 0 END ${args.colAlias}`
- );
+ AND: async (args: MapFnArgs) => {
+ return {
+ builder: args.knex.raw(
+ `CASE WHEN ${args.knex
+ .raw(
+ `${(
+ await Promise.all(
+ args.pt.arguments.map(async (ar) =>
+ (await args.fn(ar, '', 'AND')).builder.toQuery()
+ )
+ )
+ ).join(' AND ')}`
+ )
+ .wrap('(', ')')
+ .toQuery()} THEN 1 ELSE 0 END ${args.colAlias}`
+ ),
+ };
},
- OR: (args: MapFnArgs) => {
- return args.knex.raw(
- `CASE WHEN ${args.knex
- .raw(
- `${args.pt.arguments
- .map((ar) => args.fn(ar, '', 'OR').toQuery())
- .join(' OR ')}`
- )
- .wrap('(', ')')
- .toQuery()} THEN 1 ELSE 0 END ${args.colAlias}`
- );
+ OR: async (args: MapFnArgs) => {
+ return {
+ builder: args.knex.raw(
+ `CASE WHEN ${args.knex
+ .raw(
+ `${(
+ await Promise.all(
+ args.pt.arguments.map(async (ar) =>
+ (await args.fn(ar, '', 'OR')).builder.toQuery()
+ )
+ )
+ ).join(' OR ')}`
+ )
+ .wrap('(', ')')
+ .toQuery()} THEN 1 ELSE 0 END ${args.colAlias}`
+ ),
+ };
},
};
diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/mapFunctionName.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/mapFunctionName.ts
index 49abd50ac2..001e82fa26 100644
--- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/mapFunctionName.ts
+++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/mapFunctionName.ts
@@ -7,16 +7,19 @@ import type { Knex } from 'knex';
export interface MapFnArgs {
pt: any;
- aliasToCol: { [alias: string]: string };
+ aliasToCol: Record<
+ string,
+ (() => Promise<{ builder: any }>) | string | undefined
+ >;
knex: XKnex;
alias: string;
a?: string;
- fn: (...args: any) => Knex.QueryBuilder | any;
+ fn: (...args: any) => Promise<{ builder: Knex.QueryBuilder | any }>;
colAlias: string;
prevBinaryOp?: any;
}
-const mapFunctionName = (args: MapFnArgs): any => {
+const mapFunctionName = async (args: MapFnArgs): Promise => {
const name = args.pt.callee.name;
let val;
diff --git a/packages/nocodb/src/lib/meta/NcMetaIOImpl.ts b/packages/nocodb/src/lib/meta/NcMetaIOImpl.ts
index 31000760c6..b04157a088 100644
--- a/packages/nocodb/src/lib/meta/NcMetaIOImpl.ts
+++ b/packages/nocodb/src/lib/meta/NcMetaIOImpl.ts
@@ -855,6 +855,9 @@ export default class NcMetaIOImpl extends NcMetaIO {
case MetaTable.HOOKS:
prefix = 'hk_';
break;
+ case MetaTable.HOOK_LOGS:
+ prefix = 'hkl_';
+ break;
case MetaTable.AUDIT:
prefix = 'adt_';
break;
diff --git a/packages/nocodb/src/lib/meta/helpers/NcPluginMgrv2.ts b/packages/nocodb/src/lib/meta/helpers/NcPluginMgrv2.ts
index 49b1bc61bd..d236eb8597 100644
--- a/packages/nocodb/src/lib/meta/helpers/NcPluginMgrv2.ts
+++ b/packages/nocodb/src/lib/meta/helpers/NcPluginMgrv2.ts
@@ -174,6 +174,7 @@ class NcPluginMgrv2 {
}
public static async emailAdapter(
+ isUserInvite = true,
ncMeta = Noco.ncMeta
): Promise {
const pluginData = await ncMeta.metaGet2(null, null, MetaTable.PLUGIN, {
@@ -181,7 +182,12 @@ class NcPluginMgrv2 {
active: true,
});
- if (!pluginData) return null;
+ if (!pluginData) {
+ // return null to show the invite link in UI
+ if (isUserInvite) return null;
+ // for webhooks, throw the error
+ throw new Error('Plugin not configured / active');
+ }
const pluginConfig = defaultPlugins.find(
(c) => c.title === pluginData.title && c.category === PluginCategory.EMAIL
@@ -205,7 +211,7 @@ class NcPluginMgrv2 {
active: true,
});
- if (!pluginData) throw new Error('Plugin not configured/active');
+ if (!pluginData) throw new Error('Plugin not configured / active');
const pluginConfig = defaultPlugins.find(
(c) => c.title === pluginData.title
diff --git a/packages/nocodb/src/lib/meta/helpers/populateSamplePayload.ts b/packages/nocodb/src/lib/meta/helpers/populateSamplePayload.ts
index 8167134b27..b1e9073710 100644
--- a/packages/nocodb/src/lib/meta/helpers/populateSamplePayload.ts
+++ b/packages/nocodb/src/lib/meta/helpers/populateSamplePayload.ts
@@ -1,4 +1,5 @@
import { RelationTypes, UITypes } from 'nocodb-sdk';
+import { v4 as uuidv4 } from 'uuid';
import View from '../../models/View';
import Column from '../../models/Column';
import Model from '../../models/Model';
@@ -6,7 +7,7 @@ import type LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColu
import type LookupColumn from '../../models/LookupColumn';
import type SelectOption from '../../models/SelectOption';
-export default async function populateSamplePayload(
+export async function populateSamplePayload(
viewOrModel: View | Model,
includeNested = false,
operation = 'insert'
@@ -42,6 +43,68 @@ export default async function populateSamplePayload(
return out;
}
+export async function populateSamplePayloadV2(
+ viewOrModel: View | Model,
+ includeNested = false,
+ operation = 'insert',
+ scope = 'records'
+) {
+ const rows = {};
+ let columns: Column[] = [];
+ let model: Model;
+ if (viewOrModel instanceof View) {
+ const viewColumns = await viewOrModel.getColumns();
+ for (const col of viewColumns) {
+ if (col.show) columns.push(await Column.get({ colId: col.fk_column_id }));
+ }
+ model = await viewOrModel.getModel();
+ await model.getColumns();
+ } else if (viewOrModel instanceof Model) {
+ columns = await viewOrModel.getColumns();
+ model = viewOrModel;
+ }
+
+ await model.getViews();
+
+ const samplePayload = {
+ type: `${scope}.after.${operation}`,
+ id: uuidv4(),
+ data: {
+ table_id: model.id,
+ table_name: model.title,
+ view_id: model.views[0].id,
+ view_name: model.views[0].title,
+ },
+ };
+
+ for (const column of columns) {
+ if (
+ !includeNested &&
+ [UITypes.LinkToAnotherRecord, UITypes.Lookup].includes(column.uidt)
+ )
+ continue;
+
+ rows[column.title] = await getSampleColumnValue(column);
+ }
+
+ let prevRows;
+ if (['update', 'bulkUpdate'].includes(operation)) {
+ prevRows = rows;
+ }
+
+ samplePayload.data = {
+ ...samplePayload.data,
+ ...(prevRows && { previous_rows: [prevRows] }),
+ ...(operation !== 'bulkInsert' && rows && { rows: [rows] }),
+ ...(operation === 'bulkInsert' &&
+ rows && {
+ row_inserted: 10,
+ }),
+ };
+
+ return samplePayload;
+}
+
async function getSampleColumnValue(column: Column): Promise {
switch (column.uidt) {
case UITypes.ID:
diff --git a/packages/nocodb/src/lib/meta/helpers/webhookHelpers.ts b/packages/nocodb/src/lib/meta/helpers/webhookHelpers.ts
index a80ac3efd3..e19673708a 100644
--- a/packages/nocodb/src/lib/meta/helpers/webhookHelpers.ts
+++ b/packages/nocodb/src/lib/meta/helpers/webhookHelpers.ts
@@ -1,10 +1,12 @@
import Handlebars from 'handlebars';
+import { v4 as uuidv4 } from 'uuid';
import Filter from '../../models/Filter';
import HookLog from '../../models/HookLog';
import NcPluginMgrv2 from './NcPluginMgrv2';
import type Model from '../../models/Model';
-import type Column from '../../models/Column';
+import type View from '../../models/View';
import type Hook from '../../models/Hook';
+import type Column from '../../models/Column';
import type { HookLogType } from 'nocodb-sdk';
import type FormView from '../../models/FormView';
@@ -133,13 +135,54 @@ export async function validateCondition(filters: Filter[], data: any) {
return isValid;
}
-export async function handleHttpWebHook(apiMeta, user, data) {
- // try {
- const req = axiosRequestMake(apiMeta, user, data);
- await require('axios')(req);
- // } catch (e) {
- // console.log(e);
- // }
+export function constructWebHookData(hook, model, view, prevData, newData) {
+ if (hook.version === 'v2') {
+ // extend in the future - currently only support records
+ const scope = 'records';
+
+ return {
+ type: `${scope}.${hook.event}.${hook.operation}`,
+ id: uuidv4(),
+ data: {
+ table_id: model.id,
+ table_name: model.title,
+ view_id: view?.id,
+ view_name: view?.title,
+ ...(prevData && {
+ previous_rows: Array.isArray(prevData) ? prevData : [prevData],
+ }),
+ ...(hook.operation !== 'bulkInsert' &&
+ newData && { rows: Array.isArray(newData) ? newData : [newData] }),
+ ...(hook.operation === 'bulkInsert' && {
+ rows_inserted: Array.isArray(newData)
+ ? newData.length
+ : newData
+ ? 1
+ : 0,
+ }),
+ },
+ };
+ }
+
+ // for v1, keep it as it is
+ return newData;
+}
+
+export async function handleHttpWebHook(
+ hook,
+ model,
+ view,
+ apiMeta,
+ user,
+ prevData,
+ newData
+) {
+ const req = axiosRequestMake(
+ apiMeta,
+ user,
+ constructWebHookData(hook, model, view, prevData, newData)
+ );
+ return require('axios')(req);
}
export function axiosRequestMake(_apiMeta, _user, data) {
@@ -203,29 +246,57 @@ export function axiosRequestMake(_apiMeta, _user, data) {
export async function invokeWebhook(
hook: Hook,
- _model: Model,
- data,
+ model: Model,
+ view: View,
+ prevData,
+ newData,
user,
testFilters = null,
- throwErrorOnFailure = false
+ throwErrorOnFailure = false,
+ testHook = false
) {
let hookLog: HookLogType;
const startTime = process.hrtime();
+ let notification;
try {
- // for (const hook of hooks) {
- const notification =
+ notification =
typeof hook.notification === 'string'
? JSON.parse(hook.notification)
: hook.notification;
+ const isBulkOperation = Array.isArray(newData);
+
+ if (isBulkOperation && notification?.type !== 'URL') {
+ // only URL hook is supported for bulk operations
+ return;
+ }
+
if (hook.condition) {
- if (
- !(await validateCondition(
- testFilters || (await hook.getFilters()),
- data
- ))
- ) {
- return;
+ if (isBulkOperation) {
+ const filteredData = [];
+ for (const data of newData) {
+ if (
+ await validateCondition(
+ testFilters || (await hook.getFilters()),
+ data
+ )
+ ) {
+ filteredData.push(data);
+ }
+ if (!filteredData.length) {
+ return;
+ }
+ newData = filteredData;
+ }
+ } else {
+ if (
+ !(await validateCondition(
+ testFilters || (await hook.getFilters()),
+ newData
+ ))
+ ) {
+ return;
+ }
}
}
@@ -233,36 +304,57 @@ export async function invokeWebhook(
case 'Email':
{
const res = await (
- await NcPluginMgrv2.emailAdapter()
+ await NcPluginMgrv2.emailAdapter(false)
)?.mailSend({
- to: parseBody(notification?.payload?.to, data),
- subject: parseBody(notification?.payload?.subject, data),
- html: parseBody(notification?.payload?.body, data),
+ to: parseBody(notification?.payload?.to, newData),
+ subject: parseBody(notification?.payload?.subject, newData),
+ html: parseBody(notification?.payload?.body, newData),
});
- hookLog = {
- ...hook,
- type: notification.type,
- payload: JSON.stringify(notification?.payload),
- response: JSON.stringify(res),
- triggered_by: user?.email,
- };
+ if (process.env.NC_AUTOMATION_LOG_LEVEL === 'ALL') {
+ hookLog = {
+ ...hook,
+ fk_hook_id: hook.id,
+ type: notification.type,
+ payload: JSON.stringify(notification?.payload),
+ response: JSON.stringify(res),
+ triggered_by: user?.email,
+ };
+ }
}
break;
case 'URL':
{
const res = await handleHttpWebHook(
+ hook,
+ model,
+ view,
notification?.payload,
user,
- data
+ prevData,
+ newData
);
- hookLog = {
- ...hook,
- type: notification.type,
- payload: JSON.stringify(notification?.payload),
- response: JSON.stringify(res),
- triggered_by: user?.email,
- };
+ if (process.env.NC_AUTOMATION_LOG_LEVEL === 'ALL') {
+ hookLog = {
+ ...hook,
+ fk_hook_id: hook.id,
+ type: notification.type,
+ payload: JSON.stringify(notification?.payload),
+ response: JSON.stringify({
+ status: res.status,
+ statusText: res.statusText,
+ headers: res.headers,
+ config: {
+ url: res.config.url,
+ method: res.config.method,
+ data: res.config.data,
+ headers: res.config.headers,
+ params: res.config.params,
+ },
+ }),
+ triggered_by: user?.email,
+ };
+ }
}
break;
default:
@@ -270,37 +362,59 @@ export async function invokeWebhook(
const res = await (
await NcPluginMgrv2.webhookNotificationAdapters(notification.type)
).sendMessage(
- parseBody(notification?.payload?.body, data),
+ parseBody(notification?.payload?.body, newData),
JSON.parse(JSON.stringify(notification?.payload), (_key, value) => {
- return typeof value === 'string' ? parseBody(value, data) : value;
+ return typeof value === 'string'
+ ? parseBody(value, newData)
+ : value;
})
);
- hookLog = {
- ...hook,
- type: notification.type,
- payload: JSON.stringify(notification?.payload),
- response: JSON.stringify(res),
- triggered_by: user?.email,
- };
+ if (process.env.NC_AUTOMATION_LOG_LEVEL === 'ALL') {
+ hookLog = {
+ ...hook,
+ fk_hook_id: hook.id,
+ type: notification.type,
+ payload: JSON.stringify(notification?.payload),
+ response: JSON.stringify({
+ status: res.status,
+ statusText: res.statusText,
+ headers: res.headers,
+ config: {
+ url: res.config.url,
+ method: res.config.method,
+ data: res.config.data,
+ headers: res.config.headers,
+ params: res.config.params,
+ },
+ }),
+ triggered_by: user?.email,
+ };
+ }
}
break;
}
} catch (e) {
console.log(e);
- hookLog = {
- ...hook,
- error_code: e.error_code,
- error_message: e.message,
- error: JSON.stringify(e),
- };
+ if (['ERROR', 'ALL'].includes(process.env.NC_AUTOMATION_LOG_LEVEL)) {
+ hookLog = {
+ ...hook,
+ type: notification.type,
+ payload: JSON.stringify(notification?.payload),
+ fk_hook_id: hook.id,
+ error_code: e.error_code,
+ error_message: e.message,
+ error: JSON.stringify(e),
+ triggered_by: user?.email,
+ };
+ }
if (throwErrorOnFailure) throw e;
} finally {
if (hookLog) {
hookLog.execution_time = parseHrtimeToMilliSeconds(
process.hrtime(startTime)
);
- HookLog.insert({ ...hookLog, test_call: !!testFilters });
+ HookLog.insert({ ...hookLog, test_call: testHook });
}
}
}
diff --git a/packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts b/packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts
index 52c877bb79..664e29ca2e 100644
--- a/packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts
+++ b/packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts
@@ -16,6 +16,7 @@ import * as nc_025_add_row_height from './v2/nc_025_add_row_height';
import * as nc_026_map_view from './v2/nc_026_map_view';
import * as nc_027_add_comparison_sub_op from './v2/nc_027_add_comparison_sub_op';
import * as nc_028_add_enable_scanner_in_form_columns_meta_table from './v2/nc_028_add_enable_scanner_in_form_columns_meta_table';
+import * as nc_029_webhook from './v2/nc_029_webhook';
// Create a custom migration source class
export default class XcMigrationSourcev2 {
@@ -43,6 +44,7 @@ export default class XcMigrationSourcev2 {
'nc_026_map_view',
'nc_027_add_comparison_sub_op',
'nc_028_add_enable_scanner_in_form_columns_meta_table',
+ 'nc_029_webhook',
]);
}
@@ -88,6 +90,8 @@ export default class XcMigrationSourcev2 {
return nc_027_add_comparison_sub_op;
case 'nc_028_add_enable_scanner_in_form_columns_meta_table':
return nc_028_add_enable_scanner_in_form_columns_meta_table;
+ case 'nc_029_webhook':
+ return nc_029_webhook;
}
}
}
diff --git a/packages/nocodb/src/lib/migrations/v2/nc_029_webhook.ts b/packages/nocodb/src/lib/migrations/v2/nc_029_webhook.ts
new file mode 100644
index 0000000000..0c0aa450b0
--- /dev/null
+++ b/packages/nocodb/src/lib/migrations/v2/nc_029_webhook.ts
@@ -0,0 +1,35 @@
+import { MetaTable } from '../../utils/globals';
+import type { Knex } from 'knex';
+
+const up = async (knex: Knex) => {
+ if (knex.client.config.client === 'mssql') {
+ await knex.schema.alterTable(MetaTable.HOOK_LOGS, (table) => {
+ table.dropColumn('response');
+ });
+ await knex.schema.alterTable(MetaTable.HOOK_LOGS, (table) => {
+ table.text('response');
+ });
+ } else if (knex.client.config.client !== 'sqlite3') {
+ await knex.schema.alterTable(MetaTable.HOOK_LOGS, (table) => {
+ table.text('response').alter();
+ });
+ }
+
+ await knex.schema.alterTable(MetaTable.HOOKS, (table) => {
+ table.string('version');
+ });
+};
+
+const down = async (knex) => {
+ if (knex.client.config.client !== 'sqlite3') {
+ await knex.schema.alterTable(MetaTable.HOOK_LOGS, (table) => {
+ table.boolean('response').alter();
+ });
+ }
+
+ await knex.schema.alterTable(MetaTable.HOOKS, (table) => {
+ table.dropColumn('version');
+ });
+};
+
+export { up, down };
diff --git a/packages/nocodb/src/lib/models/Hook.ts b/packages/nocodb/src/lib/models/Hook.ts
index 3334244e50..1b0e0761a3 100644
--- a/packages/nocodb/src/lib/models/Hook.ts
+++ b/packages/nocodb/src/lib/models/Hook.ts
@@ -7,6 +7,7 @@ import {
import Noco from '../Noco';
import NocoCache from '../cache/NocoCache';
import { extractProps } from '../meta/helpers/extractProps';
+import { NcError } from '../meta/helpers/catchError';
import Model from './Model';
import Filter from './Filter';
import HookFilter from './HookFilter';
@@ -19,8 +20,8 @@ export default class Hook implements HookType {
description?: string;
env?: string;
type?: string;
- event?: 'after' | 'before';
- operation?: 'insert' | 'delete' | 'update';
+ event?: HookType['event'];
+ operation?: HookType['operation'];
async?: BoolType;
payload?: string;
url?: string;
@@ -34,6 +35,7 @@ export default class Hook implements HookType {
project_id?: string;
base_id?: string;
+ version?: 'v1' | 'v2';
constructor(hook: Partial) {
Object.assign(this, hook);
@@ -78,8 +80,8 @@ export default class Hook implements HookType {
static async list(
param: {
fk_model_id: string;
- event?: 'after' | 'before';
- operation?: 'insert' | 'delete' | 'update';
+ event?: HookType['event'];
+ operation?: HookType['operation'];
},
ncMeta = Noco.ncMeta
) {
@@ -135,17 +137,6 @@ export default class Hook implements HookType {
'base_id',
]);
- if (insertObj.event) {
- insertObj.event = insertObj.event.toLowerCase() as 'after' | 'before';
- }
-
- if (insertObj.operation) {
- insertObj.operation = insertObj.operation.toLowerCase() as
- | 'insert'
- | 'delete'
- | 'update';
- }
-
if (insertObj.notification && typeof insertObj.notification === 'object') {
insertObj.notification = JSON.stringify(insertObj.notification);
}
@@ -156,6 +147,9 @@ export default class Hook implements HookType {
insertObj.base_id = model.base_id;
}
+ // new hook will set as version 2
+ insertObj.version = 'v2';
+
const { id } = await ncMeta.metaInsert2(
null,
null,
@@ -194,17 +188,16 @@ export default class Hook implements HookType {
'retry_interval',
'timeout',
'active',
+ 'version',
]);
- if (updateObj.event) {
- updateObj.event = updateObj.event.toLowerCase() as 'after' | 'before';
- }
-
- if (updateObj.operation) {
- updateObj.operation = updateObj.operation.toLowerCase() as
- | 'insert'
- | 'delete'
- | 'update';
+ if (
+ updateObj.version &&
+ updateObj.operation &&
+ updateObj.version === 'v1' &&
+ ['bulkInsert', 'bulkUpdate', 'bulkDelete'].includes(updateObj.operation)
+ ) {
+ NcError.badRequest(`${updateObj.operation} not supported in v1 hook`);
}
if (updateObj.notification && typeof updateObj.notification === 'object') {
diff --git a/packages/nocodb/src/lib/models/HookLog.ts b/packages/nocodb/src/lib/models/HookLog.ts
index 09f18063af..12d0ed8b0d 100644
--- a/packages/nocodb/src/lib/models/HookLog.ts
+++ b/packages/nocodb/src/lib/models/HookLog.ts
@@ -6,13 +6,12 @@ import type { HookLogType } from 'nocodb-sdk';
export default class HookLog implements HookLogType {
id?: string;
-
base_id?: string;
project_id?: string;
fk_hook_id?: string;
type?: string;
- event?: string;
- operation?: string;
+ event?: HookLogType['event'];
+ operation?: HookLogType['operation'];
test_call?: boolean;
payload?: string;
conditions?: string;
@@ -24,47 +23,49 @@ export default class HookLog implements HookLogType {
response?: string;
triggered_by?: string;
- constructor(hook: Partial) {
- Object.assign(this, hook);
+ constructor(hookLog: Partial) {
+ Object.assign(this, hookLog);
}
static async list(
param: {
fk_hook_id: string;
- event?: 'after' | 'before';
- operation?: 'insert' | 'delete' | 'update';
+ event?: HookLogType['event'];
+ operation?: HookLogType['operation'];
+ },
+ {
+ limit = 25,
+ offset = 0,
+ }: {
+ limit?: number;
+ offset?: number;
},
ncMeta = Noco.ncMeta
) {
- // todo: redis cache ??
- // let hooks = await NocoCache.getList(CacheScope.HOOK, [param.fk_model_id]);
- // if (!hooks.length) {
- const hookLogs = await ncMeta.metaList(null, null, MetaTable.HOOK_LOGS, {
+ const hookLogs = await ncMeta.metaList2(null, null, MetaTable.HOOK_LOGS, {
condition: {
fk_hook_id: param.fk_hook_id,
- // ...(param.event ? { event: param.event?.toLowerCase?.() } : {}),
- // ...(param.operation
- // ? { operation: param.operation?.toLowerCase?.() }
- // : {})
},
+ ...(process.env.NC_AUTOMATION_LOG_LEVEL === 'ERROR' && {
+ xcCondition: {
+ error_message: {
+ neq: null,
+ },
+ },
+ }),
+ orderBy: {
+ created_at: 'desc',
+ },
+ limit,
+ offset,
});
- // await NocoCache.setList(CacheScope.HOOK, [param.fk_model_id], hooks);
- // }
- // // filter event & operation
- // if (param.event) {
- // hooks = hooks.filter(
- // h => h.event?.toLowerCase() === param.event?.toLowerCase()
- // );
- // }
- // if (param.operation) {
- // hooks = hooks.filter(
- // h => h.operation?.toLowerCase() === param.operation?.toLowerCase()
- // );
- // }
return hookLogs?.map((h) => new HookLog(h));
}
public static async insert(hookLog: Partial, ncMeta = Noco.ncMeta) {
+ if (process.env.NC_AUTOMATION_LOG_LEVEL === 'OFF') {
+ return;
+ }
const insertObj: any = extractProps(hookLog, [
'base_id',
'project_id',
@@ -98,4 +99,21 @@ export default class HookLog implements HookLogType {
return await ncMeta.metaInsert2(null, null, MetaTable.HOOK_LOGS, insertObj);
}
+
+ public static async count(
+ { hookId }: { hookId?: string },
+ ncMeta = Noco.ncMeta
+ ) {
+ const qb = ncMeta.knex(MetaTable.HOOK_LOGS);
+
+ if (hookId) {
+ qb.where(`${MetaTable.HOOK_LOGS}.fk_hook_id`, hookId);
+ }
+
+ if (process.env.NC_AUTOMATION_LOG_LEVEL === 'ERROR') {
+ qb.whereNotNull(`${MetaTable.HOOK_LOGS}.error_message`);
+ }
+
+ return (await qb.count('id', { as: 'count' }).first())?.count ?? 0;
+ }
}
diff --git a/packages/nocodb/src/lib/plugins/discord/Discord.ts b/packages/nocodb/src/lib/plugins/discord/Discord.ts
index 6bfbedd952..a83f522503 100644
--- a/packages/nocodb/src/lib/plugins/discord/Discord.ts
+++ b/packages/nocodb/src/lib/plugins/discord/Discord.ts
@@ -9,11 +9,12 @@ export default class Discord implements IWebhookNotificationAdapter {
public async sendMessage(content: string, payload: any): Promise {
for (const { webhook_url } of payload?.channels) {
try {
- await axios.post(webhook_url, {
+ return await axios.post(webhook_url, {
content,
});
} catch (e) {
console.log(e);
+ throw e;
}
}
}
diff --git a/packages/nocodb/src/lib/plugins/mattermost/Mattermost.ts b/packages/nocodb/src/lib/plugins/mattermost/Mattermost.ts
index 7be83b32c8..65bfa902b7 100644
--- a/packages/nocodb/src/lib/plugins/mattermost/Mattermost.ts
+++ b/packages/nocodb/src/lib/plugins/mattermost/Mattermost.ts
@@ -9,11 +9,12 @@ export default class Mattermost implements IWebhookNotificationAdapter {
public async sendMessage(text: string, payload: any): Promise {
for (const { webhook_url } of payload?.channels) {
try {
- await axios.post(webhook_url, {
+ return await axios.post(webhook_url, {
text,
});
} catch (e) {
console.log(e);
+ throw e;
}
}
}
diff --git a/packages/nocodb/src/lib/plugins/slack/Slack.ts b/packages/nocodb/src/lib/plugins/slack/Slack.ts
index fbdaac6be4..2e2ebcf0dc 100644
--- a/packages/nocodb/src/lib/plugins/slack/Slack.ts
+++ b/packages/nocodb/src/lib/plugins/slack/Slack.ts
@@ -9,11 +9,12 @@ export default class Slack implements IWebhookNotificationAdapter {
public async sendMessage(text: string, payload: any): Promise {
for (const { webhook_url } of payload?.channels) {
try {
- await axios.post(webhook_url, {
+ return await axios.post(webhook_url, {
text,
});
} catch (e) {
console.log(e);
+ throw e;
}
}
}
diff --git a/packages/nocodb/src/lib/plugins/teams/Teams.ts b/packages/nocodb/src/lib/plugins/teams/Teams.ts
index 13ca771d02..868d7d5aa2 100644
--- a/packages/nocodb/src/lib/plugins/teams/Teams.ts
+++ b/packages/nocodb/src/lib/plugins/teams/Teams.ts
@@ -9,11 +9,12 @@ export default class Teams implements IWebhookNotificationAdapter {
public async sendMessage(Text: string, payload: any): Promise {
for (const { webhook_url } of payload?.channels) {
try {
- await axios.post(webhook_url, {
+ return await axios.post(webhook_url, {
Text,
});
} catch (e) {
console.log(e);
+ throw e;
}
}
}
diff --git a/packages/nocodb/src/lib/plugins/twilio/Twilio.ts b/packages/nocodb/src/lib/plugins/twilio/Twilio.ts
index 7de848c872..fcbbbc7dcd 100644
--- a/packages/nocodb/src/lib/plugins/twilio/Twilio.ts
+++ b/packages/nocodb/src/lib/plugins/twilio/Twilio.ts
@@ -23,6 +23,7 @@ export default class Twilio implements IWebhookNotificationAdapter {
});
} catch (e) {
console.log(e);
+ throw e;
}
}
}
diff --git a/packages/nocodb/src/lib/plugins/twilioWhatsapp/TwilioWhatsapp.ts b/packages/nocodb/src/lib/plugins/twilioWhatsapp/TwilioWhatsapp.ts
index 1120fa1f35..be5290580f 100644
--- a/packages/nocodb/src/lib/plugins/twilioWhatsapp/TwilioWhatsapp.ts
+++ b/packages/nocodb/src/lib/plugins/twilioWhatsapp/TwilioWhatsapp.ts
@@ -23,6 +23,7 @@ export default class TwilioWhatsapp implements IWebhookNotificationAdapter {
});
} catch (e) {
console.log(e);
+ throw e;
}
}
}
diff --git a/packages/nocodb/src/lib/services/hook.svc.ts b/packages/nocodb/src/lib/services/hook.svc.ts
index 04d2dc5f96..7e61419d69 100644
--- a/packages/nocodb/src/lib/services/hook.svc.ts
+++ b/packages/nocodb/src/lib/services/hook.svc.ts
@@ -1,9 +1,13 @@
import { T } from 'nc-help';
import { validatePayload } from '../meta/api/helpers';
import { NcError } from '../meta/helpers/catchError';
-import { Hook, Model } from '../models';
+import { Hook, HookLog, Model } from '../models';
import { invokeWebhook } from '../meta/helpers/webhookHelpers';
-import populateSamplePayload from '../meta/helpers/populateSamplePayload';
+import {
+ populateSamplePayload,
+ populateSamplePayloadV2,
+} from '../meta/helpers/populateSamplePayload';
+import type { HookType } from 'nocodb-sdk';
import type { HookReqType, HookTestReqType } from 'nocodb-sdk';
function validateHookPayload(
@@ -26,6 +30,10 @@ export async function hookList(param: { tableId: string }) {
return await Hook.list({ fk_model_id: param.tableId });
}
+export async function hookLogList(param: { query: any; hookId: string }) {
+ return await HookLog.list({ fk_hook_id: param.hookId }, param.query);
+}
+
export async function hookCreate(param: {
tableId: string;
hook: HookReqType;
@@ -73,29 +81,44 @@ export async function hookTest(param: {
const model = await Model.getByIdOrName({ id: param.tableId });
+ T.emit('evt', { evt_type: 'webhooks:tested' });
+
const {
hook,
payload: { data, user },
} = param.hookTest;
- await invokeWebhook(
- new Hook(hook),
- model,
- data,
- user,
- (hook as any)?.filters,
- true
- );
-
- T.emit('evt', { evt_type: 'webhooks:tested' });
+ try {
+ await invokeWebhook(
+ new Hook(hook),
+ model,
+ null,
+ null,
+ data,
+ user,
+ (hook as any)?.filters,
+ true,
+ true
+ );
+ } catch (e) {
+ throw e;
+ }
return true;
}
export async function tableSampleData(param: {
tableId: string;
- operation: 'insert' | 'update';
+ operation: HookType['operation'];
+ version: HookType['version'];
}) {
const model = await Model.getByIdOrName({ id: param.tableId });
- return await populateSamplePayload(model, false, param.operation);
+ if (param.version === 'v1') {
+ return await populateSamplePayload(model, false, param.operation);
+ }
+ return await populateSamplePayloadV2(model, false, param.operation);
+}
+
+export async function hookLogCount(param: { hookId: string }) {
+ return await HookLog.count({ hookId: param.hookId });
}
diff --git a/packages/nocodb/src/lib/services/util.svc.ts b/packages/nocodb/src/lib/services/util.svc.ts
index 47e5f30308..6296797040 100644
--- a/packages/nocodb/src/lib/services/util.svc.ts
+++ b/packages/nocodb/src/lib/services/util.svc.ts
@@ -56,6 +56,7 @@ export async function appInfo(param: { req: { ncSiteUrl: string } }) {
ncAttachmentFieldSize: NC_ATTACHMENT_FIELD_SIZE,
ncMaxAttachmentsAllowed: +(process.env.NC_MAX_ATTACHMENTS_ALLOWED || 10),
isCloud: process.env.NC_CLOUD === 'true',
+ automationLogLevel: process.env.NC_AUTOMATION_LOG_LEVEL || 'OFF',
};
return result;
diff --git a/packages/nocodb/src/lib/utils/NcConfigFactory.ts b/packages/nocodb/src/lib/utils/NcConfigFactory.ts
index bfd748ed0c..85c1fe1d7c 100644
--- a/packages/nocodb/src/lib/utils/NcConfigFactory.ts
+++ b/packages/nocodb/src/lib/utils/NcConfigFactory.ts
@@ -380,22 +380,25 @@ export default class NcConfigFactory implements NcConfig {
typeof dbConfig?.connection?.ssl === 'object'
) {
if (dbConfig.connection.ssl.caFilePath && !dbConfig.connection.ssl.ca) {
- dbConfig.connection.ssl.ca = await promisify(fs.readFile)(
- dbConfig.connection.ssl.caFilePath
+ dbConfig.connection.ssl.ca = (
+ await promisify(fs.readFile)(dbConfig.connection.ssl.caFilePath)
).toString();
+ delete dbConfig.connection.ssl.caFilePath;
}
if (dbConfig.connection.ssl.keyFilePath && !dbConfig.connection.ssl.key) {
- dbConfig.connection.ssl.key = await promisify(fs.readFile)(
- dbConfig.connection.ssl.keyFilePath
+ dbConfig.connection.ssl.key = (
+ await promisify(fs.readFile)(dbConfig.connection.ssl.keyFilePath)
).toString();
+ delete dbConfig.connection.ssl.keyFilePath;
}
if (
dbConfig.connection.ssl.certFilePath &&
!dbConfig.connection.ssl.cert
) {
- dbConfig.connection.ssl.cert = await promisify(fs.readFile)(
- dbConfig.connection.ssl.certFilePath
+ dbConfig.connection.ssl.cert = (
+ await promisify(fs.readFile)(dbConfig.connection.ssl.certFilePath)
).toString();
+ delete dbConfig.connection.ssl.certFilePath;
}
}
diff --git a/packages/nocodb/src/lib/v1-legacy/plugins/adapters/discord/Discord.ts b/packages/nocodb/src/lib/v1-legacy/plugins/adapters/discord/Discord.ts
index 3ec868ab9a..310e97dd4c 100644
--- a/packages/nocodb/src/lib/v1-legacy/plugins/adapters/discord/Discord.ts
+++ b/packages/nocodb/src/lib/v1-legacy/plugins/adapters/discord/Discord.ts
@@ -9,11 +9,12 @@ export default class Discord {
): Promise {
for (const { webhook_url } of webhooks) {
try {
- await axios.post(webhook_url, {
+ return await axios.post(webhook_url, {
content,
});
} catch (e) {
console.log(e);
+ throw e;
}
}
}
diff --git a/packages/nocodb/src/lib/v1-legacy/plugins/adapters/mattermost/Mattermost.ts b/packages/nocodb/src/lib/v1-legacy/plugins/adapters/mattermost/Mattermost.ts
index 4157253c54..fa0a08ef5f 100644
--- a/packages/nocodb/src/lib/v1-legacy/plugins/adapters/mattermost/Mattermost.ts
+++ b/packages/nocodb/src/lib/v1-legacy/plugins/adapters/mattermost/Mattermost.ts
@@ -9,11 +9,12 @@ export default class Mattermost {
): Promise {
for (const { webhook_url } of webhooks) {
try {
- await axios.post(webhook_url, {
+ return await axios.post(webhook_url, {
text,
});
} catch (e) {
console.log(e);
+ throw e;
}
}
}
diff --git a/packages/nocodb/src/lib/v1-legacy/plugins/adapters/slack/Slack.ts b/packages/nocodb/src/lib/v1-legacy/plugins/adapters/slack/Slack.ts
index fa53c8f6d7..5e4097c70d 100644
--- a/packages/nocodb/src/lib/v1-legacy/plugins/adapters/slack/Slack.ts
+++ b/packages/nocodb/src/lib/v1-legacy/plugins/adapters/slack/Slack.ts
@@ -9,11 +9,12 @@ export default class Slack {
): Promise {
for (const { webhook_url } of webhooks) {
try {
- await axios.post(webhook_url, {
+ return await axios.post(webhook_url, {
text,
});
} catch (e) {
console.log(e);
+ throw e;
}
}
}
diff --git a/packages/nocodb/src/lib/v1-legacy/plugins/adapters/twilio/Twilio.ts b/packages/nocodb/src/lib/v1-legacy/plugins/adapters/twilio/Twilio.ts
index 5f718ea0de..cc8bc9bd70 100644
--- a/packages/nocodb/src/lib/v1-legacy/plugins/adapters/twilio/Twilio.ts
+++ b/packages/nocodb/src/lib/v1-legacy/plugins/adapters/twilio/Twilio.ts
@@ -37,6 +37,7 @@ export default class Twilio {
});
} catch (e) {
console.log(e);
+ throw e;
}
}
}
diff --git a/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts b/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts
index 193f614d49..ebf48b75da 100644
--- a/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts
+++ b/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts
@@ -12,6 +12,7 @@ import ncDataTypesUpgrader from './ncDataTypesUpgrader';
import ncProjectUpgraderV2_0090000 from './ncProjectUpgraderV2_0090000';
import ncProjectEnvUpgrader0011045 from './ncProjectEnvUpgrader0011045';
import ncProjectEnvUpgrader from './ncProjectEnvUpgrader';
+import ncHookUpgrader from './ncHookUpgrader';
import type { NcConfig } from '../../interface/config';
import type NcMetaIO from '../meta/NcMetaIO';
@@ -46,6 +47,7 @@ export default class NcUpgrader {
{ name: '0104004', handler: ncFilterUpgrader_0104004 },
{ name: '0105002', handler: ncStickyColumnUpgrader },
{ name: '0105003', handler: ncFilterUpgrader_0105003 },
+ { name: '0105004', handler: ncHookUpgrader },
];
if (!(await ctx.ncMeta.knexConnection?.schema?.hasTable?.('nc_store'))) {
return;
diff --git a/packages/nocodb/src/lib/version-upgrader/ncHookUpgrader.ts b/packages/nocodb/src/lib/version-upgrader/ncHookUpgrader.ts
new file mode 100644
index 0000000000..41a842b4ea
--- /dev/null
+++ b/packages/nocodb/src/lib/version-upgrader/ncHookUpgrader.ts
@@ -0,0 +1,13 @@
+import { MetaTable } from '../utils/globals';
+import type { NcUpgraderCtx } from './NcUpgrader';
+
+export default async function ({ ncMeta }: NcUpgraderCtx) {
+ const actions = [];
+ const hooks = await ncMeta.metaList2(null, null, MetaTable.HOOKS);
+ for (const hook of hooks) {
+ actions.push(
+ ncMeta.metaUpdate(null, null, MetaTable.HOOKS, { version: 'v1' }, hook.id)
+ );
+ }
+ await Promise.all(actions);
+}
diff --git a/packages/nocodb/src/run/local.ts b/packages/nocodb/src/run/local.ts
new file mode 100644
index 0000000000..f06c6845b8
--- /dev/null
+++ b/packages/nocodb/src/run/local.ts
@@ -0,0 +1,18 @@
+import path from 'path';
+import cors from 'cors';
+import express from 'express';
+
+import Noco from '../lib/Noco';
+
+const server = express();
+server.enable('trust proxy');
+server.use(cors());
+server.use('/dashboard', express.static(path.join(__dirname, 'nc-gui')));
+server.set('view engine', 'ejs');
+
+(async () => {
+ const httpServer = server.listen(process.env.PORT || 8080, () => {
+ console.log(`App started successfully.\nVisit -> ${Noco.dashboardUrl}`);
+ });
+ server.use(await Noco.init({}, httpServer, server));
+})().catch((e) => console.log(e));
diff --git a/packages/nocodb/src/schema/swagger.json b/packages/nocodb/src/schema/swagger.json
index a2e22b0477..caa9c9002c 100644
--- a/packages/nocodb/src/schema/swagger.json
+++ b/packages/nocodb/src/schema/swagger.json
@@ -5934,6 +5934,92 @@
]
}
},
+ "/api/v1/db/meta/hooks/{hookId}/logs": {
+ "parameters": [
+ {
+ "schema": {
+ "$ref": "#/components/schemas/Id",
+ "example": "hk_0063k4o1frnxbr"
+ },
+ "name": "hookId",
+ "in": "path",
+ "required": true,
+ "description": "Unique Hook ID"
+ }
+ ],
+ "get": {
+ "summary": "List Hook Logs",
+ "operationId": "db-table-webhook-logs-list",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HookLogList"
+ },
+ "examples": {
+ "Example 1": {
+ "value": {
+ "list": [
+ {
+ "base_id": "ds_jxuewivwbxeum2",
+ "event": "after",
+ "execution_time": "98",
+ "fk_hook_id": "hk_035ijv5qdi97y5",
+ "id": "string",
+ "notifications": "{\"type\":\"URL\",\"payload\":{\"method\":\"POST\",\"body\":\"{{ json data }}\",\"headers\":[{}],\"parameters\":[{}],\"auth\":\"\",\"path\":\"https://webhook.site/6eb45ce5-b611-4be1-8b96-c2965755662b\"}}",
+ "operation": "insert",
+ "payload": "{\"method\":\"POST\",\"body\":\"{{ json data }}\",\"headers\":[{}],\"parameters\":[{}],\"auth\":\"\",\"path\":\"https://webhook.site/6eb45ce5-b611-4be1-8b96-c2965755662b\"}",
+ "project_id": "p_tbhl1hnycvhe5l",
+ "response": "{\"status\":200,\"statusText\":\"OK\",\"headers\":{\"server\":\"nginx\",\"content-type\":\"text/plain; charset=UTF-8\",\"transfer-encoding\":\"chunked\",\"connection\":\"close\",\"vary\":\"Accept-Encoding\",\"x-request-id\":\"53844a7d-ede8-4798-adf7-8af441908a72\",\"x-token-id\":\"6eb45ce5-b611-4be1-8b96-c2965755662b\",\"cache-control\":\"no-cache, private\",\"date\":\"Fri, 24 Mar 2023 10:50:10 GMT\"},\"config\":{\"url\":\"https://webhook.site/6eb45ce5-b611-4be1-8b96-c2965755662b\",\"method\":\"post\",\"data\":\"{\\\"type\\\":\\\"records.after.insert\\\",\\\"id\\\":\\\"a77d97dc-a3e4-4719-9b46-45f93e0cc99a\\\",\\\"data\\\":{\\\"table_id\\\":\\\"md_d8v403o74mf5lf\\\",\\\"table_name\\\":\\\"Sheet-2\\\"}}\",\"headers\":{\"Accept\":\"application/json, text/plain, */*\",\"Content-Type\":\"application/x-www-form-urlencoded\",\"User-Agent\":\"axios/0.21.4\",\"Content-Length\":138},\"params\":{}}}",
+ "test_call": 0,
+ "triggered_by": "w@nocodb.com",
+ "type": "URL"
+ }
+ ],
+ "pageInfo": {
+ "isFirstPage": true,
+ "isLastPage": true,
+ "page": 1,
+ "pageSize": 10,
+ "totalRows": 1
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ }
+ },
+ "tags": ["DB Table Webhook Logs"],
+ "description": "List the log data in a given Hook",
+ "parameters": [
+ {
+ "schema": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "in": "query",
+ "name": "limit"
+ },
+ {
+ "schema": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "in": "query",
+ "name": "offset"
+ },
+ {
+ "$ref": "#/components/parameters/xc-auth"
+ }
+ ]
+ }
+ },
"/api/v1/db/meta/filters/{filterId}": {
"parameters": [
{
@@ -12060,7 +12146,7 @@
]
}
},
- "/api/v1/db/meta/tables/{tableId}/hooks/samplePayload/{operation}": {
+ "/api/v1/db/meta/tables/{tableId}/hooks/samplePayload/{operation}/{version}": {
"parameters": [
{
"schema": {
@@ -12076,11 +12162,29 @@
{
"schema": {
"type": "string",
- "enum": ["update", "delete", "insert"]
+ "enum": [
+ "insert",
+ "update",
+ "delete",
+ "bulkInsert",
+ "bulkUpdate",
+ "bulkDelete"
+ ]
},
"name": "operation",
"in": "path",
- "required": true
+ "required": true,
+ "description": "Hook Operation"
+ },
+ {
+ "schema": {
+ "type": "string",
+ "enum": ["v1", "v2"]
+ },
+ "name": "version",
+ "in": "path",
+ "required": true,
+ "description": "Hook Version"
}
],
"get": {
@@ -12803,6 +12907,14 @@
"x-stoplight": {
"id": "bstdkpky2131f"
}
+ },
+ "automationLogLevel": {
+ "type": "string",
+ "x-stoplight": {
+ "id": "uc3vaotye2eu8"
+ },
+ "enum": ["OFF", "ERROR", "ALL"],
+ "example": "OFF"
}
}
},
@@ -12826,7 +12938,8 @@
"ee": false,
"ncAttachmentFieldSize": 20971520,
"ncMaxAttachmentsAllowed": 10,
- "isCloud": false
+ "isCloud": false,
+ "automationLogLevel": "OFF"
}
}
}
@@ -13646,7 +13759,7 @@
}
},
"x-stoplight": {
- "id": "zc9ztw0ih4ttt"
+ "id": "95rfb0httdmw0"
}
},
"ApiTokenReq": {
@@ -13667,13 +13780,13 @@
}
},
"x-stoplight": {
- "id": "t9dpbaw4dhpx3"
+ "id": "h3g2houpa6y9y"
}
},
"ApiTokenList": {
"description": "Model for API Token List",
"x-stoplight": {
- "id": "dgoo5jfkvypld"
+ "id": "35b7cvjplxmfm"
},
"examples": [
{
@@ -13778,7 +13891,7 @@
}
},
"x-stoplight": {
- "id": "9uff8of377ouf"
+ "id": "pbx8hqgwdz7lw"
}
},
"AttachmentReq": {
@@ -13816,7 +13929,7 @@
}
},
"x-stoplight": {
- "id": "7mvkmys63d0bx"
+ "id": "jmahtr8c5rw7f"
}
},
"Audit": {
@@ -13941,7 +14054,7 @@
}
},
"x-stoplight": {
- "id": "dz9hjvx07jaje"
+ "id": "i5ug0n7j1wub8"
}
},
"AuditRowUpdateReq": {
@@ -13981,7 +14094,7 @@
}
},
"x-stoplight": {
- "id": "t4l2bqjgukcr1"
+ "id": "jcw57b1yv3xog"
}
},
"Base": {
@@ -14058,7 +14171,7 @@
}
},
"x-stoplight": {
- "id": "l5nhyvoa0dfag"
+ "id": "9hy3cgp0r2c95"
}
},
"BaseList": {
@@ -14160,7 +14273,7 @@
},
"required": ["list", "pageInfo"],
"x-stoplight": {
- "id": "pn96b18y3k0je"
+ "id": "kk8uqxeefumma"
}
},
"BaseReq": {
@@ -14216,7 +14329,7 @@
"title": "Base Request",
"type": "object",
"x-stoplight": {
- "id": "gebxq2b5pp1a3"
+ "id": "j2z6ain3204zi"
}
},
"Bool": {
@@ -14238,7 +14351,7 @@
],
"title": "Bool Model",
"x-stoplight": {
- "id": "6qmxbfclxa8em"
+ "id": "8n3vnyr22sbvj"
}
},
"Column": {
@@ -14526,7 +14639,7 @@
}
},
"x-stoplight": {
- "id": "0nszsk1q6done"
+ "id": "3a4lsn477mx4e"
}
},
"ColumnList": {
@@ -14634,7 +14747,7 @@
},
"required": ["list", "pageInfo"],
"x-stoplight": {
- "id": "fjqsqgw1ahlon"
+ "id": "rldg44w1alasg"
}
},
"ColumnReq": {
@@ -14726,7 +14839,7 @@
"title": "Column Request Model",
"type": "object",
"x-stoplight": {
- "id": "fw6an7vvhpqfu"
+ "id": "dvbn9dnme6jgi"
}
},
"CommentReq": {
@@ -14759,13 +14872,13 @@
},
"required": ["fk_model_id", "row_id"],
"x-stoplight": {
- "id": "k7vnf7oc33p6m"
+ "id": "7nwlyinuliuvy"
}
},
"CommentUpdateReq": {
"description": "Model for Comment Update Request",
"x-stoplight": {
- "id": "wan84coiq9lyn"
+ "id": "r0f3ng2rahe38"
},
"examples": [
{
@@ -14938,7 +15051,7 @@
"title": "Filter Model",
"type": "object",
"x-stoplight": {
- "id": "goytqs4ua69w0"
+ "id": "f8vsb8alix01i"
}
},
"FilterList": {
@@ -15027,9 +15140,98 @@
},
"required": ["list", "pageInfo"],
"x-stoplight": {
- "id": "wzp2nwu7k19uj"
+ "id": "mfxg43iomcoue"
}
},
+ "FilterLogList": {
+ "description": "Model for Filter Log List",
+ "x-stoplight": {
+ "id": "0voe9zdizy5qd"
+ },
+ "examples": [
+ {
+ "list": [
+ {
+ "base_id": "ds_g4ccx6e77h1dmi",
+ "comparison_op": "eq",
+ "comparison_sub_op": null,
+ "created_at": "2023-03-02 18:18:05",
+ "fk_column_id": "cl_d7ah9n2qfupgys",
+ "fk_hook_id": null,
+ "fk_parent_id": null,
+ "fk_view_id": "vw_b739e29vqmrxnf",
+ "id": "fi_xn647tpmdq8fu8",
+ "is_group": null,
+ "logical_op": "and",
+ "order": 1,
+ "project_id": "p_xm3thidrblw4n7",
+ "updated_at": "2023-03-02 18:18:05",
+ "value": "foo"
+ }
+ ],
+ "pageInfo": {
+ "isFirstPage": true,
+ "isLastPage": true,
+ "page": 1,
+ "pageSize": 10,
+ "totalRows": 1
+ }
+ }
+ ],
+ "title": "Filter Log List Model",
+ "type": "object",
+ "x-examples": {
+ "example-1": {
+ "bases": {
+ "list": [
+ {
+ "alias": "string",
+ "database": "string",
+ "host": "string",
+ "id": "string",
+ "params": "string",
+ "password": "string",
+ "port": 0,
+ "project_id": "string",
+ "ssl": "string",
+ "type": "string",
+ "url": "string",
+ "username": "string"
+ }
+ ],
+ "pageInfo": {
+ "isFirstPage": true,
+ "isLastPage": true,
+ "pageSize": 0,
+ "sort": "string",
+ "totalRows": 0
+ }
+ }
+ }
+ },
+ "properties": {
+ "list": {
+ "type": "array",
+ "x-stoplight": {
+ "id": "22sgv37ve9kxo"
+ },
+ "description": "List of filter objects",
+ "items": {
+ "$ref": "#/components/schemas/Filter",
+ "x-stoplight": {
+ "id": "ttw5rxhy83k8p"
+ }
+ }
+ },
+ "pageInfo": {
+ "$ref": "#/components/schemas/Paginated",
+ "x-stoplight": {
+ "id": "7cyrb1770mrzz"
+ }
+ }
+ },
+ "required": ["list", "pageInfo"]
+ },
"FilterReq": {
"description": "Model for Filter Request",
"examples": [
@@ -15045,7 +15247,7 @@
"title": "Filter Request Model",
"type": "object",
"x-stoplight": {
- "id": "viq9ex4d7k8k2"
+ "id": "pugjn1hobswqf"
},
"properties": {
"comparison_op": {
@@ -15266,7 +15468,7 @@
}
},
"x-stoplight": {
- "id": "bjh5qbmqyzy1k"
+ "id": "918aga51qb7w1"
}
},
"FormUpdateReq": {
@@ -15337,7 +15539,7 @@
}
},
"x-stoplight": {
- "id": "76m5jsz93rjsc"
+ "id": "zn4juts75pksq"
}
},
"FormColumn": {
@@ -15431,7 +15633,7 @@
}
},
"x-stoplight": {
- "id": "liiv7vd1hgeqf"
+ "id": "gm46pinkspvrz"
}
},
"FormColumnReq": {
@@ -15498,7 +15700,7 @@
}
},
"x-stoplight": {
- "id": "21k1kvmq14s9a"
+ "id": "nmffmb1p3wkg9"
}
},
"Formula": {
@@ -15539,7 +15741,7 @@
}
},
"x-stoplight": {
- "id": "8ahp8va0147v1"
+ "id": "hlu7yokpavxwb"
}
},
"FormulaColumnReq": {
@@ -15576,7 +15778,7 @@
}
},
"x-stoplight": {
- "id": "s3c9yjip3dank"
+ "id": "v26r0c9hlimtv"
}
},
"Gallery": {
@@ -15670,7 +15872,7 @@
"title": "Gallery Model",
"type": "object",
"x-stoplight": {
- "id": "e1o8q9fieshbc"
+ "id": "i0h7av37o4eks"
}
},
"GalleryColumn": {
@@ -15705,13 +15907,13 @@
"title": "Gallery Column Model",
"type": "object",
"x-stoplight": {
- "id": "llcstnvsjdgz0"
+ "id": "jdhvl7gb33fqo"
}
},
"GalleryUpdateReq": {
"description": "Model for Gallery View Update Request",
"x-stoplight": {
- "id": "5cxhum5vh2nr2"
+ "id": "x57k1fq4101e1"
},
"examples": [
{
@@ -15764,7 +15966,7 @@
"title": "Geo Location Model",
"type": "object",
"x-stoplight": {
- "id": "hwuzujovr9qwx"
+ "id": "685e1l91iuxhv"
}
},
"Grid": {
@@ -15874,7 +16076,117 @@
}
},
"x-stoplight": {
- "id": "xntep33dod0ys"
+ "id": "1xswddf91q5pu"
+ }
+ },
+ "Grid - copy": {
+ "description": "Model for Grid",
+ "x-stoplight": {
+ "id": "xheueli0578g0"
+ },
+ "examples": [
+ {
+ "project_id": "p_xm3thidrblw4n7",
+ "base_id": "ds_g4ccx6e77h1dmi",
+ "fk_view_id": "vw_p2jcatxz4mvcfw",
+ "row_height": 1,
+ "meta": null,
+ "columns": [
+ {
+ "id": "cl_phvuuwjrzcdo0g",
+ "base_id": "ds_g4ccx6e77h1dmi",
+ "project_id": "p_xm3thidrblw4n7",
+ "fk_model_id": "md_rsu68aqjsbyqtl",
+ "title": "Id",
+ "column_name": "id",
+ "uidt": "ID",
+ "dt": "int",
+ "np": "10",
+ "ns": "0",
+ "clen": null,
+ "cop": "1",
+ "pk": 1,
+ "pv": null,
+ "rqd": 1,
+ "un": 1,
+ "ct": "int unsigned",
+ "ai": 1,
+ "unique": 0,
+ "cdf": null,
+ "cc": "",
+ "csn": null,
+ "dtx": "specificType",
+ "dtxp": "",
+ "dtxs": "0",
+ "au": 0,
+ "validate": null,
+ "virtual": null,
+ "deleted": null,
+ "system": 0,
+ "order": 1,
+ "created_at": "2023-03-02 17:04:06",
+ "updated_at": "2023-03-02 17:04:06",
+ "meta": null
+ }
+ ]
+ }
+ ],
+ "title": "Grid Model",
+ "type": "object",
+ "properties": {
+ "id": {
+ "$ref": "#/components/schemas/Id",
+ "description": "Unique ID",
+ "x-stoplight": {
+ "id": "e3ti3fc0ocjyu"
+ }
+ },
+ "project_id": {
+ "$ref": "#/components/schemas/Id",
+ "description": "Project ID",
+ "x-stoplight": {
+ "id": "e3ti3fc0ocjyu"
+ }
+ },
+ "base_id": {
+ "$ref": "#/components/schemas/Id",
+ "description": "Base ID",
+ "x-stoplight": {
+ "id": "m8v3iyf1tidy9"
+ }
+ },
+ "fk_view_id": {
+ "$ref": "#/components/schemas/Id",
+ "description": "Foreign Key to View",
+ "x-stoplight": {
+ "id": "m8v3iyf1tidy9"
+ }
+ },
+ "row_height": {
+ "type": "number",
+ "example": 1,
+ "description": "Row Height"
+ },
+ "meta": {
+ "$ref": "#/components/schemas/Meta",
+ "x-stoplight": {
+ "id": "n8cud3jyqw5yv"
+ },
+ "description": "Meta info for Grid Model"
+ },
+ "columns": {
+ "type": "array",
+ "x-stoplight": {
+ "id": "22y0gipx2jdf8"
+ },
+ "description": "Grid View Columns",
+ "items": {
+ "$ref": "#/components/schemas/GridColumn",
+ "x-stoplight": {
+ "id": "nmzp6w3o6b24u"
+ }
+ }
+ }
}
},
"GridColumn": {
@@ -15958,7 +16270,7 @@
}
},
"x-stoplight": {
- "id": "2go3w0igl2243"
+ "id": "g06nri6t6h6ke"
}
},
"GridColumnReq": {
@@ -15996,13 +16308,13 @@
"title": "Grid Column Request Model",
"type": "object",
"x-stoplight": {
- "id": "2j3uyoabzmsrz"
+ "id": "8v9cxve0332zg"
}
},
"GridUpdateReq": {
"description": "Model for Grid View Update",
"x-stoplight": {
- "id": "2q9ya4zmyr5x7"
+ "id": "g10t5gray4bk5"
},
"examples": [
{
@@ -16052,6 +16364,9 @@
],
"title": "Hook Model",
"type": "object",
+ "x-stoplight": {
+ "id": "32blg2oqo6g7p"
+ },
"properties": {
"active": {
"$ref": "#/components/schemas/Bool",
@@ -16091,7 +16406,14 @@
"type": ["object", "string"]
},
"operation": {
- "enum": ["delete", "insert", "update"],
+ "enum": [
+ "insert",
+ "update",
+ "delete",
+ "bulkInsert",
+ "bulkUpdate",
+ "bulkDelete"
+ ],
"type": "string",
"description": "Hook Operation",
"example": "insert"
@@ -16119,16 +16441,22 @@
"type": {
"type": "string",
"description": "Hook Type"
+ },
+ "version": {
+ "type": "string",
+ "x-stoplight": {
+ "id": "socewqvadj91n"
+ },
+ "example": "v2",
+ "description": "Hook Version",
+ "enum": ["v1", "v2"]
}
- },
- "x-stoplight": {
- "id": "q8cxkpronjmyx"
}
},
"HookReq": {
"description": "Model for Hook",
"x-stoplight": {
- "id": "4zlzrppfjxrqb"
+ "id": "85vr8fx3oea31"
},
"examples": [
{
@@ -16189,7 +16517,14 @@
"type": ["object", "string"]
},
"operation": {
- "enum": ["delete", "insert", "update"],
+ "enum": [
+ "insert",
+ "update",
+ "delete",
+ "bulkInsert",
+ "bulkUpdate",
+ "bulkDelete"
+ ],
"type": "string",
"description": "Hook Operation",
"example": "insert"
@@ -16306,90 +16641,173 @@
},
"required": ["list", "pageInfo"],
"x-stoplight": {
- "id": "2tsq8h6hjctju"
+ "id": "n2sbatqu55day"
}
},
"HookLog": {
"description": "Model for Hook Log",
"examples": [
{
- "base_id": "string",
- "conditions": "string",
- "error": "string",
- "error_code": "string",
- "error_message": "string",
- "event": "string",
- "execution_time": "string",
- "fk_hook_id": "string",
+ "base_id": "ds_jxuewivwbxeum2",
+ "event": "after",
+ "execution_time": "98",
+ "fk_hook_id": "hk_035ijv5qdi97y5",
"id": "string",
- "notifications": "string",
- "operation": "string",
- "payload": null,
- "project_id": "string",
- "response": "string",
- "test_call": true,
- "triggered_by": "string",
- "type": "string"
+ "notifications": "{\"type\":\"URL\",\"payload\":{\"method\":\"POST\",\"body\":\"{{ json data }}\",\"headers\":[{}],\"parameters\":[{}],\"auth\":\"\",\"path\":\"https://webhook.site/6eb45ce5-b611-4be1-8b96-c2965755662b\"}}",
+ "operation": "insert",
+ "payload": "{\"method\":\"POST\",\"body\":\"{{ json data }}\",\"headers\":[{}],\"parameters\":[{}],\"auth\":\"\",\"path\":\"https://webhook.site/6eb45ce5-b611-4be1-8b96-c2965755662b\"}",
+ "project_id": "p_tbhl1hnycvhe5l",
+ "response": "{\"status\":200,\"statusText\":\"OK\",\"headers\":{\"server\":\"nginx\",\"content-type\":\"text/plain; charset=UTF-8\",\"transfer-encoding\":\"chunked\",\"connection\":\"close\",\"vary\":\"Accept-Encoding\",\"x-request-id\":\"53844a7d-ede8-4798-adf7-8af441908a72\",\"x-token-id\":\"6eb45ce5-b611-4be1-8b96-c2965755662b\",\"cache-control\":\"no-cache, private\",\"date\":\"Fri, 24 Mar 2023 10:50:10 GMT\"},\"config\":{\"url\":\"https://webhook.site/6eb45ce5-b611-4be1-8b96-c2965755662b\",\"method\":\"post\",\"data\":\"{\\\"type\\\":\\\"records.after.insert\\\",\\\"id\\\":\\\"a77d97dc-a3e4-4719-9b46-45f93e0cc99a\\\",\\\"data\\\":{\\\"table_id\\\":\\\"md_d8v403o74mf5lf\\\",\\\"table_name\\\":\\\"Sheet-2\\\"}}\",\"headers\":{\"Accept\":\"application/json, text/plain, */*\",\"Content-Type\":\"application/x-www-form-urlencoded\",\"User-Agent\":\"axios/0.21.4\",\"Content-Length\":138},\"params\":{}}}",
+ "test_call": 0,
+ "triggered_by": "w@nocodb.com",
+ "type": "URL"
}
],
"title": "Hook Log Model",
"type": "object",
+ "x-stoplight": {
+ "id": "ni303795b18y5"
+ },
"properties": {
"base_id": {
- "type": "string"
+ "type": "string",
+ "description": "Unique Base ID",
+ "example": "ds_jxuewivwbxeum2"
},
"conditions": {
- "type": "string"
+ "type": "string",
+ "description": "Hook Conditions"
},
"error": {
- "type": "string"
+ "$ref": "#/components/schemas/StringOrNull",
+ "description": "Error"
},
"error_code": {
- "type": "string"
+ "$ref": "#/components/schemas/StringOrNull",
+ "description": "Error Code"
},
"error_message": {
- "type": "string"
+ "$ref": "#/components/schemas/StringOrNull",
+ "description": "Error Message"
},
"event": {
- "type": "string"
+ "type": "string",
+ "description": "Hook Event",
+ "example": "after",
+ "enum": ["after", "before"]
},
"execution_time": {
- "type": "string"
+ "type": "string",
+ "description": "Execution Time in milliseconds",
+ "example": "98"
},
"fk_hook_id": {
- "$ref": "#/components/schemas/StringOrNull"
+ "$ref": "#/components/schemas/StringOrNull",
+ "description": "Foreign Key to Hook"
},
"id": {
- "$ref": "#/components/schemas/Id",
+ "$ref": "#/components/schemas/StringOrNull",
"description": "Unique ID"
},
"notifications": {
- "type": "string"
+ "type": "string",
+ "description": "Hook Notification"
},
"operation": {
- "type": "string"
+ "type": "string",
+ "description": "Hook Operation",
+ "enum": [
+ "insert",
+ "update",
+ "delete",
+ "bulkInsert",
+ "bulkUpdate",
+ "bulkDelete"
+ ],
+ "example": "insert"
+ },
+ "payload": {
+ "type": "string",
+ "description": "Hook Payload",
+ "example": "{\"method\":\"POST\",\"body\":\"{{ json data }}\",\"headers\":[{}],\"parameters\":[{}],\"auth\":\"\",\"path\":\"https://webhook.site/6eb45ce5-b611-4be1-8b96-c2965755662b\"}"
},
- "payload": {},
"project_id": {
- "type": "string"
+ "type": "string",
+ "description": "Project ID",
+ "example": "p_tbhl1hnycvhe5l"
},
"response": {
- "type": "string"
+ "$ref": "#/components/schemas/StringOrNull",
+ "description": "Hook Response"
},
"test_call": {
- "$ref": "#/components/schemas/Bool"
+ "$ref": "#/components/schemas/Bool",
+ "description": "Is this testing hook call?"
},
"triggered_by": {
- "type": "string"
+ "$ref": "#/components/schemas/StringOrNull",
+ "description": "Who triggered the hook?"
},
"type": {
- "type": "string"
+ "type": "string",
+ "example": "URL",
+ "description": "Hook Type"
}
- },
- "x-stoplight": {
- "id": "t3hlkw7zdqpom"
}
},
+ "HookLogList": {
+ "description": "Model for Hook Log List",
+ "x-stoplight": {
+ "id": "tfv7y629w2ap2"
+ },
+ "examples": [],
+ "title": "Hook Log List Model",
+ "type": "object",
+ "x-examples": {
+ "example-1": {
+ "bases": {
+ "list": [
+ {
+ "alias": "string",
+ "database": "string",
+ "host": "string",
+ "id": "string",
+ "params": "string",
+ "password": "string",
+ "port": 0,
+ "project_id": "string",
+ "ssl": "string",
+ "type": "string",
+ "url": "string",
+ "username": "string"
+ }
+ ],
+ "pageInfo": {
+ "isFirstPage": true,
+ "isLastPage": true,
+ "pageSize": 0,
+ "sort": "string",
+ "totalRows": 0
+ }
+ }
+ }
+ },
+ "properties": {
+ "list": {
+ "minItems": 1,
+ "type": "array",
+ "uniqueItems": true,
+ "description": "List of hook objects",
+ "items": {
+ "$ref": "#/components/schemas/HookLog"
+ }
+ },
+ "pageInfo": {
+ "$ref": "#/components/schemas/Paginated"
+ }
+ },
+ "required": ["list", "pageInfo"]
+ },
"HookTestReq": {
"description": "Model for Hook Test Request",
"examples": [
@@ -16441,7 +16859,7 @@
},
"required": ["hook", "payload"],
"x-stoplight": {
- "id": "p2t60nmuy6x26"
+ "id": "w64q11jiv01np"
}
},
"Id": {
@@ -16452,7 +16870,7 @@
"title": "ID Model",
"type": "string",
"x-stoplight": {
- "id": "srvzhcm5j7d65"
+ "id": "obj8r7jhyfoqg"
}
},
"Kanban": {
@@ -16519,7 +16937,7 @@
}
},
"x-stoplight": {
- "id": "1jq82nu2uugxr"
+ "id": "sfbbvgbcv7je6"
}
},
"KanbanColumn": {
@@ -16592,7 +17010,7 @@
}
},
"x-stoplight": {
- "id": "ipigcz5c7b5qy"
+ "id": "epfedw9ccxgxe"
}
},
"KanbanUpdateReq": {
@@ -16669,7 +17087,7 @@
}
},
"x-stoplight": {
- "id": "ufp4dyqfvlk6a"
+ "id": "b1jlyk12kfuwx"
}
},
"LicenseReq": {
@@ -16691,7 +17109,7 @@
"title": "License Key Request Model",
"type": "object",
"x-stoplight": {
- "id": "2hys6jqfbam2w"
+ "id": "trgk6qlmgdyfo"
}
},
"LinkToAnotherColumnReq": {
@@ -16740,7 +17158,7 @@
"title": "LinkToAnotherColumn Request Model",
"type": "object",
"x-stoplight": {
- "id": "nx1h0s2wseid0"
+ "id": "x59gz5reg12x3"
}
},
"LinkToAnotherRecord": {
@@ -16816,7 +17234,7 @@
"title": "LinkToAnotherRecord Model",
"type": "object",
"x-stoplight": {
- "id": "845623nb0zp0w"
+ "id": "27q0vnr79hxla"
}
},
"Lookup": {
@@ -16856,7 +17274,7 @@
}
},
"x-stoplight": {
- "id": "22i9gmsgqt79n"
+ "id": "guyozhka8sx1t"
}
},
"LookupColumnReq": {
@@ -16893,7 +17311,7 @@
}
},
"x-stoplight": {
- "id": "dhmss9g7ljbw7"
+ "id": "hmeckhq2ciwpf"
}
},
"Map": {
@@ -16970,13 +17388,13 @@
"title": "Map Model",
"type": "object",
"x-stoplight": {
- "id": "2gkbn859ho3ct"
+ "id": "hd60sf3k0bs3w"
}
},
"MapUpdateReq": {
"description": "Model for Map",
"x-stoplight": {
- "id": "u7xywz40dryws"
+ "id": "3hceyh9ukp5ln"
},
"examples": [
{
@@ -17051,7 +17469,7 @@
"title": "Map Column Model",
"type": "object",
"x-stoplight": {
- "id": "ftaul6uruvc4z"
+ "id": "bvk0acuepoqw1"
}
},
"Meta": {
@@ -17070,7 +17488,7 @@
],
"title": "Meta Model",
"x-stoplight": {
- "id": "pqxyr0shwtrp5"
+ "id": "2ohjoms6hjcym"
}
},
"ModelRoleVisibility": {
@@ -17113,7 +17531,7 @@
"title": "ModelRoleVisibility Model",
"type": "object",
"x-stoplight": {
- "id": "d3j1x4pulo4ng"
+ "id": "68m1dblcswem2"
}
},
"NormalColumnRequest": {
@@ -17292,7 +17710,7 @@
"type": "object",
"required": ["column_name"],
"x-stoplight": {
- "id": "uzaqliigop3nw"
+ "id": "b67hswwa49t9u"
}
},
"OrgUserReq": {
@@ -17317,7 +17735,7 @@
"title": "Organisation User Request Model",
"type": "object",
"x-stoplight": {
- "id": "pk8a8itnqcis1"
+ "id": "hs5aforkqjbkb"
}
},
"Paginated": {
@@ -17359,7 +17777,7 @@
"title": "Paginated Model",
"type": "object",
"x-stoplight": {
- "id": "a5zhehpll01cm"
+ "id": "ny0v2losvmitz"
}
},
"Password": {
@@ -17370,7 +17788,7 @@
"title": "Password Model",
"type": "string",
"x-stoplight": {
- "id": "mleq9lljoh76x"
+ "id": "c6iocwuo2j8z8"
}
},
"PasswordChangeReq": {
@@ -17394,7 +17812,7 @@
"title": "Password Change Request Model",
"type": "object",
"x-stoplight": {
- "id": "8a21c5za82w87"
+ "id": "99dogahvh0gla"
}
},
"PasswordForgotReq": {
@@ -17415,7 +17833,7 @@
"title": "Password Forgot Request Model",
"type": "object",
"x-stoplight": {
- "id": "ggt9lpf2gz2va"
+ "id": "1kkft7m54ax45"
}
},
"PasswordResetReq": {
@@ -17437,7 +17855,7 @@
"title": "Password Reset Request Model",
"type": "object",
"x-stoplight": {
- "id": "f63x3tpb8ti9r"
+ "id": "i3gzz403zjwh8"
}
},
"Plugin": {
@@ -17554,7 +17972,7 @@
}
},
"x-stoplight": {
- "id": "0atoct4o6csg1"
+ "id": "uux5n95kbh2r5"
}
},
"PluginReq": {
@@ -17578,7 +17996,7 @@
}
},
"x-stoplight": {
- "id": "w6jwfvgpbhmt0"
+ "id": "pci8zg4wgn3bx"
}
},
"PluginTestReq": {
@@ -17620,7 +18038,7 @@
},
"required": ["title", "input", "category"],
"x-stoplight": {
- "id": "yye6yekmme06k"
+ "id": "gest5kbecpjdi"
}
},
"Project": {
@@ -17710,7 +18128,7 @@
}
},
"x-stoplight": {
- "id": "xzqakuahxm8d1"
+ "id": "gae6rj3whdnj9"
}
},
"ProjectList": {
@@ -17806,7 +18224,7 @@
},
"required": ["list", "pageInfo"],
"x-stoplight": {
- "id": "rb5o2yaf8ue64"
+ "id": "xbosckfm1h9l0"
}
},
"ProjectReq": {
@@ -17859,13 +18277,13 @@
"title": "Project Request Model",
"type": "object",
"x-stoplight": {
- "id": "km2fj1ebsroys"
+ "id": "aicbp4keo57ld"
}
},
"ProjectUpdateReq": {
"description": "Model for Project Update Request",
"x-stoplight": {
- "id": "ou8zri28ak9yl"
+ "id": "83257b35r118q"
},
"examples": [
{
@@ -17930,7 +18348,7 @@
},
"required": ["email", "roles"],
"x-stoplight": {
- "id": "9pgemwq7iirkp"
+ "id": "tq2wpd2agebjz"
}
},
"Rollup": {
@@ -17979,7 +18397,7 @@
}
},
"x-stoplight": {
- "id": "rq1o1yy2htfv0"
+ "id": "vdzh0qojyn9a9"
}
},
"RollupColumnReq": {
@@ -18030,7 +18448,7 @@
}
},
"x-stoplight": {
- "id": "8yc4ashgpj37h"
+ "id": "uw9fq6lp1lone"
}
},
"SelectOption": {
@@ -18072,7 +18490,7 @@
}
},
"x-stoplight": {
- "id": "sc5hl2s3rm8x7"
+ "id": "onnh5qie1ul32"
}
},
"SelectOptions": {
@@ -18103,7 +18521,7 @@
},
"required": ["options"],
"x-stoplight": {
- "id": "r6d5rziqt288w"
+ "id": "3i68n0spduuf9"
}
},
"SharedBaseReq": {
@@ -18131,7 +18549,7 @@
}
},
"x-stoplight": {
- "id": "ybt81tfrpfvd2"
+ "id": "ep7ikmx3sudcl"
}
},
"SharedView": {
@@ -18169,7 +18587,7 @@
}
],
"x-stoplight": {
- "id": "9wxqjviu1tfd4"
+ "id": "fqfohevz2zj4u"
}
},
"SharedViewList": {
@@ -18263,7 +18681,7 @@
},
"required": ["list", "pageInfo"],
"x-stoplight": {
- "id": "jtlp6vswbc96e"
+ "id": "zdpekhmw0wvj3"
}
},
"SharedViewReq": {
@@ -18287,7 +18705,7 @@
}
},
"x-stoplight": {
- "id": "zja4lmiol065l"
+ "id": "0g3o2uo9y4h4g"
}
},
"SignInReq": {
@@ -18313,7 +18731,7 @@
"title": "Signin Request Model",
"type": "object",
"x-stoplight": {
- "id": "2m1y6fuop0adc"
+ "id": "dxxafjky40cz1"
}
},
"SignUpReq": {
@@ -18372,7 +18790,7 @@
},
"required": ["email", "password"],
"x-stoplight": {
- "id": "t7stkblz9wllz"
+ "id": "2rcbk0flkc4yu"
}
},
"Sort": {
@@ -18425,7 +18843,7 @@
}
},
"x-stoplight": {
- "id": "tlcmtmjzcyhm6"
+ "id": "3jjbe2mjzy5li"
}
},
"SortList": {
@@ -18506,7 +18924,7 @@
},
"required": ["list", "pageInfo"],
"x-stoplight": {
- "id": "7j3nbohh43tyy"
+ "id": "fsq6d3pi8b3iz"
}
},
"SortReq": {
@@ -18532,7 +18950,7 @@
}
},
"x-stoplight": {
- "id": "shzdd8k3xswo9"
+ "id": "lao7jfow2dbe7"
}
},
"StringOrNull": {
@@ -18549,7 +18967,7 @@
],
"title": "StringOrNull Model",
"x-stoplight": {
- "id": "1lecfrpmnndhi"
+ "id": "xh9dwrizrvfh6"
}
},
"Table": {
@@ -18978,7 +19396,7 @@
},
"required": ["table_name", "title"],
"x-stoplight": {
- "id": "986a3bp33neka"
+ "id": "rvdu5okgpz1xk"
}
},
"TableList": {
@@ -19108,7 +19526,7 @@
},
"required": ["list", "pageInfo"],
"x-stoplight": {
- "id": "gemdr6sjuj4gl"
+ "id": "25z7dodhqs3lc"
}
},
"TableReq": {
@@ -19249,7 +19667,7 @@
"title": "Table Request Model",
"type": "object",
"x-stoplight": {
- "id": "3i0r55tix0aso"
+ "id": "kilqakhuuzstv"
}
},
"User": {
@@ -19301,7 +19719,7 @@
},
"required": ["email", "email_verified", "firstname", "id", "lastname"],
"x-stoplight": {
- "id": "2uxbpdjvs379s"
+ "id": "nwug6wvbc5yrp"
}
},
"UserInfo": {
@@ -19345,7 +19763,7 @@
"title": "User Info Model",
"type": "object",
"x-stoplight": {
- "id": "uatcf3jcnna1t"
+ "id": "qravubcoxvr6x"
}
},
"UserList": {
@@ -19426,7 +19844,7 @@
},
"required": ["list", "pageInfo"],
"x-stoplight": {
- "id": "ygjybmp21hdgp"
+ "id": "11epslmm9jcga"
}
},
"View": {
@@ -19544,7 +19962,7 @@
},
"required": ["fk_model_id", "show", "title", "type"],
"x-stoplight": {
- "id": "66ki9gaxq3kwg"
+ "id": "pbahdgaroikdz"
}
},
"ViewList": {
@@ -19672,13 +20090,13 @@
},
"required": ["list", "pageInfo"],
"x-stoplight": {
- "id": "2iat5dhhl4hoa"
+ "id": "5wwtppugkhbeh"
}
},
"ViewCreateReq": {
"type": "object",
"x-stoplight": {
- "id": "venaxkw5uod3n"
+ "id": "lksv99846uy60"
},
"title": "ViewCreateReq",
"description": "Model for View Create Request",
@@ -19747,7 +20165,7 @@
"ViewUpdateReq": {
"description": "Model for View Update Request",
"x-stoplight": {
- "id": "bo1mzzdpk82z0"
+ "id": "1enlswadfc6p5"
},
"examples": [
{
@@ -19812,7 +20230,7 @@
"ViewColumnUpdateReq": {
"description": "Model for View Column Update Request",
"x-stoplight": {
- "id": "jmhr4prd02ssy"
+ "id": "ifzuv29axterb"
},
"examples": [
{
@@ -19841,7 +20259,7 @@
"ViewColumnReq": {
"description": "Model for View Column Request",
"x-stoplight": {
- "id": "05tr5rxotp432"
+ "id": "z4bq84nyqkhd8"
},
"examples": [
{
@@ -19932,7 +20350,7 @@
"title": "Visibility Rule Request Model",
"type": "array",
"x-stoplight": {
- "id": "c6q9v9rtiduxg"
+ "id": "vuo5q4vwtarx8"
}
}
},
diff --git a/packages/nocodb/tests/unit/model/tests/baseModelSql.test.ts b/packages/nocodb/tests/unit/model/tests/baseModelSql.test.ts
index f523c74490..16054c7f3e 100644
--- a/packages/nocodb/tests/unit/model/tests/baseModelSql.test.ts
+++ b/packages/nocodb/tests/unit/model/tests/baseModelSql.test.ts
@@ -273,7 +273,7 @@ function baseModelSqlTests() {
const deletedRow = await baseModelSql.readByPk(rowIdToDeleted);
- expect(deletedRow).to.be.undefined;
+ expect(deletedRow).to.be.an('object').that.is.empty;
console.log('Delete record', await Audit.projectAuditList(project.id, {}));
const rowDeletedAudit = (await Audit.projectAuditList(project.id, {})).find(
diff --git a/packages/nocodb/webpack.local.config.js b/packages/nocodb/webpack.local.config.js
new file mode 100644
index 0000000000..39677bd35d
--- /dev/null
+++ b/packages/nocodb/webpack.local.config.js
@@ -0,0 +1,53 @@
+const nodeExternals = require('webpack-node-externals');
+const webpack = require('webpack');
+const TerserPlugin = require('terser-webpack-plugin');
+
+const path = require('path');
+module.exports = {
+ entry: './src/run/local.ts',
+ // devtool: 'inline-source-map',
+ module: {
+ rules: [
+ {
+ test: /\.tsx?$/,
+ exclude: /node_modules/,
+ use: {
+ loader: 'ts-loader',
+ options: {
+ transpileOnly: true
+ }
+ },
+ },
+ ],
+ },
+
+ optimization: {
+ minimize: true, //Update this to true or false
+ minimizer: [new TerserPlugin()],
+ nodeEnv: false
+ },
+ externals: [nodeExternals({
+ allowlist: ['nocodb-sdk']
+ })],
+ resolve: {
+ extensions: ['.tsx', '.ts', '.js', '.json'],
+ },
+ output: {
+ filename: 'main.js',
+ path: path.resolve(__dirname, 'docker'),
+ library: 'libs',
+ libraryTarget: 'umd',
+ globalObject: "typeof self !== 'undefined' ? self : this"
+ },
+ node: {
+ fs: 'empty',
+ __dirname: false,
+ },
+ plugins: [
+ new webpack.EnvironmentPlugin([
+ 'EE'
+ ]),
+ ],
+
+ target: 'node',
+};
diff --git a/tests/playwright/pages/Dashboard/Kanban/index.ts b/tests/playwright/pages/Dashboard/Kanban/index.ts
index c6a06339a4..a48984c4f2 100644
--- a/tests/playwright/pages/Dashboard/Kanban/index.ts
+++ b/tests/playwright/pages/Dashboard/Kanban/index.ts
@@ -65,6 +65,7 @@ export class KanbanPage extends BasePage {
const stacks = await this.get().locator(`.nc-kanban-stack`).count();
for (let i = 0; i < stacks; i++) {
const stack = await this.get().locator(`.nc-kanban-stack`).nth(i);
+ await stack.scrollIntoViewIfNeeded();
// Since otherwise stack title will be repeated as title is in two divs, with one having hidden class
const stackTitle = await stack.locator(`.nc-kanban-stack-head >> [data-testid="truncate-label"]`);
await expect(stackTitle).toHaveText(order[i], { ignoreCase: true });
@@ -76,6 +77,7 @@ export class KanbanPage extends BasePage {
const stacks = await this.get().locator(`.nc-kanban-stack`).count();
for (let i = 0; i < stacks; i++) {
const stack = await this.get().locator(`.nc-kanban-stack`).nth(i);
+ await stack.scrollIntoViewIfNeeded();
const stackFooter = await stack.locator(`.nc-kanban-data-count`).innerText();
await expect(stackFooter).toContain(`${count[i]} record${count[i] !== 1 ? 's' : ''}`);
}
@@ -86,6 +88,7 @@ export class KanbanPage extends BasePage {
const stacks = await this.get().locator(`.nc-kanban-stack`).count();
for (let i = 0; i < stacks; i++) {
const stack = await this.get().locator(`.nc-kanban-stack`).nth(i);
+ await stack.scrollIntoViewIfNeeded();
const stackCards = stack.locator(`.nc-kanban-item`);
await expect(stackCards).toHaveCount(count[i]);
}
@@ -96,6 +99,7 @@ export class KanbanPage extends BasePage {
const stack = await this.get().locator(`.nc-kanban-stack`).nth(stackIndex);
for (let i = 0; i < order.length; i++) {
const card = await stack.locator(`.nc-kanban-item`).nth(i);
+ await card.scrollIntoViewIfNeeded();
const cardTitle = await card.locator(`.nc-cell`);
await expect(cardTitle).toHaveText(order[i]);
}
diff --git a/tests/playwright/pages/Dashboard/common/Cell/AttachmentCell.ts b/tests/playwright/pages/Dashboard/common/Cell/AttachmentCell.ts
index 52ad7fcdb8..bb0b8ccb1c 100644
--- a/tests/playwright/pages/Dashboard/common/Cell/AttachmentCell.ts
+++ b/tests/playwright/pages/Dashboard/common/Cell/AttachmentCell.ts
@@ -22,6 +22,7 @@ export class AttachmentCellPageObject extends BasePage {
// e.g. ['path/to/file1', 'path/to/file2']
//
async addFile({ index, columnHeader, filePath }: { index?: number; columnHeader: string; filePath: string[] }) {
+ await this.get({ index, columnHeader }).scrollIntoViewIfNeeded();
const attachFileAction = this.get({ index, columnHeader })
.locator('[data-testid="attachment-cell-file-picker-button"]')
.click();
@@ -52,7 +53,7 @@ export class AttachmentCellPageObject extends BasePage {
let retryCount = 0;
while (retryCount < 5) {
const attachments = await this.get({ index, columnHeader }).locator('.nc-attachment');
- console.log(await attachments.count());
+ // console.log(await attachments.count());
if ((await attachments.count()) === count) {
break;
}
diff --git a/tests/playwright/pages/Dashboard/common/Cell/RatingCell.ts b/tests/playwright/pages/Dashboard/common/Cell/RatingCell.ts
index 8514e4ea21..93de8be449 100644
--- a/tests/playwright/pages/Dashboard/common/Cell/RatingCell.ts
+++ b/tests/playwright/pages/Dashboard/common/Cell/RatingCell.ts
@@ -23,6 +23,7 @@ export class RatingCellPageObject extends BasePage {
}
async verify({ index, columnHeader, rating }: { index?: number; columnHeader: string; rating: number }) {
+ await this.get({ index, columnHeader }).scrollIntoViewIfNeeded();
await expect(await this.get({ index, columnHeader }).locator(`div[role="radio"][aria-checked="true"]`)).toHaveCount(
rating
);
diff --git a/tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts b/tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts
index 38531e4d1e..219d03ba2c 100644
--- a/tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts
+++ b/tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts
@@ -85,9 +85,8 @@ export class SelectOptionCellPageObject extends BasePage {
if (multiSelect) {
return await expect(this.cell.get({ index, columnHeader })).toContainText(option, { useInnerText: true });
}
- return await expect(
- this.cell.get({ index, columnHeader }).locator('.ant-select-selection-item > .ant-tag')
- ).toHaveText(option, { useInnerText: true });
+ const text = await (await this.cell.get({ index, columnHeader }).locator('.ant-tag')).allInnerTexts();
+ return expect(text).toContain(option);
}
async verifyNoOptionsSelected({ index, columnHeader }: { index: number; columnHeader: string }) {
diff --git a/tests/playwright/pages/Dashboard/common/Cell/index.ts b/tests/playwright/pages/Dashboard/common/Cell/index.ts
index 3fc20abc18..bede598f4e 100644
--- a/tests/playwright/pages/Dashboard/common/Cell/index.ts
+++ b/tests/playwright/pages/Dashboard/common/Cell/index.ts
@@ -114,6 +114,10 @@ export class CellPageObject extends BasePage {
// if text is found, return
// if text is not found, throw error
let count = 0;
+ await this.get({
+ index,
+ columnHeader,
+ }).scrollIntoViewIfNeeded();
while (count < 5) {
const innerTexts = await this.get({
index,
@@ -265,9 +269,11 @@ export class CellPageObject extends BasePage {
value: string[];
}) {
// const count = value.length;
- const cell = this.get({ index, columnHeader });
+ const cell = await this.get({ index, columnHeader });
const chips = cell.locator('.chips > .chip');
+ await this.get({ index, columnHeader }).scrollIntoViewIfNeeded();
+
// verify chip count & contents
if (count) await expect(chips).toHaveCount(count);
@@ -316,6 +322,7 @@ export class CellPageObject extends BasePage {
}
async copyToClipboard({ index, columnHeader }: CellProps, ...clickOptions: Parameters) {
+ await this.get({ index, columnHeader }).scrollIntoViewIfNeeded();
await this.get({ index, columnHeader }).click(...clickOptions);
await (await this.get({ index, columnHeader }).elementHandle()).waitForElementState('stable');
diff --git a/tests/playwright/pages/Dashboard/common/Toolbar/Fields.ts b/tests/playwright/pages/Dashboard/common/Toolbar/Fields.ts
index e27450133d..cd64933fec 100644
--- a/tests/playwright/pages/Dashboard/common/Toolbar/Fields.ts
+++ b/tests/playwright/pages/Dashboard/common/Toolbar/Fields.ts
@@ -97,7 +97,7 @@ export class ToolbarFieldsPage extends BasePage {
}
async getFieldsTitles() {
- let fields: string[] = await this.rootPage.locator(`.nc-grid-header .name`).allTextContents();
+ const fields: string[] = await this.rootPage.locator(`.nc-grid-header .name`).allTextContents();
return fields;
}
diff --git a/tests/playwright/tests/01-webhook.spec.ts b/tests/playwright/tests/01-webhook.spec.ts
index 66b8b5cc8c..325bde44bb 100644
--- a/tests/playwright/tests/01-webhook.spec.ts
+++ b/tests/playwright/tests/01-webhook.spec.ts
@@ -3,8 +3,12 @@ import { DashboardPage } from '../pages/Dashboard';
import setup from '../setup';
import makeServer from '../setup/server';
import { WebhookFormPage } from '../pages/Dashboard/WebhookForm';
+import { isSubset } from './utils/general';
+import { Api, UITypes } from 'nocodb-sdk';
+import { isMysql, isPg, isSqlite } from '../setup/db';
const hookPath = 'http://localhost:9090/hook';
+let api: Api;
// clear server data
async function clearServerData({ request }) {
@@ -16,9 +20,28 @@ async function clearServerData({ request }) {
await expect(await response.json()).toBe(0);
}
-async function verifyHookTrigger(count: number, value: string, request) {
+async function getWebhookResponses({ request, count = 1 }) {
+ let response;
+
+ // retry since there can be lag between the time the hook is triggered and the time the server receives the request
+ for (let i = 0; i < 20; i++) {
+ response = await request.get(hookPath + '/count');
+ if ((await response.json()) === count) {
+ break;
+ }
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+ await expect(await response.json()).toBe(count);
+
+ response = await request.get(hookPath + '/all');
+ return await response.json();
+}
+
+async function verifyHookTrigger(count: number, value: string, request, expectedData?: any) {
// Retry since there can be lag between the time the hook is triggered and the time the server receives the request
let response;
+
+ // retry since there can be lag between the time the hook is triggered and the time the server receives the request
for (let i = 0; i < 20; i++) {
response = await request.get(hookPath + '/count');
if ((await response.json()) === count) {
@@ -30,17 +53,51 @@ async function verifyHookTrigger(count: number, value: string, request) {
if (count) {
let response;
+
+ // retry since there can be lag between the time the hook is triggered and the time the server receives the request
for (let i = 0; i < 20; i++) {
response = await request.get(hookPath + '/last');
- if ((await response.json()).Title === value) {
+ const rspJson = await response.json();
+ if (rspJson.data.rows[0].Title === value) {
break;
}
await new Promise(resolve => setTimeout(resolve, 150));
}
- await expect((await response.json()).Title).toBe(value);
+ const rspJson = await response.json();
+ await expect(rspJson?.data?.rows[0]?.Title).toBe(value);
+ if (expectedData) {
+ await expect(isSubset(rspJson, expectedData)).toBe(true);
+ }
}
}
+async function buildExpectedResponseData(type, value, oldValue?) {
+ const expectedData = {
+ type: 'records.after.insert',
+ data: {
+ table_name: 'Test',
+ view_name: 'Test',
+ rows: [
+ {
+ Title: 'Poole',
+ },
+ ],
+ },
+ };
+
+ expectedData.type = type;
+ expectedData.data.rows[0].Title = value;
+
+ if (oldValue) {
+ expectedData.data['previous_rows'] = [];
+ expectedData.data['previous_rows'][0] = {
+ Title: oldValue,
+ };
+ }
+
+ return expectedData;
+}
+
test.describe.serial('Webhook', () => {
// start a server locally for webhook tests
@@ -52,13 +109,20 @@ test.describe.serial('Webhook', () => {
});
test.beforeEach(async ({ page }) => {
- context = await setup({ page });
+ context = await setup({ page, isEmptyProject: true });
dashboard = new DashboardPage(page, context.project);
webhook = dashboard.webhookForm;
+
+ api = new Api({
+ baseURL: `http://localhost:8080/`,
+ headers: {
+ 'xc-auth': context.token,
+ },
+ });
});
test('CRUD', async ({ request, page }) => {
- // todo: Waiting for the server to start
+ // Waiting for the server to start
await page.waitForTimeout(1000);
// close 'Team & Auth' tab
@@ -66,6 +130,14 @@ test.describe.serial('Webhook', () => {
await dashboard.closeTab({ title: 'Team & Auth' });
await dashboard.treeView.createTable({ title: 'Test' });
+ // create
+ //
+ // hook order
+ // hook-1: after insert
+ // - verify trigger after insert
+ // - verify no trigger after edit
+ // - verify no trigger after delete
+
// after insert hook
await webhook.create({
title: 'hook-1',
@@ -77,12 +149,26 @@ test.describe.serial('Webhook', () => {
columnHeader: 'Title',
value: 'Poole',
});
- await verifyHookTrigger(1, 'Poole', request);
+ await verifyHookTrigger(1, 'Poole', request, buildExpectedResponseData('records.after.insert', 'Poole'));
+
+ // trigger edit row & delete row
+ // verify that the hook is not triggered (count doesn't change in this case)
await dashboard.grid.editRow({ index: 0, value: 'Delaware' });
await verifyHookTrigger(1, 'Poole', request);
await dashboard.grid.deleteRow(0);
await verifyHookTrigger(1, 'Poole', request);
+ ///////////////////////////////////////////////////////////////////////////
+
+ // update
+ //
+ // hook order
+ // hook-1: after insert
+ // hook-2: after update
+ // - verify trigger after insert
+ // - verify trigger after edit
+ // - verify no trigger after delete
+
// after update hook
await webhook.create({
title: 'hook-2',
@@ -95,12 +181,27 @@ test.describe.serial('Webhook', () => {
columnHeader: 'Title',
value: 'Poole',
});
- await verifyHookTrigger(1, 'Poole', request);
+ await verifyHookTrigger(1, 'Poole', request, buildExpectedResponseData('records.after.insert', 'Poole'));
await dashboard.grid.editRow({ index: 0, value: 'Delaware' });
- await verifyHookTrigger(2, 'Delaware', request);
+ await verifyHookTrigger(
+ 2,
+ 'Delaware',
+ request,
+ buildExpectedResponseData('records.after.update', 'Delaware', 'Poole')
+ );
await dashboard.grid.deleteRow(0);
await verifyHookTrigger(2, 'Delaware', request);
+ ///////////////////////////////////////////////////////////////////////////
+
+ // hook order
+ // hook-1: after insert
+ // hook-2: after update
+ // hook-3: after delete
+ // - verify trigger after insert
+ // - verify trigger after edit
+ // - verify trigger after delete
+
// after delete hook
await webhook.create({
title: 'hook-3',
@@ -112,13 +213,29 @@ test.describe.serial('Webhook', () => {
columnHeader: 'Title',
value: 'Poole',
});
- await verifyHookTrigger(1, 'Poole', request);
+ await verifyHookTrigger(1, 'Poole', request, buildExpectedResponseData('records.after.insert', 'Poole'));
await dashboard.grid.editRow({ index: 0, value: 'Delaware' });
- await verifyHookTrigger(2, 'Delaware', request);
+ await verifyHookTrigger(
+ 2,
+ 'Delaware',
+ request,
+ buildExpectedResponseData('records.after.update', 'Delaware', 'Poole')
+ );
await dashboard.grid.deleteRow(0);
- await verifyHookTrigger(3, 'Delaware', request);
+ await verifyHookTrigger(3, 'Delaware', request, buildExpectedResponseData('records.after.delete', 'Delaware'));
+
+ ///////////////////////////////////////////////////////////////////////////
// modify webhook
+
+ // hook order
+ // hook-1: after delete
+ // hook-2: after delete
+ // hook-3: after delete
+ // - verify no trigger after insert
+ // - verify no trigger after edit
+ // - verify trigger after delete
+
await webhook.open({ index: 0 });
await webhook.configureWebhook({
title: 'hook-1-modified',
@@ -140,13 +257,28 @@ test.describe.serial('Webhook', () => {
columnHeader: 'Title',
value: 'Poole',
});
+
+ // for insert & edit, the hook should not be triggered (count doesn't change in this case)
await verifyHookTrigger(0, 'Poole', request);
await dashboard.grid.editRow({ index: 0, value: 'Delaware' });
await verifyHookTrigger(0, 'Delaware', request);
await dashboard.grid.deleteRow(0);
- await verifyHookTrigger(3, 'Delaware', request);
+
+ // for delete, the hook should be triggered (thrice in this case)
+ await verifyHookTrigger(3, 'Delaware', request, buildExpectedResponseData('records.after.delete', 'Delaware'));
+
+ ///////////////////////////////////////////////////////////////////////////
// delete webhook
+
+ // hook order
+ // hook-1: -
+ // hook-2: -
+ // hook-3: -
+ // - verify no trigger after insert
+ // - verify no trigger after edit
+ // - verify no trigger after delete
+
await webhook.delete({ index: 0 });
await webhook.delete({ index: 0 });
await webhook.delete({ index: 0 });
@@ -212,7 +344,18 @@ test.describe.serial('Webhook', () => {
save: true,
});
- // verify
+ ///////////////////////////////////////////////////////////////////////////
+
+ // webhook with condition
+
+ // hook order
+ // hook-1: after insert where Title is like 'Poole'
+ // hook-2: after update where Title is like 'Poole'
+ // hook-3: after delete where Title is like 'Poole'
+ // - verify trigger after insert gets triggered only when Title is like 'Poole'
+ // - verify trigger after edit gets triggered only when Title is like 'Poole'
+ // - verify trigger after delete gets triggered only when Title is like 'Poole'
+
await clearServerData({ request });
await dashboard.grid.addNewRow({
index: 0,
@@ -224,15 +367,30 @@ test.describe.serial('Webhook', () => {
columnHeader: 'Title',
value: 'Delaware',
});
- await verifyHookTrigger(1, 'Poole', request);
+ await verifyHookTrigger(1, 'Poole', request, buildExpectedResponseData('records.after.insert', 'Poole'));
await dashboard.grid.editRow({ index: 0, value: 'Delaware' });
await dashboard.grid.editRow({ index: 1, value: 'Poole' });
- await verifyHookTrigger(2, 'Poole', request);
+ await verifyHookTrigger(
+ 2,
+ 'Poole',
+ request,
+ buildExpectedResponseData('records.after.update', 'Poole', 'Delaware')
+ );
await dashboard.grid.deleteRow(1);
await dashboard.grid.deleteRow(0);
- await verifyHookTrigger(3, 'Poole', request);
+ await verifyHookTrigger(3, 'Poole', request, buildExpectedResponseData('records.after.delete', 'Poole'));
- // Delete condition
+ ///////////////////////////////////////////////////////////////////////////
+
+ // webhook after conditions are removed
+
+ // hook order
+ // hook-1: after insert
+ // hook-2: after update
+ // hook-3: after delete
+ // - verify trigger after insert gets triggered when Title is like 'Poole' or not
+ // - verify trigger after edit gets triggered when Title is like 'Poole' or not
+ // - verify trigger after delete gets triggered when Title is like 'Poole' or not
await webhook.open({ index: 2 });
await webhook.deleteCondition({ save: true });
await webhook.open({ index: 1 });
@@ -251,12 +409,334 @@ test.describe.serial('Webhook', () => {
columnHeader: 'Title',
value: 'Delaware',
});
- await verifyHookTrigger(2, 'Delaware', request);
+ await verifyHookTrigger(2, 'Delaware', request, buildExpectedResponseData('records.after.insert', 'Delaware'));
await dashboard.grid.editRow({ index: 0, value: 'Delaware' });
await dashboard.grid.editRow({ index: 1, value: 'Poole' });
- await verifyHookTrigger(4, 'Poole', request);
+ await verifyHookTrigger(
+ 4,
+ 'Poole',
+ request,
+ buildExpectedResponseData('records.after.update', 'Poole', 'Delaware')
+ );
await dashboard.grid.deleteRow(1);
await dashboard.grid.deleteRow(0);
- await verifyHookTrigger(6, 'Delaware', request);
+ await verifyHookTrigger(6, 'Delaware', request, buildExpectedResponseData('records.after.delete', 'Delaware'));
+ });
+
+ test('Bulk operations', async ({ request, page }) => {
+ async function verifyBulkOperationTrigger(rsp, type) {
+ for (let i = 0; i < rsp.length; i++) {
+ expect(rsp[i].type).toBe(type);
+ expect(rsp[i].data.table_name).toBe('numberBased');
+ expect(rsp[i].data.view_name).toBe('numberBased');
+
+ // only for insert, rows inserted will not be returned in response. just count
+ if (type === 'records.after.bulkInsert') {
+ expect(rsp[i].data.rows_inserted).toBe(50);
+ } else if (type === 'records.after.bulkUpdate') {
+ expect(rsp[i].data.rows.length).toBe(50);
+ expect(rsp[i].data.previous_rows.length).toBe(50);
+
+ // verify records
+ for (let j = 0; j < rsp[i].data.rows.length; j++) {
+ expect(+rsp[i].data.rows[j].Number).toBe(111 * (j + 1));
+ expect(+rsp[i].data.previous_rows[j].Number).toBe(100 * (j + 1));
+ }
+ } else if (type === 'records.after.bulkDelete') {
+ expect(rsp[i].data.rows.length).toBe(50);
+
+ // verify records
+ for (let j = 0; j < rsp[i].data.rows.length; j++) {
+ expect(+rsp[i].data.rows[j].Number).toBe(111 * (j + 1));
+ }
+ }
+ }
+ }
+
+ // Waiting for the server to start
+ await page.waitForTimeout(1000);
+
+ // close 'Team & Auth' tab
+ await dashboard.closeTab({ title: 'Team & Auth' });
+
+ const columns = [
+ {
+ column_name: 'Id',
+ title: 'Id',
+ uidt: UITypes.ID,
+ },
+ {
+ column_name: 'Number',
+ title: 'Number',
+ uidt: UITypes.Number,
+ },
+ ];
+ let project, table;
+
+ try {
+ project = await api.project.read(context.project.id);
+ table = await api.base.tableCreate(context.project.id, project.bases?.[0].id, {
+ table_name: 'numberBased',
+ title: 'numberBased',
+ columns: columns,
+ });
+ } catch (e) {
+ console.error(e);
+ }
+
+ await page.reload();
+ await dashboard.treeView.openTable({ title: 'numberBased' });
+
+ // create after insert webhook
+ await webhook.create({
+ title: 'hook-1',
+ event: 'After Bulk Insert',
+ });
+ await webhook.create({
+ title: 'hook-1',
+ event: 'After Bulk Update',
+ });
+ await webhook.create({
+ title: 'hook-1',
+ event: 'After Bulk Delete',
+ });
+
+ await clearServerData({ request });
+ const rowAttributesForInsert = Array.from({ length: 50 }, (_, i) => ({
+ Id: i + 1,
+ Number: (i + 1) * 100,
+ }));
+ await api.dbTableRow.bulkCreate('noco', context.project.id, table.id, rowAttributesForInsert);
+ await page.reload();
+ // 50 records inserted, we expect 2 webhook responses
+ let rsp = await getWebhookResponses({ request, count: 1 });
+ await verifyBulkOperationTrigger(rsp, 'records.after.bulkInsert');
+
+ // bulk update all rows
+ await clearServerData({ request });
+ // build rowAttributes for update to contain all the ids & their value set to 100
+ const rowAttributesForUpdate = Array.from({ length: 50 }, (_, i) => ({
+ Id: i + 1,
+ Number: (i + 1) * 111,
+ }));
+
+ await api.dbTableRow.bulkUpdate('noco', context.project.id, table.id, rowAttributesForUpdate);
+ await page.reload();
+ // 50 records updated, we expect 2 webhook responses
+ rsp = await getWebhookResponses({ request, count: 1 });
+ await verifyBulkOperationTrigger(rsp, 'records.after.bulkUpdate');
+
+ // bulk delete all rows
+ await clearServerData({ request });
+ const rowAttributesForDelete = Array.from({ length: 50 }, (_, i) => ({ Id: i + 1 }));
+
+ await api.dbTableRow.bulkDelete('noco', context.project.id, table.id, rowAttributesForDelete);
+ await page.reload();
+ rsp = await getWebhookResponses({ request, count: 1 });
+ await verifyBulkOperationTrigger(rsp, 'records.after.bulkDelete');
+ });
+
+ test('Virtual columns', async ({ request, page }) => {
+ let cityTable, countryTable;
+ const cityColumns = [
+ {
+ column_name: 'Id',
+ title: 'Id',
+ uidt: UITypes.ID,
+ },
+ {
+ column_name: 'City',
+ title: 'City',
+ uidt: UITypes.SingleLineText,
+ pv: true,
+ },
+ {
+ column_name: 'CityCode',
+ title: 'CityCode',
+ uidt: UITypes.Number,
+ },
+ ];
+ const countryColumns = [
+ {
+ column_name: 'Id',
+ title: 'Id',
+ uidt: UITypes.ID,
+ },
+ {
+ column_name: 'Country',
+ title: 'Country',
+ uidt: UITypes.SingleLineText,
+ pv: true,
+ },
+ {
+ column_name: 'CountryCode',
+ title: 'CountryCode',
+ uidt: UITypes.Number,
+ },
+ ];
+
+ try {
+ const project = await api.project.read(context.project.id);
+ cityTable = await api.base.tableCreate(context.project.id, project.bases?.[0].id, {
+ table_name: 'City',
+ title: 'City',
+ columns: cityColumns,
+ });
+ countryTable = await api.base.tableCreate(context.project.id, project.bases?.[0].id, {
+ table_name: 'Country',
+ title: 'Country',
+ columns: countryColumns,
+ });
+
+ const cityRowAttributes = [
+ { City: 'Mumbai', CityCode: 23 },
+ { City: 'Pune', CityCode: 33 },
+ { City: 'Delhi', CityCode: 43 },
+ { City: 'Bangalore', CityCode: 53 },
+ ];
+ await api.dbTableRow.bulkCreate('noco', context.project.id, cityTable.id, cityRowAttributes);
+
+ const countryRowAttributes = [
+ { Country: 'India', CountryCode: 1 },
+ { Country: 'USA', CountryCode: 2 },
+ { Country: 'UK', CountryCode: 3 },
+ { Country: 'Australia', CountryCode: 4 },
+ ];
+ await api.dbTableRow.bulkCreate('noco', context.project.id, countryTable.id, countryRowAttributes);
+
+ // create LTAR Country has-many City
+ countryTable = await api.dbTableColumn.create(countryTable.id, {
+ column_name: 'CityList',
+ title: 'CityList',
+ uidt: UITypes.LinkToAnotherRecord,
+ parentId: countryTable.id,
+ childId: cityTable.id,
+ type: 'hm',
+ });
+
+ // Create Lookup column in Country table
+ countryTable = await api.dbTableColumn.create(countryTable.id, {
+ column_name: 'CityCodeLookup',
+ title: 'CityCodeLookup',
+ uidt: UITypes.Lookup,
+ fk_relation_column_id: countryTable.columns.filter(c => c.title === 'CityList')[0].id,
+ fk_lookup_column_id: cityTable.columns.filter(c => c.title === 'CityCode')[0].id,
+ });
+
+ // Create Rollup column in Country table
+ countryTable = await api.dbTableColumn.create(countryTable.id, {
+ column_name: 'CityCodeRollup',
+ title: 'CityCodeRollup',
+ uidt: UITypes.Rollup,
+ fk_relation_column_id: countryTable.columns.filter(c => c.title === 'CityList')[0].id,
+ fk_rollup_column_id: cityTable.columns.filter(c => c.title === 'CityCode')[0].id,
+ rollup_function: 'count',
+ });
+
+ // Create links
+ await api.dbTableRow.nestedAdd('noco', context.project.title, countryTable.title, 1, 'hm', 'CityList', '1');
+ await api.dbTableRow.nestedAdd('noco', context.project.title, countryTable.title, 1, 'hm', 'CityList', '2');
+ await api.dbTableRow.nestedAdd('noco', context.project.title, countryTable.title, 2, 'hm', 'CityList', '3');
+ await api.dbTableRow.nestedAdd('noco', context.project.title, countryTable.title, 3, 'hm', 'CityList', '4');
+
+ // create formula column
+ countryTable = await api.dbTableColumn.create(countryTable.id, {
+ column_name: 'CityCodeFormula',
+ title: 'CityCodeFormula',
+ uidt: UITypes.Formula,
+ formula_raw: '({Id} * 100)',
+ });
+ } catch (e) {
+ console.log(e);
+ }
+
+ await page.reload();
+ await dashboard.treeView.openTable({ title: 'Country' });
+
+ // create after update webhook
+ // after update hook
+ await webhook.create({
+ title: 'hook-2',
+ event: 'After Update',
+ });
+
+ // clear server data
+ await clearServerData({ request });
+
+ // edit first record
+ await dashboard.grid.editRow({ index: 0, columnHeader: 'Country', value: 'INDIA', networkValidation: false });
+ const rsp = await getWebhookResponses({ request, count: 1 });
+
+ const expectedData = {
+ type: 'records.after.update',
+ data: {
+ table_name: 'Country',
+ view_name: 'Country',
+ previous_rows: [
+ {
+ Id: 1,
+ Country: 'India',
+ CountryCode: '1',
+ CityCodeRollup: '2',
+ CityCodeFormula: 100,
+ CityList: [
+ {
+ Id: 1,
+ City: 'Mumbai',
+ },
+ {
+ Id: 2,
+ City: 'Pune',
+ },
+ ],
+ CityCodeLookup: ['23', '33'],
+ },
+ ],
+ rows: [
+ {
+ Id: 1,
+ Country: 'INDIA',
+ CountryCode: '1',
+ CityCodeRollup: '2',
+ CityCodeFormula: 100,
+ CityList: [
+ {
+ Id: 1,
+ City: 'Mumbai',
+ },
+ {
+ Id: 2,
+ City: 'Pune',
+ },
+ ],
+ CityCodeLookup: ['23', '33'],
+ },
+ ],
+ },
+ };
+
+ if (isSqlite(context) || isMysql(context)) {
+ // @ts-ignore
+ expectedData.data.previous_rows[0].CountryCode = 1;
+ // @ts-ignore
+ expectedData.data.rows[0].CountryCode = 1;
+ // @ts-ignore
+ expectedData.data.previous_rows[0].CityCodeRollup = 2;
+ // @ts-ignore
+ expectedData.data.rows[0].CityCodeRollup = 2;
+ // @ts-ignore
+ expectedData.data.previous_rows[0].CityCodeLookup = [23, 33];
+ // @ts-ignore
+ expectedData.data.rows[0].CityCodeLookup = [23, 33];
+
+ if (isMysql(context)) {
+ // @ts-ignore
+ expectedData.data.previous_rows[0].CityCodeFormula = '100';
+ // @ts-ignore
+ expectedData.data.rows[0].CityCodeFormula = '100';
+ }
+ }
+
+ await expect(isSubset(rsp[0], expectedData)).toBe(true);
});
});
diff --git a/tests/playwright/tests/columnAttachments.spec.ts b/tests/playwright/tests/columnAttachments.spec.ts
index cd88220172..a9858f07e2 100644
--- a/tests/playwright/tests/columnAttachments.spec.ts
+++ b/tests/playwright/tests/columnAttachments.spec.ts
@@ -39,12 +39,12 @@ test.describe('Attachment column', () => {
});
}
await dashboard.grid.cell.attachment.addFile({
- index: 14,
+ index: 4,
columnHeader: 'testAttach',
filePath: [`${process.cwd()}/fixtures/sampleFiles/sampleImage.jpeg`],
});
await dashboard.grid.cell.attachment.verifyFile({
- index: 14,
+ index: 4,
columnHeader: 'testAttach',
});
diff --git a/tests/playwright/tests/megaTable.spec.ts b/tests/playwright/tests/megaTable.spec.ts
new file mode 100644
index 0000000000..578e121103
--- /dev/null
+++ b/tests/playwright/tests/megaTable.spec.ts
@@ -0,0 +1,160 @@
+import { test } from '@playwright/test';
+import setup from '../setup';
+import { UITypes } from 'nocodb-sdk';
+import { Api } from 'nocodb-sdk';
+let api: Api;
+
+// configuration
+
+// To use, modify the test.skip to test.only
+// Add columns as required to megaTblColumns
+// Add row count as required to megaTblRows
+
+const megaTblColumns = [
+ { type: 'SingleLineText', count: 30 },
+ { type: 'LongText', count: 100 },
+ { type: 'Number', count: 30 },
+ { type: 'Checkbox', count: 30 },
+ { type: 'SingleSelect', count: 30 },
+ { type: 'MultiSelect', count: 100 },
+ { type: 'Date', count: 100 },
+ { type: 'DateTime', count: 100 },
+ { type: 'Email', count: 100 },
+ { type: 'Currency', count: 100 },
+ { type: 'Duration', count: 100 },
+ { type: 'Rating', count: 100 },
+];
+const megaTblRows = 1000;
+const bulkInsertAfterRows = 1000;
+const formulaRowCnt = 100;
+
+test.describe.serial('Test table', () => {
+ let context: any;
+
+ test.beforeEach(async ({ page }) => {
+ context = await setup({ page });
+
+ api = new Api({
+ baseURL: `http://localhost:8080/`,
+ headers: {
+ 'xc-auth': context.token,
+ },
+ });
+ });
+
+ test.skip('mega table', async ({ page }) => {
+ let table_1;
+ const table_1_columns = [];
+
+ // a Primary key column & display column
+ table_1_columns.push(
+ {
+ column_name: 'Id',
+ title: 'Id',
+ uidt: UITypes.ID,
+ },
+ {
+ column_name: 'SingleLineText',
+ title: 'SingleLineText',
+ uidt: UITypes.SingleLineText,
+ pv: true,
+ }
+ );
+
+ for (let i = 0; i < megaTblColumns.length; i++) {
+ for (let j = 0; j < megaTblColumns[i].count; j++) {
+ // skip if Formula
+ if (megaTblColumns[i].type === 'Formula') continue;
+ const column = {
+ column_name: `${megaTblColumns[i].type}${j}`,
+ title: `${megaTblColumns[i].type}${j}`,
+ uidt: UITypes[megaTblColumns[i].type],
+ };
+ if (megaTblColumns[i].type === 'SingleSelect' || megaTblColumns[i].type === 'MultiSelect') {
+ column['dtxp'] = "'jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'";
+ }
+ if (megaTblColumns[i].type === 'Email') {
+ column['meta'] = {
+ validate: true,
+ };
+ }
+ table_1_columns.push(column);
+ }
+ }
+
+ try {
+ const project = await api.project.read(context.project.id);
+ table_1 = await api.base.tableCreate(context.project.id, project.bases?.[0].id, {
+ table_name: 'table_1',
+ title: 'table_1',
+ columns: table_1_columns,
+ });
+
+ // run loop for formula count
+ for (let i = 0; i < formulaRowCnt; i++) {
+ table_1 = await api.dbTableColumn.create(table_1.id, {
+ column_name: `Formula${i}`,
+ title: `Formula${i}`,
+ uidt: UITypes.Formula,
+ formula_raw: '{SingleLineText}',
+ });
+ }
+
+ const table_1_rows = [];
+ for (let rowCnt = 0; rowCnt < megaTblRows; rowCnt++) {
+ const row = {
+ Id: rowCnt + 1,
+ SingleLineText: `SingleLineText${rowCnt + 1}`,
+ };
+ for (let colCnt = 0; colCnt < megaTblColumns.length; colCnt++) {
+ if (megaTblColumns[colCnt].type === 'Formula') continue;
+ for (let colInstanceCnt = 0; colInstanceCnt < megaTblColumns[colCnt].count; colInstanceCnt++) {
+ const columnName = `${megaTblColumns[colCnt].type}${colInstanceCnt}`;
+ if (megaTblColumns[colCnt].type === 'SingleLineText') {
+ row[columnName] = `SingleLineText${rowCnt + 1}`;
+ } else if (
+ megaTblColumns[colCnt].type === 'Number' ||
+ megaTblColumns[colCnt].type === 'Currency' ||
+ megaTblColumns[colCnt].type === 'Duration'
+ ) {
+ row[columnName] = rowCnt + 1;
+ } else if (megaTblColumns[colCnt].type === 'Checkbox') {
+ row[columnName] = rowCnt % 2 === 0;
+ } else if (megaTblColumns[colCnt].type === 'SingleSelect') {
+ row[columnName] = 'jan';
+ } else if (megaTblColumns[colCnt].type === 'MultiSelect') {
+ row[columnName] = 'jan,feb,mar,apr';
+ } else if (megaTblColumns[colCnt].type === 'LongText') {
+ row[columnName] = `Some length text here. Some length text here`;
+ } else if (megaTblColumns[colCnt].type === 'DateTime') {
+ row[columnName] = '2023-04-25 16:25:11+05:30';
+ } else if (megaTblColumns[colCnt].type === 'Date') {
+ row[columnName] = '2023-04-25 16:25:11+05:30';
+ } else if (megaTblColumns[colCnt].type === 'Email') {
+ row[columnName] = 'raju@nocodb.com';
+ } else if (megaTblColumns[colCnt].type === 'Rating') {
+ row[columnName] = (rowCnt % 5) + 1;
+ }
+ }
+ }
+ table_1_rows.push(row);
+
+ // insert as soon as we have 1k records ready
+ if (table_1_rows.length === bulkInsertAfterRows) {
+ await api.dbTableRow.bulkCreate('noco', context.project.id, table_1.id, table_1_rows);
+ console.log(`table_1_rows ${rowCnt + 1} created`);
+ table_1_rows.length = 0;
+ }
+ }
+
+ if (table_1_rows.length > 0) {
+ await api.dbTableRow.bulkCreate('noco', context.project.id, table_1.id, table_1_rows);
+ console.log(`table_1_rows ${megaTblRows} created`);
+ }
+ } catch (e) {
+ console.log(e);
+ }
+
+ await page.reload();
+ });
+});
diff --git a/tests/playwright/tests/utils/general.ts b/tests/playwright/tests/utils/general.ts
index 1571666b1a..2bb3bdf035 100644
--- a/tests/playwright/tests/utils/general.ts
+++ b/tests/playwright/tests/utils/general.ts
@@ -19,4 +19,23 @@ async function getTextExcludeIconText(selector) {
return text.trim();
}
-export { getTextExcludeIconText };
+function isSubset(obj, potentialSubset) {
+ for (const prop in potentialSubset) {
+ // eslint-disable-next-line no-prototype-builtins
+ if (potentialSubset.hasOwnProperty(prop)) {
+ const potentialValue = potentialSubset[prop];
+ const objValue = obj[prop];
+ if (typeof potentialValue === 'object' && typeof objValue === 'object') {
+ if (!isSubset(objValue, potentialValue)) {
+ return false;
+ }
+ // eslint-disable-next-line no-prototype-builtins
+ } else if (!obj.hasOwnProperty(prop) || objValue !== potentialValue) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+export { getTextExcludeIconText, isSubset };
| |