Skip to content

Commit

Permalink
feat: add basic graphql-http example
Browse files Browse the repository at this point in the history
  • Loading branch information
darkbasic committed Oct 13, 2023
1 parent 5eb7eec commit 5c4c7e0
Show file tree
Hide file tree
Showing 11 changed files with 310 additions and 11 deletions.
1 change: 1 addition & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@examples/graphql-typeorm-typescript",
"@examples/graphql-server-typescript",
"@examples/graphql-server-typescript-apollo",
"@examples/graphql-server-typescript-basic",
"@examples/magic-link-server-typescript",
"@examples/react-graphql-typescript",
"@examples/react-rest-typescript",
Expand Down
1 change: 0 additions & 1 deletion examples/graphql-server-typescript-apollo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"@accounts/module-mongo": "^0.34.0",
"@accounts/module-password": "^0.34.0",
"@accounts/password": "^0.32.2",
"@accounts/rest-express": "^0.33.1",
"@accounts/server": "^0.33.1",
"@apollo/server": "4.9.4",
"@apollo/server-plugin-landing-page-graphql-playground": "4.0.1",
Expand Down
57 changes: 57 additions & 0 deletions examples/graphql-server-typescript-basic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# graphql-server-typescript

This example demonstrate how to use [accounts-js](https://github.com/accounts-js/accounts).

## Setup example

In order to be able to run this example on your machine you first need to do the following steps:

- Clone the repository `git clone git@github.com:accounts-js/accounts.git`
- Install project dependencies: `yarn install`
- Compile the packages `yarn run compile`
- Go to the example folder `cd examples/graphql-server-typescript`

## Prerequisites

You will need a MongoDB server to run this server. If you don't have a MongoDB server running already, and you have Docker & Docker Compose, you can do

```bash
docker-compose up -d
```

to start a new one.

## Getting Started

Start the app.

Visit http://localhost:4000/

```bash
yarn run start
```

-> [Start the client side](../react-graphql-typescript).

```graphql
mutation CreateUser {
createUser(
user: { email: "john.does@john.com", password: "1234567", firstName: "John", lastName: "Doe" }
)
}

mutation Auth {
authenticate(
serviceName: "password"
params: { password: "1234567", user: { email: "john.does@john.com" } }
) {
tokens {
accessToken
}
}
}

query Test {
privateField
}
```
7 changes: 7 additions & 0 deletions examples/graphql-server-typescript-basic/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version: '3.6'
services:
db-mongo-accounts:
image: mongo:3.6.5-jessie
ports:
- '27017:27017'
restart: always
27 changes: 27 additions & 0 deletions examples/graphql-server-typescript-basic/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@examples/graphql-server-typescript-basic",
"private": true,
"version": "0.32.0",
"main": "lib/index.js",
"license": "MIT",
"scripts": {
"start": "NODE_ENV=development yarn run -T nodemon -w src -x ts-node src/index.ts",
"build": "yarn run -T tsc",
"test": "yarn run build"
},
"dependencies": {
"@accounts/module-core": "^0.34.0",
"@accounts/module-mongo": "^0.34.0",
"@accounts/module-password": "^0.34.0",
"@accounts/password": "^0.32.2",
"@accounts/server": "^0.33.1",
"@graphql-tools/merge": "9.0.0",
"@graphql-tools/schema": "10.0.0",
"graphql": "16.8.1",
"graphql-http": "1.22.0",
"graphql-modules": "3.0.0-alpha-20231010152921-a1eddea3",
"graphql-tag": "2.12.6",
"mongoose": "7.6.1",
"tslib": "2.6.2"
}
}
151 changes: 151 additions & 0 deletions examples/graphql-server-typescript-basic/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import 'reflect-metadata';
import {
authenticated,
buildSchema,
context,
createAccountsCoreModule,
} from '@accounts/module-core';
import { createAccountsPasswordModule } from '@accounts/module-password';
import { AccountsPassword } from '@accounts/password';
import { AccountsServer, AuthenticationServicesToken, ServerHooks } from '@accounts/server';
import gql from 'graphql-tag';
import mongoose from 'mongoose';
import { createApplication } from 'graphql-modules';
import { createAccountsMongoModule } from '@accounts/module-mongo';
import { createHandler } from 'graphql-http/lib/use/http';
import http from 'http';
import { IContext } from '@accounts/types';

void (async () => {
// Create database connection
await mongoose.connect('mongodb://localhost:27017/accounts-js-graphql-example');
const dbConn = mongoose.connection;

const typeDefs = gql`
type PrivateType @auth {
field: String
}
# Our custom fields to add to the user
extend input CreateUserInput {
firstName: String!
lastName: String!
}
extend type User {
firstName: String!
lastName: String!
}
extend type Query {
# Example of how to get the userId from the context and return the current logged in user or null
me: User
publicField: String
# You can only query this if you are logged in
privateField: String @auth
privateType: PrivateType
privateFieldWithAuthResolver: String
}
extend type Mutation {
privateMutation: String @auth
publicMutation: String
}
`;

// TODO: use resolvers typings from codegen
const resolvers = {
Query: {
me: (_, __, ctx) => {
// ctx.userId will be set if user is logged in
if (ctx.userId) {
// We could have simply returned ctx.user instead
return ctx.injector.get(AccountsServer).findUserById(ctx.userId);
}
return null;
},
publicField: () => 'public',
privateField: () => 'private',
privateFieldWithAuthResolver: authenticated(() => {
return 'private';
}),
privateType: () => ({
field: () => 'private',
}),
},
Mutation: {
privateMutation: () => 'private',
publicMutation: () => 'public',
},
};

const app = createApplication({
modules: [
createAccountsCoreModule({ tokenSecret: 'secret' }),
createAccountsPasswordModule({
// This option is called when a new user create an account
// Inside we can apply our logic to validate the user fields
validateNewUser: (user) => {
if (!user.firstName) {
throw new Error('First name required');
}
if (!user.lastName) {
throw new Error('Last name required');
}

// For example we can allow only some kind of emails
if (user.email.endsWith('.xyz')) {
throw new Error('Invalid email');
}
return user;
},
}),
createAccountsMongoModule({ dbConn }),
],
providers: [
{
provide: AuthenticationServicesToken,
useValue: { password: AccountsPassword },
global: true,
},
],
schemaBuilder: buildSchema({ typeDefs, resolvers }),
});
const { injector, createOperationController } = app;

// Create the GraphQL over HTTP Node request handler
const handler = createHandler<Pick<IContext, keyof IContext>>({
schema: app.schema,
execute: app.createExecution(),
context: (request) => context({ request }, { createOperationController }),
});

injector.get(AccountsServer).on(ServerHooks.ValidateLogin, ({ user }) => {
// This hook is called every time a user try to login.
// You can use it to only allow users with verified email to login.
// If you throw an error here it will be returned to the client.
console.log(`${user.firstName} ${user.lastName} logged in`);
});

// Create a HTTP server using the listener on `/graphql`
const server = http.createServer((req, res) => {
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Request-Method', '*');
res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET');
res.setHeader('Access-Control-Allow-Headers', '*');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
if (req.url?.startsWith('/graphql')) {
handler(req, res);
} else {
res.writeHead(404).end();
}
});

server.listen(4000);
console.log(`🚀 Server ready at http://localhost:4000/graphql`);
})();
13 changes: 13 additions & 0 deletions examples/graphql-server-typescript-basic/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"outDir": "./lib",
"target": "es5",
"lib": ["es2015", "esnext.asynciterable"],
"sourceMap": true,
"importHelpers": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["./src/**/*"],
"exclude": ["node_modules", "lib"]
}
1 change: 0 additions & 1 deletion examples/graphql-server-typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"@accounts/module-mongo": "^0.34.0",
"@accounts/module-password": "^0.34.0",
"@accounts/password": "^0.32.2",
"@accounts/rest-express": "^0.33.1",
"@accounts/server": "^0.33.1",
"@envelop/core": "4.0.3",
"@envelop/graphql-modules": "5.0.3",
Expand Down
1 change: 1 addition & 0 deletions modules/module-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@graphql-tools/schema": "10.0.0",
"@types/request-ip": "0.0.39",
"graphql": "16.8.1",
"graphql-http": "1.22.0",
"graphql-modules": "3.0.0-alpha-20231010152921-a1eddea3",
"graphql-tag": "2.12.6"
}
Expand Down
30 changes: 23 additions & 7 deletions modules/module-core/src/utils/context-builder.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import AccountsServer from '@accounts/server';
import { IncomingMessage } from 'http';
import { IncomingHttpHeaders, IncomingMessage } from 'http';
import { getClientIp } from 'request-ip';
import { IContext, User } from '@accounts/types';
import { Application } from 'graphql-modules';
import { Request as RequestGraphqlHttp, RequestHeaders } from 'graphql-http';
import { RequestContext } from 'graphql-http/lib/use/http';
import http from 'http';

export type AccountsContextOptions<Ctx extends object> = {
createOperationController: Application['createOperationController'];
Expand All @@ -11,12 +14,20 @@ export type AccountsContextOptions<Ctx extends object> = {
excludeAddUserInContext?: boolean;
};

function isFetchRequest(request: Request | IncomingMessage): request is Request {
return (request as Request).headers.get != null;
function isFetchHeaders(
headers: Headers | IncomingHttpHeaders | RequestHeaders
): headers is Exclude<
Headers | IncomingHttpHeaders | RequestHeaders,
IncomingHttpHeaders | { [key: string]: string | string[] | undefined }
> {
return headers.get != null;
}

function getHeader(request: Request | IncomingMessage, headerName: string): string | null {
const header = isFetchRequest(request)
function getHeader(
request: Request | IncomingMessage | RequestGraphqlHttp<http.IncomingMessage, RequestContext>,
headerName: string
): string | null {
const header = isFetchHeaders(request.headers)
? request.headers.get(headerName)
: request.headers[headerName];
if (Array.isArray(header)) {
Expand All @@ -36,7 +47,7 @@ export const context = async <IUser extends User = User, Ctx extends object = ob
}
| {
req?: undefined;
request: Request;
request: Request | RequestGraphqlHttp<http.IncomingMessage, RequestContext>;
},
{ createOperationController, ctx, ...options }: AccountsContextOptions<Ctx>
): AccountsContextOptions<Ctx> extends { ctx: any }
Expand Down Expand Up @@ -84,7 +95,12 @@ export const context = async <IUser extends User = User, Ctx extends object = ob
//controller.destroy();
}

const ip = getClientIp(req!); // TODO: we should be able to retrieve the ip from the request object as well
let ip: string | null = null;
try {
ip = getClientIp(req!); // TODO: we should be able to retrieve the ip from the fetch request object as well
} catch (e) {
console.error("Couldn't retrieve the IP from the headers");
}
const userAgent =
/* special case of UC Browser */ getHeader(reqOrRequest, 'x-ucbrowser-ua') ??
getHeader(reqOrRequest, 'user-agent') ??
Expand Down
Loading

0 comments on commit 5c4c7e0

Please sign in to comment.