Browse Source

Merge branch 'develop' into i18n-interpolations

pull/1808/head
աɨռɢӄաօռɢ 3 years ago committed by GitHub
parent
commit
9b4f99ba76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      .all-contributorsrc
  2. 1
      README.md
  3. 5
      packages/nc-gui/components/ProjectTreeView.vue
  4. 2
      packages/nc-gui/components/auth/shareOrInviteModal.vue
  5. 2
      packages/nc-gui/components/auth/userManagement.vue
  6. 6
      packages/nc-gui/components/monaco/MonacoJsonObjectEditor.js
  7. 18
      packages/nc-gui/components/project/projectMetadata/disableOrEnableModels.vue
  8. 10
      packages/nc-gui/components/project/projectMetadata/sync/metaDiffSync.vue
  9. 17
      packages/nc-gui/components/project/spreadsheet/components/editableCell/jsonEditableCell.vue
  10. 4
      packages/nc-gui/components/project/spreadsheet/components/moreActions.vue
  11. 1
      packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue
  12. 2
      packages/nc-gui/components/project/spreadsheet/views/xcGridView.vue
  13. 2
      packages/nc-gui/components/settings/settingsModal.vue
  14. 2
      packages/nc-gui/config/vuetify.options.js
  15. 4
      packages/nc-gui/helpers/rolePermissionsEE.js
  16. 2
      packages/nc-gui/lang/en.json
  17. 2
      packages/nc-gui/lang/zh_TW.json
  18. 418
      packages/nc-gui/layouts/default.vue
  19. 17058
      packages/nc-gui/package-lock.json
  20. 2
      packages/nc-gui/package.json
  21. 12
      packages/nc-gui/pages/project/name.vue
  22. 24
      packages/nc-gui/pages/projects/index.vue
  23. 19
      packages/nc-gui/pages/user/password/forgot.vue
  24. 19
      packages/nc-gui/pages/user/settings/index.vue
  25. 32
      packages/noco-docs/content/en/developer-resources/accessing-apis.md
  26. 18
      packages/noco-docs/content/en/developer-resources/rest-apis.md
  27. 52
      packages/nocodb-sdk/src/lib/Api.ts
  28. 25179
      packages/nocodb/package-lock.json
  29. 2
      packages/nocodb/package.json
  30. 41
      packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSqlv2.ts
  31. 1
      packages/nocodb/src/lib/noco-models/Column.ts
  32. 40
      packages/nocodb/src/lib/noco/meta/api/dataApis/dataAliasApis.ts
  33. 12
      packages/nocodb/src/lib/noco/meta/api/dataApis/oldDataApis.ts
  34. 2
      packages/nocodb/src/lib/noco/meta/api/index.ts
  35. 48
      packages/nocodb/src/lib/noco/meta/api/swagger/helpers/getPaths.ts
  36. 46
      packages/nocodb/src/lib/noco/meta/api/swagger/helpers/getSchemas.ts
  37. 59
      packages/nocodb/src/lib/noco/meta/api/swagger/helpers/getSwaggerColumnMetas.ts
  38. 66
      packages/nocodb/src/lib/noco/meta/api/swagger/helpers/getSwaggerJSON.ts
  39. 61
      packages/nocodb/src/lib/noco/meta/api/swagger/helpers/swagger-base.json
  40. 10
      packages/nocodb/src/lib/noco/meta/api/swagger/helpers/templates/headers.ts
  41. 192
      packages/nocodb/src/lib/noco/meta/api/swagger/helpers/templates/params.ts
  42. 611
      packages/nocodb/src/lib/noco/meta/api/swagger/helpers/templates/paths.ts
  43. 85
      packages/nocodb/src/lib/noco/meta/api/swagger/helpers/templates/schemas.ts
  44. 24
      packages/nocodb/src/lib/noco/meta/api/swagger/redocHtml.ts
  45. 35
      packages/nocodb/src/lib/noco/meta/api/swagger/swaggerApis.ts
  46. 28
      packages/nocodb/src/lib/noco/meta/api/swagger/swaggerHtml.ts
  47. 5
      packages/nocodb/src/lib/noco/meta/api/userApi/ui/auth/emailVerify.ts
  48. 7
      packages/nocodb/src/lib/noco/meta/api/userApi/ui/auth/resetPassword.ts
  49. 119
      packages/nocodb/src/lib/noco/meta/api/userApi/ui/auth/signin.ts
  50. 135
      packages/nocodb/src/lib/noco/meta/api/userApi/ui/auth/signup.ts
  51. 425
      packages/nocodb/src/lib/noco/meta/api/userApi/ui/auth/swagger-base.xc.json
  52. 43
      packages/nocodb/src/lib/noco/meta/api/userApi/userApis.ts
  53. 19
      packages/nocodb/src/lib/noco/meta/api/utilApis.ts
  54. 1
      packages/nocodb/src/lib/sqlMgr/code/routers/xc-ts/SwaggerTypes.ts
  55. 3
      packages/nocodb/tsconfig.json
  56. 2
      scripts/cypress/integration/common/5a_user_role.js
  57. 2
      scripts/cypress/integration/common/5b_preview_role.js
  58. 5
      scripts/cypress/integration/spec/roleValidation.spec.js
  59. 162
      scripts/sdk/swagger.json

9
.all-contributorsrc

@ -747,6 +747,15 @@
"contributions": [
"code"
]
},
{
"login": "mudream4869",
"name": "神楽坂帕琪",
"avatar_url": "https://avatars.githubusercontent.com/u/6008539?v=4",
"profile": "http://blog.mukyu.tw/",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

1
README.md

@ -444,6 +444,7 @@ Our mission is to provide the most powerful no-code interface for databases whic
<td align="center"><a href="https://github.com/nilsreichardt"><img src="https://avatars.githubusercontent.com/u/24459435?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nils Reichardt</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=nilsreichardt" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/iamnamananand996"><img src="https://avatars.githubusercontent.com/u/31537362?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Naman Anand</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=iamnamananand996" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/GeoffMaciolek"><img src="https://avatars.githubusercontent.com/u/10995633?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Geo Maciolek</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=GeoffMaciolek" title="Code">💻</a></td>
<td align="center"><a href="http://blog.mukyu.tw/"><img src="https://avatars.githubusercontent.com/u/6008539?v=4?s=100" width="100px;" alt=""/><br /><sub><b>神楽坂帕琪</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=mudream4869" title="Code">💻</a></td>
</tr>
</table>

5
packages/nc-gui/components/ProjectTreeView.vue

@ -700,7 +700,7 @@
<div
v-t="['e:api-docs']"
class="caption pointer nc-docs pb-3 pl-5 pr-3 pt-2 d-flex align-center"
@click="openLink('https://apis.nocodb.com')"
@click="openLink(apiLink)"
>
<v-icon small class="mr-2">
mdi-api
@ -906,6 +906,9 @@ export default {
},
}),
computed: {
apiLink(){
return new URL(`/api/v1/db/meta/projects/${this.projectId}/swagger`, this.$store.state.project.projectInfo && this.$store.state.project.projectInfo.ncSiteUrl)
},
previewAs: {
get() {
return this.$store.state.users.previewAs;

2
packages/nc-gui/components/auth/shareOrInviteModal.vue

@ -206,7 +206,7 @@ export default {
const colors = this.$store.state.windows.darkTheme
? enumColor.dark
: enumColor.light;
return this.roles.reduce((o, r, i) => {
return ['owner'].concat(this.roles).reduce((o, r, i) => {
o[r] = colors[i % colors.length];
return o;
}, {});

2
packages/nc-gui/components/auth/userManagement.vue

@ -494,7 +494,7 @@ export default {
const colors = this.$store.state.windows.darkTheme
? enumColor.dark
: enumColor.light;
return this.roles.reduce((o, r, i) => {
return ['owner'].concat(this.roles).reduce((o, r, i) => {
o[r] = colors[i % colors.length];
return o;
}, {});

6
packages/nc-gui/components/monaco/MonacoJsonObjectEditor.js

@ -31,13 +31,13 @@ export default {
default: true
}
},
emits: ['validate'],
model: {
event: "change"
},
watch: {
value(newVal) {
if (!this.deepcompare(newVal, JSON.parse(this.editor.getValue())))
if (this.editor && !this.deepcompare(newVal, JSON.parse(this.editor.getValue())))
this.editor.setValue(JSON.stringify(newVal, 0, 2));
}
@ -109,7 +109,9 @@ export default {
if (!this.deepcompare(this.value, JSON.parse(value))) {
this.$emit("change", JSON.parse(value), event);
}
this.$emit("validate", true);
} catch (e) {
this.$emit("validate", false, e);
// console.log('monaco', e)
}
});

18
packages/nc-gui/components/project/projectMetadata/disableOrEnableModels.vue

@ -1,15 +1,15 @@
<template>
<div>
<v-tabs v-model="dbsTab" color="x-active" height="30">
<v-tab href="#xc-project-meta">
<v-icon icon x-small class="mr-2">
mdi-file-table-box-multiple-outline
</v-icon>
<span class="caption text-capitalize nc-exp-imp-metadata">
<!-- Export/Import Metadata -->
{{ $t('title.exportImportMeta') }}
</span>
</v-tab>
<!-- <v-tab href="#xc-project-meta">-->
<!-- <v-icon icon x-small class="mr-2">-->
<!-- mdi-file-table-box-multiple-outline-->
<!-- </v-icon>-->
<!-- <span class="caption text-capitalize nc-exp-imp-metadata">-->
<!-- &lt;!&ndash; Export/Import Metadata &ndash;&gt;-->
<!-- {{ $t('title.exportImportMeta') }}-->
<!-- </span>-->
<!-- </v-tab>-->
<v-tab-item value="xc-project-meta">
<div class="d-flex justify-center d-100">
<xc-meta />

10
packages/nc-gui/components/project/projectMetadata/sync/metaDiffSync.vue

@ -1,12 +1,12 @@
<template>
<v-container fluid>
<v-card>
<v-row>
<v-row class="pt-4">
<!-- <v-col cols="12">-->
<!-- <h4 class="text-center my-2 grey&#45;&#45;text text&#45;&#45;darken-2 title"> Metadata Management-->
<!-- </h4></v-col>-->
<v-col cols="8">
<v-card class="pb-2">
<v-card class="pb-2 pa">
<v-toolbar flat height="50" class="toolbar-border-bottom">
<v-text-field
v-if="dbAliasList && dbAliasList[dbsTab]"
@ -270,8 +270,8 @@
</div>
</v-col>
</v-row>
</v-card>
</v-container>
</v-container fluid>
</template>
<script>

17
packages/nc-gui/components/project/spreadsheet/components/editableCell/jsonEditableCell.vue

@ -10,7 +10,7 @@
<!-- Cancel -->
{{ $t('general.cancel') }}
</v-btn>
<v-btn x-small color="primary" @click="save">
<v-btn x-small color="primary" :disabled="!isValid" @click="save">
<!-- Save -->
{{ $t('general.save') }}
</v-btn>
@ -25,13 +25,18 @@
v-model="localState"
class="text-left caption"
style="width: 300px;min-height:min(600px,80vh);min-width:100%; "
@validate="validate"
/>
<monaco-json-object-editor
v-else
v-model="localState"
class="text-left caption"
style="width: 300px;min-height:200px;min-width:100%;"
@validate="validate"
/>
<div v-show="error" class="px-2 py-1 text-left caption error--text">
{{ error }}
</div>
</v-dialog>
</template>
@ -42,12 +47,14 @@ export default {
name: 'JsonEditableCell',
components: { MonacoJsonObjectEditor },
props: {
value: String,
value: [String, Object],
isForm: Boolean
},
data: () => ({
localState: '',
expand: false
expand: false,
isValid: true,
error: undefined
}),
computed: {
@ -91,6 +98,10 @@ export default {
save() {
this.expand = false
this.$emit('input', JSON.stringify(this.localState))
},
validate(n, e) {
this.isValid = n
this.error = e
}
}
}

4
packages/nc-gui/components/project/spreadsheet/components/moreActions.vue

@ -63,7 +63,7 @@
</v-list-item-title>
</v-list-item>
<v-list-item
v-if="_isUIAllowed('csvImport') && !isView"
v-if="_isUIAllowed('SharedViewList') && !isView"
v-t="['a:actions:shared-view-list']"
dense
@click="$emit('showAdditionalFeatOverlay', 'shared-views')"
@ -79,7 +79,7 @@
</v-list-item-title>
</v-list-item>
<v-list-item
v-if="_isUIAllowed('csvImport') && !isView"
v-if="_isUIAllowed('webhook') && !isView"
v-t="['c:actions:webhook']"
dense
@click="$emit('webhook')"

1
packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue

@ -1140,6 +1140,7 @@ export default {
// return if there is no change
if (
!column ||
saving ||
(oldRow[column.title] === rowObj[column.title] &&
(lastSave || rowObj[column.title]) === rowObj[column.title])
) {

2
packages/nc-gui/components/project/spreadsheet/views/xcGridView.vue

@ -245,7 +245,7 @@
:is-locked="isLocked"
:is-public="isPublicView"
:view-id="viewId"
@save="editEnabled = {}"
@save="editEnabled = {}; onCellValueChange(col, row, columnObj, true);"
@cancel="editEnabled = {}"
@update="onCellValueChange(col, row, columnObj, false)"
@blur="onCellValueChange(col, row, columnObj, true)"

2
packages/nc-gui/components/settings/settingsModal.vue

@ -16,7 +16,7 @@
left
permanent
height="90vh"
style="background-color: #f7f6f3"
class="backgroundColor1"
>
<div class=" advance-menu ">
<v-list

2
packages/nc-gui/config/vuetify.options.js

@ -21,6 +21,7 @@ export default function({ app }) {
text: '#ffffff',
textLight: '#b3b3b3',
backgroundColor: '#565656',
backgroundColor1: '#252525',
backgroundColorDefault: '#1f1f1f'
},
light: {
@ -31,6 +32,7 @@ export default function({ app }) {
text: '#333333',
textLight: '#929292',
backgroundColor: '#f7f7f7',
backgroundColor1: '#f7f6f3',
backgroundColorDefault: '#ffffff'
}
}

4
packages/nc-gui/helpers/rolePermissionsEE.js

@ -13,7 +13,8 @@ export default {
sortSync: true,
fieldsSync: true,
gridColUpdate: true,
filterSync: true
filterSync: true,
csvImport: true
},
commenter: {
smartSheet: true,
@ -34,6 +35,7 @@ export default {
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
* @author Wing-Kam Wong <wingkwong.code@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*

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

@ -159,7 +159,7 @@
"projMeta": "Project Metadata",
"metaMgmt": "Meta Management",
"metadata": "Metadata",
"exportImportMeta": "Export/ Import Metadata",
"exportImportMeta": "Export / Import Metadata",
"uiACL": "UI Access Control",
"metaOperations": "Metadata Operations",
"audit": "Audit",

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

@ -340,7 +340,7 @@
"testDbConn": "測試資料庫連線",
"removeDbFromEnv": "從環境移除資料庫",
"editConnJson": "編輯連線 JSON",
"sponsorUs": "贊助美國",
"sponsorUs": "贊助我們",
"sendEmail": "傳送電子郵件"
},
"tooltip": {

418
packages/nc-gui/layouts/default.vue

@ -26,25 +26,25 @@
</template>
<!-- Home -->
{{ $t("general.home") }}
<span class="caption font-weight-light pointer"
>(v{{
$store.state.project.projectInfo &&
<span
class="caption font-weight-light pointer"
>(v{{
$store.state.project.projectInfo &&
$store.state.project.projectInfo.version
}})</span
>
}})</span>
</v-tooltip>
<span class="body-1 ml-n1" @click="$router.push('/projects')">
{{ brandName }}</span
>
{{ brandName }}</span>
</v-toolbar-title>
<!-- <v-toolbar-items />-->
<!-- loading -->
<span v-show="$nuxt.$loading.show" class="caption grey--text ml-3"
>{{ $t("general.loading") }}
<v-icon small color="grey">mdi-spin mdi-loading</v-icon></span
>
<span
v-show="$nuxt.$loading.show"
class="caption grey--text ml-3"
>{{ $t("general.loading") }}
<v-icon small color="grey">mdi-spin mdi-loading</v-icon></span>
<span v-shortkey="['ctrl', 'shift', 'd']" @shortkey="openDiscord" />
</div>
@ -91,7 +91,9 @@
tooltip="Enable/Disable Models"
@click="cronTabAdd()"
>
<v-icon size="20"> mdi-timetable </v-icon> &nbsp; Crons
<v-icon size="20">
mdi-timetable
</v-icon> &nbsp; Crons
</x-btn>
</template>
<template v-else>
@ -126,7 +128,9 @@
to="/user/settings"
>
<v-list-item-title>
<v-icon small> mdi-at </v-icon>&nbsp;
<v-icon small>
mdi-at
</v-icon>&nbsp;
<span class="font-weight-bold caption">{{
userEmail
}}</span>
@ -147,30 +151,44 @@
"
>
<v-list-item-title>
<v-icon key="terminal-dash" small> mdi-content-copy </v-icon
>&nbsp;
<v-icon key="terminal-dash" small>
mdi-content-copy
</v-icon>&nbsp;
<span class="font-weight-regular caption">{{
$t("activity.account.authToken")
}}</span>
</v-list-item-title>
</v-list-item>
<v-list-item
v-if="swaggerOrGraphiqlUrl"
v-t="['a:navbar:user:swagger']"
dense
@click.stop="
openUrl(`${$axios.defaults.baseURL}${swaggerOrGraphiqlUrl}`)
openUrl(swaggerLink)
"
>
<v-list-item-title>
<v-icon key="terminal-dash" small>
{{ isGql ? "mdi-graphql" : "mdi-code-json" }} </v-icon
>&nbsp;
mdi-code-json
</v-icon>&nbsp;
<span class="font-weight-regular caption">
{{ isGql ? "GraphQL APIs" : "Swagger APIs Doc" }}</span
>
{{ "Swagger API Doc" }}</span>
</v-list-item-title>
</v-list-item>
<!-- <v-list-item
v-t="['a:navbar:user:redoc']"
dense
@click.stop="
openUrl(redocLink)
"
>
<v-list-item-title>
<v-icon key="terminal-dash" small>
mdi-code-json
</v-icon>&nbsp;
<span class="font-weight-regular caption">
{{ "Redoc API Doc" }}</span>
</v-list-item-title>
</v-list-item>-->
<v-divider />
<v-list-item
v-if="isDashboard"
@ -180,7 +198,9 @@
@click="copyProjectInfo"
>
<v-list-item-title>
<v-icon small> mdi-content-copy </v-icon>&nbsp;
<v-icon small>
mdi-content-copy
</v-icon>&nbsp;
<span class="font-weight-regular caption">{{
$t("activity.account.projInfo")
}}</span>
@ -194,8 +214,9 @@
@click.stop="settingsTabAdd"
>
<v-list-item-title>
<v-icon key="terminal-dash" small> mdi-palette </v-icon
>&nbsp;
<v-icon key="terminal-dash" small>
mdi-palette
</v-icon>&nbsp;
<span class="font-weight-regular caption">{{
$t("activity.account.themes")
}}</span>
@ -211,7 +232,9 @@
@click="MtdSignOut"
>
<v-list-item-title>
<v-icon small> mdi-logout </v-icon>&nbsp;
<v-icon small>
mdi-logout
</v-icon>&nbsp;
<span class="font-weight-regular caption">{{
$t("general.signOut")
}}</span>
@ -240,7 +263,9 @@
to="/user/authentication/signup"
>
<v-list-item-title>
<v-icon small> mdi-account-plus-outline </v-icon> &nbsp;
<v-icon small>
mdi-account-plus-outline
</v-icon> &nbsp;
<span class="font-weight-regular caption">{{
$t("general.signUp")
}}</span>
@ -252,7 +277,9 @@
to="/user/authentication/signin"
>
<v-list-item-title>
<v-icon small> mdi-login </v-icon> &nbsp;
<v-icon small>
mdi-login
</v-icon> &nbsp;
<span class="font-weight-regular caption">{{
$t("general.signIn")
}}</span>
@ -328,7 +355,7 @@ export default {
Snackbar,
dlgUnexpectedError,
settings,
ImportantAnnouncement,
ImportantAnnouncement
},
data: () => ({
clickCount: true,
@ -336,10 +363,10 @@ export default {
swaggerOrGraphiqlUrl: null,
showScreensaver: false,
roleIcon: {
owner: "mdi-account-star",
creator: "mdi-account-hard-hat",
editor: "mdi-account-edit",
viewer: "mdi-eye-outline",
owner: 'mdi-account-star',
creator: 'mdi-account-hard-hat',
editor: 'mdi-account-edit',
viewer: 'mdi-eye-outline'
},
showAppStore: false,
showChangeEnv: false,
@ -356,72 +383,79 @@ export default {
drawer: null,
fixed: false,
right: true,
title: "Xgene",
title: 'Xgene',
isHydrated: false,
snackbar: false,
timeout: 10000,
rolesList: null,
shareModal: false,
shareModal: false
}),
computed: {
swaggerLink() {
return new URL(`/api/v1/db/meta/projects/${this.projectId}/swagger`, this.$store.state.project.projectInfo && this.$store.state.project.projectInfo.ncSiteUrl)
},
redocLink() {
return new URL(`/api/v1/db/meta/projects/${this.projectId}/redoc`, this.$store.state.project.projectInfo && this.$store.state.project.projectInfo.ncSiteUrl)
},
...mapGetters({
logo: "plugins/brandLogo",
brandName: "plugins/brandName",
projects: "project/list",
tabs: "tabs/list",
sqldMgr: "sqlMgr/sqlMgr",
GetPendingStatus: "notification/GetPendingStatus",
isAuthenticated: "users/GtrIsAuthenticated",
isAdmin: "users/GtrIsAdmin",
isDocker: "project/GtrIsDocker",
isFirstLoad: "project/GtrIsFirstLoad",
isGql: "project/GtrProjectIsGraphql",
isRest: "project/GtrProjectIsRest",
isGrpc: "project/GtrProjectIsGrpc",
role: "users/GtrRole",
userEmail: "users/GtrUserEmail",
logo: 'plugins/brandLogo',
brandName: 'plugins/brandName',
projects: 'project/list',
tabs: 'tabs/list',
sqldMgr: 'sqlMgr/sqlMgr',
GetPendingStatus: 'notification/GetPendingStatus',
isAuthenticated: 'users/GtrIsAuthenticated',
isAdmin: 'users/GtrIsAdmin',
isDocker: 'project/GtrIsDocker',
isFirstLoad: 'project/GtrIsFirstLoad',
isGql: 'project/GtrProjectIsGraphql',
isRest: 'project/GtrProjectIsRest',
isGrpc: 'project/GtrProjectIsGrpc',
role: 'users/GtrRole',
userEmail: 'users/GtrUserEmail'
}),
user() {
return this.$store.getters["users/GtrUser"];
return this.$store.getters['users/GtrUser']
},
isThisMobile() {
// just an example, could be one specific value if that's all you need
return this.isHydrated ? this.$vuetify.breakpoint.smAndDown : false;
},
return this.isHydrated ? this.$vuetify.breakpoint.smAndDown : false
}
},
watch: {
"$route.path"(path, oldPath) {
'$route.path'(path, oldPath) {
try {
if (oldPath === path) {
return;
return
}
const recaptcha = this.$recaptchaInstance;
if (path.startsWith("/user/")) {
recaptcha.showBadge();
const recaptcha = this.$recaptchaInstance
if (path.startsWith('/user/')) {
recaptcha.showBadge()
} else {
recaptcha.hideBadge();
recaptcha.hideBadge()
}
} catch (e) {}
},
"$route.params.project_id"(newId, oldId) {
'$route.params.project_id'(newId, oldId) {
if (newId && newId !== oldId) {
this.loadProjectInfo();
this.loadProjectInfo()
}
if (!newId) {
this.swaggerOrGraphiqlUrl = null;
this.swaggerOrGraphiqlUrl = null
}
},
}
},
mounted() {
this.selectedEnv = this.$store.getters["project/GtrActiveEnv"];
this.loadProjectInfo();
this.selectedEnv = this.$store.getters['project/GtrActiveEnv']
this.loadProjectInfo()
},
methods: {
...mapActions({ changeActiveTab: "tabs/changeActiveTab" }),
...mapActions({ changeActiveTab: 'tabs/changeActiveTab' }),
...mapMutations({
toggleLogWindow: "windows/MutToggleLogWindow",
toggleOutputWindow: "windows/MutToggleOutputWindow",
toggleTreeviewWindow: "windows/MutToggleTreeviewWindow",
toggleLogWindow: 'windows/MutToggleLogWindow',
toggleOutputWindow: 'windows/MutToggleOutputWindow',
toggleTreeviewWindow: 'windows/MutToggleTreeviewWindow'
}),
async loadProjectInfo() {
// if (this.$route.params.project_id) {
@ -438,23 +472,23 @@ export default {
// }
},
setPreviewUSer(previewAs) {
this.previewAs = previewAs;
window.location.reload();
this.previewAs = previewAs
window.location.reload()
},
showAppStoreIcon() {
this.showAppStore = true;
this.$toast.info("Apps unlocked").goAway(5000);
this.showAppStore = true
this.$toast.info('Apps unlocked').goAway(5000)
},
isProjectInfoLoaded() {
return this.$store.state.project.projectInfo !== null;
return this.$store.state.project.projectInfo !== null
},
githubClickHandler(e) {
// e.preventDefault();
// shell.openExternal(e.path.find(e => e.href).href);
},
openUrl(url) {
window.open(url, "_blank");
window.open(url, '_blank')
},
openPricingPage() {
// shell.openExternal(process.env.serverUrl + '/pricing')
@ -469,253 +503,253 @@ export default {
// shell.openExternal('https://github.com/NocoDB/NocoDB')
},
dialogDebugCancel() {
this.dialogDebug = false;
this.dialogDebug = false
},
dialogDebugShow() {
this.dialogDebug = true;
this.dialogDebug = true
},
errorDialogCancel() {
this.dialogErrorShow = false;
this.dialogErrorShow = false
},
errorDialogReport() {
this.dialogErrorShow = false;
this.dialogErrorShow = false
},
loadChat() {
if (!window.Tawk_API) {
const s1 = document.createElement("script");
const s0 = document.getElementsByTagName("script")[0];
s1.async = true;
s1.src = "https://embed.tawk.to/5d81b8de9f6b7a4457e23ba7/default";
s1.charset = "UTF-8";
s1.setAttribute("crossorigin", "*");
s0.parentNode.insertBefore(s1, s0);
setTimeout(() => window.Tawk_API && window.Tawk_API.maximize(), 2000);
const s1 = document.createElement('script')
const s0 = document.getElementsByTagName('script')[0]
s1.async = true
s1.src = 'https://embed.tawk.to/5d81b8de9f6b7a4457e23ba7/default'
s1.charset = 'UTF-8'
s1.setAttribute('crossorigin', '*')
s0.parentNode.insertBefore(s1, s0)
setTimeout(() => window.Tawk_API && window.Tawk_API.maximize(), 2000)
} else {
window.Tawk_API.maximize();
window.Tawk_API.maximize()
}
},
handleMigrationsMenuClick(item, closeMenu = true, sqlEditor = false) {},
apiClientTabAdd() {
// if (this.$route.path.indexOf('dashboard') > -1) {
const tabIndex = this.tabs.findIndex((el) => el.key === "apiClientDir");
const tabIndex = this.tabs.findIndex(el => el.key === 'apiClientDir')
if (tabIndex !== -1) {
this.changeActiveTab(tabIndex);
this.changeActiveTab(tabIndex)
} else {
const item = { name: "API Client", key: "apiClientDir" };
item._nodes = { env: "_noco" };
item._nodes.type = "apiClientDir";
this.$store.dispatch("tabs/ActAddTab", item);
const item = { name: 'API Client', key: 'apiClientDir' }
item._nodes = { env: '_noco' }
item._nodes.type = 'apiClientDir'
this.$store.dispatch('tabs/ActAddTab', item)
}
},
apiClientSwaggerTabAdd() {
// if (this.$route.path.indexOf('dashboard') > -1) {
const tabIndex = this.tabs.findIndex(
(el) => el.key === "apiClientSwaggerDir"
);
el => el.key === 'apiClientSwaggerDir'
)
if (tabIndex !== -1) {
this.changeActiveTab(tabIndex);
this.changeActiveTab(tabIndex)
} else {
const item = { name: "API Client", key: "apiClientSwaggerDir" };
item._nodes = { env: "_noco" };
item._nodes.type = "apiClientSwaggerDir";
this.$store.dispatch("tabs/ActAddTab", item);
const item = { name: 'API Client', key: 'apiClientSwaggerDir' }
item._nodes = { env: '_noco' }
item._nodes.type = 'apiClientSwaggerDir'
this.$store.dispatch('tabs/ActAddTab', item)
}
},
projectInfoTabAdd() {
const tabIndex = this.tabs.findIndex((el) => el.key === "projectInfo");
const tabIndex = this.tabs.findIndex(el => el.key === 'projectInfo')
if (tabIndex !== -1) {
this.changeActiveTab(tabIndex);
this.changeActiveTab(tabIndex)
} else {
const item = { name: "Info", key: "projectInfo" };
item._nodes = { env: "_noco" };
item._nodes.type = "projectInfo";
this.$store.dispatch("tabs/ActAddTab", item);
const item = { name: 'Info', key: 'projectInfo' }
item._nodes = { env: '_noco' }
item._nodes.type = 'projectInfo'
this.$store.dispatch('tabs/ActAddTab', item)
}
},
xcMetaTabAdd() {
// if (this.$route.path.indexOf('dashboard') > -1) {
const tabIndex = this.tabs.findIndex((el) => el.key === "meta");
const tabIndex = this.tabs.findIndex(el => el.key === 'meta')
if (tabIndex !== -1) {
this.changeActiveTab(tabIndex);
this.changeActiveTab(tabIndex)
} else {
const item = { name: "Meta", key: "meta" };
item._nodes = { env: "_noco" };
item._nodes.type = "meta";
this.$store.dispatch("tabs/ActAddTab", item);
const item = { name: 'Meta', key: 'meta' }
item._nodes = { env: '_noco' }
item._nodes.type = 'meta'
this.$store.dispatch('tabs/ActAddTab', item)
}
},
apiClientSwaggerOpen() {
this.$router.push("/apiClient");
this.$router.push('/apiClient')
},
graphqlClientTabAdd() {
window.open(this.swaggerOrGraphiqlUrl, "_blank");
window.open(this.swaggerOrGraphiqlUrl, '_blank')
},
swaggerClientTabAdd() {
window.open(this.swaggerOrGraphiqlUrl, "_blank");
window.open(this.swaggerOrGraphiqlUrl, '_blank')
},
grpcTabAdd() {
const tabIndex = this.tabs.findIndex((el) => el.key === "grpcClient");
const tabIndex = this.tabs.findIndex(el => el.key === 'grpcClient')
if (tabIndex !== -1) {
this.changeActiveTab(tabIndex);
this.changeActiveTab(tabIndex)
} else {
const item = { name: "gRPC Client", key: "grpcClient" };
item._nodes = { env: "_noco" };
item._nodes.type = "grpcClient";
this.$store.dispatch("tabs/ActAddTab", item);
const item = { name: 'gRPC Client', key: 'grpcClient' }
item._nodes = { env: '_noco' }
item._nodes.type = 'grpcClient'
this.$store.dispatch('tabs/ActAddTab', item)
}
},
rolesTabAdd() {
const tabIndex = this.tabs.findIndex((el) => el.key === "roles");
const tabIndex = this.tabs.findIndex(el => el.key === 'roles')
if (tabIndex !== -1) {
this.changeActiveTab(tabIndex);
this.changeActiveTab(tabIndex)
} else {
const item = { name: "Team & Auth ", key: "roles" };
item._nodes = { env: "_noco" };
item._nodes.type = "roles";
this.$store.dispatch("tabs/ActAddTab", item);
const item = { name: 'Team & Auth ', key: 'roles' }
item._nodes = { env: '_noco' }
item._nodes.type = 'roles'
this.$store.dispatch('tabs/ActAddTab', item)
}
setTimeout(() => {
this.$eventBus.$emit("show-add-user");
}, 200);
this.$eventBus.$emit('show-add-user')
}, 200)
},
settingsTabAdd() {
const tabIndex = this.tabs.findIndex(
(el) => el.key === "projectSettings"
);
el => el.key === 'projectSettings'
)
if (tabIndex !== -1) {
this.changeActiveTab(tabIndex);
this.changeActiveTab(tabIndex)
} else {
const item = { name: "Themes", key: "projectSettings" };
item._nodes = { env: "_noco" };
item._nodes.type = "projectSettings";
this.$store.dispatch("tabs/ActAddTab", item);
const item = { name: 'Themes', key: 'projectSettings' }
item._nodes = { env: '_noco' }
item._nodes.type = 'projectSettings'
this.$store.dispatch('tabs/ActAddTab', item)
}
},
aclTabAdd() {
const tabIndex = this.tabs.findIndex((el) => el.key === "acl");
const tabIndex = this.tabs.findIndex(el => el.key === 'acl')
if (tabIndex !== -1) {
this.changeActiveTab(tabIndex);
this.changeActiveTab(tabIndex)
} else {
const item = { name: "ACL", key: "acl" };
item._nodes = { env: "_noco" };
item._nodes.type = "acl";
this.$store.dispatch("tabs/ActAddTab", item);
const item = { name: 'ACL', key: 'acl' }
item._nodes = { env: '_noco' }
item._nodes.type = 'acl'
this.$store.dispatch('tabs/ActAddTab', item)
}
},
disableOrEnableModelTabAdd() {
const tabIndex = this.tabs.findIndex(
(el) => el.key === "disableOrEnableModel"
);
el => el.key === 'disableOrEnableModel'
)
if (tabIndex !== -1) {
this.changeActiveTab(tabIndex);
this.changeActiveTab(tabIndex)
} else {
const item = { name: "Meta Management", key: "disableOrEnableModel" };
item._nodes = { env: "_noco" };
item._nodes.type = "disableOrEnableModel";
this.$store.dispatch("tabs/ActAddTab", item);
const item = { name: 'Meta Management', key: 'disableOrEnableModel' }
item._nodes = { env: '_noco' }
item._nodes.type = 'disableOrEnableModel'
this.$store.dispatch('tabs/ActAddTab', item)
}
},
cronTabAdd() {
const tabIndex = this.tabs.findIndex((el) => el.key === "cronJobs");
const tabIndex = this.tabs.findIndex(el => el.key === 'cronJobs')
if (tabIndex !== -1) {
this.changeActiveTab(tabIndex);
this.changeActiveTab(tabIndex)
} else {
const item = { name: "Cron Jobs", key: "cronJobs" };
item._nodes = { env: "_noco" };
item._nodes.type = "cronJobs";
this.$store.dispatch("tabs/ActAddTab", item);
const item = { name: 'Cron Jobs', key: 'cronJobs' }
item._nodes = { env: '_noco' }
item._nodes.type = 'cronJobs'
this.$store.dispatch('tabs/ActAddTab', item)
}
},
appsTabAdd() {
const tabIndex = this.tabs.findIndex((el) => el.key === "appStore");
const tabIndex = this.tabs.findIndex(el => el.key === 'appStore')
if (tabIndex !== -1) {
this.changeActiveTab(tabIndex);
this.changeActiveTab(tabIndex)
} else {
const item = { name: "App Store", key: "appStore" };
item._nodes = { env: "_noco" };
item._nodes.type = "appStore";
this.$store.dispatch("tabs/ActAddTab", item);
const item = { name: 'App Store', key: 'appStore' }
item._nodes = { env: '_noco' }
item._nodes.type = 'appStore'
this.$store.dispatch('tabs/ActAddTab', item)
}
},
async codeGenerateMvc() {
try {
await this.sqlMgr.projectGenerateBackend({
env: "_noco",
});
this.$toast.success("Yay, REST APIs with MVC generated").goAway(4000);
env: '_noco'
})
this.$toast.success('Yay, REST APIs with MVC generated').goAway(4000)
} catch (e) {
this.$toast.error("Error generating REST APIs code :" + e).goAway(4000);
throw e;
this.$toast.error('Error generating REST APIs code :' + e).goAway(4000)
throw e
}
},
cookieStatus(status) {
this.status = status;
this.status = status
},
cookieClickedAccept() {
this.status = "accept";
this.status = 'accept'
},
cookieClickedDecline() {
this.status = "decline";
this.status = 'decline'
// localStorage.removeItem('vue-cookie-accept-decline')
},
removeCookie() {
// console.log('Cookie removed')
localStorage.removeItem("vue-cookie-accept-decline");
this.status = "Cookie removed, refresh the page.";
localStorage.removeItem('vue-cookie-accept-decline')
this.status = 'Cookie removed, refresh the page.'
},
MtdContactUs() {
this.snackbar = true;
this.snackbar = true
},
MtdHiring() {
this.$router.push("/info/hiring");
this.$router.push('/info/hiring')
},
MtdFaq() {
this.$router.push("/info/faq");
this.$router.push('/info/faq')
},
MtdTos() {
this.$router.push("/info/tos");
this.$router.push('/info/tos')
},
async MtdSignOut() {
await this.$store.dispatch("users/ActSignOut");
this.$router.push("/user/authentication/signin");
await this.$store.dispatch('users/ActSignOut')
this.$router.push('/user/authentication/signin')
},
MtdToggleDrawer() {
if (!this.$store.getters["users/GtrUser"]) {
this.drawer = false;
if (!this.$store.getters['users/GtrUser']) {
this.drawer = false
} else {
this.drawer = !this.drawer;
this.drawer = !this.drawer
}
// console.log('Toggling drawer', this.drawer);
},
changeTheme() {
this.$store.dispatch(
"windows/ActToggleDarkMode",
'windows/ActToggleDarkMode',
!this.$store.state.windows.darkTheme
);
this.$e("c:navbar:theme");
)
this.$e('c:navbar:theme')
},
async copyProjectInfo() {
try {
const data = await this.$api.project.metaGet(
this.$store.state.project.projectId
);
)
copyTextToClipboard(
Object.entries(data)
.map(([k, v]) => `${k}: **${v}**`)
.join("\n")
);
this.$toast.info("Copied project info to clipboard").goAway(3000);
.join('\n')
)
this.$toast.info('Copied project info to clipboard').goAway(3000)
} catch (e) {
console.log(e);
this.$toast.error(e.message).goAway(3000);
console.log(e)
this.$toast.error(e.message).goAway(3000)
}
},
},
};
}
}
}
</script>
<style scoped>
a {

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

File diff suppressed because it is too large Load Diff

2
packages/nc-gui/package.json

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

12
packages/nc-gui/pages/project/name.vue

@ -1,9 +1,9 @@
<template>
<div>
<v-toolbar>
<v-spacer />
<v-spacer/>
Create New Project
<v-spacer />
<v-spacer/>
</v-toolbar>
<v-container>
<v-form ref="form" v-model="valid">
@ -69,8 +69,8 @@ export default {
loading: false,
projectType: 'rest',
projectTypes: [
{ text: 'Automatic REST APIs on database', value: 'rest', icon: 'mdi-code-json', iconColor: 'green' },
{ text: 'Automatic GRAPHQL APIs on database', value: 'graphql', icon: 'mdi-graphql', iconColor: 'pink' },
{text: 'Automatic REST APIs on database', value: 'rest', icon: 'mdi-code-json', iconColor: 'green'},
{text: 'Automatic GRAPHQL APIs on database', value: 'graphql', icon: 'mdi-graphql', iconColor: 'pink'},
{
text: 'Automatic gRPC APIs on database',
value: 'grpc',
@ -82,9 +82,9 @@ export default {
computed: {
typeIcon() {
if (this.projectType) {
return this.projectTypes.find(({ value }) => value === this.projectType)
return this.projectTypes.find(({value}) => value === this.projectType)
} else {
return { icon: 'mdi-server', iconColor: 'primary' }
return {icon: 'mdi-server', iconColor: 'primary'}
}
}
},

24
packages/nc-gui/pages/projects/index.vue

@ -52,17 +52,17 @@
</v-text-field>
<v-spacer />
<!-- Import NocoDB Project by uploading metadata zip file -->
<x-btn
vbind:tooltip="$t('msg.info.importText')"
outlined
color="grey"
@click="
$refs.importFile.click();
project_id = null;
"
>
<v-icon>mdi-import</v-icon>
</x-btn>
<!-- <x-btn-->
<!-- vbind:tooltip="$t('msg.info.importText')"-->
<!-- outlined-->
<!-- color="grey"-->
<!-- @click="-->
<!-- $refs.importFile.click();-->
<!-- project_id = null;-->
<!-- "-->
<!-- >-->
<!-- <v-icon>mdi-import</v-icon>-->
<!-- </x-btn>-->
<template v-if="connectToExternalDB">
<v-menu offset-y bottom open-on-hover>
@ -71,8 +71,8 @@
<x-btn
v-if="_isUIAllowed('projectCreate', true)"
v-ge="['home', 'project-new']"
outlined
data-v-step="1"
outlined
color="primary"
class="nc-new-project-menu"
v-on="on"

19
packages/nc-gui/pages/user/password/forgot.vue

@ -97,23 +97,26 @@ export default {
beforeDestroy() {
},
methods: {
onNormalVerify() {
this.recpatcha = true
},
async resetPasswordHandle(e) {
if (this.$refs.formType.validate()) {
e.preventDefault()
// await this.$recaptchaLoaded()
// const recaptchaToken = await this.$recaptcha('login')
const err = await this.$store.dispatch('users/ActPasswordForgot', { ...this.form })// recaptchaToken});
if (err) {
this.formUtil.formErr = true
this.formUtil.formErrMsg = err.data.msg
} else {
try {
await this.$api.auth.passwordForgot(
{
email: this.form.email
}
)
this.showMsg = true
} catch (e) {
const err = await this._extractSdkResponseErrorMsg(e)
this.formUtil.formErr = true
this.formUtil.formErrMsg = err
return;
}
}
}

19
packages/nc-gui/pages/user/settings/index.vue

@ -220,14 +220,20 @@ export default {
async resetUserPassword(e) {
e.preventDefault()
if (this.$refs.formType[0].validate()) {
// console.log('passworDetails',this.passwordDetails);
const err = await this.$store.dispatch('users/ActPostPasswordChange', this.passwordDetails)
if (err) {
this.formUtil.formErr = true
this.formUtil.formErrMsg = err.data.msg
} else {
try {
await this.$api.auth.passwordChange(
{
currentPassword: this.passwordDetails.currentPassword,
newPassword: this.passwordDetails.newPassword
}
)
this.$toast.success('Password changed successfully.').goAway(3000)
this.$refs.formType[0].reset()
} catch (e) {
this.$toast
.error(await this._extractSdkResponseErrorMsg(e))
.goAway(3000);
return;
}
}
},
@ -251,6 +257,7 @@ export default {
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
* @author Wing-Kam Wong <wingkwong.code@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*

32
packages/noco-docs/content/en/developer-resources/accessing-apis.md

@ -14,22 +14,7 @@ Auth Token is a JWT Token generated based on the logged-in user. By default, the
- Go to NocoDB Project, click the rightmost button and click ``Copy Auth Token``.
![image](https://user-images.githubusercontent.com/35857179/161957971-e4888983-25e1-46a4-8419-7b9fae6cb6fa.png)
- Click the same button and click ``Swagger APIs Doc``.
<!-- TODO: update screenshot -->
![image](https://user-images.githubusercontent.com/35857179/126187534-32c41de9-f17d-4f95-9acc-88aaed044b36.png)
- Select ``Schemes`` and Click ``Authorize``.
![image](https://user-images.githubusercontent.com/35857179/126188482-f3aacabf-dbc5-41a8-a190-9f225347ebd1.png)
- Paste the token you just copy in step 1 and click Authorize
![image](https://user-images.githubusercontent.com/35857179/126188510-b3790348-6809-4182-911a-a4031ace2fd2.png)
<img width="219" alt="image" src="https://user-images.githubusercontent.com/35857179/164874424-7622112f-9729-4514-81d2-5c6631b19ed0.png">
## API Token
@ -49,4 +34,17 @@ NocoDB allows creating API tokens which allow it to be integrated seamlessly wit
![image](https://user-images.githubusercontent.com/35857179/161958676-e4faa321-13ca-4b11-8d22-1332c522dde7.png)
- Copy API token to your clipboard
![image](https://user-images.githubusercontent.com/35857179/161958822-b0689a6a-a864-429f-8bb2-71eb92808339.png)
![image](https://user-images.githubusercontent.com/35857179/161958822-b0689a6a-a864-429f-8bb2-71eb92808339.png)
## Swagger UI
You can interact with the API's resources via Swagger UI.
- Go to NocoDB Project, click the rightmost button and click ``Swagger APIs Doc``.
<img width="215" alt="image" src="https://user-images.githubusercontent.com/35857179/164874429-d8e8f129-9cca-4d47-92c4-0b34b6e0b922.png">
- Click ``Authorize``, paste the token you copied in above steps and click `Authorize` to save.
![image](https://user-images.githubusercontent.com/35857179/164874471-29fc1630-ab99-4c25-8ce2-b41e5415e4be.png)

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

@ -9,11 +9,9 @@ Once you've created the schemas, you can manipulate the data or invoke actions u
## API Overview
Here's the overview of all APIs. For the details, please check out <a href="https://all-apis.nocodb.com/" target="_blank">NocoDB API Documentation</a>.
Here's the overview of all APIs. For the details, please check out <a href="https://all-apis.nocodb.com/" target="_blank">NocoDB API Documentation</a>.
<alert type="danger">
Swagger UI has been removed from GUI and may be supported in the future release.
</alert>
You may also interact with the API's resources via <a href="./accessing-apis#swagger-ui" target="_blank">Swagger UI</a>.
<alert type="success">
Currently, the default value for {orgs} is <b>noco</b>. Users will be able to change it in the future release.
@ -55,16 +53,18 @@ Currently, the default value for {orgs} is <b>noco</b>. Users will be able to ch
| Data | Patch | dbTableRow | bulkUpdateAll | /api/v1/db/data/bulk/{orgs}/{projectName}/{tableName}/all |
| Data | Delete| dbTableRow | bulkDeleteAll | /api/v1/db/data/bulk/{orgs}/{projectName}/{tableName}/all |
| Data | Get | dbTableRow | list | /api/v1/db/data/{orgs}/{projectName}/{tableName} |
| Data | Get | dbTableRow | findOne | /api/v1/db/data/{orgs}/{projectName}/{tableName}/find-one |
| Data | Post | dbTableRow | create | /api/v1/db/data/{orgs}/{projectName}/{tableName} |
| Data | Get | dbTableRow | read | /api/v1/db/data/{orgs}/{projectName}/{tableName}/{rowId} |
| Data | Patch | dbTableRow | update | /api/v1/db/data/{orgs}/{projectName}/{tableName}/{rowId} |
| Data | Delete| dbTableRow | delete | /api/v1/db/data/{orgs}/{projectName}/{tableName}/{rowId} |
| Data | Get | dbTableRow | count | /api/v1/db/data/{orgs}/{projectName}/{tableName}/count |
| Data | Get | dbViewRow | list | /api/v1/db/data/{orgs}/{projectName}/{tableName}/view/{viewName} |
| Data | Post | dbViewRow | create | /api/v1/db/data/{orgs}/{projectName}/{tableName}/view/{viewName} |
| Data | Get | dbViewRow | read | /api/v1/db/data/{orgs}/{projectName}/{tableName}/view/{viewName}/{rowId} |
| Data | Patch | dbViewRow | update | /api/v1/db/data/{orgs}/{projectName}/{tableName}/view/{viewName}/{rowId} |
| Data | Delete| dbViewRow | delete | /api/v1/db/data/{orgs}/{projectName}/{tableName}/view/{viewName}/{rowId} |
| Data | Get | dbViewRow | list | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName} |
| Data | Get | dbViewRow | findOne | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/find-one |
| Data | Post | dbViewRow | create | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName} |
| Data | Get | dbViewRow | read | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/{rowId} |
| Data | Patch | dbViewRow | update | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/{rowId} |
| Data | Delete| dbViewRow | delete | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/{rowId} |
| Data | Get | dbViewRow | count | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/count |
### Meta APIs

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

@ -1097,7 +1097,7 @@ export class Api<
* @request GET:/api/v1/db/meta/projects/{projectId}/info
* @response `200` `{ Node?: string, Arch?: string, Platform?: string, Docker?: boolean, Database?: string, ProjectOnRootDB?: string, RootDB?: string, PackageVersion?: string }` OK
*/
metaGet: (projectId: string, params: RequestParams = {}) =>
metaGet: (projectId: string, params: RequestParams = {}, query: object) =>
this.request<
{
Node?: string;
@ -1113,6 +1113,7 @@ export class Api<
>({
path: `/api/v1/db/meta/projects/${projectId}/info`,
method: 'GET',
query: query,
format: 'json',
...params,
}),
@ -2233,6 +2234,30 @@ export class Api<
...params,
}),
/**
* No description
*
* @tags DB table row
* @name FindOne
* @summary Table row FindOne
* @request GET:/api/v1/db/data/{orgs}/{projectName}/{tableName}/find-one
* @response `200` `any` OK
*/
findOne: (
orgs: string,
projectName: string,
tableName: string,
query?: { fields?: any[]; sort?: any[]; where?: string },
params: RequestParams = {}
) =>
this.request<any, any>({
path: `/api/v1/db/data/${orgs}/${projectName}/${tableName}/find-one`,
method: 'GET',
query: query,
format: 'json',
...params,
}),
/**
* No description
*
@ -2636,6 +2661,31 @@ export class Api<
...params,
}),
/**
* No description
*
* @tags DB view row
* @name FindOne
* @summary Table view row FindOne
* @request GET:/api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/find-one
* @response `200` `any` OK
*/
findOne: (
orgs: string,
projectName: string,
tableName: string,
viewName: string,
query?: { fields?: any[]; sort?: any[]; where?: string; nested?: any },
params: RequestParams = {}
) =>
this.request<any, any>({
path: `/api/v1/db/data/${orgs}/${projectName}/${tableName}/views/${viewName}/find-one`,
method: 'GET',
query: query,
format: 'json',
...params,
}),
/**
* No description
*

25179
packages/nocodb/package-lock.json generated

File diff suppressed because it is too large Load Diff

2
packages/nocodb/package.json

@ -154,7 +154,7 @@
"nc-lib-gui": "0.90.5",
"nc-plugin": "^0.1.1",
"ncp": "^2.0.0",
"nocodb-sdk": "0.90.5",
"nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",
"ora": "^4.0.4",
"os-locale": "^5.0.0",

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

@ -90,6 +90,45 @@ class BaseModelSqlv2 {
return data;
}
public async findOne(
args: {
where?: string;
filterArr?: Filter[];
} = {}
): Promise<any> {
const qb = this.dbDriver(this.model.table_name);
await this.selectObject({ qb });
const aliasColObjMap = await this.model.getAliasColObjMap();
const filterObj = extractFilterFromXwhere(args?.where, aliasColObjMap);
await conditionV2(
[
new Filter({
children: args.filterArr || [],
is_group: true,
logical_op: 'and'
}),
new Filter({
children: filterObj,
is_group: true,
logical_op: 'and'
}),
...(args.filterArr || [])
],
qb,
this.dbDriver
);
const data = await qb.first();
if (data) {
const proto = await this.getProto();
data.__proto__ = proto;
}
return data;
}
public async list(
args: {
where?: string;
@ -291,6 +330,8 @@ class BaseModelSqlv2 {
? allowedCols[col.id] &&
(!isSystemColumn(col) || view.show_system_fields) &&
(!fields?.length || fields.includes(col.title))
: fields?.length
? fields.includes(col.title)
: 1
}),
{}

1
packages/nocodb/src/lib/noco-models/Column.ts

@ -48,6 +48,7 @@ export default class Column<T = any> implements ColumnType {
public dtxp: string;
public dtxs: string;
public au: boolean;
public system: boolean;
public colOptions: T;
public model: Model;

40
packages/nocodb/src/lib/noco/meta/api/dataApis/dataAliasApis.ts

@ -13,6 +13,11 @@ async function dataList(req: Request, res: Response) {
res.json(await getDataList(model, view, req));
}
async function dataFindOne(req: Request, res: Response) {
const { model, view } = await getViewAndModelFromRequestByAliasOrId(req);
res.json(await getFindOne(model, view, req));
}
async function dataCount(req: Request, res: Response) {
const { model, view } = await getViewAndModelFromRequestByAliasOrId(req);
@ -106,6 +111,31 @@ async function getDataList(model, view: View, req) {
});
}
async function getFindOne(model, view: View, req) {
const base = await Base.get(model.base_id);
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view?.id,
dbDriver: NcConnectionMgrv2.get(base)
});
const args: any = { ...req.query };
try {
args.filterArr = JSON.parse(args.filterArrJson);
} catch (e) {}
try {
args.sortArr = JSON.parse(args.sortArrJson);
} catch (e) {}
return await nocoExecute(
await baseModel.defaultResolverReq(),
await baseModel.findOne(args),
{},
{}
);
}
async function dataRead(req: Request, res: Response) {
const { model, view } = await getViewAndModelFromRequestByAliasOrId(req);
@ -135,6 +165,11 @@ router.get(
ncMetaAclMw(dataList, 'dataList')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/find-one',
ncMetaAclMw(dataFindOne, 'dataFindOne')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/count',
ncMetaAclMw(dataCount, 'dataCount')
@ -168,6 +203,11 @@ router.get(
ncMetaAclMw(dataList, 'dataList')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/find-one',
ncMetaAclMw(dataFindOne, 'dataFindOne')
);
router.post(
'/api/v1/db/data/:orgs/:projectName/:tableName',
ncMetaAclMw(dataInsert, 'dataInsert')

12
packages/nocodb/src/lib/noco/meta/api/dataApis/oldDataApis.ts

@ -86,7 +86,7 @@ async function getDataList(model, view: View, req) {
});
}
async function getViewAndModelFromRequest(req) {
const project = await Project.get(req.params.projectId);
const project = await Project.getWithInfo(req.params.projectId);
const model = await Model.getByAliasOrId({
project_id: project.id,
base_id: project.bases?.[0]?.id,
@ -126,24 +126,24 @@ async function dataRead(req: Request, res: Response) {
const router = Router({ mergeParams: true });
router.get(
'/nc/:projectId/api/v2/:tableName',
'/nc/:projectId/api/v1/:tableName',
ncMetaAclMw(dataList, 'dataList')
);
router.post(
'/nc/:projectId/api/v2/:tableName',
'/nc/:projectId/api/v1/:tableName',
ncMetaAclMw(dataInsert, 'dataInsert')
);
router.get(
'/nc/:projectId/api/v2/:tableName/:rowId',
'/nc/:projectId/api/v1/:tableName/:rowId',
ncMetaAclMw(dataRead, 'dataRead')
);
router.patch(
'/nc/:projectId/api/v2/:tableName/:rowId',
'/nc/:projectId/api/v1/:tableName/:rowId',
ncMetaAclMw(dataUpdate, 'dataUpdate')
);
router.delete(
'/nc/:projectId/api/v2/:tableName/:rowId',
'/nc/:projectId/api/v1/:tableName/:rowId',
ncMetaAclMw(dataDelete, 'dataDelete')
);

2
packages/nocodb/src/lib/noco/meta/api/index.ts

@ -45,6 +45,7 @@ import { Server } from 'socket.io';
import passport from 'passport';
import crypto from 'crypto';
import swaggerApis from './swagger/swaggerApis';
export default function(router: Router, server) {
initStrategies(router);
@ -83,6 +84,7 @@ export default function(router: Router, server) {
router.use(cacheApis);
router.use(apiTokenApis);
router.use(hookFilterApis);
router.use(swaggerApis);
userApis(router);

48
packages/nocodb/src/lib/noco/meta/api/swagger/helpers/getPaths.ts

@ -0,0 +1,48 @@
import Noco from '../../../../Noco';
import Model from '../../../../../noco-models/Model';
import Project from '../../../../../noco-models/Project';
import { getModelPaths, getViewPaths } from './templates/paths';
import { SwaggerColumn } from './getSwaggerColumnMetas';
import { SwaggerView } from './getSwaggerJSON';
export default async function getPaths(
{
project,
model,
columns,
views
}: {
project: Project;
model: Model;
columns: SwaggerColumn[];
views: SwaggerView[];
},
_ncMeta = Noco.ncMeta
) {
const swaggerPaths = await getModelPaths({
tableName: model.title,
type: model.type,
orgs: 'noco',
columns,
projectName: project.title
});
for (const { view, columns: viewColumns } of views) {
const swaggerColumns = columns.filter(
c => viewColumns.find(vc => vc.fk_column_id === c.column.id)?.show
);
Object.assign(
swaggerPaths,
await getViewPaths({
tableName: model.title,
viewName: view.title,
type: model.type,
orgs: 'noco',
columns: swaggerColumns,
projectName: project.title
})
);
}
return swaggerPaths;
}

46
packages/nocodb/src/lib/noco/meta/api/swagger/helpers/getSchemas.ts

@ -0,0 +1,46 @@
import Noco from '../../../../Noco';
import Model from '../../../../../noco-models/Model';
import Project from '../../../../../noco-models/Project';
import { getModelSchemas, getViewSchemas } from './templates/schemas';
import { SwaggerColumn } from './getSwaggerColumnMetas';
import { SwaggerView } from './getSwaggerJSON';
export default async function getSchemas(
{
project,
model,
columns,
views
}: {
project: Project;
model: Model;
columns: SwaggerColumn[];
views: SwaggerView[];
},
_ncMeta = Noco.ncMeta
) {
const swaggerSchemas = getModelSchemas({
tableName: model.title,
orgs: 'noco',
projectName: project.title,
columns
});
for (const { view, columns: viewColumns } of views) {
const swaggerColumns = columns.filter(
c => viewColumns.find(vc => vc.fk_column_id === c.column.id)?.show
);
Object.assign(
swaggerSchemas,
getViewSchemas({
tableName: model.title,
viewName: view.title,
orgs: 'noco',
columns: swaggerColumns,
projectName: project.title
})
);
}
return swaggerSchemas;
}

59
packages/nocodb/src/lib/noco/meta/api/swagger/helpers/getSwaggerColumnMetas.ts

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

66
packages/nocodb/src/lib/noco/meta/api/swagger/helpers/getSwaggerJSON.ts

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

61
packages/nocodb/src/lib/noco/meta/api/swagger/helpers/swagger-base.json

@ -0,0 +1,61 @@
{
"openapi": "3.0.0",
"info": {
"title": "nocodb",
"version": "1.0"
},
"servers": [
{
"url": "http://localhost:8080"
}
],
"paths": {
},
"components": {
"schemas": {
"Paginated": {
"title": "Paginated",
"type": "object",
"properties": {
"pageSize": {
"type": "integer"
},
"totalRows": {
"type": "integer"
},
"isFirstPage": {
"type": "boolean"
},
"isLastPage": {
"type": "boolean"
},
"page": {
"type": "number"
}
}
}
},
"securitySchemes": {
"xcAuth": {
"type": "apiKey",
"in": "header",
"name": "xc-auth",
"description": "JWT access token"
},
"xcToken": {
"type": "apiKey",
"in": "header",
"name": "xc-token",
"description": "API token"
}
}
},
"security": [
{
"xcAuth": []
},
{
"xcToken": []
}
]
}

10
packages/nocodb/src/lib/noco/meta/api/swagger/helpers/templates/headers.ts

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

192
packages/nocodb/src/lib/noco/meta/api/swagger/helpers/templates/params.ts

@ -0,0 +1,192 @@
import { SwaggerColumn } from '../getSwaggerColumnMetas';
import { RelationTypes, UITypes } from 'nocodb-sdk';
import LinkToAnotherRecordColumn from '../../../../../../noco-models/LinkToAnotherRecordColumn';
export const rowIdParam = {
schema: {
type: 'string'
},
name: 'rowId',
in: 'path',
required: true,
example: 1,
description:
'Primary key of the record you want to read. If the table have composite primary key then combine them by using `___` and pass it as primary key.'
};
export const relationTypeParam = {
schema: {
type: 'string',
enum: ['mm', 'hm']
},
name: 'relationType',
in: 'path',
required: true
};
export const fieldsParam = {
schema: {
type: 'string'
},
in: 'query',
name: 'fields',
description:
'Array of field names or comma separated filed names to include in the response objects. In array syntax pass it like `fields[]=field1&fields[]=field2` or alternately `fields=field1,field2`.'
};
export const sortParam = {
schema: {
type: 'string'
},
in: 'query',
name: 'sort',
description:
'Comma separated field names to sort rows, rows will sort in ascending order based on provided columns. To sort in descending order provide `-` prefix along with column name, like `-field`. Example : `sort=field1,-field2`'
};
export const whereParam = {
schema: {
type: 'string'
},
in: 'query',
name: 'where',
description:
'This can be used for filtering rows, which accepts complicated where conditions. For more info visit [here](https://docs.nocodb.com/developer-resources/rest-apis#comparison-operators). Example : `where=(field1,eq,value)`'
};
export const limitParam = {
schema: {
type: 'number',
minimum: 1
},
in: 'query',
name: 'limit',
description:
'The `limit` parameter used for pagination, the response collection size depends on limit value and default value is `25`.',
example: 25
};
export const offsetParam = {
schema: {
type: 'number',
minimum: 0
},
in: 'query',
name: 'offset',
description:
'The `offset` parameter used for pagination, the value helps to select collection from a certain index.',
example: 0
};
export const columnNameParam = (columns: SwaggerColumn[]) => {
const columnNames = [];
for (const { column } of columns) {
if (column.uidt !== UITypes.LinkToAnotherRecord || column.system) continue;
columnNames.push(column.title);
}
return {
schema: {
type: 'enum',
enum: columnNames
},
name: 'columnName',
in: 'path',
required: true
};
};
export const referencedRowIdParam = {
schema: {
type: 'string'
},
name: 'refRowId',
in: 'path',
required: true
};
export const exportTypeParam = {
schema: {
type: 'string',
enum: ['csv', 'excel']
},
name: 'type',
in: 'path',
required: true
};
export const csvExportOffsetParam = {
schema: {
type: 'number',
minimum: 0
},
in: 'query',
name: 'offset',
description:
'Helps to start export from a certain index. You can get the next set of data offset from previous response header named `nc-export-offset`.',
example: 0
};
export const nestedWhereParam = colName => ({
schema: {
type: 'string'
},
in: 'query',
name: `nested[${colName}][where]`,
description: `This can be used for filtering rows in nested column \`${colName}\`, which accepts complicated where conditions. For more info visit [here](https://docs.nocodb.com/developer-resources/rest-apis#comparison-operators). Example : \`nested[${colName}][where]=(field1,eq,value)\``
});
export const nestedFieldParam = colName => ({
schema: {
type: 'string'
},
in: 'query',
name: `nested[${colName}][fields]`,
description: `Array of field names or comma separated filed names to include in the in nested column \`${colName}\` result. In array syntax pass it like \`fields[]=field1&fields[]=field2.\`. Example : \`nested[${colName}][fields]=field1,field2\``
});
export const nestedSortParam = colName => ({
schema: {
type: 'string'
},
in: 'query',
name: `nested[${colName}][sort]`,
description: `Comma separated field names to sort rows in nested column \`${colName}\` rows, it will sort in ascending order based on provided columns. To sort in descending order provide \`-\` prefix along with column name, like \`-field\`. Example : \`nested[${colName}][sort]=field1,-field2\``
});
export const nestedLimitParam = colName => ({
schema: {
type: 'number',
minimum: 1
},
in: 'query',
name: `nested[${colName}][limit]`,
description: `The \`limit\` parameter used for pagination of nested \`${colName}\` rows, the response collection size depends on limit value and default value is \`25\`.`,
example: '25'
});
export const nestedOffsetParam = colName => ({
schema: {
type: 'number',
minimum: 0
},
in: 'query',
name: `nested[${colName}][offset]`,
description: `The \`offset\` parameter used for pagination of nested \`${colName}\` rows, the value helps to select collection from a certain index.`,
example: 0
});
export const getNestedParams = async (
columns: SwaggerColumn[]
): Promise<any[]> => {
return await columns.reduce(async (paramsArr, { column }) => {
if (column.uidt === UITypes.LinkToAnotherRecord) {
const colOpt = await column.getColOptions<LinkToAnotherRecordColumn>();
if (colOpt.type !== RelationTypes.BELONGS_TO) {
return [
...(await paramsArr),
nestedWhereParam(column.title),
nestedOffsetParam(column.title),
nestedLimitParam(column.title),
nestedFieldParam(column.title),
nestedSortParam(column.title)
];
}
}
return paramsArr;
}, Promise.resolve([]));
};

611
packages/nocodb/src/lib/noco/meta/api/swagger/helpers/templates/paths.ts

@ -0,0 +1,611 @@
import { ModelTypes, UITypes } from 'nocodb-sdk';
import {
columnNameParam,
csvExportOffsetParam,
exportTypeParam,
fieldsParam,
getNestedParams,
limitParam,
offsetParam,
referencedRowIdParam,
relationTypeParam,
rowIdParam,
sortParam,
whereParam
} from './params';
import { csvExportResponseHeader } from './headers';
import { SwaggerColumn } from '../getSwaggerColumnMetas';
export const getModelPaths = async (ctx: {
tableName: string;
orgs: string;
type: ModelTypes;
columns: SwaggerColumn[];
projectName: string;
}): Promise<{ [path: string]: any }> => ({
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}`]: {
get: {
summary: `${ctx.tableName} list`,
operationId: 'db-table-row-list',
description: `List of all rows from ${ctx.tableName} ${ctx.type} and response data fields can be filtered based on query params.`,
tags: [ctx.tableName],
parameters: [
fieldsParam,
sortParam,
whereParam,
limitParam,
offsetParam,
...(await getNestedParams(ctx.columns))
],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: getPaginatedResponseType(`${ctx.tableName}Response`)
}
}
}
}
},
...(ctx.type === ModelTypes.TABLE
? {
post: {
summary: `${ctx.tableName} create`,
description:
'Insert a new row in table by providing a key value pair object where key refers to the column alias. All the required fields should be included with payload excluding `autoincrement` and column with default value.',
operationId: `${ctx.tableName.toLowerCase()}-create`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}Response`
}
}
}
}
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}Request`
}
}
}
}
}
}
: {})
},
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/{rowId}`]: {
parameters: [rowIdParam],
...(ctx.type === ModelTypes.TABLE
? {
get: {
parameters: [fieldsParam],
summary: `${ctx.tableName} read`,
description:
'Read a row data by using the **primary key** column value.',
operationId: `${ctx.tableName.toLowerCase()}-read`,
responses: {
'201': {
description: 'Created',
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}Response`
}
}
}
}
},
tags: [ctx.tableName]
},
patch: {
summary: `${ctx.tableName} update`,
operationId: `${ctx.tableName.toLowerCase()}-update`,
description:
'Partial update row in table by providing a key value pair object where key refers to the column alias. You need to only include columns which you want to update.',
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}Request`
}
}
}
}
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {}
}
}
}
},
delete: {
summary: `${ctx.tableName} delete`,
operationId: `${ctx.tableName.toLowerCase()}-delete`,
responses: {
'200': {
description: 'OK'
}
},
tags: [ctx.tableName],
description:
'Delete a row by using the **primary key** column value.'
}
}
: {})
},
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/count`]: {
get: {
summary: `${ctx.tableName} count`,
operationId: `${ctx.tableName.toLowerCase()}-count`,
description: 'Get rows count of a table by applying optional filters.',
tags: [ctx.tableName],
parameters: [whereParam],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {}
}
}
}
}
}
},
...(ctx.type === ModelTypes.TABLE
? {
[`/api/v1/db/data/bulk/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}`]: {
post: {
summary: `${ctx.tableName} bulk insert`,
description:
"To insert large amount of data in a single api call you can use this api. It's similar to insert method but here you can pass array of objects to insert into table. Array object will be key value paired column name and value.",
operationId: `${ctx.tableName.toLowerCase()}-bulk-create`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {}
}
}
}
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {}
}
}
}
},
patch: {
summary: `${ctx.tableName} bulk update`,
description:
"To update multiple records using it's primary key you can use this api. Bulk updated api accepts array object in which each object should contain it's primary columns value mapped to corresponding alias. In addition to primary key you can include the fields which you want to update",
operationId: `${ctx.tableName.toLowerCase()}-bulk-update`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {}
}
}
}
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {}
}
}
}
},
delete: {
summary: `${ctx.tableName} bulk delete by IDs`,
description:
"To delete multiple records using it's primary key you can use this api. Bulk delete api accepts array object in which each object should contain it's primary columns value mapped to corresponding alias.",
operationId: `${ctx.tableName.toLowerCase()}-bulk-delete`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {}
}
}
}
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {}
}
}
}
}
},
[`/api/v1/db/data/bulk/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/all`]: {
parameters: [whereParam],
patch: {
summary: `${ctx.tableName} Bulk update with conditions`,
description:
"This api helps you update multiple table rows in a single api call. You don't have to pass the record id instead you can filter records and apply the changes to filtered records. Payload is similar as normal update in which you can pass any partial row data to be updated.",
operationId: `${ctx.tableName.toLowerCase()}-bulk-update-all`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {}
}
}
}
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {}
}
}
}
},
delete: {
summary: 'Bulk delete with conditions',
description:
"This api helps you delete multiple table rows in a single api call. You don't have to pass the record id instead you can filter records and delete filtered records.",
operationId: `${ctx.tableName.toLowerCase()}-bulk-delete-all`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {}
}
}
}
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {}
}
}
}
}
},
...(isRelationExist(ctx.columns)
? {
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/{rowId}/{relationType}/{columnName}`]: {
parameters: [
rowIdParam,
relationTypeParam,
columnNameParam(ctx.columns)
],
get: {
summary: 'Relation row list',
operationId: `${ctx.tableName.toLowerCase()}-nested-list`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {}
}
}
}
},
tags: [ctx.tableName],
parameters: [limitParam, offsetParam]
}
},
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/{rowId}/{relationType}/{columnName}/{refRowId}`]: {
parameters: [
rowIdParam,
relationTypeParam,
columnNameParam(ctx.columns),
referencedRowIdParam
],
post: {
summary: 'Relation row add',
operationId: `${ctx.tableName.toLowerCase()}-nested-add`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {}
}
}
}
},
tags: [ctx.tableName],
parameters: [limitParam, offsetParam],
description: ''
},
delete: {
summary: 'Relation row remove',
operationId: `${ctx.tableName.toLowerCase()}-nested-remove`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {}
}
}
}
},
tags: [ctx.tableName]
}
},
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/{rowId}/{relationType}/{columnName}/exclude`]: {
parameters: [
rowIdParam,
relationTypeParam,
columnNameParam(ctx.columns)
],
get: {
summary:
'Referenced tables rows excluding current records children/parent',
operationId: `${ctx.tableName.toLowerCase()}-nested-children-excluded-list`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {}
}
}
}
},
tags: [ctx.tableName],
parameters: [limitParam, offsetParam]
}
}
}
: {})
}
: {}),
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/export/{type}`]: {
parameters: [exportTypeParam],
get: {
summary: 'Rows export',
operationId: `${ctx.tableName.toLowerCase()}-csv-export`,
description:
'Export all the records from a table.Currently we are only supports `csv` export.',
tags: [ctx.tableName],
wrapped: true,
responses: {
'200': {
description: 'OK',
content: {
'application/octet-stream': {
schema: {}
}
},
headers: csvExportResponseHeader
}
},
parameters: [csvExportOffsetParam]
}
}
});
export const getViewPaths = async (ctx: {
tableName: string;
viewName: string;
type: ModelTypes;
orgs: string;
projectName: string;
columns: SwaggerColumn[];
}): Promise<any> => ({
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/views/${ctx.viewName}`]: {
get: {
summary: `${ctx.viewName} list`,
operationId: `${ctx.tableName}-${ctx.viewName}-row-list`,
description: `List of all rows from ${ctx.viewName} grid view and data of fields can be filtered based on query params. Data and fields in a grid view will be filtered and sorted by default based on the applied options in Dashboard.`,
tags: [`${ctx.viewName} ( ${ctx.tableName} grid )`],
parameters: [
fieldsParam,
sortParam,
whereParam,
...(await getNestedParams(ctx.columns))
],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: getPaginatedResponseType(
`${ctx.tableName}${ctx.viewName}GridResponse`
)
}
}
}
}
},
...(ctx.type === ModelTypes.TABLE
? {
post: {
summary: `${ctx.viewName} create`,
description:
'Insert a new row in table by providing a key value pair object where key refers to the column alias. All the required fields should be included with payload excluding `autoincrement` and column with default value.',
operationId: `${ctx.tableName}-${ctx.viewName}-row-create`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {}
}
}
}
},
tags: [`${ctx.viewName} ( ${ctx.tableName} grid )`],
requestBody: {
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}${ctx.viewName}GridRequest`
}
}
}
}
}
}
: {})
},
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/views/${ctx.viewName}/count`]: {
get: {
summary: `${ctx.viewName} count`,
operationId: `${ctx.tableName}-${ctx.viewName}-row-count`,
description: '',
tags: [`${ctx.viewName} ( ${ctx.tableName} grid )`],
parameters: [whereParam],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
count: 'number'
}
}
}
}
}
}
}
},
...(ctx.type === ModelTypes.TABLE
? {
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/views/${ctx.viewName}/{rowId}`]: {
parameters: [rowIdParam],
get: {
summary: `${ctx.viewName} read`,
description:
'Read a row data by using the **primary key** column value.',
operationId: `${ctx.tableName}-${ctx.viewName}-row-read`,
responses: {
'200': {
description: 'Created',
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}${ctx.viewName}GridResponse`
}
}
}
}
},
tags: [`${ctx.viewName} ( ${ctx.tableName} grid )`]
},
patch: {
summary: `${ctx.viewName} update`,
description:
'Partial update row in table by providing a key value pair object where key refers to the column alias. You need to only include columns which you want to update.',
operationId: `${ctx.tableName}-${ctx.viewName}-row-update`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {}
}
}
}
},
tags: [`${ctx.viewName} ( ${ctx.tableName} grid )`],
requestBody: {
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}${ctx.viewName}GridRequest`
}
}
}
}
},
delete: {
summary: `${ctx.viewName} delete`,
operationId: `${ctx.tableName}-${ctx.viewName}-row-delete`,
responses: {
'200': {
description: 'OK'
}
},
tags: [`${ctx.viewName} ( ${ctx.tableName} grid )`],
description:
'Delete a row by using the **primary key** column value.'
}
}
}
: {}),
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/views/${ctx.viewName}/export/{type}`]: {
parameters: [exportTypeParam],
get: {
summary: `${ctx.viewName} export`,
operationId: `${ctx.tableName}-${ctx.viewName}-row-export`,
description:
'Export all the records from a table view. Currently we are only supports `csv` export.',
tags: [`${ctx.viewName} ( ${ctx.tableName} grid )`],
wrapped: true,
responses: {
'200': {
description: 'OK',
content: {
'application/octet-stream': {
schema: {}
}
},
headers: csvExportResponseHeader
}
},
parameters: []
}
}
});
function getPaginatedResponseType(type: string) {
return {
type: 'object',
properties: {
list: {
type: 'array',
items: {
$ref: `#/components/schemas/${type}`
}
},
PageInfo: {
$ref: `#/components/schemas/Paginated`
}
}
};
}
function isRelationExist(columns: SwaggerColumn[]) {
return columns.some(
c => c.column.uidt === UITypes.LinkToAnotherRecord && !c.column.system
);
}

85
packages/nocodb/src/lib/noco/meta/api/swagger/helpers/templates/schemas.ts

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

24
packages/nocodb/src/lib/noco/meta/api/swagger/redocHtml.ts

@ -0,0 +1,24 @@
export default `<!DOCTYPE html>
<html>
<head>
<title>NocoDB API Documentation</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<!--
Redoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url='./swagger.json'></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"> </script>
</body>
</html>`;

35
packages/nocodb/src/lib/noco/meta/api/swagger/swaggerApis.ts

@ -0,0 +1,35 @@
// @ts-ignore
import catchError from '../../helpers/catchError';
import { Router } from 'express';
import Model from '../../../../noco-models/Model';
import getSwaggerJSON from './helpers/getSwaggerJSON';
import Project from '../../../../noco-models/Project';
import swaggerHtml from './swaggerHtml';
import redocHtml from './redocHtml';
async function swaggerJson(req, res) {
const project = await Project.get(req.params.projectId);
const models = await Model.list({
project_id: req.params.project_id,
base_id: null
});
const swagger = await getSwaggerJSON(project, models);
res.json(swagger);
}
const router = Router({ mergeParams: true });
// todo: auth
router.get(
'/api/v1/db/meta/projects/:projectId/swagger.json',
catchError(swaggerJson)
);
router.get('/api/v1/db/meta/projects/:projectId/swagger', (_req, res) =>
res.send(swaggerHtml)
);
router.get('/api/v1/db/meta/projects/:projectId/redoc', (_req, res) =>
res.send(redocHtml)
);
export default router;

28
packages/nocodb/src/lib/noco/meta/api/userApi/ui/auth/swagger.ts → packages/nocodb/src/lib/noco/meta/api/swagger/swaggerHtml.ts

File diff suppressed because one or more lines are too long

5
packages/nocodb/src/lib/noco/meta/api/userApi/ui/auth/emailVerify.ts

@ -7,7 +7,7 @@ export default `<!DOCTYPE html>
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
<script src="https://unpkg.com/vue"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.14/vue.min.js" integrity="sha512-XdUZ5nrNkVySQBnnM5vzDqHai823Spoq1W3pJoQwomQja+o4Nw0Ew1ppxo5bhF2vMug6sfibhKWcNJsG8Vj9tg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>
<div id="app">
@ -54,7 +54,7 @@ export default `<!DOCTYPE html>
methods: {},
async created() {
try {
const valid = (await axios.post('<%- baseUrl %>auth/email/validate/' + this.token)).data;
const valid = (await axios.post('<%- baseUrl %>/api/v1/db/auth/email/validate/' + this.token)).data;
this.valid = !!valid;
} catch (e) {
this.valid = false;
@ -74,6 +74,7 @@ export default `<!DOCTYPE html>
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
* @author Wing-Kam Wong <wingkwong.code@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*

7
packages/nocodb/src/lib/noco/meta/api/userApi/ui/auth/resetPassword.ts

@ -7,7 +7,7 @@ export default `<!DOCTYPE html>
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
<script src="https://unpkg.com/vue"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.14/vue.min.js" integrity="sha512-XdUZ5nrNkVySQBnnM5vzDqHai823Spoq1W3pJoQwomQja+o4Nw0Ew1ppxo5bhF2vMug6sfibhKWcNJsG8Vj9tg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>
<div id="app">
@ -81,7 +81,7 @@ export default `<!DOCTYPE html>
async resetPassword() {
if (this.$refs.form.validate()) {
try {
const res = await axios.post('<%- baseUrl %>auth/password/reset/' + this.token, {
const res = await axios.post('<%- baseUrl %>api/v1/db/auth/password/reset/' + this.token, {
...this.formdata
});
this.success = true;
@ -93,7 +93,7 @@ export default `<!DOCTYPE html>
},
async created() {
try {
const valid = (await axios.post('<%- baseUrl %>auth/token/validate/' + this.token)).data;
const valid = (await axios.post('<%- baseUrl %>api/v1/db/auth/token/validate/' + this.token)).data;
this.valid = !!valid;
} catch (e) {
this.valid = false;
@ -108,6 +108,7 @@ export default `<!DOCTYPE html>
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
* @author Wing-Kam Wong <wingkwong.code@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*

119
packages/nocodb/src/lib/noco/meta/api/userApi/ui/auth/signin.ts

@ -1,119 +0,0 @@
export default `<!DOCTYPE html>
<html>
<head>
<title>NocoDB - Sign In</title>
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
<script src="https://unpkg.com/vue"></script>
</head>
<body>
<div id="app">
<v-app>
<v-container>
<v-row class="justify-center">
<v-col class="col-12 col-md-6">
<v-form ref="form" v-model="validForm" ref="formType" class="ma-auto"
lazy-validation>
<v-text-field
name="input-10-2"
label="email"
type="email"
v-model="formdata.email"
:rules="[v => !!v || 'Email is required']"
></v-text-field>
<v-text-field
name="input-10-2"
type="password"
label="Password"
v-model="formdata.password"
:rules="[v => !!v || 'Password is required']"
></v-text-field>
<v-btn
:disabled="!validForm"
large
@click="signin"
>
Sign In
</v-btn>
</v-form>
<br>
<pre style="overflow: auto" v-if="success" v-html="success"></pre>
<v-alert v-else-if="errMsg" type="error">
{{errMsg}}
</v-alert>
</v-col>
</v-row>
</v-container>
</v-app>
</div>
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"></script>
<script>
var app = new Vue({
el: '#app',
vuetify: new Vuetify(),
data: {
valid: null,
validForm: false,
greeting: 'Password Reset',
formdata: {
password: '',
newPassword: ''
},
success: false,
errMsg: null
},
methods: {
async signin() {
if (this.$refs.form.validate()) {
try {
const res = await axios.post('<%- baseUrl %>auth/signin', this.formdata);
this.success = res.data;
} catch (e) {
if (e.response && e.response.data && e.response.data.msg) {
this.errMsg = e.response.data.msg;
} else {
this.errMsg = 'Some error occurred';
}
}
}
}
}
})
</script>
</body>
</html>`;
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @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/>.
*
*/

135
packages/nocodb/src/lib/noco/meta/api/userApi/ui/auth/signup.ts

@ -1,135 +0,0 @@
export default `<!DOCTYPE html>
<html>
<head>
<title>NocoDB - Sign Up</title>
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
<script src="https://unpkg.com/vue"></script>
</head>
<body>
<div id="app">
<v-app>
<v-container>
<v-row class="justify-center">
<v-col class="col-12 col-md-6">
<v-form ref="form" v-model="validForm" ref="formType" class="ma-auto"
lazy-validation>
<v-text-field
name="input-10-2"
label="First Name"
type="text"
v-model="formdata.firstname"
:rules="[v => !!v || 'First Name is required']"
></v-text-field>
<v-text-field
name="input-10-2"
label="Last Name"
type="text"
v-model="formdata.lastname"
:rules="[v => !!v || 'Last Name is required']"
></v-text-field>
<v-text-field
name="input-10-2"
label="email"
type="email"
v-model="formdata.email"
:rules="[v => !!v || 'Email is required']"
></v-text-field>
<v-text-field
name="input-10-2"
type="password"
label="Password"
v-model="formdata.password"
:rules="[v => !!v || 'Password is required']"
></v-text-field>
<v-btn
:disabled="!validForm"
large
@click="signin"
>
Sign Up
</v-btn>
</v-form>
<br>
<pre style="overflow: auto" v-if="success" v-html="success"></pre>
<v-alert v-else-if="errMsg" type="error">
{{errMsg}}
</v-alert>
</v-col>
</v-row>
</v-container>
</v-app>
</div>
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"></script>
<script>
var app = new Vue({
el: '#app',
vuetify: new Vuetify(),
data: {
valid: null,
validForm: false,
greeting: 'Password Reset',
formdata: {
password: '',
newPassword: ''
},
success: false,
errMsg: null
},
methods: {
async signin() {
if (this.$refs.form.validate()) {
try {
const res = await axios.post('<%- baseUrl %>auth/signup', this.formdata);
this.success = res.data;
} catch (e) {
if (e.response && e.response.data && e.response.data.msg) {
this.errMsg = e.response.data.msg;
} else {
this.errMsg = 'Some error occurred';
}
}
}
}
}
})
</script>
</body>
</html>`;
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @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/>.
*
*/

425
packages/nocodb/src/lib/noco/meta/api/userApi/ui/auth/swagger-base.xc.json

@ -1,425 +0,0 @@
{
"swagger": "2.0",
"info": {
"description": "Create APIs at the speed of your thoughts",
"version": "1.0.0",
"title": "NocoDB",
"contact": {}
},
"host": "localhost:8080",
"basePath": "/",
"tags": [
{
"name": "common"
}
],
"schemes": [
"http"
],
"paths": {
"/auth/signin": {
"post": {
"security": [
],
"tags": [
"Authentication"
],
"summary": "User login",
"description": "",
"operationId": "login",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"parameters": [
{
"in": "body",
"name": "body",
"description": "Authentication user details",
"required": true,
"schema": {
"$ref": "#/definitions/userAuth"
}
}
],
"responses": {
"200": {
"description": "Authenticated successfully",
"schema": {
"$ref": "#/definitions/token"
}
}
}
}
},
"/auth/signup": {
"post": {
"tags": [
"Authentication"
],
"summary": "User signup",
"description": "",
"operationId": "signup",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"parameters": [
{
"in": "body",
"name": "body",
"description": "Signup user details",
"required": true,
"schema": {
"$ref": "#/definitions/user"
}
}
],
"responses": {
"200": {
"description": "Registration success",
"schema": {
"$ref": "#/definitions/token"
}
},
"400": {
"description": "Bad request"
}
}
}
},
"/auth/password/forgot": {
"post": {
"tags": [
"Authentication"
],
"summary": "Password Forgot",
"description": "",
"operationId": "passwordForgot",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"parameters": [
{
"in": "body",
"name": "body",
"description": "Email address",
"required": true,
"schema": {
"type": "object",
"properties": {
"email": {
"type": "string",
"required": true,
"example": "test@nocodb.com"
}
}
}
}
],
"responses": {
"200": {
"description": "Mailed password reset link",
"schema": {
"type": "boolean"
}
}
}
}
},
"/auth/email/validate/{tokenId}": {
"post": {
"tags": [
"Authentication"
],
"summary": "Email validate link",
"description": "",
"operationId": "emailValidate",
"produces": [
"application/json"
],
"parameters": [
{
"name": "tokenId",
"in": "path",
"description": "random token id received",
"required": true,
"type": "string",
"format": "uuid"
}
],
"responses": {
"200": {
"description": "Validated successfully"
}
}
}
},
"/auth/token/validate/{tokenId}": {
"get": {
"tags": [
"Authentication"
],
"summary": "Validate password reset token",
"description": "",
"operationId": "passwordResetTokenValidate",
"produces": [
"application/json"
],
"parameters": [
{
"name": "tokenId",
"in": "path",
"description": "random token id received",
"required": true,
"type": "string",
"format": "uuid"
}
],
"responses": {
"200": {
"description": "Validated successfully"
}
}
}
},
"/auth/password/reset/": {
"post": {
"tags": [
"Authentication"
],
"summary": "Password reset",
"description": "",
"operationId": "passwordReset",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"parameters": [
{
"name": "tokenId",
"in": "path",
"description": "random token id received",
"required": true,
"type": "string",
"format": "uuid"
},
{
"in": "body",
"name": "body",
"description": "Reset password details",
"required": true,
"schema": {
"type": "object",
"properties": {
"password": {
"type": "string",
"format": "password",
"example": "password",
"required": true
}
}
}
}
],
"responses": {
"200": {
"description": "Password reset successfully"
}
}
}
},
"/user/me": {
"get": {
"tags": [
"Authentication"
],
"summary": "User details",
"description": "",
"operationId": "userDetails",
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "User details"
}
}
}
},
"/user": {
"put": {
"tags": [
"Authentication"
],
"summary": "Update user details",
"description": "",
"operationId": "updateUserDetails",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "User details"
}
},
"parameters": [
{
"in": "body",
"name": "body",
"description": "Updated user details",
"required": true,
"schema": {
"$ref": "#/definitions/user"
}
}
]
}
},
"/user/password/change": {
"post": {
"tags": [
"Authentication"
],
"summary": "Update user details",
"description": "",
"operationId": "passwordChange",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "User details"
}
},
"parameters": [
{
"in": "body",
"name": "body",
"description": "Current password and new password",
"required": true,
"schema": {
"type": "object",
"properties": {
"currentPassword": {
"type": "string",
"format": "password",
"example": "password"
},
"newPassword": {
"type": "string",
"format": "password",
"example": "newPassword"
}
}
}
}
]
}
}
},
"definitions": {
"userAuth": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"nullable": false,
"example": "test@nocodb.com"
},
"password": {
"type": "string",
"format": "password",
"nullable": false,
"example": "password"
}
}
},
"token": {
"type": "object",
"properties": {
"token": {
"type": "string",
"format": "email",
"nullable": false,
"example": "< JWT Token >"
}
}
},
"user": {
"allOf": [
{
"$ref": "#/definitions/userAuth"
},
{
"type": "object",
"properties": {
"id": {
"type": "integer",
"nullable": false,
"readOnly": true
},
"firstname": {
"type": "string",
"nullable": false,
"example": "FirstName"
},
"lastname": {
"type": "string",
"nullable": false,
"example": "LastName"
},
"roles": {
"type": "object",
"readOnly": true
},
"created_at": {
"type": "string",
"readOnly": true
},
"updated_at": {
"type": "string",
"readOnly": true
},
"email_verified": {
"type": "boolean",
"readOnly": true
}
}
}
]
}
},
"security": [
{
"xcAuth": []
}
],
"externalDocs": {
"description": "Find out more about NocoDB",
"url": "http://nocodb.com"
},
"securityDefinitions": {
"xcAuth": {
"type": "apiKey",
"in": "header",
"name": "xc-auth"
}
}
}

43
packages/nocodb/src/lib/noco/meta/api/userApi/userApis.ts

@ -269,8 +269,8 @@ async function passwordForgot(req: Request<any, any>, res): Promise<any> {
}
const email = _email.toLowerCase();
const user = await User.getByEmail(email);
if (user) {
const token = uuidv4();
await User.update(user.id, {
@ -286,15 +286,16 @@ async function passwordForgot(req: Request<any, any>, res): Promise<any> {
subject: 'Password Reset Link',
text: `Visit following link to update your password : ${
(req as any).ncSiteUrl
}/password/reset/${token}.`,
}/api/v1/db/auth/password/reset/${token}.`,
html: ejs.render(template, {
resetLink: (req as any).ncSiteUrl + `/password/reset/${token}`
resetLink: (req as any).ncSiteUrl + `/api/v1/db/auth/password/reset/${token}`
})
})
);
} catch (e) {
console.log(
'Warning : `mailSend` failed, Please configure emailClient configuration.'
console.log(e)
return NcError.badRequest(
'Email Plugin is not found. Please contact administrators to configure it in App Store first.'
);
}
@ -305,12 +306,16 @@ async function passwordForgot(req: Request<any, any>, res): Promise<any> {
description: `requested for password reset `,
ip: (req as any).clientIp
});
} else {
return NcError.badRequest(
'Your email has not been registered.'
);
}
res.json({ msg: 'Check your email if you are registered with us.' });
res.json({ msg: 'Please check your email to reset the password' });
}
async function tokenValidate(req, res): Promise<any> {
const token = req.params.token;
const token = req.params.tokenId;
const user = await Noco.ncMeta.metaGet(null, null, MetaTable.USERS, {
reset_password_token: token
@ -326,7 +331,7 @@ async function tokenValidate(req, res): Promise<any> {
}
async function passwordReset(req, res): Promise<any> {
const token = req.params.token;
const token = req.params.tokenId;
const user = await Noco.ncMeta.metaGet(null, null, MetaTable.USERS, {
reset_password_token: token
@ -364,7 +369,7 @@ async function passwordReset(req, res): Promise<any> {
}
async function emailVerification(req, res): Promise<any> {
const token = req.params.token;
const token = req.params.tokenId;
const user = await Noco.ncMeta.metaGet(null, null, MetaTable.USERS, {
email_verification_token: token
@ -428,6 +433,19 @@ async function refreshToken(req, res): Promise<any> {
}
}
async function renderPasswordReset(req, res): Promise<any> {
try {
res.send(
ejs.render((await import('./ui/auth/resetPassword')).default, {
token: JSON.stringify(req.params.tokenId),
baseUrl: `/`
})
);
} catch (e) {
return res.status(400).json({ msg: e.message });
}
}
const mapRoutes = router => {
// todo: old api - /auth/signup?tool=1
router.post('/auth/user/signup', catchError(signup));
@ -443,6 +461,7 @@ const mapRoutes = router => {
);
router.post('/auth/token/refresh', ncMetaAclMw(refreshToken, 'refreshToken'));
// new API
router.post('/api/v1/db/auth/user/signup', catchError(signup));
router.post('/api/v1/db/auth/user/signin', catchError(signin));
router.get(
@ -464,12 +483,16 @@ const mapRoutes = router => {
catchError(emailVerification)
);
router.post(
'/user/password/change',
'/api/v1/db/auth/password/change',
ncMetaAclMw(passwordChange, 'passwordChange')
);
router.post(
'/api/v1/db/auth/token/refresh',
ncMetaAclMw(refreshToken, 'refreshToken')
);
router.get(
'/api/v1/db/auth/password/reset/:tokenId',
catchError(renderPasswordReset)
);
};
export { mapRoutes as userApis };

19
packages/nocodb/src/lib/noco/meta/api/utilApis.ts

@ -12,7 +12,7 @@ import axios from 'axios';
export async function testConnection(req: Request, res: Response) {
res.json(await SqlMgrv2.testConnection(req.body));
}
export async function appInfo(_req: Request, res: Response) {
export async function appInfo(req: Request, res: Response) {
const projectHasAdmin = !(await User.isFirst());
const result = {
authType: 'jwt',
@ -38,17 +38,24 @@ export async function appInfo(_req: Request, res: Response) {
),
timezone: defaultConnectionConfig.timezone,
ncMin: !!process.env.NC_MIN,
teleEnabled: !process.env.NC_DISABLE_TELE
teleEnabled: !process.env.NC_DISABLE_TELE,
ncSiteUrl: (req as any).ncSiteUrl
};
res.json(result);
}
export async function releaseVersion(_req: Request, res: Response) {
const result = await axios.get('https://github.com/nocodb/nocodb/releases/latest')
.then((response) => {
return { releaseVersion: response.request.res.responseUrl.replace('https://github.com/nocodb/nocodb/releases/tag/', '') }
})
const result = await axios
.get('https://github.com/nocodb/nocodb/releases/latest')
.then(response => {
return {
releaseVersion: response.request.res.responseUrl.replace(
'https://github.com/nocodb/nocodb/releases/tag/',
''
)
};
});
res.json(result);
}

1
packages/nocodb/src/lib/sqlMgr/code/routers/xc-ts/SwaggerTypes.ts

@ -2,6 +2,7 @@ class SwaggerTypes {
static setSwaggerType(column, field, dbType = 'mysql') {
switch (dbType) {
case 'mysql':
case 'mysql2':
case 'mariadb':
SwaggerTypes.setSwaggerTypeForMysql(column, field);
break;

3
packages/nocodb/tsconfig.json

@ -45,8 +45,7 @@
// "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
"lib": [
"es2017",
"dom"
"es2017"
],
"types": [
"node"

2
scripts/cypress/integration/common/5a_user_role.js

@ -64,7 +64,7 @@ export const genTest = (apiType, dbType) => {
// open Project metadata tab
//
mainPage.navigationDraw(mainPage.PROJ_METADATA).click();
cy.get(".nc-exp-imp-metadata").dblclick({ force: true });
// cy.get(".nc-exp-imp-metadata").dblclick({ force: true });
cy.get(".nc-ui-acl-tab").click({ force: true });
cy.snip("Meta_Tab3");

2
scripts/cypress/integration/common/5b_preview_role.js

@ -58,7 +58,7 @@ export const genTest = (apiType, dbType, roleType) => {
// open Project metadata tab
//
mainPage.navigationDraw(mainPage.PROJ_METADATA).click();
cy.get(".nc-exp-imp-metadata").dblclick({ force: true });
// cy.get(".nc-exp-imp-metadata").dblclick({ force: true });
cy.get(".nc-ui-acl-tab").click({ force: true });
// validate if it has 19 entries representing tables & views

5
scripts/cypress/integration/spec/roleValidation.spec.js

@ -215,6 +215,7 @@ export function _editComment(roleType, previewMode) {
export function _viewMenu(roleType, previewMode) {
let columnName = "City";
let navDrawListCnt = 1;
// Download CSV
let actionsMenuItemsCnt = 1;
cy.openTableTab(columnName, 25);
@ -231,7 +232,11 @@ export function _viewMenu(roleType, previewMode) {
// Owner, Creator will have two navigation drawer (on each side of center panel)
if (roleType == "owner" || roleType == "creator") {
navDrawListCnt = 2;
// Download CSV / Upload CSV / Shared View List / Webhook
actionsMenuItemsCnt = 4;
} else if (roleType == "editor") {
// Download CSV / Upload CSV
actionsMenuItemsCnt = 2;
}
cy.get(".v-navigation-drawer__content")

162
scripts/sdk/swagger.json

@ -573,6 +573,16 @@
},
"tags": [
"Project"
],
"parameters": [
{
"schema": {
"type": "number",
"minimum": 1,
"multipleOf": 1
},
"in": "query"
}
]
}
},
@ -2521,6 +2531,75 @@
}
}
},
"/api/v1/db/data/{orgs}/{projectName}/{tableName}/find-one": {
"parameters": [
{
"schema": {
"type": "string"
},
"name": "orgs",
"in": "path",
"required": true
},
{
"schema": {
"type": "string"
},
"name": "projectName",
"in": "path",
"required": true
},
{
"schema": {
"type": "string"
},
"name": "tableName",
"in": "path",
"required": true
}
],
"get": {
"summary": "Table row FindOne",
"operationId": "db-table-row-find-one",
"description": "",
"tags": [
"DB table row"
],
"parameters": [
{
"schema": {
"type": "array"
},
"in": "query",
"name": "fields"
},
{
"schema": {
"type": "array"
},
"in": "query",
"name": "sort"
},
{
"schema": {
"type": "string"
},
"in": "query",
"name": "where"
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/api/v1/db/data/{orgs}/{projectName}/{tableName}/count": {
"parameters": [
{
@ -2689,6 +2768,89 @@
}
}
},
"/api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/find-one": {
"parameters": [
{
"schema": {
"type": "string"
},
"name": "orgs",
"in": "path",
"required": true
},
{
"schema": {
"type": "string"
},
"name": "projectName",
"in": "path",
"required": true
},
{
"schema": {
"type": "string"
},
"name": "tableName",
"in": "path",
"required": true
},
{
"schema": {
"type": "string"
},
"name": "viewName",
"in": "path",
"required": true
}
],
"get": {
"summary": "Table view row FindOne",
"operationId": "db-view-row-find-one",
"description": "",
"tags": [
"DB view row"
],
"parameters": [
{
"schema": {
"type": "array"
},
"in": "query",
"name": "fields"
},
{
"schema": {
"type": "array"
},
"in": "query",
"name": "sort"
},
{
"schema": {
"type": "string"
},
"in": "query",
"name": "where"
},
{
"schema": {},
"in": "query",
"name": "nested",
"description": "Query params for nested data"
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/count": {
"parameters": [
{

Loading…
Cancel
Save