@budibase/server 2.4.40 → 2.4.41

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.
@@ -1,22 +1,25 @@
1
1
  import {
2
2
  DatasourceFieldType,
3
3
  DatasourcePlus,
4
+ FieldType,
4
5
  Integration,
6
+ Operation,
5
7
  PaginationJson,
6
8
  QueryJson,
7
9
  QueryType,
10
+ Row,
8
11
  SearchFilters,
9
12
  SortJson,
10
13
  Table,
11
- TableSchema,
14
+ TableRequest,
12
15
  } from "@budibase/types"
13
16
  import { OAuth2Client } from "google-auth-library"
14
- import { buildExternalTableId } from "./utils"
15
- import { DataSourceOperation, FieldTypes } from "../constants"
17
+ import { buildExternalTableId, finaliseExternalTables } from "./utils"
16
18
  import { GoogleSpreadsheet } from "google-spreadsheet"
17
19
  import fetch from "node-fetch"
18
20
  import { configs, HTTPError } from "@budibase/backend-core"
19
21
  import { dataFilters } from "@budibase/shared-core"
22
+ import { GOOGLE_SHEETS_PRIMARY_KEY } from "../constants"
20
23
 
21
24
  interface GoogleSheetsConfig {
22
25
  spreadsheetId: string
@@ -39,6 +42,17 @@ interface AuthTokenResponse {
39
42
  access_token: string
40
43
  }
41
44
 
45
+ const ALLOWED_TYPES = [
46
+ FieldType.STRING,
47
+ FieldType.FORMULA,
48
+ FieldType.NUMBER,
49
+ FieldType.LONGFORM,
50
+ FieldType.DATETIME,
51
+ FieldType.OPTIONS,
52
+ FieldType.BOOLEAN,
53
+ FieldType.BARCODEQR,
54
+ ]
55
+
42
56
  const SCHEMA: Integration = {
43
57
  plus: true,
44
58
  auth: {
@@ -199,73 +213,90 @@ class GoogleSheetsIntegration implements DatasourcePlus {
199
213
 
200
214
  this.client.useOAuth2Client(oauthClient)
201
215
  await this.client.loadInfo()
202
- } catch (err) {
216
+ } catch (err: any) {
217
+ // this happens for xlsx imports
218
+ if (err.message?.includes("operation is not supported")) {
219
+ err.message =
220
+ "This operation is not supported - XLSX sheets must be converted."
221
+ }
203
222
  console.error("Error connecting to google sheets", err)
204
223
  throw err
205
224
  }
206
225
  }
207
226
 
208
- async buildSchema(datasourceId: string) {
227
+ getTableSchema(title: string, headerValues: string[], id?: string) {
228
+ // base table
229
+ const table: Table = {
230
+ name: title,
231
+ primary: [GOOGLE_SHEETS_PRIMARY_KEY],
232
+ schema: {},
233
+ }
234
+ if (id) {
235
+ table._id = id
236
+ }
237
+ // build schema from headers
238
+ for (let header of headerValues) {
239
+ table.schema[header] = {
240
+ name: header,
241
+ type: FieldType.STRING,
242
+ }
243
+ }
244
+ return table
245
+ }
246
+
247
+ async buildSchema(datasourceId: string, entities: Record<string, Table>) {
209
248
  await this.connect()
210
249
  const sheets = this.client.sheetsByIndex
211
250
  const tables: Record<string, Table> = {}
212
251
  for (let sheet of sheets) {
213
252
  // must fetch rows to determine schema
214
253
  await sheet.getRows()
215
- // build schema
216
- const schema: TableSchema = {}
217
-
218
- // build schema from headers
219
- for (let header of sheet.headerValues) {
220
- schema[header] = {
221
- name: header,
222
- type: FieldTypes.STRING,
223
- }
224
- }
225
254
 
226
- // create tables
227
- tables[sheet.title] = {
228
- _id: buildExternalTableId(datasourceId, sheet.title),
229
- name: sheet.title,
230
- primary: ["rowNumber"],
231
- schema,
232
- }
255
+ const id = buildExternalTableId(datasourceId, sheet.title)
256
+ tables[sheet.title] = this.getTableSchema(
257
+ sheet.title,
258
+ sheet.headerValues,
259
+ id
260
+ )
233
261
  }
234
-
235
- this.tables = tables
262
+ const final = finaliseExternalTables(tables, entities)
263
+ this.tables = final.tables
264
+ this.schemaErrors = final.errors
236
265
  }
237
266
 
238
267
  async query(json: QueryJson) {
239
268
  const sheet = json.endpoint.entityId
240
-
241
- const handlers = {
242
- [DataSourceOperation.CREATE]: () =>
243
- this.create({ sheet, row: json.body }),
244
- [DataSourceOperation.READ]: () => this.read({ ...json, sheet }),
245
- [DataSourceOperation.UPDATE]: () =>
246
- this.update({
269
+ switch (json.endpoint.operation) {
270
+ case Operation.CREATE:
271
+ return this.create({ sheet, row: json.body as Row })
272
+ case Operation.BULK_CREATE:
273
+ return this.createBulk({ sheet, rows: json.body as Row[] })
274
+ case Operation.READ:
275
+ return this.read({ ...json, sheet })
276
+ case Operation.UPDATE:
277
+ return this.update({
247
278
  // exclude the header row and zero index
248
279
  rowIndex: json.extra?.idFilter?.equal?.rowNumber - 2,
249
280
  sheet,
250
281
  row: json.body,
251
- }),
252
- [DataSourceOperation.DELETE]: () =>
253
- this.delete({
282
+ })
283
+ case Operation.DELETE:
284
+ return this.delete({
254
285
  // exclude the header row and zero index
255
286
  rowIndex: json.extra?.idFilter?.equal?.rowNumber - 2,
256
287
  sheet,
257
- }),
258
- [DataSourceOperation.CREATE_TABLE]: () =>
259
- this.createTable(json?.table?.name),
260
- [DataSourceOperation.UPDATE_TABLE]: () => this.updateTable(json.table),
261
- [DataSourceOperation.DELETE_TABLE]: () =>
262
- this.deleteTable(json?.table?.name),
288
+ })
289
+ case Operation.CREATE_TABLE:
290
+ return this.createTable(json?.table?.name)
291
+ case Operation.UPDATE_TABLE:
292
+ return this.updateTable(json.table!)
293
+ case Operation.DELETE_TABLE:
294
+ return this.deleteTable(json?.table?.name)
295
+ default:
296
+ throw new Error(
297
+ `GSheets integration does not support "${json.endpoint.operation}".`
298
+ )
263
299
  }
264
-
265
- // @ts-ignore
266
- const internalQueryMethod = handlers[json.endpoint.operation]
267
-
268
- return await internalQueryMethod()
269
300
  }
270
301
 
271
302
  buildRowObject(headers: string[], values: string[], rowNumber: number) {
@@ -278,47 +309,70 @@ class GoogleSheetsIntegration implements DatasourcePlus {
278
309
  }
279
310
 
280
311
  async createTable(name?: string) {
312
+ if (!name) {
313
+ throw new Error("Must provide name for new sheet.")
314
+ }
281
315
  try {
282
316
  await this.connect()
283
- return await this.client.addSheet({ title: name, headerValues: ["test"] })
317
+ return await this.client.addSheet({ title: name, headerValues: [name] })
284
318
  } catch (err) {
285
319
  console.error("Error creating new table in google sheets", err)
286
320
  throw err
287
321
  }
288
322
  }
289
323
 
290
- async updateTable(table?: any) {
291
- try {
292
- await this.connect()
293
- const sheet = this.client.sheetsByTitle[table.name]
294
- await sheet.loadHeaderRow()
295
-
296
- if (table._rename) {
297
- const headers = []
298
- for (let header of sheet.headerValues) {
299
- if (header === table._rename.old) {
300
- headers.push(table._rename.updated)
301
- } else {
302
- headers.push(header)
303
- }
324
+ async updateTable(table: TableRequest) {
325
+ await this.connect()
326
+ const sheet = this.client.sheetsByTitle[table.name]
327
+ await sheet.loadHeaderRow()
328
+
329
+ if (table._rename) {
330
+ const headers = []
331
+ for (let header of sheet.headerValues) {
332
+ if (header === table._rename.old) {
333
+ headers.push(table._rename.updated)
334
+ } else {
335
+ headers.push(header)
304
336
  }
337
+ }
338
+ try {
305
339
  await sheet.setHeaderRow(headers)
306
- } else {
307
- const updatedHeaderValues = [...sheet.headerValues]
308
-
309
- const newField = Object.keys(table.schema).find(
310
- key => !sheet.headerValues.includes(key)
311
- )
340
+ } catch (err) {
341
+ console.error("Error updating column name in google sheets", err)
342
+ throw err
343
+ }
344
+ } else {
345
+ const updatedHeaderValues = [...sheet.headerValues]
346
+
347
+ // add new column - doesn't currently exist
348
+ for (let [key, column] of Object.entries(table.schema)) {
349
+ if (!ALLOWED_TYPES.includes(column.type)) {
350
+ throw new Error(
351
+ `Column type: ${column.type} not allowed for GSheets integration.`
352
+ )
353
+ }
354
+ if (
355
+ !sheet.headerValues.includes(key) &&
356
+ column.type !== FieldType.FORMULA
357
+ ) {
358
+ updatedHeaderValues.push(key)
359
+ }
360
+ }
312
361
 
313
- if (newField) {
314
- updatedHeaderValues.push(newField)
362
+ // clear out deleted columns
363
+ for (let key of sheet.headerValues) {
364
+ if (!Object.keys(table.schema).includes(key)) {
365
+ const idx = updatedHeaderValues.indexOf(key)
366
+ updatedHeaderValues.splice(idx, 1)
315
367
  }
368
+ }
316
369
 
370
+ try {
317
371
  await sheet.setHeaderRow(updatedHeaderValues)
372
+ } catch (err) {
373
+ console.error("Error updating table in google sheets", err)
374
+ throw err
318
375
  }
319
- } catch (err) {
320
- console.error("Error updating table in google sheets", err)
321
- throw err
322
376
  }
323
377
  }
324
378
 
@@ -349,6 +403,24 @@ class GoogleSheetsIntegration implements DatasourcePlus {
349
403
  }
350
404
  }
351
405
 
406
+ async createBulk(query: { sheet: string; rows: any[] }) {
407
+ try {
408
+ await this.connect()
409
+ const sheet = this.client.sheetsByTitle[query.sheet]
410
+ let rowsToInsert = []
411
+ for (let row of query.rows) {
412
+ rowsToInsert.push(typeof row === "string" ? JSON.parse(row) : row)
413
+ }
414
+ const rows = await sheet.addRows(rowsToInsert)
415
+ return rows.map(row =>
416
+ this.buildRowObject(sheet.headerValues, row._rawData, row._rowNumber)
417
+ )
418
+ } catch (err) {
419
+ console.error("Error bulk writing to google sheets", err)
420
+ throw err
421
+ }
422
+ }
423
+
352
424
  async read(query: {
353
425
  sheet: string
354
426
  filters?: SearchFilters
@@ -4,6 +4,7 @@ import { FieldTypes, BuildSchemaErrors, InvalidColumns } from "../constants"
4
4
 
5
5
  const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
6
6
  const ROW_ID_REGEX = /^\[.*]$/g
7
+ const ENCODED_SPACE = encodeURIComponent(" ")
7
8
 
8
9
  const SQL_NUMBER_TYPE_MAP = {
9
10
  integer: FieldTypes.NUMBER,
@@ -79,6 +80,10 @@ export function isExternalTable(tableId: string) {
79
80
  }
80
81
 
81
82
  export function buildExternalTableId(datasourceId: string, tableName: string) {
83
+ // encode spaces
84
+ if (tableName.includes(" ")) {
85
+ tableName = encodeURIComponent(tableName)
86
+ }
82
87
  return `${datasourceId}${DOUBLE_SEPARATOR}${tableName}`
83
88
  }
84
89
 
@@ -90,6 +95,10 @@ export function breakExternalTableId(tableId: string | undefined) {
90
95
  let datasourceId = parts.shift()
91
96
  // if they need joined
92
97
  let tableName = parts.join(DOUBLE_SEPARATOR)
98
+ // if contains encoded spaces, decode it
99
+ if (tableName.includes(ENCODED_SPACE)) {
100
+ tableName = decodeURIComponent(tableName)
101
+ }
93
102
  return { datasourceId, tableName }
94
103
  }
95
104
 
@@ -200,9 +209,9 @@ export function isIsoDateString(str: string) {
200
209
  * @param column The column to check, to see if it is a valid relationship.
201
210
  * @param tableIds The IDs of the tables which currently exist.
202
211
  */
203
- function shouldCopyRelationship(
212
+ export function shouldCopyRelationship(
204
213
  column: { type: string; tableId?: string },
205
- tableIds: [string]
214
+ tableIds: string[]
206
215
  ) {
207
216
  return (
208
217
  column.type === FieldTypes.LINK &&
@@ -219,7 +228,7 @@ function shouldCopyRelationship(
219
228
  * @param column The column to check for options or boolean type.
220
229
  * @param fetchedColumn The fetched column to check for the type in the external database.
221
230
  */
222
- function shouldCopySpecialColumn(
231
+ export function shouldCopySpecialColumn(
223
232
  column: { type: string },
224
233
  fetchedColumn: { type: string } | undefined
225
234
  ) {
@@ -257,9 +266,12 @@ function copyExistingPropsOver(
257
266
  tableIds: [string]
258
267
  ) {
259
268
  if (entities && entities[tableName]) {
260
- if (entities[tableName].primaryDisplay) {
269
+ if (entities[tableName]?.primaryDisplay) {
261
270
  table.primaryDisplay = entities[tableName].primaryDisplay
262
271
  }
272
+ if (entities[tableName]?.created) {
273
+ table.created = entities[tableName]?.created
274
+ }
263
275
  const existingTableSchema = entities[tableName].schema
264
276
  for (let key in existingTableSchema) {
265
277
  if (!existingTableSchema.hasOwnProperty(key)) {