From 8b40e8075017a608d8ef6cf96b824ec23d72d5f9 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 10 Jul 2024 13:26:02 +0200 Subject: [PATCH 01/13] Return 401 instead of 403 --- packages/server/src/middleware/authorized.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/middleware/authorized.ts b/packages/server/src/middleware/authorized.ts index ec8a3711cf..b23a9846b7 100644 --- a/packages/server/src/middleware/authorized.ts +++ b/packages/server/src/middleware/authorized.ts @@ -96,7 +96,7 @@ const authorized = } if (!ctx.user) { - return ctx.throw(403, "No user info found") + return ctx.throw(401, "No user info found") } // get the resource roles @@ -148,7 +148,7 @@ const authorized = // check authenticated if (!ctx.isAuthenticated) { - return ctx.throw(403, "Session not authenticated") + return ctx.throw(401, "Session not authenticated") } // check general builder stuff, this middleware is a good way From 3f5161aaf7fbf2508f5ad195dc870151760906c1 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 10 Jul 2024 13:36:07 +0200 Subject: [PATCH 02/13] Fix tests --- packages/server/src/api/routes/tests/permissions.spec.ts | 8 ++++---- packages/server/src/api/routes/tests/viewV2.spec.ts | 4 ++-- packages/server/src/middleware/tests/authorized.spec.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/server/src/api/routes/tests/permissions.spec.ts b/packages/server/src/api/routes/tests/permissions.spec.ts index bee794da47..838e1aca0b 100644 --- a/packages/server/src/api/routes/tests/permissions.spec.ts +++ b/packages/server/src/api/routes/tests/permissions.spec.ts @@ -203,7 +203,7 @@ describe("/permission", () => { // replicate changes before checking permissions await config.publish() - await config.api.viewV2.publicSearch(view.id, undefined, { status: 403 }) + await config.api.viewV2.publicSearch(view.id, undefined, { status: 401 }) }) it("should ignore the view permissions if the flag is not on", async () => { @@ -221,7 +221,7 @@ describe("/permission", () => { await config.publish() await config.api.viewV2.publicSearch(view.id, undefined, { - status: 403, + status: 401, }) }) @@ -250,8 +250,8 @@ describe("/permission", () => { .send(basicRow(table._id)) .set(config.publicHeaders()) .expect("Content-Type", /json/) - .expect(403) - expect(res.status).toEqual(403) + .expect(401) + expect(res.status).toEqual(401) }) }) diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index ba044acf81..e9853e5dff 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -1490,7 +1490,7 @@ describe.each([ it("does not allow public users to fetch by default", async () => { await config.publish() await config.api.viewV2.publicSearch(view.id, undefined, { - status: 403, + status: 401, }) }) @@ -1534,7 +1534,7 @@ describe.each([ await config.publish() await config.api.viewV2.publicSearch(view.id, undefined, { - status: 403, + status: 401, }) }) }) diff --git a/packages/server/src/middleware/tests/authorized.spec.ts b/packages/server/src/middleware/tests/authorized.spec.ts index 79cfeca54e..e8fe8bd914 100644 --- a/packages/server/src/middleware/tests/authorized.spec.ts +++ b/packages/server/src/middleware/tests/authorized.spec.ts @@ -105,7 +105,7 @@ describe("Authorization middleware", () => { it("throws when no user data is present in context", async () => { await config.executeMiddleware() - expect(config.throw).toHaveBeenCalledWith(403, "No user info found") + expect(config.throw).toHaveBeenCalledWith(401, "No user info found") }) it("passes on to next() middleware if user is an admin", async () => { @@ -157,7 +157,7 @@ describe("Authorization middleware", () => { await config.executeMiddleware() expect(config.throw).toHaveBeenCalledWith( - 403, + 401, "Session not authenticated" ) }) From c1eafe5b284c353536ef2a9182ca5d3f5b068d2b Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 10 Jul 2024 14:05:21 +0200 Subject: [PATCH 03/13] Fix --- packages/server/src/api/routes/tests/utilities/TestFunctions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/api/routes/tests/utilities/TestFunctions.ts b/packages/server/src/api/routes/tests/utilities/TestFunctions.ts index 27d8592849..15a3ede39b 100644 --- a/packages/server/src/api/routes/tests/utilities/TestFunctions.ts +++ b/packages/server/src/api/routes/tests/utilities/TestFunctions.ts @@ -151,7 +151,7 @@ export const checkPermissionsEndpoint = async ({ await exports .createRequest(config.request, method, url, body) .set(failHeader) - .expect(403) + .expect(401) } export const getDB = () => { From 6db0379504f52552bc6f8d918936a61d323946bc Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 11 Jul 2024 16:35:57 +0100 Subject: [PATCH 04/13] Resync if it is found that a table or column is missing, this was previously done for specific cases but have expanded to cover all, but only retry once (not get into an infinite loop). --- packages/server/src/sdk/app/rows/search/sqs.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/server/src/sdk/app/rows/search/sqs.ts b/packages/server/src/sdk/app/rows/search/sqs.ts index 4745aee7fb..0ae43563de 100644 --- a/packages/server/src/sdk/app/rows/search/sqs.ts +++ b/packages/server/src/sdk/app/rows/search/sqs.ts @@ -240,10 +240,10 @@ async function runSqlQuery( function resyncDefinitionsRequired(status: number, message: string) { // pre data_ prefix on column names, need to resync return ( - (status === 400 && message?.match(USER_COLUMN_PREFIX_REGEX)) || - // default tables aren't included in definition - (status === 400 && - DEFAULT_TABLE_IDS.find(tableId => message?.includes(tableId))) || + // there are tables missing - try a resync + (status === 400 && message.includes("no such table: ")) || + // there are columns missing - try a resync + (status === 400 && message.includes("no such column: ")) || // no design document found, needs a full sync (status === 404 && message?.includes(SQLITE_DESIGN_DOC_ID)) ) @@ -251,7 +251,8 @@ function resyncDefinitionsRequired(status: number, message: string) { export async function search( options: RowSearchParams, - table: Table + table: Table, + opts?: { retry?: boolean } ): Promise> { let { paginate, query, ...params } = options @@ -376,9 +377,9 @@ export async function search( return response } catch (err: any) { const msg = typeof err === "string" ? err : err.message - if (resyncDefinitionsRequired(err.status, msg)) { + if (!opts?.retry && resyncDefinitionsRequired(err.status, msg)) { await sdk.tables.sqs.syncDefinition() - return search(options, table) + return search(options, table, { retry: true }) } // previously the internal table didn't error when a column didn't exist in search if (err.status === 400 && msg?.match(MISSING_COLUMN_REGEX)) { From 1e189450c08feded0b36ee28e2c178608a70950e Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 11 Jul 2024 16:38:30 +0100 Subject: [PATCH 05/13] Switching to regex. --- packages/server/src/sdk/app/rows/search/sqs.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/server/src/sdk/app/rows/search/sqs.ts b/packages/server/src/sdk/app/rows/search/sqs.ts index 0ae43563de..ead970ca82 100644 --- a/packages/server/src/sdk/app/rows/search/sqs.ts +++ b/packages/server/src/sdk/app/rows/search/sqs.ts @@ -45,13 +45,10 @@ import { getTableIDList, } from "./filters" import { dataFilters } from "@budibase/shared-core" -import { DEFAULT_TABLE_IDS } from "../../../../constants" const builder = new sql.Sql(SqlClient.SQL_LITE) const MISSING_COLUMN_REGEX = new RegExp(`no such column: .+`) -const USER_COLUMN_PREFIX_REGEX = new RegExp( - `no such column: .+${USER_COLUMN_PREFIX}` -) +const MISSING_TABLE_REGX = new RegExp(`no such table: .+`) function buildInternalFieldList( table: Table, @@ -241,9 +238,9 @@ function resyncDefinitionsRequired(status: number, message: string) { // pre data_ prefix on column names, need to resync return ( // there are tables missing - try a resync - (status === 400 && message.includes("no such table: ")) || + (status === 400 && message.match(MISSING_TABLE_REGX)) || // there are columns missing - try a resync - (status === 400 && message.includes("no such column: ")) || + (status === 400 && message.match(MISSING_COLUMN_REGEX)) || // no design document found, needs a full sync (status === 404 && message?.includes(SQLITE_DESIGN_DOC_ID)) ) From 5b1850d28f8146a4d1ea0031ff9b93aa742e177f Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 11 Jul 2024 16:41:12 +0100 Subject: [PATCH 06/13] PR comments. --- packages/server/src/sdk/app/rows/search/sqs.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/server/src/sdk/app/rows/search/sqs.ts b/packages/server/src/sdk/app/rows/search/sqs.ts index ead970ca82..4bf3c374e4 100644 --- a/packages/server/src/sdk/app/rows/search/sqs.ts +++ b/packages/server/src/sdk/app/rows/search/sqs.ts @@ -249,7 +249,7 @@ function resyncDefinitionsRequired(status: number, message: string) { export async function search( options: RowSearchParams, table: Table, - opts?: { retry?: boolean } + opts?: { retries?: number } ): Promise> { let { paginate, query, ...params } = options @@ -374,9 +374,10 @@ export async function search( return response } catch (err: any) { const msg = typeof err === "string" ? err : err.message - if (!opts?.retry && resyncDefinitionsRequired(err.status, msg)) { + const firstTry = !opts?.retries || opts.retries === 0 + if (firstTry && resyncDefinitionsRequired(err.status, msg)) { await sdk.tables.sqs.syncDefinition() - return search(options, table, { retry: true }) + return search(options, table, { retries: 1 }) } // previously the internal table didn't error when a column didn't exist in search if (err.status === 400 && msg?.match(MISSING_COLUMN_REGEX)) { From 9129e47dd941213391fba1b59c8f46431a867e74 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 11 Jul 2024 17:09:41 +0100 Subject: [PATCH 07/13] PR comment 2. --- packages/server/src/sdk/app/rows/search/sqs.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/server/src/sdk/app/rows/search/sqs.ts b/packages/server/src/sdk/app/rows/search/sqs.ts index 4bf3c374e4..7042d6fa2c 100644 --- a/packages/server/src/sdk/app/rows/search/sqs.ts +++ b/packages/server/src/sdk/app/rows/search/sqs.ts @@ -249,7 +249,7 @@ function resyncDefinitionsRequired(status: number, message: string) { export async function search( options: RowSearchParams, table: Table, - opts?: { retries?: number } + opts?: { retrying?: boolean } ): Promise> { let { paginate, query, ...params } = options @@ -374,10 +374,9 @@ export async function search( return response } catch (err: any) { const msg = typeof err === "string" ? err : err.message - const firstTry = !opts?.retries || opts.retries === 0 - if (firstTry && resyncDefinitionsRequired(err.status, msg)) { + if (!opts?.retrying && resyncDefinitionsRequired(err.status, msg)) { await sdk.tables.sqs.syncDefinition() - return search(options, table, { retries: 1 }) + return search(options, table, { retrying: true }) } // previously the internal table didn't error when a column didn't exist in search if (err.status === 400 && msg?.match(MISSING_COLUMN_REGEX)) { From 4c4e7c12de4c8c190ad87c570dfeaf4daeb01aea Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 12 Jul 2024 09:39:38 +0100 Subject: [PATCH 08/13] Fix a very rare flake in row.spec.ts. --- packages/server/src/api/routes/tests/row.spec.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 96be59a7e1..27a0d0983e 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -1460,17 +1460,12 @@ describe.each([ delete tableRequest.schema.id const table = await config.api.table.save(tableRequest) + const toCreate = generator + .unique(() => generator.integer({ min: 0, max: 10000 }), 10) + .map(number => ({ number, string: generator.word({ length: 30 }) })) const rows = await Promise.all( - generator - .unique( - () => ({ - string: generator.word({ length: 30 }), - number: generator.integer({ min: 0, max: 10000 }), - }), - 10 - ) - .map(d => config.api.row.save(table._id!, d)) + toCreate.map(d => config.api.row.save(table._id!, d)) ) const res = await config.api.row.exportRows(table._id!, { From 6440280b44e06830ae4f4ab6f800f10f6d82f857 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Fri, 12 Jul 2024 09:21:31 +0000 Subject: [PATCH 09/13] Bump version to 2.29.19 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index ff532def0b..bef5f0c268 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "2.29.18", + "version": "2.29.19", "npmClient": "yarn", "packages": [ "packages/*", From 0e8d216e22b164c08e786d3371dbd311d624295f Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Jul 2024 15:27:48 +0200 Subject: [PATCH 10/13] Unify newid --- packages/server/src/api/controllers/deploy/Deployment.ts | 5 ++--- packages/server/src/automations/utils.ts | 5 ++--- packages/server/src/db/inMemoryView.ts | 5 ++--- packages/server/src/db/newid.ts | 5 ----- packages/server/src/db/utils.ts | 5 +++-- packages/server/src/tests/utilities/TestConfiguration.ts | 4 +++- 6 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 packages/server/src/db/newid.ts diff --git a/packages/server/src/api/controllers/deploy/Deployment.ts b/packages/server/src/api/controllers/deploy/Deployment.ts index 611c82f28b..fe817730b6 100644 --- a/packages/server/src/api/controllers/deploy/Deployment.ts +++ b/packages/server/src/api/controllers/deploy/Deployment.ts @@ -1,5 +1,4 @@ -import newid from "../../../db/newid" -import { context } from "@budibase/backend-core" +import { context, utils } from "@budibase/backend-core" /** * This is used to pass around information about the deployment that is occurring @@ -12,7 +11,7 @@ export default class Deployment { appUrl?: string constructor(id = null) { - this._id = id || newid() + this._id = id || utils.newid() } setVerification(verification: any) { diff --git a/packages/server/src/automations/utils.ts b/packages/server/src/automations/utils.ts index 4d7e169f52..784632b626 100644 --- a/packages/server/src/automations/utils.ts +++ b/packages/server/src/automations/utils.ts @@ -1,10 +1,9 @@ import { Thread, ThreadType } from "../threads" import { definitions } from "./triggerInfo" import { automationQueue } from "./bullboard" -import newid from "../db/newid" import { updateEntityMetadata } from "../utilities" import { MetadataTypes } from "../constants" -import { db as dbCore, context } from "@budibase/backend-core" +import { db as dbCore, context, utils } from "@budibase/backend-core" import { getAutomationMetadataParams } from "../db/utils" import { cloneDeep } from "lodash/fp" import { quotas } from "@budibase/pro" @@ -207,7 +206,7 @@ export async function enableCronTrigger(appId: any, automation: Automation) { ) } // make a job id rather than letting Bull decide, makes it easier to handle on way out - const jobId = `${appId}_cron_${newid()}` + const jobId = `${appId}_cron_${utils.newid()}` const job: any = await automationQueue.add( { automation, diff --git a/packages/server/src/db/inMemoryView.ts b/packages/server/src/db/inMemoryView.ts index 73e5c622eb..525c4b456e 100644 --- a/packages/server/src/db/inMemoryView.ts +++ b/packages/server/src/db/inMemoryView.ts @@ -1,9 +1,8 @@ -import newid from "./newid" import { Row, Document, DBView } from "@budibase/types" // bypass the main application db config // use in memory pouchdb directly -import { db as dbCore } from "@budibase/backend-core" +import { db as dbCore, utils } from "@budibase/backend-core" const Pouch = dbCore.getPouch({ inMemory: true }) @@ -16,7 +15,7 @@ export async function runView( // use a different ID each time for the DB, make sure they // are always unique for each query, don't want overlap // which could cause 409s - const db = new Pouch(newid()) + const db = new Pouch(utils.newid()) try { // write all the docs to the in memory Pouch (remove revs) await db.bulkDocs( diff --git a/packages/server/src/db/newid.ts b/packages/server/src/db/newid.ts deleted file mode 100644 index bc8f3bb04b..0000000000 --- a/packages/server/src/db/newid.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { v4 } from "uuid" - -export default function (): string { - return v4().replace(/-/g, "") -} diff --git a/packages/server/src/db/utils.ts b/packages/server/src/db/utils.ts index 3bd1749d77..dfad00535e 100644 --- a/packages/server/src/db/utils.ts +++ b/packages/server/src/db/utils.ts @@ -1,5 +1,4 @@ -import newid from "./newid" -import { context, db as dbCore } from "@budibase/backend-core" +import { context, db as dbCore, utils } from "@budibase/backend-core" import { DatabaseQueryOpts, Datasource, @@ -15,6 +14,8 @@ import { export { DocumentType, VirtualDocumentType } from "@budibase/types" +const newid = utils.newid + type Optional = string | null export const enum AppStatus { diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts index 828b389add..3d53149385 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.ts +++ b/packages/server/src/tests/utilities/TestConfiguration.ts @@ -26,6 +26,7 @@ import { roles, sessions, tenancy, + utils, } from "@budibase/backend-core" import { app as appController, @@ -40,7 +41,6 @@ import { } from "./controllers" import { cleanup } from "../../utilities/fileSystem" -import newid from "../../db/newid" import { generateUserMetadataID } from "../../db/utils" import { startup } from "../../startup" import supertest from "supertest" @@ -74,6 +74,8 @@ import { cloneDeep } from "lodash" import jwt, { Secret } from "jsonwebtoken" import { Server } from "http" +const newid = utils.newid + mocks.licenses.init(pro) // use unlimited license by default From 79c292538c70b7d2d2697eb78d2d3207955647cc Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 12 Jul 2024 13:51:06 +0100 Subject: [PATCH 11/13] There is a risk with default tables that the schema may exist in the DB as well as existing in memory - in this case we should merge the schemas to make sure that all possible attributes from the in memory representation, and the on disk version (which may have been updated by the user) have been captured in the SQLite schema. --- packages/server/src/sdk/app/tables/internal/sqs.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/server/src/sdk/app/tables/internal/sqs.ts b/packages/server/src/sdk/app/tables/internal/sqs.ts index fc0ee8fc0b..aeaa33ed2d 100644 --- a/packages/server/src/sdk/app/tables/internal/sqs.ts +++ b/packages/server/src/sdk/app/tables/internal/sqs.ts @@ -14,7 +14,7 @@ import { CONSTANT_INTERNAL_ROW_COLS, generateJunctionTableID, } from "../../../../db/utils" -import { isEqual } from "lodash" +import { isEqual, merge } from "lodash" import { DEFAULT_TABLES } from "../../../../db/defaultData/datasource_bb_default" const FieldTypeMap: Record = { @@ -130,9 +130,18 @@ async function buildBaseDefinition(): Promise { const defaultTables = DEFAULT_TABLES const definition = sql.designDoc.base("tableId") for (let table of tables.concat(defaultTables)) { + const tableId = table._id! + let existing = definition.sql.tables[tableId] + let mapped = mapTable(table) + // there are multiple definitions for this table (default table overlap) + // when there is overlap - we have to make sure we have columns from all definitions + // this problem really only applies to sample data tables where they've been expanded + if (existing) { + mapped[tableId] = merge(mapped[tableId], existing) + } definition.sql.tables = { ...definition.sql.tables, - ...mapTable(table), + ...mapped, } } return definition From 745a05fe8d34517715a968cd9d562cf77b2140f8 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 12 Jul 2024 13:54:55 +0100 Subject: [PATCH 12/13] Updating how the 'merging' is handled, don't include the in-memory representation if it exists on disk in Couch, prefer that. --- .../server/src/sdk/app/tables/internal/sqs.ts | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/server/src/sdk/app/tables/internal/sqs.ts b/packages/server/src/sdk/app/tables/internal/sqs.ts index aeaa33ed2d..9db10f2b41 100644 --- a/packages/server/src/sdk/app/tables/internal/sqs.ts +++ b/packages/server/src/sdk/app/tables/internal/sqs.ts @@ -14,7 +14,7 @@ import { CONSTANT_INTERNAL_ROW_COLS, generateJunctionTableID, } from "../../../../db/utils" -import { isEqual, merge } from "lodash" +import { isEqual } from "lodash" import { DEFAULT_TABLES } from "../../../../db/defaultData/datasource_bb_default" const FieldTypeMap: Record = { @@ -127,21 +127,17 @@ function mapTable(table: Table): SQLiteTables { // nothing exists, need to iterate though existing tables async function buildBaseDefinition(): Promise { const tables = await tablesSdk.getAllInternalTables() - const defaultTables = DEFAULT_TABLES - const definition = sql.designDoc.base("tableId") - for (let table of tables.concat(defaultTables)) { - const tableId = table._id! - let existing = definition.sql.tables[tableId] - let mapped = mapTable(table) - // there are multiple definitions for this table (default table overlap) - // when there is overlap - we have to make sure we have columns from all definitions - // this problem really only applies to sample data tables where they've been expanded - if (existing) { - mapped[tableId] = merge(mapped[tableId], existing) + for (const defaultTable of DEFAULT_TABLES) { + // the default table doesn't exist in Couch, use the in-memory representation + if (!tables.find(table => table._id === defaultTable._id)) { + tables.push(defaultTable) } + } + const definition = sql.designDoc.base("tableId") + for (let table of tables) { definition.sql.tables = { ...definition.sql.tables, - ...mapped, + ...mapTable(table), } } return definition From ec8c6edf6800c4ad9a5e18c3baa6d896ab16860e Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Fri, 12 Jul 2024 14:11:15 +0000 Subject: [PATCH 13/13] Bump version to 2.29.20 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index bef5f0c268..efcbb7694c 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "2.29.19", + "version": "2.29.20", "npmClient": "yarn", "packages": [ "packages/*",