feat: implement user history syncing for selfhost (#60)
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
mutation CreateUserHistory(
|
||||
$reqData: String!
|
||||
$resMetadata: String!
|
||||
$reqType: ReqType!
|
||||
) {
|
||||
createUserHistory(
|
||||
reqData: $reqData
|
||||
resMetadata: $resMetadata
|
||||
reqType: $reqType
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
mutation DeleteAllUserHistory($reqType: ReqType!) {
|
||||
deleteAllUserHistory(reqType: $reqType) {
|
||||
count
|
||||
reqType
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation RemoveRequestFromHistory($id: ID!) {
|
||||
removeRequestFromHistory(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation ToggleHistoryStarStatus($id: ID!) {
|
||||
toggleHistoryStarStatus(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
query GetRESTUserHistory {
|
||||
me {
|
||||
RESTHistory {
|
||||
id
|
||||
userUid
|
||||
reqType
|
||||
request
|
||||
responseMetadata
|
||||
isStarred
|
||||
executedOn
|
||||
}
|
||||
|
||||
GQLHistory {
|
||||
id
|
||||
userUid
|
||||
reqType
|
||||
request
|
||||
responseMetadata
|
||||
isStarred
|
||||
executedOn
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
subscription UserHistoryCreated {
|
||||
userHistoryCreated {
|
||||
id
|
||||
reqType
|
||||
request
|
||||
responseMetadata
|
||||
isStarred
|
||||
executedOn
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
subscription userHistoryDeleted {
|
||||
userHistoryDeleted {
|
||||
id
|
||||
reqType
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
subscription UserHistoryDeletedMany {
|
||||
userHistoryDeletedMany {
|
||||
count
|
||||
reqType
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
subscription UserHistoryUpdated {
|
||||
userHistoryUpdated {
|
||||
id
|
||||
reqType
|
||||
request
|
||||
responseMetadata
|
||||
isStarred
|
||||
executedOn
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
Reference in New Issue
Block a user