Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make API extensible via npm packages #67

Merged
merged 38 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
b27a2e5
just plugin that mb works
KirillDogadin-std Sep 20, 2023
fb43f88
make modular
KirillDogadin-std Sep 20, 2023
187e587
adjust place of setup
KirillDogadin-std Sep 20, 2023
d9a3180
core unit mod
KirillDogadin-std Sep 20, 2023
a6c9cd9
add the core unit as mod
KirillDogadin-std Sep 20, 2023
c2f77bc
remove the 2nd example dir and remove extra deps
KirillDogadin-std Sep 21, 2023
ae7d149
remake example
KirillDogadin-std Sep 21, 2023
b4cbdf6
adjust the module to have different name and type
KirillDogadin-std Sep 21, 2023
b54876d
rm generated
KirillDogadin-std Sep 21, 2023
d48bc51
ignore generated
KirillDogadin-std Sep 21, 2023
7dfb331
remove extra extensions
KirillDogadin-std Sep 21, 2023
b55865f
rm prisma from deps
KirillDogadin-std Sep 21, 2023
2d33687
single setup place
KirillDogadin-std Sep 21, 2023
d843d90
Revert "rm prisma from deps"
KirillDogadin-std Sep 21, 2023
eb0eab6
poc add crud
KirillDogadin-std Sep 26, 2023
3eee231
lint in polish
KirillDogadin-std Sep 27, 2023
91fa1e6
lint in module
KirillDogadin-std Sep 27, 2023
b9d1e10
lint
KirillDogadin-std Sep 27, 2023
6272f83
typecheck
KirillDogadin-std Sep 27, 2023
97b9eed
some fixes
KirillDogadin-std Sep 27, 2023
2d1c79f
return the generated dir
KirillDogadin-std Sep 27, 2023
0b363ad
regenerage lock
KirillDogadin-std Sep 27, 2023
4eff304
rm gitignore
KirillDogadin-std Sep 27, 2023
7a5cb95
modify ci workflow
KirillDogadin-std Sep 27, 2023
f663e38
fix test?
KirillDogadin-std Sep 27, 2023
6b50c41
lint
KirillDogadin-std Sep 27, 2023
bd2922d
use prisma extension
KirillDogadin-std Sep 29, 2023
c465071
add preinstall script
valiafetisov Nov 7, 2023
62ab285
remove src/generated/nexus.ts
valiafetisov Nov 7, 2023
362ef96
refactor module import logic and add comments
valiafetisov Nov 8, 2023
c1aabed
remove @ts-ignore
valiafetisov Nov 8, 2023
c592ab4
try to update dateSchema to fix types
valiafetisov Nov 8, 2023
000d81b
make getExtraResolvers into object
valiafetisov Nov 8, 2023
93be093
check if pre-running helps
valiafetisov Nov 8, 2023
8c6872a
revert back
valiafetisov Nov 8, 2023
fae31f7
add back generated/nexus.ts as ci fails without it
valiafetisov Nov 8, 2023
b863e6a
add readme
valiafetisov Nov 8, 2023
3e9d12f
add a separate readme to the module-example
valiafetisov Nov 8, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
valiafetisov marked this conversation as resolved.
Show resolved Hide resolved
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