Browse Source

Merge branch 'develop' into refactor/timezone-locale

pull/5642/head
Wing-Kam Wong 2 years ago
parent
commit
d703a45cfc
  1. 2
      packages/nc-gui/lang/ru.json
  2. 2
      packages/nc-gui/package-lock.json
  3. 9
      packages/nc-gui/pages/index/index/index.vue
  4. 2
      packages/nc-lib-gui/package.json
  5. 4
      packages/nocodb-sdk/package-lock.json
  6. 2
      packages/nocodb-sdk/package.json
  7. 4
      packages/nocodb-sdk/src/lib/globals.ts
  8. 20
      packages/nocodb/package-lock.json
  9. 4
      packages/nocodb/package.json
  10. 3
      packages/nocodb/src/modules/jobs/export-import/duplicate.controller.ts
  11. 305
      packages/nocodb/src/modules/jobs/export-import/duplicate.processor.ts
  12. 31
      packages/nocodb/src/modules/jobs/export-import/export.service.ts
  13. 504
      packages/nocodb/src/modules/jobs/export-import/import.service.ts
  14. 23
      packages/nocodb/src/modules/jobs/helpers.ts
  15. 2
      tests/playwright/pages/Dashboard/Grid/index.ts
  16. 41
      tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts

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

@ -670,7 +670,7 @@
"showNullInCells": "Показывать NULL в ячейках",
"showNullInCellsDesc": "Отображайте тег 'NULL' в ячейках, содержащих значение NULL. Это помогает отличить ячейки, содержащие строку EMPTY.",
"showNullAndEmptyInFilter": "Показать NULL и EMPTY в фильтре",
"showNullAndEmptyInFilterDesc": "Enable 'additional' filters to differentiate fields containing NULL & Empty Strings. Default support for Blank treats both NULL & Empty strings alike.",
"showNullAndEmptyInFilterDesc": "Включите \"дополнительные\" фильтры для различения полей, содержащих NULL и пустые строки. По умолчанию поддержка Blank одинаково обрабатывает строки NULL и Empty.",
"deleteKanbanStackConfirmation": "Удаление этого стека также удалит опцию выбора `{stackToBeDeleted}` из `{groupingField}`. Записи переместятся в стек без категории.",
"computedFieldEditWarning": "Вычисляемое поле: содержимое доступно только для чтения. Используйте меню редактирования столбцов для изменения конфигурации",
"computedFieldDeleteWarning": "Вычисляемое поле: содержимое доступно только для чтения. Невозможно очистить содержимое.",

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

@ -110,7 +110,7 @@
}
},
"../nocodb-sdk": {
"version": "0.107.0-beta.0",
"version": "0.107.0-beta.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",

9
packages/nc-gui/pages/index/index/index.vue

@ -1,4 +1,5 @@
<script lang="ts" setup>
import { ProjectStatus } from 'nocodb-sdk'
import type { ProjectType } from 'nocodb-sdk'
import tinycolor from 'tinycolor2'
import { breakpointsTailwind } from '@vueuse/core'
@ -161,7 +162,7 @@ const getProjectPrimary = (project: ProjectType) => {
const customRow = (record: ProjectType) => ({
onClick: async () => {
if (record.status !== 'job') await navigateTo(`/nc/${record.id}`)
if (record.status !== ProjectStatus.JOB) await navigateTo(`/nc/${record.id}`)
$e('a:project:open')
},
@ -292,8 +293,8 @@ const copyProjectMeta = async () => {
>
<component
:is="iconMap.reload"
v-if="record.status === 'job'"
:class="{ 'animate-infinite animate-spin text-gray-500': record.status === 'job' }"
v-if="record.status === ProjectStatus.JOB"
:class="{ 'animate-infinite animate-spin text-gray-500': record.status === ProjectStatus.JOB }"
/>
{{ text }}
</div>
@ -304,7 +305,7 @@ const copyProjectMeta = async () => {
<a-table-column key="id" :title="$t('labels.actions')" data-index="id">
<template #default="{ text, record }">
<div v-if="record.status !== 'job'" class="flex items-center gap-2">
<div v-if="record.status !== ProjectStatus.JOB" class="flex items-center gap-2">
<component
:is="iconMap.edit"
v-e="['c:project:edit:rename']"

2
packages/nc-lib-gui/package.json

@ -1,6 +1,6 @@
{
"name": "nc-lib-gui",
"version": "0.107.0-beta.0",
"version": "0.107.0-beta.1",
"description": "NocoDB GUI",
"author": {
"name": "NocoDB",

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

@ -1,12 +1,12 @@
{
"name": "nocodb-sdk",
"version": "0.107.0-beta.0",
"version": "0.107.0-beta.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb-sdk",
"version": "0.107.0-beta.0",
"version": "0.107.0-beta.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",

2
packages/nocodb-sdk/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb-sdk",
"version": "0.107.0-beta.0",
"version": "0.107.0-beta.1",
"description": "NocoDB SDK",
"main": "build/main/index.js",
"typings": "build/main/index.d.ts",

4
packages/nocodb-sdk/src/lib/globals.ts

@ -72,3 +72,7 @@ export enum ModelTypes {
TABLE = 'table',
VIEW = 'view',
}
export enum ProjectStatus {
JOB = 'job',
}

20
packages/nocodb/package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "nocodb",
"version": "0.107.0-beta.0",
"version": "0.107.0-beta.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb",
"version": "0.107.0-beta.0",
"version": "0.107.0-beta.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@google-cloud/storage": "^5.7.2",
@ -80,7 +80,7 @@
"mysql2": "^3.2.0",
"nanoid": "^3.1.20",
"nc-help": "^0.2.87",
"nc-lib-gui": "0.107.0-beta.0",
"nc-lib-gui": "0.107.0-beta.1",
"nc-plugin": "^0.1.3",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",
@ -190,7 +190,7 @@
}
},
"../nocodb-sdk": {
"version": "0.107.0-beta.0",
"version": "0.107.0-beta.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@ -13157,9 +13157,9 @@
}
},
"node_modules/nc-lib-gui": {
"version": "0.107.0-beta.0",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.107.0-beta.0.tgz",
"integrity": "sha512-MyfFETnqNwDtkIbu9NRhNOXsLz0M/8wMGeP92ei8oodr0m++zwdM6lcpd6KeCz5AO+sNWQDMwN8MnWh+12dEQg==",
"version": "0.107.0-beta.1",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.107.0-beta.1.tgz",
"integrity": "sha512-xP069IpkCMrOCTKAGOWf5/GhnMVm+wqZflgt7G5CSWF3A46v5pL5SYj1yKK8HUN0v2ZVP2Agjzp44RZBv4QqqA==",
"dependencies": {
"express": "^4.17.1"
}
@ -28442,9 +28442,9 @@
}
},
"nc-lib-gui": {
"version": "0.107.0-beta.0",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.107.0-beta.0.tgz",
"integrity": "sha512-MyfFETnqNwDtkIbu9NRhNOXsLz0M/8wMGeP92ei8oodr0m++zwdM6lcpd6KeCz5AO+sNWQDMwN8MnWh+12dEQg==",
"version": "0.107.0-beta.1",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.107.0-beta.1.tgz",
"integrity": "sha512-xP069IpkCMrOCTKAGOWf5/GhnMVm+wqZflgt7G5CSWF3A46v5pL5SYj1yKK8HUN0v2ZVP2Agjzp44RZBv4QqqA==",
"requires": {
"express": "^4.17.1"
}

4
packages/nocodb/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb",
"version": "0.107.0-beta.0",
"version": "0.107.0-beta.1",
"description": "NocoDB Backend",
"main": "dist/bundle.js",
"author": {
@ -113,7 +113,7 @@
"mysql2": "^3.2.0",
"nanoid": "^3.1.20",
"nc-help": "^0.2.87",
"nc-lib-gui": "0.107.0-beta.0",
"nc-lib-gui": "0.107.0-beta.1",
"nc-plugin": "^0.1.3",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",

3
packages/nocodb/src/modules/jobs/export-import/duplicate.controller.ts

@ -7,6 +7,7 @@ import {
Request,
UseGuards,
} from '@nestjs/common';
import { ProjectStatus } from 'nocodb-sdk';
import { GlobalGuard } from '../../../guards/global/global.guard';
import {
Acl,
@ -62,7 +63,7 @@ export class DuplicateController {
);
const dupProject = await this.projectsService.projectCreate({
project: { title: uniqueTitle, status: 'job' },
project: { title: uniqueTitle, status: ProjectStatus.JOB },
user: { id: req.user.id },
});

305
packages/nocodb/src/modules/jobs/export-import/duplicate.processor.ts

@ -3,41 +3,22 @@ import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';
import papaparse from 'papaparse';
import { UITypes } from 'nocodb-sdk';
import { Logger } from '@nestjs/common';
import { Base, Column, Model, Project } from '../../../models';
import { ProjectsService } from '../../../services/projects.service';
import { findWithIdentifier } from '../../../helpers/exportImportHelpers';
import { BulkDataAliasService } from '../../../services/bulk-data-alias.service';
import { JOBS_QUEUE, JobTypes } from '../../../interface/Jobs';
import { elapsedTime, initTime } from '../helpers';
import { ExportService } from './export.service';
import { ImportService } from './import.service';
import type { LinkToAnotherRecordColumn } from '../../../models';
const DEBUG = false;
const debugLog = function (...args: any[]) {
if (DEBUG) {
console.log(...args);
}
};
const initTime = function () {
return {
hrTime: process.hrtime(),
};
};
const elapsedTime = function (
time: { hrTime: [number, number] },
label?: string,
) {
const elapsedS = process.hrtime(time.hrTime)[0].toFixed(3);
const elapsedMs = process.hrtime(time.hrTime)[1] / 1000000;
if (label) debugLog(`${label}: ${elapsedS}s ${elapsedMs}ms`);
time.hrTime = process.hrtime();
};
@Processor(JOBS_QUEUE)
export class DuplicateProcessor {
private readonly logger = new Logger(
`${JOBS_QUEUE}:${DuplicateProcessor.name}`,
);
constructor(
private readonly exportService: ExportService,
private readonly importService: ImportService,
@ -77,7 +58,11 @@ export class DuplicateProcessor {
excludeHooks,
});
elapsedTime(hrTime, 'serializeModels');
elapsedTime(
hrTime,
`serialize models schema for ${base.project_id}::${base.id}`,
'duplicateBase',
);
if (!exportedModels) {
throw new Error(`Export failed for base '${base.id}'`);
@ -95,7 +80,7 @@ export class DuplicateProcessor {
req: req,
});
elapsedTime(hrTime, 'importModels');
elapsedTime(hrTime, `import models schema`, 'duplicateBase');
if (!idMap) {
throw new Error(`Import failed for base '${base.id}'`);
@ -166,7 +151,11 @@ export class DuplicateProcessor {
})
)[0];
elapsedTime(hrTime, 'serializeModel');
elapsedTime(
hrTime,
`serialize model schema for ${modelId}`,
'duplicateModel',
);
if (!exportedModel) {
throw new Error(`Export failed for base '${base.id}'`);
@ -184,7 +173,7 @@ export class DuplicateProcessor {
externalModels: relatedModels,
});
elapsedTime(hrTime, 'reimportModelSchema');
elapsedTime(hrTime, 'import model schema', 'duplicateModel');
if (!idMap) {
throw new Error(`Import failed for model '${modelId}'`);
@ -220,7 +209,7 @@ export class DuplicateProcessor {
externalModels: relatedModels,
});
elapsedTime(hrTime, 'reimportModelData');
elapsedTime(hrTime, 'import model data', 'duplicateModel');
}
return await Model.get(findWithIdentifier(idMap, sourceModel.id));
@ -247,28 +236,7 @@ export class DuplicateProcessor {
externalModels,
} = param;
const handledLinks = [];
const lChunks: Record<string, any[]> = {}; // fk_mm_model_id: { rowId, childId }[]
const insertChunks = async () => {
for (const [k, v] of Object.entries(lChunks)) {
try {
if (v.length === 0) continue;
await this.bulkDataService.bulkDataInsert({
projectName: destProject.id,
tableName: k,
body: v,
cookie: null,
chunkSize: 1000,
foreign_key_checks: false,
raw: true,
});
lChunks[k] = [];
} catch (e) {
console.log(e);
}
}
};
let handledLinks = [];
for (const sourceModel of sourceModels) {
const dataStream = new Readable({
@ -279,7 +247,7 @@ export class DuplicateProcessor {
read() {},
});
this.exportService.streamModelData({
this.exportService.streamModelDataAsCsv({
dataStream,
linkStream,
projectId: sourceProject.id,
@ -287,181 +255,29 @@ export class DuplicateProcessor {
handledMmList: handledLinks,
});
const headers: string[] = [];
let chunk = [];
const model = await Model.get(findWithIdentifier(idMap, sourceModel.id));
await new Promise((resolve) => {
papaparse.parse(dataStream, {
newline: '\r\n',
step: async (results, parser) => {
if (!headers.length) {
parser.pause();
for (const header of results.data) {
const id = idMap.get(header);
if (id) {
const col = await Column.get({
base_id: destBase.id,
colId: id,
});
if (col.colOptions?.type === 'bt') {
const childCol = await Column.get({
base_id: destBase.id,
colId: col.colOptions.fk_child_column_id,
});
headers.push(childCol.column_name);
} else {
headers.push(col.column_name);
}
} else {
debugLog('header not found', header);
}
}
parser.resume();
} else {
if (results.errors.length === 0) {
const row = {};
for (let i = 0; i < headers.length; i++) {
if (results.data[i] !== '') {
row[headers[i]] = results.data[i];
}
}
chunk.push(row);
if (chunk.length > 1000) {
parser.pause();
try {
await this.bulkDataService.bulkDataInsert({
projectName: destProject.id,
tableName: model.id,
body: chunk,
cookie: null,
chunkSize: chunk.length + 1,
foreign_key_checks: false,
raw: true,
});
} catch (e) {
console.log(e);
}
chunk = [];
parser.resume();
}
}
}
},
complete: async () => {
if (chunk.length > 0) {
try {
await this.bulkDataService.bulkDataInsert({
projectName: destProject.id,
tableName: model.id,
body: chunk,
cookie: null,
chunkSize: chunk.length + 1,
foreign_key_checks: false,
raw: true,
});
} catch (e) {
console.log(e);
}
chunk = [];
}
resolve(null);
},
});
await this.importService.importDataFromCsvStream({
idMap,
dataStream,
destProject,
destBase,
destModel: model,
});
let headersFound = false;
let childIndex = -1;
let parentIndex = -1;
let columnIndex = -1;
const mmColumns: Record<string, Column> = {};
const mmParentChild: any = {};
await new Promise((resolve) => {
papaparse.parse(linkStream, {
newline: '\r\n',
step: async (results, parser) => {
if (!headersFound) {
for (const [i, header] of Object.entries(results.data)) {
if (header === 'child') {
childIndex = parseInt(i);
} else if (header === 'parent') {
parentIndex = parseInt(i);
} else if (header === 'column') {
columnIndex = parseInt(i);
}
}
headersFound = true;
} else {
if (results.errors.length === 0) {
const child = results.data[childIndex];
const parent = results.data[parentIndex];
const columnId = results.data[columnIndex];
if (child && parent && columnId) {
if (mmColumns[columnId]) {
// push to chunk
const mmModelId =
mmColumns[columnId].colOptions.fk_mm_model_id;
const mm = mmParentChild[mmModelId];
lChunks[mmModelId].push({
[mm.parent]: parent,
[mm.child]: child,
});
} else {
// get column for the first time
parser.pause();
await insertChunks();
const col = await Column.get({
base_id: destBase.id,
colId: findWithIdentifier(idMap, columnId),
});
const colOptions =
await col.getColOptions<LinkToAnotherRecordColumn>();
const vChildCol = await colOptions.getMMChildColumn();
const vParentCol = await colOptions.getMMParentColumn();
mmParentChild[col.colOptions.fk_mm_model_id] = {
parent: vParentCol.column_name,
child: vChildCol.column_name,
};
mmColumns[columnId] = col;
handledLinks.push(col.colOptions.fk_mm_model_id);
const mmModelId = col.colOptions.fk_mm_model_id;
// create chunk
lChunks[mmModelId] = [];
// push to chunk
const mm = mmParentChild[mmModelId];
lChunks[mmModelId].push({
[mm.parent]: parent,
[mm.child]: child,
});
parser.resume();
}
}
}
}
},
complete: async () => {
await insertChunks();
resolve(null);
},
});
handledLinks = await this.importService.importLinkFromCsvStream({
idMap,
linkStream,
destProject,
destBase,
handledLinks,
});
elapsedTime(hrTime, model.title);
elapsedTime(
hrTime,
`import data and links for ${model.title}`,
'importModelsData',
);
}
// update external models (has bt to this model)
@ -479,7 +295,7 @@ export class DuplicateProcessor {
read() {},
});
this.exportService.streamModelData({
this.exportService.streamModelDataAsCsv({
dataStream,
linkStream,
projectId: sourceProject.id,
@ -506,17 +322,28 @@ export class DuplicateProcessor {
base_id: destBase.id,
colId: id,
});
if (col.colOptions?.type === 'bt') {
const childCol = await Column.get({
base_id: destBase.id,
colId: col.colOptions.fk_child_column_id,
});
headers.push(childCol.column_name);
if (col) {
if (col.colOptions?.type === 'bt') {
const childCol = await Column.get({
base_id: destBase.id,
colId: col.colOptions.fk_child_column_id,
});
if (childCol) {
headers.push(childCol.column_name);
} else {
headers.push(null);
this.logger.error(`child column not found (${id})`);
}
} else {
headers.push(col.column_name);
}
} else {
headers.push(col.column_name);
headers.push(null);
this.logger.error(`column not found (${id})`);
}
} else {
debugLog('header not found', header);
headers.push(null);
this.logger.error(`id not found (${header})`);
}
}
parser.resume();
@ -524,8 +351,10 @@ export class DuplicateProcessor {
if (results.errors.length === 0) {
const row = {};
for (let i = 0; i < headers.length; i++) {
if (results.data[i] !== '') {
row[headers[i]] = results.data[i];
if (headers[i]) {
if (results.data[i] !== '') {
row[headers[i]] = results.data[i];
}
}
}
chunk.push(row);
@ -540,7 +369,7 @@ export class DuplicateProcessor {
raw: true,
});
} catch (e) {
console.log(e);
this.logger.error(e);
}
chunk = [];
parser.resume();
@ -559,7 +388,7 @@ export class DuplicateProcessor {
raw: true,
});
} catch (e) {
console.log(e);
this.logger.error(e);
}
chunk = [];
}
@ -568,7 +397,11 @@ export class DuplicateProcessor {
});
});
elapsedTime(hrTime, `external bt ${model.title}`);
elapsedTime(
hrTime,
`map existing links to ${model.title}`,
'importModelsData',
);
}
}
}

31
packages/nocodb/src/modules/jobs/export-import/export.service.ts

@ -1,7 +1,7 @@
import { Readable } from 'stream';
import { UITypes, ViewTypes } from 'nocodb-sdk';
import { unparse } from 'papaparse';
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2';
import { getViewAndModelByAliasOrId } from '../../../modules/datas/helpers';
import {
@ -12,11 +12,14 @@ import NcPluginMgrv2 from '../../../helpers/NcPluginMgrv2';
import { NcError } from '../../../helpers/catchError';
import { Base, Hook, Model, Project } from '../../../models';
import { DatasService } from '../../../services/datas.service';
import { elapsedTime, initTime } from '../helpers';
import type { BaseModelSqlv2 } from '../../../db/BaseModelSqlv2';
import type { LinkToAnotherRecordColumn, View } from '../../../models';
import type { View } from '../../../models';
@Injectable()
export class ExportService {
private readonly logger = new Logger(ExportService.name);
constructor(private datasService: DatasService) {}
async serializeModels(param: {
@ -293,7 +296,7 @@ export class ExportService {
return serializedModels;
}
async streamModelData(param: {
async streamModelDataAsCsv(param: {
dataStream: Readable;
linkStream: Readable;
projectId: string;
@ -422,7 +425,7 @@ export class ExportService {
true,
);
} catch (e) {
console.error(e);
this.logger.error(e);
throw e;
}
@ -488,7 +491,7 @@ export class ExportService {
true,
);
} catch (e) {
console.error(e);
this.logger.error(e);
throw e;
}
@ -597,6 +600,8 @@ export class ExportService {
}
async exportBase(param: { path: string; baseId: string }) {
const hrTime = initTime();
const base = await Base.get(param.baseId);
if (!base)
@ -613,6 +618,12 @@ export class ExportService {
modelIds: models.map((m) => m.id),
});
elapsedTime(
hrTime,
`serialize models for ${base.project_id}::${base.id}`,
'exportBase',
);
const exportData = {
id: `${project.id}::${base.id}`,
models: exportedModels,
@ -669,7 +680,7 @@ export class ExportService {
});
linkStream.on('error', (e) => {
console.error(e);
this.logger.error(e);
resolve(null);
});
});
@ -679,7 +690,7 @@ export class ExportService {
dataStream,
);
this.streamModelData({
this.streamModelDataAsCsv({
dataStream,
linkStream,
projectId: project.id,
@ -693,6 +704,12 @@ export class ExportService {
combinedLinkStream.push(null);
await uploadLinkPromise;
elapsedTime(
hrTime,
`export base ${base.project_id}::${base.id}`,
'exportBase',
);
} catch (e) {
throw NcError.badRequest(e);
}

504
packages/nocodb/src/modules/jobs/export-import/import.service.ts

@ -1,5 +1,5 @@
import { UITypes, ViewTypes } from 'nocodb-sdk';
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import papaparse from 'papaparse';
import {
findWithIdentifier,
@ -27,11 +27,15 @@ import { HooksService } from '../../../services/hooks.service';
import { ViewsService } from '../../../services/views.service';
import NcPluginMgrv2 from '../../../helpers/NcPluginMgrv2';
import { BulkDataAliasService } from '../../../services/bulk-data-alias.service';
import { elapsedTime, initTime } from '../helpers';
import type { Readable } from 'stream';
import type { ViewCreateReqType } from 'nocodb-sdk';
import type { LinkToAnotherRecordColumn, User, View } from '../../../models';
@Injectable()
export class ImportService {
private readonly logger = new Logger(ImportService.name);
constructor(
private tablesService: TablesService,
private columnsService: ColumnsService,
@ -59,6 +63,8 @@ export class ImportService {
req: any;
externalModels?: Model[];
}) {
const hrTime = initTime();
// structured id to db id
const idMap = new Map<string, string>();
const externalIdMap = new Map<string, string>();
@ -111,6 +117,8 @@ export class ImportService {
}
}
elapsedTime(hrTime, 'generate id map for external models', 'importModels');
// create tables with static columns
for (const data of param.data) {
const modelData = data.model;
@ -148,9 +156,11 @@ export class ImportService {
tableReferences.set(modelData.id, table);
}
elapsedTime(hrTime, 'create tables with static columns', 'importModels');
const referencedColumnSet = [];
// create columns with reference to other columns
// create LTAR columns
for (const data of param.data) {
const modelData = data.model;
const table = tableReferences.get(modelData.id);
@ -159,7 +169,6 @@ export class ImportService {
(a) => a.uidt === UITypes.LinkToAnotherRecord,
);
// create columns with reference to other columns
for (const col of linkedColumnSet) {
if (col.colOptions) {
const colOptions = col.colOptions;
@ -701,6 +710,8 @@ export class ImportService {
);
}
elapsedTime(hrTime, 'create LTAR columns', 'importModels');
const sortedReferencedColumnSet = [];
// sort referenced columns to avoid referencing before creation
@ -814,6 +825,8 @@ export class ImportService {
}
}
elapsedTime(hrTime, 'create referenced columns', 'importModels');
// create views
for (const data of param.data) {
const modelData = data.model;
@ -930,6 +943,8 @@ export class ImportService {
}
}
elapsedTime(hrTime, 'create views', 'importModels');
// create hooks
for (const data of param.data) {
if (!data?.hooks) break;
@ -972,6 +987,8 @@ export class ImportService {
}
}
elapsedTime(hrTime, 'create hooks', 'importModels');
return idMap;
}
@ -1114,25 +1131,17 @@ export class ImportService {
file?: any;
};
req: any;
debug?: boolean;
}) {
const { user, projectId, baseId, src, req } = param;
const hrTime = initTime();
const debug = param?.debug === true;
const debugLog = (...args: any[]) => {
if (!debug) return;
console.log(...args);
};
const { user, projectId, baseId, src, req } = param;
let start = process.hrtime();
const destProject = await Project.get(projectId);
const destBase = await Base.get(baseId);
const elapsedTime = function (label?: string) {
const elapsedS = process.hrtime(start)[0].toFixed(3);
const elapsedMs = process.hrtime(start)[1] / 1000000;
if (label) debugLog(`${label}: ${elapsedS}s ${elapsedMs}ms`);
start = process.hrtime();
};
if (!destProject || !destBase) {
throw NcError.badRequest('Project or Base not found');
}
switch (src.type) {
case 'local': {
@ -1145,10 +1154,10 @@ export class ImportService {
await storageAdapter.fileRead(`${path}/schema.json`),
);
elapsedTime('read schema');
elapsedTime(hrTime, 'read schema from file', 'importBase');
// store fk_mm_model_id (mm) to link once
const handledLinks = [];
let handledLinks = [];
const idMap = await this.importModels({
user,
@ -1158,7 +1167,7 @@ export class ImportService {
req,
});
elapsedTime('import models');
elapsedTime(hrTime, 'import models schema', 'importBase');
if (idMap) {
const files = await (storageAdapter as any).getDirectoryList(
@ -1174,9 +1183,6 @@ export class ImportService {
`${path}/data/${file}`,
);
const headers: string[] = [];
let chunk = [];
const modelId = findWithIdentifier(
idMap,
file.replace(/\.csv$/, ''),
@ -1184,209 +1190,39 @@ export class ImportService {
const model = await Model.get(modelId);
debugLog(`Importing ${model.title}...`);
await new Promise((resolve) => {
papaparse.parse(readStream, {
newline: '\r\n',
step: async (results, parser) => {
if (!headers.length) {
parser.pause();
for (const header of results.data) {
const id = idMap.get(header);
if (id) {
const col = await Column.get({
base_id: baseId,
colId: id,
});
if (col.colOptions?.type === 'bt') {
const childCol = await Column.get({
base_id: baseId,
colId: col.colOptions.fk_child_column_id,
});
headers.push(childCol.column_name);
} else {
headers.push(col.column_name);
}
} else {
debugLog(header);
}
}
parser.resume();
} else {
if (results.errors.length === 0) {
const row = {};
for (let i = 0; i < headers.length; i++) {
if (results.data[i] !== '') {
row[headers[i]] = results.data[i];
}
}
chunk.push(row);
if (chunk.length > 100) {
parser.pause();
elapsedTime('before import chunk');
try {
await this.bulkDataService.bulkDataInsert({
projectName: projectId,
tableName: modelId,
body: chunk,
cookie: null,
chunkSize: chunk.length + 1,
foreign_key_checks: false,
raw: true,
});
} catch (e) {
debugLog(`${model.title} import throwed an error!`);
console.log(e);
}
chunk = [];
elapsedTime('after import chunk');
parser.resume();
}
}
}
},
complete: async () => {
if (chunk.length > 0) {
elapsedTime('before import chunk');
try {
await this.bulkDataService.bulkDataInsert({
projectName: projectId,
tableName: modelId,
body: chunk,
cookie: null,
chunkSize: chunk.length + 1,
foreign_key_checks: false,
raw: true,
});
} catch (e) {
debugLog(chunk);
console.log(e);
}
chunk = [];
elapsedTime('after import chunk');
}
resolve(null);
},
});
this.logger.debug(`Importing ${model.title}...`);
await this.importDataFromCsvStream({
idMap,
dataStream: readStream,
destProject,
destBase,
destModel: model,
});
elapsedTime(
hrTime,
`import data for ${model.title}`,
'importBase',
);
}
// reset timer
elapsedTime();
elapsedTime(hrTime);
const linkReadStream = await (
storageAdapter as any
).fileReadByStream(linkFile);
const lChunk: Record<string, any[]> = {}; // fk_mm_model_id: { rowId, childId }[]
let headersFound = false;
let childIndex = -1;
let parentIndex = -1;
let columnIndex = -1;
const mmColumns: Record<string, Column> = {};
const mmParentChild: any = {};
await new Promise((resolve) => {
papaparse.parse(linkReadStream, {
newline: '\r\n',
step: async (results, parser) => {
if (!headersFound) {
for (const [i, header] of Object.entries(results.data)) {
if (header === 'child') {
childIndex = parseInt(i);
} else if (header === 'parent') {
parentIndex = parseInt(i);
} else if (header === 'column') {
columnIndex = parseInt(i);
}
}
headersFound = true;
} else {
if (results.errors.length === 0) {
if (
results.data[childIndex] === 'child' &&
results.data[parentIndex] === 'parent' &&
results.data[columnIndex] === 'column'
)
return;
const child = results.data[childIndex];
const parent = results.data[parentIndex];
const columnId = results.data[columnIndex];
if (child && parent && columnId) {
if (mmColumns[columnId]) {
// push to chunk
const mmModelId =
mmColumns[columnId].colOptions.fk_mm_model_id;
const mm = mmParentChild[mmModelId];
lChunk[mmModelId].push({
[mm.parent]: parent,
[mm.child]: child,
});
} else {
// get column for the first time
parser.pause();
const col = await Column.get({
colId: findWithIdentifier(idMap, columnId),
});
const colOptions =
await col.getColOptions<LinkToAnotherRecordColumn>();
const vChildCol = await colOptions.getMMChildColumn();
const vParentCol =
await colOptions.getMMParentColumn();
mmParentChild[col.colOptions.fk_mm_model_id] = {
parent: vParentCol.column_name,
child: vChildCol.column_name,
};
mmColumns[columnId] = col;
handledLinks.push(col.colOptions.fk_mm_model_id);
const mmModelId = col.colOptions.fk_mm_model_id;
// create chunk
lChunk[mmModelId] = [];
// push to chunk
const mm = mmParentChild[mmModelId];
lChunk[mmModelId].push({
[mm.parent]: parent,
[mm.child]: child,
});
parser.resume();
}
}
}
}
},
complete: async () => {
for (const [k, v] of Object.entries(lChunk)) {
try {
await this.bulkDataService.bulkDataInsert({
projectName: projectId,
tableName: k,
body: v,
cookie: null,
chunkSize: 1000,
foreign_key_checks: false,
raw: true,
});
} catch (e) {
console.log(e);
}
}
resolve(null);
},
});
handledLinks = await this.importLinkFromCsvStream({
idMap,
linkStream: linkReadStream,
destProject,
destBase,
handledLinks,
});
elapsedTime(hrTime, `import links`, 'importBase');
}
} catch (e) {
throw new Error(e);
@ -1399,4 +1235,238 @@ export class ImportService {
break;
}
}
importDataFromCsvStream(param: {
idMap: Map<string, string>;
dataStream: Readable;
destProject: Project;
destBase: Base;
destModel: Model;
}): Promise<void> {
const { idMap, dataStream, destBase, destProject, destModel } = param;
const headers: string[] = [];
let chunk = [];
return new Promise((resolve) => {
papaparse.parse(dataStream, {
newline: '\r\n',
step: async (results, parser) => {
if (!headers.length) {
parser.pause();
for (const header of results.data) {
const id = idMap.get(header);
if (id) {
const col = await Column.get({
base_id: destBase.id,
colId: id,
});
if (col) {
if (col.colOptions?.type === 'bt') {
const childCol = await Column.get({
base_id: destBase.id,
colId: col.colOptions.fk_child_column_id,
});
if (childCol) {
headers.push(childCol.column_name);
} else {
headers.push(null);
this.logger.error(
`child column not found (${col.colOptions.fk_child_column_id})`,
);
}
} else {
headers.push(col.column_name);
}
} else {
headers.push(null);
this.logger.error(`column not found (${id})`);
}
} else {
headers.push(null);
this.logger.error(`id not found (${header})`);
}
}
parser.resume();
} else {
if (results.errors.length === 0) {
const row = {};
for (let i = 0; i < headers.length; i++) {
if (headers[i]) {
if (results.data[i] !== '') {
row[headers[i]] = results.data[i];
}
}
}
chunk.push(row);
if (chunk.length > 1000) {
parser.pause();
try {
await this.bulkDataService.bulkDataInsert({
projectName: destProject.id,
tableName: destModel.id,
body: chunk,
cookie: null,
chunkSize: chunk.length + 1,
foreign_key_checks: false,
raw: true,
});
} catch (e) {
this.logger.error(e);
}
chunk = [];
parser.resume();
}
}
}
},
complete: async () => {
if (chunk.length > 0) {
try {
await this.bulkDataService.bulkDataInsert({
projectName: destProject.id,
tableName: destModel.id,
body: chunk,
cookie: null,
chunkSize: chunk.length + 1,
foreign_key_checks: false,
raw: true,
});
} catch (e) {
this.logger.error(e);
}
chunk = [];
}
resolve(null);
},
});
});
}
// import links and return handled links
async importLinkFromCsvStream(param: {
idMap: Map<string, string>;
linkStream: Readable;
destProject: Project;
destBase: Base;
handledLinks: string[];
}): Promise<string[]> {
const { idMap, linkStream, destBase, destProject, handledLinks } = param;
const lChunks: Record<string, any[]> = {}; // fk_mm_model_id: { rowId, childId }[]
const insertChunks = async () => {
for (const [k, v] of Object.entries(lChunks)) {
try {
if (v.length === 0) continue;
await this.bulkDataService.bulkDataInsert({
projectName: destProject.id,
tableName: k,
body: v,
cookie: null,
chunkSize: 1000,
foreign_key_checks: false,
raw: true,
});
lChunks[k] = [];
} catch (e) {
this.logger.error(e);
}
}
};
let headersFound = false;
let childIndex = -1;
let parentIndex = -1;
let columnIndex = -1;
const mmColumns: Record<string, Column> = {};
const mmParentChild: any = {};
return new Promise((resolve) => {
papaparse.parse(linkStream, {
newline: '\r\n',
step: async (results, parser) => {
if (!headersFound) {
for (const [i, header] of Object.entries(results.data)) {
if (header === 'child') {
childIndex = parseInt(i);
} else if (header === 'parent') {
parentIndex = parseInt(i);
} else if (header === 'column') {
columnIndex = parseInt(i);
}
}
headersFound = true;
} else {
if (results.errors.length === 0) {
const child = results.data[childIndex];
const parent = results.data[parentIndex];
const columnId = results.data[columnIndex];
if (child && parent && columnId) {
if (mmColumns[columnId]) {
// push to chunk
const mmModelId =
mmColumns[columnId].colOptions.fk_mm_model_id;
const mm = mmParentChild[mmModelId];
lChunks[mmModelId].push({
[mm.parent]: parent,
[mm.child]: child,
});
} else {
// get column for the first time
parser.pause();
await insertChunks();
const col = await Column.get({
base_id: destBase.id,
colId: findWithIdentifier(idMap, columnId),
});
if (col) {
const colOptions =
await col.getColOptions<LinkToAnotherRecordColumn>();
const vChildCol = await colOptions.getMMChildColumn();
const vParentCol = await colOptions.getMMParentColumn();
mmParentChild[col.colOptions.fk_mm_model_id] = {
parent: vParentCol.column_name,
child: vChildCol.column_name,
};
mmColumns[columnId] = col;
handledLinks.push(col.colOptions.fk_mm_model_id);
const mmModelId = col.colOptions.fk_mm_model_id;
// create chunk
lChunks[mmModelId] = [];
// push to chunk
const mm = mmParentChild[mmModelId];
lChunks[mmModelId].push({
[mm.parent]: parent,
[mm.child]: child,
});
} else {
this.logger.error(`column not found (${columnId})`);
}
parser.resume();
}
}
}
}
},
complete: async () => {
await insertChunks();
resolve(handledLinks);
},
});
});
}
}

23
packages/nocodb/src/modules/jobs/helpers.ts

@ -0,0 +1,23 @@
import { Logger } from '@nestjs/common';
import { JOBS_QUEUE } from '../../interface/Jobs';
export const initTime = function () {
return {
hrTime: process.hrtime(),
};
};
export const elapsedTime = function (
time: { hrTime: [number, number] },
label?: string,
context?: string,
) {
const elapsedS = process.hrtime(time.hrTime)[0].toFixed(3);
const elapsedMs = process.hrtime(time.hrTime)[1] / 1000000;
if (label)
Logger.debug(
`${label}: ${elapsedS}s ${elapsedMs}ms`,
`${JOBS_QUEUE}${context ? `:${context}` : ''}`,
);
time.hrTime = process.hrtime();
};

2
tests/playwright/pages/Dashboard/Grid/index.ts

@ -75,6 +75,8 @@ export class GridPage extends BasePage {
// wait for render to complete before count
if (index !== 0) await this.get().locator('.nc-grid-row').nth(0).waitFor({ state: 'attached' });
const rowCount = await this.get().locator('.nc-grid-row').count();
await (await this.get().locator('.nc-grid-add-new-cell').elementHandle())?.waitForElementState('stable');
await this.get().locator('.nc-grid-add-new-cell').click();
await expect(await this.get().locator('.nc-grid-row')).toHaveCount(rowCount + 1);

41
tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts

@ -56,24 +56,34 @@ export class ToolbarFilterPage extends BasePage {
}) {
if (!openModal) await this.get().locator(`button:has-text("Add Filter")`).first().click();
const selectedField = await getTextExcludeIconText(await this.rootPage.locator('.nc-filter-field-select'));
const selectedField = await getTextExcludeIconText(await this.rootPage.locator('.nc-filter-field-select .ant-select-selection-item'));
if (selectedField !== title) {
await this.rootPage.locator('.nc-filter-field-select').last().click();
await this.rootPage
await this.waitForResponse({
uiAction: () => this.rootPage
.locator('div.ant-select-dropdown.nc-dropdown-toolbar-field-list')
.locator(`div[label="${title}"]:visible`)
.click();
.click(),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: locallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`,
});
}
const selectedOpType = await getTextExcludeIconText(await this.rootPage.locator('.nc-filter-operation-select'));
if (selectedOpType !== operation) {
await this.rootPage.locator('.nc-filter-operation-select').click();
// first() : filter list has >, >=
await this.rootPage
await this.waitForResponse({
uiAction: () => this.rootPage
.locator('.nc-dropdown-filter-comp-op')
.locator(`.ant-select-item:has-text("${operation}")`)
.first()
.click();
.click(),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: locallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`,
});
}
// subtype for date
@ -84,11 +94,16 @@ export class ToolbarFilterPage extends BasePage {
if (selectedSubType !== subOperation) {
await this.rootPage.locator('.nc-filter-sub_operation-select').click();
// first() : filter list has >, >=
await this.rootPage
await this.waitForResponse({
uiAction: () => this.rootPage
.locator('.nc-dropdown-filter-comp-sub-op')
.locator(`.ant-select-item:has-text("${subOperation}")`)
.first()
.click();
.click(),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: locallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`,
});
}
}
@ -120,7 +135,11 @@ export class ToolbarFilterPage extends BasePage {
if (subOperation === 'exact date') {
await this.get().locator('.nc-filter-value-select').click();
await this.rootPage.locator(`.ant-picker-dropdown:visible`);
await this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click();
await this.waitForResponse({
uiAction: () => this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click(),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: locallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`,
});
} else {
fillFilter = () => this.rootPage.locator('.nc-filter-value-select > input').last().fill(value);
await this.waitForResponse({
@ -133,7 +152,11 @@ export class ToolbarFilterPage extends BasePage {
}
break;
case UITypes.Duration:
await this.get().locator('.nc-filter-value-select').locator('input').fill(value);
await this.waitForResponse({
uiAction: () => this.get().locator('.nc-filter-value-select').locator('input').fill(value),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: locallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`,
});
break;
case UITypes.Rating:
await this.get()

Loading…
Cancel
Save