feat: implement environments for selfhosted (#30)

This commit is contained in:
Akash K
2023-03-08 16:47:29 +05:30
committed by GitHub
parent 40208a13e0
commit 80898407c3
23 changed files with 960 additions and 69 deletions

View File

@@ -25,3 +25,6 @@ dist-ssr
# Sitemap Generation Artifacts (see vite.config.ts)
.sitemap-gen
# Backend Code generation
src/api/generated

View File

@@ -0,0 +1,19 @@
overwrite: true
schema:
- ${VITE_BACKEND_GQL_URL}
generates:
src/api/generated/graphql.ts:
documents: "src/**/*.graphql"
plugins:
- add:
content: >
/* eslint-disable */
// Auto-generated file (DO NOT EDIT!!!), refer gql-codegen.yml
- typescript
- typescript-operations
- typed-document-node
- typescript-urql-graphcache
src/helpers/backend/backend-schema.json:
plugins:
- urql-introspection

View File

@@ -16,13 +16,16 @@
"do-build-prod": "pnpm run build",
"do-lint": "pnpm run prod-lint",
"do-typecheck": "pnpm run lint",
"do-lintfix": "pnpm run lintfix"
"do-lintfix": "pnpm run lintfix",
"gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml dotenv_config_path=\"../../.env\""
},
"dependencies": {
"@hoppscotch/common": "workspace:^",
"axios": "^0.21.4",
"buffer": "^6.0.3",
"firebase": "^9.8.4",
"fp-ts": "^2.13.1",
"graphql": "^15.8.0",
"process": "^0.11.10",
"rxjs": "^7.5.5",
"stream-browserify": "^3.0.0",
@@ -31,6 +34,14 @@
"workbox-window": "^6.5.4"
},
"devDependencies": {
"@graphql-codegen/add": "^3.2.0",
"@graphql-codegen/cli": "^2.8.0",
"@graphql-codegen/typed-document-node": "^2.3.1",
"@graphql-codegen/typescript": "^2.7.1",
"@graphql-codegen/typescript-operations": "^2.5.1",
"@graphql-codegen/typescript-urql-graphcache": "^2.3.1",
"@graphql-codegen/urql-introspection": "^2.2.0",
"@graphql-typed-document-node/core": "^3.1.1",
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
"@rushstack/eslint-patch": "^1.1.4",
"@typescript-eslint/eslint-plugin": "^5.19.0",

View File

@@ -0,0 +1,5 @@
mutation ClearGlobalEnvironments($id: ID!) {
clearGlobalEnvironments(id: $id) {
id
}
}

View File

@@ -0,0 +1,9 @@
mutation CreateUserEnvironment($name: String!, $variables: String!) {
createUserEnvironment(name: $name, variables: $variables) {
id
userUid
name
variables
isGlobal
}
}

View File

@@ -0,0 +1,5 @@
mutation CreateUserGlobalEnvironment($variables: String!) {
createUserGlobalEnvironment(variables: $variables) {
id
}
}

View File

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

View File

@@ -0,0 +1,9 @@
mutation UpdateUserEnvironment($id: ID!, $name: String!, $variables: String!) {
updateUserEnvironment(id: $id, name: $name, variables: $variables) {
id
userUid
name
variables
isGlobal
}
}

View File

@@ -0,0 +1,9 @@
mutation CreateUserEnvironment($name: String!, $variables: String!) {
createUserEnvironment(name: $name, variables: $variables) {
id
userUid
name
variables
isGlobal
}
}

View File

@@ -0,0 +1,11 @@
query GetGlobalEnvironments {
me {
globalEnvironments {
id
isGlobal
name
userUid
variables
}
}
}

View File

@@ -0,0 +1,11 @@
query GetUserEnvironments {
me {
environments {
id
isGlobal
name
userUid
variables
}
}
}

View File

@@ -0,0 +1,9 @@
subscription UserEnvironmentCreated {
userEnvironmentCreated {
id
isGlobal
name
userUid
variables
}
}

View File

@@ -0,0 +1,5 @@
subscription UserEnvironmentDeleted {
userEnvironmentDeleted {
id
}
}

View File

@@ -0,0 +1,9 @@
subscription UserEnvironmentUpdated {
userEnvironmentUpdated {
id
userUid
name
variables
isGlobal
}
}

View File

@@ -0,0 +1,101 @@
import { Observable } from "rxjs"
import DispatchingStore from "@hoppscotch/common/newstore/DispatchingStore"
export type DispatchersOf<T extends DispatchingStore<any, any>> =
T extends DispatchingStore<any, infer U>
? U extends Record<infer D, any>
? D
: never
: never
export type StoreSyncDefinitionOf<T extends DispatchingStore<any, any>> = {
[x in DispatchersOf<T>]?: T extends DispatchingStore<any, infer U>
? U extends Record<x, any>
? U[x] extends (x: any, y: infer Y) => any
? (payload: Y) => void
: never
: never
: never
}
let _isRunningDispatchWithoutSyncing = true
export function runDispatchWithOutSyncing(func: () => void) {
_isRunningDispatchWithoutSyncing = false
func()
_isRunningDispatchWithoutSyncing = true
}
export const getSyncInitFunction = <T extends DispatchingStore<any, any>>(
store: T,
storeSyncDefinition: StoreSyncDefinitionOf<T>,
shouldSyncValue: () => boolean,
shouldSyncObservable: Observable<boolean>
) => {
let startSubscriptions: () => () => void | undefined
let stopSubscriptions: () => void | undefined
let oldSyncStatus = shouldSyncValue()
// Start and stop the subscriptions according to the sync settings from profile
shouldSyncObservable.subscribe((newSyncStatus) => {
if (oldSyncStatus === true && newSyncStatus === false) {
stopListeningToSubscriptions()
} else if (oldSyncStatus === false && newSyncStatus === true) {
startListeningToSubscriptions()
}
oldSyncStatus = newSyncStatus
})
function startStoreSync() {
store.dispatches$.subscribe((actionParams) => {
// typescript cannot understand that the dispatcher can be the index, so casting to any
if ((storeSyncDefinition as any)[actionParams.dispatcher]) {
const dispatcher = actionParams.dispatcher
const payload = actionParams.payload
const operationMapperFunction = (storeSyncDefinition as any)[dispatcher]
if (
operationMapperFunction &&
_isRunningDispatchWithoutSyncing &&
shouldSyncValue()
) {
operationMapperFunction(payload)
}
}
})
}
function setupSubscriptions(func: () => () => void) {
startSubscriptions = func
}
function startListeningToSubscriptions() {
if (!startSubscriptions) {
console.warn(
"We don't have a function to start subscriptions. Please use `setupSubscriptions` to setup the start function."
)
}
startSubscriptions()
}
function stopListeningToSubscriptions() {
if (!stopSubscriptions) {
console.warn(
"We don't have a function to unsubscribe. make sure you return the unsubscribe function when using setupSubscriptions"
)
}
stopSubscriptions()
}
return {
startStoreSync,
setupSubscriptions,
startListeningToSubscriptions,
stopListeningToSubscriptions,
}
}

View File

@@ -0,0 +1,33 @@
export const createMapper = () => {
const indexBackendIDMap = new Map<number, string | undefined>()
const backendIdIndexMap = new Map<string, number | undefined>()
return {
addEntry(localIndex: number, backendId: string) {
indexBackendIDMap.set(localIndex, backendId)
backendIdIndexMap.set(backendId, localIndex)
},
getValue() {
return indexBackendIDMap
},
getBackendIdByIndex(localIndex: number) {
return indexBackendIDMap.get(localIndex)
},
getIndexByBackendId(backendId: string) {
return backendIdIndexMap.get(backendId)
},
removeEntry(backendId?: string, index?: number) {
if (backendId) {
const index = backendIdIndexMap.get(backendId)
backendIdIndexMap.delete(backendId)
index && indexBackendIDMap.delete(index)
} else if (index) {
const backendId = indexBackendIDMap.get(index)
indexBackendIDMap.delete(index)
backendId && backendIdIndexMap.delete(backendId)
}
},
}
}

View File

@@ -1,6 +1,10 @@
import { createHoppApp } from "@hoppscotch/common"
import { def as authDef } from "./platform/auth"
import { def as environmentsDef } from "./platform/environments/environments.platform"
createHoppApp("#app", {
auth: authDef,
sync: {
environments: environmentsDef,
},
})

View File

@@ -0,0 +1,112 @@
import {
runMutation,
runGQLQuery,
runGQLSubscription,
} from "@hoppscotch/common/helpers/backend/GQLClient"
import {
CreateUserEnvironmentDocument,
CreateUserEnvironmentMutation,
CreateUserEnvironmentMutationVariables,
UpdateUserEnvironmentMutation,
UpdateUserEnvironmentMutationVariables,
UpdateUserEnvironmentDocument,
DeleteUserEnvironmentMutation,
DeleteUserEnvironmentMutationVariables,
DeleteUserEnvironmentDocument,
ClearGlobalEnvironmentsMutation,
ClearGlobalEnvironmentsMutationVariables,
ClearGlobalEnvironmentsDocument,
CreateUserGlobalEnvironmentMutation,
CreateUserGlobalEnvironmentMutationVariables,
CreateUserGlobalEnvironmentDocument,
GetGlobalEnvironmentsDocument,
GetGlobalEnvironmentsQueryVariables,
GetGlobalEnvironmentsQuery,
GetUserEnvironmentsDocument,
UserEnvironmentCreatedDocument,
UserEnvironmentUpdatedDocument,
UserEnvironmentDeletedDocument,
} from "./../../api/generated/graphql"
import { Environment } from "@hoppscotch/data"
export const createUserEnvironment = (name: string, variables: string) =>
runMutation<
CreateUserEnvironmentMutation,
CreateUserEnvironmentMutationVariables,
""
>(CreateUserEnvironmentDocument, {
name,
variables,
})()
export const updateUserEnvironment = (
id: string,
{ name, variables }: Environment
) =>
runMutation<
UpdateUserEnvironmentMutation,
UpdateUserEnvironmentMutationVariables,
""
>(UpdateUserEnvironmentDocument, {
id,
name,
variables: JSON.stringify(variables),
})
export const deleteUserEnvironment = (id: string) =>
runMutation<
DeleteUserEnvironmentMutation,
DeleteUserEnvironmentMutationVariables,
""
>(DeleteUserEnvironmentDocument, {
id,
})
export const clearGlobalEnvironmentVariables = (id: string) =>
runMutation<
ClearGlobalEnvironmentsMutation,
ClearGlobalEnvironmentsMutationVariables,
""
>(ClearGlobalEnvironmentsDocument, {
id,
})()
export const getUserEnvironments = () =>
runGQLQuery({
query: GetUserEnvironmentsDocument,
})
export const getGlobalEnvironments = () =>
runGQLQuery<
GetGlobalEnvironmentsQuery,
GetGlobalEnvironmentsQueryVariables,
"user_environment/user_env_does_not_exists"
>({
query: GetGlobalEnvironmentsDocument,
})
export const createUserGlobalEnvironment = (variables: string) =>
runMutation<
CreateUserGlobalEnvironmentMutation,
CreateUserGlobalEnvironmentMutationVariables,
""
>(CreateUserGlobalEnvironmentDocument, {
variables,
})()
export const runUserEnvironmentCreatedSubscription = () =>
runGQLSubscription({
query: UserEnvironmentCreatedDocument,
})
export const runUserEnvironmentUpdatedSubscription = () =>
runGQLSubscription({
query: UserEnvironmentUpdatedDocument,
})
export const runUserEnvironmentDeletedSubscription = () =>
runGQLSubscription({
query: UserEnvironmentDeletedDocument,
})

View File

@@ -0,0 +1,219 @@
import { authEvents$, def as platformAuth } from "@platform/auth"
import {
createEnvironment,
deleteEnvironment,
replaceEnvironments,
setGlobalEnvVariables,
updateEnvironment,
} from "@hoppscotch/common/newstore/environments"
import { EnvironmentsPlatformDef } from "@hoppscotch/common/src/platform/environments"
import { runGQLSubscription } from "@hoppscotch/common/helpers/backend/GQLClient"
import {
environmentsMapper,
globalEnvironmentMapper,
environnmentsSyncer,
} from "@platform/environments/environments.sync"
import * as E from "fp-ts/Either"
import { runDispatchWithOutSyncing } from "@lib/sync"
import {
createUserGlobalEnvironment,
getGlobalEnvironments,
getUserEnvironments,
runUserEnvironmentCreatedSubscription,
runUserEnvironmentDeletedSubscription,
runUserEnvironmentUpdatedSubscription,
} from "@platform/environments/environments.api"
export function initEnvironmentsSync() {
const currentUser$ = platformAuth.getCurrentUserStream()
environnmentsSyncer.startStoreSync()
environnmentsSyncer.setupSubscriptions(setupSubscriptions)
currentUser$.subscribe(async (user) => {
if (user) {
await loadAllEnvironments()
}
})
authEvents$.subscribe((event) => {
if (event.event == "login" || event.event == "token_refresh") {
environnmentsSyncer.startListeningToSubscriptions()
}
if (event.event == "logout") {
environnmentsSyncer.stopListeningToSubscriptions()
}
})
}
export const def: EnvironmentsPlatformDef = {
initEnvironmentsSync,
}
function setupSubscriptions() {
let subs: ReturnType<typeof runGQLSubscription>[1][] = []
const userEnvironmentCreatedSub = setupUserEnvironmentCreatedSubscription()
const userEnvironmentUpdatedSub = setupUserEnvironmentUpdatedSubscription()
const userEnvironmentDeletedSub = setupUserEnvironmentDeletedSubscription()
subs = [
userEnvironmentCreatedSub,
userEnvironmentUpdatedSub,
userEnvironmentDeletedSub,
]
return () => {
subs.forEach((sub) => sub.unsubscribe())
}
}
async function loadUserEnvironments() {
const res = await getUserEnvironments()
if (E.isRight(res)) {
const environments = res.right.me.environments
if (environments.length > 0) {
environments.forEach((env, index) => {
environmentsMapper.addEntry(index, env.id)
})
runDispatchWithOutSyncing(() => {
replaceEnvironments(
environments.map(({ id, variables, name }) => ({
id,
name,
variables: JSON.parse(variables),
}))
)
})
}
}
}
async function loadGlobalEnvironments() {
const res = await getGlobalEnvironments()
if (E.isRight(res)) {
const globalEnv = res.right.me.globalEnvironments
if (globalEnv) {
runDispatchWithOutSyncing(() => {
setGlobalEnvVariables(JSON.parse(globalEnv.variables))
})
globalEnvironmentMapper.addEntry(0, globalEnv.id)
}
} else if (res.left.error == "user_environment/user_env_does_not_exists") {
const res = await createUserGlobalEnvironment(JSON.stringify([]))
if (E.isRight(res)) {
const backendId = res.right.createUserGlobalEnvironment.id
globalEnvironmentMapper.addEntry(0, backendId)
}
}
}
async function loadAllEnvironments() {
await loadUserEnvironments()
await loadGlobalEnvironments()
}
function setupUserEnvironmentCreatedSubscription() {
const [userEnvironmentCreated$, userEnvironmentCreatedSub] =
runUserEnvironmentCreatedSubscription()
userEnvironmentCreated$.subscribe((res) => {
console.group("Subscription: User Environment Created")
console.log(res)
console.groupEnd()
if (E.isRight(res)) {
const { name, variables } = res.right.userEnvironmentCreated
if (name) {
runDispatchWithOutSyncing(() => {
createEnvironment(name, JSON.parse(variables))
})
}
}
})
return userEnvironmentCreatedSub
}
function setupUserEnvironmentUpdatedSubscription() {
const [userEnvironmentUpdated$, userEnvironmentUpdatedSub] =
runUserEnvironmentUpdatedSubscription()
userEnvironmentUpdated$.subscribe((res) => {
console.group("Subscription: User Environment Updated")
console.log(res)
console.groupEnd()
if (E.isRight(res)) {
const { name, variables, id, isGlobal } = res.right.userEnvironmentUpdated
// handle the case for global environments
if (isGlobal) {
runDispatchWithOutSyncing(() => {
setGlobalEnvVariables(JSON.parse(variables))
})
} else {
// handle the case for normal environments
const localIndex = environmentsMapper.getIndexByBackendId(id)
if (localIndex && name) {
runDispatchWithOutSyncing(() => {
updateEnvironment(localIndex, {
name,
variables: JSON.parse(variables),
})
})
}
}
}
})
return userEnvironmentUpdatedSub
}
function setupUserEnvironmentDeletedSubscription() {
console.log("setting up user environments for user deleted")
const [userEnvironmentDeleted$, userEnvironmentDeletedSub] =
runUserEnvironmentDeletedSubscription()
userEnvironmentDeleted$.subscribe((res) => {
console.group("Subscription: User Environment Deleted")
console.log(res)
console.groupEnd()
if (E.isRight(res)) {
const { id } = res.right.userEnvironmentDeleted
const localIndex = environmentsMapper.getIndexByBackendId(id)
if (localIndex) {
runDispatchWithOutSyncing(() => {
deleteEnvironment(localIndex)
})
environmentsMapper.removeEntry(id)
} else {
console.log("could not find the localIndex")
// TODO:
// handle order of events
// eg: update coming before create
// skipping for this release
}
}
})
return userEnvironmentDeletedSub
}

View File

@@ -0,0 +1,112 @@
import { environmentsStore } from "@hoppscotch/common/newstore/environments"
import {
getSettingSubject,
settingsStore,
} from "@hoppscotch/common/newstore/settings"
import { getSyncInitFunction } from "../../lib/sync"
import * as E from "fp-ts/Either"
import { StoreSyncDefinitionOf } from "../../lib/sync"
import { createMapper } from "../../lib/sync/mapper"
import {
clearGlobalEnvironmentVariables,
createUserEnvironment,
deleteUserEnvironment,
updateUserEnvironment,
} from "./environments.api"
export const environmentsMapper = createMapper()
export const globalEnvironmentMapper = createMapper()
export const storeSyncDefinition: StoreSyncDefinitionOf<
typeof environmentsStore
> = {
async createEnvironment({ name, variables }) {
const lastCreatedEnvIndex = environmentsStore.value.environments.length - 1
const res = await createUserEnvironment(name, JSON.stringify(variables))
if (E.isRight(res)) {
const id = res.right.createUserEnvironment.id
environmentsMapper.addEntry(lastCreatedEnvIndex, id)
}
},
async appendEnvironments({ envs }) {
const appendListLength = envs.length
let appendStart =
environmentsStore.value.environments.length - appendListLength - 1
envs.forEach((env) => {
const envId = ++appendStart
;(async function () {
const res = await createUserEnvironment(
env.name,
JSON.stringify(env.variables)
)
if (E.isRight(res)) {
const id = res.right.createUserEnvironment.id
environmentsMapper.addEntry(envId, id)
}
})()
})
},
async duplicateEnvironment({ envIndex }) {
const environmentToDuplicate = environmentsStore.value.environments.find(
(_, index) => index === envIndex
)
const lastCreatedEnvIndex = environmentsStore.value.environments.length - 1
if (environmentToDuplicate) {
const res = await createUserEnvironment(
environmentToDuplicate?.name,
JSON.stringify(environmentToDuplicate?.variables)
)
if (E.isRight(res)) {
const id = res.right.createUserEnvironment.id
environmentsMapper.addEntry(lastCreatedEnvIndex, id)
}
}
},
updateEnvironment({ envIndex, updatedEnv }) {
const backendId = environmentsMapper.getBackendIdByIndex(envIndex)
console.log(environmentsMapper)
if (backendId) {
updateUserEnvironment(backendId, updatedEnv)()
}
},
async deleteEnvironment({ envIndex }) {
const backendId = environmentsMapper.getBackendIdByIndex(envIndex)
if (backendId) {
await deleteUserEnvironment(backendId)()
environmentsMapper.removeEntry(backendId)
}
},
setGlobalVariables({ entries }) {
const backendId = globalEnvironmentMapper.getBackendIdByIndex(0)
if (backendId) {
updateUserEnvironment(backendId, { name: "", variables: entries })()
}
},
clearGlobalVariables() {
const backendId = globalEnvironmentMapper.getBackendIdByIndex(0)
if (backendId) {
clearGlobalEnvironmentVariables(backendId)
}
},
}
export const environnmentsSyncer = getSyncInitFunction(
environmentsStore,
storeSyncDefinition,
() => settingsStore.value.syncEnvironments,
getSettingSubject("syncEnvironments")
)

View File

@@ -14,7 +14,9 @@
"noEmit": true,
"paths": {
"@hoppscotch/common": [ "../hoppscotch-common/src/index.ts" ],
"@hoppscotch/common/*": [ "../hoppscotch-common/src/*" ]
"@hoppscotch/common/*": [ "../hoppscotch-common/src/*" ],
"@platform/*": ["./src/platform/*"],
"@lib/*": ["./src/lib/*"],
}
},

View File

@@ -58,7 +58,8 @@ export default defineConfig({
"../hoppscotch-common/src/helpers/functional"
),
"@workers": path.resolve(__dirname, "../hoppscotch-common/src/workers"),
"@platform": path.resolve(__dirname, "./src/platform"),
"@lib": path.resolve(__dirname, "./src/lib"),
stream: "stream-browserify",
util: "util",
},
@@ -117,6 +118,11 @@ export default defineConfig({
prefix: "icon",
customCollections: ["hopp", "auth", "brands"],
}),
(compName: string) => {
if (compName.startsWith("Hopp"))
return { name: compName, from: "@hoppscotch/ui" }
else return undefined
},
],
types: [
{