diff --git a/package.json b/package.json index 4d2f607020..c8ebc7a2e8 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@graphql-codegen/urql-introspection": "3.0.0", "@graphql-eslint/eslint-plugin": "3.20.1", "@graphql-inspector/cli": "4.0.3", + "@graphql-inspector/core": "^6.0.0", "@manypkg/get-packages": "2.2.2", "@next/eslint-plugin-next": "14.2.23", "@parcel/watcher": "2.5.0", diff --git a/packages/migrations/src/actions/2025.05.29T00.00.00.schema-proposals.ts b/packages/migrations/src/actions/2025.05.29T00.00.00.schema-proposals.ts new file mode 100644 index 0000000000..d9d52ab4a3 --- /dev/null +++ b/packages/migrations/src/actions/2025.05.29T00.00.00.schema-proposals.ts @@ -0,0 +1,179 @@ +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 + , 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 + , user_id UUID REFERENCES users (id) ON DELETE SET NULL + -- The original schema version that this proposal referenced. In case the version is deleted, + -- set this to null to avoid completely erasing the change... This should never happen. + , diff_schema_version_id UUID NOT NULL REFERENCES schema_versions (id) ON DELETE SET NULL + ) + ; + 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 schema_version delete + CREATE INDEX IF NOT EXISTS schema_proposals_diff_schema_version_id on schema_proposals ( + diff_schema_version_id + ) + ; + -- For performance during user delete + CREATE INDEX IF NOT EXISTS schema_proposals_diff_user_id on schema_proposals ( + user_id + ) + ; + /** + * Request patterns include: + * - Get by ID + * - List proposal's latest versions for each service + * - List all proposal's versions ordered by date + */ + CREATE TABLE IF NOT EXISTS "schema_proposal_versions" + ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4 () + , user_id UUID REFERENCES users (id) ON DELETE SET NULL + , created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + , schema_proposal_id UUID NOT NULL REFERENCES schema_proposals (id) ON DELETE CASCADE + , service_name text + , schema_sdl text NOT NULL + ) + ; + CREATE INDEX IF NOT EXISTS schema_proposal_versions_list_latest_by_distinct_service ON schema_proposal_versions( + schema_proposal_id + , service_name + , created_at DESC + ) + ; + CREATE INDEX IF NOT EXISTS schema_proposal_versions_schema_proposal_id_created_at ON schema_proposal_versions( + schema_proposal_id + , created_at DESC + ) + ; + /** + * Request patterns include: + * - Get by ID + * - List proposal's latest versions for each service + * - List all proposal's versions ordered by date + */ + /** + SELECT * FROM schema_proposal_comments as c JOIN schema_proposal_reviews as r + ON r.schema_proposal_review_id = c.id + WHERE schema_proposal_id = $1 + ORDER BY created_at + LIMIT 10 + ; + */ + 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 originally proposed version to be able to reference back as outdated if unable to attribute + -- the review to another version. + , original_schema_proposal_version_id UUID NOT NULL REFERENCES schema_proposal_versions (id) ON DELETE SET NULL + -- 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 + -- used in combination with the line_text to determine what line in the current version this review is attributed to + , original_line_num INT + -- 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 + ) + ; + 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 schema_proposal_versions delete + CREATE INDEX IF NOT EXISTS schema_proposal_reviews_original_schema_proposal_version_id ON schema_proposal_reviews( + original_schema_proposal_version_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 + ) + ; + `, + }, + ], +} satisfies MigrationExecutor; diff --git a/packages/migrations/src/run-pg-migrations.ts b/packages/migrations/src/run-pg-migrations.ts index fc0b902d47..6790644d2f 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.05.29T00.00.00.schema-proposals'), ], }); diff --git a/packages/services/api/src/create.ts b/packages/services/api/src/create.ts index 9f9389d8f0..d9c8c8d320 100644 --- a/packages/services/api/src/create.ts +++ b/packages/services/api/src/create.ts @@ -36,6 +36,7 @@ import { SchemaPolicyServiceConfig, } from './modules/policy/providers/tokens'; import { projectModule } from './modules/project'; +import { proposalsModule } from './modules/proposals'; import { schemaModule } from './modules/schema'; import { ArtifactStorageWriter } from './modules/schema/providers/artifact-storage-writer'; import { provideSchemaModuleConfig, SchemaModuleConfig } from './modules/schema/providers/config'; @@ -88,6 +89,7 @@ const modules = [ collectionModule, appDeploymentsModule, auditLogsModule, + proposalsModule, ]; export function createRegistry({ 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..d1b6862b56 --- /dev/null +++ b/packages/services/api/src/modules/proposals/index.ts @@ -0,0 +1,11 @@ +import { createModule } from 'graphql-modules'; +import { resolvers } from './resolvers.generated'; +import typeDefs from './module.graphql'; + +export const proposalsModule = createModule({ + id: 'proposals', + dirname: __dirname, + typeDefs, + resolvers, + providers: [], +}); 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..d04b328952 --- /dev/null +++ b/packages/services/api/src/modules/proposals/module.graphql.ts @@ -0,0 +1,249 @@ +import { gql } from 'graphql-modules'; + +export default gql` + extend type Mutation { + createSchemaProposal(input: CreateSchemaProposalInput!): SchemaProposal! + createSchemaProposalReview(input: CreateSchemaProposalReviewInput!): SchemaProposalReview! + createSchemaProposalComment(input: CreateSchemaProposalCommentInput!): SchemaProposalComment! + } + + input CreateSchemaProposalInput { + diffSchemaVersionId: ID! + title: String! + + """ + The initial changes by serviceName submitted as part of this proposal. Initial versions must have + unique "serviceName"s. + """ + initialVersions: [CreateSchemaProposalInitialVersionInput!]! = [] + + """ + The default initial stage is OPEN. Set this to true to create this as proposal + as a DRAFT instead. + """ + isDraft: Boolean! = false + } + + input CreateSchemaProposalInitialVersionInput { + schemaSDL: String! + serviceName: String + } + + input CreateSchemaProposalReviewInput { + schemaProposalVersionId: ID! + lineNumber: Int + """ + One or both of stageTransition or initialComment inputs is/are required. + """ + stageTransition: SchemaProposalStage + """ + One or both of stageTransition or initialComment inputs is/are required. + """ + commentBody: String + } + + input CreateSchemaProposalCommentInput { + schemaProposalReviewId: ID! + body: String! + } + + extend type Query { + schemaProposals( + after: String + first: Int! = 30 + input: SchemaProposalsInput + ): SchemaProposalConnection + schemaProposal(input: SchemaProposalInput!): SchemaProposal + schemaProposalReviews( + after: String + first: Int! = 30 + input: SchemaProposalReviewsInput! + ): SchemaProposalReviewConnection + schemaProposalReview(input: SchemaProposalReviewInput!): SchemaProposalReview + } + + input SchemaProposalsInput { + target: TargetReferenceInput! + userIds: [ID!] + stages: [SchemaProposalStage!] + } + + input SchemaProposalInput { + id: ID! + } + + input SchemaProposalReviewInput { + id: ID! + } + + input SchemaProposalReviewsInput { + schemaProposalId: ID! + } + + extend type User { + id: ID! + } + + extend type Target { + id: ID! + } + + type SchemaProposalConnection { + edges: [SchemaProposalEdge!] + pageInfo: PageInfo! + } + + type SchemaProposalEdge { + cursor: String! + node: SchemaProposal! + } + + extend type SchemaVersion { + id: ID! + } + + enum SchemaProposalStage { + DRAFT + OPEN + APPROVED + IMPLEMENTED + CLOSED + } + + type SchemaProposal { + id: ID! + createdAt: DateTime! + diffSchema: SchemaVersion + reviews(after: String, first: Int! = 30): SchemaProposalReviewConnection + stage: SchemaProposalStage! + target: Target + title: String + updatedAt: DateTime! + user: User + versions( + after: String + first: Int! = 15 + input: SchemaProposalVersionsInput + ): SchemaProposalVersionConnection + commentsCount: Int! + } + + type SchemaProposalReviewEdge { + cursor: String! + node: SchemaProposalReview! + } + + type SchemaProposalReviewConnection { + edges: [SchemaProposalReviewEdge!] + pageInfo: PageInfo! + } + + type SchemaProposalVersionEdge { + cursor: String! + node: SchemaProposalVersion! + } + + type SchemaProposalVersionConnection { + edges: [SchemaProposalVersionEdge!] + pageInfo: PageInfo! + } + + input SchemaProposalVersionsInput { + onlyLatest: Boolean! = false + } + + type SchemaProposalVersion { + id: ID! + createdAt: DateTime! + schemaProposal: SchemaProposal! + schemaSDL: String! + serviceName: String + user: ID + reviews(after: String, first: Int! = 30): SchemaProposalReviewConnection + } + + 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! + + """ + If the "lineText" can be found in the referenced SchemaProposalVersion, + then the "lineNumber" will be for that version. If there is no matching + "lineText", then this "lineNumber" will reference the originally + reviewed version of the proposal, and will be considered outdated. + + If the "lineNumber" is null then this review references the entire SchemaProposalVersion + and not any specific line. + """ + lineNumber: Int + + """ + 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 + + """ + The specific version 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 author of this review. + """ + user: User + } + + type SchemaProposalComment { + id: ID! + + createdAt: DateTime! + + """ + Content of this comment. E.g. "Nice job!" + """ + body: String! + + updatedAt: DateTime! + + """ + The author of this comment + """ + user: User + } +`; 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..cb75602425 --- /dev/null +++ b/packages/services/api/src/modules/proposals/resolvers/Mutation/createSchemaProposal.ts @@ -0,0 +1,16 @@ +import type { MutationResolvers } from './../../../../__generated__/types'; + +export const createSchemaProposal: NonNullable = async ( + _parent, + _arg, + _ctx, +) => { + /* Implement Mutation.createSchemaProposal resolver logic here */ + return { + createdAt: Date.now(), + commentsCount: 5, + id: `abcd-1234-efgh-5678-wxyz`, + stage: 'DRAFT', + updatedAt: Date.now(), + }; +}; diff --git a/packages/services/api/src/modules/proposals/resolvers/Mutation/createSchemaProposalComment.ts b/packages/services/api/src/modules/proposals/resolvers/Mutation/createSchemaProposalComment.ts new file mode 100644 index 0000000000..b6bdea4675 --- /dev/null +++ b/packages/services/api/src/modules/proposals/resolvers/Mutation/createSchemaProposalComment.ts @@ -0,0 +1,13 @@ +import type { MutationResolvers } from './../../../../__generated__/types'; + +export const createSchemaProposalComment: NonNullable< + MutationResolvers['createSchemaProposalComment'] +> = async (_parent, { input: { body } }, _ctx) => { + /* Implement Mutation.createSchemaProposalComment 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/createSchemaProposalReview.ts b/packages/services/api/src/modules/proposals/resolvers/Mutation/createSchemaProposalReview.ts new file mode 100644 index 0000000000..251ffa65e4 --- /dev/null +++ b/packages/services/api/src/modules/proposals/resolvers/Mutation/createSchemaProposalReview.ts @@ -0,0 +1,18 @@ +import type { MutationResolvers } from './../../../../__generated__/types'; + +export const createSchemaProposalReview: NonNullable< + MutationResolvers['createSchemaProposalReview'] +> = async (_parent, { input: { stageTransition, commentBody } }, _ctx) => { + /* Implement Mutation.createSchemaProposalReview resolver logic here */ + return { + createdAt: Date.now(), + id: `abcd-1234-efgh-5678-wxyz`, + schemaProposal: { + stage: stageTransition ?? 'OPEN', + commentsCount: commentBody ? 1 : 0, + createdAt: Date.now(), + id: `abcd-1234-efgh-5678-wxyz`, + updatedAt: Date.now(), + }, + }; +}; 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..defdf93f96 --- /dev/null +++ b/packages/services/api/src/modules/proposals/resolvers/Query/schemaProposal.ts @@ -0,0 +1,62 @@ +import type { QueryResolvers } from './../../../../__generated__/types'; + +export const schemaProposal: NonNullable = async ( + _parent, + { input: { id } }, + _ctx, +) => { + /* Implement Query.schemaProposal resolver logic here */ + return { + createdAt: Date.now(), + id, + stage: 'OPEN', + updatedAt: Date.now(), + commentsCount: 5, + title: 'This adds some stuff to the thing.', + user: { + id: 'asdffff', + displayName: 'jdolle', + fullName: 'Jeff Dolle', + email: 'jdolle+test@the-guild.dev', + }, + reviews: { + edges: [ + { + cursor: 'asdf', + node: { + id: '1', + comments: { + pageInfo: { + endCursor: crypto.randomUUID(), + startCursor: crypto.randomUUID(), + hasNextPage: false, + hasPreviousPage: false, + }, + edges: [ + { + cursor: crypto.randomUUID(), + node: { + id: crypto.randomUUID(), + createdAt: Date.now(), + body: 'This is a comment. The first comment.', + updatedAt: Date.now(), + }, + }, + ], + }, + createdAt: Date.now(), + lineText: 'type User {', + lineNumber: 2, + stageTransition: 'OPEN', + }, + }, + ], + pageInfo: { + startCursor: 'asdf', + endCursor: 'wxyz', + hasNextPage: false, + hasPreviousPage: false, + }, + }, + }; +}; diff --git a/packages/services/api/src/modules/proposals/resolvers/Query/schemaProposalReview.ts b/packages/services/api/src/modules/proposals/resolvers/Query/schemaProposalReview.ts new file mode 100644 index 0000000000..af1cc84e47 --- /dev/null +++ b/packages/services/api/src/modules/proposals/resolvers/Query/schemaProposalReview.ts @@ -0,0 +1,32 @@ +import type { QueryResolvers } from './../../../../__generated__/types'; + +export const schemaProposalReview: NonNullable = async ( + _parent, + { input: { id } }, + _ctx, +) => { + /* Implement Query.schemaProposalReview resolver logic here */ + return { + createdAt: Date.now(), + id, + schemaProposal: { + id: crypto.randomUUID(), + createdAt: Date.now(), + commentsCount: 3, + stage: 'OPEN', + updatedAt: Date.now(), + comments: { + edges: [ + { + cursor: crypto.randomUUID(), + node: { + id: crypto.randomUUID(), + body: 'This is a comment. The first comment.', + updatedAt: Date.now(), + }, + }, + ], + }, + }, + }; +}; diff --git a/packages/services/api/src/modules/proposals/resolvers/Query/schemaProposalReviews.ts b/packages/services/api/src/modules/proposals/resolvers/Query/schemaProposalReviews.ts new file mode 100644 index 0000000000..2ee7be2903 --- /dev/null +++ b/packages/services/api/src/modules/proposals/resolvers/Query/schemaProposalReviews.ts @@ -0,0 +1,40 @@ +import type { QueryResolvers } from './../../../../__generated__/types'; + +export const schemaProposalReviews: NonNullable = async ( + _parent, + _arg, + _ctx, +) => { + /* Implement Query.schemaProposalReviews resolver logic here */ + return { + edges: [ + { + node: { + id: crypto.randomUUID(), + createdAt: Date.now(), + lineNumber: 3, + schemaCoordinate: 'User', + lineText: 'type User {', + comments: { + edges: [ + { + cursor: crypto.randomUUID(), + node: { + id: crypto.randomUUID(), + body: 'This is a comment. The first comment.', + updatedAt: Date.now(), + }, + }, + ], + }, + }, + }, + ], + pageInfo: { + endCursor: '', + hasNextPage: false, + hasPreviousPage: false, + startCursor: '', + }, + }; +}; 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..256bef961b --- /dev/null +++ b/packages/services/api/src/modules/proposals/resolvers/Query/schemaProposals.ts @@ -0,0 +1,52 @@ +import type { QueryResolvers } from './../../../../__generated__/types'; + +export const schemaProposals: NonNullable = async ( + _parent, + _arg, + _ctx, +) => { + /* Implement Query.schemaProposals resolver logic here */ + const edges = Array.from({ length: 10 }).map(() => ({ + cursor: crypto.randomUUID(), + node: { + id: crypto.randomUUID(), + createdAt: Date.now(), + stage: 'DRAFT' as const, + updatedAt: Date.now(), + title: 'Add user types to registration service.', + user: { + displayName: 'jdolle', + fullName: 'Jeff Dolle', + id: crypto.randomUUID(), + } as any, + commentsCount: 7, + }, + })); + + return { + edges: edges.map((e: any, i) => { + if (i == 2) { + return { + ...e, + node: { + ...e.node, + title: + "Does some other things as well as this has a long time that should be truncated. So let's see what happens", + stage: 'OPEN' as const, + commentsCount: 3, + user: { + ...e.node.user, + }, + }, + }; + } + return e; + }), + pageInfo: { + endCursor: '', + hasNextPage: false, + hasPreviousPage: false, + startCursor: '', + }, + } as any; +}; diff --git a/packages/services/api/src/modules/proposals/resolvers/SchemaVersion.ts b/packages/services/api/src/modules/proposals/resolvers/SchemaVersion.ts new file mode 100644 index 0000000000..3e26ca5881 --- /dev/null +++ b/packages/services/api/src/modules/proposals/resolvers/SchemaVersion.ts @@ -0,0 +1,14 @@ +import type { SchemaVersionResolvers } from './../../../__generated__/types'; + +/* + * Note: This object type is generated because "SchemaVersionMapper" is declared. This is to ensure runtime safety. + * + * When a mapper is used, it is possible to hit runtime errors in some scenarios: + * - given a field name, the schema type's field type does not match mapper's field type + * - or a schema type's field does not exist in the mapper's fields + * + * If you want to skip this file generation, remove the mapper or update the pattern in the `resolverGeneration.object` config. + */ +export const SchemaVersion: Pick = { + /* Implement SchemaVersion resolver logic here */ +}; diff --git a/packages/services/api/src/modules/proposals/resolvers/Target.ts b/packages/services/api/src/modules/proposals/resolvers/Target.ts new file mode 100644 index 0000000000..c1fb215e2c --- /dev/null +++ b/packages/services/api/src/modules/proposals/resolvers/Target.ts @@ -0,0 +1,14 @@ +import type { TargetResolvers } from './../../../__generated__/types'; + +/* + * Note: This object type is generated because "TargetMapper" is declared. This is to ensure runtime safety. + * + * When a mapper is used, it is possible to hit runtime errors in some scenarios: + * - given a field name, the schema type's field type does not match mapper's field type + * - or a schema type's field does not exist in the mapper's fields + * + * If you want to skip this file generation, remove the mapper or update the pattern in the `resolverGeneration.object` config. + */ +export const Target: Pick = { + /* Implement Target resolver logic here */ +}; diff --git a/packages/services/api/src/modules/schema/resolvers/SchemaVersion.ts b/packages/services/api/src/modules/schema/resolvers/SchemaVersion.ts index 69f12e184a..a20166c9bf 100644 --- a/packages/services/api/src/modules/schema/resolvers/SchemaVersion.ts +++ b/packages/services/api/src/modules/schema/resolvers/SchemaVersion.ts @@ -9,7 +9,32 @@ import { SchemaManager } from '../providers/schema-manager'; import { SchemaVersionHelper } from '../providers/schema-version-helper'; import type { SchemaVersionResolvers } from './../../../__generated__/types'; -export const SchemaVersion: SchemaVersionResolvers = { +export const SchemaVersion: Pick< + SchemaVersionResolvers, + | 'baseSchema' + | 'breakingSchemaChanges' + | 'contractVersions' + | 'date' + | 'deprecatedSchema' + | 'explorer' + | 'githubMetadata' + | 'hasSchemaChanges' + | 'isComposable' + | 'isFirstComposableVersion' + | 'isValid' + | 'log' + | 'previousDiffableSchemaVersion' + | 'safeSchemaChanges' + | 'schemaChanges' + | 'schemaCompositionErrors' + | 'schemas' + | 'sdl' + | 'supergraph' + | 'tags' + | 'unusedSchema' + | 'valid' + | '__isTypeOf' +> = { isComposable: version => { return version.schemaCompositionErrors === null; }, diff --git a/packages/services/api/src/modules/target/resolvers/Target.ts b/packages/services/api/src/modules/target/resolvers/Target.ts index 8436523476..a9ad64474a 100644 --- a/packages/services/api/src/modules/target/resolvers/Target.ts +++ b/packages/services/api/src/modules/target/resolvers/Target.ts @@ -10,7 +10,6 @@ export const Target: Pick< | 'experimental_forcedLegacySchemaComposition' | 'failDiffOnDangerousChange' | 'graphqlEndpointUrl' - | 'id' | 'name' | 'project' | 'slug' diff --git a/packages/services/storage/src/db/types.ts b/packages/services/storage/src/db/types.ts index 2d563d08ef..1920119761 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 { @@ -318,6 +319,46 @@ 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; + original_line_num: number | null; + original_schema_proposal_version_id: string; + schema_proposal_id: string; + stage_transition: schema_proposal_stage; + user_id: string | null; +} + +export interface schema_proposal_versions { + created_at: Date; + id: string; + schema_proposal_id: string; + schema_sdl: string; + service_name: string | null; + user_id: string | null; +} + +export interface schema_proposals { + created_at: Date; + diff_schema_version_id: 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 +492,10 @@ 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_proposal_versions: schema_proposal_versions; + 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/schema-change-model.ts b/packages/services/storage/src/schema-change-model.ts index 10106fe083..f48eb3be6b 100644 --- a/packages/services/storage/src/schema-change-model.ts +++ b/packages/services/storage/src/schema-change-model.ts @@ -281,6 +281,7 @@ export const DirectiveArgumentAddedModel = implement 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 && ( ; + lineNumber: number; +}) { + const review = useFragment(ProposalOverview_ReviewCommentsFragment, props.review); + if (!review.comments) { + return null; + } + + return ( +
+ {review.comments?.edges?.map(({ node: comment }, idx) => { + return ; + })} +
+ ); +} + +const ProposalOverview_CommentFragment = graphql(/** GraphQL */ ` + fragment ProposalOverview_CommentFragment on SchemaProposalComment { + id + user { + id + email + displayName + fullName + } + body + updatedAt + } +`); + +export function ReviewComment(props: { + first?: boolean; + comment: FragmentType; +}) { + const comment = useFragment(ProposalOverview_CommentFragment, props.comment); + return ( + <> +
+
+ {comment.user?.displayName ?? comment.user?.fullName ?? 'Unknown'} +
+
+ +
+
+
{comment.body}
+ + ); +} diff --git a/packages/web/app/src/components/proposal/__tests__/collect-coordinate-locations.spec.ts b/packages/web/app/src/components/proposal/__tests__/collect-coordinate-locations.spec.ts new file mode 100644 index 0000000000..c1bec574a3 --- /dev/null +++ b/packages/web/app/src/components/proposal/__tests__/collect-coordinate-locations.spec.ts @@ -0,0 +1,109 @@ +import { buildSchema, Source } from 'graphql'; +import { collectCoordinateLocations } from '../collect-coordinate-locations'; + +const coordinatesFromSDL = (sdl: string) => { + const schema = buildSchema(sdl); + return collectCoordinateLocations(schema, new Source(sdl)); +}; + +describe('schema coordinate location collection', () => { + describe('should include the location of', () => { + test('types', () => { + const sdl = /** GraphQL */ ` + type Query { + foo: Foo + } + type Foo { + id: ID! + } + `; + const coords = coordinatesFromSDL(sdl); + expect(coords.get('Foo')).toBe(5); + }); + + test('fields', () => { + const sdl = /** GraphQL */ ` + type Query { + foo: Foo + } + + type Foo { + id: ID! + } + `; + const coords = coordinatesFromSDL(sdl); + expect(coords.get('Query.foo')).toBe(3); + }); + + test('arguments', () => { + const sdl = /** GraphQL */ ` + type Query { + foo(bar: Boolean): Boolean + } + `; + const coords = coordinatesFromSDL(sdl); + expect(coords.get('Query.foo.bar')).toBe(3); + }); + + test('scalars', () => { + const sdl = /** GraphQL */ ` + scalar Foo + type Query { + foo(bar: Boolean): Boolean + } + `; + const coords = coordinatesFromSDL(sdl); + expect(coords.get('Foo')).toBe(2); + }); + + test('enums', () => { + const sdl = /** GraphQL */ ` + enum Foo { + FIRST + SECOND + THIRD + } + type Query { + foo(bar: Boolean): Foo + } + `; + const coords = coordinatesFromSDL(sdl); + expect(coords.get('Foo')).toBe(2); + expect(coords.get('Foo.FIRST')).toBe(3); + expect(coords.get('Foo.SECOND')).toBe(4); + expect(coords.get('Foo.THIRD')).toBe(5); + }); + + test('unions', () => { + const sdl = /** GraphQL */ ` + union Foo = + | Bar + | Blar + type Bar { + bar: Boolean + } + type Blar { + blar: String + } + type Query { + foo: Foo + } + `; + const coords = coordinatesFromSDL(sdl); + expect(coords.get('Foo')).toBe(2); + // @note The AST is limited and does not give the location of union values. + // expect(coords.get('Foo.Bar')).toBe(2); + // expect(coords.get('Foo.Blar')).toBe(2); + }); + + test('subscriptions', () => { + const sdl = /** GraphQL */ ` + type Subscription { + foo: String + } + `; + const coords = coordinatesFromSDL(sdl); + expect(coords.get('Subscription.foo')).toBe(3); + }); + }); +}); diff --git a/packages/web/app/src/components/proposal/collect-coordinate-locations.ts b/packages/web/app/src/components/proposal/collect-coordinate-locations.ts new file mode 100644 index 0000000000..9ec11cbe48 --- /dev/null +++ b/packages/web/app/src/components/proposal/collect-coordinate-locations.ts @@ -0,0 +1,106 @@ +import { + getLocation, + GraphQLArgument, + GraphQLEnumType, + GraphQLField, + GraphQLInputObjectType, + GraphQLInterfaceType, + GraphQLNamedType, + GraphQLObjectType, + GraphQLSchema, + GraphQLUnionType, + isIntrospectionType, + Location, + Source, +} from 'graphql'; + +export function collectCoordinateLocations( + schema: GraphQLSchema, + source: Source, +): Map { + const coordinateToLine = new Map(); + + const collectObjectType = ( + type: GraphQLObjectType | GraphQLInputObjectType | GraphQLInterfaceType, + ) => { + collect(type.name, type.astNode?.loc); + const fields = type.getFields(); + if (fields) { + for (const field of Object.values(fields)) { + collectField(type, field); + } + } + }; + + const collect = (coordinate: string, location: Location | undefined) => { + const sourceLoc = location && getLocation(source, location.start); + if (sourceLoc?.line) { + coordinateToLine.set(coordinate, sourceLoc.line); + } else { + console.warn(`Location not found for "${coordinate}"`); + } + }; + + const collectEnumType = (type: GraphQLEnumType) => { + collect(type.name, type.astNode?.loc); + for (const val of type.getValues()) { + const coord = `${type.name}.${val.name}`; + collect(coord, val.astNode?.loc); + } + }; + + const collectUnionType = (type: GraphQLUnionType) => { + collect(type.name, type.astNode?.loc); + // for (const unionType of type.getTypes()) { + // const coordinate = `${type.name}.${unionType.name}`; + // collect(coordinate, type.astNode?.loc); + // } + }; + + const collectNamedType = (type: GraphQLNamedType) => { + if (isIntrospectionType(type)) { + return; + } + + if ( + type instanceof GraphQLObjectType || + type instanceof GraphQLInputObjectType || + type instanceof GraphQLInterfaceType + ) { + collectObjectType(type); + } else if (type instanceof GraphQLUnionType) { + collectUnionType(type); + } else if (type instanceof GraphQLEnumType) { + collectEnumType(type); + } else { + collect(type.name, type.astNode?.loc); + } + }; + + const collectArg = ( + type: GraphQLObjectType | GraphQLInputObjectType | GraphQLInterfaceType, + field: GraphQLField, + arg: GraphQLArgument, + ) => { + const coord = `${type.name}.${field.name}.${arg.name}`; + collect(coord, arg.astNode?.loc); + }; + + const collectField = ( + type: GraphQLObjectType | GraphQLInputObjectType | GraphQLInterfaceType, + field: GraphQLField, + ) => { + const coord = `${type.name}.${field.name}`; + collect(coord, field.astNode?.loc); + + for (const arg of field.args) { + collectArg(type, field, arg); + } + }; + + for (const named of Object.values(schema.getTypeMap())) { + collectNamedType(named); + } + + return coordinateToLine; +} diff --git a/packages/web/app/src/components/proposal/print-diff/compare-lists.ts b/packages/web/app/src/components/proposal/print-diff/compare-lists.ts new file mode 100644 index 0000000000..6df6613a3b --- /dev/null +++ b/packages/web/app/src/components/proposal/print-diff/compare-lists.ts @@ -0,0 +1,125 @@ +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; +} + +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/proposal/print-diff/components.tsx b/packages/web/app/src/components/proposal/print-diff/components.tsx new file mode 100644 index 0000000000..c707ca436c --- /dev/null +++ b/packages/web/app/src/components/proposal/print-diff/components.tsx @@ -0,0 +1,1090 @@ +/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ +import React, { ReactElement, ReactNode } 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 { cn } from '@/lib/utils'; +import { compareLists, diffArrays, matchArrays } from './compare-lists'; + +type RootFieldsType = { + query: GraphQLField; + mutation: GraphQLField; + subscription: GraphQLField; +}; + +const TAB = <>  ; + +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'; + indent?: boolean | number; +}) { + const incrementCounter = + props.type === 'mutual' || props.type === undefined + ? 'olddoc newdoc' + : props.type === 'removal' + ? 'olddoc' + : 'newdoc'; + return ( + + + + + + {props.indent && + Array.from({ length: Number(props.indent) }).map((_, i) => ( + {TAB} + ))} + {props.children} + + + + ); +} + +function Keyword(props: { term: string }) { + return {props.term}; +} + +function Removal(props: { + children: React.ReactNode | string; + className?: string; +}): React.ReactNode { + return {props.children}; +} + +function Addition(props: { children: React.ReactNode; className?: string }): React.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; +}): React.ReactNode { + const lines = props.content.split('\n'); + + return ( + <> + {lines.map((line, index) => ( + + + {line} + + + ))} + + ); +} + +function FieldName(props: { name: string }): React.ReactNode { + return {props.name}; +} + +function FieldReturnType(props: { returnType: string }): React.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 && ( + + )} + {newDesc && ( + + )} + + ); + } + if (newDesc) { + return ; + } +} + +export function DiffInputField({ + oldField, + newField, +}: + | { + oldField: GraphQLInputField | null; + newField: GraphQLInputField; + } + | { + oldField: GraphQLInputField; + newField: GraphQLInputField | null; + }) { + const changeType = determineChangeType(oldField, newField); + return ( + <> + + + + + :  + + + + + ); +} + +function Change(props: { + children: ReactElement; + type?: 'addition' | 'removal' | 'mutual'; +}): ReactElement { + if (props.type === 'addition') { + return {props.children} + } + if (props.type === 'removal') { + return {props.children} + } + return props.children +} + +export function DiffField({ + oldField, + newField, +}: + | { + oldField: GraphQLField | null; + newField: GraphQLField; + } + | { + oldField: GraphQLField; + newField: GraphQLField | 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 changeType = determineChangeType(oldField, newField); + const AfterArguments = ( + <> + :  + + + + ); + return ( + <> + + + + {hasArgs && <>(} + {!hasArgs && AfterArguments} + + + {!!hasArgs && ( + + <>){AfterArguments} + + )} + + ); +} + +export function DirectiveName(props: { name: string }) { + return @{props.name}; +} + +export function DiffArguments(props: { + oldArgs: readonly GraphQLArgument[]; + newArgs: readonly GraphQLArgument[]; + indent: boolean | number; +}) { + 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; + } + | { + oldDirective: GraphQLDirective; + newDirective: GraphQLDirective | null; + }, +) { + 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 = ( + <> +   + + + + ); + 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, +}: { + oldSchema: GraphQLSchema; + newSchema: GraphQLSchema; +}) { + 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: {}, + }, + }; + + return ( + <> + + + {' {'} + + + + + {'}'} + + ); +} + +/** For any named type */ +export function DiffType({ + oldType, + newType, +}: + | { + oldType: GraphQLNamedType; + newType: GraphQLNamedType | null; + } + | { + oldType: GraphQLNamedType | null; + newType: GraphQLNamedType; + }) { + 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, +}: + | { + oldInput: GraphQLInputObjectType | null; + newInput: GraphQLInputObjectType; + } + | { + oldInput: GraphQLInputObjectType; + newInput: GraphQLInputObjectType | null; + }) { + const { added, mutual, removed } = compareLists( + Object.values(oldInput?.getFields() ?? {}), + Object.values(newInput?.getFields() ?? {}), + ); + const changeType = determineChangeType(oldInput, newInput); + return ( + <> + + + + +   + + + {' {'} + + {removed.map(a => ( + + ))} + {added.map(a => ( + + ))} + {mutual.map(a => ( + + ))} + {'}'} + + ); +} + +export function DiffObject({ + oldObject, + newObject, +}: + | { + oldObject: GraphQLObjectType | GraphQLInterfaceType | null; + newObject: GraphQLObjectType | GraphQLInterfaceType; + } + | { + oldObject: GraphQLObjectType | GraphQLInterfaceType; + newObject: GraphQLObjectType | GraphQLInterfaceType | null; + }) { + const { added, mutual, removed } = compareLists( + Object.values(oldObject?.getFields() ?? {}), + Object.values(newObject?.getFields() ?? {}), + ); + const changeType = determineChangeType(oldObject, newObject); + return ( + <> + + + + +   + + + {' {'} + + {removed.map(a => ( + + ))} + {added.map(a => ( + + ))} + {mutual.map(a => ( + + ))} + {'}'} + + ); +} + +export function DiffEnumValue({ + oldValue, + newValue, +}: { + oldValue: GraphQLEnumValue | null; + newValue: GraphQLEnumValue | null; +}) { + const changeType = determineChangeType(oldValue, newValue); + const name = oldValue?.name ?? newValue?.name ?? ''; + return ( + <> + + + + + + + ); +} + +export function DiffEnum({ + oldEnum, + newEnum, +}: { + oldEnum: GraphQLEnumType | null; + newEnum: GraphQLEnumType | null; +}) { + const { added, mutual, removed } = compareLists( + oldEnum?.getValues() ?? [], + newEnum?.getValues() ?? [], + ); + + const changeType = determineChangeType(oldEnum, newEnum); + + return ( + <> + + + + +   + + {' {'} + + {removed.map(a => ( + + ))} + {added.map(a => ( + + ))} + {mutual.map(a => ( + + ))} + {'}'} + + ); +} + +export function DiffUnion({ + oldUnion, + newUnion, +}: { + oldUnion: GraphQLUnionType | null; + newUnion: GraphQLUnionType | null; +}) { + const { added, mutual, removed } = compareLists( + oldUnion?.getTypes() ?? [], + newUnion?.getTypes() ?? [], + ); + + const changeType = determineChangeType(oldUnion, newUnion); + const name = oldUnion?.name ?? newUnion?.name ?? ''; + return ( + <> + + + + +   + + + {' = '} + + {removed.map(a => ( + + + | + + + ))} + {added.map(a => ( + + + | + + + ))} + {mutual.map(a => ( + + + | + + + ))} + + ); +} + +export function DiffScalar({ + oldScalar, + newScalar, +}: + | { + oldScalar: GraphQLScalarType; + newScalar: GraphQLScalarType | null; + } + | { + oldScalar: GraphQLScalarType | null; + newScalar: GraphQLScalarType; + }) { + const changeType = determineChangeType(oldScalar, newScalar); + if (oldScalar?.name === newScalar?.name) { + return ( + <> + + + + +   + + + + + ); + } + return ( + + +   + {oldScalar && ( + + + + )} + {newScalar && ( + + + + )} + + + ); +} + +export function DiffDirectiveUsages(props: { + oldDirectives: readonly ConstDirectiveNode[]; + newDirectives: readonly ConstDirectiveNode[]; +}) { + const { added, mutual, removed } = compareLists(props.oldDirectives, props.newDirectives); + + return ( + <> + {removed.map(d => ( + + ))} + {added.map(d => ( + + ))} + {mutual.map(d => ( + + ))} + + ); +} + +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 Klass = + changeType === 'addition' ? Addition : changeType === 'removal' ? Removal : React.Fragment; + const { added, mutual, removed } = compareLists(oldArgs, newArgs); + const argumentElements = [ + ...removed.map(r => ), + ...added.map(r => ), + ...mutual.map(r => ), + ]; + + return ( + +   + + {hasArgs && <>(} + {argumentElements.map((e, index) => ( + + {e} + {index === argumentElements.length - 1 ? '' : ', '} + + ))} + {hasArgs && <>)} + + ); +} + +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); + + const DiffType = ({ + oldType, + newType, + }: { + oldType: string | null; + newType: string | null; + }): ReactNode => { + if (oldType === newType) { + return newType; + } + return ( + <> + {oldType && {oldType}} + {newType && {newType}} + + ); + }; + + return ( + <> + + :  + + + ); +} diff --git a/packages/web/app/src/components/proposal/print-diff/print-diff.tsx b/packages/web/app/src/components/proposal/print-diff/print-diff.tsx new file mode 100644 index 0000000000..65cbadbdee --- /dev/null +++ b/packages/web/app/src/components/proposal/print-diff/print-diff.tsx @@ -0,0 +1,54 @@ +/* eslint-disable tailwindcss/no-custom-classname */ +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 printSchemaDiff(oldSchema: GraphQLSchema, newSchema: GraphQLSchema): JSX.Element { + const { + added: addedTypes, + mutual: mutualTypes, + removed: removedTypes, + } = compareLists( + Object.values(oldSchema.getTypeMap()).filter(t => !isPrimitive(t) && !isIntrospectionType(t)), + Object.values(newSchema.getTypeMap()).filter(t => !isPrimitive(t) && !isIntrospectionType(t)), + ); + + const { + added: addedDirectives, + mutual: mutualDirectives, + removed: removedDirectives, + } = compareLists( + oldSchema.getDirectives().filter(d => !isSpecifiedDirective(d)), + newSchema.getDirectives().filter(d => !isSpecifiedDirective(d)), + ); + + return ( + + {removedDirectives.map(d => ( + + ))} + {addedDirectives.map(d => ( + + ))} + {mutualDirectives.map(d => ( + + ))} + + {addedTypes.map(a => ( + + ))} + {removedTypes.map(a => ( + + ))} + {mutualTypes.map(a => ( + + ))} + + ); +} diff --git a/packages/web/app/src/components/proposal/proposal-sdl.tsx b/packages/web/app/src/components/proposal/proposal-sdl.tsx new file mode 100644 index 0000000000..550f293c35 --- /dev/null +++ b/packages/web/app/src/components/proposal/proposal-sdl.tsx @@ -0,0 +1,256 @@ +import { buildSchema } from 'graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import type { Change } from '@graphql-inspector/core'; +import { printSchemaDiff } from './print-diff/print-diff'; + +const ProposalOverview_ReviewsFragment = graphql(/** GraphQL */ ` + fragment ProposalOverview_ReviewsFragment on SchemaProposalReviewConnection { + pageInfo { + startCursor + } + edges { + cursor + node { + id + schemaProposalVersion { + id + serviceName + # schemaSDL + } + stageTransition + lineNumber + lineText + schemaCoordinate + ...ProposalOverview_ReviewCommentsFragment + } + } + } +`); + +// @todo should this be done on proposal update AND then the proposal can reference the check???? +// So it can get the changes + +// const ProposalOverview_CheckSchema = graphql(/** GraphQL */` +// mutation ProposalOverview_CheckSchema($target: TargetReferenceInput!, $sdl: String!, $service: ID) { +// schemaCheck(input: { +// target: $target +// sdl: $sdl +// service: $service +// }) { +// __typename +// ...on SchemaCheckSuccess { + +// } +// ...on SchemaCheckError { +// changes { +// edges { +// node { +// path +// } +// } +// } +// } +// } +// } +// `); + +// type ReviewNode = NonNullable[number]['node']; + +export function ProposalSDL(props: { + baseSchemaSDL: string; + changes: Change[]; + serviceName?: string; + latestProposalVersionId: string; + reviews: FragmentType | null; +}) { + /** + * 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 _reviewsConnection = useFragment(ProposalOverview_ReviewsFragment, props.reviews); + + try { + // @todo use props.baseSchemaSDL + const baseSchemaSDL = /* GraphQL */ ` + """ + This is old + """ + directive @old on FIELD + + directive @foo on OBJECT + + "Doesn't change" + type Query { + okay: Boolean @deprecated + dokay: Boolean + } + `; + + const patchedSchemaSDL = /* GraphQL */ ` + """ + Custom scalar that can represent any valid JSON + """ + scalar JSON + + directive @foo repeatable on OBJECT | FIELD + + """ + Enhances fields with meta data + """ + directive @meta( + "The metadata key" + name: String! + "The value of the metadata" + content: String! + ) on FIELD + + "Doesn't change" + type Query { + ok: Boolean @meta(name: "team", content: "hive") + + """ + This is a new description on a field + """ + dokay(foo: String = "What"): Boolean! + } + + "Yups" + enum Status { + OKAY + """ + Hi + """ + SMOKAY + } + + """ + Crusty flaky delicious goodness. + """ + type Pie { + name: String! + flavor: String! + slices: Int + } + + """ + Delicious baked flour based product + """ + type Cake { + name: String! + flavor: String! + tiers: Int! + } + + input FooInput { + """ + Hi + """ + asdf: String @foo + } + + union Dessert = Pie | Cake + `; // APPLY PATCH + + return printSchemaDiff( + buildSchema(baseSchemaSDL, { assumeValid: true, assumeValidSDL: true }), + buildSchema(patchedSchemaSDL, { assumeValid: true, assumeValidSDL: true }), + ); + + // // @note assume reviews are specific to the current service... + // const globalReviews: ReviewNode[] = []; + // const reviewsByLine = new Map(); + // const serviceReviews = + // reviewsConnection?.edges?.filter(edge => { + // const { schemaProposalVersion } = edge.node; + // return schemaProposalVersion?.serviceName === props.serviceName; + // }) ?? []; + + // for (const edge of serviceReviews) { + // const { lineNumber, schemaCoordinate, schemaProposalVersion } = edge.node; + // const coordinateLine = !!schemaCoordinate && coordinateToLineMap.get(schemaCoordinate); + // const isStale = + // !coordinateLine && schemaProposalVersion?.id !== props.latestProposalVersionId; + // const line = coordinateLine || lineNumber; + // if (line) { + // reviewsByLine.set(line, { ...edge.node, isStale }); + // } else { + // globalReviews.push(edge.node); + // } + // } + + // const baseSchemaSdlLines = baseSchemaSDL.split('\n'); + // let diffLineNumber = 0; + // return ( + // <> + // + // {patchedSchemaSDL.split('\n').flatMap((txt, index) => { + // const lineNumber = index + 1; + // const diffLineMatch = txt === baseSchemaSdlLines[diffLineNumber]; + // const elements = [ + // + // {txt} + // , + // ]; + // if (diffLineMatch) { + // diffLineNumber = diffLineNumber + 1; + // } + + // const review = reviewsByLine.get(lineNumber); + // if (review) { + // if (review.isStale) { + // elements.push( + // + // + // + //
+ // This review references an outdated version of the proposal. + //
+ // {!!review.lineText && ( + // + // + // {review.lineText} + // + // + // )} + // + // , + // ); + // } + // elements.push( + // + // + // + // + // + // , + // ); + // } + // return elements; + // })} + //
+ // {globalReviews.map(r => { + // return
{r.id}
; + // })} + // + // ); + // console.log(printJsx(document)); + } catch (e: unknown) { + return ( + <> +
Invalid SDL
+
{e instanceof Error ? e.message : String(e)}
+ + ); + } +} diff --git a/packages/web/app/src/components/proposal/util.ts b/packages/web/app/src/components/proposal/util.ts new file mode 100644 index 0000000000..0f1ac510af --- /dev/null +++ b/packages/web/app/src/components/proposal/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/stage-filter.tsx b/packages/web/app/src/components/target/proposals/stage-filter.tsx new file mode 100644 index 0000000000..52835642a7 --- /dev/null +++ b/packages/web/app/src/components/target/proposals/stage-filter.tsx @@ -0,0 +1,68 @@ +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 ( + + + + + + + + + {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/user-filter.tsx b/packages/web/app/src/components/target/proposals/user-filter.tsx new file mode 100644 index 0000000000..27ef2f59aa --- /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/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-edit.tsx b/packages/web/app/src/pages/target-proposal-edit.tsx new file mode 100644 index 0000000000..031dfce94b --- /dev/null +++ b/packages/web/app/src/pages/target-proposal-edit.tsx @@ -0,0 +1,9 @@ +export function TargetProposalEditPage(props: { + organizationSlug: string; + projectSlug: string; + targetSlug: string; + proposalId: string; + page: string; +}) { + return
Edit
; +} diff --git a/packages/web/app/src/pages/target-proposal-history.tsx b/packages/web/app/src/pages/target-proposal-history.tsx new file mode 100644 index 0000000000..bc7abc424c --- /dev/null +++ b/packages/web/app/src/pages/target-proposal-history.tsx @@ -0,0 +1,9 @@ +export function TargetProposalHistoryPage(props: { + organizationSlug: string; + projectSlug: string; + targetSlug: string; + proposalId: string; + page: string; +}) { + return
History
; +} diff --git a/packages/web/app/src/pages/target-proposal-layout.tsx b/packages/web/app/src/pages/target-proposal-layout.tsx new file mode 100644 index 0000000000..c719b6aab9 --- /dev/null +++ b/packages/web/app/src/pages/target-proposal-layout.tsx @@ -0,0 +1,84 @@ +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Link } from '@tanstack/react-router'; +import { TargetProposalEditPage } from './target-proposal-edit'; +import { TargetProposalHistoryPage } from './target-proposal-history'; +import { TargetProposalOverviewPage } from './target-proposal-overview'; + +enum Page { + OVERVIEW = 'overview', + HISTORY = 'history', + EDIT = 'edit', +} + +export function TargetProposalLayoutPage(props: { + organizationSlug: string; + projectSlug: string; + targetSlug: string; + proposalId: string; + page: string; +}) { + return ( +
+ + + + + Overview + + + + + History + + + + + Edit + + + + +
+ +
+
+ +
+ +
+
+ + + +
+
+ ); +} + +export const ProposalPage = Page; diff --git a/packages/web/app/src/pages/target-proposal-overview.tsx b/packages/web/app/src/pages/target-proposal-overview.tsx new file mode 100644 index 0000000000..2df7b0ab7a --- /dev/null +++ b/packages/web/app/src/pages/target-proposal-overview.tsx @@ -0,0 +1,265 @@ +import { useQuery } from 'urql'; +import { ProposalSDL } from '@/components/proposal/proposal-sdl'; +import { stageToColor, userText } from '@/components/proposal/util'; +import { Callout } from '@/components/ui/callout'; +import { Subtitle, Title } from '@/components/ui/page'; +import { Spinner } from '@/components/ui/spinner'; +import { Tag, TimeAgo } from '@/components/v2'; +import { graphql } from '@/gql'; + +const ProposalOverviewQuery = graphql(/** GraphQL */ ` + query ProposalOverviewQuery($id: ID!) { + latestVersion { + id + isValid + } + latestValidVersion { + id + sdl + schemas { + edges { + node { + ... on CompositeSchema { + id + source + service + } + ... on SingleSchema { + id + source + } + } + } + } + } + schemaProposal(input: { id: $id }) { + id + createdAt + updatedAt + commentsCount + stage + title + versions(input: { onlyLatest: true }) { + edges { + node { + id + schemaSDL + serviceName + } + } + } + user { + id + email + displayName + fullName + } + reviews { + ...ProposalOverview_ReviewsFragment + } + } + } +`); + +export function TargetProposalOverviewPage(props: { + organizationSlug: string; + projectSlug: string; + targetSlug: string; + proposalId: string; + page: string; +}) { + const [query] = useQuery({ + query: ProposalOverviewQuery, + variables: { + id: props.proposalId, + }, + requestPolicy: 'cache-and-network', + }); + + const proposal = query.data?.schemaProposal; + + // const diffSdl = /** GraphQL */ ` + // 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 + + // directive @myDirective(a: String!) on FIELD_DEFINITION + + // directive @hello on 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! @tag(name: "hi-from-products") + // sku: String @meta(name: "unique", content: "true") + // name: String @hello + // package: String + // variation: ProductVariation + // dimensions: ProductDimension + // createdBy: User + // hidden: String + // reviewsScore: Float! + // oldField: String + // } + + // 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 + // } + // `; + + const sdl = /** GraphQL */ ` + 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 + + directive @myDirective(a: String!) on FIELD_DEFINITION + + directive @hello on 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 + } + + interface SkuItf { + sku: String + } + + type Product implements ProductItf & SkuItf + @key(fields: "id") + @key(fields: "sku package") + @meta(name: "owner", content: "product-team") { + id: ID! @tag(name: "hi-from-products") + sku: String @meta(name: "unique", content: "true") + name: String @hello + package: String + variation: ProductVariation + dimensions: ProductDimension + createdBy: User + hidden: String + reviewsScore: Float! + } + + enum ShippingClass { + STANDARD + EXPRESS + OVERNIGHT + } + + type ProductVariation { + id: ID! + name: String + } + + type ProductDimension @shareable { + size: String + weight: Float + } + + type User @key(fields: "email") { + email: ID! + totalProductsCreated: Int @shareable + } + `; + + return ( +
+ {query.fetching && } + {proposal && ( + <> + + {userText(proposal.user)} proposed {' '} + +
+ {proposal.title} + {proposal.stage} +
+
+ Last updated +
+ {query.data?.latestVersion && query.data.latestVersion.isValid === false && ( + + The latest schema is invalid. Showing comparison against latest valid schema{' '} + {query.data.latestValidVersion?.id} + + )} + + + )} +
+ ); +} 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..c4020ef11b --- /dev/null +++ b/packages/web/app/src/pages/target-proposals.tsx @@ -0,0 +1,309 @@ +import { useEffect, useState } from 'react'; +import { useQuery } from 'urql'; +import { + ListNavigationProvider, + ListNavigationTrigger, + ListNavigationWrapper, + useListNavCollapsedToggle, + useListNavigationContext, +} from '@/components/common/ListNavigation'; +import { Page, TargetLayout } from '@/components/layouts/target'; +import { stageToColor } from '@/components/proposal/util'; +import { StageFilter } from '@/components/target/proposals/stage-filter'; +import { UserFilter } from '@/components/target/proposals/user-filter'; +import { BadgeRounded } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Link } from '@/components/ui/link'; +import { Meta } from '@/components/ui/meta'; +import { Subtitle, Title } from '@/components/ui/page'; +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, PinLeftIcon, PinRightIcon } from '@radix-ui/react-icons'; +import { Outlet, useRouter, useSearch } from '@tanstack/react-router'; + +export function TargetProposalsPage(props: { + organizationSlug: string; + projectSlug: string; + targetSlug: string; + filterUserIds?: string[]; + filterStages?: string[]; + selectedProposalId?: string; +}) { + return ( + <> + + + + + + ); +} + +const ProposalsQuery = graphql(` + query listProposals($input: SchemaProposalsInput) { + schemaProposals(input: $input) { + edges { + node { + id + title + stage + updatedAt + diffSchema { + id + } + user { + id + displayName + fullName + } + commentsCount + } + cursor + } + pageInfo { + endCursor + hasNextPage + } + } + } +`); + +const ProposalsContent = (props: Parameters[0]) => { + const isFullScreen = !props.selectedProposalId; + + return ( + +
+
+ + Proposals + + Collaborate on schema changes to reduce friction during development. +
+
+ +
+
+ {!isFullScreen && ( +
+ + + +
+ )} + } content={} /> +
+ ); +}; + +function HideMenuButton() { + return ( + + + + ); +} + +function ExpandMenuButton(props: { className?: string }) { + const { isListNavHidden } = useListNavigationContext(); + const [collapsed, toggle] = useListNavCollapsedToggle(); + + return isListNavHidden ? null : ( + + ); +} + +function ProposalsListv2(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); + + const hasHasProposalSelected = !!props.selectedProposalId; + const { setIsListNavCollapsed, isListNavCollapsed, setIsListNavHidden } = + useListNavigationContext(); + useEffect(() => { + if (props.selectedProposalId) { + setIsListNavCollapsed(true); + } else { + setIsListNavCollapsed(false); + setIsListNavHidden(false); + } + }, [props.selectedProposalId]); + + const isFiltersHorizontalUI = !hasHasProposalSelected || !isListNavCollapsed; + + 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: { + byId: 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 { isListNavCollapsed } = useListNavigationContext(); + const isWide = !props.selectedProposalId || !isListNavCollapsed; + + return ( + <> + {query.fetching ? : null} + {query.data?.schemaProposals?.edges?.map(({ node: proposal }) => { + return ( +
+ +
+
+
+ + {proposal.title} + + + + + {proposal.stage} +
+
+
+ proposed +
+ {proposal.user ? ( +
+ by {proposal.user.displayName ?? proposal.user.fullName} +
+ ) : 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..9e67575591 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,12 @@ 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 { + ProposalPage, + TargetProposalLayoutPage, + TargetProposalsViewPage, +} from './pages/target-proposal-layout'; +import { TargetProposalsPage } from './pages/target-proposals'; import { TargetSettingsPage, TargetSettingsPageEnum } from './pages/target-settings'; SuperTokens.init(frontendConfig()); @@ -820,6 +828,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 targetProposalRoute = createRoute({ + getParentRoute: () => targetProposalsRoute, + path: '$proposalId', + validateSearch: z.object({ + page: z + .enum(Object.values(ProposalPage).map(s => s.toLowerCase()) as [string, ...string[]]) + .optional() + .catch(() => void 0), + }), + component: function TargetProposalRoute() { + const { organizationSlug, projectSlug, targetSlug, proposalId } = + targetProposalRoute.useParams(); + const { page } = targetProposalRoute.useSearch(); + return ( + + ); + }, +}); + const routeTree = root.addChildren([ notFoundRoute, anonymousRoute.addChildren([ @@ -875,6 +940,7 @@ const routeTree = root.addChildren([ targetChecksRoute.addChildren([targetChecksSingleRoute]), targetAppVersionRoute, targetAppsRoute, + targetProposalsRoute.addChildren([targetProposalRoute]), ]), ]), ]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c74af3dddf..76acb6131e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,6 +121,9 @@ importers: '@graphql-inspector/cli': specifier: 4.0.3 version: 4.0.3(@types/node@22.10.5)(encoding@0.1.13)(graphql@16.9.0) + '@graphql-inspector/core': + specifier: ^6.0.0 + version: 6.2.1(graphql@16.9.0) '@manypkg/get-packages': specifier: 2.2.2 version: 2.2.2 @@ -213,10 +216,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 +376,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 +410,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 @@ -530,7 +533,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 +609,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 @@ -863,7 +866,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 +899,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 @@ -1669,7 +1672,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) @@ -1798,7 +1801,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 +1867,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 +2026,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 @@ -3103,11 +3106,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==} @@ -3563,6 +3566,7 @@ packages: '@fastify/vite@6.0.7': resolution: {integrity: sha512-+dRo9KUkvmbqdmBskG02SwigWl06Mwkw8SBDK1zTNH6vd4DyXbRvI7RmJEmBkLouSU81KTzy1+OzwHSffqSD6w==} + bundledDependencies: [] '@floating-ui/core@1.2.6': resolution: {integrity: sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==} @@ -3777,6 +3781,12 @@ packages: peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 + '@graphql-inspector/core@6.2.1': + resolution: {integrity: sha512-PxL3fNblfKx/h/B4MIXN1yGHsGdY+uuySz8MAy/ogDk7eU1+va2zDZicLMEBHf7nsKfHWCAN1WFtD1GQP824NQ==} + engines: {node: '>=18.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'} @@ -8260,6 +8270,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 +8684,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 +8808,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 +9225,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 +9598,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 +9632,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 +9784,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==} @@ -11833,68 +11862,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: @@ -12958,6 +12987,10 @@ packages: 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==} engines: {node: '>= 0.4'} @@ -13329,6 +13362,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'} @@ -15513,8 +15551,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' @@ -17978,20 +18016,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 @@ -18485,14 +18524,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 @@ -18945,6 +18984,13 @@ snapshots: object-inspect: 1.12.3 tslib: 2.6.2 + '@graphql-inspector/core@6.2.1(graphql@16.9.0)': + dependencies: + dependency-graph: 1.0.0 + graphql: 16.9.0 + object-inspect: 1.13.2 + 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) @@ -20014,11 +20060,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 +23944,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 +24012,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 +24026,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 @@ -24824,14 +24870,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 +24895,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: @@ -25044,6 +25090,9 @@ snapshots: acorn@8.14.0: {} + acorn@8.15.0: + optional: true + addressparser@1.0.1: {} agent-base@6.0.2: @@ -25490,12 +25539,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 +25707,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 +26152,8 @@ snapshots: create-require@1.1.1: {} + crelt@1.0.6: {} + cron-parser@4.9.0: dependencies: luxon: 3.5.0 @@ -26521,6 +26574,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 +26593,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 +26759,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: {} @@ -29253,50 +29310,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: {} @@ -30921,6 +30978,8 @@ snapshots: object-inspect@1.13.1: {} + object-inspect@1.13.2: {} + object-is@1.1.5: dependencies: call-bind: 1.0.7 @@ -31368,6 +31427,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 +31456,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 +31589,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)): @@ -33268,7 +33331,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 +33883,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 +34063,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 +34084,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 +34104,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 +34117,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 +34135,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 +34159,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