@raiyyan/brisk 0.0.0-stage → 0.1.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 +467 -0
- package/LICENSE +21 -0
- package/README.md +1417 -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 +127 -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/generate.d.ts +9 -0
- package/dist/src/cli/generate.d.ts.map +1 -0
- package/dist/src/cli/generate.js +39 -0
- package/dist/src/cli/generate.js.map +1 -0
- package/dist/src/cli/init.d.ts +19 -0
- package/dist/src/cli/init.d.ts.map +1 -0
- package/dist/src/cli/init.js +125 -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/system.d.ts +35 -0
- package/dist/src/cli/system.d.ts.map +1 -0
- package/dist/src/cli/system.js +83 -0
- package/dist/src/cli/system.js.map +1 -0
- package/dist/src/cli/templates/auth.d.ts +4 -0
- package/dist/src/cli/templates/auth.d.ts.map +1 -0
- package/dist/src/cli/templates/auth.js +90 -0
- package/dist/src/cli/templates/auth.js.map +1 -0
- package/dist/src/cli/templates/database.d.ts +4 -0
- package/dist/src/cli/templates/database.d.ts.map +1 -0
- package/dist/src/cli/templates/database.js +107 -0
- package/dist/src/cli/templates/database.js.map +1 -0
- package/dist/src/cli/templates/docker.d.ts +4 -0
- package/dist/src/cli/templates/docker.d.ts.map +1 -0
- package/dist/src/cli/templates/docker.js +102 -0
- package/dist/src/cli/templates/docker.js.map +1 -0
- package/dist/src/cli/templates/feature.d.ts +8 -0
- package/dist/src/cli/templates/feature.d.ts.map +1 -0
- package/dist/src/cli/templates/feature.js +145 -0
- package/dist/src/cli/templates/feature.js.map +1 -0
- package/dist/src/cli/templates/project.d.ts +24 -0
- package/dist/src/cli/templates/project.d.ts.map +1 -0
- package/dist/src/cli/templates/project.js +166 -0
- package/dist/src/cli/templates/project.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 +143 -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 +186 -0
- package/dist/src/di/container.d.ts.map +1 -0
- package/dist/src/di/container.js +208 -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 +200 -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 +178 -0
- package/dist/src/http/controller.d.ts.map +1 -0
- package/dist/src/http/controller.js +317 -0
- package/dist/src/http/controller.js.map +1 -0
- package/dist/src/http/error-handler.d.ts +59 -0
- package/dist/src/http/error-handler.d.ts.map +1 -0
- package/dist/src/http/error-handler.js +124 -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 +309 -0
- package/dist/src/http/instance.js.map +1 -0
- package/dist/src/http/middleware.d.ts +54 -0
- package/dist/src/http/middleware.d.ts.map +1 -0
- package/dist/src/http/middleware.js +101 -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 +123 -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 +119 -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/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 +65 -0
- package/dist/src/testing/test-client.d.ts.map +1 -0
- package/dist/src/testing/test-client.js +77 -0
- package/dist/src/testing/test-client.js.map +1 -0
- package/package.json +88 -3
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** An error the CLI reports as a message and exit code 1, without a stack trace. */
|
|
3
|
+
export declare class CliError extends Error {
|
|
4
|
+
}
|
|
5
|
+
/** Where the CLI prints, and how it asks; tests pass their own. */
|
|
6
|
+
export interface CliIo {
|
|
7
|
+
log(message: string): void;
|
|
8
|
+
error(message: string): void;
|
|
9
|
+
/** Asks a question and resolves with the answer; absent when the CLI must not prompt. */
|
|
10
|
+
ask?: (question: string) => Promise<string>;
|
|
11
|
+
}
|
|
12
|
+
export type PackageManager = "pnpm" | "npm" | "yarn";
|
|
13
|
+
/** The package manager that ran the CLI, else the one whose lockfile is present, else npm. */
|
|
14
|
+
export declare function detectPackageManager(root: string): PackageManager;
|
|
15
|
+
/** The command that adds packages, such as `pnpm add -D vitest`. */
|
|
16
|
+
export declare function addCommand(pm: PackageManager, packages: readonly string[], dev: boolean): string[];
|
|
17
|
+
/** Runs a command in `root` with its output shown, as a person would type it. */
|
|
18
|
+
export declare function run(command: readonly string[], root: string): void;
|
|
19
|
+
/** Writes a file, creating its folder. */
|
|
20
|
+
export declare function write(path: string, content: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Runs the project's own formatter on the written files, so generated code matches the codebase, and returns
|
|
23
|
+
* the line to print. A failing formatter is reported, never fatal: the files are already written and valid.
|
|
24
|
+
*/
|
|
25
|
+
export declare function format(root: string, files: readonly string[]): string | undefined;
|
|
26
|
+
export declare const PackageJson: z.ZodObject<{
|
|
27
|
+
type: z.ZodOptional<z.ZodString>;
|
|
28
|
+
scripts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
29
|
+
dependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
30
|
+
devDependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
31
|
+
}, z.core.$loose>;
|
|
32
|
+
export type PackageJson = z.infer<typeof PackageJson>;
|
|
33
|
+
/** Reads `package.json`, or an empty one when it does not exist. */
|
|
34
|
+
export declare function readPackageJson(root: string): PackageJson;
|
|
35
|
+
//# sourceMappingURL=system.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system.d.ts","sourceRoot":"","sources":["../../../src/cli/system.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,oFAAoF;AACpF,qBAAa,QAAS,SAAQ,KAAK;CAAG;AAEtC,mEAAmE;AACnE,MAAM,WAAW,KAAK;IACpB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,yFAAyF;IACzF,GAAG,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7C;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;AAErD,8FAA8F;AAC9F,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAOjE;AAED,oEAAoE;AACpE,wBAAgB,UAAU,CAAC,EAAE,EAAE,cAAc,EAAE,QAAQ,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,EAAE,CAGlG;AAED,iFAAiF;AACjF,wBAAgB,GAAG,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAIlE;AAED,0CAA0C;AAC1C,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAI3D;AAOD;;;GAGG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAejF;AAED,eAAO,MAAM,WAAW;;;;;iBAKtB,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,oEAAoE;AACpE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAQzD"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
/** An error the CLI reports as a message and exit code 1, without a stack trace. */
|
|
6
|
+
export class CliError extends Error {
|
|
7
|
+
}
|
|
8
|
+
/** The package manager that ran the CLI, else the one whose lockfile is present, else npm. */
|
|
9
|
+
export function detectPackageManager(root) {
|
|
10
|
+
const agent = process.env["npm_config_user_agent"] ?? "";
|
|
11
|
+
if (agent.startsWith("pnpm"))
|
|
12
|
+
return "pnpm";
|
|
13
|
+
if (agent.startsWith("yarn"))
|
|
14
|
+
return "yarn";
|
|
15
|
+
if (existsSync(join(root, "pnpm-lock.yaml")))
|
|
16
|
+
return "pnpm";
|
|
17
|
+
if (existsSync(join(root, "yarn.lock")))
|
|
18
|
+
return "yarn";
|
|
19
|
+
return "npm";
|
|
20
|
+
}
|
|
21
|
+
/** The command that adds packages, such as `pnpm add -D vitest`. */
|
|
22
|
+
export function addCommand(pm, packages, dev) {
|
|
23
|
+
const verb = pm === "npm" ? "install" : "add";
|
|
24
|
+
return [pm, verb, ...(dev ? ["-D"] : []), ...packages];
|
|
25
|
+
}
|
|
26
|
+
/** Runs a command in `root` with its output shown, as a person would type it. */
|
|
27
|
+
export function run(command, root) {
|
|
28
|
+
const [bin, ...args] = command;
|
|
29
|
+
if (!bin)
|
|
30
|
+
return;
|
|
31
|
+
execFileSync(bin, args, { cwd: root, stdio: "inherit" });
|
|
32
|
+
}
|
|
33
|
+
/** Writes a file, creating its folder. */
|
|
34
|
+
export function write(path, content) {
|
|
35
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
36
|
+
writeFileSync(path, content);
|
|
37
|
+
return path;
|
|
38
|
+
}
|
|
39
|
+
const FORMATTERS = [
|
|
40
|
+
{ bin: "oxfmt", args: [] },
|
|
41
|
+
{ bin: "prettier", args: ["--write", "--log-level", "silent"] },
|
|
42
|
+
];
|
|
43
|
+
/**
|
|
44
|
+
* Runs the project's own formatter on the written files, so generated code matches the codebase, and returns
|
|
45
|
+
* the line to print. A failing formatter is reported, never fatal: the files are already written and valid.
|
|
46
|
+
*/
|
|
47
|
+
export function format(root, files) {
|
|
48
|
+
for (let dir = root;; dir = dirname(dir)) {
|
|
49
|
+
for (const { bin, args } of FORMATTERS) {
|
|
50
|
+
const path = join(dir, "node_modules/.bin", bin);
|
|
51
|
+
if (existsSync(path)) {
|
|
52
|
+
try {
|
|
53
|
+
execFileSync(path, [...args, ...files], { cwd: root, stdio: "ignore" });
|
|
54
|
+
return `formatted with ${bin}`;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return `${bin} failed on the generated files; run it yourself`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (dirname(dir) === dir)
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export const PackageJson = z.looseObject({
|
|
66
|
+
type: z.string().optional(),
|
|
67
|
+
scripts: z.record(z.string(), z.string()).optional(),
|
|
68
|
+
dependencies: z.record(z.string(), z.string()).optional(),
|
|
69
|
+
devDependencies: z.record(z.string(), z.string()).optional(),
|
|
70
|
+
});
|
|
71
|
+
/** Reads `package.json`, or an empty one when it does not exist. */
|
|
72
|
+
export function readPackageJson(root) {
|
|
73
|
+
const path = join(root, "package.json");
|
|
74
|
+
if (!existsSync(path))
|
|
75
|
+
return {};
|
|
76
|
+
try {
|
|
77
|
+
return PackageJson.parse(JSON.parse(readFileSync(path, "utf8")));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw new CliError("package.json is not valid JSON; fix it and run the command again.");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=system.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system.js","sourceRoot":"","sources":["../../../src/cli/system.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../../src/cli/templates/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEzC,2GAA2G;AAC3G,wBAAgB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAuFzC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/** Bearer-token auth with HS256 tokens from `node:crypto`: no dependency, constant-time checks, expiry. */
|
|
2
|
+
export function authFiles() {
|
|
3
|
+
return {
|
|
4
|
+
files: {
|
|
5
|
+
"src/auth/tokens.ts": `import { createHmac, timingSafeEqual } from "node:crypto";
|
|
6
|
+
import { z } from "@raiyyan/brisk";
|
|
7
|
+
import { env } from "../env.ts";
|
|
8
|
+
|
|
9
|
+
export const Role = z.enum(["user", "admin"]);
|
|
10
|
+
export type Role = z.infer<typeof Role>;
|
|
11
|
+
|
|
12
|
+
const Claims = z.object({ sub: z.string(), role: Role, exp: z.number() });
|
|
13
|
+
export type Claims = z.infer<typeof Claims>;
|
|
14
|
+
|
|
15
|
+
const HEADER = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
|
|
16
|
+
|
|
17
|
+
/** Signs and verifies HS256 JSON Web Tokens with \`AUTH_SECRET\`. */
|
|
18
|
+
export class Tokens {
|
|
19
|
+
readonly #key = Buffer.from(env.AUTH_SECRET);
|
|
20
|
+
|
|
21
|
+
sign(subject: string, role: Role, ttlSeconds = env.AUTH_TOKEN_TTL_SECONDS): string {
|
|
22
|
+
const claims: Claims = { sub: subject, role, exp: Math.floor(Date.now() / 1000) + ttlSeconds };
|
|
23
|
+
const body = \`\${HEADER}.\${Buffer.from(JSON.stringify(claims)).toString("base64url")}\`;
|
|
24
|
+
return \`\${body}.\${this.#signature(body)}\`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The token's claims, or \`undefined\` when it is malformed, forged or expired. */
|
|
28
|
+
verify(token: string): Claims | undefined {
|
|
29
|
+
const [header, payload, signature, ...rest] = token.split(".");
|
|
30
|
+
if (!header || !payload || !signature || rest.length > 0 || header !== HEADER) return undefined;
|
|
31
|
+
const expected = Buffer.from(this.#signature(\`\${header}.\${payload}\`));
|
|
32
|
+
const actual = Buffer.from(signature);
|
|
33
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) return undefined;
|
|
34
|
+
try {
|
|
35
|
+
const claims = Claims.parse(JSON.parse(Buffer.from(payload, "base64url").toString("utf8")));
|
|
36
|
+
return claims.exp > Date.now() / 1000 ? claims : undefined;
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#signature(body: string): string {
|
|
43
|
+
return createHmac("sha256", this.#key).update(body).digest("base64url");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
`,
|
|
47
|
+
"src/middlewares/authenticate.ts": `import { defineMiddleware, ForbiddenError, inject, UnauthorizedError } from "@raiyyan/brisk";
|
|
48
|
+
import { Tokens, type Role } from "../auth/tokens.ts";
|
|
49
|
+
|
|
50
|
+
/** Requires \`Authorization: Bearer <token>\` and adds \`user\` to ctx; request logs gain the user id. */
|
|
51
|
+
export const authenticate = defineMiddleware(({ req, log }) => {
|
|
52
|
+
const header = req.get("authorization");
|
|
53
|
+
const token = header?.startsWith("Bearer ") ? header.slice(7) : undefined;
|
|
54
|
+
const claims = token ? inject(Tokens).verify(token) : undefined;
|
|
55
|
+
if (!claims) throw new UnauthorizedError("Valid bearer token required", { challenge: "Bearer" });
|
|
56
|
+
const user = { id: claims.sub, role: claims.role };
|
|
57
|
+
return { user, log: log.child({ userId: user.id }) };
|
|
58
|
+
}, "authenticate");
|
|
59
|
+
|
|
60
|
+
/** Requires an authenticated user with \`role\`; use it after or instead of \`authenticate\`. */
|
|
61
|
+
export const requireRole = (role: Role) =>
|
|
62
|
+
defineMiddleware(async (ctx) => {
|
|
63
|
+
const { user } = await authenticate.run(ctx);
|
|
64
|
+
if (user.role !== role) throw new ForbiddenError(\`Requires the \${role} role\`);
|
|
65
|
+
return { user };
|
|
66
|
+
}, \`requireRole(\${role})\`);
|
|
67
|
+
`,
|
|
68
|
+
"test/auth.test.ts": `import { describe, expect, it } from "vitest";
|
|
69
|
+
import { Container } from "@raiyyan/brisk";
|
|
70
|
+
import { Tokens } from "../src/auth/tokens.ts";
|
|
71
|
+
|
|
72
|
+
const tokens = new Container().get(Tokens);
|
|
73
|
+
|
|
74
|
+
describe("tokens", () => {
|
|
75
|
+
it("round-trips claims", () => {
|
|
76
|
+
expect(tokens.verify(tokens.sign("u1", "admin"))).toMatchObject({ sub: "u1", role: "admin" });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("rejects forged, malformed and expired tokens", () => {
|
|
80
|
+
const [header, payload] = tokens.sign("u1", "user").split(".");
|
|
81
|
+
expect(tokens.verify(\`\${header}.\${payload}.forged\`)).toBeUndefined();
|
|
82
|
+
expect(tokens.verify("not-a-token")).toBeUndefined();
|
|
83
|
+
expect(tokens.verify(tokens.sign("u1", "user", -1))).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
`,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../../src/cli/templates/auth.ts"],"names":[],"mappings":"AAEA,2GAA2G;AAC3G,MAAM,UAAU,SAAS;IACvB,OAAO;QACL,KAAK,EAAE;YACL,oBAAoB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyC3B;YACK,iCAAiC,EAAE;;;;;;;;;;;;;;;;;;;;CAoBxC;YACK,mBAAmB,EAAE;;;;;;;;;;;;;;;;;;CAkB1B;SACI;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../../../src/cli/templates/database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEzC,oGAAoG;AACpG,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAEtE"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** A database provider with its lifecycle, a unique-violation mapper, and what the engine needs. */
|
|
2
|
+
export function databaseFiles(db) {
|
|
3
|
+
return db === "sqlite" ? sqlite() : postgres();
|
|
4
|
+
}
|
|
5
|
+
const errorsTemplate = (check) => `import { ConflictError, type ErrorMapper } from "@raiyyan/brisk";
|
|
6
|
+
|
|
7
|
+
/** A unique constraint failed, here or in an error the driver wrapped: answer 409 instead of 500. */
|
|
8
|
+
export const uniqueViolation: ErrorMapper = (error) => {
|
|
9
|
+
for (let current: unknown = error; current instanceof Error; current = current.cause) {
|
|
10
|
+
if (${check}) {
|
|
11
|
+
return new ConflictError("Already exists", { code: "ALREADY_EXISTS", cause: error });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
};
|
|
16
|
+
`;
|
|
17
|
+
function sqlite() {
|
|
18
|
+
return {
|
|
19
|
+
files: {
|
|
20
|
+
"src/database/database.ts": `import { DatabaseSync } from "node:sqlite";
|
|
21
|
+
import { inject, Logger, type OnInit, type OnShutdown } from "@raiyyan/brisk";
|
|
22
|
+
import { env } from "../env.ts";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The app's SQLite connection (Node's built-in \`node:sqlite\`), opened at boot and closed on shutdown.
|
|
26
|
+
* Repositories inject it: \`readonly #db = inject(Database)\`, then \`this.#db.sql.prepare(...)\`.
|
|
27
|
+
*/
|
|
28
|
+
export class Database implements OnInit, OnShutdown {
|
|
29
|
+
readonly #log = inject(Logger);
|
|
30
|
+
#connection: DatabaseSync | undefined;
|
|
31
|
+
|
|
32
|
+
get sql(): DatabaseSync {
|
|
33
|
+
if (!this.#connection) throw new Error("Database used before the app booted");
|
|
34
|
+
return this.#connection;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
onInit(): void {
|
|
38
|
+
this.#connection = new DatabaseSync(env.DATABASE_PATH);
|
|
39
|
+
this.#connection.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;");
|
|
40
|
+
this.#log.info({ path: env.DATABASE_PATH }, "database opened");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
onShutdown(): void {
|
|
44
|
+
this.#connection?.close();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
`,
|
|
48
|
+
"src/database/errors.ts": errorsTemplate(`"errcode" in current && (current.errcode === 2067 || current.errcode === 1555)`),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function postgres() {
|
|
53
|
+
return {
|
|
54
|
+
files: {
|
|
55
|
+
"src/database/database.ts": `import { inject, Logger, type OnInit, type OnShutdown } from "@raiyyan/brisk";
|
|
56
|
+
import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
57
|
+
import { Pool } from "pg";
|
|
58
|
+
import { env } from "../env.ts";
|
|
59
|
+
import * as schema from "./schema.ts";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The app's PostgreSQL pool with Drizzle on top. Connections open lazily; \`onInit\` checks the database is
|
|
63
|
+
* reachable before the server listens, and \`onShutdown\` closes the pool after the last request.
|
|
64
|
+
* Repositories inject it: \`readonly #db = inject(Database).db\`.
|
|
65
|
+
*/
|
|
66
|
+
export class Database implements OnInit, OnShutdown {
|
|
67
|
+
readonly #log = inject(Logger);
|
|
68
|
+
readonly pool = new Pool({ connectionString: env.DATABASE_URL, max: env.DATABASE_POOL_SIZE });
|
|
69
|
+
readonly db: NodePgDatabase<typeof schema> = drizzle(this.pool, { schema });
|
|
70
|
+
|
|
71
|
+
async onInit(): Promise<void> {
|
|
72
|
+
await this.pool.query("select 1");
|
|
73
|
+
this.#log.info("database connected");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async onShutdown(): Promise<void> {
|
|
77
|
+
await this.pool.end();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
`,
|
|
81
|
+
"src/database/schema.ts": `// Drizzle tables. Define them here, then \`npm run db:generate\` and \`npm run db:migrate\`.
|
|
82
|
+
// import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
|
83
|
+
//
|
|
84
|
+
// export const users = pgTable("users", {
|
|
85
|
+
// id: uuid().primaryKey().defaultRandom(),
|
|
86
|
+
// email: text().notNull().unique(),
|
|
87
|
+
// createdAt: timestamp().notNull().defaultNow(),
|
|
88
|
+
// });
|
|
89
|
+
export {};
|
|
90
|
+
`,
|
|
91
|
+
"src/database/errors.ts": errorsTemplate(`"code" in current && current.code === "23505"`),
|
|
92
|
+
"drizzle.config.ts": `import { defineConfig } from "drizzle-kit";
|
|
93
|
+
|
|
94
|
+
export default defineConfig({
|
|
95
|
+
dialect: "postgresql",
|
|
96
|
+
schema: "./src/database/schema.ts",
|
|
97
|
+
out: "./drizzle",
|
|
98
|
+
dbCredentials: { url: process.env["DATABASE_URL"] ?? "" },
|
|
99
|
+
});
|
|
100
|
+
`,
|
|
101
|
+
},
|
|
102
|
+
dependencies: ["pg", "drizzle-orm"],
|
|
103
|
+
devDependencies: ["drizzle-kit", "@types/pg"],
|
|
104
|
+
scripts: { "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate" },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=database.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../../../src/cli/templates/database.ts"],"names":[],"mappings":"AAEA,oGAAoG;AACpG,MAAM,UAAU,aAAa,CAAC,EAAyB;IACrD,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;AACjD,CAAC;AAED,MAAM,cAAc,GAAG,CACrB,KAAa,EACL,EAAE,CAAC;;;;;UAKH,KAAK;;;;;;CAMd,CAAC;AAEF,SAAS,MAAM;IACb,OAAO;QACL,KAAK,EAAE;YACL,0BAA0B,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BjC;YACK,wBAAwB,EAAE,cAAc,CACtC,gFAAgF,CACjF;SACF;KACF,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ;IACf,OAAO;QACL,KAAK,EAAE;YACL,0BAA0B,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;CAyBjC;YACK,wBAAwB,EAAE;;;;;;;;;CAS/B;YACK,wBAAwB,EAAE,cAAc,CAAC,+CAA+C,CAAC;YACzF,mBAAmB,EAAE;;;;;;;;CAQ1B;SACI;QACD,YAAY,EAAE,CAAC,IAAI,EAAE,aAAa,CAAC;QACnC,eAAe,EAAE,CAAC,aAAa,EAAE,WAAW,CAAC;QAC7C,OAAO,EAAE,EAAE,aAAa,EAAE,sBAAsB,EAAE,YAAY,EAAE,qBAAqB,EAAE;KACxF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docker.d.ts","sourceRoot":"","sources":["../../../../src/cli/templates/docker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAgBtD,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAwB9E"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const INSTALL = {
|
|
2
|
+
pnpm: {
|
|
3
|
+
lock: "pnpm-lock.yaml",
|
|
4
|
+
prod: "pnpm install --prod --frozen-lockfile",
|
|
5
|
+
ci: "pnpm install --frozen-lockfile",
|
|
6
|
+
},
|
|
7
|
+
npm: { lock: "package-lock.json", prod: "npm ci --omit=dev", ci: "npm ci" },
|
|
8
|
+
yarn: {
|
|
9
|
+
lock: "yarn.lock",
|
|
10
|
+
prod: "yarn install --production --frozen-lockfile",
|
|
11
|
+
ci: "yarn install --frozen-lockfile",
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
/** Dockerfile and compose for `docker`, a GitHub Actions workflow for `ci`. */
|
|
15
|
+
export function dockerFiles({ pm, db, docker, ci }) {
|
|
16
|
+
const install = INSTALL[pm];
|
|
17
|
+
const files = {};
|
|
18
|
+
if (docker) {
|
|
19
|
+
files["Dockerfile"] = `# Node runs the TypeScript sources directly: no build step.
|
|
20
|
+
FROM node:26-slim
|
|
21
|
+
WORKDIR /app
|
|
22
|
+
ENV NODE_ENV=production
|
|
23
|
+
${pm === "npm" ? "" : "RUN corepack enable\n"}COPY package.json ${install.lock} ./
|
|
24
|
+
RUN ${install.prod}
|
|
25
|
+
COPY tsconfig.json ./
|
|
26
|
+
COPY src ./src
|
|
27
|
+
${db === "sqlite" ? "RUN mkdir -p /app/data && chown node:node /app/data\n" : ""}USER node
|
|
28
|
+
EXPOSE 3000
|
|
29
|
+
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \\
|
|
30
|
+
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))"
|
|
31
|
+
# SIGTERM from \`docker stop\` starts brisk's graceful shutdown.
|
|
32
|
+
CMD ["node", "src/main.ts"]
|
|
33
|
+
`;
|
|
34
|
+
files[".dockerignore"] = "node_modules\n.env\ncoverage\n.git\n*.db\n*.db-*\n";
|
|
35
|
+
files["compose.yaml"] = composeTemplate(db);
|
|
36
|
+
}
|
|
37
|
+
if (ci)
|
|
38
|
+
files[".github/workflows/ci.yml"] = ciTemplate(pm, install.ci);
|
|
39
|
+
return { files };
|
|
40
|
+
}
|
|
41
|
+
function composeTemplate(db) {
|
|
42
|
+
const app = [
|
|
43
|
+
" app:",
|
|
44
|
+
" build: .",
|
|
45
|
+
" env_file: .env",
|
|
46
|
+
" ports:",
|
|
47
|
+
' - "3000:3000"',
|
|
48
|
+
" stop_grace_period: 30s",
|
|
49
|
+
];
|
|
50
|
+
if (db === "postgres") {
|
|
51
|
+
app.push(" environment:", " DATABASE_URL: postgres://app:app@db:5432/app", " depends_on:", " db:", " condition: service_healthy");
|
|
52
|
+
return `services:
|
|
53
|
+
${app.join("\n")}
|
|
54
|
+
db:
|
|
55
|
+
image: postgres:17-alpine
|
|
56
|
+
environment:
|
|
57
|
+
POSTGRES_USER: app
|
|
58
|
+
POSTGRES_PASSWORD: app
|
|
59
|
+
POSTGRES_DB: app
|
|
60
|
+
ports:
|
|
61
|
+
- "5432:5432"
|
|
62
|
+
volumes:
|
|
63
|
+
- db:/var/lib/postgresql/data
|
|
64
|
+
healthcheck:
|
|
65
|
+
test: ["CMD-SHELL", "pg_isready -U app"]
|
|
66
|
+
interval: 5s
|
|
67
|
+
retries: 10
|
|
68
|
+
|
|
69
|
+
volumes:
|
|
70
|
+
db:
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
if (db === "sqlite") {
|
|
74
|
+
app.push(" environment:", " DATABASE_PATH: /app/data/app.db", " volumes:", " - data:/app/data");
|
|
75
|
+
return `services:\n${app.join("\n")}\n\nvolumes:\n data:\n`;
|
|
76
|
+
}
|
|
77
|
+
return `services:\n${app.join("\n")}\n`;
|
|
78
|
+
}
|
|
79
|
+
function ciTemplate(pm, install) {
|
|
80
|
+
const setup = pm === "pnpm" ? " - uses: pnpm/action-setup@v4\n" : "";
|
|
81
|
+
return `name: CI
|
|
82
|
+
|
|
83
|
+
on:
|
|
84
|
+
push:
|
|
85
|
+
branches: [main]
|
|
86
|
+
pull_request:
|
|
87
|
+
|
|
88
|
+
jobs:
|
|
89
|
+
check:
|
|
90
|
+
runs-on: ubuntu-latest
|
|
91
|
+
steps:
|
|
92
|
+
- uses: actions/checkout@v4
|
|
93
|
+
${setup} - uses: actions/setup-node@v4
|
|
94
|
+
with:
|
|
95
|
+
node-version: 26
|
|
96
|
+
cache: ${pm}
|
|
97
|
+
- run: ${install}
|
|
98
|
+
- run: ${pm} run typecheck
|
|
99
|
+
- run: ${pm} test
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=docker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docker.js","sourceRoot":"","sources":["../../../../src/cli/templates/docker.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,GAAG;IACd,IAAI,EAAE;QACJ,IAAI,EAAE,gBAAgB;QACtB,IAAI,EAAE,uCAAuC;QAC7C,EAAE,EAAE,gCAAgC;KACrC;IACD,GAAG,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,mBAAmB,EAAE,EAAE,EAAE,QAAQ,EAAE;IAC3E,IAAI,EAAE;QACJ,IAAI,EAAE,WAAW;QACjB,IAAI,EAAE,6CAA6C;QACnD,EAAE,EAAE,gCAAgC;KACrC;CACO,CAAC;AAEX,+EAA+E;AAC/E,MAAM,UAAU,WAAW,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAe;IAC7D,MAAM,OAAO,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC;IAC5B,MAAM,KAAK,GAA2B,EAAE,CAAC;IACzC,IAAI,MAAM,EAAE,CAAC;QACX,KAAK,CAAC,YAAY,CAAC,GAAG;;;;EAIxB,EAAE,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,uBAAuB,qBAAqB,OAAO,CAAC,IAAI;MACxE,OAAO,CAAC,IAAI;;;EAGhB,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,uDAAuD,CAAC,CAAC,CAAC,EAAE;;;;;;CAM/E,CAAC;QACE,KAAK,CAAC,eAAe,CAAC,GAAG,oDAAoD,CAAC;QAC9E,KAAK,CAAC,cAAc,CAAC,GAAG,eAAe,CAAC,EAAE,CAAC,CAAC;IAC9C,CAAC;IACD,IAAI,EAAE;QAAE,KAAK,CAAC,0BAA0B,CAAC,GAAG,UAAU,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IACvE,OAAO,EAAE,KAAK,EAAE,CAAC;AACnB,CAAC;AAED,SAAS,eAAe,CAAC,EAAqB;IAC5C,MAAM,GAAG,GAAG;QACV,QAAQ;QACR,cAAc;QACd,oBAAoB;QACpB,YAAY;QACZ,qBAAqB;QACrB,4BAA4B;KAC7B,CAAC;IACF,IAAI,EAAE,KAAK,UAAU,EAAE,CAAC;QACtB,GAAG,CAAC,IAAI,CACN,kBAAkB,EAClB,oDAAoD,EACpD,iBAAiB,EACjB,WAAW,EACX,oCAAoC,CACrC,CAAC;QACF,OAAO;EACT,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;CAkBf,CAAC;IACA,CAAC;IACD,IAAI,EAAE,KAAK,QAAQ,EAAE,CAAC;QACpB,GAAG,CAAC,IAAI,CACN,kBAAkB,EAClB,uCAAuC,EACvC,cAAc,EACd,wBAAwB,CACzB,CAAC;QACF,OAAO,cAAc,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,yBAAyB,CAAC;IAC/D,CAAC;IACD,OAAO,cAAc,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AAC1C,CAAC;AAED,SAAS,UAAU,CAAC,EAAqB,EAAE,OAAe;IACxD,MAAM,KAAK,GAAG,EAAE,KAAK,MAAM,CAAC,CAAC,CAAC,sCAAsC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1E,OAAO;;;;;;;;;;;;EAYP,KAAK;;;mBAGY,EAAE;eACN,OAAO;eACP,EAAE;eACF,EAAE;CAChB,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Names } from "../names.ts";
|
|
2
|
+
export declare const schemaTemplate: ({ entity }: Names) => string;
|
|
3
|
+
export declare const repositoryTemplate: ({ pascal, entity }: Names) => string;
|
|
4
|
+
export declare const serviceTemplate: ({ kebab, pascal, entity, }: Names) => string;
|
|
5
|
+
export declare const controllerTemplate: ({ kebab, camel, pascal, entity, }: Names) => string;
|
|
6
|
+
export declare const testTemplate: ({ kebab, camel, }: Names) => string;
|
|
7
|
+
export declare const middlewareTemplate: ({ kebab, camel, }: Names) => string;
|
|
8
|
+
//# sourceMappingURL=feature.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature.d.ts","sourceRoot":"","sources":["../../../../src/cli/templates/feature.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,eAAO,MAAM,cAAc,eAAgB,KAAK,KAAG,MAkBlD,CAAC;AAEF,eAAO,MAAM,kBAAkB,uBAAwB,KAAK,KAAG,MA0B9D,CAAC;AAEF,eAAO,MAAM,eAAe,+BAIzB,KAAK,KAAG,MA+BV,CAAC;AAEF,eAAO,MAAM,kBAAkB,sCAK5B,KAAK,KAAG,MA6BV,CAAC;AAEF,eAAO,MAAM,YAAY,sBAGtB,KAAK,KAAG,MAuBV,CAAC;AAEF,eAAO,MAAM,kBAAkB,sBAG5B,KAAK,KAAG,MAWV,CAAC"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
export const schemaTemplate = ({ entity }) => `import { z } from "@raiyyan/brisk";
|
|
2
|
+
|
|
3
|
+
export const Create${entity}Body = z.object({
|
|
4
|
+
name: z.string().trim().min(1),
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
// Keep .default() out of the create schema: .partial() keeps defaults, and a PATCH would reset omitted fields.
|
|
8
|
+
export const Update${entity}Body = Create${entity}Body.partial();
|
|
9
|
+
|
|
10
|
+
export const ${entity}IdParams = z.object({ id: z.uuid() });
|
|
11
|
+
|
|
12
|
+
export const ${entity}Response = z.object({
|
|
13
|
+
id: z.string(),
|
|
14
|
+
name: z.string(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export type Create${entity}Body = z.infer<typeof Create${entity}Body>;
|
|
18
|
+
export type Update${entity}Body = z.infer<typeof Update${entity}Body>;
|
|
19
|
+
`;
|
|
20
|
+
export const repositoryTemplate = ({ pascal, entity }) => `export interface ${entity} {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** In-memory storage. Swap the map for your database; the service does not change. */
|
|
26
|
+
export class ${pascal}Repository {
|
|
27
|
+
readonly #rows = new Map<string, ${entity}>();
|
|
28
|
+
|
|
29
|
+
findAll(): ${entity}[] {
|
|
30
|
+
return [...this.#rows.values()];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
findById(id: string): ${entity} | undefined {
|
|
34
|
+
return this.#rows.get(id);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
save(row: ${entity}): ${entity} {
|
|
38
|
+
this.#rows.set(row.id, row);
|
|
39
|
+
return row;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
delete(id: string): boolean {
|
|
43
|
+
return this.#rows.delete(id);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
export const serviceTemplate = ({ kebab, pascal, entity, }) => `import { randomUUID } from "node:crypto";
|
|
48
|
+
import { inject, NotFoundError } from "@raiyyan/brisk";
|
|
49
|
+
import { ${pascal}Repository, type ${entity} } from "./${kebab}.repository.ts";
|
|
50
|
+
import type { Create${entity}Body, Update${entity}Body } from "./${kebab}.schema.ts";
|
|
51
|
+
|
|
52
|
+
export class ${pascal}Service {
|
|
53
|
+
readonly #repository = inject(${pascal}Repository);
|
|
54
|
+
|
|
55
|
+
list(): ${entity}[] {
|
|
56
|
+
return this.#repository.findAll();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get(id: string): ${entity} {
|
|
60
|
+
const row = this.#repository.findById(id);
|
|
61
|
+
if (!row) throw new NotFoundError("${entity} not found", { details: { id } });
|
|
62
|
+
return row;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
create(input: Create${entity}Body): ${entity} {
|
|
66
|
+
return this.#repository.save({ id: randomUUID(), ...input });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
update(id: string, { name }: Update${entity}Body): ${entity} {
|
|
70
|
+
const row = this.get(id);
|
|
71
|
+
return this.#repository.save({ ...row, name: name ?? row.name });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
remove(id: string): void {
|
|
75
|
+
if (!this.#repository.delete(id)) throw new NotFoundError("${entity} not found", { details: { id } });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
79
|
+
export const controllerTemplate = ({ kebab, camel, pascal, entity, }) => `import { defineController, inject } from "@raiyyan/brisk";
|
|
80
|
+
import { Create${entity}Body, ${entity}IdParams, ${entity}Response, Update${entity}Body } from "./${kebab}.schema.ts";
|
|
81
|
+
import { ${pascal}Service } from "./${kebab}.service.ts";
|
|
82
|
+
|
|
83
|
+
export const ${camel}Controller = defineController({ prefix: "/${kebab}" }, (route) => {
|
|
84
|
+
const ${camel} = inject(${pascal}Service);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
list: route.get("/", { response: ${entity}Response.array() }, () => ${camel}.list()),
|
|
88
|
+
|
|
89
|
+
get: route.get("/:id", { params: ${entity}IdParams, response: ${entity}Response }, ({ params }) =>
|
|
90
|
+
${camel}.get(params.id),
|
|
91
|
+
),
|
|
92
|
+
|
|
93
|
+
create: route.post("/", { body: Create${entity}Body, response: ${entity}Response }, ({ body }) =>
|
|
94
|
+
${camel}.create(body),
|
|
95
|
+
),
|
|
96
|
+
|
|
97
|
+
update: route.patch(
|
|
98
|
+
"/:id",
|
|
99
|
+
{ params: ${entity}IdParams, body: Update${entity}Body, response: ${entity}Response },
|
|
100
|
+
({ params, body }) => ${camel}.update(params.id, body),
|
|
101
|
+
),
|
|
102
|
+
|
|
103
|
+
remove: route.delete("/:id", { params: ${entity}IdParams }, ({ params }) => {
|
|
104
|
+
${camel}.remove(params.id);
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
`;
|
|
109
|
+
export const testTemplate = ({ kebab, camel, }) => `import { afterAll, describe, expect, it } from "vitest";
|
|
110
|
+
import { createTestClient } from "@raiyyan/brisk/testing";
|
|
111
|
+
import { app } from "../src/app.ts";
|
|
112
|
+
|
|
113
|
+
const api = await createTestClient(app);
|
|
114
|
+
afterAll(() => api.close());
|
|
115
|
+
|
|
116
|
+
describe("${kebab}", () => {
|
|
117
|
+
it("creates, reads, updates and deletes", async () => {
|
|
118
|
+
const created = await api.call.${camel}.create({ body: { name: "first" } });
|
|
119
|
+
const { id } = created;
|
|
120
|
+
|
|
121
|
+
expect(await api.call.${camel}.get({ params: { id } })).toEqual(created);
|
|
122
|
+
expect(await api.call.${camel}.update({ params: { id }, body: { name: "second" } })).toEqual({ id, name: "second" });
|
|
123
|
+
await api.call.${camel}.remove({ params: { id } });
|
|
124
|
+
await expect(api.call.${camel}.get({ params: { id } })).rejects.toMatchObject({ status: 404 });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("rejects invalid input with field issues", async () => {
|
|
128
|
+
const res = await api.post("/api/${kebab}", { body: { name: "" } });
|
|
129
|
+
expect(res.status).toBe(400);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
`;
|
|
133
|
+
export const middlewareTemplate = ({ kebab, camel, }) => `import { BadRequestError, defineMiddleware } from "@raiyyan/brisk";
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Reads the \`x-${kebab}\` header and adds \`${camel}\` to the ctx of every route that uses it.
|
|
137
|
+
* Replace the check with your own logic; return nothing for a check-only middleware.
|
|
138
|
+
*/
|
|
139
|
+
export const ${camel} = defineMiddleware(({ req }) => {
|
|
140
|
+
const value = req.get("x-${kebab}");
|
|
141
|
+
if (!value) throw new BadRequestError("Missing x-${kebab} header", { code: "MISSING_${kebab.toUpperCase().replaceAll("-", "_")}" });
|
|
142
|
+
return { ${camel}: value };
|
|
143
|
+
}, "${camel}");
|
|
144
|
+
`;
|
|
145
|
+
//# sourceMappingURL=feature.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature.js","sourceRoot":"","sources":["../../../../src/cli/templates/feature.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,EAAE,MAAM,EAAS,EAAU,EAAE,CAAC;;qBAExC,MAAM;;;;;qBAKN,MAAM,gBAAgB,MAAM;;eAElC,MAAM;;eAEN,MAAM;;;;;oBAKD,MAAM,+BAA+B,MAAM;oBAC3C,MAAM,+BAA+B,MAAM;CAC9D,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,EAAE,MAAM,EAAE,MAAM,EAAS,EAAU,EAAE,CAAC,oBAAoB,MAAM;;;;;;eAMpF,MAAM;qCACgB,MAAM;;eAE5B,MAAM;;;;0BAIK,MAAM;;;;cAIlB,MAAM,MAAM,MAAM;;;;;;;;;CAS/B,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,EAC9B,KAAK,EACL,MAAM,EACN,MAAM,GACA,EAAU,EAAE,CAAC;;WAEV,MAAM,oBAAoB,MAAM,cAAc,KAAK;sBACxC,MAAM,eAAe,MAAM,kBAAkB,KAAK;;eAEzD,MAAM;kCACa,MAAM;;YAE5B,MAAM;;;;qBAIG,MAAM;;yCAEc,MAAM;;;;wBAIvB,MAAM,UAAU,MAAM;;;;uCAIP,MAAM,UAAU,MAAM;;;;;;iEAMI,MAAM;;;CAGtE,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,EACjC,KAAK,EACL,KAAK,EACL,MAAM,EACN,MAAM,GACA,EAAU,EAAE,CAAC;iBACJ,MAAM,SAAS,MAAM,aAAa,MAAM,mBAAmB,MAAM,kBAAkB,KAAK;WAC9F,MAAM,qBAAqB,KAAK;;eAE5B,KAAK,6CAA6C,KAAK;UAC5D,KAAK,aAAa,MAAM;;;uCAGK,MAAM,6BAA6B,KAAK;;uCAExC,MAAM,uBAAuB,MAAM;QAClE,KAAK;;;4CAG+B,MAAM,mBAAmB,MAAM;QACnE,KAAK;;;;;kBAKK,MAAM,yBAAyB,MAAM,mBAAmB,MAAM;8BAClD,KAAK;;;6CAGU,MAAM;QAC3C,KAAK;;;;CAIZ,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,EAC3B,KAAK,EACL,KAAK,GACC,EAAU,EAAE,CAAC;;;;;;;YAOT,KAAK;;qCAEoB,KAAK;;;4BAGd,KAAK;4BACL,KAAK;qBACZ,KAAK;4BACE,KAAK;;;;uCAIM,KAAK;;;;CAI3C,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,EACjC,KAAK,EACL,KAAK,GACC,EAAU,EAAE,CAAC;;;mBAGF,KAAK,wBAAwB,KAAK;;;eAGtC,KAAK;6BACS,KAAK;qDACmB,KAAK,8BAA8B,KAAK,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC;aACnH,KAAK;MACZ,KAAK;CACV,CAAC"}
|