Merge branch 'feature/oidc-support' of https://github.com/Budibase/budibase into oidc-config-management
This commit is contained in:
commit
4b518af605
|
@ -75,6 +75,28 @@
|
||||||
"design"
|
"design"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"login": "Rory-Powell",
|
||||||
|
"name": "Rory Powell",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/8755148?v=4",
|
||||||
|
"profile": "https://github.com/Rory-Powell",
|
||||||
|
"contributions": [
|
||||||
|
"code",
|
||||||
|
"doc",
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "PClmnt",
|
||||||
|
"name": "Peter Clement",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/5665926?v=4",
|
||||||
|
"profile": "https://github.com/PClmnt",
|
||||||
|
"contributions": [
|
||||||
|
"code",
|
||||||
|
"doc",
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"login": "Conor-Mack",
|
"login": "Conor-Mack",
|
||||||
"name": "Conor_Mack",
|
"name": "Conor_Mack",
|
||||||
|
|
|
@ -7,3 +7,5 @@ Contributors
|
||||||
* Martin McKeaveney - [@shogunpurple](https://github.com/shogunpurple)
|
* Martin McKeaveney - [@shogunpurple](https://github.com/shogunpurple)
|
||||||
* Andrew Kingston - [@aptkingston](https://github.com/aptkingston)
|
* Andrew Kingston - [@aptkingston](https://github.com/aptkingston)
|
||||||
* Michael Drury - [@mike12345567](https://github.com/mike12345567)
|
* Michael Drury - [@mike12345567](https://github.com/mike12345567)
|
||||||
|
* Peter Clement - [@PClmnt](https://github.com/PClmnt)
|
||||||
|
* Rory Powell - [@Rory-Powell](https://github.com/Rory-Powell)
|
|
@ -126,7 +126,16 @@ To run the budibase server and builder in dev mode (i.e. with live reloading):
|
||||||
|
|
||||||
This will enable watch mode for both the builder app, server, client library and any component libraries.
|
This will enable watch mode for both the builder app, server, client library and any component libraries.
|
||||||
|
|
||||||
### 5. Cleanup
|
### 5. Debugging using VS Code
|
||||||
|
|
||||||
|
To debug the budibase server and worker a VS Code launch configuration has been provided.
|
||||||
|
|
||||||
|
Visit the debug window and select `Budibase Server` or `Budibase Worker` to debug the respective component.
|
||||||
|
Alternatively to start both components simultaneously select `Start Budibase`.
|
||||||
|
|
||||||
|
In addition to the above, the remaining budibase components may be ran in dev mode using: `yarn dev:noserver`.
|
||||||
|
|
||||||
|
### 6. Cleanup
|
||||||
|
|
||||||
If you wish to delete all the apps created in development and reset the environment then run the following:
|
If you wish to delete all the apps created in development and reset the environment then run the following:
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,27 @@
|
||||||
"args": [],
|
"args": [],
|
||||||
"cwd":"C:/code/my-apps",
|
"cwd":"C:/code/my-apps",
|
||||||
"console": "externalTerminal"
|
"console": "externalTerminal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Budibase Server",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
|
||||||
|
"args": ["${workspaceFolder}/packages/server/src/index.ts"],
|
||||||
|
"cwd": "${workspaceFolder}/packages/server"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Budibase Worker",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/packages/worker/src/index.js",
|
||||||
|
"cwd": "${workspaceFolder}/packages/worker"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compounds": [
|
||||||
|
{
|
||||||
|
"name": "Start Budibase",
|
||||||
|
"configurations": ["Budibase Server", "Budibase Worker"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -211,9 +211,11 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||||
<td align="center"><a href="https://budibase.com/"><img src="https://avatars3.githubusercontent.com/u/3524181?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Shanks</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Tests">⚠️</a></td>
|
<td align="center"><a href="https://budibase.com/"><img src="https://avatars3.githubusercontent.com/u/3524181?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Shanks</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Tests">⚠️</a></td>
|
||||||
<td align="center"><a href="https://github.com/kevmodrome"><img src="https://avatars3.githubusercontent.com/u/534488?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kevin Åberg Kultalahti</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Tests">⚠️</a></td>
|
<td align="center"><a href="https://github.com/kevmodrome"><img src="https://avatars3.githubusercontent.com/u/534488?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kevin Åberg Kultalahti</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Tests">⚠️</a></td>
|
||||||
<td align="center"><a href="https://www.budibase.com/"><img src="https://avatars2.githubusercontent.com/u/49767913?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Joe</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Code">💻</a> <a href="#content-joebudi" title="Content">🖋</a> <a href="#design-joebudi" title="Design">🎨</a></td>
|
<td align="center"><a href="https://www.budibase.com/"><img src="https://avatars2.githubusercontent.com/u/49767913?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Joe</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Code">💻</a> <a href="#content-joebudi" title="Content">🖋</a> <a href="#design-joebudi" title="Design">🎨</a></td>
|
||||||
<td align="center"><a href="https://github.com/Conor-Mack"><img src="https://avatars1.githubusercontent.com/u/36074859?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Conor_Mack</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Tests">⚠️</a></td>
|
<td align="center"><a href="https://github.com/Rory-Powell"><img src="https://avatars.githubusercontent.com/u/8755148?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rory Powell</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Tests">⚠️</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td align="center"><a href="https://github.com/PClmnt"><img src="https://avatars.githubusercontent.com/u/5665926?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Peter Clement</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/Conor-Mack"><img src="https://avatars1.githubusercontent.com/u/36074859?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Conor_Mack</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Tests">⚠️</a></td>
|
||||||
<td align="center"><a href="https://github.com/pngwn"><img src="https://avatars1.githubusercontent.com/u/12937446?v=4?s=100" width="100px;" alt=""/><br /><sub><b>pngwn</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Tests">⚠️</a></td>
|
<td align="center"><a href="https://github.com/pngwn"><img src="https://avatars1.githubusercontent.com/u/12937446?v=4?s=100" width="100px;" alt=""/><br /><sub><b>pngwn</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Tests">⚠️</a></td>
|
||||||
<td align="center"><a href="https://github.com/HugoLd"><img src="https://avatars0.githubusercontent.com/u/26521848?v=4?s=100" width="100px;" alt=""/><br /><sub><b>HugoLd</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=HugoLd" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/HugoLd"><img src="https://avatars0.githubusercontent.com/u/26521848?v=4?s=100" width="100px;" alt=""/><br /><sub><b>HugoLd</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=HugoLd" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="https://github.com/victoriasloan"><img src="https://avatars.githubusercontent.com/u/9913651?v=4?s=100" width="100px;" alt=""/><br /><sub><b>victoriasloan</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=victoriasloan" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/victoriasloan"><img src="https://avatars.githubusercontent.com/u/9913651?v=4?s=100" width="100px;" alt=""/><br /><sub><b>victoriasloan</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=victoriasloan" title="Code">💻</a></td>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "0.9.67",
|
"version": "0.9.70",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/auth",
|
"name": "@budibase/auth",
|
||||||
"version": "0.9.67",
|
"version": "0.9.70",
|
||||||
"description": "Authentication middlewares for budibase builder and apps",
|
"description": "Authentication middlewares for budibase builder and apps",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
|
@ -13,6 +13,7 @@
|
||||||
"koa-passport": "^4.1.4",
|
"koa-passport": "^4.1.4",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
|
"@techpass/passport-openidconnect": "^0.3.0",
|
||||||
"passport-google-auth": "^1.0.2",
|
"passport-google-auth": "^1.0.2",
|
||||||
"passport-google-oauth": "^2.0.0",
|
"passport-google-oauth": "^2.0.0",
|
||||||
"passport-jwt": "^4.0.0",
|
"passport-jwt": "^4.0.0",
|
||||||
|
|
|
@ -2,7 +2,14 @@ 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 { StaticDatabases } = require("./db/utils")
|
const { StaticDatabases } = require("./db/utils")
|
||||||
const { jwt, local, authenticated, google, auditLog } = require("./middleware")
|
const {
|
||||||
|
jwt,
|
||||||
|
local,
|
||||||
|
authenticated,
|
||||||
|
google,
|
||||||
|
oidc,
|
||||||
|
auditLog,
|
||||||
|
} = require("./middleware")
|
||||||
const { setDB, getDB } = require("./db")
|
const { setDB, getDB } = require("./db")
|
||||||
|
|
||||||
// Strategies
|
// Strategies
|
||||||
|
@ -44,6 +51,7 @@ module.exports = {
|
||||||
buildAuthMiddleware: authenticated,
|
buildAuthMiddleware: authenticated,
|
||||||
passport,
|
passport,
|
||||||
google,
|
google,
|
||||||
|
oidc,
|
||||||
jwt: require("jsonwebtoken"),
|
jwt: require("jsonwebtoken"),
|
||||||
auditLog,
|
auditLog,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
const jwt = require("./passport/jwt")
|
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 authenticated = require("./authenticated")
|
const authenticated = require("./authenticated")
|
||||||
const auditLog = require("./auditLog")
|
const auditLog = require("./auditLog")
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
google,
|
google,
|
||||||
|
oidc,
|
||||||
jwt,
|
jwt,
|
||||||
local,
|
local,
|
||||||
authenticated,
|
authenticated,
|
||||||
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
const env = require("../../environment")
|
||||||
|
const jwt = require("jsonwebtoken")
|
||||||
|
const database = require("../../db")
|
||||||
|
const fetch = require("node-fetch")
|
||||||
|
const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy
|
||||||
|
const {
|
||||||
|
StaticDatabases,
|
||||||
|
generateGlobalUserID,
|
||||||
|
ViewNames,
|
||||||
|
} = require("../../db/utils")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to parse the users email address.
|
||||||
|
*
|
||||||
|
* It is not guaranteed that the email will be returned by the user info endpoint (e.g. github connected account used in azure ad).
|
||||||
|
* Fallback to the id token where possible.
|
||||||
|
*
|
||||||
|
* @param {*} profile The structured profile created by passport using the user info endpoint
|
||||||
|
* @param {*} jwtClaims The raw claims returned in the id token
|
||||||
|
*/
|
||||||
|
function getEmail(profile, jwtClaims) {
|
||||||
|
if (profile._json.email) {
|
||||||
|
return profile._json.email
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jwtClaims.email) {
|
||||||
|
return jwtClaims.email
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} issuer The identity provider base URL
|
||||||
|
* @param {*} sub The user ID
|
||||||
|
* @param {*} profile The user profile information. Created by passport from the /userinfo response
|
||||||
|
* @param {*} jwtClaims The parsed id_token claims
|
||||||
|
* @param {*} accessToken The access_token for contacting the identity provider - may or may not be a JWT
|
||||||
|
* @param {*} refreshToken The refresh_token for obtaining a new access_token - usually not a JWT
|
||||||
|
* @param {*} idToken The id_token - always a JWT
|
||||||
|
* @param {*} params The response body from requesting an access_token
|
||||||
|
* @param {*} done The passport callback: err, user, info
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async function authenticate(
|
||||||
|
issuer,
|
||||||
|
sub,
|
||||||
|
profile,
|
||||||
|
jwtClaims,
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
idToken,
|
||||||
|
params,
|
||||||
|
done
|
||||||
|
) {
|
||||||
|
// Check the user exists in the instance DB by email
|
||||||
|
const db = database.getDB(StaticDatabases.GLOBAL.name)
|
||||||
|
|
||||||
|
let dbUser
|
||||||
|
|
||||||
|
const userId = generateGlobalUserID(profile.id)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// use the OIDC profile id
|
||||||
|
dbUser = await db.get(userId)
|
||||||
|
} catch (err) {
|
||||||
|
const user = {
|
||||||
|
_id: userId,
|
||||||
|
provider: profile.provider,
|
||||||
|
roles: {},
|
||||||
|
...profile._json,
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if an account with the OIDC email address exists locally
|
||||||
|
const email = getEmail(profile, jwtClaims)
|
||||||
|
if (!email) {
|
||||||
|
return done(null, false, { message: "No email address found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await db.query(`database/${ViewNames.USER_BY_EMAIL}`, {
|
||||||
|
key: email,
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// OIDC user already exists by email
|
||||||
|
if (users.rows.length > 0) {
|
||||||
|
const existing = users.rows[0].doc
|
||||||
|
|
||||||
|
// remove the local account to avoid conflicts
|
||||||
|
await db.remove(existing._id, existing._rev)
|
||||||
|
|
||||||
|
// merge with existing account
|
||||||
|
user.roles = existing.roles
|
||||||
|
user.builder = existing.builder
|
||||||
|
user.admin = existing.admin
|
||||||
|
|
||||||
|
const response = await db.post(user)
|
||||||
|
dbUser = user
|
||||||
|
dbUser._rev = response.rev
|
||||||
|
} else {
|
||||||
|
return done(null, false, { message: "Email does not yet exist. You must set up your local budibase account first." })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// authenticate
|
||||||
|
const payload = {
|
||||||
|
userId: dbUser._id,
|
||||||
|
builder: dbUser.builder,
|
||||||
|
email: dbUser.email,
|
||||||
|
}
|
||||||
|
|
||||||
|
dbUser.token = jwt.sign(payload, env.JWT_SECRET, {
|
||||||
|
expiresIn: "1 day",
|
||||||
|
})
|
||||||
|
|
||||||
|
return done(null, dbUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance of the oidc passport strategy. This wrapper fetches the configuration
|
||||||
|
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
|
||||||
|
* @returns Dynamically configured Passport OIDC Strategy
|
||||||
|
*/
|
||||||
|
exports.strategyFactory = async function (callbackUrl) {
|
||||||
|
try {
|
||||||
|
const configurationUrl =
|
||||||
|
"https://login.microsoftonline.com/2668c0dd-7ed2-4db3-b387-05b6f9204a70/v2.0/.well-known/openid-configuration"
|
||||||
|
const clientSecret = "g-ty~2iW.bo.88xj_QI6~hdc-H8mP2Xbnd"
|
||||||
|
const clientId = "bed2017b-2f53-42a9-8ef9-e58918935e07"
|
||||||
|
|
||||||
|
if (!clientId || !clientSecret || !callbackUrl || !configurationUrl) {
|
||||||
|
throw new Error(
|
||||||
|
"Configuration invalid. Must contain clientID, clientSecret, callbackUrl and configurationUrl"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(configurationUrl)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Unexpected response when fetching openid-configuration: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await response.json()
|
||||||
|
|
||||||
|
return new OIDCStrategy(
|
||||||
|
{
|
||||||
|
issuer: body.issuer,
|
||||||
|
authorizationURL: body.authorization_endpoint,
|
||||||
|
tokenURL: body.token_endpoint,
|
||||||
|
userInfoURL: body.userinfo_endpoint,
|
||||||
|
clientID: clientId,
|
||||||
|
clientSecret: clientSecret,
|
||||||
|
callbackURL: callbackUrl,
|
||||||
|
scope: "profile email",
|
||||||
|
},
|
||||||
|
authenticate
|
||||||
|
)
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
throw new Error("Error constructing OIDC authentication strategy", err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,17 @@
|
||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@techpass/passport-openidconnect@^0.3.0":
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@techpass/passport-openidconnect/-/passport-openidconnect-0.3.0.tgz#a60b2bbf3f262649a5a02d5d186219944acc3010"
|
||||||
|
integrity sha512-bVsPwl66s7J7GHxTPlW/RJYhZol9SshNznQsx83OOh9G+JWFGoeWxh+xbX+FTdJNoUvGIGbJnpWPY2wC6NOHPw==
|
||||||
|
dependencies:
|
||||||
|
base64url "^3.0.1"
|
||||||
|
oauth "^0.9.15"
|
||||||
|
passport-strategy "^1.0.0"
|
||||||
|
request "^2.88.0"
|
||||||
|
webfinger "^0.4.2"
|
||||||
|
|
||||||
ajv@^6.12.3:
|
ajv@^6.12.3:
|
||||||
version "6.12.6"
|
version "6.12.6"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
||||||
|
@ -71,7 +82,7 @@ base64-js@^1.0.2, base64-js@^1.3.1:
|
||||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||||
|
|
||||||
base64url@3.x.x:
|
base64url@3.x.x, base64url@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
|
resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
|
||||||
integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==
|
integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==
|
||||||
|
@ -574,7 +585,7 @@ oauth-sign@~0.9.0:
|
||||||
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
|
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
|
||||||
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
|
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
|
||||||
|
|
||||||
oauth@0.9.x:
|
oauth@0.9.x, oauth@^0.9.15:
|
||||||
version "0.9.15"
|
version "0.9.15"
|
||||||
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
|
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
|
||||||
integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
|
integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
|
||||||
|
@ -748,7 +759,7 @@ redis-parser@^3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
redis-errors "^1.0.0"
|
redis-errors "^1.0.0"
|
||||||
|
|
||||||
request@^2.72.0, request@^2.74.0:
|
request@^2.72.0, request@^2.74.0, request@^2.88.0:
|
||||||
version "2.88.2"
|
version "2.88.2"
|
||||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
|
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
|
||||||
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
|
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
|
||||||
|
@ -794,7 +805,7 @@ sax@1.2.1:
|
||||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
|
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
|
||||||
integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o=
|
integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o=
|
||||||
|
|
||||||
sax@>=0.6.0:
|
sax@>=0.1.1, sax@>=0.6.0:
|
||||||
version "1.2.4"
|
version "1.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||||
|
@ -829,6 +840,11 @@ standard-as-callback@^2.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
|
resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
|
||||||
integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==
|
integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==
|
||||||
|
|
||||||
|
step@0.0.x:
|
||||||
|
version "0.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/step/-/step-0.0.6.tgz#143e7849a5d7d3f4a088fe29af94915216eeede2"
|
||||||
|
integrity sha1-FD54SaXX0/SgiP4pr5SRUhbu7eI=
|
||||||
|
|
||||||
string-template@~1.0.0:
|
string-template@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96"
|
resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96"
|
||||||
|
@ -943,11 +959,26 @@ verror@1.10.0:
|
||||||
core-util-is "1.0.2"
|
core-util-is "1.0.2"
|
||||||
extsprintf "^1.2.0"
|
extsprintf "^1.2.0"
|
||||||
|
|
||||||
|
webfinger@^0.4.2:
|
||||||
|
version "0.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/webfinger/-/webfinger-0.4.2.tgz#3477a6d97799461896039fcffc650b73468ee76d"
|
||||||
|
integrity sha1-NHem2XeZRhiWA5/P/GULc0aO520=
|
||||||
|
dependencies:
|
||||||
|
step "0.0.x"
|
||||||
|
xml2js "0.1.x"
|
||||||
|
|
||||||
wrappy@1:
|
wrappy@1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||||
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
||||||
|
|
||||||
|
xml2js@0.1.x:
|
||||||
|
version "0.1.14"
|
||||||
|
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.1.14.tgz#5274e67f5a64c5f92974cd85139e0332adc6b90c"
|
||||||
|
integrity sha1-UnTmf1pkxfkpdM2FE54DMq3GuQw=
|
||||||
|
dependencies:
|
||||||
|
sax ">=0.1.1"
|
||||||
|
|
||||||
xml2js@0.4.19:
|
xml2js@0.4.19:
|
||||||
version "0.4.19"
|
version "0.4.19"
|
||||||
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
|
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
|
||||||
|
|
|
@ -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": "0.9.67",
|
"version": "0.9.70",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "0.9.67",
|
"version": "0.9.70",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -65,10 +65,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^0.9.67",
|
"@budibase/bbui": "^0.9.70",
|
||||||
"@budibase/client": "^0.9.67",
|
"@budibase/client": "^0.9.70",
|
||||||
"@budibase/colorpicker": "1.1.2",
|
"@budibase/colorpicker": "1.1.2",
|
||||||
"@budibase/string-templates": "^0.9.67",
|
"@budibase/string-templates": "^0.9.70",
|
||||||
"@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",
|
||||||
|
|
|
@ -98,7 +98,6 @@ const createScreen = table => {
|
||||||
valueType: "Binding",
|
valueType: "Binding",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
limit: 1,
|
|
||||||
paginate: false,
|
paginate: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
let deletion
|
let deletion
|
||||||
|
|
||||||
$: tableOptions = $tables.list.filter(
|
$: tableOptions = $tables.list.filter(
|
||||||
table => table._id !== $tables.draft._id
|
table => table._id !== $tables.draft._id && table.type !== "external"
|
||||||
)
|
)
|
||||||
$: required = !!field?.constraints?.presence || primaryDisplay
|
$: required = !!field?.constraints?.presence || primaryDisplay
|
||||||
$: uneditable =
|
$: uneditable =
|
||||||
|
@ -172,11 +172,6 @@
|
||||||
alt: `Many ${table.name} rows → many ${linkTable.name} rows`,
|
alt: `Many ${table.name} rows → many ${linkTable.name} rows`,
|
||||||
value: RelationshipTypes.MANY_TO_MANY,
|
value: RelationshipTypes.MANY_TO_MANY,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: `One ${linkName} row → many ${thisName} rows`,
|
|
||||||
alt: `One ${linkTable.name} rows → many ${table.name} rows`,
|
|
||||||
value: RelationshipTypes.ONE_TO_MANY,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: `One ${thisName} row → many ${linkName} rows`,
|
name: `One ${thisName} row → many ${linkName} rows`,
|
||||||
alt: `One ${table.name} rows → many ${linkTable.name} rows`,
|
alt: `One ${table.name} rows → many ${linkTable.name} rows`,
|
||||||
|
|
|
@ -6,9 +6,14 @@
|
||||||
import EditViewPopover from "./popovers/EditViewPopover.svelte"
|
import EditViewPopover from "./popovers/EditViewPopover.svelte"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
|
|
||||||
|
const alphabetical = (a, b) => a.name?.toLowerCase() > b.name?.toLowerCase()
|
||||||
|
|
||||||
export let sourceId
|
export let sourceId
|
||||||
|
|
||||||
$: selectedView = $views.selected && $views.selected.name
|
$: selectedView = $views.selected && $views.selected.name
|
||||||
|
$: sortedTables = $tables.list
|
||||||
|
.filter(table => table.sourceId === sourceId)
|
||||||
|
.sort(alphabetical)
|
||||||
|
|
||||||
function selectTable(table) {
|
function selectTable(table) {
|
||||||
tables.select(table)
|
tables.select(table)
|
||||||
|
@ -33,7 +38,7 @@
|
||||||
|
|
||||||
{#if $database?._id}
|
{#if $database?._id}
|
||||||
<div class="hierarchy-items-container">
|
<div class="hierarchy-items-container">
|
||||||
{#each $tables.list.filter(table => table.sourceId === sourceId) as table, idx}
|
{#each sortedTables as table, idx}
|
||||||
<NavItem
|
<NavItem
|
||||||
indentLevel={1}
|
indentLevel={1}
|
||||||
border={idx > 0}
|
border={idx > 0}
|
||||||
|
@ -46,7 +51,7 @@
|
||||||
<EditTablePopover {table} />
|
<EditTablePopover {table} />
|
||||||
{/if}
|
{/if}
|
||||||
</NavItem>
|
</NavItem>
|
||||||
{#each Object.keys(table.views || {}) as viewName, idx (idx)}
|
{#each [...Object.keys(table.views || {})].sort() as viewName, idx (idx)}
|
||||||
<NavItem
|
<NavItem
|
||||||
indentLevel={2}
|
indentLevel={2}
|
||||||
icon="Remove"
|
icon="Remove"
|
||||||
|
|
|
@ -284,7 +284,7 @@ export const background = {
|
||||||
|
|
||||||
export const border = {
|
export const border = {
|
||||||
label: "Border",
|
label: "Border",
|
||||||
columns: "auto 1fr",
|
columns: "1fr 1fr",
|
||||||
settings: [
|
settings: [
|
||||||
{
|
{
|
||||||
label: "Color",
|
label: "Color",
|
||||||
|
@ -295,17 +295,31 @@ export const border = {
|
||||||
label: "Width",
|
label: "Width",
|
||||||
key: "border-width",
|
key: "border-width",
|
||||||
control: Select,
|
control: Select,
|
||||||
|
column: "1 / 2",
|
||||||
|
placeholder: "None",
|
||||||
options: [
|
options: [
|
||||||
{ label: "Small", value: "1px" },
|
{ label: "Small", value: "1px" },
|
||||||
{ label: "Medium", value: "2px" },
|
{ label: "Medium", value: "2px" },
|
||||||
{ label: "Large", value: "4px" },
|
{ label: "Large", value: "4px" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Style",
|
||||||
|
key: "border-style",
|
||||||
|
control: Select,
|
||||||
|
column: "2 / 3",
|
||||||
|
placeholder: "None",
|
||||||
|
options: [
|
||||||
|
{ label: "Solid", value: "solid" },
|
||||||
|
{ label: "Dotted", value: "dotted" },
|
||||||
|
{ label: "Dashed", value: "dashed" },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Radius",
|
label: "Radius",
|
||||||
key: "border-radius",
|
key: "border-radius",
|
||||||
control: Select,
|
control: Select,
|
||||||
column: "1 / 3",
|
placeholder: "None",
|
||||||
options: [
|
options: [
|
||||||
{ label: "Small", value: "0.25rem" },
|
{ label: "Small", value: "0.25rem" },
|
||||||
{ label: "Medium", value: "0.5rem" },
|
{ label: "Medium", value: "0.5rem" },
|
||||||
|
@ -317,7 +331,7 @@ export const border = {
|
||||||
label: "Shadow",
|
label: "Shadow",
|
||||||
key: "box-shadow",
|
key: "box-shadow",
|
||||||
control: Select,
|
control: Select,
|
||||||
column: "1 / 3",
|
placeholder: "None",
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: "Small",
|
label: "Small",
|
||||||
|
|
|
@ -0,0 +1,194 @@
|
||||||
|
<script>
|
||||||
|
import { RelationshipTypes } from "constants/backend"
|
||||||
|
import { Button, Input, ModalContent, Select, Detail } from "@budibase/bbui"
|
||||||
|
import { tables } from "stores/backend"
|
||||||
|
import { uuid } from "builderStore/uuid"
|
||||||
|
|
||||||
|
export let save
|
||||||
|
export let datasource
|
||||||
|
export let plusTables = []
|
||||||
|
export let fromRelationship = {}
|
||||||
|
export let toRelationship = {}
|
||||||
|
export let close
|
||||||
|
|
||||||
|
let originalFromName = fromRelationship.name,
|
||||||
|
originalToName = toRelationship.name
|
||||||
|
|
||||||
|
function isValid(relationship) {
|
||||||
|
if (
|
||||||
|
relationship.relationshipType === RelationshipTypes.MANY_TO_MANY &&
|
||||||
|
!relationship.through
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
relationship.name && relationship.tableId && relationship.relationshipType
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$: tableOptions = plusTables.map(table => ({
|
||||||
|
label: table.name,
|
||||||
|
value: table._id,
|
||||||
|
}))
|
||||||
|
$: fromTable = plusTables.find(table => table._id === toRelationship?.tableId)
|
||||||
|
$: toTable = plusTables.find(table => table._id === fromRelationship?.tableId)
|
||||||
|
$: through = plusTables.find(table => table._id === fromRelationship?.through)
|
||||||
|
$: valid = toTable && fromTable && isValid(fromRelationship)
|
||||||
|
$: linkTable = through || toTable
|
||||||
|
$: relationshipTypes = [
|
||||||
|
{
|
||||||
|
label: "Many",
|
||||||
|
value: RelationshipTypes.MANY_TO_MANY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "One",
|
||||||
|
value: RelationshipTypes.MANY_TO_ONE,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
$: updateRelationshipType(fromRelationship?.relationshipType)
|
||||||
|
|
||||||
|
function updateRelationshipType(fromType) {
|
||||||
|
if (fromType === RelationshipTypes.MANY_TO_MANY) {
|
||||||
|
toRelationship.relationshipType = RelationshipTypes.MANY_TO_MANY
|
||||||
|
} else {
|
||||||
|
toRelationship.relationshipType = RelationshipTypes.MANY_TO_ONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRelationships() {
|
||||||
|
// if any to many only need to check from
|
||||||
|
const manyToMany =
|
||||||
|
fromRelationship.relationshipType === RelationshipTypes.MANY_TO_MANY
|
||||||
|
// main is simply used to know this is the side the user configured it from
|
||||||
|
const id = uuid()
|
||||||
|
if (!manyToMany) {
|
||||||
|
delete fromRelationship.through
|
||||||
|
delete toRelationship.through
|
||||||
|
}
|
||||||
|
let relateFrom = {
|
||||||
|
...fromRelationship,
|
||||||
|
type: "link",
|
||||||
|
main: true,
|
||||||
|
_id: id,
|
||||||
|
}
|
||||||
|
let relateTo = {
|
||||||
|
...toRelationship,
|
||||||
|
type: "link",
|
||||||
|
_id: id,
|
||||||
|
}
|
||||||
|
|
||||||
|
// [0] is because we don't support composite keys for relationships right now
|
||||||
|
if (manyToMany) {
|
||||||
|
relateFrom = {
|
||||||
|
...relateFrom,
|
||||||
|
through: through._id,
|
||||||
|
fieldName: toTable.primary[0],
|
||||||
|
}
|
||||||
|
relateTo = {
|
||||||
|
...relateTo,
|
||||||
|
through: through._id,
|
||||||
|
fieldName: fromTable.primary[0],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
relateFrom = {
|
||||||
|
...relateFrom,
|
||||||
|
foreignKey: relateFrom.fieldName,
|
||||||
|
fieldName: fromTable.primary[0],
|
||||||
|
}
|
||||||
|
relateTo = {
|
||||||
|
...relateTo,
|
||||||
|
relationshipType: RelationshipTypes.ONE_TO_MANY,
|
||||||
|
foreignKey: relateFrom.fieldName,
|
||||||
|
fieldName: fromTable.primary[0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fromRelationship = relateFrom
|
||||||
|
toRelationship = relateTo
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the relationship on to the datasource
|
||||||
|
async function saveRelationship() {
|
||||||
|
buildRelationships()
|
||||||
|
// source of relationship
|
||||||
|
datasource.entities[fromTable.name].schema[fromRelationship.name] =
|
||||||
|
fromRelationship
|
||||||
|
// save other side of relationship in the other schema
|
||||||
|
datasource.entities[toTable.name].schema[toRelationship.name] =
|
||||||
|
toRelationship
|
||||||
|
|
||||||
|
// If relationship has been renamed
|
||||||
|
if (originalFromName !== fromRelationship.name) {
|
||||||
|
delete datasource.entities[fromTable.name].schema[originalFromName]
|
||||||
|
}
|
||||||
|
if (originalToName !== toRelationship.name) {
|
||||||
|
delete datasource.entities[toTable.name].schema[originalToName]
|
||||||
|
}
|
||||||
|
|
||||||
|
await save()
|
||||||
|
await tables.fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteRelationship() {
|
||||||
|
delete datasource.entities[fromTable.name].schema[fromRelationship.name]
|
||||||
|
delete datasource.entities[toTable.name].schema[toRelationship.name]
|
||||||
|
await save()
|
||||||
|
await tables.fetch()
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
title="Create Relationship"
|
||||||
|
confirmText="Save"
|
||||||
|
onConfirm={saveRelationship}
|
||||||
|
disabled={!valid}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
label="Relationship type"
|
||||||
|
options={relationshipTypes}
|
||||||
|
bind:value={fromRelationship.relationshipType}
|
||||||
|
/>
|
||||||
|
<div class="headings">
|
||||||
|
<Detail>Tables</Detail>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
label="Select from table"
|
||||||
|
options={tableOptions}
|
||||||
|
bind:value={toRelationship.tableId}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label={"Select to table"}
|
||||||
|
options={tableOptions}
|
||||||
|
bind:value={fromRelationship.tableId}
|
||||||
|
/>
|
||||||
|
{#if fromRelationship?.relationshipType === RelationshipTypes.MANY_TO_MANY}
|
||||||
|
<Select
|
||||||
|
label={"Through"}
|
||||||
|
options={tableOptions}
|
||||||
|
bind:value={fromRelationship.through}
|
||||||
|
/>
|
||||||
|
{:else if toTable}
|
||||||
|
<Select
|
||||||
|
label={`Foreign Key (${toTable?.name})`}
|
||||||
|
options={Object.keys(toTable?.schema)}
|
||||||
|
bind:value={fromRelationship.fieldName}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<div class="headings">
|
||||||
|
<Detail>Column names</Detail>
|
||||||
|
</div>
|
||||||
|
<Input label="From table column" bind:value={fromRelationship.name} />
|
||||||
|
<Input label="To table column" bind:value={toRelationship.name} />
|
||||||
|
<div slot="footer">
|
||||||
|
{#if originalFromName != null}
|
||||||
|
<Button warning text on:click={deleteRelationship}>Delete</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.headings {
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script>
|
||||||
|
import { Menu, Icon, MenuSection, MenuItem } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let heading
|
||||||
|
export let tables
|
||||||
|
export let selected = false
|
||||||
|
export let select
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Menu>
|
||||||
|
<MenuSection {heading}>
|
||||||
|
{#each tables as table}
|
||||||
|
<MenuItem noClose icon="Table" on:click={() => select(table)}>
|
||||||
|
{table.name}
|
||||||
|
{#if selected}
|
||||||
|
<Icon size="S" name="Checkmark" />
|
||||||
|
{/if}
|
||||||
|
</MenuItem>
|
||||||
|
{/each}
|
||||||
|
</MenuSection>
|
||||||
|
</Menu>
|
|
@ -1,16 +1,70 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto, beforeUrlChange } from "@roxi/routify"
|
import { goto, beforeUrlChange } from "@roxi/routify"
|
||||||
import { Button, Heading, Body, Divider, Layout } from "@budibase/bbui"
|
import { Button, Heading, Body, Divider, Layout, Modal } from "@budibase/bbui"
|
||||||
import { datasources, integrations, queries, tables } from "stores/backend"
|
import { datasources, integrations, queries, tables } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||||
|
import CreateEditRelationship from "./CreateEditRelationship/CreateEditRelationship.svelte"
|
||||||
|
import DisplayColumnModal from "./modals/EditDisplayColumnsModal.svelte"
|
||||||
import ICONS from "components/backend/DatasourceNavigator/icons"
|
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
|
|
||||||
let unsaved = false
|
let unsaved = false
|
||||||
|
let relationshipModal
|
||||||
|
let displayColumnModal
|
||||||
|
let selectedFromRelationship, selectedToRelationship
|
||||||
|
|
||||||
$: datasource = $datasources.list.find(ds => ds._id === $datasources.selected)
|
$: datasource = $datasources.list.find(ds => ds._id === $datasources.selected)
|
||||||
$: integration = datasource && $integrations[datasource.source]
|
$: integration = datasource && $integrations[datasource.source]
|
||||||
|
$: plusTables = datasource?.plus
|
||||||
|
? Object.values(datasource.entities || {})
|
||||||
|
: []
|
||||||
|
$: relationships = getRelationships(plusTables)
|
||||||
|
|
||||||
|
function getRelationships(tables) {
|
||||||
|
if (!tables || !Array.isArray(tables)) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
let pairs = {}
|
||||||
|
for (let table of tables) {
|
||||||
|
for (let column of Object.values(table.schema)) {
|
||||||
|
if (column.type !== "link") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// these relationships have an id to pair them to each other
|
||||||
|
// one has a main for the from side
|
||||||
|
const key = column.main ? "from" : "to"
|
||||||
|
pairs[column._id] = {
|
||||||
|
...pairs[column._id],
|
||||||
|
[key]: column,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pairs
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRelationshipDisplayString(fromCol, toCol) {
|
||||||
|
function getTableName(tableId) {
|
||||||
|
if (!tableId || typeof tableId !== "string") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return plusTables.find(table => table._id === tableId)?.name || "Unknown"
|
||||||
|
}
|
||||||
|
if (!toCol || !fromCol) {
|
||||||
|
return "Cannot build name"
|
||||||
|
}
|
||||||
|
const fromTableName = getTableName(toCol.tableId)
|
||||||
|
const toTableName = getTableName(fromCol.tableId)
|
||||||
|
const throughTableName = getTableName(fromCol.through)
|
||||||
|
|
||||||
|
let displayString
|
||||||
|
if (throughTableName) {
|
||||||
|
displayString = `${fromTableName} through ${throughTableName} → ${toTableName}`
|
||||||
|
} else {
|
||||||
|
displayString = `${fromTableName} → ${toTableName}`
|
||||||
|
}
|
||||||
|
return displayString
|
||||||
|
}
|
||||||
|
|
||||||
async function saveDatasource() {
|
async function saveDatasource() {
|
||||||
try {
|
try {
|
||||||
|
@ -48,6 +102,16 @@
|
||||||
unsaved = true
|
unsaved = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openRelationshipModal(fromRelationship, toRelationship) {
|
||||||
|
selectedFromRelationship = fromRelationship || {}
|
||||||
|
selectedToRelationship = toRelationship || {}
|
||||||
|
relationshipModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDisplayColumnModal() {
|
||||||
|
displayColumnModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
$beforeUrlChange(() => {
|
$beforeUrlChange(() => {
|
||||||
if (unsaved) {
|
if (unsaved) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
|
@ -59,6 +123,21 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={relationshipModal}>
|
||||||
|
<CreateEditRelationship
|
||||||
|
{datasource}
|
||||||
|
save={saveDatasource}
|
||||||
|
close={relationshipModal.hide}
|
||||||
|
{plusTables}
|
||||||
|
fromRelationship={selectedFromRelationship}
|
||||||
|
toRelationship={selectedToRelationship}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal bind:this={displayColumnModal}>
|
||||||
|
<DisplayColumnModal {datasource} {plusTables} save={saveDatasource} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{#if datasource && integration}
|
{#if datasource && integration}
|
||||||
<section>
|
<section>
|
||||||
<Layout>
|
<Layout>
|
||||||
|
@ -92,9 +171,18 @@
|
||||||
<Divider />
|
<Divider />
|
||||||
<div class="query-header">
|
<div class="query-header">
|
||||||
<Heading size="S">Tables</Heading>
|
<Heading size="S">Tables</Heading>
|
||||||
<Button primary on:click={updateDatasourceSchema}
|
<div class="table-buttons">
|
||||||
>Fetch Tables From Database</Button
|
{#if plusTables && plusTables.length !== 0}
|
||||||
>
|
<Button primary on:click={openDisplayColumnModal}>
|
||||||
|
Update display columns
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<Button primary on:click={updateDatasourceSchema}>
|
||||||
|
Fetch tables from database
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Body>
|
<Body>
|
||||||
This datasource can determine tables automatically. Budibase can fetch
|
This datasource can determine tables automatically. Budibase can fetch
|
||||||
|
@ -102,18 +190,44 @@
|
||||||
having to write any queries at all.
|
having to write any queries at all.
|
||||||
</Body>
|
</Body>
|
||||||
<div class="query-list">
|
<div class="query-list">
|
||||||
{#if datasource.entities}
|
{#each plusTables as table}
|
||||||
{#each Object.keys(datasource.entities) as entity}
|
<div class="query-list-item" on:click={() => onClickTable(table)}>
|
||||||
<div
|
<p class="query-name">{table.name}</p>
|
||||||
class="query-list-item"
|
<p>Primary Key: {table.primary}</p>
|
||||||
on:click={() => onClickTable(datasource.entities[entity])}
|
|
||||||
>
|
|
||||||
<p class="query-name">{entity}</p>
|
|
||||||
<p>Primary Key: {datasource.entities[entity].primary}</p>
|
|
||||||
<p>→</p>
|
<p>→</p>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#if plusTables?.length !== 0}
|
||||||
|
<Divider />
|
||||||
|
<div class="query-header">
|
||||||
|
<Heading size="S">Relationships</Heading>
|
||||||
|
<Button primary on:click={() => openRelationshipModal()}
|
||||||
|
>Create relationship</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<Body>
|
||||||
|
Tell budibase how your tables are related to get even more smart
|
||||||
|
features.
|
||||||
|
</Body>
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="query-list">
|
||||||
|
{#each Object.values(relationships) as relationship}
|
||||||
|
<div
|
||||||
|
class="query-list-item"
|
||||||
|
on:click={() =>
|
||||||
|
openRelationshipModal(relationship.from, relationship.to)}
|
||||||
|
>
|
||||||
|
<p class="query-name">
|
||||||
|
{buildRelationshipDisplayString(
|
||||||
|
relationship.from,
|
||||||
|
relationship.to
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p>{relationship.from?.name} to {relationship.to?.name}</p>
|
||||||
|
<p>→</p>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<Divider />
|
<Divider />
|
||||||
|
@ -202,4 +316,14 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-buttons {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-buttons div {
|
||||||
|
grid-column-end: -1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
<script>
|
||||||
|
import { ModalContent, Select, Body } from "@budibase/bbui"
|
||||||
|
import { tables } from "stores/backend"
|
||||||
|
|
||||||
|
export let datasource
|
||||||
|
export let plusTables
|
||||||
|
export let save
|
||||||
|
|
||||||
|
async function saveDisplayColumns() {
|
||||||
|
// be explicit about copying over
|
||||||
|
for (let table of plusTables) {
|
||||||
|
datasource.entities[table.name].primaryDisplay = table.primaryDisplay
|
||||||
|
}
|
||||||
|
save()
|
||||||
|
await tables.fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColumnOptions(table) {
|
||||||
|
if (!table || !table.schema) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return Object.entries(table.schema)
|
||||||
|
.filter(field => field[1].type !== "link")
|
||||||
|
.map(([fieldName]) => fieldName)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
title="Edit display columns"
|
||||||
|
confirmText="Save"
|
||||||
|
onConfirm={saveDisplayColumns}
|
||||||
|
>
|
||||||
|
<Body
|
||||||
|
>Select the columns that will be shown when displaying relationships.</Body
|
||||||
|
>
|
||||||
|
{#each plusTables as table}
|
||||||
|
<Select
|
||||||
|
label={table.name}
|
||||||
|
options={getColumnOptions(table)}
|
||||||
|
bind:value={table.primaryDisplay}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</ModalContent>
|
|
@ -0,0 +1,32 @@
|
||||||
|
<script>
|
||||||
|
import { ActionButton } from "@budibase/bbui"
|
||||||
|
// import { admin } from "stores/portal"
|
||||||
|
|
||||||
|
let show = true
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if show}
|
||||||
|
<ActionButton on:click={() => window.open("/api/admin/auth/oidc", "_blank")}>
|
||||||
|
<div class="inner">
|
||||||
|
<p>Sign in with OIDC</p>
|
||||||
|
</div>
|
||||||
|
</ActionButton>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<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>
|
|
@ -12,7 +12,7 @@
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import { auth, organisation } from "stores/portal"
|
import { auth, organisation } from "stores/portal"
|
||||||
import GoogleButton from "./_components/GoogleButton.svelte"
|
import GoogleButton from "./_components/GoogleButton.svelte"
|
||||||
import OidcButton from "./_components/OidcButton.svelte"
|
import OIDCButton from "./_components/OIDCButton.svelte"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@
|
||||||
<Heading>Sign in to {company}</Heading>
|
<Heading>Sign in to {company}</Heading>
|
||||||
</Layout>
|
</Layout>
|
||||||
<GoogleButton />
|
<GoogleButton />
|
||||||
<OidcButton />
|
<OIDCButton />
|
||||||
<Divider noGrid />
|
<Divider noGrid />
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Body size="S" textAlign="center">Sign in with email</Body>
|
<Body size="S" textAlign="center">Sign in with email</Body>
|
||||||
|
|
|
@ -60,6 +60,16 @@
|
||||||
|
|
||||||
let toggleDisabled = false
|
let toggleDisabled = false
|
||||||
|
|
||||||
|
async function updateUserFirstName(evt) {
|
||||||
|
await users.save({ ...$userFetch?.data, firstName: evt.target.value })
|
||||||
|
await userFetch.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateUserLastName(evt) {
|
||||||
|
await users.save({ ...$userFetch?.data, lastName: evt.target.value })
|
||||||
|
await userFetch.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
async function toggleFlag(flagName, detail) {
|
async function toggleFlag(flagName, detail) {
|
||||||
toggleDisabled = true
|
toggleDisabled = true
|
||||||
await users.save({ ...$userFetch?.data, [flagName]: { global: detail } })
|
await users.save({ ...$userFetch?.data, [flagName]: { global: detail } })
|
||||||
|
@ -113,11 +123,19 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Label size="L">First name</Label>
|
<Label size="L">First name</Label>
|
||||||
<Input disabled thin value={$userFetch?.data?.firstName} />
|
<Input
|
||||||
|
thin
|
||||||
|
value={$userFetch?.data?.firstName}
|
||||||
|
on:blur={updateUserFirstName}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Label size="L">Last name</Label>
|
<Label size="L">Last name</Label>
|
||||||
<Input disabled thin value={$userFetch?.data?.lastName} />
|
<Input
|
||||||
|
thin
|
||||||
|
value={$userFetch?.data?.lastName}
|
||||||
|
on:blur={updateUserLastName}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- don't let a user remove the privileges that let them be here -->
|
<!-- don't let a user remove the privileges that let them be here -->
|
||||||
{#if userId !== $auth.user._id}
|
{#if userId !== $auth.user._id}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
const roles = app.roles
|
const roles = app.roles
|
||||||
let options = roles.map(role => role._id)
|
let options = roles.map(role => ({ value: role._id, label: role.name }))
|
||||||
let selectedRole = user?.roles?.[app?._id]
|
let selectedRole = user?.roles?.[app?._id]
|
||||||
|
|
||||||
async function updateUserRoles() {
|
async function updateUserRoles() {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "0.9.67",
|
"version": "0.9.70",
|
||||||
"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": {
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
"docker-compose": "^0.23.6",
|
"docker-compose": "^0.23.6",
|
||||||
"inquirer": "^8.0.0",
|
"inquirer": "^8.0.0",
|
||||||
"lookpath": "^1.1.0",
|
"lookpath": "^1.1.0",
|
||||||
"pkg": "^4.4.9",
|
"pkg": "^5.3.0",
|
||||||
"posthog-node": "1.0.7",
|
"posthog-node": "1.0.7",
|
||||||
"randomstring": "^1.1.5"
|
"randomstring": "^1.1.5"
|
||||||
},
|
},
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "0.9.67",
|
"version": "0.9.70",
|
||||||
"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",
|
||||||
|
@ -18,9 +18,9 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^0.9.67",
|
"@budibase/bbui": "^0.9.70",
|
||||||
"@budibase/standard-components": "^0.9.67",
|
"@budibase/standard-components": "^0.9.70",
|
||||||
"@budibase/string-templates": "^0.9.67",
|
"@budibase/string-templates": "^0.9.70",
|
||||||
"regexparam": "^1.3.0",
|
"regexparam": "^1.3.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"svelte-spa-router": "^3.0.5"
|
"svelte-spa-router": "^3.0.5"
|
||||||
|
|
|
@ -34,11 +34,6 @@ export const styleable = (node, styles = {}) => {
|
||||||
baseStyles.overflow = "hidden"
|
baseStyles.overflow = "hidden"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append border-style css if border-width is specified
|
|
||||||
if (newStyles.normal?.["border-width"]) {
|
|
||||||
baseStyles["border-style"] = "solid"
|
|
||||||
}
|
|
||||||
|
|
||||||
const componentId = newStyles.id
|
const componentId = newStyles.id
|
||||||
const customStyles = newStyles.custom || ""
|
const customStyles = newStyles.custom || ""
|
||||||
const normalStyles = { ...baseStyles, ...newStyles.normal }
|
const normalStyles = { ...baseStyles, ...newStyles.normal }
|
||||||
|
|
|
@ -5,10 +5,13 @@
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
|
"name": "Start Server",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Start Server",
|
"runtimeExecutable": "node",
|
||||||
"program": "${workspaceFolder}/src/index.js"
|
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
|
||||||
|
"args": ["src/index.ts"],
|
||||||
|
"cwd": "${workspaceRoot}",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "0.9.67",
|
"version": "0.9.70",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -59,9 +59,9 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/auth": "^0.9.67",
|
"@budibase/auth": "^0.9.70",
|
||||||
"@budibase/client": "^0.9.67",
|
"@budibase/client": "^0.9.70",
|
||||||
"@budibase/string-templates": "^0.9.67",
|
"@budibase/string-templates": "^0.9.70",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
"@koa/router": "8.0.0",
|
"@koa/router": "8.0.0",
|
||||||
"@sendgrid/mail": "7.1.1",
|
"@sendgrid/mail": "7.1.1",
|
||||||
|
@ -114,7 +114,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.14.3",
|
"@babel/core": "^7.14.3",
|
||||||
"@babel/preset-env": "^7.14.4",
|
"@babel/preset-env": "^7.14.4",
|
||||||
"@budibase/standard-components": "^0.9.67",
|
"@budibase/standard-components": "^0.9.70",
|
||||||
"@jest/test-sequencer": "^24.8.0",
|
"@jest/test-sequencer": "^24.8.0",
|
||||||
"@types/bull": "^3.15.1",
|
"@types/bull": "^3.15.1",
|
||||||
"@types/jest": "^26.0.23",
|
"@types/jest": "^26.0.23",
|
||||||
|
|
|
@ -15,3 +15,28 @@ CREATE TABLE Tasks (
|
||||||
FOREIGN KEY(PersonID)
|
FOREIGN KEY(PersonID)
|
||||||
REFERENCES Persons(PersonID)
|
REFERENCES Persons(PersonID)
|
||||||
);
|
);
|
||||||
|
CREATE TABLE Products (
|
||||||
|
ProductID INT NOT NULL PRIMARY KEY,
|
||||||
|
ProductName varchar(255)
|
||||||
|
);
|
||||||
|
CREATE TABLE Products_Tasks (
|
||||||
|
ProductID INT NOT NULL,
|
||||||
|
TaskID INT NOT NULL,
|
||||||
|
CONSTRAINT fkProducts
|
||||||
|
FOREIGN KEY(ProductID)
|
||||||
|
REFERENCES Products(ProductID),
|
||||||
|
CONSTRAINT fkTasks
|
||||||
|
FOREIGN KEY(TaskID)
|
||||||
|
REFERENCES Tasks(TaskID),
|
||||||
|
PRIMARY KEY (ProductID, TaskID)
|
||||||
|
);
|
||||||
|
INSERT INTO Persons (PersonID, FirstName, LastName, Address, City) VALUES (1, 'Mike', 'Hughes', '123 Fake Street', 'Belfast');
|
||||||
|
INSERT INTO Tasks (TaskID, PersonID, TaskName) VALUES (1, 1, 'assembling');
|
||||||
|
INSERT INTO Tasks (TaskID, PersonID, TaskName) VALUES (2, 1, 'processing');
|
||||||
|
INSERT INTO Products (ProductID, ProductName) VALUES (1, 'Computers');
|
||||||
|
INSERT INTO Products (ProductID, ProductName) VALUES (2, 'Laptops');
|
||||||
|
INSERT INTO Products (ProductID, ProductName) VALUES (3, 'Chairs');
|
||||||
|
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (1, 1);
|
||||||
|
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (2, 1);
|
||||||
|
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (3, 1);
|
||||||
|
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (1, 2);
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
docker-compose down
|
||||||
|
docker volume prune -f
|
|
@ -48,7 +48,7 @@ exports.buildSchemaFromDb = async function (ctx) {
|
||||||
|
|
||||||
// Connect to the DB and build the schema
|
// Connect to the DB and build the schema
|
||||||
const connector = new Connector(datasource.config)
|
const connector = new Connector(datasource.config)
|
||||||
await connector.buildSchema(datasource._id)
|
await connector.buildSchema(datasource._id, datasource.entities)
|
||||||
datasource.entities = connector.tables
|
datasource.entities = connector.tables
|
||||||
|
|
||||||
const response = await db.post(datasource)
|
const response = await db.post(datasource)
|
||||||
|
|
|
@ -0,0 +1,525 @@
|
||||||
|
import {
|
||||||
|
Operation,
|
||||||
|
SearchFilters,
|
||||||
|
SortJson,
|
||||||
|
PaginationJson,
|
||||||
|
RelationshipsJson,
|
||||||
|
} from "../../../definitions/datasource"
|
||||||
|
import {
|
||||||
|
Row,
|
||||||
|
Table,
|
||||||
|
FieldSchema,
|
||||||
|
Datasource,
|
||||||
|
} from "../../../definitions/common"
|
||||||
|
import {
|
||||||
|
breakRowIdField,
|
||||||
|
generateRowIdField,
|
||||||
|
} from "../../../integrations/utils"
|
||||||
|
|
||||||
|
interface ManyRelationship {
|
||||||
|
tableId?: string
|
||||||
|
id?: string
|
||||||
|
isUpdate?: boolean
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RunConfig {
|
||||||
|
id: string
|
||||||
|
row: Row
|
||||||
|
filters: SearchFilters
|
||||||
|
sort: SortJson
|
||||||
|
paginate: PaginationJson
|
||||||
|
}
|
||||||
|
|
||||||
|
module External {
|
||||||
|
const { makeExternalQuery } = require("./utils")
|
||||||
|
const { DataSourceOperation, FieldTypes } = require("../../../constants")
|
||||||
|
const { breakExternalTableId, isSQL } = require("../../../integrations/utils")
|
||||||
|
const { processObjectSync } = require("@budibase/string-templates")
|
||||||
|
const { cloneDeep } = require("lodash/fp")
|
||||||
|
const { isEqual } = require("lodash")
|
||||||
|
const CouchDB = require("../../../db")
|
||||||
|
|
||||||
|
function buildFilters(
|
||||||
|
id: string | undefined,
|
||||||
|
filters: SearchFilters,
|
||||||
|
table: Table
|
||||||
|
) {
|
||||||
|
const primary = table.primary
|
||||||
|
// if passed in array need to copy for shifting etc
|
||||||
|
let idCopy = cloneDeep(id)
|
||||||
|
if (filters) {
|
||||||
|
// need to map over the filters and make sure the _id field isn't present
|
||||||
|
for (let filter of Object.values(filters)) {
|
||||||
|
if (filter._id && primary) {
|
||||||
|
const parts = breakRowIdField(filter._id)
|
||||||
|
for (let field of primary) {
|
||||||
|
filter[field] = parts.shift()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// make sure this field doesn't exist on any filter
|
||||||
|
delete filter._id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// there is no id, just use the user provided filters
|
||||||
|
if (!idCopy || !table) {
|
||||||
|
return filters
|
||||||
|
}
|
||||||
|
// if used as URL parameter it will have been joined
|
||||||
|
if (!Array.isArray(idCopy)) {
|
||||||
|
idCopy = breakRowIdField(idCopy)
|
||||||
|
}
|
||||||
|
const equal: any = {}
|
||||||
|
if (primary && idCopy) {
|
||||||
|
for (let field of primary) {
|
||||||
|
// work through the ID and get the parts
|
||||||
|
equal[field] = idCopy.shift()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
equal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateIdForRow(row: Row, table: Table): string {
|
||||||
|
const primary = table.primary
|
||||||
|
if (!row || !primary) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// build id array
|
||||||
|
let idParts = []
|
||||||
|
for (let field of primary) {
|
||||||
|
if (row[field]) {
|
||||||
|
idParts.push(row[field])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (idParts.length === 0) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return generateRowIdField(idParts)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEndpoint(tableId: string | undefined, operation: string) {
|
||||||
|
if (!tableId) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
const { datasourceId, tableName } = breakExternalTableId(tableId)
|
||||||
|
return {
|
||||||
|
datasourceId,
|
||||||
|
entityId: tableName,
|
||||||
|
operation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function basicProcessing(row: Row, table: Table) {
|
||||||
|
const thisRow: { [key: string]: any } = {}
|
||||||
|
// filter the row down to what is actually the row (not joined)
|
||||||
|
for (let fieldName of Object.keys(table.schema)) {
|
||||||
|
thisRow[fieldName] = row[fieldName]
|
||||||
|
}
|
||||||
|
thisRow._id = generateIdForRow(row, table)
|
||||||
|
thisRow.tableId = table._id
|
||||||
|
thisRow._rev = "rev"
|
||||||
|
return thisRow
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMany(field: FieldSchema) {
|
||||||
|
return (
|
||||||
|
field.relationshipType && field.relationshipType.split("-")[0] === "many"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExternalRequest {
|
||||||
|
private readonly appId: string
|
||||||
|
private operation: Operation
|
||||||
|
private tableId: string
|
||||||
|
private datasource: Datasource
|
||||||
|
private tables: { [key: string]: Table } = {}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
appId: string,
|
||||||
|
operation: Operation,
|
||||||
|
tableId: string,
|
||||||
|
datasource: Datasource
|
||||||
|
) {
|
||||||
|
this.appId = appId
|
||||||
|
this.operation = operation
|
||||||
|
this.tableId = tableId
|
||||||
|
this.datasource = datasource
|
||||||
|
if (datasource && datasource.entities) {
|
||||||
|
this.tables = datasource.entities
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputProcessing(row: Row, table: Table) {
|
||||||
|
if (!row) {
|
||||||
|
return { row, manyRelationships: [] }
|
||||||
|
}
|
||||||
|
// we don't really support composite keys for relationships, this is why [0] is used
|
||||||
|
// @ts-ignore
|
||||||
|
const tablePrimary: string = table.primary[0]
|
||||||
|
let newRow: Row = {},
|
||||||
|
manyRelationships: ManyRelationship[] = []
|
||||||
|
for (let [key, field] of Object.entries(table.schema)) {
|
||||||
|
// if set already, or not set just skip it
|
||||||
|
if (!row[key] || newRow[key]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// if its not a link then just copy it over
|
||||||
|
if (field.type !== FieldTypes.LINK) {
|
||||||
|
newRow[key] = row[key]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const { tableName: linkTableName } = breakExternalTableId(field.tableId)
|
||||||
|
// table has to exist for many to many
|
||||||
|
if (!this.tables[linkTableName]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const linkTable = this.tables[linkTableName]
|
||||||
|
// @ts-ignore
|
||||||
|
const linkTablePrimary = linkTable.primary[0]
|
||||||
|
if (!isMany(field)) {
|
||||||
|
newRow[field.foreignKey || linkTablePrimary] = breakRowIdField(
|
||||||
|
row[key][0]
|
||||||
|
)[0]
|
||||||
|
} else {
|
||||||
|
// we're not inserting a doc, will be a bunch of update calls
|
||||||
|
const isUpdate = !field.through
|
||||||
|
const thisKey: string = isUpdate ? "id" : linkTablePrimary
|
||||||
|
// @ts-ignore
|
||||||
|
const otherKey: string = isUpdate ? field.foreignKey : tablePrimary
|
||||||
|
row[key].map((relationship: any) => {
|
||||||
|
// we don't really support composite keys for relationships, this is why [0] is used
|
||||||
|
manyRelationships.push({
|
||||||
|
tableId: field.through || field.tableId,
|
||||||
|
isUpdate,
|
||||||
|
[thisKey]: breakRowIdField(relationship)[0],
|
||||||
|
// leave the ID for enrichment later
|
||||||
|
[otherKey]: `{{ literal ${tablePrimary} }}`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// we return the relationships that may need to be created in the through table
|
||||||
|
// we do this so that if the ID is generated by the DB it can be inserted
|
||||||
|
// after the fact
|
||||||
|
return { row: newRow, manyRelationships }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This iterates through the returned rows and works out what elements of the rows
|
||||||
|
* actually match up to another row (based on primary keys) - this is pretty specific
|
||||||
|
* to SQL and the way that SQL relationships are returned based on joins.
|
||||||
|
*/
|
||||||
|
updateRelationshipColumns(
|
||||||
|
row: Row,
|
||||||
|
rows: { [key: string]: Row },
|
||||||
|
relationships: RelationshipsJson[]
|
||||||
|
) {
|
||||||
|
const columns: { [key: string]: any } = {}
|
||||||
|
for (let relationship of relationships) {
|
||||||
|
const linkedTable = this.tables[relationship.tableName]
|
||||||
|
if (!linkedTable) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let linked = basicProcessing(row, linkedTable)
|
||||||
|
if (!linked._id) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// if not returning full docs then get the minimal links out
|
||||||
|
const display = linkedTable.primaryDisplay
|
||||||
|
linked = {
|
||||||
|
primaryDisplay: display ? linked[display] : undefined,
|
||||||
|
_id: linked._id,
|
||||||
|
}
|
||||||
|
columns[relationship.column] = linked
|
||||||
|
}
|
||||||
|
for (let [column, related] of Object.entries(columns)) {
|
||||||
|
if (!row._id) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const rowId: string = row._id
|
||||||
|
if (!Array.isArray(rows[rowId][column])) {
|
||||||
|
rows[rowId][column] = []
|
||||||
|
}
|
||||||
|
// make sure relationship hasn't been found already
|
||||||
|
if (
|
||||||
|
!rows[rowId][column].find(
|
||||||
|
(relation: Row) => relation._id === related._id
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
rows[rowId][column].push(related)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
outputProcessing(
|
||||||
|
rows: Row[],
|
||||||
|
table: Table,
|
||||||
|
relationships: RelationshipsJson[]
|
||||||
|
) {
|
||||||
|
if (rows[0].read === true) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let finalRows: { [key: string]: Row } = {}
|
||||||
|
for (let row of rows) {
|
||||||
|
const rowId = generateIdForRow(row, table)
|
||||||
|
row._id = rowId
|
||||||
|
// this is a relationship of some sort
|
||||||
|
if (finalRows[rowId]) {
|
||||||
|
finalRows = this.updateRelationshipColumns(
|
||||||
|
row,
|
||||||
|
finalRows,
|
||||||
|
relationships
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const thisRow = basicProcessing(row, table)
|
||||||
|
finalRows[thisRow._id] = thisRow
|
||||||
|
// do this at end once its been added to the final rows
|
||||||
|
finalRows = this.updateRelationshipColumns(
|
||||||
|
row,
|
||||||
|
finalRows,
|
||||||
|
relationships
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return Object.values(finalRows)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the list of relationship JSON structures based on the columns in the table,
|
||||||
|
* this will be used by the underlying library to build whatever relationship mechanism
|
||||||
|
* it has (e.g. SQL joins).
|
||||||
|
*/
|
||||||
|
buildRelationships(table: Table): RelationshipsJson[] {
|
||||||
|
const relationships = []
|
||||||
|
for (let [fieldName, field] of Object.entries(table.schema)) {
|
||||||
|
if (field.type !== FieldTypes.LINK) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const { tableName: linkTableName } = breakExternalTableId(field.tableId)
|
||||||
|
// no table to link to, this is not a valid relationships
|
||||||
|
if (!this.tables[linkTableName]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const linkTable = this.tables[linkTableName]
|
||||||
|
if (!table.primary || !linkTable.primary) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const definition = {
|
||||||
|
// if no foreign key specified then use the name of the field in other table
|
||||||
|
from: field.foreignKey || table.primary[0],
|
||||||
|
to: field.fieldName,
|
||||||
|
tableName: linkTableName,
|
||||||
|
through: undefined,
|
||||||
|
// need to specify where to put this back into
|
||||||
|
column: fieldName,
|
||||||
|
}
|
||||||
|
if (field.through) {
|
||||||
|
const { tableName: throughTableName } = breakExternalTableId(
|
||||||
|
field.through
|
||||||
|
)
|
||||||
|
definition.through = throughTableName
|
||||||
|
// don't support composite keys for relationships
|
||||||
|
definition.from = table.primary[0]
|
||||||
|
definition.to = linkTable.primary[0]
|
||||||
|
}
|
||||||
|
relationships.push(definition)
|
||||||
|
}
|
||||||
|
return relationships
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a cached lookup, of relationship records, this is mainly for creating/deleting junction
|
||||||
|
* information.
|
||||||
|
*/
|
||||||
|
async lookup(
|
||||||
|
row: Row,
|
||||||
|
relationship: ManyRelationship,
|
||||||
|
cache: { [key: string]: Row[] } = {}
|
||||||
|
) {
|
||||||
|
const { tableId, isUpdate, id, ...rest } = relationship
|
||||||
|
const { tableName } = breakExternalTableId(tableId)
|
||||||
|
const table = this.tables[tableName]
|
||||||
|
if (isUpdate) {
|
||||||
|
return { rows: [], table }
|
||||||
|
}
|
||||||
|
// if not updating need to make sure we have a list of all possible options
|
||||||
|
let fullKey: string = tableId + "/",
|
||||||
|
rowKey: string = ""
|
||||||
|
for (let key of Object.keys(rest)) {
|
||||||
|
if (row[key]) {
|
||||||
|
fullKey += key
|
||||||
|
rowKey = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cache[fullKey] == null) {
|
||||||
|
cache[fullKey] = await makeExternalQuery(this.appId, {
|
||||||
|
endpoint: getEndpoint(tableId, DataSourceOperation.READ),
|
||||||
|
filters: {
|
||||||
|
equal: {
|
||||||
|
[rowKey]: row[rowKey],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return { rows: cache[fullKey], table }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Once a row has been written we may need to update a many field, e.g. updating foreign keys
|
||||||
|
* in a bunch of rows in another table, or inserting/deleting rows from a junction table (many to many).
|
||||||
|
* This is quite a complex process and is handled by this function, there are a few things going on here:
|
||||||
|
* 1. If updating foreign keys its relatively simple, just create a filter for the row that needs updated
|
||||||
|
* and write the various components.
|
||||||
|
* 2. If junction table, then we lookup what exists already, write what doesn't exist, work out what
|
||||||
|
* isn't supposed to exist anymore and delete those. This is better than the usual method of delete them
|
||||||
|
* all and then re-create, as theres no chance of losing data (e.g. delete succeed, but write fail).
|
||||||
|
*/
|
||||||
|
async handleManyRelationships(row: Row, relationships: ManyRelationship[]) {
|
||||||
|
const { appId } = this
|
||||||
|
if (relationships.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// if we're creating (in a through table) need to wipe the existing ones first
|
||||||
|
const promises = []
|
||||||
|
const cache: { [key: string]: Row[] } = {}
|
||||||
|
for (let relationship of relationships) {
|
||||||
|
const { tableId, isUpdate, id, ...rest } = relationship
|
||||||
|
const body = processObjectSync(rest, row)
|
||||||
|
const { table, rows } = await this.lookup(row, relationship, cache)
|
||||||
|
const found = rows.find(row => isEqual(body, row))
|
||||||
|
const operation = isUpdate
|
||||||
|
? DataSourceOperation.UPDATE
|
||||||
|
: DataSourceOperation.CREATE
|
||||||
|
if (!found) {
|
||||||
|
promises.push(
|
||||||
|
makeExternalQuery(appId, {
|
||||||
|
endpoint: getEndpoint(tableId, operation),
|
||||||
|
// if we're doing many relationships then we're writing, only one response
|
||||||
|
body,
|
||||||
|
filters: buildFilters(id, {}, table),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// remove the relationship from the rows
|
||||||
|
rows.splice(rows.indexOf(found), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// finally if creating, cleanup any rows that aren't supposed to be here
|
||||||
|
for (let [key, rows] of Object.entries(cache)) {
|
||||||
|
// @ts-ignore
|
||||||
|
const tableId: string = key.split("/").shift()
|
||||||
|
const { tableName } = breakExternalTableId(tableId)
|
||||||
|
const table = this.tables[tableName]
|
||||||
|
for (let row of rows) {
|
||||||
|
promises.push(
|
||||||
|
makeExternalQuery(this.appId, {
|
||||||
|
endpoint: getEndpoint(tableId, DataSourceOperation.DELETE),
|
||||||
|
filters: buildFilters(generateIdForRow(row, table), {}, table),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is a bit crazy, but the exact purpose of it is to protect against the scenario in which
|
||||||
|
* you have column overlap in relationships, e.g. we join a few different tables and they all have the
|
||||||
|
* concept of an ID, but for some of them it will be null (if they say don't have a relationship).
|
||||||
|
* Creating the specific list of fields that we desire, and excluding the ones that are no use to us
|
||||||
|
* is more performant and has the added benefit of protecting against this scenario.
|
||||||
|
*/
|
||||||
|
buildFields(table: Table) {
|
||||||
|
function extractNonLinkFieldNames(table: Table, existing: string[] = []) {
|
||||||
|
return Object.entries(table.schema)
|
||||||
|
.filter(
|
||||||
|
column =>
|
||||||
|
column[1].type !== FieldTypes.LINK &&
|
||||||
|
!existing.find((field: string) => field.includes(column[0]))
|
||||||
|
)
|
||||||
|
.map(column => `${table.name}.${column[0]}`)
|
||||||
|
}
|
||||||
|
let fields = extractNonLinkFieldNames(table)
|
||||||
|
for (let field of Object.values(table.schema)) {
|
||||||
|
if (field.type !== FieldTypes.LINK) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const { tableName: linkTableName } = breakExternalTableId(field.tableId)
|
||||||
|
const linkTable = this.tables[linkTableName]
|
||||||
|
if (linkTable) {
|
||||||
|
const linkedFields = extractNonLinkFieldNames(linkTable, fields)
|
||||||
|
fields = fields.concat(linkedFields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
async run({ id, row, filters, sort, paginate }: RunConfig) {
|
||||||
|
const { appId, operation, tableId } = this
|
||||||
|
let { datasourceId, tableName } = breakExternalTableId(tableId)
|
||||||
|
if (!this.datasource) {
|
||||||
|
const db = new CouchDB(appId)
|
||||||
|
this.datasource = await db.get(datasourceId)
|
||||||
|
if (!this.datasource || !this.datasource.entities) {
|
||||||
|
throw "No tables found, fetch tables before query."
|
||||||
|
}
|
||||||
|
this.tables = this.datasource.entities
|
||||||
|
}
|
||||||
|
const table = this.tables[tableName]
|
||||||
|
let isSql = isSQL(this.datasource)
|
||||||
|
if (!table) {
|
||||||
|
throw `Unable to process query, table "${tableName}" not defined.`
|
||||||
|
}
|
||||||
|
// clean up row on ingress using schema
|
||||||
|
filters = buildFilters(id, filters, table)
|
||||||
|
const relationships = this.buildRelationships(table)
|
||||||
|
const processed = this.inputProcessing(row, table)
|
||||||
|
row = processed.row
|
||||||
|
if (
|
||||||
|
operation === DataSourceOperation.DELETE &&
|
||||||
|
(filters == null || Object.keys(filters).length === 0)
|
||||||
|
) {
|
||||||
|
throw "Deletion must be filtered"
|
||||||
|
}
|
||||||
|
let json = {
|
||||||
|
endpoint: {
|
||||||
|
datasourceId,
|
||||||
|
entityId: tableName,
|
||||||
|
operation,
|
||||||
|
},
|
||||||
|
resource: {
|
||||||
|
// have to specify the fields to avoid column overlap (for SQL)
|
||||||
|
fields: isSql ? this.buildFields(table) : [],
|
||||||
|
},
|
||||||
|
filters,
|
||||||
|
sort,
|
||||||
|
paginate,
|
||||||
|
relationships,
|
||||||
|
body: row,
|
||||||
|
// pass an id filter into extra, purely for mysql/returning
|
||||||
|
extra: {
|
||||||
|
idFilter: buildFilters(id || generateIdForRow(row, table), {}, table),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// can't really use response right now
|
||||||
|
const response = await makeExternalQuery(appId, json)
|
||||||
|
// handle many to many relationships now if we know the ID (could be auto increment)
|
||||||
|
if (processed.manyRelationships) {
|
||||||
|
await this.handleManyRelationships(
|
||||||
|
response[0],
|
||||||
|
processed.manyRelationships
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const output = this.outputProcessing(response, table, relationships)
|
||||||
|
// if reading it'll just be an array of rows, return whole thing
|
||||||
|
return operation === DataSourceOperation.READ && Array.isArray(response)
|
||||||
|
? output
|
||||||
|
: { row: output[0], table }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ExternalRequest
|
||||||
|
}
|
|
@ -1,136 +1,19 @@
|
||||||
const { makeExternalQuery } = require("./utils")
|
const {
|
||||||
const { DataSourceOperation, SortDirection } = require("../../../constants")
|
DataSourceOperation,
|
||||||
const { getExternalTable } = require("../table/utils")
|
SortDirection,
|
||||||
|
FieldTypes,
|
||||||
|
} = require("../../../constants")
|
||||||
const {
|
const {
|
||||||
breakExternalTableId,
|
breakExternalTableId,
|
||||||
generateRowIdField,
|
|
||||||
breakRowIdField,
|
breakRowIdField,
|
||||||
} = require("../../../integrations/utils")
|
} = require("../../../integrations/utils")
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const ExternalRequest = require("./ExternalRequest")
|
||||||
|
const CouchDB = require("../../../db")
|
||||||
|
|
||||||
function inputProcessing(row, table) {
|
async function handleRequest(appId, operation, tableId, opts = {}) {
|
||||||
if (!row) {
|
return new ExternalRequest(appId, operation, tableId, opts.datasource).run(
|
||||||
return row
|
opts
|
||||||
}
|
)
|
||||||
let newRow = {}
|
|
||||||
for (let key of Object.keys(table.schema)) {
|
|
||||||
// currently excludes empty strings
|
|
||||||
if (row[key]) {
|
|
||||||
newRow[key] = row[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newRow
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateIdForRow(row, table) {
|
|
||||||
if (!row) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const primary = table.primary
|
|
||||||
// build id array
|
|
||||||
let idParts = []
|
|
||||||
for (let field of primary) {
|
|
||||||
idParts.push(row[field])
|
|
||||||
}
|
|
||||||
return generateRowIdField(idParts)
|
|
||||||
}
|
|
||||||
|
|
||||||
function outputProcessing(rows, table) {
|
|
||||||
// if no rows this is what is returned? Might be PG only
|
|
||||||
if (rows[0].read === true) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
for (let row of rows) {
|
|
||||||
row._id = generateIdForRow(row, table)
|
|
||||||
row.tableId = table._id
|
|
||||||
row._rev = "rev"
|
|
||||||
}
|
|
||||||
return rows
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildFilters(id, filters, table) {
|
|
||||||
const primary = table.primary
|
|
||||||
// if passed in array need to copy for shifting etc
|
|
||||||
let idCopy = cloneDeep(id)
|
|
||||||
if (filters) {
|
|
||||||
// need to map over the filters and make sure the _id field isn't present
|
|
||||||
for (let filter of Object.values(filters)) {
|
|
||||||
if (filter._id) {
|
|
||||||
const parts = breakRowIdField(filter._id)
|
|
||||||
for (let field of primary) {
|
|
||||||
filter[field] = parts.shift()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// make sure this field doesn't exist on any filter
|
|
||||||
delete filter._id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// there is no id, just use the user provided filters
|
|
||||||
if (!idCopy || !table) {
|
|
||||||
return filters
|
|
||||||
}
|
|
||||||
// if used as URL parameter it will have been joined
|
|
||||||
if (typeof idCopy === "string") {
|
|
||||||
idCopy = breakRowIdField(idCopy)
|
|
||||||
}
|
|
||||||
const equal = {}
|
|
||||||
for (let field of primary) {
|
|
||||||
// work through the ID and get the parts
|
|
||||||
equal[field] = idCopy.shift()
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
equal,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRequest(
|
|
||||||
appId,
|
|
||||||
operation,
|
|
||||||
tableId,
|
|
||||||
{ id, row, filters, sort, paginate } = {}
|
|
||||||
) {
|
|
||||||
let { datasourceId, tableName } = breakExternalTableId(tableId)
|
|
||||||
const table = await getExternalTable(appId, datasourceId, tableName)
|
|
||||||
if (!table) {
|
|
||||||
throw `Unable to process query, table "${tableName}" not defined.`
|
|
||||||
}
|
|
||||||
// clean up row on ingress using schema
|
|
||||||
filters = buildFilters(id, filters, table)
|
|
||||||
row = inputProcessing(row, table)
|
|
||||||
if (
|
|
||||||
operation === DataSourceOperation.DELETE &&
|
|
||||||
(filters == null || Object.keys(filters).length === 0)
|
|
||||||
) {
|
|
||||||
throw "Deletion must be filtered"
|
|
||||||
}
|
|
||||||
let json = {
|
|
||||||
endpoint: {
|
|
||||||
datasourceId,
|
|
||||||
entityId: tableName,
|
|
||||||
operation,
|
|
||||||
},
|
|
||||||
resource: {
|
|
||||||
// not specifying any fields means "*"
|
|
||||||
fields: [],
|
|
||||||
},
|
|
||||||
filters,
|
|
||||||
sort,
|
|
||||||
paginate,
|
|
||||||
body: row,
|
|
||||||
// pass an id filter into extra, purely for mysql/returning
|
|
||||||
extra: {
|
|
||||||
idFilter: buildFilters(id || generateIdForRow(row, table), {}, table),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// can't really use response right now
|
|
||||||
const response = await makeExternalQuery(appId, json)
|
|
||||||
// we searched for rows in someway
|
|
||||||
if (operation === DataSourceOperation.READ && Array.isArray(response)) {
|
|
||||||
return outputProcessing(response, table)
|
|
||||||
} else {
|
|
||||||
row = outputProcessing(response, table)[0]
|
|
||||||
return { row, table }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.patch = async ctx => {
|
exports.patch = async ctx => {
|
||||||
|
@ -172,9 +55,15 @@ exports.find = async ctx => {
|
||||||
const appId = ctx.appId
|
const appId = ctx.appId
|
||||||
const id = ctx.params.rowId
|
const id = ctx.params.rowId
|
||||||
const tableId = ctx.params.tableId
|
const tableId = ctx.params.tableId
|
||||||
return handleRequest(appId, DataSourceOperation.READ, tableId, {
|
const response = await handleRequest(
|
||||||
|
appId,
|
||||||
|
DataSourceOperation.READ,
|
||||||
|
tableId,
|
||||||
|
{
|
||||||
id,
|
id,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
return response ? response[0] : response
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.destroy = async ctx => {
|
exports.destroy = async ctx => {
|
||||||
|
@ -270,7 +159,56 @@ exports.validate = async () => {
|
||||||
return { valid: true }
|
return { valid: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.fetchEnrichedRow = async () => {
|
exports.fetchEnrichedRow = async ctx => {
|
||||||
// TODO: How does this work
|
const appId = ctx.appId
|
||||||
throw "Not Implemented"
|
const id = ctx.params.rowId
|
||||||
|
const tableId = ctx.params.tableId
|
||||||
|
const { datasourceId, tableName } = breakExternalTableId(tableId)
|
||||||
|
const db = new CouchDB(appId)
|
||||||
|
const datasource = await db.get(datasourceId)
|
||||||
|
if (!datasource || !datasource.entities) {
|
||||||
|
ctx.throw(400, "Datasource has not been configured for plus API.")
|
||||||
|
}
|
||||||
|
const tables = datasource.entities
|
||||||
|
const response = await handleRequest(
|
||||||
|
appId,
|
||||||
|
DataSourceOperation.READ,
|
||||||
|
tableId,
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
datasource,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const table = tables[tableName]
|
||||||
|
const row = response[0]
|
||||||
|
// this seems like a lot of work, but basically we need to dig deeper for the enrich
|
||||||
|
// for a single row, there is probably a better way to do this with some smart multi-layer joins
|
||||||
|
for (let [fieldName, field] of Object.entries(table.schema)) {
|
||||||
|
if (
|
||||||
|
field.type !== FieldTypes.LINK ||
|
||||||
|
!row[fieldName] ||
|
||||||
|
row[fieldName].length === 0
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const links = row[fieldName]
|
||||||
|
const linkedTableId = field.tableId
|
||||||
|
const linkedTable = tables[breakExternalTableId(linkedTableId).tableName]
|
||||||
|
// don't support composite keys right now
|
||||||
|
const linkedIds = links.map(link => breakRowIdField(link._id)[0])
|
||||||
|
row[fieldName] = await handleRequest(
|
||||||
|
appId,
|
||||||
|
DataSourceOperation.READ,
|
||||||
|
linkedTableId,
|
||||||
|
{
|
||||||
|
tables,
|
||||||
|
filters: {
|
||||||
|
oneOf: {
|
||||||
|
[linkedTable.primary]: linkedIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return row
|
||||||
}
|
}
|
||||||
|
|
|
@ -204,15 +204,18 @@ class TableSaveFunctions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getExternalTable = async (appId, datasourceId, tableName) => {
|
exports.getAllExternalTables = async (appId, datasourceId) => {
|
||||||
const db = new CouchDB(appId)
|
const db = new CouchDB(appId)
|
||||||
const datasource = await db.get(datasourceId)
|
const datasource = await db.get(datasourceId)
|
||||||
if (!datasource || !datasource.entities) {
|
if (!datasource || !datasource.entities) {
|
||||||
throw "Datasource is not configured fully."
|
throw "Datasource is not configured fully."
|
||||||
}
|
}
|
||||||
return Object.values(datasource.entities).find(
|
return datasource.entities
|
||||||
entity => entity.name === tableName
|
}
|
||||||
)
|
|
||||||
|
exports.getExternalTable = async (appId, datasourceId, tableName) => {
|
||||||
|
const entities = await exports.getAllExternalTables(appId, datasourceId)
|
||||||
|
return entities[tableName]
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.TableSaveFunctions = TableSaveFunctions
|
exports.TableSaveFunctions = TableSaveFunctions
|
||||||
|
|
|
@ -94,7 +94,7 @@ describe("/datasources", () => {
|
||||||
.expect(200)
|
.expect(200)
|
||||||
// this is mock data, can't test it
|
// this is mock data, can't test it
|
||||||
expect(res.body).toBeDefined()
|
expect(res.body).toBeDefined()
|
||||||
expect(pg.queryMock).toHaveBeenCalledWith(`select "name", "age" from "users" where "name" like $1 limit $2`, ["John%", 5000])
|
expect(pg.queryMock).toHaveBeenCalledWith(`select "name", "age" from "users" where "users"."name" like $1 limit $2`, ["John%", 5000])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -26,3 +26,17 @@ export interface Table {
|
||||||
primaryDisplay?: string
|
primaryDisplay?: string
|
||||||
sourceId?: string
|
sourceId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BudibaseAppMetadata {
|
||||||
|
_id: string
|
||||||
|
_rev?: string
|
||||||
|
appId: string
|
||||||
|
type: string
|
||||||
|
version: string
|
||||||
|
componentlibraries: string[]
|
||||||
|
name: string
|
||||||
|
url: string
|
||||||
|
instance: { _id: string }
|
||||||
|
updatedAt: Date
|
||||||
|
createdAt: Date
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { SourceNames } from "./datasource"
|
||||||
|
|
||||||
|
interface Base {
|
||||||
|
_id?: string
|
||||||
|
_rev?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FieldSchema {
|
||||||
|
// TODO: replace with field types enum when done
|
||||||
|
type: string
|
||||||
|
fieldName?: string
|
||||||
|
name: string
|
||||||
|
tableId?: string
|
||||||
|
relationshipType?: string
|
||||||
|
through?: string
|
||||||
|
foreignKey?: string
|
||||||
|
constraints?: {
|
||||||
|
type?: string
|
||||||
|
email?: boolean
|
||||||
|
inclusion?: string[]
|
||||||
|
length?: {
|
||||||
|
minimum?: string | number
|
||||||
|
maximum?: string | number
|
||||||
|
}
|
||||||
|
presence?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TableSchema {
|
||||||
|
[key: string]: FieldSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Table extends Base {
|
||||||
|
type?: string
|
||||||
|
views?: {}
|
||||||
|
name?: string
|
||||||
|
primary?: string[]
|
||||||
|
schema: TableSchema
|
||||||
|
primaryDisplay?: string
|
||||||
|
sourceId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Row extends Base {
|
||||||
|
type?: string
|
||||||
|
tableId?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JsonSchemaField {
|
||||||
|
properties: {
|
||||||
|
[key: string]: {
|
||||||
|
type: string
|
||||||
|
title: string
|
||||||
|
customType?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
required?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomationStep {
|
||||||
|
description: string
|
||||||
|
event?: string
|
||||||
|
icon: string
|
||||||
|
id: string
|
||||||
|
inputs: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
name: string
|
||||||
|
schema: {
|
||||||
|
inputs: JsonSchemaField
|
||||||
|
outputs: JsonSchemaField
|
||||||
|
}
|
||||||
|
stepId: string
|
||||||
|
tagline: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Automation extends Base {
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
appId?: string
|
||||||
|
definition: {
|
||||||
|
steps: AutomationStep[]
|
||||||
|
trigger?: AutomationStep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Datasource extends Base {
|
||||||
|
type: string
|
||||||
|
name: string
|
||||||
|
source: SourceNames
|
||||||
|
// the config is defined by the schema
|
||||||
|
config: {
|
||||||
|
[key: string]: string | number | boolean
|
||||||
|
}
|
||||||
|
plus: boolean
|
||||||
|
entities?: {
|
||||||
|
[key: string]: Table
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,20 @@ export enum DatasourceFieldTypes {
|
||||||
JSON = "json",
|
JSON = "json",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum SourceNames {
|
||||||
|
POSTGRES = "POSTGRES",
|
||||||
|
DYNAMODB = "DYNAMODB",
|
||||||
|
MONGODB = "MONGODB",
|
||||||
|
ELASTICSEARCH = "ELASTICSEARCH",
|
||||||
|
COUCHDB = "COUCHDB",
|
||||||
|
SQL_SERVER = "SQL_SERVER",
|
||||||
|
S3 = "S3",
|
||||||
|
AIRTABLE = "AIRTABLE",
|
||||||
|
MYSQL = "MYSQL",
|
||||||
|
ARANGODB = "ARANGODB",
|
||||||
|
REST = "REST",
|
||||||
|
}
|
||||||
|
|
||||||
export interface QueryDefinition {
|
export interface QueryDefinition {
|
||||||
type: QueryTypes
|
type: QueryTypes
|
||||||
displayName?: string
|
displayName?: string
|
||||||
|
@ -47,7 +61,7 @@ export interface Integration {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchFilters {
|
export interface SearchFilters {
|
||||||
allOr: boolean
|
allOr?: boolean
|
||||||
string?: {
|
string?: {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
}
|
}
|
||||||
|
@ -72,6 +86,26 @@ export interface SearchFilters {
|
||||||
notEmpty?: {
|
notEmpty?: {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
oneOf?: {
|
||||||
|
[key: string]: any[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SortJson {
|
||||||
|
[key: string]: SortDirection
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationJson {
|
||||||
|
limit: number
|
||||||
|
page: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelationshipsJson {
|
||||||
|
through?: string
|
||||||
|
from?: string
|
||||||
|
to?: string
|
||||||
|
tableName: string
|
||||||
|
column: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryJson {
|
export interface QueryJson {
|
||||||
|
@ -84,17 +118,13 @@ export interface QueryJson {
|
||||||
fields: string[]
|
fields: string[]
|
||||||
}
|
}
|
||||||
filters?: SearchFilters
|
filters?: SearchFilters
|
||||||
sort?: {
|
sort?: SortJson
|
||||||
[key: string]: SortDirection
|
paginate?: PaginationJson
|
||||||
}
|
|
||||||
paginate?: {
|
|
||||||
limit: number
|
|
||||||
page: string | number
|
|
||||||
}
|
|
||||||
body?: object
|
body?: object
|
||||||
extra: {
|
extra?: {
|
||||||
idFilter?: SearchFilters
|
idFilter?: SearchFilters
|
||||||
}
|
}
|
||||||
|
relationships?: RelationshipsJson[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SqlQuery {
|
export interface SqlQuery {
|
|
@ -2,7 +2,7 @@ import {
|
||||||
Integration,
|
Integration,
|
||||||
DatasourceFieldTypes,
|
DatasourceFieldTypes,
|
||||||
QueryTypes,
|
QueryTypes,
|
||||||
} from "./base/definitions"
|
} from "../definitions/datasource"
|
||||||
|
|
||||||
module AirtableModule {
|
module AirtableModule {
|
||||||
const Airtable = require("airtable")
|
const Airtable = require("airtable")
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {
|
||||||
Integration,
|
Integration,
|
||||||
DatasourceFieldTypes,
|
DatasourceFieldTypes,
|
||||||
QueryTypes,
|
QueryTypes,
|
||||||
} from "./base/definitions"
|
} from "../definitions/datasource"
|
||||||
|
|
||||||
module ArangoModule {
|
module ArangoModule {
|
||||||
const { Database, aql } = require("arangojs")
|
const { Database, aql } = require("arangojs")
|
||||||
|
|
|
@ -6,18 +6,23 @@ import {
|
||||||
QueryOptions,
|
QueryOptions,
|
||||||
SortDirection,
|
SortDirection,
|
||||||
Operation,
|
Operation,
|
||||||
} from "./definitions"
|
RelationshipsJson,
|
||||||
|
} from "../../definitions/datasource"
|
||||||
|
|
||||||
|
type KnexQuery = Knex.QueryBuilder | Knex
|
||||||
|
|
||||||
|
// right now we only do filters on the specific table being queried
|
||||||
function addFilters(
|
function addFilters(
|
||||||
query: any,
|
tableName: string,
|
||||||
|
query: KnexQuery,
|
||||||
filters: SearchFilters | undefined
|
filters: SearchFilters | undefined
|
||||||
): Knex.QueryBuilder {
|
): KnexQuery {
|
||||||
function iterate(
|
function iterate(
|
||||||
structure: { [key: string]: any },
|
structure: { [key: string]: any },
|
||||||
fn: (key: string, value: any) => void
|
fn: (key: string, value: any) => void
|
||||||
) {
|
) {
|
||||||
for (let [key, value] of Object.entries(structure)) {
|
for (let [key, value] of Object.entries(structure)) {
|
||||||
fn(key, value)
|
fn(`${tableName}.${key}`, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!filters) {
|
if (!filters) {
|
||||||
|
@ -25,6 +30,12 @@ function addFilters(
|
||||||
}
|
}
|
||||||
// if all or specified in filters, then everything is an or
|
// if all or specified in filters, then everything is an or
|
||||||
const allOr = filters.allOr
|
const allOr = filters.allOr
|
||||||
|
if (filters.oneOf) {
|
||||||
|
iterate(filters.oneOf, (key, array) => {
|
||||||
|
const fnc = allOr ? "orWhereIn" : "whereIn"
|
||||||
|
query = query[fnc](key, array)
|
||||||
|
})
|
||||||
|
}
|
||||||
if (filters.string) {
|
if (filters.string) {
|
||||||
iterate(filters.string, (key, value) => {
|
iterate(filters.string, (key, value) => {
|
||||||
const fnc = allOr ? "orWhere" : "where"
|
const fnc = allOr ? "orWhere" : "where"
|
||||||
|
@ -67,9 +78,47 @@ function addFilters(
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCreate(knex: Knex, json: QueryJson, opts: QueryOptions) {
|
function addRelationships(
|
||||||
|
query: KnexQuery,
|
||||||
|
fromTable: string,
|
||||||
|
relationships: RelationshipsJson[] | undefined
|
||||||
|
): KnexQuery {
|
||||||
|
if (!relationships) {
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
for (let relationship of relationships) {
|
||||||
|
const from = relationship.from,
|
||||||
|
to = relationship.to,
|
||||||
|
toTable = relationship.tableName
|
||||||
|
if (!relationship.through) {
|
||||||
|
// @ts-ignore
|
||||||
|
query = query.leftJoin(
|
||||||
|
toTable,
|
||||||
|
`${fromTable}.${from}`,
|
||||||
|
`${relationship.tableName}.${to}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const throughTable = relationship.through
|
||||||
|
query = query
|
||||||
|
// @ts-ignore
|
||||||
|
.leftJoin(
|
||||||
|
throughTable,
|
||||||
|
`${fromTable}.${from}`,
|
||||||
|
`${throughTable}.${from}`
|
||||||
|
)
|
||||||
|
.leftJoin(toTable, `${toTable}.${to}`, `${throughTable}.${to}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCreate(
|
||||||
|
knex: Knex,
|
||||||
|
json: QueryJson,
|
||||||
|
opts: QueryOptions
|
||||||
|
): KnexQuery {
|
||||||
const { endpoint, body } = json
|
const { endpoint, body } = json
|
||||||
let query = knex(endpoint.entityId)
|
let query: KnexQuery = knex(endpoint.entityId)
|
||||||
// mysql can't use returning
|
// mysql can't use returning
|
||||||
if (opts.disableReturning) {
|
if (opts.disableReturning) {
|
||||||
return query.insert(body)
|
return query.insert(body)
|
||||||
|
@ -78,9 +127,10 @@ function buildCreate(knex: Knex, json: QueryJson, opts: QueryOptions) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRead(knex: Knex, json: QueryJson, limit: number) {
|
function buildRead(knex: Knex, json: QueryJson, limit: number): KnexQuery {
|
||||||
let { endpoint, resource, filters, sort, paginate } = json
|
let { endpoint, resource, filters, sort, paginate, relationships } = json
|
||||||
let query: Knex.QueryBuilder = knex(endpoint.entityId)
|
const tableName = endpoint.entityId
|
||||||
|
let query: KnexQuery = knex(tableName)
|
||||||
// select all if not specified
|
// select all if not specified
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
resource = { fields: [] }
|
resource = { fields: [] }
|
||||||
|
@ -92,7 +142,9 @@ function buildRead(knex: Knex, json: QueryJson, limit: number) {
|
||||||
query = query.select("*")
|
query = query.select("*")
|
||||||
}
|
}
|
||||||
// handle where
|
// handle where
|
||||||
query = addFilters(query, filters)
|
query = addFilters(tableName, query, filters)
|
||||||
|
// handle join
|
||||||
|
query = addRelationships(query, tableName, relationships)
|
||||||
// handle sorting
|
// handle sorting
|
||||||
if (sort) {
|
if (sort) {
|
||||||
for (let [key, value] of Object.entries(sort)) {
|
for (let [key, value] of Object.entries(sort)) {
|
||||||
|
@ -114,10 +166,14 @@ function buildRead(knex: Knex, json: QueryJson, limit: number) {
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUpdate(knex: Knex, json: QueryJson, opts: QueryOptions) {
|
function buildUpdate(
|
||||||
|
knex: Knex,
|
||||||
|
json: QueryJson,
|
||||||
|
opts: QueryOptions
|
||||||
|
): KnexQuery {
|
||||||
const { endpoint, body, filters } = json
|
const { endpoint, body, filters } = json
|
||||||
let query = knex(endpoint.entityId)
|
let query: KnexQuery = knex(endpoint.entityId)
|
||||||
query = addFilters(query, filters)
|
query = addFilters(endpoint.entityId, query, filters)
|
||||||
// mysql can't use returning
|
// mysql can't use returning
|
||||||
if (opts.disableReturning) {
|
if (opts.disableReturning) {
|
||||||
return query.update(body)
|
return query.update(body)
|
||||||
|
@ -126,10 +182,14 @@ function buildUpdate(knex: Knex, json: QueryJson, opts: QueryOptions) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDelete(knex: Knex, json: QueryJson, opts: QueryOptions) {
|
function buildDelete(
|
||||||
|
knex: Knex,
|
||||||
|
json: QueryJson,
|
||||||
|
opts: QueryOptions
|
||||||
|
): KnexQuery {
|
||||||
const { endpoint, filters } = json
|
const { endpoint, filters } = json
|
||||||
let query = knex(endpoint.entityId)
|
let query: KnexQuery = knex(endpoint.entityId)
|
||||||
query = addFilters(query, filters)
|
query = addFilters(endpoint.entityId, query, filters)
|
||||||
// mysql can't use returning
|
// mysql can't use returning
|
||||||
if (opts.disableReturning) {
|
if (opts.disableReturning) {
|
||||||
return query.delete()
|
return query.delete()
|
||||||
|
@ -180,6 +240,8 @@ class SqlQueryBuilder {
|
||||||
default:
|
default:
|
||||||
throw `Operation type is not supported by SQL query builder`
|
throw `Operation type is not supported by SQL query builder`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
return query.toSQL().toNative()
|
return query.toSQL().toNative()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {
|
||||||
Integration,
|
Integration,
|
||||||
DatasourceFieldTypes,
|
DatasourceFieldTypes,
|
||||||
QueryTypes,
|
QueryTypes,
|
||||||
} from "./base/definitions"
|
} from "../definitions/datasource"
|
||||||
|
|
||||||
module CouchDBModule {
|
module CouchDBModule {
|
||||||
const PouchDB = require("pouchdb")
|
const PouchDB = require("pouchdb")
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {
|
||||||
Integration,
|
Integration,
|
||||||
DatasourceFieldTypes,
|
DatasourceFieldTypes,
|
||||||
QueryTypes,
|
QueryTypes,
|
||||||
} from "./base/definitions"
|
} from "../definitions/datasource"
|
||||||
|
|
||||||
module DynamoModule {
|
module DynamoModule {
|
||||||
const AWS = require("aws-sdk")
|
const AWS = require("aws-sdk")
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {
|
||||||
Integration,
|
Integration,
|
||||||
DatasourceFieldTypes,
|
DatasourceFieldTypes,
|
||||||
QueryTypes,
|
QueryTypes,
|
||||||
} from "./base/definitions"
|
} from "../definitions/datasource"
|
||||||
|
|
||||||
module ElasticsearchModule {
|
module ElasticsearchModule {
|
||||||
const { Client } = require("@elastic/elasticsearch")
|
const { Client } = require("@elastic/elasticsearch")
|
||||||
|
|
|
@ -9,33 +9,34 @@ const airtable = require("./airtable")
|
||||||
const mysql = require("./mysql")
|
const mysql = require("./mysql")
|
||||||
const arangodb = require("./arangodb")
|
const arangodb = require("./arangodb")
|
||||||
const rest = require("./rest")
|
const rest = require("./rest")
|
||||||
|
const { SourceNames } = require("../definitions/datasource")
|
||||||
|
|
||||||
const DEFINITIONS = {
|
const DEFINITIONS = {
|
||||||
POSTGRES: postgres.schema,
|
[SourceNames.POSTGRES]: postgres.schema,
|
||||||
DYNAMODB: dynamodb.schema,
|
[SourceNames.DYNAMODB]: dynamodb.schema,
|
||||||
MONGODB: mongodb.schema,
|
[SourceNames.MONGODB]: mongodb.schema,
|
||||||
ELASTICSEARCH: elasticsearch.schema,
|
[SourceNames.ELASTICSEARCH]: elasticsearch.schema,
|
||||||
COUCHDB: couchdb.schema,
|
[SourceNames.COUCHDB]: couchdb.schema,
|
||||||
SQL_SERVER: sqlServer.schema,
|
[SourceNames.SQL_SERVER]: sqlServer.schema,
|
||||||
S3: s3.schema,
|
[SourceNames.S3]: s3.schema,
|
||||||
AIRTABLE: airtable.schema,
|
[SourceNames.AIRTABLE]: airtable.schema,
|
||||||
MYSQL: mysql.schema,
|
[SourceNames.MYSQL]: mysql.schema,
|
||||||
ARANGODB: arangodb.schema,
|
[SourceNames.ARANGODB]: arangodb.schema,
|
||||||
REST: rest.schema,
|
[SourceNames.REST]: rest.schema,
|
||||||
}
|
}
|
||||||
|
|
||||||
const INTEGRATIONS = {
|
const INTEGRATIONS = {
|
||||||
POSTGRES: postgres.integration,
|
[SourceNames.POSTGRES]: postgres.integration,
|
||||||
DYNAMODB: dynamodb.integration,
|
[SourceNames.DYNAMODB]: dynamodb.integration,
|
||||||
MONGODB: mongodb.integration,
|
[SourceNames.MONGODB]: mongodb.integration,
|
||||||
ELASTICSEARCH: elasticsearch.integration,
|
[SourceNames.ELASTICSEARCH]: elasticsearch.integration,
|
||||||
COUCHDB: couchdb.integration,
|
[SourceNames.COUCHDB]: couchdb.integration,
|
||||||
S3: s3.integration,
|
[SourceNames.SQL_SERVER]: s3.integration,
|
||||||
SQL_SERVER: sqlServer.integration,
|
[SourceNames.S3]: sqlServer.integration,
|
||||||
AIRTABLE: airtable.integration,
|
[SourceNames.AIRTABLE]: airtable.integration,
|
||||||
MYSQL: mysql.integration,
|
[SourceNames.MYSQL]: mysql.integration,
|
||||||
ARANGODB: arangodb.integration,
|
[SourceNames.ARANGODB]: arangodb.integration,
|
||||||
REST: rest.integration,
|
[SourceNames.REST]: rest.integration,
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {
|
||||||
QueryTypes,
|
QueryTypes,
|
||||||
QueryJson,
|
QueryJson,
|
||||||
SqlQuery,
|
SqlQuery,
|
||||||
} from "./base/definitions"
|
} from "../definitions/datasource"
|
||||||
import { getSqlQuery } from "./utils"
|
import { getSqlQuery } from "./utils"
|
||||||
|
|
||||||
module MSSQLModule {
|
module MSSQLModule {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {
|
||||||
Integration,
|
Integration,
|
||||||
DatasourceFieldTypes,
|
DatasourceFieldTypes,
|
||||||
QueryTypes,
|
QueryTypes,
|
||||||
} from "./base/definitions"
|
} from "../definitions/datasource"
|
||||||
|
|
||||||
module MongoDBModule {
|
module MongoDBModule {
|
||||||
const { MongoClient } = require("mongodb")
|
const { MongoClient } = require("mongodb")
|
||||||
|
|
|
@ -5,7 +5,8 @@ import {
|
||||||
Operation,
|
Operation,
|
||||||
QueryJson,
|
QueryJson,
|
||||||
SqlQuery,
|
SqlQuery,
|
||||||
} from "./base/definitions"
|
} from "../definitions/datasource"
|
||||||
|
import { Table, TableSchema } from "../definitions/common"
|
||||||
import { getSqlQuery } from "./utils"
|
import { getSqlQuery } from "./utils"
|
||||||
|
|
||||||
module MySQLModule {
|
module MySQLModule {
|
||||||
|
@ -139,7 +140,7 @@ module MySQLModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
async buildSchema(datasourceId: string) {
|
async buildSchema(datasourceId: string) {
|
||||||
const tables: any = {}
|
const tables: { [key: string]: Table } = {}
|
||||||
const database = this.config.database
|
const database = this.config.database
|
||||||
this.client.connect()
|
this.client.connect()
|
||||||
|
|
||||||
|
@ -154,7 +155,7 @@ module MySQLModule {
|
||||||
)
|
)
|
||||||
for (let tableName of tableNames) {
|
for (let tableName of tableNames) {
|
||||||
const primaryKeys = []
|
const primaryKeys = []
|
||||||
const schema: any = {}
|
const schema: TableSchema = {}
|
||||||
const descResp = await internalQuery(
|
const descResp = await internalQuery(
|
||||||
this.client,
|
this.client,
|
||||||
{ sql: `DESCRIBE ${tableName};` },
|
{ sql: `DESCRIBE ${tableName};` },
|
||||||
|
@ -166,7 +167,7 @@ module MySQLModule {
|
||||||
primaryKeys.push(columnName)
|
primaryKeys.push(columnName)
|
||||||
}
|
}
|
||||||
const constraints = {
|
const constraints = {
|
||||||
required: column.Null !== "YES",
|
presence: column.Null !== "YES",
|
||||||
}
|
}
|
||||||
schema[columnName] = {
|
schema[columnName] = {
|
||||||
name: columnName,
|
name: columnName,
|
||||||
|
@ -212,7 +213,7 @@ module MySQLModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getReturningRow(json: QueryJson) {
|
async getReturningRow(json: QueryJson) {
|
||||||
if (!json.extra.idFilter) {
|
if (!json.extra || !json.extra.idFilter) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
const input = this._query({
|
const input = this._query({
|
||||||
|
|
|
@ -4,8 +4,8 @@ import {
|
||||||
QueryTypes,
|
QueryTypes,
|
||||||
QueryJson,
|
QueryJson,
|
||||||
SqlQuery,
|
SqlQuery,
|
||||||
} from "./base/definitions"
|
} from "../definitions/datasource"
|
||||||
import { Table } from "../constants/definitions"
|
import { Table } from "../definitions/common"
|
||||||
import { getSqlQuery } from "./utils"
|
import { getSqlQuery } from "./utils"
|
||||||
|
|
||||||
module PostgresModule {
|
module PostgresModule {
|
||||||
|
@ -134,8 +134,9 @@ module PostgresModule {
|
||||||
/**
|
/**
|
||||||
* Fetches the tables from the postgres table and assigns them to the datasource.
|
* Fetches the tables from the postgres table and assigns them to the datasource.
|
||||||
* @param {*} datasourceId - datasourceId to fetch
|
* @param {*} datasourceId - datasourceId to fetch
|
||||||
|
* @param entities - the tables that are to be built
|
||||||
*/
|
*/
|
||||||
async buildSchema(datasourceId: string) {
|
async buildSchema(datasourceId: string, entities: Record<string, Table>) {
|
||||||
let tableKeys: { [key: string]: string[] } = {}
|
let tableKeys: { [key: string]: string[] } = {}
|
||||||
try {
|
try {
|
||||||
const primaryKeysResponse = await this.client.query(
|
const primaryKeysResponse = await this.client.query(
|
||||||
|
@ -167,6 +168,19 @@ module PostgresModule {
|
||||||
name: tableName,
|
name: tableName,
|
||||||
schema: {},
|
schema: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add the existing relationships from the entities if they exist, to prevent them from being overridden
|
||||||
|
if (entities && entities[tableName]) {
|
||||||
|
const existingTableSchema = entities[tableName].schema
|
||||||
|
for (let key in existingTableSchema) {
|
||||||
|
if (!existingTableSchema.hasOwnProperty(key)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (existingTableSchema[key].type === "link") {
|
||||||
|
tables[tableName].schema[key] = existingTableSchema[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const type: string = convertType(column.data_type, TYPE_MAP)
|
const type: string = convertType(column.data_type, TYPE_MAP)
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {
|
||||||
Integration,
|
Integration,
|
||||||
DatasourceFieldTypes,
|
DatasourceFieldTypes,
|
||||||
QueryTypes,
|
QueryTypes,
|
||||||
} from "./base/definitions"
|
} from "../definitions/datasource"
|
||||||
|
|
||||||
module RestModule {
|
module RestModule {
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Integration, QueryTypes } from "./base/definitions"
|
import { Integration, QueryTypes } from "../definitions/datasource"
|
||||||
|
|
||||||
module S3Module {
|
module S3Module {
|
||||||
const AWS = require("aws-sdk")
|
const AWS = require("aws-sdk")
|
||||||
|
|
|
@ -81,7 +81,7 @@ describe("SQL query builder", () => {
|
||||||
}))
|
}))
|
||||||
expect(query).toEqual({
|
expect(query).toEqual({
|
||||||
bindings: ["John%", limit],
|
bindings: ["John%", limit],
|
||||||
sql: `select * from "${TABLE_NAME}" where "name" like $1 limit $2`
|
sql: `select * from "${TABLE_NAME}" where "${TABLE_NAME}"."name" like $1 limit $2`
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@ describe("SQL query builder", () => {
|
||||||
}))
|
}))
|
||||||
expect(query).toEqual({
|
expect(query).toEqual({
|
||||||
bindings: [2, 10, limit],
|
bindings: [2, 10, limit],
|
||||||
sql: `select * from "${TABLE_NAME}" where "age" between $1 and $2 limit $3`
|
sql: `select * from "${TABLE_NAME}" where "${TABLE_NAME}"."age" between $1 and $2 limit $3`
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ describe("SQL query builder", () => {
|
||||||
}))
|
}))
|
||||||
expect(query).toEqual({
|
expect(query).toEqual({
|
||||||
bindings: [10, "John", limit],
|
bindings: [10, "John", limit],
|
||||||
sql: `select * from "${TABLE_NAME}" where ("age" = $1) or ("name" = $2) limit $3`
|
sql: `select * from "${TABLE_NAME}" where ("${TABLE_NAME}"."age" = $1) or ("${TABLE_NAME}"."name" = $2) limit $3`
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ describe("SQL query builder", () => {
|
||||||
}))
|
}))
|
||||||
expect(query).toEqual({
|
expect(query).toEqual({
|
||||||
bindings: ["John", 1001],
|
bindings: ["John", 1001],
|
||||||
sql: `update "${TABLE_NAME}" set "name" = $1 where "id" = $2 returning *`
|
sql: `update "${TABLE_NAME}" set "name" = $1 where "${TABLE_NAME}"."id" = $2 returning *`
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@ describe("SQL query builder", () => {
|
||||||
}))
|
}))
|
||||||
expect(query).toEqual({
|
expect(query).toEqual({
|
||||||
bindings: [1001],
|
bindings: [1001],
|
||||||
sql: `delete from "${TABLE_NAME}" where "id" = $1 returning *`
|
sql: `delete from "${TABLE_NAME}" where "${TABLE_NAME}"."id" = $1 returning *`
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { SqlQuery } from "./base/definitions"
|
import { SqlQuery } from "../definitions/datasource"
|
||||||
|
import { Datasource } from "../definitions/common"
|
||||||
|
import { SourceNames } from "../definitions/datasource"
|
||||||
const { DocumentTypes, SEPARATOR } = require("../db/utils")
|
const { DocumentTypes, SEPARATOR } = require("../db/utils")
|
||||||
const { FieldTypes } = require("../constants")
|
const { FieldTypes } = require("../constants")
|
||||||
|
|
||||||
|
@ -25,15 +27,21 @@ export function generateRowIdField(keyProps: any[] = []) {
|
||||||
keyProps = [keyProps]
|
keyProps = [keyProps]
|
||||||
}
|
}
|
||||||
// this conserves order and types
|
// this conserves order and types
|
||||||
|
// we have to swap the double quotes to single quotes for use in HBS statements
|
||||||
|
// when using the literal helper the double quotes can break things
|
||||||
return encodeURIComponent(JSON.stringify(keyProps).replace(/"/g, "'"))
|
return encodeURIComponent(JSON.stringify(keyProps).replace(/"/g, "'"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// should always return an array
|
// should always return an array
|
||||||
export function breakRowIdField(_id: string) {
|
export function breakRowIdField(_id: string): any[] {
|
||||||
if (!_id) {
|
if (!_id) {
|
||||||
return null
|
return []
|
||||||
}
|
}
|
||||||
return JSON.parse(decodeURIComponent(_id))
|
// have to replace on the way back as we swapped out the double quotes
|
||||||
|
// when encoding, but JSON can't handle the single quotes
|
||||||
|
const decoded: string = decodeURIComponent(_id).replace(/'/g, '"')
|
||||||
|
const parsed = JSON.parse(decoded)
|
||||||
|
return Array.isArray(parsed) ? parsed : [parsed]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertType(type: string, map: { [key: string]: any }) {
|
export function convertType(type: string, map: { [key: string]: any }) {
|
||||||
|
@ -52,3 +60,11 @@ export function getSqlQuery(query: SqlQuery | string): SqlQuery {
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSQL(datasource: Datasource): boolean {
|
||||||
|
if (!datasource || !datasource.source) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const SQL = [SourceNames.POSTGRES, SourceNames.SQL_SERVER, SourceNames.MYSQL]
|
||||||
|
return SQL.indexOf(datasource.source) !== -1
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,8 @@
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true,
|
||||||
|
"incremental": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./src/**/*"
|
"./src/**/*"
|
||||||
|
|
|
@ -29,11 +29,11 @@
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"svelte"
|
"svelte"
|
||||||
],
|
],
|
||||||
"version": "0.9.67",
|
"version": "0.9.70",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc",
|
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^0.9.67",
|
"@budibase/bbui": "^0.9.70",
|
||||||
"@spectrum-css/link": "^3.1.3",
|
"@spectrum-css/link": "^3.1.3",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
|
|
@ -19,13 +19,20 @@
|
||||||
|
|
||||||
// Add color styles to main styles object, otherwise the styleable helper
|
// Add color styles to main styles object, otherwise the styleable helper
|
||||||
// overrides the color when it's passed as inline style.
|
// overrides the color when it's passed as inline style.
|
||||||
$: styles = {
|
$: styles = enrichStyles($component.styles, color)
|
||||||
...$component.styles,
|
|
||||||
|
const enrichStyles = (styles, color) => {
|
||||||
|
if (!color) {
|
||||||
|
return styles
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...styles,
|
||||||
normal: {
|
normal: {
|
||||||
...$component.styles?.normal,
|
...styles?.normal,
|
||||||
color,
|
color,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1
|
<h1
|
||||||
|
|
|
@ -23,13 +23,22 @@
|
||||||
|
|
||||||
// Add color styles to main styles object, otherwise the styleable helper
|
// Add color styles to main styles object, otherwise the styleable helper
|
||||||
// overrides the color when it's passed as inline style.
|
// overrides the color when it's passed as inline style.
|
||||||
$: styles = {
|
// Add color styles to main styles object, otherwise the styleable helper
|
||||||
...$component.styles,
|
// overrides the color when it's passed as inline style.
|
||||||
|
$: styles = enrichStyles($component.styles, color)
|
||||||
|
|
||||||
|
const enrichStyles = (styles, color) => {
|
||||||
|
if (!color) {
|
||||||
|
return styles
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...styles,
|
||||||
normal: {
|
normal: {
|
||||||
...$component.styles?.normal,
|
...styles?.normal,
|
||||||
color,
|
color,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $builderStore.inBuilder || componentText}
|
{#if $builderStore.inBuilder || componentText}
|
||||||
|
|
|
@ -19,13 +19,20 @@
|
||||||
|
|
||||||
// Add color styles to main styles object, otherwise the styleable helper
|
// Add color styles to main styles object, otherwise the styleable helper
|
||||||
// overrides the color when it's passed as inline style.
|
// overrides the color when it's passed as inline style.
|
||||||
$: styles = {
|
$: styles = enrichStyles($component.styles, color)
|
||||||
...$component.styles,
|
|
||||||
|
const enrichStyles = (styles, color) => {
|
||||||
|
if (!color) {
|
||||||
|
return styles
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...styles,
|
||||||
normal: {
|
normal: {
|
||||||
...$component.styles?.normal,
|
...styles?.normal,
|
||||||
color,
|
color,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/string-templates",
|
"name": "@budibase/string-templates",
|
||||||
"version": "0.9.67",
|
"version": "0.9.70",
|
||||||
"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,12 +1,7 @@
|
||||||
const handlebars = require("handlebars")
|
const handlebars = require("handlebars")
|
||||||
const { registerAll } = require("./helpers/index")
|
const { registerAll } = require("./helpers/index")
|
||||||
const processors = require("./processors")
|
const processors = require("./processors")
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { removeHandlebarsStatements } = require("./utilities")
|
||||||
const {
|
|
||||||
removeNull,
|
|
||||||
updateContext,
|
|
||||||
removeHandlebarsStatements,
|
|
||||||
} = require("./utilities")
|
|
||||||
const manifest = require("../manifest.json")
|
const manifest = require("../manifest.json")
|
||||||
|
|
||||||
const hbsInstance = handlebars.create()
|
const hbsInstance = handlebars.create()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/worker",
|
"name": "@budibase/worker",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "0.9.67",
|
"version": "0.9.70",
|
||||||
"description": "Budibase background service",
|
"description": "Budibase background service",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -21,8 +21,8 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/auth": "^0.9.67",
|
"@budibase/auth": "^0.9.70",
|
||||||
"@budibase/string-templates": "^0.9.67",
|
"@budibase/string-templates": "^0.9.70",
|
||||||
"@koa/router": "^8.0.0",
|
"@koa/router": "^8.0.0",
|
||||||
"aws-sdk": "^2.811.0",
|
"aws-sdk": "^2.811.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
@ -39,6 +39,7 @@
|
||||||
"koa-static": "^5.0.0",
|
"koa-static": "^5.0.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"nodemailer": "^6.5.0",
|
"nodemailer": "^6.5.0",
|
||||||
|
"@techpass/passport-openidconnect": "^0.3.0",
|
||||||
"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",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const authPkg = require("@budibase/auth")
|
const authPkg = require("@budibase/auth")
|
||||||
const { google } = require("@budibase/auth/src/middleware")
|
const { google } = require("@budibase/auth/src/middleware")
|
||||||
|
const { oidc } = require("@budibase/auth/src/middleware")
|
||||||
const { Configs, EmailTemplatePurpose } = require("../../../constants")
|
const { Configs, EmailTemplatePurpose } = require("../../../constants")
|
||||||
const CouchDB = require("../../../db")
|
const CouchDB = require("../../../db")
|
||||||
const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
|
const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
|
||||||
|
@ -10,16 +11,16 @@ const { checkResetPasswordCode } = require("../../../utilities/redis")
|
||||||
|
|
||||||
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
|
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
|
||||||
|
|
||||||
function authInternal(ctx, user, err = null) {
|
function authInternal(ctx, user, err = null, info = null) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return ctx.throw(403, "Unauthorized")
|
return ctx.throw(403, info? info : "Unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
const expires = new Date()
|
const expires = new Date()
|
||||||
expires.setDate(expires.getDate() + 1)
|
expires.setDate(expires.getDate() + 1)
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return ctx.throw(403, "Unauthorized")
|
return ctx.throw(403, info? info : "Unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.cookies.set(Cookies.Auth, user.token, {
|
ctx.cookies.set(Cookies.Auth, user.token, {
|
||||||
|
@ -129,3 +130,34 @@ exports.googleAuth = async (ctx, next) => {
|
||||||
}
|
}
|
||||||
)(ctx, next)
|
)(ctx, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function oidcStrategyFactory(ctx) {
|
||||||
|
const callbackUrl = `${ctx.protocol}://${ctx.host}/api/admin/auth/oidc/callback`
|
||||||
|
return oidc.strategyFactory(callbackUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The initial call that OIDC authentication makes to take you to the configured OIDC login screen.
|
||||||
|
* On a successful login, you will be redirected to the oidcAuth callback route.
|
||||||
|
*/
|
||||||
|
exports.oidcPreAuth = async (ctx, next) => {
|
||||||
|
const strategy = await oidcStrategyFactory(ctx)
|
||||||
|
|
||||||
|
return passport.authenticate(strategy, {
|
||||||
|
scope: ["profile", "email"],
|
||||||
|
})(ctx, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.oidcAuth = async (ctx, next) => {
|
||||||
|
const strategy = await oidcStrategyFactory(ctx)
|
||||||
|
|
||||||
|
return passport.authenticate(
|
||||||
|
strategy,
|
||||||
|
{ successRedirect: "/", failureRedirect: "/error" },
|
||||||
|
async (err, user, info) => {
|
||||||
|
authInternal(ctx, user, err, info)
|
||||||
|
|
||||||
|
ctx.redirect("/")
|
||||||
|
}
|
||||||
|
)(ctx, next)
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ const CouchDB = require("../../../db")
|
||||||
|
|
||||||
exports.fetch = async ctx => {
|
exports.fetch = async ctx => {
|
||||||
// always use the dev apps as they'll be most up to date (true)
|
// always use the dev apps as they'll be most up to date (true)
|
||||||
const apps = await getAllApps({ CouchDB, dev: true })
|
const apps = await getAllApps({ CouchDB, all: true })
|
||||||
const promises = []
|
const promises = []
|
||||||
for (let app of apps) {
|
for (let app of apps) {
|
||||||
// use dev app IDs
|
// use dev app IDs
|
||||||
|
|
|
@ -25,6 +25,14 @@ const PUBLIC_ENDPOINTS = [
|
||||||
route: "/api/admin/auth/google/callback",
|
route: "/api/admin/auth/google/callback",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
route: "/api/admin/auth/oidc",
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/api/admin/auth/oidc/callback",
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
route: "/api/admin/auth/reset",
|
route: "/api/admin/auth/reset",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
|
@ -39,5 +39,7 @@ router
|
||||||
.post("/api/admin/auth/logout", authController.logout)
|
.post("/api/admin/auth/logout", authController.logout)
|
||||||
.get("/api/admin/auth/google", authController.googlePreAuth)
|
.get("/api/admin/auth/google", authController.googlePreAuth)
|
||||||
.get("/api/admin/auth/google/callback", authController.googleAuth)
|
.get("/api/admin/auth/google/callback", authController.googleAuth)
|
||||||
|
.get("/api/admin/auth/oidc", authController.oidcPreAuth)
|
||||||
|
.get("/api/admin/auth/oidc/callback", authController.oidcAuth)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -5,6 +5,7 @@ require("@budibase/auth").init(CouchDB)
|
||||||
const Koa = require("koa")
|
const Koa = require("koa")
|
||||||
const destroyable = require("server-destroy")
|
const destroyable = require("server-destroy")
|
||||||
const koaBody = require("koa-body")
|
const koaBody = require("koa-body")
|
||||||
|
const koaSession = require("koa-session")
|
||||||
const { passport } = require("@budibase/auth").auth
|
const { passport } = require("@budibase/auth").auth
|
||||||
const logger = require("koa-pino-logger")
|
const logger = require("koa-pino-logger")
|
||||||
const http = require("http")
|
const http = require("http")
|
||||||
|
@ -13,8 +14,11 @@ const redis = require("./utilities/redis")
|
||||||
|
|
||||||
const app = new Koa()
|
const app = new Koa()
|
||||||
|
|
||||||
|
app.keys = ["secret", "key"]
|
||||||
|
|
||||||
// set up top level koa middleware
|
// set up top level koa middleware
|
||||||
app.use(koaBody({ multipart: true }))
|
app.use(koaBody({ multipart: true }))
|
||||||
|
app.use(koaSession(app))
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
logger({
|
logger({
|
||||||
|
|
|
@ -566,6 +566,17 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
defer-to-connect "^2.0.0"
|
defer-to-connect "^2.0.0"
|
||||||
|
|
||||||
|
"@techpass/passport-openidconnect@^0.3.0":
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@techpass/passport-openidconnect/-/passport-openidconnect-0.3.0.tgz#a60b2bbf3f262649a5a02d5d186219944acc3010"
|
||||||
|
integrity sha512-bVsPwl66s7J7GHxTPlW/RJYhZol9SshNznQsx83OOh9G+JWFGoeWxh+xbX+FTdJNoUvGIGbJnpWPY2wC6NOHPw==
|
||||||
|
dependencies:
|
||||||
|
base64url "^3.0.1"
|
||||||
|
oauth "^0.9.15"
|
||||||
|
passport-strategy "^1.0.0"
|
||||||
|
request "^2.88.0"
|
||||||
|
webfinger "^0.4.2"
|
||||||
|
|
||||||
"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7":
|
"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7":
|
||||||
version "7.1.14"
|
version "7.1.14"
|
||||||
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.14.tgz#faaeefc4185ec71c389f4501ee5ec84b170cc402"
|
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.14.tgz#faaeefc4185ec71c389f4501ee5ec84b170cc402"
|
||||||
|
@ -1058,7 +1069,7 @@ base64-js@^1.0.2, base64-js@^1.3.1:
|
||||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||||
|
|
||||||
base64url@3.x.x:
|
base64url@3.x.x, base64url@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
|
resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
|
||||||
integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==
|
integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==
|
||||||
|
@ -4183,7 +4194,7 @@ oauth-sign@~0.9.0:
|
||||||
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
|
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
|
||||||
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
|
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
|
||||||
|
|
||||||
oauth@0.9.x:
|
oauth@0.9.x, oauth@^0.9.15:
|
||||||
version "0.9.15"
|
version "0.9.15"
|
||||||
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
|
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
|
||||||
integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
|
integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
|
||||||
|
@ -4933,7 +4944,7 @@ request-promise-native@^1.0.9:
|
||||||
stealthy-require "^1.1.1"
|
stealthy-require "^1.1.1"
|
||||||
tough-cookie "^2.3.3"
|
tough-cookie "^2.3.3"
|
||||||
|
|
||||||
request@^2.88.2:
|
request@^2.88.0, request@^2.88.2:
|
||||||
version "2.88.2"
|
version "2.88.2"
|
||||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
|
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
|
||||||
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
|
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
|
||||||
|
@ -5080,7 +5091,7 @@ sax@1.2.1:
|
||||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
|
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
|
||||||
integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o=
|
integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o=
|
||||||
|
|
||||||
sax@>=0.6.0:
|
sax@>=0.1.1, sax@>=0.6.0:
|
||||||
version "1.2.4"
|
version "1.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||||
|
@ -5390,6 +5401,11 @@ stealthy-require@^1.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
|
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
|
||||||
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
|
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
|
||||||
|
|
||||||
|
step@0.0.x:
|
||||||
|
version "0.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/step/-/step-0.0.6.tgz#143e7849a5d7d3f4a088fe29af94915216eeede2"
|
||||||
|
integrity sha1-FD54SaXX0/SgiP4pr5SRUhbu7eI=
|
||||||
|
|
||||||
string-length@^4.0.1:
|
string-length@^4.0.1:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"
|
resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"
|
||||||
|
@ -5923,6 +5939,14 @@ walker@^1.0.7, walker@~1.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
makeerror "1.0.x"
|
makeerror "1.0.x"
|
||||||
|
|
||||||
|
webfinger@^0.4.2:
|
||||||
|
version "0.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/webfinger/-/webfinger-0.4.2.tgz#3477a6d97799461896039fcffc650b73468ee76d"
|
||||||
|
integrity sha1-NHem2XeZRhiWA5/P/GULc0aO520=
|
||||||
|
dependencies:
|
||||||
|
step "0.0.x"
|
||||||
|
xml2js "0.1.x"
|
||||||
|
|
||||||
webidl-conversions@^5.0.0:
|
webidl-conversions@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
|
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
|
||||||
|
@ -6031,6 +6055,13 @@ xml-name-validator@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
|
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
|
||||||
integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
|
integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
|
||||||
|
|
||||||
|
xml2js@0.1.x:
|
||||||
|
version "0.1.14"
|
||||||
|
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.1.14.tgz#5274e67f5a64c5f92974cd85139e0332adc6b90c"
|
||||||
|
integrity sha1-UnTmf1pkxfkpdM2FE54DMq3GuQw=
|
||||||
|
dependencies:
|
||||||
|
sax ">=0.1.1"
|
||||||
|
|
||||||
xml2js@0.4.19:
|
xml2js@0.4.19:
|
||||||
version "0.4.19"
|
version "0.4.19"
|
||||||
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
|
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
|
||||||
|
|
Loading…
Reference in New Issue