@budibase/backend-core 2.32.12 → 2.32.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/index.js +125 -49
  2. package/dist/index.js.map +4 -4
  3. package/dist/index.js.meta.json +1 -1
  4. package/dist/package.json +4 -4
  5. package/dist/plugins.js.meta.json +1 -1
  6. package/dist/src/db/couch/DatabaseImpl.js +10 -2
  7. package/dist/src/db/couch/DatabaseImpl.js.map +1 -1
  8. package/dist/src/features/features.d.ts +47 -0
  9. package/dist/src/features/features.js +269 -0
  10. package/dist/src/features/features.js.map +1 -0
  11. package/dist/src/features/index.d.ts +2 -39
  12. package/dist/src/features/index.js +6 -235
  13. package/dist/src/features/index.js.map +1 -1
  14. package/dist/src/features/tests/utils.d.ts +3 -0
  15. package/dist/src/features/tests/utils.js +56 -0
  16. package/dist/src/features/tests/utils.js.map +1 -0
  17. package/dist/src/middleware/passport/sso/sso.js +0 -25
  18. package/dist/src/middleware/passport/sso/sso.js.map +1 -1
  19. package/dist/src/users/users.js +8 -1
  20. package/dist/src/users/users.js.map +1 -1
  21. package/dist/tests/core/utilities/structures/accounts.js +2 -2
  22. package/dist/tests/core/utilities/structures/accounts.js.map +1 -1
  23. package/dist/tests/core/utilities/structures/users.js +2 -5
  24. package/dist/tests/core/utilities/structures/users.js.map +1 -1
  25. package/package.json +4 -4
  26. package/src/db/couch/DatabaseImpl.ts +12 -2
  27. package/src/features/features.ts +300 -0
  28. package/src/features/index.ts +2 -281
  29. package/src/features/tests/utils.ts +64 -0
  30. package/src/middleware/passport/sso/sso.ts +0 -24
  31. package/src/users/users.ts +10 -2
  32. package/tests/core/utilities/structures/accounts.ts +0 -4
  33. package/tests/core/utilities/structures/users.ts +0 -5
@@ -1,281 +1,2 @@
1
- import env from "../environment"
2
- import * as context from "../context"
3
- import { PostHog, PostHogOptions } from "posthog-node"
4
- import { FeatureFlag, IdentityType, UserCtx } from "@budibase/types"
5
- import tracer from "dd-trace"
6
- import { Duration } from "../utils"
7
-
8
- let posthog: PostHog | undefined
9
- export function init(opts?: PostHogOptions) {
10
- if (
11
- env.POSTHOG_TOKEN &&
12
- env.POSTHOG_API_HOST &&
13
- !env.SELF_HOSTED &&
14
- env.POSTHOG_FEATURE_FLAGS_ENABLED
15
- ) {
16
- console.log("initializing posthog client...")
17
- posthog = new PostHog(env.POSTHOG_TOKEN, {
18
- host: env.POSTHOG_API_HOST,
19
- personalApiKey: env.POSTHOG_PERSONAL_TOKEN,
20
- featureFlagsPollingInterval: Duration.fromMinutes(3).toMs(),
21
- ...opts,
22
- })
23
- } else {
24
- console.log("posthog disabled")
25
- }
26
- }
27
-
28
- export function shutdown() {
29
- posthog?.shutdown()
30
- }
31
-
32
- export abstract class Flag<T> {
33
- static boolean(defaultValue: boolean): Flag<boolean> {
34
- return new BooleanFlag(defaultValue)
35
- }
36
-
37
- static string(defaultValue: string): Flag<string> {
38
- return new StringFlag(defaultValue)
39
- }
40
-
41
- static number(defaultValue: number): Flag<number> {
42
- return new NumberFlag(defaultValue)
43
- }
44
-
45
- protected constructor(public defaultValue: T) {}
46
-
47
- abstract parse(value: any): T
48
- }
49
-
50
- type UnwrapFlag<F> = F extends Flag<infer U> ? U : never
51
-
52
- export type FlagValues<T> = {
53
- [K in keyof T]: UnwrapFlag<T[K]>
54
- }
55
-
56
- type KeysOfType<T, U> = {
57
- [K in keyof T]: T[K] extends Flag<U> ? K : never
58
- }[keyof T]
59
-
60
- class BooleanFlag extends Flag<boolean> {
61
- parse(value: any) {
62
- if (typeof value === "string") {
63
- return ["true", "t", "1"].includes(value.toLowerCase())
64
- }
65
-
66
- if (typeof value === "boolean") {
67
- return value
68
- }
69
-
70
- throw new Error(`could not parse value "${value}" as boolean`)
71
- }
72
- }
73
-
74
- class StringFlag extends Flag<string> {
75
- parse(value: any) {
76
- if (typeof value === "string") {
77
- return value
78
- }
79
- throw new Error(`could not parse value "${value}" as string`)
80
- }
81
- }
82
-
83
- class NumberFlag extends Flag<number> {
84
- parse(value: any) {
85
- if (typeof value === "number") {
86
- return value
87
- }
88
-
89
- if (typeof value === "string") {
90
- const parsed = parseFloat(value)
91
- if (!isNaN(parsed)) {
92
- return parsed
93
- }
94
- }
95
-
96
- throw new Error(`could not parse value "${value}" as number`)
97
- }
98
- }
99
-
100
- export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
101
- // This is used to safely cache flags sets in the current request context.
102
- // Because multiple sets could theoretically exist, we don't want the cache of
103
- // one to leak into another.
104
- private readonly setId: string
105
-
106
- constructor(private readonly flagSchema: T) {
107
- this.setId = crypto.randomUUID()
108
- }
109
-
110
- defaults(): FlagValues<T> {
111
- return Object.keys(this.flagSchema).reduce((acc, key) => {
112
- const typedKey = key as keyof T
113
- acc[typedKey] = this.flagSchema[key].defaultValue
114
- return acc
115
- }, {} as FlagValues<T>)
116
- }
117
-
118
- isFlagName(name: string | number | symbol): name is keyof T {
119
- return this.flagSchema[name as keyof T] !== undefined
120
- }
121
-
122
- async get<K extends keyof T>(
123
- key: K,
124
- ctx?: UserCtx
125
- ): Promise<FlagValues<T>[K]> {
126
- const flags = await this.fetch(ctx)
127
- return flags[key]
128
- }
129
-
130
- async isEnabled<K extends KeysOfType<T, boolean>>(
131
- key: K,
132
- ctx?: UserCtx
133
- ): Promise<boolean> {
134
- const flags = await this.fetch(ctx)
135
- return flags[key]
136
- }
137
-
138
- async fetch(ctx?: UserCtx): Promise<FlagValues<T>> {
139
- return await tracer.trace("features.fetch", async span => {
140
- const cachedFlags = context.getFeatureFlags<FlagValues<T>>(this.setId)
141
- if (cachedFlags) {
142
- span?.addTags({ fromCache: true })
143
- return cachedFlags
144
- }
145
-
146
- const tags: Record<string, any> = {}
147
- const flagValues = this.defaults()
148
- const currentTenantId = context.getTenantId()
149
- const specificallySetFalse = new Set<string>()
150
-
151
- const split = (env.TENANT_FEATURE_FLAGS || "")
152
- .split(",")
153
- .map(x => x.split(":"))
154
- for (const [tenantId, ...features] of split) {
155
- if (!tenantId || (tenantId !== "*" && tenantId !== currentTenantId)) {
156
- continue
157
- }
158
-
159
- tags[`readFromEnvironmentVars`] = true
160
-
161
- for (let feature of features) {
162
- let value = true
163
- if (feature.startsWith("!")) {
164
- feature = feature.slice(1)
165
- value = false
166
- specificallySetFalse.add(feature)
167
- }
168
-
169
- // ignore unknown flags
170
- if (!this.isFlagName(feature)) {
171
- continue
172
- }
173
-
174
- if (typeof flagValues[feature] !== "boolean") {
175
- throw new Error(`Feature: ${feature} is not a boolean`)
176
- }
177
-
178
- // @ts-expect-error - TS does not like you writing into a generic type,
179
- // but we know that it's okay in this case because it's just an object.
180
- flagValues[feature as keyof FlagValues] = value
181
- tags[`flags.${feature}.source`] = "environment"
182
- }
183
- }
184
-
185
- const license = ctx?.user?.license
186
- if (license) {
187
- tags[`readFromLicense`] = true
188
-
189
- for (const feature of license.features) {
190
- if (!this.isFlagName(feature)) {
191
- continue
192
- }
193
-
194
- if (
195
- flagValues[feature] === true ||
196
- specificallySetFalse.has(feature)
197
- ) {
198
- // If the flag is already set to through environment variables, we
199
- // don't want to override it back to false here.
200
- continue
201
- }
202
-
203
- // @ts-expect-error - TS does not like you writing into a generic type,
204
- // but we know that it's okay in this case because it's just an object.
205
- flagValues[feature] = true
206
- tags[`flags.${feature}.source`] = "license"
207
- }
208
- }
209
-
210
- const identity = context.getIdentity()
211
- tags[`identity.type`] = identity?.type
212
- tags[`identity.tenantId`] = identity?.tenantId
213
- tags[`identity._id`] = identity?._id
214
-
215
- if (posthog && identity?.type === IdentityType.USER) {
216
- tags[`readFromPostHog`] = true
217
-
218
- const personProperties: Record<string, string> = {}
219
- if (identity.tenantId) {
220
- personProperties.tenantId = identity.tenantId
221
- }
222
-
223
- const posthogFlags = await posthog.getAllFlagsAndPayloads(
224
- identity._id,
225
- {
226
- personProperties,
227
- }
228
- )
229
-
230
- for (const [name, value] of Object.entries(posthogFlags.featureFlags)) {
231
- if (!this.isFlagName(name)) {
232
- // We don't want an unexpected PostHog flag to break the app, so we
233
- // just log it and continue.
234
- console.warn(`Unexpected posthog flag "${name}": ${value}`)
235
- continue
236
- }
237
-
238
- if (flagValues[name] === true || specificallySetFalse.has(name)) {
239
- // If the flag is already set to through environment variables, we
240
- // don't want to override it back to false here.
241
- continue
242
- }
243
-
244
- const payload = posthogFlags.featureFlagPayloads?.[name]
245
- const flag = this.flagSchema[name]
246
- try {
247
- // @ts-expect-error - TS does not like you writing into a generic
248
- // type, but we know that it's okay in this case because it's just
249
- // an object.
250
- flagValues[name] = flag.parse(payload || value)
251
- tags[`flags.${name}.source`] = "posthog"
252
- } catch (err) {
253
- // We don't want an invalid PostHog flag to break the app, so we just
254
- // log it and continue.
255
- console.warn(`Error parsing posthog flag "${name}": ${value}`, err)
256
- }
257
- }
258
- }
259
-
260
- context.setFeatureFlags(this.setId, flagValues)
261
- for (const [key, value] of Object.entries(flagValues)) {
262
- tags[`flags.${key}.value`] = value
263
- }
264
- span?.addTags(tags)
265
-
266
- return flagValues
267
- })
268
- }
269
- }
270
-
271
- // This is the primary source of truth for feature flags. If you want to add a
272
- // new flag, add it here and use the `fetch` and `get` functions to access it.
273
- // All of the machinery in this file is to make sure that flags have their
274
- // default values set correctly and their types flow through the system.
275
- export const flags = new FlagSet({
276
- DEFAULT_VALUES: Flag.boolean(env.isDev()),
277
- AUTOMATION_BRANCHING: Flag.boolean(env.isDev()),
278
- SQS: Flag.boolean(env.isDev()),
279
- [FeatureFlag.AI_CUSTOM_CONFIGS]: Flag.boolean(env.isDev()),
280
- [FeatureFlag.ENRICHED_RELATIONSHIPS]: Flag.boolean(env.isDev()),
281
- })
1
+ export * from "./features"
2
+ export * as testutils from "./tests/utils"
@@ -0,0 +1,64 @@
1
+ import { FeatureFlags, parseEnvFlags } from ".."
2
+ import { setEnv } from "../../environment"
3
+
4
+ function getCurrentFlags(): Record<string, Record<string, boolean>> {
5
+ const result: Record<string, Record<string, boolean>> = {}
6
+ for (const { tenantId, key, value } of parseEnvFlags(
7
+ process.env.TENANT_FEATURE_FLAGS || ""
8
+ )) {
9
+ const tenantFlags = result[tenantId] || {}
10
+ // Don't allow overwriting specifically false flags, to match the beheaviour
11
+ // of FlagSet.
12
+ if (tenantFlags[key] === false) {
13
+ continue
14
+ }
15
+ tenantFlags[key] = value
16
+ result[tenantId] = tenantFlags
17
+ }
18
+ return result
19
+ }
20
+
21
+ function buildFlagString(
22
+ flags: Record<string, Record<string, boolean>>
23
+ ): string {
24
+ const parts: string[] = []
25
+ for (const [tenantId, tenantFlags] of Object.entries(flags)) {
26
+ for (const [key, value] of Object.entries(tenantFlags)) {
27
+ if (value === false) {
28
+ parts.push(`${tenantId}:!${key}`)
29
+ } else {
30
+ parts.push(`${tenantId}:${key}`)
31
+ }
32
+ }
33
+ }
34
+ return parts.join(",")
35
+ }
36
+
37
+ export function setFeatureFlags(
38
+ tenantId: string,
39
+ flags: Partial<FeatureFlags>
40
+ ): () => void {
41
+ const current = getCurrentFlags()
42
+ for (const [key, value] of Object.entries(flags)) {
43
+ const tenantFlags = current[tenantId] || {}
44
+ tenantFlags[key] = value
45
+ current[tenantId] = tenantFlags
46
+ }
47
+ const flagString = buildFlagString(current)
48
+ return setEnv({ TENANT_FEATURE_FLAGS: flagString })
49
+ }
50
+
51
+ export function withFeatureFlags<T>(
52
+ tenantId: string,
53
+ flags: Partial<FeatureFlags>,
54
+ f: () => T
55
+ ) {
56
+ const cleanup = setFeatureFlags(tenantId, flags)
57
+ const result = f()
58
+ if (result instanceof Promise) {
59
+ return result.finally(cleanup)
60
+ } else {
61
+ cleanup()
62
+ return result
63
+ }
64
+ }
@@ -2,7 +2,6 @@ import { generateGlobalUserID } from "../../../db"
2
2
  import { authError } from "../utils"
3
3
  import * as users from "../../../users"
4
4
  import * as context from "../../../context"
5
- import fetch from "node-fetch"
6
5
  import {
7
6
  SaveSSOUserFunction,
8
7
  SSOAuthDetails,
@@ -97,28 +96,13 @@ export async function authenticate(
97
96
  return done(null, ssoUser)
98
97
  }
99
98
 
100
- async function getProfilePictureUrl(user: User, details: SSOAuthDetails) {
101
- const pictureUrl = details.profile?._json.picture
102
- if (pictureUrl) {
103
- const response = await fetch(pictureUrl)
104
- if (response.status === 200) {
105
- const type = response.headers.get("content-type") as string
106
- if (type.startsWith("image/")) {
107
- return pictureUrl
108
- }
109
- }
110
- }
111
- }
112
-
113
99
  /**
114
100
  * @returns a user that has been sync'd with third party information
115
101
  */
116
102
  async function syncUser(user: User, details: SSOAuthDetails): Promise<SSOUser> {
117
103
  let firstName
118
104
  let lastName
119
- let pictureUrl
120
105
  let oauth2
121
- let thirdPartyProfile
122
106
 
123
107
  if (details.profile) {
124
108
  const profile = details.profile
@@ -134,12 +118,6 @@ async function syncUser(user: User, details: SSOAuthDetails): Promise<SSOUser> {
134
118
  lastName = name.familyName
135
119
  }
136
120
  }
137
-
138
- pictureUrl = await getProfilePictureUrl(user, details)
139
-
140
- thirdPartyProfile = {
141
- ...profile._json,
142
- }
143
121
  }
144
122
 
145
123
  // oauth tokens for future use
@@ -155,8 +133,6 @@ async function syncUser(user: User, details: SSOAuthDetails): Promise<SSOUser> {
155
133
  providerType: details.providerType,
156
134
  firstName,
157
135
  lastName,
158
- thirdPartyProfile,
159
- pictureUrl,
160
136
  oauth2,
161
137
  }
162
138
  }
@@ -24,6 +24,7 @@ import * as context from "../context"
24
24
  import { getGlobalDB } from "../context"
25
25
  import { isCreator } from "./utils"
26
26
  import { UserDB } from "./db"
27
+ import { dataFilters } from "@budibase/shared-core"
27
28
 
28
29
  type GetOpts = { cleanup?: boolean }
29
30
 
@@ -262,10 +263,17 @@ export async function paginatedUsers({
262
263
  userList = await bulkGetGlobalUsersById(query?.oneOf?._id, {
263
264
  cleanup: true,
264
265
  })
266
+ } else if (query) {
267
+ // TODO: this should use SQS search, but the logic is built in the 'server' package. Using the in-memory filtering to get this working meanwhile
268
+ const response = await db.allDocs<User>(
269
+ getGlobalUserParams(null, { ...opts, limit: undefined })
270
+ )
271
+ userList = response.rows.map(row => row.doc!)
272
+ userList = dataFilters.search(userList, { query, limit: opts.limit }).rows
265
273
  } else {
266
274
  // no search, query allDocs
267
- const response = await db.allDocs(getGlobalUserParams(null, opts))
268
- userList = response.rows.map((row: any) => row.doc)
275
+ const response = await db.allDocs<User>(getGlobalUserParams(null, opts))
276
+ userList = response.rows.map(row => row.doc!)
269
277
  }
270
278
  return pagination(userList, pageSize, {
271
279
  paginate: true,
@@ -59,10 +59,8 @@ export function ssoAccount(account: Account = cloudAccount()): SSOAccount {
59
59
  accessToken: generator.string(),
60
60
  refreshToken: generator.string(),
61
61
  },
62
- pictureUrl: generator.url(),
63
62
  provider: provider(),
64
63
  providerType: providerType(),
65
- thirdPartyProfile: {},
66
64
  }
67
65
  }
68
66
 
@@ -76,9 +74,7 @@ export function verifiableSsoAccount(
76
74
  accessToken: generator.string(),
77
75
  refreshToken: generator.string(),
78
76
  },
79
- pictureUrl: generator.url(),
80
77
  provider: AccountSSOProvider.MICROSOFT,
81
78
  providerType: AccountSSOProviderType.MICROSOFT,
82
- thirdPartyProfile: { id: "abc123" },
83
79
  }
84
80
  }
@@ -25,7 +25,6 @@ export const user = (userProps?: Partial<Omit<User, "userId">>): User => {
25
25
  roles: { app_test: "admin" },
26
26
  firstName: generator.first(),
27
27
  lastName: generator.last(),
28
- pictureUrl: "http://example.com",
29
28
  tenantId: tenant.id(),
30
29
  ...userProps,
31
30
  }
@@ -86,9 +85,5 @@ export function ssoUser(
86
85
  oauth2: opts.details?.oauth2,
87
86
  provider: opts.details?.provider!,
88
87
  providerType: opts.details?.providerType!,
89
- thirdPartyProfile: {
90
- email: base.email,
91
- picture: base.pictureUrl,
92
- },
93
88
  }
94
89
  }