create-ej-app 16.0.0 → 18.0.0

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.
Files changed (104) hide show
  1. package/README.md +108 -13
  2. package/cli.js +32 -19
  3. package/cli.ts +45 -27
  4. package/package.json +2 -2
  5. package/template/api/.env.example +3 -0
  6. package/template/api/.github/copilot-instructions.md +129 -0
  7. package/template/api/.vscode/extensions.json +9 -0
  8. package/template/api/.vscode/settings.json +3 -0
  9. package/template/api/Dockerfile +23 -0
  10. package/template/api/package.json +31 -0
  11. package/template/api/prettier.config.mjs +11 -0
  12. package/template/api/src/data-access/todo.ts +142 -0
  13. package/template/api/src/db/fields.ts +46 -0
  14. package/template/api/src/db/index.ts +21 -0
  15. package/template/api/src/db/schema.ts +19 -0
  16. package/template/api/src/env.ts +24 -0
  17. package/template/api/src/index.ts +39 -0
  18. package/template/api/src/lib/util.ts +55 -0
  19. package/template/api/src/routes/index.ts +18 -0
  20. package/template/api/src/routes/todo/index.ts +241 -0
  21. package/template/api/src/scripts/seed.ts +25 -0
  22. package/template/api/tsconfig.json +33 -0
  23. package/template/nextjs/drizzle.config.ts +10 -0
  24. package/template/{base → nextjs}/package.json +7 -4
  25. package/template/{base → nextjs}/src/app/not-found.tsx +8 -2
  26. package/template/nextjs/src/instrumentation.ts +26 -0
  27. package/template/nextjs/src/lib/otel.ts +27 -0
  28. package/template/{base → nextjs}/src/lib/safe-action.ts +8 -0
  29. /package/template/{base → api}/drizzle.config.ts +0 -0
  30. /package/template/{base → nextjs}/.dockerignore +0 -0
  31. /package/template/{base → nextjs}/.env.example +0 -0
  32. /package/template/{base → nextjs}/Dockerfile +0 -0
  33. /package/template/{base → nextjs}/components.json +0 -0
  34. /package/template/{base → nextjs}/eslint.config.mjs +0 -0
  35. /package/template/{base → nextjs}/next.config.ts +0 -0
  36. /package/template/{base → nextjs}/postcss.config.mjs +0 -0
  37. /package/template/{base → nextjs}/prettier.config.mjs +0 -0
  38. /package/template/{base → nextjs}/public/placeholder.svg +0 -0
  39. /package/template/{base → nextjs}/src/app/(admin)/admin/page.tsx +0 -0
  40. /package/template/{base → nextjs}/src/app/(admin)/layout.tsx +0 -0
  41. /package/template/{base → nextjs}/src/app/(app)/layout.tsx +0 -0
  42. /package/template/{base → nextjs}/src/app/(auth)/login/email-login-form.tsx +0 -0
  43. /package/template/{base → nextjs}/src/app/(auth)/login/page.tsx +0 -0
  44. /package/template/{base → nextjs}/src/app/(auth)/login/social-login-buttons.tsx +0 -0
  45. /package/template/{base → nextjs}/src/app/(marketing)/layout.tsx +0 -0
  46. /package/template/{base → nextjs}/src/app/(marketing)/page.tsx +0 -0
  47. /package/template/{base → nextjs}/src/app/(marketing)/privacy/page.tsx +0 -0
  48. /package/template/{base → nextjs}/src/app/(marketing)/terms/page.tsx +0 -0
  49. /package/template/{base → nextjs}/src/app/api/[[...slugs]]/route.ts +0 -0
  50. /package/template/{base → nextjs}/src/app/api/auth/[...all]/route.ts +0 -0
  51. /package/template/{base → nextjs}/src/app/favicon.ico +0 -0
  52. /package/template/{base → nextjs}/src/app/globals.css +0 -0
  53. /package/template/{base → nextjs}/src/app/layout.tsx +0 -0
  54. /package/template/{base → nextjs}/src/components/loader.tsx +0 -0
  55. /package/template/{base → nextjs}/src/components/react-query-provider.tsx +0 -0
  56. /package/template/{base → nextjs}/src/components/screen-size.tsx +0 -0
  57. /package/template/{base → nextjs}/src/components/site-footer.tsx +0 -0
  58. /package/template/{base → nextjs}/src/components/theme-toggle.tsx +0 -0
  59. /package/template/{base → nextjs}/src/components/ui/button.tsx +0 -0
  60. /package/template/{base → nextjs}/src/components/ui/card.tsx +0 -0
  61. /package/template/{base → nextjs}/src/components/ui/form.tsx +0 -0
  62. /package/template/{base → nextjs}/src/components/ui/input.tsx +0 -0
  63. /package/template/{base → nextjs}/src/components/ui/label.tsx +0 -0
  64. /package/template/{base → nextjs}/src/components/ui/separator.tsx +0 -0
  65. /package/template/{base → nextjs}/src/components/ui/sonner.tsx +0 -0
  66. /package/template/{base → nextjs}/src/db/index.ts +0 -0
  67. /package/template/{base → nextjs}/src/db/schema.ts +0 -0
  68. /package/template/{base → nextjs}/src/emails/components/email-layout.tsx +0 -0
  69. /package/template/{base → nextjs}/src/emails/magic-link.tsx +0 -0
  70. /package/template/{base → nextjs}/src/emails/test.tsx +0 -0
  71. /package/template/{base → nextjs}/src/emails/welcome.tsx +0 -0
  72. /package/template/{base → nextjs}/src/env.ts +0 -0
  73. /package/template/{base → nextjs}/src/lib/auth-client.ts +0 -0
  74. /package/template/{base → nextjs}/src/lib/auth.ts +0 -0
  75. /package/template/{base → nextjs}/src/lib/cn.ts +0 -0
  76. /package/template/{base → nextjs}/src/lib/delay.ts +0 -0
  77. /package/template/{base → nextjs}/src/lib/eden.ts +0 -0
  78. /package/template/{base → nextjs}/src/lib/email-service.ts +0 -0
  79. /package/template/{base → nextjs}/src/lib/nodemailer.ts +0 -0
  80. /package/template/{base → nextjs}/src/lib/safe-action-client.ts +0 -0
  81. /package/template/{base → nextjs}/src/lib/siteConfig.ts +0 -0
  82. /package/template/{base → nextjs}/src/lib/toast-message.ts +0 -0
  83. /package/template/{base → nextjs}/src/scripts/test-mail.ts +0 -0
  84. /package/template/{base → nextjs}/terraform/artifact-registry.tf +0 -0
  85. /package/template/{base → nextjs}/terraform/backend-service.tf +0 -0
  86. /package/template/{base → nextjs}/terraform/cleanup.sh +0 -0
  87. /package/template/{base → nextjs}/terraform/cloudbuild.tf +0 -0
  88. /package/template/{base → nextjs}/terraform/cloudrun.tf +0 -0
  89. /package/template/{base → nextjs}/terraform/database.tf +0 -0
  90. /package/template/{base → nextjs}/terraform/forwarding-rule.tf +0 -0
  91. /package/template/{base → nextjs}/terraform/ip-address.tf +0 -0
  92. /package/template/{base → nextjs}/terraform/network-endpoint-group.tf +0 -0
  93. /package/template/{base → nextjs}/terraform/network.tf +0 -0
  94. /package/template/{base → nextjs}/terraform/pre.sh +0 -0
  95. /package/template/{base → nextjs}/terraform/providers.tf +0 -0
  96. /package/template/{base → nextjs}/terraform/secret.txt +0 -0
  97. /package/template/{base → nextjs}/terraform/setup.sh +0 -0
  98. /package/template/{base → nextjs}/terraform/ssl-certs.tf +0 -0
  99. /package/template/{base → nextjs}/terraform/target-https-proxy copy.tf +0 -0
  100. /package/template/{base → nextjs}/terraform/target-https-proxy.tf +0 -0
  101. /package/template/{base → nextjs}/terraform/terraform.tfvars +0 -0
  102. /package/template/{base → nextjs}/terraform/url-map.tf +0 -0
  103. /package/template/{base → nextjs}/terraform/variables.tf +0 -0
  104. /package/template/{base → nextjs}/tsconfig.json +0 -0
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Data-access layer for the `todos` resource.
3
+ *
4
+ * All direct database interactions for todos are encapsulated here.
5
+ * Route handlers call these functions and never import `db` directly,
6
+ * keeping database logic separate from HTTP concerns.
7
+ */
8
+ import { asc, count, desc, eq, ilike, InferSelectModel, or } from 'drizzle-orm'
9
+ import { db } from '~/db'
10
+ import { todosTable } from '~/db/schema'
11
+ import { SortOrderEnum } from '~/lib/util'
12
+
13
+ type TodoType = InferSelectModel<typeof todosTable>
14
+
15
+ /** Input parameters accepted by {@link getTodos}. */
16
+ type GetTodosInput = {
17
+ page: number
18
+ limit: number
19
+ sortBy?: keyof TodoType
20
+ sortOrder?: SortOrderEnum
21
+ search?: string
22
+ }
23
+
24
+ /**
25
+ * Fetches a paginated, optionally sorted and searched list of todos.
26
+ *
27
+ * @returns A readonly tuple `[todos, total]`.
28
+ */
29
+ export const getTodos = async ({
30
+ page,
31
+ limit,
32
+ sortBy = 'createdAt',
33
+ sortOrder = SortOrderEnum.DESC,
34
+ search
35
+ }: GetTodosInput) => {
36
+ const where = search
37
+ ? or(
38
+ ilike(todosTable.title, `%${search}%`),
39
+ ilike(todosTable.content, `%${search}%`)
40
+ )
41
+ : undefined
42
+
43
+ const [results, total] = await Promise.all([
44
+ db.query.todosTable.findMany({
45
+ where,
46
+ limit,
47
+ offset: (page - 1) * limit,
48
+ orderBy: [
49
+ sortOrder === SortOrderEnum.ASC
50
+ ? asc(todosTable[sortBy])
51
+ : desc(todosTable[sortBy])
52
+ ]
53
+ }),
54
+ db
55
+ .select({
56
+ count: count()
57
+ })
58
+ .from(todosTable)
59
+ .where(where)
60
+ ])
61
+
62
+ return [results, total[0]?.count || 0] as const
63
+ }
64
+
65
+ /**
66
+ * Fetches a single todo by its UUID.
67
+ * @returns The todo row, or `undefined` if no match was found.
68
+ */
69
+ export const getTodoById = async (id: string) => {
70
+ return await db.query.todosTable.findFirst({
71
+ where: eq(todosTable.id, id)
72
+ })
73
+ }
74
+
75
+ /** Input shape for creating a new todo. */
76
+ type CreateTodoInput = {
77
+ title: string
78
+ content?: string | null
79
+ }
80
+
81
+ /**
82
+ * Inserts a new todo row and returns the generated UUID.
83
+ * @throws If the INSERT succeeds but no id is returned (should never happen).
84
+ */
85
+ export const createTodo = async (input: CreateTodoInput) => {
86
+ const [todo] = await db
87
+ .insert(todosTable)
88
+ .values({
89
+ title: input.title,
90
+ content: input.content
91
+ })
92
+ .returning({
93
+ id: todosTable.id
94
+ })
95
+
96
+ if (!todo?.id) {
97
+ throw new Error('Failed to create todo or not returning id')
98
+ }
99
+
100
+ return todo.id
101
+ }
102
+
103
+ /** Input shape for partially updating an existing todo. */
104
+ type UpdateTodoInput = {
105
+ id: string
106
+ title?: string
107
+ content?: string | null
108
+ }
109
+
110
+ /**
111
+ * Updates an existing todo by UUID.
112
+ * Only the fields present in `input` (besides `id`) are written.
113
+ * @returns `true` if a row was updated, `false` if the id was not found.
114
+ */
115
+ export const updateTodoById = async (input: UpdateTodoInput) => {
116
+ const { id, ...rest } = input
117
+
118
+ const [todo] = await db
119
+ .update(todosTable)
120
+ .set(rest)
121
+ .where(eq(todosTable.id, id))
122
+ .returning({
123
+ id: todosTable.id
124
+ })
125
+
126
+ return !!todo?.id
127
+ }
128
+
129
+ /**
130
+ * Deletes a todo by UUID.
131
+ * @returns `true` if a row was deleted, `false` if the id was not found.
132
+ */
133
+ export const deleteTodoById = async (id: string) => {
134
+ const [todo] = await db
135
+ .delete(todosTable)
136
+ .where(eq(todosTable.id, id))
137
+ .returning({
138
+ id: todosTable.id
139
+ })
140
+
141
+ return !!todo?.id
142
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared / reusable column definitions for Drizzle schemas.
3
+ *
4
+ * Centralising common column definitions here keeps every table
5
+ * consistent (same naming, same types, same defaults) and eliminates
6
+ * duplication across schema files.
7
+ */
8
+ import { timestamp, uuid } from 'drizzle-orm/pg-core'
9
+
10
+ export const commonFieldDefs = {
11
+ /**
12
+ * Standard UUID primary key column.
13
+ * The database generates a random UUID v4 by default.
14
+ */
15
+ id: uuid('id').primaryKey().defaultRandom(),
16
+
17
+ /**
18
+ * Factory for a single timezone-aware timestamp column.
19
+ * @param name – The underlying database column name.
20
+ */
21
+ date: (name: string) =>
22
+ timestamp(name, {
23
+ mode: 'date',
24
+ withTimezone: true
25
+ }),
26
+
27
+ /**
28
+ * Standard `created_at` / `updated_at` audit columns.
29
+ * Both default to the current time at insert; `updated_at` should
30
+ * be refreshed on every UPDATE (via application logic or a trigger).
31
+ */
32
+ dates: {
33
+ createdAt: timestamp('created_at', {
34
+ mode: 'date',
35
+ withTimezone: true
36
+ })
37
+ .notNull()
38
+ .defaultNow(),
39
+ updatedAt: timestamp('updated_at', {
40
+ mode: 'date',
41
+ withTimezone: true
42
+ })
43
+ .notNull()
44
+ .defaultNow()
45
+ }
46
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Drizzle ORM database client.
3
+ *
4
+ * Creates and exports the singleton `db` instance that is used
5
+ * throughout the application to interact with the PostgreSQL database.
6
+ * The Bun-native SQL driver (`bun-sql`) is used for maximum performance.
7
+ *
8
+ * All table definitions from the schema are passed in so that Drizzle's
9
+ * relational query API (`db.query.*`) is fully typed.
10
+ */
11
+ import { drizzle } from 'drizzle-orm/bun-sql'
12
+ import { env } from '~/env'
13
+ import * as schema from './schema'
14
+
15
+ // Instantiate the Drizzle client with the validated DATABASE_URL and the
16
+ // full schema so relational queries (db.query.todosTable.*) are available.
17
+ export const db = drizzle(env.DATABASE_URL, {
18
+ schema: {
19
+ ...schema
20
+ }
21
+ })
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Database schema definitions.
3
+ *
4
+ * All Drizzle table definitions live here and are imported by both the
5
+ * Drizzle client (`db/index.ts`) and the Drizzle Kit config
6
+ * (`drizzle.config.ts`) so that migrations stay in sync with the code.
7
+ */
8
+ import { pgTable, text } from 'drizzle-orm/pg-core'
9
+ import { commonFieldDefs } from './fields'
10
+
11
+ /**
12
+ * `todos` table – stores individual todo items.
13
+ */
14
+ export const todosTable = pgTable('todos', {
15
+ id: commonFieldDefs.id,
16
+ title: text('title').notNull(),
17
+ content: text('content'),
18
+ ...commonFieldDefs.dates
19
+ })
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Validated environment configuration.
3
+ *
4
+ * Uses @t3-oss/env-core together with Zod to parse and validate all
5
+ * required environment variables at startup. The application will
6
+ * throw a descriptive error and refuse to start if any variable is
7
+ * missing or fails its validation rule.
8
+ */
9
+ import { createEnv } from '@t3-oss/env-core'
10
+ import { z } from 'zod'
11
+
12
+ export const env = createEnv({
13
+ server: {
14
+ PORT: z.coerce.number().int().positive(),
15
+ APP_ENV: z.enum(['development', 'staging', 'production']),
16
+ DATABASE_URL: z.url()
17
+ },
18
+
19
+ // Read all variables directly from process.env
20
+ runtimeEnv: process.env,
21
+
22
+ // Treat empty strings the same as missing (undefined) variables
23
+ emptyStringAsUndefined: true
24
+ })
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Application entry point.
3
+ *
4
+ * Bootstraps the Elysia HTTP server, registers all route plugins,
5
+ * mounts the OpenAPI documentation UI, and starts
6
+ * listening on the port defined in the environment.
7
+ */
8
+ import { openapi } from '@elysiajs/openapi'
9
+ import { Elysia } from 'elysia'
10
+ import { env } from './env'
11
+ import { apiRoutes } from './routes'
12
+
13
+ async function main() {
14
+ const app = new Elysia()
15
+
16
+ // Simple liveness probe used by load-balancers and container orchestrators
17
+ app.get('/health', () => 'OK', {
18
+ detail: {
19
+ description: 'Health check endpoint'
20
+ }
21
+ })
22
+
23
+ // Mount all /api/* routes (todo CRUD, etc.)
24
+ app.use(apiRoutes)
25
+
26
+ // Serve interactive OpenAPI docs at GET /docs
27
+ app.use(
28
+ openapi({
29
+ path: '/docs'
30
+ })
31
+ )
32
+
33
+ // Start the Bun HTTP server on the configured port
34
+ app.listen(env.PORT)
35
+
36
+ console.log(`🚀 Server ready at ${app.server?.hostname}:${app.server?.port}`)
37
+ }
38
+
39
+ main()
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Shared utility types, enums, and reusable Zod schema fragments.
3
+ */
4
+ import { z } from 'zod'
5
+
6
+ /**
7
+ * Allowed values for the `sortOrder` query parameter.
8
+ * Maps to the SQL `ASC` / `DESC` keywords.
9
+ */
10
+ export enum SortOrderEnum {
11
+ ASC = 'asc',
12
+ DESC = 'desc'
13
+ }
14
+
15
+ /**
16
+ * Reusable Zod schema fragment for cursor-less offset pagination.
17
+ *
18
+ * - `page` – 1-based page number (defaults to 1)
19
+ * - `limit` – Number of items per page; clamped between 10 and 100 (defaults to 10)
20
+ *
21
+ * Spread this object into a `z.object({})` call to add pagination
22
+ * to any route's query schema.
23
+ */
24
+ export const paginationSchema = {
25
+ page: z.coerce.number().int().positive().optional().default(1),
26
+ limit: z.coerce
27
+ .number()
28
+ .int()
29
+ .positive()
30
+ .min(10)
31
+ .max(100)
32
+ .optional()
33
+ .default(10)
34
+ }
35
+
36
+ /**
37
+ * Generates a reusable Zod schema fragment for sort parameters.
38
+ *
39
+ * @param keys – A non-empty tuple of the allowed `sortBy` field names for
40
+ * the resource (e.g. `['title', 'createdAt', 'updatedAt']`).
41
+ * @returns An object with `sortOrder` and `sortBy` Zod validators that can
42
+ * be spread into a `z.object({})` query schema.
43
+ *
44
+ * @example
45
+ * z.object({
46
+ * ...paginationSchema,
47
+ * ...getSortSchema(['title', 'createdAt', 'updatedAt'])
48
+ * })
49
+ */
50
+ export const getSortSchema = <T extends string>(
51
+ keys: readonly [T, ...T[]]
52
+ ) => ({
53
+ sortOrder: z.enum(SortOrderEnum).optional(),
54
+ sortBy: z.enum(keys).optional()
55
+ })
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Top-level API router.
3
+ *
4
+ * All REST resources are registered here under the `/api` prefix and
5
+ * then mounted onto the root Elysia app in `src/index.ts`.
6
+ *
7
+ * Adding a new resource:
8
+ * 1. Create its router in `src/routes/<resource>/index.ts`.
9
+ * 2. Import it here and call `apiRoutes.use(yourRouter)`.
10
+ */
11
+ import Elysia from 'elysia'
12
+ import { todoRouter } from './todo'
13
+
14
+ export const apiRoutes = new Elysia({
15
+ prefix: '/api'
16
+ })
17
+
18
+ apiRoutes.use(todoRouter)
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Todos resource router.
3
+ *
4
+ * Defines the full CRUD REST API for the `todos` resource mounted at
5
+ * `/api/todos`. Every handler delegates database work to the data-access
6
+ * layer (`~/data-access/todo`) and uses Zod schemas for request
7
+ * validation so that invalid input is rejected before it reaches the DB.
8
+ *
9
+ * Endpoints:
10
+ * GET /api/todos – List todos with pagination, sorting & search
11
+ * GET /api/todos/:id – Fetch a single todo by UUID
12
+ * POST /api/todos – Create a new todo
13
+ * PATCH /api/todos/:id – Partially update an existing todo
14
+ * DELETE /api/todos/:id – Delete a todo
15
+ */
16
+ import Elysia, { status } from 'elysia'
17
+ import { z } from 'zod'
18
+ import {
19
+ createTodo,
20
+ deleteTodoById,
21
+ getTodoById,
22
+ getTodos,
23
+ updateTodoById
24
+ } from '~/data-access/todo'
25
+ import { getSortSchema, paginationSchema } from '~/lib/util'
26
+
27
+ export const todoRouter = new Elysia({ prefix: '/todos' })
28
+
29
+ /**
30
+ * GET /api/todos
31
+ * Returns a paginated, optionally sorted and searched list of todos.
32
+ * Supports full-text search across `title` and `content` columns.
33
+ */
34
+ todoRouter.get(
35
+ '/',
36
+ async ({ query }) => {
37
+ try {
38
+ const [todos, count] = await getTodos(query)
39
+
40
+ return {
41
+ success: true,
42
+ data: {
43
+ page: query.page,
44
+ limit: query.limit,
45
+ hasMore: query.page * query.limit < count,
46
+ todos,
47
+ count
48
+ }
49
+ }
50
+ } catch (err) {
51
+ console.error('Error in GET /api/todos', err)
52
+ return status(500, {
53
+ success: false,
54
+ error: 'Something went wrong.'
55
+ })
56
+ }
57
+ },
58
+ {
59
+ query: z.object({
60
+ ...paginationSchema,
61
+ ...getSortSchema(['title', 'createdAt', 'updatedAt']),
62
+ search: z.string().trim().min(1).max(100).optional()
63
+ }),
64
+ detail: {
65
+ description: 'Get all todos',
66
+ tags: ['Todos']
67
+ }
68
+ }
69
+ )
70
+
71
+ /**
72
+ * GET /api/todos/:id
73
+ * Fetches a single todo by its UUID. Returns 404 if not found.
74
+ */
75
+ todoRouter.get(
76
+ '/:id',
77
+ async ({ params }) => {
78
+ try {
79
+ const todo = await getTodoById(params.id)
80
+
81
+ if (!todo) {
82
+ return status(404, {
83
+ success: false,
84
+ error: 'Todo not found'
85
+ })
86
+ }
87
+
88
+ return {
89
+ success: true,
90
+ data: {
91
+ todo
92
+ }
93
+ }
94
+ } catch (err) {
95
+ console.error('Error in GET /api/todos/:id', err)
96
+ return status(500, {
97
+ success: false,
98
+ error: 'Something went wrong.'
99
+ })
100
+ }
101
+ },
102
+ {
103
+ // Validate that :id is a well-formed UUID before hitting the DB
104
+ params: z.object({
105
+ id: z.uuid()
106
+ }),
107
+ detail: {
108
+ description: 'Get a todo by id',
109
+ tags: ['Todos']
110
+ }
111
+ }
112
+ )
113
+
114
+ /**
115
+ * POST /api/todos
116
+ * Creates a new todo from the request body. Returns the generated UUID.
117
+ */
118
+ todoRouter.post(
119
+ '/',
120
+ async ({ body }) => {
121
+ try {
122
+ const id = await createTodo(body)
123
+
124
+ return {
125
+ success: true,
126
+ data: {
127
+ id
128
+ }
129
+ }
130
+ } catch (err) {
131
+ console.error('Error in POST /api/todos', err)
132
+ return status(500, {
133
+ success: false,
134
+ error: 'Something went wrong.'
135
+ })
136
+ }
137
+ },
138
+ {
139
+ body: z.object({
140
+ title: z.string().trim().min(1).max(255),
141
+ content: z.string().trim().max(5000).nullish()
142
+ }),
143
+ detail: {
144
+ description: 'Create a new todo',
145
+ tags: ['Todos']
146
+ }
147
+ }
148
+ )
149
+
150
+ /**
151
+ * PATCH /api/todos/:id
152
+ * Partially updates a todo. All body fields are optional — only provided
153
+ * fields are written to the database. Returns 404 if the todo does not exist.
154
+ */
155
+ todoRouter.patch(
156
+ '/:id',
157
+ async ({ params, body }) => {
158
+ try {
159
+ const success = await updateTodoById({
160
+ id: params.id,
161
+ ...body
162
+ })
163
+
164
+ if (!success) {
165
+ return status(404, {
166
+ success: false,
167
+ error: 'Todo not found'
168
+ })
169
+ }
170
+
171
+ return {
172
+ success: true,
173
+ data: {
174
+ id: params.id
175
+ }
176
+ }
177
+ } catch (err) {
178
+ console.error('Error in PATCH /api/todos/:id', err)
179
+ return status(500, {
180
+ success: false,
181
+ error: 'Something went wrong.'
182
+ })
183
+ }
184
+ },
185
+ {
186
+ params: z.object({
187
+ id: z.uuid()
188
+ }),
189
+ // Both fields are optional for a true partial-update (PATCH) semantic
190
+ body: z.object({
191
+ title: z.string().trim().min(1).max(255).optional(),
192
+ content: z.string().trim().max(5000).nullish()
193
+ }),
194
+ detail: {
195
+ description: 'Update a todo by id',
196
+ tags: ['Todos']
197
+ }
198
+ }
199
+ )
200
+
201
+ /**
202
+ * DELETE /api/todos/:id
203
+ * Permanently removes a todo by UUID. Returns 404 if not found.
204
+ */
205
+ todoRouter.delete(
206
+ '/:id',
207
+ async ({ params }) => {
208
+ try {
209
+ const success = await deleteTodoById(params.id)
210
+
211
+ if (!success) {
212
+ return status(404, {
213
+ success: false,
214
+ error: 'Todo not found'
215
+ })
216
+ }
217
+
218
+ return {
219
+ success: true,
220
+ data: {
221
+ id: params.id
222
+ }
223
+ }
224
+ } catch (err) {
225
+ console.error('Error in DELETE /api/todos/:id', err)
226
+ return status(500, {
227
+ success: false,
228
+ error: 'Something went wrong.'
229
+ })
230
+ }
231
+ },
232
+ {
233
+ params: z.object({
234
+ id: z.uuid()
235
+ }),
236
+ detail: {
237
+ description: 'Delete a todo by id',
238
+ tags: ['Todos']
239
+ }
240
+ }
241
+ )
@@ -0,0 +1,25 @@
1
+ import { db } from '~/db'
2
+ import { todosTable } from '~/db/schema'
3
+
4
+ const seedData = async () => {
5
+ await db.insert(todosTable).values([
6
+ {
7
+ title: 'Buy groceries',
8
+ content: 'Milk, Bread, Butter, Jam'
9
+ },
10
+ {
11
+ title: 'Finish project',
12
+ content: 'Complete the frontend and backend integration'
13
+ }
14
+ ])
15
+ }
16
+
17
+ if (import.meta.main) {
18
+ try {
19
+ await seedData()
20
+ } catch (err) {
21
+ console.error('Error seeding data:', err)
22
+ } finally {
23
+ process.exit(0)
24
+ }
25
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "lib": ["ESNext"],
5
+ "allowJs": true,
6
+ "checkJs": true,
7
+ "skipLibCheck": true,
8
+ "strict": true,
9
+ "noImplicitAny": true,
10
+ "strictNullChecks": true,
11
+ "strictFunctionTypes": true,
12
+ "noImplicitThis": true,
13
+ "noUncheckedIndexedAccess": true,
14
+ "noUnusedLocals": true,
15
+ "noUnusedParameters": true,
16
+ "noImplicitReturns": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "allowUnreachableCode": false,
19
+ "forceConsistentCasingInFileNames": true,
20
+ "noEmit": true,
21
+ "esModuleInterop": true,
22
+ "module": "ESNext",
23
+ "moduleResolution": "bundler",
24
+ "resolveJsonModule": true,
25
+ "isolatedModules": true,
26
+ "incremental": true,
27
+ "paths": {
28
+ "~/*": ["./src/*"]
29
+ }
30
+ },
31
+ "include": ["**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs"],
32
+ "exclude": ["node_modules"]
33
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'drizzle-kit'
2
+ import { env } from '~/env'
3
+
4
+ export default defineConfig({
5
+ schema: './src/db/schema.ts',
6
+ dialect: 'postgresql',
7
+ dbCredentials: {
8
+ url: env.DATABASE_URL
9
+ }
10
+ })