@budibase/server 2.6.19-alpha.2 → 2.6.19-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@budibase/server",
3
3
  "email": "hi@budibase.com",
4
- "version": "2.6.19-alpha.2",
4
+ "version": "2.6.19-alpha.4",
5
5
  "description": "Budibase Web Server",
6
6
  "main": "src/index.ts",
7
7
  "repository": {
@@ -45,12 +45,12 @@
45
45
  "license": "GPL-3.0",
46
46
  "dependencies": {
47
47
  "@apidevtools/swagger-parser": "10.0.3",
48
- "@budibase/backend-core": "2.6.19-alpha.2",
49
- "@budibase/client": "2.6.19-alpha.2",
50
- "@budibase/pro": "2.6.19-alpha.2",
51
- "@budibase/shared-core": "2.6.19-alpha.2",
52
- "@budibase/string-templates": "2.6.19-alpha.2",
53
- "@budibase/types": "2.6.19-alpha.2",
48
+ "@budibase/backend-core": "2.6.19-alpha.4",
49
+ "@budibase/client": "2.6.19-alpha.4",
50
+ "@budibase/pro": "2.6.19-alpha.4",
51
+ "@budibase/shared-core": "2.6.19-alpha.4",
52
+ "@budibase/string-templates": "2.6.19-alpha.4",
53
+ "@budibase/types": "2.6.19-alpha.4",
54
54
  "@bull-board/api": "3.7.0",
55
55
  "@bull-board/koa": "3.9.4",
56
56
  "@elastic/elasticsearch": "7.10.0",
@@ -177,5 +177,5 @@
177
177
  "optionalDependencies": {
178
178
  "oracledb": "5.3.0"
179
179
  },
180
- "gitHead": "62e6959d05c2e64f53bb4090b8f714937907efa5"
180
+ "gitHead": "9a9aab1169f9639b28a0feef6f63577c3cbd0251"
181
181
  }
@@ -21,6 +21,7 @@ import {
21
21
  CreateDatasourceRequest,
22
22
  VerifyDatasourceRequest,
23
23
  VerifyDatasourceResponse,
24
+ FetchDatasourceInfoResponse,
24
25
  IntegrationBase,
25
26
  DatasourcePlus,
26
27
  } from "@budibase/types"
@@ -153,6 +154,21 @@ export async function verify(
153
154
  }
154
155
  }
155
156
 
157
+ export async function information(
158
+ ctx: UserCtx<void, FetchDatasourceInfoResponse>
159
+ ) {
160
+ const datasourceId = ctx.params.datasourceId
161
+ const datasource = await sdk.datasources.get(datasourceId, { enriched: true })
162
+ const connector = (await getConnector(datasource)) as DatasourcePlus
163
+ if (!connector.getTableNames) {
164
+ ctx.throw(400, "Table name fetching not supported by datasource")
165
+ }
166
+ const tableNames = await connector.getTableNames()
167
+ ctx.body = {
168
+ tableNames,
169
+ }
170
+ }
171
+
156
172
  export async function buildSchemaFromDb(ctx: UserCtx) {
157
173
  const db = context.getAppDB()
158
174
  const datasource = await sdk.datasources.get(ctx.params.datasourceId)
@@ -20,6 +20,11 @@ router
20
20
  authorized(permissions.BUILDER),
21
21
  datasourceController.verify
22
22
  )
23
+ .get(
24
+ "/api/datasources/:datasourceId/info",
25
+ authorized(permissions.BUILDER),
26
+ datasourceController.information
27
+ )
23
28
  .get(
24
29
  "/api/datasources/:datasourceId",
25
30
  authorized(
@@ -87,7 +87,7 @@ describe("/datasources", () => {
87
87
  expect(contents.rows.length).toEqual(1)
88
88
 
89
89
  // update the datasource to remove the variables
90
- datasource.config.dynamicVariables = []
90
+ datasource.config!.dynamicVariables = []
91
91
  const res = await request
92
92
  .put(`/api/datasources/${datasource._id}`)
93
93
  .send(datasource)
@@ -26,7 +26,7 @@ jest.setTimeout(30000)
26
26
 
27
27
  jest.unmock("pg")
28
28
 
29
- describe("row api - postgres", () => {
29
+ describe("postgres integrations", () => {
30
30
  let makeRequest: MakeRequestResponse,
31
31
  postgresDatasource: Datasource,
32
32
  primaryPostgresTable: Table,
@@ -52,8 +52,8 @@ describe("row api - postgres", () => {
52
52
  makeRequest = generateMakeRequest(apiKey, true)
53
53
  })
54
54
 
55
- beforeEach(async () => {
56
- postgresDatasource = await config.createDatasource({
55
+ function pgDatasourceConfig() {
56
+ return {
57
57
  datasource: {
58
58
  type: "datasource",
59
59
  source: SourceName.POSTGRES,
@@ -70,7 +70,11 @@ describe("row api - postgres", () => {
70
70
  ca: false,
71
71
  },
72
72
  },
73
- })
73
+ }
74
+ }
75
+
76
+ beforeEach(async () => {
77
+ postgresDatasource = await config.createDatasource(pgDatasourceConfig())
74
78
 
75
79
  async function createAuxTable(prefix: string) {
76
80
  return await config.createTable({
@@ -1024,4 +1028,43 @@ describe("row api - postgres", () => {
1024
1028
  })
1025
1029
  })
1026
1030
  })
1031
+
1032
+ describe("POST /api/datasources/verify", () => {
1033
+ it("should be able to verify the connection", async () => {
1034
+ const config = pgDatasourceConfig()
1035
+ const response = await makeRequest(
1036
+ "post",
1037
+ "/api/datasources/verify",
1038
+ config
1039
+ )
1040
+ expect(response.status).toBe(200)
1041
+ expect(response.body.connected).toBe(true)
1042
+ })
1043
+
1044
+ it("should state an invalid datasource cannot connect", async () => {
1045
+ const config = pgDatasourceConfig()
1046
+ config.datasource.config.password = "wrongpassword"
1047
+ const response = await makeRequest(
1048
+ "post",
1049
+ "/api/datasources/verify",
1050
+ config
1051
+ )
1052
+ expect(response.status).toBe(200)
1053
+ expect(response.body.connected).toBe(false)
1054
+ expect(response.body.error).toBeDefined()
1055
+ })
1056
+ })
1057
+
1058
+ describe("GET /api/datasources/:datasourceId/info", () => {
1059
+ it("should fetch information about postgres datasource", async () => {
1060
+ const primaryName = primaryPostgresTable.name
1061
+ const response = await makeRequest(
1062
+ "get",
1063
+ `/api/datasources/${postgresDatasource._id}/info`
1064
+ )
1065
+ expect(response.status).toBe(200)
1066
+ expect(response.body.tableNames).toBeDefined()
1067
+ expect(response.body.tableNames.indexOf(primaryName)).not.toBe(-1)
1068
+ })
1069
+ })
1027
1070
  })
@@ -63,10 +63,13 @@ const SCHEMA: Integration = {
63
63
  relationships: false,
64
64
  docs: "https://developers.google.com/sheets/api/quickstart/nodejs",
65
65
  description:
66
- "Create and collaborate on online spreadsheets in real-time and from any device. ",
66
+ "Create and collaborate on online spreadsheets in real-time and from any device.",
67
67
  friendlyName: "Google Sheets",
68
68
  type: "Spreadsheet",
69
- features: [DatasourceFeature.CONNECTION_CHECKING],
69
+ features: [
70
+ DatasourceFeature.CONNECTION_CHECKING,
71
+ DatasourceFeature.FETCH_TABLE_NAMES,
72
+ ],
70
73
  datasource: {
71
74
  spreadsheetId: {
72
75
  display: "Google Sheet URL",
@@ -145,7 +148,6 @@ class GoogleSheetsIntegration implements DatasourcePlus {
145
148
  async testConnection(): Promise<ConnectionInfo> {
146
149
  try {
147
150
  await this.connect()
148
- await this.client.loadInfo()
149
151
  return { connected: true }
150
152
  } catch (e: any) {
151
153
  return {
@@ -240,6 +242,12 @@ class GoogleSheetsIntegration implements DatasourcePlus {
240
242
  }
241
243
  }
242
244
 
245
+ async getTableNames(): Promise<string[]> {
246
+ await this.connect()
247
+ const sheets = this.client.sheetsByIndex
248
+ return sheets.map(s => s.title)
249
+ }
250
+
243
251
  getTableSchema(title: string, headerValues: string[], id?: string) {
244
252
  // base table
245
253
  const table: Table = {
@@ -20,7 +20,6 @@ import {
20
20
  } from "./utils"
21
21
  import Sql from "./base/sql"
22
22
  import { MSSQLTablesResponse, MSSQLColumn } from "./base/types"
23
-
24
23
  const sqlServer = require("mssql")
25
24
  const DEFAULT_SCHEMA = "dbo"
26
25
 
@@ -41,7 +40,10 @@ const SCHEMA: Integration = {
41
40
  "Microsoft SQL Server is a relational database management system developed by Microsoft. ",
42
41
  friendlyName: "MS SQL Server",
43
42
  type: "Relational",
44
- features: [DatasourceFeature.CONNECTION_CHECKING],
43
+ features: [
44
+ DatasourceFeature.CONNECTION_CHECKING,
45
+ DatasourceFeature.FETCH_TABLE_NAMES,
46
+ ],
45
47
  datasource: {
46
48
  user: {
47
49
  type: DatasourceFieldType.STRING,
@@ -284,6 +286,20 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
284
286
  this.schemaErrors = final.errors
285
287
  }
286
288
 
289
+ async queryTableNames() {
290
+ let tableInfo: MSSQLTablesResponse[] = await this.runSQL(this.TABLES_SQL)
291
+ const schema = this.config.schema || DEFAULT_SCHEMA
292
+ return tableInfo
293
+ .filter((record: any) => record.TABLE_SCHEMA === schema)
294
+ .map((record: any) => record.TABLE_NAME)
295
+ .filter((name: string) => this.MASTER_TABLES.indexOf(name) === -1)
296
+ }
297
+
298
+ async getTableNames() {
299
+ await this.connect()
300
+ return this.queryTableNames()
301
+ }
302
+
287
303
  async read(query: SqlQuery | string) {
288
304
  await this.connect()
289
305
  const response = await this.internalQuery(getSqlQuery(query))
@@ -36,7 +36,10 @@ const SCHEMA: Integration = {
36
36
  type: "Relational",
37
37
  description:
38
38
  "MySQL Database Service is a fully managed database service to deploy cloud-native applications. ",
39
- features: [DatasourceFeature.CONNECTION_CHECKING],
39
+ features: [
40
+ DatasourceFeature.CONNECTION_CHECKING,
41
+ DatasourceFeature.FETCH_TABLE_NAMES,
42
+ ],
40
43
  datasource: {
41
44
  host: {
42
45
  type: DatasourceFieldType.STRING,
@@ -214,20 +217,11 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
214
217
 
215
218
  async buildSchema(datasourceId: string, entities: Record<string, Table>) {
216
219
  const tables: { [key: string]: Table } = {}
217
- const database = this.config.database
218
220
  await this.connect()
219
221
 
220
222
  try {
221
223
  // get the tables first
222
- const tablesResp: Record<string, string>[] = await this.internalQuery(
223
- { sql: "SHOW TABLES;" },
224
- { connect: false }
225
- )
226
- const tableNames: string[] = tablesResp.map(
227
- (obj: any) =>
228
- obj[`Tables_in_${database}`] ||
229
- obj[`Tables_in_${database.toLowerCase()}`]
230
- )
224
+ const tableNames = await this.queryTableNames()
231
225
  for (let tableName of tableNames) {
232
226
  const primaryKeys = []
233
227
  const schema: TableSchema = {}
@@ -274,6 +268,28 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
274
268
  this.schemaErrors = final.errors
275
269
  }
276
270
 
271
+ async queryTableNames() {
272
+ const database = this.config.database
273
+ const tablesResp: Record<string, string>[] = await this.internalQuery(
274
+ { sql: "SHOW TABLES;" },
275
+ { connect: false }
276
+ )
277
+ return tablesResp.map(
278
+ (obj: any) =>
279
+ obj[`Tables_in_${database}`] ||
280
+ obj[`Tables_in_${database.toLowerCase()}`]
281
+ )
282
+ }
283
+
284
+ async getTableNames() {
285
+ await this.connect()
286
+ try {
287
+ return this.queryTableNames()
288
+ } finally {
289
+ await this.disconnect()
290
+ }
291
+ }
292
+
277
293
  async create(query: SqlQuery | string) {
278
294
  const results = await this.internalQuery(getSqlQuery(query))
279
295
  return results.length ? results : [{ created: true }]
@@ -50,7 +50,10 @@ const SCHEMA: Integration = {
50
50
  type: "Relational",
51
51
  description:
52
52
  "Oracle Database is an object-relational database management system developed by Oracle Corporation",
53
- features: [DatasourceFeature.CONNECTION_CHECKING],
53
+ features: [
54
+ DatasourceFeature.CONNECTION_CHECKING,
55
+ DatasourceFeature.FETCH_TABLE_NAMES,
56
+ ],
54
57
  datasource: {
55
58
  host: {
56
59
  type: DatasourceFieldType.STRING,
@@ -323,6 +326,13 @@ class OracleIntegration extends Sql implements DatasourcePlus {
323
326
  this.schemaErrors = final.errors
324
327
  }
325
328
 
329
+ async getTableNames() {
330
+ const columnsResponse = await this.internalQuery<OracleColumnsResponse>({
331
+ sql: this.COLUMNS_SQL,
332
+ })
333
+ return (columnsResponse.rows || []).map(row => row.TABLE_NAME)
334
+ }
335
+
326
336
  async testConnection() {
327
337
  const response: ConnectionInfo = {
328
338
  connected: false,
@@ -52,7 +52,10 @@ const SCHEMA: Integration = {
52
52
  type: "Relational",
53
53
  description:
54
54
  "PostgreSQL, also known as Postgres, is a free and open-source relational database management system emphasizing extensibility and SQL compliance.",
55
- features: [DatasourceFeature.CONNECTION_CHECKING],
55
+ features: [
56
+ DatasourceFeature.CONNECTION_CHECKING,
57
+ DatasourceFeature.FETCH_TABLE_NAMES,
58
+ ],
56
59
  datasource: {
57
60
  host: {
58
61
  type: DatasourceFieldType.STRING,
@@ -126,14 +129,15 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
126
129
 
127
130
  COLUMNS_SQL!: string
128
131
 
129
- PRIMARY_KEYS_SQL = `
130
- select tc.table_schema, tc.table_name, kc.column_name as primary_key
131
- from information_schema.table_constraints tc
132
- join
133
- information_schema.key_column_usage kc on kc.table_name = tc.table_name
134
- and kc.table_schema = tc.table_schema
135
- and kc.constraint_name = tc.constraint_name
136
- where tc.constraint_type = 'PRIMARY KEY';
132
+ PRIMARY_KEYS_SQL = () => `
133
+ SELECT pg_namespace.nspname table_schema
134
+ , pg_class.relname table_name
135
+ , pg_attribute.attname primary_key
136
+ FROM pg_class
137
+ JOIN pg_index ON pg_class.oid = pg_index.indrelid AND pg_index.indisprimary
138
+ JOIN pg_attribute ON pg_attribute.attrelid = pg_class.oid AND pg_attribute.attnum = ANY(pg_index.indkey)
139
+ JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace
140
+ WHERE pg_namespace.nspname = '${this.config.schema}';
137
141
  `
138
142
 
139
143
  constructor(config: PostgresConfig) {
@@ -239,7 +243,9 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
239
243
  let tableKeys: { [key: string]: string[] } = {}
240
244
  await this.openConnection()
241
245
  try {
242
- const primaryKeysResponse = await this.client.query(this.PRIMARY_KEYS_SQL)
246
+ const primaryKeysResponse = await this.client.query(
247
+ this.PRIMARY_KEYS_SQL()
248
+ )
243
249
  for (let table of primaryKeysResponse.rows) {
244
250
  const tableName = table.table_name
245
251
  if (!tableKeys[tableName]) {
@@ -311,6 +317,17 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
311
317
  }
312
318
  }
313
319
 
320
+ async getTableNames() {
321
+ try {
322
+ await this.openConnection()
323
+ const columnsResponse: { rows: PostgresColumn[] } =
324
+ await this.client.query(this.COLUMNS_SQL)
325
+ return columnsResponse.rows.map(row => row.table_name)
326
+ } finally {
327
+ await this.closeConnection()
328
+ }
329
+ }
330
+
314
331
  async create(query: SqlQuery | string) {
315
332
  const response = await this.internalQuery(getSqlQuery(query))
316
333
  return response.rows.length ? response.rows : [{ created: true }]
@@ -17,14 +17,15 @@ jest.mock("google-spreadsheet")
17
17
  const { GoogleSpreadsheet } = require("google-spreadsheet")
18
18
 
19
19
  const sheetsByTitle: { [title: string]: GoogleSpreadsheetWorksheet } = {}
20
+ const sheetsByIndex: GoogleSpreadsheetWorksheet[] = []
21
+ const mockGoogleIntegration = {
22
+ useOAuth2Client: jest.fn(),
23
+ loadInfo: jest.fn(),
24
+ sheetsByTitle,
25
+ sheetsByIndex,
26
+ }
20
27
 
21
- GoogleSpreadsheet.mockImplementation(() => {
22
- return {
23
- useOAuth2Client: jest.fn(),
24
- loadInfo: jest.fn(),
25
- sheetsByTitle,
26
- }
27
- })
28
+ GoogleSpreadsheet.mockImplementation(() => mockGoogleIntegration)
28
29
 
29
30
  import { structures } from "@budibase/backend-core/tests"
30
31
  import TestConfiguration from "../../tests/utilities/TestConfiguration"
@@ -53,6 +54,8 @@ describe("Google Sheets Integration", () => {
53
54
  },
54
55
  })
55
56
  await config.init()
57
+
58
+ jest.clearAllMocks()
56
59
  })
57
60
 
58
61
  function createBasicTable(name: string, columns: string[]): Table {
@@ -88,7 +91,7 @@ describe("Google Sheets Integration", () => {
88
91
  }
89
92
 
90
93
  describe("update table", () => {
91
- test("adding a new field will be adding a new header row", async () => {
94
+ it("adding a new field will be adding a new header row", async () => {
92
95
  await config.doInContext(structures.uuid(), async () => {
93
96
  const tableColumns = ["name", "description", "new field"]
94
97
  const table = createBasicTable(structures.uuid(), tableColumns)
@@ -103,7 +106,7 @@ describe("Google Sheets Integration", () => {
103
106
  })
104
107
  })
105
108
 
106
- test("removing an existing field will remove the header from the google sheet", async () => {
109
+ it("removing an existing field will remove the header from the google sheet", async () => {
107
110
  const sheet = await config.doInContext(structures.uuid(), async () => {
108
111
  const tableColumns = ["name"]
109
112
  const table = createBasicTable(structures.uuid(), tableColumns)
@@ -123,4 +126,33 @@ describe("Google Sheets Integration", () => {
123
126
  expect((sheet.setHeaderRow as any).mock.calls[0][0]).toHaveLength(1)
124
127
  })
125
128
  })
129
+
130
+ describe("getTableNames", () => {
131
+ it("can fetch table names", async () => {
132
+ await config.doInContext(structures.uuid(), async () => {
133
+ const sheetNames: string[] = []
134
+ for (let i = 0; i < 5; i++) {
135
+ const sheet = createSheet({ headerValues: [] })
136
+ sheetsByIndex.push(sheet)
137
+ sheetNames.push(sheet.title)
138
+ }
139
+
140
+ const res = await integration.getTableNames()
141
+
142
+ expect(mockGoogleIntegration.loadInfo).toBeCalledTimes(1)
143
+ expect(res).toEqual(sheetNames)
144
+ })
145
+ })
146
+ })
147
+
148
+ describe("testConnection", () => {
149
+ it("can test successful connections", async () => {
150
+ await config.doInContext(structures.uuid(), async () => {
151
+ const res = await integration.testConnection()
152
+
153
+ expect(mockGoogleIntegration.loadInfo).toBeCalledTimes(1)
154
+ expect(res).toEqual({ connected: true })
155
+ })
156
+ })
157
+ })
126
158
  })