From e53ec31305741a9d1b8201296cab25ab0d1f49c1 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 25 Oct 2024 13:20:15 +0200 Subject: [PATCH 01/21] Add row create test --- .../server/src/api/routes/tests/row.spec.ts | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 31067ab40f..d79a8a7379 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -939,6 +939,74 @@ describe.each([ }) }) }) + + describe("relations to same table", () => { + let relatedRows: Row[] + + beforeAll(async () => { + const relatedTable = await config.api.table.save( + defaultTable({ + schema: { + name: { name: "name", type: FieldType.STRING }, + }, + }) + ) + const relatedTableId = relatedTable._id! + table = await config.api.table.save( + defaultTable({ + schema: { + name: { name: "name", type: FieldType.STRING }, + related1: { + type: FieldType.LINK, + name: "related1", + fieldName: "main1", + tableId: relatedTableId, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + related2: { + type: FieldType.LINK, + name: "related2", + fieldName: "main2", + tableId: relatedTableId, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + }, + }) + ) + relatedRows = await Promise.all([ + config.api.row.save(relatedTableId, { name: "foo" }), + config.api.row.save(relatedTableId, { name: "bar" }), + config.api.row.save(relatedTableId, { name: "baz" }), + config.api.row.save(relatedTableId, { name: "boo" }), + ]) + }) + + it("can create rows with both relationships", async () => { + const row = await config.api.row.save(table._id!, { + name: "test", + related1: [relatedRows[0]._id!], + related2: [relatedRows[1]._id!], + }) + + expect(row).toEqual( + expect.objectContaining({ + name: "test", + related1: [ + { + _id: relatedRows[0]._id, + primaryDisplay: relatedRows[0].name, + }, + ], + related2: [ + { + _id: relatedRows[1]._id, + primaryDisplay: relatedRows[1].name, + }, + ], + }) + ) + }) + }) }) describe("get", () => { From d044d56801fc0f53184388da21ecee9f88e2c124 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 25 Oct 2024 13:25:28 +0200 Subject: [PATCH 02/21] Add more tests --- .../server/src/api/routes/tests/row.spec.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index d79a8a7379..4bfd612775 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -1006,6 +1006,36 @@ describe.each([ }) ) }) + + it("can create rows with no relationships", async () => { + const row = await config.api.row.save(table._id!, { + name: "test", + }) + + expect(row.related1).toBeUndefined() + expect(row.related2).toBeUndefined() + }) + + it("can create rows with only one relationships field", async () => { + const row = await config.api.row.save(table._id!, { + name: "test", + related1: [], + related2: [relatedRows[1]._id!], + }) + + expect(row).toEqual( + expect.objectContaining({ + name: "test", + related2: [ + { + _id: relatedRows[1]._id, + primaryDisplay: relatedRows[1].name, + }, + ], + }) + ) + expect(row.related1).toBeUndefined() + }) }) }) From 5efcf97c0d6815d8cc80fe06216b8c5d160602b3 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 25 Oct 2024 15:16:54 +0200 Subject: [PATCH 03/21] Add more tests --- .../server/src/api/routes/tests/row.spec.ts | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 4bfd612775..32788d2783 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -1139,6 +1139,133 @@ describe.each([ const rows = await config.api.row.fetch(table._id!) expect(rows).toHaveLength(1) }) + + describe("relations to same table", () => { + let relatedRows: Row[] + + beforeAll(async () => { + const relatedTable = await config.api.table.save( + defaultTable({ + schema: { + name: { name: "name", type: FieldType.STRING }, + }, + }) + ) + const relatedTableId = relatedTable._id! + table = await config.api.table.save( + defaultTable({ + schema: { + name: { name: "name", type: FieldType.STRING }, + related1: { + type: FieldType.LINK, + name: "related1", + fieldName: "main1", + tableId: relatedTableId, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + related2: { + type: FieldType.LINK, + name: "related2", + fieldName: "main2", + tableId: relatedTableId, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + }, + }) + ) + relatedRows = await Promise.all([ + config.api.row.save(relatedTableId, { name: "foo" }), + config.api.row.save(relatedTableId, { name: "bar" }), + config.api.row.save(relatedTableId, { name: "baz" }), + config.api.row.save(relatedTableId, { name: "boo" }), + ]) + }) + + it("can edit rows with both relationships", async () => { + let row = await config.api.row.save(table._id!, { + name: "test", + related1: [relatedRows[0]._id!], + related2: [relatedRows[1]._id!], + }) + + row = await config.api.row.save(table._id!, { + ...row, + related1: [relatedRows[0]._id!, relatedRows[1]._id!], + related2: [relatedRows[2]._id!], + }) + + expect(row).toEqual( + expect.objectContaining({ + name: "test", + related1: [ + { + _id: relatedRows[1]._id, + primaryDisplay: relatedRows[1].name, + }, + { + _id: relatedRows[0]._id, + primaryDisplay: relatedRows[0].name, + }, + ], + related2: [ + { + _id: relatedRows[2]._id, + primaryDisplay: relatedRows[2].name, + }, + ], + }) + ) + }) + + it("can drop existing relationship", async () => { + let row = await config.api.row.save(table._id!, { + name: "test", + related1: [relatedRows[0]._id!], + related2: [relatedRows[1]._id!], + }) + + row = await config.api.row.save(table._id!, { + ...row, + related1: [], + related2: [relatedRows[2]._id!], + }) + + expect(row).toEqual( + expect.objectContaining({ + name: "test", + related2: [ + { + _id: relatedRows[2]._id, + primaryDisplay: relatedRows[2].name, + }, + ], + }) + ) + expect(row.related1).toBeUndefined() + }) + + it("can drop both relationships", async () => { + let row = await config.api.row.save(table._id!, { + name: "test", + related1: [relatedRows[0]._id!], + related2: [relatedRows[1]._id!], + }) + + row = await config.api.row.save(table._id!, { + ...row, + related1: [], + related2: [], + }) + + expect(row).toEqual( + expect.objectContaining({ + name: "test", + }) + ) + expect(row.related1).toBeUndefined() + expect(row.related2).toBeUndefined() + }) + }) }) describe("patch", () => { From 96be44781779b981de7b3350ff21004e449e3e36 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 25 Oct 2024 15:22:57 +0200 Subject: [PATCH 04/21] Fix many to many --- .../api/controllers/row/ExternalRequest.ts | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index 5ceed002de..527bc4b3ef 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -425,8 +425,12 @@ export class ExternalRequest { */ async lookupRelations(tableId: string, row: Row) { const related: { - [key: string]: { rows: Row[]; isMany: boolean; tableId: string } - } = {} + rows: Row[] + isMany: boolean + tableId: string + field: string + }[] = [] + const { tableName } = breakExternalTableId(tableId) const table = this.tables[tableName] // @ts-ignore @@ -489,11 +493,13 @@ export class ExternalRequest { const storeTo = isManyToMany(field) ? field.throughFrom || linkPrimaryKey : fieldName - related[storeTo] = { + + related.push({ rows, isMany: isManyToMany(field), tableId: relatedTableId, - } + field: storeTo, + }) } return related } @@ -528,7 +534,9 @@ export class ExternalRequest { const linkSecondary = relationshipPrimary[1] - const rows = related[key]?.rows || [] + const rows = + related.find(r => r.tableId === relationship.tableId && r.field === key) + ?.rows || [] const relationshipMatchPredicate = ({ row, @@ -573,12 +581,12 @@ export class ExternalRequest { } } // finally cleanup anything that needs to be removed - for (let [colName, { isMany, rows, tableId }] of Object.entries(related)) { + for (let { isMany, rows, tableId, field } of related) { const table: Table | undefined = this.getTable(tableId) // if it's not the foreign key skip it, nothing to do if ( !table || - (!isMany && table.primary && table.primary.indexOf(colName) !== -1) + (!isMany && table.primary && table.primary.indexOf(field) !== -1) ) { continue } @@ -586,7 +594,7 @@ export class ExternalRequest { const rowId = generateIdForRow(row, table) const promise: Promise = isMany ? this.removeManyToManyRelationships(rowId, table) - : this.removeOneToManyRelationships(rowId, table, colName) + : this.removeOneToManyRelationships(rowId, table, field) if (promise) { promises.push(promise) } @@ -603,7 +611,9 @@ export class ExternalRequest { if (!isManyToOne(relationshipColumn)) { continue } - const { rows, isMany, tableId } = related[relationshipColumn.fieldName] + const { rows, isMany, tableId } = related.find( + r => r.field === relationshipColumn.fieldName + )! const table = this.getTable(tableId)! await Promise.all( rows.map(row => { From 00eedbf72685d6a8a24f3b9a946f054d9a0de8e3 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 25 Oct 2024 16:57:50 +0200 Subject: [PATCH 05/21] Fix deletions --- .../api/controllers/row/ExternalRequest.ts | 27 ++++---- .../server/src/api/routes/tests/row.spec.ts | 66 +++++++++++++++++++ 2 files changed, 82 insertions(+), 11 deletions(-) diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index 527bc4b3ef..1fb8ce0423 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -11,6 +11,7 @@ import { IncludeRelationship, InternalSearchFilterOperator, isManyToOne, + isOneToMany, OneToManyRelationshipFieldMetadata, Operation, PaginationJson, @@ -50,6 +51,7 @@ import sdk from "../../../sdk" import env from "../../../environment" import { makeExternalQuery } from "../../../integrations/base/query" import { dataFilters, helpers } from "@budibase/shared-core" +import { isRelationshipColumn } from "../../../db/utils" export interface ManyRelationship { tableId?: string @@ -606,25 +608,28 @@ export class ExternalRequest { async removeRelationshipsToRow(table: Table, rowId: string) { const row = await this.getRow(table, rowId) const related = await this.lookupRelations(table._id!, row) - for (let column of Object.values(table.schema)) { - const relationshipColumn = column as RelationshipFieldMetadata - if (!isManyToOne(relationshipColumn)) { + for (const column of Object.values(table.schema)) { + if (!isRelationshipColumn(column) || isOneToMany(column)) { continue } - const { rows, isMany, tableId } = related.find( - r => r.field === relationshipColumn.fieldName - )! + + const relatedByTable = isManyToMany(column) + ? related.find( + r => r.tableId === column.through && r.field === column.fieldName + ) + : related.find(r => r.field === column.fieldName) + if (!relatedByTable) { + continue + } + + const { rows, isMany, tableId } = relatedByTable const table = this.getTable(tableId)! await Promise.all( rows.map(row => { const rowId = generateIdForRow(row, table) return isMany ? this.removeManyToManyRelationships(rowId, table) - : this.removeOneToManyRelationships( - rowId, - table, - relationshipColumn.fieldName - ) + : this.removeOneToManyRelationships(rowId, table, column.fieldName) }) ) } diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 32788d2783..19d6595619 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -1542,6 +1542,72 @@ describe.each([ ) expect(res.length).toEqual(2) }) + + describe("relations to same table", () => { + let relatedRows: Row[] + + beforeAll(async () => { + const relatedTable = await config.api.table.save( + defaultTable({ + schema: { + name: { name: "name", type: FieldType.STRING }, + }, + }) + ) + const relatedTableId = relatedTable._id! + table = await config.api.table.save( + defaultTable({ + schema: { + name: { name: "name", type: FieldType.STRING }, + related1: { + type: FieldType.LINK, + name: "related1", + fieldName: "main1", + tableId: relatedTableId, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + related2: { + type: FieldType.LINK, + name: "related2", + fieldName: "main2", + tableId: relatedTableId, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + }, + }) + ) + relatedRows = await Promise.all([ + config.api.row.save(relatedTableId, { name: "foo" }), + config.api.row.save(relatedTableId, { name: "bar" }), + config.api.row.save(relatedTableId, { name: "baz" }), + config.api.row.save(relatedTableId, { name: "boo" }), + ]) + }) + + it("can delete rows with both relationships", async () => { + const row = await config.api.row.save(table._id!, { + name: "test", + related1: [relatedRows[0]._id!], + related2: [relatedRows[1]._id!], + }) + + await config.api.row.delete(table._id!, { _id: row._id! }) + + await config.api.row.get(table._id!, row._id!, { status: 404 }) + }) + + it("can delete rows with empty relationships", async () => { + const row = await config.api.row.save(table._id!, { + name: "test", + related1: [], + related2: [], + }) + + await config.api.row.delete(table._id!, { _id: row._id! }) + + await config.api.row.get(table._id!, row._id!, { status: 404 }) + }) + }) }) describe("validate", () => { From a01c5ed654dbc3100522bc5267269b406a317e97 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 25 Oct 2024 17:01:25 +0200 Subject: [PATCH 06/21] Don't run tests for lucene --- .../server/src/api/routes/tests/row.spec.ts | 495 +++++++++--------- 1 file changed, 249 insertions(+), 246 deletions(-) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 19d6595619..19f11a044a 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -940,103 +940,104 @@ describe.each([ }) }) - describe("relations to same table", () => { - let relatedRows: Row[] + !isLucene && + describe("relations to same table", () => { + let relatedRows: Row[] - beforeAll(async () => { - const relatedTable = await config.api.table.save( - defaultTable({ - schema: { - name: { name: "name", type: FieldType.STRING }, - }, - }) - ) - const relatedTableId = relatedTable._id! - table = await config.api.table.save( - defaultTable({ - schema: { - name: { name: "name", type: FieldType.STRING }, - related1: { - type: FieldType.LINK, - name: "related1", - fieldName: "main1", - tableId: relatedTableId, - relationshipType: RelationshipType.MANY_TO_MANY, + beforeAll(async () => { + const relatedTable = await config.api.table.save( + defaultTable({ + schema: { + name: { name: "name", type: FieldType.STRING }, }, - related2: { - type: FieldType.LINK, - name: "related2", - fieldName: "main2", - tableId: relatedTableId, - relationshipType: RelationshipType.MANY_TO_MANY, + }) + ) + const relatedTableId = relatedTable._id! + table = await config.api.table.save( + defaultTable({ + schema: { + name: { name: "name", type: FieldType.STRING }, + related1: { + type: FieldType.LINK, + name: "related1", + fieldName: "main1", + tableId: relatedTableId, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + related2: { + type: FieldType.LINK, + name: "related2", + fieldName: "main2", + tableId: relatedTableId, + relationshipType: RelationshipType.MANY_TO_MANY, + }, }, - }, - }) - ) - relatedRows = await Promise.all([ - config.api.row.save(relatedTableId, { name: "foo" }), - config.api.row.save(relatedTableId, { name: "bar" }), - config.api.row.save(relatedTableId, { name: "baz" }), - config.api.row.save(relatedTableId, { name: "boo" }), - ]) - }) - - it("can create rows with both relationships", async () => { - const row = await config.api.row.save(table._id!, { - name: "test", - related1: [relatedRows[0]._id!], - related2: [relatedRows[1]._id!], + }) + ) + relatedRows = await Promise.all([ + config.api.row.save(relatedTableId, { name: "foo" }), + config.api.row.save(relatedTableId, { name: "bar" }), + config.api.row.save(relatedTableId, { name: "baz" }), + config.api.row.save(relatedTableId, { name: "boo" }), + ]) }) - expect(row).toEqual( - expect.objectContaining({ + it("can create rows with both relationships", async () => { + const row = await config.api.row.save(table._id!, { name: "test", - related1: [ - { - _id: relatedRows[0]._id, - primaryDisplay: relatedRows[0].name, - }, - ], - related2: [ - { - _id: relatedRows[1]._id, - primaryDisplay: relatedRows[1].name, - }, - ], + related1: [relatedRows[0]._id!], + related2: [relatedRows[1]._id!], }) - ) - }) - it("can create rows with no relationships", async () => { - const row = await config.api.row.save(table._id!, { - name: "test", + expect(row).toEqual( + expect.objectContaining({ + name: "test", + related1: [ + { + _id: relatedRows[0]._id, + primaryDisplay: relatedRows[0].name, + }, + ], + related2: [ + { + _id: relatedRows[1]._id, + primaryDisplay: relatedRows[1].name, + }, + ], + }) + ) }) - expect(row.related1).toBeUndefined() - expect(row.related2).toBeUndefined() - }) - - it("can create rows with only one relationships field", async () => { - const row = await config.api.row.save(table._id!, { - name: "test", - related1: [], - related2: [relatedRows[1]._id!], - }) - - expect(row).toEqual( - expect.objectContaining({ + it("can create rows with no relationships", async () => { + const row = await config.api.row.save(table._id!, { name: "test", - related2: [ - { - _id: relatedRows[1]._id, - primaryDisplay: relatedRows[1].name, - }, - ], }) - ) - expect(row.related1).toBeUndefined() + + expect(row.related1).toBeUndefined() + expect(row.related2).toBeUndefined() + }) + + it("can create rows with only one relationships field", async () => { + const row = await config.api.row.save(table._id!, { + name: "test", + related1: [], + related2: [relatedRows[1]._id!], + }) + + expect(row).toEqual( + expect.objectContaining({ + name: "test", + related2: [ + { + _id: relatedRows[1]._id, + primaryDisplay: relatedRows[1].name, + }, + ], + }) + ) + expect(row.related1).toBeUndefined() + }) }) - }) }) describe("get", () => { @@ -1140,132 +1141,133 @@ describe.each([ expect(rows).toHaveLength(1) }) - describe("relations to same table", () => { - let relatedRows: Row[] + !isLucene && + describe("relations to same table", () => { + let relatedRows: Row[] - beforeAll(async () => { - const relatedTable = await config.api.table.save( - defaultTable({ - schema: { - name: { name: "name", type: FieldType.STRING }, - }, - }) - ) - const relatedTableId = relatedTable._id! - table = await config.api.table.save( - defaultTable({ - schema: { - name: { name: "name", type: FieldType.STRING }, - related1: { - type: FieldType.LINK, - name: "related1", - fieldName: "main1", - tableId: relatedTableId, - relationshipType: RelationshipType.MANY_TO_MANY, + beforeAll(async () => { + const relatedTable = await config.api.table.save( + defaultTable({ + schema: { + name: { name: "name", type: FieldType.STRING }, }, - related2: { - type: FieldType.LINK, - name: "related2", - fieldName: "main2", - tableId: relatedTableId, - relationshipType: RelationshipType.MANY_TO_MANY, + }) + ) + const relatedTableId = relatedTable._id! + table = await config.api.table.save( + defaultTable({ + schema: { + name: { name: "name", type: FieldType.STRING }, + related1: { + type: FieldType.LINK, + name: "related1", + fieldName: "main1", + tableId: relatedTableId, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + related2: { + type: FieldType.LINK, + name: "related2", + fieldName: "main2", + tableId: relatedTableId, + relationshipType: RelationshipType.MANY_TO_MANY, + }, }, - }, - }) - ) - relatedRows = await Promise.all([ - config.api.row.save(relatedTableId, { name: "foo" }), - config.api.row.save(relatedTableId, { name: "bar" }), - config.api.row.save(relatedTableId, { name: "baz" }), - config.api.row.save(relatedTableId, { name: "boo" }), - ]) - }) - - it("can edit rows with both relationships", async () => { - let row = await config.api.row.save(table._id!, { - name: "test", - related1: [relatedRows[0]._id!], - related2: [relatedRows[1]._id!], + }) + ) + relatedRows = await Promise.all([ + config.api.row.save(relatedTableId, { name: "foo" }), + config.api.row.save(relatedTableId, { name: "bar" }), + config.api.row.save(relatedTableId, { name: "baz" }), + config.api.row.save(relatedTableId, { name: "boo" }), + ]) }) - row = await config.api.row.save(table._id!, { - ...row, - related1: [relatedRows[0]._id!, relatedRows[1]._id!], - related2: [relatedRows[2]._id!], - }) - - expect(row).toEqual( - expect.objectContaining({ + it("can edit rows with both relationships", async () => { + let row = await config.api.row.save(table._id!, { name: "test", - related1: [ - { - _id: relatedRows[1]._id, - primaryDisplay: relatedRows[1].name, - }, - { - _id: relatedRows[0]._id, - primaryDisplay: relatedRows[0].name, - }, - ], - related2: [ - { - _id: relatedRows[2]._id, - primaryDisplay: relatedRows[2].name, - }, - ], + related1: [relatedRows[0]._id!], + related2: [relatedRows[1]._id!], }) - ) - }) - it("can drop existing relationship", async () => { - let row = await config.api.row.save(table._id!, { - name: "test", - related1: [relatedRows[0]._id!], - related2: [relatedRows[1]._id!], + row = await config.api.row.save(table._id!, { + ...row, + related1: [relatedRows[0]._id!, relatedRows[1]._id!], + related2: [relatedRows[2]._id!], + }) + + expect(row).toEqual( + expect.objectContaining({ + name: "test", + related1: [ + { + _id: relatedRows[1]._id, + primaryDisplay: relatedRows[1].name, + }, + { + _id: relatedRows[0]._id, + primaryDisplay: relatedRows[0].name, + }, + ], + related2: [ + { + _id: relatedRows[2]._id, + primaryDisplay: relatedRows[2].name, + }, + ], + }) + ) }) - row = await config.api.row.save(table._id!, { - ...row, - related1: [], - related2: [relatedRows[2]._id!], - }) - - expect(row).toEqual( - expect.objectContaining({ + it("can drop existing relationship", async () => { + let row = await config.api.row.save(table._id!, { name: "test", - related2: [ - { - _id: relatedRows[2]._id, - primaryDisplay: relatedRows[2].name, - }, - ], + related1: [relatedRows[0]._id!], + related2: [relatedRows[1]._id!], }) - ) - expect(row.related1).toBeUndefined() - }) - it("can drop both relationships", async () => { - let row = await config.api.row.save(table._id!, { - name: "test", - related1: [relatedRows[0]._id!], - related2: [relatedRows[1]._id!], + row = await config.api.row.save(table._id!, { + ...row, + related1: [], + related2: [relatedRows[2]._id!], + }) + + expect(row).toEqual( + expect.objectContaining({ + name: "test", + related2: [ + { + _id: relatedRows[2]._id, + primaryDisplay: relatedRows[2].name, + }, + ], + }) + ) + expect(row.related1).toBeUndefined() }) - row = await config.api.row.save(table._id!, { - ...row, - related1: [], - related2: [], - }) - - expect(row).toEqual( - expect.objectContaining({ + it("can drop both relationships", async () => { + let row = await config.api.row.save(table._id!, { name: "test", + related1: [relatedRows[0]._id!], + related2: [relatedRows[1]._id!], }) - ) - expect(row.related1).toBeUndefined() - expect(row.related2).toBeUndefined() + + row = await config.api.row.save(table._id!, { + ...row, + related1: [], + related2: [], + }) + + expect(row).toEqual( + expect.objectContaining({ + name: "test", + }) + ) + expect(row.related1).toBeUndefined() + expect(row.related2).toBeUndefined() + }) }) - }) }) describe("patch", () => { @@ -1543,71 +1545,72 @@ describe.each([ expect(res.length).toEqual(2) }) - describe("relations to same table", () => { - let relatedRows: Row[] + !isLucene && + describe("relations to same table", () => { + let relatedRows: Row[] - beforeAll(async () => { - const relatedTable = await config.api.table.save( - defaultTable({ - schema: { - name: { name: "name", type: FieldType.STRING }, - }, - }) - ) - const relatedTableId = relatedTable._id! - table = await config.api.table.save( - defaultTable({ - schema: { - name: { name: "name", type: FieldType.STRING }, - related1: { - type: FieldType.LINK, - name: "related1", - fieldName: "main1", - tableId: relatedTableId, - relationshipType: RelationshipType.MANY_TO_MANY, + beforeAll(async () => { + const relatedTable = await config.api.table.save( + defaultTable({ + schema: { + name: { name: "name", type: FieldType.STRING }, }, - related2: { - type: FieldType.LINK, - name: "related2", - fieldName: "main2", - tableId: relatedTableId, - relationshipType: RelationshipType.MANY_TO_MANY, + }) + ) + const relatedTableId = relatedTable._id! + table = await config.api.table.save( + defaultTable({ + schema: { + name: { name: "name", type: FieldType.STRING }, + related1: { + type: FieldType.LINK, + name: "related1", + fieldName: "main1", + tableId: relatedTableId, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + related2: { + type: FieldType.LINK, + name: "related2", + fieldName: "main2", + tableId: relatedTableId, + relationshipType: RelationshipType.MANY_TO_MANY, + }, }, - }, - }) - ) - relatedRows = await Promise.all([ - config.api.row.save(relatedTableId, { name: "foo" }), - config.api.row.save(relatedTableId, { name: "bar" }), - config.api.row.save(relatedTableId, { name: "baz" }), - config.api.row.save(relatedTableId, { name: "boo" }), - ]) - }) - - it("can delete rows with both relationships", async () => { - const row = await config.api.row.save(table._id!, { - name: "test", - related1: [relatedRows[0]._id!], - related2: [relatedRows[1]._id!], + }) + ) + relatedRows = await Promise.all([ + config.api.row.save(relatedTableId, { name: "foo" }), + config.api.row.save(relatedTableId, { name: "bar" }), + config.api.row.save(relatedTableId, { name: "baz" }), + config.api.row.save(relatedTableId, { name: "boo" }), + ]) }) - await config.api.row.delete(table._id!, { _id: row._id! }) + it("can delete rows with both relationships", async () => { + const row = await config.api.row.save(table._id!, { + name: "test", + related1: [relatedRows[0]._id!], + related2: [relatedRows[1]._id!], + }) - await config.api.row.get(table._id!, row._id!, { status: 404 }) - }) + await config.api.row.delete(table._id!, { _id: row._id! }) - it("can delete rows with empty relationships", async () => { - const row = await config.api.row.save(table._id!, { - name: "test", - related1: [], - related2: [], + await config.api.row.get(table._id!, row._id!, { status: 404 }) }) - await config.api.row.delete(table._id!, { _id: row._id! }) + it("can delete rows with empty relationships", async () => { + const row = await config.api.row.save(table._id!, { + name: "test", + related1: [], + related2: [], + }) - await config.api.row.get(table._id!, row._id!, { status: 404 }) + await config.api.row.delete(table._id!, { _id: row._id! }) + + await config.api.row.get(table._id!, row._id!, { status: 404 }) + }) }) - }) }) describe("validate", () => { From 8aeb19aabe0058e7ab895528198c582ae4f101d5 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 28 Oct 2024 10:54:00 +0100 Subject: [PATCH 07/21] Fix flakiness --- packages/server/src/api/routes/tests/row.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 19f11a044a..72ade0a69a 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -1199,16 +1199,16 @@ describe.each([ expect(row).toEqual( expect.objectContaining({ name: "test", - related1: [ - { - _id: relatedRows[1]._id, - primaryDisplay: relatedRows[1].name, - }, + related1: expect.arrayContaining([ { _id: relatedRows[0]._id, primaryDisplay: relatedRows[0].name, }, - ], + { + _id: relatedRows[1]._id, + primaryDisplay: relatedRows[1].name, + }, + ]), related2: [ { _id: relatedRows[2]._id, From 95de01c86c8971a745eaf0f2107ed17e5213da30 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 28 Oct 2024 13:17:07 +0100 Subject: [PATCH 08/21] Simplifying lookups --- .../api/controllers/row/ExternalRequest.ts | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index 1fb8ce0423..c5ebe69aea 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -17,6 +17,7 @@ import { PaginationJson, QueryJson, RelationshipFieldMetadata, + RelationshipType, Row, SearchFilters, SortJson, @@ -421,17 +422,30 @@ export class ExternalRequest { return { row: newRow as T, manyRelationships } } + private getLookupRelationsKey(relationship: { + relationshipType: RelationshipType + fieldName: string + through?: string + }) { + if (relationship.relationshipType === RelationshipType.MANY_TO_MANY) { + return `${relationship.through}_${relationship.fieldName}` + } + return relationship.fieldName + } /** * This is a cached lookup, of relationship records, this is mainly for creating/deleting junction * information. */ - async lookupRelations(tableId: string, row: Row) { - const related: { - rows: Row[] - isMany: boolean - tableId: string - field: string - }[] = [] + private async lookupRelations(tableId: string, row: Row) { + const related: Record< + string, + { + rows: Row[] + isMany: boolean + tableId: string + field: string + } + > = {} const { tableName } = breakExternalTableId(tableId) const table = this.tables[tableName] @@ -496,12 +510,12 @@ export class ExternalRequest { ? field.throughFrom || linkPrimaryKey : fieldName - related.push({ + related[this.getLookupRelationsKey(field)] = { rows, isMany: isManyToMany(field), tableId: relatedTableId, field: storeTo, - }) + } } return related } @@ -537,8 +551,13 @@ export class ExternalRequest { const linkSecondary = relationshipPrimary[1] const rows = - related.find(r => r.tableId === relationship.tableId && r.field === key) - ?.rows || [] + related[ + this.getLookupRelationsKey({ + relationshipType: RelationshipType.MANY_TO_MANY, + fieldName: key, + through: relationship.tableId, + }) + ]?.rows || [] const relationshipMatchPredicate = ({ row, @@ -583,7 +602,7 @@ export class ExternalRequest { } } // finally cleanup anything that needs to be removed - for (let { isMany, rows, tableId, field } of related) { + for (let { isMany, rows, tableId, field } of Object.values(related)) { const table: Table | undefined = this.getTable(tableId) // if it's not the foreign key skip it, nothing to do if ( @@ -613,11 +632,7 @@ export class ExternalRequest { continue } - const relatedByTable = isManyToMany(column) - ? related.find( - r => r.tableId === column.through && r.field === column.fieldName - ) - : related.find(r => r.field === column.fieldName) + const relatedByTable = related[this.getLookupRelationsKey(column)] if (!relatedByTable) { continue } From d944e3a8f9c3a73c2b23c75b7a69b191ab955cb2 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 28 Oct 2024 13:53:44 +0100 Subject: [PATCH 09/21] Cleanup --- .../src/api/controllers/row/ExternalRequest.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index c5ebe69aea..49f2f02ebe 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -443,7 +443,6 @@ export class ExternalRequest { rows: Row[] isMany: boolean tableId: string - field: string } > = {} @@ -478,10 +477,7 @@ export class ExternalRequest { "Unable to lookup relationships - undefined column properties." ) } - const { tableName: relatedTableName } = - breakExternalTableId(relatedTableId) - // @ts-ignore - const linkPrimaryKey = this.tables[relatedTableName].primary[0] + if (!lookupField || !row?.[lookupField] == null) { continue } @@ -506,15 +502,11 @@ export class ExternalRequest { !Array.isArray(response) || isKnexEmptyReadResponse(response) ? [] : response - const storeTo = isManyToMany(field) - ? field.throughFrom || linkPrimaryKey - : fieldName related[this.getLookupRelationsKey(field)] = { rows, isMany: isManyToMany(field), tableId: relatedTableId, - field: storeTo, } } return related @@ -602,7 +594,7 @@ export class ExternalRequest { } } // finally cleanup anything that needs to be removed - for (let { isMany, rows, tableId, field } of Object.values(related)) { + for (const [field, { isMany, rows, tableId }] of Object.entries(related)) { const table: Table | undefined = this.getTable(tableId) // if it's not the foreign key skip it, nothing to do if ( From b92427015b1ae0634e56763605a18d286545bc10 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 28 Oct 2024 13:55:08 +0100 Subject: [PATCH 10/21] Cleanup test --- packages/server/src/api/routes/tests/row.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 72ade0a69a..a11f1d758a 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -3076,7 +3076,7 @@ describe.each([ }, ], ["from original saved row", (row: Row) => row], - ["from updated row", (row: Row) => config.api.row.save(viewId, row)], + ["from updated row", (row: Row) => config.api.row.save(viewId, row)], ] it.each(testScenarios)( From 641ceea92e1baf5f1279e5f67b2273cd58849da4 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 28 Oct 2024 13:57:53 +0100 Subject: [PATCH 11/21] Fix checks --- packages/server/src/api/controllers/row/ExternalRequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index 49f2f02ebe..07331bd701 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -478,7 +478,7 @@ export class ExternalRequest { ) } - if (!lookupField || !row?.[lookupField] == null) { + if (!lookupField || !row?.[lookupField]) { continue } const endpoint = getEndpoint(relatedTableId, Operation.READ) From d36b810efcbbd6fe1c1df253d6859f8c1ee26fb3 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 28 Oct 2024 14:28:53 +0100 Subject: [PATCH 12/21] Fix flakiness --- .../server/src/api/controllers/row/ExternalRequest.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index 07331bd701..b884683197 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -54,12 +54,13 @@ import { makeExternalQuery } from "../../../integrations/base/query" import { dataFilters, helpers } from "@budibase/shared-core" import { isRelationshipColumn } from "../../../db/utils" -export interface ManyRelationship { +interface ManyRelationship { tableId?: string id?: string isUpdate?: boolean key: string [key: string]: any + relationshipType: RelationshipType } export interface RunConfig { @@ -386,6 +387,7 @@ export class ExternalRequest { [otherKey]: breakRowIdField(relationship)[0], // leave the ID for enrichment later [thisKey]: `{{ literal ${tablePrimary} }}`, + relationshipType: RelationshipType.MANY_TO_MANY, }) } } @@ -402,6 +404,7 @@ export class ExternalRequest { [thisKey]: breakRowIdField(relationship)[0], // leave the ID for enrichment later [otherKey]: `{{ literal ${tablePrimary} }}`, + relationshipType: RelationshipType.MANY_TO_ONE, }) } } @@ -531,7 +534,8 @@ export class ExternalRequest { const promises = [] const related = await this.lookupRelations(mainTableId, row) for (let relationship of relationships) { - const { key, tableId, isUpdate, id, ...rest } = relationship + const { key, tableId, isUpdate, id, relationshipType, ...rest } = + relationship const body: { [key: string]: any } = processObjectSync(rest, row, {}) const linkTable = this.getTable(tableId) const relationshipPrimary = linkTable?.primary || [] @@ -545,7 +549,7 @@ export class ExternalRequest { const rows = related[ this.getLookupRelationsKey({ - relationshipType: RelationshipType.MANY_TO_MANY, + relationshipType, fieldName: key, through: relationship.tableId, }) From 184df942791d9aa0dac781353f97788a1945793e Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Mon, 28 Oct 2024 13:35:25 +0000 Subject: [PATCH 13/21] Bump version to 2.33.3 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index df50828b30..8235ee9b60 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "2.33.2", + "version": "2.33.3", "npmClient": "yarn", "packages": [ "packages/*", From 7dcdce24807c439102c1c6f270f2790ff2990a48 Mon Sep 17 00:00:00 2001 From: melohagan <101575380+melohagan@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:46:42 +0000 Subject: [PATCH 14/21] Gro 738 refactor code that returns the account holder from user (#14872) * WIP * WIP * Remove tests * Remove TenantInfo * Remove unused export * Remove TenantInfo type * Remove unused export * Remove unused routes * Add getAccountHolder front-end endpoint * Add endpoint to no tenancy list * Get account holder via appId * lint * Update pro ref * Use account email instead of budibaseUserId (#14876) * Update account-portal ref * Update account portal ref * Correct import order * Simplify boolean * type fix * Rename endpoint to accountholder * Update account-portal ref * Refactor * Refactor to not use appId * Update type * appId not needed * Unused import --- packages/account-portal | 2 +- .../backend-core/src/events/identification.ts | 13 ++-- packages/backend-core/src/tenancy/db.ts | 23 ------ packages/backend-core/src/users/db.ts | 21 ++--- packages/backend-core/src/users/users.ts | 11 +++ packages/backend-core/src/users/utils.ts | 30 +++----- .../portal/users/users/[userId].svelte | 2 +- .../builder/portal/users/users/index.svelte | 9 ++- packages/builder/src/stores/portal/users.js | 11 ++- packages/frontend-core/src/api/user.js | 10 +-- packages/types/src/api/web/user.ts | 5 +- packages/types/src/documents/global/index.ts | 1 - .../types/src/documents/global/tenantInfo.ts | 15 ---- packages/types/src/documents/global/user.ts | 5 ++ .../src/api/controllers/global/tenant.ts | 14 ---- .../src/api/controllers/global/users.ts | 34 +++++++-- packages/worker/src/api/index.ts | 10 +-- .../worker/src/api/routes/global/tenant.ts | 11 --- .../api/routes/global/tests/tenant.spec.ts | 48 ------------ .../src/api/routes/global/tests/users.spec.ts | 76 +++---------------- .../worker/src/api/routes/global/users.ts | 1 + packages/worker/src/api/routes/index.ts | 2 - .../worker/src/api/routes/validation/users.ts | 9 ++- packages/worker/src/tests/api/tenants.ts | 9 --- packages/worker/src/tests/api/users.ts | 10 ++- 25 files changed, 126 insertions(+), 256 deletions(-) delete mode 100644 packages/types/src/documents/global/tenantInfo.ts delete mode 100644 packages/worker/src/api/controllers/global/tenant.ts delete mode 100644 packages/worker/src/api/routes/global/tenant.ts delete mode 100644 packages/worker/src/api/routes/global/tests/tenant.spec.ts diff --git a/packages/account-portal b/packages/account-portal index 8cd052ce82..607e3db866 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit 8cd052ce8288f343812a514d06c5a9459b3ba1a8 +Subproject commit 607e3db866c366cb609b0015a1293edbeb703237 diff --git a/packages/backend-core/src/events/identification.ts b/packages/backend-core/src/events/identification.ts index c7bc1c817b..69bf7009b2 100644 --- a/packages/backend-core/src/events/identification.ts +++ b/packages/backend-core/src/events/identification.ts @@ -171,9 +171,9 @@ const identifyUser = async ( if (isSSOUser(user)) { providerType = user.providerType } - const accountHolder = account?.budibaseUserId === user._id || false - const verified = - account && account?.budibaseUserId === user._id ? account.verified : false + const accountHolder = await users.getExistingAccounts([user.email]) + const isAccountHolder = accountHolder.length > 0 + const verified = !!account && isAccountHolder && account.verified const installationId = await getInstallationId() const hosting = account ? account.hosting : getHostingFromEnv() const environment = getDeploymentEnvironment() @@ -185,7 +185,7 @@ const identifyUser = async ( installationId, tenantId, verified, - accountHolder, + accountHolder: isAccountHolder, providerType, builder, admin, @@ -207,9 +207,10 @@ const identifyAccount = async (account: Account) => { const environment = getDeploymentEnvironment() if (isCloudAccount(account)) { - if (account.budibaseUserId) { + const user = await users.getGlobalUserByEmail(account.email) + if (user?._id) { // use the budibase user as the id if set - id = account.budibaseUserId + id = user._id } } diff --git a/packages/backend-core/src/tenancy/db.ts b/packages/backend-core/src/tenancy/db.ts index 332ecbca48..10477a8579 100644 --- a/packages/backend-core/src/tenancy/db.ts +++ b/packages/backend-core/src/tenancy/db.ts @@ -1,29 +1,6 @@ import { getDB } from "../db/db" import { getGlobalDBName } from "../context" -import { TenantInfo } from "@budibase/types" export function getTenantDB(tenantId: string) { return getDB(getGlobalDBName(tenantId)) } - -export async function saveTenantInfo(tenantInfo: TenantInfo) { - const db = getTenantDB(tenantInfo.tenantId) - // save the tenant info to db - return db.put({ - _id: "tenant_info", - ...tenantInfo, - }) -} - -export async function getTenantInfo( - tenantId: string -): Promise { - try { - const db = getTenantDB(tenantId) - const tenantInfo = (await db.get("tenant_info")) as TenantInfo - delete tenantInfo.owner.password - return tenantInfo - } catch { - return undefined - } -} diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index c96c615f4b..cbc0019303 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -16,14 +16,15 @@ import { isSSOUser, SaveUserOpts, User, - UserStatus, UserGroup, + UserIdentifier, + UserStatus, PlatformUserBySsoId, PlatformUserById, AnyDocument, } from "@budibase/types" import { - getAccountHolderFromUserIds, + getAccountHolderFromUsers, isAdmin, isCreator, validateUniqueUser, @@ -412,7 +413,9 @@ export class UserDB { ) } - static async bulkDelete(userIds: string[]): Promise { + static async bulkDelete( + users: Array + ): Promise { const db = getGlobalDB() const response: BulkUserDeleted = { @@ -421,13 +424,13 @@ export class UserDB { } // remove the account holder from the delete request if present - const account = await getAccountHolderFromUserIds(userIds) - if (account) { - userIds = userIds.filter(u => u !== account.budibaseUserId) + const accountHolder = await getAccountHolderFromUsers(users) + if (accountHolder) { + users = users.filter(u => u.userId !== accountHolder.userId) // mark user as unsuccessful response.unsuccessful.push({ - _id: account.budibaseUserId, - email: account.email, + _id: accountHolder.userId, + email: accountHolder.email, reason: "Account holder cannot be deleted", }) } @@ -435,7 +438,7 @@ export class UserDB { // Get users and delete const allDocsResponse = await db.allDocs({ include_docs: true, - keys: userIds, + keys: users.map(u => u.userId), }) const usersToDelete = allDocsResponse.rows.map(user => { return user.doc! diff --git a/packages/backend-core/src/users/users.ts b/packages/backend-core/src/users/users.ts index f4838597b6..0bff428fa9 100644 --- a/packages/backend-core/src/users/users.ts +++ b/packages/backend-core/src/users/users.ts @@ -70,6 +70,17 @@ export async function getAllUserIds() { return response.rows.map(row => row.id) } +export async function getAllUsers(): Promise { + const db = getGlobalDB() + const startKey = `${DocumentType.USER}${SEPARATOR}` + const response = await db.allDocs({ + startkey: startKey, + endkey: `${startKey}${UNICODE_MAX}`, + include_docs: true, + }) + return response.rows.map(row => row.doc) as User[] +} + export async function bulkUpdateGlobalUsers(users: User[]) { const db = getGlobalDB() return (await db.bulkDocs(users)) as BulkDocsResponse diff --git a/packages/backend-core/src/users/utils.ts b/packages/backend-core/src/users/utils.ts index e1e3da181d..91b667ce17 100644 --- a/packages/backend-core/src/users/utils.ts +++ b/packages/backend-core/src/users/utils.ts @@ -1,11 +1,9 @@ -import { CloudAccount, ContextUser, User, UserGroup } from "@budibase/types" +import { ContextUser, User, UserGroup, UserIdentifier } from "@budibase/types" import * as accountSdk from "../accounts" import env from "../environment" -import { getFirstPlatformUser } from "./lookup" +import { getExistingAccounts, getFirstPlatformUser } from "./lookup" import { EmailUnavailableError } from "../errors" -import { getTenantId } from "../context" import { sdk } from "@budibase/shared-core" -import { getAccountByTenantId } from "../accounts" import { BUILTIN_ROLE_IDS } from "../security/roles" import * as context from "../context" @@ -67,21 +65,17 @@ export async function validateUniqueUser(email: string, tenantId: string) { } /** - * For the given user id's, return the account holder if it is in the ids. + * For a list of users, return the account holder if there is an email match. */ -export async function getAccountHolderFromUserIds( - userIds: string[] -): Promise { +export async function getAccountHolderFromUsers( + users: Array +): Promise { if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { - const tenantId = getTenantId() - const account = await getAccountByTenantId(tenantId) - if (!account) { - throw new Error(`Account not found for tenantId=${tenantId}`) - } - - const budibaseUserId = account.budibaseUserId - if (userIds.includes(budibaseUserId)) { - return account - } + const accountMetadata = await getExistingAccounts( + users.map(user => user.email) + ) + return users.find(user => + accountMetadata.map(metadata => metadata.email).includes(user.email) + ) } } diff --git a/packages/builder/src/pages/builder/portal/users/users/[userId].svelte b/packages/builder/src/pages/builder/portal/users/users/[userId].svelte index 458c9a3f79..3547a4876e 100644 --- a/packages/builder/src/pages/builder/portal/users/users/[userId].svelte +++ b/packages/builder/src/pages/builder/portal/users/users/[userId].svelte @@ -206,7 +206,7 @@ if (!user?._id) { $goto("./") } - tenantOwner = await users.tenantOwner($auth.tenantId) + tenantOwner = await users.getAccountHolder() } async function toggleFlags(detail) { diff --git a/packages/builder/src/pages/builder/portal/users/users/index.svelte b/packages/builder/src/pages/builder/portal/users/users/index.svelte index a1d5496ff6..5a7e334c9c 100644 --- a/packages/builder/src/pages/builder/portal/users/users/index.svelte +++ b/packages/builder/src/pages/builder/portal/users/users/index.svelte @@ -280,7 +280,12 @@ } if (ids.length > 0) { - await users.bulkDelete(ids) + await users.bulkDelete( + selectedRows.map(user => ({ + userId: user._id, + email: user.email, + })) + ) } if (selectedInvites.length > 0) { @@ -319,7 +324,7 @@ groupsLoaded = true pendingInvites = await users.getInvites() invitesLoaded = true - tenantOwner = await users.tenantOwner($auth.tenantId) + tenantOwner = await users.getAccountHolder() tenantOwnerLoaded = true } catch (error) { notifications.error("Error fetching user group data") diff --git a/packages/builder/src/stores/portal/users.js b/packages/builder/src/stores/portal/users.js index e9e65c3803..747dc5144d 100644 --- a/packages/builder/src/stores/portal/users.js +++ b/packages/builder/src/stores/portal/users.js @@ -112,8 +112,8 @@ export function createUsersStore() { return await API.getUserCountByApp({ appId }) } - async function bulkDelete(userIds) { - return API.deleteUsers(userIds) + async function bulkDelete(users) { + return API.deleteUsers(users) } async function save(user) { @@ -128,9 +128,8 @@ export function createUsersStore() { return await API.removeAppBuilder({ userId, appId }) } - async function getTenantOwner(tenantId) { - const tenantInfo = await API.getTenantInfo({ tenantId }) - return tenantInfo?.owner + async function getAccountHolder() { + return await API.getAccountHolder() } const getUserRole = user => { @@ -176,7 +175,7 @@ export function createUsersStore() { save: refreshUsage(save), bulkDelete: refreshUsage(bulkDelete), delete: refreshUsage(del), - tenantOwner: getTenantOwner, + getAccountHolder, } } diff --git a/packages/frontend-core/src/api/user.js b/packages/frontend-core/src/api/user.js index a2846c1e7b..45d481183a 100644 --- a/packages/frontend-core/src/api/user.js +++ b/packages/frontend-core/src/api/user.js @@ -122,14 +122,14 @@ export const buildUserEndpoints = API => ({ /** * Deletes multiple users - * @param userIds the ID of the user to delete + * @param users the ID/email pair of the user to delete */ - deleteUsers: async userIds => { + deleteUsers: async users => { const res = await API.post({ url: `/api/global/users/bulk`, body: { delete: { - userIds, + users, }, }, }) @@ -296,9 +296,9 @@ export const buildUserEndpoints = API => ({ }) }, - getTenantInfo: async ({ tenantId }) => { + getAccountHolder: async () => { return await API.get({ - url: `/api/global/tenant/${tenantId}`, + url: `/api/global/users/accountholder`, }) }, }) diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index 471ca86616..e102f136c3 100644 --- a/packages/types/src/api/web/user.ts +++ b/packages/types/src/api/web/user.ts @@ -15,7 +15,10 @@ export interface UserDetails { export interface BulkUserRequest { delete?: { - userIds: string[] + users: Array<{ + userId: string + email: string + }> } create?: { roles?: any[] diff --git a/packages/types/src/documents/global/index.ts b/packages/types/src/documents/global/index.ts index 6784f2638c..b728439dd6 100644 --- a/packages/types/src/documents/global/index.ts +++ b/packages/types/src/documents/global/index.ts @@ -7,4 +7,3 @@ export * from "./schedule" export * from "./templates" export * from "./environmentVariables" export * from "./auditLogs" -export * from "./tenantInfo" diff --git a/packages/types/src/documents/global/tenantInfo.ts b/packages/types/src/documents/global/tenantInfo.ts deleted file mode 100644 index 4c8837cf2a..0000000000 --- a/packages/types/src/documents/global/tenantInfo.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Hosting } from "../../sdk" -import { Document } from "../document" - -export interface TenantInfo extends Document { - owner: { - email: string - password?: string - ssoId?: string - givenName?: string - familyName?: string - budibaseUserId?: string - } - tenantId: string - hosting: Hosting -} diff --git a/packages/types/src/documents/global/user.ts b/packages/types/src/documents/global/user.ts index 829a171843..1b242886b5 100644 --- a/packages/types/src/documents/global/user.ts +++ b/packages/types/src/documents/global/user.ts @@ -38,6 +38,11 @@ export function isSSOUser(user: User): user is SSOUser { // USER +export interface UserIdentifier { + userId: string + email: string +} + export interface User extends Document { tenantId: string email: string diff --git a/packages/worker/src/api/controllers/global/tenant.ts b/packages/worker/src/api/controllers/global/tenant.ts deleted file mode 100644 index 8b5ae6d528..0000000000 --- a/packages/worker/src/api/controllers/global/tenant.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { tenancy } from "@budibase/backend-core" -import { TenantInfo, Ctx } from "@budibase/types" - -export const save = async (ctx: Ctx) => { - const response = await tenancy.saveTenantInfo(ctx.request.body) - ctx.body = { - _id: response.id, - _rev: response.rev, - } -} - -export const get = async (ctx: Ctx) => { - ctx.body = await tenancy.getTenantInfo(ctx.params.id) -} diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 921e0324d1..52f8821fab 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -23,9 +23,11 @@ import { SearchUsersRequest, User, UserCtx, + UserIdentifier, } from "@budibase/types" import { accounts, + users, cache, ErrorCode, events, @@ -55,8 +57,8 @@ export const save = async (ctx: UserCtx) => { const requestUser = ctx.request.body // Do not allow the account holder role to be changed - const tenantInfo = await tenancy.getTenantInfo(requestUser.tenantId) - if (tenantInfo?.owner.email === requestUser.email) { + const accountMetadata = await users.getExistingAccounts([requestUser.email]) + if (accountMetadata?.length > 0) { if ( requestUser.admin?.global !== true || requestUser.builder?.global !== true @@ -103,11 +105,14 @@ export const addSsoSupport = async (ctx: Ctx) => { } } -const bulkDelete = async (userIds: string[], currentUserId: string) => { - if (userIds?.indexOf(currentUserId) !== -1) { +const bulkDelete = async ( + users: Array, + currentUserId: string +) => { + if (users.find(u => u.userId === currentUserId)) { throw new Error("Unable to delete self.") } - return await userSdk.db.bulkDelete(userIds) + return await userSdk.db.bulkDelete(users) } const bulkCreate = async (users: User[], groupIds: string[]) => { @@ -130,7 +135,7 @@ export const bulkUpdate = async ( created = await bulkCreate(input.create.users, input.create.groups) } if (input.delete) { - deleted = await bulkDelete(input.delete.userIds, currentUserId) + deleted = await bulkDelete(input.delete.users, currentUserId) } } catch (err: any) { ctx.throw(err.status || 400, err?.message || err) @@ -302,6 +307,23 @@ export const tenantUserLookup = async (ctx: any) => { } } +/** + * This will be paginated to a default of the first 50 users, + * So the account holder may not be found until further pagination has occurred + */ +export const accountHolderLookup = async (ctx: Ctx) => { + const users = await userSdk.core.getAllUsers() + const response = await userSdk.core.getExistingAccounts( + users.map(u => u.email) + ) + const holder = response[0] + if (!holder) { + return + } + holder._id = users.find(u => u.email === holder.email)?._id + ctx.body = holder +} + /* Encapsulate the app user onboarding flows here. */ diff --git a/packages/worker/src/api/index.ts b/packages/worker/src/api/index.ts index db0a80acfd..a7594b713e 100644 --- a/packages/worker/src/api/index.ts +++ b/packages/worker/src/api/index.ts @@ -71,10 +71,6 @@ const PUBLIC_ENDPOINTS = [ route: "/api/global/users/invite", method: "GET", }, - { - route: "/api/global/tenant", - method: "POST", - }, ] const NO_TENANCY_ENDPOINTS = [ @@ -121,11 +117,7 @@ const NO_TENANCY_ENDPOINTS = [ method: "GET", }, { - route: "/api/global/tenant", - method: "POST", - }, - { - route: "/api/global/tenant/:id", + route: "/api/global/users/accountholder", method: "GET", }, ] diff --git a/packages/worker/src/api/routes/global/tenant.ts b/packages/worker/src/api/routes/global/tenant.ts deleted file mode 100644 index f15e374774..0000000000 --- a/packages/worker/src/api/routes/global/tenant.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Router from "@koa/router" -import * as controller from "../../controllers/global/tenant" -import cloudRestricted from "../../../middleware/cloudRestricted" - -const router: Router = new Router() - -router - .post("/api/global/tenant", cloudRestricted, controller.save) - .get("/api/global/tenant/:id", controller.get) - -export default router diff --git a/packages/worker/src/api/routes/global/tests/tenant.spec.ts b/packages/worker/src/api/routes/global/tests/tenant.spec.ts deleted file mode 100644 index 390cfa2af5..0000000000 --- a/packages/worker/src/api/routes/global/tests/tenant.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Hosting, TenantInfo } from "@budibase/types" -import { TestConfiguration } from "../../../../tests" -import { tenancy as _tenancy } from "@budibase/backend-core" - -const tenancy = jest.mocked(_tenancy) - -describe("/api/global/tenant", () => { - const config = new TestConfiguration() - - beforeAll(async () => { - await config.beforeAll() - }) - - afterAll(async () => { - await config.afterAll() - }) - - beforeEach(() => { - jest.clearAllMocks() - }) - - describe("POST /api/global/tenant", () => { - it("should save the tenantInfo", async () => { - tenancy.saveTenantInfo = jest.fn().mockImplementation(async () => ({ - id: "DOC_ID", - ok: true, - rev: "DOC_REV", - })) - const tenantInfo: TenantInfo = { - owner: { - email: "test@example.com", - password: "PASSWORD123!", - ssoId: "SSO_ID", - givenName: "Jane", - familyName: "Doe", - budibaseUserId: "USER_ID", - }, - tenantId: "tenant123", - hosting: Hosting.CLOUD, - } - const response = await config.api.tenants.saveTenantInfo(tenantInfo) - - expect(_tenancy.saveTenantInfo).toHaveBeenCalledTimes(1) - expect(_tenancy.saveTenantInfo).toHaveBeenCalledWith(tenantInfo) - expect(response.text).toEqual('{"_id":"DOC_ID","_rev":"DOC_REV"}') - }) - }) -}) diff --git a/packages/worker/src/api/routes/global/tests/users.spec.ts b/packages/worker/src/api/routes/global/tests/users.spec.ts index c8e71f7eb4..b6237c7b4b 100644 --- a/packages/worker/src/api/routes/global/tests/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/users.spec.ts @@ -412,28 +412,6 @@ describe("/api/global/users", () => { expect(events.user.permissionBuilderRemoved).toHaveBeenCalledTimes(1) }) - it("should not be able to update an account holder user to a basic user", async () => { - const accountHolderUser = await config.createUser( - structures.users.adminUser() - ) - jest.clearAllMocks() - tenancy.getTenantInfo = jest.fn().mockImplementation(() => ({ - owner: { - email: accountHolderUser.email, - }, - })) - - accountHolderUser.admin!.global = false - accountHolderUser.builder!.global = false - - await config.api.users.saveUser(accountHolderUser, 400) - - expect(events.user.created).not.toHaveBeenCalled() - expect(events.user.updated).not.toHaveBeenCalled() - expect(events.user.permissionAdminRemoved).not.toHaveBeenCalled() - expect(events.user.permissionBuilderRemoved).not.toHaveBeenCalled() - }) - it("should be able to update an builder user to a basic user", async () => { const user = await config.createUser(structures.users.builderUser()) jest.clearAllMocks() @@ -592,55 +570,21 @@ describe("/api/global/users", () => { describe("POST /api/global/users/bulk (delete)", () => { it("should not be able to bulk delete current user", async () => { - const user = await config.user! + const user = config.user! - const response = await config.api.users.bulkDeleteUsers([user._id!], 400) + const response = await config.api.users.bulkDeleteUsers( + [ + { + userId: user._id!, + email: "test@example.com", + }, + ], + 400 + ) expect(response.message).toBe("Unable to delete self.") expect(events.user.deleted).not.toHaveBeenCalled() }) - - it("should not be able to bulk delete account owner", async () => { - const user = await config.createUser() - const account = structures.accounts.cloudAccount() - account.budibaseUserId = user._id! - accounts.getAccountByTenantId.mockReturnValue(Promise.resolve(account)) - - const response = await config.api.users.bulkDeleteUsers([user._id!]) - - expect(response.deleted?.successful.length).toBe(0) - expect(response.deleted?.unsuccessful.length).toBe(1) - expect(response.deleted?.unsuccessful[0].reason).toBe( - "Account holder cannot be deleted" - ) - expect(response.deleted?.unsuccessful[0]._id).toBe(user._id) - expect(events.user.deleted).not.toHaveBeenCalled() - }) - - it("should be able to bulk delete users", async () => { - const account = structures.accounts.cloudAccount() - accounts.getAccountByTenantId.mockReturnValue(Promise.resolve(account)) - - const builder = structures.users.builderUser() - const admin = structures.users.adminUser() - const user = structures.users.user() - const createdUsers = await config.api.users.bulkCreateUsers([ - builder, - admin, - user, - ]) - - const toDelete = createdUsers.created?.successful.map( - u => u._id! - ) as string[] - const response = await config.api.users.bulkDeleteUsers(toDelete) - - expect(response.deleted?.successful.length).toBe(3) - expect(response.deleted?.unsuccessful.length).toBe(0) - expect(events.user.deleted).toHaveBeenCalledTimes(3) - expect(events.user.permissionAdminRemoved).toHaveBeenCalledTimes(1) - expect(events.user.permissionBuilderRemoved).toHaveBeenCalledTimes(2) - }) }) describe("POST /api/global/users/search", () => { diff --git a/packages/worker/src/api/routes/global/users.ts b/packages/worker/src/api/routes/global/users.ts index d5dfa47923..4078f348ad 100644 --- a/packages/worker/src/api/routes/global/users.ts +++ b/packages/worker/src/api/routes/global/users.ts @@ -136,6 +136,7 @@ router buildAdminInitValidation(), controller.adminUser ) + .get("/api/global/users/accountholder", controller.accountHolderLookup) .get("/api/global/users/tenant/:id", controller.tenantUserLookup) // global endpoint but needs to come at end (blocks other endpoints otherwise) .get("/api/global/users/:id", auth.builderOrAdmin, controller.find) diff --git a/packages/worker/src/api/routes/index.ts b/packages/worker/src/api/routes/index.ts index 2eb4b5cd5d..e6cacf110f 100644 --- a/packages/worker/src/api/routes/index.ts +++ b/packages/worker/src/api/routes/index.ts @@ -1,7 +1,6 @@ import Router from "@koa/router" import { api as pro } from "@budibase/pro" import userRoutes from "./global/users" -import tenantRoutes from "./global/tenant" import configRoutes from "./global/configs" import workspaceRoutes from "./global/workspaces" import templateRoutes from "./global/templates" @@ -41,7 +40,6 @@ export const routes: Router[] = [ accountRoutes, restoreRoutes, eventRoutes, - tenantRoutes, pro.scim, ] diff --git a/packages/worker/src/api/routes/validation/users.ts b/packages/worker/src/api/routes/validation/users.ts index 333435ed29..46c66285fd 100644 --- a/packages/worker/src/api/routes/validation/users.ts +++ b/packages/worker/src/api/routes/validation/users.ts @@ -66,7 +66,14 @@ export const buildUserBulkUserValidation = (isSelf = false) => { users: Joi.array().items(Joi.object(schema).required().unknown(true)), }), delete: Joi.object({ - userIds: Joi.array().items(Joi.string()), + users: Joi.array().items( + Joi.object({ + email: Joi.string(), + userId: Joi.string(), + }) + .required() + .unknown(true) + ), }), } diff --git a/packages/worker/src/tests/api/tenants.ts b/packages/worker/src/tests/api/tenants.ts index c404b8ad58..16f970915a 100644 --- a/packages/worker/src/tests/api/tenants.ts +++ b/packages/worker/src/tests/api/tenants.ts @@ -1,4 +1,3 @@ -import { TenantInfo } from "@budibase/types" import TestConfiguration from "../TestConfiguration" import { TestAPI, TestAPIOpts } from "./base" @@ -15,12 +14,4 @@ export class TenantAPI extends TestAPI { .set(opts?.headers) .expect(opts?.status ? opts.status : 204) } - - saveTenantInfo = (tenantInfo: TenantInfo) => { - return this.request - .post("/api/global/tenant") - .set(this.config.internalAPIHeaders()) - .send(tenantInfo) - .expect(200) - } } diff --git a/packages/worker/src/tests/api/users.ts b/packages/worker/src/tests/api/users.ts index 8ff31f04fc..b54fb45d2c 100644 --- a/packages/worker/src/tests/api/users.ts +++ b/packages/worker/src/tests/api/users.ts @@ -81,8 +81,14 @@ export class UserAPI extends TestAPI { return res.body as BulkUserResponse } - bulkDeleteUsers = async (userIds: string[], status?: number) => { - const body: BulkUserRequest = { delete: { userIds } } + bulkDeleteUsers = async ( + users: Array<{ + userId: string + email: string + }>, + status?: number + ) => { + const body: BulkUserRequest = { delete: { users } } const res = await this.request .post(`/api/global/users/bulk`) .send(body) From 3768cc462ab5ec61f33820735dd9bb8249858d0e Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Mon, 28 Oct 2024 14:55:37 +0000 Subject: [PATCH 15/21] Bump version to 2.33.4 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 8235ee9b60..530c51d20b 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "2.33.3", + "version": "2.33.4", "npmClient": "yarn", "packages": [ "packages/*", From 40a0167c8929a0d828ea8adf6a6562b87f1f87a4 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 28 Oct 2024 16:34:39 +0100 Subject: [PATCH 16/21] Release feature/automation-branching-ux to QA --- .github/workflows/deploy-qa.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-qa.yml b/.github/workflows/deploy-qa.yml index 2a30e44def..92c957acbd 100644 --- a/.github/workflows/deploy-qa.yml +++ b/.github/workflows/deploy-qa.yml @@ -3,7 +3,7 @@ name: Deploy QA on: push: branches: - - v3-ui + - feature/automation-branching-ux workflow_dispatch: jobs: From a7fa7368d9c223400cea9f67bd6645ea428739e3 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 29 Oct 2024 10:38:58 +0100 Subject: [PATCH 17/21] Reset the page every time that a filter gets updated --- .../app/[application]/settings/automations/index.svelte | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/settings/automations/index.svelte b/packages/builder/src/pages/builder/app/[application]/settings/automations/index.svelte index 6518b6cf58..02b063bca8 100644 --- a/packages/builder/src/pages/builder/app/[application]/settings/automations/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/settings/automations/index.svelte @@ -38,8 +38,11 @@ let loaded = false $: app = $appsStore.apps.find(app => $appStore.appId?.includes(app.appId)) $: licensePlan = $auth.user?.license?.plan - $: page = $pageInfo.page - $: fetchLogs(automationId, status, page, timeRange) + + // Reset the page every time that a filter gets updated + $: pageInfo.reset(), automationId, status, timeRange + + $: fetchLogs(automationId, status, $pageInfo.page, timeRange) $: isCloud = $admin.cloud $: chainAutomations = app?.automations?.chainAutomations ?? !isCloud const timeOptions = [ From 70cd255ef02dd2a2eea62d24c5b2ee7417c2e57c Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 29 Oct 2024 10:39:23 +0100 Subject: [PATCH 18/21] Update pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index f6aebba944..ee0de4f489 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit f6aebba94451ce47bba551926e5ad72bd75f71c6 +Subproject commit ee0de4f489e8530d98a4ed2c3f0f66034c40bc33 From 4a91b86278628a5d4ac94b448675c32bf9d60ff9 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 29 Oct 2024 11:55:49 +0100 Subject: [PATCH 19/21] Extract values from store --- .../app/[application]/settings/automations/index.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/builder/src/pages/builder/app/[application]/settings/automations/index.svelte b/packages/builder/src/pages/builder/app/[application]/settings/automations/index.svelte index 02b063bca8..dd332c92a9 100644 --- a/packages/builder/src/pages/builder/app/[application]/settings/automations/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/settings/automations/index.svelte @@ -42,7 +42,8 @@ // Reset the page every time that a filter gets updated $: pageInfo.reset(), automationId, status, timeRange - $: fetchLogs(automationId, status, $pageInfo.page, timeRange) + $: page = $pageInfo.page + $: fetchLogs(automationId, status, page, timeRange) $: isCloud = $admin.cloud $: chainAutomations = app?.automations?.chainAutomations ?? !isCloud const timeOptions = [ From bbab4d35c3b208d9fc250918403da78cc69bc0c4 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 29 Oct 2024 11:55:56 +0100 Subject: [PATCH 20/21] Update pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index ee0de4f489..a3a06b0aaf 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit ee0de4f489e8530d98a4ed2c3f0f66034c40bc33 +Subproject commit a3a06b0aaff47a341c877fe270ee5b524b99910c From ffdd46ef1d51e7fec0c9b48817574acd95fc4ee0 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 29 Oct 2024 12:00:54 +0100 Subject: [PATCH 21/21] Update pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index a3a06b0aaf..2ab8536b60 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit a3a06b0aaff47a341c877fe270ee5b524b99910c +Subproject commit 2ab8536b6005576684810d774f1ac22239218546