Skip to content

Commit

Permalink
examples(crud-simple-todo): init example
Browse files Browse the repository at this point in the history
  • Loading branch information
dirkdev98 committed Apr 15, 2023
1 parent f267666 commit b10b20b
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/examples/crud-simple-todo.md
6 changes: 6 additions & 0 deletions examples/crud-simple-todo/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
NODE_ENV=development
APP_NAME=exampletodo

POSTGRES_HOST=127.0.0.1:5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
32 changes: 32 additions & 0 deletions examples/crud-simple-todo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Minimal CRUD api for managing todo's

This project is created using the
[crud-simple-todo](https://github.com/compasjs/compas/tree/main/examples/crud-simple-todo)
template via [create-compas](https://www.npmjs.com/package/create-compas).

```shell
# Via NPM
npx create-compas@latest --template crud-simple-todo

# Or with Yarn
yarn create compas --template crud-simple-todo
```

It uses a few Compas features, most notably:

- The code-generators, more specifically, CRUD generators and the generated api
clients
- Postgres related features like migrations and test databases

## Getting started

- Start up the development Postgres and Minio instances
- `compas docker up`
- Apply the Postgres migrations
- `compas migrate`
- Regenerate router, validators, types, sql, etc.
- `compas run generate`
- Run the tests
- `compas test --serial`
- Start the API
- `compas run api`
25 changes: 25 additions & 0 deletions examples/crud-simple-todo/migrations/001-todo.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

CREATE TABLE migration
(
"namespace" varchar NOT NULL,
"number" int,
"name" varchar NOT NULL,
"createdAt" timestamptz DEFAULT now(),
"hash" varchar
);

CREATE INDEX migration_namespace_number_idx ON "migration" ("namespace", "number");

CREATE TABLE "todo"
(
"id" uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
"title" varchar NOT NULL,
"completedAt" timestamptz NULL,
"createdAt" timestamptz NOT NULL DEFAULT now(),
"updatedAt" timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX "todoDatesIdx" ON "todo" ("createdAt", "updatedAt");
CREATE INDEX "todoTitleIdx" ON "todo" ("title");
CREATE INDEX "todoCompletedAtIdx" ON "todo" ("completedAt");
24 changes: 24 additions & 0 deletions examples/crud-simple-todo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"private": true,
"version": "0.0.1",
"type": "module",
"dependencies": {
"@compas/cli": "*",
"@compas/server": "*",
"@compas/stdlib": "*",
"@compas/store": "*"
},
"devDependencies": {
"@compas/code-gen": "*",
"@compas/eslint-plugin": "*"
},
"prettier": "@compas/eslint-plugin/prettierrc",
"exampleMetadata": {
"generating": "compas run generate",
"testing": [
"compas migrate",
"compas test --serial"
],
"initMessage": "Started a new project with the 'crud-simple-todo' template. See the README.md for how to get started."
}
}
18 changes: 18 additions & 0 deletions examples/crud-simple-todo/scripts/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { environment, isProduction, isStaging, mainFn } from "@compas/stdlib";
import { app, injectServices } from "../src/services.js";

mainFn(import.meta, main);

async function main(logger) {
await injectServices();

const port = Number(environment.PORT ?? "3001");
app.listen(port, () => {
logger.info({
message: "Listening...",
port,
isProduction: isProduction(),
isStaging: isStaging(),
});
});
}
56 changes: 56 additions & 0 deletions examples/crud-simple-todo/scripts/generate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { TypeCreator } from "@compas/code-gen";
import { Generator } from "@compas/code-gen/experimental";
import { mainFn } from "@compas/stdlib";

mainFn(import.meta, main);

function main() {
const generator = new Generator();
const T = new TypeCreator("todo");

generator.add(
new TypeCreator("database")
.object("todo")
.keys({
title: T.string().min(3).searchable(),
completedAt: T.date().optional().searchable(),
})
.enableQueries({
withDates: true,
}),

T.crud("/todo").entity(T.reference("database", "todo")).routes({
listRoute: true,
singleRoute: true,
createRoute: true,
updateRoute: true,
deleteRoute: true,
}),
);

generator.generate({
targetLanguage: "js",
outputDirectory: "./src/generated",
generators: {
database: {
target: {
dialect: "postgres",
},
},
apiClient: {
target: {
targetRuntime: "node.js",
library: "fetch",
},
responseValidation: {
looseObjectValidation: false,
},
},
router: {
target: {
library: "koa",
},
},
},
});
}
33 changes: 33 additions & 0 deletions examples/crud-simple-todo/src/services.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createBodyParsers, getApp } from "@compas/server";
import {
createTestPostgresDatabase,
newPostgresConnection,
} from "@compas/store";
import { router } from "./generated/common/router.js";
import { todoRegisterCrud } from "./generated/todo/crud.js";

export let app = undefined;

export let sql = undefined;

export async function injectServices() {
app = getApp({});
sql = await newPostgresConnection({ max: 10 });

await todoRegisterCrud({
sql,
});

app.use(router(createBodyParsers()));
}

export async function injectTestServices() {
app = getApp({});
sql = await createTestPostgresDatabase();

await todoRegisterCrud({
sql,
});

app.use(router(createBodyParsers()));
}
147 changes: 147 additions & 0 deletions examples/crud-simple-todo/test/api.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { mainTestFn, test } from "@compas/cli";
import { isNil } from "@compas/stdlib";
import { cleanupTestPostgresDatabase } from "@compas/store";
import {
fetchCatchErrorAndWrapWithAppError,
fetchWithBaseUrl,
} from "../src/generated/common/api-client.js";
import {
apiTodoCreate,
apiTodoList,
apiTodoSingle,
apiTodoUpdate,
} from "../src/generated/todo/apiClient.js";
import { app, injectTestServices, sql } from "../src/services.js";

mainTestFn(import.meta);

test("crud-simple-todo", async (t) => {
const apiPort = 4441;

await injectTestServices();

const server = app.listen(apiPort);

const fetchFn = fetchCatchErrorAndWrapWithAppError(
fetchWithBaseUrl(fetch, `http://localhost:${apiPort}/`),
);

t.test("Empty todo list to start with", async (t) => {
const { total } = await apiTodoList(fetchFn, {}, {});

t.equal(total, 0);
});

t.test("Create a new todo item", async (t) => {
const { item } = await apiTodoCreate(fetchFn, {
title: "Write more tests",
});

t.ok(isNil(item.completedAt), "Todo item should not be completed");
});

t.test("Retrieve all todo items", async (t) => {
const { total, list } = await apiTodoList(fetchFn, {}, {});

t.equal(total, 1);
t.equal(list[0].title, "Write more tests");
});

t.test("Retrieve a single todo item", async (t) => {
const { list } = await apiTodoList(fetchFn, {}, {});
const { item } = await apiTodoSingle(fetchFn, { todoId: list[0].id });

t.equal(item.id, list[0].id);
});

t.test("Insert and update title of todo", async (t) => {
const { item: insertedItem } = await apiTodoCreate(fetchFn, {
title: "Non-descriptive title",
});

await apiTodoUpdate(
fetchFn,
{
todoId: insertedItem.id,
},
{
title: "Descriptive title",
},
);

const { item } = await apiTodoSingle(fetchFn, { todoId: insertedItem.id });

t.equal(item.title, "Descriptive title");
t.ok(isNil(item.completedAt), "Todo is not completed yet");
});

t.test("Complete a todo", async (t) => {
const { item: insertedItem } = await apiTodoCreate(fetchFn, {
title: "Non-descriptive title",
});

await apiTodoUpdate(
fetchFn,
{
todoId: insertedItem.id,
},
{
title: insertedItem.title,
completedAt: new Date(),
},
);

const { item } = await apiTodoSingle(fetchFn, { todoId: insertedItem.id });

t.ok(!isNil(item.completedAt));
});

t.test("Search the list on completed todo's", async (t) => {
const { total } = await apiTodoList(
fetchFn,
{},
{
where: {
completedAtIsNotNull: true,
},
},
);

t.equal(total, 1);
});

t.test("Search the list on todo's that are not completed yet", async (t) => {
const { total } = await apiTodoList(
fetchFn,
{},
{
where: {
completedAtIsNull: true,
},
},
);

t.equal(total, 2);
});

t.test("Search the list on titles", async (t) => {
const { total } = await apiTodoList(
fetchFn,
{},
{
where: {
titleILike: `write%`,
},
},
);

t.equal(total, 1, "Only includes 'Write more tests'");
});

t.test("teardown", async (t) => {
server.close();
await cleanupTestPostgresDatabase(sql);

t.pass();
});
});
Loading

0 comments on commit b10b20b

Please sign in to comment.