Merge branch 'master' into feature-flag-helper

This commit is contained in:
Sam Rose 2024-10-08 14:42:09 +01:00 committed by GitHub
commit 2b47789fc8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 212 additions and 53 deletions

View File

@ -2,7 +2,6 @@ import { generateGlobalUserID } from "../../../db"
import { authError } from "../utils" import { authError } from "../utils"
import * as users from "../../../users" import * as users from "../../../users"
import * as context from "../../../context" import * as context from "../../../context"
import fetch from "node-fetch"
import { import {
SaveSSOUserFunction, SaveSSOUserFunction,
SSOAuthDetails, SSOAuthDetails,
@ -97,28 +96,13 @@ export async function authenticate(
return done(null, ssoUser) return done(null, ssoUser)
} }
async function getProfilePictureUrl(user: User, details: SSOAuthDetails) {
const pictureUrl = details.profile?._json.picture
if (pictureUrl) {
const response = await fetch(pictureUrl)
if (response.status === 200) {
const type = response.headers.get("content-type") as string
if (type.startsWith("image/")) {
return pictureUrl
}
}
}
}
/** /**
* @returns a user that has been sync'd with third party information * @returns a user that has been sync'd with third party information
*/ */
async function syncUser(user: User, details: SSOAuthDetails): Promise<SSOUser> { async function syncUser(user: User, details: SSOAuthDetails): Promise<SSOUser> {
let firstName let firstName
let lastName let lastName
let pictureUrl
let oauth2 let oauth2
let thirdPartyProfile
if (details.profile) { if (details.profile) {
const profile = details.profile const profile = details.profile
@ -134,12 +118,6 @@ async function syncUser(user: User, details: SSOAuthDetails): Promise<SSOUser> {
lastName = name.familyName lastName = name.familyName
} }
} }
pictureUrl = await getProfilePictureUrl(user, details)
thirdPartyProfile = {
...profile._json,
}
} }
// oauth tokens for future use // oauth tokens for future use
@ -155,8 +133,6 @@ async function syncUser(user: User, details: SSOAuthDetails): Promise<SSOUser> {
providerType: details.providerType, providerType: details.providerType,
firstName, firstName,
lastName, lastName,
thirdPartyProfile,
pictureUrl,
oauth2, oauth2,
} }
} }

View File

@ -59,10 +59,8 @@ export function ssoAccount(account: Account = cloudAccount()): SSOAccount {
accessToken: generator.string(), accessToken: generator.string(),
refreshToken: generator.string(), refreshToken: generator.string(),
}, },
pictureUrl: generator.url(),
provider: provider(), provider: provider(),
providerType: providerType(), providerType: providerType(),
thirdPartyProfile: {},
} }
} }
@ -76,9 +74,7 @@ export function verifiableSsoAccount(
accessToken: generator.string(), accessToken: generator.string(),
refreshToken: generator.string(), refreshToken: generator.string(),
}, },
pictureUrl: generator.url(),
provider: AccountSSOProvider.MICROSOFT, provider: AccountSSOProvider.MICROSOFT,
providerType: AccountSSOProviderType.MICROSOFT, providerType: AccountSSOProviderType.MICROSOFT,
thirdPartyProfile: { id: "abc123" },
} }
} }

View File

@ -25,7 +25,6 @@ export const user = (userProps?: Partial<Omit<User, "userId">>): User => {
roles: { app_test: "admin" }, roles: { app_test: "admin" },
firstName: generator.first(), firstName: generator.first(),
lastName: generator.last(), lastName: generator.last(),
pictureUrl: "http://example.com",
tenantId: tenant.id(), tenantId: tenant.id(),
...userProps, ...userProps,
} }
@ -86,9 +85,5 @@ export function ssoUser(
oauth2: opts.details?.oauth2, oauth2: opts.details?.oauth2,
provider: opts.details?.provider!, provider: opts.details?.provider!,
providerType: opts.details?.providerType!, providerType: opts.details?.providerType!,
thirdPartyProfile: {
email: base.email,
picture: base.pictureUrl,
},
} }
} }

View File

@ -127,13 +127,13 @@ export async function create(ctx: Ctx<CreateViewRequest, ViewResponse>) {
const parsedView: Omit<RequiredKeys<ViewV2>, "id" | "version"> = { const parsedView: Omit<RequiredKeys<ViewV2>, "id" | "version"> = {
name: view.name, name: view.name,
type: view.type,
tableId: view.tableId, tableId: view.tableId,
query: view.query, query: view.query,
queryUI: view.queryUI, queryUI: view.queryUI,
sort: view.sort, sort: view.sort,
schema, schema,
primaryDisplay: view.primaryDisplay, primaryDisplay: view.primaryDisplay,
uiMetadata: view.uiMetadata,
} }
const result = await sdk.views.create(tableId, parsedView) const result = await sdk.views.create(tableId, parsedView)
ctx.status = 201 ctx.status = 201
@ -163,6 +163,7 @@ export async function update(ctx: Ctx<UpdateViewRequest, ViewResponse>) {
const parsedView: RequiredKeys<ViewV2> = { const parsedView: RequiredKeys<ViewV2> = {
id: view.id, id: view.id,
name: view.name, name: view.name,
type: view.type,
version: view.version, version: view.version,
tableId: view.tableId, tableId: view.tableId,
query: view.query, query: view.query,
@ -170,7 +171,6 @@ export async function update(ctx: Ctx<UpdateViewRequest, ViewResponse>) {
sort: view.sort, sort: view.sort,
schema, schema,
primaryDisplay: view.primaryDisplay, primaryDisplay: view.primaryDisplay,
uiMetadata: view.uiMetadata,
} }
const result = await sdk.views.update(tableId, parsedView) const result = await sdk.views.update(tableId, parsedView)

View File

@ -25,6 +25,7 @@ import {
BBReferenceFieldSubType, BBReferenceFieldSubType,
NumericCalculationFieldMetadata, NumericCalculationFieldMetadata,
ViewV2Schema, ViewV2Schema,
ViewV2Type,
} from "@budibase/types" } from "@budibase/types"
import { generator, mocks } from "@budibase/backend-core/tests" import { generator, mocks } from "@budibase/backend-core/tests"
import { DatabaseName, getDatasource } from "../../../integrations/tests/utils" import { DatabaseName, getDatasource } from "../../../integrations/tests/utils"
@ -143,7 +144,7 @@ describe.each([
}) })
it("can persist views with all fields", async () => { it("can persist views with all fields", async () => {
const newView: Required<Omit<CreateViewRequest, "queryUI">> = { const newView: Required<Omit<CreateViewRequest, "queryUI" | "type">> = {
name: generator.name(), name: generator.name(),
tableId: table._id!, tableId: table._id!,
primaryDisplay: "id", primaryDisplay: "id",
@ -165,9 +166,6 @@ describe.each([
visible: true, visible: true,
}, },
}, },
uiMetadata: {
foo: "bar",
},
} }
const res = await config.api.viewV2.create(newView) const res = await config.api.viewV2.create(newView)
@ -537,6 +535,7 @@ describe.each([
let view = await config.api.viewV2.create({ let view = await config.api.viewV2.create({
tableId: table._id!, tableId: table._id!,
name: generator.guid(), name: generator.guid(),
type: ViewV2Type.CALCULATION,
schema: { schema: {
sum: { sum: {
visible: true, visible: true,
@ -560,11 +559,35 @@ describe.each([
expect(sum.field).toEqual("Price") expect(sum.field).toEqual("Price")
}) })
it("cannot create a view with calculation fields unless it has the right type", async () => {
await config.api.viewV2.create(
{
tableId: table._id!,
name: generator.guid(),
schema: {
sum: {
visible: true,
calculationType: CalculationType.SUM,
field: "Price",
},
},
},
{
status: 400,
body: {
message:
"Calculation fields are not allowed in non-calculation views",
},
}
)
})
it("cannot create a calculation view with more than 5 aggregations", async () => { it("cannot create a calculation view with more than 5 aggregations", async () => {
await config.api.viewV2.create( await config.api.viewV2.create(
{ {
tableId: table._id!, tableId: table._id!,
name: generator.guid(), name: generator.guid(),
type: ViewV2Type.CALCULATION,
schema: { schema: {
sum: { sum: {
visible: true, visible: true,
@ -606,6 +629,113 @@ describe.each([
} }
) )
}) })
it("cannot create a calculation view with duplicate calculations", async () => {
await config.api.viewV2.create(
{
tableId: table._id!,
name: generator.guid(),
type: ViewV2Type.CALCULATION,
schema: {
sum: {
visible: true,
calculationType: CalculationType.SUM,
field: "Price",
},
sum2: {
visible: true,
calculationType: CalculationType.SUM,
field: "Price",
},
},
},
{
status: 400,
body: {
message:
'Duplicate calculation on field "Price", calculation type "sum"',
},
}
)
})
it("finds duplicate counts", async () => {
await config.api.viewV2.create(
{
tableId: table._id!,
name: generator.guid(),
type: ViewV2Type.CALCULATION,
schema: {
count: {
visible: true,
calculationType: CalculationType.COUNT,
},
count2: {
visible: true,
calculationType: CalculationType.COUNT,
},
},
},
{
status: 400,
body: {
message:
'Duplicate calculation on field "*", calculation type "count"',
},
}
)
})
it("finds duplicate count distincts", async () => {
await config.api.viewV2.create(
{
tableId: table._id!,
name: generator.guid(),
type: ViewV2Type.CALCULATION,
schema: {
count: {
visible: true,
calculationType: CalculationType.COUNT,
distinct: true,
field: "Price",
},
count2: {
visible: true,
calculationType: CalculationType.COUNT,
distinct: true,
field: "Price",
},
},
},
{
status: 400,
body: {
message:
'Duplicate calculation on field "Price", calculation type "count"',
},
}
)
})
it("does not confuse counts and count distincts in the duplicate check", async () => {
await config.api.viewV2.create({
tableId: table._id!,
name: generator.guid(),
type: ViewV2Type.CALCULATION,
schema: {
count: {
visible: true,
calculationType: CalculationType.COUNT,
},
count2: {
visible: true,
calculationType: CalculationType.COUNT,
distinct: true,
field: "Price",
},
},
})
})
}) })
describe("update", () => { describe("update", () => {
@ -650,7 +780,9 @@ describe.each([
it("can update all fields", async () => { it("can update all fields", async () => {
const tableId = table._id! const tableId = table._id!
const updatedData: Required<Omit<UpdateViewRequest, "queryUI">> = { const updatedData: Required<
Omit<UpdateViewRequest, "queryUI" | "type">
> = {
version: view.version, version: view.version,
id: view.id, id: view.id,
tableId, tableId,
@ -678,9 +810,6 @@ describe.each([
readonly: true, readonly: true,
}, },
}, },
uiMetadata: {
foo: "bar",
},
} }
await config.api.viewV2.update(updatedData) await config.api.viewV2.update(updatedData)
@ -865,6 +994,32 @@ describe.each([
) )
}) })
it("cannot update view type after creation", async () => {
const view = await config.api.viewV2.create({
tableId: table._id!,
name: generator.guid(),
schema: {
id: { visible: true },
Price: {
visible: true,
},
},
})
await config.api.viewV2.update(
{
...view,
type: ViewV2Type.CALCULATION,
},
{
status: 400,
body: {
message: "Cannot update view type after creation",
},
}
)
})
isInternal && isInternal &&
it("updating schema will only validate modified field", async () => { it("updating schema will only validate modified field", async () => {
let view = await config.api.viewV2.create({ let view = await config.api.viewV2.create({
@ -937,6 +1092,7 @@ describe.each([
view = await config.api.viewV2.create({ view = await config.api.viewV2.create({
tableId: table._id!, tableId: table._id!,
name: generator.guid(), name: generator.guid(),
type: ViewV2Type.CALCULATION,
schema: { schema: {
country: { country: {
visible: true, visible: true,
@ -2690,6 +2846,7 @@ describe.each([
it("should be able to search by calculations", async () => { it("should be able to search by calculations", async () => {
const view = await config.api.viewV2.create({ const view = await config.api.viewV2.create({
tableId: table._id!, tableId: table._id!,
type: ViewV2Type.CALCULATION,
name: generator.guid(), name: generator.guid(),
schema: { schema: {
"Quantity Sum": { "Quantity Sum": {
@ -2724,6 +2881,7 @@ describe.each([
const view = await config.api.viewV2.create({ const view = await config.api.viewV2.create({
tableId: table._id!, tableId: table._id!,
name: generator.guid(), name: generator.guid(),
type: ViewV2Type.CALCULATION,
schema: { schema: {
quantity: { quantity: {
visible: true, visible: true,
@ -2762,6 +2920,7 @@ describe.each([
const view = await config.api.viewV2.create({ const view = await config.api.viewV2.create({
tableId: table._id!, tableId: table._id!,
name: generator.guid(), name: generator.guid(),
type: ViewV2Type.CALCULATION,
schema: { schema: {
aggregate: { aggregate: {
visible: true, visible: true,
@ -2822,6 +2981,7 @@ describe.each([
const view = await config.api.viewV2.create({ const view = await config.api.viewV2.create({
tableId: table._id!, tableId: table._id!,
name: generator.guid(), name: generator.guid(),
type: ViewV2Type.CALCULATION,
schema: { schema: {
count: { count: {
visible: true, visible: true,
@ -2856,6 +3016,7 @@ describe.each([
{ {
tableId: table._id!, tableId: table._id!,
name: generator.guid(), name: generator.guid(),
type: ViewV2Type.CALCULATION,
schema: { schema: {
count: { count: {
visible: true, visible: true,
@ -2904,6 +3065,7 @@ describe.each([
const view = await config.api.viewV2.create({ const view = await config.api.viewV2.create({
tableId: table._id!, tableId: table._id!,
name: generator.guid(), name: generator.guid(),
type: ViewV2Type.CALCULATION,
schema: { schema: {
sum: { sum: {
visible: true, visible: true,

View File

@ -70,6 +70,9 @@ export async function update(tableId: string, view: ViewV2): Promise<ViewV2> {
if (!existingView || !existingView.name) { if (!existingView || !existingView.name) {
throw new HTTPError(`View ${view.id} not found in table ${tableId}`, 404) throw new HTTPError(`View ${view.id} not found in table ${tableId}`, 404)
} }
if (isV2(existingView) && existingView.type !== view.type) {
throw new HTTPError(`Cannot update view type after creation`, 400)
}
delete views[existingView.name] delete views[existingView.name]
views[view.name] = view views[view.name] = view

View File

@ -72,11 +72,23 @@ async function guardCalculationViewSchema(
) )
} }
for (const calculationFieldName of Object.keys(calculationFields)) { const seen: Record<string, Record<CalculationType, boolean>> = {}
const schema = calculationFields[calculationFieldName]
for (const name of Object.keys(calculationFields)) {
const schema = calculationFields[name]
const isCount = schema.calculationType === CalculationType.COUNT const isCount = schema.calculationType === CalculationType.COUNT
const isDistinct = isCount && "distinct" in schema && schema.distinct const isDistinct = isCount && "distinct" in schema && schema.distinct
const field = isCount && !isDistinct ? "*" : schema.field
if (seen[field]?.[schema.calculationType]) {
throw new HTTPError(
`Duplicate calculation on field "${field}", calculation type "${schema.calculationType}"`,
400
)
}
seen[field] ??= {} as Record<CalculationType, boolean>
seen[field][schema.calculationType] = true
// Count fields that aren't distinct don't need to reference another field, // Count fields that aren't distinct don't need to reference another field,
// so we don't validate it. // so we don't validate it.
if (isCount && !isDistinct) { if (isCount && !isDistinct) {
@ -86,14 +98,14 @@ async function guardCalculationViewSchema(
const targetSchema = table.schema[schema.field] const targetSchema = table.schema[schema.field]
if (!targetSchema) { if (!targetSchema) {
throw new HTTPError( throw new HTTPError(
`Calculation field "${calculationFieldName}" references field "${schema.field}" which does not exist in the table schema`, `Calculation field "${name}" references field "${schema.field}" which does not exist in the table schema`,
400 400
) )
} }
if (!isCount && !helpers.schema.isNumeric(targetSchema)) { if (!isCount && !helpers.schema.isNumeric(targetSchema)) {
throw new HTTPError( throw new HTTPError(
`Calculation field "${calculationFieldName}" references field "${schema.field}" which is not a numeric field`, `Calculation field "${name}" references field "${schema.field}" which is not a numeric field`,
400 400
) )
} }
@ -119,6 +131,13 @@ async function guardViewSchema(
if (helpers.views.isCalculationView(view)) { if (helpers.views.isCalculationView(view)) {
await guardCalculationViewSchema(table, view) await guardCalculationViewSchema(table, view)
} else {
if (helpers.views.hasCalculationFields(view)) {
throw new HTTPError(
"Calculation fields are not allowed in non-calculation views",
400
)
}
} }
await checkReadonlyFields(table, view) await checkReadonlyFields(table, view)

View File

@ -59,6 +59,10 @@ export async function update(tableId: string, view: ViewV2): Promise<ViewV2> {
throw new HTTPError(`View ${view.id} not found in table ${tableId}`, 404) throw new HTTPError(`View ${view.id} not found in table ${tableId}`, 404)
} }
if (isV2(existingView) && existingView.type !== view.type) {
throw new HTTPError(`Cannot update view type after creation`, 400)
}
delete table.views[existingView.name] delete table.views[existingView.name]
table.views[view.name] = view table.views[view.name] = view
await db.put(table) await db.put(table)

View File

@ -3,6 +3,7 @@ import {
ViewCalculationFieldMetadata, ViewCalculationFieldMetadata,
ViewFieldMetadata, ViewFieldMetadata,
ViewV2, ViewV2,
ViewV2Type,
} from "@budibase/types" } from "@budibase/types"
import { pickBy } from "lodash" import { pickBy } from "lodash"
@ -21,6 +22,10 @@ export function isBasicViewField(
type UnsavedViewV2 = Omit<ViewV2, "id" | "version"> type UnsavedViewV2 = Omit<ViewV2, "id" | "version">
export function isCalculationView(view: UnsavedViewV2) { export function isCalculationView(view: UnsavedViewV2) {
return view.type === ViewV2Type.CALCULATION
}
export function hasCalculationFields(view: UnsavedViewV2) {
return Object.values(view.schema || {}).some(isCalculationField) return Object.values(view.schema || {}).some(isCalculationField)
} }

View File

@ -12,7 +12,6 @@ export interface CreateAccountRequest {
name?: string name?: string
password: string password: string
provider?: AccountSSOProvider provider?: AccountSSOProvider
thirdPartyProfile: object
} }
export interface SearchAccountsRequest { export interface SearchAccountsRequest {

View File

@ -98,8 +98,6 @@ export interface AccountSSO {
provider: AccountSSOProvider provider: AccountSSOProvider
providerType: AccountSSOProviderType providerType: AccountSSOProviderType
oauth2?: OAuthTokens oauth2?: OAuthTokens
pictureUrl?: string
thirdPartyProfile: any // TODO: define what the google profile looks like
} }
export type SSOAccount = (Account | CloudAccount) & AccountSSO export type SSOAccount = (Account | CloudAccount) & AccountSSO

View File

@ -79,10 +79,15 @@ export enum CalculationType {
MAX = "max", MAX = "max",
} }
export enum ViewV2Type {
CALCULATION = "calculation",
}
export interface ViewV2 { export interface ViewV2 {
version: 2 version: 2
id: string id: string
name: string name: string
type?: ViewV2Type
primaryDisplay?: string primaryDisplay?: string
tableId: string tableId: string
query?: LegacyFilter[] | SearchFilters query?: LegacyFilter[] | SearchFilters
@ -94,7 +99,6 @@ export interface ViewV2 {
type?: SortType type?: SortType
} }
schema?: ViewV2Schema schema?: ViewV2Schema
uiMetadata?: Record<string, any>
} }
export type ViewV2Schema = Record<string, ViewFieldMetadata> export type ViewV2Schema = Record<string, ViewFieldMetadata>

View File

@ -21,7 +21,6 @@ export interface UserSSO {
provider: string // the individual provider e.g. Okta, Auth0, Google provider: string // the individual provider e.g. Okta, Auth0, Google
providerType: SSOProviderType providerType: SSOProviderType
oauth2?: OAuth2 oauth2?: OAuth2
thirdPartyProfile?: SSOProfileJson
profile?: { profile?: {
displayName?: string displayName?: string
name?: { name?: {
@ -45,7 +44,6 @@ export interface User extends Document {
userId?: string userId?: string
firstName?: string firstName?: string
lastName?: string lastName?: string
pictureUrl?: string
forceResetPassword?: boolean forceResetPassword?: boolean
roles: UserRoles roles: UserRoles
builder?: { builder?: {