Merge pull request #7 from lysand-org/main

v0.1.2
This commit is contained in:
Gaspard Wierzbinski 2023-12-01 13:12:42 -10:00 committed by GitHub
commit 2fb3e5c529
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 676 additions and 49 deletions

View file

@ -67,8 +67,14 @@ RUN chmod +x /docker-entrypoint-initdb.d/init.sh
```
4. Copy the `config.toml.example` file to `config.toml` and fill in the values (you can leave most things to the default, but you will need to configure things such as the database connection)
5. Generate the Prisma client:
5. Run migrations:
```bash
bun prisma generate
```
6. Run migrations:
```bash
bun migrate
@ -130,9 +136,9 @@ When you are done with your changes, you can open a pull request. Please make su
We use Bun's integrated testing system to write tests. You can find more information about it [here](https://bun.sh/docs/cli/test). It uses a Jest-like syntax.
Tests **must** be written for all API routes and all functions that are not trivial. If you are not sure whether you should write a test for something, you probably should.
Tests **should** be written for all API routes and all functions that are not trivial. If you are not sure whether you should write a test for something, you probably should.
To help with the creation of tests, you may find [GitHub Copilot](https://copilot.github.com/) useful (or some of its free alternatives like [Codeium](https://codeium.com/)). Please do not blindly copy the code that it generates, but use it as a starting point for your own tests.
To help with the creation of tests, you may find [GitHub Copilot](https://copilot.github.com/) useful (or some of its free alternatives like [Codeium](https://codeium.com/)). Please do not blindly copy the code that it generates, but use it as a starting point for your own tests. I recognize that writing tests is very tedious, which is why LLMs can come in handy.
### Writing documentation

View file

@ -9,13 +9,14 @@
This is a project to create a federated social network based on the [Lysand](https://lysand.org) protocol. It is currently in alpha phase, with basic federation and API support.
This project aims to be a fully featured social network, with a focus on privacy and security. It will implement the Mastodon API for support with clients that already support Mastodon or Pleroma.
This project aims to be a fully featured social network, with a focus on privacy, security, and performance. It will implement the Mastodon API for support with clients that already support Mastodon or Pleroma.
> **Note:** This project is not affiliated with Mastodon or Pleroma, and is not a fork of either project. It is a new project built from the ground up.
## Features
- [x] Inbound federation
- [x] Hyper fast (thousands of HTTP requests per second)
- [x] S3 or local media storage
- [x] Deduplication of uploaded files
- [x] Federation limits
@ -23,10 +24,47 @@ This project aims to be a fully featured social network, with a focus on privacy
- [x] Full regex-based filters for posts, users and media
- [x] Custom emoji support
- [x] Automatic image conversion to WebP or other formats
- [x] Scripting-compatible CLI with JSON and CSV outputs
- [ ] Moderation tools
- [ ] Full Mastodon API support
- [ ] Outbound federation
## Benchmarks
> **Note**: These benchmarks are not representative of real-world performance, and are only meant to be used as a rough guide.
### Timeline Benchmarks
You may run the following command to benchmark the `/api/v1/timelines/home` endpoint:
```bash
TOKEN=token_here bun benchmark:timeline <request_count>
```
The `request_count` variable is optional and defaults to 100. `TOKEN` is your personal user token, used to login to the API.
On a quad-core laptop:
```
$ bun run benchmarks/timelines.ts 100
✓ All requests succeeded
✓ 100 requests fulfilled in 0.12611s
```
```
$ bun run benchmarks/timelines.ts 1000
✓ All requests succeeded
✓ 1000 requests fulfilled in 0.90925s
```
```
$ bun run benchmarks/timelines.ts 10000
✓ All requests succeeded
✓ 10000 requests fulfilled in 12.44852s
```
Lysand is extremely fast and can handle tens of thousands of HTTP requests per second on a good server.
## How do I run it?
### Requirements
@ -105,6 +143,12 @@ bun cli
You can use the `help` command to see a list of available commands. These include creating users, deleting users and more.
#### Scripting with the CLI
Some CLI commands that return data as tables can be used in scripts. To do so, you can use the `--json` flag to output the data as JSON instead of a table, or even `--csv` to output the data as CSV. See `bun cli help` for more information.
Flags can be used in any order and anywhere in the script (except for the `bun cli` command itself). The command arguments themselves must be in the correct order, however.
### Using Database Commands
The `bun prisma` commands allows you to use Prisma commands without needing to add in environment variables for the database config. Just run Prisma commands as you would normally, replacing `bunx prisma` with `bun prisma`.

56
benchmarks/timelines.ts Normal file
View file

@ -0,0 +1,56 @@
/**
* Usage: TOKEN=your_token_here bun benchmark:timeline <request_count>
*/
import { getConfig } from "@config";
import chalk from "chalk";
const config = getConfig();
const token = process.env.TOKEN;
const requestCount = Number(process.argv[2]) || 100;
if (!token) {
console.log(
`${chalk.red(
"✗"
)} No token provided. Provide one via the TOKEN environment variable.`
);
process.exit(1);
}
const fetchTimeline = () =>
fetch(`${config.http.base_url}/api/v1/timelines/home`, {
headers: {
Authorization: `Bearer ${token}`,
},
}).then(res => res.ok);
const timeNow = performance.now();
const requests = Array.from({ length: requestCount }, () => fetchTimeline());
Promise.all(requests)
.then(results => {
const timeTaken = performance.now() - timeNow;
if (results.every(t => t)) {
console.log(`${chalk.green("✓")} All requests succeeded`);
} else {
console.log(
`${chalk.red("✗")} ${
results.filter(t => !t).length
} requests failed`
);
}
console.log(
`${chalk.green("✓")} ${
requests.length
} requests fulfilled in ${chalk.bold(
(timeTaken / 1000).toFixed(5)
)}s`
);
})
.catch(err => {
console.log(`${chalk.red("✗")} ${err}`);
process.exit(1);
});

BIN
bun.lockb

Binary file not shown.

386
cli.ts
View file

@ -1,43 +1,92 @@
import type { Prisma } from "@prisma/client";
import chalk from "chalk";
import { client } from "~database/datasource";
import { createNewLocalUser } from "~database/entities/User";
import Table from "cli-table";
const args = process.argv;
/**
* Make the text have a width of 20 characters, padding with gray dots
* Text can be a Chalk string, in which case formatting codes should not be counted in text length
* @param text The text to align
*/
const alignDots = (text: string, length = 20) => {
// Remove formatting codes
// eslint-disable-next-line no-control-regex
const textLength = text.replace(/\u001b\[\d+m/g, "").length;
const dots = ".".repeat(length - textLength);
return `${text}${chalk.gray(dots)}`;
};
const alignDotsSmall = (text: string, length = 16) => alignDots(text, length);
const help = `
${chalk.bold(`Usage: bun cli <command> ${chalk.blue("[...flags]")} [...args]`)}
${chalk.bold("Commands:")}
${chalk.blue("help")} ${chalk.gray(
"................."
)} Show this help message
${chalk.blue("user")} ${chalk.gray(".................")} Manage users
${chalk.blue("create")} ${chalk.gray("...........")} Create a new user
${chalk.green("username")} ${chalk.gray(
"....."
)} Username of the user
${chalk.green("password")} ${chalk.gray(
"....."
)} Password of the user
${chalk.green("email")} ${chalk.gray("........")} Email of the user
${chalk.yellow("--admin")} ${chalk.gray(
"......"
${alignDots(chalk.blue("help"), 24)} Show this help message
${alignDots(chalk.blue("user"), 24)} Manage users
${alignDots(chalk.blue("create"))} Create a new user
${alignDotsSmall(chalk.green("username"))} Username of the user
${alignDotsSmall(chalk.green("password"))} Password of the user
${alignDotsSmall(chalk.green("email"))} Email of the user
${alignDotsSmall(
chalk.yellow("--admin")
)} Make the user an admin (optional)
${chalk.bold("Example:")} ${chalk.bgGray(
`bun cli user create admin password123 admin@gmail.com --admin`
)}
${chalk.blue("delete")} ${chalk.gray("...........")} Delete a user
${chalk.green("username")} ${chalk.gray(
"....."
)} Username of the user
${alignDots(chalk.blue("delete"))} Delete a user
${alignDotsSmall(chalk.green("username"))} Username of the user
${chalk.bold("Example:")} ${chalk.bgGray(
`bun cli user delete admin`
)}
${chalk.blue("list")} ${chalk.gray(".............")} List all users
${chalk.yellow("--admins")} ${chalk.gray(
"....."
${alignDots(chalk.blue("list"))} List all users
${alignDotsSmall(
chalk.yellow("--admins")
)} List only admins (optional)
${chalk.bold("Example:")} ${chalk.bgGray(`bun cli user list`)}
${alignDots(chalk.blue("search"))} Search for a user
${alignDotsSmall(chalk.green("query"))} Query to search for
${alignDotsSmall(
chalk.yellow("--displayname")
)} Search by display name (optional)
${alignDotsSmall(chalk.yellow("--bio"))} Search in bio (optional)
${alignDotsSmall(
chalk.yellow("--local")
)} Search in local users (optional)
${alignDotsSmall(
chalk.yellow("--remote")
)} Search in remote users (optional)
${alignDotsSmall(
chalk.yellow("--email")
)} Search in emails (optional)
${alignDotsSmall(chalk.yellow("--json"))} Output as JSON (optional)
${alignDotsSmall(chalk.yellow("--csv"))} Output as CSV (optional)
${chalk.bold("Example:")} ${chalk.bgGray(
`bun cli user search admin`
)}
${alignDots(chalk.blue("note"), 24)} Manage notes
${alignDots(chalk.blue("delete"))} Delete a note
${alignDotsSmall(chalk.green("id"))} ID of the note
${chalk.bold("Example:")} ${chalk.bgGray(
`bun cli note delete 018c1838-6e0b-73c4-a157-a91ea4e25d1d`
)}
${alignDots(chalk.blue("search"))} Search for a status
${alignDotsSmall(chalk.green("query"))} Query to search for
${alignDotsSmall(
chalk.yellow("--local")
)} Search in local statuses (optional)
${alignDotsSmall(
chalk.yellow("--remote")
)} Search in remote statuses (optional)
${alignDotsSmall(chalk.yellow("--json"))} Output as JSON (optional)
${alignDotsSmall(chalk.yellow("--csv"))} Output as CSV (optional)
${chalk.bold("Example:")} ${chalk.bgGray(
`bun cli note search hello`
)}
`;
if (args.length < 3) {
@ -141,6 +190,7 @@ switch (command) {
where: {
isAdmin: admins || undefined,
},
take: 200,
});
console.log(
@ -158,11 +208,305 @@ switch (command) {
}
break;
}
case "search": {
const argsWithoutFlags = args.filter(
arg => !arg.startsWith("--")
);
const query = argsWithoutFlags[4];
if (!query) {
console.log(`${chalk.red(``)} Missing query`);
process.exit(1);
}
const displayname = args.includes("--displayname");
const bio = args.includes("--bio");
const local = args.includes("--local");
const remote = args.includes("--remote");
const email = args.includes("--email");
const json = args.includes("--json");
const csv = args.includes("--csv");
const queries: Prisma.UserWhereInput[] = [];
if (displayname) {
queries.push({
displayName: {
contains: query,
mode: "insensitive",
},
});
}
if (bio) {
queries.push({
note: {
contains: query,
mode: "insensitive",
},
});
}
if (local) {
queries.push({
instanceId: null,
});
}
if (remote) {
queries.push({
instanceId: {
not: null,
},
});
}
if (email) {
queries.push({
email: {
contains: query,
mode: "insensitive",
},
});
}
const users = await client.user.findMany({
where: {
AND: queries,
},
include: {
instance: true,
},
take: 40,
});
if (json || csv) {
if (json) {
console.log(JSON.stringify(users, null, 4));
}
if (csv) {
// Convert the outputted JSON to CSV
// Remove all object children from each object
const items = users.map(user => {
const item = {
...user,
instance: undefined,
endpoints: undefined,
source: undefined,
};
return item;
});
const replacer = (key: string, value: any): any =>
value === null ? "" : value; // Null values are returned as empty strings
const header = Object.keys(items[0]);
const csv = [
header.join(","), // header row first
...items.map(row =>
header
.map(fieldName =>
// @ts-expect-error This is fine
JSON.stringify(row[fieldName], replacer)
)
.join(",")
),
].join("\r\n");
console.log(csv);
}
} else {
console.log(
`${chalk.green(``)} Found ${chalk.blue(
users.length
)} users`
);
const table = new Table({
head: [
chalk.white(chalk.bold("Username")),
chalk.white(chalk.bold("Email")),
chalk.white(chalk.bold("Display Name")),
chalk.white(chalk.bold("Admin?")),
chalk.white(chalk.bold("Instance URL")),
],
});
for (const user of users) {
table.push([
chalk.yellow(`@${user.username}`),
chalk.green(user.email),
chalk.blue(user.displayName),
chalk.red(user.isAdmin ? "Yes" : "No"),
chalk.blue(
user.instanceId
? user.instance?.base_url
: "Local"
),
]);
}
console.log(table.toString());
}
break;
}
default:
console.log(`Unknown command ${chalk.blue(command)}`);
break;
}
break;
case "note": {
switch (args[3]) {
case "delete": {
const id = args[4];
if (!id) {
console.log(`${chalk.red(``)} Missing ID`);
process.exit(1);
}
const note = await client.status.findFirst({
where: {
id: id,
},
});
if (!note) {
console.log(`${chalk.red(``)} Note not found`);
process.exit(1);
}
await client.status.delete({
where: {
id: note.id,
},
});
console.log(
`${chalk.green(``)} Deleted note ${chalk.blue(note.id)}`
);
break;
}
case "search": {
const argsWithoutFlags = args.filter(
arg => !arg.startsWith("--")
);
const query = argsWithoutFlags[4];
if (!query) {
console.log(`${chalk.red(``)} Missing query`);
process.exit(1);
}
const local = args.includes("--local");
const remote = args.includes("--remote");
const json = args.includes("--json");
const csv = args.includes("--csv");
const queries: Prisma.StatusWhereInput[] = [];
if (local) {
queries.push({
instanceId: null,
});
}
if (remote) {
queries.push({
instanceId: {
not: null,
},
});
}
const statuses = await client.status.findMany({
where: {
AND: queries,
content: {
contains: query,
mode: "insensitive",
},
},
take: 40,
include: {
author: true,
instance: true,
},
});
if (json || csv) {
if (json) {
console.log(JSON.stringify(statuses, null, 4));
}
if (csv) {
// Convert the outputted JSON to CSV
// Remove all object children from each object
const items = statuses.map(status => {
const item = {
...status,
author: undefined,
instance: undefined,
};
return item;
});
const replacer = (key: string, value: any): any =>
value === null ? "" : value; // Null values are returned as empty strings
const header = Object.keys(items[0]);
const csv = [
header.join(","), // header row first
...items.map(row =>
header
.map(fieldName =>
// @ts-expect-error This is fine
JSON.stringify(row[fieldName], replacer)
)
.join(",")
),
].join("\r\n");
console.log(csv);
}
} else {
console.log(
`${chalk.green(``)} Found ${chalk.blue(
statuses.length
)} statuses`
);
const table = new Table({
head: [
chalk.white(chalk.bold("Username")),
chalk.white(chalk.bold("Instance URL")),
chalk.white(chalk.bold("Content")),
],
});
for (const status of statuses) {
table.push([
chalk.yellow(`@${status.author.username}`),
chalk.blue(
status.instanceId
? status.instance?.base_url
: "Local"
),
chalk.green(status.content.slice(0, 50)),
]);
}
console.log(table.toString());
}
break;
}
default:
console.log(`Unknown command ${chalk.blue(command)}`);
break;
}
break;
}
default:
console.log(`Unknown command ${chalk.blue(command)}`);
break;

View file

@ -7,9 +7,16 @@ database = "lysand"
[redis.queue]
host = "localhost"
post = 6379
port = 6379
password = ""
# database = 0
database = 0
[redis.cache]
host = "localhost"
port = 6379
password = ""
database = 1
enabled = false
[http]
base_url = "https://lysand.social"

View file

@ -376,7 +376,10 @@ export const userToAPI = (
discoverable: undefined,
mute_expires_at: undefined,
group: false,
role: undefined,
pleroma: {
is_admin: user.isAdmin,
is_moderator: user.isAdmin,
},
};
};

View file

@ -8,6 +8,7 @@ services:
#- ./logs:/app/logs
- ./config:/app/config
- ./.env:/app/.env
- ./uploads:/app/uploads
restart: unless-stopped
container_name: lysand
networks:

View file

@ -11,7 +11,9 @@ import { mkdir } from "fs/promises";
import { client } from "~database/datasource";
import type { PrismaClientInitializationError } from "@prisma/client/runtime/library";
import { HookTypes, Server } from "~plugins/types";
import { initializeRedisCache } from "@redis";
const timeAtStart = performance.now();
const server = new Server();
const router = new Bun.FileSystemRouter({
@ -32,10 +34,16 @@ if (!(await requests_log.exists())) {
await Bun.write(process.cwd() + "/logs/requests.log", "");
}
const redisCache = await initializeRedisCache();
if (redisCache) {
client.$use(redisCache);
}
// Check if database is reachable
const postCount = 0;
let postCount = 0;
try {
await client.status.count();
postCount = await client.status.count();
} catch (e) {
const error = e as PrismaClientInitializationError;
console.error(
@ -171,7 +179,7 @@ console.log(
`${chalk.green(``)} ${chalk.bold(
`Lysand started at ${chalk.blue(
`${config.http.bind}:${config.http.bind_port}`
)}`
)} in ${chalk.gray((performance.now() - timeAtStart).toFixed(0))}ms`
)}`
);

View file

@ -2,7 +2,7 @@
"name": "lysand",
"module": "index.ts",
"type": "module",
"version": "0.0.1",
"version": "0.1.2",
"description": "A project to build a federated social network",
"author": {
"email": "contact@cpluspatch.com",
@ -39,6 +39,7 @@
"lint": "eslint --config .eslintrc.cjs --ext .ts .",
"prisma": "bun run prisma.ts",
"generate": "bun prisma generate",
"benchmark:timeline": "bun run benchmarks/timelines.ts",
"cli": "bun run cli.ts"
},
"trustedDependencies": [
@ -48,7 +49,9 @@
"devDependencies": {
"@julr/unocss-preset-forms": "^0.1.0",
"@microsoft/eslint-formatter-sarif": "^3.0.0",
"@types/cli-table": "^0.3.4",
"@types/html-to-text": "^9.0.4",
"@types/ioredis": "^5.0.0",
"@types/jsonld": "^1.5.13",
"@typescript-eslint/eslint-plugin": "^6.13.1",
"@typescript-eslint/parser": "^6.13.1",
@ -73,14 +76,17 @@
"blurhash": "^2.0.5",
"bullmq": "^4.14.4",
"chalk": "^5.3.0",
"cli-table": "^0.3.11",
"eventemitter3": "^5.0.1",
"html-to-text": "^9.0.5",
"ioredis": "^5.3.2",
"ip-matching": "^2.1.2",
"iso-639-1": "^3.1.0",
"isomorphic-dompurify": "^1.10.0",
"jsonld": "^8.3.1",
"marked": "^9.1.2",
"prisma": "^5.6.0",
"prisma-redis-middleware": "^4.8.0",
"semver": "^7.5.4",
"sharp": "^0.33.0-rc.2"
}

View file

@ -23,13 +23,5 @@ export default async (req: Request): Promise<Response> => {
return jsonResponse({
...userToAPI(user, true),
// TODO: Add role support
role: {
id: 0,
name: "",
permissions: "",
color: "",
highlighted: false,
},
});
};

View file

@ -2,6 +2,9 @@ import { applyConfig } from "@api";
import { getConfig } from "@config";
import { jsonResponse } from "@response";
import { client } from "~database/datasource";
import { userRelations, userToAPI } from "~database/entities/User";
import type { APIInstance } from "~types/entities/instance";
import manifest from "~package.json";
export const meta = applyConfig({
allowedMethods: ["GET"],
@ -22,6 +25,9 @@ export const meta = applyConfig({
export default async (): Promise<Response> => {
const config = getConfig();
// Get software version from package.json
const version = manifest.version;
const statusCount = await client.status.count({
where: {
instanceId: null,
@ -33,12 +39,40 @@ export default async (): Promise<Response> => {
},
});
// Get the first created admin user
const contactAccount = await client.user.findFirst({
where: {
instanceId: null,
isAdmin: true,
},
orderBy: {
id: "asc",
},
include: userRelations,
});
// Get user that have posted once in the last 30 days
const monthlyActiveUsers = await client.user.count({
where: {
instanceId: null,
statuses: {
some: {
createdAt: {
gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
},
},
},
},
});
const knownDomainsCount = await client.instance.count();
// TODO: fill in more values
return jsonResponse({
approval_required: false,
configuration: {
media_attachments: {
image_matrix_limit: 10,
image_matrix_limit: config.validation.max_media_attachments,
image_size_limit: config.validation.max_media_size,
supported_mime_types: config.validation.allowed_mime_types,
video_frame_limit: 60,
@ -46,9 +80,10 @@ export default async (): Promise<Response> => {
video_size_limit: config.validation.max_media_size,
},
polls: {
max_characters_per_option: 100,
max_expiration: 60 * 60 * 24 * 365 * 100, // 100 years,
max_options: 40,
max_characters_per_option:
config.validation.max_poll_option_size,
max_expiration: config.validation.max_poll_duration,
max_options: config.validation.max_poll_options,
min_expiration: 60,
},
statuses: {
@ -70,7 +105,7 @@ export default async (): Promise<Response> => {
languages: ["en"],
rules: [],
stats: {
domain_count: 1,
domain_count: knownDomainsCount,
status_count: statusCount,
user_count: userCount,
},
@ -80,7 +115,7 @@ export default async (): Promise<Response> => {
urls: {
streaming_api: "",
},
version: "4.2.0+glitch (compatible; Lysand 0.0.1)",
version: `4.2.0+glitch (compatible; Lysand ${version}})`,
max_toot_chars: config.validation.max_note_size,
pleroma: {
metadata: {
@ -115,8 +150,9 @@ export default async (): Promise<Response> => {
privileged_staff: false,
},
stats: {
mau: 2,
mau: monthlyActiveUsers,
},
},
});
contact_account: contactAccount ? userToAPI(contactAccount) : null,
} as APIInstance);
};

View file

@ -0,0 +1,49 @@
import { errorResponse } from "@response";
import { applyConfig } from "@api";
import type { MatchedRoute } from "bun";
export const meta = applyConfig({
allowedMethods: ["GET"],
route: "/media/:id",
ratelimits: {
max: 100,
duration: 60,
},
auth: {
required: false,
},
});
export default async (
req: Request,
matchedRoute: MatchedRoute
): Promise<Response> => {
// TODO: Add checks for disabled or not email verified accounts
const id = matchedRoute.params.id;
// parse `Range` header
const [start = 0, end = Infinity] = (
(req.headers.get("Range") || "")
.split("=") // ["Range: bytes", "0-100"]
.at(-1) || ""
) // "0-100"
.split("-") // ["0", "100"]
.map(Number); // [0, 100]
// Serve file from filesystem
const file = Bun.file(`./uploads/${id}`);
const buffer = await file.arrayBuffer();
if (!(await file.exists())) return errorResponse("File not found", 404);
// Can't directly copy file into Response because this crashes Bun for now
return new Response(buffer, {
headers: {
"Content-Type": file.type || "application/octet-stream",
"Content-Length": `${file.size - start}`,
"Content-Range": `bytes ${start}-${end}/${file.size}`,
},
});
};

View file

@ -31,4 +31,5 @@ export interface APIAccount {
source?: APISource;
role?: APIRole;
mute_expires_at?: string;
pleroma?: any;
}

View file

@ -16,6 +16,13 @@ export interface ConfigType {
password: string;
database: number | null;
};
cache: {
host: string;
port: number;
password: string;
database: number | null;
enabled: boolean;
};
};
http: {
@ -159,7 +166,14 @@ export const configDefaults: ConfigType = {
host: "localhost",
port: 6379,
password: "",
database: null,
database: 0,
},
cache: {
host: "localhost",
port: 6379,
password: "",
database: 1,
enabled: false,
},
},
instance: {

60
utils/redis.ts Normal file
View file

@ -0,0 +1,60 @@
import { getConfig } from "@config";
import type { Prisma } from "@prisma/client";
import chalk from "chalk";
import Redis from "ioredis";
import { createPrismaRedisCache } from "prisma-redis-middleware";
const config = getConfig();
const cacheRedis = config.redis.cache.enabled
? new Redis({
host: config.redis.cache.host,
port: Number(config.redis.cache.port),
password: config.redis.cache.password,
db: Number(config.redis.cache.database ?? 0),
})
: null;
cacheRedis?.on("error", e => {
console.log(e);
});
export { cacheRedis };
export const initializeRedisCache = async () => {
if (cacheRedis) {
// Test connection
try {
await cacheRedis.ping();
} catch (e) {
console.error(
`${chalk.red(``)} ${chalk.bold(
`Error while connecting to Redis`
)}`
);
throw e;
}
console.log(`${chalk.green(``)} ${chalk.bold(`Connected to Redis`)}`);
const cacheMiddleware: Prisma.Middleware = createPrismaRedisCache({
storage: {
type: "redis",
options: {
client: cacheRedis,
invalidation: {
referencesTTL: 300,
},
},
},
cacheTime: 300,
onError: e => {
console.error(e);
},
});
return cacheMiddleware;
}
return null;
};