@budibase/backend-core 2.30.3 → 2.30.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@budibase/backend-core",
3
- "version": "2.30.3",
3
+ "version": "2.30.4",
4
4
  "description": "Budibase backend core libraries used in server and worker",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -23,8 +23,8 @@
23
23
  "dependencies": {
24
24
  "@budibase/nano": "10.1.5",
25
25
  "@budibase/pouchdb-replication-stream": "1.2.11",
26
- "@budibase/shared-core": "2.30.3",
27
- "@budibase/types": "2.30.3",
26
+ "@budibase/shared-core": "2.30.4",
27
+ "@budibase/types": "2.30.4",
28
28
  "aws-cloudfront-sign": "3.0.2",
29
29
  "aws-sdk": "2.1030.0",
30
30
  "bcrypt": "5.1.0",
@@ -95,5 +95,5 @@
95
95
  }
96
96
  }
97
97
  },
98
- "gitHead": "bb6092e58b80d4343bf3b720881f2dc01a30d1c9"
98
+ "gitHead": "31ecd970317f72024de072a54e4d6759333e2b80"
99
99
  }
@@ -375,3 +375,22 @@ export function getCurrentContext(): ContextMap | undefined {
375
375
  return undefined
376
376
  }
377
377
  }
378
+
379
+ export function getFeatureFlags<T extends Record<string, any>>(
380
+ key: string
381
+ ): T | undefined {
382
+ const context = getCurrentContext()
383
+ if (!context) {
384
+ return undefined
385
+ }
386
+ return context.featureFlagCache?.[key] as T
387
+ }
388
+
389
+ export function setFeatureFlags(key: string, value: Record<string, any>) {
390
+ const context = getCurrentContext()
391
+ if (!context) {
392
+ return
393
+ }
394
+ context.featureFlagCache ??= {}
395
+ context.featureFlagCache[key] = value
396
+ }
@@ -2,7 +2,7 @@ import { testEnv } from "../../../tests/extra"
2
2
  import * as context from "../"
3
3
  import { DEFAULT_TENANT_ID } from "../../constants"
4
4
  import { structures } from "../../../tests"
5
- import { db } from "../.."
5
+ import * as db from "../../db"
6
6
  import Context from "../Context"
7
7
  import { ContextMap } from "../types"
8
8
  import { IdentityType } from "@budibase/types"
@@ -18,4 +18,7 @@ export type ContextMap = {
18
18
  oauthClient: OAuth2Client
19
19
  clients: Record<string, GoogleSpreadsheet>
20
20
  }
21
+ featureFlagCache?: {
22
+ [key: string]: Record<string, any>
23
+ }
21
24
  }
@@ -18,6 +18,10 @@ export function init(opts?: PostHogOptions) {
18
18
  }
19
19
  }
20
20
 
21
+ export function shutdown() {
22
+ posthog?.shutdown()
23
+ }
24
+
21
25
  export abstract class Flag<T> {
22
26
  static boolean(defaultValue: boolean): Flag<boolean> {
23
27
  return new BooleanFlag(defaultValue)
@@ -87,7 +91,14 @@ class NumberFlag extends Flag<number> {
87
91
  }
88
92
 
89
93
  export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
90
- constructor(private readonly flagSchema: T) {}
94
+ // This is used to safely cache flags sets in the current request context.
95
+ // Because multiple sets could theoretically exist, we don't want the cache of
96
+ // one to leak into another.
97
+ private readonly setId: string
98
+
99
+ constructor(private readonly flagSchema: T) {
100
+ this.setId = crypto.randomUUID()
101
+ }
91
102
 
92
103
  defaults(): FlagValues<T> {
93
104
  return Object.keys(this.flagSchema).reduce((acc, key) => {
@@ -119,6 +130,12 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
119
130
 
120
131
  async fetch(ctx?: UserCtx): Promise<FlagValues<T>> {
121
132
  return await tracer.trace("features.fetch", async span => {
133
+ const cachedFlags = context.getFeatureFlags<FlagValues<T>>(this.setId)
134
+ if (cachedFlags) {
135
+ span?.addTags({ fromCache: true })
136
+ return cachedFlags
137
+ }
138
+
122
139
  const tags: Record<string, any> = {}
123
140
  const flagValues = this.defaults()
124
141
  const currentTenantId = context.getTenantId()
@@ -142,8 +159,9 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
142
159
  specificallySetFalse.add(feature)
143
160
  }
144
161
 
162
+ // ignore unknown flags
145
163
  if (!this.isFlagName(feature)) {
146
- throw new Error(`Feature: ${feature} is not an allowed option`)
164
+ continue
147
165
  }
148
166
 
149
167
  if (typeof flagValues[feature] !== "boolean") {
@@ -152,7 +170,7 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
152
170
 
153
171
  // @ts-expect-error - TS does not like you writing into a generic type,
154
172
  // but we know that it's okay in this case because it's just an object.
155
- flagValues[feature] = value
173
+ flagValues[feature as keyof FlagValues] = value
156
174
  tags[`flags.${feature}.source`] = "environment"
157
175
  }
158
176
  }
@@ -187,10 +205,7 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
187
205
  tags[`identity.tenantId`] = identity?.tenantId
188
206
  tags[`identity._id`] = identity?._id
189
207
 
190
- // Until we're confident this performs well, we're only enabling it in QA
191
- // and test environments.
192
- const usePosthog = env.isTest() || env.isQA()
193
- if (usePosthog && posthog && identity?.type === IdentityType.USER) {
208
+ if (posthog && identity?.type === IdentityType.USER) {
194
209
  tags[`readFromPostHog`] = true
195
210
 
196
211
  const personProperties: Record<string, string> = {}
@@ -204,7 +219,6 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
204
219
  personProperties,
205
220
  }
206
221
  )
207
- console.log("posthog flags", JSON.stringify(posthogFlags))
208
222
 
209
223
  for (const [name, value] of Object.entries(posthogFlags.featureFlags)) {
210
224
  if (!this.isFlagName(name)) {
@@ -236,6 +250,7 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
236
250
  }
237
251
  }
238
252
 
253
+ context.setFeatureFlags(this.setId, flagValues)
239
254
  for (const [key, value] of Object.entries(flagValues)) {
240
255
  tags[`flags.${key}.value`] = value
241
256
  }
@@ -251,8 +266,5 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
251
266
  // All of the machinery in this file is to make sure that flags have their
252
267
  // default values set correctly and their types flow through the system.
253
268
  export const flags = new FlagSet({
254
- LICENSING: Flag.boolean(false),
255
- GOOGLE_SHEETS: Flag.boolean(false),
256
- USER_GROUPS: Flag.boolean(false),
257
- ONBOARDING_TOUR: Flag.boolean(false),
269
+ DEFAULT_VALUES: Flag.boolean(false),
258
270
  })
@@ -1,6 +1,6 @@
1
1
  import { IdentityContext, IdentityType, UserCtx } from "@budibase/types"
2
- import { Flag, FlagSet, FlagValues, init } from "../"
3
- import { context } from "../.."
2
+ import { Flag, FlagSet, FlagValues, init, shutdown } from "../"
3
+ import * as context from "../../context"
4
4
  import environment, { withEnv } from "../../environment"
5
5
  import nodeFetch from "node-fetch"
6
6
  import nock from "nock"
@@ -67,9 +67,9 @@ describe("feature flags", () => {
67
67
  expected: flags.defaults(),
68
68
  },
69
69
  {
70
- it: "should fail when an environment flag is not recognised",
70
+ it: "should ignore unknown feature flags",
71
71
  environmentFlags: "default:TEST_BOOLEAN,default:FOO",
72
- errorMessage: "Feature: FOO is not an allowed option",
72
+ expected: { TEST_BOOLEAN: true },
73
73
  },
74
74
  {
75
75
  it: "should be able to read boolean flags from PostHog",
@@ -197,6 +197,8 @@ describe("feature flags", () => {
197
197
  throw new Error("No expected value")
198
198
  }
199
199
  })
200
+
201
+ shutdown()
200
202
  })
201
203
  }
202
204
  )
@@ -1,7 +1,7 @@
1
1
  import { GenericContainer, StartedTestContainer } from "testcontainers"
2
2
  import { generator, structures } from "../../../tests"
3
3
  import RedisWrapper, { closeAll } from "../redis"
4
- import { env } from "../.."
4
+ import env from "../../environment"
5
5
  import { randomUUID } from "crypto"
6
6
 
7
7
  jest.setTimeout(30000)