Consolidate frontend REST calls with GraphQL
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 1s
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 1s
Replace 5 separate REST calls on home page load with a single GraphQL query. Add homeData store that fetches posts, favorites, activities, spotify, and auth in one request. Convert all admin mutations and auth flows to use GraphQL. Add album images to Spotify GraphQL schema. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7
nginx/vue/src/graphql.js
Normal file
7
nginx/vue/src/graphql.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import axios from "axios";
|
||||
|
||||
export async function gql(query, variables = {}) {
|
||||
const res = await axios.post("/api/graphql", { query, variables });
|
||||
if (res.data.errors && !res.data.data) throw new Error(res.data.errors[0].message);
|
||||
return res.data.data;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
import axios from "axios";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useHomeDataStore } from "@/stores/homeData";
|
||||
|
||||
const activity_template = {
|
||||
type: "activity",
|
||||
@@ -13,19 +13,21 @@ export const useActivityStore = defineStore("activity", () => {
|
||||
|
||||
const activityCount = computed(() => activity.value.length);
|
||||
|
||||
async function fetchActivity() {
|
||||
try {
|
||||
const res = await axios.get("/api/activity");
|
||||
if (!Array.isArray(res.data)) {
|
||||
throw new Error("Invalid response from posts API");
|
||||
const homeData = useHomeDataStore();
|
||||
watch(
|
||||
() => homeData.activities,
|
||||
(newActivities) => {
|
||||
if (newActivities.length > 0) {
|
||||
activity.value = newActivities;
|
||||
}
|
||||
activity.value = res.data;
|
||||
} catch (err) {
|
||||
console.error("Cannot connect to activity API", err);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function fetchActivity() {
|
||||
await homeData.fetchAll();
|
||||
}
|
||||
|
||||
fetchActivity();
|
||||
return {
|
||||
activity,
|
||||
activityCount,
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
import axios from "axios";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { gql } from "@/graphql";
|
||||
import { useHomeDataStore } from "@/stores/homeData";
|
||||
|
||||
export const useAuthStore = defineStore("auth", () => {
|
||||
const user = ref({});
|
||||
const loggedIn = computed(() => !!user.value.username);
|
||||
|
||||
checkToken();
|
||||
const homeData = useHomeDataStore();
|
||||
watch(
|
||||
() => homeData.me,
|
||||
(me) => {
|
||||
if (me) {
|
||||
user.value = me;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function logOut() {
|
||||
try {
|
||||
const res = await axios.post("/api/auth/logout");
|
||||
await gql(`mutation { logout }`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
@@ -19,11 +29,11 @@ export const useAuthStore = defineStore("auth", () => {
|
||||
|
||||
async function logIn(username, password) {
|
||||
try {
|
||||
const res = await axios.post("/api/auth/login", {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
user.value = res.data;
|
||||
const data = await gql(
|
||||
`mutation Login($input: LoginInput!) { login(input: $input) { user { id username admin } } }`,
|
||||
{ input: { username, password } },
|
||||
);
|
||||
user.value = data.login.user;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
@@ -31,38 +41,35 @@ export const useAuthStore = defineStore("auth", () => {
|
||||
|
||||
async function createUser(username, password) {
|
||||
try {
|
||||
const res = await axios.post("/api/user", {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
user.value = res.data;
|
||||
const data = await gql(
|
||||
`mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id username admin } }`,
|
||||
{ input: { username, password } },
|
||||
);
|
||||
return data.createUser;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshToken() {
|
||||
try {
|
||||
const res = await axios.post("/api/auth/refresh");
|
||||
user.value = res.data;
|
||||
const data = await gql(
|
||||
`mutation { refreshToken { user { id username admin } } }`,
|
||||
);
|
||||
user.value = data.refreshToken.user;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkToken() {
|
||||
try {
|
||||
const res = await axios.get("/api/auth/check");
|
||||
user.value = res.data;
|
||||
} catch (err) {
|
||||
user.value = {};
|
||||
}
|
||||
}
|
||||
|
||||
async function setUserAdmin(userId, admin) {
|
||||
try {
|
||||
const res = await axios.patch(`/api/user/${userId}/admin`, { admin });
|
||||
return res.data;
|
||||
const data = await gql(
|
||||
`mutation SetUserAdmin($id: ID!, $admin: Boolean!) { setUserAdmin(id: $id, admin: $admin) { id username admin } }`,
|
||||
{ id: userId, admin },
|
||||
);
|
||||
return data.setUserAdmin;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -75,7 +82,6 @@ export const useAuthStore = defineStore("auth", () => {
|
||||
loggedIn,
|
||||
|
||||
logIn,
|
||||
checkToken,
|
||||
refreshToken,
|
||||
logOut,
|
||||
createUser,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
import axios from "axios";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useHomeDataStore } from "@/stores/homeData";
|
||||
|
||||
const favorite_template = {
|
||||
type: "favorite",
|
||||
@@ -13,19 +13,20 @@ export const useFavoritesStore = defineStore("favorites", () => {
|
||||
|
||||
const favoritesCount = computed(() => favorites.value.length);
|
||||
|
||||
async function fetchFavorites() {
|
||||
try {
|
||||
const res = await axios.get("/api/favorites");
|
||||
if (!Array.isArray(res.data)) {
|
||||
throw new Error("Invalid response from favorites API");
|
||||
const homeData = useHomeDataStore();
|
||||
watch(
|
||||
() => homeData.favorites,
|
||||
(newFavorites) => {
|
||||
if (newFavorites.length > 0) {
|
||||
favorites.value = newFavorites;
|
||||
}
|
||||
favorites.value = res.data;
|
||||
} catch (err) {
|
||||
console.error("Cannot connect to favorites API", err);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
fetchFavorites();
|
||||
async function fetchFavorites() {
|
||||
await homeData.fetchAll();
|
||||
}
|
||||
|
||||
return {
|
||||
favorites,
|
||||
|
||||
50
nginx/vue/src/stores/homeData.js
Normal file
50
nginx/vue/src/stores/homeData.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { gql } from "@/graphql";
|
||||
|
||||
export const useHomeDataStore = defineStore("homeData", () => {
|
||||
const loaded = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
const me = ref(null);
|
||||
const posts = ref([]);
|
||||
const favorites = ref([]);
|
||||
const activities = ref([]);
|
||||
const spotifyRecent = ref([]);
|
||||
|
||||
async function fetchAll() {
|
||||
try {
|
||||
const data = await gql(`
|
||||
query HomeData {
|
||||
posts { id title content createdAt updatedAt author { id username } }
|
||||
favorites { id type name link createdAt }
|
||||
activities { id type name link createdAt }
|
||||
spotifyRecent { track { name album { name images { url } } artists { name } } playedAt }
|
||||
me { id username admin }
|
||||
}
|
||||
`);
|
||||
posts.value = data.posts;
|
||||
favorites.value = data.favorites;
|
||||
activities.value = data.activities;
|
||||
spotifyRecent.value = data.spotifyRecent;
|
||||
me.value = data.me || null;
|
||||
loaded.value = true;
|
||||
} catch (err) {
|
||||
console.error("HomeData fetch failed:", err);
|
||||
error.value = err;
|
||||
}
|
||||
}
|
||||
|
||||
fetchAll();
|
||||
|
||||
return {
|
||||
loaded,
|
||||
error,
|
||||
me,
|
||||
posts,
|
||||
favorites,
|
||||
activities,
|
||||
spotifyRecent,
|
||||
fetchAll,
|
||||
};
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
import axios from "axios";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { gql } from "@/graphql";
|
||||
import { useHomeDataStore } from "@/stores/homeData";
|
||||
|
||||
const post_template = {
|
||||
title: "Can't fetch from the db yo",
|
||||
@@ -17,32 +18,34 @@ export const usePostsStore = defineStore("posts", () => {
|
||||
|
||||
const postsCount = computed(() => posts.value.length);
|
||||
|
||||
async function fetchPosts() {
|
||||
try {
|
||||
const res = await axios.get("/api/posts");
|
||||
if (!Array.isArray(res.data)) {
|
||||
throw new Error("Invalid response from posts API");
|
||||
const homeData = useHomeDataStore();
|
||||
watch(
|
||||
() => homeData.posts,
|
||||
(newPosts) => {
|
||||
if (newPosts.length > 0) {
|
||||
posts.value = newPosts;
|
||||
}
|
||||
posts.value = res.data;
|
||||
} catch (err) {
|
||||
console.error("Cannot connect to Post API", err);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function fetchPosts() {
|
||||
await homeData.fetchAll();
|
||||
}
|
||||
|
||||
async function deletePost(post) {
|
||||
try {
|
||||
const res = await axios.delete(
|
||||
`/api/posts/${encodeURIComponent(post.id)}`,
|
||||
await gql(
|
||||
`mutation DeletePost($id: ID!) { deletePost(id: $id) { id } }`,
|
||||
{ id: post.id },
|
||||
);
|
||||
console.log("Deleted:", res.data);
|
||||
fetchPosts();
|
||||
console.log("Deleted:", post.id);
|
||||
await homeData.fetchAll();
|
||||
} catch (err) {
|
||||
console.error("Delete failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
fetchPosts();
|
||||
|
||||
return {
|
||||
posts,
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
import axios from "axios";
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { gql } from "@/graphql";
|
||||
import { useHomeDataStore } from "@/stores/homeData";
|
||||
|
||||
const song_template = {
|
||||
id: 1,
|
||||
track: {
|
||||
id: 1,
|
||||
name: "^_^",
|
||||
album: { images: [{ url: "/img/Untitled.png" }] },
|
||||
album: { name: "", images: [{ url: "/img/Untitled.png" }] },
|
||||
artists: [{ name: ">_<" }],
|
||||
},
|
||||
};
|
||||
@@ -17,13 +16,34 @@ export const useSongsStore = defineStore("songs", () => {
|
||||
|
||||
const songsCount = computed(() => songs.value.length);
|
||||
|
||||
const homeData = useHomeDataStore();
|
||||
watch(
|
||||
() => homeData.spotifyRecent,
|
||||
(newSongs) => {
|
||||
if (newSongs.length > 0) {
|
||||
songs.value = newSongs;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function fetchSongs() {
|
||||
try {
|
||||
const res = await axios.get("/api/spotify/recent");
|
||||
if (!Array.isArray(res.data)) {
|
||||
throw new Error("Invalid response from Spotify API");
|
||||
const data = await gql(`
|
||||
query {
|
||||
spotifyRecent {
|
||||
track {
|
||||
name
|
||||
album { name images { url } }
|
||||
artists { name }
|
||||
}
|
||||
playedAt
|
||||
}
|
||||
}
|
||||
`);
|
||||
if (Array.isArray(data.spotifyRecent) && data.spotifyRecent.length > 0) {
|
||||
songs.value = data.spotifyRecent;
|
||||
}
|
||||
songs.value = res.data;
|
||||
} catch (err) {
|
||||
console.error("Cannot connect to Spotify API", err);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Button from "@/components/input/Button.vue";
|
||||
|
||||
import { ref } from "vue";
|
||||
import axios from "axios";
|
||||
import { gql } from "@/graphql";
|
||||
|
||||
const type = ref("");
|
||||
const name = ref("");
|
||||
@@ -10,15 +10,14 @@ const link = ref("");
|
||||
|
||||
async function post() {
|
||||
try {
|
||||
const res = await axios.post("/api/activity", {
|
||||
type: type.value,
|
||||
name: name.value,
|
||||
link: link.value || undefined,
|
||||
});
|
||||
const data = await gql(
|
||||
`mutation CreateActivity($input: CreateActivityInput!) { createActivity(input: $input) { id } }`,
|
||||
{ input: { type: type.value, name: name.value, link: link.value || undefined } },
|
||||
);
|
||||
type.value = "";
|
||||
name.value = "";
|
||||
link.value = "";
|
||||
console.log(res.data);
|
||||
console.log(data.createActivity);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Button from "@/components/input/Button.vue";
|
||||
|
||||
import { ref } from "vue";
|
||||
import axios from "axios";
|
||||
import { gql } from "@/graphql";
|
||||
|
||||
const type = ref("");
|
||||
const name = ref("");
|
||||
@@ -10,15 +10,14 @@ const link = ref("");
|
||||
|
||||
async function post() {
|
||||
try {
|
||||
const res = await axios.post("/api/favorites", {
|
||||
type: type.value,
|
||||
name: name.value,
|
||||
link: link.value || undefined,
|
||||
});
|
||||
const data = await gql(
|
||||
`mutation CreateFavorite($input: CreateFavoriteInput!) { createFavorite(input: $input) { id } }`,
|
||||
{ input: { type: type.value, name: name.value, link: link.value || undefined } },
|
||||
);
|
||||
type.value = "";
|
||||
name.value = "";
|
||||
link.value = "";
|
||||
console.log(res.data);
|
||||
console.log(data.createFavorite);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script setup>
|
||||
import Button from "@/components/input/Button.vue";
|
||||
import { ref } from "vue";
|
||||
import axios from "axios";
|
||||
import { gql } from "@/graphql";
|
||||
|
||||
const title = ref("");
|
||||
const content = ref("");
|
||||
|
||||
async function post() {
|
||||
try {
|
||||
const res = await axios.post("/api/posts", {
|
||||
title: title.value,
|
||||
content: content.value,
|
||||
});
|
||||
const data = await gql(
|
||||
`mutation CreatePost($input: CreatePostInput!) { createPost(input: $input) { id } }`,
|
||||
{ input: { title: title.value, content: content.value } },
|
||||
);
|
||||
title.value = "";
|
||||
content.value = "";
|
||||
console.log(res.data);
|
||||
console.log(data.createPost);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Button from "@/components/input/Button.vue";
|
||||
import { ref } from "vue";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import axios from "axios";
|
||||
import { gql } from "@/graphql";
|
||||
|
||||
const auth = useAuthStore();
|
||||
const username = ref("");
|
||||
@@ -14,15 +14,15 @@ async function handleCreate() {
|
||||
message.value = "";
|
||||
error.value = "";
|
||||
try {
|
||||
const res = await axios.post("/api/user", {
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
});
|
||||
message.value = `User "${res.data.username}" created successfully.`;
|
||||
const data = await gql(
|
||||
`mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id username } }`,
|
||||
{ input: { username: username.value, password: password.value } },
|
||||
);
|
||||
message.value = `User "${data.createUser.username}" created successfully.`;
|
||||
username.value = "";
|
||||
password.value = "";
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.message || "Failed to create user.";
|
||||
error.value = err.message || "Failed to create user.";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
import Button from "@/components/input/Button.vue";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import axios from "axios";
|
||||
import { gql } from "@/graphql";
|
||||
|
||||
const auth = useAuthStore();
|
||||
const users = ref([]);
|
||||
|
||||
async function fetchUsers() {
|
||||
try {
|
||||
const res = await axios.get("/api/user");
|
||||
users.value = res.data;
|
||||
const data = await gql(`query { users { id username admin } }`);
|
||||
users.value = data.users;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
@@ -18,8 +18,8 @@ async function fetchUsers() {
|
||||
|
||||
async function toggleAdmin(user) {
|
||||
try {
|
||||
const res = await auth.setUserAdmin(user.id, !user.admin);
|
||||
user.admin = res.admin;
|
||||
const data = await auth.setUserAdmin(user.id, !user.admin);
|
||||
user.admin = data.admin;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ onUnmounted(() => {
|
||||
<Transition name="fade">
|
||||
<div
|
||||
@click="nextSong"
|
||||
:key="song.track.id"
|
||||
:key="song.track.name"
|
||||
class="flex flex-col items-center"
|
||||
>
|
||||
<Header>Listening To</Header>
|
||||
|
||||
@@ -3,11 +3,9 @@ import Wip from "@/components/util/Wip.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="items-center flex flex-col">
|
||||
<div
|
||||
class="a4page-portrait items-center bdr-1 flex flex-col relative overflow-scroll"
|
||||
>
|
||||
<Wip />
|
||||
</div>
|
||||
</main>
|
||||
<main class="items-center flex flex-col">
|
||||
<div class="a4page-portrait items-center bdr-1 flex flex-col relative overflow-scroll">
|
||||
<Wip />
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user