Skip to content

Commit

Permalink
feat: Make API extensible via npm packages (#67)
Browse files Browse the repository at this point in the history
Co-authored-by: Valia Fetisov <[email protected]>
  • Loading branch information
KirillDogadin-std and valiafetisov authored Nov 8, 2023
1 parent 42de81f commit fba28b4
Show file tree
Hide file tree
Showing 21 changed files with 5,451 additions and 44 deletions.
44 changes: 44 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,47 @@ The API authentication is implemented using "Sign-In with Ethereum" standard des
## Health endpoint

Endpoint available at `/healthz` path. Provides response if api is currently running and prisma (orm) is able to execute queries.

## Adding new modules

The api can be extended via modules developed separately and packaged via npm. The example of this approach is the [`module-example`](../module-example) folder found in the root of the project and installed via npm into the `api`.

In order to create a new external module, one have to:

- Create npm package which exports a single function `setup` ([see example](../module-example/index.ts))
- `setup` function would receive a single parameter: `prisma` – the [prisma client](https://www.prisma.io/docs/concepts/components/prisma-client#3-importing-prisma-client)
- `setup` function should return an object with 2 keys `{ extendedPrisma, resolvers }`
- `extendedPrisma` (optional) – an object with extended prisma client (see provided example)
- In case you want to not only query existing tables, but extend prisma client with new functions, you can use [prisma $extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions). This will allow other packages or core modules to use those new functions. Note that currently prisma do not support creating complete new tables via JS api, only adding models (functional extentions over the available methods)
- The prisma returned here will be later passed to the resolvers
- `resolvers` (optional) – an object with graphql types and resolvers defined via [`nexus`](https://www.npmjs.com/package/nexus). This allows you to define new graphq queries or mutations
- Note that `resolve` function inside the resolver will receive custom `ctx` object as a third parameter (i.e.: `(_root, args, ctx: Context)`). It is an object that provides (more info can be found in [`context.ts`](./src/graphql/context.ts)):
- `ctx.request` – pure `express` request object
- `ctx.prisma` – fully-extended (by all other packages) prisma client
- `ctx.getSession()` – function to get user session (or throw 401 error if it's not present). This function have to be called in case your resolver should only be accessible by registered users
- Install created package into the `api` project (i.e.: run `npm install package-name` or `npm install ../package-folder` it the package is local)
- Add default export from the created package into the `importedModules` array (inside [`importedModules.ts`](./src/importedModules.ts))
- Modify `preinstall` script found in `./api/package.json` if your package does not ship pre-built js files
- Start `api` as suggested above in the `Development Setup` section
- Open graphql playground found at http://localhost:3001/
- Make sure your endpoint is set to query `http://localhost:3001/graphql`
- Run a test query. In case of `module-example` that would be:
```gql
{
countUsers(message: "test") {
message
count
}
}
```
- In case of `module-example`, this should result in:
```json
{
"data": {
"countUsers": {
"message": "test",
"count": 1
}
}
}
```
84 changes: 84 additions & 0 deletions api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"type": "module",
"license": "AGPL-3",
"scripts": {
"preinstall": "cd ../module-example && npm ci",
"dev": "vite-node -w src/index.ts",
"start": "NODE_ENV=production vite-node src/index.ts",
"debug": "DEBUG=1 npm run dev",
Expand Down Expand Up @@ -35,6 +36,7 @@
"pino": "^8.11.0",
"pino-http": "^8.3.3",
"pino-pretty": "^10.0.0",
"module-example": "file:../module-example",
"siwe": "^2.1.4",
"vite-node": "^0.29.2",
"vitest": "^0.29.2",
Expand Down
4 changes: 2 additions & 2 deletions api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Express } from 'express';
import express from 'express';
import expressPlayground from 'graphql-playground-middleware-express';
import { getChildLogger } from './logger';
import prisma from './database';
import basePrisma from './database';
import { API_GQL_ENDPOINT } from './env';

const logger = getChildLogger({ msgPrefix: 'APP' });
Expand All @@ -15,7 +15,7 @@ export const createApp = (): Express => {
app.get('/healthz', async (_req, res) => {
try {
// TODO: after migration to postgres, do SELECT 1
await prisma.user.findFirst();
await basePrisma.user.findFirst();
} catch (error: any) {
return res.status(500).json({
status: `Failed database initialization check with error: ${error?.message}`,
Expand Down
47 changes: 32 additions & 15 deletions api/src/generated/nexus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ declare global {
/**
* Date custom scalar type
*/
date<FieldName extends string>(fieldName: FieldName, opts?: core.CommonInputFieldConfig<TypeName, FieldName>): void // "GQLDateBase";
date<FieldName extends string>(fieldName: FieldName, opts?: core.CommonInputFieldConfig<TypeName, FieldName>): void // "Date";
}
}
declare global {
interface NexusGenCustomOutputMethods<TypeName extends string> {
/**
* Date custom scalar type
*/
date<FieldName extends string>(fieldName: FieldName, ...opts: core.ScalarOutSpread<TypeName, FieldName>): void // "GQLDateBase";
date<FieldName extends string>(fieldName: FieldName, ...opts: core.ScalarOutSpread<TypeName, FieldName>): void // "Date";
/**
* Adds a Relay-style connection to the type, with numerous options for configuration
*
Expand Down Expand Up @@ -56,7 +56,7 @@ export interface NexusGenScalars {
Float: number
Boolean: boolean
ID: string
GQLDateBase: any
Date: any
}

export interface NexusGenObjects {
Expand All @@ -75,26 +75,30 @@ export interface NexusGenObjects {
name?: string | null; // String
shortCode?: string | null; // String
}
Counter: { // root type
count: number; // Int!
message: string; // String!
}
Mutation: {};
Query: {};
Session: { // root type
allowedOrigins?: string | null; // String
createdAt: NexusGenScalars['GQLDateBase']; // GQLDateBase!
createdAt: NexusGenScalars['Date']; // Date!
createdBy: string; // String!
id: string; // String!
isUserCreated: boolean; // Boolean!
name?: string | null; // String
referenceExpiryDate?: NexusGenScalars['GQLDateBase'] | null; // GQLDateBase
referenceExpiryDate?: NexusGenScalars['Date'] | null; // Date
referenceTokenId: string; // String!
revokedAt?: NexusGenScalars['GQLDateBase'] | null; // GQLDateBase
revokedAt?: NexusGenScalars['Date'] | null; // Date
}
SessionOutput: { // root type
session: NexusGenRootTypes['Session']; // Session!
token: string; // String!
}
User: { // root type
address: string; // String!
createdAt: NexusGenScalars['GQLDateBase']; // GQLDateBase!
createdAt: NexusGenScalars['Date']; // Date!
}
}

Expand Down Expand Up @@ -124,6 +128,10 @@ export interface NexusGenFieldTypes {
name: string | null; // String
shortCode: string | null; // String
}
Counter: { // field return type
count: number; // Int!
message: string; // String!
}
Mutation: { // field return type
createChallenge: NexusGenRootTypes['Challenge'] | null; // Challenge
createSession: NexusGenRootTypes['SessionOutput'] | null; // SessionOutput
Expand All @@ -133,27 +141,28 @@ export interface NexusGenFieldTypes {
Query: { // field return type
coreUnit: NexusGenRootTypes['CoreUnit'] | null; // CoreUnit
coreUnits: Array<NexusGenRootTypes['CoreUnit'] | null> | null; // [CoreUnit]
countUsers: NexusGenRootTypes['Counter'] | null; // Counter
me: NexusGenRootTypes['User'] | null; // User
sessions: Array<NexusGenRootTypes['Session'] | null> | null; // [Session]
}
Session: { // field return type
allowedOrigins: string | null; // String
createdAt: NexusGenScalars['GQLDateBase']; // GQLDateBase!
createdAt: NexusGenScalars['Date']; // Date!
createdBy: string; // String!
id: string; // String!
isUserCreated: boolean; // Boolean!
name: string | null; // String
referenceExpiryDate: NexusGenScalars['GQLDateBase'] | null; // GQLDateBase
referenceExpiryDate: NexusGenScalars['Date'] | null; // Date
referenceTokenId: string; // String!
revokedAt: NexusGenScalars['GQLDateBase'] | null; // GQLDateBase
revokedAt: NexusGenScalars['Date'] | null; // Date
}
SessionOutput: { // field return type
session: NexusGenRootTypes['Session']; // Session!
token: string; // String!
}
User: { // field return type
address: string; // String!
createdAt: NexusGenScalars['GQLDateBase']; // GQLDateBase!
createdAt: NexusGenScalars['Date']; // Date!
}
}

Expand All @@ -173,6 +182,10 @@ export interface NexusGenFieldTypeNames {
name: 'String'
shortCode: 'String'
}
Counter: { // field return type name
count: 'Int'
message: 'String'
}
Mutation: { // field return type name
createChallenge: 'Challenge'
createSession: 'SessionOutput'
Expand All @@ -182,27 +195,28 @@ export interface NexusGenFieldTypeNames {
Query: { // field return type name
coreUnit: 'CoreUnit'
coreUnits: 'CoreUnit'
countUsers: 'Counter'
me: 'User'
sessions: 'Session'
}
Session: { // field return type name
allowedOrigins: 'String'
createdAt: 'GQLDateBase'
createdAt: 'Date'
createdBy: 'String'
id: 'String'
isUserCreated: 'Boolean'
name: 'String'
referenceExpiryDate: 'GQLDateBase'
referenceExpiryDate: 'Date'
referenceTokenId: 'String'
revokedAt: 'GQLDateBase'
revokedAt: 'Date'
}
SessionOutput: { // field return type name
session: 'Session'
token: 'String'
}
User: { // field return type name
address: 'String'
createdAt: 'GQLDateBase'
createdAt: 'Date'
}
}

Expand All @@ -226,6 +240,9 @@ export interface NexusGenArgTypes {
coreUnit: { // args
id?: string | null; // String
}
countUsers: { // args
message: string; // String!
}
}
}

Expand Down
Loading

0 comments on commit fba28b4

Please sign in to comment.