@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.
- package/AGENTS.md +535 -0
- package/LICENSE +21 -0
- package/README.md +1745 -2
- package/dist/bin/brisk.d.ts +3 -0
- package/dist/bin/brisk.d.ts.map +1 -0
- package/dist/bin/brisk.js +13 -0
- package/dist/bin/brisk.js.map +1 -0
- package/dist/src/cli/agents.d.ts +7 -0
- package/dist/src/cli/agents.d.ts.map +1 -0
- package/dist/src/cli/agents.js +55 -0
- package/dist/src/cli/agents.js.map +1 -0
- package/dist/src/cli/cli.d.ts +5 -0
- package/dist/src/cli/cli.d.ts.map +1 -0
- package/dist/src/cli/cli.js +113 -0
- package/dist/src/cli/cli.js.map +1 -0
- package/dist/src/cli/client.d.ts +12 -0
- package/dist/src/cli/client.d.ts.map +1 -0
- package/dist/src/cli/client.js +76 -0
- package/dist/src/cli/client.js.map +1 -0
- package/dist/src/cli/init.d.ts +16 -0
- package/dist/src/cli/init.d.ts.map +1 -0
- package/dist/src/cli/init.js +149 -0
- package/dist/src/cli/init.js.map +1 -0
- package/dist/src/cli/names.d.ts +14 -0
- package/dist/src/cli/names.d.ts.map +1 -0
- package/dist/src/cli/names.js +25 -0
- package/dist/src/cli/names.js.map +1 -0
- package/dist/src/cli/render.d.ts +30 -0
- package/dist/src/cli/render.d.ts.map +1 -0
- package/dist/src/cli/render.js +101 -0
- package/dist/src/cli/render.js.map +1 -0
- package/dist/src/cli/system.d.ts +35 -0
- package/dist/src/cli/system.d.ts.map +1 -0
- package/dist/src/cli/system.js +81 -0
- package/dist/src/cli/system.js.map +1 -0
- package/dist/src/client/client.d.ts +64 -0
- package/dist/src/client/client.d.ts.map +1 -0
- package/dist/src/client/client.js +170 -0
- package/dist/src/client/client.js.map +1 -0
- package/dist/src/client/index.d.ts +8 -0
- package/dist/src/client/index.d.ts.map +1 -0
- package/dist/src/client/index.js +7 -0
- package/dist/src/client/index.js.map +1 -0
- package/dist/src/config/env.d.ts +28 -0
- package/dist/src/config/env.d.ts.map +1 -0
- package/dist/src/config/env.js +35 -0
- package/dist/src/config/env.js.map +1 -0
- package/dist/src/config/logger.d.ts +32 -0
- package/dist/src/config/logger.d.ts.map +1 -0
- package/dist/src/config/logger.js +31 -0
- package/dist/src/config/logger.js.map +1 -0
- package/dist/src/di/container.d.ts +194 -0
- package/dist/src/di/container.d.ts.map +1 -0
- package/dist/src/di/container.js +225 -0
- package/dist/src/di/container.js.map +1 -0
- package/dist/src/di/index.d.ts +8 -0
- package/dist/src/di/index.d.ts.map +1 -0
- package/dist/src/di/index.js +8 -0
- package/dist/src/di/index.js.map +1 -0
- package/dist/src/errors/http-errors.d.ts +295 -0
- package/dist/src/errors/http-errors.d.ts.map +1 -0
- package/dist/src/errors/http-errors.js +311 -0
- package/dist/src/errors/http-errors.js.map +1 -0
- package/dist/src/errors/index.d.ts +8 -0
- package/dist/src/errors/index.d.ts.map +1 -0
- package/dist/src/errors/index.js +8 -0
- package/dist/src/errors/index.js.map +1 -0
- package/dist/src/http/app.d.ts +229 -0
- package/dist/src/http/app.d.ts.map +1 -0
- package/dist/src/http/app.js +45 -0
- package/dist/src/http/app.js.map +1 -0
- package/dist/src/http/context.d.ts +66 -0
- package/dist/src/http/context.d.ts.map +1 -0
- package/dist/src/http/context.js +96 -0
- package/dist/src/http/context.js.map +1 -0
- package/dist/src/http/controller.d.ts +195 -0
- package/dist/src/http/controller.d.ts.map +1 -0
- package/dist/src/http/controller.js +290 -0
- package/dist/src/http/controller.js.map +1 -0
- package/dist/src/http/error-handler.d.ts +86 -0
- package/dist/src/http/error-handler.d.ts.map +1 -0
- package/dist/src/http/error-handler.js +147 -0
- package/dist/src/http/error-handler.js.map +1 -0
- package/dist/src/http/instance.d.ts +14 -0
- package/dist/src/http/instance.d.ts.map +1 -0
- package/dist/src/http/instance.js +377 -0
- package/dist/src/http/instance.js.map +1 -0
- package/dist/src/http/middleware.d.ts +141 -0
- package/dist/src/http/middleware.d.ts.map +1 -0
- package/dist/src/http/middleware.js +160 -0
- package/dist/src/http/middleware.js.map +1 -0
- package/dist/src/http/openapi.d.ts +34 -0
- package/dist/src/http/openapi.d.ts.map +1 -0
- package/dist/src/http/openapi.js +151 -0
- package/dist/src/http/openapi.js.map +1 -0
- package/dist/src/http/path-params.d.ts +24 -0
- package/dist/src/http/path-params.d.ts.map +1 -0
- package/dist/src/http/path-params.js +2 -0
- package/dist/src/http/path-params.js.map +1 -0
- package/dist/src/http/routes.d.ts +141 -0
- package/dist/src/http/routes.d.ts.map +1 -0
- package/dist/src/http/routes.js +2 -0
- package/dist/src/http/routes.js.map +1 -0
- package/dist/src/http/server.d.ts +23 -0
- package/dist/src/http/server.d.ts.map +1 -0
- package/dist/src/http/server.js +22 -0
- package/dist/src/http/server.js.map +1 -0
- package/dist/src/http/validation.d.ts +16 -0
- package/dist/src/http/validation.d.ts.map +1 -0
- package/dist/src/http/validation.js +51 -0
- package/dist/src/http/validation.js.map +1 -0
- package/dist/src/index.d.ts +22 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +19 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/testing/index.d.ts +7 -0
- package/dist/src/testing/index.d.ts.map +1 -0
- package/dist/src/testing/index.js +7 -0
- package/dist/src/testing/index.js.map +1 -0
- package/dist/src/testing/test-client.d.ts +67 -0
- package/dist/src/testing/test-client.d.ts.map +1 -0
- package/dist/src/testing/test-client.js +78 -0
- package/dist/src/testing/test-client.js.map +1 -0
- package/package.json +89 -3
- package/starters/minimal/.env.example +3 -0
- package/starters/minimal/.oxfmtrc.json +5 -0
- package/starters/minimal/.oxlintrc.json +14 -0
- package/starters/minimal/AGENTS.md +23 -0
- package/starters/minimal/CLAUDE.md +2 -0
- package/starters/minimal/README.md +53 -0
- package/starters/minimal/_gitignore +3 -0
- package/starters/minimal/package.json +29 -0
- package/starters/minimal/src/app.ts +14 -0
- package/starters/minimal/src/env.ts +7 -0
- package/starters/minimal/src/features/index.ts +8 -0
- package/starters/minimal/src/features/notes/notes.controller.ts +38 -0
- package/starters/minimal/src/features/notes/notes.repository.ts +36 -0
- package/starters/minimal/src/features/notes/notes.schema.ts +24 -0
- package/starters/minimal/src/features/notes/notes.service.ts +33 -0
- package/starters/minimal/src/main.ts +4 -0
- package/starters/minimal/src/schemas.ts +26 -0
- package/starters/minimal/templates/feature/src/features/index.ts.controllers.insert +1 -0
- package/starters/minimal/templates/feature/src/features/index.ts.imports.insert +1 -0
- package/starters/minimal/templates/feature/src/features/{{kebab}}/{{kebab}}.controller.ts +38 -0
- package/starters/minimal/templates/feature/src/features/{{kebab}}/{{kebab}}.repository.ts +36 -0
- package/starters/minimal/templates/feature/src/features/{{kebab}}/{{kebab}}.schema.ts +24 -0
- package/starters/minimal/templates/feature/src/features/{{kebab}}/{{kebab}}.service.ts +33 -0
- package/starters/minimal/templates/feature/test/{{kebab}}.service.test.ts +20 -0
- package/starters/minimal/templates/feature/test/{{kebab}}.test.ts +35 -0
- package/starters/minimal/templates/middleware/src/middlewares/{{kebab}}.ts +12 -0
- package/starters/minimal/test/notes.service.test.ts +20 -0
- package/starters/minimal/test/notes.test.ts +35 -0
- package/starters/minimal/tsconfig.json +17 -0
- package/starters/minimal/vitest.config.ts +9 -0
- package/starters/sqlite-auth/.dockerignore +8 -0
- package/starters/sqlite-auth/.env.example +9 -0
- package/starters/sqlite-auth/.oxfmtrc.json +5 -0
- package/starters/sqlite-auth/.oxlintrc.json +14 -0
- package/starters/sqlite-auth/AGENTS.md +23 -0
- package/starters/sqlite-auth/CLAUDE.md +2 -0
- package/starters/sqlite-auth/Dockerfile +23 -0
- package/starters/sqlite-auth/README.md +93 -0
- package/starters/sqlite-auth/_gitignore +5 -0
- package/starters/sqlite-auth/compose.yaml +14 -0
- package/starters/sqlite-auth/package.json +35 -0
- package/starters/sqlite-auth/public/.gitkeep +0 -0
- package/starters/sqlite-auth/src/app.ts +39 -0
- package/starters/sqlite-auth/src/auth/tokens.ts +41 -0
- package/starters/sqlite-auth/src/database/clock.ts +4 -0
- package/starters/sqlite-auth/src/database/database.ts +29 -0
- package/starters/sqlite-auth/src/database/errors.ts +11 -0
- package/starters/sqlite-auth/src/database/migrations.ts +29 -0
- package/starters/sqlite-auth/src/env.ts +17 -0
- package/starters/sqlite-auth/src/features/auth/auth.controller.ts +28 -0
- package/starters/sqlite-auth/src/features/index.ts +12 -0
- package/starters/sqlite-auth/src/features/notes/notes.controller.ts +39 -0
- package/starters/sqlite-auth/src/features/notes/notes.repository.ts +47 -0
- package/starters/sqlite-auth/src/features/notes/notes.schema.ts +24 -0
- package/starters/sqlite-auth/src/features/notes/notes.service.ts +33 -0
- package/starters/sqlite-auth/src/features/users/admin-seed.ts +21 -0
- package/starters/sqlite-auth/src/features/users/users.controller.ts +42 -0
- package/starters/sqlite-auth/src/features/users/users.errors.ts +13 -0
- package/starters/sqlite-auth/src/features/users/users.repository.ts +77 -0
- package/starters/sqlite-auth/src/features/users/users.schema.ts +35 -0
- package/starters/sqlite-auth/src/features/users/users.service.ts +80 -0
- package/starters/sqlite-auth/src/main.ts +4 -0
- package/starters/sqlite-auth/src/middlewares/authenticate.ts +29 -0
- package/starters/sqlite-auth/src/schemas.ts +26 -0
- package/starters/sqlite-auth/templates/feature/src/database/migrations.ts.migrations.insert +5 -0
- package/starters/sqlite-auth/templates/feature/src/features/index.ts.controllers.insert +1 -0
- package/starters/sqlite-auth/templates/feature/src/features/index.ts.imports.insert +1 -0
- package/starters/sqlite-auth/templates/feature/src/features/{{kebab}}/{{kebab}}.controller.ts +39 -0
- package/starters/sqlite-auth/templates/feature/src/features/{{kebab}}/{{kebab}}.repository.ts +47 -0
- package/starters/sqlite-auth/templates/feature/src/features/{{kebab}}/{{kebab}}.schema.ts +24 -0
- package/starters/sqlite-auth/templates/feature/src/features/{{kebab}}/{{kebab}}.service.ts +33 -0
- package/starters/sqlite-auth/templates/feature/test/{{kebab}}.service.test.ts +20 -0
- package/starters/sqlite-auth/templates/feature/test/{{kebab}}.test.ts +41 -0
- package/starters/sqlite-auth/templates/middleware/src/middlewares/{{kebab}}.ts +12 -0
- package/starters/sqlite-auth/test/auth.test.ts +18 -0
- package/starters/sqlite-auth/test/helpers.ts +13 -0
- package/starters/sqlite-auth/test/notes.service.test.ts +20 -0
- package/starters/sqlite-auth/test/notes.test.ts +41 -0
- package/starters/sqlite-auth/test/users.service.test.ts +25 -0
- package/starters/sqlite-auth/test/users.test.ts +164 -0
- package/starters/sqlite-auth/tsconfig.json +17 -0
- package/starters/sqlite-auth/vitest.config.ts +15 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ValidationError } from "../errors/http-errors.js";
|
|
3
|
+
/**
|
|
4
|
+
* Zod parsing shared by routes and middlewares. Every parse is synchronous, the fast path, unless a schema has
|
|
5
|
+
* async refinements: then it switches to `safeParseAsync` and the value comes back wrapped in {@link Pending}.
|
|
6
|
+
*/
|
|
7
|
+
/** An input whose schema needs async validation; never a value a schema can produce. */
|
|
8
|
+
export class Pending {
|
|
9
|
+
promise;
|
|
10
|
+
constructor(promise) {
|
|
11
|
+
this.promise = promise;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export const settle = (value) => (value instanceof Pending ? value.promise : value);
|
|
15
|
+
export function parseInput(schema, value, location) {
|
|
16
|
+
if (!schema)
|
|
17
|
+
return value;
|
|
18
|
+
const input = value ?? {};
|
|
19
|
+
const result = safeParse(schema, input);
|
|
20
|
+
if (result instanceof Promise)
|
|
21
|
+
return new Pending(result.then((settled) => unwrapInput(settled, location)));
|
|
22
|
+
return unwrapInput(result, location);
|
|
23
|
+
}
|
|
24
|
+
function unwrapInput(result, location) {
|
|
25
|
+
if (result.success)
|
|
26
|
+
return result.data;
|
|
27
|
+
throw ValidationError.fromZod(result.error, location);
|
|
28
|
+
}
|
|
29
|
+
/** A result that breaks its own response schema is a server bug: a 500, never a 400. */
|
|
30
|
+
export function serializeOutput(schema, value) {
|
|
31
|
+
if (!schema)
|
|
32
|
+
return value;
|
|
33
|
+
const result = safeParse(schema, value);
|
|
34
|
+
return result instanceof Promise ? result.then(unwrapOutput) : unwrapOutput(result);
|
|
35
|
+
}
|
|
36
|
+
function unwrapOutput(result) {
|
|
37
|
+
if (result.success)
|
|
38
|
+
return result.data;
|
|
39
|
+
throw new Error(`Response does not match its schema:\n${z.prettifyError(result.error)}`);
|
|
40
|
+
}
|
|
41
|
+
export function safeParse(schema, value) {
|
|
42
|
+
try {
|
|
43
|
+
return schema.safeParse(value);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error instanceof z.core.$ZodAsyncError)
|
|
47
|
+
return schema.safeParseAsync(value);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../../src/http/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAE3D;;;GAGG;AAEH,wFAAwF;AACxF,MAAM,OAAO,OAAO;IACT,OAAO,CAAmB;IACnC,YAAY,OAAyB;QACnC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF;AAED,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,KAAc,EAAW,EAAE,CAAC,CAAC,KAAK,YAAY,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAEtG,MAAM,UAAU,UAAU,CAAC,MAA6B,EAAE,KAAc,EAAE,QAAgB;IACxF,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1B,MAAM,KAAK,GAAG,KAAK,IAAI,EAAE,CAAC;IAC1B,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACxC,IAAI,MAAM,YAAY,OAAO;QAAE,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC5G,OAAO,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AACvC,CAAC;AAED,SAAS,WAAW,CAAC,MAAqC,EAAE,QAAgB;IAC1E,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC;IACvC,MAAM,eAAe,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;AACxD,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,eAAe,CAAC,MAA6B,EAAE,KAAc;IAC3E,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1B,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACxC,OAAO,MAAM,YAAY,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;AACtF,CAAC;AAED,SAAS,YAAY,CAAC,MAAqC;IACzD,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC;IACvC,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;AAC3F,CAAC;AAED,MAAM,UAAU,SAAS,CACvB,MAAiB,EACjB,KAAc;IAEd,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,cAAc;YAAE,OAAO,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;QAChF,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* brisk: a typed layer over Express 5 and Zod. This entry exports the whole server API. Narrower entries:
|
|
3
|
+
* `@raiyyan/brisk/di`, `@raiyyan/brisk/errors`, `@raiyyan/brisk/testing`, and the browser-safe
|
|
4
|
+
* `@raiyyan/brisk/client`.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
export { defineApp, type App, type AppHooks, type AppInstance, type AppOptions, type ControllerMap, type CreateOptions, type ErrorOptions, type HttpOptions, type ListenOptions, type StartOptions, } from "./http/app.ts";
|
|
9
|
+
export type { OpenApiOptions } from "./http/openapi.ts";
|
|
10
|
+
export { HttpServer, type NodeServer } from "./http/server.ts";
|
|
11
|
+
export { Container, defineToken, inject, injectLazy, provide, type InjectionToken, type Class, type OnDrain, type OnInit, type OnReady, type OnShutdown, type Provider, type ProviderEntry, type Token, } from "./di/index.ts";
|
|
12
|
+
export { getContext, type RequestContext } from "./http/context.ts";
|
|
13
|
+
export { defineController, type Controller, type ControllerOptions, type GroupOptions, type RouteBuilder, type RouteContext, type RouteOptions, } from "./http/controller.ts";
|
|
14
|
+
export type { PathParams } from "./http/path-params.ts";
|
|
15
|
+
export type { ApiOf, Client, HttpMethod, Jsonify, Manifest, Route, Routes } from "./http/routes.ts";
|
|
16
|
+
export { defineEnv, type DefineEnvOptions } from "./config/env.ts";
|
|
17
|
+
export { defaultErrorFormatter, ErrorTranslator, type ErrorFormatter, type ErrorMapper, } from "./http/error-handler.ts";
|
|
18
|
+
export * from "./errors/index.ts";
|
|
19
|
+
export { createLogger, Logger, type LoggerOptions } from "./config/logger.ts";
|
|
20
|
+
export { defineMiddleware, fromExpress, type Middleware, type MiddlewareContext, type MiddlewareInput, type MiddlewareOptions, type SecurityScheme, type SecuritySchemes, } from "./http/middleware.ts";
|
|
21
|
+
export { z } from "zod";
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,SAAS,EACT,KAAK,GAAG,EACR,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EACL,SAAS,EACT,WAAW,EACX,MAAM,EACN,UAAU,EACV,OAAO,EACP,KAAK,cAAc,EACnB,KAAK,KAAK,EACV,KAAK,OAAO,EACZ,KAAK,MAAM,EACX,KAAK,OAAO,EACZ,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,KAAK,GACX,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,EACL,gBAAgB,EAChB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,YAAY,GAClB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AACpG,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,GACjB,MAAM,yBAAyB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAC9E,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,eAAe,GACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* brisk: a typed layer over Express 5 and Zod. This entry exports the whole server API. Narrower entries:
|
|
3
|
+
* `@raiyyan/brisk/di`, `@raiyyan/brisk/errors`, `@raiyyan/brisk/testing`, and the browser-safe
|
|
4
|
+
* `@raiyyan/brisk/client`.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
export { defineApp, } from "./http/app.js";
|
|
9
|
+
export { HttpServer } from "./http/server.js";
|
|
10
|
+
export { Container, defineToken, inject, injectLazy, provide, } from "./di/index.js";
|
|
11
|
+
export { getContext } from "./http/context.js";
|
|
12
|
+
export { defineController, } from "./http/controller.js";
|
|
13
|
+
export { defineEnv } from "./config/env.js";
|
|
14
|
+
export { defaultErrorFormatter, ErrorTranslator, } from "./http/error-handler.js";
|
|
15
|
+
export * from "./errors/index.js";
|
|
16
|
+
export { createLogger, Logger } from "./config/logger.js";
|
|
17
|
+
export { defineMiddleware, fromExpress, } from "./http/middleware.js";
|
|
18
|
+
export { z } from "zod";
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,SAAS,GAWV,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,UAAU,EAAmB,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EACL,SAAS,EACT,WAAW,EACX,MAAM,EACN,UAAU,EACV,OAAO,GAUR,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,UAAU,EAAuB,MAAM,mBAAmB,CAAC;AACpE,OAAO,EACL,gBAAgB,GAOjB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EAAE,SAAS,EAAyB,MAAM,iBAAiB,CAAC;AACnE,OAAO,EACL,qBAAqB,EACrB,eAAe,GAGhB,MAAM,yBAAyB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,EAAsB,MAAM,oBAAoB,CAAC;AAC9E,OAAO,EACL,gBAAgB,EAChB,WAAW,GAOZ,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test helpers: boot an app on a random port with `createTestClient`, read error bodies with `readError`.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
export { createTestClient, readError, type TestClient, type TestRequest, type TestResponse, } from "./test-client.ts";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,gBAAgB,EAChB,SAAS,EACT,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,YAAY,GAClB,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,gBAAgB,EAChB,SAAS,GAIV,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ErrorResponse } from "../errors/http-errors.ts";
|
|
2
|
+
import type { App, AppInstance, CreateOptions } from "../http/app.ts";
|
|
3
|
+
import type { ApiOf, Client } from "../http/routes.ts";
|
|
4
|
+
/** A response from a {@link TestClient} raw request. */
|
|
5
|
+
export interface TestResponse {
|
|
6
|
+
status: number;
|
|
7
|
+
headers: Headers;
|
|
8
|
+
/** Parsed JSON for JSON responses (including `+json` types), text otherwise, `undefined` when empty. */
|
|
9
|
+
body: unknown;
|
|
10
|
+
}
|
|
11
|
+
/** Body and headers for a {@link TestClient} raw request. */
|
|
12
|
+
export interface TestRequest {
|
|
13
|
+
/**
|
|
14
|
+
* Sent as JSON, except a string or `Uint8Array`, which is sent as is (for raw webhook payloads or forms).
|
|
15
|
+
* `content-type` defaults to `application/json` whenever there is a body; set it in `headers` to override.
|
|
16
|
+
*/
|
|
17
|
+
body?: unknown;
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
type Send = (path: string, request?: TestRequest) => Promise<TestResponse>;
|
|
21
|
+
/**
|
|
22
|
+
* A client for an instance started by {@link createTestClient}. Its type argument is the app:
|
|
23
|
+
* `TestClient<typeof app>`: the typed client as `call`, and raw requests
|
|
24
|
+
* by path for checking status codes, headers and malformed input.
|
|
25
|
+
*/
|
|
26
|
+
export interface TestClient<A = unknown> {
|
|
27
|
+
readonly baseUrl: string;
|
|
28
|
+
/** The running instance, for its `container` or `server`. */
|
|
29
|
+
readonly instance: AppInstance;
|
|
30
|
+
/** The typed client: `await api.call.users.get({ params: { id } })`. Rejects with `ApiError` on non-2xx. */
|
|
31
|
+
readonly call: Client<ApiOf<A>>;
|
|
32
|
+
get: Send;
|
|
33
|
+
post: Send;
|
|
34
|
+
put: Send;
|
|
35
|
+
patch: Send;
|
|
36
|
+
delete: Send;
|
|
37
|
+
/** Shuts the instance down gracefully. */
|
|
38
|
+
close(): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Starts a fresh instance of the app on a random local port, with its full lifecycle, and returns a client for
|
|
42
|
+
* it. `providers` are added after the app's own: a value for the same token wins, so fakes replace real
|
|
43
|
+
* services, and a class is created alongside the app's. `close()` shuts the instance down gracefully.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* const api = await createTestClient(app, { providers: [provide(Clock, () => fixedDate)] });
|
|
48
|
+
*
|
|
49
|
+
* const user = await api.call.users.create({ body: { email: "a@b.co" }, headers: admin }); // typed
|
|
50
|
+
* const res = await api.post("/api/users", { body: { email: "nope" }, headers: admin }); // raw
|
|
51
|
+
* expect(res.status).toBe(400);
|
|
52
|
+
*
|
|
53
|
+
* await api.close();
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export declare function createTestClient<A extends App>(app: A, options?: CreateOptions): Promise<TestClient<A>>;
|
|
57
|
+
/**
|
|
58
|
+
* Reads the error out of a response, failing the test when the body is not an error response.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* expect(readError(await api.get("/api/users/nope"))).toMatchObject({ code: "NOT_FOUND" });
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export declare function readError(response: TestResponse): ErrorResponse["error"];
|
|
66
|
+
export {};
|
|
67
|
+
//# sourceMappingURL=test-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-client.d.ts","sourceRoot":"","sources":["../../../src/testing/test-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D,OAAO,KAAK,EAAE,GAAG,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEvD,wDAAwD;AACxD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,wGAAwG;IACxG,IAAI,EAAE,OAAO,CAAC;CACf;AAED,6DAA6D;AAC7D,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAKD,KAAK,IAAI,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;AAE3E;;;;GAIG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC,GAAG,OAAO;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAC/B,4GAA4G;IAC5G,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAChC,GAAG,EAAE,IAAI,CAAC;IACV,IAAI,EAAE,IAAI,CAAC;IACX,GAAG,EAAE,IAAI,CAAC;IACV,KAAK,EAAE,IAAI,CAAC;IACZ,MAAM,EAAE,IAAI,CAAC;IACb,0CAA0C;IAC1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,SAAS,GAAG,EAClD,GAAG,EAAE,CAAC,EACN,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CA0CxB;AAWD;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,YAAY,GAAG,aAAa,CAAC,OAAO,CAAC,CAExE"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createClient } from "../client/client.js";
|
|
3
|
+
/** `application/json`, and structured suffixes such as `application/problem+json`. */
|
|
4
|
+
const JSON_TYPE = /^application\/(?:[\w.-]+\+)?json\b/i;
|
|
5
|
+
/**
|
|
6
|
+
* Starts a fresh instance of the app on a random local port, with its full lifecycle, and returns a client for
|
|
7
|
+
* it. `providers` are added after the app's own: a value for the same token wins, so fakes replace real
|
|
8
|
+
* services, and a class is created alongside the app's. `close()` shuts the instance down gracefully.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const api = await createTestClient(app, { providers: [provide(Clock, () => fixedDate)] });
|
|
13
|
+
*
|
|
14
|
+
* const user = await api.call.users.create({ body: { email: "a@b.co" }, headers: admin }); // typed
|
|
15
|
+
* const res = await api.post("/api/users", { body: { email: "nope" }, headers: admin }); // raw
|
|
16
|
+
* expect(res.status).toBe(400);
|
|
17
|
+
*
|
|
18
|
+
* await api.close();
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export async function createTestClient(app, options = {}) {
|
|
22
|
+
const instance = await app.listen(0, { ...options, host: "127.0.0.1", manageProcess: false });
|
|
23
|
+
const address = instance.server?.address();
|
|
24
|
+
if (!address || typeof address === "string")
|
|
25
|
+
throw new Error("Expected a TCP server address");
|
|
26
|
+
const baseUrl = `http://127.0.0.1:${address.port}`;
|
|
27
|
+
const send = (method) => async (path, request = {}) => {
|
|
28
|
+
const payload = request.body;
|
|
29
|
+
const res = await fetch(baseUrl + path, {
|
|
30
|
+
method,
|
|
31
|
+
headers: payload === undefined
|
|
32
|
+
? { ...request.headers }
|
|
33
|
+
: { "content-type": "application/json", ...request.headers },
|
|
34
|
+
body: payload === undefined
|
|
35
|
+
? null
|
|
36
|
+
: typeof payload === "string"
|
|
37
|
+
? payload
|
|
38
|
+
: payload instanceof Uint8Array
|
|
39
|
+
? new Uint8Array(payload)
|
|
40
|
+
: JSON.stringify(payload),
|
|
41
|
+
});
|
|
42
|
+
const text = await res.text();
|
|
43
|
+
const isJson = JSON_TYPE.test(res.headers.get("content-type") ?? "");
|
|
44
|
+
const body = text === "" ? undefined : isJson ? JSON.parse(text) : text;
|
|
45
|
+
return { status: res.status, headers: res.headers, body };
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
baseUrl,
|
|
49
|
+
instance,
|
|
50
|
+
call: createClient(instance.manifest, { baseUrl }),
|
|
51
|
+
get: send("GET"),
|
|
52
|
+
post: send("POST"),
|
|
53
|
+
put: send("PUT"),
|
|
54
|
+
patch: send("PATCH"),
|
|
55
|
+
delete: send("DELETE"),
|
|
56
|
+
close: () => instance.shutdown(),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const ErrorBody = z.object({
|
|
60
|
+
error: z.object({
|
|
61
|
+
code: z.string(),
|
|
62
|
+
message: z.string(),
|
|
63
|
+
details: z.unknown().optional(),
|
|
64
|
+
requestId: z.string(),
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Reads the error out of a response, failing the test when the body is not an error response.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* expect(readError(await api.get("/api/users/nope"))).toMatchObject({ code: "NOT_FOUND" });
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export function readError(response) {
|
|
76
|
+
return ErrorBody.parse(response.body).error;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=test-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-client.js","sourceRoot":"","sources":["../../../src/testing/test-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAsBnD,sFAAsF;AACtF,MAAM,SAAS,GAAG,qCAAqC,CAAC;AAwBxD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,GAAM,EACN,OAAO,GAAkB,EAAE;IAE3B,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,CAAC;IAC9F,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;IAC3C,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IAC9F,MAAM,OAAO,GAAG,oBAAoB,OAAO,CAAC,IAAI,EAAE,CAAC;IAEnD,MAAM,IAAI,GACR,CAAC,MAAc,EAAQ,EAAE,CACzB,KAAK,EAAE,IAAI,EAAE,OAAO,GAAG,EAAE,EAAE,EAAE;QAC3B,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;QAC7B,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,OAAO,GAAG,IAAI,EAAE;YACtC,MAAM;YACN,OAAO,EACL,OAAO,KAAK,SAAS;gBACnB,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE;gBACxB,CAAC,CAAC,EAAE,cAAc,EAAE,kBAAkB,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE;YAChE,IAAI,EACF,OAAO,KAAK,SAAS;gBACnB,CAAC,CAAC,IAAI;gBACN,CAAC,CAAC,OAAO,OAAO,KAAK,QAAQ;oBAC3B,CAAC,CAAC,OAAO;oBACT,CAAC,CAAC,OAAO,YAAY,UAAU;wBAC7B,CAAC,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC;wBACzB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;SAClC,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC;QACrE,MAAM,IAAI,GAAY,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACjF,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;IAC5D,CAAC,CAAC;IAEJ,OAAO;QACL,OAAO;QACP,QAAQ;QACR,IAAI,EAAE,YAAY,CAAI,QAAQ,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,CAAC;QACrD,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC;QAChB,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC;QAClB,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC;QAChB,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC;QACpB,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC;QACtB,KAAK,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE;KACjC,CAAC;AACJ,CAAC;AAED,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC;IACzB,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC;QACd,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;QAChB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;QACnB,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;QAC/B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;KACtB,CAAC;CACH,CAAC,CAAC;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,QAAsB;IAC9C,OAAO,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC;AAC9C,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,92 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raiyyan/brisk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A small, typed, opinionated layer over Express 5 and Zod: validation, DI, lifecycle and errors without the boilerplate.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"api",
|
|
7
|
+
"dependency-injection",
|
|
8
|
+
"express",
|
|
9
|
+
"framework",
|
|
10
|
+
"typescript",
|
|
11
|
+
"validation",
|
|
12
|
+
"zod"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/MohammadRaiyyan/brisk#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/MohammadRaiyyan/brisk/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Mohammad Raiyyan <inforaiyyan@gmail.com>",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/MohammadRaiyyan/brisk.git",
|
|
23
|
+
"directory": "packages/brisk"
|
|
24
|
+
},
|
|
25
|
+
"bin": {
|
|
26
|
+
"brisk": "./dist/bin/brisk.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"AGENTS.md",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"starters"
|
|
34
|
+
],
|
|
35
|
+
"type": "module",
|
|
36
|
+
"sideEffects": false,
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"types": "./dist/src/index.d.ts",
|
|
40
|
+
"default": "./dist/src/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./di": {
|
|
43
|
+
"types": "./dist/src/di/index.d.ts",
|
|
44
|
+
"default": "./dist/src/di/index.js"
|
|
45
|
+
},
|
|
46
|
+
"./errors": {
|
|
47
|
+
"types": "./dist/src/errors/index.d.ts",
|
|
48
|
+
"default": "./dist/src/errors/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./testing": {
|
|
51
|
+
"types": "./dist/src/testing/index.d.ts",
|
|
52
|
+
"default": "./dist/src/testing/index.js"
|
|
53
|
+
},
|
|
54
|
+
"./client": {
|
|
55
|
+
"types": "./dist/src/client/index.d.ts",
|
|
56
|
+
"default": "./dist/src/client/index.js"
|
|
57
|
+
},
|
|
58
|
+
"./package.json": "./package.json"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@types/express": "^5.0.6",
|
|
65
|
+
"express": "^5.3.0",
|
|
66
|
+
"pino": "^10.4.0",
|
|
67
|
+
"zod": "^4.6.7"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@trpc/server": "^11.28.1",
|
|
71
|
+
"@types/ws": "^8.18.2",
|
|
72
|
+
"graphql": "^17.1.0",
|
|
73
|
+
"graphql-yoga": "^6.1.0",
|
|
74
|
+
"pino-pretty": "^13.2.0",
|
|
75
|
+
"ws": "^8.23.0"
|
|
76
|
+
},
|
|
77
|
+
"peerDependencies": {
|
|
78
|
+
"pino-pretty": "^13.2.0"
|
|
79
|
+
},
|
|
80
|
+
"peerDependenciesMeta": {
|
|
81
|
+
"pino-pretty": {
|
|
82
|
+
"optional": true
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"engines": {
|
|
86
|
+
"node": ">=24"
|
|
87
|
+
},
|
|
88
|
+
"scripts": {
|
|
89
|
+
"typecheck": "tsc --noEmit",
|
|
90
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json"
|
|
91
|
+
}
|
|
6
92
|
}
|
|
@@ -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,53 @@
|
|
|
1
|
+
# minimal
|
|
2
|
+
|
|
3
|
+
An API built with [brisk](https://github.com/MohammadRaiyyan/brisk) on Express 5 and Zod. It has no database: the example feature keeps its records in memory. Created with `npx brisk init` (the `minimal` starter).
|
|
4
|
+
|
|
5
|
+
## Run it
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cp .env.example .env
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The API listens on port 3000 under `/api`. `GET /health` is the health check and `GET /openapi.json` the OpenAPI document.
|
|
13
|
+
|
|
14
|
+
| Script | What it does |
|
|
15
|
+
| ------------------------- | ------------------------------------------ |
|
|
16
|
+
| `dev` | Runs the server and restarts it on changes |
|
|
17
|
+
| `start` | Runs the server |
|
|
18
|
+
| `test` | Runs the tests (vitest) |
|
|
19
|
+
| `typecheck` | Checks types (`tsc --noEmit`) |
|
|
20
|
+
| `lint` | oxlint |
|
|
21
|
+
| `format` / `format:check` | oxfmt |
|
|
22
|
+
| `brisk g <kind> <name>` | Generates code from `templates/<kind>/` |
|
|
23
|
+
|
|
24
|
+
Node 24 or later runs the TypeScript sources directly; there is no build step.
|
|
25
|
+
|
|
26
|
+
## Layout
|
|
27
|
+
|
|
28
|
+
| Path | What it holds |
|
|
29
|
+
| ---------------- | ------------------------------------------------------------------------------------------- |
|
|
30
|
+
| `src/env.ts` | Environment variables, validated at startup |
|
|
31
|
+
| `src/app.ts` | The app: controllers, logger, OpenAPI, error settings. `export type Api` for a typed client |
|
|
32
|
+
| `src/main.ts` | Starts the server |
|
|
33
|
+
| `src/schemas.ts` | Shared schema helpers: `isoDate()`, `PageQuery`, `page(item)` |
|
|
34
|
+
| `src/features/` | One folder per feature (schema, repository, service, controller), registered in `index.ts` |
|
|
35
|
+
| `templates/` | What `brisk g` renders: `feature` and `middleware`. Yours to edit |
|
|
36
|
+
| `test/` | HTTP tests with `createTestClient`, and service tests without HTTP |
|
|
37
|
+
|
|
38
|
+
## Add a feature
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx brisk g feature products
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This writes `src/features/products/`, its tests, and registers the controller in `src/features/index.ts` (keep the `@brisk:` marker lines there). The repository stores records in a `Map`; replace it with your database and the service does not change.
|
|
45
|
+
|
|
46
|
+
`npx brisk g middleware tenant` writes `src/middlewares/tenant.ts`. To change what new code looks like, edit the files in `templates/`; any folder you add there becomes a new `brisk g` kind.
|
|
47
|
+
|
|
48
|
+
The `notes` feature is an example generated from `templates/feature`. Delete `src/features/notes/`, `test/notes.test.ts`, `test/notes.service.test.ts` and its two lines in `src/features/index.ts` when you no longer need it.
|
|
49
|
+
|
|
50
|
+
## Learn more
|
|
51
|
+
|
|
52
|
+
- [brisk documentation](https://github.com/MohammadRaiyyan/brisk#readme)
|
|
53
|
+
- `AGENTS.md` and `CLAUDE.md` point coding agents at the brisk guide (`npx brisk agents` refreshes them).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "starter-minimal",
|
|
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
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^26.6.6",
|
|
20
|
+
"oxfmt": "^0.78.0",
|
|
21
|
+
"oxlint": "^1.93.0",
|
|
22
|
+
"oxlint-tsgolint": "^7.0.2007",
|
|
23
|
+
"typescript": "^7.0.2",
|
|
24
|
+
"vitest": "^5.1.0"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=24"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createLogger, defineApp } from "@raiyyan/brisk";
|
|
2
|
+
import { env } from "./env.ts";
|
|
3
|
+
import { controllers } from "./features/index.ts";
|
|
4
|
+
|
|
5
|
+
export const app = defineApp({
|
|
6
|
+
prefix: "/api",
|
|
7
|
+
controllers,
|
|
8
|
+
logger: createLogger({ level: env.NODE_ENV === "test" ? "silent" : env.LOG_LEVEL }),
|
|
9
|
+
openapi: { title: "API", version: "0.1.0" },
|
|
10
|
+
errors: { exposeInternal: env.NODE_ENV === "development" },
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/** The API's type, for a frontend's typed client: `createClient<Api>(manifest, { baseUrl })`. */
|
|
14
|
+
export type Api = typeof app;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { defineEnv, z } from "@raiyyan/brisk";
|
|
2
|
+
|
|
3
|
+
export const env = defineEnv({
|
|
4
|
+
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
|
5
|
+
PORT: z.coerce.number().int().positive().default(3000),
|
|
6
|
+
LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace", "silent"]).default("info"),
|
|
7
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { notesController } from "./notes/notes.controller.ts";
|
|
2
|
+
// @brisk:imports
|
|
3
|
+
|
|
4
|
+
/** Every controller by name. The names are the typed client's top level: `api.notes.get(...)`. */
|
|
5
|
+
export const controllers = {
|
|
6
|
+
notes: notesController,
|
|
7
|
+
// @brisk:controllers
|
|
8
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { defineController, inject } from "@raiyyan/brisk";
|
|
2
|
+
import { page } from "../../schemas.ts";
|
|
3
|
+
import {
|
|
4
|
+
CreateNoteBody,
|
|
5
|
+
NoteIdParams,
|
|
6
|
+
NoteResponse,
|
|
7
|
+
ListNotesQuery,
|
|
8
|
+
UpdateNoteBody,
|
|
9
|
+
} from "./notes.schema.ts";
|
|
10
|
+
import { NotesService } from "./notes.service.ts";
|
|
11
|
+
|
|
12
|
+
export const notesController = defineController({ prefix: "/notes" }, (route) => {
|
|
13
|
+
const notes = inject(NotesService);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
list: route.get("/", { query: ListNotesQuery, response: page(NoteResponse) }, ({ query }) =>
|
|
17
|
+
notes.list(query),
|
|
18
|
+
),
|
|
19
|
+
|
|
20
|
+
get: route.get("/:id", { params: NoteIdParams, response: NoteResponse }, ({ params }) =>
|
|
21
|
+
notes.get(params.id),
|
|
22
|
+
),
|
|
23
|
+
|
|
24
|
+
create: route.post("/", { body: CreateNoteBody, response: NoteResponse }, ({ body }) =>
|
|
25
|
+
notes.create(body),
|
|
26
|
+
),
|
|
27
|
+
|
|
28
|
+
update: route.patch(
|
|
29
|
+
"/:id",
|
|
30
|
+
{ params: NoteIdParams, body: UpdateNoteBody, response: NoteResponse },
|
|
31
|
+
({ params, body }) => notes.update(params.id, body),
|
|
32
|
+
),
|
|
33
|
+
|
|
34
|
+
remove: route.delete("/:id", { params: NoteIdParams }, async ({ params }) => {
|
|
35
|
+
await notes.remove(params.id);
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface Note {
|
|
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 NotesRepository {
|
|
12
|
+
readonly #rows = new Map<string, Note>();
|
|
13
|
+
|
|
14
|
+
async list({ page, limit }: { page: number; limit: number }): Promise<{ items: Note[]; 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<Note | undefined> {
|
|
20
|
+
return this.#rows.get(id);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async insert(row: Note): Promise<Note> {
|
|
24
|
+
this.#rows.set(row.id, row);
|
|
25
|
+
return row;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async update(row: Note): Promise<Note> {
|
|
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
|
+
}
|