The core of the system that wraps around the business logic developed in a separate repository.
node -v
to check that your Node is LTS (currently version 18.*)- Set up required environment variables outlined below
cp developer.env .env
to copy default values
npm install
to install dependenciesnpx prisma db push
to create/update database structurenpm run dev
to run application in development mode- Open
http://localhost:3001
to see graphql explorer
- Set server to
http://localhost:3001/graphql
- Execute
query { coreUnits { id } }
to see a result
Other commands include:
npm run typecheck
for typecheckingnpm run lint
for lintingnpm run test
for testing via vitest
Note: you can set environment variables directly or define them in the api/.env
file. Default values for developement can be found in developer.env
file, to copy them, run: cp developer.env .env
DATABASE_URL
(required): path to the database file- Note: in order to use postgres, one have to 1) provide valid postgres url here 2) edit the primsa schema (eg:
sed --in-place 's/sqlite/postgresql/g' ../prisma/schema.prisma
)
- Note: in order to use postgres, one have to 1) provide valid postgres url here 2) edit the primsa schema (eg:
JWT_SECRET
(required): server's jwt secretPORT
(optional, default3001
): port on which the server will runAPI_ORIGIN
(optional, defaulthttp://0.0.0.0:${PORT}
): the URL at which the API is running. it's important to provide this variable in production since it influences the message signed during authorizationAPI_GQL_ENDPOINT
(optional, default${API_ORIGIN}/graphql
): the graphql URL which will be used in the graphql playground as a server addressAUTH_SIGNUP_ENABLED
(optional, default:false
): if signing up is allowed. In case it's not set, new users cannot be created, but old users can still sign inJWT_EXPIRATION_PERIOD
(optional, default:'7d'
): how soon JWT token will expireDEBUG
(optional, default not set): if set, enables more explicit logging mode where debug levels are set todebug
for the app's logger andquery
for db logger
We use Prisma ORM as an ORM for this project. It is installed when you run npm i
. Here are some useful commands for development:
npx prisma db push
– push the current database schema to the database. This will also automatically generate the prisma clientnpx prisma generate
- create the typescript database client from theschema.prisma
filenpx prisma studio
– get a live-view of the database, useful for development and testing
Note: we use sqlite in this project during development, but automatically switch it to postgres inside docker. In order to use postges, one have to 1) set DATABASE_URL
to be valid postgres url 2) change provider = "sqlite"
to provider = "postgresql"
inside prisma schema
The configuration is received from the logger.config.ts
file at the root of the project. Adjust the file parameters to control the logger behaviour.
The API authentication is implemented using "Sign-In with Ethereum" standard described in EIP-4361. Basically, in order to get a usual JWT token, one have to sign a message provided by the API using their etherium wallet. In practice it means:
-
Create challenge via executing
createChallenge
graphql mutation-
Example request (that have to contain your public etherium address)
mutation { createChallenge(address: "paste_your_ethereum_address") { nonce message hex } }
-
Example response (that contains hex-encoded message)
{ "data": { "createChallenge": { "nonce": "6f4c7f7cd61a499290e68a2740957407", "message": "example.com wants you to sign in with your Ethereum account...", "hex": "0x302e302e302e302077616e74732029..." } } }
Where
hex
is just hex-encodedmessage
that actually needs to be signed
-
-
Sign provided message
-
Either using your metamask wallet
// those commands should be executed in the browser console of the graphql playground const addresses = await provider.send('eth_requestAccounts', []); await ethereum.request({ method: 'personal_sign', params: ['paste_hex_from_the_above', addresses[0]] });
-
Or using foundry command line tool called
cast
(note: you will be asked for your private key; for other auth methods, read the cli docs)$ cast wallet sign -i "hex_from_the_above"
-
-
Provide signature back to the API to get usual JWT token back
-
Example request
mutation { solveChallenge( nonce: "paste_nonce_from_step_1" signature: "paste_signature_from_step_2" ) { token } }
-
Example response
{ "data": { "solveChallenge": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXNzaW9uSWQiOiI5ZGM1NjI3Mi1hMjBjLTRmM2YtYjM5MC1kZDc2NjE1NTA0YTYiLCJpYXQiOjE2ODczMzc2MDEsImV4cCI6MTY4Nzk0MjQwMX0.z1lJlKXnCbcex59JkU9j7hfRGhR2EBrnUE8phwPN7C0" } } }
-
-
Use provided JWT token to make subsequent API requests
- Either sent as
Authorization: Bearer paste_token_from_step_3
- Or set as
Authorization
cookie - Example request
query { me { address } }
- Either sent as
Endpoint available at /healthz
path. Provides response if api is currently running and prisma (orm) is able to execute queries.
The api can be extended via modules developed separately and packaged via npm. The example of this approach is the 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)setup
function would receive a single parameter:prisma
– the prisma clientsetup
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. 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 vianexus
. This allows you to define new graphq queries or mutations- Note that
resolve
function inside the resolver will receive customctx
object as a third parameter (i.e.:(_root, args, ctx: Context)
). It is an object that provides (more info can be found incontext.ts
):ctx.request
– pureexpress
request objectctx.prisma
– fully-extended (by all other packages) prisma clientctx.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
- Note that
- Install created package into the
api
project (i.e.: runnpm install package-name
ornpm install ../package-folder
it the package is local) - Add default export from the created package into the
importedModules
array (insideimportedModules.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 theDevelopment Setup
section - Open graphql playground found at http://localhost:3001/
- Make sure your endpoint is set to query
http://localhost:3001/graphql
- Make sure your endpoint is set to query
- Run a test query. In case of
module-example
that would be:{ countUsers(message: "test") { message count } }
- In case of
module-example
, this should result in:{ "data": { "countUsers": { "message": "test", "count": 1 } } }