mirror of https://github.com/nocodb/nocodb
Pranav C
2 years ago
24 changed files with 4199 additions and 8 deletions
@ -1,8 +0,0 @@ |
|||||||
export default interface XcDynamicChanges { |
|
||||||
onTableCreate(tn: string): Promise<void>; |
|
||||||
onTableUpdate(changeObj: any): Promise<void>; |
|
||||||
onTableDelete(tn: string): Promise<void>; |
|
||||||
onTableRename(oldTableName: string, newTableName: string): Promise<void>; |
|
||||||
onHandlerCodeUpdate(tn: string): Promise<void>; |
|
||||||
onMetaUpdate(tn: string): Promise<void>; |
|
||||||
} |
|
@ -0,0 +1,151 @@ |
|||||||
|
import debug from 'debug'; |
||||||
|
import { T } from 'nc-help'; |
||||||
|
import boxen from 'boxen'; |
||||||
|
import { NcConfig } from '../interface/config'; |
||||||
|
import { MetaService } from '../meta/meta.service'; |
||||||
|
import ncAttachmentUpgrader from './ncAttachmentUpgrader'; |
||||||
|
import ncAttachmentUpgrader_0104002 from './ncAttachmentUpgrader_0104002'; |
||||||
|
import ncStickyColumnUpgrader from './ncStickyColumnUpgrader'; |
||||||
|
import ncFilterUpgrader_0104004 from './ncFilterUpgrader_0104004'; |
||||||
|
import ncFilterUpgrader_0105003 from './ncFilterUpgrader_0105003'; |
||||||
|
import ncFilterUpgrader from './ncFilterUpgrader'; |
||||||
|
import ncProjectRolesUpgrader from './ncProjectRolesUpgrader'; |
||||||
|
import ncDataTypesUpgrader from './ncDataTypesUpgrader'; |
||||||
|
import ncProjectUpgraderV2_0090000 from './ncProjectUpgraderV2_0090000'; |
||||||
|
import ncProjectEnvUpgrader0011045 from './ncProjectEnvUpgrader0011045'; |
||||||
|
import ncProjectEnvUpgrader from './ncProjectEnvUpgrader'; |
||||||
|
import ncHookUpgrader from './ncHookUpgrader'; |
||||||
|
|
||||||
|
const log = debug('nc:version-upgrader'); |
||||||
|
|
||||||
|
export interface NcUpgraderCtx { |
||||||
|
ncMeta: MetaService; |
||||||
|
} |
||||||
|
|
||||||
|
export default class NcUpgrader { |
||||||
|
private static STORE_KEY = 'NC_CONFIG_MAIN'; |
||||||
|
|
||||||
|
// Todo: transaction
|
||||||
|
public static async upgrade(ctx: NcUpgraderCtx): Promise<any> { |
||||||
|
this.log(`upgrade :`); |
||||||
|
let oldVersion; |
||||||
|
|
||||||
|
try { |
||||||
|
ctx.ncMeta = await ctx.ncMeta.startTransaction(); |
||||||
|
|
||||||
|
const NC_VERSIONS: any[] = [ |
||||||
|
{ name: '0009000', handler: null }, |
||||||
|
{ name: '0009044', handler: null }, |
||||||
|
{ name: '0011043', handler: ncProjectEnvUpgrader }, |
||||||
|
{ name: '0011045', handler: ncProjectEnvUpgrader0011045 }, |
||||||
|
{ name: '0090000', handler: ncProjectUpgraderV2_0090000 }, |
||||||
|
{ name: '0098004', handler: ncDataTypesUpgrader }, |
||||||
|
{ name: '0098005', handler: ncProjectRolesUpgrader }, |
||||||
|
{ name: '0100002', handler: ncFilterUpgrader }, |
||||||
|
{ name: '0101002', handler: ncAttachmentUpgrader }, |
||||||
|
{ name: '0104002', handler: ncAttachmentUpgrader_0104002 }, |
||||||
|
{ name: '0104004', handler: ncFilterUpgrader_0104004 }, |
||||||
|
{ name: '0105002', handler: ncStickyColumnUpgrader }, |
||||||
|
{ name: '0105003', handler: ncFilterUpgrader_0105003 }, |
||||||
|
{ name: '0105004', handler: ncHookUpgrader }, |
||||||
|
]; |
||||||
|
if (!(await ctx.ncMeta.knexConnection?.schema?.hasTable?.('nc_store'))) { |
||||||
|
return; |
||||||
|
} |
||||||
|
this.log(`upgrade : Getting configuration from meta database`); |
||||||
|
|
||||||
|
const config = await ctx.ncMeta.metaGet('', '', 'nc_store', { |
||||||
|
key: this.STORE_KEY, |
||||||
|
}); |
||||||
|
|
||||||
|
if (config) { |
||||||
|
const configObj: NcConfig = JSON.parse(config.value); |
||||||
|
if (configObj.version !== process.env.NC_VERSION) { |
||||||
|
oldVersion = configObj.version; |
||||||
|
for (const version of NC_VERSIONS) { |
||||||
|
// compare current version and old version
|
||||||
|
if (version.name > configObj.version) { |
||||||
|
this.log( |
||||||
|
`upgrade : Upgrading '%s' => '%s'`, |
||||||
|
configObj.version, |
||||||
|
version.name, |
||||||
|
); |
||||||
|
await version?.handler?.(ctx); |
||||||
|
|
||||||
|
// update version in meta after each upgrade
|
||||||
|
config.version = version.name; |
||||||
|
await ctx.ncMeta.metaUpdate( |
||||||
|
'', |
||||||
|
'', |
||||||
|
'nc_store', |
||||||
|
{ |
||||||
|
value: JSON.stringify(config), |
||||||
|
}, |
||||||
|
{ |
||||||
|
key: NcUpgrader.STORE_KEY, |
||||||
|
}, |
||||||
|
); |
||||||
|
|
||||||
|
// todo: backup data
|
||||||
|
} |
||||||
|
if (version.name === process.env.NC_VERSION) { |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
config.version = process.env.NC_VERSION; |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.log(`upgrade : Inserting config to meta database`); |
||||||
|
const configObj: any = {}; |
||||||
|
const isOld = (await ctx.ncMeta.projectList())?.length; |
||||||
|
configObj.version = isOld ? '0009000' : process.env.NC_VERSION; |
||||||
|
await ctx.ncMeta.metaInsert('', '', 'nc_store', { |
||||||
|
key: NcUpgrader.STORE_KEY, |
||||||
|
value: JSON.stringify(configObj), |
||||||
|
}); |
||||||
|
if (isOld) { |
||||||
|
await this.upgrade(ctx); |
||||||
|
} |
||||||
|
} |
||||||
|
await ctx.ncMeta.commit(); |
||||||
|
T.emit('evt', { |
||||||
|
evt_type: 'appMigration:upgraded', |
||||||
|
from: oldVersion, |
||||||
|
to: process.env.NC_VERSION, |
||||||
|
}); |
||||||
|
} catch (e) { |
||||||
|
await ctx.ncMeta.rollback(e); |
||||||
|
T.emit('evt', { |
||||||
|
evt_type: 'appMigration:failed', |
||||||
|
from: oldVersion, |
||||||
|
to: process.env.NC_VERSION, |
||||||
|
msg: e.message, |
||||||
|
err: e?.stack?.split?.('\n').slice(0, 2).join('\n'), |
||||||
|
}); |
||||||
|
console.log(getUpgradeErrorLog(e, oldVersion, process.env.NC_VERSION)); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static log(str, ...args): void { |
||||||
|
log(`${str}`, ...args); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function getUpgradeErrorLog(e: Error, oldVersion: string, newVersion: string) { |
||||||
|
const errorTitle = `Migration from ${oldVersion} to ${newVersion} failed`; |
||||||
|
|
||||||
|
return boxen( |
||||||
|
`Error
|
||||||
|
----- |
||||||
|
${e.stack} |
||||||
|
|
||||||
|
|
||||||
|
Please raise an issue in our github by using following link :
|
||||||
|
https://github.com/nocodb/nocodb/issues/new?labels=Type%3A%20Bug&template=bug_report.md
|
||||||
|
|
||||||
|
Or contact us in our Discord community by following link : |
||||||
|
https://discord.gg/5RgZmkW ( message @o1lab, @pranavxc or @wingkwong )`,
|
||||||
|
{ title: errorTitle, padding: 1, borderColor: 'yellow' }, |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,190 @@ |
|||||||
|
import { UITypes } from 'nocodb-sdk'; |
||||||
|
import { XKnex } from '../db/CustomKnex'; |
||||||
|
import { MetaTable } from '../utils/globals'; |
||||||
|
import Base from '../models/Base'; |
||||||
|
import Model from '../models/Model'; |
||||||
|
import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; |
||||||
|
import { throwTimeoutError } from './ncUpgradeErrors'; |
||||||
|
import type { Knex } from 'knex'; |
||||||
|
import type { NcUpgraderCtx } from './NcUpgrader'; |
||||||
|
// import type { XKnex } from '../db/sql-data-mapper';
|
||||||
|
import type { BaseType } from 'nocodb-sdk'; |
||||||
|
|
||||||
|
// before 0.103.0, an attachment object was like
|
||||||
|
// [{
|
||||||
|
// "url": "http://localhost:8080/download/noco/xcdb/Sheet-1/title5/39A410.jpeg",
|
||||||
|
// "title": "foo.jpeg",
|
||||||
|
// "mimetype": "image/jpeg",
|
||||||
|
// "size": 6494
|
||||||
|
// }]
|
||||||
|
// in this way, if the base url is changed, the url will be broken
|
||||||
|
// this upgrader is to convert the existing local attachment object to the following format
|
||||||
|
// [{
|
||||||
|
// "url": "http://localhost:8080/download/noco/xcdb/Sheet-1/title5/39A410.jpeg",
|
||||||
|
// "path": "download/noco/xcdb/Sheet-1/title5/39A410.jpeg",
|
||||||
|
// "title": "foo.jpeg",
|
||||||
|
// "mimetype": "image/jpeg",
|
||||||
|
// "size": 6494
|
||||||
|
// }]
|
||||||
|
// the new url will be constructed by `${ncSiteUrl}/${path}` in UI. the old url will be used for fallback
|
||||||
|
// while other non-local attachments will remain unchanged
|
||||||
|
|
||||||
|
function getTnPath(knex: XKnex, tb: Model) { |
||||||
|
const schema = (knex as any).searchPath?.(); |
||||||
|
const clientType = knex.clientType(); |
||||||
|
if (clientType === 'mssql' && schema) { |
||||||
|
return knex.raw('??.??', [schema, tb.table_name]).toQuery(); |
||||||
|
} else if (clientType === 'snowflake') { |
||||||
|
return [ |
||||||
|
knex.client.config.connection.database, |
||||||
|
knex.client.config.connection.schema, |
||||||
|
tb.table_name, |
||||||
|
].join('.'); |
||||||
|
} else { |
||||||
|
return tb.table_name; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default async function ({ ncMeta }: NcUpgraderCtx) { |
||||||
|
const bases: BaseType[] = await ncMeta.metaList2(null, null, MetaTable.BASES); |
||||||
|
for (const _base of bases) { |
||||||
|
const base = new Base(_base); |
||||||
|
|
||||||
|
// skip if the project_id is missing
|
||||||
|
if (!base.project_id) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
const project = await ncMeta.metaGet2(null, null, MetaTable.PROJECT, { |
||||||
|
id: base.project_id, |
||||||
|
}); |
||||||
|
|
||||||
|
// skip if the project is missing
|
||||||
|
if (!project) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
const isProjectDeleted = project.deleted; |
||||||
|
|
||||||
|
const knex: Knex = base.is_meta |
||||||
|
? ncMeta.knexConnection |
||||||
|
: await NcConnectionMgrv2.get(base); |
||||||
|
const models = await base.getModels(ncMeta); |
||||||
|
|
||||||
|
// used in timeout error message
|
||||||
|
const timeoutErrorInfo = { |
||||||
|
projectTitle: project.title, |
||||||
|
connection: knex.client.config.connection, |
||||||
|
}; |
||||||
|
|
||||||
|
for (const model of models) { |
||||||
|
try { |
||||||
|
// if the table is missing in database, skip
|
||||||
|
if (!(await knex.schema.hasTable(getTnPath(knex, model)))) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
const updateRecords = []; |
||||||
|
|
||||||
|
// get all attachment & primary key columns
|
||||||
|
// and filter out the columns that are missing in database
|
||||||
|
const columns = await (await Model.get(model.id, ncMeta)) |
||||||
|
.getColumns(ncMeta) |
||||||
|
.then(async (columns) => { |
||||||
|
const filteredColumns = []; |
||||||
|
|
||||||
|
for (const column of columns) { |
||||||
|
if (column.uidt !== UITypes.Attachment && !column.pk) continue; |
||||||
|
if ( |
||||||
|
!(await knex.schema.hasColumn( |
||||||
|
getTnPath(knex, model), |
||||||
|
column.column_name, |
||||||
|
)) |
||||||
|
) |
||||||
|
continue; |
||||||
|
filteredColumns.push(column); |
||||||
|
} |
||||||
|
|
||||||
|
return filteredColumns; |
||||||
|
}); |
||||||
|
|
||||||
|
const attachmentColumns = columns |
||||||
|
.filter((c) => c.uidt === UITypes.Attachment) |
||||||
|
.map((c) => c.column_name); |
||||||
|
if (attachmentColumns.length === 0) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
const primaryKeys = columns |
||||||
|
.filter((c) => c.pk) |
||||||
|
.map((c) => c.column_name); |
||||||
|
|
||||||
|
const records = await knex(getTnPath(knex, model)).select(); |
||||||
|
|
||||||
|
for (const record of records) { |
||||||
|
for (const attachmentColumn of attachmentColumns) { |
||||||
|
let attachmentMeta: Array<{ |
||||||
|
url: string; |
||||||
|
}>; |
||||||
|
|
||||||
|
// if parsing failed ignore the cell
|
||||||
|
try { |
||||||
|
attachmentMeta = |
||||||
|
typeof record[attachmentColumn] === 'string' |
||||||
|
? JSON.parse(record[attachmentColumn]) |
||||||
|
: record[attachmentColumn]; |
||||||
|
} catch {} |
||||||
|
|
||||||
|
// if cell data is not an array, ignore it
|
||||||
|
if (!Array.isArray(attachmentMeta)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (attachmentMeta) { |
||||||
|
const newAttachmentMeta = []; |
||||||
|
for (const attachment of attachmentMeta) { |
||||||
|
if ('url' in attachment && typeof attachment.url === 'string') { |
||||||
|
const match = attachment.url.match(/^(.*)\/download\/(.*)$/); |
||||||
|
if (match) { |
||||||
|
// e.g. http://localhost:8080/download/noco/xcdb/Sheet-1/title5/ee2G8p_nute_gunray.png
|
||||||
|
// match[1] = http://localhost:8080
|
||||||
|
// match[2] = download/noco/xcdb/Sheet-1/title5/ee2G8p_nute_gunray.png
|
||||||
|
const path = `download/${match[2]}`; |
||||||
|
|
||||||
|
newAttachmentMeta.push({ |
||||||
|
...attachment, |
||||||
|
path, |
||||||
|
}); |
||||||
|
} else { |
||||||
|
// keep it as it is
|
||||||
|
newAttachmentMeta.push(attachment); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
const where = primaryKeys |
||||||
|
.map((key) => { |
||||||
|
return { [key]: record[key] }; |
||||||
|
}) |
||||||
|
.reduce((acc, val) => Object.assign(acc, val), {}); |
||||||
|
updateRecords.push( |
||||||
|
await knex(getTnPath(knex, model)) |
||||||
|
.update({ |
||||||
|
[attachmentColumn]: JSON.stringify(newAttachmentMeta), |
||||||
|
}) |
||||||
|
.where(where), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
await Promise.all(updateRecords); |
||||||
|
} catch (e) { |
||||||
|
// ignore the error related to deleted project
|
||||||
|
if (!isProjectDeleted) { |
||||||
|
// throw the custom timeout error message if applicable
|
||||||
|
throwTimeoutError(e, timeoutErrorInfo); |
||||||
|
// throw general error
|
||||||
|
throw e; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,170 @@ |
|||||||
|
import { UITypes } from 'nocodb-sdk'; |
||||||
|
import { XKnex } from '../db/CustomKnex' |
||||||
|
import { MetaTable } from '../utils/globals'; |
||||||
|
import Base from '../models/Base'; |
||||||
|
import Model from '../models/Model'; |
||||||
|
import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; |
||||||
|
import { throwTimeoutError } from './ncUpgradeErrors'; |
||||||
|
import type { Knex } from 'knex'; |
||||||
|
import type { NcUpgraderCtx } from './NcUpgrader'; |
||||||
|
import type { BaseType } from 'nocodb-sdk'; |
||||||
|
|
||||||
|
// after 0101002 upgrader, the attachment object would become broken when
|
||||||
|
// (1) switching views after updating a singleSelect field
|
||||||
|
// since `url` will be enriched the attachment cell, and `saveOrUpdateRecords` in Grid.vue will be triggered
|
||||||
|
// in this way, the attachment value will be corrupted like
|
||||||
|
// {"{\"path\":\"download/noco/xcdb2/attachment2/a/JRubxMQgPlcumdm3jL.jpeg\",\"title\":\"haha.jpeg\",\"mimetype\":\"image/jpeg\",\"size\":6494,\"url\":\"http://localhost:8080/download/noco/xcdb2/attachment2/a/JRubxMQgPlcumdm3jL.jpeg\"}"}
|
||||||
|
// while the expected one is
|
||||||
|
// [{"path":"download/noco/xcdb2/attachment2/a/JRubxMQgPlcumdm3jL.jpeg","title":"haha.jpeg","mimetype":"image/jpeg","size":6494}]
|
||||||
|
// (2) or reordering attachments
|
||||||
|
// since the incoming value is not string, the value will be broken
|
||||||
|
// hence, this upgrader is to revert back these corrupted values
|
||||||
|
|
||||||
|
function getTnPath(knex: XKnex, tb: Model) { |
||||||
|
const schema = (knex as any).searchPath?.(); |
||||||
|
const clientType = knex.clientType(); |
||||||
|
if (clientType === 'mssql' && schema) { |
||||||
|
return knex.raw('??.??', [schema, tb.table_name]).toQuery(); |
||||||
|
} else if (clientType === 'snowflake') { |
||||||
|
return [ |
||||||
|
knex.client.config.connection.database, |
||||||
|
knex.client.config.connection.schema, |
||||||
|
tb.table_name, |
||||||
|
].join('.'); |
||||||
|
} else { |
||||||
|
return tb.table_name; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default async function ({ ncMeta }: NcUpgraderCtx) { |
||||||
|
const bases: BaseType[] = await ncMeta.metaList2(null, null, MetaTable.BASES); |
||||||
|
for (const _base of bases) { |
||||||
|
const base = new Base(_base); |
||||||
|
|
||||||
|
// skip if the project_id is missing
|
||||||
|
if (!base.project_id) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
const project = await ncMeta.metaGet2(null, null, MetaTable.PROJECT, { |
||||||
|
id: base.project_id, |
||||||
|
}); |
||||||
|
|
||||||
|
// skip if the project is missing
|
||||||
|
if (!project) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
const isProjectDeleted = project.deleted; |
||||||
|
|
||||||
|
const knex: Knex = base.is_meta |
||||||
|
? ncMeta.knexConnection |
||||||
|
: await NcConnectionMgrv2.get(base); |
||||||
|
const models = await base.getModels(ncMeta); |
||||||
|
|
||||||
|
// used in timeout error message
|
||||||
|
const timeoutErrorInfo = { |
||||||
|
projectTitle: project.title, |
||||||
|
connection: knex.client.config.connection, |
||||||
|
}; |
||||||
|
|
||||||
|
for (const model of models) { |
||||||
|
try { |
||||||
|
// if the table is missing in database, skip
|
||||||
|
if (!(await knex.schema.hasTable(getTnPath(knex, model)))) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
const updateRecords = []; |
||||||
|
|
||||||
|
// get all attachment & primary key columns
|
||||||
|
// and filter out the columns that are missing in database
|
||||||
|
const columns = await (await Model.get(model.id, ncMeta)) |
||||||
|
.getColumns(ncMeta) |
||||||
|
.then(async (columns) => { |
||||||
|
const filteredColumns = []; |
||||||
|
|
||||||
|
for (const column of columns) { |
||||||
|
if (column.uidt !== UITypes.Attachment && !column.pk) continue; |
||||||
|
if ( |
||||||
|
!(await knex.schema.hasColumn( |
||||||
|
getTnPath(knex, model), |
||||||
|
column.column_name |
||||||
|
)) |
||||||
|
) |
||||||
|
continue; |
||||||
|
filteredColumns.push(column); |
||||||
|
} |
||||||
|
|
||||||
|
return filteredColumns; |
||||||
|
}); |
||||||
|
|
||||||
|
const attachmentColumns = columns |
||||||
|
.filter((c) => c.uidt === UITypes.Attachment) |
||||||
|
.map((c) => c.column_name); |
||||||
|
if (attachmentColumns.length === 0) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
const primaryKeys = columns |
||||||
|
.filter((c) => c.pk) |
||||||
|
.map((c) => c.column_name); |
||||||
|
|
||||||
|
const records = await knex(getTnPath(knex, model)).select(); |
||||||
|
|
||||||
|
for (const record of records) { |
||||||
|
const where = primaryKeys |
||||||
|
.map((key) => { |
||||||
|
return { [key]: record[key] }; |
||||||
|
}) |
||||||
|
.reduce((acc, val) => Object.assign(acc, val), {}); |
||||||
|
for (const attachmentColumn of attachmentColumns) { |
||||||
|
if (typeof record[attachmentColumn] === 'string') { |
||||||
|
// potentially corrupted
|
||||||
|
try { |
||||||
|
JSON.parse(record[attachmentColumn]); |
||||||
|
// it works fine - skip
|
||||||
|
continue; |
||||||
|
} catch { |
||||||
|
try { |
||||||
|
// corrupted
|
||||||
|
let corruptedAttachment = record[attachmentColumn]; |
||||||
|
// replace the first and last character with `[` and `]`
|
||||||
|
// and parse it again
|
||||||
|
corruptedAttachment = JSON.parse( |
||||||
|
`[${corruptedAttachment.slice( |
||||||
|
1, |
||||||
|
corruptedAttachment.length - 1 |
||||||
|
)}]` |
||||||
|
); |
||||||
|
const newAttachmentMeta = []; |
||||||
|
for (const attachment of corruptedAttachment) { |
||||||
|
newAttachmentMeta.push(JSON.parse(attachment)); |
||||||
|
} |
||||||
|
updateRecords.push( |
||||||
|
await knex(getTnPath(knex, model)) |
||||||
|
.update({ |
||||||
|
[attachmentColumn]: JSON.stringify(newAttachmentMeta), |
||||||
|
}) |
||||||
|
.where(where) |
||||||
|
); |
||||||
|
} catch { |
||||||
|
// if parsing failed ignore the cell
|
||||||
|
continue; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
await Promise.all(updateRecords); |
||||||
|
} catch (e) { |
||||||
|
// ignore the error related to deleted project
|
||||||
|
if (!isProjectDeleted) { |
||||||
|
// throw the custom timeout error message if applicable
|
||||||
|
throwTimeoutError(e, timeoutErrorInfo); |
||||||
|
// throw general error
|
||||||
|
throw e; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
import { UITypes } from 'nocodb-sdk'; |
||||||
|
import { MetaTable } from '../utils/globals'; |
||||||
|
import type { NcUpgraderCtx } from './NcUpgrader'; |
||||||
|
|
||||||
|
// The Count and AutoNumber types are removed
|
||||||
|
// so convert all existing Count and AutoNumber fields to Number type
|
||||||
|
export default async function (ctx: NcUpgraderCtx) { |
||||||
|
// directly update uidt of all existing Count and AutoNumber fields to Number
|
||||||
|
await ctx.ncMeta.knex |
||||||
|
.update({ uidt: UITypes.Number }) |
||||||
|
.where({ uidt: UITypes.Count }) |
||||||
|
.orWhere({ uidt: UITypes.AutoNumber }) |
||||||
|
.table(MetaTable.COLUMNS); |
||||||
|
} |
@ -0,0 +1,40 @@ |
|||||||
|
import { MetaTable } from '../utils/globals'; |
||||||
|
import View from '../models/View'; |
||||||
|
import Hook from '../models/Hook'; |
||||||
|
import Column from '../models/Column'; |
||||||
|
import type { NcUpgraderCtx } from './NcUpgrader'; |
||||||
|
|
||||||
|
// before 0.101.0, an incorrect project_id was inserted when
|
||||||
|
// a filter is created without specifying the column
|
||||||
|
// this upgrader is to retrieve the correct project id from either view, hook, or column
|
||||||
|
// and update the project id
|
||||||
|
export default async function ({ ncMeta }: NcUpgraderCtx) { |
||||||
|
const filters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP); |
||||||
|
for (const filter of filters) { |
||||||
|
let model: { project_id?: string; base_id?: string }; |
||||||
|
if (filter.fk_view_id) { |
||||||
|
model = await View.get(filter.fk_view_id, ncMeta); |
||||||
|
} else if (filter.fk_hook_id) { |
||||||
|
model = await Hook.get(filter.fk_hook_id, ncMeta); |
||||||
|
} else if (filter.fk_column_id) { |
||||||
|
model = await Column.get({ colId: filter.fk_column_id }, ncMeta); |
||||||
|
} else { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
// skip if related model is not found
|
||||||
|
if (!model) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (filter.project_id !== model.project_id) { |
||||||
|
await ncMeta.metaUpdate( |
||||||
|
null, |
||||||
|
null, |
||||||
|
MetaTable.FILTER_EXP, |
||||||
|
{ base_id: model.base_id, project_id: model.project_id }, |
||||||
|
filter.id |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,359 @@ |
|||||||
|
import { UITypes } from 'nocodb-sdk'; |
||||||
|
import { MetaService } from '../meta/meta.service' |
||||||
|
import { MetaTable } from '../utils/globals'; |
||||||
|
import Column from '../models/Column'; |
||||||
|
import Filter from '../models/Filter'; |
||||||
|
import Project from '../models/Project'; |
||||||
|
import type { NcUpgraderCtx } from './NcUpgrader'; |
||||||
|
import type { SelectOptionsType } from 'nocodb-sdk'; |
||||||
|
|
||||||
|
// as of 0.104.3, almost all filter operators are available to all column types
|
||||||
|
// while some of them aren't supposed to be shown
|
||||||
|
// this upgrader is to remove those unsupported filters / migrate to the correct filter
|
||||||
|
|
||||||
|
// Change Summary:
|
||||||
|
// - Text-based columns:
|
||||||
|
// - remove `>`, `<`, `>=`, `<=`
|
||||||
|
// - Numeric-based / SingleSelect columns:
|
||||||
|
// - remove `like`
|
||||||
|
// - migrate `null`, and `empty` to `blank`
|
||||||
|
// - Checkbox columns:
|
||||||
|
// - remove `equal`
|
||||||
|
// - migrate `empty` and `null` to `notchecked`
|
||||||
|
// - MultiSelect columns:
|
||||||
|
// - remove `like`
|
||||||
|
// - migrate `equal`, `null`, `empty`
|
||||||
|
// - Attachment columns:
|
||||||
|
// - remove `>`, `<`, `>=`, `<=`, `equal`
|
||||||
|
// - migrate `empty`, `null` to `blank`
|
||||||
|
// - LTAR columns:
|
||||||
|
// - remove `>`, `<`, `>=`, `<=`
|
||||||
|
// - migrate `empty`, `null` to `blank`
|
||||||
|
// - Lookup columns:
|
||||||
|
// - migrate `empty`, `null` to `blank`
|
||||||
|
// - Duration columns:
|
||||||
|
// - remove `like`
|
||||||
|
// - migrate `empty`, `null` to `blank`
|
||||||
|
|
||||||
|
const removeEqualFilters = (filter, ncMeta) => { |
||||||
|
const actions = []; |
||||||
|
// remove `is equal`, `is not equal`
|
||||||
|
if (['eq', 'neq'].includes(filter.comparison_op)) { |
||||||
|
actions.push(Filter.delete(filter.id, ncMeta)); |
||||||
|
} |
||||||
|
return actions; |
||||||
|
}; |
||||||
|
|
||||||
|
const removeArithmeticFilters = (filter, ncMeta) => { |
||||||
|
const actions = []; |
||||||
|
// remove `>`, `<`, `>=`, `<=`
|
||||||
|
if (['gt', 'lt', 'gte', 'lte'].includes(filter.comparison_op)) { |
||||||
|
actions.push(Filter.delete(filter.id, ncMeta)); |
||||||
|
} |
||||||
|
return actions; |
||||||
|
}; |
||||||
|
|
||||||
|
const removeLikeFilters = (filter, ncMeta) => { |
||||||
|
const actions = []; |
||||||
|
// remove `is like`, `is not like`
|
||||||
|
if (['like', 'nlike'].includes(filter.comparison_op)) { |
||||||
|
actions.push(Filter.delete(filter.id, ncMeta)); |
||||||
|
} |
||||||
|
return actions; |
||||||
|
}; |
||||||
|
|
||||||
|
const migrateNullAndEmptyToBlankFilters = (filter, ncMeta) => { |
||||||
|
const actions = []; |
||||||
|
if (['empty', 'null'].includes(filter.comparison_op)) { |
||||||
|
// migrate to blank
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'blank', |
||||||
|
}, |
||||||
|
ncMeta |
||||||
|
) |
||||||
|
); |
||||||
|
} else if (['notempty', 'notnull'].includes(filter.comparison_op)) { |
||||||
|
// migrate to not blank
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'notblank', |
||||||
|
}, |
||||||
|
ncMeta |
||||||
|
) |
||||||
|
); |
||||||
|
} |
||||||
|
return actions; |
||||||
|
}; |
||||||
|
|
||||||
|
const migrateMultiSelectEq = async (filter, col: Column, ncMeta) => { |
||||||
|
// only allow eq / neq
|
||||||
|
if (!['eq', 'neq'].includes(filter.comparison_op)) return; |
||||||
|
// if there is no value -> delete this filter
|
||||||
|
if (!filter.value) { |
||||||
|
return await Filter.delete(filter.id, ncMeta); |
||||||
|
} |
||||||
|
// options inputted from users
|
||||||
|
const options = filter.value.split(','); |
||||||
|
// retrieve the possible col options
|
||||||
|
const colOptions = (await col.getColOptions()) as SelectOptionsType; |
||||||
|
// only include valid options as the input value becomes dropdown type now
|
||||||
|
const validOptions = []; |
||||||
|
for (const option of options) { |
||||||
|
if (colOptions.options.includes(option)) { |
||||||
|
validOptions.push(option); |
||||||
|
} |
||||||
|
} |
||||||
|
const newFilterValue = validOptions.join(','); |
||||||
|
// if all inputted options are invalid -> delete this filter
|
||||||
|
if (!newFilterValue) { |
||||||
|
return await Filter.delete(filter.id, ncMeta); |
||||||
|
} |
||||||
|
const actions = []; |
||||||
|
if (filter.comparison_op === 'eq') { |
||||||
|
// migrate to `contains all of`
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'anyof', |
||||||
|
value: newFilterValue, |
||||||
|
}, |
||||||
|
ncMeta |
||||||
|
) |
||||||
|
); |
||||||
|
} else if (filter.comparison_op === 'neq') { |
||||||
|
// migrate to `doesn't contain all of`
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'nanyof', |
||||||
|
value: newFilterValue, |
||||||
|
}, |
||||||
|
ncMeta |
||||||
|
) |
||||||
|
); |
||||||
|
} |
||||||
|
return await Promise.all(actions); |
||||||
|
}; |
||||||
|
|
||||||
|
const migrateToCheckboxFilter = (filter, ncMeta) => { |
||||||
|
const actions = []; |
||||||
|
const possibleTrueValues = ['true', 'True', '1', 'T', 'Y']; |
||||||
|
const possibleFalseValues = ['false', 'False', '0', 'F', 'N']; |
||||||
|
if (['empty', 'null'].includes(filter.comparison_op)) { |
||||||
|
// migrate to not checked
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'notchecked', |
||||||
|
}, |
||||||
|
ncMeta |
||||||
|
) |
||||||
|
); |
||||||
|
} else if (['notempty', 'notnull'].includes(filter.comparison_op)) { |
||||||
|
// migrate to checked
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'checked', |
||||||
|
}, |
||||||
|
ncMeta |
||||||
|
) |
||||||
|
); |
||||||
|
} else if (filter.comparison_op === 'eq') { |
||||||
|
if (possibleTrueValues.includes(filter.value)) { |
||||||
|
// migrate to checked
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'checked', |
||||||
|
value: '', |
||||||
|
}, |
||||||
|
ncMeta |
||||||
|
) |
||||||
|
); |
||||||
|
} else if (possibleFalseValues.includes(filter.value)) { |
||||||
|
// migrate to notchecked
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'notchecked', |
||||||
|
value: '', |
||||||
|
}, |
||||||
|
ncMeta |
||||||
|
) |
||||||
|
); |
||||||
|
} else { |
||||||
|
// invalid value - good to delete
|
||||||
|
actions.push(Filter.delete(filter.id, ncMeta)); |
||||||
|
} |
||||||
|
} else if (filter.comparison_op === 'neq') { |
||||||
|
if (possibleFalseValues.includes(filter.value)) { |
||||||
|
// migrate to checked
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'checked', |
||||||
|
value: '', |
||||||
|
}, |
||||||
|
ncMeta |
||||||
|
) |
||||||
|
); |
||||||
|
} else if (possibleTrueValues.includes(filter.value)) { |
||||||
|
// migrate to not checked
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'notchecked', |
||||||
|
value: '', |
||||||
|
}, |
||||||
|
ncMeta |
||||||
|
) |
||||||
|
); |
||||||
|
} else { |
||||||
|
// invalid value - good to delete
|
||||||
|
actions.push(Filter.delete(filter.id, ncMeta)); |
||||||
|
} |
||||||
|
} |
||||||
|
return actions; |
||||||
|
}; |
||||||
|
|
||||||
|
async function migrateFilters(ncMeta: MetaService) { |
||||||
|
const filters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP); |
||||||
|
for (const filter of filters) { |
||||||
|
if (!filter.fk_column_id || filter.is_group) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
const col = await Column.get({ colId: filter.fk_column_id }, ncMeta); |
||||||
|
if ( |
||||||
|
[ |
||||||
|
UITypes.SingleLineText, |
||||||
|
UITypes.LongText, |
||||||
|
UITypes.PhoneNumber, |
||||||
|
UITypes.Email, |
||||||
|
UITypes.URL, |
||||||
|
].includes(col.uidt) |
||||||
|
) { |
||||||
|
await Promise.all(removeArithmeticFilters(filter, ncMeta)); |
||||||
|
} else if ( |
||||||
|
[ |
||||||
|
// numeric fields
|
||||||
|
UITypes.Duration, |
||||||
|
UITypes.Currency, |
||||||
|
UITypes.Percent, |
||||||
|
UITypes.Number, |
||||||
|
UITypes.Decimal, |
||||||
|
UITypes.Rating, |
||||||
|
UITypes.Rollup, |
||||||
|
// select fields
|
||||||
|
UITypes.SingleSelect, |
||||||
|
].includes(col.uidt) |
||||||
|
) { |
||||||
|
await Promise.all([ |
||||||
|
...removeLikeFilters(filter, ncMeta), |
||||||
|
...migrateNullAndEmptyToBlankFilters(filter, ncMeta), |
||||||
|
]); |
||||||
|
} else if (col.uidt === UITypes.Checkbox) { |
||||||
|
await Promise.all(migrateToCheckboxFilter(filter, ncMeta)); |
||||||
|
} else if (col.uidt === UITypes.MultiSelect) { |
||||||
|
await Promise.all([ |
||||||
|
...removeLikeFilters(filter, ncMeta), |
||||||
|
...migrateNullAndEmptyToBlankFilters(filter, ncMeta), |
||||||
|
]); |
||||||
|
await migrateMultiSelectEq(filter, col, ncMeta); |
||||||
|
} else if (col.uidt === UITypes.Attachment) { |
||||||
|
await Promise.all([ |
||||||
|
...removeArithmeticFilters(filter, ncMeta), |
||||||
|
...removeEqualFilters(filter, ncMeta), |
||||||
|
...migrateNullAndEmptyToBlankFilters(filter, ncMeta), |
||||||
|
]); |
||||||
|
} else if (col.uidt === UITypes.LinkToAnotherRecord) { |
||||||
|
await Promise.all([ |
||||||
|
...removeArithmeticFilters(filter, ncMeta), |
||||||
|
...migrateNullAndEmptyToBlankFilters(filter, ncMeta), |
||||||
|
]); |
||||||
|
} else if (col.uidt === UITypes.Lookup) { |
||||||
|
await Promise.all([ |
||||||
|
...removeArithmeticFilters(filter, ncMeta), |
||||||
|
...migrateNullAndEmptyToBlankFilters(filter, ncMeta), |
||||||
|
]); |
||||||
|
} else if (col.uidt === UITypes.Duration) { |
||||||
|
await Promise.all([ |
||||||
|
...removeLikeFilters(filter, ncMeta), |
||||||
|
...migrateNullAndEmptyToBlankFilters(filter, ncMeta), |
||||||
|
]); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function updateProjectMeta(ncMeta: MetaService) { |
||||||
|
const projectHasEmptyOrFilters: Record<string, boolean> = {}; |
||||||
|
|
||||||
|
const filters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP); |
||||||
|
|
||||||
|
const actions = []; |
||||||
|
|
||||||
|
for (const filter of filters) { |
||||||
|
if ( |
||||||
|
['notempty', 'notnull', 'empty', 'null'].includes(filter.comparison_op) |
||||||
|
) { |
||||||
|
projectHasEmptyOrFilters[filter.project_id] = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const projects = await ncMeta.metaList2(null, null, MetaTable.PROJECT); |
||||||
|
|
||||||
|
const defaultProjectMeta = { |
||||||
|
showNullAndEmptyInFilter: false, |
||||||
|
}; |
||||||
|
|
||||||
|
for (const project of projects) { |
||||||
|
const oldProjectMeta = project.meta; |
||||||
|
let newProjectMeta = defaultProjectMeta; |
||||||
|
try { |
||||||
|
newProjectMeta = |
||||||
|
(typeof oldProjectMeta === 'string' |
||||||
|
? JSON.parse(oldProjectMeta) |
||||||
|
: oldProjectMeta) ?? defaultProjectMeta; |
||||||
|
} catch {} |
||||||
|
|
||||||
|
newProjectMeta = { |
||||||
|
...newProjectMeta, |
||||||
|
showNullAndEmptyInFilter: projectHasEmptyOrFilters[project.id] ?? false, |
||||||
|
}; |
||||||
|
|
||||||
|
actions.push( |
||||||
|
Project.update( |
||||||
|
project.id, |
||||||
|
{ |
||||||
|
meta: JSON.stringify(newProjectMeta), |
||||||
|
}, |
||||||
|
ncMeta |
||||||
|
) |
||||||
|
); |
||||||
|
} |
||||||
|
await Promise.all(actions); |
||||||
|
} |
||||||
|
|
||||||
|
export default async function ({ ncMeta }: NcUpgraderCtx) { |
||||||
|
// fix the existing filter behaviours or
|
||||||
|
// migrate `null` or `empty` filters to `blank`
|
||||||
|
await migrateFilters(ncMeta); |
||||||
|
// enrich `showNullAndEmptyInFilter` in project meta
|
||||||
|
// if there is empty / null filters in existing projects,
|
||||||
|
// then set `showNullAndEmptyInFilter` to true
|
||||||
|
// else set to false
|
||||||
|
await updateProjectMeta(ncMeta); |
||||||
|
} |
@ -0,0 +1,101 @@ |
|||||||
|
import { UITypes } from 'nocodb-sdk'; |
||||||
|
import { MetaService } from '../meta/meta.service'; |
||||||
|
import { MetaTable } from '../utils/globals'; |
||||||
|
import Column from '../models/Column'; |
||||||
|
import Filter from '../models/Filter'; |
||||||
|
import type { NcUpgraderCtx } from './NcUpgrader'; |
||||||
|
|
||||||
|
// as of 0.105.3, year, time, date and datetime filters include `is like` and `is not like` which are not practical
|
||||||
|
// `removeLikeAndNlikeFilters` in this upgrader is simply to remove them
|
||||||
|
|
||||||
|
// besides, `null` and `empty` will be migrated to `blank` in `migrateEmptyAndNullFilters`
|
||||||
|
|
||||||
|
// since the upcoming version will introduce a set of new filters for date / datetime with a new `comparison_sub_op`
|
||||||
|
// `eq` and `neq` would become `is` / `is not` (comparison_op) + `exact date` (comparison_sub_op)
|
||||||
|
// `migrateEqAndNeqFilters` in this upgrader is to add `exact date` in comparison_sub_op
|
||||||
|
|
||||||
|
// Change Summary:
|
||||||
|
// - Date / DateTime columns:
|
||||||
|
// - remove `is like` and `is not like`
|
||||||
|
// - migrate `null` or `empty` filters to `blank`
|
||||||
|
// - add `exact date` in comparison_sub_op for existing filters `eq` and `neq`
|
||||||
|
// - Year / Time columns:
|
||||||
|
// - remove `is like` and `is not like`
|
||||||
|
// - migrate `null` or `empty` filters to `blank`
|
||||||
|
|
||||||
|
function removeLikeAndNlikeFilters(filter: Filter, ncMeta: MetaService) { |
||||||
|
const actions = []; |
||||||
|
// remove `is like` and `is not like`
|
||||||
|
if (['like', 'nlike'].includes(filter.comparison_op)) { |
||||||
|
actions.push(Filter.delete(filter.id, ncMeta)); |
||||||
|
} |
||||||
|
return actions; |
||||||
|
} |
||||||
|
|
||||||
|
function migrateEqAndNeqFilters(filter: Filter, ncMeta: MetaService) { |
||||||
|
const actions = []; |
||||||
|
// remove `is like` and `is not like`
|
||||||
|
if (['eq', 'neq'].includes(filter.comparison_op)) { |
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_sub_op: 'exactDate', |
||||||
|
}, |
||||||
|
ncMeta, |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
return actions; |
||||||
|
} |
||||||
|
|
||||||
|
function migrateEmptyAndNullFilters(filter: Filter, ncMeta: MetaService) { |
||||||
|
const actions = []; |
||||||
|
// remove `is like` and `is not like`
|
||||||
|
if (['empty', 'null'].includes(filter.comparison_op)) { |
||||||
|
// migrate to blank
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'blank', |
||||||
|
}, |
||||||
|
ncMeta, |
||||||
|
), |
||||||
|
); |
||||||
|
} else if (['notempty', 'notnull'].includes(filter.comparison_op)) { |
||||||
|
// migrate to not blank
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'notblank', |
||||||
|
}, |
||||||
|
ncMeta, |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
return actions; |
||||||
|
} |
||||||
|
|
||||||
|
export default async function ({ ncMeta }: NcUpgraderCtx) { |
||||||
|
const filters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP); |
||||||
|
for (const filter of filters) { |
||||||
|
if (!filter.fk_column_id || filter.is_group) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
const col = await Column.get({ colId: filter.fk_column_id }, ncMeta); |
||||||
|
if ([UITypes.Date, UITypes.DateTime].includes(col.uidt)) { |
||||||
|
await Promise.all([ |
||||||
|
...removeLikeAndNlikeFilters(filter, ncMeta), |
||||||
|
...migrateEmptyAndNullFilters(filter, ncMeta), |
||||||
|
...migrateEqAndNeqFilters(filter, ncMeta), |
||||||
|
]); |
||||||
|
} else if ([UITypes.Time, UITypes.Year].includes(col.uidt)) { |
||||||
|
await Promise.all([ |
||||||
|
...removeLikeAndNlikeFilters(filter, ncMeta), |
||||||
|
...migrateEmptyAndNullFilters(filter, ncMeta), |
||||||
|
]); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
import { MetaTable } from '../utils/globals'; |
||||||
|
import type { NcUpgraderCtx } from './NcUpgrader'; |
||||||
|
|
||||||
|
export default async function ({ ncMeta }: NcUpgraderCtx) { |
||||||
|
const actions = []; |
||||||
|
const hooks = await ncMeta.metaList2(null, null, MetaTable.HOOKS); |
||||||
|
for (const hook of hooks) { |
||||||
|
actions.push( |
||||||
|
ncMeta.metaUpdate( |
||||||
|
null, |
||||||
|
null, |
||||||
|
MetaTable.HOOKS, |
||||||
|
{ version: 'v1' }, |
||||||
|
hook.id, |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
await Promise.all(actions); |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
import type { NcUpgraderCtx } from './NcUpgrader'; |
||||||
|
|
||||||
|
export default async function (ctx: NcUpgraderCtx) { |
||||||
|
const projects = await ctx.ncMeta.projectList(); |
||||||
|
|
||||||
|
for (const project of projects) { |
||||||
|
const projectConfig = JSON.parse(project.config); |
||||||
|
|
||||||
|
const envVal = projectConfig.envs?.dev; |
||||||
|
projectConfig.workingEnv = '_noco'; |
||||||
|
|
||||||
|
if (envVal) { |
||||||
|
projectConfig.envs._noco = envVal; |
||||||
|
delete projectConfig.envs.dev; |
||||||
|
} |
||||||
|
await ctx.ncMeta.projectUpdate(project?.id, projectConfig); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
import type { NcUpgraderCtx } from './NcUpgrader'; |
||||||
|
|
||||||
|
export default async function (ctx: NcUpgraderCtx) { |
||||||
|
const projects = await ctx.ncMeta.projectList(); |
||||||
|
|
||||||
|
for (const project of projects) { |
||||||
|
const projectConfig = JSON.parse(project.config); |
||||||
|
projectConfig.env = '_noco'; |
||||||
|
await ctx.ncMeta.projectUpdate(project?.id, projectConfig); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
import { OrgUserRoles } from 'nocodb-sdk'; |
||||||
|
import { NC_APP_SETTINGS } from '../constants'; |
||||||
|
import Store from '../models/Store'; |
||||||
|
import { MetaTable } from '../utils/globals'; |
||||||
|
import type { NcUpgraderCtx } from './NcUpgrader'; |
||||||
|
|
||||||
|
/** Upgrader for upgrading roles */ |
||||||
|
export default async function ({ ncMeta }: NcUpgraderCtx) { |
||||||
|
const users = await ncMeta.metaList2(null, null, MetaTable.USERS); |
||||||
|
|
||||||
|
for (const user of users) { |
||||||
|
user.roles = user.roles |
||||||
|
.split(',') |
||||||
|
.map((r) => { |
||||||
|
// update old role names with new roles
|
||||||
|
if (r === 'user') { |
||||||
|
return OrgUserRoles.CREATOR; |
||||||
|
} else if (r === 'user-new') { |
||||||
|
return OrgUserRoles.VIEWER; |
||||||
|
} |
||||||
|
return r; |
||||||
|
}) |
||||||
|
.join(','); |
||||||
|
await ncMeta.metaUpdate( |
||||||
|
null, |
||||||
|
null, |
||||||
|
MetaTable.USERS, |
||||||
|
{ roles: user.roles }, |
||||||
|
user.id, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// set invite only signup if user have environment variable set
|
||||||
|
if (process.env.NC_INVITE_ONLY_SIGNUP) { |
||||||
|
await Store.saveOrUpdate( |
||||||
|
{ |
||||||
|
value: '{ "invite_only_signup": true }', |
||||||
|
key: NC_APP_SETTINGS, |
||||||
|
}, |
||||||
|
ncMeta, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,134 @@ |
|||||||
|
import { MetaTable } from '../utils/globals'; |
||||||
|
import type { NcUpgraderCtx } from './NcUpgrader'; |
||||||
|
|
||||||
|
// before 0.104.3, display value column can be in any position in table
|
||||||
|
// with this upgrade we introduced sticky primary column feature
|
||||||
|
// this upgrader will make display value column first column in grid views
|
||||||
|
|
||||||
|
export default async function ({ ncMeta }: NcUpgraderCtx) { |
||||||
|
const grid_columns = await ncMeta.metaList2( |
||||||
|
null, |
||||||
|
null, |
||||||
|
MetaTable.GRID_VIEW_COLUMNS, |
||||||
|
); |
||||||
|
const grid_views = [...new Set(grid_columns.map((col) => col.fk_view_id))]; |
||||||
|
|
||||||
|
for (const view_id of grid_views) { |
||||||
|
// get a list of view columns sorted by order
|
||||||
|
const view_columns = await ncMeta.metaList2( |
||||||
|
null, |
||||||
|
null, |
||||||
|
MetaTable.GRID_VIEW_COLUMNS, |
||||||
|
{ |
||||||
|
condition: { |
||||||
|
fk_view_id: view_id, |
||||||
|
}, |
||||||
|
orderBy: { |
||||||
|
order: 'asc', |
||||||
|
}, |
||||||
|
}, |
||||||
|
); |
||||||
|
const view_columns_meta = []; |
||||||
|
|
||||||
|
// get column meta for each view column
|
||||||
|
for (const col of view_columns) { |
||||||
|
const col_meta = await ncMeta.metaGet(null, null, MetaTable.COLUMNS, { |
||||||
|
id: col.fk_column_id, |
||||||
|
}); |
||||||
|
view_columns_meta.push(col_meta); |
||||||
|
} |
||||||
|
|
||||||
|
// if no display value column is set
|
||||||
|
if (!view_columns_meta.some((column) => column.pv)) { |
||||||
|
const pkIndex = view_columns_meta.findIndex((column) => column.pk); |
||||||
|
|
||||||
|
// if PK is at the end of table
|
||||||
|
if (pkIndex === view_columns_meta.length - 1) { |
||||||
|
if (pkIndex > 0) { |
||||||
|
await ncMeta.metaUpdate( |
||||||
|
null, |
||||||
|
null, |
||||||
|
MetaTable.COLUMNS, |
||||||
|
{ pv: true }, |
||||||
|
view_columns_meta[pkIndex - 1].id, |
||||||
|
); |
||||||
|
} else if (view_columns_meta.length > 0) { |
||||||
|
await ncMeta.metaUpdate( |
||||||
|
null, |
||||||
|
null, |
||||||
|
MetaTable.COLUMNS, |
||||||
|
{ pv: true }, |
||||||
|
view_columns_meta[0].id, |
||||||
|
); |
||||||
|
} |
||||||
|
// pk is not at the end of table
|
||||||
|
} else if (pkIndex > -1) { |
||||||
|
await ncMeta.metaUpdate( |
||||||
|
null, |
||||||
|
null, |
||||||
|
MetaTable.COLUMNS, |
||||||
|
{ pv: true }, |
||||||
|
view_columns_meta[pkIndex + 1].id, |
||||||
|
); |
||||||
|
// no pk at all
|
||||||
|
} else if (view_columns_meta.length > 0) { |
||||||
|
await ncMeta.metaUpdate( |
||||||
|
null, |
||||||
|
null, |
||||||
|
MetaTable.COLUMNS, |
||||||
|
{ pv: true }, |
||||||
|
view_columns_meta[0].id, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const primary_value_column_meta = view_columns_meta.find((col) => col.pv); |
||||||
|
|
||||||
|
if (primary_value_column_meta) { |
||||||
|
const primary_value_column = view_columns.find( |
||||||
|
(col) => col.fk_column_id === primary_value_column_meta.id, |
||||||
|
); |
||||||
|
const primary_value_column_index = view_columns.findIndex( |
||||||
|
(col) => col.fk_column_id === primary_value_column_meta.id, |
||||||
|
); |
||||||
|
const view_orders = view_columns.map((col) => col.order); |
||||||
|
const view_min_order = Math.min(...view_orders); |
||||||
|
|
||||||
|
// if primary_value_column is not visible, make it visible
|
||||||
|
if (!primary_value_column.show) { |
||||||
|
await ncMeta.metaUpdate( |
||||||
|
null, |
||||||
|
null, |
||||||
|
MetaTable.GRID_VIEW_COLUMNS, |
||||||
|
{ show: true }, |
||||||
|
primary_value_column.id, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
if ( |
||||||
|
primary_value_column.order === view_min_order && |
||||||
|
view_orders.filter((o) => o === view_min_order).length === 1 |
||||||
|
) { |
||||||
|
// if primary_value_column is in first order do nothing
|
||||||
|
continue; |
||||||
|
} else { |
||||||
|
// if primary_value_column not in first order, move it to the start of array
|
||||||
|
if (primary_value_column_index !== 0) { |
||||||
|
const temp_pv = view_columns.splice(primary_value_column_index, 1); |
||||||
|
view_columns.unshift(...temp_pv); |
||||||
|
} |
||||||
|
|
||||||
|
// update order of all columns in view to match the order in array
|
||||||
|
for (let i = 0; i < view_columns.length; i++) { |
||||||
|
await ncMeta.metaUpdate( |
||||||
|
null, |
||||||
|
null, |
||||||
|
MetaTable.GRID_VIEW_COLUMNS, |
||||||
|
{ order: i + 1 }, |
||||||
|
view_columns[i].id, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
export function throwTimeoutError(e, timeoutErrorInfo) { |
||||||
|
if ( |
||||||
|
[ |
||||||
|
'EHOSTDOWN', |
||||||
|
'ETIMEDOUT', |
||||||
|
'EHOSTUNREACH', |
||||||
|
'ENOTFOUND', |
||||||
|
'ECONNREFUSED', |
||||||
|
].includes(e.code) |
||||||
|
) { |
||||||
|
let db = ''; |
||||||
|
if (timeoutErrorInfo.connection.filename) { |
||||||
|
// for sqlite
|
||||||
|
db = timeoutErrorInfo.connection.filename; |
||||||
|
} else if ( |
||||||
|
timeoutErrorInfo.connection.database && |
||||||
|
timeoutErrorInfo.connection.host && |
||||||
|
timeoutErrorInfo.connection.port |
||||||
|
) { |
||||||
|
db = `${timeoutErrorInfo.connection.database} (${timeoutErrorInfo.connection.host}:${timeoutErrorInfo.connection.port})`; |
||||||
|
} |
||||||
|
throw new Error( |
||||||
|
`Failed to connect the database ${db} for Project ${timeoutErrorInfo.projectTitle}.
|
||||||
|
Please fix the connection issue or remove the project before trying to upgrade.` |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,611 @@ |
|||||||
|
import debug from 'debug'; |
||||||
|
import { Router } from 'express'; |
||||||
|
import { BaseModelSql } from '../../db/BaseModelSql'; |
||||||
|
import { XKnex } from '../../db/CustomKnex'; |
||||||
|
import ModelXcMetaFactory from '../../db/sql-mgr/code/models/xc/ModelXcMetaFactory' |
||||||
|
import { DbConfig, NcConfig } from '../../interface/config'; |
||||||
|
import inflection from 'inflection'; |
||||||
|
import { MetaService } from '../../meta/meta.service'; |
||||||
|
import NcConnectionMgr from '../../utils/common/NcConnectionMgr'; |
||||||
|
import ncModelsOrderUpgrader from './jobs/ncModelsOrderUpgrader'; |
||||||
|
import ncParentModelTitleUpgrader from './jobs/ncParentModelTitleUpgrader'; |
||||||
|
import ncRemoveDuplicatedRelationRows from './jobs/ncRemoveDuplicatedRelationRows'; |
||||||
|
import type Noco from '../../Noco'; |
||||||
|
import type NcProjectBuilder from './NcProjectBuilder'; |
||||||
|
import type { MysqlClient, PgClient, SqlClient } from 'nc-help'; |
||||||
|
|
||||||
|
const log = debug('nc:api:base'); |
||||||
|
|
||||||
|
export default abstract class BaseApiBuilder<T extends Noco> { |
||||||
|
public abstract readonly type: string; |
||||||
|
|
||||||
|
public get knex(): XKnex { |
||||||
|
return this.sqlClient?.knex || this.dbDriver; |
||||||
|
} |
||||||
|
|
||||||
|
public get prefix() { |
||||||
|
return this.projectBuilder?.prefix; |
||||||
|
} |
||||||
|
|
||||||
|
public get apiType(): string { |
||||||
|
return this.connectionConfig?.meta?.api?.type; |
||||||
|
} |
||||||
|
|
||||||
|
public get apiPrefix(): string { |
||||||
|
return this.connectionConfig?.meta?.api?.prefix; |
||||||
|
} |
||||||
|
|
||||||
|
public get dbAlias(): any { |
||||||
|
return this.connectionConfig?.meta?.dbAlias; |
||||||
|
} |
||||||
|
|
||||||
|
public get router(): Router { |
||||||
|
if (!this.apiRouter) { |
||||||
|
this.baseLog(`router : Initializing builder router`); |
||||||
|
this.apiRouter = Router(); |
||||||
|
// (this.app as any).router.use('/', this.apiRouter)
|
||||||
|
(this.projectBuilder as any).router.use('/', this.apiRouter); |
||||||
|
} |
||||||
|
return this.apiRouter; |
||||||
|
} |
||||||
|
|
||||||
|
public get routeVersionLetter(): string { |
||||||
|
return this.connectionConfig?.meta?.api?.prefix || 'v1'; |
||||||
|
} |
||||||
|
|
||||||
|
protected get projectId(): string { |
||||||
|
return this.projectBuilder?.id; |
||||||
|
} |
||||||
|
|
||||||
|
public get xcModels() { |
||||||
|
return this.models; |
||||||
|
} |
||||||
|
|
||||||
|
public get client() { |
||||||
|
return this.connectionConfig?.client; |
||||||
|
} |
||||||
|
|
||||||
|
public readonly app: T; |
||||||
|
|
||||||
|
public hooks: { |
||||||
|
[tableName: string]: { |
||||||
|
[key: string]: Array<{ |
||||||
|
event: string; |
||||||
|
url: string; |
||||||
|
[key: string]: any; |
||||||
|
}>; |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
public formViews: { |
||||||
|
[tableName: string]: any; |
||||||
|
}; |
||||||
|
|
||||||
|
protected tablesCount = 0; |
||||||
|
protected relationsCount = 0; |
||||||
|
protected viewsCount = 0; |
||||||
|
protected functionsCount = 0; |
||||||
|
protected proceduresCount = 0; |
||||||
|
|
||||||
|
protected projectBuilder: NcProjectBuilder; |
||||||
|
|
||||||
|
protected models: { [key: string]: BaseModelSql }; |
||||||
|
|
||||||
|
protected metas: { [key: string]: NcMetaData }; |
||||||
|
|
||||||
|
protected sqlClient: MysqlClient | PgClient | SqlClient | any; |
||||||
|
|
||||||
|
protected dbDriver: XKnex; |
||||||
|
protected config: NcConfig; |
||||||
|
protected connectionConfig: DbConfig; |
||||||
|
|
||||||
|
protected procedureOrFunctionAcls: { |
||||||
|
[name: string]: { [role: string]: boolean }; |
||||||
|
}; |
||||||
|
protected xcMeta: MetaService; |
||||||
|
|
||||||
|
private apiRouter: Router; |
||||||
|
|
||||||
|
constructor( |
||||||
|
app: T, |
||||||
|
projectBuilder: NcProjectBuilder, |
||||||
|
config: NcConfig, |
||||||
|
connectionConfig: DbConfig, |
||||||
|
) { |
||||||
|
this.models = {}; |
||||||
|
this.app = app; |
||||||
|
this.config = config; |
||||||
|
this.connectionConfig = connectionConfig; |
||||||
|
this.metas = {}; |
||||||
|
this.procedureOrFunctionAcls = {}; |
||||||
|
this.hooks = {}; |
||||||
|
this.formViews = {}; |
||||||
|
this.projectBuilder = projectBuilder; |
||||||
|
} |
||||||
|
|
||||||
|
public getDbType(): any { |
||||||
|
return this.connectionConfig.client; |
||||||
|
} |
||||||
|
|
||||||
|
public getDbName(): any { |
||||||
|
return (this.connectionConfig.connection as any)?.database; |
||||||
|
} |
||||||
|
|
||||||
|
public getDbAlias(): any { |
||||||
|
return this.connectionConfig?.meta?.dbAlias; |
||||||
|
} |
||||||
|
|
||||||
|
public async getSqlClient() { |
||||||
|
return NcConnectionMgr.getSqlClient({ |
||||||
|
dbAlias: this.dbAlias, |
||||||
|
env: this.config.env, |
||||||
|
config: this.config, |
||||||
|
projectId: this.projectId, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public async xcUpgrade(): Promise<any> { |
||||||
|
const NC_VERSIONS = [ |
||||||
|
{ name: '0009000', handler: null }, |
||||||
|
{ name: '0009044', handler: this.ncUpManyToMany.bind(this) }, |
||||||
|
{ name: '0083006', handler: ncModelsOrderUpgrader }, |
||||||
|
{ name: '0083007', handler: ncParentModelTitleUpgrader }, |
||||||
|
{ name: '0083008', handler: ncRemoveDuplicatedRelationRows }, |
||||||
|
{ name: '0084002', handler: this.ncUpAddNestedResolverArgs.bind(this) }, |
||||||
|
]; |
||||||
|
if (!(await this.xcMeta?.knex?.schema?.hasTable?.('nc_store'))) { |
||||||
|
return; |
||||||
|
} |
||||||
|
this.baseLog(`xcUpgrade : Getting configuration from meta database`); |
||||||
|
|
||||||
|
const config = await this.xcMeta.metaGet( |
||||||
|
this.projectId, |
||||||
|
this.dbAlias, |
||||||
|
'nc_store', |
||||||
|
{ key: 'NC_CONFIG' }, |
||||||
|
); |
||||||
|
|
||||||
|
if (config) { |
||||||
|
const configObj: NcConfig = JSON.parse(config.value); |
||||||
|
if (configObj.version !== process.env.NC_VERSION) { |
||||||
|
for (const version of NC_VERSIONS) { |
||||||
|
// compare current version and old version
|
||||||
|
if (version.name > configObj.version) { |
||||||
|
this.baseLog( |
||||||
|
`xcUpgrade : Upgrading '%s' => '%s'`, |
||||||
|
configObj.version, |
||||||
|
version.name, |
||||||
|
); |
||||||
|
await version?.handler?.(<NcBuilderUpgraderCtx>{ |
||||||
|
xcMeta: this.xcMeta, |
||||||
|
builder: this, |
||||||
|
dbAlias: this.dbAlias, |
||||||
|
projectId: this.projectId, |
||||||
|
}); |
||||||
|
|
||||||
|
// update version in meta after each upgrade
|
||||||
|
configObj.version = version.name; |
||||||
|
await this.xcMeta.metaUpdate( |
||||||
|
this.projectId, |
||||||
|
this.dbAlias, |
||||||
|
'nc_store', |
||||||
|
{ |
||||||
|
value: JSON.stringify(configObj), |
||||||
|
}, |
||||||
|
{ |
||||||
|
key: 'NC_CONFIG', |
||||||
|
}, |
||||||
|
); |
||||||
|
|
||||||
|
// todo: backup data
|
||||||
|
} |
||||||
|
if (version.name === process.env.NC_VERSION) { |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
configObj.version = process.env.NC_VERSION; |
||||||
|
await this.xcMeta.metaUpdate( |
||||||
|
this.projectId, |
||||||
|
this.dbAlias, |
||||||
|
'nc_store', |
||||||
|
{ |
||||||
|
value: JSON.stringify(configObj), |
||||||
|
}, |
||||||
|
{ |
||||||
|
key: 'NC_CONFIG', |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.baseLog(`xcUpgrade : Inserting config to meta database`); |
||||||
|
const configObj: NcConfig = JSON.parse(JSON.stringify(this.config)); |
||||||
|
delete configObj.envs; |
||||||
|
const isOld = ( |
||||||
|
await this.xcMeta.metaList(this.projectId, this.dbAlias, 'nc_models') |
||||||
|
)?.length; |
||||||
|
configObj.version = isOld ? '0009000' : process.env.NC_VERSION; |
||||||
|
await this.xcMeta.metaInsert(this.projectId, this.dbAlias, 'nc_store', { |
||||||
|
key: 'NC_CONFIG', |
||||||
|
value: JSON.stringify(configObj), |
||||||
|
}); |
||||||
|
if (isOld) { |
||||||
|
await this.xcUpgrade(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public getProjectId(): string { |
||||||
|
return this.projectId; |
||||||
|
} |
||||||
|
|
||||||
|
public async init(): Promise<void> { |
||||||
|
await this.xcUpgrade(); |
||||||
|
} |
||||||
|
|
||||||
|
protected async initDbDriver(): Promise<void> { |
||||||
|
this.dbDriver = await NcConnectionMgr.get({ |
||||||
|
dbAlias: this.dbAlias, |
||||||
|
env: this.config.env, |
||||||
|
config: this.config, |
||||||
|
projectId: this.projectId, |
||||||
|
}); |
||||||
|
this.sqlClient = await NcConnectionMgr.getSqlClient({ |
||||||
|
dbAlias: this.dbAlias, |
||||||
|
env: this.config.env, |
||||||
|
config: this.config, |
||||||
|
projectId: this.projectId, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private baseLog(str, ...args): void { |
||||||
|
log(`${this.dbAlias} : ${str}`, ...args); |
||||||
|
} |
||||||
|
|
||||||
|
protected generateContextForTable( |
||||||
|
tn: string, |
||||||
|
columns: any[], |
||||||
|
relations, |
||||||
|
hasMany: any[], |
||||||
|
belongsTo: any[], |
||||||
|
type = 'table', |
||||||
|
tableNameAlias?: string, |
||||||
|
): any { |
||||||
|
this.baseLog(`generateContextForTable : '%s' %s`, tn, type); |
||||||
|
|
||||||
|
for (const col of columns) { |
||||||
|
col._cn = col._cn || this.getColumnNameAlias(col); |
||||||
|
} |
||||||
|
|
||||||
|
// tslint:disable-next-line:variable-name
|
||||||
|
const _tn = tableNameAlias || this.getTableNameAlias(tn); |
||||||
|
|
||||||
|
const ctx = { |
||||||
|
dbType: this.connectionConfig.client, |
||||||
|
tn, |
||||||
|
_tn, |
||||||
|
tn_camelize: inflection.camelize(_tn), |
||||||
|
tn_camelize_low: inflection.camelize(_tn, true), |
||||||
|
columns, |
||||||
|
relations, |
||||||
|
hasMany, |
||||||
|
belongsTo, |
||||||
|
dbAlias: '', |
||||||
|
routeVersionLetter: this.routeVersionLetter, |
||||||
|
type, |
||||||
|
project_id: this.projectId, |
||||||
|
}; |
||||||
|
return ctx; |
||||||
|
} |
||||||
|
|
||||||
|
private getColumnNameAlias(col, tableName?: string) { |
||||||
|
return ( |
||||||
|
this.metas?.[tableName]?.columns?.find((c) => c.cn === col.cn)?._cn || |
||||||
|
col._cn || |
||||||
|
this.getInflectedName(col.cn, this.connectionConfig?.meta?.inflection?.cn) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// table alias functions
|
||||||
|
protected getInflectedName(_name: string, inflectionFns: string): string { |
||||||
|
let name = _name; |
||||||
|
if (process.env.NC_INFLECTION) { |
||||||
|
inflectionFns = 'camelize'; |
||||||
|
} |
||||||
|
|
||||||
|
if (inflectionFns && inflectionFns !== 'none') { |
||||||
|
name = inflectionFns |
||||||
|
.split(',') |
||||||
|
.reduce((out, fn) => inflection?.[fn]?.(out) || out, name); |
||||||
|
} |
||||||
|
return this.apiType === 'graphql' ? name.replace(/[^_\da-z]/gi, '_') : name; |
||||||
|
} |
||||||
|
|
||||||
|
protected async ncUpAddNestedResolverArgs(_ctx: any): Promise<any> {} |
||||||
|
|
||||||
|
protected getTableNameAlias(tableName: string) { |
||||||
|
let tn = tableName; |
||||||
|
if (this.metas?.[tn]?._tn) { |
||||||
|
return this.metas?.[tn]?._tn; |
||||||
|
} |
||||||
|
|
||||||
|
if (this.projectBuilder?.prefix) { |
||||||
|
tn = tn.replace(this.projectBuilder?.prefix, ''); |
||||||
|
} |
||||||
|
|
||||||
|
const modifiedTableName = tn?.replace(/^(?=\d+)/, 'ISN___'); |
||||||
|
return this.getInflectedName( |
||||||
|
modifiedTableName, |
||||||
|
this.connectionConfig?.meta?.inflection?.tn, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
protected async ncUpManyToMany(_ctx: any): Promise<any> { |
||||||
|
const models = await this.xcMeta.metaList( |
||||||
|
this.projectId, |
||||||
|
this.dbAlias, |
||||||
|
'nc_models', |
||||||
|
{ |
||||||
|
fields: ['meta'], |
||||||
|
condition: { |
||||||
|
type: 'table', |
||||||
|
}, |
||||||
|
}, |
||||||
|
); |
||||||
|
if (!models.length) { |
||||||
|
return; |
||||||
|
} |
||||||
|
const metas = []; |
||||||
|
// add virtual columns for relations
|
||||||
|
for (const metaObj of models) { |
||||||
|
const meta = JSON.parse(metaObj.meta); |
||||||
|
metas.push(meta); |
||||||
|
const ctx = this.generateContextForTable( |
||||||
|
meta.tn, |
||||||
|
meta.columns, |
||||||
|
[], |
||||||
|
meta.hasMany, |
||||||
|
meta.belongsTo, |
||||||
|
meta.type, |
||||||
|
meta._tn, |
||||||
|
); |
||||||
|
// generate virtual columns
|
||||||
|
meta.v = ModelXcMetaFactory.create(this.connectionConfig, { |
||||||
|
dir: '', |
||||||
|
ctx, |
||||||
|
filename: '', |
||||||
|
}).getVitualColumns(); |
||||||
|
// set default display values
|
||||||
|
ModelXcMetaFactory.create( |
||||||
|
this.connectionConfig, |
||||||
|
{}, |
||||||
|
).mapDefaultDisplayValue(meta.columns); |
||||||
|
// update meta
|
||||||
|
await this.xcMeta.metaUpdate( |
||||||
|
this.projectId, |
||||||
|
this.dbAlias, |
||||||
|
'nc_models', |
||||||
|
{ |
||||||
|
meta: JSON.stringify(meta), |
||||||
|
}, |
||||||
|
{ title: meta.tn }, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// generate many to many relations an columns
|
||||||
|
await this.getManyToManyRelations({ localMetas: metas }); |
||||||
|
return metas; |
||||||
|
} |
||||||
|
|
||||||
|
protected async getManyToManyRelations({ |
||||||
|
parent = null, |
||||||
|
child = null, |
||||||
|
localMetas = null, |
||||||
|
} = {}): Promise<Set<any>> { |
||||||
|
const metas = new Set<any>(); |
||||||
|
const assocMetas = new Set<any>(); |
||||||
|
|
||||||
|
if (localMetas) { |
||||||
|
for (const meta of localMetas) { |
||||||
|
this.metas[meta.tn] = meta; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for (const meta of Object.values(this.metas)) { |
||||||
|
// check if table is a Bridge table(or Associative Table) by checking
|
||||||
|
// number of foreign keys and columns
|
||||||
|
if (meta.belongsTo?.length === 2 && meta.columns.length < 5) { |
||||||
|
if ( |
||||||
|
parent && |
||||||
|
child && |
||||||
|
!( |
||||||
|
[parent, child].includes(meta.belongsTo[0].rtn) && |
||||||
|
[parent, child].includes(meta.belongsTo[1].rtn) |
||||||
|
) |
||||||
|
) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
const tableMetaA = this.metas[meta.belongsTo[0].rtn]; |
||||||
|
const tableMetaB = this.metas[meta.belongsTo[1].rtn]; |
||||||
|
|
||||||
|
/* // remove hasmany relation with associate table from tables |
||||||
|
tableMetaA.hasMany.splice(tableMetaA.hasMany.findIndex(hm => hm.tn === meta.tn), 1) |
||||||
|
tableMetaB.hasMany.splice(tableMetaB.hasMany.findIndex(hm => hm.tn === meta.tn), 1)*/ |
||||||
|
|
||||||
|
// add manytomany data under metadata of both linked tables
|
||||||
|
tableMetaA.manyToMany = tableMetaA.manyToMany || []; |
||||||
|
if (tableMetaA.manyToMany.every((mm) => mm.vtn !== meta.tn)) { |
||||||
|
tableMetaA.manyToMany.push({ |
||||||
|
tn: tableMetaA.tn, |
||||||
|
cn: meta.belongsTo[0].rcn, |
||||||
|
vtn: meta.tn, |
||||||
|
vcn: meta.belongsTo[0].cn, |
||||||
|
vrcn: meta.belongsTo[1].cn, |
||||||
|
rtn: meta.belongsTo[1].rtn, |
||||||
|
rcn: meta.belongsTo[1].rcn, |
||||||
|
_tn: tableMetaA._tn, |
||||||
|
_cn: meta.belongsTo[0]._rcn, |
||||||
|
_rtn: meta.belongsTo[1]._rtn, |
||||||
|
_rcn: meta.belongsTo[1]._rcn, |
||||||
|
}); |
||||||
|
metas.add(tableMetaA); |
||||||
|
} |
||||||
|
// ignore if A & B are same table
|
||||||
|
if (tableMetaB !== tableMetaA) { |
||||||
|
tableMetaB.manyToMany = tableMetaB.manyToMany || []; |
||||||
|
if (tableMetaB.manyToMany.every((mm) => mm.vtn !== meta.tn)) { |
||||||
|
tableMetaB.manyToMany.push({ |
||||||
|
tn: tableMetaB.tn, |
||||||
|
cn: meta.belongsTo[1].rcn, |
||||||
|
vtn: meta.tn, |
||||||
|
vcn: meta.belongsTo[1].cn, |
||||||
|
vrcn: meta.belongsTo[0].cn, |
||||||
|
rtn: meta.belongsTo[0].rtn, |
||||||
|
rcn: meta.belongsTo[0].rcn, |
||||||
|
_tn: tableMetaB._tn, |
||||||
|
_cn: meta.belongsTo[1]._rcn, |
||||||
|
_rtn: meta.belongsTo[0]._rtn, |
||||||
|
_rcn: meta.belongsTo[0]._rcn, |
||||||
|
}); |
||||||
|
metas.add(tableMetaB); |
||||||
|
} |
||||||
|
} |
||||||
|
assocMetas.add(meta); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Update metadata of tables which have manytomany relation
|
||||||
|
// and recreate basemodel with new meta information
|
||||||
|
for (const meta of metas) { |
||||||
|
let queryParams; |
||||||
|
|
||||||
|
// update showfields on new many to many relation create
|
||||||
|
if (parent && child) { |
||||||
|
try { |
||||||
|
queryParams = JSON.parse( |
||||||
|
( |
||||||
|
await this.xcMeta.metaGet( |
||||||
|
this.projectId, |
||||||
|
this.dbAlias, |
||||||
|
'nc_models', |
||||||
|
{ title: meta.tn }, |
||||||
|
) |
||||||
|
).query_params, |
||||||
|
); |
||||||
|
} catch (e) { |
||||||
|
// ignore
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
meta.v = [ |
||||||
|
...meta.v.filter( |
||||||
|
(vc) => !(vc.hm && meta.manyToMany.some((mm) => vc.hm.tn === mm.vtn)), |
||||||
|
), |
||||||
|
// todo: ignore duplicate m2m relations
|
||||||
|
// todo: optimize, just compare associative table(Vtn)
|
||||||
|
...meta.manyToMany |
||||||
|
.filter( |
||||||
|
(v, i) => |
||||||
|
!meta.v.some( |
||||||
|
(v1) => |
||||||
|
v1.mm && |
||||||
|
((v1.mm.tn === v.tn && v.rtn === v1.mm.rtn) || |
||||||
|
(v1.mm.rtn === v.tn && v.tn === v1.mm.rtn)) && |
||||||
|
v.vtn === v1.mm.vtn, |
||||||
|
) && |
||||||
|
// ignore duplicate
|
||||||
|
!meta.manyToMany.some( |
||||||
|
(v1, i1) => |
||||||
|
i1 !== i && |
||||||
|
v1.tn === v.tn && |
||||||
|
v.rtn === v1.rtn && |
||||||
|
v.vtn === v1.vtn, |
||||||
|
), |
||||||
|
) |
||||||
|
.map((mm) => { |
||||||
|
if ( |
||||||
|
queryParams?.showFields && |
||||||
|
!(`${mm._tn} <=> ${mm._rtn}` in queryParams.showFields) |
||||||
|
) { |
||||||
|
queryParams.showFields[`${mm._tn} <=> ${mm._rtn}`] = true; |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
mm, |
||||||
|
_cn: `${mm._tn} <=> ${mm._rtn}`, |
||||||
|
}; |
||||||
|
}), |
||||||
|
]; |
||||||
|
await this.xcMeta.metaUpdate( |
||||||
|
this.projectId, |
||||||
|
this.dbAlias, |
||||||
|
'nc_models', |
||||||
|
{ |
||||||
|
meta: JSON.stringify(meta), |
||||||
|
...(queryParams ? { query_params: JSON.stringify(queryParams) } : {}), |
||||||
|
}, |
||||||
|
{ title: meta.tn }, |
||||||
|
); |
||||||
|
// XcCache.del([this.projectId, this.dbAlias, 'table', meta.tn].join('::'));
|
||||||
|
// if (!localMetas) {
|
||||||
|
// this.models[meta.tn] = this.getBaseModel(meta);
|
||||||
|
// }
|
||||||
|
} |
||||||
|
|
||||||
|
// Update metadata of associative table
|
||||||
|
for (const meta of assocMetas) { |
||||||
|
await this.xcMeta.metaUpdate( |
||||||
|
this.projectId, |
||||||
|
this.dbAlias, |
||||||
|
'nc_models', |
||||||
|
{ |
||||||
|
mm: 1, |
||||||
|
}, |
||||||
|
{ title: meta.tn }, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return metas; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
interface NcBuilderUpgraderCtx { |
||||||
|
xcMeta: MetaService; |
||||||
|
builder: BaseApiBuilder<any>; |
||||||
|
projectId: string; |
||||||
|
dbAlias: string; |
||||||
|
} |
||||||
|
|
||||||
|
interface NcMetaData { |
||||||
|
tn: string; |
||||||
|
_tn?: string; |
||||||
|
v: Array<{ |
||||||
|
_cn?: string; |
||||||
|
[key: string]: any; |
||||||
|
}>; |
||||||
|
columns: Array<{ |
||||||
|
_cn?: string; |
||||||
|
cn?: string; |
||||||
|
uidt?: string; |
||||||
|
[key: string]: any; |
||||||
|
}>; |
||||||
|
|
||||||
|
[key: string]: any; |
||||||
|
} |
||||||
|
|
||||||
|
type XcTablesPopulateParams = { |
||||||
|
tableNames?: Array<{ |
||||||
|
tn: string; |
||||||
|
_tn?: string; |
||||||
|
}>; |
||||||
|
type?: 'table' | 'view' | 'function' | 'procedure'; |
||||||
|
columns?: { |
||||||
|
[tn: string]: any; |
||||||
|
}; |
||||||
|
oldMetas?: { |
||||||
|
[tn: string]: NcMetaData; |
||||||
|
}; |
||||||
|
}; |
||||||
|
export { NcBuilderUpgraderCtx, NcMetaData, XcTablesPopulateParams }; |
@ -0,0 +1,198 @@ |
|||||||
|
import { Router } from 'express'; |
||||||
|
import { SqlClientFactory } from '../../db/sql-client/lib/SqlClientFactory'; |
||||||
|
import Noco from '../../Noco'; |
||||||
|
import { GqlApiBuilder } from './gql/GqlApiBuilder'; |
||||||
|
import { RestApiBuilder } from './rest/RestApiBuilder'; |
||||||
|
import type { NcConfig } from '../../interface/config'; |
||||||
|
|
||||||
|
export default class NcProjectBuilder { |
||||||
|
public readonly id: string; |
||||||
|
public readonly title: string; |
||||||
|
public readonly description: string; |
||||||
|
public readonly router: Router; |
||||||
|
public readonly apiBuilders: Array<RestApiBuilder | GqlApiBuilder> = []; |
||||||
|
private _config: any; |
||||||
|
|
||||||
|
protected startTime; |
||||||
|
protected app: Noco; |
||||||
|
protected appConfig: NcConfig; |
||||||
|
protected apiInfInfoList: any[] = []; |
||||||
|
protected aggregatedApiInfo: any; |
||||||
|
protected authHook: any; |
||||||
|
|
||||||
|
constructor(app: Noco, appConfig: NcConfig, project: any) { |
||||||
|
this.app = app; |
||||||
|
this.appConfig = appConfig; |
||||||
|
|
||||||
|
if (project) { |
||||||
|
this.id = project.id; |
||||||
|
this.title = project.title; |
||||||
|
this.description = project.description; |
||||||
|
this._config = { ...this.appConfig, ...JSON.parse(project.config) }; |
||||||
|
this.router = Router(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public async init(_isFirstTime?: boolean) { |
||||||
|
try { |
||||||
|
// await this.addAuthHookToMiddleware();
|
||||||
|
|
||||||
|
this.startTime = Date.now(); |
||||||
|
const allRoutesInfo: any[] = []; |
||||||
|
// await this.app.ncMeta.projectStatusUpdate(this.id, 'starting');
|
||||||
|
// await this.syncMigration();
|
||||||
|
await this._createApiBuilder(); |
||||||
|
// this.initApiInfoRoute();
|
||||||
|
|
||||||
|
/* Create REST APIs / GraphQL Resolvers */ |
||||||
|
for (const meta of this.apiBuilders) { |
||||||
|
let routeInfo; |
||||||
|
if (meta instanceof RestApiBuilder) { |
||||||
|
console.log( |
||||||
|
`Creating REST APIs ${meta.getDbType()} - > ${meta.getDbName()}`, |
||||||
|
); |
||||||
|
routeInfo = await (meta as RestApiBuilder).init(); |
||||||
|
} else if (meta instanceof GqlApiBuilder) { |
||||||
|
console.log( |
||||||
|
`Creating GraphQL APIs ${meta.getDbType()} - > ${meta.getDbName()}`, |
||||||
|
); |
||||||
|
routeInfo = await (meta as GqlApiBuilder).init(); |
||||||
|
} |
||||||
|
allRoutesInfo.push(routeInfo); |
||||||
|
// this.progress(routeInfo, allRoutesInfo, isFirstTime);
|
||||||
|
} |
||||||
|
|
||||||
|
// this.app.projectRouter.use(`/nc/${this.id}`, this.router);
|
||||||
|
// await this.app.ncMeta.projectStatusUpdate(this.id, 'started');
|
||||||
|
} catch (e) { |
||||||
|
console.log(e); |
||||||
|
throw e; |
||||||
|
// await this.app.ncMeta.projectStatusUpdate(this.id, 'stopped');
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected async _createApiBuilder() { |
||||||
|
this.apiBuilders.splice(0, this.apiBuilders.length); |
||||||
|
let i = 0; |
||||||
|
|
||||||
|
const connectionConfigs = []; |
||||||
|
|
||||||
|
/* for each db create an api builder */ |
||||||
|
for (const db of this.config?.envs?.[this.appConfig?.workingEnv]?.db || |
||||||
|
[]) { |
||||||
|
let Builder; |
||||||
|
switch (db.meta.api.type) { |
||||||
|
case 'graphql': |
||||||
|
Builder = GqlApiBuilder; |
||||||
|
break; |
||||||
|
|
||||||
|
case 'rest': |
||||||
|
Builder = RestApiBuilder; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
if ((db?.connection as any)?.database) { |
||||||
|
const connectionConfig = { |
||||||
|
...db, |
||||||
|
meta: { |
||||||
|
...db.meta, |
||||||
|
api: { |
||||||
|
...db.meta.api, |
||||||
|
prefix: db.meta.api.prefix || this.genVer(i), |
||||||
|
}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
this.apiBuilders.push( |
||||||
|
new Builder( |
||||||
|
this.app, |
||||||
|
this, |
||||||
|
this.config, |
||||||
|
connectionConfig, |
||||||
|
this.app.ncMeta, |
||||||
|
), |
||||||
|
); |
||||||
|
connectionConfigs.push(connectionConfig); |
||||||
|
i++; |
||||||
|
} else if (db.meta?.allSchemas) { |
||||||
|
/* get all schemas and create APIs for all of them */ |
||||||
|
const sqlClient = await SqlClientFactory.create({ |
||||||
|
...db, |
||||||
|
connection: { ...db.connection, database: undefined }, |
||||||
|
}); |
||||||
|
|
||||||
|
const schemaList = (await sqlClient.schemaList({}))?.data?.list; |
||||||
|
for (const schema of schemaList) { |
||||||
|
const connectionConfig = { |
||||||
|
...db, |
||||||
|
connection: { ...db.connection, database: schema.schema_name }, |
||||||
|
meta: { |
||||||
|
...db.meta, |
||||||
|
dbAlias: i ? db.meta.dbAlias + i : db.meta.dbAlias, |
||||||
|
api: { |
||||||
|
...db.meta.api, |
||||||
|
prefix: db.meta.api.prefix || this.genVer(i), |
||||||
|
}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
this.apiBuilders.push( |
||||||
|
new Builder( |
||||||
|
this.app, |
||||||
|
this, |
||||||
|
this.config, |
||||||
|
connectionConfig, |
||||||
|
this.app.ncMeta, |
||||||
|
), |
||||||
|
); |
||||||
|
connectionConfigs.push(connectionConfig); |
||||||
|
|
||||||
|
i++; |
||||||
|
} |
||||||
|
|
||||||
|
sqlClient.knex.destroy(); |
||||||
|
} |
||||||
|
} |
||||||
|
if (this.config?.envs?.[this.appConfig.workingEnv]?.db) { |
||||||
|
this.config.envs[this.appConfig.workingEnv].db.splice( |
||||||
|
0, |
||||||
|
this.config.envs[this.appConfig.workingEnv].db.length, |
||||||
|
...connectionConfigs, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected genVer(i): string { |
||||||
|
const l = 'vwxyzabcdefghijklmnopqrstu'; |
||||||
|
return ( |
||||||
|
i |
||||||
|
.toString(26) |
||||||
|
.split('') |
||||||
|
.map((v) => l[parseInt(v, 26)]) |
||||||
|
.join('') + '1' |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
protected static triggerGarbageCollect() { |
||||||
|
try { |
||||||
|
if (global.gc) { |
||||||
|
global.gc(); |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
console.log('`node --expose-gc index.js`'); |
||||||
|
process.exit(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public get prefix(): string { |
||||||
|
return this.config?.prefix; |
||||||
|
} |
||||||
|
|
||||||
|
public get config(): any { |
||||||
|
return this._config; |
||||||
|
} |
||||||
|
|
||||||
|
public updateConfig(config: string) { |
||||||
|
this._config = { ...this.appConfig, ...JSON.parse(config) }; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,149 @@ |
|||||||
|
import debug from 'debug'; |
||||||
|
import { Router } from 'express'; |
||||||
|
import GqlXcSchemaFactory from '../../../../../nocodb/src/lib/db/sql-mgr/code/gql-schema/xc-ts/GqlXcSchemaFactory'; |
||||||
|
import { MetaService } from '../../../meta/meta.service'; |
||||||
|
import Noco from '../../../Noco'; |
||||||
|
import type NcProjectBuilder from '../NcProjectBuilder'; |
||||||
|
import type { DbConfig, NcConfig } from '../../../interface/config'; |
||||||
|
import type XcMetaMgr from '../../../interface/XcMetaMgr'; |
||||||
|
import BaseApiBuilder from '../BaseApiBuilder'; |
||||||
|
|
||||||
|
export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr { |
||||||
|
public readonly type = 'gql'; |
||||||
|
|
||||||
|
private readonly gqlRouter: Router; |
||||||
|
|
||||||
|
constructor( |
||||||
|
app: Noco, |
||||||
|
projectBuilder: NcProjectBuilder, |
||||||
|
config: NcConfig, |
||||||
|
connectionConfig: DbConfig, |
||||||
|
xcMeta?: MetaService, |
||||||
|
) { |
||||||
|
super(app, projectBuilder, config, connectionConfig); |
||||||
|
this.config = config; |
||||||
|
this.connectionConfig = connectionConfig; |
||||||
|
this.gqlRouter = Router(); |
||||||
|
this.xcMeta = xcMeta; |
||||||
|
} |
||||||
|
|
||||||
|
public async init(): Promise<void> { |
||||||
|
await super.init(); |
||||||
|
} |
||||||
|
|
||||||
|
protected async ncUpAddNestedResolverArgs(_ctx: any): Promise<any> { |
||||||
|
const models = await this.xcMeta.metaList( |
||||||
|
this.projectId, |
||||||
|
this.dbAlias, |
||||||
|
'nc_models', |
||||||
|
{ |
||||||
|
fields: ['meta'], |
||||||
|
condition: { |
||||||
|
type: 'table', |
||||||
|
}, |
||||||
|
}, |
||||||
|
); |
||||||
|
if (!models.length) { |
||||||
|
return; |
||||||
|
} |
||||||
|
// add virtual columns for relations
|
||||||
|
for (const metaObj of models) { |
||||||
|
const meta = JSON.parse(metaObj.meta); |
||||||
|
const ctx = this.generateContextForTable( |
||||||
|
meta.tn, |
||||||
|
meta.columns, |
||||||
|
[], |
||||||
|
meta.hasMany, |
||||||
|
meta.belongsTo, |
||||||
|
meta.type, |
||||||
|
meta._tn, |
||||||
|
); |
||||||
|
|
||||||
|
/* generate gql schema of the table */ |
||||||
|
const schema = GqlXcSchemaFactory.create(this.connectionConfig, { |
||||||
|
dir: '', |
||||||
|
ctx: { |
||||||
|
...ctx, |
||||||
|
manyToMany: meta.manyToMany, |
||||||
|
}, |
||||||
|
filename: '', |
||||||
|
}).getString(); |
||||||
|
|
||||||
|
/* update schema in metadb */ |
||||||
|
await this.xcMeta.metaUpdate( |
||||||
|
this.projectId, |
||||||
|
this.dbAlias, |
||||||
|
'nc_models', |
||||||
|
{ |
||||||
|
schema, |
||||||
|
}, |
||||||
|
{ |
||||||
|
title: meta.tn, |
||||||
|
type: 'table', |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected async ncUpManyToMany(ctx: any): Promise<any> { |
||||||
|
const metas = await super.ncUpManyToMany(ctx); |
||||||
|
|
||||||
|
if (!metas) { |
||||||
|
return; |
||||||
|
} |
||||||
|
for (const meta of metas) { |
||||||
|
const ctx = this.generateContextForTable( |
||||||
|
meta.tn, |
||||||
|
meta.columns, |
||||||
|
[], |
||||||
|
meta.hasMany, |
||||||
|
meta.belongsTo, |
||||||
|
meta.type, |
||||||
|
meta._tn, |
||||||
|
); |
||||||
|
|
||||||
|
/* generate gql schema of the table */ |
||||||
|
const schema = GqlXcSchemaFactory.create(this.connectionConfig, { |
||||||
|
dir: '', |
||||||
|
ctx: { |
||||||
|
...ctx, |
||||||
|
manyToMany: meta.manyToMany, |
||||||
|
}, |
||||||
|
filename: '', |
||||||
|
}).getString(); |
||||||
|
|
||||||
|
/* update schema in metadb */ |
||||||
|
await this.xcMeta.metaUpdate( |
||||||
|
this.projectId, |
||||||
|
this.dbAlias, |
||||||
|
'nc_models', |
||||||
|
{ |
||||||
|
schema, |
||||||
|
}, |
||||||
|
{ |
||||||
|
title: meta.tn, |
||||||
|
type: 'table', |
||||||
|
}, |
||||||
|
); |
||||||
|
|
||||||
|
// todo : add loaders
|
||||||
|
|
||||||
|
if (meta.manyToMany) { |
||||||
|
for (const mm of meta.manyToMany) { |
||||||
|
await this.xcMeta.metaInsert( |
||||||
|
this.projectId, |
||||||
|
this.dbAlias, |
||||||
|
'nc_loaders', |
||||||
|
{ |
||||||
|
title: `${mm.tn}Mm${mm.rtn}List`, |
||||||
|
parent: mm.tn, |
||||||
|
child: mm.rtn, |
||||||
|
relation: 'mm', |
||||||
|
resolver: 'mmlist', |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,48 @@ |
|||||||
|
import type { NcBuilderUpgraderCtx } from '../BaseApiBuilder'; |
||||||
|
|
||||||
|
export default async function (ctx: NcBuilderUpgraderCtx) { |
||||||
|
const models = await ctx.xcMeta.metaList( |
||||||
|
ctx.projectId, |
||||||
|
ctx.dbAlias, |
||||||
|
'nc_models', |
||||||
|
{ |
||||||
|
xcCondition: { |
||||||
|
_or: [{ type: { eq: 'table' } }, { type: { eq: 'view' } }], |
||||||
|
}, |
||||||
|
}, |
||||||
|
); |
||||||
|
let order = 0; |
||||||
|
for (const model of models) { |
||||||
|
await ctx.xcMeta.metaUpdate( |
||||||
|
ctx.projectId, |
||||||
|
ctx.dbAlias, |
||||||
|
'nc_models', |
||||||
|
{ |
||||||
|
order: ++order, |
||||||
|
view_order: 1, |
||||||
|
}, |
||||||
|
model.id, |
||||||
|
); |
||||||
|
|
||||||
|
const views = await ctx.xcMeta.metaList( |
||||||
|
ctx.projectId, |
||||||
|
ctx.dbAlias, |
||||||
|
'nc_models', |
||||||
|
{ |
||||||
|
condition: { parent_model_title: model.title }, |
||||||
|
}, |
||||||
|
); |
||||||
|
let view_order = 1; |
||||||
|
for (const view of views) { |
||||||
|
await ctx.xcMeta.metaUpdate( |
||||||
|
ctx.projectId, |
||||||
|
ctx.dbAlias, |
||||||
|
'nc_models', |
||||||
|
{ |
||||||
|
view_order: ++view_order, |
||||||
|
}, |
||||||
|
view.id, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
import type { NcBuilderUpgraderCtx } from '../BaseApiBuilder'; |
||||||
|
|
||||||
|
export default async function (ctx: NcBuilderUpgraderCtx) { |
||||||
|
const views = await ctx.xcMeta.metaList( |
||||||
|
ctx.projectId, |
||||||
|
ctx.dbAlias, |
||||||
|
'nc_models', |
||||||
|
{ |
||||||
|
condition: { |
||||||
|
type: 'vtable', |
||||||
|
}, |
||||||
|
}, |
||||||
|
); |
||||||
|
|
||||||
|
for (const view of views) { |
||||||
|
await ctx.xcMeta.metaUpdate( |
||||||
|
ctx.projectId, |
||||||
|
ctx.dbAlias, |
||||||
|
'nc_disabled_models_for_role', |
||||||
|
{ |
||||||
|
parent_model_title: view.parent_model_title, |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: 'vtable', |
||||||
|
title: view.title, |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,113 @@ |
|||||||
|
import type { NcBuilderUpgraderCtx } from '../BaseApiBuilder'; |
||||||
|
|
||||||
|
export default async function (ctx: NcBuilderUpgraderCtx) { |
||||||
|
try { |
||||||
|
const relations = await ctx.xcMeta.metaList( |
||||||
|
ctx.projectId, |
||||||
|
ctx.dbAlias, |
||||||
|
'nc_relations', |
||||||
|
); |
||||||
|
|
||||||
|
const duplicates = []; |
||||||
|
|
||||||
|
for (const relation of relations) { |
||||||
|
if (relation.type !== 'real' || duplicates.includes(relation)) continue; |
||||||
|
const duplicateRelIndex = relations.findIndex( |
||||||
|
(rel) => |
||||||
|
rel !== relation && |
||||||
|
rel.tn === relation.tn && |
||||||
|
rel.rtn === relation.rtn && |
||||||
|
rel.cn === relation.cn && |
||||||
|
rel.rcn === relation.rcn && |
||||||
|
rel.type === 'real', |
||||||
|
); |
||||||
|
|
||||||
|
if (duplicateRelIndex > -1) duplicates.push(relations[duplicateRelIndex]); |
||||||
|
} |
||||||
|
|
||||||
|
// delete relation
|
||||||
|
for (const dupRelation of duplicates) { |
||||||
|
await ctx.xcMeta.metaDelete( |
||||||
|
ctx.projectId, |
||||||
|
ctx.dbAlias, |
||||||
|
'nc_relations', |
||||||
|
dupRelation.id, |
||||||
|
); |
||||||
|
{ |
||||||
|
const tnModel = await ctx.xcMeta.metaGet( |
||||||
|
ctx.projectId, |
||||||
|
ctx.dbAlias, |
||||||
|
'nc_models', |
||||||
|
{ |
||||||
|
type: 'table', |
||||||
|
title: dupRelation.tn, |
||||||
|
}, |
||||||
|
); |
||||||
|
|
||||||
|
const meta = JSON.parse(tnModel.meta); |
||||||
|
|
||||||
|
const duplicateBts = meta.belongsTo.filter( |
||||||
|
(bt) => |
||||||
|
bt.tn === dupRelation.tn && |
||||||
|
bt.rtn === dupRelation.rtn && |
||||||
|
bt.cn === dupRelation.cn && |
||||||
|
bt.rcn === dupRelation.rcn && |
||||||
|
bt.type === 'real', |
||||||
|
); |
||||||
|
|
||||||
|
if (duplicateBts?.length > 1) { |
||||||
|
meta.belongsTo.splice(meta.belongsTo.indexOf(duplicateBts[1]), 1); |
||||||
|
} |
||||||
|
|
||||||
|
await ctx.xcMeta.metaUpdate( |
||||||
|
ctx.projectId, |
||||||
|
ctx.dbAlias, |
||||||
|
'nc_models', |
||||||
|
{ meta: JSON.stringify(meta) }, |
||||||
|
{ |
||||||
|
type: 'table', |
||||||
|
title: dupRelation.tn, |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
{ |
||||||
|
const rtnModel = await ctx.xcMeta.metaGet( |
||||||
|
ctx.projectId, |
||||||
|
ctx.dbAlias, |
||||||
|
'nc_models', |
||||||
|
{ |
||||||
|
type: 'table', |
||||||
|
title: dupRelation.rtn, |
||||||
|
}, |
||||||
|
); |
||||||
|
|
||||||
|
const meta = JSON.parse(rtnModel.meta); |
||||||
|
|
||||||
|
const duplicateHms = meta.hasMany.filter( |
||||||
|
(bt) => |
||||||
|
bt.tn === dupRelation.tn && |
||||||
|
bt.rtn === dupRelation.rtn && |
||||||
|
bt.cn === dupRelation.cn && |
||||||
|
bt.rcn === dupRelation.rcn && |
||||||
|
bt.type === 'real', |
||||||
|
); |
||||||
|
|
||||||
|
if (duplicateHms?.length > 1) { |
||||||
|
meta.hasMany.splice(meta.hasMany.indexOf(duplicateHms[1]), 1); |
||||||
|
} |
||||||
|
await ctx.xcMeta.metaUpdate( |
||||||
|
ctx.projectId, |
||||||
|
ctx.dbAlias, |
||||||
|
'nc_models', |
||||||
|
{ meta: JSON.stringify(meta) }, |
||||||
|
{ |
||||||
|
type: 'table', |
||||||
|
title: dupRelation.rtn, |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
console.log(e); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,178 @@ |
|||||||
|
import { Router } from 'express'; |
||||||
|
import debug from 'debug'; |
||||||
|
import autoBind from 'auto-bind'; |
||||||
|
import SwaggerXc from '../../../db/sql-mgr/code/routers/xc-ts/SwaggerXc' |
||||||
|
import ExpressXcTsRoutes from '../../../db/sql-mgr/code/routes/xc-ts/ExpressXcTsRoutes' |
||||||
|
import { MetaService } from '../../../meta/meta.service'; |
||||||
|
import Noco from '../../../Noco'; |
||||||
|
import NcHelp from '../../../utils/NcHelp' |
||||||
|
import type { DbConfig, NcConfig } from '../../../interface/config'; |
||||||
|
import type NcProjectBuilder from '../NcProjectBuilder'; |
||||||
|
import BaseApiBuilder, { XcTablesPopulateParams } from '../BaseApiBuilder'; |
||||||
|
|
||||||
|
const log = debug('nc:api:rest'); |
||||||
|
|
||||||
|
export class RestApiBuilder extends BaseApiBuilder<Noco> { |
||||||
|
public readonly type = 'rest'; |
||||||
|
|
||||||
|
|
||||||
|
protected nocoTypes: any; |
||||||
|
protected nocoRootResolvers: any; |
||||||
|
private routers: { [key: string]: Router }; |
||||||
|
|
||||||
|
constructor( |
||||||
|
app: Noco, |
||||||
|
projectBuilder: NcProjectBuilder, |
||||||
|
config: NcConfig, |
||||||
|
connectionConfig: DbConfig, |
||||||
|
xcMeta?: MetaService, |
||||||
|
) { |
||||||
|
super(app, projectBuilder, config, connectionConfig); |
||||||
|
autoBind(this); |
||||||
|
this.routers = {}; |
||||||
|
this.hooks = {}; |
||||||
|
this.xcMeta = xcMeta; |
||||||
|
} |
||||||
|
|
||||||
|
public async init(): Promise<void> { |
||||||
|
await super.init(); |
||||||
|
} |
||||||
|
|
||||||
|
protected async ncUpManyToMany(ctx: any): Promise<any> { |
||||||
|
const metas = await super.ncUpManyToMany(ctx); |
||||||
|
if (!metas) { |
||||||
|
return; |
||||||
|
} |
||||||
|
for (const meta of metas) { |
||||||
|
const ctx = this.generateContextForTable( |
||||||
|
meta.tn, |
||||||
|
meta.columns, |
||||||
|
[], |
||||||
|
meta.hasMany, |
||||||
|
meta.belongsTo, |
||||||
|
meta.type, |
||||||
|
meta._tn |
||||||
|
); |
||||||
|
|
||||||
|
/* create routes for table */ |
||||||
|
const routes = new ExpressXcTsRoutes({ |
||||||
|
dir: '', |
||||||
|
ctx, |
||||||
|
filename: '', |
||||||
|
}).getObjectWithoutFunctions(); |
||||||
|
|
||||||
|
/* create nc_routes, add new routes or update order */ |
||||||
|
const routesInsertion = routes.map((route, i) => { |
||||||
|
return async () => { |
||||||
|
if ( |
||||||
|
!(await this.xcMeta.metaGet( |
||||||
|
this.projectId, |
||||||
|
this.dbAlias, |
||||||
|
'nc_routes', |
||||||
|
{ |
||||||
|
path: route.path, |
||||||
|
tn: meta.tn, |
||||||
|
title: meta.tn, |
||||||
|
type: route.type, |
||||||
|
} |
||||||
|
)) |
||||||
|
) { |
||||||
|
await this.xcMeta.metaInsert( |
||||||
|
this.projectId, |
||||||
|
this.dbAlias, |
||||||
|
'nc_routes', |
||||||
|
{ |
||||||
|
acl: JSON.stringify(route.acl), |
||||||
|
handler: JSON.stringify(route.handler), |
||||||
|
order: i, |
||||||
|
path: route.path, |
||||||
|
tn: meta.tn, |
||||||
|
title: meta.tn, |
||||||
|
type: route.type, |
||||||
|
} |
||||||
|
); |
||||||
|
} else { |
||||||
|
await this.xcMeta.metaUpdate( |
||||||
|
this.projectId, |
||||||
|
this.dbAlias, |
||||||
|
'nc_routes', |
||||||
|
{ |
||||||
|
order: i, |
||||||
|
}, |
||||||
|
{ |
||||||
|
path: route.path, |
||||||
|
tn: meta.tn, |
||||||
|
title: meta.tn, |
||||||
|
type: route.type, |
||||||
|
} |
||||||
|
); |
||||||
|
} |
||||||
|
}; |
||||||
|
}); |
||||||
|
|
||||||
|
await NcHelp.executeOperations( |
||||||
|
routesInsertion, |
||||||
|
this.connectionConfig.client |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// add new routes
|
||||||
|
} |
||||||
|
|
||||||
|
protected async getManyToManyRelations(args = {}): Promise<Set<any>> { |
||||||
|
const metas: Set<any> = await super.getManyToManyRelations(args); |
||||||
|
|
||||||
|
for (const metaObj of metas) { |
||||||
|
const ctx = this.generateContextForTable( |
||||||
|
metaObj.tn, |
||||||
|
metaObj.columns, |
||||||
|
[...metaObj.belongsTo, ...metaObj.hasMany], |
||||||
|
metaObj.hasMany, |
||||||
|
metaObj.belongsTo |
||||||
|
); |
||||||
|
|
||||||
|
const swaggerDoc = await new SwaggerXc({ |
||||||
|
dir: '', |
||||||
|
ctx: { |
||||||
|
...ctx, |
||||||
|
v: metaObj.v, |
||||||
|
}, |
||||||
|
filename: '', |
||||||
|
}).getObject(); |
||||||
|
|
||||||
|
const meta = await this.xcMeta.metaGet( |
||||||
|
this.projectId, |
||||||
|
this.dbAlias, |
||||||
|
'nc_models', |
||||||
|
{ |
||||||
|
title: metaObj.tn, |
||||||
|
type: 'table', |
||||||
|
} |
||||||
|
); |
||||||
|
const oldSwaggerDoc = JSON.parse(meta.schema); |
||||||
|
|
||||||
|
// // keep upto 5 schema backup on table update
|
||||||
|
// let previousSchemas = [oldSwaggerDoc]
|
||||||
|
// if (meta.schema_previous) {
|
||||||
|
// previousSchemas = [...JSON.parse(meta.schema_previous), oldSwaggerDoc].slice(-5);
|
||||||
|
// }
|
||||||
|
|
||||||
|
oldSwaggerDoc.definitions = swaggerDoc.definitions; |
||||||
|
await this.xcMeta.metaUpdate( |
||||||
|
this.projectId, |
||||||
|
this.dbAlias, |
||||||
|
'nc_models', |
||||||
|
{ |
||||||
|
schema: JSON.stringify(oldSwaggerDoc), |
||||||
|
// schema_previous: JSON.stringify(previousSchemas)
|
||||||
|
}, |
||||||
|
{ |
||||||
|
title: metaObj.tn, |
||||||
|
type: 'table', |
||||||
|
} |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return metas; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,253 @@ |
|||||||
|
import { SqlUiFactory, UITypes } from 'nocodb-sdk'; |
||||||
|
import type { MssqlUi, MysqlUi, OracleUi, PgUi, SqliteUi } from 'nocodb-sdk'; |
||||||
|
|
||||||
|
export default class NcTemplateParser { |
||||||
|
sqlUi: |
||||||
|
| typeof MysqlUi |
||||||
|
| typeof MssqlUi |
||||||
|
| typeof PgUi |
||||||
|
| typeof OracleUi |
||||||
|
| typeof SqliteUi; |
||||||
|
|
||||||
|
private _tables: any[]; |
||||||
|
private client: string; |
||||||
|
private _relations: any[]; |
||||||
|
private _m2mRelations: any[]; |
||||||
|
private _virtualColumns: { [tn: string]: any[] }; |
||||||
|
private prefix: string; |
||||||
|
private template: any; |
||||||
|
|
||||||
|
constructor({ client, template, prefix = '' }) { |
||||||
|
this.client = client; |
||||||
|
this.sqlUi = SqlUiFactory.create({ client }); |
||||||
|
this.template = template; |
||||||
|
this.prefix = prefix; |
||||||
|
} |
||||||
|
|
||||||
|
public parse(template?: any): any { |
||||||
|
const tables = []; |
||||||
|
this.template = template || this.template; |
||||||
|
const tableTemplates = this.template.tables.map((tableTemplate) => { |
||||||
|
const t = { |
||||||
|
...tableTemplate, |
||||||
|
tn: this.getTable(tableTemplate.tn), |
||||||
|
_tn: tableTemplate._tn || tableTemplate.tn, |
||||||
|
}; |
||||||
|
const table = this.extractTable(t); |
||||||
|
tables.push(table); |
||||||
|
return t; |
||||||
|
}); |
||||||
|
|
||||||
|
this._tables = tables; |
||||||
|
|
||||||
|
for (const tableTemplate of tableTemplates) { |
||||||
|
this.extractRelations(tableTemplate); |
||||||
|
this.extractVirtualColumns(tableTemplate); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private extractTable(tableTemplate) { |
||||||
|
if (!tableTemplate?.tn) { |
||||||
|
throw Error('Missing table name in template'); |
||||||
|
} |
||||||
|
|
||||||
|
const defaultColumns = this.sqlUi |
||||||
|
.getNewTableColumns() |
||||||
|
.filter( |
||||||
|
(column) => |
||||||
|
column.cn !== 'title' && |
||||||
|
(column.uidt !== 'ID' || |
||||||
|
tableTemplate.columns.every((c) => c.uidt !== 'ID')) |
||||||
|
); |
||||||
|
|
||||||
|
return { |
||||||
|
tn: tableTemplate.tn, |
||||||
|
_tn: tableTemplate._tn, |
||||||
|
columns: [ |
||||||
|
defaultColumns[0], |
||||||
|
...this.extractTableColumns(tableTemplate.columns), |
||||||
|
...defaultColumns.slice(1), |
||||||
|
], |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
private extractTableColumns(tableColumns: any[]) { |
||||||
|
const columns = []; |
||||||
|
for (const tableColumn of tableColumns) { |
||||||
|
if (!tableColumn?.cn) { |
||||||
|
throw Error('Missing column name in template'); |
||||||
|
} |
||||||
|
switch (tableColumn.uidt) { |
||||||
|
// case UITypes.ForeignKey:
|
||||||
|
// // todo :
|
||||||
|
// this.extractRelations(tableColumn, 'bt');
|
||||||
|
// break;
|
||||||
|
// case UITypes.LinkToAnotherRecord:
|
||||||
|
// // todo :
|
||||||
|
// this.extractRelations(tableColumn, 'hm');
|
||||||
|
// // this.extractRelations(tableColumn, 'mm');
|
||||||
|
// break;
|
||||||
|
default: |
||||||
|
{ |
||||||
|
const colProp = this.sqlUi.getDataTypeForUiType(tableColumn); |
||||||
|
columns.push({ |
||||||
|
...this.sqlUi.getNewColumn(''), |
||||||
|
rqd: false, |
||||||
|
pk: false, |
||||||
|
ai: false, |
||||||
|
cdf: null, |
||||||
|
un: false, |
||||||
|
dtx: 'specificType', |
||||||
|
dtxp: this.sqlUi.getDefaultLengthForDatatype(colProp.dt), |
||||||
|
dtxs: this.sqlUi.getDefaultScaleForDatatype(colProp.dt), |
||||||
|
...colProp, |
||||||
|
_cn: tableColumn.cn, |
||||||
|
...tableColumn, |
||||||
|
}); |
||||||
|
} |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
return columns; |
||||||
|
} |
||||||
|
|
||||||
|
protected extractRelations(tableTemplate) { |
||||||
|
if (!this._relations) this._relations = []; |
||||||
|
if (!this._m2mRelations) this._m2mRelations = []; |
||||||
|
for (const hasMany of tableTemplate.hasMany || []) { |
||||||
|
const childTable = this.tables.find( |
||||||
|
(table) => table.tn === this.getTable(hasMany.tn) |
||||||
|
); |
||||||
|
const parentTable = this.tables.find( |
||||||
|
(table) => table.tn === tableTemplate.tn |
||||||
|
); |
||||||
|
const parentPrimaryColumn = parentTable.columns.find( |
||||||
|
(column) => column.uidt === UITypes.ID |
||||||
|
); |
||||||
|
//
|
||||||
|
// // if duplicate relation ignore
|
||||||
|
// if (
|
||||||
|
// this._relations.some(rl => {
|
||||||
|
// return (
|
||||||
|
// (rl.childTable === childTable.tn &&
|
||||||
|
// rl.parentTable === parentTable.tn) ||
|
||||||
|
// (rl.parentTable === childTable.tn &&
|
||||||
|
// rl.childTable === parentTable.tn)
|
||||||
|
// );
|
||||||
|
// })
|
||||||
|
// ) {
|
||||||
|
// continue;f
|
||||||
|
// }
|
||||||
|
|
||||||
|
// add a column in child table
|
||||||
|
const childColumnName = `${tableTemplate.tn}_id`; |
||||||
|
|
||||||
|
childTable.columns.push({ |
||||||
|
column_name: childColumnName, |
||||||
|
_cn: childColumnName, |
||||||
|
rqd: false, |
||||||
|
pk: false, |
||||||
|
ai: false, |
||||||
|
cdf: null, |
||||||
|
dt: parentPrimaryColumn.dt, |
||||||
|
dtxp: parentPrimaryColumn.dtxp, |
||||||
|
dtxs: parentPrimaryColumn.dtxs, |
||||||
|
un: parentPrimaryColumn.un, |
||||||
|
altered: 1, |
||||||
|
}); |
||||||
|
|
||||||
|
// add relation create entry
|
||||||
|
this._relations.push({ |
||||||
|
childColumn: childColumnName, |
||||||
|
childTable: childTable.tn, |
||||||
|
onDelete: 'NO ACTION', |
||||||
|
onUpdate: 'NO ACTION', |
||||||
|
parentColumn: parentPrimaryColumn.cn, |
||||||
|
parentTable: tableTemplate.tn, |
||||||
|
type: this.client === 'sqlite3' ? 'virtual' : 'real', |
||||||
|
updateRelation: false, |
||||||
|
}); |
||||||
|
} |
||||||
|
for (const manyToMany of tableTemplate.manyToMany || []) { |
||||||
|
// @ts-ignore
|
||||||
|
const childTable = this.tables.find( |
||||||
|
(table) => table.tn === this.getTable(manyToMany.rtn) |
||||||
|
); |
||||||
|
const parentTable = this.tables.find( |
||||||
|
(table) => table.tn === tableTemplate.tn |
||||||
|
); |
||||||
|
const parentPrimaryColumn = parentTable.columns.find( |
||||||
|
(column) => column.uidt === UITypes.ID |
||||||
|
); |
||||||
|
const childPrimaryColumn = childTable.columns.find( |
||||||
|
(column) => column.uidt === UITypes.ID |
||||||
|
); |
||||||
|
|
||||||
|
// if duplicate relation ignore
|
||||||
|
if ( |
||||||
|
this._m2mRelations.some((mm) => { |
||||||
|
return ( |
||||||
|
(mm.childTable === childTable.tn && |
||||||
|
mm.parentTable === parentTable.tn) || |
||||||
|
(mm.parentTable === childTable.tn && |
||||||
|
mm.childTable === parentTable.tn) |
||||||
|
); |
||||||
|
}) |
||||||
|
) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
// add many to many relation create entry
|
||||||
|
this._m2mRelations.push({ |
||||||
|
alias: 'title8', |
||||||
|
childColumn: childPrimaryColumn.cn, |
||||||
|
childTable: childTable.tn, |
||||||
|
onDelete: 'NO ACTION', |
||||||
|
onUpdate: 'NO ACTION', |
||||||
|
parentColumn: parentPrimaryColumn.cn, |
||||||
|
parentTable: parentTable.tn, |
||||||
|
type: this.client === 'sqlite3' ? 'virtual' : 'real', |
||||||
|
updateRelation: false, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private extractVirtualColumns(tableMeta) { |
||||||
|
if (!this._virtualColumns) this._virtualColumns = {}; |
||||||
|
const virtualColumns = []; |
||||||
|
for (const v of tableMeta.v || []) { |
||||||
|
const v1 = { ...v }; |
||||||
|
|
||||||
|
if (v.rl) { |
||||||
|
v1.rl.rlttn = v1.rl.rltn; |
||||||
|
v1.rl.rltn = this.getTable(v1.rl.rltn); |
||||||
|
} else if (v.lk) { |
||||||
|
v1.lk._ltn = v1.lk.ltn; |
||||||
|
v1.lk.ltn = this.getTable(v1.lk.ltn); |
||||||
|
} |
||||||
|
|
||||||
|
virtualColumns.push(v1); |
||||||
|
} |
||||||
|
this.virtualColumns[tableMeta.tn] = virtualColumns; |
||||||
|
} |
||||||
|
|
||||||
|
get tables(): any[] { |
||||||
|
return this._tables; |
||||||
|
} |
||||||
|
|
||||||
|
get relations(): any[] { |
||||||
|
return this._relations; |
||||||
|
} |
||||||
|
|
||||||
|
get m2mRelations(): any[] { |
||||||
|
return this._m2mRelations; |
||||||
|
} |
||||||
|
|
||||||
|
get virtualColumns(): { [tn: string]: any[] } { |
||||||
|
return this._virtualColumns; |
||||||
|
} |
||||||
|
|
||||||
|
private getTable(tn) { |
||||||
|
return `${this.prefix}${tn}`; |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue