Merge remote-tracking branch 'origin/develop' into feature/component-condition-count
This commit is contained in:
commit
24eff350a3
|
@ -3,5 +3,17 @@
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": true
|
"source.fixAll": true
|
||||||
},
|
},
|
||||||
"editor.defaultFormatter": "svelte.svelte-vscode"
|
"editor.defaultFormatter": "svelte.svelte-vscode",
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "vscode.json-language-features"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"debug.javascript.terminalOptions": {
|
||||||
|
"skipFiles": [
|
||||||
|
"${workspaceFolder}/packages/backend-core/node_modules/**",
|
||||||
|
"<node_internals>/**"
|
||||||
|
]
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.212-alpha.12",
|
"version": "1.0.212-alpha.14",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "1.0.212-alpha.12",
|
"version": "1.0.212-alpha.14",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/src/index.js",
|
||||||
"types": "dist/src/index.d.ts",
|
"types": "dist/src/index.d.ts",
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
"test:watch": "jest --watchAll"
|
"test:watch": "jest --watchAll"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/types": "^1.0.212-alpha.12",
|
"@budibase/types": "^1.0.212-alpha.14",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
"aws-sdk": "2.1030.0",
|
"aws-sdk": "2.1030.0",
|
||||||
"bcrypt": "5.0.1",
|
"bcrypt": "5.0.1",
|
||||||
|
@ -36,6 +36,7 @@
|
||||||
"passport-google-oauth": "2.0.0",
|
"passport-google-oauth": "2.0.0",
|
||||||
"passport-jwt": "4.0.0",
|
"passport-jwt": "4.0.0",
|
||||||
"passport-local": "1.0.0",
|
"passport-local": "1.0.0",
|
||||||
|
"passport-oauth2-refresh": "^2.1.0",
|
||||||
"posthog-node": "1.3.0",
|
"posthog-node": "1.3.0",
|
||||||
"pouchdb": "7.3.0",
|
"pouchdb": "7.3.0",
|
||||||
"pouchdb-find": "7.2.2",
|
"pouchdb-find": "7.2.2",
|
||||||
|
|
|
@ -2,6 +2,9 @@ const passport = require("koa-passport")
|
||||||
const LocalStrategy = require("passport-local").Strategy
|
const LocalStrategy = require("passport-local").Strategy
|
||||||
const JwtStrategy = require("passport-jwt").Strategy
|
const JwtStrategy = require("passport-jwt").Strategy
|
||||||
const { getGlobalDB } = require("./tenancy")
|
const { getGlobalDB } = require("./tenancy")
|
||||||
|
const refresh = require("passport-oauth2-refresh")
|
||||||
|
const { Configs } = require("./constants")
|
||||||
|
const { getScopedConfig } = require("./db/utils")
|
||||||
const {
|
const {
|
||||||
jwt,
|
jwt,
|
||||||
local,
|
local,
|
||||||
|
@ -12,6 +15,7 @@ const {
|
||||||
tenancy,
|
tenancy,
|
||||||
appTenancy,
|
appTenancy,
|
||||||
authError,
|
authError,
|
||||||
|
ssoCallbackUrl,
|
||||||
csrf,
|
csrf,
|
||||||
internalApi,
|
internalApi,
|
||||||
} = require("./middleware")
|
} = require("./middleware")
|
||||||
|
@ -34,6 +38,122 @@ passport.deserializeUser(async (user, done) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function refreshOIDCAccessToken(db, chosenConfig, refreshToken) {
|
||||||
|
const callbackUrl = await oidc.getCallbackUrl(db, chosenConfig)
|
||||||
|
let enrichedConfig
|
||||||
|
let strategy
|
||||||
|
|
||||||
|
try {
|
||||||
|
enrichedConfig = await oidc.fetchStrategyConfig(chosenConfig, callbackUrl)
|
||||||
|
if (!enrichedConfig) {
|
||||||
|
throw new Error("OIDC Config contents invalid")
|
||||||
|
}
|
||||||
|
strategy = await oidc.strategyFactory(enrichedConfig)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
throw new Error("Could not refresh OAuth Token")
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh.use(strategy, {
|
||||||
|
setRefreshOAuth2() {
|
||||||
|
return strategy._getOAuth2Client(enrichedConfig)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
refresh.requestNewAccessToken(
|
||||||
|
Configs.OIDC,
|
||||||
|
refreshToken,
|
||||||
|
(err, accessToken, refreshToken, params) => {
|
||||||
|
resolve({ err, accessToken, refreshToken, params })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshGoogleAccessToken(db, config, refreshToken) {
|
||||||
|
let callbackUrl = await google.getCallbackUrl(db, config)
|
||||||
|
|
||||||
|
let strategy
|
||||||
|
try {
|
||||||
|
strategy = await google.strategyFactory(config, callbackUrl)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
throw new Error("Error constructing OIDC refresh strategy", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh.use(strategy)
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
refresh.requestNewAccessToken(
|
||||||
|
Configs.GOOGLE,
|
||||||
|
refreshToken,
|
||||||
|
(err, accessToken, refreshToken, params) => {
|
||||||
|
resolve({ err, accessToken, refreshToken, params })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshOAuthToken(refreshToken, configType, configId) {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
|
||||||
|
const config = await getScopedConfig(db, {
|
||||||
|
type: configType,
|
||||||
|
group: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
let chosenConfig = {}
|
||||||
|
let refreshResponse
|
||||||
|
if (configType === Configs.OIDC) {
|
||||||
|
// configId - retrieved from cookie.
|
||||||
|
chosenConfig = config.configs.filter(c => c.uuid === configId)[0]
|
||||||
|
if (!chosenConfig) {
|
||||||
|
throw new Error("Invalid OIDC configuration")
|
||||||
|
}
|
||||||
|
refreshResponse = await refreshOIDCAccessToken(
|
||||||
|
db,
|
||||||
|
chosenConfig,
|
||||||
|
refreshToken
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
chosenConfig = config
|
||||||
|
refreshResponse = await refreshGoogleAccessToken(
|
||||||
|
db,
|
||||||
|
chosenConfig,
|
||||||
|
refreshToken
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return refreshResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateUserOAuth(userId, oAuthConfig) {
|
||||||
|
const details = {
|
||||||
|
accessToken: oAuthConfig.accessToken,
|
||||||
|
refreshToken: oAuthConfig.refreshToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
const dbUser = await db.get(userId)
|
||||||
|
|
||||||
|
//Do not overwrite the refresh token if a valid one is not provided.
|
||||||
|
if (typeof details.refreshToken !== "string") {
|
||||||
|
delete details.refreshToken
|
||||||
|
}
|
||||||
|
|
||||||
|
dbUser.oauth2 = {
|
||||||
|
...dbUser.oauth2,
|
||||||
|
...details,
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.put(dbUser)
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not update OAuth details for current user", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
buildAuthMiddleware: authenticated,
|
buildAuthMiddleware: authenticated,
|
||||||
passport,
|
passport,
|
||||||
|
@ -46,4 +166,7 @@ module.exports = {
|
||||||
authError,
|
authError,
|
||||||
buildCsrfMiddleware: csrf,
|
buildCsrfMiddleware: csrf,
|
||||||
internalApi,
|
internalApi,
|
||||||
|
refreshOAuthToken,
|
||||||
|
updateUserOAuth,
|
||||||
|
ssoCallbackUrl,
|
||||||
}
|
}
|
||||||
|
|
|
@ -387,7 +387,9 @@ export const getScopedFullConfig = async function (
|
||||||
if (type === Configs.SETTINGS) {
|
if (type === Configs.SETTINGS) {
|
||||||
if (scopedConfig && scopedConfig.doc) {
|
if (scopedConfig && scopedConfig.doc) {
|
||||||
// overrides affected by environment variables
|
// overrides affected by environment variables
|
||||||
scopedConfig.doc.config.platformUrl = await getPlatformUrl()
|
scopedConfig.doc.config.platformUrl = await getPlatformUrl({
|
||||||
|
tenantAware: true,
|
||||||
|
})
|
||||||
scopedConfig.doc.config.analyticsEnabled =
|
scopedConfig.doc.config.analyticsEnabled =
|
||||||
await events.analytics.enabled()
|
await events.analytics.enabled()
|
||||||
} else {
|
} else {
|
||||||
|
@ -396,7 +398,7 @@ export const getScopedFullConfig = async function (
|
||||||
doc: {
|
doc: {
|
||||||
_id: generateConfigID({ type, user, workspace }),
|
_id: generateConfigID({ type, user, workspace }),
|
||||||
config: {
|
config: {
|
||||||
platformUrl: await getPlatformUrl(),
|
platformUrl: await getPlatformUrl({ tenantAware: true }),
|
||||||
analyticsEnabled: await events.analytics.enabled(),
|
analyticsEnabled: await events.analytics.enabled(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -94,7 +94,6 @@ module.exports = (
|
||||||
user = await getUser(userId, session.tenantId)
|
user = await getUser(userId, session.tenantId)
|
||||||
}
|
}
|
||||||
user.csrfToken = session.csrfToken
|
user.csrfToken = session.csrfToken
|
||||||
delete user.password
|
|
||||||
authenticated = true
|
authenticated = true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err
|
error = err
|
||||||
|
@ -128,6 +127,8 @@ module.exports = (
|
||||||
}
|
}
|
||||||
if (!user && tenantId) {
|
if (!user && tenantId) {
|
||||||
user = { tenantId }
|
user = { tenantId }
|
||||||
|
} else {
|
||||||
|
delete user.password
|
||||||
}
|
}
|
||||||
// be explicit
|
// be explicit
|
||||||
if (authenticated !== true) {
|
if (authenticated !== true) {
|
||||||
|
|
|
@ -2,7 +2,7 @@ const jwt = require("./passport/jwt")
|
||||||
const local = require("./passport/local")
|
const local = require("./passport/local")
|
||||||
const google = require("./passport/google")
|
const google = require("./passport/google")
|
||||||
const oidc = require("./passport/oidc")
|
const oidc = require("./passport/oidc")
|
||||||
const { authError } = require("./passport/utils")
|
const { authError, ssoCallbackUrl } = require("./passport/utils")
|
||||||
const authenticated = require("./authenticated")
|
const authenticated = require("./authenticated")
|
||||||
const auditLog = require("./auditLog")
|
const auditLog = require("./auditLog")
|
||||||
const tenancy = require("./tenancy")
|
const tenancy = require("./tenancy")
|
||||||
|
@ -20,6 +20,7 @@ module.exports = {
|
||||||
tenancy,
|
tenancy,
|
||||||
authError,
|
authError,
|
||||||
internalApi,
|
internalApi,
|
||||||
|
ssoCallbackUrl,
|
||||||
datasource: {
|
datasource: {
|
||||||
google: datasourceGoogle,
|
google: datasourceGoogle,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
||||||
|
const { ssoCallbackUrl } = require("./utils")
|
||||||
const { authenticateThirdParty } = require("./third-party-common")
|
const { authenticateThirdParty } = require("./third-party-common")
|
||||||
|
const { Configs } = require("../../../constants")
|
||||||
|
|
||||||
const buildVerifyFn = saveUserFn => {
|
const buildVerifyFn = saveUserFn => {
|
||||||
return (accessToken, refreshToken, profile, done) => {
|
return (accessToken, refreshToken, profile, done) => {
|
||||||
|
@ -57,5 +58,10 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.getCallbackUrl = async function (db, config) {
|
||||||
|
return ssoCallbackUrl(db, config, Configs.GOOGLE)
|
||||||
|
}
|
||||||
|
|
||||||
// expose for testing
|
// expose for testing
|
||||||
exports.buildVerifyFn = buildVerifyFn
|
exports.buildVerifyFn = buildVerifyFn
|
||||||
|
|
|
@ -55,6 +55,7 @@ exports.authenticate = async function (ctx, email, password, done) {
|
||||||
if (await compare(password, dbUser.password)) {
|
if (await compare(password, dbUser.password)) {
|
||||||
const sessionId = newid()
|
const sessionId = newid()
|
||||||
const tenantId = getTenantId()
|
const tenantId = getTenantId()
|
||||||
|
|
||||||
await createASession(dbUser._id, { sessionId, tenantId })
|
await createASession(dbUser._id, { sessionId, tenantId })
|
||||||
|
|
||||||
dbUser.token = jwt.sign(
|
dbUser.token = jwt.sign(
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy
|
const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy
|
||||||
const { authenticateThirdParty } = require("./third-party-common")
|
const { authenticateThirdParty } = require("./third-party-common")
|
||||||
|
const { ssoCallbackUrl } = require("./utils")
|
||||||
|
const { Configs } = require("../../../constants")
|
||||||
|
|
||||||
const buildVerifyFn = saveUserFn => {
|
const buildVerifyFn = saveUserFn => {
|
||||||
/**
|
/**
|
||||||
|
@ -89,11 +91,24 @@ function validEmail(value) {
|
||||||
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
|
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
|
||||||
* @returns Dynamically configured Passport OIDC Strategy
|
* @returns Dynamically configured Passport OIDC Strategy
|
||||||
*/
|
*/
|
||||||
exports.strategyFactory = async function (config, callbackUrl, saveUserFn) {
|
exports.strategyFactory = async function (config, saveUserFn) {
|
||||||
try {
|
try {
|
||||||
const { clientID, clientSecret, configUrl } = config
|
const verify = buildVerifyFn(saveUserFn)
|
||||||
|
const strategy = new OIDCStrategy(config, verify)
|
||||||
|
strategy.name = "oidc"
|
||||||
|
return strategy
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
throw new Error("Error constructing OIDC authentication strategy", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.fetchStrategyConfig = async function (enrichedConfig, callbackUrl) {
|
||||||
|
try {
|
||||||
|
const { clientID, clientSecret, configUrl } = enrichedConfig
|
||||||
|
|
||||||
if (!clientID || !clientSecret || !callbackUrl || !configUrl) {
|
if (!clientID || !clientSecret || !callbackUrl || !configUrl) {
|
||||||
|
//check for remote config and all required elements
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Configuration invalid. Must contain clientID, clientSecret, callbackUrl and configUrl"
|
"Configuration invalid. Must contain clientID, clientSecret, callbackUrl and configUrl"
|
||||||
)
|
)
|
||||||
|
@ -109,9 +124,7 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) {
|
||||||
|
|
||||||
const body = await response.json()
|
const body = await response.json()
|
||||||
|
|
||||||
const verify = buildVerifyFn(saveUserFn)
|
return {
|
||||||
return new OIDCStrategy(
|
|
||||||
{
|
|
||||||
issuer: body.issuer,
|
issuer: body.issuer,
|
||||||
authorizationURL: body.authorization_endpoint,
|
authorizationURL: body.authorization_endpoint,
|
||||||
tokenURL: body.token_endpoint,
|
tokenURL: body.token_endpoint,
|
||||||
|
@ -119,14 +132,16 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) {
|
||||||
clientID: clientID,
|
clientID: clientID,
|
||||||
clientSecret: clientSecret,
|
clientSecret: clientSecret,
|
||||||
callbackURL: callbackUrl,
|
callbackURL: callbackUrl,
|
||||||
},
|
}
|
||||||
verify
|
|
||||||
)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
throw new Error("Error constructing OIDC authentication strategy", err)
|
throw new Error("Error constructing OIDC authentication configuration", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.getCallbackUrl = async function (db, config) {
|
||||||
|
return ssoCallbackUrl(db, config, Configs.OIDC)
|
||||||
|
}
|
||||||
|
|
||||||
// expose for testing
|
// expose for testing
|
||||||
exports.buildVerifyFn = buildVerifyFn
|
exports.buildVerifyFn = buildVerifyFn
|
||||||
|
|
|
@ -48,8 +48,8 @@ describe("oidc", () => {
|
||||||
|
|
||||||
it("should create successfully create an oidc strategy", async () => {
|
it("should create successfully create an oidc strategy", async () => {
|
||||||
const oidc = require("../oidc")
|
const oidc = require("../oidc")
|
||||||
|
const enrichedConfig = await oidc.fetchStrategyConfig(oidcConfig, callbackUrl)
|
||||||
await oidc.strategyFactory(oidcConfig, callbackUrl)
|
await oidc.strategyFactory(enrichedConfig, callbackUrl)
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith(oidcConfig.configUrl)
|
expect(mockFetch).toHaveBeenCalledWith(oidcConfig.configUrl)
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
const { isMultiTenant, getTenantId } = require("../../tenancy")
|
||||||
|
const { getScopedConfig } = require("../../db/utils")
|
||||||
|
const { Configs } = require("../../constants")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility to handle authentication errors.
|
* Utility to handle authentication errors.
|
||||||
*
|
*
|
||||||
|
@ -5,6 +9,7 @@
|
||||||
* @param {*} message Message that will be returned in the response body
|
* @param {*} message Message that will be returned in the response body
|
||||||
* @param {*} err (Optional) error that will be logged
|
* @param {*} err (Optional) error that will be logged
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports.authError = function (done, message, err = null) {
|
exports.authError = function (done, message, err = null) {
|
||||||
return done(
|
return done(
|
||||||
err,
|
err,
|
||||||
|
@ -12,3 +17,21 @@ exports.authError = function (done, message, err = null) {
|
||||||
{ message: message }
|
{ message: message }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.ssoCallbackUrl = async (db, config, type) => {
|
||||||
|
// incase there is a callback URL from before
|
||||||
|
if (config && config.callbackURL) {
|
||||||
|
return config.callbackURL
|
||||||
|
}
|
||||||
|
const publicConfig = await getScopedConfig(db, {
|
||||||
|
type: Configs.SETTINGS,
|
||||||
|
})
|
||||||
|
|
||||||
|
let callbackUrl = `/api/global/auth`
|
||||||
|
if (isMultiTenant()) {
|
||||||
|
callbackUrl += `/${getTenantId()}`
|
||||||
|
}
|
||||||
|
callbackUrl += `/${type}/callback`
|
||||||
|
|
||||||
|
return `${publicConfig.platformUrl}${callbackUrl}`
|
||||||
|
}
|
||||||
|
|
|
@ -291,6 +291,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||||
|
|
||||||
|
"@budibase/types@^1.0.206":
|
||||||
|
version "1.0.208"
|
||||||
|
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.0.208.tgz#c45cb494fb5b85229e15a34c6ac1805bae5be867"
|
||||||
|
integrity sha512-zKIHg6TGK+soVxMNZNrGypP3DCrd3jhlUQEFeQ+rZR6/tCue1G74bjzydY5FjnLEsXeLH1a0hkS5HulTmvQ2bA==
|
||||||
|
|
||||||
"@istanbuljs/load-nyc-config@^1.0.0":
|
"@istanbuljs/load-nyc-config@^1.0.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
|
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
|
||||||
|
@ -4123,6 +4128,11 @@ passport-oauth1@1.x.x:
|
||||||
passport-strategy "1.x.x"
|
passport-strategy "1.x.x"
|
||||||
utils-merge "1.x.x"
|
utils-merge "1.x.x"
|
||||||
|
|
||||||
|
passport-oauth2-refresh@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/passport-oauth2-refresh/-/passport-oauth2-refresh-2.1.0.tgz#c31cd133826383f5539d16ad8ab4f35ca73ce4a4"
|
||||||
|
integrity sha512-4ML7ooCESCqiTgdDBzNUFTBcPR8zQq9iM6eppEUGMMvLdsjqRL93jKwWm4Az3OJcI+Q2eIVyI8sVRcPFvxcF/A==
|
||||||
|
|
||||||
passport-oauth2@1.x.x:
|
passport-oauth2@1.x.x:
|
||||||
version "1.6.1"
|
version "1.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.6.1.tgz#c5aee8f849ce8bd436c7f81d904a3cd1666f181b"
|
resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.6.1.tgz#c5aee8f849ce8bd436c7f81d904a3cd1666f181b"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "1.0.212-alpha.12",
|
"version": "1.0.212-alpha.14",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||||
"@budibase/string-templates": "^1.0.212-alpha.12",
|
"@budibase/string-templates": "^1.0.212-alpha.14",
|
||||||
"@spectrum-css/actionbutton": "^1.0.1",
|
"@spectrum-css/actionbutton": "^1.0.1",
|
||||||
"@spectrum-css/actiongroup": "^1.0.1",
|
"@spectrum-css/actiongroup": "^1.0.1",
|
||||||
"@spectrum-css/avatar": "^3.0.2",
|
"@spectrum-css/avatar": "^3.0.2",
|
||||||
|
|
|
@ -40,5 +40,6 @@
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
on:pick
|
on:pick
|
||||||
on:type
|
on:type
|
||||||
|
on:blur
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -52,7 +52,10 @@
|
||||||
{id}
|
{id}
|
||||||
type="text"
|
type="text"
|
||||||
on:focus={() => (focus = true)}
|
on:focus={() => (focus = true)}
|
||||||
on:blur={() => (focus = false)}
|
on:blur={() => {
|
||||||
|
focus = false
|
||||||
|
dispatch("blur")
|
||||||
|
}}
|
||||||
on:change={onType}
|
on:change={onType}
|
||||||
value={value || ""}
|
value={value || ""}
|
||||||
placeholder={placeholder || ""}
|
placeholder={placeholder || ""}
|
||||||
|
|
|
@ -85,11 +85,11 @@ filterTests(['all'], () => {
|
||||||
cy.get(interact.APP_TABLE_APP_NAME).click({ force: true })
|
cy.get(interact.APP_TABLE_APP_NAME).click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get(interact.DEPLOYMENT_TOP_NAV).click()
|
cy.get(interact.DEPLOYMENT_TOP_GLOBE).should("exist").click({ force: true })
|
||||||
cy.get(interact.PUBLISH_POPOVER_ACTION).click({ force: true })
|
|
||||||
cy.get(interact.UNPUBLISH_MODAL)
|
cy.get("[data-cy='publish-popover-menu']")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(interact.CONFIRM_WRAP_BUTTON).click({ force: true })
|
cy.get(interact.PUBLISH_POPOVER_ACTION).click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get(interact.UNPUBLISH_MODAL).should("be.visible")
|
cy.get(interact.UNPUBLISH_MODAL).should("be.visible")
|
||||||
|
|
|
@ -52,13 +52,6 @@ filterTests(['smoke', 'all'], () => {
|
||||||
|
|
||||||
// Start create app process. If apps already exist, click second button
|
// Start create app process. If apps already exist, click second button
|
||||||
cy.get(interact.CREATE_APP_BUTTON, { timeout: 1000 }).click({ force: true })
|
cy.get(interact.CREATE_APP_BUTTON, { timeout: 1000 }).click({ force: true })
|
||||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
|
||||||
.its("body")
|
|
||||||
.then(val => {
|
|
||||||
if (val.length > 0) {
|
|
||||||
cy.get(interact.CREATE_APP_BUTTON).click({ force: true })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const appName = "Cypress Tests"
|
const appName = "Cypress Tests"
|
||||||
cy.get(interact.SPECTRUM_MODAL).within(() => {
|
cy.get(interact.SPECTRUM_MODAL).within(() => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "1.0.212-alpha.12",
|
"version": "1.0.212-alpha.14",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -69,10 +69,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.212-alpha.12",
|
"@budibase/bbui": "^1.0.212-alpha.14",
|
||||||
"@budibase/client": "^1.0.212-alpha.12",
|
"@budibase/client": "^1.0.212-alpha.14",
|
||||||
"@budibase/frontend-core": "^1.0.212-alpha.12",
|
"@budibase/frontend-core": "^1.0.212-alpha.14",
|
||||||
"@budibase/string-templates": "^1.0.212-alpha.12",
|
"@budibase/string-templates": "^1.0.212-alpha.14",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
|
|
@ -49,6 +49,95 @@ export const getBindableProperties = (asset, componentId) => {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all rest bindable data fields
|
||||||
|
*/
|
||||||
|
export const getRestBindings = () => {
|
||||||
|
const userBindings = getUserBindings()
|
||||||
|
return [...userBindings, ...getAuthBindings()]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all rest bindable auth fields
|
||||||
|
*/
|
||||||
|
export const getAuthBindings = () => {
|
||||||
|
let bindings = []
|
||||||
|
const safeUser = makePropSafe("user")
|
||||||
|
const safeOAuth2 = makePropSafe("oauth2")
|
||||||
|
const safeAccessToken = makePropSafe("accessToken")
|
||||||
|
|
||||||
|
const authBindings = [
|
||||||
|
{
|
||||||
|
runtime: `${safeUser}.${safeOAuth2}.${safeAccessToken}`,
|
||||||
|
readable: `Current User.OAuthToken`,
|
||||||
|
key: "accessToken",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
bindings = Object.keys(authBindings).map(key => {
|
||||||
|
const fieldBinding = authBindings[key]
|
||||||
|
return {
|
||||||
|
type: "context",
|
||||||
|
runtimeBinding: fieldBinding.runtime,
|
||||||
|
readableBinding: fieldBinding.readable,
|
||||||
|
fieldSchema: { type: "string", name: fieldBinding.key },
|
||||||
|
providerId: "user",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return bindings
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility - convert a key/value map to an array of custom 'context' bindings
|
||||||
|
* @param {object} valueMap Key/value pairings
|
||||||
|
* @param {string} prefix A contextual string prefix/path for a user readable binding
|
||||||
|
* @return {object[]} An array containing readable/runtime binding objects
|
||||||
|
*/
|
||||||
|
export const toBindingsArray = (valueMap, prefix) => {
|
||||||
|
if (!valueMap) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return Object.keys(valueMap).reduce((acc, binding) => {
|
||||||
|
if (!binding || !valueMap[binding]) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
acc.push({
|
||||||
|
type: "context",
|
||||||
|
runtimeBinding: binding,
|
||||||
|
readableBinding: `${prefix}.${binding}`,
|
||||||
|
})
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility - coverting a map of readable bindings to runtime
|
||||||
|
*/
|
||||||
|
export const readableToRuntimeMap = (bindings, ctx) => {
|
||||||
|
if (!bindings || !ctx) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
return Object.keys(ctx).reduce((acc, key) => {
|
||||||
|
let parsedQuery = readableToRuntimeBinding(bindings, ctx[key])
|
||||||
|
acc[key] = parsedQuery
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility - coverting a map of runtime bindings to readable
|
||||||
|
*/
|
||||||
|
export const runtimeToReadableMap = (bindings, ctx) => {
|
||||||
|
if (!bindings || !ctx) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
return Object.keys(ctx).reduce((acc, key) => {
|
||||||
|
let parsedQuery = runtimeToReadableBinding(bindings, ctx[key])
|
||||||
|
acc[key] = parsedQuery
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the bindable properties exposed by a certain component.
|
* Gets the bindable properties exposed by a certain component.
|
||||||
*/
|
*/
|
||||||
|
@ -298,7 +387,6 @@ const getUserBindings = () => {
|
||||||
providerId: "user",
|
providerId: "user",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,11 +10,31 @@
|
||||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
import RestAuthenticationBuilder from "./auth/RestAuthenticationBuilder.svelte"
|
import RestAuthenticationBuilder from "./auth/RestAuthenticationBuilder.svelte"
|
||||||
import ViewDynamicVariables from "./variables/ViewDynamicVariables.svelte"
|
import ViewDynamicVariables from "./variables/ViewDynamicVariables.svelte"
|
||||||
|
import {
|
||||||
|
getRestBindings,
|
||||||
|
readableToRuntimeBinding,
|
||||||
|
runtimeToReadableMap,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
export let datasource
|
export let datasource
|
||||||
export let queries
|
export let queries
|
||||||
|
|
||||||
let addHeader
|
let addHeader
|
||||||
|
|
||||||
|
let parsedHeaders = runtimeToReadableMap(
|
||||||
|
getRestBindings(),
|
||||||
|
cloneDeep(datasource?.config?.defaultHeaders)
|
||||||
|
)
|
||||||
|
|
||||||
|
const onDefaultHeaderUpdate = headers => {
|
||||||
|
const flatHeaders = cloneDeep(headers).reduce((acc, entry) => {
|
||||||
|
acc[entry.name] = readableToRuntimeBinding(getRestBindings(), entry.value)
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
datasource.config.defaultHeaders = flatHeaders
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Divider size="S" />
|
<Divider size="S" />
|
||||||
|
@ -30,9 +50,10 @@
|
||||||
</Body>
|
</Body>
|
||||||
<KeyValueBuilder
|
<KeyValueBuilder
|
||||||
bind:this={addHeader}
|
bind:this={addHeader}
|
||||||
bind:object={datasource.config.defaultHeaders}
|
bind:object={parsedHeaders}
|
||||||
on:change
|
on:change={evt => onDefaultHeaderUpdate(evt.detail)}
|
||||||
noAddButton
|
noAddButton
|
||||||
|
bindings={getRestBindings()}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<ActionButton icon="Add" on:click={() => addHeader.addEntry()}>
|
<ActionButton icon="Add" on:click={() => addHeader.addEntry()}>
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { ModalContent, Layout, Select, Body, Input } from "@budibase/bbui"
|
import { ModalContent, Layout, Select, Body, Input } from "@budibase/bbui"
|
||||||
import { AUTH_TYPE_LABELS, AUTH_TYPES } from "./authTypes"
|
import { AUTH_TYPE_LABELS, AUTH_TYPES } from "./authTypes"
|
||||||
|
import BindableCombobox from "components/common/bindings/BindableCombobox.svelte"
|
||||||
|
import { getAuthBindings } from "builderStore/dataBinding"
|
||||||
|
|
||||||
export let configs
|
export let configs
|
||||||
export let currentConfig
|
export let currentConfig
|
||||||
|
@ -203,11 +205,23 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if form.type === AUTH_TYPES.BEARER}
|
{#if form.type === AUTH_TYPES.BEARER}
|
||||||
<Input
|
<BindableCombobox
|
||||||
label="Token"
|
label="Token"
|
||||||
bind:value={form.bearer.token}
|
value={form.bearer.token}
|
||||||
on:change={onFieldChange}
|
bindings={getAuthBindings()}
|
||||||
on:blur={() => (blurred.bearer.token = true)}
|
on:change={e => {
|
||||||
|
form.bearer.token = e.detail
|
||||||
|
console.log(e.detail)
|
||||||
|
onFieldChange()
|
||||||
|
}}
|
||||||
|
on:blur={() => {
|
||||||
|
blurred.bearer.token = true
|
||||||
|
onFieldChange()
|
||||||
|
}}
|
||||||
|
allowJS={false}
|
||||||
|
placeholder="Token"
|
||||||
|
appendBindingsAsOptions={true}
|
||||||
|
drawerEnabled={false}
|
||||||
error={blurred.bearer.token ? errors.bearer.token : null}
|
error={blurred.bearer.token ? errors.bearer.token : null}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
<script>
|
||||||
|
import { Combobox } from "@budibase/bbui"
|
||||||
|
import {
|
||||||
|
readableToRuntimeBinding,
|
||||||
|
runtimeToReadableBinding,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { isJSBinding } from "@budibase/string-templates"
|
||||||
|
|
||||||
|
export let value = ""
|
||||||
|
export let bindings = []
|
||||||
|
export let placeholder
|
||||||
|
export let label
|
||||||
|
export let disabled = false
|
||||||
|
export let options
|
||||||
|
export let appendBindingsAsOptions = true
|
||||||
|
export let error
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
$: readableValue = runtimeToReadableBinding(bindings, value)
|
||||||
|
$: isJS = isJSBinding(value)
|
||||||
|
$: allOptions = buildOptions(options, bindings, appendBindingsAsOptions)
|
||||||
|
|
||||||
|
const onChange = (value, optionPicked) => {
|
||||||
|
// Add HBS braces if picking binding
|
||||||
|
if (optionPicked && !options?.includes(value)) {
|
||||||
|
value = `{{ ${value} }}`
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch("change", readableToRuntimeBinding(bindings, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildOptions = (options, bindings, appendBindingsAsOptions) => {
|
||||||
|
if (!appendBindingsAsOptions) {
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
.concat(options || [])
|
||||||
|
.concat(bindings?.map(binding => binding.readableBinding) || [])
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="control" class:disabled>
|
||||||
|
<Combobox
|
||||||
|
{label}
|
||||||
|
{disabled}
|
||||||
|
readonly={isJS}
|
||||||
|
value={isJS ? "(JavaScript function)" : readableValue}
|
||||||
|
on:type={e => onChange(e.detail, false)}
|
||||||
|
on:pick={e => onChange(e.detail, true)}
|
||||||
|
on:blur={() => dispatch("blur")}
|
||||||
|
{placeholder}
|
||||||
|
options={allOptions}
|
||||||
|
{error}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.control {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control:not(.disabled) :global(.spectrum-Textfield-input) {
|
||||||
|
padding-right: 40px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -18,6 +18,7 @@
|
||||||
export let options
|
export let options
|
||||||
export let allowJS = true
|
export let allowJS = true
|
||||||
export let appendBindingsAsOptions = true
|
export let appendBindingsAsOptions = true
|
||||||
|
export let error
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let bindingDrawer
|
let bindingDrawer
|
||||||
|
@ -59,8 +60,10 @@
|
||||||
value={isJS ? "(JavaScript function)" : readableValue}
|
value={isJS ? "(JavaScript function)" : readableValue}
|
||||||
on:type={e => onChange(e.detail, false)}
|
on:type={e => onChange(e.detail, false)}
|
||||||
on:pick={e => onChange(e.detail, true)}
|
on:pick={e => onChange(e.detail, true)}
|
||||||
|
on:blur={() => dispatch("blur")}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
options={allOptions}
|
options={allOptions}
|
||||||
|
{error}
|
||||||
/>
|
/>
|
||||||
{#if !disabled}
|
{#if !disabled}
|
||||||
<div
|
<div
|
||||||
|
@ -72,6 +75,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Drawer bind:this={bindingDrawer} {title}>
|
<Drawer bind:this={bindingDrawer} {title}>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Add the objects on the left to enrich your text.
|
Add the objects on the left to enrich your text.
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { lowercase } from "helpers"
|
import { lowercase } from "helpers"
|
||||||
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
|
||||||
let dispatch = createEventDispatcher()
|
let dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
export let tooltip
|
export let tooltip
|
||||||
export let menuItems
|
export let menuItems
|
||||||
export let showMenu = false
|
export let showMenu = false
|
||||||
|
export let bindings = []
|
||||||
|
|
||||||
let fields = Object.entries(object || {}).map(([name, value]) => ({
|
let fields = Object.entries(object || {}).map(([name, value]) => ({
|
||||||
name,
|
name,
|
||||||
|
@ -108,6 +110,16 @@
|
||||||
/>
|
/>
|
||||||
{#if options}
|
{#if options}
|
||||||
<Select bind:value={field.value} on:change={changed} {options} />
|
<Select bind:value={field.value} on:change={changed} {options} />
|
||||||
|
{:else if bindings && bindings.length}
|
||||||
|
<DrawerBindableInput
|
||||||
|
{bindings}
|
||||||
|
placeholder="Value"
|
||||||
|
on:change={e => (field.value = e.detail)}
|
||||||
|
disabled={readOnly}
|
||||||
|
value={field.value}
|
||||||
|
allowJS={false}
|
||||||
|
fillWidth={true}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Input
|
<Input
|
||||||
placeholder={valuePlaceholder}
|
placeholder={valuePlaceholder}
|
||||||
|
|
|
@ -57,7 +57,8 @@
|
||||||
placeholder="Default"
|
placeholder="Default"
|
||||||
thin
|
thin
|
||||||
disabled={bindable}
|
disabled={bindable}
|
||||||
bind:value={binding.default}
|
on:change={evt => onBindingChange(binding.name, evt.detail)}
|
||||||
|
value={runtimeToReadableBinding(bindings, binding.default)}
|
||||||
/>
|
/>
|
||||||
{#if bindable}
|
{#if bindable}
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
|
|
|
@ -12,4 +12,6 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<slot />
|
{#key $params.selectedDatasource}
|
||||||
|
<slot />
|
||||||
|
{/key}
|
||||||
|
|
|
@ -40,13 +40,39 @@
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { RawRestBodyTypes } from "constants/backend"
|
import { RawRestBodyTypes } from "constants/backend"
|
||||||
|
|
||||||
|
import {
|
||||||
|
getRestBindings,
|
||||||
|
toBindingsArray,
|
||||||
|
runtimeToReadableBinding,
|
||||||
|
readableToRuntimeBinding,
|
||||||
|
runtimeToReadableMap,
|
||||||
|
readableToRuntimeMap,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
|
|
||||||
let query, datasource
|
let query, datasource
|
||||||
let breakQs = {},
|
let breakQs = {},
|
||||||
bindings = {}
|
requestBindings = {}
|
||||||
let saveId, url
|
let saveId, url
|
||||||
let response, schema, enabledHeaders
|
let response, schema, enabledHeaders
|
||||||
let authConfigId
|
let authConfigId
|
||||||
let dynamicVariables, addVariableModal, varBinding
|
let dynamicVariables, addVariableModal, varBinding
|
||||||
|
let restBindings = getRestBindings()
|
||||||
|
|
||||||
|
$: staticVariables = datasource?.config?.staticVariables || {}
|
||||||
|
|
||||||
|
$: customRequestBindings = toBindingsArray(requestBindings, "Binding")
|
||||||
|
$: dynamicRequestBindings = toBindingsArray(dynamicVariables, "Dynamic")
|
||||||
|
$: dataSourceStaticBindings = toBindingsArray(
|
||||||
|
staticVariables,
|
||||||
|
"Datasource.Static"
|
||||||
|
)
|
||||||
|
|
||||||
|
$: mergedBindings = [
|
||||||
|
...restBindings,
|
||||||
|
...customRequestBindings,
|
||||||
|
...dynamicRequestBindings,
|
||||||
|
...dataSourceStaticBindings,
|
||||||
|
]
|
||||||
|
|
||||||
$: datasourceType = datasource?.source
|
$: datasourceType = datasource?.source
|
||||||
$: integrationInfo = $integrations[datasourceType]
|
$: integrationInfo = $integrations[datasourceType]
|
||||||
|
@ -63,8 +89,10 @@
|
||||||
Object.keys(schema || {}).length !== 0 ||
|
Object.keys(schema || {}).length !== 0 ||
|
||||||
Object.keys(query?.schema || {}).length !== 0
|
Object.keys(query?.schema || {}).length !== 0
|
||||||
|
|
||||||
|
$: runtimeUrlQueries = readableToRuntimeMap(mergedBindings, breakQs)
|
||||||
|
|
||||||
function getSelectedQuery() {
|
function getSelectedQuery() {
|
||||||
return cloneDeep(
|
const cloneQuery = cloneDeep(
|
||||||
$queries.list.find(q => q._id === $queries.selected) || {
|
$queries.list.find(q => q._id === $queries.selected) || {
|
||||||
datasourceId: $params.selectedDatasource,
|
datasourceId: $params.selectedDatasource,
|
||||||
parameters: [],
|
parameters: [],
|
||||||
|
@ -76,6 +104,7 @@
|
||||||
queryVerb: "read",
|
queryVerb: "read",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
return cloneQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkQueryName(inputUrl = null) {
|
function checkQueryName(inputUrl = null) {
|
||||||
|
@ -89,7 +118,9 @@
|
||||||
if (!base) {
|
if (!base) {
|
||||||
return base
|
return base
|
||||||
}
|
}
|
||||||
const qs = restUtils.buildQueryString(qsObj)
|
const qs = restUtils.buildQueryString(
|
||||||
|
runtimeToReadableMap(mergedBindings, qsObj)
|
||||||
|
)
|
||||||
let newUrl = base
|
let newUrl = base
|
||||||
if (base.includes("?")) {
|
if (base.includes("?")) {
|
||||||
newUrl = base.split("?")[0]
|
newUrl = base.split("?")[0]
|
||||||
|
@ -98,14 +129,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildQuery() {
|
function buildQuery() {
|
||||||
const newQuery = { ...query }
|
const newQuery = cloneDeep(query)
|
||||||
const queryString = restUtils.buildQueryString(breakQs)
|
const queryString = restUtils.buildQueryString(runtimeUrlQueries)
|
||||||
|
|
||||||
|
newQuery.parameters = restUtils.keyValueToQueryParameters(requestBindings)
|
||||||
|
newQuery.fields.requestBody =
|
||||||
|
typeof newQuery.fields.requestBody === "object"
|
||||||
|
? readableToRuntimeMap(mergedBindings, newQuery.fields.requestBody)
|
||||||
|
: readableToRuntimeBinding(mergedBindings, newQuery.fields.requestBody)
|
||||||
|
|
||||||
newQuery.fields.path = url.split("?")[0]
|
newQuery.fields.path = url.split("?")[0]
|
||||||
newQuery.fields.queryString = queryString
|
newQuery.fields.queryString = queryString
|
||||||
newQuery.fields.authConfigId = authConfigId
|
newQuery.fields.authConfigId = authConfigId
|
||||||
newQuery.fields.disabledHeaders = restUtils.flipHeaderState(enabledHeaders)
|
newQuery.fields.disabledHeaders = restUtils.flipHeaderState(enabledHeaders)
|
||||||
newQuery.schema = restUtils.fieldsToSchema(schema)
|
newQuery.schema = restUtils.fieldsToSchema(schema)
|
||||||
newQuery.parameters = restUtils.keyValueToQueryParameters(bindings)
|
|
||||||
return newQuery
|
return newQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,6 +158,13 @@
|
||||||
datasource.config.dynamicVariables = rebuildVariables(saveId)
|
datasource.config.dynamicVariables = rebuildVariables(saveId)
|
||||||
datasource = await datasources.save(datasource)
|
datasource = await datasources.save(datasource)
|
||||||
}
|
}
|
||||||
|
prettifyQueryRequestBody(
|
||||||
|
query,
|
||||||
|
requestBindings,
|
||||||
|
dynamicVariables,
|
||||||
|
staticVariables,
|
||||||
|
restBindings
|
||||||
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(`Error saving query`)
|
notifications.error(`Error saving query`)
|
||||||
}
|
}
|
||||||
|
@ -127,7 +172,7 @@
|
||||||
|
|
||||||
async function runQuery() {
|
async function runQuery() {
|
||||||
try {
|
try {
|
||||||
response = await queries.preview(buildQuery(query))
|
response = await queries.preview(buildQuery())
|
||||||
if (response.rows.length === 0) {
|
if (response.rows.length === 0) {
|
||||||
notifications.info("Request did not return any data")
|
notifications.info("Request did not return any data")
|
||||||
} else {
|
} else {
|
||||||
|
@ -236,6 +281,36 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const prettifyQueryRequestBody = (
|
||||||
|
query,
|
||||||
|
requestBindings,
|
||||||
|
dynamicVariables,
|
||||||
|
staticVariables,
|
||||||
|
restBindings
|
||||||
|
) => {
|
||||||
|
let customRequestBindings = toBindingsArray(requestBindings, "Binding")
|
||||||
|
let dynamicRequestBindings = toBindingsArray(dynamicVariables, "Dynamic")
|
||||||
|
let dataSourceStaticBindings = toBindingsArray(
|
||||||
|
staticVariables,
|
||||||
|
"Datasource.Static"
|
||||||
|
)
|
||||||
|
|
||||||
|
const prettyBindings = [
|
||||||
|
...restBindings,
|
||||||
|
...customRequestBindings,
|
||||||
|
...dynamicRequestBindings,
|
||||||
|
...dataSourceStaticBindings,
|
||||||
|
]
|
||||||
|
|
||||||
|
//Parse the body here as now all bindings have been updated.
|
||||||
|
if (query?.fields?.requestBody) {
|
||||||
|
query.fields.requestBody =
|
||||||
|
typeof query.fields.requestBody === "object"
|
||||||
|
? runtimeToReadableMap(prettyBindings, query.fields.requestBody)
|
||||||
|
: runtimeToReadableBinding(prettyBindings, query.fields.requestBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
query = getSelectedQuery()
|
query = getSelectedQuery()
|
||||||
|
|
||||||
|
@ -250,6 +325,8 @@
|
||||||
const datasourceUrl = datasource?.config.url
|
const datasourceUrl = datasource?.config.url
|
||||||
const qs = query?.fields.queryString
|
const qs = query?.fields.queryString
|
||||||
breakQs = restUtils.breakQueryString(qs)
|
breakQs = restUtils.breakQueryString(qs)
|
||||||
|
breakQs = runtimeToReadableMap(mergedBindings, breakQs)
|
||||||
|
|
||||||
const path = query.fields.path
|
const path = query.fields.path
|
||||||
if (
|
if (
|
||||||
datasourceUrl &&
|
datasourceUrl &&
|
||||||
|
@ -260,7 +337,7 @@
|
||||||
}
|
}
|
||||||
url = buildUrl(query.fields.path, breakQs)
|
url = buildUrl(query.fields.path, breakQs)
|
||||||
schema = restUtils.schemaToFields(query.schema)
|
schema = restUtils.schemaToFields(query.schema)
|
||||||
bindings = restUtils.queryParametersToKeyValue(query.parameters)
|
requestBindings = restUtils.queryParametersToKeyValue(query.parameters)
|
||||||
authConfigId = getAuthConfigId()
|
authConfigId = getAuthConfigId()
|
||||||
if (!query.fields.disabledHeaders) {
|
if (!query.fields.disabledHeaders) {
|
||||||
query.fields.disabledHeaders = {}
|
query.fields.disabledHeaders = {}
|
||||||
|
@ -291,6 +368,14 @@
|
||||||
query.fields.pagination = {}
|
query.fields.pagination = {}
|
||||||
}
|
}
|
||||||
dynamicVariables = getDynamicVariables(datasource, query._id)
|
dynamicVariables = getDynamicVariables(datasource, query._id)
|
||||||
|
|
||||||
|
prettifyQueryRequestBody(
|
||||||
|
query,
|
||||||
|
requestBindings,
|
||||||
|
dynamicVariables,
|
||||||
|
staticVariables,
|
||||||
|
restBindings
|
||||||
|
)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -344,16 +429,26 @@
|
||||||
<Tabs selected="Bindings" quiet noPadding noHorizPadding onTop>
|
<Tabs selected="Bindings" quiet noPadding noHorizPadding onTop>
|
||||||
<Tab title="Bindings">
|
<Tab title="Bindings">
|
||||||
<KeyValueBuilder
|
<KeyValueBuilder
|
||||||
bind:object={bindings}
|
bind:object={requestBindings}
|
||||||
tooltip="Set the name of the binding which can be used in Handlebars statements throughout your query"
|
tooltip="Set the name of the binding which can be used in Handlebars statements throughout your query"
|
||||||
name="binding"
|
name="binding"
|
||||||
headings
|
headings
|
||||||
keyPlaceholder="Binding name"
|
keyPlaceholder="Binding name"
|
||||||
valuePlaceholder="Default"
|
valuePlaceholder="Default"
|
||||||
|
bindings={[
|
||||||
|
...restBindings,
|
||||||
|
...dynamicRequestBindings,
|
||||||
|
...dataSourceStaticBindings,
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Params">
|
<Tab title="Params">
|
||||||
<KeyValueBuilder bind:object={breakQs} name="param" headings />
|
<KeyValueBuilder
|
||||||
|
bind:object={breakQs}
|
||||||
|
name="param"
|
||||||
|
headings
|
||||||
|
bindings={mergedBindings}
|
||||||
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Headers">
|
<Tab title="Headers">
|
||||||
<KeyValueBuilder
|
<KeyValueBuilder
|
||||||
|
@ -362,6 +457,7 @@
|
||||||
toggle
|
toggle
|
||||||
name="header"
|
name="header"
|
||||||
headings
|
headings
|
||||||
|
bindings={mergedBindings}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Body">
|
<Tab title="Body">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "1.0.212-alpha.12",
|
"version": "1.0.212-alpha.14",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "1.0.212-alpha.12",
|
"version": "1.0.212-alpha.14",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
|
@ -19,9 +19,9 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.212-alpha.12",
|
"@budibase/bbui": "^1.0.212-alpha.14",
|
||||||
"@budibase/frontend-core": "^1.0.212-alpha.12",
|
"@budibase/frontend-core": "^1.0.212-alpha.14",
|
||||||
"@budibase/string-templates": "^1.0.212-alpha.12",
|
"@budibase/string-templates": "^1.0.212-alpha.14",
|
||||||
"@spectrum-css/button": "^3.0.3",
|
"@spectrum-css/button": "^3.0.3",
|
||||||
"@spectrum-css/card": "^3.0.3",
|
"@spectrum-css/card": "^3.0.3",
|
||||||
"@spectrum-css/divider": "^1.0.3",
|
"@spectrum-css/divider": "^1.0.3",
|
||||||
|
|
|
@ -278,6 +278,9 @@ const notEqualHandler = (value, rule) => {
|
||||||
// Evaluates a regex constraint
|
// Evaluates a regex constraint
|
||||||
const regexHandler = (value, rule) => {
|
const regexHandler = (value, rule) => {
|
||||||
const regex = parseType(rule.value, "string")
|
const regex = parseType(rule.value, "string")
|
||||||
|
if (!value) {
|
||||||
|
value = ""
|
||||||
|
}
|
||||||
return new RegExp(regex).test(value)
|
return new RegExp(regex).test(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/frontend-core",
|
"name": "@budibase/frontend-core",
|
||||||
"version": "1.0.212-alpha.12",
|
"version": "1.0.212-alpha.14",
|
||||||
"description": "Budibase frontend core libraries used in builder and client",
|
"description": "Budibase frontend core libraries used in builder and client",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.212-alpha.12",
|
"@budibase/bbui": "^1.0.212-alpha.14",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"svelte": "^3.46.2"
|
"svelte": "^3.46.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,15 @@ module FetchMock {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
json: async () => {
|
json: async () => {
|
||||||
|
//x-www-form-encoded body is a URLSearchParams
|
||||||
|
//The call to stringify it leaves it blank
|
||||||
|
if (body?.opts?.body instanceof URLSearchParams) {
|
||||||
|
const paramArray = Array.from(body.opts.body.entries())
|
||||||
|
body.opts.body = paramArray.reduce((acc: any, pair: any) => {
|
||||||
|
acc[pair[0]] = pair[1]
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
return body
|
return body
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "1.0.212-alpha.12",
|
"version": "1.0.212-alpha.14",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -77,11 +77,11 @@
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "10.0.3",
|
"@apidevtools/swagger-parser": "10.0.3",
|
||||||
"@budibase/backend-core": "^1.0.212-alpha.12",
|
"@budibase/backend-core": "^1.0.212-alpha.14",
|
||||||
"@budibase/client": "^1.0.212-alpha.12",
|
"@budibase/client": "^1.0.212-alpha.14",
|
||||||
"@budibase/pro": "1.0.212-alpha.12",
|
"@budibase/pro": "1.0.212-alpha.14",
|
||||||
"@budibase/string-templates": "^1.0.212-alpha.12",
|
"@budibase/string-templates": "^1.0.212-alpha.14",
|
||||||
"@budibase/types": "^1.0.212-alpha.12",
|
"@budibase/types": "^1.0.212-alpha.14",
|
||||||
"@bull-board/api": "3.7.0",
|
"@bull-board/api": "3.7.0",
|
||||||
"@bull-board/koa": "3.9.4",
|
"@bull-board/koa": "3.9.4",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { QUERY_THREAD_TIMEOUT } from "../../../environment"
|
||||||
import { getAppDB } from "@budibase/backend-core/context"
|
import { getAppDB } from "@budibase/backend-core/context"
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
import { events } from "@budibase/backend-core"
|
import { events } from "@budibase/backend-core"
|
||||||
|
import { getCookie } from "@budibase/backend-core/utils"
|
||||||
|
import { Cookies, Configs } from "@budibase/backend-core/constants"
|
||||||
|
|
||||||
const Runner = new Thread(ThreadType.QUERY, {
|
const Runner = new Thread(ThreadType.QUERY, {
|
||||||
timeoutMs: QUERY_THREAD_TIMEOUT || 10000,
|
timeoutMs: QUERY_THREAD_TIMEOUT || 10000,
|
||||||
|
@ -110,6 +112,21 @@ export async function find(ctx: any) {
|
||||||
ctx.body = query
|
ctx.body = query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Required to discern between OIDC OAuth config entries
|
||||||
|
function getOAuthConfigCookieId(ctx: any) {
|
||||||
|
if (ctx.user.providerType === Configs.OIDC) {
|
||||||
|
return getCookie(ctx, Cookies.OIDC_CONFIG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAuthConfig(ctx: any) {
|
||||||
|
const authCookie = getCookie(ctx, Cookies.Auth)
|
||||||
|
let authConfigCtx: any = {}
|
||||||
|
authConfigCtx["configId"] = getOAuthConfigCookieId(ctx)
|
||||||
|
authConfigCtx["sessionId"] = authCookie ? authCookie.sessionId : null
|
||||||
|
return authConfigCtx
|
||||||
|
}
|
||||||
|
|
||||||
export async function preview(ctx: any) {
|
export async function preview(ctx: any) {
|
||||||
const db = getAppDB()
|
const db = getAppDB()
|
||||||
|
|
||||||
|
@ -119,6 +136,8 @@ export async function preview(ctx: any) {
|
||||||
// this stops dynamic variables from calling the same query
|
// this stops dynamic variables from calling the same query
|
||||||
const { fields, parameters, queryVerb, transformer, queryId } = query
|
const { fields, parameters, queryVerb, transformer, queryId } = query
|
||||||
|
|
||||||
|
const authConfigCtx: any = getAuthConfig(ctx)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const runFn = () =>
|
const runFn = () =>
|
||||||
Runner.run({
|
Runner.run({
|
||||||
|
@ -129,8 +148,11 @@ export async function preview(ctx: any) {
|
||||||
parameters,
|
parameters,
|
||||||
transformer,
|
transformer,
|
||||||
queryId,
|
queryId,
|
||||||
|
ctx: {
|
||||||
|
user: ctx.user,
|
||||||
|
auth: { ...authConfigCtx },
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { rows, keys, info, extra } = await quotas.addQuery(runFn)
|
const { rows, keys, info, extra } = await quotas.addQuery(runFn)
|
||||||
await events.query.previewed(datasource, query)
|
await events.query.previewed(datasource, query)
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
|
@ -150,6 +172,8 @@ async function execute(ctx: any, opts = { rowsOnly: false }) {
|
||||||
const query = await db.get(ctx.params.queryId)
|
const query = await db.get(ctx.params.queryId)
|
||||||
const datasource = await db.get(query.datasourceId)
|
const datasource = await db.get(query.datasourceId)
|
||||||
|
|
||||||
|
const authConfigCtx: any = getAuthConfig(ctx)
|
||||||
|
|
||||||
const enrichedParameters = ctx.request.body.parameters || {}
|
const enrichedParameters = ctx.request.body.parameters || {}
|
||||||
// make sure parameters are fully enriched with defaults
|
// make sure parameters are fully enriched with defaults
|
||||||
if (query && query.parameters) {
|
if (query && query.parameters) {
|
||||||
|
@ -172,6 +196,10 @@ async function execute(ctx: any, opts = { rowsOnly: false }) {
|
||||||
parameters: enrichedParameters,
|
parameters: enrichedParameters,
|
||||||
transformer: query.transformer,
|
transformer: query.transformer,
|
||||||
queryId: ctx.params.queryId,
|
queryId: ctx.params.queryId,
|
||||||
|
ctx: {
|
||||||
|
user: ctx.user,
|
||||||
|
auth: { ...authConfigCtx },
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { rows, pagination, extra } = await quotas.addQuery(runFn)
|
const { rows, pagination, extra } = await quotas.addQuery(runFn)
|
||||||
|
|
|
@ -346,4 +346,170 @@ describe("/queries", () => {
|
||||||
expect(contents).toBe(null)
|
expect(contents).toBe(null)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("Current User Request Mapping", () => {
|
||||||
|
|
||||||
|
async function previewGet(datasource, fields, params) {
|
||||||
|
return config.previewQuery(request, config, datasource, fields, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function previewPost(datasource, fields, params) {
|
||||||
|
return config.previewQuery(request, config, datasource, fields, params, "create")
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should parse global and query level header mappings", async () => {
|
||||||
|
const userDetails = config.getUserDetails()
|
||||||
|
|
||||||
|
const datasource = await config.restDatasource({
|
||||||
|
defaultHeaders: {
|
||||||
|
"test": "headerVal",
|
||||||
|
"emailHdr": "{{[user].[email]}}"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const res = await previewGet(datasource, {
|
||||||
|
path: "www.google.com",
|
||||||
|
queryString: "email={{[user].[email]}}",
|
||||||
|
headers: {
|
||||||
|
queryHdr : "{{[user].[firstName]}}",
|
||||||
|
secondHdr : "1234"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const parsedRequest = JSON.parse(res.body.extra.raw)
|
||||||
|
expect(parsedRequest.opts.headers).toEqual({
|
||||||
|
"test": "headerVal",
|
||||||
|
"emailHdr": userDetails.email,
|
||||||
|
"queryHdr": userDetails.firstName,
|
||||||
|
"secondHdr" : "1234"
|
||||||
|
})
|
||||||
|
expect(res.body.rows[0].url).toEqual("http://www.google.com?email=" + userDetails.email)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should bind the current user to query parameters", async () => {
|
||||||
|
const userDetails = config.getUserDetails()
|
||||||
|
|
||||||
|
const datasource = await config.restDatasource()
|
||||||
|
|
||||||
|
const res = await previewGet(datasource, {
|
||||||
|
path: "www.google.com",
|
||||||
|
queryString: "test={{myEmail}}&testName={{myName}}&testParam={{testParam}}",
|
||||||
|
}, {
|
||||||
|
"myEmail" : "{{[user].[email]}}",
|
||||||
|
"myName" : "{{[user].[firstName]}}",
|
||||||
|
"testParam" : "1234"
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.body.rows[0].url).toEqual("http://www.google.com?test=" + userDetails.email +
|
||||||
|
"&testName=" + userDetails.firstName + "&testParam=1234")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should bind the current user the request body - plain text", async () => {
|
||||||
|
const userDetails = config.getUserDetails()
|
||||||
|
const datasource = await config.restDatasource()
|
||||||
|
|
||||||
|
const res = await previewPost(datasource, {
|
||||||
|
path: "www.google.com",
|
||||||
|
queryString: "testParam={{testParam}}",
|
||||||
|
requestBody: "This is plain text and this is my email: {{[user].[email]}}. This is a test param: {{testParam}}",
|
||||||
|
bodyType: "text"
|
||||||
|
}, {
|
||||||
|
"testParam" : "1234"
|
||||||
|
})
|
||||||
|
|
||||||
|
const parsedRequest = JSON.parse(res.body.extra.raw)
|
||||||
|
expect(parsedRequest.opts.body).toEqual(`This is plain text and this is my email: ${userDetails.email}. This is a test param: 1234`)
|
||||||
|
expect(res.body.rows[0].url).toEqual("http://www.google.com?testParam=1234")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should bind the current user the request body - json", async () => {
|
||||||
|
const userDetails = config.getUserDetails()
|
||||||
|
const datasource = await config.restDatasource()
|
||||||
|
|
||||||
|
const res = await previewPost(datasource, {
|
||||||
|
path: "www.google.com",
|
||||||
|
queryString: "testParam={{testParam}}",
|
||||||
|
requestBody: "{\"email\":\"{{[user].[email]}}\",\"queryCode\":{{testParam}},\"userRef\":\"{{userRef}}\"}",
|
||||||
|
bodyType: "json"
|
||||||
|
}, {
|
||||||
|
"testParam" : "1234",
|
||||||
|
"userRef" : "{{[user].[firstName]}}"
|
||||||
|
})
|
||||||
|
|
||||||
|
const parsedRequest = JSON.parse(res.body.extra.raw)
|
||||||
|
const test = `{"email":"${userDetails.email}","queryCode":1234,"userRef":"${userDetails.firstName}"}`
|
||||||
|
expect(parsedRequest.opts.body).toEqual(test)
|
||||||
|
expect(res.body.rows[0].url).toEqual("http://www.google.com?testParam=1234")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should bind the current user the request body - xml", async () => {
|
||||||
|
const userDetails = config.getUserDetails()
|
||||||
|
const datasource = await config.restDatasource()
|
||||||
|
|
||||||
|
const res = await previewPost(datasource, {
|
||||||
|
path: "www.google.com",
|
||||||
|
queryString: "testParam={{testParam}}",
|
||||||
|
requestBody: "<note> <email>{{[user].[email]}}</email> <code>{{testParam}}</code> " +
|
||||||
|
"<ref>{{userId}}</ref> <somestring>testing</somestring> </note>",
|
||||||
|
bodyType: "xml"
|
||||||
|
}, {
|
||||||
|
"testParam" : "1234",
|
||||||
|
"userId" : "{{[user].[firstName]}}"
|
||||||
|
})
|
||||||
|
|
||||||
|
const parsedRequest = JSON.parse(res.body.extra.raw)
|
||||||
|
const test = `<note> <email>${userDetails.email}</email> <code>1234</code> <ref>${userDetails.firstName}</ref> <somestring>testing</somestring> </note>`
|
||||||
|
|
||||||
|
expect(parsedRequest.opts.body).toEqual(test)
|
||||||
|
expect(res.body.rows[0].url).toEqual("http://www.google.com?testParam=1234")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should bind the current user the request body - form-data", async () => {
|
||||||
|
const userDetails = config.getUserDetails()
|
||||||
|
const datasource = await config.restDatasource()
|
||||||
|
|
||||||
|
const res = await previewPost(datasource, {
|
||||||
|
path: "www.google.com",
|
||||||
|
queryString: "testParam={{testParam}}",
|
||||||
|
requestBody: "{\"email\":\"{{[user].[email]}}\",\"queryCode\":{{testParam}},\"userRef\":\"{{userRef}}\"}",
|
||||||
|
bodyType: "form"
|
||||||
|
}, {
|
||||||
|
"testParam" : "1234",
|
||||||
|
"userRef" : "{{[user].[firstName]}}"
|
||||||
|
})
|
||||||
|
|
||||||
|
const parsedRequest = JSON.parse(res.body.extra.raw)
|
||||||
|
|
||||||
|
const emailData = parsedRequest.opts.body._streams[1]
|
||||||
|
expect(emailData).toEqual(userDetails.email)
|
||||||
|
|
||||||
|
const queryCodeData = parsedRequest.opts.body._streams[4]
|
||||||
|
expect(queryCodeData).toEqual("1234")
|
||||||
|
|
||||||
|
const userRef = parsedRequest.opts.body._streams[7]
|
||||||
|
expect(userRef).toEqual(userDetails.firstName)
|
||||||
|
|
||||||
|
expect(res.body.rows[0].url).toEqual("http://www.google.com?testParam=1234")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should bind the current user the request body - encoded", async () => {
|
||||||
|
const userDetails = config.getUserDetails()
|
||||||
|
const datasource = await config.restDatasource()
|
||||||
|
|
||||||
|
const res = await previewPost(datasource, {
|
||||||
|
path: "www.google.com",
|
||||||
|
queryString: "testParam={{testParam}}",
|
||||||
|
requestBody: "{\"email\":\"{{[user].[email]}}\",\"queryCode\":{{testParam}},\"userRef\":\"{{userRef}}\"}",
|
||||||
|
bodyType: "encoded"
|
||||||
|
}, {
|
||||||
|
"testParam" : "1234",
|
||||||
|
"userRef" : "{{[user].[firstName]}}"
|
||||||
|
})
|
||||||
|
const parsedRequest = JSON.parse(res.body.extra.raw)
|
||||||
|
|
||||||
|
expect(parsedRequest.opts.body.email).toEqual(userDetails.email)
|
||||||
|
expect(parsedRequest.opts.body.queryCode).toEqual("1234")
|
||||||
|
expect(parsedRequest.opts.body.userRef).toEqual(userDetails.firstName)
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,7 +9,9 @@ export function enrichQueryFields(
|
||||||
parameters = {}
|
parameters = {}
|
||||||
) {
|
) {
|
||||||
const enrichedQuery: { [key: string]: any } = Array.isArray(fields) ? [] : {}
|
const enrichedQuery: { [key: string]: any } = Array.isArray(fields) ? [] : {}
|
||||||
|
if (!fields || !parameters) {
|
||||||
|
return enrichedQuery
|
||||||
|
}
|
||||||
// enrich the fields with dynamic parameters
|
// enrich the fields with dynamic parameters
|
||||||
for (let key of Object.keys(fields)) {
|
for (let key of Object.keys(fields)) {
|
||||||
if (fields[key] == null) {
|
if (fields[key] == null) {
|
||||||
|
|
|
@ -287,7 +287,7 @@ module RestModule {
|
||||||
input.body = form
|
input.body = form
|
||||||
break
|
break
|
||||||
case BodyTypes.XML:
|
case BodyTypes.XML:
|
||||||
if (object != null) {
|
if (object != null && Object.keys(object).length) {
|
||||||
string = new XmlBuilder().buildObject(object)
|
string = new XmlBuilder().buildObject(object)
|
||||||
}
|
}
|
||||||
input.body = string
|
input.body = string
|
||||||
|
|
|
@ -155,12 +155,27 @@ describe("REST Integration", () => {
|
||||||
expect(output.headers["Content-Type"]).toEqual("application/json")
|
expect(output.headers["Content-Type"]).toEqual("application/json")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should allow XML", () => {
|
it("should allow raw XML", () => {
|
||||||
|
const output = config.integration.addBody("xml", "<a>1</a><b>2</b>", {})
|
||||||
|
expect(output.body.includes("<a>1</a>")).toEqual(true)
|
||||||
|
expect(output.body.includes("<b>2</b>")).toEqual(true)
|
||||||
|
expect(output.headers["Content-Type"]).toEqual("application/xml")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should allow a valid js object and parse the contents to xml", () => {
|
||||||
const output = config.integration.addBody("xml", input, {})
|
const output = config.integration.addBody("xml", input, {})
|
||||||
expect(output.body.includes("<a>1</a>")).toEqual(true)
|
expect(output.body.includes("<a>1</a>")).toEqual(true)
|
||||||
expect(output.body.includes("<b>2</b>")).toEqual(true)
|
expect(output.body.includes("<b>2</b>")).toEqual(true)
|
||||||
expect(output.headers["Content-Type"]).toEqual("application/xml")
|
expect(output.headers["Content-Type"]).toEqual("application/xml")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should allow a valid json string and parse the contents to xml", () => {
|
||||||
|
const output = config.integration.addBody("xml", JSON.stringify(input), {})
|
||||||
|
expect(output.body.includes("<a>1</a>")).toEqual(true)
|
||||||
|
expect(output.body.includes("<b>2</b>")).toEqual(true)
|
||||||
|
expect(output.headers["Content-Type"]).toEqual("application/xml")
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("response", () => {
|
describe("response", () => {
|
||||||
|
|
|
@ -93,8 +93,24 @@ describe("migrations", () => {
|
||||||
await clearMigrations()
|
await clearMigrations()
|
||||||
const appId = config.prodAppId
|
const appId = config.prodAppId
|
||||||
const roles = { [appId]: "role_12345" }
|
const roles = { [appId]: "role_12345" }
|
||||||
await config.createUser(undefined, undefined, false, true, roles) // admin only
|
await config.createUser(
|
||||||
await config.createUser(undefined, undefined, false, false, roles) // non admin non builder
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
roles
|
||||||
|
) // admin only
|
||||||
|
await config.createUser(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
roles
|
||||||
|
) // non admin non builder
|
||||||
await config.createTable()
|
await config.createTable()
|
||||||
await config.createRow()
|
await config.createRow()
|
||||||
await config.createRow()
|
await config.createRow()
|
||||||
|
|
|
@ -8,4 +8,5 @@ declare module "@budibase/backend-core/constants"
|
||||||
declare module "@budibase/backend-core/auth"
|
declare module "@budibase/backend-core/auth"
|
||||||
declare module "@budibase/backend-core/sessions"
|
declare module "@budibase/backend-core/sessions"
|
||||||
declare module "@budibase/backend-core/encryption"
|
declare module "@budibase/backend-core/encryption"
|
||||||
|
declare module "@budibase/backend-core/utils"
|
||||||
declare module "@budibase/backend-core/redis"
|
declare module "@budibase/backend-core/redis"
|
||||||
|
|
|
@ -28,6 +28,8 @@ const { encrypt } = require("@budibase/backend-core/encryption")
|
||||||
|
|
||||||
const GLOBAL_USER_ID = "us_uuid1"
|
const GLOBAL_USER_ID = "us_uuid1"
|
||||||
const EMAIL = "babs@babs.com"
|
const EMAIL = "babs@babs.com"
|
||||||
|
const FIRSTNAME = "Barbara"
|
||||||
|
const LASTNAME = "Barbington"
|
||||||
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"
|
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"
|
||||||
|
|
||||||
class TestConfiguration {
|
class TestConfiguration {
|
||||||
|
@ -59,6 +61,15 @@ class TestConfiguration {
|
||||||
return this.prodAppId
|
return this.prodAppId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserDetails() {
|
||||||
|
return {
|
||||||
|
globalId: GLOBAL_USER_ID,
|
||||||
|
email: EMAIL,
|
||||||
|
firstName: FIRSTNAME,
|
||||||
|
lastName: LASTNAME,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async doInContext(appId, task) {
|
async doInContext(appId, task) {
|
||||||
if (!appId) {
|
if (!appId) {
|
||||||
appId = this.appId
|
appId = this.appId
|
||||||
|
@ -118,6 +129,8 @@ class TestConfiguration {
|
||||||
// USER / AUTH
|
// USER / AUTH
|
||||||
async globalUser({
|
async globalUser({
|
||||||
id = GLOBAL_USER_ID,
|
id = GLOBAL_USER_ID,
|
||||||
|
firstName = FIRSTNAME,
|
||||||
|
lastName = LASTNAME,
|
||||||
builder = true,
|
builder = true,
|
||||||
admin = false,
|
admin = false,
|
||||||
email = EMAIL,
|
email = EMAIL,
|
||||||
|
@ -135,6 +148,8 @@ class TestConfiguration {
|
||||||
...existing,
|
...existing,
|
||||||
roles: roles || {},
|
roles: roles || {},
|
||||||
tenantId: TENANT_ID,
|
tenantId: TENANT_ID,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
}
|
}
|
||||||
await createASession(id, {
|
await createASession(id, {
|
||||||
sessionId: "sessionid",
|
sessionId: "sessionid",
|
||||||
|
@ -161,6 +176,8 @@ class TestConfiguration {
|
||||||
|
|
||||||
async createUser(
|
async createUser(
|
||||||
id = null,
|
id = null,
|
||||||
|
firstName = FIRSTNAME,
|
||||||
|
lastName = LASTNAME,
|
||||||
email = EMAIL,
|
email = EMAIL,
|
||||||
builder = true,
|
builder = true,
|
||||||
admin = false,
|
admin = false,
|
||||||
|
@ -169,6 +186,8 @@ class TestConfiguration {
|
||||||
const globalId = !id ? `us_${Math.random()}` : `us_${id}`
|
const globalId = !id ? `us_${Math.random()}` : `us_${id}`
|
||||||
const resp = await this.globalUser({
|
const resp = await this.globalUser({
|
||||||
id: globalId,
|
id: globalId,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
email,
|
email,
|
||||||
builder,
|
builder,
|
||||||
admin,
|
admin,
|
||||||
|
@ -520,14 +539,14 @@ class TestConfiguration {
|
||||||
|
|
||||||
// QUERY
|
// QUERY
|
||||||
|
|
||||||
async previewQuery(request, config, datasource, fields) {
|
async previewQuery(request, config, datasource, fields, params, verb) {
|
||||||
return request
|
return request
|
||||||
.post(`/api/queries/preview`)
|
.post(`/api/queries/preview`)
|
||||||
.send({
|
.send({
|
||||||
datasourceId: datasource._id,
|
datasourceId: datasource._id,
|
||||||
parameters: {},
|
parameters: params || {},
|
||||||
fields,
|
fields,
|
||||||
queryVerb: "read",
|
queryVerb: verb || "read",
|
||||||
name: datasource.name,
|
name: datasource.name,
|
||||||
})
|
})
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
|
|
|
@ -4,6 +4,12 @@ const ScriptRunner = require("../utilities/scriptRunner")
|
||||||
const { integrations } = require("../integrations")
|
const { integrations } = require("../integrations")
|
||||||
const { processStringSync } = require("@budibase/string-templates")
|
const { processStringSync } = require("@budibase/string-templates")
|
||||||
const { doInAppContext, getAppDB } = require("@budibase/backend-core/context")
|
const { doInAppContext, getAppDB } = require("@budibase/backend-core/context")
|
||||||
|
const {
|
||||||
|
refreshOAuthToken,
|
||||||
|
updateUserOAuth,
|
||||||
|
} = require("@budibase/backend-core/auth")
|
||||||
|
const { getGlobalIDFromUserMetadataID } = require("../db/utils")
|
||||||
|
|
||||||
const { isSQL } = require("../integrations/utils")
|
const { isSQL } = require("../integrations/utils")
|
||||||
const {
|
const {
|
||||||
enrichQueryFields,
|
enrichQueryFields,
|
||||||
|
@ -21,29 +27,56 @@ class QueryRunner {
|
||||||
this.queryId = input.queryId
|
this.queryId = input.queryId
|
||||||
this.noRecursiveQuery = flags.noRecursiveQuery
|
this.noRecursiveQuery = flags.noRecursiveQuery
|
||||||
this.cachedVariables = []
|
this.cachedVariables = []
|
||||||
|
// Additional context items for enrichment
|
||||||
|
this.ctx = input.ctx
|
||||||
// allows the response from a query to be stored throughout this
|
// allows the response from a query to be stored throughout this
|
||||||
// execution so that if it needs to be re-used for another variable
|
// execution so that if it needs to be re-used for another variable
|
||||||
// it can be
|
// it can be
|
||||||
this.queryResponse = {}
|
this.queryResponse = {}
|
||||||
this.hasRerun = false
|
this.hasRerun = false
|
||||||
|
this.hasRefreshedOAuth = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute() {
|
async execute() {
|
||||||
let { datasource, fields, queryVerb, transformer } = this
|
let { datasource, fields, queryVerb, transformer } = this
|
||||||
|
|
||||||
const Integration = integrations[datasource.source]
|
const Integration = integrations[datasource.source]
|
||||||
if (!Integration) {
|
if (!Integration) {
|
||||||
throw "Integration type does not exist."
|
throw "Integration type does not exist."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (datasource.config.authConfigs) {
|
||||||
|
datasource.config.authConfigs = datasource.config.authConfigs.map(
|
||||||
|
config => {
|
||||||
|
return enrichQueryFields(config, this.ctx)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const integration = new Integration(datasource.config)
|
const integration = new Integration(datasource.config)
|
||||||
|
|
||||||
// pre-query, make sure datasource variables are added to parameters
|
// pre-query, make sure datasource variables are added to parameters
|
||||||
const parameters = await this.addDatasourceVariables()
|
const parameters = await this.addDatasourceVariables()
|
||||||
|
|
||||||
|
// Enrich the parameters with the addition context items.
|
||||||
|
// 'user' is now a reserved variable key in mapping parameters
|
||||||
|
const enrichedParameters = enrichQueryFields(parameters, this.ctx)
|
||||||
|
const enrichedContext = { ...enrichedParameters, ...this.ctx }
|
||||||
|
|
||||||
|
// Parse global headers
|
||||||
|
if (datasource.config.defaultHeaders) {
|
||||||
|
datasource.config.defaultHeaders = enrichQueryFields(
|
||||||
|
datasource.config.defaultHeaders,
|
||||||
|
enrichedContext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
let query
|
let query
|
||||||
// handle SQL injections by interpolating the variables
|
// handle SQL injections by interpolating the variables
|
||||||
if (isSQL(datasource)) {
|
if (isSQL(datasource)) {
|
||||||
query = interpolateSQL(fields, parameters, integration)
|
query = interpolateSQL(fields, enrichedParameters, integration)
|
||||||
} else {
|
} else {
|
||||||
query = enrichQueryFields(fields, parameters)
|
query = enrichQueryFields(fields, enrichedContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add pagination values for REST queries
|
// Add pagination values for REST queries
|
||||||
|
@ -67,20 +100,30 @@ class QueryRunner {
|
||||||
if (transformer) {
|
if (transformer) {
|
||||||
const runner = new ScriptRunner(transformer, {
|
const runner = new ScriptRunner(transformer, {
|
||||||
data: rows,
|
data: rows,
|
||||||
params: parameters,
|
params: enrichedParameters,
|
||||||
})
|
})
|
||||||
rows = runner.execute()
|
rows = runner.execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the request fails we retry once, invalidating the cached value
|
// if the request fails we retry once, invalidating the cached value
|
||||||
|
if (info && info.code >= 400 && !this.hasRerun) {
|
||||||
if (
|
if (
|
||||||
info &&
|
this.ctx.user?.provider &&
|
||||||
info.code >= 400 &&
|
info.code === 401 &&
|
||||||
this.cachedVariables.length > 0 &&
|
!this.hasRefreshedOAuth
|
||||||
!this.hasRerun
|
|
||||||
) {
|
) {
|
||||||
|
// Attempt to refresh the access token from the provider
|
||||||
|
this.hasRefreshedOAuth = true
|
||||||
|
const authResponse = await this.refreshOAuth2(this.ctx)
|
||||||
|
|
||||||
|
if (!authResponse || authResponse.err) {
|
||||||
|
// In this event the user may have oAuth issues that
|
||||||
|
// could require re-authenticating with their provider.
|
||||||
|
throw new Error("OAuth2 access token could not be refreshed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.hasRerun = true
|
this.hasRerun = true
|
||||||
// invalidate the cache value
|
|
||||||
await threadUtils.invalidateDynamicVariables(this.cachedVariables)
|
await threadUtils.invalidateDynamicVariables(this.cachedVariables)
|
||||||
return this.execute()
|
return this.execute()
|
||||||
}
|
}
|
||||||
|
@ -126,6 +169,31 @@ class QueryRunner {
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refreshOAuth2(ctx) {
|
||||||
|
const { oauth2, providerType, _id } = ctx.user
|
||||||
|
const { configId } = ctx.auth
|
||||||
|
|
||||||
|
if (!providerType || !oauth2?.refreshToken) {
|
||||||
|
console.error("No refresh token found for authenticated user")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await refreshOAuthToken(
|
||||||
|
oauth2.refreshToken,
|
||||||
|
providerType,
|
||||||
|
configId
|
||||||
|
)
|
||||||
|
|
||||||
|
// Refresh session flow. Should be in same location as refreshOAuthToken
|
||||||
|
// There are several other properties available in 'resp'
|
||||||
|
if (!resp.error) {
|
||||||
|
const globalUserId = getGlobalIDFromUserMetadataID(_id)
|
||||||
|
await updateUserOAuth(globalUserId, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
async getDynamicVariable(variable) {
|
async getDynamicVariable(variable) {
|
||||||
let { parameters } = this
|
let { parameters } = this
|
||||||
const queryId = variable.queryId,
|
const queryId = variable.queryId,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/string-templates",
|
"name": "@budibase/string-templates",
|
||||||
"version": "1.0.212-alpha.12",
|
"version": "1.0.212-alpha.14",
|
||||||
"description": "Handlebars wrapper for Budibase templating.",
|
"description": "Handlebars wrapper for Budibase templating.",
|
||||||
"main": "src/index.cjs",
|
"main": "src/index.cjs",
|
||||||
"module": "dist/bundle.mjs",
|
"module": "dist/bundle.mjs",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/types",
|
"name": "@budibase/types",
|
||||||
"version": "1.0.212-alpha.12",
|
"version": "1.0.212-alpha.14",
|
||||||
"description": "Budibase types",
|
"description": "Budibase types",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/worker",
|
"name": "@budibase/worker",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "1.0.212-alpha.12",
|
"version": "1.0.212-alpha.14",
|
||||||
"description": "Budibase background service",
|
"description": "Budibase background service",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -34,10 +34,10 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/backend-core": "^1.0.212-alpha.12",
|
"@budibase/backend-core": "^1.0.212-alpha.14",
|
||||||
"@budibase/pro": "1.0.212-alpha.12",
|
"@budibase/pro": "1.0.212-alpha.14",
|
||||||
"@budibase/string-templates": "^1.0.212-alpha.12",
|
"@budibase/string-templates": "^1.0.212-alpha.14",
|
||||||
"@budibase/types": "^1.0.212-alpha.12",
|
"@budibase/types": "^1.0.212-alpha.14",
|
||||||
"@koa/router": "8.0.8",
|
"@koa/router": "8.0.8",
|
||||||
"@sentry/node": "6.17.7",
|
"@sentry/node": "6.17.7",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
|
|
|
@ -1,49 +1,22 @@
|
||||||
const core = require("@budibase/backend-core")
|
const core = require("@budibase/backend-core")
|
||||||
const { getScopedConfig } = require("@budibase/backend-core/db")
|
|
||||||
const { google } = require("@budibase/backend-core/middleware")
|
|
||||||
const { oidc } = require("@budibase/backend-core/middleware")
|
|
||||||
const { Configs, EmailTemplatePurpose } = require("../../../constants")
|
const { Configs, EmailTemplatePurpose } = require("../../../constants")
|
||||||
const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
|
const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
|
||||||
const { setCookie, getCookie, clearCookie, hash, platformLogout } = core.utils
|
const { setCookie, getCookie, clearCookie, hash, platformLogout } = core.utils
|
||||||
const { Cookies, Headers } = core.constants
|
const { Cookies, Headers } = core.constants
|
||||||
const { passport } = core.auth
|
const { passport, ssoCallbackUrl, google, oidc } = core.auth
|
||||||
const { checkResetPasswordCode } = require("../../../utilities/redis")
|
const { checkResetPasswordCode } = require("../../../utilities/redis")
|
||||||
const {
|
const { getGlobalDB } = require("@budibase/backend-core/tenancy")
|
||||||
getGlobalDB,
|
|
||||||
getTenantId,
|
|
||||||
isMultiTenant,
|
|
||||||
} = require("@budibase/backend-core/tenancy")
|
|
||||||
const env = require("../../../environment")
|
const env = require("../../../environment")
|
||||||
import { events, users as usersCore, context } from "@budibase/backend-core"
|
import { events, users as usersCore, context } from "@budibase/backend-core"
|
||||||
import { users } from "../../../sdk"
|
import { users } from "../../../sdk"
|
||||||
import { User } from "@budibase/types"
|
import { User } from "@budibase/types"
|
||||||
|
|
||||||
const ssoCallbackUrl = async (config: any, type: any) => {
|
|
||||||
// incase there is a callback URL from before
|
|
||||||
if (config && config.callbackURL) {
|
|
||||||
return config.callbackURL
|
|
||||||
}
|
|
||||||
|
|
||||||
const db = getGlobalDB()
|
|
||||||
const publicConfig = await getScopedConfig(db, {
|
|
||||||
type: Configs.SETTINGS,
|
|
||||||
})
|
|
||||||
|
|
||||||
let callbackUrl = `/api/global/auth`
|
|
||||||
if (isMultiTenant()) {
|
|
||||||
callbackUrl += `/${getTenantId()}`
|
|
||||||
}
|
|
||||||
callbackUrl += `/${type}/callback`
|
|
||||||
|
|
||||||
return `${publicConfig.platformUrl}${callbackUrl}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const googleCallbackUrl = async (config: any) => {
|
export const googleCallbackUrl = async (config: any) => {
|
||||||
return ssoCallbackUrl(config, "google")
|
return ssoCallbackUrl(getGlobalDB(), config, "google")
|
||||||
}
|
}
|
||||||
|
|
||||||
export const oidcCallbackUrl = async (config: any) => {
|
export const oidcCallbackUrl = async (config: any) => {
|
||||||
return ssoCallbackUrl(config, "oidc")
|
return ssoCallbackUrl(getGlobalDB(), config, "oidc")
|
||||||
}
|
}
|
||||||
|
|
||||||
async function authInternal(ctx: any, user: any, err = null, info = null) {
|
async function authInternal(ctx: any, user: any, err = null, info = null) {
|
||||||
|
@ -198,6 +171,8 @@ export const googlePreAuth = async (ctx: any, next: any) => {
|
||||||
|
|
||||||
return passport.authenticate(strategy, {
|
return passport.authenticate(strategy, {
|
||||||
scope: ["profile", "email"],
|
scope: ["profile", "email"],
|
||||||
|
accessType: "offline",
|
||||||
|
prompt: "consent",
|
||||||
})(ctx, next)
|
})(ctx, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,7 +199,7 @@ export const googleAuth = async (ctx: any, next: any) => {
|
||||||
)(ctx, next)
|
)(ctx, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function oidcStrategyFactory(ctx: any, configId: any) {
|
export const oidcStrategyFactory = async (ctx: any, configId: any) => {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
const config = await core.db.getScopedConfig(db, {
|
const config = await core.db.getScopedConfig(db, {
|
||||||
type: Configs.OIDC,
|
type: Configs.OIDC,
|
||||||
|
@ -234,7 +209,12 @@ async function oidcStrategyFactory(ctx: any, configId: any) {
|
||||||
const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0]
|
const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0]
|
||||||
let callbackUrl = await exports.oidcCallbackUrl(chosenConfig)
|
let callbackUrl = await exports.oidcCallbackUrl(chosenConfig)
|
||||||
|
|
||||||
return oidc.strategyFactory(chosenConfig, callbackUrl, users.save)
|
//Remote Config
|
||||||
|
const enrichedConfig = await oidc.fetchStrategyConfig(
|
||||||
|
chosenConfig,
|
||||||
|
callbackUrl
|
||||||
|
)
|
||||||
|
return oidc.strategyFactory(enrichedConfig, users.save)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -249,7 +229,7 @@ export const oidcPreAuth = async (ctx: any, next: any) => {
|
||||||
|
|
||||||
return passport.authenticate(strategy, {
|
return passport.authenticate(strategy, {
|
||||||
// required 'openid' scope is added by oidc strategy factory
|
// required 'openid' scope is added by oidc strategy factory
|
||||||
scope: ["profile", "email"],
|
scope: ["profile", "email", "offline_access"], //auth0 offline_access scope required for the refresh token behaviour.
|
||||||
})(ctx, next)
|
})(ctx, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,27 +77,30 @@ describe("/api/global/auth", () => {
|
||||||
describe("oidc", () => {
|
describe("oidc", () => {
|
||||||
const auth = require("@budibase/backend-core/auth")
|
const auth = require("@budibase/backend-core/auth")
|
||||||
|
|
||||||
// mock the oidc strategy implementation and return value
|
|
||||||
let strategyFactory = jest.fn()
|
|
||||||
let mockStrategyReturn = jest.fn()
|
|
||||||
strategyFactory.mockReturnValue(mockStrategyReturn)
|
|
||||||
auth.oidc.strategyFactory = strategyFactory
|
|
||||||
|
|
||||||
const passportSpy = jest.spyOn(auth.passport, "authenticate")
|
const passportSpy = jest.spyOn(auth.passport, "authenticate")
|
||||||
let oidcConf
|
let oidcConf
|
||||||
let chosenConfig
|
let chosenConfig
|
||||||
let configId
|
let configId
|
||||||
|
|
||||||
|
// mock the oidc strategy implementation and return value
|
||||||
|
let strategyFactory = jest.fn()
|
||||||
|
let mockStrategyReturn = jest.fn()
|
||||||
|
let mockStrategyConfig = jest.fn()
|
||||||
|
auth.oidc.fetchStrategyConfig = mockStrategyConfig
|
||||||
|
|
||||||
|
strategyFactory.mockReturnValue(mockStrategyReturn)
|
||||||
|
auth.oidc.strategyFactory = strategyFactory
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
oidcConf = await config.saveOIDCConfig()
|
oidcConf = await config.saveOIDCConfig()
|
||||||
chosenConfig = oidcConf.config.configs[0]
|
chosenConfig = oidcConf.config.configs[0]
|
||||||
configId = chosenConfig.uuid
|
configId = chosenConfig.uuid
|
||||||
|
mockStrategyConfig.mockReturnValue(chosenConfig)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
expect(strategyFactory).toBeCalledWith(
|
expect(strategyFactory).toBeCalledWith(
|
||||||
chosenConfig,
|
chosenConfig,
|
||||||
`http://localhost:10000/api/global/auth/${TENANT_ID}/oidc/callback`,
|
|
||||||
expect.any(Function)
|
expect.any(Function)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -107,7 +110,7 @@ describe("/api/global/auth", () => {
|
||||||
await request.get(`/api/global/auth/${TENANT_ID}/oidc/configs/${configId}`)
|
await request.get(`/api/global/auth/${TENANT_ID}/oidc/configs/${configId}`)
|
||||||
|
|
||||||
expect(passportSpy).toBeCalledWith(mockStrategyReturn, {
|
expect(passportSpy).toBeCalledWith(mockStrategyReturn, {
|
||||||
scope: ["profile", "email"],
|
scope: ["profile", "email", "offline_access"]
|
||||||
})
|
})
|
||||||
expect(passportSpy.mock.calls.length).toBe(1);
|
expect(passportSpy.mock.calls.length).toBe(1);
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue