Browse Source

Merge pull request #6092 from nocodb/develop

pull/6093/head 0.109.5
github-actions[bot] 1 year ago committed by GitHub
parent
commit
76a4c22ec8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 36
      build-local-docker-image.sh
  2. 4
      packages/nc-gui/components/account/Token.vue
  3. 22
      packages/nc-gui/components/smartsheet/Grid.vue
  4. 9
      packages/nc-gui/components/smartsheet/Kanban.vue
  5. 13
      packages/nc-gui/components/smartsheet/Pagination.vue
  6. 9
      packages/nc-gui/lang/en.json
  7. 52
      packages/nc-gui/package-lock.json
  8. 2
      packages/nc-gui/package.json
  9. 14
      packages/nc-gui/pages/account/index.vue
  10. 140
      packages/noco-docs/content/en/developer-resources/upload-via-api
  11. 4
      packages/nocodb-sdk/package-lock.json
  12. 30
      packages/nocodb/package-lock.json
  13. 10
      packages/nocodb/package.json
  14. 66
      packages/nocodb/src/db/BaseModelSqlv2.ts
  15. 2
      packages/nocodb/src/db/sql-mgr/code/models/xc/ModelXcMetaMssql.ts
  16. 2
      packages/nocodb/src/db/sql-mgr/code/models/xc/ModelXcMetaMysql.ts
  17. 2
      packages/nocodb/src/db/sql-mgr/code/models/xc/ModelXcMetaOracle.ts

36
build-local-docker-image.sh

@ -1,18 +1,30 @@
#!/bin/bash #!/bin/bash
# script to build local docker image. # script to build local docker image.
# highlevel steps involved # highlevel steps involved
# 1. build nocodb-sdk # 1. Stop and remove existing container and image
# 2. build nc-gui # 2. Build nocodb-sdk
# 2a. static build of nc-gui # 3. Build nc-gui
# 2b. copy nc-gui build to nocodb dir # 3a. static build of nc-gui
# 3. build nocodb # 3b. copy nc-gui build to nocodb dir
# 4. Build nocodb
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
LOG_FILE=${SCRIPT_DIR}/build-local-docker-image.log LOG_FILE=${SCRIPT_DIR}/build-local-docker-image.log
ERROR="" ERROR=""
function stop_and_remove_container() {
# Stop and remove the existing container
docker stop nocodb-local >/dev/null 2>&1
docker rm nocodb-local >/dev/null 2>&1
}
function remove_image() {
# Remove the existing image
docker rmi nocodb-local >/dev/null 2>&1
}
function build_sdk(){ function build_sdk(){
#build nocodb-sdk # build nocodb-sdk
cd ${SCRIPT_DIR}/packages/nocodb-sdk cd ${SCRIPT_DIR}/packages/nocodb-sdk
npm ci || ERROR="sdk build failed" npm ci || ERROR="sdk build failed"
npm run build || ERROR="sdk build failed" npm run build || ERROR="sdk build failed"
@ -57,20 +69,24 @@ function log_message(){
fi fi
} }
echo "Info: Building nocodb-sdk" | tee ${LOG_FILE} echo "Info: Stopping and removing existing container and image" | tee ${LOG_FILE}
stop_and_remove_container
remove_image
echo "Info: Building nocodb-sdk" | tee -a ${LOG_FILE}
build_sdk 1>> ${LOG_FILE} 2>> ${LOG_FILE} build_sdk 1>> ${LOG_FILE} 2>> ${LOG_FILE}
echo "Info: Building nc-gui" | tee -a ${LOG_FILE} echo "Info: Building nc-gui" | tee -a ${LOG_FILE}
build_gui 1>> ${LOG_FILE} 2>> ${LOG_FILE} build_gui 1>> ${LOG_FILE} 2>> ${LOG_FILE}
echo "Info: copy nc-gui build to nocodb dir" | tee -a ${LOG_FILE} echo "Info: Copy nc-gui build to nocodb dir" | tee -a ${LOG_FILE}
copy_gui_artifacts 1>> ${LOG_FILE} 2>> ${LOG_FILE} copy_gui_artifacts 1>> ${LOG_FILE} 2>> ${LOG_FILE}
echo "Info: build nocodb, package nocodb-sdk and nc-gui" | tee -a ${LOG_FILE} echo "Info: Build nocodb, package nocodb-sdk and nc-gui" | tee -a ${LOG_FILE}
package_nocodb 1>> ${LOG_FILE} 2>> ${LOG_FILE} package_nocodb 1>> ${LOG_FILE} 2>> ${LOG_FILE}
if [[ ${ERROR} == "" ]]; then if [[ ${ERROR} == "" ]]; then
echo "Info: building docker image" | tee -a ${LOG_FILE} echo "Info: Building docker image" | tee -a ${LOG_FILE}
build_image 1>> ${LOG_FILE} 2>> ${LOG_FILE} build_image 1>> ${LOG_FILE} 2>> ${LOG_FILE}
fi fi

4
packages/nc-gui/components/account/Token.vue

@ -101,7 +101,7 @@ const descriptionInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
<template> <template>
<div class="h-full overflow-y-scroll scrollbar-thin-dull pt-2"> <div class="h-full overflow-y-scroll scrollbar-thin-dull pt-2">
<div class="max-w-[900px] mx-auto p-4" data-testid="nc-token-list"> <div class="max-w-[900px] mx-auto p-4" data-testid="nc-token-list">
<div class="text-xl my-4 text-left font-weight-bold">Token Management</div> <div class="text-xl my-4 text-left font-weight-bold">{{ $t('title.tokenManagement') }}</div>
<div class="py-2 flex gap-4 items-center"> <div class="py-2 flex gap-4 items-center">
<div class="flex-grow"></div> <div class="flex-grow"></div>
<component :is="iconMap.reload" class="cursor-pointer" @click="loadTokens" /> <component :is="iconMap.reload" class="cursor-pointer" @click="loadTokens" />
@ -114,7 +114,7 @@ const descriptionInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
> >
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<component :is="iconMap.plus" /> <component :is="iconMap.plus" />
Add new token {{ $t('title.addNewToken') }}
</div> </div>
</a-button> </a-button>
</div> </div>

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

@ -112,7 +112,7 @@ const expandedFormRow = ref<Row>()
const expandedFormRowState = ref<Record<string, any>>() const expandedFormRowState = ref<Record<string, any>>()
const gridWrapper = ref<HTMLElement>() const gridWrapper = ref<HTMLElement>()
const tableHeadEl = ref<HTMLElement>() const tableHeadEl = ref<HTMLElement>()
const tableBodyEl = ref<HTMLElement>() const tableBodyEl = ref<HTMLTableSectionElement>()
const fillHandle = ref<HTMLElement>() const fillHandle = ref<HTMLElement>()
const gridRect = useElementBounding(gridWrapper) const gridRect = useElementBounding(gridWrapper)
@ -917,8 +917,6 @@ function addEmptyRow(row?: number) {
const fillHandleTop = ref() const fillHandleTop = ref()
const fillHandleLeft = ref() const fillHandleLeft = ref()
const cellRefs = ref<{ el: HTMLElement }[]>([])
const showFillHandle = computed( const showFillHandle = computed(
() => () =>
!readOnly.value && !readOnly.value &&
@ -929,17 +927,15 @@ const showFillHandle = computed(
) )
const refreshFillHandle = () => { const refreshFillHandle = () => {
const cellRef = cellRefs.value.find( nextTick(() => {
(cell) => const cellRef = document.querySelector('.last-cell')
cell.el.dataset.rowIndex === String(isNaN(selectedRange.end.row) ? activeCell.row : selectedRange.end.row) &&
cell.el.dataset.colIndex === String(isNaN(selectedRange.end.col) ? activeCell.col : selectedRange.end.col),
)
if (cellRef) { if (cellRef) {
const cellRect = useElementBounding(cellRef.el) const cellRect = cellRef.getBoundingClientRect()
if (!cellRect || !gridWrapper.value) return if (!cellRect || !gridWrapper.value) return
fillHandleTop.value = cellRect.top.value + cellRect.height.value - gridRect.top.value + gridWrapper.value.scrollTop fillHandleTop.value = cellRect.top + cellRect.height - gridRect.top.value + gridWrapper.value.scrollTop
fillHandleLeft.value = cellRect.left.value + cellRect.width.value - gridRect.left.value + gridWrapper.value.scrollLeft fillHandleLeft.value = cellRect.left + cellRect.width - gridRect.left.value + gridWrapper.value.scrollLeft
} }
})
} }
const addRowExpandOnClose = (row: Row) => { const addRowExpandOnClose = (row: Row) => {
@ -1132,7 +1128,6 @@ useEventListener(document, 'mouseup', () => {
<SmartsheetTableDataCell <SmartsheetTableDataCell
v-for="(columnObj, colIndex) of fields" v-for="(columnObj, colIndex) of fields"
:key="columnObj.id" :key="columnObj.id"
ref="cellRefs"
class="cell relative nc-grid-cell" class="cell relative nc-grid-cell"
:class="{ :class="{
'cursor-pointer': hasEditPermission, 'cursor-pointer': hasEditPermission,
@ -1141,6 +1136,9 @@ useEventListener(document, 'mouseup', () => {
hasEditPermission && hasEditPermission &&
((activeCell.row === rowIndex && activeCell.col === colIndex) || ((activeCell.row === rowIndex && activeCell.col === colIndex) ||
(selectedRange._start?.row === rowIndex && selectedRange._start?.col === colIndex)), (selectedRange._start?.row === rowIndex && selectedRange._start?.col === colIndex)),
'last-cell':
rowIndex === (isNaN(selectedRange.end.row) ? activeCell.row : selectedRange.end.row) &&
colIndex === (isNaN(selectedRange.end.col) ? activeCell.col : selectedRange.end.col),
'nc-required-cell': isColumnRequiredAndNull(columnObj, row.row), 'nc-required-cell': isColumnRequiredAndNull(columnObj, row.row),
'align-middle': !rowHeight || rowHeight === 1, 'align-middle': !rowHeight || rowHeight === 1,
'align-top': rowHeight && rowHeight !== 1, 'align-top': rowHeight && rowHeight !== 1,

9
packages/nc-gui/components/smartsheet/Kanban.vue

@ -21,6 +21,7 @@ import {
onBeforeUnmount, onBeforeUnmount,
provide, provide,
useAttachment, useAttachment,
useDebounceFn,
useKanbanViewStoreOrThrow, useKanbanViewStoreOrThrow,
useUndoRedo, useUndoRedo,
} from '#imports' } from '#imports'
@ -30,6 +31,8 @@ interface Attachment {
url: string url: string
} }
const INFINITY_SCROLL_THRESHOLD = 100
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref()) const view = inject(ActiveViewInj, ref())
@ -275,14 +278,14 @@ async function onMove(event: any, stackKey: string) {
} }
} }
const kanbanListScrollHandler = async (e: any) => { const kanbanListScrollHandler = useDebounceFn(async (e: any) => {
if (e.target.scrollTop + e.target.clientHeight >= e.target.scrollHeight) { if (e.target.scrollTop + e.target.clientHeight + INFINITY_SCROLL_THRESHOLD >= e.target.scrollHeight) {
const stackTitle = e.target.getAttribute('data-stack-title') const stackTitle = e.target.getAttribute('data-stack-title')
const pageSize = appInfo.defaultLimit || 25 const pageSize = appInfo.defaultLimit || 25
const page = Math.ceil(formattedData.value.get(stackTitle)!.length / pageSize) const page = Math.ceil(formattedData.value.get(stackTitle)!.length / pageSize)
await loadMoreKanbanData(stackTitle, { offset: page * pageSize }) await loadMoreKanbanData(stackTitle, { offset: page * pageSize })
} }
} })
const kanbanListRef = (kanbanListElement: HTMLElement) => { const kanbanListRef = (kanbanListElement: HTMLElement) => {
if (kanbanListElement) { if (kanbanListElement) {

13
packages/nc-gui/components/smartsheet/Pagination.vue

@ -1,10 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ChangePageInj, PaginationDataInj, computed, iconMap, inject } from '#imports' import { ChangePageInj, PaginationDataInj, computed, iconMap, inject, isRtlLang, useI18n } from '#imports'
import type { Language } from '~/lib'
const props = defineProps<{ const props = defineProps<{
alignCountOnRight?: boolean alignCountOnRight?: boolean
}>() }>()
const { locale } = useI18n()
const paginatedData = inject(PaginationDataInj)! const paginatedData = inject(PaginationDataInj)!
const changePage = inject(ChangePageInj)! const changePage = inject(ChangePageInj)!
@ -19,6 +22,8 @@ const page = computed({
changePage?.(p) changePage?.(p)
}, },
}) })
const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Language))
</script> </script>
<template> <template>
@ -39,6 +44,7 @@ const page = computed({
v-model:page-size="size" v-model:page-size="size"
size="small" size="small"
class="!text-xs !m-1 nc-pagination" class="!text-xs !m-1 nc-pagination"
:class="{ 'rtl-pagination': isRTLLanguage }"
:total="count" :total="count"
show-less-items show-less-items
:show-size-changer="false" :show-size-changer="false"
@ -77,4 +83,9 @@ const page = computed({
:deep(.ant-pagination-item-link) { :deep(.ant-pagination-item-link) {
@apply text-gray-500 flex items-center justify-center; @apply text-gray-500 flex items-center justify-center;
} }
:deep(.rtl-pagination .ant-pagination-prev .ant-pagination-item-link),
:deep(.rtl-pagination .ant-pagination-next .ant-pagination-item-link) {
@apply transform rotate-180;
}
</style> </style>

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

@ -211,7 +211,14 @@
"codeSnippet": "Code Snippet", "codeSnippet": "Code Snippet",
"keyboardShortcut": "Keyboard Shortcuts", "keyboardShortcut": "Keyboard Shortcuts",
"generateRandomName": "Generate Random Name", "generateRandomName": "Generate Random Name",
"findRowByScanningCode": "Find row by scanning a QR or Barcode" "findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
"userManagement": "User Management",
"licence": "Licence"
}, },
"labels": { "labels": {
"createdBy": "Created By", "createdBy": "Created By",

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

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

2
packages/nc-gui/package.json

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

14
packages/nc-gui/pages/account/index.vue

@ -30,7 +30,7 @@ const openKeys = ref([/^\/account\/users/.test($route.fullPath) && 'users'])
class="tabs-menu h-full" class="tabs-menu h-full"
mode="inline" mode="inline"
> >
<div class="text-xs text-gray-500 ml-4 pt-4 pb-2 font-weight-bold">Account Settings</div> <div class="text-xs text-gray-500 ml-4 pt-4 pb-2 font-weight-bold">{{ $t('title.accountSettings') }}</div>
<a-sub-menu key="users" class="!bg-white"> <a-sub-menu key="users" class="!bg-white">
<template #icon> <template #icon>
@ -44,10 +44,10 @@ const openKeys = ref([/^\/account\/users/.test($route.fullPath) && 'users'])
class="text-xs" class="text-xs"
@click="navigateTo('/account/users/list')" @click="navigateTo('/account/users/list')"
> >
<span class="ml-4">User Management</span> <span class="ml-4">{{ $t('title.userManagement') }}</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="password-reset" class="text-xs" @click="navigateTo('/account/users/password-reset')"> <a-menu-item key="password-reset" class="text-xs" @click="navigateTo('/account/users/password-reset')">
<span class="ml-4">Reset Password</span> <span class="ml-4">{{ $t('title.resetPasswordMenu') }}</span>
</a-menu-item> </a-menu-item>
<a-menu-item <a-menu-item
v-if="isUIAllowed('superAdminAppSettings')" v-if="isUIAllowed('superAdminAppSettings')"
@ -55,7 +55,7 @@ const openKeys = ref([/^\/account\/users/.test($route.fullPath) && 'users'])
class="text-xs" class="text-xs"
@click="navigateTo('/account/users/settings')" @click="navigateTo('/account/users/settings')"
> >
<span class="ml-4">Settings</span> <span class="ml-4">{{ $t('activity.settings') }}</span>
</a-menu-item> </a-menu-item>
</a-sub-menu> </a-sub-menu>
@ -67,7 +67,7 @@ const openKeys = ref([/^\/account\/users/.test($route.fullPath) && 'users'])
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<MdiShieldKeyOutline /> <MdiShieldKeyOutline />
<div class="select-none">Tokens</div> <div class="select-none">{{ $t('title.tokens') }}</div>
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item <a-menu-item
@ -79,7 +79,7 @@ const openKeys = ref([/^\/account\/users/.test($route.fullPath) && 'users'])
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<component :is="iconMap.appStore" /> <component :is="iconMap.appStore" />
<div class="select-none">App Store</div> <div class="select-none">{{ $t('title.appStore') }}</div>
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item <a-menu-item
@ -91,7 +91,7 @@ const openKeys = ref([/^\/account\/users/.test($route.fullPath) && 'users'])
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<component :is="iconMap.key" /> <component :is="iconMap.key" />
<div class="select-none">License</div> <div class="select-none">{{ $t('title.licence') }}</div>
</div> </div>
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>

140
packages/noco-docs/content/en/developer-resources/upload-via-api

@ -0,0 +1,140 @@
---
title: 'Upload via API'
description: 'Upload files locally present or from public remote URL via API'
position: 1600
category: 'Developer Resources'
menuTitle: 'Upload via API'
---
Sample code to upload files via API is listed below.
Assumes `http://localhost:8080/` as the base URL for the API calls.
# Upload local file
```
let axios = require("axios").default;
let FormData = require('form-data');
let fs = require('fs');
// Configurations
//
const project_id = '<Project Identifier>';
const table_id = '<Table Identifier>';
const xc_token = '<Auth Token>';
const file_path = '<Local File Path>';
// Insert Image
// @param image_path : local file path
// @return : JSON object to be used in insert record API for attachment field
//
async function insertImage (path) {
const formData = new FormData();
formData.append("file", fs.createReadStream(path));
const data = await axios({
url: 'http://localhost:8080/api/v1/db/storage/upload',
data: formData,
headers:{
'Content-Type':`multipart/form-data;`,
'xc-auth': xc_token
},
method: 'post',
// Optional : storage file path
params: {"path": "somePath"}
});
return data;
}
// Insert record with attachment
// Assumes a table with two columns :
// 'Title' of type SingleLineText and
// 'Attachment' of type Attachment
//
async function uploadFileExample() {
let response = await insertImage(file_path);
let row = {
"Title": "2",
"Attachment": response.data
};
await axios({
method: 'POST',
url: `http://localhost:8080/api/v1/db/data/noco/${project_id}/${table_id}`,
data: row,
headers: {
'xc-auth': xc_token
}
});
}
(async () => {
await uploadFileExample();
})();
```
# Upload via URL
```
let axios = require("axios").default;
let FormData = require('form-data');
let fs = require('fs');
// Configurations
//
const project_id = '<Project Identifier>';
const table_id = '<Table Identifier>';
const xc_token = '<Auth Token>';
// URL array : URLs of files to be uploaded
const URLs = [{ url: '<URL1>' }, { url: '<URL2>' }];
// Insert Image
// @param URLs : [] containing public URL for files to be uploaded
// @return : JSON object to be used in insert record API for attachment field
//
async function insertImageByURL (URL_array) {
const data = await axios({
url: 'http://localhost:8080/api/v1/db/storage/upload-by-url',
data: URL_array,
headers: {
'xc-auth': xc_token
},
method: 'post',
// Optional : storage file path
params: {"path": "somePath"}
});
return data;
}
// Insert record with attachment
// Assumes a table with two columns :
// 'Title' of type SingleLineText and
// 'Attachment' of type Attachment
//
async function uploadByUrlExample() {
let response = await insertImageByURL(URLs);
// Update two columns : Title and Attachment
let row = {
"Title": "3",
"Attachment": response.data
};
await axios({
method: 'POST',
url: `http://localhost:8080/api/v1/db/data/noco/${project_id}/${table_id}`,
data: row,
headers: {
'xc-auth': xc_auth
}
});
}
(async () => {
await uploadByUrlExample();
})();
```

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

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

30
packages/nocodb/package-lock.json generated

@ -83,7 +83,7 @@
"nc-lib-gui": "0.109.4", "nc-lib-gui": "0.109.4",
"nc-plugin": "^0.1.3", "nc-plugin": "^0.1.3",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nocodb-sdk": "0.109.4", "nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10", "nodemailer": "^6.4.10",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"object-sizeof": "^2.6.1", "object-sizeof": "^2.6.1",
@ -192,7 +192,6 @@
}, },
"../nocodb-sdk": { "../nocodb-sdk": {
"version": "0.109.4", "version": "0.109.4",
"extraneous": true,
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
@ -13208,13 +13207,8 @@
} }
}, },
"node_modules/nocodb-sdk": { "node_modules/nocodb-sdk": {
"version": "0.109.4", "resolved": "../nocodb-sdk",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.109.4.tgz", "link": true
"integrity": "sha512-kncrV4vFdAOMSoMoCcRrWnU74p5jWn/PIMjs2poHfvxcfRscm6tZ8dEvDpY+do8hOMfZC+WO0G0/WT2wntIwEg==",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
}
}, },
"node_modules/node-abort-controller": { "node_modules/node-abort-controller": {
"version": "3.1.1", "version": "3.1.1",
@ -28517,12 +28511,22 @@
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==" "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="
}, },
"nocodb-sdk": { "nocodb-sdk": {
"version": "0.109.4", "version": "file:../nocodb-sdk",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.109.4.tgz",
"integrity": "sha512-kncrV4vFdAOMSoMoCcRrWnU74p5jWn/PIMjs2poHfvxcfRscm6tZ8dEvDpY+do8hOMfZC+WO0G0/WT2wntIwEg==",
"requires": { "requires": {
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"axios": "^0.21.1", "axios": "^0.21.1",
"jsep": "^1.3.6" "cspell": "^4.1.0",
"eslint": "^7.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^4.0.0",
"jsep": "^1.3.6",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.1",
"typescript": "^4.0.2"
} }
}, },
"node-abort-controller": { "node-abort-controller": {

10
packages/nocodb/package.json

@ -17,14 +17,12 @@
}, },
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"scripts": { "scripts": {
"build": "nest build", "build": "npm run docker:build",
"build:obfuscate": "EE=true webpack --config webpack.config.js", "build:obfuscate": "EE=true webpack --config webpack.config.js",
"obfuscate:build:publish": "npm run build:obfuscate && npm publish .", "obfuscate:build:publish": "npm run build:obfuscate && npm publish .",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start", "start": "npm run watch:run",
"start:dev": "nest start --watch", "start:prod": "node docker/main",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
@ -118,7 +116,7 @@
"nc-lib-gui": "0.109.4", "nc-lib-gui": "0.109.4",
"nc-plugin": "^0.1.3", "nc-plugin": "^0.1.3",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nocodb-sdk": "0.109.4", "nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10", "nodemailer": "^6.4.10",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"object-sizeof": "^2.6.1", "object-sizeof": "^2.6.1",

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

@ -149,13 +149,18 @@ class BaseModelSqlv2 {
id?: any, id?: any,
validateFormula = false, validateFormula = false,
query: any = {}, query: any = {},
{
ignoreView = false,
}: {
ignoreView?: boolean;
} = {},
): Promise<any> { ): Promise<any> {
const qb = this.dbDriver(this.tnPath); const qb = this.dbDriver(this.tnPath);
const { ast, dependencyFields } = await getAst({ const { ast, dependencyFields } = await getAst({
query, query,
model: this.model, model: this.model,
view: this.viewId && (await View.get(this.viewId)), view: ignoreView ? null : this.viewId && (await View.get(this.viewId)),
}); });
await this.selectObject({ await this.selectObject({
@ -1385,7 +1390,6 @@ class BaseModelSqlv2 {
if (colOptions?.type === 'hm') { if (colOptions?.type === 'hm') {
const listLoader = new DataLoader(async (ids: string[]) => { const listLoader = new DataLoader(async (ids: string[]) => {
try {
if (ids.length > 1) { if (ids.length > 1) {
const data = await this.multipleHmList( const data = await this.multipleHmList(
{ {
@ -1406,10 +1410,6 @@ class BaseModelSqlv2 {
), ),
]; ];
} }
} catch (e) {
console.log(e);
return [];
}
}); });
const self: BaseModelSqlv2 = this; const self: BaseModelSqlv2 = this;
@ -1429,7 +1429,6 @@ class BaseModelSqlv2 {
// }); // });
} else if (colOptions.type === 'mm') { } else if (colOptions.type === 'mm') {
const listLoader = new DataLoader(async (ids: string[]) => { const listLoader = new DataLoader(async (ids: string[]) => {
try {
if (ids?.length > 1) { if (ids?.length > 1) {
const data = await this.multipleMmList( const data = await this.multipleMmList(
{ {
@ -1451,10 +1450,6 @@ class BaseModelSqlv2 {
), ),
]; ];
} }
} catch (e) {
console.log(e);
return [];
}
}); });
const self: BaseModelSqlv2 = this; const self: BaseModelSqlv2 = this;
@ -1476,7 +1471,6 @@ class BaseModelSqlv2 {
colId: colOptions.fk_child_column_id, colId: colOptions.fk_child_column_id,
}); });
const readLoader = new DataLoader(async (ids: string[]) => { const readLoader = new DataLoader(async (ids: string[]) => {
try {
const data = await ( const data = await (
await Model.getBaseModelSQL({ await Model.getBaseModelSQL({
id: pCol.fk_model_id, id: pCol.fk_model_id,
@ -1492,10 +1486,6 @@ class BaseModelSqlv2 {
); );
const gs = groupBy(data, pCol.title); const gs = groupBy(data, pCol.title);
return ids.map(async (id: string) => gs?.[id]?.[0]); return ids.map(async (id: string) => gs?.[id]?.[0]);
} catch (e) {
console.log(e);
return [];
}
}); });
// defining HasMany count method within GQL Type class // defining HasMany count method within GQL Type class
@ -1836,7 +1826,12 @@ class BaseModelSqlv2 {
// handle if autogenerated primary key is used // handle if autogenerated primary key is used
if (ag) { if (ag) {
if (!response) await this.execAndParse(query); if (!response) await this.execAndParse(query);
response = await this.readByPk(data[ag.title]); response = await this.readByPk(
data[ag.title],
false,
{},
{ ignoreView: true },
);
} else if ( } else if (
!response || !response ||
(typeof response?.[0] !== 'object' && response?.[0] !== null) (typeof response?.[0] !== 'object' && response?.[0] !== null)
@ -1864,7 +1859,7 @@ class BaseModelSqlv2 {
})) as any })) as any
)[0].id; )[0].id;
} }
response = await this.readByPk(id); response = await this.readByPk(id, false, {}, { ignoreView: true });
} else { } else {
response = data; response = data;
} }
@ -1873,6 +1868,9 @@ class BaseModelSqlv2 {
Array.isArray(response) Array.isArray(response)
? response?.[0]?.[ai.title] ? response?.[0]?.[ai.title]
: response?.[ai.title], : response?.[ai.title],
false,
{},
{ ignoreView: true },
); );
} }
@ -1889,7 +1887,7 @@ class BaseModelSqlv2 {
let trx: Transaction = _trx; let trx: Transaction = _trx;
try { try {
// retrieve data for handling params in hook // retrieve data for handling params in hook
const data = await this.readByPk(id); const data = await this.readByPk(id, false, {}, { ignoreView: true });
await this.beforeDelete(id, trx, cookie); await this.beforeDelete(id, trx, cookie);
const execQueries: ((trx: Transaction) => Promise<any>)[] = []; const execQueries: ((trx: Transaction) => Promise<any>)[] = [];
@ -2018,7 +2016,7 @@ class BaseModelSqlv2 {
await this.beforeUpdate(data, trx, cookie); await this.beforeUpdate(data, trx, cookie);
const prevData = await this.readByPk(id); const prevData = await this.readByPk(id, false, {}, { ignoreView: true });
const query = this.dbDriver(this.tnPath) const query = this.dbDriver(this.tnPath)
.update(updateObj) .update(updateObj)
@ -2026,7 +2024,7 @@ class BaseModelSqlv2 {
await this.execAndParse(query); await this.execAndParse(query);
const newData = await this.readByPk(id); const newData = await this.readByPk(id, false, {}, { ignoreView: true });
await this.afterUpdate(prevData, newData, trx, cookie, updateObj); await this.afterUpdate(prevData, newData, trx, cookie, updateObj);
return newData; return newData;
} catch (e) { } catch (e) {
@ -2210,7 +2208,7 @@ class BaseModelSqlv2 {
})) as any })) as any
).rows[0].id; ).rows[0].id;
} }
response = await this.readByPk(id); response = await this.readByPk(id, false, {}, { ignoreView: true });
} else { } else {
response = data; response = data;
} }
@ -2478,7 +2476,9 @@ class BaseModelSqlv2 {
if (!raw) { if (!raw) {
for (const pkValues of updatePkValues) { for (const pkValues of updatePkValues) {
newData.push(await this.readByPk(pkValues)); newData.push(
await this.readByPk(pkValues, false, {}, { ignoreView: true }),
);
} }
} }
@ -2570,7 +2570,9 @@ class BaseModelSqlv2 {
// pk not specified - bypass // pk not specified - bypass
continue; continue;
} }
deleted.push(await this.readByPk(pkValues)); deleted.push(
await this.readByPk(pkValues, false, {}, { ignoreView: true }),
);
res.push(d); res.push(d);
} }
@ -3227,7 +3229,12 @@ class BaseModelSqlv2 {
break; break;
} }
const response = await this.readByPk(rowId); const response = await this.readByPk(
rowId,
false,
{},
{ ignoreView: true },
);
await this.afterInsert(response, this.dbDriver, cookie); await this.afterInsert(response, this.dbDriver, cookie);
await this.afterAddChild(rowId, childId, cookie); await this.afterAddChild(rowId, childId, cookie);
} }
@ -3276,7 +3283,12 @@ class BaseModelSqlv2 {
const childTn = this.getTnPath(childTable); const childTn = this.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable); const parentTn = this.getTnPath(parentTable);
const prevData = await this.readByPk(rowId); const prevData = await this.readByPk(
rowId,
false,
{},
{ ignoreView: true },
);
switch (colOptions.type) { switch (colOptions.type) {
case RelationTypes.MANY_TO_MANY: case RelationTypes.MANY_TO_MANY:
@ -3329,7 +3341,7 @@ class BaseModelSqlv2 {
break; break;
} }
const newData = await this.readByPk(rowId); const newData = await this.readByPk(rowId, false, {}, { ignoreView: true });
await this.afterUpdate(prevData, newData, this.dbDriver, cookie); await this.afterUpdate(prevData, newData, this.dbDriver, cookie);
await this.afterRemoveChild(rowId, childId, cookie); await this.afterRemoveChild(rowId, childId, cookie);
} }

2
packages/nocodb/src/db/sql-mgr/code/models/xc/ModelXcMetaMssql.ts

@ -590,7 +590,7 @@ class ModelXcMetaMssql extends BaseModelXcMeta {
case 'set': case 'set':
return 'MultiSelect'; return 'MultiSelect';
case 'json': case 'json':
return 'LongText'; return 'JSON';
case 'blob': case 'blob':
return 'LongText'; return 'LongText';
case 'geometry': case 'geometry':

2
packages/nocodb/src/db/sql-mgr/code/models/xc/ModelXcMetaMysql.ts

@ -322,7 +322,7 @@ class ModelXcMetaMysql extends BaseModelXcMeta {
case 'set': case 'set':
return 'MultiSelect'; return 'MultiSelect';
case 'json': case 'json':
return 'LongText'; return 'JSON';
case 'blob': case 'blob':
return 'LongText'; return 'LongText';
case 'geometry': case 'geometry':

2
packages/nocodb/src/db/sql-mgr/code/models/xc/ModelXcMetaOracle.ts

@ -370,7 +370,7 @@ class ModelXcMetaOracle extends BaseModelXcMeta {
case 'set': case 'set':
return 'MultiSelect'; return 'MultiSelect';
case 'json': case 'json':
return 'LongText'; return 'JSON';
} }
} }

Loading…
Cancel
Save