Browse Source

feat: add project apis

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/5444/head
Pranav C 2 years ago
parent
commit
a0bb1f033f
  1. BIN
      packages/nocodb-nest/noco.db
  2. 14
      packages/nocodb-nest/package-lock.json
  3. 2
      packages/nocodb-nest/package.json
  4. 6
      packages/nocodb-nest/src/app.module.ts
  5. 2
      packages/nocodb-nest/src/cache/RedisMockCacheMgr.ts
  6. 2637
      packages/nocodb-nest/src/db/BaseModelSql.ts
  7. 36
      packages/nocodb-nest/src/db/BaseModelSqlv2.ts
  8. 1317
      packages/nocodb-nest/src/db/CustomKnex.ts
  9. 94
      packages/nocodb-nest/src/db/conditionV2.ts
  10. 20
      packages/nocodb-nest/src/db/formulav2/formulaQueryBuilderv2.ts
  11. 4
      packages/nocodb-nest/src/db/functionMappings/mssql.ts
  12. 4
      packages/nocodb-nest/src/db/functionMappings/mysql.ts
  13. 4
      packages/nocodb-nest/src/db/functionMappings/pg.ts
  14. 6
      packages/nocodb-nest/src/db/functionMappings/sqlite.ts
  15. 6
      packages/nocodb-nest/src/db/genRollupSelectv2.ts
  16. 2
      packages/nocodb-nest/src/db/mapFunctionName.ts
  17. 48
      packages/nocodb-nest/src/db/sortV2.ts
  18. 8
      packages/nocodb-nest/src/db/sql-data-mapper/index.ts
  19. 2
      packages/nocodb-nest/src/db/sql-data-mapper/lib/DbFactory.ts
  20. 2
      packages/nocodb-nest/src/db/sql-mgr/code/models/xc/BaseModelXcMeta.ts
  21. 153
      packages/nocodb-nest/src/db/sql-mgr/code/policies/xc/ExpressXcPolicy.ts
  22. 5
      packages/nocodb-nest/src/db/sql-mgr/v2/ProjectMgrv2.ts
  23. 9
      packages/nocodb-nest/src/db/sql-mgr/v2/SqlMgrv2Trans.ts
  24. 2
      packages/nocodb-nest/src/db/sql-migrator/lib/KnexMigratorv2.ts
  25. 6
      packages/nocodb-nest/src/db/sql-migrator/lib/KnexMigratorv2Tans.ts
  26. 67
      packages/nocodb-nest/src/helpers/NcPluginMgrv2.ts
  27. 2
      packages/nocodb-nest/src/helpers/addFormulaErrorIfMissingColumn.ts
  28. 61
      packages/nocodb-nest/src/helpers/apiHelpers.ts
  29. 13
      packages/nocodb-nest/src/helpers/cacheHelpers.ts
  30. 208
      packages/nocodb-nest/src/helpers/columnHelpers.ts
  31. 137
      packages/nocodb-nest/src/helpers/convertUnits.ts
  32. 31
      packages/nocodb-nest/src/helpers/extractProjectIdAndAuthenticate.ts
  33. 54
      packages/nocodb-nest/src/helpers/formulaFnHelper.ts
  34. 216
      packages/nocodb-nest/src/helpers/getAst.ts
  35. 4
      packages/nocodb-nest/src/helpers/getColumnPropsFromUIDT.ts
  36. 6
      packages/nocodb-nest/src/helpers/getColumnUiType.ts
  37. 2
      packages/nocodb-nest/src/helpers/getHandler.ts
  38. 2
      packages/nocodb-nest/src/helpers/getTableName.ts
  39. 2
      packages/nocodb-nest/src/helpers/getUniqueName.ts
  40. 6
      packages/nocodb-nest/src/helpers/index.ts
  41. 2
      packages/nocodb-nest/src/helpers/ncMetaAclMw.ts
  42. 280
      packages/nocodb-nest/src/helpers/populateMeta.ts
  43. 7
      packages/nocodb-nest/src/helpers/populateSamplePayload.ts
  44. 11
      packages/nocodb-nest/src/helpers/sanitize.ts
  45. 11
      packages/nocodb-nest/src/helpers/syncMigration.ts
  46. 1
      packages/nocodb-nest/src/helpers/webhookHelpers.ts
  47. 15
      packages/nocodb-nest/src/interface/IEmailAdapter.ts
  48. 17
      packages/nocodb-nest/src/interface/IStorageAdapter.ts
  49. 8
      packages/nocodb-nest/src/interface/XcDynamicChanges.ts
  50. 2
      packages/nocodb-nest/src/interface/XcMetaMgr.ts
  51. 293
      packages/nocodb-nest/src/interface/config.ts
  52. 746
      packages/nocodb-nest/src/meta/meta.service.ts
  53. 4
      packages/nocodb-nest/src/models/Model.ts
  54. 142
      packages/nocodb-nest/src/plugins/backblaze/Backblaze.ts
  55. 18
      packages/nocodb-nest/src/plugins/backblaze/BackblazePlugin.ts
  56. 68
      packages/nocodb-nest/src/plugins/backblaze/index.ts
  57. 21
      packages/nocodb-nest/src/plugins/discord/Discord.ts
  58. 18
      packages/nocodb-nest/src/plugins/discord/DiscordPlugin.ts
  59. 56
      packages/nocodb-nest/src/plugins/discord/index.ts
  60. 129
      packages/nocodb-nest/src/plugins/gcs/Gcs.ts
  61. 18
      packages/nocodb-nest/src/plugins/gcs/GcsPlugin.ts
  62. 69
      packages/nocodb-nest/src/plugins/gcs/index.ts
  63. 132
      packages/nocodb-nest/src/plugins/linode/LinodeObjectStorage.ts
  64. 18
      packages/nocodb-nest/src/plugins/linode/LinodeObjectStoragePlugin.ts
  65. 68
      packages/nocodb-nest/src/plugins/linode/index.ts
  66. 48
      packages/nocodb-nest/src/plugins/mailerSend/MailerSend.ts
  67. 18
      packages/nocodb-nest/src/plugins/mailerSend/MailerSendPlugin.ts
  68. 60
      packages/nocodb-nest/src/plugins/mailerSend/index.ts
  69. 21
      packages/nocodb-nest/src/plugins/mattermost/Mattermost.ts
  70. 18
      packages/nocodb-nest/src/plugins/mattermost/MattermostPlugin.ts
  71. 56
      packages/nocodb-nest/src/plugins/mattermost/index.ts
  72. 134
      packages/nocodb-nest/src/plugins/mino/Minio.ts
  73. 18
      packages/nocodb-nest/src/plugins/mino/MinioPlugin.ts
  74. 83
      packages/nocodb-nest/src/plugins/mino/index.ts
  75. 132
      packages/nocodb-nest/src/plugins/ovhCloud/OvhCloud.ts
  76. 18
      packages/nocodb-nest/src/plugins/ovhCloud/OvhCloudPlugin.ts
  77. 68
      packages/nocodb-nest/src/plugins/ovhCloud/index.ts
  78. 135
      packages/nocodb-nest/src/plugins/s3/S3.ts
  79. 18
      packages/nocodb-nest/src/plugins/s3/S3Plugin.ts
  80. 68
      packages/nocodb-nest/src/plugins/s3/index.ts
  81. 130
      packages/nocodb-nest/src/plugins/scaleway/ScalewayObjectStorage.ts
  82. 18
      packages/nocodb-nest/src/plugins/scaleway/ScalewayObjectStoragePlugin.ts
  83. 67
      packages/nocodb-nest/src/plugins/scaleway/index.ts
  84. 54
      packages/nocodb-nest/src/plugins/ses/SES.ts
  85. 18
      packages/nocodb-nest/src/plugins/ses/SESPlugin.ts
  86. 69
      packages/nocodb-nest/src/plugins/ses/index.ts
  87. 21
      packages/nocodb-nest/src/plugins/slack/Slack.ts
  88. 18
      packages/nocodb-nest/src/plugins/slack/SlackPlugin.ts
  89. 56
      packages/nocodb-nest/src/plugins/slack/index.ts
  90. 59
      packages/nocodb-nest/src/plugins/smtp/SMTP.ts
  91. 18
      packages/nocodb-nest/src/plugins/smtp/SMTPPlugin.ts
  92. 96
      packages/nocodb-nest/src/plugins/smtp/index.ts
  93. 140
      packages/nocodb-nest/src/plugins/spaces/Spaces.ts
  94. 18
      packages/nocodb-nest/src/plugins/spaces/SpacesPlugin.ts
  95. 69
      packages/nocodb-nest/src/plugins/spaces/index.ts
  96. 91
      packages/nocodb-nest/src/plugins/storage/Local.ts
  97. 21
      packages/nocodb-nest/src/plugins/teams/Teams.ts
  98. 18
      packages/nocodb-nest/src/plugins/teams/TeamsPlugin.ts
  99. 56
      packages/nocodb-nest/src/plugins/teams/index.ts
  100. 30
      packages/nocodb-nest/src/plugins/twilio/Twilio.ts
  101. Some files were not shown because too many files have changed in this diff Show More

BIN
packages/nocodb-nest/noco.db

Binary file not shown.

14
packages/nocodb-nest/package-lock.json generated

@ -36,7 +36,7 @@
"cron": "^1.8.2",
"crypto-js": "^4.0.0",
"dataloader": "^2.0.0",
"dayjs": "^1.11.7",
"dayjs": "^1.8.34",
"debug": "^4.2.0",
"dotenv": "^8.2.0",
"ejs": "^3.1.3",
@ -6146,9 +6146,9 @@
}
},
"node_modules/dayjs": {
"version": "1.11.7",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
"integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ=="
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz",
"integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A=="
},
"node_modules/debug": {
"version": "4.3.4",
@ -21558,9 +21558,9 @@
"integrity": "sha512-wJMBjqlwXR0Iv0wUo/lFbhSQ7MmG1hl36iuxuE91kW+5b5sWbase73manEqNH9sOLFAMG83B4ffNKq9/Iq0FVA=="
},
"dayjs": {
"version": "1.11.7",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
"integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ=="
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz",
"integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A=="
},
"debug": {
"version": "4.3.4",

2
packages/nocodb-nest/package.json

@ -47,7 +47,7 @@
"cron": "^1.8.2",
"crypto-js": "^4.0.0",
"dataloader": "^2.0.0",
"dayjs": "^1.11.7",
"dayjs": "^1.8.34",
"debug": "^4.2.0",
"dotenv": "^8.2.0",
"ejs": "^3.1.3",

6
packages/nocodb-nest/src/app.module.ts

@ -5,11 +5,13 @@ import { UsersModule } from './users/users.module';
import { MetaService } from './meta/meta.service';
import { LocalStrategy } from './local.strategy/local.strategy';
import { UtilsModule } from './utils/utils.module';
import { ProjectsModule } from './projects/projects.module';
import { JwtStrategy } from './strategies/jwt.strategy/jwt.strategy';
@Module({
imports: [AuthModule, UsersModule, UtilsModule],
imports: [AuthModule, UsersModule, UtilsModule, ProjectsModule],
controllers: [],
providers: [Connection, MetaService],
providers: [Connection, MetaService, JwtStrategy],
exports: [Connection, MetaService],
})
export class AppModule {}

2
packages/nocodb-nest/src/cache/RedisMockCacheMgr.ts vendored

@ -1,5 +1,5 @@
import debug from 'debug';
import Redis from 'ioredis-mock';
import * as Redis from 'ioredis-mock';
import { CacheDelDirection, CacheGetType, CacheScope } from '../utils/globals';
import CacheMgr from './CacheMgr';
const log = debug('nc:cache');

2637
packages/nocodb-nest/src/db/BaseModelSql.ts

File diff suppressed because it is too large Load Diff

36
packages/nocodb-nest/src/db/BaseModelSqlv2.ts

@ -23,20 +23,50 @@ const GROUP_COL = '__nc_group_id';
const nanoidv2 = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 14);
import { v4 as uuidv4 } from 'uuid';
import { NcError } from '../helpers/catchError';
import getAst from '../helpers/getAst'
import NcPluginMgrv2 from '../helpers/NcPluginMgrv2'
import { _transformSubmittedFormDataForEmail, invokeWebhook } from '../helpers/webhookHelpers'
import {
Audit,
BarcodeColumn,
Column,
Filter,
FormulaColumn, FormView, GridViewColumn, Hook,
LinkToAnotherRecordColumn,
Model,
Model, Project, QrCodeColumn, RollupColumn, SelectOption, Sort,
View,
} from '../models';
} from '../models'
import formulaQueryBuilderv2 from './formulav2/formulaQueryBuilderv2'
import genRollupSelectv2 from './genRollupSelectv2'
import { XcFilter, XcFilterWithAlias } from './sql-data-mapper/lib/BaseModel';
import { sanitize, unsanitize } from 'src/helpers/sqlSanitize';
import conditionV2 from './conditionV2';
import sortV2 from './sortV2';
import { customValidators } from './util/customValidators';
import { COMPARISON_OPS, COMPARISON_SUB_OPS, IS_WITHIN_COMPARISON_SUB_OPS } from 'src/models/Filter'
import formSubmissionEmailTemplate from 'src/utils/common/formSubmissionEmailTemplate';
export async function getViewAndModelByAliasOrId(param: {
projectName: string;
tableName: string;
viewName?: string;
}) {
const project = await Project.getWithInfoByTitleOrId(param.projectName);
const model = await Model.getByAliasOrId({
project_id: project.id,
aliasOrId: param.tableName,
});
const view =
param.viewName &&
(await View.getByTitleOrId({
titleOrId: param.viewName,
fk_model_id: model.id,
}));
if (!model) NcError.notFound('Table not found');
return { model, view };
}
async function populatePk(model: Model, insertObj: any) {
await model.getColumns();
@ -69,7 +99,7 @@ function checkColumnRequired(
* @classdesc Base class for models
*/
class BaseModelSqlv2 {
protected dbDriver: XKnex;
protected dbDriver: Knex;
protected model: Model;
protected viewId: string;
private _proto: any;

1317
packages/nocodb-nest/src/db/CustomKnex.ts

File diff suppressed because it is too large Load Diff

94
packages/nocodb-nest/src/db/conditionV2.ts

@ -1,24 +1,24 @@
import { isNumericCol, RelationTypes, UITypes } from 'nocodb-sdk';
import dayjs, { extend } from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat.js';
import Filter from '../../../../models/Filter';
// import customParseFormat from 'dayjs/plugin/customParseFormat.js';
import Filter from '../models/Filter';
import genRollupSelectv2 from './genRollupSelectv2';
import formulaQueryBuilderv2 from './formulav2/formulaQueryBuilderv2';
import { sanitize } from './helpers/sanitize';
import type LinkToAnotherRecordColumn from '../../../../models/LinkToAnotherRecordColumn';
import { sanitize } from '../helpers/sqlSanitize';
import type LinkToAnotherRecordColumn from '../models/LinkToAnotherRecordColumn';
import type { Knex } from 'knex';
import type { XKnex } from '../../index';
import type Column from '../../../../models/Column';
import type LookupColumn from '../../../../models/LookupColumn';
import type RollupColumn from '../../../../models/RollupColumn';
import type FormulaColumn from '../../../../models/FormulaColumn';
import type Column from '../models/Column';
import type LookupColumn from '../models/LookupColumn';
import type RollupColumn from '../models/RollupColumn';
import type FormulaColumn from '../models/FormulaColumn';
extend(customParseFormat);
// tod: tobe fixed
// extend(customParseFormat);
export default async function conditionV2(
conditionObj: Filter | Filter[],
qb: Knex.QueryBuilder,
knex: XKnex
knex: Knex,
) {
if (!conditionObj || typeof conditionObj !== 'object') {
return;
@ -41,10 +41,10 @@ function getLogicalOpMethod(filter: Filter) {
const parseConditionV2 = async (
_filter: Filter | Filter[],
knex: XKnex,
knex: Knex,
aliasCount = { count: 0 },
alias?,
customWhereClause?
customWhereClause?,
) => {
let filter: Filter;
if (!Array.isArray(_filter)) {
@ -53,7 +53,7 @@ const parseConditionV2 = async (
}
if (Array.isArray(_filter)) {
const qbs = await Promise.all(
_filter.map((child) => parseConditionV2(child, knex, aliasCount))
_filter.map((child) => parseConditionV2(child, knex, aliasCount)),
);
return (qbP) => {
@ -67,7 +67,9 @@ const parseConditionV2 = async (
const children = await filter.getChildren();
const qbs = await Promise.all(
(children || []).map((child) => parseConditionV2(child, knex, aliasCount))
(children || []).map((child) =>
parseConditionV2(child, knex, aliasCount),
),
);
return (qbP) => {
@ -79,6 +81,7 @@ const parseConditionV2 = async (
};
} else {
const column = await filter.getColumn();
// eslint-disable-next-line @typescript-eslint/no-empty-function
if (!column) return () => {};
if (column.uidt === UITypes.LinkToAnotherRecord) {
const colOptions =
@ -111,7 +114,7 @@ const parseConditionV2 = async (
knex.raw('??.??', [
alias || parentModel.table_name,
parentColumn.column_name,
])
]),
);
return (qb) => {
@ -123,7 +126,7 @@ const parseConditionV2 = async (
};
}
const selectQb = knex(childModel.table_name).select(
childColumn.column_name
childColumn.column_name,
);
(
await parseConditionV2(
@ -136,7 +139,7 @@ const parseConditionV2 = async (
fk_column_id: childModel?.displayValue?.id,
}),
knex,
aliasCount
aliasCount,
)
)(selectQb);
@ -167,7 +170,7 @@ const parseConditionV2 = async (
knex.raw('??.??', [
alias || childModel.table_name,
childColumn.column_name,
])
]),
);
return (qb) => {
@ -180,7 +183,7 @@ const parseConditionV2 = async (
}
const selectQb = knex(parentModel.table_name).select(
parentColumn.column_name
parentColumn.column_name,
);
(
await parseConditionV2(
@ -193,7 +196,7 @@ const parseConditionV2 = async (
fk_column_id: parentModel?.displayValue?.id,
}),
knex,
aliasCount
aliasCount,
)
)(selectQb);
@ -228,7 +231,7 @@ const parseConditionV2 = async (
knex.raw('??.??', [
alias || childModel.table_name,
childColumn.column_name,
])
]),
);
return (qb) => {
@ -245,7 +248,7 @@ const parseConditionV2 = async (
.join(
parentModel.table_name,
`${mmModel.table_name}.${mmParentColumn.column_name}`,
`${parentModel.table_name}.${parentColumn.column_name}`
`${parentModel.table_name}.${parentColumn.column_name}`,
);
(
@ -259,7 +262,7 @@ const parseConditionV2 = async (
fk_column_id: parentModel?.displayValue?.id,
}),
knex,
aliasCount
aliasCount,
)
)(selectQb);
@ -270,6 +273,7 @@ const parseConditionV2 = async (
};
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
return (_qb) => {};
} else if (column.uidt === UITypes.Lookup) {
return await generateLookupCondition(column, filter, knex, aliasCount);
@ -286,7 +290,7 @@ const parseConditionV2 = async (
knex,
aliasCount,
alias,
builder
builder,
);
} else if (column.uidt === UITypes.Formula && !customWhereClause) {
const model = await column.getModel();
@ -299,7 +303,7 @@ const parseConditionV2 = async (
knex,
aliasCount,
alias,
builder
builder,
);
} else {
if (
@ -312,7 +316,7 @@ const parseConditionV2 = async (
? filter.value
: alias
? `${alias}.${column.column_name}`
: column.column_name
: column.column_name,
);
const _val = customWhereClause ? customWhereClause : filter.value;
@ -630,7 +634,7 @@ const parseConditionV2 = async (
case 'in':
qb = qb.whereIn(
field,
Array.isArray(val) ? val : val?.split?.(',')
Array.isArray(val) ? val : val?.split?.(','),
);
break;
case 'is':
@ -692,7 +696,7 @@ const parseConditionV2 = async (
if (
!isNumericCol(column.uidt) &&
![UITypes.Date, UITypes.DateTime, UITypes.Time].includes(
column.uidt
column.uidt,
)
) {
qb = qb.orWhere(field, '');
@ -710,7 +714,7 @@ const parseConditionV2 = async (
if (
!isNumericCol(column.uidt) &&
![UITypes.Date, UITypes.DateTime, UITypes.Time].includes(
column.uidt
column.uidt,
)
) {
qb = qb.whereNot(field, '');
@ -771,7 +775,7 @@ async function generateLookupCondition(
col: Column,
filter: Filter,
knex,
aliasCount = { count: 0 }
aliasCount = { count: 0 },
): Promise<any> {
const colOptions = await col.getColOptions<LookupColumn>();
const relationColumn = await colOptions.getRelationColumn();
@ -814,7 +818,7 @@ async function generateLookupCondition(
qb,
knex,
alias,
aliasCount
aliasCount,
);
}
@ -847,7 +851,7 @@ async function generateLookupCondition(
qb,
knex,
alias,
aliasCount
aliasCount,
);
}
@ -868,7 +872,7 @@ async function generateLookupCondition(
.join(
`${parentModel.table_name} as ${childAlias}`,
`${alias}.${mmParentColumn.column_name}`,
`${childAlias}.${parentColumn.column_name}`
`${childAlias}.${parentColumn.column_name}`,
);
if (filter.comparison_op === 'blank') {
@ -891,7 +895,7 @@ async function generateLookupCondition(
qb,
knex,
childAlias,
aliasCount
aliasCount,
);
}
@ -910,7 +914,7 @@ async function nestedConditionJoin(
qb: Knex.QueryBuilder,
knex,
alias: string,
aliasCount: { count: number }
aliasCount: { count: number },
) {
if (
lookupColumn.uidt === UITypes.Lookup ||
@ -939,7 +943,7 @@ async function nestedConditionJoin(
qb.join(
`${childModel.table_name} as ${relAlias}`,
`${alias}.${parentColumn.column_name}`,
`${relAlias}.${childColumn.column_name}`
`${relAlias}.${childColumn.column_name}`,
);
}
break;
@ -948,7 +952,7 @@ async function nestedConditionJoin(
qb.join(
`${parentModel.table_name} as ${relAlias}`,
`${alias}.${childColumn.column_name}`,
`${relAlias}.${parentColumn.column_name}`
`${relAlias}.${parentColumn.column_name}`,
);
}
break;
@ -963,11 +967,11 @@ async function nestedConditionJoin(
qb.join(
`${mmModel.table_name} as ${assocAlias}`,
`${assocAlias}.${mmChildColumn.column_name}`,
`${alias}.${childColumn.column_name}`
`${alias}.${childColumn.column_name}`,
).join(
`${parentModel.table_name} as ${relAlias}`,
`${relAlias}.${parentColumn.column_name}`,
`${assocAlias}.${mmParentColumn.column_name}`
`${assocAlias}.${mmParentColumn.column_name}`,
);
}
break;
@ -983,7 +987,7 @@ async function nestedConditionJoin(
qb,
knex,
relAlias,
aliasCount
aliasCount,
);
} else {
switch (relationColOptions.type) {
@ -998,7 +1002,7 @@ async function nestedConditionJoin(
}),
knex,
aliasCount,
relAlias
relAlias,
)
)(qb);
}
@ -1014,7 +1018,7 @@ async function nestedConditionJoin(
}),
knex,
aliasCount,
relAlias
relAlias,
)
)(qb);
}
@ -1030,7 +1034,7 @@ async function nestedConditionJoin(
}),
knex,
aliasCount,
relAlias
relAlias,
)
)(qb);
}
@ -1047,7 +1051,7 @@ async function nestedConditionJoin(
}),
knex,
aliasCount,
alias
alias,
)
)(qb);
}

20
packages/nocodb-nest/src/db/formulav2/formulaQueryBuilderv2.ts

@ -2,16 +2,16 @@ import jsep from 'jsep';
import { jsepCurlyHook, UITypes } from 'nocodb-sdk';
import mapFunctionName from '../mapFunctionName';
import genRollupSelectv2 from '../genRollupSelectv2';
import FormulaColumn from '../../../../../models/FormulaColumn';
import { validateDateWithUnknownFormat } from '../helpers/formulaFnHelper';
import { CacheGetType, CacheScope } from '../../../../../utils/globals';
import NocoCache from '../../../../../cache/NocoCache';
import type Model from '../../../../../models/Model';
import type Column from '../../../../../models/Column';
import type RollupColumn from '../../../../../models/RollupColumn';
import type { XKnex } from '../../../index';
import type LinkToAnotherRecordColumn from '../../../../../models/LinkToAnotherRecordColumn';
import type LookupColumn from '../../../../../models/LookupColumn';
import FormulaColumn from '../../models/FormulaColumn';
import { validateDateWithUnknownFormat } from '../../helpers/formulaFnHelper';
import { CacheGetType, CacheScope } from '../../utils/globals';
import NocoCache from '../../cache/NocoCache';
import type Model from '../../models/Model';
import type Column from '../../models/Column';
import type RollupColumn from '../../models/RollupColumn';
import type LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn';
import type LookupColumn from '../../models/LookupColumn';
import { XKnex } from '../CustomKnex';
// todo: switch function based on database

4
packages/nocodb-nest/src/db/functionMappings/mssql.ts

@ -1,6 +1,6 @@
import dayjs from 'dayjs';
import { convertUnits } from '../helpers/convertUnits';
import { getWeekdayByText } from '../helpers/formulaFnHelper';
import { convertUnits } from '../../helpers/convertUnits';
import { getWeekdayByText } from '../../helpers/formulaFnHelper';
import commonFns from './commonFns';
import type { MapFnArgs } from '../mapFunctionName';

4
packages/nocodb-nest/src/db/functionMappings/mysql.ts

@ -1,6 +1,6 @@
import dayjs from 'dayjs';
import { convertUnits } from '../helpers/convertUnits';
import { getWeekdayByText } from '../helpers/formulaFnHelper';
import { convertUnits } from '../../helpers/convertUnits';
import { getWeekdayByText } from '../../helpers/formulaFnHelper';
import commonFns from './commonFns';
import type { MapFnArgs } from '../mapFunctionName';

4
packages/nocodb-nest/src/db/functionMappings/pg.ts

@ -1,6 +1,6 @@
import dayjs from 'dayjs';
import { convertUnits } from '../helpers/convertUnits';
import { getWeekdayByText } from '../helpers/formulaFnHelper';
import { convertUnits } from '../../helpers/convertUnits';
import { getWeekdayByText } from '../../helpers/formulaFnHelper';
import commonFns from './commonFns';
import type { MapFnArgs } from '../mapFunctionName';

6
packages/nocodb-nest/src/db/functionMappings/sqlite.ts

@ -1,10 +1,10 @@
import dayjs from 'dayjs';
import { convertUnits } from '../helpers/convertUnits';
import { getWeekdayByText } from '../helpers/formulaFnHelper';
import { convertUnits } from '../../helpers/convertUnits';
import { getWeekdayByText } from '../../helpers/formulaFnHelper';
import {
convertToTargetFormat,
getDateFormat,
} from '../../../../../utils/dateTimeUtils';
} from '../../utils/dateTimeUtils';
import commonFns from './commonFns';
import type { MapFnArgs } from '../mapFunctionName';

6
packages/nocodb-nest/src/db/genRollupSelectv2.ts

@ -1,7 +1,7 @@
import { RelationTypes } from 'nocodb-sdk';
import type RollupColumn from '../../../../models/RollupColumn';
import type { XKnex } from '../../index';
import type LinkToAnotherRecordColumn from '../../../../models/LinkToAnotherRecordColumn';
import type { RollupColumn } from '../models';
import type { XKnex } from '../db/CustomKnex';
import type { LinkToAnotherRecordColumn } from '../models';
import type { Knex } from 'knex';
export default async function ({

2
packages/nocodb-nest/src/db/mapFunctionName.ts

@ -2,7 +2,7 @@ import mssql from './functionMappings/mssql';
import mysql from './functionMappings/mysql';
import pg from './functionMappings/pg';
import sqlite from './functionMappings/sqlite';
import type { XKnex } from '../../index';
import type { XKnex } from '../db/CustomKnex';
import type { Knex } from 'knex';
export interface MapFnArgs {

48
packages/nocodb-nest/src/db/sortV2.ts

@ -1,19 +1,21 @@
import { RelationTypes, UITypes } from 'nocodb-sdk';
import Sort from '../../../../models/Sort';
import { Sort } from '../models';
import genRollupSelectv2 from './genRollupSelectv2';
import formulaQueryBuilderv2 from './formulav2/formulaQueryBuilderv2';
import { sanitize } from './helpers/sanitize';
import { sanitize } from '../helpers/sqlSanitize';
import type { Knex } from 'knex';
import type { XKnex } from '../../index';
import type LinkToAnotherRecordColumn from '../../../../models/LinkToAnotherRecordColumn';
import type RollupColumn from '../../../../models/RollupColumn';
import type LookupColumn from '../../../../models/LookupColumn';
import type FormulaColumn from '../../../../models/FormulaColumn';
import type { XKnex } from '../db/CustomKnex';
import {
LinkToAnotherRecordColumn,
RollupColumn,
LookupColumn,
FormulaColumn,
} from '../models';
export default async function sortV2(
sortList: Sort[],
qb: Knex.QueryBuilder,
knex: XKnex
knex: XKnex,
) {
if (!sortList?.length) {
return;
@ -55,7 +57,7 @@ export default async function sortV2(
null,
knex,
model,
column
column,
)
).builder;
qb.orderBy(builder, sort.direction || 'asc', nulls);
@ -85,7 +87,7 @@ export default async function sortV2(
`${alias}.${parentColumn.column_name}`,
knex.raw(`??`, [
`${childModel.table_name}.${childColumn.column_name}`,
])
]),
);
}
let lookupColumn = await lookup.getLookupColumn();
@ -111,7 +113,7 @@ export default async function sortV2(
selectQb.join(
`${parentModel.table_name} as ${nestedAlias}`,
`${nestedAlias}.${parentColumn.column_name}`,
`${prevAlias}.${childColumn.column_name}`
`${prevAlias}.${childColumn.column_name}`,
);
lookupColumn = await nestedLookup.getLookupColumn();
@ -151,7 +153,7 @@ export default async function sortV2(
.join(
`${parentModel.table_name} as ${nestedAlias}`,
`${nestedAlias}.${parentColumn.column_name}`,
`${prevAlias}.${childColumn.column_name}`
`${prevAlias}.${childColumn.column_name}`,
)
.select(parentModel?.displayValue?.column_name);
}
@ -166,7 +168,7 @@ export default async function sortV2(
null,
knex,
model,
column
column,
)
).builder;
@ -206,7 +208,7 @@ export default async function sortV2(
`${parentModel.table_name}.${parentColumn.column_name}`,
knex.raw(`??`, [
`${childModel.table_name}.${childColumn.column_name}`,
])
]),
);
qb.orderBy(selectQb, sort.direction || 'asc', nulls);
@ -218,21 +220,21 @@ export default async function sortV2(
qb.orderBy(
sanitize(knex.raw('CONCAT(??)', [column.column_name])),
sort.direction || 'asc',
nulls
nulls,
);
} else if (clientType === 'mssql') {
qb.orderBy(
sanitize(
knex.raw('CAST(?? AS VARCHAR(MAX))', [column.column_name])
knex.raw('CAST(?? AS VARCHAR(MAX))', [column.column_name]),
),
sort.direction || 'asc',
nulls
nulls,
);
} else {
qb.orderBy(
sanitize(column.column_name),
sort.direction || 'asc',
nulls
nulls,
);
}
break;
@ -243,21 +245,21 @@ export default async function sortV2(
qb.orderBy(
sanitize(knex.raw('CONCAT(??)', [column.column_name])),
sort.direction || 'asc',
nulls
nulls,
);
} else if (clientType === 'mssql') {
qb.orderBy(
sanitize(
knex.raw('CAST(?? AS VARCHAR(MAX))', [column.column_name])
knex.raw('CAST(?? AS VARCHAR(MAX))', [column.column_name]),
),
sort.direction || 'asc',
nulls
nulls,
);
} else {
qb.orderBy(
sanitize(column.column_name),
sort.direction || 'asc',
nulls
nulls,
);
}
break;
@ -266,7 +268,7 @@ export default async function sortV2(
qb.orderBy(
sanitize(column.column_name),
sort.direction || 'asc',
nulls
nulls,
);
break;
}

8
packages/nocodb-nest/src/db/sql-data-mapper/index.ts

@ -1,5 +1,5 @@
import XKnex, { Knex } from './lib/sql/CustomKnex';
// import XKnex, { Knex } from './lib/sql/CustomKnex';
//
export { DbFactory } from './lib/DbFactory';
export { BaseModelSql } from './lib/sql/BaseModelSql';
export { XKnex, Knex };
// export { BaseModelSql } from './lib/sql/BaseModelSql';
// export { XKnex, Knex };

2
packages/nocodb-nest/src/db/sql-data-mapper/lib/DbFactory.ts

@ -1,4 +1,4 @@
import knex from './sql/CustomKnex';
import knex from 'knex' //'./sql/CustomKnex';
export class DbFactory {
static create(connectionConfig) {

2
packages/nocodb-nest/src/db/sql-mgr/code/models/xc/BaseModelXcMeta.ts

@ -1,6 +1,6 @@
import { UITypes } from 'nocodb-sdk';
import BaseRender from '../../BaseRender';
import mapDefaultDisplayValue from '../../../../../meta/helpers/mapDefaultDisplayValue';
import mapDefaultDisplayValue from '../../../../../helpers/mapDefaultDisplayValue';
abstract class BaseModelXcMeta extends BaseRender {
protected abstract _getAbstractType(column: any): any;

153
packages/nocodb-nest/src/db/sql-mgr/code/policies/xc/ExpressXcPolicy.ts

@ -1,153 +0,0 @@
import uniqBy from 'lodash/uniqBy';
import BaseRender from '../../BaseRender';
import type { Acl } from '../../../../../../interface/config';
class ExpressXcMiddleware extends BaseRender {
/**
*
* @param dir
* @param filename
* @param ctx
* @param ctx.tn
* @param ctx.columns
* @param ctx.relations
*/
constructor({ dir, filename, ctx }) {
super({ dir, filename, ctx });
}
/**
* Prepare variables used in code template
*/
prepare() {
let data: any = {};
/* run of simple variable */
data = this.ctx;
/* for complex code provide a func and args - do derivation within the func cbk */
data.hasMany = {
func: this._renderXcHasManyRoutePermissions.bind(this),
args: {
tn: this.ctx.tn,
columns: this.ctx.columns,
hasMany: this.ctx.hasMany,
relations: this.ctx.relations,
routeVersionLetter: this.ctx.routeVersionLetter,
},
};
/* for complex code provide a func and args - do derivation within the func cbk */
data.belongsTo = {
func: this._renderXcBelongsToRoutePermissions.bind(this),
args: {
dbType: this.ctx.dbType,
tn: this.ctx.tn,
columns: this.ctx.columns,
belongsTo: this.ctx.belongsTo,
relations: this.ctx.relations,
routeVersionLetter: this.ctx.routeVersionLetter,
},
};
return data;
}
private _renderXcHasManyRoutePermissions(args) {
let str = '';
let hmRelations = args.relations
? args.relations.filter((r) => r.rtn === args.tn)
: [];
if (hmRelations.length > 1)
hmRelations = uniqBy(hmRelations, function (e) {
return [e.tn, e.rtn].join();
});
for (let i = 0; i < hmRelations.length; ++i) {
str += `
'/api/${args.routeVersionLetter}1/${args.tn}/has/${hmRelations[i].tn}' : {get:{admin:true,user:true,guest:true}},
'/api/${args.routeVersionLetter}1/${args.tn}/:parentId/${hmRelations[i].tn}' : {get:{admin:true,user:true,guest:true},post:{admin:true,user:true,guest:true}},
'/api/${args.routeVersionLetter}1/${args.tn}/:parentId/${hmRelations[i].tn}/findOne' : {get:{admin:true,user:true,guest:true}},
'/api/${args.routeVersionLetter}1/${args.tn}/:parentId/${hmRelations[i].tn}/count' : {get:{admin:true,user:true,guest:true}},
'/api/${args.routeVersionLetter}1/${args.tn}/:parentId/${hmRelations[i].tn}/:id' : {get:{admin:true,user:true,guest:true},put:{admin:true,user:true,guest:true},delete:{admin:true,user:true,guest:true}},
'/api/${args.routeVersionLetter}1/${args.tn}/:parentId/${hmRelations[i].tn}/:id/exists' : {get:{admin:true,user:true,guest:true}},
`;
}
return str;
/* iterate over has many relations */
}
_renderXcBelongsToRoutePermissions(args) {
let str = '';
//
let btRelations = args.relations
? args.relations.filter((r) => r.tn === args.tn)
: [];
if (btRelations.length > 1)
btRelations = uniqBy(btRelations, function (e) {
return [e.tn, e.rtn].join();
});
for (let i = 0; i < btRelations.length; ++i) {
str += `'/api/${args.routeVersionLetter}1/${args.tn}/belongs/:parents' : {get:{admin:true,user:true,guest:true}},`;
}
return str;
}
getObject(): Acl {
return {
creator: {
read: true,
...(this.ctx.type !== 'view'
? {
create: true,
update: true,
delete: true,
}
: {}),
},
editor: {
read: true,
...(this.ctx.type !== 'view'
? {
create: true,
update: true,
delete: true,
}
: {}),
},
commenter: {
read: true,
...(this.ctx.type !== 'view'
? {
create: false,
update: false,
delete: false,
}
: {}),
},
viewer: {
read: true,
...(this.ctx.type !== 'view'
? {
create: false,
update: false,
delete: false,
}
: {}),
},
guest: {
read: false,
...(this.ctx.type !== 'view'
? {
create: false,
update: false,
delete: false,
}
: {}),
},
};
}
}
export default ExpressXcMiddleware;

5
packages/nocodb-nest/src/db/sql-mgr/v2/ProjectMgrv2.ts

@ -1,6 +1,6 @@
import SqlMgrv2 from './SqlMgrv2';
import SqlMgrv2Trans from './SqlMgrv2Trans';
import type NcMetaIO from '../../../meta/NcMetaIO';
// import type NcMetaIO from '../../../meta/NcMetaIO';
import type Base from '../../../models/Base';
export default class ProjectMgrv2 {
@ -17,7 +17,8 @@ export default class ProjectMgrv2 {
public static async getSqlMgrTrans(
project: { id: string },
ncMeta: NcMetaIO,
// todo: tobe changed
ncMeta: any,
base: Base
): Promise<SqlMgrv2Trans> {
const sqlMgr = new SqlMgrv2Trans(project, ncMeta, base);

9
packages/nocodb-nest/src/db/sql-mgr/v2/SqlMgrv2Trans.ts

@ -3,12 +3,12 @@ import KnexMigratorv2Tans from '../../sql-migrator/lib/KnexMigratorv2Tans';
import SqlMgrv2 from './SqlMgrv2';
import type Base from '../../../models/Base';
import type { Knex } from 'knex';
import type { XKnex } from '../../sql-data-mapper';
import type NcMetaIO from '../../../meta/NcMetaIO';
import type { XKnex } from '../../CustomKnex';
export default class SqlMgrv2Trans extends SqlMgrv2 {
protected trx: Knex.Transaction;
protected ncMeta: NcMetaIO;
// todo: tobe changed
protected ncMeta: any // NcMetaIO;
protected projectId: string;
protected base: Base;
@ -18,7 +18,8 @@ export default class SqlMgrv2Trans extends SqlMgrv2 {
* @param {String} args.toolDbPath - path to sqlite file that sql mgr will use
* @memberof SqlMgr
*/
constructor(args: { id: string }, ncMeta: NcMetaIO, base: Base) {
// todo: tobe changed
constructor(args: { id: string }, ncMeta: any, base: Base) {
super(args);
this.projectId = args.id;
this.ncMeta = ncMeta;

2
packages/nocodb-nest/src/db/sql-migrator/lib/KnexMigratorv2.ts

@ -11,7 +11,7 @@ import Project from '../../../models/Project';
import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2';
import Result from '../../util/Result';
import type Base from '../../../models/Base';
import type { XKnex } from '../../sql-data-mapper';
import type { XKnex } from '../../CustomKnex';
import type { Knex } from 'knex';
const evt = new Emit();

6
packages/nocodb-nest/src/db/sql-migrator/lib/KnexMigratorv2Tans.ts

@ -8,8 +8,7 @@ import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2';
import Noco from '../../../Noco';
import KnexMigratorv2 from './KnexMigratorv2';
import type Base from '../../../models/Base';
import type { XKnex } from '../../sql-data-mapper';
import type NcMetaIO from '../../../meta/NcMetaIO';
import type { XKnex } from '../../CustomKnex';
import type MssqlClient from '../../sql-client/lib/mssql/MssqlClient';
import type MysqlClient from '../../sql-client/lib/mysql/MysqlClient';
import type OracleClient from '../../sql-client/lib/oracle/OracleClient';
@ -19,7 +18,8 @@ import type SqliteClient from '../../sql-client/lib/sqlite/SqliteClient';
export default class KnexMigratorv2Tans extends KnexMigratorv2 {
protected sqlClient: any;
protected ncMeta: NcMetaIO;
// todo: tobe changed
protected ncMeta: any // NcMetaIO;
constructor(project: { id: string }, sqlClient = null, ncMeta = Noco.ncMeta) {
super(project);

67
packages/nocodb-nest/src/helpers/NcPluginMgrv2.ts

@ -1,27 +1,27 @@
import { PluginCategory } from 'nocodb-sdk';
import BackblazePluginConfig from '../../plugins/backblaze';
import DiscordPluginConfig from '../../plugins/discord';
import GcsPluginConfig from '../../plugins/gcs';
import LinodePluginConfig from '../../plugins/linode';
import MattermostPluginConfig from '../../plugins/mattermost';
import MinioPluginConfig from '../../plugins/mino';
import OvhCloudPluginConfig from '../../plugins/ovhCloud';
import S3PluginConfig from '../../plugins/s3';
import ScalewayPluginConfig from '../../plugins/scaleway';
import SlackPluginConfig from '../../plugins/slack';
import SMTPPluginConfig from '../../plugins/smtp';
import MailerSendConfig from '../../plugins/mailerSend';
import SpacesPluginConfig from '../../plugins/spaces';
import TeamsPluginConfig from '../../plugins/teams';
import TwilioPluginConfig from '../../plugins/twilio';
import TwilioWhatsappPluginConfig from '../../plugins/twilioWhatsapp';
import UpcloudPluginConfig from '../../plugins/upcloud';
import VultrPluginConfig from '../../plugins/vultr';
import SESPluginConfig from '../../plugins/ses';
import Noco from '../../Noco';
import Local from '../../v1-legacy/plugins/adapters/storage/Local';
import { MetaTable } from '../../utils/globals';
import Plugin from '../../models/Plugin';
import BackblazePluginConfig from '../plugins/backblaze';
import DiscordPluginConfig from '../plugins/discord';
import GcsPluginConfig from '../plugins/gcs';
import LinodePluginConfig from '../plugins/linode';
import MattermostPluginConfig from '../plugins/mattermost';
import MinioPluginConfig from '../plugins/mino';
import OvhCloudPluginConfig from '../plugins/ovhCloud';
import S3PluginConfig from '../plugins/s3';
import ScalewayPluginConfig from '../plugins/scaleway';
import SlackPluginConfig from '../plugins/slack';
import SMTPPluginConfig from '../plugins/smtp';
import MailerSendConfig from '../plugins/mailerSend';
import SpacesPluginConfig from '../plugins/spaces';
import TeamsPluginConfig from '../plugins/teams';
import TwilioPluginConfig from '../plugins/twilio';
import TwilioWhatsappPluginConfig from '../plugins/twilioWhatsapp';
import UpcloudPluginConfig from '../plugins/upcloud';
import VultrPluginConfig from '../plugins/vultr';
import SESPluginConfig from '../plugins/ses';
import Noco from '../Noco';
import Local from '../plugins/storage/Local';
import { MetaTable } from '../utils/globals';
import Plugin from '../models/Plugin';
import { NcError } from './catchError';
import type {
IEmailAdapter,
@ -95,7 +95,7 @@ class NcPluginMgrv2 {
category: plugin.category,
input_schema: JSON.stringify(plugin.inputs),
},
pluginConfig.id
pluginConfig.id,
);
}
}
@ -150,7 +150,7 @@ class NcPluginMgrv2 {
}
public static async storageAdapter(
ncMeta = Noco.ncMeta
ncMeta = Noco.ncMeta,
): Promise<IStorageAdapterV2> {
const pluginData = await ncMeta.metaGet2(null, null, MetaTable.PLUGIN, {
category: PluginCategory.STORAGE,
@ -161,7 +161,7 @@ class NcPluginMgrv2 {
const pluginConfig = defaultPlugins.find(
(c) =>
c.title === pluginData.title && c.category === PluginCategory.STORAGE
c.title === pluginData.title && c.category === PluginCategory.STORAGE,
);
const plugin = new pluginConfig.builder(ncMeta, pluginData);
@ -175,7 +175,7 @@ class NcPluginMgrv2 {
public static async emailAdapter(
isUserInvite = true,
ncMeta = Noco.ncMeta
ncMeta = Noco.ncMeta,
): Promise<IEmailAdapter> {
const pluginData = await ncMeta.metaGet2(null, null, MetaTable.PLUGIN, {
category: PluginCategory.EMAIL,
@ -190,7 +190,8 @@ class NcPluginMgrv2 {
}
const pluginConfig = defaultPlugins.find(
(c) => c.title === pluginData.title && c.category === PluginCategory.EMAIL
(c) =>
c.title === pluginData.title && c.category === PluginCategory.EMAIL,
);
const plugin = new pluginConfig.builder(ncMeta, pluginData);
@ -204,7 +205,7 @@ class NcPluginMgrv2 {
public static async webhookNotificationAdapters(
title: string,
ncMeta = Noco.ncMeta
ncMeta = Noco.ncMeta,
): Promise<IWebhookNotificationAdapter> {
const pluginData = await ncMeta.metaGet2(null, null, MetaTable.PLUGIN, {
title,
@ -214,7 +215,7 @@ class NcPluginMgrv2 {
if (!pluginData) throw new Error('Plugin not configured / active');
const pluginConfig = defaultPlugins.find(
(c) => c.title === pluginData.title
(c) => c.title === pluginData.title,
);
const plugin = new pluginConfig.builder(ncMeta, pluginData);
@ -242,7 +243,7 @@ class NcPluginMgrv2 {
case 'Storage':
{
const plugin = defaultPlugins.find(
(pluginConfig) => pluginConfig?.title === args.title
(pluginConfig) => pluginConfig?.title === args.title,
);
const tempPlugin = new plugin.builder(Noco.ncMeta, plugin);
await tempPlugin.init(args?.input);
@ -256,7 +257,7 @@ class NcPluginMgrv2 {
case 'Email':
{
const plugin = defaultPlugins.find(
(pluginConfig) => pluginConfig?.title === args.title
(pluginConfig) => pluginConfig?.title === args.title,
);
const tempPlugin = new plugin.builder(Noco.ncMeta, plugin);
await tempPlugin.init(args?.input);
@ -269,7 +270,7 @@ class NcPluginMgrv2 {
break;
default: {
const plugin = defaultPlugins.find(
(pluginConfig) => pluginConfig?.title === args.title
(pluginConfig) => pluginConfig?.title === args.title,
);
const tempPlugin = new plugin.builder(Noco.ncMeta, plugin);
await tempPlugin.init(args?.input);

2
packages/nocodb-nest/src/helpers/addFormulaErrorIfMissingColumn.ts

@ -1,5 +1,5 @@
import jsep from 'jsep';
import type FormulaColumn from '../../models/FormulaColumn';
import type FormulaColumn from '../models/FormulaColumn';
export default function addFormulaErrorIfMissingColumn({
formula,

61
packages/nocodb-nest/src/helpers/apiHelpers.ts

@ -0,0 +1,61 @@
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
// @ts-ignore
import swagger from '../../../../schema/swagger.json';
import { NcError } from '../../helpers/catchError';
import type { ErrorObject } from 'ajv';
import type { NextFunction, Request, Response } from 'express';
export function parseHrtimeToSeconds(hrtime) {
const seconds = (hrtime[0] + hrtime[1] / 1e6).toFixed(3);
return seconds;
}
const ajv = new Ajv({ strictSchema: false, strict: false }); // Initialize AJV
ajv.addSchema(swagger, 'swagger.json');
addFormats(ajv);
// A middleware generator to validate the request body
export const getAjvValidatorMw = (schema) => {
return (req: Request, res: Response, next: NextFunction) => {
// Validate the request body against the schema
const valid = ajv.validate(
typeof schema === 'string' ? { $ref: schema } : schema,
req.body
);
// If the request body is valid, call the next middleware
if (valid) {
next();
} else {
const errors: ErrorObject[] | null | undefined = ajv.errors;
// If the request body is invalid, send a response with an error message
res.status(400).json({
message: 'Invalid request body',
errors,
});
}
};
};
// a function to validate the payload against the schema
export const validatePayload = (schema, payload) => {
// Validate the request body against the schema
const valid = ajv.validate(
typeof schema === 'string' ? { $ref: schema } : schema,
payload
);
// If the request body is not valid, throw error
if (!valid) {
const errors: ErrorObject[] | null | undefined = ajv.errors;
// If the request body is invalid, throw error with error message and errors
NcError.ajvValidationError({
message: 'Invalid request body',
errors,
});
}
};

13
packages/nocodb-nest/src/helpers/cacheHelpers.ts

@ -0,0 +1,13 @@
// return a middleware to set cache-control header
// default period is 30 days
export const getCacheMiddleware = (period: string | number = 2592000) => {
return async (req, res, next) => {
const { method } = req;
// only cache GET requests
if (method === 'GET') {
// set cache-control header
res.set('Cache-Control', `public, max-age=${period}`);
}
next();
};
};

208
packages/nocodb-nest/src/helpers/columnHelpers.ts

@ -0,0 +1,208 @@
import { customAlphabet } from 'nanoid';
import { UITypes } from 'nocodb-sdk';
import Column from '../../../models/Column';
import { getUniqueColumnAliasName } from '../../helpers/getUniqueName';
import validateParams from '../../helpers/validateParams';
import type {
BoolType,
ColumnReqType,
LinkToAnotherRecordType,
LookupColumnReqType,
RelationTypes,
RollupColumnReqType,
TableType,
} from 'nocodb-sdk';
import type LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn';
import type LookupColumn from '../../../models/LookupColumn';
import type Model from '../../../models/Model';
export const randomID = customAlphabet(
'1234567890abcdefghijklmnopqrstuvwxyz_',
10
);
export async function createHmAndBtColumn(
child: Model,
parent: Model,
childColumn: Column,
type?: RelationTypes,
alias?: string,
fkColName?: string,
virtual: BoolType = false,
isSystemCol = false
) {
// save bt column
{
const title = getUniqueColumnAliasName(
await child.getColumns(),
type === 'bt' ? alias : `${parent.title}`
);
await Column.insert<LinkToAnotherRecordColumn>({
title,
fk_model_id: child.id,
// ref_db_alias
uidt: UITypes.LinkToAnotherRecord,
type: 'bt',
// db_type:
fk_child_column_id: childColumn.id,
fk_parent_column_id: parent.primaryKey.id,
fk_related_model_id: parent.id,
virtual,
system: isSystemCol,
fk_col_name: fkColName,
fk_index_name: fkColName,
});
}
// save hm column
{
const title = getUniqueColumnAliasName(
await parent.getColumns(),
type === 'hm' ? alias : `${child.title} List`
);
await Column.insert({
title,
fk_model_id: parent.id,
uidt: UITypes.LinkToAnotherRecord,
type: 'hm',
fk_child_column_id: childColumn.id,
fk_parent_column_id: parent.primaryKey.id,
fk_related_model_id: child.id,
virtual,
system: isSystemCol,
fk_col_name: fkColName,
fk_index_name: fkColName,
});
}
}
export async function validateRollupPayload(payload: ColumnReqType | Column) {
validateParams(
[
'title',
'fk_relation_column_id',
'fk_rollup_column_id',
'rollup_function',
],
payload
);
const relation = await (
await Column.get({
colId: (payload as RollupColumnReqType).fk_relation_column_id,
})
).getColOptions<LinkToAnotherRecordType>();
if (!relation) {
throw new Error('Relation column not found');
}
let relatedColumn: Column;
switch (relation.type) {
case 'hm':
relatedColumn = await Column.get({
colId: relation.fk_child_column_id,
});
break;
case 'mm':
case 'bt':
relatedColumn = await Column.get({
colId: relation.fk_parent_column_id,
});
break;
}
const relatedTable = await relatedColumn.getModel();
if (
!(await relatedTable.getColumns()).find(
(c) => c.id === (payload as RollupColumnReqType).fk_rollup_column_id
)
)
throw new Error('Rollup column not found in related table');
}
export async function validateLookupPayload(
payload: ColumnReqType,
columnId?: string
) {
validateParams(
['title', 'fk_relation_column_id', 'fk_lookup_column_id'],
payload
);
// check for circular reference
if (columnId) {
let lkCol: LookupColumn | LookupColumnReqType =
payload as LookupColumnReqType;
while (lkCol) {
// check if lookup column is same as column itself
if (columnId === lkCol.fk_lookup_column_id)
throw new Error('Circular lookup reference not allowed');
lkCol = await Column.get({ colId: lkCol.fk_lookup_column_id }).then(
(c: Column) => {
if (c.uidt === 'Lookup') {
return c.getColOptions<LookupColumn>();
}
return null;
}
);
}
}
const relation = await (
await Column.get({
colId: (payload as LookupColumnReqType).fk_relation_column_id,
})
).getColOptions<LinkToAnotherRecordType>();
if (!relation) {
throw new Error('Relation column not found');
}
let relatedColumn: Column;
switch (relation.type) {
case 'hm':
relatedColumn = await Column.get({
colId: relation.fk_child_column_id,
});
break;
case 'mm':
case 'bt':
relatedColumn = await Column.get({
colId: relation.fk_parent_column_id,
});
break;
}
const relatedTable = await relatedColumn.getModel();
if (
!(await relatedTable.getColumns()).find(
(c) => c.id === (payload as LookupColumnReqType).fk_lookup_column_id
)
)
throw new Error('Lookup column not found in related table');
}
export const validateRequiredField = (
payload: Record<string, any>,
requiredProps: string[]
) => {
return requiredProps.every(
(prop) =>
prop in payload && payload[prop] !== undefined && payload[prop] !== null
);
};
// generate unique foreign key constraint name for foreign key
export const generateFkName = (parent: TableType, child: TableType) => {
// generate a unique constraint name by taking first 10 chars of parent and child table name (by replacing all non word chars with _)
// and appending a random string of 15 chars maximum length.
// In database constraint name can be upto 64 chars and here we are generating a name of maximum 40 chars
const constraintName = `fk_${parent.table_name
.replace(/\W+/g, '_')
.slice(0, 10)}_${child.table_name
.replace(/\W+/g, '_')
.slice(0, 10)}_${randomID(15)}`;
return constraintName;
};

137
packages/nocodb-nest/src/helpers/convertUnits.ts

@ -0,0 +1,137 @@
export function convertUnits(
unit: string,
type: 'mysql' | 'mssql' | 'pg' | 'sqlite'
) {
switch (unit) {
case 'milliseconds':
case 'ms': {
switch (type) {
case 'mssql':
return 'millisecond';
case 'mysql':
// MySQL doesn't support millisecond
// hence change from MICROSECOND to millisecond manually
return 'MICROSECOND';
case 'pg':
case 'sqlite':
return 'milliseconds';
default:
return unit;
}
}
case 'seconds':
case 's': {
switch (type) {
case 'mssql':
case 'pg':
return 'second';
case 'mysql':
return 'SECOND';
case 'sqlite':
return 'seconds';
default:
return unit;
}
}
case 'minutes':
case 'm': {
switch (type) {
case 'mssql':
case 'pg':
return 'minute';
case 'mysql':
return 'MINUTE';
case 'sqlite':
return 'minutes';
default:
return unit;
}
}
case 'hours':
case 'h': {
switch (type) {
case 'mssql':
case 'pg':
return 'hour';
case 'mysql':
return 'HOUR';
case 'sqlite':
return 'hours';
default:
return unit;
}
}
case 'days':
case 'd': {
switch (type) {
case 'mssql':
case 'pg':
return 'day';
case 'mysql':
return 'DAY';
case 'sqlite':
return 'days';
default:
return unit;
}
}
case 'weeks':
case 'w': {
switch (type) {
case 'mssql':
case 'pg':
return 'week';
case 'mysql':
return 'WEEK';
case 'sqlite':
return 'weeks';
default:
return unit;
}
}
case 'months':
case 'M': {
switch (type) {
case 'mssql':
case 'pg':
return 'month';
case 'mysql':
return 'MONTH';
case 'sqlite':
return 'months';
default:
return unit;
}
}
case 'quarters':
case 'Q': {
switch (type) {
case 'mssql':
case 'pg':
return 'quarter';
case 'mysql':
return 'QUARTER';
case 'sqlite':
return 'quarters';
default:
return unit;
}
}
case 'years':
case 'y': {
switch (type) {
case 'mssql':
case 'pg':
return 'year';
case 'mysql':
return 'YEAR';
case 'sqlite':
return 'years';
default:
return unit;
}
}
default:
return unit;
}
}

31
packages/nocodb-nest/src/helpers/extractProjectIdAndAuthenticate.ts

@ -1,15 +1,18 @@
import { promisify } from 'util';
import passport from 'passport';
import Model from '../../models/Model';
import View from '../../models/View';
import Hook from '../../models/Hook';
import GridViewColumn from '../../models/GridViewColumn';
import FormViewColumn from '../../models/FormViewColumn';
import GalleryViewColumn from '../../models/GalleryViewColumn';
import Project from '../../models/Project';
import Column from '../../models/Column';
import Filter from '../../models/Filter';
import Sort from '../../models/Sort';
import {
Model,
View,
Hook,
GridViewColumn,
FormViewColumn,
GalleryViewColumn,
Project,
Column,
Filter,
Sort,
} from '../models';
export default async (req, res, next) => {
try {
@ -48,7 +51,7 @@ export default async (req, res, next) => {
params.formViewId ||
params.gridViewId ||
params.kanbanViewId ||
params.galleryViewId
params.galleryViewId,
);
req.ncProjectId = view?.project_id;
} else if (params.publicDataUuid) {
@ -65,7 +68,7 @@ export default async (req, res, next) => {
req.ncProjectId = formViewColumn?.project_id;
} else if (params.galleryViewColumnId) {
const galleryViewColumn = await GalleryViewColumn.get(
params.galleryViewColumnId
params.galleryViewColumnId,
);
req.ncProjectId = galleryViewColumn?.project_id;
} else if (params.columnId) {
@ -106,7 +109,7 @@ export default async (req, res, next) => {
{
session: false,
optional: false,
},
} as any,
(_err, user, _info) => {
// if (_err) return reject(_err);
if (user) {
@ -118,7 +121,7 @@ export default async (req, res, next) => {
} else {
resolve({ roles: 'guest' });
}
}
},
)(req, res, next);
} else if (req.headers['xc-shared-base-id']) {
passport.authenticate('baseView', {}, (_err, user, _info) => {

54
packages/nocodb-nest/src/helpers/formulaFnHelper.ts

@ -0,0 +1,54 @@
import dayjs from 'dayjs';
// todo: tobe fixed
// import customParseFormat from 'dayjs/plugin/customParseFormat';
// extend(customParseFormat);
export function getWeekdayByText(v: string) {
return {
monday: 0,
tuesday: 1,
wednesday: 2,
thursday: 3,
friday: 4,
saturday: 5,
sunday: 6,
}[v?.toLowerCase() || 'monday'];
}
export function getWeekdayByIndex(idx: number): string {
return {
0: 'monday',
1: 'tuesday',
2: 'wednesday',
3: 'thursday',
4: 'friday',
5: 'saturday',
6: 'sunday',
}[idx || 0];
}
export function validateDateWithUnknownFormat(v: string) {
const dateFormats = [
'DD-MM-YYYY',
'MM-DD-YYYY',
'YYYY-MM-DD',
'DD/MM/YYYY',
'MM/DD/YYYY',
'YYYY/MM/DD',
'DD MM YYYY',
'MM DD YYYY',
'YYYY MM DD',
];
for (const format of dateFormats) {
if (dayjs(v, format, true).isValid() as any) {
return true;
}
for (const timeFormat of ['HH:mm', 'HH:mm:ss', 'HH:mm:ss.SSS']) {
if (dayjs(v, `${format} ${timeFormat}`, true).isValid() as any) {
return true;
}
}
}
return false;
}

216
packages/nocodb-nest/src/helpers/getAst.ts

@ -0,0 +1,216 @@
import { isSystemColumn, RelationTypes, UITypes } from 'nocodb-sdk';
import { View } from '../models';
import type {
Column,
LinkToAnotherRecordColumn,
LookupColumn,
Model,
} from '../models';
const getAst = async ({
query,
extractOnlyPrimaries = false,
includePkByDefault = true,
model,
view,
dependencyFields = {
...(query || {}),
nested: { ...(query?.nested || {}) },
fieldsSet: new Set(),
},
}: {
query?: RequestQuery;
extractOnlyPrimaries?: boolean;
includePkByDefault?: boolean;
model: Model;
view?: View;
dependencyFields?: DependantFields;
}) => {
// set default values of dependencyFields and nested
dependencyFields.nested = dependencyFields.nested || {};
dependencyFields.fieldsSet = dependencyFields.fieldsSet || new Set();
if (!model.columns?.length) await model.getColumns();
// extract only pk and pv
if (extractOnlyPrimaries) {
const ast = {
...(model.primaryKeys
? model.primaryKeys.reduce((o, pk) => ({ ...o, [pk.title]: 1 }), {})
: {}),
...(model.displayValue ? { [model.displayValue.title]: 1 } : {}),
};
await Promise.all(
model.primaryKeys.map((c) => extractDependencies(c, dependencyFields))
);
await extractDependencies(model.displayValue, dependencyFields);
return { ast, dependencyFields };
}
let fields = query?.fields || query?.f;
if (fields && fields !== '*') {
fields = Array.isArray(fields) ? fields : fields.split(',');
} else {
fields = null;
}
let allowedCols = null;
if (view)
allowedCols = (await View.getColumns(view.id)).reduce(
(o, c) => ({
...o,
[c.fk_column_id]: c.show,
}),
{}
);
const ast = await model.columns.reduce(async (obj, col: Column) => {
let value: number | boolean | { [key: string]: any } = 1;
const nestedFields =
query?.nested?.[col.title]?.fields || query?.nested?.[col.title]?.f;
if (nestedFields && nestedFields !== '*') {
if (col.uidt === UITypes.LinkToAnotherRecord) {
const model = await col
.getColOptions<LinkToAnotherRecordColumn>()
.then((colOpt) => colOpt.getRelatedTable());
const { ast } = await getAst({
model,
query: query?.nested?.[col.title],
dependencyFields: (dependencyFields.nested[col.title] =
dependencyFields.nested[col.title] || {
nested: {},
fieldsSet: new Set(),
}),
});
value = ast;
// todo: include field relative to the relation => pk / fk
} else {
value = (Array.isArray(fields) ? fields : fields.split(',')).reduce(
(o, f) => ({ ...o, [f]: 1 }),
{}
);
}
} else if (col.uidt === UITypes.LinkToAnotherRecord) {
const model = await col
.getColOptions<LinkToAnotherRecordColumn>()
.then((colOpt) => colOpt.getRelatedTable());
value = (
await getAst({
model,
query: query?.nested?.[col.title],
extractOnlyPrimaries: nestedFields !== '*',
dependencyFields: (dependencyFields.nested[col.title] =
dependencyFields.nested[col.title] || {
nested: {},
fieldsSet: new Set(),
}),
})
).ast;
}
const isRequested =
allowedCols && (!includePkByDefault || !col.pk)
? allowedCols[col.id] &&
(!isSystemColumn(col) || view.show_system_fields) &&
(!fields?.length || fields.includes(col.title)) &&
value
: fields?.length
? fields.includes(col.title) && value
: value;
if (isRequested || col.pk) await extractDependencies(col, dependencyFields);
return {
...(await obj),
[col.title]: isRequested,
};
}, Promise.resolve({}));
return { ast, dependencyFields };
};
const extractDependencies = async (
column: Column,
dependencyFields: DependantFields = {
nested: {},
fieldsSet: new Set(),
}
) => {
switch (column.uidt) {
case UITypes.Lookup:
await extractLookupDependencies(column, dependencyFields);
break;
case UITypes.LinkToAnotherRecord:
await extractRelationDependencies(column, dependencyFields);
break;
default:
dependencyFields.fieldsSet.add(column.title);
break;
}
};
const extractLookupDependencies = async (
lookUpColumn: Column<LookupColumn>,
dependencyFields: DependantFields = {
nested: {},
fieldsSet: new Set(),
}
) => {
const lookupColumnOpts = await lookUpColumn.getColOptions();
const relationColumn = await lookupColumnOpts.getRelationColumn();
await extractRelationDependencies(relationColumn, dependencyFields);
await extractDependencies(
await lookupColumnOpts.getLookupColumn(),
(dependencyFields.nested[relationColumn.title] = dependencyFields.nested[
relationColumn.title
] || {
nested: {},
fieldsSet: new Set(),
})
);
};
const extractRelationDependencies = async (
relationColumn: Column<LinkToAnotherRecordColumn>,
dependencyFields: DependantFields = {
nested: {},
fieldsSet: new Set(),
}
) => {
const relationColumnOpts = await relationColumn.getColOptions();
switch (relationColumnOpts.type) {
case RelationTypes.HAS_MANY:
dependencyFields.fieldsSet.add(
await relationColumnOpts.getParentColumn().then((col) => col.title)
);
break;
case RelationTypes.BELONGS_TO:
case RelationTypes.MANY_TO_MANY:
dependencyFields.fieldsSet.add(
await relationColumnOpts.getChildColumn().then((col) => col.title)
);
break;
}
};
type RequestQuery = {
[fields in 'f' | 'fields']?: string | string[];
} & {
nested?: {
[field: string]: RequestQuery;
};
};
interface DependantFields {
fieldsSet?: Set<string>;
nested?: DependantFields;
}
export default getAst;

4
packages/nocodb-nest/src/helpers/getColumnPropsFromUIDT.ts

@ -1,7 +1,7 @@
import { SqlUiFactory, UITypes } from 'nocodb-sdk';
import type { ColumnReqType, NormalColumnRequestType } from 'nocodb-sdk';
import type Base from '../../models/Base';
import type Column from '../../models/Column';
import type Base from '../models/Base';
import type Column from '../models/Column';
export default async function getColumnPropsFromUIDT(
column: ColumnReqType & { altered?: number },

6
packages/nocodb-nest/src/helpers/getColumnUiType.ts

@ -1,7 +1,7 @@
import ModelXcMetaFactory from '../../db/sql-mgr/code/models/xc/ModelXcMetaFactory';
import type Base from '../../models/Base';
import type Column from '../../models/Column';
import type Base from '../models/Base';
import type Column from '../models/Column';
import type { ColumnType } from 'nocodb-sdk';
import ModelXcMetaFactory from 'src/db/sql-mgr/code/models/xc/ModelXcMetaFactory';
export default function getColumnUiType(
base: Base,

2
packages/nocodb-nest/src/helpers/getHandler.ts

@ -1,4 +1,4 @@
import Noco from '../../Noco';
import Noco from '../Noco';
import type express from 'express';
export default function getHandler(

2
packages/nocodb-nest/src/helpers/getTableName.ts

@ -1,5 +1,5 @@
import inflection from 'inflection';
import type Base from '../../models/Base';
import type Base from '../models/Base';
export default function getTableNameAlias(
tableName: string,

2
packages/nocodb-nest/src/helpers/getUniqueName.ts

@ -1,4 +1,4 @@
import type Column from '../../models/Column';
import type Column from '../models/Column';
export function getUniqueColumnName(columns: Column[], initialName = 'field') {
let c = 0;

6
packages/nocodb-nest/src/helpers/index.ts

@ -0,0 +1,6 @@
import { populateMeta } from './populateMeta';
export * from './columnHelpers';
export * from './apiHelpers';
export * from './cacheHelpers';
export { populateMeta };

2
packages/nocodb-nest/src/helpers/ncMetaAclMw.ts

@ -1,5 +1,5 @@
import { OrgUserRoles } from 'nocodb-sdk';
import projectAcl from '../../utils/projectAcl';
import projectAcl from '../utils/projectAcl';
import catchError, { NcError } from './catchError';
import extractProjectIdAndAuthenticate from './extractProjectIdAndAuthenticate';
import type { NextFunction, Request, Response } from 'express';

280
packages/nocodb-nest/src/helpers/populateMeta.ts

@ -0,0 +1,280 @@
import { ModelTypes, UITypes, ViewTypes } from 'nocodb-sdk';
import Column from '../../../models/Column';
import Model from '../../../models/Model';
import NcHelp from '../../../utils/NcHelp';
import View from '../../../models/View';
import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2';
import getTableNameAlias, {
getColumnNameAlias,
} from '../../helpers/getTableName';
import getColumnUiType from '../../helpers/getColumnUiType';
import mapDefaultDisplayValue from '../../helpers/mapDefaultDisplayValue';
import { extractAndGenerateManyToManyRelations } from '../../../services/metaDiff.svc';
import { IGNORE_TABLES } from '../../../utils/common/BaseApiBuilder';
import type LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn';
import type Base from '../../../models/Base';
import type Project from '../../../models/Project';
export async function populateMeta(base: Base, project: Project): Promise<any> {
const info = {
type: 'rest',
apiCount: 0,
tablesCount: 0,
relationsCount: 0,
viewsCount: 0,
client: (await base?.getConnectionConfig())?.client,
timeTaken: 0,
};
const t = process.hrtime();
const sqlClient = await NcConnectionMgrv2.getSqlClient(base);
let order = 1;
const models2: { [tableName: string]: Model } = {};
const virtualColumnsInsert = [];
/* Get all relations */
const relations = (await sqlClient.relationListAll())?.data?.list;
info.relationsCount = relations.length;
let tables = (await sqlClient.tableList())?.data?.list
?.filter(({ tn }) => !IGNORE_TABLES.includes(tn))
?.map((t) => {
t.order = ++order;
t.title = getTableNameAlias(t.tn, project.prefix, base);
t.table_name = t.tn;
return t;
});
/* filter based on prefix */
if (base.is_meta && project?.prefix) {
tables = tables.filter((t) => {
return t?.tn?.startsWith(project?.prefix);
});
}
info.tablesCount = tables.length;
tables.forEach((t) => {
t.title = getTableNameAlias(t.tn, project.prefix, base);
});
relations.forEach((r) => {
r.title = getTableNameAlias(r.tn, project.prefix, base);
r.rtitle = getTableNameAlias(r.rtn, project.prefix, base);
});
// await this.syncRelations();
const tableMetasInsert = tables.map((table) => {
return async () => {
/* filter relation where this table is present */
const tableRelations = relations.filter(
(r) => r.tn === table.tn || r.rtn === table.tn
);
const columns: Array<
Omit<Column, 'column_name' | 'title'> & {
cn: string;
system?: boolean;
}
> = (await sqlClient.columnList({ tn: table.tn }))?.data?.list;
const hasMany =
table.type === 'view'
? []
: tableRelations.filter((r) => r.rtn === table.tn);
const belongsTo =
table.type === 'view'
? []
: tableRelations.filter((r) => r.tn === table.tn);
mapDefaultDisplayValue(columns);
// add vitual columns
const virtualColumns = [
...hasMany.map((hm) => {
return {
uidt: UITypes.LinkToAnotherRecord,
type: 'hm',
hm,
title: `${hm.title} List`,
};
}),
...belongsTo.map((bt) => {
// find and mark foreign key column
const fkColumn = columns.find((c) => c.cn === bt.cn);
if (fkColumn) {
fkColumn.uidt = UITypes.ForeignKey;
fkColumn.system = true;
}
return {
uidt: UITypes.LinkToAnotherRecord,
type: 'bt',
bt,
title: `${bt.rtitle}`,
};
}),
];
// await Model.insert(project.id, base.id, meta);
/* create nc_models and its rows if it doesn't exists */
models2[table.table_name] = await Model.insert(project.id, base.id, {
table_name: table.tn || table.table_name,
title: table.title,
type: table.type || 'table',
order: table.order,
});
// table crud apis
info.apiCount += 5;
let colOrder = 1;
for (const column of columns) {
await Column.insert({
uidt: column.uidt || getColumnUiType(base, column),
fk_model_id: models2[table.tn].id,
...column,
title: getColumnNameAlias(column.cn, base),
column_name: column.cn,
order: colOrder++,
});
}
virtualColumnsInsert.push(async () => {
const columnNames = {};
for (const column of virtualColumns) {
// generate unique name if there is any duplicate column name
let c = 0;
while (`${column.title}${c || ''}` in columnNames) {
c++;
}
column.title = `${column.title}${c || ''}`;
columnNames[column.title] = true;
const rel = column.hm || column.bt;
const rel_column_id = (await models2?.[rel.tn]?.getColumns())?.find(
(c) => c.column_name === rel.cn
)?.id;
const tnId = models2?.[rel.tn]?.id;
const ref_rel_column_id = (
await models2?.[rel.rtn]?.getColumns()
)?.find((c) => c.column_name === rel.rcn)?.id;
const rtnId = models2?.[rel.rtn]?.id;
try {
await Column.insert<LinkToAnotherRecordColumn>({
project_id: project.id,
db_alias: base.id,
fk_model_id: models2[table.tn].id,
cn: column.cn,
title: column.title,
uidt: column.uidt,
type: column.hm ? 'hm' : column.mm ? 'mm' : 'bt',
// column_id,
fk_child_column_id: rel_column_id,
fk_parent_column_id: ref_rel_column_id,
fk_index_name: rel.cstn,
ur: rel.ur,
dr: rel.dr,
order: colOrder++,
fk_related_model_id: column.hm ? tnId : rtnId,
system: column.system,
});
// nested relations data apis
info.apiCount += 5;
} catch (e) {
console.log(e);
}
}
});
};
});
/* handle xc_tables update in parallel */
await NcHelp.executeOperations(tableMetasInsert, base.type);
await NcHelp.executeOperations(virtualColumnsInsert, base.type);
await extractAndGenerateManyToManyRelations(Object.values(models2));
let views: Array<{ order: number; table_name: string; title: string }> = (
await sqlClient.viewList()
)?.data?.list
// ?.filter(({ tn }) => !IGNORE_TABLES.includes(tn))
?.map((v) => {
v.order = ++order;
v.table_name = v.view_name;
v.title = getTableNameAlias(v.view_name, project.prefix, base);
return v;
});
/* filter based on prefix */
if (base.is_meta && project?.prefix) {
views = tables.filter((t) => {
return t?.tn?.startsWith(project?.prefix);
});
}
info.viewsCount = views.length;
const viewMetasInsert = views.map((table) => {
return async () => {
const columns = (await sqlClient.columnList({ tn: table.table_name }))
?.data?.list;
mapDefaultDisplayValue(columns);
/* create nc_models and its rows if it doesn't exists */
models2[table.table_name] = await Model.insert(project.id, base.id, {
table_name: table.table_name,
title: getTableNameAlias(table.table_name, project.prefix, base),
// todo: sanitize
type: ModelTypes.VIEW,
order: table.order,
});
let colOrder = 1;
// view apis
info.apiCount += 2;
for (const column of columns) {
await Column.insert({
fk_model_id: models2[table.table_name].id,
...column,
title: getColumnNameAlias(column.cn, base),
order: colOrder++,
uidt: getColumnUiType(base, column),
});
}
};
});
await NcHelp.executeOperations(viewMetasInsert, base.type);
// fix pv column for created grid views
const models = await Model.list({ project_id: project.id, base_id: base.id });
for (const model of models) {
const views = await model.getViews();
for (const view of views) {
if (view.type === ViewTypes.GRID) {
await View.fixPVColumnForView(view.id);
}
}
}
const t1 = process.hrtime(t);
const t2 = t1[0] + t1[1] / 1000000000;
(info as any).timeTaken = t2.toFixed(1);
return info;
}

7
packages/nocodb-nest/src/helpers/populateSamplePayload.ts

@ -1,11 +1,6 @@
import { RelationTypes, UITypes } from 'nocodb-sdk';
import { v4 as uuidv4 } from 'uuid';
import View from '../../models/View';
import Column from '../../models/Column';
import Model from '../../models/Model';
import type LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn';
import type LookupColumn from '../../models/LookupColumn';
import type SelectOption from '../../models/SelectOption';
import { Column, LinkToAnotherRecordColumn, LookupColumn, Model, SelectOption, View } from '../models'
export async function populateSamplePayload(
viewOrModel: View | Model,

11
packages/nocodb-nest/src/helpers/sanitize.ts

@ -0,0 +1,11 @@
export function sanitize(v) {
if (typeof v !== 'string') return v;
return v?.replace(/([^\\]|^)(\?+)/g, (_, m1, m2) => {
return `${m1}${m2.split('?').join('\\?')}`;
});
}
export function unsanitize(v) {
if (typeof v !== 'string') return v;
return v?.replace(/\\[?]/g, '?');
}

11
packages/nocodb-nest/src/helpers/syncMigration.ts

@ -1,12 +1,11 @@
import Migrator from '../../db/sql-migrator/lib/KnexMigratorv2';
import type Project from '../../models/Project';
import type Base from '../../models/Base';
import KnexMigratorv2 from 'src/db/sql-migrator/lib/KnexMigratorv2';
import { Base, Project } from '../models';
export default async function syncMigration(project: Project): Promise<void> {
for (const base of await project.getBases()) {
try {
/* create sql-migrator */
const migrator = new Migrator(project);
const migrator = new KnexMigratorv2(project);
await migrator.init(base);
@ -23,11 +22,11 @@ export default async function syncMigration(project: Project): Promise<void> {
export async function syncBaseMigration(
project: Project,
base: Base
base: Base,
): Promise<void> {
try {
/* create sql-migrator */
const migrator = new Migrator(project);
const migrator = new KnexMigratorv2(project);
await migrator.init(base);

1
packages/nocodb-nest/src/helpers/webhookHelpers.ts

@ -2,6 +2,7 @@ import Handlebars from 'handlebars';
import { v4 as uuidv4 } from 'uuid';
import type { HookLogType } from 'nocodb-sdk';
import { Column, Filter, FormView, Hook, HookLog, Model, View } from '../models'
import NcPluginMgrv2 from './NcPluginMgrv2';
export function parseBody(template: string, data: any): string {
if (!template) {

15
packages/nocodb-nest/src/interface/IEmailAdapter.ts

@ -0,0 +1,15 @@
export default interface IEmailAdapter {
init(): Promise<any>;
mailSend(mail: XcEmail): Promise<any>;
test(email): Promise<boolean>;
}
interface XcEmail {
// from?:string;
to: string;
subject: string;
html?: string;
text?: string;
}
export { XcEmail };

17
packages/nocodb-nest/src/interface/IStorageAdapter.ts

@ -0,0 +1,17 @@
export default interface IStorageAdapter {
init(): Promise<any>;
fileCreate(destPath: string, file: XcFile): Promise<any>;
fileDelete(filePath: string): Promise<any>;
fileRead(filePath: string): Promise<any>;
test(): Promise<boolean>;
}
interface XcFile {
originalname: string;
path: string;
mimetype: string;
size: number | string;
buffer?: any;
}
export { XcFile };

8
packages/nocodb-nest/src/interface/XcDynamicChanges.ts

@ -0,0 +1,8 @@
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>;
}

2
packages/nocodb-nest/src/interface/XcMetaMgr.ts

@ -0,0 +1,2 @@
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export default interface XcMetaMgr {}

293
packages/nocodb-nest/src/interface/config.ts

@ -0,0 +1,293 @@
import type { Handler } from 'express';
import type * as e from 'express';
import type { Knex } from 'knex';
export interface Route {
path: string;
type: RouteType | string;
handler: Array<Handler | string>;
acl: {
[key: string]: boolean;
};
disabled?: boolean;
functions?: string[];
}
export enum RouteType {
GET = 'get',
POST = 'post',
PUT = 'put',
PATCH = 'patch',
DELETE = 'delete',
HEAD = 'head',
OPTIONS = 'options',
}
type InflectionTypes =
| 'pluralize'
| 'singularize'
| 'inflect'
| 'camelize'
| 'underscore'
| 'humanize'
| 'capitalize'
| 'dasherize'
| 'titleize'
| 'demodulize'
| 'tableize'
| 'classify'
| 'foreign_key'
| 'ordinalize'
| 'transform'
| 'none';
export interface DbConfig extends Knex.Config {
client: string;
connection: Knex.StaticConnectionConfig | Knex.Config | any;
meta: {
dbAlias: string;
metaTables?: 'db' | 'file';
tn?: string;
models?: {
disabled: boolean;
};
routes?: {
disabled: boolean;
};
hooks?: {
disabled: boolean;
};
migrations?: {
disabled: boolean;
name: 'nc_evolutions';
};
api: {
type: 'rest' | 'graphql' | 'grpc';
prefix: string;
swagger?: boolean;
graphiql?: boolean;
graphqlDepthLimit?: number;
};
allSchemas?: boolean;
ignoreTables?: string[];
readonly?: boolean;
query?: {
print?: boolean;
explain?: boolean;
measure?: boolean;
};
reset?: boolean;
dbtype?: 'vitess' | string;
pluralize?: boolean;
inflection?: {
tn?: InflectionTypes;
cn?: InflectionTypes;
};
};
}
// Refer : https://www.npmjs.com/package/jsonwebtoken
interface JwtOptions {
algorithm?: string;
expiresIn?: string | number;
notBefore?: string | number;
audience?: string;
issuer?: string;
jwtid?: any;
subject?: string;
noTimestamp?: any;
header?: any;
keyid?: any;
}
export interface AuthConfig {
jwt?: {
secret: string;
[key: string]: any;
dbAlias?: string;
options?: JwtOptions;
};
masterKey?: {
secret: string;
};
middleware?: {
url: string;
};
disabled?: boolean;
}
export interface MiddlewareConfig {
handler?: (...args: any[]) => any;
}
export interface ACLConfig {
roles?: string[];
defaultRoles?: string[];
}
export interface MailerConfig {
[key: string]: any;
}
export interface ServerlessConfig {
aws?: {
lambda: boolean;
};
gcp?: {
cloudFunction: boolean;
};
azure?: {
cloudFunctionApp: boolean;
};
zeit?: {
now: boolean;
};
alibaba?: {
functionCompute: boolean;
};
serverlessFramework?: {
http: boolean;
};
}
export interface NcGui {
path?: string;
disabled?: boolean;
favicon?: string;
logo?: string;
}
// @ts-ignore
export interface NcConfig {
title?: string;
version?: string;
envs: {
[key: string]: {
db: DbConfig[];
api?: any;
publicUrl?: string;
};
};
// dbs: DbConfig[];
auth?: AuthConfig;
middleware?: MiddlewareConfig[];
acl?: ACLConfig;
port?: number;
host?: string;
cluster?: number;
mailer?: MailerConfig;
make?: () => NcConfig;
serverless?: ServerlessConfig;
toolDir?: string;
env?: 'production' | 'dev' | 'test' | string;
workingEnv?: string;
seedsFolder?: string | string[];
queriesFolder?: string | string[];
apisFolder?: string | string[];
projectType?: 'rest' | 'graphql' | 'grpc';
type?: 'mvc' | 'package' | 'docker';
language?: 'ts' | 'js';
meta?: {
db?: any;
};
api?: any;
gui?: NcGui;
try?: boolean;
dashboardPath?: string;
prefix?: string;
publicUrl?: string;
}
export interface Event {
title: string;
tn: string;
url;
headers;
operation;
event;
retry;
max;
interval;
timeout;
}
export interface Acl {
[role: string]:
| {
create: boolean | ColumnAcl;
[key: string]: boolean | ColumnAcl;
}
| boolean
| any;
}
export interface ColumnAcl {
columns: {
[cn: string]: boolean;
};
assign?: {
[cn: string]: any;
};
}
export interface Acls {
[tn: string]: Acl;
}
export enum ServerlessType {
AWS_LAMBDA = 'AWS_LAMBDA',
GCP_FUNCTION = 'GCP_FUNCTION',
AZURE_FUNCTION_APP = 'AZURE_FUNCTION_APP',
ALIYUN = 'ALIYUN',
ZEIT = 'ZEIT',
LYRID = 'LYRID',
SERVERLESS = 'SERVERLESS',
}
export class Result {
public code: any;
public message: string;
public data: any;
constructor(code = 0, message = '', data = {}) {
this.code = code;
this.message = message;
this.data = data;
}
}
enum HTTPType {
GET = 'get',
POST = 'post',
PUT = 'put',
DELETE = 'delete',
PATCH = 'patch',
HEAD = 'head',
OPTIONS = 'options',
}
export interface XcRoute {
httpType: HTTPType;
path: string;
handler: e.Handler;
dbAlias?: string;
isCustom?: boolean;
}

746
packages/nocodb-nest/src/meta/meta.service.ts

@ -1,8 +1,19 @@
import { Global, Inject, Injectable, OnApplicationBootstrap, OnModuleInit } from '@nestjs/common'
import { Knex } from 'knex'
import XcMigrationSource from './migrations/XcMigrationSource';
import XcMigrationSourcev2 from './migrations/XcMigrationSourcev2';
import { Connection } from '../connection/connection';
import { customAlphabet } from 'nanoid';
import Noco from 'src/Noco';
import CryptoJS from 'crypto-js';
import { XKnex } from 'src/db/CustomKnex';
import NocoCache from 'src/cache/NocoCache';
const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 4);
// todo: tobe fixed
const META_TABLES = []
// todo: move
export enum MetaTable {
@ -171,23 +182,16 @@ const nanoidv2 = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 14);
@Global()
@Injectable()
export class MetaService implements OnApplicationBootstrap {
constructor(private connection: Connection) {}
public async metaInit(): Promise<boolean> {
constructor(private metaConnection: Connection) {}
await this.connection.knexInstance.migrate.latest({
migrationSource: new XcMigrationSource(),
tableName: 'xc_knex_migrations',
});
await this.connection.knexInstance.migrate.latest({
migrationSource: new XcMigrationSourcev2(),
tableName: 'xc_knex_migrationsv2',
});
return true;
public get connection() {
return this.metaConnection.knexInstance;
}
get knexConnection() {
return this.connection.knexInstance;
return this.connection;
}
public async metaGet(
@ -198,7 +202,7 @@ export class MetaService implements OnApplicationBootstrap {
fields?: string[],
// xcCondition?
): Promise<any> {
const query = this.connection.knexInstance(target);
const query = this.connection(target);
// if (xcCondition) {
// query.condition(xcCondition);
@ -345,6 +349,720 @@ export class MetaService implements OnApplicationBootstrap {
async onApplicationBootstrap(): Promise<void> {
await this.metaInit();
// todo: tobe fixed - temporary workaround
Noco._ncMeta = this;
}
//
public async metaPaginatedList(
projectId: string,
dbAlias: string,
target: string,
args?: {
condition?: { [key: string]: any };
limit?: number;
offset?: number;
xcCondition?;
fields?: string[];
sort?: { field: string; desc?: boolean };
}
): Promise<{ list: any[]; count: number }> {
const query = this.knexConnection(target);
const countQuery = this.knexConnection(target);
if (projectId !== null && projectId !== undefined) {
query.where('project_id', projectId);
countQuery.where('project_id', projectId);
}
if (dbAlias !== null && dbAlias !== undefined) {
query.where('db_alias', dbAlias);
countQuery.where('db_alias', dbAlias);
}
if (args?.condition) {
query.where(args.condition);
countQuery.where(args.condition);
}
if (args?.limit) {
query.limit(args.limit);
}
if (args?.sort) {
query.orderBy(args.sort.field, args.sort.desc ? 'desc' : 'asc');
}
if (args?.offset) {
query.offset(args.offset);
}
if (args?.xcCondition) {
(query as any)
.condition(args.xcCondition)(countQuery as any)
.condition(args.xcCondition);
}
if (args?.fields?.length) {
query.select(...args.fields);
}
return {
list: await query,
count: Object.values(await countQuery.count().first())?.[0] as any,
};
}
// private connection: XKnex;
// todo: need to fix
private trx: Knex.Transaction;
// constructor(app: Noco, config: NcConfig, trx = null) {
// super(app, config);
//
// if (this.config?.meta?.db) {
// this.connection = trx || XKnex(this.config?.meta?.db);
// } else {
// let dbIndex = this.config.envs?.[this.config.workingEnv]?.db.findIndex(
// (c) => c.meta.dbAlias === this.config?.auth?.jwt?.dbAlias
// );
// dbIndex = dbIndex === -1 ? 0 : dbIndex;
// this.connection = XKnex(
// this.config.envs?.[this.config.workingEnv]?.db[dbIndex] as any
// );
// }
// this.trx = trx;
// NcConnectionMgr.setXcMeta(this);
// }
// public get knexConnection(): XKnex {
// return (this.trx || this.connection) as any;
// }
// public updateKnex(connectionConfig): void {
// this.connection = XKnex(connectionConfig);
// }
// public async metaInit(): Promise<boolean> {
// await this.connection.migrate.latest({
// migrationSource: new XcMigrationSource(),
// tableName: 'xc_knex_migrations',
// });
// await this.connection.migrate.latest({
// migrationSource: new XcMigrationSourcev2(),
// tableName: 'xc_knex_migrationsv2',
// });
// return true;
// }
public async metaDelete(
project_id: string,
dbAlias: string,
target: string,
idOrCondition: string | { [p: string]: any },
xcCondition?
): Promise<void> {
const query = this.knexConnection(target);
if (project_id !== null && project_id !== undefined) {
query.where('project_id', project_id);
}
if (dbAlias !== null && dbAlias !== undefined) {
query.where('db_alias', dbAlias);
}
if (typeof idOrCondition !== 'object') {
query.where('id', idOrCondition);
} else if (idOrCondition) {
query.where(idOrCondition);
}
if (xcCondition) {
query.condition(xcCondition, {});
}
return query.del();
}
public async metaGet2(
project_id: string,
baseId: string,
target: string,
idOrCondition: string | { [p: string]: any },
fields?: string[],
xcCondition?
): Promise<any> {
const query = this.knexConnection(target);
if (xcCondition) {
query.condition(xcCondition);
}
if (fields?.length) {
query.select(...fields);
}
if (project_id !== null && project_id !== undefined) {
query.where('project_id', project_id);
}
if (baseId !== null && baseId !== undefined) {
query.where('base_id', baseId);
}
if (!idOrCondition) {
return query.first();
}
if (typeof idOrCondition !== 'object') {
query.where('id', idOrCondition);
} else {
query.where(idOrCondition);
}
return query.first();
}
public async metaGetNextOrder(
target: string,
condition: { [key: string]: any }
): Promise<number> {
const query = this.knexConnection(target);
query.where(condition);
query.max('order', { as: 'order' });
return (+(await query.first())?.order || 0) + 1;
}
public async metaInsert(
project_id: string,
dbAlias: string,
target: string,
data: any
): Promise<any> {
return this.knexConnection(target).insert({
db_alias: dbAlias,
project_id,
created_at: this.knexConnection?.fn?.now(),
updated_at: this.knexConnection?.fn?.now(),
...data,
});
}
public async metaList(
project_id: string,
_dbAlias: string,
target: string,
args?: {
condition?: { [p: string]: any };
limit?: number;
offset?: number;
xcCondition?;
fields?: string[];
orderBy?: { [key: string]: 'asc' | 'desc' };
}
): Promise<any[]> {
const query = this.knexConnection(target);
if (project_id !== null && project_id !== undefined) {
query.where('project_id', project_id);
}
/* if (dbAlias !== null && dbAlias !== undefined) {
query.where('db_alias', dbAlias);
}*/
if (args?.condition) {
query.where(args.condition);
}
if (args?.limit) {
query.limit(args.limit);
}
if (args?.offset) {
query.offset(args.offset);
}
if (args?.xcCondition) {
(query as any).condition(args.xcCondition);
}
if (args?.orderBy) {
for (const [col, dir] of Object.entries(args.orderBy)) {
query.orderBy(col, dir);
}
}
if (args?.fields?.length) {
query.select(...args.fields);
}
return query;
}
public async metaList2(
project_id: string,
dbAlias: string,
target: string,
args?: {
condition?: { [p: string]: any };
limit?: number;
offset?: number;
xcCondition?;
fields?: string[];
orderBy: { [key: string]: 'asc' | 'desc' };
}
): Promise<any[]> {
const query = this.knexConnection(target);
if (project_id !== null && project_id !== undefined) {
query.where('project_id', project_id);
}
if (dbAlias !== null && dbAlias !== undefined) {
query.where('base_id', dbAlias);
}
if (args?.condition) {
query.where(args.condition);
}
if (args?.limit) {
query.limit(args.limit);
}
if (args?.offset) {
query.offset(args.offset);
}
if (args?.xcCondition) {
(query as any).condition(args.xcCondition);
}
if (args?.orderBy) {
for (const [col, dir] of Object.entries(args.orderBy)) {
query.orderBy(col, dir);
}
}
if (args?.fields?.length) {
query.select(...args.fields);
}
return query;
}
public async metaCount(
project_id: string,
dbAlias: string,
target: string,
args?: {
condition?: { [p: string]: any };
xcCondition?;
aggField?: string;
}
): Promise<number> {
const query = this.knexConnection(target);
if (project_id !== null && project_id !== undefined) {
query.where('project_id', project_id);
}
if (dbAlias !== null && dbAlias !== undefined) {
query.where('base_id', dbAlias);
}
if (args?.condition) {
query.where(args.condition);
}
if (args?.xcCondition) {
(query as any).condition(args.xcCondition);
}
query.count(args?.aggField || 'id', { as: 'count' }).first();
return +(await query)?.['count'] || 0;
}
public async metaUpdate(
project_id: string,
dbAlias: string,
target: string,
data: any,
idOrCondition?: string | { [p: string]: any },
xcCondition?
): Promise<any> {
const query = this.knexConnection(target);
if (project_id !== null && project_id !== undefined) {
query.where('project_id', project_id);
}
if (dbAlias !== null && dbAlias !== undefined) {
query.where('db_alias', dbAlias);
}
delete data.created_at;
query.update({ ...data, updated_at: this.knexConnection?.fn?.now() });
if (typeof idOrCondition !== 'object') {
query.where('id', idOrCondition);
} else if (idOrCondition) {
query.where(idOrCondition);
}
if (xcCondition) {
query.condition(xcCondition);
}
return await query;
}
public async metaDeleteAll(
_project_id: string,
_dbAlias: string
): Promise<void> {
// await this.knexConnection..dropTableIfExists('nc_roles').;
// await this.knexConnection.schema.dropTableIfExists('nc_store').;
// await this.knexConnection.schema.dropTableIfExists('nc_hooks').;
// await this.knexConnection.schema.dropTableIfExists('nc_cron').;
// await this.knexConnection.schema.dropTableIfExists('nc_acl').;
}
public async isMetaDataExists(
project_id: string,
dbAlias: string
): Promise<boolean> {
const query = this.knexConnection('nc_models');
if (project_id !== null && project_id !== undefined) {
query.where('project_id', project_id);
}
if (dbAlias !== null && dbAlias !== undefined) {
query.where('db_alias', dbAlias);
}
const data = await query.first();
return !!data;
}
async commit() {
if (this.trx) {
await this.trx.commit();
}
this.trx = null;
}
async rollback(e?) {
if (this.trx) {
await this.trx.rollback(e);
}
this.trx = null;
}
async startTransaction(): Promise<this> {
const trx = await this.connection.transaction();
// todo: Extend transaction class to add our custom properties
Object.assign(trx, {
clientType: this.connection.clientType,
searchPath: (this.connection as any).searchPath,
});
// todo: tobe done
return this //new NcMetaIOImpl(this.app, this.config, trx);
}
async metaReset(
project_id: string,
dbAlias: string,
apiType?: string
): Promise<void> {
// const apiType: string = this.config?.envs?.[this.config.env || this.config.workingEnv]?.db.find(d => {
// return d.meta.dbAlias === dbAlias;
// })?.meta?.api?.type;
if (apiType) {
await Promise.all(
META_TABLES?.[apiType]?.map((table) => {
return (async () => {
try {
await this.knexConnection(table)
.where({ db_alias: dbAlias, project_id })
.del();
} catch (e) {
console.warn(`Error: ${table} reset failed`);
}
})();
})
);
}
}
public async projectCreate(
projectName: string,
config: any,
description?: string,
meta?: boolean
): Promise<any> {
try {
const ranId = this.getNanoId();
const id = `${projectName.toLowerCase().replace(/\W+/g, '_')}_${ranId}`;
if (meta) {
config.prefix = `nc_${ranId}__`;
// if(config.envs._noco?.db?.[0]?.meta?.tn){
// config.envs._noco.db[0].meta.tn += `_${prefix}`
// }
}
config.id = id;
const project: any = {
id,
title: projectName,
description,
config: CryptoJS.AES.encrypt(
JSON.stringify(config),
'secret'// todo: tobe replaced - this.config?.auth?.jwt?.secret
).toString(),
};
// todo: check project name used or not
await this.knexConnection('nc_projects').insert({
...project,
created_at: this.knexConnection?.fn?.now(),
updated_at: this.knexConnection?.fn?.now(),
});
// todo
await this.knexConnection(MetaTable.PROJECT).insert({
id,
title: projectName,
});
project.prefix = config.prefix;
return project;
} catch (e) {
console.log(e);
}
}
public async projectUpdate(projectId: string, config: any): Promise<any> {
try {
const project = {
config: CryptoJS.AES.encrypt(
JSON.stringify(config, null, 2),
'secret'// todo: tobe replaced - this.config?.auth?.jwt?.secret
).toString(),
};
// todo: check project name used or not
await this.knexConnection('nc_projects').update(project).where({
id: projectId,
});
} catch (e) {
console.log(e);
}
}
public async projectList(): Promise<any[]> {
return (await this.knexConnection('nc_projects').select()).map((p) => {
p.config = CryptoJS.AES.decrypt(
p.config,
'secret'// todo: tobe replaced - this.config?.auth?.jwt?.secret
).toString(CryptoJS.enc.Utf8);
return p;
});
}
public async userProjectList(userId: any): Promise<any[]> {
return (
await this.knexConnection('nc_projects')
.leftJoin(
this.knexConnection('nc_projects_users')
.where(`nc_projects_users.user_id`, userId)
.as('user'),
'user.project_id',
'nc_projects.id'
)
.select('nc_projects.*')
.select('user.user_id')
.select(
this.knexConnection('xc_users')
.select('xc_users.email')
.innerJoin(
'nc_projects_users',
'nc_projects_users.user_id',
'=',
'xc_users.id'
)
.whereRaw('nc_projects.id = nc_projects_users.project_id')
.where('nc_projects_users.roles', 'like', '%owner%')
.first()
.as('owner')
)
.select(
this.knexConnection('xc_users')
.count('xc_users.id')
.innerJoin(
'nc_projects_users',
'nc_projects_users.user_id',
'=',
'xc_users.id'
)
.where((qb) => {
qb.where('nc_projects_users.roles', 'like', '%creator%').orWhere(
'nc_projects_users.roles',
'like',
'%owner%'
);
})
.whereRaw('nc_projects.id = nc_projects_users.project_id')
.andWhere('xc_users.id', userId)
.first()
.as('is_creator')
)
).map((p) => {
p.allowed = p.user_id === userId;
p.config = CryptoJS.AES.decrypt(
p.config,
'secret'// todo: tobe replaced - this.config?.auth?.jwt?.secret
).toString(CryptoJS.enc.Utf8);
return p;
});
}
public async isUserHaveAccessToProject(
projectId: string,
userId: any
): Promise<boolean> {
return !!(await this.knexConnection('nc_projects_users')
.where({
project_id: projectId,
user_id: userId,
})
.first());
}
public async projectGet(projectName: string, encrypt?): Promise<any> {
const project = await this.knexConnection('nc_projects')
.where({
title: projectName,
})
.first();
if (project && !encrypt) {
project.config = CryptoJS.AES.decrypt(
project.config,
'secret'// todo: tobe replaced - this.config?.auth?.jwt?.secret
).toString(CryptoJS.enc.Utf8);
}
return project;
}
public async projectGetById(projectId: string, encrypt?): Promise<any> {
const project = await this.knexConnection('nc_projects')
.where({
id: projectId,
})
.first();
if (project && !encrypt) {
project.config = CryptoJS.AES.decrypt(
project.config,
'secret'// todo: tobe replaced - this.config?.auth?.jwt?.secret
).toString(CryptoJS.enc.Utf8);
}
return project;
}
public projectDelete(title: string): Promise<any> {
return this.knexConnection('nc_projects')
.where({
title,
})
.delete();
}
public projectDeleteById(id: string): Promise<any> {
return this.knexConnection('nc_projects')
.where({
id,
})
.delete();
}
public async projectStatusUpdate(
projectId: string,
status: string
): Promise<any> {
return this.knexConnection('nc_projects')
.update({
status,
})
.where({
id: projectId,
});
}
public async projectAddUser(
projectId: string,
userId: any,
roles: string
): Promise<any> {
if (
await this.knexConnection('nc_projects_users')
.where({
user_id: userId,
project_id: projectId,
})
.first()
) {
return {};
}
return this.knexConnection('nc_projects_users').insert({
user_id: userId,
project_id: projectId,
roles,
});
}
public projectRemoveUser(projectId: string, userId: any): Promise<any> {
return this.knexConnection('nc_projects_users')
.where({
user_id: userId,
project_id: projectId,
})
.delete();
}
public removeXcUser(userId: any): Promise<any> {
return this.knexConnection('xc_users')
.where({
id: userId,
})
.delete();
}
public get knex(): any {
return this.knexConnection;
}
private getNanoId() {
return nanoid();
}
public async audit(
project_id: string,
dbAlias: string,
target: string,
data: any
): Promise<any> {
if (['DATA', 'COMMENT'].includes(data?.op_type)) {
return Promise.resolve(undefined);
}
return this.metaInsert(project_id, dbAlias, target, data);
}
public async metaInit(): Promise<boolean> {
NocoCache.init();
await this.connection.migrate.latest({
migrationSource: new XcMigrationSource(),
tableName: 'xc_knex_migrations',
});
await this.connection.migrate.latest({
migrationSource: new XcMigrationSourcev2(),
tableName: 'xc_knex_migrationsv2',
});
return true;
}
}

4
packages/nocodb-nest/src/models/Model.ts

@ -1,5 +1,5 @@
import { isVirtualCol, ModelTypes, UITypes, ViewTypes } from 'nocodb-sdk';
import { BaseModelSqlv2 } from '../../../nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2'
import { BaseModelSqlv2 } from '../db/BaseModelSqlv2'
import Noco from '../Noco';
import { parseMetaProp } from '../utils/modelUtils';
import NocoCache from '../cache/NocoCache';
@ -17,7 +17,7 @@ import Audit from './Audit';
import View from './View';
import Column from './Column';
import type { BoolType, TableReqType, TableType } from 'nocodb-sdk';
import type { XKnex } from '../db/sql-data-mapper';
import type { XKnex } from '../db/CustomKnex';
export default class Model implements TableType {
copy_enabled: BoolType;

142
packages/nocodb-nest/src/plugins/backblaze/Backblaze.ts

@ -0,0 +1,142 @@
import fs from 'fs';
import { promisify } from 'util';
import AWS from 'aws-sdk';
import request from 'request';
import {
generateTempFilePath,
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
export default class Backblaze implements IStorageAdapterV2 {
private s3Client: AWS.S3;
private input: any;
constructor(input: any) {
this.input = input;
}
async fileCreate(key: string, file: XcFile): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
ContentType: file.mimetype,
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
const fileStream = fs.createReadStream(file.path);
fileStream.on('error', (err) => {
console.log('File Error', err);
reject(err);
});
uploadParams.Body = fileStream;
uploadParams.Key = key;
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err, data) => {
if (err) {
console.log('Error', err);
reject(err);
}
if (data) {
resolve(data.Location);
}
});
});
}
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
request(
{
url: url,
encoding: null,
},
(err, httpResponse, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
uploadParams.ContentType = httpResponse.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err) {
console.log('Error', err);
reject(err1);
}
if (data) {
resolve(data.Location);
}
});
}
);
});
}
patchRegion(region: string): string {
// in v0.0.1, we constructed the endpoint with `region = s3.us-west-001`
// in v0.0.2, `region` would be `us-west-001`
// as backblaze states Region is the 2nd part of your S3 Endpoint in documentation
if (region.startsWith('s3.')) {
region = region.slice(3);
}
return region;
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
public async fileRead(key: string): Promise<any> {
return new Promise((resolve, reject) => {
this.s3Client.getObject({ Key: key } as any, (err, data) => {
if (err) {
return reject(err);
}
if (!data?.Body) {
return reject(data);
}
return resolve(data.Body);
});
});
}
public async init(): Promise<any> {
const s3Options: any = {
params: { Bucket: this.input.bucket },
region: this.patchRegion(this.input.region),
};
s3Options.accessKeyId = this.input.access_key;
s3Options.secretAccessKey = this.input.access_secret;
s3Options.endpoint = new AWS.Endpoint(
`s3.${s3Options.region}.backblazeb2.com`
);
this.s3Client = new AWS.S3(s3Options);
}
public async test(): Promise<boolean> {
try {
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});
await promisify(fs.unlink)(tempFile);
return true;
} catch (e) {
throw e;
}
}
}

18
packages/nocodb-nest/src/plugins/backblaze/BackblazePlugin.ts

@ -0,0 +1,18 @@
import { XcStoragePlugin } from 'nc-plugin';
import Backblaze from './Backblaze';
import type { IStorageAdapterV2 } from 'nc-plugin';
class BackblazePlugin extends XcStoragePlugin {
private static storageAdapter: Backblaze;
public getAdapter(): IStorageAdapterV2 {
return BackblazePlugin.storageAdapter;
}
public async init(config: any): Promise<any> {
BackblazePlugin.storageAdapter = new Backblaze(config);
await BackblazePlugin.storageAdapter.init();
}
}
export default BackblazePlugin;

68
packages/nocodb-nest/src/plugins/backblaze/index.ts

@ -0,0 +1,68 @@
import { XcActionType, XcType } from 'nocodb-sdk';
import BackblazePlugin from './BackblazePlugin';
import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: BackblazePlugin,
title: 'Backblaze B2',
version: '0.0.2',
logo: 'plugins/backblaze.jpeg',
tags: 'Storage',
description:
'Backblaze B2 is enterprise-grade, S3 compatible storage that companies around the world use to store and serve data while improving their cloud OpEx vs. Amazon S3 and others.',
inputs: {
title: 'Configure Backblaze B2',
items: [
{
key: 'bucket',
label: 'Bucket Name',
placeholder: 'Bucket Name',
type: XcType.SingleLineText,
required: true,
},
{
key: 'region',
label: 'Region',
placeholder: 'e.g. us-west-001',
type: XcType.SingleLineText,
required: true,
},
{
key: 'access_key',
label: 'Access Key',
placeholder: 'i.e. keyID in App Keys',
type: XcType.SingleLineText,
required: true,
},
{
key: 'access_secret',
label: 'Access Secret',
placeholder: 'i.e. applicationKey in App Keys',
type: XcType.Password,
required: true,
},
],
actions: [
{
label: 'Test',
placeholder: 'Test',
key: 'test',
actionType: XcActionType.TEST,
type: XcType.Button,
},
{
label: 'Save',
placeholder: 'Save',
key: 'save',
actionType: XcActionType.SUBMIT,
type: XcType.Button,
},
],
msgOnInstall:
'Successfully installed and attachment will be stored in Backblaze B2',
msgOnUninstall: '',
},
category: 'Storage',
};
export default config;

21
packages/nocodb-nest/src/plugins/discord/Discord.ts

@ -0,0 +1,21 @@
import axios from 'axios';
import type { IWebhookNotificationAdapter } from 'nc-plugin';
export default class Discord implements IWebhookNotificationAdapter {
public init(): Promise<any> {
return Promise.resolve(undefined);
}
public async sendMessage(content: string, payload: any): Promise<any> {
for (const { webhook_url } of payload?.channels) {
try {
return await axios.post(webhook_url, {
content,
});
} catch (e) {
console.log(e);
throw e;
}
}
}
}

18
packages/nocodb-nest/src/plugins/discord/DiscordPlugin.ts

@ -0,0 +1,18 @@
import { XcWebhookNotificationPlugin } from 'nc-plugin';
import Discord from './Discord';
import type { IWebhookNotificationAdapter } from 'nc-plugin';
class DiscordPlugin extends XcWebhookNotificationPlugin {
private static notificationAdapter: Discord;
public getAdapter(): IWebhookNotificationAdapter {
return DiscordPlugin.notificationAdapter;
}
public async init(_config: any): Promise<any> {
DiscordPlugin.notificationAdapter = new Discord();
await DiscordPlugin.notificationAdapter.init();
}
}
export default DiscordPlugin;

56
packages/nocodb-nest/src/plugins/discord/index.ts

@ -0,0 +1,56 @@
import { XcActionType, XcType } from 'nocodb-sdk';
import DiscordPlugin from './DiscordPlugin';
import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: DiscordPlugin,
title: 'Discord',
version: '0.0.1',
logo: 'plugins/discord.png',
description:
'Discord is the easiest way to talk over voice, video, and text. Talk, chat, hang out, and stay close with your friends and communities.',
price: 'Free',
tags: 'Chat',
category: 'Chat',
inputs: {
title: 'Configure Discord',
array: true,
items: [
{
key: 'channel',
label: 'Channel Name',
placeholder: 'Channel Name',
type: XcType.SingleLineText,
required: true,
},
{
key: 'webhook_url',
label: 'Webhook URL',
type: XcType.Password,
placeholder: 'Webhook URL',
required: true,
},
],
actions: [
{
label: 'Test',
placeholder: 'Test',
key: 'test',
actionType: XcActionType.TEST,
type: XcType.Button,
},
{
label: 'Save',
placeholder: 'Save',
key: 'save',
actionType: XcActionType.SUBMIT,
type: XcType.Button,
},
],
msgOnInstall:
'Successfully installed and Discord is enabled for notification.',
msgOnUninstall: '',
},
};
export default config;

129
packages/nocodb-nest/src/plugins/gcs/Gcs.ts

@ -0,0 +1,129 @@
import fs from 'fs';
import { promisify } from 'util';
import { Storage } from '@google-cloud/storage';
import request from 'request';
import {
generateTempFilePath,
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import type { StorageOptions } from '@google-cloud/storage';
export default class Gcs implements IStorageAdapterV2 {
private storageClient: Storage;
private bucketName: string;
private input: any;
constructor(input: any) {
this.input = input;
}
async fileCreate(key: string, file: XcFile): Promise<any> {
const uploadResponse = await this.storageClient
.bucket(this.bucketName)
.upload(file.path, {
destination: key,
// Support for HTTP requests made with `Accept-Encoding: gzip`
gzip: true,
// By setting the option `destination`, you can change the name of the
// object you are uploading to a bucket.
metadata: {
// Enable long-lived HTTP caching headers
// Use only if the contents of the file will never change
// (If the contents will change, use cacheControl: 'no-cache')
cacheControl: 'public, max-age=31536000',
},
});
return uploadResponse[0].publicUrl();
}
fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
public fileRead(key: string): Promise<any> {
return new Promise((resolve, reject) => {
const file = this.storageClient.bucket(this.bucketName).file(key);
// Check for existence, since gcloud-node seemed to be caching the result
file.exists((err, exists) => {
if (exists) {
file.download((downerr, data) => {
if (err) {
return reject(downerr);
}
return resolve(data);
});
} else {
reject(err);
}
});
});
}
public async init(): Promise<any> {
const options: StorageOptions = {};
// options.credentials = {
// client_email: process.env.NC_GCS_CLIENT_EMAIL,
// private_key: process.env.NC_GCS_PRIVATE_KEY
// }
//
// this.bucketName = process.env.NC_GCS_BUCKET;
options.credentials = {
client_email: this.input.client_email,
// replace \n with real line breaks to avoid
// error:0909006C:PEM routines:get_name:no start line
private_key: this.input.private_key.replace(/\\n/gm, '\n'),
};
// default project ID would be used if it is not provided
if (this.input.project_id) {
options.projectId = this.input.project_id;
}
this.bucketName = this.input.bucket;
this.storageClient = new Storage(options);
}
public async test(): Promise<boolean> {
try {
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: '',
originalname: 'temp.txt',
size: '',
});
await promisify(fs.unlink)(tempFile);
return true;
} catch (e) {
throw e;
}
}
fileCreateByUrl(destPath: string, url: string): Promise<any> {
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
request(
{
url: url,
encoding: null,
},
(err, _, body) => {
if (err) return reject(err);
this.storageClient
.bucket(this.bucketName)
.file(destPath)
.save(body)
.then((res) => resolve(res))
.catch(reject);
}
);
});
}
}

18
packages/nocodb-nest/src/plugins/gcs/GcsPlugin.ts

@ -0,0 +1,18 @@
import { XcStoragePlugin } from 'nc-plugin';
import Gcs from './Gcs';
import type { IStorageAdapterV2 } from 'nc-plugin';
class GcsPlugin extends XcStoragePlugin {
private static storageAdapter: Gcs;
public getAdapter(): IStorageAdapterV2 {
return GcsPlugin.storageAdapter;
}
public async init(config: any): Promise<any> {
GcsPlugin.storageAdapter = new Gcs(config);
await GcsPlugin.storageAdapter.init();
}
}
export default GcsPlugin;

69
packages/nocodb-nest/src/plugins/gcs/index.ts

@ -0,0 +1,69 @@
import { XcActionType, XcType } from 'nocodb-sdk';
import GcsPlugin from './GcsPlugin';
import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: GcsPlugin,
title: 'GCS',
version: '0.0.2',
logo: 'plugins/gcs.png',
description:
'Google Cloud Storage is a RESTful online file storage web service for storing and accessing data on Google Cloud Platform infrastructure.',
price: 'Free',
tags: 'Storage',
category: 'Storage',
inputs: {
title: 'Configure Google Cloud Storage',
items: [
{
key: 'bucket',
label: 'Bucket Name',
placeholder: 'Bucket Name',
type: XcType.SingleLineText,
required: true,
},
{
key: 'client_email',
label: 'Client Email',
placeholder: 'Client Email',
type: XcType.SingleLineText,
required: true,
},
{
key: 'private_key',
label: 'Private Key',
placeholder: 'Private Key',
type: XcType.Password,
required: true,
},
{
key: 'project_id',
label: 'Project ID',
placeholder: 'Project ID',
type: XcType.SingleLineText,
required: false,
},
],
actions: [
{
label: 'Test',
placeholder: 'Test',
key: 'test',
actionType: XcActionType.TEST,
type: XcType.Button,
},
{
label: 'Save',
placeholder: 'Save',
key: 'save',
actionType: XcActionType.SUBMIT,
type: XcType.Button,
},
],
msgOnInstall:
'Successfully installed and attachment will be stored in Google Cloud Storage',
msgOnUninstall: '',
},
};
export default config;

132
packages/nocodb-nest/src/plugins/linode/LinodeObjectStorage.ts

@ -0,0 +1,132 @@
import fs from 'fs';
import { promisify } from 'util';
import AWS from 'aws-sdk';
import request from 'request';
import {
generateTempFilePath,
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
export default class LinodeObjectStorage implements IStorageAdapterV2 {
private s3Client: AWS.S3;
private input: any;
constructor(input: any) {
this.input = input;
}
async fileCreate(key: string, file: XcFile): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
ContentType: file.mimetype,
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
const fileStream = fs.createReadStream(file.path);
fileStream.on('error', (err) => {
console.log('File Error', err);
reject(err);
});
uploadParams.Body = fileStream;
uploadParams.Key = key;
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err, data) => {
if (err) {
console.log('Error', err);
reject(err);
}
if (data) {
resolve(data.Location);
}
});
});
}
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
request(
{
url: url,
encoding: null,
},
(err, httpResponse, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
uploadParams.ContentType = httpResponse.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err) {
console.log('Error', err);
reject(err1);
}
if (data) {
resolve(data.Location);
}
});
}
);
});
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
public async fileRead(key: string): Promise<any> {
return new Promise((resolve, reject) => {
this.s3Client.getObject({ Key: key } as any, (err, data) => {
if (err) {
return reject(err);
}
if (!data?.Body) {
return reject(data);
}
return resolve(data.Body);
});
});
}
public async init(): Promise<any> {
const s3Options: any = {
params: { Bucket: this.input.bucket },
region: this.input.region,
};
s3Options.accessKeyId = this.input.access_key;
s3Options.secretAccessKey = this.input.access_secret;
s3Options.endpoint = new AWS.Endpoint(
`${this.input.region}.linodeobjects.com`
);
this.s3Client = new AWS.S3(s3Options);
}
public async test(): Promise<boolean> {
try {
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});
await promisify(fs.unlink)(tempFile);
return true;
} catch (e) {
throw e;
}
}
}

18
packages/nocodb-nest/src/plugins/linode/LinodeObjectStoragePlugin.ts

@ -0,0 +1,18 @@
import { XcStoragePlugin } from 'nc-plugin';
import LinodeObjectStorage from './LinodeObjectStorage';
import type { IStorageAdapterV2 } from 'nc-plugin';
class LinodeObjectStoragePlugin extends XcStoragePlugin {
private static storageAdapter: LinodeObjectStorage;
public getAdapter(): IStorageAdapterV2 {
return LinodeObjectStoragePlugin.storageAdapter;
}
public async init(config: any): Promise<any> {
LinodeObjectStoragePlugin.storageAdapter = new LinodeObjectStorage(config);
await LinodeObjectStoragePlugin.storageAdapter.init();
}
}
export default LinodeObjectStoragePlugin;

68
packages/nocodb-nest/src/plugins/linode/index.ts

@ -0,0 +1,68 @@
import { XcActionType, XcType } from 'nocodb-sdk';
import LinodeObjectStoragePlugin from './LinodeObjectStoragePlugin';
import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: LinodeObjectStoragePlugin,
title: 'Linode Object Storage',
version: '0.0.1',
logo: 'plugins/linode.svg',
tags: 'Storage',
description:
'S3-compatible Linode Object Storage makes it easy and more affordable to manage unstructured data such as content assets, as well as sophisticated and data-intensive storage challenges around artificial intelligence and machine learning.',
inputs: {
title: 'Configure Linode Object Storage',
items: [
{
key: 'bucket',
label: 'Bucket Name',
placeholder: 'Bucket Name',
type: XcType.SingleLineText,
required: true,
},
{
key: 'region',
label: 'Region',
placeholder: 'Region',
type: XcType.SingleLineText,
required: true,
},
{
key: 'access_key',
label: 'Access Key',
placeholder: 'Access Key',
type: XcType.SingleLineText,
required: true,
},
{
key: 'access_secret',
label: 'Access Secret',
placeholder: 'Access Secret',
type: XcType.Password,
required: true,
},
],
actions: [
{
label: 'Test',
placeholder: 'Test',
key: 'test',
actionType: XcActionType.TEST,
type: XcType.Button,
},
{
label: 'Save',
placeholder: 'Save',
key: 'save',
actionType: XcActionType.SUBMIT,
type: XcType.Button,
},
],
msgOnInstall:
'Successfully installed and attachment will be stored in Linode Object Storage',
msgOnUninstall: '',
},
category: 'Storage',
};
export default config;

48
packages/nocodb-nest/src/plugins/mailerSend/MailerSend.ts

@ -0,0 +1,48 @@
import MailerSend, { EmailParams, Recipient } from 'mailersend';
import type { IEmailAdapter } from 'nc-plugin';
import type { XcEmail } from '../../interface/IEmailAdapter';
export default class Mailer implements IEmailAdapter {
private mailersend: MailerSend;
private input: any;
constructor(input: any) {
this.input = input;
}
public async init(): Promise<any> {
this.mailersend = new MailerSend({
api_key: this.input?.api_key,
});
}
public async mailSend(mail: XcEmail): Promise<any> {
const recipients = [new Recipient(mail.to)];
const emailParams = new EmailParams()
.setFrom(this.input.from)
.setFromName(this.input.from_name)
.setRecipients(recipients)
.setSubject(mail.subject)
.setHtml(mail.html)
.setText(mail.text);
const res = await this.mailersend.send(emailParams);
if (res.status === 401) {
throw new Error(res.status);
}
}
public async test(email): Promise<boolean> {
try {
await this.mailSend({
to: email,
subject: 'Test email',
html: 'Test email',
} as any);
return true;
} catch (e) {
throw e;
}
}
}

18
packages/nocodb-nest/src/plugins/mailerSend/MailerSendPlugin.ts

@ -0,0 +1,18 @@
import { XcEmailPlugin } from 'nc-plugin';
import MailerSend from './MailerSend';
import type { IEmailAdapter } from 'nc-plugin';
class MailerSendPlugin extends XcEmailPlugin {
private static storageAdapter: MailerSend;
public getAdapter(): IEmailAdapter {
return MailerSendPlugin.storageAdapter;
}
public async init(config: any): Promise<any> {
MailerSendPlugin.storageAdapter = new MailerSend(config);
await MailerSendPlugin.storageAdapter.init();
}
}
export default MailerSendPlugin;

60
packages/nocodb-nest/src/plugins/mailerSend/index.ts

@ -0,0 +1,60 @@
import { XcActionType, XcType } from 'nocodb-sdk';
import MailerSendPlugin from './MailerSendPlugin';
import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: MailerSendPlugin,
title: 'MailerSend',
version: '0.0.1',
logo: 'plugins/mailersend.svg',
// icon: 'mdi-email-outline',
description: 'MailerSend email client',
price: 'Free',
tags: 'Email',
category: 'Email',
inputs: {
title: 'Configure MailerSend',
items: [
{
key: 'api_key',
label: 'API KEy',
placeholder: 'eg: ***************',
type: XcType.Password,
required: true,
},
{
key: 'from',
label: 'From',
placeholder: 'eg: admin@run.com',
type: XcType.SingleLineText,
required: true,
},
{
key: 'from_name',
label: 'From Name',
placeholder: 'eg: Adam',
type: XcType.SingleLineText,
required: true,
},
],
actions: [
{
label: 'Test',
key: 'test',
actionType: XcActionType.TEST,
type: XcType.Button,
},
{
label: 'Save',
key: 'save',
actionType: XcActionType.SUBMIT,
type: XcType.Button,
},
],
msgOnInstall:
'Successfully installed and email notification will use MailerSend configuration',
msgOnUninstall: '',
},
};
export default config;

21
packages/nocodb-nest/src/plugins/mattermost/Mattermost.ts

@ -0,0 +1,21 @@
import axios from 'axios';
import type { IWebhookNotificationAdapter } from 'nc-plugin';
export default class Mattermost implements IWebhookNotificationAdapter {
public init(): Promise<any> {
return Promise.resolve(undefined);
}
public async sendMessage(text: string, payload: any): Promise<any> {
for (const { webhook_url } of payload?.channels) {
try {
return await axios.post(webhook_url, {
text,
});
} catch (e) {
console.log(e);
throw e;
}
}
}
}

18
packages/nocodb-nest/src/plugins/mattermost/MattermostPlugin.ts

@ -0,0 +1,18 @@
import { XcWebhookNotificationPlugin } from 'nc-plugin';
import Mattermost from './Mattermost';
import type { IWebhookNotificationAdapter } from 'nc-plugin';
class MattermostPlugin extends XcWebhookNotificationPlugin {
private static notificationAdapter: Mattermost;
public getAdapter(): IWebhookNotificationAdapter {
return MattermostPlugin.notificationAdapter;
}
public async init(_config: any): Promise<any> {
MattermostPlugin.notificationAdapter = new Mattermost();
await MattermostPlugin.notificationAdapter.init();
}
}
export default MattermostPlugin;

56
packages/nocodb-nest/src/plugins/mattermost/index.ts

@ -0,0 +1,56 @@
import { XcActionType, XcType } from 'nocodb-sdk';
import MattermostPlugin from './MattermostPlugin';
import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: MattermostPlugin,
title: 'Mattermost',
version: '0.0.1',
logo: 'plugins/mattermost.png',
description:
'Mattermost brings all your team communication into one place, making it searchable and accessible anywhere.',
price: 'Free',
tags: 'Chat',
category: 'Chat',
inputs: {
title: 'Configure Mattermost',
array: true,
items: [
{
key: 'channel',
label: 'Channel Name',
placeholder: 'Channel Name',
type: XcType.SingleLineText,
required: true,
},
{
key: 'webhook_url',
label: 'Webhook URL',
placeholder: 'Webhook URL',
type: XcType.Password,
required: true,
},
],
actions: [
{
label: 'Test',
placeholder: 'Test',
key: 'test',
actionType: XcActionType.TEST,
type: XcType.Button,
},
{
label: 'Save',
placeholder: 'Save',
key: 'save',
actionType: XcActionType.SUBMIT,
type: XcType.Button,
},
],
msgOnInstall:
'Successfully installed and Mattermost is enabled for notification.',
msgOnUninstall: '',
},
};
export default config;

134
packages/nocodb-nest/src/plugins/mino/Minio.ts

@ -0,0 +1,134 @@
import fs from 'fs';
import { promisify } from 'util';
import { Client as MinioClient } from 'minio';
import request from 'request';
import {
generateTempFilePath,
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
export default class Minio implements IStorageAdapterV2 {
private minioClient: MinioClient;
private input: any;
constructor(input: any) {
this.input = input;
}
async fileCreate(key: string, file: XcFile): Promise<any> {
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
const fileStream = fs.createReadStream(file.path);
fileStream.on('error', (err) => {
console.log('File Error', err);
reject(err);
});
// uploadParams.Body = fileStream;
// uploadParams.Key = key;
const metaData = {
'Content-Type': file.mimetype,
// 'X-Amz-Meta-Testing': 1234,
// 'run': 5678
};
// call S3 to retrieve upload file to specified bucket
this.minioClient
.putObject(this.input?.bucket, key, fileStream, metaData)
.then(() => {
resolve(
`http${this.input.useSSL ? 's' : ''}://${this.input.endPoint}:${
this.input.port
}/${this.input.bucket}/${key}`
);
})
.catch(reject);
});
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
public async fileRead(key: string): Promise<any> {
return new Promise((resolve, reject) => {
this.minioClient.getObject(this.input.bucket, key, (err, data) => {
if (err) {
return reject(err);
}
if (!data) {
return reject(data);
}
return resolve(data);
});
});
}
public async init(): Promise<any> {
// todo: update in ui(checkbox and number field)
this.input.port = +this.input.port || 9000;
this.input.useSSL = this.input.useSSL === true;
this.input.accessKey = this.input.access_key;
this.input.secretKey = this.input.access_secret;
this.minioClient = new MinioClient(this.input);
}
public async test(): Promise<boolean> {
try {
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: '',
originalname: 'temp.txt',
size: '',
});
await promisify(fs.unlink)(tempFile);
return true;
} catch (e) {
throw e;
}
}
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
request(
{
url: url,
encoding: null,
},
(err, _, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
// uploadParams.Body = fileStream;
// uploadParams.Key = key;
const metaData = {
// 'Content-Type': file.mimetype
// 'X-Amz-Meta-Testing': 1234,
// 'run': 5678
};
// call S3 to retrieve upload file to specified bucket
this.minioClient
.putObject(this.input?.bucket, key, body, metaData)
.then(() => {
resolve(
`http${this.input.useSSL ? 's' : ''}://${this.input.endPoint}:${
this.input.port
}/${this.input.bucket}/${key}`
);
})
.catch(reject);
}
);
});
}
}

18
packages/nocodb-nest/src/plugins/mino/MinioPlugin.ts

@ -0,0 +1,18 @@
import { XcStoragePlugin } from 'nc-plugin';
import Minio from './Minio';
import type { IStorageAdapterV2 } from 'nc-plugin';
class MinioPlugin extends XcStoragePlugin {
private static storageAdapter: Minio;
public getAdapter(): IStorageAdapterV2 {
return MinioPlugin.storageAdapter;
}
public async init(config: any): Promise<any> {
MinioPlugin.storageAdapter = new Minio(config);
await MinioPlugin.storageAdapter.init();
}
}
export default MinioPlugin;

83
packages/nocodb-nest/src/plugins/mino/index.ts

@ -0,0 +1,83 @@
import { XcActionType, XcType } from 'nocodb-sdk';
import S3Plugin from './MinioPlugin';
import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: S3Plugin,
title: 'Minio',
version: '0.0.1',
logo: 'plugins/minio.png',
description:
'MinIO is a High Performance Object Storage released under Apache License v2.0. It is API compatible with Amazon S3 cloud storage service.',
price: 'Free',
tags: 'Storage',
category: 'Storage',
inputs: {
title: 'Configure Minio',
items: [
{
key: 'endPoint',
label: 'Minio Endpoint',
placeholder: 'Minio Endpoint',
type: XcType.SingleLineText,
required: true,
},
{
key: 'port',
label: 'Port',
placeholder: 'Port',
type: XcType.Number,
required: true,
},
{
key: 'bucket',
label: 'Bucket Name',
placeholder: 'Bucket Name',
type: XcType.SingleLineText,
required: true,
},
{
key: 'access_key',
label: 'Access Key',
placeholder: 'Access Key',
type: XcType.SingleLineText,
required: true,
},
{
key: 'access_secret',
label: 'Access Secret',
placeholder: 'Access Secret',
type: XcType.Password,
required: true,
},
{
key: 'useSSL',
label: 'Use SSL',
placeholder: 'Use SSL',
type: XcType.Checkbox,
required: true,
},
],
actions: [
{
label: 'Test',
placeholder: 'Test',
key: 'test',
actionType: XcActionType.TEST,
type: XcType.Button,
},
{
label: 'Save',
placeholder: 'Save',
key: 'save',
actionType: XcActionType.SUBMIT,
type: XcType.Button,
},
],
msgOnInstall:
'Successfully installed and attachment will be stored in Minio',
msgOnUninstall: '',
},
};
export default config;

132
packages/nocodb-nest/src/plugins/ovhCloud/OvhCloud.ts

@ -0,0 +1,132 @@
import fs from 'fs';
import { promisify } from 'util';
import AWS from 'aws-sdk';
import request from 'request';
import {
generateTempFilePath,
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
export default class OvhCloud implements IStorageAdapterV2 {
private s3Client: AWS.S3;
private input: any;
constructor(input: any) {
this.input = input;
}
async fileCreate(key: string, file: XcFile): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
ContentType: file.mimetype,
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
const fileStream = fs.createReadStream(file.path);
fileStream.on('error', (err) => {
console.log('File Error', err);
reject(err);
});
uploadParams.Body = fileStream;
uploadParams.Key = key;
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err, data) => {
if (err) {
console.log('Error', err);
reject(err);
}
if (data) {
resolve(data.Location);
}
});
});
}
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
request(
{
url: url,
encoding: null,
},
(err, httpResponse, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
uploadParams.ContentType = httpResponse.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err) {
console.log('Error', err);
reject(err1);
}
if (data) {
resolve(data.Location);
}
});
}
);
});
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
public async fileRead(key: string): Promise<any> {
return new Promise((resolve, reject) => {
this.s3Client.getObject({ Key: key } as any, (err, data) => {
if (err) {
return reject(err);
}
if (!data?.Body) {
return reject(data);
}
return resolve(data.Body);
});
});
}
public async init(): Promise<any> {
const s3Options: any = {
params: { Bucket: this.input.bucket },
region: this.input.region,
};
s3Options.accessKeyId = this.input.access_key;
s3Options.secretAccessKey = this.input.access_secret;
s3Options.endpoint = new AWS.Endpoint(
`s3.${this.input.region}.cloud.ovh.net`
);
this.s3Client = new AWS.S3(s3Options);
}
public async test(): Promise<boolean> {
try {
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});
await promisify(fs.unlink)(tempFile);
return true;
} catch (e) {
throw e;
}
}
}

18
packages/nocodb-nest/src/plugins/ovhCloud/OvhCloudPlugin.ts

@ -0,0 +1,18 @@
import { XcStoragePlugin } from 'nc-plugin';
import OvhCloud from './OvhCloud';
import type { IStorageAdapterV2 } from 'nc-plugin';
class OvhCloudPlugin extends XcStoragePlugin {
private static storageAdapter: OvhCloud;
public getAdapter(): IStorageAdapterV2 {
return OvhCloudPlugin.storageAdapter;
}
public async init(config: any): Promise<any> {
OvhCloudPlugin.storageAdapter = new OvhCloud(config);
await OvhCloudPlugin.storageAdapter.init();
}
}
export default OvhCloudPlugin;

68
packages/nocodb-nest/src/plugins/ovhCloud/index.ts

@ -0,0 +1,68 @@
import { XcActionType, XcType } from 'nocodb-sdk';
import OvhCloud from './OvhCloudPlugin';
import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: OvhCloud,
title: 'OvhCloud Object Storage',
version: '0.0.1',
logo: 'plugins/ovhCloud.png',
tags: 'Storage',
description:
'Upload your files to a space that you can access via HTTPS using the OpenStack Swift API, or the S3 API. ',
inputs: {
title: 'Configure OvhCloud Object Storage',
items: [
{
key: 'bucket',
label: 'Bucket Name',
placeholder: 'Bucket Name',
type: XcType.SingleLineText,
required: true,
},
{
key: 'region',
label: 'Region',
placeholder: 'Region',
type: XcType.SingleLineText,
required: true,
},
{
key: 'access_key',
label: 'Access Key',
placeholder: 'Access Key',
type: XcType.SingleLineText,
required: true,
},
{
key: 'access_secret',
label: 'Access Secret',
placeholder: 'Access Secret',
type: XcType.Password,
required: true,
},
],
actions: [
{
label: 'Test',
placeholder: 'Test',
key: 'test',
actionType: XcActionType.TEST,
type: XcType.Button,
},
{
label: 'Save',
placeholder: 'Save',
key: 'save',
actionType: XcActionType.SUBMIT,
type: XcType.Button,
},
],
msgOnInstall:
'Successfully installed and attachment will be stored in OvhCloud Object Storage',
msgOnUninstall: '',
},
category: 'Storage',
};
export default config;

135
packages/nocodb-nest/src/plugins/s3/S3.ts

@ -0,0 +1,135 @@
import fs from 'fs';
import { promisify } from 'util';
import AWS from 'aws-sdk';
import request from 'request';
import {
generateTempFilePath,
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
export default class S3 implements IStorageAdapterV2 {
private s3Client: AWS.S3;
private input: any;
constructor(input: any) {
this.input = input;
}
async fileCreate(key: string, file: XcFile): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
ContentType: file.mimetype,
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
const fileStream = fs.createReadStream(file.path);
fileStream.on('error', (err) => {
console.log('File Error', err);
reject(err);
});
uploadParams.Body = fileStream;
uploadParams.Key = key;
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err, data) => {
if (err) {
console.log('Error', err);
reject(err);
}
if (data) {
resolve(data.Location);
}
});
});
}
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
request(
{
url: url,
encoding: null,
},
(err, httpResponse, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
uploadParams.ContentType = httpResponse.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err) {
console.log('Error', err);
reject(err1);
}
if (data) {
resolve(data.Location);
}
});
}
);
});
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
public async fileRead(key: string): Promise<any> {
return new Promise((resolve, reject) => {
this.s3Client.getObject({ Key: key } as any, (err, data) => {
if (err) {
return reject(err);
}
if (!data?.Body) {
return reject(data);
}
return resolve(data.Body);
});
});
}
public async init(): Promise<any> {
// const s3Options: any = {
// params: {Bucket: process.env.NC_S3_BUCKET},
// region: process.env.NC_S3_REGION
// };
//
// s3Options.accessKeyId = process.env.NC_S3_KEY;
// s3Options.secretAccessKey = process.env.NC_S3_SECRET;
const s3Options: any = {
params: { Bucket: this.input.bucket },
region: this.input.region,
};
s3Options.accessKeyId = this.input.access_key;
s3Options.secretAccessKey = this.input.access_secret;
this.s3Client = new AWS.S3(s3Options);
}
public async test(): Promise<boolean> {
try {
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});
await promisify(fs.unlink)(tempFile);
return true;
} catch (e) {
throw e;
}
}
}

18
packages/nocodb-nest/src/plugins/s3/S3Plugin.ts

@ -0,0 +1,18 @@
import { XcStoragePlugin } from 'nc-plugin';
import S3 from './S3';
import type { IStorageAdapterV2 } from 'nc-plugin';
class S3Plugin extends XcStoragePlugin {
private static storageAdapter: S3;
public getAdapter(): IStorageAdapterV2 {
return S3Plugin.storageAdapter;
}
public async init(config: any): Promise<any> {
S3Plugin.storageAdapter = new S3(config);
await S3Plugin.storageAdapter.init();
}
}
export default S3Plugin;

68
packages/nocodb-nest/src/plugins/s3/index.ts

@ -0,0 +1,68 @@
import { PluginCategory, XcActionType, XcType } from 'nocodb-sdk';
import S3Plugin from './S3Plugin';
import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: S3Plugin,
title: 'S3',
version: '0.0.1',
logo: 'plugins/s3.png',
description:
'Amazon Simple Storage Service (Amazon S3) is an object storage service that offers industry-leading scalability, data availability, security, and performance.',
inputs: {
title: 'Configure Amazon S3',
items: [
{
key: 'bucket',
label: 'Bucket Name',
placeholder: 'Bucket Name',
type: XcType.SingleLineText,
required: true,
},
{
key: 'region',
label: 'Region',
placeholder: 'Region',
type: XcType.SingleLineText,
required: true,
},
{
key: 'access_key',
label: 'Access Key',
placeholder: 'Access Key',
type: XcType.SingleLineText,
required: true,
},
{
key: 'access_secret',
label: 'Access Secret',
placeholder: 'Access Secret',
type: XcType.Password,
required: true,
},
],
actions: [
{
label: 'Test',
placeholder: 'Test',
key: 'test',
actionType: XcActionType.TEST,
type: XcType.Button,
},
{
label: 'Save',
placeholder: 'Save',
key: 'save',
actionType: XcActionType.SUBMIT,
type: XcType.Button,
},
],
msgOnInstall:
'Successfully installed and attachment will be stored in AWS S3',
msgOnUninstall: '',
},
category: PluginCategory.STORAGE,
tags: 'Storage',
};
export default config;

130
packages/nocodb-nest/src/plugins/scaleway/ScalewayObjectStorage.ts

@ -0,0 +1,130 @@
import fs from 'fs';
import { promisify } from 'util';
import AWS from 'aws-sdk';
import request from 'request';
import {
generateTempFilePath,
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
export default class ScalewayObjectStorage implements IStorageAdapterV2 {
private s3Client: AWS.S3;
private input: any;
constructor(input: any) {
this.input = input;
}
public async fileRead(key: string): Promise<any> {
return new Promise((resolve, reject) => {
this.s3Client.getObject({ Key: key } as any, (err, data) => {
if (err) {
return reject(err);
}
if (!data?.Body) {
return reject(data);
}
return resolve(data.Body);
});
});
}
public async test(): Promise<boolean> {
try {
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});
await promisify(fs.unlink)(tempFile);
return true;
} catch (e) {
throw e;
}
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
public async init(): Promise<any> {
const s3Options: any = {
params: { Bucket: this.input.bucket },
region: this.input.region,
};
s3Options.accessKeyId = this.input.access_key;
s3Options.secretAccessKey = this.input.access_secret;
s3Options.endpoint = new AWS.Endpoint(`s3.${this.input.region}.scw.cloud`);
this.s3Client = new AWS.S3(s3Options);
}
async fileCreate(key: string, file: XcFile): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
ContentType: file.mimetype,
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
const fileStream = fs.createReadStream(file.path);
fileStream.on('error', (err) => {
console.log('File Error', err);
reject(err);
});
uploadParams.Body = fileStream;
uploadParams.Key = key;
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err, data) => {
if (err) {
console.log('Error', err);
reject(err);
}
if (data) {
resolve(data.Location);
}
});
});
}
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
request(
{
url: url,
encoding: null,
},
(err, httpResponse, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
uploadParams.ContentType = httpResponse.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err) {
console.log('Error', err);
reject(err1);
}
if (data) {
resolve(data.Location);
}
});
}
);
});
}
}

18
packages/nocodb-nest/src/plugins/scaleway/ScalewayObjectStoragePlugin.ts

@ -0,0 +1,18 @@
import { XcStoragePlugin } from 'nc-plugin';
import ScalewayObjectStorage from './ScalewayObjectStorage';
import type { IStorageAdapterV2 } from 'nc-plugin';
class ScalewayObjectStoragePlugin extends XcStoragePlugin {
private static storageAdapter: ScalewayObjectStorage;
public async init(config: any): Promise<any> {
ScalewayObjectStoragePlugin.storageAdapter = new ScalewayObjectStorage(
config
);
await ScalewayObjectStoragePlugin.storageAdapter.init();
}
public getAdapter(): IStorageAdapterV2 {
return ScalewayObjectStoragePlugin.storageAdapter;
}
}
export default ScalewayObjectStoragePlugin;

67
packages/nocodb-nest/src/plugins/scaleway/index.ts

@ -0,0 +1,67 @@
import { XcActionType, XcType } from 'nocodb-sdk';
import ScalewayObjectStoragePlugin from './ScalewayObjectStoragePlugin';
import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: ScalewayObjectStoragePlugin,
title: 'Scaleway Object Storage',
version: '0.0.1',
logo: 'plugins/scaleway.png',
tags: 'Storage',
description:
'Scaleway Object Storage is an S3-compatible object store from Scaleway Cloud Platform.',
inputs: {
title: 'Setup Scaleway',
items: [
{
key: 'bucket',
label: 'Bucket name',
placeholder: 'Bucket name',
type: XcType.SingleLineText,
required: true,
},
{
key: 'region',
label: 'Region of bucket',
placeholder: 'Region of bucket',
type: XcType.SingleLineText,
required: true,
},
{
key: 'access_key',
label: 'Access Key',
placeholder: 'Access Key',
type: XcType.SingleLineText,
required: true,
},
{
key: 'access_secret',
label: 'Access Secret',
placeholder: 'Access Secret',
type: XcType.Password,
required: true,
},
],
actions: [
{
label: 'Test',
placeholder: 'Test',
key: 'test',
actionType: XcActionType.TEST,
type: XcType.Button,
},
{
label: 'Save',
placeholder: 'Save',
key: 'save',
actionType: XcActionType.SUBMIT,
type: XcType.Button,
},
],
msgOnInstall: 'Successfully installed Scaleway Object Storage',
msgOnUninstall: '',
},
category: 'Storage',
};
export default config;

54
packages/nocodb-nest/src/plugins/ses/SES.ts

@ -0,0 +1,54 @@
import nodemailer from 'nodemailer';
import AWS from 'aws-sdk';
import type { IEmailAdapter } from 'nc-plugin';
import type Mail from 'nodemailer/lib/mailer';
import type { XcEmail } from '../../interface/IEmailAdapter';
export default class SES implements IEmailAdapter {
private transporter: Mail;
private input: any;
constructor(input: any) {
this.input = input;
}
public async init(): Promise<any> {
const sesOptions: any = {
accessKeyId: this.input.access_key,
secretAccessKey: this.input.access_secret,
region: this.input.region,
};
this.transporter = nodemailer.createTransport({
SES: new AWS.SES(sesOptions),
});
}
public async mailSend(mail: XcEmail): Promise<any> {
if (this.transporter) {
this.transporter.sendMail(
{ ...mail, from: this.input.from },
(err, info) => {
if (err) {
console.log(err);
} else {
console.log('Message sent: ' + info.response);
}
}
);
}
}
public async test(): Promise<boolean> {
try {
await this.mailSend({
to: this.input.from,
subject: 'Test email',
html: 'Test email',
} as any);
return true;
} catch (e) {
throw e;
}
}
}

18
packages/nocodb-nest/src/plugins/ses/SESPlugin.ts

@ -0,0 +1,18 @@
import { XcEmailPlugin } from 'nc-plugin';
import SES from './SES';
import type { IEmailAdapter } from 'nc-plugin';
class SESPlugin extends XcEmailPlugin {
private static storageAdapter: SES;
public getAdapter(): IEmailAdapter {
return SESPlugin.storageAdapter;
}
public async init(config: any): Promise<any> {
SESPlugin.storageAdapter = new SES(config);
await SESPlugin.storageAdapter.init();
}
}
export default SESPlugin;

69
packages/nocodb-nest/src/plugins/ses/index.ts

@ -0,0 +1,69 @@
import { XcActionType, XcType } from 'nocodb-sdk';
import SESPlugin from './SESPlugin';
import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: SESPlugin,
title: 'SES',
version: '0.0.1',
logo: 'plugins/aws.png',
description:
'Amazon Simple Email Service (SES) is a cost-effective, flexible, and scalable email service that enables developers to send mail from within any application.',
price: 'Free',
tags: 'Email',
category: 'Email',
inputs: {
title: 'Configure Amazon Simple Email Service (SES)',
items: [
{
key: 'from',
label: 'From',
placeholder: 'From',
type: XcType.SingleLineText,
required: true,
},
{
key: 'region',
label: 'Region',
placeholder: 'Region',
type: XcType.SingleLineText,
required: true,
},
{
key: 'access_key',
label: 'Access Key',
placeholder: 'Access Key',
type: XcType.SingleLineText,
required: true,
},
{
key: 'access_secret',
label: 'Access Secret',
placeholder: 'Access Secret',
type: XcType.Password,
required: true,
},
],
actions: [
{
label: 'Test',
placeholder: 'Test',
key: 'test',
actionType: XcActionType.TEST,
type: XcType.Button,
},
{
label: 'Save',
placeholder: 'Save',
key: 'save',
actionType: XcActionType.SUBMIT,
type: XcType.Button,
},
],
msgOnInstall:
'Successfully installed and email notification will use Amazon SES',
msgOnUninstall: '',
},
};
export default config;

21
packages/nocodb-nest/src/plugins/slack/Slack.ts

@ -0,0 +1,21 @@
import axios from 'axios';
import type { IWebhookNotificationAdapter } from 'nc-plugin';
export default class Slack implements IWebhookNotificationAdapter {
public init(): Promise<any> {
return Promise.resolve(undefined);
}
public async sendMessage(text: string, payload: any): Promise<any> {
for (const { webhook_url } of payload?.channels) {
try {
return await axios.post(webhook_url, {
text,
});
} catch (e) {
console.log(e);
throw e;
}
}
}
}

18
packages/nocodb-nest/src/plugins/slack/SlackPlugin.ts

@ -0,0 +1,18 @@
import { XcWebhookNotificationPlugin } from 'nc-plugin';
import Slack from './Slack';
import type { IWebhookNotificationAdapter } from 'nc-plugin';
class SlackPlugin extends XcWebhookNotificationPlugin {
private static notificationAdapter: Slack;
public getAdapter(): IWebhookNotificationAdapter {
return SlackPlugin.notificationAdapter;
}
public async init(_config: any): Promise<any> {
SlackPlugin.notificationAdapter = new Slack();
await SlackPlugin.notificationAdapter.init();
}
}
export default SlackPlugin;

56
packages/nocodb-nest/src/plugins/slack/index.ts

@ -0,0 +1,56 @@
import { XcActionType, XcType } from 'nocodb-sdk';
import SlackPlugin from './SlackPlugin';
import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: SlackPlugin,
title: 'Slack',
version: '0.0.1',
logo: 'plugins/slack.webp',
description:
'Slack brings team communication and collaboration into one place so you can get more work done, whether you belong to a large enterprise or a small business. ',
price: 'Free',
tags: 'Chat',
category: 'Chat',
inputs: {
title: 'Configure Slack',
array: true,
items: [
{
key: 'channel',
label: 'Channel Name',
placeholder: 'Channel Name',
type: XcType.SingleLineText,
required: true,
},
{
key: 'webhook_url',
label: 'Webhook URL',
placeholder: 'Webhook URL',
type: XcType.Password,
required: true,
},
],
actions: [
{
label: 'Test',
placeholder: 'Test',
key: 'test',
actionType: XcActionType.TEST,
type: XcType.Button,
},
{
label: 'Save',
placeholder: 'Save',
key: 'save',
actionType: XcActionType.SUBMIT,
type: XcType.Button,
},
],
msgOnInstall:
'Successfully installed and Slack is enabled for notification.',
msgOnUninstall: '',
},
};
export default config;

59
packages/nocodb-nest/src/plugins/smtp/SMTP.ts

@ -0,0 +1,59 @@
import nodemailer from 'nodemailer';
import type { IEmailAdapter } from 'nc-plugin';
import type Mail from 'nodemailer/lib/mailer';
import type { XcEmail } from '../../interface/IEmailAdapter';
export default class SMTP implements IEmailAdapter {
private transporter: Mail;
private input: any;
constructor(input: any) {
this.input = input;
}
public async init(): Promise<any> {
const config = {
host: this.input?.host,
port: parseInt(this.input?.port, 10),
secure:
typeof this.input?.secure === 'boolean'
? this.input?.secure
: this.input?.secure === 'true',
ignoreTLS:
typeof this.input?.ignoreTLS === 'boolean'
? this.input?.ignoreTLS
: this.input?.ignoreTLS === 'true',
auth: {
user: this.input?.username,
pass: this.input?.password,
},
tls: {
rejectUnauthorized: this.input?.rejectUnauthorized,
},
};
this.transporter = nodemailer.createTransport(config);
}
public async mailSend(mail: XcEmail): Promise<any> {
if (this.transporter) {
await this.transporter.sendMail({ ...mail, from: this.input.from });
}
}
public async test(): Promise<boolean> {
try {
await this.mailSend({
to: this.input.from,
subject: 'Test email',
html: 'Test email',
} as any);
return true;
} catch (e) {
console.log('SMTP test error :: ', e);
throw new Error(
'SMTP test failed, please check server log for more details.'
);
}
}
}

18
packages/nocodb-nest/src/plugins/smtp/SMTPPlugin.ts

@ -0,0 +1,18 @@
import { XcEmailPlugin } from 'nc-plugin';
import SMTP from './SMTP';
import type { IEmailAdapter } from 'nc-plugin';
class SMTPPlugin extends XcEmailPlugin {
private static storageAdapter: SMTP;
public getAdapter(): IEmailAdapter {
return SMTPPlugin.storageAdapter;
}
public async init(config: any): Promise<any> {
SMTPPlugin.storageAdapter = new SMTP(config);
await SMTPPlugin.storageAdapter.init();
}
}
export default SMTPPlugin;

96
packages/nocodb-nest/src/plugins/smtp/index.ts

@ -0,0 +1,96 @@
import { XcActionType, XcType } from 'nocodb-sdk';
import SMTPPlugin from './SMTPPlugin';
import type { XcPluginConfig } from 'nc-plugin';
// @author <dean@deanlofts.xyz>
const config: XcPluginConfig = {
builder: SMTPPlugin,
title: 'SMTP',
version: '0.0.2',
// icon: 'mdi-email-outline',
description: 'SMTP email client',
price: 'Free',
tags: 'Email',
category: 'Email',
inputs: {
title: 'Configure Email SMTP',
items: [
{
key: 'from',
label: 'From',
placeholder: 'eg: admin@run.com',
type: XcType.SingleLineText,
required: true,
},
{
key: 'host',
label: 'Host',
placeholder: 'eg: smtp.run.com',
type: XcType.SingleLineText,
required: true,
},
{
key: 'port',
label: 'Port',
placeholder: 'Port',
type: XcType.SingleLineText,
required: true,
},
{
key: 'secure',
label: 'Secure',
placeholder: 'Secure',
type: XcType.Checkbox,
required: false,
},
{
key: 'ignoreTLS',
label: 'Ignore TLS',
placeholder: 'Ignore TLS',
type: XcType.Checkbox,
required: false,
},
{
key: 'rejectUnauthorized',
label: 'Reject Unauthorized',
placeholder: 'Reject Unauthorized',
type: XcType.Checkbox,
required: false,
},
{
key: 'username',
label: 'Username',
placeholder: 'Username',
type: XcType.SingleLineText,
required: false,
},
{
key: 'password',
label: 'Password',
placeholder: 'Password',
type: XcType.Password,
required: false,
},
],
actions: [
{
label: 'Test',
key: 'test',
actionType: XcActionType.TEST,
type: XcType.Button,
},
{
label: 'Save',
key: 'save',
actionType: XcActionType.SUBMIT,
type: XcType.Button,
},
],
msgOnInstall:
'Successfully installed and email notification will use SMTP configuration',
msgOnUninstall: '',
},
};
export default config;

140
packages/nocodb-nest/src/plugins/spaces/Spaces.ts

@ -0,0 +1,140 @@
import fs from 'fs';
import { promisify } from 'util';
import AWS from 'aws-sdk';
import request from 'request';
import {
generateTempFilePath,
waitForStreamClose,
} from '../../utils/pluginUtils';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
export default class Spaces implements IStorageAdapterV2 {
private s3Client: AWS.S3;
private input: any;
constructor(input: any) {
this.input = input;
}
async fileCreate(key: string, file: XcFile): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
ContentType: file.mimetype,
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
const fileStream = fs.createReadStream(file.path);
fileStream.on('error', (err) => {
console.log('File Error', err);
reject(err);
});
uploadParams.Body = fileStream;
uploadParams.Key = key;
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err, data) => {
if (err) {
console.log('Error', err);
reject(err);
}
if (data) {
resolve(data.Location);
}
});
});
}
async fileCreateByUrl(key: string, url: string): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
request(
{
url: url,
encoding: null,
},
(err, httpResponse, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
uploadParams.ContentType = httpResponse.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
if (err) {
console.log('Error', err);
reject(err1);
}
if (data) {
resolve(data.Location);
}
});
}
);
});
}
public async fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
public async fileRead(key: string): Promise<any> {
return new Promise((resolve, reject) => {
this.s3Client.getObject({ Key: key } as any, (err, data) => {
if (err) {
return reject(err);
}
if (!data?.Body) {
return reject(data);
}
return resolve(data.Body);
});
});
}
public async init(): Promise<any> {
// const s3Options: any = {
// params: {Bucket: process.env.NC_S3_BUCKET},
// region: process.env.NC_S3_REGION
// };
//
// s3Options.accessKeyId = process.env.NC_S3_KEY;
// s3Options.secretAccessKey = process.env.NC_S3_SECRET;
const s3Options: any = {
params: { Bucket: this.input.bucket },
region: this.input.region,
};
s3Options.accessKeyId = this.input.access_key;
s3Options.secretAccessKey = this.input.access_secret;
s3Options.endpoint = new AWS.Endpoint(
`${this.input.region || 'nyc3'}.digitaloceanspaces.com`
);
this.s3Client = new AWS.S3(s3Options);
}
public async test(): Promise<boolean> {
try {
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});
await promisify(fs.unlink)(tempFile);
return true;
} catch (e) {
throw e;
}
}
}

18
packages/nocodb-nest/src/plugins/spaces/SpacesPlugin.ts

@ -0,0 +1,18 @@
import { XcStoragePlugin } from 'nc-plugin';
import Spaces from './Spaces';
import type { IStorageAdapterV2 } from 'nc-plugin';
class SpacesPlugin extends XcStoragePlugin {
private static storageAdapter: Spaces;
public getAdapter(): IStorageAdapterV2 {
return SpacesPlugin.storageAdapter;
}
public async init(config: any): Promise<any> {
SpacesPlugin.storageAdapter = new Spaces(config);
await SpacesPlugin.storageAdapter.init();
}
}
export default SpacesPlugin;

69
packages/nocodb-nest/src/plugins/spaces/index.ts

@ -0,0 +1,69 @@
import { XcActionType, XcType } from 'nocodb-sdk';
import SpacesPlugin from './SpacesPlugin';
import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: SpacesPlugin,
title: 'Spaces',
version: '0.0.1',
logo: 'plugins/spaces.png',
description:
'Store & deliver vast amounts of content with a simple architecture.',
price: 'Free',
tags: 'Storage',
category: 'Storage',
inputs: {
title: 'DigitalOcean Spaces',
items: [
{
key: 'bucket',
label: 'Bucket Name',
placeholder: 'Bucket Name',
type: XcType.SingleLineText,
required: true,
},
{
key: 'region',
label: 'Region',
placeholder: 'Region',
type: XcType.SingleLineText,
required: true,
},
{
key: 'access_key',
label: 'Access Key',
placeholder: 'Access Key',
type: XcType.SingleLineText,
required: true,
},
{
key: 'access_secret',
label: 'Access Secret',
placeholder: 'Access Secret',
type: XcType.Password,
required: true,
},
],
actions: [
{
label: 'Test',
placeholder: 'Test',
key: 'test',
actionType: XcActionType.TEST,
type: XcType.Button,
},
{
label: 'Save',
placeholder: 'Save',
key: 'save',
actionType: XcActionType.SUBMIT,
type: XcType.Button,
},
],
msgOnInstall:
'Successfully installed and attachment will be stored in DigitalOcean Spaces',
msgOnUninstall: '',
},
};
export default config;

91
packages/nocodb-nest/src/plugins/storage/Local.ts

@ -0,0 +1,91 @@
import fs from 'fs';
import path from 'path';
import { promisify } from 'util';
import mkdirp from 'mkdirp';
import axios from 'axios';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
import NcConfigFactory from 'src/utils/NcConfigFactory';
export default class Local implements IStorageAdapterV2 {
constructor() {}
public async fileCreate(key: string, file: XcFile): Promise<any> {
const destPath = path.join(NcConfigFactory.getToolDir(), ...key.split('/'));
try {
await mkdirp(path.dirname(destPath));
const data = await promisify(fs.readFile)(file.path);
await promisify(fs.writeFile)(destPath, data);
await promisify(fs.unlink)(file.path);
// await fs.promises.rename(file.path, destPath);
} catch (e) {
throw e;
}
}
async fileCreateByUrl(key: string, url: string): Promise<any> {
const destPath = path.join(NcConfigFactory.getToolDir(), ...key.split('/'));
return new Promise((resolve, reject) => {
axios
.get(url, {
responseType: 'stream',
headers: {
accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'accept-language': 'en-US,en;q=0.9',
'cache-control': 'no-cache',
pragma: 'no-cache',
'user-agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36',
origin: 'https://www.airtable.com/',
},
})
.then(async (response) => {
await mkdirp(path.dirname(destPath));
const file = fs.createWriteStream(destPath);
// close() is async, call cb after close completes
file.on('finish', () => {
file.close((err) => {
if (err) {
return reject(err);
}
resolve(null);
});
});
file.on('error', (err) => {
// Handle errors
fs.unlink(destPath, () => reject(err.message)); // delete the (partial) file and then return the error
});
response.data.pipe(file);
})
.catch((err) => {
reject(err.message);
});
});
}
// todo: implement
fileDelete(_path: string): Promise<any> {
return Promise.resolve(undefined);
}
public async fileRead(filePath: string): Promise<any> {
try {
const fileData = await fs.promises.readFile(
path.join(NcConfigFactory.getToolDir(), ...filePath.split('/'))
);
return fileData;
} catch (e) {
throw e;
}
}
init(): Promise<any> {
return Promise.resolve(undefined);
}
test(): Promise<boolean> {
return Promise.resolve(false);
}
}

21
packages/nocodb-nest/src/plugins/teams/Teams.ts

@ -0,0 +1,21 @@
import axios from 'axios';
import type { IWebhookNotificationAdapter } from 'nc-plugin';
export default class Teams implements IWebhookNotificationAdapter {
public init(): Promise<any> {
return Promise.resolve(undefined);
}
public async sendMessage(Text: string, payload: any): Promise<any> {
for (const { webhook_url } of payload?.channels) {
try {
return await axios.post(webhook_url, {
Text,
});
} catch (e) {
console.log(e);
throw e;
}
}
}
}

18
packages/nocodb-nest/src/plugins/teams/TeamsPlugin.ts

@ -0,0 +1,18 @@
import { XcWebhookNotificationPlugin } from 'nc-plugin';
import Teams from './Teams';
import type { IWebhookNotificationAdapter } from 'nc-plugin';
class TeamsPlugin extends XcWebhookNotificationPlugin {
private static notificationAdapter: Teams;
public getAdapter(): IWebhookNotificationAdapter {
return TeamsPlugin.notificationAdapter;
}
public async init(_config: any): Promise<any> {
TeamsPlugin.notificationAdapter = new Teams();
await TeamsPlugin.notificationAdapter.init();
}
}
export default TeamsPlugin;

56
packages/nocodb-nest/src/plugins/teams/index.ts

@ -0,0 +1,56 @@
import { XcActionType, XcType } from 'nocodb-sdk';
import TeamsPlugin from './TeamsPlugin';
import type { XcPluginConfig } from 'nc-plugin';
const config: XcPluginConfig = {
builder: TeamsPlugin,
title: 'Microsoft Teams',
version: '0.0.1',
logo: 'plugins/teams.ico',
description:
'Microsoft Teams is for everyone · Instantly go from group chat to video call with the touch of a button.',
price: 'Free',
tags: 'Chat',
category: 'Chat',
inputs: {
title: 'Configure Microsoft Teams',
array: true,
items: [
{
key: 'channel',
label: 'Channel Name',
placeholder: 'Channel Name',
type: XcType.SingleLineText,
required: true,
},
{
key: 'webhook_url',
label: 'Webhook URL',
placeholder: 'Webhook URL',
type: XcType.Password,
required: true,
},
],
actions: [
{
label: 'Test',
placeholder: 'Test',
key: 'test',
actionType: XcActionType.TEST,
type: XcType.Button,
},
{
label: 'Save',
placeholder: 'Save',
key: 'save',
actionType: XcActionType.SUBMIT,
type: XcType.Button,
},
],
msgOnInstall:
'Successfully installed and Microsoft Teams is enabled for notification.',
msgOnUninstall: '',
},
};
export default config;

30
packages/nocodb-nest/src/plugins/twilio/Twilio.ts

@ -0,0 +1,30 @@
import twilio from 'twilio';
import type { IWebhookNotificationAdapter } from 'nc-plugin';
export default class Twilio implements IWebhookNotificationAdapter {
private input: any;
private client: any;
constructor(input: any) {
this.input = input;
}
public async init() {
this.client = twilio(this.input.sid, this.input.token);
}
public async sendMessage(content: string, payload: any): Promise<any> {
for (const num of payload?.to?.split(/\s*?,\s*?/)) {
try {
await this.client.messages.create({
body: content,
from: this.input.from,
to: num,
});
} catch (e) {
console.log(e);
throw e;
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save