Browse Source

Merge branch 'develop' into fix/knex-binding

pull/2424/head
Wing-Kam Wong 2 years ago
parent
commit
653cc09c68
  1. 12
      .github/workflows/ci-cd.yml
  2. 12
      package.json
  3. 27
      packages/nc-gui/components/ProjectTabs.vue
  4. 20
      packages/nc-gui/components/import/ImportFromAirtable.vue
  5. 484
      packages/nc-gui/components/import/JSONImport.vue
  6. 1
      packages/nc-gui/components/import/templateParsers/CSVTemplateAdapter.js
  7. 150
      packages/nc-gui/components/import/templateParsers/JSONTemplateAdapter.js
  8. 21
      packages/nc-gui/components/import/templateParsers/JSONUrlTemplateAdapter.js
  9. 58
      packages/nc-gui/components/import/templateParsers/parserHelpers.js
  10. 3
      packages/nc-gui/components/monaco/MonacoJsonEditor.js
  11. 16
      packages/nc-plugin/package-lock.json
  12. 14
      packages/noco-blog/package-lock.json
  13. 28
      packages/noco-docs-prev/package-lock.json
  14. 3
      packages/noco-docs/content/en/developer-resources/rest-apis.md
  15. 28
      packages/noco-docs/package-lock.json
  16. 6
      packages/noco-i18n/package-lock.json
  17. 3
      packages/nocodb-sdk/src/index.ts
  18. 20
      packages/nocodb-sdk/src/lib/Api.ts
  19. 30
      packages/nocodb-sdk/src/lib/TemplateGenerator.ts
  20. 10
      packages/nocodb/src/__tests__/restv2.test.ts
  21. 6
      packages/nocodb/src/lib/meta/api/columnApis.ts
  22. 6
      packages/nocodb/src/lib/meta/api/metaDiffApis.ts
  23. 2
      packages/nocodb/src/lib/meta/api/projectApis.ts
  24. 41
      packages/nocodb/src/lib/meta/api/userApi/initAdminFromEnv.ts
  25. 8
      packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts
  26. 7
      packages/nocodb/src/lib/meta/api/userApi/userApis.ts
  27. 11
      packages/nocodb/src/lib/meta/api/utilApis.ts
  28. 3
      packages/nocodb/src/lib/models/User.ts
  29. 4
      scripts/cypress/integration/common/1a_table_operations.js
  30. 4
      scripts/cypress/integration/common/2b_table_with_m2m_column.js
  31. 2
      scripts/cypress/integration/common/4e_form_view_share.js
  32. 20
      scripts/cypress/integration/common/4f_grid_view_share.js
  33. 18
      scripts/cypress/integration/common/4f_pg_grid_view_share.js
  34. 2
      scripts/cypress/integration/common/5a_user_role.js
  35. 23
      scripts/sdk/swagger.json

12
.github/workflows/ci-cd.yml

@ -49,7 +49,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
@ -94,7 +93,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
@ -139,7 +137,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
@ -184,7 +181,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
@ -229,7 +225,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -274,7 +269,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -319,7 +313,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -364,7 +357,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -409,7 +401,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:xcdb-api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -454,7 +445,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:xcdb-api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -499,7 +489,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:xcdb-api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -544,7 +533,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:xcdb-api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d

12
package.json

@ -15,12 +15,12 @@
"scripts": {
"build:common": "cd ./packages/nocodb-sdk; npm install; npm run build",
"install:common": "cd ./packages/nocodb; npm install ../nocodb-sdk; cd ../nc-gui; npm install ../nocodb-sdk",
"start:api": "cd ./packages/nocodb; npm install; NC_DISABLE_CACHE=true NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:xcdb-api": "cd ./packages/nocodb; npm install; NC_DISABLE_CACHE=true NC_DISABLE_TELE=true NC_INFLECTION=camelize DATABASE_URL=sqlite:../../../scripts/cypress/fixtures/sqlite-sakila/sakila.db npm run watch:run:cypress",
"start:api:cache": "cd ./packages/nocodb; npm install; NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:api:cache:pg": "cd ./packages/nocodb; npm install; NC_DISABLE_TELE=true npm run watch:run:cypress:pg",
"start:xcdb-api:cache": "cd ./packages/nocodb; npm install; NC_DISABLE_TELE=true NC_INFLECTION=camelize DATABASE_URL=sqlite:../../../scripts/cypress/fixtures/sqlite-sakila/sakila.db npm run watch:run:cypress",
"start:web": "cd ./packages/nc-gui; npm install; npm run dev",
"start:api": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk; npm install; NC_DISABLE_CACHE=true NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:xcdb-api": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk;npm install; NC_DISABLE_CACHE=true NC_DISABLE_TELE=true NC_INFLECTION=camelize DATABASE_URL=sqlite:../../../scripts/cypress/fixtures/sqlite-sakila/sakila.db npm run watch:run:cypress",
"start:api:cache": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk;npm install; NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:api:cache:pg": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk; npm install; NC_DISABLE_TELE=true npm run watch:run:cypress:pg",
"start:xcdb-api:cache": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk; npm install; NC_DISABLE_TELE=true NC_INFLECTION=camelize DATABASE_URL=sqlite:../../../scripts/cypress/fixtures/sqlite-sakila/sakila.db npm run watch:run:cypress",
"start:web": "npm run build:common ; cd ./packages/nc-gui; npm install ../nocodb-sdk; npm install; npm run dev",
"cypress:run": "cypress run --config-file ./scripts/cypress/cypress.json",
"cypress:open": "cypress open --config-file ./scripts/cypress/cypress.json",
"cypress:clear": "cypress cache clear",

27
packages/nc-gui/components/ProjectTabs.vue

@ -315,6 +315,21 @@
</span>
</v-list-item-title>
</v-list-item>
<v-list-item
v-if="_isUIAllowed('jsonImport')"
v-t="['a:actions:import-json']"
@click="jsonImportModal = true"
>
<v-list-item-title>
<v-icon small>
mdi-code-json
</v-icon>
<span class="caption">
<!-- TODO: i18n -->
JSON file
</span>
</v-list-item-title>
</v-list-item>
<v-list-item
v-if="_isUIAllowed('excelQuickImport')"
v-t="['a:actions:import-excel']"
@ -370,6 +385,13 @@
@closeModal="quickImportModal = false"
/>
<!-- Import From JSON string / file -->
<json-import
v-model="jsonImportModal"
hide-label
@closeModal="jsonImportModal = false"
/>
<import-from-airtable v-if="airtableImportModal" v-model="airtableImportModal" />
</v-container>
</template>
@ -404,9 +426,11 @@ import GlobalAcl from '~/components/GlobalAcl'
import AuditTab from '~/components/project/AuditTab'
import QuickImport from '~/components/import/QuickImport'
import ImportFromAirtable from '~/components/import/ImportFromAirtable'
import JsonImport from '~/components/import/JSONImport'
export default {
components: {
JsonImport,
ImportFromAirtable,
SwaggerClient,
// Screensaver,
@ -447,7 +471,8 @@ export default {
showScreensaver: false,
quickImportModal: false,
quickImportType: '',
airtableImportModal: false
airtableImportModal: false,
jsonImportModal: false
}
},
methods: {

20
packages/nc-gui/components/import/ImportFromAirtable.vue

@ -8,7 +8,6 @@
<div
v-t="['c:airtable-import:turbo-mode']"
class="ml-2 mt-3 title pointer nc-btn-enable-turbo"
@click="enableTurbo"
>
🚀
</div>
@ -86,6 +85,13 @@
hide-details
dense
/>
<v-checkbox
v-model="syncSource.details.options.syncViews"
class="caption"
label="Import Secondary Views"
hide-details
dense
/>
<v-checkbox
v-model="syncSource.details.options.syncRollup"
class="caption"
@ -225,7 +231,7 @@ export default {
apiKey: '',
shareId: '',
options: {
syncViews: false,
syncViews: true,
syncData: true,
syncRollup: false,
syncLookup: true,
@ -327,7 +333,7 @@ export default {
apiKey: '',
shareId: '',
options: {
syncViews: false,
syncViews: true,
syncData: true,
syncRollup: false,
syncLookup: true,
@ -350,10 +356,10 @@ export default {
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000)
}
},
enableTurbo() {
this.$set(this.syncSource.details.options, 'syncViews', true)
this.$toast.success('🚀🚀 Ludicrous mode activated! Let\'s go! 🚀🚀').goAway(3000)
},
// enableTurbo() {
// this.$set(this.syncSource.details.options, 'syncViews', true)
// this.$toast.success('🚀🚀 Ludicrous mode activated! Let\'s go! 🚀🚀').goAway(3000)
// },
migrateSync(src) {
if (!src.details?.options) {
src.details.options = {

484
packages/nc-gui/components/import/JSONImport.vue

@ -0,0 +1,484 @@
<template>
<div :class="{'pt-10':!hideLabel}">
<v-dialog v-model="dropOrUpload" max-width="600">
<v-card max-width="600">
<v-tabs height="30">
<v-tab>
<v-icon small class="mr-1">
mdi-file-upload-outline
</v-icon>
<span class="caption text-capitalize">Upload</span>
</v-tab>
<!-- <v-tab>-->
<!-- <v-icon small class="mr-1">
mdi-link-variant
</v-icon>
<span class="caption text-capitalize">URL</span>
</v-tab>-->
<v-tab>
<v-icon small class="mr-1">
mdi-link-variant
</v-icon>
<span class="caption text-capitalize">String</span>
</v-tab>
<v-tab-item>
<div class="nc-json-import-tab-item ">
<div
class="nc-droppable d-flex align-center justify-center flex-column"
:style="{
background : dragOver ? '#7772' : ''
}"
@click="$refs.file.click()"
@drop.prevent="dropHandler"
@dragover.prevent="dragOver = true"
@dragenter.prevent="dragOver = true"
@dragexit="dragOver = false"
@dragleave="dragOver = false"
@dragend="dragOver = false"
>
<x-icon :color="['primary','grey']" size="50">
mdi-file-plus-outline
</x-icon>
<p class="title mb-1 mt-2">
<!-- Select File to Upload-->
{{ $t('msg.info.upload') }}
</p>
<p class="grey--text mb-1">
<!-- or drag and drop file-->
{{ $t('msg.info.upload_sub') }}
</p>
<p v-if="quickImportType == 'excel'" class="caption grey--text">
<!-- Supported: .xls, .xlsx, .xlsm, .ods, .ots -->
{{ $t('msg.info.excelSupport') }}
</p>
</div>
</div>
</v-tab-item>
<!-- <v-tab-item>
<div class="nc-json-import-tab-item align-center">
<div class="pa-4 d-100 h-100">
<v-form ref="form" v-model="valid">
<div class="d-flex">
&lt;!&ndash; todo: i18n label&ndash;&gt;
<v-text-field
v-model="url"
hide-details="auto"
type="url"
label="Enter JSON file url"
class="caption"
outlined
dense
:rules="
[
v => !!v || $t('general.required'),
v => !(/(10)(\.([2]([0-5][0-5]|[01234][6-9])|[1][0-9][0-9]|[1-9][0-9]|[0-9])){3}|(172)\.(1[6-9]|2[0-9]|3[0-1])(\.(2[0-4][0-9]|25[0-5]|[1][0-9][0-9]|[1-9][0-9]|[0-9])){2}|(192)\.(168)(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){2}|(0.0.0.0)|localhost?/g).test(v) || errorMessages.ipBlockList
]"
/>
<v-btn v-t="['c:project:create:json:load-url']" class="ml-3" color="primary" @click="loadUrl">
&lt;!&ndash;Load&ndash;&gt;
{{ $t('general.load') }}
</v-btn>
</div>
</v-form>
</div>
</div>
</v-tab-item>-->
<v-tab-item>
<div class="nc-json-import-tab-item align-center">
<div class="pa-4 d-100 h-100">
<v-form ref="form" v-model="valid">
<div class="nc-json-editor-wrapper">
<v-btn small class="nc-json-format-btn" @click="formatJson">
Format
</v-btn>
<!--label="Enter excel file url"-->
<monaco-json-editor
ref="editor"
v-model="jsonString"
style="height:320px"
/>
<div class="text-center mt-4">
<v-btn v-t="['c:project:create:excel:load-url']" class="ml-3" color="primary" @click="loadJsonString">
<!--Load-->
{{ $t('general.load') }}
</v-btn>
</div>
</div>
</v-form>
</div>
</div>
</v-tab-item>
</v-tabs>
<div class="px-4 pb-2">
<div class="d-flex">
<v-spacer />
<span class="caption pointer grey--text" @click="showMore = !showMore">
{{ showMore ? $t('general.hideAll') : $t('general.showMore') }}
<v-icon small color="grey lighten-1">mdi-menu-{{ showMore ? 'up' : 'down' }}</v-icon>
</span>
</div>
<div class="mb-2 pt-2 nc-json-import-options" :style="{ maxHeight: showMore ? '200px' : '0'}">
<p />
<!--hint="# of rows to parse to infer data type"-->
<v-text-field
v-model="parserConfig.maxRowsToParse"
style="max-width: 250px"
class="caption mx-auto"
dense
persistent-hint
:hint="$t('msg.info.footMsg')"
outlined
type="number"
/>
<v-checkbox
v-model="parserConfig.normalizeNested"
style="width: 250px"
class="mx-auto mb-2"
dense
hide-details
>
<template #label>
<span class="caption">Flatten nested</span>
<v-tooltip bottom position-y="">
<template #activator="{ on }">
<v-icon small class="ml-1" v-on="on">
mdi-information-outline
</v-icon>
</template>
<div class="caption" style="width: 260px">
If flatten nested option is set it will flatten nested object as root level property. In normal case nested object will treat as JSON column.
<br>
<br>
For example the following input: <code class="caption font-weight-bold">{
"prop1": {
"prop2": "value"
},
"prop3": "value",
"prop4": 1
}</code> will treat as:
<code class="caption font-weight-bold">{
"prop1_prop2": "value",
"prop3": "value",
"prop4": 1
}</code>
</div>
</v-tooltip>
</template>
</v-checkbox>
<v-checkbox
v-model="parserConfig.importData"
style="width: 250px"
class="mx-auto mb-2"
dense
hide-details
>
<template #label>
<span class="caption">Import data</span>
</template>
</v-checkbox>
</div>
</div>
</v-card>
</v-dialog>
<v-tooltip bottom>
<template #activator="{on}">
<input
ref="file"
class="nc-json-import-input"
type="file"
style="display: none"
accept=".json"
@change="_change($event)"
>
<v-btn
v-if="!hideLabel"
small
outlined
v-on="on"
@click="$refs.file.click()"
>
<v-icon small class="mr-1">
mdi-file-excel-outline
</v-icon>
<!--Import-->
{{ $t('activity.import') }}
</v-btn>
</template>
<span class="caption">Create template from JSON</span>
</v-tooltip>
<v-dialog v-if="templateData" v-model="templateEditorModal" max-width="1000">
<v-card class="pa-6" min-width="500">
<template-editor :project-template.sync="templateData" json-import :quick-import-type="quickImportType">
<template #toolbar="{valid}">
<h3 class="mt-2 grey--text">
<span>
JSON Import
</span>
</h3>
<v-spacer />
<v-spacer />
<create-project-from-template-btn
:template-data="templateData"
:import-data="importData"
:import-to-project="importToProject"
json-import
:valid="valid"
create-gql-text="Import as GQL Project"
create-rest-text="Import as REST Project"
@closeModal="$emit('closeModal'),templateEditorModal = false"
>
<!--Import Excel-->
<span v-if="quickImportType === 'excel'">
{{ $t('activity.importExcel') }}
</span>
<!--Import CSV-->
<span v-if="quickImportType === 'csv'">
{{ $t('activity.importCSV') }}
</span>
</create-project-from-template-btn>
</template>
</template-editor>
</v-card>
</v-dialog>
</div>
</template>
<script>
import TemplateEditor from '~/components/templates/Editor'
import CreateProjectFromTemplateBtn from '~/components/templates/CreateProjectFromTemplateBtn'
import MonacoJsonEditor from '~/components/monaco/MonacoJsonEditor'
import JSONTemplateAdapter from '~/components/import/templateParsers/JSONTemplateAdapter'
import JSONUrlTemplateAdapter from '~/components/import/templateParsers/JSONUrlTemplateAdapter'
export default {
name: 'JsonImport',
components: { MonacoJsonEditor, CreateProjectFromTemplateBtn, TemplateEditor },
props: {
hideLabel: Boolean,
value: Boolean,
importToProject: Boolean,
quickImportType: String
},
data() {
return {
templateEditorModal: false,
valid: null,
templateData: null,
importData: null,
dragOver: false,
url: '',
showMore: false,
parserConfig: {
maxRowsToParse: 500,
normalizeNested: true,
importData: true
},
filename: '',
jsonString: '',
errorMessages: {
ipBlockList: 'IP Not allowed!',
importJSON: 'Target file is not an accepted file type. The accepted file type is .json!'
}
}
},
computed: {
dropOrUpload: {
set(v) {
this.$emit('input', v)
},
get() {
return this.value
}
},
tables() {
return this.$store.state.project.tables || []
}
},
mounted() {
if (this.$route && this.$route.query && this.$route.query.excelUrl) {
this.url = this.$route.query.excelUrl
this.loadUrl()
}
},
methods: {
formatJson() {
console.log(this.$refs.editor)
this.$refs.editor.format()
},
selectFile() {
this.$refs.file.files = null
this.$refs.file.click()
},
_change(event) {
const files = event.target.files
if (files && files[0]) {
this._file(files[0])
event.target.value = ''
}
},
async _file(file) {
this.templateData = null
this.importData = null
this.$store.commit('loader/MutMessage', 'Loading excel file')
let i = 0
const int = setInterval(() => {
this.$store.commit('loader/MutMessage', `Loading excel file${'.'.repeat(++i % 4)}`)
}, 1000)
this.dropOrUpload = false
const reader = new FileReader()
this.filename = file.name
reader.onload = async(e) => {
const ab = e.target.result
await this.parseAndExtractData('file', ab, file.name)
this.$store.commit('loader/MutMessage', null)
clearInterval(int)
}
const handleEvent = (event) => {
this.$store.commit('loader/MutMessage', `${event.type}: ${event.loaded} bytes transferred`)
}
reader.addEventListener('progress', handleEvent)
reader.onerror = (e) => {
console.log('error', e)
this.$store.commit('loader/MutClear')
}
reader.readAsText(file)
},
async parseAndExtractData(type, val, name) {
try {
let templateGenerator
this.templateData = null
this.importData = null
switch (type) {
case 'file':
templateGenerator = new JSONTemplateAdapter(name, val, this.parserConfig)
break
case 'url':
templateGenerator = new JSONUrlTemplateAdapter(val, this.$store, this.parserConfig, this.$api)
break
case 'string':
templateGenerator = new JSONTemplateAdapter(name, val, this.parserConfig)
break
}
await templateGenerator.init()
templateGenerator.parse()
this.templateData = templateGenerator.getTemplate()
this.templateData.tables[0].table_name = this.populateUniqueTableName()
this.importData = templateGenerator.getData()
this.templateEditorModal = true
} catch (e) {
console.log(e)
this.$toast
.error(await this._extractSdkResponseErrorMsg(e))
.goAway(3000)
}
},
dropHandler(ev) {
this.dragOver = false
let file
if (ev.dataTransfer.items) {
// Use DataTransferItemList interface to access the file(s)
if (ev.dataTransfer.items.length && ev.dataTransfer.items[0].kind === 'file') {
file = ev.dataTransfer.items[0].getAsFile()
}
} else if (ev.dataTransfer.files.length) {
file = ev.dataTransfer.files[0]
}
if (!file) {
return
}
if (!/.*\.json/.test(file.name)) {
return this.$toast.error(this.errorMessages.importJSON).goAway(3000)
}
this._file(file)
},
dragOverHandler(ev) {
// Prevent default behavior (Prevent file from being opened)
ev.preventDefault()
},
populateUniqueTableName() {
let c = 1
while (this.tables.some(t => t.title === `Sheet${c}`)) { c++ }
return `Sheet${c}`
},
async loadUrl() {
if ((this.$refs.form && !this.$refs.form.validate()) || !this.url) {
return
}
this.$store.commit('loader/MutMessage', 'Loading json file from url')
let i = 0
const int = setInterval(() => {
this.$store.commit('loader/MutMessage', `Loading json file${'.'.repeat(++i % 4)}`)
}, 1000)
this.dropOrUpload = false
await this.parseAndExtractData('url', this.url, '')
clearInterval(int)
this.$store.commit('loader/MutClear')
},
async loadJsonString() {
await this.parseAndExtractData('string', this.jsonString)
this.$store.commit('loader/MutClear')
}
}
}
</script>
<style scoped>
.nc-droppable {
width: 100%;
min-height: 200px;
border-radius: 4px;
border: 2px dashed #ddd;
}
.nc-json-import-tab-item {
min-height: 400px;
padding: 20px;
display: flex;
align-items: stretch;
width: 100%;
}
.nc-json-import-options {
transition: .4s max-height;
overflow: hidden;
}
.nc-json-editor-wrapper{
position: relative;
}
.nc-json-format-btn{
position:absolute;
right:4px;
top:4px;
z-index:9;
}
</style>

1
packages/nc-gui/components/import/templateParsers/CSVTemplateAdapter.js

@ -1,6 +1,5 @@
import Papaparse from 'papaparse'
import TemplateGenerator from '~/components/import/templateParsers/TemplateGenerator'
export default class CSVTemplateAdapter extends TemplateGenerator {
constructor(name, data) {
super()

150
packages/nc-gui/components/import/templateParsers/JSONTemplateAdapter.js

@ -0,0 +1,150 @@
import { TemplateGenerator, UITypes } from 'nocodb-sdk'
import {
extractMultiOrSingleSelectProps,
getCheckboxValue,
isCheckboxType, isDecimalType, isEmailType,
isMultiLineTextType, isUrlType
} from '~/components/import/templateParsers/parserHelpers'
const jsonTypeToUidt = {
number: UITypes.Number,
string: UITypes.SingleLineText,
date: UITypes.DateTime,
boolean: UITypes.Checkbox,
object: UITypes.JSON
}
const extractNestedData = (obj, path) => path.reduce((val, key) => val && val[key], obj)
export default class JSONTemplateAdapter extends TemplateGenerator {
constructor(name = 'test', data, parserConfig = {}) {
super()
this.config = {
maxRowsToParse: 500,
...parserConfig
}
this.name = name
this._jsonData = typeof data === 'string' ? JSON.parse(data) : data
this.project = {
title: this.name,
tables: []
}
this.data = {}
}
async init() {
}
parseData() {
this.columns = this.csv.meta.fields
this.data = this.csv.data
}
getColumns() {
return this.columns
}
getData() {
return this.data
}
get jsonData() {
return Array.isArray(this._jsonData) ? this._jsonData : [this._jsonData]
}
parse() {
const jsonData = this.jsonData
const tn = 'table'
const table = { table_name: tn, ref_table_name: tn, columns: [] }
this.data[tn] = []
for (const col of Object.keys(jsonData[0])) {
const columns = this._parseColumn([col], jsonData)
table.columns.push(...columns)
}
if (this.config.importData) { this._parseTableData(table) }
this.project.tables.push(table)
}
getTemplate() {
return this.project
}
_parseColumn(path = [], jsonData = this.jsonData, firstRowVal = path.reduce((val, k) => val && val[k], this.jsonData[0])) {
const columns = []
// parse nested
if (firstRowVal && typeof firstRowVal === 'object' && !Array.isArray(firstRowVal) && this.config.normalizeNested) {
for (const key of Object.keys(firstRowVal)) {
const normalizedNestedColumns = this._parseColumn([...path, key], this.jsonData, firstRowVal[key])
columns.push(...normalizedNestedColumns)
}
} else {
const cn = path.join('_').replace(/\W/g, '_').trim()
const column = {
column_name: cn,
ref_column_name: cn,
path
}
column.uidt = jsonTypeToUidt[typeof firstRowVal] || UITypes.SingleLineText
const colData = jsonData.map(r => extractNestedData(r, path))
Object.assign(column, this._getColumnUIDTAndMetas(colData, column.uidt))
columns.push(column)
}
return columns
}
_getColumnUIDTAndMetas(colData, defaultType) {
const colProps = { uidt: defaultType }
// todo: optimize
if (colProps.uidt === UITypes.SingleLineText) {
// check for long text
if (isMultiLineTextType(colData)) {
colProps.uidt = UITypes.LongText
} if (isEmailType(colData)) {
colProps.uidt = UITypes.Email
} if (isUrlType(colData)) {
colProps.uidt = UITypes.URL
} else {
const checkboxType = isCheckboxType(colData)
if (checkboxType.length === 1) {
colProps.uidt = UITypes.Checkbox
} else {
Object.assign(colProps, extractMultiOrSingleSelectProps(colData))
}
}
} else if (colProps.uidt === UITypes.Number) {
if (isDecimalType(colData)) {
colProps.uidt = UITypes.Decimal
}
}
return colProps
}
_parseTableData(tableMeta) {
for (const row of this.jsonData) {
const rowData = {}
for (let i = 0; i < tableMeta.columns.length; i++) {
const value = extractNestedData(row, tableMeta.columns[i].path || [])
if (tableMeta.columns[i].uidt === UITypes.Checkbox) {
rowData[tableMeta.columns[i].ref_column_name] = getCheckboxValue(value)
} else if (tableMeta.columns[i].uidt === UITypes.SingleSelect || tableMeta.columns[i].uidt === UITypes.MultiSelect) {
rowData[tableMeta.columns[i].ref_column_name] = (value || '').toString().trim() || null
} else if (tableMeta.columns[i].uidt === UITypes.JSON) {
rowData[tableMeta.columns[i].ref_column_name] = JSON.stringify(value)
} else {
// toto: do parsing if necessary based on type
rowData[tableMeta.columns[i].column_name] = value
}
}
this.data[tableMeta.ref_table_name].push(rowData)
// rowIndex++
}
}
}

21
packages/nc-gui/components/import/templateParsers/JSONUrlTemplateAdapter.js

@ -0,0 +1,21 @@
import JSONTemplateAdapter from '~/components/import/templateParsers/JSONTemplateAdapter'
export default class JSONUrlTemplateAdapter extends JSONTemplateAdapter {
constructor(url, $store, parserConfig, $api) {
const name = url.split('/').pop()
super(name, null, parserConfig)
this.url = url
this.$api = $api
this.$store = $store
}
async init() {
const data = await this.$api.utils.axiosRequestMake({
apiMeta: {
url: this.url
}
})
this._jsonData = data
await super.init()
}
}

58
packages/nc-gui/components/import/templateParsers/parserHelpers.js

@ -1,3 +1,6 @@
import { UITypes } from 'nocodb-sdk'
import { isEmail, isValidURL } from '~/helpers'
const booleanOptions = [
{ checked: true, unchecked: false },
{ x: true, '': false },
@ -11,14 +14,24 @@ const booleanOptions = [
{ '✔': true, '': false },
{ enabled: true, disabled: false },
{ on: true, off: false },
{ done: true, '': false }
{ done: true, '': false },
{ true: true, false: false }
]
const aggBooleanOptions = booleanOptions.reduce((obj, o) => ({ ...obj, ...o }), {})
export const isCheckboxType = (values, col = '') => {
const getColVal = (row, col = null) => {
return row && col ? row[col] : row
}
export const isCheckboxType = (values, col = null) => {
let options = booleanOptions
for (let i = 0; i < values.length; i++) {
let val = col ? values[i][col] : values[i]
val = val === null || val === undefined ? '' : val
const val = getColVal(values[i], col)
if (val === null || val === undefined || val.toString().trim() === '') {
continue
}
options = options.filter(v => val in v)
if (!options.length) {
return false
@ -29,3 +42,40 @@ export const isCheckboxType = (values, col = '') => {
export const getCheckboxValue = (value) => {
return value && aggBooleanOptions[value]
}
export const isMultiLineTextType = (values, col = null) => {
return values.some(r =>
(getColVal(r, col) || '').toString().match(/[\r\n]/) ||
(getColVal(r, col) || '').toString().length > 255)
}
export const extractMultiOrSingleSelectProps = (colData) => {
const colProps = {}
if (colData.some(v => v && (v || '').toString().includes(','))) {
let flattenedVals = colData.flatMap(v => v ? v.toString().trim().split(/\s*,\s*/) : [])
const uniqueVals = flattenedVals = flattenedVals
.filter((v, i, arr) => i === arr.findIndex(v1 => v.toLowerCase() === v1.toLowerCase()))
if (flattenedVals.length > uniqueVals.length && uniqueVals.length <= Math.ceil(flattenedVals.length / 2)) {
colProps.uidt = UITypes.MultiSelect
colProps.dtxp = `'${uniqueVals.join("','")}'`
}
} else {
const uniqueVals = colData.map(v => (v || '').toString().trim()).filter((v, i, arr) => i === arr.findIndex(v1 => v.toLowerCase() === v1.toLowerCase()))
if (colData.length > uniqueVals.length && uniqueVals.length <= Math.ceil(colData.length / 2)) {
colProps.uidt = UITypes.SingleSelect
colProps.dtxp = `'${uniqueVals.join("','")}'`
}
}
return colProps
}
export const isDecimalType = colData => colData.some((v) => {
return v && parseInt(+v) !== +v
})
export const isEmailType = colData => !colData.some((v) => {
return v && !isEmail(v)
})
export const isUrlType = colData => !colData.some((v) => {
return v && !isValidURL(v)
})

3
packages/nc-gui/components/monaco/MonacoJsonEditor.js

@ -83,6 +83,9 @@ export default {
},
methods: {
format() {
this.editor.getAction('editor.action.formatDocument').run()
},
resizeLayout() {
this.editor.layout();
},

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

@ -1,12 +1,12 @@
{
"name": "nc-plugin",
"version": "0.1.1",
"version": "0.1.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nc-plugin",
"version": "0.1.1",
"version": "0.1.3",
"license": "MIT",
"dependencies": {
"@bitauth/libauth": "^1.17.1",
@ -10298,9 +10298,9 @@
}
},
"node_modules/shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
"dev": true
},
"node_modules/shelljs": {
@ -19881,9 +19881,9 @@
"dev": true
},
"shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
"dev": true
},
"shelljs": {

14
packages/noco-blog/package-lock.json generated

@ -5836,16 +5836,16 @@
}
},
"got": {
"version": "11.8.2",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
"version": "11.8.5",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
"integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
"requires": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
@ -10797,9 +10797,9 @@
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
},
"shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg=="
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="
},
"side-channel": {
"version": "1.0.4",

28
packages/noco-docs-prev/package-lock.json generated

@ -7390,16 +7390,16 @@
}
},
"node_modules/got": {
"version": "11.8.2",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
"version": "11.8.5",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
"integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
"dependencies": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
@ -13804,9 +13804,9 @@
}
},
"node_modules/shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg=="
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="
},
"node_modules/side-channel": {
"version": "1.0.4",
@ -22650,16 +22650,16 @@
}
},
"got": {
"version": "11.8.2",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
"version": "11.8.5",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
"integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
"requires": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
@ -27611,9 +27611,9 @@
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
},
"shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg=="
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="
},
"side-channel": {
"version": "1.0.4",

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

@ -166,7 +166,8 @@ Currently, the default value for {orgs} is <b>noco</b>. Users will be able to ch
| Meta | Delete| utils | cacheDelete | /api/v1/db/meta/cache |
| Meta | Post | utils | testConnection | /api/v1/db/meta/projects/connection/test |
| Meta | Get | utils | appInfo | /api/v1/db/meta/nocodb/info |
| Meta | Get | utils | appVersion | /api/v1/db/meta/nocodb/version |
| Meta | Get | utils | appVersion | /api/v1/version |
| Meta | Get | utils | appHealth | /api/v1/health |
## Query params

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

@ -7390,16 +7390,16 @@
}
},
"node_modules/got": {
"version": "11.8.2",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
"version": "11.8.5",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
"integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
"dependencies": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
@ -13804,9 +13804,9 @@
}
},
"node_modules/shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg=="
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="
},
"node_modules/side-channel": {
"version": "1.0.4",
@ -22650,16 +22650,16 @@
}
},
"got": {
"version": "11.8.2",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
"version": "11.8.5",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
"integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
"requires": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
@ -27611,9 +27611,9 @@
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
},
"shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg=="
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="
},
"side-channel": {
"version": "1.0.4",

6
packages/noco-i18n/package-lock.json generated

@ -9793,9 +9793,9 @@
"dev": true
},
"shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
"dev": true
},
"side-channel": {

3
packages/nocodb-sdk/src/index.ts

@ -5,6 +5,7 @@ export * from './lib/sqlUi';
export * from './lib/globals';
export * from './lib/helperFunctions';
export * from './lib/formulaHelpers';
export * from './lib/passwordHelpers';
export { default as UITypes, isVirtualCol } from './lib/UITypes';
export { default as CustomAPI } from './lib/CustomAPI';
export { default as TemplateGenerator } from './lib/TemplateGenerator';
export * from './lib/passwordHelpers';

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

@ -3197,12 +3197,28 @@ export class Api<
*
* @tags Utils
* @name AppVersion
* @request GET:/api/v1/db/meta/nocodb/version
* @request GET:/api/v1/version
* @response `200` `any` OK
*/
appVersion: (params: RequestParams = {}) =>
this.request<any, any>({
path: `/api/v1/db/meta/nocodb/version`,
path: `/api/v1/version`,
method: 'GET',
format: 'json',
...params,
}),
/**
* No description
*
* @tags Utils
* @name AppHealth
* @request GET:/api/v1/health
* @response `200` `any` OK
*/
appHealth: (params: RequestParams = {}) =>
this.request<any, any>({
path: `/api/v1/health`,
method: 'GET',
format: 'json',
...params,

30
packages/nocodb-sdk/src/lib/TemplateGenerator.ts

@ -0,0 +1,30 @@
import UITypes from './UITypes';
export interface Column {
column_name: string;
ref_column_name: string;
uidt?: UITypes;
dtxp?: any;
dt?: any;
}
export interface Table {
table_name: string;
ref_table_name: string;
columns: Array<Column>;
}
export interface Template {
title: string;
tables: Array<Table>;
}
export default abstract class TemplateGenerator {
abstract parse(): Promise<any>;
abstract parseTemplate(): Promise<Template>;
abstract getColumns(): Promise<any>;
abstract parseData(): Promise<any>;
abstract getData(): Promise<{
[table_name: string]: Array<{
[key: string]: any;
}>;
}>;
}

10
packages/nocodb/src/__tests__/restv2.test.ts

@ -209,7 +209,7 @@ describe('Noco v2 Tests', () => {
type: UITypes.Rollup,
alias: 'filmCount',
rollupColumn: 'FilmId',
relationColumn: 'FilmMMList',
relationColumn: 'Film List',
rollupFunction: 'count'
}
];
@ -413,7 +413,7 @@ describe('Noco v2 Tests', () => {
type: UITypes.Lookup,
alias: 'filmNames',
lookupColumn: 'Title',
relationColumn: 'FilmMMList'
relationColumn: 'Film List'
};
request(app)
.post(`/nc/${projectId}/generate`)
@ -1335,7 +1335,7 @@ describe('Noco v2 Tests', () => {
type: UITypes.Lookup,
alias: 'filmIds',
lookupColumn: 'FilmId',
relationColumn: 'FilmMMList'
relationColumn: 'Film List'
},
{
table: 'actor',
@ -1398,7 +1398,7 @@ describe('Noco v2 Tests', () => {
type: UITypes.Rollup,
alias: 'actorsCount',
rollupColumn: 'ActorId',
relationColumn: 'ActorMMList',
relationColumn: 'ActorList',
rollupFunction: 'count'
},
{
@ -1406,7 +1406,7 @@ describe('Noco v2 Tests', () => {
type: UITypes.Lookup,
alias: 'actorsCountList',
lookupColumn: 'actorsCount',
relationColumn: 'FilmMMList'
relationColumn: 'Film List'
},
{
table: 'actor',

6
packages/nocodb/src/lib/meta/api/columnApis.ts

@ -57,7 +57,7 @@ async function createHmAndBtColumn(
{
const title = getUniqueColumnAliasName(
await child.getColumns(),
type === 'bt' ? alias : `${parent.title}Read`
type === 'bt' ? alias : `${parent.title}`
);
await Column.insert<LinkToAnotherRecordColumn>({
title,
@ -427,7 +427,7 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
await Column.insert({
title: getUniqueColumnAliasName(
await child.getColumns(),
`${child.title}MMList`
`${parent.title} List`
),
uidt: UITypes.LinkToAnotherRecord,
type: 'mm',
@ -447,7 +447,7 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
await Column.insert({
title: getUniqueColumnAliasName(
await parent.getColumns(),
req.body.title ?? `${parent.title}MMList`
req.body.title ?? `${child.title} List`
),
uidt: UITypes.LinkToAnotherRecord,

6
packages/nocodb/src/lib/meta/api/metaDiffApis.ts

@ -671,7 +671,7 @@ export async function metaDiffSync(req, res) {
if (change.relationType === RelationTypes.BELONGS_TO) {
const title = getUniqueColumnAliasName(
childModel.columns,
`${parentModel.title || parentModel.table_name}Read`
`${parentModel.title || parentModel.table_name}`
);
await Column.insert<LinkToAnotherRecordColumn>({
uidt: UITypes.LinkToAnotherRecord,
@ -785,7 +785,7 @@ export async function extractAndGenerateManyToManyRelations(
await Column.insert<LinkToAnotherRecordColumn>({
title: getUniqueColumnAliasName(
modelA.columns,
`${modelB.title}MMList`
`${modelB.title} List`
),
fk_model_id: modelA.id,
fk_related_model_id: modelB.id,
@ -803,7 +803,7 @@ export async function extractAndGenerateManyToManyRelations(
await Column.insert<LinkToAnotherRecordColumn>({
title: getUniqueColumnAliasName(
modelB.columns,
`${modelA.title}MMList`
`${modelA.title} List`
),
fk_model_id: modelB.id,
fk_related_model_id: modelA.id,

2
packages/nocodb/src/lib/meta/api/projectApis.ts

@ -230,7 +230,7 @@ async function populateMeta(base: Base, project: Project): Promise<any> {
uidt: UITypes.LinkToAnotherRecord,
type: 'bt',
bt,
title: `${bt.rtitle}Read`
title: `${bt.rtitle}`
};
})
];

41
packages/nocodb/src/lib/meta/api/userApi/initAdminFromEnv.ts

@ -5,10 +5,11 @@ import { Tele } from 'nc-help';
import bcrypt from 'bcryptjs';
import Noco from '../../../Noco';
import { MetaTable } from '../../../utils/globals';
import { CacheScope, MetaTable } from '../../../utils/globals';
import ProjectUser from '../../../models/ProjectUser';
import { validatePassword } from 'nocodb-sdk';
import boxen from 'boxen';
import NocoCache from '../../../cache/NocoCache';
const { isEmail } = require('validator');
const rolesLevel = { owner: 0, creator: 1, editor: 2, commenter: 3, viewer: 4 };
@ -103,7 +104,7 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) {
// check user account already present with the new admin email
const existingUserWithNewEmail = await User.getByEmail(email, ncMeta);
if (existingUserWithNewEmail) {
if (existingUserWithNewEmail?.id) {
// get all project access belongs to the existing account
// and migrate to the admin account
const existingUserProjects = await ncMeta.metaList2(
@ -155,13 +156,25 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) {
}
// delete existing user
ncMeta.metaDelete(
await ncMeta.metaDelete(
null,
null,
MetaTable.USERS,
existingUserWithNewEmail.id
);
// clear cache
await NocoCache.delAll(
CacheScope.USER,
`${existingUserWithNewEmail.email}___*`
);
await NocoCache.del(
`${CacheScope.USER}:${existingUserWithNewEmail.id}`
);
await NocoCache.del(
`${CacheScope.USER}:${existingUserWithNewEmail.email}`
);
// Update email and password of super admin account
await User.update(
superUser.id,
@ -169,7 +182,9 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) {
salt,
email,
password,
email_verification_token
email_verification_token,
token_version: null,
refresh_token: null
},
ncMeta
);
@ -181,24 +196,36 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) {
salt,
email,
password,
email_verification_token
email_verification_token,
token_version: null,
refresh_token: null
},
ncMeta
);
}
} else {
// if email's are not different update the password and hash
const newPasswordHash = await promisify(bcrypt.hash)(
process.env.NC_ADMIN_PASSWORD,
superUser.salt
);
if (newPasswordHash !== superUser.password) {
// if email's are same and passwords are different
// then update the password and token version
await User.update(
superUser.id,
{
salt,
password,
email_verification_token
email_verification_token,
token_version: null,
refresh_token: null
},
ncMeta
);
}
}
}
await ncMeta.commit();
} catch (e) {
console.log('Error occurred while updating/creating admin user');

8
packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts

@ -103,8 +103,8 @@ export function initStrategies(router): void {
if (cachedVal) {
if (
cachedVal.token_version &&
jwtPayload.token_version &&
!cachedVal.token_version ||
!jwtPayload.token_version ||
cachedVal.token_version !== jwtPayload.token_version
) {
return done(new Error('Token Expired. Please login again.'));
@ -115,8 +115,8 @@ export function initStrategies(router): void {
User.getByEmail(jwtPayload?.email)
.then(async user => {
if (
user.token_version &&
jwtPayload.token_version &&
!user.token_version ||
!jwtPayload.token_version ||
user.token_version !== jwtPayload.token_version
) {
return done(new Error('Token Expired. Please login again.'));

7
packages/nocodb/src/lib/meta/api/userApi/userApis.ts

@ -179,15 +179,14 @@ async function successfulSignIn({
await promisify((req as any).login.bind(req))(user);
const refreshToken = randomTokenString();
let token_version = user.token_version;
if (!token_version) {
token_version = randomTokenString();
if (!user.token_version) {
user.token_version = randomTokenString();
}
await User.update(user.id, {
refresh_token: refreshToken,
email: user.email,
token_version
token_version: user.token_version
});
setTokenCookie(res, refreshToken);

11
packages/nocodb/src/lib/meta/api/utilApis.ts

@ -60,6 +60,14 @@ export async function releaseVersion(_req: Request, res: Response) {
res.json(result);
}
export async function appHealth(_: Request, res: Response) {
res.json({
message: 'OK',
timestamp: Date.now(),
uptime: process.uptime()
});
}
async function _axiosRequestMake(req: Request, res: Response) {
const { apiMeta } = req.body;
@ -132,6 +140,7 @@ export default router => {
ncMetaAclMw(testConnection, 'testConnection')
);
router.get('/api/v1/db/meta/nocodb/info', catchError(appInfo));
router.get('/api/v1/db/meta/nocodb/version', catchError(releaseVersion));
router.post('/api/v1/db/meta/axiosRequestMake', catchError(axiosRequestMake));
router.get('/api/v1/version', catchError(releaseVersion));
router.get('/api/v1/health', catchError(appHealth));
};

3
packages/nocodb/src/lib/models/User.ts

@ -84,6 +84,9 @@ export default class User implements UserType {
if (updateObj.email) {
updateObj.email = updateObj.email.toLowerCase();
} else {
// set email prop to avoid generation of invalid cache key
updateObj.email = (await this.get(id, ncMeta))?.email?.toLowerCase();
}
// get existing cache
const keys = [

4
scripts/cypress/integration/common/1a_table_operations.js

@ -80,9 +80,9 @@ export const genTest = (apiType, dbType) => {
// 4a. Address table, has many field
cy.openTableTab("Address", 25);
mainPage.getCell("CityRead", 1).scrollIntoView();
mainPage.getCell("City", 1).scrollIntoView();
mainPage
.getCell("CityRead", 1)
.getCell("City", 1)
.find(".name")
.contains("Lethbridge")
.should("exist");

4
scripts/cypress/integration/common/2b_table_with_m2m_column.js

@ -23,10 +23,10 @@ export const genTest = (apiType, dbType) => {
it("Expand m2m column", () => {
// expand first row
cy.get('td[data-col="FilmMMList"] div', { timeout: 12000 })
cy.get('td[data-col="Film List"] div', { timeout: 12000 })
.first()
.click({ force: true });
cy.get('td[data-col="FilmMMList"] div .mdi-arrow-expand')
cy.get('td[data-col="Film List"] div .mdi-arrow-expand')
.first()
.click({ force: true });

2
scripts/cypress/integration/common/4e_form_view_share.js

@ -131,7 +131,7 @@ export const genTest = (apiType, dbType) => {
// all fields, barring removed field should exist
cy.get('[title="City"]').should("exist");
cy.get('[title="LastUpdate"]').should("exist");
cy.get('[title="CountryRead"]').should("exist");
cy.get('[title="Country"]').should("exist");
cy.get('[title="Address List"]').should("not.exist");
// order of LastUpdate & City field is retained

20
scripts/cypress/integration/common/4f_grid_view_share.js

@ -170,7 +170,7 @@ export const genTest = (apiType, dbType) => {
const verifyCsv = (retrievedRecords) => {
// expected output, statically configured
let storedRecords = [
`Address,District,PostalCode,Phone,Location,CustomerList,StaffList,CityRead,StaffMMList`,
`Address,District,PostalCode,Phone,Location,Customer List,Staff List,City,Staff List`,
`1013 Tabuk Boulevard,West Bengali,96203,158399646978,[object Object],2,,Kanchrapara,`,
`1892 Nabereznyje Telny Lane,Tutuila,28396,478229987054,[object Object],2,,Tafuna,`,
`1993 Tabuk Lane,Tamil Nadu,64221,648482415405,[object Object],2,,Tambaram,`,
@ -231,7 +231,7 @@ export const genTest = (apiType, dbType) => {
const verifyCsv = (retrievedRecords) => {
// expected output, statically configured
let storedRecords = [
`Address,District,PostalCode,Phone,Location,CustomerList,StaffList,CityRead,StaffMMList`,
`Address,District,PostalCode,Phone,Location,Customer List,Staff List,City,Staff List`,
`1993 Tabuk Lane,Tamil Nadu,64221,648482415405,[object Object],2,,Tambaram,`,
`1661 Abha Drive,Tamil Nadu,14400,270456873752,[object Object],1,,Pudukkottai,`,
];
@ -269,8 +269,8 @@ export const genTest = (apiType, dbType) => {
// verify column headers
cy.get('[data-col="Customer List"]').should("exist");
cy.get('[data-col="Staff List"]').should("exist");
cy.get('[data-col="CityRead"]').should("exist");
cy.get('[data-col="StaffMMList"]').should("exist");
cy.get('[data-col="City"]').should("exist");
cy.get('[data-col="Staff List"]').should("exist");
// has many field validation
mainPage
@ -308,17 +308,17 @@ export const genTest = (apiType, dbType) => {
it(`Share GRID view : Virtual column validation > belongs to`, () => {
// belongs to field validation
mainPage
.getCell("CityRead", 1)
.getCell("City", 1)
.click()
.find("button.mdi-close-thick")
.should("not.exist");
mainPage
.getCell("CityRead", 1)
.getCell("City", 1)
.click()
.find("button.mdi-arrow-expand")
.should("not.exist");
mainPage
.getCell("CityRead", 1)
.getCell("City", 1)
.find(".v-chip")
.contains("Kanchrapara")
.should("exist");
@ -327,17 +327,17 @@ export const genTest = (apiType, dbType) => {
it(`Share GRID view : Virtual column validation > many to many`, () => {
// many-to-many field validation
mainPage
.getCell("StaffMMList", 1)
.getCell("Staff List", 1)
.click()
.find("button.mdi-close-thick")
.should("not.exist");
mainPage
.getCell("StaffMMList", 1)
.getCell("Staff List", 1)
.click()
.find("button.mdi-plus")
.should("not.exist");
mainPage
.getCell("StaffMMList", 1)
.getCell("Staff List", 1)
.click()
.find("button.mdi-arrow-expand")
.click();

18
scripts/cypress/integration/common/4f_pg_grid_view_share.js

@ -228,7 +228,7 @@ export const genTest = (apiType, dbType) => {
const verifyCsv = (retrievedRecords) => {
// expected output, statically configured
let storedRecords = [
`Address,District,PostalCode,Phone,Location,CustomerList,StaffList,CityRead,StaffMMList`,
`Address,District,PostalCode,Phone,Location,Customer List,Staff List,City,Staff List`,
`1888 Kabul Drive,,20936,,1,,Ife,,`,
`1661 Abha Drive,,14400,,1,,Pudukkottai,,`,
];
@ -264,8 +264,8 @@ export const genTest = (apiType, dbType) => {
// verify column headers
cy.get('[data-col="Customer List"]').should("exist");
cy.get('[data-col="Staff List"]').should("exist");
cy.get('[data-col="CityRead"]').should("exist");
cy.get('[data-col="StaffMMList"]').should("exist");
cy.get('[data-col="City"]').should("exist");
cy.get('[data-col="Staff List"]').should("exist");
// has many field validation
mainPage
@ -303,17 +303,17 @@ export const genTest = (apiType, dbType) => {
it(`Share GRID view : Virtual column validation > belongs to`, () => {
// belongs to field validation
mainPage
.getCell("CityRead", 1)
.getCell("City", 1)
.click()
.find("button.mdi-close-thick")
.should("not.exist");
mainPage
.getCell("CityRead", 1)
.getCell("City", 1)
.click()
.find("button.mdi-arrow-expand")
.should("not.exist");
mainPage
.getCell("CityRead", 1)
.getCell("City", 1)
.find(".v-chip")
.contains("al-Ayn")
.should("exist");
@ -322,17 +322,17 @@ export const genTest = (apiType, dbType) => {
it(`Share GRID view : Virtual column validation > many to many`, () => {
// many-to-many field validation
mainPage
.getCell("StaffMMList", 1)
.getCell("Staff List", 1)
.click()
.find("button.mdi-close-thick")
.should("not.exist");
mainPage
.getCell("StaffMMList", 1)
.getCell("Staff List", 1)
.click()
.find("button.mdi-plus")
.should("not.exist");
mainPage
.getCell("StaffMMList", 1)
.getCell("Staff List", 1)
.click()
.find("button.mdi-arrow-expand")
.click();

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

@ -209,7 +209,7 @@ export const genTest = (apiType, dbType) => {
const verifyCsv = (retrievedRecords) => {
// expected output, statically configured
let storedRecords = [
`City,AddressList,CountryRead`,
`City,Address List,Country`,
`A Corua (La Corua),939 Probolinggo Loop,Spain`,
`Abha,733 Mandaluyong Place,Saudi Arabia`,
`Abu Dhabi,535 Ahmadnagar Manor,United Arab Emirates`,

23
scripts/sdk/swagger.json

@ -5107,7 +5107,7 @@
]
}
},
"/api/v1/db/meta/nocodb/version": {
"/api/v1/version": {
"parameters": [],
"get": {
"summary": "",
@ -5128,6 +5128,27 @@
"description": ""
}
},
"/api/v1/health": {
"parameters": [],
"get": {
"summary": "",
"operationId": "utils-app-health",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {}
}
}
}
},
"tags": [
"Utils"
],
"description": ""
}
},
"/api/v1/db/meta/cache": {
"get": {
"summary": "Your GET endpoint",

Loading…
Cancel
Save