Browse Source

Merge pull request #4156 from nocodb/feat/at-import-memory

feat: at import memory
pull/4203/head
աɨռɢӄաօռɢ 2 years ago committed by GitHub
parent
commit
9dbc74ace4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 199
      packages/nocodb/src/lib/meta/api/sync/helpers/EntityMap.ts
  2. 92
      packages/nocodb/src/lib/meta/api/sync/helpers/job.ts
  3. 213
      packages/nocodb/src/lib/meta/api/sync/helpers/readAndProcessData.ts

199
packages/nocodb/src/lib/meta/api/sync/helpers/EntityMap.ts

@ -0,0 +1,199 @@
import sqlite3 from 'sqlite3';
import { Readable } from 'stream';
class EntityMap {
initialized: boolean;
cols: string[];
db: any;
constructor(...args) {
this.initialized = false;
this.cols = args.map((arg) => processKey(arg));
this.db = new Promise((resolve, reject) => {
const db = new sqlite3.Database(':memory:');
const colStatement = this.cols.length > 0 ? this.cols.join(' TEXT, ') + ' TEXT' : 'mappingPlaceholder TEXT';
db.run(`CREATE TABLE mapping (${colStatement})`, (err) => {
if (err) {
console.log(err);
reject(err);
}
resolve(db)
});
});
}
async init () {
if (!this.initialized) {
this.db = await this.db;
this.initialized = true;
}
}
destroy() {
if (this.initialized && this.db) {
this.db.close();
}
}
async addRow(row) {
if (!this.initialized) {
throw 'Please initialize first!';
}
const cols = Object.keys(row).map((key) => processKey(key));
const colStatement = cols.map((key) => `'${key}'`).join(', ');
const questionMarks = cols.map(() => '?').join(', ');
const promises = [];
for (const col of cols.filter((col) => !this.cols.includes(col))) {
promises.push(new Promise((resolve, reject) => {
this.db.run(`ALTER TABLE mapping ADD '${col}' TEXT;`, (err) => {
if (err) {
console.log(err);
reject(err);
}
this.cols.push(col);
resolve(true);
});
}));
}
await Promise.all(promises);
const values = Object.values(row).map((val) => {
if (typeof val === 'object') {
return `JSON::${JSON.stringify(val)}`;
}
return val;
});
return new Promise((resolve, reject) => {
this.db.run(`INSERT INTO mapping (${colStatement}) VALUES (${questionMarks})`, values, (err) => {
if (err) {
console.log(err);
reject(err);
}
resolve(true);
});
});
}
getRow(col, val, res = []): Promise<Record<string, any>> {
if (!this.initialized) {
throw 'Please initialize first!';
}
return new Promise((resolve, reject) => {
col = processKey(col);
res = res.map((r) => processKey(r));
this.db.get(`SELECT ${res.length ? res.join(', ') : '*'} FROM mapping WHERE ${col} = ?`, [val], (err, rs) => {
if (err) {
console.log(err);
reject(err);
}
if (rs) {
rs = processResponseRow(rs);
}
resolve(rs)
});
});
}
getCount(): Promise<number> {
if (!this.initialized) {
throw 'Please initialize first!';
}
return new Promise((resolve, reject) => {
this.db.get(`SELECT COUNT(*) as count FROM mapping`, (err, rs) => {
if (err) {
console.log(err);
reject(err);
}
resolve(rs.count)
});
});
}
getStream(res = []): DBStream {
if (!this.initialized) {
throw 'Please initialize first!';
}
res = res.map((r) => processKey(r));
return new DBStream(this.db, `SELECT ${res.length ? res.join(', ') : '*'} FROM mapping`);
}
getLimit(limit, offset, res = []): Promise<Record<string, any>[]> {
if (!this.initialized) {
throw 'Please initialize first!';
}
return new Promise((resolve, reject) => {
res = res.map((r) => processKey(r));
this.db.all(`SELECT ${res.length ? res.join(', ') : '*'} FROM mapping LIMIT ${limit} OFFSET ${offset}`, (err, rs) => {
if (err) {
console.log(err);
reject(err);
}
for (let row of rs) {
row = processResponseRow(row);
}
resolve(rs)
});
});
}
}
class DBStream extends Readable {
db: any;
stmt: any;
sql: any;
constructor(db, sql) {
super({ objectMode: true});
this.db = db;
this.sql = sql;
this.stmt = this.db.prepare(this.sql);
this.on('end', () => this.stmt.finalize());
}
_read() {
let stream = this;
this.stmt.get(function (err, result) {
if (err) {
stream.emit('error', err);
} else {
if (result) {
result = processResponseRow(result);
}
stream.push(result || null)
}
});
}
}
function processResponseRow(res: any) {
for (const key of Object.keys(res)) {
if (res[key] && res[key].startsWith('JSON::')) {
try {
res[key] = JSON.parse(res[key].replace('JSON::', ''));
} catch (e) {
console.log(e);
}
}
if (revertKey(key) !== key) {
res[revertKey(key)] = res[key];
delete res[key];
}
}
return res;
}
function processKey(key) {
return key.replace(/'/g, "''").replace(/[A-Z]/g, (match) => `_${match}`);
}
function revertKey(key) {
return key.replace(/''/g, "'").replace(/_[A-Z]/g, (match) => match[1]);
}
export default EntityMap;

92
packages/nocodb/src/lib/meta/api/sync/helpers/job.ts

@ -14,6 +14,8 @@ import utc from 'dayjs/plugin/utc';
import tinycolor from 'tinycolor2';
import { importData, importLTARData } from './readAndProcessData';
import EntityMap from './EntityMap';
dayjs.extend(utc);
const selectColors = {
@ -67,32 +69,28 @@ export default async (
syncDB: AirtableSyncConfig,
progress: (data: { msg?: string; level?: any }) => void
) => {
const sMap = {
mapTbl: {},
const sMapEM = new EntityMap('aTblId', 'ncId', 'ncName', 'ncParent');
await sMapEM.init();
const sMap = {
// static mapping records between aTblId && ncId
addToMappingTbl(aTblId, ncId, ncName, parent?) {
this.mapTbl[aTblId] = {
ncId: ncId,
ncParent: parent,
// name added to assist in quick debug
ncName: ncName,
};
async addToMappingTbl(aTblId, ncId, ncName, ncParent?) {
await sMapEM.addRow({ aTblId, ncId, ncName, ncParent });
},
// get NcID from airtable ID
getNcIdFromAtId(aId) {
return this.mapTbl[aId]?.ncId;
async getNcIdFromAtId(aId) {
return (await sMapEM.getRow('aTblId', aId, ['ncId']))?.ncId;
},
// get nc Parent from airtable ID
getNcParentFromAtId(aId) {
return this.mapTbl[aId]?.ncParent;
async getNcParentFromAtId(aId) {
return (await sMapEM.getRow('aTblId', aId, ['ncParent']))?.ncParent;
},
// get nc-title from airtable ID
getNcNameFromAtId(aId) {
return this.mapTbl[aId]?.ncName;
async getNcNameFromAtId(aId) {
return (await sMapEM.getRow('aTblId', aId, ['ncName']))?.ncName;
},
};
@ -333,8 +331,8 @@ export default async (
// let ncCol = ncTbl.columns.find(x => x.title === aTblField.cn);
// return ncCol;
const ncTblId = sMap.getNcParentFromAtId(aTblFieldId);
const ncColId = sMap.getNcIdFromAtId(aTblFieldId);
const ncTblId = await sMap.getNcParentFromAtId(aTblFieldId);
const ncColId = await sMap.getNcIdFromAtId(aTblFieldId);
// not migrated column, skip
if (ncColId === undefined || ncTblId === undefined) return 0;
@ -424,7 +422,7 @@ export default async (
// retrieve additional options associated with selected data types
//
function getNocoTypeOptions(col: any): any {
async function getNocoTypeOptions(col: any): Promise<any> {
switch (col.type) {
case 'select':
case 'multiSelect': {
@ -462,7 +460,7 @@ export default async (
: tinycolor.random().toHexString(),
});
sMap.addToMappingTbl(
await sMap.addToMappingTbl(
(value as any).id,
undefined,
(value as any).name
@ -477,7 +475,7 @@ export default async (
// convert to Nc schema (basic, excluding relations)
//
function tablesPrepare(tblSchema: any[]) {
async function tablesPrepare(tblSchema: any[]) {
const tables: any[] = [];
for (let i = 0; i < tblSchema.length; ++i) {
@ -574,7 +572,7 @@ export default async (
}
// additional column parameters when applicable
const colOptions = getNocoTypeOptions(col);
const colOptions = await getNocoTypeOptions(col);
switch (colOptions.type) {
case 'select':
@ -607,7 +605,7 @@ export default async (
async function nocoCreateBaseSchema(aTblSchema) {
// base schema preparation: exclude
const tables: any[] = tablesPrepare(aTblSchema);
const tables: any[] = await tablesPrepare(aTblSchema);
// for each table schema, create nc table
for (let idx = 0; idx < tables.length; idx++) {
@ -701,7 +699,7 @@ export default async (
if (!nc_isLinkExists(aTblLinkColumns[i].id)) {
// parent table ID
// let srcTableId = (await nc_getTableSchema(aTblSchema[idx].name)).id;
const srcTableId = sMap.getNcIdFromAtId(aTblSchema[idx].id);
const srcTableId = await sMap.getNcIdFromAtId(aTblSchema[idx].id);
// find child table name from symmetric column ID specified
// self link, symmetricColumnId field will be undefined
@ -916,7 +914,7 @@ export default async (
// parent table ID
// let srcTableId = (await nc_getTableSchema(aTblSchema[idx].name)).id;
const srcTableId = sMap.getNcIdFromAtId(aTblSchema[idx].id);
const srcTableId = await sMap.getNcIdFromAtId(aTblSchema[idx].id);
const srcTableSchema = ncSchema.tablesById[srcTableId];
if (aTblColumns.length) {
@ -944,10 +942,10 @@ export default async (
continue;
}
const ncRelationColumnId = sMap.getNcIdFromAtId(
const ncRelationColumnId = await sMap.getNcIdFromAtId(
aTblColumns[i].typeOptions.relationColumnId
);
const ncLookupColumnId = sMap.getNcIdFromAtId(
const ncLookupColumnId = await sMap.getNcIdFromAtId(
aTblColumns[i].typeOptions.foreignTableRollupColumnId
);
@ -1020,10 +1018,10 @@ export default async (
const srcTableId = nestedLookupTbl[0].srcTableId;
const srcTableSchema = ncSchema.tablesById[srcTableId];
const ncRelationColumnId = sMap.getNcIdFromAtId(
const ncRelationColumnId = await sMap.getNcIdFromAtId(
nestedLookupTbl[0].typeOptions.relationColumnId
);
const ncLookupColumnId = sMap.getNcIdFromAtId(
const ncLookupColumnId = await sMap.getNcIdFromAtId(
nestedLookupTbl[0].typeOptions.foreignTableRollupColumnId
);
@ -1106,7 +1104,7 @@ export default async (
// parent table ID
// let srcTableId = (await nc_getTableSchema(aTblSchema[idx].name)).id;
const srcTableId = sMap.getNcIdFromAtId(aTblSchema[idx].id);
const srcTableId = await sMap.getNcIdFromAtId(aTblSchema[idx].id);
const srcTableSchema = ncSchema.tablesById[srcTableId];
if (aTblColumns.length) {
@ -1151,10 +1149,10 @@ export default async (
continue;
}
const ncRelationColumnId = sMap.getNcIdFromAtId(
const ncRelationColumnId = await sMap.getNcIdFromAtId(
aTblColumns[i].typeOptions.relationColumnId
);
const ncRollupColumnId = sMap.getNcIdFromAtId(
const ncRollupColumnId = await sMap.getNcIdFromAtId(
aTblColumns[i].typeOptions.foreignTableRollupColumnId
);
@ -1224,10 +1222,10 @@ export default async (
const srcTableId = nestedLookupTbl[0].srcTableId;
const srcTableSchema = ncSchema.tablesById[srcTableId];
const ncRelationColumnId = sMap.getNcIdFromAtId(
const ncRelationColumnId = await sMap.getNcIdFromAtId(
nestedLookupTbl[0].typeOptions.relationColumnId
);
const ncLookupColumnId = sMap.getNcIdFromAtId(
const ncLookupColumnId = await sMap.getNcIdFromAtId(
nestedLookupTbl[0].typeOptions.foreignTableRollupColumnId
);
@ -1283,7 +1281,7 @@ export default async (
);
const pColId = aTblSchema[idx].primaryColumnId;
const ncColId = sMap.getNcIdFromAtId(pColId);
const ncColId = await sMap.getNcIdFromAtId(pColId);
// skip primary column configuration if we field not migrated
if (ncColId) {
@ -1293,7 +1291,7 @@ export default async (
recordPerfStats(_perfStart, 'dbTableColumn.primaryColumnSet');
// update schema
const ncTblId = sMap.getNcIdFromAtId(aTblSchema[idx].id);
const ncTblId = await sMap.getNcIdFromAtId(aTblSchema[idx].id);
await updateNcTblSchemaById(ncTblId);
}
}
@ -1340,6 +1338,12 @@ export default async (
for (const [key, value] of Object.entries(rec as { [key: string]: any })) {
// retrieve datatype
const dt = table.columns.find((x) => x.title === key)?.uidt;
// always process LTAR, Lookup, and Rollup columns as we delete the key after processing
if (!value && dt !== UITypes.LinkToAnotherRecord && dt !== UITypes.Lookup && dt !== UITypes.Rollup) {
rec[key] = null;
continue;
}
switch (dt) {
// https://www.npmjs.com/package/validator
@ -1413,7 +1417,7 @@ export default async (
case UITypes.MultiSelect:
rec[key] = value
.map((v) => {
?.map((v) => {
if (v === '') {
return 'nc_empty';
}
@ -1572,7 +1576,7 @@ export default async (
async function nocoConfigureFormView(sDB, aTblSchema) {
if (!sDB.options.syncViews) return;
for (let idx = 0; idx < aTblSchema.length; idx++) {
const tblId = sMap.getNcIdFromAtId(aTblSchema[idx].id);
const tblId = await sMap.getNcIdFromAtId(aTblSchema[idx].id);
const formViews = aTblSchema[idx].views.filter((x) => x.type === 'form');
const configuredViews = rtc.view.grid + rtc.view.gallery + rtc.view.form;
@ -1644,7 +1648,7 @@ export default async (
async function nocoConfigureGridView(sDB, aTblSchema) {
for (let idx = 0; idx < aTblSchema.length; idx++) {
const tblId = sMap.getNcIdFromAtId(aTblSchema[idx].id);
const tblId = await sMap.getNcIdFromAtId(aTblSchema[idx].id);
const gridViews = aTblSchema[idx].views.filter((x) => x.type === 'grid');
let viewCnt = idx;
@ -1960,7 +1964,7 @@ export default async (
// one of not migrated column;
if (!colSchema) {
updateMigrationSkipLog(
sMap.getNcNameFromAtId(viewId),
await sMap.getNcNameFromAtId(viewId),
colSchema.title,
colSchema.uidt,
`filter config skipped; column not migrated`
@ -1975,7 +1979,7 @@ export default async (
if (datatype === UITypes.Date || datatype === UITypes.DateTime) {
// skip filters over data datatype
updateMigrationSkipLog(
sMap.getNcNameFromAtId(viewId),
await sMap.getNcNameFromAtId(viewId),
colSchema.title,
colSchema.uidt,
`filter config skipped; filter over date datatype not supported`
@ -1995,7 +1999,7 @@ export default async (
fk_column_id: columnId,
logical_op: f.conjunction,
comparison_op: filterMap[filter.operator],
value: sMap.getNcNameFromAtId(filter.value[i]),
value: await sMap.getNcNameFromAtId(filter.value[i]),
};
ncFilters.push(fx);
}
@ -2006,7 +2010,7 @@ export default async (
fk_column_id: columnId,
logical_op: f.conjunction,
comparison_op: filterMap[filter.operator],
value: sMap.getNcNameFromAtId(filter.value),
value: await sMap.getNcNameFromAtId(filter.value),
};
ncFilters.push(fx);
}
@ -2102,7 +2106,7 @@ export default async (
// rest of the columns from airtable- retain order & visibility property
for (let j = 0; j < c.length; j++) {
const ncColumnId = sMap.getNcIdFromAtId(c[j].columnId);
const ncColumnId = await sMap.getNcIdFromAtId(c[j].columnId);
const ncViewColumnId = await nc_getViewColumnId(
viewId,
viewType,
@ -2248,7 +2252,7 @@ export default async (
sDB: syncDB,
logDetailed,
});
rtc.data.records += recordsMap[ncTbl.id].length;
rtc.data.records += await recordsMap[ncTbl.id].getCount();
logDetailed(`Data inserted from ${ncTbl.title}`);
}

213
packages/nocodb/src/lib/meta/api/sync/helpers/readAndProcessData.ts

@ -1,59 +1,55 @@
import { AirtableBase } from 'airtable/lib/airtable_base';
import { Api, RelationTypes, TableType, UITypes } from 'nocodb-sdk';
import EntityMap from './EntityMap';
const BULK_DATA_BATCH_SIZE = 2000;
const ASSOC_BULK_DATA_BATCH_SIZE = 5000;
const BULK_DATA_BATCH_SIZE = 500;
const ASSOC_BULK_DATA_BATCH_SIZE = 1000;
const BULK_PARALLEL_PROCESS = 100;
async function readAllData({
table,
fields,
base,
logBasic = (_str) => {},
triggerThreshold = BULK_DATA_BATCH_SIZE,
onThreshold = async (_rec) => {},
}: {
table: { title?: string };
fields?;
base: AirtableBase;
logBasic?: (string) => void;
logDetailed?: (string) => void;
triggerThreshold?: number;
onThreshold?: (
records: Array<{ fields: any; id: string }>,
allRecords?: Array<{ fields: any; id: string }>
) => Promise<void>;
}): Promise<Array<any>> {
}): Promise<EntityMap> {
return new Promise((resolve, reject) => {
const data = [];
let thresholdCbkData = [];
let data = null;
const selectParams: any = {
pageSize: 100,
};
if (fields) selectParams.fields = fields;
const insertJobs: Promise<any>[] = [];
base(table.title)
.select(selectParams)
.eachPage(
async function page(records, fetchNextPage) {
data.push(...records);
thresholdCbkData.push(...records);
if (!data) {
data = new EntityMap();
await data.init();
}
for await (const record of records) {
await data.addRow({ id: record.id, ...record.fields });
}
const tmpLength = await data.getCount();
logBasic(
`:: Reading '${table.title}' data :: ${Math.max(
1,
data.length - records.length
)} - ${data.length}`
tmpLength - records.length
)} - ${tmpLength}`
);
if (thresholdCbkData.length >= triggerThreshold) {
await Promise.all(insertJobs);
insertJobs.push(onThreshold(thresholdCbkData, data));
thresholdCbkData = [];
}
// To fetch the next page of records, call `fetchNextPage`.
// If there are more records, `page` will get called again.
// If there are no more records, `done` will get called.
@ -64,11 +60,6 @@ async function readAllData({
console.error(err);
return reject(err);
}
if (thresholdCbkData.length) {
await Promise.all(insertJobs);
await onThreshold(thresholdCbkData, data);
thresholdCbkData = [];
}
resolve(data);
}
);
@ -94,7 +85,7 @@ export async function importData({
api: Api<any>;
nocoBaseDataProcessing_v2;
sDB;
}): Promise<any> {
}): Promise<EntityMap> {
try {
// @ts-ignore
const records = await readAllData({
@ -102,26 +93,55 @@ export async function importData({
base,
logDetailed,
logBasic,
async onThreshold(records, allRecords) {
const allData = [];
for (let i = 0; i < records.length; i++) {
const r = await nocoBaseDataProcessing_v2(sDB, table, records[i]);
allData.push(r);
}
});
await new Promise(async (resolve) => {
const readable = records.getStream();
const allRecordsCount = await records.getCount();
const promises = [];
let tempData = [];
let importedCount = 0;
let activeProcess = 0;
readable.on('data', async (record) => {
promises.push(new Promise(async (resolve) => {
activeProcess++;
if (activeProcess >= BULK_PARALLEL_PROCESS) readable.pause();
const { id: rid, ...fields } = record;
const r = await nocoBaseDataProcessing_v2(sDB, table, { id: rid, fields });
tempData.push(r);
logBasic(
`:: Importing '${table.title}' data :: ${
allRecords.length - records.length + 1
} - ${allRecords.length}`
);
await api.dbTableRow.bulkCreate('nc', projectName, table.id, allData);
},
if (tempData.length >= BULK_DATA_BATCH_SIZE) {
let insertArray = tempData.splice(0, tempData.length);
await api.dbTableRow.bulkCreate('nc', projectName, table.id, insertArray);
logBasic(
`:: Importing '${table.title}' data :: ${importedCount} - ${Math.min(importedCount + BULK_DATA_BATCH_SIZE, allRecordsCount)}`
);
importedCount += insertArray.length;
insertArray = [];
}
activeProcess--;
if (activeProcess < BULK_PARALLEL_PROCESS) readable.resume();
resolve(true);
}));
});
readable.on('end', async () => {
await Promise.all(promises);
if (tempData.length > 0) {
await api.dbTableRow.bulkCreate('nc', projectName, table.id, tempData);
logBasic(
`:: Importing '${table.title}' data :: ${importedCount} - ${Math.min(importedCount + BULK_DATA_BATCH_SIZE, allRecordsCount)}`
);
importedCount += tempData.length;
tempData = [];
}
resolve(true);
});
});
return records;
} catch (e) {
console.log(e);
return 0;
return null;
}
}
@ -146,7 +166,7 @@ export async function importLTARData({
logBasic: (string) => void;
api: Api<any>;
insertedAssocRef: { [assocTableId: string]: boolean };
records?: Array<{ fields: any; id: string }>;
records?: EntityMap;
atNcAliasRef: {
[ncTableId: string]: {
[ncTitle: string]: string;
@ -209,49 +229,80 @@ export async function importLTARData({
let nestedLinkCnt = 0;
// Iterate over all related M2M associative table
for (const assocMeta of assocTableMetas) {
const assocTableData = [];
for await (const assocMeta of assocTableMetas) {
let assocTableData = [];
let importedCount = 0;
// extract insert data from records
for (const record of allData) {
const rec = record.fields;
await new Promise((resolve) => {
const promises = [];
const readable = allData.getStream();
let activeProcess = 0;
readable.on('data', async (record) => {
promises.push(new Promise(async (resolve) => {
activeProcess++;
if (activeProcess >= BULK_PARALLEL_PROCESS) readable.pause();
const { id: _atId, ...rec } = record;
// todo: use actual alias instead of sanitized
assocTableData.push(
...(rec?.[atNcAliasRef[table.id][assocMeta.colMeta.title]] || []).map(
(id) => ({
[assocMeta.curCol.title]: record.id,
[assocMeta.refCol.title]: id,
})
)
);
}
// todo: use actual alias instead of sanitized
assocTableData.push(
...(rec?.[atNcAliasRef[table.id][assocMeta.colMeta.title]] || []).map(
(id) => ({
[assocMeta.curCol.title]: record.id,
[assocMeta.refCol.title]: id,
})
)
);
nestedLinkCnt += assocTableData.length;
// Insert datas as chunks of size `ASSOC_BULK_DATA_BATCH_SIZE`
for (
let i = 0;
i < assocTableData.length;
i += ASSOC_BULK_DATA_BATCH_SIZE
) {
logBasic(
`:: Importing '${table.title}' LTAR data :: ${i + 1} - ${Math.min(
i + ASSOC_BULK_DATA_BATCH_SIZE,
assocTableData.length
)}`
);
if (assocTableData.length >= ASSOC_BULK_DATA_BATCH_SIZE) {
let insertArray = assocTableData.splice(0, assocTableData.length);
logBasic(
`:: Importing '${table.title}' LTAR data :: ${importedCount} - ${Math.min(
importedCount + ASSOC_BULK_DATA_BATCH_SIZE,
insertArray.length
)}`
);
await api.dbTableRow.bulkCreate(
'nc',
projectName,
assocMeta.modelMeta.id,
insertArray
);
console.log(
assocTableData.slice(i, i + ASSOC_BULK_DATA_BATCH_SIZE).length
);
importedCount += insertArray.length;
insertArray = [];
}
activeProcess--;
if (activeProcess < BULK_PARALLEL_PROCESS) readable.resume();
resolve(true);
}));
});
readable.on('end', async () => {
await Promise.all(promises);
if (assocTableData.length >= 0) {
logBasic(
`:: Importing '${table.title}' LTAR data :: ${importedCount} - ${Math.min(
importedCount + ASSOC_BULK_DATA_BATCH_SIZE,
assocTableData.length
)}`
);
await api.dbTableRow.bulkCreate(
'nc',
projectName,
assocMeta.modelMeta.id,
assocTableData
);
await api.dbTableRow.bulkCreate(
'nc',
projectName,
assocMeta.modelMeta.id,
assocTableData.slice(i, i + ASSOC_BULK_DATA_BATCH_SIZE)
);
}
importedCount += assocTableData.length;
assocTableData = [];
}
resolve(true);
});
});
nestedLinkCnt += importedCount;
}
return nestedLinkCnt;
}

Loading…
Cancel
Save