@budibase/backend-core 2.32.11 → 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.
- package/dist/index.js +504 -172
- package/dist/index.js.map +4 -4
- package/dist/index.js.meta.json +1 -1
- package/dist/package.json +4 -4
- package/dist/plugins.js.meta.json +1 -1
- package/dist/src/db/couch/DatabaseImpl.js +10 -2
- package/dist/src/db/couch/DatabaseImpl.js.map +1 -1
- package/dist/src/features/features.d.ts +47 -0
- package/dist/src/features/features.js +269 -0
- package/dist/src/features/features.js.map +1 -0
- package/dist/src/features/index.d.ts +2 -39
- package/dist/src/features/index.js +6 -235
- package/dist/src/features/index.js.map +1 -1
- package/dist/src/features/tests/utils.d.ts +3 -0
- package/dist/src/features/tests/utils.js +56 -0
- package/dist/src/features/tests/utils.js.map +1 -0
- package/dist/src/middleware/passport/sso/sso.js +0 -25
- package/dist/src/middleware/passport/sso/sso.js.map +1 -1
- package/dist/src/sql/sql.js +93 -35
- package/dist/src/sql/sql.js.map +1 -1
- package/dist/src/users/users.js +8 -1
- package/dist/src/users/users.js.map +1 -1
- package/dist/tests/core/utilities/structures/accounts.d.ts +1 -6
- package/dist/tests/core/utilities/structures/accounts.js +4 -58
- package/dist/tests/core/utilities/structures/accounts.js.map +1 -1
- package/dist/tests/core/utilities/structures/users.js +2 -5
- package/dist/tests/core/utilities/structures/users.js.map +1 -1
- package/package.json +4 -4
- package/src/db/couch/DatabaseImpl.ts +12 -2
- package/src/features/features.ts +300 -0
- package/src/features/index.ts +2 -281
- package/src/features/tests/utils.ts +64 -0
- package/src/middleware/passport/sso/sso.ts +0 -24
- package/src/sql/sql.ts +107 -36
- package/src/users/users.ts +10 -2
- package/tests/core/utilities/structures/accounts.ts +1 -66
- package/tests/core/utilities/structures/users.ts +0 -5
package/src/features/index.ts
CHANGED
|
@@ -1,281 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
}
|
package/src/sql/sql.ts
CHANGED
|
@@ -139,29 +139,61 @@ class InternalBuilder {
|
|
|
139
139
|
return this.table.schema[column]
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
// and "foo" for Postgres.
|
|
144
|
-
private quote(str: string): string {
|
|
142
|
+
private quoteChars(): [string, string] {
|
|
145
143
|
switch (this.client) {
|
|
146
|
-
case SqlClient.SQL_LITE:
|
|
147
144
|
case SqlClient.ORACLE:
|
|
148
145
|
case SqlClient.POSTGRES:
|
|
149
|
-
return
|
|
146
|
+
return ['"', '"']
|
|
150
147
|
case SqlClient.MS_SQL:
|
|
151
|
-
return
|
|
148
|
+
return ["[", "]"]
|
|
152
149
|
case SqlClient.MARIADB:
|
|
153
150
|
case SqlClient.MY_SQL:
|
|
154
|
-
|
|
151
|
+
case SqlClient.SQL_LITE:
|
|
152
|
+
return ["`", "`"]
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Takes a string like foo and returns a quoted string like [foo] for SQL Server
|
|
157
|
+
// and "foo" for Postgres.
|
|
158
|
+
private quote(str: string): string {
|
|
159
|
+
const [start, end] = this.quoteChars()
|
|
160
|
+
return `${start}${str}${end}`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private isQuoted(key: string): boolean {
|
|
164
|
+
const [start, end] = this.quoteChars()
|
|
165
|
+
return key.startsWith(start) && key.endsWith(end)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Takes a string like a.b.c or an array like ["a", "b", "c"] and returns a
|
|
169
|
+
// quoted identifier like [a].[b].[c] for SQL Server and `a`.`b`.`c` for
|
|
170
|
+
// MySQL.
|
|
171
|
+
private quotedIdentifier(key: string | string[]): string {
|
|
172
|
+
if (!Array.isArray(key)) {
|
|
173
|
+
key = this.splitIdentifier(key)
|
|
155
174
|
}
|
|
175
|
+
return key.map(part => this.quote(part)).join(".")
|
|
156
176
|
}
|
|
157
177
|
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
.split(
|
|
163
|
-
|
|
164
|
-
|
|
178
|
+
// Turns an identifier like a.b.c or `a`.`b`.`c` into ["a", "b", "c"]
|
|
179
|
+
private splitIdentifier(key: string): string[] {
|
|
180
|
+
const [start, end] = this.quoteChars()
|
|
181
|
+
if (this.isQuoted(key)) {
|
|
182
|
+
return key.slice(1, -1).split(`${end}.${start}`)
|
|
183
|
+
}
|
|
184
|
+
return key.split(".")
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private qualifyIdentifier(key: string): string {
|
|
188
|
+
const tableName = this.getTableName()
|
|
189
|
+
const parts = this.splitIdentifier(key)
|
|
190
|
+
if (parts[0] !== tableName) {
|
|
191
|
+
parts.unshift(tableName)
|
|
192
|
+
}
|
|
193
|
+
if (this.isQuoted(key)) {
|
|
194
|
+
return this.quotedIdentifier(parts)
|
|
195
|
+
}
|
|
196
|
+
return parts.join(".")
|
|
165
197
|
}
|
|
166
198
|
|
|
167
199
|
private isFullSelectStatementRequired(): boolean {
|
|
@@ -231,8 +263,13 @@ class InternalBuilder {
|
|
|
231
263
|
// OracleDB can't use character-large-objects (CLOBs) in WHERE clauses,
|
|
232
264
|
// so when we use them we need to wrap them in to_char(). This function
|
|
233
265
|
// converts a field name to the appropriate identifier.
|
|
234
|
-
private convertClobs(field: string): string {
|
|
235
|
-
|
|
266
|
+
private convertClobs(field: string, opts?: { forSelect?: boolean }): string {
|
|
267
|
+
if (this.client !== SqlClient.ORACLE) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
"you've called convertClobs on a DB that's not Oracle, this is a mistake"
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
const parts = this.splitIdentifier(field)
|
|
236
273
|
const col = parts.pop()!
|
|
237
274
|
const schema = this.table.schema[col]
|
|
238
275
|
let identifier = this.quotedIdentifier(field)
|
|
@@ -244,7 +281,11 @@ class InternalBuilder {
|
|
|
244
281
|
schema.type === FieldType.OPTIONS ||
|
|
245
282
|
schema.type === FieldType.BARCODEQR
|
|
246
283
|
) {
|
|
247
|
-
|
|
284
|
+
if (opts?.forSelect) {
|
|
285
|
+
identifier = `to_char(${identifier}) as ${this.quotedIdentifier(col)}`
|
|
286
|
+
} else {
|
|
287
|
+
identifier = `to_char(${identifier})`
|
|
288
|
+
}
|
|
248
289
|
}
|
|
249
290
|
return identifier
|
|
250
291
|
}
|
|
@@ -859,28 +900,58 @@ class InternalBuilder {
|
|
|
859
900
|
const fields = this.query.resource?.fields || []
|
|
860
901
|
const tableName = this.getTableName()
|
|
861
902
|
if (fields.length > 0) {
|
|
862
|
-
|
|
863
|
-
|
|
903
|
+
const qualifiedFields = fields.map(field => this.qualifyIdentifier(field))
|
|
904
|
+
if (this.client === SqlClient.ORACLE) {
|
|
905
|
+
const groupByFields = qualifiedFields.map(field =>
|
|
906
|
+
this.convertClobs(field)
|
|
907
|
+
)
|
|
908
|
+
const selectFields = qualifiedFields.map(field =>
|
|
909
|
+
this.convertClobs(field, { forSelect: true })
|
|
910
|
+
)
|
|
911
|
+
query = query
|
|
912
|
+
.groupByRaw(groupByFields.join(", "))
|
|
913
|
+
.select(this.knex.raw(selectFields.join(", ")))
|
|
914
|
+
} else {
|
|
915
|
+
query = query.groupBy(qualifiedFields).select(qualifiedFields)
|
|
916
|
+
}
|
|
864
917
|
}
|
|
865
918
|
for (const aggregation of aggregations) {
|
|
866
919
|
const op = aggregation.calculationType
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
920
|
+
if (op === CalculationType.COUNT) {
|
|
921
|
+
if ("distinct" in aggregation && aggregation.distinct) {
|
|
922
|
+
if (this.client === SqlClient.ORACLE) {
|
|
923
|
+
const field = this.convertClobs(`${tableName}.${aggregation.field}`)
|
|
924
|
+
query = query.select(
|
|
925
|
+
this.knex.raw(
|
|
926
|
+
`COUNT(DISTINCT ${field}) as ${this.quotedIdentifier(
|
|
927
|
+
aggregation.name
|
|
928
|
+
)}`
|
|
929
|
+
)
|
|
930
|
+
)
|
|
931
|
+
} else {
|
|
932
|
+
query = query.countDistinct(
|
|
933
|
+
`${tableName}.${aggregation.field} as ${aggregation.name}`
|
|
934
|
+
)
|
|
935
|
+
}
|
|
936
|
+
} else {
|
|
937
|
+
query = query.count(`* as ${aggregation.name}`)
|
|
938
|
+
}
|
|
939
|
+
} else {
|
|
940
|
+
const field = `${tableName}.${aggregation.field} as ${aggregation.name}`
|
|
941
|
+
switch (op) {
|
|
942
|
+
case CalculationType.SUM:
|
|
943
|
+
query = query.sum(field)
|
|
944
|
+
break
|
|
945
|
+
case CalculationType.AVG:
|
|
946
|
+
query = query.avg(field)
|
|
947
|
+
break
|
|
948
|
+
case CalculationType.MIN:
|
|
949
|
+
query = query.min(field)
|
|
950
|
+
break
|
|
951
|
+
case CalculationType.MAX:
|
|
952
|
+
query = query.max(field)
|
|
953
|
+
break
|
|
954
|
+
}
|
|
884
955
|
}
|
|
885
956
|
}
|
|
886
957
|
return query
|
package/src/users/users.ts
CHANGED
|
@@ -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(
|
|
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,
|