Browse Source

Merge branch 'develop' of https://github.com/DIWAKARKASHYAP/nocodb into develop

pull/5799/head
DIWAKAR 2 years ago
parent
commit
3048b92ab9
  1. 8
      packages/nc-gui/components/smartsheet/Gallery.vue
  2. 29
      packages/nc-gui/components/smartsheet/Grid.vue
  3. 2
      packages/nc-gui/components/smartsheet/header/Cell.vue
  4. 3
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  5. 5
      packages/nc-gui/components/virtual-cell/BelongsTo.vue
  6. 5
      packages/nc-gui/components/virtual-cell/HasMany.vue
  7. 3
      packages/nc-gui/components/virtual-cell/Lookup.vue
  8. 5
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  9. 3
      packages/nc-gui/composables/useApi/interceptors.ts
  10. 6
      packages/nc-gui/composables/useGridViewColumnWidth.ts
  11. 1
      packages/nc-gui/composables/useMultiSelect/convertCellData.ts
  12. 5
      packages/nc-gui/composables/useMultiSelect/index.ts
  13. 1
      packages/nc-gui/context/index.ts
  14. 80
      packages/nc-gui/lang/tr.json
  15. 2
      packages/nc-gui/package-lock.json
  16. 9
      packages/nc-gui/pages/index/index/index.vue
  17. 2
      packages/nc-lib-gui/package.json
  18. 4
      packages/nocodb-sdk/package-lock.json
  19. 2
      packages/nocodb-sdk/package.json
  20. 74
      packages/nocodb-sdk/src/lib/Api.ts
  21. 10
      packages/nocodb/.eslintrc.js
  22. 20
      packages/nocodb/package-lock.json
  23. 4
      packages/nocodb/package.json
  24. 8
      packages/nocodb/src/Noco.ts
  25. 6
      packages/nocodb/src/cache/RedisCacheMgr.ts
  26. 2
      packages/nocodb/src/cache/RedisMockCacheMgr.ts
  27. 1
      packages/nocodb/src/controllers/api-tokens.controller.ts
  28. 1
      packages/nocodb/src/controllers/attachments.controller.ts
  29. 1
      packages/nocodb/src/controllers/audits.controller.ts
  30. 1
      packages/nocodb/src/controllers/bases.controller.ts
  31. 1
      packages/nocodb/src/controllers/bulk-data-alias.controller.ts
  32. 1
      packages/nocodb/src/controllers/columns.controller.ts
  33. 1
      packages/nocodb/src/controllers/data-alias-export.controller.ts
  34. 3
      packages/nocodb/src/controllers/data-alias-nested.controller.ts
  35. 1
      packages/nocodb/src/controllers/data-alias.controller.ts
  36. 2
      packages/nocodb/src/controllers/filters.controller.ts
  37. 1
      packages/nocodb/src/controllers/form-columns.controller.ts
  38. 1
      packages/nocodb/src/controllers/galleries.controller.ts
  39. 1
      packages/nocodb/src/controllers/grid-columns.controller.ts
  40. 1
      packages/nocodb/src/controllers/grids.controller.ts
  41. 1
      packages/nocodb/src/controllers/maps.controller.ts
  42. 1
      packages/nocodb/src/controllers/meta-diffs.controller.ts
  43. 1
      packages/nocodb/src/controllers/model-visibilities.controller.ts
  44. 1
      packages/nocodb/src/controllers/old-datas/old-datas.controller.ts
  45. 1
      packages/nocodb/src/controllers/org-users.controller.ts
  46. 10
      packages/nocodb/src/controllers/plugins.controller.ts
  47. 1
      packages/nocodb/src/controllers/project-users.controller.ts
  48. 77
      packages/nocodb/src/controllers/projects.controller.ts
  49. 1
      packages/nocodb/src/controllers/shared-bases.controller.ts
  50. 1
      packages/nocodb/src/controllers/sorts.controller.ts
  51. 1
      packages/nocodb/src/controllers/tables.controller.ts
  52. 111
      packages/nocodb/src/controllers/users/users.controller.ts
  53. 3
      packages/nocodb/src/controllers/view-columns.controller.ts
  54. 6
      packages/nocodb/src/db/BaseModelSql.ts
  55. 19
      packages/nocodb/src/db/BaseModelSqlv2.ts
  56. 2
      packages/nocodb/src/db/CustomKnex.ts
  57. 2
      packages/nocodb/src/db/conditionV2.ts
  58. 2
      packages/nocodb/src/db/sql-client/lib/SqlClientFactory.ts
  59. 1
      packages/nocodb/src/interceptors/is-upload-allowed/is-upload-allowed.interceptor.ts
  60. 9
      packages/nocodb/src/middlewares/extract-project-id/extract-project-id.middleware.ts
  61. 2
      packages/nocodb/src/models/Model.ts
  62. 10
      packages/nocodb/src/models/ProjectUser.ts
  63. 1
      packages/nocodb/src/modules/datas/helpers.ts
  64. 3
      packages/nocodb/src/modules/jobs/jobs/at-import/helpers/readAndProcessData.ts
  65. 5
      packages/nocodb/src/modules/users/users.module.ts
  66. 1
      packages/nocodb/src/run/docker.ts
  67. 1
      packages/nocodb/src/run/dockerRunPG_CyQuick.ts
  68. 22
      packages/nocodb/src/schema/swagger.json
  69. 5
      packages/nocodb/src/services/bulk-data-alias.service.ts
  70. 84
      packages/nocodb/src/services/public-metas.service.ts
  71. 4
      packages/nocodb/src/services/users/users.service.ts
  72. 9
      packages/nocodb/src/utils/nc-config/NcConfig.ts
  73. 1
      packages/nocodb/src/version-upgrader/v1-legacy/gql/GqlApiBuilder.ts
  74. 5
      packages/nocodb/src/version-upgrader/v1-legacy/rest/RestApiBuilder.ts
  75. 79
      tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts
  76. 49
      tests/playwright/tests/db/filters.spec.ts
  77. 34
      tests/playwright/tests/db/timezone.spec.ts

8
packages/nc-gui/components/smartsheet/Gallery.vue

@ -7,6 +7,7 @@ import {
IsFormInj, IsFormInj,
IsGalleryInj, IsGalleryInj,
IsGridInj, IsGridInj,
IsPublicInj,
MetaInj, MetaInj,
NavigateDir, NavigateDir,
OpenNewRecordFormHookInj, OpenNewRecordFormHookInj,
@ -62,6 +63,8 @@ provide(IsGridInj, ref(false))
provide(PaginationDataInj, paginationData) provide(PaginationDataInj, paginationData)
provide(ChangePageInj, changePage) provide(ChangePageInj, changePage)
const isPublic = inject(IsPublicInj, ref(false))
const fields = inject(FieldsInj, ref([])) const fields = inject(FieldsInj, ref([]))
const route = useRoute() const route = useRoute()
@ -125,6 +128,10 @@ const attachments = (record: any): Attachment[] => {
} }
const expandForm = (row: RowType, state?: Record<string, any>) => { const expandForm = (row: RowType, state?: Record<string, any>) => {
if (isPublic.value) {
return
}
const rowId = extractPkFromRow(row.row, meta.value!.columns!) const rowId = extractPkFromRow(row.row, meta.value!.columns!)
if (rowId) { if (rowId) {
@ -234,6 +241,7 @@ watch(view, async (nextView) => {
:data-testid="`nc-gallery-card-${record.row.id}`" :data-testid="`nc-gallery-card-${record.row.id}`"
@click="expandFormClick($event, record)" @click="expandFormClick($event, record)"
@contextmenu="showContextMenu($event, { row: rowIndex })" @contextmenu="showContextMenu($event, { row: rowIndex })"
:style="isPublic ? { cursor: 'default' } : { cursor: 'pointer' }"
> >
<template v-if="galleryData?.fk_cover_image_col_id" #cover> <template v-if="galleryData?.fk_cover_image_col_id" #cover>
<a-carousel v-if="!reloadAttachments && attachments(record).length" autoplay class="gallery-carousel" arrows> <a-carousel v-if="!reloadAttachments && attachments(record).length" autoplay class="gallery-carousel" arrows>

29
packages/nc-gui/components/smartsheet/Grid.vue

@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick } from '@vue/runtime-core'
import type { ColumnReqType, ColumnType, GridType, PaginatedType, TableType, ViewType } from 'nocodb-sdk' import type { ColumnReqType, ColumnType, GridType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk' import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { import {
@ -141,11 +142,20 @@ const getContainerScrollForElement = (
) => { ) => {
const childPos = el.getBoundingClientRect() const childPos = el.getBoundingClientRect()
const parentPos = container.getBoundingClientRect() const parentPos = container.getBoundingClientRect()
// provide an extra offset to show the prev/next/up/bottom cell
const extraOffset = 15
const numColWidth = container.querySelector('thead th:nth-child(1)')?.getBoundingClientRect().width ?? 0
const primaryColWidth = container.querySelector('thead th:nth-child(2)')?.getBoundingClientRect().width ?? 0
const stickyColsWidth = numColWidth + primaryColWidth
const relativePos = { const relativePos = {
top: childPos.top - parentPos.top, top: childPos.top - parentPos.top,
right: childPos.right - parentPos.right, right: childPos.right - parentPos.right,
bottom: childPos.bottom - parentPos.bottom, bottom: childPos.bottom - parentPos.bottom,
left: childPos.left - parentPos.left, left: childPos.left - parentPos.left - stickyColsWidth,
} }
const scroll = { const scroll = {
@ -159,9 +169,9 @@ const getContainerScrollForElement = (
*/ */
scroll.left = scroll.left =
relativePos.right + (offset?.right || 0) > 0 relativePos.right + (offset?.right || 0) > 0
? container.scrollLeft + relativePos.right + (offset?.right || 0) ? container.scrollLeft + relativePos.right + (offset?.right || 0) + extraOffset
: relativePos.left - (offset?.left || 0) < 0 : relativePos.left - (offset?.left || 0) < 0
? container.scrollLeft + relativePos.left - (offset?.left || 0) ? container.scrollLeft + relativePos.left - (offset?.left || 0) - extraOffset
: container.scrollLeft : container.scrollLeft
/* /*
@ -170,9 +180,9 @@ const getContainerScrollForElement = (
*/ */
scroll.top = scroll.top =
relativePos.bottom + (offset?.bottom || 0) > 0 relativePos.bottom + (offset?.bottom || 0) > 0
? container.scrollTop + relativePos.bottom + (offset?.bottom || 0) ? container.scrollTop + relativePos.bottom + (offset?.bottom || 0) + extraOffset
: relativePos.top - (offset?.top || 0) < 0 : relativePos.top - (offset?.top || 0) < 0
? container.scrollTop + relativePos.top - (offset?.top || 0) ? container.scrollTop + relativePos.top - (offset?.top || 0) - extraOffset
: container.scrollTop : container.scrollTop
return scroll return scroll
@ -342,6 +352,12 @@ function scrollToCell(row?: number | null, col?: number | null) {
const { height: headerHeight } = tableHead.value!.getBoundingClientRect() const { height: headerHeight } = tableHead.value!.getBoundingClientRect()
const tdScroll = getContainerScrollForElement(td, gridWrapper.value, { top: headerHeight, bottom: 9, right: 9 }) const tdScroll = getContainerScrollForElement(td, gridWrapper.value, { top: headerHeight, bottom: 9, right: 9 })
// if first column set left to 0 since it's sticky it will be visible and calculated value will be wrong
// setting left to 0 will make it scroll to the left
if (col === 0) {
tdScroll.left = 0
}
if (rows && row === rows.length - 2) { if (rows && row === rows.length - 2) {
// if last row make 'Add New Row' visible // if last row make 'Add New Row' visible
gridWrapper.value.scrollTo({ gridWrapper.value.scrollTo({
@ -631,6 +647,9 @@ const onNavigate = (dir: NavigateDir) => {
} }
break break
} }
nextTick(() => {
scrollToCell()
})
} }
const showContextMenu = (e: MouseEvent, target?: { row: number; col: number }) => { const showContextMenu = (e: MouseEvent, target?: { row: number; col: number }) => {

2
packages/nc-gui/components/smartsheet/header/Cell.vue

@ -52,7 +52,7 @@ const openHeaderMenu = () => {
<span <span
v-if="column" v-if="column"
class="name" class="name"
:class="{ 'cursor-pointer': !isForm && isUIAllowed('edit-column') }" :class="{ 'cursor-pointer': !isForm && isUIAllowed('edit-column') && !hideMenu }"
style="white-space: nowrap" style="white-space: nowrap"
:title="column.title" :title="column.title"
@dblclick="openHeaderMenu" @dblclick="openHeaderMenu"

3
packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue

@ -225,7 +225,8 @@ defineExpose({
/> />
<span v-else :key="`${i}dummy`" /> <span v-else :key="`${i}dummy`" />
<div :key="`${i}nested`" class="flex"> <span v-if="!i" class="flex items-center">{{ $t('labels.where') }}</span>
<div v-else :key="`${i}nested`" class="flex bob">
<a-select <a-select
v-model:value="filter.logical_op" v-model:value="filter.logical_op"
:dropdown-match-select-width="false" :dropdown-match-select-width="false"

5
packages/nc-gui/components/virtual-cell/BelongsTo.vue

@ -7,6 +7,7 @@ import {
ColumnInj, ColumnInj,
IsFormInj, IsFormInj,
IsLockedInj, IsLockedInj,
IsUnderLookupInj,
ReadonlyInj, ReadonlyInj,
ReloadRowDataHookInj, ReloadRowDataHookInj,
RowInj, RowInj,
@ -36,6 +37,8 @@ const isForm = inject(IsFormInj, ref(false))
const isLocked = inject(IsLockedInj, ref(false)) const isLocked = inject(IsLockedInj, ref(false))
const isUnderLookup = inject(IsUnderLookupInj, ref(false))
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const listItemsDlg = ref(false) const listItemsDlg = ref(false)
@ -89,7 +92,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
</div> </div>
<div <div
v-if="!readOnly && !isLocked && (isUIAllowed('xcDatatableEditable') || isForm)" v-if="!readOnly && !isLocked && (isUIAllowed('xcDatatableEditable') || isForm) && !isUnderLookup"
class="flex justify-end gap-1 min-h-[30px] items-center" class="flex justify-end gap-1 min-h-[30px] items-center"
> >
<GeneralIcon <GeneralIcon

5
packages/nc-gui/components/virtual-cell/HasMany.vue

@ -16,6 +16,7 @@ import {
useSelectedCellKeyupListener, useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow, useSmartsheetRowStoreOrThrow,
useUIPermission, useUIPermission,
IsUnderLookupInj
} from '#imports' } from '#imports'
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
@ -32,6 +33,8 @@ const readOnly = inject(ReadonlyInj, ref(false))
const isLocked = inject(IsLockedInj) const isLocked = inject(IsLockedInj)
const isUnderLookup = inject(IsUnderLookupInj, ref(false))
const listItemsDlg = ref(false) const listItemsDlg = ref(false)
const childListDlg = ref(false) const childListDlg = ref(false)
@ -112,7 +115,7 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
</template> </template>
</div> </div>
<div v-if="!isLocked" class="flex justify-end gap-1 min-h-[30px] items-center"> <div v-if="!isLocked && !isUnderLookup" class="flex justify-end gap-1 min-h-[30px] items-center">
<GeneralIcon <GeneralIcon
icon="expand" icon="expand"
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand" class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"

3
packages/nc-gui/components/virtual-cell/Lookup.vue

@ -5,6 +5,7 @@ import {
CellUrlDisableOverlayInj, CellUrlDisableOverlayInj,
CellValueInj, CellValueInj,
ColumnInj, ColumnInj,
IsUnderLookupInj,
MetaInj, MetaInj,
computed, computed,
inject, inject,
@ -70,6 +71,8 @@ const arrValue = computed(() => {
provide(MetaInj, lookupTableMeta) provide(MetaInj, lookupTableMeta)
provide(IsUnderLookupInj, ref(true))
provide(CellUrlDisableOverlayInj, ref(true)) provide(CellUrlDisableOverlayInj, ref(true))
const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activateShowEditNonEditableFieldWarning } = const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activateShowEditNonEditableFieldWarning } =

5
packages/nc-gui/components/virtual-cell/ManyToMany.vue

@ -7,6 +7,7 @@ import {
ColumnInj, ColumnInj,
IsFormInj, IsFormInj,
IsLockedInj, IsLockedInj,
IsUnderLookupInj,
ReadonlyInj, ReadonlyInj,
ReloadRowDataHookInj, ReloadRowDataHookInj,
RowInj, RowInj,
@ -34,6 +35,8 @@ const readOnly = inject(ReadonlyInj, ref(false))
const isLocked = inject(IsLockedInj) const isLocked = inject(IsLockedInj)
const isUnderLookup = inject(IsUnderLookupInj, ref(false))
const listItemsDlg = ref(false) const listItemsDlg = ref(false)
const childListDlg = ref(false) const childListDlg = ref(false)
@ -114,7 +117,7 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
</template> </template>
</div> </div>
<div v-if="!isLocked" class="flex justify-end gap-1 min-h-[30px] items-center"> <div v-if="!isLocked && !isUnderLookup" class="flex justify-end gap-1 min-h-[30px] items-center">
<GeneralIcon <GeneralIcon
icon="expand" icon="expand"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand" class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"

3
packages/nc-gui/composables/useApi/interceptors.ts

@ -67,9 +67,8 @@ export function addAxiosInterceptors(api: Api<any>) {
}) })
.catch(async (error) => { .catch(async (error) => {
await state.signOut() await state.signOut()
// todo: handle new user
navigateTo('/signIn') if (!route.meta.public) navigateTo('/signIn')
return Promise.reject(error) return Promise.reject(error)
}) })

6
packages/nc-gui/composables/useGridViewColumnWidth.ts

@ -13,7 +13,7 @@ import {
watch, watch,
} from '#imports' } from '#imports'
export function useGridViewColumnWidth(view: Ref<ViewType | undefined>) { export function useGridViewColumnWidth(view: Ref<(ViewType & { columns?: GridColumnType[] }) | undefined>) {
const { css, load: loadCss, unload: unloadCss } = useStyleTag('') const { css, load: loadCss, unload: unloadCss } = useStyleTag('')
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
@ -52,7 +52,9 @@ export function useGridViewColumnWidth(view: Ref<ViewType | undefined>) {
const loadGridViewColumns = async () => { const loadGridViewColumns = async () => {
if (!view.value?.id && !isPublic.value) return if (!view.value?.id && !isPublic.value) return
const colsData: GridColumnType[] = (isPublic.value ? columns.value : await $api.dbView.gridColumnsList(view.value!.id!)) ?? [] const colsData: GridColumnType[] =
(isPublic.value ? view.value?.columns : await $api.dbView.gridColumnsList(view.value!.id!)) ?? []
gridViewCols.value = colsData.reduce<Record<string, GridColumnType>>( gridViewCols.value = colsData.reduce<Record<string, GridColumnType>>(
(o, col) => ({ (o, col) => ({
...o, ...o,

1
packages/nc-gui/composables/useMultiSelect/convertCellData.ts

@ -7,7 +7,6 @@ import { parseProp } from '#imports'
export default function convertCellData( export default function convertCellData(
args: { from: UITypes; to: UITypes; value: any; column: ColumnType; appInfo: AppInfo }, args: { from: UITypes; to: UITypes; value: any; column: ColumnType; appInfo: AppInfo },
isMysql = false, isMysql = false,
isXcdbBase = false,
) { ) {
const { from, to, value } = args const { from, to, value } = args
if (from === to && ![UITypes.Attachment, UITypes.Date, UITypes.DateTime, UITypes.Time, UITypes.Year].includes(to)) { if (from === to && ![UITypes.Attachment, UITypes.Date, UITypes.DateTime, UITypes.Time, UITypes.Year].includes(to)) {

5
packages/nc-gui/composables/useMultiSelect/index.ts

@ -55,7 +55,7 @@ export function useMultiSelect(
const { appInfo } = useGlobal() const { appInfo } = useGlobal()
const { isMysql, isXcdbBase } = useProject() const { isMysql } = useProject()
let clipboardContext = $ref<{ value: any; uidt: UITypes } | null>(null) let clipboardContext = $ref<{ value: any; uidt: UITypes } | null>(null)
@ -208,6 +208,7 @@ export function useMultiSelect(
selectedRange.startRange({ row, col }) selectedRange.startRange({ row, col })
selectedRange.endRange({ row, col }) selectedRange.endRange({ row, col })
makeActive(row, col) makeActive(row, col)
scrollToActiveCell?.()
isMouseDown = false isMouseDown = false
} }
@ -359,7 +360,6 @@ export function useMultiSelect(
appInfo: unref(appInfo), appInfo: unref(appInfo),
}, },
isMysql(meta.value?.base_id), isMysql(meta.value?.base_id),
isXcdbBase(meta.value?.base_id),
) )
e.preventDefault() e.preventDefault()
@ -394,7 +394,6 @@ export function useMultiSelect(
appInfo: unref(appInfo), appInfo: unref(appInfo),
}, },
isMysql(meta.value?.base_id), isMysql(meta.value?.base_id),
isXcdbBase(meta.value?.base_id),
) )
e.preventDefault() e.preventDefault()
syncCellData?.(activeCell) syncCellData?.(activeCell)

1
packages/nc-gui/context/index.ts

@ -38,3 +38,4 @@ export const ToggleDialogInj: InjectionKey<Function> = Symbol('toggle-dialog-inj
export const CellClickHookInj: InjectionKey<EventHook<MouseEvent> | undefined> = Symbol('cell-click-injection') export const CellClickHookInj: InjectionKey<EventHook<MouseEvent> | undefined> = Symbol('cell-click-injection')
export const SaveRowInj: InjectionKey<(() => void) | undefined> = Symbol('save-row-injection') export const SaveRowInj: InjectionKey<(() => void) | undefined> = Symbol('save-row-injection')
export const CurrentCellInj: InjectionKey<Ref<Element | undefined>> = Symbol('current-cell-injection') export const CurrentCellInj: InjectionKey<Ref<Element | undefined>> = Symbol('current-cell-injection')
export const IsUnderLookupInj: InjectionKey<Ref<boolean>> = Symbol('is-under-lookup-injection')

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

@ -39,7 +39,7 @@
"signIn": "KAYIT OL", "signIn": "KAYIT OL",
"signOut": "Oturumu Kapat", "signOut": "Oturumu Kapat",
"required": "Gerekli", "required": "Gerekli",
"enableScanner": "Enable Scanner for filling", "enableScanner": "Doldurmak için tarayıcıyı etkinleştir",
"preferred": "Tercihen", "preferred": "Tercihen",
"mandatory": "Zorunlu", "mandatory": "Zorunlu",
"loading": "Yükleniyor ...", "loading": "Yükleniyor ...",
@ -76,7 +76,7 @@
"hideField": "Alanı Gizle", "hideField": "Alanı Gizle",
"sortAsc": "Artan Sırala", "sortAsc": "Artan Sırala",
"sortDesc": "Azalan Sıralama", "sortDesc": "Azalan Sıralama",
"geoDataField": "GeoData Field" "geoDataField": "GeoData Alanı"
}, },
"objects": { "objects": {
"project": "Proje", "project": "Proje",
@ -101,7 +101,7 @@
"form": "Form", "form": "Form",
"kanban": "Kanban", "kanban": "Kanban",
"calendar": "Takvim", "calendar": "Takvim",
"map": "Map" "map": "Harita"
}, },
"user": "Kullanıcı", "user": "Kullanıcı",
"users": "Kullanıcılar", "users": "Kullanıcılar",
@ -210,8 +210,8 @@
"advancedSettings": "Gelişmiş Ayarlar", "advancedSettings": "Gelişmiş Ayarlar",
"codeSnippet": "Kod Parçacığı", "codeSnippet": "Kod Parçacığı",
"keyboardShortcut": "Klavye Kısayolları", "keyboardShortcut": "Klavye Kısayolları",
"generateRandomName": "Generate Random Name", "generateRandomName": "Rastgele Ad Oluştur",
"findRowByScanningCode": "Find row by scanning a QR or Barcode" "findRowByScanningCode": "QR veya Barkod okutarak satır bulun"
}, },
"labels": { "labels": {
"createdBy": "Tarafından Oluşturuldu", "createdBy": "Tarafından Oluşturuldu",
@ -221,7 +221,7 @@
"viewName": "Görünüm adı", "viewName": "Görünüm adı",
"viewLink": "Görünüm Linki", "viewLink": "Görünüm Linki",
"columnName": "Sütun Adı", "columnName": "Sütun Adı",
"columnToScanFor": "Column to scan", "columnToScanFor": "Taranacak sütun",
"columnType": "Sütun Tipi", "columnType": "Sütun Tipi",
"roleName": "Rol Adı", "roleName": "Rol Adı",
"roleDescription": "Rol Tanımı", "roleDescription": "Rol Tanımı",
@ -238,7 +238,7 @@
"action": "Aksiyon", "action": "Aksiyon",
"actions": "Aksiyonlar", "actions": "Aksiyonlar",
"operation": "İşlem", "operation": "İşlem",
"operationSub": "Sub Operation", "operationSub": "Alt İşlem",
"operationType": "İşlem türü", "operationType": "İşlem türü",
"operationSubType": "İşlem alt-türü", "operationSubType": "İşlem alt-türü",
"description": "Tanım", "description": "Tanım",
@ -260,9 +260,9 @@
"barcodeFormat": "Barkod formatı", "barcodeFormat": "Barkod formatı",
"qrCodeValueTooLong": "QR kodu için çok fazla karakter", "qrCodeValueTooLong": "QR kodu için çok fazla karakter",
"barcodeValueTooLong": "Barkod için çok fazla karakter", "barcodeValueTooLong": "Barkod için çok fazla karakter",
"currentLocation": "Current Location", "currentLocation": "Geçerli Konum",
"lng": "Lng", "lng": "Boylam",
"lat": "Lat", "lat": "Enlem",
"aggregateFunction": "Birleştirme fonksiyonu", "aggregateFunction": "Birleştirme fonksiyonu",
"dbCreateIfNotExists": "Veritabanı : yoksa oluştur", "dbCreateIfNotExists": "Veritabanı : yoksa oluştur",
"clientKey": "İstemci Anahtarı", "clientKey": "İstemci Anahtarı",
@ -385,18 +385,18 @@
"nextRecord": "Sonraki kayıt", "nextRecord": "Sonraki kayıt",
"previousRecord": "Önceki kayıt", "previousRecord": "Önceki kayıt",
"copyApiURL": "API linkini kopyala", "copyApiURL": "API linkini kopyala",
"createTable": "Create New Table", "createTable": "Yeni tablo oluştur",
"refreshTable": "Tabloları Yenile", "refreshTable": "Tabloları Yenile",
"renameTable": "Rename Table", "renameTable": "Tabloyu Yeniden Adlandır",
"deleteTable": "Delete Table", "deleteTable": "Tabloyu Sil",
"addField": "Tabloya yeni alan ekle", "addField": "Tabloya yeni alan ekle",
"setDisplay": "Set as Display value", "setDisplay": "Görünüm değeri olarak ayarla",
"addRow": "Yeni satır ekle", "addRow": "Yeni satır ekle",
"saveRow": "Satırı kaydet", "saveRow": "Satırı kaydet",
"saveAndExit": "Kaydet ve Çık", "saveAndExit": "Kaydet ve Çık",
"saveAndStay": "Kaydet ve Kal", "saveAndStay": "Kaydet ve Kal",
"insertRow": "Yeni Satır Ekle", "insertRow": "Yeni Satır Ekle",
"duplicateRow": "Duplicate Row", "duplicateRow": "Satırı Çoğalt",
"deleteRow": "Satırı Sil", "deleteRow": "Satırı Sil",
"deleteSelectedRow": "Seçilen Satırları Sil", "deleteSelectedRow": "Seçilen Satırları Sil",
"importExcel": "Excel içe aktar", "importExcel": "Excel içe aktar",
@ -412,8 +412,8 @@
"changePwd": "Şifre değiştir", "changePwd": "Şifre değiştir",
"createView": "Bir görünüm oluştur", "createView": "Bir görünüm oluştur",
"shareView": "Görünümü paylaş", "shareView": "Görünümü paylaş",
"findRowByCodeScan": "Find row by scan", "findRowByCodeScan": "Tarayarak satır bul",
"fillByCodeScan": "Fill by scan", "fillByCodeScan": "Tarayarak doldur",
"listSharedView": "Paylaşılan Görünümler", "listSharedView": "Paylaşılan Görünümler",
"ListView": "Görünüm Listesi", "ListView": "Görünüm Listesi",
"copyView": "Görünümü kopyala", "copyView": "Görünümü kopyala",
@ -429,10 +429,10 @@
"openTab": "Yeni sekme aç", "openTab": "Yeni sekme aç",
"iFrame": "Gömülü HTML kodunu kopyalayın", "iFrame": "Gömülü HTML kodunu kopyalayın",
"addWebhook": "Yeni Webhook ekle", "addWebhook": "Yeni Webhook ekle",
"enableWebhook": "Enable Webhook", "enableWebhook": "Webhook'u Etkinleştir",
"testWebhook": "Test Webhook", "testWebhook": "Webhook'u Test Et",
"copyWebhook": "Copy Webhook", "copyWebhook": "Webhook'u Kopyala",
"deleteWebhook": "Delete Webhook", "deleteWebhook": "Webhook'u Sil",
"newToken": "Yeni Token ekle", "newToken": "Yeni Token ekle",
"exportZip": "Zip olarak dışa aktar", "exportZip": "Zip olarak dışa aktar",
"importZip": "Zip olarak içe aktar", "importZip": "Zip olarak içe aktar",
@ -472,10 +472,10 @@
"map": { "map": {
"mappedBy": "Mapped By", "mappedBy": "Mapped By",
"chooseMappingField": "Choose a Mapping Field", "chooseMappingField": "Choose a Mapping Field",
"openInGoogleMaps": "Google Maps", "openInGoogleMaps": "Google Haritalar",
"openInOpenStreetMap": "OSM" "openInOpenStreetMap": "OSM"
}, },
"toggleMobileMode": "Toggle Mobile Mode" "toggleMobileMode": "Mobil Modu Aç / Kapat"
}, },
"tooltip": { "tooltip": {
"saveChanges": "Değişiklikleri Kaydet", "saveChanges": "Değişiklikleri Kaydet",
@ -542,15 +542,15 @@
"orgViewer": "İzleyicinin yeni proje oluşturmasına izin verilmez ancak davet edilen herhangi bir projeye erişebilir." "orgViewer": "İzleyicinin yeni proje oluşturmasına izin verilmez ancak davet edilen herhangi bir projeye erişebilir."
}, },
"codeScanner": { "codeScanner": {
"loadingScanner": "Loading the scanner...", "loadingScanner": "Tarayıcı yükleniyor...",
"selectColumn": "Select a column (QR code or Barcode) that you want to use for finding a row by scanning.", "selectColumn": "Tarayarak satır bulmak için kullanmak istediğiniz sütunu (QR kodu veya Barkod) seçin.",
"moreThanOneRowFoundForCode": "More than one row found for this code. Currently only unique codes are supported.", "moreThanOneRowFoundForCode": "Bu kod için birden fazla satır bulundu. Şu anda yalnızca benzersiz kodlar desteklenmektedir.",
"noRowFoundForCode": "No row found for this code for the selected column" "noRowFoundForCode": "Seçilen sütunda bu kod için satır bulunamadı"
}, },
"map": { "map": {
"overLimit": "You're over the limit.", "overLimit": "Sınırı aştınız.",
"closeLimit": "You're getting close to the limit.", "closeLimit": "Sınıra yaklaşıyorsunuz.",
"limitNumber": "The limit of markers shown in a Map View is 1000 records." "limitNumber": "Bir Harita Görünümünde en fazla 1000 kayıt gösterilebilir."
}, },
"footerInfo": "Sayfa başına satır", "footerInfo": "Sayfa başına satır",
"upload": "Yüklenecek Dosyayı Seçin", "upload": "Yüklenecek Dosyayı Seçin",
@ -634,7 +634,7 @@
"gallery": "Galeri görünümü ekle", "gallery": "Galeri görünümü ekle",
"form": "Form görünümü ekle", "form": "Form görünümü ekle",
"kanban": "Kanban görünümü ekle", "kanban": "Kanban görünümü ekle",
"map": "Add Map View", "map": "Harita Görünümü Ekle",
"calendar": "Takvim görünümü ekle" "calendar": "Takvim görünümü ekle"
}, },
"tablesMetadataInSync": "Tablonun meta verileri senkronize", "tablesMetadataInSync": "Tablonun meta verileri senkronize",
@ -666,11 +666,11 @@
"deleteViewConfirmation": "Bu görünümü silmek istediğinizden emin misiniz?", "deleteViewConfirmation": "Bu görünümü silmek istediğinizden emin misiniz?",
"deleteTableConfirmation": "Tabloyu silmek istiyor musunuz", "deleteTableConfirmation": "Tabloyu silmek istiyor musunuz",
"showM2mTables": "M2M Tablolarını Göster", "showM2mTables": "M2M Tablolarını Göster",
"showM2mTablesDesc": "Many-to-many relation is supported via a junction table & is hidden by default. Enable this option to list all such tables along with existing tables.", "showM2mTablesDesc": "Çoktan çoğa ilişki bir köprü tablosu aracılığıyla oluşturulur ve bu tablo varsayılan olarak gizlenir. Mevcut tablolarla birlikte bu köprü tablolarını da listelemek için bu seçeneği etkinleştirin.",
"showNullInCells": "Show NULL in Cells", "showNullInCells": "Hücrelerde NULL Göster",
"showNullInCellsDesc": "Display 'NULL' tag in cells holding NULL value. This helps differentiate against cells holding EMPTY string.", "showNullInCellsDesc": "NULL değerini taşıyan hücreler için 'NULL' etiketini göster. Bu BOŞ dizi değerleri ile ayırt etmenize yardımcı olur.",
"showNullAndEmptyInFilter": "Show NULL and EMPTY in Filter", "showNullAndEmptyInFilter": "Filtrelerde NULL ve EMPTY Göster",
"showNullAndEmptyInFilterDesc": "Enable 'additional' filters to differentiate fields containing NULL & Empty Strings. Default support for Blank treats both NULL & Empty strings alike.", "showNullAndEmptyInFilterDesc": "NULL ve Boş dizileri ayırt etmek için 'Ekstra' filtreleri etkinleştir. Varsayılan 'Blank' değeri NULL ve boş dizileri aynı şekilde ele alır.",
"deleteKanbanStackConfirmation": "Bu yığının silinmesi `{stackToBeDeleted}` seçim seçeneğini `{groupingField}` adresinden de kaldıracaktır. Kayıtlar kategorize edilmemiş yığına taşınacaktır.", "deleteKanbanStackConfirmation": "Bu yığının silinmesi `{stackToBeDeleted}` seçim seçeneğini `{groupingField}` adresinden de kaldıracaktır. Kayıtlar kategorize edilmemiş yığına taşınacaktır.",
"computedFieldEditWarning": "Hesaplanan alan: içerik salt okunurdur. Yeniden yapılandırmak için sütun düzenleme menüsünü kullanın", "computedFieldEditWarning": "Hesaplanan alan: içerik salt okunurdur. Yeniden yapılandırmak için sütun düzenleme menüsünü kullanın",
"computedFieldDeleteWarning": "Hesaplanan alan: içerik salt okunurdur. İçerik temizlenemiyor.", "computedFieldDeleteWarning": "Hesaplanan alan: içerik salt okunurdur. İçerik temizlenemiyor.",
@ -698,7 +698,7 @@
"allowedSpecialCharList": "İzin verilen özel karakter listesi" "allowedSpecialCharList": "İzin verilen özel karakter listesi"
}, },
"invalidURL": "Geçersiz URL", "invalidURL": "Geçersiz URL",
"invalidEmail": "Invalid Email", "invalidEmail": "Geçersiz E-posta",
"internalError": "Bazı dahili hatalar oluştu", "internalError": "Bazı dahili hatalar oluştu",
"templateGeneratorNotFound": "Şablon Oluşturucu bulunamıyor!", "templateGeneratorNotFound": "Şablon Oluşturucu bulunamıyor!",
"fileUploadFailed": "Dosya yüklenemedi", "fileUploadFailed": "Dosya yüklenemedi",
@ -726,7 +726,7 @@
"nameShouldStartWithAnAlphabetOr_": "İsim bir alfabe veya _ ile başlamalıdır", "nameShouldStartWithAnAlphabetOr_": "İsim bir alfabe veya _ ile başlamalıdır",
"followingCharactersAreNotAllowed": "Aşağıdaki karakterlere izin verilmez", "followingCharactersAreNotAllowed": "Aşağıdaki karakterlere izin verilmez",
"columnNameRequired": "Sütun adı gereklidir", "columnNameRequired": "Sütun adı gereklidir",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters", "columnNameExceedsCharacters": "Sütun adının uzunluğu maksimum {value} karakter sınırını aşıyor",
"projectNameExceeds50Characters": "Proje adı 50 karakteri aşıyor", "projectNameExceeds50Characters": "Proje adı 50 karakteri aşıyor",
"projectNameCannotStartWithSpace": "Proje adı boşlukla başlayamaz", "projectNameCannotStartWithSpace": "Proje adı boşlukla başlayamaz",
"requiredField": "Zorunlu alan", "requiredField": "Zorunlu alan",
@ -759,7 +759,7 @@
}, },
"success": { "success": {
"columnDuplicated": "Sütun başarıyla çoğaltıldı", "columnDuplicated": "Sütun başarıyla çoğaltıldı",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)", "rowDuplicatedWithoutSavedYet": "Satır çoğaltıldı (kaydedilmedi)",
"updatedUIACL": "Tablolar için UI ACL başarıyla güncellendi", "updatedUIACL": "Tablolar için UI ACL başarıyla güncellendi",
"pluginUninstalled": "Eklenti başarıyla kaldırıldı", "pluginUninstalled": "Eklenti başarıyla kaldırıldı",
"pluginSettingsSaved": "Eklenti ayarları başarıyla kaydedildi", "pluginSettingsSaved": "Eklenti ayarları başarıyla kaydedildi",
@ -779,7 +779,7 @@
"userDeletedFromProject": "Kullanıcı projeden başarıyla silindi", "userDeletedFromProject": "Kullanıcı projeden başarıyla silindi",
"inviteEmailSent": "Davet E-postası başarıyla gönderildi", "inviteEmailSent": "Davet E-postası başarıyla gönderildi",
"inviteURLCopied": "Panoya kopyalanan davet URL'si", "inviteURLCopied": "Panoya kopyalanan davet URL'si",
"commentCopied": "Comment copied to clipboard", "commentCopied": "Yorum panoya kopyalandı",
"passwordResetURLCopied": "Panoya kopyalanan parola sıfırlama URL'si", "passwordResetURLCopied": "Panoya kopyalanan parola sıfırlama URL'si",
"shareableURLCopied": "Paylaşılabilir temel URL panoya kopyalandı!", "shareableURLCopied": "Paylaşılabilir temel URL panoya kopyalandı!",
"embeddableHTMLCodeCopied": "Yerleştirilebilir HTML kodu kopyalandı!", "embeddableHTMLCodeCopied": "Yerleştirilebilir HTML kodu kopyalandı!",

2
packages/nc-gui/package-lock.json generated

@ -110,7 +110,7 @@
} }
}, },
"../nocodb-sdk": { "../nocodb-sdk": {
"version": "0.108.0-beta.0", "version": "0.108.1",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",

9
packages/nc-gui/pages/index/index/index.vue

@ -308,6 +308,7 @@ const copyProjectMeta = async () => {
<div v-if="record.status !== ProjectStatus.JOB" class="flex items-center gap-2"> <div v-if="record.status !== ProjectStatus.JOB" class="flex items-center gap-2">
<component <component
:is="iconMap.edit" :is="iconMap.edit"
v-if="isUIAllowed('projectUpdate', true)"
v-e="['c:project:edit:rename']" v-e="['c:project:edit:rename']"
class="nc-action-btn" class="nc-action-btn"
@click.stop="navigateTo(`/${text}`)" @click.stop="navigateTo(`/${text}`)"
@ -315,12 +316,18 @@ const copyProjectMeta = async () => {
<component <component
:is="iconMap.delete" :is="iconMap.delete"
v-if="isUIAllowed('projectDelete', true)"
class="nc-action-btn" class="nc-action-btn"
:data-testid="`delete-project-${record.title}`" :data-testid="`delete-project-${record.title}`"
@click.stop="deleteProject(record)" @click.stop="deleteProject(record)"
/> />
<a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-import-menu" @click.stop> <a-dropdown
v-if="isUIAllowed('duplicateProject', true)"
:trigger="['click']"
overlay-class-name="nc-dropdown-import-menu"
@click.stop
>
<GeneralIcon <GeneralIcon
icon="threeDotVertical" icon="threeDotVertical"
class="nc-import-menu outline-0" class="nc-import-menu outline-0"

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

@ -1,6 +1,6 @@
{ {
"name": "nc-lib-gui", "name": "nc-lib-gui",
"version": "0.108.0-beta.0", "version": "0.108.1",
"description": "NocoDB GUI", "description": "NocoDB GUI",
"author": { "author": {
"name": "NocoDB", "name": "NocoDB",

4
packages/nocodb-sdk/package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "nocodb-sdk", "name": "nocodb-sdk",
"version": "0.108.0-beta.0", "version": "0.108.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "nocodb-sdk", "name": "nocodb-sdk",
"version": "0.108.0-beta.0", "version": "0.108.1",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",

2
packages/nocodb-sdk/package.json

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

74
packages/nocodb-sdk/src/lib/Api.ts

@ -546,7 +546,39 @@ export interface FilterType {
| 'notchecked' | 'notchecked'
| 'notempty' | 'notempty'
| 'notnull' | 'notnull'
| 'null'; | 'null'
| null
| (
| 'allof'
| 'anyof'
| 'blank'
| 'btw'
| 'checked'
| 'empty'
| 'eq'
| 'ge'
| 'gt'
| 'gte'
| 'in'
| 'is'
| 'isWithin'
| 'isnot'
| 'le'
| 'like'
| 'lt'
| 'lte'
| 'nallof'
| 'nanyof'
| 'nbtw'
| 'neq'
| 'nlike'
| 'not'
| 'notblank'
| 'notchecked'
| 'notempty'
| 'notnull'
| ('null' & null)
);
/** Comparison Sub-Operator */ /** Comparison Sub-Operator */
comparison_sub_op?: comparison_sub_op?:
| 'daysAgo' | 'daysAgo'
@ -589,7 +621,7 @@ export interface FilterType {
| ('yesterday' & null) | ('yesterday' & null)
); );
/** Foreign Key to Column */ /** Foreign Key to Column */
fk_column_id?: IdType; fk_column_id?: StringOrNullType;
/** Foreign Key to Hook */ /** Foreign Key to Hook */
fk_hook_id?: StringOrNullType; fk_hook_id?: StringOrNullType;
/** Foreign Key to Model */ /** Foreign Key to Model */
@ -664,7 +696,39 @@ export interface FilterReqType {
| 'notchecked' | 'notchecked'
| 'notempty' | 'notempty'
| 'notnull' | 'notnull'
| 'null'; | 'null'
| null
| (
| 'allof'
| 'anyof'
| 'blank'
| 'btw'
| 'checked'
| 'empty'
| 'eq'
| 'ge'
| 'gt'
| 'gte'
| 'in'
| 'is'
| 'isWithin'
| 'isnot'
| 'le'
| 'like'
| 'lt'
| 'lte'
| 'nallof'
| 'nanyof'
| 'nbtw'
| 'neq'
| 'nlike'
| 'not'
| 'notblank'
| 'notchecked'
| 'notempty'
| 'notnull'
| ('null' & null)
);
/** Comparison Sub-Operator */ /** Comparison Sub-Operator */
comparison_sub_op?: comparison_sub_op?:
| 'daysAgo' | 'daysAgo'
@ -707,7 +771,7 @@ export interface FilterReqType {
| ('yesterday' & null) | ('yesterday' & null)
); );
/** Foreign Key to Column */ /** Foreign Key to Column */
fk_column_id?: IdType; fk_column_id?: StringOrNullType;
/** Belong to which filter ID */ /** Belong to which filter ID */
fk_parent_id?: StringOrNullType; fk_parent_id?: StringOrNullType;
/** Is this filter grouped? */ /** Is this filter grouped? */
@ -2255,6 +2319,8 @@ export interface UserType {
* @example org-level-viewer * @example org-level-viewer
*/ */
roles?: string; roles?: string;
/** Access token version */
token_version?: string;
} }
/** /**

10
packages/nocodb/.eslintrc.js

@ -63,12 +63,20 @@ module.exports = {
], ],
}, },
], ],
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
'@typescript-eslint/no-this-alias': 'off', '@typescript-eslint/no-this-alias': 'off',
// todo: enable // todo: enable
'@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-var-requires': 'off',
'no-useless-catch': 'off', 'no-useless-catch': 'off',
'no-empty': 'off', 'no-empty': 'off',

20
packages/nocodb/package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "nocodb", "name": "nocodb",
"version": "0.108.0-beta.0", "version": "0.108.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "nocodb", "name": "nocodb",
"version": "0.108.0-beta.0", "version": "0.108.1",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@google-cloud/storage": "^5.7.2", "@google-cloud/storage": "^5.7.2",
@ -80,7 +80,7 @@
"mysql2": "^3.2.0", "mysql2": "^3.2.0",
"nanoid": "^3.1.20", "nanoid": "^3.1.20",
"nc-help": "^0.2.87", "nc-help": "^0.2.87",
"nc-lib-gui": "0.108.0-beta.0", "nc-lib-gui": "0.108.1",
"nc-plugin": "^0.1.3", "nc-plugin": "^0.1.3",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk", "nocodb-sdk": "file:../nocodb-sdk",
@ -190,7 +190,7 @@
} }
}, },
"../nocodb-sdk": { "../nocodb-sdk": {
"version": "0.108.0-beta.0", "version": "0.108.1",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
@ -13157,9 +13157,9 @@
} }
}, },
"node_modules/nc-lib-gui": { "node_modules/nc-lib-gui": {
"version": "0.108.0-beta.0", "version": "0.108.1",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.108.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.108.1.tgz",
"integrity": "sha512-y6WWH11Jj8Di8bK7bupLhPrZAHJ69ut5vV7i26fG0346J7A6SLrCaUcc/SGcJwJoeSwEwER3gzq2RXuWkGt1vA==", "integrity": "sha512-GYhDzkcqOCQ/pqWpxU8hI/4KcuuQrqkhG9zXmwUx1pQlz9qD70X8bQtjqb5rdvJw6diGdYneMl+wRI48bLiUAg==",
"dependencies": { "dependencies": {
"express": "^4.17.1" "express": "^4.17.1"
} }
@ -28442,9 +28442,9 @@
} }
}, },
"nc-lib-gui": { "nc-lib-gui": {
"version": "0.108.0-beta.0", "version": "0.108.1",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.108.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.108.1.tgz",
"integrity": "sha512-y6WWH11Jj8Di8bK7bupLhPrZAHJ69ut5vV7i26fG0346J7A6SLrCaUcc/SGcJwJoeSwEwER3gzq2RXuWkGt1vA==", "integrity": "sha512-GYhDzkcqOCQ/pqWpxU8hI/4KcuuQrqkhG9zXmwUx1pQlz9qD70X8bQtjqb5rdvJw6diGdYneMl+wRI48bLiUAg==",
"requires": { "requires": {
"express": "^4.17.1" "express": "^4.17.1"
} }

4
packages/nocodb/package.json

@ -1,6 +1,6 @@
{ {
"name": "nocodb", "name": "nocodb",
"version": "0.108.0-beta.0", "version": "0.108.1",
"description": "NocoDB Backend", "description": "NocoDB Backend",
"main": "dist/bundle.js", "main": "dist/bundle.js",
"author": { "author": {
@ -113,7 +113,7 @@
"mysql2": "^3.2.0", "mysql2": "^3.2.0",
"nanoid": "^3.1.20", "nanoid": "^3.1.20",
"nc-help": "^0.2.87", "nc-help": "^0.2.87",
"nc-lib-gui": "0.108.0-beta.0", "nc-lib-gui": "0.108.1",
"nc-plugin": "^0.1.3", "nc-plugin": "^0.1.3",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk", "nocodb-sdk": "file:../nocodb-sdk",

8
packages/nocodb/src/Noco.ts

@ -30,13 +30,7 @@ export default class Noco {
private static logger = new Logger(Noco.name); private static logger = new Logger(Noco.name);
public static get dashboardUrl(): string { public static get dashboardUrl(): string {
let siteUrl = `http://localhost:${process.env.PORT || 8080}`; const siteUrl = `http://localhost:${process.env.PORT || 8080}`;
// if (Noco._this?.config?.envs?.[Noco._this?.env]?.publicUrl) {
// siteUrl = Noco._this?.config?.envs?.[Noco._this?.env]?.publicUrl;
// }
if (Noco._this?.config?.envs?.['_noco']?.publicUrl) {
siteUrl = Noco._this?.config?.envs?.['_noco']?.publicUrl;
}
return `${siteUrl}${Noco._this?.config?.dashboardPath}`; return `${siteUrl}${Noco._this?.config?.dashboardPath}`;
} }

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

@ -45,7 +45,7 @@ export default class RedisCacheMgr extends CacheMgr {
} }
// @ts-ignore // @ts-ignore
async get(key: string, type: string, config?: any): Promise<any> { async get(key: string, type: string): Promise<any> {
log(`RedisCacheMgr::get: getting key ${key} with type ${type}`); log(`RedisCacheMgr::get: getting key ${key} with type ${type}`);
if (type === CacheGetType.TYPE_ARRAY) { if (type === CacheGetType.TYPE_ARRAY) {
return this.client.smembers(key); return this.client.smembers(key);
@ -135,7 +135,7 @@ export default class RedisCacheMgr extends CacheMgr {
// e.g. arr = ["nc:<orgs>:<scope>:<model_id_1>", "nc:<orgs>:<scope>:<model_id_2>"] // e.g. arr = ["nc:<orgs>:<scope>:<model_id_1>", "nc:<orgs>:<scope>:<model_id_2>"]
const arr = (await this.get(key, CacheGetType.TYPE_ARRAY)) || []; const arr = (await this.get(key, CacheGetType.TYPE_ARRAY)) || [];
log(`RedisCacheMgr::getList: getting list with key ${key}`); log(`RedisCacheMgr::getList: getting list with key ${key}`);
const isNoneList = arr.length && arr[0] === 'NONE'; const isNoneList = arr.length && arr.includes('NONE');
if (isNoneList) { if (isNoneList) {
return Promise.resolve({ return Promise.resolve({
@ -248,7 +248,7 @@ export default class RedisCacheMgr extends CacheMgr {
: `${this.prefix}:${scope}:${subListKeys.join(':')}:list`; : `${this.prefix}:${scope}:${subListKeys.join(':')}:list`;
log(`RedisCacheMgr::appendToList: append key ${key} to ${listKey}`); log(`RedisCacheMgr::appendToList: append key ${key} to ${listKey}`);
let list = (await this.get(listKey, CacheGetType.TYPE_ARRAY)) || []; let list = (await this.get(listKey, CacheGetType.TYPE_ARRAY)) || [];
if (list.length && list[0] === 'NONE') { if (list.length && list.includes('NONE')) {
list = []; list = [];
await this.del(listKey); await this.del(listKey);
} }

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

@ -40,7 +40,7 @@ export default class RedisMockCacheMgr extends CacheMgr {
} }
// @ts-ignore // @ts-ignore
async get(key: string, type: string, config?: any): Promise<any> { async get(key: string, type: string): Promise<any> {
log(`RedisMockCacheMgr::get: getting key ${key} with type ${type}`); log(`RedisMockCacheMgr::get: getting key ${key} with type ${type}`);
if (type === CacheGetType.TYPE_ARRAY) { if (type === CacheGetType.TYPE_ARRAY) {
return this.client.smembers(key); return this.client.smembers(key);

1
packages/nocodb/src/controllers/api-tokens.controller.ts

@ -9,7 +9,6 @@ import {
Request, Request,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse'; import { PagedResponseImpl } from '../helpers/PagedResponse';
import { import {

1
packages/nocodb/src/controllers/attachments.controller.ts

@ -30,7 +30,6 @@ export class AttachmentsController {
@UploadedFiles() files: Array<any>, @UploadedFiles() files: Array<any>,
@Body() body: any, @Body() body: any,
@Request() req: any, @Request() req: any,
@Query('path') path: string,
) { ) {
const attachments = await this.attachmentsService.upload({ const attachments = await this.attachmentsService.upload({
files: files, files: files,

1
packages/nocodb/src/controllers/audits.controller.ts

@ -10,7 +10,6 @@ import {
Request, Request,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse'; import { PagedResponseImpl } from '../helpers/PagedResponse';
import { import {

1
packages/nocodb/src/controllers/bases.controller.ts

@ -10,7 +10,6 @@ import {
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { BaseReqType } from 'nocodb-sdk'; import { BaseReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse'; import { PagedResponseImpl } from '../helpers/PagedResponse';
import { import {

1
packages/nocodb/src/controllers/bulk-data-alias.controller.ts

@ -10,7 +10,6 @@ import {
Response, Response,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { import {
Acl, Acl,

1
packages/nocodb/src/controllers/columns.controller.ts

@ -11,7 +11,6 @@ import {
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { ColumnReqType } from 'nocodb-sdk'; import { ColumnReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { import {
Acl, Acl,

1
packages/nocodb/src/controllers/data-alias-export.controller.ts

@ -1,6 +1,5 @@
import { Controller, Get, Request, Response, UseGuards } from '@nestjs/common'; import { Controller, Get, Request, Response, UseGuards } from '@nestjs/common';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { import {
Acl, Acl,

3
packages/nocodb/src/controllers/data-alias-nested.controller.ts

@ -8,7 +8,6 @@ import {
Request, Request,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { import {
Acl, Acl,
@ -131,7 +130,6 @@ export class DataAliasNestedController {
@Param('projectName') projectName: string, @Param('projectName') projectName: string,
@Param('tableName') tableName: string, @Param('tableName') tableName: string,
@Param('refRowId') refRowId: string, @Param('refRowId') refRowId: string,
@Param('relationType') relationType: string,
) { ) {
await this.dataAliasNestedService.relationDataRemove({ await this.dataAliasNestedService.relationDataRemove({
columnName: columnName, columnName: columnName,
@ -158,7 +156,6 @@ export class DataAliasNestedController {
@Param('projectName') projectName: string, @Param('projectName') projectName: string,
@Param('tableName') tableName: string, @Param('tableName') tableName: string,
@Param('refRowId') refRowId: string, @Param('refRowId') refRowId: string,
@Param('relationType') relationType: string,
) { ) {
await this.dataAliasNestedService.relationDataAdd({ await this.dataAliasNestedService.relationDataAdd({
columnName: columnName, columnName: columnName,

1
packages/nocodb/src/controllers/data-alias.controller.ts

@ -11,7 +11,6 @@ import {
Response, Response,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { parseHrtimeToSeconds } from '../helpers'; import { parseHrtimeToSeconds } from '../helpers';
import { import {

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

@ -9,14 +9,12 @@ import {
Post, Post,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { FilterReqType } from 'nocodb-sdk'; import { FilterReqType } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse'; import { PagedResponseImpl } from '../helpers/PagedResponse';
import { import {
Acl, Acl,
ExtractProjectIdMiddleware, ExtractProjectIdMiddleware,
UseAclMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware'; } from '../middlewares/extract-project-id/extract-project-id.middleware';
import { FiltersService } from '../services/filters.service'; import { FiltersService } from '../services/filters.service';

1
packages/nocodb/src/controllers/form-columns.controller.ts

@ -1,5 +1,4 @@
import { Body, Controller, Param, Patch, UseGuards } from '@nestjs/common'; import { Body, Controller, Param, Patch, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { import {
Acl, Acl,

1
packages/nocodb/src/controllers/galleries.controller.ts

@ -9,7 +9,6 @@ import {
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { GalleryUpdateReqType, ViewCreateReqType } from 'nocodb-sdk'; import { GalleryUpdateReqType, ViewCreateReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { import {
Acl, Acl,

1
packages/nocodb/src/controllers/grid-columns.controller.ts

@ -1,6 +1,5 @@
import { Body, Controller, Get, Param, Patch, UseGuards } from '@nestjs/common'; import { Body, Controller, Get, Param, Patch, UseGuards } from '@nestjs/common';
import { GridColumnReqType } from 'nocodb-sdk'; import { GridColumnReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { import {
Acl, Acl,

1
packages/nocodb/src/controllers/grids.controller.ts

@ -8,7 +8,6 @@ import {
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { ViewCreateReqType } from 'nocodb-sdk'; import { ViewCreateReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { import {
Acl, Acl,

1
packages/nocodb/src/controllers/maps.controller.ts

@ -9,7 +9,6 @@ import {
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { MapUpdateReqType, ViewCreateReqType } from 'nocodb-sdk'; import { MapUpdateReqType, ViewCreateReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { import {
Acl, Acl,

1
packages/nocodb/src/controllers/meta-diffs.controller.ts

@ -6,7 +6,6 @@ import {
Post, Post,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { import {
Acl, Acl,

1
packages/nocodb/src/controllers/model-visibilities.controller.ts

@ -8,7 +8,6 @@ import {
Query, Query,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { import {
Acl, Acl,

1
packages/nocodb/src/controllers/old-datas/old-datas.controller.ts

@ -11,7 +11,6 @@ import {
Response, Response,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../../guards/global/global.guard'; import { GlobalGuard } from '../../guards/global/global.guard';
import { import {
Acl, Acl,

1
packages/nocodb/src/controllers/org-users.controller.ts

@ -10,7 +10,6 @@ import {
Request, Request,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { OrgUserRoles } from 'nocodb-sdk'; import { OrgUserRoles } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse'; import { PagedResponseImpl } from '../helpers/PagedResponse';

10
packages/nocodb/src/controllers/plugins.controller.ts

@ -14,11 +14,11 @@ import { Acl } from '../middlewares/extract-project-id/extract-project-id.middle
import { PluginsService } from '../services/plugins.service'; import { PluginsService } from '../services/plugins.service';
// todo: move to a interceptor // todo: move to a interceptor
const blockInCloudMw = (_req, res, next) => { // const blockInCloudMw = (_req, res, next) => {
if (process.env.NC_CLOUD === 'true') { // if (process.env.NC_CLOUD === 'true') {
res.status(403).send('Not allowed'); // res.status(403).send('Not allowed');
} else next(); // } else next();
}; // };
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(GlobalGuard)

1
packages/nocodb/src/controllers/project-users.controller.ts

@ -11,7 +11,6 @@ import {
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { ProjectUserReqType } from 'nocodb-sdk'; import { ProjectUserReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { import {
Acl, Acl,

77
packages/nocodb/src/controllers/projects.controller.ts

@ -11,15 +11,13 @@ import {
Request, Request,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import isDocker from 'is-docker'; import isDocker from 'is-docker';
import { ProjectReqType } from 'nocodb-sdk'; import { ProjectReqType } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse'; import { PagedResponseImpl } from '../helpers/PagedResponse';
import { import {
Acl,
ExtractProjectIdMiddleware, ExtractProjectIdMiddleware,
UseAclMiddleware,
UseProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware'; } from '../middlewares/extract-project-id/extract-project-id.middleware';
import Noco from '../Noco'; import Noco from '../Noco';
import { packageVersion } from '../utils/packageVersion'; import { packageVersion } from '../utils/packageVersion';
@ -31,9 +29,7 @@ import type { ProjectType } from 'nocodb-sdk';
export class ProjectsController { export class ProjectsController {
constructor(private readonly projectsService: ProjectsService) {} constructor(private readonly projectsService: ProjectsService) {}
@UseAclMiddleware({ @Acl('projectList')
permissionName: 'projectList',
})
@Get('/api/v1/db/meta/projects/') @Get('/api/v1/db/meta/projects/')
async list(@Query() queryParams: Record<string, any>, @Request() req) { async list(@Query() queryParams: Record<string, any>, @Request() req) {
const projects = await this.projectsService.projectList({ const projects = await this.projectsService.projectList({
@ -57,7 +53,7 @@ export class ProjectsController {
PackageVersion: packageVersion, PackageVersion: packageVersion,
}; };
} }
@Acl('projectGet')
@Get('/api/v1/db/meta/projects/:projectId') @Get('/api/v1/db/meta/projects/:projectId')
async projectGet(@Param('projectId') projectId: string) { async projectGet(@Param('projectId') projectId: string) {
const project = await this.projectsService.getProjectWithInfo({ const project = await this.projectsService.getProjectWithInfo({
@ -68,7 +64,7 @@ export class ProjectsController {
return project; return project;
} }
@Acl('projectUpdate')
@Patch('/api/v1/db/meta/projects/:projectId') @Patch('/api/v1/db/meta/projects/:projectId')
async projectUpdate( async projectUpdate(
@Param('projectId') projectId: string, @Param('projectId') projectId: string,
@ -82,6 +78,7 @@ export class ProjectsController {
return project; return project;
} }
@Acl('projectDelete')
@Delete('/api/v1/db/meta/projects/:projectId') @Delete('/api/v1/db/meta/projects/:projectId')
async projectDelete(@Param('projectId') projectId: string) { async projectDelete(@Param('projectId') projectId: string) {
const deleted = await this.projectsService.projectSoftDelete({ const deleted = await this.projectsService.projectSoftDelete({
@ -91,6 +88,7 @@ export class ProjectsController {
return deleted; return deleted;
} }
@Acl('projectCreate')
@Post('/api/v1/db/meta/projects') @Post('/api/v1/db/meta/projects')
@HttpCode(200) @HttpCode(200)
async projectCreate(@Body() projectBody: ProjectReqType, @Request() req) { async projectCreate(@Body() projectBody: ProjectReqType, @Request() req) {
@ -102,66 +100,3 @@ export class ProjectsController {
return project; return project;
} }
} }
/*
// // Project CRUD
export async function projectCost(req, res) {
let cost = 0;
const project = await Project.getWithInfo(req.params.projectId);
for (const base of project.bases) {
const sqlClient = await NcConnectionMgrv2.getSqlClient(base);
const userCount = await ProjectUser.getUsersCount(req.query);
const recordCount = (await sqlClient.totalRecords())?.data.TotalRecords;
if (recordCount > 100000) {
// 36,000 or $79/user/month
cost = Math.max(36000, 948 * userCount);
} else if (recordCount > 50000) {
// $36,000 or $50/user/month
cost = Math.max(36000, 600 * userCount);
} else if (recordCount > 10000) {
// $240/user/yr
cost = Math.min(240 * userCount, 36000);
} else if (recordCount > 1000) {
// $120/user/yr
cost = Math.min(120 * userCount, 36000);
}
}
T.event({
event: 'a:project:cost',
data: {
cost,
},
});
res.json({ cost });
}
export async function hasEmptyOrNullFilters(req, res) {
res.json(await Filter.hasEmptyOrNullFilters(req.params.projectId));
}
export default (router) => {
router.get(
'/api/v1/db/meta/projects/:projectId/cost',
metaApiMetrics,
ncMetaAclMw(projectCost, 'projectCost')
);
router.get(
'/api/v1/db/meta/projects/:projectId/has-empty-or-null-filters',
metaApiMetrics,
ncMetaAclMw(hasEmptyOrNullFilters, 'hasEmptyOrNullFilters')
);
};
* */

1
packages/nocodb/src/controllers/shared-bases.controller.ts

@ -10,7 +10,6 @@ import {
Request, Request,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { import {
Acl, Acl,

1
packages/nocodb/src/controllers/sorts.controller.ts

@ -9,7 +9,6 @@ import {
Post, Post,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { SortReqType } from 'nocodb-sdk'; import { SortReqType } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse'; import { PagedResponseImpl } from '../helpers/PagedResponse';

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

@ -11,7 +11,6 @@ import {
Request, Request,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { TableReqType } from 'nocodb-sdk'; import { TableReqType } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import extractRolesObj from '../utils/extractRolesObj'; import extractRolesObj from '../utils/extractRolesObj';

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

@ -1,5 +1,3 @@
import { promisify } from 'util';
import { AuditOperationSubTypes, AuditOperationTypes } from 'nocodb-sdk';
import { import {
Body, Body,
Controller, Controller,
@ -19,23 +17,17 @@ import {
Acl, Acl,
ExtractProjectIdMiddleware, ExtractProjectIdMiddleware,
} from '../../middlewares/extract-project-id/extract-project-id.middleware'; } from '../../middlewares/extract-project-id/extract-project-id.middleware';
import Noco from '../../Noco'; import { User } from '../../models';
import { GoogleStrategy } from '../../strategies/google.strategy/google.strategy';
import extractRolesObj from '../../utils/extractRolesObj';
import { Audit, User } from '../../models';
import { import {
genJwt,
randomTokenString, randomTokenString,
setTokenCookie, setTokenCookie,
} from '../../services/users/helpers'; } from '../../services/users/helpers';
import { UsersService } from '../../services/users/users.service'; import { UsersService } from '../../services/users/users.service';
import extractRolesObj from '../../utils/extractRolesObj';
@Controller() @Controller()
export class UsersController { export class UsersController {
constructor( constructor(private readonly usersService: UsersService) {}
private readonly usersService: UsersService,
private googleStrategy: GoogleStrategy,
) {}
@Post([ @Post([
'/auth/user/signup', '/auth/user/signup',
@ -59,56 +51,14 @@ export class UsersController {
'/api/v1/auth/token/refresh', '/api/v1/auth/token/refresh',
]) ])
@HttpCode(200) @HttpCode(200)
async refreshToken(@Request() req: any, @Request() res: any): Promise<any> { async refreshToken(@Request() req: any, @Response() res: any): Promise<any> {
return await this.usersService.refreshToken({ res.json(
await this.usersService.refreshToken({
body: req.body, body: req.body,
req, req,
res, res,
}); }),
} );
async successfulSignIn({ user, err, info, req, res, auditDescription }) {
try {
if (!user || !user.email) {
if (err) {
return res.status(400).send(err);
}
if (info) {
return res.status(400).send(info);
}
return res.status(400).send({ msg: 'Your signin has failed' });
}
await promisify((req as any).login.bind(req))(user);
const refreshToken = randomTokenString();
if (!user.token_version) {
user.token_version = randomTokenString();
}
await User.update(user.id, {
refresh_token: refreshToken,
email: user.email,
token_version: user.token_version,
});
setTokenCookie(res, refreshToken);
await Audit.insert({
op_type: AuditOperationTypes.AUTHENTICATION,
op_sub_type: AuditOperationSubTypes.SIGNIN,
user: user.email,
ip: req.clientIp,
description: auditDescription,
});
res.json({
token: genJwt(user, Noco.getConfig()),
} as any);
} catch (e) {
console.log(e);
throw e;
}
} }
@Post([ @Post([
@ -118,8 +68,9 @@ export class UsersController {
]) ])
@UseGuards(AuthGuard('local')) @UseGuards(AuthGuard('local'))
@HttpCode(200) @HttpCode(200)
async signin(@Request() req) { async signin(@Request() req, @Response() res) {
return this.usersService.login(req.user); await this.setRefreshToken({ req, res });
res.json(this.usersService.login(req.user));
} }
@Post('/api/v1/auth/user/signout') @Post('/api/v1/auth/user/signout')
@ -136,18 +87,15 @@ export class UsersController {
@Post(`/auth/google/genTokenByCode`) @Post(`/auth/google/genTokenByCode`)
@HttpCode(200) @HttpCode(200)
@UseGuards(AuthGuard('google')) @UseGuards(AuthGuard('google'))
async googleSignin(@Request() req) { async googleSignin(@Request() req, @Response() res) {
return this.usersService.login(req.user); await this.setRefreshToken({ req, res });
res.json(this.usersService.login(req.user));
} }
@Get('/auth/google') @Get('/auth/google')
@UseGuards(AuthGuard('google')) @UseGuards(AuthGuard('google'))
googleAuthenticate(@Request() req) { googleAuthenticate() {
// this.googleStrategy.authenticate(req, { // google strategy will take care the request
// scope: ['profile', 'email'],
// state: req.query.state,
// callbackURL: req.ncSiteUrl + Noco.getConfig().dashboardPath,
// });
} }
@Get(['/auth/user/me', '/api/v1/db/auth/user/me', '/api/v1/auth/user/me']) @Get(['/auth/user/me', '/api/v1/db/auth/user/me', '/api/v1/auth/user/me'])
@ -168,7 +116,7 @@ export class UsersController {
@UseGuards(GlobalGuard) @UseGuards(GlobalGuard)
@Acl('passwordChange') @Acl('passwordChange')
@HttpCode(200) @HttpCode(200)
async passwordChange(@Request() req: any, @Body() body: any): Promise<any> { async passwordChange(@Request() req: any): Promise<any> {
if (!(req as any).isAuthenticated()) { if (!(req as any).isAuthenticated()) {
NcError.forbidden('Not allowed'); NcError.forbidden('Not allowed');
} }
@ -188,7 +136,7 @@ export class UsersController {
'/api/v1/auth/password/forgot', '/api/v1/auth/password/forgot',
]) ])
@HttpCode(200) @HttpCode(200)
async passwordForgot(@Request() req: any, @Body() body: any): Promise<any> { async passwordForgot(@Request() req: any): Promise<any> {
await this.usersService.passwordForgot({ await this.usersService.passwordForgot({
siteUrl: (req as any).ncSiteUrl, siteUrl: (req as any).ncSiteUrl,
body: req.body, body: req.body,
@ -269,4 +217,27 @@ export class UsersController {
return res.status(400).json({ msg: e.message }); return res.status(400).json({ msg: e.message });
} }
} }
async setRefreshToken({ res, req }) {
const userId = req.user?.id;
if (!userId) return;
const user = await User.get(userId);
if (!user) return;
const refreshToken = randomTokenString();
if (!user.token_version) {
user.token_version = randomTokenString();
}
await User.update(user.id, {
refresh_token: refreshToken,
email: user.email,
token_version: user.token_version,
});
setTokenCookie(res, refreshToken);
}
} }

3
packages/nocodb/src/controllers/view-columns.controller.ts

@ -8,8 +8,7 @@ import {
Post, Post,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { ColumnReqType, ViewColumnReqType } from 'nocodb-sdk'; import { ViewColumnReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard'; import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse'; import { PagedResponseImpl } from '../helpers/PagedResponse';
import { import {

6
packages/nocodb/src/db/BaseModelSql.ts

@ -1613,7 +1613,7 @@ class BaseModelSql extends BaseModel {
} }
} }
async nestedRead(id, { where, fields: fields1, f, ...rest }, trx = null) { async nestedRead(id, { fields: fields1, f, ...rest }, trx = null) {
rest = Object.assign({}, this.defaultNestedQueryParams, rest); rest = Object.assign({}, this.defaultNestedQueryParams, rest);
const { hm: childs = '', bt: parents = '', mm: many = '' } = rest; const { hm: childs = '', bt: parents = '', mm: many = '' } = rest;
@ -1852,7 +1852,7 @@ class BaseModelSql extends BaseModel {
return null; return null;
} }
// @ts-ignore // @ts-ignore
const { tn, cn, vtn, vcn, vrcn, rtn, rcn } = const { vtn, vcn, vrcn, rtn, rcn } =
this.manyToManyRelations.find(({ vtn }) => assoc === vtn) || {}; this.manyToManyRelations.find(({ vtn }) => assoc === vtn) || {};
const childModel = this.dbModels[rtn]; const childModel = this.dbModels[rtn];
@ -1894,7 +1894,7 @@ class BaseModelSql extends BaseModel {
return null; return null;
} }
// @ts-ignore // @ts-ignore
const { tn, cn, vtn, vcn, vrcn, rtn, rcn } = const { vtn, vcn, vrcn, rtn, rcn } =
this.manyToManyRelations.find(({ vtn }) => assoc === vtn) || {}; this.manyToManyRelations.find(({ vtn }) => assoc === vtn) || {};
const childModel = this.dbModels[rtn]; const childModel = this.dbModels[rtn];

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

@ -19,16 +19,7 @@ import DOMPurify from 'isomorphic-dompurify';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { NcError } from '../helpers/catchError'; import { NcError } from '../helpers/catchError';
import getAst from '../helpers/getAst'; import getAst from '../helpers/getAst';
import { import { Audit, Column, Filter, Model, Project, Sort, View } from '../models';
Audit,
Base,
Column,
Filter,
Model,
Project,
Sort,
View,
} from '../models';
import { sanitize, unsanitize } from '../helpers/sqlSanitize'; import { sanitize, unsanitize } from '../helpers/sqlSanitize';
import { import {
COMPARISON_OPS, COMPARISON_OPS,
@ -57,7 +48,7 @@ import type {
SelectOption, SelectOption,
} from '../models'; } from '../models';
import type { Knex } from 'knex'; import type { Knex } from 'knex';
import type { BoolType, SortType } from 'nocodb-sdk'; import type { SortType } from 'nocodb-sdk';
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
@ -1361,12 +1352,6 @@ class BaseModelSqlv2 {
const columns = await this.model.getColumns(); const columns = await this.model.getColumns();
for (const column of columns) { for (const column of columns) {
switch (column.uidt) { switch (column.uidt) {
case UITypes.Rollup:
{
// @ts-ignore
const colOptions: RollupColumn = await column.getColOptions();
}
break;
case UITypes.Lookup: case UITypes.Lookup:
{ {
// @ts-ignore // @ts-ignore

2
packages/nocodb/src/db/CustomKnex.ts

@ -1,6 +1,6 @@
import { Knex, knex } from 'knex'; import { Knex, knex } from 'knex';
import { SnowflakeClient } from 'nc-help'; import { SnowflakeClient } from 'nc-help';
import pg, { types } from 'pg'; import { types } from 'pg';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import Filter from '../models/Filter'; import Filter from '../models/Filter';
import type { FilterType } from 'nocodb-sdk'; import type { FilterType } from 'nocodb-sdk';

2
packages/nocodb/src/db/conditionV2.ts

@ -1,5 +1,5 @@
import { isNumericCol, RelationTypes, UITypes } from 'nocodb-sdk'; import { isNumericCol, RelationTypes, UITypes } from 'nocodb-sdk';
import dayjs, { extend } from 'dayjs'; import dayjs from 'dayjs';
// import customParseFormat from 'dayjs/plugin/customParseFormat.js'; // import customParseFormat from 'dayjs/plugin/customParseFormat.js';
import Filter from '../models/Filter'; import Filter from '../models/Filter';
import { sanitize } from '../helpers/sqlSanitize'; import { sanitize } from '../helpers/sqlSanitize';

2
packages/nocodb/src/db/sql-client/lib/SqlClientFactory.ts

@ -1,7 +1,5 @@
import fs from 'fs'; import fs from 'fs';
import { promisify } from 'util'; import { promisify } from 'util';
import Noco from '../../../Noco';
import SqlClientFactoryEE from './ee/SqlClientFactoryEE';
import MySqlClient from './mysql/MysqlClient'; import MySqlClient from './mysql/MysqlClient';
import MssqlClient from './mssql/MssqlClient'; import MssqlClient from './mssql/MssqlClient';
import OracleClient from './oracle/OracleClient'; import OracleClient from './oracle/OracleClient';

1
packages/nocodb/src/interceptors/is-upload-allowed/is-upload-allowed.interceptor.ts

@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { OrgUserRoles, ProjectRoles } from 'nocodb-sdk'; import { OrgUserRoles, ProjectRoles } from 'nocodb-sdk';
import { throwError } from 'rxjs';
import { NcError } from '../../helpers/catchError'; import { NcError } from '../../helpers/catchError';
import Noco from '../../Noco'; import Noco from '../../Noco';
import extractRolesObj from '../../utils/extractRolesObj'; import extractRolesObj from '../../utils/extractRolesObj';

9
packages/nocodb/src/middlewares/extract-project-id/extract-project-id.middleware.ts

@ -1,10 +1,7 @@
import { promisify } from 'util';
import { Injectable, SetMetadata, UseInterceptors } from '@nestjs/common'; import { Injectable, SetMetadata, UseInterceptors } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { NextFunction, Request, Response } from 'express';
import { OrgUserRoles } from 'nocodb-sdk'; import { OrgUserRoles } from 'nocodb-sdk';
import passport from 'passport'; import { map } from 'rxjs';
import { map, throwError } from 'rxjs';
import { import {
Column, Column,
Filter, Filter,
@ -19,8 +16,7 @@ import {
} from '../../models'; } from '../../models';
import extractRolesObj from '../../utils/extractRolesObj'; import extractRolesObj from '../../utils/extractRolesObj';
import projectAcl from '../../utils/projectAcl'; import projectAcl from '../../utils/projectAcl';
import catchError, { NcError } from '../catchError'; import { NcError } from '../catchError';
import extractProjectIdAndAuthenticate from '../extractProjectIdAndAuthenticate';
import type { Observable } from 'rxjs'; import type { Observable } from 'rxjs';
import type { import type {
CallHandler, CallHandler,
@ -216,7 +212,6 @@ export class AclMiddleware implements NestInterceptor {
); );
const req = context.switchToHttp().getRequest(); const req = context.switchToHttp().getRequest();
const res = context.switchToHttp().getResponse();
req.customProperty = 'This is a custom property'; req.customProperty = 'This is a custom property';
const roles: Record<string, boolean> = extractRolesObj(req.user?.roles); const roles: Record<string, boolean> = extractRolesObj(req.user?.roles);

2
packages/nocodb/src/models/Model.ts

@ -16,7 +16,6 @@ import { sanitize } from '../helpers/sqlSanitize';
import { extractProps } from '../helpers/extractProps'; import { extractProps } from '../helpers/extractProps';
import Audit from './Audit'; import Audit from './Audit';
import View from './View'; import View from './View';
import Base from './Base';
import Column from './Column'; import Column from './Column';
import type { BoolType, TableReqType, TableType } from 'nocodb-sdk'; import type { BoolType, TableReqType, TableType } from 'nocodb-sdk';
import type { XKnex } from '../db/CustomKnex'; import type { XKnex } from '../db/CustomKnex';
@ -471,7 +470,6 @@ export default class Model implements TableType {
knex, knex,
) { ) {
const insertObj = {}; const insertObj = {};
const base = await Base.get(this.base_id);
for (const col of await this.getColumns()) { for (const col of await this.getColumns()) {
if (isVirtualCol(col)) continue; if (isVirtualCol(col)) continue;
let val = let val =

10
packages/nocodb/src/models/ProjectUser.ts

@ -174,11 +174,6 @@ export default class ProjectUser {
} }
static async delete(projectId: string, userId: string, ncMeta = Noco.ncMeta) { static async delete(projectId: string, userId: string, ncMeta = Noco.ncMeta) {
// await NocoCache.deepDel(
// CacheScope.PROJECT_USER,
// `${CacheScope.PROJECT_USER}:${projectId}:${userId}`,
// CacheDelDirection.CHILD_TO_PARENT
// );
const { email } = await ncMeta.metaGet2(null, null, MetaTable.USERS, { const { email } = await ncMeta.metaGet2(null, null, MetaTable.USERS, {
id: userId, id: userId,
}); });
@ -194,12 +189,17 @@ export default class ProjectUser {
const { isNoneList } = cachedList; const { isNoneList } = cachedList;
if (!isNoneList && cachedProjectList?.length) { if (!isNoneList && cachedProjectList?.length) {
cachedProjectList = cachedProjectList.filter((p) => p.id !== projectId); cachedProjectList = cachedProjectList.filter((p) => p.id !== projectId);
// delete the whole list first so that the old one won't be included
await NocoCache.del(`${CacheScope.USER_PROJECT}:${userId}:list`);
if (cachedProjectList.length > 0) {
// set the updated list (i.e. excluding the to-be-deleted project id)
await NocoCache.setList( await NocoCache.setList(
CacheScope.USER_PROJECT, CacheScope.USER_PROJECT,
[userId], [userId],
cachedProjectList, cachedProjectList,
); );
} }
}
await NocoCache.del(`${CacheScope.PROJECT_USER}:${projectId}:${userId}`); await NocoCache.del(`${CacheScope.PROJECT_USER}:${projectId}:${userId}`);
return await ncMeta.metaDelete(null, null, MetaTable.PROJECT_USERS, { return await ncMeta.metaDelete(null, null, MetaTable.PROJECT_USERS, {

1
packages/nocodb/src/modules/datas/helpers.ts

@ -12,7 +12,6 @@ import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2';
import type { BaseModelSqlv2 } from '../../db/BaseModelSqlv2'; import type { BaseModelSqlv2 } from '../../db/BaseModelSqlv2';
import type LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn'; import type LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn';
import type LookupColumn from '../../models/LookupColumn'; import type LookupColumn from '../../models/LookupColumn';
import type { Request } from 'express';
export interface PathParams { export interface PathParams {
projectName: string; projectName: string;

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

@ -21,14 +21,12 @@ async function readAllData({
fields, fields,
base, base,
logBasic = (_str) => {}, logBasic = (_str) => {},
services,
}: { }: {
table: { title?: string }; table: { title?: string };
fields?; fields?;
base: AirtableBase; base: AirtableBase;
logBasic?: (string) => void; logBasic?: (string) => void;
logDetailed?: (string) => void; logDetailed?: (string) => void;
services: AirtableImportContext;
}): Promise<EntityMap> { }): Promise<EntityMap> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let data = null; let data = null;
@ -229,7 +227,6 @@ export async function importLTARData({
base, base,
logDetailed, logDetailed,
logBasic, logBasic,
services,
})); }));
const modelMeta: any = const modelMeta: any =

5
packages/nocodb/src/modules/users/users.module.ts

@ -1,9 +1,6 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport'; import { PassportModule } from '@nestjs/passport';
import { import { GoogleStrategyProvider } from '../../strategies/google.strategy/google.strategy';
GoogleStrategy,
GoogleStrategyProvider,
} from '../../strategies/google.strategy/google.strategy';
import { GlobalModule } from '../global/global.module'; import { GlobalModule } from '../global/global.module';
import { UsersService } from '../../services/users/users.service'; import { UsersService } from '../../services/users/users.service';
import { UsersController } from '../../controllers/users/users.controller'; import { UsersController } from '../../controllers/users/users.controller';

1
packages/nocodb/src/run/docker.ts

@ -1,4 +1,3 @@
import axios from 'axios';
import cors from 'cors'; import cors from 'cors';
import express from 'express'; import express from 'express';
import Noco from '../Noco'; import Noco from '../Noco';

1
packages/nocodb/src/run/dockerRunPG_CyQuick.ts

@ -1,7 +1,6 @@
import cors from 'cors'; import cors from 'cors';
import express from 'express'; import express from 'express';
import Noco from '../Noco'; import Noco from '../Noco';
import nocobuild from '../nocobuild';
const server = express(); const server = express();
server.enable('trust proxy'); server.enable('trust proxy');

22
packages/nocodb/src/schema/swagger.json

@ -15197,6 +15197,8 @@
}, },
"comparison_op": { "comparison_op": {
"description": "Comparison Operator", "description": "Comparison Operator",
"anyOf": [
{
"enum": [ "enum": [
"allof", "allof",
"anyof", "anyof",
@ -15230,6 +15232,11 @@
], ],
"type": "string" "type": "string"
}, },
{
"type": "null"
}
]
},
"comparison_sub_op": { "comparison_sub_op": {
"anyOf": [ "anyOf": [
{ {
@ -15262,7 +15269,7 @@
"description": "Comparison Sub-Operator" "description": "Comparison Sub-Operator"
}, },
"fk_column_id": { "fk_column_id": {
"$ref": "#/components/schemas/Id", "$ref": "#/components/schemas/StringOrNull",
"description": "Foreign Key to Column" "description": "Foreign Key to Column"
}, },
"fk_hook_id": { "fk_hook_id": {
@ -15518,6 +15525,8 @@
"properties": { "properties": {
"comparison_op": { "comparison_op": {
"description": "Comparison Operator", "description": "Comparison Operator",
"anyOf": [
{
"enum": [ "enum": [
"allof", "allof",
"anyof", "anyof",
@ -15551,6 +15560,11 @@
], ],
"type": "string" "type": "string"
}, },
{
"type": "null"
}
]
},
"comparison_sub_op": { "comparison_sub_op": {
"anyOf": [ "anyOf": [
{ {
@ -15583,7 +15597,7 @@
"description": "Comparison Sub-Operator" "description": "Comparison Sub-Operator"
}, },
"fk_column_id": { "fk_column_id": {
"$ref": "#/components/schemas/Id", "$ref": "#/components/schemas/StringOrNull",
"description": "Foreign Key to Column" "description": "Foreign Key to Column"
}, },
"fk_parent_id": { "fk_parent_id": {
@ -20000,6 +20014,10 @@
"description": "The roles of the user", "description": "The roles of the user",
"example": "org-level-viewer", "example": "org-level-viewer",
"type": "string" "type": "string"
},
"token_version": {
"description": "Access token version",
"type": "string"
} }
}, },
"required": ["email", "email_verified", "firstname", "id", "lastname"], "required": ["email", "email_verified", "firstname", "id", "lastname"],

5
packages/nocodb/src/services/bulk-data-alias.service.ts

@ -1,8 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { isSystemColumn, UITypes } from 'nocodb-sdk'; import { Base, Model } from '../models';
import * as XLSX from 'xlsx';
import { NcError } from '../helpers/catchError';
import { Base, Column, Model, Project, View } from '../models';
import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2';
import { getViewAndModelByAliasOrId } from '../modules/datas/helpers'; import { getViewAndModelByAliasOrId } from '../modules/datas/helpers';
import type { PathParams } from '../modules/datas/helpers'; import type { PathParams } from '../modules/datas/helpers';

84
packages/nocodb/src/services/public-metas.service.ts

@ -2,8 +2,7 @@ import { Injectable } from '@nestjs/common';
import { ErrorMessages, RelationTypes, UITypes } from 'nocodb-sdk'; import { ErrorMessages, RelationTypes, UITypes } from 'nocodb-sdk';
import { NcError } from '../helpers/catchError'; import { NcError } from '../helpers/catchError';
import { Base, Column, Model, Project, View } from '../models'; import { Base, Column, Model, Project, View } from '../models';
import type { LinkToAnotherRecordColumn } from '../models'; import type { LinkToAnotherRecordColumn, LookupColumn } from '../models';
import type { LinkToAnotherRecordType } from 'nocodb-sdk';
@Injectable() @Injectable()
export class PublicMetasService { export class PublicMetasService {
@ -63,23 +62,88 @@ export class PublicMetasService {
// load related table metas // load related table metas
for (const col of view.model.columns) { for (const col of view.model.columns) {
await this.extractRelatedMetas({ col, relatedMetas });
}
view.relatedMetas = relatedMetas;
return view;
}
private async extractRelatedMetas({
col,
relatedMetas = {},
}: {
col: Column<any>;
relatedMetas: Record<string, Model>;
}) {
if (UITypes.LinkToAnotherRecord === col.uidt) { if (UITypes.LinkToAnotherRecord === col.uidt) {
const colOpt = await col.getColOptions<LinkToAnotherRecordType>(); await this.extractLTARRelatedMetas({
relatedMetas[colOpt.fk_related_model_id] = await Model.getWithInfo({ ltarColOption: await col.getColOptions<LinkToAnotherRecordColumn>(),
id: colOpt.fk_related_model_id, relatedMetas,
}); });
if (colOpt.type === 'mm') { } else if (UITypes.Lookup === col.uidt) {
relatedMetas[colOpt.fk_mm_model_id] = await Model.getWithInfo({ await this.extractLookupRelatedMetas({
id: colOpt.fk_mm_model_id, lookupColOption: await col.getColOptions<LookupColumn>(),
relatedMetas,
}); });
} }
} }
private async extractLTARRelatedMetas({
ltarColOption,
relatedMetas = {},
}: {
ltarColOption: LinkToAnotherRecordColumn;
relatedMetas: { [key: string]: Model };
}) {
relatedMetas[ltarColOption.fk_related_model_id] = await Model.getWithInfo({
id: ltarColOption.fk_related_model_id,
});
if (ltarColOption.type === 'mm') {
relatedMetas[ltarColOption.fk_mm_model_id] = await Model.getWithInfo({
id: ltarColOption.fk_mm_model_id,
});
}
} }
view.relatedMetas = relatedMetas; private async extractLookupRelatedMetas({
lookupColOption,
relatedMetas = {},
}: {
lookupColOption: LookupColumn;
relatedMetas: { [key: string]: Model };
}) {
const relationCol = await Column.get({
colId: lookupColOption.fk_relation_column_id,
});
const lookedUpCol = await Column.get({
colId: lookupColOption.fk_lookup_column_id,
});
return view; // extract meta for table which belongs the relation column
// if not already extracted
if (!relatedMetas[relationCol.fk_model_id]) {
relatedMetas[relationCol.fk_model_id] = await Model.getWithInfo({
id: relationCol.fk_model_id,
});
} }
// extract meta for table in which looked up column belongs
// if not already extracted
if (!relatedMetas[lookedUpCol.fk_model_id]) {
relatedMetas[lookedUpCol.fk_model_id] = await Model.getWithInfo({
id: lookedUpCol.fk_model_id,
});
}
// extract metas related to the looked up column
await this.extractRelatedMetas({
col: lookedUpCol,
relatedMetas,
});
}
async publicSharedBaseGet(param: { sharedBaseUuid: string }): Promise<any> { async publicSharedBaseGet(param: { sharedBaseUuid: string }): Promise<any> {
const project = await Project.getByUuid(param.sharedBaseUuid); const project = await Project.getByUuid(param.sharedBaseUuid);

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

@ -486,9 +486,9 @@ export class UsersService {
return this.login(user); return this.login(user);
} }
async login(user: any) { login(user: UserType) {
return { return {
token: genJwt(user, Noco.getConfig()), //this.jwtService.sign(payload), token: genJwt(user, Noco.getConfig()),
}; };
} }

9
packages/nocodb/src/utils/nc-config/NcConfig.ts

@ -40,9 +40,6 @@ export class NcConfig {
publicUrl?: string; publicUrl?: string;
dashboardPath?: string; dashboardPath?: string;
// TODO what is this?
envs: any;
queriesFolder: string; queriesFolder: string;
env: string; env: string;
workingEnv: string; workingEnv: string;
@ -81,8 +78,7 @@ export class NcConfig {
ncConfig.env = '_noco'; ncConfig.env = '_noco';
ncConfig.workingEnv = '_noco'; ncConfig.workingEnv = '_noco';
ncConfig.projectType = ncConfig.projectType = 'rest';
ncConfig?.envs?.[ncConfig.workingEnv]?.db?.[0]?.meta?.api?.type || 'rest';
if (ncConfig.meta?.db?.connection?.filename) { if (ncConfig.meta?.db?.connection?.filename) {
ncConfig.meta.db.connection.filename = path.join( ncConfig.meta.db.connection.filename = path.join(
@ -120,12 +116,13 @@ export class NcConfig {
} }
if (publicUrl) { if (publicUrl) {
ncConfig.envs['_noco'].publicUrl = publicUrl;
ncConfig.publicUrl = publicUrl; ncConfig.publicUrl = publicUrl;
} }
if (dashboardPath) { if (dashboardPath) {
ncConfig.dashboardPath = dashboardPath; ncConfig.dashboardPath = dashboardPath;
} else {
ncConfig.dashboardPath = '/dashboard';
} }
try { try {

1
packages/nocodb/src/version-upgrader/v1-legacy/gql/GqlApiBuilder.ts

@ -1,4 +1,3 @@
import debug from 'debug';
import { Router } from 'express'; import { Router } from 'express';
import GqlXcSchemaFactory from '../../../db/sql-mgr/code/gql-schema/xc-ts/GqlXcSchemaFactory'; import GqlXcSchemaFactory from '../../../db/sql-mgr/code/gql-schema/xc-ts/GqlXcSchemaFactory';
import BaseApiBuilder from '../BaseApiBuilder'; import BaseApiBuilder from '../BaseApiBuilder';

5
packages/nocodb/src/version-upgrader/v1-legacy/rest/RestApiBuilder.ts

@ -1,17 +1,14 @@
import debug from 'debug';
import autoBind from 'auto-bind'; import autoBind from 'auto-bind';
import SwaggerXc from '../../../db/sql-mgr/code/routers/xc-ts/SwaggerXc'; import SwaggerXc from '../../../db/sql-mgr/code/routers/xc-ts/SwaggerXc';
import ExpressXcTsRoutes from '../../../db/sql-mgr/code/routes/xc-ts/ExpressXcTsRoutes'; import ExpressXcTsRoutes from '../../../db/sql-mgr/code/routes/xc-ts/ExpressXcTsRoutes';
import NcHelp from '../../../utils/NcHelp'; import NcHelp from '../../../utils/NcHelp';
import BaseApiBuilder, { XcTablesPopulateParams } from '../BaseApiBuilder'; import BaseApiBuilder from '../BaseApiBuilder';
import type { MetaService } from '../../../meta/meta.service'; import type { MetaService } from '../../../meta/meta.service';
import type Noco from '../../../Noco'; import type Noco from '../../../Noco';
import type { Router } from 'express'; import type { Router } from 'express';
import type { DbConfig, NcConfig } from '../../../interface/config'; import type { DbConfig, NcConfig } from '../../../interface/config';
import type NcProjectBuilder from '../NcProjectBuilder'; import type NcProjectBuilder from '../NcProjectBuilder';
const log = debug('nc:api:rest');
export class RestApiBuilder extends BaseApiBuilder<Noco> { export class RestApiBuilder extends BaseApiBuilder<Noco> {
public readonly type = 'rest'; public readonly type = 'rest';

79
tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts

@ -37,6 +37,75 @@ export class ToolbarFilterPage extends BasePage {
await this.get().locator(`button:has-text("Add Filter")`).first().click(); await this.get().locator(`button:has-text("Add Filter")`).first().click();
} }
// can reuse code for addFilterGroup and addFilter
// support for subOperation & datatype specific filter operations not supported yet
async addFilterGroup({
title,
operation,
_subOperation: _subOperation,
value,
_locallySaved: _locallySaved = false,
_dataType: _dataType,
_openModal: _openModal = false,
_skipWaitingResponse: _skipWaitingResponse = false, // used for undo (single request, less stable)
filterGroupIndex = 0,
filterLogicalOperator = 'AND',
}: {
title: string;
operation: string;
_subOperation?: string; // for date datatype
value?: string;
_locallySaved?: boolean;
_dataType?: string;
_openModal?: boolean;
_skipWaitingResponse?: boolean;
filterGroupIndex?: number;
filterLogicalOperator?: string;
}) {
await this.get().locator(`button:has-text("Add Filter Group")`).last().click();
const filterDropdown = await this.get().locator('.menu-filter-dropdown').nth(filterGroupIndex);
await filterDropdown.waitFor({ state: 'visible' });
await filterDropdown.locator(`button:has-text("Add Filter")`).first().click();
const selectField = await filterDropdown.locator('.nc-filter-field-select').last();
const selectOperation = await filterDropdown.locator('.nc-filter-operation-select').last();
const selectValue = await filterDropdown.locator('.nc-filter-value-select > input').last();
await selectField.waitFor({ state: 'visible' });
await selectField.click();
const fieldDropdown = await this.rootPage
.locator('div.ant-select-dropdown.nc-dropdown-toolbar-field-list')
.last()
.locator(`div[label="${title}"]:visible`);
await fieldDropdown.waitFor({ state: 'visible' });
await fieldDropdown.click();
await selectOperation.waitFor({ state: 'visible' });
await selectOperation.click();
const operationDropdown = await this.rootPage
.locator('div.ant-select-dropdown.nc-dropdown-filter-comp-op')
.last()
.locator(`.ant-select-item:has-text("${operation}")`);
await operationDropdown.waitFor({ state: 'visible' });
await operationDropdown.click();
await selectValue.waitFor({ state: 'visible' });
await selectValue.fill(value);
if (filterGroupIndex) {
if (filterLogicalOperator === 'OR') {
const logicalButton = await this.rootPage.locator('div.flex.bob').nth(filterGroupIndex - 1);
await logicalButton.waitFor({ state: 'visible' });
await logicalButton.click();
const logicalDropdown = await this.rootPage.locator(
'div.ant-select-dropdown.nc-dropdown-filter-logical-op-group'
);
await logicalDropdown.waitFor({ state: 'visible' });
await logicalDropdown.locator(`.ant-select-item:has-text("${filterLogicalOperator}")`).click();
}
}
}
async add({ async add({
title, title,
operation, operation,
@ -65,7 +134,7 @@ export class ToolbarFilterPage extends BasePage {
await this.rootPage.locator('.nc-filter-field-select').last().click(); await this.rootPage.locator('.nc-filter-field-select').last().click();
if (skipWaitingResponse) { if (skipWaitingResponse) {
this.rootPage await this.rootPage
.locator('div.ant-select-dropdown.nc-dropdown-toolbar-field-list') .locator('div.ant-select-dropdown.nc-dropdown-toolbar-field-list')
.locator(`div[label="${title}"]:visible`) .locator(`div[label="${title}"]:visible`)
.click(); .click();
@ -88,7 +157,7 @@ export class ToolbarFilterPage extends BasePage {
// first() : filter list has >, >= // first() : filter list has >, >=
if (skipWaitingResponse) { if (skipWaitingResponse) {
this.rootPage await this.rootPage
.locator('.nc-dropdown-filter-comp-op') .locator('.nc-dropdown-filter-comp-op')
.locator(`.ant-select-item:has-text("${operation}")`) .locator(`.ant-select-item:has-text("${operation}")`)
.first() .first()
@ -117,7 +186,7 @@ export class ToolbarFilterPage extends BasePage {
// first() : filter list has >, >= // first() : filter list has >, >=
if (skipWaitingResponse) { if (skipWaitingResponse) {
this.rootPage await this.rootPage
.locator('.nc-dropdown-filter-comp-sub-op') .locator('.nc-dropdown-filter-comp-sub-op')
.locator(`.ant-select-item:has-text("${subOperation}")`) .locator(`.ant-select-item:has-text("${subOperation}")`)
.first() .first()
@ -167,7 +236,7 @@ export class ToolbarFilterPage extends BasePage {
await this.rootPage.locator(`.ant-picker-dropdown:visible`); await this.rootPage.locator(`.ant-picker-dropdown:visible`);
if (skipWaitingResponse) { if (skipWaitingResponse) {
this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click(); await this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click();
} else { } else {
await this.waitForResponse({ await this.waitForResponse({
uiAction: () => this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click(), uiAction: () => this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click(),
@ -188,7 +257,7 @@ export class ToolbarFilterPage extends BasePage {
break; break;
case UITypes.Duration: case UITypes.Duration:
if (skipWaitingResponse) { if (skipWaitingResponse) {
this.get().locator('.nc-filter-value-select').locator('input').fill(value); await this.get().locator('.nc-filter-value-select').locator('input').fill(value);
} else { } else {
await this.waitForResponse({ await this.waitForResponse({
uiAction: () => this.get().locator('.nc-filter-value-select').locator('input').fill(value), uiAction: () => this.get().locator('.nc-filter-value-select').locator('input').fill(value),

49
tests/playwright/tests/db/filters.spec.ts

@ -1339,3 +1339,52 @@ test.describe('Filter Tests: Toggle button', () => {
await dashboard.settings.toggleNullEmptyFilters(); await dashboard.settings.toggleNullEmptyFilters();
}); });
}); });
test.describe('Filter Tests: Filter groups', () => {
/**
* Steps
*
* 1. Open table
* 2. Verify filter options : should not include NULL & EMPTY options
* 3. Enable `Show NULL & EMPTY in Filter` in Project Settings
* 4. Verify filter options : should include NULL & EMPTY options
* 5. Add NULL & EMPTY filters
* 6. Disable `Show NULL & EMPTY in Filter` in Project Settings : should not be allowed
* 7. Remove the NULL & EMPTY filters
* 8. Disable `Show NULL & EMPTY in Filter` in Project Settings again : should be allowed
*
*/
test.beforeEach(async ({ page }) => {
context = await setup({ page, isEmptyProject: false });
dashboard = new DashboardPage(page, context.project);
toolbar = dashboard.grid.toolbar;
});
test('Filter: Empty filters', async () => {
await dashboard.closeTab({ title: 'Team & Auth' });
await dashboard.treeView.openTable({ title: 'Country', networkResponse: false });
await toolbar.clickFilter({ networkValidation: false });
await toolbar.filter.addFilterGroup({
title: 'Country',
operation: 'is equal',
value: 'Argentina',
});
await toolbar.clickFilter({ networkValidation: false });
await toolbar.clickFilter({ networkValidation: false });
await toolbar.filter.addFilterGroup({
title: 'Country',
operation: 'is equal',
value: 'Indonesia',
filterGroupIndex: 1,
filterLogicalOperator: 'OR',
});
await toolbar.clickFilter({ networkValidation: false });
await validateRowArray({
rowCount: 2,
});
});
});

34
tests/playwright/tests/db/timezone.spec.ts

@ -147,9 +147,16 @@ test.describe('Timezone-XCDB : Japan/Tokyo', () => {
await dashboard.treeView.openTable({ title: 'dateTimeTable' }); await dashboard.treeView.openTable({ title: 'dateTimeTable' });
// DateTime inserted using API without timezone is converted to UTC // DateTime inserted using API without timezone is converted to db-timezone (server timezone in case of sqlite)
// Display value is converted to Asia/Tokyo // Display value is converted to Asia/Tokyo
await dashboard.grid.cell.verifyDateCell({ index: 0, columnHeader: 'DateTime', value: '2021-01-01 09:00' }); const dateInserted = new Date(`2021-01-01 00:00:00${getBrowserTimezoneOffset()}`);
// convert dateInserted to Japan/Tokyo timezone in YYYY-MM-DD HH:mm format
const dateInsertedInJapan = new Date(dateInserted.getTime() + 9 * 60 * 60 * 1000)
.toISOString()
.slice(0, 16)
.replace('T', ' ');
await dashboard.grid.cell.verifyDateCell({ index: 0, columnHeader: 'DateTime', value: dateInsertedInJapan });
// DateTime inserted using API with timezone is converted to UTC // DateTime inserted using API with timezone is converted to UTC
// Display value is converted to Asia/Tokyo // Display value is converted to Asia/Tokyo
@ -173,8 +180,16 @@ test.describe('Timezone-XCDB : Japan/Tokyo', () => {
test('API Insert, verify API read response', async () => { test('API Insert, verify API read response', async () => {
if (!isSqlite(context)) return; if (!isSqlite(context)) return;
const dateInserted = new Date(`2021-01-01 00:00:00${getBrowserTimezoneOffset()}`);
// translate dateInserted to UTC in YYYY-MM-DD HH:mm format
const dateInsertedInUTC = dateInserted.toISOString().replace('T', ' ').replace('Z', '');
// UTC expected response // UTC expected response
const dateUTC = ['2021-01-01 00:00:00+00:00', '2021-01-01 00:00:00+00:00', '2021-01-01 00:00:00+00:00']; const dateUTC = [
`${dateInsertedInUTC.slice(0, 19)}+00:00`,
'2021-01-01 00:00:00+00:00',
'2021-01-01 00:00:00+00:00',
];
const readDate = records.list.map(record => record.DateTime); const readDate = records.list.map(record => record.DateTime);
@ -231,7 +246,18 @@ test.describe('Timezone-XCDB : Asia/Hong-kong', () => {
// DateTime inserted using API without timezone is converted to UTC // DateTime inserted using API without timezone is converted to UTC
// Display value is converted to Asia/Hong_Kong // Display value is converted to Asia/Hong_Kong
await dashboard.grid.cell.verifyDateCell({ index: 0, columnHeader: 'DateTime', value: '2021-01-01 08:00' }); const dateInserted = new Date(`2021-01-01 00:00:00${getBrowserTimezoneOffset()}`);
// convert dateInserted to Asia/Hong-kong timezone using offset
const dateInsertedInHK = new Date(dateInserted.getTime() + 8 * 60 * 60 * 1000)
.toISOString()
.slice(0, 16)
.replace('T', ' ');
await dashboard.grid.cell.verifyDateCell({
index: 0,
columnHeader: 'DateTime',
value: dateInsertedInHK,
});
// DateTime inserted using API with timezone is converted to UTC // DateTime inserted using API with timezone is converted to UTC
// Display value is converted to Asia/Hong_Kong // Display value is converted to Asia/Hong_Kong

Loading…
Cancel
Save