Skip to content

Commit

Permalink
Merge pull request #3 from evantahler/orm
Browse files Browse the repository at this point in the history
ORM, types, and more!
  • Loading branch information
evantahler authored Mar 25, 2024
2 parents d7d0ecf + dbbe59c commit 40d26f9
Show file tree
Hide file tree
Showing 35 changed files with 839 additions and 138 deletions.
9 changes: 7 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,10 @@ servers.web.host=0.0.0.0
servers.web.apiRoute="/api"
servers.web.assetRoute="/assets"
servers.web.pageRoute="/pages"
servers.web.closeActiveConnectionsOnStop=false
servers.web.closeActiveConnectionsOnStop.test=true

db.connectionString="postgres://postgres:postgres@localhost:5432/bun"
db.connectionString.test="postgres://postgres:postgres@localhost:5432/bun-test"
db.autoMigrate=true

redis.connectionString="redis://localhost:6379/0"
redis.connectionString.test="redis://localhost:6379/1"
17 changes: 17 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@ jobs:

test:
runs-on: ubuntu-latest

services:
redis:
image: redis
postgres:
image: postgres
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: bun-test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432

steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ bun run prettier --write .

This project was created using `bun init` in bun v1.0.29. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

## Databases and Migrations

This project uses Drizzle as the ORM. Migrations are derived from the schemas. To create a migration from changes in `scheams/*.ts` run `bun run migrations.ts`. Then, restart the server - pending migrations are auto-applied.

## Intentional changes from ActionHero

**Process**
Expand Down Expand Up @@ -90,3 +94,7 @@ This project was created using `bun init` in bun v1.0.29. [Bun](https://bun.sh)
**Testing**

- No mock server. Let's make real API requests. Now that bun has `fetch` included, it's easy.

**ORM**

- we use drizzle for the ORM and migrations.
47 changes: 47 additions & 0 deletions __tests__/actions/user.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { test, expect, beforeAll, afterAll } from "bun:test";
import { api, type ActionResponse } from "../../api";
import type { UserCreate } from "../../actions/user";
import { config } from "../../config";

const url = `http://${config.server.web.host}:${config.server.web.port}`;

beforeAll(async () => {
await api.start();
await api.db.clearDatabase();
});

afterAll(async () => {
await api.stop();
});

test("user can be created", async () => {
const res = await fetch(url + "/api/user", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "person 1",
email: "[email protected]",
password: "password",
}),
});
const response = (await res.json()) as ActionResponse<UserCreate>;
expect(res.status).toBe(200);

expect(response.id).toEqual(1);
expect(response.email).toEqual("[email protected]");
});

test("email must be unique", async () => {
const res = await fetch(url + "/api/user", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "person 1",
email: "[email protected]",
password: "password",
}),
});
const response = (await res.json()) as ActionResponse<UserCreate>;
expect(res.status).toBe(500);
expect(response.error?.message).toMatch(/violates unique constraint/);
});
26 changes: 12 additions & 14 deletions actions/hello.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import { Action, type ActionParams } from "../api";
import { HTTP_METHOD } from "../classes/Action";
import { ensureString } from "../util/formatters";

export class Hello extends Action {
constructor() {
super({
name: "hello",
web: { route: "/hello", method: "POST" },
inputs: {
name: {
required: true,
validator: (p) => p.length > 0,
formatter: ensureString,
},
},
});
}
export class Hello implements Action {
name = "hello";
web = { route: "/hello", method: HTTP_METHOD.POST };
inputs = {
name: {
required: true,
validator: (p: string) =>
p.length <= 0 ? "Name must be at least 1 character" : undefined,
formatter: ensureString,
},
};

async run(params: ActionParams<Hello>) {
return { message: `Hello, ${params.name}!` };
Expand Down
14 changes: 6 additions & 8 deletions actions/status.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { api, Action } from "../api";
import { api, Action, type Inputs } from "../api";
import { HTTP_METHOD } from "../classes/Action";
import packageJSON from "../package.json";

export class Status extends Action {
constructor() {
super({
name: "status",
web: { route: "/status", method: "GET" },
});
}
export class Status implements Action {
name = "status";
inputs = {};
web = { route: "/status", method: HTTP_METHOD.GET };

async run() {
const consumedMemoryMB =
Expand Down
14 changes: 6 additions & 8 deletions actions/swagger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Action, config, api } from "../api";
import { HTTP_METHOD } from "../classes/Action";
import packageJSON from "../package.json";

const SWAGGER_VERSION = "2.0";
Expand Down Expand Up @@ -28,14 +29,11 @@ type SwaggerPath = {
};
};

export class Swagger extends Action {
constructor() {
super({
name: "swagger",
description: "return API documentation in the OpenAPI specification",
web: { route: "/swagger", method: "GET" },
});
}
export class Swagger implements Action {
name = "swagger";
description = "return API documentation in the OpenAPI specification";
inputs = {};
web = { route: "/swagger", method: HTTP_METHOD.GET };

async run() {
const swaggerPaths = buildSwaggerPaths();
Expand Down
45 changes: 45 additions & 0 deletions actions/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { api, Action, type ActionParams } from "../api";
import { HTTP_METHOD } from "../classes/Action";
import { hashPassword, serializeUser } from "../ops/UserOps";
import { users } from "../schema/users";
import { ensureString } from "../util/formatters";

export class UserCreate implements Action {
name = "userCreate";
web = { route: "/user", method: HTTP_METHOD.PUT };
inputs = {
name: {
required: true,
validator: (p: string) =>
p.length < 3 ? "Name must be at least 3 characters" : undefined,
formatter: ensureString,
},
email: {
required: true,
validator: (p: string) =>
p.length < 3 || !p.includes("@") ? "Email invalid" : undefined,
formatter: ensureString,
},
password: {
required: true,
validator: (p: string) =>
p.length < 3 ? "Password must be at least 3 characters" : undefined,
formatter: ensureString,
},
};

async run(params: ActionParams<UserCreate>) {
const user = (
await api.db.db
.insert(users)
.values({
name: params.name,
email: params.email,
password_hash: await hashPassword(params.password),
})
.returning()
)[0];

return serializeUser(user);
}
}
Binary file modified bun.lockb
Binary file not shown.
34 changes: 23 additions & 11 deletions classes/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { config } from "../config";
import { globLoader } from "../util/glob";
import type { Initializer, InitializerSortKeys } from "./Initializer";
import { Logger } from "./Logger";
import { ErrorType, TypedError } from "./TypedError";

export class API {
rootDir: string;
Expand Down Expand Up @@ -36,11 +37,14 @@ export class API {
this.sortInitializers("loadPriority");

for (const initializer of this.initializers) {
this.logger.debug(`Initializing initializer ${initializer.name}`);
await initializer.validate();
const response = await initializer.initialize?.();
if (response) this[initializer.name] = response;
this.logger.debug(`Initialized initializer ${initializer.name}`);
try {
this.logger.debug(`Initializing initializer ${initializer.name}`);
const response = await initializer.initialize?.();
if (response) this[initializer.name] = response;
this.logger.debug(`Initialized initializer ${initializer.name}`);
} catch (e) {
throw new TypedError(`${e}`, ErrorType.SERVER_INITIALIZATION);
}
}

this.initialized = true;
Expand All @@ -57,9 +61,13 @@ export class API {
this.sortInitializers("startPriority");

for (const initializer of this.initializers) {
this.logger.debug(`Starting initializer ${initializer.name}`);
const response = await initializer.start?.();
this.logger.debug(`Started initializer ${initializer.name}`);
try {
this.logger.debug(`Starting initializer ${initializer.name}`);
await initializer.start?.();
this.logger.debug(`Started initializer ${initializer.name}`);
} catch (e) {
throw new TypedError(`${e}`, ErrorType.SERVER_START);
}
}

this.started = true;
Expand All @@ -74,9 +82,13 @@ export class API {
this.sortInitializers("stopPriority");

for (const initializer of this.initializers) {
this.logger.debug(`Stopping initializer ${initializer.name}`);
await initializer.stop?.();
this.logger.debug(`Stopped initializer ${initializer.name}`);
try {
this.logger.debug(`Stopping initializer ${initializer.name}`);
await initializer.stop?.();
this.logger.debug(`Stopped initializer ${initializer.name}`);
} catch (e) {
throw new TypedError(`${e}`, ErrorType.SERVER_STOP);
}
}

this.stopped = true;
Expand Down
26 changes: 10 additions & 16 deletions classes/Action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import type { Inputs } from "./Inputs";
import type { Connection } from "./Connection";
import type { Input } from "./Input";

export const httpMethods = [
"GET",
"POST",
"PUT",
"DELETE",
"PATCH",
"OPTIONS",
] as const;
export type HTTP_METHOD = (typeof httpMethods)[number];
export enum HTTP_METHOD {
"GET" = "GET",
"POST" = "POST",
"PUT" = "PUT",
"DELETE" = "DELETE",
"PATCH" = "PATCH",
"OPTIONS" = "OPTIONS",
}

export type ActionConstructorInputs = {
name: string;
Expand All @@ -24,7 +23,7 @@ export type ActionConstructorInputs = {

export abstract class Action {
name: string;
description: string;
description?: string;
inputs: Inputs;
web: {
route: RegExp | string;
Expand All @@ -37,7 +36,7 @@ export abstract class Action {
this.inputs = args.inputs ?? ({} as Inputs);
this.web = {
route: args.web?.route ?? `/${this.name}`,
method: args.web?.method ?? "GET",
method: args.web?.method ?? HTTP_METHOD.GET,
};
}

Expand All @@ -51,11 +50,6 @@ export abstract class Action {
params: ActionParams<typeof this>,
connection: Connection, // ): ActionResponse<typeof this>;
): Promise<any>;

async validate() {
if (!this.name) throw new Error("Action name is required");
if (!this.description) throw new Error("Action description is required");
}
}

export type ActionParams<A extends Action> = {
Expand Down
Loading

0 comments on commit 40d26f9

Please sign in to comment.