create-ely 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/package.json +60 -0
- package/src/index.ts +187 -0
- package/templates/monorepo/README.md +75 -0
- package/templates/monorepo/apps/backend/.cursor/mcp.json +8 -0
- package/templates/monorepo/apps/backend/.dockerignore +60 -0
- package/templates/monorepo/apps/backend/.env.example +19 -0
- package/templates/monorepo/apps/backend/.github/workflows/lint.yml +31 -0
- package/templates/monorepo/apps/backend/.github/workflows/tests.yml +36 -0
- package/templates/monorepo/apps/backend/AGENTS.md +79 -0
- package/templates/monorepo/apps/backend/CHANGELOG.md +190 -0
- package/templates/monorepo/apps/backend/CLAUDE.md +149 -0
- package/templates/monorepo/apps/backend/Dockerfile +35 -0
- package/templates/monorepo/apps/backend/LICENSE +21 -0
- package/templates/monorepo/apps/backend/README.md +274 -0
- package/templates/monorepo/apps/backend/biome.json +58 -0
- package/templates/monorepo/apps/backend/bun.lock +303 -0
- package/templates/monorepo/apps/backend/docker-compose.yml +37 -0
- package/templates/monorepo/apps/backend/drizzle.config.ts +14 -0
- package/templates/monorepo/apps/backend/package.json +42 -0
- package/templates/monorepo/apps/backend/src/common/config.ts +29 -0
- package/templates/monorepo/apps/backend/src/common/logger.ts +18 -0
- package/templates/monorepo/apps/backend/src/db/index.ts +31 -0
- package/templates/monorepo/apps/backend/src/db/migrations/20251111132328_curly_spectrum.sql +8 -0
- package/templates/monorepo/apps/backend/src/db/migrations/meta/20251111132328_snapshot.json +70 -0
- package/templates/monorepo/apps/backend/src/db/migrations/meta/_journal.json +13 -0
- package/templates/monorepo/apps/backend/src/db/schema/users.ts +39 -0
- package/templates/monorepo/apps/backend/src/main.ts +67 -0
- package/templates/monorepo/apps/backend/src/middleware/error-handler.ts +36 -0
- package/templates/monorepo/apps/backend/src/modules/users/index.ts +61 -0
- package/templates/monorepo/apps/backend/src/modules/users/model.ts +48 -0
- package/templates/monorepo/apps/backend/src/modules/users/service.ts +46 -0
- package/templates/monorepo/apps/backend/src/tests/users.test.ts +102 -0
- package/templates/monorepo/apps/backend/src/util/graceful-shutdown.ts +37 -0
- package/templates/monorepo/apps/backend/tsconfig.json +35 -0
- package/templates/monorepo/apps/backend-biome.json.template +14 -0
- package/templates/monorepo/apps/frontend/.env.example +1 -0
- package/templates/monorepo/apps/frontend/README.md +59 -0
- package/templates/monorepo/apps/frontend/biome.json +16 -0
- package/templates/monorepo/apps/frontend/components.json +21 -0
- package/templates/monorepo/apps/frontend/index.html +15 -0
- package/templates/monorepo/apps/frontend/package.json +48 -0
- package/templates/monorepo/apps/frontend/public/favicon.ico +0 -0
- package/templates/monorepo/apps/frontend/src/assets/fonts/.gitkeep +0 -0
- package/templates/monorepo/apps/frontend/src/assets/images/.gitkeep +0 -0
- package/templates/monorepo/apps/frontend/src/features/layout/Header.tsx +73 -0
- package/templates/monorepo/apps/frontend/src/main.tsx +36 -0
- package/templates/monorepo/apps/frontend/src/routeTree.gen.ts +95 -0
- package/templates/monorepo/apps/frontend/src/routes/__root.tsx +25 -0
- package/templates/monorepo/apps/frontend/src/routes/index.tsx +34 -0
- package/templates/monorepo/apps/frontend/src/routes/users/index.tsx +79 -0
- package/templates/monorepo/apps/frontend/src/routes/users/new.tsx +148 -0
- package/templates/monorepo/apps/frontend/src/shared/api/client.ts +6 -0
- package/templates/monorepo/apps/frontend/src/shared/components/.gitkeep +0 -0
- package/templates/monorepo/apps/frontend/src/shared/constants/.gitkeep +0 -0
- package/templates/monorepo/apps/frontend/src/shared/hooks/.gitkeep +0 -0
- package/templates/monorepo/apps/frontend/src/shared/types/.gitkeep +0 -0
- package/templates/monorepo/apps/frontend/src/shared/utils/utils.ts +6 -0
- package/templates/monorepo/apps/frontend/src/styles.css +138 -0
- package/templates/monorepo/apps/frontend/src/vite-env.d.ts +13 -0
- package/templates/monorepo/apps/frontend/tsconfig.json +27 -0
- package/templates/monorepo/apps/frontend/vite.config.ts +27 -0
- package/templates/monorepo/biome.json +65 -0
- package/templates/monorepo/bun.lock +1044 -0
- package/templates/monorepo/package.json +13 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "ae4e6c14-3efe-4132-b1c4-cc83dccf17d4",
|
|
3
|
+
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
4
|
+
"version": "7",
|
|
5
|
+
"dialect": "postgresql",
|
|
6
|
+
"tables": {
|
|
7
|
+
"public.users": {
|
|
8
|
+
"name": "users",
|
|
9
|
+
"schema": "",
|
|
10
|
+
"columns": {
|
|
11
|
+
"id": {
|
|
12
|
+
"name": "id",
|
|
13
|
+
"type": "uuid",
|
|
14
|
+
"primaryKey": true,
|
|
15
|
+
"notNull": true
|
|
16
|
+
},
|
|
17
|
+
"name": {
|
|
18
|
+
"name": "name",
|
|
19
|
+
"type": "varchar(255)",
|
|
20
|
+
"primaryKey": false,
|
|
21
|
+
"notNull": true
|
|
22
|
+
},
|
|
23
|
+
"surname": {
|
|
24
|
+
"name": "surname",
|
|
25
|
+
"type": "varchar(255)",
|
|
26
|
+
"primaryKey": false,
|
|
27
|
+
"notNull": true
|
|
28
|
+
},
|
|
29
|
+
"email": {
|
|
30
|
+
"name": "email",
|
|
31
|
+
"type": "varchar(255)",
|
|
32
|
+
"primaryKey": false,
|
|
33
|
+
"notNull": true
|
|
34
|
+
},
|
|
35
|
+
"created_at": {
|
|
36
|
+
"name": "created_at",
|
|
37
|
+
"type": "timestamp",
|
|
38
|
+
"primaryKey": false,
|
|
39
|
+
"notNull": true,
|
|
40
|
+
"default": "now()"
|
|
41
|
+
},
|
|
42
|
+
"updated_at": {
|
|
43
|
+
"name": "updated_at",
|
|
44
|
+
"type": "timestamp",
|
|
45
|
+
"primaryKey": false,
|
|
46
|
+
"notNull": true,
|
|
47
|
+
"default": "now()"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"indexes": {},
|
|
51
|
+
"foreignKeys": {},
|
|
52
|
+
"compositePrimaryKeys": {},
|
|
53
|
+
"uniqueConstraints": {},
|
|
54
|
+
"policies": {},
|
|
55
|
+
"checkConstraints": {},
|
|
56
|
+
"isRLSEnabled": false
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"enums": {},
|
|
60
|
+
"schemas": {},
|
|
61
|
+
"sequences": {},
|
|
62
|
+
"roles": {},
|
|
63
|
+
"policies": {},
|
|
64
|
+
"views": {},
|
|
65
|
+
"_meta": {
|
|
66
|
+
"columns": {},
|
|
67
|
+
"schemas": {},
|
|
68
|
+
"tables": {}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core';
|
|
2
|
+
import {
|
|
3
|
+
createInsertSchema,
|
|
4
|
+
createSelectSchema,
|
|
5
|
+
createUpdateSchema,
|
|
6
|
+
} from 'drizzle-typebox';
|
|
7
|
+
import { t } from 'elysia';
|
|
8
|
+
|
|
9
|
+
// Users table definition
|
|
10
|
+
export const users = pgTable('users', {
|
|
11
|
+
id: uuid('id')
|
|
12
|
+
.primaryKey()
|
|
13
|
+
.$defaultFn(() => Bun.randomUUIDv7()),
|
|
14
|
+
name: varchar('name', { length: 255 }).notNull(),
|
|
15
|
+
surname: varchar('surname', { length: 255 }).notNull(),
|
|
16
|
+
email: varchar('email', { length: 255 }).notNull(),
|
|
17
|
+
createdAt: timestamp('created_at').notNull().defaultNow(),
|
|
18
|
+
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Field validation refinements for Elysia
|
|
22
|
+
const fieldRefinements = {
|
|
23
|
+
name: t.String({ minLength: 1, maxLength: 255, examples: ['User Name'] }),
|
|
24
|
+
surname: t.String({
|
|
25
|
+
minLength: 1,
|
|
26
|
+
maxLength: 255,
|
|
27
|
+
examples: ['User Surname'],
|
|
28
|
+
}),
|
|
29
|
+
email: t.String({ minLength: 1, maxLength: 255, examples: ['User Email'] }),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Drizzle-TypeBox schemas (declare variables to avoid infinite type instantiation)
|
|
33
|
+
const _userCreate = createInsertSchema(users, fieldRefinements);
|
|
34
|
+
const _userSelect = createSelectSchema(users, fieldRefinements);
|
|
35
|
+
const _userUpdate = createUpdateSchema(users, fieldRefinements);
|
|
36
|
+
|
|
37
|
+
export const userCreate = _userCreate;
|
|
38
|
+
export const userSelect = _userSelect;
|
|
39
|
+
export const userUpdate = _userUpdate;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import cors from '@elysiajs/cors';
|
|
2
|
+
import openapi from '@elysiajs/openapi';
|
|
3
|
+
import { Elysia } from 'elysia';
|
|
4
|
+
import config from './common/config';
|
|
5
|
+
import { log } from './common/logger';
|
|
6
|
+
import { migrateDb } from './db';
|
|
7
|
+
import { errorHandler } from './middleware/error-handler';
|
|
8
|
+
import { users } from './modules/users';
|
|
9
|
+
import { gracefulShutdown } from './util/graceful-shutdown';
|
|
10
|
+
|
|
11
|
+
const app = new Elysia()
|
|
12
|
+
.use(cors())
|
|
13
|
+
.use(errorHandler)
|
|
14
|
+
.use(
|
|
15
|
+
openapi({
|
|
16
|
+
documentation: {
|
|
17
|
+
info: {
|
|
18
|
+
title: 'Elysia Boilerplate',
|
|
19
|
+
version: '0.1.5',
|
|
20
|
+
description: 'A simple boilerplate service for Elysia',
|
|
21
|
+
},
|
|
22
|
+
servers: [
|
|
23
|
+
{
|
|
24
|
+
url: `http://${config.SERVER_HOSTNAME}:${config.SERVER_PORT}`,
|
|
25
|
+
description: 'Local development server',
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
enabled: config.ENABLE_OPENAPI,
|
|
30
|
+
}),
|
|
31
|
+
)
|
|
32
|
+
.use(users);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Bootstrap the application.
|
|
36
|
+
* Runs all initialization tasks before starting the server.
|
|
37
|
+
*/
|
|
38
|
+
async function bootstrap(): Promise<void> {
|
|
39
|
+
// Run database migrations before accepting any requests
|
|
40
|
+
if (config.DB_AUTO_MIGRATE) {
|
|
41
|
+
await migrateDb();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Start the server only after all initialization is complete
|
|
45
|
+
app.listen(config.SERVER_PORT, ({ development, hostname, port }) => {
|
|
46
|
+
log.info(
|
|
47
|
+
`🦊 Elysia is running at http://${hostname}:${port} ${development ? '🚧 in development mode!🚧' : ''}`,
|
|
48
|
+
);
|
|
49
|
+
if (config.ENABLE_OPENAPI) {
|
|
50
|
+
log.info(
|
|
51
|
+
`📚 OpenAPI documentation is available at http://${hostname}:${port}/openapi`,
|
|
52
|
+
);
|
|
53
|
+
} else {
|
|
54
|
+
log.info('📚 OpenAPI documentation is disabled');
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
process.once('SIGINT', () => gracefulShutdown(app, 'SIGINT'));
|
|
59
|
+
process.once('SIGTERM', () => gracefulShutdown(app, 'SIGTERM'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
bootstrap().catch((error) => {
|
|
63
|
+
log.fatal({ err: error }, 'Failed to start application');
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export type App = typeof app;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Elysia, ElysiaCustomStatusResponse, status } from 'elysia';
|
|
2
|
+
import { log as logger } from 'src/common/logger';
|
|
3
|
+
|
|
4
|
+
const log = logger.child({ name: 'error-handler' });
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Global error handling middleware
|
|
8
|
+
* Catches all unhandled errors and logs them
|
|
9
|
+
*/
|
|
10
|
+
export const errorHandler = new Elysia({ name: 'error-handler' }).onError(
|
|
11
|
+
({ code, error, request }) => {
|
|
12
|
+
// Return Elysia's handled errors as-is
|
|
13
|
+
if (error instanceof ElysiaCustomStatusResponse || code !== 'UNKNOWN') {
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Log unhandled errors
|
|
18
|
+
log.error(
|
|
19
|
+
{
|
|
20
|
+
code,
|
|
21
|
+
err: error,
|
|
22
|
+
http: request
|
|
23
|
+
? {
|
|
24
|
+
method: request.method,
|
|
25
|
+
url: request.url,
|
|
26
|
+
referrer: request.headers.get('referer') ?? undefined,
|
|
27
|
+
}
|
|
28
|
+
: undefined,
|
|
29
|
+
},
|
|
30
|
+
'Unhandled error',
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Do not expose unhandled errors to the client
|
|
34
|
+
return status(500, 'Internal Server Error');
|
|
35
|
+
},
|
|
36
|
+
);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Elysia, status } from 'elysia';
|
|
2
|
+
import { log as logger } from 'src/common/logger';
|
|
3
|
+
import { type UsersModel, usersModelPlugin } from './model';
|
|
4
|
+
import { UsersService } from './service';
|
|
5
|
+
|
|
6
|
+
const log = logger.child({ name: 'users' });
|
|
7
|
+
|
|
8
|
+
export const users = new Elysia({ prefix: '/users', tags: ['Users'] })
|
|
9
|
+
.use(usersModelPlugin)
|
|
10
|
+
.post(
|
|
11
|
+
'/',
|
|
12
|
+
async ({ body }): Promise<UsersModel.createResponse> => {
|
|
13
|
+
try {
|
|
14
|
+
const user = await UsersService.create(body);
|
|
15
|
+
log.info(`Created user ${user.name}`);
|
|
16
|
+
return user;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
log.error({ err: error }, 'Failed to create user');
|
|
19
|
+
throw status(422, {
|
|
20
|
+
message: 'Failed to create user' satisfies UsersModel.createError,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
body: 'users.createRequest',
|
|
26
|
+
response: {
|
|
27
|
+
200: 'users.createResponse',
|
|
28
|
+
422: 'users.createError',
|
|
29
|
+
},
|
|
30
|
+
detail: {
|
|
31
|
+
summary: 'Create a new user',
|
|
32
|
+
description: 'Create a new user in the database with name and surname.',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
.get(
|
|
37
|
+
'/',
|
|
38
|
+
async ({ query }): Promise<UsersModel.getResponse> => {
|
|
39
|
+
try {
|
|
40
|
+
const users = await UsersService.get(query);
|
|
41
|
+
log.info(`Got users ${users.total}`);
|
|
42
|
+
return users;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
log.error({ err: error }, 'Failed to get users');
|
|
45
|
+
throw status(422, {
|
|
46
|
+
message: 'Failed to get users' satisfies UsersModel.getError,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
query: 'users.getQuery',
|
|
52
|
+
response: {
|
|
53
|
+
200: 'users.getResponse',
|
|
54
|
+
422: 'users.getError',
|
|
55
|
+
},
|
|
56
|
+
detail: {
|
|
57
|
+
summary: 'Get all users',
|
|
58
|
+
description: 'Get all users from the database with name and surname.',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import Elysia, { t } from 'elysia';
|
|
2
|
+
import { userCreate, userSelect } from '../../db/schema/users';
|
|
3
|
+
|
|
4
|
+
export namespace UsersModel {
|
|
5
|
+
// Create user
|
|
6
|
+
export const createRequest = t.Omit(userCreate, [
|
|
7
|
+
'id',
|
|
8
|
+
'createdAt',
|
|
9
|
+
'updatedAt',
|
|
10
|
+
]);
|
|
11
|
+
export type createRequest = typeof createRequest.static;
|
|
12
|
+
|
|
13
|
+
export const createResponse = t.Omit(userSelect, ['createdAt', 'updatedAt']);
|
|
14
|
+
export type createResponse = typeof createResponse.static;
|
|
15
|
+
|
|
16
|
+
export const createError = t.Literal('Failed to create user');
|
|
17
|
+
export type createError = typeof createError.static;
|
|
18
|
+
|
|
19
|
+
// Get users
|
|
20
|
+
export const getQuery = t.Object({
|
|
21
|
+
limit: t.Number({
|
|
22
|
+
default: 100,
|
|
23
|
+
minimum: 1,
|
|
24
|
+
maximum: 100,
|
|
25
|
+
examples: [100],
|
|
26
|
+
}),
|
|
27
|
+
offset: t.Number({ default: 0, minimum: 0, examples: [0] }),
|
|
28
|
+
});
|
|
29
|
+
export type getQuery = typeof getQuery.static;
|
|
30
|
+
|
|
31
|
+
export const getResponse = t.Object({
|
|
32
|
+
users: t.Array(t.Omit(userSelect, ['createdAt', 'updatedAt'])),
|
|
33
|
+
total: t.Number(),
|
|
34
|
+
});
|
|
35
|
+
export type getResponse = typeof getResponse.static;
|
|
36
|
+
|
|
37
|
+
export const getError = t.Literal('Failed to get users');
|
|
38
|
+
export type getError = typeof getError.static;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const usersModelPlugin = new Elysia().model({
|
|
42
|
+
'users.createRequest': UsersModel.createRequest,
|
|
43
|
+
'users.createResponse': UsersModel.createResponse,
|
|
44
|
+
'users.createError': UsersModel.createError,
|
|
45
|
+
'users.getQuery': UsersModel.getQuery,
|
|
46
|
+
'users.getResponse': UsersModel.getResponse,
|
|
47
|
+
'users.getError': UsersModel.getError,
|
|
48
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { count } from 'drizzle-orm';
|
|
2
|
+
import { status } from 'elysia';
|
|
3
|
+
import db from '../../db';
|
|
4
|
+
import { users } from '../../db/schema/users';
|
|
5
|
+
import type { UsersModel } from './model';
|
|
6
|
+
|
|
7
|
+
export abstract class UsersService {
|
|
8
|
+
static async create(
|
|
9
|
+
body: UsersModel.createRequest,
|
|
10
|
+
): Promise<UsersModel.createResponse> {
|
|
11
|
+
const [user] = await db
|
|
12
|
+
.insert(users)
|
|
13
|
+
.values({ ...body })
|
|
14
|
+
.returning();
|
|
15
|
+
|
|
16
|
+
if (!user) {
|
|
17
|
+
throw status(422, {
|
|
18
|
+
message: 'Failed to create user' satisfies UsersModel.createError,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return user;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static async get(
|
|
26
|
+
params: UsersModel.getQuery,
|
|
27
|
+
): Promise<UsersModel.getResponse> {
|
|
28
|
+
const [total] = await db.select({ count: count() }).from(users);
|
|
29
|
+
|
|
30
|
+
const res = await db
|
|
31
|
+
.select({
|
|
32
|
+
id: users.id,
|
|
33
|
+
name: users.name,
|
|
34
|
+
surname: users.surname,
|
|
35
|
+
email: users.email,
|
|
36
|
+
})
|
|
37
|
+
.from(users)
|
|
38
|
+
.limit(params.limit)
|
|
39
|
+
.offset(params.offset);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
users: res,
|
|
43
|
+
total: total?.count ?? 0,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
|
2
|
+
import type { UsersModel } from 'src/modules/users/model';
|
|
3
|
+
import { users } from '../modules/users/index';
|
|
4
|
+
import { UsersService } from '../modules/users/service';
|
|
5
|
+
|
|
6
|
+
// Mock the UsersService
|
|
7
|
+
const mockUsersService = {
|
|
8
|
+
create: mock(() =>
|
|
9
|
+
Promise.resolve({
|
|
10
|
+
id: '0199477e-7aa7-7000-a65a-96e2efd46c10',
|
|
11
|
+
name: 'John',
|
|
12
|
+
surname: 'Doe',
|
|
13
|
+
email: 'john.doe@example.com',
|
|
14
|
+
}),
|
|
15
|
+
),
|
|
16
|
+
get: mock(() =>
|
|
17
|
+
Promise.resolve({
|
|
18
|
+
users: [
|
|
19
|
+
{
|
|
20
|
+
id: '0199477e-7aa7-7000-a65a-96e2efd46c10',
|
|
21
|
+
name: 'John',
|
|
22
|
+
surname: 'Doe',
|
|
23
|
+
email: 'john.doe@example.com',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
total: 1,
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Mock the service methods
|
|
32
|
+
Object.assign(UsersService, mockUsersService);
|
|
33
|
+
|
|
34
|
+
describe('Users Module', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
mockUsersService.create.mockClear();
|
|
37
|
+
mockUsersService.get.mockClear();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('POST /users', () => {
|
|
41
|
+
it('should create a new user successfully', async () => {
|
|
42
|
+
const userData = {
|
|
43
|
+
name: 'John',
|
|
44
|
+
surname: 'Doe',
|
|
45
|
+
email: 'john.doe@example.com',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const response = await users.handle(
|
|
49
|
+
new Request('http://localhost/users', {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
body: JSON.stringify(userData),
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
expect(response.status).toBe(200);
|
|
56
|
+
|
|
57
|
+
const responseData = await response.json();
|
|
58
|
+
expect(responseData).toHaveProperty('id');
|
|
59
|
+
expect(responseData).toHaveProperty('name', 'John');
|
|
60
|
+
expect(responseData).toHaveProperty('surname', 'Doe');
|
|
61
|
+
expect(responseData).toHaveProperty('email', 'john.doe@example.com');
|
|
62
|
+
|
|
63
|
+
// Verify the service was called with correct data
|
|
64
|
+
expect(mockUsersService.create).toHaveBeenCalledWith(userData);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should return validation error for missing required fields', async () => {
|
|
68
|
+
const invalidUserData = {
|
|
69
|
+
name: 'John',
|
|
70
|
+
// missing surname and email
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const response = await users.handle(
|
|
74
|
+
new Request('http://localhost/users', {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: { 'Content-Type': 'application/json' },
|
|
77
|
+
body: JSON.stringify(invalidUserData),
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
expect(response.status).toBe(422);
|
|
81
|
+
|
|
82
|
+
expect(mockUsersService.create).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('GET /users', () => {
|
|
87
|
+
it('should get all users successfully', async () => {
|
|
88
|
+
const response = await users.handle(
|
|
89
|
+
new Request('http://localhost/users'),
|
|
90
|
+
);
|
|
91
|
+
expect(response.status).toBe(200);
|
|
92
|
+
|
|
93
|
+
const responseData = (await response.json()) as UsersModel.getResponse;
|
|
94
|
+
expect(responseData).toHaveProperty('users');
|
|
95
|
+
expect(responseData).toHaveProperty('total');
|
|
96
|
+
expect(Array.isArray(responseData.users)).toBe(true);
|
|
97
|
+
expect(typeof responseData.total).toBe('number');
|
|
98
|
+
|
|
99
|
+
expect(mockUsersService.get).toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { log as logger } from 'src/common/logger';
|
|
2
|
+
import db from 'src/db';
|
|
3
|
+
import type { App } from 'src/main';
|
|
4
|
+
|
|
5
|
+
const log = logger.child({ name: 'graceful-shutdown' });
|
|
6
|
+
|
|
7
|
+
let isShuttingDown = false;
|
|
8
|
+
|
|
9
|
+
export async function gracefulShutdown(
|
|
10
|
+
app: App,
|
|
11
|
+
signal: NodeJS.Signals,
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
if (isShuttingDown) {
|
|
14
|
+
log.warn('Already shutting down, ignoring signal');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
isShuttingDown = true;
|
|
19
|
+
|
|
20
|
+
log.info(`${signal} received, shutting down...`);
|
|
21
|
+
|
|
22
|
+
const shutdownTimeout = setTimeout(() => {
|
|
23
|
+
log.error('Shutdown timeout exceeded, forcing exit');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}, 10000); // 10 second timeout
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await app.stop();
|
|
29
|
+
await db.$client.end();
|
|
30
|
+
clearTimeout(shutdownTimeout);
|
|
31
|
+
process.exit(0);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
clearTimeout(shutdownTimeout);
|
|
34
|
+
log.error(error, `Error during ${signal} shutdown`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false,
|
|
28
|
+
|
|
29
|
+
// Paths
|
|
30
|
+
"baseUrl": "."
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
"include": ["src/**/*"],
|
|
34
|
+
"exclude": ["node_modules", "dist"]
|
|
35
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"root": false,
|
|
3
|
+
"$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
|
|
4
|
+
"extends": ["../../biome.json"],
|
|
5
|
+
"files": {
|
|
6
|
+
"includes": [
|
|
7
|
+
"**/src/**/*",
|
|
8
|
+
"**/.vscode/**/*",
|
|
9
|
+
"**/drizzle.config.ts",
|
|
10
|
+
"**/tsconfig.json",
|
|
11
|
+
"!**/src/db/migrations/**/*"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VITE_API_URL=http://localhost:3000
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Frontend Boilerplate
|
|
2
|
+
|
|
3
|
+
React boilerplate for ElysiaJS full-stack applications.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun install
|
|
9
|
+
bun run dev # Start dev server on port 5173
|
|
10
|
+
bun run build # Build for production
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Stack
|
|
14
|
+
|
|
15
|
+
- **React 19** with TypeScript
|
|
16
|
+
- **TanStack Router** - File-based routing in `src/routes/`
|
|
17
|
+
- **Tailwind CSS** - Styling with v4
|
|
18
|
+
- **Biome** - Linting & formatting
|
|
19
|
+
- **Vitest** - Testing
|
|
20
|
+
|
|
21
|
+
## Project Structure
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
src/
|
|
25
|
+
├── features/ # Feature modules (e.g., layout)
|
|
26
|
+
├── shared/ # Shared code
|
|
27
|
+
│ ├── components/ # Reusable UI components
|
|
28
|
+
│ ├── hooks/ # Custom hooks
|
|
29
|
+
│ ├── utils/ # Utilities
|
|
30
|
+
│ ├── types/ # Type definitions
|
|
31
|
+
│ └── constants/ # App constants
|
|
32
|
+
├── assets/ # Images, fonts, etc.
|
|
33
|
+
├── routes/ # File-based routes
|
|
34
|
+
└── main.tsx
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Commands
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
bun run dev # Development server
|
|
41
|
+
bun run build # Production build
|
|
42
|
+
bun run preview # Preview production build
|
|
43
|
+
bun run test # Run tests
|
|
44
|
+
bun run lint # Lint code
|
|
45
|
+
bun run format # Format code
|
|
46
|
+
bun run check # Lint + format check
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Adding Routes
|
|
50
|
+
|
|
51
|
+
Create a new file in `src/routes/` - TanStack Router handles the rest automatically.
|
|
52
|
+
|
|
53
|
+
## Shadcn UI
|
|
54
|
+
|
|
55
|
+
Add components:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
bunx shadcn@latest add button
|
|
59
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"root": false,
|
|
3
|
+
"$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
|
|
4
|
+
"extends": ["../../biome.json"],
|
|
5
|
+
"files": {
|
|
6
|
+
"includes": [
|
|
7
|
+
"**/src/**/*",
|
|
8
|
+
"**/.vscode/**/*",
|
|
9
|
+
"**/index.html",
|
|
10
|
+
"**/vite.config.js",
|
|
11
|
+
"!**/src/routeTree.gen.ts",
|
|
12
|
+
"!**/src/styles.css",
|
|
13
|
+
"!**/src/vite-env.d.ts"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": false,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/styles.css",
|
|
9
|
+
"baseColor": "zinc",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"aliases": {
|
|
14
|
+
"components": "@/components",
|
|
15
|
+
"utils": "@/lib/utils",
|
|
16
|
+
"ui": "@/components/ui",
|
|
17
|
+
"lib": "@/lib",
|
|
18
|
+
"hooks": "@/hooks"
|
|
19
|
+
},
|
|
20
|
+
"iconLibrary": "lucide"
|
|
21
|
+
}
|