Browse Source

Merge commit '78efdfa0286f390ca276c7fe1713d3109f19b822' into NCDBOSS-39

pull/5419/head
gitstart 2 years ago
parent
commit
0636ea46f3
  1. 16
      packages/nc-gui/components/template/Editor.vue
  2. 3
      packages/nc-gui/components/template/utils.ts
  3. 8
      packages/nc-gui/composables/useGlobal/actions.ts
  4. 18
      packages/nc-gui/lang/zh-Hans.json
  5. 12
      packages/nocodb/package-lock.json
  6. 10
      packages/nocodb/src/lib/controllers/user/user.ctl.ts
  7. 13
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
  8. 40
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts
  9. 6
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/pg.ts
  10. 23
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/convertDateFormat.ts
  11. 45
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/formulaFnHelper.ts
  12. 4
      packages/nocodb/src/lib/plugins/vultr/Vultr.ts
  13. 16
      packages/nocodb/src/lib/plugins/vultr/index.ts
  14. 15
      packages/nocodb/src/lib/services/user/index.ts

16
packages/nc-gui/components/template/Editor.vue

@ -3,6 +3,7 @@ import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc' import utc from 'dayjs/plugin/utc'
import type { ColumnType, TableType } from 'nocodb-sdk' import type { ColumnType, TableType } from 'nocodb-sdk'
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface'
import { srcDestMappingColumns, tableColumns } from './utils' import { srcDestMappingColumns, tableColumns } from './utils'
import { import {
Empty, Empty,
@ -87,6 +88,8 @@ const isImporting = ref(false)
const importingTips = ref<Record<string, string>>({}) const importingTips = ref<Record<string, string>>({})
const checkAllRecord = ref<boolean[]>([])
const uiTypeOptions = ref<Option[]>( const uiTypeOptions = ref<Option[]>(
(Object.keys(UITypes) as (keyof typeof UITypes)[]) (Object.keys(UITypes) as (keyof typeof UITypes)[])
.filter( .filter(
@ -615,6 +618,13 @@ function handleEditableTnChange(idx: number) {
function isSelectDisabled(uidt: string, disableSelect = false) { function isSelectDisabled(uidt: string, disableSelect = false) {
return (uidt === UITypes.SingleSelect || uidt === UITypes.MultiSelect) && disableSelect return (uidt === UITypes.SingleSelect || uidt === UITypes.MultiSelect) && disableSelect
} }
function handleCheckAllRecord(event: CheckboxChangeEvent, tableName: string) {
const isChecked = event.target.checked
for (const record of srcDestMapping.value[tableName]) {
record.enabled = isChecked
}
}
</script> </script>
<template> <template>
@ -671,6 +681,12 @@ function isSelectDisabled(uidt: string, disableSelect = false) {
<span v-if="column.key === 'source_column' || column.key === 'destination_column'"> <span v-if="column.key === 'source_column' || column.key === 'destination_column'">
{{ column.name }} {{ column.name }}
</span> </span>
<span v-if="column.key === 'action'">
<a-checkbox
v-model:checked="checkAllRecord[table.table_name]"
@change="handleCheckAllRecord($event, table.table_name)"
/>
</span>
</template> </template>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">

3
packages/nc-gui/components/template/utils.ts

@ -40,6 +40,7 @@ export const srcDestMappingColumns: (Omit<ColumnGroupType<any>, 'children'> & {
{ {
name: 'Action', name: 'Action',
key: 'action', key: 'action',
align: 'right', align: 'center',
width: 50,
}, },
] ]

8
packages/nc-gui/composables/useGlobal/actions.ts

@ -46,11 +46,13 @@ export function useGlobalActions(state: State): Actions {
signIn(response.data.token) signIn(response.data.token)
} }
}) })
.catch(async (err) => { .catch(async () => {
message.error(err.message || t('msg.error.youHaveBeenSignedOut')) if (state.token.value && state.user.value) {
await signOut() await signOut()
message.error(t('msg.error.youHaveBeenSignedOut'))
}
}) })
.finally(() => resolve()) .finally(() => resolve(true))
}) })
} }

18
packages/nc-gui/lang/zh-Hans.json

@ -221,7 +221,7 @@
"viewName": "查看名称", "viewName": "查看名称",
"viewLink": "查看链接", "viewLink": "查看链接",
"columnName": "列名", "columnName": "列名",
"columnToScanFor": "Column to scan", "columnToScanFor": "要扫描的列",
"columnType": "列类型", "columnType": "列类型",
"roleName": "权限组", "roleName": "权限组",
"roleDescription": "权限描述", "roleDescription": "权限描述",
@ -412,8 +412,8 @@
"changePwd": "更改密码", "changePwd": "更改密码",
"createView": "创建视图", "createView": "创建视图",
"shareView": "分享视图", "shareView": "分享视图",
"findRowByCodeScan": "Find row by scan", "findRowByCodeScan": "通过扫描查找行",
"fillByCodeScan": "Fill by scan", "fillByCodeScan": "通过扫描填充",
"listSharedView": "共享视图列表", "listSharedView": "共享视图列表",
"ListView": "视图列表", "ListView": "视图列表",
"copyView": "复制视图", "copyView": "复制视图",
@ -430,7 +430,7 @@
"iFrame": "复制可嵌入的 HTML 代码", "iFrame": "复制可嵌入的 HTML 代码",
"addWebhook": "添加新的 Webhook", "addWebhook": "添加新的 Webhook",
"enableWebhook": "启用 Webhook", "enableWebhook": "启用 Webhook",
"testWebhook": "Test Webhook", "testWebhook": "测试Webhook",
"copyWebhook": "复制 Webhook", "copyWebhook": "复制 Webhook",
"deleteWebhook": "删除 Webhook", "deleteWebhook": "删除 Webhook",
"newToken": "添加新 Token", "newToken": "添加新 Token",
@ -470,7 +470,7 @@
"addOrEditStack": "添加/编辑分类标签" "addOrEditStack": "添加/编辑分类标签"
}, },
"map": { "map": {
"mappedBy": "Mapped By", "mappedBy": "映射字段",
"chooseMappingField": "选择映射字段", "chooseMappingField": "选择映射字段",
"openInGoogleMaps": "谷歌地图", "openInGoogleMaps": "谷歌地图",
"openInOpenStreetMap": "OSM" "openInOpenStreetMap": "OSM"
@ -542,15 +542,15 @@
"orgViewer": "游客不能创建新项目,仅允许访问受邀项目。" "orgViewer": "游客不能创建新项目,仅允许访问受邀项目。"
}, },
"codeScanner": { "codeScanner": {
"loadingScanner": "Loading the scanner...", "loadingScanner": "正在加载扫描仪...",
"selectColumn": "Select a column (QR code or Barcode) that you want to use for finding a row by scanning.", "selectColumn": "选择要用于扫描查找行的列(二维码或条形码)。",
"moreThanOneRowFoundForCode": "More than one row found for this code. Currently only unique codes are supported.", "moreThanOneRowFoundForCode": "找到了多行此代码。目前只支持唯一的代码。",
"noRowFoundForCode": "所选列没有找到此代码行" "noRowFoundForCode": "所选列没有找到此代码行"
}, },
"map": { "map": {
"overLimit": "你已经超出了限制。", "overLimit": "你已经超出了限制。",
"closeLimit": "您已接近上限。", "closeLimit": "您已接近上限。",
"limitNumber": "The limit of markers shown in a Map View is 1000 records." "limitNumber": "在地图视图中显示的标记的限制是1000条记录。"
}, },
"footerInfo": "每页行驶", "footerInfo": "每页行驶",
"upload": "选择文件以上传", "upload": "选择文件以上传",

12
packages/nocodb/package-lock.json generated

@ -17364,9 +17364,9 @@
"dev": true "dev": true
}, },
"node_modules/vm2": { "node_modules/vm2": {
"version": "3.9.15", "version": "3.9.16",
"resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.15.tgz", "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.16.tgz",
"integrity": "sha512-XqNqknHGw2avJo13gbIwLNZUumvrSHc9mLqoadFZTpo3KaNEJoe1I0lqTFhRXmXD7WkLyG01aaraXdXT0pa4ag==", "integrity": "sha512-3T9LscojNTxdOyG+e8gFeyBXkMlOBYDoF6dqZbj+MPVHi9x10UfiTAJIobuchRCp3QvC+inybTbMJIUrLsig0w==",
"dependencies": { "dependencies": {
"acorn": "^8.7.0", "acorn": "^8.7.0",
"acorn-walk": "^8.2.0" "acorn-walk": "^8.2.0"
@ -32786,9 +32786,9 @@
"dev": true "dev": true
}, },
"vm2": { "vm2": {
"version": "3.9.15", "version": "3.9.16",
"resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.15.tgz", "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.16.tgz",
"integrity": "sha512-XqNqknHGw2avJo13gbIwLNZUumvrSHc9mLqoadFZTpo3KaNEJoe1I0lqTFhRXmXD7WkLyG01aaraXdXT0pa4ag==", "integrity": "sha512-3T9LscojNTxdOyG+e8gFeyBXkMlOBYDoF6dqZbj+MPVHi9x10UfiTAJIobuchRCp3QvC+inybTbMJIUrLsig0w==",
"requires": { "requires": {
"acorn": "^8.7.0", "acorn": "^8.7.0",
"acorn-walk": "^8.2.0" "acorn-walk": "^8.2.0"

10
packages/nocodb/src/lib/controllers/user/user.ctl.ts

@ -98,6 +98,15 @@ async function signin(req, res, next) {
)(req, res, next); )(req, res, next);
} }
async function signout(req: Request<any, any>, res): Promise<any> {
res.json(
await userService.signout({
req,
res,
})
);
}
async function googleSignin(req, res, next) { async function googleSignin(req, res, next) {
passport.authenticate( passport.authenticate(
'google', 'google',
@ -246,6 +255,7 @@ const mapRoutes = (router) => {
// new API // new API
router.post('/api/v1/auth/user/signup', catchError(signup)); router.post('/api/v1/auth/user/signup', catchError(signup));
router.post('/api/v1/auth/user/signin', catchError(signin)); router.post('/api/v1/auth/user/signin', catchError(signin));
router.post('/api/v1/auth/user/signout', catchError(signout));
router.get( router.get(
'/api/v1/auth/user/me', '/api/v1/auth/user/me',
extractProjectIdAndAuthenticate, extractProjectIdAndAuthenticate,

13
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts

@ -3242,17 +3242,24 @@ function extractCondition(nestedArrayConditions, aliasColObjMap) {
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let [logicOp, alias, op, value] = let [logicOp, alias, op, value] =
str.match(/(?:~(and|or|not))?\((.*?),(\w+),(.*)\)/)?.slice(1) || []; str.match(/(?:~(and|or|not))?\((.*?),(\w+),(.*)\)/)?.slice(1) || [];
if (!alias && !op && !value) {
// try match with blank filter format
[logicOp, alias, op, value] =
str.match(/(?:~(and|or|not))?\((.*?),(\w+)\)/)?.slice(1) || [];
}
let sub_op = null; let sub_op = null;
if (aliasColObjMap[alias]) { if (aliasColObjMap[alias]) {
if ( if (
[UITypes.Date, UITypes.DateTime].includes(aliasColObjMap[alias].uidt) [UITypes.Date, UITypes.DateTime].includes(aliasColObjMap[alias].uidt)
) { ) {
value = value.split(','); value = value?.split(',');
// the first element would be sub_op // the first element would be sub_op
sub_op = value[0]; sub_op = value?.[0];
// remove the first element which is sub_op // remove the first element which is sub_op
value.shift(); value?.shift();
value = value?.[0];
} else if (op === 'in') { } else if (op === 'in') {
value = value.split(','); value = value.split(',');
} }

40
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts

@ -3,11 +3,14 @@ import { jsepCurlyHook, UITypes } from 'nocodb-sdk';
import mapFunctionName from '../mapFunctionName'; import mapFunctionName from '../mapFunctionName';
import genRollupSelectv2 from '../genRollupSelectv2'; import genRollupSelectv2 from '../genRollupSelectv2';
import FormulaColumn from '../../../../../models/FormulaColumn'; import FormulaColumn from '../../../../../models/FormulaColumn';
import { validateDateWithUnknownFormat } from '../helpers/formulaFnHelper'; import {
convertDateFormatForConcat,
validateDateWithUnknownFormat,
} from '../helpers/formulaFnHelper';
import { CacheGetType, CacheScope } from '../../../../../utils/globals'; import { CacheGetType, CacheScope } from '../../../../../utils/globals';
import NocoCache from '../../../../../cache/NocoCache'; import NocoCache from '../../../../../cache/NocoCache';
import type Model from '../../../../../models/Model';
import type Column from '../../../../../models/Column'; import type Column from '../../../../../models/Column';
import type Model from '../../../../../models/Model';
import type RollupColumn from '../../../../../models/RollupColumn'; import type RollupColumn from '../../../../../models/RollupColumn';
import type { XKnex } from '../../../index'; import type { XKnex } from '../../../index';
import type LinkToAnotherRecordColumn from '../../../../../models/LinkToAnotherRecordColumn'; import type LinkToAnotherRecordColumn from '../../../../../models/LinkToAnotherRecordColumn';
@ -633,8 +636,19 @@ async function _formulaQueryBuilder(
`${pt.callee.name}(${( `${pt.callee.name}(${(
await Promise.all( await Promise.all(
pt.arguments.map(async (arg) => { pt.arguments.map(async (arg) => {
const query = (await fn(arg)).builder.toQuery(); let query = (await fn(arg)).builder.toQuery();
if (pt.callee.name === 'CONCAT') { if (pt.callee.name === 'CONCAT') {
if (knex.clientType() !== 'sqlite3') {
query = await convertDateFormatForConcat(
arg,
columnIdToUidt,
query,
knex.clientType()
);
} else {
// sqlite3: special handling - See BinaryExpression
}
if (knex.clientType() === 'mysql2') { if (knex.clientType() === 'mysql2') {
// mysql2: CONCAT() returns NULL if any argument is NULL. // mysql2: CONCAT() returns NULL if any argument is NULL.
// adding IFNULL to convert NULL values to empty strings // adding IFNULL to convert NULL values to empty strings
@ -679,8 +693,8 @@ async function _formulaQueryBuilder(
pt.left.fnName = pt.left.fnName || 'ARITH'; pt.left.fnName = pt.left.fnName || 'ARITH';
pt.right.fnName = pt.right.fnName || 'ARITH'; pt.right.fnName = pt.right.fnName || 'ARITH';
const left = (await fn(pt.left, null, pt.operator)).builder.toQuery(); let left = (await fn(pt.left, null, pt.operator)).builder.toQuery();
const right = (await fn(pt.right, null, pt.operator)).builder.toQuery(); let right = (await fn(pt.right, null, pt.operator)).builder.toQuery();
let sql = `${left} ${pt.operator} ${right}${colAlias}`; let sql = `${left} ${pt.operator} ${right}${colAlias}`;
// comparing a date with empty string would throw // comparing a date with empty string would throw
@ -724,8 +738,22 @@ async function _formulaQueryBuilder(
} }
} }
// handle NULL values when calling CONCAT for sqlite3
if (pt.left.fnName === 'CONCAT' && knex.clientType() === 'sqlite3') { if (pt.left.fnName === 'CONCAT' && knex.clientType() === 'sqlite3') {
// handle date format
left = await convertDateFormatForConcat(
pt.left?.arguments?.[0],
columnIdToUidt,
left,
knex.clientType()
);
right = await convertDateFormatForConcat(
pt.right?.arguments?.[0],
columnIdToUidt,
right,
knex.clientType()
);
// handle NULL values when calling CONCAT for sqlite3
sql = `COALESCE(${left}, '') ${pt.operator} COALESCE(${right},'')${colAlias}`; sql = `COALESCE(${left}, '') ${pt.operator} COALESCE(${right},'')${colAlias}`;
} }

6
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/pg.ts

@ -17,9 +17,9 @@ const pg = {
builder: args.knex.raw( builder: args.knex.raw(
`POSITION(${args.knex.raw( `POSITION(${args.knex.raw(
(await args.fn(args.pt.arguments[1])).builder.toQuery() (await args.fn(args.pt.arguments[1])).builder.toQuery()
)} in ${args.knex )} in ${args.knex.raw(
.raw((await args.fn(args.pt.arguments[0])).builder) (await args.fn(args.pt.arguments[0])).builder.toQuery()
.toQuery()})${args.colAlias}` )})${args.colAlias}`
), ),
}; };
}, },

23
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/convertDateFormat.ts

@ -0,0 +1,23 @@
export function convertDateFormat(date_format: string, type: string) {
if (date_format === 'YYYY-MM-DD') {
if (type === 'mysql2' || type === 'sqlite3') return '%Y-%m-%d';
} else if (date_format === 'YYYY/MM/DD') {
if (type === 'mysql2' || type === 'sqlite3') return '%Y/%m/%d';
} else if (date_format === 'DD-MM-YYYY') {
if (type === 'mysql2' || type === 'sqlite3') return '%d/%m/%Y';
} else if (date_format === 'MM-DD-YYYY') {
if (type === 'mysql2' || type === 'sqlite3') return '%d-%m-%Y';
} else if (date_format === 'DD/MM/YYYY') {
if (type === 'mysql2' || type === 'sqlite3') return '%d/%m/%Y';
} else if (date_format === 'MM/DD/YYYY') {
if (type === 'mysql2' || type === 'sqlite3') return '%m-%d-%Y';
} else if (date_format === 'DD MM YYYY') {
if (type === 'mysql2' || type === 'sqlite3') return '%d %m %Y';
} else if (date_format === 'MM DD YYYY') {
if (type === 'mysql2' || type === 'sqlite3') return '%m %d %Y';
} else if (date_format === 'YYYY MM DD') {
if (type === 'mysql2' || type === 'sqlite3') return '%Y %m %d';
}
// pg / mssql
return date_format;
}

45
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/formulaFnHelper.ts

@ -1,5 +1,8 @@
import dayjs, { extend } from 'dayjs'; import dayjs, { extend } from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat.js'; import customParseFormat from 'dayjs/plugin/customParseFormat.js';
import { UITypes } from 'nocodb-sdk';
import Column from '../../../../../models/Column';
import { convertDateFormat } from './convertDateFormat';
extend(customParseFormat); extend(customParseFormat);
export function getWeekdayByText(v: string) { export function getWeekdayByText(v: string) {
@ -50,3 +53,45 @@ export function validateDateWithUnknownFormat(v: string) {
} }
return false; return false;
} }
export async function convertDateFormatForConcat(
o,
columnIdToUidt,
query,
clientType
) {
if (
o?.type === 'Identifier' &&
o?.name in columnIdToUidt &&
columnIdToUidt[o.name] === UITypes.Date
) {
const meta = (
await Column.get({
colId: o.name,
})
).meta;
if (clientType === 'mysql2') {
query = `DATE_FORMAT(${query}, '${convertDateFormat(
meta.date_format,
clientType
)}')`;
} else if (clientType === 'pg') {
query = `TO_CHAR(${query}, '${convertDateFormat(
meta.date_format,
clientType
)}')`;
} else if (clientType === 'sqlite3') {
query = `strftime('${convertDateFormat(
meta.date_format,
clientType
)}', ${query})`;
} else if (clientType === 'mssql') {
query = `FORMAT(${query}, '${convertDateFormat(
meta.date_format,
clientType
)}')`;
}
}
return query;
}

4
packages/nocodb/src/lib/plugins/vultr/Vultr.ts

@ -105,9 +105,7 @@ export default class Vultr implements IStorageAdapterV2 {
s3Options.accessKeyId = this.input.access_key; s3Options.accessKeyId = this.input.access_key;
s3Options.secretAccessKey = this.input.access_secret; s3Options.secretAccessKey = this.input.access_secret;
s3Options.endpoint = new AWS.Endpoint( s3Options.endpoint = new AWS.Endpoint(this.input.hostname);
`s3.${this.input.region}.cloud.ovh.net`
);
this.s3Client = new AWS.S3(s3Options); this.s3Client = new AWS.S3(s3Options);
} }

16
packages/nocodb/src/lib/plugins/vultr/index.ts

@ -5,7 +5,7 @@ import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = { const config: XcPluginConfig = {
builder: VultrPlugin, builder: VultrPlugin,
title: 'Vultr Object Storage', title: 'Vultr Object Storage',
version: '0.0.1', version: '0.0.2',
logo: 'plugins/vultr.png', logo: 'plugins/vultr.png',
description: description:
'Using Vultr Object Storage can give flexibility and cloud storage that allows applications greater flexibility and access worldwide.', 'Using Vultr Object Storage can give flexibility and cloud storage that allows applications greater flexibility and access worldwide.',
@ -20,13 +20,13 @@ const config: XcPluginConfig = {
type: XcType.SingleLineText, type: XcType.SingleLineText,
required: true, required: true,
}, },
// { {
// key: 'region', key: 'hostname',
// label: 'Region', label: 'Host Name',
// placeholder: 'Region', placeholder: 'e.g.: ewr1.vultrobjects.com',
// type: XcType.SingleLineText, type: XcType.SingleLineText,
// required: true required: true
// }, },
{ {
key: 'access_key', key: 'access_key',
label: 'Access Key', label: 'Access Key',

15
packages/nocodb/src/lib/services/user/index.ts

@ -458,5 +458,20 @@ export async function signup(param: {
} as any; } as any;
} }
export async function signout(param: { req: any; res: any }): Promise<any> {
try {
param.res.clearCookie('refresh_token');
const user = (param.req as any).user;
if (user) {
await User.update(user.id, {
refresh_token: null,
});
}
return { msg: 'Signed out successfully' };
} catch (e) {
NcError.badRequest(e.message);
}
}
export * from './helpers'; export * from './helpers';
export { default as initAdminFromEnv } from './initAdminFromEnv'; export { default as initAdminFromEnv } from './initAdminFromEnv';

Loading…
Cancel
Save