Browse Source

style(nocodb): applied prettier styling

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/642/head
Pranav C 3 years ago
parent
commit
6c923aa12d
  1. 3
      packages/nocodb/.prettierrc.js
  2. 137
      packages/nocodb/src/__tests__/formula.test.ts
  3. 479
      packages/nocodb/src/__tests__/graphql.test.ts
  4. 37
      packages/nocodb/src/__tests__/noco/NcConfigFactory.test.ts
  5. 789
      packages/nocodb/src/__tests__/rest.test.ts
  6. 16
      packages/nocodb/src/example/docker.ts
  7. 38
      packages/nocodb/src/example/try.ts
  8. 19
      packages/nocodb/src/interface/IEmailAdapter.ts
  9. 15
      packages/nocodb/src/interface/IStorageAdapter.ts
  10. 5
      packages/nocodb/src/interface/XcDynamicChanges.ts
  11. 6
      packages/nocodb/src/interface/XcMetaMgr.ts
  12. 163
      packages/nocodb/src/interface/config.ts
  13. 8
      packages/nocodb/src/lib/dataMapper/index.ts
  14. 670
      packages/nocodb/src/lib/dataMapper/lib/BaseModel.ts
  15. 12
      packages/nocodb/src/lib/dataMapper/lib/DbFactory.ts
  16. 794
      packages/nocodb/src/lib/dataMapper/lib/sql/CustomKnex.ts
  17. 103
      packages/nocodb/src/lib/dataMapper/lib/sql/formulaQueryBuilderFromString.ts
  18. 80
      packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/commonFns.ts
  19. 63
      packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/mssql.ts
  20. 41
      packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/mysql.ts
  21. 29
      packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/pg.ts
  22. 47
      packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/sqlite.ts
  23. 29
      packages/nocodb/src/lib/dataMapper/lib/sql/genRollupSelect.ts
  24. 34
      packages/nocodb/src/lib/dataMapper/lib/sql/mapFunctionName.ts
  25. 9
      packages/nocodb/src/lib/index.ts
  26. 660
      packages/nocodb/src/lib/migrator/SqlMigrator/lib/KnexMigrator.ts
  27. 5
      packages/nocodb/src/lib/migrator/SqlMigrator/lib/SqlMigrator.ts
  28. 16
      packages/nocodb/src/lib/migrator/SqlMigrator/lib/SqlMigratorFactory.ts
  29. 42
      packages/nocodb/src/lib/migrator/SqlMigrator/lib/templates/mssql.template.ts
  30. 34
      packages/nocodb/src/lib/migrator/SqlMigrator/lib/templates/mysql.template.ts
  31. 34
      packages/nocodb/src/lib/migrator/SqlMigrator/lib/templates/pg.template.ts
  32. 28
      packages/nocodb/src/lib/migrator/SqlMigrator/lib/templates/sqlite.template.ts
  33. 37
      packages/nocodb/src/lib/migrator/util/Debug.ts
  34. 24
      packages/nocodb/src/lib/migrator/util/DebugMgr.ts
  35. 12
      packages/nocodb/src/lib/migrator/util/FileCollection.ts
  36. 8
      packages/nocodb/src/lib/migrator/util/Result.ts
  37. 4
      packages/nocodb/src/lib/migrator/util/emit.ts
  38. 29
      packages/nocodb/src/lib/migrator/util/file.help.ts
  39. 434
      packages/nocodb/src/lib/noco/NcProjectBuilder.ts
  40. 42
      packages/nocodb/src/lib/noco/NcProjectBuilderEE.ts
  41. 306
      packages/nocodb/src/lib/noco/Noco.ts
  42. 1724
      packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts
  43. 17
      packages/nocodb/src/lib/noco/common/BaseProcedure.ts
  44. 138
      packages/nocodb/src/lib/noco/common/NcConnectionMgr.ts
  45. 19
      packages/nocodb/src/lib/noco/common/XcAudit.ts
  46. 33
      packages/nocodb/src/lib/noco/common/XcCron.ts
  47. 53
      packages/nocodb/src/lib/noco/common/XcProcedure.ts
  48. 2
      packages/nocodb/src/lib/noco/common/formSubmissionEmailTemplate.ts
  49. 19
      packages/nocodb/src/lib/noco/common/helpers/addErrorOnColumnDeleteInFormula.ts
  50. 48
      packages/nocodb/src/lib/noco/common/helpers/jsepTreeToFormula.ts
  51. 23
      packages/nocodb/src/lib/noco/common/helpers/updateColumnNameInFormula.ts
  52. 2503
      packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts
  53. 411
      packages/nocodb/src/lib/noco/gql/GqlAuthResolver.ts
  54. 22
      packages/nocodb/src/lib/noco/gql/GqlBaseResolver.ts
  55. 27
      packages/nocodb/src/lib/noco/gql/GqlCommonResolvers.ts
  56. 107
      packages/nocodb/src/lib/noco/gql/GqlMiddleware.ts
  57. 59
      packages/nocodb/src/lib/noco/gql/GqlProcedureResolver.ts
  58. 215
      packages/nocodb/src/lib/noco/gql/GqlResolver.ts
  59. 3
      packages/nocodb/src/lib/noco/gql/auth/schema.ts
  60. 3
      packages/nocodb/src/lib/noco/gql/common.schema.ts
  61. 2
      packages/nocodb/src/lib/noco/gql/emailTemplate/forgotPassword.ts
  62. 2
      packages/nocodb/src/lib/noco/gql/emailTemplate/verify.ts
  63. 30
      packages/nocodb/src/lib/noco/meta/MetaAPILogger.ts
  64. 285
      packages/nocodb/src/lib/noco/meta/NcMetaIO.ts
  65. 444
      packages/nocodb/src/lib/noco/meta/NcMetaIOImpl.ts
  66. 12
      packages/nocodb/src/lib/noco/meta/NcMetaIOImplEE.ts
  67. 161
      packages/nocodb/src/lib/noco/migrations/nc_001_init.ts
  68. 58
      packages/nocodb/src/lib/noco/migrations/nc_002_add_m2m.ts
  69. 14
      packages/nocodb/src/lib/noco/migrations/nc_003_add_fkn_column.ts
  70. 43
      packages/nocodb/src/lib/noco/nc.try.ts
  71. 111
      packages/nocodb/src/lib/noco/plugins/NcPluginMgr.ts
  72. 13
      packages/nocodb/src/lib/noco/plugins/adapters/cache/XcCache.ts
  73. 21
      packages/nocodb/src/lib/noco/plugins/adapters/discord/Discord.ts
  74. 12
      packages/nocodb/src/lib/noco/plugins/adapters/email/EmailFactory.ts
  75. 18
      packages/nocodb/src/lib/noco/plugins/adapters/email/SES.ts
  76. 27
      packages/nocodb/src/lib/noco/plugins/adapters/email/SMTP.ts
  77. 21
      packages/nocodb/src/lib/noco/plugins/adapters/mattermost/Mattermost.ts
  78. 19
      packages/nocodb/src/lib/noco/plugins/adapters/slack/Slack.ts
  79. 24
      packages/nocodb/src/lib/noco/plugins/adapters/storage/Local.ts
  80. 20
      packages/nocodb/src/lib/noco/plugins/adapters/twilio/Twilio.ts
  81. 84
      packages/nocodb/src/lib/noco/plugins/azure.ts
  82. 35
      packages/nocodb/src/lib/noco/plugins/brand.ts
  83. 69
      packages/nocodb/src/lib/noco/plugins/cache.ts
  84. 72
      packages/nocodb/src/lib/noco/plugins/discord.ts
  85. 54
      packages/nocodb/src/lib/noco/plugins/ee.ts
  86. 81
      packages/nocodb/src/lib/noco/plugins/githubAuth.ts
  87. 81
      packages/nocodb/src/lib/noco/plugins/googleAuth.ts
  88. 72
      packages/nocodb/src/lib/noco/plugins/mattermost.ts
  89. 123
      packages/nocodb/src/lib/noco/plugins/ses.ts
  90. 69
      packages/nocodb/src/lib/noco/plugins/slack.ts
  91. 129
      packages/nocodb/src/lib/noco/plugins/smtp.ts
  92. 85
      packages/nocodb/src/lib/noco/plugins/twilio.ts
  93. 2248
      packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts
  94. 1390
      packages/nocodb/src/lib/noco/rest/RestAuthCtrl.ts
  95. 233
      packages/nocodb/src/lib/noco/rest/RestAuthCtrlEE.ts
  96. 89
      packages/nocodb/src/lib/noco/rest/RestBaseCtrl.ts
  97. 156
      packages/nocodb/src/lib/noco/rest/RestCtrl.ts
  98. 141
      packages/nocodb/src/lib/noco/rest/RestCtrlBelongsTo.ts
  99. 33
      packages/nocodb/src/lib/noco/rest/RestCtrlCustom.ts
  100. 146
      packages/nocodb/src/lib/noco/rest/RestCtrlHasMany.ts
  101. Some files were not shown because too many files have changed in this diff Show More

3
packages/nocodb/.prettierrc.js

@ -2,5 +2,6 @@ module.exports = {
semi: true,
trailingComma: "all",
singleQuote: true,
tabWidth: 2
tabWidth: 2,
printWidth: 120
};

137
packages/nocodb/src/__tests__/formula.test.ts

@ -1,59 +1,126 @@
import {expect} from 'chai';
import { expect } from 'chai';
import 'mocha';
import knex from '../lib/dataMapper/lib/sql/CustomKnex';
import formulaQueryBuilderFromString from "../lib/dataMapper/lib/sql/formulaQueryBuilderFromString";
import formulaQueryBuilderFromString from '../lib/dataMapper/lib/sql/formulaQueryBuilderFromString';
process.env.TEST = 'test';
describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
let knexMysqlRef;
let knexPgRef;
let knexMssqlRef;
let knexSqliteRef;
// Called once before any of the tests in this block begin.
before(function (done) {
knexMysqlRef = knex({client:'mysql2'})
knexMssqlRef = knex({client:'mssql'})
knexPgRef = knex({client:'pg'})
knexSqliteRef = knex({client:'sqlite3'})
done()
before(function(done) {
knexMysqlRef = knex({ client: 'mysql2' });
knexMssqlRef = knex({ client: 'mssql' });
knexPgRef = knex({ client: 'pg' });
knexSqliteRef = knex({ client: 'sqlite3' });
done();
});
after((done) => {
after(done => {
done();
});
describe('Formulas', function () {
it('Simple formula', function (done) {
expect(formulaQueryBuilderFromString("concat(city, ' _ ',city_id+country_id)", 'a',knexMysqlRef).toQuery()).eq('concat(`city`,\' _ \',`city_id` + `country_id`) as a')
expect(formulaQueryBuilderFromString("concat(city, ' _ ',city_id+country_id)", 'a',knexPgRef).toQuery()).eq('concat("city",\' _ \',"city_id" + "country_id") as a')
expect(formulaQueryBuilderFromString("concat(city, ' _ ',city_id+country_id)", 'a',knexMssqlRef).toQuery()).eq('concat([city],\' _ \',[city_id] + [country_id]) as a')
expect(formulaQueryBuilderFromString("concat(city, ' _ ',city_id+country_id)", 'a',knexSqliteRef).toQuery()).eq('`city` || \' _ \' || (`city_id` + `country_id`) as a')
done()
describe('Formulas', function() {
it('Simple formula', function(done) {
expect(
formulaQueryBuilderFromString(
"concat(city, ' _ ',city_id+country_id)",
'a',
knexMysqlRef
).toQuery()
).eq("concat(`city`,' _ ',`city_id` + `country_id`) as a");
expect(
formulaQueryBuilderFromString(
"concat(city, ' _ ',city_id+country_id)",
'a',
knexPgRef
).toQuery()
).eq('concat("city",\' _ \',"city_id" + "country_id") as a');
expect(
formulaQueryBuilderFromString(
"concat(city, ' _ ',city_id+country_id)",
'a',
knexMssqlRef
).toQuery()
).eq("concat([city],' _ ',[city_id] + [country_id]) as a");
expect(
formulaQueryBuilderFromString(
"concat(city, ' _ ',city_id+country_id)",
'a',
knexSqliteRef
).toQuery()
).eq("`city` || ' _ ' || (`city_id` + `country_id`) as a");
done();
});
it('Addition', function (done) {
expect(formulaQueryBuilderFromString("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexMysqlRef).toQuery()).eq('`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4 as a')
expect(formulaQueryBuilderFromString("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexPgRef).toQuery()).eq('"city_id" + "country_id" + 2 + 3 + 4 + 5 + 4 as a')
expect(formulaQueryBuilderFromString("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexMssqlRef).toQuery()).eq('[city_id] + [country_id] + 2 + 3 + 4 + 5 + 4 as a')
expect(formulaQueryBuilderFromString("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexSqliteRef).toQuery()).eq('`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4 as a')
done()
it('Addition', function(done) {
expect(
formulaQueryBuilderFromString(
'ADD(city_id,country_id,2,3,4,5,4)',
'a',
knexMysqlRef
).toQuery()
).eq('`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4 as a');
expect(
formulaQueryBuilderFromString(
'ADD(city_id,country_id,2,3,4,5,4)',
'a',
knexPgRef
).toQuery()
).eq('"city_id" + "country_id" + 2 + 3 + 4 + 5 + 4 as a');
expect(
formulaQueryBuilderFromString(
'ADD(city_id,country_id,2,3,4,5,4)',
'a',
knexMssqlRef
).toQuery()
).eq('[city_id] + [country_id] + 2 + 3 + 4 + 5 + 4 as a');
expect(
formulaQueryBuilderFromString(
'ADD(city_id,country_id,2,3,4,5,4)',
'a',
knexSqliteRef
).toQuery()
).eq('`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4 as a');
done();
});
it('Average', function (done) {
expect(formulaQueryBuilderFromString("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexMysqlRef).toQuery()).eq('(`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4) / 7 as a')
expect(formulaQueryBuilderFromString("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexPgRef).toQuery()).eq('("city_id" + "country_id" + 2 + 3 + 4 + 5 + 4) / 7 as a')
expect(formulaQueryBuilderFromString("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexMssqlRef).toQuery()).eq('([city_id] + [country_id] + 2 + 3 + 4 + 5 + 4) / 7 as a')
expect(formulaQueryBuilderFromString("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexSqliteRef).toQuery()).eq('(`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4) / 7 as a')
done()
it('Average', function(done) {
expect(
formulaQueryBuilderFromString(
'AVG(city_id,country_id,2,3,4,5,4)',
'a',
knexMysqlRef
).toQuery()
).eq('(`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4) / 7 as a');
expect(
formulaQueryBuilderFromString(
'AVG(city_id,country_id,2,3,4,5,4)',
'a',
knexPgRef
).toQuery()
).eq('("city_id" + "country_id" + 2 + 3 + 4 + 5 + 4) / 7 as a');
expect(
formulaQueryBuilderFromString(
'AVG(city_id,country_id,2,3,4,5,4)',
'a',
knexMssqlRef
).toQuery()
).eq('([city_id] + [country_id] + 2 + 3 + 4 + 5 + 4) / 7 as a');
expect(
formulaQueryBuilderFromString(
'AVG(city_id,country_id,2,3,4,5,4)',
'a',
knexSqliteRef
).toQuery()
).eq('(`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4) / 7 as a');
done();
});
});
});/**
});
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>

479
packages/nocodb/src/__tests__/graphql.test.ts

@ -1,76 +1,77 @@
import {expect} from 'chai';
import { expect } from 'chai';
import 'mocha';
import express from 'express';
import request from 'supertest';
import {Noco} from "../lib";
import NcConfigFactory from "../lib/utils/NcConfigFactory";
import { Noco } from '../lib';
import NcConfigFactory from '../lib/utils/NcConfigFactory';
process.env.TEST = 'test';
const dbConfig = NcConfigFactory.urlToDbConfig(NcConfigFactory.extractXcUrlFromJdbc(process.env[`DATABASE_URL`]), null, null, 'graphql');
const dbConfig = NcConfigFactory.urlToDbConfig(
NcConfigFactory.extractXcUrlFromJdbc(process.env[`DATABASE_URL`]),
null,
null,
'graphql'
);
const projectCreateReqBody = {
"api": "projectCreateByWeb",
"query": {"skipProjectHasDb": 1},
"args": {
"project": {"title": "sebulba", "folder": "config.xc.json", "type": "pg"},
"projectJson": {
"title": "sebulba",
"version": "0.6",
"envs": {
"_noco": {
"db": [
dbConfig
], "apiClient": {"data": []}
api: 'projectCreateByWeb',
query: { skipProjectHasDb: 1 },
args: {
project: { title: 'sebulba', folder: 'config.xc.json', type: 'pg' },
projectJson: {
title: 'sebulba',
version: '0.6',
envs: {
_noco: {
db: [dbConfig],
apiClient: { data: [] }
}
},
"workingEnv": "_noco",
"meta": {
"version": "0.6",
"seedsFolder": "seeds",
"queriesFolder": "queries",
"apisFolder": "apis",
"projectType": "graphql",
"type": "mvc",
"language": "ts",
"db": {"client": "sqlite3", "connection": {"filename": "noco.db"}}
workingEnv: '_noco',
meta: {
version: '0.6',
seedsFolder: 'seeds',
queriesFolder: 'queries',
apisFolder: 'apis',
projectType: 'graphql',
type: 'mvc',
language: 'ts',
db: { client: 'sqlite3', connection: { filename: 'noco.db' } }
},
"seedsFolder": "seeds",
"queriesFolder": "queries",
"apisFolder": "apis",
"projectType": "graphql",
"type": "docker",
"language": "ts",
"apiClient": {"data": []},
"auth": {"jwt": {"secret": "b8ed266d-4475-4028-8c3d-590f58bee867", "dbAlias": "db"}}
seedsFolder: 'seeds',
queriesFolder: 'queries',
apisFolder: 'apis',
projectType: 'graphql',
type: 'docker',
language: 'ts',
apiClient: { data: [] },
auth: {
jwt: { secret: 'b8ed266d-4475-4028-8c3d-590f58bee867', dbAlias: 'db' }
}
}
}
}
};
describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
let app;
let token;
let projectId;
// Called once before any of the tests in this block begin.
before(function (done) {
before(function(done) {
this.timeout(10000);
(async () => {
const server = express();
server.use(await Noco.init({}));
app = server;
})().then(done).catch(done);
})()
.then(done)
.catch(done);
});
after((done) => {
after(done => {
done();
// process.exit();
});
@ -233,39 +234,39 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
// /**** Authentication : END ****/
/**************** START : Auth ****************/
describe('Authentication', function () {
describe('Authentication', function() {
this.timeout(10000);
const EMAIL_ID = 'abc@g.com'
const EMAIL_ID = 'abc@g.com';
const VALID_PASSWORD = '1234566778';
it('Signup with valid email', function (done) {
this.timeout(20000)
it('Signup with valid email', function(done) {
this.timeout(20000);
request(app)
.post('/auth/signup')
.send({email: EMAIL_ID, password: VALID_PASSWORD})
.send({ email: EMAIL_ID, password: VALID_PASSWORD })
.expect(200, (err, res) => {
if (err) {
expect(res.status).to.equal(400)
expect(res.status).to.equal(400);
} else {
const token = res.body.token;
expect(token).to.be.a("string")
expect(token).to.be.a('string');
}
done();
});
});
it('Signup with invalid email', (done) => {
it('Signup with invalid email', done => {
request(app)
.post('/auth/signup')
.send({email: 'test', password: VALID_PASSWORD})
.send({ email: 'test', password: VALID_PASSWORD })
.expect(400, done);
});
it('Signin with valid credentials', function (done) {
it('Signin with valid credentials', function(done) {
request(app)
.post('/auth/signin')
.send({email: EMAIL_ID, password: VALID_PASSWORD})
.expect(200, async function (err, res) {
.send({ email: EMAIL_ID, password: VALID_PASSWORD })
.expect(200, async function(err, res) {
if (err) {
return done(err);
}
@ -279,11 +280,11 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
});
});
it('me', function (done) {
it('me', function(done) {
request(app)
.get('/user/me')
.set('xc-auth', token)
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) {
return done(err);
}
@ -293,68 +294,62 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
});
});
it('Change password', function (done) {
it('Change password', function(done) {
request(app)
.post('/user/password/change')
.set('xc-auth', token)
.send({currentPassword: 'password', newPassword: 'password'})
.send({ currentPassword: 'password', newPassword: 'password' })
.expect(400, done);
});
it('Change password - after logout', function (done) {
it('Change password - after logout', function(done) {
// todo:
request(app)
.post('/user/password/change')
.send({currentPassword: 'password', newPassword: 'password'})
.expect(500, function (_err, _res) {
done()
.send({ currentPassword: 'password', newPassword: 'password' })
.expect(500, function(_err, _res) {
done();
});
});
it('Signin with invalid credentials', function (done) {
it('Signin with invalid credentials', function(done) {
request(app)
.post('/auth/signin')
.send({email: 'abc@abc.com', password: VALID_PASSWORD})
.send({ email: 'abc@abc.com', password: VALID_PASSWORD })
.expect(400, done);
});
it('Signin with invalid password', function (done) {
it('Signin with invalid password', function(done) {
request(app)
.post('/auth/signin')
.send({email: EMAIL_ID, password: 'wrongPassword'})
.send({ email: EMAIL_ID, password: 'wrongPassword' })
.expect(400, done);
});
it('Forgot password with a non-existing email id', function (done) {
it('Forgot password with a non-existing email id', function(done) {
request(app)
.post('/auth/password/forgot')
.send({email: 'abc@abc.com'})
.send({ email: 'abc@abc.com' })
.expect(400, done);
});
it('Forgot password with an existing email id', function (done) {
this.timeout(10000)
it('Forgot password with an existing email id', function(done) {
this.timeout(10000);
request(app)
.post('/auth/password/forgot')
.send({email: EMAIL_ID})
.send({ email: EMAIL_ID })
.expect(200, done);
});
it('Email validate with an invalid token', function (done) {
it('Email validate with an invalid token', function(done) {
request(app)
.post('/auth/email/validate/someRandomValue')
.send({email: EMAIL_ID})
.send({ email: EMAIL_ID })
.expect(400, done);
});
it('Email validate with a valid token', function (done) {
console.log('eeee')
it('Email validate with a valid token', function(done) {
console.log('eeee');
// todo :
done();
@ -365,18 +360,17 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
// .expect(500, done);
});
it('Forgot password validate with an invalid token', function (done) {
it('Forgot password validate with an invalid token', function(done) {
request(app)
.post('/auth/token/validate/someRandomValue')
.send({email: EMAIL_ID})
.send({ email: EMAIL_ID })
.expect(400, done);
});
it('Forgot password validate with a valid token', function (done) {
it('Forgot password validate with a valid token', function(done) {
// todo
done()
done();
// request(app)
// .post('/auth/token/validate/someRandomValue')
@ -384,38 +378,34 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
// .expect(500, done);
});
it('Reset Password with an invalid token', function (done) {
it('Reset Password with an invalid token', function(done) {
request(app)
.post('/auth/password/reset/someRandomValue')
.send({password: 'anewpassword'})
.send({ password: 'anewpassword' })
.expect(400, done);
});
it('Reset Password with an valid token', function (done) {
it('Reset Password with an valid token', function(done) {
//todo
done()
done();
// request(app)
// .post('/auth/password/reset/someRandomValue')
// .send({password: 'anewpassword'})
// .expect(500, done);
});
});
describe('Project', function () {
const EMAIL_ID = 'abc@g.com'
describe('Project', function() {
const EMAIL_ID = 'abc@g.com';
const VALID_PASSWORD = '1234566778';
before(function (done) {
this.timeout(120000)
before(function(done) {
this.timeout(120000);
request(app)
.post('/auth/signin')
.send({email: EMAIL_ID, password: VALID_PASSWORD})
.expect(200, async function (_err, res) {
.send({ email: EMAIL_ID, password: VALID_PASSWORD })
.expect(200, async function(_err, res) {
token = res.body.token;
request(app)
.post('/dashboard')
@ -423,34 +413,33 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
.send(projectCreateReqBody)
.expect(200, (err, res) => {
if (err) {
return done(err)
return done(err);
}
projectId = res.body.id;
done();
})
});
});
})
});
/**** country : START ****/
describe('country', function () {
describe('country', function() {
/**** Query : START ****/
it('countryList', function (done) {
it('countryList', function(done) {
request(app)
.post(`/nc/${projectId}/v1/graphql`)
.set('xc-auth', token)
.send({
query: `{ countryList(limit:5){ country_id country } }`
})
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) done(err);
const list = res.body.data.countryList;
expect(list).length.to.be.most(5)
expect(list[0]).to.have.all.keys(['country_id', 'country'])
done()
})
expect(list).length.to.be.most(5);
expect(list[0]).to.have.all.keys(['country_id', 'country']);
done();
});
});
it('countryList - with sort', function (done) {
it('countryList - with sort', function(done) {
// todo: order -> sort
request(app)
@ -460,24 +449,24 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
.send({
query: `{ countryList(sort:"-country_id"){ country_id country } }`
})
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) done(err);
const list = res.body.data.countryList;
expect(list[0]).to.have.all.keys(['country_id', 'country'])
expect(list[0]).to.have.all.keys(['country_id', 'country']);
expect(list).satisfy(array => {
let i = array.length;
while (--i) {
if (array[i].country_id > array[i - 1].country_id) return false;
}
return true
}, 'Should be in descending order')
return true;
}, 'Should be in descending order');
done()
})
done();
});
});
it('countryList - with limit', function (done) {
it('countryList - with limit', function(done) {
request(app)
.post(`/nc/${projectId}/v1/graphql`)
@ -485,16 +474,16 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
.send({
query: `{ countryList(limit:6){ country_id country } }`
})
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) done(err);
const list = res.body.data.countryList;
expect(list[0]).to.have.all.keys(['country_id', 'country'])
expect(list).to.have.length.most(6)
done()
})
expect(list[0]).to.have.all.keys(['country_id', 'country']);
expect(list).to.have.length.most(6);
done();
});
});
it('countryList - with offset', function (done) {
it('countryList - with offset', function(done) {
request(app)
.post(`/nc/${projectId}/v1/graphql`)
@ -502,10 +491,10 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
.send({
query: `{ countryList(offset:0,limit:6){ country_id country } }`
})
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) done(err);
const list1 = res.body.data.countryList;
expect(list1[0]).to.have.all.keys(['country_id', 'country'])
expect(list1[0]).to.have.all.keys(['country_id', 'country']);
request(app)
.post(`/nc/${projectId}/v1/graphql`)
@ -513,20 +502,26 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
.send({
query: `{ countryList(offset:1,limit:5){ country_id country } }`
})
.expect(200, function (err, res1) {
.expect(200, function(err, res1) {
if (err) done(err);
const list2 = res1.body.data.countryList;
expect(list2[0]).to.have.all.keys(['country_id', 'country'])
expect(list2).satisfy(arr => arr.every(({country, country_id}, i) =>
country === list1[i + 1].country && country_id === list1[i + 1].country_id
), 'Both data should need to be equal where offset vary with 1')
done()
expect(list2[0]).to.have.all.keys(['country_id', 'country']);
expect(list2).satisfy(
arr =>
arr.every(
({ country, country_id }, i) =>
country === list1[i + 1].country &&
country_id === list1[i + 1].country_id
),
'Both data should need to be equal where offset vary with 1'
);
done();
});
})
});
});
it('countryList - nested count', function (done) {
it('countryList - nested count', function(done) {
request(app)
.post(`/nc/${projectId}/v1/graphql`)
@ -534,17 +529,21 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
.send({
query: `{ countryList{ country_id country cityCount} }`
})
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) done(err);
const list = res.body.data.countryList;
expect(list[0]).to.have.all.keys(['country_id', 'country', 'cityCount'])
expect(list[0].cityCount).to.be.a('number')
expect(list[0]).to.have.all.keys([
'country_id',
'country',
'cityCount'
]);
expect(list[0].cityCount).to.be.a('number');
expect(list[0].cityCount % 1).to.be.equal(0);
done()
})
done();
});
});
it('countryList - nested cityList', function (done) {
it('countryList - nested cityList', function(done) {
request(app)
.post(`/nc/${projectId}/v1/graphql`)
@ -552,22 +551,31 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
.send({
query: `{ countryList{ country_id country cityList { city country_id }} }`
})
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) done(err);
const list = res.body.data.countryList;
expect(list[0]).to.have.all.keys(['country_id', 'country', 'cityList'])
expect(list[0].cityList).to.be.a('Array')
expect(list[0]).to.have.all.keys([
'country_id',
'country',
'cityList'
]);
expect(list[0].cityList).to.be.a('Array');
if (dbConfig.client !== 'mssql') {
expect(list[0].cityList[0]).to.be.a('object');
expect(list[0].cityList[0]).to.have.all.keys(['country_id', 'city'])
expect(Object.keys(list[0].cityList[0])).to.have.length(2)
expect(list[0].cityList[0].country_id).to.be.equal(list[0].country_id)
expect(list[0].cityList[0]).to.have.all.keys([
'country_id',
'city'
]);
expect(Object.keys(list[0].cityList[0])).to.have.length(2);
expect(list[0].cityList[0].country_id).to.be.equal(
list[0].country_id
);
}
done()
})
done();
});
});
it('countryRead', function (done) {
it('countryRead', function(done) {
request(app)
.post(`/nc/${projectId}/v1/graphql`)
@ -575,17 +583,17 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
.send({
query: `{ countryRead(id: "1"){ country_id country } } `
})
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) done(err);
const data = res.body.data.countryRead;
expect(data).to.be.a('object')
expect(data).to.have.all.keys(['country_id', 'country'])
expect(data).to.be.a('object');
expect(data).to.have.all.keys(['country_id', 'country']);
done()
})
done();
});
});
it('countryExists', function (done) {
it('countryExists', function(done) {
request(app)
.post(`/nc/${projectId}/v1/graphql`)
@ -593,16 +601,16 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
.send({
query: `{ countryExists(id: "1") } `
})
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) done(err);
const data = res.body.data.countryExists;
expect(data).to.be.a('boolean')
expect(data).to.be.equal(true)
done()
})
expect(data).to.be.a('boolean');
expect(data).to.be.equal(true);
done();
});
});
it('countryExists - with non-existing id', function (done) {
it('countryExists - with non-existing id', function(done) {
request(app)
.post(`/nc/${projectId}/v1/graphql`)
@ -610,16 +618,16 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
.send({
query: `{ countryExists(id: "30000") } `
})
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) done(err);
const data = res.body.data.countryExists;
expect(data).to.be.a('boolean')
expect(data).to.be.equal(false)
done()
})
expect(data).to.be.a('boolean');
expect(data).to.be.equal(false);
done();
});
});
it('countryFindOne', function (done) {
it('countryFindOne', function(done) {
request(app)
.post(`/nc/${projectId}/v1/graphql`)
@ -627,17 +635,17 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
.send({
query: `{ countryFindOne (where: "(country_id,eq,1)"){ country country_id } } `
})
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) done(err);
const data = res.body.data.countryFindOne;
expect(data).to.be.a('object')
expect(data).to.have.all.keys(['country', 'country_id'])
expect(data).to.be.a('object');
expect(data).to.have.all.keys(['country', 'country_id']);
expect(data.country_id).to.be.equal(1);
done()
})
done();
});
});
it('countryCount - filter by id', function (done) {
it('countryCount - filter by id', function(done) {
request(app)
.post(`/nc/${projectId}/v1/graphql`)
@ -645,16 +653,16 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
.send({
query: `{ countryCount (where: "(country_id,eq,1)") } `
})
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) done(err);
const data = res.body.data.countryCount;
expect(data).to.be.a('number')
expect(data).to.be.a('number');
expect(data).to.be.equal(1);
done()
})
done();
});
});
it('countryDistinct', function (done) {
it('countryDistinct', function(done) {
request(app)
.post(`/nc/${projectId}/v1/graphql`)
@ -662,19 +670,19 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
.send({
query: `{ countryDistinct(column_name: "last_update") { last_update } } `
})
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) done(err);
const data = res.body.data.countryDistinct;
expect(data).to.be.a('array')
expect(data[0]).to.be.a('object')
expect(data[0]).to.have.all.keys(['last_update'])
expect(data).to.be.a('array');
expect(data[0]).to.be.a('object');
expect(data[0]).to.have.all.keys(['last_update']);
expect(data[0].last_update).to.be.match(/\d+/);
done()
})
done();
});
});
if (dbConfig.client !== 'mssql') {
it('countryGroupBy', function (done) {
it('countryGroupBy', function(done) {
request(app)
.post(`/nc/${projectId}/v1/graphql`)
@ -682,18 +690,18 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
.send({
query: `{ countryGroupBy(fields: "last_update",limit:5) { last_update count } } `
})
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) done(err);
const data = res.body.data.countryGroupBy;
expect(data.length).to.be.most(5);
expect(data[0].count).to.be.greaterThan(0);
expect(data[0].last_update).to.be.a('string');
expect(Object.keys(data[0]).length).to.be.equal(2);
done()
})
done();
});
});
it('countryGroupBy - Multiple', function (done) {
it('countryGroupBy - Multiple', function(done) {
request(app)
.post(`/nc/${projectId}/v1/graphql`)
@ -701,7 +709,7 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
.send({
query: `{ countryGroupBy(fields: "last_update,country",limit:5) { last_update country count } } `
})
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) done(err);
const data = res.body.data.countryGroupBy;
expect(data.length).to.be.most(5);
@ -709,11 +717,11 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
expect(data[0].last_update).to.be.a('string');
expect(data[0].country).to.be.a('string');
expect(Object.keys(data[0]).length).to.be.equal(3);
done()
})
done();
});
});
it('countryAggregate', function (done) {
it('countryAggregate', function(done) {
request(app)
.post(`/nc/${projectId}/v1/graphql`)
@ -721,7 +729,7 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
.send({
query: `{ countryAggregate(func: "sum,avg,min,max,count", column_name : "country_id") { sum avg min max count } } `
})
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) done(err);
const data = res.body.data.countryAggregate;
expect(data).to.be.a('array');
@ -730,14 +738,19 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
expect(data[0].max).to.be.a('number');
expect(data[0].avg).to.be.a('number');
expect(data[0].sum).to.be.a('number');
expect(data[0].count).to.be.a('number').and.satisfy(num => num === parseInt(num), 'count should be an integer');
expect(data[0].count)
.to.be.a('number')
.and.satisfy(
num => num === parseInt(num),
'count should be an integer'
);
expect(Object.keys(data[0]).length).to.be.equal(5);
}
done();
})
});
});
it('countryDistribution', function (done) {
it('countryDistribution', function(done) {
request(app)
.post(`/nc/${projectId}/v1/graphql`)
@ -745,67 +758,71 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
.send({
query: `{ countryDistribution(column_name : "country_id") { range count } } `
})
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) done(err);
const data = res.body.data.countryDistribution;
expect(data).to.be.a('array');
expect(data[0].count).to.be.a('number');
expect(data[0].count).satisfies(num => num === parseInt(num) && num >= 0, 'should be a positive integer');
expect(data[0].count).satisfies(
num => num === parseInt(num) && num >= 0,
'should be a positive integer'
);
expect(data[0].range).to.be.a('string');
expect(data[0].range).to.be.match(/^\d+-\d+$/, 'should match {num start}-{num end} format')
expect(data[0].range).to.be.match(
/^\d+-\d+$/,
'should match {num start}-{num end} format'
);
done();
})
});
});
}
/**** Query : END ****/
/**** Mutation : START ****/
describe('Mutation', function () {
describe('Mutation', function() {
const COUNTRY_ID = 9999;
// const COUNTRY_CREATE_ID = 9998;
// const COUNTRY_NAME = 'test-name';
before(function (done) {
before(function(done) {
// create table entry for update and delete
// let db = knex(config.envs.dev.db[0])('country');
// db.insert({
// country_id: COUNTRY_ID,
// country: COUNTRY_NAME
// }).finally(() => done())
done()
})
after(function (done) {
done();
});
after(function(done) {
// delete table entries which is created for the test
// let db = knex(config.envs.dev.db[0])('country');
// db.whereIn('country_id', [COUNTRY_ID, COUNTRY_CREATE_ID])
// .del()
// .finally(() => done())
done()
})
done();
});
it('countryCreate', function (done) {
it('countryCreate', function(done) {
request(app)
.post(`/nc/${projectId}/v1/graphql`)
.set('xc-auth', token)
.send({
query: `mutation{ countryCreate( data : { country: "abcd" ${dbConfig.client === 'sqlite3' ? ' country_id : 999 ' : ''} }) { country_id country } } `
query: `mutation{ countryCreate( data : { country: "abcd" ${
dbConfig.client === 'sqlite3' ? ' country_id : 999 ' : ''
} }) { country_id country } } `
})
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) done(err);
const data = res.body.data.countryCreate;
expect(data).to.be.a('object');
expect(data.country_id).to.be.a('number');
expect(data.country).to.be.equal('abcd');
done();
})
});
});
it('countryUpdate', function (done) {
it('countryUpdate', function(done) {
request(app)
.post(`/nc/${projectId}/v1/graphql`)
@ -813,16 +830,16 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
.send({
query: `mutation{ countryUpdate( id : "${COUNTRY_ID}", data : { country: "abcd" }){ country } } `
})
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) done(err);
const data = res.body.data.countryUpdate;
expect(data).to.be.a('object');
// todo:
done();
})
});
});
it('countryDelete', function (done) {
it('countryDelete', function(done) {
request(app)
.post(`/nc/${projectId}/v1/graphql`)
@ -830,16 +847,15 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
.send({
query: `mutation{ countryDelete( id : "${COUNTRY_ID}") } `
})
.expect(200, function (err, res) {
.expect(200, function(err, res) {
if (err) done(err);
const data = res.body.data.countryDelete;
expect(data).to.be.a('number')
expect(data).to.be.a('number');
// todo:
done();
})
});
});
})
});
/**** Mutation : END ****/
// countryCreateBulk(data: [countryInput]): [Int]
@ -849,7 +865,8 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
});
/**** country : END ****/
});
});/**
});
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>

37
packages/nocodb/src/__tests__/noco/NcConfigFactory.test.ts

@ -1,10 +1,8 @@
import {expect} from 'chai';
import { expect } from 'chai';
import 'mocha';
import {NcConfigFactory} from "../../lib";
import { NcConfigFactory } from '../../lib';
describe('Config Factory Tests', () => {
const expectedObject = {
client: 'pg',
connection: {
@ -24,31 +22,36 @@ describe('Config Factory Tests', () => {
acquireConnectionTimeout: 600000
};
before(function (done) {
before(function(done) {
done();
});
after((done) => {
after(done => {
done();
})
});
it('Generate config from string', function (done) {
const config = NcConfigFactory.metaUrlToDbConfig(`pg://localhost:5432?u=postgres&p=xgene&d=abcde`);
it('Generate config from string', function(done) {
const config = NcConfigFactory.metaUrlToDbConfig(
`pg://localhost:5432?u=postgres&p=xgene&d=abcde`
);
const { pool, ssl, ...rest } = expectedObject;
expect(config).to.deep.equal(rest);
done();
});
it('Connection string with nested property', function (done) {
const config = NcConfigFactory.metaUrlToDbConfig(`pg://localhost:5432?u=postgres&p=xgene&d=abcde&pool.min=1&pool.max=2&ssl.rejectUnauthorized=false`);
it('Connection string with nested property', function(done) {
const config = NcConfigFactory.metaUrlToDbConfig(
`pg://localhost:5432?u=postgres&p=xgene&d=abcde&pool.min=1&pool.max=2&ssl.rejectUnauthorized=false`
);
expect(config).to.deep.equal(expectedObject);
done();
});
it('Allow creating config from JSON string', function(done) {
try {
process.env.NC_DB_JSON = JSON.stringify(expectedObject);
const { meta: { db: config } } = NcConfigFactory.make();
const {
meta: { db: config }
} = NcConfigFactory.make();
expect(config).to.deep.equal(expectedObject);
done();
} finally {
@ -58,8 +61,10 @@ describe('Config Factory Tests', () => {
it('Allow creating config from JSON file', function(done) {
try {
process.env.NC_DB_JSON_FILE = `${__dirname}/dbConfig.json`;
const { meta: { db: config } } = NcConfigFactory.make();
const {
meta: { db: config }
} = NcConfigFactory.make();
expect(config).to.deep.equal(expectedObject);
done();
} finally {

789
packages/nocodb/src/__tests__/rest.test.ts

File diff suppressed because it is too large Load Diff

16
packages/nocodb/src/example/docker.ts

@ -1,13 +1,15 @@
import cors from 'cors';
import express from 'express';
import Noco from "../lib/noco/Noco";
import Noco from '../lib/noco/Noco';
process.env.NC_VERSION = '0009044';
const server = express();
server.use(cors({
exposedHeaders: 'xc-db-response'
}));
server.use(
cors({
exposedHeaders: 'xc-db-response'
})
);
server.set('view engine', 'ejs');
@ -17,16 +19,14 @@ server.set('view engine', 'ejs');
// process.env[`NC_TRY`] = 'true';
// process.env[`NC_DASHBOARD_URL`] = '/test';
process.env[`DEBUG`] = 'xc*';
(async () => {
server.use(await Noco.init({}));
server.listen(process.env.PORT || 8080, () => {
console.log(`App started successfully.\nVisit -> ${Noco.dashboardUrl}`);
})
})().catch(e => console.log(e))
});
})().catch(e => console.log(e));
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd

38
packages/nocodb/src/example/try.ts

@ -1,38 +1,42 @@
import cors from 'cors';
import express from 'express';
import {NcConfigFactory, Noco} from "../lib";
process.env.DATABASE_URL = 'mysql://root:password@localhost:3306/sakila'
import { NcConfigFactory, Noco } from '../lib';
process.env.DATABASE_URL = 'mysql://root:password@localhost:3306/sakila';
const url = NcConfigFactory.extractXcUrlFromJdbc(process.env.DATABASE_URL);
process.env.NC_DB = url;
(async () => {
const server = express();
server.use(cors());
server.set('view engine', 'ejs');
const app = new Noco();
server.use(await app.init({
const server = express();
server.use(cors());
server.set('view engine', 'ejs');
const app = new Noco();
server.use(
await app.init({
async afterMetaMigrationInit(): Promise<void> {
if (!(await app.ncMeta.projectList())?.length) {
const config = NcConfigFactory.makeProjectConfigFromUrl(url);
const project = await app.ncMeta.projectCreate(config.title, config, '');
const project = await app.ncMeta.projectCreate(
config.title,
config,
''
);
await app.ncMeta.projectStatusUpdate(config.title, 'started');
await app.ncMeta.projectAddUser(project.id, 1, 'owner,creator');
}
}
}));
server.listen(process.env.PORT || 8080, () => {
console.log(`App started successfully.\nVisit -> http://localhost:${process.env.PORT || 8080}/xc`);
})
}
)().catch(e => console.log(e))
);
server.listen(process.env.PORT || 8080, () => {
console.log(
`App started successfully.\nVisit -> http://localhost:${process.env
.PORT || 8080}/xc`
);
});
})().catch(e => console.log(e));
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd

19
packages/nocodb/src/interface/IEmailAdapter.ts

@ -1,21 +1,18 @@
export default interface IEmailAdapter {
init(): Promise<any>
mailSend(mail:XcEmail): Promise<any>
test(email): Promise<boolean>
init(): Promise<any>;
mailSend(mail: XcEmail): Promise<any>;
test(email): Promise<boolean>;
}
interface XcEmail {
// from?:string;
to:string;
subject:string;
html?:string;
text?:string;
to: string;
subject: string;
html?: string;
text?: string;
}
export {
XcEmail
}
export { XcEmail };
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd

15
packages/nocodb/src/interface/IStorageAdapter.ts

@ -1,12 +1,11 @@
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>
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;
@ -14,9 +13,7 @@ interface XcFile {
size: number | string;
}
export {
XcFile
}
export { XcFile };
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd

5
packages/nocodb/src/interface/XcDynamicChanges.ts

@ -1,13 +1,12 @@
export default interface XcDynamicChanges {
onTableCreate(tn: string): Promise<void>;
onTableUpdate(changeObj:any):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>;
onMetaUpdate(tn: string): Promise<void>;
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*

6
packages/nocodb/src/interface/XcMetaMgr.ts

@ -1,9 +1,5 @@
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export default interface XcMetaMgr {
}
export default interface XcMetaMgr {}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*

163
packages/nocodb/src/interface/config.ts

@ -1,6 +1,6 @@
import {Handler} from "express";
import { Handler } from 'express';
import * as e from 'express';
import Knex from "knex";
import Knex from 'knex';
export interface Route {
path: string;
@ -13,7 +13,6 @@ export interface Route {
functions?: string[];
}
export enum RouteType {
GET = 'get',
POST = 'post',
@ -24,30 +23,30 @@ export enum RouteType {
OPTIONS = 'options'
}
type InflectionTypes = 'pluralize' |
'singularize' |
'inflect' |
'camelize' |
'underscore' |
'humanize' |
'capitalize' |
'dasherize' |
'titleize' |
'demodulize' |
'tableize' |
'classify' |
'foreign_key' |
'ordinalize' |
'transform' | 'none' ;
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';
@ -73,7 +72,7 @@ export interface DbConfig extends Knex.Config {
type: 'rest' | 'graphql' | 'grpc';
prefix: string;
swagger?: boolean;
graphiql?: boolean
graphiql?: boolean;
graphqlDepthLimit?: number;
};
@ -86,15 +85,14 @@ export interface DbConfig extends Knex.Config {
print?: boolean;
explain?: boolean;
measure?: boolean;
},
};
reset?: boolean;
dbtype?: "vitess" | string;
dbtype?: 'vitess' | string;
pluralize?: boolean;
inflection?: {
tn?: InflectionTypes;
cn?: InflectionTypes;
}
};
};
}
@ -117,19 +115,19 @@ export interface AuthConfig {
secret: string;
[key: string]: any;
dbAlias?: string;
options?: JwtOptions
},
options?: JwtOptions;
};
masterKey?: {
secret: string
secret: string;
};
middleware?: {
url: string;
},
};
disabled?: boolean;
}
export interface MiddlewareConfig {
handler?: (...args: any[]) => any
handler?: (...args: any[]) => any;
}
export interface ACLConfig {
@ -138,31 +136,30 @@ export interface ACLConfig {
}
export interface MailerConfig {
[key: string]: any
[key: string]: any;
}
export interface ServerlessConfig {
aws?: {
lambda: boolean
lambda: boolean;
};
gcp?: {
cloudFunction: boolean
cloudFunction: boolean;
};
azure?: {
cloudFunctionApp: boolean
cloudFunctionApp: boolean;
};
zeit?: {
now: boolean
now: boolean;
};
alibaba?: {
functionCompute: boolean
functionCompute: boolean;
};
serverlessFramework?: {
http: boolean
http: boolean;
};
}
export interface NcGui {
path?: string;
disabled?: boolean;
@ -177,12 +174,11 @@ export interface NcConfig {
envs: {
[key: string]: {
db: DbConfig[],
api?: any,
db: DbConfig[];
api?: any;
publicUrl?: string;
}
}
};
};
// dbs: DbConfig[];
@ -197,20 +193,19 @@ export interface NcConfig {
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",
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
},
db?: any;
};
api?: any;
gui?: NcGui;
try?: boolean;
@ -219,51 +214,52 @@ export interface NcConfig {
prefix?: string;
publicUrl?: string;
}
export interface Event {
title: string;
tn: string;
url
headers
operation
event
retry
max
interval
timeout
url;
headers;
operation;
event;
retry;
max;
interval;
timeout;
}
export interface Acl {
[role: string]: {
create: boolean | ColumnAcl
[key: string]: boolean | ColumnAcl
} | boolean |any
[role: string]:
| {
create: boolean | ColumnAcl;
[key: string]: boolean | ColumnAcl;
}
| boolean
| any;
}
export interface ColumnAcl {
columns: {
[cn: string]: boolean
},
[cn: string]: boolean;
};
assign?: {
[cn: string]: any
}
[cn: string]: any;
};
}
export interface Acls {
[tn: string]: Acl
[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"
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 {
@ -276,12 +272,8 @@ export class Result {
this.message = message;
this.data = data;
}
}
enum HTTPType {
GET = 'get',
POST = 'post',
@ -290,7 +282,6 @@ enum HTTPType {
PATCH = 'patch',
HEAD = 'head',
OPTIONS = 'options'
}
export interface XcRoute {

8
packages/nocodb/src/lib/dataMapper/index.ts

@ -1,9 +1,9 @@
export {DbFactory} from './lib/DbFactory';
export {BaseModelSql} from './lib/sql/BaseModelSql';
export { DbFactory } from './lib/DbFactory';
export { BaseModelSql } from './lib/sql/BaseModelSql';
import XKnex, {Knex} from './lib/sql/CustomKnex';
import XKnex, { Knex } from './lib/sql/CustomKnex';
export {XKnex, Knex};
export { XKnex, Knex };
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*

670
packages/nocodb/src/lib/dataMapper/lib/BaseModel.ts

File diff suppressed because it is too large Load Diff

12
packages/nocodb/src/lib/dataMapper/lib/DbFactory.ts

@ -2,12 +2,14 @@ import knex from './sql/CustomKnex';
export class DbFactory {
static create(connectionConfig) {
if (connectionConfig.client === "sqlite3") {
return knex(connectionConfig.connection)
} else if (['mysql', 'mysql2', 'pg', 'mssql'].includes(connectionConfig.client)) {
return knex(connectionConfig)
if (connectionConfig.client === 'sqlite3') {
return knex(connectionConfig.connection);
} else if (
['mysql', 'mysql2', 'pg', 'mssql'].includes(connectionConfig.client)
) {
return knex(connectionConfig);
}
throw new Error("Database not supported");
throw new Error('Database not supported');
}
}
/**

794
packages/nocodb/src/lib/dataMapper/lib/sql/CustomKnex.ts

File diff suppressed because it is too large Load Diff

103
packages/nocodb/src/lib/dataMapper/lib/sql/formulaQueryBuilderFromString.ts

@ -1,30 +1,37 @@
import jsep from 'jsep';
import mapFunctionName from "./mapFunctionName";
import mapFunctionName from './mapFunctionName';
// todo: switch function based on database
export function formulaQueryBuilderFromString(str, alias, knex) {
return formulaQueryBuilder(jsep(str), alias, knex)
return formulaQueryBuilder(jsep(str), alias, knex);
}
export default function formulaQueryBuilder(tree, alias, knex, aliasToColumn = {}) {
const fn = (pt, a?, prevBinaryOp ?) => {
export default function formulaQueryBuilder(
tree,
alias,
knex,
aliasToColumn = {}
) {
const fn = (pt, a?, prevBinaryOp?) => {
const colAlias = a ? ` as ${a}` : '';
if (pt.type === 'CallExpression') {
switch (pt.callee.name) {
case 'ADD':
case 'SUM':
if (pt.arguments.length > 1) {
return fn({
type: 'BinaryExpression',
operator: '+',
left: pt.arguments[0],
right: {...pt, arguments: pt.arguments.slice(1)}
}, a, prevBinaryOp)
return fn(
{
type: 'BinaryExpression',
operator: '+',
left: pt.arguments[0],
right: { ...pt, arguments: pt.arguments.slice(1) }
},
a,
prevBinaryOp
);
} else {
return fn(pt.arguments[0], a, prevBinaryOp)
return fn(pt.arguments[0], a, prevBinaryOp);
}
break;
// case 'AVG':
@ -42,54 +49,72 @@ export default function formulaQueryBuilder(tree, alias, knex, aliasToColumn = {
case 'CONCAT':
if (knex.clientType() === 'sqlite3') {
if (pt.arguments.length > 1) {
return fn({
type: 'BinaryExpression',
operator: '||',
left: pt.arguments[0],
right: {...pt, arguments: pt.arguments.slice(1)}
}, a, prevBinaryOp)
return fn(
{
type: 'BinaryExpression',
operator: '||',
left: pt.arguments[0],
right: { ...pt, arguments: pt.arguments.slice(1) }
},
a,
prevBinaryOp
);
} else {
return fn(pt.arguments[0], a, prevBinaryOp)
return fn(pt.arguments[0], a, prevBinaryOp);
}
}
break;
default: {
const res = mapFunctionName({pt, knex, alias, a, aliasToCol: aliasToColumn, fn, colAlias, prevBinaryOp})
if (res) return res;
}
break
default:
{
const res = mapFunctionName({
pt,
knex,
alias,
a,
aliasToCol: aliasToColumn,
fn,
colAlias,
prevBinaryOp
});
if (res) return res;
}
break;
}
return knex.raw(`${pt.callee.name}(${pt.arguments.map(arg => fn(arg).toQuery()).join()})${colAlias}`)
return knex.raw(
`${pt.callee.name}(${pt.arguments
.map(arg => fn(arg).toQuery())
.join()})${colAlias}`
);
} else if (pt.type === 'Literal') {
return knex.raw(`?${colAlias}`, [pt.value]);
} else if (pt.type === 'Identifier') {
return knex.raw(`??${colAlias}`, [aliasToColumn[pt.name] || pt.name]);
} else if (pt.type === 'BinaryExpression') {
if (pt.operator === '==') {
pt.operator = '='
pt.operator = '=';
}
if (pt.operator === '/') {
pt.left = {
callee: {name: 'FLOAT'},
callee: { name: 'FLOAT' },
type: 'CallExpression',
arguments: [
pt.left
]
}
arguments: [pt.left]
};
}
const query = knex.raw(`${fn(pt.left, null, pt.operator).toQuery()} ${pt.operator} ${fn(pt.right, null, pt.operator).toQuery()}${colAlias}`)
const query = knex.raw(
`${fn(pt.left, null, pt.operator).toQuery()} ${pt.operator} ${fn(
pt.right,
null,
pt.operator
).toQuery()}${colAlias}`
);
if (prevBinaryOp && pt.operator !== prevBinaryOp) {
query.wrap('(', ')')
query.wrap('(', ')');
}
return query;
}
};
return fn(tree, alias)
return fn(tree, alias);
}

80
packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/commonFns.ts

@ -1,49 +1,87 @@
import {MapFnArgs} from "../mapFunctionName";
import { MapFnArgs } from '../mapFunctionName';
export default {
// todo: handle default case
SWITCH: (args: MapFnArgs) => {
const count = Math.floor((args.pt.arguments.length - 1) / 2)
const count = Math.floor((args.pt.arguments.length - 1) / 2);
let query = '';
const switchVal = args.fn(args.pt.arguments[0]).toQuery();
for (let i = 0; i < count; i++) {
query += args.knex.raw(`\n\tWHEN ${args.fn(args.pt.arguments[i * 2 + 1]).toQuery()} THEN ${args.fn(args.pt.arguments[i * 2 + 2]).toQuery()}`).toQuery()
query += args.knex
.raw(
`\n\tWHEN ${args
.fn(args.pt.arguments[i * 2 + 1])
.toQuery()} THEN ${args.fn(args.pt.arguments[i * 2 + 2]).toQuery()}`
)
.toQuery();
}
if (args.pt.arguments.length % 2 === 0) {
query += args.knex.raw(`\n\tELSE ${args.fn(args.pt.arguments[args.pt.arguments.length - 1]).toQuery()}`).toQuery()
query += args.knex
.raw(
`\n\tELSE ${args
.fn(args.pt.arguments[args.pt.arguments.length - 1])
.toQuery()}`
)
.toQuery();
}
return args.knex.raw(`CASE ${switchVal} ${query}\n END${args.colAlias}`)
return args.knex.raw(`CASE ${switchVal} ${query}\n END${args.colAlias}`);
},
IF: (args: MapFnArgs) => {
let query = args.knex.raw(`\n\tWHEN ${args.fn(args.pt.arguments[0]).toQuery()} THEN ${args.fn(args.pt.arguments[1]).toQuery()}`).toQuery();
let query = args.knex
.raw(
`\n\tWHEN ${args.fn(args.pt.arguments[0]).toQuery()} THEN ${args
.fn(args.pt.arguments[1])
.toQuery()}`
)
.toQuery();
if (args.pt.arguments[2]) {
query += args.knex.raw(`\n\tELSE ${args.fn(args.pt.arguments[2]).toQuery()}`).toQuery()
query += args.knex
.raw(`\n\tELSE ${args.fn(args.pt.arguments[2]).toQuery()}`)
.toQuery();
}
return args.knex.raw(`CASE ${query}\n END${args.colAlias}`)
return args.knex.raw(`CASE ${query}\n END${args.colAlias}`);
},
TRUE: (_args) => 1,
FALSE: (_args) => 0,
TRUE: _args => 1,
FALSE: _args => 0,
AND: (args: MapFnArgs) => {
return args.knex.raw(`${args.knex.raw(`${args.pt.arguments.map(ar => args.fn(ar).toQuery()).join(' AND ')}`).wrap('(', ')').toQuery()}${args.colAlias}`)
return args.knex.raw(
`${args.knex
.raw(
`${args.pt.arguments.map(ar => args.fn(ar).toQuery()).join(' AND ')}`
)
.wrap('(', ')')
.toQuery()}${args.colAlias}`
);
},
OR: (args: MapFnArgs) => {
return args.knex.raw(`${args.knex.raw(`${args.pt.arguments.map(ar => args.fn(ar).toQuery()).join(' OR ')}`).wrap('(', ')').toQuery()}${args.colAlias}`)
return args.knex.raw(
`${args.knex
.raw(
`${args.pt.arguments.map(ar => args.fn(ar).toQuery()).join(' OR ')}`
)
.wrap('(', ')')
.toQuery()}${args.colAlias}`
);
},
AVG: (args: MapFnArgs) => {
if (args.pt.arguments.length > 1) {
return args.fn({
type: 'BinaryExpression',
operator: '/',
left: {...args.pt, callee: {name: 'SUM'}},
right: {type: 'Literal', value: args.pt.arguments.length}
}, args.a, args.prevBinaryOp)
return args.fn(
{
type: 'BinaryExpression',
operator: '/',
left: { ...args.pt, callee: { name: 'SUM' } },
right: { type: 'Literal', value: args.pt.arguments.length }
},
args.a,
args.prevBinaryOp
);
} else {
return args.fn(args.pt.arguments[0], args.a, args.prevBinaryOp)
return args.fn(args.pt.arguments[0], args.a, args.prevBinaryOp);
}
},
FLOAT: (args: MapFnArgs) => {
return args.fn(args.pt?.arguments?.[0]).wrap('(',')');
return args.fn(args.pt?.arguments?.[0]).wrap('(', ')');
}
}
};

63
packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/mssql.ts

@ -1,59 +1,86 @@
import {MapFnArgs} from "../mapFunctionName";
import commonFns from "./commonFns";
import { MapFnArgs } from '../mapFunctionName';
import commonFns from './commonFns';
const mssql = {
...commonFns,
MIN: (args: MapFnArgs) => {
if (args.pt.arguments.length === 1) {
return args.fn(args.pt.arguments[0])
return args.fn(args.pt.arguments[0]);
}
let query = '';
for (const [i, arg] of Object.entries(args.pt.arguments)) {
if (+i === args.pt.arguments.length - 1) {
query += args.knex.raw(`\n\tElse ${args.fn(arg).toQuery()}`).toQuery()
query += args.knex.raw(`\n\tElse ${args.fn(arg).toQuery()}`).toQuery();
} else {
query += args.knex.raw(`\n\tWhen ${args.pt.arguments.filter((_, j) => +i !== j).map(arg1 => `${args.fn(arg).toQuery()} < ${args.fn(arg1).toQuery()}`).join(' And ')} Then ${args.fn(arg).toQuery()}`).toQuery()
query += args.knex
.raw(
`\n\tWhen ${args.pt.arguments
.filter((_, j) => +i !== j)
.map(
arg1 => `${args.fn(arg).toQuery()} < ${args.fn(arg1).toQuery()}`
)
.join(' And ')} Then ${args.fn(arg).toQuery()}`
)
.toQuery();
}
}
return args.knex.raw(`Case ${query}\n End${args.colAlias}`)
return args.knex.raw(`Case ${query}\n End${args.colAlias}`);
},
MAX: (args: MapFnArgs) => {
if (args.pt.arguments.length === 1) {
return args.fn(args.pt.arguments[0])
return args.fn(args.pt.arguments[0]);
}
let query = '';
for (const [i, arg] of Object.entries(args.pt.arguments)) {
if (+i === args.pt.arguments.length - 1) {
query += args.knex.raw(`\nElse ${args.fn(arg).toQuery()}`).toQuery()
query += args.knex.raw(`\nElse ${args.fn(arg).toQuery()}`).toQuery();
} else {
query += args.knex.raw(`\nWhen ${args.pt.arguments.filter((_, j) => +i !== j).map(arg1 => `${args.fn(arg).toQuery()} > ${args.fn(arg1).toQuery()}`).join(' And ')} Then ${args.fn(arg).toQuery()}`).toQuery()
query += args.knex
.raw(
`\nWhen ${args.pt.arguments
.filter((_, j) => +i !== j)
.map(
arg1 => `${args.fn(arg).toQuery()} > ${args.fn(arg1).toQuery()}`
)
.join(' And ')} Then ${args.fn(arg).toQuery()}`
)
.toQuery();
}
}
return args.knex.raw(`Case ${query}\n End${args.colAlias}`)
return args.knex.raw(`Case ${query}\n End${args.colAlias}`);
},
MOD: (pt) => {
MOD: pt => {
Object.assign(pt, {
type: 'BinaryExpression',
operator: '%',
left: pt.arguments[0],
right: pt.arguments[1]
})
});
},
REPEAT: 'REPLICATE',
NOW: 'getdate',
SEARCH: (args: MapFnArgs) => {
args.pt.callee.name = 'CHARINDEX';
const temp = args.pt.arguments[0]
args.pt.arguments[0] = args.pt.arguments[1]
const temp = args.pt.arguments[0];
args.pt.arguments[0] = args.pt.arguments[1];
args.pt.arguments[1] = temp;
},
INT: (args: MapFnArgs) => {
return args.knex.raw(`CASE WHEN ISNUMERIC(${args.fn(args.pt.arguments[0]).toQuery()}) = 1 THEN FLOOR(${args.fn(args.pt.arguments[0]).toQuery()}) ELSE 0 END${args.colAlias}`)
return args.knex.raw(
`CASE WHEN ISNUMERIC(${args
.fn(args.pt.arguments[0])
.toQuery()}) = 1 THEN FLOOR(${args
.fn(args.pt.arguments[0])
.toQuery()}) ELSE 0 END${args.colAlias}`
);
},
MID:'SUBSTR',
MID: 'SUBSTR',
FLOAT: (args: MapFnArgs) => {
return args.knex.raw(`CAST(${args.fn(args.pt.arguments[0])} as FLOAT)${args.colAlias}`).wrap('(',')')
}}
return args.knex
.raw(`CAST(${args.fn(args.pt.arguments[0])} as FLOAT)${args.colAlias}`)
.wrap('(', ')');
}
};
export default mssql;

41
packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/mysql.ts

@ -1,6 +1,5 @@
import {MapFnArgs} from "../mapFunctionName";
import commonFns from "./commonFns";
import { MapFnArgs } from '../mapFunctionName';
import commonFns from './commonFns';
const mysql2 = {
...commonFns,
@ -9,25 +8,35 @@ const mysql2 = {
MAX: 'GREATEST',
SEARCH: (args: MapFnArgs) => {
args.pt.callee.name = 'LOCATE';
const temp = args.pt.arguments[0]
args.pt.arguments[0] = args.pt.arguments[1]
const temp = args.pt.arguments[0];
args.pt.arguments[0] = args.pt.arguments[1];
args.pt.arguments[1] = temp;
},
INT:(args: MapFnArgs) =>{
return args.knex.raw(`CAST(${args.fn(args.pt.arguments[0])} as SIGNED)${args.colAlias}`)
INT: (args: MapFnArgs) => {
return args.knex.raw(
`CAST(${args.fn(args.pt.arguments[0])} as SIGNED)${args.colAlias}`
);
},
LEFT:(args: MapFnArgs)=> {
return args.knex.raw(`SUBSTR(${args.fn(args.pt.arguments[0])},1,${args.fn(args.pt.arguments[1])})${args.colAlias}`)
LEFT: (args: MapFnArgs) => {
return args.knex.raw(
`SUBSTR(${args.fn(args.pt.arguments[0])},1,${args.fn(
args.pt.arguments[1]
)})${args.colAlias}`
);
},
RIGHT:(args: MapFnArgs)=> {
return args.knex.raw(`SUBSTR(${args.fn(args.pt.arguments[0])},-${args.fn(args.pt.arguments[1])})${args.colAlias}`)
RIGHT: (args: MapFnArgs) => {
return args.knex.raw(
`SUBSTR(${args.fn(args.pt.arguments[0])},-${args.fn(
args.pt.arguments[1]
)})${args.colAlias}`
);
},
MID:'SUBSTR',
MID: 'SUBSTR',
FLOAT: (args: MapFnArgs) => {
return args.knex.raw(`CAST(${args.fn(args.pt.arguments[0])} as DOUBLE)${args.colAlias}`).wrap('(',')')
return args.knex
.raw(`CAST(${args.fn(args.pt.arguments[0])} as DOUBLE)${args.colAlias}`)
.wrap('(', ')');
}
}
};
export default mysql2;

29
packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/pg.ts

@ -1,5 +1,5 @@
import {MapFnArgs} from "../mapFunctionName";
import commonFns from "./commonFns";
import { MapFnArgs } from '../mapFunctionName';
import commonFns from './commonFns';
const pg = {
...commonFns,
@ -11,17 +11,32 @@ const pg = {
POWER: 'pow',
SQRT: 'sqrt',
SEARCH: (args: MapFnArgs) => {
return args.knex.raw(`POSITION(${args.knex.raw(args.fn(args.pt.arguments[1]).toQuery())} in ${args.knex.raw(args.fn(args.pt.arguments[0]).toQuery())})${args.colAlias}`)
return args.knex.raw(
`POSITION(${args.knex.raw(
args.fn(args.pt.arguments[1]).toQuery()
)} in ${args.knex.raw(args.fn(args.pt.arguments[0]).toQuery())})${
args.colAlias
}`
);
},
INT(args: MapFnArgs) {
// todo: correction
return args.knex.raw(`REGEXP_REPLACE(COALESCE(${args.fn(args.pt.arguments[0])}::character varying, '0'), '[^0-9]+|\\.[0-9]+' ,'')${args.colAlias}`)
return args.knex.raw(
`REGEXP_REPLACE(COALESCE(${args.fn(
args.pt.arguments[0]
)}::character varying, '0'), '[^0-9]+|\\.[0-9]+' ,'')${args.colAlias}`
);
},
MID: 'SUBSTR',
FLOAT: (args: MapFnArgs) => {
return args.knex.raw(`CAST(${args.fn(args.pt.arguments[0])} as DOUBLE PRECISION)${args.colAlias}`).wrap('(',')')
return args.knex
.raw(
`CAST(${args.fn(args.pt.arguments[0])} as DOUBLE PRECISION)${
args.colAlias
}`
)
.wrap('(', ')');
}
}
};
export default pg;

47
packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/sqlite.ts

@ -1,14 +1,18 @@
import {MapFnArgs} from "../mapFunctionName";
import commonFns from "./commonFns";
import { MapFnArgs } from '../mapFunctionName';
import commonFns from './commonFns';
const sqlite3 = {
...commonFns,
LEN: 'LENGTH',
CEILING(args) {
return args.knex.raw(`round(${args.fn(args.pt.arguments[0])} + 0.5)${args.colAlias}`)
}, FLOOR(args) {
return args.knex.raw(`round(${args.fn(args.pt.arguments[0])} - 0.5)${args.colAlias}`)
return args.knex.raw(
`round(${args.fn(args.pt.arguments[0])} + 0.5)${args.colAlias}`
);
},
FLOOR(args) {
return args.knex.raw(
`round(${args.fn(args.pt.arguments[0])} - 0.5)${args.colAlias}`
);
},
MOD: (args: MapFnArgs) => {
return args.fn({
@ -16,27 +20,42 @@ const sqlite3 = {
operator: '%',
left: args.pt.arguments[0],
right: args.pt.arguments[1]
})
});
},
REPEAT(args: MapFnArgs) {
return args.knex.raw(`replace(printf('%.' || ${args.fn(args.pt.arguments[1])} || 'c', '/'),'/',${args.fn(args.pt.arguments[0])})${args.colAlias}`)
return args.knex.raw(
`replace(printf('%.' || ${args.fn(
args.pt.arguments[1]
)} || 'c', '/'),'/',${args.fn(args.pt.arguments[0])})${args.colAlias}`
);
},
NOW: 'DATE',
SEARCH: 'INSTR',
INT(args: MapFnArgs) {
return args.knex.raw(`CAST(${args.fn(args.pt.arguments[0])} as INTEGER)${args.colAlias}`)
return args.knex.raw(
`CAST(${args.fn(args.pt.arguments[0])} as INTEGER)${args.colAlias}`
);
},
LEFT: (args: MapFnArgs) => {
return args.knex.raw(`SUBSTR(${args.fn(args.pt.arguments[0])},1,${args.fn(args.pt.arguments[1])})${args.colAlias}`)
return args.knex.raw(
`SUBSTR(${args.fn(args.pt.arguments[0])},1,${args.fn(
args.pt.arguments[1]
)})${args.colAlias}`
);
},
RIGHT: (args: MapFnArgs) => {
return args.knex.raw(`SUBSTR(${args.fn(args.pt.arguments[0])},-${args.fn(args.pt.arguments[1])})${args.colAlias}`)
return args.knex.raw(
`SUBSTR(${args.fn(args.pt.arguments[0])},-${args.fn(
args.pt.arguments[1]
)})${args.colAlias}`
);
},
MID: 'SUBSTR',
FLOAT: (args: MapFnArgs) => {
return args.knex.raw(`CAST(${args.fn(args.pt.arguments[0])} as FLOAT)${args.colAlias}`).wrap('(',')')
return args.knex
.raw(`CAST(${args.fn(args.pt.arguments[0])} as FLOAT)${args.colAlias}`)
.wrap('(', ')');
}
}
};
export default sqlite3;

29
packages/nocodb/src/lib/dataMapper/lib/sql/genRollupSelect.ts

@ -1,25 +1,32 @@
import Knex from "knex";
import Knex from 'knex';
export default function ({knex, rollup,}: { knex: Knex, rollup: any }) {
export default function({ knex, rollup }: { knex: Knex; rollup: any }) {
switch (rollup.type) {
case 'hm':
return knex(rollup.rltn)
[rollup.fn]?.(knex.ref(`${rollup.rltn}.${rollup.rlcn}`))
.where(
knex.ref(`${rollup.tn}.${rollup.cn}`), '=', knex.ref(`${rollup.rtn}.${rollup.rcn}`)
)
knex.ref(`${rollup.tn}.${rollup.cn}`),
'=',
knex.ref(`${rollup.rtn}.${rollup.rcn}`)
);
break;
case 'mm':
return knex(rollup.rltn)
[rollup.fn]?.(knex.ref(`${rollup.rltn}.${rollup.rlcn}`))
.innerJoin(rollup.vtn, knex.ref(`${rollup.vtn}.${rollup.vrcn}`), '=', knex.ref(`${rollup.rtn}.${rollup.rcn}`))
.where(knex.ref(`${rollup.vtn}.${rollup.vcn}`), '=', knex.ref(`${rollup.tn}.${rollup.cn}`))
.innerJoin(
rollup.vtn,
knex.ref(`${rollup.vtn}.${rollup.vrcn}`),
'=',
knex.ref(`${rollup.rtn}.${rollup.rcn}`)
)
.where(
knex.ref(`${rollup.vtn}.${rollup.vcn}`),
'=',
knex.ref(`${rollup.tn}.${rollup.cn}`)
);
default:
throw Error(`Unsupported relation type '${rollup.type}'`)
throw Error(`Unsupported relation type '${rollup.type}'`);
}
}

34
packages/nocodb/src/lib/dataMapper/lib/sql/mapFunctionName.ts

@ -1,23 +1,22 @@
import {XKnex} from "../../index";
import mssql from "./functionMappings/mssql";
import mysql from "./functionMappings/mysql";
import pg from "./functionMappings/pg";
import sqlite from "./functionMappings/sqlite";
import {QueryBuilder} from "knex";
import { XKnex } from '../../index';
import mssql from './functionMappings/mssql';
import mysql from './functionMappings/mysql';
import pg from './functionMappings/pg';
import sqlite from './functionMappings/sqlite';
import { QueryBuilder } from 'knex';
export interface MapFnArgs {
pt: any,
aliasToCol: { [alias: string]: string },
knex: XKnex,
alias: string,
a?: string,
fn: (...args: any) => QueryBuilder | any,
colAlias: string,
prevBinaryOp?: any
pt: any;
aliasToCol: { [alias: string]: string };
knex: XKnex;
alias: string;
a?: string;
fn: (...args: any) => QueryBuilder | any;
colAlias: string;
prevBinaryOp?: any;
}
const mapFunctionName = (args: MapFnArgs): any => {
const name = args.pt.callee.name;
let val;
@ -40,11 +39,10 @@ const mapFunctionName = (args: MapFnArgs): any => {
}
if (typeof val === 'function') {
return val(args)
return val(args);
} else if (typeof val === 'string') {
args.pt.callee.name = val;
}
}
};
export default mapFunctionName;

9
packages/nocodb/src/lib/index.ts

@ -1,15 +1,10 @@
import Noco from './noco/Noco'
import Noco from './noco/Noco';
import XcTry from './noco/nc.try';
import NcConfigFactory from './utils/NcConfigFactory';
export default Noco;
export {
Noco,
NcConfigFactory,
XcTry
};
export { Noco, NcConfigFactory, XcTry };
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd

660
packages/nocodb/src/lib/migrator/SqlMigrator/lib/KnexMigrator.ts

File diff suppressed because it is too large Load Diff

5
packages/nocodb/src/lib/migrator/SqlMigrator/lib/SqlMigrator.ts

@ -5,9 +5,7 @@ export default class SqlMigrator {
this.project = null;
}
init(_project = null) {
}
init(_project = null) {}
// migrationsInit() {
//
@ -36,7 +34,6 @@ export default class SqlMigrator {
// migrationsDelete() {
//
// }
}
/**

16
packages/nocodb/src/lib/migrator/SqlMigrator/lib/SqlMigratorFactory.ts

@ -1,18 +1,18 @@
import KnexMigrator from "./KnexMigrator";
import KnexMigrator from './KnexMigrator';
export default class SqlMigratorFactory {
static create(args) {
switch (args.client) {
case "mysql":
case "mysql2":
case "pg":
case "oracledb":
case "mssql":
case "sqlite3":
case 'mysql':
case 'mysql2':
case 'pg':
case 'oracledb':
case 'mssql':
case 'sqlite3':
return new KnexMigrator();
break;
default:
throw new Error("Database not supported");
throw new Error('Database not supported');
break;
}
}

42
packages/nocodb/src/lib/migrator/SqlMigrator/lib/templates/mssql.template.ts

@ -1,48 +1,48 @@
module.exports = {
title: "default",
title: 'default',
envs: {
_noco: {
api:{},
api: {},
db: [
{
client: "mssql",
client: 'mssql',
connection: {
host: process.env.DOCKER_DB_HOST || "localhost",
port: process.env.DOCKER_DB_PORT ? parseInt(process.env.DOCKER_DB_PORT,10) : null || 1433,
user: "sa",
password: "Password123.",
database: "default_dev"
host: process.env.DOCKER_DB_HOST || 'localhost',
port: process.env.DOCKER_DB_PORT
? parseInt(process.env.DOCKER_DB_PORT, 10)
: null || 1433,
user: 'sa',
password: 'Password123.',
database: 'default_dev'
},
meta: {
tn: "nc_evolutions",
dbAlias: "primary"
tn: 'nc_evolutions',
dbAlias: 'primary'
}
}
]
},
test: {
api:{},
api: {},
db: [
{
client: "mssql",
client: 'mssql',
connection: {
host: DOCKER_DB_HOST || "localhost",
host: DOCKER_DB_HOST || 'localhost',
port: DOCKER_DB_PORT ? parseInt(DOCKER_DB_PORT) : null || 1433,
user: "sa",
password: "Password123.",
database: "default_test"
user: 'sa',
password: 'Password123.',
database: 'default_test'
},
meta: {
tn: "nc_evolutions",
dbAlias: "primary"
tn: 'nc_evolutions',
dbAlias: 'primary'
}
}
]
}
},
workingEnv: "_noco",
workingEnv: '_noco',
meta: {
version: '0.5',
seedsFolder: 'seeds',

34
packages/nocodb/src/lib/migrator/SqlMigrator/lib/templates/mysql.template.ts

@ -1,22 +1,20 @@
module.exports = {
title: "default",
title: 'default',
envs: {
_noco: {
db: [
{
client: "mysql2",
client: 'mysql2',
connection: {
host: process.env.DOCKER_DB_HOST || "localhost",
host: process.env.DOCKER_DB_HOST || 'localhost',
port: process.env.DOCKER_DB_PORT || 3306,
user: "root",
password: "password",
database: "default_dev"
user: 'root',
password: 'password',
database: 'default_dev'
},
meta: {
tn: "nc_evolutions",
dbAlias: "primary"
tn: 'nc_evolutions',
dbAlias: 'primary'
}
}
]
@ -25,23 +23,23 @@ module.exports = {
api: {},
db: [
{
client: "mysql2",
client: 'mysql2',
connection: {
host: DOCKER_DB_HOST || "localhost",
host: DOCKER_DB_HOST || 'localhost',
port: DOCKER_DB_PORT || 3306,
user: "root",
password: "password",
database: "default_test"
user: 'root',
password: 'password',
database: 'default_test'
},
meta: {
tn: "nc_evolutions",
dbAlias: "primary"
tn: 'nc_evolutions',
dbAlias: 'primary'
}
}
]
}
},
workingEnv: "_noco",
workingEnv: '_noco',
meta: {
version: '0.5',
seedsFolder: 'seeds',

34
packages/nocodb/src/lib/migrator/SqlMigrator/lib/templates/pg.template.ts

@ -1,22 +1,22 @@
const {DOCKER_DB_HOST, DOCKER_DB_PORT} = process.env;
const { DOCKER_DB_HOST, DOCKER_DB_PORT } = process.env;
module.exports = {
title: "default",
title: 'default',
envs: {
_noco: {
db: [
{
client: "pg",
client: 'pg',
connection: {
host: DOCKER_DB_HOST || "localhost",
host: DOCKER_DB_HOST || 'localhost',
port: DOCKER_DB_PORT || 5432,
user: "postgres",
password: "password",
database: "default_dev"
user: 'postgres',
password: 'password',
database: 'default_dev'
},
meta: {
tn: "nc_evolutions",
dbAlias: "primary"
tn: 'nc_evolutions',
dbAlias: 'primary'
}
}
]
@ -24,23 +24,23 @@ module.exports = {
test: {
db: [
{
client: "pg",
client: 'pg',
connection: {
host: DOCKER_DB_HOST || "localhost",
host: DOCKER_DB_HOST || 'localhost',
port: DOCKER_DB_PORT || 5432,
user: "postgres",
password: "password",
database: "default_test"
user: 'postgres',
password: 'password',
database: 'default_test'
},
meta: {
tn: "nc_evolutions",
dbAlias: "primary"
tn: 'nc_evolutions',
dbAlias: 'primary'
}
}
]
}
},
workingEnv: "_noco",
workingEnv: '_noco',
meta: {
version: '0.5',
seedsFolder: 'seeds',

28
packages/nocodb/src/lib/migrator/SqlMigrator/lib/templates/sqlite.template.ts

@ -1,26 +1,26 @@
import path from "path";
import path from 'path';
const {DOCKER_DB_FILE} = process.env;
const { DOCKER_DB_FILE } = process.env;
module.exports = {
title: "default",
title: 'default',
envs: {
_noco: {
db: [
{
client: "sqlite3",
client: 'sqlite3',
connection: {
client: "sqlite3",
client: 'sqlite3',
connection: {
filename:
DOCKER_DB_FILE ||
`${path.join(process.cwd(), "xmigrator", "default_dev.db")}`
`${path.join(process.cwd(), 'xmigrator', 'default_dev.db')}`
},
useNullAsDefault: true
},
meta: {
tn: "nc_evolutions",
dbAlias: "primary"
tn: 'nc_evolutions',
dbAlias: 'primary'
}
}
]
@ -29,25 +29,25 @@ module.exports = {
api: {},
db: [
{
client: "sqlite3",
client: 'sqlite3',
connection: {
client: "sqlite3",
client: 'sqlite3',
connection: {
filename:
DOCKER_DB_FILE ||
`${path.join(process.cwd(), "xmigrator", "default_test.db")}`
`${path.join(process.cwd(), 'xmigrator', 'default_test.db')}`
},
useNullAsDefault: true
},
meta: {
tn: "nc_evolutions",
dbAlias: "primary"
tn: 'nc_evolutions',
dbAlias: 'primary'
}
}
]
}
},
workingEnv: "_noco",
workingEnv: '_noco',
meta: {
version: '0.5',
seedsFolder: 'seeds',

37
packages/nocodb/src/lib/migrator/util/Debug.ts

@ -1,17 +1,16 @@
import boxen from "boxen";
import debug from "debug";
import boxen from 'boxen';
import debug from 'debug';
import("colors");
import DebugMgr from "./DebugMgr";
import('colors');
import DebugMgr from './DebugMgr';
export default class Debug {
public namespace:any;
public api:any;
public warn:any;
public info:any;
public error:any;
public debug:any;
public namespace: any;
public api: any;
public warn: any;
public info: any;
public error: any;
public debug: any;
constructor(namespace) {
this.namespace = namespace;
@ -25,25 +24,23 @@ export default class Debug {
}
ppException(e, func = null) {
let log = "";
log += ` EXCEPTION OCCURED!! in ${
this.namespace.red.bold
} @ ${func}`;
log += "\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n"
let log = '';
log += ` EXCEPTION OCCURED!! in ${this.namespace.red.bold} @ ${func}`;
log += '\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n'
.red.bold;
log += `MESSAGE:\n`.yellow.bold;
log += `${e.message}\n`.yellow.bold;
log += "\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n"
log += '\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n'
.red.bold;
log += `CODE:\n`.yellow.bold;
log += `${e.code}\n`.yellow.bold;
log += "\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n"
log += '\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n'
.red.bold;
log += `STACK:\n`.yellow.bold;
log += `${e.stack}\n`.yellow.bold;
log += "\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n"
log += '\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n'
.red.bold;
console.log(boxen(log, { padding: 1, borderStyle: "double" }));
console.log(boxen(log, { padding: 1, borderStyle: 'double' }));
console.log(e);
return log;
}

24
packages/nocodb/src/lib/migrator/util/DebugMgr.ts

@ -1,13 +1,13 @@
import debug from "debug";
import debug from 'debug';
const namespaces = {};
const levels = {
api: "A",
info: "I",
error: "E",
warn: "W",
debug: "D"
api: 'A',
info: 'I',
error: 'E',
warn: 'W',
debug: 'D'
};
export default class DebugMgr {
static _create(namespace) {
@ -31,23 +31,23 @@ export default class DebugMgr {
namespaces[namespace] = {};
namespaces[namespace][`${namespace}_A`] = {
level: "api",
level: 'api',
enabled: debug.enabled(`${namespace}_A`)
};
namespaces[namespace][`${namespace}_W`] = {
level: "warn",
level: 'warn',
enabled: debug.enabled(`${namespace}_W`)
};
namespaces[namespace][`${namespace}_I`] = {
level: "info",
level: 'info',
enabled: debug.enabled(`${namespace}_I`)
};
namespaces[namespace][`${namespace}_E`] = {
level: "error",
level: 'error',
enabled: debug.enabled(`${namespace}_E`)
};
namespaces[namespace][`${namespace}_D`] = {
level: "debug",
level: 'debug',
enabled: debug.enabled(`${namespace}_D`)
};
}
@ -77,7 +77,7 @@ export default class DebugMgr {
static disable(namespace, level) {
const toBeRemoved = `${namespace}_${levels[level]}`;
let list = `${debug.disable()}`;
list = list.replace(toBeRemoved, "");
list = list.replace(toBeRemoved, '');
debug.enable(list);
this.refreshNamespace(namespace);
}

12
packages/nocodb/src/lib/migrator/util/FileCollection.ts

@ -1,11 +1,11 @@
import fs from 'fs';
import { promisify } from "util";
import { promisify } from 'util';
import jsonfile from 'jsonfile';
export default class FileCollection {
public args;
public path;
public args;
public path;
constructor(args) {
this.args = args;
this.path = args.path;
@ -18,7 +18,7 @@ public path;
*/
const exists = await promisify(fs.exists)(this.args.path);
if(!exists) {
if (!exists) {
await promisify(jsonfile.writeFile)(this.args.path, [], {
spaces: 2
});
@ -29,17 +29,13 @@ public path;
return await promisify(jsonfile.readFile)(this.args.path);
}
async write(args) {
await promisify(jsonfile.writeFile)(this.args.path, args.data, {
spaces: 2
});
}
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*

8
packages/nocodb/src/lib/migrator/util/Result.ts

@ -1,8 +1,8 @@
export default class Result {
public code:any;
public message:any;
public data:any;
public object:any;
public code: any;
public message: any;
public data: any;
public object: any;
constructor(code = 0, message = '', data = {}) {
this.code = code;
this.message = message;

4
packages/nocodb/src/lib/migrator/util/emit.ts

@ -1,9 +1,9 @@
import Emittery from "emittery";
import Emittery from 'emittery';
let emitSingleton = null;
export default class Emit {
public evt:any;
public evt: any;
constructor() {
if (emitSingleton) return emitSingleton;

29
packages/nocodb/src/lib/migrator/util/file.help.ts

@ -1,30 +1,19 @@
import dayjs from 'dayjs';
const getUniqFilenamePrefix = function () {
return dayjs().format('YYYYMMDD_HHmmssSSS')
const getUniqFilenamePrefix = function() {
return dayjs().format('YYYYMMDD_HHmmssSSS');
};
const getFilenameForUp = function (prefix) {
return prefix + '.up.sql'
const getFilenameForUp = function(prefix) {
return prefix + '.up.sql';
};
const getFilenameForDown = function (prefix) {
return prefix + '.down.sql'
}
const getFilenameForDown = function(prefix) {
return prefix + '.down.sql';
};
export {
getUniqFilenamePrefix,
getFilenameForUp,
getFilenameForDown
}
;/**
export { getUniqFilenamePrefix, getFilenameForUp, getFilenameForDown };
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>

434
packages/nocodb/src/lib/noco/NcProjectBuilder.ts

@ -1,21 +1,20 @@
import fs from "fs";
import path from "path";
import fs from 'fs';
import path from 'path';
import axios from "axios";
import {Router} from "express";
import {SqlClientFactory, Tele} from 'nc-help';
import axios from 'axios';
import { Router } from 'express';
import { SqlClientFactory, Tele } from 'nc-help';
import {NcConfig} from "../../interface/config";
import { NcConfig } from '../../interface/config';
import Migrator from '../migrator/SqlMigrator/lib/KnexMigrator';
import Noco from "./Noco";
import {GqlApiBuilder} from "./gql/GqlApiBuilder";
import {XCEeError} from "./meta/NcMetaMgr";
import {RestApiBuilder} from "./rest/RestApiBuilder";
import NcConnectionMgr from "./common/NcConnectionMgr";
import Noco from './Noco';
import { GqlApiBuilder } from './gql/GqlApiBuilder';
import { XCEeError } from './meta/NcMetaMgr';
import { RestApiBuilder } from './rest/RestApiBuilder';
import NcConnectionMgr from './common/NcConnectionMgr';
export default class NcProjectBuilder {
public readonly id: string;
public readonly title: string;
public readonly description: string;
@ -38,16 +37,13 @@ export default class NcProjectBuilder {
this.id = project.id;
this.title = project.title;
this.description = project.description;
this._config = {...this.appConfig, ...JSON.parse(project.config)};
this._config = { ...this.appConfig, ...JSON.parse(project.config) };
this.router = Router();
}
}
public async init(isFirstTime?: boolean) {
try {
await this.addAuthHookToMiddleware();
this.startTime = Date.now();
@ -59,13 +55,16 @@ export default class NcProjectBuilder {
/* Create REST APIs / GraphQL Resolvers */
for (const meta of this.apiBuilders) {
let routeInfo;
if (meta instanceof RestApiBuilder) {
console.log(`Creating REST APIs ${meta.getDbType()} - > ${meta.getDbName()}`);
console.log(
`Creating REST APIs ${meta.getDbType()} - > ${meta.getDbName()}`
);
routeInfo = await (meta as RestApiBuilder).init();
} else if (meta instanceof GqlApiBuilder) {
console.log(`Creating GraphQL APIs ${meta.getDbType()} - > ${meta.getDbName()}`);
console.log(
`Creating GraphQL APIs ${meta.getDbType()} - > ${meta.getDbName()}`
);
routeInfo = await (meta as GqlApiBuilder).init();
}
allRoutesInfo.push(routeInfo);
@ -74,32 +73,44 @@ export default class NcProjectBuilder {
this.app.projectRouter.use(`/nc/${this.id}`, this.router);
await this.app.ncMeta.projectStatusUpdate(this.title, 'started');
} catch (e) {
console.log(e);
await this.app.ncMeta.projectStatusUpdate(this.title, 'stopped');
}
}
public async handleRunTimeChanges(data: any): Promise<any> {
const curBuilder = this.apiBuilders.find(builder => {
return (data.req?.dbAlias || data.req?.args?.dbAlias) === builder.getDbAlias();
return (
(data.req?.dbAlias || data.req?.args?.dbAlias) === builder.getDbAlias()
);
});
switch (data?.req?.api) {
case 'xcAuthHookSet':
this.authHook = await this.app.ncMeta.metaGet(this.id, 'db', 'nc_hooks', {
type: 'AUTH_MIDDLEWARE'
});
this.authHook = await this.app.ncMeta.metaGet(
this.id,
'db',
'nc_hooks',
{
type: 'AUTH_MIDDLEWARE'
}
);
break;
case 'xcM2MRelationCreate':
await curBuilder.onManyToManyRelationCreate(data.req.args.parentTable, data.req.args.childTable, data.req.args);
await curBuilder.onManyToManyRelationCreate(
data.req.args.parentTable,
data.req.args.childTable,
data.req.args
);
break;
case 'relationCreate':
await curBuilder.onRelationCreate(data.req.args.parentTable, data.req.args.childTable, data.req.args);
await curBuilder.onRelationCreate(
data.req.args.parentTable,
data.req.args.childTable,
data.req.args
);
this.app.ncMeta.audit(this.id, curBuilder.getDbAlias(), 'nc_audit', {
op_type: 'RELATION',
@ -108,11 +119,17 @@ export default class NcProjectBuilder {
description: `created relation between tables ${data.req.args.childTable} and ${data.req.args.parentTable} `,
ip: data.ctx.req.clientIp
});
console.log(`Added new relation between : ${data.req.args.parentTable} ==> ${data.req.args.childTable}`)
console.log(
`Added new relation between : ${data.req.args.parentTable} ==> ${data.req.args.childTable}`
);
break;
case 'relationDelete':
await curBuilder.onRelationDelete(data.req.args.parentTable, data.req.args.childTable, data.req.args);
await curBuilder.onRelationDelete(
data.req.args.parentTable,
data.req.args.childTable,
data.req.args
);
this.app.ncMeta.audit(this.id, curBuilder.getDbAlias(), 'nc_audit', {
op_type: 'RELATION',
op_sub_type: 'DELETED',
@ -120,29 +137,48 @@ export default class NcProjectBuilder {
description: `deleted relation between tables ${data.req.args.childTable} and ${data.req.args.parentTable} `,
ip: data.ctx.req.clientIp
});
console.log(`Deleted relation between : ${data.req.args.parentTable} ==> ${data.req.args.childTable}`)
console.log(
`Deleted relation between : ${data.req.args.parentTable} ==> ${data.req.args.childTable}`
);
break;
case 'xcVirtualRelationCreate':
await curBuilder.onVirtualRelationCreate(data.req.args.parentTable, data.req.args.childTable);
await curBuilder.onRelationCreate(data.req.args.parentTable, data.req.args.childTable, {
...data.req.args,
virtual: true
});
console.log(`Added new relation between : ${data.req.args.parentTable} ==> ${data.req.args.childTable}`)
await curBuilder.onVirtualRelationCreate(
data.req.args.parentTable,
data.req.args.childTable
);
await curBuilder.onRelationCreate(
data.req.args.parentTable,
data.req.args.childTable,
{
...data.req.args,
virtual: true
}
);
console.log(
`Added new relation between : ${data.req.args.parentTable} ==> ${data.req.args.childTable}`
);
break;
case 'xcVirtualRelationDelete':
await curBuilder.onRelationDelete(data.req.args.parentTable, data.req.args.childTable, {
...data.req.args,
virtual: true
});
console.log(`Added new relation between : ${data.req.args.parentTable} ==> ${data.req.args.childTable}`)
await curBuilder.onRelationDelete(
data.req.args.parentTable,
data.req.args.childTable,
{
...data.req.args,
virtual: true
}
);
console.log(
`Added new relation between : ${data.req.args.parentTable} ==> ${data.req.args.childTable}`
);
break;
case 'xcRelationColumnDelete':
if (data.req.args?.type === 'mm') {
await curBuilder.onManyToManyRelationDelete(data.req.args.parentTable, data.req.args.childTable)
await curBuilder.onManyToManyRelationDelete(
data.req.args.parentTable,
data.req.args.childTable
);
}
break;
@ -154,7 +190,6 @@ export default class NcProjectBuilder {
await curBuilder.loadFormViews();
break;
case 'tableCreate':
await curBuilder.onTableCreate(data.req.args.tn, data.req.args);
@ -164,8 +199,8 @@ export default class NcProjectBuilder {
user: data.user.email,
description: `created table ${data.req.args.tn} with alias ${data.req.args._tn} `,
ip: data.ctx.req.clientIp
})
console.log(`Added new routes for table : ${data.req.args.tn}`)
});
console.log(`Added new routes for table : ${data.req.args.tn}`);
break;
case 'viewCreate':
@ -174,9 +209,10 @@ export default class NcProjectBuilder {
op_type: 'VIEW',
op_sub_type: 'CREATED',
user: data.user.email,
description: `created view ${data.req.args.view_name} `, ip: data.ctx.req.clientIp
})
console.log(`Added new routes for table : ${data.req.args.tn}`)
description: `created view ${data.req.args.view_name} `,
ip: data.ctx.req.clientIp
});
console.log(`Added new routes for table : ${data.req.args.tn}`);
break;
case 'viewUpdate':
@ -185,9 +221,10 @@ export default class NcProjectBuilder {
op_type: 'VIEW',
op_sub_type: 'UPDATED',
user: data.user.email,
description: `updated view ${data.req.args.view_name} `, ip: data.ctx.req.clientIp
})
console.log(`Added new routes for table : ${data.req.args.tn}`)
description: `updated view ${data.req.args.view_name} `,
ip: data.ctx.req.clientIp
});
console.log(`Added new routes for table : ${data.req.args.tn}`);
break;
case 'tableDelete':
@ -196,9 +233,10 @@ export default class NcProjectBuilder {
op_type: 'TABLE',
op_sub_type: 'DELETED',
user: data.user.email,
description: `deleted table ${data.req.args.tn} `, ip: data.ctx.req.clientIp
})
console.log(`Deleted routes for table : ${data.req.args.tn}`)
description: `deleted table ${data.req.args.tn} `,
ip: data.ctx.req.clientIp
});
console.log(`Deleted routes for table : ${data.req.args.tn}`);
break;
case 'tableRename':
@ -210,50 +248,52 @@ export default class NcProjectBuilder {
user: data.user.email,
description: `renamed table ${data.req.args.tn_old} to ${data.req.args.tn} `,
ip: data.ctx.req.clientIp
})
console.log(`Updated routes for table : ${data.req.args.tn}`)
});
console.log(`Updated routes for table : ${data.req.args.tn}`);
break;
case 'xcRoutesHandlerUpdate':
case 'xcResolverHandlerUpdate':
case 'xcRpcHandlerUpdate':
// todo: implement separate function
await curBuilder.onHandlerCodeUpdate(data.req.args.tn);
console.log(`Updated routes handler for table : ${data.req.tn}`)
console.log(`Updated routes handler for table : ${data.req.tn}`);
break;
case 'xcRoutesMiddlewareUpdate':
case 'xcResolverMiddlewareUpdate':
// todo: implement separate function
await curBuilder.onMiddlewareCodeUpdate(data.req.args.tn);
console.log(`Updated routes handler for table : ${data.req.args.tn}`)
console.log(`Updated routes handler for table : ${data.req.args.tn}`);
break;
case 'xcModelSet':
await curBuilder.onMetaUpdate(data.req.args.tn);
console.log(`Updated validations for table : ${data.req.args.tn}`)
console.log(`Updated validations for table : ${data.req.args.tn}`);
break;
case 'xcUpdateVirtualKeyAlias':
await curBuilder.onVirtualColumnAliasUpdate(data.req.args.tn);
console.log(`Updated validations for table : ${data.req.args.tn}`)
console.log(`Updated validations for table : ${data.req.args.tn}`);
break;
case 'xcModelSchemaSet':
await curBuilder.onGqlSchemaUpdate(data.req.args.tn, data.req.args.schema);
console.log(`Updated validations for table : ${data.req.args.tn}`)
await curBuilder.onGqlSchemaUpdate(
data.req.args.tn,
data.req.args.schema
);
console.log(`Updated validations for table : ${data.req.args.tn}`);
break;
case 'tableXcHooksSet':
await curBuilder.onHooksUpdate(data.req.args.tn);
console.log(`Updated validations for table : ${data.req.args.tn}`)
console.log(`Updated validations for table : ${data.req.args.tn}`);
break;
case 'xcModelSwaggerDocSet':
await (curBuilder as RestApiBuilder).onSwaggerDocUpdate(data.req.args.tn);
console.log(`Updated validations for table : ${data.req.args.tn}`)
await (curBuilder as RestApiBuilder).onSwaggerDocUpdate(
data.req.args.tn
);
console.log(`Updated validations for table : ${data.req.args.tn}`);
break;
case 'tableUpdate':
@ -264,40 +304,40 @@ export default class NcProjectBuilder {
user: data.user.email,
description: `updated table ${data.req.args.tn} with alias ${data.req.args._tn} `,
ip: data.ctx.req.clientIp
})
console.log(`Updated validations for table : ${data.req.args.tn}`)
});
console.log(`Updated validations for table : ${data.req.args.tn}`);
break;
case 'procedureCreate':
await curBuilder.onProcedureCreate(data.req.args.procedure_name)
await curBuilder.onProcedureCreate(data.req.args.procedure_name);
break;
case 'functionUpdate':
await curBuilder.onFunctionDelete(data.req.args.function_name)
await curBuilder.onFunctionCreate(data.req.args.function_name)
await curBuilder.onFunctionDelete(data.req.args.function_name);
await curBuilder.onFunctionCreate(data.req.args.function_name);
break;
case 'procedureUpdate':
await curBuilder.onProcedureDelete(data.req.args.procedure_name)
await curBuilder.onProcedureCreate(data.req.args.procedure_name)
await curBuilder.onProcedureDelete(data.req.args.procedure_name);
await curBuilder.onProcedureCreate(data.req.args.procedure_name);
break;
case 'procedureDelete':
await curBuilder.onProcedureDelete(data.req.args.procedure_name)
await curBuilder.onProcedureDelete(data.req.args.procedure_name);
break;
case 'functionCreate':
await curBuilder.onFunctionCreate(data.req.args.function_name)
await curBuilder.onFunctionCreate(data.req.args.function_name);
break;
case 'functionDelete':
await curBuilder.onFunctionDelete(data.req.args.function_name)
await curBuilder.onFunctionDelete(data.req.args.function_name);
break;
case 'xcRoutesPolicyUpdate':
case 'xcResolverPolicyUpdate':
await curBuilder.onPolicyUpdate(data.req.args.tn);
console.log(`Updated validations for table : ${data.req.args.tn}`)
console.log(`Updated validations for table : ${data.req.args.tn}`);
break;
case 'xcModelsEnable':
@ -325,18 +365,17 @@ export default class NcProjectBuilder {
break;
case 'xcCronSave':
await curBuilder.restartCron(data.req.args)
await curBuilder.restartCron(data.req.args);
break;
case 'cronDelete':
await curBuilder.removeCron(data.req.args)
await curBuilder.removeCron(data.req.args);
break;
// todo: optimize
if (Array.isArray(data.req.args.tableNames)) {
for (const procedure of data.req.args.tableNames) {
await curBuilder.onProcedureCreate(procedure)
await curBuilder.onProcedureCreate(procedure);
}
}
case 'tableMetaCreate':
@ -355,13 +394,14 @@ export default class NcProjectBuilder {
XCEeError.throw();
break;
case 'viewDelete':
await curBuilder.onViewDelete(data.req.args.view_name)
await curBuilder.onViewDelete(data.req.args.view_name);
this.app.ncMeta.audit(this.id, curBuilder.getDbAlias(), 'nc_audit', {
op_type: 'VIEW',
op_sub_type: 'DELETED',
user: data.user.email,
description: `deleted view ${data.req.args.view_name} `, ip: data.ctx.req.clientIp
})
description: `deleted view ${data.req.args.view_name} `,
ip: data.ctx.req.clientIp
});
break;
case 'tableMetaRecreate':
@ -391,28 +431,27 @@ export default class NcProjectBuilder {
break;
case 'procedureMetaDelete':
await curBuilder.onProcedureDelete(data.req.args.procedure_name)
await curBuilder.onProcedureDelete(data.req.args.procedure_name);
break;
case 'procedureMetaRecreate':
await curBuilder.onProcedureDelete(data.req.args.tn)
await curBuilder.onProcedureCreate(data.req.args.tn)
await curBuilder.onProcedureDelete(data.req.args.tn);
await curBuilder.onProcedureCreate(data.req.args.tn);
break;
case 'functionMetaCreate':
// todo: optimize
if (Array.isArray(data.req.args.tableNames)) {
for (const functionName of data.req.args.tableNames) {
await curBuilder.onFunctionCreate(functionName)
await curBuilder.onFunctionCreate(functionName);
}
}
break;
case 'functionMetaDelete':
await curBuilder.onFunctionDelete(data.req.args.procedure_name)
await curBuilder.onFunctionDelete(data.req.args.procedure_name);
break;
case 'projectStop':
this.router.stack.splice(0, this.router.stack.length);
this.apiBuilders.splice(0, this.apiBuilders.length);
@ -422,8 +461,9 @@ export default class NcProjectBuilder {
op_type: 'PROJECT',
op_sub_type: 'STOPPED',
user: data.user.email,
description: `stopped project ${this.title}(${this.id}) `, ip: data?.ctx?.req?.clientIp
})
description: `stopped project ${this.title}(${this.id}) `,
ip: data?.ctx?.req?.clientIp
});
break;
case 'projectStart':
@ -432,16 +472,21 @@ export default class NcProjectBuilder {
op_type: 'PROJECT',
op_sub_type: 'STARTED',
user: data.user.email,
description: `started project ${this.title}(${this.id}) `, ip: data?.ctx?.req?.clientIp
})
description: `started project ${this.title}(${this.id}) `,
ip: data?.ctx?.req?.clientIp
});
break;
case 'projectDelete':
this.router.stack.splice(0, this.router.stack.length);
this.apiBuilders.splice(0, this.apiBuilders.length);
await this.app.ncMeta.projectDeleteById(this.id);
await this.app.ncMeta.knex('nc_projects_users').where({project_id: this.id}).del();
for (const db of (this.config?.envs?.[this.appConfig?.workingEnv]?.db || [])) {
await this.app.ncMeta
.knex('nc_projects_users')
.where({ project_id: this.id })
.del();
for (const db of this.config?.envs?.[this.appConfig?.workingEnv]?.db ||
[]) {
const dbAlias = db?.meta?.dbAlias;
const apiType = db?.meta?.api?.type;
await this.app.ncMeta.metaReset(this.id, dbAlias, apiType);
@ -451,8 +496,9 @@ export default class NcProjectBuilder {
op_type: 'PROJECT',
op_sub_type: 'DELETED',
user: data.user.email,
description: `deleted project ${this.title}(${this.id}) `, ip: data?.ctx?.req?.clientIp
})
description: `deleted project ${this.title}(${this.id}) `,
ip: data?.ctx?.req?.clientIp
});
break;
case 'xcMetaTablesImportLocalFsToDb':
@ -463,18 +509,17 @@ export default class NcProjectBuilder {
op_type: 'PROJECT',
op_sub_type: 'RESTARTED',
user: data.user.email,
description: `restarted project ${this.title}(${this.id}) `, ip: data?.ctx?.req?.clientIp
})
description: `restarted project ${this.title}(${this.id}) `,
ip: data?.ctx?.req?.clientIp
});
break;
default:
console.log('DB OPS', data.req.api);
}
// export metadata to filesystem after meta changes
switch (data?.req?.api) {
case 'procedureCreate':
case 'functionUpdate':
case 'procedureUpdate':
@ -509,23 +554,20 @@ export default class NcProjectBuilder {
}
break;
}
}
protected async _createApiBuilder() {
this.apiBuilders.splice(0, this.apiBuilders.length);
let i = 0;
const connectionConfigs = [];
/* for each db create an api builder */
for (const db of (this.config?.envs?.[this.appConfig?.workingEnv]?.db || [])) {
for (const db of this.config?.envs?.[this.appConfig?.workingEnv]?.db ||
[]) {
let Builder;
switch (db.meta.api.type) {
case "graphql":
case 'graphql':
Builder = GqlApiBuilder;
break;
@ -535,7 +577,6 @@ export default class NcProjectBuilder {
}
if ((db?.connection as any)?.database) {
const connectionConfig = {
...db,
meta: {
@ -547,16 +588,22 @@ export default class NcProjectBuilder {
}
};
this.apiBuilders.push(new Builder(this.app, this, this.config, connectionConfig, this.app.ncMeta));
this.apiBuilders.push(
new Builder(
this.app,
this,
this.config,
connectionConfig,
this.app.ncMeta
)
);
connectionConfigs.push(connectionConfig);
i++;
} else if (db.meta?.allSchemas) {
/* get all schemas and create APIs for all of them */
const sqlClient = SqlClientFactory.create({
...db,
connection: {...db.connection, database: undefined}
connection: { ...db.connection, database: undefined }
});
// @ts-ignore
@ -564,7 +611,7 @@ export default class NcProjectBuilder {
for (const schema of schemaList) {
const connectionConfig = {
...db,
connection: {...db.connection, database: schema.schema_name},
connection: { ...db.connection, database: schema.schema_name },
meta: {
...db.meta,
dbAlias: i ? db.meta.dbAlias + i : db.meta.dbAlias,
@ -575,54 +622,62 @@ export default class NcProjectBuilder {
}
};
this.apiBuilders.push(new Builder(this.app, this, this.config, connectionConfig, this.app.ncMeta));
this.apiBuilders.push(
new Builder(
this.app,
this,
this.config,
connectionConfig,
this.app.ncMeta
)
);
connectionConfigs.push(connectionConfig);
i++;
}
sqlClient.knex.destroy();
}
}
if (this.config?.envs?.[this.appConfig.workingEnv]?.db) {
this.config.envs[this.appConfig.workingEnv].db.splice(0, this.config.envs[this.appConfig.workingEnv].db.length, ...connectionConfigs);
this.config.envs[this.appConfig.workingEnv].db.splice(
0,
this.config.envs[this.appConfig.workingEnv].db.length,
...connectionConfigs
);
}
}
protected genVer(i): string {
const l = 'vwxyzabcdefghijklmnopqrstu';
return i
.toString(26)
.split('')
.map(v => l[parseInt(v, 26)])
.join('') + '1';
return (
i
.toString(26)
.split('')
.map(v => l[parseInt(v, 26)])
.join('') + '1'
);
}
protected async syncMigration(): Promise<void> {
if (this.appConfig?.toolDir
if (
this.appConfig?.toolDir
// && !('NC_MIGRATIONS_DISABLED' in process.env)
) {
const dbs = this.config?.envs?.[this.appConfig.workingEnv]?.db
const dbs = this.config?.envs?.[this.appConfig.workingEnv]?.db;
if (!dbs || !dbs.length) {
return;
}
for (const connectionConfig of dbs) {
try {
const sqlClient = NcConnectionMgr.getSqlClient({
dbAlias: connectionConfig?.mets?.dbAlias,
env: this.config.env,
config: this.config,
projectId: this.id
})
});
/* create migrator */
const migrator = new Migrator({
project_id: this.id,
@ -631,7 +686,13 @@ export default class NcProjectBuilder {
});
/* if migrator folder doesn't exist for project - call migratior init */
const migrationFolder = path.join(this.config.toolDir, 'nc', this.id, connectionConfig.meta.dbAlias, 'migrations');
const migrationFolder = path.join(
this.config.toolDir,
'nc',
this.id,
connectionConfig.meta.dbAlias,
'migrations'
);
if (!fs.existsSync(migrationFolder)) {
await migrator.init({
folder: this.config?.toolDir,
@ -656,7 +717,6 @@ export default class NcProjectBuilder {
sqlContentMigrate: 1,
sqlClient
});
} catch (e) {
console.log(e);
// throw e;
@ -666,44 +726,47 @@ export default class NcProjectBuilder {
}
}
protected static triggerGarbageCollect() {
try {
if (global.gc) {
global.gc();
}
} catch (e) {
console.log("`node --expose-gc index.js`");
console.log('`node --expose-gc index.js`');
process.exit();
}
}
protected initApiInfoRoute(): void {
this.router.get(`/projectApiInfo`, (req: any, res): any => {
// auth to admin
if (this.config.auth) {
if (this.config.auth.jwt) {
if (!(req.session.passport.user.roles.creator
|| req.session.passport.user.roles.editor
|| req.session.passport.user.roles.commenter
|| req.session.passport.user.roles.viewer)) {
if (
!(
req.session.passport.user.roles.creator ||
req.session.passport.user.roles.editor ||
req.session.passport.user.roles.commenter ||
req.session.passport.user.roles.viewer
)
) {
return res.status(401).json({
msg: 'Unauthorized access : xc-auth does not have admin permission'
})
msg:
'Unauthorized access : xc-auth does not have admin permission'
});
}
} else if (this.config.auth.masterKey) {
if (req.headers['xc-master-key'] !== this.config.auth.masterKey.secret) {
if (
req.headers['xc-master-key'] !== this.config.auth.masterKey.secret
) {
return res.status(401).json({
msg: 'Unauthorized access : xc-admin header missing or not matching'
})
msg:
'Unauthorized access : xc-admin header missing or not matching'
});
}
}
}
const info: any = {};
for (const builder of this.apiBuilders) {
@ -713,8 +776,8 @@ export default class NcProjectBuilder {
gqlApiUrl: `/nc/${this.id}/${builder.apiPrefix}/graphql`,
grpcApiUrl: ``,
apiType: builder.apiType,
database: builder.getDbName(),
}
database: builder.getDbName()
};
}
const result = {
@ -723,62 +786,64 @@ export default class NcProjectBuilder {
list: this.apiInfInfoList,
aggregated: this.aggregatedApiInfo
}
}
};
res.json(result);
});
}
protected async progress(info, allInfo, isFirstTime?) {
const aggregatedInfo = allInfo.reduce((arrSum, infoObj) => [
'',
arrSum[1] + +infoObj.tablesCount,
arrSum[2] + (infoObj.type === 'graphql' ? 1 : 0),
arrSum[3] + +(infoObj.type === 'rest' ? 1 : 0),
arrSum[4] + (+infoObj.apiCount || +infoObj.resolversCount || 0),
// arrSum[3] + +info.timeTaken
(Date.now() - this.startTime) / 1000
],
[0, 0, 0, 0, 0, 0])
.map((v, i) => (i === 5 ? v.toFixed(1) + 's' : (i === 2 ? v.toLocaleString() : v)));
const aggregatedInfo = allInfo
.reduce(
(arrSum, infoObj) => [
'',
arrSum[1] + +infoObj.tablesCount,
arrSum[2] + (infoObj.type === 'graphql' ? 1 : 0),
arrSum[3] + +(infoObj.type === 'rest' ? 1 : 0),
arrSum[4] + (+infoObj.apiCount || +infoObj.resolversCount || 0),
// arrSum[3] + +info.timeTaken
(Date.now() - this.startTime) / 1000
],
[0, 0, 0, 0, 0, 0]
)
.map((v, i) =>
i === 5 ? v.toFixed(1) + 's' : i === 2 ? v.toLocaleString() : v
);
this.apiInfInfoList.push(info);
this.aggregatedApiInfo = aggregatedInfo;
if (isFirstTime) {
Tele.emit('evt_api_created', info);
}
}
protected async addAuthHookToMiddleware(): Promise<any> {
this.authHook = await this.app.ncMeta.metaGet(this.id, 'db', 'nc_hooks', {
type: 'AUTH_MIDDLEWARE'
});
this.router.use(async (req: any, _res, next) => {
if (this.authHook && this.authHook.url) {
try {
const result = await axios.post(this.authHook.url, {}, {
headers: req.headers
});
const result = await axios.post(
this.authHook.url,
{},
{
headers: req.headers
}
);
req.locals = req.locals || {};
req.locals = {user: result.data};
req.locals = { user: result.data };
} catch (e) {
console.log(e)
console.log(e);
}
}
next();
})
});
}
public get prefix(): string {
return this.config?.prefix;
}
@ -788,14 +853,14 @@ export default class NcProjectBuilder {
}
public updateConfig(config: string) {
this._config = {...this.appConfig, ...JSON.parse(config)};
this._config = { ...this.appConfig, ...JSON.parse(config) };
}
public async reInit() {
this.router.stack.splice(0, this.router.stack.length);
this.apiBuilders.splice(0, this.apiBuilders.length);
await this.app.ncMeta.projectStatusUpdate(this.title, 'stopped');
const dbs = this.config?.envs?.[this.appConfig.workingEnv]?.db
const dbs = this.config?.envs?.[this.appConfig.workingEnv]?.db;
if (!dbs || !dbs.length) {
return;
@ -806,12 +871,11 @@ export default class NcProjectBuilder {
dbAlias: connectionConfig?.mets?.dbAlias,
env: this.config.env,
projectId: this.id
})
});
}
NcProjectBuilder.triggerGarbageCollect();
await this.init();
}
}
/**

42
packages/nocodb/src/lib/noco/NcProjectBuilderEE.ts

@ -1,58 +1,62 @@
import NcProjectBuilder from "./NcProjectBuilder";
import NcProjectBuilder from './NcProjectBuilder';
export default class NcProjectBuilderEE extends NcProjectBuilder {
public async handleRunTimeChanges(data: any): Promise<any> {
const curBuilder = this.apiBuilders.find(builder => {
return (data.req?.dbAlias || data.req?.args?.dbAlias) === builder.getDbAlias();
return (
(data.req?.dbAlias || data.req?.args?.dbAlias) === builder.getDbAlias()
);
});
switch (data?.req?.api) {
case 'tableMetaRecreate':
await curBuilder.onTableDelete(data.req.args.tn)
await curBuilder.onTableCreate(data.req.args.tn, {})
await curBuilder.onTableDelete(data.req.args.tn);
await curBuilder.onTableCreate(data.req.args.tn, {});
break;
case 'viewMetaRecreate':
await curBuilder.onViewDelete(data.req.args.tn)
await curBuilder.onViewCreate(data.req.args.tn, {})
await curBuilder.onViewDelete(data.req.args.tn);
await curBuilder.onViewCreate(data.req.args.tn, {});
break;
case 'procedureMetaCreate':
// todo: optimize
if (Array.isArray(data.req.args.tableNames)) {
for (const procedure of data.req.args.tableNames) {
await curBuilder.onProcedureCreate(procedure)
await curBuilder.onProcedureCreate(procedure);
}
}
break;
case 'functionMetaRecreate':
await curBuilder.onFunctionDelete(data.req.args.tn)
await curBuilder.onFunctionCreate(data.req.args.tn)
await curBuilder.onFunctionDelete(data.req.args.tn);
await curBuilder.onFunctionCreate(data.req.args.tn);
break;
case 'tableMetaCreate':
// await curBuilder.onTableCreate(data.req.args.tn)
await curBuilder.xcTablesPopulate({tableNames: data.req.args.tableNames.map(tn => ({tn})), type:'table'});
await curBuilder.xcTablesPopulate({
tableNames: data.req.args.tableNames.map(tn => ({ tn })),
type: 'table'
});
break;
case 'viewMetaCreate':
// await curBuilder.onTableCreate(data.req.args.tn)
await curBuilder.xcTablesPopulate({tableNames: data.req.args.viewNames, type: 'view'});
await curBuilder.xcTablesPopulate({
tableNames: data.req.args.viewNames,
type: 'view'
});
break;
case 'tableMetaDelete':
for (const table of data.req.args.tableNames) {
await curBuilder.onTableDelete(table)
await curBuilder.onTableDelete(table);
}
break;
case 'viewMetaDelete':
for (const table of data.req.args.viewNames) {
await curBuilder.onViewDelete(table)
await curBuilder.onViewDelete(table);
}
break;
case 'xcRelationsSet':
@ -60,11 +64,9 @@ export default class NcProjectBuilderEE extends NcProjectBuilder {
break;
default:
return super.handleRunTimeChanges(data)
return super.handleRunTimeChanges(data);
}
}
}
/**

306
packages/nocodb/src/lib/noco/Noco.ts

@ -1,47 +1,48 @@
/* eslint-disable @typescript-eslint/ban-types */
import fs from "fs";
import fs from 'fs';
import path from 'path';
import * as Sentry from '@sentry/node';
import bodyParser from "body-parser";
import bodyParser from 'body-parser';
import clear from 'clear';
import cookieParser from 'cookie-parser';
import debug from 'debug';
import * as express from 'express'
import {Router} from "express";
import importFresh from "import-fresh";
import morgan from "morgan";
import {Tele} from "nc-help";
import NcToolGui from "nc-lib-gui";
import * as express from 'express';
import { Router } from 'express';
import importFresh from 'import-fresh';
import morgan from 'morgan';
import { Tele } from 'nc-help';
import NcToolGui from 'nc-lib-gui';
import requestIp from 'request-ip';
import {v4 as uuidv4} from 'uuid';
import { v4 as uuidv4 } from 'uuid';
import {NcConfig} from "../../interface/config";
import { NcConfig } from '../../interface/config';
import Migrator from '../migrator/SqlMigrator/lib/KnexMigrator';
import NcConfigFactory from "../utils/NcConfigFactory";
import NcProjectBuilderCE from "./NcProjectBuilder";
import NcProjectBuilderEE from "./NcProjectBuilderEE";
import {GqlApiBuilder} from "./gql/GqlApiBuilder";
import NcMetaIO from "./meta/NcMetaIO";
import NcMetaImplCE from "./meta/NcMetaIOImpl";
import NcMetaImplEE from "./meta/NcMetaIOImplEE";
import NcMetaMgrCE from "./meta/NcMetaMgr";
import NcMetaMgrEE from "./meta/NcMetaMgrEE";
import {RestApiBuilder} from "./rest/RestApiBuilder";
import RestAuthCtrlCE from "./rest/RestAuthCtrl";
import RestAuthCtrlEE from "./rest/RestAuthCtrlEE";
import NcConfigFactory from '../utils/NcConfigFactory';
import NcProjectBuilderCE from './NcProjectBuilder';
import NcProjectBuilderEE from './NcProjectBuilderEE';
import { GqlApiBuilder } from './gql/GqlApiBuilder';
import NcMetaIO from './meta/NcMetaIO';
import NcMetaImplCE from './meta/NcMetaIOImpl';
import NcMetaImplEE from './meta/NcMetaIOImplEE';
import NcMetaMgrCE from './meta/NcMetaMgr';
import NcMetaMgrEE from './meta/NcMetaMgrEE';
import { RestApiBuilder } from './rest/RestApiBuilder';
import RestAuthCtrlCE from './rest/RestAuthCtrl';
import RestAuthCtrlEE from './rest/RestAuthCtrlEE';
import mkdirp from 'mkdirp';
import MetaAPILogger from "./meta/MetaAPILogger";
import NcUpgrader from "./upgrader/NcUpgrader";
import MetaAPILogger from './meta/MetaAPILogger';
import NcUpgrader from './upgrader/NcUpgrader';
const log = debug('nc:app');
require('dotenv').config();
const NcProjectBuilder = process.env.EE ? NcProjectBuilderEE : NcProjectBuilderCE;
const NcProjectBuilder = process.env.EE
? NcProjectBuilderEE
: NcProjectBuilderCE;
export default class Noco {
private static _this: Noco;
public static get dashboardUrl(): string {
@ -53,15 +54,15 @@ export default class Noco {
siteUrl = Noco._this?.config?.envs?.['_noco']?.publicUrl;
}
return `${siteUrl}${Noco._this?.config?.dashboardPath}`
return `${siteUrl}${Noco._this?.config?.dashboardPath}`;
}
public static async init(args?: {
progressCallback?: Function,
registerRoutes?: Function,
registerGql?: Function,
registerContext?: Function,
afterMetaMigrationInit?: Function
progressCallback?: Function;
registerRoutes?: Function;
registerGql?: Function;
registerContext?: Function;
afterMetaMigrationInit?: Function;
}): Promise<Router> {
if (Noco._this) {
return Noco._this.router;
@ -87,7 +88,6 @@ export default class Noco {
private socketClient: any;
constructor() {
process.env.PORT = process.env.PORT || '8080';
// todo: move
process.env.NC_VERSION = '0011043';
@ -99,7 +99,7 @@ export default class Noco {
this.config = NcConfigFactory.make();
/******************* setup : start *******************/
this.env = '_noco';//process.env['NODE_ENV'] || this.config.workingEnv || 'dev';
this.env = '_noco'; //process.env['NODE_ENV'] || this.config.workingEnv || 'dev';
this.config.workingEnv = this.env;
this.config.type = 'docker';
@ -136,19 +136,17 @@ export default class Noco {
// });
clear();
/******************* prints : end *******************/
}
public async init(args?: {
progressCallback?: Function,
registerRoutes?: Function,
registerGql?: Function,
registerContext?: Function,
afterMetaMigrationInit?: Function
progressCallback?: Function;
registerRoutes?: Function;
registerGql?: Function;
registerContext?: Function;
afterMetaMigrationInit?: Function;
}) {
const {
progressCallback,
progressCallback
// registerRoutes,
// registerContext,
// registerGql
@ -156,7 +154,6 @@ export default class Noco {
log('Initializing app');
// create tool directory if missing
mkdirp.sync(this.config.toolDir);
@ -177,7 +174,7 @@ export default class Noco {
await this.readOrGenJwtSecret();
await NcUpgrader.upgrade({ncMeta: this.ncMeta})
await NcUpgrader.upgrade({ ncMeta: this.ncMeta });
if (args?.afterMetaMigrationInit) {
await args.afterMetaMigrationInit();
@ -186,26 +183,30 @@ export default class Noco {
/******************* Middlewares : start *******************/
this.router.use((req: any, _res, next) => {
req.nc = this.requestContext;
req.ncSiteUrl = this.config?.envs?.[this.env]?.publicUrl || this.config?.publicUrl || (req.protocol + '://' + req.get('host'));
req.ncSiteUrl =
this.config?.envs?.[this.env]?.publicUrl ||
this.config?.publicUrl ||
req.protocol + '://' + req.get('host');
req.ncFullUrl = req.protocol + '://' + req.get('host') + req.originalUrl;
next();
});
// to get ip addresses
this.router.use(requestIp.mw())
this.router.use(requestIp.mw());
this.router.use(cookieParser());
this.router.use(bodyParser.json({
limit: process.env.NC_REQUEST_BODY_SIZE || 1024 * 1024
}));
this.router.use(
bodyParser.json({
limit: process.env.NC_REQUEST_BODY_SIZE || 1024 * 1024
})
);
this.router.use(morgan('tiny'));
this.router.use(express.static(path.join(__dirname, './public')));
this.router.use((req: any, _res, next) => {
req.ncProjectId = req?.query?.project_id || req?.body?.project_id;
next();
})
});
/* this.router.use(this.config.dashboardPath, (req: any, _res, next) => {
req.ncProjectId = req?.body?.project_id;
next();
@ -213,7 +214,7 @@ export default class Noco {
this.router.use(`/nc/:project_id/*`, (req: any, _res, next) => {
req.ncProjectId = req.ncProjectId || req.params.project_id;
next();
})
});
this.router.use(MetaAPILogger.mw);
/******************* Middlewares : end *******************/
@ -225,21 +226,25 @@ export default class Noco {
this.ncToolApi.addListener(runTimeHandler);
this.metaMgr.setListener(runTimeHandler);
await this.metaMgr.initHandler(this.router);
this.router.use(this.config.dashboardPath, await this.ncToolApi.expressMiddleware());
this.router.get('/', (_req, res) => res.redirect(this.config.dashboardPath));
this.router.use(
this.config.dashboardPath,
await this.ncToolApi.expressMiddleware()
);
this.router.get('/', (_req, res) =>
res.redirect(this.config.dashboardPath)
);
this.initSentryErrorHandler();
/* catch error */
this.router.use((err, _req, res, next) => {
if (err) {
return res.status(400).json({msg: err.message});
return res.status(400).json({ msg: err.message });
}
next();
});
Tele.emit('evt_app_started', {})
Tele.emit('evt_app_started', {});
return this.router;
}
@ -252,17 +257,14 @@ export default class Noco {
private initSentry() {
if (process.env.NC_SENTRY_DSN) {
Sentry.init({dsn: process.env.NC_SENTRY_DSN});
Sentry.init({ dsn: process.env.NC_SENTRY_DSN });
// The request handler must be the first middleware on the app
// The request handler must be the first middleware on the app
this.router.use(Sentry.Handlers.requestHandler());
}
}
async initServerless() {
}
async initServerless() {}
public getBuilders(): Array<RestApiBuilder | GqlApiBuilder> {
return this.apiBuilders;
@ -276,128 +278,150 @@ export default class Noco {
this.requestContext = context;
}
private handleRuntimeChanges(_progressCallback: Function) {
return async (data): Promise<any> => {
switch (data?.req?.api) {
case 'projectCreateByWeb':
case 'projectCreateByOneClick':
case 'projectCreateByWebWithXCDB': {
// || data?.req?.args?.project?.title || data?.req?.args?.title
const project = await this.ncMeta.projectGetById(data?.res?.id)
const builder = new NcProjectBuilder(this, this.config, project);
this.projectBuilders.push(builder)
await builder.init(true);
}
case 'projectCreateByWebWithXCDB':
{
// || data?.req?.args?.project?.title || data?.req?.args?.title
const project = await this.ncMeta.projectGetById(data?.res?.id);
const builder = new NcProjectBuilder(this, this.config, project);
this.projectBuilders.push(builder);
await builder.init(true);
}
break;
// create project builder for newly imported project
// duplicated code - projectCreateByWeb
case 'xcMetaTablesImportZipToLocalFsAndDb': {
if (data.req?.freshImport) {
const project = await this.ncMeta.projectGetById(data?.req?.project_id)
const builder = new NcProjectBuilder(this, this.config, project);
this.projectBuilders.push(builder)
await builder.init(true);
} else {
const projectBuilder = this.projectBuilders.find(pb => pb.id == data.req?.project_id);
return projectBuilder?.handleRunTimeChanges(data);
case 'xcMetaTablesImportZipToLocalFsAndDb':
{
if (data.req?.freshImport) {
const project = await this.ncMeta.projectGetById(
data?.req?.project_id
);
const builder = new NcProjectBuilder(this, this.config, project);
this.projectBuilders.push(builder);
await builder.init(true);
} else {
const projectBuilder = this.projectBuilders.find(
pb => pb.id == data.req?.project_id
);
return projectBuilder?.handleRunTimeChanges(data);
}
}
}
break;
case 'projectUpdateByWeb': {
const projectId = data.req?.project_id;
const project = await this.ncMeta.projectGetById(data?.req?.project_id)
const projectBuilder = this.projectBuilders.find(pb => pb.id === projectId);
projectBuilder.updateConfig(project.config)
await projectBuilder.reInit()
console.log(`Project updated: ${projectId}`)
}
case 'projectUpdateByWeb':
{
const projectId = data.req?.project_id;
const project = await this.ncMeta.projectGetById(
data?.req?.project_id
);
const projectBuilder = this.projectBuilders.find(
pb => pb.id === projectId
);
projectBuilder.updateConfig(project.config);
await projectBuilder.reInit();
console.log(`Project updated: ${projectId}`);
}
break;
case 'projectChangeEnv':
try {
this.config = importFresh(path.join(process.cwd(), 'config.xc.json')) as NcConfig;
this.config = importFresh(
path.join(process.cwd(), 'config.xc.json')
) as NcConfig;
this.config.toolDir = this.config.toolDir || process.cwd();
this.ncMeta.setConfig(this.config);
this.metaMgr.setConfig(this.config);
Object.assign(process.env, {NODE_ENV: this.env = this.config.workingEnv});
Object.assign(process.env, {
NODE_ENV: this.env = this.config.workingEnv
});
this.router.stack.splice(0, this.router.stack.length);
this.ncToolApi.destroy();
this.ncToolApi.reInitialize(this.config);
// await this.init({progressCallback});
console.log(`Loaded env : ${data.req.args.env}`)
console.log(`Loaded env : ${data.req.args.env}`);
} catch (e) {
console.log(e);
}
break;
default: {
const projectBuilder = this.projectBuilders.find(pb => pb.id == data.req?.project_id);
const projectBuilder = this.projectBuilders.find(
pb => pb.id == data.req?.project_id
);
return projectBuilder?.handleRunTimeChanges(data);
}
}
};
}
private async initProjectBuilders() {
const RestAuthCtrl = process.env.EE ? RestAuthCtrlEE : RestAuthCtrlCE;
this.projectBuilders.splice(0, this.projectBuilders.length);
await new RestAuthCtrl(this as any,
await new RestAuthCtrl(
this as any,
this.ncMeta?.knex,
this.config?.meta?.db,
this.config, this.ncMeta).init();
this.config,
this.ncMeta
).init();
this.router.use(this.projectRouter);
const projects = await this.ncMeta.projectList();
for (const project of projects) {
const projectBuilder = new NcProjectBuilder(this, this.config, project);
this.projectBuilders.push(projectBuilder)
this.projectBuilders.push(projectBuilder);
}
let i = 0;
for (const builder of this.projectBuilders) {
if (projects[i].status === 'started' || projects[i].status === 'starting') {
if (
projects[i].status === 'started' ||
projects[i].status === 'starting'
) {
await builder.init();
}
i++;
}
}
private async syncMigration(): Promise<void> {
if (this.config?.toolDir
if (
this.config?.toolDir
// && !('NC_MIGRATIONS_DISABLED' in process.env)
) {
const dbs = this.config?.envs?.[this.env]?.db
const dbs = this.config?.envs?.[this.env]?.db;
if (!dbs || !dbs.length) {
log(`'${this.env}' environment doesn't have any database configuration.`)
log(
`'${this.env}' environment doesn't have any database configuration.`
);
return;
}
for (const connectionConfig of dbs) {
log(`Migrations start >> ${connectionConfig?.connection?.['database']} (${connectionConfig.meta?.dbAlias})`)
log(
`Migrations start >> ${connectionConfig?.connection?.['database']} (${connectionConfig.meta?.dbAlias})`
);
try {
/* Update database migrations */
const migrator = new Migrator();
/* initialize migration if folder doesn't exist */
const migrationFolder = path.join(this.config.toolDir, 'server', 'tool', connectionConfig.meta.dbAlias, 'migrations');
const migrationFolder = path.join(
this.config.toolDir,
'server',
'tool',
connectionConfig.meta.dbAlias,
'migrations'
);
if (!fs.existsSync(migrationFolder)) {
await migrator.init({
folder: this.config?.toolDir,
@ -417,38 +441,40 @@ export default class Noco {
env: this.env,
dbAlias: connectionConfig.meta.dbAlias,
migrationSteps: 99999,
sqlContentMigrate: 1,
sqlContentMigrate: 1
});
log(`Migrations end << ${connectionConfig?.connection?.['database']} (${connectionConfig.meta?.dbAlias})`)
log(
`Migrations end << ${connectionConfig?.connection?.['database']} (${connectionConfig.meta?.dbAlias})`
);
} catch (e) {
log(`Migrations Failed !! ${connectionConfig?.connection?.['database']} (${connectionConfig.meta?.dbAlias})`)
log(
`Migrations Failed !! ${connectionConfig?.connection?.['database']} (${connectionConfig.meta?.dbAlias})`
);
console.log(e);
// throw e;
}
}
} else {
log('Warning : ignoring migrations on boot since tools directory not defined')
log(
'Warning : ignoring migrations on boot since tools directory not defined'
);
}
}
private initWebSocket(): void {
// todo: Auth
this.router.get(`${this.config.dashboardPath}/demo`, (_req, res) => {
(this.ncMeta as any).updateKnex({
"client": "sqlite3",
"connection": {
"filename": "xcDemo.db"
client: 'sqlite3',
connection: {
filename: 'xcDemo.db'
}
});
res.json({msg: 'done'});
})
res.json({ msg: 'done' });
});
this.io = require('socket.io')();
this.io.listen(8083);
@ -459,7 +485,6 @@ export default class Noco {
console.log('Disconnected');
this.socketClient = null;
});
});
const statusMonitor = require('express-status-monitor')({
@ -468,8 +493,10 @@ export default class Noco {
});
this.router.use(statusMonitor);
this.router.get(`${this.config.dashboardPath}/status`, statusMonitor.pageRoute)
this.router.get(
`${this.config.dashboardPath}/status`,
statusMonitor.pageRoute
);
/*
title: 'Express Status', // Default title
@ -499,27 +526,24 @@ export default class Noco {
},
healthChecks: [],
ignoreStartsWith: '/admin'*/
}
private async readOrGenJwtSecret(): Promise<any> {
if (this.config?.auth?.jwt && !this.config.auth.jwt.secret) {
let secret = (await this.ncMeta.metaGet('', '', 'nc_store', {
key: 'nc_auth_jwt_secret'
}))?.value;
let secret = (
await this.ncMeta.metaGet('', '', 'nc_store', {
key: 'nc_auth_jwt_secret'
})
)?.value;
if (!secret) {
(await this.ncMeta.metaInsert('', '', 'nc_store', {
await this.ncMeta.metaInsert('', '', 'nc_store', {
key: 'nc_auth_jwt_secret',
value: secret = uuidv4()
}))
});
}
this.config.auth.jwt.secret = secret;
}
}
}
/**

1724
packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts

File diff suppressed because it is too large Load Diff

17
packages/nocodb/src/lib/noco/common/BaseProcedure.ts

@ -1,16 +1,14 @@
import {GqlApiBuilder} from "../gql/GqlApiBuilder";
import {RestApiBuilder} from "../rest/RestApiBuilder";
import { GqlApiBuilder } from '../gql/GqlApiBuilder';
import { RestApiBuilder } from '../rest/RestApiBuilder';
import XcProcedure from "./XcProcedure";
import XcProcedure from './XcProcedure';
export default class BaseProcedure {
protected builder: GqlApiBuilder | RestApiBuilder;
protected builder: GqlApiBuilder | RestApiBuilder;
protected procedures: any[];
protected functions: any[]
protected functions: any[];
protected xcProcedure: XcProcedure;
public functionsSet(functions): void {
this.functions = functions;
}
@ -30,17 +28,16 @@ export default class BaseProcedure {
public functionDelete(name: string): void {
const index = this.functions.findIndex(f => f.function_name === name);
if (index > -1) {
this.functions.splice(index, 1)
this.functions.splice(index, 1);
}
}
public procedureDelete(name: string): void {
const index = this.procedures.findIndex(f => f.procedure_name === name);
if (index > -1) {
this.procedures.splice(index, 1)
this.procedures.splice(index, 1);
}
}
}
/**

138
packages/nocodb/src/lib/noco/common/NcConnectionMgr.ts

@ -1,19 +1,19 @@
import {XKnex} from "../../dataMapper";
import {NcConfig} from "../../../interface/config";
import fs from "fs";
import Knex from "knex";
import { XKnex } from '../../dataMapper';
import { NcConfig } from '../../../interface/config';
import fs from 'fs';
import Knex from 'knex';
import {SqlClientFactory} from 'nc-help';
import NcMetaIO from "../meta/NcMetaIO";
import {defaultConnectionConfig} from "../../utils/NcConfigFactory";
import { SqlClientFactory } from 'nc-help';
import NcMetaIO from '../meta/NcMetaIO';
import { defaultConnectionConfig } from '../../utils/NcConfigFactory';
export default class NcConnectionMgr {
private static connectionRefs: {
[projectId: string]: {
[env: string]: {
[dbAlias: string]: XKnex
}
}
[dbAlias: string]: XKnex;
};
};
} = {};
private static metaKnex: NcMetaIO;
@ -23,13 +23,13 @@ export default class NcConnectionMgr {
}
public static delete({
dbAlias = 'db',
env = '_noco',
projectId
}: {
dbAlias: string,
env: string,
projectId: string
dbAlias = 'db',
env = '_noco',
projectId
}: {
dbAlias: string;
env: string;
projectId: string;
}) {
// todo: ignore meta projects
if (this.connectionRefs?.[projectId]?.[env]?.[dbAlias]) {
@ -44,27 +44,31 @@ export default class NcConnectionMgr {
}
public static get({
dbAlias = 'db',
env = '_noco',
config,
projectId
}: {
dbAlias: string,
env: string,
config: NcConfig,
projectId: string
dbAlias = 'db',
env = '_noco',
config,
projectId
}: {
dbAlias: string;
env: string;
config: NcConfig;
projectId: string;
}): XKnex {
if (this.connectionRefs?.[projectId]?.[env]?.[dbAlias]) {
return this.connectionRefs?.[projectId]?.[env]?.[dbAlias];
}
this.connectionRefs[projectId] = this.connectionRefs[projectId] || {};
this.connectionRefs[projectId][env] = this.connectionRefs[projectId] [env] || {};
this.connectionRefs[projectId][env] =
this.connectionRefs[projectId][env] || {};
if (config?.prefix && this.metaKnex) {
this.connectionRefs[projectId][env][dbAlias] = this.metaKnex?.knex;
} else {
const connectionConfig = this.getConnectionConfig(config, env, dbAlias)
const connectionConfig = this.getConnectionConfig(config, env, dbAlias);
if (connectionConfig?.connection?.ssl && typeof connectionConfig?.connection?.ssl === 'object') {
if (
connectionConfig?.connection?.ssl &&
typeof connectionConfig?.connection?.ssl === 'object'
) {
if (connectionConfig.connection.ssl.caFilePath) {
connectionConfig.connection.ssl.ca = fs
.readFileSync(connectionConfig.connection.ssl.caFilePath)
@ -85,58 +89,66 @@ export default class NcConnectionMgr {
const isSqlite = connectionConfig?.client === 'sqlite3';
if (connectionConfig?.connection?.port) {
connectionConfig.connection.port = +connectionConfig.connection.port
connectionConfig.connection.port = +connectionConfig.connection.port;
}
this.connectionRefs[projectId][env][dbAlias] = XKnex(isSqlite ?
connectionConfig.connection as Knex.Config :
{
...connectionConfig,
connection: {
...defaultConnectionConfig,
...connectionConfig.connection,
typeCast(_field, next) {
const res = next();
if (res instanceof Buffer) {
return [...res].map(v => ('00' + v.toString(16)).slice(-2)).join('');
this.connectionRefs[projectId][env][dbAlias] = XKnex(
isSqlite
? (connectionConfig.connection as Knex.Config)
: ({
...connectionConfig,
connection: {
...defaultConnectionConfig,
...connectionConfig.connection,
typeCast(_field, next) {
const res = next();
if (res instanceof Buffer) {
return [...res]
.map(v => ('00' + v.toString(16)).slice(-2))
.join('');
}
return res;
}
}
return res;
}
}
} as any);
} as any)
);
if (isSqlite) {
this.connectionRefs[projectId][env][dbAlias].raw(`PRAGMA journal_mode=WAL;`).then(() => {
})
this.connectionRefs[projectId][env][dbAlias]
.raw(`PRAGMA journal_mode=WAL;`)
.then(() => {});
}
}
return this.connectionRefs[projectId][env][dbAlias];
}
private static getConnectionConfig(config: NcConfig, env: string, dbAlias: string) {
private static getConnectionConfig(
config: NcConfig,
env: string,
dbAlias: string
) {
return config?.envs?.[env]?.db?.find(db => db?.meta?.dbAlias === dbAlias);
}
public static getSqlClient({
projectId,
dbAlias = 'db',
env = '_noco',
config
}: {
dbAlias: string,
env: string,
config: NcConfig,
projectId: string
projectId,
dbAlias = 'db',
env = '_noco',
config
}: {
dbAlias: string;
env: string;
config: NcConfig;
projectId: string;
}): any {
const knex = this.get({
dbAlias,
env,
config,
projectId
})
return SqlClientFactory.create({knex, ...this.getConnectionConfig(config, env, dbAlias)})
});
return SqlClientFactory.create({
knex,
...this.getConnectionConfig(config, env, dbAlias)
});
}
}

19
packages/nocodb/src/lib/noco/common/XcAudit.ts

@ -1,20 +1,13 @@
import Noco from "../Noco";
import Noco from '../Noco';
export default class XcAudit{
public static init(app:Noco){
export default class XcAudit {
public static init(app: Noco) {
this.app = app;
}
// @ts-ignore
private static app:Noco;
private static app: Noco;
// @ts-ignore
public static async log(data:{
project
}){
}
}
public static async log(data: { project }) {}
}

33
packages/nocodb/src/lib/noco/common/XcCron.ts

@ -1,15 +1,13 @@
import {CronJob} from 'cron';
import { CronJob } from 'cron';
import {NcConfig} from "../../../interface/config";
import Noco from "../Noco";
import { NcConfig } from '../../../interface/config';
import Noco from '../Noco';
import BaseApiBuilder from "./BaseApiBuilder";
import BaseApiBuilder from './BaseApiBuilder';
// import * as tsc from "typescript";
export class XcCron {
// @ts-ignore
private app: Noco;
// @ts-ignore
@ -17,7 +15,6 @@ export class XcCron {
private apiBuilder: BaseApiBuilder<Noco>;
private cronJobs: { [key: string]: CronJob };
constructor(config: NcConfig, apiBuilder: BaseApiBuilder<Noco>, app: Noco) {
this.app = app;
this.config = config;
@ -27,17 +24,20 @@ export class XcCron {
public async init(): Promise<any> {
// const cronJobs = await this.apiBuilder.getDbDriver()('nc_cron').select();
const cronJobs = await this.apiBuilder.getXcMeta().metaList('', this.apiBuilder.dbAlias, 'nc_cron');
const cronJobs = await this.apiBuilder
.getXcMeta()
.metaList('', this.apiBuilder.dbAlias, 'nc_cron');
for (const cron of cronJobs) {
this.startCronJob(cron);
}
}
public async restartCron(args: any): Promise<any> {
// const cron = await this.apiBuilder.getDbDriver()('nc_cron').where('title', args.title).first();
const cron = await this.apiBuilder.getXcMeta().metaGet('', this.apiBuilder.dbAlias, 'nc_cron', {title: args.title});
const cron = await this.apiBuilder
.getXcMeta()
.metaGet('', this.apiBuilder.dbAlias, 'nc_cron', { title: args.title });
if (cron.id in this.cronJobs) {
this.cronJobs[cron.id].stop();
@ -52,10 +52,9 @@ export class XcCron {
}
}
private startCronJob(cron): void {
if (!cron.active) {
return
return;
}
try {
const job = new CronJob(
@ -68,13 +67,11 @@ export class XcCron {
job.start();
this.cronJobs[cron.id] = job;
} catch (e) {
console.log('Error in cron initialization : ', e.message)
console.log('Error in cron initialization : ', e.message);
}
}
private generateCronHandlerFromStringBody(fnBody: string): any {
// @ts-ignore
let handler = () => {
console.log('Empty handler');
@ -95,16 +92,16 @@ export class XcCron {
// tslint:disable-next-line:no-eval
handler = eval(js);
// console.timeEnd('startTrans')
} catch (e) {
console.log('Error in Cron handler transpilation', e)
console.log('Error in Cron handler transpilation', e);
}
// tslint:disable-next-line:no-eval
}
return handler;
}
}/**
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>

53
packages/nocodb/src/lib/noco/common/XcProcedure.ts

@ -1,4 +1,4 @@
import BaseApiBuilder from "./BaseApiBuilder";
import BaseApiBuilder from './BaseApiBuilder';
export default class XcProcedure {
private builder: BaseApiBuilder<any>;
@ -10,10 +10,22 @@ export default class XcProcedure {
public async callFunction(name: string, args: any[]) {
try {
if (this.builder.getDbType() === 'mssql') {
const result = await this.builder.getDbDriver().raw(`select dbo.??(${new Array(args.length).fill('?').join(',')}) as ??`, [name, ...args, name]);
const result = await this.builder
.getDbDriver()
.raw(
`select dbo.??(${new Array(args.length)
.fill('?')
.join(',')}) as ??`,
[name, ...args, name]
);
return result[0];
} else {
const result = await this.builder.getDbDriver().raw(`select ??(${new Array(args.length).fill('?').join(',')}) as ??`, [name, ...args, name]);
const result = await this.builder
.getDbDriver()
.raw(
`select ??(${new Array(args.length).fill('?').join(',')}) as ??`,
[name, ...args, name]
);
return result[0];
}
} catch (e) {
@ -24,7 +36,7 @@ export default class XcProcedure {
public async callProcedure(name: string, args: any[]) {
try {
if (this.builder.getDbType() === 'mssql') {
throw new Error('Not implemented')
throw new Error('Not implemented');
/*
const sql = require('mssql');
const request = new sql.Request({
@ -47,22 +59,39 @@ export default class XcProcedure {
// const result = '' // mcnd await this.builder.getDbDriver().raw(`Call ??(${Array.from({length: count}, (_, i) => '@var' + i).join(',')})`, [name]);
//
// return result)
} else if (this.builder.getDbType() === 'mysql2' || this.builder.getDbType() === 'mysql') {
const knexRef = args.reduce((knex, val, i) => knex.raw(`SET @var${i}=?`, [val]), this.builder.getDbDriver().schema)
const count = args.length
const result = await knexRef.raw(`Call ??(${Array.from({length: count}, (_, i) => '@var' + i).join(',')})`, [name]);
} else if (
this.builder.getDbType() === 'mysql2' ||
this.builder.getDbType() === 'mysql'
) {
const knexRef = args.reduce(
(knex, val, i) => knex.raw(`SET @var${i}=?`, [val]),
this.builder.getDbDriver().schema
);
const count = args.length;
const result = await knexRef.raw(
`Call ??(${Array.from({ length: count }, (_, i) => '@var' + i).join(
','
)})`,
[name]
);
return [result[count][0][0]];
} else if (this.builder.getDbType() === 'pg') {
const result = await this.builder.getDbDriver().raw(`Call ??(${new Array(args.length).fill('?').join(',')})`, [name, ...args]);
const result = await this.builder
.getDbDriver()
.raw(`Call ??(${new Array(args.length).fill('?').join(',')})`, [
name,
...args
]);
return result;
} else {
throw new Error('Not implemented')
throw new Error('Not implemented');
}
} catch (e) {
throw (e)
throw e;
}
}
}/**
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>

2
packages/nocodb/src/lib/noco/common/formSubmissionEmailTemplate.ts

@ -193,7 +193,7 @@ export default `<!doctype html>
</table>
</body>
</html>
`
`;
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd

19
packages/nocodb/src/lib/noco/common/helpers/addErrorOnColumnDeleteInFormula.ts

@ -1,18 +1,19 @@
export default function (args: {
virtualColumns,
columnName: string
export default function(args: {
virtualColumns;
columnName: string;
}): void | boolean {
let modified = false;
const fn = (pt, virtualColumn) => {
if (pt.type === 'CallExpression') {
pt.arguments.map(arg => fn(arg, virtualColumn))
pt.arguments.map(arg => fn(arg, virtualColumn));
} else if (pt.type === 'Literal') {
} else if (pt.type === 'Identifier') {
if (pt.name === args.columnName) {
virtualColumn.formula.error = virtualColumn.formula.error || [];
virtualColumn.formula.error.push(`Column '${args.columnName}' was deleted`)
virtualColumn.formula.error.push(
`Column '${args.columnName}' was deleted`
);
modified = true;
}
} else if (pt.type === 'BinaryExpression') {
@ -22,13 +23,13 @@ export default function (args: {
};
if (!args.virtualColumns) {
return
return;
}
for (const v of args.virtualColumns) {
if (!v.formula?.tree) {
continue;
}
fn(v.formula.tree, v)
fn(v.formula.tree, v);
}
return modified;
}
}

48
packages/nocodb/src/lib/noco/common/helpers/jsepTreeToFormula.ts

@ -1,43 +1,67 @@
export default function jsepTreeToFormula(node) {
if (node.type === 'BinaryExpression' || node.type === 'LogicalExpression') {
return '(' + jsepTreeToFormula(node.left) + ' ' + node.operator + ' ' + jsepTreeToFormula(node.right) + ')'
return (
'(' +
jsepTreeToFormula(node.left) +
' ' +
node.operator +
' ' +
jsepTreeToFormula(node.right) +
')'
);
}
if (node.type === 'UnaryExpression') {
return node.operator + jsepTreeToFormula(node.argument)
return node.operator + jsepTreeToFormula(node.argument);
}
if (node.type === 'MemberExpression') {
return jsepTreeToFormula(node.object) + '[' + jsepTreeToFormula(node.property) + ']'
return (
jsepTreeToFormula(node.object) +
'[' +
jsepTreeToFormula(node.property) +
']'
);
}
if (node.type === 'Identifier') {
return node.name
return node.name;
}
if (node.type === 'Literal') {
if (typeof node.value === 'string') {
return '"' + node.value + '"'
return '"' + node.value + '"';
}
return '' + node.value
return '' + node.value;
}
if (node.type === 'CallExpression') {
return jsepTreeToFormula(node.callee) + '(' + node.arguments.map(jsepTreeToFormula).join(', ') + ')'
return (
jsepTreeToFormula(node.callee) +
'(' +
node.arguments.map(jsepTreeToFormula).join(', ') +
')'
);
}
if (node.type === 'ArrayExpression') {
return '[' + node.elements.map(jsepTreeToFormula).join(', ') + ']'
return '[' + node.elements.map(jsepTreeToFormula).join(', ') + ']';
}
if (node.type === 'Compound') {
return node.body.map(e => jsepTreeToFormula(e)).join(' ')
return node.body.map(e => jsepTreeToFormula(e)).join(' ');
}
if (node.type === 'ConditionalExpression') {
return jsepTreeToFormula(node.test) + ' ? ' + jsepTreeToFormula(node.consequent) + ' : ' + jsepTreeToFormula(node.alternate)
return (
jsepTreeToFormula(node.test) +
' ? ' +
jsepTreeToFormula(node.consequent) +
' : ' +
jsepTreeToFormula(node.alternate)
);
}
return ''
}
return '';
}

23
packages/nocodb/src/lib/noco/common/helpers/updateColumnNameInFormula.ts

@ -1,16 +1,15 @@
import jsepTreeToFormula from "./jsepTreeToFormula";
import jsepTreeToFormula from './jsepTreeToFormula';
export default function (args: {
virtualColumns,
oldColumnName: string,
newColumnName: string,
export default function(args: {
virtualColumns;
oldColumnName: string;
newColumnName: string;
}): void | boolean {
let modified = false;
const fn = (pt) => {
const fn = pt => {
if (pt.type === 'CallExpression') {
pt.arguments.map(arg => fn(arg))
pt.arguments.map(arg => fn(arg));
} else if (pt.type === 'Literal') {
} else if (pt.type === 'Identifier') {
if (pt.name === args.oldColumnName) {
@ -24,14 +23,14 @@ export default function (args: {
};
if (!args.virtualColumns) {
return
return;
}
for (const v of args.virtualColumns) {
if (!v.formula?.tree) {
continue;
}
fn(v.formula.tree)
v.formula.value = jsepTreeToFormula(v.formula.tree)
fn(v.formula.tree);
v.formula.value = jsepTreeToFormula(v.formula.tree);
}
return modified;
}
}

2503
packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts

File diff suppressed because it is too large Load Diff

411
packages/nocodb/src/lib/noco/gql/GqlAuthResolver.ts

@ -1,45 +1,46 @@
import {promisify} from 'util';
import { promisify } from 'util';
import bcrypt from 'bcryptjs';
import * as ejs from 'ejs';
import * as jwt from 'jsonwebtoken';
import passport from 'passport';
import {ExtractJwt, Strategy} from 'passport-jwt';
import { ExtractJwt, Strategy } from 'passport-jwt';
import IEmailAdapter from "../../../interface/IEmailAdapter";
import {DbConfig, NcConfig} from "../../../interface/config";
import {Knex, XKnex} from "../../dataMapper";
import Noco from "../Noco";
import IEmailAdapter from '../../../interface/IEmailAdapter';
import { DbConfig, NcConfig } from '../../../interface/config';
import { Knex, XKnex } from '../../dataMapper';
import Noco from '../Noco';
import authSchema from './auth/schema';
const {v4: uuidv4} = require('uuid');
const { v4: uuidv4 } = require('uuid');
const PassportLocalStrategy = require('passport-local').Strategy;
const autoBind = require('auto-bind');
const {isEmail} = require('validator');
const { isEmail } = require('validator');
// import swaggerUi from 'swagger-ui-express';
passport.serializeUser(function ({id, email, email_verified, roles, provider, firstname, lastname}, done) {
passport.serializeUser(function(
{ id, email, email_verified, roles, provider, firstname, lastname },
done
) {
done(null, {
id,
email,
email_verified,
provider,
firstname, lastname,
firstname,
lastname,
roles: (roles || '')
.split(',')
.reduce((obj, role) => ({...obj, [role]: true}), {})
})
;
.reduce((obj, role) => ({ ...obj, [role]: true }), {})
});
});
passport.deserializeUser(function (user, done) {
passport.deserializeUser(function(user, done) {
done(null, user);
});
export default class GqlAuthResolver {
private app: Noco;
private dbDriver: Knex;
@ -48,58 +49,60 @@ export default class GqlAuthResolver {
private jwtOptions: any;
constructor(app: Noco, dbDriver: XKnex, connectionConfig: DbConfig, config: NcConfig) {
constructor(
app: Noco,
dbDriver: XKnex,
connectionConfig: DbConfig,
config: NcConfig
) {
this.app = app;
this.dbDriver = dbDriver;
this.connectionConfig = connectionConfig;
this.config = config;
autoBind(this);
this.jwtOptions = {}
this.jwtOptions = {};
this.jwtOptions.jwtFromRequest = ExtractJwt.fromHeader('xc-auth');
this.jwtOptions.secretOrKey = this.config?.auth?.jwt?.secret ?? 'secret';
if (this.config?.auth?.jwt?.options) {
Object.assign(this.jwtOptions, this.config?.auth?.jwt?.options);
}
}
get users() {
return this.dbDriver('xc_users')
return this.dbDriver('xc_users');
}
public async init() {
await this.emailClient?.init();
await this.createTableIfNotExist();
this.initStrategies();
this.app.router.use(passport.initialize())
this.app.router.use(passport.initialize());
const apiPrefix = this.connectionConfig?.meta?.api?.prefix || 'v1';
this.app.router.get('/password/reset/:token', function (req, res) {
this.app.router.get('/password/reset/:token', function(req, res) {
res.render(__dirname + '/auth/resetPassword', {
token: JSON.stringify(req.params?.token),
baseUrl: `/api/${apiPrefix}/`
});
});
this.app.router.get('/email/verify/:token', function (req, res) {
this.app.router.get('/email/verify/:token', function(req, res) {
res.render(__dirname + '/auth/emailVerify', {
token: JSON.stringify(req.params?.token),
baseUrl: `/api/${apiPrefix}/`
});
});
this.app.router.get('/signin', function (_req, res) {
this.app.router.get('/signin', function(_req, res) {
res.render(__dirname + '/auth/signin', {
baseUrl: `/api/${apiPrefix}/`
});
});
this.app.router.get('/signup', function (_req, res) {
this.app.router.get('/signup', function(_req, res) {
res.render(__dirname + '/auth/signup', {
baseUrl: `/api/${apiPrefix}/`
});
@ -107,65 +110,76 @@ export default class GqlAuthResolver {
this.app.router.use(async (req, res, next) => {
const user = await new Promise(resolve => {
passport.authenticate('jwt', {session: false}, (_err, user, _info) => {
if (user) {
return resolve(user);
passport.authenticate(
'jwt',
{ session: false },
(_err, user, _info) => {
if (user) {
return resolve(user);
}
resolve({ roles: 'guest' });
}
resolve({roles: 'guest'})
})(req, res, next);
})
)(req, res, next);
});
await promisify((req as any).login.bind(req))(user);
next();
});
}
public initStrategies() {
const self = this;
passport.use(new Strategy(this.jwtOptions, (jwt_payload, done) => {
this.users.where({
email: jwt_payload?.email
}).first().then(user => {
if (user) {
return done(null, user);
} else {
return done(new Error('User not found'));
}
}).catch(err => {
return done(err);
passport.use(
new Strategy(this.jwtOptions, (jwt_payload, done) => {
this.users
.where({
email: jwt_payload?.email
})
.first()
.then(user => {
if (user) {
return done(null, user);
} else {
return done(new Error('User not found'));
}
})
.catch(err => {
return done(err);
});
})
}));
passport.use(new PassportLocalStrategy({
usernameField: 'email',
session: false
}, async function (email, password, done) {
try {
const user = await self.users.where({email}).first();
if (!user) {
return done({msg: `Email ${email} is not registered!`});
}
);
passport.use(
new PassportLocalStrategy(
{
usernameField: 'email',
session: false
},
async function(email, password, done) {
try {
const user = await self.users.where({ email }).first();
if (!user) {
return done({ msg: `Email ${email} is not registered!` });
}
const hashedPassword = await promisify(bcrypt.hash)(password, user.salt);
if (user.password !== hashedPassword) {
return done({msg: `Password not valid!`});
} else {
return done(null, user);
const hashedPassword = await promisify(bcrypt.hash)(
password,
user.salt
);
if (user.password !== hashedPassword) {
return done({ msg: `Password not valid!` });
} else {
return done(null, user);
}
} catch (e) {
done(e);
}
} catch (e) {
done(e);
}
}
));
)
);
}
public getResolvers() {
return {
mapResolvers: () => ({
@ -178,65 +192,68 @@ export default class GqlAuthResolver {
TokenVerify: this.tokenValidate,
ChangePassword: this.passwordChange
})
}
};
}
public getSchema() {
return authSchema;
}
public async signin(args, {req, res, next}) {
public async signin(args, { req, res, next }) {
req.body = args.data;
return new Promise((resolve, reject) => {
passport.authenticate('local', {session: false}, async (err, user, info): Promise<any> => {
try {
if (!user || !user.email) {
if (err) {
return reject(err)
}
if (info) {
return reject(info)
passport.authenticate(
'local',
{ session: false },
async (err, user, info): Promise<any> => {
try {
if (!user || !user.email) {
if (err) {
return reject(err);
}
if (info) {
return reject(info);
}
return reject({ msg: 'Your signin has failed' });
}
return reject({msg: 'Your signin has failed'});
}
await promisify((req as any).login.bind(req))(user);
resolve({
token: jwt.sign({
email: user.email,
firstname: user.firstname,
lastname: user.lastname,
id: user.id,
roles: user.roles
}, this.jwtOptions.secretOrKey, this.config?.auth?.jwt?.options)
});
} catch (e) {
console.log(e);
reject(e);
await promisify((req as any).login.bind(req))(user);
resolve({
token: jwt.sign(
{
email: user.email,
firstname: user.firstname,
lastname: user.lastname,
id: user.id,
roles: user.roles
},
this.jwtOptions.secretOrKey,
this.config?.auth?.jwt?.options
)
});
} catch (e) {
console.log(e);
reject(e);
}
}
})(req, res, next);
)(req, res, next);
});
}
public async signup(args, {req}) {
const {email, firstname, lastname} = args.data;
let {password} = args.data;
public async signup(args, { req }) {
const { email, firstname, lastname } = args.data;
let { password } = args.data;
if (!isEmail(email)) {
throw new Error(`Not a valid email`);
}
let user = await this.users.where({
email
}).first();
let user = await this.users
.where({
email
})
.first();
if (user) {
throw new Error(`Email '${email}' already registered`);
@ -251,90 +268,102 @@ export default class GqlAuthResolver {
const email_verification_token = uuidv4();
await this.users.insert({
firstname, lastname,
firstname,
lastname,
email,
salt,
password,
email_verification_token
});
user = await this.users.where({
email
}).first();
user = await this.users
.where({
email
})
.first();
try {
const template = (await import('./emailTemplate/verify')).default;
await this.emailClient.mailSend({
to: email,
subject: "Verify email",
subject: 'Verify email',
html: ejs.render(template, {
verifyLink: `${req.ncSiteUrl}/email/verify/${user.email_verification_token}`
})
})
});
} catch (e) {
console.log('Warning : `mailSend` failed, Please configure email configuration.');
console.log(
'Warning : `mailSend` failed, Please configure email configuration.'
);
}
await promisify((req as any).login.bind(req))(user);
user = (req as any).user;
return {
token: jwt.sign({
email: user.email,
firstname: user.firstname,
lastname: user.lastname,
id: user.id,
roles: user.roles
}, this.jwtOptions.secretOrKey, this.config?.auth?.jwt?.options)
token: jwt.sign(
{
email: user.email,
firstname: user.firstname,
lastname: user.lastname,
id: user.id,
roles: user.roles
},
this.jwtOptions.secretOrKey,
this.config?.auth?.jwt?.options
)
};
}
public async passwordForgot(args, {req}) {
public async passwordForgot(args, { req }) {
const email = args.email;
if (!email) {
throw (new Error('Please enter your email address.'));
throw new Error('Please enter your email address.');
}
const user = await this.users.where({email}).first();
const user = await this.users.where({ email }).first();
if (!user) {
throw (new Error('This email is not registered with us.'));
throw new Error('This email is not registered with us.');
}
const token = uuidv4();
await this.users.update({
reset_password_token: token,
reset_password_expires: new Date(Date.now() + (60 * 60 * 1000))
}).where({id: user.id});
await this.users
.update({
reset_password_token: token,
reset_password_expires: new Date(Date.now() + 60 * 60 * 1000)
})
.where({ id: user.id });
try {
const template = (await import('./emailTemplate/forgotPassword')).default;
await this.emailClient.mailSend({
to: user.email,
subject: "Password Reset Link",
subject: 'Password Reset Link',
text: `Visit following link to update your password : ${req.ncSiteUrl}/password/reset/${token}.`,
html: ejs.render(template, {
resetLink: `${req.ncSiteUrl}/password/reset/${token}`
})
})
});
} catch (e) {
console.log('Warning : `mailSend` failed, Please configure email configuration.');
console.log(
'Warning : `mailSend` failed, Please configure email configuration.'
);
}
console.log(`Password reset token : ${token}`)
console.log(`Password reset token : ${token}`);
return true;
}
public async tokenValidate(args) {
const token = args.tokenId;
const user = await this.users.where({reset_password_token: token}).first();
const user = await this.users
.where({ reset_password_token: token })
.first();
if (!user || !user.email) {
throw new Error('Invalid token');
}
@ -345,97 +374,107 @@ export default class GqlAuthResolver {
return true;
}
public async passwordReset(args) {
const token = args.tokenId;
const user = await this.users.where({reset_password_token: token}).first();
const user = await this.users
.where({ reset_password_token: token })
.first();
if (!user) {
throw (new Error('Invalid token'));
throw new Error('Invalid token');
}
if (user.reset_password_expires < new Date()) {
throw (new Error('Password reset url expired'));
throw new Error('Password reset url expired');
}
if (user.provider && user.provider !== 'local') {
throw (new Error('Email registered via social account'));
throw new Error('Email registered via social account');
}
const salt = await promisify(bcrypt.genSalt)(10);
const password = await promisify(bcrypt.hash)(args.password, salt);
await this.users.update({
salt, password,
reset_password_expires: null,
reset_password_token: ''
}).where({
id: user.id
});
await this.users
.update({
salt,
password,
reset_password_expires: null,
reset_password_token: ''
})
.where({
id: user.id
});
return true
return true;
}
public async passwordChange(args, {req}): Promise<any> {
const {currentPassword, newPassword} = args;
public async passwordChange(args, { req }): Promise<any> {
const { currentPassword, newPassword } = args;
if (!req.isAuthenticated() || !req.user.id) {
throw new Error('Not authenticated')
throw new Error('Not authenticated');
}
if (!currentPassword || !newPassword) {
throw new Error('Missing new/old password')
throw new Error('Missing new/old password');
}
const user = await this.users.where({email: req.user.email}).first();
const hashedPassword = await promisify(bcrypt.hash)(currentPassword, user.salt);
const user = await this.users.where({ email: req.user.email }).first();
const hashedPassword = await promisify(bcrypt.hash)(
currentPassword,
user.salt
);
if (hashedPassword !== user.password) {
throw new Error('Current password is wrong')
throw new Error('Current password is wrong');
}
const salt = await promisify(bcrypt.genSalt)(10);
const password = await promisify(bcrypt.hash)(newPassword, salt);
await this.users.update({
salt, password
}).where({id: user.id});
await this.users
.update({
salt,
password
})
.where({ id: user.id });
return true;
}
public async emailVerification(args) {
const token = args.tokenId;
const user = await this.users.where({email_verification_token: token}).first();
const user = await this.users
.where({ email_verification_token: token })
.first();
if (!user) {
throw new Error('Invalid verification url');
}
await this.users.update({
email_verification_token: '',
email_verified: true
}).where({id: user.id});
await this.users
.update({
email_verification_token: '',
email_verified: true
})
.where({ id: user.id });
return true;
}
public async me(_args, {req}) {
public async me(_args, { req }) {
return req?.user ?? {};
}
public async updateUser(req, res) {
await this.users.update({
firstname: req.body.firstname,
lastname: req.body.lastname,
}).where({
id: req.user.id
})
res.json({msg: 'Updated successfully'});
await this.users
.update({
firstname: req.body.firstname,
lastname: req.body.lastname
})
.where({
id: req.user.id
});
res.json({ msg: 'Updated successfully' });
}
private async createTableIfNotExist() {
if (!(await this.dbDriver.schema.hasTable('xc_users'))) {
await this.dbDriver.schema.createTable('xc_users', function (table) {
await this.dbDriver.schema.createTable('xc_users', function(table) {
table.increments();
table.string('email');
table.string('password', 255);
@ -450,15 +489,15 @@ export default class GqlAuthResolver {
table.boolean('email_verified');
table.string('roles', 255).defaultTo('editor');
table.timestamps();
})
});
}
}
private get emailClient(): IEmailAdapter {
return this.app?.metaMgr?.emailAdapter;
}
}/**
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>

22
packages/nocodb/src/lib/noco/gql/GqlBaseResolver.ts

@ -1,9 +1,12 @@
export default class GqlBaseResolver {
constructor() {
}
constructor() {}
// todo: type correction
public static applyMiddlewares(handlers: any[] = [], resolvers = {}, postHandlers: any[] = []): any {
public static applyMiddlewares(
handlers: any[] = [],
resolvers = {},
postHandlers: any[] = []
): any {
if (!handlers) {
return resolvers;
}
@ -11,31 +14,29 @@ export default class GqlBaseResolver {
resolvers[name] = async (...args) => {
try {
for (const handler of handlers) {
await handler(...args)
await handler(...args);
}
const result = await (resolver as any)(...args);
if (postHandlers) {
for (const handler of postHandlers) {
await handler(result, ...args)
await handler(result, ...args);
}
}
return result;
} catch (e) {
throw e;
}
}
};
}
return resolvers;
}
protected generateResolverFromStringBody(fnBody: string[]): any {
if (!(fnBody && Array.isArray(fnBody) && fnBody.length)) {
return;
}
// @ts-ignore
let handler = (args) => {
let handler = args => {
return null;
};
@ -53,9 +54,8 @@ export default class GqlBaseResolver {
// tslint:disable-next-line:no-eval
handler = eval(js);
// console.timeEnd('startTrans')
} catch (e) {
console.log('Error in transpilation', e)
console.log('Error in transpilation', e);
}
// tslint:disable-next-line:no-eval

27
packages/nocodb/src/lib/noco/gql/GqlCommonResolvers.ts

@ -1,16 +1,23 @@
import {BaseModelSql} from "../../dataMapper";
import { BaseModelSql } from '../../dataMapper';
export const m2mNotChildren = ({models = {}}: { models: { [key: string]: BaseModelSql } }) => {
return async (args) => {
export const m2mNotChildren = ({
models = {}
}: {
models: { [key: string]: BaseModelSql };
}) => {
return async args => {
return models[args?.parent]?.m2mNotChildren(args);
}
}
export const m2mNotChildrenCount = ({models = {}}: { models: { [key: string]: BaseModelSql } }) => {
return async (args) => {
};
};
export const m2mNotChildrenCount = ({
models = {}
}: {
models: { [key: string]: BaseModelSql };
}) => {
return async args => {
return models[args?.parent]?.m2mNotChildrenCount(args);
}
}
};
};
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd

107
packages/nocodb/src/lib/noco/gql/GqlMiddleware.ts

@ -1,7 +1,7 @@
import autoBind from 'auto-bind';
import Handlebars from "handlebars";
import Handlebars from 'handlebars';
import {Acls} from "../../../interface/config";
import { Acls } from '../../../interface/config';
export default class GqlMiddleware {
private tn: any;
@ -9,7 +9,6 @@ export default class GqlMiddleware {
private models: any;
constructor(acls: Acls, tn: string, middleWareBody?: string, models?: any) {
autoBind(this);
this.acls = acls;
this.tn = tn;
@ -18,7 +17,7 @@ export default class GqlMiddleware {
if (middleWareBody) {
Object.defineProperty(this, 'middleware', {
value: this.generateResolverFromStringBody(middleWareBody)
})
});
}
}
@ -27,12 +26,11 @@ export default class GqlMiddleware {
}
private generateResolverFromStringBody(fnBody: string): any {
if (!(fnBody && fnBody.length)) {
return;
}
// @ts-ignore
let handler = (args) => {
let handler = args => {
return null;
};
@ -41,40 +39,38 @@ export default class GqlMiddleware {
// tslint:disable-next-line:no-eval
handler = eval(js);
} catch (e) {
console.log('Error in GQL Middleware transpilation', e)
console.log('Error in GQL Middleware transpilation', e);
}
return handler;
}
// @ts-ignore
public async middleware(_args, {req, res, next}, info: any): Promise<any> {
const replaceEnvVarRec = (obj) => {
public async middleware(_args, { req, res, next }, info: any): Promise<any> {
const replaceEnvVarRec = obj => {
return JSON.parse(JSON.stringify(obj), (_key, value) => {
return typeof value === 'string' ? Handlebars.compile(value, {noEscape: true})({
req
// : {
// user: {id: 1} // (req as any).user
// }
}) : value;
return typeof value === 'string'
? Handlebars.compile(value, { noEscape: true })({
req
// : {
// user: {id: 1} // (req as any).user
// }
})
: value;
});
}
};
const getOperation = (operation, fieldName) => {
if (operation === 'mutation') {
if (fieldName.endsWith('Create')) {
return 'create'
return 'create';
} else if (fieldName.endsWith('Update')) {
return 'update'
return 'update';
} else if (fieldName.endsWith('Delete')) {
return 'delete'
return 'delete';
}
}
return 'read';
}
};
const roleOperationPossible = (roles, operation, object) => {
res.locals.xcAcl = null;
@ -90,7 +86,10 @@ export default class GqlMiddleware {
if (this.acl[roleName][operation]) {
return true;
}
} else if (this.acl?.[roleName]?.[operation] && roleOperationObjectGet(roleName, operation, object)) {
} else if (
this.acl?.[roleName]?.[operation] &&
roleOperationObjectGet(roleName, operation, object)
) {
return true;
}
} catch (e) {
@ -98,32 +97,39 @@ export default class GqlMiddleware {
}
}
if (errors?.length) {
throw errors[0]
throw errors[0];
}
return false;
}
};
// @ts-ignore
const roleOperationObjectGet = (role, operation, object) => {
const columns = this.acl[role][operation].columns;
if (columns) {
// todo: merge allowed columns if multiple roles
const allowedCols = Object.keys(columns).filter(col => columns[col])
res.locals.xcAcl = {allowedCols, operation, columns};
const allowedCols = Object.keys(columns).filter(col => columns[col]);
res.locals.xcAcl = { allowedCols, operation, columns };
if (info.fieldName.endsWith('Update') || info.fieldName.endsWith('Create')) {
if (
info.fieldName.endsWith('Update') ||
info.fieldName.endsWith('Create')
) {
if (Array.isArray(object)) {
for (const row of object) {
for (const colInReq of Object.keys(row)) {
if (!allowedCols.includes(colInReq)) {
throw new Error(`User doesn't have permission to add/edit '${colInReq}' column`);
throw new Error(
`User doesn't have permission to add/edit '${colInReq}' column`
);
}
}
}
} else {
for (const colInReq of Object.keys(object)) {
if (!allowedCols.includes(colInReq)) {
throw new Error(`User doesn't have permission to edit '${colInReq}' column`);
throw new Error(
`User doesn't have permission to edit '${colInReq}' column`
);
}
}
}
@ -131,25 +137,34 @@ export default class GqlMiddleware {
} else {
if (this.acl?.[role]?.[operation]?.custom) {
if (this.acl?.[role]?.[operation]?.custom) {
const condition = replaceEnvVarRec(this.acl?.[role]?.[operation]?.custom)
_args.conditionGraph = {condition, models: this.models};
const condition = replaceEnvVarRec(
this.acl?.[role]?.[operation]?.custom
);
_args.conditionGraph = { condition, models: this.models };
}
}
return Object.values(columns).some(Boolean);
}
}
}
const roles = (req as any)?.locals?.user?.roles ?? (req as any)?.session?.passport?.user?.roles ?? {
guest: true
};
const roles = (req as any)?.locals?.user?.roles ??
(req as any)?.session?.passport?.user?.roles ?? {
guest: true
};
try {
const allowed = roleOperationPossible(roles, getOperation(info.operation.operation, info.fieldName), _args?.data);
const allowed = roleOperationPossible(
roles,
getOperation(info.operation.operation, info.fieldName),
_args?.data
);
if (allowed) {
return;
} else {
const msg = roles.guest ? `Access Denied : Please Login or Signup for a new account` : `Access Denied for this account`;
const msg = roles.guest
? `Access Denied : Please Login or Signup for a new account`
: `Access Denied for this account`;
throw new Error(msg);
}
} catch (e) {
@ -158,13 +173,13 @@ export default class GqlMiddleware {
}
// @ts-ignore
public async postMiddleware(data, args, {req, res}, info): Promise<any> {
public async postMiddleware(data, args, { req, res }, info): Promise<any> {
if (!res.locals.xcAcl) {
return data;
}
// @ts-ignore
const {allowedCols, operation, columns} = res.locals.xcAcl;
const { allowedCols, operation, columns } = res.locals.xcAcl;
if (!columns) {
return data;
@ -201,18 +216,16 @@ export default class GqlMiddleware {
delete data[colInReq];
}
}
}
return data;
}
public async postLoaderMiddleware(...args): Promise<any> {
return this.postMiddleware(args[0], args[2], args[3], args[4])
return this.postMiddleware(args[0], args[2], args[3], args[4]);
}
}/**
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>

59
packages/nocodb/src/lib/noco/gql/GqlProcedureResolver.ts

@ -1,17 +1,20 @@
import autoBind from 'auto-bind';
import BaseProcedure from "../common/BaseProcedure";
import XcProcedure from "../common/XcProcedure";
import BaseProcedure from '../common/BaseProcedure';
import XcProcedure from '../common/XcProcedure';
import {GqlApiBuilder} from "./GqlApiBuilder";
import GqlBaseResolver from "./GqlBaseResolver";
export class GqlProcedureResolver
extends BaseProcedure {
import { GqlApiBuilder } from './GqlApiBuilder';
import GqlBaseResolver from './GqlBaseResolver';
export class GqlProcedureResolver extends BaseProcedure {
private acls: { [aclName: string]: { [role: string]: boolean } };
constructor(builder: GqlApiBuilder, functions: any[], procedures: any[], acls) {
constructor(
builder: GqlApiBuilder,
functions: any[],
procedures: any[],
acls
) {
super();
autoBind(this);
this.builder = builder;
@ -22,90 +25,90 @@ export class GqlProcedureResolver
}
public fnHandler(name) {
return (async (args) => {
return (async args => {
let body = [];
try {
body = JSON.parse(args.body);
} catch (_e) {
}
} catch (_e) {}
const result = await this.xcProcedure.callFunction(name, body);
return JSON.stringify(result, null, 2);
}).bind(this)
}).bind(this);
}
private procHandler(name) {
// @ts-ignore
return (async (args) => {
return (async args => {
let body = [];
try {
body = JSON.parse(args.body);
} catch (_e) {
}
} catch (_e) {}
const result = await this.xcProcedure.callProcedure(name, body);
return JSON.stringify(result, null, 2);
}).bind(this)
}).bind(this);
}
public mapResolvers(): any {
const resolvers = {};
if (this.functions) {
for (const {function_name} of this.functions) {
for (const { function_name } of this.functions) {
resolvers[`_${function_name}`] = this.fnHandler(function_name);
}
}
if (this.procedures) {
for (const {procedure_name} of this.procedures) {
for (const { procedure_name } of this.procedures) {
resolvers[`_${procedure_name}`] = this.procHandler(procedure_name);
}
}
return GqlBaseResolver.applyMiddlewares([this.middleware], resolvers);
}
public getSchema() {
if (!this.functions?.length && !this.procedures?.length) {
return ''
return '';
}
let resolvers = `
type Mutation {
`;
if (this.functions) {
for (const {function_name} of this.functions) {
for (const { function_name } of this.functions) {
resolvers += `_${function_name}(body:String):String\r\n`;
}
}
if (this.procedures) {
for (const {procedure_name} of this.procedures) {
for (const { procedure_name } of this.procedures) {
resolvers += `_${procedure_name}(body:String):String\r\n`;
}
}
return resolvers + `}\r\n`;
}
updateMiddlewareBody(_body: string) { }
updateMiddlewareBody(_body: string) {}
private async middleware(_args, {req}, info: any) {
private async middleware(_args, { req }, info: any) {
const roles = (req as any)?.session?.passport?.user?.roles ?? {
guest: true
};
try {
const allowed = Object.keys(roles).some(role => roles[role] && this.acls?.[info.fieldName.slice(1)]?.[role]);
const allowed = Object.keys(roles).some(
role => roles[role] && this.acls?.[info.fieldName.slice(1)]?.[role]
);
if (allowed) {
// any additional rules can be made here
return;
} else {
const msg = roles.guest ? `Access Denied : Please Login or Signup for a new account` : `Access Denied for this account`;
const msg = roles.guest
? `Access Denied : Please Login or Signup for a new account`
: `Access Denied for this account`;
throw new Error(msg);
}
} catch (e) {
throw e
throw e;
}
}
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*

215
packages/nocodb/src/lib/noco/gql/GqlResolver.ts

@ -1,30 +1,36 @@
import autoBind from 'auto-bind';
import {Acls} from "../../../interface/config";
import {BaseModelSql} from "../../dataMapper";
import Noco from "../Noco";
import { Acls } from '../../../interface/config';
import { BaseModelSql } from '../../dataMapper';
import Noco from '../Noco';
import GqlBaseResolver from "./GqlBaseResolver";
import GqlMiddleware from "./GqlMiddleware";
import GqlBaseResolver from './GqlBaseResolver';
import GqlMiddleware from './GqlMiddleware';
function parseHrtimeToSeconds(hrtime) {
const seconds = (hrtime[0] + (hrtime[1] / 1e6)).toFixed(3);
const seconds = (hrtime[0] + hrtime[1] / 1e6).toFixed(3);
return seconds;
}
export default class GqlResolver extends GqlBaseResolver {
// @ts-ignore
private app: Noco;
private models: { [key: string]: BaseModelSql };
private table: string;
private typeClass: new(obj: any) => any;
private typeClass: new (obj: any) => any;
private acls: Acls;
private functions: { [key: string]: any };
private middlewareStringBody?: string;
constructor(app: Noco, models: { [key: string]: BaseModelSql }, table: string, typeClass: { new(obj: any): any }, acls: Acls, functions: { [key: string]: string[] }, middlewareStringBody?: string) {
constructor(
app: Noco,
models: { [key: string]: BaseModelSql },
table: string,
typeClass: { new (obj: any): any },
acls: Acls,
functions: { [key: string]: string[] },
middlewareStringBody?: string
) {
super();
autoBind(this);
this.app = app;
@ -36,19 +42,24 @@ export default class GqlResolver extends GqlBaseResolver {
this.middlewareStringBody = middlewareStringBody;
}
private get model(): BaseModelSql {
return this.models?.[this.table];
}
public async list(args, {req, res}: { req: any & { model: BaseModelSql }, res: any }): Promise<any> {
public async list(
args,
{ req, res }: { req: any & { model: BaseModelSql }; res: any }
): Promise<any> {
const startTime = process.hrtime();
try {
if (args.conditionGraph && typeof args.conditionGraph === 'string') {
args.conditionGraph = {models: this.models, condition: JSON.parse(args.conditionGraph)}
args.conditionGraph = {
models: this.models,
condition: JSON.parse(args.conditionGraph)
};
}
if (args.condition && typeof args.condition === 'string') {
args.condition = JSON.parse(args.condition)
args.condition = JSON.parse(args.condition);
}
} catch (e) {
/* ignore parse error */
@ -57,63 +68,66 @@ export default class GqlResolver extends GqlBaseResolver {
const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime));
res.setHeader('xc-db-response', elapsedSeconds);
return (data).map(o => {
return data.map(o => {
return new req.gqlType(o);
});
}
public async create(args, {req}): Promise<any> {
public async create(args, { req }): Promise<any> {
const data = await req.model.insert(args.data, null, req);
return new req.gqlType(data);
}
public async read(args, {req}): Promise<any> {
public async read(args, { req }): Promise<any> {
const data = await req.model.readByPk(args.id, args);
return new req.gqlType(data);
}
public async update(args, {req}): Promise<any> {
public async update(args, { req }): Promise<any> {
const data = await req.model.updateByPk(args.id, args.data, null, req);
return data;
}
public async delete(args, {req}): Promise<any> {
public async delete(args, { req }): Promise<any> {
const data = await req.model.delByPk(args.id, null, req);
return data;
}
public async exists(args, {req}): Promise<any> {
public async exists(args, { req }): Promise<any> {
const data = await req.model.exists(args.id, args);
return data;
}
public async findOne(args, {req}): Promise<any> {
public async findOne(args, { req }): Promise<any> {
const data = await req.model.findOne(args);
return new req.gqlType(data);
}
public async groupBy(args, {req}): Promise<any> {
public async groupBy(args, { req }): Promise<any> {
const data = await req.model.groupBy(args);
return data;
}
public async aggregate(args, {req}): Promise<any> {
public async aggregate(args, { req }): Promise<any> {
const data = await req.model.aggregate(args);
return data;
}
public async distinct(args, {req}): Promise<any> {
public async distinct(args, { req }): Promise<any> {
const data = (await req.model.distinct(args)).map(d => new req.gqlType(d));
return data;
}
public async count(args, {req}): Promise<any> {
public async count(args, { req }): Promise<any> {
try {
if (args.conditionGraph && typeof args.conditionGraph === 'string') {
args.conditionGraph = {models: this.models, condition: JSON.parse(args.conditionGraph)}
args.conditionGraph = {
models: this.models,
condition: JSON.parse(args.conditionGraph)
};
}
if (args.condition && typeof args.condition === 'string') {
args.condition = JSON.parse(args.condition)
args.condition = JSON.parse(args.condition);
}
} catch (e) {
/* ignore parse error */
@ -122,62 +136,145 @@ export default class GqlResolver extends GqlBaseResolver {
return data.count;
}
public async distribution(args, {req}): Promise<any> {
public async distribution(args, { req }): Promise<any> {
const data = await req.model.distribution(args);
return data;
}
public async createb(args, {req}): Promise<any> {
public async createb(args, { req }): Promise<any> {
const data = await req.model.insertb(args.data);
return data;
}
public async updateb(args, {req}): Promise<any> {
public async updateb(args, { req }): Promise<any> {
const data = await req.model.updateb(args.data);
return data;
}
public async deleteb(args, {req}): Promise<any> {
public async deleteb(args, { req }): Promise<any> {
const data = await req.model.delb(args.data);
return data;
}
public updateMiddlewareBody(body: string): this {
this.middlewareStringBody = body;
return this;
}
public mapResolvers(customResolver: any): any {
const mw = new GqlMiddleware(this.acls, this.table, this.middlewareStringBody, this.models);
const mw = new GqlMiddleware(
this.acls,
this.table,
this.middlewareStringBody,
this.models
);
// todo: replace with inflection
const name = this.model._tn;
return GqlResolver.applyMiddlewares([(_, {req}) => {
req.models = this.models;
req.model = this.model;
req.gqlType = this.typeClass;
}, mw.middleware], {
...(customResolver?.additional?.[this.table] || {}),
[`${name}List`]: customResolver?.override?.[`${name}List`] || this.generateResolverFromStringBody(this.functions[`${name}List`]) || this.list,
[`${name}FindOne`]: customResolver?.override?.[`${name}FindOne`] || this.generateResolverFromStringBody(this.functions[`${name}FindOne`]) || this.findOne,
[`${name}Count`]: customResolver?.override?.[`${name}Count`] || this.generateResolverFromStringBody(this.functions[`${name}Count`]) || this.count,
[`${name}Distinct`]: customResolver?.override?.[`${name}Distinct`] || this.generateResolverFromStringBody(this.functions[`${name}Distinct`]) || this.distinct,
[`${name}GroupBy`]: customResolver?.override?.[`${name}GroupBy`] || this.generateResolverFromStringBody(this.functions[`${name}GroupBy`]) || this.groupBy,
[`${name}Aggregate`]: customResolver?.override?.[`${name}Aggregate`] || this.generateResolverFromStringBody(this.functions[`${name}Aggregate`]) || this.aggregate,
[`${name}Distribution`]: customResolver?.override?.[`${name}Distribution`] || this.generateResolverFromStringBody(this.functions[`${name}Distribution`]) || this.distribution,
...(this.model.type === 'table' ? {
[`${name}Read`]: customResolver?.override?.[`${name}Read`] || this.generateResolverFromStringBody(this.functions[`${name}Read`]) || this.read,
[`${name}Exists`]: customResolver?.override?.[`${name}Exists`] || this.generateResolverFromStringBody(this.functions[`${name}Exists`]) || this.exists,
[`${name}Create`]: customResolver?.override?.[`${name}Create`] || this.generateResolverFromStringBody(this.functions[`${name}Create`]) || this.create,
[`${name}Update`]: customResolver?.override?.[`${name}Update`] || this.generateResolverFromStringBody(this.functions[`${name}Update`]) || this.update,
[`${name}Delete`]: customResolver?.override?.[`${name}Delete`] || this.generateResolverFromStringBody(this.functions[`${name}Delete`]) || this.delete,
[`${name}CreateBulk`]: customResolver?.override?.[`${name}CreateBulk`] || this.generateResolverFromStringBody(this.functions[`${name}CreateBulk`]) || this.createb,
[`${name}UpdateBulk`]: customResolver?.override?.[`${name}UpdateBulk`] || this.generateResolverFromStringBody(this.functions[`${name}UpdateBulk`]) || this.updateb,
[`${name}DeleteBulk`]: customResolver?.override?.[`${name}DeleteBulk`] || this.generateResolverFromStringBody(this.functions[`${name}DeleteBulk`]) || this.deleteb,
} : {})
}, [mw.postMiddleware]);
return GqlResolver.applyMiddlewares(
[
(_, { req }) => {
req.models = this.models;
req.model = this.model;
req.gqlType = this.typeClass;
},
mw.middleware
],
{
...(customResolver?.additional?.[this.table] || {}),
[`${name}List`]:
customResolver?.override?.[`${name}List`] ||
this.generateResolverFromStringBody(this.functions[`${name}List`]) ||
this.list,
[`${name}FindOne`]:
customResolver?.override?.[`${name}FindOne`] ||
this.generateResolverFromStringBody(
this.functions[`${name}FindOne`]
) ||
this.findOne,
[`${name}Count`]:
customResolver?.override?.[`${name}Count`] ||
this.generateResolverFromStringBody(this.functions[`${name}Count`]) ||
this.count,
[`${name}Distinct`]:
customResolver?.override?.[`${name}Distinct`] ||
this.generateResolverFromStringBody(
this.functions[`${name}Distinct`]
) ||
this.distinct,
[`${name}GroupBy`]:
customResolver?.override?.[`${name}GroupBy`] ||
this.generateResolverFromStringBody(
this.functions[`${name}GroupBy`]
) ||
this.groupBy,
[`${name}Aggregate`]:
customResolver?.override?.[`${name}Aggregate`] ||
this.generateResolverFromStringBody(
this.functions[`${name}Aggregate`]
) ||
this.aggregate,
[`${name}Distribution`]:
customResolver?.override?.[`${name}Distribution`] ||
this.generateResolverFromStringBody(
this.functions[`${name}Distribution`]
) ||
this.distribution,
...(this.model.type === 'table'
? {
[`${name}Read`]:
customResolver?.override?.[`${name}Read`] ||
this.generateResolverFromStringBody(
this.functions[`${name}Read`]
) ||
this.read,
[`${name}Exists`]:
customResolver?.override?.[`${name}Exists`] ||
this.generateResolverFromStringBody(
this.functions[`${name}Exists`]
) ||
this.exists,
[`${name}Create`]:
customResolver?.override?.[`${name}Create`] ||
this.generateResolverFromStringBody(
this.functions[`${name}Create`]
) ||
this.create,
[`${name}Update`]:
customResolver?.override?.[`${name}Update`] ||
this.generateResolverFromStringBody(
this.functions[`${name}Update`]
) ||
this.update,
[`${name}Delete`]:
customResolver?.override?.[`${name}Delete`] ||
this.generateResolverFromStringBody(
this.functions[`${name}Delete`]
) ||
this.delete,
[`${name}CreateBulk`]:
customResolver?.override?.[`${name}CreateBulk`] ||
this.generateResolverFromStringBody(
this.functions[`${name}CreateBulk`]
) ||
this.createb,
[`${name}UpdateBulk`]:
customResolver?.override?.[`${name}UpdateBulk`] ||
this.generateResolverFromStringBody(
this.functions[`${name}UpdateBulk`]
) ||
this.updateb,
[`${name}DeleteBulk`]:
customResolver?.override?.[`${name}DeleteBulk`] ||
this.generateResolverFromStringBody(
this.functions[`${name}DeleteBulk`]
) ||
this.deleteb
}
: {})
},
[mw.postMiddleware]
);
}
}
/**

3
packages/nocodb/src/lib/noco/gql/auth/schema.ts

@ -41,7 +41,8 @@ type User {
type XcToken{
token: String
}
`/**
`;
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>

3
packages/nocodb/src/lib/noco/gql/common.schema.ts

@ -49,7 +49,8 @@ type Query{
`/**
`;
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>

2
packages/nocodb/src/lib/noco/gql/emailTemplate/forgotPassword.ts

@ -169,7 +169,7 @@ table[class=body] .article {
</table>
</body>
</html>
`
`;
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*

2
packages/nocodb/src/lib/noco/gql/emailTemplate/verify.ts

@ -206,7 +206,7 @@ export default `<!doctype html>
</table>
</body>
</html>
`
`;
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd

30
packages/nocodb/src/lib/noco/meta/MetaAPILogger.ts

@ -1,8 +1,7 @@
import {XKnex} from "../../dataMapper";
import {Request} from 'express';
import { XKnex } from '../../dataMapper';
import { Request } from 'express';
export default class MetaAPILogger {
static _instance: MetaAPILogger;
knex: XKnex;
@ -13,7 +12,7 @@ export default class MetaAPILogger {
filename: 'noco_log.db'
},
useNullAsDefault: true
})
});
}
async init() {
@ -23,29 +22,26 @@ export default class MetaAPILogger {
});
}
static async mw(req, res, next) {
if (process.env.NC_LOGGER) {
const oldWrite = res.write,
oldEnd = res.end;
const chunks = [];
res.write = function (chunk) {
res.write = function(chunk) {
chunks.push(chunk);
// eslint-disable-next-line prefer-rest-params
oldWrite.apply(res, arguments);
};
res.end = function (chunk) {
if (chunk)
chunks.push(chunk);
res.end = function(chunk) {
if (chunk) chunks.push(chunk);
const body = Buffer.concat(chunks).toString('utf8');
MetaAPILogger.log(req, body)
MetaAPILogger.log(req, body);
// eslint-disable-next-line prefer-rest-params
oldEnd.apply(res, arguments);
};
@ -59,7 +55,7 @@ export default class MetaAPILogger {
}
if (!this._instance) {
this._instance = new MetaAPILogger();
await this._instance.init()
await this._instance.init();
}
await this._instance.knex('nc_log').insert({
path: req.url,
@ -69,10 +65,8 @@ export default class MetaAPILogger {
method: req.method,
operation: req.body?.api,
response: typeof res === 'string' ? res : JSON.stringify(res)
})
});
}
}
class XcLoggerMigrationSource {
@ -81,7 +75,7 @@ class XcLoggerMigrationSource {
// arguments to getMigrationName and getMigration
public getMigrations(): Promise<any> {
// In this example we are just returning migration names
return Promise.resolve(['logger'])
return Promise.resolve(['logger']);
}
public getMigrationName(migration): string {
@ -104,10 +98,10 @@ class XcLoggerMigrationSource {
table.text('response');
table.text('comments');
table.timestamps(true, true);
})
});
},
async down(knex) {
await knex.schema.dropTable('nc_log')
await knex.schema.dropTable('nc_log');
}
};
}

285
packages/nocodb/src/lib/noco/meta/NcMetaIO.ts

@ -1,26 +1,60 @@
import {NcConfig} from "../../../interface/config";
import Noco from "../Noco";
import {XKnex} from "../../dataMapper";
import { NcConfig } from '../../../interface/config';
import Noco from '../Noco';
import { XKnex } from '../../dataMapper';
const META_TABLES = {
graphql: ['nc_models', 'nc_resolvers', 'nc_loaders', 'nc_store', 'nc_hooks', 'nc_roles', 'nc_acl', 'nc_api_tokens', 'nc_relations', 'nc_migrations',
graphql: [
'nc_models',
'nc_resolvers',
'nc_loaders',
'nc_store',
'nc_hooks',
'nc_roles',
'nc_acl',
'nc_api_tokens',
'nc_relations',
'nc_migrations',
'nc_disabled_models_for_role',
'nc_shared_views', 'nc_cron', 'nc_audit'
'nc_shared_views',
'nc_cron',
'nc_audit'
],
grpc: [
'nc_models',
'nc_rpc',
'nc_store',
'nc_hooks',
'nc_roles',
'nc_acl',
'nc_relations',
'nc_migrations',
'nc_api_tokens',
'nc_disabled_models_for_role',
'nc_shared_views',
'nc_cron'
],
grpc: ['nc_models', 'nc_rpc', 'nc_store', 'nc_hooks', 'nc_roles', 'nc_acl', 'nc_relations', 'nc_migrations', 'nc_api_tokens', 'nc_disabled_models_for_role',
'nc_shared_views', 'nc_cron'],
rest: [
'nc_models', 'nc_routes', 'nc_store', 'nc_hooks', 'nc_roles', 'nc_acl', 'nc_relations', 'nc_migrations', 'nc_api_tokens',
'nc_models',
'nc_routes',
'nc_store',
'nc_hooks',
'nc_roles',
'nc_acl',
'nc_relations',
'nc_migrations',
'nc_api_tokens',
'nc_disabled_models_for_role',
'nc_shared_views', 'nc_cron', 'nc_audit'],
}
'nc_shared_views',
'nc_cron',
'nc_audit'
]
};
export default abstract class NcMetaIO {
protected app: Noco;
protected config: NcConfig;
public abstract get knexConnection(): XKnex ;
public abstract get knexConnection(): XKnex;
constructor(app: Noco, config: NcConfig) {
this.app = app;
@ -29,104 +63,135 @@ export default abstract class NcMetaIO {
public abstract metaInit(): Promise<boolean>;
public abstract metaInsert(project_id: string,
dbAlias: string,
target: string,
data: any)
: Promise<any>;
public abstract audit(project_id: string,
dbAlias: string,
target: string,
data: any)
: Promise<any>;
public abstract metaUpdate(project_id: string,
dbAlias: string,
target: string,
data: any,
idOrCondition: string | { [key: string]: any },
xcCondition?: XcCondition)
: Promise<void>;
public abstract metaDelete(project_id: string,
dbAlias: string,
target: string,
idOrCondition?: string | { [key: string]: any },
xcCondition?: XcCondition)
: Promise<void>;
public abstract metaDeleteAll(project_id: string,
dbAlias: string)
: Promise<void>;
public abstract metaGet(project_id: string,
dbAlias: string,
target: string,
idOrCondition: string | { [key: string]: any },
fields?: string[], xcCondition?: XcCondition)
: Promise<any>;
public abstract metaList(project_id: string,
dbAlias: string,
target: string,
args?: {
condition?: { [key: string]: any },
limit?: number,
offset?: number,
xcCondition?: XcCondition,
fields?: string[]
}): Promise<any[]>;
public abstract metaPaginatedList(project_id: string,
dbAlias: string,
target: string,
args?: {
condition?: { [key: string]: any },
limit?: number,
offset?: number,
xcCondition?: XcCondition,
fields?: string[],
sort?: { field: string, desc?: boolean }
}): Promise<{
list: any[],
count: number
public abstract metaInsert(
project_id: string,
dbAlias: string,
target: string,
data: any
): Promise<any>;
public abstract audit(
project_id: string,
dbAlias: string,
target: string,
data: any
): Promise<any>;
public abstract metaUpdate(
project_id: string,
dbAlias: string,
target: string,
data: any,
idOrCondition: string | { [key: string]: any },
xcCondition?: XcCondition
): Promise<void>;
public abstract metaDelete(
project_id: string,
dbAlias: string,
target: string,
idOrCondition?: string | { [key: string]: any },
xcCondition?: XcCondition
): Promise<void>;
public abstract metaDeleteAll(
project_id: string,
dbAlias: string
): Promise<void>;
public abstract metaGet(
project_id: string,
dbAlias: string,
target: string,
idOrCondition: string | { [key: string]: any },
fields?: string[],
xcCondition?: XcCondition
): Promise<any>;
public abstract metaList(
project_id: string,
dbAlias: string,
target: string,
args?: {
condition?: { [key: string]: any };
limit?: number;
offset?: number;
xcCondition?: XcCondition;
fields?: string[];
}
): Promise<any[]>;
public abstract metaPaginatedList(
project_id: string,
dbAlias: string,
target: string,
args?: {
condition?: { [key: string]: any };
limit?: number;
offset?: number;
xcCondition?: XcCondition;
fields?: string[];
sort?: { field: string; desc?: boolean };
}
): Promise<{
list: any[];
count: number;
}>;
public abstract isMetaDataExists(project_id: string,
dbAlias: string): Promise<boolean>;
public abstract metaReset(project_id: string,
dbAlias: string, apiType?: string): Promise<void>;
public abstract projectCreate(projectName: string,
config: any,
description?: string,
meta?:boolean): Promise<any>;
public abstract projectUpdate(projectId: string,
config: any): Promise<any>;
public abstract projectAddUser(projectId: string,
userId: any, roles: string): Promise<any>;
public abstract projectRemoveUser(projectId: string,
userId: any): Promise<any>;
public abstract projectStatusUpdate(projectName: string,
status: string): Promise<any>;
public abstract isMetaDataExists(
project_id: string,
dbAlias: string
): Promise<boolean>;
public abstract metaReset(
project_id: string,
dbAlias: string,
apiType?: string
): Promise<void>;
public abstract projectCreate(
projectName: string,
config: any,
description?: string,
meta?: boolean
): Promise<any>;
public abstract projectUpdate(projectId: string, config: any): Promise<any>;
public abstract projectAddUser(
projectId: string,
userId: any,
roles: string
): Promise<any>;
public abstract projectRemoveUser(
projectId: string,
userId: any
): Promise<any>;
public abstract projectStatusUpdate(
projectName: string,
status: string
): Promise<any>;
public abstract projectList(): Promise<any[]>;
public abstract userProjectList(userId: any): Promise<any[]>;
public abstract isUserHaveAccessToProject(projectId: string,
userId: any): Promise<boolean>;
public abstract isUserHaveAccessToProject(
projectId: string,
userId: any
): Promise<boolean>;
public abstract projectGet(projectName: string, encrypt?: boolean): Promise<any>;
public abstract projectGet(
projectName: string,
encrypt?: boolean
): Promise<any>;
public abstract projectGetById(projectId: string, encrypt?: boolean): Promise<any>;
public abstract projectGetById(
projectId: string,
encrypt?: boolean
): Promise<any>;
public abstract projectDelete(title: string): Promise<any>;
public abstract projectDeleteById(id: string): Promise<any>;
@ -142,11 +207,19 @@ export default abstract class NcMetaIO {
public setConfig(config: NcConfig) {
this.config = config;
}
}
type XcConditionStr = {
[key in 'lt' | 'gt' | 'le' | 'ge' | 'like' | 'nlike' | 'eq' | 'in' | 'nin']: any;
[key in
| 'lt'
| 'gt'
| 'le'
| 'ge'
| 'like'
| 'nlike'
| 'eq'
| 'in'
| 'nin']: any;
};
interface XcCondition {
@ -155,12 +228,10 @@ interface XcCondition {
_not?: XcCondition;
[key: string]: XcConditionStr | any;
}
export {
META_TABLES
}/**
export { META_TABLES };
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>

444
packages/nocodb/src/lib/noco/meta/NcMetaIOImpl.ts

@ -1,35 +1,42 @@
import CryptoJS from 'crypto-js';
import {customAlphabet} from 'nanoid'
import { customAlphabet } from 'nanoid';
import {NcConfig} from "../../../interface/config";
import {Knex, XKnex} from "../../dataMapper";
import Noco from "../Noco";
import XcMigrationSource from "../common/XcMigrationSource";
import { NcConfig } from '../../../interface/config';
import { Knex, XKnex } from '../../dataMapper';
import Noco from '../Noco';
import XcMigrationSource from '../common/XcMigrationSource';
import NcMetaIO, {META_TABLES} from "./NcMetaIO";
import NcConnectionMgr from "../common/NcConnectionMgr";
const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 4)
import NcMetaIO, { META_TABLES } from './NcMetaIO';
import NcConnectionMgr from '../common/NcConnectionMgr';
const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 4);
export default class NcMetaIOImpl extends NcMetaIO {
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)
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) {
query.where('project_id', projectId)
countQuery.where('project_id', projectId)
query.where('project_id', projectId);
countQuery.where('project_id', projectId);
}
if (dbAlias !== null) {
query.where('db_alias', dbAlias);
countQuery.where('db_alias', dbAlias);
}
if (args?.condition) {
query.where(args.condition);
countQuery.where(args.condition);
@ -44,27 +51,25 @@ export default class NcMetaIOImpl extends NcMetaIO {
query.offset(args.offset);
}
if (args?.xcCondition) {
(query as any).condition(args.xcCondition)
(countQuery as any).condition(args.xcCondition)
(query as any)
.condition(args.xcCondition)(countQuery as any)
.condition(args.xcCondition);
}
if (args?.fields?.length) {
query.select(...args.fields)
query.select(...args.fields);
}
return {
list: await query,
count: (Object.values(await countQuery.count().first()))?.[0] as any
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) {
super(app, config);
@ -75,9 +80,13 @@ export default class NcMetaIOImpl extends NcMetaIO {
if (this.config?.meta?.db) {
this.connection = 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)
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.connection = XKnex(
this.config.envs?.[this.config.workingEnv]?.db[dbIndex] as any
);
}
NcConnectionMgr.setXcMeta(this);
}
@ -107,9 +116,8 @@ export default class NcMetaIOImpl extends NcMetaIO {
): Promise<void> {
const query = this.knexConnection(target);
if (project_id !== null) {
query.where('project_id', project_id)
query.where('project_id', project_id);
}
if (dbAlias !== null) {
query.where('db_alias', dbAlias);
@ -122,31 +130,32 @@ export default class NcMetaIOImpl extends NcMetaIO {
}
if (xcCondition) {
query.condition(xcCondition, {})
query.condition(xcCondition, {});
}
return query.del();
}
public async metaGet(project_id: string,
dbAlias: string,
target: string,
idOrCondition: string | { [p: string]: any },
fields?: string[], xcCondition?): Promise<any> {
public async metaGet(
project_id: string,
dbAlias: string,
target: string,
idOrCondition: string | { [p: string]: any },
fields?: string[],
xcCondition?
): Promise<any> {
const query = this.knexConnection(target);
if (xcCondition) {
query.condition(xcCondition)
query.condition(xcCondition);
}
if (fields?.length) {
query.select(...fields)
query.select(...fields);
}
if (project_id !== null) {
query.where('project_id', project_id)
query.where('project_id', project_id);
}
if (dbAlias !== null) {
query.where('db_alias', dbAlias);
@ -161,36 +170,47 @@ export default class NcMetaIOImpl extends NcMetaIO {
query.where(idOrCondition);
}
// console.log(query.toQuery())
return query.first();
}
public async metaInsert(project_id: string, dbAlias: string, target: string, data: any): Promise<any> {
public async metaInsert(
project_id: string,
dbAlias: string,
target: string,
data: any
): Promise<any> {
return this.knexConnection(target).insert({
'db_alias': dbAlias,
db_alias: dbAlias,
project_id,
created_at: this.knexConnection?.fn?.now(),
updated_at: this.knexConnection?.fn?.now(), ...data
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[]
}): Promise<any[]> {
const query = this.knexConnection(target)
public async metaList(
project_id: string,
dbAlias: string,
target: string,
args?: {
condition?: { [p: string]: any };
limit?: number;
offset?: number;
xcCondition?;
fields?: string[];
}
): Promise<any[]> {
const query = this.knexConnection(target);
if (project_id !== null) {
query.where('project_id', project_id)
query.where('project_id', project_id);
}
if (dbAlias !== null) {
query.where('db_alias', dbAlias);
}
if (args?.condition) {
query.where(args.condition);
}
@ -201,20 +221,27 @@ export default class NcMetaIOImpl extends NcMetaIO {
query.offset(args.offset);
}
if (args?.xcCondition) {
(query as any).condition(args.xcCondition)
(query as any).condition(args.xcCondition);
}
if (args?.fields?.length) {
query.select(...args.fields)
query.select(...args.fields);
}
return query;
}
public async metaUpdate(project_id: string, dbAlias: string, target: string, data: any, idOrCondition?: string | { [p: string]: any }, xcCondition?): Promise<any> {
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) {
query.where('project_id', project_id)
query.where('project_id', project_id);
}
if (dbAlias !== null) {
query.where('db_alias', dbAlias);
@ -222,23 +249,25 @@ export default class NcMetaIOImpl extends NcMetaIO {
delete data.created_at;
query.update({...data, updated_at: this.knexConnection?.fn?.now()});
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)
query.condition(xcCondition);
}
// console.log(query.toQuery())
return query;
}
public async metaDeleteAll(_project_id: string, _dbAlias: string): Promise<void> {
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').;
@ -246,10 +275,13 @@ export default class NcMetaIOImpl extends NcMetaIO {
// await this.knexConnection.schema.dropTableIfExists('nc_acl').;
}
public async isMetaDataExists(project_id: string, dbAlias: string): Promise<boolean> {
public async isMetaDataExists(
project_id: string,
dbAlias: string
): Promise<boolean> {
const query = this.knexConnection('nc_models');
if (project_id !== null) {
query.where('project_id', project_id)
query.where('project_id', project_id);
}
if (dbAlias !== null) {
query.where('db_alias', dbAlias);
@ -279,31 +311,43 @@ export default class NcMetaIOImpl extends NcMetaIO {
}
}
async metaReset(project_id: string, dbAlias: string, apiType?: string): Promise<void> {
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> {
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}__`
config.prefix = `nc_${ranId}__`;
// if(config.envs._noco?.db?.[0]?.meta?.tn){
// config.envs._noco.db[0].meta.tn += `_${prefix}`
// }
@ -313,124 +357,180 @@ export default class NcMetaIOImpl extends NcMetaIO {
id,
title: projectName,
description,
config: CryptoJS.AES.encrypt(JSON.stringify(config), this.config?.auth?.jwt?.secret).toString()
config: CryptoJS.AES.encrypt(
JSON.stringify(config),
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(),
updated_at: this.knexConnection?.fn?.now()
});
return project;
} catch (e) {
console.log(e)
console.log(e);
}
}
public async projectUpdate(projectId: string,
config: any): Promise<any> {
public async projectUpdate(projectId: string, config: any): Promise<any> {
try {
const project = {
config: CryptoJS.AES.encrypt(JSON.stringify(config, null, 2), this.config?.auth?.jwt?.secret).toString()
config: CryptoJS.AES.encrypt(
JSON.stringify(config, null, 2),
this.config?.auth?.jwt?.secret
).toString()
};
// todo: check project name used or not
await this.knexConnection('nc_projects').update(project).where({
id: projectId
});
await this.knexConnection('nc_projects')
.update(project)
.where({
id: projectId
});
} catch (e) {
console.log(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, this.config?.auth?.jwt?.secret).toString(CryptoJS.enc.Utf8)
p.config = CryptoJS.AES.decrypt(
p.config,
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 `xc_users`.`email`
// FROM `xc_users`
// INNER JOIN `nc_projects_users`
// ON `nc_projects_users`.`user_id` =
// `xc_users`.`id` and `nc_projects_users`.project_id=`nc_projects`.id where `nc_projects_users`.`roles` like '%owner%' limit 1)
.select(this.knexConnection('xc_users')
.select('xc_users.email')
.innerJoin('nc_projects_users', 'nc_projects_users.user_id', '=', 'xc_users.id')
.where('nc_projects_users.roles', 'like', '%owner%')
.first()
.as('owner')
)
).map(p => {
p.allowed = p.user_id === userId;
p.config = CryptoJS.AES.decrypt(p.config, 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();
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 `xc_users`.`email`
// FROM `xc_users`
// INNER JOIN `nc_projects_users`
// ON `nc_projects_users`.`user_id` =
// `xc_users`.`id` and `nc_projects_users`.project_id=`nc_projects`.id where `nc_projects_users`.`roles` like '%owner%' limit 1)
.select(
this.knexConnection('xc_users')
.select('xc_users.email')
.innerJoin(
'nc_projects_users',
'nc_projects_users.user_id',
'=',
'xc_users.id'
)
.where('nc_projects_users.roles', 'like', '%owner%')
.first()
.as('owner')
)
).map(p => {
p.allowed = p.user_id === userId;
p.config = CryptoJS.AES.decrypt(
p.config,
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, this.config?.auth?.jwt?.secret).toString(CryptoJS.enc.Utf8)
project.config = CryptoJS.AES.decrypt(
project.config,
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();
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, this.config?.auth?.jwt?.secret).toString(CryptoJS.enc.Utf8)
project.config = CryptoJS.AES.decrypt(
project.config,
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();
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(projectName: string, status: string): Promise<any> {
return this.knexConnection('nc_projects').update({
status
}).where({
title: projectName
});
return this.knexConnection('nc_projects')
.where({
id
})
.delete();
}
public async projectStatusUpdate(
projectName: string,
status: string
): Promise<any> {
return this.knexConnection('nc_projects')
.update({
status
})
.where({
title: projectName
});
}
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 {}
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,
@ -440,23 +540,30 @@ export default class NcMetaIOImpl extends NcMetaIO {
}
public projectRemoveUser(projectId: string, userId: any): Promise<any> {
return this.knexConnection('nc_projects_users').where({
user_id: userId,
project_id: projectId
}).delete();
return this.knexConnection('nc_projects_users')
.where({
user_id: userId,
project_id: projectId
})
.delete();
}
get isRest(): boolean {
return this.config?.envs?.[this.config.workingEnv]?.db?.some(db => db?.meta?.api?.type === 'rest');
return this.config?.envs?.[this.config.workingEnv]?.db?.some(
db => db?.meta?.api?.type === 'rest'
);
}
get isGql(): boolean {
return this.config?.envs?.[this.config.workingEnv]?.db?.some(db => db?.meta?.api?.type === 'graphql');
return this.config?.envs?.[this.config.workingEnv]?.db?.some(
db => db?.meta?.api?.type === 'graphql'
);
}
get isGrpc(): boolean {
return this.config?.envs?.[this.config.workingEnv]?.db?.some(db => db?.meta?.api?.type === 'grpc');
return this.config?.envs?.[this.config.workingEnv]?.db?.some(
db => db?.meta?.api?.type === 'grpc'
);
}
public get knex(): any {
@ -464,17 +571,22 @@ export default class NcMetaIOImpl extends NcMetaIO {
}
private getNanoId() {
return nanoid()
return nanoid();
}
public async audit(project_id: string, dbAlias: string, target: string, data: any): Promise<any> {
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)
return this.metaInsert(project_id, dbAlias, target, data);
}
}/**
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>

12
packages/nocodb/src/lib/noco/meta/NcMetaIOImplEE.ts

@ -1,10 +1,16 @@
import NcMetaIOImpl from "./NcMetaIOImpl";
import NcMetaIOImpl from './NcMetaIOImpl';
export default class NcMetaIOImplEE extends NcMetaIOImpl {
public async audit(project_id: string, dbAlias: string, target: string, data: any): Promise<any> {
public async audit(
project_id: string,
dbAlias: string,
target: string,
data: any
): Promise<any> {
return this.metaInsert(project_id, dbAlias, target, data);
}
}/**
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>

161
packages/nocodb/src/lib/noco/migrations/nc_001_init.ts

@ -1,9 +1,9 @@
// import s3 from "../plugins/s3";
// import gcs from "../plugins/gcs";
// import smtp from "../plugins/smtp";
import cache from "../plugins/cache";
import googleAuth from "../plugins/googleAuth";
import ses from "../plugins/ses";
import cache from '../plugins/cache';
import googleAuth from '../plugins/googleAuth';
import ses from '../plugins/ses';
// import azure from "../plugins/azure";
// import brand from "../plugins/brand";
// import discord from "../plugins/discord";
@ -14,7 +14,7 @@ import ses from "../plugins/ses";
// import spaces from "../plugins/spaces";
// import githubAuth from "../instant/common/plugins/githubAuth";
const up = async (knex) => {
const up = async knex => {
await knex.schema.createTable('nc_projects', table => {
table.string('id', 128).primary();
table.string('title');
@ -23,8 +23,7 @@ const up = async (knex) => {
table.text('config');
table.text('meta');
table.timestamps();
})
});
await knex.schema.createTable('nc_roles', table => {
table.increments();
@ -41,7 +40,8 @@ const up = async (knex) => {
db_alias: '',
project_id: '',
title: 'owner',
description: 'Can add/remove creators. And full edit database structures & fields.',
description:
'Can add/remove creators. And full edit database structures & fields.',
type: 'SYSTEM'
},
{
@ -55,7 +55,8 @@ const up = async (knex) => {
db_alias: '',
project_id: '',
title: 'editor',
description: 'Can edit records but cannot change structure of database/fields',
description:
'Can edit records but cannot change structure of database/fields',
type: 'SYSTEM'
},
{
@ -71,7 +72,7 @@ const up = async (knex) => {
title: 'viewer',
description: 'Can view the records but cannot edit anything',
type: 'SYSTEM'
},
}
// {
// db_alias: '',
// project_id: '',
@ -79,12 +80,12 @@ const up = async (knex) => {
// description: 'API access for an unauthorized user',
// type: 'SYSTEM'
// },
])
]);
await knex.schema.createTable('nc_hooks', table => {
table.increments();
table.string('project_id');
table.string('db_alias').defaultTo('db')
table.string('db_alias').defaultTo('db');
table.string('title');
table.string('description', 255);
table.string('env').defaultTo('all');
@ -107,28 +108,25 @@ const up = async (knex) => {
table.integer('timeout').defaultTo(60000);
table.boolean('active').defaultTo(true);
table.timestamps();
})
});
await knex('nc_hooks').insert({
// url: 'http://localhost:4000/auth/hook',
type: 'AUTH_MIDDLEWARE'
})
});
await knex.schema.createTable('nc_store', table => {
table.increments();
table.string('project_id');
table.string('db_alias').defaultTo('db')
table.string('db_alias').defaultTo('db');
table.string('key').index();
table.text('value', 'text');
table.string('type');
table.string('env');
table.string('tag');
table.timestamps();
})
});
await knex('nc_store').insert({
key: 'NC_DEBUG',
@ -139,22 +137,21 @@ const up = async (knex) => {
'nc:api:gql': false,
'nc:api:grpc': false,
'nc:migrator': false,
'nc:datamapper': false,
'nc:datamapper': false
}),
db_alias: ''
})
});
await knex('nc_store').insert({
key: 'NC_PROJECT_COUNT',
value: '0',
db_alias: ''
})
});
await knex.schema.createTable('nc_cron', table => {
table.increments();
table.string('project_id');
table.string('db_alias').defaultTo('db')
table.string('db_alias').defaultTo('db');
table.string('title');
table.string('description', 255);
table.string('env');
@ -170,30 +167,28 @@ const up = async (knex) => {
table.integer('timeout').defaultTo(60000);
table.timestamps();
})
});
await knex.schema.createTable('nc_acl', table => {
table.increments();
table.string('project_id');
table.string('db_alias').defaultTo('db')
table.string('db_alias').defaultTo('db');
table.string('tn');
table.text('acl');
table.string('type').defaultTo('table');
table.timestamps();
})
});
await knex.schema.createTable('nc_models', table => {
table.increments();
table.string('project_id');
table.string('db_alias').defaultTo('db')
table.string('db_alias').defaultTo('db');
table.string('title');
table.string('alias');
table.string('type').defaultTo('table');
table.text('meta', 'mediumtext');
table.text('schema', 'text');
table.text('schema_previous', 'text')
table.text('schema_previous', 'text');
table.text('services', 'mediumtext');
table.text('messages', 'text');
table.boolean('enabled').defaultTo(true);
@ -207,14 +202,13 @@ const up = async (knex) => {
table.boolean('pinned');
table.timestamps();
table.index(['db_alias', 'title'])
})
table.index(['db_alias', 'title']);
});
await knex.schema.createTable('nc_relations', table => {
table.increments();
table.string('project_id');
table.string('db_alias')
table.string('db_alias');
table.string('tn');
table.string('rtn');
table.string('_tn');
@ -230,13 +224,13 @@ const up = async (knex) => {
table.string('dr');
table.timestamps();
table.index(['db_alias', 'tn'])
})
table.index(['db_alias', 'tn']);
});
await knex.schema.createTable('nc_routes', table => {
table.increments();
table.string('project_id');
table.string('db_alias').defaultTo('db')
table.string('db_alias').defaultTo('db');
table.string('title');
table.string('tn');
table.string('tnp');
@ -252,13 +246,13 @@ const up = async (knex) => {
table.boolean('is_custom');
// table.text('placeholder', 'longtext');
table.timestamps();
table.index(['db_alias', 'title', 'tn'])
})
table.index(['db_alias', 'title', 'tn']);
});
await knex.schema.createTable('nc_resolvers', (table) => {
await knex.schema.createTable('nc_resolvers', table => {
table.increments();
table.string('project_id');
table.string('db_alias').defaultTo('db')
table.string('db_alias').defaultTo('db');
table.string('title');
table.text('resolver', 'text');
table.string('type');
@ -267,12 +261,12 @@ const up = async (knex) => {
table.integer('handler_type').defaultTo(1);
// table.text('placeholder', 'text');
table.timestamps();
})
});
await knex.schema.createTable('nc_loaders', table => {
table.increments();
table.string('project_id');
table.string('db_alias').defaultTo('db')
table.string('db_alias').defaultTo('db');
table.string('title');
table.string('parent');
table.string('child');
@ -280,12 +274,12 @@ const up = async (knex) => {
table.string('resolver');
table.text('functions');
table.timestamps();
})
});
await knex.schema.createTable('nc_rpc', (table) => {
await knex.schema.createTable('nc_rpc', table => {
table.increments();
table.string('project_id');
table.string('db_alias').defaultTo('db')
table.string('db_alias').defaultTo('db');
table.string('title');
table.string('tn');
table.text('service', 'text');
@ -301,17 +295,20 @@ const up = async (knex) => {
table.integer('handler_type').defaultTo(1);
// table.text('placeholder', 'text');
table.timestamps();
})
await knex.schema.createTable('nc_projects_users', (table) => {
table.string('project_id').index() // .references('id').inTable('nc_projects')
});
await knex.schema.createTable('nc_projects_users', table => {
table.string('project_id').index(); // .references('id').inTable('nc_projects')
// todo: foreign key
table.integer('user_id').unsigned().index()//.references('id').inTable('xc_users')
table
.integer('user_id')
.unsigned()
.index(); //.references('id').inTable('xc_users')
table.text('roles');
// table.text('placeholder', 'text');
table.timestamps();
})
});
await knex.schema.createTable('nc_shared_views', (table) => {
await knex.schema.createTable('nc_shared_views', table => {
table.increments();
table.string('project_id');
table.string('db_alias');
@ -323,9 +320,9 @@ const up = async (knex) => {
table.boolean('allow_copy');
table.string('password');
table.timestamps();
})
});
await knex.schema.createTable('nc_disabled_models_for_role', (table) => {
await knex.schema.createTable('nc_disabled_models_for_role', table => {
table.increments();
table.string('project_id');
table.string('db_alias', 45);
@ -340,10 +337,13 @@ const up = async (knex) => {
table.string('rcn');
table.string('relation_type');
table.timestamps();
table.index(['project_id', 'db_alias', 'title', 'type', 'role'], 'xc_disabled124_idx');
})
table.index(
['project_id', 'db_alias', 'title', 'type', 'role'],
'xc_disabled124_idx'
);
});
await knex.schema.createTable('nc_plugins', (table) => {
await knex.schema.createTable('nc_plugins', table => {
table.increments();
table.string('project_id');
table.string('db_alias');
@ -365,18 +365,17 @@ const up = async (knex) => {
table.string('creator_website');
table.string('price');
table.timestamps();
})
});
await knex('nc_plugins').insert([
googleAuth,
ses,
cache,
cache
// ee,
// brand,
])
]);
await knex.schema.createTable('nc_audit', (table) => {
await knex.schema.createTable('nc_audit', table => {
table.increments();
table.string('user');
table.string('ip');
@ -390,31 +389,32 @@ const up = async (knex) => {
table.string('status');
table.text('description');
table.text('details');
table.index(['db_alias', 'project_id', 'model_name', 'model_id'], '`nc_audit_index`')
table.index(
['db_alias', 'project_id', 'model_name', 'model_id'],
'`nc_audit_index`'
);
table.timestamps();
})
});
await knex.schema.createTable('nc_migrations', (table) => {
await knex.schema.createTable('nc_migrations', table => {
table.increments();
table.string('project_id');
table.string('db_alias');
table.text('up');
table.text('down');
table.string("title").notNullable();
table.string("title_down").nullable();
table.string("description").nullable();
table.integer("batch").nullable();
table.string("checksum").nullable();
table.integer("status").nullable();
table.string('title').notNullable();
table.string('title_down').nullable();
table.string('description').nullable();
table.integer('batch').nullable();
table.string('checksum').nullable();
table.integer('status').nullable();
table.timestamps();
})
});
await knex.schema.createTable('nc_api_tokens', (table) => {
await knex.schema.createTable('nc_api_tokens', table => {
table.increments();
table.string('project_id');
table.string('db_alias');
@ -424,12 +424,10 @@ const up = async (knex) => {
table.string('expiry');
table.boolean('enabled').defaultTo(true);
table.timestamps();
})
});
};
const down = async (knex) => {
const down = async knex => {
await knex.schema.dropTable('nc_plugins');
await knex.schema.dropTable('nc_disabled_models_for_role');
await knex.schema.dropTable('nc_shared_views');
@ -451,7 +449,4 @@ const down = async (knex) => {
await knex.schema.dropTable('nc_api_tokens');
};
export {
up, down
}
export { up, down };

58
packages/nocodb/src/lib/noco/migrations/nc_002_add_m2m.ts

@ -1,43 +1,39 @@
import Knex from "knex";
import Knex from 'knex';
const up = async (knex: Knex) => {
await knex.schema.alterTable('nc_models', table => {
table.integer('mm');
table.text('m_to_m_meta');
})
});
};
const down = async (knex) => {
const down = async knex => {
await knex.schema.alterTable('nc_models', table => {
table.dropColumns('mm', 'm_to_m_meta');
})
});
};
export { up, down };
export {
up, down
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

14
packages/nocodb/src/lib/noco/migrations/nc_003_add_fkn_column.ts

@ -1,22 +1,18 @@
import Knex from "knex";
import Knex from 'knex';
const up = async (knex: Knex) => {
await knex.schema.alterTable('nc_relations', table => {
table.string('fkn');
})
});
};
const down = async (knex) => {
const down = async knex => {
await knex.schema.alterTable('nc_relations', table => {
table.dropColumns('fkn');
})
});
};
export {
up, down
}
export { up, down };
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd

43
packages/nocodb/src/lib/noco/nc.try.ts

@ -1,13 +1,11 @@
import cors from 'cors';
import express from 'express';
import NcConfigFactory from "../utils/NcConfigFactory";
import Noco from "./Noco";
import NcConfigFactory from '../utils/NcConfigFactory';
import Noco from './Noco';
export default async function (dbUrl): Promise<void> {
export default async function(dbUrl): Promise<void> {
const server = express();
server.use(cors());
@ -15,22 +13,31 @@ export default async function (dbUrl): Promise<void> {
process.env[`NC_TRY`] = 'true';
(async () => {
const app = new Noco();
server.use(await app.init({
async afterMetaMigrationInit(): Promise<void> {
const config = NcConfigFactory.makeProjectConfigFromUrl(dbUrl);
await app.ncMeta.projectCreate('Dvdrental (Sample SQLite Database)', config, '');
await app.ncMeta.projectStatusUpdate('Dvdrental (Sample SQLite Database)', 'started');
}
}));
server.use(
await app.init({
async afterMetaMigrationInit(): Promise<void> {
const config = NcConfigFactory.makeProjectConfigFromUrl(dbUrl);
await app.ncMeta.projectCreate(
'Dvdrental (Sample SQLite Database)',
config,
''
);
await app.ncMeta.projectStatusUpdate(
'Dvdrental (Sample SQLite Database)',
'started'
);
}
})
);
server.listen(process.env.PORT || 8080, () => {
console.log(`App started successfully.\nVisit -> http://localhost:${process.env.PORT || 8080}/xc`);
})
})().catch(e => console.log(e))
console.log(
`App started successfully.\nVisit -> http://localhost:${process.env
.PORT || 8080}/xc`
);
});
})().catch(e => console.log(e));
}
/**

111
packages/nocodb/src/lib/noco/plugins/NcPluginMgr.ts

@ -4,31 +4,32 @@ import {
IWebhookNotificationAdapter,
XcEmailPlugin,
XcPlugin,
XcStoragePlugin, XcWebhookNotificationPlugin
} from "nc-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 Noco from "../Noco";
import NcMetaIO from "../meta/NcMetaIO";
import Local from "./adapters/storage/Local";
XcStoragePlugin,
XcWebhookNotificationPlugin
} from 'nc-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 Noco from '../Noco';
import NcMetaIO from '../meta/NcMetaIO';
import Local from './adapters/storage/Local';
const defaultPlugins = [
SlackPluginConfig,
@ -48,16 +49,15 @@ const defaultPlugins = [
UpcloudPluginConfig,
SMTPPluginConfig,
MailerSendConfig,
ScalewayPluginConfig,
]
ScalewayPluginConfig
];
class NcPluginMgr {
private ncMeta: NcMetaIO;
private app: Noco;
/* active plugins */
private activePlugins: Array<XcPlugin | XcStoragePlugin | XcEmailPlugin>
private activePlugins: Array<XcPlugin | XcStoragePlugin | XcEmailPlugin>;
constructor(app: Noco, ncMeta: NcMetaIO) {
this.app = app;
@ -66,16 +66,13 @@ class NcPluginMgr {
}
public async init(): Promise<void> {
/* Populate rows into nc_plugins table if not present */
for (const plugin of defaultPlugins) {
const pluginConfig = (await this.ncMeta.metaGet(null, null, 'nc_plugins', {
const pluginConfig = await this.ncMeta.metaGet(null, null, 'nc_plugins', {
title: plugin.title
}));
});
if (!pluginConfig) {
await this.ncMeta.metaInsert(null, null, 'nc_plugins', {
title: plugin.title,
version: plugin.version,
@ -85,12 +82,10 @@ class NcPluginMgr {
category: plugin.category,
input_schema: JSON.stringify(plugin.inputs)
});
}
/* init only the active plugins */
if (pluginConfig?.active) {
const tempPlugin = new plugin.builder(this.app, plugin);
this.activePlugins.push(tempPlugin);
@ -102,52 +97,62 @@ class NcPluginMgr {
try {
await tempPlugin.init(pluginConfig?.input);
} catch (e) {
console.log(`Plugin(${plugin?.title}) initialization failed : ${e.message}`)
console.log(
`Plugin(${plugin?.title}) initialization failed : ${e.message}`
);
}
}
}
}
public async reInit(): Promise<void> {
this.activePlugins = [];
await this.init();
}
public get storageAdapter(): IStorageAdapter {
return (this.activePlugins?.find(plugin => plugin instanceof XcStoragePlugin) as XcStoragePlugin)?.getAdapter() || new Local();
return (
(this.activePlugins?.find(
plugin => plugin instanceof XcStoragePlugin
) as XcStoragePlugin)?.getAdapter() || new Local()
);
}
public get emailAdapter(): IEmailAdapter {
return (this.activePlugins?.find(plugin => plugin instanceof XcEmailPlugin) as XcEmailPlugin)?.getAdapter();
return (this.activePlugins?.find(
plugin => plugin instanceof XcEmailPlugin
) as XcEmailPlugin)?.getAdapter();
}
public get webhookNotificationAdapters(): { [key: string]: IWebhookNotificationAdapter } {
public get webhookNotificationAdapters(): {
[key: string]: IWebhookNotificationAdapter;
} {
return this.activePlugins?.reduce((obj, plugin) => {
if (plugin instanceof XcWebhookNotificationPlugin) {
obj[plugin?.config?.title] = (plugin as XcWebhookNotificationPlugin)?.getAdapter()
obj[
plugin?.config?.title
] = (plugin as XcWebhookNotificationPlugin)?.getAdapter();
}
return obj;
}, {});
}
public async test(args: any): Promise<boolean> {
switch (args.category) {
case 'Storage': {
const plugin = defaultPlugins.find(pluginConfig => pluginConfig?.title === args.title);
const tempPlugin = new plugin.builder(this.app, plugin);
await tempPlugin.init(args?.input);
return tempPlugin?.getAdapter()?.test?.();
}
case 'Storage':
{
const plugin = defaultPlugins.find(
pluginConfig => pluginConfig?.title === args.title
);
const tempPlugin = new plugin.builder(this.app, plugin);
await tempPlugin.init(args?.input);
return tempPlugin?.getAdapter()?.test?.();
}
break;
default:
throw new Error('Not implemented');
}
}
}
export default NcPluginMgr;
@ -173,4 +178,4 @@ export default NcPluginMgr;
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
*/

13
packages/nocodb/src/lib/noco/plugins/adapters/cache/XcCache.ts vendored

@ -1,9 +1,7 @@
import LRU from 'lru-cache';
export default class XcCache {
public static init(config: any, overwrite = false) {
if (overwrite && this.instance) {
this.instance.reset();
this.instance = null;
@ -11,18 +9,18 @@ export default class XcCache {
if (!this.instance) {
const options = {
max: 500, maxAge: 1000 * 60 * 60
}
max: 500,
maxAge: 1000 * 60 * 60
};
if (config) {
const input = JSON.parse(config.input);
Object.assign(options, input);
}
this.instance = new LRU(options)
this.instance = new LRU(options);
}
}
public static get(key): any {
return this.instance?.get(key);
}
@ -36,5 +34,4 @@ export default class XcCache {
}
private static instance: LRU<any, any>;
}
}

21
packages/nocodb/src/lib/noco/plugins/adapters/discord/Discord.ts

@ -1,19 +1,20 @@
import axios from 'axios';
export default class Discord {
public static async sendMessage(content: string, webhooks: Array<{
webhook_url: string
}>): Promise<any> {
for (const {webhook_url} of webhooks) {
public static async sendMessage(
content: string,
webhooks: Array<{
webhook_url: string;
}>
): Promise<any> {
for (const { webhook_url } of webhooks) {
try {
await axios.post(webhook_url, {
content
})
}catch(e){
console.log(e)
});
} catch (e) {
console.log(e);
}
}
}
}
}

12
packages/nocodb/src/lib/noco/plugins/adapters/email/EmailFactory.ts

@ -1,10 +1,9 @@
import IEmailAdapter from "../../../../../interface/IEmailAdapter";
import IEmailAdapter from '../../../../../interface/IEmailAdapter';
import SES from "./SES";
import SMTP from "./SMTP";
import SES from './SES';
import SMTP from './SMTP';
export default class EmailFactory {
private static instance: IEmailAdapter;
// tslint:disable-next-line:typedef
@ -34,12 +33,8 @@ export default class EmailFactory {
break;
}
}
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
@ -62,4 +57,3 @@ export default class EmailFactory {
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

18
packages/nocodb/src/lib/noco/plugins/adapters/email/SES.ts

@ -1,30 +1,22 @@
// @ts-ignore
import IEmailAdapter, {XcEmail} from "../../../../../interface/IEmailAdapter";
import IEmailAdapter, { XcEmail } from '../../../../../interface/IEmailAdapter';
export default // @ts-ignore
class SES implements IEmailAdapter {
// @ts-ignore
private input: any;
constructor(input:any) {
this.input= input;
}
public async init(): Promise<any> {
constructor(input: any) {
this.input = input;
}
public async mailSend(_mail:XcEmail): Promise<any> {
public async init(): Promise<any> {}
}
public async mailSend(_mail: XcEmail): Promise<any> {}
test(_email): Promise<boolean> {
return Promise.resolve(false);
}
}
/**

27
packages/nocodb/src/lib/noco/plugins/adapters/email/SMTP.ts

@ -1,12 +1,11 @@
// @ts-ignore
import nodemailer from 'nodemailer';
import Mail from "nodemailer/lib/mailer";
import Mail from 'nodemailer/lib/mailer';
import IEmailAdapter, {XcEmail} from "../../../../../interface/IEmailAdapter";
import IEmailAdapter, { XcEmail } from '../../../../../interface/IEmailAdapter';
export default // @ts-ignore
class SMTP implements IEmailAdapter {
private transporter: Mail;
private input: any;
@ -18,22 +17,22 @@ class SMTP implements IEmailAdapter {
const config = {
// from: this.input.from,
// options: {
"host": this.input?.host,
"port": parseInt(this.input?.port, 10),
"secure": this.input?.secure === 'true',
"ignoreTLS": this.input?.ignoreTLS === 'true',
"auth": {
"user": this.input?.username,
"pass": this.input?.password
host: this.input?.host,
port: parseInt(this.input?.port, 10),
secure: this.input?.secure === 'true',
ignoreTLS: this.input?.ignoreTLS === 'true',
auth: {
user: this.input?.username,
pass: this.input?.password
}
// }
}
};
this.transporter = nodemailer.createTransport(config);
}
public async mailSend(mail: XcEmail): Promise<any> {
if (this.transporter) {
await this.transporter.sendMail({...mail, from: this.input.from})
await this.transporter.sendMail({ ...mail, from: this.input.from });
}
}
@ -41,9 +40,9 @@ class SMTP implements IEmailAdapter {
try {
this.mailSend({
to: email,
subject: "Test email",
subject: 'Test email',
html: 'Test email'
} as any)
} as any);
return true;
} catch (e) {
throw e;

21
packages/nocodb/src/lib/noco/plugins/adapters/mattermost/Mattermost.ts

@ -1,19 +1,20 @@
import axios from 'axios';
export default class Mattermost {
public static async sendMessage(text: string, webhooks: Array<{
webhook_url: string
}>): Promise<any> {
for (const {webhook_url} of webhooks) {
public static async sendMessage(
text: string,
webhooks: Array<{
webhook_url: string;
}>
): Promise<any> {
for (const { webhook_url } of webhooks) {
try {
await axios.post(webhook_url, {
text
})
}catch(e){
console.log(e)
});
} catch (e) {
console.log(e);
}
}
}
}
}

19
packages/nocodb/src/lib/noco/plugins/adapters/slack/Slack.ts

@ -1,19 +1,20 @@
import axios from 'axios';
export default class Slack {
public static async sendMessage(text: string, webhooks: Array<{
webhook_url: string
}>): Promise<any> {
for (const {webhook_url} of webhooks) {
public static async sendMessage(
text: string,
webhooks: Array<{
webhook_url: string;
}>
): Promise<any> {
for (const { webhook_url } of webhooks) {
try {
await axios.post(webhook_url, {
text
})
});
} catch (e) {
console.log(e)
console.log(e);
}
}
}
}
}

24
packages/nocodb/src/lib/noco/plugins/adapters/storage/Local.ts

@ -1,15 +1,15 @@
import fs from "fs";
import path from "path";
import fs from 'fs';
import path from 'path';
import mkdirp from "mkdirp";
import mkdirp from 'mkdirp';
import IStorageAdapter, {XcFile} from "../../../../../interface/IStorageAdapter";
import NcConfigFactory from "../../../../utils/NcConfigFactory";
import IStorageAdapter, {
XcFile
} from '../../../../../interface/IStorageAdapter';
import NcConfigFactory from '../../../../utils/NcConfigFactory';
export default class Local implements IStorageAdapter {
constructor() {
}
constructor() {}
public async fileCreate(key: string, file: XcFile): Promise<any> {
const destPath = path.join(NcConfigFactory.getToolDir(), ...key.split('/'));
@ -28,7 +28,9 @@ export default class Local implements IStorageAdapter {
public async fileRead(filePath: string): Promise<any> {
try {
const fileData = await fs.promises.readFile(path.join(NcConfigFactory.getToolDir(), ...filePath.split('/')));
const fileData = await fs.promises.readFile(
path.join(NcConfigFactory.getToolDir(), ...filePath.split('/'))
);
return fileData;
} catch (e) {
throw e;
@ -42,8 +44,8 @@ export default class Local implements IStorageAdapter {
test(): Promise<boolean> {
return Promise.resolve(false);
}
}/**
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>

20
packages/nocodb/src/lib/noco/plugins/adapters/twilio/Twilio.ts

@ -1,7 +1,6 @@
import twilio from "twilio";
import twilio from 'twilio';
export default class Twilio {
private static instance: Twilio;
private input: any;
@ -17,7 +16,6 @@ export default class Twilio {
// tslint:disable-next-line:typedef
public static create(config: any, overwrite = false): Twilio {
if (this.instance && !overwrite) {
return this.instance;
}
@ -32,16 +30,14 @@ export default class Twilio {
public async sendMessage(content: string, numbers: string[]): Promise<any> {
for (const num of numbers) {
try {
await this.client.messages
.create({
body: content,
from: this.input.from,
to: num
})
await this.client.messages.create({
body: content,
from: this.input.from,
to: num
});
} catch (e) {
console.log(e)
console.log(e);
}
}
}
}
}

84
packages/nocodb/src/lib/noco/plugins/azure.ts

@ -1,52 +1,58 @@
import {XcActionType, XcForm, XcType} from "nc-common";
import { XcActionType, XcForm, XcType } from 'nc-common';
const input: XcForm = {
title: 'Configure Azure Storage',
items: [{
key: 'account',
label: 'Azure Account Name',
placeholder: 'Azure Account Name',
type: XcType.SingleLineText,
required: true
}, {
key: 'container',
label: 'Storage Container',
placeholder: 'Storage Container',
type: XcType.SingleLineText,
required: true
}, {
key: 'access_key',
label: 'Access Key',
placeholder: 'Access Key',
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 Azure',
msgOnUninstall:'',
items: [
{
key: 'account',
label: 'Azure Account Name',
placeholder: 'Azure Account Name',
type: XcType.SingleLineText,
required: true
},
{
key: 'container',
label: 'Storage Container',
placeholder: 'Storage Container',
type: XcType.SingleLineText,
required: true
},
{
key: 'access_key',
label: 'Access Key',
placeholder: 'Access Key',
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 Azure',
msgOnUninstall: ''
};
export default {
title: 'Azure',
version: '0.0.1',
logo: 'plugins/azure.png',
description: 'Azure Blob storage is Microsoft\'s object storage solution for the cloud. Blob storage is optimized for storing massive amounts of unstructured data, such as text or binary data.',
description:
"Azure Blob storage is Microsoft's object storage solution for the cloud. Blob storage is optimized for storing massive amounts of unstructured data, such as text or binary data.",
price: 'Free',
tags: 'Storage',
category: 'Storage',
input_schema: JSON.stringify(input)
};
};

35
packages/nocodb/src/lib/noco/plugins/brand.ts

@ -1,4 +1,4 @@
import {XcActionType, XcForm, XcType} from "nc-common";
import { XcActionType, XcForm, XcType } from 'nc-common';
const input: XcForm = {
title: 'Branding',
@ -9,13 +9,15 @@ const input: XcForm = {
placeholder: 'Title',
type: XcType.SingleLineText,
required: true
}, {
},
{
key: 'logo',
label: 'Logo',
placeholder: 'Logo',
type: XcType.Attachment,
required: true
}, {
},
{
key: 'favicon',
label: 'Favicon',
placeholder: 'Favicon',
@ -49,18 +51,21 @@ const input: XcForm = {
placeholder: 'Youtube',
type: XcType.URL,
required: false
},],
actions: [{
label: 'Save',
key: 'save',
actionType: XcActionType.SUBMIT,
type: XcType.Button
},],
msgOnInstall: 'Successfully installed and hard refresh the browser to reflect the changes',
msgOnUninstall: '',
}
],
actions: [
{
label: 'Save',
key: 'save',
actionType: XcActionType.SUBMIT,
type: XcType.Button
}
],
msgOnInstall:
'Successfully installed and hard refresh the browser to reflect the changes',
msgOnUninstall: ''
};
export default {
title: 'Branding',
version: '0.0.1',
@ -69,5 +74,5 @@ export default {
price: 'Free',
tags: 'Brand',
category: 'Brand',
input_schema: JSON.stringify(input),
};
input_schema: JSON.stringify(input)
};

69
packages/nocodb/src/lib/noco/plugins/cache.ts

@ -1,39 +1,43 @@
import {XcActionType, XcForm, XcType} from "nc-common";
import { XcActionType, XcForm, XcType } from 'nc-common';
const input: XcForm = {
title: 'Configure Metadata LRU Cache',
items: [{
key: 'max',
label: 'Maximum Size',
placeholder: 'Maximum Size',
type: XcType.SingleLineText,
required: true
}, {
key: 'maxAge',
label: 'Maximum Age(in ms)',
placeholder: 'Maximum Age(in ms)',
type: XcType.SingleLineText,
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
},],
items: [
{
key: 'max',
label: 'Maximum Size',
placeholder: 'Maximum Size',
type: XcType.SingleLineText,
required: true
},
{
key: 'maxAge',
label: 'Maximum Age(in ms)',
placeholder: 'Maximum Age(in ms)',
type: XcType.SingleLineText,
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 updated LRU cache options.',
msgOnUninstall: '',
msgOnUninstall: ''
};
export default {
title: 'Metadata LRU Cache',
version: '0.0.1',
@ -44,7 +48,8 @@ export default {
category: 'Cache',
active: true,
input: JSON.stringify({
max: 500, maxAge: 1000 * 60 * 60 * 24
max: 500,
maxAge: 1000 * 60 * 60 * 24
}),
input_schema: JSON.stringify(input)
};
};

72
packages/nocodb/src/lib/noco/plugins/discord.ts

@ -1,47 +1,53 @@
import {XcActionType, XcForm, XcType} from "nc-common";
import { XcActionType, XcForm, XcType } from 'nc-common';
const input: XcForm = {
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:'',
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 {
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.',
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',
input_schema: JSON.stringify(input)
};
};

54
packages/nocodb/src/lib/noco/plugins/ee.ts

@ -1,41 +1,43 @@
import {XcActionType, XcForm, XcType} from "nc-common";
import { XcActionType, XcForm, XcType } from 'nc-common';
const input: XcForm = {
title: 'Configure Enterprise Edition',
items: [{
key: 'key',
label: 'Key',
placeholder: 'Key',
type: XcType.Password,
required: true
},
items: [
{
key: 'key',
label: 'Key',
placeholder: 'Key',
type: XcType.Password,
required: true
}
// {
// key: 'callback_url',
// label: 'Callback URL',
// placeholder: 'Callback URL',
// type: XcType.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
}
],
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 enabled Enterprise Edition.',
msgOnUninstall: '',
msgOnUninstall: ''
};
export default {
title: 'Enterprise Edition',
version: '0.0.1',
@ -45,4 +47,4 @@ export default {
tags: 'Enterprise',
category: 'Enterprise',
input_schema: JSON.stringify(input)
};
};

81
packages/nocodb/src/lib/noco/plugins/githubAuth.ts

@ -1,44 +1,51 @@
import {XcActionType, XcForm, XcType} from "nc-common";
import { XcActionType, XcForm, XcType } from 'nc-common';
const input: XcForm = {
title: 'Configure Github Auth',
items: [{
key: 'client_id',
label: 'Client ID',
placeholder: 'Client ID',
type: XcType.SingleLineText,
required: true
},{
key: 'client_secret',
label: 'Client Secret',
placeholder: 'Client Secret',
type: XcType.Password,
required: true
},{
key: 'redirect_url',
label: 'Redirect URL',
placeholder: 'Redirect URL',
type: XcType.SingleLineText,
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 configured Github Authentication, restart NocoDB',
msgOnUninstall:'',
items: [
{
key: 'client_id',
label: 'Client ID',
placeholder: 'Client ID',
type: XcType.SingleLineText,
required: true
},
{
key: 'client_secret',
label: 'Client Secret',
placeholder: 'Client Secret',
type: XcType.Password,
required: true
},
{
key: 'redirect_url',
label: 'Redirect URL',
placeholder: 'Redirect URL',
type: XcType.SingleLineText,
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 configured Github Authentication, restart NocoDB',
msgOnUninstall: ''
};
export default {
title: 'Github',
version: '0.0.1',
@ -48,4 +55,4 @@ export default {
tags: 'Authentication',
category: 'Github',
input_schema: JSON.stringify(input)
};
};

81
packages/nocodb/src/lib/noco/plugins/googleAuth.ts

@ -1,44 +1,51 @@
import {XcActionType, XcForm, XcType} from "nc-common";
import { XcActionType, XcForm, XcType } from 'nc-common';
const input: XcForm = {
title: 'Configure Google Auth',
items: [{
key: 'client_id',
label: 'Client ID',
placeholder: 'Client ID',
type: XcType.SingleLineText,
required: true
}, {
key: 'client_secret',
label: 'Client Secret',
placeholder: 'Client Secret',
type: XcType.Password,
required: true
}, {
key: 'redirect_url',
label: 'Redirect URL',
placeholder: 'Redirect URL',
type: XcType.SingleLineText,
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 configured Google Authentication, restart NocoDB',
msgOnUninstall: '',
items: [
{
key: 'client_id',
label: 'Client ID',
placeholder: 'Client ID',
type: XcType.SingleLineText,
required: true
},
{
key: 'client_secret',
label: 'Client Secret',
placeholder: 'Client Secret',
type: XcType.Password,
required: true
},
{
key: 'redirect_url',
label: 'Redirect URL',
placeholder: 'Redirect URL',
type: XcType.SingleLineText,
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 configured Google Authentication, restart NocoDB',
msgOnUninstall: ''
};
export default {
title: 'Google',
version: '0.0.1',
@ -48,4 +55,4 @@ export default {
tags: 'Authentication',
category: 'Google',
input_schema: JSON.stringify(input)
};
};

72
packages/nocodb/src/lib/noco/plugins/mattermost.ts

@ -1,47 +1,53 @@
import {XcActionType, XcForm, XcType} from "nc-common";
import { XcActionType, XcForm, XcType } from 'nc-common';
const input: XcForm = {
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: '',
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 {
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.',
description:
'Mattermost brings all your team communication into one place, making it searchable and accessible anywhere.',
price: 'Free',
tags: 'Chat',
category: 'Chat',
input_schema: JSON.stringify(input)
};
};

123
packages/nocodb/src/lib/noco/plugins/ses.ts

@ -1,69 +1,80 @@
import {XcActionType, XcForm, XcType} from "nc-common";
import { XcActionType, XcForm, XcType } from 'nc-common';
const input: XcForm = {
title: 'Configure Amazon Simple Email Service(SES)',
items: [{
key: 'from',
label: 'From',
placeholder: 'From',
type: XcType.SingleLineText,
required: true
}, {
key: 'host',
label: 'Jost',
placeholder: 'Jost',
type: XcType.SingleLineText,
required: true
}, {
key: 'port',
label: 'Port',
placeholder: 'Port',
type: XcType.SingleLineText,
required: true
}, {
key: 'secure',
label: 'Secure',
placeholder: 'Secure',
type: XcType.SingleLineText,
required: true
}, {
key: 'username',
label: 'Username',
placeholder: 'Username',
type: XcType.SingleLineText,
required: true
},{
key: 'password',
label: 'Password',
placeholder: 'Password',
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:'',
items: [
{
key: 'from',
label: 'From',
placeholder: 'From',
type: XcType.SingleLineText,
required: true
},
{
key: 'host',
label: 'Jost',
placeholder: 'Jost',
type: XcType.SingleLineText,
required: true
},
{
key: 'port',
label: 'Port',
placeholder: 'Port',
type: XcType.SingleLineText,
required: true
},
{
key: 'secure',
label: 'Secure',
placeholder: 'Secure',
type: XcType.SingleLineText,
required: true
},
{
key: 'username',
label: 'Username',
placeholder: 'Username',
type: XcType.SingleLineText,
required: true
},
{
key: 'password',
label: 'Password',
placeholder: 'Password',
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 {
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.',
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',
input_schema: JSON.stringify(input)
};
};

69
packages/nocodb/src/lib/noco/plugins/slack.ts

@ -1,47 +1,52 @@
import {XcActionType, XcForm, XcType} from "nc-common";
import { XcActionType, XcForm, XcType } from 'nc-common';
const input: XcForm = {
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
},],
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: '',
msgOnUninstall: ''
};
export default {
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. ',
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',
input_schema: JSON.stringify(input)
};
};

129
packages/nocodb/src/lib/noco/plugins/smtp.ts

@ -1,66 +1,77 @@
import {XcActionType, XcForm, XcType} from "nc-common";
import { XcActionType, XcForm, XcType } from 'nc-common';
const input: XcForm = {
title: 'Configure Email SMTP',
items: [{
key: 'from',
label: 'From',
placeholder: 'eg: admin@example.com',
type: XcType.SingleLineText,
required: true
}, {
key: 'host',
label: 'Host',
placeholder: 'eg: smtp.example.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.SingleLineText,
required: true
}, {
key: 'ignoreTLS',
label: 'IgnoreTLS',
placeholder: 'IgnoreTLS',
type: XcType.SingleLineText,
required: true
}, {
key: 'username',
label: 'Username',
placeholder: 'Username',
type: XcType.SingleLineText,
required: true
}, {
key: 'password',
label: 'Password',
placeholder: 'Password',
type: XcType.Password,
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 SMTP configuration',
msgOnUninstall: '',
items: [
{
key: 'from',
label: 'From',
placeholder: 'eg: admin@example.com',
type: XcType.SingleLineText,
required: true
},
{
key: 'host',
label: 'Host',
placeholder: 'eg: smtp.example.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.SingleLineText,
required: true
},
{
key: 'ignoreTLS',
label: 'IgnoreTLS',
placeholder: 'IgnoreTLS',
type: XcType.SingleLineText,
required: true
},
{
key: 'username',
label: 'Username',
placeholder: 'Username',
type: XcType.SingleLineText,
required: true
},
{
key: 'password',
label: 'Password',
placeholder: 'Password',
type: XcType.Password,
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 SMTP configuration',
msgOnUninstall: ''
};
export default {
title: 'SMTP',
version: '0.0.1',
@ -70,4 +81,4 @@ export default {
tags: 'Email',
category: 'Email',
input_schema: JSON.stringify(input)
};
};

85
packages/nocodb/src/lib/noco/plugins/twilio.ts

@ -1,52 +1,59 @@
import {XcActionType, XcForm, XcType} from "nc-common";
import { XcActionType, XcForm, XcType } from 'nc-common';
const input: XcForm = {
title: 'Configure Twilio',
items: [{
key: 'sid',
label: 'Account SID',
placeholder: 'Account SID',
type: XcType.SingleLineText,
required: true
}, {
key: 'token',
label: 'Auth Token' ,
placeholder: 'Auth Token',
type: XcType.Password,
required: true
}, {
key: 'from',
label: 'From Phone Number',
placeholder: 'From Phone Number',
type: XcType.SingleLineText,
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 Twilio is enabled for notification.',
msgOnUninstall: '',
items: [
{
key: 'sid',
label: 'Account SID',
placeholder: 'Account SID',
type: XcType.SingleLineText,
required: true
},
{
key: 'token',
label: 'Auth Token',
placeholder: 'Auth Token',
type: XcType.Password,
required: true
},
{
key: 'from',
label: 'From Phone Number',
placeholder: 'From Phone Number',
type: XcType.SingleLineText,
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 Twilio is enabled for notification.',
msgOnUninstall: ''
};
export default {
title: 'Twilio',
version: '0.0.1',
logo: 'plugins/twilio.png',
description: 'With Twilio, unite communications and strengthen customer relationships across your business – from marketing and sales to customer service and operations.',
description:
'With Twilio, unite communications and strengthen customer relationships across your business – from marketing and sales to customer service and operations.',
price: 'Free',
tags: 'Chat',
category: 'Twilio',
input_schema: JSON.stringify(input)
};
};

2248
packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts

File diff suppressed because it is too large Load Diff

1390
packages/nocodb/src/lib/noco/rest/RestAuthCtrl.ts

File diff suppressed because it is too large Load Diff

233
packages/nocodb/src/lib/noco/rest/RestAuthCtrlEE.ts

@ -1,32 +1,33 @@
import {Tele} from 'nc-help';
import { Tele } from 'nc-help';
import passport from 'passport';
import {Strategy} from 'passport-jwt';
import {v4 as uuidv4} from 'uuid';
import validator from "validator";
import { Strategy } from 'passport-jwt';
import { v4 as uuidv4 } from 'uuid';
import validator from 'validator';
import XcCache from "../plugins/adapters/cache/XcCache";
import XcCache from '../plugins/adapters/cache/XcCache';
import RestAuthCtrl from "./RestAuthCtrl";
import RestAuthCtrl from './RestAuthCtrl';
export default class RestAuthCtrlEE extends RestAuthCtrl {
protected async addAdmin(req, res, next): Promise<any> {
const emails = (req.body.email || '').split(/\s*,\s*/).map(v => v.trim());
// check for invalid emails
const invalidEmails = emails.filter(v => !validator.isEmail(v))
const invalidEmails = emails.filter(v => !validator.isEmail(v));
if (!emails.length) {
return next(new Error('Invalid email address'));
}
if (invalidEmails.length) {
return next(new Error('Invalid email address : ' + invalidEmails.join(', ')));
return next(
new Error('Invalid email address : ' + invalidEmails.join(', '))
);
}
// todo: handle roles which contains super
if (!req.session?.passport?.user?.roles?.owner && req.body.roles.indexOf('owner') > -1) {
if (
!req.session?.passport?.user?.roles?.owner &&
req.body.roles.indexOf('owner') > -1
) {
return next(new Error('Insufficient privilege to add super admin role.'));
}
@ -34,95 +35,122 @@ export default class RestAuthCtrlEE extends RestAuthCtrl {
const error = [];
for (const email of emails) {
// add user to project if user already exist
const user = await this.users.where({email}).first();
const user = await this.users.where({ email }).first();
if (user) {
await this.users.update({
roles: 'user'
}).where({roles: 'user_new', email});
if (!await this.xcMeta.isUserHaveAccessToProject(req.body.project_id, user.id)) {
await this.xcMeta.projectAddUser(req.body.project_id, user.id, 'editor');
await this.users
.update({
roles: 'user'
})
.where({ roles: 'user_new', email });
if (
!(await this.xcMeta.isUserHaveAccessToProject(
req.body.project_id,
user.id
))
) {
await this.xcMeta.projectAddUser(
req.body.project_id,
user.id,
'editor'
);
}
this.xcMeta.audit(req.body.project_id, null, 'nc_audit', {
op_type: 'AUTHENTICATION',
op_sub_type: 'INVITE',
user: req.user.email,
description: `invited ${email} to ${req.body.project_id} project `, ip: req.clientIp
})
description: `invited ${email} to ${req.body.project_id} project `,
ip: req.clientIp
});
} else {
try {
// create new user with invite token
await this.users.insert({
invite_token,
invite_token_expires: new Date(Date.now() + (24 * 60 * 60 * 1000)),
invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000),
email,
roles: 'user'
});
const {id} = await this.users.where({email}).first();
const { id } = await this.users.where({ email }).first();
const count = await this.users.count('id').first();
// add user to project
await this.xcMeta.projectAddUser(req.body.project_id, id, req.body.roles);
await this.xcMeta.projectAddUser(
req.body.project_id,
id,
req.body.roles
);
Tele.emit('evt', {evt_type: 'project:invite', count: count?.count});
Tele.emit('evt', { evt_type: 'project:invite', count: count?.count });
this.xcMeta.audit(req.body.project_id, null, 'nc_audit', {
op_type: 'AUTHENTICATION',
op_sub_type: 'INVITE',
user: req.user.email,
description: `invited ${email} to ${req.body.project_id} project `, ip: req.clientIp
})
description: `invited ${email} to ${req.body.project_id} project `,
ip: req.clientIp
});
// in case of single user check for smtp failure
// and send back token if failed
if (emails.length === 1 && !await this.sendInviteEmail(email, invite_token, req)) {
return res.json({invite_token, email});
if (
emails.length === 1 &&
!(await this.sendInviteEmail(email, invite_token, req))
) {
return res.json({ invite_token, email });
} else {
this.sendInviteEmail(email, invite_token, req)
this.sendInviteEmail(email, invite_token, req);
}
} catch (e) {
if (emails.length === 1) {
return next(e);
} else {
error.push({email, error: e.message})
error.push({ email, error: e.message });
}
}
}
}
if (emails.length === 1) {
res.json({
msg: 'success'
})
});
} else {
return res.json({invite_token, emails, error});
return res.json({ invite_token, emails, error });
}
}
protected async updateAdmin(req, res, next): Promise<any> {
if (!req?.body?.project_id) {
return next(new Error('Missing project id in request body.'));
}
if (req.session?.passport?.user?.roles?.owner && req.session?.passport?.user?.id === +req.params.id && req.body.roles.indexOf('owner') === -1) {
return next(new Error('Super admin can\'t remove Super role themselves'));
if (
req.session?.passport?.user?.roles?.owner &&
req.session?.passport?.user?.id === +req.params.id &&
req.body.roles.indexOf('owner') === -1
) {
return next(new Error("Super admin can't remove Super role themselves"));
}
try {
const user = await this.users.where({
id: req.params.id
}).first();
const user = await this.users
.where({
id: req.params.id
})
.first();
if (!user) {
return next(`User with id '${req.params.id}' doesn't exist`);
}
// todo: handle roles which contains super
if (!req.session?.passport?.user?.roles?.owner && req.body.roles.indexOf('owner') > -1) {
return next(new Error('Insufficient privilege to add super admin role.'));
if (
!req.session?.passport?.user?.roles?.owner &&
req.body.roles.indexOf('owner') > -1
) {
return next(
new Error('Insufficient privilege to add super admin role.')
);
}
// await this.users.update({
@ -131,77 +159,88 @@ export default class RestAuthCtrlEE extends RestAuthCtrl {
// id: req.params.id
// });
await this.xcMeta.metaUpdate(req?.body?.project_id, null, 'nc_projects_users', {
roles: req.body.roles
}, {
user_id: req.params.id,
// email: req.body.email
});
await this.xcMeta.metaUpdate(
req?.body?.project_id,
null,
'nc_projects_users',
{
roles: req.body.roles
},
{
user_id: req.params.id
// email: req.body.email
}
);
XcCache.del(`${req.body.email}___${req?.body?.project_id}`);
this.xcMeta.audit(null, null, 'nc_audit', {
op_type: 'AUTHENTICATION',
op_sub_type: 'ROLES_MANAGEMENT',
user: req.user.email,
description: `updated roles for ${user.email} with ${req.body.roles} `, ip: req.clientIp
})
description: `updated roles for ${user.email} with ${req.body.roles} `,
ip: req.clientIp
});
res.json({
msg: 'User details updated successfully'
})
});
} catch (e) {
next(e);
}
}
protected initJwtStrategy(): void {
passport.use(new Strategy({
...this.jwtOptions,
passReqToCallback: true
}, (req, jwtPayload, done) => {
const keyVals = [jwtPayload?.email]
if (req.ncProjectId) {
keyVals.push(req.ncProjectId);
}
const key = keyVals.join('___');
const cachedVal = XcCache.get(key);
if (cachedVal) {
return done(null, cachedVal);
}
this.users.where({
email: jwtPayload?.email
}).first().then(user => {
if (req.ncProjectId) {
this.xcMeta.metaGet(req.ncProjectId, null, 'nc_projects_users', {
user_id: user?.id
}).then(projectUser => {
user.roles = projectUser.roles;
XcCache.set(key, user);
done(null, user)
})
} else {
// const roles = projectUser?.roles ? JSON.parse(projectUser.roles) : {guest: true};
if (user) {
XcCache.set(key, user);
return done(null, user);
} else {
return done(new Error('User not found'));
passport.use(
new Strategy(
{
...this.jwtOptions,
passReqToCallback: true
},
(req, jwtPayload, done) => {
const keyVals = [jwtPayload?.email];
if (req.ncProjectId) {
keyVals.push(req.ncProjectId);
}
const key = keyVals.join('___');
const cachedVal = XcCache.get(key);
if (cachedVal) {
return done(null, cachedVal);
}
this.users
.where({
email: jwtPayload?.email
})
.first()
.then(user => {
if (req.ncProjectId) {
this.xcMeta
.metaGet(req.ncProjectId, null, 'nc_projects_users', {
user_id: user?.id
})
.then(projectUser => {
user.roles = projectUser.roles;
XcCache.set(key, user);
done(null, user);
});
} else {
// const roles = projectUser?.roles ? JSON.parse(projectUser.roles) : {guest: true};
if (user) {
XcCache.set(key, user);
return done(null, user);
} else {
return done(new Error('User not found'));
}
}
})
.catch(err => {
return done(err);
});
}
}).catch(err => {
return done(err);
})
}));
)
);
}
}
/**

89
packages/nocodb/src/lib/noco/rest/RestBaseCtrl.ts

@ -1,17 +1,15 @@
import {Handler, NextFunction, Request, Response, Router} from "express";
import Handlebars from "handlebars";
import { Handler, NextFunction, Request, Response, Router } from 'express';
import Handlebars from 'handlebars';
import {Route} from "../../../interface/config";
import { Route } from '../../../interface/config';
export abstract class RestBaseCtrl {
public router: Router;
public routes: Route[];
protected rootPath: string;
protected middlewareBody: string;
public updateMiddleware(middlewareBody: string): this {
this.middlewareBody = middlewareBody;
return this;
@ -22,9 +20,10 @@ export abstract class RestBaseCtrl {
return this;
}
public mapRoutes(router: Router, customRoutes?: any): any {
const middleware = this.middlewareBody ? this.generateHandlerFromStringBody(this.middlewareBody) : this.middleware;
const middleware = this.middlewareBody
? this.generateHandlerFromStringBody(this.middlewareBody)
: this.middleware;
customRoutes?.additional?.[this.controllerName]?.forEach(addRoute => {
const handlers = [middleware];
@ -33,31 +32,50 @@ export abstract class RestBaseCtrl {
handlers.push(this.postMiddleware);
router[addRoute.method](encodeURI(addRoute.path.slice(this.rootPath.length)), ...handlers);
})
router[addRoute.method](
encodeURI(addRoute.path.slice(this.rootPath.length)),
...handlers
);
});
this.routes.forEach((route: Route) => {
const handlers = [middleware];
if (customRoutes?.override?.[route.path]?.[route.type]?.length) {
handlers.push(
...customRoutes.override[route.path][route.type].map(hn => this.catchErr(hn))
...customRoutes.override[route.path][route.type].map(hn =>
this.catchErr(hn)
)
);
} else if (
route.functions &&
Array.isArray(route.functions) &&
route.functions.length
) {
handlers.push(
...route.functions.map(fnBody => {
return this.catchErr(this.generateHandlerFromStringBody(fnBody));
})
);
} else if (route.functions && Array.isArray(route.functions) && route.functions.length) {
handlers.push(...route.functions.map(fnBody => {
return this.catchErr(this.generateHandlerFromStringBody(fnBody))
}));
} else {
handlers.push(...route.handler.map(h => {
return this.catchErr((typeof h === 'string' ? (h in this ? this[h] : ((_req, res) => res.json({}))).bind(this) : h));
}))
handlers.push(
...route.handler.map(h => {
return this.catchErr(
typeof h === 'string'
? (h in this ? this[h] : (_req, res) => res.json({})).bind(this)
: h
);
})
);
}
handlers.push(this.postMiddleware);
router[route.type](encodeURI(route.path.slice(this.rootPath.length)), ...handlers);
})
router[route.type](
encodeURI(route.path.slice(this.rootPath.length)),
...handlers
);
});
}
protected generateHandlerFromStringBody(fnBody: string): Handler {
@ -72,12 +90,11 @@ export abstract class RestBaseCtrl {
// tslint:disable-next-line:no-eval
handler = eval(js);
} catch (e) {
console.log('Error in transpilation', e)
console.log('Error in transpilation', e);
}
return handler;
}
protected generateMiddlewareFromStringBody(fnBody: string): Handler {
// @ts-ignore
let middleware = (_req: Request, res: Response, _next: NextFunction) => {
@ -88,37 +105,45 @@ export abstract class RestBaseCtrl {
const js = `((${fnBody}).bind(this))`;
// tslint:disable-next-line:no-eval
middleware = eval(js);
} catch (e) {
console.log('Error in transpilation', e)
console.log('Error in transpilation', e);
}
return middleware;
}
protected catchErr(handler): Handler {
return (req, res, next) => {
(res as any).xcJson = (data) => {
(res as any).xcJson = data => {
res.locals.responseData = data;
next();
}
};
Promise.resolve(handler.call(this, req, res, next)).catch(err => {
next(err);
});
};
}
protected abstract postMiddleware(req: Request, res: Response, next: NextFunction): Promise<any>;
protected abstract postMiddleware(
req: Request,
res: Response,
next: NextFunction
): Promise<any>;
protected abstract middleware(req: Request, res: Response, next: NextFunction): Promise<any>;
protected abstract middleware(
req: Request,
res: Response,
next: NextFunction
): Promise<any>;
abstract get controllerName(): string;
protected replaceEnvVarRec(obj, req): any {
return JSON.parse(JSON.stringify(obj), (_key, value) => {
return typeof value === 'string' ? Handlebars.compile(value, {noEscape: true})({
req
}) : value;
return typeof value === 'string'
? Handlebars.compile(value, { noEscape: true })({
req
})
: value;
});
}
}

156
packages/nocodb/src/lib/noco/rest/RestCtrl.ts

@ -1,29 +1,32 @@
import autoBind from 'auto-bind';
import {NextFunction, Request, Response, Router} from "express";
import { NextFunction, Request, Response, Router } from 'express';
import {Acl, Acls, Route} from "../../../interface/config";
import {BaseModelSql} from "../../dataMapper";
import {RestBaseCtrl} from "./RestBaseCtrl";
import { Acl, Acls, Route } from '../../../interface/config';
import { BaseModelSql } from '../../dataMapper';
import { RestBaseCtrl } from './RestBaseCtrl';
function parseHrtimeToSeconds(hrtime) {
const seconds = (hrtime[0] + (hrtime[1] / 1e6)).toFixed(3);
const seconds = (hrtime[0] + hrtime[1] / 1e6).toFixed(3);
return seconds;
}
export class RestCtrl extends RestBaseCtrl {
public app: any;
private table: string;
private models: { [key: string]: BaseModelSql };
private acls: Acls;
constructor(app: any, models: { [key: string]: BaseModelSql }, table: string, routes: Route[], rootPath: string, acls: Acls, middlewareBody?: string) {
constructor(
app: any,
models: { [key: string]: BaseModelSql },
table: string,
routes: Route[],
rootPath: string,
acls: Acls,
middlewareBody?: string
) {
super();
autoBind(this);
this.app = app;
@ -36,7 +39,6 @@ export class RestCtrl extends RestBaseCtrl {
this.acls = acls;
}
private get model(): BaseModelSql {
return this.models?.[this.table];
}
@ -102,8 +104,14 @@ export class RestCtrl extends RestBaseCtrl {
}
public async count(req: Request | any, res: Response): Promise<void> {
if (req.query.conditionGraph && typeof req.query.conditionGraph === 'string') {
req.query.conditionGraph = {models: this.models, condition: JSON.parse(req.query.conditionGraph)}
if (
req.query.conditionGraph &&
typeof req.query.conditionGraph === 'string'
) {
req.query.conditionGraph = {
models: this.models,
condition: JSON.parse(req.query.conditionGraph)
};
}
const data = await req.model.countByPk({
...req.query
@ -125,7 +133,6 @@ export class RestCtrl extends RestBaseCtrl {
res.json(data);
}
public async bulkInsert(req: Request | any, res: Response): Promise<void> {
const data = await req.model.insertb(req.body);
res.json(data);
@ -137,7 +144,7 @@ export class RestCtrl extends RestBaseCtrl {
}
public async bulkDelete(req: Request | any, res: Response): Promise<void> {
const data = await req.model.delb(req.body)
const data = await req.model.delb(req.body);
res.json(data);
}
@ -145,13 +152,19 @@ export class RestCtrl extends RestBaseCtrl {
const startTime = process.hrtime();
try {
if (req.query.conditionGraph && typeof req.query.conditionGraph === 'string') {
req.query.conditionGraph = {models: this.models, condition: JSON.parse(req.query.conditionGraph)}
if (
req.query.conditionGraph &&
typeof req.query.conditionGraph === 'string'
) {
req.query.conditionGraph = {
models: this.models,
condition: JSON.parse(req.query.conditionGraph)
};
}
if (req.query.condition && typeof req.query.condition === 'string') {
req.query.condition = JSON.parse(req.query.condition)
req.query.condition = JSON.parse(req.query.condition);
}
}catch (e){
} catch (e) {
/* ignore parse error */
}
@ -167,13 +180,19 @@ export class RestCtrl extends RestBaseCtrl {
const startTime = process.hrtime();
try {
if (req.query.conditionGraph && typeof req.query.conditionGraph === 'string') {
req.query.conditionGraph = {models: this.models, condition: JSON.parse(req.query.conditionGraph)}
if (
req.query.conditionGraph &&
typeof req.query.conditionGraph === 'string'
) {
req.query.conditionGraph = {
models: this.models,
condition: JSON.parse(req.query.conditionGraph)
};
}
if (req.query.condition && typeof req.query.condition === 'string') {
req.query.condition = JSON.parse(req.query.condition)
req.query.condition = JSON.parse(req.query.condition);
}
}catch (e){
} catch (e) {
/* ignore parse error */
}
@ -185,12 +204,17 @@ export class RestCtrl extends RestBaseCtrl {
res.xcJson(data);
}
public async m2mNotChildren(req: Request | any, res): Promise<void> {
const startTime = process.hrtime();
if (req.query.conditionGraph && typeof req.query.conditionGraph === 'string') {
req.query.conditionGraph = {models: this.models, condition: JSON.parse(req.query.conditionGraph)}
if (
req.query.conditionGraph &&
typeof req.query.conditionGraph === 'string'
) {
req.query.conditionGraph = {
models: this.models,
condition: JSON.parse(req.query.conditionGraph)
};
}
const list = await req.model.m2mNotChildren({
@ -204,32 +228,39 @@ export class RestCtrl extends RestBaseCtrl {
const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime));
res.setHeader('xc-db-response', elapsedSeconds);
res.xcJson({list, info: count});
res.xcJson({ list, info: count });
}
protected async middleware(req: Request | any, res: Response, next: NextFunction): Promise<any> {
protected async middleware(
req: Request | any,
res: Response,
next: NextFunction
): Promise<any> {
req.model = this.model;
req.models = this.models;
req.table = this.table;
const methodOperationMap = {
get: 'read',
post: 'create',
put: 'update',
delete: 'delete',
}
delete: 'delete'
};
const roleOperationPossible = (roles, operation, object) => {
const errors = [];
for (const [roleName, isAllowed] of Object.entries(roles)) {
// todo: handle conditions from multiple roles
if (this.acl?.[roleName]?.[operation]?.custom) {
const condition = this.replaceEnvVarRec(this.acl?.[roleName]?.[operation]?.custom, req);
(req as any).query.conditionGraph = {condition, models: this.models};
const condition = this.replaceEnvVarRec(
this.acl?.[roleName]?.[operation]?.custom,
req
);
(req as any).query.conditionGraph = {
condition,
models: this.models
};
}
if (!isAllowed) {
@ -241,43 +272,48 @@ export class RestCtrl extends RestBaseCtrl {
if (this.acl[roleName][operation]) {
return true;
}
} else if (this.acl?.[roleName]?.[operation] && roleOperationObjectGet(roleName, operation, object)) {
} else if (
this.acl?.[roleName]?.[operation] &&
roleOperationObjectGet(roleName, operation, object)
) {
return true;
}
} catch (e) {
errors.push(e);
}
}
if (errors?.length) {
throw errors[0]
throw errors[0];
}
return false;
}
};
// @ts-ignore
const roleOperationObjectGet = (role, operation, object) => {
const columns = this.acl[role][operation].columns;
if (columns) {
// todo: merge allowed columns if multiple roles
const allowedCols = Object.keys(columns).filter(col => columns[col])
res.locals.xcAcl = {allowedCols, operation, columns};
const allowedCols = Object.keys(columns).filter(col => columns[col]);
res.locals.xcAcl = { allowedCols, operation, columns };
if (operation === 'update' || operation === 'create') {
if (Array.isArray(object)) {
for (const row of object) {
for (const colInReq of Object.keys(row)) {
if (!allowedCols.includes(colInReq)) {
throw new Error(`User doesn't have permission to edit '${colInReq}' column`);
throw new Error(
`User doesn't have permission to edit '${colInReq}' column`
);
}
}
}
} else {
for (const colInReq of Object.keys(object)) {
if (!allowedCols.includes(colInReq)) {
throw new Error(`User doesn't have permission to edit '${colInReq}' column`);
throw new Error(
`User doesn't have permission to edit '${colInReq}' column`
);
}
}
}
@ -286,21 +322,27 @@ export class RestCtrl extends RestBaseCtrl {
return Object.values(columns).some(Boolean);
}
}
}
const roles = (req as any)?.locals?.user?.roles ?? (req as any)?.session?.passport?.user?.roles ?? {
guest: true
};
const roles = (req as any)?.locals?.user?.roles ??
(req as any)?.session?.passport?.user?.roles ?? {
guest: true
};
try {
const allowed = roleOperationPossible(roles, methodOperationMap[req.method.toLowerCase()], req.body);
const allowed = roleOperationPossible(
roles,
methodOperationMap[req.method.toLowerCase()],
req.body
);
if (allowed) {
// any additional rules can be made here
return next();
} else {
const msg = roles.guest ? `Access Denied : Please Login or Signup for a new account` : `Access Denied for this account`;
const msg = roles.guest
? `Access Denied : Please Login or Signup for a new account`
: `Access Denied for this account`;
return res.status(403).json({
msg
});
@ -312,9 +354,11 @@ export class RestCtrl extends RestBaseCtrl {
}
}
protected async postMiddleware(_req: Request, res: Response, _next: NextFunction): Promise<any> {
protected async postMiddleware(
_req: Request,
res: Response,
_next: NextFunction
): Promise<any> {
const data = res.locals.responseData;
if (!res.locals.xcAcl) {
@ -322,7 +366,7 @@ export class RestCtrl extends RestBaseCtrl {
}
// @ts-ignore
const {allowedCols, operation, columns} = res.locals.xcAcl;
const { allowedCols, operation, columns } = res.locals.xcAcl;
if (Array.isArray(data)) {
for (const row of data) {
@ -342,11 +386,9 @@ export class RestCtrl extends RestBaseCtrl {
return res.json(data);
}
get controllerName(): string {
return this.table;
}
}
/**

141
packages/nocodb/src/lib/noco/rest/RestCtrlBelongsTo.ts

@ -1,25 +1,31 @@
import autoBind from 'auto-bind';
import {NextFunction, Request, Response} from "express";
import { NextFunction, Request, Response } from 'express';
import {Acl, Acls, Route} from "../../../interface/config";
import {BaseModelSql} from "../../dataMapper";
import { Acl, Acls, Route } from '../../../interface/config';
import { BaseModelSql } from '../../dataMapper';
import {RestBaseCtrl} from "./RestBaseCtrl";
import { RestBaseCtrl } from './RestBaseCtrl';
export class RestCtrlBelongsTo extends RestBaseCtrl {
public parentTable: string;
public childTable: string;
public app: any;
// public routes: Route[];
private models: { [key: string]: BaseModelSql };
private acls: Acls;
constructor(app: any, models: { [key: string]: BaseModelSql }, parentTable: string, childTable: string, routes: Route[], rootPath: string, acls: Acls, middlewareBody?: string) {
constructor(
app: any,
models: { [key: string]: BaseModelSql },
parentTable: string,
childTable: string,
routes: Route[],
rootPath: string,
acls: Acls,
middlewareBody?: string
) {
super();
autoBind(this);
this.app = app;
@ -42,7 +48,6 @@ export class RestCtrlBelongsTo extends RestBaseCtrl {
return this.models?.[this.childTable];
}
private get parentAcl(): Acl {
return this.acls?.[this.parentTable];
}
@ -59,7 +64,11 @@ export class RestCtrlBelongsTo extends RestBaseCtrl {
res.xcJson(data);
}
protected async middleware(req: Request | any, res: Response, next: NextFunction): Promise<any> {
protected async middleware(
req: Request | any,
res: Response,
next: NextFunction
): Promise<any> {
req.childModel = this.childModel;
req.parentModel = this.parentModel;
req.parentTable = this.parentTable;
@ -69,31 +78,45 @@ export class RestCtrlBelongsTo extends RestBaseCtrl {
get: 'read',
post: 'create',
put: 'update',
delete: 'delete',
}
delete: 'delete'
};
const roleOperationPossible = (roles, operation, object) => {
const errors = [];
res.locals.xcAcl = {operation};
res.locals.xcAcl = { operation };
for (const [roleName, isAllowed] of Object.entries(roles)) {
// todo: handling conditions from multiple roles
if (this.childAcl?.[roleName]?.[operation]?.custom) {
// req.query.condition = replaceEnvVarRec(this.acl?.[roleName]?.[operation]?.custom)
const condition = this.replaceEnvVarRec(this.childAcl?.[roleName]?.[operation]?.custom,req);
(req as any).query.childNestedCondition = {condition, models: this.models};
const condition = this.replaceEnvVarRec(
this.childAcl?.[roleName]?.[operation]?.custom,
req
);
(req as any).query.childNestedCondition = {
condition,
models: this.models
};
}
if (this.parentAcl?.[roleName]?.[operation]?.custom) {
// req.query.condition = replaceEnvVarRec(this.acl?.[roleName]?.[operation]?.custom)
const condition = this.replaceEnvVarRec(this.parentAcl?.[roleName]?.[operation]?.custom,req);
(req as any).query.conditionGraph = {condition, models: this.models};
const condition = this.replaceEnvVarRec(
this.parentAcl?.[roleName]?.[operation]?.custom,
req
);
(req as any).query.conditionGraph = {
condition,
models: this.models
};
}
const childColumns = this.childAcl[roleName]?.[operation]?.columns;
if (childColumns) {
const allowedChildCols = Object.keys(childColumns).filter(col => childColumns[col]);
res.locals.xcAcl.allowedChildCols = res.locals.xcAcl.allowedChildCols || [];
const allowedChildCols = Object.keys(childColumns).filter(
col => childColumns[col]
);
res.locals.xcAcl.allowedChildCols =
res.locals.xcAcl.allowedChildCols || [];
res.locals.xcAcl.allowedChildCols.push(...allowedChildCols);
res.locals.xcAcl.childColumns = childColumns;
}
@ -107,7 +130,10 @@ export class RestCtrlBelongsTo extends RestBaseCtrl {
if (this.parentAcl[roleName][operation]) {
return true;
}
} else if (this.parentAcl?.[roleName]?.[operation] && roleOperationObjectGet(roleName, operation, object)) {
} else if (
this.parentAcl?.[roleName]?.[operation] &&
roleOperationObjectGet(roleName, operation, object)
) {
return true;
}
} catch (e) {
@ -115,10 +141,10 @@ export class RestCtrlBelongsTo extends RestBaseCtrl {
}
}
if (errors?.length) {
throw errors[0]
throw errors[0];
}
return false;
}
};
// @ts-ignore
const roleOperationObjectGet = (role, operation, object) => {
@ -126,28 +152,38 @@ export class RestCtrlBelongsTo extends RestBaseCtrl {
if (columns) {
// todo: merge allowed columns if multiple roles
const allowedParentCols = Object.keys(columns).filter(col => columns[col]);
Object.assign(res.locals.xcAcl, {allowedParentCols, parentColumns: columns})
const allowedParentCols = Object.keys(columns).filter(
col => columns[col]
);
Object.assign(res.locals.xcAcl, {
allowedParentCols,
parentColumns: columns
});
return Object.values(columns).some(Boolean);
}
}
};
console.log(`${this.parentModel.tn}Hm${this.childModel.tn} middleware`)
console.log(`${this.parentModel.tn}Hm${this.childModel.tn} middleware`);
const roles = (req as any)?.locals?.user?.roles ?? (req as any)?.session?.passport?.user?.roles ?? {
guest: true
};
const roles = (req as any)?.locals?.user?.roles ??
(req as any)?.session?.passport?.user?.roles ?? {
guest: true
};
try {
const allowed = roleOperationPossible(roles, methodOperationMap[req.method.toLowerCase()], req.body);
const allowed = roleOperationPossible(
roles,
methodOperationMap[req.method.toLowerCase()],
req.body
);
if (allowed) {
// any additional rules can be made here
return next();
} else {
const msg = roles.guest ? `Access Denied : Please Login or Signup for a new account` : `Access Denied for this account`;
const msg = roles.guest
? `Access Denied : Please Login or Signup for a new account`
: `Access Denied for this account`;
return res.status(403).json({
msg
});
@ -159,20 +195,28 @@ export class RestCtrlBelongsTo extends RestBaseCtrl {
}
}
protected async postMiddleware(req: Request, res: Response, _next: NextFunction): Promise<any> {
protected async postMiddleware(
req: Request,
res: Response,
_next: NextFunction
): Promise<any> {
const data = res.locals.responseData;
if (!res.locals.xcAcl) {
return res.json(data);
}
// @ts-ignore
const {allowedChildCols, operation, allowedParentCols, parentColumns, childColumns} = res.locals.xcAcl;
const isBt = req.url.toLowerCase().startsWith('/belongs/' + this.parentTable.toLowerCase());
const {
allowedChildCols,
operation,
allowedParentCols,
parentColumns,
childColumns
} = res.locals.xcAcl;
const isBt = req.url
.toLowerCase()
.startsWith('/belongs/' + this.parentTable.toLowerCase());
if ((!allowedChildCols || !isBt) && !allowedParentCols) {
return res.json(data);
@ -191,10 +235,18 @@ export class RestCtrlBelongsTo extends RestBaseCtrl {
delete row[colInRes][colInChild];
}
}
} else if (childColumns && colInRes in childColumns && !childColumns[colInRes]) {
} else if (
childColumns &&
colInRes in childColumns &&
!childColumns[colInRes]
) {
delete row[colInRes];
}
} else if (childColumns && colInRes in childColumns && !childColumns[colInRes]) {
} else if (
childColumns &&
colInRes in childColumns &&
!childColumns[colInRes]
) {
delete row[colInRes];
}
}
@ -209,7 +261,6 @@ export class RestCtrlBelongsTo extends RestBaseCtrl {
return res.json(data);
}
get controllerName(): string {
return `${this.childTable}.bt.${this.parentTable}`;
}

33
packages/nocodb/src/lib/noco/rest/RestCtrlCustom.ts

@ -1,20 +1,22 @@
import autoBind from 'auto-bind';
import {NextFunction, Request, Response, Router} from "express";
import { NextFunction, Request, Response, Router } from 'express';
import {Route} from "../../../interface/config";
import {BaseModelSql} from "../../dataMapper";
import { Route } from '../../../interface/config';
import { BaseModelSql } from '../../dataMapper';
import {RestBaseCtrl} from "./RestBaseCtrl";
import { RestBaseCtrl } from './RestBaseCtrl';
export class RestCtrlCustom extends RestBaseCtrl {
public app: any;
protected models: { [key: string]: BaseModelSql };
constructor(app: any, models: { [key: string]: BaseModelSql }, routes: Route[], middlewareBody?: string) {
constructor(
app: any,
models: { [key: string]: BaseModelSql },
routes: Route[],
middlewareBody?: string
) {
super();
autoBind(this);
this.app = app;
@ -25,18 +27,25 @@ export class RestCtrlCustom extends RestBaseCtrl {
this.rootPath = '';
}
protected async middleware(_req: Request, _res: Response, next: NextFunction): Promise<any> {
protected async middleware(
_req: Request,
_res: Response,
next: NextFunction
): Promise<any> {
next();
// return Promise.resolve(undefined);
}
protected postMiddleware(_req: Request, _res: Response, _next: NextFunction): Promise<any> {
protected postMiddleware(
_req: Request,
_res: Response,
_next: NextFunction
): Promise<any> {
return Promise.resolve(undefined);
}
get controllerName(): string {
return "__xc_custom";
return '__xc_custom';
}
}
/**

146
packages/nocodb/src/lib/noco/rest/RestCtrlHasMany.ts

@ -1,24 +1,30 @@
import autoBind from 'auto-bind';
import {NextFunction, Request, Response} from "express";
import { NextFunction, Request, Response } from 'express';
import {Acl, Acls, Route} from "../../../interface/config";
import {BaseModelSql} from "../../dataMapper";
import { Acl, Acls, Route } from '../../../interface/config';
import { BaseModelSql } from '../../dataMapper';
import {RestBaseCtrl} from "./RestBaseCtrl";
import { RestBaseCtrl } from './RestBaseCtrl';
export class RestCtrlHasMany extends RestBaseCtrl {
public parentTable: string;
public childTable: string;
public app: any;
// public routes: Route[];
private models: { [key: string]: BaseModelSql };
private acls: Acls;
constructor(app: any, models: { [key: string]: BaseModelSql }, parentTable: string, childTable: string, routes: Route[], rootPath: string, acls: Acls, middlewareBody?: string) {
constructor(
app: any,
models: { [key: string]: BaseModelSql },
parentTable: string,
childTable: string,
routes: Route[],
rootPath: string,
acls: Acls,
middlewareBody?: string
) {
super();
autoBind(this);
this.app = app;
@ -114,13 +120,12 @@ export class RestCtrlHasMany extends RestBaseCtrl {
res.json(data);
}
public async list(req: Request | any, res): Promise<void> {
const data = await req.parentModel.hasManyChildren({
child: req.childModel.tn,
...req.params,
...req.query
} as any)
} as any);
res.xcJson(data);
}
@ -132,8 +137,11 @@ export class RestCtrlHasMany extends RestBaseCtrl {
res.xcJson(data);
}
protected async middleware(req: Request | any, res: Response, next: NextFunction): Promise<any> {
protected async middleware(
req: Request | any,
res: Response,
next: NextFunction
): Promise<any> {
req.childModel = this.childModel;
req.parentModel = this.parentModel;
req.parentTable = this.parentTable;
@ -143,30 +151,43 @@ export class RestCtrlHasMany extends RestBaseCtrl {
get: 'read',
post: 'create',
put: 'update',
delete: 'delete',
}
delete: 'delete'
};
const roleOperationPossible = (roles, operation, object) => {
const errors = [];
res.locals.xcAcl = {operation};
res.locals.xcAcl = { operation };
for (const [roleName, isAllowed] of Object.entries(roles)) {
if (this.childAcl?.[roleName]?.[operation]?.custom) {
const condition = this.replaceEnvVarRec(this.childAcl?.[roleName]?.[operation]?.custom, req);
(req as any).query.conditionGraph = {condition, models: this.models};
const condition = this.replaceEnvVarRec(
this.childAcl?.[roleName]?.[operation]?.custom,
req
);
(req as any).query.conditionGraph = {
condition,
models: this.models
};
}
if (this.parentAcl?.[roleName]?.[operation]?.custom) {
const condition = this.replaceEnvVarRec(this.parentAcl?.[roleName]?.[operation]?.custom, req);
(req as any).query.parentNestedCondition = {condition, models: this.models};
const condition = this.replaceEnvVarRec(
this.parentAcl?.[roleName]?.[operation]?.custom,
req
);
(req as any).query.parentNestedCondition = {
condition,
models: this.models
};
}
const parentColumns = this.parentAcl?.[roleName]?.[operation]?.columns;
if (parentColumns) {
const allowedParentCols = Object.keys(parentColumns).filter(col => parentColumns[col]);
res.locals.xcAcl.allowedParentCols = res.locals.xcAcl.allowedParentCols || [];
const allowedParentCols = Object.keys(parentColumns).filter(
col => parentColumns[col]
);
res.locals.xcAcl.allowedParentCols =
res.locals.xcAcl.allowedParentCols || [];
res.locals.xcAcl.allowedParentCols.push(...allowedParentCols);
// todo: merge columns
@ -182,7 +203,10 @@ export class RestCtrlHasMany extends RestBaseCtrl {
if (this.childAcl?.[roleName]?.[operation]) {
return true;
}
} else if (this.childAcl?.[roleName]?.[operation] && roleOperationObjectGet(roleName, operation, object)) {
} else if (
this.childAcl?.[roleName]?.[operation] &&
roleOperationObjectGet(roleName, operation, object)
) {
return true;
}
} catch (e) {
@ -190,10 +214,10 @@ export class RestCtrlHasMany extends RestBaseCtrl {
}
}
if (errors?.length) {
throw errors[0]
throw errors[0];
}
return false;
}
};
// @ts-ignore
const roleOperationObjectGet = (role, operation, object) => {
@ -201,22 +225,31 @@ export class RestCtrlHasMany extends RestBaseCtrl {
if (columns) {
// todo: merge allowed columns if multiple roles
const allowedChildCols = Object.keys(columns).filter(col => columns[col]);
Object.assign(res.locals.xcAcl, {allowedChildCols, childColumns: columns})
const allowedChildCols = Object.keys(columns).filter(
col => columns[col]
);
Object.assign(res.locals.xcAcl, {
allowedChildCols,
childColumns: columns
});
if (operation === 'update' || operation === 'create') {
if (Array.isArray(object)) {
for (const row of object) {
for (const colInReq of Object.keys(row)) {
if (!allowedChildCols.includes(colInReq)) {
throw new Error(`User doesn't have permission to edit '${colInReq}' column`);
throw new Error(
`User doesn't have permission to edit '${colInReq}' column`
);
}
}
}
} else {
for (const colInReq of Object.keys(object)) {
if (!allowedChildCols.includes(colInReq)) {
throw new Error(`User doesn't have permission to edit '${colInReq}' column`);
throw new Error(
`User doesn't have permission to edit '${colInReq}' column`
);
}
}
}
@ -225,23 +258,28 @@ export class RestCtrlHasMany extends RestBaseCtrl {
return Object.values(columns).some(Boolean);
}
}
}
console.log(`${this.parentModel.tn}Hm${this.childModel.tn} middleware`)
};
console.log(`${this.parentModel.tn}Hm${this.childModel.tn} middleware`);
const roles = (req as any)?.locals?.user?.roles ?? (req as any)?.session?.passport?.user?.roles ?? {
guest: true
};
const roles = (req as any)?.locals?.user?.roles ??
(req as any)?.session?.passport?.user?.roles ?? {
guest: true
};
try {
const allowed = roleOperationPossible(roles, methodOperationMap[req.method.toLowerCase()], req.body);
const allowed = roleOperationPossible(
roles,
methodOperationMap[req.method.toLowerCase()],
req.body
);
if (allowed) {
// any additional rules can be made here
return next();
} else {
const msg = roles.guest ? `Access Denied : Please Login or Signup for a new account` : `Access Denied for this account`;
const msg = roles.guest
? `Access Denied : Please Login or Signup for a new account`
: `Access Denied for this account`;
return res.status(403).json({
msg
});
@ -253,20 +291,22 @@ export class RestCtrlHasMany extends RestBaseCtrl {
}
}
protected async postMiddleware(req: Request, res: Response, _next: NextFunction): Promise<any> {
protected async postMiddleware(
req: Request,
res: Response,
_next: NextFunction
): Promise<any> {
const data = res.locals.responseData;
if (!res.locals.xcAcl) {
return res.json(data);
}
// @ts-ignore
const {operation, parentColumns, childColumns} = res.locals.xcAcl;
const isHm = req.url.toLowerCase().startsWith('/has/' + this.childTable.toLowerCase());
const { operation, parentColumns, childColumns } = res.locals.xcAcl;
const isHm = req.url
.toLowerCase()
.startsWith('/has/' + this.childTable.toLowerCase());
if ((!parentColumns || !isHm) && !childColumns) {
return res.json(data);
@ -287,10 +327,18 @@ export class RestCtrlHasMany extends RestBaseCtrl {
}
}
}
} else if (parentColumns && colInRes in parentColumns && !parentColumns[colInRes]) {
} else if (
parentColumns &&
colInRes in parentColumns &&
!parentColumns[colInRes]
) {
delete row[colInRes];
}
} else if (childColumns && colInRes in childColumns && !childColumns[colInRes]) {
} else if (
childColumns &&
colInRes in childColumns &&
!childColumns[colInRes]
) {
delete row[colInRes];
}
}
@ -306,11 +354,9 @@ export class RestCtrlHasMany extends RestBaseCtrl {
return res.json(data);
}
get controllerName(): string {
return `${this.parentTable}.hm.${this.childTable}`;
}
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd

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

Loading…
Cancel
Save