Browse Source

Merge branch 'develop' into feat/pnpm

pull/5903/head
Wing-Kam Wong 1 year ago
parent
commit
a2d30268bd
  1. 15
      packages/nc-gui/components.d.ts
  2. 30
      packages/nc-gui/components/cell/TextArea.vue
  3. 31
      packages/nc-gui/components/cell/attachment/index.vue
  4. 5
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  5. 4
      packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue
  6. 8
      packages/nc-gui/components/dlg/ViewCreate.vue
  7. 8
      packages/nc-gui/components/nc/Button.vue
  8. 7
      packages/nc-gui/components/nc/Tabs.vue
  9. 2
      packages/nc-gui/components/project/AccessSettings.vue
  10. 5
      packages/nc-gui/components/project/AllTables.vue
  11. 8
      packages/nc-gui/components/project/InviteProjectCollabSection.vue
  12. 28
      packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue
  13. 2
      packages/nc-gui/components/smartsheet/sidebar/index.vue
  14. 1
      packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
  15. 2
      packages/nc-gui/components/workspace/CollaboratorsList.vue
  16. 5
      packages/nc-gui/utils/domUtils.ts
  17. 2
      packages/nc-lib-gui/package.json
  18. 2
      packages/nocodb-sdk/package.json
  19. 6
      packages/nocodb/package.json
  20. 2
      packages/nocodb/src/cache/RedisCacheMgr.ts
  21. 2
      packages/nocodb/src/cache/RedisMockCacheMgr.ts
  22. 13
      packages/nocodb/src/controllers/caches.controller.ts
  23. 2
      packages/nocodb/src/controllers/tables.controller.ts
  24. 8
      packages/nocodb/src/models/ModelRoleVisibility.ts
  25. 2
      packages/nocodb/src/modules/global/init-meta-service.provider.ts
  26. 30
      packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts
  27. 4
      packages/nocodb/src/services/tables.service.ts
  28. 8
      packages/nocodb/src/services/views.service.ts
  29. 4
      packages/nocodb/src/utils/acl.ts
  30. 2
      packages/nocodb/src/version-upgrader/NcUpgrader.ts
  31. 168
      packages/nocodb/src/version-upgrader/ncXcdbLTARIndexUpgrader.ts

15
packages/nc-gui/components.d.ts vendored

@ -78,15 +78,12 @@ declare module '@vue/runtime-core' {
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
CilFullscreen: typeof import('~icons/cil/fullscreen')['default']
CilFullscreenExit: typeof import('~icons/cil/fullscreen-exit')['default']
ClarityColorPickerSolid: typeof import('~icons/clarity/color-picker-solid')['default']
ClaritySuccessLine: typeof import('~icons/clarity/success-line')['default']
IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['default']
IcOutlineAccessTime: typeof import('~icons/ic/outline-access-time')['default']
IcOutlineInsertDriveFile: typeof import('~icons/ic/outline-insert-drive-file')['default']
IcRoundEdit: typeof import('~icons/ic/round-edit')['default']
IcRoundKeyboardArrowDown: typeof import('~icons/ic/round-keyboard-arrow-down')['default']
IcRoundSearch: typeof import('~icons/ic/round-search')['default']
IcRoundStarBorder: typeof import('~icons/ic/round-star-border')['default']
LogosGoogleGmail: typeof import('~icons/logos/google-gmail')['default']
MaterialSymbolsArrowCircleLeftRounded: typeof import('~icons/material-symbols/arrow-circle-left-rounded')['default']
MaterialSymbolsArrowCircleRightRounded: typeof import('~icons/material-symbols/arrow-circle-right-rounded')['default']
@ -94,10 +91,7 @@ declare module '@vue/runtime-core' {
MaterialSymbolsChevronRightRounded: typeof import('~icons/material-symbols/chevron-right-rounded')['default']
MaterialSymbolsCloseRounded: typeof import('~icons/material-symbols/close-rounded')['default']
MaterialSymbolsDarkModeOutline: typeof import('~icons/material-symbols/dark-mode-outline')['default']
MaterialSymbolsDeleteOutlineRounded: typeof import('~icons/material-symbols/delete-outline-rounded')['default']
MaterialSymbolsFileCopyOutline: typeof import('~icons/material-symbols/file-copy-outline')['default']
MaterialSymbolsGroupOutlineRounded: typeof import('~icons/material-symbols/group-outline-rounded')['default']
MaterialSymbolsInboxOutlineRounded: typeof import('~icons/material-symbols/inbox-outline-rounded')['default']
MaterialSymbolsKeyboardArrowDownRounded: typeof import('~icons/material-symbols/keyboard-arrow-down-rounded')['default']
MaterialSymbolsKeyboardReturn: typeof import('~icons/material-symbols/keyboard-return')['default']
MaterialSymbolsLightModeOutline: typeof import('~icons/material-symbols/light-mode-outline')['default']
@ -126,23 +120,16 @@ declare module '@vue/runtime-core' {
MdiChatProcessingOutline: typeof import('~icons/mdi/chat-processing-outline')['default']
MdiCheck: typeof import('~icons/mdi/check')['default']
MdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
MdiChevronLeft: typeof import('~icons/mdi/chevron-left')['default']
MdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
MdiCircleMedium: typeof import('~icons/mdi/circle-medium')['default']
MdiClose: typeof import('~icons/mdi/close')['default']
MdiCloseCircleOutline: typeof import('~icons/mdi/close-circle-outline')['default']
MdiCodeTags: typeof import('~icons/mdi/code-tags')['default']
MdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
MdiCurrencyUsd: typeof import('~icons/mdi/currency-usd')['default']
MdiDatabaseOutline: typeof import('~icons/mdi/database-outline')['default']
MdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
MdiDiscord: typeof import('~icons/mdi/discord')['default']
MdiDotsHorizontal: typeof import('~icons/mdi/dots-horizontal')['default']
MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
MdiEditOutline: typeof import('~icons/mdi/edit-outline')['default']
MdiEye: typeof import('~icons/mdi/eye')['default']
MdiFlag: typeof import('~icons/mdi/flag')['default']
MdiFolder: typeof import('~icons/mdi/folder')['default']
MdiHeart: typeof import('~icons/mdi/heart')['default']
MdiKeyStar: typeof import('~icons/mdi/key-star')['default']
MdiLinkVariant: typeof import('~icons/mdi/link-variant')['default']
@ -153,8 +140,6 @@ declare module '@vue/runtime-core' {
MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default']
MdiMoonFull: typeof import('~icons/mdi/moon-full')['default']
MdiPlus: typeof import('~icons/mdi/plus')['default']
MdiPlusOutline: typeof import('~icons/mdi/plus-outline')['default']
MdiRefresh: typeof import('~icons/mdi/refresh')['default']
MdiReload: typeof import('~icons/mdi/reload')['default']
MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['default']
MdiScriptTextOutline: typeof import('~icons/mdi/script-text-outline')['default']

30
packages/nc-gui/components/cell/TextArea.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { EditModeInj, IsExpandedFormOpenInj, RowHeightInj, inject, useVModel } from '#imports'
import { EditModeInj, IsExpandedFormOpenInj, RowHeightInj, inject, useVModel, iconMap, ActiveCellInj } from '#imports'
const props = defineProps<{
modelValue?: string | number
@ -32,6 +32,8 @@ const isVisible = ref(false)
const inputWrapperRef = ref<HTMLElement | null>(null)
const inputRef = ref<HTMLTextAreaElement | null>(null)
const active = inject(ActiveCellInj, ref(false))
watch(isVisible, () => {
if (isVisible.value) {
setTimeout(() => {
@ -86,21 +88,21 @@ onClickOutside(inputWrapperRef, (e) => {
<span v-else>{{ vModel }}</span>
<NcButton
class="!absolute right-0 bottom-0 nc-long-text-toggle-expand !duration-0"
:class="{
'top-1': rowHeight !== 1,
'mt-2': editEnabled,
'top-0.15': rowHeight === 1,
'!hidden': isExpandedFormOpen,
}"
type="text"
size="xsmall"
<div
v-if="active"
class="!absolute right-0 bottom-0 h-6 w-5 group cursor-pointer flex justify-end gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none p-1 hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
:class="{'right-2 bottom-2':editEnabled}"
data-testid="attachment-cell-file-picker-button"
@click.stop="isVisible = !isVisible"
>
<GeneralIcon v-if="isVisible" icon="shrink" class="nc-long-text-toggle-expand h-3.75 w-3.75 !text-xs" />
<GeneralIcon v-else icon="expand" class="nc-long-text-toggle-expand h-3.75 w-3.75 !text-xs" />
</NcButton>
<NcTooltip placement="bottom">
<template #title>Expand</template>
<component
:is="iconMap.expand"
class="transform dark:(!text-white) group-hover:(!text-grey-800 scale-120) text-gray-500 text-xs"
/>
</NcTooltip>
</div>
</div>
<template #overlay>
<div ref="inputWrapperRef" class="flex flex-col min-w-120 min-h-70 py-3 pl-3 pr-1 expanded-cell-input">

31
packages/nc-gui/components/cell/attachment/index.vue

@ -64,6 +64,8 @@ const {
const { dragging } = useSortable(sortableRef, visibleItems, updateModelValue, _isReadonly)
const active = inject(ActiveCellInj, ref(false))
const isReadonly = computed(() => {
return isLockedMode.value || _isReadonly.value
})
@ -147,7 +149,8 @@ const rowHeight = inject(RowHeightInj, ref(1.8))
:style="{
height: isForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`,
}"
class="nc-attachment-cell relative flex-1 color-transition flex items-center justify-between gap-1"
class="nc-attachment-cell relative flex color-transition flex items-center"
:class="{'justify-center': !active, 'justify-between': active}"
>
<LazyCellAttachmentCarousel />
@ -166,20 +169,19 @@ const rowHeight = inject(RowHeightInj, ref(1.8))
<div
v-if="!isReadonly"
:class="{ 'mx-auto px-4': !visibleItems.length }"
class="group cursor-pointer py-1 flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-1 shadow-sm hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
class="group cursor-pointer py-1 flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none shadow-sm hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
data-testid="attachment-cell-file-picker-button"
@click.stop="open"
>
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />
<a-tooltip v-else placement="bottom">
<NcTooltip placement="bottom">
<template #title> Click or drop a file into cell</template>
<div class="flex items-center gap-1">
<div v-if="active || !visibleItems.length" class="flex items-center gap-1">
<MaterialSymbolsAttachFile
class="transform dark:(!text-white) group-hover:(!text-accent scale-120) text-gray-500 text-[0.75rem]"
/>
<div
v-if="!visibleItems.length"
class="group-hover:text-primary text-gray-500 dark:text-gray-200 dark:group-hover:!text-white text-xs"
@ -187,7 +189,7 @@ const rowHeight = inject(RowHeightInj, ref(1.8))
Add file(s)
</div>
</div>
</a-tooltip>
</NcTooltip>
</div>
<div v-else class="flex" />
@ -202,12 +204,12 @@ const rowHeight = inject(RowHeightInj, ref(1.8))
}"
>
<template v-for="(item, i) of visibleItems" :key="item.url || item.title">
<a-tooltip placement="bottom">
<NcTooltip placement="bottom">
<template #title>
<div class="text-center w-full">{{ item.title }}</div>
</template>
<div v-if="isImage(item.title, item.mimetype ?? item.type)">
<div class="nc-attachment flex items-center justify-center" @click.stop="selectedImage = item">
<div class="nc-attachment flex items-center justify-center" :class="{'ml-2':active}" @click.stop="selectedImage = item">
<LazyCellAttachmentImage
class="max-h-[1.8rem] max-w-[1.8rem]"
:alt="item.title || `#${i}`"
@ -215,29 +217,30 @@ const rowHeight = inject(RowHeightInj, ref(1.8))
/>
</div>
</div>
<div v-else class="nc-attachment flex items-center justify-center" @click="openAttachment(item)">
<div v-else class="nc-attachment flex items-center justify-center" :class="{'ml-2':active}" @click="openAttachment(item)">
<component :is="FileIcon(item.icon)" v-if="item.icon" />
<IcOutlineInsertDriveFile v-else />
</div>
</a-tooltip>
</NcTooltip>
</template>
</div>
<div
class="group cursor-pointer flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-1 p-1 shadow-sm hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
v-if="active"
class="h-6 w-5 group cursor-pointer flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none p-1 hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
>
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />
<a-tooltip v-else placement="bottom">
<NcTooltip v-else placement="bottom">
<template #title> View attachments</template>
<component
:is="iconMap.expand"
class="transform dark:(!text-white) group-hover:(!text-accent scale-120) text-gray-500 text-[0.75rem]"
class="transform dark:(!text-white) group-hover:(!text-grey-800 scale-120) text-gray-500 text-[0.75rem]"
@click.stop="modalVisible = true"
/>
</a-tooltip>
</NcTooltip>
</div>
</template>

5
packages/nc-gui/components/dashboard/settings/DataSources.vue

@ -52,6 +52,7 @@ async function loadBases(changed?: boolean) {
try {
if (changed) refreshCommandPalette()
await until(() => !!project.value.id).toBeTruthy()
isReloading.value = true
vReload.value = true
const baseList = await $api.base.list(project.value.id as string)
@ -164,8 +165,7 @@ const forceAwaken = () => {
onMounted(async () => {
if (sources.value.length === 0) {
await loadBases()
await loadMetaDiff()
loadBases()
}
})
@ -174,7 +174,6 @@ watch(
async (reload) => {
if (reload && !isReloading.value) {
await loadBases()
await loadMetaDiff()
}
},
)

4
packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue

@ -350,8 +350,8 @@ onMounted(async () => {
// todo: replace setTimeout and follow better approach
setTimeout(() => {
const input = form.value?.$el?.querySelector('input[type=text]')
input.setSelectionRange(0, formState.value.title.length)
input.focus()
input?.setSelectionRange(0, formState.value.title.length)
input?.focus()
}, 500)
})
})

8
packages/nc-gui/components/dlg/ViewCreate.vue

@ -156,7 +156,13 @@ function init() {
}
async function onSubmit() {
const isValid = await formValidator.value?.validateFields()
let isValid = null
try {
isValid = await formValidator.value?.validateFields()
} catch (e) {
console.error(e)
}
if (isValid && form.type) {
const _meta = unref(meta)

8
packages/nc-gui/components/nc/Button.vue

@ -33,6 +33,8 @@ const emits = defineEmits(['update:loading'])
const slots = useSlots()
const NcButton = ref<HTMLElement | null>(null)
const size = computed(() => props.size)
const type = computed(() => props.type)
@ -63,10 +65,15 @@ const onBlur = () => {
isFocused.value = false
isClicked.value = false
}
useEventListener(NcButton, 'mousedown', () => {
isClicked.value = true
})
</script>
<template>
<a-button
ref="NcButton"
:disabled="props.disabled"
:loading="loading"
:type="type"
@ -80,7 +87,6 @@ const onBlur = () => {
}"
@focus="onFocus"
@blur="onBlur"
@mousedown="isClicked = true"
>
<div
class="flex flex-row gap-x-2.5 w-full"

7
packages/nc-gui/components/nc/Tabs.vue

@ -3,17 +3,10 @@ const props = defineProps<{
modelValue?: string
centered?: boolean
}>()
const emits = defineEmits<{
(event: 'update:modelValue', data: string): void
}>()
const vModel = useVModel(props, 'modelValue', emits)
</script>
<template>
<a-tabs
v-model:activeKey="vModel"
class="nc-tabs"
:class="{
centered: props.centered,

2
packages/nc-gui/components/project/AccessSettings.vue

@ -154,7 +154,7 @@ const accessibleRoles = computed<(typeof ProjectRoles)[keyof typeof ProjectRoles
v-else-if="!collaborators?.length"
class="nc-collaborators-list w-full h-full flex flex-col items-center justify-center mt-36"
>
<Empty description="No collaborators found" />
<a-empty description="No collaborators found" />
</div>
<div v-else class="nc-collaborators-list nc-scrollbar-md">
<div class="nc-collaborators-list-header">

5
packages/nc-gui/components/project/AllTables.vue

@ -1,4 +1,5 @@
<script lang="ts" setup>
import { stringifyRolesObj } from 'nocodb-sdk'
import type { BaseType, TableType } from 'nocodb-sdk'
import dayjs from 'dayjs'
@ -8,6 +9,8 @@ const { openedProject } = storeToRefs(useProjects())
const { isUIAllowed } = useUIPermission()
const { allRoles } = useRoles()
const { $e } = useNuxtApp()
const isImportModalOpen = ref(false)
@ -71,7 +74,7 @@ function openTableCreateDialog(baseIndex?: number | undefined) {
<template>
<div class="nc-all-tables-view">
<div v-if="isUIAllowed('tableCreate', false, roles)" class="flex flex-row gap-x-6 pb-3 pt-6">
<div v-if="isUIAllowed('tableCreate', false, stringifyRolesObj(allRoles))" class="flex flex-row gap-x-6 pb-3 pt-6">
<div class="nc-project-view-all-table-btn" data-testid="proj-view-btn__add-new-table" @click="openTableCreateDialog()">
<GeneralIcon icon="addOutlineBox" />
<div class="label">{{ $t('general.new') }} {{ $t('objects.table') }}</div>

8
packages/nc-gui/components/project/InviteProjectCollabSection.vue

@ -23,7 +23,13 @@ const usersData = ref<{
roles?: string
}>()
const isInvitingCollaborators = ref(false)
const inviteCollaborator = async () => {
isInvitingCollaborators.value = true
if (isInvitingCollaborators.value) return
try {
usersData.value = await inviteUser(inviteData)
usersData.roles = inviteData.roles
@ -35,6 +41,8 @@ const inviteCollaborator = async () => {
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
isInvitingCollaborators.value = false
}
const inviteUrl = computed(() =>

28
packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue

@ -297,13 +297,38 @@ const setIcon = async (icon: string, view: ViewType) => {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const scrollViewNode = () => {
const activeViewDom = document.querySelector(`.nc-views-menu [data-view-id="${activeView.value?.id}"]`) as HTMLElement
if (!activeViewDom) return
if (isElementInvisible(activeViewDom)) {
// Scroll to the view node
activeViewDom?.scrollIntoView({ behavior: 'auto', inline: 'start' })
}
}
watch(
() => activeView.value?.id,
() => {
if (!activeView.value?.id) return
// TODO: Find a better way to scroll to the view node
setTimeout(() => {
scrollViewNode()
}, 800)
},
{
immediate: true,
},
)
</script>
<template>
<a-menu
ref="menuRef"
:class="{ dragging }"
class="nc-views-menu flex flex-col !ml-3 !mr-2.5 w-full !border-r-0 !bg-inherit nc-scrollbar-md"
class="nc-views-menu flex flex-col !ml-3 w-full !border-r-0 !bg-inherit"
:selected-keys="selected"
>
<!-- Lazy load breaks menu item active styles, i.e. styles never change even when active item changes -->
@ -319,6 +344,7 @@ const setIcon = async (icon: string, view: ViewType) => {
'active': activeView?.id === view.id,
[`nc-${view.type ? viewTypeAlias[view.type] : undefined || view.type}-view-item`]: true,
}"
:data-view-id="view.id"
@change-view="changeView"
@open-modal="$emit('openModal', $event)"
@delete="openDeleteDialog"

2
packages/nc-gui/components/smartsheet/sidebar/index.vue

@ -186,7 +186,7 @@ function onOpenModal({
<div class="flex-1 flex flex-col min-h-0">
<div class="flex flex-col h-full justify-between w-full">
<div class="flex flex-grow w-full">
<div class="flex flex-grow nc-scrollbar-md pr-1.75 mr-0.5">
<div v-if="isViewsLoading" class="flex flex-col w-full">
<div class="flex flex-row items-center w-full mt-1.5 ml-5 gap-x-3">
<a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" />

1
packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue

@ -145,7 +145,6 @@ watch(open, () => {
<NcDropdown
v-model:visible="open"
offset-y
class=""
:trigger="['click']"
overlay-class-name="nc-dropdown-group-by-menu nc-toolbar-dropdown"
>

2
packages/nc-gui/components/workspace/CollaboratorsList.vue

@ -48,7 +48,7 @@ const accessibleRoles = computed<WorkspaceUserRoles[]>(() => {
</a-input>
</div>
<div v-if="!filterCollaborators?.length" class="w-full h-full flex flex-col items-center justify-center mt-36">
<Empty description="No collaborators found" />
<a-empty description="No collaborators found" />
</div>
<table v-else class="nc-collaborators-list-table !nc-scrollbar-md">
<thead>

5
packages/nc-gui/utils/domUtils.ts

@ -1,6 +1,5 @@
const isElementInvisible = (el: HTMLElement | Element) => {
const rect = el.getBoundingClientRect()
return rect.bottom < 0 || rect.right < 0 || rect.left > window.innerWidth || rect.top > window.innerHeight
const isElementInvisible = (elem: HTMLElement) => {
return !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length)
}
export { isElementInvisible }

2
packages/nc-lib-gui/package.json

@ -1,6 +1,6 @@
{
"name": "nc-lib-gui",
"version": "0.111.1",
"version": "0.111.2",
"description": "NocoDB GUI",
"author": {
"name": "NocoDB",

2
packages/nocodb-sdk/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb-sdk",
"version": "0.111.1",
"version": "0.111.2",
"description": "NocoDB SDK",
"main": "build/main/index.js",
"typings": "build/main/index.d.ts",

6
packages/nocodb/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb",
"version": "0.111.1",
"version": "0.111.2",
"description": "NocoDB Backend",
"main": "dist/bundle.js",
"author": {
@ -129,7 +129,7 @@
"mysql2": "^3.2.0",
"nanoid": "^3.1.20",
"nc-help": "^0.2.88",
"nc-lib-gui": "0.111.1",
"nc-lib-gui": "0.111.2",
"nc-plugin": "^0.1.3",
"ncp": "^2.0.0",
"nestjs-kafka": "^1.0.6",
@ -226,4 +226,4 @@
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
}

2
packages/nocodb/src/cache/RedisCacheMgr.ts vendored

@ -178,7 +178,7 @@ export default class RedisCacheMgr extends CacheMgr {
let getKey = `${this.prefix}:${scope}:${o.id}`;
// special case - MODEL_ROLE_VISIBILITY
if (scope === CacheScope.MODEL_ROLE_VISIBILITY) {
getKey = `${this.prefix}:${scope}:${o.id}:${o.role}`;
getKey = `${this.prefix}:${scope}:${o.fk_view_id}:${o.role}`;
}
// set Get Key
log(`RedisCacheMgr::setList: setting key ${getKey}`);

2
packages/nocodb/src/cache/RedisMockCacheMgr.ts vendored

@ -174,7 +174,7 @@ export default class RedisMockCacheMgr extends CacheMgr {
let getKey = `${this.prefix}:${scope}:${o.id}`;
// special case - MODEL_ROLE_VISIBILITY
if (scope === CacheScope.MODEL_ROLE_VISIBILITY) {
getKey = `${this.prefix}:${scope}:${o.id}:${o.role}`;
getKey = `${this.prefix}:${scope}:${o.fk_view_id}:${o.role}`;
}
// set Get Key
log(`RedisMockCacheMgr::setList: setting key ${getKey}`);

13
packages/nocodb/src/controllers/caches.controller.ts

@ -1,4 +1,5 @@
import { Controller, Delete, Get, UseGuards } from '@nestjs/common';
import { OrgUserRoles } from 'nocodb-sdk';
import { CachesService } from '~/services/caches.service';
import { GlobalGuard } from '~/guards/global/global.guard';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
@ -9,7 +10,11 @@ export class CachesController {
constructor(private readonly cachesService: CachesService) {}
@Get('/api/v1/db/meta/cache')
@Acl('cacheGet')
@Acl('cacheGet', {
scope: 'org',
allowedRoles: [OrgUserRoles.SUPER_ADMIN],
blockApiTokenAccess: true,
})
async cacheGet(_, res) {
const data = await this.cachesService.cacheGet();
res.set({
@ -20,7 +25,11 @@ export class CachesController {
}
@Delete('/api/v1/db/meta/cache')
@Acl('cacheDelete')
@Acl('cacheDelete', {
scope: 'org',
allowedRoles: [OrgUserRoles.SUPER_ADMIN],
blockApiTokenAccess: true,
})
async cacheDelete() {
return await this.cachesService.cacheDelete();
}

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

@ -38,7 +38,7 @@ export class TablesController {
projectId,
baseId,
includeM2M: includeM2M === 'true',
roles: extractRolesObj(req.user.roles),
roles: extractRolesObj(req.user.project_roles),
}),
);
}

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

@ -147,17 +147,21 @@ export default class ModelRoleVisibility implements ModelRoleVisibilityType {
insertObj.base_id = view.base_id;
}
await ncMeta.metaInsert2(
const result = await ncMeta.metaInsert2(
null,
null,
MetaTable.MODEL_ROLE_VISIBILITY,
insertObj,
);
const key = `${CacheScope.MODEL_ROLE_VISIBILITY}:${body.fk_view_id}:${body.role}`;
insertObj.id = result.id;
await NocoCache.appendToList(
CacheScope.MODEL_ROLE_VISIBILITY,
[insertObj.project_id],
`${CacheScope.MODEL_ROLE_VISIBILITY}:${body.fk_view_id}:${body.role}`,
key,
);
return this.get(

2
packages/nocodb/src/modules/global/init-meta-service.provider.ts

@ -27,7 +27,7 @@ export const InitMetaServiceProvider: Provider = {
const config = await NcConfig.createByEnv();
// set version
process.env.NC_VERSION = '0108003';
process.env.NC_VERSION = '0111002';
// init cache
await NocoCache.init();

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

@ -113,7 +113,7 @@ export class AtImportProcessor {
await sMapEM.init();
const userRole = syncDB.user.roles
.split(',')
.reduce((rolesObj, role) => ({ [role]: true, ...rolesObj }), {});
.reduce((rolesObj, role) => ({ [role]: true, ...rolesObj }));
const sMap = {
// static mapping records between aTblId && ncId
@ -666,7 +666,12 @@ export class AtImportProcessor {
const view = { list: [] };
view['list'] = await this.viewsService.viewList({
tableId: table.id,
user: { roles: userRole },
user: {
roles: userRole,
project_roles: {
owner: true,
},
},
});
recordPerfStats(_perfStart, 'dbView.list');
@ -745,7 +750,7 @@ export class AtImportProcessor {
const srcTbl: any =
await this.tablesService.getTableWithAccessibleViews({
tableId: srcTableId,
user: syncDB.user,
user: { ...syncDB.user, project_roles: { owner: true } },
});
recordPerfStats(_perfStart, 'dbTable.read');
@ -829,7 +834,7 @@ export class AtImportProcessor {
const childTblSchema: any =
await this.tablesService.getTableWithAccessibleViews({
tableId: ncLinkMappingTable[x].nc.childId,
user: syncDB.user,
user: { ...syncDB.user, project_roles: { owner: true } },
});
recordPerfStats(_perfStart, 'dbTable.read');
@ -837,7 +842,7 @@ export class AtImportProcessor {
const parentTblSchema: any =
await this.tablesService.getTableWithAccessibleViews({
tableId: ncLinkMappingTable[x].nc.parentId,
user: syncDB.user,
user: { ...syncDB.user, project_roles: { owner: true } },
});
recordPerfStats(_perfStart, 'dbTable.read');
@ -1734,7 +1739,12 @@ export class AtImportProcessor {
const viewList = { list: [] };
viewList['list'] = await this.viewsService.viewList({
tableId: tblId,
user: { roles: userRole },
user: {
roles: userRole,
project_roles: {
owner: true,
},
} as any,
});
recordPerfStats(_perfStart, 'dbView.list');
@ -1854,7 +1864,7 @@ export class AtImportProcessor {
const _perfStart = recordPerfStart();
const ncTbl: any = await this.tablesService.getTableWithAccessibleViews({
tableId: tblId,
user: syncDB.user,
user: { ...syncDB.user, project_roles: { owner: true } },
});
recordPerfStats(_perfStart, 'dbTable.read');
@ -2328,7 +2338,7 @@ export class AtImportProcessor {
ncTblList['list'] = await this.tablesService.getAccessibleTables({
projectId: ncCreatedProjectSchema.id,
baseId: syncDB.baseId,
roles: userRole,
roles: { ...userRole, owner: true },
});
recordPerfStats(_perfStart, 'base.tableList');
@ -2348,7 +2358,7 @@ export class AtImportProcessor {
const ncTbl: any =
await this.tablesService.getTableWithAccessibleViews({
tableId: ncTblList.list[i].id,
user: syncDB.user,
user: { ...syncDB.user, project_roles: { owner: true } },
});
recordPerfStats(_perfStart, 'dbTable.read');
@ -2383,7 +2393,7 @@ export class AtImportProcessor {
const ncTbl: any =
await this.tablesService.getTableWithAccessibleViews({
tableId: ncTblList.list[i].id,
user: syncDB.user,
user: { ...syncDB.user, project_roles: { owner: true } },
});
rtc.data.nestedLinks += await importLTARData({

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

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import DOMPurify from 'isomorphic-dompurify';
import { isLinksOrLTAR, isVirtualCol, ModelTypes, UITypes } from 'nocodb-sdk';
import { isLinksOrLTAR, isVirtualCol, ModelTypes, ProjectRoles, UITypes } from 'nocodb-sdk'
import { AppEvents } from 'nocodb-sdk';
import { MetaDiffsService } from './meta-diffs.service';
import { ColumnsService } from './columns.service';
@ -328,7 +328,7 @@ export class TablesService {
const tableViewMapping = viewList.reduce((o, view: any) => {
o[view.fk_model_id] = o[view.fk_model_id] || 0;
if (
Object.keys(param.roles).some(
Object.values(ProjectRoles).some(
(role) => param.roles[role] && !view.disabled[role],
)
) {

8
packages/nocodb/src/services/views.service.ts

@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AppEvents } from 'nocodb-sdk';
import { AppEvents, ProjectRoles } from 'nocodb-sdk';
import type {
SharedViewReqType,
UserType,
@ -70,6 +70,7 @@ export class ViewsService {
tableId: string;
user: {
roles: Record<string, boolean>;
project_roles: Record<string, boolean>;
};
}) {
const model = await Model.get(param.tableId);
@ -82,8 +83,9 @@ export class ViewsService {
// todo: user roles
//await View.list(param.tableId)
const filteredViewList = viewList.filter((view: any) => {
return Object.keys(param?.user?.roles).some(
(role) => param?.user?.roles[role] && !view.disabled[role],
return Object.values(ProjectRoles).some(
(role) =>
param?.user?.['project_roles']?.[role] && !view.disabled[role],
);
});

4
packages/nocodb/src/utils/acl.ts

@ -37,6 +37,10 @@ const permissionScopes = {
'testConnection',
'genericGPT',
// Cache
'cacheGet',
'cacheDelete',
// TODO: add ACL with project scope
'upload',
'uploadViaURL',

2
packages/nocodb/src/version-upgrader/NcUpgrader.ts

@ -15,6 +15,7 @@ import ncProjectEnvUpgrader from './ncProjectEnvUpgrader';
import ncHookUpgrader from './ncHookUpgrader';
import ncProjectConfigUpgrader from './ncProjectConfigUpgrader';
import ncXcdbLTARUpgrader from './ncXcdbLTARUpgrader';
import ncXcdbLTARIndexUpgrader from './ncXcdbLTARIndexUpgrader';
import type { MetaService } from '~/meta/meta.service';
import type { NcConfig } from '~/interface/config';
@ -142,6 +143,7 @@ export default class NcUpgrader {
{ name: '0105004', handler: ncHookUpgrader },
{ name: '0107004', handler: ncProjectConfigUpgrader },
{ name: '0108002', handler: ncXcdbLTARUpgrader },
{ name: '0111002', handler: ncXcdbLTARIndexUpgrader },
];
}
}

168
packages/nocodb/src/version-upgrader/ncXcdbLTARIndexUpgrader.ts

@ -0,0 +1,168 @@
import { Logger } from '@nestjs/common';
import { RelationTypes, UITypes } from 'nocodb-sdk';
import type { LinkToAnotherRecordColumn } from '~/models';
import type { MetaService } from '~/meta/meta.service';
import type { NcUpgraderCtx } from './NcUpgrader';
import { MetaTable } from '~/utils/globals';
import { Base } from '~/models';
import NcConnectionMgrv2 from '~/utils/common/NcConnectionMgrv2';
import { Model } from '~/models';
const logger = new Logger('LTARIndexUpgrader');
// An upgrader for adding missing index of LTAR relations in XCDB bases
async function upgradeModelRelationsIndex({
model,
indexes,
ncMeta,
sqlClient,
}: {
ncMeta: MetaService;
model: Model;
sqlClient: ReturnType<
(typeof NcConnectionMgrv2)['getSqlClient']
> extends Promise<infer U>
? U
: ReturnType<(typeof NcConnectionMgrv2)['getSqlClient']>;
indexes: {
cn: string;
key_name: string;
type: string;
rqd: boolean | number;
cst: string;
cstn: string;
}[];
}) {
// Iterate over each column and upgrade LTAR
for (const column of await model.getColumns(ncMeta)) {
if (column.uidt !== UITypes.LinkToAnotherRecord) {
continue;
}
const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>(
ncMeta,
);
// if colOptions not found then skip
if (!colOptions) {
continue;
}
switch (colOptions.type) {
// use belongs to since child column belongs to current table in that case
case RelationTypes.BELONGS_TO:
{
// const parentCol = await colOptions.getParentColumn(ncMeta);
const childCol = await colOptions.getChildColumn(ncMeta);
// const parentModel = await parentCol.getModel(ncMeta);
const childModel = await childCol.getModel(ncMeta);
// check index already exists or not
const indexExists = indexes.find((index) => {
return index.cn === childCol.column_name;
});
if (indexExists) {
continue;
}
logger.log(`Creating index for column '${childCol.column_name}'`);
// create a new index for the column
const indexArgs = {
columns: [childCol.column_name],
tn: childModel.table_name,
non_unique: true,
};
await sqlClient.indexCreate(indexArgs);
}
break;
}
}
}
// An upgrader for adding missing index for LTAR relations in XCDB bases
async function upgradeBaseRelations({
ncMeta,
base,
}: {
ncMeta: MetaService;
base: Base;
}) {
const sqlClient = await NcConnectionMgrv2.getSqlClient(base, ncMeta.knex);
// get models for the base
const models = await ncMeta.metaList2(null, base.id, MetaTable.MODELS);
// get all columns and filter out relations and create index if not exists
for (const model of models) {
logger.log(`Upgrading model '${model.title}'`);
logger.log(`Fetching index list of model '${model.title}'`);
const indexes = await sqlClient.indexList({
tn: model.table_name,
});
const indexList = indexes.data?.list ?? [];
// exclude composite indexes
const filteredIndexList = indexList.filter((indexObj, i) => {
return indexList.every((indexObj2, j) => {
if (i === j) return true;
return indexObj2.key_name !== indexObj.key_name;
});
});
await upgradeModelRelationsIndex({
ncMeta,
model: new Model(model),
sqlClient,
indexes: filteredIndexList,
});
logger.log(`Upgraded model '${model.title}'`);
}
}
// Add missing index for LTAR relations
export default async function ({ ncMeta }: NcUpgraderCtx) {
logger.log(
'Starting upgrade for LTAR relations in XCDB bases to add missing index',
);
// get all xcdb bases
const bases = await ncMeta.metaList2(null, null, MetaTable.BASES, {
condition: {
is_meta: 1,
},
orderBy: {},
});
if (!bases.length) return;
// iterate and upgrade each base
for (const _base of bases) {
const base = new Base(_base);
// skip if not pg, since for other db we don't need to upgrade
if (ncMeta.knex.clientType() !== 'pg') {
continue;
}
const project = await base.getProject(ncMeta);
// skip deleted projects
if (!project || project.deleted) continue;
logger.log(`Upgrading base '${base.alias}'(${project.title})`);
await upgradeBaseRelations({
ncMeta,
base,
});
logger.log(`Upgraded base '${base.alias}'(${project.title})`);
}
logger.log(
'Finished upgrade for LTAR relations in XCDB bases to add missing index',
);
}
Loading…
Cancel
Save