@budibase/backend-core 2.30.2 → 2.30.3

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 (57) hide show
  1. package/dist/index.js +267 -89
  2. package/dist/index.js.map +3 -3
  3. package/dist/index.js.meta.json +1 -1
  4. package/dist/package.json +5 -5
  5. package/dist/plugins.js.meta.json +1 -1
  6. package/dist/src/environment.d.ts +4 -0
  7. package/dist/src/environment.js +27 -1
  8. package/dist/src/environment.js.map +1 -1
  9. package/dist/src/events/processors/posthog/PosthogProcessor.d.ts +1 -1
  10. package/dist/src/events/processors/posthog/PosthogProcessor.js +2 -2
  11. package/dist/src/events/processors/posthog/PosthogProcessor.js.map +1 -1
  12. package/dist/src/features/index.d.ts +29 -26
  13. package/dist/src/features/index.js +195 -79
  14. package/dist/src/features/index.js.map +1 -1
  15. package/dist/src/index.d.ts +1 -1
  16. package/dist/src/index.js +3 -1
  17. package/dist/src/index.js.map +1 -1
  18. package/dist/src/redis/redis.d.ts +1 -0
  19. package/dist/src/redis/redis.js +4 -0
  20. package/dist/src/redis/redis.js.map +1 -1
  21. package/dist/src/security/auth.js +1 -1
  22. package/dist/src/security/auth.js.map +1 -1
  23. package/dist/src/sql/sqlTable.js +23 -8
  24. package/dist/src/sql/sqlTable.js.map +1 -1
  25. package/dist/tests/core/utilities/mocks/index.d.ts +0 -2
  26. package/dist/tests/core/utilities/mocks/index.js +1 -7
  27. package/dist/tests/core/utilities/mocks/index.js.map +1 -1
  28. package/dist/tests/core/utilities/structures/users.js +1 -1
  29. package/dist/tests/core/utilities/structures/users.js.map +1 -1
  30. package/dist/tests/jestSetup.js +7 -2
  31. package/dist/tests/jestSetup.js.map +1 -1
  32. package/package.json +5 -5
  33. package/src/environment.ts +29 -0
  34. package/src/events/processors/posthog/PosthogProcessor.ts +1 -1
  35. package/src/events/processors/posthog/tests/PosthogProcessor.spec.ts +16 -22
  36. package/src/features/index.ts +231 -81
  37. package/src/features/tests/features.spec.ts +204 -60
  38. package/src/index.ts +1 -1
  39. package/src/middleware/passport/sso/tests/oidc.spec.ts +4 -12
  40. package/src/middleware/passport/sso/tests/sso.spec.ts +10 -12
  41. package/src/plugin/tests/validation.spec.ts +168 -42
  42. package/src/redis/redis.ts +4 -0
  43. package/src/redis/tests/redis.spec.ts +5 -2
  44. package/src/security/auth.ts +1 -1
  45. package/src/security/tests/auth.spec.ts +2 -2
  46. package/src/sql/sqlTable.ts +21 -7
  47. package/tests/core/utilities/mocks/index.ts +0 -2
  48. package/tests/core/utilities/structures/users.ts +1 -1
  49. package/tests/jestSetup.ts +10 -3
  50. package/dist/tests/core/utilities/mocks/fetch.d.ts +0 -32
  51. package/dist/tests/core/utilities/mocks/fetch.js +0 -15
  52. package/dist/tests/core/utilities/mocks/fetch.js.map +0 -1
  53. package/dist/tests/core/utilities/mocks/posthog.d.ts +0 -0
  54. package/dist/tests/core/utilities/mocks/posthog.js +0 -9
  55. package/dist/tests/core/utilities/mocks/posthog.js.map +0 -1
  56. package/tests/core/utilities/mocks/fetch.ts +0 -17
  57. package/tests/core/utilities/mocks/posthog.ts +0 -7
@@ -25,19 +25,13 @@ var __importStar = (this && this.__importStar) || function (mod) {
25
25
  var __exportStar = (this && this.__exportStar) || function(m, exports) {
26
26
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
27
27
  };
28
- var __importDefault = (this && this.__importDefault) || function (mod) {
29
- return (mod && mod.__esModule) ? mod : { "default": mod };
30
- };
31
28
  Object.defineProperty(exports, "__esModule", { value: true });
32
- exports.fetch = exports.licenses = exports.date = exports.accounts = void 0;
29
+ exports.licenses = exports.date = exports.accounts = void 0;
33
30
  jest.mock("../../../../src/accounts");
34
31
  const _accounts = __importStar(require("../../../../src/accounts"));
35
32
  exports.accounts = jest.mocked(_accounts);
36
33
  exports.date = __importStar(require("./date"));
37
34
  exports.licenses = __importStar(require("./licenses"));
38
- var fetch_1 = require("./fetch");
39
- Object.defineProperty(exports, "fetch", { enumerable: true, get: function () { return __importDefault(fetch_1).default; } });
40
35
  __exportStar(require("./alerts"), exports);
41
36
  require("./events");
42
- require("./posthog");
43
37
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../tests/core/utilities/mocks/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,IAAI,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;AACrC,oEAAqD;AAExC,QAAA,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;AAE9C,+CAA8B;AAC9B,uDAAsC;AACtC,iCAA0C;AAAjC,+GAAA,OAAO,OAAS;AACzB,2CAAwB;AACxB,oBAAiB;AACjB,qBAAkB"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../tests/core/utilities/mocks/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,IAAI,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;AACrC,oEAAqD;AAExC,QAAA,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;AAE9C,+CAA8B;AAC9B,uDAAsC;AACtC,2CAAwB;AACxB,oBAAiB"}
@@ -12,7 +12,7 @@ const newEmail = () => {
12
12
  exports.newEmail = newEmail;
13
13
  const user = (userProps) => {
14
14
  const userId = userProps === null || userProps === void 0 ? void 0 : userProps._id;
15
- return Object.assign({ _id: userId, userId, email: (0, exports.newEmail)(), password: "password", roles: { app_test: "admin" }, firstName: generator_1.generator.first(), lastName: generator_1.generator.last(), pictureUrl: "http://example.com", tenantId: _1.tenant.id() }, userProps);
15
+ return Object.assign({ _id: userId, userId, email: (0, exports.newEmail)(), password: "password123!", roles: { app_test: "admin" }, firstName: generator_1.generator.first(), lastName: generator_1.generator.last(), pictureUrl: "http://example.com", tenantId: _1.tenant.id() }, userProps);
16
16
  };
17
17
  exports.user = user;
18
18
  const adminUser = (userProps) => {
@@ -1 +1 @@
1
- {"version":3,"file":"users.js","sourceRoot":"","sources":["../../../../../tests/core/utilities/structures/users.ts"],"names":[],"mappings":";;;AAwEA,0BAqBC;AArFD,+BAAmC;AACnC,qCAA+B;AAC/B,2CAAuC;AACvC,wBAA0B;AAEnB,MAAM,QAAQ,GAAG,GAAG,EAAE;IAC3B,OAAO,GAAG,IAAA,aAAI,GAAE,cAAc,CAAA;AAChC,CAAC,CAAA;AAFY,QAAA,QAAQ,YAEpB;AAEM,MAAM,IAAI,GAAG,CAAC,SAAyC,EAAQ,EAAE;IACtE,MAAM,MAAM,GAAG,SAAS,aAAT,SAAS,uBAAT,SAAS,CAAE,GAAG,CAAA;IAC7B,uBACE,GAAG,EAAE,MAAM,EACX,MAAM,EACN,KAAK,EAAE,IAAA,gBAAQ,GAAE,EACjB,QAAQ,EAAE,UAAU,EACpB,KAAK,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,EAC5B,SAAS,EAAE,qBAAS,CAAC,KAAK,EAAE,EAC5B,QAAQ,EAAE,qBAAS,CAAC,IAAI,EAAE,EAC1B,UAAU,EAAE,oBAAoB,EAChC,QAAQ,EAAE,SAAM,CAAC,EAAE,EAAE,IAClB,SAAS,EACb;AACH,CAAC,CAAA;AAdY,QAAA,IAAI,QAchB;AAEM,MAAM,SAAS,GAAG,CAAC,SAAe,EAAa,EAAE;IACtD,uCACK,IAAA,YAAI,EAAC,SAAS,CAAC,KAClB,KAAK,EAAE;YACL,MAAM,EAAE,IAAI;SACb,EACD,OAAO,EAAE;YACP,MAAM,EAAE,IAAI;SACb,IACF;AACH,CAAC,CAAA;AAVY,QAAA,SAAS,aAUrB;AAEM,MAAM,aAAa,GAAG,CAAC,SAAe,EAAiB,EAAE;IAC9D,uCACK,IAAA,YAAI,EAAC,SAAS,CAAC,KAClB,KAAK,EAAE;YACL,MAAM,EAAE,IAAI;SACb,IACF;AACH,CAAC,CAAA;AAPY,QAAA,aAAa,iBAOzB;AAEM,MAAM,WAAW,GAAG,CAAC,SAAyB,EAAe,EAAE;IACpE,uCACK,IAAA,YAAI,EAAC,SAAS,CAAC,KAClB,OAAO,EAAE;YACP,MAAM,EAAE,IAAI;SACb,IACF;AACH,CAAC,CAAA;AAPY,QAAA,WAAW,eAOvB;AAEM,MAAM,cAAc,GAAG,CAAC,KAAa,EAAE,SAAe,EAAe,EAAE;IAC5E,uCACK,IAAA,YAAI,EAAC,SAAS,CAAC,KAClB,OAAO,EAAE;YACP,IAAI,EAAE,CAAC,KAAK,CAAC;SACd,IACF;AACH,CAAC,CAAA;AAPY,QAAA,cAAc,kBAO1B;AAED,SAAgB,OAAO,CACrB,OAAiD,EAAE;;IAEnD,MAAM,IAAI,GAAG,IAAA,YAAI,EAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC5B,OAAO,IAAI,CAAC,QAAQ,CAAA;IAEpB,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QAClB,IAAI,CAAC,OAAO,GAAG,IAAA,iBAAW,EAAC,IAAI,CAAC,CAAA;IAClC,CAAC;IAED,uCACK,IAAI,KACP,kBAAkB,EAAE,KAAK,EACzB,MAAM,EAAE,MAAA,IAAI,CAAC,OAAO,0CAAE,MAAM,EAC5B,QAAQ,EAAE,MAAA,IAAI,CAAC,OAAO,0CAAE,QAAS,EACjC,YAAY,EAAE,MAAA,IAAI,CAAC,OAAO,0CAAE,YAAa,EACzC,iBAAiB,EAAE;YACjB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,OAAO,EAAE,IAAI,CAAC,UAAU;SACzB,IACF;AACH,CAAC"}
1
+ {"version":3,"file":"users.js","sourceRoot":"","sources":["../../../../../tests/core/utilities/structures/users.ts"],"names":[],"mappings":";;;AAwEA,0BAqBC;AArFD,+BAAmC;AACnC,qCAA+B;AAC/B,2CAAuC;AACvC,wBAA0B;AAEnB,MAAM,QAAQ,GAAG,GAAG,EAAE;IAC3B,OAAO,GAAG,IAAA,aAAI,GAAE,cAAc,CAAA;AAChC,CAAC,CAAA;AAFY,QAAA,QAAQ,YAEpB;AAEM,MAAM,IAAI,GAAG,CAAC,SAAyC,EAAQ,EAAE;IACtE,MAAM,MAAM,GAAG,SAAS,aAAT,SAAS,uBAAT,SAAS,CAAE,GAAG,CAAA;IAC7B,uBACE,GAAG,EAAE,MAAM,EACX,MAAM,EACN,KAAK,EAAE,IAAA,gBAAQ,GAAE,EACjB,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,EAC5B,SAAS,EAAE,qBAAS,CAAC,KAAK,EAAE,EAC5B,QAAQ,EAAE,qBAAS,CAAC,IAAI,EAAE,EAC1B,UAAU,EAAE,oBAAoB,EAChC,QAAQ,EAAE,SAAM,CAAC,EAAE,EAAE,IAClB,SAAS,EACb;AACH,CAAC,CAAA;AAdY,QAAA,IAAI,QAchB;AAEM,MAAM,SAAS,GAAG,CAAC,SAAe,EAAa,EAAE;IACtD,uCACK,IAAA,YAAI,EAAC,SAAS,CAAC,KAClB,KAAK,EAAE;YACL,MAAM,EAAE,IAAI;SACb,EACD,OAAO,EAAE;YACP,MAAM,EAAE,IAAI;SACb,IACF;AACH,CAAC,CAAA;AAVY,QAAA,SAAS,aAUrB;AAEM,MAAM,aAAa,GAAG,CAAC,SAAe,EAAiB,EAAE;IAC9D,uCACK,IAAA,YAAI,EAAC,SAAS,CAAC,KAClB,KAAK,EAAE;YACL,MAAM,EAAE,IAAI;SACb,IACF;AACH,CAAC,CAAA;AAPY,QAAA,aAAa,iBAOzB;AAEM,MAAM,WAAW,GAAG,CAAC,SAAyB,EAAe,EAAE;IACpE,uCACK,IAAA,YAAI,EAAC,SAAS,CAAC,KAClB,OAAO,EAAE;YACP,MAAM,EAAE,IAAI;SACb,IACF;AACH,CAAC,CAAA;AAPY,QAAA,WAAW,eAOvB;AAEM,MAAM,cAAc,GAAG,CAAC,KAAa,EAAE,SAAe,EAAe,EAAE;IAC5E,uCACK,IAAA,YAAI,EAAC,SAAS,CAAC,KAClB,OAAO,EAAE;YACP,IAAI,EAAE,CAAC,KAAK,CAAC;SACd,IACF;AACH,CAAC,CAAA;AAPY,QAAA,cAAc,kBAO1B;AAED,SAAgB,OAAO,CACrB,OAAiD,EAAE;;IAEnD,MAAM,IAAI,GAAG,IAAA,YAAI,EAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC5B,OAAO,IAAI,CAAC,QAAQ,CAAA;IAEpB,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QAClB,IAAI,CAAC,OAAO,GAAG,IAAA,iBAAW,EAAC,IAAI,CAAC,CAAA;IAClC,CAAC;IAED,uCACK,IAAI,KACP,kBAAkB,EAAE,KAAK,EACzB,MAAM,EAAE,MAAA,IAAI,CAAC,OAAO,0CAAE,MAAM,EAC5B,QAAQ,EAAE,MAAA,IAAI,CAAC,OAAO,0CAAE,QAAS,EACjC,YAAY,EAAE,MAAA,IAAI,CAAC,OAAO,0CAAE,YAAa,EACzC,iBAAiB,EAAE;YACjB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,OAAO,EAAE,IAAI,CAAC,UAAU;SACzB,IACF;AACH,CAAC"}
@@ -7,11 +7,16 @@ require("./core/logging");
7
7
  const environment_1 = __importDefault(require("../src/environment"));
8
8
  const timers_1 = require("../src/timers");
9
9
  const utilities_1 = require("./core/utilities");
10
- // must explicitly enable fetch mock
11
- utilities_1.mocks.fetch.enable();
10
+ const nock_1 = __importDefault(require("nock"));
12
11
  // mock all dates to 2020-01-01T00:00:00.000Z
13
12
  // use tk.reset() to use real dates in individual tests
14
13
  const timekeeper_1 = __importDefault(require("timekeeper"));
14
+ nock_1.default.disableNetConnect();
15
+ nock_1.default.enableNetConnect(host => {
16
+ return (host.includes("localhost") ||
17
+ host.includes("127.0.0.1") ||
18
+ host.includes("::1"));
19
+ });
15
20
  timekeeper_1.default.freeze(utilities_1.mocks.date.MOCK_DATE);
16
21
  if (!process.env.DEBUG) {
17
22
  console.log = jest.fn(); // console.log are ignored in tests
@@ -1 +1 @@
1
- {"version":3,"file":"jestSetup.js","sourceRoot":"","sources":["../../tests/jestSetup.ts"],"names":[],"mappings":";;;;;AAAA,0BAAuB;AACvB,qEAAoC;AACpC,0CAAuC;AACvC,gDAA4D;AAE5D,oCAAoC;AACpC,iBAAK,CAAC,KAAK,CAAC,MAAM,EAAE,CAAA;AAEpB,6CAA6C;AAC7C,uDAAuD;AACvD,4DAA2B;AAE3B,oBAAE,CAAC,MAAM,CAAC,iBAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;AAE/B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;IACvB,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,EAAE,EAAE,CAAA,CAAC,mCAAmC;AAC7D,CAAC;AAED,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;IACpB,4CAA4C;IAC5C,cAAc;IACd,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;AACzB,CAAC;AAED,8BAAkB,CAAC,QAAQ,CAAC,qBAAG,CAAC,CAAA;AAEhC,QAAQ,CAAC,GAAG,EAAE;IACZ,IAAA,gBAAO,GAAE,CAAA;AACX,CAAC,CAAC,CAAA"}
1
+ {"version":3,"file":"jestSetup.js","sourceRoot":"","sources":["../../tests/jestSetup.ts"],"names":[],"mappings":";;;;;AAAA,0BAAuB;AACvB,qEAAoC;AACpC,0CAAuC;AACvC,gDAA4D;AAC5D,gDAAuB;AAEvB,6CAA6C;AAC7C,uDAAuD;AACvD,4DAA2B;AAE3B,cAAI,CAAC,iBAAiB,EAAE,CAAA;AACxB,cAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE;IAC3B,OAAO,CACL,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC;QAC1B,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC;QAC1B,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CACrB,CAAA;AACH,CAAC,CAAC,CAAA;AAEF,oBAAE,CAAC,MAAM,CAAC,iBAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;AAE/B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;IACvB,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,EAAE,EAAE,CAAA,CAAC,mCAAmC;AAC7D,CAAC;AAED,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;IACpB,4CAA4C;IAC5C,cAAc;IACd,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;AACzB,CAAC;AAED,8BAAkB,CAAC,QAAQ,CAAC,qBAAG,CAAC,CAAA;AAEhC,QAAQ,CAAC,GAAG,EAAE;IACZ,IAAA,gBAAO,GAAE,CAAA;AACX,CAAC,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@budibase/backend-core",
3
- "version": "2.30.2",
3
+ "version": "2.30.3",
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.2",
27
- "@budibase/types": "2.30.2",
26
+ "@budibase/shared-core": "2.30.3",
27
+ "@budibase/types": "2.30.3",
28
28
  "aws-cloudfront-sign": "3.0.2",
29
29
  "aws-sdk": "2.1030.0",
30
30
  "bcrypt": "5.1.0",
@@ -46,7 +46,7 @@
46
46
  "passport-oauth2-refresh": "^2.1.0",
47
47
  "pino": "8.11.0",
48
48
  "pino-http": "8.3.3",
49
- "posthog-node": "1.3.0",
49
+ "posthog-node": "4.0.1",
50
50
  "pouchdb": "7.3.0",
51
51
  "pouchdb-find": "7.2.2",
52
52
  "redlock": "4.2.0",
@@ -95,5 +95,5 @@
95
95
  }
96
96
  }
97
97
  },
98
- "gitHead": "c5877bb1bf52f056cd63b3792054e986f781a180"
98
+ "gitHead": "bb6092e58b80d4343bf3b720881f2dc01a30d1c9"
99
99
  }
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from "fs"
2
2
  import { ServiceType } from "@budibase/types"
3
+ import { cloneDeep } from "lodash"
3
4
 
4
5
  function isTest() {
5
6
  return isJest()
@@ -144,6 +145,8 @@ const environment = {
144
145
  COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
145
146
  PLATFORM_URL: process.env.PLATFORM_URL || "",
146
147
  POSTHOG_TOKEN: process.env.POSTHOG_TOKEN,
148
+ POSTHOG_PERSONAL_TOKEN: process.env.POSTHOG_PERSONAL_TOKEN,
149
+ POSTHOG_API_HOST: process.env.POSTHOG_API_HOST || "https://us.i.posthog.com",
147
150
  ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS,
148
151
  TENANT_FEATURE_FLAGS: process.env.TENANT_FEATURE_FLAGS,
149
152
  CLOUDFRONT_CDN: process.env.CLOUDFRONT_CDN,
@@ -208,6 +211,32 @@ const environment = {
208
211
  OPENAI_API_KEY: process.env.OPENAI_API_KEY,
209
212
  }
210
213
 
214
+ export function setEnv(newEnvVars: Partial<typeof environment>): () => void {
215
+ const oldEnv = cloneDeep(environment)
216
+
217
+ let key: keyof typeof newEnvVars
218
+ for (key in newEnvVars) {
219
+ environment._set(key, newEnvVars[key])
220
+ }
221
+
222
+ return () => {
223
+ for (const [key, value] of Object.entries(oldEnv)) {
224
+ environment._set(key, value)
225
+ }
226
+ }
227
+ }
228
+
229
+ export function withEnv<T>(envVars: Partial<typeof environment>, f: () => T) {
230
+ const cleanup = setEnv(envVars)
231
+ const result = f()
232
+ if (result instanceof Promise) {
233
+ return result.finally(cleanup)
234
+ } else {
235
+ cleanup()
236
+ return result
237
+ }
238
+ }
239
+
211
240
  type EnvironmentKey = keyof typeof environment
212
241
  export const SECRETS: EnvironmentKey[] = [
213
242
  "API_ENCRYPTION_KEY",
@@ -1,4 +1,4 @@
1
- import PostHog from "posthog-node"
1
+ import { PostHog } from "posthog-node"
2
2
  import { Event, Identity, Group, BaseEvent } from "@budibase/types"
3
3
  import { EventProcessor } from "../types"
4
4
  import env from "../../../environment"
@@ -1,9 +1,7 @@
1
1
  import { testEnv } from "../../../../../tests/extra"
2
2
  import PosthogProcessor from "../PosthogProcessor"
3
3
  import { Event, IdentityType, Hosting } from "@budibase/types"
4
-
5
- const tk = require("timekeeper")
6
-
4
+ import tk from "timekeeper"
7
5
  import * as cache from "../../../../cache/generic"
8
6
  import { CacheKey } from "../../../../cache/generic"
9
7
  import * as context from "../../../../context"
@@ -18,6 +16,9 @@ const newIdentity = () => {
18
16
  }
19
17
 
20
18
  describe("PosthogProcessor", () => {
19
+ let processor: PosthogProcessor
20
+ let spy: jest.SpyInstance
21
+
21
22
  beforeAll(() => {
22
23
  testEnv.singleTenant()
23
24
  })
@@ -27,33 +28,29 @@ describe("PosthogProcessor", () => {
27
28
  await cache.bustCache(
28
29
  `${CacheKey.EVENTS_RATE_LIMIT}:${Event.SERVED_BUILDER}`
29
30
  )
31
+
32
+ processor = new PosthogProcessor("test")
33
+ spy = jest.spyOn(processor.posthog, "capture")
30
34
  })
31
35
 
32
36
  describe("processEvent", () => {
33
37
  it("processes event", async () => {
34
- const processor = new PosthogProcessor("test")
35
-
36
38
  const identity = newIdentity()
37
39
  const properties = {}
38
40
 
39
41
  await processor.processEvent(Event.APP_CREATED, identity, properties)
40
-
41
- expect(processor.posthog.capture).toHaveBeenCalledTimes(1)
42
+ expect(spy).toHaveBeenCalledTimes(1)
42
43
  })
43
44
 
44
45
  it("honours exclusions", async () => {
45
- const processor = new PosthogProcessor("test")
46
-
47
46
  const identity = newIdentity()
48
47
  const properties = {}
49
48
 
50
49
  await processor.processEvent(Event.AUTH_SSO_UPDATED, identity, properties)
51
- expect(processor.posthog.capture).toHaveBeenCalledTimes(0)
50
+ expect(spy).toHaveBeenCalledTimes(0)
52
51
  })
53
52
 
54
53
  it("removes audited information", async () => {
55
- const processor = new PosthogProcessor("test")
56
-
57
54
  const identity = newIdentity()
58
55
  const properties = {
59
56
  email: "test",
@@ -63,7 +60,8 @@ describe("PosthogProcessor", () => {
63
60
  }
64
61
 
65
62
  await processor.processEvent(Event.USER_CREATED, identity, properties)
66
- expect(processor.posthog.capture).toHaveBeenCalled()
63
+ expect(spy).toHaveBeenCalled()
64
+
67
65
  // @ts-ignore
68
66
  const call = processor.posthog.capture.mock.calls[0][0]
69
67
  expect(call.properties.audited).toBeUndefined()
@@ -72,7 +70,6 @@ describe("PosthogProcessor", () => {
72
70
 
73
71
  describe("rate limiting", () => {
74
72
  it("sends daily event once in same day", async () => {
75
- const processor = new PosthogProcessor("test")
76
73
  const identity = newIdentity()
77
74
  const properties = {}
78
75
 
@@ -82,11 +79,10 @@ describe("PosthogProcessor", () => {
82
79
  tk.freeze(new Date(2022, 0, 1, 15, 0))
83
80
  await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
84
81
 
85
- expect(processor.posthog.capture).toHaveBeenCalledTimes(1)
82
+ expect(spy).toHaveBeenCalledTimes(1)
86
83
  })
87
84
 
88
85
  it("sends daily event once per unique day", async () => {
89
- const processor = new PosthogProcessor("test")
90
86
  const identity = newIdentity()
91
87
  const properties = {}
92
88
 
@@ -102,11 +98,10 @@ describe("PosthogProcessor", () => {
102
98
  tk.freeze(new Date(2022, 0, 3, 6, 0))
103
99
  await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
104
100
 
105
- expect(processor.posthog.capture).toHaveBeenCalledTimes(3)
101
+ expect(spy).toHaveBeenCalledTimes(3)
106
102
  })
107
103
 
108
104
  it("sends event again after cache expires", async () => {
109
- const processor = new PosthogProcessor("test")
110
105
  const identity = newIdentity()
111
106
  const properties = {}
112
107
 
@@ -120,11 +115,10 @@ describe("PosthogProcessor", () => {
120
115
  tk.freeze(new Date(2022, 0, 1, 14, 0))
121
116
  await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
122
117
 
123
- expect(processor.posthog.capture).toHaveBeenCalledTimes(2)
118
+ expect(spy).toHaveBeenCalledTimes(2)
124
119
  })
125
120
 
126
121
  it("sends per app events once per day per app", async () => {
127
- const processor = new PosthogProcessor("test")
128
122
  const identity = newIdentity()
129
123
  const properties = {}
130
124
 
@@ -160,10 +154,10 @@ describe("PosthogProcessor", () => {
160
154
  }
161
155
 
162
156
  await runAppEvents("app_1")
163
- expect(processor.posthog.capture).toHaveBeenCalledTimes(4)
157
+ expect(spy).toHaveBeenCalledTimes(4)
164
158
 
165
159
  await runAppEvents("app_2")
166
- expect(processor.posthog.capture).toHaveBeenCalledTimes(8)
160
+ expect(spy).toHaveBeenCalledTimes(8)
167
161
  })
168
162
  })
169
163
  })
@@ -1,108 +1,258 @@
1
1
  import env from "../environment"
2
2
  import * as context from "../context"
3
- import { cloneDeep } from "lodash"
3
+ import { PostHog, PostHogOptions } from "posthog-node"
4
+ import { IdentityType, UserCtx } from "@budibase/types"
5
+ import tracer from "dd-trace"
4
6
 
5
- class Flag<T> {
6
- static withDefault<T>(value: T) {
7
- return new Flag(value)
7
+ let posthog: PostHog | undefined
8
+ export function init(opts?: PostHogOptions) {
9
+ if (env.POSTHOG_TOKEN && env.POSTHOG_API_HOST) {
10
+ console.log("initializing posthog client...")
11
+ posthog = new PostHog(env.POSTHOG_TOKEN, {
12
+ host: env.POSTHOG_API_HOST,
13
+ personalApiKey: env.POSTHOG_PERSONAL_TOKEN,
14
+ ...opts,
15
+ })
16
+ } else {
17
+ console.log("posthog disabled")
8
18
  }
9
-
10
- private constructor(public defaultValue: T) {}
11
19
  }
12
20
 
13
- // This is the primary source of truth for feature flags. If you want to add a
14
- // new flag, add it here and use the `fetch` and `get` functions to access it.
15
- // All of the machinery in this file is to make sure that flags have their
16
- // default values set correctly and their types flow through the system.
17
- const FLAGS = {
18
- LICENSING: Flag.withDefault(false),
19
- GOOGLE_SHEETS: Flag.withDefault(false),
20
- USER_GROUPS: Flag.withDefault(false),
21
- ONBOARDING_TOUR: Flag.withDefault(false),
22
- }
21
+ export abstract class Flag<T> {
22
+ static boolean(defaultValue: boolean): Flag<boolean> {
23
+ return new BooleanFlag(defaultValue)
24
+ }
25
+
26
+ static string(defaultValue: string): Flag<string> {
27
+ return new StringFlag(defaultValue)
28
+ }
29
+
30
+ static number(defaultValue: number): Flag<number> {
31
+ return new NumberFlag(defaultValue)
32
+ }
33
+
34
+ protected constructor(public defaultValue: T) {}
23
35
 
24
- const DEFAULTS = Object.keys(FLAGS).reduce((acc, key) => {
25
- const typedKey = key as keyof typeof FLAGS
26
- // @ts-ignore
27
- acc[typedKey] = FLAGS[typedKey].defaultValue
28
- return acc
29
- }, {} as Flags)
36
+ abstract parse(value: any): T
37
+ }
30
38
 
31
39
  type UnwrapFlag<F> = F extends Flag<infer U> ? U : never
32
- export type Flags = {
33
- [K in keyof typeof FLAGS]: UnwrapFlag<(typeof FLAGS)[K]>
40
+
41
+ export type FlagValues<T> = {
42
+ [K in keyof T]: UnwrapFlag<T[K]>
34
43
  }
35
44
 
36
- // Exported for use in tests, should not be used outside of this file.
37
- export function defaultFlags(): Flags {
38
- return cloneDeep(DEFAULTS)
45
+ type KeysOfType<T, U> = {
46
+ [K in keyof T]: T[K] extends Flag<U> ? K : never
47
+ }[keyof T]
48
+
49
+ class BooleanFlag extends Flag<boolean> {
50
+ parse(value: any) {
51
+ if (typeof value === "string") {
52
+ return ["true", "t", "1"].includes(value.toLowerCase())
53
+ }
54
+
55
+ if (typeof value === "boolean") {
56
+ return value
57
+ }
58
+
59
+ throw new Error(`could not parse value "${value}" as boolean`)
60
+ }
39
61
  }
40
62
 
41
- function isFlagName(name: string): name is keyof Flags {
42
- return FLAGS[name as keyof typeof FLAGS] !== undefined
63
+ class StringFlag extends Flag<string> {
64
+ parse(value: any) {
65
+ if (typeof value === "string") {
66
+ return value
67
+ }
68
+ throw new Error(`could not parse value "${value}" as string`)
69
+ }
43
70
  }
44
71
 
45
- /**
46
- * Reads the TENANT_FEATURE_FLAGS environment variable and returns a Flags object
47
- * populated with the flags for the current tenant, filling in the default values
48
- * if the flag is not set.
49
- *
50
- * Check the tests for examples of how TENANT_FEATURE_FLAGS should be formatted.
51
- *
52
- * In future we plan to add more ways of setting feature flags, e.g. PostHog, and
53
- * they will be accessed through this function as well.
54
- */
55
- export async function fetch(): Promise<Flags> {
56
- const currentTenantId = context.getTenantId()
57
- const flags = defaultFlags()
58
-
59
- const split = (env.TENANT_FEATURE_FLAGS || "")
60
- .split(",")
61
- .map(x => x.split(":"))
62
- for (const [tenantId, ...features] of split) {
63
- if (!tenantId || (tenantId !== "*" && tenantId !== currentTenantId)) {
64
- continue
72
+ class NumberFlag extends Flag<number> {
73
+ parse(value: any) {
74
+ if (typeof value === "number") {
75
+ return value
65
76
  }
66
77
 
67
- for (let feature of features) {
68
- let value = true
69
- if (feature.startsWith("!")) {
70
- feature = feature.slice(1)
71
- value = false
78
+ if (typeof value === "string") {
79
+ const parsed = parseFloat(value)
80
+ if (!isNaN(parsed)) {
81
+ return parsed
72
82
  }
83
+ }
84
+
85
+ throw new Error(`could not parse value "${value}" as number`)
86
+ }
87
+ }
88
+
89
+ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
90
+ constructor(private readonly flagSchema: T) {}
91
+
92
+ defaults(): FlagValues<T> {
93
+ return Object.keys(this.flagSchema).reduce((acc, key) => {
94
+ const typedKey = key as keyof T
95
+ acc[typedKey] = this.flagSchema[key].defaultValue
96
+ return acc
97
+ }, {} as FlagValues<T>)
98
+ }
99
+
100
+ isFlagName(name: string | number | symbol): name is keyof T {
101
+ return this.flagSchema[name as keyof T] !== undefined
102
+ }
73
103
 
74
- if (!isFlagName(feature)) {
75
- throw new Error(`Feature: ${feature} is not an allowed option`)
104
+ async get<K extends keyof T>(
105
+ key: K,
106
+ ctx?: UserCtx
107
+ ): Promise<FlagValues<T>[K]> {
108
+ const flags = await this.fetch(ctx)
109
+ return flags[key]
110
+ }
111
+
112
+ async isEnabled<K extends KeysOfType<T, boolean>>(
113
+ key: K,
114
+ ctx?: UserCtx
115
+ ): Promise<boolean> {
116
+ const flags = await this.fetch(ctx)
117
+ return flags[key]
118
+ }
119
+
120
+ async fetch(ctx?: UserCtx): Promise<FlagValues<T>> {
121
+ return await tracer.trace("features.fetch", async span => {
122
+ const tags: Record<string, any> = {}
123
+ const flagValues = this.defaults()
124
+ const currentTenantId = context.getTenantId()
125
+ const specificallySetFalse = new Set<string>()
126
+
127
+ const split = (env.TENANT_FEATURE_FLAGS || "")
128
+ .split(",")
129
+ .map(x => x.split(":"))
130
+ for (const [tenantId, ...features] of split) {
131
+ if (!tenantId || (tenantId !== "*" && tenantId !== currentTenantId)) {
132
+ continue
133
+ }
134
+
135
+ tags[`readFromEnvironmentVars`] = true
136
+
137
+ for (let feature of features) {
138
+ let value = true
139
+ if (feature.startsWith("!")) {
140
+ feature = feature.slice(1)
141
+ value = false
142
+ specificallySetFalse.add(feature)
143
+ }
144
+
145
+ if (!this.isFlagName(feature)) {
146
+ throw new Error(`Feature: ${feature} is not an allowed option`)
147
+ }
148
+
149
+ if (typeof flagValues[feature] !== "boolean") {
150
+ throw new Error(`Feature: ${feature} is not a boolean`)
151
+ }
152
+
153
+ // @ts-expect-error - TS does not like you writing into a generic type,
154
+ // but we know that it's okay in this case because it's just an object.
155
+ flagValues[feature] = value
156
+ tags[`flags.${feature}.source`] = "environment"
157
+ }
76
158
  }
77
159
 
78
- if (typeof flags[feature] !== "boolean") {
79
- throw new Error(`Feature: ${feature} is not a boolean`)
160
+ const license = ctx?.user?.license
161
+ if (license) {
162
+ tags[`readFromLicense`] = true
163
+
164
+ for (const feature of license.features) {
165
+ if (!this.isFlagName(feature)) {
166
+ continue
167
+ }
168
+
169
+ if (
170
+ flagValues[feature] === true ||
171
+ specificallySetFalse.has(feature)
172
+ ) {
173
+ // If the flag is already set to through environment variables, we
174
+ // don't want to override it back to false here.
175
+ continue
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] = true
181
+ tags[`flags.${feature}.source`] = "license"
182
+ }
80
183
  }
81
184
 
82
- // @ts-ignore
83
- flags[feature] = value
84
- }
85
- }
185
+ const identity = context.getIdentity()
186
+ tags[`identity.type`] = identity?.type
187
+ tags[`identity.tenantId`] = identity?.tenantId
188
+ tags[`identity._id`] = identity?._id
86
189
 
87
- return flags
88
- }
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) {
194
+ tags[`readFromPostHog`] = true
89
195
 
90
- // Gets a single feature flag value. This is a convenience function for
91
- // `fetch().then(flags => flags[name])`.
92
- export async function get<K extends keyof Flags>(name: K): Promise<Flags[K]> {
93
- const flags = await fetch()
94
- return flags[name]
95
- }
196
+ const personProperties: Record<string, string> = {}
197
+ if (identity.tenantId) {
198
+ personProperties.tenantId = identity.tenantId
199
+ }
200
+
201
+ const posthogFlags = await posthog.getAllFlagsAndPayloads(
202
+ identity._id,
203
+ {
204
+ personProperties,
205
+ }
206
+ )
207
+ console.log("posthog flags", JSON.stringify(posthogFlags))
208
+
209
+ for (const [name, value] of Object.entries(posthogFlags.featureFlags)) {
210
+ if (!this.isFlagName(name)) {
211
+ // We don't want an unexpected PostHog flag to break the app, so we
212
+ // just log it and continue.
213
+ console.warn(`Unexpected posthog flag "${name}": ${value}`)
214
+ continue
215
+ }
96
216
 
97
- type BooleanFlags = {
98
- [K in keyof typeof FLAGS]: (typeof FLAGS)[K] extends Flag<boolean> ? K : never
99
- }[keyof typeof FLAGS]
100
-
101
- // Convenience function for boolean flag values. This makes callsites more
102
- // readable for boolean flags.
103
- export async function isEnabled<K extends BooleanFlags>(
104
- name: K
105
- ): Promise<boolean> {
106
- const flags = await fetch()
107
- return flags[name]
217
+ if (flagValues[name] === true || specificallySetFalse.has(name)) {
218
+ // If the flag is already set to through environment variables, we
219
+ // don't want to override it back to false here.
220
+ continue
221
+ }
222
+
223
+ const payload = posthogFlags.featureFlagPayloads?.[name]
224
+ const flag = this.flagSchema[name]
225
+ try {
226
+ // @ts-expect-error - TS does not like you writing into a generic
227
+ // type, but we know that it's okay in this case because it's just
228
+ // an object.
229
+ flagValues[name] = flag.parse(payload || value)
230
+ tags[`flags.${name}.source`] = "posthog"
231
+ } catch (err) {
232
+ // We don't want an invalid PostHog flag to break the app, so we just
233
+ // log it and continue.
234
+ console.warn(`Error parsing posthog flag "${name}": ${value}`, err)
235
+ }
236
+ }
237
+ }
238
+
239
+ for (const [key, value] of Object.entries(flagValues)) {
240
+ tags[`flags.${key}.value`] = value
241
+ }
242
+ span?.addTags(tags)
243
+
244
+ return flagValues
245
+ })
246
+ }
108
247
  }
248
+
249
+ // This is the primary source of truth for feature flags. If you want to add a
250
+ // new flag, add it here and use the `fetch` and `get` functions to access it.
251
+ // All of the machinery in this file is to make sure that flags have their
252
+ // default values set correctly and their types flow through the system.
253
+ 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),
258
+ })