Browse Source

Merge branch 'develop' into fix/runtime-directive-warnings

pull/6954/head
աɨռɢӄաօռɢ 10 months ago
parent
commit
629a06304c
  1. 2
      .github/uffizzi/docker-compose.uffizzi.yml
  2. 2
      README.md
  3. 2
      docker-compose/mysql/docker-compose.yml
  4. 2
      docker-compose/nginx-proxy-manager/docker-compose.yml
  5. 2
      package.json
  6. 4
      packages/nc-gui/assets/style.scss
  7. 3
      packages/nc-gui/components.d.ts
  8. 8
      packages/nc-gui/components/cell/Json.vue
  9. 12
      packages/nc-gui/components/cell/TextArea.vue
  10. 125
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  11. 7
      packages/nc-gui/components/dlg/AirtableImport.vue
  12. 14
      packages/nc-gui/components/dlg/ColumnDuplicate.vue
  13. 14
      packages/nc-gui/components/dlg/QuickImport.vue
  14. 2
      packages/nc-gui/components/smartsheet/Cell.vue
  15. 44
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  16. 11
      packages/nc-gui/components/smartsheet/details/Fields.vue
  17. 17
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  18. 39
      packages/nc-gui/components/smartsheet/grid/GroupBy.vue
  19. 28
      packages/nc-gui/components/smartsheet/grid/GroupByLabel.vue
  20. 22
      packages/nc-gui/components/smartsheet/header/Menu.vue
  21. 50
      packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
  22. 85
      packages/nc-gui/components/template/Editor.vue
  23. 6
      packages/nc-gui/components/virtual-cell/Lookup.vue
  24. 8
      packages/nc-gui/components/virtual-cell/components/Header.vue
  25. 2
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  26. 69
      packages/nc-gui/components/virtual-cell/components/ListItems.vue
  27. 10
      packages/nc-gui/composables/useColumnCreateStore.ts
  28. 10
      packages/nc-gui/composables/useViewGroupBy.ts
  29. 5
      packages/nc-gui/lang/en.json
  30. 2
      packages/nc-gui/lib/enums.ts
  31. 102
      packages/nc-gui/package.json
  32. 11
      packages/nc-gui/store/sidebar.ts
  33. 22
      packages/nc-gui/utils/validation.ts
  34. 2
      packages/nc-lib-gui/package.json
  35. 2
      packages/noco-docs/docs/030.workspaces/040.actions-on-workspace.md
  36. 2
      packages/noco-docs/docs/040.bases/070.actions-on-base.md
  37. 2
      packages/noco-docs/docs/050.tables/060.actions-on-table.md
  38. 12
      packages/noco-docs/docs/070.fields/040.field-types/050.custom-types/010.attachment.md
  39. 2
      packages/noco-docs/docs/070.fields/040.field-types/060.formula/015.operators.md
  40. 16
      packages/noco-docs/docs/070.fields/040.field-types/060.formula/040.date-functions.md
  41. 2
      packages/noco-docs/docs/070.fields/060.actions-on-field.md
  42. 4
      packages/noco-docs/docs/080.records/070.actions-on-record.md
  43. 4
      packages/noco-docs/docs/090.views/040.view-types/030.form.md
  44. 2
      packages/noco-docs/docs/090.views/090.actions-on-view.md
  45. 6
      packages/noco-docs/docs/150.engineering/060.builds-and-releases.md
  46. 2
      packages/noco-docs/docs/150.engineering/070.translation.md
  47. 7598
      packages/noco-docs/package-lock.json
  48. 30
      packages/noco-docs/package.json
  49. 18
      packages/noco-docs/versioned_docs/version-0.109.7/030.setup-and-usages/090.formulas.md
  50. 14
      packages/noco-docs/versioned_docs/version-0.109.7/030.setup-and-usages/160.views.md
  51. 6
      packages/noco-docs/versioned_docs/version-0.109.7/030.setup-and-usages/200.import-airtable-to-sql-database-within-a-minute-for-free.md
  52. 4
      packages/noco-docs/versioned_docs/version-0.109.7/030.setup-and-usages/230.team-and-auth.md
  53. 246
      packages/noco-docs/versioned_docs/version-0.109.7/040.developer-resources/020.rest-apis.md
  54. 2
      packages/noco-docs/versioned_docs/version-0.109.7/040.developer-resources/030.sdk.md
  55. 18
      packages/noco-docs/versioned_docs/version-0.109.7/040.developer-resources/040.webhooks.md
  56. 12
      packages/nocodb-sdk/package.json
  57. 123
      packages/nocodb/package.json
  58. 39
      packages/nocodb/src/controllers/api-docs/api-docs.controller.ts
  59. 66
      packages/nocodb/src/db/BaseModelSqlv2.ts
  60. 46
      packages/nocodb/src/db/conditionV2.ts
  61. 163
      packages/nocodb/src/db/generateBTLookupSelectQuery.ts
  62. 399
      packages/nocodb/src/db/generateLookupSelectQuery.ts
  63. 4
      packages/nocodb/src/db/sortV2.ts
  64. 2
      packages/nocodb/src/helpers/catchError.ts
  65. 9
      packages/nocodb/src/helpers/getAst.ts
  66. 7
      packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts
  67. 20
      packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts
  68. 11
      packages/nocodb/src/modules/jobs/jobs/export-import/export.service.ts
  69. 1
      packages/nocodb/src/plugins/backblaze/Backblaze.ts
  70. 1
      packages/nocodb/src/plugins/gcs/Gcs.ts
  71. 1
      packages/nocodb/src/plugins/linode/LinodeObjectStorage.ts
  72. 1
      packages/nocodb/src/plugins/mino/Minio.ts
  73. 1
      packages/nocodb/src/plugins/ovhCloud/OvhCloud.ts
  74. 12
      packages/nocodb/src/plugins/s3/S3.ts
  75. 1
      packages/nocodb/src/plugins/scaleway/ScalewayObjectStorage.ts
  76. 1
      packages/nocodb/src/plugins/spaces/Spaces.ts
  77. 1
      packages/nocodb/src/plugins/upcloud/UpoCloud.ts
  78. 1
      packages/nocodb/src/plugins/vultr/Vultr.ts
  79. 30
      packages/nocodb/src/services/api-docs/api-docs.service.ts
  80. 2
      packages/nocodb/src/services/api-docs/swagger/templates/paths.ts
  81. 28
      packages/nocodb/src/services/api-docs/swaggerV2/getPaths.ts
  82. 29
      packages/nocodb/src/services/api-docs/swaggerV2/getSchemas.ts
  83. 67
      packages/nocodb/src/services/api-docs/swaggerV2/getSwaggerColumnMetas.ts
  84. 66
      packages/nocodb/src/services/api-docs/swaggerV2/getSwaggerJSONV2.ts
  85. 128
      packages/nocodb/src/services/api-docs/swaggerV2/swagger-base.json
  86. 10
      packages/nocodb/src/services/api-docs/swaggerV2/templates/headers.ts
  87. 237
      packages/nocodb/src/services/api-docs/swaggerV2/templates/params.ts
  88. 407
      packages/nocodb/src/services/api-docs/swaggerV2/templates/paths.ts
  89. 109
      packages/nocodb/src/services/api-docs/swaggerV2/templates/schemas.ts
  90. 3
      packages/nocodb/src/services/datas.service.ts
  91. 4
      packages/nocodb/src/services/tables.service.ts
  92. 5
      packages/nocodb/src/utils/globals.ts
  93. 10
      packages/nocodb/tests/unit/rest/tests/groupby.test.ts
  94. 3496
      pnpm-lock.yaml
  95. 3
      renovate.json
  96. 8
      scripts/pkg-executable/package.json
  97. 6
      tests/playwright/pages/Dashboard/Import/ImportTemplate.ts

2
.github/uffizzi/docker-compose.uffizzi.yml

@ -31,7 +31,7 @@ services:
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
MYSQL_USER: noco
image: "mysql:8.0.32"
image: "mysql:8.0.35"
deploy:
resources:
limits:

2
README.md

@ -64,7 +64,7 @@ Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart spreadshe
</a>
-->
[![Stargazers repo roster for @nocodb/nocodb](https://reporoster.com/stars/nocodb/nocodb)](https://github.com/nocodb/nocodb/stargazers)
[![Stargazers repo roster for @nocodb/nocodb](http://reporoster.com/stars/nocodb/nocodb)](https://github.com/nocodb/nocodb/stargazers)
# Quick try

2
docker-compose/mysql/docker-compose.yml

@ -27,7 +27,7 @@ services:
- "-h"
- localhost
timeout: 20s
image: "mysql:8.0.32"
image: "mysql:8.0.35"
restart: always
volumes:
- "db_data:/var/lib/mysql"

2
docker-compose/nginx-proxy-manager/docker-compose.yml

@ -46,7 +46,7 @@ services:
- "-h"
- localhost
timeout: 20s
image: "mysql:8.0.32"
image: "mysql:8.0.35"
networks:
- default
restart: always

2
package.json

@ -18,7 +18,7 @@
"devDependencies": {
"fs": "0.0.1-security",
"lerna": "^7.0.2",
"husky": "^8.0.0",
"husky": "^8.0.3",
"xlsx": "^0.17.4"
},
"husky": {

4
packages/nc-gui/assets/style.scss

@ -678,3 +678,7 @@ input[type='number'] {
@apply xs:(visible opacity-100 !text-gray-500)
}
}
.ant-message-notice-content {
@apply !rounded-md;
}

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

@ -26,7 +26,6 @@ declare module '@vue/runtime-core' {
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
ADivider: typeof import('ant-design-vue/es')['Divider']
ADrawer: typeof import('ant-design-vue/es')['Drawer']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
ADropdownButton: typeof import('ant-design-vue/es')['DropdownButton']
AEmpty: typeof import('ant-design-vue/es')['Empty']
@ -47,7 +46,6 @@ declare module '@vue/runtime-core' {
AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AMenuItemGroup: typeof import('ant-design-vue/es')['MenuItemGroup']
AModal: typeof import('ant-design-vue/es')['Modal']
APagination: typeof import('ant-design-vue/es')['Pagination']
APopover: typeof import('ant-design-vue/es')['Popover']
@ -64,7 +62,6 @@ declare module '@vue/runtime-core' {
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table']
ATableColumn: typeof import('ant-design-vue/es')['TableColumn']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']

8
packages/nc-gui/components/cell/Json.vue

@ -72,13 +72,7 @@ const clear = () => {
const formatJson = (json: string) => {
try {
json = json
.trim()
.replace(/^\{\s*|\s*\}$/g, '')
.replace(/\n\s*/g, '')
json = `{${json}}`
return json
return JSON.stringify(JSON.parse(json))
} catch (e) {
console.log(e)
return json

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

@ -16,6 +16,7 @@ import {
const props = defineProps<{
modelValue?: string | number
isFocus?: boolean
virtual?: boolean
}>()
const emits = defineEmits(['update:modelValue'])
@ -65,6 +66,13 @@ onClickOutside(inputWrapperRef, (e) => {
isVisible.value = false
})
const onDblClick = () => {
if (!props.virtual) return
isVisible.value = true
editEnabled.value = true
}
</script>
<template>
@ -113,7 +121,9 @@ onClickOutside(inputWrapperRef, (e) => {
class="mr-7 nc-text-area-clamped-text"
:style="{
'word-break': 'break-word',
'white-space': 'pre-line',
}"
@click="onDblClick"
/>
<span v-else>{{ vModel }}</span>
@ -148,7 +158,7 @@ onClickOutside(inputWrapperRef, (e) => {
<a-textarea
ref="inputRef"
v-model:value="vModel"
class="p-1 !pt-1 !pr-3 !border-0 !border-r-0 !focus:outline-transparent nc-scrollbar-md !text-black"
class="p-1 !pt-1 !pr-3 !border-0 !border-r-0 !focus:outline-transparent nc-scrollbar-md !text-black !cursor-text"
:placeholder="$t('activity.enterText')"
:bordered="false"
:auto-size="{ minRows: 20, maxRows: 20 }"

125
packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue

@ -77,7 +77,7 @@ const tempTitle = ref('')
const activeBaseId = ref('')
const isErdModalOpen = ref<Boolean>(false)
const isErdModalOpen = ref<boolean>(false)
const { t } = useI18n()
@ -116,7 +116,7 @@ const showBaseOption = computed(() => {
return ['airtableImport', 'csvImport', 'jsonImport', 'excelImport'].some((permission) => isUIAllowed(permission))
})
const enableEditMode = () => {
function enableEditMode() {
editMode.value = true
tempTitle.value = base.value.title!
nextTick(() => {
@ -126,7 +126,7 @@ const enableEditMode = () => {
})
}
const updateProjectTitle = async () => {
async function updateProjectTitle() {
if (!tempTitle.value) return
try {
@ -139,14 +139,15 @@ const updateProjectTitle = async () => {
$e('a:base:rename')
useTitle(`${base.value?.title}`)
} catch (e: any) {
}
catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const { copy } = useCopy(true)
const copyProjectInfo = async () => {
async function copyProjectInfo() {
try {
if (
await copy(
@ -158,7 +159,8 @@ const copyProjectInfo = async () => {
// Copied to clipboard
message.info(t('msg.info.copiedToClipboard'))
}
} catch (e: any) {
}
catch (e: any) {
console.error(e)
message.error(e.message)
}
@ -168,7 +170,7 @@ defineExpose({
enableEditMode,
})
const setIcon = async (icon: string, base: BaseType) => {
async function setIcon(icon: string, base: BaseType) {
try {
const meta = {
...((base.meta as object) || {}),
@ -178,7 +180,8 @@ const setIcon = async (icon: string, base: BaseType) => {
basesStore.updateProject(base.id!, { meta: JSON.stringify(meta) })
$e('a:base:icon:navdraw', { icon })
} catch (e: any) {
}
catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
@ -224,7 +227,7 @@ function openTableCreateDialog(sourceIndex?: number | undefined) {
}
const isAddNewProjectChildEntityLoading = ref(false)
const addNewProjectChildEntity = async () => {
async function addNewProjectChildEntity() {
if (isAddNewProjectChildEntityLoading.value) return
isAddNewProjectChildEntityLoading.value = true
@ -243,12 +246,13 @@ const addNewProjectChildEntity = async () => {
if (!base.value.isExpanded && base.value.type !== NcProjectType.DB) {
base.value.isExpanded = true
}
} finally {
}
finally {
isAddNewProjectChildEntityLoading.value = false
}
}
const onProjectClick = async (base: NcProject, ignoreNavigation?: boolean, toggleIsExpanded?: boolean) => {
async function onProjectClick(base: NcProject, ignoreNavigation?: boolean, toggleIsExpanded?: boolean) {
if (!base) {
return
}
@ -260,7 +264,8 @@ const onProjectClick = async (base: NcProject, ignoreNavigation?: boolean, toggl
if (toggleIsExpanded) {
base.isExpanded = !base.isExpanded
} else {
}
else {
base.isExpanded = true
}
@ -310,7 +315,8 @@ function openErdView(source: SourceType) {
const contextMenuBase = computed(() => {
if (contextMenuTarget.type === 'source') {
return contextMenuTarget.value
} else if (contextMenuTarget.type === 'table') {
}
else if (contextMenuTarget.type === 'table') {
const source = base.value?.sources?.find((b) => b.id === contextMenuTarget.value.source_id)
if (source) return source
}
@ -347,17 +353,17 @@ onKeyStroke('Escape', () => {
const isDuplicateDlgOpen = ref(false)
const selectedProjectToDuplicate = ref()
const duplicateProject = (base: BaseType) => {
function duplicateProject(base: BaseType) {
selectedProjectToDuplicate.value = base
isDuplicateDlgOpen.value = true
}
const tableDelete = () => {
function tableDelete() {
isTableDeleteDialogVisible.value = true
$e('c:table:delete')
}
const projectDelete = () => {
function projectDelete() {
isProjectDeleteDialogVisible.value = true
$e('c:project:delete')
}
@ -449,15 +455,78 @@ const projectDelete = () => {
<GeneralIcon icon="threeDotHorizontal" class="text-xl w-4.75" />
</NcButton>
<template #overlay>
<NcMenu
class="nc-scrollbar-md"
:style="{
maxHeight: '70vh',
overflow: 'overlay',
}"
:data-testid="`nc-sidebar-base-${base.title}-options`"
@click="isOptionsOpen = false"
<NcMenuItem
v-if="isUIAllowed('baseDuplicate', { roles: [stringifyRolesObj(orgRoles), baseRole].join() })"
data-testid="nc-sidebar-base-duplicate"
@click="duplicateProject(base)"
>
<div v-e="['c:base:duplicate']" class="flex gap-2 items-center">
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }}
</div>
</NcMenuItem>
<NcDivider v-if="['baseDuplicate', 'baseRename'].some((permission) => isUIAllowed(permission))" />
<!-- Copy Project Info -->
<NcMenuItem
v-if="!isEeUI"
key="copy"
data-testid="nc-sidebar-base-copy-base-info"
@click.stop="copyProjectInfo"
>
<div v-e="['c:base:copy-proj-info']" class="flex gap-2 items-center">
<GeneralIcon icon="copy" class="group-hover:text-black" />
{{ $t('activity.account.projInfo') }}
</div>
</NcMenuItem>
<!-- ERD View -->
<NcMenuItem
key="erd"
data-testid="nc-sidebar-base-relations"
@click="openErdView(base?.sources?.[0]!)"
>
<div v-e="['c:base:erd']" class="flex gap-2 items-center">
<GeneralIcon icon="erd" />
{{ $t('title.relations') }}
</div>
</NcMenuItem>
<!-- Swagger: Rest APIs -->
<NcMenuItem
v-if="isUIAllowed('apiDocs')"
key="api"
v-e="['c:base:api-docs']"
data-testid="nc-sidebar-base-rest-apis"
@click.stop="
() => {
$e('c:base:api-docs')
openLink(`/api/v2/meta/bases/${base.id}/swagger`, appInfo.ncSiteUrl)
}
"
>
<div v-e="['c:base:api-docs']" class="flex gap-2 items-center">
<GeneralIcon icon="snippet" class="group-hover:text-black !max-w-3.9" />
{{ $t('activity.account.swagger') }}
</div>
</NcMenuItem>
</template>
<template v-if="base.sources && base.sources[0] && showBaseOption">
<NcDivider />
<DashboardTreeViewBaseOptions v-model:base="base" :source="base.sources[0]" />
</template>
<NcDivider v-if="['baseMiscSettings', 'baseDelete'].some((permission) => isUIAllowed(permission))" />
<NcMenuItem
v-if="isUIAllowed('baseMiscSettings')"
key="teamAndSettings"
v-e="['c:base:settings']"
data-testid="nc-sidebar-base-settings"
class="nc-sidebar-base-base-settings"
@click="toggleDialog(true, 'teamAndAuth', undefined, base.id)"
>
<template v-if="!isSharedBase">
<NcMenuItem v-if="isUIAllowed('baseRename')" data-testid="nc-sidebar-project-rename" @click="enableEditMode">
@ -610,6 +679,7 @@ const projectDelete = () => {
:class="{ '!rotate-180': isActive }"
/>
</div>
</template>
<a-collapse-panel :key="`collapse-${source.id}`">
<template #header>
@ -703,6 +773,7 @@ const projectDelete = () => {
<DashboardTreeViewTableList :base="base" :source-index="sourceIndex" />
</div>
</a-collapse-panel>
</a-collapse>
</div>
</div>
@ -743,6 +814,7 @@ const projectDelete = () => {
</template>
</NcMenu>
</template>
</NcDropdown>
<DlgTableDelete
v-if="contextMenuTarget.value?.id && base?.id"
@ -750,8 +822,11 @@ const projectDelete = () => {
:table-id="contextMenuTarget.value?.id"
:base-id="base?.id"
/>
<DlgProjectDelete v-model:visible="isProjectDeleteDialogVisible" :base-id="base?.id" />
<DlgProjectDuplicate v-if="selectedProjectToDuplicate" v-model="isDuplicateDlgOpen" :base="selectedProjectToDuplicate" />
<GeneralModal v-model:visible="isErdModalOpen" size="large">
<div class="h-[80vh]">
<LazyDashboardSettingsErd :source-id="activeBaseId" />

7
packages/nc-gui/components/dlg/AirtableImport.vue

@ -90,8 +90,10 @@ const onStatus = async (status: JobStatus, data?: any) => {
refreshCommandPalette()
// TODO: add tab of the first table
} else if (status === JobStatus.FAILED) {
await loadTables()
goBack.value = true
pushProgress(data.error.message, status)
refreshCommandPalette()
}
}
@ -115,7 +117,10 @@ const { validateInfos } = useForm(syncSource, validators)
const disableImportButton = computed(() => !syncSource.value.details.apiKey || !syncSource.value.details.syncSourceUrlOrId)
const isLoading = ref(false)
async function saveAndSync() {
isLoading.value = true
await createOrUpdate()
await sync()
}
@ -178,6 +183,7 @@ async function listenForUpdates(id?: string) {
}
} else {
listeningForUpdates.value = false
isLoading.value = false
}
},
)
@ -494,6 +500,7 @@ onMounted(async () => {
v-e="['c:sync-airtable:save-and-sync']"
type="primary"
class="nc-btn-airtable-import"
:loading="isLoading"
:disabled="disableImportButton"
@click="saveAndSync"
>

14
packages/nc-gui/components/dlg/ColumnDuplicate.vue

@ -17,10 +17,6 @@ const dialogShow = useVModel(props, 'modelValue', emit)
const { $e, $poller } = useNuxtApp()
const basesStore = useBases()
const { createProject: _createProject } = basesStore
const { activeTable: _activeTable } = storeToRefs(useTablesStore())
const reloadDataHook = inject(ReloadViewDataHookInj)
@ -101,7 +97,9 @@ onKeyStroke('Enter', () => {
}
})
const isEaster = ref(false)
defineExpose({
duplicate: _duplicate,
})
</script>
<template>
@ -118,11 +116,9 @@ const isEaster = ref(false)
@keydown.esc="dialogShow = false"
>
<div>
<div class="prose-xl font-bold self-center" @dblclick="isEaster = !isEaster">
{{ $t('general.duplicate') }} {{ $t('objects.column') }}
</div>
<div class="prose-xl font-bold self-center">{{ $t('general.duplicate') }} {{ $t('objects.column') }}</div>
<div class="mt-4">{{ $t('msg.warning.duplicateProject') }}</div>
<div class="mt-4">Are you sure you want to duplicate the field?</div>
<div class="prose-md self-center text-gray-500 mt-4">{{ $t('title.advancedSettings') }}</div>

14
packages/nc-gui/components/dlg/QuickImport.vue

@ -172,7 +172,9 @@ const disablePreImportButton = computed(() => {
}
})
const disableImportButton = computed(() => !templateEditorRef.value?.isValid)
const isError = ref(false)
const disableImportButton = computed(() => !templateEditorRef.value?.isValid || isError.value)
const disableFormatJsonButton = computed(() => !jsonEditorRef.value?.isValid)
@ -530,6 +532,14 @@ async function parseAndExtractData(val: UploadFile[] | ArrayBuffer | string) {
preImportLoading.value = false
}
}
const onError = () => {
isError.value = true
}
const onChange = () => {
isError.value = false
}
</script>
<template>
@ -558,6 +568,8 @@ async function parseAndExtractData(val: UploadFile[] | ArrayBuffer | string) {
:import-worker="importWorker"
class="nc-quick-import-template-editor"
@import="handleImport"
@error="onError"
@change="onChange"
/>
<a-tabs v-else v-model:activeKey="activeKey" hide-add type="editable-card" tab-position="top">

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

@ -216,7 +216,7 @@ onUnmounted(() => {
>
<template v-if="column">
<template v-if="intersected">
<LazyCellTextArea v-if="isTextArea(column)" v-model="vModel" />
<LazyCellTextArea v-if="isTextArea(column)" v-model="vModel" :virtual="props.virtual" />
<LazyCellGeoData v-else-if="isGeoData(column)" v-model="vModel" />
<LazyCellCheckbox v-else-if="isBoolean(column, abstractType)" v-model="vModel" />
<LazyCellAttachment v-else-if="isAttachment(column)" v-model="vModel" :row-index="props.rowIndex" />

44
packages/nc-gui/components/smartsheet/column/SelectOptions.vue

@ -29,7 +29,7 @@ const { optionsMagic: _optionsMagic } = useNocoEe()
const optionsWrapperDomRef = ref<HTMLElement>()
const options = ref<(Option & { status?: 'remove' })[]>([])
const options = ref<(Option & { status?: 'remove'; index?: number })[]>([])
const isAddingOption = ref(false)
@ -38,7 +38,7 @@ const OPTIONS_PAGE_COUNT = 20
const loadedOptionAnchor = ref(OPTIONS_PAGE_COUNT)
const isReverseLazyLoad = ref(false)
const renderedOptions = ref<(Option & { status?: 'remove' })[]>([])
const renderedOptions = ref<(Option & { status?: 'remove'; index?: number })[]>([])
const savedDefaultOption = ref<Option | null>(null)
const savedCdf = ref<string | null>(null)
@ -98,6 +98,12 @@ onMounted(() => {
options.value = vModel.value.colOptions.options
let indexCounter = 0
options.value.map((el) => {
el.index = indexCounter++
return el
})
loadedOptionAnchor.value = Math.min(loadedOptionAnchor.value, options.value.length)
renderedOptions.value = [...options.value].slice(0, loadedOptionAnchor.value)
@ -135,6 +141,7 @@ const addNewOption = () => {
const tempOption = {
title: '',
color: getNextColor(),
index: options.value.length,
}
options.value.push(tempOption)
@ -168,11 +175,30 @@ const addNewOption = () => {
// }
const syncOptions = () => {
vModel.value.colOptions.options = renderedOptions.value.filter((op) => op.status !== 'remove')
vModel.value.colOptions.options = options.value
.filter((op) => op.status !== 'remove')
.sort((a, b) => {
const renderA = renderedOptions.value.findIndex((el) => a.index !== undefined && el.index === a.index)
const renderB = renderedOptions.value.findIndex((el) => a.index !== undefined && el.index === b.index)
if (renderA === -1 || renderB === -1) return 0
return renderA - renderB
})
.map((op) => {
const { index: _i, status: _s, ...rest } = op
return rest
})
}
const removeRenderedOption = (index: number) => {
renderedOptions.value[index].status = 'remove'
const renderedOption = renderedOptions.value[index]
if (renderedOption.index === undefined || isNaN(renderedOption.index)) return
const option = options.value[renderedOption.index]
renderedOption.status = 'remove'
option.status = 'remove'
syncOptions()
const optionId = renderedOptions.value[index]?.id
@ -193,7 +219,15 @@ const optionChanged = (changedId: string) => {
}
const undoRemoveRenderedOption = (index: number) => {
renderedOptions.value[index].status = undefined
const renderedOption = renderedOptions.value[index]
if (renderedOption.index === undefined || isNaN(renderedOption.index)) return
const option = options.value[renderedOption.index]
renderedOption.status = undefined
option.status = undefined
syncOptions()
const optionId = renderedOptions.value[index]?.id

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

@ -1,7 +1,7 @@
<script setup lang="ts">
import { diff } from 'deep-object-diff'
import { message } from 'ant-design-vue'
import { UITypes, isSystemColumn } from 'nocodb-sdk'
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import Draggable from 'vuedraggable'
import { onKeyDown, useMagicKeys } from '@vueuse/core'
import type { ColumnType, SelectOptionsType } from 'nocodb-sdk'
@ -774,8 +774,15 @@ const onFieldOptionUpdate = () => {
"
/>
<NcCheckbox v-else :disabled="true" class="opacity-0" :checked="true" />
<SmartsheetHeaderVirtualCellIcon
v-if="field && isVirtualCol(fieldState(field) || field)"
:column-meta="fieldState(field) || field"
:class="{
'text-brand-500': compareCols(field, activeField),
}"
/>
<SmartsheetHeaderCellIcon
v-if="field"
v-else
:column-meta="fieldState(field) || field"
:class="{
'text-brand-500': compareCols(field, activeField),

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

@ -41,11 +41,13 @@ interface Props {
showNextPrevIcons?: boolean
firstRow?: boolean
lastRow?: boolean
closeAfterSave?: boolean
newRecordHeader?: string
}
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue', 'cancel', 'next', 'prev'])
const emits = defineEmits(['update:modelValue', 'cancel', 'next', 'prev', 'createdRecord'])
const { activeView } = storeToRefs(useViewsStore())
@ -201,6 +203,12 @@ const save = async () => {
reloadTrigger?.trigger()
}
isUnsavedFormExist.value = false
if (props.closeAfterSave) {
isExpanded.value = false
}
emits('createdRecord', _row.value.row)
}
const isPreventChangeModalOpen = ref(false)
@ -486,12 +494,17 @@ export default {
<div v-if="isLoading">
<a-skeleton-input class="!h-8 !sm:mr-14 !w-52 mt-1 !rounded-md !overflow-hidden" active size="small" />
</div>
<div
v-if="row.rowMeta?.new || props.newRecordHeader"
class="flex items-center truncate font-bold text-gray-800 text-xl"
>
{{ props.newRecordHeader ?? $t('activity.newRecord') }}
</div>
<div v-else-if="displayValue && !row.rowMeta?.new" class="flex items-center font-bold text-gray-800 text-xl w-64">
<span class="truncate">
{{ displayValue }}
</span>
</div>
<div v-if="row.rowMeta?.new" class="flex items-center truncate font-bold text-gray-800 text-xl">New Record</div>
</div>
<div class="flex gap-2">
<NcButton

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

@ -1,8 +1,10 @@
<script lang="ts" setup>
import tinycolor from 'tinycolor2'
import { UITypes } from 'nocodb-sdk'
import Table from './Table.vue'
import GroupBy from './GroupBy.vue'
import GroupByTable from './GroupByTable.vue'
import GroupByLabel from './GroupByLabel.vue'
import { GROUP_BY_VARS, computed, ref } from '#imports'
import type { Group, Row } from '#imports'
@ -134,6 +136,27 @@ const onScroll = (e: Event) => {
if (!vGroup.value.root) return
_scrollLeft.value = (e.target as HTMLElement).scrollLeft
}
// a method to parse group key if grouped column type is LTAR or Lookup
// in these 2 scenario it will return json array or `___` separated value
const parseKey = (group) => {
const key = group.key.toString()
// parse json array key if it's a lookup or link to another record
if ((key && group.column?.uidt === UITypes.Lookup) || group.column?.uidt === UITypes.LinkToAnotherRecord) {
try {
const parsedKey = JSON.parse(key)
return parsedKey
} catch {
// if parsing try to split it by `___` (for sqlite)
return key.split('___')
}
}
return [key]
}
const shouldRenderCell = (column) =>
[UITypes.Lookup, UITypes.Attachment, UITypes.Barcode, UITypes.QrCode, UITypes.Links].includes(column?.uidt)
</script>
<template>
@ -227,6 +250,15 @@ const onScroll = (e: Event) => {
</span>
</a-tag>
</template>
<div
v-else-if="!(grp.key in GROUP_BY_VARS.VAR_TITLES) && shouldRenderCell(grp.column)"
class="flex min-w-[100px] flex-wrap"
>
<template v-for="(val, ind) of parseKey(grp)" :key="ind">
<GroupByLabel v-if="val" :column="grp.column" :model-value="val" />
<span v-else class="text-gray-400">No mapped value</span>
</template>
</div>
<a-tag
v-else
:key="`panel-tag-${grp.column.id}-${grp.key}`"
@ -247,7 +279,12 @@ const onScroll = (e: Event) => {
'font-weight': 500,
}"
>
{{ grp.key in GROUP_BY_VARS.VAR_TITLES ? GROUP_BY_VARS.VAR_TITLES[grp.key] : grp.key }}
<template v-if="grp.key in GROUP_BY_VARS.VAR_TITLES">{{
GROUP_BY_VARS.VAR_TITLES[grp.key]
}}</template>
<template v-else>
{{ parseKey(grp)?.join(', ') }}
</template>
</span>
</a-tag>
</div>

28
packages/nc-gui/components/smartsheet/grid/GroupByLabel.vue

@ -0,0 +1,28 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { isVirtualCol } from 'nocodb-sdk'
defineProps<{
column: ColumnType
modelValue: any
}>()
provide(ReadonlyInj, true)
</script>
<template>
<div class="pointer-events-none">
<LazySmartsheetRow :row="{ row: { [column.title]: modelValue }, rowMeta: {} }">
<LazySmartsheetVirtualCell v-if="isVirtualCol(column)" :model-value="modelValue" class="!text-gray-600" :column="column" />
<LazySmartsheetCell
v-else
:model-value="modelValue"
class="!text-gray-600"
:column="column"
:edit-enabled="false"
:read-only="true"
/>
</LazySmartsheetRow>
</div>
</template>

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

@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { ColumnReqType, ColumnType } from 'nocodb-sdk'
import type { ColumnReqType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR } from 'nocodb-sdk'
import {
ActiveViewInj,
@ -111,8 +111,8 @@ const sortByColumn = async (direction: 'asc' | 'desc') => {
}
const isDuplicateDlgOpen = ref(false)
const selectedColumnToDuplicate = ref<ColumnType>()
const selectedColumnExtra = ref<any>()
const duplicateDialogRef = ref<any>()
const duplicateVirtualColumn = async () => {
let columnCreatePayload = {}
@ -165,7 +165,7 @@ const duplicateVirtualColumn = async () => {
const openDuplicateDlg = async () => {
if (!column?.value) return
if (column.value.uidt && [UITypes.Formula, UITypes.Lookup, UITypes.Rollup].includes(column.value.uidt as UITypes)) {
if (column.value.uidt && [UITypes.Lookup, UITypes.Rollup].includes(column.value.uidt as UITypes)) {
duplicateVirtualColumn()
} else {
const gridViewColumnList = (await $api.dbViewColumn.list(view.value?.id as string)).list
@ -186,8 +186,15 @@ const openDuplicateDlg = async () => {
view_id: view.value!.id as string,
},
}
selectedColumnToDuplicate.value = column.value
isDuplicateDlgOpen.value = true
if (column.value.uidt === UITypes.Formula) {
nextTick(() => {
duplicateDialogRef?.value?.duplicate()
})
} else {
isDuplicateDlgOpen.value = true
}
isOpen.value = false
}
}
@ -373,9 +380,10 @@ const onInsertAfter = () => {
</a-dropdown>
<SmartsheetHeaderDeleteColumnModal v-model:visible="showDeleteColumnModal" />
<DlgColumnDuplicate
v-if="selectedColumnToDuplicate"
v-if="column"
ref="duplicateDialogRef"
v-model="isDuplicateDlgOpen"
:column="selectedColumnToDuplicate"
:column="column"
:extra="selectedColumnExtra"
/>
</template>

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { ColumnType, LinkToAnotherRecordType, LookupType } from 'nocodb-sdk'
import { RelationTypes, UITypes } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import {
ActiveViewInj,
IsLockedInj,
@ -15,20 +15,10 @@ import {
useNuxtApp,
useSmartsheetStoreOrThrow,
useViewColumnsOrThrow,
watch,
} from '#imports'
const groupingUidt = [
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Checkbox,
UITypes.Date,
UITypes.SingleLineText,
UITypes.Number,
UITypes.Rollup,
UITypes.Lookup,
UITypes.Links,
UITypes.Formula,
]
const excludedGroupingUidt = [UITypes.Attachment]
const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref())
@ -62,16 +52,16 @@ const groupedByColumnIds = computed(() => groupBy.value.map((g) => g.fk_column_i
const { eventBus } = useSmartsheetStoreOrThrow()
const { isMobileMode } = useGlobal()
const btLookups = ref([])
const supportedLookups = ref([])
const fieldsToGroupBy = computed(() => {
const fields = meta.value?.columns || []
return fields.filter((field) => {
if (!groupingUidt.includes(field.uidt as UITypes)) return false
if (excludedGroupingUidt.includes(field.uidt as UITypes)) return false
if (field.uidt === UITypes.Lookup) {
return btLookups.value.includes(field.id)
return supportedLookups.value.includes(field.id)
}
return true
@ -161,25 +151,18 @@ watch(open, () => {
}
})
const loadBtLookups = async () => {
const loadAllowedLookups = async () => {
const filteredLookupCols = []
try {
for (const col of meta.value?.columns || []) {
if (col.uidt !== UITypes.Lookup) continue
let nextCol = col
let btLookup = true
// check all the relation of nested lookup columns is bt or not
// include the column only if all only if all relations are bt
while (btLookup && nextCol && nextCol.uidt === UITypes.Lookup) {
// check the lookup column is supported type or not
while (nextCol && nextCol.uidt === UITypes.Lookup) {
const lookupRelation = (await getMeta(nextCol.fk_model_id))?.columns?.find(
(c) => c.id === (nextCol.colOptions as LookupType).fk_relation_column_id,
)
if ((lookupRelation.colOptions as LinkToAnotherRecordType).type !== RelationTypes.BELONGS_TO) {
btLookup = false
continue
}
const relatedTableMeta = await getMeta((lookupRelation.colOptions as LinkToAnotherRecordType).fk_related_model_id)
@ -190,22 +173,25 @@ const loadBtLookups = async () => {
// if next column is same as root lookup column then break the loop
// since it's going to be a circular loop, and ignore the column
if (nextCol.id === col.id) {
btLookup = false
break
}
}
if (btLookup) filteredLookupCols.push(col.id)
if (nextCol.uidt !== UITypes.Attachment) filteredLookupCols.push(col.id)
}
btLookups.value = filteredLookupCols
supportedLookups.value = filteredLookupCols
} catch (e) {
console.error(e)
}
}
onMounted(async () => {
await loadBtLookups()
await loadAllowedLookups()
})
watch(meta, async () => {
await loadAllowedLookups()
})
</script>
@ -242,9 +228,7 @@ onMounted(async () => {
<LazySmartsheetToolbarFieldListAutoCompleteDropdown
v-model="group.fk_column_id"
class="caption nc-sort-field-select"
:columns="
fieldsToGroupBy.filter((f) => (f.id && !groupedByColumnIds.includes(f.id)) || f.id === group.fk_column_id)
"
:columns="fieldsToGroupBy"
:allow-empty="true"
@change="saveGroupBy"
@click.stop

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

@ -34,12 +34,13 @@ import {
useI18n,
useNuxtApp,
useTabs,
validateTableName,
} from '#imports'
const { quickImportType, baseTemplate, importData, importColumns, importDataOnly, maxRowsToParse, sourceId, importWorker } =
defineProps<Props>()
const emit = defineEmits(['import'])
const emit = defineEmits(['import', 'error', 'change'])
dayjs.extend(utc)
@ -95,6 +96,8 @@ const importingTips = ref<Record<string, string>>({})
const checkAllRecord = ref<boolean[]>([])
const formError = ref()
const uiTypeOptions = ref<Option[]>(
(Object.keys(UITypes) as (keyof typeof UITypes)[])
.filter(
@ -124,11 +127,14 @@ const data = reactive<{
const validators = computed(() =>
data.tables.reduce<Record<string, [ReturnType<typeof fieldRequiredValidator>]>>((acc: Record<string, any>, table, tableIdx) => {
acc[`tables.${tableIdx}.table_name`] = [fieldRequiredValidator()]
acc[`tables.${tableIdx}.table_name`] = [validateTableName]
hasSelectColumn.value[tableIdx] = false
table.columns?.forEach((column, columnIdx) => {
acc[`tables.${tableIdx}.columns.${columnIdx}.column_name`] = [fieldRequiredValidator(), fieldLengthValidator()]
acc[`tables.${tableIdx}.columns.${columnIdx}.column_name`] = [
fieldRequiredValidator(),
fieldLengthValidator(base.value?.sources?.[0].type || ClientType.MYSQL),
]
acc[`tables.${tableIdx}.columns.${columnIdx}.uidt`] = [fieldRequiredValidator()]
if (isSelect(column)) {
hasSelectColumn.value[tableIdx] = true
@ -139,10 +145,12 @@ const validators = computed(() =>
}, {}),
)
const { validate, validateInfos } = useForm(data, validators)
const { validate, validateInfos, modelRef } = useForm(data, validators)
const isValid = ref(!importDataOnly)
const formRef = ref()
watch(
() => srcDestMapping.value,
() => {
@ -674,6 +682,39 @@ function handleUIDTChange(column, table) {
])
}
}
const setErrorState = (errorsFields: any[]) => {
const errorMap: any = {}
for (const error of errorsFields) {
errorMap[error.name] = error.errors
}
formError.value = errorMap
}
watch(formRef, () => {
setTimeout(async () => {
try {
await validate()
emit('change')
formError.value = null
} catch (e: any) {
emit('error', e)
setErrorState(e.errorFields)
}
}, 500)
})
watch(modelRef, async () => {
try {
await validate()
emit('change')
formError.value = null
} catch (e: any) {
emit('error', e)
setErrorState(e.errorFields)
}
})
</script>
<template>
@ -694,7 +735,7 @@ function handleUIDTChange(column, table) {
<a-collapse v-if="data.tables && data.tables.length" v-model:activeKey="expansionPanel" class="template-collapse" accordion>
<a-collapse-panel v-for="(table, tableIdx) of data.tables" :key="tableIdx">
<template #header>
<span class="font-weight-bold text-lg flex items-center gap-2">
<span class="font-weight-bold text-lg flex items-center gap-2 truncate">
<component :is="iconMap.table" class="text-primary" />
{{ table.table_name }}
</span>
@ -769,7 +810,7 @@ function handleUIDTChange(column, table) {
</a-card>
<a-card v-else>
<a-form :model="data" name="template-editor-form" @keydown.enter="emit('import')">
<a-form ref="formRef" :model="data" name="template-editor-form" @keydown.enter="emit('import')">
<p v-if="data.tables && quickImportType === 'excel'" class="text-center">
{{ data.tables.length }} sheet{{ data.tables.length > 1 ? 's' : '' }}
available for import
@ -783,22 +824,24 @@ function handleUIDTChange(column, table) {
>
<a-collapse-panel v-for="(table, tableIdx) of data.tables" :key="tableIdx">
<template #header>
<a-form-item v-if="editableTn[tableIdx]" v-bind="validateInfos[`tables.${tableIdx}.table_name`]" no-style>
<a-input
v-model:value.lazy="table.table_name"
class="max-w-xs font-weight-bold text-lg"
size="large"
hide-details
:bordered="false"
@click.stop
@blur="handleEditableTnChange(tableIdx)"
@keydown.enter="handleEditableTnChange(tableIdx)"
/>
<a-form-item v-bind="validateInfos[`tables.${tableIdx}.table_name`]" no-style>
<div class="flex flex-col w-full">
<a-input
v-model:value="table.table_name"
class="font-weight-bold text-lg"
size="large"
hide-details
:bordered="false"
@click.stop
@blur="handleEditableTnChange(tableIdx)"
@keydown.enter="handleEditableTnChange(tableIdx)"
@dblclick="setEditableTn(tableIdx, true)"
/>
<div v-if="formError?.[`tables.${tableIdx}.table_name`]" class="text-red-500 ml-3">
{{ formError?.[`tables.${tableIdx}.table_name`].join('\n') }}
</div>
</div>
</a-form-item>
<span v-else class="font-weight-bold text-lg flex items-center gap-2" @click="setEditableTn(tableIdx, true)">
<component :is="iconMap.table" class="text-primary" />
{{ table.table_name }}
</span>
</template>
<template #extra>

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

@ -98,7 +98,7 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activ
<template>
<div
class="h-full w-full"
class="h-full w-full nc-lookup-cell"
:style="{ height: rowHeight && rowHeight !== 1 ? `${rowHeight * 2}rem` : `2.85rem` }"
@dblclick="activateShowEditNonEditableFieldWarning"
>
@ -206,4 +206,8 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activ
@apply bg-gray-200;
}
}
.nc-lookup-cell .nc-text-area-clamped-text {
@apply !mr-1;
}
</style>

8
packages/nc-gui/components/virtual-cell/components/Header.vue

@ -5,9 +5,9 @@ import FileIcon from '~icons/nc-icons/file'
import { iconMap } from '#imports'
const { relation, relatedTableTitle, displayValue, showHeader, tableTitle } = defineProps<{
const { relation, relatedTableTitle, displayValue, header, tableTitle } = defineProps<{
relation: string
showHeader?: boolean
header?: string | null
tableTitle: string
relatedTableTitle: string
displayValue?: string
@ -54,12 +54,12 @@ const relationMeta = computed(() => {
<template>
<div class="flex sm:justify-between relative pb-2 items-center">
<div v-if="!isMobileMode" class="flex text-base font-bold justify-start items-center min-w-36">
{{ showHeader ? 'Linked Records' : '' }}
{{ header ?? '' }}
</div>
<div class="flex flex-row sm:w-[calc(100%-16rem)] xs:w-full items-center justify-center gap-2 xs:(h-full)">
<div class="flex sm:justify-end w-[calc(50%-1.5rem)] xs:(w-[calc(50%-1.5rem)] h-full)">
<div
class="flex max-w-full xs:w-full flex-shrink-0 xs:(h-full) rounded-md gap-1 text-brand-500 items-center bg-gray-100 px-2 py-1"
class="flex max-w-full xs:w-full flex-shrink-0 xs:(h-full) rounded-md gap-1 text-gray-700 items-center bg-gray-100 px-2 py-1"
>
<FileIcon class="w-4 h-4 min-w-4" />
<span class="truncate">

2
packages/nc-gui/components/virtual-cell/components/ListChildItems.vue

@ -192,7 +192,7 @@ const linkOrUnLink = (rowRef: Record<string, string>, id: string) => {
:relation="relation"
:linked-records="childrenListCount"
:table-title="meta?.title"
:show-header="true"
:header="$t('activity.linkedRecords')"
:related-table-title="relatedTableMeta?.title"
:display-value="row.row[displayValueProp]"
/>

69
packages/nc-gui/components/virtual-cell/components/ListItems.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import { RelationTypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import InboxIcon from '~icons/nc-icons/inbox'
import {
@ -29,6 +29,8 @@ const { isSharedBase } = storeToRefs(useBase())
const filterQueryRef = ref()
const { t } = useI18n()
const { $e } = useNuxtApp()
const {
@ -53,6 +55,8 @@ const { addLTARRef, isNew, removeLTARRef, state: rowState } = useSmartsheetRowSt
const isPublic = inject(IsPublicInj, ref(false))
const isExpandedFormCloseAfterSave = ref(false)
isChildrenExcludedLoading.value = true
const isForm = inject(IsFormInj, ref(false))
@ -112,7 +116,8 @@ const newRowState = computed(() => {
if (isNew.value) return {}
const colOpt = (injectedColumn?.value as ColumnType)?.colOptions as LinkToAnotherRecordType
const colInRelatedTable: ColumnType | undefined = relatedTableMeta?.value?.columns?.find((col) => {
if (col.uidt !== UITypes.LinkToAnotherRecord) return false
// Links as for the case of 'mm' we need the 'Links' column
if (!isLinksOrLTAR(col)) return false
const colOpt1 = col?.colOptions as LinkToAnotherRecordType
if (colOpt1?.fk_related_model_id !== meta.value.id) return false
@ -157,6 +162,10 @@ const relation = computed(() => {
watch(expandedFormDlg, () => {
if (!expandedFormDlg.value) {
isExpandedFormCloseAfterSave.value = false
if (!isForm.value) {
loadChildrenList()
}
loadChildrenExcludedList(rowState.value)
}
})
@ -173,6 +182,42 @@ const onClick = (refRow: any, id: string) => {
linkRow(refRow, Number.parseInt(id))
}
}
const addNewRecord = () => {
expandedFormRow.value = {}
expandedFormDlg.value = true
isExpandedFormCloseAfterSave.value = true
}
const onCreatedRecord = (record: any) => {
const msgVNode = h(
'div',
{
class: 'ml-1 inline-flex flex-col gap-1 items-start',
},
[
h(
'span',
{
class: 'font-semibold',
},
t('activity.recordCreatedLinked'),
),
h(
'span',
{
class: 'text-gray-500',
},
t('activity.gotSavedLinkedSuccessfully', {
tableName: relatedTableMeta.value?.title,
recordTitle: record[relatedTableDisplayValueProp.value],
}),
),
],
)
message.success(msgVNode)
}
</script>
<template>
@ -191,14 +236,14 @@ const onClick = (refRow: any, id: string) => {
:table-title="meta?.title"
:related-table-title="relatedTableMeta?.title"
:display-value="row.row[displayValueProp]"
:header="$t('activity.addNewLink')"
/>
<div class="!xs:hidden my-3 bg-gray-50 border-gray-50 border-b-2"></div>
<div class="flex mt-2 mb-2 items-center gap-2">
<div
class="flex items-center border-1 p-1 rounded-md w-full border-gray-200"
:class="{ '!border-primary': childrenExcludedListPagination.query.length !== 0 || isFocused }"
>
<MdiMagnify class="w-5 h-5 ml-2" />
<MdiMagnify class="w-5 h-5 ml-2 text-gray-500" />
<a-input
ref="filterQueryRef"
v-model:value="childrenExcludedListPagination.query"
@ -223,12 +268,7 @@ const onClick = (refRow: any, id: string) => {
type="secondary"
:size="isMobileMode ? 'medium' : 'small'"
class="!text-brand-500"
@click="
() => {
expandedFormRow = {}
expandedFormDlg = true
}
"
@click="addNewRecord"
>
<div class="flex items-center gap-1 px-4"><MdiPlus v-if="!isMobileMode" /> {{ $t('activity.newRecord') }}</div>
</NcButton>
@ -344,6 +384,15 @@ const onClick = (refRow: any, id: string) => {
:row-id="extractPkFromRow(expandedFormRow, relatedTableMeta.columns as ColumnType[])"
:state="newRowState"
use-meta-fields
:close-after-save="isExpandedFormCloseAfterSave"
:new-record-header="
isExpandedFormCloseAfterSave
? $t('activity.tableNameCreateNewRecord', {
tableName: relatedTableMeta?.title,
})
: undefined
"
@created-record="onCreatedRecord"
/>
</Suspense>
</NcModal>

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

@ -40,6 +40,8 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const { sqlUis } = storeToRefs(baseStore)
const { bases } = storeToRefs(useBases())
const { $api } = useNuxtApp()
const { getMeta } = useMetas()
@ -64,6 +66,12 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
isXcdbBaseFunc(meta.value?.source_id ? meta.value?.source_id : Object.keys(sqlUis.value)[0]),
)
const source = computed(() =>
meta.value && meta.value.source_id && meta.value.base_id
? bases.value.get(meta.value?.base_id as string)?.sources?.find((s) => s.id === meta.value!.source_id)
: undefined,
)
const idType = null
const additionalValidations = ref<ValidationsObj>({})
@ -128,7 +136,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
})
},
},
fieldLengthValidator(),
fieldLengthValidator(source.value?.type || ClientType.MYSQL),
],
uidt: [
{

10
packages/nc-gui/composables/useViewGroupBy.ts

@ -89,6 +89,12 @@ export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: Computed
if (col.uidt === UITypes.Checkbox) {
return value ? GROUP_BY_VARS.TRUE : GROUP_BY_VARS.FALSE
}
// convert to JSON string if non-string value
if (value && typeof value === 'object') {
value = JSON.stringify(value)
}
return value ?? GROUP_BY_VARS.NULL
}
@ -144,13 +150,13 @@ export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: Computed
const calculateNestedWhere = (nestedIn: GroupNestedIn[], existing = '') => {
return nestedIn.reduce((acc, curr) => {
if (curr.key === GROUP_BY_VARS.NULL) {
acc += `${acc.length ? '~and' : ''}(${curr.title},blank)`
acc += `${acc.length ? '~and' : ''}(${curr.title},gb_null)`
} else if (curr.column_uidt === UITypes.Checkbox) {
acc += `${acc.length ? '~and' : ''}(${curr.title},${curr.key === GROUP_BY_VARS.TRUE ? 'checked' : 'notchecked'})`
} else if ([UITypes.Date, UITypes.DateTime].includes(curr.column_uidt as UITypes)) {
acc += `${acc.length ? '~and' : ''}(${curr.title},eq,exactDate,${curr.key})`
} else {
acc += `${acc.length ? '~and' : ''}(${curr.title},eq,${curr.key})`
acc += `${acc.length ? '~and' : ''}(${curr.title},gb_eq,${curr.key})`
}
return acc
}, existing)

5
packages/nc-gui/lang/en.json

@ -713,6 +713,8 @@
"inviteTeam": "Invite Team",
"inviteUser": "Invite User",
"inviteToken": "Invite Token",
"linkedRecords": "Linked Records",
"addNewLink": "Add New Link",
"newUser": "New User",
"editUser": "Edit user",
"deleteUser": "Remove user from base",
@ -799,6 +801,9 @@
"linkRecord": "Link record",
"addNewRecord": "Add new record",
"newRecord": "New record",
"tableNameCreateNewRecord": "{tableName}: Create new record",
"gotSavedLinkedSuccessfully": "{tableName} '{recordTitle}' got saved & linked successfully",
"recordCreatedLinked": "Record Created & Linked",
"useConnectionUrl": "Use Connection URL",
"toggleCommentsDraw": "Toggle comments draw",
"expandRecord": "Expand Record",

2
packages/nc-gui/lib/enums.ts

@ -25,7 +25,7 @@ export enum Language {
id = 'Bahasa Indonesia',
it = 'Italiano',
ja = '日本語',
ko = '한국',
ko = '한국',
lv = 'Latviešu',
nl = 'Nederlandse',
no = 'Norsk',

102
packages/nc-gui/package.json

@ -37,42 +37,42 @@
},
"dependencies": {
"@braks/revue-draggable": "^0.4.3",
"@ckpack/vue-color": "^1.2.0",
"@ckpack/vue-color": "^1.5.0",
"@iconify/vue": "^4.1.1",
"@pinia/nuxt": "^0.4.11",
"@vue-flow/additional-components": "^1.2.0",
"@vue-flow/core": "^1.3.0",
"@vuelidate/core": "^2.0.0-alpha.44",
"@vuelidate/validators": "^2.0.0-alpha.31",
"@vue-flow/additional-components": "^1.3.3",
"@vue-flow/core": "^1.26.0",
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
"@vueuse/core": "^10.2.1",
"@vueuse/integrations": "^10.2.1",
"ant-design-vue": "^3.2.11",
"chart.js": "^4.3.0",
"ant-design-vue": "^3.2.20",
"chart.js": "^4.4.0",
"crossoriginworker": "^1.1.0",
"d3-scale": "^4.0.2",
"dagre": "^0.8.5",
"dayjs": "^1.11.9",
"dayjs": "^1.11.10",
"deep-object-diff": "^1.1.9",
"emoji-mart-vue-fast": "^15.0.0",
"file-saver": "^2.0.5",
"fuse.js": "^6.6.2",
"httpsnippet": "^2.0.0",
"jsbarcode": "^3.11.5",
"jsep": "^1.3.6",
"jsbarcode": "^3.11.6",
"jsep": "^1.3.8",
"jwt-decode": "^3.1.2",
"leaflet": "^1.9.2",
"leaflet": "^1.9.4",
"leaflet.markercluster": "^1.5.3",
"locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0",
"monaco-sql-languages": "^0.11.0",
"nocodb-sdk": "workspace:^",
"papaparse": "^5.3.2",
"papaparse": "^5.4.1",
"parse-github-url": "^1.0.2",
"pinia": "^2.1.4",
"qrcode": "^1.5.1",
"pinia": "^2.1.7",
"qrcode": "^1.5.3",
"rfdc": "^1.3.0",
"showdown": "^2.1.0",
"socket.io-client": "^4.5.1",
"socket.io-client": "^4.7.2",
"sortablejs": "^1.15.0",
"splitpanes": "^3.1.5",
"tinycolor2": "^1.4.2",
@ -84,56 +84,56 @@
"vue-dompurify-html": "^3.0.0",
"vue-github-button": "^3.1.0",
"vue-i18n": "^9.2.2",
"vue-qrcode-reader": "3.1.0-vue3-compatibility.2",
"vue3-calendar-heatmap": "^2.0.0",
"vue-qrcode-reader": "3.1.8",
"vue3-calendar-heatmap": "^2.0.5",
"vue3-contextmenu": "^0.2.12",
"vue3-grid-layout-next": "^1.0.5",
"vue3-text-clamp": "^0.1.1",
"vue3-grid-layout-next": "^1.0.6",
"vue3-text-clamp": "^0.1.2",
"vuedraggable": "^4.1.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@antfu/eslint-config": "^0.26.0",
"@antfu/eslint-config": "^0.26.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@iconify-json/ant-design": "^1.1.9",
"@iconify-json/bi": "^1.1.18",
"@iconify-json/carbon": "^1.1.20",
"@iconify-json/ant-design": "^1.1.10",
"@iconify-json/bi": "^1.1.20",
"@iconify-json/carbon": "^1.1.21",
"@iconify-json/cil": "^1.1.5",
"@iconify-json/clarity": "^1.1.9",
"@iconify-json/eva": "^1.1.7",
"@iconify-json/ic": "^1.1.14",
"@iconify-json/ion": "^1.1.12",
"@iconify-json/la": "^1.1.5",
"@iconify-json/logos": "^1.1.34",
"@iconify-json/lucide": "^1.1.119",
"@iconify-json/material-symbols": "^1.1.57",
"@iconify-json/mdi": "^1.1.54",
"@iconify-json/logos": "^1.1.38",
"@iconify-json/lucide": "^1.1.141",
"@iconify-json/material-symbols": "^1.1.63",
"@iconify-json/mdi": "^1.1.55",
"@iconify-json/mi": "^1.1.5",
"@iconify-json/ph": "^1.1.6",
"@iconify-json/ri": "^1.1.12",
"@iconify-json/simple-icons": "^1.1.67",
"@iconify-json/simple-icons": "^1.1.78",
"@iconify-json/system-uicons": "^1.1.9",
"@iconify-json/tabler": "^1.1.89",
"@iconify-json/vscode-icons": "^1.1.28",
"@intlify/unplugin-vue-i18n": "^0.12.2",
"@nuxt/image-edge": "^1.0.0-rc.1-28217290.de85e17",
"@types/d3-scale": "^4.0.3",
"@types/dagre": "^0.7.48",
"@types/file-saver": "^2.0.5",
"@types/leaflet": "^1.9.0",
"@types/leaflet.markercluster": "^1.5.1",
"@types/papaparse": "^5.3.2",
"@types/parse-github-url": "^1.0.0",
"@types/qrcode": "^1.5.0",
"@types/showdown": "^2.0.0",
"@iconify-json/tabler": "^1.1.96",
"@iconify-json/vscode-icons": "^1.1.29",
"@intlify/unplugin-vue-i18n": "^0.12.3",
"@nuxt/image-edge": "1.0.0-28336957.57c0f74",
"@types/d3-scale": "^4.0.8",
"@types/dagre": "^0.7.52",
"@types/file-saver": "^2.0.7",
"@types/leaflet": "^1.9.8",
"@types/leaflet.markercluster": "^1.5.4",
"@types/papaparse": "^5.3.11",
"@types/parse-github-url": "^1.0.3",
"@types/qrcode": "^1.5.5",
"@types/showdown": "^2.0.4",
"@types/sortablejs": "^1.13.0",
"@types/splitpanes": "^2.2.1",
"@types/tinycolor2": "^1.4.3",
"@types/validator": "^13.7.10",
"@types/vue-barcode-reader": "^0.0.0",
"@unocss/nuxt": "^0.51.12",
"@vitest/ui": "^0.18.0",
"@vue/compiler-sfc": "^3.2.37",
"@types/splitpanes": "^2.2.5",
"@types/tinycolor2": "^1.4.6",
"@types/validator": "^13.11.6",
"@types/vue-barcode-reader": "^0.0.3",
"@unocss/nuxt": "^0.51.13",
"@vitest/ui": "^0.18.1",
"@vue/compiler-sfc": "^3.3.8",
"@vue/test-utils": "^2.0.2",
"@vueuse/nuxt": "^10.2.1",
"@windicss/plugin-animations": "^1.0.9",
@ -142,11 +142,11 @@
"eslint": "^8.33.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"happy-dom": "^6.0.3",
"happy-dom": "^6.0.4",
"nuxt": "^3.8.1",
"nuxt-windicss": "^2.6.1",
"prettier": "^2.7.1",
"sass": "^1.63.4",
"prettier": "^2.8.8",
"sass": "^1.69.5",
"ts-loader": "^9.4.4",
"unplugin-icons": "^0.14.15",
"unplugin-vue-components": "^0.22.12",

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

@ -42,6 +42,16 @@ export const useSidebarStore = defineStore('sidebarStore', () => {
const leftSidebarWidth = computed(() => (width.value * mobileNormalizedSidebarSize.value) / 100)
const nonHiddenMobileSidebarSize = computed(() => {
if (isMobileMode.value) {
return 100
}
return leftSideBarSize.value.current ?? leftSideBarSize.value.old
})
const nonHiddenLeftSidebarWidth = computed(() => (width.value * nonHiddenMobileSidebarSize.value) / 100)
return {
isLeftSidebarOpen,
isRightSidebarOpen,
@ -50,6 +60,7 @@ export const useSidebarStore = defineStore('sidebarStore', () => {
leftSidebarState,
leftSidebarWidth,
mobileNormalizedSidebarSize,
nonHiddenLeftSidebarWidth,
}
})

22
packages/nc-gui/utils/validation.ts

@ -13,6 +13,10 @@ export const validateTableName = {
return reject(new Error(t('msg.error.tableNameRequired')))
}
if (value.length > 52) {
return reject(new Error(t('msg.error.columnNameExceedsCharacters', { value: 52 })))
}
// exclude . / \
// rest all characters allowed
// https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/acreldb/n0rfg6x1shw0ppn1cwhco6yn09f7.htm#:~:text=By%20default%2C%20MySQL%20encloses%20column,not%20truncate%20a%20longer%20name.
@ -98,17 +102,21 @@ export const fieldRequiredValidator = () => {
}
}
export const fieldLengthValidator = () => {
export const fieldLengthValidator = (sqlClientType: string) => {
return {
validator: (rule: any, value: any) => {
const { t } = getI18n().global
// mysql allows 64 characters for column_name
// postgres allows 59 characters for column_name
// mssql allows 128 characters for column_name
// sqlite allows any number of characters for column_name
// We allow 255 for all databases, truncate will be handled by backend for column_name
const fieldLengthLimit = 255
// no limit for sqlite but set as 255
let fieldLengthLimit = 255
if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') {
fieldLengthLimit = 64
} else if (sqlClientType === 'pg') {
fieldLengthLimit = 59
} else if (sqlClientType === 'mssql') {
fieldLengthLimit = 128
}
return new Promise((resolve, reject) => {
if (value?.length > fieldLengthLimit) {

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

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

2
packages/noco-docs/docs/030.workspaces/040.actions-on-workspace.md

@ -20,7 +20,7 @@ To update the workspace name:
## Delete workspace
If you determine that a workspace is no longer necessary, you have the option to permanently remove it from your settings. Deleting a workspace will delete all the bases and data associated with it.
:::danger
:::info
**This action cannot be undone.**
:::

2
packages/noco-docs/docs/040.bases/070.actions-on-base.md

@ -69,7 +69,7 @@ To duplicate a base, you can follow these straightforward steps:
If you determine that a base is no longer necessary, you have the option to permanently remove it from your workspace. Deleting a base will delete all the tables and data associated with it.
:::danger
:::info
**This action cannot be undone.**
:::

2
packages/noco-docs/docs/050.tables/060.actions-on-table.md

@ -46,7 +46,7 @@ A new table will be generated, mirroring the original table's schema and content
## Delete table
:::danger
:::info
**This action cannot be undone.**
:::

12
packages/noco-docs/docs/070.fields/040.field-types/050.custom-types/010.attachment.md

@ -39,21 +39,21 @@ Expand modal for `Attachment` field displays the list of files uploaded to the f
Expand modal supports the following actions:
### Attach file(s)
- Click on `Attach file(s)` button <1>
- Click on `Attach file(s)` button {"<"}1{">"}
- Choose the file(s) to upload
### Delete file
- Click on `x` icon <2> to the top left of the image card to delete the file
- Click on `x` icon {"<"}2{">"} to the top left of the image card to delete the file
### Download file
- Click on `Download` button <5> to download the file
- Click on `Download` button {"<"}5{">"} to download the file
### Bulk Download file(s)
- Select the files by clicking on the checkbox <3> to the top left of the image card
- Click on `Bulk Download` button <4> to download the selected files
- Select the files by clicking on the checkbox {"<"}3{">"} to the top left of the image card
- Click on `Bulk Download` button {"<"}4{">"} to download the selected files
### Rename file
- Click on `Rename` button <5> to rename the file
- Click on `Rename` button {"<"}5{">"} to rename the file
- Enter the new name in the input field
- Click on `Rename` button to save the new name

2
packages/noco-docs/docs/070.fields/040.field-types/060.formula/015.operators.md

@ -17,7 +17,7 @@ keywords: ['Fields', 'Field types', 'Formula', 'Create formula field', 'Numeric
:::tip
To change the order of arithmetic operation, you can use round bracket parenthesis ().
Example: ({field1} + ({field2} * {field3}) / (3 - $field4$ ))
Example: `({field1} + ({field2} * {field3}) / (3 - {field4} ))`
:::

16
packages/noco-docs/docs/070.fields/040.field-types/060.formula/040.date-functions.md

@ -10,14 +10,14 @@ keywords: ['Fields', 'Field types', 'Formula', 'Date & Time', 'Create formula fi
| Name | Syntax | Sample | Output | Remark |
|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------|---------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **NOW** | `NOW()` | `NOW()` | 2022-05-19 17:20:43 | Returns the current time and day |
| | `IF(NOW() < {DATE_COL}, "true", "false")` | `IF(NOW() < date, "true", "false")` | If current date is less than {DATE_COL}, it returns true. Otherwise, it returns false. | DateTime fields and negative values are supported. |
| **DATEADD** | `DATEADD(date \| datetime, value, ["day" \| "week" \| "month" \| "year"])` | `DATEADD(date, 1, 'day')` | Supposing {DATE_COL} is 2022-03-14. The result is 2022-03-15. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'day')` |
| | | `DATEADD(date, 1, 'week')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2022-03-21 03:14. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'week')` |
| | | `DATEADD(date, 1, 'month')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2022-04-14 03:14. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'month')` |
| | | `DATEADD(date, 1, 'year')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2023-03-14 03:14. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'year')` |
| | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than {DATE_COL} plus 10 days, it returns true. Otherwise, it returns false. | DateTime fields and negative values are supported. |
| | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than {DATE_COL} plus 10 days, it returns true. Otherwise, it returns false. | DateTime fields and negative values are supported. |
| **DATETIME_DIFF** | `DATETIME_DIFF(date, date, ["milliseconds" \| "ms" \| "seconds" \| "s" \| "minutes" \| "m" \| "hours" \| "h" \| "days" \| "d" \| "weeks" \| "w" \| "months" \| "M" \| "quarters" \| "Q" \| "years" \| "y"])` | `DATETIME_DIFF("2022/10/14", "2022/10/15", "second")` | Supposing {DATE_COL_1} is 2017-08-25 and {DATE_COL_2} is 2011-08-25. The result is 86400. | Compares two dates and returns the difference in the unit specified. Positive integers indicate the second date being in the past compared to the first and vice versa for negative ones. |
| | `IF(NOW() < {DATE_COL}`, "true", "false")` | `IF(NOW() < date, "true", "false")` | If current date is less than `{DATE_COL}`, it returns true. Otherwise, it returns false. | DateTime fields and negative values are supported. |
| **DATEADD** | `DATEADD(date \| datetime, value, ["day" \| "week" \| "month" \| "year"])` | `DATEADD(date, 1, 'day')` | Supposing `{DATE_COL}` is 2022-03-14. The result is 2022-03-15. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'day')` |
| | | `DATEADD(date, 1, 'week')` | Supposing `{DATE_COL}` is 2022-03-14 03:14. The result is 2022-03-21 03:14. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'week')` |
| | | `DATEADD(date, 1, 'month')` | Supposing `{DATE_COL}` is 2022-03-14 03:14. The result is 2022-04-14 03:14. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'month')` |
| | | `DATEADD(date, 1, 'year')` | Supposing `{DATE_COL}` is 2022-03-14 03:14. The result is 2023-03-14 03:14. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'year')` |
| | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than `{DATE_COL}` plus 10 days, it returns true. Otherwise, it returns false. | DateTime fields and negative values are supported. |
| | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than `{DATE_COL}` plus 10 days, it returns true. Otherwise, it returns false. | DateTime fields and negative values are supported. |
| **DATETIME_DIFF** | `DATETIME_DIFF(date, date, ["milliseconds" \| "ms" \| "seconds" \| "s" \| "minutes" \| "m" \| "hours" \| "h" \| "days" \| "d" \| "weeks" \| "w" \| "months" \| "M" \| "quarters" \| "Q" \| "years" \| "y"])` | `DATETIME_DIFF("2022/10/14", "2022/10/15", "second")` | Supposing `{DATE_COL_1}` is 2017-08-25 and `{DATE_COL_2}` is 2011-08-25. The result is 86400. | Compares two dates and returns the difference in the unit specified. Positive integers indicate the second date being in the past compared to the first and vice versa for negative ones. |
| | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday |
| **WEEKDAY** | `WEEKDAY(date, [startDayOfWeek])` | `WEEKDAY(NOW())` | If today is Monday, it returns 0 | Returns the day of the week as an integer between 0 and 6 inclusive starting from Monday by default. You can optionally change the start day of the week by specifying in the second argument |
| | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday |

2
packages/noco-docs/docs/070.fields/060.actions-on-field.md

@ -83,7 +83,7 @@ New field will be created to the right of the original field.
New field will be created to the left of the original field.
### Delete field
:::danger
:::info
**This action cannot be undone.**
:::

4
packages/noco-docs/docs/080.records/070.actions-on-record.md

@ -54,8 +54,8 @@ On the bulk update modal,
5. Click on the `Bulk Update all` button
6. A confirmation dialog will be displayed. Click on `Confirm` to update the records.
:::danger
This operation cannot be undone.
:::info
**This action cannot be undone.**
:::
![Bulk Update](/img/v2/records/bulk-update-1.png)

4
packages/noco-docs/docs/090.views/040.view-types/030.form.md

@ -28,7 +28,7 @@ Form view builder layout can be divided into 3 sections:
## Form View Operations
### Add Form Title & Description
In the **Form View** area, click on in input boxes provided for **Title** <1> & **Description** <2> to add/update title & description to the form.
In the **Form View** area, click on in input boxes provided for **Title** {"<"}1{">"} & **Description** {"<"}2{">"} to add/update title & description to the form.
![Form Title & Description](/img/v2/views/form-view-title-description.png)
@ -38,7 +38,7 @@ To add a field to the form, either
- Click on the field in the **Fields Area** to add it to the end of the **Form Area**
### Change field label & help-text
To change the field label displayed on the form & add help-text, click on the field in the **Form Area** and update the values in the input boxes provided for **Label** <1> & **Help Text** <2>.
To change the field label displayed on the form & add help-text, click on the field in the **Form Area** and update the values in the input boxes provided for **Label** {"<"}1{">"} & **Help Text** {"<"}2{">"}.
![Field Label & Help Text](/img/v2/views/form-view-field-label-help-text.png)

2
packages/noco-docs/docs/090.views/090.actions-on-view.md

@ -41,7 +41,7 @@ The view context menu provides a set of tools to interact with the view. The vie
## Delete view
:::danger
:::info
**This action cannot be undone.**
:::

6
packages/noco-docs/docs/150.engineering/060.builds-and-releases.md

@ -137,13 +137,13 @@ When a non-draft Pull Request is created, reopened or synchronized, a timely bui
- `packages/nc-plugin/**`
- `packages/nocodb/**`
The docker images will be built and pushed to Docker Hub (See [nocodb/nocodb-timely](https://hub.docker.com/r/nocodb/nocodb-timely/tags) for the full list). Once the image is ready, Github bot will add a comment with the command in the pull request. The tag would be `<NOCODB_CURRENT_VERSION>-pr-<PR_NUMBER>-<YYYYMMDD>-<HHMM>`.
The docker images will be built and pushed to Docker Hub (See [nocodb/nocodb-timely](https://hub.docker.com/r/nocodb/nocodb-timely/tags) for the full list). Once the image is ready, GitHub bot will add a comment with the command in the pull request. The tag would be `<NOCODB_CURRENT_VERSION>-pr-<PR_NUMBER>-<YYYYMMDD>-<HHMM>`.
![image](https://user-images.githubusercontent.com/35857179/175012097-240dab05-da93-4c4e-87c1-1c36fb1350bd.png)
## Executables or Binaries
Similarly, we provide a timely build for executables for non-docker users. The source code will be built, packaged as binary files, and pushed to Github (See [nocodb/nocodb-timely](https://github.com/nocodb/nocodb-timely/releases) for the full list).
Similarly, we provide a timely build for executables for non-docker users. The source code will be built, packaged as binary files, and pushed to GitHub (See [nocodb/nocodb-timely](https://github.com/nocodb/nocodb-timely/releases) for the full list).
Currently, we only support the following targets:
@ -154,7 +154,7 @@ Currently, we only support the following targets:
- `node16-macos-x64`
- `node16-win-x64`
Once the executables are ready, Github bot will add a comment with the commands in the pull request.
Once the executables are ready, GitHub bot will add a comment with the commands in the pull request.
![image](https://user-images.githubusercontent.com/35857179/175012070-f5f3e7b8-6dc5-4d1c-9f7e-654bc5039521.png)

2
packages/noco-docs/docs/150.engineering/070.translation.md

@ -9,7 +9,7 @@ tags: ['Engineering']
## How to add / edit translations ?
### Using Github
### Using GitHub
- For English, make changes directly to [en.json](https://github.com/nocodb/nocodb/blob/develop/packages/nc-gui/lang/en.json) & commit to `develop`
- For any other language, use `crowdin` option.

7598
packages/noco-docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

30
packages/noco-docs/package.json

@ -28,26 +28,26 @@
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "2.4.1",
"@docusaurus/plugin-client-redirects": "2.4.1",
"@docusaurus/plugin-ideal-image": "2.4.1",
"@docusaurus/plugin-sitemap": "2.4.1",
"@docusaurus/preset-classic": "2.4.1",
"@mdx-js/react": "^1.6.22",
"@docusaurus/core": "3.0.0",
"@docusaurus/plugin-client-redirects": "3.0.0",
"@docusaurus/plugin-ideal-image": "3.0.0",
"@docusaurus/plugin-sitemap": "3.0.0",
"@docusaurus/preset-classic": "3.0.0",
"@mdx-js/react": "^3.0.0",
"clsx": "^1.2.1",
"docusaurus-plugin-sass": "^0.2.5",
"docusaurus-theme-search-typesense": "^0.12.0-0",
"nc-analytics": "^0.0.4",
"docusaurus-theme-search-typesense": "^0.14.0",
"nc-analytics": "^0.0.7",
"plugin-image-zoom": "github:flexanalytics/plugin-image-zoom",
"prism-react-renderer": "^1.3.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"sass": "^1.66.1"
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.69.5"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.4.1",
"@tsconfig/docusaurus": "^1.0.5",
"typescript": "^4.7.4"
"@docusaurus/module-type-aliases": "3.0.0",
"@tsconfig/docusaurus": "^1.0.7",
"typescript": "^4.9.5"
},
"browserslist": {
"production": [
@ -62,6 +62,6 @@
]
},
"engines": {
"node": ">=16.14"
"node": ">=16.14.2"
}
}

18
packages/noco-docs/versioned_docs/version-0.109.7/030.setup-and-usages/090.formulas.md vendored

@ -62,7 +62,7 @@ Unlike other column types, formula cells cannot be modified by double-clicking s
:::tip
To change the order of arithmetic operation, you can use round bracket parantheses (). <br/>
Example: ({Column1} + ({Column2} * {Column3}) / (3 - $Column4$ ))
Example: `({Column1} + ({Column2} * {Column3}) / (3 - $Column4$ ))`
:::
@ -89,14 +89,14 @@ Example: ({Column1} + ({Column2} * {Column3}) / (3 - $Column4$ ))
| Name | Syntax | Sample | Output | Remark |
|---|---|---|---|---|
| **NOW** | `NOW()` | `NOW()` | 2022-05-19 17:20:43 | Returns the current time and day |
| | `IF(NOW() < {DATE_COL}, "true", "false")` | `IF(NOW() < date, "true", "false")` | If current date is less than {DATE_COL}, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. |
| **DATEADD** | `DATEADD(date \| datetime, value, ["day" \| "week" \| "month" \| "year"])` | `DATEADD(date, 1, 'day')` | Supposing {DATE_COL} is 2022-03-14. The result is 2022-03-15. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'day')` |
| | | `DATEADD(date, 1, 'week')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2022-03-21 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'week')` |
| | | `DATEADD(date, 1, 'month')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2022-04-14 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'month')` |
| | | `DATEADD(date, 1, 'year')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2023-03-14 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'year')` |
| | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than {DATE_COL} plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. |
| | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than {DATE_COL} plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. |
| **DATETIME_DIFF** | `DATETIME_DIFF(date, date, ["milliseconds" \| "ms" \| "seconds" \| "s" \| "minutes" \| "m" \| "hours" \| "h" \| "days" \| "d" \| "weeks" \| "w" \| "months" \| "M" \| "quarters" \| "Q" \| "years" \| "y"])` | `DATETIME_DIFF("2022/10/14", "2022/10/15", "second")` | Supposing {DATE_COL_1} is 2017-08-25 and {DATE_COL_2} is 2011-08-25. The result is 86400. | Compares two dates and returns the difference in the unit specified. Positive integers indicate the second date being in the past compared to the first and vice versa for negative ones. |
| | `IF(NOW() < {DATE_COL}, "true", "false")` | `IF(NOW() < date, "true", "false")` | If current date is less than `{DATE_COL}`, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. |
| **DATEADD** | `DATEADD(date \| datetime, value, ["day" \| "week" \| "month" \| "year"])` | `DATEADD(date, 1, 'day')` | Supposing `{DATE_COL}` is 2022-03-14. The result is 2022-03-15. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'day')` |
| | | `DATEADD(date, 1, 'week')` | Supposing `{DATE_COL}` is 2022-03-14 03:14. The result is 2022-03-21 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'week')` |
| | | `DATEADD(date, 1, 'month')` | Supposing `{DATE_COL}` is 2022-03-14 03:14. The result is 2022-04-14 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'month')` |
| | | `DATEADD(date, 1, 'year')` | Supposing `{DATE_COL}` is 2022-03-14 03:14. The result is 2023-03-14 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'year')` |
| | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than `{DATE_COL}` plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. |
| | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than `{DATE_COL}` plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. |
| **DATETIME_DIFF** | `DATETIME_DIFF(date, date, ["milliseconds" \| "ms" \| "seconds" \| "s" \| "minutes" \| "m" \| "hours" \| "h" \| "days" \| "d" \| "weeks" \| "w" \| "months" \| "M" \| "quarters" \| "Q" \| "years" \| "y"])` | `DATETIME_DIFF("2022/10/14", "2022/10/15", "second")` | Supposing `{DATE_COL_1}` is 2017-08-25 and `{DATE_COL_2}` is 2011-08-25. The result is 86400. | Compares two dates and returns the difference in the unit specified. Positive integers indicate the second date being in the past compared to the first and vice versa for negative ones. |
| | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday |
| **WEEKDAY** | `WEEKDAY(date, [startDayOfWeek])` | `WEEKDAY(NOW())` | If today is Monday, it returns 0 | Returns the day of the week as an integer between 0 and 6 inclusive starting from Monday by default. You can optionally change the start day of the week by specifying in the second argument |
| | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday |

14
packages/noco-docs/versioned_docs/version-0.109.7/030.setup-and-usages/160.views.md vendored

@ -12,10 +12,10 @@ To navigate different views, we can select the target one in the view sidebar. B
## View Menu Bar
To work with `Views`, use View menu-bar on the right hand side -
- <1> Toggle View menu-bar.
- <2> Displays created view-list for the selected table
- {"<"}1{">"} Toggle View menu-bar.
- {"<"}2{">"} Displays created view-list for the selected table
- Currently active view is high-lighted
- <3> Add new view to the list
- {"<"}3{">"} Add new view to the list
![image](https://user-images.githubusercontent.com/35857179/194814369-53fa8875-7610-4849-9a91-f94096b15b3f.png)
@ -77,7 +77,7 @@ We can apply permission to each View. By default, Collaborative Views will be us
### Create a View
Click '+' in View-menu sidebar, as shown in <3>.
Click '+' in View-menu sidebar, as shown in {"<"}3{">"}.
### Rename a View
@ -87,7 +87,7 @@ Double click on `view-name`, edit, <enter />.
### Delete a View
Hover the target View and click the delete icon, as shown in <2>.
Hover the target View and click the delete icon, as shown in {"<"}2{">"}.
:::note
@ -99,12 +99,12 @@ You cannot delete the very first Grid View (termed as `Default view`).
### Duplicate a View
Hover the target View and click the copy icon, as shown in <2>.
Hover the target View and click the copy icon, as shown in {"<"}2{">"}.
<!-- ![image](https://user-images.githubusercontent.com/35857179/163353865-7275499e-c685-44f4-906c-ba08f0ee419e.png) -->
### Reorder a View
Hover the target View and re-order it as needed by drag-drop the drag icon, as shown in <1>.
Hover the target View and re-order it as needed by drag-drop the drag icon, as shown in {"<"}1{">"}.
<!-- ![image](https://user-images.githubusercontent.com/35857179/163359674-c4aeff74-1cb4-498d-b79c-c6ddf84ad352.png) -->

6
packages/noco-docs/versioned_docs/version-0.109.7/030.setup-and-usages/200.import-airtable-to-sql-database-within-a-minute-for-free.md vendored

@ -58,9 +58,9 @@ Below are 3 simple steps
<!-- ![image](https://user-images.githubusercontent.com/35857179/168773192-f3ef9d36-3329-4324-ae25-989b611f66bf.png) -->
2. Input API key & Shared Base ID / URL (retrieved from `Get Airtable Credentials` above).
- <1> API Key
- <2> Share Base ID
- <3> Configuration option
- {"<"}1{">"} API Key
- {"<"}2{">"} Share Base ID
- {"<"}3{">"} Configuration option
- Import Data: disable this option to import only table & view schema's
- Import Secondary Views: disable this option to import only primary grid view per table
- Import Rollup Columns: disable this option to skip Rollup column import

4
packages/noco-docs/versioned_docs/version-0.109.7/030.setup-and-usages/230.team-and-auth.md vendored

@ -36,8 +36,8 @@ If you do not have an SMTP sender configured, make sure to copy the invite link
### How to Update user permissions
1. Use `Edit` <1> menu to assign a different role to existing user
2. Use `Delete` <2> menu to remove a user from accessing current project
1. Use `Edit` {"<"}1{">"} menu to assign a different role to existing user
2. Use `Delete` {"<"}2{">"} menu to remove a user from accessing current project
![image](https://user-images.githubusercontent.com/35857179/219830858-be7a4656-9f3b-440c-9a79-165f919223d7.png)

246
packages/noco-docs/versioned_docs/version-0.109.7/040.developer-resources/020.rest-apis.md vendored

@ -13,7 +13,7 @@ You may also interact with the API's resources via <a href="/0.109.7/developer-r
:::note
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.
:::
@ -26,145 +26,145 @@ Currently, the default value for {orgs} is <b>noco</b>. Users will be able to ch
| Auth | Get | auth | me | /api/v1/auth/user/me |
| Auth | Post | auth | passwordForgot | /api/v1/auth/password/forgot |
| Auth | Post | auth | passwordChange | /api/v1/auth/password/change |
| Auth | Post | auth | passwordReset | /api/v1/auth/password/reset/{token} |
| Auth | Post | auth | passwordReset | /api/v1/auth/password/reset/`{token}` |
| Auth | Post | auth | tokenRefresh | /api/v1/auth/token/refresh |
| Auth | Post | auth | passwordResetTokenValidate | /api/v1/auth/token/validate/{token} |
| Auth | Post | auth | emailValidate | /api/v1/auth/email/validate/{email} |
| Auth | Post | auth | passwordResetTokenValidate | /api/v1/auth/token/validate/`{token}` |
| Auth | Post | auth | emailValidate | /api/v1/auth/email/validate/`{email}` |
### Public APIs
| Category | Method | Tag | Function Name | Path |
|---|---|---|---|---|
| Public | Get | public | sharedBaseGet | /api/v1/db/public/shared-base/{sharedBaseUuid}/meta |
| Public | Post | public | dataList | /api/v1/db/public/shared-view/{sharedViewUuid}/rows |
| Public | Get | public | dataNestedList | /api/v1/db/public/shared-view/{sharedViewUuid}/rows/{rowId}/{relationType}/{columnName} |
| Public | Post | public | dataCreate | /api/v1/db/public/shared-view/{sharedViewUuid}/rows |
| Public | Get | public | csvExport | /api/v1/db/public/shared-view/{sharedViewUuid}/rows/export/{type} |
| Public | Get | public | dataRelationList | /api/v1/db/public/shared-view/{sharedViewUuid}/nested/{columnName} |
| Public | Get | public | sharedViewMetaGet | /api/v1/db/public/shared-view/{sharedViewUuid}/meta |
| Public | Get | public | groupedDataList | /api/v1/db/public/shared-view/{sharedViewUuid}/group/{columnId} |
| Public | Get | public | sharedBaseGet | /api/v1/db/public/shared-base/`{sharedBaseUuid}`/meta |
| Public | Post | public | dataList | /api/v1/db/public/shared-view/`{sharedViewUuid}`/rows |
| Public | Get | public | dataNestedList | /api/v1/db/public/shared-view/`{sharedViewUuid}`/rows/`{rowId}`/`{relationType}`/`{columnName}` |
| Public | Post | public | dataCreate | /api/v1/db/public/shared-view/`{sharedViewUuid}`/rows |
| Public | Get | public | csvExport | /api/v1/db/public/shared-view/`{sharedViewUuid}`/rows/export/`{type}` |
| Public | Get | public | dataRelationList | /api/v1/db/public/shared-view/`{sharedViewUuid}`/nested/`{columnName}` |
| Public | Get | public | sharedViewMetaGet | /api/v1/db/public/shared-view/`{sharedViewUuid}`/meta |
| Public | Get | public | groupedDataList | /api/v1/db/public/shared-view/`{sharedViewUuid}`/group/`{columnId}` |
### Data APIs
| Category | Method | Tag | Function Name | Path |
|---|---|---|---|---|
| Data | Delete| dbTableRow | bulkDelete | /api/v1/db/data/bulk/{orgs}/{projectName}/{tableName}/ |
| Data | Post | dbTableRow | bulkCreate | /api/v1/db/data/bulk/{orgs}/{projectName}/{tableName}/ |
| Data | Patch | dbTableRow | bulkUpdate | /api/v1/db/data/bulk/{orgs}/{projectName}/{tableName}/ |
| Data | Patch | dbTableRow | bulkUpdateAll | /api/v1/db/data/bulk/{orgs}/{projectName}/{tableName}/all |
| Data | Delete| dbTableRow | bulkDeleteAll | /api/v1/db/data/bulk/{orgs}/{projectName}/{tableName}/all |
| Data | Get | dbTableRow | list | /api/v1/db/data/{orgs}/{projectName}/{tableName} |
| Data | Get | dbTableRow | findOne | /api/v1/db/data/{orgs}/{projectName}/{tableName}/find-one |
| Data | Get | dbTableRow | groupBy | /api/v1/db/data/{orgs}/{projectName}/{tableName}/groupby |
| Data | Get | dbTableRow | exist | /api/v1/db/data/{orgs}/{projectName}/{tableName}/{rowId}/exist |
| Data | Post | dbTableRow | create | /api/v1/db/data/{orgs}/{projectName}/{tableName} |
| Data | Get | dbTableRow | read | /api/v1/db/data/{orgs}/{projectName}/{tableName}/{rowId} |
| Data | Patch | dbTableRow | update | /api/v1/db/data/{orgs}/{projectName}/{tableName}/{rowId} |
| Data | Delete| dbTableRow | delete | /api/v1/db/data/{orgs}/{projectName}/{tableName}/{rowId} |
| Data | Get | dbTableRow | count | /api/v1/db/data/{orgs}/{projectName}/{tableName}/count |
| Data | Get | dbTableRow | groupedDataList | /api/v1/db/data/{orgs}/{projectName}/{tableName}/group/{columnId} |
| Data | Get | dbViewRow | list | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName} |
| Data | Get | dbViewRow | findOne | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/find-one |
| Data | Get | dbViewRow | groupBy | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/groupby |
| Data | Get | dbViewRow | exist | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/{rowId}/exist |
| Data | Post | dbViewRow | create | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName} |
| Data | Get | dbViewRow | read | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/{rowId} |
| Data | Patch | dbViewRow | update | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/{rowId} |
| Data | Delete| dbViewRow | delete | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/{rowId} |
| Data | Get | dbViewRow | count | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/count |
| Data | Get | dbViewRow | groupedDataList | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/group/{columnId} |
| Data | Delete| dbTableRow | bulkDelete | /api/v1/db/data/bulk/`{orgs}`/`{projectName}`/`{tableName}`/ |
| Data | Post | dbTableRow | bulkCreate | /api/v1/db/data/bulk/`{orgs}`/`{projectName}`/`{tableName}`/ |
| Data | Patch | dbTableRow | bulkUpdate | /api/v1/db/data/bulk/`{orgs}`/`{projectName}`/`{tableName}`/ |
| Data | Patch | dbTableRow | bulkUpdateAll | /api/v1/db/data/bulk/`{orgs}`/`{projectName}`/`{tableName}`/all |
| Data | Delete| dbTableRow | bulkDeleteAll | /api/v1/db/data/bulk/`{orgs}`/`{projectName}`/`{tableName}`/all |
| Data | Get | dbTableRow | list | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}` |
| Data | Get | dbTableRow | findOne | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/find-one |
| Data | Get | dbTableRow | groupBy | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/groupby |
| Data | Get | dbTableRow | exist | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/`{rowId}`/exist |
| Data | Post | dbTableRow | create | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}` |
| Data | Get | dbTableRow | read | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/`{rowId}` |
| Data | Patch | dbTableRow | update | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/`{rowId}` |
| Data | Delete| dbTableRow | delete | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/`{rowId}` |
| Data | Get | dbTableRow | count | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/count |
| Data | Get | dbTableRow | groupedDataList | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/group/`{columnId}` |
| Data | Get | dbViewRow | list | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}` |
| Data | Get | dbViewRow | findOne | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}`/find-one |
| Data | Get | dbViewRow | groupBy | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}`/groupby |
| Data | Get | dbViewRow | exist | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}`/`{rowId}`/exist |
| Data | Post | dbViewRow | create | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}` |
| Data | Get | dbViewRow | read | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}`/`{rowId}` |
| Data | Patch | dbViewRow | update | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}`/`{rowId}` |
| Data | Delete| dbViewRow | delete | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}`/`{rowId}` |
| Data | Get | dbViewRow | count | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}`/count |
| Data | Get | dbViewRow | groupedDataList | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}`/group/`{columnId}` |
### Meta APIs
| Category | Method | Tag | Function Name | Path |
|---|---|---|---|---|
| Meta | Get | apiToken | list | /api/v1/db/meta/projects/{projectId}/api-tokens |
| Meta | Post | apiToken | create | /api/v1/db/meta/projects/{projectId}/api-tokens |
| Meta | Delete| apiToken | delete | /api/v1/db/meta/projects/{projectId}/api-tokens/{token} |
| Meta | Get | auth | projectUserList | /api/v1/db/meta/projects/{projectId}/users |
| Meta | Post | auth | projectUserAdd | /api/v1/db/meta/projects/{projectId}/users |
| Meta | Patch | auth | projectUserUpdate | /api/v1/db/meta/projects/{projectId}/users/{userId} |
| Meta | Delete| auth | projectUserRemove | /api/v1/db/meta/projects/{projectId}/users/{userId} |
| Meta | Post | auth | projectUserResendInvite | /api/v1/db/meta/projects/{projectId}/users/{userId}/resend-invite |
| Meta | Post | dbTable | create | /api/v1/db/meta/projects/{projectId}/tables |
| Meta | Get | dbTable | list | /api/v1/db/meta/projects/{projectId}/tables |
| Meta | Post | dbTableColumn | create | /api/v1/db/meta/tables/{tableId}/columns |
| Meta | Patch | dbTableColumn | update | /api/v1/db/meta/tables/{tableId}/columns/{columnId} |
| Meta | Delete| dbTableColumn | delete | /api/v1/db/meta/tables/{tableId}/columns/{columnId} |
| Meta | Post | dbTableColumn | primaryColumnSet | /api/v1/db/meta/tables/{tableId}/columns/{columnId}/primary |
| Meta | Get | dbTableFilter | get | /api/v1/db/meta/filters/{filterId} |
| Meta | Patch | dbTableFilter | update | /api/v1/db/meta/filters/{filterId} |
| Meta | Delete| dbTableFilter | delete | /api/v1/db/meta/filters/{filterId} |
| Meta | Get | dbTableFilter | read | /api/v1/db/meta/views/{viewId}/filters |
| Meta | Post | dbTableFilter | create | /api/v1/db/meta/views/{viewId}/filters |
| Meta | Get | dbTableFilter | get | /api/v1/db/meta/filters/{filterId} |
| Meta | Patch | dbTableFilter | update | /api/v1/db/meta/filters/{filterId} |
| Meta | Delete| dbTableFilter | delete | /api/v1/db/meta/filters/{filterId} |
| Meta | Get | dbTableFilter | childrenRead | /api/v1/db/meta/filters/{filterGroupId}/children |
| Meta | Get | dbTableSort | list | /api/v1/db/meta/views/{viewId}/sorts |
| Meta | Post | dbTableSort | create | /api/v1/db/meta/views/{viewId}/sorts |
| Meta | Get | dbTableSort | read | /api/v1/db/meta/sorts/{sortId} |
| Meta | Patch | dbTableSort | update | /api/v1/db/meta/sorts/{sortId} |
| Meta | Delete| dbTableSort | delete | /api/v1/db/meta/sorts/{sortId}/api/v1/db |
| Meta | Patch | dbTableWebhook | update | /api/v1/db/meta/hooks/{hookId} |
| Meta | Delete| dbTableWebhook | delete | /api/v1/db/meta/hooks/{hookId} |
| Meta | Get | dbTableWebhook | list | /api/v1/db/meta/tables/{tableId}/hooks |
| Meta | Post | dbTableWebhook | create | /api/v1/db/meta/tables/{tableId}/hooks |
| Meta | Post | dbTableWebhook | test | /api/v1/db/meta/tables/{tableId}/hooks/test |
| Meta | Get | dbTableWebhook | samplePayloadGet | /api/v1/db/meta/tables/{tableId}/hooks/samplePayload/{operation} |
| Meta | Get | dbTableWebhookFilter | read | /api/v1/db/meta/hooks/{hookId}/filters |
| Meta | Post | dbTableWebhookFilter | create | /api/v1/db/meta/hooks/{hookId}/filters |
| Meta | Get | dbView | list | /api/v1/db/meta/tables/{tableId}/views |
| Meta | Get | dbView | read | /api/v1/db/meta/tables/{tableId} |
| Meta | Patch | dbView | update | /api/v1/db/meta/tables/{tableId} |
| Meta | Delete| dbView | delete | /api/v1/db/meta/tables/{tableId} |
| Meta | Post | dbView | reorder | /api/v1/db/meta/tables/{tableId}/reorder |
| Meta | Post | dbView | formCreate | /api/v1/db/meta/tables/{tableId}/forms |
| Meta | Patch | dbView | formUpdate | /api/v1/db/meta/forms/{formViewId} |
| Meta | Get | dbView | formRead | /api/v1/db/meta/forms/{formViewId} |
| Meta | Patch | dbView | formColumnUpdate | /api/v1/db/meta/form-columns/{formViewColumnId} |
| Meta | Post | dbView | galleryCreate | /api/v1/db/meta/tables/{tableId}/galleries |
| Meta | Patch | dbView | galleryUpdate | /api/v1/db/meta/galleries/{galleryViewId} |
| Meta | Get | dbView | galleryRead | /api/v1/db/meta/galleries/{galleryViewId} |
| Meta | Post | dbView | kanbanCreate | /api/v1/db/meta/tables/{tableId}/kanbans |
| Meta | Patch | dbView | kanbanUpdate | /api/v1/db/meta/kanban/{kanbanViewId} |
| Meta | Get | dbView | kanbanRead | /api/v1/db/meta/kanbans/{kanbanViewId} |
| Meta | Post | dbView | mapCreate | /api/v1/db/meta/tables/{tableId}/maps |
| Meta | Patch | dbView | mapUpdate | /api/v1/db/meta/maps/{mapViewId} |
| Meta | Get | dbView | mapRead | /api/v1/db/meta/maps/{mapViewId} |
| Meta | Post | dbView | gridCreate | /api/v1/db/meta/tables/{tableId}/grids |
| Meta | Get | dbView | gridColumnsList | /api/v1/db/meta/grids/{gridId}/grid-columns |
| Meta | Patch | dbView | gridColumnUpdate | /api/v1/db/meta/grid-columns/{columnId} |
| Meta | Patch | dbView | update | /api/v1/db/meta/views/{viewId} |
| Meta | Delete| dbView | delete | /api/v1/db/meta/views/{viewId} |
| Meta | Post | dbView | showAllColumn | /api/v1/db/meta/views/{viewId}/show-all |
| Meta | Post | dbView | hideAllColumn | /api/v1/db/meta/views/{viewId}/hide-all |
| Meta | Get | dbViewColumn | list | /api/v1/db/meta/views/{viewId}/columns |
| Meta | Post | dbViewColumn | create | /api/v1/db/meta/views/{viewId}/columns |
| Meta | Patch | dbViewColumn | update | /api/v1/db/meta/views/{viewId}/columns/{columnId} |
| Meta | Get | dbViewShare | list | /api/v1/db/meta/views/{viewId}/share |
| Meta | Post | dbViewShare | create | /api/v1/db/meta/views/{viewId}/share |
| Meta | Patch | dbViewShare | update | /api/v1/db/meta/views/{viewId}/share |
| Meta | Delete| dbViewShare | delete | /api/v1/db/meta/views/{viewId}/share |
| Meta | Get | apiToken | list | /api/v1/db/meta/projects/`{projectId}`/api-tokens |
| Meta | Post | apiToken | create | /api/v1/db/meta/projects/`{projectId}`/api-tokens |
| Meta | Delete| apiToken | delete | /api/v1/db/meta/projects/`{projectId}`/api-tokens/`{token}` |
| Meta | Get | auth | projectUserList | /api/v1/db/meta/projects/`{projectId}`/users |
| Meta | Post | auth | projectUserAdd | /api/v1/db/meta/projects/`{projectId}`/users |
| Meta | Patch | auth | projectUserUpdate | /api/v1/db/meta/projects/`{projectId}`/users/`{userId}` |
| Meta | Delete| auth | projectUserRemove | /api/v1/db/meta/projects/`{projectId}`/users/`{userId}` |
| Meta | Post | auth | projectUserResendInvite | /api/v1/db/meta/projects/`{projectId}`/users/`{userId}`/resend-invite |
| Meta | Post | dbTable | create | /api/v1/db/meta/projects/`{projectId}`/tables |
| Meta | Get | dbTable | list | /api/v1/db/meta/projects/`{projectId}`/tables |
| Meta | Post | dbTableColumn | create | /api/v1/db/meta/tables/`{tableId}`/columns |
| Meta | Patch | dbTableColumn | update | /api/v1/db/meta/tables/`{tableId}`/columns/`{columnId}` |
| Meta | Delete| dbTableColumn | delete | /api/v1/db/meta/tables/`{tableId}`/columns/`{columnId}` |
| Meta | Post | dbTableColumn | primaryColumnSet | /api/v1/db/meta/tables/`{tableId}`/columns/`{columnId}`/primary |
| Meta | Get | dbTableFilter | get | /api/v1/db/meta/filters/`{filterId}` |
| Meta | Patch | dbTableFilter | update | /api/v1/db/meta/filters/`{filterId}` |
| Meta | Delete| dbTableFilter | delete | /api/v1/db/meta/filters/`{filterId}` |
| Meta | Get | dbTableFilter | read | /api/v1/db/meta/views/`{viewId}`/filters |
| Meta | Post | dbTableFilter | create | /api/v1/db/meta/views/`{viewId}`/filters |
| Meta | Get | dbTableFilter | get | /api/v1/db/meta/filters/`{filterId}` |
| Meta | Patch | dbTableFilter | update | /api/v1/db/meta/filters/`{filterId}` |
| Meta | Delete| dbTableFilter | delete | /api/v1/db/meta/filters/`{filterId}` |
| Meta | Get | dbTableFilter | childrenRead | /api/v1/db/meta/filters/`{filterGroupId}`/children |
| Meta | Get | dbTableSort | list | /api/v1/db/meta/views/`{viewId}`/sorts |
| Meta | Post | dbTableSort | create | /api/v1/db/meta/views/`{viewId}`/sorts |
| Meta | Get | dbTableSort | read | /api/v1/db/meta/sorts/`{sortId}` |
| Meta | Patch | dbTableSort | update | /api/v1/db/meta/sorts/`{sortId}` |
| Meta | Delete| dbTableSort | delete | /api/v1/db/meta/sorts/`{sortId}`/api/v1/db |
| Meta | Patch | dbTableWebhook | update | /api/v1/db/meta/hooks/`{hookId}` |
| Meta | Delete| dbTableWebhook | delete | /api/v1/db/meta/hooks/`{hookId}` |
| Meta | Get | dbTableWebhook | list | /api/v1/db/meta/tables/`{tableId}`/hooks |
| Meta | Post | dbTableWebhook | create | /api/v1/db/meta/tables/`{tableId}`/hooks |
| Meta | Post | dbTableWebhook | test | /api/v1/db/meta/tables/`{tableId}`/hooks/test |
| Meta | Get | dbTableWebhook | samplePayloadGet | /api/v1/db/meta/tables/`{tableId}`/hooks/samplePayload/`{operation}` |
| Meta | Get | dbTableWebhookFilter | read | /api/v1/db/meta/hooks/`{hookId}`/filters |
| Meta | Post | dbTableWebhookFilter | create | /api/v1/db/meta/hooks/`{hookId}`/filters |
| Meta | Get | dbView | list | /api/v1/db/meta/tables/`{tableId}`/views |
| Meta | Get | dbView | read | /api/v1/db/meta/tables/`{tableId}` |
| Meta | Patch | dbView | update | /api/v1/db/meta/tables/`{tableId}` |
| Meta | Delete| dbView | delete | /api/v1/db/meta/tables/`{tableId}` |
| Meta | Post | dbView | reorder | /api/v1/db/meta/tables/`{tableId}`/reorder |
| Meta | Post | dbView | formCreate | /api/v1/db/meta/tables/`{tableId}`/forms |
| Meta | Patch | dbView | formUpdate | /api/v1/db/meta/forms/`{formViewId}` |
| Meta | Get | dbView | formRead | /api/v1/db/meta/forms/`{formViewId}` |
| Meta | Patch | dbView | formColumnUpdate | /api/v1/db/meta/form-columns/`{formViewColumnId}` |
| Meta | Post | dbView | galleryCreate | /api/v1/db/meta/tables/`{tableId}`/galleries |
| Meta | Patch | dbView | galleryUpdate | /api/v1/db/meta/galleries/`{galleryViewId}` |
| Meta | Get | dbView | galleryRead | /api/v1/db/meta/galleries/`{galleryViewId}` |
| Meta | Post | dbView | kanbanCreate | /api/v1/db/meta/tables/`{tableId}`/kanbans |
| Meta | Patch | dbView | kanbanUpdate | /api/v1/db/meta/kanban/`{kanbanViewId}` |
| Meta | Get | dbView | kanbanRead | /api/v1/db/meta/kanbans/`{kanbanViewId}` |
| Meta | Post | dbView | mapCreate | /api/v1/db/meta/tables/`{tableId}`/maps |
| Meta | Patch | dbView | mapUpdate | /api/v1/db/meta/maps/`{mapViewId}` |
| Meta | Get | dbView | mapRead | /api/v1/db/meta/maps/`{mapViewId}` |
| Meta | Post | dbView | gridCreate | /api/v1/db/meta/tables/`{tableId}`/grids |
| Meta | Get | dbView | gridColumnsList | /api/v1/db/meta/grids/`{gridId}`/grid-columns |
| Meta | Patch | dbView | gridColumnUpdate | /api/v1/db/meta/grid-columns/`{columnId}` |
| Meta | Patch | dbView | update | /api/v1/db/meta/views/`{viewId}` |
| Meta | Delete| dbView | delete | /api/v1/db/meta/views/`{viewId}` |
| Meta | Post | dbView | showAllColumn | /api/v1/db/meta/views/`{viewId}`/show-all |
| Meta | Post | dbView | hideAllColumn | /api/v1/db/meta/views/`{viewId}`/hide-all |
| Meta | Get | dbViewColumn | list | /api/v1/db/meta/views/`{viewId}`/columns |
| Meta | Post | dbViewColumn | create | /api/v1/db/meta/views/`{viewId}`/columns |
| Meta | Patch | dbViewColumn | update | /api/v1/db/meta/views/`{viewId}`/columns/`{columnId}` |
| Meta | Get | dbViewShare | list | /api/v1/db/meta/views/`{viewId}`/share |
| Meta | Post | dbViewShare | create | /api/v1/db/meta/views/`{viewId}`/share |
| Meta | Patch | dbViewShare | update | /api/v1/db/meta/views/`{viewId}`/share |
| Meta | Delete| dbViewShare | delete | /api/v1/db/meta/views/`{viewId}`/share |
| Meta | Get | plugin | list | /api/v1/db/meta/plugins |
| Meta | Get | plugin | status | /api/v1/db/meta/plugins/{pluginTitle}/status |
| Meta | Get | plugin | status | /api/v1/db/meta/plugins/`{pluginTitle}`/status |
| Meta | Post | plugin | test | /api/v1/db/meta/plugins/test |
| Meta | PATCH | plugin | update | /api/v1/db/meta/plugins/{pluginId} |
| Meta | Get | plugin | read | /api/v1/db/meta/plugins/{pluginId} |
| Meta | Get | project | metaGet | /api/v1/db/meta/projects/{projectId}/info |
| Meta | Get | project | modelVisibilityList | /api/v1/db/meta/projects/{projectId}/visibility-rules |
| Meta | Post | project | modelVisibilitySet | /api/v1/db/meta/projects/{projectId}/visibility-rules |
| Meta | PATCH | plugin | update | /api/v1/db/meta/plugins/`{pluginId}` |
| Meta | Get | plugin | read | /api/v1/db/meta/plugins/`{pluginId}` |
| Meta | Get | project | metaGet | /api/v1/db/meta/projects/`{projectId}`/info |
| Meta | Get | project | modelVisibilityList | /api/v1/db/meta/projects/`{projectId}`/visibility-rules |
| Meta | Post | project | modelVisibilitySet | /api/v1/db/meta/projects/`{projectId}`/visibility-rules |
| Meta | Get | project | list | /api/v1/db/meta/projects |
| Meta | Post | project | create | /api/v1/db/meta/projects |
| Meta | Get | project | read | /api/v1/db/meta/projects/{projectId} |
| Meta | Delete| project | delete | /api/v1/db/meta/projects/{projectId} |
| Meta | Get | project | auditList | /api/v1/db/meta/projects/{projectId}/audits |
| Meta | Get | project | metaDiffGet | /api/v1/db/meta/projects/{projectId}/meta-diff |
| Meta | Post | project | metaDiffSync | /api/v1/db/meta/projects/{projectId}/meta-diff |
| Meta | Get | project | sharedBaseGet | /api/v1/db/meta/projects/{projectId}/shared |
| Meta | Delete| project | sharedBaseDisable | /api/v1/db/meta/projects/{projectId}/shared |
| Meta | Post | project | sharedBaseCreate | /api/v1/db/meta/projects/{projectId}/shared |
| Meta | Patch | project | sharedBaseUpdate | /api/v1/db/meta/projects/{projectId}/shared |
| Meta | Get | project | read | /api/v1/db/meta/projects/`{projectId}` |
| Meta | Delete| project | delete | /api/v1/db/meta/projects/`{projectId}` |
| Meta | Get | project | auditList | /api/v1/db/meta/projects/`{projectId}`/audits |
| Meta | Get | project | metaDiffGet | /api/v1/db/meta/projects/`{projectId}`/meta-diff |
| Meta | Post | project | metaDiffSync | /api/v1/db/meta/projects/`{projectId}`/meta-diff |
| Meta | Get | project | sharedBaseGet | /api/v1/db/meta/projects/`{projectId}`/shared |
| Meta | Delete| project | sharedBaseDisable | /api/v1/db/meta/projects/`{projectId}`/shared |
| Meta | Post | project | sharedBaseCreate | /api/v1/db/meta/projects/`{projectId}`/shared |
| Meta | Patch | project | sharedBaseUpdate | /api/v1/db/meta/projects/`{projectId}`/shared |
| Meta | Post | storage | upload | /api/v1/db/storage/upload |
| Meta | Post | storage | uploadByUrl | /api/v1/db/storage/upload-by-url |
| Meta | Get | utils | commentList | /api/v1/db/meta/audits/comments |
@ -180,11 +180,11 @@ Currently, the default value for {orgs} is <b>noco</b>. Users will be able to ch
| Meta | Get | utils | aggregatedMetaInfo | /api/v1/aggregated-meta-info |
| Meta | Get | orgUsers | list | /api/v1/users |
| Meta | Post | orgUsers | add | /api/v1/users |
| Meta | Patch | orgUsers | update | /api/v1/users/{userId} |
| Meta | Delete | orgUsers | delete | /api/v1/users/{userId} |
| Meta | Patch | orgUsers | update | /api/v1/users/`{userId}` |
| Meta | Delete | orgUsers | delete | /api/v1/users/`{userId}` |
| Meta | Get | orgTokens | list | /api/v1/tokens |
| Meta | Post | orgTokens | create | /api/v1/tokens |
| Meta | Delete | orgTokens | delete | /api/v1/tokens/{token} |
| Meta | Delete | orgTokens | delete | /api/v1/tokens/`{token}` |
| Meta | Get | orgAppSettings | get | /api/v1/app-settings |
| Meta | Post | orgAppSettings | set | /api/v1/app-settings |

2
packages/noco-docs/versioned_docs/version-0.109.7/040.developer-resources/030.sdk.md vendored

@ -61,7 +61,7 @@ For Tag and FunctionName, please check out the API table <a href="/0.109.7/devel
:::
#### Example: Calling API - /api/v1/db/meta/projects/{projectId}/tables
#### Example: Calling API - /api/v1/db/meta/projects/`{projectId}`/tables
```js
await api.dbTable.create(params)

18
packages/noco-docs/versioned_docs/version-0.109.7/040.developer-resources/040.webhooks.md vendored

@ -84,17 +84,17 @@ The current row data and other details will be available in the hooks payload so
For a table with column names (id, title, created_at, updated_at).
For INSERT/ UPDATE based triggers, use following handlebars to access corresponding **data** fields.
- {{ **data**.id }}
- {{ **data**.title }}
- {{ **data**.created_at }}
- {{ **data**.updated_at }}
- `{{ **data**.id }}`
- `{{ **data**.title }}`
- `{{ **data**.created_at }}`
- `{{ **data**.updated_at }}`
Note that, for Update trigger - all the fields in the ROW will be accessible, not just the field updated.
For DELETE based triggers, **only** {{ data.id }} is accessible representing ID of the column deleted.
For DELETE based triggers, **only** `{{ data.id }}` is accessible representing ID of the column deleted.
### JSON format
Use {{ json data }} to dump complete data & user information available in JSON format
Use `{{ json data }}` to dump complete data & user information available in JSON format
### Additional references:
@ -148,7 +148,7 @@ Detailed procedure for discord webhook described [here](https://support.discord.
- **Select Discord Channels**: Select from the drop down list, channel name configured in Step (2). Please click on 'Reload' if drop down list is empty.
- **Body**: Message to be posted over Discord channel, via webhooks on trigger of configured event.
- Body can contain plain text &
- Handlebars {{ }}
- Handlebars `{{ }}`
## Slack
@ -199,7 +199,7 @@ Detailed procedure for discord webhook described [here](https://support.discord.
### 3. Configure
- Open project and choose a table.
- Click 'More' > 'Webhooks'.
- Click 'More' {">"} 'Webhooks'.
- Click 'Create webhook'
- Configure webhook
- **Title**: Name of your choice to identify this Webhook.
@ -212,7 +212,7 @@ Detailed procedure for discord webhook described [here](https://support.discord.
- **Select Teams Channels**: Select from the drop down list, channel name configured in Step (2). Please click on 'Reload' if drop down list is empty.
- **Body**: Message to be posted over Teams channel, via webhooks on trigger of configured event.
- Body can contain plain text &
- Handlebars {{ }}
- Handlebars `{{ }}`
## Webhook V2

12
packages/nocodb-sdk/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb-sdk",
"version": "0.202.5",
"version": "0.202.7",
"description": "NocoDB SDK",
"main": "build/main/index.js",
"typings": "build/main/index.d.ts",
@ -39,12 +39,12 @@
},
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
"jsep": "^1.3.8"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0",
"cspell": "^4.1.0",
"cspell": "^4.2.8",
"eslint": "^8.33.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-eslint-comments": "^3.2.0",
@ -53,9 +53,9 @@
"eslint-plugin-prettier": "^4.2.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.8",
"rimraf": "^5.0.1",
"tsc-alias": "^1.8.7",
"typescript": "^4.7.4"
"rimraf": "^5.0.5",
"tsc-alias": "^1.8.8",
"typescript": "^5.2.2"
},
"files": [
"build/main",

123
packages/nocodb/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb",
"version": "0.202.5",
"version": "0.202.7",
"description": "NocoDB Backend",
"main": "dist/bundle.js",
"author": {
@ -47,28 +47,29 @@
"dependencies": {
"@aws-sdk/client-kafka": "^3.410.0",
"@aws-sdk/client-s3": "^3.423.0",
"@aws-sdk/lib-storage": "^3.451.0",
"@aws-sdk/s3-request-presigner": "^3.423.0",
"@google-cloud/storage": "^7.1.0",
"@graphql-tools/merge": "^6.0.12",
"@graphql-tools/merge": "^6.2.17",
"@jm18457/kafkajs-msk-iam-authentication-mechanism": "^3.1.2",
"@nestjs/bull": "^10.0.1",
"@nestjs/common": "^10.2.1",
"@nestjs/config": "^3.0.0",
"@nestjs/core": "^10.2.1",
"@nestjs/event-emitter": "^2.0.2",
"@nestjs/jwt": "^10.1.0",
"@nestjs/mapped-types": "^2.0.2",
"@nestjs/passport": "^10.0.1",
"@nestjs/platform-express": "^10.2.1",
"@nestjs/platform-socket.io": "^10.2.1",
"@nestjs/common": "^10.2.9",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.2.9",
"@nestjs/event-emitter": "^2.0.3",
"@nestjs/jwt": "^10.2.0",
"@nestjs/mapped-types": "^2.0.4",
"@nestjs/passport": "^10.0.2",
"@nestjs/platform-express": "^10.2.9",
"@nestjs/platform-socket.io": "^10.2.9",
"@nestjs/serve-static": "^4.0.0",
"@nestjs/throttler": "^4.2.1",
"@nestjs/websockets": "^10.2.1",
"@nestjs/websockets": "^10.2.9",
"@ntegral/nestjs-sentry": "^4.0.0",
"@sentry/node": "^6.3.5",
"@sentry/node": "^6.19.7",
"@techpass/passport-openidconnect": "^0.3.3",
"@types/chai": "^4.2.12",
"airtable": "^0.12.1",
"@types/chai": "^4.3.10",
"airtable": "^0.12.2",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"archiver": "^5.0.2",
@ -77,74 +78,74 @@
"aws-sdk": "^2.1455.0",
"axios": "^0.21.1",
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.1",
"boxen": "^5.1.0",
"bull": "^4.11.3",
"bullmq": "^1.81.1",
"body-parser": "^1.20.2",
"boxen": "^5.1.2",
"bull": "^4.11.5",
"bullmq": "^1.91.1",
"clear": "^0.1.0",
"clickhouse": "^2.6.0",
"clickhouse-migrations": "^0.1.13",
"colors": "^1.4.0",
"compare-versions": "^6.0.0-rc.1",
"cookie-parser": "^1.4.5",
"compare-versions": "^6.1.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"cron": "^1.8.2",
"crypto-js": "^4.0.0",
"dataloader": "^2.0.0",
"dayjs": "^1.11.9",
"dayjs": "^1.11.10",
"debug": "^4.3.4",
"dotenv": "^8.2.0",
"ejs": "^3.1.3",
"emittery": "^0.7.1",
"express": "^4.18.1",
"emittery": "^0.7.2",
"express": "^4.18.2",
"extract-zip": "^2.0.1",
"fast-levenshtein": "^2.0.6",
"fs-extra": "^9.0.1",
"glob": "^7.1.6",
"glob": "^7.2.3",
"graphql": "^15.3.0",
"graphql-depth-limit": "^1.1.0",
"graphql-type-json": "^0.3.2",
"handlebars": "^4.7.6",
"html-to-json-parser": "^1.1.0",
"import-fresh": "^3.2.1",
"import-fresh": "^3.3.0",
"inflection": "^1.12.0",
"ioredis": "^5.3.2",
"ioredis-mock": "^8.8.3",
"is-docker": "^2.2.1",
"isomorphic-dompurify": "^1.8.0",
"jsep": "^1.3.6",
"jsep": "^1.3.8",
"json5": "^2.2.3",
"jsonfile": "^6.1.0",
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.2",
"kafkajs": "^2.2.4",
"knex": "2.4.2",
"list-github-dir-content": "^3.0.0",
"lodash": "^4.17.19",
"lru-cache": "^6.0.0",
"mailersend": "^1.1.0",
"mailersend": "^1.5.0",
"marked": "^4.3.0",
"minio": "^7.0.18",
"mkdirp": "^2.1.3",
"minio": "^7.1.3",
"mkdirp": "^2.1.6",
"morgan": "^1.10.0",
"mssql": "^10.0.0",
"mssql": "^10.0.1",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.2.0",
"mysql2": "^3.6.3",
"nanoid": "^3.1.20",
"nc-help": "0.3.1",
"nc-lib-gui": "0.202.5",
"nc-lib-gui": "0.202.7",
"nc-plugin": "^0.1.3",
"ncp": "^2.0.0",
"nestjs-kafka": "^1.0.6",
"nestjs-throttler-storage-redis": "^0.3.0",
"nestjs-throttler-storage-redis": "^0.3.3",
"nocodb-sdk": "workspace:^",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"object-sizeof": "^2.6.1",
"object-sizeof": "^2.6.3",
"os-locale": "^6.0.2",
"p-queue": "^6.6.2",
"papaparse": "^5.4.0",
"papaparse": "^5.4.1",
"parse-database-url": "^0.3.0",
"passport": "^0.4.1",
"passport": "^0.6.0",
"passport-auth-token": "^1.0.1",
"passport-custom": "^1.1.1",
"passport-github": "^1.1.0",
@ -160,35 +161,35 @@
"rmdir": "^1.2.0",
"rxjs": "^7.2.0",
"slash": "^3.0.0",
"slug": "^8.2.2",
"slug": "^8.2.3",
"socket.io": "^4.4.1",
"sql-query-identifier": "^2.5.0",
"sqlite3": "^5.1.6",
"tedious": "^16.4.0",
"tedious": "^16.6.0",
"tinycolor2": "^1.4.2",
"twilio": "^3.55.1",
"unique-names-generator": "^4.3.1",
"uuid": "^9.0.0",
"validator": "^13.1.1",
"unique-names-generator": "^4.7.1",
"uuid": "^9.0.1",
"validator": "^13.1.17",
"xc-core-ts": "^0.1.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@nestjs/cli": "^10.1.10",
"@nestjs/schematics": "^10.0.1",
"@nestjs/testing": "^10.1.0",
"@nestjs/cli": "^10.2.1",
"@nestjs/schematics": "^10.0.3",
"@nestjs/testing": "^10.2.9",
"@nestjsplus/dyn-schematics": "^1.0.12",
"@types/ejs": "^3.1.2",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/mocha": "^10.0.1",
"@types/multer": "^1.4.7",
"@types/node": "20.3.1",
"@types/passport-google-oauth20": "^2.0.11",
"@types/passport-jwt": "^3.0.8",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.7.0",
"@typescript-eslint/parser": "^6.7.0",
"@types/ejs": "^3.1.5",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.8",
"@types/mocha": "^10.0.4",
"@types/multer": "^1.4.10",
"@types/node": "20.3.3",
"@types/passport-google-oauth20": "^2.0.14",
"@types/passport-jwt": "^3.0.13",
"@types/supertest": "^2.0.16",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"chai": "^4.2.0",
"copy-webpack-plugin": "^11.0.0",
"cross-env": "^7.0.3",
@ -201,14 +202,14 @@
"jest": "29.5.0",
"mocha": "^10.1.0",
"nodemon": "^3.0.1",
"prettier": "^2.7.1",
"source-map-support": "^0.5.20",
"prettier": "^2.8.8",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "29.0.5",
"ts-loader": "^9.2.3",
"ts-loader": "^9.2.9",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3",
"typescript": "^5.2.2",
"webpack-cli": "^5.1.4"
},
"jest": {

39
packages/nocodb/src/controllers/api-docs/api-docs.controller.ts

@ -18,10 +18,7 @@ import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
export class ApiDocsController {
constructor(private readonly apiDocsService: ApiDocsService) {}
@Get([
'/api/v1/db/meta/projects/:baseId/swagger.json',
'/api/v2/meta/bases/:baseId/swagger.json',
])
@Get(['/api/v1/db/meta/projects/:baseId/swagger.json'])
@UseGuards(MetaApiLimiterGuard, GlobalGuard)
@Acl('swaggerJson')
async swaggerJson(@Param('baseId') baseId: string, @Request() req) {
@ -33,21 +30,39 @@ export class ApiDocsController {
return swagger;
}
@Get([
'/api/v2/meta/bases/:baseId/swagger',
'/api/v1/db/meta/projects/:baseId/swagger',
])
@Get(['/api/v2/meta/bases/:baseId/swagger.json'])
@UseGuards(MetaApiLimiterGuard, GlobalGuard)
@Acl('swaggerJson')
async swaggerJsonV2(@Param('baseId') baseId: string, @Request() req) {
const swagger = await this.apiDocsService.swaggerJsonV2({
baseId: baseId,
siteUrl: req.ncSiteUrl,
});
return swagger;
}
@Get(['/api/v1/db/meta/projects/:baseId/swagger'])
@UseGuards(PublicApiLimiterGuard)
swaggerHtml(@Param('baseId') baseId: string, @Response() res) {
res.send(getSwaggerHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' }));
}
@UseGuards(PublicApiLimiterGuard)
@Get([
'/api/v1/db/meta/projects/:baseId/redoc',
'/api/v2/meta/bases/:baseId/redoc',
])
@Get(['/api/v1/db/meta/projects/:baseId/redoc'])
redocHtml(@Param('baseId') baseId: string, @Response() res) {
res.send(getRedocHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' }));
}
@Get(['/api/v2/meta/bases/:baseId/swagger'])
@UseGuards(PublicApiLimiterGuard)
swaggerHtmlV2(@Param('baseId') baseId: string, @Response() res) {
res.send(getSwaggerHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' }));
}
@UseGuards(PublicApiLimiterGuard)
@Get(['/api/v2/meta/bases/:baseId/redoc'])
redocHtmlV2(@Param('baseId') baseId: string, @Response() res) {
res.send(getRedocHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' }));
}
}

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

@ -35,7 +35,6 @@ import type {
SelectOption,
} from '~/models';
import type { SortType } from 'nocodb-sdk';
import generateBTLookupSelectQuery from '~/db/generateBTLookupSelectQuery';
import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
import genRollupSelectv2 from '~/db/genRollupSelectv2';
import conditionV2 from '~/db/conditionV2';
@ -60,10 +59,13 @@ import { HANDLE_WEBHOOK } from '~/services/hook-handler.service';
import {
COMPARISON_OPS,
COMPARISON_SUB_OPS,
GROUPBY_COMPARISON_OPS,
IS_WITHIN_COMPARISON_SUB_OPS,
} from '~/utils/globals';
import { extractProps } from '~/helpers/extractProps';
import { defaultLimitConfig } from '~/helpers/extractLimitAndOffset';
import generateLookupSelectQuery from '~/db/generateLookupSelectQuery';
import { getAliasGenerator } from '~/utils';
dayjs.extend(utc);
@ -386,6 +388,7 @@ class BaseModelSqlv2 {
validateFormula: true,
});
}
return data?.map((d) => {
d.__proto__ = proto;
return d;
@ -549,18 +552,32 @@ class BaseModelSqlv2 {
const selectors = [];
const groupBySelectors = [];
const getAlias = getAliasGenerator('__nc_gb');
await Promise.all(
args.column_name.split(',').map(async (col) => {
const column = cols.find(
(c) => c.column_name === col || c.title === col,
);
groupByColumns[column.id] = column;
let column = cols.find((c) => c.column_name === col || c.title === col);
if (!column) {
throw NcError.notFound('Column not found');
}
// if qrCode or Barcode replace it with value column nd keep the alias
if ([UITypes.QrCode, UITypes.Barcode].includes(column.uidt))
column = new Column({
...(await column
.getColOptions<BarcodeColumn | QrCodeColumn>()
.then((col) => col.getValueColumn())),
title: column.title,
});
groupByColumns[column.id] = column;
switch (column.uidt) {
case UITypes.Attachment:
NcError.badRequest(
'Group by using attachment column is not supported',
);
break;
case UITypes.Links:
case UITypes.Rollup:
selectors.push(
@ -599,12 +616,14 @@ class BaseModelSqlv2 {
}
break;
case UITypes.Lookup:
case UITypes.LinkToAnotherRecord:
{
const _selectQb = await generateBTLookupSelectQuery({
const _selectQb = await generateLookupSelectQuery({
baseModelSqlv2: this,
column,
alias: null,
model: this.model,
getAlias,
});
const selectQb = this.dbDriver.raw(`?? as ??`, [
@ -695,6 +714,7 @@ class BaseModelSqlv2 {
qb.groupBy(...groupBySelectors);
applyPaginate(qb, rest);
return await this.execAndParse(qb);
}
@ -711,18 +731,34 @@ class BaseModelSqlv2 {
const selectors = [];
const groupBySelectors = [];
const getAlias = getAliasGenerator('__nc_gb');
// todo: refactor and avoid duplicate code
await this.model.getColumns().then((cols) =>
Promise.all(
args.column_name.split(',').map(async (col) => {
const column = cols.find(
let column = cols.find(
(c) => c.column_name === col || c.title === col,
);
if (!column) {
throw NcError.notFound('Column not found');
}
// if qrCode or Barcode replace it with value column nd keep the alias
if ([UITypes.QrCode, UITypes.Barcode].includes(column.uidt))
column = new Column({
...(await column
.getColOptions<BarcodeColumn | QrCodeColumn>()
.then((col) => col.getValueColumn())),
title: column.title,
});
switch (column.uidt) {
case UITypes.Attachment:
NcError.badRequest(
'Group by using attachment column is not supported',
);
break;
case UITypes.Rollup:
case UITypes.Links:
selectors.push(
@ -764,12 +800,14 @@ class BaseModelSqlv2 {
break;
}
case UITypes.Lookup:
case UITypes.LinkToAnotherRecord:
{
const _selectQb = await generateBTLookupSelectQuery({
const _selectQb = await generateLookupSelectQuery({
baseModelSqlv2: this,
column,
alias: null,
model: this.model,
getAlias,
});
const selectQb = this.dbDriver.raw(`?? as ??`, [
@ -2994,7 +3032,7 @@ class BaseModelSqlv2 {
// insert one by one as fallback to get ids for sqlite and mysql
if (insertOneByOneAsFallback && (this.isSqlite || this.isMySQL)) {
// sqlite and mysql doesnt support returning, so insert one by one and return ids
// sqlite and mysql doesn't support returning, so insert one by one and return ids
response = [];
const aiPkCol = this.model.primaryKeys.find((pk) => pk.ai);
@ -5156,9 +5194,7 @@ export function extractSortsObject(
else sort.fk_column_id = aliasColObjMap[s.replace(/^\+/, '')]?.id;
if (throwErrorIfInvalid && !sort.fk_column_id)
NcError.unprocessableEntity(
`Invalid column '${s.replace(/^[+-]/, '')}' in sort`,
);
NcError.unprocessableEntity(`Invalid field: ${s.replace(/^[+-]/, '')}`);
return new Sort(sort);
});
@ -5247,7 +5283,7 @@ export function extractFilterFromXwhere(
// mark `op` and `sub_op` any for being assignable to parameter of type
function validateFilterComparison(uidt: UITypes, op: any, sub_op?: any) {
if (!COMPARISON_OPS.includes(op)) {
if (!COMPARISON_OPS.includes(op) && !GROUPBY_COMPARISON_OPS.includes(op)) {
NcError.badRequest(`${op} is not supported.`);
}
@ -5322,7 +5358,7 @@ export function extractCondition(
validateFilterComparison(aliasColObjMap[alias].uidt, op, sub_op);
} else if (throwErrorIfInvalid) {
NcError.unprocessableEntity(`Column '${alias}' not found.`);
NcError.unprocessableEntity(`Invalid field: ${alias}`);
}
return new Filter({
@ -5366,7 +5402,7 @@ export function _wherePk(primaryKeys: Column[], id: unknown | unknown[]) {
}
}
return id;
return where;
}
const ids = Array.isArray(id) ? id : (id + '').split('___');

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

@ -8,11 +8,14 @@ import type Column from '~/models/Column';
import type LookupColumn from '~/models/LookupColumn';
import type RollupColumn from '~/models/RollupColumn';
import type FormulaColumn from '~/models/FormulaColumn';
import type { BarcodeColumn, QrCodeColumn } from '~/models';
import { NcError } from '~/helpers/catchError';
import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
import genRollupSelectv2 from '~/db/genRollupSelectv2';
import { sanitize } from '~/helpers/sqlSanitize';
import Filter from '~/models/Filter';
import generateLookupSelectQuery from '~/db/generateLookupSelectQuery';
import { getAliasGenerator } from '~/utils';
// tod: tobe fixed
// extend(customParseFormat);
@ -112,12 +115,47 @@ const parseConditionV2 = async (
});
};
} else {
// handle group by filter separately,
// `gb_eq` is equivalent to `eq` but for lookup it compares on aggregated value returns in group by api
// aggregated value will be either json array or `___` separated string
// `gb_null` is equivalent to `blank` but for lookup it compares on aggregated value is null
if (
(filter.comparison_op as any) === 'gb_eq' ||
(filter.comparison_op as any) === 'gb_null'
) {
const column = await filter.getColumn();
if (
column.uidt === UITypes.Lookup ||
column.uidt === UITypes.LinkToAnotherRecord
) {
const model = await column.getModel();
const lkQb = await generateLookupSelectQuery({
baseModelSqlv2,
alias: alias,
model,
column,
getAlias: getAliasGenerator('__gb_filter_lk'),
});
return (qb) => {
if ((filter.comparison_op as any) === 'gb_eq')
qb.where(knex.raw('?', [filter.value]), lkQb.builder);
else qb.whereNull(knex.raw(lkQb.builder).wrap('(', ')'));
};
} else {
filter.comparison_op =
(filter.comparison_op as any) === 'gb_eq' ? 'eq' : 'blank';
// if qrCode or Barcode replace it with value column
if ([UITypes.QrCode, UITypes.Barcode].includes(column.uidt))
filter.fk_column_id = await column
.getColOptions<BarcodeColumn | QrCodeColumn>()
.then((col) => col.fk_column_id);
}
}
const column = await filter.getColumn();
if (!column) {
if (throwErrorIfInvalid) {
NcError.unprocessableEntity(
`Invalid column id '${filter.fk_column_id}' in filter`,
);
NcError.unprocessableEntity(`Invalid field: ${filter.fk_column_id}`);
}
return;
}
@ -342,7 +380,7 @@ const parseConditionV2 = async (
return (qbP: Knex.QueryBuilder) => {
if (filter.comparison_op in negatedMapping)
qbP.where((qb) =>
qbP
qb
.whereNotIn(childColumn.column_name, selectQb)
.orWhereNull(childColumn.column_name),
);

163
packages/nocodb/src/db/generateBTLookupSelectQuery.ts

@ -1,163 +0,0 @@
import { RelationTypes, UITypes } from 'nocodb-sdk';
import type LookupColumn from '../models/LookupColumn';
import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2';
import type {
Column,
FormulaColumn,
LinkToAnotherRecordColumn,
Model,
RollupColumn,
} from '~/models';
import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
import genRollupSelectv2 from '~/db/genRollupSelectv2';
import { NcError } from '~/helpers/catchError';
export default async function generateBTLookupSelectQuery({
column,
baseModelSqlv2,
alias,
model,
}: {
column: Column;
baseModelSqlv2: BaseModelSqlv2;
alias: string;
model: Model;
}): Promise<any> {
const knex = baseModelSqlv2.dbDriver;
const rootAlias = alias;
{
let aliasCount = 0,
selectQb;
const alias = `__nc_lk_${aliasCount++}`;
const lookup = await column.getColOptions<LookupColumn>();
{
const relationCol = await lookup.getRelationColumn();
const relation =
await relationCol.getColOptions<LinkToAnotherRecordColumn>();
// if not belongs to then throw error as we don't support
if (relation.type !== RelationTypes.BELONGS_TO)
NcError.badRequest('HasMany/ManyToMany lookup is not supported');
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
selectQb = knex(
`${baseModelSqlv2.getTnPath(parentModel.table_name)} as ${alias}`,
).where(
`${alias}.${parentColumn.column_name}`,
knex.raw(`??`, [
`${rootAlias || baseModelSqlv2.getTnPath(childModel.table_name)}.${
childColumn.column_name
}`,
]),
);
}
let lookupColumn = await lookup.getLookupColumn();
let prevAlias = alias;
while (lookupColumn.uidt === UITypes.Lookup) {
const nestedAlias = `__nc_lk_nested_${aliasCount++}`;
const nestedLookup = await lookupColumn.getColOptions<LookupColumn>();
const relationCol = await nestedLookup.getRelationColumn();
const relation =
await relationCol.getColOptions<LinkToAnotherRecordColumn>();
// if any of the relation in nested lookup is
// not belongs to then throw error as we don't support
if (relation.type !== RelationTypes.BELONGS_TO)
NcError.badRequest('HasMany/ManyToMany lookup is not supported');
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
selectQb.join(
`${baseModelSqlv2.getTnPath(parentModel.table_name)} as ${nestedAlias}`,
`${nestedAlias}.${parentColumn.column_name}`,
`${prevAlias}.${childColumn.column_name}`,
);
lookupColumn = await nestedLookup.getLookupColumn();
prevAlias = nestedAlias;
}
switch (lookupColumn.uidt) {
case UITypes.Links:
case UITypes.Rollup:
{
const builder = (
await genRollupSelectv2({
baseModelSqlv2,
knex,
columnOptions:
(await lookupColumn.getColOptions()) as RollupColumn,
alias: prevAlias,
})
).builder;
selectQb.select(builder);
}
break;
case UITypes.LinkToAnotherRecord:
{
const nestedAlias = `__nc_sort${aliasCount++}`;
const relation =
await lookupColumn.getColOptions<LinkToAnotherRecordColumn>();
if (relation.type !== 'bt') return;
const colOptions =
(await column.getColOptions()) as LinkToAnotherRecordColumn;
const childColumn = await colOptions.getChildColumn();
const parentColumn = await colOptions.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
selectQb
.join(
`${baseModelSqlv2.getTnPath(
parentModel.table_name,
)} as ${nestedAlias}`,
`${nestedAlias}.${parentColumn.column_name}`,
`${prevAlias}.${childColumn.column_name}`,
)
.select(parentModel?.displayValue?.column_name);
}
break;
case UITypes.Formula:
{
const builder = (
await formulaQueryBuilderv2(
baseModelSqlv2,
(
await column.getColOptions<FormulaColumn>()
).formula,
null,
model,
column,
)
).builder;
selectQb.select(builder);
}
break;
default:
{
selectQb.select(`${prevAlias}.${lookupColumn.column_name}`);
}
break;
}
return { builder: selectQb };
}
}

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

@ -0,0 +1,399 @@
import { RelationTypes, UITypes } from 'nocodb-sdk';
import type LookupColumn from '../models/LookupColumn';
import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2';
import type {
BarcodeColumn,
Column,
FormulaColumn,
LinksColumn,
LinkToAnotherRecordColumn,
QrCodeColumn,
RollupColumn,
} from '~/models';
import { Model } from '~/models';
import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
import genRollupSelectv2 from '~/db/genRollupSelectv2';
import { getAliasGenerator } from '~/utils';
import { NcError } from '~/helpers/catchError';
const LOOKUP_VAL_SEPARATOR = '___';
export async function getDisplayValueOfRefTable(
relationCol: Column<LinkToAnotherRecordColumn | LinksColumn>,
) {
return await relationCol
.getColOptions()
.then((colOpt) => colOpt.getRelatedTable())
.then((model) => model.getColumns())
.then((cols) => cols.find((col) => col.pv));
}
export default async function generateLookupSelectQuery({
column,
baseModelSqlv2,
alias,
model: _model,
getAlias = getAliasGenerator('__lk_slt_'),
}: {
column: Column;
baseModelSqlv2: BaseModelSqlv2;
alias: string;
model: Model;
getAlias?: ReturnType<typeof getAliasGenerator>;
}): Promise<any> {
const knex = baseModelSqlv2.dbDriver;
const rootAlias = alias;
{
let selectQb;
const alias = getAlias();
let lookupColOpt: LookupColumn;
let isBtLookup = true;
if (column.uidt === UITypes.Lookup) {
lookupColOpt = await column.getColOptions<LookupColumn>();
} else if (column.uidt !== UITypes.LinkToAnotherRecord) {
NcError.badRequest('Invalid field type');
}
await column.getColOptions<LookupColumn>();
{
const relationCol = lookupColOpt
? await lookupColOpt.getRelationColumn()
: column;
const relation =
await relationCol.getColOptions<LinkToAnotherRecordColumn>();
// if not belongs to then throw error as we don't support
if (relation.type === RelationTypes.BELONGS_TO) {
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
selectQb = knex(
`${baseModelSqlv2.getTnPath(parentModel.table_name)} as ${alias}`,
).where(
`${alias}.${parentColumn.column_name}`,
knex.raw(`??`, [
`${rootAlias || baseModelSqlv2.getTnPath(childModel.table_name)}.${
childColumn.column_name
}`,
]),
);
}
// if not belongs to then throw error as we don't support
else if (relation.type === RelationTypes.HAS_MANY) {
isBtLookup = false;
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
selectQb = knex(
`${baseModelSqlv2.getTnPath(childModel.table_name)} as ${alias}`,
).where(
`${alias}.${childColumn.column_name}`,
knex.raw(`??`, [
`${rootAlias || baseModelSqlv2.getTnPath(parentModel.table_name)}.${
parentColumn.column_name
}`,
]),
);
}
// if not belongs to then throw error as we don't support
else if (relation.type === RelationTypes.MANY_TO_MANY) {
isBtLookup = false;
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
selectQb = knex(
`${baseModelSqlv2.getTnPath(parentModel.table_name)} as ${alias}`,
);
const mmTableAlias = getAlias();
const mmModel = await relation.getMMModel();
const mmChildCol = await relation.getMMChildColumn();
const mmParentCol = await relation.getMMParentColumn();
selectQb
.innerJoin(
baseModelSqlv2.getTnPath(mmModel.table_name, mmTableAlias),
knex.ref(`${mmTableAlias}.${mmParentCol.column_name}`),
'=',
knex.ref(`${alias}.${parentColumn.column_name}`),
)
.where(
knex.ref(`${mmTableAlias}.${mmChildCol.column_name}`),
'=',
knex.ref(
`${
rootAlias || baseModelSqlv2.getTnPath(childModel.table_name)
}.${childColumn.column_name}`,
),
);
}
}
let lookupColumn = lookupColOpt
? await lookupColOpt.getLookupColumn()
: await getDisplayValueOfRefTable(column);
// if lookup column is qr code or barcode extract the referencing column
if ([UITypes.QrCode, UITypes.Barcode].includes(lookupColumn.uidt)) {
lookupColumn = await lookupColumn
.getColOptions<BarcodeColumn | QrCodeColumn>()
.then((barcode) => barcode.getValueColumn());
}
let prevAlias = alias;
while (
lookupColumn.uidt === UITypes.Lookup ||
lookupColumn.uidt === UITypes.LinkToAnotherRecord
) {
const nestedAlias = getAlias();
let relationCol: Column<LinkToAnotherRecordColumn | LinksColumn>;
let nestedLookupColOpt: LookupColumn;
if (lookupColumn.uidt === UITypes.Lookup) {
nestedLookupColOpt = await lookupColumn.getColOptions<LookupColumn>();
relationCol = await nestedLookupColOpt.getRelationColumn();
} else {
relationCol = lookupColumn;
}
const relation =
await relationCol.getColOptions<LinkToAnotherRecordColumn>();
// if any of the relation in nested lookupColOpt is
// not belongs to then throw error as we don't support
if (relation.type === RelationTypes.BELONGS_TO) {
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
selectQb.join(
`${baseModelSqlv2.getTnPath(
parentModel.table_name,
)} as ${nestedAlias}`,
`${nestedAlias}.${parentColumn.column_name}`,
`${prevAlias}.${childColumn.column_name}`,
);
} else if (relation.type === RelationTypes.HAS_MANY) {
isBtLookup = false;
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
selectQb.join(
`${baseModelSqlv2.getTnPath(
childModel.table_name,
)} as ${nestedAlias}`,
`${nestedAlias}.${childColumn.column_name}`,
`${prevAlias}.${parentColumn.column_name}`,
);
} else if (relation.type === RelationTypes.MANY_TO_MANY) {
isBtLookup = false;
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
const mmTableAlias = getAlias();
const mmModel = await relation.getMMModel();
const mmChildCol = await relation.getMMChildColumn();
const mmParentCol = await relation.getMMParentColumn();
selectQb
.innerJoin(
baseModelSqlv2.getTnPath(mmModel.table_name, mmTableAlias),
knex.ref(`${mmTableAlias}.${mmChildCol.column_name}`),
'=',
knex.ref(`${prevAlias}.${childColumn.column_name}`),
)
.innerJoin(
knex.raw('?? as ??', [
baseModelSqlv2.getTnPath(parentModel.table_name),
nestedAlias,
]),
knex.ref(`${mmTableAlias}.${mmParentCol.column_name}`),
'=',
knex.ref(`${nestedAlias}.${parentColumn.column_name}`),
)
.where(
knex.ref(`${mmTableAlias}.${mmChildCol.column_name}`),
'=',
knex.ref(
`${alias || baseModelSqlv2.getTnPath(childModel.table_name)}.${
childColumn.column_name
}`,
),
);
}
if (lookupColumn.uidt === UITypes.Lookup)
lookupColumn = await nestedLookupColOpt.getLookupColumn();
else lookupColumn = await getDisplayValueOfRefTable(relationCol);
prevAlias = nestedAlias;
}
{
// get basemodel and model of lookup column
const model = await lookupColumn.getModel();
const baseModelSqlv2 = await Model.getBaseModelSQL({
model,
dbDriver: knex,
});
switch (lookupColumn.uidt) {
case UITypes.Attachment:
NcError.badRequest(
'Group by using attachment column is not supported',
);
break;
case UITypes.Links:
case UITypes.Rollup:
{
const builder = (
await genRollupSelectv2({
baseModelSqlv2,
knex,
columnOptions:
(await lookupColumn.getColOptions()) as RollupColumn,
alias: prevAlias,
})
).builder;
selectQb.select(builder);
}
break;
case UITypes.Formula:
{
const builder = (
await formulaQueryBuilderv2(
baseModelSqlv2,
(
await lookupColumn.getColOptions<FormulaColumn>()
).formula,
lookupColumn.title,
model,
lookupColumn,
await model.getAliasColMapping(),
prevAlias,
)
).builder;
selectQb.select(builder);
}
break;
case UITypes.DateTime:
{
await baseModelSqlv2.selectObject({
qb: selectQb,
columns: [lookupColumn],
alias: prevAlias,
});
}
break;
default:
{
selectQb.select(
`${prevAlias}.${lookupColumn.column_name} as ${lookupColumn.title}`,
);
}
break;
}
}
// if all relation are belongs to then we don't need to do the aggregation
if (isBtLookup) {
return {
builder: selectQb,
};
}
const subQueryAlias = getAlias();
if (baseModelSqlv2.isPg) {
// alternate approach with array_agg
return {
builder: knex
.select(knex.raw('json_agg(??)::text', [lookupColumn.title]))
.from(selectQb.as(subQueryAlias)),
};
/*
// alternate approach with array_agg
return {
builder: knex
.select(knex.raw('array_agg(??)', [lookupColumn.title]))
.from(selectQb),
};*/
// alternate approach with string aggregation
// return {
// builder: knex
// .select(
// knex.raw('STRING_AGG(??::text, ?)', [
// lookupColumn.title,
// LOOKUP_VAL_SEPARATOR,
// ]),
// )
// .from(selectQb.as(subQueryAlias)),
// };
} else if (baseModelSqlv2.isMySQL) {
return {
builder: knex
.select(
knex.raw('cast(JSON_ARRAYAGG(??) as NCHAR)', [lookupColumn.title]),
)
.from(selectQb.as(subQueryAlias)),
};
// return {
// builder: knex
// .select(
// knex.raw('GROUP_CONCAT(?? ORDER BY ?? ASC SEPARATOR ?)', [
// lookupColumn.title,
// lookupColumn.title,
// LOOKUP_VAL_SEPARATOR,
// ]),
// )
// .from(selectQb.as(subQueryAlias)),
// };
} else if (baseModelSqlv2.isSqlite) {
// ref: https://stackoverflow.com/questions/13382856/sqlite3-join-group-concat-using-distinct-with-custom-separator
// selectQb.orderBy(`${lookupColumn.title}`, 'asc');
return {
builder: knex
.select(
knex.raw(`group_concat(??, ?)`, [
lookupColumn.title,
LOOKUP_VAL_SEPARATOR,
]),
)
.from(selectQb.as(subQueryAlias)),
};
}
NcError.notImplemented('Database not supported Group by on Lookup');
}
}

4
packages/nocodb/src/db/sortV2.ts

@ -36,9 +36,7 @@ export default async function sortV2(
const column = await sort.getColumn();
if (!column) {
if (throwErrorIfInvalid) {
NcError.unprocessableEntity(
`Invalid column id '${sort.fk_column_id}' in sort`,
);
NcError.unprocessableEntity(`Invalid field: ${sort.fk_column_id}`);
}
continue;
}

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

@ -324,7 +324,7 @@ export function extractDBError(error): {
/ Invalid object name '(\w+)'./i,
);
const extractMissingColMatch = error.message.match(
/ Invalid column name '(\w+)'./i,
/ Invalid field: (\w+)./i,
);
if (extractTableNameMatch && extractTableNameMatch[1]) {

9
packages/nocodb/src/helpers/getAst.ts

@ -68,11 +68,12 @@ const getAst = async ({
fields = Array.isArray(fields) ? fields : fields.split(',');
if (throwErrorIfInvalidParams) {
const colAliasMap = await model.getColAliasMapping();
const invalidFields = fields.filter((f) => !colAliasMap[f]);
const aliasColMap = await model.getAliasColMapping();
const invalidFields = fields.filter(
(f) => !colAliasMap[f] && !aliasColMap[f],
);
if (invalidFields.length) {
NcError.unprocessableEntity(
`Following fields are invalid: ${invalidFields.join(', ')}`,
);
NcError.unprocessableEntity(`Invalid field: ${invalidFields[0]}`);
}
}
} else {

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

@ -2529,6 +2529,13 @@ export class AtImportProcessor {
await generateMigrationStats(aTblSchema);
}
} catch (e) {
// delete tables that were created
for (const table of ncSchema.tables) {
await this.tablesService.tableDelete({
tableId: table.id,
user: syncDB.user,
});
}
if (e.message) {
this.telemetryService.sendEvent({
evt_type: 'a:airtable-import:error',

20
packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts

@ -3,7 +3,7 @@ import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';
import papaparse from 'papaparse';
import debug from 'debug';
import { isLinksOrLTAR } from 'nocodb-sdk';
import { isLinksOrLTAR, isVirtualCol } from 'nocodb-sdk';
import { Base, Column, Model, Source } from '~/models';
import { BasesService } from '~/services/bases.service';
import {
@ -373,14 +373,16 @@ export class DuplicateProcessor {
});
// update cdf
await this.columnsService.columnUpdate({
columnId: findWithIdentifier(idMap, sourceColumn.id),
column: {
...destColumn,
cdf: oldCdf,
},
user: req.user,
});
if (!isVirtualCol(destColumn)) {
await this.columnsService.columnUpdate({
columnId: findWithIdentifier(idMap, sourceColumn.id),
column: {
...destColumn,
cdf: oldCdf,
},
user: req.user,
});
}
this.debugLog(`job completed for ${job.id} (${JobTypes.DuplicateModel})`);

11
packages/nocodb/src/modules/jobs/jobs/export-import/export.service.ts

@ -129,6 +129,17 @@ export class ExportService {
}
break;
case 'formula':
// rewrite formula_raw with aliases
column.colOptions['formula_raw'] = column.colOptions[k].replace(
/\{\{.*?\}\}/gm,
(match) => {
const col = model.columns.find(
(c) => c.id === match.slice(2, -2),
);
return `{${col?.title}}`;
},
);
column.colOptions[k] = column.colOptions[k].replace(
/(?<=\{\{).*?(?=\}\})/gm,
(match) => idMap.get(match),

1
packages/nocodb/src/plugins/backblaze/Backblaze.ts

@ -54,6 +54,7 @@ export default class Backblaze implements IStorageAdapterV2 {
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer',
})
.then((response) => {

1
packages/nocodb/src/plugins/gcs/Gcs.ts

@ -110,6 +110,7 @@ export default class Gcs implements IStorageAdapterV2 {
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer',
})
.then((response) => {

1
packages/nocodb/src/plugins/linode/LinodeObjectStorage.ts

@ -53,6 +53,7 @@ export default class LinodeObjectStorage implements IStorageAdapterV2 {
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer',
})
.then((response) => {

1
packages/nocodb/src/plugins/mino/Minio.ts

@ -100,6 +100,7 @@ export default class Minio implements IStorageAdapterV2 {
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer',
})
.then((response) => {

1
packages/nocodb/src/plugins/ovhCloud/OvhCloud.ts

@ -53,6 +53,7 @@ export default class OvhCloud implements IStorageAdapterV2 {
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer',
})
.then((response) => {

12
packages/nocodb/src/plugins/s3/S3.ts

@ -2,6 +2,7 @@ import fs from 'fs';
import { promisify } from 'util';
import { GetObjectCommand, S3 as S3Client } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Upload } from '@aws-sdk/lib-storage';
import axios from 'axios';
import { useAgent } from 'request-filtering-agent';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
@ -60,7 +61,7 @@ export default class S3 implements IStorageAdapterV2 {
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
responseType: 'arraybuffer',
responseType: 'stream',
})
.then((response) => {
uploadParams.Body = response.data;
@ -162,8 +163,13 @@ export default class S3 implements IStorageAdapterV2 {
private async upload(uploadParams): Promise<any> {
return new Promise((resolve, reject) => {
// call S3 to retrieve upload file to specified bucket
this.s3Client
.putObject({ ...this.defaultParams, ...uploadParams })
const upload = new Upload({
client: this.s3Client,
params: { ...this.defaultParams, ...uploadParams },
});
upload
.done()
.then((data) => {
if (data) {
resolve(

1
packages/nocodb/src/plugins/scaleway/ScalewayObjectStorage.ts

@ -103,6 +103,7 @@ export default class ScalewayObjectStorage implements IStorageAdapterV2 {
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer',
})
.then((response) => {

1
packages/nocodb/src/plugins/spaces/Spaces.ts

@ -53,6 +53,7 @@ export default class Spaces implements IStorageAdapterV2 {
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer',
})
.then((response) => {

1
packages/nocodb/src/plugins/upcloud/UpoCloud.ts

@ -53,6 +53,7 @@ export default class UpoCloud implements IStorageAdapterV2 {
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer',
})
.then((response) => {

1
packages/nocodb/src/plugins/vultr/Vultr.ts

@ -53,6 +53,7 @@ export default class Vultr implements IStorageAdapterV2 {
.get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer',
})
.then((response) => {

30
packages/nocodb/src/services/api-docs/api-docs.service.ts

@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import getSwaggerJSON from './swagger/getSwaggerJSON';
import getSwaggerJSONV2 from './swaggerV2/getSwaggerJSONV2';
import { NcError } from '~/helpers/catchError';
import { Base, Model } from '~/models';
@ -32,6 +33,35 @@ export class ApiDocsService {
},
] as any;
return swagger;
}
async swaggerJsonV2(param: { baseId: string; siteUrl: string }) {
const base = await Base.get(param.baseId);
if (!base) NcError.notFound();
const models = await Model.list({
base_id: param.baseId,
source_id: null,
});
const swagger = await getSwaggerJSONV2(base, models);
swagger.servers = [
{
url: param.siteUrl,
},
{
url: '{customUrl}',
variables: {
customUrl: {
default: param.siteUrl,
description: 'Provide custom nocodb app base url',
},
},
},
] as any;
return swagger;
}
}

2
packages/nocodb/src/services/api-docs/swagger/templates/paths.ts

@ -667,6 +667,6 @@ function getPaginatedResponseType(type: string) {
},
};
}
function isRelationExist(columns: SwaggerColumn[]) {
export function isRelationExist(columns: SwaggerColumn[]) {
return columns.some((c) => isLinksOrLTAR(c.column) && !c.column.system);
}

28
packages/nocodb/src/services/api-docs/swaggerV2/getPaths.ts

@ -0,0 +1,28 @@
import { getModelPaths } from './templates/paths';
import type { Model } from '~/models';
import type { SwaggerColumn } from './getSwaggerColumnMetas';
import type { SwaggerView } from './getSwaggerJSONV2';
import Noco from '~/Noco';
export default async function getPaths(
{
model,
columns,
views,
}: {
model: Model;
columns: SwaggerColumn[];
views: SwaggerView[];
},
_ncMeta = Noco.ncMeta,
) {
const swaggerPaths = await getModelPaths({
tableName: model.title,
tableId: model.id,
views,
type: model.type,
columns,
});
return swaggerPaths;
}

29
packages/nocodb/src/services/api-docs/swaggerV2/getSchemas.ts

@ -0,0 +1,29 @@
import { getModelSchemas } from './templates/schemas';
import type { Base, Model } from '~/models';
import type { SwaggerColumn } from './getSwaggerColumnMetas';
import type { SwaggerView } from './getSwaggerJSONV2';
import Noco from '~/Noco';
export default async function getSchemas(
{
base,
model,
columns,
}: {
base: Base;
model: Model;
columns: SwaggerColumn[];
views: SwaggerView[];
},
_ncMeta = Noco.ncMeta,
) {
const swaggerSchemas = getModelSchemas({
tableName: model.title,
orgs: 'v1',
baseName: base.title,
columns,
});
return swaggerSchemas;
}

67
packages/nocodb/src/services/api-docs/swaggerV2/getSwaggerColumnMetas.ts

@ -0,0 +1,67 @@
import { UITypes } from 'nocodb-sdk';
import type { Base, Column, LinkToAnotherRecordColumn } from '~/models';
import SwaggerTypes from '~/db/sql-mgr/code/routers/xc-ts/SwaggerTypes';
import Noco from '~/Noco';
export default async (
columns: Column[],
base: Base,
ncMeta = Noco.ncMeta,
): Promise<SwaggerColumn[]> => {
const dbType = await base.getBases().then((b) => b?.[0]?.type);
return Promise.all(
columns.map(async (c) => {
const field: SwaggerColumn = {
title: c.title,
type: 'object',
virtual: true,
column: c,
};
switch (c.uidt) {
case UITypes.LinkToAnotherRecord:
{
const colOpt = await c.getColOptions<LinkToAnotherRecordColumn>(
ncMeta,
);
if (colOpt) {
const relTable = await colOpt.getRelatedTable(ncMeta);
field.type = undefined;
field.$ref = `#/components/schemas/${relTable.title}Request`;
}
}
break;
case UITypes.Formula:
case UITypes.Lookup:
field.type = 'object';
break;
case UITypes.Rollup:
case UITypes.Links:
field.type = 'number';
break;
case UITypes.Attachment:
field.type = 'array';
field.items = {
$ref: `#/components/schemas/Attachment`,
};
break;
default:
field.virtual = false;
SwaggerTypes.setSwaggerType(c, field, dbType);
break;
}
return field;
}),
);
};
export interface SwaggerColumn {
type: any;
title: string;
description?: string;
virtual?: boolean;
$ref?: any;
column: Column;
items?: any;
}

66
packages/nocodb/src/services/api-docs/swaggerV2/getSwaggerJSONV2.ts

@ -0,0 +1,66 @@
import { ViewTypes } from 'nocodb-sdk';
import swaggerBase from './swagger-base.json';
import getPaths from './getPaths';
import getSchemas from './getSchemas';
import getSwaggerColumnMetas from './getSwaggerColumnMetas';
import type {
Base,
FormViewColumn,
GalleryViewColumn,
GridViewColumn,
Model,
View,
} from '~/models';
import Noco from '~/Noco';
export default async function getSwaggerJSONV2(
base: Base,
models: Model[],
ncMeta = Noco.ncMeta,
) {
// base swagger object
const swaggerObj = {
...swaggerBase,
paths: {},
components: {
...swaggerBase.components,
schemas: { ...swaggerBase.components.schemas },
},
};
// iterate and populate swagger schema and path for models and views
for (const model of models) {
let paths = {};
const columns = await getSwaggerColumnMetas(
await model.getColumns(ncMeta),
base,
ncMeta,
);
const views: SwaggerView[] = [];
for (const view of (await model.getViews(false, ncMeta)) || []) {
if (view.type !== ViewTypes.GRID) continue;
views.push({
view,
columns: await view.getColumns(ncMeta),
});
}
// skip mm tables
if (!model.mm) paths = await getPaths({ model, columns, views }, ncMeta);
const schemas = await getSchemas({ base, model, columns, views }, ncMeta);
Object.assign(swaggerObj.paths, paths);
Object.assign(swaggerObj.components.schemas, schemas);
}
return swaggerObj;
}
export interface SwaggerView {
view: View;
columns: Array<GridViewColumn | GalleryViewColumn | FormViewColumn>;
}

128
packages/nocodb/src/services/api-docs/swaggerV2/swagger-base.json

@ -0,0 +1,128 @@
{
"openapi": "3.0.0",
"info": {
"title": "nocodb",
"version": "2.0"
},
"servers": [
{
"url": "http://localhost:8080"
}
],
"paths": {
},
"components": {
"schemas": {
"Paginated": {
"title": "Paginated",
"type": "object",
"properties": {
"pageSize": {
"type": "integer"
},
"totalRows": {
"type": "integer"
},
"isFirstPage": {
"type": "boolean"
},
"isLastPage": {
"type": "boolean"
},
"page": {
"type": "number"
}
}
},
"Attachment": {
"title": "Attachment",
"type": "object",
"properties": {
"mimetype": {
"type": "string"
},
"size": {
"type": "integer"
},
"title": {
"type": "string"
},
"url": {
"type": "string"
},
"icon": {
"type": "string"
}
}
},
"Groupby": {
"title": "Groupby",
"type": "object",
"properties": {
"count": {
"type": "number",
"description": "count"
},
"column_name": {
"type": "string",
"description": "the value of the given column"
}
}
}
},
"securitySchemes": {
"xcAuth": {
"type": "apiKey",
"in": "header",
"name": "xc-auth",
"description": "JWT access token"
},
"xcToken": {
"type": "apiKey",
"in": "header",
"name": "xc-token",
"description": "API token"
}
},
"responses": {
"BadRequest": {
"description": "BadReqeust",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"msg": {
"type": "string",
"x-stoplight": {
"id": "p9mk4oi0hbihm"
},
"example": "BadRequest [Error]: <ERROR MESSAGE>"
}
},
"required": [
"msg"
]
},
"examples": {
"Example 1": {
"value": {
"msg": "BadRequest [Error]: <ERROR MESSAGE>"
}
}
}
}
},
"headers": {}
}
}
},
"security": [
{
"xcAuth": []
},
{
"xcToken": []
}
]
}

10
packages/nocodb/src/services/api-docs/swaggerV2/templates/headers.ts

@ -0,0 +1,10 @@
export const csvExportResponseHeader = {
'nc-export-offset': {
schema: {
type: 'integer',
},
description:
'Offset of next set of data which will be helpful if there is large amount of data. It will returns `-1` if all set of data exported.',
example: '1000',
},
};

237
packages/nocodb/src/services/api-docs/swaggerV2/templates/params.ts

@ -0,0 +1,237 @@
import { isLinksOrLTAR, RelationTypes, UITypes } from 'nocodb-sdk';
import type { LinkToAnotherRecordColumn } from '~/models';
import type { SwaggerColumn } from '../getSwaggerColumnMetas';
import type { SwaggerView } from '~/services/api-docs/swaggerV2/getSwaggerJSONV2';
export const recordIdParam = {
schema: {
type: 'string',
},
name: 'recordId',
in: 'path',
required: true,
example: 1,
description:
'Primary key of the record you want to read. If the table have composite primary key then combine them by using `___` and pass it as primary key.',
};
export const fieldsParam = {
schema: {
type: 'string',
},
in: 'query',
name: 'fields',
description:
'Array of field names or comma separated filed names to include in the response objects. In array syntax pass it like `fields[]=field1&fields[]=field2` or alternately `fields=field1,field2`.',
};
export const sortParam = {
schema: {
type: 'string',
},
in: 'query',
name: 'sort',
description:
'Comma separated field names to sort rows, rows will sort in ascending order based on provided columns. To sort in descending order provide `-` prefix along with column name, like `-field`. Example : `sort=field1,-field2`',
};
export const whereParam = {
schema: {
type: 'string',
},
in: 'query',
name: 'where',
description:
'This can be used for filtering rows, which accepts complicated where conditions. For more info visit [here](https://docs.nocodb.com/developer-resources/rest-apis#comparison-operators). Example : `where=(field1,eq,value)`',
};
export const limitParam = {
schema: {
type: 'number',
minimum: 1,
},
in: 'query',
name: 'limit',
description:
'The `limit` parameter used for pagination, the response collection size depends on limit value with default value `25` and maximum value `1000`, which can be overridden by environment variables `DB_QUERY_LIMIT_DEFAULT` and `DB_QUERY_LIMIT_MAX` respectively.',
example: 25,
};
export const offsetParam = {
schema: {
type: 'number',
minimum: 0,
},
in: 'query',
name: 'offset',
description:
'The `offset` parameter used for pagination, the value helps to select collection from a certain index.',
example: 0,
};
export const shuffleParam = {
schema: {
type: 'number',
minimum: 0,
maximum: 1,
},
in: 'query',
name: 'shuffle',
description:
'The `shuffle` parameter used for pagination, the response will be shuffled if it is set to 1.',
example: 0,
};
export const columnNameQueryParam = {
schema: {
type: 'string',
},
in: 'query',
name: 'column_name',
description:
'Column name of the column you want to group by, eg. `column_name=column1`',
};
export const linkFieldNameParam = (columns: SwaggerColumn[]) => {
const linkColumnIds = [];
const description = [
'**Links Field Identifier** corresponding to the relation field `Links` established between tables.\n\nLink Columns:',
];
for (const { column } of columns) {
if (!isLinksOrLTAR(column) || column.system) continue;
linkColumnIds.push(column.id);
description.push(`* ${column.id} - ${column.title}`);
}
return {
schema: {
type: 'string',
enum: linkColumnIds,
},
name: 'linkFieldId',
in: 'path',
required: true,
description: description.join('\n'),
};
};
export const viewIdParams = (views: SwaggerView[]) => {
const viewIds = [];
const description = [
'Allows you to fetch records that are currently visible within a specific view.\n\nViews:',
];
for (const { view } of views) {
viewIds.push(view.id);
description.push(
`* ${view.id} - ${view.is_default ? 'Default view' : view.title}`,
);
}
return {
schema: {
type: 'string',
enum: viewIds,
},
description: description.join('\n'),
name: 'viewId',
in: 'query',
required: false,
};
};
export const referencedRowIdParam = {
schema: {
type: 'string',
},
name: 'refRowId',
in: 'path',
required: true,
};
export const exportTypeParam = {
schema: {
type: 'string',
enum: ['csv', 'excel'],
},
name: 'type',
in: 'path',
required: true,
};
export const csvExportOffsetParam = {
schema: {
type: 'number',
minimum: 0,
},
in: 'query',
name: 'offset',
description:
'Helps to start export from a certain index. You can get the next set of data offset from previous response header named `nc-export-offset`.',
example: 0,
};
export const nestedWhereParam = (colName) => ({
schema: {
type: 'string',
},
in: 'query',
name: `nested[${colName}][where]`,
description: `This can be used for filtering rows in nested column \`${colName}\`, which accepts complicated where conditions. For more info visit [here](https://docs.nocodb.com/developer-resources/rest-apis#comparison-operators). Example : \`nested[${colName}][where]=(field1,eq,value)\``,
});
export const nestedFieldParam = (colName) => ({
schema: {
type: 'string',
},
in: 'query',
name: `nested[${colName}][fields]`,
description: `Array of field names or comma separated filed names to include in the in nested column \`${colName}\` result. In array syntax pass it like \`fields[]=field1&fields[]=field2.\`. Example : \`nested[${colName}][fields]=field1,field2\``,
});
export const nestedSortParam = (colName) => ({
schema: {
type: 'string',
},
in: 'query',
name: `nested[${colName}][sort]`,
description: `Comma separated field names to sort rows in nested column \`${colName}\` rows, it will sort in ascending order based on provided columns. To sort in descending order provide \`-\` prefix along with column name, like \`-field\`. Example : \`nested[${colName}][sort]=field1,-field2\``,
});
export const nestedLimitParam = (colName) => ({
schema: {
type: 'number',
minimum: 1,
},
in: 'query',
name: `nested[${colName}][limit]`,
description: `The \`limit\` parameter used for pagination of nested \`${colName}\` rows, the response collection size depends on limit value and default value is \`25\`.`,
example: '25',
});
export const nestedOffsetParam = (colName) => ({
schema: {
type: 'number',
minimum: 0,
},
in: 'query',
name: `nested[${colName}][offset]`,
description: `The \`offset\` parameter used for pagination of nested \`${colName}\` rows, the value helps to select collection from a certain index.`,
example: 0,
});
export const getNestedParams = async (
columns: SwaggerColumn[],
): Promise<any[]> => {
return await columns.reduce(async (paramsArr, { column }) => {
if (column.uidt === UITypes.LinkToAnotherRecord && !column.system) {
const colOpt = await column.getColOptions<LinkToAnotherRecordColumn>();
if (colOpt.type !== RelationTypes.BELONGS_TO) {
return [
...(await paramsArr),
nestedWhereParam(column.title),
nestedOffsetParam(column.title),
nestedLimitParam(column.title),
nestedFieldParam(column.title),
nestedSortParam(column.title),
];
} else {
return [...(await paramsArr), nestedFieldParam(column.title)];
}
}
return paramsArr;
}, Promise.resolve([]));
};

407
packages/nocodb/src/services/api-docs/swaggerV2/templates/paths.ts

@ -0,0 +1,407 @@
import { ModelTypes } from 'nocodb-sdk';
import {
fieldsParam,
getNestedParams,
limitParam,
linkFieldNameParam,
offsetParam,
recordIdParam,
shuffleParam,
sortParam,
viewIdParams,
whereParam,
} from './params';
import type { SwaggerColumn } from '../getSwaggerColumnMetas';
import type { SwaggerView } from '~/services/api-docs/swaggerV2/getSwaggerJSONV2';
import { isRelationExist } from '~/services/api-docs/swagger/templates/paths';
export const getModelPaths = async (ctx: {
tableName: string;
type: ModelTypes;
columns: SwaggerColumn[];
tableId: string;
views: SwaggerView[];
}): Promise<{ [path: string]: any }> => ({
[`/api/v2/tables/${ctx.tableId}/records`]: {
get: {
summary: `${ctx.tableName} list`,
operationId: `${ctx.tableName.toLowerCase()}-db-table-row-list`,
description: `List of all rows from ${ctx.tableName} ${ctx.type} and response data fields can be filtered based on query params.`,
tags: [ctx.tableName],
parameters: [
viewIdParams(ctx.views),
fieldsParam,
sortParam,
whereParam,
limitParam,
shuffleParam,
offsetParam,
...(await getNestedParams(ctx.columns)),
],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: getPaginatedResponseType(`${ctx.tableName}Response`),
},
},
},
},
},
...(ctx.type === ModelTypes.TABLE
? {
post: {
summary: `${ctx.tableName} create`,
description:
'Insert a new row in table by providing a key value pair object where key refers to the column alias. All the required fields should be included with payload excluding `autoincrement` and column with default value.',
operationId: `${ctx.tableName.toLowerCase()}-create`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}Response`,
},
},
},
},
'400': {
$ref: '#/components/responses/BadRequest',
},
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {
oneOf: [
{
$ref: `#/components/schemas/${ctx.tableName}Request`,
},
{
type: 'array',
items: {
$ref: `#/components/schemas/${ctx.tableName}Request`,
},
},
],
},
},
},
},
},
patch: {
summary: `${ctx.tableName} update`,
operationId: `${ctx.tableName.toLowerCase()}-update`,
description:
'Partial update row in table by providing a key value pair object where key refers to the column alias. You need to only include columns which you want to update.',
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {},
},
},
},
'400': {
$ref: '#/components/responses/BadRequest',
},
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {
oneOf: [
{
$ref: `#/components/schemas/${ctx.tableName}Request`,
},
{
type: 'array',
items: {
$ref: `#/components/schemas/${ctx.tableName}Request`,
},
},
],
},
},
},
},
},
delete: {
summary: `${ctx.tableName} delete`,
operationId: `${ctx.tableName.toLowerCase()}-delete`,
responses: {
'200': {
description: 'OK',
},
},
tags: [ctx.tableName],
description:
'Delete a row by using the **primary key** column value.',
requestBody: {
content: {
'application/json': {
schema: {
oneOf: [
{
$ref: `#/components/schemas/${ctx.tableName}IdRequest`,
},
{
type: 'array',
items: {
$ref: `#/components/schemas/${ctx.tableName}IdRequest`,
},
},
],
},
},
},
},
},
}
: {}),
},
[`/api/v2/tables/${ctx.tableId}/records/{recordId}`]: {
get: {
parameters: [recordIdParam, fieldsParam],
summary: `${ctx.tableName} read`,
description: 'Read a row data by using the **primary key** column value.',
operationId: `${ctx.tableName.toLowerCase()}-read`,
tags: [ctx.tableName],
responses: {
'201': {
description: 'Created',
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}Response`,
},
},
},
},
},
},
},
[`/api/v2/tables/${ctx.tableId}/records/count`]: {
parameters: [viewIdParams(ctx.views)],
get: {
summary: `${ctx.tableName} count`,
operationId: `${ctx.tableName.toLowerCase()}-count`,
description: 'Get rows count of a table by applying optional filters.',
tags: [ctx.tableName],
parameters: [whereParam],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
count: {
type: 'number',
},
},
required: ['list', 'pageInfo'],
},
examples: {
'Example 1': {
value: {
count: 3,
},
},
},
},
},
},
'400': {
$ref: '#/components/responses/BadRequest',
},
},
},
},
...(isRelationExist(ctx.columns)
? {
[`/api/v2/tables/${ctx.tableId}/links/{linkFieldId}/records/{recordId}`]:
{
parameters: [linkFieldNameParam(ctx.columns), recordIdParam],
get: {
summary: 'Link Records list',
operationId: `${ctx.tableName.toLowerCase()}-nested-list`,
description:
'This API endpoint allows you to retrieve list of linked records for a specific `Link field` and `Record ID`. The response is an array of objects containing Primary Key and its corresponding display value.',
tags: [ctx.tableName],
parameters: [
fieldsParam,
sortParam,
whereParam,
limitParam,
offsetParam,
],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
list: {
type: 'array',
description: 'List of data objects',
items: {
type: 'object',
},
},
pageInfo: {
$ref: '#/components/schemas/Paginated',
description: 'Paginated Info',
},
},
required: ['list', 'pageInfo'],
},
},
},
},
'400': {
$ref: '#/components/responses/BadRequest',
},
},
},
post: {
summary: 'Link Records',
operationId: `${ctx.tableName.toLowerCase()}-nested-link`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {},
examples: {
'Example 1': {
value: true,
},
},
},
},
},
'400': {
$ref: '#/components/responses/BadRequest',
},
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {
oneOf: [
{
type: 'object',
},
{
type: 'array',
items: {
type: 'object',
},
},
],
},
examples: {
'Example 1': {
value: [
{
Id: 4,
},
{
Id: 5,
},
],
},
},
},
},
},
description:
'This API endpoint allows you to link records to a specific `Link field` and `Record ID`. The request payload is an array of record-ids from the adjacent table for linking purposes. Note that any existing links, if present, will be unaffected during this operation.',
parameters: [recordIdParam],
},
delete: {
summary: 'Unlink Records',
operationId: `${ctx.tableName.toLowerCase()}-nested-unlink`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {},
examples: {
'Example 1': {
value: true,
},
},
},
},
},
'400': {
$ref: '#/components/responses/BadRequest',
},
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {
oneOf: [
{
type: 'array',
items: {
type: 'object',
},
},
],
},
examples: {
'Example 1': {
value: [
{
Id: 1,
},
{
Id: 2,
},
],
},
},
},
},
},
description:
'This API endpoint allows you to unlink records from a specific `Link field` and `Record ID`. The request payload is an array of record-ids from the adjacent table for unlinking purposes. Note that, \n- duplicated record-ids will be ignored.\n- non-existent record-ids will be ignored.',
parameters: [recordIdParam],
},
},
}
: {}),
});
function getPaginatedResponseType(type: string) {
return {
type: 'object',
properties: {
list: {
type: 'array',
items: {
$ref: `#/components/schemas/${type}`,
},
},
PageInfo: {
$ref: `#/components/schemas/Paginated`,
},
},
};
}

109
packages/nocodb/src/services/api-docs/swaggerV2/templates/schemas.ts

@ -0,0 +1,109 @@
import { isSystemColumn } from 'nocodb-sdk';
import type { SwaggerColumn } from '../getSwaggerColumnMetas';
export const getModelSchemas = (ctx: {
tableName: string;
orgs: string;
baseName: string;
columns: Array<SwaggerColumn>;
}) => ({
[`${ctx.tableName}Response`]: {
title: `${ctx.tableName} Response`,
type: 'object',
description: '',
'x-internal': false,
properties: {
...(ctx.columns?.reduce(
(colsObj, { title, virtual, column, ...fieldProps }) => ({
...colsObj,
...(column.system
? {}
: {
[title]: fieldProps,
}),
}),
{},
) || {}),
},
},
[`${ctx.tableName}Request`]: {
title: `${ctx.tableName} Request`,
type: 'object',
description: '',
'x-internal': false,
properties: {
...(ctx.columns?.reduce(
(colsObj, { title, virtual, column, ...fieldProps }) => ({
...colsObj,
...(virtual || isSystemColumn(column) || column.ai || column.meta?.ag
? {}
: {
[title]: fieldProps,
}),
}),
{},
) || {}),
},
},
[`${ctx.tableName}IdRequest`]: {
title: `${ctx.tableName} Id Request`,
type: 'object',
description: '',
'x-internal': false,
properties: {
...(ctx.columns?.reduce(
(colsObj, { title, virtual, column, ...fieldProps }) => ({
...colsObj,
...(column.pk
? {
[title]: fieldProps,
}
: {}),
}),
{},
) || {}),
},
},
});
export const getViewSchemas = (ctx: {
tableName: string;
viewName: string;
orgs: string;
baseName: string;
columns: Array<SwaggerColumn>;
}) => ({
[`${ctx.tableName}${ctx.viewName}GridResponse`]: {
title: `${ctx.tableName} : ${ctx.viewName} Response`,
type: 'object',
description: '',
'x-internal': false,
properties: {
...(ctx.columns?.reduce(
(colsObj, { title, virtual, column, ...fieldProps }) => ({
...colsObj,
[title]: fieldProps,
}),
{},
) || {}),
},
},
[`${ctx.tableName}${ctx.viewName}GridRequest`]: {
title: `${ctx.tableName} : ${ctx.viewName} Request`,
type: 'object',
description: '',
'x-internal': false,
properties: {
...(ctx.columns?.reduce(
(colsObj, { title, virtual, column, ...fieldProps }) => ({
...colsObj,
...(virtual
? {}
: {
[title]: fieldProps,
}),
}),
{},
) || {}),
},
},
});

3
packages/nocodb/src/services/datas.service.ts

@ -27,6 +27,7 @@ export class DatasService {
model,
view,
query: param.query,
throwErrorIfInvalidParams: true,
});
}
@ -51,7 +52,7 @@ export class DatasService {
dbDriver: await NcConnectionMgrv2.get(source),
});
const countArgs: any = { ...param.query };
const countArgs: any = { ...param.query, throwErrorIfInvalidParams: true };
try {
countArgs.filterArr = JSON.parse(countArgs.filterArrJson);
} catch (e) {}

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

@ -461,9 +461,7 @@ export class TablesService {
}
if (column.column_name.length > mxColumnLength) {
NcError.badRequest(
`Column name ${column.column_name} exceeds ${mxColumnLength} characters`,
);
column.column_name = column.column_name.slice(0, mxColumnLength);
}
if (column.title && column.title.length > 255) {

5
packages/nocodb/src/utils/globals.ts

@ -171,6 +171,11 @@ export enum CacheDelDirection {
CHILD_TO_PARENT = 'CHILD_TO_PARENT',
}
export const GROUPBY_COMPARISON_OPS = <const>[
// these are used for groupby
'gb_eq',
'gb_null',
];
export const COMPARISON_OPS = <const>[
'eq',
'neq',

10
packages/nocodb/tests/unit/rest/tests/groupby.test.ts

@ -299,7 +299,7 @@ function groupByTests() {
expect(response.body.list.length).to.equal(1);
});
it('Check One GroupBy Column with MM Lookup which is not supported', async function () {
it('Check One GroupBy Column with MM Lookup which is supported', async function () {
await createLookupColumn(context, {
base: sakilaProject,
title: 'ActorNames',
@ -308,15 +308,17 @@ function groupByTests() {
relatedTableColumnTitle: 'FirstName',
});
const res = await request(context.app)
const response = await request(context.app)
.get(`/api/v1/db/data/noco/${sakilaProject.id}/${filmTable.id}/groupby`)
.set('xc-auth', context.token)
.query({
column_name: 'ActorNames',
})
.expect(400);
.expect(200);
assert.match(res.body.msg, /not supported/);
assert.match(response.body.list[1]['ActorNames'], /ADAM|ANNE/);
expect(+response.body.list[1]['count']).to.gt(0);
expect(response.body.list.length).to.equal(25);
});
it('Check One GroupBy Column with Formula and Formula referring another formula', async function () {

3496
pnpm-lock.yaml

File diff suppressed because it is too large Load Diff

3
renovate.json

@ -3,7 +3,6 @@
"extends": [
"config:base",
":dependencyDashboard",
":onlyNpm",
":prConcurrentLimit20",
":autodetectPinVersions",
":label(renovate)",
@ -62,5 +61,5 @@
"assignees": [
"wingkwong"
],
"enabled": false
"enabled": true
}

8
scripts/pkg-executable/package.json

@ -27,9 +27,9 @@
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"nocodb": "0.202.5",
"@nestjs/common": "^10.2.1",
"@nestjs/core": "^10.2.1"
"express": "^4.17.3",
"nocodb": "0.202.7",
"@nestjs/common": "^10.2.9",
"@nestjs/core": "^10.2.9"
}
}

6
tests/playwright/pages/Dashboard/Import/ImportTemplate.ts

@ -23,7 +23,11 @@ export class ImportTemplatePage extends BasePage {
const rowCount = await tr.count();
const tableList: string[] = [];
for (let i = 0; i < rowCount; i++) {
const tableName = await getTextExcludeIconText(tr.nth(i));
const tableName = await this.get()
.locator(`.ant-collapse-header`)
.nth(i)
.locator('input[type="text"]')
.inputValue();
tableList.push(tableName);
}
return tableList;

Loading…
Cancel
Save