Skip to content

Commit

Permalink
chore: move from urql to houdini for graphql
Browse files Browse the repository at this point in the history
- toasts now have a small border on the bottom for added visibility
- with houdini came the removal of the local storage auth session token, meaning the source of truth for the session is now the cookie, happy days
  • Loading branch information
V-ed committed Aug 9, 2023
1 parent 20064af commit 043c156
Show file tree
Hide file tree
Showing 56 changed files with 1,086 additions and 946 deletions.
9 changes: 7 additions & 2 deletions .graphqlrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ const tsCommonConfig: RawTypesConfig = {
skipTypename: true,
};

const clientDocuments = ['client/src/**/*.graphql', 'client/src/**/*.svelte', 'client/src/**/*.server.ts', 'client/src/lib/urql.ts'];
const clientDocuments = [
'client/$houdini/graphql/documents.gql',
'client/src/**/*.graphql',
'client/src/**/*.svelte',
'client/src/**/*.server.ts',
];
const apiDocuments = ['api/tests/**/*.ts'];

const codegen: Types.Config = {
Expand Down Expand Up @@ -60,7 +65,7 @@ const codegen: Types.Config = {
};

const config: IGraphQLConfig = {
schema: `./api/src/_generated/nestjs-graphql/schema.gql`,
schema: ['./api/src/_generated/nestjs-graphql/schema.gql', './client/$houdini/graphql/schema.graphql'],
documents: [...clientDocuments, ...apiDocuments],
extensions: {
codegen,
Expand Down
10 changes: 8 additions & 2 deletions api/src/@common/users/users.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,14 @@ export class UsersResolver {

@Mutation(() => GetUserOutput, { nullable: true })
async deleteUser(@SelectQL() select: PrismaSelector, @Args('where') where: UserWhereUniqueInput) {
const user = await this.usersService.deleteUser(select, where);
try {
const user = await this.usersService.deleteUser(select, where);

return user;
return user;
} catch (error) {
const message = error instanceof Error ? error.message : 'Unhandled exception.';

throw new ForbiddenException(message);
}
}
}
12 changes: 12 additions & 0 deletions api/tests/@generated/graphql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ export type Scalars = {
DateTime: { input: any; output: any; }
};

export enum CachePolicy {
CacheAndNetwork = 'CacheAndNetwork',
CacheOnly = 'CacheOnly',
CacheOrNetwork = 'CacheOrNetwork',
NetworkOnly = 'NetworkOnly'
}

export type CreateUserOutput = {
/** Email of the created user */
email: Scalars['String']['output'];
Expand Down Expand Up @@ -247,6 +254,11 @@ export type NullableStringFieldUpdateOperationsInput = {
set?: InputMaybe<Scalars['String']['input']>;
};

export enum PaginateMode {
Infinite = 'Infinite',
SinglePage = 'SinglePage'
}

export type Query = {
getRoles: Array<Role>;
getSessionUser: User;
Expand Down
2 changes: 2 additions & 0 deletions client/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ vite.config.ts.timestamp-*

.devcontainer/volumes
.vercel

$houdini
24 changes: 24 additions & 0 deletions client/houdini.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/** @type {import('houdini').ConfigFile} */
const config = {
schemaPath: '../api/src/_generated/nestjs-graphql/schema.gql',
defaultCachePolicy: 'NetworkOnly',
plugins: {
'houdini-svelte': {
framework: 'kit',
client: './src/lib/houdini/client.ts',
},
},
scalars: {
DateTime: {
type: 'Date',
unmarshal(val) {
return val ? new Date(val) : null;
},
marshal(date) {
return date && date.getTime();
},
},
},
};

export default config;
10 changes: 5 additions & 5 deletions client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "client",
"description": "Web client using Sveltekit, Flowbite and supports GraphQL using URQL.",
"description": "Web client using Sveltekit, Flowbite and supports GraphQL using Houdini.",
"version": "0.0.0",
"private": true,
"type": "module",
Expand Down Expand Up @@ -36,8 +36,6 @@
},
"dependencies": {
"@mdi/js": "^7.2.96",
"@urql/exchange-auth": "2.1.6",
"@urql/svelte": "^4.0.2",
"graphql-ws": "^5.13.1",
"http-status-codes": "^2.2.0",
"set-cookie-parser": "2.6.0",
Expand All @@ -53,8 +51,8 @@
"@sveltejs/kit": "1.22.4",
"@types/set-cookie-parser": "2.4.3",
"@types/ws": "8.5.5",
"@typescript-eslint/eslint-plugin": "6.2.1",
"@typescript-eslint/parser": "6.2.1",
"@typescript-eslint/eslint-plugin": "6.3.0",
"@typescript-eslint/parser": "6.3.0",
"@v-ed/eslint-config": "0.1.6",
"@v-ed/prettier-config": "0.3.0",
"@v-ed/tsconfig": "0.3.0",
Expand All @@ -63,6 +61,8 @@
"flowbite": "1.8.1",
"flowbite-svelte": "0.41.2",
"flowbite-svelte-blocks": "0.3.9",
"houdini": "1.2.8",
"houdini-svelte": "1.2.8",
"npm-run-all": "4.1.5",
"postcss": "8.4.27",
"prettier": "3.0.1",
Expand Down
35 changes: 24 additions & 11 deletions client/src/app.d.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
import type { Client } from '@urql/svelte';
/* eslint-disable @typescript-eslint/no-empty-interface */

import type { ClientUser } from './hooks.server';
import type { LayoutAlertData } from './lib/components/LayoutAlert/helper';
import type { ToastData } from './lib/components/ToastManager/helper';
import type { createHoudiniHelpers } from './lib/houdini/helper';
import type { Theme } from './lib/stores';

export interface AppLocals {
gql: ReturnType<typeof createHoudiniHelpers>;
sessionUser: ClientUser;
theme?: Theme;
}

export interface AppPageData {
sessionUser: ClientUser;
layoutAlert: LayoutAlertData | undefined;
toasts: ToastData[];
}

export interface HoudiniSession {
token?: string;
}

// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
interface Locals {
urql: Client;
sessionUser: ClientUser;
theme?: Theme;
}
interface PageData {
sessionUser: ClientUser;
layoutAlert: LayoutAlertData | undefined;
toasts: ToastData[];
}
interface Locals extends AppLocals {}
interface PageData extends AppPageData {}
// interface Error {}
// interface Platform {}

// Houdini
interface Session extends HoudiniSession {}
}
}
130 changes: 13 additions & 117 deletions client/src/graphql/@generated/index.ts

Large diffs are not rendered by default.

37 changes: 20 additions & 17 deletions client/src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,44 @@
import { PUBLIC_API_ADDR } from '$env/static/public';
import { createClient } from '$lib/urql';
import type { Handle, HandleFetch, ResolveOptions } from '@sveltejs/kit';
import type { Client } from '@urql/svelte';
import { GetUserFromSessionStore, setSession } from '$houdini';
import type { Handle, HandleFetch } from '@sveltejs/kit';
import { parseString } from 'set-cookie-parser';
import ws from 'ws';
import { GetUserFromSessionDocument } from './graphql/@generated';
import type { AppLocals } from './app';
import { createHoudiniHelpers } from './lib/houdini/helper';
import { themeCookieName, themes, type Theme } from './lib/stores';
import { AUTH_COOKIE_NAME } from './lib/utils/auth';

export async function getAuthUser(urql: Client) {
const result = await urql.query(GetUserFromSessionDocument, {}).toPromise();
export async function getAuthUser(query: AppLocals['gql']['query']) {
const result = await query(GetUserFromSessionStore);

if (!result.data) {
if (result.type === 'failure') {
return null;
}

return {
...result.data.getSessionUser,
};
const { getSessionUser: sessionUser } = result.data;

return sessionUser;
}

export type ClientUser = Awaited<ReturnType<typeof getAuthUser>>;

export const handle: Handle = async ({ event, resolve }) => {
event.locals.urql = createClient({
fetch: event.fetch,
ws,
});
event.locals.gql = createHoudiniHelpers(event);

const token = event.cookies.get(AUTH_COOKIE_NAME);

event.locals.sessionUser = await getAuthUser(event.locals.urql);
setSession(event, { token });

if (token) {
event.locals.sessionUser = await getAuthUser(event.locals.gql.query);
}

const themeCookie = event.cookies.get(themeCookieName) as Theme | 'null' | undefined;

if (themeCookie) {
event.locals.theme = themeCookie != 'null' && themes.includes(themeCookie) ? themeCookie : undefined;
}

const opts: ResolveOptions | undefined = ['dark', undefined].includes(event.locals.theme)
const opts: Parameters<typeof resolve>[1] = ['dark', undefined].includes(event.locals.theme)
? {
transformPageChunk({ html }) {
return html.replace('<html lang="en">', '<html lang="en" class="dark">');
Expand Down
22 changes: 14 additions & 8 deletions client/src/lib/components/ToastManager/ToastManager.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@
import { Toast } from 'flowbite-svelte';
import { onDestroy } from 'svelte';
import Icon from '../Icon.svelte';
import { toastColorMapping, type ToastData } from './helper';
import { toastBorderColorMapping, toastColorMapping, type ToastData } from './helper';
const toastOffTimeout = 500;
export let data: ToastData[];
$: data.forEach(toast => {
if (toasts.some(existingToast => toast.id === existingToast.id)) {
$: data.forEach((toast) => {
if (toasts.some((existingToast) => toast.id === existingToast.id)) {
return;
}
toasts = [...toasts, toast];
});
$: toasts.forEach(toast => {
$: toasts.forEach((toast) => {
if (!browser || toast.timeoutId !== undefined || !toast.timeout) {
return;
}
Expand All @@ -27,15 +27,15 @@
toasts = [...toasts];
setTimeout(() => {
toast.markedForDeletion = true;
toasts = toasts.filter(toast => !toast.markedForDeletion);
toasts = toasts.filter((toast) => !toast.markedForDeletion);
}, toastOffTimeout);
}, toast.timeout);
});
let toasts: ToastData[] = [];
onDestroy(() => {
toasts.forEach(toast => {
toasts.forEach((toast) => {
clearTimeout(toast.timeoutId);
});
});
Expand All @@ -44,7 +44,11 @@
{#if toasts.length}
<div class="toast-manager">
{#each toasts as toast (toast.id)}
<Toast bind:open={toast.open} color={toastColorMapping[toast.type]} class="{toast.classes ? toast.classes : ''}">
<Toast
bind:open={toast.open}
color={toastColorMapping[toast.type]}
class={toast.classes ? toast.classes : `border-b-2 ${toastBorderColorMapping[toast.type]}`}
>
<svelte:fragment slot="icon">
{#if toast.icon}
<Icon path={toast.icon} />
Expand All @@ -64,5 +68,7 @@
display: flex;
flex-direction: column;
gap: 1rem;
z-index: 90000;
}
</style>
5 changes: 5 additions & 0 deletions client/src/lib/components/ToastManager/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,8 @@ export const toastColorMapping: Record<ToastAlertLevel, Toast['$$prop_def']['col
warning: 'yellow',
error: 'red',
};
export const toastBorderColorMapping: Record<ToastAlertLevel, string> = {
info: 'border-blue-400',
warning: 'border-yellow-400',
error: 'border-red-400',
};
41 changes: 41 additions & 0 deletions client/src/lib/houdini/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { HoudiniClient } from '$houdini';
import { subscription } from '$houdini/plugins';
import { createClient as createWSClient } from 'graphql-ws';
import { getApiUrl } from '../utils';

const apiUrl = getApiUrl();

export default new HoudiniClient({
url: `${apiUrl.origin}/graphql`,
fetchParams({ session }) {
if (!session?.token) {
return {};
}

return {
headers: {
Authorization: `Bearer ${session.token}`,
},
};
},
plugins: [
subscription(({ session }) => {
const wsProtocol = apiUrl.protocol == 'https:' ? 'wss' : 'ws';

const wsClient = createWSClient({
url: `${wsProtocol}://${apiUrl.host}/graphql`,
connectionParams() {
if (!session?.token) {
return {};
}

return {
Authorization: `Bearer ${session.token}`,
};
},
});

return wsClient;
}),
],
});
Loading

0 comments on commit 043c156

Please sign in to comment.