forked from tsai/budibase
committed by
GitHub
29 changed files with 985 additions and 58 deletions
@ -0,0 +1,76 @@ |
|||||
|
const { getScopedConfig } = require("../../../db/utils") |
||||
|
const { getGlobalDB } = require("../../../tenancy") |
||||
|
const google = require("../google") |
||||
|
const { Configs, Cookies } = require("../../../constants") |
||||
|
const { clearCookie, getCookie } = require("../../../utils") |
||||
|
const { getDB } = require("../../../db") |
||||
|
|
||||
|
async function preAuth(passport, ctx, next) { |
||||
|
const db = getGlobalDB() |
||||
|
// get the relevant config
|
||||
|
const config = await getScopedConfig(db, { |
||||
|
type: Configs.GOOGLE, |
||||
|
workspace: ctx.query.workspace, |
||||
|
}) |
||||
|
const publicConfig = await getScopedConfig(db, { |
||||
|
type: Configs.SETTINGS, |
||||
|
}) |
||||
|
let callbackUrl = `${publicConfig.platformUrl}/api/global/auth/datasource/google/callback` |
||||
|
const strategy = await google.strategyFactory(config, callbackUrl) |
||||
|
|
||||
|
if (!ctx.query.appId || !ctx.query.datasourceId) { |
||||
|
ctx.throw(400, "appId and datasourceId query params not present.") |
||||
|
} |
||||
|
|
||||
|
return passport.authenticate(strategy, { |
||||
|
scope: ["profile", "email", "https://www.googleapis.com/auth/spreadsheets"], |
||||
|
accessType: "offline", |
||||
|
prompt: "consent", |
||||
|
})(ctx, next) |
||||
|
} |
||||
|
|
||||
|
async function postAuth(passport, ctx, next) { |
||||
|
const db = getGlobalDB() |
||||
|
|
||||
|
const config = await getScopedConfig(db, { |
||||
|
type: Configs.GOOGLE, |
||||
|
workspace: ctx.query.workspace, |
||||
|
}) |
||||
|
|
||||
|
const publicConfig = await getScopedConfig(db, { |
||||
|
type: Configs.SETTINGS, |
||||
|
}) |
||||
|
|
||||
|
let callbackUrl = `${publicConfig.platformUrl}/api/global/auth/datasource/google/callback` |
||||
|
const strategy = await google.strategyFactory( |
||||
|
config, |
||||
|
callbackUrl, |
||||
|
(accessToken, refreshToken, profile, done) => { |
||||
|
clearCookie(ctx, Cookies.DatasourceAuth) |
||||
|
done(null, { accessToken, refreshToken }) |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
const authStateCookie = getCookie(ctx, Cookies.DatasourceAuth) |
||||
|
|
||||
|
return passport.authenticate( |
||||
|
strategy, |
||||
|
{ successRedirect: "/", failureRedirect: "/error" }, |
||||
|
async (err, tokens) => { |
||||
|
// update the DB for the datasource with all the user info
|
||||
|
const db = getDB(authStateCookie.appId) |
||||
|
const datasource = await db.get(authStateCookie.datasourceId) |
||||
|
if (!datasource.config) { |
||||
|
datasource.config = {} |
||||
|
} |
||||
|
datasource.config.auth = { type: "google", ...tokens } |
||||
|
await db.put(datasource) |
||||
|
ctx.redirect( |
||||
|
`/builder/app/${authStateCookie.appId}/data/datasource/${authStateCookie.datasourceId}` |
||||
|
) |
||||
|
} |
||||
|
)(ctx, next) |
||||
|
} |
||||
|
|
||||
|
exports.preAuth = preAuth |
||||
|
exports.postAuth = postAuth |
||||
@ -0,0 +1,47 @@ |
|||||
|
<script> |
||||
|
import { ActionButton } from "@budibase/bbui" |
||||
|
import GoogleLogo from "assets/google-logo.png" |
||||
|
import { store } from "builderStore" |
||||
|
import { auth } from "stores/portal" |
||||
|
|
||||
|
export let preAuthStep |
||||
|
export let datasource |
||||
|
|
||||
|
$: tenantId = $auth.tenantId |
||||
|
</script> |
||||
|
|
||||
|
<ActionButton |
||||
|
on:click={async () => { |
||||
|
let ds = datasource |
||||
|
if (!ds) { |
||||
|
ds = await preAuthStep() |
||||
|
} |
||||
|
window.open( |
||||
|
`/api/global/auth/${tenantId}/datasource/google?datasourceId=${datasource._id}&appId=${$store.appId}`, |
||||
|
"_blank" |
||||
|
) |
||||
|
}} |
||||
|
> |
||||
|
<div class="inner"> |
||||
|
<img src={GoogleLogo} alt="google icon" /> |
||||
|
<p>Sign in with Google</p> |
||||
|
</div> |
||||
|
</ActionButton> |
||||
|
|
||||
|
<style> |
||||
|
.inner { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
padding-top: var(--spacing-xs); |
||||
|
padding-bottom: var(--spacing-xs); |
||||
|
} |
||||
|
.inner img { |
||||
|
width: 18px; |
||||
|
margin: 3px 10px 3px 3px; |
||||
|
} |
||||
|
.inner p { |
||||
|
margin: 0; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,184 @@ |
|||||
|
<script> |
||||
|
export let width = "100" |
||||
|
export let height = "100" |
||||
|
</script> |
||||
|
|
||||
|
<svg |
||||
|
{width} |
||||
|
{height} |
||||
|
version="1.0" |
||||
|
xmlns="http://www.w3.org/2000/svg" |
||||
|
viewBox="0 0 50 80" |
||||
|
preserveAspectRatio="xMidYMid meet" |
||||
|
> |
||||
|
<defs> |
||||
|
<path |
||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
||||
|
id="path-1" |
||||
|
/> |
||||
|
<path |
||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
||||
|
id="path-3" |
||||
|
/> |
||||
|
<path |
||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
||||
|
id="path-5" |
||||
|
/> |
||||
|
<linearGradient |
||||
|
x1="50.0053945%" |
||||
|
y1="8.58610612%" |
||||
|
x2="50.0053945%" |
||||
|
y2="100.013939%" |
||||
|
id="linearGradient-7" |
||||
|
> |
||||
|
<stop stop-color="#263238" stop-opacity="0.2" offset="0%" /> |
||||
|
<stop stop-color="#263238" stop-opacity="0.02" offset="100%" /> |
||||
|
</linearGradient> |
||||
|
<path |
||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
||||
|
id="path-8" |
||||
|
/> |
||||
|
<path |
||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
||||
|
id="path-10" |
||||
|
/> |
||||
|
<path |
||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
||||
|
id="path-12" |
||||
|
/> |
||||
|
<path |
||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
||||
|
id="path-14" |
||||
|
/> |
||||
|
<radialGradient |
||||
|
cx="3.16804688%" |
||||
|
cy="2.71744318%" |
||||
|
fx="3.16804688%" |
||||
|
fy="2.71744318%" |
||||
|
r="161.248516%" |
||||
|
gradientTransform="translate(0.031680,0.027174),scale(1.000000,0.727273),translate(-0.031680,-0.027174)" |
||||
|
id="radialGradient-16" |
||||
|
> |
||||
|
<stop stop-color="#FFFFFF" stop-opacity="0.1" offset="0%" /> |
||||
|
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%" /> |
||||
|
</radialGradient> |
||||
|
</defs> |
||||
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
||||
|
<g |
||||
|
id="Consumer-Apps-Sheets-Large-VD-R8-" |
||||
|
transform="translate(-451.000000, -451.000000)" |
||||
|
> |
||||
|
<g id="Hero" transform="translate(0.000000, 63.000000)"> |
||||
|
<g id="Personal" transform="translate(277.000000, 299.000000)"> |
||||
|
<g id="Sheets-icon" transform="translate(174.833333, 89.958333)"> |
||||
|
<g id="Group"> |
||||
|
<g id="Clipped"> |
||||
|
<mask id="mask-2" fill="white"> |
||||
|
<use xlink:href="#path-1" /> |
||||
|
</mask> |
||||
|
<g id="SVGID_1_" /> |
||||
|
<path |
||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L36.9791667,10.3541667 L29.5833333,0 Z" |
||||
|
id="Path" |
||||
|
fill="#0F9D58" |
||||
|
fill-rule="nonzero" |
||||
|
mask="url(#mask-2)" |
||||
|
/> |
||||
|
</g> |
||||
|
<g id="Clipped"> |
||||
|
<mask id="mask-4" fill="white"> |
||||
|
<use xlink:href="#path-3" /> |
||||
|
</mask> |
||||
|
<g id="SVGID_1_" /> |
||||
|
<path |
||||
|
d="M11.8333333,31.8020833 L11.8333333,53.25 L35.5,53.25 L35.5,31.8020833 L11.8333333,31.8020833 Z M22.1875,50.2916667 L14.7916667,50.2916667 L14.7916667,46.59375 L22.1875,46.59375 L22.1875,50.2916667 Z M22.1875,44.375 L14.7916667,44.375 L14.7916667,40.6770833 L22.1875,40.6770833 L22.1875,44.375 Z M22.1875,38.4583333 L14.7916667,38.4583333 L14.7916667,34.7604167 L22.1875,34.7604167 L22.1875,38.4583333 Z M32.5416667,50.2916667 L25.1458333,50.2916667 L25.1458333,46.59375 L32.5416667,46.59375 L32.5416667,50.2916667 Z M32.5416667,44.375 L25.1458333,44.375 L25.1458333,40.6770833 L32.5416667,40.6770833 L32.5416667,44.375 Z M32.5416667,38.4583333 L25.1458333,38.4583333 L25.1458333,34.7604167 L32.5416667,34.7604167 L32.5416667,38.4583333 Z" |
||||
|
id="Shape" |
||||
|
fill="#F1F1F1" |
||||
|
fill-rule="nonzero" |
||||
|
mask="url(#mask-4)" |
||||
|
/> |
||||
|
</g> |
||||
|
<g id="Clipped"> |
||||
|
<mask id="mask-6" fill="white"> |
||||
|
<use xlink:href="#path-5" /> |
||||
|
</mask> |
||||
|
<g id="SVGID_1_" /> |
||||
|
<polygon |
||||
|
id="Path" |
||||
|
fill="url(#linearGradient-7)" |
||||
|
fill-rule="nonzero" |
||||
|
mask="url(#mask-6)" |
||||
|
points="30.8813021 16.4520313 47.3333333 32.9003646 47.3333333 17.75" |
||||
|
/> |
||||
|
</g> |
||||
|
<g id="Clipped"> |
||||
|
<mask id="mask-9" fill="white"> |
||||
|
<use xlink:href="#path-8" /> |
||||
|
</mask> |
||||
|
<g id="SVGID_1_" /> |
||||
|
<g id="Group" mask="url(#mask-9)"> |
||||
|
<g transform="translate(26.625000, -2.958333)"> |
||||
|
<path |
||||
|
d="M2.95833333,2.95833333 L2.95833333,16.2708333 C2.95833333,18.7225521 4.94411458,20.7083333 7.39583333,20.7083333 L20.7083333,20.7083333 L2.95833333,2.95833333 Z" |
||||
|
id="Path" |
||||
|
fill="#87CEAC" |
||||
|
fill-rule="nonzero" |
||||
|
/> |
||||
|
</g> |
||||
|
</g> |
||||
|
</g> |
||||
|
<g id="Clipped"> |
||||
|
<mask id="mask-11" fill="white"> |
||||
|
<use xlink:href="#path-10" /> |
||||
|
</mask> |
||||
|
<g id="SVGID_1_" /> |
||||
|
<path |
||||
|
d="M4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,4.80729167 C0,2.36666667 1.996875,0.369791667 4.4375,0.369791667 L29.5833333,0.369791667 L29.5833333,0 L4.4375,0 Z" |
||||
|
id="Path" |
||||
|
fill-opacity="0.2" |
||||
|
fill="#FFFFFF" |
||||
|
fill-rule="nonzero" |
||||
|
mask="url(#mask-11)" |
||||
|
/> |
||||
|
</g> |
||||
|
<g id="Clipped"> |
||||
|
<mask id="mask-13" fill="white"> |
||||
|
<use xlink:href="#path-12" /> |
||||
|
</mask> |
||||
|
<g id="SVGID_1_" /> |
||||
|
<path |
||||
|
d="M42.8958333,64.7135417 L4.4375,64.7135417 C1.996875,64.7135417 0,62.7166667 0,60.2760417 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,60.2760417 C47.3333333,62.7166667 45.3364583,64.7135417 42.8958333,64.7135417 Z" |
||||
|
id="Path" |
||||
|
fill-opacity="0.2" |
||||
|
fill="#263238" |
||||
|
fill-rule="nonzero" |
||||
|
mask="url(#mask-13)" |
||||
|
/> |
||||
|
</g> |
||||
|
<g id="Clipped"> |
||||
|
<mask id="mask-15" fill="white"> |
||||
|
<use xlink:href="#path-14" /> |
||||
|
</mask> |
||||
|
<g id="SVGID_1_" /> |
||||
|
<path |
||||
|
d="M34.0208333,17.75 C31.5691146,17.75 29.5833333,15.7642188 29.5833333,13.3125 L29.5833333,13.6822917 C29.5833333,16.1340104 31.5691146,18.1197917 34.0208333,18.1197917 L47.3333333,18.1197917 L47.3333333,17.75 L34.0208333,17.75 Z" |
||||
|
id="Path" |
||||
|
fill-opacity="0.1" |
||||
|
fill="#263238" |
||||
|
fill-rule="nonzero" |
||||
|
mask="url(#mask-15)" |
||||
|
/> |
||||
|
</g> |
||||
|
</g> |
||||
|
<path |
||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
||||
|
id="Path" |
||||
|
fill="url(#radialGradient-16)" |
||||
|
fill-rule="nonzero" |
||||
|
/> |
||||
|
</g> |
||||
|
</g> |
||||
|
</g> |
||||
|
</g> |
||||
|
</g> |
||||
|
</svg> |
||||
@ -0,0 +1,29 @@ |
|||||
|
<script> |
||||
|
import { ModalContent, Body, Layout } from "@budibase/bbui" |
||||
|
import { IntegrationNames } from "constants/backend" |
||||
|
import cloneDeep from "lodash/cloneDeepWith" |
||||
|
import GoogleButton from "../_components/GoogleButton.svelte" |
||||
|
import { saveDatasource as save } from "builderStore/datasource" |
||||
|
|
||||
|
export let integration |
||||
|
export let modal |
||||
|
|
||||
|
// kill the reference so the input isn't saved |
||||
|
let datasource = cloneDeep(integration) |
||||
|
</script> |
||||
|
|
||||
|
<ModalContent |
||||
|
title={`Connect to ${IntegrationNames[datasource.type]}`} |
||||
|
onCancel={() => modal.show()} |
||||
|
cancelText="Back" |
||||
|
size="L" |
||||
|
> |
||||
|
<Layout noPadding> |
||||
|
<Body size="XS" |
||||
|
>Authenticate with your google account to use the {IntegrationNames[ |
||||
|
datasource.type |
||||
|
]} integration.</Body |
||||
|
> |
||||
|
</Layout> |
||||
|
<GoogleButton preAuthStep={() => save(datasource, true)} /> |
||||
|
</ModalContent> |
||||
@ -0,0 +1,353 @@ |
|||||
|
import { |
||||
|
DatasourceFieldTypes, |
||||
|
Integration, |
||||
|
QueryJson, |
||||
|
QueryTypes, |
||||
|
} from "../definitions/datasource" |
||||
|
import { OAuth2Client } from "google-auth-library" |
||||
|
import { DatasourcePlus } from "./base/datasourcePlus" |
||||
|
import { Row, Table, TableSchema } from "../definitions/common" |
||||
|
import { buildExternalTableId } from "./utils" |
||||
|
import { DataSourceOperation, FieldTypes } from "../constants" |
||||
|
import { GoogleSpreadsheet } from "google-spreadsheet" |
||||
|
import { table } from "console" |
||||
|
|
||||
|
module GoogleSheetsModule { |
||||
|
const { getGlobalDB } = require("@budibase/backend-core/tenancy") |
||||
|
const { getScopedConfig } = require("@budibase/backend-core/db") |
||||
|
const { Configs } = require("@budibase/backend-core/constants") |
||||
|
|
||||
|
interface GoogleSheetsConfig { |
||||
|
spreadsheetId: string |
||||
|
auth: OAuthClientConfig |
||||
|
} |
||||
|
|
||||
|
interface OAuthClientConfig { |
||||
|
appId: string |
||||
|
accessToken: string |
||||
|
refreshToken: string |
||||
|
} |
||||
|
|
||||
|
const SCHEMA: Integration = { |
||||
|
plus: true, |
||||
|
auth: { |
||||
|
type: "google", |
||||
|
}, |
||||
|
relationships: false, |
||||
|
docs: "https://developers.google.com/sheets/api/quickstart/nodejs", |
||||
|
description: |
||||
|
"Create and collaborate on online spreadsheets in real-time and from any device. ", |
||||
|
friendlyName: "Google Sheets", |
||||
|
datasource: { |
||||
|
spreadsheetId: { |
||||
|
type: DatasourceFieldTypes.STRING, |
||||
|
required: true, |
||||
|
}, |
||||
|
}, |
||||
|
query: { |
||||
|
create: { |
||||
|
type: QueryTypes.FIELDS, |
||||
|
fields: { |
||||
|
sheet: { |
||||
|
type: DatasourceFieldTypes.STRING, |
||||
|
required: true, |
||||
|
}, |
||||
|
row: { |
||||
|
type: QueryTypes.JSON, |
||||
|
required: true, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
read: { |
||||
|
type: QueryTypes.FIELDS, |
||||
|
fields: { |
||||
|
sheet: { |
||||
|
type: DatasourceFieldTypes.STRING, |
||||
|
required: true, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
update: { |
||||
|
type: QueryTypes.FIELDS, |
||||
|
fields: { |
||||
|
sheet: { |
||||
|
type: DatasourceFieldTypes.STRING, |
||||
|
required: true, |
||||
|
}, |
||||
|
rowIndex: { |
||||
|
type: DatasourceFieldTypes.STRING, |
||||
|
required: true, |
||||
|
}, |
||||
|
row: { |
||||
|
type: QueryTypes.JSON, |
||||
|
required: true, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
delete: { |
||||
|
type: QueryTypes.FIELDS, |
||||
|
fields: { |
||||
|
sheet: { |
||||
|
type: DatasourceFieldTypes.STRING, |
||||
|
required: true, |
||||
|
}, |
||||
|
rowIndex: { |
||||
|
type: DatasourceFieldTypes.NUMBER, |
||||
|
required: true, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
class GoogleSheetsIntegration implements DatasourcePlus { |
||||
|
private readonly config: GoogleSheetsConfig |
||||
|
private client: any |
||||
|
public tables: Record<string, Table> = {} |
||||
|
public schemaErrors: Record<string, string> = {} |
||||
|
|
||||
|
constructor(config: GoogleSheetsConfig) { |
||||
|
this.config = config |
||||
|
const spreadsheetId = this.cleanSpreadsheetUrl(this.config.spreadsheetId) |
||||
|
this.client = new GoogleSpreadsheet(spreadsheetId) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Pull the spreadsheet ID out from a valid google sheets URL |
||||
|
* @param spreadsheetId - the URL or standard spreadsheetId of the google sheet |
||||
|
* @returns spreadsheet Id of the google sheet |
||||
|
*/ |
||||
|
cleanSpreadsheetUrl(spreadsheetId: string) { |
||||
|
if (!spreadsheetId) { |
||||
|
throw new Error( |
||||
|
"You must set a spreadsheet ID in your configuration to fetch tables." |
||||
|
) |
||||
|
} |
||||
|
const parts = spreadsheetId.split("/") |
||||
|
return parts.length > 5 ? parts[5] : spreadsheetId |
||||
|
} |
||||
|
|
||||
|
async connect() { |
||||
|
try { |
||||
|
// Initialise oAuth client
|
||||
|
const db = getGlobalDB() |
||||
|
const googleConfig = await getScopedConfig(db, { |
||||
|
type: Configs.GOOGLE, |
||||
|
}) |
||||
|
const oauthClient = new OAuth2Client({ |
||||
|
clientId: googleConfig.clientID, |
||||
|
clientSecret: googleConfig.clientSecret, |
||||
|
}) |
||||
|
oauthClient.credentials.access_token = this.config.auth.accessToken |
||||
|
oauthClient.credentials.refresh_token = this.config.auth.refreshToken |
||||
|
this.client.useOAuth2Client(oauthClient) |
||||
|
await this.client.loadInfo() |
||||
|
} catch (err) { |
||||
|
console.error("Error connecting to google sheets", err) |
||||
|
throw err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async buildSchema(datasourceId: string) { |
||||
|
await this.connect() |
||||
|
const sheets = await this.client.sheetsByIndex |
||||
|
const tables: Record<string, Table> = {} |
||||
|
for (let sheet of sheets) { |
||||
|
// must fetch rows to determine schema
|
||||
|
await sheet.getRows() |
||||
|
// build schema
|
||||
|
const schema: TableSchema = {} |
||||
|
|
||||
|
// build schema from headers
|
||||
|
for (let header of sheet.headerValues) { |
||||
|
schema[header] = { |
||||
|
name: header, |
||||
|
type: FieldTypes.STRING, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// create tables
|
||||
|
tables[sheet.title] = { |
||||
|
_id: buildExternalTableId(datasourceId, sheet.title), |
||||
|
name: sheet.title, |
||||
|
primary: ["rowNumber"], |
||||
|
schema, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.tables = tables |
||||
|
} |
||||
|
|
||||
|
async query(json: QueryJson) { |
||||
|
const sheet = json.endpoint.entityId |
||||
|
|
||||
|
const handlers = { |
||||
|
[DataSourceOperation.CREATE]: () => |
||||
|
this.create({ sheet, row: json.body }), |
||||
|
[DataSourceOperation.READ]: () => this.read({ sheet }), |
||||
|
[DataSourceOperation.UPDATE]: () => |
||||
|
this.update({ |
||||
|
// exclude the header row and zero index
|
||||
|
rowIndex: json.extra?.idFilter?.equal?.rowNumber - 2, |
||||
|
sheet, |
||||
|
row: json.body, |
||||
|
}), |
||||
|
[DataSourceOperation.DELETE]: () => |
||||
|
this.delete({ |
||||
|
// exclude the header row and zero index
|
||||
|
rowIndex: json.extra?.idFilter?.equal?.rowNumber - 2, |
||||
|
sheet, |
||||
|
}), |
||||
|
[DataSourceOperation.CREATE_TABLE]: () => |
||||
|
this.createTable(json?.table?.name), |
||||
|
[DataSourceOperation.UPDATE_TABLE]: () => this.updateTable(json.table), |
||||
|
[DataSourceOperation.DELETE_TABLE]: () => |
||||
|
this.deleteTable(json?.table?.name), |
||||
|
} |
||||
|
|
||||
|
const internalQueryMethod = handlers[json.endpoint.operation] |
||||
|
|
||||
|
return await internalQueryMethod() |
||||
|
} |
||||
|
|
||||
|
buildRowObject(headers: string[], values: string[], rowNumber: number) { |
||||
|
const rowObject: { rowNumber: number; [key: string]: any } = { rowNumber } |
||||
|
for (let i = 0; i < headers.length; i++) { |
||||
|
rowObject._id = rowNumber |
||||
|
rowObject[headers[i]] = values[i] |
||||
|
} |
||||
|
return rowObject |
||||
|
} |
||||
|
|
||||
|
async createTable(name?: string) { |
||||
|
try { |
||||
|
await this.connect() |
||||
|
const sheet = await this.client.addSheet({ title: name }) |
||||
|
return sheet |
||||
|
} catch (err) { |
||||
|
console.error("Error creating new table in google sheets", err) |
||||
|
throw err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async updateTable(table?: any) { |
||||
|
try { |
||||
|
await this.connect() |
||||
|
const sheet = await this.client.sheetsByTitle[table.name] |
||||
|
await sheet.loadHeaderRow() |
||||
|
|
||||
|
if (table._rename) { |
||||
|
const headers = [] |
||||
|
for (let header of sheet.headerValues) { |
||||
|
if (header === table._rename.old) { |
||||
|
headers.push(table._rename.updated) |
||||
|
} else { |
||||
|
headers.push(header) |
||||
|
} |
||||
|
} |
||||
|
await sheet.setHeaderRow(headers) |
||||
|
} else { |
||||
|
let newField = Object.keys(table.schema).find( |
||||
|
key => !sheet.headerValues.includes(key) |
||||
|
) |
||||
|
await sheet.setHeaderRow([...sheet.headerValues, newField]) |
||||
|
} |
||||
|
} catch (err) { |
||||
|
console.error("Error updating table in google sheets", err) |
||||
|
throw err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async deleteTable(sheet: any) { |
||||
|
try { |
||||
|
await this.connect() |
||||
|
const sheetToDelete = await this.client.sheetsByTitle[sheet] |
||||
|
return await sheetToDelete.delete() |
||||
|
} catch (err) { |
||||
|
console.error("Error deleting table in google sheets", err) |
||||
|
throw err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async create(query: { sheet: string; row: any }) { |
||||
|
try { |
||||
|
await this.connect() |
||||
|
const sheet = await this.client.sheetsByTitle[query.sheet] |
||||
|
const rowToInsert = |
||||
|
typeof query.row === "string" ? JSON.parse(query.row) : query.row |
||||
|
const row = await sheet.addRow(rowToInsert) |
||||
|
return [ |
||||
|
this.buildRowObject(sheet.headerValues, row._rawData, row._rowNumber), |
||||
|
] |
||||
|
} catch (err) { |
||||
|
console.error("Error writing to google sheets", err) |
||||
|
throw err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async read(query: { sheet: string }) { |
||||
|
try { |
||||
|
await this.connect() |
||||
|
const sheet = await this.client.sheetsByTitle[query.sheet] |
||||
|
const rows = await sheet.getRows() |
||||
|
const headerValues = sheet.headerValues |
||||
|
const response = [] |
||||
|
for (let row of rows) { |
||||
|
response.push( |
||||
|
this.buildRowObject(headerValues, row._rawData, row._rowNumber) |
||||
|
) |
||||
|
} |
||||
|
return response |
||||
|
} catch (err) { |
||||
|
console.error("Error reading from google sheets", err) |
||||
|
throw err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async update(query: { sheet: string; rowIndex: number; row: any }) { |
||||
|
try { |
||||
|
await this.connect() |
||||
|
const sheet = await this.client.sheetsByTitle[query.sheet] |
||||
|
const rows = await sheet.getRows() |
||||
|
const row = rows[query.rowIndex] |
||||
|
if (row) { |
||||
|
const updateValues = query.row |
||||
|
for (let key in updateValues) { |
||||
|
row[key] = updateValues[key] |
||||
|
} |
||||
|
await row.save() |
||||
|
return [ |
||||
|
this.buildRowObject( |
||||
|
sheet.headerValues, |
||||
|
row._rawData, |
||||
|
row._rowNumber |
||||
|
), |
||||
|
] |
||||
|
} else { |
||||
|
throw new Error("Row does not exist.") |
||||
|
} |
||||
|
} catch (err) { |
||||
|
console.error("Error reading from google sheets", err) |
||||
|
throw err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async delete(query: { sheet: string; rowIndex: number }) { |
||||
|
await this.connect() |
||||
|
const sheet = await this.client.sheetsByTitle[query.sheet] |
||||
|
const rows = await sheet.getRows() |
||||
|
const row = rows[query.rowIndex] |
||||
|
if (row) { |
||||
|
await row.delete() |
||||
|
return [{ deleted: query.rowIndex }] |
||||
|
} else { |
||||
|
throw new Error("Row does not exist.") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
schema: SCHEMA, |
||||
|
integration: GoogleSheetsIntegration, |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue