Skip to content

Commit 5c4c7e0

Browse files
committed
feat: add basic graphql-http example
1 parent 5eb7eec commit 5c4c7e0

File tree

11 files changed

+310
-11
lines changed

11 files changed

+310
-11
lines changed

.changeset/config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@examples/graphql-typeorm-typescript",
2121
"@examples/graphql-server-typescript",
2222
"@examples/graphql-server-typescript-apollo",
23+
"@examples/graphql-server-typescript-basic",
2324
"@examples/magic-link-server-typescript",
2425
"@examples/react-graphql-typescript",
2526
"@examples/react-rest-typescript",

examples/graphql-server-typescript-apollo/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
"@accounts/module-mongo": "^0.34.0",
1515
"@accounts/module-password": "^0.34.0",
1616
"@accounts/password": "^0.32.2",
17-
"@accounts/rest-express": "^0.33.1",
1817
"@accounts/server": "^0.33.1",
1918
"@apollo/server": "4.9.4",
2019
"@apollo/server-plugin-landing-page-graphql-playground": "4.0.1",
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# graphql-server-typescript
2+
3+
This example demonstrate how to use [accounts-js](https://github.com/accounts-js/accounts).
4+
5+
## Setup example
6+
7+
In order to be able to run this example on your machine you first need to do the following steps:
8+
9+
- Clone the repository `git clone [email protected]:accounts-js/accounts.git`
10+
- Install project dependencies: `yarn install`
11+
- Compile the packages `yarn run compile`
12+
- Go to the example folder `cd examples/graphql-server-typescript`
13+
14+
## Prerequisites
15+
16+
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
17+
18+
```bash
19+
docker-compose up -d
20+
```
21+
22+
to start a new one.
23+
24+
## Getting Started
25+
26+
Start the app.
27+
28+
Visit http://localhost:4000/
29+
30+
```bash
31+
yarn run start
32+
```
33+
34+
-> [Start the client side](../react-graphql-typescript).
35+
36+
```graphql
37+
mutation CreateUser {
38+
createUser(
39+
user: { email: "[email protected]", password: "1234567", firstName: "John", lastName: "Doe" }
40+
)
41+
}
42+
43+
mutation Auth {
44+
authenticate(
45+
serviceName: "password"
46+
params: { password: "1234567", user: { email: "[email protected]" } }
47+
) {
48+
tokens {
49+
accessToken
50+
}
51+
}
52+
}
53+
54+
query Test {
55+
privateField
56+
}
57+
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
version: '3.6'
2+
services:
3+
db-mongo-accounts:
4+
image: mongo:3.6.5-jessie
5+
ports:
6+
- '27017:27017'
7+
restart: always
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "@examples/graphql-server-typescript-basic",
3+
"private": true,
4+
"version": "0.32.0",
5+
"main": "lib/index.js",
6+
"license": "MIT",
7+
"scripts": {
8+
"start": "NODE_ENV=development yarn run -T nodemon -w src -x ts-node src/index.ts",
9+
"build": "yarn run -T tsc",
10+
"test": "yarn run build"
11+
},
12+
"dependencies": {
13+
"@accounts/module-core": "^0.34.0",
14+
"@accounts/module-mongo": "^0.34.0",
15+
"@accounts/module-password": "^0.34.0",
16+
"@accounts/password": "^0.32.2",
17+
"@accounts/server": "^0.33.1",
18+
"@graphql-tools/merge": "9.0.0",
19+
"@graphql-tools/schema": "10.0.0",
20+
"graphql": "16.8.1",
21+
"graphql-http": "1.22.0",
22+
"graphql-modules": "3.0.0-alpha-20231010152921-a1eddea3",
23+
"graphql-tag": "2.12.6",
24+
"mongoose": "7.6.1",
25+
"tslib": "2.6.2"
26+
}
27+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import 'reflect-metadata';
2+
import {
3+
authenticated,
4+
buildSchema,
5+
context,
6+
createAccountsCoreModule,
7+
} from '@accounts/module-core';
8+
import { createAccountsPasswordModule } from '@accounts/module-password';
9+
import { AccountsPassword } from '@accounts/password';
10+
import { AccountsServer, AuthenticationServicesToken, ServerHooks } from '@accounts/server';
11+
import gql from 'graphql-tag';
12+
import mongoose from 'mongoose';
13+
import { createApplication } from 'graphql-modules';
14+
import { createAccountsMongoModule } from '@accounts/module-mongo';
15+
import { createHandler } from 'graphql-http/lib/use/http';
16+
import http from 'http';
17+
import { IContext } from '@accounts/types';
18+
19+
void (async () => {
20+
// Create database connection
21+
await mongoose.connect('mongodb://localhost:27017/accounts-js-graphql-example');
22+
const dbConn = mongoose.connection;
23+
24+
const typeDefs = gql`
25+
type PrivateType @auth {
26+
field: String
27+
}
28+
29+
# Our custom fields to add to the user
30+
extend input CreateUserInput {
31+
firstName: String!
32+
lastName: String!
33+
}
34+
35+
extend type User {
36+
firstName: String!
37+
lastName: String!
38+
}
39+
40+
extend type Query {
41+
# Example of how to get the userId from the context and return the current logged in user or null
42+
me: User
43+
publicField: String
44+
# You can only query this if you are logged in
45+
privateField: String @auth
46+
privateType: PrivateType
47+
privateFieldWithAuthResolver: String
48+
}
49+
50+
extend type Mutation {
51+
privateMutation: String @auth
52+
publicMutation: String
53+
}
54+
`;
55+
56+
// TODO: use resolvers typings from codegen
57+
const resolvers = {
58+
Query: {
59+
me: (_, __, ctx) => {
60+
// ctx.userId will be set if user is logged in
61+
if (ctx.userId) {
62+
// We could have simply returned ctx.user instead
63+
return ctx.injector.get(AccountsServer).findUserById(ctx.userId);
64+
}
65+
return null;
66+
},
67+
publicField: () => 'public',
68+
privateField: () => 'private',
69+
privateFieldWithAuthResolver: authenticated(() => {
70+
return 'private';
71+
}),
72+
privateType: () => ({
73+
field: () => 'private',
74+
}),
75+
},
76+
Mutation: {
77+
privateMutation: () => 'private',
78+
publicMutation: () => 'public',
79+
},
80+
};
81+
82+
const app = createApplication({
83+
modules: [
84+
createAccountsCoreModule({ tokenSecret: 'secret' }),
85+
createAccountsPasswordModule({
86+
// This option is called when a new user create an account
87+
// Inside we can apply our logic to validate the user fields
88+
validateNewUser: (user) => {
89+
if (!user.firstName) {
90+
throw new Error('First name required');
91+
}
92+
if (!user.lastName) {
93+
throw new Error('Last name required');
94+
}
95+
96+
// For example we can allow only some kind of emails
97+
if (user.email.endsWith('.xyz')) {
98+
throw new Error('Invalid email');
99+
}
100+
return user;
101+
},
102+
}),
103+
createAccountsMongoModule({ dbConn }),
104+
],
105+
providers: [
106+
{
107+
provide: AuthenticationServicesToken,
108+
useValue: { password: AccountsPassword },
109+
global: true,
110+
},
111+
],
112+
schemaBuilder: buildSchema({ typeDefs, resolvers }),
113+
});
114+
const { injector, createOperationController } = app;
115+
116+
// Create the GraphQL over HTTP Node request handler
117+
const handler = createHandler<Pick<IContext, keyof IContext>>({
118+
schema: app.schema,
119+
execute: app.createExecution(),
120+
context: (request) => context({ request }, { createOperationController }),
121+
});
122+
123+
injector.get(AccountsServer).on(ServerHooks.ValidateLogin, ({ user }) => {
124+
// This hook is called every time a user try to login.
125+
// You can use it to only allow users with verified email to login.
126+
// If you throw an error here it will be returned to the client.
127+
console.log(`${user.firstName} ${user.lastName} logged in`);
128+
});
129+
130+
// Create a HTTP server using the listener on `/graphql`
131+
const server = http.createServer((req, res) => {
132+
// Set CORS headers
133+
res.setHeader('Access-Control-Allow-Origin', '*');
134+
res.setHeader('Access-Control-Request-Method', '*');
135+
res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET');
136+
res.setHeader('Access-Control-Allow-Headers', '*');
137+
if (req.method === 'OPTIONS') {
138+
res.writeHead(200);
139+
res.end();
140+
return;
141+
}
142+
if (req.url?.startsWith('/graphql')) {
143+
handler(req, res);
144+
} else {
145+
res.writeHead(404).end();
146+
}
147+
});
148+
149+
server.listen(4000);
150+
console.log(`🚀 Server ready at http://localhost:4000/graphql`);
151+
})();
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"outDir": "./lib",
4+
"target": "es5",
5+
"lib": ["es2015", "esnext.asynciterable"],
6+
"sourceMap": true,
7+
"importHelpers": true,
8+
"esModuleInterop": true,
9+
"skipLibCheck": true
10+
},
11+
"include": ["./src/**/*"],
12+
"exclude": ["node_modules", "lib"]
13+
}

examples/graphql-server-typescript/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
"@accounts/module-mongo": "^0.34.0",
1515
"@accounts/module-password": "^0.34.0",
1616
"@accounts/password": "^0.32.2",
17-
"@accounts/rest-express": "^0.33.1",
1817
"@accounts/server": "^0.33.1",
1918
"@envelop/core": "4.0.3",
2019
"@envelop/graphql-modules": "5.0.3",

modules/module-core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"@graphql-tools/schema": "10.0.0",
6060
"@types/request-ip": "0.0.39",
6161
"graphql": "16.8.1",
62+
"graphql-http": "1.22.0",
6263
"graphql-modules": "3.0.0-alpha-20231010152921-a1eddea3",
6364
"graphql-tag": "2.12.6"
6465
}

modules/module-core/src/utils/context-builder.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import AccountsServer from '@accounts/server';
2-
import { IncomingMessage } from 'http';
2+
import { IncomingHttpHeaders, IncomingMessage } from 'http';
33
import { getClientIp } from 'request-ip';
44
import { IContext, User } from '@accounts/types';
55
import { Application } from 'graphql-modules';
6+
import { Request as RequestGraphqlHttp, RequestHeaders } from 'graphql-http';
7+
import { RequestContext } from 'graphql-http/lib/use/http';
8+
import http from 'http';
69

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

14-
function isFetchRequest(request: Request | IncomingMessage): request is Request {
15-
return (request as Request).headers.get != null;
17+
function isFetchHeaders(
18+
headers: Headers | IncomingHttpHeaders | RequestHeaders
19+
): headers is Exclude<
20+
Headers | IncomingHttpHeaders | RequestHeaders,
21+
IncomingHttpHeaders | { [key: string]: string | string[] | undefined }
22+
> {
23+
return headers.get != null;
1624
}
1725

18-
function getHeader(request: Request | IncomingMessage, headerName: string): string | null {
19-
const header = isFetchRequest(request)
26+
function getHeader(
27+
request: Request | IncomingMessage | RequestGraphqlHttp<http.IncomingMessage, RequestContext>,
28+
headerName: string
29+
): string | null {
30+
const header = isFetchHeaders(request.headers)
2031
? request.headers.get(headerName)
2132
: request.headers[headerName];
2233
if (Array.isArray(header)) {
@@ -36,7 +47,7 @@ export const context = async <IUser extends User = User, Ctx extends object = ob
3647
}
3748
| {
3849
req?: undefined;
39-
request: Request;
50+
request: Request | RequestGraphqlHttp<http.IncomingMessage, RequestContext>;
4051
},
4152
{ createOperationController, ctx, ...options }: AccountsContextOptions<Ctx>
4253
): AccountsContextOptions<Ctx> extends { ctx: any }
@@ -84,7 +95,12 @@ export const context = async <IUser extends User = User, Ctx extends object = ob
8495
//controller.destroy();
8596
}
8697

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

0 commit comments

Comments
 (0)