diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 876a4d7d4b..8a872df410 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -74,7 +74,7 @@ services: clickhouse: image: clickhouse/clickhouse-server:24.8-alpine - mem_limit: 2048m + mem_limit: 4096m environment: CLICKHOUSE_USER: test CLICKHOUSE_PASSWORD: test diff --git a/package.json b/package.json index 4d2f607020..2d795c44ac 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,9 @@ "@graphql-codegen/typescript-resolvers": "4.4.4", "@graphql-codegen/urql-introspection": "3.0.0", "@graphql-eslint/eslint-plugin": "3.20.1", - "@graphql-inspector/cli": "4.0.3", + "@graphql-inspector/cli": "link:../graphql-inspector/packages/cli", + "@graphql-inspector/core": "file:../graphql-inspector/packages/core", + "@graphql-inspector/patch": "file:../graphql-inspector/packages/patch", "@manypkg/get-packages": "2.2.2", "@next/eslint-plugin-next": "14.2.23", "@parcel/watcher": "2.5.0", diff --git a/packages/libraries/cli/examples/federation.products-changes.graphql b/packages/libraries/cli/examples/federation.products-changes.graphql new file mode 100644 index 0000000000..8a3e21caf8 --- /dev/null +++ b/packages/libraries/cli/examples/federation.products-changes.graphql @@ -0,0 +1,70 @@ +extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@shareable", "@inaccessible", "@tag"] + ) + @link(url: "https://specs.graphql-hive.com/hive/v1.0", import: ["@meta"]) + @meta(name: "priority", content: "tier1") + +directive @meta( + name: String! + content: String! +) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +type Query { + allProducts: [ProductItf] @meta(name: "owner", content: "hive-team") + product(id: ID!): ProductItf +} + +interface ProductItf implements SkuItf @meta(name: "domain", content: "products") { + id: ID! + sku: String + name: String + package: String + variation: ProductVariation + dimensions: ProductDimension + createdBy: User + hidden: String @inaccessible + oldField: String @deprecated(reason: "refactored out") +} + +interface SkuItf { + sku: String +} + +type Product implements ProductItf & SkuItf + @key(fields: "id") + @key(fields: "sku package") + @key(fields: "sku variation { id }") + @meta(name: "owner", content: "product-team") { + id: ID! + sku: String @meta(name: "unique", content: "true") + name: String + package: String + variation: ProductVariation + dimensions: ProductDimension + createdBy: User + hidden: String + reviewsScore: Float! + oldField: String @deprecated(reason: "Not used any longer") +} + +enum ShippingClass { + STANDARD + EXPRESS +} + +type ProductVariation { + id: ID! + name: String +} + +type ProductDimension @shareable { + size: String + weight: Float +} + +type User @key(fields: "email") { + email: ID! + totalProductsCreated: Int @shareable +} diff --git a/packages/libraries/cli/examples/federation.reviews-changes.graphql b/packages/libraries/cli/examples/federation.reviews-changes.graphql new file mode 100644 index 0000000000..eb4d5eb164 --- /dev/null +++ b/packages/libraries/cli/examples/federation.reviews-changes.graphql @@ -0,0 +1,50 @@ +extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@shareable", "@override"] + ) + @link(url: "https://specs.graphql-hive.com/hive/v1.0", import: ["@meta"]) + @meta(name: "owner", content: "reviews-team") + +directive @meta( + name: String! + content: String! +) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +type Product implements ProductItf @key(fields: "id") { + id: ID! + reviewsCount: Int! + reviewsScore: Float! @shareable @override(from: "products") + reviews: [Review!]! +} + +interface ProductItf { + id: ID! + reviewsCount: Int! + reviewsScore: Float! + reviews: [Review!]! +} + +type Query { + review(id: ID!): Review + reviewsByProductId(id: ID!): [Review] +} + +type Mutation { + reviewProduct(productId: ID!, input: ReviewProductInput!): Review +} + +input ReviewProductInput { + body: String! + + """ + Rating on a scale of 0 - 5 + """ + rating: Int! = 3 +} + +type Review { + id: ID! + body: String! + rating: Int! +} diff --git a/packages/libraries/cli/package.json b/packages/libraries/cli/package.json index 2d2cfa2a8a..031d15c369 100644 --- a/packages/libraries/cli/package.json +++ b/packages/libraries/cli/package.json @@ -48,7 +48,7 @@ }, "dependencies": { "@graphql-hive/core": "workspace:*", - "@graphql-inspector/core": "5.1.0-alpha-20231208113249-34700c8a", + "@graphql-inspector/core": "file:../../../../graphql-inspector/packages/core", "@graphql-tools/code-file-loader": "~8.1.0", "@graphql-tools/graphql-file-loader": "~8.0.0", "@graphql-tools/json-file-loader": "~8.0.0", diff --git a/packages/libraries/cli/src/commands/proposal/create.ts b/packages/libraries/cli/src/commands/proposal/create.ts new file mode 100644 index 0000000000..80d37a2a6d --- /dev/null +++ b/packages/libraries/cli/src/commands/proposal/create.ts @@ -0,0 +1,250 @@ +import { Args, Errors, Flags } from '@oclif/core'; +import Command from '../../base-command'; +import { graphql } from '../../gql'; +import * as GraphQLSchema from '../../gql/graphql'; +import { graphqlEndpoint } from '../../helpers/config'; +import { + APIError, + CommitRequiredError, + GithubRepositoryRequiredError, + InvalidTargetError, + MissingEndpointError, + MissingRegistryTokenError, + SchemaFileEmptyError, + SchemaFileNotFoundError, + UnexpectedError, +} from '../../helpers/errors'; +import { gitInfo } from '../../helpers/git'; +import { loadSchema, minifySchema } from '../../helpers/schema'; +import * as TargetInput from '../../helpers/target-input'; + +const proposeSchemaMutation = graphql(/* GraphQL */ ` + mutation proposeSchema($input: CreateSchemaProposalInput!) { + createSchemaProposal(input: $input) { + __typename + ok { + schemaProposal { + id + } + } + error { + message + ... on CreateSchemaProposalError { + details { + description + title + } + } + } + } + } +`); + +export default class ProposalCreate extends Command { + static description = 'Proposes a schema change'; + static flags = { + 'registry.endpoint': Flags.string({ + description: 'registry endpoint', + }), + /** @deprecated */ + registry: Flags.string({ + description: 'registry address', + deprecated: { + message: 'use --registry.endpoint instead', + version: '0.21.0', + }, + }), + 'registry.accessToken': Flags.string({ + description: 'registry access token', + }), + /** @deprecated */ + token: Flags.string({ + description: 'api token', + deprecated: { + message: 'use --registry.accessToken instead', + version: '0.21.0', + }, + }), + target: Flags.string({ + required: true, + description: + 'The target against which to propose the schema (slug or ID).' + + ' This can either be a slug following the format "$organizationSlug/$projectSlug/$targetSlug" (e.g "the-guild/graphql-hive/staging")' + + ' or an UUID (e.g. "a0f4c605-6541-4350-8cfe-b31f21a4bf80").', + }), + title: Flags.string({ + required: true, + description: 'Title of the proposal. This should be a short description of the change.', + }), + description: Flags.string({ + required: false, + description: + 'Description of the proposal. This should be a more detailed explanation of the change.', + }), + draft: Flags.boolean({ + default: false, + description: + 'Set to true to open the proposal as a Draft. This indicates the proposal is still in progress.', + }), + + /** CLI Only supports service at a time right now. */ + service: Flags.string({ + description: 'service name (only for distributed schemas)', + }), + github: Flags.boolean({ + description: 'Connect with GitHub Application', + default: false, + }), + author: Flags.string({ + description: 'Author of the change', + }), + commit: Flags.string({ + description: 'Associated commit sha', + }), + contextId: Flags.string({ + description: 'Context ID for grouping the schema check.', + }), + url: Flags.string({ + description: + 'If checking a service, then you can optionally provide the service URL to see the difference in the supergraph during the check.', + }), + }; + + static args = { + file: Args.string({ + name: 'file', + required: true, + description: 'Path to the schema file(s)', + hidden: false, + }), + }; + + async run() { + try { + const { flags, args } = await this.parse(ProposalCreate); + let target: GraphQLSchema.TargetReferenceInput | null = null; + { + const result = TargetInput.parse(flags.target); + if (result.type === 'error') { + throw new InvalidTargetError(); + } + target = result.data; + } + + const service = flags.service; + const usesGitHubApp = flags.github === true; + let endpoint: string, accessToken: string; + try { + endpoint = this.ensure({ + key: 'registry.endpoint', + args: flags, + legacyFlagName: 'registry', + defaultValue: graphqlEndpoint, + env: 'HIVE_REGISTRY', + description: ProposalCreate.flags['registry.endpoint'].description!, + }); + } catch (e) { + throw new MissingEndpointError(); + } + const file = args.file; + try { + accessToken = this.ensure({ + key: 'registry.accessToken', + args: flags, + legacyFlagName: 'token', + env: 'HIVE_TOKEN', + description: ProposalCreate.flags['registry.accessToken'].description!, + }); + } catch (e) { + throw new MissingRegistryTokenError(); + } + + const sdl = await loadSchema(file).catch(e => { + throw new SchemaFileNotFoundError(file, e); + }); + const git = await gitInfo(() => { + // noop + }); + + const commit = flags.commit || git?.commit; + const author = flags.author || git?.author; + + if (typeof sdl !== 'string' || sdl.length === 0) { + throw new SchemaFileEmptyError(file); + } + + let github: null | { + commit: string; + repository: string | null; + pullRequestNumber: string | null; + } = null; + + if (usesGitHubApp) { + if (!commit) { + throw new CommitRequiredError(); + } + if (!git.repository) { + throw new GithubRepositoryRequiredError(); + } + if (!git.pullRequestNumber) { + this.warn( + "Could not resolve pull request number. Are you running this command on a 'pull_request' event?\n" + + 'See https://the-guild.dev/graphql/hive/docs/other-integrations/ci-cd#github-workflow-for-ci', + ); + } + + github = { + commit: commit, + repository: git.repository, + pullRequestNumber: git.pullRequestNumber, + }; + } + + const result = await this.registryApi(endpoint, accessToken).request({ + operation: proposeSchemaMutation, + variables: { + input: { + target, + title: flags.title, + description: flags.description, + isDraft: flags.draft, + initialChecks: [ + { + service, + sdl: minifySchema(sdl), + github, + meta: + !!commit && !!author + ? { + commit, + author, + } + : null, + contextId: flags.contextId ?? undefined, + url: flags.url, + }, + ], + }, + }, + }); + + if (result.createSchemaProposal.ok) { + const id = result.createSchemaProposal.ok.schemaProposal.id; + if (id) { + this.logSuccess(`Created proposal ${id}.`); + } + + if (result.createSchemaProposal.error) { + throw new APIError(result.createSchemaProposal.error.message); + } + } + } catch (error) { + if (error instanceof Errors.CLIError) { + throw error; + } else { + this.logFailure('Failed to create schema proposal'); + throw new UnexpectedError(error); + } + } + } +} diff --git a/packages/libraries/cli/src/commands/schema/check.ts b/packages/libraries/cli/src/commands/schema/check.ts index e0462ce5bf..a544eb729e 100644 --- a/packages/libraries/cli/src/commands/schema/check.ts +++ b/packages/libraries/cli/src/commands/schema/check.ts @@ -150,6 +150,9 @@ export default class SchemaCheck extends Command { description: 'If checking a service, then you can optionally provide the service URL to see the difference in the supergraph during the check.', }), + schemaProposalId: Flags.string({ + description: 'Attach the schema check to a schema proposal.', + }), }; static args = { @@ -263,6 +266,7 @@ export default class SchemaCheck extends Command { contextId: flags.contextId ?? undefined, target, url: flags.url, + schemaProposalId: flags.schemaProposalId, }, }, }); diff --git a/packages/migrations/src/actions/2025.08.30T00-00-00.schema-proposals.ts b/packages/migrations/src/actions/2025.08.30T00-00-00.schema-proposals.ts new file mode 100644 index 0000000000..4185c33f7c --- /dev/null +++ b/packages/migrations/src/actions/2025.08.30T00-00-00.schema-proposals.ts @@ -0,0 +1,143 @@ +import { type MigrationExecutor } from '../pg-migrator'; + +/** + * This migration establishes the schema proposal tables. + */ +export default { + name: '2025.05.29T00-00-00.schema-proposals.ts', + run: ({ sql }) => [ + { + name: 'create schema_proposal tables', + query: sql` + CREATE TYPE + schema_proposal_stage AS ENUM('DRAFT', 'OPEN', 'APPROVED', 'IMPLEMENTED', 'CLOSED') + ; + /** + * Request patterns include: + * - Get by ID + * - List target's proposals by date + * - List target's proposals by date, filtered by author/user_id and/or stage (for now) + */ + CREATE TABLE IF NOT EXISTS "schema_proposals" + ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4 () + , created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + , updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + , title VARCHAR(72) NOT NULL + , description text NOT NULL + , stage schema_proposal_stage NOT NULL + , target_id UUID NOT NULL REFERENCES targets (id) ON DELETE CASCADE + -- ID for the user that opened the proposal @todo + , user_id UUID REFERENCES users (id) ON DELETE SET NULL + -- projection of the number of comments on the PR to optimize the list view + , comments_count INT NOT NULL DEFAULT 0 + ) + ; + CREATE INDEX IF NOT EXISTS schema_proposals_list ON schema_proposals ( + target_id + , created_at DESC + ) + ; + CREATE INDEX IF NOT EXISTS schema_proposals_list_by_user_id ON schema_proposals ( + target_id + , user_id + , created_at DESC + ) + ; + CREATE INDEX IF NOT EXISTS schema_proposals_list_by_stage ON schema_proposals ( + target_id + , stage + , created_at DESC + ) + ; + CREATE INDEX IF NOT EXISTS schema_proposals_list_by_user_id_stage ON schema_proposals ( + target_id + , user_id + , stage + , created_at DESC + ) + ; + -- For performance during user delete + CREATE INDEX IF NOT EXISTS schema_proposals_diff_user_id on schema_proposals ( + user_id + ) + ; + CREATE TABLE IF NOT EXISTS "schema_proposal_reviews" + ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4 () + , created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + -- null if just a comment + , stage_transition schema_proposal_stage NOT NULL + , user_id UUID REFERENCES users (id) ON DELETE SET NULL + , schema_proposal_id UUID NOT NULL REFERENCES schema_proposals (id) ON DELETE CASCADE + -- store the original text of the line that is being reviewed. If the base schema version changes, then this is + -- used to determine which line this review falls on. If no line matches in the current version, then + -- show as outdated and attribute to the original line. + , line_text text + -- the coordinate closest to the reviewed line. E.g. if a comment is reviewed, then + -- this is the coordinate that the comment applies to. + -- note that the line_text must still be stored in case the coordinate can no + -- longer be found in the latest proposal version. That way a preview of the reviewed + -- line can be provided. + , schema_coordinate text + , resolved_by_user_id UUID REFERENCES users (id) ON DELETE SET NULL + , service_name TEXT NOT NULL + ) + ; + CREATE INDEX IF NOT EXISTS schema_proposal_reviews_schema_proposal_id ON schema_proposal_reviews( + schema_proposal_id + , created_at ASC + ) + ; + -- For performance on user delete + CREATE INDEX IF NOT EXISTS schema_proposal_reviews_user_id ON schema_proposal_reviews( + user_id + ) + ; + -- For performance on user delete + CREATE INDEX IF NOT EXISTS schema_proposal_reviews_resolved_by_user_id ON schema_proposal_reviews( + resolved_by_user_id + ) + ; + /** + * Request patterns include: + * - Get by ID + * - List a proposal's comments in order of creation, grouped by review. + */ + CREATE TABLE IF NOT EXISTS "schema_proposal_comments" + ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4 () + , user_id UUID REFERENCES users (id) ON DELETE SET NULL + , body TEXT NOT NULL + , created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + , updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + , schema_proposal_review_id UUID REFERENCES schema_proposal_reviews (id) ON DELETE CASCADE + ) + ; + CREATE INDEX IF NOT EXISTS schema_proposal_comments_list ON schema_proposal_comments( + schema_proposal_review_id + , created_at ASC + ) + ; + -- For performance on user delete + CREATE INDEX IF NOT EXISTS schema_proposal_comments_user_id ON schema_proposal_comments( + user_id + ) + ; + `, + }, + { + // Associate schema checks with schema proposals + name: 'Add "organization_member_roles"."created_at" column', + query: sql` + ALTER TABLE "schema_checks" + ADD COLUMN IF NOT EXISTS "schema_proposal_id" UUID REFERENCES "schema_proposals" ("id") ON DELETE SET NULL + ; + CREATE INDEX IF NOT EXISTS schema_checks_schema_proposal_id ON schema_checks( + schema_proposal_id, lower(service_name) + ) + ; + `, + }, + ], +} satisfies MigrationExecutor; diff --git a/packages/migrations/src/run-pg-migrations.ts b/packages/migrations/src/run-pg-migrations.ts index fc0b902d47..b640ae650b 100644 --- a/packages/migrations/src/run-pg-migrations.ts +++ b/packages/migrations/src/run-pg-migrations.ts @@ -167,5 +167,6 @@ export const runPGMigrations = async (args: { slonik: DatabasePool; runTo?: stri await import('./actions/2025.05.15T00-00-00.contracts-foreign-key-constraint-fix'), await import('./actions/2025.05.15T00-00-01.organization-member-pagination'), await import('./actions/2025.05.28T00-00-00.schema-log-by-ids'), + await import('./actions/2025.08.30T00-00-00.schema-proposals'), ], }); diff --git a/packages/services/api/package.json b/packages/services/api/package.json index eb673650bd..2db58b5344 100644 --- a/packages/services/api/package.json +++ b/packages/services/api/package.json @@ -17,7 +17,7 @@ "@date-fns/utc": "2.1.0", "@graphql-hive/core": "workspace:*", "@graphql-hive/signal": "1.0.0", - "@graphql-inspector/core": "5.1.0-alpha-20231208113249-34700c8a", + "@graphql-inspector/core": "file:../../../../graphql-inspector/packages/core", "@graphql-tools/merge": "9.0.24", "@hive/cdn-script": "workspace:*", "@hive/emails": "workspace:*", diff --git a/packages/services/api/src/create.ts b/packages/services/api/src/create.ts index 9f9389d8f0..a19e06b465 100644 --- a/packages/services/api/src/create.ts +++ b/packages/services/api/src/create.ts @@ -36,6 +36,8 @@ import { SchemaPolicyServiceConfig, } from './modules/policy/providers/tokens'; import { projectModule } from './modules/project'; +import { proposalsModule } from './modules/proposals'; +import { SCHEMA_PROPOSALS_ENABLED } from './modules/proposals/providers/schema-proposals-enabled-token'; import { schemaModule } from './modules/schema'; import { ArtifactStorageWriter } from './modules/schema/providers/artifact-storage-writer'; import { provideSchemaModuleConfig, SchemaModuleConfig } from './modules/schema/providers/config'; @@ -88,6 +90,7 @@ const modules = [ collectionModule, appDeploymentsModule, auditLogsModule, + proposalsModule, ]; export function createRegistry({ @@ -113,6 +116,7 @@ export function createRegistry({ organizationOIDC, pubSub, appDeploymentsEnabled, + schemaProposalsEnabled, prometheus, }: { logger: Logger; @@ -157,6 +161,7 @@ export function createRegistry({ organizationOIDC: boolean; pubSub: HivePubSub; appDeploymentsEnabled: boolean; + schemaProposalsEnabled: boolean; prometheus: null | Record; }) { const s3Config: S3Config = [ @@ -284,6 +289,11 @@ export function createRegistry({ useValue: appDeploymentsEnabled, scope: Scope.Singleton, }, + { + provide: SCHEMA_PROPOSALS_ENABLED, + useValue: schemaProposalsEnabled, + scope: Scope.Singleton, + }, { provide: WEB_APP_URL, useValue: app?.baseUrl.replace(/\/$/, '') ?? 'http://localhost:3000', diff --git a/packages/services/api/src/modules/auth/lib/authz.ts b/packages/services/api/src/modules/auth/lib/authz.ts index 0e6163269e..4a25110bca 100644 --- a/packages/services/api/src/modules/auth/lib/authz.ts +++ b/packages/services/api/src/modules/auth/lib/authz.ts @@ -351,6 +351,18 @@ function defaultAppDeploymentIdentity( return ids; } +function defaultSchemaProposalIdentity( + args: { schemaProposalId: string | null } & Parameters[0], +) { + const ids = defaultTargetIdentity(args); + + if (args.schemaProposalId !== null) { + ids.push(`target/${args.targetId}/schemaProposal/${args.schemaProposalId}`); + } + + return ids; +} + function schemaCheckOrPublishIdentity( args: { serviceName: string | null } & Parameters[0], ) { @@ -418,6 +430,7 @@ const permissionsByLevel = { z.literal('appDeployment:publish'), z.literal('appDeployment:retire'), ], + schemaProposal: [z.literal('schemaProposal:modify')], } as const; export const allPermissions = [ @@ -510,6 +523,9 @@ const actionDefinitions = { ...objectFromEntries( permissionsByLevel['appDeployment'].map(t => [t.value, defaultAppDeploymentIdentity]), ), + ...objectFromEntries( + permissionsByLevel['schemaProposal'].map(t => [t.value, defaultSchemaProposalIdentity]), + ), } satisfies ActionDefinitionMap; type Actions = keyof typeof actionDefinitions; diff --git a/packages/services/api/src/modules/proposals/index.ts b/packages/services/api/src/modules/proposals/index.ts new file mode 100644 index 0000000000..658ea3a07e --- /dev/null +++ b/packages/services/api/src/modules/proposals/index.ts @@ -0,0 +1,31 @@ +import { createModule } from 'graphql-modules'; +import { BreakingSchemaChangeUsageHelper } from '../schema/providers/breaking-schema-changes-helper'; +import { ContractsManager } from '../schema/providers/contracts-manager'; +import { models as schemaModels } from '../schema/providers/models'; +import { CompositionOrchestrator } from '../schema/providers/orchestrator/composition-orchestrator'; +import { RegistryChecks } from '../schema/providers/registry-checks'; +import { SchemaPublisher } from '../schema/providers/schema-publisher'; +import { Storage } from '../shared/providers/storage'; +import { SchemaProposalManager } from './providers/schema-proposal-manager'; +import { SchemaProposalStorage } from './providers/schema-proposal-storage'; +import { resolvers } from './resolvers.generated'; +import typeDefs from './module.graphql'; + +export const proposalsModule = createModule({ + id: 'proposals', + dirname: __dirname, + typeDefs, + resolvers, + providers: [ + SchemaProposalManager, + SchemaProposalStorage, + + /** Schema module providers -- To allow publishing checks */ + SchemaPublisher, + RegistryChecks, + ContractsManager, + BreakingSchemaChangeUsageHelper, + CompositionOrchestrator, + ...schemaModels, + ], +}); diff --git a/packages/services/api/src/modules/proposals/module.graphql.ts b/packages/services/api/src/modules/proposals/module.graphql.ts new file mode 100644 index 0000000000..75a700e638 --- /dev/null +++ b/packages/services/api/src/modules/proposals/module.graphql.ts @@ -0,0 +1,997 @@ +import { gql } from 'graphql-modules'; + +export default gql` + extend type Mutation { + createSchemaProposal(input: CreateSchemaProposalInput!): CreateSchemaProposalResult! + reviewSchemaProposal(input: ReviewSchemaProposalInput!): ReviewSchemaProposalResult! + replyToSchemaProposalReview( + input: CommentOnSchemaProposalReviewInput! + ): ReplyToSchemaProposalReviewResult! + } + + type ReplyToSchemaProposalReviewResult { + ok: ReplyToSchemaProposalReviewOk + error: ReplyToSchemaProposalReviewError + } + + type ReplyToSchemaProposalReviewOk { + reply: SchemaProposalComment! + } + + type ReplyToSchemaProposalReviewError implements Error { + message: String! + } + + type ReviewSchemaProposalResult { + ok: ReviewSchemaProposalOk + error: ReviewSchemaProposalError + } + + type ReviewSchemaProposalOk { + review: SchemaProposalReview! + } + + type ReviewSchemaProposalError implements Error { + message: String! + } + + type CreateSchemaProposalResult { + error: CreateSchemaProposalError + ok: CreateSchemaProposalOk + } + + type CreateSchemaProposalOk { + schemaProposal: SchemaProposal! + } + + type CreateSchemaProposalErrorDetails { + """ + Error message for the input title. + """ + title: String + """ + Error message for the input description. + """ + description: String + } + + type CreateSchemaProposalError implements Error { + message: String! + details: CreateSchemaProposalErrorDetails! + } + + input SchemaProposalCheckInput { + service: ID + sdl: String! + github: GitHubSchemaCheckInput + meta: SchemaCheckMetaInput + """ + Optional context ID to group schema checks together. + Manually approved breaking changes will be memorized for schema checks with the same context id. + """ + contextId: String + """ + Optional url if wanting to show subgraph url changes inside checks. + """ + url: String + } + + input CreateSchemaProposalInput { + """ + Reference to the proposal's target. Either an ID or path. + """ + target: TargetReferenceInput! + + """ + The title of the proposal. A short description of the proposal's main focus/theme. + """ + title: String! + + """ + If no description was provided then this will be an empty string. + """ + description: String! = "" + + """ + The default initial stage is OPEN. Set this to true to create this as proposal + as a DRAFT instead. + """ + isDraft: Boolean! = false + + """ + The initial proposed service changes to be ran as checks + """ + initialChecks: [SchemaProposalCheckInput!]! + } + + input ReviewSchemaProposalInput { + """ + The schema proposal that this review is being made on. + """ + schemaProposalId: ID! + + """ + The schema coordinate being referenced. E.g. "Type.field". + If null, then this review is for the entire proposal. + """ + coordinate: String + + """ + One or both of stageTransition or initialComment inputs is/are required. + """ + stageTransition: SchemaProposalStage + + """ + The initial comment message attached to the review + """ + commentBody: String! = "" + + """ + The service this review applies to. If the target is a monorepo, then use + an empty string. + """ + serviceName: String! + } + + input CommentOnSchemaProposalReviewInput { + schemaProposalReviewId: ID! + body: String! + } + + extend type Query { + schemaProposals( + after: String + first: Int! = 30 + input: SchemaProposalsInput! + ): SchemaProposalConnection + schemaProposal(input: SchemaProposalInput!): SchemaProposal + } + + input SchemaProposalsInput { + target: TargetReferenceInput! + userIds: [ID!] + stages: [SchemaProposalStage!] + } + + input SchemaProposalInput { + """ + Unique identifier of the desired SchemaProposal + """ + id: ID! + } + + type SchemaProposalConnection { + edges: [SchemaProposalEdge!] + pageInfo: PageInfo! + } + + type SchemaProposalEdge { + cursor: String! + node: SchemaProposal! + } + + enum SchemaProposalStage { + DRAFT + OPEN + APPROVED + IMPLEMENTED + CLOSED + } + + type SchemaProposal { + id: ID! + createdAt: DateTime! + + """ + A paginated list of reviews. + """ + reviews(after: String, first: Int! = 30): SchemaProposalReviewConnection + + """ + The current stage of the proposal. Proposals should be transitioned through the + course of the review from DRAFT, OPEN, APPROVED, IMPLEMENTED, but may be CLOSED + at any point in its lifecycle prior to being IMPLEMENTED. DRAFT, OPEN, APPROVED, + and CLOSED may be triggered by user action. But IMPLEMENTED can only happen if + the target schema contains the proposed changes while the proposal is in the + APPROVED state. + """ + stage: SchemaProposalStage! + + """ + A short title of this proposal. Meant to give others an easy way to refer to this + set of changes. + """ + title: String! + + """ + The proposal description. If no description was given, this will be an empty string. + """ + description: String! + + """ + When the proposal was last modified. Adding a review or comment does not count. + """ + updatedAt: DateTime! + + """ + The author of the proposal. If no author has been assigned, then this returns an empty string. + The author is taken from the author of the oldest check ran for this proposal. + """ + author: String! + + """ + The checks associated with this proposal. Each proposed change triggers a check + for the set of changes. And each service is checked separately. This is a limitation + of the schema check API at this time. + + The check "cursor" can be considered the proposal "version". + """ + checks( + after: String + first: Int! = 20 + input: SchemaProposalChecksInput! + ): SchemaCheckConnection + + """ + Applies changes to each service subgraph for each of the service's latest check belonging to the SchemaProposal. + """ + rebasedSchemaSDL( + """ + A schema check cursor. This indicates from where in the list of schema checks to start applying + the changes. The check "cursor" can be considered the proposal "version". + """ + after: String + """ + The number of service SDLs return + """ + first: Int! = 20 + ): SubgraphSchemaConnection + + """ + Applies changes to the supergraph for each of the service's latest check belonging to the SchemaProposal. + """ + rebasedSupergraphSDL( + """ + A schema check cursor. This indicates from where in the list of schema checks to start applying + the changes. + """ + fromCursor: String + ): String + + commentsCount: Int! + } + + input SchemaProposalChecksInput { + """ + Set to "true" to only return the latest checks for each service. + """ + latestPerService: Boolean! = false + } + + type SubgraphSchemaConnection { + edges: [SubgraphSchemaEdge] + pageInfo: PageInfo! + } + + type SubgraphSchemaEdge { + cursor: String! + node: SubgraphSchema! + } + + type SubgraphSchema { + """ + The SDL of the schema that was checked. + """ + schemaSDL: String! + + """ + The name of the service that owns the schema. Is null for non composite project types. + """ + serviceName: String + } + + type SchemaProposalReviewEdge { + cursor: String! + node: SchemaProposalReview! + } + + type SchemaProposalReviewConnection { + edges: [SchemaProposalReviewEdge!] + pageInfo: PageInfo! + } + + type SchemaProposalCommentEdge { + cursor: String! + node: SchemaProposalComment! + } + + type SchemaProposalCommentConnection { + edges: [SchemaProposalCommentEdge!] + pageInfo: PageInfo! + } + + type SchemaProposalReview { + """ + A UUID unique to this review. Used for querying. + """ + id: ID! + + """ + Comments attached to this review. + """ + comments(after: String, first: Int! = 200): SchemaProposalCommentConnection + + """ + When the review was first made. Only a review's comments are mutable, so there is no + updatedAt on the review. + """ + createdAt: DateTime! + + """ + The text on the line at the time of being reviewed. This should be displayed if that + coordinate's text has been modified + + If the "lineText" is null then this review references the entire SchemaProposalVersion + and not any specific line within the proposal. + """ + lineText: String + + """ + The coordinate being referenced by this review. This is the most accurate location and should be used prior + to falling back to the lineNumber. Only if this coordinate does not exist in the comparing schema, should the line number be used. + """ + schemaCoordinate: String + + """ + Name of the service if reviewing a specific service's schema. + Else an empty string. + """ + serviceName: String! + + # @todo + # """ + # The specific version of the proposal that this review is for. + # """ + # schemaProposalVersion: SchemaProposalVersion + + """ + If null then this review is just a comment. Otherwise, the reviewer changed the state of the + proposal as part of their review. E.g. The reviewer can approve a version with a comment. + """ + stageTransition: SchemaProposalStage + + """ + The stored author of this review. Does not update if a user changes their name for the time being. + """ + author: String! + } + + type SchemaProposalComment { + id: ID! + + """ + When the comment was initially posted + """ + createdAt: DateTime! + + """ + Content of this comment. E.g. "Nice job!" + """ + body: String! + + """ + If edited, then when it was last edited. + """ + updatedAt: DateTime + + """ + The stored author of this comment. Does not update if a user changes their name for the time being. + """ + author: String! + } + + extend type SchemaChange { + meta: SchemaChangeMeta + } + + union SchemaChangeMeta = + | FieldArgumentDefaultChanged + | FieldArgumentDescriptionChanged + | FieldArgumentTypeChanged + | DirectiveRemoved + | DirectiveAdded + | DirectiveDescriptionChanged + | DirectiveLocationAdded + | DirectiveLocationRemoved + | DirectiveArgumentAdded + | DirectiveArgumentRemoved + | DirectiveArgumentDescriptionChanged + | DirectiveArgumentDefaultValueChanged + | DirectiveArgumentTypeChanged + | EnumValueRemoved + | EnumValueAdded + | EnumValueDescriptionChanged + | EnumValueDeprecationReasonChanged + | EnumValueDeprecationReasonAdded + | EnumValueDeprecationReasonRemoved + | FieldRemoved + | FieldAdded + | FieldDescriptionChanged + | FieldDescriptionAdded + | FieldDescriptionRemoved + | FieldDeprecationAdded + | FieldDeprecationRemoved + | FieldDeprecationReasonChanged + | FieldDeprecationReasonAdded + | FieldDeprecationReasonRemoved + | FieldTypeChanged + | DirectiveUsageUnionMemberAdded + | DirectiveUsageUnionMemberRemoved + | FieldArgumentAdded + | FieldArgumentRemoved + | InputFieldRemoved + | InputFieldAdded + | InputFieldDescriptionAdded + | InputFieldDescriptionRemoved + | InputFieldDescriptionChanged + | InputFieldDefaultValueChanged + | InputFieldTypeChanged + | ObjectTypeInterfaceAdded + | ObjectTypeInterfaceRemoved + | SchemaQueryTypeChanged + | SchemaMutationTypeChanged + | SchemaSubscriptionTypeChanged + | TypeRemoved + | TypeAdded + | TypeKindChanged + | TypeDescriptionChanged + | TypeDescriptionAdded + | TypeDescriptionRemoved + | UnionMemberRemoved + | UnionMemberAdded + | DirectiveUsageEnumAdded + | DirectiveUsageEnumRemoved + | DirectiveUsageEnumValueAdded + | DirectiveUsageEnumValueRemoved + | DirectiveUsageInputObjectRemoved + | DirectiveUsageInputObjectAdded + | DirectiveUsageInputFieldDefinitionAdded + | DirectiveUsageInputFieldDefinitionRemoved + | DirectiveUsageFieldAdded + | DirectiveUsageFieldRemoved + | DirectiveUsageScalarAdded + | DirectiveUsageScalarRemoved + | DirectiveUsageObjectAdded + | DirectiveUsageObjectRemoved + | DirectiveUsageInterfaceAdded + | DirectiveUsageSchemaAdded + | DirectiveUsageSchemaRemoved + | DirectiveUsageFieldDefinitionAdded + | DirectiveUsageFieldDefinitionRemoved + | DirectiveUsageArgumentDefinitionRemoved + | DirectiveUsageInterfaceRemoved + | DirectiveUsageArgumentDefinitionAdded + | DirectiveUsageArgumentAdded + | DirectiveUsageArgumentRemoved + + # Directive + + type FieldArgumentDescriptionChanged { + typeName: String! + fieldName: String! + argumentName: String! + oldDescription: String + newDescription: String + } + + type FieldArgumentDefaultChanged { + typeName: String! + fieldName: String! + argumentName: String! + oldDefaultValue: String + newDefaultValue: String + } + + type FieldArgumentTypeChanged { + typeName: String! + fieldName: String! + argumentName: String! + oldArgumentType: String! + newArgumentType: String! + isSafeArgumentTypeChange: Boolean + } + + type DirectiveRemoved { + removedDirectiveName: String! + } + + type DirectiveAdded { + addedDirectiveName: String! + addedDirectiveRepeatable: Boolean + addedDirectiveLocations: [String!]! + addedDirectiveDescription: String + } + + type DirectiveDescriptionChanged { + directiveName: String! + oldDirectiveDescription: String + newDirectiveDescription: String + } + + type DirectiveLocationAdded { + directiveName: String! + addedDirectiveLocation: String! + } + + type DirectiveLocationRemoved { + directiveName: String! + removedDirectiveLocation: String! + } + + type DirectiveArgumentAdded { + directiveName: String! + addedDirectiveArgumentName: String! + addedDirectiveArgumentTypeIsNonNull: Boolean + addedToNewDirective: Boolean + addedDirectiveArgumentDescription: String + addedDirectiveArgumentType: String! + addedDirectiveDefaultValue: String + } + + type DirectiveArgumentRemoved { + directiveName: String! + removedDirectiveArgumentName: String! + } + + type DirectiveArgumentDescriptionChanged { + directiveName: String! + directiveArgumentName: String! + oldDirectiveArgumentDescription: String + newDirectiveArgumentDescription: String + } + + type DirectiveArgumentDefaultValueChanged { + directiveName: String! + directiveArgumentName: String! + oldDirectiveArgumentDefaultValue: String + newDirectiveArgumentDefaultValue: String + } + + type DirectiveArgumentTypeChanged { + directiveName: String! + directiveArgumentName: String! + oldDirectiveArgumentType: String! + newDirectiveArgumentType: String! + isSafeDirectiveArgumentTypeChange: Boolean + } + + # Enum + + type EnumValueRemoved { + enumName: String! + removedEnumValueName: String! + isEnumValueDeprecated: Boolean + } + + type EnumValueAdded { + enumName: String! + addedEnumValueName: String! + addedToNewType: Boolean + addedDirectiveDescription: String + } + + type EnumValueDescriptionChanged { + enumName: String! + enumValueName: String! + oldEnumValueDescription: String + newEnumValueDescription: String + } + + type EnumValueDeprecationReasonChanged { + enumName: String! + enumValueName: String! + oldEnumValueDeprecationReason: String! + newEnumValueDeprecationReason: String! + } + + type EnumValueDeprecationReasonAdded { + enumName: String! + enumValueName: String! + addedValueDeprecationReason: String! + } + + type EnumValueDeprecationReasonRemoved { + enumName: String! + enumValueName: String! + removedEnumValueDeprecationReason: String! + } + + # Field + + type FieldRemoved { + typeName: String! + removedFieldName: String! + isRemovedFieldDeprecated: Boolean + typeType: String! + } + + type FieldAdded { + typeName: String! + addedFieldName: String! + typeType: String! + addedFieldReturnType: String! + } + + type FieldDescriptionChanged { + typeName: String! + fieldName: String! + oldDescription: String + newDescription: String + } + + type FieldDescriptionAdded { + typeName: String! + fieldName: String! + addedDescription: String! + } + + type FieldDescriptionRemoved { + typeName: String! + fieldName: String! + } + + type FieldDeprecationAdded { + typeName: String! + fieldName: String! + deprecationReason: String! + } + + type FieldDeprecationRemoved { + typeName: String! + fieldName: String! + } + + type FieldDeprecationReasonChanged { + typeName: String! + fieldName: String! + oldDeprecationReason: String! + newDeprecationReason: String! + } + + type FieldDeprecationReasonAdded { + typeName: String! + fieldName: String! + addedDeprecationReason: String! + } + + type FieldDeprecationReasonRemoved { + typeName: String! + fieldName: String! + } + + type FieldTypeChanged { + typeName: String! + fieldName: String! + oldFieldType: String! + newFieldType: String! + isSafeFieldTypeChange: Boolean + } + + type DirectiveUsageUnionMemberAdded { + unionName: String! + addedUnionMemberTypeName: String! + addedDirectiveName: String! + addedToNewType: Boolean + } + + type DirectiveUsageUnionMemberRemoved { + unionName: String! + removedUnionMemberTypeName: String! + removedDirectiveName: String! + } + + type FieldArgumentAdded { + typeName: String! + fieldName: String! + addedArgumentName: String! + addedArgumentType: String! + hasDefaultValue: Boolean + isAddedFieldArgumentBreaking: Boolean + addedToNewField: Boolean + } + + type FieldArgumentRemoved { + typeName: String! + fieldName: String! + removedFieldArgumentName: String! + removedFieldType: String! + } + + # Input + + type InputFieldRemoved { + inputName: String! + removedFieldName: String! + isInputFieldDeprecated: Boolean + } + + type InputFieldAdded { + inputName: String! + addedInputFieldName: String! + isAddedInputFieldTypeNullable: Boolean + addedInputFieldType: String! + addedFieldDefault: String + addedToNewType: Boolean + } + + type InputFieldDescriptionAdded { + inputName: String! + inputFieldName: String! + addedInputFieldDescription: String! + } + + type InputFieldDescriptionRemoved { + inputName: String! + inputFieldName: String! + removedDescription: String! + } + + type InputFieldDescriptionChanged { + inputName: String! + inputFieldName: String! + oldInputFieldDescription: String! + newInputFieldDescription: String! + } + + type InputFieldDefaultValueChanged { + inputName: String! + inputFieldName: String! + oldDefaultValue: String + newDefaultValue: String + } + + type InputFieldTypeChanged { + inputName: String! + inputFieldName: String! + oldInputFieldType: String! + newInputFieldType: String! + isInputFieldTypeChangeSafe: Boolean + } + + # Type + + type ObjectTypeInterfaceAdded { + objectTypeName: String! + addedInterfaceName: String! + addedToNewType: Boolean + } + + type ObjectTypeInterfaceRemoved { + objectTypeName: String! + removedInterfaceName: String! + } + + # Schema + + type SchemaQueryTypeChanged { + oldQueryTypeName: String! + newQueryTypeName: String! + } + + type SchemaMutationTypeChanged { + oldMutationTypeName: String! + newMutationTypeName: String! + } + + type SchemaSubscriptionTypeChanged { + oldSubscriptionTypeName: String! + newSubscriptionTypeName: String! + } + + # Type + + type TypeRemoved { + removedTypeName: String! + } + + type TypeAdded { + addedTypeName: String! + addedTypeKind: String! + } + + type TypeKindChanged { + typeName: String! + oldTypeKind: String! + newTypeKind: String! + } + + type TypeDescriptionChanged { + typeName: String! + oldTypeDescription: String! + newTypeDescription: String! + } + + type TypeDescriptionAdded { + typeName: String! + addedTypeDescription: String! + } + + type TypeDescriptionRemoved { + typeName: String! + removedTypeDescription: String! + } + + # Union + + type UnionMemberRemoved { + unionName: String! + removedUnionMemberTypeName: String! + } + + type UnionMemberAdded { + unionName: String! + addedUnionMemberTypeName: String! + addedToNewType: Boolean + } + + # Directive Usage + + type DirectiveUsageEnumAdded { + enumName: String! + addedDirectiveName: String! + addedToNewType: Boolean + } + + type DirectiveUsageEnumRemoved { + enumName: String! + removedDirectiveName: String! + } + + type DirectiveUsageEnumValueAdded { + enumName: String! + enumValueName: String! + addedDirectiveName: String! + addedToNewType: Boolean + } + + type DirectiveUsageEnumValueRemoved { + enumName: String! + enumValueName: String! + removedDirectiveName: String! + } + + type DirectiveUsageInputObjectRemoved { + inputObjectName: String! + removedInputFieldName: String! + isRemovedInputFieldTypeNullable: Boolean + removedInputFieldType: String! + removedDirectiveName: String! + } + + type DirectiveUsageInputObjectAdded { + inputObjectName: String! + addedInputFieldName: String! + isAddedInputFieldTypeNullable: Boolean + addedInputFieldType: String! + addedDirectiveName: String! + addedToNewType: Boolean + } + + type DirectiveUsageInputFieldDefinitionAdded { + inputObjectName: String! + inputFieldName: String! + inputFieldType: String! + addedDirectiveName: String! + addedToNewType: Boolean + } + + type DirectiveUsageInputFieldDefinitionRemoved { + inputObjectName: String! + inputFieldName: String! + removedDirectiveName: String! + } + + type DirectiveUsageFieldAdded { + typeName: String! + fieldName: String! + addedDirectiveName: String! + } + + type DirectiveUsageFieldRemoved { + typeName: String! + fieldName: String! + removedDirectiveName: String! + } + + type DirectiveUsageScalarAdded { + scalarName: String! + addedDirectiveName: String! + addedToNewType: Boolean + } + + type DirectiveUsageScalarRemoved { + scalarName: String! + removedDirectiveName: String! + } + + type DirectiveUsageObjectAdded { + objectName: String! + addedDirectiveName: String! + addedToNewType: Boolean + } + + type DirectiveUsageObjectRemoved { + objectName: String! + removedDirectiveName: String! + } + + type DirectiveUsageInterfaceAdded { + interfaceName: String! + addedDirectiveName: String! + addedToNewType: Boolean + } + + type DirectiveUsageSchemaAdded { + addedDirectiveName: String! + schemaTypeName: String! + addedToNewType: Boolean + } + + type DirectiveUsageSchemaRemoved { + removedDirectiveName: String! + schemaTypeName: String! + } + + type DirectiveUsageFieldDefinitionAdded { + typeName: String! + fieldName: String! + addedDirectiveName: String! + addedToNewType: Boolean + } + + type DirectiveUsageFieldDefinitionRemoved { + typeName: String! + fieldName: String! + removedDirectiveName: String! + } + + type DirectiveUsageArgumentDefinitionRemoved { + typeName: String! + fieldName: String! + argumentName: String! + removedDirectiveName: String! + } + + type DirectiveUsageInterfaceRemoved { + interfaceName: String! + removedDirectiveName: String! + } + + type DirectiveUsageArgumentDefinitionAdded { + typeName: String! + fieldName: String! + argumentName: String! + addedDirectiveName: String! + addedToNewType: Boolean + } + + type DirectiveUsageArgumentAdded { + directiveName: String! + addedArgumentName: String! + addedArgumentValue: String! + oldArgumentValue: String + parentTypeName: String + parentFieldName: String + parentArgumentName: String + parentEnumValueName: String + } + + type DirectiveUsageArgumentRemoved { + directiveName: String! + removedArgumentName: String! + parentTypeName: String + parentFieldName: String + parentArgumentName: String + parentEnumValueName: String + } +`; diff --git a/packages/services/api/src/modules/proposals/providers/schema-proposal-manager.ts b/packages/services/api/src/modules/proposals/providers/schema-proposal-manager.ts new file mode 100644 index 0000000000..d173395b75 --- /dev/null +++ b/packages/services/api/src/modules/proposals/providers/schema-proposal-manager.ts @@ -0,0 +1,208 @@ +/** + * This wraps the higher level logic with schema proposals. + */ +import { Injectable, Scope } from 'graphql-modules'; +import { TargetReferenceInput } from 'packages/libraries/core/src/client/__generated__/types'; +import { HiveError } from '@hive/api/shared/errors'; +import { SchemaChangeType } from '@hive/storage'; +import { SchemaProposalCheckInput, SchemaProposalStage } from '../../../__generated__/types'; +import { Session } from '../../auth/lib/authz'; +import { SchemaPublisher } from '../../schema/providers/schema-publisher'; +import { IdTranslator } from '../../shared/providers/id-translator'; +import { Logger } from '../../shared/providers/logger'; +import { Storage } from '../../shared/providers/storage'; +import { SchemaProposalStorage } from './schema-proposal-storage'; + +@Injectable({ + scope: Scope.Operation, +}) +export class SchemaProposalManager { + private logger: Logger; + + constructor( + logger: Logger, + private proposalStorage: SchemaProposalStorage, + private storage: Storage, + private session: Session, + private idTranslator: IdTranslator, + private schemaPublisher: SchemaPublisher, + ) { + this.logger = logger.child({ source: 'SchemaProposalsManager' }); + } + + async proposeSchema(args: { + target: TargetReferenceInput; + title: string; + description: string; + isDraft: boolean; + user: { + id: string; + displayName: string; + } | null; + initialChecks: ReadonlyArray; + }) { + const selector = await this.idTranslator.resolveTargetReference({ reference: args.target }); + if (selector === null) { + this.session.raise('schemaProposal:modify'); + } + + const createProposalResult = await this.proposalStorage.createProposal({ + organizationId: selector.organizationId, + userId: args.user?.id ?? null, + description: args.description, + stage: args.isDraft ? 'DRAFT' : 'OPEN', + targetId: selector.targetId, + title: args.title, + }); + + if (createProposalResult.type === 'error') { + return createProposalResult; + } + + const proposal = createProposalResult.proposal; + const changes: SchemaChangeType[] = []; + const checkPromises = args.initialChecks.map(async check => { + const result = await this.schemaPublisher.check({ + ...check, + service: check.service?.toLowerCase(), + target: { byId: selector.targetId }, + schemaProposalId: proposal.id, + }); + if ('changes' in result && result.changes) { + changes.push(...result.changes); + return { + ...result, + changes: result.changes, + errors: + result.errors?.map(error => ({ + ...error, + path: 'path' in error ? error.path?.split('.') : null, + })) ?? [], + }; + } + }); + + // @todo handle errors... rollback? + const checks = await Promise.all(checkPromises); + + // @todo consider mapping this here vs using the nested resolver... This is more efficient but riskier bc logic lives in two places. + // const checkEdges = checks.map(check => ({ + // node: check, + // cursor: 'schemaCheck' in check && encodeCreatedAtAndUUIDIdBasedCursor({ id: check.schemaCheck!.id, createdAt: check.schemaCheck!.createdAt} ) || undefined, + // })) as any; // @todo + return { + type: 'ok' as const, + schemaProposal: { + title: proposal.title, + description: proposal.description, + id: proposal.id, + createdAt: proposal.createdAt, + updatedAt: proposal.updatedAt, + stage: proposal.stage, + targetId: proposal.targetId, + reviews: null, + author: args.user?.displayName ?? '', + commentsCount: 0, + // checks: { + // edges: checkEdges, + // pageInfo: { + // hasNextPage: false, + // hasPreviousPage: false, + // startCursor: checkEdges[0]?.cursor || '', + // endCursor: checkEdges[checkEdges.length -1]?.cursor || '', + // }, + // }, + // rebasedSchemaSDL(checkId: ID): [SubgraphSchema!] + // rebasedSupergraphSDL(versionId: ID): String + }, + }; + } + + async getProposal(args: { id: string }) { + return this.proposalStorage.getProposal(args); + } + + async getPaginatedReviews(args: { + proposalId: string; + first: number; + after: string; + stages: SchemaProposalStage[]; + authors: string[]; + }) { + this.logger.debug('Get paginated reviews (target=%s, after=%s)', args.proposalId, args.after); + + return this.proposalStorage.getPaginatedReviews(args); + } + + async getPaginatedProposals(args: { + target: TargetReferenceInput; + first: number; + after: string; + stages: ReadonlyArray; + users: ReadonlyArray; + }) { + this.logger.debug( + 'Get paginated proposals (target=%s, after=%s, stages=%s)', + args.target.bySelector?.targetSlug || args.target.byId, + args.after, + args.stages.join(','), + ); + const selector = await this.idTranslator.resolveTargetReference({ + reference: args.target, + }); + if (selector === null) { + this.session.raise('schemaProposal:modify'); + } + + return this.proposalStorage.getPaginatedProposals({ + targetId: selector.targetId, + after: args.after, + first: args.first, + stages: args.stages, + users: args.users, + }); + } + + async reviewProposal(args: { + proposalId: string; + stage: SchemaProposalStage | null; + body: string | null; + serviceName: string; + }) { + this.logger.debug(`Reviewing proposal (proposal=%s, stage=%s)`, args.proposalId, args.stage); + + // @todo check permissions for user + const proposal = await this.proposalStorage.getProposal({ id: args.proposalId }); + const user = await this.session.getViewer(); + const target = await this.storage.getTargetById(proposal.targetId); + + if (!target) { + throw new HiveError('Proposal target lookup failed.'); + } + + if (args.stage) { + const review = await this.proposalStorage.manuallyTransitionProposal({ + organizationId: target.orgId, + targetId: proposal.targetId, + id: args.proposalId, + stage: args.stage, + userId: user.id, + serviceName: args.serviceName, + }); + + if (review.type === 'error') { + return review; + } + + return { + ...review, + review: { + ...review.review, + author: user.displayName, + }, + }; + } + + throw new HiveError('Not implemented'); + } +} diff --git a/packages/services/api/src/modules/proposals/providers/schema-proposal-storage.ts b/packages/services/api/src/modules/proposals/providers/schema-proposal-storage.ts new file mode 100644 index 0000000000..c9db2ee8be --- /dev/null +++ b/packages/services/api/src/modules/proposals/providers/schema-proposal-storage.ts @@ -0,0 +1,422 @@ +/** + * This wraps the database calls for schema proposals and required validation + */ +import { Inject, Injectable, Scope } from 'graphql-modules'; +import { sql, type DatabasePool } from 'slonik'; +import { z } from 'zod'; +import { + decodeCreatedAtAndUUIDIdBasedCursor, + encodeCreatedAtAndUUIDIdBasedCursor, +} from '@hive/storage'; +import { SchemaProposalStage } from '../../../__generated__/types'; +import { Logger } from '../../shared/providers/logger'; +import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool'; +import { Storage } from '../../shared/providers/storage'; +import { SCHEMA_PROPOSALS_ENABLED } from './schema-proposals-enabled-token'; + +const SchemaProposalsTitleModel = z + .string() + .min(1, 'Must be at least 1 character long') + .max(64, 'Must be at most 64 characters long'); + +const SchemaProposalsDescriptionModel = z + .string() + .trim() + .max(1024, 'Must be at most 1024 characters long') + .default(''); + +const noAccessToSchemaProposalsMessage = + 'This organization has no access to schema proposals. Please contact the Hive team for early access.'; + +@Injectable({ + scope: Scope.Operation, +}) +export class SchemaProposalStorage { + private logger: Logger; + + constructor( + logger: Logger, + @Inject(PG_POOL_CONFIG) private pool: DatabasePool, + private storage: Storage, + @Inject(SCHEMA_PROPOSALS_ENABLED) private schemaProposalsEnabled: Boolean, // @todo + ) { + this.logger = logger.child({ source: 'SchemaProposalStorage' }); + } + + private async assertSchemaProposalsEnabled(args: { + organizationId: string; + targetId: string; + proposalId?: string; + }) { + if (this.schemaProposalsEnabled === false) { + const organization = await this.storage.getOrganization({ + organizationId: args.organizationId, + }); + if (organization.featureFlags.appDeployments === false) { + this.logger.debug( + 'organization has no access to schema proposals (target=%s, proposal=%s)', + args.targetId, + args.proposalId, + ); + return { + type: 'error' as const, + error: { + message: noAccessToSchemaProposalsMessage, + details: null, + }, + }; + } + } + } + + async manuallyTransitionProposal(args: { + organizationId: string; + targetId: string; + id: string; + stage: SchemaProposalStage; + userId: string; + serviceName: string; + }) { + this.logger.debug( + 'manually transition schema (proposal=%s, stage=%s, userId=%s)', + args.id, + args.stage, + args.userId, + ); + + this.assertSchemaProposalsEnabled({ + organizationId: args.organizationId, + targetId: args.targetId, + proposalId: undefined, + }); + + const stageValidationResult = ManualTransitionStageModel.safeParse(args.stage); + if (stageValidationResult.error) { + return { + type: 'error' as const, + error: { + message: 'Invalid input', + details: { + stage: stageValidationResult.error?.issues[0].message ?? null, + }, + }, + }; + } + const review = await this.pool.transaction(async conn => { + await conn.maybeOne( + sql` + UPDATE "schema_proposals" + SET "stage" = ${args.stage} + WHERE "id" = ${args.id} AND "stage" <> 'IMPLEMENTED' + `, + ); + const row = await conn.maybeOne(sql` + INSERT INTO schema_proposal_reviews + ("schema_proposal_id", "stage_transition", "user_id", "service_name") + VALUES ( + ${args.id} + , ${args.stage} + , ${args.userId} + , ${args.serviceName} + ) + RETURNING ${schemaProposalReviewFields} + `); + return SchemaProposalReviewModel.parse(row); + }); + + console.log(JSON.stringify(review)); + + return { + type: 'ok' as const, + review, + }; + } + + async createProposal(args: { + organizationId: string; + targetId: string; + title: string; + description: string; + stage: SchemaProposalStage; + userId: string | null; + }) { + this.logger.debug( + 'propose schema (targetId=%s, title=%s, stage=%s)', + args.targetId, + args.title, + args.stage, + ); + + this.assertSchemaProposalsEnabled({ + organizationId: args.organizationId, + targetId: args.targetId, + proposalId: undefined, + }); + + const titleValidationResult = SchemaProposalsTitleModel.safeParse(args.title); + const descriptionValidationResult = SchemaProposalsDescriptionModel.safeParse(args.description); + if (titleValidationResult.error || descriptionValidationResult.error) { + return { + type: 'error' as const, + error: { + message: 'Invalid input', + details: { + title: titleValidationResult.error?.issues[0].message ?? null, + description: descriptionValidationResult.error?.issues[0].message ?? null, + }, + }, + }; + } + const proposal = await this.pool + .maybeOne( + sql` + INSERT INTO "schema_proposals" as "sp" + ("target_id", "title", "description", "stage", "user_id") + VALUES + ( + ${args.targetId} + , ${args.title} + , ${args.description} + , ${args.stage} + , ${args.userId} + ) + RETURNING ${schemaProposalFields} + `, + ) + .then(row => SchemaProposalModel.parse(row)); + + return { + type: 'ok' as const, + proposal, + }; + } + + async getProposal(args: { id: string }) { + this.logger.debug('Get proposal (proposal=%s)', args.id); + const result = await this.pool + .maybeOne( + sql` + SELECT + ${schemaProposalFields} + , u."display_name" as "author" + FROM + "schema_proposals" AS "sp" + LEFT JOIN "users" AS "u" + ON "u"."id" = "sp"."user_id" + WHERE + "sp"."id" = ${args.id} + LIMIT 1 + `, + ) + .then(row => SchemaProposalModel.parse(row)); + + return result; + } + + async getPaginatedProposals(args: { + targetId: string; + first: number; + after: string; + stages: ReadonlyArray; + users: ReadonlyArray; + }) { + this.logger.debug( + 'Get paginated proposals (target=%s, after=%s, stages=%s)', + args.targetId, + args.after, + args.stages.join(','), + ); + const limit = args.first ? (args.first > 0 ? Math.min(args.first, 20) : 20) : 20; + const cursor = args.after ? decodeCreatedAtAndUUIDIdBasedCursor(args.after) : null; + + this.logger.debug( + 'Select by target ID (targetId=%s, cursor=%s, limit=%d)', + args.targetId, + cursor, + limit, + ); + const result = await this.pool.query(sql` + SELECT + ${schemaProposalFields} + , u."display_name" as "author" + FROM + "schema_proposals" as "sp" + LEFT JOIN "users" as "u" + ON u."id" = sp."user_id" + WHERE + sp."target_id" = ${args.targetId} + ${ + cursor + ? sql` + AND ( + ( + sp."created_at" = ${cursor.createdAt} + AND sp."id" < ${cursor.id} + ) + OR sp."created_at" < ${cursor.createdAt} + ) + ` + : sql`` + } + ${ + args.stages.length > 0 + ? sql` + AND ( + sp."stage" = ANY(${sql.array(args.stages, 'schema_proposal_stage')}) + ) + ` + : sql`` + } + ${ + args.users.length > 0 + ? sql` + AND ( + sp."user_id" = ANY(${sql.array(args.users, 'uuid')}) + ) + ` + : sql`` + } + ORDER BY sp."created_at" DESC, sp."id" + LIMIT ${limit + 1} + `); + + let items = result.rows.map(row => { + const node = SchemaProposalModel.parse(row); + + return { + cursor: encodeCreatedAtAndUUIDIdBasedCursor(node), + node, + }; + }); + + const hasNextPage = items.length > limit; + items = items.slice(0, limit); + + return { + edges: items, + pageInfo: { + hasNextPage, + hasPreviousPage: cursor !== null, + endCursor: items[items.length - 1]?.cursor ?? '', + startCursor: items[0]?.cursor ?? '', + }, + }; + } + + async getPaginatedReviews(args: { + proposalId: string; + first: number; + after: string; + authors: string[]; + }) { + this.logger.debug('Get paginated reviews (proposal=%s, after=%s)', args.proposalId, args.after); + const limit = args.first ? (args.first > 0 ? Math.min(args.first, 20) : 20) : 20; + const cursor = args.after ? decodeCreatedAtAndUUIDIdBasedCursor(args.after) : null; + + this.logger.debug( + 'Select by proposalId ID (proposal=%s, cursor=%s, limit=%d)', + args.proposalId, + cursor, + limit, + ); + const result = await this.pool.query(sql` + SELECT + ${schemaProposalReviewFields} + FROM + "schema_proposal_reviews" + WHERE + "schema_proposal_id" = ${args.proposalId} + ${ + cursor + ? sql` + AND ( + ( + "created_at" = ${cursor.createdAt} + AND "id" < ${cursor.id} + ) + OR "created_at" < ${cursor.createdAt} + ) + ` + : sql`` + } + ORDER BY "created_at" DESC, "id" + LIMIT ${limit + 1} + `); + + let items = result.rows.map(row => { + const node = SchemaProposalReviewModel.parse(row); + + return { + cursor: encodeCreatedAtAndUUIDIdBasedCursor(node), + node, + }; + }); + + const hasNextPage = items.length > limit; + items = items.slice(0, limit); + + return { + edges: items, + pageInfo: { + hasNextPage, + hasPreviousPage: cursor !== null, + endCursor: items[items.length - 1]?.cursor ?? '', + startCursor: items[0]?.cursor ?? '', + }, + }; + } +} + +const schemaProposalFields = sql` + sp."id" + , to_json(sp."created_at") as "createdAt" + , to_json(sp."updated_at") as "updatedAt" + , sp."title" + , sp."description" + , sp."stage" + , sp."target_id" as "targetId" + , sp."user_id" as "userId" + , sp."comments_count" as "commentsCount" +`; + +const schemaProposalReviewFields = sql` + "id" + , "schema_proposal_id" + , to_json("created_at") as "createdAt" + , "stage_transition" as "stageTransition" + , "user_id" as "userId" + , "line_text" as "lineText" + , "schema_coordinate" as "schemaCoordinate" + , "resolved_by_user_id" as "resolvedByUserId" + , "service_name" as "serviceName" +`; + +const ManualTransitionStageModel = z.enum(['DRAFT', 'OPEN', 'APPROVED', 'CLOSED']); + +const SchemaProposalReviewModel = z.object({ + id: z.string(), + createdAt: z.string(), + stageTransition: ManualTransitionStageModel, + userId: z.string().nullable().optional().default(null), // if deleted + lineText: z.string().nullable().optional().default(null), + schemaCoordinate: z.string().nullable().optional().default(null), + resolvedByUserId: z.string().nullable().optional().default(null), + serviceName: z.string(), +}); + +const StageModel = z.enum(['DRAFT', 'OPEN', 'APPROVED', 'IMPLEMENTED', 'CLOSED']); + +const SchemaProposalModel = z.object({ + id: z.string(), + createdAt: z.string(), + updatedAt: z.string(), + title: z.string(), + description: z.string(), + stage: StageModel, + targetId: z.string(), + author: z.string().nullable().default('none'), + commentsCount: z.number(), +}); + +export type SchemaProposalRecord = z.infer; +export type SchemaProposalReviewRecord = z.infer; diff --git a/packages/services/api/src/modules/proposals/providers/schema-proposals-enabled-token.ts b/packages/services/api/src/modules/proposals/providers/schema-proposals-enabled-token.ts new file mode 100644 index 0000000000..d2f4085e51 --- /dev/null +++ b/packages/services/api/src/modules/proposals/providers/schema-proposals-enabled-token.ts @@ -0,0 +1,3 @@ +import { InjectionToken } from 'graphql-modules'; + +export const SCHEMA_PROPOSALS_ENABLED = new InjectionToken('SCHEMA_PROPOSALS_ENABLED'); diff --git a/packages/services/api/src/modules/proposals/resolvers/Mutation/commentOnSchemaProposalReview.ts b/packages/services/api/src/modules/proposals/resolvers/Mutation/commentOnSchemaProposalReview.ts new file mode 100644 index 0000000000..4ed39e2842 --- /dev/null +++ b/packages/services/api/src/modules/proposals/resolvers/Mutation/commentOnSchemaProposalReview.ts @@ -0,0 +1,13 @@ +import type { MutationResolvers } from '../../../../__generated__/types'; + +export const commentOnSchemaProposalReview: NonNullable< + MutationResolvers['commentOnSchemaProposalReview'] +> = async (_parent, { input: { body } }, _ctx) => { + /* Implement Mutation.commentOnSchemaProposalReview resolver logic here */ + return { + createdAt: Date.now(), + id: crypto.randomUUID(), + updatedAt: Date.now(), + body, + }; +}; diff --git a/packages/services/api/src/modules/proposals/resolvers/Mutation/createSchemaProposal.ts b/packages/services/api/src/modules/proposals/resolvers/Mutation/createSchemaProposal.ts new file mode 100644 index 0000000000..a25f071915 --- /dev/null +++ b/packages/services/api/src/modules/proposals/resolvers/Mutation/createSchemaProposal.ts @@ -0,0 +1,56 @@ +import { SchemaProposalManager } from '../../providers/schema-proposal-manager'; +import type { MutationResolvers } from './../../../../__generated__/types'; + +export const createSchemaProposal: NonNullable = async ( + _, + { input }, + { injector, session }, +) => { + const { target, title, description, isDraft, initialChecks } = input; + let user: { + id: string; + displayName: string; + } | null = null; + try { + const actor = await session.getActor(); + if (actor.type === 'user') { + user = { + id: actor.user.id, + displayName: actor.user.displayName, + }; + } + } catch (e) { + // ignore + } + + const result = await injector.get(SchemaProposalManager).proposeSchema({ + target, + title, + description: description ?? '', + isDraft: isDraft ?? false, + user: user + ? { + id: user.id, + displayName: user.displayName, + } + : null, + initialChecks, + }); + + if (result.type === 'error') { + return { + error: { + message: result.error.message, + details: result.error.details, + }, + ok: null, + }; + } + + return { + error: null, + ok: { + schemaProposal: result.schemaProposal, + }, + }; +}; diff --git a/packages/services/api/src/modules/proposals/resolvers/Mutation/replyToSchemaProposalReview.ts b/packages/services/api/src/modules/proposals/resolvers/Mutation/replyToSchemaProposalReview.ts new file mode 100644 index 0000000000..92fe70bf11 --- /dev/null +++ b/packages/services/api/src/modules/proposals/resolvers/Mutation/replyToSchemaProposalReview.ts @@ -0,0 +1,10 @@ +import type { MutationResolvers } from '../../../../__generated__/types'; + +export const replyToSchemaProposalReview: NonNullable< + MutationResolvers['replyToSchemaProposalReview'] +> = async (_parent, { input: { body, schemaProposalReviewId } }, { session }) => { + const user = await session.getViewer(); + return { + author: user.displayName ?? user.fullName, + } as any /** @todo */; +}; diff --git a/packages/services/api/src/modules/proposals/resolvers/Mutation/reviewSchemaProposal.ts b/packages/services/api/src/modules/proposals/resolvers/Mutation/reviewSchemaProposal.ts new file mode 100644 index 0000000000..c55e325e62 --- /dev/null +++ b/packages/services/api/src/modules/proposals/resolvers/Mutation/reviewSchemaProposal.ts @@ -0,0 +1,30 @@ +import { SchemaProposalManager } from '../../providers/schema-proposal-manager'; +import type { MutationResolvers } from './../../../../__generated__/types'; + +export const reviewSchemaProposal: NonNullable = async ( + _, + args, + { injector }, +) => { + const result = await injector.get(SchemaProposalManager).reviewProposal({ + proposalId: args.input.schemaProposalId, + stage: args.input.stageTransition ?? null, + body: args.input.commentBody ?? null, + serviceName: args.input.serviceName, + // @todo coordinate etc + }); + if (result.type === 'error') { + return { + error: { + message: result.error.message, + details: result.error.details, + }, + ok: null, + }; + } + return { + ok: { + review: result.review, + }, + }; +}; diff --git a/packages/services/api/src/modules/proposals/resolvers/Query/schemaProposal.ts b/packages/services/api/src/modules/proposals/resolvers/Query/schemaProposal.ts new file mode 100644 index 0000000000..db6765ccfb --- /dev/null +++ b/packages/services/api/src/modules/proposals/resolvers/Query/schemaProposal.ts @@ -0,0 +1,14 @@ +import { SchemaProposalManager } from '../../providers/schema-proposal-manager'; +import type { QueryResolvers } from './../../../../__generated__/types'; + +export const schemaProposal: NonNullable = async ( + _parent, + { input: { id } }, + { injector }, +) => { + const proposal = await injector.get(SchemaProposalManager).getProposal({ id }); + return { + ...proposal, + author: '', // populated in its own resolver + }; +}; diff --git a/packages/services/api/src/modules/proposals/resolvers/Query/schemaProposals.ts b/packages/services/api/src/modules/proposals/resolvers/Query/schemaProposals.ts new file mode 100644 index 0000000000..30013ceb9c --- /dev/null +++ b/packages/services/api/src/modules/proposals/resolvers/Query/schemaProposals.ts @@ -0,0 +1,16 @@ +import { SchemaProposalManager } from '../../providers/schema-proposal-manager'; +import type { QueryResolvers } from './../../../../__generated__/types'; + +export const schemaProposals: NonNullable = async ( + _, + args, + { injector }, +) => { + return injector.get(SchemaProposalManager).getPaginatedProposals({ + target: args.input.target, + first: args.first, + after: args.after ?? '', + stages: (args.input?.stages as any[]) ?? [], + users: args.input.userIds ?? [], + }); +}; diff --git a/packages/services/api/src/modules/proposals/resolvers/SchemaChange.ts b/packages/services/api/src/modules/proposals/resolvers/SchemaChange.ts new file mode 100644 index 0000000000..f1a6c7571a --- /dev/null +++ b/packages/services/api/src/modules/proposals/resolvers/SchemaChange.ts @@ -0,0 +1,17 @@ +import type { SchemaChangeResolvers } from './../../../__generated__/types'; + +export function toTitleCase(str: string) { + return str.toLowerCase().replace(/^_*(.)|_+(.)/g, (_, c: string, d: string) => { + return (c ?? d).toUpperCase(); + }); +} + +export const SchemaChange: Pick = { + meta: ({ meta, type }, _arg, _ctx) => { + // @todo consider validating + return { + __typename: toTitleCase(type), + ...(meta as any), + }; + }, +}; diff --git a/packages/services/api/src/modules/proposals/resolvers/SchemaProposal.ts b/packages/services/api/src/modules/proposals/resolvers/SchemaProposal.ts new file mode 100644 index 0000000000..2d475ec9e1 --- /dev/null +++ b/packages/services/api/src/modules/proposals/resolvers/SchemaProposal.ts @@ -0,0 +1,95 @@ +import { SchemaCheckManager } from '../../schema/providers/schema-check-manager'; +import { SchemaManager } from '../../schema/providers/schema-manager'; +import { toGraphQLSchemaCheckCurry } from '../../schema/to-graphql-schema-check'; +import { Storage } from '../../shared/providers/storage'; +import { SchemaProposalManager } from '../providers/schema-proposal-manager'; +import type { SchemaProposalResolvers } from './../../../__generated__/types'; + +// @todo +export const SchemaProposal: SchemaProposalResolvers = { + async author(proposal, _, { injector }) { + const userId = (proposal as any)?.userId; + if (userId) { + const user = await injector.get(Storage).getUserById(userId); + return user?.displayName ?? ''; + } + return ''; + }, + async rebasedSchemaSDL(proposal, args, { injector }) { + if (proposal.rebasedSchemaSDL) { + return proposal.rebasedSchemaSDL; + } + const target = await injector.get(Storage).getTargetById((proposal as any).targetId); + if (!target) { + throw new Error('uh oh'); + } + const schemaChecks = await injector + .get(SchemaManager) + .getPaginatedSchemaChecksForSchemaProposal({ + transformNode: toGraphQLSchemaCheckCurry({ + organizationId: target.orgId, + projectId: target.projectId, + }), + proposalId: proposal.id, + cursor: args.after ?? null, + first: args.first ?? null, + latest: true, + }); + + if (target) { + const latest = await injector.get(SchemaManager).getMaybeLatestValidVersion(target); + if (latest) { + const schemas = await injector.get(SchemaManager).getMaybeSchemasOfVersion(latest); + return { + edges: schemaChecks.edges.map(({ node, cursor }) => { + const schema = schemas.find( + s => + (node.serviceName === '' && s.kind === 'single') || + (s.kind === 'composite' && s.service_name === node.serviceName), + ); + return { + node: { + schemaSDL: schema?.sdl ?? '', // @todo patch + serviceName: node.serviceName, + }, + cursor, + }; + }), + pageInfo: schemaChecks.pageInfo, + }; + } + } + return null; + // @todo error if not found... + }, + async checks(proposal, args, { injector }) { + const target = await injector.get(Storage).getTargetById((proposal as any).targetId); + if (!target) { + throw new Error('oops'); + } + const schemaChecks = await injector + .get(SchemaManager) + .getPaginatedSchemaChecksForSchemaProposal({ + transformNode: toGraphQLSchemaCheckCurry({ + organizationId: target.orgId, + projectId: target.projectId, + }), + proposalId: proposal.id, + cursor: args.after ?? null, + first: args.first ?? null, + latest: args.input.latestPerService ?? false, + }); + return schemaChecks; + }, + async rebasedSupergraphSDL(proposal, args, { injector }) { + return ''; + }, + async reviews(proposal, args, { injector }) { + injector.get(SchemaProposalManager).getPaginatedReviews({ + proposalId: proposal.id, + after: args.after ?? '', + first: args.first, + }); + return proposal.reviews ?? null; + }, +}; diff --git a/packages/services/api/src/modules/schema/index.ts b/packages/services/api/src/modules/schema/index.ts index 88bc377e88..60f4f1c491 100644 --- a/packages/services/api/src/modules/schema/index.ts +++ b/packages/services/api/src/modules/schema/index.ts @@ -1,4 +1,5 @@ import { createModule } from 'graphql-modules'; +import { SchemaProposalStorage } from '../proposals/providers/schema-proposal-storage'; import { BreakingSchemaChangeUsageHelper } from './providers/breaking-schema-changes-helper'; import { Contracts } from './providers/contracts'; import { ContractsManager } from './providers/contracts-manager'; @@ -32,5 +33,6 @@ export const schemaModule = createModule({ BreakingSchemaChangeUsageHelper, CompositionOrchestrator, ...models, + SchemaProposalStorage, ], }); diff --git a/packages/services/api/src/modules/schema/module.graphql.ts b/packages/services/api/src/modules/schema/module.graphql.ts index 0f5ea13860..77fd462aeb 100644 --- a/packages/services/api/src/modules/schema/module.graphql.ts +++ b/packages/services/api/src/modules/schema/module.graphql.ts @@ -693,6 +693,11 @@ export default gql` Optional url if wanting to show subgraph url changes inside checks. """ url: String + + """ + Optional. Attaches the check to a schema proposal. + """ + schemaProposalId: ID } input SchemaDeleteInput { diff --git a/packages/services/api/src/modules/schema/providers/inspector.ts b/packages/services/api/src/modules/schema/providers/inspector.ts index b0037768ad..f0e75a18eb 100644 --- a/packages/services/api/src/modules/schema/providers/inspector.ts +++ b/packages/services/api/src/modules/schema/providers/inspector.ts @@ -10,7 +10,7 @@ import { type GraphQLSchema, } from 'graphql'; import { Injectable, Scope } from 'graphql-modules'; -import { Change, ChangeType, diff } from '@graphql-inspector/core'; +import { Change, ChangeType, diff, TypeOfChangeType } from '@graphql-inspector/core'; import { traceFn } from '@hive/service-common'; import { HiveSchemaChangeModel } from '@hive/storage'; import { Logger } from '../../shared/providers/logger'; @@ -54,7 +54,7 @@ export class Inspector { * If they are equal, it means that the change is no longer relevant and can be dropped. * All other changes are kept. */ -function dropTrimmedDescriptionChangedChange(change: Change): boolean { +function dropTrimmedDescriptionChangedChange(change: Change): boolean { return ( matchChange(change, { [ChangeType.DirectiveArgumentDescriptionChanged]: change => @@ -112,7 +112,7 @@ function trimDescription(description: unknown): string { type PropEndsWith = T extends `${any}${E}` ? T : never; function shouldKeepDescriptionChangedChange< - T extends ChangeType, + T extends TypeOfChangeType, TO extends PropEndsWith['meta'], 'Description'>, // Prevents comparing values of the same key (e.g. newDescription, newDescription will result in TS error) TN extends Exclude['meta'], 'Description'>, TO>, @@ -120,7 +120,7 @@ function shouldKeepDescriptionChangedChange< return trimDescription(change.meta[oldKey]) !== trimDescription(change.meta[newKey]); } -function matchChange( +function matchChange( change: Change, pattern: { [K in T]?: (change: Change) => R; diff --git a/packages/services/api/src/modules/schema/providers/schema-check-manager.ts b/packages/services/api/src/modules/schema/providers/schema-check-manager.ts index 0cde5cd329..9a63b89a8e 100644 --- a/packages/services/api/src/modules/schema/providers/schema-check-manager.ts +++ b/packages/services/api/src/modules/schema/providers/schema-check-manager.ts @@ -44,7 +44,7 @@ export class SchemaCheckManager { } getAllSchemaChanges(schemaCheck: SchemaCheck) { - if (!schemaCheck.safeSchemaChanges?.length || !schemaCheck.breakingSchemaChanges?.length) { + if (!schemaCheck.safeSchemaChanges?.length && !schemaCheck.breakingSchemaChanges?.length) { return null; } 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 8bd2bf5b7a..b10e61574f 100644 --- a/packages/services/api/src/modules/schema/providers/schema-manager.ts +++ b/packages/services/api/src/modules/schema/providers/schema-manager.ts @@ -356,6 +356,19 @@ export class SchemaManager { }; } + async getPaginatedSchemaChecksForSchemaProposal< + TransformedSchemaCheck extends SchemaCheck = SchemaCheck, + >(args: { + transformNode?: (check: SchemaCheck) => TransformedSchemaCheck; + proposalId: string; + first: number | null; + cursor: string | null; + latest?: boolean; + }) { + const connection = await this.storage.getPaginatedSchemaChecksForSchemaProposal(args); + return connection; + } + async getSchemaLog(selector: { commit: string } & TargetSelector) { this.logger.debug('Fetching schema log (selector=%o)', selector); return this.storage.getSchemaLog({ 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 4eb6982da3..5810f6d91c 100644 --- a/packages/services/api/src/modules/schema/providers/schema-publisher.ts +++ b/packages/services/api/src/modules/schema/providers/schema-publisher.ts @@ -27,6 +27,7 @@ import { type GitHubCheckRun, } from '../../integrations/providers/github-integration-manager'; import { OperationsReader } from '../../operations/providers/operations-reader'; +import { SchemaProposalStorage } from '../../proposals/providers/schema-proposal-storage'; import { DistributedCache } from '../../shared/providers/distributed-cache'; import { IdTranslator } from '../../shared/providers/id-translator'; import { Logger } from '../../shared/providers/logger'; @@ -87,7 +88,9 @@ const schemaDeleteCount = new promClient.Counter({ labelNames: ['model', 'projectType'], }); -export type CheckInput = Types.SchemaCheckInput; +export type CheckInput = Types.SchemaCheckInput & { + schemaProposalId?: string | null; +}; export type DeleteInput = Types.SchemaDeleteInput; @@ -142,6 +145,7 @@ export class SchemaPublisher { private schemaManager: SchemaManager, private targetManager: TargetManager, private alertsManager: AlertsManager, + private schemaProposals: SchemaProposalStorage, private gitHubIntegrationManager: GitHubIntegrationManager, private distributedCache: DistributedCache, private artifactStorageWriter: ArtifactStorageWriter, @@ -313,7 +317,7 @@ export class SchemaPublisher { }, }); - const [target, project, organization, latestVersion, latestComposableVersion] = + const [target, project, organization, latestVersion, latestComposableVersion, schemaProposal] = await Promise.all([ this.storage.getTarget({ organizationId: selector.organizationId, @@ -338,8 +342,28 @@ export class SchemaPublisher { targetId: selector.targetId, onlyComposable: true, }), + input.schemaProposalId + ? this.schemaProposals.getProposal({ + id: input.schemaProposalId, + }) + : null, ]); + if (input.schemaProposalId && schemaProposal?.targetId !== selector.targetId) { + return { + __typename: 'SchemaCheckError', + valid: false, + changes: [], + warnings: [], + errors: [ + { + message: + 'Invalid schema proposal reference. No proposal found with that ID for the target.', + }, + ], + } as const; + } + if (input.service) { let serviceExists = false; if (latestVersion?.schemas) { @@ -462,6 +486,7 @@ export class SchemaPublisher { } if (github != null) { + // @todo should proposals do anything special here? const result = await this.createGithubCheckRunStartForSchemaCheck({ organization, project, @@ -700,6 +725,7 @@ export class SchemaPublisher { breakingSchemaChanges: contract.schemaChanges?.breaking ?? null, safeSchemaChanges: contract.schemaChanges?.safe ?? null, })) ?? null, + schemaProposalId: input.schemaProposalId, }); this.logger.info('created failed schema check. (schemaCheckId=%s)', schemaCheck.id); } else if (checkResult.conclusion === SchemaCheckConclusion.Success) { @@ -745,6 +771,7 @@ export class SchemaPublisher { breakingSchemaChanges: contract.schemaChanges?.breaking ?? null, safeSchemaChanges: contract.schemaChanges?.safe ?? null, })) ?? null, + schemaProposalId: input.schemaProposalId, }); this.logger.info('created successful schema check. (schemaCheckId=%s)', schemaCheck.id); } else if (checkResult.conclusion === SchemaCheckConclusion.Skip) { @@ -822,6 +849,7 @@ export class SchemaPublisher { })), ) : null, + schemaProposalId: input.schemaProposalId, }); this.logger.info('created skipped schema check. (schemaCheckId=%s)', schemaCheck.id); } diff --git a/packages/services/api/src/modules/schema/resolvers/Mutation/schemaCheck.ts b/packages/services/api/src/modules/schema/resolvers/Mutation/schemaCheck.ts index 93160c8d92..8da9e3db98 100644 --- a/packages/services/api/src/modules/schema/resolvers/Mutation/schemaCheck.ts +++ b/packages/services/api/src/modules/schema/resolvers/Mutation/schemaCheck.ts @@ -10,6 +10,7 @@ export const schemaCheck: NonNullable = async ...input, service: input.service?.toLowerCase(), target: input.target ?? null, + schemaProposalId: input.schemaProposalId, // @todo check permission }); if ('changes' in result && result.changes) { diff --git a/packages/services/api/src/modules/schema/resolvers/SchemaChange.ts b/packages/services/api/src/modules/schema/resolvers/SchemaChange.ts index 26cac07f58..30936229de 100644 --- a/packages/services/api/src/modules/schema/resolvers/SchemaChange.ts +++ b/packages/services/api/src/modules/schema/resolvers/SchemaChange.ts @@ -18,7 +18,19 @@ const severityMap: Record = { [CriticalityLevelEnum.Breaking]: 'BREAKING', }; -export const SchemaChange: SchemaChangeResolvers = { +export const SchemaChange: Pick< + SchemaChangeResolvers, + | 'approval' + | 'criticality' + | 'criticalityReason' + | 'isSafeBasedOnUsage' + | 'message' + | 'path' + | 'severityLevel' + | 'severityReason' + | 'usageStatistics' + | '__isTypeOf' +> = { message: (change, args) => { return args.withSafeBasedOnUsageNote && change.isSafeBasedOnUsage === true ? `${change.message} (non-breaking based on usage)` diff --git a/packages/services/api/src/modules/shared/providers/storage.ts b/packages/services/api/src/modules/shared/providers/storage.ts index b64edcf0db..1167e2292c 100644 --- a/packages/services/api/src/modules/shared/providers/storage.ts +++ b/packages/services/api/src/modules/shared/providers/storage.ts @@ -405,6 +405,31 @@ export interface Storage { first: number | null; cursor: null | string; }): Promise; + // @todo consider moving to proposals provider + getPaginatedSchemaChecksForSchemaProposal< + TransformedSchemaCheck extends SchemaCheck = SchemaCheck, + >(_: { + proposalId: string; + first: number | null; + cursor: null | string; + transformNode?: (check: SchemaCheck) => TransformedSchemaCheck; + latest?: boolean; + }): Promise< + Readonly<{ + edges: ReadonlyArray<{ + // @todo consider conditionally excluding this from the query for performance + // Omit; + node: TransformedSchemaCheck; + cursor: string; + }>; + pageInfo: Readonly<{ + hasNextPage: boolean; + hasPreviousPage: boolean; + startCursor: string; + endCursor: string; + }>; + }> + >; getVersion(_: TargetSelector & { versionId: string }): Promise; deleteSchema( _: { @@ -743,7 +768,9 @@ export interface Storage { /** * Persist a schema check record in the database. */ - createSchemaCheck(_: SchemaCheckInput & { expiresAt: Date | null }): Promise; + createSchemaCheck( + _: SchemaCheckInput & { expiresAt: Date | null; schemaProposalId?: string | null }, + ): Promise; /** * Delete the expired schema checks from the database. */ diff --git a/packages/services/server/.env.template b/packages/services/server/.env.template index 2f724735e9..adaa5f5286 100644 --- a/packages/services/server/.env.template +++ b/packages/services/server/.env.template @@ -24,6 +24,7 @@ INTEGRATION_GITHUB="" INTEGRATION_GITHUB_APP_ID="" INTEGRATION_GITHUB_APP_PRIVATE_KEY="" FEATURE_FLAGS_APP_DEPLOYMENTS_ENABLED="0" +FEATURE_FLAGS_SCHEMA_PROPOSALS_ENABLED="0" # Zendesk Support ZENDESK_SUPPORT='0' diff --git a/packages/services/server/README.md b/packages/services/server/README.md index 10f8fc5981..ccff442b49 100644 --- a/packages/services/server/README.md +++ b/packages/services/server/README.md @@ -64,6 +64,7 @@ The GraphQL API for GraphQL Hive. | `INTEGRATION_GITHUB_GITHUB_APP_ID` | No (Yes if `INTEGRATION_GITHUB` is set to `1`) | The GitHub app id. | `123` | | `INTEGRATION_GITHUB_GITHUB_APP_PRIVATE_KEY` | No (Yes if `INTEGRATION_GITHUB` is set to `1`) | The GitHub app private key. | `letmein1` | | `FEATURE_FLAGS_APP_DEPLOYMENTS_ENABLED` | No | Whether app deployments should be enabled for every organization. | `1` (enabled) or `0` (disabled) | +| `FEATURE_FLAGS_SCHEMA_PROPOSALS_ENABLED` | No | Whether schema proposals should be enabled for every organization. | `1` (enabled) or `0` (disabled) | | `S3_AUDIT_LOG` | No (audit log uses default S3 if not configured) | Whether audit logs should be stored on another S3 bucket than the artifacts. | `1` (enabled) or `0` (disabled) | | `S3_AUDIT_LOG_ENDPOINT` | **Yes** (if `S3_AUDIT_LOG` is `1`) | The S3 endpoint. | `http://localhost:9000` | | `S3_AUDIT_LOG_ACCESS_KEY_ID` | **Yes** (if `S3_AUDIT_LOG` is `1`) | The S3 access key id. | `minioadmin` | diff --git a/packages/services/server/src/environment.ts b/packages/services/server/src/environment.ts index 00f74df3b9..43c1b1c6c8 100644 --- a/packages/services/server/src/environment.ts +++ b/packages/services/server/src/environment.ts @@ -42,6 +42,9 @@ const EnvironmentModel = zod.object({ FEATURE_FLAGS_APP_DEPLOYMENTS_ENABLED: emptyString( zod.union([zod.literal('1'), zod.literal('0')]).optional(), ), + FEATURE_FLAGS_SCHEMA_PROPOSALS_ENABLED: emptyString( + zod.union([zod.literal('1'), zod.literal('0')]).optional(), + ), }); const CommerceModel = zod.object({ @@ -518,5 +521,6 @@ export const env = { featureFlags: { /** Whether app deployments should be enabled by default for everyone. */ appDeploymentsEnabled: base.FEATURE_FLAGS_APP_DEPLOYMENTS_ENABLED === '1', + schemaProposalsEnabled: base.FEATURE_FLAGS_SCHEMA_PROPOSALS_ENABLED === '1', }, } as const; diff --git a/packages/services/server/src/index.ts b/packages/services/server/src/index.ts index 51716090ab..84be8f5b23 100644 --- a/packages/services/server/src/index.ts +++ b/packages/services/server/src/index.ts @@ -400,6 +400,7 @@ export async function main() { supportConfig: env.zendeskSupport, pubSub, appDeploymentsEnabled: env.featureFlags.appDeploymentsEnabled, + schemaProposalsEnabled: env.featureFlags.schemaProposalsEnabled, prometheus: env.prometheus, }); diff --git a/packages/services/storage/package.json b/packages/services/storage/package.json index 0be5aecc22..d79c7303e1 100644 --- a/packages/services/storage/package.json +++ b/packages/services/storage/package.json @@ -16,7 +16,7 @@ "db:generate": "schemats generate --config schemats.cjs -o src/db/types.ts && prettier --write src/db/types.ts" }, "devDependencies": { - "@graphql-inspector/core": "5.1.0-alpha-20231208113249-34700c8a", + "@graphql-inspector/core": "file:../../../../graphql-inspector/packages/core", "@hive/service-common": "workspace:*", "@sentry/node": "7.120.2", "@sentry/types": "7.120.2", diff --git a/packages/services/storage/src/db/types.ts b/packages/services/storage/src/db/types.ts index 2d563d08ef..56e7a8f706 100644 --- a/packages/services/storage/src/db/types.ts +++ b/packages/services/storage/src/db/types.ts @@ -11,6 +11,7 @@ export type alert_channel_type = 'MSTEAMS_WEBHOOK' | 'SLACK' | 'WEBHOOK'; export type alert_type = 'SCHEMA_CHANGE_NOTIFICATIONS'; export type breaking_change_formula = 'PERCENTAGE' | 'REQUEST_COUNT'; export type schema_policy_resource = 'ORGANIZATION' | 'PROJECT'; +export type schema_proposal_stage = 'APPROVED' | 'CLOSED' | 'DRAFT' | 'IMPLEMENTED' | 'OPEN'; export type user_role = 'ADMIN' | 'MEMBER'; export interface alert_channels { @@ -276,6 +277,7 @@ export interface schema_checks { schema_composition_errors: any | null; schema_policy_errors: any | null; schema_policy_warnings: any | null; + schema_proposal_id: string | null; schema_sdl: string | null; schema_sdl_store_id: string | null; schema_version_id: string | null; @@ -318,6 +320,39 @@ export interface schema_policy_config { updated_at: Date; } +export interface schema_proposal_comments { + body: string; + created_at: Date; + id: string; + schema_proposal_review_id: string | null; + updated_at: Date; + user_id: string | null; +} + +export interface schema_proposal_reviews { + created_at: Date; + id: string; + line_text: string | null; + resolved_by_user_id: string | null; + schema_coordinate: string | null; + schema_proposal_id: string; + service_name: string; + stage_transition: schema_proposal_stage; + user_id: string | null; +} + +export interface schema_proposals { + comments_count: number; + created_at: Date; + description: string; + id: string; + stage: schema_proposal_stage; + target_id: string; + title: string; + updated_at: Date; + user_id: string | null; +} + export interface schema_version_changes { change_type: string; id: string; @@ -451,6 +486,9 @@ export interface DBTables { schema_coordinate_status: schema_coordinate_status; schema_log: schema_log; schema_policy_config: schema_policy_config; + schema_proposal_comments: schema_proposal_comments; + schema_proposal_reviews: schema_proposal_reviews; + schema_proposals: schema_proposals; schema_version_changes: schema_version_changes; schema_version_to_log: schema_version_to_log; schema_versions: schema_versions; diff --git a/packages/services/storage/src/index.ts b/packages/services/storage/src/index.ts index 3e1ca34295..c44daa44ee 100644 --- a/packages/services/storage/src/index.ts +++ b/packages/services/storage/src/index.ts @@ -3901,6 +3901,7 @@ export async function createStorage( , "context_id" , "has_contract_schema_changes" , "conditional_breaking_change_metadata" + , "schema_proposal_id" ) VALUES ( ${schemaSDLHash} @@ -3929,6 +3930,7 @@ export async function createStorage( ) ?? false } , ${jsonify(InsertConditionalBreakingChangeMetadataModel.parse(args.conditionalBreakingChangeMetadata))} + , ${args.schemaProposalId ?? null} ) RETURNING "id" @@ -4248,6 +4250,98 @@ export async function createStorage( }; }, + async getPaginatedSchemaChecksForSchemaProposal(args) { + let cursor: null | { + createdAt: string; + id: string; + } = null; + + const limit = args.first ? (args.first > 0 ? Math.min(args.first, 20) : 20) : 20; + + if (args.cursor) { + cursor = decodeCreatedAtAndUUIDIdBasedCursor(args.cursor); + } + + // gets the most recently created schema checks per service name + const result = await pool.any(sql`/* getPaginatedSchemaChecksForSchemaProposal */ + SELECT + ${schemaCheckSQLFields} + FROM + "schema_checks" as c + ${ + args.latest + ? sql` + INNER JOIN ( + SELECT "service_name", "schema_proposal_id", max("created_at") as maxdate + FROM schema_checks + GROUP BY "service_name", "schema_proposal_id" + ) as cc + ON c."schema_proposal_id" = cc."schema_proposal_id" + AND c."service_name" = cc."service_name" + AND c."created_at" = cc."maxdate" + ` + : sql`` + } + LEFT JOIN "sdl_store" as s_schema + ON s_schema."id" = c."schema_sdl_store_id" + LEFT JOIN "sdl_store" as s_composite_schema + ON s_composite_schema."id" = c."composite_schema_sdl_store_id" + LEFT JOIN "sdl_store" as s_supergraph + ON s_supergraph."id" = c."supergraph_sdl_store_id" + WHERE + c."schema_proposal_id" = ${args.proposalId} + ${ + cursor + ? sql` + AND ( + ( + c."created_at" = ${cursor.createdAt} + AND c."id" < ${cursor.id} + ) + OR c."created_at" < ${cursor.createdAt} + ) + ` + : sql`` + } + ORDER BY + c."created_at" DESC + , c."id" DESC + LIMIT ${limit + 1} + `); + + let items = result.map(row => { + const node = SchemaCheckModel.parse(row); + + return { + get node() { + // TODO: remove this any cast and fix the type issues... + return (args.transformNode?.(node) ?? node) as any; + }, + get cursor() { + return encodeCreatedAtAndUUIDIdBasedCursor(node); + }, + }; + }); + + const hasNextPage = items.length > limit; + + items = items.slice(0, limit); + + return { + edges: items, + pageInfo: { + hasNextPage, + hasPreviousPage: cursor !== null, + get endCursor() { + return items[items.length - 1]?.cursor ?? ''; + }, + get startCursor() { + return items[0]?.cursor ?? ''; + }, + }, + }; + }, + async getTargetBreadcrumbForTargetId(args) { const result = await pool.maybeOne(sql`/* getTargetBreadcrumbForTargetId */ SELECT @@ -5174,6 +5268,7 @@ const schemaCheckSQLFields = sql` , c."manual_approval_comment" as "manualApprovalComment" , c."context_id" as "contextId" , c."conditional_breaking_change_metadata" as "conditionalBreakingChangeMetadata" + , c."schema_proposal_id" as "schemaProposalId" `; const schemaVersionSQLFields = (t = sql``) => sql` diff --git a/packages/services/storage/src/schema-change-meta.ts b/packages/services/storage/src/schema-change-meta.ts index 9562ea45f6..5009472910 100644 --- a/packages/services/storage/src/schema-change-meta.ts +++ b/packages/services/storage/src/schema-change-meta.ts @@ -13,6 +13,20 @@ import { directiveLocationAddedFromMeta, directiveLocationRemovedFromMeta, directiveRemovedFromMeta, + directiveUsageArgumentAddedFromMeta, + directiveUsageArgumentDefinitionAddedFromMeta, + directiveUsageArgumentDefinitionRemovedFromMeta, + directiveUsageArgumentRemovedFromMeta, + directiveUsageEnumAddedFromMeta, + directiveUsageEnumValueAddedFromMeta, + directiveUsageFieldDefinitionAddedFromMeta, + directiveUsageInputFieldDefinitionAddedFromMeta, + directiveUsageInputObjectAddedFromMeta, + directiveUsageInterfaceAddedFromMeta, + directiveUsageObjectAddedFromMeta, + directiveUsageScalarAddedFromMeta, + directiveUsageSchemaAddedFromMeta, + directiveUsageUnionMemberAddedFromMeta, enumValueAddedFromMeta, enumValueDeprecationReasonAddedFromMeta, enumValueDeprecationReasonChangedFromMeta, @@ -95,7 +109,7 @@ export type RegistryServiceUrlChangeChange = RegistryServiceUrlChangeSerializabl */ export function schemaChangeFromSerializableChange( change: SerializableChange, -): Change | RegistryServiceUrlChangeChange { +): Change | RegistryServiceUrlChangeChange | null { switch (change.type) { case ChangeType.FieldArgumentDescriptionChanged: return fieldArgumentDescriptionChangedFromMeta(change); @@ -201,8 +215,39 @@ export function schemaChangeFromSerializableChange( return unionMemberRemovedFromMeta(change); case ChangeType.UnionMemberAdded: return buildUnionMemberAddedMessageFromMeta(change); + case ChangeType.DirectiveUsageArgumentDefinitionAdded: + return directiveUsageArgumentDefinitionAddedFromMeta(change); + case ChangeType.DirectiveUsageArgumentDefinitionRemoved: + return directiveUsageArgumentDefinitionRemovedFromMeta(change); + case ChangeType.DirectiveUsageInputFieldDefinitionAdded: + return directiveUsageInputFieldDefinitionAddedFromMeta(change); + case ChangeType.DirectiveUsageInputObjectAdded: + return directiveUsageInputObjectAddedFromMeta(change); + case ChangeType.DirectiveUsageInterfaceAdded: + return directiveUsageInterfaceAddedFromMeta(change); + case ChangeType.DirectiveUsageObjectAdded: + return directiveUsageObjectAddedFromMeta(change); + case ChangeType.DirectiveUsageEnumAdded: + return directiveUsageEnumAddedFromMeta(change); + case ChangeType.DirectiveUsageFieldDefinitionAdded: + return directiveUsageFieldDefinitionAddedFromMeta(change); + case ChangeType.DirectiveUsageUnionMemberAdded: + return directiveUsageUnionMemberAddedFromMeta(change); + case ChangeType.DirectiveUsageEnumValueAdded: + return directiveUsageEnumValueAddedFromMeta(change); + case ChangeType.DirectiveUsageSchemaAdded: + return directiveUsageSchemaAddedFromMeta(change); + case ChangeType.DirectiveUsageScalarAdded: + return directiveUsageScalarAddedFromMeta(change); + case ChangeType.DirectiveUsageArgumentAdded: + return directiveUsageArgumentAddedFromMeta(change); + case ChangeType.DirectiveUsageArgumentRemoved: + return directiveUsageArgumentRemovedFromMeta(change); case 'REGISTRY_SERVICE_URL_CHANGED': return buildRegistryServiceURLFromMeta(change); + default: + // @todo unhandled change + return null; } } diff --git a/packages/services/storage/src/schema-change-model.ts b/packages/services/storage/src/schema-change-model.ts index 10106fe083..400f1e2fda 100644 --- a/packages/services/storage/src/schema-change-model.ts +++ b/packages/services/storage/src/schema-change-model.ts @@ -16,6 +16,32 @@ import { DirectiveLocationAddedChange, DirectiveLocationRemovedChange, DirectiveRemovedChange, + DirectiveUsageArgumentAddedChange, + DirectiveUsageArgumentDefinitionAddedChange, + DirectiveUsageArgumentDefinitionRemovedChange, + DirectiveUsageArgumentRemovedChange, + DirectiveUsageEnumAddedChange, + DirectiveUsageEnumRemovedChange, + DirectiveUsageEnumValueAddedChange, + DirectiveUsageEnumValueRemovedChange, + DirectiveUsageFieldAddedChange, + DirectiveUsageFieldDefinitionAddedChange, + DirectiveUsageFieldDefinitionRemovedChange, + DirectiveUsageFieldRemovedChange, + DirectiveUsageInputFieldDefinitionAddedChange, + DirectiveUsageInputFieldDefinitionRemovedChange, + DirectiveUsageInputObjectAddedChange, + DirectiveUsageInputObjectRemovedChange, + DirectiveUsageInterfaceAddedChange, + DirectiveUsageInterfaceRemovedChange, + DirectiveUsageObjectAddedChange, + DirectiveUsageObjectRemovedChange, + DirectiveUsageScalarAddedChange, + DirectiveUsageScalarRemovedChange, + DirectiveUsageSchemaAddedChange, + DirectiveUsageSchemaRemovedChange, + DirectiveUsageUnionMemberAddedChange, + DirectiveUsageUnionMemberRemovedChange, EnumValueAddedChange, EnumValueDeprecationReasonAddedChange, EnumValueDeprecationReasonChangedChange, @@ -56,6 +82,7 @@ import { TypeDescriptionChangedChange, TypeDescriptionRemovedChange, TypeKindChangedChange, + TypeOfChangeType, TypeRemovedChange, UnionMemberAddedChange, UnionMemberRemovedChange, @@ -66,109 +93,161 @@ import { } from './schema-change-meta'; // prettier-ignore -const FieldArgumentDescriptionChangedLiteral = z.literal("FIELD_ARGUMENT_DESCRIPTION_CHANGED" satisfies `${ChangeType.FieldArgumentDescriptionChanged}`) +const FieldArgumentDescriptionChangedLiteral = z.literal("FIELD_ARGUMENT_DESCRIPTION_CHANGED" satisfies `${typeof ChangeType.FieldArgumentDescriptionChanged}`) // prettier-ignore -const FieldArgumentDefaultChangedLiteral = z.literal("FIELD_ARGUMENT_DEFAULT_CHANGED" satisfies `${ChangeType.FieldArgumentDefaultChanged}`) +const FieldArgumentDefaultChangedLiteral = z.literal("FIELD_ARGUMENT_DEFAULT_CHANGED" satisfies `${typeof ChangeType.FieldArgumentDefaultChanged}`) // prettier-ignore -const FieldArgumentTypeChangedLiteral = z.literal("FIELD_ARGUMENT_TYPE_CHANGED" satisfies `${ChangeType.FieldArgumentTypeChanged}`) +const FieldArgumentTypeChangedLiteral = z.literal("FIELD_ARGUMENT_TYPE_CHANGED" satisfies `${typeof ChangeType.FieldArgumentTypeChanged}`) // prettier-ignore -const DirectiveRemovedLiteral = z.literal("DIRECTIVE_REMOVED" satisfies `${ChangeType.DirectiveRemoved}`) +const DirectiveRemovedLiteral = z.literal("DIRECTIVE_REMOVED" satisfies `${typeof ChangeType.DirectiveRemoved}`) // prettier-ignore -const DirectiveAddedLiteral = z.literal("DIRECTIVE_ADDED" satisfies `${ChangeType.DirectiveAdded}`) +const DirectiveAddedLiteral = z.literal("DIRECTIVE_ADDED" satisfies `${typeof ChangeType.DirectiveAdded}`) // prettier-ignore -const DirectiveDescriptionChangedLiteral = z.literal("DIRECTIVE_DESCRIPTION_CHANGED" satisfies `${ChangeType.DirectiveDescriptionChanged}`) +const DirectiveDescriptionChangedLiteral = z.literal("DIRECTIVE_DESCRIPTION_CHANGED" satisfies `${typeof ChangeType.DirectiveDescriptionChanged}`) // prettier-ignore -const DirectiveLocationAddedLiteral = z.literal("DIRECTIVE_LOCATION_ADDED" satisfies `${ChangeType.DirectiveLocationAdded}`) +const DirectiveLocationAddedLiteral = z.literal("DIRECTIVE_LOCATION_ADDED" satisfies `${typeof ChangeType.DirectiveLocationAdded}`) // prettier-ignore -const DirectiveLocationRemovedLiteral = z.literal("DIRECTIVE_LOCATION_REMOVED" satisfies `${ChangeType.DirectiveLocationRemoved}`) +const DirectiveLocationRemovedLiteral = z.literal("DIRECTIVE_LOCATION_REMOVED" satisfies `${typeof ChangeType.DirectiveLocationRemoved}`) // prettier-ignore -const DirectiveArgumentAddedLiteral = z.literal("DIRECTIVE_ARGUMENT_ADDED" satisfies `${ChangeType.DirectiveArgumentAdded}`) +const DirectiveArgumentAddedLiteral = z.literal("DIRECTIVE_ARGUMENT_ADDED" satisfies `${typeof ChangeType.DirectiveArgumentAdded}`) // prettier-ignore -const DirectiveArgumentRemovedLiteral = z.literal("DIRECTIVE_ARGUMENT_REMOVED" satisfies `${ChangeType.DirectiveArgumentRemoved}`) +const DirectiveArgumentRemovedLiteral = z.literal("DIRECTIVE_ARGUMENT_REMOVED" satisfies `${typeof ChangeType.DirectiveArgumentRemoved}`) // prettier-ignore -const DirectiveArgumentDescriptionChangedLiteral = z.literal("DIRECTIVE_ARGUMENT_DESCRIPTION_CHANGED" satisfies `${ChangeType.DirectiveArgumentDescriptionChanged}`) +const DirectiveArgumentDescriptionChangedLiteral = z.literal("DIRECTIVE_ARGUMENT_DESCRIPTION_CHANGED" satisfies `${typeof ChangeType.DirectiveArgumentDescriptionChanged}`) // prettier-ignore -const DirectiveArgumentDefaultValueChangedLiteral = z.literal("DIRECTIVE_ARGUMENT_DEFAULT_VALUE_CHANGED" satisfies `${ChangeType.DirectiveArgumentDefaultValueChanged}`) +const DirectiveArgumentDefaultValueChangedLiteral = z.literal("DIRECTIVE_ARGUMENT_DEFAULT_VALUE_CHANGED" satisfies `${typeof ChangeType.DirectiveArgumentDefaultValueChanged}`) // prettier-ignore -const DirectiveArgumentTypeChangedLiteral = z.literal("DIRECTIVE_ARGUMENT_TYPE_CHANGED" satisfies `${ChangeType.DirectiveArgumentTypeChanged}`) +const DirectiveArgumentTypeChangedLiteral = z.literal("DIRECTIVE_ARGUMENT_TYPE_CHANGED" satisfies `${typeof ChangeType.DirectiveArgumentTypeChanged}`) // prettier-ignore -const EnumValueRemovedLiteral = z.literal("ENUM_VALUE_REMOVED" satisfies `${ChangeType.EnumValueRemoved}`) +const DirectiveUsageUnionMemberAddedLiteral = z.literal('DIRECTIVE_USAGE_UNION_MEMBER_ADDED' satisfies `${typeof ChangeType.DirectiveUsageUnionMemberAdded}`) // prettier-ignore -const EnumValueAddedLiteral = z.literal("ENUM_VALUE_ADDED" satisfies `${ChangeType.EnumValueAdded}`) +const DirectiveUsageUnionMemberRemovedLiteral = z.literal('DIRECTIVE_USAGE_UNION_MEMBER_REMOVED' satisfies `${typeof ChangeType.DirectiveUsageUnionMemberRemoved}`) // prettier-ignore -const EnumValueDescriptionChangedLiteral = z.literal("ENUM_VALUE_DESCRIPTION_CHANGED" satisfies `${ChangeType.EnumValueDescriptionChanged}`) +const DirectiveUsageEnumAddedLiteral = z.literal('DIRECTIVE_USAGE_ENUM_ADDED' satisfies `${typeof ChangeType.DirectiveUsageEnumAdded}`) // prettier-ignore -const EnumValueDeprecationReasonChangedLiteral = z.literal("ENUM_VALUE_DEPRECATION_REASON_CHANGED" satisfies `${ChangeType.EnumValueDeprecationReasonChanged}`) +const DirectiveUsageEnumRemovedLiteral = z.literal('DIRECTIVE_USAGE_ENUM_REMOVED' satisfies `${typeof ChangeType.DirectiveUsageEnumRemoved}`) // prettier-ignore -const EnumValueDeprecationReasonAddedLiteral = z.literal("ENUM_VALUE_DEPRECATION_REASON_ADDED" satisfies `${ChangeType.EnumValueDeprecationReasonAdded}`) +const DirectiveUsageEnumValueAddedLiteral = z.literal('DIRECTIVE_USAGE_ENUM_VALUE_ADDED' satisfies `${typeof ChangeType.DirectiveUsageEnumValueAdded}`) // prettier-ignore -const EnumValueDeprecationReasonRemovedLiteral = z.literal("ENUM_VALUE_DEPRECATION_REASON_REMOVED" satisfies `${ChangeType.EnumValueDeprecationReasonRemoved}`) +const DirectiveUsageEnumValueRemovedLiteral = z.literal('DIRECTIVE_USAGE_ENUM_VALUE_REMOVED' satisfies `${typeof ChangeType.DirectiveUsageEnumValueRemoved}`) // prettier-ignore -const FieldRemovedLiteral = z.literal("FIELD_REMOVED" satisfies `${ChangeType.FieldRemoved}`) +const DirectiveUsageInputObjectAddedLiteral = z.literal('DIRECTIVE_USAGE_INPUT_OBJECT_ADDED' satisfies `${typeof ChangeType.DirectiveUsageInputObjectAdded}`) // prettier-ignore -const FieldAddedLiteral = z.literal("FIELD_ADDED" satisfies `${ChangeType.FieldAdded}`) +const DirectiveUsageInputObjectRemovedLiteral = z.literal('DIRECTIVE_USAGE_INPUT_OBJECT_REMOVED' satisfies `${typeof ChangeType.DirectiveUsageInputObjectRemoved}`) // prettier-ignore -const FieldDescriptionChangedLiteral = z.literal("FIELD_DESCRIPTION_CHANGED" satisfies `${ChangeType.FieldDescriptionChanged}`) +const DirectiveUsageFieldAddedLiteral = z.literal('DIRECTIVE_USAGE_FIELD_ADDED' satisfies `${typeof ChangeType.DirectiveUsageFieldAdded}`) // prettier-ignore -const FieldDescriptionAddedLiteral = z.literal("FIELD_DESCRIPTION_ADDED" satisfies `${ChangeType.FieldDescriptionAdded}`) +const DirectiveUsageFieldRemovedLiteral = z.literal('DIRECTIVE_USAGE_FIELD_REMOVED' satisfies `${typeof ChangeType.DirectiveUsageFieldRemoved}`) // prettier-ignore -const FieldDescriptionRemovedLiteral = z.literal("FIELD_DESCRIPTION_REMOVED" satisfies `${ChangeType.FieldDescriptionRemoved}`) +const DirectiveUsageScalarAddedLiteral = z.literal('DIRECTIVE_USAGE_SCALAR_ADDED' satisfies `${typeof ChangeType.DirectiveUsageScalarAdded}`) // prettier-ignore -const FieldDeprecationAddedLiteral = z.literal("FIELD_DEPRECATION_ADDED" satisfies `${ChangeType.FieldDeprecationAdded}`) +const DirectiveUsageScalarRemovedLiteral = z.literal('DIRECTIVE_USAGE_SCALAR_REMOVED' satisfies `${typeof ChangeType.DirectiveUsageScalarRemoved}`) // prettier-ignore -const FieldDeprecationRemovedLiteral = z.literal("FIELD_DEPRECATION_REMOVED" satisfies `${ChangeType.FieldDeprecationRemoved}`) +const DirectiveUsageObjectAddedLiteral = z.literal('DIRECTIVE_USAGE_OBJECT_ADDED' satisfies `${typeof ChangeType.DirectiveUsageObjectAdded}`) // prettier-ignore -const FieldDeprecationReasonChangedLiteral = z.literal("FIELD_DEPRECATION_REASON_CHANGED" satisfies `${ChangeType.FieldDeprecationReasonChanged}`) +const DirectiveUsageObjectRemovedLiteral = z.literal('DIRECTIVE_USAGE_OBJECT_REMOVED' satisfies `${typeof ChangeType.DirectiveUsageObjectRemoved}`) // prettier-ignore -const FieldDeprecationReasonAddedLiteral = z.literal("FIELD_DEPRECATION_REASON_ADDED" satisfies `${ChangeType.FieldDeprecationReasonAdded}`) +const DirectiveUsageInterfaceAddedLiteral = z.literal('DIRECTIVE_USAGE_INTERFACE_ADDED' satisfies `${typeof ChangeType.DirectiveUsageInterfaceAdded}`) // prettier-ignore -const FieldDeprecationReasonRemovedLiteral = z.literal("FIELD_DEPRECATION_REASON_REMOVED" satisfies `${ChangeType.FieldDeprecationReasonRemoved}`) +const DirectiveUsageInterfaceRemovedLiteral = z.literal('DIRECTIVE_USAGE_INTERFACE_REMOVED' satisfies `${typeof ChangeType.DirectiveUsageInterfaceRemoved}`) // prettier-ignore -const FieldTypeChangedLiteral = z.literal("FIELD_TYPE_CHANGED" satisfies `${ChangeType.FieldTypeChanged}`) +const DirectiveUsageArgumentDefinitionAddedLiteral = z.literal('DIRECTIVE_USAGE_ARGUMENT_DEFINITION_ADDED' satisfies `${typeof ChangeType.DirectiveUsageArgumentDefinitionAdded}`) // prettier-ignore -const FieldArgumentAddedLiteral = z.literal("FIELD_ARGUMENT_ADDED" satisfies `${ChangeType.FieldArgumentAdded}`) +const DirectiveUsageArgumentDefinitionRemovedLiteral = z.literal('DIRECTIVE_USAGE_ARGUMENT_DEFINITION_REMOVED' satisfies `${typeof ChangeType.DirectiveUsageArgumentDefinitionRemoved}`) // prettier-ignore -const FieldArgumentRemovedLiteral = z.literal("FIELD_ARGUMENT_REMOVED" satisfies `${ChangeType.FieldArgumentRemoved}`) +const DirectiveUsageSchemaAddedLiteral = z.literal('DIRECTIVE_USAGE_SCHEMA_ADDED' satisfies `${typeof ChangeType.DirectiveUsageSchemaAdded}`) // prettier-ignore -const InputFieldRemovedLiteral = z.literal("INPUT_FIELD_REMOVED" satisfies `${ChangeType.InputFieldRemoved}`) +const DirectiveUsageSchemaRemovedLiteral = z.literal('DIRECTIVE_USAGE_SCHEMA_REMOVED' satisfies `${typeof ChangeType.DirectiveUsageSchemaRemoved}`) // prettier-ignore -const InputFieldAddedLiteral = z.literal("INPUT_FIELD_ADDED" satisfies `${ChangeType.InputFieldAdded}`) +const DirectiveUsageFieldDefinitionAddedLiteral = z.literal('DIRECTIVE_USAGE_FIELD_DEFINITION_ADDED' satisfies `${typeof ChangeType.DirectiveUsageFieldDefinitionAdded}`) // prettier-ignore -const InputFieldDescriptionAddedLiteral = z.literal("INPUT_FIELD_DESCRIPTION_ADDED" satisfies `${ChangeType.InputFieldDescriptionAdded}`) +const DirectiveUsageFieldDefinitionRemovedLiteral = z.literal('DIRECTIVE_USAGE_FIELD_DEFINITION_REMOVED' satisfies `${typeof ChangeType.DirectiveUsageFieldDefinitionRemoved}`) // prettier-ignore -const InputFieldDescriptionRemovedLiteral = z.literal("INPUT_FIELD_DESCRIPTION_REMOVED" satisfies `${ChangeType.InputFieldDescriptionRemoved}`) +const DirectiveUsageInputFieldDefinitionAddedLiteral = z.literal('DIRECTIVE_USAGE_INPUT_FIELD_DEFINITION_ADDED' satisfies `${typeof ChangeType.DirectiveUsageInputFieldDefinitionAdded}`) // prettier-ignore -const InputFieldDescriptionChangedLiteral = z.literal("INPUT_FIELD_DESCRIPTION_CHANGED" satisfies `${ChangeType.InputFieldDescriptionChanged}`) +const DirectiveUsageInputFieldDefinitionRemovedLiteral = z.literal('DIRECTIVE_USAGE_INPUT_FIELD_DEFINITION_REMOVED' satisfies `${typeof ChangeType.DirectiveUsageInputFieldDefinitionRemoved}`) // prettier-ignore -const InputFieldDefaultValueChangedLiteral = z.literal("INPUT_FIELD_DEFAULT_VALUE_CHANGED" satisfies `${ChangeType.InputFieldDefaultValueChanged}`) +const DirectiveUsageArgumentAddedLiteral = z.literal('DIRECTIVE_USAGE_ARGUMENT_ADDED' satisfies `${typeof ChangeType.DirectiveUsageArgumentAdded}`) // prettier-ignore -const InputFieldTypeChangedLiteral = z.literal("INPUT_FIELD_TYPE_CHANGED" satisfies `${ChangeType.InputFieldTypeChanged}`) +const DirectiveUsageArgumentRemovedLiteral = z.literal('DIRECTIVE_USAGE_ARGUMENT_REMOVED' satisfies `${typeof ChangeType.DirectiveUsageArgumentRemoved}`) // prettier-ignore -const ObjectTypeInterfaceAddedLiteral = z.literal("OBJECT_TYPE_INTERFACE_ADDED" satisfies `${ChangeType.ObjectTypeInterfaceAdded}`) +const EnumValueRemovedLiteral = z.literal("ENUM_VALUE_REMOVED" satisfies `${typeof ChangeType.EnumValueRemoved}`) // prettier-ignore -const ObjectTypeInterfaceRemovedLiteral = z.literal("OBJECT_TYPE_INTERFACE_REMOVED" satisfies `${ChangeType.ObjectTypeInterfaceRemoved}`) +const EnumValueAddedLiteral = z.literal("ENUM_VALUE_ADDED" satisfies `${typeof ChangeType.EnumValueAdded}`) // prettier-ignore -const SchemaQueryTypeChangedLiteral = z.literal("SCHEMA_QUERY_TYPE_CHANGED" satisfies `${ChangeType.SchemaQueryTypeChanged}`) +const EnumValueDescriptionChangedLiteral = z.literal("ENUM_VALUE_DESCRIPTION_CHANGED" satisfies `${typeof ChangeType.EnumValueDescriptionChanged}`) // prettier-ignore -const SchemaMutationTypeChangedLiteral = z.literal("SCHEMA_MUTATION_TYPE_CHANGED" satisfies `${ChangeType.SchemaMutationTypeChanged}`) +const EnumValueDeprecationReasonChangedLiteral = z.literal("ENUM_VALUE_DEPRECATION_REASON_CHANGED" satisfies `${typeof ChangeType.EnumValueDeprecationReasonChanged}`) // prettier-ignore -const SchemaSubscriptionTypeChangedLiteral = z.literal("SCHEMA_SUBSCRIPTION_TYPE_CHANGED" satisfies `${ChangeType.SchemaSubscriptionTypeChanged}`) +const EnumValueDeprecationReasonAddedLiteral = z.literal("ENUM_VALUE_DEPRECATION_REASON_ADDED" satisfies `${typeof ChangeType.EnumValueDeprecationReasonAdded}`) // prettier-ignore -const TypeRemovedLiteral = z.literal("TYPE_REMOVED" satisfies `${ChangeType.TypeRemoved}`) +const EnumValueDeprecationReasonRemovedLiteral = z.literal("ENUM_VALUE_DEPRECATION_REASON_REMOVED" satisfies `${typeof ChangeType.EnumValueDeprecationReasonRemoved}`) // prettier-ignore -const TypeAddedLiteral = z.literal("TYPE_ADDED" satisfies `${ChangeType.TypeAdded}`) +const FieldRemovedLiteral = z.literal("FIELD_REMOVED" satisfies `${typeof ChangeType.FieldRemoved}`) // prettier-ignore -const TypeKindChangedLiteral = z.literal("TYPE_KIND_CHANGED" satisfies `${ChangeType.TypeKindChanged}`) +const FieldAddedLiteral = z.literal("FIELD_ADDED" satisfies `${typeof ChangeType.FieldAdded}`) // prettier-ignore -const TypeDescriptionChangedLiteral = z.literal("TYPE_DESCRIPTION_CHANGED" satisfies `${ChangeType.TypeDescriptionChanged}`) +const FieldDescriptionChangedLiteral = z.literal("FIELD_DESCRIPTION_CHANGED" satisfies `${typeof ChangeType.FieldDescriptionChanged}`) // prettier-ignore -const TypeDescriptionRemovedLiteral = z.literal("TYPE_DESCRIPTION_REMOVED" satisfies `${ChangeType.TypeDescriptionRemoved}`) +const FieldDescriptionAddedLiteral = z.literal("FIELD_DESCRIPTION_ADDED" satisfies `${typeof ChangeType.FieldDescriptionAdded}`) // prettier-ignore -const TypeDescriptionAddedLiteral = z.literal("TYPE_DESCRIPTION_ADDED" satisfies `${ChangeType.TypeDescriptionAdded}`) +const FieldDescriptionRemovedLiteral = z.literal("FIELD_DESCRIPTION_REMOVED" satisfies `${typeof ChangeType.FieldDescriptionRemoved}`) // prettier-ignore -const UnionMemberRemovedLiteral = z.literal("UNION_MEMBER_REMOVED" satisfies `${ChangeType.UnionMemberRemoved}`) +const FieldDeprecationAddedLiteral = z.literal("FIELD_DEPRECATION_ADDED" satisfies `${typeof ChangeType.FieldDeprecationAdded}`) // prettier-ignore -const UnionMemberAddedLiteral = z.literal("UNION_MEMBER_ADDED" satisfies `${ChangeType.UnionMemberAdded}`) +const FieldDeprecationRemovedLiteral = z.literal("FIELD_DEPRECATION_REMOVED" satisfies `${typeof ChangeType.FieldDeprecationRemoved}`) +// prettier-ignore +const FieldDeprecationReasonChangedLiteral = z.literal("FIELD_DEPRECATION_REASON_CHANGED" satisfies `${typeof ChangeType.FieldDeprecationReasonChanged}`) +// prettier-ignore +const FieldDeprecationReasonAddedLiteral = z.literal("FIELD_DEPRECATION_REASON_ADDED" satisfies `${typeof ChangeType.FieldDeprecationReasonAdded}`) +// prettier-ignore +const FieldDeprecationReasonRemovedLiteral = z.literal("FIELD_DEPRECATION_REASON_REMOVED" satisfies `${typeof ChangeType.FieldDeprecationReasonRemoved}`) +// prettier-ignore +const FieldTypeChangedLiteral = z.literal("FIELD_TYPE_CHANGED" satisfies `${typeof ChangeType.FieldTypeChanged}`) +// prettier-ignore +const FieldArgumentAddedLiteral = z.literal("FIELD_ARGUMENT_ADDED" satisfies `${typeof ChangeType.FieldArgumentAdded}`) +// prettier-ignore +const FieldArgumentRemovedLiteral = z.literal("FIELD_ARGUMENT_REMOVED" satisfies `${typeof ChangeType.FieldArgumentRemoved}`) +// prettier-ignore +const InputFieldRemovedLiteral = z.literal("INPUT_FIELD_REMOVED" satisfies `${typeof ChangeType.InputFieldRemoved}`) +// prettier-ignore +const InputFieldAddedLiteral = z.literal("INPUT_FIELD_ADDED" satisfies `${typeof ChangeType.InputFieldAdded}`) +// prettier-ignore +const InputFieldDescriptionAddedLiteral = z.literal("INPUT_FIELD_DESCRIPTION_ADDED" satisfies `${typeof ChangeType.InputFieldDescriptionAdded}`) +// prettier-ignore +const InputFieldDescriptionRemovedLiteral = z.literal("INPUT_FIELD_DESCRIPTION_REMOVED" satisfies `${typeof ChangeType.InputFieldDescriptionRemoved}`) +// prettier-ignore +const InputFieldDescriptionChangedLiteral = z.literal("INPUT_FIELD_DESCRIPTION_CHANGED" satisfies `${typeof ChangeType.InputFieldDescriptionChanged}`) +// prettier-ignore +const InputFieldDefaultValueChangedLiteral = z.literal("INPUT_FIELD_DEFAULT_VALUE_CHANGED" satisfies `${typeof ChangeType.InputFieldDefaultValueChanged}`) +// prettier-ignore +const InputFieldTypeChangedLiteral = z.literal("INPUT_FIELD_TYPE_CHANGED" satisfies `${typeof ChangeType.InputFieldTypeChanged}`) +// prettier-ignore +const ObjectTypeInterfaceAddedLiteral = z.literal("OBJECT_TYPE_INTERFACE_ADDED" satisfies `${typeof ChangeType.ObjectTypeInterfaceAdded}`) +// prettier-ignore +const ObjectTypeInterfaceRemovedLiteral = z.literal("OBJECT_TYPE_INTERFACE_REMOVED" satisfies `${typeof ChangeType.ObjectTypeInterfaceRemoved}`) +// prettier-ignore +const SchemaQueryTypeChangedLiteral = z.literal("SCHEMA_QUERY_TYPE_CHANGED" satisfies `${typeof ChangeType.SchemaQueryTypeChanged}`) +// prettier-ignore +const SchemaMutationTypeChangedLiteral = z.literal("SCHEMA_MUTATION_TYPE_CHANGED" satisfies `${typeof ChangeType.SchemaMutationTypeChanged}`) +// prettier-ignore +const SchemaSubscriptionTypeChangedLiteral = z.literal("SCHEMA_SUBSCRIPTION_TYPE_CHANGED" satisfies `${typeof ChangeType.SchemaSubscriptionTypeChanged}`) +// prettier-ignore +const TypeRemovedLiteral = z.literal("TYPE_REMOVED" satisfies `${typeof ChangeType.TypeRemoved}`) +// prettier-ignore +const TypeAddedLiteral = z.literal("TYPE_ADDED" satisfies `${typeof ChangeType.TypeAdded}`) +// prettier-ignore +const TypeKindChangedLiteral = z.literal("TYPE_KIND_CHANGED" satisfies `${typeof ChangeType.TypeKindChanged}`) +// prettier-ignore +const TypeDescriptionChangedLiteral = z.literal("TYPE_DESCRIPTION_CHANGED" satisfies `${typeof ChangeType.TypeDescriptionChanged}`) +// prettier-ignore +const TypeDescriptionRemovedLiteral = z.literal("TYPE_DESCRIPTION_REMOVED" satisfies `${typeof ChangeType.TypeDescriptionRemoved}`) +// prettier-ignore +const TypeDescriptionAddedLiteral = z.literal("TYPE_DESCRIPTION_ADDED" satisfies `${typeof ChangeType.TypeDescriptionAdded}`) +// prettier-ignore +const UnionMemberRemovedLiteral = z.literal("UNION_MEMBER_REMOVED" satisfies `${typeof ChangeType.UnionMemberRemoved}`) +// prettier-ignore +const UnionMemberAddedLiteral = z.literal("UNION_MEMBER_ADDED" satisfies `${typeof ChangeType.UnionMemberAdded}`) /** * @source https://github.com/colinhacks/zod/issues/372#issuecomment-1280054492 @@ -180,7 +259,7 @@ type Implements = { : z.ZodOptionalType> : null extends Model[key] ? z.ZodNullableType> - : Model[key] extends ChangeType + : Model[key] extends TypeOfChangeType ? z.ZodLiteral<`${Model[key]}`> : z.ZodType; }; @@ -245,7 +324,11 @@ export const DirectiveAddedModel = implement().with({ type: DirectiveAddedLiteral, meta: z.object({ addedDirectiveName: z.string(), - }), + // for backwards compatibility + addedDirectiveRepeatable: z.boolean().default(false), // boolean; + addedDirectiveLocations: z.array(z.string()).default([]), // string[]; + addedDirectiveDescription: z.string().nullable().default(null), // string | null; + }) as any, // @todo fix typing }); export const DirectiveDescriptionChangedModel = implement().with( @@ -281,7 +364,11 @@ export const DirectiveArgumentAddedModel = implement().with({ @@ -326,6 +413,255 @@ export const DirectiveArgumentTypeChangedModel = }), }); +export const DirectiveUsageUnionMemberAddedModel = + implement().with({ + type: DirectiveUsageUnionMemberAddedLiteral, + meta: z.object({ + addedUnionMember: z.string(), + unionName: z.string(), + addedUnionMemberTypeName: z.string(), + addedDirectiveName: z.string(), + addedToNewType: z.boolean(), + }), + }); +export const DirectiveUsageUnionMemberRemovedModel = + implement().with({ + type: DirectiveUsageUnionMemberRemovedLiteral, + meta: z.object({ + unionName: z.string(), + removedUnionMemberTypeName: z.string(), + removedDirectiveName: z.string(), + }), + }); +export const DirectiveUsageEnumAddedModel = implement().with({ + type: DirectiveUsageEnumAddedLiteral, + meta: z.object({ + enumName: z.string(), + addedDirectiveName: z.string(), + addedToNewType: z.boolean(), + }), +}); +export const DirectiveUsageEnumRemovedModel = implement().with({ + type: DirectiveUsageEnumRemovedLiteral, + meta: z.object({ + enumName: z.string(), + removedDirectiveName: z.string(), + }), +}); +export const DirectiveUsageEnumValueAddedModel = + implement().with({ + type: DirectiveUsageEnumValueAddedLiteral, + meta: z.object({ + enumName: z.string(), + enumValueName: z.string(), + addedDirectiveName: z.string(), + addedToNewType: z.boolean(), + }), + }); +export const DirectiveUsageEnumValueRemovedModel = + implement().with({ + type: DirectiveUsageEnumValueRemovedLiteral, + meta: z.object({ + enumName: z.string(), + enumValueName: z.string(), + removedDirectiveName: z.string(), + }), + }); +export const DirectiveUsageInputObjectAddedModel = + implement().with({ + type: DirectiveUsageInputObjectAddedLiteral, + meta: z.object({ + inputObjectName: z.string(), + addedInputFieldName: z.string(), + isAddedInputFieldTypeNullable: z.boolean(), + addedInputFieldType: z.string(), + addedDirectiveName: z.string(), + addedToNewType: z.boolean(), + }), + }); +export const DirectiveUsageInputObjectRemovedModel = + implement().with({ + type: DirectiveUsageInputObjectRemovedLiteral, + meta: z.object({ + inputObjectName: z.string(), + removedInputFieldName: z.string(), + isRemovedInputFieldTypeNullable: z.boolean(), + removedInputFieldType: z.string(), + removedDirectiveName: z.string(), + }), + }); +export const DirectiveUsageFieldAddedModel = implement().with({ + type: DirectiveUsageFieldAddedLiteral, + meta: z.object({ + typeName: z.string(), + fieldName: z.string(), + addedDirectiveName: z.string(), + }), +}); +export const DirectiveUsageFieldRemovedModel = implement().with({ + type: DirectiveUsageFieldRemovedLiteral, + meta: z.object({ + typeName: z.string(), + fieldName: z.string(), + removedDirectiveName: z.string(), + }), +}); +export const DirectiveUsageScalarAddedModel = implement().with({ + type: DirectiveUsageScalarAddedLiteral, + meta: z.object({ + scalarName: z.string(), + addedDirectiveName: z.string(), + addedToNewType: z.boolean(), + }), +}); +export const DirectiveUsageScalarRemovedModel = implement().with( + { + type: DirectiveUsageScalarRemovedLiteral, + meta: z.object({ + scalarName: z.string(), + removedDirectiveName: z.string(), + }), + }, +); +export const DirectiveUsageObjectAddedModel = implement().with({ + type: DirectiveUsageObjectAddedLiteral, + meta: z.object({ + objectName: z.string(), + addedDirectiveName: z.string(), + addedToNewType: z.boolean(), + }), +}); +export const DirectiveUsageObjectRemovedModel = implement().with( + { + type: DirectiveUsageObjectRemovedLiteral, + meta: z.object({ + objectName: z.string(), + removedDirectiveName: z.string(), + }), + }, +); +export const DirectiveUsageInterfaceAddedModel = + implement().with({ + type: DirectiveUsageInterfaceAddedLiteral, + meta: z.object({ + interfaceName: z.string(), + addedDirectiveName: z.string(), + addedToNewType: z.boolean(), + }), + }); +export const DirectiveUsageInterfaceRemovedModel = + implement().with({ + type: DirectiveUsageInterfaceRemovedLiteral, + meta: z.object({ + interfaceName: z.string(), + removedDirectiveName: z.string(), + }), + }); +export const DirectiveUsageArgumentDefinitionAddedModel = + implement().with({ + type: DirectiveUsageArgumentDefinitionAddedLiteral, + meta: z.object({ + typeName: z.string(), + fieldName: z.string(), + argumentName: z.string(), + addedDirectiveName: z.string(), + addedToNewType: z.boolean(), + }), + }); +export const DirectiveUsageArgumentDefinitionRemovedModel = + implement().with({ + type: DirectiveUsageArgumentDefinitionRemovedLiteral, + meta: z.object({ + typeName: z.string(), + fieldName: z.string(), + argumentName: z.string(), + removedDirectiveName: z.string(), + }), + }); +export const DirectiveUsageSchemaAddedModel = implement().with({ + type: DirectiveUsageSchemaAddedLiteral, + meta: z.object({ + addedDirectiveName: z.string(), + schemaTypeName: z.string(), + addedToNewType: z.boolean(), + }), +}); +export const DirectiveUsageSchemaRemovedModel = implement().with( + { + type: DirectiveUsageSchemaRemovedLiteral, + meta: z.object({ + removedDirectiveName: z.string(), + schemaTypeName: z.string(), + }), + }, +); +export const DirectiveUsageFieldDefinitionAddedModel = + implement().with({ + type: DirectiveUsageFieldDefinitionAddedLiteral, + meta: z.object({ + typeName: z.string(), + fieldName: z.string(), + addedDirectiveName: z.string(), + addedToNewType: z.boolean(), + }), + }); +export const DirectiveUsageFieldDefinitionRemovedModel = + implement().with({ + type: DirectiveUsageFieldDefinitionRemovedLiteral, + meta: z.object({ + typeName: z.string(), + fieldName: z.string(), + removedDirectiveName: z.string(), + }), + }); +export const DirectiveUsageInputFieldDefinitionAddedModel = + implement().with({ + type: DirectiveUsageInputFieldDefinitionAddedLiteral, + meta: z.object({ + inputObjectName: z.string(), + inputFieldName: z.string(), + inputFieldType: z.string(), + addedDirectiveName: z.string(), + addedToNewType: z.boolean(), + }), + }); +export const DirectiveUsageInputFieldDefinitionRemovedModel = + implement().with({ + type: DirectiveUsageInputFieldDefinitionRemovedLiteral, + meta: z.object({ + inputObjectName: z.string(), + inputFieldName: z.string(), + removedDirectiveName: z.string(), + }), + }); +export const DirectiveUsageArgumentAddedModel = implement().with( + { + type: DirectiveUsageArgumentAddedLiteral, + meta: z.object({ + directiveName: z.string(), + addedArgumentName: z.string(), + addedArgumentValue: z.string(), + oldArgumentValue: z.string().nullable(), + parentTypeName: z.string().nullable(), + parentFieldName: z.string().nullable(), + parentArgumentName: z.string().nullable(), + parentEnumValueName: z.string().nullable(), + }), + }, +); +export const DirectiveUsageArgumentRemovedModel = + implement().with({ + type: DirectiveUsageArgumentRemovedLiteral, + meta: z.object({ + directiveName: z.string(), + removedArgumentName: z.string(), + parentTypeName: z.string().nullable(), + parentFieldName: z.string().nullable(), + parentArgumentName: z.string().nullable(), + parentEnumValueName: z.string().nullable(), + }), + }); + // Enum export const EnumValueRemovedModel = implement().with({ @@ -337,12 +673,14 @@ export const EnumValueRemovedModel = implement().with({ }), }); -export const EnumValueAdded = implement().with({ +export const EnumValueAddedModel = implement().with({ type: EnumValueAddedLiteral, meta: z.object({ enumName: z.string(), addedEnumValueName: z.string(), - }), + addedToNewType: z.boolean().default(false), // default for backwards compatibility + addedDirectiveDescription: z.string().nullable().optional(), + }) as any, // @todo fix typing complaint }); export const EnumValueDescriptionChangedModel = implement().with( @@ -405,8 +743,9 @@ export const FieldAddedModel = implement().with({ meta: z.object({ typeName: z.string(), addedFieldName: z.string(), + addedFieldReturnType: z.string().optional(), // optional for backwards compatibility typeType: z.string(), - }), + }) as any, // @todo fix typing }); export const FieldDescriptionChangedModel = implement().with({ @@ -441,7 +780,8 @@ export const FieldDeprecationAddedModel = implement meta: z.object({ typeName: z.string(), fieldName: z.string(), - }), + deprecationReason: z.string().optional(), // for backwards compatibility + }) as any, // @todo fix typing }); export const FieldDeprecationRemovedModel = implement().with({ @@ -503,7 +843,8 @@ export const FieldArgumentAddedModel = implement().wit addedArgumentType: z.string(), hasDefaultValue: z.boolean(), isAddedFieldArgumentBreaking: z.boolean(), - }), + addedToNewField: z.boolean().optional(), // for backwards compatibility + }) as any, // @todo fix typing }); export const FieldArgumentRemovedModel = implement().with({ @@ -534,7 +875,9 @@ export const InputFieldAddedModel = implement().with({ addedInputFieldName: z.string(), isAddedInputFieldTypeNullable: z.boolean(), addedInputFieldType: z.string(), - }), + addedToNewType: z.boolean().default(false), // default to make backwards compatible + addedFieldDefault: z.string().optional(), + }) as any, // @todo fix typing }); export const InputFieldDescriptionAddedModel = implement().with({ @@ -596,7 +939,8 @@ export const ObjectTypeInterfaceAddedModel = implement().with({ @@ -604,6 +948,7 @@ export const ObjectTypeInterfaceRemovedModel = implement().with({ type: TypeAddedLiteral, meta: z.object({ addedTypeName: z.string(), - }), + addedTypeKind: z.string().optional(), // optional for backwards compatibility + }) as any, // @todo fix typing }); export const TypeKindChangedModel = implement().with({ @@ -697,7 +1043,8 @@ export const UnionMemberAddedModel = implement().with({ meta: z.object({ unionName: z.string(), addedUnionMemberTypeName: z.string(), - }), + addedToNewType: z.boolean().default(false), // default for backwards compatibility + }) as any, // @todo fix typing }); // Service Registry Url Change @@ -727,62 +1074,89 @@ export const RegistryServiceUrlChangeModel = // TODO: figure out a way to make sure that all the changes are included in the union // Similar to implement().with() but for unions export const SchemaChangeModel = z.union([ - FieldArgumentDescriptionChangedModel, - FieldArgumentDefaultChangedModel, - FieldArgumentTypeChangedModel, - DirectiveRemovedModel, DirectiveAddedModel, + DirectiveArgumentAddedModel, + DirectiveArgumentDefaultValueChangedModel, + DirectiveArgumentDescriptionChangedModel, + DirectiveArgumentRemovedModel, DirectiveDescriptionChangedModel, DirectiveLocationAddedModel, DirectiveLocationRemovedModel, - DirectiveArgumentAddedModel, - DirectiveArgumentRemovedModel, - DirectiveArgumentDescriptionChangedModel, - DirectiveArgumentDefaultValueChangedModel, + DirectiveRemovedModel, + DirectiveUsageArgumentAddedModel, + DirectiveUsageArgumentDefinitionAddedModel, + DirectiveUsageArgumentDefinitionRemovedModel, + DirectiveUsageArgumentRemovedModel, + DirectiveUsageEnumAddedModel, + DirectiveUsageEnumRemovedModel, + DirectiveUsageEnumValueAddedModel, + DirectiveUsageEnumValueRemovedModel, + DirectiveUsageFieldAddedModel, + DirectiveUsageFieldDefinitionAddedModel, + DirectiveUsageFieldDefinitionRemovedModel, + DirectiveUsageFieldRemovedModel, + DirectiveUsageInputFieldDefinitionAddedModel, + DirectiveUsageInputFieldDefinitionRemovedModel, + DirectiveUsageInputObjectAddedModel, + DirectiveUsageInputObjectRemovedModel, + DirectiveUsageInterfaceAddedModel, + DirectiveUsageInterfaceRemovedModel, + DirectiveUsageObjectAddedModel, + DirectiveUsageObjectRemovedModel, + DirectiveUsageScalarAddedModel, + DirectiveUsageScalarRemovedModel, + DirectiveUsageSchemaAddedModel, + DirectiveUsageSchemaRemovedModel, + DirectiveUsageUnionMemberAddedModel, + DirectiveUsageUnionMemberRemovedModel, DirectiveArgumentTypeChangedModel, - EnumValueRemovedModel, - EnumValueAdded, - EnumValueDescriptionChangedModel, - EnumValueDeprecationReasonChangedModel, + EnumValueAddedModel, EnumValueDeprecationReasonAddedModel, + EnumValueDeprecationReasonChangedModel, EnumValueDeprecationReasonRemovedModel, - FieldRemovedModel, + EnumValueDescriptionChangedModel, + EnumValueRemovedModel, FieldAddedModel, - FieldDescriptionChangedModel, - FieldDescriptionAddedModel, - FieldDescriptionRemovedModel, + FieldArgumentAddedModel, + FieldArgumentDefaultChangedModel, + FieldArgumentRemovedModel, + FieldArgumentTypeChangedModel, FieldDeprecationAddedModel, - FieldDeprecationRemovedModel, - FieldDeprecationReasonChangedModel, FieldDeprecationReasonAddedModel, + FieldDeprecationReasonChangedModel, FieldDeprecationReasonRemovedModel, + FieldDeprecationRemovedModel, + FieldDescriptionAddedModel, + FieldDescriptionChangedModel, + FieldDescriptionRemovedModel, + FieldRemovedModel, FieldTypeChangedModel, - FieldArgumentAddedModel, - FieldArgumentRemovedModel, - InputFieldRemovedModel, InputFieldAddedModel, + InputFieldDefaultValueChangedModel, InputFieldDescriptionAddedModel, - InputFieldDescriptionRemovedModel, InputFieldDescriptionChangedModel, - InputFieldDefaultValueChangedModel, + InputFieldDescriptionRemovedModel, + InputFieldRemovedModel, InputFieldTypeChangedModel, ObjectTypeInterfaceAddedModel, ObjectTypeInterfaceRemovedModel, - SchemaQueryTypeChangedModel, SchemaMutationTypeChangedModel, + SchemaQueryTypeChangedModel, SchemaSubscriptionTypeChangedModel, - TypeRemovedModel, TypeAddedModel, - TypeKindChangedModel, - TypeDescriptionChangedModel, TypeDescriptionAddedModel, + TypeDescriptionChangedModel, TypeDescriptionRemovedModel, - UnionMemberRemovedModel, + TypeKindChangedModel, + TypeRemovedModel, UnionMemberAddedModel, + UnionMemberRemovedModel, + // @here >? // Hive Federation/Stitching Specific RegistryServiceUrlChangeModel, ]); +// @todo figure out what this is doing... ({}) as SerializableChange satisfies z.TypeOf; export type Change = z.infer; @@ -877,7 +1251,11 @@ export const HiveSchemaChangeModel = z } | null; readonly breakingChangeSchemaCoordinate: string | null; } => { - const change = schemaChangeFromSerializableChange(rawChange as any); + let change = schemaChangeFromSerializableChange(rawChange as any); + // @todo figure out more permanent solution for unhandled change types. + if (!change) { + throw new Error(`Cannot deserialize change "${rawChange.type}"`); + } /** The schema coordinate used for detecting whether something is a breaking change can be different based on the change type. */ let breakingChangeSchemaCoordinate: string | null = null; @@ -887,9 +1265,9 @@ export const HiveSchemaChangeModel = z if ( isInputFieldAddedChange(rawChange) && - rawChange.meta.isAddedInputFieldTypeNullable === false + change.meta.isAddedInputFieldTypeNullable === false ) { - breakingChangeSchemaCoordinate = rawChange.meta.inputName; + breakingChangeSchemaCoordinate = change.meta.inputName; } } @@ -978,6 +1356,7 @@ const SchemaCheckSharedOutputFields = { githubRepository: z.string().nullable(), githubSha: z.string().nullable(), contextId: z.string().nullable(), + schemaProposalId: z.string().nullable().optional(), }; const SchemaCheckSharedInputFields = { diff --git a/packages/web/app/package.json b/packages/web/app/package.json index 05e215662b..f2e86715e9 100644 --- a/packages/web/app/package.json +++ b/packages/web/app/package.json @@ -20,6 +20,8 @@ "@graphiql/react": "1.0.0-alpha.4", "@graphiql/toolkit": "0.9.1", "@graphql-codegen/client-preset-swc-plugin": "0.2.0", + "@graphql-inspector/core": "file:../../../../graphql-inspector/packages/core", + "@graphql-inspector/patch": "file:../../../../graphql-inspector/packages/patch", "@graphql-tools/mock": "9.0.22", "@graphql-typed-document-node/core": "3.2.0", "@headlessui/react": "2.2.0", diff --git a/packages/web/app/src/components/common/ListNavigation.tsx b/packages/web/app/src/components/common/ListNavigation.tsx new file mode 100644 index 0000000000..d28f418ba1 --- /dev/null +++ b/packages/web/app/src/components/common/ListNavigation.tsx @@ -0,0 +1,137 @@ +import { createContext, ReactNode, useCallback, useContext, useState } from 'react'; +import { cn } from '@/lib/utils'; +import { HamburgerMenuIcon } from '@radix-ui/react-icons'; +import { Button } from '../ui/button'; + +type ListNavigationContextType = { + isListNavCollapsed: boolean; + setIsListNavCollapsed: (collapsed: boolean) => void; + isListNavHidden: boolean; + setIsListNavHidden: (hidden: boolean) => void; +}; + +const ListNavigationContext = createContext({ + isListNavCollapsed: true, + setIsListNavCollapsed: () => {}, + isListNavHidden: false, + setIsListNavHidden: () => {}, +}); + +export function useListNavigationContext() { + return useContext(ListNavigationContext); +} + +export function ListNavigationProvider({ + children, + isCollapsed, + isHidden, +}: { + children: ReactNode; + isCollapsed: boolean; + isHidden: boolean; +}) { + const [isListNavCollapsed, setIsListNavCollapsed] = useState(isCollapsed); + const [isListNavHidden, setIsListNavHidden] = useState(isHidden); + + return ( + + {children} + + ); +} + +export function useListNavCollapsedToggle() { + const { setIsListNavCollapsed, isListNavCollapsed } = useListNavigationContext(); + const toggle = useCallback(() => { + setIsListNavCollapsed(!isListNavCollapsed); + }, [setIsListNavCollapsed, isListNavCollapsed]); + + return [isListNavCollapsed, toggle] as const; +} + +export function useListNavHiddenToggle() { + const { setIsListNavHidden, isListNavHidden, isListNavCollapsed, setIsListNavCollapsed } = + useListNavigationContext(); + const toggle = useCallback(() => { + if (isListNavHidden === false && isListNavCollapsed === true) { + setIsListNavCollapsed(false); + } else { + setIsListNavHidden(!isListNavHidden); + } + }, [isListNavHidden, setIsListNavHidden, isListNavCollapsed, setIsListNavCollapsed]); + + return [isListNavHidden, toggle] as const; +} + +function MenuButton({ onClick, className }: { className?: string; onClick: () => void }) { + return ( + + ); +} + +export function ListNavigationTrigger(props: { children?: ReactNode; className?: string }) { + const [_hidden, toggle] = useListNavHiddenToggle(); + + return props.children ? ( + + ) : ( + + ); +} + +export function ListNavigationWrapper(props: { list: ReactNode; content: ReactNode }) { + const { isListNavCollapsed, isListNavHidden } = useListNavigationContext(); + + return ( +
+ {props.list} +
+ {props.content} +
+
+ ); +} + +export function ListNavigation(props: { children: ReactNode }) { + const { isListNavCollapsed, isListNavHidden } = useListNavigationContext(); + return ( +
+
+
+ {props.children} +
+
+
+ ); +} diff --git a/packages/web/app/src/components/layouts/target.tsx b/packages/web/app/src/components/layouts/target.tsx index 64d508f9d6..3f83cdd2d1 100644 --- a/packages/web/app/src/components/layouts/target.tsx +++ b/packages/web/app/src/components/layouts/target.tsx @@ -41,6 +41,7 @@ export enum Page { Insights = 'insights', Laboratory = 'laboratory', Apps = 'apps', + Proposals = 'proposals', Settings = 'settings', } @@ -230,6 +231,18 @@ export const TargetLayout = ({ )} + + + Proposals + + {currentTarget.viewerCanAccessSettings && ( ; +}) { + const review = useFragment(ProposalOverview_ReviewCommentsFragment, props.review); + if (!review.comments) { + return null; + } + + return ( + <> +
+ {review.comments?.edges?.map(({ node: comment }, idx) => { + return ( + + ); + })} +
+ {/* @todo check if able to reply */} +
+ + +
+ + ); +} + +const ProposalOverview_CommentFragment = graphql(/** GraphQL */ ` + fragment ProposalOverview_CommentFragment on SchemaProposalComment { + id + author + body + updatedAt + createdAt + } +`); + +export function ReviewComment(props: { + first?: boolean; + comment: FragmentType; +}) { + const comment = useFragment(ProposalOverview_CommentFragment, props.comment); + return ( + <> +
+
{comment.author ?? 'Unknown'}
+
+ {!!comment.updatedAt && 'updated '} + +
+
+
{comment.body}
+ + ); +} + +export function DetachedAnnotations(props: { + /** All of the coordinates that have annotations */ + coordinates: string[]; + annotate: (coordinate: string, withPreview?: boolean) => ReactElement | null; +}) { + /** Get the list of coordinates that have already been annotated */ + const { annotatedCoordinates } = useContext(AnnotatedContext); + const detachedReviewCoordinates = props.coordinates.filter(c => annotatedCoordinates?.has(c)); + return detachedReviewCoordinates.length + ? detachedReviewCoordinates.map(c => {props.annotate(c, true)}) + : null; +} diff --git a/packages/web/app/src/components/target/proposals/change-detail.tsx b/packages/web/app/src/components/target/proposals/change-detail.tsx new file mode 100644 index 0000000000..a8546344cf --- /dev/null +++ b/packages/web/app/src/components/target/proposals/change-detail.tsx @@ -0,0 +1,34 @@ +import { ReactNode } from 'react'; +import { + AccordionContent, + AccordionHeader, + AccordionItem, + AccordionTrigger, +} from '@/components/ui/accordion'; +import { Accordion } from '@/components/v2'; +import type { Change } from '@graphql-inspector/core'; +import { labelize } from '../history/errors-and-changes'; + +export function ProposalChangeDetail(props: { + change: Change; + error?: Error; + icon?: ReactNode; +}) { + return ( + + + + +
+
{labelize(props.change.message)}
+
{props.icon}
+
+
+
+ + {props.error?.message ?? <>No details available for this change.} + +
+
+ ); +} diff --git a/packages/web/app/src/components/target/proposals/index.tsx b/packages/web/app/src/components/target/proposals/index.tsx new file mode 100644 index 0000000000..06ad3e9b87 --- /dev/null +++ b/packages/web/app/src/components/target/proposals/index.tsx @@ -0,0 +1,543 @@ +import { Fragment, useMemo } from 'react'; +import { GraphQLSchema } from 'graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { DetachedAnnotations, ReviewComments } from './Review'; +import { AnnotatedProvider } from './schema-diff/components'; +import { SchemaDiff } from './schema-diff/schema-diff'; + +/** + * Fragment containing a list of reviews. Each review is tied to a coordinate + * and may contain one or more comments. This should be fetched in its entirety, + * but this can be done serially because there should not be so many reviews within + * a single screen's height that it matters. + * */ +export const ProposalOverview_ReviewsFragment = graphql(/** GraphQL */ ` + fragment ProposalOverview_ReviewsFragment on SchemaProposalReviewConnection { + pageInfo { + startCursor + } + edges { + cursor + node { + id + stageTransition + lineText + schemaCoordinate + serviceName + ...ProposalOverview_ReviewCommentsFragment + } + } + } +`); + +/** Move to utils? */ +export const ProposalOverview_ChangeFragment = graphql(/* GraphQL */ ` + fragment ProposalOverview_ChangeFragment on SchemaChange { + message(withSafeBasedOnUsageNote: false) + path + severityLevel + meta { + __typename + ... on FieldArgumentDescriptionChanged { + argumentName + fieldName + newDescription + oldDescription + typeName + } + ... on FieldArgumentTypeChanged { + argumentName + fieldName + isSafeArgumentTypeChange + newArgumentType + oldArgumentType + typeName + } + ... on DirectiveRemoved { + removedDirectiveName + } + ... on DirectiveAdded { + addedDirectiveDescription + addedDirectiveLocations + addedDirectiveName + addedDirectiveRepeatable + } + ... on DirectiveDescriptionChanged { + directiveName + newDirectiveDescription + oldDirectiveDescription + } + ... on DirectiveLocationAdded { + addedDirectiveLocation + directiveName + } + ... on DirectiveLocationRemoved { + directiveName + removedDirectiveLocation + } + ... on DirectiveArgumentAdded { + addedDirectiveArgumentDescription + addedDirectiveArgumentName + addedDirectiveArgumentType + addedDirectiveArgumentTypeIsNonNull + addedDirectiveDefaultValue + addedToNewDirective + directiveName + } + ... on DirectiveArgumentRemoved { + directiveName + removedDirectiveArgumentName + } + ... on DirectiveArgumentDescriptionChanged { + directiveArgumentName + directiveName + newDirectiveArgumentDescription + oldDirectiveArgumentDescription + } + ... on DirectiveArgumentDefaultValueChanged { + directiveArgumentName + directiveName + newDirectiveArgumentDefaultValue + oldDirectiveArgumentDefaultValue + } + ... on DirectiveArgumentTypeChanged { + directiveArgumentName + directiveName + newDirectiveArgumentType + oldDirectiveArgumentType + } + ... on EnumValueRemoved { + enumName + removedEnumValueName + } + ... on EnumValueAdded { + addedDirectiveDescription + addedEnumValueName + enumName + } + ... on EnumValueDescriptionChanged { + enumName + enumValueName + newEnumValueDescription + oldEnumValueDescription + } + ... on EnumValueDeprecationReasonChanged { + enumName + enumValueName + newEnumValueDeprecationReason + oldEnumValueDeprecationReason + } + ... on EnumValueDeprecationReasonAdded { + addedValueDeprecationReason + enumName + enumValueName + } + ... on EnumValueDeprecationReasonRemoved { + enumName + enumValueName + removedEnumValueDeprecationReason + } + ... on FieldRemoved { + removedFieldName + typeName + typeType + } + ... on FieldAdded { + addedFieldName + addedFieldReturnType + typeName + typeType + } + ... on FieldDescriptionChanged { + fieldName + newDescription + oldDescription + typeName + } + ... on FieldDescriptionAdded { + addedDescription + fieldName + typeName + } + ... on FieldDescriptionRemoved { + fieldName + typeName + } + ... on FieldDeprecationAdded { + deprecationReason + fieldName + typeName + } + ... on FieldDeprecationRemoved { + fieldName + typeName + } + ... on FieldDeprecationReasonChanged { + fieldName + newDeprecationReason + oldDeprecationReason + typeName + } + ... on FieldDeprecationReasonAdded { + addedDeprecationReason + fieldName + typeName + } + ... on FieldDeprecationReasonRemoved { + fieldName + typeName + } + ... on FieldTypeChanged { + fieldName + newFieldType + oldFieldType + typeName + } + ... on DirectiveUsageUnionMemberAdded { + addedDirectiveName + addedUnionMemberTypeName + addedUnionMemberTypeName + unionName + } + ... on DirectiveUsageUnionMemberRemoved { + removedDirectiveName + removedUnionMemberTypeName + unionName + } + ... on FieldArgumentAdded { + addedArgumentName + addedArgumentType + addedToNewField + fieldName + typeName + } + ... on FieldArgumentRemoved { + fieldName + removedFieldArgumentName + removedFieldType + typeName + } + ... on InputFieldRemoved { + inputName + removedFieldName + } + ... on InputFieldAdded { + addedFieldDefault + addedInputFieldName + addedInputFieldType + inputName + } + ... on InputFieldDescriptionAdded { + addedInputFieldDescription + inputFieldName + inputName + } + ... on InputFieldDescriptionRemoved { + inputFieldName + inputName + removedDescription + } + ... on InputFieldDescriptionChanged { + inputFieldName + inputName + newInputFieldDescription + oldInputFieldDescription + } + ... on InputFieldDefaultValueChanged { + inputFieldName + inputName + newDefaultValue + oldDefaultValue + } + ... on InputFieldTypeChanged { + inputFieldName + inputName + newInputFieldType + oldInputFieldType + } + ... on ObjectTypeInterfaceAdded { + addedInterfaceName + objectTypeName + } + ... on ObjectTypeInterfaceRemoved { + objectTypeName + removedInterfaceName + } + ... on SchemaQueryTypeChanged { + newQueryTypeName + oldQueryTypeName + } + ... on SchemaMutationTypeChanged { + newMutationTypeName + oldMutationTypeName + } + ... on SchemaSubscriptionTypeChanged { + newSubscriptionTypeName + oldSubscriptionTypeName + } + ... on TypeRemoved { + removedTypeName + } + ... on TypeAdded { + addedTypeKind + addedTypeName + } + ... on TypeKindChanged { + newTypeKind + oldTypeKind + typeName + } + ... on TypeDescriptionChanged { + newTypeDescription + oldTypeDescription + typeName + } + ... on TypeDescriptionAdded { + addedTypeDescription + typeName + } + ... on TypeDescriptionRemoved { + removedTypeDescription + typeName + } + ... on UnionMemberRemoved { + removedUnionMemberTypeName + unionName + } + ... on UnionMemberAdded { + addedUnionMemberTypeName + unionName + } + ... on DirectiveUsageEnumAdded { + addedDirectiveName + enumName + } + ... on DirectiveUsageEnumRemoved { + enumName + removedDirectiveName + } + ... on DirectiveUsageEnumValueAdded { + addedDirectiveName + enumName + enumValueName + } + ... on DirectiveUsageEnumValueRemoved { + enumName + enumValueName + removedDirectiveName + } + ... on DirectiveUsageInputObjectRemoved { + inputObjectName + removedDirectiveName + removedInputFieldName + removedInputFieldType + } + ... on DirectiveUsageInputObjectAdded { + addedDirectiveName + addedInputFieldName + addedInputFieldType + inputObjectName + } + ... on DirectiveUsageInputFieldDefinitionAdded { + addedDirectiveName + inputFieldName + inputFieldType + inputObjectName + } + ... on DirectiveUsageInputFieldDefinitionRemoved { + inputFieldName + inputObjectName + removedDirectiveName + } + ... on DirectiveUsageFieldAdded { + addedDirectiveName + fieldName + typeName + } + ... on DirectiveUsageFieldRemoved { + fieldName + removedDirectiveName + typeName + } + ... on DirectiveUsageScalarAdded { + addedDirectiveName + scalarName + } + ... on DirectiveUsageScalarRemoved { + removedDirectiveName + scalarName + } + ... on DirectiveUsageObjectAdded { + addedDirectiveName + objectName + } + ... on DirectiveUsageObjectRemoved { + objectName + removedDirectiveName + } + ... on DirectiveUsageInterfaceAdded { + addedDirectiveName + interfaceName + } + ... on DirectiveUsageSchemaAdded { + addedDirectiveName + schemaTypeName + } + ... on DirectiveUsageSchemaRemoved { + removedDirectiveName + schemaTypeName + } + ... on DirectiveUsageFieldDefinitionAdded { + addedDirectiveName + fieldName + typeName + } + ... on DirectiveUsageFieldDefinitionRemoved { + fieldName + removedDirectiveName + typeName + } + ... on DirectiveUsageArgumentDefinitionRemoved { + argumentName + fieldName + removedDirectiveName + typeName + } + ... on DirectiveUsageInterfaceRemoved { + interfaceName + removedDirectiveName + } + ... on DirectiveUsageArgumentDefinitionAdded { + addedDirectiveName + argumentName + fieldName + typeName + } + ... on DirectiveUsageArgumentAdded { + addedArgumentName + addedArgumentValue + directiveName + oldArgumentValue + } + ... on DirectiveUsageArgumentRemoved { + directiveName + removedArgumentName + } + } + } +`); + +/** Move to utils */ +export function toUpperSnakeCase(str: string) { + // Use a regular expression to find uppercase letters and insert underscores + // The 'g' flag ensures all occurrences are replaced. + // The 'replace' function uses a callback to add an underscore before the matched uppercase letter. + const snakeCaseString = str.replace(/([A-Z])/g, (match, p1, offset) => { + // If it's the first character, don't add an underscore + if (offset === 0) { + return p1; + } + return `_${p1}`; + }); + + return snakeCaseString.toUpperCase(); +} + +// @todo ServiceProposalDetails +export function Proposal(props: { + beforeSchema: GraphQLSchema | null; + afterSchema: GraphQLSchema | null; + reviews: FragmentType; + serviceName: string; +}) { + /** + * Reviews can change position because the coordinate changes... placing them out of order from their original line numbers. + * Because of this, we have to fetch every single page of comments... + * But because generally they are in order, we can take our time doing this. So fetch in small batches. + * + * Odds are there will never be so many reviews/comments that this is even a problem. + */ + const [annotations, reviewssByCoordinate] = useMemo(() => { + const reviewsConnection = useFragment(ProposalOverview_ReviewsFragment, props.reviews); + const serviceReviews = + reviewsConnection?.edges?.filter(edge => { + return edge.node.serviceName === props.serviceName; + }) ?? []; + const reviewssByCoordinate = serviceReviews.reduce((result, review) => { + const coordinate = review.node.schemaCoordinate; + if (coordinate) { + const reviews = result.get(coordinate); + if (reviews) { + result.set(review.node.schemaCoordinate!, [...reviews, review]); + } else { + result.set(review.node.schemaCoordinate!, [review]); + } + } + // @todo else add to global reviews + return result; + }, new Map>()); + + const annotate = (coordinate: string, withPreview = false) => { + const reviews = reviewssByCoordinate.get(coordinate); + if (reviews) { + return ( + <> + {reviews?.map(({ node, cursor }) => ( + + {/* @todo if node.resolvedBy/resolvedAt is set, then minimize this */} + {withPreview === true && node.lineText && ( + + {node.lineText} + + )} + + + ))} + + ); + } + return null; + }; + return [annotate, reviewssByCoordinate]; + }, [props.reviews, props.serviceName]); + + try { + // THIS IS IMPORTANT!! must be rendered first so that it sets up the state in the + // AnnotatedContext for . Otherwise, the DetachedAnnotations will be empty. + const diff = + props.beforeSchema && props.afterSchema ? ( + + ) : ( + <> + ); + + // @todo AnnotatedProvider doesnt work 100% of the time... A different solution must be found + return ( + + ( + <> +
+ This comment refers to a schema coordinate that no longer exists. +
+ {annotations(coordinate, withPreview)} + + )} + /> + {diff} +
+ ); + } catch (e: unknown) { + return ( + <> +
Invalid SDL
+
{e instanceof Error ? e.message : String(e)}
+ + ); + } +} diff --git a/packages/web/app/src/components/target/proposals/schema-diff/compare-lists.ts b/packages/web/app/src/components/target/proposals/schema-diff/compare-lists.ts new file mode 100644 index 0000000000..87b2f651d7 --- /dev/null +++ b/packages/web/app/src/components/target/proposals/schema-diff/compare-lists.ts @@ -0,0 +1,126 @@ +import type { NameNode } from 'graphql'; + +export function keyMap(list: readonly T[], keyFn: (item: T) => string): Record { + return list.reduce((map, item) => { + map[keyFn(item)] = item; + return map; + }, Object.create(null)); +} + +export function isEqual(a: T, b: T): boolean { + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) return false; + + for (let index = 0; index < a.length; index++) { + if (!isEqual(a[index], b[index])) { + return false; + } + } + + return true; + } + + if (a && b && typeof a === 'object' && typeof b === 'object') { + const aRecord = a as Record; + const bRecord = b as Record; + + const aKeys: string[] = Object.keys(aRecord); + const bKeys: string[] = Object.keys(bRecord); + + if (aKeys.length !== bKeys.length) return false; + + for (const key of aKeys) { + if (!isEqual(aRecord[key], bRecord[key])) { + return false; + } + } + + return true; + } + + return a === b || (!a && !b); +} + +export function isNotEqual(a: T, b: T): boolean { + return !isEqual(a, b); +} + +export function isVoid(a: T): boolean { + return typeof a === 'undefined' || a === null; +} + +export function diffArrays(a: T[] | readonly T[], b: T[] | readonly T[]): T[] { + return a.filter(c => !b.some(d => isEqual(d, c))); +} + +export function matchArrays(a: T[] | readonly T[], b: T[] | readonly T[]): T[] { + return a.filter(c => b.some(d => isEqual(d, c))); +} + +function extractName(name: string | NameNode): string { + if (typeof name === 'string') { + return name; + } + + return name.value; +} + +/** @todo support repeat directives */ +export function compareLists( + oldList: readonly T[], + newList: readonly T[], + callbacks?: { + onAdded?(t: T): void; + onRemoved?(t: T): void; + onMutual?(t: { newVersion: T; oldVersion: T }): void; + }, +) { + const oldMap = keyMap(oldList, ({ name }) => extractName(name)); + const newMap = keyMap(newList, ({ name }) => extractName(name)); + + const added: T[] = []; + const removed: T[] = []; + const mutual: Array<{ newVersion: T; oldVersion: T }> = []; + + for (const oldItem of oldList) { + const newItem = newMap[extractName(oldItem.name)]; + if (newItem === undefined) { + removed.push(oldItem); + } else { + mutual.push({ + newVersion: newItem, + oldVersion: oldItem, + }); + } + } + + for (const newItem of newList) { + if (oldMap[extractName(newItem.name)] === undefined) { + added.push(newItem); + } + } + + if (callbacks) { + if (callbacks.onAdded) { + for (const item of added) { + callbacks.onAdded(item); + } + } + if (callbacks.onRemoved) { + for (const item of removed) { + callbacks.onRemoved(item); + } + } + if (callbacks.onMutual) { + for (const item of mutual) { + callbacks.onMutual(item); + } + } + } + + return { + added, + removed, + mutual, + }; +} diff --git a/packages/web/app/src/components/target/proposals/schema-diff/components.tsx b/packages/web/app/src/components/target/proposals/schema-diff/components.tsx new file mode 100644 index 0000000000..8d24d4a151 --- /dev/null +++ b/packages/web/app/src/components/target/proposals/schema-diff/components.tsx @@ -0,0 +1,1453 @@ +/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ +import { createContext, Fragment, ReactElement, ReactNode, useContext, useState } from 'react'; +import { + astFromValue, + ConstArgumentNode, + ConstDirectiveNode, + DirectiveLocation, + GraphQLArgument, + GraphQLDirective, + GraphQLEnumType, + GraphQLEnumValue, + GraphQLField, + GraphQLInputField, + GraphQLInputObjectType, + GraphQLInputType, + GraphQLInterfaceType, + GraphQLNamedType, + GraphQLObjectType, + GraphQLOutputType, + GraphQLScalarType, + GraphQLSchema, + GraphQLUnionType, + isEnumType, + isInputObjectType, + isInterfaceType, + isObjectType, + isScalarType, + isUnionType, + Kind, + print, +} from 'graphql'; +import { isPrintableAsBlockString } from 'graphql/language/blockString'; +import { CheckIcon, XIcon } from '@/components/ui/icon'; +import { SeverityLevelType } from '@/gql/graphql'; +import { cn } from '@/lib/utils'; +import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; +import { compareLists, diffArrays, matchArrays } from './compare-lists'; + +type RootFieldsType = { + query: GraphQLField; + mutation: GraphQLField; + subscription: GraphQLField; +}; + +const TAB = <>  ; + +export const AnnotatedContext = createContext({ + annotatedCoordinates: null, +} as Readonly<{ + /** + * As annotations are rendered, this tracks coordinates used. This is used internally to + * show annotations that are not resolved but that are not tied to a coordinate that exists anymore. + * + * Note that adding a value to this Set does not trigger a rerender. + * Special care must be taken to ensure the render order is correct + */ + annotatedCoordinates: Set | null; +}>); + +export function AnnotatedProvider(props: { children: ReactNode }) { + const [context] = useState({ annotatedCoordinates: new Set() }); + return {props.children}; +} + +export function ChangeDocument(props: { children: ReactNode; className?: string }) { + return ( + + {props.children} +
+ ); +} + +export function ChangeSpacing(props: { type?: 'removal' | 'addition' | 'mutual' }) { + return ( + + + + + + ); +} + +export function ChangeRow(props: { + children?: ReactNode; + className?: string; + /** Default is mutual */ + type?: 'removal' | 'addition' | 'mutual'; + severityLevel?: SeverityLevelType; + indent?: boolean | number; + coordinate?: string; + annotations?: (coordinate: string) => ReactElement | null; +}) { + const ctx = useContext(AnnotatedContext); + const incrementCounter = + props.type === 'mutual' || props.type === undefined + ? 'olddoc newdoc' + : props.type === 'removal' + ? 'olddoc' + : 'newdoc'; + const annotation = !!props.coordinate && props.annotations?.(props.coordinate); + + if (!!annotation) { + ctx.annotatedCoordinates?.add(props.coordinate!); + } + + return ( + <> + + + + + + {props.indent && + Array.from({ length: Number(props.indent) }).map((_, i) => ( + {TAB} + ))} + {props.severityLevel === SeverityLevelType.Breaking && ( + + + + )} + {props.severityLevel === SeverityLevelType.Dangerous && ( + + + + )} + {props.severityLevel === SeverityLevelType.Safe && ( + + + + )} + {props.children} + + + + {annotation && ( + + {annotation} + + )} + + ); +} + +function Keyword(props: { term: string }) { + return {props.term}; +} + +function Removal(props: { children: ReactNode | string; className?: string }): ReactNode { + return ( + + {props.children} + + ); +} + +function Addition(props: { children: ReactNode; className?: string }): ReactNode { + return ( + {props.children} + ); +} + +function printDescription(def: { readonly description: string | undefined | null }): string | null { + const { description } = def; + if (description == null) { + return null; + } + + const blockString = print({ + kind: Kind.STRING, + value: description, + block: isPrintableAsBlockString(description), + }); + + return blockString; +} + +function Description(props: { + content: string; + type?: 'removal' | 'addition' | 'mutual'; + indent?: boolean | number; + annotations: (coordinat: string) => ReactElement | null; +}): ReactNode { + const lines = props.content.split('\n'); + + return ( + <> + {lines.map((line, index) => ( + + + {line} + + + ))} + + ); +} + +function FieldName(props: { name: string }): ReactNode { + return {props.name}; +} + +function FieldReturnType(props: { returnType: string }): ReactNode { + return {props.returnType}; +} + +export function DiffDescription( + props: + | { + oldNode: { description: string | null | undefined } | null; + newNode: { description: string | null | undefined }; + indent?: boolean | number; + } + | { + oldNode: { description: string | null | undefined }; + newNode: { description: string | null | undefined } | null; + indent?: boolean | number; + }, +) { + const oldDesc = props.oldNode?.description; + const newDesc = props.newNode?.description; + if (oldDesc !== newDesc) { + return ( + <> + {/* + To improve this and how only the minimal change, + do a string diff of the description instead of this simple compare. + */} + {oldDesc && ( + null} + /> + )} + {newDesc && ( + null} + /> + )} + + ); + } + if (newDesc) { + return ( + null} + /> + ); + } +} + +export function DiffInputField({ + parentPath, + oldField, + newField, + annotations, +}: + | { + parentPath: string[]; + oldField: GraphQLInputField | null; + newField: GraphQLInputField; + annotations: (coordinat: string) => ReactElement | null; + } + | { + parentPath: string[]; + oldField: GraphQLInputField; + newField: GraphQLInputField | null; + annotations: (coordinat: string) => ReactElement | null; + }) { + const changeType = determineChangeType(oldField, newField); + const name = newField?.name ?? oldField?.name ?? ''; + const path = [...parentPath, name]; + // @todo consider allowing comments on nested coordinates. + // const directiveCoordinates = [...newField?.astNode?.directives ?? [], ...oldField?.astNode?.directives ?? []].map(d => { + // return [...path, `@${d.name.value}`].join('.') + // }); + return ( + <> + + + + + + :  + + + + + ); +} + +function Change({ + type, + children, +}: { + children: ReactNode; + type?: 'addition' | 'removal' | 'mutual'; +}): ReactNode { + const Klass = type === 'addition' ? Addition : type === 'removal' ? Removal : Fragment; + return {children}; +} + +export function DiffField({ + parentPath, + oldField, + newField, + annotations, +}: + | { + parentPath: string[]; + oldField: GraphQLField | null; + newField: GraphQLField; + annotations: (coordinat: string) => ReactElement | null; + } + | { + parentPath: string[]; + oldField: GraphQLField; + newField: GraphQLField | null; + annotations: (coordinat: string) => ReactElement | null; + }) { + const hasNewArgs = !!newField?.args.length; + const hasOldArgs = !!oldField?.args.length; + const hasArgs = hasNewArgs || hasOldArgs; + const argsChangeType = hasNewArgs + ? hasOldArgs + ? 'mutual' + : 'addition' + : hasOldArgs + ? 'removal' + : 'mutual'; + const name = newField?.name ?? oldField?.name ?? ''; + const changeType = determineChangeType(oldField, newField); + const AfterArguments = ( + <> + :  + + + + ); + const path = [...parentPath, name]; + return ( + <> + + + + + + {hasArgs && ( + + <>( + + )} + {!hasArgs && AfterArguments} + + + {!!hasArgs && ( + + + <>) + + {AfterArguments} + + )} + + ); +} + +export function DirectiveName(props: { name: string }) { + return @{props.name}; +} + +export function DiffArguments(props: { + parentPath: string[]; + oldArgs: readonly GraphQLArgument[]; + newArgs: readonly GraphQLArgument[]; + indent: boolean | number; + annotations: (coordinat: string) => ReactElement | null; +}) { + const { added, mutual, removed } = compareLists(props.oldArgs, props.newArgs); + return ( + <> + {removed.map(a => ( + + + + + + + : + + + + + ))} + {added.map(a => ( + + + + + + + : + + + + + ))} + {mutual.map(a => ( + + + + + + + : + + + + + ))} + + ); +} + +function determineChangeType(oldType: T | null, newType: T | null) { + if (oldType && !newType) { + return 'removal' as const; + } + if (newType && !oldType) { + return 'addition' as const; + } + return 'mutual' as const; +} + +export function DiffLocations(props: { + newLocations: readonly DirectiveLocation[]; + oldLocations: readonly DirectiveLocation[]; +}) { + const locations = { + added: diffArrays(props.newLocations, props.oldLocations), + removed: diffArrays(props.oldLocations, props.newLocations), + mutual: matchArrays(props.oldLocations, props.newLocations), + }; + + const locationElements = [ + ...locations.removed.map(r => ( + + + + )), + ...locations.added.map(r => ( + + + + )), + ...locations.mutual.map(r => ), + ]; + + return ( + <> + +   + {locationElements.map((e, index) => ( + + {e} + {index !== locationElements.length - 1 && <> | } + + ))} + + ); +} + +function DiffRepeatable( + props: + | { + oldDirective: GraphQLDirective | null; + newDirective: GraphQLDirective; + } + | { + oldDirective: GraphQLDirective; + newDirective: GraphQLDirective | null; + }, +) { + const oldRepeatable = !!props.oldDirective?.isRepeatable; + const newRepeatable = !!props.newDirective?.isRepeatable; + if (oldRepeatable === newRepeatable) { + return newRepeatable ? ( + <> + +   + + ) : null; + } + return ( + <> + {oldRepeatable && ( + + +   + + )} + {newRepeatable && ( + + +   + + )} + + ); +} + +export function DiffDirective( + props: + | { + oldDirective: GraphQLDirective | null; + newDirective: GraphQLDirective; + annotations: (coordinat: string) => ReactElement | null; + } + | { + oldDirective: GraphQLDirective; + newDirective: GraphQLDirective | null; + annotations: (coordinat: string) => ReactElement | null; + }, +) { + const name = props.newDirective?.name ?? props.oldDirective?.name ?? ''; + const changeType = determineChangeType(props.oldDirective, props.newDirective); + const hasNewArgs = !!props.newDirective?.args.length; + const hasOldArgs = !!props.oldDirective?.args.length; + const hasArgs = hasNewArgs || hasOldArgs; + const argsChangeType = hasNewArgs + ? hasOldArgs + ? 'mutual' + : 'addition' + : hasOldArgs + ? 'removal' + : 'mutual'; + const AfterArguments = ( + <> +   + + + + ); + const path = [`@${name}`]; + return ( + <> + + + + + +   + + + {!!hasArgs && ( + + <>( + + )} + {!hasArgs && AfterArguments} + + + {!!hasArgs && ( + + + <>) + + {AfterArguments} + + )} + + ); +} + +function DiffReturnType( + props: + | { + oldType: GraphQLInputType | GraphQLOutputType; + newType: GraphQLInputType | GraphQLOutputType | null | undefined; + } + | { + oldType: GraphQLInputType | GraphQLOutputType | null | undefined; + newType: GraphQLInputType | GraphQLOutputType; + } + | { + oldType: GraphQLInputType | GraphQLOutputType; + newType: GraphQLInputType | GraphQLOutputType; + }, +) { + const oldStr = props.oldType?.toString(); + const newStr = props.newType?.toString(); + if (newStr && oldStr === newStr) { + return ; + } + + return ( + <> + {oldStr && ( + + + + )} + {newStr && ( + + + + )} + + ); +} + +function printDefault(arg: GraphQLArgument) { + const defaultAST = astFromValue(arg.defaultValue, arg.type); + return defaultAST && print(defaultAST); +} + +function DiffDefaultValue({ + oldArg, + newArg, +}: { + oldArg: GraphQLArgument | null; + newArg: GraphQLArgument | null; +}) { + const oldDefault = oldArg && printDefault(oldArg); + const newDefault = newArg && printDefault(newArg); + + if (oldDefault === newDefault) { + return newDefault ? <> = {newDefault} : null; + } + return ( + <> + {oldDefault && = {oldDefault}} + {newDefault && ( + = {newDefault} + )} + + ); +} + +export function SchemaDefinitionDiff({ + oldSchema, + newSchema, + annotations, +}: { + oldSchema: GraphQLSchema | undefined | null; + newSchema: GraphQLSchema | undefined | null; + annotations: (coordinat: string) => ReactElement | null; +}) { + const defaultNames = { + query: 'Query', + mutation: 'Mutation', + subscription: 'Subscription', + }; + const oldRoot: RootFieldsType = { + query: { + args: [], + name: 'query', + type: + oldSchema?.getQueryType() ?? + ({ name: defaultNames.query, toString: () => defaultNames.query } as GraphQLOutputType), + astNode: null, + deprecationReason: null, + description: null, + extensions: {}, + }, + mutation: { + args: [], + name: 'mutation', + type: + oldSchema?.getMutationType() ?? + ({ + name: defaultNames.mutation, + toString: () => defaultNames.mutation, + } as GraphQLOutputType), + astNode: null, + deprecationReason: null, + description: null, + extensions: {}, + }, + subscription: { + args: [], + name: 'subscription', + type: + oldSchema?.getSubscriptionType() ?? + ({ + name: defaultNames.subscription, + toString: () => defaultNames.subscription, + } as GraphQLOutputType), + astNode: null, + deprecationReason: null, + description: null, + extensions: {}, + }, + }; + const newRoot: RootFieldsType = { + query: { + args: [], + name: 'query', + type: + newSchema?.getQueryType() ?? + ({ name: defaultNames.query, toString: () => defaultNames.query } as GraphQLOutputType), + astNode: null, + deprecationReason: null, + description: null, + extensions: {}, + }, + mutation: { + args: [], + name: 'mutation', + type: + newSchema?.getMutationType() ?? + ({ + name: defaultNames.mutation, + toString: () => defaultNames.mutation, + } as GraphQLOutputType), + astNode: null, + deprecationReason: null, + description: null, + extensions: {}, + }, + subscription: { + args: [], + name: 'subscription', + type: + newSchema?.getSubscriptionType() ?? + ({ + name: defaultNames.subscription, + toString: () => defaultNames.subscription, + } as GraphQLOutputType), + astNode: null, + deprecationReason: null, + description: null, + extensions: {}, + }, + }; + // @todo verify using this as the path is correct. + const path = ['']; + const changeType = determineChangeType(oldSchema, newSchema); + + return ( + <> + + + + {' {'} + + + + + {'}'} + + ); +} + +/** For any named type */ +export function DiffType({ + oldType, + newType, + annotations, +}: + | { + oldType: GraphQLNamedType; + newType: GraphQLNamedType | null; + annotations: (coordinat: string) => ReactElement | null; + } + | { + oldType: GraphQLNamedType | null; + newType: GraphQLNamedType; + annotations: (coordinat: string) => ReactElement | null; + }) { + if ((isEnumType(oldType) || oldType === null) && (isEnumType(newType) || newType === null)) { + return ; + } + if ((isUnionType(oldType) || oldType === null) && (isUnionType(newType) || newType === null)) { + return ; + } + if ( + (isInputObjectType(oldType) || oldType === null) && + (isInputObjectType(newType) || newType === null) + ) { + return ; + } + if ((isObjectType(oldType) || oldType === null) && (isObjectType(newType) || newType === null)) { + return ; + } + if ( + (isInterfaceType(oldType) || oldType === null) && + (isInterfaceType(newType) || newType === null) + ) { + return ; + } + if ((isScalarType(oldType) || oldType === null) && (isScalarType(newType) || newType === null)) { + return ; + } +} + +function TypeName({ name }: { name: string }) { + return {name}; +} + +export function DiffInputObject({ + oldInput, + newInput, + annotations, +}: + | { + oldInput: GraphQLInputObjectType | null; + newInput: GraphQLInputObjectType; + annotations: (coordinat: string) => ReactElement | null; + } + | { + oldInput: GraphQLInputObjectType; + newInput: GraphQLInputObjectType | null; + annotations: (coordinat: string) => ReactElement | null; + }) { + const { added, mutual, removed } = compareLists( + Object.values(oldInput?.getFields() ?? {}), + Object.values(newInput?.getFields() ?? {}), + ); + const changeType = determineChangeType(oldInput, newInput); + const name = oldInput?.name ?? newInput?.name ?? ''; + const path = [name]; + return ( + <> + + + + + +   + + + + {' {'} + + {removed.map(a => ( + + ))} + {added.map(a => ( + + ))} + {mutual.map(a => ( + + ))} + + {'}'} + + + ); +} + +export function DiffObject({ + oldObject, + newObject, + annotations, +}: + | { + oldObject: GraphQLObjectType | GraphQLInterfaceType | null; + newObject: GraphQLObjectType | GraphQLInterfaceType; + annotations: (coordinat: string) => ReactElement | null; + } + | { + oldObject: GraphQLObjectType | GraphQLInterfaceType; + newObject: GraphQLObjectType | GraphQLInterfaceType | null; + annotations: (coordinat: string) => ReactElement | null; + }) { + const { added, mutual, removed } = compareLists( + Object.values(oldObject?.getFields() ?? {}), + Object.values(newObject?.getFields() ?? {}), + ); + const name = oldObject?.name ?? newObject?.name ?? ''; + const changeType = determineChangeType(oldObject, newObject); + const path = [name]; + return ( + <> + + + + + +   + + + + + {' {'} + + {removed.map(a => ( + + ))} + {added.map(a => ( + + ))} + {mutual.map(a => ( + + ))} + + {'}'} + + + ); +} + +export function DiffEnumValue({ + parentPath, + oldValue, + newValue, + annotations, +}: { + parentPath: string[]; + oldValue: GraphQLEnumValue | null; + newValue: GraphQLEnumValue | null; + annotations: (coordinat: string) => ReactElement | null; +}) { + const changeType = determineChangeType(oldValue, newValue); + const name = oldValue?.name ?? newValue?.name ?? ''; + return ( + <> + + + + + + + + + ); +} + +export function DiffEnum({ + oldEnum, + newEnum, + annotations, +}: { + oldEnum: GraphQLEnumType | null; + newEnum: GraphQLEnumType | null; + annotations: (coordinat: string) => ReactElement | null; +}) { + const { added, mutual, removed } = compareLists( + oldEnum?.getValues() ?? [], + newEnum?.getValues() ?? [], + ); + + const changeType = determineChangeType(oldEnum, newEnum); + const name = oldEnum?.name ?? newEnum?.name ?? ''; + + return ( + <> + + + + + +   + + + {' {'} + + {removed.map(a => ( + + ))} + {added.map(a => ( + + ))} + {mutual.map(a => ( + + ))} + + {'}'} + + + ); +} + +export function DiffUnion({ + oldUnion, + newUnion, + annotations, +}: { + oldUnion: GraphQLUnionType | null; + newUnion: GraphQLUnionType | null; + annotations: (coordinat: string) => ReactElement | null; +}) { + const { added, mutual, removed } = compareLists( + oldUnion?.getTypes() ?? [], + newUnion?.getTypes() ?? [], + ); + + const changeType = determineChangeType(oldUnion, newUnion); + const name = oldUnion?.name ?? newUnion?.name ?? ''; + const path = [name]; + return ( + <> + + + + + +   + + + + {' = '} + + {removed.map(a => ( + + + + | + + + + ))} + {added.map(a => ( + + + + | + + + + ))} + {mutual.map(a => ( + + + + | + + + + ))} + + ); +} + +export function DiffScalar({ + oldScalar, + newScalar, + annotations, +}: + | { + oldScalar: GraphQLScalarType; + newScalar: GraphQLScalarType | null; + annotations: (coordinat: string) => ReactElement | null; + } + | { + oldScalar: GraphQLScalarType | null; + newScalar: GraphQLScalarType; + annotations: (coordinat: string) => ReactElement | null; + }) { + const changeType = determineChangeType(oldScalar, newScalar); + const name = newScalar?.name ?? oldScalar?.name ?? ''; + return ( + <> + + + + + +   + + + + + + ); +} + +export function DiffInterfaces(props: { + oldInterfaces: readonly GraphQLInterfaceType[]; + newInterfaces: readonly GraphQLInterfaceType[]; +}) { + if (props.oldInterfaces.length + props.newInterfaces.length === 0) { + return null; + } + const { added, mutual, removed } = compareLists(props.oldInterfaces, props.newInterfaces); + + let implementsChangeType: 'mutual' | 'addition' | 'removal'; + if (props.oldInterfaces.length === 0 && props.newInterfaces.length !== 0) { + implementsChangeType = 'addition'; + } else if (props.oldInterfaces.length !== 0 && props.newInterfaces.length === 0) { + implementsChangeType = 'removal'; + } else { + implementsChangeType = 'mutual'; + } + + return ( + <> +  implements  + {/* @todo move amp to other side */} + {...removed.map((r, idx) => ( + + {idx !== 0 && ' & '} + {r.name} + + ))} + {...added.map((r, idx) => ( + + {(removed.length !== 0 || idx !== 0) && ' & '} + {r.name} + + ))} + {...mutual.map(({ newVersion: r }, idx) => ( + + {(removed.length !== 0 || added.length !== 0 || idx !== 0) && ' & '} + {r.name} + + ))} + + ); +} + +export function DiffDirectiveUsages(props: { + oldDirectives: readonly ConstDirectiveNode[]; + newDirectives: readonly ConstDirectiveNode[]; +}) { + const { added, mutual, removed } = compareLists(props.oldDirectives, props.newDirectives); + + return ( + <> + {removed.map((d, index) => ( + + ))} + {added.map((d, index) => ( + + ))} + {mutual.map((d, index) => ( + + ))} + + ); +} + +export function DiffDirectiveUsage( + props: + | { + oldDirective: ConstDirectiveNode | null; + newDirective: ConstDirectiveNode; + } + | { + oldDirective: ConstDirectiveNode; + newDirective: ConstDirectiveNode | null; + }, +) { + const name = props.newDirective?.name.value ?? props.oldDirective?.name.value ?? ''; + const newArgs = props.newDirective?.arguments ?? []; + const oldArgs = props.oldDirective?.arguments ?? []; + const hasNewArgs = !!newArgs.length; + const hasOldArgs = !!oldArgs.length; + const hasArgs = hasNewArgs || hasOldArgs; + const argsChangeType = hasNewArgs + ? hasOldArgs + ? 'mutual' + : 'addition' + : hasOldArgs + ? 'removal' + : 'mutual'; + const changeType = determineChangeType(props.oldDirective, props.newDirective); + const { added, mutual, removed } = compareLists(oldArgs, newArgs); + const argumentElements = [ + ...removed.map((r, index) => ( + + + + )), + ...added.map((r, index) => ( + + + + )), + ...mutual.map((r, index) => ( + + + + )), + ]; + + return ( + +   + + {hasArgs && ( + + <>( + + )} + {argumentElements.map((e, index) => ( + + {e} + {index === argumentElements.length - 1 ? '' : ', '} + + ))} + {hasArgs && ( + + <>) + + )} + + ); +} + +const DiffTypeStr = ({ + oldType, + newType, +}: { + oldType: string | null; + newType: string | null; +}): ReactNode => { + if (oldType === newType) { + return newType; + } + return ( + <> + {oldType && {oldType}} + {newType && {newType}} + + ); +}; + +export function DiffArgumentAST({ + oldArg, + newArg, +}: { + oldArg: ConstArgumentNode | null; + newArg: ConstArgumentNode | null; +}) { + const name = oldArg?.name.value ?? newArg?.name.value ?? ''; + const oldType = oldArg && print(oldArg.value); + const newType = newArg && print(newArg.value); + return ( + <> + + :  + + + ); +} diff --git a/packages/web/app/src/components/target/proposals/schema-diff/schema-diff.tsx b/packages/web/app/src/components/target/proposals/schema-diff/schema-diff.tsx new file mode 100644 index 0000000000..15f1468709 --- /dev/null +++ b/packages/web/app/src/components/target/proposals/schema-diff/schema-diff.tsx @@ -0,0 +1,91 @@ +/* eslint-disable tailwindcss/no-custom-classname */ +import { ReactElement, useMemo } from 'react'; +import type { GraphQLSchema } from 'graphql'; +import { isIntrospectionType, isSpecifiedDirective } from 'graphql'; +import { isPrimitive } from '@graphql-inspector/core/utils/graphql'; +import { compareLists } from './compare-lists'; +import { ChangeDocument, DiffDirective, DiffType, SchemaDefinitionDiff } from './components'; + +export function SchemaDiff({ + before, + after, + annotations = () => null, + // annotatedCoordinates = [], +}: { + before: GraphQLSchema; + after: GraphQLSchema; + annotations?: (coordinate: string) => ReactElement | null; + /** + * A list of all the annotated coordinates, used or unused. + * Required to track which coordinates have inline annotations and which are detached + * from the current schemas. E.g. if previously commented on an addition but that addition + * has been removed. + */ + // annotatedCoordinates?: string[]; +}): JSX.Element { + const { + added: addedTypes, + mutual: mutualTypes, + removed: removedTypes, + } = useMemo(() => { + return compareLists( + Object.values(before.getTypeMap()).filter(t => !isPrimitive(t) && !isIntrospectionType(t)), + Object.values(after.getTypeMap()).filter(t => !isPrimitive(t) && !isIntrospectionType(t)), + ); + }, [before, after]); + + const { + added: addedDirectives, + mutual: mutualDirectives, + removed: removedDirectives, + } = useMemo(() => { + return compareLists( + before.getDirectives().filter(d => !isSpecifiedDirective(d)), + after.getDirectives().filter(d => !isSpecifiedDirective(d)), + ); + }, [before, after]); + + return ( + + {removedDirectives.map(d => ( + + ))} + {addedDirectives.map(d => ( + + ))} + {mutualDirectives.map(d => ( + + ))} + + {removedTypes.map(a => ( + + ))} + {addedTypes.map(a => ( + + ))} + {mutualTypes.map(a => ( + + ))} + + ); +} diff --git a/packages/web/app/src/components/target/proposals/service-heading.tsx b/packages/web/app/src/components/target/proposals/service-heading.tsx new file mode 100644 index 0000000000..f06ee78818 --- /dev/null +++ b/packages/web/app/src/components/target/proposals/service-heading.tsx @@ -0,0 +1,13 @@ +import { CubeIcon } from '@radix-ui/react-icons'; + +export function ServiceHeading(props: { serviceName: string }) { + if (props.serviceName.length === 0) { + return null; + } + return ( +
+ + {props.serviceName} +
+ ); +} diff --git a/packages/web/app/src/components/target/proposals/stage-filter.tsx b/packages/web/app/src/components/target/proposals/stage-filter.tsx new file mode 100644 index 0000000000..f0c80027c6 --- /dev/null +++ b/packages/web/app/src/components/target/proposals/stage-filter.tsx @@ -0,0 +1,93 @@ +import { useState } from 'react'; +import { ChevronsUpDown } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Command, CommandGroup, CommandItem } from '@/components/ui/command'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Checkbox } from '@/components/v2'; +import { SchemaProposalStage } from '@/gql/graphql'; +import { useRouter, useSearch } from '@tanstack/react-router'; + +export const StageFilter = ({ selectedStages }: { selectedStages: string[] }) => { + const [open, setOpen] = useState(false); + const hasSelection = selectedStages.length !== 0; + const router = useRouter(); + const search = useSearch({ strict: false }); + const stages = Object.values(SchemaProposalStage).map(s => s.toLocaleLowerCase()); + + return ( + + + + + + + + + { + const allSelected = stages.every(s => selectedStages.includes(s)); + let updated: string[] | undefined; + if (allSelected) { + updated = undefined; + } else { + updated = [...stages]; + } + void router.navigate({ + search: { ...search, stage: updated }, + }); + }} + className="cursor-pointer truncate border-b" + > +
+ selectedStages.includes(s))} + /> +
All
+
+
+ {stages?.map(stage => ( + { + let updated: string[] | undefined = [...selectedStages]; + const selectionIdx = updated.findIndex(s => s === selectedStage); + if (selectionIdx >= 0) { + updated.splice(selectionIdx, 1); + if (updated.length === 0) { + updated = undefined; + } + } else { + updated.push(selectedStage); + } + void router.navigate({ + search: { ...search, stage: updated }, + }); + }} + className="cursor-pointer truncate" + > +
+ +
{stage}
+
+
+ ))} +
+
+
+
+
+ ); +}; diff --git a/packages/web/app/src/components/target/proposals/stage-transition-select.tsx b/packages/web/app/src/components/target/proposals/stage-transition-select.tsx new file mode 100644 index 0000000000..303bfe119a --- /dev/null +++ b/packages/web/app/src/components/target/proposals/stage-transition-select.tsx @@ -0,0 +1,106 @@ +import { useState } from 'react'; +import { ChevronsUpDown } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Command, CommandGroup, CommandItem } from '@/components/ui/command'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { SchemaProposalStage } from '@/gql/graphql'; +import { cn } from '@/lib/utils'; + +const STAGE_TRANSITIONS: ReadonlyArray< + Readonly<{ + fromStates: ReadonlyArray; + value: SchemaProposalStage; + label: string; + }> +> = [ + { + fromStates: [SchemaProposalStage.Open, SchemaProposalStage.Approved], + value: SchemaProposalStage.Draft, + label: 'REVERT TO DRAFT', + }, + { + fromStates: [SchemaProposalStage.Draft], + value: SchemaProposalStage.Open, + label: 'READY FOR REVIEW', + }, + { + fromStates: [SchemaProposalStage.Closed], + value: SchemaProposalStage.Draft, + label: 'REOPEN AS DRAFT', + }, + { + fromStates: [SchemaProposalStage.Closed, SchemaProposalStage.Approved], + value: SchemaProposalStage.Open, + label: 'REOPEN', + }, + { + fromStates: [SchemaProposalStage.Open], + value: SchemaProposalStage.Approved, + label: 'APPROVE FOR IMPLEMENTING', + }, + { + fromStates: [SchemaProposalStage.Draft, SchemaProposalStage.Open, SchemaProposalStage.Approved], + value: SchemaProposalStage.Closed, + label: 'CANCEL PROPOSAL', + }, +]; + +const STAGE_TITLES = { + [SchemaProposalStage.Open]: 'READY FOR REVIEW', + [SchemaProposalStage.Approved]: 'AWAITING IMPLEMENTATION', + [SchemaProposalStage.Closed]: 'CANCELED', + [SchemaProposalStage.Draft]: 'IN DRAFT', + [SchemaProposalStage.Implemented]: 'IMPLEMENTED', +} as const; + +export function StageTransitionSelect(props: { + stage: SchemaProposalStage; + onSelect: (stage: SchemaProposalStage) => void | Promise; +}) { + const [open, setOpen] = useState(false); + return ( + + + + + + + + + {STAGE_TRANSITIONS.filter(s => s.fromStates.includes(props.stage))?.map(s => ( + { + // @todo debounce... + await props.onSelect(value.toUpperCase() as SchemaProposalStage); + setOpen(false); + }} + className="cursor-pointer truncate" + > +
+ {s.label} +
+
+ ))} +
+
+
+
+
+ ); +} diff --git a/packages/web/app/src/components/target/proposals/user-filter.tsx b/packages/web/app/src/components/target/proposals/user-filter.tsx new file mode 100644 index 0000000000..b11b6bceae --- /dev/null +++ b/packages/web/app/src/components/target/proposals/user-filter.tsx @@ -0,0 +1,127 @@ +import { useMemo, useState } from 'react'; +import { ChevronsUpDown } from 'lucide-react'; +import { useQuery } from 'urql'; +import { Button } from '@/components/ui/button'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from '@/components/ui/command'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Checkbox } from '@/components/v2'; +import { graphql } from '@/gql'; +import { useRouter, useSearch } from '@tanstack/react-router'; + +const UsersSearchQuery = graphql(` + query UsersSearch($organizationSlug: String!, $after: String, $first: Int) { + organization(reference: { bySelector: { organizationSlug: $organizationSlug } }) { + id + viewerCanSeeMembers + members(first: $first, after: $after) { + edges { + node { + id + user { + id + displayName + fullName + } + } + } + pageInfo { + hasNextPage + startCursor + } + } + } + } +`); + +export const UserFilter = ({ + selectedUsers, + organizationSlug, +}: { + selectedUsers: string[]; + organizationSlug: string; +}) => { + const [open, setOpen] = useState(false); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [pages, setPages] = useState([{ after: null, first: 200 }]); + const hasSelection = selectedUsers.length !== 0; + const router = useRouter(); + const [query] = useQuery({ + query: UsersSearchQuery, + variables: { + after: pages[pages.length - 1]?.after, + first: pages[pages.length - 1]?.first, + organizationSlug, + }, + }); + const search = useSearch({ strict: false }); + const users = query.data?.organization?.members.edges.map(e => e.node.user) ?? []; + // @todo handle preloading selected users to populate on refresh.... And only search on open. + const selectedUserNames = useMemo(() => { + return selectedUsers.map(selectedUserId => { + const match = users.find(user => user.id === selectedUserId); + return match?.displayName ?? match?.fullName ?? 'Unknown'; + }); + }, [users]); + + return ( + + + + + + + + No results. + + + {users?.map(user => ( + { + const selectedUserId = selectedUser.split(' ')[0]; + let updated: string[] | undefined = [...selectedUsers]; + const selectionIdx = updated.findIndex(u => u === selectedUserId); + if (selectionIdx >= 0) { + updated.splice(selectionIdx, 1); + if (updated.length === 0) { + updated = undefined; + } + } else { + updated.push(selectedUserId); + } + void router.navigate({ + search: { ...search, user: updated }, + }); + }} + className="cursor-pointer truncate" + > +
+ + {user.displayName ?? user.fullName} +
+
+ ))} +
+
+
+
+
+ ); +}; diff --git a/packages/web/app/src/components/target/proposals/util.ts b/packages/web/app/src/components/target/proposals/util.ts new file mode 100644 index 0000000000..0f1ac510af --- /dev/null +++ b/packages/web/app/src/components/target/proposals/util.ts @@ -0,0 +1,24 @@ +import { SchemaProposalStage } from '@/gql/graphql'; + +export function stageToColor(stage: SchemaProposalStage | string) { + switch (stage) { + case SchemaProposalStage.Closed: + return 'red' as const; + case SchemaProposalStage.Draft: + return 'gray' as const; + case SchemaProposalStage.Open: + return 'orange' as const; + default: + return 'green' as const; + } +} + +export function userText( + user?: { + email: string; + displayName?: string | null; + fullName?: string | null; + } | null, +) { + return user?.displayName || user?.fullName || user?.email || 'Unknown'; +} diff --git a/packages/web/app/src/components/target/proposals/version-select.tsx b/packages/web/app/src/components/target/proposals/version-select.tsx new file mode 100644 index 0000000000..1956e2587d --- /dev/null +++ b/packages/web/app/src/components/target/proposals/version-select.tsx @@ -0,0 +1,103 @@ +import { useState } from 'react'; +import { ChevronsUpDown } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Command, CommandGroup, CommandItem } from '@/components/ui/command'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { TimeAgo } from '@/components/v2'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { cn } from '@/lib/utils'; +import { useRouter, useSearch } from '@tanstack/react-router'; + +const ProposalQuery_VersionsListFragment = graphql(/* GraphQL */ ` + fragment ProposalQuery_VersionsListFragment on SchemaCheckConnection { + edges { + node { + id + createdAt + meta { + author + commit + } + } + } + } +`); + +export function VersionSelect(props: { + proposalId: string; + versions: FragmentType; +}) { + const [open, setOpen] = useState(false); + const router = useRouter(); + const search = useSearch({ strict: false }); + // @todo typing + const selectedVersionId = (search as any).version as string; + + // @todo handle pagination + const versions = + useFragment(ProposalQuery_VersionsListFragment, props.versions)?.edges?.map(e => e.node) ?? + null; + const selectedVersion = selectedVersionId + ? versions?.find(node => node.id === selectedVersionId) + : versions?.[0]; + + return ( + + + + + + + + + {versions?.map(version => ( + { + // @todo make more generic by taking in version via arg + void router.navigate({ + search: { ...search, version: selectedVersion }, + }); + setOpen(false); + }} + className="cursor-pointer truncate" + > +
+
+ {version.meta?.commit ?? version.id} +
+
+ () +
+
+ by {version.meta?.author ?? 'null'} +
+
+
+ ))} +
+
+
+
+
+ ); +} diff --git a/packages/web/app/src/components/v2/checkbox.tsx b/packages/web/app/src/components/v2/checkbox.tsx index 5e67e9218f..2934872e71 100644 --- a/packages/web/app/src/components/v2/checkbox.tsx +++ b/packages/web/app/src/components/v2/checkbox.tsx @@ -1,12 +1,16 @@ import { ReactElement } from 'react'; +import { cn } from '@/lib/utils'; import { CheckboxProps, Indicator, Root } from '@radix-ui/react-checkbox'; import { CheckIcon } from '@radix-ui/react-icons'; export const Checkbox = (props: CheckboxProps): ReactElement => { return ( diff --git a/packages/web/app/src/components/v2/tag.tsx b/packages/web/app/src/components/v2/tag.tsx index 4d0f724fb5..572c27d491 100644 --- a/packages/web/app/src/components/v2/tag.tsx +++ b/packages/web/app/src/components/v2/tag.tsx @@ -6,6 +6,7 @@ const colors = { yellow: 'bg-yellow-500/10 text-yellow-500', gray: 'bg-gray-500/10 text-gray-500', orange: 'bg-orange-500/10 text-orange-500', + red: 'bg-red-500/10 text-red-500', } as const; export function Tag({ diff --git a/packages/web/app/src/index.css b/packages/web/app/src/index.css index 48fdc54309..aa2834d0fc 100644 --- a/packages/web/app/src/index.css +++ b/packages/web/app/src/index.css @@ -198,8 +198,25 @@ input::-webkit-inner-spin-button { -webkit-appearance: none; } + + .schema-doc-row-old::before { + content: counter(olddoc); + } + .schema-doc-row-new::before { + content: counter(newdoc); + } } .hive-badge-is-changed:after { @apply absolute right-2 size-1.5 rounded-full border border-orange-600 bg-orange-400 content-['']; } + +@layer utilities { + .no-scrollbar::-webkit-scrollbar { + display: none; + } + .no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; + } +} diff --git a/packages/web/app/src/pages/target-proposal-checks.tsx b/packages/web/app/src/pages/target-proposal-checks.tsx new file mode 100644 index 0000000000..c50ce96c01 --- /dev/null +++ b/packages/web/app/src/pages/target-proposal-checks.tsx @@ -0,0 +1,134 @@ +import { CalendarIcon, CheckIcon, XIcon } from '@/components/ui/icon'; +import { TimeAgo } from '@/components/v2'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { cn } from '@/lib/utils'; +import { ComponentNoneIcon, CubeIcon } from '@radix-ui/react-icons'; +import { Link } from '@tanstack/react-router'; + +export const ProposalOverview_ChecksFragment = graphql(/* GraphQL */ ` + fragment ProposalOverview_ChecksFragment on SchemaCheckConnection { + pageInfo { + startCursor + } + edges { + cursor + node { + id + createdAt + serviceName + webUrl + hasSchemaCompositionErrors + hasUnapprovedBreakingChanges + hasSchemaChanges + meta { + commit + author + } + } + } + } +`); + +export function TargetProposalChecksPage(props: { + organizationSlug: string; + projectSlug: string; + targetSlug: string; + proposalId: string; + checks: FragmentType | null; +}) { + const checks = useFragment(ProposalOverview_ChecksFragment, props.checks); + return ( +
+ {checks?.edges?.map(({ node }, index) => { + return ( + + ); + })} +
+ ); +} + +function CheckItem(props: { + organizationSlug: string; + projectSlug: string; + targetSlug: string; + id: string; + commit?: string | null; + author?: string | null; + serviceName: string; + createdAt: string; + hasSchemaCompositionErrors: boolean; + hasUnapprovedBreakingChanges: boolean; + hasSchemaChanges: boolean; + className?: string | boolean; +}) { + return ( + +
+ +
+
+ {props.serviceName.length !== 0 && ( +
+ +
{props.serviceName}
+
+ )} +
+
{props.commit ?? props.id}
+
+ + +
+
+ {props.author ? props.author : ''} +
+ + ); +} + +function SchemaCheckIcon(props: { + hasSchemaCompositionErrors: boolean; + hasUnapprovedBreakingChanges: boolean; + hasSchemaChanges: boolean; +}) { + if (props.hasSchemaCompositionErrors || props.hasUnapprovedBreakingChanges) { + return ( +
+ ERROR +
+ ); + } + if (props.hasSchemaChanges) { + return ( +
+ PASS +
+ ); + } + return ( +
+ NO CHANGE +
+ ); +} diff --git a/packages/web/app/src/pages/target-proposal-details.tsx b/packages/web/app/src/pages/target-proposal-details.tsx new file mode 100644 index 0000000000..133307cc31 --- /dev/null +++ b/packages/web/app/src/pages/target-proposal-details.tsx @@ -0,0 +1,180 @@ +import { Fragment, ReactNode, useMemo } from 'react'; +import { ProposalOverview_ReviewsFragment } from '@/components/target/proposals'; +import { ProposalChangeDetail } from '@/components/target/proposals/change-detail'; +import { ServiceHeading } from '@/components/target/proposals/service-heading'; +import { Button } from '@/components/ui/button'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { FragmentType } from '@/gql'; +import { Change, CriticalityLevel } from '@graphql-inspector/core'; +import { ComponentNoneIcon, ExclamationTriangleIcon, InfoCircledIcon } from '@radix-ui/react-icons'; +import type { ServiceProposalDetails } from './target-proposal-types'; + +export enum MergeStatus { + CONFLICT, + IGNORED, +} + +type MappedChange = { + change: Change; + error?: Error; + mergeStatus?: MergeStatus; +}; + +export function TargetProposalDetailsPage(props: { + organizationSlug: string; + projectSlug: string; + targetSlug: string; + proposalId: string; + services: ServiceProposalDetails[]; + reviews: FragmentType; +}) { + const mappedServices = useMemo(() => { + return props.services?.map( + ({ allChanges, ignoredChanges, conflictingChanges, serviceName }) => { + const changes = allChanges.map(c => { + const conflict = conflictingChanges.find(({ change }) => c === change); + if (conflict) { + return { + change: c, + error: conflict.error, + mergeStatus: MergeStatus.CONFLICT, + }; + } + const ignored = ignoredChanges.find(({ change }) => c === change); + if (ignored) { + return null; + } + return { change: c }; + }); + + const breaking: MappedChange[] = []; + const dangerous: MappedChange[] = []; + const safe: MappedChange[] = []; + for (const change of changes) { + if (change) { + const level = change.change.criticality.level; + if (level === CriticalityLevel.Breaking) { + breaking.push(change); + } else if (level === CriticalityLevel.Dangerous) { + dangerous.push(change); + } else { + // if (level === CriticalityLevel.NonBreaking) { + safe.push(change); + } + } + } + return { + hasChanges: allChanges.length > 0, + safe, + breaking, + dangerous, + ignored: ignoredChanges.map(c => ({ ...c, mergeStatus: MergeStatus.IGNORED })), + serviceName, + }; + }, + ); + }, [props.services]); + + return ( +
+ {mappedServices?.map(({ safe, dangerous, breaking, ignored, serviceName, hasChanges }) => { + // don't print service name if service was not changed + if (!hasChanges) { + return null; + } + return ( + + +
+ + + + +
+
+ ); + })} +
+ ); +} + +function ChangeBlock(props: { + title: string; + info: string; + changes: Array<{ + change: Change; + error?: Error; + mergeStatus?: MergeStatus; + }>; +}) { + return ( + props.changes.length !== 0 && ( + <> +

+ {props.title} + {props.info && } +

+
+ {props.changes.map(({ change, error, mergeStatus }) => { + let icon: ReactNode | undefined; + if (mergeStatus === MergeStatus.CONFLICT) { + icon = ( + + + CONFLICT + + ); + } else if (mergeStatus === MergeStatus.IGNORED) { + icon = ( + + NO CHANGE + + ); + } + return ( + + ); + })} +
+ + ) + ); +} + +function ChangesBlockTooltip(props: { info: string }) { + return ( + + + + + + +

{props.info}

+
+
+
+ ); +} diff --git a/packages/web/app/src/pages/target-proposal-edit.tsx b/packages/web/app/src/pages/target-proposal-edit.tsx new file mode 100644 index 0000000000..b694385e65 --- /dev/null +++ b/packages/web/app/src/pages/target-proposal-edit.tsx @@ -0,0 +1,13 @@ +export function TargetProposalEditPage(props: { + organizationSlug: string; + projectSlug: string; + targetSlug: string; + proposalId: string; +}) { + return ( +
+ Edit is not available yet. Use @graphql-hive/cli to + propose changes. +
+ ); +} diff --git a/packages/web/app/src/pages/target-proposal-schema.tsx b/packages/web/app/src/pages/target-proposal-schema.tsx new file mode 100644 index 0000000000..8aaec6d630 --- /dev/null +++ b/packages/web/app/src/pages/target-proposal-schema.tsx @@ -0,0 +1,27 @@ +import { Fragment } from 'react'; +import { Proposal, ProposalOverview_ReviewsFragment } from '@/components/target/proposals'; +import { ServiceHeading } from '@/components/target/proposals/service-heading'; +import { FragmentType } from '@/gql'; +import { ServiceProposalDetails } from './target-proposal-types'; + +export function TargetProposalSchemaPage(props: { + organizationSlug: string; + projectSlug: string; + targetSlug: string; + proposalId: string; // @todo pass to proposal for commenting etc + services: ServiceProposalDetails[]; + reviews: FragmentType; +}) { + if (props.services.length) { + return ( +
+ {props.services.map(proposed => ( + + + + + ))} +
+ ); + } +} diff --git a/packages/web/app/src/pages/target-proposal-supergraph.tsx b/packages/web/app/src/pages/target-proposal-supergraph.tsx new file mode 100644 index 0000000000..4a593eff26 --- /dev/null +++ b/packages/web/app/src/pages/target-proposal-supergraph.tsx @@ -0,0 +1,132 @@ +import { buildSchema } from 'graphql'; +import { useQuery } from 'urql'; +import { ProposalOverview_ChangeFragment, toUpperSnakeCase } from '@/components/target/proposals'; +import { SchemaDiff } from '@/components/target/proposals/schema-diff/schema-diff'; +import { Spinner } from '@/components/ui/spinner'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { Change } from '@graphql-inspector/core'; +import { patchSchema } from '@graphql-inspector/patch'; + +// @todo compose the changes subgraphs instead of modifying the supergraph... +const ProposalSupergraphChangesQuery = graphql(/* GraphQL */ ` + query ProposalSupergraphChangesQuery($id: ID!) { + schemaProposal(input: { id: $id }) { + id + checks(after: null, input: { latestPerService: true }) { + edges { + __typename + node { + id + serviceName + hasSchemaChanges + schemaChanges { + edges { + node { + __typename + ...ProposalOverview_ChangeFragment + } + } + } + } + } + } + } + } +`); + +const ProposalSupergraphLatestQuery = graphql(/* GraphQL */ ` + query ProposalSupergraphLatestQuery($reference: TargetReferenceInput!) { + latestValidVersion(target: $reference) { + id + supergraph + } + } +`); + +export function TargetProposalSupergraphPage(props: { + organizationSlug: string; + projectSlug: string; + targetSlug: string; + proposalId: string; +}) { + const [query] = useQuery({ + query: ProposalSupergraphChangesQuery, + variables: { + id: props.proposalId, + }, + requestPolicy: 'cache-and-network', + }); + const [latestQuery] = useQuery({ + query: ProposalSupergraphLatestQuery, + variables: { + reference: { + bySelector: { + organizationSlug: props.organizationSlug, + projectSlug: props.projectSlug, + targetSlug: props.targetSlug, + }, + }, + }, + }); + + // @todo use pagination to collect all + const allChanges: (FragmentType | null | undefined)[] = + []; + query?.data?.schemaProposal?.checks?.edges?.map(({ node: { schemaChanges } }) => { + if (schemaChanges) { + const changes = schemaChanges.edges.map(edge => edge.node); + allChanges.push(...changes); + } + }); + + return ( +
+ {query.fetching || (query.fetching && )} + +
+ ); +} + +function SupergraphDiff(props: { + baseSchemaSDL: string; + changes: (FragmentType | null | undefined)[] | null; +}) { + if (props.baseSchemaSDL.length === 0) { + return null; + } + try { + const before = buildSchema(props.baseSchemaSDL, { assumeValid: true, assumeValidSDL: true }); + const changes = + props.changes + ?.map((change): Change | null => { + const c = useFragment(ProposalOverview_ChangeFragment, change); + if (c) { + return { + criticality: { + // isSafeBasedOnUsage: , + // reason: , + level: c.severityLevel as any, + }, + message: c.message, + meta: c.meta, + type: (c.meta && toUpperSnakeCase(c.meta?.__typename)) ?? '', // convert to upper snake + path: c.path?.join('.'), + }; + } + return null; + }) + .filter(c => !!c) ?? []; + const after = patchSchema(before, changes, { throwOnError: false }); + return null} />; + } catch (e: unknown) { + return ( + <> +
Invalid SDL
+
{e instanceof Error ? e.message : String(e)}
+ + ); + } +} diff --git a/packages/web/app/src/pages/target-proposal-types.ts b/packages/web/app/src/pages/target-proposal-types.ts new file mode 100644 index 0000000000..e39e548531 --- /dev/null +++ b/packages/web/app/src/pages/target-proposal-types.ts @@ -0,0 +1,21 @@ +import type { GraphQLSchema } from 'graphql'; +import { ProposalOverview_ChangeFragment } from '@/components/target/proposals'; +import { FragmentType } from '@/gql'; +import type { Change } from '@graphql-inspector/core'; + +export type ServiceProposalDetails = { + beforeSchema: GraphQLSchema | null; + afterSchema: GraphQLSchema | null; + allChanges: Change[]; + // Required because the component ChangesBlock uses this fragment. + rawChanges: FragmentType[]; + ignoredChanges: Array<{ + change: Change; + error: Error; + }>; + conflictingChanges: Array<{ + change: Change; + error: Error; + }>; + serviceName: string; +}; diff --git a/packages/web/app/src/pages/target-proposal.tsx b/packages/web/app/src/pages/target-proposal.tsx new file mode 100644 index 0000000000..2dddb325bc --- /dev/null +++ b/packages/web/app/src/pages/target-proposal.tsx @@ -0,0 +1,461 @@ +import { useMemo } from 'react'; +import { buildSchema } from 'graphql'; +import { useMutation, useQuery } from 'urql'; +import { Page, TargetLayout } from '@/components/layouts/target'; +import { + ProposalOverview_ChangeFragment, + ProposalOverview_ReviewsFragment, + toUpperSnakeCase, +} from '@/components/target/proposals'; +import { StageTransitionSelect } from '@/components/target/proposals/stage-transition-select'; +import { VersionSelect } from '@/components/target/proposals/version-select'; +import { CardDescription } from '@/components/ui/card'; +import { DiffIcon, EditIcon, GraphQLIcon } from '@/components/ui/icon'; +import { Meta } from '@/components/ui/meta'; +import { Subtitle, Title } from '@/components/ui/page'; +import { SubPageLayoutHeader } from '@/components/ui/page-content-layout'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Spinner } from '@/components/ui/spinner'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { TimeAgo } from '@/components/v2'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { Change } from '@graphql-inspector/core'; +import { patchSchema } from '@graphql-inspector/patch'; +import { NoopError } from '@graphql-inspector/patch/errors'; +import { ListBulletIcon, PieChartIcon } from '@radix-ui/react-icons'; +import { Link } from '@tanstack/react-router'; +import { + ProposalOverview_ChecksFragment, + TargetProposalChecksPage, +} from './target-proposal-checks'; +import { TargetProposalDetailsPage } from './target-proposal-details'; +import { TargetProposalEditPage } from './target-proposal-edit'; +import { TargetProposalSchemaPage } from './target-proposal-schema'; +import { TargetProposalSupergraphPage } from './target-proposal-supergraph'; +import { ServiceProposalDetails } from './target-proposal-types'; + +enum Tab { + SCHEMA = 'schema', + SUPERGRAPH = 'supergraph', + DETAILS = 'details', + CHECKS = 'checks', + EDIT = 'edit', +} + +const ProposalQuery = graphql(/* GraphQL */ ` + query ProposalQuery($proposalId: ID!, $latestValidVersionReference: TargetReferenceInput) { + schemaProposal(input: { id: $proposalId }) { + id + createdAt + stage + title + description + checks(after: null, input: {}) { + ...ProposalQuery_VersionsListFragment + ...ProposalOverview_ChecksFragment + } + reviews { + ...ProposalOverview_ReviewsFragment + } + author + } + latestValidVersion(target: $latestValidVersionReference) { + id + # sdl + schemas { + edges { + node { + ... on CompositeSchema { + id + source + service + } + ... on SingleSchema { + id + source + } + } + } + } + } + } +`); + +const ProposalChangesQuery = graphql(/* GraphQL */ ` + query ProposalChangesQuery($id: ID!) { + schemaProposal(input: { id: $id }) { + id + checks(after: null, input: { latestPerService: true }) { + edges { + __typename + node { + id + serviceName + hasSchemaChanges + schemaChanges { + edges { + node { + __typename + ...ProposalOverview_ChangeFragment + } + } + } + } + } + } + } + } +`); + +const ReviewSchemaProposalMutation = graphql(/* GraphQL */ ` + mutation ReviewSchemaProposalMutation($input: ReviewSchemaProposalInput!) { + reviewSchemaProposal(input: $input) { + ok { + __typename + } + error { + message + } + } + } +`); + +export function TargetProposalsSinglePage(props: { + organizationSlug: string; + projectSlug: string; + targetSlug: string; + proposalId: string; + tab?: string; +}) { + return ( + <> + + + + + + ); +} + +const ProposalsContent = (props: Parameters[0]) => { + // fetch main page details + const [query, refreshProposal] = useQuery({ + query: ProposalQuery, + variables: { + latestValidVersionReference: { + bySelector: { + organizationSlug: props.organizationSlug, + projectSlug: props.projectSlug, + targetSlug: props.targetSlug, + }, + }, + proposalId: props.proposalId, + }, + requestPolicy: 'cache-and-network', + }); + + // fetch all proposed changes for the selected version + const [changesQuery] = useQuery({ + query: ProposalChangesQuery, + variables: { + id: props.proposalId, + // @todo versionId + // @todo deal with pagination + }, + requestPolicy: 'cache-and-network', + }); + + const [_, reviewSchemaProposal] = useMutation(ReviewSchemaProposalMutation); + + // This does a lot of heavy lifting to avoid having to reapply patching etc on each tab... + // Takes all the data provided by the queries to apply the patch to the schema and + // categorize changes. + const services = useMemo(() => { + return ( + changesQuery.data?.schemaProposal?.checks?.edges?.map( + ({ node: proposalVersion }): ServiceProposalDetails => { + const existingSchema = query.data?.latestValidVersion?.schemas.edges.find( + ({ node: latestSchema }) => + (latestSchema.__typename === 'CompositeSchema' && + latestSchema.service === proposalVersion.serviceName) || + (latestSchema.__typename === 'SingleSchema' && proposalVersion.serviceName == null), + )?.node.source; + const beforeSchema = existingSchema?.length + ? buildSchema(existingSchema, { assumeValid: true, assumeValidSDL: true }) + : null; + // @todo better handle pagination + const allChanges = + proposalVersion.schemaChanges?.edges + .filter(c => !!c) + ?.map(({ node: change }): Change => { + const c = useFragment(ProposalOverview_ChangeFragment, change); + return { + criticality: { + // isSafeBasedOnUsage: , + // reason: , + level: c.severityLevel as any, + }, + message: c.message, + meta: c.meta, + type: (c.meta && toUpperSnakeCase(c.meta?.__typename)) ?? '', // convert to upper snake + path: c.path?.join('.'), + }; + }) ?? []; + const conflictingChanges: Array<{ change: Change; error: Error }> = []; + const ignoredChanges: Array<{ change: Change; error: Error }> = []; + const afterSchema = beforeSchema + ? patchSchema(beforeSchema, allChanges, { + throwOnError: false, + onError(error, change) { + if (error instanceof NoopError) { + ignoredChanges.push({ change, error }); + return false; + } + conflictingChanges.push({ change, error }); + return true; + }, + }) + : null; + + return { + beforeSchema, + afterSchema, + allChanges, + rawChanges: proposalVersion.schemaChanges?.edges.map(({ node }) => node) ?? [], + conflictingChanges, + ignoredChanges, + serviceName: proposalVersion.serviceName ?? '', + }; + }, + ) ?? [] + ); + }, [ + // @todo handle pagination + changesQuery.data?.schemaProposal?.checks?.edges, + query.data?.latestValidVersion?.schemas.edges, + ]); + + const proposal = query.data?.schemaProposal; + return ( + <> +
+
+ + + Schema Proposals + {' '} + /{' '} + {/* @todo use query data to show loading */} + {props.proposalId ? ( + `${props.proposalId}` + ) : ( + + )} + + } + description={ + + Collaborate on schema changes to reduce friction during development. + + } + /> +
+
+
+ {query.fetching ? ( + + ) : ( + proposal && ( + <> +
+ +
+ { + const review = await reviewSchemaProposal({ + input: { + schemaProposalId: props.proposalId, + stageTransition: stage, + // for monorepos and non-service related comments, use an empty string + serviceName: '', + }, + }); + refreshProposal(); + }} + /> +
+
+
+ {proposal.title} +
+ proposed by {proposal.author} +
+
{proposal.description}
+
+ + ) + )} + {changesQuery.fetching ? ( + + ) : !services.length ? ( + <> + No changes found + + This proposed version would result in no changes to the latest schemas. + + + ) : ( + + )} +
+ + ); +}; + +function TabbedContent(props: { + organizationSlug: string; + projectSlug: string; + targetSlug: string; + proposalId: string; + page?: string; + services: ServiceProposalDetails[]; + reviews: FragmentType; + checks: FragmentType | null; +}) { + return ( + + + + + + Details + + + + + + Schema + + + + + + Supergraph Preview + + + + + + Checks + + + + + + Edit + + + + +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+ ); +} + +export const ProposalTab = Tab; diff --git a/packages/web/app/src/pages/target-proposals.tsx b/packages/web/app/src/pages/target-proposals.tsx new file mode 100644 index 0000000000..9bebd9d973 --- /dev/null +++ b/packages/web/app/src/pages/target-proposals.tsx @@ -0,0 +1,249 @@ +import { useState } from 'react'; +import { useQuery } from 'urql'; +import { Page, TargetLayout } from '@/components/layouts/target'; +import { StageFilter } from '@/components/target/proposals/stage-filter'; +import { UserFilter } from '@/components/target/proposals/user-filter'; +import { stageToColor } from '@/components/target/proposals/util'; +import { BadgeRounded } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { CardDescription } from '@/components/ui/card'; +import { Link } from '@/components/ui/link'; +import { Meta } from '@/components/ui/meta'; +import { Subtitle, Title } from '@/components/ui/page'; +import { SubPageLayoutHeader } from '@/components/ui/page-content-layout'; +import { Spinner } from '@/components/ui/spinner'; +import { TimeAgo } from '@/components/v2'; +import { graphql } from '@/gql'; +import { SchemaProposalStage } from '@/gql/graphql'; +import { cn } from '@/lib/utils'; +import { ChatBubbleIcon } from '@radix-ui/react-icons'; +import { useRouter, useSearch } from '@tanstack/react-router'; + +export function TargetProposalsPage(props: { + organizationSlug: string; + projectSlug: string; + targetSlug: string; + filterUserIds?: string[]; + filterStages?: string[]; + selectedProposalId?: string; +}) { + return ( + <> + + + + + + ); +} + +const ProposalsContent = (props: Parameters[0]) => { + return ( + <> +
+
+ Schema Proposals} + description={ + <> + + Collaborate on schema changes to reduce friction during development. + + + } + /> +
+
+ +
+
+ + + ); +}; + +const ProposalsQuery = graphql(` + query listProposals($input: SchemaProposalsInput!) { + schemaProposals(input: $input) { + edges { + node { + id + title + stage + updatedAt + author + commentsCount + } + cursor + } + pageInfo { + endCursor + hasNextPage + } + } + } +`); + +function TargetProposalsList(props: Parameters[0]) { + const [pageVariables, setPageVariables] = useState([{ first: 20, after: null as string | null }]); + const router = useRouter(); + const reset = () => { + void router.navigate({ + search: { stage: undefined, user: undefined }, + }); + }; + const hasFilterSelection = !!(props.filterStages?.length || props.filterUserIds?.length); + + return ( + <> +
+ + + {hasFilterSelection ? ( + + ) : null} +
+ +
+ {pageVariables.map(({ after }, i) => ( + { + setPageVariables([...pageVariables, { after, first: 10 }]); + }} + /> + ))} +
+ + ); +} + +/** + * This renders a single page of proposals for the ProposalList component. + */ +const ProposalsListPage = (props: { + organizationSlug: string; + projectSlug: string; + targetSlug: string; + filterUserIds?: string[]; + filterStages?: string[]; + selectedProposalId?: string; + isLastPage: boolean; + onLoadMore: (after: string) => void | Promise; +}) => { + const [query] = useQuery({ + query: ProposalsQuery, + variables: { + input: { + target: { + bySelector: { + organizationSlug: props.organizationSlug, + projectSlug: props.projectSlug, + targetSlug: props.targetSlug, + }, + }, + stages: ( + props.filterStages ?? [ + SchemaProposalStage.Draft, + SchemaProposalStage.Open, + SchemaProposalStage.Approved, + ] + ) + .sort() + .map(s => s.toUpperCase() as SchemaProposalStage), + userIds: props.filterUserIds, + }, + }, + requestPolicy: 'cache-and-network', + }); + const pageInfo = query.data?.schemaProposals?.pageInfo; + const search = useSearch({ strict: false }); + const hasFilter = props.filterStages?.length || props.filterUserIds?.length; + + return ( + <> + {query.fetching ? : null} + {query.data?.schemaProposals?.edges?.length === 0 && ( +
+ + No proposals {hasFilter ? 'match your search criteria' : 'have been created yet'} + + To get started, use the Hive CLI to propose a schema change. +
+ )} + {query.data?.schemaProposals?.edges?.map(({ node: proposal }) => { + return ( +
+ +
+
+
+ + {proposal.title} + + + + + {proposal.stage} +
+
+
+ proposed +
+ {proposal.author ?
by {proposal.author}
: null} +
+
+
+ {proposal.commentsCount} + +
+
+ +
+ ); + })} + {props.isLastPage && pageInfo?.hasNextPage ? ( + + ) : null} + + ); +}; diff --git a/packages/web/app/src/router.tsx b/packages/web/app/src/router.tsx index cd018fb864..9378c8ea64 100644 --- a/packages/web/app/src/router.tsx +++ b/packages/web/app/src/router.tsx @@ -20,12 +20,14 @@ import { Navigate, Outlet, useNavigate, + useParams, } from '@tanstack/react-router'; import { ErrorComponent } from './components/error'; import { NotFound } from './components/not-found'; import 'react-toastify/dist/ReactToastify.css'; import { zodValidator } from '@tanstack/zod-adapter'; import { authenticated } from './components/authenticated-container'; +import { SchemaProposalStage } from './gql/graphql'; import { AuthPage } from './pages/auth'; import { AuthCallbackPage } from './pages/auth-callback'; import { AuthOIDCPage } from './pages/auth-oidc'; @@ -72,6 +74,8 @@ import { TargetInsightsClientPage } from './pages/target-insights-client'; import { TargetInsightsCoordinatePage } from './pages/target-insights-coordinate'; import { TargetInsightsOperationPage } from './pages/target-insights-operation'; import { TargetLaboratoryPage } from './pages/target-laboratory'; +import { ProposalTab, TargetProposalsSinglePage } from './pages/target-proposal'; +import { TargetProposalsPage } from './pages/target-proposals'; import { TargetSettingsPage, TargetSettingsPageEnum } from './pages/target-settings'; SuperTokens.init(frontendConfig()); @@ -820,6 +824,63 @@ const targetChecksSingleRoute = createRoute({ }, }); +const targetProposalsRoute = createRoute({ + getParentRoute: () => targetRoute, + path: 'proposals', + validateSearch: z.object({ + stage: z + .enum(Object.values(SchemaProposalStage).map(s => s.toLowerCase()) as [string, ...string[]]) + .array() + .optional() + .catch(() => void 0), + user: z.string().array().optional(), + }), + component: function TargetProposalsRoute() { + const { organizationSlug, projectSlug, targetSlug } = targetProposalsRoute.useParams(); + // select proposalId from child route + const proposalId = useParams({ + strict: false, + select: p => p.proposalId, + }); + const { stage, user } = targetProposalsRoute.useSearch(); + return ( + + ); + }, +}); + +const targetProposalsSingleRoute = createRoute({ + getParentRoute: () => targetRoute, + path: 'proposals/$proposalId', + validateSearch: z.object({ + page: z + .enum(Object.values(ProposalTab).map(s => s.toLowerCase()) as [string, ...string[]]) + .optional() + .catch(() => void 0), + }), + component: function TargetProposalRoute() { + const { organizationSlug, projectSlug, targetSlug, proposalId } = + targetProposalsSingleRoute.useParams(); + const { page } = targetProposalsSingleRoute.useSearch(); + return ( + + ); + }, +}); + const routeTree = root.addChildren([ notFoundRoute, anonymousRoute.addChildren([ @@ -875,6 +936,7 @@ const routeTree = root.addChildren([ targetChecksRoute.addChildren([targetChecksSingleRoute]), targetAppVersionRoute, targetAppsRoute, + targetProposalsRoute.addChildren([targetProposalsSingleRoute]), ]), ]), ]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c74af3dddf..d223696385 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,7 +99,7 @@ importers: version: 5.0.3(graphql@16.9.0) '@graphql-codegen/cli': specifier: 5.0.5 - version: 5.0.5(@babel/core@7.22.9)(@parcel/watcher@2.5.0)(@types/node@22.10.5)(encoding@0.1.13)(enquirer@2.4.1)(graphql@16.9.0)(typescript@5.7.3) + version: 5.0.5(@babel/core@7.26.0)(@parcel/watcher@2.5.0)(@types/node@22.10.5)(encoding@0.1.13)(enquirer@2.4.1)(graphql@16.9.0)(typescript@5.7.3) '@graphql-codegen/client-preset': specifier: 4.7.0 version: 4.7.0(encoding@0.1.13)(graphql@16.9.0) @@ -117,10 +117,16 @@ importers: version: 3.0.0(graphql@16.9.0) '@graphql-eslint/eslint-plugin': specifier: 3.20.1 - version: 3.20.1(patch_hash=695fba67df25ba9d46472c8398c94c6a2ccf75d902321d8f95150f68e940313e)(@babel/core@7.22.9)(@types/node@22.10.5)(encoding@0.1.13)(graphql@16.9.0) + version: 3.20.1(patch_hash=695fba67df25ba9d46472c8398c94c6a2ccf75d902321d8f95150f68e940313e)(@babel/core@7.26.0)(@types/node@22.10.5)(encoding@0.1.13)(graphql@16.9.0) '@graphql-inspector/cli': - specifier: 4.0.3 - version: 4.0.3(@types/node@22.10.5)(encoding@0.1.13)(graphql@16.9.0) + specifier: link:../graphql-inspector/packages/cli + version: link:../graphql-inspector/packages/cli + '@graphql-inspector/core': + specifier: file:../graphql-inspector/packages/core + version: file:../graphql-inspector/packages/core(graphql@16.9.0) + '@graphql-inspector/patch': + specifier: file:../graphql-inspector/packages/patch + version: file:../graphql-inspector/packages/patch(graphql@16.9.0) '@manypkg/get-packages': specifier: 2.2.2 version: 2.2.2 @@ -213,10 +219,10 @@ importers: version: 5.7.3 vite-tsconfig-paths: specifier: 5.1.4 - version: 5.1.4(typescript@5.7.3)(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)) + version: 5.1.4(typescript@5.7.3)(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)) vitest: specifier: 3.1.1 - version: 3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + version: 3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) deployment: dependencies: @@ -373,7 +379,7 @@ importers: version: 2.8.1 vitest: specifier: 3.1.1 - version: 3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + version: 3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) zod: specifier: 3.24.1 version: 3.24.1 @@ -407,7 +413,7 @@ importers: version: 14.0.0 vitest: specifier: 3.1.1 - version: 3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + version: 3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) ws: specifier: '>=8.18.0 || >=7.5.10 || >=6.2.3 || >=5.2.4' version: 8.18.0 @@ -419,8 +425,8 @@ importers: specifier: workspace:* version: link:../core/dist '@graphql-inspector/core': - specifier: 5.1.0-alpha-20231208113249-34700c8a - version: 5.1.0-alpha-20231208113249-34700c8a(graphql@16.9.0) + specifier: file:../../../../graphql-inspector/packages/core + version: file:../graphql-inspector/packages/core(graphql@16.9.0) '@graphql-tools/code-file-loader': specifier: ~8.1.0 version: 8.1.0(graphql@16.9.0) @@ -530,7 +536,7 @@ importers: version: 2.8.1 vitest: specifier: 3.1.1 - version: 3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + version: 3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) publishDirectory: dist packages/libraries/envelop: @@ -606,7 +612,7 @@ importers: version: 14.0.0 vitest: specifier: 3.1.1 - version: 3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + version: 3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) ws: specifier: '>=8.18.0 || >=7.5.10 || >=6.2.3 || >=5.2.4' version: 8.18.0 @@ -697,8 +703,8 @@ importers: specifier: 1.0.0 version: 1.0.0 '@graphql-inspector/core': - specifier: 5.1.0-alpha-20231208113249-34700c8a - version: 5.1.0-alpha-20231208113249-34700c8a(graphql@16.9.0) + specifier: file:../../../../graphql-inspector/packages/core + version: file:../graphql-inspector/packages/core(graphql@16.9.0) '@graphql-tools/merge': specifier: 9.0.24 version: 9.0.24(graphql@16.9.0) @@ -863,7 +869,7 @@ importers: version: 6.21.2 vitest: specifier: 3.1.1 - version: 3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + version: 3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) zod: specifier: 3.24.1 version: 3.24.1 @@ -896,7 +902,7 @@ importers: version: 6.21.2 vitest: specifier: 3.1.1 - version: 3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + version: 3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) workers-loki-logger: specifier: 0.1.15 version: 0.1.15 @@ -1401,8 +1407,8 @@ importers: packages/services/storage: devDependencies: '@graphql-inspector/core': - specifier: 5.1.0-alpha-20231208113249-34700c8a - version: 5.1.0-alpha-20231208113249-34700c8a(graphql@17.0.0-alpha.7) + specifier: file:../../../../graphql-inspector/packages/core + version: file:../graphql-inspector/packages/core(graphql@16.9.0) '@hive/service-common': specifier: workspace:* version: link:../service-common @@ -1669,7 +1675,7 @@ importers: version: 7.0.4 '@fastify/vite': specifier: 6.0.7 - version: 6.0.7(patch_hash=f5ce073a4db250ed3db1d9c19e2a253418454ee379530bdee25869570d7b500b)(@types/node@22.10.5)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0) + version: 6.0.7(patch_hash=f5ce073a4db250ed3db1d9c19e2a253418454ee379530bdee25869570d7b500b)(@types/node@22.10.5)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0) '@graphiql/plugin-explorer': specifier: 4.0.0-alpha.2 version: 4.0.0-alpha.2(@graphiql/react@1.0.0-alpha.4(patch_hash=1018befc9149cbc43bc2bf8982d52090a580e68df34b46674234f4e58eb6d0a0)(@codemirror/language@6.10.2)(@types/node@22.10.5)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.1(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1682,6 +1688,12 @@ importers: '@graphql-codegen/client-preset-swc-plugin': specifier: 0.2.0 version: 0.2.0 + '@graphql-inspector/core': + specifier: file:../../../../graphql-inspector/packages/core + version: file:../graphql-inspector/packages/core(graphql@16.9.0) + '@graphql-inspector/patch': + specifier: file:../../../../graphql-inspector/packages/patch + version: file:../graphql-inspector/packages/patch(graphql@16.9.0) '@graphql-tools/mock': specifier: 9.0.22 version: 9.0.22(graphql@16.9.0) @@ -1798,7 +1810,7 @@ importers: version: 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(prettier@3.4.2))(typescript@5.7.3) '@storybook/react-vite': specifier: 8.4.7 - version: 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(prettier@3.4.2))(typescript@5.7.3)(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)) + version: 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(prettier@3.4.2))(typescript@5.7.3)(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)) '@stripe/react-stripe-js': specifier: 3.1.1 version: 3.1.1(@stripe/stripe-js@5.5.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1864,7 +1876,7 @@ importers: version: 7.1.0(@urql/core@5.0.3(graphql@16.9.0))(graphql@16.9.0) '@vitejs/plugin-react': specifier: 4.3.4 - version: 4.3.4(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)) + version: 4.3.4(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)) autoprefixer: specifier: 10.4.20 version: 10.4.20(postcss@8.4.49) @@ -2023,10 +2035,10 @@ importers: version: 1.13.2(@types/react@18.3.18)(react@18.3.1) vite: specifier: 6.3.4 - version: 6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + version: 6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) vite-tsconfig-paths: specifier: 5.1.4 - version: 5.1.4(typescript@5.7.3)(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)) + version: 5.1.4(typescript@5.7.3)(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)) wonka: specifier: 6.3.4 version: 6.3.4 @@ -2053,13 +2065,13 @@ importers: version: 1.1.6(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@theguild/components': specifier: 9.7.1 - version: 9.7.1(@theguild/tailwind-config@0.6.3(postcss-import@16.1.0(postcss@8.4.49))(postcss-lightningcss@1.0.1(postcss@8.4.49))(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@22.10.5)(typescript@5.7.3))))(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(immer@10.1.1)(next@15.2.4(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3)(use-sync-external-store@1.5.0(react@19.0.0)) + version: 9.7.1(@theguild/tailwind-config@0.6.3(postcss-import@16.1.0(postcss@8.4.49))(postcss-lightningcss@1.0.1(postcss@8.4.49))(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@22.10.5)(typescript@5.7.3))))(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(immer@10.1.1)(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3)(use-sync-external-store@1.5.0(react@19.0.0)) date-fns: specifier: 4.1.0 version: 4.1.0 next: specifier: 15.2.4 - version: 15.2.4(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: specifier: 19.0.0 version: 19.0.0 @@ -2099,7 +2111,7 @@ importers: version: 0.0.32 next-sitemap: specifier: 4.2.3 - version: 4.2.3(next@15.2.4(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) + version: 4.2.3(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) pagefind: specifier: ^1.2.0 version: 1.3.0 @@ -2668,10 +2680,6 @@ packages: resolution: {integrity: sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==} engines: {node: '>=6.9.0'} - '@babel/core@7.22.9': - resolution: {integrity: sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==} - engines: {node: '>=6.9.0'} - '@babel/core@7.26.0': resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} engines: {node: '>=6.9.0'} @@ -3103,11 +3111,11 @@ packages: '@codemirror/language@6.10.2': resolution: {integrity: sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==} - '@codemirror/state@6.5.0': - resolution: {integrity: sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==} + '@codemirror/state@6.5.2': + resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} - '@codemirror/view@6.36.1': - resolution: {integrity: sha512-miD1nyT4m4uopZaDdO2uXU/LLHliKNYL9kB1C1wJHrunHLm/rpkb5QVSokqgw9hFqEZakrdlb/VGWX8aYZTslQ==} + '@codemirror/view@6.37.2': + resolution: {integrity: sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw==} '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} @@ -3212,10 +3220,6 @@ packages: '@emotion/weak-memoize@0.3.0': resolution: {integrity: sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==} - '@envelop/core@4.0.3': - resolution: {integrity: sha512-O0Vz8E0TObT6ijAob8jYFVJavcGywKThM3UAsxUIBBVPYZTMiqI9lo2gmAnbMUnrDcAYkUTZEW9FDYPRdF5l6g==} - engines: {node: '>=16.0.0'} - '@envelop/core@5.0.2': resolution: {integrity: sha512-tVL6OrMe6UjqLosiE+EH9uxh2TQC0469GwF4tE014ugRaDDKKVWwFwZe0TBMlcyHKh5MD4ZxktWo/1hqUxIuhw==} engines: {node: '>=18.0.0'} @@ -3279,10 +3283,6 @@ packages: '@sentry/node': ^6 || ^7 graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - '@envelop/types@4.0.1': - resolution: {integrity: sha512-ULo27/doEsP7uUhm2iTnElx13qTO6I5FKvmLoX41cpfuw8x6e0NUFknoqhEsLzAbgz8xVS5mjwcxGCXh4lDYzg==} - engines: {node: '>=16.0.0'} - '@envelop/types@5.0.0': resolution: {integrity: sha512-IPjmgSc4KpQRlO4qbEDnBEixvtb06WDmjKfi/7fkZaryh5HuOmTtixe1EupQI5XfXO8joc3d27uUZ0QdC++euA==} engines: {node: '>=18.0.0'} @@ -3563,6 +3563,7 @@ packages: '@fastify/vite@6.0.7': resolution: {integrity: sha512-+dRo9KUkvmbqdmBskG02SwigWl06Mwkw8SBDK1zTNH6vd4DyXbRvI7RmJEmBkLouSU81KTzy1+OzwHSffqSD6w==} + bundledDependencies: [] '@floating-ui/core@1.2.6': resolution: {integrity: sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==} @@ -3731,132 +3732,15 @@ packages: resolution: {integrity: sha512-RiwLMc89lTjvyLEivZ/qxAC5nBHoS2CtsWFSOsN35sxG9zoo5Z+JsFHM8MlvmO9yt+MJNIyC5MLE1rsbOphlag==} engines: {node: '>=18.0.0'} - '@graphql-inspector/audit-command@4.0.3': - resolution: {integrity: sha512-cm4EtieIp9PUSDBze+Sn5HHF80jDF9V7sYyXqFa7+Vtw4Jlet98Ig48dFVtoLuFCPtCv2eZ22I8JOkBKL5WgVA==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-inspector/cli@4.0.3': - resolution: {integrity: sha512-54pJ/SkFGz/qKEP2sjiCqxTG6QkWSzG4NdfKqhlinQstlyCgtbmgwgh90fUPoG6yOgZZhcsiSJXHkz255kRu4w==} - engines: {node: '>=16.0.0'} - hasBin: true - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-inspector/code-loader@4.0.2': - resolution: {integrity: sha512-WNMorwDSknFFnv2GF3FvjuxQy1VMxIVMsTgL3E6amhzSPvKd3SPplw9rpqhQJX9jKXHvu70PgE2FvbPCTapE5A==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-inspector/commands@4.0.3': - resolution: {integrity: sha512-68RO/nvt/9cKNmBpMCdExp1PkIeEdUpmv5WtcJwrIneTF/A9Cg45z2oDGiYr0CZgknprFgYQNVNFbYJV9AIamg==} - engines: {node: '>=16.0.0'} - peerDependencies: - '@graphql-inspector/config': 4.0.2 - '@graphql-inspector/loaders': 4.0.3 - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - yargs: 17.7.2 - - '@graphql-inspector/config@4.0.2': - resolution: {integrity: sha512-fnIwVpGM5AtTr4XyV8NJkDnwpXxZSBzi3BopjuXwBPXXD1F3tcVkCKNT6/5WgUQGfNPskBVbitcOPtM4hIYAOQ==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-inspector/core@5.0.2': - resolution: {integrity: sha512-pXHPCggwLmgi5NACPPV4qyf2xW/sQONnu6ZqCAid3k/S2APmVYN4Z3OvxvLA12NFhzby5Sz5K4fRsId43cK8ww==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-inspector/core@5.1.0-alpha-20231208113249-34700c8a': - resolution: {integrity: sha512-vzJEhQsZz+suo8T32o9dJNOa42IcLHfvmBm3EqRuMKQ0PU8KintUdRG1kFd6NFvNnpPHOVWFc+PYgFKCm7mPoA==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-inspector/coverage-command@5.0.3': - resolution: {integrity: sha512-LeAsn9+LjyxCzRnDvcfnQT6I0cI8UWnjPIxDkHNlkJLB0YWUTD1Z73fpRdw+l2kbYgeoMLFOK8TmilJjFN1+qQ==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-inspector/diff-command@4.0.3': - resolution: {integrity: sha512-9yDP4brhY44sgRMsy0hUJxoBLvem4J74XbQEU+87eecKDZ6SttiiadnQ6935SaQF/DOx0b6P1gH2phknDL/EDw==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-inspector/docs-command@4.0.3': - resolution: {integrity: sha512-69DPm0JpQPUfm1myNV6WqTuT9toG1fEwsheYCJf2Wn/P36HQ+neYTWbsXJF7EwSPzUsT5JWVxMyGg3RfX7IkOw==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-inspector/git-loader@4.0.2': - resolution: {integrity: sha512-RLDj4xZWpYt3gNgtu4hiAOTpabmBwcePLSs7Lem558ma7+Ud1tjRhKhVfY2/wIFGNgD7pTzVHvyB0Yc7bD6Wpg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-inspector/github-loader@4.0.2': - resolution: {integrity: sha512-SrOUjz1RppK5Vxj8fMoCN+i+yvzIj1ZnP+mNdLZIsms9ou6u0+h8NuE/KfANR5Z1VJIQGIifMONIv2eTx4SjCQ==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-inspector/graphql-loader@4.0.2': - resolution: {integrity: sha512-cYBLE5LNQbnK3jOmi49IaaEcBjfLIYyGOY89ZH0O4kgOmKJ5WgbQCLK5xL6vCl2IXkCQy52EmN8KlVr1gVOnmw==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-inspector/introspect-command@4.0.3': - resolution: {integrity: sha512-43yJvNxGOTzoqZPGAeocC4qBSYAQ09f1QX8bzkinzWvGS8WViWGlYwN6Lxup41GOUNscl7EPp1UMl+IRU5Yu3Q==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-inspector/json-loader@4.0.2': - resolution: {integrity: sha512-cbSs/5Mzw0ltm9/SkDHKpq2u1yqEwxAuMLBs7uG+lqaFGgkIuzN9U6i4LRRYgBJcfn/mScodlvo0W0IG83Wn3g==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-inspector/loaders@4.0.3': - resolution: {integrity: sha512-V9T+IUJgmneWx1kEaSbFHLcjMs929TKLs6m3p544/CMPdFIsRYmYhd2jfUBhzXC8P6avnsLxxzVEOR45wJgOYA==} - engines: {node: '>=16.0.0'} - peerDependencies: - '@graphql-inspector/config': 4.0.2 - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-inspector/logger@4.0.2': - resolution: {integrity: sha512-2VLU90HjQyhwFgiU5uBs1+sBjs71Q42MWBQnkc3GSAaVCE+bzYRurO4bRS/z7UXl0hJsJda41z5QPOFeyeMMwQ==} - engines: {node: '>=16.0.0'} - - '@graphql-inspector/serve-command@4.0.3': - resolution: {integrity: sha512-+8vovcRBFjrzZ+E5QTsG8GXSHm5q2czuqHosAU8bx6tfv2NM3FoSHUkctZulubA0+rodt2xrsD1L3R0ZCR8TBQ==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-inspector/similar-command@4.0.3': - resolution: {integrity: sha512-qWJ+E0UFzba05ymwxu/M9Yb3J4GBj97T/NNEAaflgFaVjsiYuZEywfYekKMt1kALQOP8pufH0yDOYBHObKO9sQ==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-inspector/url-loader@4.0.2': - resolution: {integrity: sha512-bor9yZ6PIW8Fc5swo8t5TeexiQS8nRySF3oF4iG8d/aYPh0e7/DYM3lGF4M7VY3lH760r3caIRXXNOnn79tBrw==} - engines: {node: '>=16.0.0'} + '@graphql-inspector/core@file:../graphql-inspector/packages/core': + resolution: {directory: ../graphql-inspector/packages/core, type: directory} + engines: {node: '>=18.0.0'} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - '@graphql-inspector/validate-command@4.0.3': - resolution: {integrity: sha512-wOcnkpU9zA30y1ggnJH1IvRDlEM24LP1J8/3tQx8rVn4zfBQpIVb7s31F/N++mZBAWXPTp2+t0JiUNbHR82odA==} - engines: {node: '>=16.0.0'} + '@graphql-inspector/patch@file:../graphql-inspector/packages/patch': + resolution: {directory: ../graphql-inspector/packages/patch, type: directory} + engines: {node: '>=18.0.0'} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 @@ -3900,12 +3784,6 @@ packages: peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@graphql-tools/code-file-loader@8.0.1': - resolution: {integrity: sha512-pmg81lsIXGW3uW+nFSCIG0lFQIxWVbgDjeBkSWlnP8CZsrHTQEkB53DT7t4BHLryoxDS4G4cPxM52yNINDSL8w==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@graphql-tools/code-file-loader@8.1.0': resolution: {integrity: sha512-HKWW/B2z15ves8N9+xnVbGmFEVGyHEK80a4ghrjeTa6nwNZaKDVfq5CoYFfF0xpfjtH6gOVUExo2XCOEz4B8mQ==} engines: {node: '>=16.0.0'} @@ -4139,12 +4017,6 @@ packages: peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@graphql-tools/utils@10.0.3': - resolution: {integrity: sha512-6uO41urAEIs4sXQT2+CYGsUTkHkVo/2MpM/QjoHj6D6xoEF2woXHBpdAVi0HKIInDwZqWgEYOwIFez0pERxa1Q==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@graphql-tools/utils@10.5.6': resolution: {integrity: sha512-JAC44rhbLzXUHiltceyEpWkxmX4e45Dfg19wRFoA9EbDxQVbOzVNF76eEECdg0J1owFsJwfLqCwz7/6xzrovOw==} engines: {node: '>=16.0.0'} @@ -4190,10 +4062,6 @@ packages: peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@graphql-yoga/logger@1.0.0': - resolution: {integrity: sha512-JYoxwnPggH2BfO+dWlWZkDeFhyFZqaTRGLvFhy+Pjp2UxitEW6nDrw+pEDw/K9tJwMjIFMmTT9VfTqrnESmBHg==} - engines: {node: '>=16.0.0'} - '@graphql-yoga/logger@2.0.1': resolution: {integrity: sha512-Nv0BoDGLMg9QBKy9cIswQ3/6aKaKjlTh87x3GiBg2Z4RrjyrM48DvOOK0pJh1C1At+b0mUIM67cwZcFTDLN4sA==} engines: {node: '>=18.0.0'} @@ -4247,18 +4115,10 @@ packages: peerDependencies: ioredis: ^5.0.6 - '@graphql-yoga/subscription@4.0.0': - resolution: {integrity: sha512-0qsN/BPPZNMoC2CZ8i+P6PgiJyHh1H35aKDt37qARBDaIOKDQuvEOq7+4txUKElcmXi7DYFo109FkhSQoEajrg==} - engines: {node: '>=16.0.0'} - '@graphql-yoga/subscription@5.0.4': resolution: {integrity: sha512-Bcj1LYVQyQmFN/vsl73TRSNistd8lBJJcPxqFVCT8diUuH/gTv2be2OYZsirpeO0l5tFjJWJv9RPgIlCYv3Khw==} engines: {node: '>=18.0.0'} - '@graphql-yoga/typed-event-target@2.0.0': - resolution: {integrity: sha512-oA/VGxGmaSDym1glOHrltw43qZsFwLLjBwvh57B79UKX/vo3+UQcRgOyE44c5RP7DCYjkrC2tuArZmb6jCzysw==} - engines: {node: '>=16.0.0'} - '@graphql-yoga/typed-event-target@3.0.2': resolution: {integrity: sha512-ZpJxMqB+Qfe3rp6uszCQoag4nSw42icURnBRfFYSOmTgEeOe4rD0vYlbA8spvCu2TlCesNTlEN9BLWtQqLxabA==} engines: {node: '>=18.0.0'} @@ -8213,10 +8073,6 @@ packages: resolution: {integrity: sha512-ydxzH1iox9AzLe+uaX9jjyVFkQO+h15j+JClropw0P4Vz+ES4+xTZVu5leUsWW8AYTVZBFkiC0iHl/PwFZ+Q1Q==} engines: {node: '>=18.0.0'} - '@whatwg-node/server@0.9.65': - resolution: {integrity: sha512-CnYTFEUJkbbAcuBXnXirVIgKBfs2YA6sSGjxeq07AUiyXuoQ0fbvTIQoteMglmn09QeGzcH/l0B7nIml83xvVw==} - engines: {node: '>=18.0.0'} - abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -8260,6 +8116,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + addressparser@1.0.1: resolution: {integrity: sha512-aQX7AISOMM7HFE0iZ3+YnD07oIeJqWGVnJ+ZIKaBZAk03ftmVYVqsGas/rbXKR21n4D/hKCSHypvcyOkds/xzg==} @@ -8669,8 +8530,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - browserslist@4.24.3: - resolution: {integrity: sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==} + browserslist@4.25.0: + resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -8793,6 +8654,9 @@ packages: caniuse-lite@1.0.30001690: resolution: {integrity: sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==} + caniuse-lite@1.0.30001723: + resolution: {integrity: sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==} + capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -9207,6 +9071,9 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} @@ -9577,6 +9444,10 @@ packages: resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} engines: {node: '>= 0.6.0'} + dependency-graph@1.0.0: + resolution: {integrity: sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==} + engines: {node: '>=4'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -9607,6 +9478,10 @@ packages: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + detect-newline@4.0.1: resolution: {integrity: sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -9755,12 +9630,12 @@ packages: engines: {node: '>=0.10.0'} hasBin: true + electron-to-chromium@1.5.170: + resolution: {integrity: sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==} + electron-to-chromium@1.5.41: resolution: {integrity: sha512-dfdv/2xNjX0P8Vzme4cfzHqnPm5xsZXwsolTYr0eyW18IUmNyG08vL+fttvinTfhKfIKdRoqkDIC9e9iWQCNYQ==} - electron-to-chromium@1.5.76: - resolution: {integrity: sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==} - emoji-regex@10.3.0: resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} @@ -10853,12 +10728,6 @@ packages: peerDependencies: graphql: '>=0.11 <=16' - graphql-yoga@4.0.3: - resolution: {integrity: sha512-MP+v+yxCqM3lXg95vaA+kXjyRvyRxHUlgZryTecN7ugzEEnyQKKu8JBXA4ziEuLi3liRriyjCAyV4pqFzhHujA==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^15.2.0 || ^16.0.0 - graphql-yoga@5.13.3: resolution: {integrity: sha512-W8efVmPhKOreIJgiYbC2CCn60ORr7kj+5dRg7EoBg6+rbMdL4EqlXp1hYdrmbB5GGgS2g3ivyHutXKUK0S0UZw==} engines: {node: '>=18.0.0'} @@ -10869,10 +10738,6 @@ packages: resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - graphql@17.0.0-alpha.7: - resolution: {integrity: sha512-kdteHez9s0lfNAGntSwnDBpxSl09sBWEFxFRPS/Z8K1nCD4FZ2wVGwXuj5dvrTKcqOA+O8ujAJ3CiY/jXhs14g==} - engines: {node: ^16.19.0 || ^18.14.0 || >=19.7.0} - gray-matter@4.0.3: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} @@ -11833,68 +11698,68 @@ packages: light-my-request@5.14.0: resolution: {integrity: sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==} - lightningcss-darwin-arm64@1.28.2: - resolution: {integrity: sha512-/8cPSqZiusHSS+WQz0W4NuaqFjquys1x+NsdN/XOHb+idGHJSoJ7SoQTVl3DZuAgtPZwFZgRfb/vd1oi8uX6+g==} + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.28.2: - resolution: {integrity: sha512-R7sFrXlgKjvoEG8umpVt/yutjxOL0z8KWf0bfPT3cYMOW4470xu5qSHpFdIOpRWwl3FKNMUdbKtMUjYt0h2j4g==} + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.28.2: - resolution: {integrity: sha512-l2qrCT+x7crAY+lMIxtgvV10R8VurzHAoUZJaVFSlHrN8kRLTvEg9ObojIDIexqWJQvJcVVV3vfzsEynpiuvgA==} + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.28.2: - resolution: {integrity: sha512-DKMzpICBEKnL53X14rF7hFDu8KKALUJtcKdFUCW5YOlGSiwRSgVoRjM97wUm/E0NMPkzrTi/rxfvt7ruNK8meg==} + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.28.2: - resolution: {integrity: sha512-nhfjYkfymWZSxdtTNMWyhFk2ImUm0X7NAgJWFwnsYPOfmtWQEapzG/DXZTfEfMjSzERNUNJoQjPAbdqgB+sjiw==} + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-arm64-musl@1.28.2: - resolution: {integrity: sha512-1SPG1ZTNnphWvAv8RVOymlZ8BDtAg69Hbo7n4QxARvkFVCJAt0cgjAw1Fox0WEhf4PwnyoOBaVH0Z5YNgzt4dA==} + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-x64-gnu@1.28.2: - resolution: {integrity: sha512-ZhQy0FcO//INWUdo/iEdbefntTdpPVQ0XJwwtdbBuMQe+uxqZoytm9M+iqR9O5noWFaxK+nbS2iR/I80Q2Ofpg==} + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-linux-x64-musl@1.28.2: - resolution: {integrity: sha512-alb/j1NMrgQmSFyzTbN1/pvMPM+gdDw7YBuQ5VSgcFDypN3Ah0BzC2dTZbzwzaMdUVDszX6zH5MzjfVN1oGuww==} + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-win32-arm64-msvc@1.28.2: - resolution: {integrity: sha512-WnwcjcBeAt0jGdjlgbT9ANf30pF0C/QMb1XnLnH272DQU8QXh+kmpi24R55wmWBwaTtNAETZ+m35ohyeMiNt+g==} + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.28.2: - resolution: {integrity: sha512-3piBifyT3avz22o6mDKywQC/OisH2yDK+caHWkiMsF82i3m5wDBadyCjlCQ5VNgzYkxrWZgiaxHDdd5uxsi0/A==} + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - lightningcss@1.28.2: - resolution: {integrity: sha512-ePLRrbt3fgjXI5VFZOLbvkLD5ZRuxGKm+wJ3ujCqBtL3NanDHPo/5zicR5uEKAPiIjBYF99BM4K4okvMznjkVA==} + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} engines: {node: '>= 12.0.0'} lilconfig@3.1.3: @@ -12952,11 +12817,9 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} - object-inspect@1.12.3: - resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} - - object-inspect@1.13.1: - resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + object-inspect@1.13.2: + resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} + engines: {node: '>= 0.4'} object-is@1.1.5: resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} @@ -13329,6 +13192,11 @@ packages: peerDependencies: pg: ^8 + pg-cursor@2.15.1: + resolution: {integrity: sha512-H3pT6fqIO1/u55mDGen2v6gvoaIBwVxhoJWEdF0qhQfsF7hXGW1BbJ8CwMtyoZRWZH7fASVoT3p2/4BGUoSxTg==} + peerDependencies: + pg: ^8 + pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} @@ -14737,9 +14605,6 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} - std-env@3.3.3: - resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==} - std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} @@ -15513,8 +15378,8 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - update-browserslist-db@1.1.1: - resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -17389,26 +17254,6 @@ snapshots: '@babel/compat-data@7.26.3': {} - '@babel/core@7.22.9': - dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.26.3 - '@babel/helper-compilation-targets': 7.25.9 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.22.9) - '@babel/helpers': 7.26.10 - '@babel/parser': 7.26.3 - '@babel/template': 7.26.9 - '@babel/traverse': 7.26.4 - '@babel/types': 7.26.10 - convert-source-map: 1.9.0 - debug: 4.4.0(supports-color@8.1.1) - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - '@babel/core@7.26.0': dependencies: '@ampproject/remapping': 2.3.0 @@ -17482,15 +17327,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.26.0(@babel/core@7.22.9)': - dependencies: - '@babel/core': 7.22.9 - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - '@babel/traverse': 7.26.4 - transitivePeerDependencies: - - supports-color - '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -17572,11 +17408,6 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-import-assertions@7.24.1(@babel/core@7.22.9)': - dependencies: - '@babel/core': 7.22.9 - '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-import-assertions@7.24.1(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -17978,20 +17809,21 @@ snapshots: '@codemirror/language@6.10.2': dependencies: - '@codemirror/state': 6.5.0 - '@codemirror/view': 6.36.1 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.37.2 '@lezer/common': 1.2.3 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 style-mod: 4.1.2 - '@codemirror/state@6.5.0': + '@codemirror/state@6.5.2': dependencies: '@marijn/find-cluster-break': 1.0.2 - '@codemirror/view@6.36.1': + '@codemirror/view@6.37.2': dependencies: - '@codemirror/state': 6.5.0 + '@codemirror/state': 6.5.2 + crelt: 1.0.6 style-mod: 4.1.2 w3c-keyname: 2.2.8 @@ -18157,11 +17989,6 @@ snapshots: '@emotion/weak-memoize@0.3.0': {} - '@envelop/core@4.0.3': - dependencies: - '@envelop/types': 4.0.1 - tslib: 2.8.1 - '@envelop/core@5.0.2': dependencies: '@envelop/types': 5.0.0 @@ -18236,10 +18063,6 @@ snapshots: graphql: 16.9.0 tslib: 2.8.1 - '@envelop/types@4.0.1': - dependencies: - tslib: 2.8.1 - '@envelop/types@5.0.0': dependencies: tslib: 2.8.1 @@ -18485,14 +18308,14 @@ snapshots: fastq: 1.19.1 glob: 10.3.12 - '@fastify/vite@6.0.7(patch_hash=f5ce073a4db250ed3db1d9c19e2a253418454ee379530bdee25869570d7b500b)(@types/node@22.10.5)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)': + '@fastify/vite@6.0.7(patch_hash=f5ce073a4db250ed3db1d9c19e2a253418454ee379530bdee25869570d7b500b)(@types/node@22.10.5)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)': dependencies: '@fastify/middie': 8.3.1 '@fastify/static': 6.12.0 fastify-plugin: 4.5.1 fs-extra: 10.1.0 klaw: 4.1.0 - vite: 5.4.12(@types/node@22.10.5)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0) + vite: 5.4.12(@types/node@22.10.5)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0) transitivePeerDependencies: - '@types/node' - less @@ -18617,7 +18440,7 @@ snapshots: graphql: 16.9.0 tslib: 2.6.3 - '@graphql-codegen/cli@5.0.5(@babel/core@7.22.9)(@parcel/watcher@2.5.0)(@types/node@22.10.5)(encoding@0.1.13)(enquirer@2.4.1)(graphql@16.9.0)(typescript@5.7.3)': + '@graphql-codegen/cli@5.0.5(@babel/core@7.26.0)(@parcel/watcher@2.5.0)(@types/node@22.10.5)(encoding@0.1.13)(enquirer@2.4.1)(graphql@16.9.0)(typescript@5.7.3)': dependencies: '@babel/generator': 7.26.3 '@babel/template': 7.26.9 @@ -18627,7 +18450,7 @@ snapshots: '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.9.0) '@graphql-tools/apollo-engine-loader': 8.0.0(encoding@0.1.13)(graphql@16.9.0) '@graphql-tools/code-file-loader': 8.1.0(graphql@16.9.0) - '@graphql-tools/git-loader': 8.0.1(@babel/core@7.22.9)(graphql@16.9.0) + '@graphql-tools/git-loader': 8.0.1(@babel/core@7.26.0)(graphql@16.9.0) '@graphql-tools/github-loader': 8.0.0(@types/node@22.10.5)(encoding@0.1.13)(graphql@16.9.0) '@graphql-tools/graphql-file-loader': 8.0.0(graphql@16.9.0) '@graphql-tools/json-file-loader': 8.0.0(graphql@16.9.0) @@ -18810,29 +18633,6 @@ snapshots: - encoding - supports-color - '@graphql-eslint/eslint-plugin@3.20.1(patch_hash=695fba67df25ba9d46472c8398c94c6a2ccf75d902321d8f95150f68e940313e)(@babel/core@7.22.9)(@types/node@22.10.5)(encoding@0.1.13)(graphql@16.9.0)': - dependencies: - '@babel/code-frame': 7.26.2 - '@graphql-tools/code-file-loader': 7.3.23(@babel/core@7.22.9)(graphql@16.9.0) - '@graphql-tools/graphql-tag-pluck': 7.5.2(@babel/core@7.22.9)(graphql@16.9.0) - '@graphql-tools/utils': 9.2.1(graphql@16.9.0) - chalk: 4.1.2 - debug: 4.3.7(supports-color@8.1.1) - fast-glob: 3.3.2 - graphql: 16.9.0 - graphql-config: 4.5.0(@types/node@22.10.5)(encoding@0.1.13)(graphql@16.9.0) - graphql-depth-limit: 1.1.0(graphql@16.9.0) - lodash.lowercase: 4.3.0 - tslib: 2.8.1 - transitivePeerDependencies: - - '@babel/core' - - '@types/node' - - bufferutil - - cosmiconfig-toml-loader - - encoding - - supports-color - - utf-8-validate - '@graphql-eslint/eslint-plugin@3.20.1(patch_hash=695fba67df25ba9d46472c8398c94c6a2ccf75d902321d8f95150f68e940313e)(@babel/core@7.26.0)(@types/node@22.10.5)(encoding@0.1.13)(graphql@16.9.0)': dependencies: '@babel/code-frame': 7.26.2 @@ -18858,240 +18658,18 @@ snapshots: '@graphql-hive/signal@1.0.0': {} - '@graphql-inspector/audit-command@4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2)': + '@graphql-inspector/core@file:../graphql-inspector/packages/core(graphql@16.9.0)': dependencies: - '@graphql-inspector/commands': 4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2) - '@graphql-inspector/core': 5.0.2(graphql@16.9.0) - '@graphql-inspector/logger': 4.0.2 - '@graphql-tools/utils': 10.0.3(graphql@16.9.0) - cli-table3: 0.6.3 - graphql: 16.9.0 - tslib: 2.6.2 - transitivePeerDependencies: - - '@graphql-inspector/config' - - '@graphql-inspector/loaders' - - yargs - - '@graphql-inspector/cli@4.0.3(@types/node@22.10.5)(encoding@0.1.13)(graphql@16.9.0)': - dependencies: - '@babel/core': 7.22.9 - '@graphql-inspector/audit-command': 4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2) - '@graphql-inspector/code-loader': 4.0.2(@babel/core@7.22.9)(graphql@16.9.0) - '@graphql-inspector/commands': 4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2) - '@graphql-inspector/config': 4.0.2(graphql@16.9.0) - '@graphql-inspector/coverage-command': 5.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2) - '@graphql-inspector/diff-command': 4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2) - '@graphql-inspector/docs-command': 4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2) - '@graphql-inspector/git-loader': 4.0.2(@babel/core@7.22.9)(graphql@16.9.0) - '@graphql-inspector/github-loader': 4.0.2(@types/node@22.10.5)(encoding@0.1.13)(graphql@16.9.0) - '@graphql-inspector/graphql-loader': 4.0.2(graphql@16.9.0) - '@graphql-inspector/introspect-command': 4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2) - '@graphql-inspector/json-loader': 4.0.2(graphql@16.9.0) - '@graphql-inspector/loaders': 4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0) - '@graphql-inspector/serve-command': 4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2) - '@graphql-inspector/similar-command': 4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2) - '@graphql-inspector/url-loader': 4.0.2(@types/node@22.10.5)(encoding@0.1.13)(graphql@16.9.0) - '@graphql-inspector/validate-command': 4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2) + dependency-graph: 1.0.0 graphql: 16.9.0 + object-inspect: 1.13.2 tslib: 2.6.2 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - bufferutil - - encoding - - supports-color - - utf-8-validate - '@graphql-inspector/code-loader@4.0.2(@babel/core@7.22.9)(graphql@16.9.0)': + '@graphql-inspector/patch@file:../graphql-inspector/packages/patch(graphql@16.9.0)': dependencies: - '@graphql-tools/code-file-loader': 8.0.1(@babel/core@7.22.9)(graphql@16.9.0) - graphql: 16.9.0 - tslib: 2.6.2 - transitivePeerDependencies: - - '@babel/core' - - supports-color - - '@graphql-inspector/commands@4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2)': - dependencies: - '@graphql-inspector/config': 4.0.2(graphql@16.9.0) - '@graphql-inspector/loaders': 4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0) - graphql: 16.9.0 - tslib: 2.6.2 - yargs: 17.7.2 - - '@graphql-inspector/config@4.0.2(graphql@16.9.0)': - dependencies: - graphql: 16.9.0 - tslib: 2.6.2 - - '@graphql-inspector/core@5.0.2(graphql@16.9.0)': - dependencies: - dependency-graph: 0.11.0 - graphql: 16.9.0 - object-inspect: 1.12.3 - tslib: 2.6.2 - - '@graphql-inspector/core@5.1.0-alpha-20231208113249-34700c8a(graphql@16.9.0)': - dependencies: - dependency-graph: 0.11.0 - graphql: 16.9.0 - object-inspect: 1.12.3 - tslib: 2.6.2 - - '@graphql-inspector/core@5.1.0-alpha-20231208113249-34700c8a(graphql@17.0.0-alpha.7)': - dependencies: - dependency-graph: 0.11.0 - graphql: 17.0.0-alpha.7 - object-inspect: 1.12.3 - tslib: 2.6.2 - - '@graphql-inspector/coverage-command@5.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2)': - dependencies: - '@graphql-inspector/commands': 4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2) - '@graphql-inspector/core': 5.0.2(graphql@16.9.0) - '@graphql-inspector/logger': 4.0.2 - '@graphql-tools/utils': 10.0.3(graphql@16.9.0) - graphql: 16.9.0 - tslib: 2.6.2 - transitivePeerDependencies: - - '@graphql-inspector/config' - - '@graphql-inspector/loaders' - - yargs - - '@graphql-inspector/diff-command@4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2)': - dependencies: - '@graphql-inspector/commands': 4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2) - '@graphql-inspector/core': 5.0.2(graphql@16.9.0) - '@graphql-inspector/logger': 4.0.2 - graphql: 16.9.0 - tslib: 2.6.2 - transitivePeerDependencies: - - '@graphql-inspector/config' - - '@graphql-inspector/loaders' - - yargs - - '@graphql-inspector/docs-command@4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2)': - dependencies: - '@graphql-inspector/commands': 4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2) - graphql: 16.9.0 - open: 8.4.2 - tslib: 2.6.2 - transitivePeerDependencies: - - '@graphql-inspector/config' - - '@graphql-inspector/loaders' - - yargs - - '@graphql-inspector/git-loader@4.0.2(@babel/core@7.22.9)(graphql@16.9.0)': - dependencies: - '@graphql-tools/git-loader': 8.0.1(@babel/core@7.22.9)(graphql@16.9.0) - graphql: 16.9.0 - tslib: 2.6.2 - transitivePeerDependencies: - - '@babel/core' - - supports-color - - '@graphql-inspector/github-loader@4.0.2(@types/node@22.10.5)(encoding@0.1.13)(graphql@16.9.0)': - dependencies: - '@graphql-tools/github-loader': 8.0.0(@types/node@22.10.5)(encoding@0.1.13)(graphql@16.9.0) - graphql: 16.9.0 - tslib: 2.6.2 - transitivePeerDependencies: - - '@types/node' - - encoding - - supports-color - - '@graphql-inspector/graphql-loader@4.0.2(graphql@16.9.0)': - dependencies: - '@graphql-tools/graphql-file-loader': 8.0.0(graphql@16.9.0) - graphql: 16.9.0 - tslib: 2.6.2 - - '@graphql-inspector/introspect-command@4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2)': - dependencies: - '@graphql-inspector/commands': 4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2) - '@graphql-inspector/core': 5.0.2(graphql@16.9.0) - '@graphql-inspector/logger': 4.0.2 - graphql: 16.9.0 - tslib: 2.6.2 - transitivePeerDependencies: - - '@graphql-inspector/config' - - '@graphql-inspector/loaders' - - yargs - - '@graphql-inspector/json-loader@4.0.2(graphql@16.9.0)': - dependencies: - '@graphql-tools/json-file-loader': 8.0.0(graphql@16.9.0) - graphql: 16.9.0 - tslib: 2.6.2 - - '@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0)': - dependencies: - '@graphql-inspector/config': 4.0.2(graphql@16.9.0) - '@graphql-tools/code-file-loader': 8.0.1(@babel/core@7.22.9)(graphql@16.9.0) - '@graphql-tools/load': 8.0.0(graphql@16.9.0) - '@graphql-tools/utils': 10.0.3(graphql@16.9.0) - graphql: 16.9.0 - tslib: 2.6.2 - transitivePeerDependencies: - - '@babel/core' - - supports-color - - '@graphql-inspector/logger@4.0.2': - dependencies: - chalk: 4.1.2 - figures: 3.2.0 - log-symbols: 4.1.0 - std-env: 3.3.3 - tslib: 2.6.2 - - '@graphql-inspector/serve-command@4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2)': - dependencies: - '@graphql-inspector/commands': 4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2) - '@graphql-inspector/logger': 4.0.2 - graphql: 16.9.0 - graphql-yoga: 4.0.3(graphql@16.9.0) - open: 8.4.2 - tslib: 2.6.2 - transitivePeerDependencies: - - '@graphql-inspector/config' - - '@graphql-inspector/loaders' - - yargs - - '@graphql-inspector/similar-command@4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2)': - dependencies: - '@graphql-inspector/commands': 4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2) - '@graphql-inspector/core': 5.0.2(graphql@16.9.0) - '@graphql-inspector/logger': 4.0.2 - graphql: 16.9.0 - tslib: 2.6.2 - transitivePeerDependencies: - - '@graphql-inspector/config' - - '@graphql-inspector/loaders' - - yargs - - '@graphql-inspector/url-loader@4.0.2(@types/node@22.10.5)(encoding@0.1.13)(graphql@16.9.0)': - dependencies: - '@graphql-tools/url-loader': 8.0.0(@types/node@22.10.5)(encoding@0.1.13)(graphql@16.9.0) - graphql: 16.9.0 - tslib: 2.6.2 - transitivePeerDependencies: - - '@types/node' - - bufferutil - - encoding - - utf-8-validate - - '@graphql-inspector/validate-command@4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2)': - dependencies: - '@graphql-inspector/commands': 4.0.3(@graphql-inspector/config@4.0.2(graphql@16.9.0))(@graphql-inspector/loaders@4.0.3(@babel/core@7.22.9)(@graphql-inspector/config@4.0.2(graphql@16.9.0))(graphql@16.9.0))(graphql@16.9.0)(yargs@17.7.2) - '@graphql-inspector/core': 5.0.2(graphql@16.9.0) - '@graphql-inspector/logger': 4.0.2 - '@graphql-tools/utils': 10.0.3(graphql@16.9.0) + '@graphql-tools/utils': 10.8.6(graphql@16.9.0) graphql: 16.9.0 tslib: 2.6.2 - transitivePeerDependencies: - - '@graphql-inspector/config' - - '@graphql-inspector/loaders' - - yargs '@graphql-tools/apollo-engine-loader@8.0.0(encoding@0.1.13)(graphql@16.9.0)': dependencies: @@ -19143,18 +18721,6 @@ snapshots: tslib: 2.8.1 value-or-promise: 1.0.12 - '@graphql-tools/code-file-loader@7.3.23(@babel/core@7.22.9)(graphql@16.9.0)': - dependencies: - '@graphql-tools/graphql-tag-pluck': 7.5.2(@babel/core@7.22.9)(graphql@16.9.0) - '@graphql-tools/utils': 9.2.1(graphql@16.9.0) - globby: 11.1.0 - graphql: 16.9.0 - tslib: 2.8.1 - unixify: 1.0.0 - transitivePeerDependencies: - - '@babel/core' - - supports-color - '@graphql-tools/code-file-loader@7.3.23(@babel/core@7.26.0)(graphql@16.9.0)': dependencies: '@graphql-tools/graphql-tag-pluck': 7.5.2(@babel/core@7.26.0)(graphql@16.9.0) @@ -19167,18 +18733,6 @@ snapshots: - '@babel/core' - supports-color - '@graphql-tools/code-file-loader@8.0.1(@babel/core@7.22.9)(graphql@16.9.0)': - dependencies: - '@graphql-tools/graphql-tag-pluck': 8.0.1(@babel/core@7.22.9)(graphql@16.9.0) - '@graphql-tools/utils': 10.8.6(graphql@16.9.0) - globby: 11.1.0 - graphql: 16.9.0 - tslib: 2.8.1 - unixify: 1.0.0 - transitivePeerDependencies: - - '@babel/core' - - supports-color - '@graphql-tools/code-file-loader@8.1.0(graphql@16.9.0)': dependencies: '@graphql-tools/graphql-tag-pluck': 8.2.0(graphql@16.9.0) @@ -19363,9 +18917,9 @@ snapshots: graphql: 16.9.0 tslib: 2.8.1 - '@graphql-tools/git-loader@8.0.1(@babel/core@7.22.9)(graphql@16.9.0)': + '@graphql-tools/git-loader@8.0.1(@babel/core@7.26.0)(graphql@16.9.0)': dependencies: - '@graphql-tools/graphql-tag-pluck': 8.0.1(@babel/core@7.22.9)(graphql@16.9.0) + '@graphql-tools/graphql-tag-pluck': 8.0.1(@babel/core@7.26.0)(graphql@16.9.0) '@graphql-tools/utils': 10.8.6(graphql@16.9.0) graphql: 16.9.0 is-glob: 4.0.3 @@ -19409,19 +18963,6 @@ snapshots: tslib: 2.8.1 unixify: 1.0.0 - '@graphql-tools/graphql-tag-pluck@7.5.2(@babel/core@7.22.9)(graphql@16.9.0)': - dependencies: - '@babel/parser': 7.26.3 - '@babel/plugin-syntax-import-assertions': 7.24.1(@babel/core@7.22.9) - '@babel/traverse': 7.26.4 - '@babel/types': 7.26.3 - '@graphql-tools/utils': 9.2.1(graphql@16.9.0) - graphql: 16.9.0 - tslib: 2.8.1 - transitivePeerDependencies: - - '@babel/core' - - supports-color - '@graphql-tools/graphql-tag-pluck@7.5.2(@babel/core@7.26.0)(graphql@16.9.0)': dependencies: '@babel/parser': 7.26.3 @@ -19435,10 +18976,10 @@ snapshots: - '@babel/core' - supports-color - '@graphql-tools/graphql-tag-pluck@8.0.1(@babel/core@7.22.9)(graphql@16.9.0)': + '@graphql-tools/graphql-tag-pluck@8.0.1(@babel/core@7.26.0)(graphql@16.9.0)': dependencies: '@babel/parser': 7.26.10 - '@babel/plugin-syntax-import-assertions': 7.24.1(@babel/core@7.22.9) + '@babel/plugin-syntax-import-assertions': 7.24.1(@babel/core@7.26.0) '@babel/traverse': 7.26.4 '@babel/types': 7.26.10 '@graphql-tools/utils': 10.8.6(graphql@16.9.0) @@ -19649,13 +19190,6 @@ snapshots: - encoding - utf-8-validate - '@graphql-tools/utils@10.0.3(graphql@16.9.0)': - dependencies: - '@graphql-typed-document-node/core': 3.2.0(graphql@16.9.0) - dset: 3.1.4 - graphql: 16.9.0 - tslib: 2.8.1 - '@graphql-tools/utils@10.5.6(graphql@16.9.0)': dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.9.0) @@ -19718,10 +19252,6 @@ snapshots: dependencies: graphql: 16.9.0 - '@graphql-yoga/logger@1.0.0': - dependencies: - tslib: 2.8.1 - '@graphql-yoga/logger@2.0.1': dependencies: tslib: 2.8.1 @@ -19771,13 +19301,6 @@ snapshots: '@whatwg-node/events': 0.1.1 ioredis: 5.4.2 - '@graphql-yoga/subscription@4.0.0': - dependencies: - '@graphql-yoga/typed-event-target': 2.0.0 - '@repeaterjs/repeater': 3.0.6 - '@whatwg-node/events': 0.1.1 - tslib: 2.8.1 - '@graphql-yoga/subscription@5.0.4': dependencies: '@graphql-yoga/typed-event-target': 3.0.2 @@ -19785,11 +19308,6 @@ snapshots: '@whatwg-node/events': 0.1.1 tslib: 2.8.1 - '@graphql-yoga/typed-event-target@2.0.0': - dependencies: - '@repeaterjs/repeater': 3.0.6 - tslib: 2.8.1 - '@graphql-yoga/typed-event-target@3.0.2': dependencies: '@repeaterjs/repeater': 3.0.6 @@ -20014,11 +19532,11 @@ snapshots: '@josephg/resolvable@1.0.1': {} - '@joshwooding/vite-plugin-react-docgen-typescript@0.4.2(typescript@5.7.3)(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.4.2(typescript@5.7.3)(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0))': dependencies: magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.7.3) - vite: 6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + vite: 6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) optionalDependencies: typescript: 5.7.3 @@ -23898,13 +23416,13 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/builder-vite@8.4.7(storybook@8.4.7(prettier@3.4.2))(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0))': + '@storybook/builder-vite@8.4.7(storybook@8.4.7(prettier@3.4.2))(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0))': dependencies: '@storybook/csf-plugin': 8.4.7(storybook@8.4.7(prettier@3.4.2)) browser-assert: 1.2.1 storybook: 8.4.7(prettier@3.4.2) ts-dedent: 2.2.0 - vite: 6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + vite: 6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) '@storybook/components@8.4.7(storybook@8.4.7(prettier@3.4.2))': dependencies: @@ -23966,11 +23484,11 @@ snapshots: react-dom: 18.3.1(react@18.3.1) storybook: 8.4.7(prettier@3.4.2) - '@storybook/react-vite@8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(prettier@3.4.2))(typescript@5.7.3)(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0))': + '@storybook/react-vite@8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(prettier@3.4.2))(typescript@5.7.3)(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.4.2(typescript@5.7.3)(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.4.2(typescript@5.7.3)(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)) '@rollup/pluginutils': 5.0.2 - '@storybook/builder-vite': 8.4.7(storybook@8.4.7(prettier@3.4.2))(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)) + '@storybook/builder-vite': 8.4.7(storybook@8.4.7(prettier@3.4.2))(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)) '@storybook/react': 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(prettier@3.4.2))(typescript@5.7.3) find-up: 5.0.0 magic-string: 0.30.10 @@ -23980,7 +23498,7 @@ snapshots: resolve: 1.22.8 storybook: 8.4.7(prettier@3.4.2) tsconfig-paths: 4.2.0 - vite: 6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + vite: 6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) transitivePeerDependencies: - '@storybook/test' - rollup @@ -24207,7 +23725,7 @@ snapshots: typescript: 4.9.5 yargs: 16.2.0 - '@theguild/components@9.7.1(@theguild/tailwind-config@0.6.3(postcss-import@16.1.0(postcss@8.4.49))(postcss-lightningcss@1.0.1(postcss@8.4.49))(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@22.10.5)(typescript@5.7.3))))(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(immer@10.1.1)(next@15.2.4(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3)(use-sync-external-store@1.5.0(react@19.0.0))': + '@theguild/components@9.7.1(@theguild/tailwind-config@0.6.3(postcss-import@16.1.0(postcss@8.4.49))(postcss-lightningcss@1.0.1(postcss@8.4.49))(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@22.10.5)(typescript@5.7.3))))(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(immer@10.1.1)(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3)(use-sync-external-store@1.5.0(react@19.0.0))': dependencies: '@giscus/react': 3.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@next/bundle-analyzer': 15.1.5 @@ -24217,9 +23735,9 @@ snapshots: '@theguild/tailwind-config': 0.6.3(postcss-import@16.1.0(postcss@8.4.49))(postcss-lightningcss@1.0.1(postcss@8.4.49))(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@22.10.5)(typescript@5.7.3))) clsx: 2.1.1 fuzzy: 0.1.3 - next: 15.2.4(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - nextra: 4.0.5(patch_hash=c1d11430a02e4d51d69b615df3f615fd6dfbccfd71b122bcf781a8a35208fbc1)(next@15.2.4(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3) - nextra-theme-docs: 4.0.5(patch_hash=38956679ac61493f4dbc6862445316e9909dd989c221357f4b21ce70d8c8fd5b)(@types/react@18.3.18)(immer@10.1.1)(next@15.2.4(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nextra@4.0.5(patch_hash=c1d11430a02e4d51d69b615df3f615fd6dfbccfd71b122bcf781a8a35208fbc1)(next@15.2.4(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(use-sync-external-store@1.5.0(react@19.0.0)) + next: 15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + nextra: 4.0.5(patch_hash=c1d11430a02e4d51d69b615df3f615fd6dfbccfd71b122bcf781a8a35208fbc1)(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3) + nextra-theme-docs: 4.0.5(patch_hash=38956679ac61493f4dbc6862445316e9909dd989c221357f4b21ce70d8c8fd5b)(@types/react@18.3.18)(immer@10.1.1)(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nextra@4.0.5(patch_hash=c1d11430a02e4d51d69b615df3f615fd6dfbccfd71b122bcf781a8a35208fbc1)(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(use-sync-external-store@1.5.0(react@19.0.0)) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) react-paginate: 8.2.0(react@19.0.0) @@ -24824,14 +24342,14 @@ snapshots: dependencies: graphql: 16.9.0 - '@vitejs/plugin-react@4.3.4(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0))': + '@vitejs/plugin-react@4.3.4(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0))': dependencies: '@babel/core': 7.26.0 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + vite: 6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) transitivePeerDependencies: - supports-color @@ -24849,13 +24367,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.1.1(vite@6.1.5(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0))': + '@vitest/mocker@3.1.1(vite@6.1.5(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0))': dependencies: '@vitest/spy': 3.1.1 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.1.5(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + vite: 6.1.5(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) '@vitest/pretty-format@2.0.5': dependencies: @@ -25007,12 +24525,6 @@ snapshots: '@whatwg-node/promise-helpers': 1.3.1 tslib: 2.8.1 - '@whatwg-node/server@0.9.65': - dependencies: - '@whatwg-node/disposablestack': 0.0.5 - '@whatwg-node/fetch': 0.10.6 - tslib: 2.8.1 - abbrev@1.1.1: {} abbrev@2.0.0: {} @@ -25044,6 +24556,9 @@ snapshots: acorn@8.14.0: {} + acorn@8.15.0: + optional: true + addressparser@1.0.1: {} agent-base@6.0.2: @@ -25490,12 +25005,12 @@ snapshots: node-releases: 2.0.18 update-browserslist-db: 1.1.0(browserslist@4.24.0) - browserslist@4.24.3: + browserslist@4.25.0: dependencies: - caniuse-lite: 1.0.30001690 - electron-to-chromium: 1.5.76 + caniuse-lite: 1.0.30001723 + electron-to-chromium: 1.5.170 node-releases: 2.0.19 - update-browserslist-db: 1.1.1(browserslist@4.24.3) + update-browserslist-db: 1.1.3(browserslist@4.25.0) bser@2.1.1: dependencies: @@ -25658,6 +25173,8 @@ snapshots: caniuse-lite@1.0.30001690: {} + caniuse-lite@1.0.30001723: {} + capital-case@1.0.4: dependencies: no-case: 3.0.4 @@ -26101,6 +25618,8 @@ snapshots: create-require@1.1.1: {} + crelt@1.0.6: {} + cron-parser@4.9.0: dependencies: luxon: 3.5.0 @@ -26521,6 +26040,8 @@ snapshots: dependency-graph@0.11.0: {} + dependency-graph@1.0.0: {} + dequal@2.0.3: {} derive-valtio@0.1.0(valtio@1.13.2(@types/react@18.3.18)(react@18.3.1)): @@ -26538,6 +26059,8 @@ snapshots: detect-libc@2.0.3: optional: true + detect-libc@2.0.4: {} + detect-newline@4.0.1: {} detect-node-es@1.1.0: {} @@ -26702,9 +26225,9 @@ snapshots: dependencies: jake: 10.8.5 - electron-to-chromium@1.5.41: {} + electron-to-chromium@1.5.170: {} - electron-to-chromium@1.5.76: {} + electron-to-chromium@1.5.41: {} emoji-regex@10.3.0: {} @@ -26784,7 +26307,7 @@ snapshots: is-string: 1.0.7 is-typed-array: 1.1.12 is-weakref: 1.0.2 - object-inspect: 1.13.1 + object-inspect: 1.13.2 object-keys: 1.1.1 object.assign: 4.1.5 regexp.prototype.flags: 1.5.1 @@ -28191,21 +27714,6 @@ snapshots: dependencies: graphql: 16.9.0 - graphql-yoga@4.0.3(graphql@16.9.0): - dependencies: - '@envelop/core': 4.0.3 - '@graphql-tools/executor': 1.3.12(graphql@16.9.0) - '@graphql-tools/schema': 10.0.23(graphql@16.9.0) - '@graphql-tools/utils': 10.8.6(graphql@16.9.0) - '@graphql-yoga/logger': 1.0.0 - '@graphql-yoga/subscription': 4.0.0 - '@whatwg-node/fetch': 0.9.22 - '@whatwg-node/server': 0.9.65 - dset: 3.1.4 - graphql: 16.9.0 - lru-cache: 10.2.0 - tslib: 2.8.1 - graphql-yoga@5.13.3(graphql@16.9.0): dependencies: '@envelop/core': 5.2.3 @@ -28225,8 +27733,6 @@ snapshots: graphql@16.9.0: {} - graphql@17.0.0-alpha.7: {} - gray-matter@4.0.3: dependencies: js-yaml: 3.14.1 @@ -29253,50 +28759,50 @@ snapshots: process-warning: 3.0.0 set-cookie-parser: 2.7.1 - lightningcss-darwin-arm64@1.28.2: + lightningcss-darwin-arm64@1.30.1: optional: true - lightningcss-darwin-x64@1.28.2: + lightningcss-darwin-x64@1.30.1: optional: true - lightningcss-freebsd-x64@1.28.2: + lightningcss-freebsd-x64@1.30.1: optional: true - lightningcss-linux-arm-gnueabihf@1.28.2: + lightningcss-linux-arm-gnueabihf@1.30.1: optional: true - lightningcss-linux-arm64-gnu@1.28.2: + lightningcss-linux-arm64-gnu@1.30.1: optional: true - lightningcss-linux-arm64-musl@1.28.2: + lightningcss-linux-arm64-musl@1.30.1: optional: true - lightningcss-linux-x64-gnu@1.28.2: + lightningcss-linux-x64-gnu@1.30.1: optional: true - lightningcss-linux-x64-musl@1.28.2: + lightningcss-linux-x64-musl@1.30.1: optional: true - lightningcss-win32-arm64-msvc@1.28.2: + lightningcss-win32-arm64-msvc@1.30.1: optional: true - lightningcss-win32-x64-msvc@1.28.2: + lightningcss-win32-x64-msvc@1.30.1: optional: true - lightningcss@1.28.2: + lightningcss@1.30.1: dependencies: - detect-libc: 1.0.3 + detect-libc: 2.0.4 optionalDependencies: - lightningcss-darwin-arm64: 1.28.2 - lightningcss-darwin-x64: 1.28.2 - lightningcss-freebsd-x64: 1.28.2 - lightningcss-linux-arm-gnueabihf: 1.28.2 - lightningcss-linux-arm64-gnu: 1.28.2 - lightningcss-linux-arm64-musl: 1.28.2 - lightningcss-linux-x64-gnu: 1.28.2 - lightningcss-linux-x64-musl: 1.28.2 - lightningcss-win32-arm64-msvc: 1.28.2 - lightningcss-win32-x64-msvc: 1.28.2 + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 lilconfig@3.1.3: {} @@ -30644,20 +30150,20 @@ snapshots: neoip@2.1.0: {} - next-sitemap@4.2.3(next@15.2.4(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)): + next-sitemap@4.2.3(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)): dependencies: '@corex/deepmerge': 4.0.43 '@next/env': 13.5.6 fast-glob: 3.3.2 minimist: 1.2.8 - next: 15.2.4(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next-themes@0.4.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - next@15.2.4(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@next/env': 15.2.4 '@swc/counter': 0.1.3 @@ -30667,7 +30173,7 @@ snapshots: postcss: 8.4.31 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - styled-jsx: 5.1.6(@babel/core@7.22.9)(react@19.0.0) + styled-jsx: 5.1.6(@babel/core@7.26.0)(react@19.0.0) optionalDependencies: '@next/swc-darwin-arm64': 15.2.4 '@next/swc-darwin-x64': 15.2.4 @@ -30683,13 +30189,13 @@ snapshots: - '@babel/core' - babel-plugin-macros - nextra-theme-docs@4.0.5(patch_hash=38956679ac61493f4dbc6862445316e9909dd989c221357f4b21ce70d8c8fd5b)(@types/react@18.3.18)(immer@10.1.1)(next@15.2.4(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nextra@4.0.5(patch_hash=c1d11430a02e4d51d69b615df3f615fd6dfbccfd71b122bcf781a8a35208fbc1)(next@15.2.4(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(use-sync-external-store@1.5.0(react@19.0.0)): + nextra-theme-docs@4.0.5(patch_hash=38956679ac61493f4dbc6862445316e9909dd989c221357f4b21ce70d8c8fd5b)(@types/react@18.3.18)(immer@10.1.1)(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nextra@4.0.5(patch_hash=c1d11430a02e4d51d69b615df3f615fd6dfbccfd71b122bcf781a8a35208fbc1)(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(use-sync-external-store@1.5.0(react@19.0.0)): dependencies: '@headlessui/react': 2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) clsx: 2.1.1 - next: 15.2.4(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next-themes: 0.4.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - nextra: 4.0.5(patch_hash=c1d11430a02e4d51d69b615df3f615fd6dfbccfd71b122bcf781a8a35208fbc1)(next@15.2.4(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3) + nextra: 4.0.5(patch_hash=c1d11430a02e4d51d69b615df3f615fd6dfbccfd71b122bcf781a8a35208fbc1)(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3) react: 19.0.0 react-compiler-runtime: 0.0.0-experimental-22c6e49-20241219(react@19.0.0) react-dom: 19.0.0(react@19.0.0) @@ -30702,7 +30208,7 @@ snapshots: - immer - use-sync-external-store - nextra@4.0.5(patch_hash=c1d11430a02e4d51d69b615df3f615fd6dfbccfd71b122bcf781a8a35208fbc1)(next@15.2.4(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3): + nextra@4.0.5(patch_hash=c1d11430a02e4d51d69b615df3f615fd6dfbccfd71b122bcf781a8a35208fbc1)(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3): dependencies: '@formatjs/intl-localematcher': 0.5.5 '@headlessui/react': 2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -30723,7 +30229,7 @@ snapshots: mdast-util-gfm: 3.0.0 mdast-util-to-hast: 13.2.0 negotiator: 1.0.0 - next: 15.2.4(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 react-compiler-runtime: 0.0.0-experimental-22c6e49-20241219(react@19.0.0) react-dom: 19.0.0(react@19.0.0) @@ -30917,9 +30423,7 @@ snapshots: object-hash@3.0.0: {} - object-inspect@1.12.3: {} - - object-inspect@1.13.1: {} + object-inspect@1.13.2: {} object-is@1.1.5: dependencies: @@ -31368,6 +30872,10 @@ snapshots: dependencies: pg: 8.13.1 + pg-cursor@2.15.1(pg@8.13.1): + dependencies: + pg: 8.13.1 + pg-int8@1.0.1: {} pg-minify@1.6.5: {} @@ -31393,7 +30901,7 @@ snapshots: pg-query-stream@4.7.0(pg@8.13.1): dependencies: pg: 8.13.1 - pg-cursor: 2.12.1(pg@8.13.1) + pg-cursor: 2.15.1(pg@8.13.1) pg-types@2.2.0: dependencies: @@ -31526,8 +31034,8 @@ snapshots: postcss-lightningcss@1.0.1(postcss@8.4.49): dependencies: - browserslist: 4.24.3 - lightningcss: 1.28.2 + browserslist: 4.25.0 + lightningcss: 1.30.1 postcss: 8.4.49 postcss-load-config@4.0.2(postcss@8.4.49)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@22.10.5)(typescript@5.7.3)): @@ -32709,7 +32217,7 @@ snapshots: call-bind: 1.0.7 es-errors: 1.3.0 get-intrinsic: 1.2.4 - object-inspect: 1.13.1 + object-inspect: 1.13.2 siginfo@2.0.0: {} @@ -32958,8 +32466,6 @@ snapshots: statuses@2.0.1: {} - std-env@3.3.3: {} - std-env@3.9.0: {} stoppable@1.1.0: {} @@ -33098,12 +32604,12 @@ snapshots: dependencies: inline-style-parser: 0.2.3 - styled-jsx@5.1.6(@babel/core@7.22.9)(react@19.0.0): + styled-jsx@5.1.6(@babel/core@7.26.0)(react@19.0.0): dependencies: client-only: 0.0.1 react: 19.0.0 optionalDependencies: - '@babel/core': 7.22.9 + '@babel/core': 7.26.0 stylis@4.1.3: {} @@ -33268,7 +32774,7 @@ snapshots: terser@5.37.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.14.0 + acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 optional: true @@ -33820,9 +33326,9 @@ snapshots: escalade: 3.1.2 picocolors: 1.1.1 - update-browserslist-db@1.1.1(browserslist@4.24.3): + update-browserslist-db@1.1.3(browserslist@4.25.0): dependencies: - browserslist: 4.24.3 + browserslist: 4.25.0 escalade: 3.2.0 picocolors: 1.1.1 @@ -34000,13 +33506,13 @@ snapshots: unist-util-stringify-position: 4.0.0 vfile-message: 4.0.2 - vite-node@3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0): + vite-node@3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0): dependencies: cac: 6.7.14 debug: 4.4.0(supports-color@8.1.1) es-module-lexer: 1.6.0 pathe: 2.0.3 - vite: 6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + vite: 6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) transitivePeerDependencies: - '@types/node' - jiti @@ -34021,18 +33527,18 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)): + vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)): dependencies: debug: 4.3.7(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 3.0.3(typescript@5.7.3) optionalDependencies: - vite: 6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + vite: 6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) transitivePeerDependencies: - supports-color - typescript - vite@5.4.12(@types/node@22.10.5)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0): + vite@5.4.12(@types/node@22.10.5)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0): dependencies: esbuild: 0.25.0 postcss: 8.4.49 @@ -34041,10 +33547,10 @@ snapshots: '@types/node': 22.10.5 fsevents: 2.3.3 less: 4.2.0 - lightningcss: 1.28.2 + lightningcss: 1.30.1 terser: 5.37.0 - vite@6.1.5(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0): + vite@6.1.5(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0): dependencies: esbuild: 0.25.0 postcss: 8.5.2 @@ -34054,12 +33560,12 @@ snapshots: fsevents: 2.3.3 jiti: 2.3.3 less: 4.2.0 - lightningcss: 1.28.2 + lightningcss: 1.30.1 terser: 5.37.0 tsx: 4.19.2 yaml: 2.5.0 - vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0): + vite@6.3.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0): dependencies: esbuild: 0.25.0 fdir: 6.4.4(picomatch@4.0.2) @@ -34072,15 +33578,15 @@ snapshots: fsevents: 2.3.3 jiti: 2.3.3 less: 4.2.0 - lightningcss: 1.28.2 + lightningcss: 1.30.1 terser: 5.37.0 tsx: 4.19.2 yaml: 2.5.0 - vitest@3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0): + vitest@3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0): dependencies: '@vitest/expect': 3.1.1 - '@vitest/mocker': 3.1.1(vite@6.1.5(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)) + '@vitest/mocker': 3.1.1(vite@6.1.5(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)) '@vitest/pretty-format': 3.1.1 '@vitest/runner': 3.1.1 '@vitest/snapshot': 3.1.1 @@ -34096,8 +33602,8 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.1.5(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) - vite-node: 3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + vite: 6.1.5(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + vite-node: 3.1.1(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.10.5