Browse Source

Merge pull request #2217 from nocodb/develop

0.91.2 Pre Release
pull/2222/head 0.91.6
աɨռɢӄաօռɢ 3 years ago committed by GitHub
parent
commit
14978f9cbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 31
      .github/workflows/release-nocodb.yml
  2. 32
      packages/nc-gui/components/project/spreadsheet/components/ColumnFilter.vue
  3. 66
      packages/nc-gui/components/project/spreadsheet/components/FieldListAutoCompleteDropdown.vue
  4. 100
      packages/nc-gui/components/project/spreadsheet/components/FieldsMenu.vue
  5. 2
      packages/nc-gui/components/project/spreadsheet/components/MoreActions.vue
  6. 31
      packages/nc-gui/components/project/spreadsheet/components/SortListMenu.vue
  7. 6
      packages/nc-gui/components/project/spreadsheet/components/editableCell/DateTimePickerCell.vue
  8. 33
      packages/nc-gui/components/project/spreadsheet/components/editableCell/EditableAttachmentCell.vue
  9. 12
      packages/nc-gui/components/project/spreadsheet/components/editableCell/TimePickerCell.vue
  10. 17436
      packages/nc-gui/package-lock.json
  11. 2
      packages/nc-gui/package.json
  12. 3
      packages/nc-gui/store/project.js
  13. 12099
      packages/nc-plugin/package-lock.json
  14. 2
      packages/nc-plugin/package.json
  15. 4
      packages/nc-plugin/src/index.ts
  16. 34
      packages/nc-plugin/src/lib/IStorageAdapterV2.ts
  17. 3
      packages/noco-docs/content/en/developer-resources/rest-apis.md
  18. 10531
      packages/nocodb-sdk/package-lock.json
  19. 22
      packages/nocodb-sdk/src/lib/Api.ts
  20. 24974
      packages/nocodb/package-lock.json
  21. 4
      packages/nocodb/package.json
  22. 19
      packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSqlv2.ts
  23. 27
      packages/nocodb/src/lib/dataMapper/lib/sql/conditionV2.ts
  24. 6
      packages/nocodb/src/lib/noco/common/XcMigrationSourcev2.ts
  25. 5
      packages/nocodb/src/lib/noco/meta/NcMetaIOImpl.ts
  26. 44
      packages/nocodb/src/lib/noco/meta/api/attachmentApis.ts
  27. 6
      packages/nocodb/src/lib/noco/meta/api/sync/helpers/job.ts
  28. 12
      packages/nocodb/src/lib/noco/meta/helpers/NcPluginMgrv2.ts
  29. 42
      packages/nocodb/src/lib/noco/migrationsv2/nc_016_alter_hooklog_payload_types.ts
  30. 46
      packages/nocodb/src/lib/noco/plugins/adapters/storage/Local.ts
  31. 4
      packages/nocodb/src/lib/utils/projectAcl.ts
  32. 37
      packages/nocodb/src/plugins/backblaze/Backblaze.ts
  33. 4
      packages/nocodb/src/plugins/backblaze/BackblazePlugin.ts
  34. 36
      packages/nocodb/src/plugins/gcs/Gcs.ts
  35. 4
      packages/nocodb/src/plugins/gcs/GcsPlugin.ts
  36. 9
      packages/nocodb/src/plugins/gcs/index.ts
  37. 37
      packages/nocodb/src/plugins/linode/LinodeObjectStorage.ts
  38. 4
      packages/nocodb/src/plugins/linode/LinodeObjectStoragePlugin.ts
  39. 74
      packages/nocodb/src/plugins/mino/Minio.ts
  40. 4
      packages/nocodb/src/plugins/mino/MinioPlugin.ts
  41. 37
      packages/nocodb/src/plugins/ovhCloud/OvhCloud.ts
  42. 4
      packages/nocodb/src/plugins/ovhCloud/OvhCloudPlugin.ts
  43. 36
      packages/nocodb/src/plugins/s3/S3.ts
  44. 4
      packages/nocodb/src/plugins/s3/S3Plugin.ts
  45. 37
      packages/nocodb/src/plugins/scaleway/ScalewayObjectStorage.ts
  46. 4
      packages/nocodb/src/plugins/scaleway/ScalewayObjectStoragePlugin.ts
  47. 37
      packages/nocodb/src/plugins/spaces/Spaces.ts
  48. 4
      packages/nocodb/src/plugins/spaces/SpacesPlugin.ts
  49. 4
      packages/nocodb/src/plugins/upcloud/UpCloudPlugin.ts
  50. 37
      packages/nocodb/src/plugins/upcloud/UpoCloud.ts
  51. 37
      packages/nocodb/src/plugins/vultr/Vultr.ts
  52. 4
      packages/nocodb/src/plugins/vultr/VultrPlugin.ts
  53. 13
      scripts/cypress/support/page_objects/mainPage.js
  54. 58
      scripts/sdk/swagger.json

31
.github/workflows/release-nocodb.yml

@ -61,23 +61,9 @@ jobs:
tag: ${{ needs.process-input.outputs.target_tag }} tag: ${{ needs.process-input.outputs.target_tag }}
targetEnv: ${{ github.event.inputs.targetEnv || 'PROD' }} targetEnv: ${{ github.event.inputs.targetEnv || 'PROD' }}
# Close all issues with target tags 'Fixed' & 'Resolved'
close-fixed-issues:
needs: [pr-to-master, process-input]
uses: ./.github/workflows/release-close-issue.yml
with:
issue_label: 'Status: Fixed'
version: ${{ needs.process-input.outputs.target_tag }}
close-resolved-issues:
needs: [close-fixed-issues, process-input]
uses: ./.github/workflows/release-close-issue.yml
with:
issue_label: 'Status: Resolved'
version: ${{ needs.process-input.outputs.target_tag }}
# Build, install, publish frontend and backend to npm # Build, install, publish frontend and backend to npm
release-npm: release-npm:
needs: [close-resolved-issues, process-input] needs: [pr-to-master, process-input]
uses: ./.github/workflows/release-npm.yml uses: ./.github/workflows/release-npm.yml
with: with:
tag: ${{ needs.process-input.outputs.target_tag }} tag: ${{ needs.process-input.outputs.target_tag }}
@ -105,6 +91,21 @@ jobs:
DOCKERHUB_USERNAME: "${{ secrets.DOCKERHUB_USERNAME }}" DOCKERHUB_USERNAME: "${{ secrets.DOCKERHUB_USERNAME }}"
DOCKERHUB_TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}" DOCKERHUB_TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}"
# Close all issues with target tags 'Fixed' & 'Resolved'
close-fixed-issues:
needs: [release-docker, process-input]
uses: ./.github/workflows/release-close-issue.yml
with:
issue_label: 'Status: Fixed'
version: ${{ needs.process-input.outputs.target_tag }}
close-resolved-issues:
needs: [close-fixed-issues, process-input]
uses: ./.github/workflows/release-close-issue.yml
with:
issue_label: 'Status: Resolved'
version: ${{ needs.process-input.outputs.target_tag }}
# Publish Docs # Publish Docs
publish-docs: publish-docs:
needs: release-docker needs: release-docker

32
packages/nc-gui/components/project/spreadsheet/components/ColumnFilter.vue

@ -92,36 +92,16 @@
</template> </template>
</v-select> </v-select>
<v-select <field-list-auto-complete-dropdown
:key="i + '_6'" :key="i + '_6'"
v-model="filter.fk_column_id" v-model="filter.fk_column_id"
class="caption nc-filter-field-select" class="caption nc-filter-field-select"
:items="columns" :columns="columns"
:placeholder="$t('objects.field')"
solo
flat
dense
:disabled="filter.readOnly" :disabled="filter.readOnly"
hide-details
item-value="id"
item-text="title"
@click.stop @click.stop
@change="saveOrUpdate(filter, i)" @change="saveOrUpdate(filter, i)"
> />
<template #item="{ item }">
<span :class="`caption font-weight-regular nc-filter-fld-${item.title}`">
<v-icon small class="mr-1">
{{ item.icon }}
</v-icon>
{{ item.title }}
</span>
</template>
<template #selection="{item}">
<v-icon small class="mr-1">
{{ item.icon }}
</v-icon> {{ item.title }}
</template>
</v-select>
<v-select <v-select
:key="'k' + i" :key="'k' + i"
v-model="filter.comparison_op" v-model="filter.comparison_op"
@ -189,9 +169,13 @@
<script> <script>
import { getUIDTIcon, UITypes } from '~/components/project/spreadsheet/helpers/uiTypes' import { getUIDTIcon, UITypes } from '~/components/project/spreadsheet/helpers/uiTypes'
import FieldListAutoCompleteDropdown from '~/components/project/spreadsheet/components/FieldListAutoCompleteDropdown'
export default { export default {
name: 'ColumnFilter', name: 'ColumnFilter',
components: {
FieldListAutoCompleteDropdown
},
props: { props: {
fieldList: [Array], fieldList: [Array],
meta: Object, meta: Object,

66
packages/nc-gui/components/project/spreadsheet/components/FieldListAutoCompleteDropdown.vue

@ -0,0 +1,66 @@
<template>
<v-autocomplete
ref="field"
v-model="localValue"
class="caption"
:items="columns"
item-value="id"
item-text="title"
:label="$t('objects.field')"
solo
flat
dense
hide-details
@click.stop
@change="$emit('change')"
>
<template #selection="{item}">
<v-icon small class="mr-1">
{{ item.icon }}
</v-icon> {{ item.title }}
</template>
<template #item="{ item }">
<span
:class="`caption font-weight-regular nc-fld-${item.title}`"
>
<v-icon color="grey" small class="mr-1">
{{ item.icon }}
</v-icon>
{{ item.title }}
</span>
</template>
</v-autocomplete>
</template>
<script>
export default {
name: 'FieldListAutoCompleteDropdown',
props: {
columns: Array,
value: String
},
computed: {
localValue: {
set(v) {
this.$emit('input', v)
},
get() {
return this.value
}
}
},
mounted() {
const autocompleteInput = this.$refs.field.$refs.input
autocompleteInput.addEventListener('focus', this.onFocus, true)
},
methods: {
onFocus(e) {
this.$refs.field.isMenuActive = true // open item list
}
}
}
</script>
<style scoped>
</style>

100
packages/nc-gui/components/project/spreadsheet/components/FieldsMenu.vue

@ -96,53 +96,57 @@
</template>--> </template>-->
</v-text-field> </v-text-field>
</v-list-item> </v-list-item>
<draggable <div
v-model="fields" class="nc-fields-list py-1"
@start="drag = true"
@end="drag = false"
@change="onMove($event)"
> >
<template v-for="(field, i) in fields"> <draggable
<v-list-item v-model="fields"
v-show=" @start="drag = true"
(!fieldFilter || @end="drag = false"
(field.title || '') @change="onMove($event)"
.toLowerCase() >
.includes(fieldFilter.toLowerCase())) && <template v-for="(field, i) in fields">
!( <v-list-item
!showSystemFieldsLoc && v-show="
systemColumnsIds.includes(field.fk_column_id) (!fieldFilter ||
) (field.title || '')
" .toLowerCase()
:key="field.id" .includes(fieldFilter.toLowerCase())) &&
dense !(
> !showSystemFieldsLoc &&
<v-checkbox systemColumnsIds.includes(field.fk_column_id)
v-model="field.show" )
class="mt-0 pt-0" "
:key="field.id"
dense dense
hide-details
@click.stop
@change="saveOrUpdate(field, i)"
>
<template #label>
<v-icon small class="mr-1">
{{ field.icon }}
</v-icon>
<span class="caption">{{ field.title }}</span>
</template>
</v-checkbox>
<v-spacer />
<v-icon
small
color="grey"
:class="`align-self-center drag-icon nc-child-draggable-icon-${field}`"
> >
mdi-drag <v-checkbox
</v-icon> v-model="field.show"
</v-list-item> class="mt-0 pt-0"
</template> dense
</draggable> hide-details
@click.stop
@change="saveOrUpdate(field, i)"
>
<template #label>
<v-icon small class="mr-1">
{{ field.icon }}
</v-icon>
<span class="caption">{{ field.title }}</span>
</template>
</v-checkbox>
<v-spacer />
<v-icon
small
color="grey"
:class="`align-self-center drag-icon nc-child-draggable-icon-${field}`"
>
mdi-drag
</v-icon>
</v-list-item>
</template>
</draggable>
</div>
<v-divider class="my-2" /> <v-divider class="my-2" />
<v-list-item v-if="!isPublic" dense> <v-list-item v-if="!isPublic" dense>
@ -459,7 +463,7 @@ export default {
'order', 'order',
(this.fields[event.moved.newIndex - 1].order + (this.fields[event.moved.newIndex - 1].order +
this.fields[event.moved.newIndex + 1].order) / this.fields[event.moved.newIndex + 1].order) /
2 2
) )
} }
this.saveOrUpdate( this.saveOrUpdate(
@ -504,4 +508,10 @@ export default {
.drag-icon { .drag-icon {
cursor: all-scroll; /*cursor: grab;*/ cursor: all-scroll; /*cursor: grab;*/
} }
.nc-fields-list {
height: auto;
max-height: 500px;
overflow-y: auto;
}
</style> </style>

2
packages/nc-gui/components/project/spreadsheet/components/MoreActions.vue

@ -311,6 +311,8 @@ export default {
} }
} else if (v.uidt === UITypes.Number) { } else if (v.uidt === UITypes.Number) {
if (input == '') { input = null } if (input == '') { input = null }
} else if (v.uidt === UITypes.SingleSelect || v.uidt === UITypes.MultiSelect) {
if (input == '') { input = null }
} }
res[col.destCn] = input res[col.destCn] = input
} }

31
packages/nc-gui/components/project/spreadsheet/components/SortListMenu.vue

@ -41,37 +41,14 @@
mdi-close-box mdi-close-box
</v-icon> </v-icon>
<v-select <field-list-auto-complete-dropdown
:key="i + 'sel1'" :key="i + 'sel1'"
v-model="sort.fk_column_id" v-model="sort.fk_column_id"
class="caption nc-sort-field-select" class="caption nc-sort-field-select"
:items="columns" :columns="columns"
item-value="id"
item-text="title"
:label="$t('objects.field')"
solo
flat
dense
hide-details
@click.stop @click.stop
@change="saveOrUpdate(sort, i)" @change="saveOrUpdate(sort, i)"
> />
<template #selection="{item}">
<v-icon small class="mr-1">
{{ item.icon }}
</v-icon> {{ item.title }}
</template>
<template #item="{ item }">
<span
:class="`caption font-weight-regular nc-sort-fld-${item.title}`"
>
<v-icon color="grey darken-4" small class="mr-1">
{{ item.icon }}
</v-icon>
{{ item.title }}
</span>
</template>
</v-select>
<v-select <v-select
:key="i + 'sel2'" :key="i + 'sel2'"
v-model="sort.direction" v-model="sort.direction"
@ -108,9 +85,11 @@
<script> <script>
import { RelationTypes, UITypes } from 'nocodb-sdk' import { RelationTypes, UITypes } from 'nocodb-sdk'
import { getUIDTIcon } from '~/components/project/spreadsheet/helpers/uiTypes' import { getUIDTIcon } from '~/components/project/spreadsheet/helpers/uiTypes'
import FieldListAutoCompleteDropdown from '~/components/project/spreadsheet/components/FieldListAutoCompleteDropdown'
export default { export default {
name: 'SortListMenu', name: 'SortListMenu',
components: { FieldListAutoCompleteDropdown },
props: { props: {
fieldList: Array, fieldList: Array,
value: [Array, Object], value: [Array, Object],

6
packages/nc-gui/components/project/spreadsheet/components/editableCell/DateTimePickerCell.vue

@ -32,17 +32,19 @@ export default {
value: [String, Date, Number], ignoreFocus: Boolean value: [String, Date, Number], ignoreFocus: Boolean
}, },
computed: { computed: {
isMysql() {
return ['mysql', 'mysql2'].indexOf(this.$store.getters['project/GtrClientType'])
},
localState: { localState: {
get() { get() {
if (!this.value) { if (!this.value) {
return this.value return this.value
} }
return (/^\d+$/.test(this.value) ? dayjs(+this.value) : dayjs(this.value)) return (/^\d+$/.test(this.value) ? dayjs(+this.value) : dayjs(this.value))
.format('YYYY-MM-DD HH:mm') .format('YYYY-MM-DD HH:mm')
}, },
set(value) { set(value) {
if (this.$parent.sqlUi.name === 'MysqlUi') { if (this.isMysql) {
this.$emit('input', value && dayjs(value).format('YYYY-MM-DD HH:mm:ss')) this.$emit('input', value && dayjs(value).format('YYYY-MM-DD HH:mm:ss'))
} else { } else {
this.$emit('input', value && dayjs(value).format('YYYY-MM-DD HH:mm:ssZ')) this.$emit('input', value && dayjs(value).format('YYYY-MM-DD HH:mm:ssZ'))

33
packages/nc-gui/components/project/spreadsheet/components/editableCell/EditableAttachmentCell.vue

@ -62,7 +62,11 @@
</v-tooltip> </v-tooltip>
</div> </div>
</div> </div>
<div v-if="isForm || active && !isPublicGrid && !isLocked" class="add d-flex align-center justify-center px-1 nc-attachment-add" @click="addFile"> <div
v-if="isForm || active && !isPublicGrid && !isLocked"
class="add d-flex align-center justify-center px-1 nc-attachment-add"
@click="addFile"
>
<v-icon v-if="uploading" small color="primary" class="nc-attachment-add-spinner"> <v-icon v-if="uploading" small color="primary" class="nc-attachment-add-spinner">
mdi-loading mdi-spin mdi-loading mdi-spin
</v-icon> </v-icon>
@ -76,9 +80,15 @@
> >
<v-icon x-small color=""> <v-icon x-small color="">
mdi-plus mdi-plus
</v-icon> Attachment </v-icon>
Attachment
</v-btn> </v-btn>
<v-icon v-else-if="_isUIAllowed('tableAttachment')" v-show="active" small color="primary nc-attachment-add-icon"> <v-icon
v-else-if="_isUIAllowed('tableAttachment')"
v-show="active"
small
color="primary nc-attachment-add-icon"
>
mdi-plus mdi-plus
</v-icon> </v-icon>
</div> </div>
@ -110,6 +120,8 @@
</v-icon> </v-icon>
<span class="caption">Attach File</span> <span class="caption">Attach File</span>
</v-btn> </v-btn>
<!-- <v-text-field v-model="urlString" @keypress.enter="uploadByUrl" />-->
</div> </div>
<div class="d-flex flex-wrap h-100"> <div class="d-flex flex-wrap h-100">
@ -261,7 +273,8 @@ export default {
showImage: false, showImage: false,
selectedImage: null, selectedImage: null,
dragOver: false, dragOver: false,
localFilesState: [] localFilesState: [],
urlString: ''
}), }),
watch: { watch: {
value(val, prev) { value(val, prev) {
@ -286,6 +299,18 @@ export default {
mounted() { mounted() {
}, },
methods: { methods: {
async uploadByUrl() {
const data = await this.$api.storage.uploadByUrl(
{
path: ['noco', this.projectName, this.meta.title, this.column.title].join('/')
},
[{
url: this.urlString
}]
)
this.localState.push(...data)
},
openUrl(url, target) { openUrl(url, target) {
window.open(url, target) window.open(url, target)
}, },

12
packages/nc-gui/components/project/spreadsheet/components/editableCell/TimePickerCell.vue

@ -15,6 +15,7 @@
<script> <script>
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { MysqlUi } from 'nocodb-sdk'
export default { export default {
name: 'TimePickerCell', name: 'TimePickerCell',
@ -22,6 +23,9 @@ export default {
value: [String, Date] value: [String, Date]
}, },
computed: { computed: {
isMysql() {
return ['mysql', 'mysql2'].indexOf(this.$store.getters['project/GtrClientType'])
},
localState: { localState: {
get() { get() {
if (!this.value) { if (!this.value) {
@ -41,7 +45,13 @@ export default {
}, },
set(val) { set(val) {
const dateTime = dayjs(`1999-01-01 ${val}:00`) const dateTime = dayjs(`1999-01-01 ${val}:00`)
if (dateTime.isValid()) { this.$emit('input', dateTime.format('YYYY-MM-DD HH:mm:ssZ')) } if (dateTime.isValid()) {
if (this.isMysql) {
this.$emit('input', dateTime.format('YYYY-MM-DD HH:mm:ss'))
} else {
this.$emit('input', dateTime.format('YYYY-MM-DD HH:mm:ssZ'))
}
}
} }
}, },

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

File diff suppressed because it is too large Load Diff

2
packages/nc-gui/package.json

@ -31,7 +31,7 @@
"monaco-editor": "^0.19.3", "monaco-editor": "^0.19.3",
"monaco-themes": "^0.2.5", "monaco-themes": "^0.2.5",
"nano-assign": "^1.0.1", "nano-assign": "^1.0.1",
"nocodb-sdk": "0.91.1", "nocodb-sdk": "file:../nocodb-sdk",
"nuxt": "^2.14.0", "nuxt": "^2.14.0",
"odometer": "^0.4.8", "odometer": "^0.4.8",
"papaparse": "^5.3.1", "papaparse": "^5.3.1",

3
packages/nc-gui/store/project.js

@ -200,6 +200,9 @@ export const getters = {
GtrProjectPrefix(state) { GtrProjectPrefix(state) {
return state.project && state.project.prefix return state.project && state.project.prefix
}, },
GtrClientType(state) {
return state.project && state.project.bases && state.project.bases[0]&& state.project.bases[0].type
},
GtrApiEnvironment(state) { GtrApiEnvironment(state) {
const projJson = state.unserializedList && state.unserializedList[0] ? state.unserializedList[0].projectJson : null const projJson = state.unserializedList && state.unserializedList[0] ? state.unserializedList[0].projectJson : null

12099
packages/nc-plugin/package-lock.json generated

File diff suppressed because it is too large Load Diff

2
packages/nc-plugin/package.json

@ -1,6 +1,6 @@
{ {
"name": "nc-plugin", "name": "nc-plugin",
"version": "0.1.1", "version": "0.1.3",
"description": "Xgene plugin template", "description": "Xgene plugin template",
"main": "build/main/index.js", "main": "build/main/index.js",
"typings": "build/main/index.d.ts", "typings": "build/main/index.d.ts",

4
packages/nc-plugin/src/index.ts

@ -5,6 +5,7 @@ import XcPluginMigration from './lib/XcPluginMigration';
import XcStoragePlugin from './lib/XcStoragePlugin'; import XcStoragePlugin from './lib/XcStoragePlugin';
import XcEmailPlugin from './lib/XcEmailPlugin'; import XcEmailPlugin from './lib/XcEmailPlugin';
import IStorageAdapter, {XcFile} from './lib/IStorageAdapter'; import IStorageAdapter, {XcFile} from './lib/IStorageAdapter';
import IStorageAdapterV2 from './lib/IStorageAdapterV2';
import IEmailAdapter, {XcEmail} from './lib/IEmailAdapter'; import IEmailAdapter, {XcEmail} from './lib/IEmailAdapter';
import IWebhookNotificationAdapter from './lib/IWebhookNotificationAdapter'; import IWebhookNotificationAdapter from './lib/IWebhookNotificationAdapter';
import XcWebhookNotificationPlugin from './lib/XcWebhookNotificationPlugin'; import XcWebhookNotificationPlugin from './lib/XcWebhookNotificationPlugin';
@ -21,5 +22,6 @@ export {
XcFile, XcFile,
XcEmail, XcEmail,
IWebhookNotificationAdapter, IWebhookNotificationAdapter,
XcWebhookNotificationPlugin XcWebhookNotificationPlugin,
IStorageAdapterV2
} }

34
packages/nc-plugin/src/lib/IStorageAdapterV2.ts

@ -0,0 +1,34 @@
import IStorageAdapter from "./IStorageAdapter";
export default interface IStorageAdapterV2 extends IStorageAdapter {
fileCreateByUrl(destPath: string, url: string, fileMeta?: FileMeta): Promise<any>
}
interface FileMeta {
fileName?: string;
mimetype?: string;
size?: number | string;
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

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

@ -157,6 +157,7 @@ Currently, the default value for {orgs} is <b>noco</b>. Users will be able to ch
| Meta | Post | project | sharedBaseCreate | /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 | Patch | project | sharedBaseUpdate | /api/v1/db/meta/projects/{projectId}/shared |
| Meta | Post | storage | upload | /api/v1/db/storage/upload | | 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 | | Meta | Get | utils | commentList | /api/v1/db/meta/audits/comments |
| Meta | Post | utils | commentRow | /api/v1/db/meta/audits/comments | | Meta | Post | utils | commentRow | /api/v1/db/meta/audits/comments |
| Meta | Get | utils | commentCount | /api/v1/db/meta/audits/comments/count | | Meta | Get | utils | commentCount | /api/v1/db/meta/audits/comments/count |
@ -210,4 +211,4 @@ Currently, the default value for {orgs} is <b>noco</b>. Users will be able to ch
|---|---| |---|---|
| ~or | (checkNumber,eq,JM555205)~or((amount, gt, 200)~and(amount, lt, 2000)) | | ~or | (checkNumber,eq,JM555205)~or((amount, gt, 200)~and(amount, lt, 2000)) |
| ~and | (checkNumber,eq,JM555205)~and((amount, gt, 200)~and(amount, lt, 2000)) | | ~and | (checkNumber,eq,JM555205)~and((amount, gt, 200)~and(amount, lt, 2000)) |
| ~not | ~not(checkNumber,eq,JM555205) | | ~not | ~not(checkNumber,eq,JM555205) |

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

File diff suppressed because it is too large Load Diff

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

@ -3501,5 +3501,27 @@ export class Api<
type: ContentType.FormData, type: ContentType.FormData,
...params, ...params,
}), }),
/**
* No description
*
* @tags Storage
* @name UploadByUrl
* @summary Attachment
* @request POST:/api/v1/db/storage/upload-by-url
*/
uploadByUrl: (
query: { path: string },
data: { url?: string }[],
params: RequestParams = {}
) =>
this.request<any, any>({
path: `/api/v1/db/storage/upload-by-url`,
method: 'POST',
query: query,
body: data,
type: ContentType.Json,
...params,
}),
}; };
} }

24974
packages/nocodb/package-lock.json generated

File diff suppressed because it is too large Load Diff

4
packages/nocodb/package.json

@ -154,9 +154,9 @@
"nc-common": "0.0.6", "nc-common": "0.0.6",
"nc-help": "0.2.59", "nc-help": "0.2.59",
"nc-lib-gui": "0.91.1", "nc-lib-gui": "0.91.1",
"nc-plugin": "^0.1.1", "nc-plugin": "0.1.2",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nocodb-sdk": "0.91.1", "nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10", "nodemailer": "^6.4.10",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"ora": "^4.0.4", "ora": "^4.0.4",

19
packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSqlv2.ts

@ -1527,9 +1527,19 @@ class BaseModelSqlv2 {
for (const data of datas) { for (const data of datas) {
await this.validate(data); await this.validate(data);
} }
// let chunkSize = 50;
//
// if (this.isSqlite && datas[0]) {
// chunkSize = Math.max(1, Math.floor(999 / Object.keys(datas[0]).length));
// }
// fallbacks to `10` if database client is sqlite
// to avoid `too many SQL variables` error
// refer : https://www.sqlite.org/limits.html
const chunkSize = this.isSqlite ? 10 : 50;
const response = await this.dbDriver const response = await this.dbDriver
.batchInsert(this.model.table_name, insertDatas, 50) .batchInsert(this.model.table_name, insertDatas, chunkSize)
.returning(this.model.primaryKey?.column_name); .returning(this.model.primaryKey?.column_name);
// await this.afterInsertb(insertDatas, null); // await this.afterInsertb(insertDatas, null);
@ -1834,7 +1844,12 @@ class BaseModelSqlv2 {
if (!validate) continue; if (!validate) continue;
const { func, msg } = validate; const { func, msg } = validate;
for (let j = 0; j < func.length; ++j) { for (let j = 0; j < func.length; ++j) {
const fn = typeof func[j] === 'string' ? (customValidators[func[j]] ? customValidators[func[j]] : Validator[func[j]]) : func[j]; const fn =
typeof func[j] === 'string'
? customValidators[func[j]]
? customValidators[func[j]]
: Validator[func[j]]
: func[j];
const arg = const arg =
typeof func[j] === 'string' ? columns[cn] + '' : columns[cn]; typeof func[j] === 'string' ? columns[cn] + '' : columns[cn];
if ( if (

27
packages/nocodb/src/lib/dataMapper/lib/sql/conditionV2.ts

@ -203,12 +203,13 @@ const parseConditionV2 = async (
filter.comparison_op === 'notempty' filter.comparison_op === 'notempty'
) )
filter.value = ''; filter.value = '';
const field = customWhereClause let field = customWhereClause
? filter.value ? filter.value
: alias : alias
? `${alias}.${column.column_name}` ? `${alias}.${column.column_name}`
: column.column_name; : column.column_name;
const val = customWhereClause ? customWhereClause : filter.value; let val = customWhereClause ? customWhereClause : filter.value;
return qb => { return qb => {
switch (filter.comparison_op) { switch (filter.comparison_op) {
case 'eq': case 'eq':
@ -218,17 +219,29 @@ const parseConditionV2 = async (
qb = qb.whereNot(field, val); qb = qb.whereNot(field, val);
break; break;
case 'like': case 'like':
if (column.uidt === UITypes.Formula) {
[field, val] = [val, field];
val = `%${val}%`.replace(/^%'([\s\S]*)'%$/, '%$1%')
} else {
val = `%${val}%`;
}
qb = qb.where( qb = qb.where(
field, field,
qb?.client?.config?.client === 'pg' ? 'ilike' : 'like', qb?.client?.config?.client === 'pg' ? 'ilike' : 'like',
`%${val}%` val
); );
break; break;
case 'nlike': case 'nlike':
if (column.uidt === UITypes.Formula) {
[field, val] = [val, field];
val = `%${val}%`.replace(/^%'([\s\S]*)'%$/, '%$1%')
} else {
val = `%${val}%`;
}
qb = qb.whereNot( qb = qb.whereNot(
field, field,
qb?.client?.config?.client === 'pg' ? 'ilike' : 'like', qb?.client?.config?.client === 'pg' ? 'ilike' : 'like',
`%${val}%` val
); );
break; break;
case 'gt': case 'gt':
@ -273,9 +286,15 @@ const parseConditionV2 = async (
break; break;
case 'empty': case 'empty':
if (column.uidt === UITypes.Formula) {
[field, val] = [val, field];
}
qb = qb.where(field, val); qb = qb.where(field, val);
break; break;
case 'notempty': case 'notempty':
if (column.uidt === UITypes.Formula) {
[field, val] = [val, field];
}
qb = qb.whereNot(field, val); qb = qb.whereNot(field, val);
break; break;
case 'null': case 'null':

6
packages/nocodb/src/lib/noco/common/XcMigrationSourcev2.ts

@ -3,6 +3,7 @@ import * as nc_012_alter_column_data_types from '../migrationsv2/nc_012_alter_co
import * as nc_013_sync_source from '../migrationsv2/nc_013_sync_source'; import * as nc_013_sync_source from '../migrationsv2/nc_013_sync_source';
import * as nc_014_alter_column_data_types from '../migrationsv2/nc_014_alter_column_data_types'; import * as nc_014_alter_column_data_types from '../migrationsv2/nc_014_alter_column_data_types';
import * as nc_015_add_meta_col_in_column_table from '../migrationsv2/nc_015_add_meta_col_in_column_table'; import * as nc_015_add_meta_col_in_column_table from '../migrationsv2/nc_015_add_meta_col_in_column_table';
import * as nc_016_alter_hooklog_payload_types from '../migrationsv2/nc_016_alter_hooklog_payload_types';
// Create a custom migration source class // Create a custom migration source class
export default class XcMigrationSourcev2 { export default class XcMigrationSourcev2 {
@ -16,7 +17,8 @@ export default class XcMigrationSourcev2 {
'nc_012_alter_column_data_types', 'nc_012_alter_column_data_types',
'nc_013_sync_source', 'nc_013_sync_source',
'nc_014_alter_column_data_types', 'nc_014_alter_column_data_types',
'nc_015_add_meta_col_in_column_table' 'nc_015_add_meta_col_in_column_table',
'nc_016_alter_hooklog_payload_types'
]); ]);
} }
@ -36,6 +38,8 @@ export default class XcMigrationSourcev2 {
return nc_014_alter_column_data_types; return nc_014_alter_column_data_types;
case 'nc_015_add_meta_col_in_column_table': case 'nc_015_add_meta_col_in_column_table':
return nc_015_add_meta_col_in_column_table; return nc_015_add_meta_col_in_column_table;
case 'nc_016_alter_hooklog_payload_types':
return nc_016_alter_hooklog_payload_types;
} }
} }
} }

5
packages/nocodb/src/lib/noco/meta/NcMetaIOImpl.ts

@ -307,6 +307,11 @@ export default class NcMetaIOImpl extends NcMetaIO {
(query as any).condition(args.xcCondition); (query as any).condition(args.xcCondition);
} }
if (args?.orderBy) {
for (const [col, dir] of Object.entries(args.orderBy)) {
query.orderBy(col, dir);
}
}
if (args?.fields?.length) { if (args?.fields?.length) {
query.select(...args.fields); query.select(...args.fields);
} }

44
packages/nocodb/src/lib/noco/meta/api/attachmentApis.ts

@ -47,6 +47,45 @@ export async function upload(req: Request, res: Response) {
res.json(attachments); res.json(attachments);
} }
export async function uploadViaURL(req: Request, res: Response) {
const filePath = sanitizeUrlPath(
req.query?.path?.toString()?.split('/') || ['']
);
const destPath = path.join('nc', 'uploads', ...filePath);
const storageAdapter = await NcPluginMgrv2.storageAdapter();
const attachments = await Promise.all(
req.body?.map?.(async urlMeta => {
const { url, fileName: _fileName } = urlMeta;
const fileName = `${nanoid(6)}${_fileName || url.split('/').pop()}`;
let attachmentUrl = await (storageAdapter as any).fileCreateByUrl(
slash(path.join(destPath, fileName)),
url
);
if (!attachmentUrl) {
attachmentUrl = `${(req as any).ncSiteUrl}/download/${filePath.join(
'/'
)}/${fileName}`;
}
return {
url: attachmentUrl,
title: fileName,
mimetype: urlMeta.mimetype,
size: urlMeta.size,
icon: mimeIcons[path.extname(fileName).slice(1)] || undefined
};
})
);
Tele.emit('evt', { evt_type: 'image:uploaded' });
res.json(attachments);
}
export async function fileRead(req, res) { export async function fileRead(req, res) {
try { try {
const storageAdapter = await NcPluginMgrv2.storageAdapter(); const storageAdapter = await NcPluginMgrv2.storageAdapter();
@ -79,6 +118,7 @@ export async function fileRead(req, res) {
res.status(404).send('Not found'); res.status(404).send('Not found');
} }
} }
const router = Router({ mergeParams: true }); const router = Router({ mergeParams: true });
router.get(/^\/dl\/([^/]+)\/([^/]+)\/(.+)$/, async (req, res) => { router.get(/^\/dl\/([^/]+)\/([^/]+)\/(.+)$/, async (req, res) => {
@ -124,6 +164,10 @@ router.post(
}).any(), }).any(),
ncMetaAclMw(upload, 'upload') ncMetaAclMw(upload, 'upload')
); );
router.post(
'/api/v1/db/storage/upload-by-url',
ncMetaAclMw(uploadViaURL, 'uploadViaURL')
);
router.get(/^\/download\/(.+)$/, catchError(fileRead)); router.get(/^\/download\/(.+)$/, catchError(fileRead));
export default router; export default router;

6
packages/nocodb/src/lib/noco/meta/api/sync/helpers/job.ts

@ -81,7 +81,6 @@ export default async (
const nestedRollupTbl: any[] = []; const nestedRollupTbl: any[] = [];
const ncSysFields = { id: 'ncRecordId', hash: 'ncRecordHash' }; const ncSysFields = { id: 'ncRecordId', hash: 'ncRecordHash' };
const storeLinks = false; const storeLinks = false;
const skipAttachments = false;
const ncLinkDataStore: any = {}; const ncLinkDataStore: any = {};
const uniqueTableNameGen = getUniqueNameGenerator('sheet'); const uniqueTableNameGen = getUniqueNameGenerator('sheet');
@ -164,8 +163,7 @@ export default async (
collaborator: UITypes.Collaborator, collaborator: UITypes.Collaborator,
multiCollaborator: UITypes.Collaborator, multiCollaborator: UITypes.Collaborator,
date: UITypes.Date, date: UITypes.Date,
// kludge: phone: UITypes.PhoneNumber, phone: UITypes.PhoneNumber,
phone: UITypes.SingleLineText,
number: UITypes.Number, number: UITypes.Number,
rating: UITypes.Rating, rating: UITypes.Rating,
formula: UITypes.Formula, formula: UITypes.Formula,
@ -1315,7 +1313,7 @@ export default async (
break; break;
case UITypes.Attachment: case UITypes.Attachment:
if (skipAttachments) rec[key] = null; if (syncDB.options.syncLookup) rec[key] = null;
else { else {
const tempArr = []; const tempArr = [];
for (const v of value) { for (const v of value) {

12
packages/nocodb/src/lib/noco/meta/helpers/NcPluginMgrv2.ts

@ -1,6 +1,6 @@
import { import {
IEmailAdapter, IEmailAdapter,
IStorageAdapter, IStorageAdapterV2,
IWebhookNotificationAdapter IWebhookNotificationAdapter
// XcEmailPlugin, // XcEmailPlugin,
// XcPlugin, // XcPlugin,
@ -80,6 +80,14 @@ class NcPluginMgrv2 {
category: plugin.category, category: plugin.category,
input_schema: JSON.stringify(plugin.inputs) input_schema: JSON.stringify(plugin.inputs)
}); });
} else if (pluginConfig.version !== plugin.version) {
await ncMeta.metaUpdate(
null,
null,
MetaTable.PLUGIN,
plugin,
pluginConfig.id
);
} }
/* init only the active plugins */ /* init only the active plugins */
@ -105,7 +113,7 @@ class NcPluginMgrv2 {
public static async storageAdapter( public static async storageAdapter(
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
): Promise<IStorageAdapter> { ): Promise<IStorageAdapterV2> {
const pluginData = await ncMeta.metaGet2(null, null, MetaTable.PLUGIN, { const pluginData = await ncMeta.metaGet2(null, null, MetaTable.PLUGIN, {
category: PluginCategory.STORAGE, category: PluginCategory.STORAGE,
active: true active: true

42
packages/nocodb/src/lib/noco/migrationsv2/nc_016_alter_hooklog_payload_types.ts

@ -0,0 +1,42 @@
import Knex from 'knex';
import { MetaTable } from '../../utils/globals';
const up = async (knex: Knex) => {
if (knex.client.config.client !== 'sqlite3') {
await knex.schema.alterTable(MetaTable.HOOK_LOGS, table => {
table.text('payload').alter();
});
}
};
const down = async knex => {
if (knex.client.config.client !== 'sqlite3') {
await knex.schema.alterTable(MetaTable.HOOK_LOGS, table => {
table.boolean('payload').alter();
});
}
};
export { up, down };
/**
* @copyright Copyright (c) 2022, Xgene Cloud Ltd
*
* @author willnewii <willnewii@163.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

46
packages/nocodb/src/lib/noco/plugins/adapters/storage/Local.ts

@ -3,12 +3,12 @@ import path from 'path';
import mkdirp from 'mkdirp'; import mkdirp from 'mkdirp';
import IStorageAdapter, { import { IStorageAdapterV2, XcFile } from 'nc-plugin';
XcFile
} from '../../../../../interface/IStorageAdapter';
import NcConfigFactory from '../../../../utils/NcConfigFactory'; import NcConfigFactory from '../../../../utils/NcConfigFactory';
export default class Local implements IStorageAdapter { import request from 'request';
export default class Local implements IStorageAdapterV2 {
constructor() {} constructor() {}
public async fileCreate(key: string, file: XcFile): Promise<any> { public async fileCreate(key: string, file: XcFile): Promise<any> {
@ -24,6 +24,44 @@ export default class Local implements IStorageAdapter {
} }
} }
async fileCreateByUrl(key: string, url: string): Promise<any> {
const destPath = path.join(NcConfigFactory.getToolDir(), ...key.split('/'));
return new Promise((resolve, reject) => {
mkdirp.sync(path.dirname(destPath));
const file = fs.createWriteStream(destPath);
const sendReq = request.get(url);
// verify response code
sendReq.on('response', response => {
if (response.statusCode !== 200) {
return reject('Response status was ' + response.statusCode);
}
sendReq.pipe(file);
});
// close() is async, call cb after close completes
file.on('finish', () => {
file.close(err => {
if (err) {
return reject(err);
}
resolve(null);
});
});
// check for request errors
sendReq.on('error', err => {
fs.unlink(destPath, () => reject(err.message)); // delete the (partial) file and then return the error
});
file.on('error', err => {
// Handle errors
fs.unlink(destPath, () => reject(err.message)); // delete the (partial) file and then return the error
});
});
}
// todo: implement // todo: implement
fileDelete(_path: string): Promise<any> { fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined); return Promise.resolve(undefined);

4
packages/nocodb/src/lib/utils/projectAcl.ts

@ -132,7 +132,8 @@ export default {
relationDataRemove: true, relationDataRemove: true,
relationDataAdd: true, relationDataAdd: true,
dataCount: true, dataCount: true,
upload: true upload: true,
uploadViaURL: true
}, },
commenter: { commenter: {
formViewGet: true, formViewGet: true,
@ -242,6 +243,7 @@ export default {
super: '*', super: '*',
user: { user: {
upload: true, upload: true,
uploadViaURL: true,
passwordChange: true, passwordChange: true,
pluginList: true, pluginList: true,
pluginRead: true, pluginRead: true,

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

@ -2,9 +2,10 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import AWS from 'aws-sdk'; import AWS from 'aws-sdk';
import { IStorageAdapter, XcFile } from 'nc-plugin'; import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import request from 'request';
export default class Backblaze implements IStorageAdapter { export default class Backblaze implements IStorageAdapterV2 {
private s3Client: AWS.S3; private s3Client: AWS.S3;
private input: any; private input: any;
@ -40,6 +41,38 @@ export default class Backblaze implements IStorageAdapter {
}); });
} }
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read'
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
request(
{
url: url,
encoding: null
},
(err, _, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err) {
console.log('Error', err);
reject(err1);
}
if (data) {
resolve(data.Location);
}
});
}
);
});
}
public async fileDelete(_path: string): Promise<any> { public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }

4
packages/nocodb/src/plugins/backblaze/BackblazePlugin.ts

@ -1,11 +1,11 @@
import { IStorageAdapter, XcStoragePlugin } from 'nc-plugin'; import { IStorageAdapterV2, XcStoragePlugin } from 'nc-plugin';
import Backblaze from './Backblaze'; import Backblaze from './Backblaze';
class BackblazePlugin extends XcStoragePlugin { class BackblazePlugin extends XcStoragePlugin {
private static storageAdapter: Backblaze; private static storageAdapter: Backblaze;
public getAdapter(): IStorageAdapter { public getAdapter(): IStorageAdapterV2 {
return BackblazePlugin.storageAdapter; return BackblazePlugin.storageAdapter;
} }

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

@ -2,9 +2,10 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import { Storage, StorageOptions } from '@google-cloud/storage'; import { Storage, StorageOptions } from '@google-cloud/storage';
import { IStorageAdapter, XcFile } from 'nc-plugin'; import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import request from 'request';
export default class Gcs implements IStorageAdapter { export default class Gcs implements IStorageAdapterV2 {
private storageClient: Storage; private storageClient: Storage;
private bucketName: string; private bucketName: string;
private input: any; private input: any;
@ -67,9 +68,16 @@ export default class Gcs implements IStorageAdapter {
// this.bucketName = process.env.NC_GCS_BUCKET; // this.bucketName = process.env.NC_GCS_BUCKET;
options.credentials = { options.credentials = {
client_email: this.input.client_email, client_email: this.input.client_email,
private_key: this.input.private_key // replace \n with real line breaks to avoid
// error:0909006C:PEM routines:get_name:no start line
private_key: this.input.private_key.replace(/\\n/gm, '\n')
}; };
// default project ID would be used if it is not provided
if (this.input.project_id) {
options.projectId = this.input.project_id
}
this.bucketName = this.input.bucket; this.bucketName = this.input.bucket;
this.storageClient = new Storage(options); this.storageClient = new Storage(options);
@ -92,6 +100,28 @@ export default class Gcs implements IStorageAdapter {
throw e; throw e;
} }
} }
fileCreateByUrl(destPath: string, url: string): Promise<any> {
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
request(
{
url: url,
encoding: null
},
(err, _, body) => {
if (err) return reject(err);
this.storageClient
.bucket(this.bucketName)
.file(destPath)
.save(body)
.then(res => resolve(res))
.catch(reject);
}
);
});
}
} }
/** /**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd * @copyright Copyright (c) 2021, Xgene Cloud Ltd

4
packages/nocodb/src/plugins/gcs/GcsPlugin.ts

@ -1,11 +1,11 @@
import { IStorageAdapter, XcStoragePlugin } from 'nc-plugin'; import { IStorageAdapterV2, XcStoragePlugin } from 'nc-plugin';
import Gcs from './Gcs'; import Gcs from './Gcs';
class GcsPlugin extends XcStoragePlugin { class GcsPlugin extends XcStoragePlugin {
private static storageAdapter: Gcs; private static storageAdapter: Gcs;
public getAdapter(): IStorageAdapter { public getAdapter(): IStorageAdapterV2 {
return GcsPlugin.storageAdapter; return GcsPlugin.storageAdapter;
} }

9
packages/nocodb/src/plugins/gcs/index.ts

@ -6,7 +6,7 @@ import GcsPlugin from './GcsPlugin';
const config: XcPluginConfig = { const config: XcPluginConfig = {
builder: GcsPlugin, builder: GcsPlugin,
title: 'GCS', title: 'GCS',
version: '0.0.1', version: '0.0.2',
logo: 'plugins/gcs.png', logo: 'plugins/gcs.png',
description: description:
'Google Cloud Storage is a RESTful online file storage web service for storing and accessing data on Google Cloud Platform infrastructure.', 'Google Cloud Storage is a RESTful online file storage web service for storing and accessing data on Google Cloud Platform infrastructure.',
@ -36,6 +36,13 @@ const config: XcPluginConfig = {
placeholder: 'Private Key', placeholder: 'Private Key',
type: XcType.Password, type: XcType.Password,
required: true required: true
},
{
key: 'project_id',
label: 'Project ID',
placeholder: 'Project ID',
type: XcType.SingleLineText,
required: false
} }
], ],
actions: [ actions: [

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

@ -2,9 +2,10 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import AWS from 'aws-sdk'; import AWS from 'aws-sdk';
import { IStorageAdapter, XcFile } from 'nc-plugin'; import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import request from 'request';
export default class LinodeObjectStorage implements IStorageAdapter { export default class LinodeObjectStorage implements IStorageAdapterV2 {
private s3Client: AWS.S3; private s3Client: AWS.S3;
private input: any; private input: any;
@ -40,6 +41,38 @@ export default class LinodeObjectStorage implements IStorageAdapter {
}); });
} }
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read'
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
request(
{
url: url,
encoding: null
},
(err, _, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err) {
console.log('Error', err);
reject(err1);
}
if (data) {
resolve(data.Location);
}
});
}
);
});
}
public async fileDelete(_path: string): Promise<any> { public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }

4
packages/nocodb/src/plugins/linode/LinodeObjectStoragePlugin.ts

@ -1,11 +1,11 @@
import { IStorageAdapter, XcStoragePlugin } from 'nc-plugin'; import { IStorageAdapterV2, XcStoragePlugin } from 'nc-plugin';
import LinodeObjectStorage from './LinodeObjectStorage'; import LinodeObjectStorage from './LinodeObjectStorage';
class LinodeObjectStoragePlugin extends XcStoragePlugin { class LinodeObjectStoragePlugin extends XcStoragePlugin {
private static storageAdapter: LinodeObjectStorage; private static storageAdapter: LinodeObjectStorage;
public getAdapter(): IStorageAdapter { public getAdapter(): IStorageAdapterV2 {
return LinodeObjectStoragePlugin.storageAdapter; return LinodeObjectStoragePlugin.storageAdapter;
} }

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

@ -1,12 +1,11 @@
import fs from "fs"; import fs from 'fs';
import path from "path"; import path from 'path';
import {Client as MinioClient} from "minio";
import {IStorageAdapter, XcFile} from "nc-plugin";
export default class Minio implements IStorageAdapter {
import { Client as MinioClient } from 'minio';
import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import request from 'request';
export default class Minio implements IStorageAdapterV2 {
private minioClient: MinioClient; private minioClient: MinioClient;
private input: any; private input: any;
@ -14,13 +13,11 @@ export default class Minio implements IStorageAdapter {
this.input = input; this.input = input;
} }
async fileCreate(key: string, file: XcFile): Promise<any> { async fileCreate(key: string, file: XcFile): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters // Configure the file stream and obtain the upload parameters
const fileStream = fs.createReadStream(file.path); const fileStream = fs.createReadStream(file.path);
fileStream.on('error', (err) => { fileStream.on('error', err => {
console.log('File Error', err); console.log('File Error', err);
reject(err); reject(err);
}); });
@ -28,14 +25,21 @@ export default class Minio implements IStorageAdapter {
// uploadParams.Body = fileStream; // uploadParams.Body = fileStream;
// uploadParams.Key = key; // uploadParams.Key = key;
const metaData = { const metaData = {
'Content-Type': file.mimetype, 'Content-Type': file.mimetype
// 'X-Amz-Meta-Testing': 1234, // 'X-Amz-Meta-Testing': 1234,
// 'example': 5678 // 'example': 5678
} };
// call S3 to retrieve upload file to specified bucket // call S3 to retrieve upload file to specified bucket
this.minioClient.putObject(this.input?.bucket, key, fileStream,metaData).then(()=>{ this.minioClient
resolve(`http${this.input.useSSL ? 's' : ''}://${this.input.endPoint}:${this.input.port}/${this.input.bucket}/${key}`) .putObject(this.input?.bucket, key, fileStream, metaData)
}).catch(reject) .then(() => {
resolve(
`http${this.input.useSSL ? 's' : ''}://${this.input.endPoint}:${
this.input.port
}/${this.input.bucket}/${key}`
);
})
.catch(reject);
}); });
} }
@ -85,9 +89,47 @@ export default class Minio implements IStorageAdapter {
} }
} }
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read'
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
request(
{
url: url,
encoding: null
},
(err, _, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
// uploadParams.Body = fileStream;
// uploadParams.Key = key;
const metaData = {
// 'Content-Type': file.mimetype
// 'X-Amz-Meta-Testing': 1234,
// 'example': 5678
};
// call S3 to retrieve upload file to specified bucket
this.minioClient
.putObject(this.input?.bucket, key, body, metaData)
.then(() => {
resolve(
`http${this.input.useSSL ? 's' : ''}://${this.input.endPoint}:${
this.input.port
}/${this.input.bucket}/${key}`
);
})
.catch(reject);
}
);
});
}
} }
/** /**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd * @copyright Copyright (c) 2021, Xgene Cloud Ltd
* *

4
packages/nocodb/src/plugins/mino/MinioPlugin.ts

@ -1,11 +1,11 @@
import { IStorageAdapter, XcStoragePlugin } from 'nc-plugin'; import { IStorageAdapterV2, XcStoragePlugin } from 'nc-plugin';
import Minio from './Minio'; import Minio from './Minio';
class MinioPlugin extends XcStoragePlugin { class MinioPlugin extends XcStoragePlugin {
private static storageAdapter: Minio; private static storageAdapter: Minio;
public getAdapter(): IStorageAdapter { public getAdapter(): IStorageAdapterV2 {
return MinioPlugin.storageAdapter; return MinioPlugin.storageAdapter;
} }

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

@ -2,9 +2,10 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import AWS from 'aws-sdk'; import AWS from 'aws-sdk';
import { IStorageAdapter, XcFile } from 'nc-plugin'; import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import request from 'request';
export default class OvhCloud implements IStorageAdapter { export default class OvhCloud implements IStorageAdapterV2 {
private s3Client: AWS.S3; private s3Client: AWS.S3;
private input: any; private input: any;
@ -40,6 +41,38 @@ export default class OvhCloud implements IStorageAdapter {
}); });
} }
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read'
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
request(
{
url: url,
encoding: null
},
(err, _, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err) {
console.log('Error', err);
reject(err1);
}
if (data) {
resolve(data.Location);
}
});
}
);
});
}
public async fileDelete(_path: string): Promise<any> { public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }

4
packages/nocodb/src/plugins/ovhCloud/OvhCloudPlugin.ts

@ -1,11 +1,11 @@
import { IStorageAdapter, XcStoragePlugin } from 'nc-plugin'; import { IStorageAdapterV2, XcStoragePlugin } from 'nc-plugin';
import OvhCloud from './OvhCloud'; import OvhCloud from './OvhCloud';
class OvhCloudPlugin extends XcStoragePlugin { class OvhCloudPlugin extends XcStoragePlugin {
private static storageAdapter: OvhCloud; private static storageAdapter: OvhCloud;
public getAdapter(): IStorageAdapter { public getAdapter(): IStorageAdapterV2 {
return OvhCloudPlugin.storageAdapter; return OvhCloudPlugin.storageAdapter;
} }

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

@ -2,9 +2,10 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import AWS from 'aws-sdk'; import AWS from 'aws-sdk';
import { IStorageAdapter, XcFile } from 'nc-plugin'; import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import request from 'request';
export default class S3 implements IStorageAdapter { export default class S3 implements IStorageAdapterV2 {
private s3Client: AWS.S3; private s3Client: AWS.S3;
private input: any; private input: any;
@ -39,6 +40,37 @@ export default class S3 implements IStorageAdapter {
}); });
}); });
} }
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read'
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
request(
{
url: url,
encoding: null
},
(err, _, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err) {
console.log('Error', err);
reject(err1);
}
if (data) {
resolve(data.Location);
}
});
}
);
});
}
public async fileDelete(_path: string): Promise<any> { public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined); return Promise.resolve(undefined);

4
packages/nocodb/src/plugins/s3/S3Plugin.ts

@ -1,11 +1,11 @@
import { IStorageAdapter, XcStoragePlugin } from 'nc-plugin'; import { IStorageAdapterV2, XcStoragePlugin } from 'nc-plugin';
import S3 from './S3'; import S3 from './S3';
class S3Plugin extends XcStoragePlugin { class S3Plugin extends XcStoragePlugin {
private static storageAdapter: S3; private static storageAdapter: S3;
public getAdapter(): IStorageAdapter { public getAdapter(): IStorageAdapterV2 {
return S3Plugin.storageAdapter; return S3Plugin.storageAdapter;
} }

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

@ -1,9 +1,10 @@
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
import { IStorageAdapter, XcFile } from 'nc-plugin'; import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import AWS from 'aws-sdk'; import AWS from 'aws-sdk';
import request from 'request';
export default class ScalewayObjectStorage implements IStorageAdapter { export default class ScalewayObjectStorage implements IStorageAdapterV2 {
private s3Client: AWS.S3; private s3Client: AWS.S3;
private input: any; private input: any;
@ -88,4 +89,36 @@ export default class ScalewayObjectStorage implements IStorageAdapter {
}); });
}); });
} }
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read'
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
request(
{
url: url,
encoding: null
},
(err, _, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err) {
console.log('Error', err);
reject(err1);
}
if (data) {
resolve(data.Location);
}
});
}
);
});
}
} }

4
packages/nocodb/src/plugins/scaleway/ScalewayObjectStoragePlugin.ts

@ -1,4 +1,4 @@
import { IStorageAdapter, XcStoragePlugin } from 'nc-plugin'; import { IStorageAdapterV2, XcStoragePlugin } from 'nc-plugin';
import ScalewayObjectStorage from './ScalewayObjectStorage'; import ScalewayObjectStorage from './ScalewayObjectStorage';
@ -10,7 +10,7 @@ class ScalewayObjectStoragePlugin extends XcStoragePlugin {
); );
await ScalewayObjectStoragePlugin.storageAdapter.init(); await ScalewayObjectStoragePlugin.storageAdapter.init();
} }
public getAdapter(): IStorageAdapter { public getAdapter(): IStorageAdapterV2 {
return ScalewayObjectStoragePlugin.storageAdapter; return ScalewayObjectStoragePlugin.storageAdapter;
} }
} }

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

@ -2,9 +2,10 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import AWS from 'aws-sdk'; import AWS from 'aws-sdk';
import { IStorageAdapter, XcFile } from 'nc-plugin'; import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import request from 'request';
export default class Spaces implements IStorageAdapter { export default class Spaces implements IStorageAdapterV2 {
private s3Client: AWS.S3; private s3Client: AWS.S3;
private input: any; private input: any;
@ -40,6 +41,38 @@ export default class Spaces implements IStorageAdapter {
}); });
} }
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read'
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
request(
{
url: url,
encoding: null
},
(err, _, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err) {
console.log('Error', err);
reject(err1);
}
if (data) {
resolve(data.Location);
}
});
}
);
});
}
public async fileDelete(_path: string): Promise<any> { public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }

4
packages/nocodb/src/plugins/spaces/SpacesPlugin.ts

@ -1,11 +1,11 @@
import { IStorageAdapter, XcStoragePlugin } from 'nc-plugin'; import { IStorageAdapterV2, XcStoragePlugin } from 'nc-plugin';
import Spaces from './Spaces'; import Spaces from './Spaces';
class SpacesPlugin extends XcStoragePlugin { class SpacesPlugin extends XcStoragePlugin {
private static storageAdapter: Spaces; private static storageAdapter: Spaces;
public getAdapter(): IStorageAdapter { public getAdapter(): IStorageAdapterV2 {
return SpacesPlugin.storageAdapter; return SpacesPlugin.storageAdapter;
} }

4
packages/nocodb/src/plugins/upcloud/UpCloudPlugin.ts

@ -1,11 +1,11 @@
import { IStorageAdapter, XcStoragePlugin } from 'nc-plugin'; import { IStorageAdapterV2, XcStoragePlugin } from 'nc-plugin';
import UpoCloud from './UpoCloud'; import UpoCloud from './UpoCloud';
class UpCloudPlugin extends XcStoragePlugin { class UpCloudPlugin extends XcStoragePlugin {
private static storageAdapter: UpoCloud; private static storageAdapter: UpoCloud;
public getAdapter(): IStorageAdapter { public getAdapter(): IStorageAdapterV2 {
return UpCloudPlugin.storageAdapter; return UpCloudPlugin.storageAdapter;
} }

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

@ -2,9 +2,10 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import AWS from 'aws-sdk'; import AWS from 'aws-sdk';
import { IStorageAdapter, XcFile } from 'nc-plugin'; import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import request from 'request';
export default class UpoCloud implements IStorageAdapter { export default class UpoCloud implements IStorageAdapterV2 {
private s3Client: AWS.S3; private s3Client: AWS.S3;
private input: any; private input: any;
@ -40,6 +41,38 @@ export default class UpoCloud implements IStorageAdapter {
}); });
} }
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read'
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
request(
{
url: url,
encoding: null
},
(err, _, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err) {
console.log('Error', err);
reject(err1);
}
if (data) {
resolve(data.Location);
}
});
}
);
});
}
public async fileDelete(_path: string): Promise<any> { public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }

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

@ -2,9 +2,10 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import AWS from 'aws-sdk'; import AWS from 'aws-sdk';
import { IStorageAdapter, XcFile } from 'nc-plugin'; import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import request from 'request';
export default class Vultr implements IStorageAdapter { export default class Vultr implements IStorageAdapterV2 {
private s3Client: AWS.S3; private s3Client: AWS.S3;
private input: any; private input: any;
@ -40,6 +41,38 @@ export default class Vultr implements IStorageAdapter {
}); });
} }
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read'
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
request(
{
url: url,
encoding: null
},
(err, _, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err) {
console.log('Error', err);
reject(err1);
}
if (data) {
resolve(data.Location);
}
});
}
);
});
}
public async fileDelete(_path: string): Promise<any> { public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }

4
packages/nocodb/src/plugins/vultr/VultrPlugin.ts

@ -1,11 +1,11 @@
import { IStorageAdapter, XcStoragePlugin } from 'nc-plugin'; import { IStorageAdapterV2, XcStoragePlugin } from 'nc-plugin';
import Vultr from './Vultr'; import Vultr from './Vultr';
class VultrPlugin extends XcStoragePlugin { class VultrPlugin extends XcStoragePlugin {
private static storageAdapter: Vultr; private static storageAdapter: Vultr;
public getAdapter(): IStorageAdapter { public getAdapter(): IStorageAdapterV2 {
return VultrPlugin.storageAdapter; return VultrPlugin.storageAdapter;
} }

13
scripts/cypress/support/page_objects/mainPage.js

@ -297,12 +297,13 @@ export class _mainPage {
cy.snipActiveMenu("Menu_SortField"); cy.snipActiveMenu("Menu_SortField");
cy.get(".nc-sort-field-select div").first().click(); cy.get(".nc-sort-field-select div").first().click().type(field);
cy.snipActiveMenu("Menu_SortField_fieldSelection"); cy.snipActiveMenu("Menu_SortField_fieldSelection");
// cy.get(`.menuable__content__active .v-list-item:contains(${field})`) // cy.get(`.menuable__content__active .v-list-item:contains(${field})`)
// .first() // .first()
// .click(); // .click();
cy.getActiveMenu().find(`.nc-sort-fld-${field}`).click(); // cy.wait(3000)
cy.getActiveMenu().find(`.nc-fld-${field}`).should('exist').click();
cy.get(".nc-sort-dir-select div").first().click(); cy.get(".nc-sort-dir-select div").first().click();
cy.snipActiveMenu("Menu_SortField_criteriaSelection"); cy.snipActiveMenu("Menu_SortField_criteriaSelection");
cy.get( cy.get(
@ -319,15 +320,15 @@ export class _mainPage {
filterField = (field, operation, value) => { filterField = (field, operation, value) => {
cy.get(".nc-filter-menu-btn").click(); cy.get(".nc-filter-menu-btn").click();
cy.wait(2000); // cy.wait(2000);
cy.contains("Add Filter").click(); cy.contains("Add Filter").click();
cy.wait(2000); // cy.wait(2000);
cy.snipActiveMenu("Menu_FilterField"); cy.snipActiveMenu("Menu_FilterField");
cy.get(".nc-filter-field-select").should("exist").last().click(); cy.get(".nc-filter-field-select").should("exist").last().click().type(field);;
cy.snipActiveMenu("Menu_FilterField-fieldSelect"); cy.snipActiveMenu("Menu_FilterField-fieldSelect");
cy.getActiveMenu().find(`.nc-filter-fld-${field}`).click(); cy.getActiveMenu().find(`.nc-fld-${field}`).should('exist').click();
cy.get(".nc-filter-operation-select").should("exist").last().click(); cy.get(".nc-filter-operation-select").should("exist").last().click();
cy.snipActiveMenu("Menu_FilterField-operationSelect"); cy.snipActiveMenu("Menu_FilterField-operationSelect");

58
scripts/sdk/swagger.json

@ -2625,14 +2625,14 @@
"name": "tableName", "name": "tableName",
"in": "path", "in": "path",
"required": true "required": true
},{ },
{
"schema": { "schema": {
"type": "string" "type": "string"
}, },
"in": "query", "in": "query",
"name": "column_name", "name": "column_name",
"description": "description": "Column name of the column you want to group by, eg. `column_name=column1`"
"Column name of the column you want to group by, eg. `column_name=column1`"
} }
], ],
"get": { "get": {
@ -2907,14 +2907,14 @@
"name": "viewName", "name": "viewName",
"in": "path", "in": "path",
"required": true "required": true
},{ },
{
"schema": { "schema": {
"type": "string" "type": "string"
}, },
"in": "query", "in": "query",
"name": "column_name", "name": "column_name",
"description": "description": "Column name of the column you want to group by, eg. `column_name=column1`"
"Column name of the column you want to group by, eg. `column_name=column1`"
} }
], ],
"get": { "get": {
@ -5248,6 +5248,52 @@
] ]
} }
}, },
"/api/v1/db/storage/upload-by-url": {
"post": {
"summary": "Attachment",
"operationId": "storage-upload-by-url",
"responses": {},
"tags": [
"Storage"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"url": {
"type": "string"
},
"fileName": {
"type": "string"
},
"mimetype": {
"type": "string"
},
"size": {
"type": "string"
}
}
}
}
}
}
},
"parameters": [
{
"schema": {
"type": "string"
},
"name": "path",
"in": "query",
"required": true
}
]
}
},
"/api/v1/db/meta/projects/{projectId}/users/{userId}/resend-invite": { "/api/v1/db/meta/projects/{projectId}/users/{userId}/resend-invite": {
"parameters": [ "parameters": [
{ {

Loading…
Cancel
Save