Skip to content

Commit

Permalink
feat: backport stuff from real-world projects
Browse files Browse the repository at this point in the history
  • Loading branch information
lewebsimple committed Dec 4, 2023
1 parent 8f007c4 commit f085f04
Show file tree
Hide file tree
Showing 25 changed files with 736 additions and 1,097 deletions.
2 changes: 1 addition & 1 deletion app/layouts/default.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="min-h-screen flex flex-col">
<main class="flex-grow container">
<main class="flex-grow container py-4">
<slot />
</main>
<TheAppFooter />
Expand Down
1 change: 1 addition & 0 deletions app/server/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { logger } from "@nuxt/kit";
6 changes: 6 additions & 0 deletions app/types/utils.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
declare global {
type PartialBy<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
type SinglePropertyType<T> = { [K in keyof T]: { [P in K]: T[K] } }[keyof T];
}

export { type PartialBy, type SinglePropertyType };
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { FormSubmitEvent } from "#ui/types";
const { login } = useAuth();
const refAuthLoginForm = ref();
const refFormAuthLogin = ref();
const state = ref<AuthLogin>({ email: "", password: "" });
const isSubmitting = ref(false);
Expand All @@ -24,20 +24,15 @@ async function onSubmit(event: FormSubmitEvent<AuthLogin>) {
const redirect = useRoute().query.redirect?.toString() || "/";
await useRouter().replace(redirect);
} catch (error) {
useToast().add({
title: "Échec de la connexion",
description: "Veuillez vérifier vos identifiants",
icon: "i-heroicons-x-circle",
color: "red",
});
useToaster().error("Veuillez vérifier vos identifiants");
} finally {
isSubmitting.value = false;
}
}
</script>

<template>
<UForm ref="refAuthLoginForm" :schema="authLoginSchema" :state="state" @submit="onSubmit">
<UForm form="refFormAuthLogin" :schema="authLoginSchema" :state="state" @submit="onSubmit">
<div class="form-wrapper">
<UFormGroup name="email" label="Courriel">
<UInput v-model="state.email" type="email" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,7 @@ async function onSubmit() {
await logout();
await useRouter().replace("/");
} catch (error) {
// Ignore logout errors
useToast().add({
title: "Échec de la déconnexion",
description: "Veuillez réessayer plus tard",
icon: "i-heroicons-x-circle",
color: "red",
});
useToaster().error("Veuillez réessayer plus tard");
} finally {
isSubmitting.value = false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { FormSubmitEvent } from "#ui/types";
const { signup } = useAuth();
const refAuthSignupForm = ref();
const refFormAuthSignup = ref();
const state = ref<AuthSignup>({ email: "", password: "", role: AuthRole.VERIFIED });
Expand All @@ -18,7 +18,7 @@ const isSubmitting = ref(false);
const submitAttrs = computed(() => ({
block: true,
color: <any>isSubmitting.value ? "gray" : "primary",
disabled: isSubmitting.value || refAuthSignupForm.value?.errors.length > 0,
disabled: isSubmitting.value || refFormAuthSignup.value?.errors.length > 0,
label: isSubmitting.value ? "Inscription en cours..." : "Inscription",
loading: isSubmitting.value,
type: "submit",
Expand All @@ -30,27 +30,17 @@ async function onSubmit(event: FormSubmitEvent<AuthSignup>) {
isSubmitting.value = true;
await signup(event.data);
state.value = { email: "", password: "", role: "VERIFIED" };
useToast().add({
title: "Inscription réussie",
description: `Le compte ${event.data.email} a été créé avec succès`,
icon: "i-heroicons-check-circle",
color: "green",
});
useToaster().success(`Le compte ${event.data.email} a été créé avec succès`);
} catch (error) {
useToast().add({
title: "Échec de l'inscription",
description: "Le courriel est déjà utilisé ou une erreur est survenue",
icon: "i-heroicons-x-circle",
color: "red",
});
useToaster().error("Le courriel est déjà utilisé ou une erreur est survenue");
} finally {
isSubmitting.value = false;
}
}
</script>

<template>
<UForm ref="refAuthSignupForm" :schema="authLoginSchema" :state="state" @submit="onSubmit">
<UForm form="refAuthSignup" :schema="authLoginSchema" :state="state" @submit="onSubmit">
<div class="form-wrapper">
<UFormGroup name="email" label="Courriel">
<UInput v-model="state.email" type="email" />
Expand Down
2 changes: 1 addition & 1 deletion auth/pages/auth/login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ useHead({ title: "Connexion" });
<template>
<div id="page-auth-login">
<TheAuthPage title="Veuillez vous identifier">
<TheAuthLoginForm />
<FormAuthLogin />
</TheAuthPage>
</div>
</template>
2 changes: 1 addition & 1 deletion auth/pages/auth/logout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ useHead({ title: "Déconnexion" });
<template>
<div id="page-auth-logout">
<TheAuthPage title="Confirmez la déconnexion">
<TheAuthLogoutForm />
<FormAuthLogout />
</TheAuthPage>
</div>
</template>
2 changes: 1 addition & 1 deletion auth/pages/auth/signup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ useHead({ title: "Inscription" });
<template>
<div id="page-auth-signup">
<TheAuthPage title="Veuillez vous inscrire">
<TheAuthSignupForm />
<FormAuthSignup />
</TheAuthPage>
</div>
</template>
2 changes: 1 addition & 1 deletion graphql.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default {
projects: {
default: {
schema: "graphql/schema.graphql",
documents: ["graphql/**/*.{vue,ts}"],
documents: ["{app,graphql}/**/*.{vue,ts}"],
extensions: {
codegen: {
generates: {
Expand Down
35 changes: 35 additions & 0 deletions graphql/composables/cursor-pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export const pageInfoFragment = graphql(`
fragment PageInfo on PageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
`);

export type CursorPagination = {
after: string | null;
before: string | null;
first: number | null;
last: number | null;
};

export function useCursorPagination(stateName: string, pageInfo: Ref<PageInfoFragment | null | undefined>, perPage = 10) {
const cursorPagination = useState<CursorPagination>(stateName, () => ({ after: null, before: null, first: perPage, last: null }));

function firstPage() {
Object.assign(cursorPagination.value, { after: null, before: null, first: perPage, last: null });
}

function previousPage() {
if (!pageInfo.value?.hasPreviousPage) return;
Object.assign(cursorPagination.value, { after: null, before: pageInfo.value.startCursor, first: null, last: perPage });
}

function nextPage() {
if (!pageInfo.value?.hasNextPage) return;
Object.assign(cursorPagination.value, { after: pageInfo.value.endCursor, before: null, first: perPage, last: null });
}

return { cursorPagination, firstPage, previousPage, nextPage };
}
61 changes: 61 additions & 0 deletions graphql/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
enum AuthRole {
ADMINISTRATOR
UNVERIFIED
VERIFIED
}

input AuthRoleFilter {
equals: AuthRole
in: [AuthRole!]
notIn: [AuthRole!]
}

type AuthUser implements Node {
email: String!
globalId: ID!
id: ID!
role: AuthRole!
}

input AuthUserFilter {
role: AuthRoleFilter
}

input AuthUserOrderBy {
email: OrderBy
role: OrderBy
}

input AuthUserUniqueFilter {
email: String!
}

"""
A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.
"""
Expand All @@ -7,9 +39,38 @@ type Mutation {
ping: String!
}

interface Node {
globalId: ID!
}

enum OrderBy {
Asc
Desc
}

type PageInfo {
endCursor: String
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
}

type Query {
authUsers(after: String, before: String, first: Int, last: Int, orderBy: AuthUserOrderBy!, where: AuthUserFilter!): QueryAuthUsersConnection!

"""Current application version"""
version: String!
}

type QueryAuthUsersConnection {
edges: [QueryAuthUsersConnectionEdge]!
pageInfo: PageInfo!
totalCount: Int!
}

type QueryAuthUsersConnectionEdge {
cursor: String!
node: AuthUser!
}

scalar Upload
1 change: 1 addition & 0 deletions graphql/server/types.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./types/app-info";
export * from "./types/auth-user";
export * from "./types/scalars";
52 changes: 52 additions & 0 deletions graphql/server/types/auth-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { AuthRole } from "@prisma/client";

// AuthRole type and filter
export const AuthRoleEnumType = builder.enumType(AuthRole, { name: "AuthRole" });

export const AuthRoleFilter = builder.prismaFilter(AuthRole, {
ops: ["equals", "in", "notIn"],
});

// AuthUser Prisma node, Where and OrderBy inputs
export const AuthUserPrismaNode = builder.prismaNode("AuthUser", {
id: { field: "id" },
fields: (t) => ({
id: t.exposeID("id"),
email: t.exposeString("email"),
role: t.expose("role", { type: AuthRoleEnumType }),
}),
authScopes: { hasAuthRole: "ADMINISTRATOR" },
});

export const AuthUserUniqueFilter = builder.prismaWhereUnique("AuthUser", {
fields: (t) => ({
email: t.field({ type: "String", required: true }),
}),
});

export const AuthUserFilter = builder.prismaWhere("AuthUser", {
fields: (t) => ({
role: AuthRoleFilter,
}),
});

export const AuthUserOrderByInput = builder.prismaOrderBy("AuthUser", {
fields: {
email: true,
role: true,
},
});

export const AuthUserQueries = builder.queryFields((t) => ({
authUsers: t.prismaConnection({
type: "AuthUser",
cursor: "id",
args: {
where: t.arg({ type: AuthUserFilter, required: true }),
orderBy: t.arg({ type: AuthUserOrderByInput, required: true }),
},
totalCount: async (_root, { where }, { prisma }) => await prisma.authUser.count({ where }),
resolve: async (query, _root, { where, orderBy }, { prisma }) => await prisma.authUser.findMany({ ...query, where, orderBy }),
authScopes: { hasAuthRole: "ADMINISTRATOR" },
}),
}));
5 changes: 5 additions & 0 deletions graphql/utils/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-
const documents = {
"\n query Version {\n version\n }\n ": types.VersionDocument,
"\n mutation Ping {\n ping\n }\n ": types.PingDocument,
"\n fragment PageInfo on PageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n": types.PageInfoFragmentDoc,
};

/**
Expand All @@ -39,6 +40,10 @@ export function graphql(source: "\n query Version {\n version\n
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation Ping {\n ping\n }\n "): (typeof documents)["\n mutation Ping {\n ping\n }\n "];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment PageInfo on PageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n"): (typeof documents)["\n fragment PageInfo on PageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n"];

export function graphql(source: string) {
return (documents as any)[source] ?? {};
Expand Down
Loading

0 comments on commit f085f04

Please sign in to comment.