@raiyyan/brisk 0.0.0-stage → 0.2.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 (206) hide show
  1. package/AGENTS.md +535 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1745 -2
  4. package/dist/bin/brisk.d.ts +3 -0
  5. package/dist/bin/brisk.d.ts.map +1 -0
  6. package/dist/bin/brisk.js +13 -0
  7. package/dist/bin/brisk.js.map +1 -0
  8. package/dist/src/cli/agents.d.ts +7 -0
  9. package/dist/src/cli/agents.d.ts.map +1 -0
  10. package/dist/src/cli/agents.js +55 -0
  11. package/dist/src/cli/agents.js.map +1 -0
  12. package/dist/src/cli/cli.d.ts +5 -0
  13. package/dist/src/cli/cli.d.ts.map +1 -0
  14. package/dist/src/cli/cli.js +113 -0
  15. package/dist/src/cli/cli.js.map +1 -0
  16. package/dist/src/cli/client.d.ts +12 -0
  17. package/dist/src/cli/client.d.ts.map +1 -0
  18. package/dist/src/cli/client.js +76 -0
  19. package/dist/src/cli/client.js.map +1 -0
  20. package/dist/src/cli/init.d.ts +16 -0
  21. package/dist/src/cli/init.d.ts.map +1 -0
  22. package/dist/src/cli/init.js +149 -0
  23. package/dist/src/cli/init.js.map +1 -0
  24. package/dist/src/cli/names.d.ts +14 -0
  25. package/dist/src/cli/names.d.ts.map +1 -0
  26. package/dist/src/cli/names.js +25 -0
  27. package/dist/src/cli/names.js.map +1 -0
  28. package/dist/src/cli/render.d.ts +30 -0
  29. package/dist/src/cli/render.d.ts.map +1 -0
  30. package/dist/src/cli/render.js +101 -0
  31. package/dist/src/cli/render.js.map +1 -0
  32. package/dist/src/cli/system.d.ts +35 -0
  33. package/dist/src/cli/system.d.ts.map +1 -0
  34. package/dist/src/cli/system.js +81 -0
  35. package/dist/src/cli/system.js.map +1 -0
  36. package/dist/src/client/client.d.ts +64 -0
  37. package/dist/src/client/client.d.ts.map +1 -0
  38. package/dist/src/client/client.js +170 -0
  39. package/dist/src/client/client.js.map +1 -0
  40. package/dist/src/client/index.d.ts +8 -0
  41. package/dist/src/client/index.d.ts.map +1 -0
  42. package/dist/src/client/index.js +7 -0
  43. package/dist/src/client/index.js.map +1 -0
  44. package/dist/src/config/env.d.ts +28 -0
  45. package/dist/src/config/env.d.ts.map +1 -0
  46. package/dist/src/config/env.js +35 -0
  47. package/dist/src/config/env.js.map +1 -0
  48. package/dist/src/config/logger.d.ts +32 -0
  49. package/dist/src/config/logger.d.ts.map +1 -0
  50. package/dist/src/config/logger.js +31 -0
  51. package/dist/src/config/logger.js.map +1 -0
  52. package/dist/src/di/container.d.ts +194 -0
  53. package/dist/src/di/container.d.ts.map +1 -0
  54. package/dist/src/di/container.js +225 -0
  55. package/dist/src/di/container.js.map +1 -0
  56. package/dist/src/di/index.d.ts +8 -0
  57. package/dist/src/di/index.d.ts.map +1 -0
  58. package/dist/src/di/index.js +8 -0
  59. package/dist/src/di/index.js.map +1 -0
  60. package/dist/src/errors/http-errors.d.ts +295 -0
  61. package/dist/src/errors/http-errors.d.ts.map +1 -0
  62. package/dist/src/errors/http-errors.js +311 -0
  63. package/dist/src/errors/http-errors.js.map +1 -0
  64. package/dist/src/errors/index.d.ts +8 -0
  65. package/dist/src/errors/index.d.ts.map +1 -0
  66. package/dist/src/errors/index.js +8 -0
  67. package/dist/src/errors/index.js.map +1 -0
  68. package/dist/src/http/app.d.ts +229 -0
  69. package/dist/src/http/app.d.ts.map +1 -0
  70. package/dist/src/http/app.js +45 -0
  71. package/dist/src/http/app.js.map +1 -0
  72. package/dist/src/http/context.d.ts +66 -0
  73. package/dist/src/http/context.d.ts.map +1 -0
  74. package/dist/src/http/context.js +96 -0
  75. package/dist/src/http/context.js.map +1 -0
  76. package/dist/src/http/controller.d.ts +195 -0
  77. package/dist/src/http/controller.d.ts.map +1 -0
  78. package/dist/src/http/controller.js +290 -0
  79. package/dist/src/http/controller.js.map +1 -0
  80. package/dist/src/http/error-handler.d.ts +86 -0
  81. package/dist/src/http/error-handler.d.ts.map +1 -0
  82. package/dist/src/http/error-handler.js +147 -0
  83. package/dist/src/http/error-handler.js.map +1 -0
  84. package/dist/src/http/instance.d.ts +14 -0
  85. package/dist/src/http/instance.d.ts.map +1 -0
  86. package/dist/src/http/instance.js +377 -0
  87. package/dist/src/http/instance.js.map +1 -0
  88. package/dist/src/http/middleware.d.ts +141 -0
  89. package/dist/src/http/middleware.d.ts.map +1 -0
  90. package/dist/src/http/middleware.js +160 -0
  91. package/dist/src/http/middleware.js.map +1 -0
  92. package/dist/src/http/openapi.d.ts +34 -0
  93. package/dist/src/http/openapi.d.ts.map +1 -0
  94. package/dist/src/http/openapi.js +151 -0
  95. package/dist/src/http/openapi.js.map +1 -0
  96. package/dist/src/http/path-params.d.ts +24 -0
  97. package/dist/src/http/path-params.d.ts.map +1 -0
  98. package/dist/src/http/path-params.js +2 -0
  99. package/dist/src/http/path-params.js.map +1 -0
  100. package/dist/src/http/routes.d.ts +141 -0
  101. package/dist/src/http/routes.d.ts.map +1 -0
  102. package/dist/src/http/routes.js +2 -0
  103. package/dist/src/http/routes.js.map +1 -0
  104. package/dist/src/http/server.d.ts +23 -0
  105. package/dist/src/http/server.d.ts.map +1 -0
  106. package/dist/src/http/server.js +22 -0
  107. package/dist/src/http/server.js.map +1 -0
  108. package/dist/src/http/validation.d.ts +16 -0
  109. package/dist/src/http/validation.d.ts.map +1 -0
  110. package/dist/src/http/validation.js +51 -0
  111. package/dist/src/http/validation.js.map +1 -0
  112. package/dist/src/index.d.ts +22 -0
  113. package/dist/src/index.d.ts.map +1 -0
  114. package/dist/src/index.js +19 -0
  115. package/dist/src/index.js.map +1 -0
  116. package/dist/src/testing/index.d.ts +7 -0
  117. package/dist/src/testing/index.d.ts.map +1 -0
  118. package/dist/src/testing/index.js +7 -0
  119. package/dist/src/testing/index.js.map +1 -0
  120. package/dist/src/testing/test-client.d.ts +67 -0
  121. package/dist/src/testing/test-client.d.ts.map +1 -0
  122. package/dist/src/testing/test-client.js +78 -0
  123. package/dist/src/testing/test-client.js.map +1 -0
  124. package/package.json +89 -3
  125. package/starters/minimal/.env.example +3 -0
  126. package/starters/minimal/.oxfmtrc.json +5 -0
  127. package/starters/minimal/.oxlintrc.json +14 -0
  128. package/starters/minimal/AGENTS.md +23 -0
  129. package/starters/minimal/CLAUDE.md +2 -0
  130. package/starters/minimal/README.md +53 -0
  131. package/starters/minimal/_gitignore +3 -0
  132. package/starters/minimal/package.json +29 -0
  133. package/starters/minimal/src/app.ts +14 -0
  134. package/starters/minimal/src/env.ts +7 -0
  135. package/starters/minimal/src/features/index.ts +8 -0
  136. package/starters/minimal/src/features/notes/notes.controller.ts +38 -0
  137. package/starters/minimal/src/features/notes/notes.repository.ts +36 -0
  138. package/starters/minimal/src/features/notes/notes.schema.ts +24 -0
  139. package/starters/minimal/src/features/notes/notes.service.ts +33 -0
  140. package/starters/minimal/src/main.ts +4 -0
  141. package/starters/minimal/src/schemas.ts +26 -0
  142. package/starters/minimal/templates/feature/src/features/index.ts.controllers.insert +1 -0
  143. package/starters/minimal/templates/feature/src/features/index.ts.imports.insert +1 -0
  144. package/starters/minimal/templates/feature/src/features/{{kebab}}/{{kebab}}.controller.ts +38 -0
  145. package/starters/minimal/templates/feature/src/features/{{kebab}}/{{kebab}}.repository.ts +36 -0
  146. package/starters/minimal/templates/feature/src/features/{{kebab}}/{{kebab}}.schema.ts +24 -0
  147. package/starters/minimal/templates/feature/src/features/{{kebab}}/{{kebab}}.service.ts +33 -0
  148. package/starters/minimal/templates/feature/test/{{kebab}}.service.test.ts +20 -0
  149. package/starters/minimal/templates/feature/test/{{kebab}}.test.ts +35 -0
  150. package/starters/minimal/templates/middleware/src/middlewares/{{kebab}}.ts +12 -0
  151. package/starters/minimal/test/notes.service.test.ts +20 -0
  152. package/starters/minimal/test/notes.test.ts +35 -0
  153. package/starters/minimal/tsconfig.json +17 -0
  154. package/starters/minimal/vitest.config.ts +9 -0
  155. package/starters/sqlite-auth/.dockerignore +8 -0
  156. package/starters/sqlite-auth/.env.example +9 -0
  157. package/starters/sqlite-auth/.oxfmtrc.json +5 -0
  158. package/starters/sqlite-auth/.oxlintrc.json +14 -0
  159. package/starters/sqlite-auth/AGENTS.md +23 -0
  160. package/starters/sqlite-auth/CLAUDE.md +2 -0
  161. package/starters/sqlite-auth/Dockerfile +23 -0
  162. package/starters/sqlite-auth/README.md +93 -0
  163. package/starters/sqlite-auth/_gitignore +5 -0
  164. package/starters/sqlite-auth/compose.yaml +14 -0
  165. package/starters/sqlite-auth/package.json +35 -0
  166. package/starters/sqlite-auth/public/.gitkeep +0 -0
  167. package/starters/sqlite-auth/src/app.ts +39 -0
  168. package/starters/sqlite-auth/src/auth/tokens.ts +41 -0
  169. package/starters/sqlite-auth/src/database/clock.ts +4 -0
  170. package/starters/sqlite-auth/src/database/database.ts +29 -0
  171. package/starters/sqlite-auth/src/database/errors.ts +11 -0
  172. package/starters/sqlite-auth/src/database/migrations.ts +29 -0
  173. package/starters/sqlite-auth/src/env.ts +17 -0
  174. package/starters/sqlite-auth/src/features/auth/auth.controller.ts +28 -0
  175. package/starters/sqlite-auth/src/features/index.ts +12 -0
  176. package/starters/sqlite-auth/src/features/notes/notes.controller.ts +39 -0
  177. package/starters/sqlite-auth/src/features/notes/notes.repository.ts +47 -0
  178. package/starters/sqlite-auth/src/features/notes/notes.schema.ts +24 -0
  179. package/starters/sqlite-auth/src/features/notes/notes.service.ts +33 -0
  180. package/starters/sqlite-auth/src/features/users/admin-seed.ts +21 -0
  181. package/starters/sqlite-auth/src/features/users/users.controller.ts +42 -0
  182. package/starters/sqlite-auth/src/features/users/users.errors.ts +13 -0
  183. package/starters/sqlite-auth/src/features/users/users.repository.ts +77 -0
  184. package/starters/sqlite-auth/src/features/users/users.schema.ts +35 -0
  185. package/starters/sqlite-auth/src/features/users/users.service.ts +80 -0
  186. package/starters/sqlite-auth/src/main.ts +4 -0
  187. package/starters/sqlite-auth/src/middlewares/authenticate.ts +29 -0
  188. package/starters/sqlite-auth/src/schemas.ts +26 -0
  189. package/starters/sqlite-auth/templates/feature/src/database/migrations.ts.migrations.insert +5 -0
  190. package/starters/sqlite-auth/templates/feature/src/features/index.ts.controllers.insert +1 -0
  191. package/starters/sqlite-auth/templates/feature/src/features/index.ts.imports.insert +1 -0
  192. package/starters/sqlite-auth/templates/feature/src/features/{{kebab}}/{{kebab}}.controller.ts +39 -0
  193. package/starters/sqlite-auth/templates/feature/src/features/{{kebab}}/{{kebab}}.repository.ts +47 -0
  194. package/starters/sqlite-auth/templates/feature/src/features/{{kebab}}/{{kebab}}.schema.ts +24 -0
  195. package/starters/sqlite-auth/templates/feature/src/features/{{kebab}}/{{kebab}}.service.ts +33 -0
  196. package/starters/sqlite-auth/templates/feature/test/{{kebab}}.service.test.ts +20 -0
  197. package/starters/sqlite-auth/templates/feature/test/{{kebab}}.test.ts +41 -0
  198. package/starters/sqlite-auth/templates/middleware/src/middlewares/{{kebab}}.ts +12 -0
  199. package/starters/sqlite-auth/test/auth.test.ts +18 -0
  200. package/starters/sqlite-auth/test/helpers.ts +13 -0
  201. package/starters/sqlite-auth/test/notes.service.test.ts +20 -0
  202. package/starters/sqlite-auth/test/notes.test.ts +41 -0
  203. package/starters/sqlite-auth/test/users.service.test.ts +25 -0
  204. package/starters/sqlite-auth/test/users.test.ts +164 -0
  205. package/starters/sqlite-auth/tsconfig.json +17 -0
  206. package/starters/sqlite-auth/vitest.config.ts +15 -0
@@ -0,0 +1,24 @@
1
+ import { z } from "@raiyyan/brisk";
2
+ import { isoDate, PageQuery } from "../../schemas.ts";
3
+
4
+ export const CreateNoteBody = z.object({
5
+ name: z.string().trim().min(1).max(200),
6
+ });
7
+
8
+ // Keep .default() out of the create schema: .partial() keeps defaults, and a PATCH would reset omitted fields.
9
+ export const UpdateNoteBody = CreateNoteBody.partial();
10
+
11
+ export const NoteIdParams = z.object({ id: z.uuid() });
12
+
13
+ export const ListNotesQuery = PageQuery;
14
+
15
+ /** What clients see of a note: fields not listed here never leave the server. */
16
+ export const NoteResponse = z.object({
17
+ id: z.string(),
18
+ name: z.string(),
19
+ createdAt: isoDate(),
20
+ });
21
+
22
+ export type CreateNoteBody = z.infer<typeof CreateNoteBody>;
23
+ export type UpdateNoteBody = z.infer<typeof UpdateNoteBody>;
24
+ export type ListNotesQuery = z.infer<typeof ListNotesQuery>;
@@ -0,0 +1,33 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { inject, NotFoundError } from "@raiyyan/brisk";
3
+ import { NotesRepository, type Note } from "./notes.repository.ts";
4
+ import type { CreateNoteBody, ListNotesQuery, UpdateNoteBody } from "./notes.schema.ts";
5
+
6
+ /** Business rules for notes. Throws errors the client understands; knows nothing about Express. */
7
+ export class NotesService {
8
+ readonly #repository = inject(NotesRepository);
9
+
10
+ async list(query: ListNotesQuery) {
11
+ const { items, total } = await this.#repository.list(query);
12
+ return { items, total, page: query.page, limit: query.limit };
13
+ }
14
+
15
+ async get(id: string): Promise<Note> {
16
+ const row = await this.#repository.findById(id);
17
+ if (!row) throw new NotFoundError("Note not found", { details: { id } });
18
+ return row;
19
+ }
20
+
21
+ create(input: CreateNoteBody): Promise<Note> {
22
+ return this.#repository.insert({ id: randomUUID(), name: input.name, createdAt: new Date() });
23
+ }
24
+
25
+ async update(id: string, { name }: UpdateNoteBody): Promise<Note> {
26
+ const row = await this.get(id);
27
+ return this.#repository.update({ ...row, name: name ?? row.name });
28
+ }
29
+
30
+ async remove(id: string): Promise<void> {
31
+ if (!(await this.#repository.delete(id))) throw new NotFoundError("Note not found", { details: { id } });
32
+ }
33
+ }
@@ -0,0 +1,4 @@
1
+ import { app } from "./app.ts";
2
+ import { env } from "./env.ts";
3
+
4
+ await app.listen(env.PORT);
@@ -0,0 +1,26 @@
1
+ import { z } from "@raiyyan/brisk";
2
+
3
+ /**
4
+ * A response field for a timestamp: accepts a `Date` or an ISO string and always sends an ISO 8601 string,
5
+ * so clients type it as `string` and OpenAPI documents it as `date-time`.
6
+ */
7
+ export const isoDate = () =>
8
+ z
9
+ .union([z.date(), z.string()])
10
+ .transform((value) => (value instanceof Date ? value : new Date(value)).toISOString())
11
+ .pipe(z.iso.datetime());
12
+
13
+ /** Query fields for paged lists: `page` from 1, and `limit` up to 100. */
14
+ export const PageQuery = z.object({
15
+ page: z.coerce.number().int().min(1).default(1),
16
+ limit: z.coerce.number().int().min(1).max(100).default(20),
17
+ });
18
+
19
+ /** A page of `item`s, with the total across all pages. */
20
+ export const page = <Item extends z.ZodType>(item: Item) =>
21
+ z.object({
22
+ items: z.array(item),
23
+ page: z.number().int(),
24
+ limit: z.number().int(),
25
+ total: z.number().int(),
26
+ });
@@ -0,0 +1 @@
1
+ {{camel}}: {{camel}}Controller,
@@ -0,0 +1 @@
1
+ import { {{camel}}Controller } from "./{{kebab}}/{{kebab}}.controller.ts";
@@ -0,0 +1,38 @@
1
+ import { defineController, inject } from "@raiyyan/brisk";
2
+ import { page } from "../../schemas.ts";
3
+ import {
4
+ Create{{entity}}Body,
5
+ {{entity}}IdParams,
6
+ {{entity}}Response,
7
+ List{{pascal}}Query,
8
+ Update{{entity}}Body,
9
+ } from "./{{kebab}}.schema.ts";
10
+ import { {{pascal}}Service } from "./{{kebab}}.service.ts";
11
+
12
+ export const {{camel}}Controller = defineController({ prefix: "/{{kebab}}" }, (route) => {
13
+ const {{camel}} = inject({{pascal}}Service);
14
+
15
+ return {
16
+ list: route.get("/", { query: List{{pascal}}Query, response: page({{entity}}Response) }, ({ query }) =>
17
+ {{camel}}.list(query),
18
+ ),
19
+
20
+ get: route.get("/:id", { params: {{entity}}IdParams, response: {{entity}}Response }, ({ params }) =>
21
+ {{camel}}.get(params.id),
22
+ ),
23
+
24
+ create: route.post("/", { body: Create{{entity}}Body, response: {{entity}}Response }, ({ body }) =>
25
+ {{camel}}.create(body),
26
+ ),
27
+
28
+ update: route.patch(
29
+ "/:id",
30
+ { params: {{entity}}IdParams, body: Update{{entity}}Body, response: {{entity}}Response },
31
+ ({ params, body }) => {{camel}}.update(params.id, body),
32
+ ),
33
+
34
+ remove: route.delete("/:id", { params: {{entity}}IdParams }, async ({ params }) => {
35
+ await {{camel}}.remove(params.id);
36
+ }),
37
+ };
38
+ });
@@ -0,0 +1,36 @@
1
+ export interface {{entity}} {
2
+ id: string;
3
+ name: string;
4
+ createdAt: Date;
5
+ }
6
+
7
+ /**
8
+ * In-memory storage behind an async interface, the shape a database gives: swap the map for your database and
9
+ * the service does not change. Rows are lost on restart.
10
+ */
11
+ export class {{pascal}}Repository {
12
+ readonly #rows = new Map<string, {{entity}}>();
13
+
14
+ async list({ page, limit }: { page: number; limit: number }): Promise<{ items: {{entity}}[]; total: number }> {
15
+ const rows = [...this.#rows.values()].toReversed();
16
+ return { items: rows.slice((page - 1) * limit, page * limit), total: rows.length };
17
+ }
18
+
19
+ async findById(id: string): Promise<{{entity}} | undefined> {
20
+ return this.#rows.get(id);
21
+ }
22
+
23
+ async insert(row: {{entity}}): Promise<{{entity}}> {
24
+ this.#rows.set(row.id, row);
25
+ return row;
26
+ }
27
+
28
+ async update(row: {{entity}}): Promise<{{entity}}> {
29
+ this.#rows.set(row.id, row);
30
+ return row;
31
+ }
32
+
33
+ async delete(id: string): Promise<boolean> {
34
+ return this.#rows.delete(id);
35
+ }
36
+ }
@@ -0,0 +1,24 @@
1
+ import { z } from "@raiyyan/brisk";
2
+ import { isoDate, PageQuery } from "../../schemas.ts";
3
+
4
+ export const Create{{entity}}Body = z.object({
5
+ name: z.string().trim().min(1).max(200),
6
+ });
7
+
8
+ // Keep .default() out of the create schema: .partial() keeps defaults, and a PATCH would reset omitted fields.
9
+ export const Update{{entity}}Body = Create{{entity}}Body.partial();
10
+
11
+ export const {{entity}}IdParams = z.object({ id: z.uuid() });
12
+
13
+ export const List{{pascal}}Query = PageQuery;
14
+
15
+ /** What clients see of a {{entityCamel}}: fields not listed here never leave the server. */
16
+ export const {{entity}}Response = z.object({
17
+ id: z.string(),
18
+ name: z.string(),
19
+ createdAt: isoDate(),
20
+ });
21
+
22
+ export type Create{{entity}}Body = z.infer<typeof Create{{entity}}Body>;
23
+ export type Update{{entity}}Body = z.infer<typeof Update{{entity}}Body>;
24
+ export type List{{pascal}}Query = z.infer<typeof List{{pascal}}Query>;
@@ -0,0 +1,33 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { inject, NotFoundError } from "@raiyyan/brisk";
3
+ import { {{pascal}}Repository, type {{entity}} } from "./{{kebab}}.repository.ts";
4
+ import type { Create{{entity}}Body, List{{pascal}}Query, Update{{entity}}Body } from "./{{kebab}}.schema.ts";
5
+
6
+ /** Business rules for {{camel}}. Throws errors the client understands; knows nothing about Express. */
7
+ export class {{pascal}}Service {
8
+ readonly #repository = inject({{pascal}}Repository);
9
+
10
+ async list(query: List{{pascal}}Query) {
11
+ const { items, total } = await this.#repository.list(query);
12
+ return { items, total, page: query.page, limit: query.limit };
13
+ }
14
+
15
+ async get(id: string): Promise<{{entity}}> {
16
+ const row = await this.#repository.findById(id);
17
+ if (!row) throw new NotFoundError("{{entity}} not found", { details: { id } });
18
+ return row;
19
+ }
20
+
21
+ create(input: Create{{entity}}Body): Promise<{{entity}}> {
22
+ return this.#repository.insert({ id: randomUUID(), name: input.name, createdAt: new Date() });
23
+ }
24
+
25
+ async update(id: string, { name }: Update{{entity}}Body): Promise<{{entity}}> {
26
+ const row = await this.get(id);
27
+ return this.#repository.update({ ...row, name: name ?? row.name });
28
+ }
29
+
30
+ async remove(id: string): Promise<void> {
31
+ if (!(await this.#repository.delete(id))) throw new NotFoundError("{{entity}} not found", { details: { id } });
32
+ }
33
+ }
@@ -0,0 +1,20 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Container, NotFoundError } from "@raiyyan/brisk";
3
+ import { afterAll, describe, expect, it } from "vitest";
4
+ import { {{pascal}}Service } from "../src/features/{{kebab}}/{{kebab}}.service.ts";
5
+
6
+ const container = new Container();
7
+ const {{camel}} = container.get({{pascal}}Service);
8
+ await container.init();
9
+ afterAll(() => container.shutdown(() => {}));
10
+
11
+ describe("{{pascal}}Service", () => {
12
+ it("creates a {{entityCamel}} and finds it again", async () => {
13
+ const created = await {{camel}}.create({ name: "unit" });
14
+ expect(await {{camel}}.get(created.id)).toEqual(created);
15
+ });
16
+
17
+ it("throws NotFoundError for an id that does not exist", async () => {
18
+ await expect({{camel}}.get(randomUUID())).rejects.toBeInstanceOf(NotFoundError);
19
+ });
20
+ });
@@ -0,0 +1,35 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createTestClient } from "@raiyyan/brisk/testing";
3
+ import { afterAll, describe, expect, it } from "vitest";
4
+ import { app } from "../src/app.ts";
5
+
6
+ const api = await createTestClient(app);
7
+ afterAll(() => api.close());
8
+
9
+ describe("{{kebab}} API", () => {
10
+ it("creates, reads, lists, updates and deletes", async () => {
11
+ const created = await api.call.{{camel}}.create({ body: { name: "first" } });
12
+ const params = { id: created.id };
13
+ expect(created).toMatchObject({ name: "first" });
14
+
15
+ expect(await api.call.{{camel}}.get({ params })).toEqual(created);
16
+ expect((await api.call.{{camel}}.list({ query: { limit: 100 } })).items).toContainEqual(created);
17
+ expect(await api.call.{{camel}}.update({ params, body: { name: "second" } })).toMatchObject({
18
+ id: created.id,
19
+ name: "second",
20
+ });
21
+
22
+ await api.call.{{camel}}.remove({ params });
23
+ await expect(api.call.{{camel}}.get({ params })).rejects.toMatchObject({ status: 404 });
24
+ });
25
+
26
+ it("rejects invalid input with the field that failed", async () => {
27
+ expect((await api.post("/api/{{kebab}}", { body: { name: "" } })).status).toBe(400);
28
+ });
29
+
30
+ it("answers 404 for an id that does not exist", async () => {
31
+ await expect(api.call.{{camel}}.get({ params: { id: randomUUID() } })).rejects.toMatchObject({
32
+ status: 404,
33
+ });
34
+ });
35
+ });
@@ -0,0 +1,12 @@
1
+ import { BadRequestError, defineMiddleware } from "@raiyyan/brisk";
2
+
3
+ /**
4
+ * Reads the `x-{{kebab}}` header and adds `{{camel}}` to the ctx of every route that uses it. Replace the check
5
+ * with your own; return nothing for a check-only middleware. To validate path params or a query first,
6
+ * declare them: `defineMiddleware({ params: z.object({ ... }) }, ({ params }) => ...)`.
7
+ */
8
+ export const {{camel}} = defineMiddleware(({ req }) => {
9
+ const value = req.get("x-{{kebab}}");
10
+ if (!value) throw new BadRequestError("Missing x-{{kebab}} header");
11
+ return { {{camel}}: value };
12
+ });
@@ -0,0 +1,20 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Container, NotFoundError } from "@raiyyan/brisk";
3
+ import { afterAll, describe, expect, it } from "vitest";
4
+ import { NotesService } from "../src/features/notes/notes.service.ts";
5
+
6
+ const container = new Container();
7
+ const notes = container.get(NotesService);
8
+ await container.init();
9
+ afterAll(() => container.shutdown(() => {}));
10
+
11
+ describe("NotesService", () => {
12
+ it("creates a note and finds it again", async () => {
13
+ const created = await notes.create({ name: "unit" });
14
+ expect(await notes.get(created.id)).toEqual(created);
15
+ });
16
+
17
+ it("throws NotFoundError for an id that does not exist", async () => {
18
+ await expect(notes.get(randomUUID())).rejects.toBeInstanceOf(NotFoundError);
19
+ });
20
+ });
@@ -0,0 +1,35 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createTestClient } from "@raiyyan/brisk/testing";
3
+ import { afterAll, describe, expect, it } from "vitest";
4
+ import { app } from "../src/app.ts";
5
+
6
+ const api = await createTestClient(app);
7
+ afterAll(() => api.close());
8
+
9
+ describe("notes API", () => {
10
+ it("creates, reads, lists, updates and deletes", async () => {
11
+ const created = await api.call.notes.create({ body: { name: "first" } });
12
+ const params = { id: created.id };
13
+ expect(created).toMatchObject({ name: "first" });
14
+
15
+ expect(await api.call.notes.get({ params })).toEqual(created);
16
+ expect((await api.call.notes.list({ query: { limit: 100 } })).items).toContainEqual(created);
17
+ expect(await api.call.notes.update({ params, body: { name: "second" } })).toMatchObject({
18
+ id: created.id,
19
+ name: "second",
20
+ });
21
+
22
+ await api.call.notes.remove({ params });
23
+ await expect(api.call.notes.get({ params })).rejects.toMatchObject({ status: 404 });
24
+ });
25
+
26
+ it("rejects invalid input with the field that failed", async () => {
27
+ expect((await api.post("/api/notes", { body: { name: "" } })).status).toBe(400);
28
+ });
29
+
30
+ it("answers 404 for an id that does not exist", async () => {
31
+ await expect(api.call.notes.get({ params: { id: randomUUID() } })).rejects.toMatchObject({
32
+ status: 404,
33
+ });
34
+ });
35
+ });
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2024",
4
+ "module": "nodenext",
5
+ "moduleResolution": "nodenext",
6
+ "strict": true,
7
+ "exactOptionalPropertyTypes": true,
8
+ "noUncheckedIndexedAccess": true,
9
+ "noEmit": true,
10
+ "allowImportingTsExtensions": true,
11
+ "verbatimModuleSyntax": true,
12
+ "erasableSyntaxOnly": true,
13
+ "skipLibCheck": true,
14
+ "types": ["node"]
15
+ },
16
+ "include": ["src", "test", "vitest.config.ts"]
17
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ // templates/ holds the files `brisk g` renders; they are not tests until rendered.
6
+ include: ["test/**/*.test.ts"],
7
+ env: { NODE_ENV: "test" },
8
+ },
9
+ });
@@ -0,0 +1,8 @@
1
+ **/node_modules
2
+ .env
3
+ coverage
4
+ .git
5
+ test
6
+ templates
7
+ *.db
8
+ *.db-*
@@ -0,0 +1,9 @@
1
+ NODE_ENV=development
2
+ PORT=3000
3
+ LOG_LEVEL=info
4
+ DATABASE_PATH=data.db
5
+ AUTH_SECRET=replace-with-32-or-more-random-characters
6
+ WEB_ORIGIN=http://localhost:5173
7
+ # Optional: the first admin, created at boot
8
+ ADMIN_EMAIL=admin@example.com
9
+ ADMIN_PASSWORD=change-me-to-a-long-password
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
3
+ "printWidth": 110,
4
+ "ignorePatterns": ["templates", "node_modules", "coverage"]
5
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3
+ "plugins": ["typescript", "import", "unicorn", "oxc", "vitest"],
4
+ "categories": {
5
+ "correctness": "error",
6
+ "suspicious": "warn"
7
+ },
8
+ "rules": {
9
+ "typescript/no-floating-promises": "error",
10
+ "typescript/no-misused-promises": "error",
11
+ "import/no-cycle": "error"
12
+ },
13
+ "ignorePatterns": ["templates", "node_modules", "coverage"]
14
+ }
@@ -0,0 +1,23 @@
1
+ # AGENTS.md
2
+
3
+ <!-- brisk:start -->
4
+
5
+ ## Building with brisk
6
+
7
+ This app uses [brisk](https://www.npmjs.com/package/@raiyyan/brisk), a typed layer over Express 5 and Zod.
8
+ Before writing or changing server code, read the full guide: `node_modules/@raiyyan/brisk/AGENTS.md`.
9
+
10
+ Essentials:
11
+
12
+ - The app is declared once in `src/app.ts` with `defineApp`; `app.listen(port)` runs it, `createTestClient(app, { providers })` tests it.
13
+ - Routes are named: a controller's setup returns `{ list: route.get(...), create: route.post(...) }`. The names form the typed client (`api.users.create({ body })`).
14
+ - Validate every input with a Zod schema on the route; set `response` to a DTO schema so internal fields never leak.
15
+ - New code follows this project's own templates: `npx brisk g feature <name>` renders `templates/feature/`. Edit the templates when the project's conventions change.
16
+ - Dependencies: `readonly #repo = inject(Repository)` in class fields. No decorators, no constructor wiring.
17
+ - Errors: `throw new NotFoundError("…")` (or another error class) anywhere. Never try/catch to build responses; never call `next()`.
18
+ - Shared request logic: `defineMiddleware` returning an object types `ctx`; declare `params` or `query` it reads to get them validated first. Express middleware such as multer goes in a route's `use` through `fromExpress()`.
19
+ - App-wide middleware goes in `defineApp({ use })` (`fromExpress(cors())` included); handlers that are not brisk routes (static files, GraphQL, webhooks) go in `defineApp({ mount })`. Express settings go in `http.settings`.
20
+ - Long-lived work (WebSockets, jobs, consumers, pools) is a provider class with `onInit`, `onReady`, `onDrain`, `onShutdown`; a feature lists its own in `defineController({ providers })`.
21
+ - Test with `createTestClient` from `@raiyyan/brisk/testing`: `api.call.<controller>.<route>(input)` is typed; `api.post(path)` checks raw HTTP.
22
+
23
+ <!-- brisk:end -->
@@ -0,0 +1,2 @@
1
+ @AGENTS.md
2
+ @node_modules/@raiyyan/brisk/AGENTS.md
@@ -0,0 +1,23 @@
1
+ # Node runs the TypeScript sources directly: no build step.
2
+ FROM node:26-slim
3
+ WORKDIR /app
4
+ ENV NODE_ENV=production
5
+
6
+ # Production dependencies only, with whichever package manager the lockfile belongs to.
7
+ COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* ./
8
+ RUN if [ -f pnpm-lock.yaml ]; then corepack enable && pnpm install --prod --frozen-lockfile; \
9
+ elif [ -f yarn.lock ]; then corepack enable && yarn install --production --frozen-lockfile; \
10
+ else npm ci --omit=dev; fi
11
+
12
+ COPY --chown=node:node tsconfig.json ./
13
+ COPY --chown=node:node src ./src
14
+ # The app runs as `node` and may write only to /app/data: mount a volume there (compose.yaml does).
15
+ RUN mkdir -p /app/data && chown node:node /app/data
16
+ USER node
17
+ EXPOSE 3000
18
+
19
+ # Docker uses this; Kubernetes and Podman's default OCI format ignore it and use their own probes on /health.
20
+ HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
21
+ CMD node -e "fetch('http://127.0.0.1:' + (process.env.PORT ?? 3000) + '/health').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))"
22
+ # SIGTERM from `docker stop` starts brisk's graceful shutdown.
23
+ CMD ["node", "src/main.ts"]
@@ -0,0 +1,93 @@
1
+ # sqlite-auth
2
+
3
+ An API built with [brisk](https://github.com/MohammadRaiyyan/brisk) on Express 5 and Zod, with a SQLite database and bearer-token accounts. Created with `npx brisk init --starter sqlite-auth`.
4
+
5
+ What it brings:
6
+
7
+ - SQLite through Node's built-in `node:sqlite` (no driver dependency), opened at startup with the migrations it has not applied yet, and closed on shutdown.
8
+ - Accounts: passwords hashed with scrypt, HS256 bearer tokens signed with `AUTH_SECRET`, and `authenticate` / `requireRole` middlewares.
9
+ - The first admin, created at startup from `ADMIN_EMAIL` and `ADMIN_PASSWORD` when that user does not exist.
10
+ - CORS for `WEB_ORIGIN`, a per-minute rate limit, and static files from `public/` under `/static`.
11
+ - A Dockerfile and `compose.yaml` that keep the database in a volume.
12
+
13
+ ## Run it
14
+
15
+ ```bash
16
+ cp .env.example .env # then set AUTH_SECRET to 32+ random characters: openssl rand -base64 48
17
+ npm run dev
18
+ ```
19
+
20
+ The API listens on port 3000 under `/api`. `GET /health` is the health check and `GET /openapi.json` the OpenAPI document.
21
+
22
+ ```bash
23
+ curl -X POST localhost:3000/api/auth/login -H 'content-type: application/json' \
24
+ -d '{"email":"admin@example.com","password":"change-me-to-a-long-password"}'
25
+ curl localhost:3000/api/users/me -H "authorization: Bearer <token>"
26
+ ```
27
+
28
+ | Script | What it does |
29
+ | ------------------------- | ------------------------------------------ |
30
+ | `dev` | Runs the server and restarts it on changes |
31
+ | `start` | Runs the server |
32
+ | `test` | Runs the tests (vitest, in-memory SQLite) |
33
+ | `typecheck` | Checks types (`tsc --noEmit`) |
34
+ | `lint` | oxlint |
35
+ | `format` / `format:check` | oxfmt |
36
+ | `brisk g <kind> <name>` | Generates code from `templates/<kind>/` |
37
+
38
+ Node 24 or later runs the TypeScript sources directly; there is no build step.
39
+
40
+ ## Environment
41
+
42
+ | Variable | Default | What it sets |
43
+ | ------------------------------- | ----------------------- | ----------------------------------------------------- |
44
+ | `NODE_ENV` | `development` | `development`, `test` or `production` |
45
+ | `PORT` | `3000` | HTTP port |
46
+ | `LOG_LEVEL` | `info` | pino log level |
47
+ | `DATABASE_PATH` | `data.db` | SQLite file |
48
+ | `AUTH_SECRET` | required | HMAC key for tokens, 32 characters or more |
49
+ | `AUTH_TOKEN_TTL_SECONDS` | `3600` | Token lifetime |
50
+ | `WEB_ORIGIN` | `http://localhost:5173` | Browser origin allowed by CORS |
51
+ | `RATE_LIMIT_PER_MINUTE` | `300` | Requests per client per minute |
52
+ | `ADMIN_EMAIL`, `ADMIN_PASSWORD` | not set | The first admin, created at startup when both are set |
53
+
54
+ The app refuses to start when a variable is invalid. Behind a proxy, set `http: { settings: { "trust proxy": 1 } }` in `src/app.ts` so the rate limit counts clients, not the proxy. The rate limit is kept in memory, per instance.
55
+
56
+ ## Layout
57
+
58
+ | Path | What it holds |
59
+ | -------------------- | ----------------------------------------------------------------------------------- |
60
+ | `src/env.ts` | Environment variables, validated at startup |
61
+ | `src/app.ts` | The app: controllers, CORS, rate limit, static files, error mappers |
62
+ | `src/database/` | The `Database` provider, migrations, the `Clock` token, the unique-violation mapper |
63
+ | `src/auth/tokens.ts` | Signing and verifying tokens |
64
+ | `src/middlewares/` | `authenticate` and `requireRole` |
65
+ | `src/features/` | `auth` (login), `users`, and the example `notes`, registered in `index.ts` |
66
+ | `src/schemas.ts` | Shared schema helpers: `isoDate()`, `PageQuery`, `page(item)` |
67
+ | `templates/` | What `brisk g` renders: `feature` and `middleware`. Yours to edit |
68
+ | `test/` | HTTP and service tests; `helpers.ts` signs in as the admin |
69
+
70
+ ## Add a feature
71
+
72
+ ```bash
73
+ npx brisk g feature products
74
+ ```
75
+
76
+ This writes `src/features/products/` (behind `authenticate`), its tests, registers the controller in `src/features/index.ts` and appends a `CREATE TABLE products` migration to `src/database/migrations.ts`. Keep the `@brisk:` marker lines in those files. Migrations run in order at startup; append new ones and never edit one that has shipped.
77
+
78
+ `npx brisk g middleware tenant` writes `src/middlewares/tenant.ts`. To change what new code looks like, edit the files in `templates/`.
79
+
80
+ The `notes` feature is an example generated from `templates/feature`. Before anything ships, you can delete `src/features/notes/`, `test/notes.test.ts`, `test/notes.service.test.ts`, its two lines in `src/features/index.ts` and its migration.
81
+
82
+ ## Docker
83
+
84
+ ```bash
85
+ docker compose up --build
86
+ ```
87
+
88
+ The image installs production dependencies with whichever lockfile is present (pnpm, yarn or npm), runs as the `node` user, and may write only to `/app/data`. `compose.yaml` mounts a volume there and sets `DATABASE_PATH=/app/data/app.db`, so the database survives new images. `docker stop` sends SIGTERM, which starts a graceful shutdown.
89
+
90
+ ## Learn more
91
+
92
+ - [brisk documentation](https://github.com/MohammadRaiyyan/brisk#readme)
93
+ - `AGENTS.md` and `CLAUDE.md` point coding agents at the brisk guide (`npx brisk agents` refreshes them).
@@ -0,0 +1,5 @@
1
+ node_modules
2
+ .env
3
+ coverage
4
+ *.db
5
+ *.db-*
@@ -0,0 +1,14 @@
1
+ services:
2
+ app:
3
+ build: .
4
+ env_file: .env
5
+ ports:
6
+ - "3000:3000"
7
+ stop_grace_period: 30s
8
+ environment:
9
+ DATABASE_PATH: /app/data/app.db
10
+ volumes:
11
+ - data:/app/data
12
+
13
+ volumes:
14
+ data:
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "starter-sqlite-auth",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "node --watch src/main.ts",
7
+ "start": "node src/main.ts",
8
+ "typecheck": "tsc --noEmit",
9
+ "test": "vitest run",
10
+ "brisk": "brisk",
11
+ "lint": "oxlint --type-aware",
12
+ "format": "oxfmt",
13
+ "format:check": "oxfmt --check"
14
+ },
15
+ "dependencies": {
16
+ "@raiyyan/brisk": "workspace:*",
17
+ "cors": "^2.8.6",
18
+ "express": "^5.3.0",
19
+ "express-rate-limit": "^8.7.4"
20
+ },
21
+ "devDependencies": {
22
+ "@types/cors": "^2.8.19",
23
+ "@types/express": "^5.0.6",
24
+ "@types/node": "^26.6.6",
25
+ "oxfmt": "^0.78.0",
26
+ "oxlint": "^1.93.0",
27
+ "oxlint-tsgolint": "^7.0.2007",
28
+ "pino-pretty": "^13.2.0",
29
+ "typescript": "^7.0.2",
30
+ "vitest": "^5.1.0"
31
+ },
32
+ "engines": {
33
+ "node": ">=24"
34
+ }
35
+ }
File without changes