Skip to content

Commit

Permalink
feat: mikroorm boilerplate and example models
Browse files Browse the repository at this point in the history
  • Loading branch information
Sv443 committed Oct 20, 2024
1 parent 23b78e8 commit 94c1d60
Show file tree
Hide file tree
Showing 24 changed files with 715 additions and 15 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
"email": "[email protected]"
},
"dependencies": {
"@mikro-orm/core": "^6.3.13",
"@mikro-orm/postgresql": "^6.3.13",
"@pm2/io": "^5.0.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
Expand All @@ -43,6 +45,7 @@
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
"@mikro-orm/cli": "^6.3.13",
"@types/compression": "^1.7.2",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.14",
Expand Down
501 changes: 501 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions src/lib/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { Stringifiable } from "@/types/index.js";
import "dotenv/config";

/**
* Grabs an environment variable's value, and casts it to a `string` (or what's passed in the TRetVal generic).
* However if the string is empty (or unset), undefined is returned.
*/
export function getEnvVar<TRetVal extends string>(varName: string, asType?: "stringNoEmpty"): undefined | TRetVal
/** Grabs an environment variable's value, and casts it to a `string` (or what's passed in the TRetVal generic) */
export function getEnvVar<TRetVal extends string>(varName: string, asType?: "string"): undefined | TRetVal
/** Grabs an environment variable's value, and casts it to a `number` (or what's passed in the TRetVal generic) */
export function getEnvVar<TRetVal extends number>(varName: string, asType: "number"): undefined | TRetVal
/** Grabs an environment variable's value, and casts it to a `string[]` (or what's passed in the TRetVal generic) */
export function getEnvVar<TRetVal extends string[]>(varName: string, asType: "stringArray"): undefined | TRetVal
/** Grabs an environment variable's value, and casts it to a `number[]` (or what's passed in the TRetVal generic) */
export function getEnvVar<TRetVal extends number[]>(varName: string, asType: "numberArray"): undefined | TRetVal
/** Grabs an environment variable's value, and casts it to a specific type (stringNoEmpty by default) */
export function getEnvVar<
T extends ("string" | "number" | "stringArray" | "numberArray" | "stringNoEmpty")
>(
varName: string,
asType: T = "stringNoEmpty" as T,
): undefined | (string | number | string[] | number[]) {
const val = process.env[varName];

if(!val)
return undefined;

let transform: (value: string) => unknown = v => v.trim();

const commasRegex = /[,،,٫٬]/g;

switch(asType) {
case "number":
transform = v => parseInt(v.trim());
break;
case "stringArray":
transform = v => v.trim().split(commasRegex);
break;
case "numberArray":
transform = v => v.split(commasRegex).map(n => parseInt(n.trim()));
break;
case "stringNoEmpty":
transform = v => String(v).trim().length == 0 ? undefined : String(v).trim();
}

return transform(val) as string; // I'm lazy and ts is happy, so we can all be happy and pretend this doesn't exist
}

/**
* Tests if the value of the environment variable {@linkcode varName} equals {@linkcode compareValue} casted to string.
* Set {@linkcode caseSensitive} to true to make the comparison case-sensitive.
*/
export function envVarEquals(varName: string, compareValue: Stringifiable, caseSensitive = false) {
const envVal = (caseSensitive ? getEnvVar(varName) : getEnvVar(varName)?.toLowerCase());
const compVal = (caseSensitive ? String(compareValue) : String(compareValue).toLowerCase());
return envVal === compVal;
}
32 changes: 32 additions & 0 deletions src/mikro-orm.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { defineConfig } from "@mikro-orm/core";
import { MikroORM } from "@mikro-orm/postgresql";
import { envVarEquals, getEnvVar } from "@lib/env.js";

const config = defineConfig({
clientUrl: getEnvVar("DATABASE_URL", "stringNoEmpty"),
charset: "utf8",
entities: ["dist/**/*.model.js"],
entitiesTs: ["src/**/*.model.ts"],
debug: envVarEquals("DATABASE_DEBUG", true),
});

/** MikroORM instance */
export let orm: Awaited<ReturnType<typeof MikroORM.init>>;
/** EntityManager instance */
export let em: typeof orm.em;

/** Load MikroORM instances */
export async function initDatabase() {
orm = await MikroORM.init(config);
em = orm.em.fork();

// run migrations
try {
await orm.getSchemaGenerator().updateSchema();
}
catch(e) {
console.error("Error running migrations:", e);
setImmediate(() => process.exit(1));
}
}

Empty file added src/models/Billing.model.ts
Empty file.
Empty file added src/models/Connection.model.ts
Empty file.
Empty file added src/models/Invoice.model.ts
Empty file.
Empty file added src/models/Joke.model.ts
Empty file.
Empty file added src/models/Report.model.ts
Empty file.
40 changes: 40 additions & 0 deletions src/models/ReportNote.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { em } from "@/mikro-orm.config.js";
import { Entity, ManyToOne, PrimaryKey, Property } from "@mikro-orm/core";
import { User } from "@models/User.model.js";

export type ReportNoteProps = {
text: string;
authorId: string;
};

@Entity()
export class ReportNote {
constructor(props: ReportNoteProps) {
this.text = props.text;
this.authorId = props.authorId;
this.createdAt = this.updatedAt = new Date();
}

@PrimaryKey({ type: "string", generated: "uuid" })
id!: string;

@Property({ type: "string", length: 255 })
text: string;

@Property({ type: "string" })
authorId: string;

@ManyToOne({ entity: () => User })
get author(): User {
return em.getReference(User, this.authorId);
}

@Property({ type: "date" })
createdAt: Date;

@Property({ type: "date", onUpdate: () => new Date() })
updatedAt: Date;

@Property({ type: "date", nullable: true })
deletedAt?: Date;
}
Empty file added src/models/Submission.model.ts
Empty file.
53 changes: 53 additions & 0 deletions src/models/User.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Entity, PrimaryKey, Property } from "@mikro-orm/core";
import type { AccountTier, UserRole } from "@/types/user.js";

export type UserProps = {
username: string;
email: string;
tier: AccountTier;
roles: UserRole[];
};

@Entity()
export class User {
constructor(props: UserProps) {
this.username = props.username;
this.email = props.email;
this.createdAt = this.updatedAt = new Date();
}

@PrimaryKey({ type: "string", generated: "uuid" })
id!: string;

@PrimaryKey({ type: "string", length: 24 })
username: string;

@Property({ type: "string", length: 320 })
email: string;

@Property({ type: "string", length: 16 })
tier: AccountTier = "free";

@Property({ type: "array", length: 16, runtimeType: "string" })
roles = ["user"];

// TODO:

// @OneToOne({ entity: () => UserSettings, orphanRemoval: true })
// settings: UserSettings;

// @OneToOne({ entity: () => UserBilling, orphanRemoval: true })
// billing: Billing;

// @OneToMany({ entity: () => ReportNote, mappedBy: "author" })
// connections: Connection[];

@Property({ type: "date" })
createdAt: Date;

@Property({ type: "date", onUpdate: () => new Date() })
updatedAt: Date;

@Property({ type: "date", nullable: true })
deletedAt?: Date;
}
Empty file.
9 changes: 9 additions & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export * from "./Joke.model.js";
export * from "./Submission.model.js";
export * from "./User.model.js";
export * from "./UserSettings.model.js";
export * from "./Billing.model.js";
export * from "./Invoice.model.js";
export * from "./Connection.model.js";
export * from "./Report.model.js";
export * from "./ReportNote.model.js";
2 changes: 1 addition & 1 deletion src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { createHash } from "node:crypto";
import type { JSONCompatible } from "svcorelib";

import { settings } from "../settings.js";
import { initFuncs as routeInitFuncs } from "../routes/index.js";
import { initFuncs as routeInitFuncs } from "./routes/index.js";
import { error } from "../error.js";
import { isValidToken } from "../auth.js";
import { genericRateLimit } from "../rateLimiters.js";
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
6 changes: 3 additions & 3 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from "./misc";

export * from "./jokeapi/index";
export * from "./jokeapi/index.js";
export * from "./misc.js";
export * from "./user.js";
2 changes: 1 addition & 1 deletion src/types/jokeapi
Submodule jokeapi updated 2 files
+32 −7 categories.json
+7 −11 flags.json
2 changes: 2 additions & 0 deletions src/types/misc.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export type Stringifiable = string | number | boolean | null | undefined | { toString(): string };

export type ResponseFormat = "json" | "xml" | "text";

export enum LogLevel {
Expand Down
3 changes: 3 additions & 0 deletions src/types/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type UserRole = "user" | "moderator" | "admin";

export type AccountTier = "free" | "premium" | "enterprise";
19 changes: 9 additions & 10 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"baseUrl": ".",
"target": "ESNext",
"lib": ["ESNext"],
"module": "ESNext",
"moduleResolution": "node",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "./src/",
"outDir": "./out/",
"strict": true,
Expand All @@ -20,19 +20,18 @@
"useDefineForClassFields": true,
"noImplicitThis": false,
"paths": {
"src/*": ["src/*"],
"@src/*": ["src/*"],
"server/*": ["src/server/*"],
"@db/*": ["src/db/*"],
"@lib/*": ["src/lib/*"],
"@models/*": ["src/models/*"],
"@routes/*": ["src/server/routes/*"],
"@server/*": ["src/server/*"],
"db/*": ["src/database/*"],
"@db/*": ["src/database/*"],
"types/*": ["src/types/*"],
"@types/*": ["src/types/*"],
"@/*": ["src/*"],
},
},
"ts-node": {
"esm": true,
"preferTsExts": true
"preferTsExts": true,
"transpileOnly": true
},
"files": ["src/index.ts"],
"include": ["src/**/*.ts", "src/types/**.*"],
Expand Down

0 comments on commit 94c1d60

Please sign in to comment.