From 80204194373c8054364c98b7996083d7e9c765cb Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Mon, 12 Aug 2024 11:23:32 +0200 Subject: [PATCH] Creation and deprecation status of schema coordinates - schema age filter (part 1) (#5224) --- .eslintrc.cjs | 1 + integration-tests/package.json | 2 +- integration-tests/testkit/seed.ts | 9 + .../tests/api/schema/cleanup-tracker.spec.ts | 991 ++++++++++++++++++ package.json | 2 +- packages/libraries/apollo/package.json | 2 +- .../libraries/apollo/tests/apollo.spec.ts | 2 +- packages/libraries/core/package.json | 2 +- packages/libraries/yoga/package.json | 2 +- packages/libraries/yoga/tests/yoga.spec.ts | 2 +- packages/migrations/package.json | 1 + ...4.07.23T09.36.00.schema-cleanup-tracker.ts | 496 +++++++++ packages/migrations/src/index.ts | 15 +- packages/migrations/src/run-pg-migrations.ts | 2 + ...23T09.36.00.schema-cleanup-tracker.test.ts | 246 +++++ packages/migrations/test/root.ts | 1 + packages/services/api/package.json | 2 +- .../schema/providers/inspector.spec.ts | 371 +++++++ .../src/modules/schema/providers/inspector.ts | 143 ++- .../providers/models/composite-legacy.ts | 6 + .../schema/providers/models/composite.ts | 10 + .../modules/schema/providers/models/shared.ts | 10 +- .../schema/providers/models/single-legacy.ts | 6 + .../modules/schema/providers/models/single.ts | 5 + .../schema/providers/registry-checks.ts | 56 +- .../schema/providers/schema-manager.ts | 21 +- .../schema/providers/schema-publisher.ts | 15 +- .../src/modules/shared/providers/storage.ts | 3 + packages/services/broker-worker/package.json | 2 +- packages/services/storage/src/db/types.ts | 10 + packages/services/storage/src/index.ts | 91 ++ .../web/app/src/components/schema-editor.ts | 2 +- pnpm-lock.yaml | 375 ++++--- prettier.config.cjs | 7 +- 34 files changed, 2707 insertions(+), 204 deletions(-) create mode 100644 integration-tests/tests/api/schema/cleanup-tracker.spec.ts create mode 100644 packages/migrations/src/actions/2024.07.23T09.36.00.schema-cleanup-tracker.ts create mode 100644 packages/migrations/test/2024.07.23T09.36.00.schema-cleanup-tracker.test.ts create mode 100644 packages/services/api/src/modules/schema/providers/inspector.spec.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 724815a1a..846b4b48e 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -130,6 +130,7 @@ module.exports = { 'prefer-destructuring': 'off', 'prefer-const': 'off', 'no-useless-escape': 'off', + 'no-inner-declarations': 'off', '@typescript-eslint/no-unnecessary-type-assertion': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', diff --git a/integration-tests/package.json b/integration-tests/package.json index e5a9ce7e5..ffe5fc0a7 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -36,7 +36,7 @@ "slonik": "30.4.4", "strip-ansi": "7.1.0", "tslib": "2.6.3", - "vitest": "1.6.0", + "vitest": "2.0.3", "zod": "3.23.8" } } diff --git a/integration-tests/testkit/seed.ts b/integration-tests/testkit/seed.ts index 3f41a0a74..5497b5d03 100644 --- a/integration-tests/testkit/seed.ts +++ b/integration-tests/testkit/seed.ts @@ -72,6 +72,15 @@ export function initSeed() { } return { + async createDbConnection() { + const pool = await createConnectionPool(); + return { + pool, + [Symbol.asyncDispose]: async () => { + await pool.end(); + }, + }; + }, authenticate: authenticate, generateEmail: () => userEmail(generateUnique()), async createOwner() { diff --git a/integration-tests/tests/api/schema/cleanup-tracker.spec.ts b/integration-tests/tests/api/schema/cleanup-tracker.spec.ts new file mode 100644 index 000000000..649de8438 --- /dev/null +++ b/integration-tests/tests/api/schema/cleanup-tracker.spec.ts @@ -0,0 +1,991 @@ +import 'reflect-metadata'; +import { sql, type CommonQueryMethods } from 'slonik'; +/* eslint-disable no-process-env */ +import { ProjectType, TargetAccessScope } from 'testkit/gql/graphql'; +import { test } from 'vitest'; +import { initSeed } from '../../../testkit/seed'; + +async function fetchCoordinates(db: CommonQueryMethods, target: { id: string }) { + const result = await db.query<{ + coordinate: string; + created_in_version_id: string; + deprecated_in_version_id: string | null; + }>(sql` + SELECT coordinate, created_in_version_id, deprecated_in_version_id + FROM schema_coordinate_status WHERE target_id = ${target.id} + `); + + return result.rows; +} + +describe('schema cleanup tracker', () => { + test.concurrent('single', async ({ expect }) => { + const { publishSchema, target, createDbConnection } = await prepare(); + // This API is soooooooooooo awkward xD + await using db = await createDbConnection(); + + const ver1 = await publishSchema(/* GraphQL */ ` + type Query { + hello: String + } + `); + + await expect(fetchCoordinates(db.pool, target)).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + coordinate: 'Query', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.hello', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + ]), + ); + + const ver2 = await publishSchema(/* GraphQL */ ` + type Query { + hello: String + hi: String + } + `); + + await expect(fetchCoordinates(db.pool, target)).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + coordinate: 'Query', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.hello', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.hi', + created_in_version_id: ver2, + deprecated_in_version_id: null, + }), + ]), + ); + + await publishSchema(/* GraphQL */ ` + type Query { + hello: String + } + `); + + await expect(fetchCoordinates(db.pool, target)).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + coordinate: 'Query', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.hello', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.not.objectContaining({ + coordinate: 'Query.hi', + created_in_version_id: ver2, + deprecated_in_version_id: null, + }), + ]), + ); + + const ver4 = await publishSchema(/* GraphQL */ ` + type Query { + hello: String + hi: String + } + `); + + await expect(fetchCoordinates(db.pool, target)).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + coordinate: 'Query', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.hello', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.hi', + created_in_version_id: ver4, + deprecated_in_version_id: null, + }), + ]), + ); + + const ver5 = await publishSchema(/* GraphQL */ ` + type Query { + hello: String @deprecated(reason: "no longer needed") + bye: String + goodbye: String + hi: String @deprecated(reason: "no longer needed") + } + `); + + await expect(fetchCoordinates(db.pool, target)).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + coordinate: 'Query', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.hello', + created_in_version_id: ver1, + deprecated_in_version_id: ver5, + }), + expect.objectContaining({ + coordinate: 'Query.bye', + created_in_version_id: ver5, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.goodbye', + created_in_version_id: ver5, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.hi', + created_in_version_id: ver4, + deprecated_in_version_id: ver5, + }), + ]), + ); + + await publishSchema(/* GraphQL */ ` + type Query { + hello: String + bye: String + hi: String @deprecated(reason: "no longer needed") + } + `); + + await expect(fetchCoordinates(db.pool, target)).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + coordinate: 'Query', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.hello', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.bye', + created_in_version_id: ver5, + deprecated_in_version_id: null, + }), + expect.not.objectContaining({ + coordinate: 'Query.goodbye', + created_in_version_id: ver5, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.hi', + created_in_version_id: ver4, + deprecated_in_version_id: ver5, + }), + ]), + ); + }); + + test.concurrent('federation', async ({ expect }) => { + const { publishSchema, deleteSchema, target, createDbConnection } = await prepare( + ProjectType.Federation, + ); + await using db = await createDbConnection(); + + const serviceFoo = { + name: 'foo', + url: 'https://api.com/foo', + }; + + const serviceBar = { + name: 'bar', + url: 'https://api.com/bar', + }; + + const ver1 = await publishSchema( + /* GraphQL */ ` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + + type Query { + user(id: ID!): User + } + + type User @key(fields: "id") { + id: ID! + name: String + } + `, + serviceFoo, + ); + + await expect(fetchCoordinates(db.pool, target)).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + coordinate: 'Query', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.name', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + ]), + ); + + const ver2 = await publishSchema( + /* GraphQL */ ` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + + type Query { + users: User + } + + type User @key(fields: "id") { + id: ID! + pictureUrl: String + } + `, + serviceBar, + ); + + await expect(fetchCoordinates(db.pool, target)).resolves.toEqual( + expect.arrayContaining([ + // foo + expect.objectContaining({ + coordinate: 'Query', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.name', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + // bar + expect.objectContaining({ + coordinate: 'Query.users', + created_in_version_id: ver2, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.pictureUrl', + created_in_version_id: ver2, + deprecated_in_version_id: null, + }), + ]), + ); + + await deleteSchema(serviceBar.name); + + await expect(fetchCoordinates(db.pool, target)).resolves.toEqual( + expect.arrayContaining([ + // foo + expect.objectContaining({ + coordinate: 'Query', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.name', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + // bar + expect.not.objectContaining({ + coordinate: 'Query.users', + created_in_version_id: ver2, + deprecated_in_version_id: null, + }), + expect.not.objectContaining({ + coordinate: 'User.pictureUrl', + created_in_version_id: ver2, + deprecated_in_version_id: null, + }), + ]), + ); + + const ver4 = await publishSchema( + /* GraphQL */ ` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + + type Query { + users: User + } + + type User @key(fields: "id") { + id: ID! + pictureUrl: String + } + `, + serviceBar, + ); + + await expect(fetchCoordinates(db.pool, target)).resolves.toEqual( + expect.arrayContaining([ + // foo + expect.objectContaining({ + coordinate: 'Query', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.name', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + // bar + expect.objectContaining({ + coordinate: 'Query.users', + created_in_version_id: ver4, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.pictureUrl', + created_in_version_id: ver4, + deprecated_in_version_id: null, + }), + ]), + ); + + const ver5 = await publishSchema( + /* GraphQL */ ` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + + type Query { + users: User @deprecated(reason: "no longer needed") + randomUser: User + admin: User + } + + type User @key(fields: "id") { + id: ID! + pictureUrl: String @deprecated(reason: "no longer needed") + } + `, + serviceBar, + ); + + await expect(fetchCoordinates(db.pool, target)).resolves.toEqual( + expect.arrayContaining([ + // foo + expect.objectContaining({ + coordinate: 'Query', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.name', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + // bar + expect.objectContaining({ + coordinate: 'Query.users', + created_in_version_id: ver4, + deprecated_in_version_id: ver5, + }), + expect.objectContaining({ + coordinate: 'Query.randomUser', + created_in_version_id: ver5, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.admin', + created_in_version_id: ver5, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.pictureUrl', + created_in_version_id: ver4, + deprecated_in_version_id: ver5, + }), + ]), + ); + + await publishSchema( + /* GraphQL */ ` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + + type Query { + users: User + admin: User + } + + type User @key(fields: "id") { + id: ID! + pictureUrl: String @deprecated(reason: "no longer needed") + } + `, + serviceBar, + ); + + await expect(fetchCoordinates(db.pool, target)).resolves.toEqual( + expect.arrayContaining([ + // foo + expect.objectContaining({ + coordinate: 'Query', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.name', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + // bar + expect.objectContaining({ + coordinate: 'Query.users', + created_in_version_id: ver4, + deprecated_in_version_id: null, + }), + expect.not.objectContaining({ + coordinate: 'Query.randomUser', + created_in_version_id: ver5, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.admin', + created_in_version_id: ver5, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.pictureUrl', + created_in_version_id: ver4, + deprecated_in_version_id: ver5, + }), + ]), + ); + }); + + test.concurrent('stitching', async ({ expect }) => { + const { publishSchema, deleteSchema, target, createDbConnection } = await prepare( + ProjectType.Stitching, + ); + await using db = await createDbConnection(); + + const serviceFoo = { + name: 'foo', + url: 'https://api.com/foo', + }; + + const serviceBar = { + name: 'bar', + url: 'https://api.com/bar', + }; + + const ver1 = await publishSchema( + /* GraphQL */ ` + type Query { + user(id: ID!): User @merge + } + + type User @key(selectionSet: "{ id }") { + id: ID! + name: String + } + `, + serviceFoo, + ); + + await expect(fetchCoordinates(db.pool, target)).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + coordinate: 'Query', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.name', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + ]), + ); + + const ver2 = await publishSchema( + /* GraphQL */ ` + type Query { + users: User + user(id: ID!): User @merge + } + + type User @key(selectionSet: "{ id }") { + id: ID! + pictureUrl: String + } + `, + serviceBar, + ); + + await expect(fetchCoordinates(db.pool, target)).resolves.toEqual( + expect.arrayContaining([ + // foo + expect.objectContaining({ + coordinate: 'Query', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.name', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + // bar + expect.objectContaining({ + coordinate: 'Query.users', + created_in_version_id: ver2, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.pictureUrl', + created_in_version_id: ver2, + deprecated_in_version_id: null, + }), + ]), + ); + + await deleteSchema(serviceBar.name); + + await expect(fetchCoordinates(db.pool, target)).resolves.toEqual( + expect.arrayContaining([ + // foo + expect.objectContaining({ + coordinate: 'Query', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.name', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + // bar + expect.not.objectContaining({ + coordinate: 'Query.users', + created_in_version_id: ver2, + deprecated_in_version_id: null, + }), + expect.not.objectContaining({ + coordinate: 'User.pictureUrl', + created_in_version_id: ver2, + deprecated_in_version_id: null, + }), + ]), + ); + + const ver4 = await publishSchema( + /* GraphQL */ ` + type Query { + users: User + user(id: ID!): User @merge + } + + type User @key(selectionSet: "{ id }") { + id: ID! + pictureUrl: String + } + `, + serviceBar, + ); + + await expect(fetchCoordinates(db.pool, target)).resolves.toEqual( + expect.arrayContaining([ + // foo + expect.objectContaining({ + coordinate: 'Query', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.name', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + // bar + expect.objectContaining({ + coordinate: 'Query.users', + created_in_version_id: ver4, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.pictureUrl', + created_in_version_id: ver4, + deprecated_in_version_id: null, + }), + ]), + ); + + const ver5 = await publishSchema( + /* GraphQL */ ` + type Query { + users: User @deprecated(reason: "no longer needed") + randomUser: User + admin: User + user(id: ID!): User @merge + } + + type User @key(selectionSet: "{ id }") { + id: ID! + pictureUrl: String @deprecated(reason: "no longer needed") + } + `, + serviceBar, + ); + + await expect(fetchCoordinates(db.pool, target)).resolves.toEqual( + expect.arrayContaining([ + // foo + expect.objectContaining({ + coordinate: 'Query', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.name', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + // bar + expect.objectContaining({ + coordinate: 'Query.users', + created_in_version_id: ver4, + deprecated_in_version_id: ver5, + }), + expect.objectContaining({ + coordinate: 'Query.randomUser', + created_in_version_id: ver5, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.admin', + created_in_version_id: ver5, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.pictureUrl', + created_in_version_id: ver4, + deprecated_in_version_id: ver5, + }), + ]), + ); + + await publishSchema( + /* GraphQL */ ` + type Query { + users: User + admin: User + user(id: ID!): User @merge + } + + type User @key(selectionSet: "{ id }") { + id: ID! + pictureUrl: String @deprecated(reason: "no longer needed") + } + `, + serviceBar, + ); + + await expect(fetchCoordinates(db.pool, target)).resolves.toEqual( + expect.arrayContaining([ + // foo + expect.objectContaining({ + coordinate: 'Query', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.user.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.id', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.name', + created_in_version_id: ver1, + deprecated_in_version_id: null, + }), + // bar + expect.objectContaining({ + coordinate: 'Query.users', + created_in_version_id: ver4, + deprecated_in_version_id: null, + }), + expect.not.objectContaining({ + coordinate: 'Query.randomUser', + created_in_version_id: ver5, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'Query.admin', + created_in_version_id: ver5, + deprecated_in_version_id: null, + }), + expect.objectContaining({ + coordinate: 'User.pictureUrl', + created_in_version_id: ver4, + deprecated_in_version_id: ver5, + }), + ]), + ); + }); +}); + +async function prepare(projectType: ProjectType = ProjectType.Single) { + const { createOwner, createDbConnection } = initSeed(); + const { createOrg } = await createOwner(); + const { createProject, organization } = await createOrg(); + const { createToken, project, target } = await createProject(projectType); + const token = await createToken({ + targetScopes: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], + projectScopes: [], + organizationScopes: [], + }); + + return { + createDbConnection, + async publishSchema( + sdl: string, + service?: { + name: string; + url: string; + }, + ) { + const result = await token + .publishSchema({ sdl, service: service?.name, url: service?.url }) + .then(r => r.expectNoGraphQLErrors()); + + if (result.schemaPublish.__typename !== 'SchemaPublishSuccess') { + console.log(JSON.stringify(result.schemaPublish, null, 2)); + throw new Error(`Expected schemaPublish success, got ${result.schemaPublish.__typename}`); + } + + if (!result.schemaPublish.valid) { + throw new Error('Expected schema to be valid'); + } + + const version = await token.fetchLatestValidSchema(); + const versionId = version.latestValidVersion?.id; + + if (!versionId) { + throw new Error('Expected version id to be defined'); + } + + return versionId; + }, + async deleteSchema(serviceName: string) { + const result = await token.deleteSchema(serviceName).then(r => r.expectNoGraphQLErrors()); + + if (result.schemaDelete.__typename !== 'SchemaDeleteSuccess') { + throw new Error(`Expected schemaDelete success, got ${result.schemaDelete.__typename}`); + } + + if (!result.schemaDelete.valid) { + throw new Error('Expected schema to be valid'); + } + + const version = await token.fetchLatestValidSchema(); + const versionId = version.latestValidVersion?.id; + + if (!versionId) { + throw new Error('Expected version id to be defined'); + } + + return versionId; + }, + organization, + project, + target, + }; +} diff --git a/package.json b/package.json index e64b2329a..3c05cef6d 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "turbo": "1.13.4", "typescript": "5.5.3", "vite-tsconfig-paths": "4.3.2", - "vitest": "1.6.0" + "vitest": "2.0.3" }, "husky": { "hooks": { diff --git a/packages/libraries/apollo/package.json b/packages/libraries/apollo/package.json index 6a044f169..e6c8417f2 100644 --- a/packages/libraries/apollo/package.json +++ b/packages/libraries/apollo/package.json @@ -57,7 +57,7 @@ "graphql": "16.9.0", "graphql-ws": "5.16.0", "nock": "14.0.0-beta.7", - "vitest": "1.6.0", + "vitest": "2.0.3", "ws": "8.18.0" }, "publishConfig": { diff --git a/packages/libraries/apollo/tests/apollo.spec.ts b/packages/libraries/apollo/tests/apollo.spec.ts index d41d10975..e98bcd8c8 100644 --- a/packages/libraries/apollo/tests/apollo.spec.ts +++ b/packages/libraries/apollo/tests/apollo.spec.ts @@ -110,7 +110,7 @@ test('should not interrupt the process', async () => { test('should capture client name and version headers', async () => { const clean = handleProcess(); - const fetchSpy = vi.fn<[RequestInfo | URL, options: RequestInit | undefined]>(async () => + const fetchSpy = vi.fn(async (_input: string | URL | globalThis.Request, _init?: RequestInit) => Response.json({}, { status: 200 }), ); diff --git a/packages/libraries/core/package.json b/packages/libraries/core/package.json index 5a9d92f13..d036084d5 100644 --- a/packages/libraries/core/package.json +++ b/packages/libraries/core/package.json @@ -59,7 +59,7 @@ "graphql": "16.9.0", "nock": "14.0.0-beta.7", "tslib": "2.6.3", - "vitest": "1.6.0" + "vitest": "2.0.3" }, "publishConfig": { "registry": "https://registry.npmjs.org", diff --git a/packages/libraries/yoga/package.json b/packages/libraries/yoga/package.json index d705807de..0b3c93d6a 100644 --- a/packages/libraries/yoga/package.json +++ b/packages/libraries/yoga/package.json @@ -61,7 +61,7 @@ "graphql-ws": "5.16.0", "graphql-yoga": "5.6.0", "nock": "14.0.0-beta.7", - "vitest": "1.6.0", + "vitest": "2.0.3", "ws": "8.18.0" }, "publishConfig": { diff --git a/packages/libraries/yoga/tests/yoga.spec.ts b/packages/libraries/yoga/tests/yoga.spec.ts index b472c734d..04e4104a6 100644 --- a/packages/libraries/yoga/tests/yoga.spec.ts +++ b/packages/libraries/yoga/tests/yoga.spec.ts @@ -123,7 +123,7 @@ test('should not interrupt the process', async () => { }, 1_000); test('should capture client name and version headers', async () => { - const fetchSpy = vi.fn<[RequestInfo | URL, options: RequestInit | undefined]>(async () => + const fetchSpy = vi.fn(async (_input: string | URL | globalThis.Request, _init?: RequestInit) => Response.json({}, { status: 200 }), ); const clean = handleProcess(); diff --git a/packages/migrations/package.json b/packages/migrations/package.json index 5f0cb0841..bdbe38350 100644 --- a/packages/migrations/package.json +++ b/packages/migrations/package.json @@ -27,6 +27,7 @@ "copyfiles": "2.4.1", "dotenv": "16.4.5", "got": "14.4.1", + "graphql": "16.9.0", "p-limit": "4.0.0", "pg-promise": "11.9.0", "slonik": "30.4.4", diff --git a/packages/migrations/src/actions/2024.07.23T09.36.00.schema-cleanup-tracker.ts b/packages/migrations/src/actions/2024.07.23T09.36.00.schema-cleanup-tracker.ts new file mode 100644 index 000000000..4a4db7285 --- /dev/null +++ b/packages/migrations/src/actions/2024.07.23T09.36.00.schema-cleanup-tracker.ts @@ -0,0 +1,496 @@ +import { + buildSchema, + GraphQLFieldMap, + GraphQLSchema, + isEnumType, + isInputObjectType, + isInterfaceType, + isIntrospectionType, + isObjectType, + isScalarType, + isUnionType, +} from 'graphql'; +import { sql, type CommonQueryMethods } from 'slonik'; +import { env } from '../environment'; +import type { MigrationExecutor } from '../pg-migrator'; + +export default { + name: '2024.07.23T09.36.00.schema-cleanup-tracker.ts', + async run({ connection }) { + await connection.query(sql` + CREATE TABLE IF NOT EXISTS "schema_coordinate_status" ( + coordinate text NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + created_in_version_id UUID NOT NULL REFERENCES "schema_versions" ("id") ON DELETE CASCADE, + deprecated_at TIMESTAMPTZ, + deprecated_in_version_id UUID REFERENCES "schema_versions" ("id") ON DELETE CASCADE, + "target_id" UUID NOT NULL REFERENCES "targets" ("id") ON DELETE CASCADE, + PRIMARY KEY (coordinate, target_id) + ); + + CREATE INDEX IF NOT EXISTS idx_schema_coordinate_status_by_target_timestamp + ON schema_coordinate_status( + target_id, + created_at, + deprecated_at + ); + CREATE INDEX IF NOT EXISTS idx_schema_coordinate_status_by_target_coordinate_timestamp + ON schema_coordinate_status( + target_id, + coordinate, + created_at, + deprecated_at + ); + `); + + if (env.isHiveCloud) { + console.log('Skipping schema coordinate status migration for hive cloud'); + return; + } + + const schemaVersionsTotal = await connection.oneFirst(sql` + SELECT count(*) as total FROM schema_versions + `); + console.log(`Found ${schemaVersionsTotal} schema versions`); + + if (schemaVersionsTotal > 1000) { + console.warn( + `[WARN] There are more than 1000 schema versions (${schemaVersionsTotal}). Skipping a data backfill.`, + ); + return; + } + + await schemaCoordinateStatusMigration(connection); + }, +} satisfies MigrationExecutor; + +type SchemaCoordinatesDiffResult = { + /** + * Coordinates that are in incoming but not in existing (including deprecated ones) + */ + added: Set; + /** + * Coordinates that are deprecated in incoming, but were not deprecated in existing or non-existent + */ + deprecated: Set; +}; + +function diffSchemaCoordinates( + existingSchema: GraphQLSchema, + incomingSchema: GraphQLSchema, +): SchemaCoordinatesDiffResult { + const before = getSchemaCoordinates(existingSchema); + const after = getSchemaCoordinates(incomingSchema); + + const added = after.coordinates.difference(before.coordinates); + const deprecated = after.deprecated.difference(before.deprecated); + + return { + added, + deprecated, + }; +} + +export async function schemaCoordinateStatusMigration(connection: CommonQueryMethods) { + // Fetch targets + const targetResult = await connection.query<{ id: string }>(sql` + SELECT id FROM targets WHERE ID NOT IN (SELECT target_id FROM schema_coordinate_status) + `); + + console.log(`Found ${targetResult.rowCount} targets`); + + let i = 0; + for await (const target of targetResult.rows) { + try { + console.log(`Processing target (${i++}/${targetResult.rowCount}) - ${target.id}`); + + const latestSchema = await connection.maybeOne<{ + id: string; + created_at: number; + is_composable: boolean; + sdl?: string; + previous_schema_version_id?: string; + }>(sql` + SELECT + id, + created_at, + is_composable, + previous_schema_version_id, + composite_schema_sdl as sdl + FROM schema_versions + WHERE target_id = ${target.id} AND is_composable = true + ORDER BY created_at DESC + LIMIT 1 + `); + + if (!latestSchema) { + console.log('[SKIPPING] No latest composable schema found for target %s', target.id); + continue; + } + + if (!latestSchema.sdl) { + console.warn( + `[SKIPPING] No latest, composable schema with non-empty sdl found for target ${target.id}.`, + ); + continue; + } + + const schema = buildSchema(latestSchema.sdl, { + assumeValid: true, + assumeValidSDL: true, + }); + const targetCoordinates = getSchemaCoordinates(schema); + + // The idea here is to + // 1. start from the latest composable version. + // 2. create a list of coordinates that are in the latest version, all and deprecated. + // 3. navigate to the previous version and compare the coordinates. + // 4. if a coordinate is added, upsert it into the schema_coordinate_status and remove it from the list. + // 5. if a coordinate is deprecated, upsert it into the schema_coordinate_status and remove it from the list of deprecated coordinates. + // 6. if the list of coordinates is empty, stop the process. + // 7. if the previous version is not composable, skip it and continue with the next previous version. + // 8. if the previous version is not found, insert all remaining coordinates and stop the process. This step might create incorrect dates! + await processVersion(1, connection, targetCoordinates, target.id, { + schema, + versionId: latestSchema.id, + createdAt: latestSchema.created_at, + previousVersionId: latestSchema.previous_schema_version_id ?? null, + }); + } catch (error) { + console.error(`Error processing target ${target.id}`); + console.error(error); + } + } +} + +function getSchemaCoordinates(schema: GraphQLSchema): { + coordinates: Set; + deprecated: Set; +} { + const coordinates = new Set(); + const deprecated = new Set(); + + const typeMap = schema.getTypeMap(); + + for (const typeName in typeMap) { + const typeDefinition = typeMap[typeName]; + + if (isIntrospectionType(typeDefinition)) { + continue; + } + + coordinates.add(typeName); + + if (isObjectType(typeDefinition) || isInterfaceType(typeDefinition)) { + visitSchemaCoordinatesOfGraphQLFieldMap( + typeName, + typeDefinition.getFields(), + coordinates, + deprecated, + ); + } else if (isInputObjectType(typeDefinition)) { + const fieldMap = typeDefinition.getFields(); + for (const fieldName in fieldMap) { + const fieldDefinition = fieldMap[fieldName]; + + coordinates.add(`${typeName}.${fieldName}`); + if (fieldDefinition.deprecationReason) { + deprecated.add(`${typeName}.${fieldName}`); + } + } + } else if (isUnionType(typeDefinition)) { + for (const member of typeDefinition.getTypes()) { + coordinates.add(`${typeName}.${member.name}`); + } + } else if (isEnumType(typeDefinition)) { + const values = typeDefinition.getValues(); + for (const value of values) { + coordinates.add(`${typeName}.${value.name}`); + if (value.deprecationReason) { + deprecated.add(`${typeName}.${value.name}`); + } + } + } else if (isScalarType(typeDefinition)) { + // + } else { + throw new Error(`Unsupported type kind ${typeName}`); + } + } + + return { + coordinates, + deprecated, + }; +} + +function visitSchemaCoordinatesOfGraphQLFieldMap( + typeName: string, + fieldMap: GraphQLFieldMap, + coordinates: Set, + deprecated: Set, +) { + for (const fieldName in fieldMap) { + const fieldDefinition = fieldMap[fieldName]; + + coordinates.add(`${typeName}.${fieldName}`); + if (fieldDefinition.deprecationReason) { + deprecated.add(`${typeName}.${fieldName}`); + } + + for (const arg of fieldDefinition.args) { + coordinates.add(`${typeName}.${fieldName}.${arg.name}`); + if (arg.deprecationReason) { + deprecated.add(`${typeName}.${fieldName}.${arg.name}`); + } + } + } +} + +async function insertRemainingCoordinates( + connection: CommonQueryMethods, + targetId: string, + targetCoordinates: { + coordinates: Set; + deprecated: Set; + }, + versionId: string, + createdAt: number, +) { + if (targetCoordinates.coordinates.size === 0) { + return; + } + + const pgDate = new Date(createdAt).toISOString(); + + // Deprecated only the coordinates that are still in the queue + const remainingDeprecated = targetCoordinates.deprecated.intersection( + targetCoordinates.coordinates, + ); + + console.log( + `Adding remaining ${targetCoordinates.coordinates.size} coordinates for target ${targetId}`, + ); + await connection.query(sql` + INSERT INTO schema_coordinate_status + ( target_id, coordinate, created_at, created_in_version_id ) + SELECT * FROM ${sql.unnest( + Array.from(targetCoordinates.coordinates).map(coordinate => [ + targetId, + coordinate, + pgDate, + versionId, + ]), + ['uuid', 'text', 'date', 'uuid'], + )} + ON CONFLICT (target_id, coordinate) + DO UPDATE SET created_at = ${pgDate}, created_in_version_id = ${versionId} + `); + + if (remainingDeprecated.size) { + console.log( + `Deprecating remaining ${remainingDeprecated.size} coordinates for target ${targetId}`, + ); + await connection.query(sql` + INSERT INTO schema_coordinate_status + ( target_id, coordinate, created_at, created_in_version_id, deprecated_at, deprecated_in_version_id ) + SELECT * FROM ${sql.unnest( + Array.from(remainingDeprecated).map(coordinate => [ + targetId, + coordinate, + pgDate, + versionId, + pgDate, + versionId, + ]), + ['uuid', 'text', 'date', 'uuid', 'date', 'uuid'], + )} + ON CONFLICT (target_id, coordinate) + DO UPDATE SET deprecated_at = ${pgDate}, deprecated_in_version_id = ${versionId} + `); + // there will be a conflict, because we are going from deprecated to added order. + } +} + +async function processVersion( + depth: number, + connection: CommonQueryMethods, + targetCoordinates: { + coordinates: Set; + deprecated: Set; + }, + targetId: string, + after: { + schema: GraphQLSchema; + versionId: string; + createdAt: number; + previousVersionId: string | null; + }, +): Promise { + console.log(`Processing target %s at depth %s - version`, targetId, depth, after.versionId); + const previousVersionId = after.previousVersionId; + if (!previousVersionId) { + // Seems like there is no previous version. + console.log( + `[END] No previous version found. Inserting all remaining coordinates for ${targetId}`, + ); + await insertRemainingCoordinates( + connection, + targetId, + targetCoordinates, + after.versionId, + after.createdAt, + ); + return; + } + + const versionBefore = await connection.maybeOne<{ + id: string; + sdl?: string; + previous_schema_version_id?: string; + created_at: number; + is_composable: boolean; + }>(sql` + SELECT + id, + composite_schema_sdl as sdl, + previous_schema_version_id, + created_at, + is_composable + FROM schema_versions + WHERE id = ${previousVersionId} AND target_id = ${targetId} + `); + + if (!versionBefore) { + console.error( + `[ERROR] No schema found for version ${previousVersionId}. Inserting all remaining coordinates for ${targetId}`, + ); + await insertRemainingCoordinates( + connection, + targetId, + targetCoordinates, + after.versionId, + after.createdAt, + ); + return; + } + + if (!versionBefore.is_composable) { + // Skip non-composable schemas and continue with the previous version. + return processVersion(depth + 1, connection, targetCoordinates, targetId, { + schema: after.schema, + versionId: after.versionId, + createdAt: after.createdAt, + previousVersionId: versionBefore.previous_schema_version_id ?? null, + }); + } + + if (!versionBefore.sdl) { + console.error( + `[ERROR] No SDL found for version ${previousVersionId}. Inserting all remaining coordinates for ${targetId}`, + ); + await insertRemainingCoordinates( + connection, + targetId, + targetCoordinates, + after.versionId, + after.createdAt, + ); + return; + } + + const before: { + schema: GraphQLSchema; + versionId: string; + createdAt: number; + previousVersionId: string | null; + } = { + schema: buildSchema(versionBefore.sdl, { + assumeValid: true, + assumeValidSDL: true, + }), + versionId: versionBefore.id, + createdAt: versionBefore.created_at, + previousVersionId: versionBefore.previous_schema_version_id ?? null, + }; + const diff = diffSchemaCoordinates(before.schema, after.schema); + + // We don't have to track undeprecated or deleted coordinates + // as we only want to represent the current state of the schema. + const added: string[] = []; + const deprecated: string[] = []; + const deleteAdded = new Set(); + const deleteDeprecated = new Set(); + + for (const coordinate of diff.added) { + if (targetCoordinates.coordinates.has(coordinate)) { + added.push(coordinate); + // We found a schema version that added a coordinate, so we don't have to look further + deleteAdded.add(coordinate); + } + } + + for (const coordinate of diff.deprecated) { + if (targetCoordinates.deprecated.has(coordinate)) { + deprecated.push(coordinate); + deleteDeprecated.add(coordinate); + } + } + + const datePG = new Date(after.createdAt).toISOString(); + + if (added.length) { + console.log(`Adding ${added.length} coordinates for target ${targetId}`); + await connection.query(sql` + INSERT INTO schema_coordinate_status + ( target_id, coordinate, created_at, created_in_version_id ) + SELECT * FROM ${sql.unnest( + added.map(coordinate => [targetId, coordinate, datePG, after.versionId]), + ['uuid', 'text', 'date', 'uuid'], + )} + ON CONFLICT (target_id, coordinate) + DO UPDATE SET created_at = ${datePG}, created_in_version_id = ${after.versionId} + `); + // there will be a conflict, because we are going from deprecated to added order. + } + + if (deprecated.length) { + console.log(`deprecating ${deprecated.length} coordinates for target ${targetId}`); + + await connection.query(sql` + INSERT INTO schema_coordinate_status + ( target_id, coordinate, created_at, created_in_version_id, deprecated_at, deprecated_in_version_id ) + SELECT * FROM ${sql.unnest( + deprecated.map(coordinate => [ + targetId, + coordinate, + datePG, + after.versionId, + datePG, + after.versionId, + ]), + ['uuid', 'text', 'date', 'uuid', 'date', 'uuid'], + )} + ON CONFLICT (target_id, coordinate) + DO UPDATE SET deprecated_at = ${datePG}, deprecated_in_version_id = ${after.versionId} + `); + // there will be a conflict, because we are going from deprecated to added order. + } + + // Remove coordinates that were added in this diff. + // We don't need to look for them in previous versions. + for (const coordinate of deleteAdded) { + targetCoordinates.coordinates.delete(coordinate); + } + // Remove coordinates that were deprecated in this diff. + // To avoid marking them as deprecated later on. + for (const coordinate of deleteDeprecated) { + targetCoordinates.deprecated.delete(coordinate); + } + + if (deleteAdded.size) { + console.log(`Deleted ${deleteAdded.size} coordinates from the stack`); + console.log(`Coordinates in queue: ${targetCoordinates.coordinates.size}`); + } + + return processVersion(depth + 1, connection, targetCoordinates, targetId, before); +} diff --git a/packages/migrations/src/index.ts b/packages/migrations/src/index.ts index 85c036987..bb34f2779 100644 --- a/packages/migrations/src/index.ts +++ b/packages/migrations/src/index.ts @@ -1,5 +1,6 @@ #!/usr/bin/env node import { createPool } from 'slonik'; +import { schemaCoordinateStatusMigration } from './actions/2024.07.23T09.36.00.schema-cleanup-tracker'; import { migrateClickHouse } from './clickhouse'; import { createConnectionString } from './connection-string'; import { env } from './environment'; @@ -13,9 +14,21 @@ const slonik = await createPool(createConnectionString(env.postgres), { // This is used by production build of this package. // We are building a "cli" out of the package, so we need a workaround to pass the command to run. -console.log('Running the UP migrations'); +// This is only used for GraphQL Hive Cloud to perform a long running migration. +// eslint-disable-next-line no-process-env +if (process.env.SCHEMA_COORDINATE_STATUS_MIGRATION === '1') { + try { + console.log('Running the SCHEMA_COORDINATE_STATUS_MIGRATION'); + await schemaCoordinateStatusMigration(slonik); + process.exit(0); + } catch (error) { + console.error(error); + process.exit(1); + } +} try { + console.log('Running the UP migrations'); await runPGMigrations({ slonik }); if (env.clickhouse) { await migrateClickHouse(env.isClickHouseMigrator, env.isHiveCloud, env.clickhouse); diff --git a/packages/migrations/src/run-pg-migrations.ts b/packages/migrations/src/run-pg-migrations.ts index 82fac5638..2becabd6d 100644 --- a/packages/migrations/src/run-pg-migrations.ts +++ b/packages/migrations/src/run-pg-migrations.ts @@ -66,6 +66,7 @@ import migration_2024_04_09T10_10_00_check_approval_comment from './actions/2024 import migration_2024_06_11T10_10_00_ms_teams_webhook from './actions/2024.06.11T10-10-00.ms-teams-webhook'; import migration_2024_07_16T13_44_00_oidc_only_access from './actions/2024.07.16T13-44-00.oidc-only-access'; import migration_2024_07_17T00_00_00_app_deployments from './actions/2024.07.17T00-00-00.app-deployments'; +import migration_2024_07_23T_09_36_00_schema_cleanup_tracker from './actions/2024.07.23T09.36.00.schema-cleanup-tracker'; import { runMigrations } from './pg-migrator'; export const runPGMigrations = (args: { slonik: DatabasePool; runTo?: string }) => @@ -140,5 +141,6 @@ export const runPGMigrations = (args: { slonik: DatabasePool; runTo?: string }) migration_2024_06_11T10_10_00_ms_teams_webhook, migration_2024_07_16T13_44_00_oidc_only_access, migration_2024_07_17T00_00_00_app_deployments, + migration_2024_07_23T_09_36_00_schema_cleanup_tracker, ], }); diff --git a/packages/migrations/test/2024.07.23T09.36.00.schema-cleanup-tracker.test.ts b/packages/migrations/test/2024.07.23T09.36.00.schema-cleanup-tracker.test.ts new file mode 100644 index 000000000..7e6cb7185 --- /dev/null +++ b/packages/migrations/test/2024.07.23T09.36.00.schema-cleanup-tracker.test.ts @@ -0,0 +1,246 @@ +import assert from 'node:assert'; +import { describe, test } from 'node:test'; +import { sql } from 'slonik'; +import { createStorage } from '../../services/storage/src/index'; +import { initMigrationTestingEnvironment } from './utils/testkit'; + +await describe('migration: schema-cleanup-tracker', async () => { + await test('schema coordinates backfill', async () => { + const { db, runTo, complete, done, seed, connectionString } = + await initMigrationTestingEnvironment(); + const storage = await createStorage(connectionString, 1); + try { + // Run migrations all the way to the point before the one we are testing + await runTo('2024.07.17T00-00-00.app-deployments.ts'); + + // Seed the database with some data (schema_sdl, supergraph_sdl, composite_schema_sdl) + const admin = await seed.user({ + user: { + name: 'test1', + email: 'test1@test.com', + }, + }); + + const organization = await seed.organization({ + organization: { + name: 'org-1', + }, + user: admin, + }); + + const project = await seed.project({ + project: { + name: 'project-1', + type: 'SINGLE', + }, + organization, + }); + + const target = await seed.target({ + target: { + name: 'target-1', + }, + project, + }); + + async function createVersion( + schema: string, + previousSchemaVersionId: string | null, + ): Promise { + const logId = await db.oneFirst(sql` + INSERT INTO schema_log + ( + author, + service_name, + service_url, + commit, + sdl, + project_id, + target_id, + metadata, + action + ) + VALUES + ( + ${'Kamil'}, + ${null}, + ${null}, + ${'random'}, + ${schema}, + ${project.id}, + ${target.id}, + ${null}, + 'PUSH' + ) + RETURNING id + `); + + const versionId = await db.oneFirst(sql` + INSERT INTO schema_versions + ( + record_version, + is_composable, + target_id, + action_id, + base_schema, + has_persisted_schema_changes, + previous_schema_version_id, + diff_schema_version_id, + composite_schema_sdl, + supergraph_sdl, + schema_composition_errors, + github_repository, + github_sha, + tags, + has_contract_composition_errors, + conditional_breaking_change_metadata + ) + VALUES + ( + '2024-01-10', + ${true}, + ${target.id}, + ${logId}, + ${null}, + ${true}, + ${previousSchemaVersionId}, + ${previousSchemaVersionId}, + ${schema}, + ${null}, + ${null}, + ${null}, + ${null}, + ${null}, + ${false}, + ${null} + ) + RETURNING id + `); + + return versionId; + } + + const schemas = [ + // [0] + // first + /* GraphQL */ ` + type Query { + hello: String + } + `, + // [1] + // second + /* GraphQL */ ` + type Query { + hello: String + hi: String + } + `, + // [2] + // third + /* GraphQL */ ` + type Query { + hello: String + } + `, + // [3] + // fourth + /* GraphQL */ ` + type Query { + hello: String + hi: String + } + `, + // [4] + // fifth + /* GraphQL */ ` + type Query { + hello: String @deprecated(reason: "no longer needed") + bye: String + goodbye: String + hi: String @deprecated(reason: "no longer needed") + } + `, + // [5] + // sixth + /* GraphQL */ ` + type Query { + hello: String + bye: String + hi: String @deprecated(reason: "no longer needed") + } + `, + ]; + + // insert schema versions + let previousSchemaVersionId: string | null = null; + for await (const schema of schemas) { + const versionId = await createVersion(schema, previousSchemaVersionId); + previousSchemaVersionId = versionId; + } + + // Run the remaining migrations + await complete(); + + // check that coordinates are correct + + const versions = await db.manyFirst(sql` + SELECT id FROM schema_versions WHERE target_id = ${target.id} ORDER BY created_at ASC + `); + + const coordinates = await db.many<{ + coordinate: string; + created_in_version_id: string; + deprecated_in_version_id: string | null; + }>(sql` + SELECT * FROM schema_coordinate_status WHERE target_id = ${target.id} + `); + + assert.strictEqual(versions.length, 6); + + const queryType = coordinates.find(c => c.coordinate === 'Query'); + const helloField = coordinates.find(c => c.coordinate === 'Query.hello'); + const hiField = coordinates.find(c => c.coordinate === 'Query.hi'); + const byeField = coordinates.find(c => c.coordinate === 'Query.bye'); + const goodbyeField = coordinates.find(c => c.coordinate === 'Query.goodbye'); + + assert.ok(queryType, 'Query type not found'); + assert.ok(helloField, 'Query.hello field not found'); + assert.ok(hiField, 'Query.hi field not found'); + assert.ok(byeField, 'Query.bye field not found'); + + // Query + // was create in the first version + // never deprecated + assert.strictEqual(queryType.created_in_version_id, versions[0]); + assert.strictEqual(queryType.deprecated_in_version_id, null); + + // Query.hello + // was created in the first version, + // deprecated in fifth + // undeprecated in the sixth + assert.strictEqual(helloField.created_in_version_id, versions[0]); + assert.strictEqual(helloField.deprecated_in_version_id, null); + + // Query.hi + // was created in the second version + // removed in the third + // added back in the fourth + // deprecated in the fifth + assert.strictEqual(hiField.created_in_version_id, versions[3]); + assert.strictEqual(hiField.deprecated_in_version_id, versions[4]); + + // Query.bye + // was created in the fifth version + assert.strictEqual(byeField.created_in_version_id, versions[4]); + + // Query.goodbye + // was created in the fifth version + // removed in the sixth + assert.ok(!goodbyeField, 'Query.goodbye field should not be found'); + } finally { + await done(); + await storage.destroy(); + } + }); +}); diff --git a/packages/migrations/test/root.ts b/packages/migrations/test/root.ts index e8301e801..c32dadf69 100644 --- a/packages/migrations/test/root.ts +++ b/packages/migrations/test/root.ts @@ -2,3 +2,4 @@ import './2023.02.22T09.27.02.delete-personal-org.test'; import './2023.09.25T15.23.00.github-check-with-project-name.test'; import './2023.11.20T10-00-00.organization-member-roles.test'; import './2024.01.26T00.00.01.schema-check-purging.test'; +import './2024.07.23T09.36.00.schema-cleanup-tracker.test'; diff --git a/packages/services/api/package.json b/packages/services/api/package.json index 7b0901546..34f959c91 100644 --- a/packages/services/api/package.json +++ b/packages/services/api/package.json @@ -65,7 +65,7 @@ "slonik": "30.4.4", "supertokens-node": "15.2.1", "tslib": "2.6.3", - "vitest": "1.6.0", + "vitest": "2.0.3", "zod": "3.23.8", "zod-validation-error": "3.3.0" } diff --git a/packages/services/api/src/modules/schema/providers/inspector.spec.ts b/packages/services/api/src/modules/schema/providers/inspector.spec.ts new file mode 100644 index 000000000..36b498ede --- /dev/null +++ b/packages/services/api/src/modules/schema/providers/inspector.spec.ts @@ -0,0 +1,371 @@ +import 'reflect-metadata'; +import { buildSchema } from 'graphql'; +import { describe, expect, test } from 'vitest'; +import { diffSchemaCoordinates } from './inspector'; + +describe('diffSchemaCoordinates', () => { + test('should return empty arrays when schemas are equal', () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + hello: String + } + `); + + const result = diffSchemaCoordinates(schema, schema); + + expect(result).toMatchInlineSnapshot(` + { + added: Set {}, + deleted: Set {}, + deprecated: Set {}, + undeprecated: Set {}, + } + `); + }); + + test('field becomes deprecated', () => { + const before = buildSchema(/* GraphQL */ ` + type Query { + hello: String + goodbye: String + } + `); + const after = buildSchema(/* GraphQL */ ` + type Query { + hello: String @deprecated + goodbye: String + } + `); + + const result = diffSchemaCoordinates(before, after); + + expect(result).toMatchInlineSnapshot(` + { + added: Set {}, + deleted: Set {}, + deprecated: Set { + Query.hello, + }, + undeprecated: Set {}, + } + `); + }); + + test('added field is deprecated', () => { + const before = buildSchema(/* GraphQL */ ` + type Query { + hello: String + } + `); + const after = buildSchema(/* GraphQL */ ` + type Query { + hello: String + hi: String @deprecated + } + `); + + const result = diffSchemaCoordinates(before, after); + + expect(result).toMatchInlineSnapshot(` + { + added: Set { + Query.hi, + }, + deleted: Set {}, + deprecated: Set { + Query.hi, + }, + undeprecated: Set {}, + } + `); + }); + + test('field is deleted', () => { + const before = buildSchema(/* GraphQL */ ` + type Query { + goodbye: String + } + `); + const after = buildSchema(/* GraphQL */ ` + type Query { + hi: String + } + `); + + const result = diffSchemaCoordinates(before, after); + + expect(result).toMatchInlineSnapshot(` + { + added: Set { + Query.hi, + }, + deleted: Set { + Query.goodbye, + }, + deprecated: Set {}, + undeprecated: Set {}, + } + `); + }); + + test('deprecated field is deleted', () => { + const before = buildSchema(/* GraphQL */ ` + type Query { + hello: String + goodbye: String @deprecated + } + `); + const after = buildSchema(/* GraphQL */ ` + type Query { + hello: String + } + `); + + const result = diffSchemaCoordinates(before, after); + + expect(result).toMatchInlineSnapshot(` + { + added: Set {}, + deleted: Set { + Query.goodbye, + }, + deprecated: Set {}, + undeprecated: Set {}, + } + `); + }); + + test('deprecated field is undeprecated', () => { + const before = buildSchema(/* GraphQL */ ` + type Query { + hello: String + hi: String @deprecated + } + `); + const after = buildSchema(/* GraphQL */ ` + type Query { + hello: String + hi: String + } + `); + + const result = diffSchemaCoordinates(before, after); + + expect(result).toMatchInlineSnapshot(` + { + added: Set {}, + deleted: Set {}, + deprecated: Set {}, + undeprecated: Set { + Query.hi, + }, + } + `); + }); + + test('deprecated field is undeprecated, undeprecated field is deprecated', () => { + const before = buildSchema(/* GraphQL */ ` + type Query { + hello: String @deprecated(reason: "no longer needed") + bye: String + goodbye: String + hi: String @deprecated(reason: "no longer needed") + } + `); + const after = buildSchema(/* GraphQL */ ` + type Query { + hello: String + bye: String + hi: String @deprecated(reason: "no longer needed") + } + `); + + const result = diffSchemaCoordinates(before, after); + + expect(result).toMatchInlineSnapshot(` + { + added: Set {}, + deleted: Set { + Query.goodbye, + }, + deprecated: Set {}, + undeprecated: Set { + Query.hello, + }, + } + `); + }); + + test('removed a deprecated field', () => { + const before = buildSchema(/* GraphQL */ ` + type Query { + hello: String + goodbye: String @deprecated + } + `); + const after = buildSchema(/* GraphQL */ ` + type Query { + hello: String + } + `); + + const result = diffSchemaCoordinates(before, after); + + expect(result).toMatchInlineSnapshot(` + { + added: Set {}, + deleted: Set { + Query.goodbye, + }, + deprecated: Set {}, + undeprecated: Set {}, + } + `); + }); + + test('added and removed an argument on deprecated and non-deprecated fields', () => { + const before = buildSchema(/* GraphQL */ ` + type Query { + hello(lang: String): String + hi: String @deprecated + } + `); + const after = buildSchema(/* GraphQL */ ` + type Query { + hello: String @deprecated + hi(lang: String): String + } + `); + + const result = diffSchemaCoordinates(before, after); + + expect(result).toMatchInlineSnapshot(` + { + added: Set { + Query.hi.lang, + }, + deleted: Set { + Query.hello.lang, + }, + deprecated: Set { + Query.hello, + }, + undeprecated: Set { + Query.hi, + }, + } + `); + }); + + test('added removed enum members', () => { + const before = buildSchema(/* GraphQL */ ` + type Query { + hello: String + } + enum Role { + ADMIN + USER + } + `); + const after = buildSchema(/* GraphQL */ ` + type Query { + hello: String + } + enum Role { + ANONYMOUS + USER + } + `); + + const result = diffSchemaCoordinates(before, after); + + expect(result).toMatchInlineSnapshot(` + { + added: Set { + Role.ANONYMOUS, + }, + deleted: Set { + Role.ADMIN, + }, + deprecated: Set {}, + undeprecated: Set {}, + } + `); + }); + + test('added removed union members', () => { + const before = buildSchema(/* GraphQL */ ` + type Query { + hello: String + } + union Account = Admin | User + type Admin { + id: ID + } + type User { + id: ID + } + `); + const after = buildSchema(/* GraphQL */ ` + type Query { + hello: String + } + union Account = Anonymous | User + type Anonymous { + ip: String + } + type User { + id: ID + } + `); + + const result = diffSchemaCoordinates(before, after); + + expect(result).toMatchInlineSnapshot(` + { + added: Set { + Account.Anonymous, + Anonymous, + Anonymous.ip, + }, + deleted: Set { + Account.Admin, + Admin, + Admin.id, + }, + deprecated: Set {}, + undeprecated: Set {}, + } + `); + }); + + test('added removed scalars', () => { + const before = buildSchema(/* GraphQL */ ` + type Query { + hello: String + } + scalar GOODBYE + `); + const after = buildSchema(/* GraphQL */ ` + type Query { + hello: String + } + scalar HELLO + `); + + const result = diffSchemaCoordinates(before, after); + + expect(result).toMatchInlineSnapshot(` + { + added: Set { + HELLO, + }, + deleted: Set { + GOODBYE, + }, + deprecated: Set {}, + undeprecated: Set {}, + } + `); + }); +}); diff --git a/packages/services/api/src/modules/schema/providers/inspector.ts b/packages/services/api/src/modules/schema/providers/inspector.ts index ae8178b77..b0037768a 100644 --- a/packages/services/api/src/modules/schema/providers/inspector.ts +++ b/packages/services/api/src/modules/schema/providers/inspector.ts @@ -1,8 +1,18 @@ -import { type GraphQLSchema } from 'graphql'; +import { + GraphQLFieldMap, + isEnumType, + isInputObjectType, + isInterfaceType, + isIntrospectionType, + isObjectType, + isScalarType, + isUnionType, + type GraphQLSchema, +} from 'graphql'; import { Injectable, Scope } from 'graphql-modules'; import { Change, ChangeType, diff } from '@graphql-inspector/core'; import { traceFn } from '@hive/service-common'; -import { HiveSchemaChangeModel, SchemaChangeType } from '@hive/storage'; +import { HiveSchemaChangeModel } from '@hive/storage'; import { Logger } from '../../shared/providers/logger'; @Injectable({ @@ -21,7 +31,7 @@ export class Inspector { 'hive.diff.changes.count': result.length, }), }) - async diff(existing: GraphQLSchema, incoming: GraphQLSchema): Promise> { + async diff(existing: GraphQLSchema, incoming: GraphQLSchema) { this.logger.debug('Comparing Schemas'); const changes = await diff(existing, incoming); @@ -120,3 +130,130 @@ function matchChange( return pattern[change.type]?.(change); } } + +export type SchemaCoordinatesDiffResult = { + /** + * Coordinates that are in incoming but not in existing (including deprecated ones) + */ + added: Set; + /** + * Coordinates that are in existing but not in incoming (including deprecated ones) + */ + deleted: Set; + /** + * Coordinates that are deprecated in incoming, but were not deprecated in existing or non-existent + */ + deprecated: Set; + /** + * Coordinates that exists in incoming and are not deprecated in incoming, but were deprecated in existing + */ + undeprecated: Set; +}; + +export function diffSchemaCoordinates( + existingSchema: GraphQLSchema | null, + incomingSchema: GraphQLSchema, +): SchemaCoordinatesDiffResult { + const before = existingSchema + ? getSchemaCoordinates(existingSchema) + : { coordinates: new Set(), deprecated: new Set() }; + const after = getSchemaCoordinates(incomingSchema); + + const added = after.coordinates.difference(before.coordinates); + const deleted = before.coordinates.difference(after.coordinates); + const deprecated = after.deprecated.difference(before.deprecated); + const undeprecated = before.deprecated + .difference(after.deprecated) + .intersection(after.coordinates); + + return { + added, + deleted, + deprecated, + undeprecated, + }; +} + +export function getSchemaCoordinates(schema: GraphQLSchema): { + coordinates: Set; + deprecated: Set; +} { + const coordinates = new Set(); + const deprecated = new Set(); + + const typeMap = schema.getTypeMap(); + + for (const typeName in typeMap) { + const typeDefinition = typeMap[typeName]; + + if (isIntrospectionType(typeDefinition)) { + continue; + } + + coordinates.add(typeName); + + if (isObjectType(typeDefinition) || isInterfaceType(typeDefinition)) { + visitSchemaCoordinatesOfGraphQLFieldMap( + typeName, + typeDefinition.getFields(), + coordinates, + deprecated, + ); + } else if (isInputObjectType(typeDefinition)) { + const fieldMap = typeDefinition.getFields(); + for (const fieldName in fieldMap) { + const fieldDefinition = fieldMap[fieldName]; + + coordinates.add(`${typeName}.${fieldName}`); + if (fieldDefinition.deprecationReason) { + deprecated.add(`${typeName}.${fieldName}`); + } + } + } else if (isUnionType(typeDefinition)) { + coordinates.add(typeName); + for (const member of typeDefinition.getTypes()) { + coordinates.add(`${typeName}.${member.name}`); + } + } else if (isEnumType(typeDefinition)) { + const values = typeDefinition.getValues(); + for (const value of values) { + coordinates.add(`${typeName}.${value.name}`); + if (value.deprecationReason) { + deprecated.add(`${typeName}.${value.name}`); + } + } + } else if (isScalarType(typeDefinition)) { + coordinates.add(typeName); + } else { + throw new Error(`Unsupported type kind ${typeName}`); + } + } + + return { + coordinates, + deprecated, + }; +} + +function visitSchemaCoordinatesOfGraphQLFieldMap( + typeName: string, + fieldMap: GraphQLFieldMap, + coordinates: Set, + deprecated: Set, +) { + for (const fieldName in fieldMap) { + const fieldDefinition = fieldMap[fieldName]; + + coordinates.add(`${typeName}.${fieldName}`); + if (fieldDefinition.deprecationReason) { + deprecated.add(`${typeName}.${fieldName}`); + } + + for (const arg of fieldDefinition.args) { + coordinates.add(`${typeName}.${fieldName}.${arg.name}`); + if (arg.deprecationReason) { + deprecated.add(`${typeName}.${fieldName}.${arg.name}`); + } + } + } +} diff --git a/packages/services/api/src/modules/schema/providers/models/composite-legacy.ts b/packages/services/api/src/modules/schema/providers/models/composite-legacy.ts index 6f4f7dab0..627adbef8 100644 --- a/packages/services/api/src/modules/schema/providers/models/composite-legacy.ts +++ b/packages/services/api/src/modules/schema/providers/models/composite-legacy.ts @@ -347,6 +347,11 @@ export class CompositeLegacyModel { messages, changes, breakingChanges: breakingChanges ?? null, + coordinatesDiff: + diffCheck.result?.coordinatesDiff ?? + diffCheck.reason?.coordinatesDiff ?? + diffCheck.data?.coordinatesDiff ?? + null, compositionErrors, schema: incoming, schemas, @@ -372,6 +377,7 @@ export class CompositeLegacyModel { code: PublishFailureReasonCode.BreakingChanges, changes: diffCheck.reason.all ?? [], breakingChanges: diffCheck.reason.breaking ?? [], + coordinatesDiff: diffCheck.reason?.coordinatesDiff ?? null, }); } diff --git a/packages/services/api/src/modules/schema/providers/models/composite.ts b/packages/services/api/src/modules/schema/providers/models/composite.ts index df7909ec0..8df5e75e9 100644 --- a/packages/services/api/src/modules/schema/providers/models/composite.ts +++ b/packages/services/api/src/modules/schema/providers/models/composite.ts @@ -483,6 +483,11 @@ export class CompositeModel { composable: compositionCheck.status === 'completed', initial: latestVersion === null, changes: diffCheck.result?.all ?? diffCheck.reason?.all ?? null, + coordinatesDiff: + diffCheck.result?.coordinatesDiff ?? + diffCheck.reason?.coordinatesDiff ?? + diffCheck.data?.coordinatesDiff ?? + null, messages, breakingChanges: null, compositionErrors: compositionCheck.reason?.errors ?? null, @@ -688,6 +693,11 @@ export class CompositeModel { ...composablePartial, changes, breakingChanges, + coordinatesDiff: + diffCheck.result?.coordinatesDiff ?? + diffCheck.reason?.coordinatesDiff ?? + diffCheck.data?.coordinatesDiff ?? + null, compositionErrors: compositionCheck.reason?.errors ?? [], supergraph: compositionCheck.result?.supergraph ?? null, tags: compositionCheck.result?.tags ?? null, diff --git a/packages/services/api/src/modules/schema/providers/models/shared.ts b/packages/services/api/src/modules/schema/providers/models/shared.ts index 73677b4c9..cc58beede 100644 --- a/packages/services/api/src/modules/schema/providers/models/shared.ts +++ b/packages/services/api/src/modules/schema/providers/models/shared.ts @@ -2,14 +2,15 @@ import { PushedCompositeSchema, SingleSchema } from 'packages/services/api/src/s import type { CheckPolicyResponse } from '@hive/policy'; import { CompositionFailureError } from '@hive/schema'; import type { SchemaChangeType, SchemaCompositionError } from '@hive/storage'; -import { type Contract, type ValidContractVersion } from '../contracts'; -import { +import type { Contract, ValidContractVersion } from '../contracts'; +import type { SchemaCoordinatesDiffResult } from '../inspector'; +import type { ContractCompositionResult, ContractCompositionSuccess, + RegistryChecks, SchemaDiffResult, SchemaDiffSkip, SchemaDiffSuccess, - type RegistryChecks, } from '../registry-checks'; export const SchemaPublishConclusion = { @@ -207,6 +208,7 @@ export type SchemaPublishFailureReason = code: (typeof PublishFailureReasonCode)['BreakingChanges']; breakingChanges: Array; changes: Array; + coordinatesDiff: SchemaCoordinatesDiffResult; }; type ContractResult = { @@ -223,6 +225,7 @@ type SchemaPublishSuccess = { state: { composable: boolean; initial: boolean; + coordinatesDiff: SchemaCoordinatesDiffResult | null; changes: Array | null; messages: string[] | null; breakingChanges: Array<{ @@ -277,6 +280,7 @@ export type SchemaDeleteSuccess = { schemas: PushedCompositeSchema[]; breakingChanges: Array | null; compositionErrors: Array | null; + coordinatesDiff: SchemaCoordinatesDiffResult | null; supergraph: string | null; tags: null | Array; contracts: null | Array; diff --git a/packages/services/api/src/modules/schema/providers/models/single-legacy.ts b/packages/services/api/src/modules/schema/providers/models/single-legacy.ts index 09d17550e..507c3f235 100644 --- a/packages/services/api/src/modules/schema/providers/models/single-legacy.ts +++ b/packages/services/api/src/modules/schema/providers/models/single-legacy.ts @@ -279,6 +279,11 @@ export class SingleLegacyModel { messages, changes, breakingChanges: breakingChanges ?? null, + coordinatesDiff: + diffCheck.result?.coordinatesDiff ?? + diffCheck.reason?.coordinatesDiff ?? + diffCheck.data?.coordinatesDiff ?? + null, compositionErrors, schema: incoming, schemas, @@ -304,6 +309,7 @@ export class SingleLegacyModel { code: PublishFailureReasonCode.BreakingChanges, changes: diffCheck.reason.all ?? [], breakingChanges: diffCheck.reason.breaking ?? [], + coordinatesDiff: diffCheck.reason?.coordinatesDiff ?? null, }); } diff --git a/packages/services/api/src/modules/schema/providers/models/single.ts b/packages/services/api/src/modules/schema/providers/models/single.ts index c2282156e..b4a27dcab 100644 --- a/packages/services/api/src/modules/schema/providers/models/single.ts +++ b/packages/services/api/src/modules/schema/providers/models/single.ts @@ -307,6 +307,11 @@ export class SingleModel { changes: diffCheck.result?.all ?? diffCheck.reason?.all ?? null, messages, breakingChanges: null, + coordinatesDiff: + diffCheck.result?.coordinatesDiff ?? + diffCheck.reason?.coordinatesDiff ?? + diffCheck.data?.coordinatesDiff ?? + null, compositionErrors: compositionCheck.reason?.errors ?? null, schema: incoming, schemas, diff --git a/packages/services/api/src/modules/schema/providers/registry-checks.ts b/packages/services/api/src/modules/schema/providers/registry-checks.ts index e35602ee1..122ed6f83 100644 --- a/packages/services/api/src/modules/schema/providers/registry-checks.ts +++ b/packages/services/api/src/modules/schema/providers/registry-checks.ts @@ -24,7 +24,7 @@ import type { SingleSchema, } from './../../../shared/entities'; import { Logger } from './../../shared/providers/logger'; -import { Inspector } from './inspector'; +import { diffSchemaCoordinates, Inspector, SchemaCoordinatesDiffResult } from './inspector'; import { SchemaCheckWarning } from './models/shared'; import { extendWithBase, isCompositeSchema, SchemaHelper } from './schema-helper'; @@ -37,7 +37,7 @@ export type ConditionalBreakingChangeDiffConfig = { // The reason why I'm using `result` and `reason` instead of just `data` for both: // https://bit.ly/hive-check-result-data -export type CheckResult = +export type CheckResult = | { status: 'completed'; result: C; @@ -48,6 +48,7 @@ export type CheckResult = } | { status: 'skipped'; + data?: S; }; type Schemas = [SingleSchema] | PushedCompositeSchema[]; @@ -134,6 +135,7 @@ type SchemaDiffFailure = { breaking: Array | null; safe: Array | null; all: Array | null; + coordinatesDiff: SchemaCoordinatesDiffResult | null; }; result?: never; }; @@ -144,6 +146,7 @@ export type SchemaDiffSuccess = { breaking: Array | null; safe: Array | null; all: Array | null; + coordinatesDiff: SchemaCoordinatesDiffResult | null; }; reason?: never; }; @@ -412,32 +415,39 @@ export class RegistryChecks { /** Settings for fetching conditional breaking changes. */ conditionalBreakingChangeConfig: null | ConditionalBreakingChangeDiffConfig; }) { - if (args.existingSdl == null || args.incomingSdl == null) { - this.logger.debug('Skip diff check due to either existing or incoming SDL being absent.'); + let existingSchema: GraphQLSchema | null = null; + let incomingSchema: GraphQLSchema | null = null; + + try { + existingSchema = args.existingSdl + ? buildSortedSchemaFromSchemaObject( + this.helper.createSchemaObject({ + sdl: args.existingSdl, + }), + ) + : null; + + incomingSchema = args.incomingSdl + ? buildSortedSchemaFromSchemaObject( + this.helper.createSchemaObject({ + sdl: args.incomingSdl, + }), + ) + : null; + } catch (error) { + this.logger.error('Failed to build schema for diff. Skip diff check.'); return { status: 'skipped', } satisfies CheckResult; } - let existingSchema: GraphQLSchema; - let incomingSchema: GraphQLSchema; - - try { - existingSchema = buildSortedSchemaFromSchemaObject( - this.helper.createSchemaObject({ - sdl: args.existingSdl, - }), - ); - - incomingSchema = buildSortedSchemaFromSchemaObject( - this.helper.createSchemaObject({ - sdl: args.incomingSdl, - }), - ); - } catch (error) { - this.logger.error('Failed to build schema for diff. Skip diff check.'); + if (existingSchema === null || incomingSchema === null) { + this.logger.debug('Skip diff check due to either existing or incoming SDL being absent.'); return { status: 'skipped', + data: { + coordinatesDiff: incomingSchema ? diffSchemaCoordinates(null, incomingSchema) : null, + }, } satisfies CheckResult; } @@ -511,6 +521,8 @@ export class RegistryChecks { const safeChanges: Array = []; const breakingChanges: Array = []; + const coordinatesDiff = diffSchemaCoordinates(existingSchema, incomingSchema); + for (const change of inspectorChanges) { if (change.criticality === CriticalityLevel.Breaking) { if (change.isSafeBasedOnUsage === true) { @@ -549,6 +561,7 @@ export class RegistryChecks { } return null; }, + coordinatesDiff, }, } satisfies SchemaDiffFailure; } @@ -568,6 +581,7 @@ export class RegistryChecks { } return null; }, + coordinatesDiff, }, } satisfies SchemaDiffSuccess; } diff --git a/packages/services/api/src/modules/schema/providers/schema-manager.ts b/packages/services/api/src/modules/schema/providers/schema-manager.ts index 3ce58db27..046517ad7 100644 --- a/packages/services/api/src/modules/schema/providers/schema-manager.ts +++ b/packages/services/api/src/modules/schema/providers/schema-manager.ts @@ -40,6 +40,7 @@ import { TargetManager } from '../../target/providers/target-manager'; import { BreakingSchemaChangeUsageHelper } from './breaking-schema-changes-helper'; import { SCHEMA_MODULE_CONFIG, type SchemaModuleConfig } from './config'; import { Contracts } from './contracts'; +import type { SchemaCoordinatesDiffResult } from './inspector'; import { FederationOrchestrator } from './orchestrators/federation'; import { SingleOrchestrator } from './orchestrators/single'; import { StitchingOrchestrator } from './orchestrators/stitching'; @@ -427,6 +428,7 @@ export class SchemaManager { projectType: ProjectType; actionFn(): Promise; changes: Array; + coordinatesDiff: SchemaCoordinatesDiffResult | null; previousSchemaVersion: string | null; diffSchemaVersionId: string | null; github: null | { @@ -460,13 +462,18 @@ export class SchemaManager { ) { this.logger.info( 'Creating a new version (input=%o)', - lodash.omit(input, [ - 'schema', - 'actionFn', - 'changes', - 'compositeSchemaSDL', - 'supergraphSDL', - 'schemaCompositionErrors', + lodash.pick(input, [ + 'commit', + 'author', + 'valid', + 'service', + 'logIds', + 'url', + 'projectType', + 'previousSchemaVersion', + 'diffSchemaVersionId', + 'github', + 'conditionalBreakingChangeMetadata', ]), ); diff --git a/packages/services/api/src/modules/schema/providers/schema-publisher.ts b/packages/services/api/src/modules/schema/providers/schema-publisher.ts index c2b4d20d7..e632636ca 100644 --- a/packages/services/api/src/modules/schema/providers/schema-publisher.ts +++ b/packages/services/api/src/modules/schema/providers/schema-publisher.ts @@ -1028,7 +1028,14 @@ export class SchemaPublisher { ); const token = this.authManager.ensureApiToken(); - const contracts = await this.contracts.getActiveContractsByTargetId({ targetId: input.target }); + const [contracts, latestVersion] = await Promise.all([ + this.contracts.getActiveContractsByTargetId({ targetId: input.target }), + this.schemaManager.getMaybeLatestVersion({ + organization: input.organization, + project: input.project, + target: input.target, + }), + ]); const checksum = createHash('md5') .update( @@ -1042,6 +1049,10 @@ export class SchemaPublisher { contractId: contract.id, contractName: contract.contractName, })), + // We include the latest version ID to avoid caching a schema publication that targets different versions. + // When deleting a schema, and publishing it again, the latest version ID will be different. + // If we don't include it, the cache will return the previous result. + latestVersionId: latestVersion?.id, }), ) .update(token) @@ -1335,6 +1346,7 @@ export class SchemaPublisher { composable: deleteResult.state.composable, diffSchemaVersionId, changes: deleteResult.state.changes, + coordinatesDiff: deleteResult.state.coordinatesDiff, contracts: deleteResult.state.contracts?.map(contract => ({ contractId: contract.contractId, @@ -1909,6 +1921,7 @@ export class SchemaPublisher { } }, changes, + coordinatesDiff: publishResult.state.coordinatesDiff, diffSchemaVersionId, previousSchemaVersion: latestVersion?.version ?? null, conditionalBreakingChangeMetadata: await this.getConditionalBreakingChangeMetadata({ diff --git a/packages/services/api/src/modules/shared/providers/storage.ts b/packages/services/api/src/modules/shared/providers/storage.ts index 1b82ab401..d5986cb59 100644 --- a/packages/services/api/src/modules/shared/providers/storage.ts +++ b/packages/services/api/src/modules/shared/providers/storage.ts @@ -44,6 +44,7 @@ import type { OrganizationAccessScope } from '../../auth/providers/organization- import type { ProjectAccessScope } from '../../auth/providers/project-access'; import type { TargetAccessScope } from '../../auth/providers/target-access'; import type { Contracts } from '../../schema/providers/contracts'; +import type { SchemaCoordinatesDiffResult } from '../../schema/providers/inspector'; export interface OrganizationSelector { organization: string; @@ -442,6 +443,7 @@ export interface Storage { diffSchemaVersionId: string | null; conditionalBreakingChangeMetadata: null | ConditionalBreakingChangeMetadata; contracts: null | Array; + coordinatesDiff: SchemaCoordinatesDiffResult | null; } & TargetSelector & ( | { @@ -480,6 +482,7 @@ export interface Storage { }; contracts: null | Array; conditionalBreakingChangeMetadata: null | ConditionalBreakingChangeMetadata; + coordinatesDiff: SchemaCoordinatesDiffResult | null; } & TargetSelector) & ( | { diff --git a/packages/services/broker-worker/package.json b/packages/services/broker-worker/package.json index c2688b270..fef26ba7a 100644 --- a/packages/services/broker-worker/package.json +++ b/packages/services/broker-worker/package.json @@ -16,7 +16,7 @@ "itty-router": "4.2.2", "toucan-js": "3.4.0", "undici": "6.19.2", - "vitest": "1.6.0", + "vitest": "2.0.3", "workers-loki-logger": "0.1.15", "zod": "3.23.8" } diff --git a/packages/services/storage/src/db/types.ts b/packages/services/storage/src/db/types.ts index 0820c7349..8fca2bf27 100644 --- a/packages/services/storage/src/db/types.ts +++ b/packages/services/storage/src/db/types.ts @@ -271,6 +271,15 @@ export interface schema_checks { updated_at: Date; } +export interface schema_coordinate_status { + coordinate: string; + created_at: Date; + created_in_version_id: string; + deprecated_at: Date | null; + deprecated_in_version_id: string | null; + target_id: string; +} + export interface schema_log { action: string; author: string; @@ -418,6 +427,7 @@ export interface DBTables { projects: projects; schema_change_approvals: schema_change_approvals; schema_checks: schema_checks; + schema_coordinate_status: schema_coordinate_status; schema_log: schema_log; schema_policy_config: schema_policy_config; schema_version_changes: schema_version_changes; diff --git a/packages/services/storage/src/index.ts b/packages/services/storage/src/index.ts index 305afeccf..1eae543d5 100644 --- a/packages/services/storage/src/index.ts +++ b/packages/services/storage/src/index.ts @@ -25,6 +25,7 @@ import type { } from '@hive/api'; import { context, SpanKind, SpanStatusCode, trace } from '@hive/service-common'; import { batch } from '@theguild/buddy'; +import type { SchemaCoordinatesDiffResult } from '../../api/src/modules/schema/providers/inspector'; import { createSDLHash, OrganizationMemberRoleModel, @@ -2644,6 +2645,14 @@ export async function createStorage( }); } + if (args.coordinatesDiff) { + await updateSchemaCoordinateStatus(trx, { + targetId: args.target, + versionId: newVersion.id, + coordinatesDiff: args.coordinatesDiff, + }); + } + for (const contract of args.contracts ?? []) { const schemaVersionContractId = await insertSchemaVersionContract(trx, { schemaVersionId: newVersion.id, @@ -2756,6 +2765,14 @@ export async function createStorage( }); } + if (input.coordinatesDiff) { + await updateSchemaCoordinateStatus(trx, { + targetId: input.target, + versionId: version.id, + coordinatesDiff: input.coordinatesDiff, + }); + } + await input.actionFn(); return { @@ -5137,6 +5154,80 @@ async function insertSchemaVersionContract( return zod.string().parse(id); } +async function updateSchemaCoordinateStatus( + trx: DatabaseTransactionConnection, + args: { + targetId: string; + versionId: string; + coordinatesDiff: SchemaCoordinatesDiffResult; + }, +) { + const actions: Promise[] = []; + + if (args.coordinatesDiff.deleted) { + actions.push( + trx.query(sql`/* schema_coordinate_status_deleted */ + DELETE FROM schema_coordinate_status + WHERE + target_id = ${args.targetId} + AND + coordinate = ANY(${sql.array(Array.from(args.coordinatesDiff.deleted), 'text')}) + AND + created_at <= NOW() + `), + ); + } + + if (args.coordinatesDiff.added) { + actions.push( + trx.query(sql`/* schema_coordinate_status_inserted */ + INSERT INTO schema_coordinate_status + ( target_id, coordinate, created_in_version_id, deprecated_at, deprecated_in_version_id ) + SELECT * FROM ${sql.unnest( + Array.from(args.coordinatesDiff.added).map(coordinate => { + const isDeprecatedAsWell = args.coordinatesDiff.deprecated.has(coordinate); + return [ + args.targetId, + coordinate, + args.versionId, + // if it's added and deprecated at the same time + isDeprecatedAsWell ? 'NOW()' : null, + isDeprecatedAsWell ? args.versionId : null, + ]; + }), + ['uuid', 'text', 'uuid', 'date', 'uuid'], + )} + `), + ); + } + + if (args.coordinatesDiff.undeprecated) { + actions.push( + trx.query(sql`/* schema_coordinate_status_undeprecated */ + UPDATE schema_coordinate_status + SET deprecated_at = NULL, deprecated_in_version_id = NULL + WHERE + target_id = ${args.targetId} + AND + coordinate = ANY(${sql.array(Array.from(args.coordinatesDiff.undeprecated), 'text')}) + `), + ); + } + + await Promise.all(actions); + + if (args.coordinatesDiff.deprecated) { + await trx.query(sql`/* schema_coordinate_status_deprecated */ + UPDATE schema_coordinate_status + SET deprecated_at = NOW(), deprecated_in_version_id = ${args.versionId} + WHERE + target_id = ${args.targetId} + AND + coordinate = ANY(${sql.array(Array.from(args.coordinatesDiff.deprecated), 'text')}) + `); + } +} + /** * Small helper utility for jsonifying a nullable object. */ diff --git a/packages/web/app/src/components/schema-editor.ts b/packages/web/app/src/components/schema-editor.ts index 35d0ffddb..fde19d601 100644 --- a/packages/web/app/src/components/schema-editor.ts +++ b/packages/web/app/src/components/schema-editor.ts @@ -4,7 +4,7 @@ import { DiffEditor as MonacoDiffEditor, Editor as MonacoEditor, } from '@monaco-editor/react'; -import pkg from '../../package.json' assert { type: 'json' }; +import pkg from '../../package.json' with { type: 'json' }; loader.config({ paths: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48b29caea..c5b5a0f33 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -159,7 +159,7 @@ importers: version: 0.18.1(prettier@3.3.3) prettier-plugin-tailwindcss: specifier: 0.6.5 - version: 0.6.5(@ianvs/prettier-plugin-sort-imports@4.2.1(prettier@3.3.3))(prettier@3.3.3) + version: 0.6.5(@ianvs/prettier-plugin-sort-imports@4.3.1(prettier@3.3.3))(prettier@3.3.3) pretty-quick: specifier: 4.0.0 version: 4.0.0(prettier@3.3.3) @@ -185,8 +185,8 @@ importers: specifier: 4.3.2 version: 4.3.2(typescript@5.5.3)(vite@5.3.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1)) vitest: - specifier: 1.6.0 - version: 1.6.0(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) + specifier: 2.0.3 + version: 2.0.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) deployment: dependencies: @@ -333,8 +333,8 @@ importers: specifier: 2.6.3 version: 2.6.3 vitest: - specifier: 1.6.0 - version: 1.6.0(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) + specifier: 2.0.3 + version: 2.0.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) zod: specifier: 3.23.8 version: 3.23.8 @@ -367,8 +367,8 @@ importers: specifier: 14.0.0-beta.7 version: 14.0.0-beta.7 vitest: - specifier: 1.6.0 - version: 1.6.0(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) + specifier: 2.0.3 + version: 2.0.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) ws: specifier: 8.18.0 version: 8.18.0 @@ -496,8 +496,8 @@ importers: specifier: 2.6.3 version: 2.6.3 vitest: - specifier: 1.6.0 - version: 1.6.0(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) + specifier: 2.0.3 + version: 2.0.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) publishDirectory: dist packages/libraries/envelop: @@ -573,8 +573,8 @@ importers: specifier: 14.0.0-beta.7 version: 14.0.0-beta.7 vitest: - specifier: 1.6.0 - version: 1.6.0(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) + specifier: 2.0.3 + version: 2.0.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) ws: specifier: 8.18.0 version: 8.18.0 @@ -612,6 +612,9 @@ importers: got: specifier: 14.4.1 version: 14.4.1(patch_hash=b6pwqmrs3qqykctltsasvrfwti) + graphql: + specifier: 16.9.0 + version: 16.9.0 p-limit: specifier: 4.0.0 version: 4.0.0 @@ -800,8 +803,8 @@ importers: specifier: 2.6.3 version: 2.6.3 vitest: - specifier: 1.6.0 - version: 1.6.0(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) + specifier: 2.0.3 + version: 2.0.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) zod: specifier: 3.23.8 version: 3.23.8 @@ -833,8 +836,8 @@ importers: specifier: 6.19.2 version: 6.19.2 vitest: - specifier: 1.6.0 - version: 1.6.0(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) + specifier: 2.0.3 + version: 2.0.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) workers-loki-logger: specifier: 0.1.15 version: 0.1.15 @@ -1744,7 +1747,7 @@ importers: version: 8.2.2(storybook@8.2.2(@babel/preset-env@7.24.5(@babel/core@7.24.7))) '@storybook/addon-interactions': specifier: 8.2.2 - version: 8.2.2(storybook@8.2.2(@babel/preset-env@7.24.5(@babel/core@7.24.7)))(vitest@1.6.0(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1)) + version: 8.2.2(storybook@8.2.2(@babel/preset-env@7.24.5(@babel/core@7.24.7)))(vitest@2.0.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1)) '@storybook/addon-links': specifier: 8.2.2 version: 8.2.2(react@18.3.1)(storybook@8.2.2(@babel/preset-env@7.24.5(@babel/core@7.24.7))) @@ -4021,6 +4024,7 @@ packages: '@fastify/vite@6.0.7': resolution: {integrity: sha512-+dRo9KUkvmbqdmBskG02SwigWl06Mwkw8SBDK1zTNH6vd4DyXbRvI7RmJEmBkLouSU81KTzy1+OzwHSffqSD6w==} + bundledDependencies: [] '@floating-ui/core@1.2.6': resolution: {integrity: sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==} @@ -4735,6 +4739,15 @@ packages: '@vue/compiler-sfc': optional: true + '@ianvs/prettier-plugin-sort-imports@4.3.1': + resolution: {integrity: sha512-ZHwbyjkANZOjaBm3ZosADD2OUYGFzQGxfy67HmGZU94mHqe7g1LCMA7YYKB1Cq+UTPCBqlAYapY0KXAjKEw8Sg==} + peerDependencies: + '@vue/compiler-sfc': 2.7.x || 3.x + prettier: 2 || 3 + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + '@inquirer/confirm@3.1.8': resolution: {integrity: sha512-f3INZ+ca4dQdn+MQiq1yP/mOIR/Oc8BLRYuDh6ciToWd6z4W8yArfzjBCMQ0BPY8PcJKwZxGIt8Z6yNT32eSTw==} engines: {node: '>=18'} @@ -4820,10 +4833,6 @@ packages: resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.1.2': - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} - engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} @@ -8150,18 +8159,30 @@ packages: '@vitest/expect@1.6.0': resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} - '@vitest/runner@1.6.0': - resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==} + '@vitest/expect@2.0.3': + resolution: {integrity: sha512-X6AepoOYePM0lDNUPsGXTxgXZAl3EXd0GYe/MZyVE4HzkUqyUVC6S3PrY5mClDJ6/7/7vALLMV3+xD/Ko60Hqg==} - '@vitest/snapshot@1.6.0': - resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==} + '@vitest/pretty-format@2.0.3': + resolution: {integrity: sha512-URM4GLsB2xD37nnTyvf6kfObFafxmycCL8un3OC9gaCs5cti2u+5rJdIflZ2fUJUen4NbvF6jCufwViAFLvz1g==} + + '@vitest/runner@2.0.3': + resolution: {integrity: sha512-EmSP4mcjYhAcuBWwqgpjR3FYVeiA4ROzRunqKltWjBfLNs1tnMLtF+qtgd5ClTwkDP6/DGlKJTNa6WxNK0bNYQ==} + + '@vitest/snapshot@2.0.3': + resolution: {integrity: sha512-6OyA6v65Oe3tTzoSuRPcU6kh9m+mPL1vQ2jDlPdn9IQoUxl8rXhBnfICNOC+vwxWY684Vt5UPgtcA2aPFBb6wg==} '@vitest/spy@1.6.0': resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==} + '@vitest/spy@2.0.3': + resolution: {integrity: sha512-sfqyAw/ypOXlaj4S+w8689qKM1OyPOqnonqOc9T91DsoHbfN5mU7FdifWWv3MtQFf0lEUstEwR9L/q/M390C+A==} + '@vitest/utils@1.6.0': resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} + '@vitest/utils@2.0.3': + resolution: {integrity: sha512-c/UdELMuHitQbbc/EVctlBaxoYAwQPQdSNwv7z/vHyBKy2edYZaFgptE27BRueZB7eW8po+cllotMNTDpL3HWg==} + '@webassemblyjs/ast@1.12.1': resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} @@ -8518,6 +8539,10 @@ packages: assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -8880,6 +8905,10 @@ packages: resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} engines: {node: '>=4'} + chai@5.1.1: + resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} + engines: {node: '>=12'} + chalk@2.3.0: resolution: {integrity: sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==} engines: {node: '>=4'} @@ -8936,6 +8965,10 @@ packages: check-error@1.0.3: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + check-more-types@2.24.0: resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} engines: {node: '>= 0.8.0'} @@ -9628,6 +9661,10 @@ packages: resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} engines: {node: '>=6'} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-equal@2.1.0: resolution: {integrity: sha512-2pxgvWu3Alv1PoWEyVg7HS8YhGlUFUV7N5oOvfL6d+7xAmLSemMwv/c8Zv/i9KFzxV5Kt5CAvQc70fLwVuf4UA==} @@ -11848,9 +11885,6 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-tokens@8.0.3: - resolution: {integrity: sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==} - js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true @@ -11953,9 +11987,6 @@ packages: resolution: {integrity: sha512-qCRJWlbP2v6HbmKW7R3lFbeiVWHo+oMJ0j+MizwvauqnCV/EvtAeEeuCgoc/ErtsuoKgYB8U4Ih8AxJbXoE6/g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - jsonc-parser@3.2.0: - resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} - jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -12159,10 +12190,6 @@ packages: resolution: {integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==} engines: {node: '>=4.0.0'} - local-pkg@0.5.0: - resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} - engines: {node: '>=14'} - localforage@1.10.0: resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} @@ -12262,6 +12289,9 @@ packages: loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + loupe@3.1.1: + resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} + lower-case-first@2.0.2: resolution: {integrity: sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==} @@ -12321,6 +12351,9 @@ packages: resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} engines: {node: '>=12'} + magic-string@0.30.10: + resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + magic-string@0.30.5: resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} engines: {node: '>=12'} @@ -12940,9 +12973,6 @@ packages: engines: {node: '>=10'} hasBin: true - mlly@1.4.2: - resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==} - mnemonist@0.39.6: resolution: {integrity: sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==} @@ -13419,10 +13449,6 @@ packages: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-limit@5.0.0: - resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} - engines: {node: '>=18'} - p-limit@6.1.0: resolution: {integrity: sha512-H0jc0q1vOzlEk0TqAKXKZxdl7kX3OFUzCnNVUnq5Pc3DGo0kpeaMuPqxQn235HibwBEb0/pm9dgKTjXy66fBkg==} engines: {node: '>=18'} @@ -13626,9 +13652,16 @@ packages: pathe@1.1.1: resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathval@1.1.1: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -13768,9 +13801,6 @@ packages: resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} engines: {node: '>=14.16'} - pkg-types@1.0.3: - resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} - pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -15228,8 +15258,8 @@ packages: std-env@3.3.3: resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==} - std-env@3.6.0: - resolution: {integrity: sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==} + std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} storybook@8.2.2: resolution: {integrity: sha512-xDT9gyzAEFQNeK7P+Mj/8bNzN+fbm6/4D6ihdSzmczayjydpNjMs74HDHMY6S4Bfu6tRVyEK2ALPGnr6ZVofBA==} @@ -15338,9 +15368,6 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strip-literal@2.0.0: - resolution: {integrity: sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==} - stripe@14.25.0: resolution: {integrity: sha512-wQS3GNMofCXwH8TSje8E1SE8zr6ODiGtHQgPtO95p9Mb4FhKC9jvXR2NUTpZ9ZINlckJcFidCmaTFV4P6vsb9g==} engines: {node: '>=12.*'} @@ -15580,17 +15607,25 @@ packages: tiny-warning@1.0.3: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} - tinybench@2.5.1: - resolution: {integrity: sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==} + tinybench@2.8.0: + resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==} - tinypool@0.8.3: - resolution: {integrity: sha512-Ud7uepAklqRH1bvwy22ynrliC7Dljz7Tm8M/0RBUW+YRa4YHhZ6e4PpgE+fu1zr/WqB1kbeuVrdfeuyIBpy4tw==} + tinypool@1.0.0: + resolution: {integrity: sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} engines: {node: '>=14.0.0'} tinyspy@2.2.0: resolution: {integrity: sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==} engines: {node: '>=14.0.0'} + tinyspy@3.0.0: + resolution: {integrity: sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==} + engines: {node: '>=14.0.0'} + title-case@3.0.3: resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} @@ -15901,9 +15936,6 @@ packages: uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - ufo@1.3.0: - resolution: {integrity: sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==} - uglify-js@3.17.4: resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} engines: {node: '>=0.8.0'} @@ -16225,8 +16257,8 @@ packages: vfile@6.0.1: resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==} - vite-node@1.6.0: - resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} + vite-node@2.0.3: + resolution: {integrity: sha512-14jzwMx7XTcMB+9BhGQyoEAmSl0eOr3nrnn+Z12WNERtOvLN+d2scbRUvyni05rT3997Bg+rZb47NyP4IQPKXg==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -16266,15 +16298,15 @@ packages: terser: optional: true - vitest@1.6.0: - resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} + vitest@2.0.3: + resolution: {integrity: sha512-o3HRvU93q6qZK4rI2JrhKyZMMuxg/JRt30E6qeQs6ueaiz5hr1cPj+Sk2kATgQzMMqsa2DiNI0TIK++1ULx8Jw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 1.6.0 - '@vitest/ui': 1.6.0 + '@vitest/browser': 2.0.3 + '@vitest/ui': 2.0.3 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -18015,19 +18047,19 @@ snapshots: '@babel/helper-module-transforms@7.23.3(@babel/core@7.22.9)': dependencies: '@babel/core': 7.22.9 - '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-environment-visitor': 7.24.7 '@babel/helper-module-imports': 7.22.15 '@babel/helper-simple-access': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-split-export-declaration': 7.24.7 '@babel/helper-validator-identifier': 7.24.7 '@babel/helper-module-transforms@7.23.3(@babel/core@7.24.0)': dependencies: '@babel/core': 7.24.0 - '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-environment-visitor': 7.24.7 '@babel/helper-module-imports': 7.22.15 '@babel/helper-simple-access': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-split-export-declaration': 7.24.7 '@babel/helper-validator-identifier': 7.24.7 '@babel/helper-module-transforms@7.24.5(@babel/core@7.24.5)': @@ -18821,7 +18853,7 @@ snapshots: '@babel/template@7.24.0': dependencies: '@babel/code-frame': 7.24.2 - '@babel/parser': 7.24.5 + '@babel/parser': 7.24.7 '@babel/types': 7.24.7 '@babel/template@7.24.7': @@ -18848,12 +18880,12 @@ snapshots: '@babel/traverse@7.24.5': dependencies: '@babel/code-frame': 7.24.2 - '@babel/generator': 7.24.5 + '@babel/generator': 7.24.7 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-function-name': 7.23.0 '@babel/helper-hoist-variables': 7.22.5 '@babel/helper-split-export-declaration': 7.24.5 - '@babel/parser': 7.24.5 + '@babel/parser': 7.24.7 '@babel/types': 7.24.7 debug: 4.3.5(supports-color@8.1.1) globals: 11.12.0 @@ -20933,15 +20965,28 @@ snapshots: '@ianvs/prettier-plugin-sort-imports@4.2.1(prettier@3.3.3)': dependencies: '@babel/core': 7.24.7 - '@babel/generator': 7.23.6 - '@babel/parser': 7.24.0 - '@babel/traverse': 7.24.0 + '@babel/generator': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/traverse': 7.24.7 '@babel/types': 7.24.7 prettier: 3.3.3 semver: 7.6.2 transitivePeerDependencies: - supports-color + '@ianvs/prettier-plugin-sort-imports@4.3.1(prettier@3.3.3)': + dependencies: + '@babel/core': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + prettier: 3.3.3 + semver: 7.6.2 + transitivePeerDependencies: + - supports-color + optional: true + '@inquirer/confirm@3.1.8': dependencies: '@inquirer/core': 8.2.1 @@ -21046,9 +21091,9 @@ snapshots: '@jridgewell/gen-mapping@0.3.3': dependencies: - '@jridgewell/set-array': 1.1.2 + '@jridgewell/set-array': 1.2.1 '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.20 + '@jridgewell/trace-mapping': 0.3.25 '@jridgewell/gen-mapping@0.3.5': dependencies: @@ -21058,8 +21103,6 @@ snapshots: '@jridgewell/resolve-uri@3.1.1': {} - '@jridgewell/set-array@1.1.2': {} - '@jridgewell/set-array@1.2.1': {} '@jridgewell/source-map@0.3.6': @@ -24213,11 +24256,11 @@ snapshots: '@storybook/global': 5.0.0 storybook: 8.2.2(@babel/preset-env@7.24.5(@babel/core@7.24.7)) - '@storybook/addon-interactions@8.2.2(storybook@8.2.2(@babel/preset-env@7.24.5(@babel/core@7.24.7)))(vitest@1.6.0(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1))': + '@storybook/addon-interactions@8.2.2(storybook@8.2.2(@babel/preset-env@7.24.5(@babel/core@7.24.7)))(vitest@2.0.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1))': dependencies: '@storybook/global': 5.0.0 '@storybook/instrumenter': 8.2.2(storybook@8.2.2(@babel/preset-env@7.24.5(@babel/core@7.24.7))) - '@storybook/test': 8.2.2(storybook@8.2.2(@babel/preset-env@7.24.5(@babel/core@7.24.7)))(vitest@1.6.0(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1)) + '@storybook/test': 8.2.2(storybook@8.2.2(@babel/preset-env@7.24.5(@babel/core@7.24.7)))(vitest@2.0.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1)) polished: 4.2.2 storybook: 8.2.2(@babel/preset-env@7.24.5(@babel/core@7.24.7)) ts-dedent: 2.2.0 @@ -24411,12 +24454,12 @@ snapshots: optionalDependencies: typescript: 5.5.3 - '@storybook/test@8.2.2(storybook@8.2.2(@babel/preset-env@7.24.5(@babel/core@7.24.7)))(vitest@1.6.0(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1))': + '@storybook/test@8.2.2(storybook@8.2.2(@babel/preset-env@7.24.5(@babel/core@7.24.7)))(vitest@2.0.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1))': dependencies: '@storybook/csf': 0.1.11 '@storybook/instrumenter': 8.2.2(storybook@8.2.2(@babel/preset-env@7.24.5(@babel/core@7.24.7))) '@testing-library/dom': 10.1.0 - '@testing-library/jest-dom': 6.4.5(vitest@1.6.0(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1)) + '@testing-library/jest-dom': 6.4.5(vitest@2.0.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1)) '@testing-library/user-event': 14.5.2(@testing-library/dom@10.1.0) '@vitest/expect': 1.6.0 '@vitest/spy': 1.6.0 @@ -24581,7 +24624,7 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.5(vitest@1.6.0(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1))': + '@testing-library/jest-dom@6.4.5(vitest@2.0.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1))': dependencies: '@adobe/css-tools': 4.3.3 '@babel/runtime': 7.24.7 @@ -24592,7 +24635,7 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 optionalDependencies: - vitest: 1.6.0(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) + vitest: 2.0.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) '@testing-library/user-event@14.5.2(@testing-library/dom@10.1.0)': dependencies: @@ -24655,8 +24698,8 @@ snapshots: '@typescript-eslint/parser': 7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3) eslint: 8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva) eslint-config-prettier: 9.1.0(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) eslint-plugin-jsonc: 2.11.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) eslint-plugin-mdx: 3.0.0(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) @@ -24776,7 +24819,7 @@ snapshots: '@types/babel__template@7.4.1': dependencies: - '@babel/parser': 7.24.5 + '@babel/parser': 7.24.7 '@babel/types': 7.24.7 '@types/babel__traverse@7.18.3': @@ -25300,22 +25343,36 @@ snapshots: '@vitest/utils': 1.6.0 chai: 4.4.1 - '@vitest/runner@1.6.0': + '@vitest/expect@2.0.3': dependencies: - '@vitest/utils': 1.6.0 - p-limit: 5.0.0 - pathe: 1.1.1 + '@vitest/spy': 2.0.3 + '@vitest/utils': 2.0.3 + chai: 5.1.1 + tinyrainbow: 1.2.0 - '@vitest/snapshot@1.6.0': + '@vitest/pretty-format@2.0.3': dependencies: - magic-string: 0.30.5 - pathe: 1.1.1 - pretty-format: 29.7.0 + tinyrainbow: 1.2.0 + + '@vitest/runner@2.0.3': + dependencies: + '@vitest/utils': 2.0.3 + pathe: 1.1.2 + + '@vitest/snapshot@2.0.3': + dependencies: + '@vitest/pretty-format': 2.0.3 + magic-string: 0.30.10 + pathe: 1.1.2 '@vitest/spy@1.6.0': dependencies: tinyspy: 2.2.0 + '@vitest/spy@2.0.3': + dependencies: + tinyspy: 3.0.0 + '@vitest/utils@1.6.0': dependencies: diff-sequences: 29.6.3 @@ -25323,6 +25380,13 @@ snapshots: loupe: 2.3.7 pretty-format: 29.7.0 + '@vitest/utils@2.0.3': + dependencies: + '@vitest/pretty-format': 2.0.3 + estree-walker: 3.0.3 + loupe: 3.1.1 + tinyrainbow: 1.2.0 + '@webassemblyjs/ast@1.12.1': dependencies: '@webassemblyjs/helper-numbers': 1.11.6 @@ -25716,6 +25780,8 @@ snapshots: assertion-error@1.1.0: {} + assertion-error@2.0.1: {} + ast-types-flow@0.0.8: {} ast-types@0.16.1: @@ -26187,6 +26253,14 @@ snapshots: pathval: 1.1.1 type-detect: 4.0.8 + chai@5.1.1: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.1 + pathval: 2.0.0 + chalk@2.3.0: dependencies: ansi-styles: 3.2.1 @@ -26261,6 +26335,8 @@ snapshots: dependencies: get-func-name: 2.0.2 + check-error@2.1.1: {} + check-more-types@2.24.0: {} cheerio-select@1.6.0: @@ -27037,6 +27113,8 @@ snapshots: dependencies: type-detect: 4.0.8 + deep-eql@5.0.2: {} + deep-equal@2.1.0: dependencies: call-bind: 1.0.5 @@ -27564,13 +27642,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)): dependencies: debug: 4.3.5(supports-color@8.1.1) enhanced-resolve: 5.17.0 eslint: 8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva) - eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) fast-glob: 3.3.2 get-tsconfig: 4.7.5 is-core-module: 2.13.1 @@ -27601,14 +27679,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.0(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)): + eslint-module-utils@2.8.0(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: '@typescript-eslint/parser': 7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3) eslint: 8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) transitivePeerDependencies: - supports-color @@ -27624,7 +27702,7 @@ snapshots: eslint: 8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva) eslint-compat-utils: 0.1.2(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)): dependencies: array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 @@ -27634,7 +27712,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.5.3))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) hasown: 2.0.0 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -29803,8 +29881,6 @@ snapshots: js-tokens@4.0.0: {} - js-tokens@8.0.3: {} - js-yaml@3.14.1: dependencies: argparse: 1.0.10 @@ -29923,8 +29999,6 @@ snapshots: espree: 9.6.1 semver: 7.6.2 - jsonc-parser@3.2.0: {} - jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -30171,11 +30245,6 @@ snapshots: emojis-list: 3.0.0 json5: 1.0.2 - local-pkg@0.5.0: - dependencies: - mlly: 1.4.2 - pkg-types: 1.0.3 - localforage@1.10.0: dependencies: lie: 3.1.1 @@ -30265,6 +30334,10 @@ snapshots: dependencies: get-func-name: 2.0.2 + loupe@3.1.1: + dependencies: + get-func-name: 2.0.2 + lower-case-first@2.0.2: dependencies: tslib: 2.6.3 @@ -30314,6 +30387,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 + magic-string@0.30.10: + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + magic-string@0.30.5: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 @@ -31534,13 +31611,6 @@ snapshots: mkdirp@3.0.1: {} - mlly@1.4.2: - dependencies: - acorn: 8.12.0 - pathe: 1.1.1 - pkg-types: 1.0.3 - ufo: 1.3.0 - mnemonist@0.39.6: dependencies: obliterator: 2.0.4 @@ -32106,10 +32176,6 @@ snapshots: dependencies: yocto-queue: 1.0.0 - p-limit@5.0.0: - dependencies: - yocto-queue: 1.0.0 - p-limit@6.1.0: dependencies: yocto-queue: 1.1.1 @@ -32346,8 +32412,12 @@ snapshots: pathe@1.1.1: {} + pathe@1.1.2: {} + pathval@1.1.1: {} + pathval@2.0.0: {} + pend@1.2.0: {} performance-now@2.1.0: {} @@ -32516,12 +32586,6 @@ snapshots: dependencies: find-up: 6.3.0 - pkg-types@1.0.3: - dependencies: - jsonc-parser: 3.2.0 - mlly: 1.4.2 - pathe: 1.1.1 - pluralize@8.0.0: {} polished@4.2.2: @@ -32786,11 +32850,11 @@ snapshots: sql-formatter: 15.0.2 tslib: 2.6.3 - prettier-plugin-tailwindcss@0.6.5(@ianvs/prettier-plugin-sort-imports@4.2.1(prettier@3.3.3))(prettier@3.3.3): + prettier-plugin-tailwindcss@0.6.5(@ianvs/prettier-plugin-sort-imports@4.3.1(prettier@3.3.3))(prettier@3.3.3): dependencies: prettier: 3.3.3 optionalDependencies: - '@ianvs/prettier-plugin-sort-imports': 4.2.1(prettier@3.3.3) + '@ianvs/prettier-plugin-sort-imports': 4.3.1(prettier@3.3.3) prettier@2.8.8: {} @@ -34117,7 +34181,7 @@ snapshots: std-env@3.3.3: {} - std-env@3.6.0: {} + std-env@3.7.0: {} storybook@8.2.2(@babel/preset-env@7.24.5(@babel/core@7.24.7)): dependencies: @@ -34270,10 +34334,6 @@ snapshots: strip-json-comments@3.1.1: {} - strip-literal@2.0.0: - dependencies: - js-tokens: 8.0.3 - stripe@14.25.0: dependencies: '@types/node': 20.14.10 @@ -34571,12 +34631,16 @@ snapshots: tiny-warning@1.0.3: {} - tinybench@2.5.1: {} + tinybench@2.8.0: {} - tinypool@0.8.3: {} + tinypool@1.0.0: {} + + tinyrainbow@1.2.0: {} tinyspy@2.2.0: {} + tinyspy@3.0.0: {} + title-case@3.0.3: dependencies: tslib: 2.6.3 @@ -34870,8 +34934,6 @@ snapshots: uc.micro@2.1.0: {} - ufo@1.3.0: {} - uglify-js@3.17.4: {} unbox-primitive@1.0.2: @@ -35252,12 +35314,12 @@ snapshots: unist-util-stringify-position: 4.0.0 vfile-message: 4.0.2 - vite-node@1.6.0(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1): + vite-node@2.0.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1): dependencies: cac: 6.7.14 debug: 4.3.5(supports-color@8.1.1) - pathe: 1.1.1 - picocolors: 1.0.1 + pathe: 1.1.2 + tinyrainbow: 1.2.0 vite: 5.3.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) transitivePeerDependencies: - '@types/node' @@ -35291,27 +35353,26 @@ snapshots: less: 4.2.0 terser: 5.31.1 - vitest@1.6.0(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1): + vitest@2.0.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1): dependencies: - '@vitest/expect': 1.6.0 - '@vitest/runner': 1.6.0 - '@vitest/snapshot': 1.6.0 - '@vitest/spy': 1.6.0 - '@vitest/utils': 1.6.0 - acorn-walk: 8.3.2 - chai: 4.4.1 + '@ampproject/remapping': 2.3.0 + '@vitest/expect': 2.0.3 + '@vitest/pretty-format': 2.0.3 + '@vitest/runner': 2.0.3 + '@vitest/snapshot': 2.0.3 + '@vitest/spy': 2.0.3 + '@vitest/utils': 2.0.3 + chai: 5.1.1 debug: 4.3.5(supports-color@8.1.1) execa: 8.0.1 - local-pkg: 0.5.0 - magic-string: 0.30.5 - pathe: 1.1.1 - picocolors: 1.0.1 - std-env: 3.6.0 - strip-literal: 2.0.0 - tinybench: 2.5.1 - tinypool: 0.8.3 + magic-string: 0.30.10 + pathe: 1.1.2 + std-env: 3.7.0 + tinybench: 2.8.0 + tinypool: 1.0.0 + tinyrainbow: 1.2.0 vite: 5.3.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) - vite-node: 1.6.0(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) + vite-node: 2.0.3(@types/node@20.14.10)(less@4.2.0)(terser@5.31.1) why-is-node-running: 2.2.2 optionalDependencies: '@types/node': 20.14.10 diff --git a/prettier.config.cjs b/prettier.config.cjs index 2299ef203..d7606a86e 100644 --- a/prettier.config.cjs +++ b/prettier.config.cjs @@ -5,7 +5,12 @@ const { plugins, ...prettierConfig } = require('@theguild/prettier-config'); module.exports = { ...prettierConfig, - importOrderParserPlugins: ['importAssertions', ...prettierConfig.importOrderParserPlugins], + importOrderParserPlugins: [ + 'importAssertions', + // `using` keyword + 'explicitResourceManagement', + ...prettierConfig.importOrderParserPlugins, + ], plugins: [ 'prettier-plugin-sql', ...plugins,