From 9640d0ab42418f99d36a791119f0183b3607f4c5 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sat, 16 Mar 2024 23:45:26 -0400 Subject: [PATCH] feat(ts-client): introduce an experimental typescript client (#672) --- .gitignore | 1 + .prettierignore | 1 + .vscode/settings.json | 3 +- examples/schema.graphql | 1247 +++++++++++++++++ package.json | 27 +- pnpm-lock.yaml | 195 +-- src/ResultSet/ResultSet.test-d.ts | 119 ++ src/ResultSet/ResultSet.ts | 108 ++ src/ResultSet/__.ts | 1 + src/Schema/Field/Field.ts | 49 + src/Schema/Field/Type.ts | 38 + src/Schema/Field/_.ts | 1 + src/Schema/Field/__.ts | 2 + src/Schema/Index.ts | 16 + src/Schema/NamedType/Enum.ts | 19 + src/Schema/NamedType/Interface.ts | 31 + src/Schema/NamedType/NamedType.test-d.ts | 17 + src/Schema/NamedType/NamedType.ts | 30 + src/Schema/NamedType/Object.ts | 32 + src/Schema/NamedType/Scalar/Scalar.ts | 47 + src/Schema/NamedType/Scalar/_.ts | 1 + .../NamedType/Scalar/nativeConstructors.ts | 5 + src/Schema/NamedType/Union.ts | 25 + src/Schema/NamedType/_.ts | 6 + src/Schema/NamedType/__.ts | 1 + src/Schema/__.ts | 5 + src/Schema/__Schema.ts | 3 + src/SelectionSet/SelectionSet.test-d.ts | 213 +++ src/SelectionSet/SelectionSet.ts | 279 ++++ src/SelectionSet/_.ts | 2 + src/SelectionSet/__.ts | 1 + .../toGraphQLDocumentString.test.ts.snap | 866 ++++++++++++ .../toGraphQLDocumentString.test.ts | 132 ++ src/SelectionSet/toGraphQLDocumentString.ts | 143 ++ src/cli/generate.ts | 6 + src/client.test.ts | 15 + src/client.ts | 57 + src/entrypoints/alpha.ts | 1 + src/generator/generator.ts | 451 ++++++ src/lib/Code.ts | 51 + src/lib/TSError.ts | 11 + src/lib/graphql.ts | 173 +++ src/lib/prelude.ts | 83 ++ tests/builder/_/schema.graphql | 94 ++ tests/builder/_/schema.ts | 204 +++ .../__snapshots__/generate.test.ts.snap | 190 +++ tests/builder/generate.test.ts | 13 + tsconfig.build.json | 2 +- 48 files changed, 4910 insertions(+), 107 deletions(-) create mode 100644 examples/schema.graphql create mode 100644 src/ResultSet/ResultSet.test-d.ts create mode 100644 src/ResultSet/ResultSet.ts create mode 100644 src/ResultSet/__.ts create mode 100644 src/Schema/Field/Field.ts create mode 100644 src/Schema/Field/Type.ts create mode 100644 src/Schema/Field/_.ts create mode 100644 src/Schema/Field/__.ts create mode 100644 src/Schema/Index.ts create mode 100644 src/Schema/NamedType/Enum.ts create mode 100644 src/Schema/NamedType/Interface.ts create mode 100644 src/Schema/NamedType/NamedType.test-d.ts create mode 100644 src/Schema/NamedType/NamedType.ts create mode 100644 src/Schema/NamedType/Object.ts create mode 100644 src/Schema/NamedType/Scalar/Scalar.ts create mode 100644 src/Schema/NamedType/Scalar/_.ts create mode 100644 src/Schema/NamedType/Scalar/nativeConstructors.ts create mode 100644 src/Schema/NamedType/Union.ts create mode 100644 src/Schema/NamedType/_.ts create mode 100644 src/Schema/NamedType/__.ts create mode 100644 src/Schema/__.ts create mode 100644 src/Schema/__Schema.ts create mode 100644 src/SelectionSet/SelectionSet.test-d.ts create mode 100644 src/SelectionSet/SelectionSet.ts create mode 100644 src/SelectionSet/_.ts create mode 100644 src/SelectionSet/__.ts create mode 100644 src/SelectionSet/__snapshots__/toGraphQLDocumentString.test.ts.snap create mode 100644 src/SelectionSet/toGraphQLDocumentString.test.ts create mode 100644 src/SelectionSet/toGraphQLDocumentString.ts create mode 100644 src/cli/generate.ts create mode 100644 src/client.test.ts create mode 100644 src/client.ts create mode 100644 src/entrypoints/alpha.ts create mode 100644 src/generator/generator.ts create mode 100644 src/lib/Code.ts create mode 100644 src/lib/TSError.ts create mode 100644 src/lib/graphql.ts create mode 100644 tests/builder/_/schema.graphql create mode 100644 tests/builder/_/schema.ts create mode 100644 tests/builder/__snapshots__/generate.test.ts.snap create mode 100644 tests/builder/generate.test.ts diff --git a/.gitignore b/.gitignore index 5637f627a..a2483e00e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build .DS_Store *.log coverage +tsconfig.vitest-temp.json diff --git a/.prettierignore b/.prettierignore index 9e74d29e1..8810254eb 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ build pnpm-lock.yaml coverage +tsconfig.vitest-temp.json diff --git a/.vscode/settings.json b/.vscode/settings.json index eafd9269a..f25c05b6f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,5 +11,6 @@ "source.addMissingImports": "explicit", "source.fixAll.eslint": "explicit", "source.organizeImports": "never" - } + }, + "prettier.enable": false } diff --git a/examples/schema.graphql b/examples/schema.graphql new file mode 100644 index 000000000..bcb30461c --- /dev/null +++ b/examples/schema.graphql @@ -0,0 +1,1247 @@ +union AccelerateStatus = AccelerateStatusDisabled | AccelerateStatusEnabled + +type AccelerateStatusDisabled implements ProductStatus { + enabled: Boolean! +} + +type AccelerateStatusEnabled implements ProductStatus { + enabled: Boolean! +} + +type Count { + number: Int! +} + +""" +ISO 3166-1 alpha-2 country code +""" +enum CountryCode { + AD + AE + AF + AG + AI + AL + AM + AO + AQ + AR + AS + AT + AU + AW + AX + AZ + BA + BB + BD + BE + BF + BG + BH + BI + BJ + BL + BM + BN + BO + BQ + BR + BS + BT + BV + BW + BY + BZ + CA + CC + CD + CF + CG + CH + CI + CK + CL + CM + CN + CO + CR + CU + CV + CW + CX + CY + CZ + DE + DJ + DK + DM + DO + DZ + EC + EE + EG + EH + ER + ES + ET + FI + FJ + FK + FM + FO + FR + GA + GB + GD + GE + GF + GG + GH + GI + GL + GM + GN + GP + GQ + GR + GS + GT + GU + GW + GY + HK + HM + HN + HR + HT + HU + ID + IE + IL + IM + IN + IO + IQ + IR + IS + IT + JE + JM + JO + JP + KE + KG + KH + KI + KM + KN + KP + KR + KW + KY + KZ + LA + LB + LC + LI + LK + LR + LS + LT + LU + LV + LY + MA + MC + MD + ME + MF + MG + MH + MK + ML + MM + MN + MO + MP + MQ + MR + MS + MT + MU + MV + MW + MX + MY + MZ + NA + NC + NE + NF + NG + NI + NL + NO + NP + NR + NU + NZ + OM + PA + PE + PF + PG + PH + PK + PL + PM + PN + PR + PS + PT + PW + PY + QA + RE + RO + RS + RU + RW + SA + SB + SC + SD + SE + SG + SH + SI + SJ + SK + SL + SM + SN + SO + SR + SS + ST + SV + SX + SY + SZ + TC + TD + TF + TG + TH + TJ + TK + TL + TM + TN + TO + TR + TT + TV + TW + TZ + UA + UG + UM + US + UY + UZ + VA + VC + VE + VG + VI + VN + VU + WF + WS + YE + YT + ZA + ZM + ZW +} + +type DatabaseLink { + connectionStringHint: String! + id: ID! + protocol: String! + region: String +} + +type DatabaseLinkNode implements Node { + connectionStringHint: ID! + displayName: String! + id: String! +} + +""" +A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.This scalar is serialized to a string in ISO 8601 format and parsed from a string in ISO 8601 format. +""" +scalar Date + +type Environment { + accelerate: EnvironmentAccelerate! + createdAt: Date! + displayName: String! + id: ID! + isDefault: Boolean! + project: Project! + pulse: EnvironmentPulse! + serviceKeys: [ServiceKey!]! + tenantId: ID! +} + +type EnvironmentAccelerate { + databaseLink: DatabaseLink + holds: [ProductHold!]! + status: AccelerateStatus! + usage(timeWindow: EnvironmentAccelerateUsageTimeWindowInput!): EnvironmentAccelerateUsage! +} + +type EnvironmentAccelerateTimeSeriesPoints { + queries: EnvironmentAccelerateUsageTimeSeriesPointsQueries! + timestamps: [Date!]! +} + +type EnvironmentAccelerateUsage { + latency: EnvironmentAccelerateUsageLatency! + overview: EnvironmentAccelerateUsageOverview! + timeInterval: TimeInterval! + timeSeries: EnvironmentAccelerateUsageTimeSeries! +} + +type EnvironmentAccelerateUsageLatency { + queries: EnvironmentAccelerateUsageLatencyQueries! +} + +type EnvironmentAccelerateUsageLatencyQueries { + cached: EnvironmentAccelerateUsageLatencyQuery! + origin: EnvironmentAccelerateUsageLatencyQuery! +} + +type EnvironmentAccelerateUsageLatencyQuery { + count: Count! + durationAverage: MetricValue! + durationPercentiles: [Percentile!]! +} + +type EnvironmentAccelerateUsageOverview { + egress: EnvironmentAccelerateUsageOverviewEgress! + queries: EnvironmentAccelerateUsageOverviewQueries! +} + +type EnvironmentAccelerateUsageOverviewCacheHit { + ratioToMiss: MetricValue! +} + +type EnvironmentAccelerateUsageOverviewEgress { + averageResponseSize: StorageValue! + requestsServedFromOrigin: Count! + total: StorageValue! +} + +type EnvironmentAccelerateUsageOverviewQueries { + cacheHit: EnvironmentAccelerateUsageOverviewCacheHit! + cacheableCount: Count! + totalCount: Count! +} + +type EnvironmentAccelerateUsageTimeSeries { + points: EnvironmentAccelerateTimeSeriesPoints! +} + +type EnvironmentAccelerateUsageTimeSeriesPointsQueries { + miss: [EnvironmentAccelerateUsageTimeSeriesPointsQuery!] + none: [EnvironmentAccelerateUsageTimeSeriesPointsQuery!] + swr: [EnvironmentAccelerateUsageTimeSeriesPointsQuery!] + ttl: [EnvironmentAccelerateUsageTimeSeriesPointsQuery!] +} + +type EnvironmentAccelerateUsageTimeSeriesPointsQuery { + count: Count! + timestamp: Date! +} + +enum EnvironmentAccelerateUsageTimeWindowInput { + last6h + last7d + last24h + last30d + last30m +} + +type EnvironmentPulse { + databaseLink: DatabaseLink + status: PulseStatus! +} + +interface Error { + message: String! +} + +type ErrorInternal implements Error { + message: String! +} + +type ErrorUser implements Error { + message: String! +} + +type ErrorUserBusinessDeleteWorkspaceOnPaidPlan implements Error { + context: ErrorUserBusinessDeleteWorkspaceOnPaidPlanContext! + message: String! +} + +type ErrorUserBusinessDeleteWorkspaceOnPaidPlanContext { + plan: Plan! +} + +type ErrorUserBusinessNotAuthorized implements Error { + message: String! +} + +type ErrorUserBusinessPlanLimitHit implements Error { + context: ErrorUserBusinessPlanLimitHitContext! + message: String! +} + +type ErrorUserBusinessPlanLimitHitContext { + featureHandle: String +} + +type ErrorUserBusinessResourceNotFound implements Error { + context: ErrorUserBusinessResourceNotFoundContext! + message: String! +} + +type ErrorUserBusinessResourceNotFoundContext { + id: ID + typeName: ResourceType! +} + +type ErrorUserBusinessUserAlreadyMemberOfOrganization implements Error { + context: ErrorUserBusinessUserAlreadyMemberOfOrganizationContext! + message: String! +} + +type ErrorUserBusinessUserAlreadyMemberOfOrganizationContext { + user: User! + workspace: Workspace! +} + +type ErrorUserInput implements Error { + message: String! +} + +interface Feature { + displayName: String + handle: FeatureHandle! + id: ID! + stripeProductId: String! +} + +type FeatureAbstract implements Feature { + displayName: String + handle: FeatureHandle! + id: ID! + stripeProductId: String! +} + +enum FeatureHandle { + accelerateEgress + acceleratePurgeCache + accelerateQuery + access + createProject + organizationRole + platformSupport +} + +type FeatureResourceAggregation implements Feature { + displayName: String + handle: FeatureHandle! + id: ID! + resource: String! + scope: String! + stripeProductId: String! + valueResolver: FeatureResourceAggregationValueResolver! +} + +type FeatureResourceAggregationValueResolver { + type: FeatureResourceAggregationValueResolverType! +} + +enum FeatureResourceAggregationValueResolverType { + count +} + +type FeatureResourceProperty implements Feature { + displayName: String + handle: FeatureHandle! + id: ID! + resource: String! + scope: String! + stripeProductId: String! + valueResolver: FeatureResourcePropertyValueResolver! +} + +type FeatureResourcePropertyValueResolver { + field: String! + type: FeatureValueType! +} + +type FeatureValue implements Feature { + displayName: String + handle: FeatureHandle! + id: ID! + stripeProductId: String! + valueType: FeatureValueType! +} + +union FeatureValueType = + | FeatureValueTypeBoolean + | FeatureValueTypeEnum + | FeatureValueTypeNumber + | FeatureValueTypeString + +type FeatureValueTypeBoolean { + displayName: String! +} + +type FeatureValueTypeEnum { + displayName: String! + members: [FeatureValueTypeEnumMember!]! +} + +type FeatureValueTypeEnumMember { + description: String + value: String! +} + +type FeatureValueTypeNumber { + displayName: String! +} + +type FeatureValueTypeString { + displayName: String! +} + +union Limit = LimitEnum | LimitNumber + +type LimitEnum { + allowed: [String!]! +} + +type LimitNumber { + amount: Int! + type: NumberPredicateFnType! +} + +type Me { + user: User! + workspaces(orderBy: WorkspaceOrderBy = {}): [Workspace!]! +} + +enum MetricUnit { + ms + percent +} + +type MetricValue { + number: Float! + unit: MetricUnit +} + +type Mutation { + accelerateCachePurge(input: MutationAccelerateCachePurgeInput!): MutationAccelerateCachePurgeResult! + accelerateDisable(input: MutationAccelerateDisableInput!): MutationAccelerateDisableResult! + accelerateEnable(input: MutationAccelerateEnableInput!): MutationAccelerateEnableResult! + databaseLinkCreate(input: MutationDatabaseLinkCreateInput!): MutationDatabaseLinkCreateResult! + databaseLinkDelete(input: MutationDatabaseLinkDeleteInput!): MutationDatabaseLinkDeleteResult! + databaseLinkUpdate(input: MutationDatabaseLinkUpdateInput!): MutationDatabaseLinkUpdateResult! + environmentCreate(input: MutationEnvironmentCreateInput!): MutationEnvironmentCreateResult! + environmentDelete(input: MutationEnvironmentDeleteInput!): MutationEnvironmentDeleteResult! + environmentUpdate(input: MutationEnvironmentUpdateInput!): MutationEnvironmentUpdateResult! + projectCreate(input: MutationProjectCreateInput!): MutationProjectCreateResult! + projectDelete(input: MutationProjectDeleteInput!): MutationProjectDeleteResult! + projectUpdate(input: MutationProjectUpdateInput!): MutationProjectUpdateResult! + pulseDisable(input: MutationPulseDisableInput!): MutationPulseDisableResult! + pulseEnable(input: MutationPulseEnableInput!): MutationPulseEnableResult! + serviceKeyCreate(input: MutationServiceKeyCreateInput!): MutationServiceKeyCreateResult! + serviceKeyDelete(input: MutationServiceKeyDeleteInput!): MutationServiceKeyDeleteResult! + userUpdate(input: MutationUserUpdateInput!): MutationUserUpdateResult! + userUpdateDefaultWorkspace( + input: MutationUserUpdateDefaultWorkspaceInput! + ): MutationUserUpdateDefaultWorkspaceResult! + workspaceCreate(input: MutationWorkspaceCreateInput!): MutationWorkspaceCreateResult! + workspaceDelete(input: MutationWorkspaceDeleteInput!): MutationWorkspaceDeleteResult! + workspaceMembershipCreate( + input: MutationWorkspaceMembershipCreateInput! + ): MutationWorkspaceMembershipCreateResult! + workspaceMembershipDelete( + input: MutationWorkspaceMembershipDeleteInput! + ): MutationWorkspaceMembershipDeleteResult! + workspacePlanSubscriptionChange( + input: MutationWorkspacePlanSubscriptionChangeInput! + ): MutationWorkspacePlanSubscriptionChangeResult! + workspaceUpdate(input: MutationWorkspaceUpdateInput!): MutationWorkspaceUpdateResult! + workspaceUpdateBillingAddress( + input: MutationWorkspaceUpdateBillingAddressInput! + ): MutationWorkspaceUpdateBillingAddressResult! + workspaceUpdateBillingEmail( + input: MutationWorkspaceUpdateBillingEmailInput! + ): MutationWorkspaceUpdateBillingEmailResult! +} + +input MutationAccelerateCachePurgeInput { + environmentId: ID! +} + +union MutationAccelerateCachePurgeResult = ErrorInternal | SideEffectConfirmation + +input MutationAccelerateDisableInput { + environmentId: ID! +} + +union MutationAccelerateDisableResult = ErrorInternal | SideEffectConfirmation + +input MutationAccelerateEnableInput { + databaseLinkId: ID! +} + +union MutationAccelerateEnableResult = ErrorInternal | SideEffectConfirmation + +input MutationDatabaseLinkCreateInput { + connectionString: String! + displayName: String + environmentId: ID! + regionId: String +} + +union MutationDatabaseLinkCreateResult = DatabaseLink | ErrorInternal | ErrorUserBusinessResourceNotFound + +input MutationDatabaseLinkDeleteInput { + id: ID! +} + +union MutationDatabaseLinkDeleteResult = DatabaseLinkNode | ErrorInternal | ErrorUserBusinessResourceNotFound + +input MutationDatabaseLinkUpdateInput { + connectionString: String! + id: ID! + regionId: String! +} + +union MutationDatabaseLinkUpdateResult = DatabaseLink | ErrorInternal | ErrorUserBusinessResourceNotFound + +input MutationEnvironmentCreateInput { + displayName: String + isDefault: Boolean + projectId: ID! +} + +union MutationEnvironmentCreateResult = + | Environment + | ErrorInternal + | ErrorUserBusinessPlanLimitHit + | ErrorUserBusinessResourceNotFound + +input MutationEnvironmentDeleteInput { + id: ID! +} + +union MutationEnvironmentDeleteResult = Environment | ErrorInternal | ErrorUserBusinessResourceNotFound + +input MutationEnvironmentUpdateInput { + displayName: String + id: ID! + isDefault: Boolean +} + +union MutationEnvironmentUpdateResult = Environment | ErrorInternal | ErrorUserBusinessResourceNotFound + +input MutationProjectCreateInput { + displayName: String + workspaceId: ID! +} + +union MutationProjectCreateResult = + | ErrorInternal + | ErrorUserBusinessPlanLimitHit + | ErrorUserBusinessResourceNotFound + | Project + +input MutationProjectDeleteInput { + id: ID! +} + +union MutationProjectDeleteResult = ErrorInternal | ErrorUserBusinessResourceNotFound | ProjectNode + +input MutationProjectUpdateInput { + displayName: String + id: ID! +} + +union MutationProjectUpdateResult = ErrorInternal | ErrorUserBusinessResourceNotFound | Project + +input MutationPulseDisableInput { + environmentId: String! +} + +union MutationPulseDisableResult = ErrorInternal | ErrorUserBusinessResourceNotFound | SideEffectConfirmation + +input MutationPulseEnableInput { + databaseLinkId: String! +} + +union MutationPulseEnableResult = ErrorInternal | ErrorUser | SideEffectConfirmation + +input MutationServiceKeyCreateInput { + displayName: String + environmentId: ID! +} + +union MutationServiceKeyCreateResult = ErrorInternal | ErrorUserBusinessResourceNotFound | ServiceKeyWithValue + +input MutationServiceKeyDeleteInput { + id: String! +} + +union MutationServiceKeyDeleteResult = ErrorInternal | ErrorUserBusinessResourceNotFound | ServiceKeyNode + +input MutationUserUpdateDefaultWorkspaceInput { + workspaceId: ID! +} + +union MutationUserUpdateDefaultWorkspaceResult = ErrorInternal | ErrorUserBusinessResourceNotFound | User + +input MutationUserUpdateInput { + displayName: String + id: ID! +} + +union MutationUserUpdateResult = ErrorInternal | ErrorUserBusinessResourceNotFound | User + +input MutationWorkspaceCreateInput { + displayName: String +} + +union MutationWorkspaceCreateResult = ErrorInternal | Workspace + +input MutationWorkspaceDeleteInput { + id: ID! +} + +union MutationWorkspaceDeleteResult = + | ErrorInternal + | ErrorUserBusinessDeleteWorkspaceOnPaidPlan + | ErrorUserBusinessResourceNotFound + | WorkspaceNode + +input MutationWorkspaceMembershipCreateInput { + email: String! + role: WorkspaceRole! + workspaceId: ID! +} + +union MutationWorkspaceMembershipCreateResult = + | ErrorInternal + | ErrorUserBusinessResourceNotFound + | ErrorUserBusinessUserAlreadyMemberOfOrganization + | WorkspaceMembership + +input MutationWorkspaceMembershipDeleteInput { + id: ID! +} + +union MutationWorkspaceMembershipDeleteResult = + | ErrorInternal + | ErrorUserBusinessResourceNotFound + | WorkspaceMembershipNode + +input MutationWorkspacePlanSubscriptionChangeInput { + targetPlanId: ID! + workspaceId: ID! +} + +union MutationWorkspacePlanSubscriptionChangeResult = + | ErrorInternal + | ErrorUserBusinessResourceNotFound + | PlanSubscription + +input MutationWorkspaceUpdateBillingAddressInput { + address: PhysicalAddressInput! + id: ID! +} + +union MutationWorkspaceUpdateBillingAddressResult = + | ErrorInternal + | ErrorUserBusinessResourceNotFound + | Workspace + +input MutationWorkspaceUpdateBillingEmailInput { + email: String! + id: ID! +} + +union MutationWorkspaceUpdateBillingEmailResult = + | ErrorInternal + | ErrorUserBusinessResourceNotFound + | Workspace + +input MutationWorkspaceUpdateInput { + displayName: String + id: ID! +} + +union MutationWorkspaceUpdateResult = ErrorInternal | ErrorUserBusinessResourceNotFound | Workspace + +interface Node { + id: String! +} + +enum NumberPredicateFnType { + NumberPredicateFnEQ + NumberPredicateFnGT + NumberPredicateFnGTE + NumberPredicateFnLT + NumberPredicateFnLTE +} + +interface Offer { + context: OfferContext! + id: ID! + price: Price +} + +type OfferAbstract implements Offer { + context: OfferContext! + feature: FeatureAbstract! + id: ID! + price: Price +} + +union OfferContext = Plan | PlanSubscription + +type OfferResourceAggregation implements Offer { + context: OfferContext! + feature: FeatureResourceAggregation! + id: ID! + limit: Limit + price: Price + timeInterval: OfferTimeInterval +} + +type OfferResourceProperty implements Offer { + context: OfferContext! + feature: FeatureResourceProperty! + id: ID! + limit: Limit + price: Price + timeInterval: OfferTimeInterval +} + +union OfferTimeInterval = OfferTimeIntervalCycle | OfferTimeIntervalPrevious + +type OfferTimeIntervalCycle { + ok: Boolean! +} + +type OfferTimeIntervalPrevious { + milliseconds: Int! +} + +type OfferValue implements Offer { + context: OfferContext! + feature: FeatureValue! + id: ID! + limit: Limit + price: Price + value: String! +} + +enum Order { + asc + desc +} + +type PaymentMethod { + card: PaymentMethodCard! + id: ID! + isDefault: Boolean! +} + +type PaymentMethodCard { + brand: PaymentMethodCardBrand! + expiryMonth: Int! + expiryYear: Int! + id: ID! + last4: String! +} + +enum PaymentMethodCardBrand { + amex + diners + discover + eftpos_au + jcb + mastercard + unionpay + unknown + visa +} + +type Percentile { + percentile: Int! + value: MetricValue! +} + +type PhysicalAddress { + addressLine1: String + addressLine2: String + city: String + country: String + postalCodeOrZIP: String + region: String +} + +input PhysicalAddressInput { + addressLine1: String + addressLine2: String + city: String + country: CountryCode + postalCodeOrZIP: String + region: String +} + +type Plan { + displayName: String! + handle: String! + id: ID! + isDefault: Boolean! + isFree: Boolean! + offers: PlanOffers! + power: Int! + selectable: Boolean! + version: Int! + versionIsLatest: Boolean! + versions: PlanVersions! +} + +type PlanOffers { + accelerate: PlanOffersAccelerate! + conductor: PlanOffersConductor! + platform: PlanOffersPlatform! +} + +type PlanOffersAccelerate { + egress: OfferResourceProperty! + purgeCache: OfferResourceAggregation! + query: OfferResourceAggregation! +} + +type PlanOffersConductor { + createProject: OfferResourceAggregation! + organizationRole: OfferResourceProperty! +} + +type PlanOffersPlatform { + access: OfferAbstract! + support: OfferValue! +} + +type PlanSubscription { + createdAt: Date! + id: ID! + plan: Plan! + stripeSubscriptionId: String + stripeSubscriptionLineItems: [StripeSubscriptionLineItem!]! + workspace: Workspace! +} + +type PlanVersions { + isLatest: Boolean! + next: [Plan!]! + previous: [Plan!]! +} + +enum PreviousDateHandle { + last6h + last7d + last24h + last30d + last30m + startOfCycle +} + +union Price = PriceConstant | PriceTiered + +type PriceConstant implements PriceI { + cents: Int! + id: String! + stripePriceId: ID! +} + +interface PriceI { + id: String! + stripePriceId: ID! +} + +type PriceTiered implements PriceI { + id: String! + stripePriceId: ID! + tiers: [PriceTieredTier!]! +} + +type PriceTieredTier { + cents: Float! + from: Int! + to: Int +} + +type ProductHold { + createdAt: Int! + expiresAt: Int! + reason: String! +} + +interface ProductStatus { + enabled: Boolean! +} + +type Project { + accelerate: EnvironmentAccelerate! + createdAt: Date! + displayName: String! + environments: [Environment!]! + id: ID! + pulse: EnvironmentPulse! + workspace: Workspace! +} + +type ProjectNode implements Node { + createdAt: Date! + displayName: String! + id: String! + workspaceId: ID! +} + +union PulseStatus = PulseStatusDisabled | PulseStatusEnabled + +type PulseStatusDisabled implements ProductStatus { + enabled: Boolean! +} + +type PulseStatusEnabled implements ProductStatus { + enabled: Boolean! + error: String +} + +type Query { + environment(id: ID!): QueryEnvironmentResult! + me: Me! + plan(handle: String, id: ID, version: Int): QueryPlanResult! + project(id: ID!): QueryProjectResult! + serviceKeys(projectId: ID!): [ServiceKey!]! + system: System! + user(auth0Id: ID, id: ID): User! + workspace(id: ID!): QueryWorkspaceResult! +} + +union QueryEnvironmentResult = + | Environment + | ErrorInternal + | ErrorUserBusinessNotAuthorized + | ErrorUserBusinessResourceNotFound + +union QueryPlanResult = ErrorInternal | ErrorUserBusinessResourceNotFound | ErrorUserInput | Plan + +union QueryProjectResult = + | ErrorInternal + | ErrorUserBusinessNotAuthorized + | ErrorUserBusinessResourceNotFound + | Project + +union QueryWorkspaceResult = + | ErrorInternal + | ErrorUserBusinessNotAuthorized + | ErrorUserBusinessResourceNotFound + | Workspace + +enum ResourceType { + Project + Workspace +} + +type ServiceKey { + createdAt: Date! + displayName: String! + id: ID! + valueHint: String! +} + +type ServiceKeyNode implements Node { + displayName: String! + id: String! + valueHint: String! +} + +type ServiceKeyWithValue { + serviceKey: ServiceKey! + value: ID! +} + +type SideEffectConfirmation { + ok: Boolean! +} + +enum StorageUnit { + bytes +} + +type StorageValue { + number: Float! + unit: StorageUnit +} + +type StripeSubscriptionLineItem { + feature: FeatureHandle! + id: ID! +} + +type System { + accelerate: SystemAccelerate! + plans: [Plan!]! + pulse: SystemPulse! +} + +type SystemAccelerate { + defaultRegion: SystemAccelerateRegion! + regions: [SystemAccelerateRegion!]! +} + +type SystemAccelerateRegion { + displayName: String! + id: ID! +} + +type SystemPulse { + defaultRegion: SystemAccelerateRegion! + regions: [SystemAccelerateRegion!]! +} + +type TimeInterval { + from: Date! + to: Date! +} + +input TimeIntervalInput { + fromDate: Date + fromDateHandle: PreviousDateHandle + toDate: Date +} + +type UsageProductAccelerate { + egress: UsageProductAccelerateFeatureEgress! + request: UsageProductAccelerateFeatureRequest! +} + +type UsageProductAccelerateFeatureEgress { + averageResponseSize: Float! + total: Float! +} + +type UsageProductAccelerateFeatureRequest { + all: UsageProductAccelerateFeatureRequestFilterAll! + cacheHit: UsageProductAccelerateFeatureRequestFilterCacheHit! +} + +type UsageProductAccelerateFeatureRequestFilterAll { + count: Int! +} + +type UsageProductAccelerateFeatureRequestFilterCacheHit { + ratioToMiss: Int! +} + +type User { + displayName: String + email: String! + featureFlags: UserFeatureFlags! + handle: String + id: ID! + image: String + preferences: UserPreferences! +} + +type UserFeatureFlags { + adminDashboard: Boolean! + mars: Boolean! + mercury: Boolean! + venus: Boolean! +} + +type UserPreferences { + """ + If null, user's default organization could have been deleted by another user. + """ + defaultWorkspace: Workspace +} + +type Workspace { + billingAddress: PhysicalAddress + billingEmail: String! + createdAt: Date! + displayName: String! + id: ID! + + """ + Whether this is the last workspace of the user or not. + """ + isUsersLastMembership: Boolean! + memberships: [WorkspaceMembership!]! + paymentMethods: [PaymentMethod!]! + planSubscription: PlanSubscription! + projects: [Project!]! + stripeCustomerId: String! + usage(timeInterval: TimeIntervalInput): WorkspaceUsage! +} + +type WorkspaceMembership { + id: ID! + role: WorkspaceRole! + user: User! +} + +type WorkspaceMembershipNode implements Node { + id: String! + workspaceId: ID! +} + +type WorkspaceNode implements Node { + billingEmail: String! + displayName: String! + id: String! +} + +input WorkspaceOrderBy { + displayName: Order = asc +} + +enum WorkspaceRole { + accountant + admin + developer + viewer +} + +type WorkspaceUsage { + accelerate: UsageProductAccelerate! + timeInterval: TimeInterval! +} diff --git a/package.json b/package.json index c8298a010..23f5800ae 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,12 @@ "types": "./build/entrypoints/main.d.ts", "default": "./build/entrypoints/main.js" } + }, + "./alpha": { + "import": { + "types": "./build/entrypoints/alpha.d.ts", + "default": "./build/entrypoints/alpha.js" + } } }, "packageManager": "pnpm@8.15.4", @@ -36,6 +42,7 @@ }, "homepage": "https://github.com/jasonkuhrt/graphql-request", "scripts": { + "demo": "tsx src/cli/generate.ts && dprint fmt src/demo.ts", "dev": "rm -rf dist && tsc --watch", "format": "pnpm build:docs && dprint fmt", "lint": "eslint . --ext .ts,.tsx --fix", @@ -48,12 +55,14 @@ "build": "pnpm clean && pnpm tsc --project tsconfig.build.json", "clean": "tsc --build --clean && rm -rf build", "test": "vitest", + "test:types": "vitest --typecheck", "test:coverage": "pnpm test -- --coverage", "release:stable": "dripip stable", "release:preview": "dripip preview", "release:pr": "dripip pr" }, "dependencies": { + "@dprint/formatter": "^0.2.1", "@graphql-typed-document-node/core": "^3.2.0", "dprint": "^0.45.0" }, @@ -65,29 +74,29 @@ "@types/body-parser": "^1.19.5", "@types/express": "^4.17.21", "@types/json-bigint": "^1.0.4", - "@types/node": "^20.11.19", - "@typescript-eslint/eslint-plugin": "^7.0.1", - "@typescript-eslint/parser": "^7.0.1", + "@types/node": "^20.11.28", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", "apollo-server-express": "^3.13.0", "body-parser": "^1.20.2", "doctoc": "^2.2.1", "dripip": "^0.10.0", - "eslint": "^8.56.0", + "eslint": "^8.57.0", "eslint-config-prisma": "^0.2.0", "eslint-plugin-deprecation": "^2.0.0", "eslint-plugin-only-warn": "^1.1.0", "eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-simple-import-sort": "^12.0.0", "eslint-plugin-tsdoc": "^0.2.17", - "express": "^4.18.2", + "express": "^4.18.3", "get-port": "^7.0.0", "graphql": "^16.8.1", "graphql-tag": "^2.12.6", - "happy-dom": "^13.3.8", + "happy-dom": "^13.8.6", "json-bigint": "^1.0.0", "tsx": "^4.7.1", - "type-fest": "^4.10.2", - "typescript": "^5.3.3", - "vitest": "^1.3.0" + "type-fest": "^4.12.0", + "typescript": "^5.4.2", + "vitest": "^1.4.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc5c8bc36..b7c10b95e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@dprint/formatter': + specifier: ^0.2.1 + version: 0.2.1 '@graphql-typed-document-node/core': specifier: ^3.2.0 version: 3.2.0(graphql@16.8.1) @@ -26,14 +29,14 @@ devDependencies: specifier: ^1.0.4 version: 1.0.4 '@types/node': - specifier: ^20.11.19 + specifier: ^20.11.28 version: 20.11.28 '@typescript-eslint/eslint-plugin': - specifier: ^7.0.1 - version: 7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.56.0)(typescript@5.4.2) + specifier: ^7.2.0 + version: 7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/parser': - specifier: ^7.0.1 - version: 7.2.0(eslint@8.56.0)(typescript@5.4.2) + specifier: ^7.2.0 + version: 7.2.0(eslint@8.57.0)(typescript@5.4.2) apollo-server-express: specifier: ^3.13.0 version: 3.13.0(express@4.18.3)(graphql@16.8.1) @@ -47,28 +50,28 @@ devDependencies: specifier: ^0.10.0 version: 0.10.0 eslint: - specifier: ^8.56.0 - version: 8.56.0 + specifier: ^8.57.0 + version: 8.57.0 eslint-config-prisma: specifier: ^0.2.0 - version: 0.2.0(@typescript-eslint/eslint-plugin@7.2.0)(@typescript-eslint/parser@7.2.0)(eslint-plugin-deprecation@2.0.0)(eslint-plugin-only-warn@1.1.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-simple-import-sort@12.0.0)(eslint-plugin-tsdoc@0.2.17)(eslint@8.56.0) + version: 0.2.0(@typescript-eslint/eslint-plugin@7.2.0)(@typescript-eslint/parser@7.2.0)(eslint-plugin-deprecation@2.0.0)(eslint-plugin-only-warn@1.1.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-simple-import-sort@12.0.0)(eslint-plugin-tsdoc@0.2.17)(eslint@8.57.0) eslint-plugin-deprecation: specifier: ^2.0.0 - version: 2.0.0(eslint@8.56.0)(typescript@5.4.2) + version: 2.0.0(eslint@8.57.0)(typescript@5.4.2) eslint-plugin-only-warn: specifier: ^1.1.0 version: 1.1.0 eslint-plugin-prefer-arrow: specifier: ^1.2.3 - version: 1.2.3(eslint@8.56.0) + version: 1.2.3(eslint@8.57.0) eslint-plugin-simple-import-sort: specifier: ^12.0.0 - version: 12.0.0(eslint@8.56.0) + version: 12.0.0(eslint@8.57.0) eslint-plugin-tsdoc: specifier: ^0.2.17 version: 0.2.17 express: - specifier: ^4.18.2 + specifier: ^4.18.3 version: 4.18.3 get-port: specifier: ^7.0.0 @@ -80,7 +83,7 @@ devDependencies: specifier: ^2.12.6 version: 2.12.6(graphql@16.8.1) happy-dom: - specifier: ^13.3.8 + specifier: ^13.8.6 version: 13.8.6 json-bigint: specifier: ^1.0.0 @@ -89,13 +92,13 @@ devDependencies: specifier: ^4.7.1 version: 4.7.1 type-fest: - specifier: ^4.10.2 + specifier: ^4.12.0 version: 4.12.0 typescript: - specifier: ^5.3.3 + specifier: ^5.4.2 version: 5.4.2 vitest: - specifier: ^1.3.0 + specifier: ^1.4.0 version: 1.4.0(@types/node@20.11.28)(happy-dom@13.8.6) packages: @@ -234,7 +237,7 @@ packages: /@apollographql/graphql-playground-html@1.6.29: resolution: {integrity: sha512-xCcXpoz52rI4ksJSdOCxeOCn2DLocxwHf9dVT/Q90Pte1LX+LY+91SFtJF3KXVHH8kEin+g1KKCQPKBjZJfWNA==} dependencies: - xss: 1.0.14 + xss: 1.0.15 dev: true /@dprint/darwin-arm64@0.45.0: @@ -253,6 +256,10 @@ packages: dev: false optional: true + /@dprint/formatter@0.2.1: + resolution: {integrity: sha512-GCzgRt2o4mhZLy8L47k2A+q9EMG/jWhzZebE29EqKsxmjDrSfv2VisEj/Q+39OOf04jTkEfB/TRO+IZSyxHdYg==} + dev: false + /@dprint/linux-arm64-glibc@0.45.0: resolution: {integrity: sha512-NgIpvZHpiQaY4DxSygxknxBtvKE2KLK9dEbUNKNE098yTHhGq7ouPsoM7RtsO34RHJ3tEZLLJEuBHn20XP8LMg==} cpu: [arm64] @@ -500,13 +507,13 @@ packages: dev: true optional: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.56.0): + /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 8.56.0 + eslint: 8.57.0 eslint-visitor-keys: 3.4.3 dev: true @@ -532,8 +539,8 @@ packages: - supports-color dev: true - /@eslint/js@8.56.0: - resolution: {integrity: sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==} + /@eslint/js@8.57.0: + resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true @@ -1181,7 +1188,7 @@ packages: resolution: {integrity: sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==} dependencies: '@types/node': 20.11.28 - '@types/qs': 6.9.11 + '@types/qs': 6.9.12 '@types/range-parser': 1.2.7 dev: true @@ -1189,7 +1196,7 @@ packages: resolution: {integrity: sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==} dependencies: '@types/node': 20.11.28 - '@types/qs': 6.9.11 + '@types/qs': 6.9.12 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 dev: true @@ -1199,7 +1206,7 @@ packages: dependencies: '@types/body-parser': 1.19.5 '@types/express-serve-static-core': 4.17.31 - '@types/qs': 6.9.11 + '@types/qs': 6.9.12 '@types/serve-static': 1.15.5 dev: true @@ -1208,7 +1215,7 @@ packages: dependencies: '@types/body-parser': 1.19.5 '@types/express-serve-static-core': 4.17.43 - '@types/qs': 6.9.11 + '@types/qs': 6.9.12 '@types/serve-static': 1.15.5 dev: true @@ -1266,18 +1273,14 @@ packages: '@types/node': 20.11.28 dev: true - /@types/qs@6.9.11: - resolution: {integrity: sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==} + /@types/qs@6.9.12: + resolution: {integrity: sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg==} dev: true /@types/range-parser@1.2.7: resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} dev: true - /@types/semver@7.5.7: - resolution: {integrity: sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==} - dev: true - /@types/semver@7.5.8: resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} dev: true @@ -1301,7 +1304,7 @@ packages: resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} dev: true - /@typescript-eslint/eslint-plugin@7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.56.0)(typescript@5.4.2): + /@typescript-eslint/eslint-plugin@7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)(typescript@5.4.2): resolution: {integrity: sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -1313,13 +1316,13 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 7.2.0(eslint@8.56.0)(typescript@5.4.2) + '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/scope-manager': 7.2.0 - '@typescript-eslint/type-utils': 7.2.0(eslint@8.56.0)(typescript@5.4.2) - '@typescript-eslint/utils': 7.2.0(eslint@8.56.0)(typescript@5.4.2) + '@typescript-eslint/type-utils': 7.2.0(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/visitor-keys': 7.2.0 debug: 4.3.4 - eslint: 8.56.0 + eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.1 natural-compare: 1.4.0 @@ -1330,7 +1333,7 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@7.2.0(eslint@8.56.0)(typescript@5.4.2): + /@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.2): resolution: {integrity: sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -1345,7 +1348,7 @@ packages: '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.2) '@typescript-eslint/visitor-keys': 7.2.0 debug: 4.3.4 - eslint: 8.56.0 + eslint: 8.57.0 typescript: 5.4.2 transitivePeerDependencies: - supports-color @@ -1367,7 +1370,7 @@ packages: '@typescript-eslint/visitor-keys': 7.2.0 dev: true - /@typescript-eslint/type-utils@7.2.0(eslint@8.56.0)(typescript@5.4.2): + /@typescript-eslint/type-utils@7.2.0(eslint@8.57.0)(typescript@5.4.2): resolution: {integrity: sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -1378,9 +1381,9 @@ packages: optional: true dependencies: '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.2) - '@typescript-eslint/utils': 7.2.0(eslint@8.56.0)(typescript@5.4.2) + '@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@5.4.2) debug: 4.3.4 - eslint: 8.56.0 + eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.4.2) typescript: 5.4.2 transitivePeerDependencies: @@ -1441,38 +1444,38 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@6.21.0(eslint@8.56.0)(typescript@5.4.2): + /@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.4.2): resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@types/json-schema': 7.0.15 - '@types/semver': 7.5.7 + '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.2) - eslint: 8.56.0 + eslint: 8.57.0 semver: 7.6.0 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/utils@7.2.0(eslint@8.56.0)(typescript@5.4.2): + /@typescript-eslint/utils@7.2.0(eslint@8.57.0)(typescript@5.4.2): resolution: {integrity: sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^8.56.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@types/json-schema': 7.0.15 '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 7.2.0 '@typescript-eslint/types': 7.2.0 '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.2) - eslint: 8.56.0 + eslint: 8.57.0 semver: 7.6.0 transitivePeerDependencies: - supports-color @@ -1853,7 +1856,7 @@ packages: es-errors: 1.3.0 function-bind: 1.1.2 get-intrinsic: 1.2.4 - set-function-length: 1.2.1 + set-function-length: 1.2.2 dev: true /callsites@3.1.0: @@ -2172,7 +2175,7 @@ packages: common-tags: 1.8.2 debug: 4.3.4 fs-jetpack: 3.2.0 - isomorphic-git: 1.25.5 + isomorphic-git: 1.25.6 parse-git-config: 3.0.0 parse-github-url: 1.0.2 request: 2.88.2 @@ -2272,16 +2275,16 @@ packages: engines: {node: '>=10'} dev: true - /eslint-config-prettier@9.1.0(eslint@8.56.0): + /eslint-config-prettier@9.1.0(eslint@8.57.0): resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} hasBin: true peerDependencies: eslint: '>=7.0.0' dependencies: - eslint: 8.56.0 + eslint: 8.57.0 dev: true - /eslint-config-prisma@0.2.0(@typescript-eslint/eslint-plugin@7.2.0)(@typescript-eslint/parser@7.2.0)(eslint-plugin-deprecation@2.0.0)(eslint-plugin-only-warn@1.1.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-simple-import-sort@12.0.0)(eslint-plugin-tsdoc@0.2.17)(eslint@8.56.0): + /eslint-config-prisma@0.2.0(@typescript-eslint/eslint-plugin@7.2.0)(@typescript-eslint/parser@7.2.0)(eslint-plugin-deprecation@2.0.0)(eslint-plugin-only-warn@1.1.0)(eslint-plugin-prefer-arrow@1.2.3)(eslint-plugin-simple-import-sort@12.0.0)(eslint-plugin-tsdoc@0.2.17)(eslint@8.57.0): resolution: {integrity: sha512-ky6iBCU9jk4o/SqkUmIUwspXjTfDO/d4glQ1VHrkL/SBra+PZjzsrDVX3pdXZgDw81+NFVYEBJIL4+lBL/yMCw==} peerDependencies: '@typescript-eslint/eslint-plugin': ^6 @@ -2293,25 +2296,25 @@ packages: eslint-plugin-simple-import-sort: ^10.0 eslint-plugin-tsdoc: ^0.2 dependencies: - '@typescript-eslint/eslint-plugin': 7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.56.0)(typescript@5.4.2) - '@typescript-eslint/parser': 7.2.0(eslint@8.56.0)(typescript@5.4.2) - eslint: 8.56.0 - eslint-config-prettier: 9.1.0(eslint@8.56.0) - eslint-plugin-deprecation: 2.0.0(eslint@8.56.0)(typescript@5.4.2) + '@typescript-eslint/eslint-plugin': 7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.4.2) + eslint: 8.57.0 + eslint-config-prettier: 9.1.0(eslint@8.57.0) + eslint-plugin-deprecation: 2.0.0(eslint@8.57.0)(typescript@5.4.2) eslint-plugin-only-warn: 1.1.0 - eslint-plugin-prefer-arrow: 1.2.3(eslint@8.56.0) - eslint-plugin-simple-import-sort: 12.0.0(eslint@8.56.0) + eslint-plugin-prefer-arrow: 1.2.3(eslint@8.57.0) + eslint-plugin-simple-import-sort: 12.0.0(eslint@8.57.0) eslint-plugin-tsdoc: 0.2.17 dev: true - /eslint-plugin-deprecation@2.0.0(eslint@8.56.0)(typescript@5.4.2): + /eslint-plugin-deprecation@2.0.0(eslint@8.57.0)(typescript@5.4.2): resolution: {integrity: sha512-OAm9Ohzbj11/ZFyICyR5N6LbOIvQMp7ZU2zI7Ej0jIc8kiGUERXPNMfw2QqqHD1ZHtjMub3yPZILovYEYucgoQ==} peerDependencies: eslint: ^7.0.0 || ^8.0.0 typescript: ^4.2.4 || ^5.0.0 dependencies: - '@typescript-eslint/utils': 6.21.0(eslint@8.56.0)(typescript@5.4.2) - eslint: 8.56.0 + '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.2) + eslint: 8.57.0 tslib: 2.6.2 tsutils: 3.21.0(typescript@5.4.2) typescript: 5.4.2 @@ -2324,20 +2327,20 @@ packages: engines: {node: '>=6'} dev: true - /eslint-plugin-prefer-arrow@1.2.3(eslint@8.56.0): + /eslint-plugin-prefer-arrow@1.2.3(eslint@8.57.0): resolution: {integrity: sha512-J9I5PKCOJretVuiZRGvPQxCbllxGAV/viI20JO3LYblAodofBxyMnZAJ+WGeClHgANnSJberTNoFWWjrWKBuXQ==} peerDependencies: eslint: '>=2.0.0' dependencies: - eslint: 8.56.0 + eslint: 8.57.0 dev: true - /eslint-plugin-simple-import-sort@12.0.0(eslint@8.56.0): + /eslint-plugin-simple-import-sort@12.0.0(eslint@8.57.0): resolution: {integrity: sha512-8o0dVEdAkYap0Cn5kNeklaKcT1nUsa3LITWEuFk3nJifOoD+5JQGoyDUW2W/iPWwBsNBJpyJS9y4je/BgxLcyQ==} peerDependencies: eslint: '>=5.0.0' dependencies: - eslint: 8.56.0 + eslint: 8.57.0 dev: true /eslint-plugin-tsdoc@0.2.17: @@ -2360,15 +2363,15 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint@8.56.0: - resolution: {integrity: sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==} + /eslint@8.57.0: + resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@eslint-community/regexpp': 4.10.0 '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.56.0 + '@eslint/js': 8.57.0 '@humanwhocodes/config-array': 0.11.14 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 @@ -2590,13 +2593,13 @@ packages: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} dependencies: - flatted: 3.2.9 + flatted: 3.3.1 keyv: 4.5.4 rimraf: 3.0.2 dev: true - /flatted@3.2.9: - resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + /flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} dev: true /forever-agent@0.6.1: @@ -2669,9 +2672,9 @@ packages: dependencies: es-errors: 1.3.0 function-bind: 1.1.2 - has-proto: 1.0.1 + has-proto: 1.0.3 has-symbols: 1.0.3 - hasown: 2.0.1 + hasown: 2.0.2 dev: true /get-port@7.0.0: @@ -2684,8 +2687,8 @@ packages: engines: {node: '>=16'} dev: true - /get-tsconfig@4.7.2: - resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + /get-tsconfig@4.7.3: + resolution: {integrity: sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==} dependencies: resolve-pkg-maps: 1.0.0 dev: true @@ -2807,8 +2810,8 @@ packages: es-define-property: 1.0.0 dev: true - /has-proto@1.0.1: - resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + /has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} engines: {node: '>= 0.4'} dev: true @@ -2817,8 +2820,8 @@ packages: engines: {node: '>= 0.4'} dev: true - /hasown@2.0.1: - resolution: {integrity: sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==} + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} dependencies: function-bind: 1.1.2 @@ -2927,7 +2930,7 @@ packages: /is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} dependencies: - hasown: 2.0.1 + hasown: 2.0.2 dev: true /is-decimal@1.0.4: @@ -3001,8 +3004,8 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true - /isomorphic-git@1.25.5: - resolution: {integrity: sha512-gEsQpICiw7u68pitnHdJFe3ep6sLbz45uHdGac1NSKPm+Q22hxenIH9uXvXdCYfx7NhwOV7dUKgVk6rKzeNmCA==} + /isomorphic-git@1.25.6: + resolution: {integrity: sha512-zA3k3QOO7doqOnBgwsaXJwHKSIIl5saEdH4xxalu082WHVES4KghsG6RE2SDwjXMCIlNa1bWocbitH6bRIrmLQ==} engines: {node: '>=12'} hasBin: true dependencies: @@ -3424,7 +3427,7 @@ packages: acorn: 8.11.3 pathe: 1.1.2 pkg-types: 1.0.3 - ufo: 1.4.0 + ufo: 1.5.1 dev: true /ms@2.0.0: @@ -3695,7 +3698,7 @@ packages: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} dependencies: - side-channel: 1.0.5 + side-channel: 1.0.6 dev: true /qs@6.5.3: @@ -3918,8 +3921,8 @@ packages: - supports-color dev: true - /set-function-length@1.2.1: - resolution: {integrity: sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==} + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} dependencies: define-data-property: 1.1.4 @@ -3954,8 +3957,8 @@ packages: engines: {node: '>=8'} dev: true - /side-channel@1.0.5: - resolution: {integrity: sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==} + /side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.7 @@ -4163,7 +4166,7 @@ packages: hasBin: true dependencies: esbuild: 0.19.12 - get-tsconfig: 4.7.2 + get-tsconfig: 4.7.3 optionalDependencies: fsevents: 2.3.3 dev: true @@ -4214,8 +4217,8 @@ packages: hasBin: true dev: true - /ufo@1.4.0: - resolution: {integrity: sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==} + /ufo@1.5.1: + resolution: {integrity: sha512-HGyF79+/qZ4soRvM+nHERR2pJ3VXDZ/8sL1uLahdgEDf580NkgiWOxLk33FetExqOWp352JZRsgXbG/4MaGOSg==} dev: true /underscore@1.13.6: @@ -4520,8 +4523,8 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true - /xss@1.0.14: - resolution: {integrity: sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==} + /xss@1.0.15: + resolution: {integrity: sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==} engines: {node: '>= 0.10.0'} hasBin: true dependencies: diff --git a/src/ResultSet/ResultSet.test-d.ts b/src/ResultSet/ResultSet.test-d.ts new file mode 100644 index 000000000..4555a787c --- /dev/null +++ b/src/ResultSet/ResultSet.test-d.ts @@ -0,0 +1,119 @@ +/* eslint-disable @typescript-eslint/ban-types */ + +import { expectTypeOf, test } from 'vitest' +import type * as Schema from '../../tests/builder/_/schema.js' +import type { SelectionSet } from '../SelectionSet/__.js' +import type { ResultSet } from './__.js' + +type I = Schema.$.Index +type RS<$selectionSet extends SelectionSet.Query> = ResultSet.Query<$selectionSet, I> + +// dprint-ignore +test(`general`, () => { + // __typename + expectTypeOf>().toEqualTypeOf<{ __typename: 'Query' }>() + + // Scalar + expectTypeOf>().toEqualTypeOf<{ id: null | string }>() + expectTypeOf>().toEqualTypeOf<{ id: null | string }>() + // non-nullable + expectTypeOf>().toEqualTypeOf<{ idNonNull: string }>() + // indicator negative + expectTypeOf>().toEqualTypeOf<{ id: null | string }>() + expectTypeOf>().toEqualTypeOf<{ id: null | string }>() + expectTypeOf>().toEqualTypeOf<{ id: null | string }>() + + // List + expectTypeOf>().toEqualTypeOf<{ listIntNonNull: number[] }>() + expectTypeOf>().toEqualTypeOf<{ listInt: null|(null|number)[] }>() + expectTypeOf>().toEqualTypeOf<{ listListIntNonNull: number[][] }>() + expectTypeOf>().toEqualTypeOf<{ listListInt: null|((null|(null|number)[])[]) }>() + + // Enum + expectTypeOf>().toEqualTypeOf<{ abcEnum: null|'A'|'B'|'C' }>() + + // Object + expectTypeOf>().toEqualTypeOf<{ object: null | { id: string | null } }>() + // non-nullable + expectTypeOf>().toEqualTypeOf<{ objectNonNull: { id: string | null } }>() + + // scalars-wildcard + expectTypeOf>().toEqualTypeOf<{ objectNonNull: { __typename: "Object"; string: null|string; int: null|number; float: null|number; boolean: null|boolean; id: null|string; } }>() + // scalars-wildcard with nested object + expectTypeOf>().toEqualTypeOf<{ objectNested: null | { __typename: "ObjectNested"; id: null|string } }>() + // __typename + expectTypeOf>().toEqualTypeOf<{ objectNonNull: { __typename: "Object" } }>() + + // Union + expectTypeOf>().toEqualTypeOf<{ fooBarUnion: null | { __typename: "Foo" } | { __typename: "Bar" } }>() + expectTypeOf>().toEqualTypeOf<{ fooBarUnion: null | {} | { __typename: "Foo" } }>() + expectTypeOf>().toEqualTypeOf<{ fooBarUnion: null | {} | { id: null|string } }>() + expectTypeOf>().toEqualTypeOf<{ fooBarUnion: null | { __typename: "Bar" } | { __typename: "Foo"; id: null|string } }>() + + // Union fragments Case + expectTypeOf>().toEqualTypeOf<{ lowerCaseUnion: null | { __typename: 'lowerCaseObject'; id: null|string } | { __typename: 'lowerCaseObject2'; int: null|number } }>() + + + // Interface + expectTypeOf>().toEqualTypeOf<{ interface: null | { id: null | string} | {} }>() + expectTypeOf>().toEqualTypeOf<{ interface: null | { int: null | number} | {} }>() + expectTypeOf>().toEqualTypeOf<{ interface: null | { id: null | string} }>() + expectTypeOf>().toEqualTypeOf<{ interface: null | { id: null | string} }>() + expectTypeOf>().toEqualTypeOf<{ interface: null | { id: null | string} | { id: null | string; int: null | number }}>() + expectTypeOf>().toEqualTypeOf<{ interface: null | { __typename: 'Object1ImplementingInterface' } | { __typename: 'Object2ImplementingInterface' } }>() + expectTypeOf>().toEqualTypeOf<{ interface: null | { __typename: 'Object1ImplementingInterface' } | {}}>() + expectTypeOf>().toEqualTypeOf<{ interface: null | { __typename: 'Object1ImplementingInterface', id: null | string, int: null|number} | { __typename: 'Object2ImplementingInterface', id: null | string; boolean:null|boolean} }>() + + // Alias + // scalar + expectTypeOf>().toEqualTypeOf<{ id2: null | string }>() + expectTypeOf>().toEqualTypeOf<{ id2: string }>() + expectTypeOf>().toEqualTypeOf<{ id_as: ResultSet.Errors.UnknownFieldName<'id_as', Schema.Root.Query> }>() + expectTypeOf>().toEqualTypeOf<{ id_as_$: ResultSet.Errors.UnknownFieldName<'id_as_$', Schema.Root.Query> }>() + // union fragment + expectTypeOf>().toEqualTypeOf<{ fooBarUnion: null | {} | { id2: null|string } }>() + + // Directive @include + // On scalar non-nullable + expectTypeOf>().toEqualTypeOf<{ idNonNull: null|string }>() + expectTypeOf>().toEqualTypeOf<{ idNonNull: null|string }>() + expectTypeOf>().toEqualTypeOf<{ idNonNull: string }>() + expectTypeOf>().toEqualTypeOf<{ idNonNull: string }>() + expectTypeOf>().toEqualTypeOf<{ idNonNull: null }>() + expectTypeOf>().toEqualTypeOf<{ idNonNull: null }>() + // On scalar nullable + expectTypeOf>().toEqualTypeOf<{ id: null|string }>() + expectTypeOf>().toEqualTypeOf<{ id: null }>() + expectTypeOf>().toEqualTypeOf<{ id: null|string }>() + + // Directive @skip + // On scalar non-nullable + expectTypeOf>().toEqualTypeOf<{ idNonNull: null|string }>() + expectTypeOf>().toEqualTypeOf<{ idNonNull: null|string }>() + expectTypeOf>().toEqualTypeOf<{ idNonNull: null }>() + expectTypeOf>().toEqualTypeOf<{ idNonNull: null }>() + expectTypeOf>().toEqualTypeOf<{ idNonNull: string }>() + expectTypeOf>().toEqualTypeOf<{ idNonNull: string }>() + // On scalar nullable + expectTypeOf>().toEqualTypeOf<{ id: null|string }>() + expectTypeOf>().toEqualTypeOf<{ id: null|string }>() + expectTypeOf>().toEqualTypeOf<{ id: null }>() + + // Directive @defer + // todo + + // Directive @stream + // todo + + // Field Group + // todo + + // Arguments + // scalar + expectTypeOf>().toEqualTypeOf<{ stringWithArgs: null | string }>() + expectTypeOf>().toEqualTypeOf<{ stringWithArgs: null | string }>() + + // Errors + // unknown field + expectTypeOf>().toEqualTypeOf<{ id2: ResultSet.Errors.UnknownFieldName<'id2', Schema.Root.Query> }>() +}) diff --git a/src/ResultSet/ResultSet.ts b/src/ResultSet/ResultSet.ts new file mode 100644 index 000000000..0c4245009 --- /dev/null +++ b/src/ResultSet/ResultSet.ts @@ -0,0 +1,108 @@ +/* eslint-disable @typescript-eslint/ban-types */ + +import type { Simplify } from 'type-fest' +import type { GetKeyOr, SimplifyDeep } from '../lib/prelude.js' +import type { TSError } from '../lib/TSError.js' +import type { Schema } from '../Schema/__.js' +import type { SelectionSet } from '../SelectionSet/__.js' + +// dprint-ignore +export type Query<$SelectionSetQuery extends object, $Index extends Schema.Index> = + SimplifyDeep, $Index>> + +// dprint-ignore +export type Mutation<$SelectionSetMutation extends object, $Index extends Schema.Index> = + SimplifyDeep, $Index>> + +// dprint-ignore +export type Subscription<$SelectionSetSubscription extends object, $Index extends Schema.Index> = + SimplifyDeep, $Index>> + +// dprint-ignore +export type Object<$SelectionSet, $Node extends Schema.Named.Object, $Index extends Schema.Index> = + SelectionSet.IsSelectScalarsWildcard<$SelectionSet> extends true + + /** + * Handle Scalars Wildcard + */ + ? + { + [$Key in keyof $Node['fields'] as $Node['fields'][$Key] extends Schema.Field.Field | {'typeUnwrapped':{kind:'Scalar'}} ? $Key : never]: + // eslint-disable-next-line + // @ts-ignore infinite depth issue, can this be fixed? + Field<$SelectionSet, Schema.Field.As<$Node['fields'][$Key]>, $Index> + } + /** + * Handle fields in regular way. + */ + : + SelectionSet.ResolveAliasTargets<{ + [K in keyof SelectionSet.OmitNegativeIndicators<$SelectionSet> & string as K extends `${K}_as_${infer s}` ? s : K]: + SelectionSet.AliasNameOrigin extends keyof $Node['fields'] + ? Field<$SelectionSet[K], $Node['fields'][SelectionSet.AliasNameOrigin], $Index> + : Errors.UnknownFieldName, $Node> + }> + +// dprint-ignore +type Union<$SelectionSet, $Node extends Schema.Named.Union, $Index extends Schema.Index> = + OnTypeFragment<$SelectionSet,$Node['members'][number], $Index> + +// dprint-ignore +type Interface<$SelectionSet, $Node extends Schema.Named.Interface, $Index extends Schema.Index> = + OnTypeFragment<$SelectionSet, $Node['implementors'][number], $Index> + +// dprint-ignore +type OnTypeFragment<$SelectionSet, $Node extends Schema.Named.Object, $Index extends Schema.Index> = + $Node extends any // force distribution + ? Object< + GetKeyOr<$SelectionSet, `on${Capitalize<$Node['fields']['__typename']['type']['type']>}`, {}> & SelectionSet.OmitOnTypeFragments<$SelectionSet>, + $Node, + $Index + > + : never + +// dprint-ignore +type Field<$SelectionSet, $Field extends Schema.Field.Field, $Index extends Schema.Index> = + $SelectionSet extends SelectionSet.Directive.Include.Negative | SelectionSet.Directive.Skip.Positive ? + null : + ( + | FieldDirectiveInclude<$SelectionSet> + | FieldDirectiveSkip<$SelectionSet> + | FieldType<$SelectionSet, $Field['type'], $Index> + ) + +// dprint-ignore +type FieldType< + $SelectionSet, + $Type extends Schema.Field.Type.Any, + $Index extends Schema.Index +> =Simplify< + $Type extends Schema.Field.Type.__typename ? $Value : + $Type extends Schema.Field.Type.Nullable ? null | FieldType<$SelectionSet, $InnerType, $Index> : + $Type extends Schema.Field.Type.List ? Array> : + $Type extends Schema.Named.Enum ? $Members[number] : + $Type extends Schema.Named.Scalar.Any ? ReturnType<$Type['constructor']> : + $Type extends Schema.Named.Object ? Object<$SelectionSet,$Type,$Index> : + $Type extends Schema.Named.Interface ? Interface<$SelectionSet,$Type,$Index> : + $Type extends Schema.Named.Union ? Union<$SelectionSet,$Type,$Index> : + TSError<'FieldType', `Unknown type`, { $Type: $Type }> + > + +// dprint-ignore +type FieldDirectiveInclude<$SelectionSet> = + $SelectionSet extends SelectionSet.Directive.Include ? $SelectionSet extends SelectionSet.Directive.Include.Positive ? + never : + null + : never + +// dprint-ignore +type FieldDirectiveSkip<$SelectionSet> = + $SelectionSet extends SelectionSet.Directive.Skip ? $SelectionSet extends SelectionSet.Directive.Skip.Negative ? + never : + null + : never + +// dprint-ignore +export namespace Errors { + export type UnknownFieldName<$FieldName extends string, $Object extends Schema.Named.Object> = TSError<'Object', `field "${$FieldName}" does not exist on object "${$Object['fields']['__typename']['type']['type']}"`> +} diff --git a/src/ResultSet/__.ts b/src/ResultSet/__.ts new file mode 100644 index 000000000..fb546c609 --- /dev/null +++ b/src/ResultSet/__.ts @@ -0,0 +1 @@ +export * as ResultSet from './ResultSet.js' diff --git a/src/Schema/Field/Field.ts b/src/Schema/Field/Field.ts new file mode 100644 index 000000000..83912f8f1 --- /dev/null +++ b/src/Schema/Field/Field.ts @@ -0,0 +1,49 @@ +import type { NamedType } from '../NamedType/__.js' +import type { Scalar } from '../NamedType/Scalar/_.js' +import type * as Type from './Type.js' +import { unwrap } from './Type.js' + +export type * as Type from './Type.js' + +export type As = T extends Field ? T : never + +export type Enum<$Args extends Args | null = null> = Field + +export type Scalar<$Args extends Args | null = Args | null> = Field + +export type String<$Args extends Args | null = null> = Field + +export type Number<$Args extends Args | null = null> = Field + +export type Boolean<$Args extends Args | null = null> = Field + +export namespace Input { + export type Nullable = Type.Nullable + export type List = Type.List + export type Any = Scalar.Any | List | Nullable +} + +// export interface Args<$Fields extends Record = Record> { +export interface Args<$Fields extends any = any> { + allOptional: Exclude<$Fields[keyof $Fields], Type.Nullable> extends never ? true : false + fields: $Fields +} + +export const field = <$Type extends Type.Any, $Args extends null | Args = null>( + type: $Type, + args: $Args = null as $Args, +): Field<$Type, $Args> => { + return { + // eslint-disable-next-line + // @ts-ignore infinite depth issue, can this be fixed? + typeUnwrapped: unwrap(type), + type, + args, + } +} + +export type Field<$Type extends any = any, $Args extends Args | null = Args | null> = { + typeUnwrapped: Type.Unwrap<$Type> + type: $Type + args: $Args +} diff --git a/src/Schema/Field/Type.ts b/src/Schema/Field/Type.ts new file mode 100644 index 000000000..63e26b4ed --- /dev/null +++ b/src/Schema/Field/Type.ts @@ -0,0 +1,38 @@ +import type { TSError } from '../../lib/TSError.js' +import type { NamedType } from '../NamedType/__.js' + +export interface __typename<$Type extends string = string> { + kind: 'typename' + type: $Type +} +export interface Nullable<$Type extends Any> { + kind: 'nullable' + type: $Type +} +export interface List<$Type extends Any> { + kind: 'list' + type: $Type +} + +export type Any = List | __typename | Nullable | NamedType.Any + +export const __typename = <$Type extends string>(type: $Type): __typename<$Type> => ({ kind: `typename`, type }) +export const nullable = <$Type extends __typename | List>(type: $Type): Nullable<$Type> => ({ + kind: `nullable`, + type, +}) +export const list = <$Type extends Any>(type: $Type): List<$Type> => ({ kind: `list`, type }) + +// todo extends any because of infinite depth issue in generated schema types +// dprint-ignore +export type Unwrap<$Type extends any> = + $Type extends List ? Unwrap<$innerType> : + $Type extends Nullable ? Unwrap<$innerType> : + $Type extends __typename ? $Type['type'] : + $Type extends NamedType.Any ? $Type : + TSError<'Unwrap', 'Unknown $Type', { $Type: $Type }> + +export const unwrap = <$Type extends Any>(type: $Type): Unwrap<$Type> => { + // @ts-expect-error fixme + return type.kind === `named` ? type.type : unwrap(type.type) +} diff --git a/src/Schema/Field/_.ts b/src/Schema/Field/_.ts new file mode 100644 index 000000000..f9956dd53 --- /dev/null +++ b/src/Schema/Field/_.ts @@ -0,0 +1 @@ +export * as Field from './__.js' diff --git a/src/Schema/Field/__.ts b/src/Schema/Field/__.ts new file mode 100644 index 000000000..3292e0087 --- /dev/null +++ b/src/Schema/Field/__.ts @@ -0,0 +1,2 @@ +export * from './Field.js' +export * from './Type.js' diff --git a/src/Schema/Index.ts b/src/Schema/Index.ts new file mode 100644 index 000000000..99f7eff38 --- /dev/null +++ b/src/Schema/Index.ts @@ -0,0 +1,16 @@ +/* eslint-disable @typescript-eslint/ban-types */ + +import type { Object, Union } from './__.js' + +export interface Index { + Root: { + Query: null | Object + Mutation: null | Object + Subscription: null | Object + } + objects: Record + unions: { + Union: null | Union + } + scalars: object +} diff --git a/src/Schema/NamedType/Enum.ts b/src/Schema/NamedType/Enum.ts new file mode 100644 index 000000000..cec53ca24 --- /dev/null +++ b/src/Schema/NamedType/Enum.ts @@ -0,0 +1,19 @@ +import type { __typename } from '../__.js' + +export interface Enum< + $Name extends string = string, + $Members extends [string, ...string[]] = [string, ...string[]], +> { + kind: 'Enum' + name: $Name + members: $Members +} + +export const Enum = <$Name extends string, $Members extends [string, ...string[]]>( + name: $Name, + members: $Members, +): Enum<$Name, $Members> => ({ + kind: `Enum`, + name, + members, +}) diff --git a/src/Schema/NamedType/Interface.ts b/src/Schema/NamedType/Interface.ts new file mode 100644 index 000000000..59cf32ff4 --- /dev/null +++ b/src/Schema/NamedType/Interface.ts @@ -0,0 +1,31 @@ +/* eslint-disable @typescript-eslint/ban-types */ + +import type { __typename } from '../__.js' +import type { Field } from '../Field/Field.js' +import type { Object } from './Object.js' + +export type Interface< + $Name extends string = string, + $Fields extends Record> = Record>, + $Implementors extends [Object, ...Object[]] = [Object, ...Object[]], +> = { + kind: 'Interface' + name: $Name + fields: $Fields + implementors: $Implementors +} + +export const Interface = < + $Name extends string, + $Fields extends Record, + $Implementors extends [Object, ...Object[]], +>( + name: $Name, + fields: $Fields, + implementors: $Implementors, +): Interface<$Name, $Fields, $Implementors> => ({ + kind: `Interface`, + name, + fields, + implementors, +}) diff --git a/src/Schema/NamedType/NamedType.test-d.ts b/src/Schema/NamedType/NamedType.test-d.ts new file mode 100644 index 000000000..df28fbcde --- /dev/null +++ b/src/Schema/NamedType/NamedType.test-d.ts @@ -0,0 +1,17 @@ +import { expectTypeOf, test } from 'vitest' +import type * as NamedType from './NamedType.js' + +test(`NameParse`, () => { + expectTypeOf>().toEqualTypeOf<'a'>() + expectTypeOf>().toEqualTypeOf<'a1'>() + expectTypeOf>().toEqualTypeOf<'A'>() + expectTypeOf>().toEqualTypeOf<'aa'>() + expectTypeOf>().toEqualTypeOf<'a_'>() + expectTypeOf>().toEqualTypeOf<'a__'>() + expectTypeOf>().toEqualTypeOf<'a__b'>() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() +}) diff --git a/src/Schema/NamedType/NamedType.ts b/src/Schema/NamedType/NamedType.ts new file mode 100644 index 000000000..1def4305b --- /dev/null +++ b/src/Schema/NamedType/NamedType.ts @@ -0,0 +1,30 @@ +/* eslint-disable @typescript-eslint/ban-types */ + +import type { Digit, Letter } from '../../lib/prelude.js' +import type { Enum } from './Enum.js' +import type { Interface } from './Interface.js' +import type { Object } from './Object.js' +import type { Scalar } from './Scalar/_.js' +import type { Union } from './Union.js' + +export type Any = Interface | Enum | Object | Scalar.Any | Union + +/** + * @see http://spec.graphql.org/draft/#sec-Names + */ +// dprint-ignore +export type NameParse = + T extends NameHead ? T : + T extends `${NameHead}${infer Rest}` ? Rest extends NameBodyParse ? T + : never + : never + +// dprint-ignore +export type NameBodyParse = + S extends NameBody ? S : + S extends `${NameBody}${infer Rest}` ? NameBodyParse extends string ? S + : never + : never + +export type NameHead = Letter | '_' +export type NameBody = Letter | '_' | Digit diff --git a/src/Schema/NamedType/Object.ts b/src/Schema/NamedType/Object.ts new file mode 100644 index 000000000..efd3b53b6 --- /dev/null +++ b/src/Schema/NamedType/Object.ts @@ -0,0 +1,32 @@ +/* eslint-disable @typescript-eslint/ban-types */ + +import type { Field } from '../Field/Field.js' +import { field } from '../Field/Field.js' +import { __typename } from '../Field/Type.js' + +export type Fields = Record> + +export type ObjectFields = { + __typename: Field<__typename> +} & Fields + +export interface Object< + $Name extends string = string, + $Fields extends Fields = Fields, +> { + kind: 'Object' + fields: { + __typename: Field<__typename<$Name>> + } & $Fields +} + +export const Object = <$Name extends string, $Fields extends Record>( + name: $Name, + fields: $Fields, +): Object<$Name, $Fields> => ({ + kind: `Object`, + fields: { + __typename: field(__typename(name)), + ...fields, + }, +}) diff --git a/src/Schema/NamedType/Scalar/Scalar.ts b/src/Schema/NamedType/Scalar/Scalar.ts new file mode 100644 index 000000000..acee465a6 --- /dev/null +++ b/src/Schema/NamedType/Scalar/Scalar.ts @@ -0,0 +1,47 @@ +/* eslint-disable @typescript-eslint/ban-types */ + +import { nativeScalarConstructors } from './nativeConstructors.js' + +export const ScalarKind = `Scalar` as const + +export type ScalarKind = typeof ScalarKind + +export const scalar = <$Name extends string, $TypeConstructor extends () => string | number | boolean>( + name: $Name, + constructor: $TypeConstructor, +): Scalar<$Name, $TypeConstructor> => ({ + kind: ScalarKind, + name: name, + constructor: constructor as any, // eslint-disable-line +}) + +export interface Scalar< + $Name extends string = string, + $TypeConstructor extends () => string | number | boolean = () => string | number | boolean, +> { + kind: ScalarKind + name: $Name + constructor: $TypeConstructor +} + +export const String = scalar(`String`, nativeScalarConstructors.String) + +export const ID = scalar(`ID`, nativeScalarConstructors.String) + +export const Int = scalar(`Int`, nativeScalarConstructors.Number) + +export const Float = scalar(`Float`, nativeScalarConstructors.Number) + +export const Boolean = scalar(`Boolean`, nativeScalarConstructors.Boolean) + +export type ID = typeof ID + +export type String = typeof String + +export type Int = typeof Int + +export type Boolean = typeof Boolean + +export type Float = typeof Float + +export type Any = String | Int | Boolean | ID | Float diff --git a/src/Schema/NamedType/Scalar/_.ts b/src/Schema/NamedType/Scalar/_.ts new file mode 100644 index 000000000..e21bb3cfa --- /dev/null +++ b/src/Schema/NamedType/Scalar/_.ts @@ -0,0 +1 @@ +export * as Scalar from './Scalar.js' diff --git a/src/Schema/NamedType/Scalar/nativeConstructors.ts b/src/Schema/NamedType/Scalar/nativeConstructors.ts new file mode 100644 index 000000000..5eeef04ac --- /dev/null +++ b/src/Schema/NamedType/Scalar/nativeConstructors.ts @@ -0,0 +1,5 @@ +export const nativeScalarConstructors = { + String: String, + Number: Number, + Boolean: Boolean, +} diff --git a/src/Schema/NamedType/Union.ts b/src/Schema/NamedType/Union.ts new file mode 100644 index 000000000..e3a1eaa4e --- /dev/null +++ b/src/Schema/NamedType/Union.ts @@ -0,0 +1,25 @@ +/* eslint-disable @typescript-eslint/ban-types */ + +import type { __typename } from '../__.js' +import type { Object } from './Object.js' + +export type Union< + $Name extends string = string, + $Members extends [Object, ...Object[]] = [Object, ...Object[]], +> = { + kind: `Union` + name: $Name + members: $Members +} + +export const Union = < + $Name extends string, + $Members extends [Object, ...Object[]], +>( + name: $Name, + members: $Members, +): Union<$Name, $Members> => ({ + kind: `Union`, + name, + members, +}) diff --git a/src/Schema/NamedType/_.ts b/src/Schema/NamedType/_.ts new file mode 100644 index 000000000..c2650fcae --- /dev/null +++ b/src/Schema/NamedType/_.ts @@ -0,0 +1,6 @@ +export * from './Enum.js' +export * from './Interface.js' +export * from './NamedType.js' +export * from './Object.js' +export * from './Scalar/_.js' +export * from './Union.js' diff --git a/src/Schema/NamedType/__.ts b/src/Schema/NamedType/__.ts new file mode 100644 index 000000000..974d0f166 --- /dev/null +++ b/src/Schema/NamedType/__.ts @@ -0,0 +1 @@ +export * as NamedType from './_.js' diff --git a/src/Schema/__.ts b/src/Schema/__.ts new file mode 100644 index 000000000..3574a0b54 --- /dev/null +++ b/src/Schema/__.ts @@ -0,0 +1,5 @@ +export * as Schema from './__Schema.js' +export { Args, As, Field } from './Field/__.js' +export { __typename, List, Nullable } from './Field/Type.js' +export { Index } from './Index.js' +export * from './NamedType/_.js' diff --git a/src/Schema/__Schema.ts b/src/Schema/__Schema.ts new file mode 100644 index 000000000..d8ec79685 --- /dev/null +++ b/src/Schema/__Schema.ts @@ -0,0 +1,3 @@ +export * from './Field/_.js' +export { Index } from './Index.js' +export * as Named from './NamedType/_.js' diff --git a/src/SelectionSet/SelectionSet.test-d.ts b/src/SelectionSet/SelectionSet.test-d.ts new file mode 100644 index 000000000..356055734 --- /dev/null +++ b/src/SelectionSet/SelectionSet.test-d.ts @@ -0,0 +1,213 @@ +import { assertType, expectTypeOf, test } from 'vitest' +import type * as Schema from '../../tests/builder/_/schema.js' +import type { SelectionSet } from './__.js' + +type Q = SelectionSet.Query + +test(`ParseAliasExpression`, () => { + expectTypeOf>().toEqualTypeOf>() + expectTypeOf>().toEqualTypeOf<'a'>() + expectTypeOf>().toEqualTypeOf<'$'>() + expectTypeOf>().toEqualTypeOf<'a_as_$'>() + expectTypeOf>().toEqualTypeOf<'$_as_b'>() + expectTypeOf>().toEqualTypeOf<'1_as_2'>() +}) + +test(`Query`, () => { + assertType({ __typename: true }) + + // @ts-expect-error directive on root type Query + assertType({ $defer: true }) + + // Scalar + assertType({ id: true }) + assertType({ id: true }) + assertType({ id: false }) + assertType({ id: 1 }) + assertType({ id: 0 }) + assertType({ id: undefined }) + // non-null + assertType({ idNonNull: true }) + + // Enum + assertType({ abcEnum: true }) + + // Object + assertType({ object: { id: true } }) + // typename + assertType({ __typename: true }) + // Non-Null + assertType({ objectNonNull: { id: true } }) + // @ts-expect-error excess property check + assertType({ id2: true }) + // @ts-expect-error excess property check + assertType({ object: { a2: true } }) + + // Union + assertType({ fooBarUnion: { __typename: true } }) + assertType({ fooBarUnion: { onFoo: { __typename: true } } }) + assertType({ fooBarUnion: { onFoo: { id: true } } }) + // @ts-expect-error no b + assertType({ fooBarUnion: { onFoo: { id2: true } } }) + assertType({ fooBarUnion: { onBar: { __typename: true } } }) + assertType({ fooBarUnion: { onBar: { int: true } } }) + // @ts-expect-error no a + assertType({ fooBarUnion: { onBar: { int2: true } } }) + + // Union fragments Case + assertType({ lowerCaseUnion: { onLowerCaseObject: { id: true }, onLowerCaseObject2: { int: true } } }) + + // Interface + assertType({ interface: { id: true } }) + assertType({ interface: { id: { $defer: true } } }) + assertType({ interface: { id: { $include: true } } }) + assertType({ interface: { id: { $skip: true } } }) + assertType({ interface: { id: { $stream: true } } }) + assertType({ interface: { __typename: true } }) + assertType({ interface: { __typename: { $defer: true } } }) + assertType({ interface: { $scalars: true } }) + // @ts-expect-error needs fragment + assertType({ interface: { id: true, int: true } }) + // @ts-expect-error needs fragment + assertType({ interface: { id: true, boolean: true } }) + assertType({ interface: { id: true, onObject1ImplementingInterface: { int: true } } }) + assertType({ interface: { id: true, onObject2ImplementingInterface: { boolean: true } } }) + // @ts-expect-error incorrect implementor name + assertType({ interface: { id: true, onObject1ImplementingInterface2: { int: true } } }) + // directives work on fragments + assertType({ interface: { id: true, onObject1ImplementingInterface: { $include: true } } }) // todo should REQUIRE field selection + + // Alias + // alias: enum + assertType({ abcEnum_as_enum: true }) + // alias: object + assertType({ object_as_o: { id: true } }) + // @ts-expect-error invalid alias key format + assertType({ object_as_: { id: true } }) + // @ts-expect-error invalid alias key format + assertType({ object_as: { id: true } }) + // @ts-expect-error invalid alias key format + assertType({ object2_as_o: { id: true } }) + + // directives + // @skip + // on scalar + assertType({ string: { $skip: true } }) + assertType({ string: { $skip: false } }) + assertType({ string: { $skip: { if: true } } }) + assertType({ string: { $skip: { if: false } } }) + assertType({ string: { $skip: {} } }) + assertType({ string: { $skip: {} } }) + // assertType({ string: skip() }) + // on object + assertType({ object: { $skip: true, string: true } }) + // assertType({ scalars: skip().select({ a: true }) }) + // on fragment + assertType({ fooBarUnion: { onBar: { $skip: true, int: true } } }) + // @include + assertType({ string: { $include: true } }) + assertType({ string: { $include: false } }) + assertType({ string: { $include: { if: true } } }) + assertType({ string: { $include: { if: false } } }) + assertType({ string: { $include: {} } }) + assertType({ string: { $include: {} } }) + // assertType({ string: include() }) + + // @defer + assertType({ string: { $defer: true } }) + assertType({ string: { $defer: { if: true, label: `foo` } } }) + assertType({ string: { $defer: { if: true } } }) + assertType({ string: { $defer: {} } }) + + // (todo limit to lists?) + // @stream + assertType({ string: { $stream: true } }) + assertType({ string: { $stream: { if: true, label: `foo`, initialCount: 0 } } }) + assertType({ string: { $stream: { if: true, label: `foo` } } }) + assertType({ string: { $stream: { if: true } } }) + assertType({ string: { $stream: {} } }) + + // Field Group + // On Object + assertType({ object: { ___: { int: true, id: true } } }) + assertType({ object: { ___: { $skip: true, int: true, id: true } } }) + assertType({ object: { ___: [{ $skip: true, int: true, id: true }] } }) + // On Root (Query) + assertType({ ___: { id: true } }) + assertType({ ___: { $skip: true, id: true } }) + + // Arguments + // all-optional on object + assertType({ objectWithArgs: { $: {}, id: true } }) + assertType({ objectWithArgs: { id: true } }) + assertType({ + objectWithArgs: { + $: { + boolean: true, + float: 1, + id: `id`, + int: 3, + string: `abc`, + }, + id: true, + }, + }) + // builder interface + // assertType({ foo: args({ ... }) }) + // all-optional on scalar + assertType({ stringWithArgs: true }) + assertType({ stringWithArgs: {} }) + assertType({ stringWithArgs: { $: { boolean: true, float: 1, id: `id`, int: 3, string: `abc` } } }) + assertType({ stringWithArgs: { $: { boolean: null, float: null, id: null, int: null, string: null } } }) + + // enum arg + assertType({ stringWithArgEnum: { $: { ABCEnum: `A` } } }) + assertType({ stringWithArgEnum: { $: { ABCEnum: `B` } } }) + assertType({ stringWithArgEnum: { $: { ABCEnum: `C` } } }) + assertType({ stringWithArgEnum: { $: { ABCEnum: null } } }) + assertType({ stringWithArgEnum: { $: {} } }) + // @ts-expect-error invalid enum value + assertType({ stringWithArgEnum: { $: { ABCEnum: `D` } } }) + // @ts-expect-error invalid enum value + assertType({ stringWithArgEnum: { $: { ABCEnum: `` } } }) + // @ts-expect-error invalid enum value + assertType({ stringWithArgEnum: { $: { ABCEnum: 1 } } }) + + // input list + assertType({ stringWithListArg: { $: { ints: [1, 2, 3] } } }) + assertType({ stringWithListArg: { $: { ints: [] } } }) + assertType({ stringWithListArg: { $: { ints: [null] } } }) + assertType({ stringWithListArg: { $: { ints: null } } }) + assertType({ stringWithListArg: { $: {} } }) + // @ts-expect-error missing "ints" arg + assertType({ stringWithListArgRequired: { $: {} } }) + // @ts-expect-error missing non-null "ints" arg + assertType({ stringWithListArgRequired: { $: { ints: null } } }) + + // all-optional + scalar + directive + assertType({ stringWithArgs: { $: { boolean: true }, $skip: true } }) + // builder interface + // assertType({ foo: args({ boolean: true }).skip().select({ x: 1 }) }) + // 1+ required + scalar + assertType({ stringWithRequiredArg: { $: { string: `` } } }) + // @ts-expect-error missing "string" arg + assertType({ stringWithRequiredArg: { $: {} } }) + // @ts-expect-error missing args ("$") + assertType({ stringWithRequiredArg: {} }) + + // Scalars Wildcard ("client directive") + // object + assertType({ object: { $scalars: true } }) + // @ts-expect-error no directives on scalars field + assertType({ scalars: { $scalars: { $skip: true } } }) + // union fragment + assertType({ fooBarUnion: { onBar: { $scalars: true } } }) + // assertType({ scalars: select() }) + + // todo empty selection set not allowed, with arguments given + // todo empty selection set not allowed, with directive given + // todo empty selection set not allowed + // // @ts-expect-error empty selection set not allowed + // assertType({ scalars: {} }) + // todo selection set of _only_ negative indicators should not be allowed +}) diff --git a/src/SelectionSet/SelectionSet.ts b/src/SelectionSet/SelectionSet.ts new file mode 100644 index 000000000..c3013bc3c --- /dev/null +++ b/src/SelectionSet/SelectionSet.ts @@ -0,0 +1,279 @@ +/* eslint-disable @typescript-eslint/ban-types */ + +import type { MaybeList, StringNonEmpty, Values } from '../lib/prelude.js' +import type { TSError } from '../lib/TSError.js' +import type { Schema } from '../Schema/__.js' + +export type Query<$Index extends Schema.Index> = $Index['Root']['Query'] extends Schema.Named.Object + ? Object<$Index['Root']['Query'], $Index> + : never + +export type Mutation<$Index extends Schema.Index> = $Index['Root']['Mutation'] extends Schema.Named.Object + ? Object<$Index['Root']['Mutation'], $Index> + : never + +export type Subscription<$Index extends Schema.Index> = $Index['Root']['Subscription'] extends Schema.Named.Object + ? Object<$Index['Root']['Subscription'], $Index> + : never + +// dprint-ignore +type Object< + $Fields extends Schema.Named.Object, + $Index extends Schema.Index, +> = Fields<$Fields['fields'], $Index> + +// dprint-ignore +type Fields< + $Fields extends Schema.Named.Fields, + $Index extends Schema.Index, +> = + & + { + [Key in keyof $Fields]?: + // eslint-disable-next-line + // @ts-ignore excessive deep error, fixme? + Field, $Index> + } + & + /** + * Alias support. + * Allow every field to also be given as a key with this pattern `_as_: ...` + */ + { + [ + Key in keyof $Fields as `${keyof $Fields & string}_as_${StringNonEmpty}` + ]?: + Field, $Index> + } + & + /** + * Inline fragments for field groups. + * @see https://spec.graphql.org/draft/#sec-Inline-Fragments + */ + { + ___?: MaybeList & FieldDirectives> + } + & + /** + * Special property to select all scalars. + */ + { + $scalars?: ClientIndicator + } + +export type IsSelectScalarsWildcard = SS extends { $scalars: ClientIndicatorPositive } ? true : false + +// dprint-ignore +export type Field< + $Field extends Schema.Field.Field, + $Index extends Schema.Index, +> = + $Field['type']['kind'] extends 'typename' ? NoArgsIndicator : + // eslint-disable-next-line + // @ts-ignore infinite depth issue, can this be fixed? + $Field['typeUnwrapped']['kind'] extends 'Scalar' ? Indicator<$Field> : + $Field['typeUnwrapped']['kind'] extends 'Enum' ? Indicator<$Field> : + $Field['typeUnwrapped']['kind'] extends 'Object' ? Object<$Field['typeUnwrapped'], $Index> & FieldDirectives & Arguments<$Field> : + $Field['typeUnwrapped']['kind'] extends 'Union' ? Union<$Field['typeUnwrapped'], $Index> : + $Field['typeUnwrapped']['kind'] extends 'Interface' ? Interface<$Field['typeUnwrapped'], $Index> : + TSError<'SelectionSetField', '$Field case not handled', { $Field: $Field }> +// dprint-ignore +type Arguments<$Field extends Schema.Field.Field> = +$Field['args'] extends Schema.Field.Args ? $Field['args']['allOptional'] extends true ? { $?: Args<$Field['args']> } : + { $: Args<$Field['args']> } : + {} + +// dprint-ignore +type Interface<$Node extends Schema.Named.Interface, $Index extends Schema.Index> = + & InterfaceDistributed<$Node['implementors'][number], $Index> + & Fields< + & $Node['fields'] + & { + __typename: $Node['implementors'][number]['fields']['__typename'] + }, + $Index + > + +// dprint-ignore +type InterfaceDistributed<$Node extends Schema.Named.Object, $Index extends Schema.Index> = + $Node extends any + ? { + [$typename in $Node['fields']['__typename']['type']['type'] as `on${Capitalize<$typename>}`]?: + Object<$Node, $Index> & FieldDirectives + } + : never + +// dprint-ignore +type Union<$Node extends Schema.Named.Union, $Index extends Schema.Index> = + & UnionDistributed<$Node['members'][number], $Index> + & { __typename?: NoArgsIndicator } + +// dprint-ignore +type UnionDistributed<$Object extends Schema.Named.Object,$Index extends Schema.Index> = + $Object extends any + ? { + [$typename in $Object['fields']['__typename']['type']['type'] as `on${Capitalize<$typename>}`]?: + Object<$Object, $Index> & FieldDirectives + } + : never + +/** + * Helpers + * --------------------------------------------------------------------------------------------------- + */ + +/** + * Unions + */ + +export type UnionFragmentExtractName = T extends `on${infer $Name}` ? $Name : never +export type UnionExtractFragmentNames = Values< + { + [Key in keyof T]: UnionFragmentExtractName + } +> +export type OmitOnTypeFragments = { + [$K in keyof T as $K extends `on${StringNonEmpty}` ? never : $K]: T[$K] +} + +/** + * Aliases + */ + +export interface Alias { + origin: O + target: T +} + +// dprint-ignore +export type ParseAliasExpression = + E extends `${infer O}_as_${infer T}` ? Schema.Named.NameParse extends never ? E : + Schema.Named.NameParse extends never ? E : + Alias + : E + +export type AliasNameOrigin = ParseAliasExpression extends Alias ? O : N + +export type AliasNameTarget = ParseAliasExpression extends Alias ? T : N + +export type ResolveAliasTargets = { + [Field in keyof SelectionSet as AliasNameTarget]: SelectionSet[Field] +} + +/** + * Directives + */ + +export namespace Directive { + export type Include = { $include: boolean | { if: boolean } } + export namespace Include { + export type Positive = { $include: true | { if: true } } + export type Negative = { $include: false | { if: false } } + } + export type Skip = { $skip: boolean | { if: boolean } } + export namespace Skip { + export type Positive = { $skip: true | { if: true } } + export type Negative = { $skip: false | { if: false } } + } + export type Defer = { $defer: boolean | { if?: boolean; label?: string } } + export namespace Defer { + export type Positive = { $defer: true | { if: true } } + export type Negative = { $defer: false | { if: false } } + } + export type Stream = { $stream: boolean | { if?: boolean; label?: string; initialCount?: number } } + export namespace Stream { + export type Positive = { $stream: true | { if: true } } + export type Negative = { $stream: false | { if: false } } + } +} + +/** + * Indicators + */ + +/** + * Should this field be selected? + */ +export type ClientIndicator = ClientIndicatorPositive | ClientIndicatorNegative +export type ClientIndicatorPositive = true | 1 +export type ClientIndicatorNegative = false | 0 | undefined + +/** + * @see https://regex101.com/r/XfOTMX/1 + * @see http://spec.graphql.org/draft/#sec-Names + */ +export const aliasPattern = /^(?[A-z][A-z_0-9]*)_as_(?[A-z][A-z_0-9]*)$/ +export const fragmentPattern = /^on(?[A-Z][A-z_0-9]*)$/ + +export type OmitNegativeIndicators<$SelectionSet> = { + [K in keyof $SelectionSet as $SelectionSet[K] extends ClientIndicatorNegative ? never : K]: $SelectionSet[K] +} + +/** + * Field selection in general, with directives support too. + * If a field directive is given as an indicator then it implies "select this" e.g. `true`/`1`. + * Of course the semantics of the directive may change the derived type (e.g. `skip` means the field might not show up in the result set) + */ +export type NoArgsIndicator = ClientIndicator | FieldDirectives + +// dprint-ignore +export type Indicator<$Field extends Schema.Field.Field = Schema.Field.Field> = +// $Field['args']['allOptional'] +$Field['args'] extends Schema.Field.Args ? $Field['args']['allOptional'] extends true + ? ({ $?: Args<$Field['args']> } & FieldDirectives) | ClientIndicator : + { $: Args<$Field['args']> } & FieldDirectives : + NoArgsIndicator + +// dprint-ignore +export type Args<$Args extends Schema.Field.Args> = +& { + [ + Key in keyof $Args['fields'] as $Args['fields'][Key] extends Schema.Field.Nullable ? never : Key + ]: InferTypeInput<$Args['fields'][Key]> +} +& { + [ + Key in keyof $Args['fields'] as $Args['fields'][Key] extends Schema.Field.Nullable ? Key : never + ]?: null | InferTypeInput<$Args['fields'][Key]> +} + +// todo input objects +// dprint-ignore +type InferTypeInput<$InputType extends Schema.Field.Input.Any> = + $InputType extends Schema.Field.Input.Nullable ? InferTypeInput<$InputType['type']> | null : + $InputType extends Schema.Field.Input.List ? InferTypeInput<$InputType['type']>[] : + $InputType extends Schema.Named.Enum ? $Members[number] : + $InputType extends Schema.Named.Scalar.Any ? ReturnType<$InputType['constructor']> : + TSError<'InferTypeInput', 'Unknown $InputType', { $InputType: $InputType }> // never + +/** + * @see https://spec.graphql.org/draft/#sec-Type-System.Directives.Built-in-Directives + */ +export interface FieldDirectives { + /** + * https://spec.graphql.org/draft/#sec--skip + */ + $skip?: boolean | { if?: boolean } + /** + * https://spec.graphql.org/draft/#sec--include + */ + $include?: boolean | { if?: boolean } + /** + * @see https://github.com/graphql/graphql-wg/blob/main/rfcs/DeferStream.md#defer + */ + $defer?: boolean | { if?: boolean; label?: string } + /** + * @see https://github.com/graphql/graphql-wg/blob/main/rfcs/DeferStream.md#stream + */ + $stream?: boolean | { if?: boolean; label?: string; initialCount?: number } +} + +// type UnwrapListAndNullableFieldType<$Field extends Schema.Field> = UnwrapListAndNullableFieldType_<$Field['type']> + +// // dprint-ignore +// type UnwrapListAndNullableFieldType_ = +// FT extends Schema.FieldTypeList ? UnwrapListAndNullableFieldType_ : +// FT extends Schema.FieldTypeNullable ? UnwrapListAndNullableFieldType_ : +// FT extends Schema.FieldTypeLiteral ? FT : +// FT extends Schema.FieldTypeNamed ? FT +// : TSError<'UnwrapFieldType_', 'FT case not handled', { FT: FT }> diff --git a/src/SelectionSet/_.ts b/src/SelectionSet/_.ts new file mode 100644 index 000000000..fadcf1c32 --- /dev/null +++ b/src/SelectionSet/_.ts @@ -0,0 +1,2 @@ +export * from './SelectionSet.js' +export * from './toGraphQLDocumentString.js' diff --git a/src/SelectionSet/__.ts b/src/SelectionSet/__.ts new file mode 100644 index 000000000..d927f56c3 --- /dev/null +++ b/src/SelectionSet/__.ts @@ -0,0 +1 @@ +export * as SelectionSet from './_.js' diff --git a/src/SelectionSet/__snapshots__/toGraphQLDocumentString.test.ts.snap b/src/SelectionSet/__snapshots__/toGraphQLDocumentString.test.ts.snap new file mode 100644 index 000000000..0ea0838af --- /dev/null +++ b/src/SelectionSet/__snapshots__/toGraphQLDocumentString.test.ts.snap @@ -0,0 +1,866 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`$defer > Query 1`] = ` +" +{ + "object": { + "$defer": true, + "id": true + } +} +-------------- +{ + object @defer(if: true) { + id + } +} +" +`; + +exports[`$defer > Query 2`] = ` +" +{ + "object": { + "$defer": false, + "id": true + } +} +-------------- +{ + object @defer(if: false) { + id + } +} +" +`; + +exports[`$defer > Query 3`] = ` +" +{ + "object": { + "id": true + } +} +-------------- +{ + object { + id + } +} +" +`; + +exports[`$defer > Query 4`] = ` +" +{ + "object": { + "$defer": { + "if": true + }, + "id": true + } +} +-------------- +{ + object @defer(if: true) { + id + } +} +" +`; + +exports[`$defer > Query 5`] = ` +" +{ + "object": { + "$defer": { + "if": false + }, + "id": true + } +} +-------------- +{ + object @defer(if: false) { + id + } +} +" +`; + +exports[`$defer > Query 6`] = ` +" +{ + "object": { + "$defer": {}, + "id": true + } +} +-------------- +{ + object @defer(if: true) { + id + } +} +" +`; + +exports[`$defer > Query 7`] = ` +" +{ + "object": { + "$defer": {}, + "id": true + } +} +-------------- +{ + object @defer(if: true) { + id + } +} +" +`; + +exports[`$defer > Query 8`] = ` +" +{ + "object": { + "$defer": { + "label": "foobar" + }, + "id": true + } +} +-------------- +{ + object @defer(if: true, label: "foobar") { + id + } +} +" +`; + +exports[`$include > Query 1`] = ` +" +{ + "object": { + "$include": true, + "id": true + } +} +-------------- +{ + object @include(if: true) { + id + } +} +" +`; + +exports[`$include > Query 2`] = ` +" +{ + "object": { + "$include": false, + "id": true + } +} +-------------- +{ + object @include(if: false) { + id + } +} +" +`; + +exports[`$include > Query 3`] = ` +" +{ + "object": { + "id": true + } +} +-------------- +{ + object { + id + } +} +" +`; + +exports[`$include > Query 4`] = ` +" +{ + "object": { + "$include": { + "if": true + }, + "id": true + } +} +-------------- +{ + object @include(if: true) { + id + } +} +" +`; + +exports[`$include > Query 5`] = ` +" +{ + "object": { + "$include": { + "if": false + }, + "id": true + } +} +-------------- +{ + object @include(if: false) { + id + } +} +" +`; + +exports[`$include > Query 6`] = ` +" +{ + "object": { + "$include": {}, + "id": true + } +} +-------------- +{ + object @include(if: true) { + id + } +} +" +`; + +exports[`$include > Query 7`] = ` +" +{ + "object": { + "$include": {}, + "id": true + } +} +-------------- +{ + object @include(if: true) { + id + } +} +" +`; + +exports[`$skip > Query 1`] = ` +" +{ + "object": { + "$skip": true, + "id": true + } +} +-------------- +{ + object @skip(if: true) { + id + } +} +" +`; + +exports[`$skip > Query 2`] = ` +" +{ + "object": { + "$skip": false, + "id": true + } +} +-------------- +{ + object @skip(if: false) { + id + } +} +" +`; + +exports[`$skip > Query 3`] = ` +" +{ + "object": { + "id": true + } +} +-------------- +{ + object { + id + } +} +" +`; + +exports[`$skip > Query 4`] = ` +" +{ + "object": { + "$skip": { + "if": true + }, + "id": true + } +} +-------------- +{ + object @skip(if: true) { + id + } +} +" +`; + +exports[`$skip > Query 5`] = ` +" +{ + "object": { + "$skip": { + "if": false + }, + "id": true + } +} +-------------- +{ + object @skip(if: false) { + id + } +} +" +`; + +exports[`$skip > Query 6`] = ` +" +{ + "object": { + "$skip": {}, + "id": true + } +} +-------------- +{ + object @skip(if: true) { + id + } +} +" +`; + +exports[`$skip > Query 7`] = ` +" +{ + "object": { + "$skip": {}, + "id": true + } +} +-------------- +{ + object @skip(if: true) { + id + } +} +" +`; + +exports[`$stream > Query 1`] = ` +" +{ + "object": { + "$stream": true, + "id": true + } +} +-------------- +{ + object @defer(if: true) { + id + } +} +" +`; + +exports[`$stream > Query 2`] = ` +" +{ + "object": { + "$stream": false, + "id": true + } +} +-------------- +{ + object @defer(if: false) { + id + } +} +" +`; + +exports[`$stream > Query 3`] = ` +" +{ + "object": { + "id": true + } +} +-------------- +{ + object { + id + } +} +" +`; + +exports[`$stream > Query 4`] = ` +" +{ + "object": { + "$stream": { + "if": true + }, + "id": true + } +} +-------------- +{ + object @defer(if: true) { + id + } +} +" +`; + +exports[`$stream > Query 5`] = ` +" +{ + "object": { + "$stream": { + "if": false + }, + "id": true + } +} +-------------- +{ + object @defer(if: false) { + id + } +} +" +`; + +exports[`$stream > Query 6`] = ` +" +{ + "object": { + "$stream": {}, + "id": true + } +} +-------------- +{ + object @defer(if: true) { + id + } +} +" +`; + +exports[`$stream > Query 7`] = ` +" +{ + "object": { + "$stream": {}, + "id": true + } +} +-------------- +{ + object @defer(if: true) { + id + } +} +" +`; + +exports[`$stream > Query 8`] = ` +" +{ + "object": { + "$stream": { + "label": "foobar" + }, + "id": true + } +} +-------------- +{ + object @defer(if: true, label: "foobar") { + id + } +} +" +`; + +exports[`$stream > Query 9`] = ` +" +{ + "object": { + "$stream": { + "initialCount": 5 + }, + "id": true + } +} +-------------- +{ + object @defer(if: true, initialCount: 5) { + id + } +} +" +`; + +exports[`alias > Query 1`] = ` +" +{ + "id_as_x": true +} +-------------- +{ + id: x +} +" +`; + +exports[`alias > Query 2`] = ` +" +{ + "id_as_x": true, + "id_as_id2": true +} +-------------- +{ + id: x + id: id2 +} +" +`; + +exports[`alias > Query 3`] = ` +" +{ + "id_as_x": { + "$skip": true + } +} +-------------- +{ + id: x @skip(if: true) +} +" +`; + +exports[`alias > Query 4`] = ` +" +{ + "object_as_x": { + "$skip": true, + "id": true + } +} +-------------- +{ + object: x @skip(if: true) { + id + } +} +" +`; + +exports[`args > Query 1`] = ` +" +{ + "stringWithArgs": { + "$": { + "boolean": true, + "float": 1 + } + } +} +-------------- +{ + stringWithArgs(boolean: true, float: 1) +} +" +`; + +exports[`args > Query 2`] = ` +" +{ + "stringWithArgs": { + "$": {} + } +} +-------------- +{ + stringWithArgs +} +" +`; + +exports[`args > Query 3`] = ` +" +{ + "objectWithArgs": { + "$": { + "id": "" + }, + "id": true + } +} +-------------- +{ + objectWithArgs(id: "") { + id + } +} +" +`; + +exports[`args > Query 4`] = ` +" +{ + "objectWithArgs": { + "$": {}, + "id": true + } +} +-------------- +{ + objectWithArgs { + id + } +} +" +`; + +exports[`other > Query 1`] = ` +" +{ + "__typename": true +} +-------------- +{ + __typename +} +" +`; + +exports[`other > Query 2`] = ` +" +{ + "string": true +} +-------------- +{ + string +} +" +`; + +exports[`other > Query 3`] = ` +" +{ + "string": 1 +} +-------------- +{ + string +} +" +`; + +exports[`other > Query 4`] = ` +" +{ + "id": true, + "string": false +} +-------------- +{ + id +} +" +`; + +exports[`other > Query 5`] = ` +" +{ + "id": true, + "string": 0 +} +-------------- +{ + id +} +" +`; + +exports[`other > Query 6`] = ` +" +{ + "id": true +} +-------------- +{ + id +} +" +`; + +exports[`other > Query 7`] = ` +" +{ + "object": { + "id": true + } +} +-------------- +{ + object { + id + } +} +" +`; + +exports[`other > Query 8`] = ` +" +{ + "objectNested": { + "object": { + "string": true, + "id": true, + "int": false + } + } +} +-------------- +{ + objectNested { + object { + string + id + } + } +} +" +`; + +exports[`other > Query 9`] = ` +" +{ + "objectNested": { + "object": { + "string": true, + "id": true, + "int": { + "$skip": true + } + } + } +} +-------------- +{ + objectNested { + object { + string + id + int @skip(if: true) + } + } +} +" +`; + +exports[`union > Query 1`] = ` +" +{ + "fooBarUnion": { + "__typename": true + } +} +-------------- +{ + fooBarUnion { + __typename + } +} +" +`; + +exports[`union > Query 2`] = ` +" +{ + "fooBarUnion": { + "onBar": { + "int": true + } + } +} +-------------- +{ + fooBarUnion { + ... on Bar { + int + } + } +} +" +`; + +exports[`union > Query 3`] = ` +" +{ + "fooBarUnion": { + "onBar": { + "$skip": true, + "int": true + } + } +} +-------------- +{ + fooBarUnion { + ... on Bar @skip(if: true) { + int + } + } +} +" +`; diff --git a/src/SelectionSet/toGraphQLDocumentString.test.ts b/src/SelectionSet/toGraphQLDocumentString.test.ts new file mode 100644 index 000000000..aa06996e2 --- /dev/null +++ b/src/SelectionSet/toGraphQLDocumentString.test.ts @@ -0,0 +1,132 @@ +import { parse, print } from 'graphql' +import { describe, expect, test } from 'vitest' +import type * as Schema from '../../tests/builder/_/schema.js' +import type { SelectionSet } from './__.js' +import { toGraphQLDocumentString } from './toGraphQLDocumentString.js' + +type Q = SelectionSet.Query +const s = (selectionSet: Q) => selectionSet +const prepareResult = (ss: Q) => { + const graphqlDocumentString = toGraphQLDocumentString(ss as any) + // console.log(graphqlDocumentString) + // Should parse, ensures is syntactically valid graphql document. + const document = parse(graphqlDocumentString) + const graphqlDocumentStringFormatted = print(document) + const beforeAfter = `\n` + + JSON.stringify(ss, null, 2) + + `\n--------------\n` + + graphqlDocumentStringFormatted + + `\n` + return beforeAfter +} + +describe(`union`, () => { + test.each([ + s({ fooBarUnion: { __typename: true } }), + s({ fooBarUnion: { onBar: { int: true } } }), + s({ fooBarUnion: { onBar: { $skip: true, int: true } } }), + // s({ fooBarUnion: { onBar: {} } }), // todo should be static type error + ])(`Query`, (ss) => { + expect(prepareResult(ss)).toMatchSnapshot() + }) +}) + +describe(`alias`, () => { + test.each([ + s({ id_as_x: true }), + s({ id_as_x: true, id_as_id2: true }), + s({ id_as_x: { $skip: true } }), + s({ object_as_x: { $skip: true, id: true } }), + ])(`Query`, (ss) => { + expect(prepareResult(ss)).toMatchSnapshot() + }) +}) + +describe(`args`, () => { + test.each([ + s({ stringWithArgs: { $: { boolean: true, float: 1 } } }), + s({ stringWithArgs: { $: {} } }), + // s({ objectWithArgs: { $: { id: `` } } }), // todo should be static error + // s({ objectWithArgs: { $: {} } }), // todo should be static error + s({ objectWithArgs: { $: { id: `` }, id: true } }), + s({ objectWithArgs: { $: {}, id: true } }), + ])(`Query`, (ss) => { + expect(prepareResult(ss)).toMatchSnapshot() + }) +}) + +describe(`$include`, () => { + test.each([ + s({ object: { $include: true, id: true } }), + s({ object: { $include: false, id: true } }), + s({ object: { $include: undefined, id: true } }), + s({ object: { $include: { if: true }, id: true } }), + s({ object: { $include: { if: false }, id: true } }), + s({ object: { $include: { if: undefined }, id: true } }), + s({ object: { $include: {}, id: true } }), + ])(`Query`, (ss) => { + expect(prepareResult(ss)).toMatchSnapshot() + }) +}) + +describe(`$skip`, () => { + test.each([ + s({ object: { $skip: true, id: true } }), + s({ object: { $skip: false, id: true } }), + s({ object: { $skip: undefined, id: true } }), + s({ object: { $skip: { if: true }, id: true } }), + s({ object: { $skip: { if: false }, id: true } }), + s({ object: { $skip: { if: undefined }, id: true } }), + s({ object: { $skip: {}, id: true } }), + ])(`Query`, (ss) => { + expect(prepareResult(ss)).toMatchSnapshot() + }) +}) + +describe(`$defer`, () => { + test.each([ + s({ object: { $defer: true, id: true } }), + s({ object: { $defer: false, id: true } }), + s({ object: { $defer: undefined, id: true } }), + s({ object: { $defer: { if: true }, id: true } }), + s({ object: { $defer: { if: false }, id: true } }), + s({ object: { $defer: { if: undefined }, id: true } }), + s({ object: { $defer: {}, id: true } }), + s({ object: { $defer: { label: `foobar` }, id: true } }), + ])(`Query`, (ss) => { + expect(prepareResult(ss)).toMatchSnapshot() + }) +}) + +describe(`$stream`, () => { + test.each([ + s({ object: { $stream: true, id: true } }), + s({ object: { $stream: false, id: true } }), + s({ object: { $stream: undefined, id: true } }), + s({ object: { $stream: { if: true }, id: true } }), + s({ object: { $stream: { if: false }, id: true } }), + s({ object: { $stream: { if: undefined }, id: true } }), + s({ object: { $stream: {}, id: true } }), + s({ object: { $stream: { label: `foobar` }, id: true } }), + s({ object: { $stream: { initialCount: 5 }, id: true } }), + ])(`Query`, (ss) => { + expect(prepareResult(ss)).toMatchSnapshot() + }) +}) + +describe(`other`, () => { + test.each([ + s({ __typename: true }), + s({ string: true }), + s({ string: 1 }), + // s({ string: false }), // todo should be static error + s({ id: true, string: false }), + s({ id: true, string: 0 }), + s({ id: true, string: undefined }), + s({ object: { id: true } }), + s({ objectNested: { object: { string: true, id: true, int: false } } }), + s({ objectNested: { object: { string: true, id: true, int: { $skip: true } } } }), + ])(`Query`, (ss) => { + expect(prepareResult(ss)).toMatchSnapshot() + }) +}) diff --git a/src/SelectionSet/toGraphQLDocumentString.ts b/src/SelectionSet/toGraphQLDocumentString.ts new file mode 100644 index 000000000..efc8067e9 --- /dev/null +++ b/src/SelectionSet/toGraphQLDocumentString.ts @@ -0,0 +1,143 @@ +import type { SelectionSet } from './__.js' +import { aliasPattern, fragmentPattern } from './SelectionSet.js' + +type SpecialFields = { + // todo - this requires having the schema at runtime to know which fields to select. + // $scalars?: SelectionSet.Indicator + $include?: SelectionSet.Directive.Include['$include'] + $skip?: SelectionSet.Directive.Skip['$skip'] + $defer?: SelectionSet.Directive.Defer['$defer'] + $stream?: SelectionSet.Directive.Stream['$stream'] + $?: Args +} + +type Args = { [k: string]: Args_ } + +type Args_ = string | boolean | null | number | Args + +type Indicator = 0 | 1 | boolean + +type SSRoot = { + [k: string]: Indicator | SS +} + +type SS = { + [k: string]: Indicator | SS +} & SpecialFields + +export const toGraphQLDocumentString = (ss: SSRoot) => { + let docString = `` + docString += `query { + ${selectionSet(ss)} + }` + return docString +} + +const directiveArgs = (config: object) => { + return Object.entries(config).filter(([_, v]) => v !== undefined).map(([k, v]) => { + return `${k}: ${JSON.stringify(v)}` + }).join(`, `) +} + +const indicatorOrSelectionSet = (ss: Indicator | SS): string => { + if (isIndicator(ss)) return `` + + const { $include, $skip, $defer, $stream, $, ...rest } = ss + + let args = `` + let directives = `` + + if ($stream !== undefined) { + const config = { + if: typeof $stream === `boolean` ? $stream : $stream.if === undefined ? true : $stream.if, + label: typeof $stream === `boolean` ? undefined : $stream.label, + initialCount: typeof $stream === `boolean` ? undefined : $stream.initialCount, + } + directives += `@defer(${directiveArgs(config)})` + } + + if ($defer !== undefined) { + const config = { + if: typeof $defer === `boolean` ? $defer : $defer.if === undefined ? true : $defer.if, + label: typeof $defer === `boolean` ? undefined : $defer.label, + } + directives += `@defer(${directiveArgs(config)})` + } + + if ($include !== undefined) { + directives += `@include(if: ${ + typeof $include === `boolean` ? $include : $include.if === undefined ? true : $include.if + })` + } + + if ($skip !== undefined) { + directives += `@skip(if: ${typeof $skip === `boolean` ? $skip : $skip.if === undefined ? true : $skip.if})` + } + + if ($ !== undefined) { + const entries = Object.entries($) + args = entries.length === 0 ? `` : `(${ + entries.map(([k, v]) => { + return `${k}: ${JSON.stringify(v)}` + }).join(`, `) + })` + } + + if (Object.keys(rest).length === 0) { + return `${args} ${directives}` + } + + return `${args} ${directives} { + ${selectionSet(rest)} + }` +} + +const selectionSet = (ss: SSRoot) => { + return Object.entries(ss).filter(([_, v]) => { + return isPositiveIndicator(v) + }).map(([field, ss]) => { + return `${resolveFragment(resolveAlias(field))} ${indicatorOrSelectionSet(ss)}` + }).join(`\n`) + `\n` +} + +// todo use a given schema to ensure that field is actually a fragment and not just happened to be using pattern onX +const resolveFragment = (field: string) => { + const match = field.match(fragmentPattern) + if (match?.groups) { + return `...on ${match.groups[`name`]}` + } + return field +} + +// todo use a given schema to ensure that field is actually a fragment and not just happened to be using pattern onX +const resolveAlias = (field: string) => { + const match = field.match(aliasPattern) + if (match?.groups) { + return `${match.groups[`actual`]}: ${match.groups[`alias`]}` + } + return field +} + +const isIndicator = (v: any): v is Indicator => { + return String(v) in indicator +} + +const isPositiveIndicator = (v: any): v is SelectionSet.ClientIndicatorPositive => { + return !(String(v) in negativeIndicator) +} + +const negativeIndicator = { + '0': 0, + 'false': false, + 'undefined': undefined, +} + +const positiveIndicator = { + '1': 1, + 'true': true, +} + +const indicator = { + ...negativeIndicator, + ...positiveIndicator, +} diff --git a/src/cli/generate.ts b/src/cli/generate.ts new file mode 100644 index 000000000..a627597c9 --- /dev/null +++ b/src/cli/generate.ts @@ -0,0 +1,6 @@ +import { generateFile } from '../generator/generator.js' + +await generateFile({ + schemaPath: `./examples/schema.graphql`, + typeScriptPath: `./src/demo.ts`, +}) diff --git a/src/client.test.ts b/src/client.test.ts new file mode 100644 index 000000000..234eb883b --- /dev/null +++ b/src/client.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from 'vitest' +import type { $ } from '../tests/builder/_/schema.js' +import { setupMockServer } from '../tests/legacy/__helpers.js' +import { create } from './client.js' + +const ctx = setupMockServer() +const data = { fooBarUnion: { int: 1 } } + +test(`query`, async () => { + const mockRes = ctx.res({ body: { data } }).spec.body! + // eslint-disable-next-line + // @ts-ignore infinite depth + const client = create<$.Index>({ url: ctx.url }) + expect(await client.query({ fooBarUnion: { onBar: { int: true } } })).toEqual(mockRes.data) +}) diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 000000000..574a9ae95 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,57 @@ +import request from './entrypoints/main.js' +import type { ResultSet } from './ResultSet/__.js' +import type { Schema } from './Schema/__.js' +import { SelectionSet } from './SelectionSet/__.js' + +// dprint-ignore +export type Client<$SchemaIndex extends Schema.Index> = + & ( + $SchemaIndex['Root']['Query'] extends null + ? unknown + : { + query: <$SelectionSet extends SelectionSet.Query<$SchemaIndex>>(selectionSet: $SelectionSet) => Promise> + } + ) + & ( + $SchemaIndex['Root']['Mutation'] extends null + ? unknown + : { + mutation: <$SelectionSet extends SelectionSet.Mutation<$SchemaIndex>>(selectionSet: $SelectionSet) => Promise> + } + ) +// todo +// & ($SchemaIndex['Root']['Subscription'] extends null ? { +// subscription: <$SelectionSet extends SelectionSet.Subscription<$SchemaIndex>>(selectionSet: $SelectionSet) => Promise> +// } +// : unknown) + +interface Input { + url: URL | string + headers?: HeadersInit +} + +export const create = <$SchemaIndex extends Schema.Index>(input: Input): Client<$SchemaIndex> => { + // @ts-expect-error ignoreme + const client: Client<$SchemaIndex> = { + query: async (documentQueryObject: any) => { + const documentQueryString = SelectionSet.toGraphQLDocumentString(documentQueryObject) + return await request({ + url: new URL(input.url).href, + requestHeaders: input.headers, + document: documentQueryString, + }) + }, + mutation: async (documentMutationObject: any) => { + const documentMutationString = SelectionSet.toGraphQLDocumentString(documentMutationObject) + return await request({ + url: new URL(input.url).href, + requestHeaders: input.headers, + document: documentMutationString, + }) + }, + // todo + // subscription: async () => { + // }, + } + return client +} diff --git a/src/entrypoints/alpha.ts b/src/entrypoints/alpha.ts new file mode 100644 index 000000000..f07fac449 --- /dev/null +++ b/src/entrypoints/alpha.ts @@ -0,0 +1 @@ +export * from '../client.js' diff --git a/src/generator/generator.ts b/src/generator/generator.ts new file mode 100644 index 000000000..5fec88177 --- /dev/null +++ b/src/generator/generator.ts @@ -0,0 +1,451 @@ +import type { + GraphQLArgument, + GraphQLEnumValue, + GraphQLInputObjectType, + GraphQLInterfaceType, + GraphQLNamedType, + GraphQLObjectType, + GraphQLSchema, +} from 'graphql' +import { GraphQLNonNull, isEnumType, isListType, isNamedType } from 'graphql' +import { buildSchema } from 'graphql' +import _ from 'json-bigint' +import fs from 'node:fs/promises' +import { Code } from '../lib/Code.js' +import type { + AnyClass, + AnyField, + AnyNamedClassName, + ClassToName, + Describable, + NameToClassNamedType, + TypeMapByKind, +} from '../lib/graphql.js' +import { + getNodeDisplayName, + getTypeMapByKind, + isDeprecatableNode, + isGraphQLOutputField, + type NameToClass, +} from '../lib/graphql.js' +import { entries, values } from '../lib/prelude.js' + +const namespaceNames = { + GraphQLEnumType: `Enum`, + GraphQLInputObjectType: `InputObject`, + GraphQLInterfaceType: `Interface`, + GraphQLObjectType: `Object`, + GraphQLScalarType: `Scalar`, + GraphQLUnionType: `Union`, +} satisfies Record + +type AnyGraphQLFieldsType = + | GraphQLObjectType + | GraphQLInterfaceType + | GraphQLInputObjectType + +const defineReferenceRenderers = < + $Renderers extends { [ClassName in keyof NameToClass]: any }, +>( + renderers: { + [ClassName in keyof $Renderers]: ( + config: Config, + node: ClassName extends keyof NameToClass ? InstanceType + : never, + ) => string + }, +) => renderers + +const defineConcreteRenderers = < + $Renderers extends { [ClassName in keyof NameToClassNamedType]: any }, +>( + renderers: { + [ClassName in keyof $Renderers]: ( + config: Config, + node: ClassName extends keyof NameToClassNamedType ? InstanceType + : never, + ) => string + }, +): { + [ClassName in keyof $Renderers]: ( + node: ClassName extends keyof NameToClass ? InstanceType | null | undefined + : never, + ) => string +} => { + return Object.fromEntries( + Object.entries(renderers).map(([key, renderer]) => { + return [ + key, + (config: Config, node: any) => { + if (!node) return `` + return renderer(config, node) // eslint-disable-line + }, + ] + }), + ) as any +} + +const dispatchToReferenceRenderer = (config: Config, node: AnyClass): string => + getReferenceRenderer(node)(config, node as any) + +const getReferenceRenderer = (node: N): (typeof referenceRenderers)[ClassToName] => { + // @ts-expect-error lookup + const renderer = referenceRenderers[node.constructor.name] // eslint-disable-line + if (!renderer) { + throw new Error(`No renderer found for class: ${node.constructor.name}`) + } + return renderer +} + +const referenceRenderers = defineReferenceRenderers({ + GraphQLNonNull: (config, node) => dispatchToReferenceRenderer(config, node.ofType), + GraphQLEnumType: (_, node) => Code.propertyAccess(namespaceNames.GraphQLEnumType, node.name), + GraphQLInputObjectType: (_, node) => Code.propertyAccess(namespaceNames.GraphQLInputObjectType, node.name), + GraphQLInterfaceType: (_, node) => Code.propertyAccess(namespaceNames.GraphQLInterfaceType, node.name), + GraphQLObjectType: (_, node) => Code.propertyAccess(namespaceNames.GraphQLObjectType, node.name), + GraphQLUnionType: (_, node) => Code.propertyAccess(namespaceNames.GraphQLUnionType, node.name), + GraphQLList: (config, node) => `_.List<${(buildType(config, node.ofType))}>`, + GraphQLScalarType: (_, node) => `_.Scalar.${node.name}`, +}) + +const dispatchToConcreteRenderer = ( + config: Config, + node: GraphQLNamedType, +): string => { + // @ts-expect-error lookup + const renderer = concreteRenderers[node.constructor.name] // eslint-disable-line + if (!renderer) { + throw new Error(`No renderer found for class: ${node.constructor.name}`) + } + return renderer(config, node) // eslint-disable-line +} + +const concreteRenderers = defineConcreteRenderers({ + GraphQLEnumType: (config, node) => + Code.TSDoc( + getDocumentation(config, node), + Code.export$( + Code.type( + node.name, + `_.Enum<${Code.quote(node.name)}, ${Code.tuple(node.getValues().map((_) => Code.quote(_.name)))} >`, + ), + ), + ), + GraphQLInputObjectType: (config, node) => + Code.TSDoc( + getDocumentation(config, node), + Code.export$(Code.interface$(node.name, renderFields(config, node))), + ), + GraphQLInterfaceType: (config, node) => { + const implementors = config.typeMapByKind.GraphQLObjectType.filter(_ => + _.getInterfaces().filter(_ => _.name === node.name).length > 0 + ) + return Code.TSDoc( + getDocumentation(config, node), + Code.export$(Code.type( + node.name, + `_.Interface<${Code.quote(node.name)}, ${renderFields(config, node)}, ${ + Code.tuple(implementors.map(_ => `Object.${_.name}`)) + }>`, + )), + ) + }, + GraphQLObjectType: (config, node) => + Code.TSDoc( + getDocumentation(config, node), + Code.export$(Code.type(node.name, `_.Object<${Code.quote(node.name)}, ${renderFields(config, node)}>`)), + ), + GraphQLScalarType: () => ``, + GraphQLUnionType: (config, node) => + Code.TSDoc( + getDocumentation(config, node), + Code.export$( + Code.type( + node.name, + `_.Union<${Code.quote(node.name)},${ + Code.tuple( + node + .getTypes() + .map( + (_) => dispatchToReferenceRenderer(config, _), + ), + ) + }>`, + ), + ), + ), +}) + +const getDocumentation = (config: Config, node: Describable) => { + const generalDescription = node.description + ?? (config.TSDoc.noDocPolicy === `message` ? defaultDescription(node) : null) + + const deprecationDescription = isDeprecatableNode(node) && node.deprecationReason + ? `@deprecated ${node.deprecationReason}` + : null + + const enumMemberDescriptions: string[] = isEnumType(node) + ? node + .getValues() + .map((_) => { + const deprecationDescription = _.deprecationReason + ? `(DEPRECATED: ${_.deprecationReason})` + : null + const generalDescription = _.description + ? _.description + : config.TSDoc.noDocPolicy === `message` + ? `Missing description.` + : null + if (!generalDescription && !deprecationDescription) return null + const content = [generalDescription, deprecationDescription] + .filter((_) => _ !== null) + .join(` `) + return [_, content] as const + }) + .filter((_): _ is [GraphQLEnumValue, string] => _ !== null) + .map(([node, description]) => { + const content = `"${node.name}" - ${description}` + return content + }) + : [] + const enumMemberDescription = enumMemberDescriptions.length > 0 + ? `Members\n${enumMemberDescriptions.join(`\n`)}` + : null + if (!enumMemberDescription && !generalDescription && !deprecationDescription) { + return null + } + const content = [ + generalDescription, + enumMemberDescription, + deprecationDescription, + ] + .filter((_) => _ !== null) + .join(`\n\n`) + return content +} + +const defaultDescription = (node: Describable) => `There is no documentation for this ${getNodeDisplayName(node)}.` + +const renderFields = (config: Config, node: AnyGraphQLFieldsType): string => { + return Code.object(Code.fields([ + ...values(node.getFields()).map((field) => + Code.TSDoc( + getDocumentation(config, field), + Code.field(field.name, renderField(config, field)), + ) + ), + ])) +} + +const buildType = (config: Config, node: AnyClass) => { + const { node: nodeInner, nullable } = unwrapNonNull(node) + + if (isNamedType(nodeInner)) { + const namedTypeReference = dispatchToReferenceRenderer(config, nodeInner) + // const namedTypeCode = `_.Named<${namedTypeReference}>` + const namedTypeCode = namedTypeReference + return nullable + ? `_.Nullable<${namedTypeCode}>` + : namedTypeCode + } + + if (isListType(nodeInner)) { + const fieldType = `_.List<${buildType(config, nodeInner.ofType)}>` as any as string + return nullable + ? `_.Nullable<${fieldType}>` + : fieldType + } + + throw new Error(`Unhandled type: ${String(node)}`) +} + +// const getNamedType = (config: Config, node: AnyClass): GraphQLNamedType => { +// if (isNamedType(node)) return node +// if (isNonNullType(node)) return getNamedType(config, node.ofType) +// if (isListType(node)) return getNamedType(config, node.ofType) +// throw new Error(`Unhandled type: ${String(node)}`) +// } + +const renderField = (config: Config, field: AnyField): string => { + const type = buildType(config, field.type) + + const args = isGraphQLOutputField(field) && field.args.length > 0 + ? renderArgs(config, field.args) + : null + + return `_.Field<${type}${args ? `, ${args}` : ``}>` +} + +const renderArgs = (config: Config, args: readonly GraphQLArgument[]) => { + let hasRequiredArgs = false + const argsRendered = `_.Args<${ + Code.object( + Code.fields( + args.map((arg) => { + const { node, nullable } = unwrapNonNull(arg.type) + hasRequiredArgs = hasRequiredArgs || !nullable + return Code.field( + arg.name, + nullable + ? `_.Nullable<${dispatchToReferenceRenderer(config, node)}>` + : dispatchToReferenceRenderer(config, node), + ) + }), + ), + ) + }>` + return argsRendered + return Code.objectFrom({ + type: { type: argsRendered }, + // allOptional: { type: !hasRequiredArgs }, + }) +} + +const unwrapNonNull = ( + node: AnyClass, +): { node: AnyClass; nullable: boolean } => { + const [nodeUnwrapped, nullable] = node instanceof GraphQLNonNull ? [node.ofType, false] : [node, true] + return { node: nodeUnwrapped, nullable } +} + +const scalarTypeMap: Record = { + ID: `string`, + Int: `number`, + String: `string`, + Float: `number`, + Boolean: `boolean`, +} + +// high level + +interface Input { + schemaSource: string + options?: { + TSDoc?: { + noDocPolicy?: 'message' | 'ignore' + } + } +} + +interface Config { + schema: GraphQLSchema + typeMapByKind: TypeMapByKind + TSDoc: { + noDocPolicy: 'message' | 'ignore' + } +} + +const resolveOptions = (input: Input): Config => { + const schema = buildSchema(input.schemaSource) + return { + schema, + typeMapByKind: getTypeMapByKind(schema), + TSDoc: { + noDocPolicy: input.options?.TSDoc?.noDocPolicy ?? `ignore`, + }, + } +} + +export const generateCode = (input: Input) => { + const config = resolveOptions(input) + const { typeMapByKind } = config + + const hasQuery = typeMapByKind.GraphQLRootTypes.find( + (_) => _.name === `Query`, + ) + const hasMutation = typeMapByKind.GraphQLRootTypes.find( + (_) => _.name === `Mutation`, + ) + const hasSubscription = typeMapByKind.GraphQLRootTypes.find( + (_) => _.name === `Subscription`, + ) + + let code = `` + + code += `import type * as _ from '../../../src/Schema/__.js'\n\n` + + code += Code.export$( + Code.namespace( + `$`, + Code.group( + Code.export$( + Code.interface$( + `Index`, + Code.objectFrom({ + Root: { + type: Code.objectFrom({ + Query: hasQuery ? `Root.Query` : null, + Mutation: hasMutation ? `Root.Mutation` : null, + Subscription: hasSubscription ? `Root.Subscription` : null, + }), + }, + objects: Code.objectFromEntries( + typeMapByKind.GraphQLObjectType.map(_ => [_.name, Code.propertyAccess(`Object`, _.name)]), + ), + // unionMemberNames: Code.objectFromEntries( + // typeMapByKind.GraphQLUnionType.map( + // (_) => [_.name, Code.unionItems(_.getTypes().map(_ => Code.quote(_.name)))], + // ), + // ), + unions: { + type: Code.objectFrom( + { + Union: { + type: typeMapByKind.GraphQLUnionType.length > 0 + ? Code.unionItems( + typeMapByKind.GraphQLUnionType.map( + (_) => Code.propertyAccess(`Union`, _.name), + ), + ) + : null, + }, + }, + ), + }, + scalars: `Scalars`, + }), + ), + ), + Code.export$( + Code.interface$( + `Scalars`, + Code.objectFromEntries(typeMapByKind.GraphQLScalarType.map((_) => { + // todo strict mode where instead of falling back to "any" we throw an error + const type = scalarTypeMap[_.name] || `string` + return [_.name, type] + })), + ), + ), + ), + ), + ) + + for (const [name, types] of entries(typeMapByKind)) { + if (name === `GraphQLScalarType`) continue + + const namespaceName = name === `GraphQLRootTypes` ? `Root` : namespaceNames[name] + code += Code.commentSectionTitle(namespaceName) + code += Code.export$( + Code.namespace( + namespaceName, + types.length === 0 + ? `// -- no types --\n` + : types + .map((_) => dispatchToConcreteRenderer(config, _)) + .join(`\n\n`), + ), + ) + } + + return code +} + +export const generateFile = async (params: { + schemaPath: string + typeScriptPath: string +}) => { + // todo use @dprint/formatter + const schemaSource = await fs.readFile(params.schemaPath, `utf8`) + const code = generateCode({ schemaSource }) + await fs.writeFile(params.typeScriptPath, code, { encoding: `utf8` }) +} diff --git a/src/lib/Code.ts b/src/lib/Code.ts new file mode 100644 index 000000000..4bcc8af85 --- /dev/null +++ b/src/lib/Code.ts @@ -0,0 +1,51 @@ +export namespace Code { + export const propertyAccess = (object: string, name: string) => `${object}.${name}` + export const quote = (str: string) => `"${str}"` + export const nullable = (type: string) => `${type} | null` + export const union = (name: string, types: string[]) => `type ${name} =\n| ${Code.unionItems(types)}` + export const unionItems = (types: string[]) => types.join(`\n| `) + export const tuple = (types: string[]) => `[${types.join(`, `)}]` + export const list = (type: string) => `Array<${type}>` + export const field = (name: string, type: string, options?: { optional?: boolean }) => { + if (options?.optional) return `${name}?: ${type}` + return `${name}: ${type}` + } + export const optionalField = (name: string, type: string) => Code.field(name, type, { optional: true }) + export const fields = (fieldTypes: string[]) => fieldTypes.join(`\n`) + export const intersection = (a: string, b: string) => `${a} & ${b}` + export const object = (fields: string) => `{\n${fields}\n}` + export const objectFromEntries = (entries: [string, string][]) => + Code.objectFrom(Object.fromEntries(entries.map(([name, type]) => [name, { type }]))) + export const objectFrom = ( + object: Record< + string, + null | string | boolean | number | { type: null | string | boolean | number; optional?: boolean; tsdoc?: string } + >, + ) => + Code.object( + Code.fields( + Object.entries(object).map(([name, spec]) => + [name, spec && typeof spec === `object` ? spec : { type: spec }] as const + ) + .map(( + [name, spec], + ) => Code.field(name, String(spec.type), { optional: spec.optional })), + ), + ) + export const type = (name: string, type: string) => `type ${name} = ${type}` + export const interface$ = (name: string, object: string) => `interface ${name} ${object}` + export const export$ = (thing: string) => `export ${thing}` + export const TSDoc = (content: string | null, block: string) => + content === null ? block : `/**\n${prependLines(`* `, content) || `*`}\n*/\n${block}` + export const namespace = (name: string, content: string) => `namespace ${name} ${Code.object(content)}` + export const group = (...content: string[]) => content.join(`\n`) + export const commentSectionTitle = (title: string) => { + const lineSize = 60 + const line = `-`.repeat(lineSize) + const titlePrefixSpace = ` `.repeat(Math.round(lineSize / 2) - Math.round(title.length / 2)) + const titleSuffixSpace = ` `.repeat(lineSize - (titlePrefixSpace.length + title.length)) + return `\n\n// ${line} //\n// ${titlePrefixSpace + title + titleSuffixSpace} //\n// ${line} //\n\n` + } +} + +const prependLines = (prepend: string, str: string) => str.split(`\n`).map((line) => `${prepend}${line}`).join(`\n`) diff --git a/src/lib/TSError.ts b/src/lib/TSError.ts new file mode 100644 index 000000000..fd131382b --- /dev/null +++ b/src/lib/TSError.ts @@ -0,0 +1,11 @@ +export const TypeErrorSymbol = Symbol(`TypeError`) + +export type TSError< + Location extends string, + Message extends string, + Context extends Record = never, +> = { + [TypeErrorSymbol]: true + message: `Error (${Location}): ${Message}` + context: Context +} diff --git a/src/lib/graphql.ts b/src/lib/graphql.ts new file mode 100644 index 000000000..d987b4f06 --- /dev/null +++ b/src/lib/graphql.ts @@ -0,0 +1,173 @@ +import type { GraphQLEnumValue, GraphQLField, GraphQLInputField, GraphQLSchema } from 'graphql' +import { + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLInterfaceType, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLScalarType, + GraphQLUnionType, +} from 'graphql' + +export type TypeMapByKind = + & { + [Name in keyof NameToClassNamedType]: InstanceType[] + } + & { GraphQLRootTypes: GraphQLObjectType[] } + +export const getTypeMapByKind = (schema: GraphQLSchema) => { + const typeMap = schema.getTypeMap() + const typeMapValues = Object.values(typeMap) + const typeMapByKind: TypeMapByKind = { + GraphQLRootTypes: [], + GraphQLScalarType: [], + GraphQLEnumType: [], + GraphQLInputObjectType: [], + GraphQLInterfaceType: [], + GraphQLObjectType: [], + GraphQLUnionType: [], + } + for (const type of typeMapValues) { + if (type.name.startsWith(`__`)) continue + switch (true) { + case type instanceof GraphQLScalarType: + typeMapByKind.GraphQLScalarType.push(type) + break + case type instanceof GraphQLEnumType: + typeMapByKind.GraphQLEnumType.push(type) + break + case type instanceof GraphQLInputObjectType: + typeMapByKind.GraphQLInputObjectType.push(type) + break + case type instanceof GraphQLInterfaceType: + typeMapByKind.GraphQLInterfaceType.push(type) + break + case type instanceof GraphQLObjectType: + if (type.name === `Query` || type.name === `Mutation` || type.name === `Subscription`) { + typeMapByKind.GraphQLRootTypes.push(type) + } else { + typeMapByKind.GraphQLObjectType.push(type) + } + break + case type instanceof GraphQLUnionType: + typeMapByKind.GraphQLUnionType.push(type) + break + default: + // skip + break + } + } + return typeMapByKind +} + +export type ClassToName = C extends GraphQLScalarType ? `GraphQLScalarType` + : C extends GraphQLObjectType ? `GraphQLObjectType` + : C extends GraphQLInterfaceType ? `GraphQLInterfaceType` + : C extends GraphQLUnionType ? `GraphQLUnionType` + : C extends GraphQLEnumType ? `GraphQLEnumType` + : C extends GraphQLInputObjectType ? `GraphQLInputObjectType` + : C extends GraphQLList ? `GraphQLList` + : C extends GraphQLNonNull ? `GraphQLNonNull` + : never + +export const NameToClassNamedType = { + GraphQLScalarType: GraphQLScalarType, + GraphQLObjectType: GraphQLObjectType, + GraphQLInterfaceType: GraphQLInterfaceType, + GraphQLUnionType: GraphQLUnionType, + GraphQLEnumType: GraphQLEnumType, + GraphQLInputObjectType: GraphQLInputObjectType, +} + +export type NameToClassNamedType = typeof NameToClassNamedType + +export const NamedNameToClass = { + GraphQLScalarType: GraphQLScalarType, + GraphQLObjectType: GraphQLObjectType, + GraphQLInterfaceType: GraphQLInterfaceType, + GraphQLUnionType: GraphQLUnionType, + GraphQLEnumType: GraphQLEnumType, + GraphQLInputObjectType: GraphQLInputObjectType, +} as const + +export type NamedNameToClass = typeof NamedNameToClass + +export const NameToClass = { + GraphQLNonNull: GraphQLNonNull, + GraphQLList: GraphQLList, + ...NamedNameToClass, +} as const + +export type AnyGraphQLOutputField = GraphQLField + +export type AnyField = AnyGraphQLOutputField | GraphQLInputField + +export type NameToClass = typeof NameToClass + +export type NodeName = keyof NameToClass + +export type NodeNamePlus = NodeName | 'GraphQLField' + +export type AnyNamedClassName = keyof NamedNameToClass + +export type AnyClass = InstanceType + +export const isGraphQLOutputField = (object: object): object is AnyGraphQLOutputField => { + return `args` in object +} + +/** + * Groups + */ + +export type Describable = + | GraphQLUnionType + | GraphQLObjectType + | GraphQLInputObjectType + | AnyField + | GraphQLInterfaceType + | GraphQLEnumType + +export const getNodeName = (node: Describable): NodeNamePlus => { + switch (true) { + case node instanceof GraphQLObjectType: + return `GraphQLObjectType` + case node instanceof GraphQLInputObjectType: + return `GraphQLInputObjectType` + case node instanceof GraphQLUnionType: + return `GraphQLUnionType` + case node instanceof GraphQLInterfaceType: + return `GraphQLInterfaceType` + case node instanceof GraphQLEnumType: + return `GraphQLEnumType` + case node instanceof GraphQLScalarType: + return `GraphQLScalarType` + default: + return `GraphQLField` + throw new Error(`Unknown node type: ${node.name}`) + } +} + +// const displayNames = { +// GraphQLEnumType: `Enum`, +// GraphQLInputObjectType: `InputObject`, +// GraphQLInterfaceType: `Interface`, +// GraphQLList: `List`, +// GraphQLNonNull: `NonNull`, +// GraphQLObjectType: `Object`, +// GraphQLScalarType: `Scalar`, +// GraphQLUnionType: `Union`, +// } satisfies Record + +export const getNodeDisplayName = (node: Describable) => { + return toDisplayName(getNodeName(node)) +} + +const toDisplayName = (nodeName: NodeNamePlus) => { + return nodeName.replace(/^GraphQL/, ``).replace(/Type$/, ``) +} + +export const isDeprecatableNode = (node: object): node is GraphQLEnumValue | AnyField => { + return `deprecationReason` in node +} diff --git a/src/lib/prelude.ts b/src/lib/prelude.ts index 18caa5b50..e62c811bd 100644 --- a/src/lib/prelude.ts +++ b/src/lib/prelude.ts @@ -83,3 +83,86 @@ export const casesExhausted = (value: never): never => { export const isPlainObject = (value: unknown): value is object => { return typeof value === `object` && value !== null && !Array.isArray(value) } + +export const entries = >(obj: T) => Object.entries(obj) as [keyof T, T[keyof T]][] + +export const values = >(obj: T): T[keyof T][] => Object.values(obj) as T[keyof T][] + +export type Exact = + | (A extends unknown ? (W extends A ? { [K in keyof A]: Exact } : W) : never) + | (A extends Narrowable ? A : never) + +export type Narrowable = string | number | bigint | boolean | [] + +export type Letter = LetterLower | LetterUpper + +export type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' + +export type LetterLower = + | 'a' + | 'b' + | 'c' + | 'd' + | 'e' + | 'f' + | 'g' + | 'h' + | 'i' + | 'j' + | 'k' + | 'l' + | 'm' + | 'n' + | 'o' + | 'p' + | 'q' + | 'r' + | 's' + | 't' + | 'u' + | 'v' + | 'w' + | 'x' + | 'y' + | 'z' +export type LetterUpper = + | 'A' + | 'B' + | 'C' + | 'D' + | 'E' + | 'F' + | 'G' + | 'H' + | 'I' + | 'J' + | 'K' + | 'L' + | 'M' + | 'N' + | 'O' + | 'P' + | 'Q' + | 'R' + | 'S' + | 'T' + | 'U' + | 'V' + | 'W' + | 'X' + | 'Y' + | 'Z' + +export type StringNonEmpty = `${Letter}${string}` + +export type MaybeList = T | T[] + +export type NotEmptyObject = keyof T extends never ? never : T + +export type Values = T[keyof T] + +export type GetKeyOr = Key extends keyof T ? T[Key] : Or + +import type { ConditionalSimplifyDeep } from 'type-fest/source/conditional-simplify.js' + +export type SimplifyDeep = ConditionalSimplifyDeep | Date, object> // eslint-disable-line diff --git a/tests/builder/_/schema.graphql b/tests/builder/_/schema.graphql new file mode 100644 index 000000000..f4edb7118 --- /dev/null +++ b/tests/builder/_/schema.graphql @@ -0,0 +1,94 @@ +type Query { + interface: Interface + id: ID + idNonNull: ID! + string: String + stringWithRequiredArg(string:String!): String + stringWithArgs(string:String, int:Int, float:Float, boolean:Boolean, id:ID): String + stringWithArgEnum(ABCEnum:ABCEnum): String + stringWithListArg(ints:[Int]): String + stringWithListArgRequired(ints:[Int]!): String + object: Object + listListIntNonNull: [[Int!]!]! + listListInt: [[Int]] + listInt: [Int] + listIntNonNull: [Int!]! + objectNested: ObjectNested + objectNonNull: Object! + objectWithArgs(string:String, int:Int, float:Float, boolean:Boolean, id:ID): Object + fooBarUnion: FooBarUnion + """ + Query enum field documentation. + """ + abcEnum: ABCEnum + lowerCaseUnion: lowerCaseUnion +} + +""" +Union documentation. +""" +union FooBarUnion = Foo | Bar + +""" +Object documentation. +""" +type Foo { + """ + Field documentation. + """ + id: ID @deprecated(reason: "Field a is deprecated.") +} + +type Bar { + int: Int +} + +type ObjectNested { + id: ID + object: Object +} + +type lowerCaseObject { + id: ID +} +type lowerCaseObject2 { + int: Int +} +union lowerCaseUnion = lowerCaseObject | lowerCaseObject2 + +type Object { + string: String + int: Int + float: Float + boolean: Boolean + id: ID +} + +interface Interface { + id: ID +} + +type Object1ImplementingInterface implements Interface { + id: ID + int: Int +} + +type Object2ImplementingInterface implements Interface { + id: ID + boolean: Boolean +} + +""" +Enum documentation. +""" +enum ABCEnum { + A @deprecated(reason: "Enum value A is deprecated.") + """ + Enum B member documentation. + """ + B + """ + Enum C member documentation. + """ + C @deprecated(reason: "Enum value C is deprecated.") +} diff --git a/tests/builder/_/schema.ts b/tests/builder/_/schema.ts new file mode 100644 index 000000000..3c795cd83 --- /dev/null +++ b/tests/builder/_/schema.ts @@ -0,0 +1,204 @@ +import type * as _ from '../../../src/Schema/__.js' + +export namespace $ { + export interface Index { + Root: { + Query: Root.Query + Mutation: null + Subscription: null + } + objects: { + Foo: Object.Foo + Bar: Object.Bar + ObjectNested: Object.ObjectNested + lowerCaseObject: Object.lowerCaseObject + lowerCaseObject2: Object.lowerCaseObject2 + Object: Object.Object + Object1ImplementingInterface: Object.Object1ImplementingInterface + Object2ImplementingInterface: Object.Object2ImplementingInterface + } + unions: { + Union: + | Union.FooBarUnion + | Union.lowerCaseUnion + } + scalars: Scalars + } + export interface Scalars { + ID: string + String: string + Int: number + Float: number + Boolean: boolean + } +} + +// ------------------------------------------------------------ // +// Root // +// ------------------------------------------------------------ // + +export namespace Root { + export type Query = _.Object<'Query', { + interface: _.Field<_.Nullable> + id: _.Field<_.Nullable<_.Scalar.ID>> + idNonNull: _.Field<_.Scalar.ID> + string: _.Field<_.Nullable<_.Scalar.String>> + stringWithRequiredArg: _.Field< + _.Nullable<_.Scalar.String>, + _.Args<{ + string: _.Scalar.String + }> + > + stringWithArgs: _.Field< + _.Nullable<_.Scalar.String>, + _.Args<{ + string: _.Nullable<_.Scalar.String> + int: _.Nullable<_.Scalar.Int> + float: _.Nullable<_.Scalar.Float> + boolean: _.Nullable<_.Scalar.Boolean> + id: _.Nullable<_.Scalar.ID> + }> + > + stringWithArgEnum: _.Field< + _.Nullable<_.Scalar.String>, + _.Args<{ + ABCEnum: _.Nullable + }> + > + stringWithListArg: _.Field< + _.Nullable<_.Scalar.String>, + _.Args<{ + ints: _.Nullable<_.List<_.Nullable<_.Scalar.Int>>> + }> + > + stringWithListArgRequired: _.Field< + _.Nullable<_.Scalar.String>, + _.Args<{ + ints: _.List<_.Nullable<_.Scalar.Int>> + }> + > + object: _.Field<_.Nullable> + listListIntNonNull: _.Field<_.List<_.List<_.Scalar.Int>>> + listListInt: _.Field<_.Nullable<_.List<_.Nullable<_.List<_.Nullable<_.Scalar.Int>>>>>> + listInt: _.Field<_.Nullable<_.List<_.Nullable<_.Scalar.Int>>>> + listIntNonNull: _.Field<_.List<_.Scalar.Int>> + objectNested: _.Field<_.Nullable> + objectNonNull: _.Field + objectWithArgs: _.Field< + _.Nullable, + _.Args<{ + string: _.Nullable<_.Scalar.String> + int: _.Nullable<_.Scalar.Int> + float: _.Nullable<_.Scalar.Float> + boolean: _.Nullable<_.Scalar.Boolean> + id: _.Nullable<_.Scalar.ID> + }> + > + fooBarUnion: _.Field<_.Nullable> + /** + * Query enum field documentation. + */ + abcEnum: _.Field<_.Nullable> + lowerCaseUnion: _.Field<_.Nullable> + }> +} + +// ------------------------------------------------------------ // +// Enum // +// ------------------------------------------------------------ // + +export namespace Enum { + /** + * Enum documentation. + * + * Members + * "A" - (DEPRECATED: Enum value A is deprecated.) + * "B" - Enum B member documentation. + * "C" - Enum C member documentation. (DEPRECATED: Enum value C is deprecated.) + */ + export type ABCEnum = _.Enum<'ABCEnum', ['A', 'B', 'C']> +} + +// ------------------------------------------------------------ // +// InputObject // +// ------------------------------------------------------------ // + +export namespace InputObject { + // -- no types -- +} + +// ------------------------------------------------------------ // +// Interface // +// ------------------------------------------------------------ // + +export namespace Interface { + export type Interface = _.Interface<'Interface', { + id: _.Field<_.Nullable<_.Scalar.ID>> + }, [Object.Object1ImplementingInterface, Object.Object2ImplementingInterface]> +} + +// ------------------------------------------------------------ // +// Object // +// ------------------------------------------------------------ // + +export namespace Object { + /** + * Object documentation. + */ + export type Foo = _.Object<'Foo', { + /** + * Field documentation. + * + * @deprecated Field a is deprecated. + */ + id: _.Field<_.Nullable<_.Scalar.ID>> + }> + + export type Bar = _.Object<'Bar', { + int: _.Field<_.Nullable<_.Scalar.Int>> + }> + + export type ObjectNested = _.Object<'ObjectNested', { + id: _.Field<_.Nullable<_.Scalar.ID>> + object: _.Field<_.Nullable> + }> + + export type lowerCaseObject = _.Object<'lowerCaseObject', { + id: _.Field<_.Nullable<_.Scalar.ID>> + }> + + export type lowerCaseObject2 = _.Object<'lowerCaseObject2', { + int: _.Field<_.Nullable<_.Scalar.Int>> + }> + + export type Object = _.Object<'Object', { + string: _.Field<_.Nullable<_.Scalar.String>> + int: _.Field<_.Nullable<_.Scalar.Int>> + float: _.Field<_.Nullable<_.Scalar.Float>> + boolean: _.Field<_.Nullable<_.Scalar.Boolean>> + id: _.Field<_.Nullable<_.Scalar.ID>> + }> + + export type Object1ImplementingInterface = _.Object<'Object1ImplementingInterface', { + id: _.Field<_.Nullable<_.Scalar.ID>> + int: _.Field<_.Nullable<_.Scalar.Int>> + }> + + export type Object2ImplementingInterface = _.Object<'Object2ImplementingInterface', { + id: _.Field<_.Nullable<_.Scalar.ID>> + boolean: _.Field<_.Nullable<_.Scalar.Boolean>> + }> +} + +// ------------------------------------------------------------ // +// Union // +// ------------------------------------------------------------ // + +export namespace Union { + /** + * Union documentation. + */ + export type FooBarUnion = _.Union<'FooBarUnion', [Object.Foo, Object.Bar]> + + export type lowerCaseUnion = _.Union<'lowerCaseUnion', [Object.lowerCaseObject, Object.lowerCaseObject2]> +} diff --git a/tests/builder/__snapshots__/generate.test.ts.snap b/tests/builder/__snapshots__/generate.test.ts.snap new file mode 100644 index 000000000..c9f54f5f4 --- /dev/null +++ b/tests/builder/__snapshots__/generate.test.ts.snap @@ -0,0 +1,190 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`generates types from GraphQL SDL file 1`] = ` +"import type * as _ from '../../../src/Schema/__.js' + +export namespace $ { +export interface Index { +Root: { +Query: Root.Query +Mutation: null +Subscription: null +} +objects: { +Foo: Object.Foo +Bar: Object.Bar +ObjectNested: Object.ObjectNested +lowerCaseObject: Object.lowerCaseObject +lowerCaseObject2: Object.lowerCaseObject2 +Object: Object.Object +Object1ImplementingInterface: Object.Object1ImplementingInterface +Object2ImplementingInterface: Object.Object2ImplementingInterface +} +unions: { +Union: Union.FooBarUnion +| Union.lowerCaseUnion +} +scalars: Scalars +} +export interface Scalars { +ID: string +String: string +Int: number +Float: number +Boolean: boolean +} +} + +// ------------------------------------------------------------ // +// Root // +// ------------------------------------------------------------ // + +export namespace Root { +export type Query = _.Object<"Query", { +interface: _.Field<_.Nullable> +id: _.Field<_.Nullable<_.Scalar.ID>> +idNonNull: _.Field<_.Scalar.ID> +string: _.Field<_.Nullable<_.Scalar.String>> +stringWithRequiredArg: _.Field<_.Nullable<_.Scalar.String>, _.Args<{ +string: _.Scalar.String +}>> +stringWithArgs: _.Field<_.Nullable<_.Scalar.String>, _.Args<{ +string: _.Nullable<_.Scalar.String> +int: _.Nullable<_.Scalar.Int> +float: _.Nullable<_.Scalar.Float> +boolean: _.Nullable<_.Scalar.Boolean> +id: _.Nullable<_.Scalar.ID> +}>> +stringWithArgEnum: _.Field<_.Nullable<_.Scalar.String>, _.Args<{ +ABCEnum: _.Nullable +}>> +stringWithListArg: _.Field<_.Nullable<_.Scalar.String>, _.Args<{ +ints: _.Nullable<_.List<_.Nullable<_.Scalar.Int>>> +}>> +stringWithListArgRequired: _.Field<_.Nullable<_.Scalar.String>, _.Args<{ +ints: _.List<_.Nullable<_.Scalar.Int>> +}>> +object: _.Field<_.Nullable> +listListIntNonNull: _.Field<_.List<_.List<_.Scalar.Int>>> +listListInt: _.Field<_.Nullable<_.List<_.Nullable<_.List<_.Nullable<_.Scalar.Int>>>>>> +listInt: _.Field<_.Nullable<_.List<_.Nullable<_.Scalar.Int>>>> +listIntNonNull: _.Field<_.List<_.Scalar.Int>> +objectNested: _.Field<_.Nullable> +objectNonNull: _.Field +objectWithArgs: _.Field<_.Nullable, _.Args<{ +string: _.Nullable<_.Scalar.String> +int: _.Nullable<_.Scalar.Int> +float: _.Nullable<_.Scalar.Float> +boolean: _.Nullable<_.Scalar.Boolean> +id: _.Nullable<_.Scalar.ID> +}>> +fooBarUnion: _.Field<_.Nullable> +/** +* Query enum field documentation. +*/ +abcEnum: _.Field<_.Nullable> +lowerCaseUnion: _.Field<_.Nullable> +}> +} + +// ------------------------------------------------------------ // +// Enum // +// ------------------------------------------------------------ // + +export namespace Enum { +/** +* Enum documentation. +* +* Members +* "A" - (DEPRECATED: Enum value A is deprecated.) +* "B" - Enum B member documentation. +* "C" - Enum C member documentation. (DEPRECATED: Enum value C is deprecated.) +*/ +export type ABCEnum = _.Enum<"ABCEnum", ["A", "B", "C"] > +} + +// ------------------------------------------------------------ // +// InputObject // +// ------------------------------------------------------------ // + +export namespace InputObject { +// -- no types -- + +} + +// ------------------------------------------------------------ // +// Interface // +// ------------------------------------------------------------ // + +export namespace Interface { +export type Interface = _.Interface<"Interface", { +id: _.Field<_.Nullable<_.Scalar.ID>> +}, [Object.Object1ImplementingInterface, Object.Object2ImplementingInterface]> +} + +// ------------------------------------------------------------ // +// Object // +// ------------------------------------------------------------ // + +export namespace Object { +/** +* Object documentation. +*/ +export type Foo = _.Object<"Foo", { +/** +* Field documentation. +* +* @deprecated Field a is deprecated. +*/ +id: _.Field<_.Nullable<_.Scalar.ID>> +}> + +export type Bar = _.Object<"Bar", { +int: _.Field<_.Nullable<_.Scalar.Int>> +}> + +export type ObjectNested = _.Object<"ObjectNested", { +id: _.Field<_.Nullable<_.Scalar.ID>> +object: _.Field<_.Nullable> +}> + +export type lowerCaseObject = _.Object<"lowerCaseObject", { +id: _.Field<_.Nullable<_.Scalar.ID>> +}> + +export type lowerCaseObject2 = _.Object<"lowerCaseObject2", { +int: _.Field<_.Nullable<_.Scalar.Int>> +}> + +export type Object = _.Object<"Object", { +string: _.Field<_.Nullable<_.Scalar.String>> +int: _.Field<_.Nullable<_.Scalar.Int>> +float: _.Field<_.Nullable<_.Scalar.Float>> +boolean: _.Field<_.Nullable<_.Scalar.Boolean>> +id: _.Field<_.Nullable<_.Scalar.ID>> +}> + +export type Object1ImplementingInterface = _.Object<"Object1ImplementingInterface", { +id: _.Field<_.Nullable<_.Scalar.ID>> +int: _.Field<_.Nullable<_.Scalar.Int>> +}> + +export type Object2ImplementingInterface = _.Object<"Object2ImplementingInterface", { +id: _.Field<_.Nullable<_.Scalar.ID>> +boolean: _.Field<_.Nullable<_.Scalar.Boolean>> +}> +} + +// ------------------------------------------------------------ // +// Union // +// ------------------------------------------------------------ // + +export namespace Union { +/** +* Union documentation. +*/ +export type FooBarUnion = _.Union<"FooBarUnion",[Object.Foo, Object.Bar]> + +export type lowerCaseUnion = _.Union<"lowerCaseUnion",[Object.lowerCaseObject, Object.lowerCaseObject2]> +}" +`; diff --git a/tests/builder/generate.test.ts b/tests/builder/generate.test.ts new file mode 100644 index 000000000..1fb013c2c --- /dev/null +++ b/tests/builder/generate.test.ts @@ -0,0 +1,13 @@ +import { readFile } from 'fs/promises' +import { expect, test } from 'vitest' +import { generateFile } from '../../src/generator/generator.js' + +test(`generates types from GraphQL SDL file`, async () => { + await generateFile({ + schemaPath: `./tests/builder/_/schema.graphql`, + typeScriptPath: `./tests/builder/_/schema.ts`, + }) + expect( + await readFile(`./tests/builder/_/schema.ts`, `utf8`), + ).toMatchSnapshot() +}) diff --git a/tsconfig.build.json b/tsconfig.build.json index c51514c70..321b962af 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -5,5 +5,5 @@ "rootDir": "src" }, "include": ["src"], - "exclude": ["**/*.spec.*"] + "exclude": ["**/*.test.*", "**/*.spec.*", "**/*.test-d.ts"] }