@niledatabase/server
Installation and configuration
Install the SDK:
npm install @niledatabase/server
yarn install @niledatabase/server
Set an .env file with database credentials
NILEDB_USER=
NILEDB_PASSWORD=
Then import the SDK:
import Nile from "@niledatabase/server";
const nile = await Nile();
This creates an instance of Nile Server, which allows you to interact with Nile APIs and DB.
Configuration
If you would like to pass a configuration manually, that can be done by passing the config to Nile
. Configs passed in this way take precedence over .env
vars.
Property | Type | .env var | Description |
---|---|---|---|
user | string | NILEDB_USER | Required. Username for database authentication. |
password | string | NILEDB_PASSWORD | Required. Password for database authentication. |
databaseId | string | NILEDB_ID | ID of the database. |
databaseName | string | NILEDB_NAME | Name of the database. |
tenantId | string | NILEDB_TENANT | ID of the tenant associated. |
userId | string | ID of the user associated. | |
db | PoolConfig | Configuration object for pg.Pool. | |
api | object | Configuration object for API settings. | |
api.basePath | string | NILEDB_API | Base host for API for a specific region. Default is
|
api.cookieKey | string | Key for API cookie. Default is | |
api.token | string | NILEDB_TOKEN | Token for API authentication. Mostly for debugging. |
debug | boolean | Flag for enabling debug logging. |
Virtual Tenant Databases
In order to connect to Nile's virtual tenant databases and enjoy the full isolation and security, you need to get a reference to tenant-specific instance of Nile Server. This function will either create a new client connection based on the config, or return the existing instance if it has already been registered.
nile.getInstance({
tenantId: tenantId,
userId: userId,
api: {
token: token,
},
});
userId
and token
are usually obtained from a cookie that is set during authentication. tenantId
is the id of the tenant you want to connect to,
and can be set and obtained in the path parameters, query parameters, or headers of the request. In our examples, we use path parameters for the tenant id.
Handling of both cookies and path parameters is framework-specific.
import { cookies } from 'next/headers';
export default async function Page({ params }: { params: { tenantid: string } }) {
// First, initialize global Nile instance if needed
const nile = await Nile()
const parsedCookies = JSON.parse(String(cookies().get('authData')?.value));
const tenantNile = nile.getInstance({
tenantId: params.tenantid,
userId: parsedCookies.tokenData.sub,
api: {
token: parsedCookies.accessToken,
}
})
}
import Nile from "@niledatabase/server";
import cookieParser from "cookie-parser";
// First, initialize global Nile instance if needed
const nile = await Nile();
app.use(cookieParser());
app.use('/api/tenants', (req, res, next) => {
const parsedCookies = JSON.parse(req.cookies.authData);
nile.userId = parsedCookies.tokenData.sub,
nile.token = parsedCookies.accessToken,
next();
});
// set the tenant ID in the context based on the URL parameter - this runs after the auth middleware
app.param('tenantId', (req, res, next, tenantId) => {
nile.tenantId = tenantId;
next();
});
When you work with tenant-specific references, you can use the same APIs as with the global instance, but they will be scoped to the tenant and user you specified. Behind the scenes, the Nile SDK manages the connections to the tenant-specific database, and the authentication to the API.
You can leave tenantId
, userId
or token
as undefined
- in that case, the SDK will use the default configuration.
In our examples, after a user logs in and before they pick a tenant, we set the userId
and token
, but leave tenantId
as undefined
.
This lets us call the createTenant
API as the authicated user.
nile.getInstance({
tenantId: undefined,
userId: parsedCookies.tokenData.sub,
api: {
token: parsedCookies.accessToken,
},
});
APIs
Overview
Nile SDK provides a set of APIs that you can use to interact with Nile. The inputs and outputs of these APIs are all generated from the OpenAPI spec. You can find the latest spec here. The API is documented in the API Reference.
All inputs are extensions of Request type, and all outputs are extensions of Response type. This allows us to pass requests from the browser directly to Nile and return the response to the browser.
This is a valid API handler in NextJS:
export async function POST(req: Request) {
return await api.auth.login(req);
}
However, you should examine the response and handle errors appropriately.
Error handling
When Nile returns an error response (40X or 500), the response body will be text, not JSON and contain the error message.
Therefore you need to either check the status code before calling response.json
or handle the exception that response.json
will throw when called on an error response.
Most APIs will respond with 401 Unauthorized
if nile.token
is unset, invalid, not signed by Nile, or expired. The exception are login and signup APIs which don't require a token.
An example of handling errors can be something like this. We use create tenant
as an example, but this applies to all APIs:
try {
const createTenantResponse = await nile.api.tenants.createTenant({
name: name,
});
if (tenantResponse.status === 401) {
return res.status(401).json({
message: "Unauthorized. Please log in again.",
});
}
const tenant = await createTenantResponse.json();
res.json(tenant);
} catch (error: any) {
console.log("error creating tenant: " + error.message);
res.status(500).json({
message: "Internal Server Error",
});
}
Create Tenant
You call createTenant
with the name of the tenant you want to create.
This will create a tenant, and the current user (based on nile.token
or nile.api.token
) will be a member of that tenant.
const createTenantResponse = await nile.api.tenants.createTenant({
name: name,
});
const tenant = await createTenantResponse.json();
res.json(tenant);
Get Tenant
This API call doesn't need any input parameters because it uses the tenant ID from the context and returns the current tenant.
const tenantResponse = await nile.api.tenants.getTenant();
const tenant = await tenantResponse.json();
res.json(tenant);
User signup
Signing up a user is a two-step process. First, you call signUp
with the email and password of the user you want to create, and optionally a user name:
app.post("/api/sign-up", async (req, res) => {
const resp = await nile.api.auth.signUp({
email: req.body.email,
password: req.body.password,
preferredName: req.body.preferredName, // optional
});
});
When the request succeeds, the response will include a JWT token that you can then use for this user. Usually, we set this token in a cookie and include it in the response to the browser:
const body = await resp.json();
const accessToken = body.token.jwt;
const decodedJWT = jwtDecode(accessToken);
const cookieData = {
accessToken: accessToken,
tokenData: decodedJWT,
};
res.cookie("authData", JSON.stringify(cookieData), {
secure: process.env.NODE_ENV !== "development",
});
res.status(resp.status).json(JSON.stringify(body));
User login
User login is nearly identical to signup, but you only need to provide the email and password.
app.post("/api/login", async (req, res) => {
const resp = await nile.api.auth.login({
email: req.body.email,
password: req.body.password,
});
});
Similarly, if the login request succeeds, we should set the token in a cookie and include it in the response to the browser:
const body = await resp.json();
const accessToken = body.token.jwt;
const decodedJWT = jwtDecode(accessToken);
const cookieData = {
accessToken: accessToken,
tokenData: decodedJWT,
};
res.cookie("authData", JSON.stringify(cookieData), {
secure: process.env.NODE_ENV !== "development",
});
res.status(resp.status).json(JSON.stringify(body));
Note that we are returning a response to the browser and expect it to redirect to the correct post-login page, based on whether login succeeds or fails.
Query Builder
Nile SDK includes a query builder and a connection pool that were designed to work with the Nile's virtual tenant databases with minimal overhead. You can use Nile with any ORM or database client that you prefer, but using the query builder will give you the best developer experience.
The query builder is built on top of pg-node, so the documentation for pg-node applies to Nile's query builder as well.
Connecting to the database
When you initialize the Nile Server object, you can pass any valid pg-node pool configuration object as the db
parameter.
One special case is the afterCreate
method, which will configure the context of the pool connections for a specific tenantId
and userId
if they are provided.
For example:
import Nile from "@niledatabase/server";
export const nile = await Nile({
user: process.env.NILEDB_USER,
password: process.env.NILEDB_PASSWORD,
});
Because Nile manages the connection pool, we recommend not overriding the pool
settings unless you are sure you know what you are doing.
Querying the database
The query builder is available as nile.db
. You can use it to query the database directly, or to build a model layer on top of it. It works exactly like pg-node.
For example, to get all tenants that the current user is a member of:
const tenants = await nile.db.query(
`SELECT
id, name
FROM
tenants JOIN users.tenant_users ON tenants.id = tenant_users.id
WHERE tenant_users.id = $1`,
[nile.userId]
);
In order to query a virtual tenant database, you need to use a reference that you obtained from nile.getInstance
with the current tenantId
, userId
and token
.
For example, the following query will all rows in todos
table for the current tenant:
const todos = await nile.db.query("SELECT * from todos ORDER BY title");
This also applies to inserts, updates and deletes. For example, to update a todo:
const { id, complete } = req.body;
await nile.db.query("UPDATE todos SET complete = $1 WHERE id = $2", [
complete,
id,
]);
and to create a new one:
const { title, complete } = req.body;
const newTodo = await nile.query(
`INSERT INTO todos (title, complete, tenant_id)
VALUES ($1, $2, $3)
RETURNING *`,
[title, Boolean(complete), nile.tenantId]
);