diff --git a/packages/server/src/utilities/rowProcessor/bbReferenceProcessor.ts b/packages/server/src/utilities/rowProcessor/bbReferenceProcessor.ts index e5edc65edd..f26c29a0cd 100644 --- a/packages/server/src/utilities/rowProcessor/bbReferenceProcessor.ts +++ b/packages/server/src/utilities/rowProcessor/bbReferenceProcessor.ts @@ -10,99 +10,93 @@ import { InvalidBBRefError } from "./errors" const ROW_PREFIX = DocumentType.ROW + SEPARATOR -export function processInputBBReferences( +export async function processInputBBReference( value: string | { _id: string }, - type: FieldType.BB_REFERENCE_SINGLE -): Promise -export function processInputBBReferences( - value: string | string[] | { _id: string } | { _id: string }[], - type: FieldType.BB_REFERENCE, - subtype: BBReferenceFieldSubType -): Promise + subtype: BBReferenceFieldSubType.USER +): Promise { + if (value && Array.isArray(value)) { + throw "BB_REFERENCE_SINGLE cannot be an array" + } + let id = typeof value === "string" ? value : value?._id -export async function processInputBBReferences( - value: string | string[] | { _id: string } | { _id: string }[], - type: FieldType.BB_REFERENCE | FieldType.BB_REFERENCE_SINGLE, - subtype?: BBReferenceFieldSubType -): Promise { - switch (type) { - case FieldType.BB_REFERENCE: { - let referenceIds: string[] = [] + if (!id) { + return null + } - if (Array.isArray(value)) { - referenceIds.push( - ...value.map(idOrDoc => - typeof idOrDoc === "string" ? idOrDoc : idOrDoc._id - ) - ) - } else if (typeof value !== "string") { - referenceIds.push(value._id) - } else { - referenceIds.push( - ...value - .split(",") - .filter(x => x) - .map((id: string) => id.trim()) - ) + switch (subtype) { + case BBReferenceFieldSubType.USER: { + if (id.startsWith(ROW_PREFIX)) { + id = dbCore.getGlobalIDFromUserMetadataID(id) } - // make sure all reference IDs are correct global user IDs - // they may be user metadata references (start with row prefix) - // and these need to be converted to global IDs - referenceIds = referenceIds.map(id => { - if (id?.startsWith(ROW_PREFIX)) { - return dbCore.getGlobalIDFromUserMetadataID(id) - } else { - return id + try { + await cache.user.getUser(id) + return id + } catch (e: any) { + if (e.statusCode === 404) { + throw new InvalidBBRefError(id, BBReferenceFieldSubType.USER) } - }) - - switch (subtype) { - case undefined: - throw "Subtype must be defined" - case BBReferenceFieldSubType.USER: - case BBReferenceFieldSubType.USERS: { - const { notFoundIds } = await cache.user.getUsers(referenceIds) - - if (notFoundIds?.length) { - throw new InvalidBBRefError( - notFoundIds[0], - BBReferenceFieldSubType.USER - ) - } - - if (!referenceIds?.length) { - return null - } - - if (subtype === BBReferenceFieldSubType.USERS) { - return referenceIds - } - - return referenceIds.join(",") - } - default: - throw utils.unreachable(subtype) + throw e } } - case FieldType.BB_REFERENCE_SINGLE: { - if (value && Array.isArray(value)) { - throw "BB_REFERENCE_SINGLE cannot be an array" - } - - const id = typeof value === "string" ? value : value._id - - const user = await cache.user.getUser(id) - - if (!user) { - throw new InvalidBBRefError(id, BBReferenceFieldSubType.USER) - } - - return user._id! - } default: - throw utils.unreachable(type) + throw utils.unreachable(subtype) + } +} +export async function processInputBBReferences( + value: string | string[] | { _id: string }[], + subtype: BBReferenceFieldSubType +): Promise { + if (!value || !value[0]) { + return null + } + + let referenceIds + if (typeof value === "string") { + referenceIds = value + .split(",") + .map(u => u.trim()) + .filter(u => !!u) + } else { + referenceIds = value.map(idOrDoc => + typeof idOrDoc === "string" ? idOrDoc : idOrDoc._id + ) + } + + // make sure all reference IDs are correct global user IDs + // they may be user metadata references (start with row prefix) + // and these need to be converted to global IDs + referenceIds = referenceIds.map(id => { + if (id?.startsWith(ROW_PREFIX)) { + return dbCore.getGlobalIDFromUserMetadataID(id) + } else { + return id + } + }) + + switch (subtype) { + case undefined: + throw "Subtype must be defined" + case BBReferenceFieldSubType.USER: + case BBReferenceFieldSubType.USERS: { + const { notFoundIds } = await cache.user.getUsers(referenceIds) + + if (notFoundIds?.length) { + throw new InvalidBBRefError( + notFoundIds[0], + BBReferenceFieldSubType.USER + ) + } + + if (!referenceIds?.length) { + return null + } + + return referenceIds + } + default: + throw utils.unreachable(subtype) } } diff --git a/packages/server/src/utilities/rowProcessor/index.ts b/packages/server/src/utilities/rowProcessor/index.ts index 499ea0e4c5..8c8ac6e27d 100644 --- a/packages/server/src/utilities/rowProcessor/index.ts +++ b/packages/server/src/utilities/rowProcessor/index.ts @@ -12,6 +12,7 @@ import { } from "@budibase/types" import { cloneDeep } from "lodash/fp" import { + processInputBBReference, processInputBBReferences, processOutputBBReferences, } from "./bbReferenceProcessor" @@ -161,13 +162,9 @@ export async function inputProcessing( delete clonedRow[key].url } } else if (field.type === FieldType.BB_REFERENCE && value) { - clonedRow[key] = await processInputBBReferences( - value, - field.type, - field.subtype - ) + clonedRow[key] = await processInputBBReferences(value, field.subtype) } else if (field.type === FieldType.BB_REFERENCE_SINGLE && value) { - clonedRow[key] = await processInputBBReferences(value, field.type) + clonedRow[key] = await processInputBBReference(value, field.subtype) } } diff --git a/packages/server/src/utilities/rowProcessor/tests/bbReferenceProcessor.spec.ts b/packages/server/src/utilities/rowProcessor/tests/bbReferenceProcessor.spec.ts index 609dc9ffc0..a3c5cf8911 100644 --- a/packages/server/src/utilities/rowProcessor/tests/bbReferenceProcessor.spec.ts +++ b/packages/server/src/utilities/rowProcessor/tests/bbReferenceProcessor.spec.ts @@ -2,6 +2,7 @@ import _ from "lodash" import * as backendCore from "@budibase/backend-core" import { BBReferenceFieldSubType, FieldType, User } from "@budibase/types" import { + processInputBBReference, processInputBBReferences, processOutputBBReferences, } from "../bbReferenceProcessor" @@ -22,6 +23,7 @@ jest.mock("@budibase/backend-core", (): typeof backendCore => { ...actual.cache, user: { ...actual.cache.user, + getUser: jest.fn(actual.cache.user.getUser), getUsers: jest.fn(actual.cache.user.getUsers), }, }, @@ -31,9 +33,6 @@ jest.mock("@budibase/backend-core", (): typeof backendCore => { const config = new DBTestConfiguration() describe("bbReferenceProcessor", () => { - const cacheGetUsersSpy = backendCore.cache.user - .getUsers as jest.MockedFunction - const users: User[] = [] beforeAll(async () => { const userCount = 10 @@ -56,21 +55,81 @@ describe("bbReferenceProcessor", () => { jest.clearAllMocks() }) - describe("processInputBBReferences", () => { + describe("processInputBBReference", () => { describe("subtype user", () => { + const cacheGetUserSpy = backendCore.cache.user + .getUser as jest.MockedFunction + it("validate valid string id", async () => { const user = _.sample(users) const userId = user!._id! const result = await config.doInTenant(() => - processInputBBReferences( - userId, - FieldType.BB_REFERENCE, - BBReferenceFieldSubType.USER - ) + processInputBBReference(userId, BBReferenceFieldSubType.USER) ) expect(result).toEqual(userId) + expect(cacheGetUserSpy).toHaveBeenCalledTimes(1) + expect(cacheGetUserSpy).toHaveBeenCalledWith(userId) + }) + + it("throws an error given an invalid id", async () => { + const userId = generator.guid() + + await expect( + config.doInTenant(() => + processInputBBReference(userId, BBReferenceFieldSubType.USER) + ) + ).rejects.toThrow( + new InvalidBBRefError(userId, BBReferenceFieldSubType.USER) + ) + }) + + it("validate valid user object", async () => { + const userId = _.sample(users)!._id! + + const result = await config.doInTenant(() => + processInputBBReference({ _id: userId }, BBReferenceFieldSubType.USER) + ) + + expect(result).toEqual(userId) + expect(cacheGetUserSpy).toHaveBeenCalledTimes(1) + expect(cacheGetUserSpy).toHaveBeenCalledWith(userId) + }) + + it("empty strings will return null", async () => { + const result = await config.doInTenant(() => + processInputBBReference("", BBReferenceFieldSubType.USER) + ) + + expect(result).toEqual(null) + }) + + it("should convert user medata IDs to global IDs", async () => { + const userId = _.sample(users)!._id! + const userMetadataId = backendCore.db.generateUserMetadataID(userId) + const result = await config.doInTenant(() => + processInputBBReference(userMetadataId, BBReferenceFieldSubType.USER) + ) + expect(result).toBe(userId) + }) + }) + }) + + describe("processInputBBReferences", () => { + describe("subtype user", () => { + const cacheGetUsersSpy = backendCore.cache.user + .getUsers as jest.MockedFunction + + it("validate valid string id", async () => { + const user = _.sample(users) + const userId = user!._id! + + const result = await config.doInTenant(() => + processInputBBReferences(userId, BBReferenceFieldSubType.USER) + ) + + expect(result).toEqual([userId]) expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1) expect(cacheGetUsersSpy).toHaveBeenCalledWith([userId]) }) @@ -80,11 +139,7 @@ describe("bbReferenceProcessor", () => { await expect( config.doInTenant(() => - processInputBBReferences( - userId, - FieldType.BB_REFERENCE, - BBReferenceFieldSubType.USER - ) + processInputBBReferences(userId, BBReferenceFieldSubType.USER) ) ).rejects.toThrow( new InvalidBBRefError(userId, BBReferenceFieldSubType.USER) @@ -98,14 +153,10 @@ describe("bbReferenceProcessor", () => { const userIdCsv = userIds.join(" , ") const result = await config.doInTenant(() => - processInputBBReferences( - userIdCsv, - FieldType.BB_REFERENCE, - BBReferenceFieldSubType.USER - ) + processInputBBReferences(userIdCsv, BBReferenceFieldSubType.USER) ) - expect(result).toEqual(userIds.join(",")) + expect(result).toEqual(userIds) expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1) expect(cacheGetUsersSpy).toHaveBeenCalledWith(userIds) }) @@ -122,45 +173,22 @@ describe("bbReferenceProcessor", () => { await expect( config.doInTenant(() => - processInputBBReferences( - userIdCsv, - FieldType.BB_REFERENCE, - BBReferenceFieldSubType.USER - ) + processInputBBReferences(userIdCsv, BBReferenceFieldSubType.USER) ) ).rejects.toThrow( new InvalidBBRefError(wrongId, BBReferenceFieldSubType.USER) ) }) - it("validate valid user object", async () => { - const userId = _.sample(users)!._id! - - const result = await config.doInTenant(() => - processInputBBReferences( - { _id: userId }, - FieldType.BB_REFERENCE, - BBReferenceFieldSubType.USER - ) - ) - - expect(result).toEqual(userId) - expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1) - expect(cacheGetUsersSpy).toHaveBeenCalledWith([userId]) - }) - it("validate valid user object array", async () => { - const userIds = _.sampleSize(users, 3).map(x => x._id!) + const inputUsers = _.sampleSize(users, 3).map(u => ({ _id: u._id! })) + const userIds = inputUsers.map(u => u._id) const result = await config.doInTenant(() => - processInputBBReferences( - userIds, - FieldType.BB_REFERENCE, - BBReferenceFieldSubType.USER - ) + processInputBBReferences(inputUsers, BBReferenceFieldSubType.USER) ) - expect(result).toEqual(userIds.join(",")) + expect(result).toEqual(userIds) expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1) expect(cacheGetUsersSpy).toHaveBeenCalledWith(userIds) }) @@ -169,7 +197,7 @@ describe("bbReferenceProcessor", () => { const result = await config.doInTenant(() => processInputBBReferences( "", - FieldType.BB_REFERENCE, + BBReferenceFieldSubType.USER ) ) @@ -179,11 +207,7 @@ describe("bbReferenceProcessor", () => { it("empty arrays will return null", async () => { const result = await config.doInTenant(() => - processInputBBReferences( - [], - FieldType.BB_REFERENCE, - BBReferenceFieldSubType.USER - ) + processInputBBReferences([], BBReferenceFieldSubType.USER) ) expect(result).toEqual(null) @@ -193,13 +217,9 @@ describe("bbReferenceProcessor", () => { const userId = _.sample(users)!._id! const userMetadataId = backendCore.db.generateUserMetadataID(userId) const result = await config.doInTenant(() => - processInputBBReferences( - userMetadataId, - FieldType.BB_REFERENCE, - BBReferenceFieldSubType.USER - ) + processInputBBReferences(userMetadataId, BBReferenceFieldSubType.USER) ) - expect(result).toBe(userId) + expect(result).toEqual([userId]) }) }) })