Browse Source

Merge pull request #8367 from nocodb/nc-feat/links-from-existing-cols

Nc feat/links from existing cols
pull/8991/head
Pranav C 4 months ago committed by GitHub
parent
commit
2e66dfdc65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      packages/nc-gui/components/general/DeleteModal.vue
  2. 24
      packages/nc-gui/components/nc/Select.vue
  3. 6
      packages/nc-gui/components/smartsheet/Cell.vue
  4. 6
      packages/nc-gui/components/smartsheet/PlainCell.vue
  5. 1
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  6. 9
      packages/nc-gui/components/smartsheet/column/LinkAdvancedOptions.vue
  7. 111
      packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue
  8. 4
      packages/nc-gui/components/smartsheet/column/LookupOptions.vue
  9. 5
      packages/nc-gui/components/smartsheet/details/Fields.vue
  10. 4
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  11. 2
      packages/nc-gui/components/smartsheet/grid/GroupBy.vue
  12. 4
      packages/nc-gui/components/smartsheet/grid/GroupByTable.vue
  13. 4
      packages/nc-gui/components/smartsheet/grid/useColumnDrag.ts
  14. 28
      packages/nc-gui/components/smartsheet/header/DeleteColumnModal.vue
  15. 17
      packages/nc-gui/components/smartsheet/header/Menu.vue
  16. 6
      packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue
  17. 9
      packages/nc-gui/composables/useColumnCreateStore.ts
  18. 2
      packages/nc-gui/composables/useExpandedFormStore.ts
  19. 2
      packages/nc-gui/composables/useViewColumns.ts
  20. 2
      packages/nc-gui/store/sidebar.ts
  21. 2
      packages/nocodb-sdk/src/lib/globals.ts
  22. 60
      packages/nocodb-sdk/src/lib/sqlUi/DatabricksUi.ts
  23. 16
      packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts
  24. 16
      packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts
  25. 16
      packages/nocodb-sdk/src/lib/sqlUi/OracleUi.ts
  26. 20
      packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts
  27. 20
      packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts
  28. 20
      packages/nocodb-sdk/src/lib/sqlUi/SqliteUi.ts
  29. 9
      packages/nocodb/src/controllers/notifications.controller.ts
  30. 2
      packages/nocodb/src/controllers/users/users.controller.ts
  31. 279
      packages/nocodb/src/db/BaseModelSqlv2.ts
  32. 33
      packages/nocodb/src/db/genRollupSelectv2.ts
  33. 54
      packages/nocodb/src/db/generateLookupSelectQuery.ts
  34. 16
      packages/nocodb/src/helpers/catchError.ts
  35. 19
      packages/nocodb/src/helpers/columnHelpers.ts
  36. 3
      packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts
  37. 3
      packages/nocodb/src/redis/pubsub-redis.ts
  38. 86
      packages/nocodb/src/services/columns.service.ts
  39. 23
      packages/nocodb/src/services/tables.service.ts
  40. 3
      packages/nocodb/tests/unit/factory/table.ts
  41. 3
      packages/nocodb/tests/unit/rest/index.test.ts
  42. 2
      packages/nocodb/tsconfig.json
  43. 4
      tests/playwright/pages/Dashboard/Details/FieldsPage.ts
  44. 33
      tests/playwright/pages/Dashboard/Grid/Column/index.ts
  45. 349
      tests/playwright/tests/db/columns/columnLinkToAnotherRecordAdvanceOptions.spec.ts

8
packages/nc-gui/components/general/DeleteModal.vue

@ -56,7 +56,13 @@ onKeyStroke('Enter', () => {
</div> </div>
<slot name="entity-preview"></slot> <slot name="entity-preview"></slot>
<template v-if="$slots.warning">
<a-alert type="warning" show-icon>
<template #message>
<slot name="warning"></slot>
</template>
</a-alert>
</template>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end"> <div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton type="secondary" size="small" @click="visible = false"> <NcButton type="secondary" size="small" @click="visible = false">
{{ $t('general.cancel') }} {{ $t('general.cancel') }}

24
packages/nc-gui/components/nc/Select.vue

@ -1,5 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
const props = defineProps<{ import type { iconMap } from '#imports'
const props = withDefaults(
defineProps<{
value?: string | string[] value?: string | string[]
placeholder?: string placeholder?: string
mode?: 'multiple' | 'tags' mode?: 'multiple' | 'tags'
@ -11,7 +14,12 @@ const props = defineProps<{
dropdownMatchSelectWidth?: boolean dropdownMatchSelectWidth?: boolean
allowClear?: boolean allowClear?: boolean
loading?: boolean loading?: boolean
}>() suffixIcon?: keyof typeof iconMap
}>(),
{
suffixIcon: 'arrowDown',
},
)
const emits = defineEmits(['update:value', 'change']) const emits = defineEmits(['update:value', 'change'])
@ -25,15 +33,7 @@ const dropdownClassName = computed(() => {
return className return className
}) })
const showSearch = computed(() => props.showSearch) const { showSearch, filterOption, dropdownMatchSelectWidth, loading, mode } = toRefs(props)
const filterOption = computed(() => props.filterOption)
const dropdownMatchSelectWidth = computed(() => props.dropdownMatchSelectWidth)
const loading = computed(() => props.loading)
const mode = computed(() => props.mode)
const vModel = useVModel(props, 'value', emits) const vModel = useVModel(props, 'value', emits)
@ -60,7 +60,7 @@ const onChange = (value: string) => {
> >
<template #suffixIcon> <template #suffixIcon>
<GeneralLoader v-if="loading" /> <GeneralLoader v-if="loading" />
<GeneralIcon v-else class="text-gray-800 nc-select-expand-btn" icon="arrowDown" /> <GeneralIcon v-else class="text-gray-800 nc-select-expand-btn" :icon="suffixIcon" />
</template> </template>
<slot /> <slot />
</a-select> </a-select>

6
packages/nc-gui/components/smartsheet/Cell.vue

@ -51,7 +51,11 @@ const { currentRow } = useSmartsheetRowStoreOrThrow()
const { sqlUis } = storeToRefs(useBase()) const { sqlUis } = storeToRefs(useBase())
const sqlUi = ref(column.value?.source_id ? sqlUis.value[column.value?.source_id] : Object.values(sqlUis.value)[0]) const sqlUi = ref(
column.value?.source_id && sqlUis.value[column.value?.source_id]
? sqlUis.value[column.value?.source_id]
: Object.values(sqlUis.value)[0],
)
const abstractType = computed(() => column.value && sqlUi.value.getAbstractType(column.value)) const abstractType = computed(() => column.value && sqlUi.value.getAbstractType(column.value))

6
packages/nc-gui/components/smartsheet/PlainCell.vue

@ -37,7 +37,11 @@ const { basesUser } = storeToRefs(basesStore)
const { isXcdbBase, isMssql, isMysql } = useBase() const { isXcdbBase, isMssql, isMysql } = useBase()
const sqlUi = ref(column.value?.source_id ? sqlUis.value[column.value?.source_id] : Object.values(sqlUis.value)[0]) const sqlUi = ref(
column.value?.source_id && sqlUis.value[column.value?.source_id]
? sqlUis.value[column.value?.source_id]
: Object.values(sqlUis.value)[0],
)
const abstractType = computed(() => column.value && sqlUi.value.getAbstractType(column.value)) const abstractType = computed(() => column.value && sqlUi.value.getAbstractType(column.value))

1
packages/nc-gui/components/smartsheet/column/EditOrAdd.vue

@ -363,6 +363,7 @@ const isFullUpdateAllowed = computed(() => {
'min-w-[500px]': formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links, 'min-w-[500px]': formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links,
'overflow-visible': formState.uidt === UITypes.Formula, 'overflow-visible': formState.uidt === UITypes.Formula,
'!w-[600px]': formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links, '!w-[600px]': formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links,
'min-w-[422px] !w-full': isLinksOrLTAR(formState.uidt),
'shadow-lg shadow-gray-300 border-1 border-gray-200 rounded-xl p-5': !embedMode, 'shadow-lg shadow-gray-300 border-1 border-gray-200 rounded-xl p-5': !embedMode,
}" }"
@keydown="handleEscape" @keydown="handleEscape"

9
packages/nc-gui/components/smartsheet/column/LinkAdvancedOptions.vue

@ -0,0 +1,9 @@
<script setup lang="ts">
defineProps<{
value: any
}>()
</script>
<template>
<span></span>
</template>

111
packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue

@ -47,9 +47,18 @@ if (!isEdit.value) {
if (!vModel.value.onDelete) vModel.value.onDelete = onUpdateDeleteOptions[0] if (!vModel.value.onDelete) vModel.value.onDelete = onUpdateDeleteOptions[0]
if (!vModel.value.virtual) vModel.value.virtual = sqlUi === SqliteUi // appInfo.isCloud || sqlUi === SqliteUi if (!vModel.value.virtual) vModel.value.virtual = sqlUi === SqliteUi // appInfo.isCloud || sqlUi === SqliteUi
if (!vModel.value.alias) vModel.value.alias = vModel.value.column_name if (!vModel.value.alias) vModel.value.alias = vModel.value.column_name
} else {
if (!vModel.value.childId)
vModel.custom = {
ref_model_id: vModel.value?.colOptions?.fk_related_model_id,
base_id: meta.value?.base_id,
junc_base_id: meta.value?.base_id,
}
if (!vModel.value.childViewId) vModel.value.childViewId = vModel.value?.colOptions?.fk_target_view_id || null
} }
if (!vModel.value.childId) vModel.value.childId = vModel.value?.colOptions?.fk_related_model_id || null if (!vModel.value.childId) vModel.value.childId = vModel.value?.colOptions?.fk_related_model_id || null
if (!vModel.value.childViewId) vModel.value.childViewId = vModel.value?.colOptions?.fk_target_view_id || null if (!vModel.value.childViewId) vModel.value.childViewId = vModel.value?.colOptions?.fk_target_view_id || null
if (!vModel.value.type) vModel.value.type = vModel.value?.colOptions?.type || 'mm'
const advancedOptions = ref(false) const advancedOptions = ref(false)
@ -157,6 +166,41 @@ const handleUpdateRefTable = () => {
updateFieldName() updateFieldName()
}) })
} }
const isAdvancedOptionsShownEasterEgg = ref(false)
const cusValidators = {
'custom.column_id': [{ required: true, message: t('general.required') }],
'custom.ref_model_id': [{ required: true, message: t('general.required') }],
'custom.ref_column_id': [{ required: true, message: t('general.required') }],
}
const cusJuncTableValidations = {
'custom.junc_model_id': [{ required: true, message: t('general.required') }],
'custom.junc_column_id': [{ required: true, message: t('general.required') }],
'custom.junc_ref_column_id': [{ required: true, message: t('general.required') }],
}
const onCustomSwitchToggle = () => {
if (vModel.value?.is_custom_link)
setAdditionalValidations({
childId: [],
...cusValidators,
...(vModel.value.type === RelationTypes.MANY_TO_MANY ? cusJuncTableValidations : {}),
})
else
setAdditionalValidations({
childId: [{ required: true, message: t('general.required') }],
})
}
const handleShowAdvanceOptions = () => {
isAdvancedOptionsShownEasterEgg.value = !isAdvancedOptionsShownEasterEgg.value
if (!isAdvancedOptionsShownEasterEgg.value) {
vModel.value.is_custom_link = false
}
}
</script> </script>
<template> <template>
@ -176,7 +220,7 @@ const handleUpdateRefTable = () => {
</span> </span>
{{ $t('title.hasMany') }} {{ $t('title.hasMany') }}
</a-radio> </a-radio>
<a-radio value="oo" data-testid="One to One"> <a-radio value="oo" data-testid="One to One" @dblclick="handleShowAdvanceOptions">
<span class="nc-ltar-icon nc-oo-icon"> <span class="nc-ltar-icon nc-oo-icon">
<GeneralIcon icon="oneToOneSolid" /> <GeneralIcon icon="oneToOneSolid" />
</span> </span>
@ -184,8 +228,23 @@ const handleUpdateRefTable = () => {
</a-radio> </a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
</div>
<a-form-item class="flex w-full nc-ltar-child-table" v-bind="validateInfos.childId"> <div v-if="isAdvancedOptionsShownEasterEgg && isEeUI">
<a-switch
v-model:checked="vModel.is_custom_link"
:disabled="isEdit"
:is-edit="isEdit"
size="small"
name="Custom"
@change="onCustomSwitchToggle"
/>
<span class="ml-3">Advanced Link</span>
</div>
<div v-if="isEeUI && vModel.is_custom_link">
<LazySmartsheetColumnLinkAdvancedOptions v-model:value="vModel" :is-edit="isEdit" :meta="meta" />
</div>
<template v-else>
<a-form-item class="flex w-full pb-2 nc-ltar-child-table" v-bind="validateInfos.childId">
<a-select <a-select
v-model:value="referenceTableChildId" v-model:value="referenceTableChildId"
show-search show-search
@ -211,8 +270,9 @@ const handleUpdateRefTable = () => {
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</template>
<div v-if="isEeUI" class="w-full flex-col"> <template v-if="isEeUI">
<div class="flex gap-2 items-center" :class="{ 'mb-2': limitRecToView }"> <div class="flex gap-2 items-center" :class="{ 'mb-2': limitRecToView }">
<a-switch <a-switch
v-model:checked="limitRecToView" v-model:checked="limitRecToView"
@ -251,7 +311,7 @@ const handleUpdateRefTable = () => {
</NcSelect> </NcSelect>
</a-form-item> </a-form-item>
<div class="mt-4 flex gap-2 items-center" :class="{ 'mb-2': limitRecToCond }"> <div class="flex gap-2 items-center" :class="{ 'mb-2': limitRecToCond }">
<a-switch <a-switch
v-model:checked="limitRecToCond" v-model:checked="limitRecToCond"
v-e="['c:link:limit-record-by-filter', { status: limitRecToCond }]" v-e="['c:link:limit-record-by-filter', { status: limitRecToCond }]"
@ -278,8 +338,7 @@ const handleUpdateRefTable = () => {
:link-col-id="vModel.id" :link-col-id="vModel.id"
/> />
</div> </div>
</div> </template>
</div>
<template v-if="(!isXcdbBase && !isEdit) || isLinks"> <template v-if="(!isXcdbBase && !isEdit) || isLinks">
<div> <div>
<NcButton <NcButton
@ -401,3 +460,41 @@ const handleUpdateRefTable = () => {
@apply h-8.5; @apply h-8.5;
} }
</style> </style>
<!-- todo: remove later
<style lang="scss" scoped>
.nc-ltar-relation-type-radio-group {
.nc-ltar-icon {
@apply flex items-center p-1 rounded;
&.nc-mm-icon {
@apply bg-pink-500;
}
&.nc-hm-icon {
@apply bg-orange-500;
}
&.nc-oo-icon {
@apply bg-purple-500;
:deep(svg path) {
@apply stroke-purple-50;
}
}
}
:deep(.ant-radio-wrapper) {
@apply px-3 py-2 flex items-center mr-0;
&:not(:last-child) {
@apply border-b border-gray-200;
}
}
:deep(.ant-radio) {
@apply top-0;
& + span {
@apply flex items-center gap-2;
}
}
}
</style>
-->

4
packages/nc-gui/components/smartsheet/column/LookupOptions.vue

@ -42,7 +42,9 @@ const refTables = computed(() => {
.map((column) => ({ .map((column) => ({
col: column.colOptions, col: column.colOptions,
column, column,
...tables.value.find((table) => table.id === (column.colOptions as LinkToAnotherRecordType).fk_related_model_id), ...(tables.value.find((table) => table.id === (column.colOptions as LinkToAnotherRecordType).fk_related_model_id) ||
metas.value[(column.colOptions as LinkToAnotherRecordType).fk_related_model_id!] ||
{}),
})) }))
.filter((table) => (table.col as LinkToAnotherRecordType)?.fk_related_model_id === table.id && !table.mm) .filter((table) => (table.col as LinkToAnotherRecordType)?.fk_related_model_id === table.id && !table.mm)
return _refTables as Required<TableType & { column: ColumnType; col: Required<LinkToAnotherRecordType> }>[] return _refTables as Required<TableType & { column: ColumnType; col: Required<LinkToAnotherRecordType> }>[]

5
packages/nc-gui/components/smartsheet/details/Fields.vue

@ -514,7 +514,10 @@ const isColumnValid = (column: TableExplorerColumn) => {
return false return false
} }
if ((column.uidt === UITypes.Links || column.uidt === UITypes.LinkToAnotherRecord) && isNew) { if ((column.uidt === UITypes.Links || column.uidt === UITypes.LinkToAnotherRecord) && isNew) {
if (!column.childColumn || !column.childTable || !column.childId) { if (
(!column.childColumn || !column.childTable || !column.childId) &&
(!column.custom?.ref_model_id || !column.custom?.ref_column_id)
) {
return false return false
} }
} }

4
packages/nc-gui/components/smartsheet/expanded-form/index.vue

@ -112,9 +112,7 @@ const hiddenFields = computed(() => {
.filter( .filter(
(col) => (col) =>
!fields.value?.includes(col) && !fields.value?.includes(col) &&
(isLocalMode.value && col?.id && fieldsMap.value[col.id] (isLocalMode.value && col?.id && fieldsMap.value[col.id] ? fieldsMap.value[col.id]?.initialShow : true),
? fieldsMap.value[col.id]?.initialShow
: true),
) )
.filter((col) => !isSystemColumn(col)) .filter((col) => !isSystemColumn(col))
}) })

2
packages/nc-gui/components/smartsheet/grid/GroupBy.vue

@ -398,7 +398,7 @@ const bgColor = computed(() => {
@change="findAndLoadSubGroup" @change="findAndLoadSubGroup"
> >
<a-collapse-panel <a-collapse-panel
v-for="[i, grp] of Object.entries(vGroup?.children ?? [])" v-for="[_, grp] of Object.entries(vGroup?.children ?? [])"
:key="`group-panel-${grp.key}`" :key="`group-panel-${grp.key}`"
class="!border-1 border-gray-300 nc-group rounded-[8px] mb-2" class="!border-1 border-gray-300 nc-group rounded-[8px] mb-2"
:style="`background: ${bgColor};`" :style="`background: ${bgColor};`"

4
packages/nc-gui/components/smartsheet/grid/GroupByTable.vue

@ -46,10 +46,10 @@ const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook())
const { eventBus } = useSmartsheetStoreOrThrow() const { eventBus } = useSmartsheetStoreOrThrow()
const routeQuery = computed(() => route.value.query as Record<string, string>)
const route = router.currentRoute const route = router.currentRoute
const routeQuery = computed(() => route.value.query as Record<string, string>)
const expandedFormDlg = ref(false) const expandedFormDlg = ref(false)
const expandedFormRow = ref<Row>() const expandedFormRow = ref<Row>()
const expandedFormRowState = ref<Record<string, any>>() const expandedFormRowState = ref<Record<string, any>>()

4
packages/nc-gui/components/smartsheet/grid/useColumnDrag.ts

@ -109,6 +109,8 @@ export const useColumnDrag = ({
const handleReorderColumn = async () => { const handleReorderColumn = async () => {
isProcessing.value = true isProcessing.value = true
try { try {
if (!dragColPlaceholderDomRef.value) return
dragColPlaceholderDomRef.value!.style.left = '0px' dragColPlaceholderDomRef.value!.style.left = '0px'
dragColPlaceholderDomRef.value!.style.height = '0px' dragColPlaceholderDomRef.value!.style.height = '0px'
await reorderColumn(draggedCol.value!.id!, toBeDroppedColId.value!) await reorderColumn(draggedCol.value!.id!, toBeDroppedColId.value!)
@ -126,6 +128,8 @@ export const useColumnDrag = ({
const dom = document.querySelector('[data-testid="drag-icon-placeholder"]') const dom = document.querySelector('[data-testid="drag-icon-placeholder"]')
if (!dom || !dragColPlaceholderDomRef.value) return
e.dataTransfer.dropEffect = 'none' e.dataTransfer.dropEffect = 'none'
e.dataTransfer.effectAllowed = 'none' e.dataTransfer.effectAllowed = 'none'

28
packages/nc-gui/components/smartsheet/header/DeleteColumnModal.vue

@ -27,6 +27,31 @@ const viewsStore = useViewsStore()
const isLoading = ref(false) const isLoading = ref(false)
// disable for time being - internal discussion required
/*
const warningMsg = computed(() => {
if (!column?.value) return []
const columns = meta?.value?.columns.filter((c) => {
if (isLinksOrLTAR(c) && c.colOptions) {
return (
(c.colOptions as LinkToAnotherRecordType).fk_parent_column_id === column.value?.id ||
(c.colOptions as LinkToAnotherRecordType).fk_child_column_id === column.value?.id ||
(c.colOptions as LinkToAnotherRecordType).fk_mm_child_column_id === column.value?.id ||
(c.colOptions as LinkToAnotherRecordType).fk_mm_parent_column_id === column.value?.id
)
}
return false
})
if (!columns.length) return null
return `This column is used in following Link column${columns.length > 1 ? 's' : ''}: '${columns
.map((c) => c.title)
.join("', '")}'. Deleting this column will also delete the related Link column${columns.length > 1 ? 's' : ''}.`
}) */
const onDelete = async () => { const onDelete = async () => {
if (!column?.value) return if (!column?.value) return
@ -80,5 +105,8 @@ const onDelete = async () => {
</div> </div>
</div> </div>
</template> </template>
<!-- disable for time being - internal discussion required -->
<!-- <template v-if="warningMsg" #warning>{{ warningMsg }}</template> -->
</GeneralDeleteModal> </GeneralDeleteModal>
</template> </template>

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

@ -328,7 +328,8 @@ const isDuplicateAllowed = computed(() => {
return ( return (
column?.value && column?.value &&
!column.value.system && !column.value.system &&
((!isMetaReadOnly.value && !isDataReadOnly.value) || readonlyMetaAllowedTypes.includes(column.value?.uidt)) ((!isMetaReadOnly.value && !isDataReadOnly.value) || readonlyMetaAllowedTypes.includes(column.value?.uidt)) &&
!column.value.meta?.custom
) )
}) })
const isFilterSupported = computed( const isFilterSupported = computed(
@ -373,6 +374,13 @@ const isColumnEditAllowed = computed(() => {
return false return false
return true return true
}) })
// check if the column is associated as foreign key in any of the link column
const linksAssociated = computed(() => {
return meta.value?.columns?.filter(
(c) => isLinksOrLTAR(c) && [c.colOptions?.fk_child_column_id, c.colOptions?.fk_parent_column_id].includes(column?.value?.id),
)
})
</script> </script>
<template> <template>
@ -398,7 +406,8 @@ const isColumnEditAllowed = computed(() => {
<GeneralSourceRestrictionTooltip message="Field properties cannot be edited." :enabled="!isColumnEditAllowed"> <GeneralSourceRestrictionTooltip message="Field properties cannot be edited." :enabled="!isColumnEditAllowed">
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('fieldAlter')" v-if="isUIAllowed('fieldAlter')"
:disabled="column?.pk || isSystemColumn(column) || !isColumnEditAllowed" :disabled="column?.pk || isSystemColumn(column) || !isColumnEditAllowed || linksAssociated.length"
:title="linksAssociated.length ? 'Field is associated with a link column' : undefined"
@click="onEditPress" @click="onEditPress"
> >
<div class="nc-column-edit nc-header-menu-item"> <div class="nc-column-edit nc-header-menu-item">
@ -408,7 +417,6 @@ const isColumnEditAllowed = computed(() => {
</div> </div>
</NcMenuItem> </NcMenuItem>
</GeneralSourceRestrictionTooltip> </GeneralSourceRestrictionTooltip>
<GeneralSourceRestrictionTooltip message="Field cannot be duplicated." :enabled="!isDuplicateAllowed"> <GeneralSourceRestrictionTooltip message="Field cannot be duplicated." :enabled="!isDuplicateAllowed">
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('duplicateColumn') && isExpandedForm && !column?.pk" v-if="isUIAllowed('duplicateColumn') && isExpandedForm && !column?.pk"
@ -553,8 +561,9 @@ const isColumnEditAllowed = computed(() => {
<GeneralSourceRestrictionTooltip message="Field cannot be deleted." :enabled="!isColumnUpdateAllowed"> <GeneralSourceRestrictionTooltip message="Field cannot be deleted." :enabled="!isColumnUpdateAllowed">
<NcMenuItem <NcMenuItem
v-if="!column?.pv && isUIAllowed('fieldDelete')" v-if="!column?.pv && isUIAllowed('fieldDelete')"
:disabled="!isDeleteAllowed || !isColumnUpdateAllowed" :disabled="!isDeleteAllowed || !isColumnUpdateAllowed || linksAssociated.length"
class="!hover:bg-red-50" class="!hover:bg-red-50"
:title="linksAssociated ? 'Field is associated with a link column' : undefined"
@click="handleDelete" @click="handleDelete"
> >
<div <div

6
packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue

@ -65,7 +65,11 @@ type FilterType = keyof typeof checkTypeFunctions
const { sqlUis } = storeToRefs(useBase()) const { sqlUis } = storeToRefs(useBase())
const sqlUi = ref(column.value?.source_id ? sqlUis.value[column.value?.source_id] : Object.values(sqlUis.value)[0]) const sqlUi = ref(
column.value?.source_id && sqlUis.value[column.value?.source_id]
? sqlUis.value[column.value?.source_id]
: Object.values(sqlUis.value)[0],
)
const abstractType = computed(() => column.value && sqlUi.value.getAbstractType(column.value)) const abstractType = computed(() => column.value && sqlUi.value.getAbstractType(column.value))

9
packages/nc-gui/composables/useColumnCreateStore.ts

@ -79,6 +79,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const formState = ref<Record<string, any>>({ const formState = ref<Record<string, any>>({
title: '', title: '',
uidt: fromTableExplorer?.value ? defaultType : null, uidt: fromTableExplorer?.value ? defaultType : null,
custom: {},
...clone(column.value || {}), ...clone(column.value || {}),
}) })
@ -89,6 +90,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const colProp = sqlUi.value.getDataTypeForUiType(formState.value as { uidt: UITypes }, idType ?? undefined) const colProp = sqlUi.value.getDataTypeForUiType(formState.value as { uidt: UITypes }, idType ?? undefined)
formState.value = { formState.value = {
custom: {},
...(!isEdit.value && { ...(!isEdit.value && {
// only take title, column_name and uidt when creating a column // only take title, column_name and uidt when creating a column
// to avoid the extra props from being taken (e.g. SingleLineText -> LTAR -> SingleLineText) // to avoid the extra props from being taken (e.g. SingleLineText -> LTAR -> SingleLineText)
@ -284,6 +286,9 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
try { try {
formState.value.table_name = meta.value?.table_name formState.value.table_name = meta.value?.table_name
const refModelId = formState.value.custom?.ref_model_id
// formState.value.title = formState.value.column_name // formState.value.title = formState.value.column_name
if (column.value) { if (column.value) {
// reset column validation if column is not to be validated // reset column validation if column is not to be validated
@ -337,8 +342,12 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
/** if LTAR column then force reload related table meta */ /** if LTAR column then force reload related table meta */
if (isLinksOrLTAR(formState.value) && meta.value?.id !== formState.value.childId) { if (isLinksOrLTAR(formState.value) && meta.value?.id !== formState.value.childId) {
if (refModelId) {
getMeta(refModelId, true).then(() => {})
} else {
getMeta(formState.value.childId, true).then(() => {}) getMeta(formState.value.childId, true).then(() => {})
} }
}
// Column created // Column created
// message.success(t('msg.success.columnCreated')) // message.success(t('msg.success.columnCreated'))

2
packages/nc-gui/composables/useExpandedFormStore.ts

@ -331,7 +331,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
if (missingRequiredColumns.size) return if (missingRequiredColumns.size) return
data = await $api.dbTableRow.create('noco', base.value.id as string, meta.value.id, { data = await $api.dbTableRow.create('noco', meta.value.base_id, meta.value.id, {
...insertObj, ...insertObj,
...(ltarState || {}), ...(ltarState || {}),
}) })

2
packages/nc-gui/composables/useViewColumns.ts

@ -136,7 +136,7 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
if (isLocalMode.value) { if (isLocalMode.value) {
const fieldById = (fields.value || []).reduce<Record<string, any>>((acc, curr) => { const fieldById = (fields.value || []).reduce<Record<string, any>>((acc, curr) => {
if (curr.fk_column_id) { if (curr.fk_column_id) {
curr.show = curr.initialShow ? true : false curr.show = !!curr.initialShow
acc[curr.fk_column_id] = curr acc[curr.fk_column_id] = curr
} }
return acc return acc

2
packages/nc-gui/store/sidebar.ts

@ -1,5 +1,5 @@
import { acceptHMRUpdate, defineStore } from 'pinia' import { acceptHMRUpdate, defineStore } from 'pinia'
import { MAX_WIDTH_FOR_MOBILE_MODE, INITIAL_LEFT_SIDEBAR_WIDTH } from '~/lib/constants' import { INITIAL_LEFT_SIDEBAR_WIDTH, MAX_WIDTH_FOR_MOBILE_MODE } from '~/lib/constants'
export const useSidebarStore = defineStore('sidebarStore', () => { export const useSidebarStore = defineStore('sidebarStore', () => {
const { width } = useWindowSize() const { width } = useWindowSize()

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

@ -190,6 +190,8 @@ export enum NcErrorType {
UNKNOWN_ERROR = 'UNKNOWN_ERROR', UNKNOWN_ERROR = 'UNKNOWN_ERROR',
BAD_JSON = 'BAD_JSON', BAD_JSON = 'BAD_JSON',
INVALID_PK_VALUE = 'INVALID_PK_VALUE', INVALID_PK_VALUE = 'INVALID_PK_VALUE',
COLUMN_ASSOCIATED_WITH_LINK = 'COLUMN_ASSOCIATED_WITH_LINK',
TABLE_ASSOCIATED_WITH_LINK = 'TABLE_ASSOCIATED_WITH_LINK',
} }
type Roles = OrgUserRoles | ProjectRoles | WorkspaceUserRoles; type Roles = OrgUserRoles | ProjectRoles | WorkspaceUserRoles;

60
packages/nocodb-sdk/src/lib/sqlUi/DatabricksUi.ts

@ -531,7 +531,7 @@ export class DatabricksUi {
} }
} }
static getDataTypeForUiType(col: { uidt: UITypes; }) { static getDataTypeForUiType(col: { uidt: UITypes }) {
const colProp: any = {}; const colProp: any = {};
switch (col.uidt) { switch (col.uidt) {
case 'ID': case 'ID':
@ -667,7 +667,7 @@ export class DatabricksUi {
return colProp; return colProp;
} }
static getDataTypeListForUiType(col: { uidt: UITypes; }, idType?: IDType) { static getDataTypeListForUiType(col: { uidt: UITypes }, idType?: IDType) {
switch (col.uidt) { switch (col.uidt) {
case 'ID': case 'ID':
if (idType === 'AG') { if (idType === 'AG') {
@ -696,9 +696,7 @@ export class DatabricksUi {
return ['string']; return ['string'];
case 'Checkbox': case 'Checkbox':
return [ return ['boolean'];
'boolean',
];
case 'MultiSelect': case 'MultiSelect':
return ['string']; return ['string'];
@ -707,14 +705,10 @@ export class DatabricksUi {
return ['string']; return ['string'];
case 'Year': case 'Year':
return [ return ['int'];
'int',
];
case 'Time': case 'Time':
return [ return ['string'];
'string',
];
case 'PhoneNumber': case 'PhoneNumber':
case 'Email': case 'Email':
@ -724,32 +718,22 @@ export class DatabricksUi {
return ['string']; return ['string'];
case 'Number': case 'Number':
return [ return ['int'];
'int',
];
case 'Decimal': case 'Decimal':
return ['decimal', 'float', 'double']; return ['decimal', 'float', 'double'];
case 'Currency': case 'Currency':
return [ return ['decimal'];
'decimal',
];
case 'Percent': case 'Percent':
return [ return ['decimal'];
'decimal',
];
case 'Duration': case 'Duration':
return [ return ['decimal'];
'decimal',
];
case 'Rating': case 'Rating':
return [ return ['int'];
'int',
];
case 'Formula': case 'Formula':
return ['string']; return ['string'];
@ -758,9 +742,7 @@ export class DatabricksUi {
return ['string']; return ['string'];
case 'Count': case 'Count':
return [ return ['int'];
'int',
];
case 'Lookup': case 'Lookup':
return ['string']; return ['string'];
@ -774,9 +756,7 @@ export class DatabricksUi {
return ['datetime']; return ['datetime'];
case 'AutoNumber': case 'AutoNumber':
return [ return ['int'];
'int',
];
case 'Barcode': case 'Barcode':
return ['string']; return ['string'];
@ -813,4 +793,20 @@ export class DatabricksUi {
'HOUR', 'HOUR',
]; ];
} }
static isEqual(dataType1: string, dataType2: string) {
if (dataType1 === dataType2) return true;
const abstractType1 = this.getAbstractType({ dt: dataType1 });
const abstractType2 = this.getAbstractType({ dt: dataType2 });
if (
abstractType1 &&
abstractType1 === abstractType2 &&
['integer', 'float'].includes(abstractType1)
)
return true;
return false;
}
} }

16
packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts

@ -968,4 +968,20 @@ export class MssqlUi {
'DATESTR', 'DATESTR',
]; ];
} }
static isEqual(dataType1: string, dataType2: string) {
if (dataType1 === dataType2) return true;
const abstractType1 = this.getAbstractType({ dt: dataType1 });
const abstractType2 = this.getAbstractType({ dt: dataType2 });
if (
abstractType1 &&
abstractType1 === abstractType2 &&
['integer', 'float'].includes(abstractType1)
)
return true;
return false;
}
} }

16
packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts

@ -1334,4 +1334,20 @@ export class MysqlUi {
static getUnsupportedFnList() { static getUnsupportedFnList() {
return ['COUNTA', 'COUNT', 'DATESTR']; return ['COUNTA', 'COUNT', 'DATESTR'];
} }
static isEqual(dataType1: string, dataType2: string) {
if (dataType1 === dataType2) return true;
const abstractType1 = this.getAbstractType({ dt: dataType1 });
const abstractType2 = this.getAbstractType({ dt: dataType2 });
if (
abstractType1 &&
abstractType1 === abstractType2 &&
['integer', 'float'].includes(abstractType1)
)
return true;
return false;
}
} }

16
packages/nocodb-sdk/src/lib/sqlUi/OracleUi.ts

@ -951,6 +951,22 @@ export class OracleUi {
'DATESTR', 'DATESTR',
]; ];
} }
static isEqual(dataType1: string, dataType2: string) {
if (dataType1 === dataType2) return true;
const abstractType1 = this.getAbstractType({ dt: dataType1 });
const abstractType2 = this.getAbstractType({ dt: dataType2 });
if (
abstractType1 &&
abstractType1 === abstractType2 &&
['integer', 'float'].includes(abstractType1)
)
return true;
return false;
}
} }
// module.exports = PgUiHelp; // module.exports = PgUiHelp;

20
packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts

@ -1613,7 +1613,7 @@ export class PgUi {
} }
} }
static getDataTypeForUiType(col: { uidt: UITypes; }, idType?: IDType) { static getDataTypeForUiType(col: { uidt: UITypes }, idType?: IDType) {
const colProp: any = {}; const colProp: any = {};
switch (col.uidt) { switch (col.uidt) {
case 'ID': case 'ID':
@ -1754,7 +1754,7 @@ export class PgUi {
return colProp; return colProp;
} }
static getDataTypeListForUiType(col: { uidt?: UITypes; }, idType: IDType) { static getDataTypeListForUiType(col: { uidt?: UITypes }, idType: IDType) {
switch (col.uidt) { switch (col.uidt) {
case 'ID': case 'ID':
if (idType === 'AG') { if (idType === 'AG') {
@ -2031,6 +2031,22 @@ export class PgUi {
static getUnsupportedFnList() { static getUnsupportedFnList() {
return []; return [];
} }
static isEqual(dataType1: string, dataType2: string) {
if (dataType1?.toLowerCase() === dataType2?.toLowerCase()) return true;
const abstractType1 = this.getAbstractType({ dt: dataType1 });
const abstractType2 = this.getAbstractType({ dt: dataType2 });
if (
abstractType1 &&
abstractType1 === abstractType2 &&
['integer', 'float'].includes(abstractType1)
)
return true;
return false;
}
} }
// module.exports = PgUiHelp; // module.exports = PgUiHelp;

20
packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts

@ -707,7 +707,7 @@ export class SnowflakeUi {
} }
} }
static getDataTypeForUiType(col: { uidt: UITypes; }, idType?: IDType) { static getDataTypeForUiType(col: { uidt: UITypes }, idType?: IDType) {
const colProp: any = {}; const colProp: any = {};
switch (col.uidt) { switch (col.uidt) {
case 'ID': case 'ID':
@ -847,7 +847,7 @@ export class SnowflakeUi {
return colProp; return colProp;
} }
static getDataTypeListForUiType(col: { uidt: UITypes; }, idType: IDType) { static getDataTypeListForUiType(col: { uidt: UITypes }, idType: IDType) {
switch (col.uidt) { switch (col.uidt) {
case 'ID': case 'ID':
if (idType === 'AG') { if (idType === 'AG') {
@ -1034,6 +1034,22 @@ export class SnowflakeUi {
'DATESTR', 'DATESTR',
]; ];
} }
static isEqual(dataType1: string, dataType2: string) {
if (dataType1 === dataType2) return true;
const abstractType1 = this.getAbstractType({ dt: dataType1 });
const abstractType2 = this.getAbstractType({ dt: dataType2 });
if (
abstractType1 &&
abstractType1 === abstractType2 &&
['integer', 'float'].includes(abstractType1)
)
return true;
return false;
}
} }
// module.exports = SnowflakeUiHelp; // module.exports = SnowflakeUiHelp;

20
packages/nocodb-sdk/src/lib/sqlUi/SqliteUi.ts

@ -556,7 +556,7 @@ export class SqliteUi {
} }
} }
static getDataTypeForUiType(col: { uidt: UITypes; }, idType?: IDType) { static getDataTypeForUiType(col: { uidt: UITypes }, idType?: IDType) {
const colProp: any = {}; const colProp: any = {};
switch (col.uidt) { switch (col.uidt) {
case 'ID': case 'ID':
@ -697,7 +697,7 @@ export class SqliteUi {
return colProp; return colProp;
} }
static getDataTypeListForUiType(col: { uidt: UITypes; }, idType?: IDType) { static getDataTypeListForUiType(col: { uidt: UITypes }, idType?: IDType) {
switch (col.uidt) { switch (col.uidt) {
case 'ID': case 'ID':
if (idType === 'AG') { if (idType === 'AG') {
@ -928,4 +928,20 @@ export class SqliteUi {
'HOUR', 'HOUR',
]; ];
} }
static isEqual(dataType1: string, dataType2: string) {
if (dataType1 === dataType2) return true;
const abstractType1 = this.getAbstractType({ dt: dataType1 });
const abstractType2 = this.getAbstractType({ dt: dataType2 });
if (
abstractType1 &&
abstractType1 === abstractType2 &&
['integer', 'float'].includes(abstractType1)
)
return true;
return false;
}
} }

9
packages/nocodb/src/controllers/notifications.controller.ts

@ -45,7 +45,8 @@ export class NotificationsController {
this.notificationsService.addConnection(req.user.id, res); this.notificationsService.addConnection(req.user.id, res);
let unsubscribeCallback: (keepRedisChannel?: boolean) => Promise<void> = null; let unsubscribeCallback: (keepRedisChannel?: boolean) => Promise<void> =
null;
if (PubSubRedis.available) { if (PubSubRedis.available) {
unsubscribeCallback = await PubSubRedis.subscribe( unsubscribeCallback = await PubSubRedis.subscribe(
@ -57,7 +58,11 @@ export class NotificationsController {
} }
res.on('close', async () => { res.on('close', async () => {
await this.notificationsService.removeConnection(req.user.id, res, unsubscribeCallback); await this.notificationsService.removeConnection(
req.user.id,
res,
unsubscribeCallback,
);
}); });
setTimeout(() => { setTimeout(() => {

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

@ -15,8 +15,6 @@ import { GlobalGuard } from '~/guards/global/global.guard';
import { AppHooksService } from '~/services/app-hooks/app-hooks.service'; import { AppHooksService } from '~/services/app-hooks/app-hooks.service';
import { UsersService } from '~/services/users/users.service'; import { UsersService } from '~/services/users/users.service';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard'; import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
import { TenantContext } from '~/decorators/tenant-context.decorator';
import { NcContext, NcRequest } from '~/interface/config';
@Controller() @Controller()
export class UsersController { export class UsersController {

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

@ -1713,13 +1713,13 @@ class BaseModelSqlv2 {
)) as LinkToAnotherRecordColumn )) as LinkToAnotherRecordColumn
).getParentColumn(this.context); ).getParentColumn(this.context);
const parentTable = await parentCol.getModel(this.context); const parentTable = await parentCol.getModel(this.context);
const childModel = await Model.getBaseModelSQL(this.context, { const childBaseModel = await Model.getBaseModelSQL(this.context, {
model: childTable, model: childTable,
dbDriver: this.dbDriver, dbDriver: this.dbDriver,
}); });
await parentTable.getColumns(this.context); await parentTable.getColumns(this.context);
const childTn = this.getTnPath(childTable); const childTn = childBaseModel.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable); const parentTn = this.getTnPath(parentTable);
const qb = this.dbDriver(childTn); const qb = this.dbDriver(childTn);
@ -1736,7 +1736,7 @@ class BaseModelSqlv2 {
qb.limit(+rest?.limit || 25); qb.limit(+rest?.limit || 25);
qb.offset(+rest?.offset || 0); qb.offset(+rest?.offset || 0);
await childModel.selectObject({ await childBaseModel.selectObject({
qb, qb,
fieldsSet: args.fieldSet, fieldsSet: args.fieldSet,
}); });
@ -1790,7 +1790,11 @@ class BaseModelSqlv2 {
const parentTable = await parentCol.getModel(this.context); const parentTable = await parentCol.getModel(this.context);
await parentTable.getColumns(this.context); await parentTable.getColumns(this.context);
const childTn = this.getTnPath(childTable); const childBaseModel = await Model.getBaseModelSQL(this.context, {
dbDriver: this.dbDriver,
model: childTable,
});
const childTn = childBaseModel.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable); const parentTn = this.getTnPath(parentTable);
const query = this.dbDriver(childTn) const query = this.dbDriver(childTn)
@ -1945,7 +1949,12 @@ class BaseModelSqlv2 {
)) as LinkToAnotherRecordColumn; )) as LinkToAnotherRecordColumn;
const mmTable = await relColOptions.getMMModel(this.context); const mmTable = await relColOptions.getMMModel(this.context);
const vtn = this.getTnPath(mmTable); const assocBaseModel = await Model.getBaseModelSQL(this.context, {
id: mmTable.id,
dbDriver: this.dbDriver,
});
const vtn = assocBaseModel.getTnPath(mmTable);
const vcn = (await relColOptions.getMMChildColumn(this.context)) const vcn = (await relColOptions.getMMChildColumn(this.context))
.column_name; .column_name;
const vrcn = (await relColOptions.getMMParentColumn(this.context)) const vrcn = (await relColOptions.getMMParentColumn(this.context))
@ -1980,8 +1989,16 @@ class BaseModelSqlv2 {
).getModel(this.context); ).getModel(this.context);
await parentTable.getColumns(this.context); await parentTable.getColumns(this.context);
const childTn = this.getTnPath(childTable); const parentBaseModel = await Model.getBaseModelSQL(this.context, {
const parentTn = this.getTnPath(parentTable); id: parentTable.id,
dbDriver: this.dbDriver,
});
const childBaseModel = await Model.getBaseModelSQL(this.context, {
id: childTable.id,
dbDriver: this.dbDriver,
});
const childTn = childBaseModel.getTnPath(childTable);
const parentTn = parentBaseModel.getTnPath(parentTable);
const rtn = childTn; const rtn = childTn;
const qb = this.dbDriver(rtn) const qb = this.dbDriver(rtn)
@ -2096,7 +2113,13 @@ class BaseModelSqlv2 {
)) as LinkToAnotherRecordColumn; )) as LinkToAnotherRecordColumn;
const mmTable = await relColOptions.getMMModel(this.context); const mmTable = await relColOptions.getMMModel(this.context);
const vtn = this.getTnPath(mmTable);
const assocBaseModel = await Model.getBaseModelSQL(this.context, {
model: mmTable,
dbDriver: this.dbDriver,
});
const vtn = assocBaseModel.getTnPath(mmTable);
const vcn = (await relColOptions.getMMChildColumn(this.context)) const vcn = (await relColOptions.getMMChildColumn(this.context))
.column_name; .column_name;
const vrcn = (await relColOptions.getMMParentColumn(this.context)) const vrcn = (await relColOptions.getMMParentColumn(this.context))
@ -2112,7 +2135,12 @@ class BaseModelSqlv2 {
).getModel(this.context); ).getModel(this.context);
await parentTable.getColumns(this.context); await parentTable.getColumns(this.context);
const childTn = this.getTnPath(childTable); const childBaseModel = await Model.getBaseModelSQL(this.context, {
dbDriver: this.dbDriver,
model: childTable,
});
const childTn = childBaseModel.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable); const parentTn = this.getTnPath(parentTable);
const rtn = childTn; const rtn = childTn;
@ -2162,20 +2190,37 @@ class BaseModelSqlv2 {
)) as LinkToAnotherRecordColumn; )) as LinkToAnotherRecordColumn;
const mmTable = await relColOptions.getMMModel(this.context); const mmTable = await relColOptions.getMMModel(this.context);
const vtn = this.getTnPath(mmTable); const assocBaseModel = await Model.getBaseModelSQL(this.context, {
id: mmTable.id,
dbDriver: this.dbDriver,
});
const vtn = assocBaseModel.getTnPath(mmTable);
const vcn = (await relColOptions.getMMChildColumn(this.context)) const vcn = (await relColOptions.getMMChildColumn(this.context))
.column_name; .column_name;
const vrcn = (await relColOptions.getMMParentColumn(this.context)) const vrcn = (await relColOptions.getMMParentColumn(this.context))
.column_name; .column_name;
const rcn = (await relColOptions.getParentColumn(this.context)).column_name; const rcn = (await relColOptions.getParentColumn(this.context)).column_name;
const cn = (await relColOptions.getChildColumn(this.context)).column_name; const cn = (await relColOptions.getChildColumn(this.context)).column_name;
const childTable = await ( const childTable = await (
await relColOptions.getParentColumn(this.context) await relColOptions.getParentColumn(this.context)
).getModel(this.context); ).getModel(this.context);
const childModel = await Model.getBaseModelSQL(this.context, { const parentTable = await (
await relColOptions.getChildColumn(this.context)
).getModel(this.context);
await parentTable.getColumns(this.context);
const parentBaseModel = await Model.getBaseModelSQL(this.context, {
id: parentTable.id,
dbDriver: this.dbDriver, dbDriver: this.dbDriver,
model: childTable,
}); });
const childBaseModel = await Model.getBaseModelSQL(this.context, {
dbDriver: this.dbDriver,
id: childTable.id,
});
const childTn = childBaseModel.getTnPath(childTable);
const parentTn = parentBaseModel.getTnPath(parentTable);
const childView = await relColOptions.getChildView(this.context); const childView = await relColOptions.getChildView(this.context);
let listArgs: any = {}; let listArgs: any = {};
if (childView) { if (childView) {
@ -2188,14 +2233,6 @@ class BaseModelSqlv2 {
listArgs = dependencyFields; listArgs = dependencyFields;
} }
const parentTable = await (
await relColOptions.getChildColumn(this.context)
).getModel(this.context);
await parentTable.getColumns(this.context);
const childTn = this.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable);
const rtn = childTn; const rtn = childTn;
const qb = this.dbDriver(rtn).where((qb) => const qb = this.dbDriver(rtn).where((qb) =>
@ -2220,7 +2257,7 @@ class BaseModelSqlv2 {
await this.shuffle({ qb }); await this.shuffle({ qb });
} }
await childModel.selectObject({ await childBaseModel.selectObject({
qb, qb,
fieldsSet: listArgs?.fieldsSet, fieldsSet: listArgs?.fieldsSet,
viewId: childView?.id, viewId: childView?.id,
@ -2248,7 +2285,7 @@ class BaseModelSqlv2 {
applyPaginate(qb, rest); applyPaginate(qb, rest);
const proto = await childModel.getProto(); const proto = await childBaseModel.getProto();
const data = await this.execAndParse( const data = await this.execAndParse(
qb, qb,
await childTable.getColumns(this.context), await childTable.getColumns(this.context),
@ -2280,16 +2317,20 @@ class BaseModelSqlv2 {
const parentTable = await ( const parentTable = await (
await relColOptions.getParentColumn(this.context) await relColOptions.getParentColumn(this.context)
).getModel(this.context); ).getModel(this.context);
const childModel = await Model.getBaseModelSQL(this.context, { const childBaseModel = await Model.getBaseModelSQL(this.context, {
dbDriver: this.dbDriver, dbDriver: this.dbDriver,
model: childTable, model: childTable,
}); });
const parentBaseModel = await Model.getBaseModelSQL(this.context, {
dbDriver: this.dbDriver,
model: parentTable,
});
await parentTable.getColumns(this.context); await parentTable.getColumns(this.context);
const childView = await relColOptions.getChildView(this.context); const childView = await relColOptions.getChildView(this.context);
const childTn = this.getTnPath(childTable); const childTn = childBaseModel.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable); const parentTn = parentBaseModel.getTnPath(parentTable);
const tn = childTn; const tn = childTn;
const rtn = parentTn; const rtn = parentTn;
@ -2308,7 +2349,7 @@ class BaseModelSqlv2 {
await this.shuffle({ qb }); await this.shuffle({ qb });
} }
await childModel.selectObject({ qb }); await childBaseModel.selectObject({ qb });
const aliasColObjMap = await childTable.getAliasColObjMap(this.context); const aliasColObjMap = await childTable.getAliasColObjMap(this.context);
const filterObj = extractFilterFromXwhere(where, aliasColObjMap); const filterObj = extractFilterFromXwhere(where, aliasColObjMap);
@ -2330,12 +2371,11 @@ class BaseModelSqlv2 {
applyPaginate(qb, rest); applyPaginate(qb, rest);
const proto = await childModel.getProto(); const proto = await childBaseModel.getProto();
const data = await this.execAndParse( const data = await this.execAndParse(
qb, qb,
await childTable.getColumns(this.context), await childTable.getColumns(this.context),
); );
return data.map((c) => { return data.map((c) => {
c.__proto__ = proto; c.__proto__ = proto;
return c; return c;
@ -2367,7 +2407,12 @@ class BaseModelSqlv2 {
const childView = await relColOptions.getChildView(this.context); const childView = await relColOptions.getChildView(this.context);
const childTn = this.getTnPath(childTable); const childBaseModel = await Model.getBaseModelSQL(this.context, {
dbDriver: this.dbDriver,
model: childTable,
});
const childTn = childBaseModel.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable); const parentTn = this.getTnPath(parentTable);
const tn = childTn; const tn = childTn;
@ -2530,8 +2575,13 @@ class BaseModelSqlv2 {
await relColOptions.getChildColumn(this.context) await relColOptions.getChildColumn(this.context)
).getModel(this.context); ).getModel(this.context);
const parentBaseModel = await Model.getBaseModelSQL(this.context, {
dbDriver: this.dbDriver,
model: parentTable,
});
const childTn = this.getTnPath(childTable); const childTn = this.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable); const parentTn = parentBaseModel.getTnPath(parentTable);
const rtn = parentTn; const rtn = parentTn;
const tn = childTn; const tn = childTn;
@ -2591,9 +2641,16 @@ class BaseModelSqlv2 {
).getModel(this.context); ).getModel(this.context);
const childView = await relColOptions.getChildView(this.context); const childView = await relColOptions.getChildView(this.context);
const parentBaseModel = await Model.getBaseModelSQL(this.context, {
const childTn = this.getTnPath(childTable); dbDriver: this.dbDriver,
const parentTn = this.getTnPath(parentTable); model: parentTable,
});
const childBaseModel = await Model.getBaseModelSQL(this.context, {
dbDriver: this.dbDriver,
model: childTable,
});
const childTn = childBaseModel.getTnPath(childTable);
const parentTn = parentBaseModel.getTnPath(parentTable);
const rtn = parentTn; const rtn = parentTn;
const tn = childTn; const tn = childTn;
@ -2652,13 +2709,13 @@ class BaseModelSqlv2 {
const childTable = await ( const childTable = await (
await relColOptions.getChildColumn(this.context) await relColOptions.getChildColumn(this.context)
).getModel(this.context); ).getModel(this.context);
const parentModel = await Model.getBaseModelSQL(this.context, { const parentBaseModel = await Model.getBaseModelSQL(this.context, {
dbDriver: this.dbDriver, dbDriver: this.dbDriver,
model: parentTable, model: parentTable,
}); });
const childTn = this.getTnPath(childTable); const childTn = this.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable); const parentTn = parentBaseModel.getTnPath(parentTable);
const rtn = parentTn; const rtn = parentTn;
const tn = childTn; const tn = childTn;
@ -2679,7 +2736,7 @@ class BaseModelSqlv2 {
await this.shuffle({ qb }); await this.shuffle({ qb });
} }
await parentModel.selectObject({ qb }); await parentBaseModel.selectObject({ qb });
const aliasColObjMap = await parentTable.getAliasColObjMap(this.context); const aliasColObjMap = await parentTable.getAliasColObjMap(this.context);
const filterObj = extractFilterFromXwhere(where, aliasColObjMap); const filterObj = extractFilterFromXwhere(where, aliasColObjMap);
@ -2706,7 +2763,7 @@ class BaseModelSqlv2 {
applyPaginate(qb, rest); applyPaginate(qb, rest);
const proto = await parentModel.getProto(); const proto = await parentBaseModel.getProto();
const data = await this.execAndParse( const data = await this.execAndParse(
qb, qb,
await parentTable.getColumns(this.context), await parentTable.getColumns(this.context),
@ -5525,8 +5582,18 @@ class BaseModelSqlv2 {
await childTable.getColumns(this.context); await childTable.getColumns(this.context);
await parentTable.getColumns(this.context); await parentTable.getColumns(this.context);
const childTn = this.getTnPath(childTable); const parentBaseModel = await Model.getBaseModelSQL(this.context, {
const parentTn = this.getTnPath(parentTable); model: parentTable,
dbDriver: this.dbDriver,
});
const childBaseModel = await Model.getBaseModelSQL(this.context, {
dbDriver: this.dbDriver,
model: childTable,
});
const childTn = childBaseModel.getTnPath(childTable);
const parentTn = parentBaseModel.getTnPath(parentTable);
const relatedChildCol = getRelatedLinksColumn( const relatedChildCol = getRelatedLinksColumn(
column, column,
@ -5582,15 +5649,22 @@ class BaseModelSqlv2 {
const vParentCol = await colOptions.getMMParentColumn(this.context); const vParentCol = await colOptions.getMMParentColumn(this.context);
const vTable = await colOptions.getMMModel(this.context); const vTable = await colOptions.getMMModel(this.context);
const vTn = this.getTnPath(vTable); const assocBaseModel = await Model.getBaseModelSQL(this.context, {
model: vTable,
dbDriver: this.dbDriver,
});
const vTn = assocBaseModel.getTnPath(vTable);
if (this.isSnowflake || this.isDatabricks) { if (this.isSnowflake || this.isDatabricks) {
const parentPK = this.dbDriver(parentTn) const parentPK = parentBaseModel
.dbDriver(parentTn)
.select(parentColumn.column_name) .select(parentColumn.column_name)
.where(_wherePk(parentTable.primaryKeys, childId)) .where(_wherePk(parentTable.primaryKeys, childId))
.first(); .first();
const childPK = this.dbDriver(childTn) const childPK = childBaseModel
.dbDriver(childTn)
.select(childColumn.column_name) .select(childColumn.column_name)
.where(_wherePk(childTable.primaryKeys, rowId)) .where(_wherePk(childTable.primaryKeys, rowId))
.first(); .first();
@ -5621,11 +5695,13 @@ class BaseModelSqlv2 {
} }
await this.updateLastModified({ await this.updateLastModified({
baseModel: parentBaseModel,
model: parentTable, model: parentTable,
rowIds: [childId], rowIds: [childId],
cookie, cookie,
}); });
await this.updateLastModified({ await this.updateLastModified({
baseModel: childBaseModel,
model: childTable, model: childTable,
rowIds: [rowId], rowIds: [rowId],
cookie, cookie,
@ -5641,14 +5717,20 @@ class BaseModelSqlv2 {
{ {
const linkedHmRowObj = await this.execAndParse( const linkedHmRowObj = await this.execAndParse(
this.dbDriver(childTn) this.dbDriver(childTn)
.select(`${childTable.table_name}.${childColumn.column_name}`) .select(
...new Set(
[childColumn, ...childTable.primaryKeys].map(
(col) => `${childTable.table_name}.${col.column_name}`,
),
),
)
.where(_wherePk(childTable.primaryKeys, childId)), .where(_wherePk(childTable.primaryKeys, childId)),
null, null,
{ raw: true, first: true }, { raw: true, first: true },
); );
const oldRowId = linkedHmRowObj const oldRowId = linkedHmRowObj
? Object.values(linkedHmRowObj)?.[0] ? linkedHmRowObj?.[childTable.primaryKey?.column_name]
: null; : null;
if (oldRowId) { if (oldRowId) {
@ -5699,6 +5781,7 @@ class BaseModelSqlv2 {
await triggerAfterRemoveChild(); await triggerAfterRemoveChild();
await this.updateLastModified({ await this.updateLastModified({
baseModel: parentBaseModel,
model: parentTable, model: parentTable,
rowIds: [rowId], rowIds: [rowId],
cookie, cookie,
@ -5752,14 +5835,20 @@ class BaseModelSqlv2 {
} else { } else {
const linkedHmRowObj = await this.execAndParse( const linkedHmRowObj = await this.execAndParse(
this.dbDriver(childTn) this.dbDriver(childTn)
.select(childColumn.column_name) .select(
...new Set(
[childColumn, ...childTable.primaryKeys].map(
(col) => col.column_name,
),
),
)
.where(_wherePk(childTable.primaryKeys, rowId)), .where(_wherePk(childTable.primaryKeys, rowId)),
null, null,
{ raw: true, first: true }, { raw: true, first: true },
); );
const oldChildRowId = linkedHmRowObj const oldChildRowId = linkedHmRowObj
? Object.values(linkedHmRowObj)?.[0] ? linkedHmRowObj[childTable.primaryKeys[0]?.column_name]
: null; : null;
if (oldChildRowId) { if (oldChildRowId) {
@ -5811,6 +5900,7 @@ class BaseModelSqlv2 {
await triggerAfterRemoveChild(); await triggerAfterRemoveChild();
await this.updateLastModified({ await this.updateLastModified({
baseModel: parentBaseModel,
model: parentTable, model: parentTable,
rowIds: [childId], rowIds: [childId],
cookie, cookie,
@ -5830,14 +5920,20 @@ class BaseModelSqlv2 {
// 1. check current row is linked with another child // 1. check current row is linked with another child
linkedCurrentOoRowObj = await this.execAndParse( linkedCurrentOoRowObj = await this.execAndParse(
this.dbDriver(childTn) this.dbDriver(childTn)
.select(childColumn.column_name) .select(
...new Set(
[childColumn, ...childTable.primaryKeys].map(
(col) => col.column_name,
),
),
)
.where(_wherePk(childTable.primaryKeys, rowId)), .where(_wherePk(childTable.primaryKeys, rowId)),
null, null,
{ raw: true, first: true }, { raw: true, first: true },
); );
const oldChildRowId = linkedCurrentOoRowObj const oldChildRowId = linkedCurrentOoRowObj
? Object.values(linkedCurrentOoRowObj)?.[0] ? linkedCurrentOoRowObj[childTable.primaryKeys[0]?.column_name]
: null; : null;
if (oldChildRowId) { if (oldChildRowId) {
@ -5979,14 +6075,20 @@ class BaseModelSqlv2 {
// 2. check current child is linked with another row cell // 2. check current child is linked with another row cell
linkedOoRowObj = await this.execAndParse( linkedOoRowObj = await this.execAndParse(
this.dbDriver(childTn) this.dbDriver(childTn)
.select(childColumn.column_name) .select(
...new Set(
[childColumn, ...childTable.primaryKeys].map(
(col) => `${childTable.table_name}.${col.column_name}`,
),
),
)
.where(_wherePk(childTable.primaryKeys, childId)), .where(_wherePk(childTable.primaryKeys, childId)),
null, null,
{ raw: true, first: true }, { raw: true, first: true },
); );
const oldRowId = linkedOoRowObj const oldRowId = linkedOoRowObj
? Object.values(linkedOoRowObj)?.[0] ? linkedOoRowObj[childTable.primaryKeys[0]?.column_name]
: null; : null;
if (oldRowId) { if (oldRowId) {
const [parentRelatedPkValue, childRelatedPkValue] = const [parentRelatedPkValue, childRelatedPkValue] =
@ -6060,6 +6162,7 @@ class BaseModelSqlv2 {
); );
await this.updateLastModified({ await this.updateLastModified({
baseModel: parentBaseModel,
model: parentTable, model: parentTable,
rowIds: [childId], rowIds: [childId],
cookie, cookie,
@ -6176,8 +6279,18 @@ class BaseModelSqlv2 {
await childTable.getColumns(this.context); await childTable.getColumns(this.context);
await parentTable.getColumns(this.context); await parentTable.getColumns(this.context);
const childTn = this.getTnPath(childTable); const parentBaseModel = await Model.getBaseModelSQL(this.context, {
const parentTn = this.getTnPath(parentTable); model: parentTable,
dbDriver: this.dbDriver,
});
const childBaseModel = await Model.getBaseModelSQL(this.context, {
dbDriver: this.dbDriver,
model: childTable,
});
const childTn = childBaseModel.getTnPath(childTable);
const parentTn = parentBaseModel.getTnPath(parentTable);
const relatedChildCol = getRelatedLinksColumn( const relatedChildCol = getRelatedLinksColumn(
column, column,
@ -6210,8 +6323,11 @@ class BaseModelSqlv2 {
const vChildCol = await colOptions.getMMChildColumn(this.context); const vChildCol = await colOptions.getMMChildColumn(this.context);
const vParentCol = await colOptions.getMMParentColumn(this.context); const vParentCol = await colOptions.getMMParentColumn(this.context);
const vTable = await colOptions.getMMModel(this.context); const vTable = await colOptions.getMMModel(this.context);
const assocBaseModel = await Model.getBaseModelSQL(this.context, {
const vTn = this.getTnPath(vTable); model: vTable,
dbDriver: this.dbDriver,
});
const vTn = assocBaseModel.getTnPath(vTable);
await this.execAndParse( await this.execAndParse(
this.dbDriver(vTn) this.dbDriver(vTn)
@ -6231,11 +6347,13 @@ class BaseModelSqlv2 {
); );
await this.updateLastModified({ await this.updateLastModified({
baseModel: parentBaseModel,
model: parentTable, model: parentTable,
rowIds: [childId], rowIds: [childId],
cookie, cookie,
}); });
await this.updateLastModified({ await this.updateLastModified({
baseModel: childBaseModel,
model: childTable, model: childTable,
rowIds: [rowId], rowIds: [rowId],
cookie, cookie,
@ -6264,6 +6382,7 @@ class BaseModelSqlv2 {
); );
await this.updateLastModified({ await this.updateLastModified({
baseModel: parentBaseModel,
model: parentTable, model: parentTable,
rowIds: [rowId], rowIds: [rowId],
cookie, cookie,
@ -6290,6 +6409,7 @@ class BaseModelSqlv2 {
); );
await this.updateLastModified({ await this.updateLastModified({
baseModel: parentBaseModel,
model: parentTable, model: parentTable,
rowIds: [childId], rowIds: [childId],
cookie, cookie,
@ -6312,6 +6432,7 @@ class BaseModelSqlv2 {
); );
await this.updateLastModified({ await this.updateLastModified({
baseModel: parentBaseModel,
model: parentTable, model: parentTable,
rowIds: [childId], rowIds: [childId],
cookie, cookie,
@ -6791,7 +6912,7 @@ class BaseModelSqlv2 {
colId: k, colId: k,
}) })
.then((col) => { .then((col) => {
return col.title; return col?.title;
}) })
.catch((e) => { .catch((e) => {
return Promise.resolve(e); return Promise.resolve(e);
@ -6806,7 +6927,7 @@ class BaseModelSqlv2 {
colId: col.id, colId: col.id,
}) })
.then((col) => { .then((col) => {
return col.title; return col?.title;
}) })
.catch((e) => { .catch((e) => {
return Promise.resolve(e); return Promise.resolve(e);
@ -7265,8 +7386,18 @@ class BaseModelSqlv2 {
await childTable.getColumns(this.context); await childTable.getColumns(this.context);
await parentTable.getColumns(this.context); await parentTable.getColumns(this.context);
const childTn = this.getTnPath(childTable); const childBaseModel = await Model.getBaseModelSQL(this.context, {
const parentTn = this.getTnPath(parentTable); model: childTable,
dbDriver: this.dbDriver,
});
const parentBaseModel = await Model.getBaseModelSQL(this.context, {
model: parentTable,
dbDriver: this.dbDriver,
});
const childTn = childBaseModel.getTnPath(childTable);
const parentTn = parentBaseModel.getTnPath(parentTable);
let relationType = colOptions.type; let relationType = colOptions.type;
let childIds = _childIds; let childIds = _childIds;
@ -7338,7 +7469,12 @@ class BaseModelSqlv2 {
const vParentCol = await colOptions.getMMParentColumn(this.context); const vParentCol = await colOptions.getMMParentColumn(this.context);
const vTable = await colOptions.getMMModel(this.context); const vTable = await colOptions.getMMModel(this.context);
const vTn = this.getTnPath(vTable); const assocBaseModel = await Model.getBaseModelSQL(this.context, {
model: vTable,
dbDriver: this.dbDriver,
});
const vTn = assocBaseModel.getTnPath(vTable);
let insertData: Record<string, any>[]; let insertData: Record<string, any>[];
@ -7640,8 +7776,18 @@ class BaseModelSqlv2 {
await childTable.getColumns(this.context); await childTable.getColumns(this.context);
await parentTable.getColumns(this.context); await parentTable.getColumns(this.context);
const childTn = this.getTnPath(childTable); const childBaseModel = await Model.getBaseModelSQL(this.context, {
const parentTn = this.getTnPath(parentTable); model: childTable,
dbDriver: this.dbDriver,
});
const parentBaseModel = await Model.getBaseModelSQL(this.context, {
model: parentTable,
dbDriver: this.dbDriver,
});
const childTn = childBaseModel.getTnPath(childTable);
const parentTn = parentBaseModel.getTnPath(parentTable);
const relatedChildCol = getRelatedLinksColumn( const relatedChildCol = getRelatedLinksColumn(
column, column,
@ -7678,6 +7824,11 @@ class BaseModelSqlv2 {
const vParentCol = await colOptions.getMMParentColumn(this.context); const vParentCol = await colOptions.getMMParentColumn(this.context);
const vTable = await colOptions.getMMModel(this.context); const vTable = await colOptions.getMMModel(this.context);
const assocBaseModel = await Model.getBaseModelSQL(this.context, {
model: vTable,
dbDriver: this.dbDriver,
});
// validate Ids // validate Ids
{ {
const childRowsQb = this.dbDriver(parentTn).select( const childRowsQb = this.dbDriver(parentTn).select(
@ -7732,7 +7883,7 @@ class BaseModelSqlv2 {
} }
} }
const vTn = this.getTnPath(vTable); const vTn = assocBaseModel.getTnPath(vTable);
const delQb = this.dbDriver(vTn) const delQb = this.dbDriver(vTn)
.where({ .where({
@ -8027,11 +8178,13 @@ class BaseModelSqlv2 {
cookie, cookie,
model = this.model, model = this.model,
knex = this.dbDriver, knex = this.dbDriver,
baseModel = this,
}: { }: {
rowIds: any | any[]; rowIds: any | any[];
cookie?: { user?: any }; cookie?: { user?: any };
model?: Model; model?: Model;
knex?: XKnex; knex?: XKnex;
baseModel?: BaseModelSqlv2;
}) { }) {
const columns = await model.getColumns(this.context); const columns = await model.getColumns(this.context);
@ -8055,7 +8208,7 @@ class BaseModelSqlv2 {
if (Object.keys(updateObject).length === 0) return; if (Object.keys(updateObject).length === 0) return;
const qb = knex(this.getTnPath(model.table_name)).update(updateObject); const qb = knex(baseModel.getTnPath(model.table_name)).update(updateObject);
for (const rowId of Array.isArray(rowIds) ? rowIds : [rowIds]) { for (const rowId of Array.isArray(rowIds) ? rowIds : [rowIds]) {
qb.orWhere(_wherePk(model.primaryKeys, rowId)); qb.orWhere(_wherePk(model.primaryKeys, rowId));

33
packages/nocodb/src/db/genRollupSelectv2.ts

@ -7,6 +7,7 @@ import type {
} from '~/models'; } from '~/models';
import type { XKnex } from '~/db/CustomKnex'; import type { XKnex } from '~/db/CustomKnex';
import type { Knex } from 'knex'; import type { Knex } from 'knex';
import { Model } from '~/models';
export default async function ({ export default async function ({
baseModelSqlv2, baseModelSqlv2,
@ -33,11 +34,20 @@ export default async function ({
const parentModel = await parentCol?.getModel(context); const parentModel = await parentCol?.getModel(context);
const refTableAlias = `__nc_rollup`; const refTableAlias = `__nc_rollup`;
const parentBaseModel = await Model.getBaseModelSQL(context, {
model: parentModel,
dbDriver: knex,
});
const childBaseModel = await Model.getBaseModelSQL(context, {
model: childModel,
dbDriver: knex,
});
switch (relationColumnOption.type) { switch (relationColumnOption.type) {
case RelationTypes.HAS_MANY: { case RelationTypes.HAS_MANY: {
const queryBuilder: any = knex( const queryBuilder: any = knex(
knex.raw(`?? as ??`, [ knex.raw(`?? as ??`, [
baseModelSqlv2.getTnPath(childModel?.table_name), childBaseModel.getTnPath(childModel),
refTableAlias, refTableAlias,
]), ]),
) )
@ -46,7 +56,7 @@ export default async function ({
) )
.where( .where(
knex.ref( knex.ref(
`${alias || baseModelSqlv2.getTnPath(parentModel.table_name)}.${ `${alias || parentBaseModel.getTnPath(parentModel.table_name)}.${
parentCol.column_name parentCol.column_name
}`, }`,
), ),
@ -62,7 +72,7 @@ export default async function ({
case RelationTypes.ONE_TO_ONE: { case RelationTypes.ONE_TO_ONE: {
const qb = knex( const qb = knex(
knex.raw(`?? as ??`, [ knex.raw(`?? as ??`, [
baseModelSqlv2.getTnPath(childModel?.table_name), childBaseModel.getTnPath(childModel?.table_name),
refTableAlias, refTableAlias,
]), ]),
) )
@ -71,7 +81,7 @@ export default async function ({
) )
.where( .where(
knex.ref( knex.ref(
`${alias || baseModelSqlv2.getTnPath(parentModel.table_name)}.${ `${alias || parentBaseModel.getTnPath(parentModel.table_name)}.${
parentCol.column_name parentCol.column_name
}`, }`,
), ),
@ -88,7 +98,10 @@ export default async function ({
const mmModel = await relationColumnOption.getMMModel(context); const mmModel = await relationColumnOption.getMMModel(context);
const mmChildCol = await relationColumnOption.getMMChildColumn(context); const mmChildCol = await relationColumnOption.getMMChildColumn(context);
const mmParentCol = await relationColumnOption.getMMParentColumn(context); const mmParentCol = await relationColumnOption.getMMParentColumn(context);
const assocBaseModel = await Model.getBaseModelSQL(context, {
id: mmModel.id,
dbDriver: knex,
});
if (!mmModel) { if (!mmModel) {
return this.dbDriver.raw(`?`, [ return this.dbDriver.raw(`?`, [
NcDataErrorCodes.NC_ERR_MM_MODEL_NOT_FOUND, NcDataErrorCodes.NC_ERR_MM_MODEL_NOT_FOUND,
@ -97,7 +110,7 @@ export default async function ({
const qb = knex( const qb = knex(
knex.raw(`?? as ??`, [ knex.raw(`?? as ??`, [
baseModelSqlv2.getTnPath(parentModel?.table_name), parentBaseModel.getTnPath(parentModel?.table_name),
refTableAlias, refTableAlias,
]), ]),
) )
@ -105,9 +118,9 @@ export default async function ({
knex.ref(`${refTableAlias}.${rollupColumn.column_name}`), knex.ref(`${refTableAlias}.${rollupColumn.column_name}`),
) )
.innerJoin( .innerJoin(
baseModelSqlv2.getTnPath(mmModel.table_name), assocBaseModel.getTnPath(mmModel.table_name),
knex.ref( knex.ref(
`${baseModelSqlv2.getTnPath(mmModel.table_name)}.${ `${assocBaseModel.getTnPath(mmModel.table_name)}.${
mmParentCol.column_name mmParentCol.column_name
}`, }`,
), ),
@ -116,13 +129,13 @@ export default async function ({
) )
.where( .where(
knex.ref( knex.ref(
`${baseModelSqlv2.getTnPath(mmModel.table_name)}.${ `${assocBaseModel.getTnPath(mmModel.table_name)}.${
mmChildCol.column_name mmChildCol.column_name
}`, }`,
), ),
'=', '=',
knex.ref( knex.ref(
`${alias || baseModelSqlv2.getTnPath(childModel.table_name)}.${ `${alias || childBaseModel.getTnPath(childModel.table_name)}.${
childCol.column_name childCol.column_name
}`, }`,
), ),

54
packages/nocodb/src/db/generateLookupSelectQuery.ts

@ -1,4 +1,4 @@
import { isVirtualCol, RelationTypes, UITypes } from 'nocodb-sdk'; import { RelationTypes, UITypes } from 'nocodb-sdk';
import type LookupColumn from '../models/LookupColumn'; import type LookupColumn from '../models/LookupColumn';
import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2'; import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2';
import type { import type {
@ -92,9 +92,14 @@ export default async function generateLookupSelectQuery({
const parentModel = await parentColumn.getModel(context); const parentModel = await parentColumn.getModel(context);
await parentModel.getColumns(context); await parentModel.getColumns(context);
const parentBaseModel = await Model.getBaseModelSQL(context, {
model: parentModel,
dbDriver: knex,
});
selectQb = knex( selectQb = knex(
knex.raw(`?? as ??`, [ knex.raw(`?? as ??`, [
baseModelSqlv2.getTnPath(parentModel.table_name), parentBaseModel.getTnPath(parentModel.table_name),
alias, alias,
]), ]),
).where( ).where(
@ -113,10 +118,14 @@ export default async function generateLookupSelectQuery({
await childModel.getColumns(context); await childModel.getColumns(context);
const parentModel = await parentColumn.getModel(context); const parentModel = await parentColumn.getModel(context);
await parentModel.getColumns(context); await parentModel.getColumns(context);
const parentBaseModel = await Model.getBaseModelSQL(context, {
model: parentModel,
dbDriver: knex,
});
selectQb = knex( selectQb = knex(
knex.raw(`?? as ??`, [ knex.raw(`?? as ??`, [
baseModelSqlv2.getTnPath(childModel.table_name), parentBaseModel.getTnPath(childModel.table_name),
alias, alias,
]), ]),
).where( ).where(
@ -136,9 +145,14 @@ export default async function generateLookupSelectQuery({
const parentModel = await parentColumn.getModel(context); const parentModel = await parentColumn.getModel(context);
await parentModel.getColumns(context); await parentModel.getColumns(context);
const parentBaseModel = await Model.getBaseModelSQL(context, {
model: parentModel,
dbDriver: knex,
});
selectQb = knex( selectQb = knex(
knex.raw(`?? as ??`, [ knex.raw(`?? as ??`, [
baseModelSqlv2.getTnPath(parentModel.table_name), parentBaseModel.getTnPath(parentModel.table_name),
alias, alias,
]), ]),
); );
@ -149,9 +163,14 @@ export default async function generateLookupSelectQuery({
const mmChildCol = await relation.getMMChildColumn(context); const mmChildCol = await relation.getMMChildColumn(context);
const mmParentCol = await relation.getMMParentColumn(context); const mmParentCol = await relation.getMMParentColumn(context);
const associatedBaseModel = await Model.getBaseModelSQL(context, {
model: mmModel,
dbDriver: knex,
});
selectQb selectQb
.innerJoin( .innerJoin(
baseModelSqlv2.getTnPath(mmModel.table_name, mmTableAlias), associatedBaseModel.getTnPath(mmModel.table_name, mmTableAlias),
knex.ref(`${mmTableAlias}.${mmParentCol.column_name}`), knex.ref(`${mmTableAlias}.${mmParentCol.column_name}`),
'=', '=',
knex.ref(`${alias}.${parentColumn.column_name}`), knex.ref(`${alias}.${parentColumn.column_name}`),
@ -217,10 +236,14 @@ export default async function generateLookupSelectQuery({
await childModel.getColumns(context); await childModel.getColumns(context);
const parentModel = await parentColumn.getModel(context); const parentModel = await parentColumn.getModel(context);
await parentModel.getColumns(context); await parentModel.getColumns(context);
const parentBaseModel = await Model.getBaseModelSQL(context, {
model: parentModel,
dbDriver: knex,
});
selectQb.join( selectQb.join(
knex.raw(`?? as ??`, [ knex.raw(`?? as ??`, [
baseModelSqlv2.getTnPath(parentModel.table_name), parentBaseModel.getTnPath(parentModel.table_name),
nestedAlias, nestedAlias,
]), ]),
`${nestedAlias}.${parentColumn.column_name}`, `${nestedAlias}.${parentColumn.column_name}`,
@ -234,10 +257,14 @@ export default async function generateLookupSelectQuery({
await childModel.getColumns(context); await childModel.getColumns(context);
const parentModel = await parentColumn.getModel(context); const parentModel = await parentColumn.getModel(context);
await parentModel.getColumns(context); await parentModel.getColumns(context);
const childBaseModel = await Model.getBaseModelSQL(context, {
model: childModel,
dbDriver: knex,
});
selectQb.join( selectQb.join(
knex.raw(`?? as ??`, [ knex.raw(`?? as ??`, [
baseModelSqlv2.getTnPath(childModel.table_name), childBaseModel.getTnPath(childModel.table_name),
nestedAlias, nestedAlias,
]), ]),
`${nestedAlias}.${childColumn.column_name}`, `${nestedAlias}.${childColumn.column_name}`,
@ -251,6 +278,10 @@ export default async function generateLookupSelectQuery({
await childModel.getColumns(context); await childModel.getColumns(context);
const parentModel = await parentColumn.getModel(context); const parentModel = await parentColumn.getModel(context);
await parentModel.getColumns(context); await parentModel.getColumns(context);
const parentBaseModel = await Model.getBaseModelSQL(context, {
model: parentModel,
dbDriver: knex,
});
const mmTableAlias = getAlias(); const mmTableAlias = getAlias();
@ -258,16 +289,21 @@ export default async function generateLookupSelectQuery({
const mmChildCol = await relation.getMMChildColumn(context); const mmChildCol = await relation.getMMChildColumn(context);
const mmParentCol = await relation.getMMParentColumn(context); const mmParentCol = await relation.getMMParentColumn(context);
const associatedBaseModel = await Model.getBaseModelSQL(context, {
model: mmModel,
dbDriver: knex,
});
selectQb selectQb
.innerJoin( .innerJoin(
baseModelSqlv2.getTnPath(mmModel.table_name, mmTableAlias), associatedBaseModel.getTnPath(mmModel.table_name, mmTableAlias),
knex.ref(`${mmTableAlias}.${mmChildCol.column_name}`), knex.ref(`${mmTableAlias}.${mmChildCol.column_name}`),
'=', '=',
knex.ref(`${prevAlias}.${childColumn.column_name}`), knex.ref(`${prevAlias}.${childColumn.column_name}`),
) )
.innerJoin( .innerJoin(
knex.raw('?? as ??', [ knex.raw('?? as ??', [
baseModelSqlv2.getTnPath(parentModel.table_name), parentBaseModel.getTnPath(parentModel.table_name),
nestedAlias, nestedAlias,
]), ]),
knex.ref(`${mmTableAlias}.${mmParentCol.column_name}`), knex.ref(`${mmTableAlias}.${mmParentCol.column_name}`),

16
packages/nocodb/src/helpers/catchError.ts

@ -558,6 +558,14 @@ const errorHelpers: {
message: 'Invalid JSON in request body', message: 'Invalid JSON in request body',
code: 400, code: 400,
}, },
[NcErrorType.COLUMN_ASSOCIATED_WITH_LINK]: {
message: 'Column is associated with a link, please remove the link first',
code: 400,
},
[NcErrorType.TABLE_ASSOCIATED_WITH_LINK]: {
message: 'Table is associated with a link, please remove the link first',
code: 400,
},
}; };
function generateError( function generateError(
@ -631,6 +639,14 @@ export class NcError {
}); });
} }
static columnAssociatedWithLink(_id: string, args: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.COLUMN_ASSOCIATED_WITH_LINK, args);
}
static tableAssociatedWithLink(_id: string, args: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.TABLE_ASSOCIATED_WITH_LINK, args);
}
static baseNotFound(id: string, args?: NcErrorArgs) { static baseNotFound(id: string, args?: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.BASE_NOT_FOUND, { throw new NcBaseErrorv2(NcErrorType.BASE_NOT_FOUND, {
params: id, params: id,

19
packages/nocodb/src/helpers/columnHelpers.ts

@ -43,6 +43,8 @@ export async function createHmAndBtColumn(
columnMeta = null, columnMeta = null,
isLinks = false, isLinks = false,
colExtra?: any, colExtra?: any,
parentColumn?: Column,
isCustom = false,
) { ) {
// save bt column // save bt column
{ {
@ -60,7 +62,7 @@ export async function createHmAndBtColumn(
// db_type: // db_type:
fk_child_column_id: childColumn.id, fk_child_column_id: childColumn.id,
fk_parent_column_id: parent.primaryKey.id, fk_parent_column_id: parentColumn?.id || parent.primaryKey.id,
fk_related_model_id: parent.id, fk_related_model_id: parent.id,
virtual, virtual,
// if self referencing treat it as system field to hide from ui // if self referencing treat it as system field to hide from ui
@ -68,6 +70,10 @@ export async function createHmAndBtColumn(
fk_col_name: fkColName, fk_col_name: fkColName,
fk_index_name: fkColName, fk_index_name: fkColName,
...(type === 'bt' ? colExtra : {}), ...(type === 'bt' ? colExtra : {}),
meta: {
...(colExtra?.meta || {}),
custom: isCustom,
},
}); });
} }
// save hm column // save hm column
@ -80,6 +86,7 @@ export async function createHmAndBtColumn(
...(columnMeta || {}), ...(columnMeta || {}),
plural: columnMeta?.plural || pluralize(child.title), plural: columnMeta?.plural || pluralize(child.title),
singular: columnMeta?.singular || singularize(child.title), singular: columnMeta?.singular || singularize(child.title),
custom: isCustom,
}; };
await Column.insert(context, { await Column.insert(context, {
@ -89,7 +96,7 @@ export async function createHmAndBtColumn(
type: 'hm', type: 'hm',
fk_target_view_id: childView?.id, fk_target_view_id: childView?.id,
fk_child_column_id: childColumn.id, fk_child_column_id: childColumn.id,
fk_parent_column_id: parent.primaryKey.id, fk_parent_column_id: parentColumn?.id || parent.primaryKey.id,
fk_related_model_id: child.id, fk_related_model_id: child.id,
virtual, virtual,
system: isSystemCol, system: isSystemCol,
@ -128,6 +135,8 @@ export async function createOOColumn(
isSystemCol = false, isSystemCol = false,
columnMeta = null, columnMeta = null,
colExtra?: any, colExtra?: any,
parentColumn?: Column,
isCustom = false,
) { ) {
// save bt column // save bt column
{ {
@ -144,7 +153,7 @@ export async function createOOColumn(
// Child View ID is given for relation from parent to child. not for child to parent // Child View ID is given for relation from parent to child. not for child to parent
fk_target_view_id: null, fk_target_view_id: null,
fk_child_column_id: childColumn.id, fk_child_column_id: childColumn.id,
fk_parent_column_id: parent.primaryKey.id, fk_parent_column_id: parentColumn?.id || parent.primaryKey.id,
fk_related_model_id: parent.id, fk_related_model_id: parent.id,
virtual, virtual,
// if self referencing treat it as system field to hide from ui // if self referencing treat it as system field to hide from ui
@ -157,6 +166,7 @@ export async function createOOColumn(
// one-to-one relation is combination of both hm and bt to identify table which have // one-to-one relation is combination of both hm and bt to identify table which have
// foreign key column(similar to bt) we are adding a boolean flag `bt` under meta // foreign key column(similar to bt) we are adding a boolean flag `bt` under meta
bt: true, bt: true,
custom: isCustom,
}, },
}); });
} }
@ -176,6 +186,7 @@ export async function createOOColumn(
...(columnMeta || {}), ...(columnMeta || {}),
plural: columnMeta?.plural || pluralize(child.title), plural: columnMeta?.plural || pluralize(child.title),
singular: columnMeta?.singular || singularize(child.title), singular: columnMeta?.singular || singularize(child.title),
custom: isCustom,
}; };
await Column.insert(context, { await Column.insert(context, {
@ -185,7 +196,7 @@ export async function createOOColumn(
type: 'oo', type: 'oo',
fk_target_view_id: childView?.id, fk_target_view_id: childView?.id,
fk_child_column_id: childColumn.id, fk_child_column_id: childColumn.id,
fk_parent_column_id: parent.primaryKey.id, fk_parent_column_id: parentColumn?.id || parent.primaryKey.id,
fk_related_model_id: child.id, fk_related_model_id: child.id,
virtual, virtual,
system: isSystemCol, system: isSystemCol,

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

@ -15,6 +15,7 @@ import FetchAT from './helpers/fetchAT';
import { importData } from './helpers/readAndProcessData'; import { importData } from './helpers/readAndProcessData';
import EntityMap from './helpers/EntityMap'; import EntityMap from './helpers/EntityMap';
import type { UserType } from 'nocodb-sdk'; import type { UserType } from 'nocodb-sdk';
import type { AtImportJobData } from '~/interface/Jobs';
import { type Base, Model, Source } from '~/models'; import { type Base, Model, Source } from '~/models';
import { sanitizeColumnName } from '~/helpers'; import { sanitizeColumnName } from '~/helpers';
import { AttachmentsService } from '~/services/attachments.service'; import { AttachmentsService } from '~/services/attachments.service';
@ -31,7 +32,7 @@ import { TablesService } from '~/services/tables.service';
import { ViewColumnsService } from '~/services/view-columns.service'; import { ViewColumnsService } from '~/services/view-columns.service';
import { ViewsService } from '~/services/views.service'; import { ViewsService } from '~/services/views.service';
import { FormsService } from '~/services/forms.service'; import { FormsService } from '~/services/forms.service';
import { AtImportJobData, JOBS_QUEUE, JobTypes } from '~/interface/Jobs'; import { JOBS_QUEUE, JobTypes } from '~/interface/Jobs';
import { GridColumnsService } from '~/services/grid-columns.service'; import { GridColumnsService } from '~/services/grid-columns.service';
import { TelemetryService } from '~/services/telemetry.service'; import { TelemetryService } from '~/services/telemetry.service';
import NcConnectionMgrv2 from '~/utils/common/NcConnectionMgrv2'; import NcConnectionMgrv2 from '~/utils/common/NcConnectionMgrv2';

3
packages/nocodb/src/redis/pubsub-redis.ts

@ -74,7 +74,8 @@ export class PubSubRedis {
PubSubRedis.redisSubscriber.on('message', onMessage); PubSubRedis.redisSubscriber.on('message', onMessage);
return async (keepRedisChannel = false) => { return async (keepRedisChannel = false) => {
// keepRedisChannel is used to keep the channel open for other subscribers // keepRedisChannel is used to keep the channel open for other subscribers
if (!keepRedisChannel) await PubSubRedis.redisSubscriber.unsubscribe(channel); if (!keepRedisChannel)
await PubSubRedis.redisSubscriber.unsubscribe(channel);
PubSubRedis.redisSubscriber.off('message', onMessage); PubSubRedis.redisSubscriber.off('message', onMessage);
}; };
} }

86
packages/nocodb/src/services/columns.service.ts

@ -72,7 +72,7 @@ export enum Altered {
UPDATE_COLUMN = 8, UPDATE_COLUMN = 8,
} }
interface ReusableParams { export interface ReusableParams {
table?: Model; table?: Model;
source?: Source; source?: Source;
base?: Base; base?: Base;
@ -2099,6 +2099,59 @@ export class ColumnsService {
ProjectMgrv2.getSqlMgr(context, { id: source.base_id }, ncMeta), ProjectMgrv2.getSqlMgr(context, { id: source.base_id }, ncMeta),
); );
// check column association with any custom links or LTAR
if (!isVirtualCol(column)) {
const columns = await table.getColumns(context, ncMeta);
let link = columns.find((c) => {
return (
isLinksOrLTAR(c.uidt) &&
((c.colOptions as LinkToAnotherRecordColumn)?.fk_child_column_id ===
param.columnId ||
(c.colOptions as LinkToAnotherRecordColumn)?.fk_parent_column_id ===
param.columnId ||
(c.colOptions as LinkToAnotherRecordColumn)
?.fk_mm_child_column_id === param.columnId ||
(c.colOptions as LinkToAnotherRecordColumn)
?.fk_mm_parent_column_id === param.columnId)
);
})?.colOptions as LinkToAnotherRecordColumn;
if (!link) {
link = await ncMeta.metaGet2(
table.fk_workspace_id,
table.base_id,
MetaTable.COL_RELATIONS,
{},
null,
{
_or: [
{ fk_child_column_id: { eq: param.columnId } },
{ fk_parent_column_id: { eq: param.columnId } },
{ fk_mm_child_column_id: { eq: param.columnId } },
{ fk_mm_parent_column_id: { eq: param.columnId } },
],
},
);
}
// if relation found then throw error
if (link) {
const linkCol = await Column.get(
context,
{ colId: link.fk_column_id },
ncMeta,
);
const table = await linkCol.getModel(context, ncMeta);
NcError.columnAssociatedWithLink(column.id, {
customMessage: `Column is associated with Link column '${
linkCol.title || linkCol.column_name
}' (${
table.title || table.table_name
}). Please delete the link column first.`,
});
}
}
/** /**
* @Note: When using 'falls through to default' cases in a switch statement, * @Note: When using 'falls through to default' cases in a switch statement,
* it is crucial to place them after cases with break statements. * it is crucial to place them after cases with break statements.
@ -2178,6 +2231,7 @@ export class ColumnsService {
ncMeta, ncMeta,
); );
const parentTable = await parentColumn.getModel(context, ncMeta); const parentTable = await parentColumn.getModel(context, ncMeta);
const custom = column.meta?.custom;
switch (relationColOpt.type) { switch (relationColOpt.type) {
case 'bt': case 'bt':
@ -2192,6 +2246,7 @@ export class ColumnsService {
parentTable, parentTable,
sqlMgr, sqlMgr,
ncMeta, ncMeta,
custom,
}); });
} }
break; break;
@ -2206,6 +2261,7 @@ export class ColumnsService {
parentTable, parentTable,
sqlMgr, sqlMgr,
ncMeta, ncMeta,
custom,
}); });
} }
break; break;
@ -2224,6 +2280,7 @@ export class ColumnsService {
ncMeta, ncMeta,
); );
if (!custom) {
await this.deleteHmOrBtRelation( await this.deleteHmOrBtRelation(
context, context,
{ {
@ -2255,6 +2312,7 @@ export class ColumnsService {
}, },
true, true,
); );
}
const columnsInRelatedTable: Column[] = await relationColOpt const columnsInRelatedTable: Column[] = await relationColOpt
.getRelatedTable(context, ncMeta) .getRelatedTable(context, ncMeta)
.then((m) => m.getColumns(context, ncMeta)); .then((m) => m.getColumns(context, ncMeta));
@ -2287,6 +2345,7 @@ export class ColumnsService {
ncMeta, ncMeta,
); );
if (!custom) {
if (mmTable) { if (mmTable) {
// delete bt columns in m2m table // delete bt columns in m2m table
await mmTable.getColumns(context, ncMeta); await mmTable.getColumns(context, ncMeta);
@ -2313,7 +2372,8 @@ export class ColumnsService {
ncMeta, ncMeta,
); );
if ( if (
colOpt.fk_related_model_id === relationColOpt.fk_mm_model_id colOpt.fk_related_model_id ===
relationColOpt.fk_mm_model_id
) { ) {
await Column.delete(context, c.id, ncMeta); await Column.delete(context, c.id, ncMeta);
} }
@ -2329,7 +2389,8 @@ export class ColumnsService {
ncMeta, ncMeta,
); );
if ( if (
colOpt.fk_related_model_id === relationColOpt.fk_mm_model_id colOpt.fk_related_model_id ===
relationColOpt.fk_mm_model_id
) { ) {
await Column.delete(context, c.id, ncMeta); await Column.delete(context, c.id, ncMeta);
} }
@ -2348,6 +2409,7 @@ export class ColumnsService {
} }
} }
} }
}
break; break;
} }
} }
@ -2450,6 +2512,7 @@ export class ColumnsService {
sqlMgr, sqlMgr,
ncMeta = Noco.ncMeta, ncMeta = Noco.ncMeta,
virtual, virtual,
custom = false,
}: { }: {
relationColOpt: LinkToAnotherRecordColumn; relationColOpt: LinkToAnotherRecordColumn;
source: Source; source: Source;
@ -2460,10 +2523,11 @@ export class ColumnsService {
sqlMgr: SqlMgrv2; sqlMgr: SqlMgrv2;
ncMeta?: MetaService; ncMeta?: MetaService;
virtual?: boolean; virtual?: boolean;
custom?: boolean;
}, },
ignoreFkDelete = false, ignoreFkDelete = false,
) => { ) => {
if (childTable) { if (childTable && !custom) {
let foreignKeyName; let foreignKeyName;
// if relationColOpt is not provided, extract it from child table // if relationColOpt is not provided, extract it from child table
@ -2514,7 +2578,7 @@ export class ColumnsService {
.then((m) => m.getColumns(context, ncMeta)); .then((m) => m.getColumns(context, ncMeta));
const relType = relationColOpt.type === 'bt' ? 'hm' : 'bt'; const relType = relationColOpt.type === 'bt' ? 'hm' : 'bt';
for (const c of columnsInRelatedTable) { for (const c of columnsInRelatedTable) {
if (c.uidt !== UITypes.LinkToAnotherRecord) continue; if (!isLinksOrLTAR(c.uidt)) continue;
const colOpt = await c.getColOptions<LinkToAnotherRecordColumn>( const colOpt = await c.getColOptions<LinkToAnotherRecordColumn>(
context, context,
ncMeta, ncMeta,
@ -2532,6 +2596,7 @@ export class ColumnsService {
// delete virtual columns // delete virtual columns
await Column.delete(context, relationColOpt.fk_column_id, ncMeta); await Column.delete(context, relationColOpt.fk_column_id, ncMeta);
if (custom) return;
if (!ignoreFkDelete) { if (!ignoreFkDelete) {
const cTable = await Model.getWithInfo( const cTable = await Model.getWithInfo(
context, context,
@ -2603,6 +2668,7 @@ export class ColumnsService {
sqlMgr, sqlMgr,
ncMeta = Noco.ncMeta, ncMeta = Noco.ncMeta,
virtual, virtual,
custom = false,
}: { }: {
relationColOpt: LinkToAnotherRecordColumn; relationColOpt: LinkToAnotherRecordColumn;
source: Source; source: Source;
@ -2613,10 +2679,12 @@ export class ColumnsService {
sqlMgr: SqlMgrv2; sqlMgr: SqlMgrv2;
ncMeta?: MetaService; ncMeta?: MetaService;
virtual?: boolean; virtual?: boolean;
custom?: boolean;
}, },
ignoreFkDelete = false, ignoreFkDelete = false,
) => { ) => {
if (childTable) { if (childTable) {
if (!custom) {
let foreignKeyName; let foreignKeyName;
// if relationColOpt is not provided, extract it from child table // if relationColOpt is not provided, extract it from child table
@ -2624,7 +2692,9 @@ export class ColumnsService {
if (!relationColOpt) { if (!relationColOpt) {
foreignKeyName = ( foreignKeyName = (
( (
await childTable.getColumns(context, ncMeta).then(async (cols) => { await childTable
.getColumns(context, ncMeta)
.then(async (cols) => {
for (const col of cols) { for (const col of cols) {
if (col.uidt === UITypes.LinkToAnotherRecord) { if (col.uidt === UITypes.LinkToAnotherRecord) {
const colOptions = const colOptions =
@ -2660,6 +2730,7 @@ export class ColumnsService {
} }
} }
} }
}
if (!relationColOpt) return; if (!relationColOpt) return;
const columnsInRelatedTable: Column[] = await relationColOpt const columnsInRelatedTable: Column[] = await relationColOpt
@ -2685,6 +2756,8 @@ export class ColumnsService {
// delete virtual columns // delete virtual columns
await Column.delete(context, relationColOpt.fk_column_id, ncMeta); await Column.delete(context, relationColOpt.fk_column_id, ncMeta);
if (custom) return;
if (!ignoreFkDelete) { if (!ignoreFkDelete) {
const cTable = await Model.getWithInfo( const cTable = await Model.getWithInfo(
context, context,
@ -2739,7 +2812,6 @@ export class ColumnsService {
}; };
await sqlMgr.sqlOpPlus(source, 'tableUpdate', tableUpdateBody); await sqlMgr.sqlOpPlus(source, 'tableUpdate', tableUpdateBody);
// delete foreign key column // delete foreign key column
await Column.delete(context, childColumn.id, ncMeta); await Column.delete(context, childColumn.id, ncMeta);
} }

23
packages/nocodb/src/services/tables.service.ts

@ -37,6 +37,7 @@ import {
getUniqueColumnAliasName, getUniqueColumnAliasName,
getUniqueColumnName, getUniqueColumnName,
} from '~/helpers/getUniqueName'; } from '~/helpers/getUniqueName';
import { MetaTable } from '~/utils/globals';
@Injectable() @Injectable()
export class TablesService { export class TablesService {
@ -224,6 +225,28 @@ export class TablesService {
NcError.badRequest( NcError.badRequest(
`This is a many to many table for ${tables[0]?.title} (${relColumns[0]?.title}) & ${tables[1]?.title} (${relColumns[1]?.title}). You can disable "Show M2M tables" in base settings to avoid seeing this.`, `This is a many to many table for ${tables[0]?.title} (${relColumns[0]?.title}) & ${tables[1]?.title} (${relColumns[1]?.title}). You can disable "Show M2M tables" in base settings to avoid seeing this.`,
); );
} else {
// if table is using in custom relation as junction table then delete all the relation
const relations = await Noco.ncMeta.metaList2(
table.fk_workspace_id,
table.base_id,
MetaTable.COL_RELATIONS,
{
condition: {
fk_mm_model_id: table.id,
},
},
);
if (relations.length) {
const relCol = await Column.get(context, {
colId: relations[0].fk_column_id,
});
const relTable = await Model.get(context, relCol.fk_model_id);
NcError.tableAssociatedWithLink(table.id, {
customMessage: `This is a many to many table for '${relTable?.title}' (${relTable?.title}), please delete the column before deleting the table.`,
});
}
} }
const base = await Base.getWithInfo(context, table.base_id); const base = await Base.getWithInfo(context, table.base_id);

3
packages/nocodb/tests/unit/factory/table.ts

@ -14,7 +14,8 @@ const createTable = async (context, base, args = {}) => {
const response = await request(context.app) const response = await request(context.app)
.post(`/api/v1/db/meta/projects/${base.id}/tables`) .post(`/api/v1/db/meta/projects/${base.id}/tables`)
.set('xc-auth', context.token) .set('xc-auth', context.token)
.send({ ...defaultValue, ...args }); .send({ ...defaultValue, ...args })
.expect(200);
const table: Model = await Model.get( const table: Model = await Model.get(
{ {

3
packages/nocodb/tests/unit/rest/index.test.ts

@ -19,11 +19,13 @@ let workspaceTest = () => {};
let ssoTest = () => {}; let ssoTest = () => {};
let cloudOrgTest = () => {}; let cloudOrgTest = () => {};
let bulkAggregationTest = () => {}; let bulkAggregationTest = () => {};
let columnTest = () => {};
if (process.env.EE === 'true') { if (process.env.EE === 'true') {
workspaceTest = require('./tests/ee/workspace.test').default; workspaceTest = require('./tests/ee/workspace.test').default;
ssoTest = require('./tests/ee/sso.test').default; ssoTest = require('./tests/ee/sso.test').default;
cloudOrgTest = require('./tests/ee/cloud-org.test').default; cloudOrgTest = require('./tests/ee/cloud-org.test').default;
bulkAggregationTest = require('./tests/ee/bulkAggregation.test').default; bulkAggregationTest = require('./tests/ee/bulkAggregation.test').default;
columnTest = require('./tests/ee/column.test').default;
} }
// import layoutTests from './tests/layout.test'; // import layoutTests from './tests/layout.test';
// import widgetTest from './tests/widget.test'; // import widgetTest from './tests/widget.test';
@ -48,6 +50,7 @@ function restTests() {
readOnlyTest(); readOnlyTest();
aggregationTest(); aggregationTest();
bulkAggregationTest(); bulkAggregationTest();
columnTest();
// Enable for dashboard feature // Enable for dashboard feature
// widgetTest(); // widgetTest();

2
packages/nocodb/tsconfig.json

@ -41,5 +41,5 @@
] ]
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["src/ee", "src/ee-on-prem"] "exclude": ["src/ee", "src/ee-on-prem", "src/ee-cloud"]
} }

4
tests/playwright/pages/Dashboard/Details/FieldsPage.ts

@ -208,6 +208,10 @@ export class FieldsPage extends BasePage {
break; break;
case 'Links': case 'Links':
await this.addOrEditColumn.locator('.nc-ltar-relation-type').getByTestId(relationType).click(); await this.addOrEditColumn.locator('.nc-ltar-relation-type').getByTestId(relationType).click();
// await this.addOrEditColumn
// .locator('.nc-ltar-relation-type >> .ant-radio')
// .nth(relationType === 'Has Many' ? 1 : 0)
// .click();
await this.addOrEditColumn.locator('.ant-select-single').nth(1).click(); await this.addOrEditColumn.locator('.ant-select-single').nth(1).click();
await this.rootPage.locator(`.nc-ltar-child-table >> input[type="search"]`).first().fill(childTable); await this.rootPage.locator(`.nc-ltar-child-table >> input[type="search"]`).first().fill(childTable);
await this.rootPage await this.rootPage

33
tests/playwright/pages/Dashboard/Grid/Column/index.ts

@ -73,6 +73,8 @@ export class ColumnPageObject extends BasePage {
isDisplayValue = false, isDisplayValue = false,
ltarFilters, ltarFilters,
ltarView, ltarView,
custom = false,
refColumn,
}: { }: {
title: string; title: string;
type?: string; type?: string;
@ -92,6 +94,8 @@ export class ColumnPageObject extends BasePage {
isDisplayValue?: boolean; isDisplayValue?: boolean;
ltarFilters?: any[]; ltarFilters?: any[];
ltarView?: string; ltarView?: string;
custom?: boolean;
refColumn?: string;
}) { }) {
if (insertBeforeColumnTitle) { if (insertBeforeColumnTitle) {
await this.grid.get().locator(`th[data-title="${insertBeforeColumnTitle}"]`).scrollIntoViewIfNeeded(); await this.grid.get().locator(`th[data-title="${insertBeforeColumnTitle}"]`).scrollIntoViewIfNeeded();
@ -197,6 +201,10 @@ export class ColumnPageObject extends BasePage {
await this.rootPage.waitForTimeout(2000); await this.rootPage.waitForTimeout(2000);
await this.get().locator('.nc-ltar-relation-type').getByTestId(relationType).click(); await this.get().locator('.nc-ltar-relation-type').getByTestId(relationType).click();
// await this.get()
// .locator('.nc-ltar-relation-type >> .ant-radio')
// .nth(relationType === 'Has Many' ? 1 : 0)
// .click();
await this.get().locator('.ant-select-single').nth(1).click(); await this.get().locator('.ant-select-single').nth(1).click();
await this.rootPage.locator(`.nc-ltar-child-table >> input[type="search"]`).fill(childTable); await this.rootPage.locator(`.nc-ltar-child-table >> input[type="search"]`).fill(childTable);
await this.rootPage await this.rootPage
@ -214,6 +222,31 @@ export class ColumnPageObject extends BasePage {
await this.ltarOption.addFilters(ltarFilters); await this.ltarOption.addFilters(ltarFilters);
} }
if (custom) {
// enable advance options
await this.get().locator('.nc-ltar-relation-type >> .ant-radio').nth(1).dblclick();
await this.get().locator('.nc-ltar-relation-type >> .ant-radio').nth(2).dblclick();
await this.get().locator(':has(:has-text("Advanced Link")) > button.ant-switch').click();
// data-testid="custom-link-source-base-id"
// data-testid="custom-link-source-table-id"
// data-testid="custom-link-source-column-id"
// data-testid="custom-link-junction-base-id"
// data-testid="custom-link-junction-table-id"
// data-testid="custom-link-junction-source-column-id"
// data-testid="custom-link-junction-target-column-id"
// data-testid="custom-link-target-base-id"
// data-testid="custom-link-target-table-id"
// data-testid="custom-link-target-column-id"
// select target table and column
// await this.get().get('').nth(2).click();
// select referenced base, column and column
}
break; break;
case 'User': case 'User':
break; break;

349
tests/playwright/tests/db/columns/columnLinkToAnotherRecordAdvanceOptions.spec.ts

@ -0,0 +1,349 @@
import { test } from '@playwright/test';
import { DashboardPage } from '../../../pages/Dashboard';
import setup, { unsetup } from '../../../setup';
import { enableQuickRun } from '../../../setup/db';
import { UITypes } from 'nocodb-sdk';
test.describe('LTAR create & update', () => {
// force disabled temporarily
// to be re-visited after advance options menu is finalised
test.skip();
if (enableQuickRun()) test.skip();
let dashboard: DashboardPage;
let context: any;
// todo: Break the test into smaller tests
test.setTimeout(150000);
test.beforeEach(async ({ page }) => {
context = await setup({ page, isEmptyProject: true });
dashboard = new DashboardPage(page, context.base);
});
test.afterEach(async () => {
await unsetup(context);
});
test('LTAR', async () => {
await dashboard.treeView.createTable({ title: 'Sheet1', baseTitle: context.base.title });
// subsequent table creation fails; hence delay
await dashboard.rootPage.waitForTimeout(1000);
await dashboard.treeView.createTable({ title: 'Sheet2', baseTitle: context.base.title });
await dashboard.grid.column.create({
title: 'Sheet1Id',
type: UITypes.Number,
});
await dashboard.treeView.openTable({ title: 'Sheet1' });
await dashboard.grid.addNewRow({ index: 0, value: '1a' });
await dashboard.grid.addNewRow({ index: 1, value: '1b' });
await dashboard.grid.addNewRow({ index: 2, value: '1c' });
await dashboard.treeView.openTable({ title: 'Sheet2' });
await dashboard.grid.addNewRow({ index: 0, value: '2a' });
await dashboard.grid.addNewRow({ index: 1, value: '2b' });
await dashboard.grid.addNewRow({ index: 2, value: '2c' });
await dashboard.grid.editRow({ index: 0, columnHeader: 'Sheet1Id', value: '1' });
await dashboard.grid.editRow({ index: 1, columnHeader: 'Sheet1Id', value: '1' });
await dashboard.grid.editRow({ index: 2, columnHeader: 'Sheet1Id', value: '1' });
// Create LTAR-HM column
await dashboard.grid.column.create({
title: 'Link1-2hm',
type: UITypes.Links,
childTable: 'Sheet2',
relationType: 'Has Many',
custom: true,
refColumn: 'Sheet1Id',
});
// Sheet2 now has all 3 column categories : HM, BT, MM
// Verify fields and toggle the visibility
await dashboard.grid.toolbar.clickFields();
for (const title of ['Sheet1', 'Sheet1s']) {
await dashboard.grid.toolbar.fields.verify({ title, checked: false });
await dashboard.grid.toolbar.fields.click({ title, isLocallySaved: false });
}
await dashboard.grid.toolbar.clickFields();
// Expanded form insert
await dashboard.grid.footbar.clickAddRecordFromForm();
await dashboard.expandedForm.fillField({
columnTitle: 'Title',
value: '2a',
});
await dashboard.expandedForm.fillField({
columnTitle: 'Sheet1',
value: '1a',
type: 'belongsTo',
});
await dashboard.expandedForm.fillField({
columnTitle: 'Sheet1s',
value: '1a',
type: 'manyToMany',
});
await dashboard.expandedForm.fillField({
columnTitle: 'Link2-1hm',
value: '1a',
type: 'hasMany',
});
await dashboard.expandedForm.save();
// In cell insert
await dashboard.grid.addNewRow({ index: 1, value: '2b' });
await dashboard.grid.cell.inCellAdd({ index: 1, columnHeader: 'Sheet1' });
await dashboard.linkRecord.select('1b');
await dashboard.grid.cell.inCellAdd({
index: 1,
columnHeader: 'Sheet1s',
});
await dashboard.linkRecord.select('1b');
await dashboard.grid.cell.inCellAdd({
index: 1,
columnHeader: 'Link2-1hm',
});
await dashboard.linkRecord.select('1b');
// Expand record insert
await dashboard.grid.addNewRow({ index: 2, value: '2c-temp' });
await dashboard.grid.openExpandedRow({ index: 2 });
await dashboard.expandedForm.fillField({
columnTitle: 'Sheet1',
value: '1c',
type: 'belongsTo',
});
await dashboard.expandedForm.fillField({
columnTitle: 'Sheet1s',
value: '1c',
type: 'manyToMany',
});
await dashboard.expandedForm.fillField({
columnTitle: 'Link2-1hm',
value: '1c',
type: 'hasMany',
});
await dashboard.expandedForm.fillField({
columnTitle: 'Title',
value: '2c',
type: 'text',
});
await dashboard.rootPage.waitForTimeout(1000);
await dashboard.expandedForm.save();
const expected = [
[['1a'], ['1b'], ['1c']],
[['1 Sheet1'], ['1 Sheet1'], ['1 Sheet1']],
[['1 Sheet1'], ['1 Sheet1'], ['1 Sheet1']],
];
const colHeaders = ['Sheet1', 'Sheet1s', 'Link2-1hm'];
// verify LTAR cell values
for (let i = 0; i < expected.length; i++) {
for (let j = 0; j < expected[i].length; j++) {
await dashboard.grid.cell.verifyVirtualCell({
index: j,
columnHeader: colHeaders[i],
count: 1,
value: expected[i][j],
type: i === 0 ? 'bt' : undefined,
options: { singular: 'Sheet1', plural: 'Sheet1s' },
});
}
}
await dashboard.closeTab({ title: 'Sheet2' });
await dashboard.treeView.openTable({ title: 'Sheet1' });
// Verify fields and toggle the visibility
await dashboard.grid.toolbar.clickFields();
await dashboard.grid.toolbar.fields.verify({ title: 'Sheet2', checked: false });
await dashboard.grid.toolbar.fields.click({ title: 'Sheet2', isLocallySaved: false });
await dashboard.grid.toolbar.clickFields();
const expected2 = [
[['1 Sheet2'], ['1 Sheet2'], ['1 Sheet2']],
[['1 Sheet2'], ['1 Sheet2'], ['1 Sheet2']],
[['2a'], ['2b'], ['2c']],
];
const colHeaders2 = ['Link1-2hm', 'Link1-2mm', 'Sheet2'];
// verify LTAR cell values
for (let i = 0; i < expected2.length; i++) {
for (let j = 0; j < expected2[i].length; j++) {
await dashboard.grid.cell.verifyVirtualCell({
index: j,
columnHeader: colHeaders2[i],
count: 1,
value: expected2[i][j],
type: i === 2 ? 'bt' : undefined,
options: { singular: 'Sheet2', plural: 'Sheet2s' },
});
}
}
// Unlink LTAR cells
for (let i = 0; i < expected2.length; i++) {
for (let j = 0; j < expected2[i].length; j++) {
await dashboard.rootPage.waitForTimeout(500);
await dashboard.grid.cell.unlinkVirtualCell({
index: j,
columnHeader: colHeaders2[i],
});
}
}
// delete columns
await dashboard.grid.column.delete({ title: 'Link1-2hm' });
await dashboard.grid.column.delete({ title: 'Link1-2mm' });
await dashboard.grid.column.delete({ title: 'Sheet2' });
// delete table
await dashboard.treeView.deleteTable({ title: 'Sheet1' });
await dashboard.treeView.deleteTable({ title: 'Sheet2' });
});
});
test.describe('Links after edit record', () => {
let dashboard: DashboardPage;
let context: any;
test.beforeEach(async ({ page }) => {
context = await setup({ page, isEmptyProject: false });
dashboard = new DashboardPage(page, context.base);
});
test.afterEach(async () => {
await unsetup(context);
});
async function verifyRow(param: {
index: number;
value: {
Country: string;
formula?: string;
SLT?: string;
Cities: string[];
};
}) {
await dashboard.grid.cell.verify({
index: param.index,
columnHeader: 'Country',
value: param.value.Country,
});
if (param.value.formula) {
await dashboard.grid.cell.verify({
index: param.index,
columnHeader: 'formula',
value: param.value.formula,
});
}
await dashboard.grid.cell.verifyVirtualCell({
index: param.index,
columnHeader: 'Cities',
count: param.value.Cities.length,
options: { singular: 'City', plural: 'Cities' },
});
if (param.value.SLT) {
await dashboard.grid.cell.verify({
index: param.index,
columnHeader: 'SLT',
value: param.value.SLT,
});
}
}
/**
* Scope:
* - Verify LTAR and lookup cell after updating any non-virtual column
* - Verify the formula cell in which the updated cell is referring
* - Verify other non-virtual cells
*
* https://github.com/nocodb/nocodb/issues/4220
*
*/
test('Existing LTAR table verification', async () => {
// open table
await dashboard.treeView.openTable({ title: 'Country' });
await verifyRow({
index: 0,
value: {
Country: 'Afghanistan',
Cities: ['Kabul'],
},
});
await verifyRow({
index: 1,
value: {
Country: 'Algeria',
Cities: ['Batna', 'Bchar', 'Skikda'],
},
});
// create new columns
await dashboard.grid.column.create({
title: 'SLT',
type: 'SingleLineText',
});
await dashboard.grid.column.create({
title: 'formula',
type: 'Formula',
formula: "CONCAT({Country}, ' ', {SLT})",
});
// insert new content into a cell
await dashboard.grid.editRow({
index: 0,
columnHeader: 'SLT',
value: 'test',
});
await verifyRow({
index: 0,
value: {
Country: 'Afghanistan',
Cities: ['Kabul'],
SLT: 'test',
formula: 'Afghanistan test',
},
});
// edit record
await dashboard.grid.editRow({
index: 0,
columnHeader: 'Country',
value: 'Afghanistan2',
});
await verifyRow({
index: 0,
value: {
Country: 'Afghanistan2',
Cities: ['Kabul'],
SLT: 'test',
formula: 'Afghanistan2 test',
},
});
// Delete cell contents and verify
await dashboard.grid.cell.click({ index: 0, columnHeader: 'SLT' });
// trigger delete button key
await dashboard.rootPage.keyboard.press('Delete');
// Verify other non-virtual cells
await verifyRow({
index: 0,
value: {
Country: 'Afghanistan2',
Cities: ['Kabul'],
SLT: '',
formula: 'Afghanistan2',
},
});
});
});
Loading…
Cancel
Save