express-generator-typescript 2.9.1 → 3.0.2
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/README.md +46 -40
- package/lib/template/README.md +33 -15
- package/lib/template/package.json +11 -21
- package/lib/template/src/common/{utils → classes}/route-errors.ts +1 -1
- package/lib/template/src/common/constants/HttpStatusCodes.ts +1 -1
- package/lib/template/src/common/constants/env-inv.ts +2 -1
- package/lib/template/src/common/utils/dev-only.ts +25 -0
- package/lib/template/src/entities/User/_internal/validators.ts +47 -0
- package/lib/template/src/entities/User/index.ts +2 -0
- package/lib/template/src/entities/User/module.ts +63 -0
- package/lib/template/src/entities/User/types.ts +18 -0
- package/lib/template/src/{models → entities}/common/types.ts +1 -1
- package/lib/template/src/public/scripts/HttpClient.js +59 -0
- package/lib/template/src/public/scripts/renderUsers.js +43 -5
- package/lib/template/src/public/scripts/users.js +75 -14
- package/lib/template/src/repos/MockOrm.ts +3 -3
- package/lib/template/src/repos/UserRepo.ts +6 -6
- package/lib/template/src/repos/common/database.json +2 -2
- package/lib/template/src/routes/UserRoutes.ts +2 -2
- package/lib/template/src/routes/common/express-types.ts +1 -1
- package/lib/template/src/routes/common/parseReq.ts +2 -2
- package/lib/template/src/server.ts +16 -4
- package/lib/template/src/services/UserService.ts +6 -6
- package/lib/template/src/views/users.html +1 -1
- package/lib/template/tests/common/comparators.ts +3 -3
- package/lib/template/tests/common/supertest-types.ts +2 -2
- package/lib/template/tests/frontend.test.ts +1 -1
- package/lib/template/tests/support/agent.ts +2 -2
- package/lib/template/tests/users.test.ts +11 -11
- package/lib/template/tsconfig.json +9 -10
- package/lib/template/tsconfig.prod.json +13 -4
- package/lib/template/{vitest.config.mts → vitest.config.ts} +6 -4
- package/package.json +1 -1
- package/lib/template/bs-config.js +0 -8
- package/lib/template/src/models/User.model.ts +0 -95
- package/lib/template/src/public/scripts/http.js +0 -25
package/README.md
CHANGED
|
@@ -8,23 +8,23 @@
|
|
|
8
8
|
[](https://www.npmjs.com/package/express-generator-typescript)
|
|
9
9
|
[](https://github.com/seanpmaxwell/express-generator-typescript/blob/main/LICENSE)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
A command-line tool that generates production-ready Express projects with TypeScript built in. Spin up a web server in seconds that follows the [TypeScript best practices](https://github.com/seanpmaxwell/Typescript-Best-Practices).
|
|
12
12
|
|
|
13
13
|
<p align="center">· · ·</p>
|
|
14
14
|
|
|
15
15
|
## 🧭 Overview
|
|
16
16
|
|
|
17
|
-
`express-generator-typescript`
|
|
17
|
+
`express-generator-typescript` works like the classic `express-generator` package, but the project it creates is fully set up for TypeScript. You get strict typing, linting, hot reloading, testing, and production builds, with defaults aimed at APIs. The project is an ES module and comes with an `@src/*` import alias, so imports stay clean as the app grows.
|
|
18
18
|
|
|
19
19
|
<p align="center">· · ·</p>
|
|
20
20
|
|
|
21
21
|
## ✨ Features
|
|
22
22
|
|
|
23
|
-
- **TypeScript-first** – strict compiler settings, linting, and sensible tsconfig defaults ready to go.
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
26
|
-
- **Path aliases** – `@src/*`
|
|
27
|
-
- **
|
|
23
|
+
- **TypeScript-first** – strict compiler settings, linting, and sensible tsconfig defaults, ready to go.
|
|
24
|
+
- **Built for APIs** – ideal for SPAs, mobile backends, or services.
|
|
25
|
+
- **Fast development** – runs TypeScript directly with tsx (no build step), restarts the server when you change it, and refreshes the browser when you change front-end files. Vitest, ESLint, and production builds are included.
|
|
26
|
+
- **Path aliases** – import from `@src/*` anywhere. It works in development, tests, and production builds.
|
|
27
|
+
- **Lean dependencies** – no view engine, ORM, or UI layer; only the essentials for Express + TypeScript.
|
|
28
28
|
|
|
29
29
|
<p align="center">· · ·</p>
|
|
30
30
|
|
|
@@ -52,65 +52,71 @@ cd my-api
|
|
|
52
52
|
npm run dev
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
Use `--use-yarn` if you prefer Yarn over npm. If you omit the project name, the generator creates `express-gen-ts`.
|
|
56
|
-
|
|
57
55
|
<p align="center">· · ·</p>
|
|
58
56
|
|
|
59
57
|
## 🖥️ CLI Options
|
|
60
58
|
|
|
61
|
-
| Option | Description
|
|
62
|
-
| ----------------- |
|
|
63
|
-
| `project name` | Folder to create. Defaults to `express-gen-ts
|
|
64
|
-
| `--use-yarn` |
|
|
65
|
-
| `--force` |
|
|
66
|
-
| `-h`, `--help` |
|
|
67
|
-
| `-v`, `--version` |
|
|
59
|
+
| Option | Description |
|
|
60
|
+
| ----------------- | ------------------------------------------------------------------ |
|
|
61
|
+
| `project name` | Folder to create. Defaults to `express-gen-ts`. |
|
|
62
|
+
| `--use-yarn` | Install dependencies with Yarn instead of npm. |
|
|
63
|
+
| `--force` | Write into a folder that isn't empty. Files with the same name are overwritten. |
|
|
64
|
+
| `-h`, `--help` | Show usage. |
|
|
65
|
+
| `-v`, `--version` | Show the generator version. |
|
|
68
66
|
|
|
69
|
-
>
|
|
67
|
+
> Without `--force`, the generator won't write into a folder that already has files in it, so it can't overwrite your work by accident.
|
|
70
68
|
|
|
71
69
|
<p align="center">· · ·</p>
|
|
72
70
|
|
|
73
71
|
## 🧩 Generated Template
|
|
74
72
|
|
|
75
|
-
The generated
|
|
73
|
+
The generated project is a small CRUD app for a `User` record. It shows how to structure models, services, and routes in Express + TypeScript. Linting, formatting, building, and hot reloading are all set up for you.
|
|
76
74
|
|
|
77
75
|
### Available `package.json` Scripts
|
|
78
76
|
|
|
79
|
-
- `npm run dev` – Run the server in
|
|
80
|
-
- `npm
|
|
81
|
-
- `npm
|
|
82
|
-
- `npm run lint` –
|
|
83
|
-
- `npm run format`
|
|
84
|
-
- `npm run build` –
|
|
85
|
-
- `npm start` –
|
|
86
|
-
- `npm run typecheck` –
|
|
77
|
+
- `npm run dev` – Run the server in development with live reload and browser refresh.
|
|
78
|
+
- `npm test` – Run the tests with Vitest.
|
|
79
|
+
- `npm test -- users.test.ts` – Run a single test file.
|
|
80
|
+
- `npm run lint` – Check the code with ESLint.
|
|
81
|
+
- `npm run format` – Format the code with Prettier.
|
|
82
|
+
- `npm run build` – Build the project for production.
|
|
83
|
+
- `npm start` – Run the production build.
|
|
84
|
+
- `npm run typecheck` – Check for TypeScript errors without building.
|
|
87
85
|
- `npm run install:clean` – Delete `node_modules` and the lockfile, then reinstall.
|
|
88
86
|
|
|
89
87
|
### Architecture
|
|
90
88
|
|
|
91
|
-
|
|
89
|
+
The app uses a **layered** architecture, which suits a small CRUD app. If you plan to grow it, consider switching to a **domain-based** layout. The [Typescript Best Practices README](https://github.com/seanpmaxwell/Typescript-Best-Practices/tree/main?tab=readme-ov-file#architecture) explains both patterns.
|
|
92
90
|
|
|
93
91
|
Layers explained:
|
|
94
|
-
```
|
|
95
|
-
-
|
|
96
|
-
|
|
92
|
+
```md
|
|
93
|
+
- config/ → .env files for each environment
|
|
94
|
+
- src/ → Source code
|
|
95
|
+
- common/ → Shared code
|
|
96
|
+
- classes/ → Shared classes (e.g. route errors)
|
|
97
97
|
- constants/
|
|
98
|
-
- Paths.ts
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
98
|
+
- Paths.ts → Single source of truth for all API routes
|
|
99
|
+
- types/ → Shared types
|
|
100
|
+
- utils/ → Shared helper functions
|
|
101
|
+
- entities/ → Describe and handle database records (one folder per entity)
|
|
102
|
+
- routes/ → Read and validate values from Express requests; send responses
|
|
103
|
+
- services/ → Business logic (where everything comes together)
|
|
104
|
+
- repos/ → Talk to the database
|
|
105
|
+
- public/ → Front-end scripts and styles
|
|
106
|
+
- views/ → HTML pages
|
|
107
|
+
- main.ts → Starts the server
|
|
108
|
+
- server.ts → Sets up the Express app
|
|
109
|
+
- tests/ → Tests
|
|
104
110
|
```
|
|
105
111
|
|
|
106
112
|
<p align="center">· · ·</p>
|
|
107
113
|
|
|
108
|
-
## Notes for
|
|
114
|
+
## Notes for VS Code users
|
|
109
115
|
|
|
110
116
|
<details>
|
|
111
117
|
<summary>Format on save</summary>
|
|
112
118
|
|
|
113
|
-
The generated
|
|
119
|
+
The generated project uses ESLint for linting and Prettier for formatting. To format on save, install the Prettier extension for VS Code and set it as the default formatter in `.vscode/settings.json`:
|
|
114
120
|
|
|
115
121
|
```json
|
|
116
122
|
// .vscode/settings.json
|
|
@@ -160,7 +166,7 @@ The generated template uses `eslint`+`prettier`, so if you want features like _f
|
|
|
160
166
|
<details>
|
|
161
167
|
<summary>Debugging</summary>
|
|
162
168
|
|
|
163
|
-
|
|
169
|
+
To debug with breakpoints in VS Code, start the app or tests from `.vscode/launch.json`:
|
|
164
170
|
|
|
165
171
|
```json
|
|
166
172
|
// .vscode/launch.json
|
|
@@ -168,7 +174,7 @@ If you want to debug in VSCode with breakpoints you need to start the processes
|
|
|
168
174
|
"version": "0.2.0",
|
|
169
175
|
"configurations": [
|
|
170
176
|
{
|
|
171
|
-
"name": "Dev
|
|
177
|
+
"name": "Dev",
|
|
172
178
|
"type": "node",
|
|
173
179
|
"request": "launch",
|
|
174
180
|
"runtimeExecutable": "npm",
|
package/lib/template/README.md
CHANGED
|
@@ -2,32 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
This project was created with [express-generator-typescript](https://github.com/seanpmaxwell/express-generator-typescript). It requires Node.js 22.12 or newer.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The original template follows the [TypeScript best practices](https://github.com/seanpmaxwell/Typescript-Best-Practices).
|
|
6
6
|
|
|
7
|
+
<p align="center">· · ·</p>
|
|
7
8
|
|
|
8
9
|
## Available Scripts
|
|
9
10
|
|
|
10
|
-
### `npm run
|
|
11
|
-
|
|
12
|
-
Remove the existing `node_modules/` folder, `package-lock.json`, and reinstall all library modules.
|
|
13
|
-
|
|
14
|
-
### `npm run dev`
|
|
11
|
+
### `npm run dev`
|
|
15
12
|
|
|
16
|
-
Run the server in development
|
|
13
|
+
Run the server in development at http://localhost:3000. The server restarts when you change server code, and the browser refreshes when you change server code or anything in `src/public` or `src/views`.
|
|
17
14
|
|
|
18
|
-
**
|
|
15
|
+
> **Note:** development mode runs your `.ts` files directly with `tsx`, which doesn't check for TypeScript errors. Run `npm run typecheck` to check for them, and let your editor catch most of them as you work.
|
|
19
16
|
|
|
20
17
|
### `npm test`
|
|
21
18
|
|
|
22
|
-
Run
|
|
19
|
+
Run the tests with [Vitest](https://vitest.dev/guide/).
|
|
23
20
|
|
|
24
21
|
### `npm run lint`
|
|
25
22
|
|
|
26
|
-
Check
|
|
23
|
+
Check the code with ESLint.
|
|
27
24
|
|
|
28
25
|
### `npm run format`
|
|
29
26
|
|
|
30
|
-
Format `src/` and `tests/` with
|
|
27
|
+
Format `src/` and `tests/` with Prettier.
|
|
31
28
|
|
|
32
29
|
### `npm run build`
|
|
33
30
|
|
|
@@ -35,16 +32,37 @@ Build the project for production.
|
|
|
35
32
|
|
|
36
33
|
### `npm start`
|
|
37
34
|
|
|
38
|
-
Run the production build
|
|
35
|
+
Run the production build. Run `npm run build` first.
|
|
39
36
|
|
|
40
37
|
### `npm run typecheck`
|
|
41
38
|
|
|
42
|
-
Check for
|
|
39
|
+
Check for TypeScript errors without building.
|
|
40
|
+
|
|
41
|
+
### `npm run install:clean`
|
|
42
|
+
|
|
43
|
+
Delete `node_modules/` and `package-lock.json`, then reinstall all dependencies.
|
|
43
44
|
|
|
44
45
|
<p align="center">· · ·</p>
|
|
45
46
|
|
|
47
|
+
## Tech Stack
|
|
48
|
+
|
|
49
|
+
- **Language**: [TypeScript](https://www.typescriptlang.org/) (strict mode, ES modules)
|
|
50
|
+
- **Web server framework**: [Express](https://expressjs.com/en/) (v5)
|
|
51
|
+
- **Security headers**: [helmet](https://helmet.js.org/) (production only)
|
|
52
|
+
- **Logging**
|
|
53
|
+
- **Request logging**: [morgan](https://github.com/expressjs/morgan) (development only)
|
|
54
|
+
- **General logging**: [jet-logger](https://github.com/seanpmaxwell/jet-logger)
|
|
55
|
+
- **Validation**: [jet-validators](https://github.com/seanpmaxwell/jet-validators)
|
|
56
|
+
- **Environment variables**: [dotenv](https://github.com/motdotla/dotenv) loads `config/.env.*`, and [jet-env](https://github.com/seanpmaxwell/jet-env) validates them
|
|
57
|
+
- **Reloading**: [tsx](https://tsx.hirok.io) (`tsx watch` restarts the server) and [livereload](https://github.com/napcs/node-livereload) + [connect-livereload](https://github.com/intesso/connect-livereload) (refreshes the browser)
|
|
58
|
+
- **Testing**: [Vitest](https://vitest.dev) + [Supertest](https://github.com/ladjs/supertest)
|
|
59
|
+
- **Linting**: [ESLint](https://eslint.org) with [typescript-eslint](https://typescript-eslint.io/packages/typescript-eslint) and [eslint-plugin-n](https://github.com/eslint-community/eslint-plugin-n)
|
|
60
|
+
- **Formatting**: [Prettier](https://prettier.io) with [@trivago/prettier-plugin-sort-imports](https://github.com/trivago/prettier-plugin-sort-imports)
|
|
61
|
+
- **Building**: `tsc` + [tsc-alias](https://github.com/justkey007/tsc-alias) (rewrites `@src/*` imports in `dist/`)
|
|
62
|
+
|
|
63
|
+
<p align="center">· · ·</p>
|
|
46
64
|
|
|
47
65
|
## Additional Notes
|
|
48
66
|
|
|
49
|
-
- `config/.env.production` is
|
|
50
|
-
- The database is a JSON file
|
|
67
|
+
- `config/.env.production` is in `.gitignore` so production secrets don't get committed. Keep it out of version control and supply its values through your deployment tooling.
|
|
68
|
+
- The database is a JSON file meant only for the demo: `src/repos/common/database.json` in development, or `dist/repos/common/database.json` in production. It's created automatically if missing, but it isn't safe for simultaneous writes. Replace `src/repos/MockOrm.ts` with a real database before going to production.
|
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "express-typescript-example",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"scripts": {
|
|
5
|
-
"build": "shx rm -rf dist && npm run lint && tsc --project tsconfig.prod.json && npm run build:copy-static",
|
|
6
|
+
"build": "shx rm -rf dist && npm run lint && tsc --project tsconfig.prod.json && tsc-alias -p tsconfig.prod.json && npm run build:copy-static",
|
|
6
7
|
"build:copy-static": "shx mkdir -p dist/public dist/views && shx cp -r src/public/* dist/public && shx cp -r src/views/* dist/views",
|
|
7
8
|
"install:clean": "shx rm -rf ./node_modules package-lock.json && npm i",
|
|
8
|
-
"dev
|
|
9
|
-
"dev:watch": "nodemon --exec \"npm run dev:basic\" --watch ./src --ext .ts",
|
|
10
|
-
"dev": "concurrently \"npm run dev:watch\" \"npm run sync\"",
|
|
9
|
+
"dev": "cross-env DOTENV_CONFIG_PATH=./config/.env.development tsx watch --import dotenv/config ./src/main.ts",
|
|
11
10
|
"lint": "eslint .",
|
|
12
11
|
"format": "prettier --write .",
|
|
13
|
-
"start": "cross-env DOTENV_CONFIG_PATH=./config/.env.production node
|
|
14
|
-
"sync": "delay 1s && browser-sync start --config bs-config.js",
|
|
12
|
+
"start": "cross-env DOTENV_CONFIG_PATH=./config/.env.production node --import dotenv/config ./dist/main.js",
|
|
15
13
|
"test": "cross-env NODE_ENV=test vitest",
|
|
16
14
|
"typecheck": "tsc -b --noEmit"
|
|
17
15
|
},
|
|
18
16
|
"engines": {
|
|
19
17
|
"node": ">=22.12.0"
|
|
20
18
|
},
|
|
21
|
-
"_moduleAliases": {
|
|
22
|
-
"@src": "dist"
|
|
23
|
-
},
|
|
24
19
|
"dependencies": {
|
|
25
20
|
"cross-env": "^10.1.0",
|
|
26
21
|
"dotenv": "^18.0.4",
|
|
@@ -32,36 +27,31 @@
|
|
|
32
27
|
"jet-paths": "^4.0.2",
|
|
33
28
|
"jet-validators": "^2.3.1",
|
|
34
29
|
"jsonfile": "^6.2.1",
|
|
35
|
-
"module-alias": "^2.3.4",
|
|
36
30
|
"morgan": "^1.12.1"
|
|
37
31
|
},
|
|
38
32
|
"devDependencies": {
|
|
39
33
|
"@eslint/js": "^10.0.1",
|
|
40
|
-
"@swc/core": "^1.16.2",
|
|
41
34
|
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
35
|
+
"@types/connect-livereload": "^0.6.3",
|
|
42
36
|
"@types/express": "^5.0.6",
|
|
43
37
|
"@types/jsonfile": "^6.1.4",
|
|
38
|
+
"@types/livereload": "^0.9.5",
|
|
44
39
|
"@types/morgan": "^1.9.10",
|
|
45
40
|
"@types/node": "^26.6.2",
|
|
46
41
|
"@types/supertest": "^7.2.1",
|
|
47
|
-
"
|
|
48
|
-
"concurrently": "^10.0.5",
|
|
49
|
-
"delay-cli": "^3.0.0",
|
|
42
|
+
"connect-livereload": "^0.6.1",
|
|
50
43
|
"eslint": "^10.11.0",
|
|
51
44
|
"eslint-config-prettier": "^10.1.8",
|
|
52
45
|
"eslint-plugin-n": "^18.3.0",
|
|
53
46
|
"jiti": "^2.7.0",
|
|
54
|
-
"
|
|
47
|
+
"livereload": "^0.10.3",
|
|
55
48
|
"prettier": "^3.9.9",
|
|
56
49
|
"shx": "^0.4.0",
|
|
57
50
|
"supertest": "^7.3.0",
|
|
58
|
-
"
|
|
59
|
-
"
|
|
51
|
+
"tsc-alias": "^1.9.5",
|
|
52
|
+
"tsx": "^4.23.15",
|
|
60
53
|
"typescript": "^6.0.3",
|
|
61
54
|
"typescript-eslint": "^8.70.1",
|
|
62
55
|
"vitest": "^5.0.2"
|
|
63
|
-
},
|
|
64
|
-
"allowScripts": {
|
|
65
|
-
"@swc/core@1.16.2": true
|
|
66
56
|
}
|
|
67
57
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import connectLiveReload from 'connect-livereload';
|
|
2
|
+
import type { Express } from 'express';
|
|
3
|
+
import livereload from 'livereload';
|
|
4
|
+
|
|
5
|
+
// ========================================================================= //
|
|
6
|
+
// FUNCTIONS //
|
|
7
|
+
// ========================================================================= //
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Development only: refresh the browser when front-end files change, and
|
|
11
|
+
* after `tsx watch` restarts the server for a TypeScript change.
|
|
12
|
+
*
|
|
13
|
+
* Must be registered before the routes that serve html so the reload script
|
|
14
|
+
* gets injected into those pages.
|
|
15
|
+
*/
|
|
16
|
+
export function setupLiveReload(app: Express, watchDirs: string[]): void {
|
|
17
|
+
const server = livereload.createServer();
|
|
18
|
+
server.watch(watchDirs);
|
|
19
|
+
// A new process means the backend just restarted; reload once the browser
|
|
20
|
+
// reconnects so it picks up the new server code.
|
|
21
|
+
server.server.once('connection', () => {
|
|
22
|
+
setTimeout(() => server.refresh('/'), 100);
|
|
23
|
+
});
|
|
24
|
+
app.use(connectLiveReload());
|
|
25
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import jetid from 'jet-id';
|
|
2
|
+
import { isNonEmptyString, isString } from 'jet-validators';
|
|
3
|
+
import { parseObject, type Schema, testObject } from 'jet-validators/utils';
|
|
4
|
+
|
|
5
|
+
import { isISOString } from '@src/common/utils/date-utils';
|
|
6
|
+
|
|
7
|
+
import type { UserEntity, UserInput } from '../types';
|
|
8
|
+
|
|
9
|
+
// ========================================================================= //
|
|
10
|
+
// FUNCTIONS //
|
|
11
|
+
// ========================================================================= //
|
|
12
|
+
|
|
13
|
+
const schema: Schema<UserEntity> = {
|
|
14
|
+
id: isUserId,
|
|
15
|
+
name: isString,
|
|
16
|
+
email: isString,
|
|
17
|
+
created: isISOString,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate the `User` schema.
|
|
22
|
+
*/
|
|
23
|
+
export const parseUser = parseObject<UserEntity>(schema);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* For the APIs make sure the right fields are complete
|
|
27
|
+
*/
|
|
28
|
+
export const isCompleteUser = testObject<UserEntity>({
|
|
29
|
+
...schema,
|
|
30
|
+
name: isNonEmptyString,
|
|
31
|
+
email: isNonEmptyString,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate the fields a client sends to create a user.
|
|
36
|
+
*/
|
|
37
|
+
export const isUserInput = testObject<UserInput>({
|
|
38
|
+
name: isNonEmptyString,
|
|
39
|
+
email: isNonEmptyString,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Test if an id is a valid user id.
|
|
44
|
+
*/
|
|
45
|
+
export function isUserId(val: unknown): val is string {
|
|
46
|
+
return jetid.test(val);
|
|
47
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import jetid from 'jet-id';
|
|
2
|
+
|
|
3
|
+
import { getISOString, type ISOString } from '@src/common/utils/date-utils';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
isCompleteUser,
|
|
7
|
+
isUserId,
|
|
8
|
+
isUserInput,
|
|
9
|
+
parseUser,
|
|
10
|
+
} from './_internal/validators';
|
|
11
|
+
import type { UserEntity } from './types';
|
|
12
|
+
|
|
13
|
+
// ========================================================================= //
|
|
14
|
+
// FUNCTIONS //
|
|
15
|
+
// ========================================================================= //
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get a new `UserEntity` object with default values.
|
|
19
|
+
*/
|
|
20
|
+
function getDefaults(): UserEntity {
|
|
21
|
+
return {
|
|
22
|
+
id: jetid(),
|
|
23
|
+
name: '',
|
|
24
|
+
email: '',
|
|
25
|
+
created: getISOString(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Factory-function.
|
|
31
|
+
*
|
|
32
|
+
* Create a `UserEntity` from a partial or `undefined`
|
|
33
|
+
*/
|
|
34
|
+
function create(user?: Partial<UserEntity>): UserEntity {
|
|
35
|
+
return parseUser({ ...getDefaults(), ...user }, (errors) => {
|
|
36
|
+
throw new Error('Setup new user failed ' + JSON.stringify(errors, null, 2));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Factory-function.
|
|
42
|
+
*
|
|
43
|
+
* Create a `UserEntity` from individual properties
|
|
44
|
+
*/
|
|
45
|
+
function of(name: string, email?: string, created?: ISOString): UserEntity {
|
|
46
|
+
const retVal = getDefaults();
|
|
47
|
+
if (name) retVal.name = name;
|
|
48
|
+
if (email) retVal.email = email;
|
|
49
|
+
if (created) retVal.created = created;
|
|
50
|
+
return retVal;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ========================================================================= //
|
|
54
|
+
// EXPORT //
|
|
55
|
+
// ========================================================================= //
|
|
56
|
+
|
|
57
|
+
export default {
|
|
58
|
+
of,
|
|
59
|
+
create,
|
|
60
|
+
isId: isUserId,
|
|
61
|
+
isComplete: isCompleteUser,
|
|
62
|
+
isInput: isUserInput,
|
|
63
|
+
} as const;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Entity } from '@src/entities/common/types';
|
|
2
|
+
|
|
3
|
+
// ========================================================================= //
|
|
4
|
+
// TYPES //
|
|
5
|
+
// ========================================================================= //
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @entity `users`
|
|
9
|
+
*/
|
|
10
|
+
export interface UserEntity extends Entity {
|
|
11
|
+
name: string;
|
|
12
|
+
email: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Fields a client supplies when creating a user; the server sets the rest.
|
|
17
|
+
*/
|
|
18
|
+
export type UserInput = Pick<UserEntity, 'name' | 'email'>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// ========================================================================= //
|
|
2
|
+
// TYPES //
|
|
3
|
+
// ========================================================================= //
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Thin wrapper around `fetch` for calling the JSON API. Each method sends and
|
|
7
|
+
* accepts JSON and resolves with the raw `Response`.
|
|
8
|
+
*
|
|
9
|
+
* @typedef {object} HttpClient
|
|
10
|
+
* @property {(path: string) => Promise<Response>} get
|
|
11
|
+
* @property {(path: string, data: unknown) => Promise<Response>} post - `data`
|
|
12
|
+
* is sent as the JSON request body.
|
|
13
|
+
* @property {(path: string, data: unknown) => Promise<Response>} put - `data`
|
|
14
|
+
* is sent as the JSON request body.
|
|
15
|
+
* @property {(path: string) => Promise<Response>} delete
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ========================================================================= //
|
|
19
|
+
// EXEC //
|
|
20
|
+
// ========================================================================= //
|
|
21
|
+
|
|
22
|
+
/** @type {HttpClient} */
|
|
23
|
+
var HttpClient = (() => {
|
|
24
|
+
/**
|
|
25
|
+
* Build `fetch` options for a JSON request.
|
|
26
|
+
*
|
|
27
|
+
* Used by:
|
|
28
|
+
* {@link HttpClient.get}
|
|
29
|
+
* {@link HttpClient.post}
|
|
30
|
+
* {@link HttpClient.put}
|
|
31
|
+
* {@link HttpClient.delete}
|
|
32
|
+
*
|
|
33
|
+
* @param {string} verb - HTTP method, e.g. "GET".
|
|
34
|
+
* @param {unknown} [data] - Request body; serialized to JSON when present.
|
|
35
|
+
* @returns {RequestInit}
|
|
36
|
+
*/
|
|
37
|
+
var getOptions = (verb, data) => {
|
|
38
|
+
var options = {
|
|
39
|
+
dataType: 'json',
|
|
40
|
+
method: verb,
|
|
41
|
+
headers: {
|
|
42
|
+
Accept: 'application/json',
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
if (!!data) {
|
|
47
|
+
options.body = JSON.stringify(data);
|
|
48
|
+
}
|
|
49
|
+
return options;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Set HttpClient methods
|
|
53
|
+
return {
|
|
54
|
+
get: (path) => fetch(path, getOptions('GET')),
|
|
55
|
+
post: (path, data) => fetch(path, getOptions('POST', data)),
|
|
56
|
+
put: (path, data) => fetch(path, getOptions('PUT', data)),
|
|
57
|
+
delete: (path) => fetch(path, getOptions('DELETE')),
|
|
58
|
+
};
|
|
59
|
+
})();
|
|
@@ -8,8 +8,6 @@ const DateFormatter = new Intl.DateTimeFormat('en-US', {
|
|
|
8
8
|
day: '2-digit',
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
const formatDate = (date) => DateFormatter.format(new Date(date));
|
|
12
|
-
|
|
13
11
|
const HTML_ESCAPES = {
|
|
14
12
|
'&': '&',
|
|
15
13
|
'<': '<',
|
|
@@ -18,15 +16,29 @@ const HTML_ESCAPES = {
|
|
|
18
16
|
"'": ''',
|
|
19
17
|
};
|
|
20
18
|
|
|
21
|
-
//
|
|
22
|
-
|
|
19
|
+
// ========================================================================= //
|
|
20
|
+
// TYPES //
|
|
21
|
+
// ========================================================================= //
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A user record as returned by the API.
|
|
25
|
+
*
|
|
26
|
+
* @typedef {object} User
|
|
27
|
+
* @property {string} id
|
|
28
|
+
* @property {string} name
|
|
29
|
+
* @property {string} email
|
|
30
|
+
* @property {string} created - ISO 8601 date string.
|
|
31
|
+
*/
|
|
23
32
|
|
|
24
33
|
// ========================================================================= //
|
|
25
34
|
// FUNCTIONS //
|
|
26
35
|
// ========================================================================= //
|
|
27
36
|
|
|
28
37
|
/**
|
|
29
|
-
*
|
|
38
|
+
* Build the HTML for the users list, including each user's edit form.
|
|
39
|
+
*
|
|
40
|
+
* @param {User[]} users
|
|
41
|
+
* @returns {string} HTML to assign to `innerHTML`.
|
|
30
42
|
*/
|
|
31
43
|
function renderUsers(users) {
|
|
32
44
|
return users
|
|
@@ -88,3 +100,29 @@ function renderUsers(users) {
|
|
|
88
100
|
})
|
|
89
101
|
.join('');
|
|
90
102
|
}
|
|
103
|
+
|
|
104
|
+
// ================================ Helpers ================================ //
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Format a date as MM/DD/YYYY.
|
|
108
|
+
*
|
|
109
|
+
* Used by: {@link renderUsers}
|
|
110
|
+
*
|
|
111
|
+
* @param {string | number | Date} date - Anything `new Date()` accepts.
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
function formatDate(date) {
|
|
115
|
+
return DateFormatter.format(new Date(date));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* User data is untrusted, so escape it before it goes into innerHTML.
|
|
120
|
+
*
|
|
121
|
+
* Used by: {@link renderUsers}
|
|
122
|
+
*
|
|
123
|
+
* @param {unknown} val - Converted to a string before escaping.
|
|
124
|
+
* @returns {string}
|
|
125
|
+
*/
|
|
126
|
+
function esc(val) {
|
|
127
|
+
return String(val).replace(/[&<>"']/g, (ch) => HTML_ESCAPES[ch]);
|
|
128
|
+
}
|