@budibase/worker 3.27.5 → 3.28.0
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 +2 -2
- package/src/api/controllers/global/github.ts +77 -0
- package/src/api/controllers/global/tests/github.spec.ts +193 -0
- package/src/api/controllers/global/users.ts +5 -3
- package/src/api/routes/global/github.ts +4 -0
- package/src/api/routes/global/users.ts +1 -1
- package/src/api/routes/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/worker",
|
|
3
3
|
"email": "hi@budibase.com",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.28.0",
|
|
5
5
|
"description": "Budibase background service",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"repository": {
|
|
@@ -107,5 +107,5 @@
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
},
|
|
110
|
-
"gitHead": "
|
|
110
|
+
"gitHead": "b57235144ae6ed94b1b54d9ea73335106feedc02"
|
|
111
111
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fetch from "node-fetch"
|
|
2
|
+
import { type GetGitHubStarsResponse, type UserCtx } from "@budibase/types"
|
|
3
|
+
import { cache } from "@budibase/backend-core"
|
|
4
|
+
|
|
5
|
+
const CACHE_TTL_MS = 6 * 60 * 60 * 1000
|
|
6
|
+
const FAILURE_TTL_MS = 5 * 60 * 1000
|
|
7
|
+
const GITHUB_TIMEOUT_MS = 5000
|
|
8
|
+
const GITHUB_REPO_URL = "https://api.github.com/repos/budibase/budibase"
|
|
9
|
+
const USER_AGENT = "Budibase"
|
|
10
|
+
|
|
11
|
+
const CACHE_KEY = "global:github:stars"
|
|
12
|
+
const RETENTION_TTL_SECONDS = 30 * 24 * 60 * 60
|
|
13
|
+
|
|
14
|
+
interface StarsCacheEnvelope {
|
|
15
|
+
value: GetGitHubStarsResponse
|
|
16
|
+
expiresAt: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function getStars(ctx: UserCtx<void, GetGitHubStarsResponse>) {
|
|
20
|
+
const envelope = (await cache.get(CACHE_KEY, {
|
|
21
|
+
useTenancy: false,
|
|
22
|
+
})) as StarsCacheEnvelope | null
|
|
23
|
+
if (envelope && envelope.expiresAt > Date.now()) {
|
|
24
|
+
ctx.body = envelope.value
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(GITHUB_REPO_URL, {
|
|
30
|
+
headers: {
|
|
31
|
+
Accept: "application/vnd.github+json",
|
|
32
|
+
"User-Agent": USER_AGENT,
|
|
33
|
+
},
|
|
34
|
+
timeout: GITHUB_TIMEOUT_MS,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error(`GitHub response: ${response.status}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const json = (await response.json()) as { stargazers_count?: number }
|
|
42
|
+
const stars = json.stargazers_count
|
|
43
|
+
|
|
44
|
+
if (typeof stars !== "number") {
|
|
45
|
+
throw new Error("GitHub stars missing")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const value: GetGitHubStarsResponse = {
|
|
49
|
+
stars,
|
|
50
|
+
fetchedAt: new Date().toISOString(),
|
|
51
|
+
}
|
|
52
|
+
const toStore: StarsCacheEnvelope = {
|
|
53
|
+
value,
|
|
54
|
+
expiresAt: Date.now() + CACHE_TTL_MS,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await cache.store(CACHE_KEY, toStore, RETENTION_TTL_SECONDS, {
|
|
58
|
+
useTenancy: false,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
ctx.body = value
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error("Failed to fetch GitHub stars", err)
|
|
64
|
+
|
|
65
|
+
const value = envelope?.value || { stars: null, fetchedAt: null }
|
|
66
|
+
const toStore: StarsCacheEnvelope = {
|
|
67
|
+
value,
|
|
68
|
+
expiresAt: Date.now() + FAILURE_TTL_MS,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await cache.store(CACHE_KEY, toStore, RETENTION_TTL_SECONDS, {
|
|
72
|
+
useTenancy: false,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
ctx.body = value
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { type GetGitHubStarsResponse } from "@budibase/types"
|
|
2
|
+
|
|
3
|
+
jest.mock("node-fetch", () => jest.fn())
|
|
4
|
+
|
|
5
|
+
interface CacheOpts {
|
|
6
|
+
useTenancy?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface TestCtx {
|
|
10
|
+
body: GetGitHubStarsResponse | undefined
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("GitHub controller", () => {
|
|
14
|
+
let cacheGet: jest.Mock
|
|
15
|
+
let cacheStore: jest.Mock
|
|
16
|
+
let cacheState: Map<string, unknown>
|
|
17
|
+
let consoleErrorSpy: jest.SpyInstance
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
jest.resetModules()
|
|
21
|
+
jest.clearAllMocks()
|
|
22
|
+
|
|
23
|
+
jest.useFakeTimers()
|
|
24
|
+
jest.setSystemTime(new Date("2020-01-01T00:00:00.000Z"))
|
|
25
|
+
|
|
26
|
+
consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
|
|
27
|
+
|
|
28
|
+
cacheState = new Map()
|
|
29
|
+
jest.doMock("@budibase/backend-core", () => {
|
|
30
|
+
cacheGet = jest.fn(async (key: string, opts?: CacheOpts) => {
|
|
31
|
+
if (opts?.useTenancy !== false) {
|
|
32
|
+
throw new Error("Expected global cache usage")
|
|
33
|
+
}
|
|
34
|
+
return cacheState.get(key) ?? null
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
cacheStore = jest.fn(
|
|
38
|
+
async (
|
|
39
|
+
key: string,
|
|
40
|
+
value: unknown,
|
|
41
|
+
_ttl?: number,
|
|
42
|
+
opts?: CacheOpts
|
|
43
|
+
) => {
|
|
44
|
+
if (opts?.useTenancy !== false) {
|
|
45
|
+
throw new Error("Expected global cache usage")
|
|
46
|
+
}
|
|
47
|
+
cacheState.set(key, value)
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
cache: {
|
|
53
|
+
get: cacheGet,
|
|
54
|
+
store: cacheStore,
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
consoleErrorSpy.mockRestore()
|
|
62
|
+
jest.useRealTimers()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const getFetchMock = () => {
|
|
66
|
+
return jest.requireMock("node-fetch") as unknown as jest.Mock
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const makeCtx = () => {
|
|
70
|
+
const ctx: TestCtx = { body: undefined }
|
|
71
|
+
return ctx
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function assertHasBody(
|
|
75
|
+
ctx: TestCtx
|
|
76
|
+
): asserts ctx is { body: GetGitHubStarsResponse } {
|
|
77
|
+
if (!ctx.body) {
|
|
78
|
+
throw new Error("Expected ctx.body to be set")
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
it("adds a request timeout", async () => {
|
|
83
|
+
const fetchMock = getFetchMock()
|
|
84
|
+
fetchMock.mockResolvedValue({
|
|
85
|
+
ok: true,
|
|
86
|
+
status: 200,
|
|
87
|
+
json: async () => ({ stargazers_count: 123 }),
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const { getStars } = require("../github")
|
|
91
|
+
const ctx = makeCtx()
|
|
92
|
+
await getStars(ctx)
|
|
93
|
+
|
|
94
|
+
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
95
|
+
expect(fetchMock.mock.calls[0][1]).toEqual(
|
|
96
|
+
expect.objectContaining({ timeout: 5000 })
|
|
97
|
+
)
|
|
98
|
+
assertHasBody(ctx)
|
|
99
|
+
expect(ctx.body.stars).toEqual(123)
|
|
100
|
+
|
|
101
|
+
expect(cacheGet).toHaveBeenCalledWith("global:github:stars", {
|
|
102
|
+
useTenancy: false,
|
|
103
|
+
})
|
|
104
|
+
expect(cacheStore).toHaveBeenCalledWith(
|
|
105
|
+
"global:github:stars",
|
|
106
|
+
expect.any(Object),
|
|
107
|
+
expect.any(Number),
|
|
108
|
+
{ useTenancy: false }
|
|
109
|
+
)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("caches successful responses", async () => {
|
|
113
|
+
const fetchMock = getFetchMock()
|
|
114
|
+
fetchMock.mockResolvedValue({
|
|
115
|
+
ok: true,
|
|
116
|
+
status: 200,
|
|
117
|
+
json: async () => ({ stargazers_count: 999 }),
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const { getStars } = require("../github")
|
|
121
|
+
const ctx1 = makeCtx()
|
|
122
|
+
await getStars(ctx1)
|
|
123
|
+
const ctx2 = makeCtx()
|
|
124
|
+
await getStars(ctx2)
|
|
125
|
+
|
|
126
|
+
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
127
|
+
expect(ctx2.body).toEqual(ctx1.body)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it("serves cached value and sets a short failure TTL on error", async () => {
|
|
131
|
+
const fetchMock = getFetchMock()
|
|
132
|
+
fetchMock.mockRejectedValue(new Error("timeout"))
|
|
133
|
+
|
|
134
|
+
const { getStars } = require("../github")
|
|
135
|
+
const ctx1 = makeCtx()
|
|
136
|
+
await getStars(ctx1)
|
|
137
|
+
const ctx2 = makeCtx()
|
|
138
|
+
await getStars(ctx2)
|
|
139
|
+
|
|
140
|
+
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
141
|
+
expect(ctx1.body).toEqual({ stars: null, fetchedAt: null })
|
|
142
|
+
expect(ctx2.body).toEqual(ctx1.body)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it("returns last known value on refresh failure", async () => {
|
|
146
|
+
const fetchMock = getFetchMock()
|
|
147
|
+
fetchMock.mockResolvedValue({
|
|
148
|
+
ok: true,
|
|
149
|
+
status: 200,
|
|
150
|
+
json: async () => ({ stargazers_count: 111 }),
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const { getStars } = require("../github")
|
|
154
|
+
const ctx1 = makeCtx()
|
|
155
|
+
await getStars(ctx1)
|
|
156
|
+
|
|
157
|
+
jest.setSystemTime(new Date(Date.now() + 6 * 60 * 60 * 1000 + 1))
|
|
158
|
+
fetchMock.mockRejectedValue(new Error("github down"))
|
|
159
|
+
|
|
160
|
+
const ctx2 = makeCtx()
|
|
161
|
+
await getStars(ctx2)
|
|
162
|
+
|
|
163
|
+
expect(fetchMock).toHaveBeenCalledTimes(2)
|
|
164
|
+
expect(ctx2.body).toEqual(ctx1.body)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it("retries after failure backoff expires", async () => {
|
|
168
|
+
const fetchMock = getFetchMock()
|
|
169
|
+
fetchMock.mockRejectedValue(new Error("timeout"))
|
|
170
|
+
|
|
171
|
+
const { getStars } = require("../github")
|
|
172
|
+
const ctx1 = makeCtx()
|
|
173
|
+
await getStars(ctx1)
|
|
174
|
+
const ctx2 = makeCtx()
|
|
175
|
+
await getStars(ctx2)
|
|
176
|
+
|
|
177
|
+
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
178
|
+
|
|
179
|
+
jest.setSystemTime(new Date(Date.now() + 5 * 60 * 1000 + 1))
|
|
180
|
+
fetchMock.mockResolvedValue({
|
|
181
|
+
ok: true,
|
|
182
|
+
status: 200,
|
|
183
|
+
json: async () => ({ stargazers_count: 222 }),
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const ctx3 = makeCtx()
|
|
187
|
+
await getStars(ctx3)
|
|
188
|
+
|
|
189
|
+
expect(fetchMock).toHaveBeenCalledTimes(2)
|
|
190
|
+
assertHasBody(ctx3)
|
|
191
|
+
expect(ctx3.body.stars).toEqual(222)
|
|
192
|
+
})
|
|
193
|
+
})
|
|
@@ -268,10 +268,12 @@ export const adminUser = async (
|
|
|
268
268
|
})
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
export const
|
|
272
|
-
|
|
271
|
+
export const countByWorkspace = async (
|
|
272
|
+
ctx: UserCtx<void, CountUserResponse>
|
|
273
|
+
) => {
|
|
274
|
+
const workspaceId = ctx.params.workspaceId
|
|
273
275
|
try {
|
|
274
|
-
ctx.body = await userSdk.db.
|
|
276
|
+
ctx.body = await userSdk.db.countUsersByWorkspace(workspaceId)
|
|
275
277
|
} catch (err: any) {
|
|
276
278
|
ctx.throw(err.status || 400, err)
|
|
277
279
|
}
|
|
@@ -92,7 +92,7 @@ adminRoutes
|
|
|
92
92
|
|
|
93
93
|
builderOrAdminRoutes
|
|
94
94
|
.get("/api/global/users", controller.fetch)
|
|
95
|
-
.get("/api/global/users/count/:
|
|
95
|
+
.get("/api/global/users/count/:workspaceId", controller.countByWorkspace)
|
|
96
96
|
.get("/api/global/users/invites", controller.getUserInvites)
|
|
97
97
|
.get("/api/global/users/:id", controller.find)
|
|
98
98
|
.post(
|