Browse Source

Feat/1769 swagger (#1834)

* wip: swagger endpoint for all data apis in project

Signed-off-by: Pranav C <pranavxc@gmail.com>

* wip: swagger endpoint for all data apis in project

Signed-off-by: Pranav C <pranavxc@gmail.com>

* wip(swagger): add description, sample and improve

Signed-off-by: Pranav C <pranavxc@gmail.com>

* feat: add nested column params

Signed-off-by: Pranav C <pranavxc@gmail.com>

* feat: add auth header in swagger.json

Signed-off-by: Pranav C <pranavxc@gmail.com>

* feat: populate enum type for columnnames

Signed-off-by: Pranav C <pranavxc@gmail.com>

* feat: swagger apis for views

Signed-off-by: Pranav C <pranavxc@gmail.com>

* feat: add swagger and redoc url in GUI

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: limit fields in model api based on fields query parameter

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: project settings navdrawer background color correction

re #1793

Signed-off-by: Pranav C <pranavxc@gmail.com>

* fix: update swagger server url and update swagger icon

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/1840/head
Pranav C 2 years ago committed by GitHub
parent
commit
417dec65dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 99
      packages/nc-gui/components/ProjectTreeView.vue
  2. 2
      packages/nc-gui/components/settings/settingsModal.vue
  3. 2
      packages/nc-gui/config/vuetify.options.js
  4. 418
      packages/nc-gui/layouts/default.vue
  5. 17058
      packages/nc-gui/package-lock.json
  6. 26017
      packages/nocodb/package-lock.json
  7. 1
      packages/nocodb/package.json
  8. 2
      packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSqlv2.ts
  9. 1
      packages/nocodb/src/lib/noco-models/Column.ts
  10. 2
      packages/nocodb/src/lib/noco/meta/api/index.ts
  11. 48
      packages/nocodb/src/lib/noco/meta/api/swagger/helpers/getPaths.ts
  12. 46
      packages/nocodb/src/lib/noco/meta/api/swagger/helpers/getSchemas.ts
  13. 59
      packages/nocodb/src/lib/noco/meta/api/swagger/helpers/getSwaggerColumnMetas.ts
  14. 66
      packages/nocodb/src/lib/noco/meta/api/swagger/helpers/getSwaggerJSON.ts
  15. 61
      packages/nocodb/src/lib/noco/meta/api/swagger/helpers/swagger-base.json
  16. 10
      packages/nocodb/src/lib/noco/meta/api/swagger/helpers/templates/headers.ts
  17. 192
      packages/nocodb/src/lib/noco/meta/api/swagger/helpers/templates/params.ts
  18. 611
      packages/nocodb/src/lib/noco/meta/api/swagger/helpers/templates/paths.ts
  19. 85
      packages/nocodb/src/lib/noco/meta/api/swagger/helpers/templates/schemas.ts
  20. 24
      packages/nocodb/src/lib/noco/meta/api/swagger/redocHtml.ts
  21. 73
      packages/nocodb/src/lib/noco/meta/api/swagger/swaggerApis.ts
  22. 26
      packages/nocodb/src/lib/noco/meta/api/swagger/swaggerHtml.ts
  23. 19
      packages/nocodb/src/lib/noco/meta/api/utilApis.ts
  24. 1
      packages/nocodb/src/lib/sqlMgr/code/routers/xc-ts/SwaggerTypes.ts
  25. 3
      packages/nocodb/tsconfig.json

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

@ -10,7 +10,7 @@
class="primary nc-project-title theme--dark"
:class="{ shared: sharedBase }"
>
<img v-if="sharedBase" src="favicon-32.png" height="18" class="ml-2" />
<img v-if="sharedBase" src="favicon-32.png" height="18" class="ml-2">
<h3
v-if="sharedBase"
class="nc-project-title white--text text-capitalize"
@ -43,7 +43,9 @@
class="elevation-0 mr-2 pl-3 pr-1 caption nc-table-list-filter"
>
<template #prepend-inner>
<v-icon small class="mt-2 ml-2 mr-1"> mdi-magnify </v-icon>
<v-icon small class="mt-2 ml-2 mr-1">
mdi-magnify
</v-icon>
</template>
<template #append>
<v-icon
@ -91,7 +93,9 @@
@click.stop="addTab({ ...item }, open, leaf)"
>
<template v-if="item._nodes.type === 'db'">
<v-icon size="16"> mdi-database </v-icon>
<v-icon size="16">
mdi-database
</v-icon>
<!-- <img-->
<!-- class="grey lighten-3"-->
<!-- :width="16" :src="`/db-icons/${dbIcons[item._nodes.dbConnection.client]}`"/>-->
@ -120,8 +124,7 @@
icons[item._nodes.type].class,
item.active ? 'font-weight-bold' : '',
]"
>{{ item.name }}</span
>
>{{ item.name }}</span>
</div>
</template>
<span>{{ item.tooltip || item.name }}</span>
@ -149,7 +152,9 @@
@contextmenu.prevent="showCTXMenu($event, item, true, false)"
>
<template #appendIcon>
<v-icon small color="grey"> mdi-chevron-down </v-icon>
<v-icon small color="grey">
mdi-chevron-down
</v-icon>
</template>
<template #activator>
<v-list-item-icon>
@ -190,16 +195,13 @@
.toLowerCase()
.includes(search.toLowerCase())
).length
}})</template
></span
>
}})</template></span>
<span
v-else
class="body-2 font-weight-medium"
v-on="on"
>
{{ item.name }}</span
>
{{ item.name }}</span>
</template>
<span class="caption">Only visible to Creator</span>
</v-tooltip>
@ -220,12 +222,9 @@
.toLowerCase()
.includes(search.toLowerCase())
).length
}})</template
></span
>
}})</template></span>
<span v-else class="caption font-weight-regular">
{{ item.name }}</span
>
{{ item.name }}</span>
</template>
</v-list-item-title>
@ -236,7 +235,7 @@
<x-icon
v-if="
_isUIAllowed('treeview-add-button') &&
item.type !== 'viewDir'
item.type !== 'viewDir'
"
:color="['x-active', 'grey']"
small
@ -248,12 +247,12 @@
mdi-plus-circle-outline
</x-icon>
</template>
<span class="caption"
>Add new
<span
class="caption"
>Add new
<span class="text-capitalize">{{
item.type.slice(0, -3)
}}</span></span
>
}}</span></span>
</v-tooltip>
</template>
@ -277,9 +276,9 @@
v-for="child in item.children || []"
v-show="
!search ||
child.name
.toLowerCase()
.includes(search.toLowerCase())
child.name
.toLowerCase()
.includes(search.toLowerCase())
"
:key="child.key"
v-t="['a:table:open']"
@ -327,7 +326,7 @@
<v-tooltip
v-if="
_isUIAllowed('creator_tooltip') &&
child.creator_tooltip
child.creator_tooltip
"
bottom
>
@ -354,7 +353,7 @@
<v-icon
v-if="
_isUIAllowed('treeview-rename-button') ||
_isUIAllowed('ui-acl')
_isUIAllowed('ui-acl')
"
small
v-on="on"
@ -422,7 +421,7 @@
<v-list-item
v-else-if="
(item.type !== 'sqlClientDir' || showSqlClient) &&
(item.type !== 'migrationsDir' || _isUIAllowed('audit'))
(item.type !== 'migrationsDir' || _isUIAllowed('audit'))
"
:key="item.key"
:selectable="false"
@ -458,8 +457,7 @@
class="caption font-weight-regular"
v-on="on"
@dblclick="showSqlClient = true"
>{{ item.name }}</span
>
>{{ item.name }}</span>
</template>
<span class="caption">Only visible to Creator</span>
</v-tooltip>
@ -467,8 +465,7 @@
v-else
class="caption font-weight-regular"
@dblclick="showSqlClient = true"
>{{ item.name }}</span
>
>{{ item.name }}</span>
</v-list-item-title>
</v-list-item>
</template>
@ -528,7 +525,9 @@
v-on="on"
>
<v-list-item-icon>
<v-icon x-small> mdi-storefront-outline </v-icon>
<v-icon x-small>
mdi-storefront-outline
</v-icon>
</v-list-item-icon>
<!-- App Store -->
<v-list-item-title>
@ -552,7 +551,9 @@
v-on="on"
>
<v-list-item-icon>
<v-icon x-small> mdi-account-group </v-icon>
<v-icon x-small>
mdi-account-group
</v-icon>
</v-list-item-icon>
<!-- Team & Auth -->
<v-list-item-title>
@ -575,7 +576,9 @@
v-on="on"
>
<v-list-item-icon>
<v-icon x-small> mdi-table-multiple </v-icon>
<v-icon x-small>
mdi-table-multiple
</v-icon>
</v-list-item-icon>
<!-- Project Metadata -->
<v-list-item-title>
@ -599,7 +602,9 @@
v-on="on"
>
<v-list-item-icon>
<v-icon x-small> mdi-notebook-outline </v-icon>
<v-icon x-small>
mdi-notebook-outline
</v-icon>
</v-list-item-icon>
<!-- Project Metadata -->
<v-list-item-title>
@ -622,7 +627,9 @@
<span class="body-2 font-weight-medium">{{
$t("activity.previewAs")
}}</span>
<v-icon small class="ml-1"> mdi-drama-masks </v-icon>
<v-icon small class="ml-1">
mdi-drama-masks
</v-icon>
</v-list-item>
<v-list dense>
@ -649,15 +656,16 @@
<span
class="caption text-capitalize"
:class="{ 'x-active--text': role.title === previewAs }"
>{{ role.title }}</span
>
>{{ role.title }}</span>
</div>
</template>
</div>
<template v-if="previewAs">
<!-- <v-divider></v-divider>-->
<v-list-item @click="setPreviewUSer(null)">
<v-icon small class="mr-1"> mdi-close </v-icon>
<v-icon small class="mr-1">
mdi-close
</v-icon>
<!-- Reset Preview -->
<span class="caption nc-preview-reset">{{
$t("activity.resetReview")
@ -679,7 +687,9 @@
class="caption pointer nc-team-settings"
@click="click"
>
<v-icon color="brown" small class="mr-1"> mdi-cog </v-icon>
<v-icon color="brown" small class="mr-1">
mdi-cog
</v-icon>
Team & Settings
</div>
</template>
@ -690,9 +700,11 @@
<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 </v-icon>
<v-icon small class="mr-2">
mdi-api
</v-icon>
API Docs
</div>
<v-divider />
@ -894,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/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'
}
}

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

26017
packages/nocodb/package-lock.json generated

File diff suppressed because it is too large Load Diff

1
packages/nocodb/package.json

@ -156,6 +156,7 @@
"ncp": "^2.0.0",
"nocodb-sdk": "0.90.5",
"nodemailer": "^6.4.10",
"openapi-to-postmanv2": "^3.1.0",
"ora": "^4.0.4",
"os-locale": "^5.0.0",
"papaparse": "^5.3.1",

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

@ -291,6 +291,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;

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>`;

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

@ -0,0 +1,73 @@
// @ts-ignore
import catchError from '../../helpers/catchError';
import { Request, 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';
const Converter = require('openapi-to-postmanv2');
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);
}
async function postmanJson(req: Request, 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);
swagger.servers = [
{
url: (req as any).ncSiteUrl
}
];
Converter.convert(
{ type: 'json', data: swagger },
{},
(err, conversionResult) => {
if (err) {
res.status(400).json({ msg: err });
}
if (!conversionResult.result) {
res
.status(400)
.json({ msg: 'Could not convert : ' + conversionResult.reason });
} else {
res.json(conversionResult.output[0].data);
}
}
);
}
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/postman.json',
catchError(postmanJson)
);
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;

26
packages/nocodb/src/lib/noco/meta/api/swagger/swaggerHtml.ts

File diff suppressed because one or more lines are too long

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"

Loading…
Cancel
Save