chore: split app to commons and web (squash commit)

This commit is contained in:
Andrew Bastin
2022-12-02 02:57:46 -05:00
parent fb827e3586
commit 3d004f2322
535 changed files with 1487 additions and 501 deletions

View File

@@ -0,0 +1,388 @@
import { ref } from "vue"
import {
createClient,
TypedDocumentNode,
dedupExchange,
OperationContext,
fetchExchange,
makeOperation,
createRequest,
subscriptionExchange,
errorExchange,
CombinedError,
Operation,
OperationResult,
} from "@urql/core"
import { authExchange } from "@urql/exchange-auth"
import { devtoolsExchange } from "@urql/devtools"
import { SubscriptionClient } from "subscriptions-transport-ws"
import * as E from "fp-ts/Either"
import * as TE from "fp-ts/TaskEither"
import { pipe, constVoid, flow } from "fp-ts/function"
import { subscribe, pipe as wonkaPipe } from "wonka"
import { filter, map, Subject } from "rxjs"
import {
authIdToken$,
getAuthIDToken,
probableUser$,
waitProbableLoginToConfirm,
} from "~/helpers/fb/auth"
// TODO: Implement caching
const BACKEND_GQL_URL =
import.meta.env.VITE_BACKEND_GQL_URL ?? "https://api.hoppscotch.io/graphql"
const BACKEND_WS_URL =
import.meta.env.VITE_BACKEND_WS_URL ?? "wss://api.hoppscotch.io/graphql"
type GQLOpType = "query" | "mutation" | "subscription"
/**
* A type that defines error events that are possible during backend operations on the GQLCLient
*/
export type GQLClientErrorEvent =
| { type: "SUBSCRIPTION_CONN_CALLBACK_ERR_REPORT"; errors: Error[] }
| { type: "CLIENT_REPORTED_ERROR"; error: CombinedError; op: Operation }
| {
type: "GQL_CLIENT_REPORTED_ERROR"
opType: GQLOpType
opResult: OperationResult
}
/**
* A stream of the errors that occur during GQLClient operations.
* Exposed to be subscribed to by systems like sentry for error reporting
*/
export const gqlClientError$ = new Subject<GQLClientErrorEvent>()
const createSubscriptionClient = () => {
return new SubscriptionClient(BACKEND_WS_URL, {
reconnect: true,
connectionParams: () => {
return {
authorization: `Bearer ${authIdToken$.value}`,
}
},
connectionCallback(error) {
if (error?.length > 0) {
gqlClientError$.next({
type: "SUBSCRIPTION_CONN_CALLBACK_ERR_REPORT",
errors: error,
})
}
},
})
}
const createHoppClient = () => {
const exchanges = [
devtoolsExchange,
dedupExchange,
authExchange({
addAuthToOperation({ authState, operation }) {
if (!authState || !authState.authToken) {
return operation
}
const fetchOptions =
typeof operation.context.fetchOptions === "function"
? operation.context.fetchOptions()
: operation.context.fetchOptions || {}
return makeOperation(operation.kind, operation, {
...operation.context,
fetchOptions: {
...fetchOptions,
headers: {
...fetchOptions.headers,
Authorization: `Bearer ${authState.authToken}`,
},
},
})
},
willAuthError({ authState }) {
return !authState || !authState.authToken
},
getAuth: async () => {
if (!probableUser$.value) return { authToken: null }
await waitProbableLoginToConfirm()
return {
authToken: getAuthIDToken(),
}
},
}),
fetchExchange,
errorExchange({
onError(error, op) {
gqlClientError$.next({
type: "CLIENT_REPORTED_ERROR",
error,
op,
})
},
}),
]
if (subscriptionClient) {
exchanges.push(
subscriptionExchange({
forwardSubscription: (operation) => {
return subscriptionClient!.request(operation)
},
})
)
}
return createClient({
url: BACKEND_GQL_URL,
exchanges,
})
}
let subscriptionClient: SubscriptionClient | null
export const client = ref(createHoppClient())
authIdToken$.subscribe((idToken) => {
// triggering reconnect by closing the websocket client
if (idToken && subscriptionClient) {
subscriptionClient?.client?.close()
}
// creating new subscription
if (idToken && !subscriptionClient) {
subscriptionClient = createSubscriptionClient()
}
// closing existing subscription client.
if (!idToken && subscriptionClient) {
subscriptionClient.close()
subscriptionClient = null
}
client.value = createHoppClient()
})
type RunQueryOptions<T = any, V = object> = {
query: TypedDocumentNode<T, V>
variables?: V
}
/**
* A wrapper type for defining errors possible in a GQL operation
*/
export type GQLError<T extends string> =
| {
type: "network_error"
error: Error
}
| {
type: "gql_error"
error: T
}
export const runGQLQuery = <DocType, DocVarType, DocErrorType extends string>(
args: RunQueryOptions<DocType, DocVarType>
): Promise<E.Either<GQLError<DocErrorType>, DocType>> => {
const request = createRequest<DocType, DocVarType>(args.query, args.variables)
const source = client.value.executeQuery(request, {
requestPolicy: "network-only",
})
return new Promise((resolve) => {
const sub = wonkaPipe(
source,
subscribe((res) => {
if (sub) {
sub.unsubscribe()
}
pipe(
// The target
res.data as DocType | undefined,
// Define what happens if data does not exist (it is an error)
E.fromNullable(
pipe(
// Take the network error value
res.error?.networkError,
// If it null, set the left to the generic error name
E.fromNullable(res.error?.message),
E.match(
// The left case (network error was null)
(gqlErr) => {
if (res.error) {
gqlClientError$.next({
type: "GQL_CLIENT_REPORTED_ERROR",
opType: "query",
opResult: res,
})
}
return <GQLError<DocErrorType>>{
type: "gql_error",
error: parseGQLErrorString(gqlErr ?? "") as DocErrorType,
}
},
// The right case (it was a GraphQL Error)
(networkErr) =>
<GQLError<DocErrorType>>{
type: "network_error",
error: networkErr,
}
)
)
),
resolve
)
})
)
})
}
// TODO: The subscription system seems to be firing multiple updates for certain subscriptions.
// Make sure to handle cases if the subscription fires with the same update multiple times
export const runGQLSubscription = <
DocType,
DocVarType,
DocErrorType extends string
>(
args: RunQueryOptions<DocType, DocVarType>
) => {
const result$ = new Subject<E.Either<GQLError<DocErrorType>, DocType>>()
const source = client.value.executeSubscription(
createRequest(args.query, args.variables)
)
const sub = wonkaPipe(
source,
subscribe((res) => {
result$.next(
pipe(
// The target
res.data as DocType | undefined,
// Define what happens if data does not exist (it is an error)
E.fromNullable(
pipe(
// Take the network error value
res.error?.networkError,
// If it null, set the left to the generic error name
E.fromNullable(res.error?.message),
E.match(
// The left case (network error was null)
(gqlErr) => {
if (res.error) {
gqlClientError$.next({
type: "GQL_CLIENT_REPORTED_ERROR",
opType: "subscription",
opResult: res,
})
}
return <GQLError<DocErrorType>>{
type: "gql_error",
error: parseGQLErrorString(gqlErr ?? "") as DocErrorType,
}
},
// The right case (it was a GraphQL Error)
(networkErr) =>
<GQLError<DocErrorType>>{
type: "network_error",
error: networkErr,
}
)
)
)
)
)
})
)
// Returns the stream and a subscription handle to unsub
return [result$, sub] as const
}
/**
* Same as `runGQLSubscription` but stops the subscription silently
* if there is an authentication error because of logged out
*/
export const runAuthOnlyGQLSubscription = flow(
runGQLSubscription,
([result$, sub]) => {
const updatedResult$ = result$.pipe(
map((res) => {
if (
E.isLeft(res) &&
res.left.type === "gql_error" &&
res.left.error === "auth/fail"
) {
sub.unsubscribe()
return null
} else return res
}),
filter((res): res is Exclude<typeof res, null> => res !== null)
)
return [updatedResult$, sub] as const
}
)
export const parseGQLErrorString = (s: string) =>
s.startsWith("[GraphQL] ") ? s.split("[GraphQL] ")[1] : s
export const runMutation = <
DocType,
DocVariables extends object | undefined,
DocErrors extends string
>(
mutation: TypedDocumentNode<DocType, DocVariables>,
variables?: DocVariables,
additionalConfig?: Partial<OperationContext>
): TE.TaskEither<GQLError<DocErrors>, DocType> =>
pipe(
TE.tryCatch(
() =>
client.value
.mutation(mutation, variables, {
requestPolicy: "cache-and-network",
...additionalConfig,
})
.toPromise(),
() => constVoid() as never // The mutation function can never fail, so this will never be called ;)
),
TE.chainEitherK((result) =>
pipe(
result.data,
E.fromNullable(
// Result is null
pipe(
result.error?.networkError,
E.fromNullable(result.error?.message),
E.match(
// The left case (network error was null)
(gqlErr) => {
if (result.error) {
gqlClientError$.next({
type: "GQL_CLIENT_REPORTED_ERROR",
opType: "mutation",
opResult: result,
})
}
return <GQLError<DocErrors>>{
type: "gql_error",
error: parseGQLErrorString(gqlErr ?? ""),
}
},
// The right case (it was a network error)
(networkErr) =>
<GQLError<DocErrors>>{
type: "network_error",
error: networkErr,
}
)
)
)
)
)
)

View File

@@ -0,0 +1,3 @@
export type UserQueryError = "user/not_found"
export type MyTeamsQueryError = "ea/not_invite_or_admin"

View File

@@ -0,0 +1,12 @@
mutation AcceptTeamInvitation($inviteID: ID!) {
acceptTeamInvitation(inviteID: $inviteID) {
membershipID
role
user {
uid
displayName
photoURL
email
}
}
}

View File

@@ -0,0 +1,11 @@
mutation CreateChildCollection(
$childTitle: String!
$collectionID: ID!
) {
createChildCollection(
childTitle: $childTitle
collectionID: $collectionID
) {
id
}
}

View File

@@ -0,0 +1,8 @@
mutation CreateDuplicateEnvironment($id: ID!){
createDuplicateEnvironment (id: $id ){
id
teamID
name
variables
}
}

View File

@@ -0,0 +1,5 @@
mutation CreateNewRootCollection($title: String!, $teamID: ID!) {
createRootCollection(title: $title, teamID: $teamID) {
id
}
}

View File

@@ -0,0 +1,12 @@
mutation CreateRequestInCollection($data: CreateTeamRequestInput!, $collectionID: ID!) {
createRequestInCollection(data: $data, collectionID: $collectionID) {
id
collection {
id
team {
id
name
}
}
}
}

View File

@@ -0,0 +1,6 @@
mutation CreateShortcode($request: String!) {
createShortcode(request: $request) {
id
request
}
}

View File

@@ -0,0 +1,20 @@
mutation CreateTeam($name: String!) {
createTeam(name: $name) {
id
name
members {
membershipID
role
user {
uid
displayName
email
photoURL
}
}
myRole
ownersCount
editorsCount
viewersCount
}
}

View File

@@ -0,0 +1,7 @@
mutation CreateTeamEnvironment($variables: String!,$teamID: ID!,$name: String!){
createTeamEnvironment( variables: $variables ,teamID: $teamID ,name: $name){
variables
name
teamID
}
}

View File

@@ -0,0 +1,9 @@
mutation CreateTeamInvitation($inviteeEmail: String!, $inviteeRole: TeamMemberRole!, $teamID: ID!) {
createTeamInvitation(inviteeRole: $inviteeRole, inviteeEmail: $inviteeEmail, teamID: $teamID) {
id
teamID
creatorUid
inviteeEmail
inviteeRole
}
}

View File

@@ -0,0 +1,3 @@
mutation DeleteCollection($collectionID: ID!) {
deleteCollection(collectionID: $collectionID)
}

View File

@@ -0,0 +1,3 @@
mutation DeleteRequest($requestID: ID!) {
deleteRequest(requestID: $requestID)
}

View File

@@ -0,0 +1,3 @@
mutation DeleteShortcode($code: ID!) {
revokeShortcode(code: $code)
}

View File

@@ -0,0 +1,3 @@
mutation DeleteTeam($teamID: ID!) {
deleteTeam(teamID: $teamID)
}

View File

@@ -0,0 +1,3 @@
mutation DeleteTeamEnvironment($id: ID!){
deleteTeamEnvironment (id: $id )
}

View File

@@ -0,0 +1,3 @@
mutation importFromJSON($jsonString: String!, $teamID: ID!) {
importCollectionsFromJSON(jsonString: $jsonString, teamID: $teamID)
}

View File

@@ -0,0 +1,3 @@
mutation LeaveTeam($teamID: ID!) {
leaveTeam(teamID: $teamID)
}

View File

@@ -0,0 +1,5 @@
mutation MoveRESTTeamRequest($requestID: ID!, $collectionID: ID!) {
moveRequest(requestID: $requestID, destCollID: $collectionID) {
id
}
}

View File

@@ -0,0 +1,3 @@
mutation RemoveTeamMember($userUid: ID!, $teamID: ID!) {
removeTeamMember(userUid: $userUid, teamID: $teamID)
}

View File

@@ -0,0 +1,5 @@
mutation RenameCollection($newTitle: String!, $collectionID: ID!) {
renameCollection(newTitle: $newTitle, collectionID: $collectionID) {
id
}
}

View File

@@ -0,0 +1,13 @@
mutation RenameTeam($newName: String!, $teamID: ID!) {
renameTeam(newName: $newName, teamID: $teamID) {
id
name
teamMembers {
membershipID
user {
uid
}
role
}
}
}

View File

@@ -0,0 +1,3 @@
mutation RevokeTeamInvitation($inviteID: ID!) {
revokeTeamInvitation(inviteID: $inviteID)
}

View File

@@ -0,0 +1,6 @@
mutation UpdateRequest($data: UpdateTeamRequestInput!, $requestID: ID!) {
updateRequest(data: $data, requestID: $requestID) {
id
title
}
}

View File

@@ -0,0 +1,7 @@
mutation UpdateTeamEnvironment($variables: String!,$id: ID!,$name: String!){
updateTeamEnvironment( variables: $variables ,id: $id ,name: $name){
variables
name
id
}
}

View File

@@ -0,0 +1,14 @@
mutation UpdateTeamMemberRole(
$newRole: TeamMemberRole!,
$userUid: ID!,
$teamID: ID!
) {
updateTeamMemberRole(
newRole: $newRole
userUid: $userUid
teamID: $teamID
) {
membershipID
role
}
}

View File

@@ -0,0 +1,3 @@
query ExportAsJSON($teamID: ID!) {
exportCollectionsToJSON(teamID: $teamID)
}

View File

@@ -0,0 +1,8 @@
query GetCollectionChildren($collectionID: ID!, $cursor: String) {
collection(collectionID: $collectionID) {
children(cursor: $cursor) {
id
title
}
}
}

View File

@@ -0,0 +1,7 @@
query GetCollectionChildrenIDs($collectionID: ID!, $cursor: String) {
collection(collectionID: $collectionID) {
children(cursor: $cursor) {
id
}
}
}

View File

@@ -0,0 +1,7 @@
query GetCollectionRequests($collectionID: ID!, $cursor: ID) {
requestsInCollection(collectionID: $collectionID, cursor: $cursor) {
id
title
request
}
}

View File

@@ -0,0 +1,5 @@
query GetCollectionTitle($collectionID: ID!) {
collection(collectionID: $collectionID) {
title
}
}

View File

@@ -0,0 +1,16 @@
query GetInviteDetails($inviteID: ID!) {
teamInvitation(inviteID: $inviteID) {
id
inviteeEmail
inviteeRole
team {
id
name
}
creator {
uid
displayName
email
}
}
}

View File

@@ -0,0 +1,7 @@
query GetUserShortcodes($cursor: ID) {
myShortcodes(cursor: $cursor) {
id
request
createdOn
}
}

View File

@@ -0,0 +1,18 @@
query GetMyTeams($cursor: ID) {
myTeams(cursor: $cursor) {
id
name
myRole
ownersCount
teamMembers {
membershipID
user {
photoURL
displayName
email
uid
}
role
}
}
}

View File

@@ -0,0 +1,14 @@
query GetTeam($teamID: ID!) {
team(teamID: $teamID) {
id
name
teamMembers {
membershipID
user {
uid
email
}
role
}
}
}

View File

@@ -0,0 +1,10 @@
query GetTeamEnvironments($teamID: ID!){
team(teamID: $teamID){
teamEnvironments{
id
name
variables
teamID
}
}
}

View File

@@ -0,0 +1,12 @@
query GetTeamMembers($teamID: ID!, $cursor: ID) {
team(teamID: $teamID) {
members(cursor: $cursor) {
membershipID
user {
uid
email
}
role
}
}
}

View File

@@ -0,0 +1,8 @@
query GetUserInfo {
me {
uid
displayName
email
photoURL
}
}

View File

@@ -0,0 +1,7 @@
query Me {
me {
uid
displayName
photoURL
}
}

View File

@@ -0,0 +1,6 @@
query ResolveShortcode($code: ID!) {
shortcode(code: $code) {
id
request
}
}

View File

@@ -0,0 +1,6 @@
query RootCollectionsOfTeam($teamID: ID!, $cursor: ID) {
rootCollectionsOfTeam(teamID: $teamID, cursor: $cursor) {
id
title
}
}

View File

@@ -0,0 +1,10 @@
query GetPendingInvites($teamID: ID!) {
team(teamID: $teamID) {
id
teamInvitations {
inviteeRole
inviteeEmail
id
}
}
}

View File

@@ -0,0 +1,7 @@
subscription ShortcodeCreated {
myShortcodesCreated {
id
request
createdOn
}
}

View File

@@ -0,0 +1,5 @@
subscription ShortcodeDeleted {
myShortcodesRevoked {
id
}
}

View File

@@ -0,0 +1,9 @@
subscription TeamCollectionAdded($teamID: ID!) {
teamCollectionAdded(teamID: $teamID) {
id
title
parent {
id
}
}
}

View File

@@ -0,0 +1,3 @@
subscription TeamCollectionRemoved($teamID: ID!) {
teamCollectionRemoved(teamID: $teamID)
}

View File

@@ -0,0 +1,9 @@
subscription TeamCollectionUpdated($teamID: ID!) {
teamCollectionUpdated(teamID: $teamID) {
id
title
parent {
id
}
}
}

View File

@@ -0,0 +1,8 @@
subscription TeamEnvironmentCreated ($teamID: ID!) {
teamEnvironmentCreated(teamID: $teamID) {
id
teamID
name
variables
}
}

View File

@@ -0,0 +1,5 @@
subscription TeamEnvironmentDeleted ($teamID: ID!) {
teamEnvironmentDeleted(teamID: $teamID) {
id
}
}

View File

@@ -0,0 +1,8 @@
subscription TeamEnvironmentUpdated ($teamID: ID!) {
teamEnvironmentUpdated(teamID: $teamID) {
id
teamID
name
variables
}
}

View File

@@ -0,0 +1,5 @@
subscription TeamInvitationAdded($teamID: ID!) {
teamInvitationAdded(teamID: $teamID) {
id
}
}

View File

@@ -0,0 +1,3 @@
subscription TeamInvitationRemoved($teamID: ID!) {
teamInvitationRemoved(teamID: $teamID)
}

View File

@@ -0,0 +1,10 @@
subscription TeamMemberAdded($teamID: ID!) {
teamMemberAdded(teamID: $teamID) {
membershipID
user {
uid
email
}
role
}
}

View File

@@ -0,0 +1,3 @@
subscription TeamMemberRemoved($teamID: ID!) {
teamMemberRemoved(teamID: $teamID)
}

View File

@@ -0,0 +1,10 @@
subscription TeamMemberUpdated($teamID: ID!) {
teamMemberUpdated(teamID: $teamID) {
membershipID
user {
uid
email
}
role
}
}

View File

@@ -0,0 +1,8 @@
subscription TeamRequestAdded($teamID: ID!) {
teamRequestAdded(teamID: $teamID) {
id
collectionID
request
title
}
}

View File

@@ -0,0 +1,3 @@
subscription TeamRequestDeleted($teamID: ID!) {
teamRequestDeleted(teamID: $teamID)
}

View File

@@ -0,0 +1,8 @@
subscription TeamRequestUpdated($teamID: ID!) {
teamRequestUpdated(teamID: $teamID) {
id
collectionID
request
title
}
}

View File

@@ -0,0 +1,127 @@
import * as A from "fp-ts/Array"
import * as E from "fp-ts/Either"
import * as TE from "fp-ts/TaskEither"
import { pipe, flow } from "fp-ts/function"
import {
HoppCollection,
HoppRESTRequest,
makeCollection,
translateToNewRequest,
} from "@hoppscotch/data"
import { TeamCollection } from "../teams/TeamCollection"
import { TeamRequest } from "../teams/TeamRequest"
import { GQLError, runGQLQuery } from "./GQLClient"
import {
GetCollectionChildrenIDsDocument,
GetCollectionRequestsDocument,
GetCollectionTitleDocument,
} from "./graphql"
export const BACKEND_PAGE_SIZE = 10
const getCollectionChildrenIDs = async (collID: string) => {
const collsList: string[] = []
while (true) {
const data = await runGQLQuery({
query: GetCollectionChildrenIDsDocument,
variables: {
collectionID: collID,
cursor:
collsList.length > 0 ? collsList[collsList.length - 1] : undefined,
},
})
if (E.isLeft(data)) {
return E.left(data.left)
}
collsList.push(...data.right.collection!.children.map((x) => x.id))
if (data.right.collection!.children.length !== BACKEND_PAGE_SIZE) break
}
return E.right(collsList)
}
const getCollectionRequests = async (collID: string) => {
const reqList: TeamRequest[] = []
while (true) {
const data = await runGQLQuery({
query: GetCollectionRequestsDocument,
variables: {
collectionID: collID,
cursor: reqList.length > 0 ? reqList[reqList.length - 1].id : undefined,
},
})
if (E.isLeft(data)) {
return E.left(data.left)
}
reqList.push(
...data.right.requestsInCollection.map(
(x) =>
<TeamRequest>{
id: x.id,
request: translateToNewRequest(JSON.parse(x.request)),
collectionID: collID,
title: x.title,
}
)
)
if (data.right.requestsInCollection.length !== BACKEND_PAGE_SIZE) break
}
return E.right(reqList)
}
export const getCompleteCollectionTree = (
collID: string
): TE.TaskEither<GQLError<string>, TeamCollection> =>
pipe(
TE.Do,
TE.bind("title", () =>
pipe(
() =>
runGQLQuery({
query: GetCollectionTitleDocument,
variables: {
collectionID: collID,
},
}),
TE.map((x) => x.collection!.title)
)
),
TE.bind("children", () =>
pipe(
// TaskEither -> () => Promise<Either>
() => getCollectionChildrenIDs(collID),
TE.chain(flow(A.map(getCompleteCollectionTree), TE.sequenceArray))
)
),
TE.bind("requests", () => () => getCollectionRequests(collID)),
TE.map(
({ title, children, requests }) =>
<TeamCollection>{
id: collID,
children,
requests,
title,
}
)
)
export const teamCollToHoppRESTColl = (
coll: TeamCollection
): HoppCollection<HoppRESTRequest> =>
makeCollection({
name: coll.title,
folders: coll.children?.map(teamCollToHoppRESTColl) ?? [],
requests: coll.requests?.map((x) => x.request) ?? [],
})

View File

@@ -0,0 +1,29 @@
import { HoppRESTRequest } from "@hoppscotch/data"
import { runMutation } from "../GQLClient"
import {
CreateShortcodeDocument,
CreateShortcodeMutation,
CreateShortcodeMutationVariables,
DeleteShortcodeDocument,
DeleteShortcodeMutation,
DeleteShortcodeMutationVariables,
} from "../graphql"
type DeleteShortcodeErrors = "shortcode/not_found"
export const createShortcode = (request: HoppRESTRequest) =>
runMutation<CreateShortcodeMutation, CreateShortcodeMutationVariables, "">(
CreateShortcodeDocument,
{
request: JSON.stringify(request),
}
)
export const deleteShortcode = (code: string) =>
runMutation<
DeleteShortcodeMutation,
DeleteShortcodeMutationVariables,
DeleteShortcodeErrors
>(DeleteShortcodeDocument, {
code,
})

View File

@@ -0,0 +1,132 @@
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { runMutation } from "../GQLClient"
import { TeamName } from "../types/TeamName"
import {
CreateTeamDocument,
CreateTeamMutation,
CreateTeamMutationVariables,
DeleteTeamDocument,
DeleteTeamMutation,
DeleteTeamMutationVariables,
LeaveTeamDocument,
LeaveTeamMutation,
LeaveTeamMutationVariables,
RemoveTeamMemberDocument,
RemoveTeamMemberMutation,
RemoveTeamMemberMutationVariables,
RenameTeamDocument,
RenameTeamMutation,
RenameTeamMutationVariables,
TeamMemberRole,
UpdateTeamMemberRoleDocument,
UpdateTeamMemberRoleMutation,
UpdateTeamMemberRoleMutationVariables,
} from "../graphql"
type DeleteTeamErrors =
| "team/not_required_role"
| "team/invalid_id"
| "team/member_not_found"
| "ea/not_invite_or_admin"
type LeaveTeamErrors =
| "team/invalid_id"
| "team/member_not_found"
| "ea/not_invite_or_admin"
type CreateTeamErrors = "team/name_invalid" | "ea/not_invite_or_admin"
type RenameTeamErrors =
| "ea/not_invite_or_admin"
| "team/invalid_id"
| "team/not_required_role"
type UpdateTeamMemberRoleErrors =
| "ea/not_invite_or_admin"
| "team/invalid_id"
| "team/not_required_role"
type RemoveTeamMemberErrors =
| "ea/not_invite_or_admin"
| "team/invalid_id"
| "team/not_required_role"
export const createTeam = (name: TeamName) =>
pipe(
runMutation<
CreateTeamMutation,
CreateTeamMutationVariables,
CreateTeamErrors
>(CreateTeamDocument, {
name,
}),
TE.map(({ createTeam }) => createTeam)
)
export const deleteTeam = (teamID: string) =>
runMutation<
DeleteTeamMutation,
DeleteTeamMutationVariables,
DeleteTeamErrors
>(
DeleteTeamDocument,
{
teamID,
},
{
additionalTypenames: ["Team"],
}
)
export const leaveTeam = (teamID: string) =>
runMutation<LeaveTeamMutation, LeaveTeamMutationVariables, LeaveTeamErrors>(
LeaveTeamDocument,
{
teamID,
},
{
additionalTypenames: ["Team"],
}
)
export const renameTeam = (teamID: string, newName: TeamName) =>
pipe(
runMutation<
RenameTeamMutation,
RenameTeamMutationVariables,
RenameTeamErrors
>(RenameTeamDocument, {
newName,
teamID,
}),
TE.map(({ renameTeam }) => renameTeam)
)
export const updateTeamMemberRole = (
userUid: string,
teamID: string,
newRole: TeamMemberRole
) =>
pipe(
runMutation<
UpdateTeamMemberRoleMutation,
UpdateTeamMemberRoleMutationVariables,
UpdateTeamMemberRoleErrors
>(UpdateTeamMemberRoleDocument, {
newRole,
userUid,
teamID,
}),
TE.map(({ updateTeamMemberRole }) => updateTeamMemberRole)
)
export const removeTeamMember = (userUid: string, teamID: string) =>
runMutation<
RemoveTeamMemberMutation,
RemoveTeamMemberMutationVariables,
RemoveTeamMemberErrors
>(RemoveTeamMemberDocument, {
userUid,
teamID,
})

View File

@@ -0,0 +1,69 @@
import { runMutation } from "../GQLClient"
import {
CreateDuplicateEnvironmentDocument,
CreateDuplicateEnvironmentMutation,
CreateDuplicateEnvironmentMutationVariables,
CreateTeamEnvironmentDocument,
CreateTeamEnvironmentMutation,
CreateTeamEnvironmentMutationVariables,
DeleteTeamEnvironmentDocument,
DeleteTeamEnvironmentMutation,
DeleteTeamEnvironmentMutationVariables,
UpdateTeamEnvironmentDocument,
UpdateTeamEnvironmentMutation,
UpdateTeamEnvironmentMutationVariables,
} from "../graphql"
type DeleteTeamEnvironmentError = "team_environment/not_found"
type UpdateTeamEnvironmentError = "team_environment/not_found"
type DuplicateTeamEnvironmentError = "team_environment/not_found"
export const createTeamEnvironment = (
variables: string,
teamID: string,
name: string
) =>
runMutation<
CreateTeamEnvironmentMutation,
CreateTeamEnvironmentMutationVariables,
""
>(CreateTeamEnvironmentDocument, {
variables,
teamID,
name,
})
export const deleteTeamEnvironment = (id: string) =>
runMutation<
DeleteTeamEnvironmentMutation,
DeleteTeamEnvironmentMutationVariables,
DeleteTeamEnvironmentError
>(DeleteTeamEnvironmentDocument, {
id,
})
export const updateTeamEnvironment = (
variables: string,
id: string,
name: string
) =>
runMutation<
UpdateTeamEnvironmentMutation,
UpdateTeamEnvironmentMutationVariables,
UpdateTeamEnvironmentError
>(UpdateTeamEnvironmentDocument, {
variables,
id,
name,
})
export const createDuplicateEnvironment = (id: string) =>
runMutation<
CreateDuplicateEnvironmentMutation,
CreateDuplicateEnvironmentMutationVariables,
DuplicateTeamEnvironmentError
>(CreateDuplicateEnvironmentDocument, {
id,
})

View File

@@ -0,0 +1,68 @@
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { runMutation } from "../GQLClient"
import {
AcceptTeamInvitationDocument,
AcceptTeamInvitationMutation,
AcceptTeamInvitationMutationVariables,
CreateTeamInvitationDocument,
CreateTeamInvitationMutation,
CreateTeamInvitationMutationVariables,
RevokeTeamInvitationDocument,
RevokeTeamInvitationMutation,
RevokeTeamInvitationMutationVariables,
TeamMemberRole,
} from "../graphql"
import { Email } from "../types/Email"
export type CreateTeamInvitationErrors =
| "invalid/email"
| "team/invalid_id"
| "team/member_not_found"
| "team_invite/already_member"
| "team_invite/member_has_invite"
type RevokeTeamInvitationErrors =
| "team/not_required_role"
| "team_invite/no_invite_found"
type AcceptTeamInvitationErrors =
| "team_invite/no_invite_found"
| "team_invite/already_member"
| "team_invite/email_do_not_match"
export const createTeamInvitation = (
inviteeEmail: Email,
inviteeRole: TeamMemberRole,
teamID: string
) =>
pipe(
runMutation<
CreateTeamInvitationMutation,
CreateTeamInvitationMutationVariables,
CreateTeamInvitationErrors
>(CreateTeamInvitationDocument, {
inviteeEmail,
inviteeRole,
teamID,
}),
TE.map((x) => x.createTeamInvitation)
)
export const revokeTeamInvitation = (inviteID: string) =>
runMutation<
RevokeTeamInvitationMutation,
RevokeTeamInvitationMutationVariables,
RevokeTeamInvitationErrors
>(RevokeTeamInvitationDocument, {
inviteID,
})
export const acceptTeamInvitation = (inviteID: string) =>
runMutation<
AcceptTeamInvitationMutation,
AcceptTeamInvitationMutationVariables,
AcceptTeamInvitationErrors
>(AcceptTeamInvitationDocument, {
inviteID,
})

View File

@@ -0,0 +1,20 @@
import { runMutation } from "../GQLClient"
import {
MoveRestTeamRequestDocument,
MoveRestTeamRequestMutation,
MoveRestTeamRequestMutationVariables,
} from "../graphql"
type MoveRestTeamRequestErrors =
| "team_req/not_found"
| "team_req/invalid_target_id"
export const moveRESTTeamRequest = (requestID: string, collectionID: string) =>
runMutation<
MoveRestTeamRequestMutation,
MoveRestTeamRequestMutationVariables,
MoveRestTeamRequestErrors
>(MoveRestTeamRequestDocument, {
requestID,
collectionID,
})

View File

@@ -0,0 +1,16 @@
import * as t from "io-ts"
const emailRegex =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
interface EmailBrand {
readonly Email: unique symbol
}
export const EmailCodec = t.brand(
t.string,
(x): x is t.Branded<string, EmailBrand> => emailRegex.test(x),
"Email"
)
export type Email = t.TypeOf<typeof EmailCodec>

View File

@@ -0,0 +1,13 @@
import * as t from "io-ts"
interface TeamNameBrand {
readonly TeamName: unique symbol
}
export const TeamNameCodec = t.brand(
t.string,
(x): x is t.Branded<string, TeamNameBrand> => x.trim().length >= 6,
"TeamName"
)
export type TeamName = t.TypeOf<typeof TeamNameCodec>