create-nwire 0.7.1 → 0.8.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/dist/__tests__/scaffold.test.d.ts +8 -3
- package/dist/__tests__/scaffold.test.d.ts.map +1 -1
- package/dist/__tests__/scaffold.test.js +49 -22
- package/dist/__tests__/scaffold.test.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/L1/README.md +34 -3
- package/templates/L1/__tests__/hello.test.ts +54 -0
- package/templates/L1/app/api.ts +17 -13
- package/templates/L1/app/main.ts +11 -1
- package/templates/L1/app/routes/hello.ts +29 -0
- package/templates/L1/tsconfig.json +1 -1
- package/templates/L2/README.md +33 -13
- package/templates/L2/__tests__/todo-api.test.ts +115 -0
- package/templates/L2/app/api.ts +20 -115
- package/templates/L2/app/{errors.ts → errors/todo-errors.ts} +6 -6
- package/templates/L2/app/main.ts +7 -7
- package/templates/L2/app/middleware/require-user.ts +29 -0
- package/templates/L2/app/{model.ts → resources/todo.ts} +21 -5
- package/templates/L2/app/routes/complete-todo.ts +36 -0
- package/templates/L2/app/routes/create-todo.ts +27 -0
- package/templates/L2/app/routes/delete-todo.ts +31 -0
- package/templates/L2/app/routes/list-todos.ts +28 -0
- package/templates/L2/app/{store.ts → store/todo-store.ts} +13 -5
- package/templates/L2/tsconfig.json +1 -1
- package/templates/L4/README.md +67 -17
- package/templates/L4/__tests__/auto-moderate.test.ts +96 -0
- package/templates/L4/__tests__/submit-flow.test.ts +80 -0
- package/templates/L4/app/api.ts +19 -109
- package/templates/L4/app/app.ts +4 -4
- package/templates/L4/app/main.ts +11 -12
- package/templates/L4/modules/posts/actions/approve-post.ts +28 -0
- package/templates/L4/modules/posts/actions/reject-post.ts +29 -0
- package/templates/L4/modules/posts/actions/submit-post.ts +36 -0
- package/templates/L4/modules/posts/events/post-was-approved.ts +22 -0
- package/templates/L4/modules/posts/events/post-was-rejected.ts +20 -0
- package/templates/L4/modules/posts/events/post-was-submitted.ts +27 -0
- package/templates/L4/modules/posts/posts.module.ts +35 -0
- package/templates/L4/{app/queue.projection.ts → modules/posts/projections/queue-dashboard.ts} +18 -30
- package/templates/L4/modules/posts/routes/approve-post.ts +26 -0
- package/templates/L4/modules/posts/routes/get-post.ts +20 -0
- package/templates/L4/modules/posts/routes/list-queue.ts +22 -0
- package/templates/L4/modules/posts/routes/reject-post.ts +29 -0
- package/templates/L4/modules/posts/routes/submit-post.ts +28 -0
- package/templates/L4/{app/auto-moderate.workflow.ts → modules/posts/workflows/auto-moderate.ts} +13 -20
- package/templates/L4/tsconfig.json +1 -1
- package/templates/L2/app/middleware.ts +0 -32
- package/templates/L4/app/actions.ts +0 -81
- package/templates/L4/app/events.ts +0 -53
- package/templates/L4/app/moderation.module.ts +0 -28
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scaffold tests — verify each template materializes a coherent project
|
|
3
3
|
* directory with the project name substituted and the canonical files
|
|
4
|
-
* present.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* present.
|
|
5
|
+
*
|
|
6
|
+
* Every template is folder-organized: a flat-file shape would be smaller
|
|
7
|
+
* but graduating L1→L2→L4 with new conventions per level is hostile to
|
|
8
|
+
* users. One shape, one set of mental models.
|
|
9
|
+
*
|
|
10
|
+
* Does NOT install or boot the project (npm install in CI burns minutes
|
|
11
|
+
* and adds nothing — the boot path is exercised by the e2e examples).
|
|
7
12
|
*/
|
|
8
13
|
export {};
|
|
9
14
|
//# sourceMappingURL=scaffold.test.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffold.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/scaffold.test.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"scaffold.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/scaffold.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scaffold tests — verify each template materializes a coherent project
|
|
3
3
|
* directory with the project name substituted and the canonical files
|
|
4
|
-
* present.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* present.
|
|
5
|
+
*
|
|
6
|
+
* Every template is folder-organized: a flat-file shape would be smaller
|
|
7
|
+
* but graduating L1→L2→L4 with new conventions per level is hostile to
|
|
8
|
+
* users. One shape, one set of mental models.
|
|
9
|
+
*
|
|
10
|
+
* Does NOT install or boot the project (npm install in CI burns minutes
|
|
11
|
+
* and adds nothing — the boot path is exercised by the e2e examples).
|
|
7
12
|
*/
|
|
8
13
|
import { mkdtempSync, readFileSync, existsSync, rmSync } from "node:fs";
|
|
9
14
|
import { tmpdir } from "node:os";
|
|
@@ -20,48 +25,70 @@ describe("create-nwire scaffold", () => {
|
|
|
20
25
|
if (existsSync(target))
|
|
21
26
|
rmSync(target, { recursive: true, force: true });
|
|
22
27
|
});
|
|
23
|
-
it("L1 —
|
|
28
|
+
it("L1 — minimal http+endpoint with folder-organized routes", () => {
|
|
24
29
|
const { written } = scaffold({ target, template: "L1", name: "alpha" });
|
|
25
30
|
expect(written.length).toBeGreaterThan(0);
|
|
26
31
|
const pkg = JSON.parse(readFileSync(join(target, "package.json"), "utf8"));
|
|
27
32
|
expect(pkg.name).toBe("alpha");
|
|
28
33
|
expect(pkg.dependencies["@nwire/endpoint"]).toBeDefined();
|
|
29
34
|
expect(pkg.dependencies["@nwire/http"]).toBeDefined();
|
|
30
|
-
|
|
31
|
-
expect(existsSync(join(target, "
|
|
35
|
+
// Folder shape: app/main.ts + app/api.ts + app/routes/<route>.ts + __tests__/
|
|
36
|
+
expect(existsSync(join(target, "app/main.ts"))).toBe(true);
|
|
37
|
+
expect(existsSync(join(target, "app/api.ts"))).toBe(true);
|
|
38
|
+
expect(existsSync(join(target, "app/routes/hello.ts"))).toBe(true);
|
|
39
|
+
expect(existsSync(join(target, "__tests__/hello.test.ts"))).toBe(true);
|
|
32
40
|
expect(existsSync(join(target, "tsconfig.json"))).toBe(true);
|
|
33
41
|
expect(existsSync(join(target, ".gitignore"))).toBe(true);
|
|
34
|
-
expect(readFileSync(join(target, "
|
|
42
|
+
expect(readFileSync(join(target, "app/main.ts"), "utf8")).toContain('"alpha"');
|
|
35
43
|
});
|
|
36
|
-
it("L2 —
|
|
44
|
+
it("L2 — service shape with resources/errors/middleware/store/routes folders", () => {
|
|
37
45
|
const { written } = scaffold({ target, template: "L2", name: "beta-svc" });
|
|
38
46
|
expect(written.length).toBeGreaterThan(0);
|
|
39
47
|
const pkg = JSON.parse(readFileSync(join(target, "package.json"), "utf8"));
|
|
40
48
|
expect(pkg.name).toBe("beta-svc");
|
|
41
49
|
expect(pkg.dependencies["@nwire/forge"]).toBeDefined();
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
// Boot files
|
|
51
|
+
for (const f of ["app/main.ts", "app/api.ts"]) {
|
|
52
|
+
expect(existsSync(join(target, f))).toBe(true);
|
|
44
53
|
}
|
|
54
|
+
// Folder-organized pieces
|
|
55
|
+
expect(existsSync(join(target, "app/resources/todo.ts"))).toBe(true);
|
|
56
|
+
expect(existsSync(join(target, "app/errors/todo-errors.ts"))).toBe(true);
|
|
57
|
+
expect(existsSync(join(target, "app/middleware/require-user.ts"))).toBe(true);
|
|
58
|
+
expect(existsSync(join(target, "app/store/todo-store.ts"))).toBe(true);
|
|
59
|
+
// One route per file
|
|
60
|
+
for (const r of ["list-todos", "create-todo", "complete-todo", "delete-todo"]) {
|
|
61
|
+
expect(existsSync(join(target, `app/routes/${r}.ts`))).toBe(true);
|
|
62
|
+
}
|
|
63
|
+
expect(existsSync(join(target, "__tests__/todo-api.test.ts"))).toBe(true);
|
|
45
64
|
});
|
|
46
|
-
it("L4 —
|
|
65
|
+
it("L4 — enterprise shape with modules/<bc>/{events,actions,workflows,projections,routes}", () => {
|
|
47
66
|
const { written } = scaffold({ target, template: "L4", name: "gamma-app" });
|
|
48
67
|
expect(written.length).toBeGreaterThan(0);
|
|
49
68
|
const pkg = JSON.parse(readFileSync(join(target, "package.json"), "utf8"));
|
|
50
69
|
expect(pkg.name).toBe("gamma-app");
|
|
51
70
|
expect(pkg.dependencies["@nwire/app"]).toBeDefined();
|
|
52
71
|
expect(pkg.dependencies["@nwire/forge"]).toBeDefined();
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
]) {
|
|
63
|
-
expect(existsSync(join(target,
|
|
72
|
+
// Boot trio in app/
|
|
73
|
+
for (const f of ["app/main.ts", "app/api.ts", "app/app.ts"]) {
|
|
74
|
+
expect(existsSync(join(target, f))).toBe(true);
|
|
75
|
+
}
|
|
76
|
+
// The posts bounded context
|
|
77
|
+
expect(existsSync(join(target, "modules/posts/posts.module.ts"))).toBe(true);
|
|
78
|
+
for (const e of ["post-was-submitted", "post-was-approved", "post-was-rejected"]) {
|
|
79
|
+
expect(existsSync(join(target, `modules/posts/events/${e}.ts`))).toBe(true);
|
|
80
|
+
}
|
|
81
|
+
for (const a of ["submit-post", "approve-post", "reject-post"]) {
|
|
82
|
+
expect(existsSync(join(target, `modules/posts/actions/${a}.ts`))).toBe(true);
|
|
83
|
+
}
|
|
84
|
+
expect(existsSync(join(target, "modules/posts/workflows/auto-moderate.ts"))).toBe(true);
|
|
85
|
+
expect(existsSync(join(target, "modules/posts/projections/queue-dashboard.ts"))).toBe(true);
|
|
86
|
+
for (const r of ["submit-post", "approve-post", "reject-post", "list-queue", "get-post"]) {
|
|
87
|
+
expect(existsSync(join(target, `modules/posts/routes/${r}.ts`))).toBe(true);
|
|
64
88
|
}
|
|
89
|
+
// Tests
|
|
90
|
+
expect(existsSync(join(target, "__tests__/submit-flow.test.ts"))).toBe(true);
|
|
91
|
+
expect(existsSync(join(target, "__tests__/auto-moderate.test.ts"))).toBe(true);
|
|
65
92
|
});
|
|
66
93
|
it("refuses to scaffold into a non-empty target", () => {
|
|
67
94
|
const { written } = scaffold({ target, template: "L1", name: "first" });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffold.test.js","sourceRoot":"","sources":["../../src/__tests__/scaffold.test.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"scaffold.test.js","sourceRoot":"","sources":["../../src/__tests__/scaffold.test.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAkBH,MAAM,MAAM,aAAa,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAiB/C,UAAU,eAAe;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAeD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG;IAAE,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAiBrE;AAoHD,wBAAsB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAE5C"}
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* no template fetching from GitHub. Picking a level is the only decision;
|
|
13
13
|
* everything else is whatever the user does once their starter is live.
|
|
14
14
|
*/
|
|
15
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, } from "node:fs";
|
|
16
16
|
import { dirname, join, resolve } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
import { defineCommand, runMain } from "citty";
|
|
@@ -21,15 +21,15 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
21
21
|
const TEMPLATES = {
|
|
22
22
|
L1: {
|
|
23
23
|
title: "Minimal (L1)",
|
|
24
|
-
blurb: "httpInterface + endpoint
|
|
24
|
+
blurb: "httpInterface + endpoint. Folder-organized routes/. No app, no DI.",
|
|
25
25
|
},
|
|
26
26
|
L2: {
|
|
27
27
|
title: "Service (L2)",
|
|
28
|
-
blurb: "
|
|
28
|
+
blurb: "Per-file routes/resources/errors/middleware/store. In-memory CRUD.",
|
|
29
29
|
},
|
|
30
30
|
L4: {
|
|
31
31
|
title: "Enterprise (L4)",
|
|
32
|
-
blurb: "
|
|
32
|
+
blurb: "modules/<bc>/{events,actions,workflows,projections,routes}. Full forge shape.",
|
|
33
33
|
},
|
|
34
34
|
};
|
|
35
35
|
/**
|
|
@@ -64,13 +64,27 @@ export function scaffold(opts) {
|
|
|
64
64
|
applyVariables(opts.target, { PROJECT_NAME: projectName });
|
|
65
65
|
return { written };
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Files that get a leading underscore in the template tree because
|
|
69
|
+
* npm strips dotfiles when packing tarballs. We rename them back to
|
|
70
|
+
* their canonical dotfile names during scaffold.
|
|
71
|
+
*
|
|
72
|
+
* The earlier blanket rule "any entry starting with `_` becomes `.`"
|
|
73
|
+
* was wrong — it mangled directories like `__tests__` into `._tests__`.
|
|
74
|
+
* Keep the rename explicit so new template files don't surprise us.
|
|
75
|
+
*/
|
|
76
|
+
const UNDERSCORE_TO_DOT = {
|
|
77
|
+
_gitignore: ".gitignore",
|
|
78
|
+
_npmrc: ".npmrc",
|
|
79
|
+
_eslintrc: ".eslintrc",
|
|
80
|
+
_eslintignore: ".eslintignore",
|
|
81
|
+
_prettierrc: ".prettierrc",
|
|
82
|
+
_env: ".env",
|
|
83
|
+
};
|
|
67
84
|
function copyTree(src, dest, log) {
|
|
68
85
|
for (const entry of readdirSync(src)) {
|
|
69
86
|
const s = join(src, entry);
|
|
70
|
-
|
|
71
|
-
// as `_gitignore` / `_npmrc`; rename back on copy. Same trick that
|
|
72
|
-
// create-vite uses.
|
|
73
|
-
const renamed = entry.startsWith("_") ? "." + entry.slice(1) : entry;
|
|
87
|
+
const renamed = UNDERSCORE_TO_DOT[entry] ?? entry;
|
|
74
88
|
const d = join(dest, renamed);
|
|
75
89
|
if (statSync(s).isDirectory()) {
|
|
76
90
|
mkdirSync(d, { recursive: true });
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,MAAM,EACN,UAAU,EACV,SAAS,EACT,YAAY,EACZ,WAAW,EACX,QAAQ,EACR,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAC/C,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAI1D,MAAM,SAAS,GAA4D;IACzE,EAAE,EAAE;QACF,KAAK,EAAE,cAAc;QACrB,KAAK,EAAE,oEAAoE;KAC5E;IACD,EAAE,EAAE;QACF,KAAK,EAAE,cAAc;QACrB,KAAK,EAAE,oEAAoE;KAC5E;IACD,EAAE,EAAE;QACF,KAAK,EAAE,iBAAiB;QACxB,KAAK,EAAE,+EAA+E;KACvF;CACF,CAAC;AAQF;;;;GAIG;AACH,SAAS,aAAa;IACpB,mEAAmE;IACnE,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;IACxD,IAAI,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,CAAC;IAC5C,wCAAwC;IACxC,OAAO,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;AACrD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAqB;IAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IACjD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,aAAa,IAAI,CAAC,QAAQ,kBAAkB,GAAG,EAAE,CAAC,CAAC;IACrE,CAAC;IACD,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACnE,MAAM,IAAI,KAAK,CAAC,qBAAqB,IAAI,CAAC,MAAM,4BAA4B,CAAC,CAAC;IAChF,CAAC;IACD,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE5C,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEpC,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACvD,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,YAAY,EAAE,WAAW,EAAE,CAAC,CAAC;IAE3D,OAAO,EAAE,OAAO,EAAE,CAAC;AACrB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,iBAAiB,GAAqC;IAC1D,UAAU,EAAK,YAAY;IAC3B,MAAM,EAAS,QAAQ;IACvB,SAAS,EAAM,WAAW;IAC1B,aAAa,EAAE,eAAe;IAC9B,WAAW,EAAI,aAAa;IAC5B,IAAI,EAAW,MAAM;CACtB,CAAC;AAEF,SAAS,QAAQ,CAAC,GAAW,EAAE,IAAY,EAAE,GAAa;IACxD,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC3B,MAAM,OAAO,GAAG,iBAAiB,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC;QAClD,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC9B,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;YAC9B,SAAS,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAClC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;QACtB,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YACb,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACd,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,IAAY,EAAE,IAA4B;IAChE,0EAA0E;IAC1E,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC5B,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QACvB,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;YACrB,cAAc,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YACxB,SAAS;QACX,CAAC;QACD,IAAI,CAAC,qCAAqC,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,SAAS;QACjE,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QACrC,IAAI,GAAG,GAAG,IAAI,CAAC;QACf,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1C,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACtC,CAAC;QACD,IAAI,GAAG,KAAK,IAAI;YAAE,aAAa,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC1C,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,CAAS;IACzB,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/C,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,WAAW,CAAC;AAChD,CAAC;AAED,MAAM,IAAI,GAAG,aAAa,CAAC;IACzB,IAAI,EAAE;QACJ,IAAI,EAAE,cAAc;QACpB,WAAW,EAAE,+BAA+B;KAC7C;IACD,IAAI,EAAE;QACJ,MAAM,EAAE;YACN,IAAI,EAAE,YAAY;YAClB,WAAW,EAAE,gDAAgD;YAC7D,QAAQ,EAAE,KAAK;SAChB;QACD,QAAQ,EAAE;YACR,IAAI,EAAE,QAAQ;YACd,WAAW,EAAE,8BAA8B;YAC3C,OAAO,EAAE,IAAI;SACd;QACD,IAAI,EAAE;YACJ,IAAI,EAAE,QAAQ;YACd,WAAW,EAAE,iDAAiD;SAC/D;KACF;IACD,KAAK,CAAC,GAAG,CAAC,GAAG;QACX,MAAM,SAAS,GAAI,GAAG,CAAC,IAA4B,CAAC,MAAM,CAAC;QAC3D,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC,CAAC;YACnD,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,wCAAwC,CAAC,EAAE,CAAC,CAAC;YAC1F,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAClB,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YAC5B,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC/C,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACrF,CAAC;YACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;QACvD,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YAC1D,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,qBAAqB,GAAG,CAAC,IAAI,CAAC,QAAQ,uBAAuB,CAAC,CAAC,CAAC;YACrF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,MAAM,QAAQ,GAAG,MAAuB,CAAC;QACzC,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,CAAC,CAAC;QAEjD,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,mBAAmB,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;QACrF,IAAI,CAAC;YACH,MAAM,EAAE,OAAO,EAAE,GAAG,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;YACxE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,CAAC,aAAa,OAAO,CAAC,MAAM,UAAU,CAAC,CAAC,CAAC;YAC7D,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACvB,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,MAAM,SAAS,EAAE,CAAC,EAAE,CAAC,CAAC;YAChE,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;YAC7D,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;YACzD,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAClB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,OAAQ,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YACvD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;CACF,CAAC,CAAC;AAEH,MAAM,CAAC,KAAK,UAAU,MAAM;IAC1B,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;AACtB,CAAC"}
|
package/package.json
CHANGED
package/templates/L1/README.md
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
The smallest possible Nwire shape — one POST route, one inline handler,
|
|
4
4
|
no app, no DI, no domain primitives.
|
|
5
5
|
|
|
6
|
+
Despite being tiny, the project is **folder-organized** so it matches L2
|
|
7
|
+
and L4. Adding route #2 is a single new file, not a rewrite.
|
|
8
|
+
|
|
6
9
|
## Run
|
|
7
10
|
|
|
8
11
|
```bash
|
|
@@ -18,17 +21,45 @@ curl -X POST http://localhost:3000/hello \
|
|
|
18
21
|
-d '{"name":"Alice"}'
|
|
19
22
|
```
|
|
20
23
|
|
|
24
|
+
## Test
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm test
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Folder shape
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
{{PROJECT_NAME}}/
|
|
34
|
+
├── app/
|
|
35
|
+
│ ├── main.ts ← endpoint().serve(api).run()
|
|
36
|
+
│ ├── api.ts ← httpInterface().wire(route, handler)
|
|
37
|
+
│ └── routes/
|
|
38
|
+
│ └── hello.ts ← POST /hello — route + handler pair
|
|
39
|
+
└── __tests__/
|
|
40
|
+
└── hello.test.ts
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Where to add a …
|
|
44
|
+
|
|
45
|
+
| You want to add … | Where it goes |
|
|
46
|
+
| ----------------- | --------------------------------------------------------------- |
|
|
47
|
+
| A new route | `app/routes/<verb-noun>.ts` exports `{name}Route + {name}Handler`; `.wire()` it from `app/api.ts` |
|
|
48
|
+
| Middleware | Inline in `app/api.ts` via `.use(middlewareFn)` |
|
|
49
|
+
| Container deps | Graduate to L2 — `app/store/` + `.provide(container)` in api.ts |
|
|
50
|
+
|
|
21
51
|
## What you get free
|
|
22
52
|
|
|
23
53
|
| Feature | Comes from |
|
|
24
54
|
| -------------------------------------- | ----------------- |
|
|
25
55
|
| Zod request validation | `@nwire/http` |
|
|
26
56
|
| OpenAPI schema (visit `/openapi.json`) | `@nwire/http` |
|
|
57
|
+
| Scalar UI docs (visit `/docs`) | `@nwire/http` |
|
|
27
58
|
| Graceful SIGTERM drain | `@nwire/endpoint` |
|
|
28
59
|
| K8s probes on port 9400 | `@nwire/endpoint` |
|
|
29
60
|
|
|
30
61
|
## Grow up
|
|
31
62
|
|
|
32
|
-
When you outgrow inline handlers (multiple routes, persistence,
|
|
33
|
-
re-scaffold with `pnpm create nwire <name> --template L2`
|
|
34
|
-
service shape or `--template L4` for the full enterprise shape.
|
|
63
|
+
When you outgrow inline handlers (multiple routes, persistence, container
|
|
64
|
+
bindings, errors) re-scaffold with `pnpm create nwire <name> --template L2`
|
|
65
|
+
for the service shape or `--template L4` for the full enterprise shape.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke test — POST /hello returns the personalized greeting, and an
|
|
3
|
+
* empty `name` is rejected with a structured validation error.
|
|
4
|
+
*
|
|
5
|
+
* The test boots `api` (no full endpoint — no port binding) and dispatches
|
|
6
|
+
* raw fetch requests against `api.compile()` mounted on a Node http server.
|
|
7
|
+
* That's the same boot path as production, just without the lightship
|
|
8
|
+
* probes and SIGTERM dance.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
12
|
+
import http from "node:http";
|
|
13
|
+
import type { Server } from "node:http";
|
|
14
|
+
import { api } from "../app/api";
|
|
15
|
+
|
|
16
|
+
let server: Server;
|
|
17
|
+
let url: string;
|
|
18
|
+
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
server = http.createServer(api.compile());
|
|
21
|
+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
22
|
+
const addr = server.address() as { port: number };
|
|
23
|
+
url = `http://127.0.0.1:${addr.port}`;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterAll(async () => {
|
|
27
|
+
await new Promise<void>((resolve, reject) =>
|
|
28
|
+
server.close((err) => (err ? reject(err) : resolve())),
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("hello route", () => {
|
|
33
|
+
it("returns the personalized greeting", async () => {
|
|
34
|
+
const res = await fetch(`${url}/hello`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "content-type": "application/json" },
|
|
37
|
+
body: JSON.stringify({ name: "Alice" }),
|
|
38
|
+
});
|
|
39
|
+
expect(res.status).toBe(200);
|
|
40
|
+
const body = (await res.json()) as { message: string };
|
|
41
|
+
expect(body.message).toBe("Hello, Alice!");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("rejects an empty name with a structured validation error", async () => {
|
|
45
|
+
const res = await fetch(`${url}/hello`, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: { "content-type": "application/json" },
|
|
48
|
+
body: JSON.stringify({ name: "" }),
|
|
49
|
+
});
|
|
50
|
+
expect(res.status).toBe(400);
|
|
51
|
+
const body = (await res.json()) as { error: { code: string } };
|
|
52
|
+
expect(body.error.code).toBe("validation_failed");
|
|
53
|
+
});
|
|
54
|
+
});
|
package/templates/L1/app/api.ts
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* HTTP interface — the surface this app exposes.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The interface is built by wiring route bindings to handlers. Each route
|
|
5
|
+
* lives under `./routes/` and exports a `{name}Route` + `{name}Handler`
|
|
6
|
+
* pair. The interface stays a 1-line-per-route reducer; new routes drop in
|
|
7
|
+
* with a single import + a single `.wire(...)` call.
|
|
8
|
+
*
|
|
9
|
+
* Why a folder of routes for "just one route"?
|
|
10
|
+
*
|
|
11
|
+
* - Adding route #2 is a single new file, not a rewrite of api.ts.
|
|
12
|
+
* - Each route's verb + path + schema + handler live together.
|
|
13
|
+
* - The pattern matches L2 + L4, so going up a level is mechanical.
|
|
14
|
+
*
|
|
15
|
+
* When you outgrow inline handlers (multiple bounded contexts, domain
|
|
16
|
+
* events, persistence) graduate via `pnpm create nwire <name> --template L2`.
|
|
7
17
|
*/
|
|
8
18
|
|
|
9
|
-
import { httpInterface
|
|
10
|
-
import {
|
|
19
|
+
import { httpInterface } from "@nwire/http";
|
|
20
|
+
import { helloRoute, helloHandler } from "./routes/hello";
|
|
11
21
|
|
|
12
|
-
export const api = httpInterface().wire(
|
|
13
|
-
post("/hello", { body: z.object({ name: z.string().min(1) }) }),
|
|
14
|
-
async ({ input }) => {
|
|
15
|
-
const { name } = input as { name: string };
|
|
16
|
-
return { message: `Hello, ${name}!` };
|
|
17
|
-
},
|
|
18
|
-
);
|
|
22
|
+
export const api = httpInterface().wire(helloRoute, helloHandler);
|
package/templates/L1/app/main.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Entry —
|
|
2
|
+
* Entry point — boot the HTTP interface inside an endpoint.
|
|
3
|
+
*
|
|
4
|
+
* The lifecycle:
|
|
5
|
+
*
|
|
6
|
+
* 1. `api` (./api.ts) is a built interface — routes registered, schemas
|
|
7
|
+
* compiled, ready to serve.
|
|
8
|
+
* 2. `endpoint(name, { port }).serve(api).run()` starts the HTTP listener
|
|
9
|
+
* AND wires graceful SIGTERM drain + K8s readiness/liveness probes.
|
|
10
|
+
*
|
|
11
|
+
* No createApp, no DI container, no domain primitives. Two packages,
|
|
12
|
+
* three files of source code. The smallest possible Nwire shape.
|
|
3
13
|
*
|
|
4
14
|
* Run: pnpm dev
|
|
5
15
|
* Try: curl -X POST http://localhost:3000/hello \
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /hello — Zod-validated body, typed handler return.
|
|
3
|
+
*
|
|
4
|
+
* The shape every route file follows:
|
|
5
|
+
*
|
|
6
|
+
* - `{name}Route` — the `post(path, schema)` binding
|
|
7
|
+
* - `{name}Handler` — the `async ({ input }) => ...` handler
|
|
8
|
+
*
|
|
9
|
+
* `api.ts` wires them with `.wire(helloRoute, helloHandler)`.
|
|
10
|
+
*
|
|
11
|
+
* The framework converts the Zod schema into OpenAPI on boot — visit
|
|
12
|
+
* `/openapi.json` or the Scalar UI at `/docs` to see it.
|
|
13
|
+
*
|
|
14
|
+
* To add a route, copy this file, rename, then `.wire()` the new pair
|
|
15
|
+
* from `api.ts`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { post, type HttpHandler } from "@nwire/http";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
|
|
21
|
+
const HelloBody = z.object({
|
|
22
|
+
name: z.string().min(1).describe("Caller's first name"),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const helloRoute = post("/hello", { body: HelloBody });
|
|
26
|
+
|
|
27
|
+
export const helloHandler: HttpHandler<z.infer<typeof HelloBody>> = async ({ input }) => {
|
|
28
|
+
return { message: `Hello, ${input.name}!` };
|
|
29
|
+
};
|
package/templates/L2/README.md
CHANGED
|
@@ -30,19 +30,39 @@ curl -X POST http://localhost:3000/api/todos/<id>/complete \
|
|
|
30
30
|
-H "x-user-id: alice"
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
##
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
33
|
+
## Folder shape
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
{{PROJECT_NAME}}/
|
|
37
|
+
├── app/
|
|
38
|
+
│ ├── main.ts ← endpoint().serve(api).run()
|
|
39
|
+
│ ├── api.ts ← httpInterface().use(...).wire(...)
|
|
40
|
+
│ ├── resources/
|
|
41
|
+
│ │ └── todo.ts ← defineResource — public field list + project()
|
|
42
|
+
│ ├── errors/
|
|
43
|
+
│ │ └── todo-errors.ts ← defineError instances
|
|
44
|
+
│ ├── middleware/
|
|
45
|
+
│ │ └── require-user.ts ← requireUser middleware
|
|
46
|
+
│ ├── store/
|
|
47
|
+
│ │ └── todo-store.ts ← in-memory TodoStore + plugin shape
|
|
48
|
+
│ └── routes/
|
|
49
|
+
│ ├── list-todos.ts ← GET /api/todos
|
|
50
|
+
│ ├── create-todo.ts ← POST /api/todos
|
|
51
|
+
│ ├── complete-todo.ts ← POST /api/todos/:id/complete
|
|
52
|
+
│ └── delete-todo.ts ← DELETE /api/todos/:id
|
|
53
|
+
└── __tests__/
|
|
54
|
+
└── todo-api.test.ts
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Where to add a …
|
|
58
|
+
|
|
59
|
+
| You want to add … | Where it goes |
|
|
60
|
+
| ----------------- | ----------------------------------------------------------------------------- |
|
|
61
|
+
| A new route | `app/routes/<verb-noun>.ts` exports `{name}Route + {name}Handler`; `.wire()` it from `api.ts` |
|
|
62
|
+
| A new error type | `app/errors/<resource>-errors.ts` — `defineError({ code, status, summary })` |
|
|
63
|
+
| A new middleware | `app/middleware/<name>.ts` exports `HttpMiddleware`; `.use()` it from `api.ts`|
|
|
64
|
+
| A new resource | `app/resources/<name>.ts` — `defineResource` schema + `public` field list |
|
|
65
|
+
| Real persistence | Swap `app/store/todo-store.ts` for `@nwire/data-drizzle` or `@nwire/store-mongo` |
|
|
46
66
|
|
|
47
67
|
## Grow up
|
|
48
68
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke test — the four CRUD operations + the requireUser middleware.
|
|
3
|
+
*
|
|
4
|
+
* Boots the api against a real Node http server, registers a fresh
|
|
5
|
+
* in-memory TodoStore on a container, and dispatches fetch requests
|
|
6
|
+
* exactly the way clients will.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
10
|
+
import http from "node:http";
|
|
11
|
+
import type { Server } from "node:http";
|
|
12
|
+
import { InMemoryContainer } from "@nwire/container";
|
|
13
|
+
import { TodoStore } from "../app/store/todo-store";
|
|
14
|
+
import { api } from "../app/api";
|
|
15
|
+
|
|
16
|
+
let server: Server;
|
|
17
|
+
let url: string;
|
|
18
|
+
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
const container = new InMemoryContainer();
|
|
21
|
+
container.register("todos", new TodoStore());
|
|
22
|
+
api.provide(container);
|
|
23
|
+
|
|
24
|
+
server = http.createServer(api.compile());
|
|
25
|
+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
26
|
+
const addr = server.address() as { port: number };
|
|
27
|
+
url = `http://127.0.0.1:${addr.port}`;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterAll(async () => {
|
|
31
|
+
await new Promise<void>((resolve, reject) =>
|
|
32
|
+
server.close((err) => (err ? reject(err) : resolve())),
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
async function req(
|
|
37
|
+
path: string,
|
|
38
|
+
init: RequestInit & { user?: string } = {},
|
|
39
|
+
): Promise<{ status: number; body: unknown }> {
|
|
40
|
+
const headers: Record<string, string> = {
|
|
41
|
+
"content-type": "application/json",
|
|
42
|
+
...((init.headers as Record<string, string>) ?? {}),
|
|
43
|
+
};
|
|
44
|
+
if (init.user) headers["x-user-id"] = init.user;
|
|
45
|
+
const res = await fetch(`${url}${path}`, { ...init, headers });
|
|
46
|
+
const text = await res.text();
|
|
47
|
+
let body: unknown = text;
|
|
48
|
+
try {
|
|
49
|
+
body = JSON.parse(text);
|
|
50
|
+
} catch {
|
|
51
|
+
/* keep as text */
|
|
52
|
+
}
|
|
53
|
+
return { status: res.status, body };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("todo api", () => {
|
|
57
|
+
it("requires x-user-id (requireUser middleware)", async () => {
|
|
58
|
+
const r = await req("/api/todos", { method: "GET" });
|
|
59
|
+
expect(r.status).toBe(401);
|
|
60
|
+
expect((r.body as { error: { code: string } }).error.code).toBe("MISSING_USER_ID");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("creates, lists, completes, deletes", async () => {
|
|
64
|
+
// create
|
|
65
|
+
const created = await req("/api/todos", {
|
|
66
|
+
method: "POST",
|
|
67
|
+
body: JSON.stringify({ text: "buy milk" }),
|
|
68
|
+
user: "alice",
|
|
69
|
+
});
|
|
70
|
+
expect(created.status).toBe(201);
|
|
71
|
+
const todo = created.body as { id: string; text: string; status: string };
|
|
72
|
+
expect(todo.text).toBe("buy milk");
|
|
73
|
+
expect(todo.status).toBe("open");
|
|
74
|
+
|
|
75
|
+
// list
|
|
76
|
+
const listed = await req("/api/todos", { method: "GET", user: "alice" });
|
|
77
|
+
expect(listed.status).toBe(200);
|
|
78
|
+
const list = listed.body as { items: Array<{ id: string }>; total: number };
|
|
79
|
+
expect(list.total).toBe(1);
|
|
80
|
+
expect(list.items[0]!.id).toBe(todo.id);
|
|
81
|
+
|
|
82
|
+
// complete
|
|
83
|
+
const completed = await req(`/api/todos/${todo.id}/complete`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
user: "alice",
|
|
86
|
+
});
|
|
87
|
+
expect(completed.status).toBe(200);
|
|
88
|
+
expect((completed.body as { status: string }).status).toBe("completed");
|
|
89
|
+
|
|
90
|
+
// double-complete → 409
|
|
91
|
+
const again = await req(`/api/todos/${todo.id}/complete`, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
user: "alice",
|
|
94
|
+
});
|
|
95
|
+
expect(again.status).toBe(409);
|
|
96
|
+
|
|
97
|
+
// delete
|
|
98
|
+
const removed = await req(`/api/todos/${todo.id}`, {
|
|
99
|
+
method: "DELETE",
|
|
100
|
+
user: "alice",
|
|
101
|
+
});
|
|
102
|
+
expect(removed.status).toBe(204);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("isolates todos per user (alice doesn't see bob's)", async () => {
|
|
106
|
+
await req("/api/todos", {
|
|
107
|
+
method: "POST",
|
|
108
|
+
body: JSON.stringify({ text: "bob's task" }),
|
|
109
|
+
user: "bob",
|
|
110
|
+
});
|
|
111
|
+
const aliceList = await req("/api/todos", { method: "GET", user: "alice" });
|
|
112
|
+
const items = (aliceList.body as { items: Array<{ text: string }> }).items;
|
|
113
|
+
expect(items.find((i) => i.text === "bob's task")).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
});
|