diff --git a/Cargo.lock b/Cargo.lock index 36761856996..3e16c3ad309 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -231,6 +231,19 @@ dependencies = [ "syn 2.0.42", ] +[[package]] +name = "async-tungstenite" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e9efbe14612da0a19fb983059a0b621e9cf6225d7018ecab4f9988215540dc" +dependencies = [ + "futures-io", + "futures-util", + "log", + "pin-project-lite", + "tungstenite", +] + [[package]] name = "atomic-polyfill" version = "1.0.3" @@ -1847,13 +1860,41 @@ dependencies = [ "thiserror", ] +[[package]] +name = "graphql-ws-client" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0063064d93204da0f0baf4dc0e03bf8a9307f6924a37e604b912a63b08aa7ea" +dependencies = [ + "async-tungstenite", + "futures", + "graphql_client 0.13.0", + "log", + "pin-project", + "serde", + "serde_json", + "thiserror", + "uuid", +] + [[package]] name = "graphql_client" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fc16d75d169fddb720d8f1c7aed6413e329e1584079b9734ff07266a193f5bc" dependencies = [ - "graphql_query_derive", + "graphql_query_derive 0.11.0", + "serde", + "serde_json", +] + +[[package]] +name = "graphql_client" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cdf7b487d864c2939b23902291a5041bc4a84418268f25fda1c8d4e15ad8fa" +dependencies = [ + "graphql_query_derive 0.13.0", "serde", "serde_json", ] @@ -1875,13 +1916,41 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "graphql_client_codegen" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a40f793251171991c4eb75bd84bc640afa8b68ff6907bc89d3b712a22f700506" +dependencies = [ + "graphql-introspection-query", + "graphql-parser", + "heck 0.4.1", + "lazy_static", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 1.0.109", +] + [[package]] name = "graphql_query_derive" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a755cc59cda2641ea3037b4f9f7ef40471c329f55c1fa2db6fa0bb7ae6c1f7ce" dependencies = [ - "graphql_client_codegen", + "graphql_client_codegen 0.11.0", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "graphql_query_derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bda454f3d313f909298f626115092d348bc231025699f557b27e248475f48c" +dependencies = [ + "graphql_client_codegen 0.13.0", "proc-macro2", "syn 1.0.109", ] @@ -3772,6 +3841,18 @@ dependencies = [ "sct", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -4706,7 +4787,10 @@ checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", + "rustls", + "rustls-native-certs", "tokio", + "tokio-rustls", "tungstenite", ] @@ -4984,6 +5068,7 @@ dependencies = [ "httparse", "log", "rand", + "rustls", "sha1", "thiserror", "url", @@ -6106,7 +6191,7 @@ dependencies = [ "filetime", "flate2", "futures-util", - "graphql_client", + "graphql_client 0.11.0", "hex", "indexmap 1.9.3", "indicatif", @@ -6147,8 +6232,10 @@ dependencies = [ "dirs", "filetime", "flate2", + "futures", "futures-util", - "graphql_client", + "graphql-ws-client", + "graphql_client 0.13.0", "hex", "indexmap 1.9.3", "indicatif", @@ -6170,6 +6257,7 @@ dependencies = [ "time", "tldextract", "tokio", + "tokio-tungstenite", "toml 0.5.11", "tracing", "url", diff --git a/lib/cli/src/commands/publish.rs b/lib/cli/src/commands/publish.rs index d2d83eecd51..9f2c65b3e6c 100644 --- a/lib/cli/src/commands/publish.rs +++ b/lib/cli/src/commands/publish.rs @@ -34,13 +34,14 @@ pub struct Publish { /// /// Note that this is not the timeout for the entire publish process, but /// for each individual query to the registry during the publish flow. - #[clap(long, default_value = "30s")] + #[clap(long, default_value = "2m")] pub timeout: humantime::Duration, } impl Publish { /// Executes `wasmer publish` - pub fn execute(&self) -> Result<(), anyhow::Error> { + #[tokio::main] + pub async fn execute(&self) -> Result<(), anyhow::Error> { let token = self .env .token() @@ -58,7 +59,7 @@ impl Publish { wait: self.wait, timeout: self.timeout.into(), }; - publish.execute().map_err(on_error)?; + publish.execute().await.map_err(on_error)?; if let Err(e) = invalidate_graphql_query_cache(&self.env) { tracing::warn!( diff --git a/lib/registry/Cargo.toml b/lib/registry/Cargo.toml index c6fcb531999..0c71ad4fa00 100644 --- a/lib/registry/Cargo.toml +++ b/lib/registry/Cargo.toml @@ -20,8 +20,10 @@ dialoguer = "0.11.0" dirs = "4.0.0" filetime = "0.2.19" flate2 = "1.0.24" +futures = "0.3" futures-util = "0.3.25" -graphql_client = "0.11.0" +graphql-ws-client = {version = "0.6.0", features = ["client-graphql-client"]} +graphql_client = "0.13.0" hex = "0.4.3" indexmap = { version = "1.9.3", optional = true } indicatif = "0.17.2" @@ -41,7 +43,8 @@ tempfile = "3.6.0" thiserror = "1.0.37" time = { version = "0.3.17", default-features = false, features = ["parsing", "std", "formatting"], optional = true } tldextract = "0.6.0" -tokio = "1.24.0" +tokio = {version = "1"} +tokio-tungstenite = {version = "0.20", features = ["rustls-tls-native-roots"]} toml = "0.5.9" tracing = "0.1.40" url = "2.3.1" diff --git a/lib/registry/graphql/mutations/publish_package_chunked.graphql b/lib/registry/graphql/mutations/publish_package_chunked.graphql index 398b4fbbaf4..63b11ffd821 100644 --- a/lib/registry/graphql/mutations/publish_package_chunked.graphql +++ b/lib/registry/graphql/mutations/publish_package_chunked.graphql @@ -35,6 +35,7 @@ mutation PublishPackageMutationChunked( ) { success packageVersion { + id version } } diff --git a/lib/registry/graphql/schema.graphql b/lib/registry/graphql/schema.graphql index a3d7ebeec18..7bed7db3a31 100644 --- a/lib/registry/graphql/schema.graphql +++ b/lib/registry/graphql/schema.graphql @@ -40,7 +40,7 @@ type User implements Node & PackageOwner & Owner { twitterUrl: String companyRole: String companyDescription: String - publicActivity(before: String, after: String, first: Int, last: Int): ActivityEventConnection! + publicActivity(offset: Int, before: String, after: String, first: Int, last: Int): ActivityEventConnection! billing: Billing waitlist(name: String!): WaitlistMember namespaces(role: GrapheneRole, offset: Int, before: String, after: String, first: Int, last: Int): NamespaceConnection! @@ -83,6 +83,9 @@ type ActivityEventConnection { """Contains the nodes in this connection.""" edges: [ActivityEventEdge]! + + """Total number of items in the connection.""" + totalCount: Int } """ @@ -114,12 +117,12 @@ type ActivityEventEdge { type ActivityEvent implements Node { """The ID of the object""" id: ID! - body: ActivityEventBody! + body: EventBody! actorIcon: String! createdAt: DateTime! } -type ActivityEventBody { +type EventBody { text: String! ranges: [NodeBodyRange!]! } @@ -178,6 +181,9 @@ type Namespace implements Node & PackageOwner & Owner { description: String! avatar: String! avatarUpdatedAt: DateTime + twitterHandle: String + githubHandle: String + websiteUrl: String createdAt: DateTime! updatedAt: DateTime! maintainerInvites(offset: Int, before: String, after: String, first: Int, last: Int): NamespaceCollaboratorInviteConnection! @@ -189,9 +195,15 @@ type Namespace implements Node & PackageOwner & Owner { packageVersions(offset: Int, before: String, after: String, first: Int, last: Int): PackageVersionConnection! collaborators(offset: Int, before: String, after: String, first: Int, last: Int): NamespaceCollaboratorConnection! publicActivity(before: String, after: String, first: Int, last: Int): ActivityEventConnection! - pendingInvites(before: String, after: String, first: Int, last: Int): NamespaceCollaboratorInviteConnection! + pendingInvites(offset: Int, before: String, after: String, first: Int, last: Int): NamespaceCollaboratorInviteConnection! viewerHasRole(role: GrapheneRole!): Boolean! - packageTransfersIncoming(before: String, after: String, first: Int, last: Int): PackageTransferRequestConnection! + + """Whether the current user is invited to the namespace""" + viewerIsInvited: Boolean! + + """The invitation for the current user to the namespace""" + viewerInvitation: NamespaceCollaboratorInvite + packageTransfersIncoming(offset: Int, before: String, after: String, first: Int, last: Int): PackageTransferRequestConnection! usageMetrics(forRange: MetricRange!, variant: MetricType!): [UsageMetric]! } @@ -323,8 +335,10 @@ type Package implements Likeable & Node & PackageOwner { totalDownloads: Int! iconUpdatedAt: DateTime watchersCount: Int! + webcs(offset: Int, before: String, after: String, first: Int, last: Int): WebcImageConnection! versions: [PackageVersion]! collectionSet: [Collection!]! + categories(offset: Int, before: String, after: String, first: Int, last: Int): CategoryConnection! keywords(offset: Int, before: String, after: String, first: Int, last: Int): PackageKeywordConnection! likersCount: Int! viewerHasLiked: Boolean! @@ -355,6 +369,12 @@ type Package implements Likeable & Node & PackageOwner { viewerIsWatching: Boolean! showDeployButton: Boolean! similarPackageVersions(before: String, after: String, first: Int, last: Int): PackageSearchConnection! + + """Whether the current user is invited to the package""" + viewerIsInvited: Boolean! + + """The invitation for the current user to the package""" + viewerInvitation: PackageCollaboratorInvite } interface Likeable { @@ -381,20 +401,15 @@ type PackageVersion implements Node { staticObjectsCompiled: Boolean! nativeExecutablesCompiled: Boolean! publishedBy: User! + clientName: String signature: Signature isArchived: Boolean! file: String! """""" fileSize: BigInt! - piritaFile: String - - """""" - piritaFileSize: BigInt! - piritaManifest: JSONString - piritaVolumes: JSONString + webc: WebcImage totalDownloads: Int! - pirita256hash: String @deprecated(reason: "Please use distribution.piritaSha256Hash instead.") bindingsState: RegistryPackageVersionBindingsStateChoices! nativeExecutablesState: RegistryPackageVersionNativeExecutablesStateChoices! @@ -407,6 +422,12 @@ type PackageVersion implements Node { bindingsgeneratorSet(offset: Int, before: String, after: String, first: Int, last: Int): BindingsGeneratorConnection! javascriptlanguagebindingSet(offset: Int, before: String, after: String, first: Int, last: Int): PackageVersionNPMBindingConnection! pythonlanguagebindingSet(offset: Int, before: String, after: String, first: Int, last: Int): PackageVersionPythonBindingConnection! + piritaFile: String @deprecated(reason: "Please use distribution.piritaDownloadUrl instead.") + piritaFileSize: Int @deprecated(reason: "Please use distribution.piritaSize instead.") + piritaManifest: JSONString + piritaOffsets: JSONString + piritaVolumes: JSONString + pirita256hash: String @deprecated(reason: "Please use distribution.piritaSha256Hash instead.") distribution: PackageDistribution! filesystem: [PackageVersionFilesystem]! isLastVersion: Boolean! @@ -432,6 +453,22 @@ compatible type. """ scalar BigInt +type WebcImage implements Node { + """The ID of the object""" + id: ID! + + """""" + fileSize: BigInt! + manifest: JSONString! + volumes: JSONString! + offsets: JSONString! + webcSha256: String! + targzSha256: String + createdAt: DateTime! + updatedAt: DateTime! + webcUrl: String! +} + """ Allows use of a JSON String for input / output from the GraphQL schema. @@ -514,6 +551,7 @@ type DeployAppVersion implements Node { app: DeployApp! yamlConfig: String! userYamlConfig: String! + clientName: String signature: String description: String publishedBy: User! @@ -532,7 +570,10 @@ type DeployAppVersion implements Node { """ Get logs starting from this timestamp. Takes EPOCH timestamp in seconds. """ - startingFrom: Float! + startingFrom: Float + + """Get logs starting from this timestamp. Takes ISO timestamp.""" + startingFromISO: DateTime """Fetch logs until this timestamp. Takes EPOCH timestamp in seconds.""" until: Float @@ -680,6 +721,19 @@ type PackageVersionModule { source: String! abi: String publicUrl: String! + atom: PiritaFilesystemFile! + rangeHeader: String! +} + +type PiritaFilesystemFile { + name(display: PiritaFilesystemNameDisplay): String! + size: Int! + offset: Int! +} + +enum PiritaFilesystemNameDisplay { + RELATIVE + ABSOLUTE } type NativeExecutableConnection { @@ -780,6 +834,12 @@ type PackageVersionNPMBinding implements PackageVersionLanguageBinding & Node { """Name of package source""" packageName: String! + + """Name of the package to import""" + importablePackageName: String! + + """Code snippet example to use the package""" + codeSnippetExample: String! module: String! @deprecated(reason: "Do not use this field, since bindings for all modules are generated at once now.") npmDefaultInstallPackageName(url: String): String! @deprecated(reason: "Please use packageName instead") } @@ -801,6 +861,12 @@ interface PackageVersionLanguageBinding { """Name of package source""" packageName: String! + + """Name of the package to import""" + importablePackageName: String! + + """Code snippet example to use the package""" + codeSnippetExample: String! module: String! @deprecated(reason: "Do not use this field, since bindings for all modules are generated at once now.") } @@ -849,15 +915,21 @@ type PackageVersionPythonBinding implements PackageVersionLanguageBinding & Node """Name of package source""" packageName: String! + + """Name of the package to import""" + importablePackageName: String! + + """Code snippet example to use the package""" + codeSnippetExample: String! module: String! @deprecated(reason: "Do not use this field, since bindings for all modules are generated at once now.") pythonDefaultInstallPackageName(url: String): String! } type PackageDistribution { downloadUrl: String! - size: Int! + size: Int piritaDownloadUrl: String - piritaSize: Int! + piritaSize: Int piritaSha256Hash: String } @@ -914,17 +986,6 @@ type InterfaceVersionEdge { union PiritaFilesystemItem = PiritaFilesystemFile | PiritaFilesystemDir -type PiritaFilesystemFile { - name(display: PiritaFilesystemNameDisplay): String! - size: Int! - offset: Int! -} - -enum PiritaFilesystemNameDisplay { - RELATIVE - ABSOLUTE -} - type PiritaFilesystemDir { name(display: PiritaFilesystemNameDisplay): String! } @@ -936,6 +997,26 @@ type WEBCFilesystemItem { offset: Int! } +type WebcImageConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [WebcImageEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `WebcImage` and its cursor.""" +type WebcImageEdge { + """The item at the end of the edge""" + node: WebcImage + + """A cursor for use in pagination""" + cursor: String! +} + type Collection { slug: String! displayName: String! @@ -945,6 +1026,35 @@ type Collection { packages(before: String, after: String, first: Int, last: Int): PackageConnection! } +type CategoryConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [CategoryEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `Category` and its cursor.""" +type CategoryEdge { + """The item at the end of the edge""" + node: Category + + """A cursor for use in pagination""" + cursor: String! +} + +type Category implements Node { + """The ID of the object""" + id: ID! + + """A category is a label that can be attached to a package.""" + name: String! + packages(offset: Int, before: String, after: String, first: Int, last: Int): PackageConnection +} + type PackageKeywordConnection { """Pagination data for this connection.""" pageInfo: PageInfo! @@ -1233,6 +1343,7 @@ type UserNotificationConnection { """Contains the nodes in this connection.""" edges: [UserNotificationEdge]! hasPendingNotifications: Boolean! + pendingNotificationsCount: Int! } """A Relay edge containing a `UserNotification` and its cursor.""" @@ -1248,24 +1359,19 @@ type UserNotification implements Node { """The ID of the object""" id: ID! icon: String - body: UserNotificationBody! + body: EventBody! seenState: UserNotificationSeenState! kind: UserNotificationKind createdAt: DateTime! } -type UserNotificationBody { - text: String! - ranges: [NodeBodyRange]! -} - enum UserNotificationSeenState { UNSEEN SEEN SEEN_AND_READ } -union UserNotificationKind = UserNotificationKindPublishedPackageVersion | UserNotificationKindIncomingPackageTransfer | UserNotificationKindIncomingPackageInvite | UserNotificationKindIncomingNamespaceInvite +union UserNotificationKind = UserNotificationKindPublishedPackageVersion | UserNotificationKindIncomingPackageTransfer | UserNotificationKindIncomingPackageInvite | UserNotificationKindIncomingNamespaceInvite | UserNotificationKindValidateEmail type UserNotificationKindPublishedPackageVersion { packageVersion: PackageVersion! @@ -1275,11 +1381,15 @@ type UserNotificationKindIncomingNamespaceInvite { namespaceInvite: NamespaceCollaboratorInvite! } +type UserNotificationKindValidateEmail { + user: User! +} + """ Enum of ways a user can login. One user can have many login methods associated with their account. - + """ enum LoginMethod { GOOGLE @@ -1524,7 +1634,13 @@ type Payment { """Log entry for deploy app.""" type Log { + """Timestamp in nanoseconds""" timestamp: Float! + + """ISO 8601 string in UTC""" + datetime: DateTime! + + """Log message""" message: String! } @@ -1539,14 +1655,16 @@ type Query { getUser(username: String!): User getPasswordResetToken(token: String!): GetPasswordResetToken getAuthNonce(name: String!): Nonce - packages(before: String, after: String, first: Int, last: Int): PackageConnection + packages(offset: Int, before: String, after: String, first: Int, last: Int): PackageConnection recentPackageVersions(curated: Boolean, offset: Int, before: String, after: String, first: Int, last: Int): PackageVersionConnection! allPackageVersions(sortBy: PackageVersionSortBy, createdAfter: DateTime, updatedAfter: DateTime, offset: Int, before: String, after: String, first: Int, last: Int): PackageVersionConnection! + getWebcImage(hash: String!): WebcImage getNamespace(name: String!): Namespace getPackage(name: String!): Package getPackages(names: [String!]!): [Package]! getPackageVersion(name: String!, version: String = "latest"): PackageVersion getPackageVersions(names: [String!]!): [PackageVersion] + getPackageVersionByHash(name: String!, hash: String!): PackageVersion getInterface(name: String!): Interface getInterfaces(names: [String!]!): [Interface]! getInterfaceVersion(name: String!, version: String = "latest"): InterfaceVersion @@ -1557,6 +1675,7 @@ type Query { getCommands(names: [String!]!): [Command] getCollections(before: String, after: String, first: Int, last: Int): CollectionConnection getSignedUrlForPackageUpload(name: String!, version: String = "latest", expiresAfterSeconds: Int = 60): SignedUrl + categories(offset: Int, before: String, after: String, first: Int, last: Int): CategoryConnection! blogposts(tags: [String!], before: String, after: String, first: Int, last: Int): BlogPostConnection! getBlogpost(slug: String, featured: Boolean): BlogPost allBlogpostTags(offset: Int, before: String, after: String, first: Int, last: Int): BlogPostTagConnection @@ -1642,8 +1761,10 @@ type BlogPost implements Node { owner: User body: String! publishDate: DateTime + theme: BlogBlogPostThemeChoices! url: String! coverImageUrl: String + opengraphImageUrl: String tagline: String! relatedArticles: [BlogPost!] updatedAt: DateTime! @@ -1651,6 +1772,20 @@ type BlogPost implements Node { editUrl: String } +enum BlogBlogPostThemeChoices { + """Green""" + GREEN + + """Purple""" + PURPLE + + """Orange""" + ORANGE + + """Blue""" + BLUE +} + type BlogPostTag implements Node { """The ID of the object""" id: ID! @@ -1701,19 +1836,59 @@ union SearchResult = PackageVersion | User | Namespace | DeployApp | BlogPost input PackagesFilter { count: Int = 1000 sortBy: SearchOrderSort = ASC + + """Filter packages by being curated.""" curated: Boolean + + """Filter packages by publish date.""" publishDate: SearchPublishDate + + """Filter packages by having bindings.""" hasBindings: Boolean = false + + """Filter packages by being standalone.""" isStandalone: Boolean = false + + """Filter packages by having commands.""" hasCommands: Boolean = false + + """Filter packages by interface.""" withInterfaces: [String] + + """Filter packages by deployable status.""" deployable: Boolean + + """Filter packages by license.""" license: String + + """Filter packages created after this date.""" + createdAfter: DateTime + + """Filter packages created before this date.""" + createdBefore: DateTime + + """Filter packages with version published after this date.""" + lastPublishedAfter: DateTime + + """Filter packages with version published before this date.""" + lastPublishedBefore: DateTime + + """Filter packages by size.""" size: CountFilter + + """Filter packages by download count.""" downloads: CountFilter + + """Filter packages by like count.""" likes: CountFilter + + """Filter packages by owner.""" owner: String + + """Filter packages by published by.""" publishedBy: String + + """Order packages by field.""" orderBy: PackageOrderBy = PUBLISHED_DATE } @@ -1747,14 +1922,30 @@ enum PackageOrderBy { SIZE TOTAL_DOWNLOADS PUBLISHED_DATE + CREATED_DATE + TOTAL_LIKES } input NamespacesFilter { count: Int = 1000 sortBy: SearchOrderSort = ASC + + """Filter namespaces by package count.""" packageCount: CountFilter + + """Filter namespaces created after this date.""" + createdAfter: DateTime + + """Filter namespaces created before this date.""" + createdBefore: DateTime + + """Filter namespaces by user count.""" userCount: CountFilter + + """Filter namespaces by collaborator.""" collaborator: String + + """Order namespaces by field.""" orderBy: NamespaceOrderBy = CREATED_DATE } @@ -1768,8 +1959,20 @@ enum NamespaceOrderBy { input UsersFilter { count: Int = 1000 sortBy: SearchOrderSort = ASC + + """Filter users by package count.""" packageCount: CountFilter + + """Filter users by namespace count.""" namespaceCount: CountFilter + + """Filter users joined after this date.""" + joinedAfter: DateTime + + """Filter users joined before this date.""" + joinedBefore: DateTime + + """Order users by field.""" orderBy: UserOrderBy = CREATED_DATE } @@ -1782,13 +1985,36 @@ enum UserOrderBy { input AppFilter { count: Int = 1000 sortBy: SearchOrderSort = ASC + + """Filter apps by deployed by.""" deployedBy: String + + """Filter apps last deployed after this date.""" + lastDeployedAfter: DateTime + + """Filter apps last deployed before this date.""" + lastDeployedBefore: DateTime + + """Filter apps by owner.""" owner: String + + """Order apps by field.""" + orderBy: AppOrderBy = CREATED_DATE + + """Filter apps by client name.""" + clientName: String +} + +enum AppOrderBy { + PUBLISHED_DATE + CREATED_DATE } input BlogPostsFilter { count: Int = 1000 sortBy: SearchOrderSort = ASC + + """Filter blog posts by tag.""" tags: [String] } @@ -1869,6 +2095,8 @@ type Mutation { """ detachPaymentMethod(input: DetachPaymentMethodInput!): DetachPaymentMethodPayload generateDeployConfigToken(input: GenerateDeployConfigTokenInput!): GenerateDeployConfigTokenPayload + renameApp(input: RenameAppInput!): RenameAppPayload + renameAppAlias(input: RenameAppAliasInput!): RenameAppAliasPayload requestAppTransfer(input: RequestAppTransferInput!): RequestAppTransferPayload acceptAppTransferRequest(input: AcceptAppTransferRequestInput!): AcceptAppTransferRequestPayload removeAppTransferRequest(input: RemoveAppTransferRequestInput!): RemoveAppTransferRequestPayload @@ -2061,6 +2289,36 @@ input GenerateDeployConfigTokenInput { clientMutationId: String } +type RenameAppPayload { + success: Boolean! + app: DeployApp! + clientMutationId: String +} + +input RenameAppInput { + """App ID to delete.""" + id: ID! + + """New name for the app.""" + name: String! + clientMutationId: String +} + +type RenameAppAliasPayload { + success: Boolean! + alias: AppAlias! + clientMutationId: String +} + +input RenameAppAliasInput { + """App alias ID to delete.""" + id: ID! + + """New name for the alias.""" + name: String! + clientMutationId: String +} + type RequestAppTransferPayload { appTransferRequest: AppTransferRequest! clientMutationId: String @@ -2441,6 +2699,12 @@ input PublishPackageInput { """Whether the package is private""" private: Boolean = false + + """The upload format of the package""" + uploadFormat: UploadFormat = targz + + """Whether to wait for webc generation to finish""" + wait: Boolean = false clientMutationId: String } @@ -2449,6 +2713,11 @@ input InputSignature { data: String! } +enum UploadFormat { + targz + webcv2 +} + type UpdatePackagePayload { package: Package! clientMutationId: String @@ -2562,6 +2831,19 @@ input UpdateNamespaceInput { """The namespace avatar""" avatar: String + + """ + The user Twitter (it can be the url, or the handle with or without the @) + """ + twitter: String + + """ + The user Github (it can be the url, or the handle with or without the @) + """ + github: String + + """The user website (it must be a valid url)""" + websiteUrl: String clientMutationId: String } @@ -2763,9 +3045,27 @@ input MakePackagePublicInput { type Subscription { packageVersionCreated(publishedBy: ID, ownerId: ID): PackageVersion! + + """Subscribe to package version ready""" + packageVersionReady(packageVersionId: ID!): PackageVersionReadyResponse! + + """Subscribe to new messages""" + newMessage: String! userNotificationCreated(userId: ID!): UserNotificationCreated! } +type PackageVersionReadyResponse { + state: PackageVersionState! + packageVersion: PackageVersion! + success: Boolean! +} + +enum PackageVersionState { + WEBC_GENERATED + BINDINGS_GENERATED + NATIVE_EXES_GENERATED +} + type UserNotificationCreated { notification: UserNotification notificationDeletedId: ID diff --git a/lib/registry/graphql/subscriptions/packageVersionReady.graphql b/lib/registry/graphql/subscriptions/packageVersionReady.graphql new file mode 100644 index 00000000000..f7324e50daf --- /dev/null +++ b/lib/registry/graphql/subscriptions/packageVersionReady.graphql @@ -0,0 +1,6 @@ +subscription PackageVersionReady ($packageVersionId: ID!) { + packageVersionReady(packageVersionId: $packageVersionId) { + state + success + } +} diff --git a/lib/registry/src/graphql/mod.rs b/lib/registry/src/graphql/mod.rs index 2c560454912..467a1aaec84 100644 --- a/lib/registry/src/graphql/mod.rs +++ b/lib/registry/src/graphql/mod.rs @@ -1,6 +1,7 @@ pub(crate) mod mutations; pub(crate) mod proxy; pub(crate) mod queries; +pub(crate) mod subscriptions; use graphql_client::*; use reqwest::{ diff --git a/lib/registry/src/graphql/subscriptions.rs b/lib/registry/src/graphql/subscriptions.rs new file mode 100644 index 00000000000..6f0115f4a02 --- /dev/null +++ b/lib/registry/src/graphql/subscriptions.rs @@ -0,0 +1,9 @@ +use graphql_client::GraphQLQuery; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "graphql/schema.graphql", + query_path = "graphql/subscriptions/packageVersionReady.graphql", + response_derives = "Debug" +)] +pub struct PackageVersionReady; diff --git a/lib/registry/src/lib.rs b/lib/registry/src/lib.rs index 89edf9e9ba5..d2f1afce06c 100644 --- a/lib/registry/src/lib.rs +++ b/lib/registry/src/lib.rs @@ -17,6 +17,8 @@ pub mod interface; pub mod login; pub mod package; pub mod publish; +pub mod subscriptions; +pub(crate) mod tokio_spawner; pub mod types; pub mod utils; pub mod wasmer_env; diff --git a/lib/registry/src/package/builder.rs b/lib/registry/src/package/builder.rs index 386b90f8782..612c1da21c2 100644 --- a/lib/registry/src/package/builder.rs +++ b/lib/registry/src/package/builder.rs @@ -63,7 +63,7 @@ enum PackageBuildError { impl Publish { /// Executes `wasmer publish` - pub fn execute(&self) -> Result<(), anyhow::Error> { + pub async fn execute(&self) -> Result<(), anyhow::Error> { let input_path = match self.package_path.as_ref() { Some(s) => std::env::current_dir()?.join(s), None => std::env::current_dir()?, @@ -159,7 +159,7 @@ impl Publish { // dry run: publish is done here println!( - "Successfully published package `{}@{}`", + "🚀 Successfully published package `{}@{}`", manifest.package.name, manifest.package.version ); @@ -194,6 +194,7 @@ impl Publish { self.wait, self.timeout, ) + .await } fn validation_policy(&self) -> Box { diff --git a/lib/registry/src/publish.rs b/lib/registry/src/publish.rs index 126604e201e..03b3f9c12b4 100644 --- a/lib/registry/src/publish.rs +++ b/lib/registry/src/publish.rs @@ -1,21 +1,27 @@ -use anyhow::Context; -use console::{style, Emoji}; -use graphql_client::GraphQLQuery; -use indicatif::{ProgressBar, ProgressState, ProgressStyle}; -use std::fmt::Write; -use std::io::BufRead; -use std::path::PathBuf; -use std::{collections::BTreeMap, time::Duration}; - use crate::graphql::queries::get_signed_url::GetSignedUrlUrl; + +use crate::graphql::subscriptions::package_version_ready::PackageVersionState; use crate::graphql::{ mutations::{publish_package_mutation_chunked, PublishPackageMutationChunked}, queries::{get_signed_url, GetSignedUrl}, }; +use crate::subscriptions::subscribe_package_version_ready; use crate::{format_graphql, WasmerConfig}; +use anyhow::{Context, Result}; +use console::{style, Emoji}; +use futures_util::StreamExt; +use graphql_client::GraphQLQuery; +use indicatif::{MultiProgress, ProgressBar, ProgressState, ProgressStyle}; +use std::collections::BTreeMap; +use std::fmt::Write; +use std::io::BufRead; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; -static UPLOAD: Emoji<'_, '_> = Emoji("⬆️ ", ""); -static PACKAGE: Emoji<'_, '_> = Emoji("📦 ", ""); +static UPLOAD: Emoji<'_, '_> = Emoji("⬆️ ", ""); +static PACKAGE: Emoji<'_, '_> = Emoji("📦", ""); #[derive(Debug, Clone)] pub enum SignArchiveResult { @@ -27,7 +33,7 @@ pub enum SignArchiveResult { } #[allow(clippy::too_many_arguments)] -pub fn try_chunked_uploading( +pub async fn try_chunked_uploading( registry: Option, token: Option, package: &wasmer_toml::Package, @@ -54,11 +60,10 @@ pub fn try_chunked_uploading( if !quiet { println!("{} {} Uploading...", style("[1/2]").bold().dim(), UPLOAD); } - upload_package(&signed_url.url, archive_path, archived_data_size, timeout)?; if !quiet { - println!("{} {}Publishing...", style("[2/2]").bold().dim(), PACKAGE); + println!("{} {} Publishing...", style("[2/2]").bold().dim(), PACKAGE); } let q = @@ -79,12 +84,27 @@ pub fn try_chunked_uploading( wait: Some(wait), }); - let _response: publish_package_mutation_chunked::ResponseData = + let response: publish_package_mutation_chunked::ResponseData = crate::graphql::execute_query_with_timeout(®istry, &token, timeout, &q)?; + if let Some(pkg) = response.publish_package { + if !pkg.success { + return Err(anyhow::anyhow!("Could not publish package")); + } + if wait { + wait_for_package_version_to_become_ready( + ®istry, + &token, + pkg.package_version.id, + quiet, + ) + .await?; + } + } + println!( - "Successfully published package `{}@{}`", - package.name, package.version + "🚀 Successfully published package `{}@{}`", + package.name, package.version, ); Ok(()) @@ -289,3 +309,107 @@ fn upload_package( pb.finish_and_clear(); Ok(()) } + +struct PackageVersionReadySharedState { + webc_generated: Arc>>, + bindings_generated: Arc>>, + native_exes_generated: Arc>>, +} + +impl PackageVersionReadySharedState { + fn new() -> Self { + Self { + webc_generated: Arc::new(Mutex::new(Option::None)), + bindings_generated: Arc::new(Mutex::new(Option::None)), + native_exes_generated: Arc::new(Mutex::new(Option::None)), + } + } +} + +fn create_spinner(m: &MultiProgress, message: String) -> ProgressBar { + let spinner = m.add(ProgressBar::new_spinner()); + spinner.set_message(message); + spinner.set_style(ProgressStyle::default_spinner()); + spinner.enable_steady_tick(Duration::from_millis(100)); + spinner +} + +fn show_spinners_while_waiting(state: &PackageVersionReadySharedState) { + // Clone shared state for threads + let (state_webc, state_bindings, state_native) = ( + Arc::clone(&state.webc_generated), + Arc::clone(&state.bindings_generated), + Arc::clone(&state.native_exes_generated), + ); + let m = MultiProgress::new(); + + let webc_spinner = create_spinner(&m, String::from("Generating WEBC...")); + let bindings_spinner = create_spinner(&m, String::from("Generating language bindings...")); + let exe_spinner = create_spinner(&m, String::from("Generating native executables...")); + + let check_and_finish = |spinner: ProgressBar, state: Arc>>, name: String| { + thread::spawn(move || loop { + match state.lock() { + Ok(lock) => { + if lock.is_some() { + spinner.finish_with_message(format!("✅ {} generation complete", name)); + break; + } + } + Err(_) => { + break; + } + } + thread::sleep(Duration::from_millis(100)); + }); + }; + check_and_finish(webc_spinner, state_webc, String::from("WEBC")); + check_and_finish( + bindings_spinner, + state_bindings, + String::from("Language bindings"), + ); + check_and_finish( + exe_spinner, + state_native, + String::from("Native executables"), + ); +} + +async fn wait_for_package_version_to_become_ready( + registry: &str, + token: &str, + package_version_id: impl AsRef, + quiet: bool, +) -> Result<()> { + let (mut stream, _client) = + subscribe_package_version_ready(registry, token, package_version_id.as_ref()).await?; + + let state = PackageVersionReadySharedState::new(); + + if !quiet { + show_spinners_while_waiting(&state); + } + + while let Some(data) = stream.next().await { + if let Some(res_data) = data.unwrap().data { + match res_data.package_version_ready.state { + PackageVersionState::BINDINGS_GENERATED => { + let mut st = state.bindings_generated.lock().unwrap(); + *st = Some(res_data.package_version_ready.success); + } + PackageVersionState::NATIVE_EXES_GENERATED => { + let mut st = state.native_exes_generated.lock().unwrap(); + *st = Some(res_data.package_version_ready.success); + } + PackageVersionState::WEBC_GENERATED => { + let mut st = state.webc_generated.lock().unwrap(); + *st = Some(res_data.package_version_ready.success); + } + PackageVersionState::Other(_) => {} + } + } + } + + Ok(()) +} diff --git a/lib/registry/src/subscriptions.rs b/lib/registry/src/subscriptions.rs new file mode 100644 index 00000000000..f310dd4b0c8 --- /dev/null +++ b/lib/registry/src/subscriptions.rs @@ -0,0 +1,86 @@ +use std::env; + +use crate::{ + graphql::subscriptions::{package_version_ready, PackageVersionReady}, + tokio_spawner::TokioSpawner, +}; + +use futures_util::StreamExt; + +use graphql_client::GraphQLQuery; +use graphql_ws_client::{ + graphql::{GraphQLClient, StreamingOperation}, + GraphQLClientClient, SubscriptionStream, +}; + +use tokio_tungstenite::{ + connect_async, + tungstenite::{client::IntoClientRequest, http::HeaderValue, Message}, +}; + +async fn subscribe_graphql( + registry_url: &str, + login_token: &str, + variables: Q::Variables, +) -> anyhow::Result<( + SubscriptionStream>, + GraphQLClientClient, +)> +where + ::Variables: Send + Sync + Unpin, +{ + let mut url = url::Url::parse(registry_url).unwrap(); + if url.scheme() == "http" { + url.set_scheme("ws").unwrap(); + } else if url.scheme() == "https" { + url.set_scheme("wss").unwrap(); + } + + let mut req = url.into_client_request()?; + req.headers_mut().insert( + "Sec-WebSocket-Protocol", + HeaderValue::from_str("graphql-transport-ws")?, + ); + let token = env::var("WASMER_TOKEN") + .ok() + .or_else(|| env::var("WAPM_REGISTRY_TOKEN").ok()) + .unwrap_or_else(|| login_token.to_string()); + req.headers_mut().insert( + reqwest::header::AUTHORIZATION, + HeaderValue::from_str(&format!("Bearer {}", token))?, + ); + let user_agent = crate::client::RegistryClient::default_user_agent(); + req.headers_mut().insert( + reqwest::header::USER_AGENT, + HeaderValue::from_str(&user_agent)?, + ); + let (ws_con, _resp) = connect_async(req).await?; + let (sink, stream) = ws_con.split(); + + let mut client = graphql_ws_client::GraphQLClientClientBuilder::new() + .build(stream, sink, TokioSpawner::current()) + .await?; + let stream = client + .streaming_operation(StreamingOperation::::new(variables)) + .await?; + + Ok((stream, client)) +} + +pub async fn subscribe_package_version_ready( + registry_url: &str, + login_token: &str, + package_version_id: &str, +) -> anyhow::Result<( + SubscriptionStream>, + GraphQLClientClient, +)> { + subscribe_graphql( + registry_url, + login_token, + package_version_ready::Variables { + package_version_id: package_version_id.to_string(), + }, + ) + .await +} diff --git a/lib/registry/src/tokio_spawner.rs b/lib/registry/src/tokio_spawner.rs new file mode 100644 index 00000000000..d4937e1d9ab --- /dev/null +++ b/lib/registry/src/tokio_spawner.rs @@ -0,0 +1,22 @@ +#[derive(Debug)] +pub struct TokioSpawner(tokio::runtime::Handle); + +impl TokioSpawner { + pub fn new(handle: tokio::runtime::Handle) -> Self { + TokioSpawner(handle) + } + + pub fn current() -> Self { + TokioSpawner::new(tokio::runtime::Handle::current()) + } +} + +impl futures::task::Spawn for TokioSpawner { + fn spawn_obj( + &self, + obj: futures::task::FutureObj<'static, ()>, + ) -> Result<(), futures::task::SpawnError> { + self.0.spawn(obj); + Ok(()) + } +} diff --git a/tests/integration/cli/tests/publish.rs b/tests/integration/cli/tests/publish.rs index b34ceae1974..003cd6ff743 100644 --- a/tests/integration/cli/tests/publish.rs +++ b/tests/integration/cli/tests/publish.rs @@ -44,7 +44,7 @@ fn wasmer_publish() { } cmd.assert().success().stdout(format!( - "Successfully published package `{username}/largewasmfile@{random1}.{random2}.{random3}`\n" + "🚀 Successfully published package `{username}/largewasmfile@{random1}.{random2}.{random3}`\n" )); } @@ -116,7 +116,7 @@ fn wasmer_init_publish() { let assert = cmd.assert(); assert.success().stdout(format!( - "Successfully published package `{username}/randomversion@{random1}.{random2}.{random3}`\n" + "🚀 Successfully published package `{username}/randomversion@{random1}.{random2}.{random3}`\n" )); } @@ -166,9 +166,9 @@ fn wasmer_publish_and_run() { cmd.arg("--token").arg(token); } - cmd.assert() - .success() - .stdout(format!("Successfully published package `{package_name}`\n")); + cmd.assert().success().stdout(format!( + "🚀 Successfully published package `{package_name}`\n" + )); let assert = std::process::Command::new(get_wasmer_path()) .arg("run")