feat: implement user history syncing for selfhost (#60)

This commit is contained in:
Akash K
2023-04-01 18:24:58 +05:30
committed by GitHub
parent 2b44ede92b
commit 8586ced3cc
13 changed files with 543 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
mutation CreateUserHistory(
$reqData: String!
$resMetadata: String!
$reqType: ReqType!
) {
createUserHistory(
reqData: $reqData
resMetadata: $resMetadata
reqType: $reqType
) {
id
}
}

View File

@@ -0,0 +1,6 @@
mutation DeleteAllUserHistory($reqType: ReqType!) {
deleteAllUserHistory(reqType: $reqType) {
count
reqType
}
}

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
query GetRESTUserHistory {
me {
RESTHistory {
id
userUid
reqType
request
responseMetadata
isStarred
executedOn
}
GQLHistory {
id
userUid
reqType
request
responseMetadata
isStarred
executedOn
}
}
}

View File

@@ -0,0 +1,10 @@
subscription UserHistoryCreated {
userHistoryCreated {
id
reqType
request
responseMetadata
isStarred
executedOn
}
}

View File

@@ -0,0 +1,6 @@
subscription userHistoryDeleted {
userHistoryDeleted {
id
reqType
}
}

View File

@@ -0,0 +1,6 @@
subscription UserHistoryDeletedMany {
userHistoryDeletedMany {
count
reqType
}
}

View File

@@ -0,0 +1,10 @@
subscription UserHistoryUpdated {
userHistoryUpdated {
id
reqType
request
responseMetadata
isStarred
executedOn
}
}

View File

@@ -3,6 +3,7 @@ import { def as authDef } from "./platform/auth"
import { def as environmentsDef } from "./platform/environments/environments.platform"
import { def as collectionsDef } from "./platform/collections/collections.platform"
import { def as settingsDef } from "./platform/settings/settings.platform"
import { def as historyDef } from "./platform/history/history.platform"
createHoppApp("#app", {
auth: authDef,
@@ -10,5 +11,6 @@ createHoppApp("#app", {
environments: environmentsDef,
collections: collectionsDef,
settings: settingsDef,
history: historyDef,
},
})

View File

@@ -0,0 +1,95 @@
import {
runMutation,
runGQLQuery,
runGQLSubscription,
} from "@hoppscotch/common/helpers/backend/GQLClient"
import {
CreateUserHistoryDocument,
CreateUserHistoryMutation,
CreateUserHistoryMutationVariables,
DeleteAllUserHistoryDocument,
DeleteAllUserHistoryMutation,
DeleteAllUserHistoryMutationVariables,
GetRestUserHistoryDocument,
GetRestUserHistoryQuery,
GetRestUserHistoryQueryVariables,
RemoveRequestFromHistoryDocument,
RemoveRequestFromHistoryMutation,
RemoveRequestFromHistoryMutationVariables,
ReqType,
ToggleHistoryStarStatusDocument,
ToggleHistoryStarStatusMutation,
ToggleHistoryStarStatusMutationVariables,
UserHistoryCreatedDocument,
UserHistoryDeletedDocument,
UserHistoryDeletedManyDocument,
UserHistoryUpdatedDocument,
} from "../../api/generated/graphql"
export const getUserHistoryEntries = () =>
runGQLQuery<GetRestUserHistoryQuery, GetRestUserHistoryQueryVariables, "">({
query: GetRestUserHistoryDocument,
})
export const createUserHistory = (
reqData: string,
resMetadata: string,
reqType: ReqType
) =>
runMutation<
CreateUserHistoryMutation,
CreateUserHistoryMutationVariables,
""
>(CreateUserHistoryDocument, {
reqData,
resMetadata,
reqType,
})()
export const toggleHistoryStarStatus = (id: string) =>
runMutation<
ToggleHistoryStarStatusMutation,
ToggleHistoryStarStatusMutationVariables,
""
>(ToggleHistoryStarStatusDocument, {
id,
})()
export const removeRequestFromHistory = (id: string) =>
runMutation<
RemoveRequestFromHistoryMutation,
RemoveRequestFromHistoryMutationVariables,
""
>(RemoveRequestFromHistoryDocument, {
id,
})()
export const deleteAllUserHistory = (reqType: ReqType) =>
runMutation<
DeleteAllUserHistoryMutation,
DeleteAllUserHistoryMutationVariables,
""
>(DeleteAllUserHistoryDocument, {
reqType,
})()
export const runUserHistoryCreatedSubscription = () =>
runGQLSubscription({
query: UserHistoryCreatedDocument,
})
export const runUserHistoryUpdatedSubscription = () =>
runGQLSubscription({
query: UserHistoryUpdatedDocument,
})
export const runUserHistoryDeletedSubscription = () =>
runGQLSubscription({
query: UserHistoryDeletedDocument,
})
export const runUserHistoryDeletedManySubscription = () =>
runGQLSubscription({
query: UserHistoryDeletedManyDocument,
})

View File

@@ -0,0 +1,261 @@
import { authEvents$, def as platformAuth } from "@platform/auth"
import {
restHistoryStore,
RESTHistoryEntry,
setRESTHistoryEntries,
addRESTHistoryEntry,
toggleRESTHistoryEntryStar,
deleteRESTHistoryEntry,
clearRESTHistory,
setGraphqlHistoryEntries,
GQLHistoryEntry,
addGraphqlHistoryEntry,
toggleGraphqlHistoryEntryStar,
graphqlHistoryStore,
deleteGraphqlHistoryEntry,
clearGraphqlHistory,
} from "@hoppscotch/common/newstore/history"
import { HistoryPlatformDef } from "@hoppscotch/common/platform/history"
import {
getUserHistoryEntries,
runUserHistoryCreatedSubscription,
runUserHistoryDeletedManySubscription,
runUserHistoryDeletedSubscription,
runUserHistoryUpdatedSubscription,
} from "./history.api"
import * as E from "fp-ts/Either"
import { restHistorySyncer, gqlHistorySyncer } from "./history.sync"
import { runGQLSubscription } from "@hoppscotch/common/helpers/backend/GQLClient"
import { runDispatchWithOutSyncing } from "@lib/sync"
import { ReqType } from "../../api/generated/graphql"
function initHistorySync() {
const currentUser$ = platformAuth.getCurrentUserStream()
restHistorySyncer.startStoreSync()
restHistorySyncer.setupSubscriptions(setupSubscriptions)
gqlHistorySyncer.startStoreSync()
loadHistoryEntries()
currentUser$.subscribe(async (user) => {
if (user) {
await loadHistoryEntries()
}
})
authEvents$.subscribe((event) => {
if (event.event == "login" || event.event == "token_refresh") {
restHistorySyncer.startListeningToSubscriptions()
}
if (event.event == "logout") {
restHistorySyncer.stopListeningToSubscriptions()
}
})
}
function setupSubscriptions() {
let subs: ReturnType<typeof runGQLSubscription>[1][] = []
const userHistoryCreatedSub = setupUserHistoryCreatedSubscription()
const userHistoryUpdatedSub = setupUserHistoryUpdatedSubscription()
const userHistoryDeletedSub = setupUserHistoryDeletedSubscription()
const userHistoryDeletedManySub = setupUserHistoryDeletedManySubscription()
subs = [
userHistoryCreatedSub,
userHistoryUpdatedSub,
userHistoryDeletedSub,
userHistoryDeletedManySub,
]
return () => {
subs.forEach((sub) => sub.unsubscribe())
}
}
async function loadHistoryEntries() {
const res = await getUserHistoryEntries()
if (E.isRight(res)) {
const restEntries = res.right.me.RESTHistory
const gqlEntries = res.right.me.GQLHistory
const restHistoryEntries: RESTHistoryEntry[] = restEntries.map((entry) => ({
v: 1,
request: JSON.parse(entry.request),
responseMeta: JSON.parse(entry.responseMetadata),
star: entry.isStarred,
updatedOn: new Date(entry.executedOn),
id: entry.id,
}))
const gqlHistoryEntries: GQLHistoryEntry[] = gqlEntries.map((entry) => ({
v: 1,
request: JSON.parse(entry.request),
response: JSON.parse(entry.responseMetadata),
star: entry.isStarred,
updatedOn: new Date(entry.executedOn),
id: entry.id,
}))
runDispatchWithOutSyncing(() => {
setRESTHistoryEntries(restHistoryEntries)
setGraphqlHistoryEntries(gqlHistoryEntries)
})
}
}
function setupUserHistoryCreatedSubscription() {
const [userHistoryCreated$, userHistoryCreatedSub] =
runUserHistoryCreatedSubscription()
userHistoryCreated$.subscribe((res) => {
if (E.isRight(res)) {
const { id, reqType, request, responseMetadata, isStarred, executedOn } =
res.right.userHistoryCreated
const hasAlreadyBeenAdded =
reqType == ReqType.Rest
? restHistoryStore.value.state.some((entry) => entry.id == id)
: graphqlHistoryStore.value.state.some((entry) => entry.id == id)
!hasAlreadyBeenAdded &&
runDispatchWithOutSyncing(() => {
reqType == ReqType.Rest
? addRESTHistoryEntry({
v: 1,
id,
request: JSON.parse(request),
responseMeta: JSON.parse(responseMetadata),
star: isStarred,
updatedOn: new Date(executedOn),
})
: addGraphqlHistoryEntry({
v: 1,
id,
request: JSON.parse(request),
response: JSON.parse(responseMetadata),
star: isStarred,
updatedOn: new Date(executedOn),
})
})
}
})
return userHistoryCreatedSub
}
// currently the updates are only for toggling the star
function setupUserHistoryUpdatedSubscription() {
const [userHistoryUpdated$, userHistoryUpdatedSub] =
runUserHistoryUpdatedSubscription()
userHistoryUpdated$.subscribe((res) => {
if (E.isRight(res)) {
const { id, executedOn, isStarred, request, responseMetadata, reqType } =
res.right.userHistoryUpdated
if (reqType == ReqType.Rest) {
const updatedRestEntryIndex = restHistoryStore.value.state.findIndex(
(entry) => entry.id == id
)
if (updatedRestEntryIndex != -1) {
runDispatchWithOutSyncing(() => {
toggleRESTHistoryEntryStar({
v: 1,
id,
request: JSON.parse(request),
responseMeta: JSON.parse(responseMetadata),
// because the star will be toggled in the store, we need to pass the opposite value
star: !isStarred,
updatedOn: new Date(executedOn),
})
})
}
}
if (reqType == ReqType.Gql) {
const updatedGQLEntryIndex = graphqlHistoryStore.value.state.findIndex(
(entry) => entry.id == id
)
if (updatedGQLEntryIndex != -1) {
runDispatchWithOutSyncing(() => {
toggleGraphqlHistoryEntryStar({
v: 1,
id,
request: JSON.parse(request),
response: JSON.parse(responseMetadata),
// because the star will be toggled in the store, we need to pass the opposite value
star: !isStarred,
updatedOn: new Date(executedOn),
})
})
}
}
}
})
return userHistoryUpdatedSub
}
function setupUserHistoryDeletedSubscription() {
const [userHistoryDeleted$, userHistoryDeletedSub] =
runUserHistoryDeletedSubscription()
userHistoryDeleted$.subscribe((res) => {
if (E.isRight(res)) {
const { id, reqType } = res.right.userHistoryDeleted
if (reqType == ReqType.Gql) {
const deletedEntry = graphqlHistoryStore.value.state.find(
(entry) => entry.id == id
)
deletedEntry &&
runDispatchWithOutSyncing(() => {
deleteGraphqlHistoryEntry(deletedEntry)
})
}
if (reqType == ReqType.Rest) {
const deletedEntry = restHistoryStore.value.state.find(
(entry) => entry.id == id
)
deletedEntry &&
runDispatchWithOutSyncing(() => {
deleteRESTHistoryEntry(deletedEntry)
})
}
}
})
return userHistoryDeletedSub
}
function setupUserHistoryDeletedManySubscription() {
const [userHistoryDeletedMany$, userHistoryDeletedManySub] =
runUserHistoryDeletedManySubscription()
userHistoryDeletedMany$.subscribe((res) => {
if (E.isRight(res)) {
const { reqType } = res.right.userHistoryDeletedMany
runDispatchWithOutSyncing(() => {
reqType == ReqType.Rest ? clearRESTHistory() : clearGraphqlHistory()
})
}
})
return userHistoryDeletedManySub
}
export const def: HistoryPlatformDef = {
initHistorySync,
}

View File

@@ -0,0 +1,101 @@
import {
graphqlHistoryStore,
removeDuplicateRestHistoryEntry,
removeDuplicateGraphqlHistoryEntry,
restHistoryStore,
} from "@hoppscotch/common/newstore/history"
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 {
createUserHistory,
deleteAllUserHistory,
removeRequestFromHistory,
toggleHistoryStarStatus,
} from "./history.api"
import { ReqType } from "../../api/generated/graphql"
export const restHistoryStoreSyncDefinition: StoreSyncDefinitionOf<
typeof restHistoryStore
> = {
async addEntry({ entry }) {
const res = await createUserHistory(
JSON.stringify(entry.request),
JSON.stringify(entry.responseMeta),
ReqType.Rest
)
if (E.isRight(res)) {
entry.id = res.right.createUserHistory.id
// preventing double insertion from here and subscription
removeDuplicateRestHistoryEntry(entry.id)
}
},
deleteEntry({ entry }) {
if (entry.id) {
removeRequestFromHistory(entry.id)
}
},
toggleStar({ entry }) {
if (entry.id) {
toggleHistoryStarStatus(entry.id)
}
},
clearHistory() {
deleteAllUserHistory(ReqType.Rest)
},
}
export const gqlHistoryStoreSyncDefinition: StoreSyncDefinitionOf<
typeof graphqlHistoryStore
> = {
async addEntry({ entry }) {
const res = await createUserHistory(
JSON.stringify(entry.request),
JSON.stringify(entry.response),
ReqType.Gql
)
if (E.isRight(res)) {
entry.id = res.right.createUserHistory.id
// preventing double insertion from here and subscription
removeDuplicateGraphqlHistoryEntry(entry.id)
}
},
deleteEntry({ entry }) {
if (entry.id) {
removeRequestFromHistory(entry.id)
}
},
toggleStar({ entry }) {
if (entry.id) {
toggleHistoryStarStatus(entry.id)
}
},
clearHistory() {
deleteAllUserHistory(ReqType.Gql)
},
}
export const restHistorySyncer = getSyncInitFunction(
restHistoryStore,
restHistoryStoreSyncDefinition,
() => settingsStore.value.syncHistory,
getSettingSubject("syncHistory")
)
export const gqlHistorySyncer = getSyncInitFunction(
graphqlHistoryStore,
gqlHistoryStoreSyncDefinition,
() => settingsStore.value.syncHistory,
getSettingSubject("syncHistory")
)