Browse Source

Merge pull request #5500 from nocodb/develop

pull/5501/head 0.106.1
github-actions[bot] 1 year ago committed by GitHub
parent
commit
e817ee89ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      packages/nc-gui/components/cell/Checkbox.vue
  2. 2
      packages/nc-gui/components/cell/attachment/Image.vue
  3. 2
      packages/nc-gui/components/dlg/TableCreate.vue
  4. 2
      packages/nc-gui/components/smartsheet/Cell.vue
  5. 17
      packages/nc-gui/components/smartsheet/DivDataCell.vue
  6. 26
      packages/nc-gui/components/smartsheet/Form.vue
  7. 2
      packages/nc-gui/components/smartsheet/TableDataCell.vue
  8. 4
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  9. 16
      packages/nc-gui/components/template/Editor.vue
  10. 3
      packages/nc-gui/components/template/utils.ts
  11. 1
      packages/nc-gui/composables/useAttachment.ts
  12. 10
      packages/nc-gui/composables/useGlobal/actions.ts
  13. 2
      packages/nc-gui/lang/pt_BR.json
  14. 18
      packages/nc-gui/lang/zh-Hans.json
  15. 52
      packages/nc-gui/package-lock.json
  16. 2
      packages/nc-gui/package.json
  17. 4
      packages/nc-gui/pages/[projectType]/form/[viewId]/index/index.vue
  18. 4
      packages/nc-gui/pages/[projectType]/form/[viewId]/index/survey.vue
  19. 2
      packages/noco-docs/content/en/developer-resources/rest-apis.md
  20. 4
      packages/noco-docs/content/en/developer-resources/sdk.md
  21. 2
      packages/noco-docs/content/en/getting-started/upgrading.md
  22. 6
      packages/noco-docs/content/en/index.md
  23. 2
      packages/noco-docs/content/en/setup-and-usages/meta-management.md
  24. 6
      packages/noco-docs/content/en/setup-and-usages/table-operations.md
  25. 4
      packages/nocodb-sdk/package-lock.json
  26. 4
      packages/nocodb-sdk/src/lib/Api.ts
  27. 14
      packages/nocodb/docker-compose.yml
  28. 42
      packages/nocodb/package-lock.json
  29. 4
      packages/nocodb/package.json
  30. 8
      packages/nocodb/src/lib/cache/CacheMgr.ts
  31. 11
      packages/nocodb/src/lib/cache/NocoCache.ts
  32. 37
      packages/nocodb/src/lib/cache/RedisCacheMgr.ts
  33. 46
      packages/nocodb/src/lib/cache/RedisMockCacheMgr.ts
  34. 5
      packages/nocodb/src/lib/controllers/publicControllers/publicData.ctl.ts
  35. 10
      packages/nocodb/src/lib/controllers/user/user.ctl.ts
  36. 25
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
  37. 40
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts
  38. 16
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/pg.ts
  39. 23
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/convertDateFormat.ts
  40. 45
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/formulaFnHelper.ts
  41. 6
      packages/nocodb/src/lib/models/Base.ts
  42. 82
      packages/nocodb/src/lib/models/Column.ts
  43. 38
      packages/nocodb/src/lib/models/Filter.ts
  44. 18
      packages/nocodb/src/lib/models/FormViewColumn.ts
  45. 18
      packages/nocodb/src/lib/models/GalleryViewColumn.ts
  46. 19
      packages/nocodb/src/lib/models/GridViewColumn.ts
  47. 8
      packages/nocodb/src/lib/models/Hook.ts
  48. 18
      packages/nocodb/src/lib/models/HookFilter.ts
  49. 6
      packages/nocodb/src/lib/models/KanbanViewColumn.ts
  50. 16
      packages/nocodb/src/lib/models/MapViewColumn.ts
  51. 27
      packages/nocodb/src/lib/models/Model.ts
  52. 11
      packages/nocodb/src/lib/models/ModelRoleVisibility.ts
  53. 6
      packages/nocodb/src/lib/models/Plugin.ts
  54. 6
      packages/nocodb/src/lib/models/Project.ts
  55. 12
      packages/nocodb/src/lib/models/ProjectUser.ts
  56. 6
      packages/nocodb/src/lib/models/SelectOption.ts
  57. 6
      packages/nocodb/src/lib/models/Sort.ts
  58. 24
      packages/nocodb/src/lib/models/View.ts
  59. 4
      packages/nocodb/src/lib/plugins/vultr/Vultr.ts
  60. 16
      packages/nocodb/src/lib/plugins/vultr/index.ts
  61. 4
      packages/nocodb/src/lib/services/attachment.svc.ts
  62. 8
      packages/nocodb/src/lib/services/dbData/index.ts
  63. 37
      packages/nocodb/src/lib/services/public/publicData.svc.ts
  64. 15
      packages/nocodb/src/lib/services/user/index.ts
  65. 15
      packages/nocodb/src/schema/swagger.json
  66. 23
      tests/playwright/pages/Dashboard/common/Cell/index.ts
  67. 6
      tests/playwright/scripts/docker-compose-pg.yml
  68. 4
      tests/playwright/scripts/docker-compose-playwright-pg.yml
  69. 12
      tests/playwright/tests/columnFormula.spec.ts
  70. 63
      tests/playwright/tests/megaTable.spec.ts
  71. 2
      tests/playwright/tests/viewGridShare.spec.ts

11
packages/nc-gui/components/cell/Checkbox.vue

@ -7,6 +7,7 @@ import {
getMdiIcon, getMdiIcon,
inject, inject,
parseProp, parseProp,
useProject,
useSelectedCellKeyupListener, useSelectedCellKeyupListener,
} from '#imports' } from '#imports'
@ -26,10 +27,7 @@ const emits = defineEmits<Emits>()
const active = inject(ActiveCellInj, ref(false)) const active = inject(ActiveCellInj, ref(false))
let vModel = $computed<boolean>({ const { isMssql } = useProject()
get: () => !!props.modelValue && props.modelValue !== '0' && props.modelValue !== 0,
set: (val: boolean) => emits('update:modelValue', val),
})
const column = inject(ColumnInj) const column = inject(ColumnInj)
@ -48,6 +46,11 @@ const checkboxMeta = $computed(() => {
} }
}) })
let vModel = $computed<boolean | number>({
get: () => !!props.modelValue && props.modelValue !== '0' && props.modelValue !== 0,
set: (val: any) => emits('update:modelValue', isMssql(column?.value?.base_id) ? +val : val),
})
function onClick(force?: boolean, event?: MouseEvent) { function onClick(force?: boolean, event?: MouseEvent) {
if ( if (
(event?.target as HTMLElement)?.classList?.contains('nc-checkbox') || (event?.target as HTMLElement)?.classList?.contains('nc-checkbox') ||

2
packages/nc-gui/components/cell/attachment/Image.vue

@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { iconMap } from '#imports'
interface Props { interface Props {
srcs: string[] srcs: string[]
alt?: string alt?: string

2
packages/nc-gui/components/dlg/TableCreate.vue

@ -53,7 +53,7 @@ const validators = computed(() => {
validator: (_: any, value: any) => { validator: (_: any, value: any) => {
// validate duplicate alias // validate duplicate alias
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if ((tables.value || []).some((t) => t.title === (value || ''))) { if ((tables.value || []).some((t) => t.title === (value || '') && t.base_id === props.baseId)) {
return reject(new Error('Duplicate table alias')) return reject(new Error('Duplicate table alias'))
} }
return resolve(true) return resolve(true)

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

@ -193,7 +193,7 @@ onUnmounted(() => {
`nc-cell-${(column?.uidt || 'default').toLowerCase()}`, `nc-cell-${(column?.uidt || 'default').toLowerCase()}`,
{ 'text-blue-600': isPrimary(column) && !props.virtual && !isForm }, { 'text-blue-600': isPrimary(column) && !props.virtual && !isForm },
{ 'nc-grid-numeric-cell': isGrid && !isForm && isNumericField }, { 'nc-grid-numeric-cell': isGrid && !isForm && isNumericField },
{ 'h-[40px]': !props.editEnabled && isForm && !isSurveyForm }, { 'h-[40px]': !props.editEnabled && isForm && !isSurveyForm && !isAttachment(column) },
]" ]"
@keydown.enter.exact="navigate(NavigateDir.NEXT, $event)" @keydown.enter.exact="navigate(NavigateDir.NEXT, $event)"
@keydown.shift.enter.exact="navigate(NavigateDir.PREV, $event)" @keydown.shift.enter.exact="navigate(NavigateDir.PREV, $event)"

17
packages/nc-gui/components/smartsheet/DivDataCell.vue

@ -0,0 +1,17 @@
<script lang="ts" setup>
import { CellClickHookInj, CurrentCellInj, createEventHook, ref } from '#imports'
const el = ref()
const cellClickHook = createEventHook()
provide(CellClickHookInj, cellClickHook)
provide(CurrentCellInj, el)
</script>
<template>
<div ref="el" class="select-none" @click="cellClickHook.trigger($event)">
<slot />
</div>
</template>

26
packages/nc-gui/components/smartsheet/Form.vue

@ -727,18 +727,20 @@ watch(view, (nextView) => {
}, },
]" ]"
> >
<LazySmartsheetCell <LazySmartsheetDivDataCell class="relative">
v-model="formState[element.title]" <LazySmartsheetCell
class="nc-input" v-model="formState[element.title]"
:class="`nc-form-input-${element.title.replaceAll(' ', '')}`" class="nc-input"
:data-testid="`nc-form-input-${element.title.replaceAll(' ', '')}`" :class="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:column="element" :data-testid="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:edit-enabled="editEnabled[index]" :column="element"
@click="editEnabled[index] = true" :edit-enabled="editEnabled[index]"
@cancel="editEnabled[index] = false" @click="editEnabled[index] = true"
@update:edit-enabled="editEnabled[index] = $event" @cancel="editEnabled[index] = false"
@click.stop.prevent @update:edit-enabled="editEnabled[index] = $event"
/> @click.stop.prevent
/>
</LazySmartsheetDivDataCell>
</a-form-item> </a-form-item>
<div class="text-gray-500 text-xs" data-testid="nc-form-input-help-text-label">{{ element.description }}</div> <div class="text-gray-500 text-xs" data-testid="nc-form-input-help-text-label">{{ element.description }}</div>

2
packages/nc-gui/components/smartsheet/TableDataCell.vue

@ -11,7 +11,7 @@ provide(CurrentCellInj, el)
</script> </script>
<template> <template>
<td ref="el" @click="cellClickHook.trigger($event)"> <td ref="el" class="select-none" @click="cellClickHook.trigger($event)">
<slot /> <slot />
</td> </td>
</template> </template>

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

@ -329,7 +329,7 @@ export default {
<LazySmartsheetHeaderCell v-else :column="col" /> <LazySmartsheetHeaderCell v-else :column="col" />
<div <LazySmartsheetDivDataCell
:ref="i ? null : (el) => (cellWrapperEl = el)" :ref="i ? null : (el) => (cellWrapperEl = el)"
class="!bg-white rounded px-1 min-h-[35px] flex items-center mt-2 relative" class="!bg-white rounded px-1 min-h-[35px] flex items-center mt-2 relative"
> >
@ -343,7 +343,7 @@ export default {
:active="true" :active="true"
@update:model-value="changedColumns.add(col.title)" @update:model-value="changedColumns.add(col.title)"
/> />
</div> </LazySmartsheetDivDataCell>
</div> </div>
</div> </div>
</div> </div>

16
packages/nc-gui/components/template/Editor.vue

@ -3,6 +3,7 @@ import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc' import utc from 'dayjs/plugin/utc'
import type { ColumnType, TableType } from 'nocodb-sdk' import type { ColumnType, TableType } from 'nocodb-sdk'
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface'
import { srcDestMappingColumns, tableColumns } from './utils' import { srcDestMappingColumns, tableColumns } from './utils'
import { import {
Empty, Empty,
@ -87,6 +88,8 @@ const isImporting = ref(false)
const importingTips = ref<Record<string, string>>({}) const importingTips = ref<Record<string, string>>({})
const checkAllRecord = ref<boolean[]>([])
const uiTypeOptions = ref<Option[]>( const uiTypeOptions = ref<Option[]>(
(Object.keys(UITypes) as (keyof typeof UITypes)[]) (Object.keys(UITypes) as (keyof typeof UITypes)[])
.filter( .filter(
@ -615,6 +618,13 @@ function handleEditableTnChange(idx: number) {
function isSelectDisabled(uidt: string, disableSelect = false) { function isSelectDisabled(uidt: string, disableSelect = false) {
return (uidt === UITypes.SingleSelect || uidt === UITypes.MultiSelect) && disableSelect return (uidt === UITypes.SingleSelect || uidt === UITypes.MultiSelect) && disableSelect
} }
function handleCheckAllRecord(event: CheckboxChangeEvent, tableName: string) {
const isChecked = event.target.checked
for (const record of srcDestMapping.value[tableName]) {
record.enabled = isChecked
}
}
</script> </script>
<template> <template>
@ -671,6 +681,12 @@ function isSelectDisabled(uidt: string, disableSelect = false) {
<span v-if="column.key === 'source_column' || column.key === 'destination_column'"> <span v-if="column.key === 'source_column' || column.key === 'destination_column'">
{{ column.name }} {{ column.name }}
</span> </span>
<span v-if="column.key === 'action'">
<a-checkbox
v-model:checked="checkAllRecord[table.table_name]"
@change="handleCheckAllRecord($event, table.table_name)"
/>
</span>
</template> </template>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">

3
packages/nc-gui/components/template/utils.ts

@ -40,6 +40,7 @@ export const srcDestMappingColumns: (Omit<ColumnGroupType<any>, 'children'> & {
{ {
name: 'Action', name: 'Action',
key: 'action', key: 'action',
align: 'right', align: 'center',
width: 50,
}, },
] ]

1
packages/nc-gui/composables/useAttachment.ts

@ -5,6 +5,7 @@ const useAttachment = () => {
const getPossibleAttachmentSrc = (item: Record<string, any>) => { const getPossibleAttachmentSrc = (item: Record<string, any>) => {
const res: string[] = [] const res: string[] = []
if (item?.data) res.push(item.data)
if (item?.path) res.push(`${appInfo.value.ncSiteUrl}/${item.path}`) if (item?.path) res.push(`${appInfo.value.ncSiteUrl}/${item.path}`)
if (item?.url) res.push(item.url) if (item?.url) res.push(item.url)
return res return res

10
packages/nc-gui/composables/useGlobal/actions.ts

@ -46,11 +46,13 @@ export function useGlobalActions(state: State): Actions {
signIn(response.data.token) signIn(response.data.token)
} }
}) })
.catch(async (err) => { .catch(async () => {
message.error(err.message || t('msg.error.youHaveBeenSignedOut')) if (state.token.value && state.user.value) {
await signOut() await signOut()
message.error(t('msg.error.youHaveBeenSignedOut'))
}
}) })
.finally(() => resolve()) .finally(() => resolve(true))
}) })
} }

2
packages/nc-gui/lang/pt_BR.json

@ -698,7 +698,7 @@
"allowedSpecialCharList": "Lista de caracteres especiais permitidos" "allowedSpecialCharList": "Lista de caracteres especiais permitidos"
}, },
"invalidURL": "URL inválido", "invalidURL": "URL inválido",
"invalidEmail": "Invalid Email", "invalidEmail": "E-mail inválido",
"internalError": "Ocorreu algum erro interno", "internalError": "Ocorreu algum erro interno",
"templateGeneratorNotFound": "O Gerador de Modelos não pode ser encontrado!", "templateGeneratorNotFound": "O Gerador de Modelos não pode ser encontrado!",
"fileUploadFailed": "Falha no carregamento do ficheiro", "fileUploadFailed": "Falha no carregamento do ficheiro",

18
packages/nc-gui/lang/zh-Hans.json

@ -221,7 +221,7 @@
"viewName": "查看名称", "viewName": "查看名称",
"viewLink": "查看链接", "viewLink": "查看链接",
"columnName": "列名", "columnName": "列名",
"columnToScanFor": "Column to scan", "columnToScanFor": "要扫描的列",
"columnType": "列类型", "columnType": "列类型",
"roleName": "权限组", "roleName": "权限组",
"roleDescription": "权限描述", "roleDescription": "权限描述",
@ -412,8 +412,8 @@
"changePwd": "更改密码", "changePwd": "更改密码",
"createView": "创建视图", "createView": "创建视图",
"shareView": "分享视图", "shareView": "分享视图",
"findRowByCodeScan": "Find row by scan", "findRowByCodeScan": "通过扫描查找行",
"fillByCodeScan": "Fill by scan", "fillByCodeScan": "通过扫描填充",
"listSharedView": "共享视图列表", "listSharedView": "共享视图列表",
"ListView": "视图列表", "ListView": "视图列表",
"copyView": "复制视图", "copyView": "复制视图",
@ -430,7 +430,7 @@
"iFrame": "复制可嵌入的 HTML 代码", "iFrame": "复制可嵌入的 HTML 代码",
"addWebhook": "添加新的 Webhook", "addWebhook": "添加新的 Webhook",
"enableWebhook": "启用 Webhook", "enableWebhook": "启用 Webhook",
"testWebhook": "Test Webhook", "testWebhook": "测试Webhook",
"copyWebhook": "复制 Webhook", "copyWebhook": "复制 Webhook",
"deleteWebhook": "删除 Webhook", "deleteWebhook": "删除 Webhook",
"newToken": "添加新 Token", "newToken": "添加新 Token",
@ -470,7 +470,7 @@
"addOrEditStack": "添加/编辑分类标签" "addOrEditStack": "添加/编辑分类标签"
}, },
"map": { "map": {
"mappedBy": "Mapped By", "mappedBy": "映射字段",
"chooseMappingField": "选择映射字段", "chooseMappingField": "选择映射字段",
"openInGoogleMaps": "谷歌地图", "openInGoogleMaps": "谷歌地图",
"openInOpenStreetMap": "OSM" "openInOpenStreetMap": "OSM"
@ -542,15 +542,15 @@
"orgViewer": "游客不能创建新项目,仅允许访问受邀项目。" "orgViewer": "游客不能创建新项目,仅允许访问受邀项目。"
}, },
"codeScanner": { "codeScanner": {
"loadingScanner": "Loading the scanner...", "loadingScanner": "正在加载扫描仪...",
"selectColumn": "Select a column (QR code or Barcode) that you want to use for finding a row by scanning.", "selectColumn": "选择要用于扫描查找行的列(二维码或条形码)。",
"moreThanOneRowFoundForCode": "More than one row found for this code. Currently only unique codes are supported.", "moreThanOneRowFoundForCode": "找到了多行此代码。目前只支持唯一的代码。",
"noRowFoundForCode": "所选列没有找到此代码行" "noRowFoundForCode": "所选列没有找到此代码行"
}, },
"map": { "map": {
"overLimit": "你已经超出了限制。", "overLimit": "你已经超出了限制。",
"closeLimit": "您已接近上限。", "closeLimit": "您已接近上限。",
"limitNumber": "The limit of markers shown in a Map View is 1000 records." "limitNumber": "在地图视图中显示的标记的限制是1000条记录。"
}, },
"footerInfo": "每页行驶", "footerInfo": "每页行驶",
"upload": "选择文件以上传", "upload": "选择文件以上传",

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

@ -30,7 +30,7 @@
"leaflet.markercluster": "^1.5.3", "leaflet.markercluster": "^1.5.3",
"locale-codes": "^1.3.1", "locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"nocodb-sdk": "0.106.0", "nocodb-sdk": "file:../nocodb-sdk",
"papaparse": "^5.3.2", "papaparse": "^5.3.2",
"pinia": "^2.0.33", "pinia": "^2.0.33",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
@ -111,7 +111,6 @@
}, },
"../nocodb-sdk": { "../nocodb-sdk": {
"version": "0.106.0", "version": "0.106.0",
"extraneous": true,
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
@ -8776,6 +8775,7 @@
"version": "1.15.1", "version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
"devOptional": true,
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@ -12294,21 +12294,8 @@
} }
}, },
"node_modules/nocodb-sdk": { "node_modules/nocodb-sdk": {
"version": "0.106.0", "resolved": "../nocodb-sdk",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.106.0.tgz", "link": true
"integrity": "sha512-1LkBv+qX1P8PxEDvqvgfM70lqjEgD6k0VX7wWLYcllBeMSI7I276FaV+yF/al5C4vONQhs7LqrAIhaqav9EZgg==",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
}
},
"node_modules/nocodb-sdk/node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"dependencies": {
"follow-redirects": "^1.14.0"
}
}, },
"node_modules/node-abi": { "node_modules/node-abi": {
"version": "3.23.0", "version": "3.23.0",
@ -24810,7 +24797,8 @@
"follow-redirects": { "follow-redirects": {
"version": "1.15.1", "version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
"devOptional": true
}, },
"form-data": { "form-data": {
"version": "4.0.0", "version": "4.0.0",
@ -27360,22 +27348,22 @@
} }
}, },
"nocodb-sdk": { "nocodb-sdk": {
"version": "0.106.0", "version": "file:../nocodb-sdk",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.106.0.tgz",
"integrity": "sha512-1LkBv+qX1P8PxEDvqvgfM70lqjEgD6k0VX7wWLYcllBeMSI7I276FaV+yF/al5C4vONQhs7LqrAIhaqav9EZgg==",
"requires": { "requires": {
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"axios": "^0.21.1", "axios": "^0.21.1",
"jsep": "^1.3.6" "cspell": "^4.1.0",
}, "eslint": "^7.8.0",
"dependencies": { "eslint-config-prettier": "^6.11.0",
"axios": { "eslint-plugin-eslint-comments": "^3.2.0",
"version": "0.21.4", "eslint-plugin-functional": "^3.0.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", "eslint-plugin-import": "^2.22.0",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", "eslint-plugin-prettier": "^4.0.0",
"requires": { "jsep": "^1.3.6",
"follow-redirects": "^1.14.0" "npm-run-all": "^4.1.5",
} "prettier": "^2.1.1",
} "typescript": "^4.0.2"
} }
}, },
"node-abi": { "node-abi": {

2
packages/nc-gui/package.json

@ -54,7 +54,7 @@
"leaflet.markercluster": "^1.5.3", "leaflet.markercluster": "^1.5.3",
"locale-codes": "^1.3.1", "locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"nocodb-sdk": "0.106.0", "nocodb-sdk": "file:../nocodb-sdk",
"papaparse": "^5.3.2", "papaparse": "^5.3.2",
"pinia": "^2.0.33", "pinia": "^2.0.33",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",

4
packages/nc-gui/pages/[projectType]/form/[viewId]/index/index.vue

@ -146,7 +146,7 @@ const onDecode = async (scannedCodeValue: string) => {
</div> </div>
<div> <div>
<div class="flex"> <LazySmartsheetDivDataCell class="flex relative">
<LazySmartsheetVirtualCell <LazySmartsheetVirtualCell
v-if="isVirtualCol(field)" v-if="isVirtualCol(field)"
:model-value="null" :model-value="null"
@ -178,7 +178,7 @@ const onDecode = async (scannedCodeValue: string) => {
<component :is="iconMap.qrCodeScan" class="h-5 w-5" /> <component :is="iconMap.qrCodeScan" class="h-5 w-5" />
</div> </div>
</a-button> </a-button>
</div> </LazySmartsheetDivDataCell>
<div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-[0.75rem] my-2 px-1"> <div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-[0.75rem] my-2 px-1">
<div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500"> <div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500">

4
packages/nc-gui/pages/[projectType]/form/[viewId]/index/survey.vue

@ -283,7 +283,7 @@ onMounted(() => {
/> />
</div> </div>
<div v-if="field.title"> <LazySmartsheetDivDataCell v-if="field.title" class="relative">
<LazySmartsheetVirtualCell <LazySmartsheetVirtualCell
v-if="isVirtualCol(field)" v-if="isVirtualCol(field)"
v-model="formState[field.title]" v-model="formState[field.title]"
@ -323,7 +323,7 @@ onMounted(() => {
<MaterialSymbolsKeyboardReturn class="mx-1 text-primary" /> to make a line break <MaterialSymbolsKeyboardReturn class="mx-1 text-primary" /> to make a line break
</div> </div>
</div> </div>
</div> </LazySmartsheetDivDataCell>
</div> </div>
<div class="ml-1 mt-4 flex w-full text-lg"> <div class="ml-1 mt-4 flex w-full text-lg">

2
packages/noco-docs/content/en/developer-resources/rest-apis.md

@ -12,7 +12,7 @@ Once you've created the schemas, you can manipulate the data or invoke actions u
Here's the overview of all APIs. For the details, please check out <a href="https://all-apis.nocodb.com/" target="_blank">NocoDB API Documentation</a>. Here's the overview of all APIs. For the details, please check out <a href="https://all-apis.nocodb.com/" target="_blank">NocoDB API Documentation</a>.
You may also interact with the API's resources via <a href="./accessing-apis#swagger-ui" target="_blank">Swagger UI</a>. You may also interact with the API's resources via <NuxtLink to="/developer-resources/accessing-apis#swagger-ui" target="_blank">Swagger UI</NuxtLink>.
<alert type="success"> <alert type="success">
Currently, the default value for {orgs} is <b>noco</b>. Users will be able to change it in the future release. Currently, the default value for {orgs} is <b>noco</b>. Users will be able to change it in the future release.

4
packages/noco-docs/content/en/developer-resources/sdk.md

@ -9,7 +9,7 @@ menuTitle: 'NocoDB SDK'
We provide SDK for users to integrate with their applications. Currently only SDK for Javascript is supported. We provide SDK for users to integrate with their applications. Currently only SDK for Javascript is supported.
<alert> <alert>
Note: The NocoDB SDK requires authorization token. If you haven't created one, please check out <a href="./accessing-apis" target="_blank">Accessing APIs</a> for details. Note: The NocoDB SDK requires authorization token. If you haven't created one, please check out <NuxtLink to="/developer-resources/accessing-apis" target="_blank">Accessing APIs</NuxtLink> for details.
</alert> </alert>
## SDK For Javascript ## SDK For Javascript
@ -57,7 +57,7 @@ const api = new Api({
Once you have configured `api`, you can call different types of APIs by `api.<Tag>.<FunctionName>`. Once you have configured `api`, you can call different types of APIs by `api.<Tag>.<FunctionName>`.
<alert> <alert>
For Tag and FunctionName, please check out the API table <a href="./rest-apis" target="_blank">here</a>. For Tag and FunctionName, please check out the API table <NuxtLink to="/developer-resources/rest-apis" target="_blank">here</NuxtLink>.
</alert> </alert>
#### Example: Calling API - /api/v1/db/meta/projects/{projectId}/tables #### Example: Calling API - /api/v1/db/meta/projects/{projectId}/tables

2
packages/noco-docs/content/en/getting-started/upgrading.md

@ -8,7 +8,7 @@ link: https://codesandbox.io/embed/vigorous-firefly-80kq5?hidenavigation=1&theme
--- ---
By default, if `NC_DB` is not specified upon By default, if `NC_DB` is not specified upon
<a href="./installation" target="_blank">installation</a>, then SQLite will be used to store metadata. We suggest users to separate the metadata and user data in different databases as pictured in our <a href="../engineering/architecture" target="_blank">architecture</a>. <NuxtLink to="/getting-started/installation" target="_blank">installation</NuxtLink>, then SQLite will be used to store metadata. We suggest users to separate the metadata and user data in different databases as pictured in our <NuxtLink to="/engineering/architecture" target="_blank">architecture</NuxtLink>.
## Docker ## Docker

6
packages/noco-docs/content/en/index.md

@ -31,7 +31,7 @@ Also NocoDB's app store allows you to build business workflows on views with com
### App Store for Workflow Automations ### App Store for Workflow Automations
We provide different integrations in three main categories. See <a href="./setup-and-usages/app-store" target="_blank">App Store</a> for details. We provide different integrations in three main categories. See <NuxtLink to="/setup-and-usages/account-settings#app-store" target="_blank">App Store</NuxtLink> for details.
- ⚡ &nbsp;Chat : Slack, Discord, Mattermost, and etc - ⚡ &nbsp;Chat : Slack, Discord, Mattermost, and etc
- ⚡ &nbsp;Email : AWS SES, SMTP, MailerSend, and etc - ⚡ &nbsp;Email : AWS SES, SMTP, MailerSend, and etc
@ -46,11 +46,11 @@ We provide the following ways to let users to invoke actions in a programmatic w
### Sync Schema ### Sync Schema
We allow you to sync schema changes if you have made changes outside NocoDB GUI. However, it has to be noted then you will have to bring your own schema migrations for moving from environment to others. See <a href="./setup-and-usages/sync-schema" target="_blank">Sync Schema</a> for details. We allow you to sync schema changes if you have made changes outside NocoDB GUI. However, it has to be noted then you will have to bring your own schema migrations for moving from environment to others. See <NuxtLink to="/setup-and-usages/sync-schema" target="_blank">Sync Schema</NuxtLink> for details.
### Audit ### Audit
We are keeping all the user operation logs under one place. See <a href="./setup-and-usages/audit" target="_blank">Audit</a> for details. We are keeping all the user operation logs under one place. See <NuxtLink to="/setup-and-usages/audit" target="_blank">Audit</NuxtLink> for details.
## Why are we building this? ## Why are we building this?
Most internet businesses equip themselves with either spreadsheet or a database to solve their business needs. Spreadsheets are used by a Billion+ humans collaboratively every single day. However, we are way off working at similar speeds on databases which are way more powerful tools when it comes to computing. Attempts to solve this with SaaS offerings has meant horrible access controls, vendor lockin, data lockin, abrupt price changes & most importantly a glass ceiling on what's possible in future. Most internet businesses equip themselves with either spreadsheet or a database to solve their business needs. Spreadsheets are used by a Billion+ humans collaboratively every single day. However, we are way off working at similar speeds on databases which are way more powerful tools when it comes to computing. Attempts to solve this with SaaS offerings has meant horrible access controls, vendor lockin, data lockin, abrupt price changes & most importantly a glass ceiling on what's possible in future.

2
packages/noco-docs/content/en/setup-and-usages/meta-management.md

@ -28,7 +28,7 @@ To access it, click the down arrow button next to Project Name on the top left s
## Sync Metadata ## Sync Metadata
Go to `Data Sources`, click ``Sync Metadata``, you can see your metadata sync status. If it is out of sync, you can sync the schema. See <a href="./sync-schema">Sync Schema</a> for more.0 Go to `Data Sources`, click ``Sync Metadata``, you can see your metadata sync status. If it is out of sync, you can sync the schema. See <NuxtLink to="/setup-and-usages/sync-schema">Sync Schema</NuxtLink> for more.
![image](https://user-images.githubusercontent.com/35857179/219833485-3bcaa6ec-88bc-47cc-b938-5abb4835dc31.png) ![image](https://user-images.githubusercontent.com/35857179/219833485-3bcaa6ec-88bc-47cc-b938-5abb4835dc31.png)

6
packages/noco-docs/content/en/setup-and-usages/table-operations.md

@ -155,7 +155,7 @@ You can use Quick Import when you have data from external sources such as Airtab
### Import Airtable into an Existing Project ### Import Airtable into an Existing Project
- See <a href="./import-airtable-to-sql-database-within-a-minute-for-free">here</a> - See <NuxtLink to="/setup-and-usages/import-airtable-to-sql-database-within-a-minute-for-free">here</NuxtLink>
### Import CSV data into an Existing Project ### Import CSV data into an Existing Project
@ -165,7 +165,7 @@ You can use Quick Import when you have data from external sources such as Airtab
- **Use First Row as Headers**: If it is checked, the first row will be treated as header row. - **Use First Row as Headers**: If it is checked, the first row will be treated as header row.
- **Import Data**: If it is checked, all data will be imported. Otherwise, only table will be created. - **Import Data**: If it is checked, all data will be imported. Otherwise, only table will be created.
![image](https://user-images.githubusercontent.com/35857179/197454479-1ed18dce-1d0b-4ee3-88b3-9b6a132dea2a.png) ![image](https://user-images.githubusercontent.com/35857179/197454479-1ed18dce-1d0b-4ee3-88b3-9b6a132dea2a.png)
- You can revise the table name by double clicking it, column name and column type. By default, the first column will be chosen as <a href="./display-value" target="_blank">Display Value</a> and cannot be deleted. - You can revise the table name by double clicking it, column name and column type. By default, the first column will be chosen as <NuxtLink to="/setup-and-usages/display-value" target="_blank">Display Value</NuxtLink> and cannot be deleted.
![image](https://user-images.githubusercontent.com/35857179/197454633-5b30323e-2b13-4c55-843a-948c093d373e.png) ![image](https://user-images.githubusercontent.com/35857179/197454633-5b30323e-2b13-4c55-843a-948c093d373e.png)
- Click `Import` to start importing process. The table will be created and the data will be imported. - Click `Import` to start importing process. The table will be created and the data will be imported.
![image](https://user-images.githubusercontent.com/35857179/197455547-2d93df5e-a7f0-4c88-af53-990067625967.png) ![image](https://user-images.githubusercontent.com/35857179/197455547-2d93df5e-a7f0-4c88-af53-990067625967.png)
@ -178,7 +178,7 @@ You can use Quick Import when you have data from external sources such as Airtab
- **Use First Row as Headers**: If it is checked, the first row will be treated as header row. - **Use First Row as Headers**: If it is checked, the first row will be treated as header row.
- **Import Data**: If it is checked, all data will be imported. Otherwise, only table will be created. - **Import Data**: If it is checked, all data will be imported. Otherwise, only table will be created.
![image](https://user-images.githubusercontent.com/35857179/197455788-8dd8a7d1-38f3-48c3-a05e-6ab0cf25045c.png) ![image](https://user-images.githubusercontent.com/35857179/197455788-8dd8a7d1-38f3-48c3-a05e-6ab0cf25045c.png)
- You can revise the table name, column name and column type. By default, the first column will be chosen as <a href="./display-value" target="_blank">Display Value</a> and cannot be deleted. - You can revise the table name, column name and column type. By default, the first column will be chosen as <NuxtLink to="/setup-and-usages/display-value" target="_blank">Display Value</NuxtLink> and cannot be deleted.
<alert> <alert>
Note: If your Excel file contains multiple sheets, each sheet will be stored in a separate table. Note: If your Excel file contains multiple sheets, each sheet will be stored in a separate table.
</alert> </alert>

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

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

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

@ -7905,7 +7905,7 @@ export class Api<
*/ */
dataCreate: ( dataCreate: (
sharedViewUuid: string, sharedViewUuid: string,
data: object, data: any,
params: RequestParams = {} params: RequestParams = {}
) => ) =>
this.request< this.request<
@ -7918,7 +7918,7 @@ export class Api<
path: `/api/v1/db/public/shared-view/${sharedViewUuid}/rows`, path: `/api/v1/db/public/shared-view/${sharedViewUuid}/rows`,
method: 'POST', method: 'POST',
body: data, body: data,
type: ContentType.Json, type: ContentType.FormData,
format: 'json', format: 'json',
...params, ...params,
}), }),

14
packages/nocodb/docker-compose.yml

@ -1,4 +1,4 @@
version: "2.1" version: "2.2"
services: services:
# db55: # db55:
@ -96,8 +96,8 @@ services:
# - 5495:5432 # - 5495:5432
# volumes: # volumes:
# - ./pg-sakila-db:/docker-entrypoint-initdb.d # - ./pg-sakila-db:/docker-entrypoint-initdb.d
pg96: pg147:
image: postgres:9.6 image: postgres:14.7
restart: always restart: always
environment: environment:
POSTGRES_PASSWORD: password POSTGRES_PASSWORD: password
@ -337,13 +337,13 @@ services:
xc-test-pg: xc-test-pg:
image: node:12.22.1-slim image: node:12.22.1-slim
depends_on: depends_on:
pg96: pg147:
condition: service_healthy condition: service_healthy
volumes: volumes:
- ./:/home/app - ./:/home/app
environment: environment:
NODE_ENV: test NODE_ENV: test
DATABASE_URL: postgres://postgres:password@pg96:5432/postgres DATABASE_URL: postgres://postgres:password@pg147:5432/postgres
command: command:
- /bin/bash - /bin/bash
- -c - -c
@ -405,13 +405,13 @@ services:
xc-test-gql-pg: xc-test-gql-pg:
image: node:12.22.1-slim image: node:12.22.1-slim
depends_on: depends_on:
pg96: pg147:
condition: service_healthy condition: service_healthy
volumes: volumes:
- ./:/home/app - ./:/home/app
environment: environment:
NODE_ENV: test NODE_ENV: test
DATABASE_URL: postgres://postgres:password@pg96:5432/postgres DATABASE_URL: postgres://postgres:password@pg147:5432/postgres
command: command:
- /bin/bash - /bin/bash
- -c - -c

42
packages/nocodb/package-lock.json generated

@ -70,7 +70,7 @@
"nc-lib-gui": "0.106.0", "nc-lib-gui": "0.106.0",
"nc-plugin": "0.1.2", "nc-plugin": "0.1.2",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nocodb-sdk": "0.106.0", "nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10", "nodemailer": "^6.4.10",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"os-locale": "^5.0.0", "os-locale": "^5.0.0",
@ -157,7 +157,6 @@
}, },
"../nocodb-sdk": { "../nocodb-sdk": {
"version": "0.106.0", "version": "0.106.0",
"extraneous": true,
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
@ -11392,13 +11391,8 @@
"dev": true "dev": true
}, },
"node_modules/nocodb-sdk": { "node_modules/nocodb-sdk": {
"version": "0.106.0", "resolved": "../nocodb-sdk",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.106.0.tgz", "link": true
"integrity": "sha512-1LkBv+qX1P8PxEDvqvgfM70lqjEgD6k0VX7wWLYcllBeMSI7I276FaV+yF/al5C4vONQhs7LqrAIhaqav9EZgg==",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
}
}, },
"node_modules/node-abort-controller": { "node_modules/node-abort-controller": {
"version": "3.0.1", "version": "3.0.1",
@ -17370,9 +17364,9 @@
"dev": true "dev": true
}, },
"node_modules/vm2": { "node_modules/vm2": {
"version": "3.9.15", "version": "3.9.16",
"resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.15.tgz", "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.16.tgz",
"integrity": "sha512-XqNqknHGw2avJo13gbIwLNZUumvrSHc9mLqoadFZTpo3KaNEJoe1I0lqTFhRXmXD7WkLyG01aaraXdXT0pa4ag==", "integrity": "sha512-3T9LscojNTxdOyG+e8gFeyBXkMlOBYDoF6dqZbj+MPVHi9x10UfiTAJIobuchRCp3QvC+inybTbMJIUrLsig0w==",
"dependencies": { "dependencies": {
"acorn": "^8.7.0", "acorn": "^8.7.0",
"acorn-walk": "^8.2.0" "acorn-walk": "^8.2.0"
@ -28097,12 +28091,22 @@
"dev": true "dev": true
}, },
"nocodb-sdk": { "nocodb-sdk": {
"version": "0.106.0", "version": "file:../nocodb-sdk",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.106.0.tgz",
"integrity": "sha512-1LkBv+qX1P8PxEDvqvgfM70lqjEgD6k0VX7wWLYcllBeMSI7I276FaV+yF/al5C4vONQhs7LqrAIhaqav9EZgg==",
"requires": { "requires": {
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"axios": "^0.21.1", "axios": "^0.21.1",
"jsep": "^1.3.6" "cspell": "^4.1.0",
"eslint": "^7.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^4.0.0",
"jsep": "^1.3.6",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.1",
"typescript": "^4.0.2"
} }
}, },
"node-abort-controller": { "node-abort-controller": {
@ -32782,9 +32786,9 @@
"dev": true "dev": true
}, },
"vm2": { "vm2": {
"version": "3.9.15", "version": "3.9.16",
"resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.15.tgz", "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.16.tgz",
"integrity": "sha512-XqNqknHGw2avJo13gbIwLNZUumvrSHc9mLqoadFZTpo3KaNEJoe1I0lqTFhRXmXD7WkLyG01aaraXdXT0pa4ag==", "integrity": "sha512-3T9LscojNTxdOyG+e8gFeyBXkMlOBYDoF6dqZbj+MPVHi9x10UfiTAJIobuchRCp3QvC+inybTbMJIUrLsig0w==",
"requires": { "requires": {
"acorn": "^8.7.0", "acorn": "^8.7.0",
"acorn-walk": "^8.2.0" "acorn-walk": "^8.2.0"

4
packages/nocodb/package.json

@ -112,7 +112,7 @@
"nc-lib-gui": "0.106.0", "nc-lib-gui": "0.106.0",
"nc-plugin": "0.1.2", "nc-plugin": "0.1.2",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nocodb-sdk": "0.106.0", "nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10", "nodemailer": "^6.4.10",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"os-locale": "^5.0.0", "os-locale": "^5.0.0",
@ -188,4 +188,4 @@
"prettier": { "prettier": {
"singleQuote": true "singleQuote": true
} }
} }

8
packages/nocodb/src/lib/cache/CacheMgr.ts vendored

@ -4,7 +4,13 @@ export default abstract class CacheMgr {
public abstract del(key: string): Promise<any>; public abstract del(key: string): Promise<any>;
public abstract getAll(pattern: string): Promise<any[]>; public abstract getAll(pattern: string): Promise<any[]>;
public abstract delAll(scope: string, pattern: string): Promise<any[]>; public abstract delAll(scope: string, pattern: string): Promise<any[]>;
public abstract getList(scope: string, list: string[]): Promise<any[]>; public abstract getList(
scope: string,
list: string[]
): Promise<{
list: any[];
isNoneList: boolean;
}>;
public abstract setList( public abstract setList(
scope: string, scope: string,
subListKeys: string[], subListKeys: string[],

11
packages/nocodb/src/lib/cache/NocoCache.ts vendored

@ -56,8 +56,15 @@ export default class NocoCache {
public static async getList( public static async getList(
scope: string, scope: string,
subKeys: string[] subKeys: string[]
): Promise<any[]> { ): Promise<{
if (this.cacheDisabled) return Promise.resolve([]); list: any[];
isNoneList: boolean;
}> {
if (this.cacheDisabled)
return Promise.resolve({
list: [],
isNoneList: false,
});
return this.client.getList(scope, subKeys); return this.client.getList(scope, subKeys);
} }

37
packages/nocodb/src/lib/cache/RedisCacheMgr.ts vendored

@ -114,7 +114,13 @@ export default class RedisCacheMgr extends CacheMgr {
); );
} }
async getList(scope: string, subKeys: string[]): Promise<any[]> { async getList(
scope: string,
subKeys: string[]
): Promise<{
list: any[];
isNoneList: boolean;
}> {
// remove null from arrays // remove null from arrays
subKeys = subKeys.filter((k) => k); subKeys = subKeys.filter((k) => k);
// e.g. key = nc:<orgs>:<scope>:<project_id_1>:<base_id_1>:list // e.g. key = nc:<orgs>:<scope>:<project_id_1>:<base_id_1>:list
@ -125,9 +131,22 @@ 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}`);
return Promise.all(
arr.map(async (k) => await this.get(k, CacheGetType.TYPE_OBJECT)) const isNoneList = arr.length && arr[0] === 'NONE';
);
if (isNoneList) {
return Promise.resolve({
list: [],
isNoneList,
});
}
return {
list: await Promise.all(
arr.map(async (k) => await this.get(k, CacheGetType.TYPE_OBJECT))
),
isNoneList,
};
} }
async setList( async setList(
@ -144,8 +163,8 @@ export default class RedisCacheMgr extends CacheMgr {
? `${this.prefix}:${scope}:list` ? `${this.prefix}:${scope}:list`
: `${this.prefix}:${scope}:${subListKeys.join(':')}:list`; : `${this.prefix}:${scope}:${subListKeys.join(':')}:list`;
if (!list.length) { if (!list.length) {
log(`RedisCacheMgr::setList: List is empty for ${listKey}. Skipping ...`); // Set NONE here so that it won't hit the DB on each page load
return Promise.resolve(true); return this.set(listKey, ['NONE']);
} }
// fetch existing list // fetch existing list
const listOfGetKeys = const listOfGetKeys =
@ -225,7 +244,11 @@ export default class RedisCacheMgr extends CacheMgr {
? `${this.prefix}:${scope}:list` ? `${this.prefix}:${scope}:list`
: `${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}`);
const list = (await this.get(listKey, CacheGetType.TYPE_ARRAY)) || []; let list = (await this.get(listKey, CacheGetType.TYPE_ARRAY)) || [];
if (list.length && list[0] === 'NONE') {
list = [];
await this.del(listKey);
}
list.push(key); list.push(key);
return this.set(listKey, list); return this.set(listKey, list);
} }

46
packages/nocodb/src/lib/cache/RedisMockCacheMgr.ts vendored

@ -2,6 +2,7 @@ import debug from 'debug';
import Redis from 'ioredis-mock'; import Redis from 'ioredis-mock';
import { CacheDelDirection, CacheGetType, CacheScope } from '../utils/globals'; import { CacheDelDirection, CacheGetType, CacheScope } from '../utils/globals';
import CacheMgr from './CacheMgr'; import CacheMgr from './CacheMgr';
const log = debug('nc:cache'); const log = debug('nc:cache');
export default class RedisMockCacheMgr extends CacheMgr { export default class RedisMockCacheMgr extends CacheMgr {
@ -102,10 +103,9 @@ export default class RedisMockCacheMgr extends CacheMgr {
`RedisMockCacheMgr::delAll: deleting all keys with pattern ${this.prefix}:${scope}:${pattern}` `RedisMockCacheMgr::delAll: deleting all keys with pattern ${this.prefix}:${scope}:${pattern}`
); );
await Promise.all( await Promise.all(
keys.map( keys.map(async (k) => {
async (k) => await this.deepDel(scope, k, CacheDelDirection.CHILD_TO_PARENT);
await this.deepDel(scope, k, CacheDelDirection.CHILD_TO_PARENT) })
)
); );
return Promise.all( return Promise.all(
keys.map(async (k) => { keys.map(async (k) => {
@ -114,7 +114,13 @@ export default class RedisMockCacheMgr extends CacheMgr {
); );
} }
async getList(scope: string, subKeys: string[]): Promise<any[]> { async getList(
scope: string,
subKeys: string[]
): Promise<{
list: any[];
isNoneList: boolean;
}> {
// remove null from arrays // remove null from arrays
subKeys = subKeys.filter((k) => k); subKeys = subKeys.filter((k) => k);
// e.g. key = nc:<orgs>:<scope>:<project_id_1>:<base_id_1>:list // e.g. key = nc:<orgs>:<scope>:<project_id_1>:<base_id_1>:list
@ -125,9 +131,21 @@ export default class RedisMockCacheMgr 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(`RedisMockCacheMgr::getList: getting list with key ${key}`); log(`RedisMockCacheMgr::getList: getting list with key ${key}`);
return Promise.all( const isNoneList = arr.length && arr[0] === 'NONE';
arr.map(async (k) => await this.get(k, CacheGetType.TYPE_OBJECT))
); if (isNoneList) {
return Promise.resolve({
list: [],
isNoneList,
});
}
return {
list: await Promise.all(
arr.map(async (k) => await this.get(k, CacheGetType.TYPE_OBJECT))
),
isNoneList,
};
} }
async setList( async setList(
@ -144,10 +162,8 @@ export default class RedisMockCacheMgr extends CacheMgr {
? `${this.prefix}:${scope}:list` ? `${this.prefix}:${scope}:list`
: `${this.prefix}:${scope}:${subListKeys.join(':')}:list`; : `${this.prefix}:${scope}:${subListKeys.join(':')}:list`;
if (!list.length) { if (!list.length) {
log( // Set NONE here so that it won't hit the DB on each page load
`RedisMockCacheMgr::setList: List is empty for ${listKey}. Skipping ...` return this.set(listKey, ['NONE']);
);
return Promise.resolve(true);
} }
// fetch existing list // fetch existing list
const listOfGetKeys = const listOfGetKeys =
@ -227,7 +243,11 @@ export default class RedisMockCacheMgr extends CacheMgr {
? `${this.prefix}:${scope}:list` ? `${this.prefix}:${scope}:list`
: `${this.prefix}:${scope}:${subListKeys.join(':')}:list`; : `${this.prefix}:${scope}:${subListKeys.join(':')}:list`;
log(`RedisMockCacheMgr::appendToList: append key ${key} to ${listKey}`); log(`RedisMockCacheMgr::appendToList: append key ${key} to ${listKey}`);
const list = (await this.get(listKey, CacheGetType.TYPE_ARRAY)) || []; let list = (await this.get(listKey, CacheGetType.TYPE_ARRAY)) || [];
if (list.length && list[0] === 'NONE') {
list = [];
await this.del(listKey);
}
list.push(key); list.push(key);
return this.set(listKey, list); return this.set(listKey, list);
} }

5
packages/nocodb/src/lib/controllers/publicControllers/publicData.ctl.ts

@ -31,6 +31,7 @@ async function dataInsert(req: Request & { files: any[] }, res: Response) {
password: req.headers?.['xc-password'] as string, password: req.headers?.['xc-password'] as string,
body: req.body?.data, body: req.body?.data,
siteUrl: (req as any).ncSiteUrl, siteUrl: (req as any).ncSiteUrl,
// req.files is enriched by multer
files: req.files, files: req.files,
}); });
@ -95,11 +96,11 @@ router.post(
); );
router.get( router.get(
'/api/v1/db/public/shared-view/:sharedViewUuid/rows/:rowId/mm/:colId', '/api/v1/db/public/shared-view/:sharedViewUuid/rows/:rowId/mm/:columnId',
catchError(publicMmList) catchError(publicMmList)
); );
router.get( router.get(
'/api/v1/db/public/shared-view/:sharedViewUuid/rows/:rowId/hm/:colId', '/api/v1/db/public/shared-view/:sharedViewUuid/rows/:rowId/hm/:columnId',
catchError(publicHmList) catchError(publicHmList)
); );

10
packages/nocodb/src/lib/controllers/user/user.ctl.ts

@ -98,6 +98,15 @@ async function signin(req, res, next) {
)(req, res, next); )(req, res, next);
} }
async function signout(req: Request<any, any>, res): Promise<any> {
res.json(
await userService.signout({
req,
res,
})
);
}
async function googleSignin(req, res, next) { async function googleSignin(req, res, next) {
passport.authenticate( passport.authenticate(
'google', 'google',
@ -246,6 +255,7 @@ const mapRoutes = (router) => {
// new API // new API
router.post('/api/v1/auth/user/signup', catchError(signup)); router.post('/api/v1/auth/user/signup', catchError(signup));
router.post('/api/v1/auth/user/signin', catchError(signin)); router.post('/api/v1/auth/user/signin', catchError(signin));
router.post('/api/v1/auth/user/signout', catchError(signout));
router.get( router.get(
'/api/v1/auth/user/me', '/api/v1/auth/user/me',
extractProjectIdAndAuthenticate, extractProjectIdAndAuthenticate,

25
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts

@ -2117,12 +2117,10 @@ class BaseModelSqlv2 {
datas.map((d) => this.model.mapAliasToColumn(d)) datas.map((d) => this.model.mapAliasToColumn(d))
); );
transaction = await this.dbDriver.transaction();
// await this.beforeUpdateb(updateDatas, transaction);
const prevData = []; const prevData = [];
const newData = []; const newData = [];
const updatePkValues = []; const updatePkValues = [];
const toBeUpdated = [];
const res = []; const res = [];
for (const d of updateDatas) { for (const d of updateDatas) {
await this.validate(d); await this.validate(d);
@ -2133,11 +2131,17 @@ class BaseModelSqlv2 {
} }
prevData.push(await this.readByPk(pkValues)); prevData.push(await this.readByPk(pkValues));
const wherePk = await this._wherePk(pkValues); const wherePk = await this._wherePk(pkValues);
await transaction(this.tnPath).update(d).where(wherePk);
res.push(wherePk); res.push(wherePk);
toBeUpdated.push({ d, wherePk });
updatePkValues.push(pkValues); updatePkValues.push(pkValues);
} }
transaction = await this.dbDriver.transaction();
for (const o of toBeUpdated) {
await transaction(this.tnPath).update(o.d).where(o.wherePk);
}
await transaction.commit(); await transaction.commit();
for (const pkValues of updatePkValues) { for (const pkValues of updatePkValues) {
@ -3242,17 +3246,24 @@ function extractCondition(nestedArrayConditions, aliasColObjMap) {
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let [logicOp, alias, op, value] = let [logicOp, alias, op, value] =
str.match(/(?:~(and|or|not))?\((.*?),(\w+),(.*)\)/)?.slice(1) || []; str.match(/(?:~(and|or|not))?\((.*?),(\w+),(.*)\)/)?.slice(1) || [];
if (!alias && !op && !value) {
// try match with blank filter format
[logicOp, alias, op, value] =
str.match(/(?:~(and|or|not))?\((.*?),(\w+)\)/)?.slice(1) || [];
}
let sub_op = null; let sub_op = null;
if (aliasColObjMap[alias]) { if (aliasColObjMap[alias]) {
if ( if (
[UITypes.Date, UITypes.DateTime].includes(aliasColObjMap[alias].uidt) [UITypes.Date, UITypes.DateTime].includes(aliasColObjMap[alias].uidt)
) { ) {
value = value.split(','); value = value?.split(',');
// the first element would be sub_op // the first element would be sub_op
sub_op = value[0]; sub_op = value?.[0];
// remove the first element which is sub_op // remove the first element which is sub_op
value.shift(); value?.shift();
value = value?.[0];
} else if (op === 'in') { } else if (op === 'in') {
value = value.split(','); value = value.split(',');
} }

40
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts

@ -3,11 +3,14 @@ import { jsepCurlyHook, UITypes } from 'nocodb-sdk';
import mapFunctionName from '../mapFunctionName'; import mapFunctionName from '../mapFunctionName';
import genRollupSelectv2 from '../genRollupSelectv2'; import genRollupSelectv2 from '../genRollupSelectv2';
import FormulaColumn from '../../../../../models/FormulaColumn'; import FormulaColumn from '../../../../../models/FormulaColumn';
import { validateDateWithUnknownFormat } from '../helpers/formulaFnHelper'; import {
convertDateFormatForConcat,
validateDateWithUnknownFormat,
} from '../helpers/formulaFnHelper';
import { CacheGetType, CacheScope } from '../../../../../utils/globals'; import { CacheGetType, CacheScope } from '../../../../../utils/globals';
import NocoCache from '../../../../../cache/NocoCache'; import NocoCache from '../../../../../cache/NocoCache';
import type Model from '../../../../../models/Model';
import type Column from '../../../../../models/Column'; import type Column from '../../../../../models/Column';
import type Model from '../../../../../models/Model';
import type RollupColumn from '../../../../../models/RollupColumn'; import type RollupColumn from '../../../../../models/RollupColumn';
import type { XKnex } from '../../../index'; import type { XKnex } from '../../../index';
import type LinkToAnotherRecordColumn from '../../../../../models/LinkToAnotherRecordColumn'; import type LinkToAnotherRecordColumn from '../../../../../models/LinkToAnotherRecordColumn';
@ -633,8 +636,19 @@ async function _formulaQueryBuilder(
`${pt.callee.name}(${( `${pt.callee.name}(${(
await Promise.all( await Promise.all(
pt.arguments.map(async (arg) => { pt.arguments.map(async (arg) => {
const query = (await fn(arg)).builder.toQuery(); let query = (await fn(arg)).builder.toQuery();
if (pt.callee.name === 'CONCAT') { if (pt.callee.name === 'CONCAT') {
if (knex.clientType() !== 'sqlite3') {
query = await convertDateFormatForConcat(
arg,
columnIdToUidt,
query,
knex.clientType()
);
} else {
// sqlite3: special handling - See BinaryExpression
}
if (knex.clientType() === 'mysql2') { if (knex.clientType() === 'mysql2') {
// mysql2: CONCAT() returns NULL if any argument is NULL. // mysql2: CONCAT() returns NULL if any argument is NULL.
// adding IFNULL to convert NULL values to empty strings // adding IFNULL to convert NULL values to empty strings
@ -679,8 +693,8 @@ async function _formulaQueryBuilder(
pt.left.fnName = pt.left.fnName || 'ARITH'; pt.left.fnName = pt.left.fnName || 'ARITH';
pt.right.fnName = pt.right.fnName || 'ARITH'; pt.right.fnName = pt.right.fnName || 'ARITH';
const left = (await fn(pt.left, null, pt.operator)).builder.toQuery(); let left = (await fn(pt.left, null, pt.operator)).builder.toQuery();
const right = (await fn(pt.right, null, pt.operator)).builder.toQuery(); let right = (await fn(pt.right, null, pt.operator)).builder.toQuery();
let sql = `${left} ${pt.operator} ${right}${colAlias}`; let sql = `${left} ${pt.operator} ${right}${colAlias}`;
// comparing a date with empty string would throw // comparing a date with empty string would throw
@ -724,8 +738,22 @@ async function _formulaQueryBuilder(
} }
} }
// handle NULL values when calling CONCAT for sqlite3
if (pt.left.fnName === 'CONCAT' && knex.clientType() === 'sqlite3') { if (pt.left.fnName === 'CONCAT' && knex.clientType() === 'sqlite3') {
// handle date format
left = await convertDateFormatForConcat(
pt.left?.arguments?.[0],
columnIdToUidt,
left,
knex.clientType()
);
right = await convertDateFormatForConcat(
pt.right?.arguments?.[0],
columnIdToUidt,
right,
knex.clientType()
);
// handle NULL values when calling CONCAT for sqlite3
sql = `COALESCE(${left}, '') ${pt.operator} COALESCE(${right},'')${colAlias}`; sql = `COALESCE(${left}, '') ${pt.operator} COALESCE(${right},'')${colAlias}`;
} }

16
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/pg.ts

@ -17,9 +17,9 @@ const pg = {
builder: args.knex.raw( builder: args.knex.raw(
`POSITION(${args.knex.raw( `POSITION(${args.knex.raw(
(await args.fn(args.pt.arguments[1])).builder.toQuery() (await args.fn(args.pt.arguments[1])).builder.toQuery()
)} in ${args.knex )} in ${args.knex.raw(
.raw((await args.fn(args.pt.arguments[0])).builder) (await args.fn(args.pt.arguments[0])).builder.toQuery()
.toQuery()})${args.colAlias}` )})${args.colAlias}`
), ),
}; };
}, },
@ -157,11 +157,13 @@ const pg = {
builder: args.knex.raw( builder: args.knex.raw(
`CASE WHEN ${args.knex `CASE WHEN ${args.knex
.raw( .raw(
`${args.pt.arguments `${(
.map(async (ar) => await Promise.all(
(await args.fn(ar, '', 'OR')).builder.toQuery() args.pt.arguments.map(async (ar) =>
(await args.fn(ar, '', 'OR')).builder.toQuery()
)
) )
.join(' OR ')}` ).join(' OR ')}`
) )
.wrap('(', ')') .wrap('(', ')')
.toQuery()} THEN TRUE ELSE FALSE END ${args.colAlias}` .toQuery()} THEN TRUE ELSE FALSE END ${args.colAlias}`

23
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/convertDateFormat.ts

@ -0,0 +1,23 @@
export function convertDateFormat(date_format: string, type: string) {
if (date_format === 'YYYY-MM-DD') {
if (type === 'mysql2' || type === 'sqlite3') return '%Y-%m-%d';
} else if (date_format === 'YYYY/MM/DD') {
if (type === 'mysql2' || type === 'sqlite3') return '%Y/%m/%d';
} else if (date_format === 'DD-MM-YYYY') {
if (type === 'mysql2' || type === 'sqlite3') return '%d/%m/%Y';
} else if (date_format === 'MM-DD-YYYY') {
if (type === 'mysql2' || type === 'sqlite3') return '%d-%m-%Y';
} else if (date_format === 'DD/MM/YYYY') {
if (type === 'mysql2' || type === 'sqlite3') return '%d/%m/%Y';
} else if (date_format === 'MM/DD/YYYY') {
if (type === 'mysql2' || type === 'sqlite3') return '%m-%d-%Y';
} else if (date_format === 'DD MM YYYY') {
if (type === 'mysql2' || type === 'sqlite3') return '%d %m %Y';
} else if (date_format === 'MM DD YYYY') {
if (type === 'mysql2' || type === 'sqlite3') return '%m %d %Y';
} else if (date_format === 'YYYY MM DD') {
if (type === 'mysql2' || type === 'sqlite3') return '%Y %m %d';
}
// pg / mssql
return date_format;
}

45
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/formulaFnHelper.ts

@ -1,5 +1,8 @@
import dayjs, { extend } from 'dayjs'; import dayjs, { extend } from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat.js'; import customParseFormat from 'dayjs/plugin/customParseFormat.js';
import { UITypes } from 'nocodb-sdk';
import Column from '../../../../../models/Column';
import { convertDateFormat } from './convertDateFormat';
extend(customParseFormat); extend(customParseFormat);
export function getWeekdayByText(v: string) { export function getWeekdayByText(v: string) {
@ -50,3 +53,45 @@ export function validateDateWithUnknownFormat(v: string) {
} }
return false; return false;
} }
export async function convertDateFormatForConcat(
o,
columnIdToUidt,
query,
clientType
) {
if (
o?.type === 'Identifier' &&
o?.name in columnIdToUidt &&
columnIdToUidt[o.name] === UITypes.Date
) {
const meta = (
await Column.get({
colId: o.name,
})
).meta;
if (clientType === 'mysql2') {
query = `DATE_FORMAT(${query}, '${convertDateFormat(
meta.date_format,
clientType
)}')`;
} else if (clientType === 'pg') {
query = `TO_CHAR(${query}, '${convertDateFormat(
meta.date_format,
clientType
)}')`;
} else if (clientType === 'sqlite3') {
query = `strftime('${convertDateFormat(
meta.date_format,
clientType
)}', ${query})`;
} else if (clientType === 'mssql') {
query = `FORMAT(${query}, '${convertDateFormat(
meta.date_format,
clientType
)}')`;
}
}
return query;
}

6
packages/nocodb/src/lib/models/Base.ts

@ -150,10 +150,12 @@ export default class Base implements BaseType {
args: { projectId: string }, args: { projectId: string },
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
): Promise<Base[]> { ): Promise<Base[]> {
let baseDataList = await NocoCache.getList(CacheScope.BASE, [ const cachedList = await NocoCache.getList(CacheScope.BASE, [
args.projectId, args.projectId,
]); ]);
if (!baseDataList.length) { let { list: baseDataList } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !baseDataList.length) {
baseDataList = await ncMeta.metaList2( baseDataList = await ncMeta.metaList2(
args.projectId, args.projectId,
null, null,

82
packages/nocodb/src/lib/models/Column.ts

@ -474,8 +474,12 @@ export default class Column<T = any> implements ColumnType {
}, },
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
): Promise<Column[]> { ): Promise<Column[]> {
let columnsList = await NocoCache.getList(CacheScope.COLUMN, [fk_model_id]); const cachedList = await NocoCache.getList(CacheScope.COLUMN, [
if (!columnsList.length) { fk_model_id,
]);
let { list: columnsList } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !columnsList.length) {
columnsList = await ncMeta.metaList2(null, null, MetaTable.COLUMNS, { columnsList = await ncMeta.metaList2(null, null, MetaTable.COLUMNS, {
condition: { condition: {
fk_model_id, fk_model_id,
@ -627,8 +631,10 @@ export default class Column<T = any> implements ColumnType {
// get lookup columns and delete // get lookup columns and delete
{ {
let lookups = await NocoCache.getList(CacheScope.COL_LOOKUP, [id]); const cachedList = await NocoCache.getList(CacheScope.COL_LOOKUP, [id]);
if (!lookups.length) { let { list: lookups } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !lookups.length) {
lookups = await ncMeta.metaList2(null, null, MetaTable.COL_LOOKUP, { lookups = await ncMeta.metaList2(null, null, MetaTable.COL_LOOKUP, {
condition: { fk_lookup_column_id: id }, condition: { fk_lookup_column_id: id },
}); });
@ -640,8 +646,10 @@ export default class Column<T = any> implements ColumnType {
// get rollup column and delete // get rollup column and delete
{ {
let rollups = await NocoCache.getList(CacheScope.COL_ROLLUP, [id]); const cachedList = await NocoCache.getList(CacheScope.COL_ROLLUP, [id]);
if (!rollups.length) { let { list: rollups } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !rollups.length) {
rollups = await ncMeta.metaList2(null, null, MetaTable.COL_ROLLUP, { rollups = await ncMeta.metaList2(null, null, MetaTable.COL_ROLLUP, {
condition: { fk_rollup_column_id: id }, condition: { fk_rollup_column_id: id },
}); });
@ -652,10 +660,12 @@ export default class Column<T = any> implements ColumnType {
} }
{ {
let formulaColumns = await NocoCache.getList(CacheScope.COLUMN, [ const cachedList = await NocoCache.getList(CacheScope.COLUMN, [
col.fk_model_id, col.fk_model_id,
]); ]);
if (!formulaColumns.length) { let { list: formulaColumns } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !formulaColumns.length) {
formulaColumns = await ncMeta.metaList2(null, null, MetaTable.COLUMNS, { formulaColumns = await ncMeta.metaList2(null, null, MetaTable.COLUMNS, {
condition: { condition: {
fk_model_id: col.fk_model_id, fk_model_id: col.fk_model_id,
@ -682,33 +692,43 @@ export default class Column<T = any> implements ColumnType {
// if relation column check lookup and rollup and delete // if relation column check lookup and rollup and delete
if (col.uidt === UITypes.LinkToAnotherRecord) { if (col.uidt === UITypes.LinkToAnotherRecord) {
// get lookup columns using relation and delete {
let lookups = await NocoCache.getList(CacheScope.COL_LOOKUP, [id]); // get lookup columns using relation and delete
if (!lookups.length) { const cachedList = await NocoCache.getList(CacheScope.COL_LOOKUP, [id]);
lookups = await ncMeta.metaList2(null, null, MetaTable.COL_LOOKUP, { let { list: lookups } = cachedList;
condition: { fk_relation_column_id: id }, const { isNoneList } = cachedList;
}); if (!isNoneList && !lookups.length) {
} lookups = await ncMeta.metaList2(null, null, MetaTable.COL_LOOKUP, {
for (const lookup of lookups) { condition: { fk_relation_column_id: id },
await Column.delete(lookup.fk_column_id, ncMeta); });
}
for (const lookup of lookups) {
await Column.delete(lookup.fk_column_id, ncMeta);
}
} }
// get rollup columns using relation and delete {
let rollups = await NocoCache.getList(CacheScope.COL_ROLLUP, [id]); // get rollup columns using relation and delete
if (!rollups.length) { const cachedList = await NocoCache.getList(CacheScope.COL_ROLLUP, [id]);
rollups = await ncMeta.metaList2(null, null, MetaTable.COL_ROLLUP, { let { list: rollups } = cachedList;
condition: { fk_relation_column_id: id }, const { isNoneList } = cachedList;
}); if (!isNoneList && !rollups.length) {
} rollups = await ncMeta.metaList2(null, null, MetaTable.COL_ROLLUP, {
for (const rollup of rollups) { condition: { fk_relation_column_id: id },
await Column.delete(rollup.fk_column_id, ncMeta); });
}
for (const rollup of rollups) {
await Column.delete(rollup.fk_column_id, ncMeta);
}
} }
} }
// delete sorts // delete sorts
{ {
let sorts = await NocoCache.getList(CacheScope.SORT, [id]); const cachedList = await NocoCache.getList(CacheScope.SORT, [id]);
if (!sorts.length) { let { list: sorts } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !sorts.length) {
sorts = await ncMeta.metaList2(null, null, MetaTable.SORT, { sorts = await ncMeta.metaList2(null, null, MetaTable.SORT, {
condition: { condition: {
fk_column_id: id, fk_column_id: id,
@ -721,8 +741,10 @@ export default class Column<T = any> implements ColumnType {
} }
// delete filters // delete filters
{ {
let filters = await NocoCache.getList(CacheScope.FILTER_EXP, [id]); const cachedList = await NocoCache.getList(CacheScope.FILTER_EXP, [id]);
if (!filters.length) { let { list: filters } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !filters.length) {
filters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, { filters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, {
condition: { condition: {
fk_column_id: id, fk_column_id: id,

38
packages/nocodb/src/lib/models/Filter.ts

@ -326,10 +326,12 @@ export default class Filter implements FilterType {
public async getChildren(ncMeta = Noco.ncMeta): Promise<Filter[]> { public async getChildren(ncMeta = Noco.ncMeta): Promise<Filter[]> {
if (this.children) return this.children; if (this.children) return this.children;
if (!this.is_group) return null; if (!this.is_group) return null;
let childFilters = await NocoCache.getList(CacheScope.FILTER_EXP, [ const cachedList = await NocoCache.getList(CacheScope.FILTER_EXP, [
this.id, this.id,
]); ]);
if (!childFilters.length) { let { list: childFilters } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !childFilters.length) {
childFilters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, { childFilters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, {
condition: { condition: {
fk_parent_id: this.id, fk_parent_id: this.id,
@ -369,10 +371,12 @@ export default class Filter implements FilterType {
}, },
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
): Promise<FilterType> { ): Promise<FilterType> {
let filters = await NocoCache.getList(CacheScope.FILTER_EXP, [ const cachedList = await NocoCache.getList(CacheScope.FILTER_EXP, [
viewId || hookId, viewId || hookId,
]); ]);
if (!filters.length) { let { list: filters } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !filters.length) {
filters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, { filters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, {
condition: viewId ? { fk_view_id: viewId } : { fk_hook_id: hookId }, condition: viewId ? { fk_view_id: viewId } : { fk_hook_id: hookId },
orderBy: { orderBy: {
@ -480,8 +484,10 @@ export default class Filter implements FilterType {
{ viewId }: { viewId: any }, { viewId }: { viewId: any },
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
) { ) {
let filterObjs = await NocoCache.getList(CacheScope.FILTER_EXP, [viewId]); const cachedList = await NocoCache.getList(CacheScope.FILTER_EXP, [viewId]);
if (!filterObjs.length) { let { list: filterObjs } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !filterObjs.length) {
filterObjs = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, { filterObjs = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, {
condition: { fk_view_id: viewId }, condition: { fk_view_id: viewId },
orderBy: { orderBy: {
@ -499,8 +505,10 @@ export default class Filter implements FilterType {
{ hookId }: { hookId: any }, { hookId }: { hookId: any },
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
) { ) {
let filterObjs = await NocoCache.getList(CacheScope.FILTER_EXP, [hookId]); const cachedList = await NocoCache.getList(CacheScope.FILTER_EXP, [hookId]);
if (!filterObjs.length) { let { list: filterObjs } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !filterObjs.length) {
filterObjs = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, { filterObjs = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, {
condition: { fk_hook_id: hookId }, condition: { fk_hook_id: hookId },
orderBy: { orderBy: {
@ -520,8 +528,12 @@ export default class Filter implements FilterType {
}, },
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
) { ) {
let filterObjs = await NocoCache.getList(CacheScope.FILTER_EXP, [parentId]); const cachedList = await NocoCache.getList(CacheScope.FILTER_EXP, [
if (!filterObjs.length) { parentId,
]);
let { list: filterObjs } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !filterObjs.length) {
filterObjs = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, { filterObjs = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, {
condition: { condition: {
fk_parent_id: parentId, fk_parent_id: parentId,
@ -546,11 +558,13 @@ export default class Filter implements FilterType {
}, },
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
) { ) {
let filterObjs = await NocoCache.getList(CacheScope.FILTER_EXP, [ const cachedList = await NocoCache.getList(CacheScope.FILTER_EXP, [
hookId, hookId,
parentId, parentId,
]); ]);
if (!filterObjs.length) { let { list: filterObjs } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !filterObjs.length) {
filterObjs = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, { filterObjs = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, {
condition: { condition: {
fk_parent_id: parentId, fk_parent_id: parentId,

18
packages/nocodb/src/lib/models/FormViewColumn.ts

@ -100,13 +100,11 @@ export default class FormViewColumn implements FormColumnType {
await NocoCache.set(`${CacheScope.FORM_VIEW_COLUMN}:${fk_column_id}`, id); await NocoCache.set(`${CacheScope.FORM_VIEW_COLUMN}:${fk_column_id}`, id);
// if cache is not present skip pushing it into the list to avoid unexpected behaviour // if cache is not present skip pushing it into the list to avoid unexpected behaviour
if ( const { list } = await NocoCache.getList(CacheScope.FORM_VIEW_COLUMN, [
( column.fk_view_id,
await NocoCache.getList(CacheScope.FORM_VIEW_COLUMN, [ ]);
column.fk_view_id,
]) if (list?.length)
)?.length
)
await NocoCache.appendToList( await NocoCache.appendToList(
CacheScope.FORM_VIEW_COLUMN, CacheScope.FORM_VIEW_COLUMN,
[column.fk_view_id], [column.fk_view_id],
@ -119,10 +117,12 @@ export default class FormViewColumn implements FormColumnType {
viewId: string, viewId: string,
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
): Promise<FormViewColumn[]> { ): Promise<FormViewColumn[]> {
let viewColumns = await NocoCache.getList(CacheScope.FORM_VIEW_COLUMN, [ const cachedList = await NocoCache.getList(CacheScope.FORM_VIEW_COLUMN, [
viewId, viewId,
]); ]);
if (!viewColumns.length) { let { list: viewColumns } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !viewColumns.length) {
viewColumns = await ncMeta.metaList2( viewColumns = await ncMeta.metaList2(
null, null,
null, null,

18
packages/nocodb/src/lib/models/GalleryViewColumn.ts

@ -79,13 +79,11 @@ export default class GalleryViewColumn {
); );
// if cache is not present skip pushing it into the list to avoid unexpected behaviour // if cache is not present skip pushing it into the list to avoid unexpected behaviour
if ( const { list } = await NocoCache.getList(CacheScope.GALLERY_VIEW_COLUMN, [
( column.fk_view_id,
await NocoCache.getList(CacheScope.GALLERY_VIEW_COLUMN, [ ]);
column.fk_view_id,
]) if (list?.length)
)?.length
)
await NocoCache.appendToList( await NocoCache.appendToList(
CacheScope.GALLERY_VIEW_COLUMN, CacheScope.GALLERY_VIEW_COLUMN,
[column.fk_view_id], [column.fk_view_id],
@ -99,10 +97,12 @@ export default class GalleryViewColumn {
viewId: string, viewId: string,
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
): Promise<GalleryViewColumn[]> { ): Promise<GalleryViewColumn[]> {
let views = await NocoCache.getList(CacheScope.GALLERY_VIEW_COLUMN, [ const cachedList = await NocoCache.getList(CacheScope.GALLERY_VIEW_COLUMN, [
viewId, viewId,
]); ]);
if (!views.length) { let { list: views } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !views.length) {
views = await ncMeta.metaList2( views = await ncMeta.metaList2(
null, null,
null, null,

19
packages/nocodb/src/lib/models/GridViewColumn.ts

@ -24,8 +24,12 @@ export default class GridViewColumn implements GridColumnType {
viewId: string, viewId: string,
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
): Promise<GridViewColumn[]> { ): Promise<GridViewColumn[]> {
let views = await NocoCache.getList(CacheScope.GRID_VIEW_COLUMN, [viewId]); const cachedList = await NocoCache.getList(CacheScope.GRID_VIEW_COLUMN, [
if (!views.length) { viewId,
]);
let { list: views } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !views.length) {
views = await ncMeta.metaList2(null, null, MetaTable.GRID_VIEW_COLUMNS, { views = await ncMeta.metaList2(null, null, MetaTable.GRID_VIEW_COLUMNS, {
condition: { condition: {
fk_view_id: viewId, fk_view_id: viewId,
@ -99,13 +103,10 @@ export default class GridViewColumn implements GridColumnType {
await NocoCache.set(`${CacheScope.GRID_VIEW_COLUMN}:${fk_column_id}`, id); await NocoCache.set(`${CacheScope.GRID_VIEW_COLUMN}:${fk_column_id}`, id);
// if cache is not present skip pushing it into the list to avoid unexpected behaviour // if cache is not present skip pushing it into the list to avoid unexpected behaviour
if ( const { list } = await NocoCache.getList(CacheScope.GRID_VIEW_COLUMN, [
( column.fk_view_id,
await NocoCache.getList(CacheScope.GRID_VIEW_COLUMN, [ ]);
column.fk_view_id, if (list.length)
])
)?.length
)
await NocoCache.appendToList( await NocoCache.appendToList(
CacheScope.GRID_VIEW_COLUMN, CacheScope.GRID_VIEW_COLUMN,
[column.fk_view_id], [column.fk_view_id],

8
packages/nocodb/src/lib/models/Hook.ts

@ -85,8 +85,12 @@ export default class Hook implements HookType {
}, },
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
) { ) {
let hooks = await NocoCache.getList(CacheScope.HOOK, [param.fk_model_id]); const cachedList = await NocoCache.getList(CacheScope.HOOK, [
if (!hooks.length) { param.fk_model_id,
]);
let { list: hooks } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !hooks.length) {
hooks = await ncMeta.metaList(null, null, MetaTable.HOOKS, { hooks = await ncMeta.metaList(null, null, MetaTable.HOOKS, {
condition: { condition: {
fk_model_id: param.fk_model_id, fk_model_id: param.fk_model_id,

18
packages/nocodb/src/lib/models/HookFilter.ts

@ -205,10 +205,12 @@ export default class Filter {
public async getChildren(ncMeta = Noco.ncMeta): Promise<Filter[]> { public async getChildren(ncMeta = Noco.ncMeta): Promise<Filter[]> {
if (this.children) return this.children; if (this.children) return this.children;
if (!this.is_group) return null; if (!this.is_group) return null;
let childFilters = await NocoCache.getList(CacheScope.FILTER_EXP, [ const cachedList = await NocoCache.getList(CacheScope.FILTER_EXP, [
this.id, this.id,
]); ]);
if (!childFilters.length) { let { list: childFilters } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !childFilters.length) {
childFilters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, { childFilters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, {
condition: { condition: {
fk_parent_id: this.id, fk_parent_id: this.id,
@ -243,8 +245,10 @@ export default class Filter {
}, },
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
): Promise<FilterType> { ): Promise<FilterType> {
let filters = await NocoCache.getList(CacheScope.FILTER_EXP, [viewId]); const cachedList = await NocoCache.getList(CacheScope.FILTER_EXP, [viewId]);
if (!filters.length) { let { list: filters } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !filters.length) {
filters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, { filters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, {
condition: { fk_view_id: viewId }, condition: { fk_view_id: viewId },
}); });
@ -327,8 +331,10 @@ export default class Filter {
{ viewId }: { viewId: any }, { viewId }: { viewId: any },
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
) { ) {
let filterObjs = await NocoCache.getList(CacheScope.FILTER_EXP, [viewId]); const cachedList = await NocoCache.getList(CacheScope.FILTER_EXP, [viewId]);
if (!filterObjs.length) { let { list: filterObjs } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !filterObjs.length) {
filterObjs = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, { filterObjs = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, {
condition: { fk_view_id: viewId }, condition: { fk_view_id: viewId },
}); });

6
packages/nocodb/src/lib/models/KanbanViewColumn.ts

@ -85,10 +85,12 @@ export default class KanbanViewColumn implements KanbanColumnType {
viewId: string, viewId: string,
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
): Promise<KanbanViewColumn[]> { ): Promise<KanbanViewColumn[]> {
let views = await NocoCache.getList(CacheScope.KANBAN_VIEW_COLUMN, [ const cachedList = await NocoCache.getList(CacheScope.KANBAN_VIEW_COLUMN, [
viewId, viewId,
]); ]);
if (!views.length) { let { list: views } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !views.length) {
views = await ncMeta.metaList2( views = await ncMeta.metaList2(
null, null,
null, null,

16
packages/nocodb/src/lib/models/MapViewColumn.ts

@ -67,10 +67,10 @@ export default class MapViewColumn {
await NocoCache.set(`${CacheScope.MAP_VIEW_COLUMN}:${fk_column_id}`, id); await NocoCache.set(`${CacheScope.MAP_VIEW_COLUMN}:${fk_column_id}`, id);
// if cache is not present skip pushing it into the list to avoid unexpected behaviour // if cache is not present skip pushing it into the list to avoid unexpected behaviour
if ( const { list } = await NocoCache.getList(CacheScope.MAP_VIEW_COLUMN, [
(await NocoCache.getList(CacheScope.MAP_VIEW_COLUMN, [column.fk_view_id])) column.fk_view_id,
?.length ]);
) if (list?.length)
await NocoCache.appendToList( await NocoCache.appendToList(
CacheScope.MAP_VIEW_COLUMN, CacheScope.MAP_VIEW_COLUMN,
[column.fk_view_id], [column.fk_view_id],
@ -84,8 +84,12 @@ export default class MapViewColumn {
viewId: string, viewId: string,
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
): Promise<MapViewColumn[]> { ): Promise<MapViewColumn[]> {
let views = await NocoCache.getList(CacheScope.MAP_VIEW_COLUMN, [viewId]); const cachedList = await NocoCache.getList(CacheScope.MAP_VIEW_COLUMN, [
if (!views.length) { viewId,
]);
let { list: views } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !views.length) {
views = await ncMeta.metaList2(null, null, MetaTable.MAP_VIEW_COLUMNS, { views = await ncMeta.metaList2(null, null, MetaTable.MAP_VIEW_COLUMNS, {
condition: { condition: {
fk_view_id: viewId, fk_view_id: viewId,

27
packages/nocodb/src/lib/models/Model.ts

@ -160,13 +160,10 @@ export default class Model implements TableType {
}, },
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
): Promise<Model[]> { ): Promise<Model[]> {
let modelList = []; const cachedList = await NocoCache.getList(CacheScope.MODEL, [project_id]);
if (base_id) { let { list: modelList } = cachedList;
await NocoCache.getList(CacheScope.MODEL, [project_id, base_id]); const { isNoneList } = cachedList;
} else { if (!isNoneList && !modelList.length) {
await NocoCache.getList(CacheScope.MODEL, [project_id]);
}
if (!modelList.length) {
modelList = await ncMeta.metaList2( modelList = await ncMeta.metaList2(
project_id, project_id,
base_id, base_id,
@ -183,15 +180,7 @@ export default class Model implements TableType {
model.meta = parseMetaProp(model); model.meta = parseMetaProp(model);
} }
if (base_id) { await NocoCache.setList(CacheScope.MODEL, [project_id], modelList);
await NocoCache.setList(
CacheScope.MODEL,
[project_id, base_id],
modelList
);
} else {
await NocoCache.setList(CacheScope.MODEL, [project_id], modelList);
}
} }
modelList.sort( modelList.sort(
(a, b) => (a, b) =>
@ -211,11 +200,13 @@ export default class Model implements TableType {
}, },
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
): Promise<Model[]> { ): Promise<Model[]> {
let modelList = await NocoCache.getList(CacheScope.MODEL, [ const cachedList = await NocoCache.getList(CacheScope.MODEL, [
project_id, project_id,
db_alias, db_alias,
]); ]);
if (!modelList.length) { let { list: modelList } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !modelList.length) {
modelList = await ncMeta.metaList2( modelList = await ncMeta.metaList2(
project_id, project_id,
db_alias, db_alias,

11
packages/nocodb/src/lib/models/ModelRoleVisibility.ts

@ -24,10 +24,13 @@ export default class ModelRoleVisibility implements ModelRoleVisibilityType {
} }
static async list(projectId): Promise<ModelRoleVisibility[]> { static async list(projectId): Promise<ModelRoleVisibility[]> {
let data = await NocoCache.getList(CacheScope.MODEL_ROLE_VISIBILITY, [ const cachedList = await NocoCache.getList(
projectId, CacheScope.MODEL_ROLE_VISIBILITY,
]); [projectId]
if (!data.length) { );
let { list: data } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !data.length) {
data = await Noco.ncMeta.metaList2( data = await Noco.ncMeta.metaList2(
projectId, projectId,
null, null,

6
packages/nocodb/src/lib/models/Plugin.ts

@ -43,8 +43,10 @@ export default class Plugin implements PluginType {
} }
static async list(ncMeta = Noco.ncMeta) { static async list(ncMeta = Noco.ncMeta) {
let pluginList = await NocoCache.getList(CacheScope.PLUGIN, []); const cachedList = await NocoCache.getList(CacheScope.PLUGIN, []);
if (!pluginList.length) { let { list: pluginList } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !pluginList.length) {
pluginList = await ncMeta.metaList2(null, null, MetaTable.PLUGIN); pluginList = await ncMeta.metaList2(null, null, MetaTable.PLUGIN);
await NocoCache.setList(CacheScope.PLUGIN, [], pluginList); await NocoCache.setList(CacheScope.PLUGIN, [], pluginList);
} }

6
packages/nocodb/src/lib/models/Project.ts

@ -79,8 +79,10 @@ export default class Project implements ProjectType {
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
): Promise<Project[]> { ): Promise<Project[]> {
// todo: pagination // todo: pagination
let projectList = await NocoCache.getList(CacheScope.PROJECT, []); const cachedList = await NocoCache.getList(CacheScope.PROJECT, []);
if (!projectList.length) { let { list: projectList } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !projectList.length) {
projectList = await ncMeta.metaList2(null, null, MetaTable.PROJECT, { projectList = await ncMeta.metaList2(null, null, MetaTable.PROJECT, {
xcCondition: { xcCondition: {
_or: [ _or: [

12
packages/nocodb/src/lib/models/ProjectUser.ts

@ -187,10 +187,12 @@ export default class ProjectUser {
} }
// remove project from user project list cache // remove project from user project list cache
let cachedProjectList = await NocoCache.getList(CacheScope.USER_PROJECT, [ const cachedList = await NocoCache.getList(CacheScope.USER_PROJECT, [
userId, userId,
]); ]);
if (cachedProjectList?.length) { let { list: cachedProjectList } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && cachedProjectList?.length) {
cachedProjectList = cachedProjectList.filter((p) => p.id !== projectId); cachedProjectList = cachedProjectList.filter((p) => p.id !== projectId);
await NocoCache.setList( await NocoCache.setList(
CacheScope.USER_PROJECT, CacheScope.USER_PROJECT,
@ -221,11 +223,13 @@ export default class ProjectUser {
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
): Promise<ProjectType[]> { ): Promise<ProjectType[]> {
// todo: pagination // todo: pagination
let projectList = await NocoCache.getList(CacheScope.USER_PROJECT, [ const cachedList = await NocoCache.getList(CacheScope.USER_PROJECT, [
userId, userId,
]); ]);
let { list: projectList } = cachedList;
const { isNoneList } = cachedList;
if (projectList.length) { if (!isNoneList && projectList.length) {
return projectList; return projectList;
} }

6
packages/nocodb/src/lib/models/SelectOption.ts

@ -69,10 +69,12 @@ export default class SelectOption implements SelectOptionType {
} }
public static async read(fk_column_id: string, ncMeta = Noco.ncMeta) { public static async read(fk_column_id: string, ncMeta = Noco.ncMeta) {
let options = await NocoCache.getList(CacheScope.COL_SELECT_OPTION, [ const cachedList = await NocoCache.getList(CacheScope.COL_SELECT_OPTION, [
fk_column_id, fk_column_id,
]); ]);
if (!options.length) { let { list: options } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !options.length) {
options = await ncMeta.metaList2( options = await ncMeta.metaList2(
null, //, null, //,
null, //model.db_alias, null, //model.db_alias,

6
packages/nocodb/src/lib/models/Sort.ts

@ -114,8 +114,10 @@ export default class Sort {
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
): Promise<Sort[]> { ): Promise<Sort[]> {
if (!viewId) return null; if (!viewId) return null;
let sortList = await NocoCache.getList(CacheScope.SORT, [viewId]); const cachedList = await NocoCache.getList(CacheScope.SORT, [viewId]);
if (!sortList.length) { let { list: sortList } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !sortList.length) {
sortList = await ncMeta.metaList2(null, null, MetaTable.SORT, { sortList = await ncMeta.metaList2(null, null, MetaTable.SORT, {
condition: { fk_view_id: viewId }, condition: { fk_view_id: viewId },
orderBy: { orderBy: {

24
packages/nocodb/src/lib/models/View.ts

@ -205,8 +205,10 @@ export default class View implements ViewType {
} }
public static async list(modelId: string, ncMeta = Noco.ncMeta) { public static async list(modelId: string, ncMeta = Noco.ncMeta) {
let viewsList = await NocoCache.getList(CacheScope.VIEW, [modelId]); const cachedList = await NocoCache.getList(CacheScope.VIEW, [modelId]);
if (!viewsList.length) { let { list: viewsList } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !viewsList.length) {
viewsList = await ncMeta.metaList2(null, null, MetaTable.VIEWS, { viewsList = await ncMeta.metaList2(null, null, MetaTable.VIEWS, {
condition: { condition: {
fk_model_id: modelId, fk_model_id: modelId,
@ -1123,8 +1125,10 @@ export default class View implements ViewType {
); );
// get existing cache // get existing cache
const dataList = await NocoCache.getList(scope, [viewId]); const cachedList = await NocoCache.getList(scope, [viewId]);
if (dataList?.length) { const { list: dataList } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && dataList?.length) {
for (const o of dataList) { for (const o of dataList) {
if (!ignoreColdIds?.length || !ignoreColdIds.includes(o.fk_column_id)) { if (!ignoreColdIds?.length || !ignoreColdIds.includes(o.fk_column_id)) {
// set data // set data
@ -1209,7 +1213,9 @@ export default class View implements ViewType {
} }
// get existing cache // get existing cache
const dataList = await NocoCache.getList(scope, [viewId]); const cachedList = await NocoCache.getList(scope, [viewId]);
const { list: dataList } = cachedList;
const { isNoneList } = cachedList;
const colsEssentialForView = const colsEssentialForView =
view.type === ViewTypes.MAP view.type === ViewTypes.MAP
@ -1218,7 +1224,7 @@ export default class View implements ViewType {
const mergedIgnoreColdIds = [...ignoreColdIds, ...colsEssentialForView]; const mergedIgnoreColdIds = [...ignoreColdIds, ...colsEssentialForView];
if (dataList?.length) { if (!isNoneList && dataList?.length) {
for (const o of dataList) { for (const o of dataList) {
if ( if (
!mergedIgnoreColdIds?.length || !mergedIgnoreColdIds?.length ||
@ -1257,8 +1263,10 @@ export default class View implements ViewType {
} }
static async shareViewList(tableId, ncMeta = Noco.ncMeta) { static async shareViewList(tableId, ncMeta = Noco.ncMeta) {
let sharedViews = await NocoCache.getList(CacheScope.VIEW, [tableId]); const cachedList = await NocoCache.getList(CacheScope.VIEW, [tableId]);
if (!sharedViews.length) { let { list: sharedViews } = cachedList;
const { isNoneList } = cachedList;
if (!isNoneList && !sharedViews.length) {
sharedViews = await ncMeta.metaList2(null, null, MetaTable.VIEWS, { sharedViews = await ncMeta.metaList2(null, null, MetaTable.VIEWS, {
xcCondition: { xcCondition: {
fk_model_id: { fk_model_id: {

4
packages/nocodb/src/lib/plugins/vultr/Vultr.ts

@ -105,9 +105,7 @@ export default class Vultr implements IStorageAdapterV2 {
s3Options.accessKeyId = this.input.access_key; s3Options.accessKeyId = this.input.access_key;
s3Options.secretAccessKey = this.input.access_secret; s3Options.secretAccessKey = this.input.access_secret;
s3Options.endpoint = new AWS.Endpoint( s3Options.endpoint = new AWS.Endpoint(this.input.hostname);
`s3.${this.input.region}.cloud.ovh.net`
);
this.s3Client = new AWS.S3(s3Options); this.s3Client = new AWS.S3(s3Options);
} }

16
packages/nocodb/src/lib/plugins/vultr/index.ts

@ -5,7 +5,7 @@ import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = { const config: XcPluginConfig = {
builder: VultrPlugin, builder: VultrPlugin,
title: 'Vultr Object Storage', title: 'Vultr Object Storage',
version: '0.0.1', version: '0.0.2',
logo: 'plugins/vultr.png', logo: 'plugins/vultr.png',
description: description:
'Using Vultr Object Storage can give flexibility and cloud storage that allows applications greater flexibility and access worldwide.', 'Using Vultr Object Storage can give flexibility and cloud storage that allows applications greater flexibility and access worldwide.',
@ -20,13 +20,13 @@ const config: XcPluginConfig = {
type: XcType.SingleLineText, type: XcType.SingleLineText,
required: true, required: true,
}, },
// { {
// key: 'region', key: 'hostname',
// label: 'Region', label: 'Host Name',
// placeholder: 'Region', placeholder: 'e.g.: ewr1.vultrobjects.com',
// type: XcType.SingleLineText, type: XcType.SingleLineText,
// required: true required: true,
// }, },
{ {
key: 'access_key', key: 'access_key',
label: 'Access Key', label: 'Access Key',

4
packages/nocodb/src/lib/services/attachment.svc.ts

@ -30,8 +30,8 @@ export async function upload(param: {
// if `url` is null, then it is local attachment // if `url` is null, then it is local attachment
if (!url) { if (!url) {
// then store the attachement path only // then store the attachment path only
// url will be constructued in `useAttachmentCell` // url will be constructed in `useAttachmentCell`
attachmentPath = `download/${filePath.join('/')}/${fileName}`; attachmentPath = `download/${filePath.join('/')}/${fileName}`;
} }

8
packages/nocodb/src/lib/services/dbData/index.ts

@ -131,9 +131,7 @@ export async function getDataList(param: {
count = await baseModel.count(listArgs); count = await baseModel.count(listArgs);
} catch (e) { } catch (e) {
console.log(e); console.log(e);
NcError.internalServerError( NcError.internalServerError('Please check server log for more details');
'Internal Server Error, check server log for more details'
);
} }
return new PagedResponseImpl(data, { return new PagedResponseImpl(data, {
@ -642,9 +640,7 @@ export async function dataReadByViewId(param: {
); );
} catch (e) { } catch (e) {
console.log(e); console.log(e);
NcError.internalServerError( NcError.internalServerError('Please check server log for more details');
'Internal Server Error, check server log for more details'
);
} }
} }

37
packages/nocodb/src/lib/services/public/publicData.svc.ts

@ -69,10 +69,7 @@ export async function dataList(param: {
count = await baseModel.count(listArgs); count = await baseModel.count(listArgs);
} catch (e) { } catch (e) {
console.log(e); console.log(e);
// show empty result instead of throwing error here NcError.internalServerError('Please check server log for more details');
// e.g. search some text in a numeric field
NcError.internalServerError('Please try after some time');
} }
return new PagedResponseImpl(data, { ...param.query, count }); return new PagedResponseImpl(data, { ...param.query, count });
@ -235,7 +232,7 @@ export async function dataInsert(param: {
const fieldName = file?.fieldname?.replace(/^_|\[\d*]$/g, ''); const fieldName = file?.fieldname?.replace(/^_|\[\d*]$/g, '');
const filePath = sanitizeUrlPath([ const filePath = sanitizeUrlPath([
'v1', 'noco',
project.title, project.title,
model.title, model.title,
fieldName, fieldName,
@ -243,18 +240,25 @@ export async function dataInsert(param: {
if (fieldName in fields && fields[fieldName].uidt === UITypes.Attachment) { if (fieldName in fields && fields[fieldName].uidt === UITypes.Attachment) {
attachments[fieldName] = attachments[fieldName] || []; attachments[fieldName] = attachments[fieldName] || [];
const fileName = `${nanoid(6)}_${file.originalname}`; const fileName = `${nanoid(18)}${path.extname(file.originalname)}`;
let url = await storageAdapter.fileCreate(
const url = await storageAdapter.fileCreate(
slash(path.join('nc', 'uploads', ...filePath, fileName)), slash(path.join('nc', 'uploads', ...filePath, fileName)),
file file
); );
let attachmentPath;
// if `url` is null, then it is local attachment
if (!url) { if (!url) {
url = `${param.siteUrl}/download/${filePath.join('/')}/${fileName}`; // then store the attachment path only
// url will be constructed in `useAttachmentCell`
attachmentPath = `download/${filePath.join('/')}/${fileName}`;
} }
attachments[fieldName].push({ attachments[fieldName].push({
url, ...(url ? { url } : {}),
...(attachmentPath ? { path: attachmentPath } : {}),
title: file.originalname, title: file.originalname,
mimetype: file.mimetype, mimetype: file.mimetype,
size: file.size, size: file.size,
@ -287,6 +291,7 @@ export async function relDataList(param: {
} }
const column = await Column.get({ colId: param.columnId }); const column = await Column.get({ colId: param.columnId });
const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>(); const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>();
const model = await colOptions.getRelatedTable(); const model = await colOptions.getRelatedTable();
@ -299,25 +304,27 @@ export async function relDataList(param: {
dbDriver: await NcConnectionMgrv2.get(base), dbDriver: await NcConnectionMgrv2.get(base),
}); });
const { ast } = await getAst({ const { ast, dependencyFields } = await getAst({
query: param.query, query: param.query,
model, model,
extractOnlyPrimaries: true, extractOnlyPrimaries: true,
}); });
let data = []; let data = [];
let count = 0; let count = 0;
try { try {
data = data = await nocoExecute( data = data = await nocoExecute(
ast, ast,
await baseModel.list(param.query), await baseModel.list(dependencyFields),
{}, {},
param.query dependencyFields
); );
count = await baseModel.count(param.query); count = await baseModel.count(dependencyFields);
} catch (e) { } catch (e) {
// show empty result instead of throwing error here console.log(e);
// e.g. search some text in a numeric field NcError.internalServerError('Please check server log for more details');
} }
return new PagedResponseImpl(data, { ...param.query, count }); return new PagedResponseImpl(data, { ...param.query, count });

15
packages/nocodb/src/lib/services/user/index.ts

@ -458,5 +458,20 @@ export async function signup(param: {
} as any; } as any;
} }
export async function signout(param: { req: any; res: any }): Promise<any> {
try {
param.res.clearCookie('refresh_token');
const user = (param.req as any).user;
if (user) {
await User.update(user.id, {
refresh_token: null,
});
}
return { msg: 'Signed out successfully' };
} catch (e) {
NcError.badRequest(e.message);
}
}
export * from './helpers'; export * from './helpers';
export { default as initAdminFromEnv } from './initAdminFromEnv'; export { default as initAdminFromEnv } from './initAdminFromEnv';

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

@ -10886,20 +10886,7 @@
}, },
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "multipart/form-data": {}
"schema": {
"type": "object",
"description": "Data Object where the key is column and the value is the data value"
},
"examples": {
"Example 1": {
"value": {
"col1": "foo",
"col2": "bar"
}
}
}
}
}, },
"description": "" "description": ""
}, },

23
tests/playwright/pages/Dashboard/common/Cell/index.ts

@ -264,9 +264,11 @@ export class CellPageObject extends BasePage {
columnHeader, columnHeader,
count, count,
value, value,
verifyChildList = false,
}: CellProps & { }: CellProps & {
count?: number; count?: number;
value: string[]; value: string[];
verifyChildList?: boolean;
}) { }) {
// const count = value.length; // const count = value.length;
const cell = await this.get({ index, columnHeader }); const cell = await this.get({ index, columnHeader });
@ -281,6 +283,27 @@ export class CellPageObject extends BasePage {
for (let i = 0; i < value.length; ++i) { for (let i = 0; i < value.length; ++i) {
await expect(await chips.nth(i).locator('.name')).toHaveText(value[i]); await expect(await chips.nth(i).locator('.name')).toHaveText(value[i]);
} }
if (verifyChildList) {
// open child list
await this.get({ index, columnHeader }).hover();
const arrow_expand = await this.get({ index, columnHeader }).locator('.nc-arrow-expand');
// arrow expand doesn't exist for bt columns
if (await arrow_expand.count()) {
await arrow_expand.click();
// wait for child list to open
await this.rootPage.waitForSelector('.nc-modal-child-list:visible');
// verify child list count & contents
const childList = await this.rootPage.locator('.ant-card:visible');
expect(await childList.count()).toBe(count);
// close child list
await this.rootPage.locator('.nc-modal-child-list').locator('button.ant-modal-close:visible').click();
}
}
} }
async unlinkVirtualCell({ index, columnHeader }: CellProps) { async unlinkVirtualCell({ index, columnHeader }: CellProps) {

6
tests/playwright/scripts/docker-compose-pg.yml

@ -1,8 +1,8 @@
version: "2.1" version: "2.2"
services: services:
pg96: pg147:
image: postgres:9.6 image: postgres:14.7
restart: always restart: always
environment: environment:
POSTGRES_PASSWORD: password POSTGRES_PASSWORD: password

4
tests/playwright/scripts/docker-compose-playwright-pg.yml

@ -1,8 +1,8 @@
version: "2.1" version: "2.1"
services: services:
pg96: pg147:
image: postgres:15 image: postgres:14.7
restart: always restart: always
environment: environment:
POSTGRES_PASSWORD: password POSTGRES_PASSWORD: password

12
tests/playwright/tests/columnFormula.spec.ts

@ -122,6 +122,18 @@ const formulaDataByDbType = (context: NcContext) => [
formula: `NOW()`, formula: `NOW()`,
result: ['1', '1', '1', '1', '1'], result: ['1', '1', '1', '1', '1'],
}, },
{
formula: `OR(true, false)`,
result: isPg(context) ? ['true', 'true', 'true', 'true', 'true'] : ['1', '1', '1', '1', '1'],
},
{
formula: `AND(false, false)`,
result: isPg(context) ? ['false', 'false', 'false', 'false', 'false'] : ['0', '0', '0', '0', '0'],
},
{
formula: `IF((SEARCH({Address List}, "Parkway") != 0), "2.0","WRONG")`,
result: ['WRONG', 'WRONG', 'WRONG', '2.0', '2.0'],
},
]; ];
test.describe('Virtual Columns', () => { test.describe('Virtual Columns', () => {

63
tests/playwright/tests/megaTable.spec.ts

@ -157,4 +157,67 @@ test.describe.serial('Test table', () => {
await page.reload(); await page.reload();
}); });
test.skip('mega table - LinkToAnotherRecord', async ({ page }) => {
let table_1, table_2;
const columns = [];
// a Primary key column & display column
columns.push(
{
column_name: 'Id',
title: 'Id',
uidt: UITypes.ID,
},
{
column_name: 'SingleLineText',
title: 'SingleLineText',
uidt: UITypes.SingleLineText,
pv: true,
}
);
const project = await api.project.read(context.project.id);
// eslint-disable-next-line prefer-const
table_1 = await api.base.tableCreate(context.project.id, project.bases?.[0].id, {
table_name: 'table_1',
title: 'table_1',
columns: columns,
});
// eslint-disable-next-line prefer-const
table_2 = await api.base.tableCreate(context.project.id, project.bases?.[0].id, {
table_name: 'table_2',
title: 'table_2',
columns: columns,
});
const rows = [];
for (let i = 0; i < 1000; i++) {
rows.push({
Id: i + 1,
SingleLineText: `SingleLineText${i + 1}`,
});
}
await api.dbTableRow.bulkCreate('noco', context.project.id, table_1.id, rows);
await api.dbTableRow.bulkCreate('noco', context.project.id, table_2.id, rows);
await api.dbTableColumn.create(table_2.id, {
uidt: UITypes.LinkToAnotherRecord,
title: 'LinkToAnotherRecord',
column_name: 'LinkToAnotherRecord',
parentId: table_1.id,
childId: table_2.id,
type: 'hm',
});
// // nested add : hm
// for (let i = 1; i <= 1000; i++) {
// await api.dbTableRow.nestedAdd('noco', project.title, table_1.table_name, i, 'hm', 'LinkToAnotherRecord', `${i}`);
// }
// nested add : bt
for (let i = 1; i <= 1000; i++) {
await api.dbTableRow.nestedAdd('noco', project.title, table_2.table_name, i, 'bt', 'table_1', `${i}`);
}
});
}); });

2
tests/playwright/tests/viewGridShare.spec.ts

@ -90,7 +90,7 @@ test.describe('Shared view', () => {
// verify virtual records // verify virtual records
for (const record of expectedVirtualRecordsByDb) { for (const record of expectedVirtualRecordsByDb) {
await sharedPage.grid.cell.verifyVirtualCell(record); await sharedPage.grid.cell.verifyVirtualCell({ ...record, verifyChildList: true });
} }
/** /**

Loading…
Cancel
Save