@ix-xs/node-comfort 1.2.0 → 2.0.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/CHANGELOG.md +190 -0
- package/README.md +136 -549
- package/index.js +80 -35
- package/package.json +160 -33
- package/src/Arr.js +887 -419
- package/src/Async.js +585 -0
- package/src/Cache.js +329 -0
- package/src/Checker.js +971 -539
- package/src/Cli.js +1034 -0
- package/src/Color.js +415 -0
- package/src/Crypto.js +707 -0
- package/src/Emitter.js +289 -0
- package/src/Env.js +555 -0
- package/src/FS.js +1483 -894
- package/src/Func.js +670 -353
- package/src/Http.js +538 -0
- package/src/Id.js +384 -201
- package/src/Logger.js +739 -378
- package/src/Num.js +844 -361
- package/src/Obj.js +855 -372
- package/src/SQLite.js +1308 -1005
- package/src/Schema.js +1586 -0
- package/src/Str.js +983 -436
- package/src/Sys.js +511 -0
- package/src/Time.js +1260 -422
- package/src/Utils.js +321 -325
- package/src/errors.js +292 -0
- package/types/index.d.ts +693 -526
- package/types/src/Arr.d.ts +455 -145
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +321 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +195 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +501 -235
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +582 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +294 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +534 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +170 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +382 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +686 -238
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +380 -135
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +374 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +149 -68
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +363 -46
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +473 -137
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +390 -104
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +646 -309
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +940 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +569 -166
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +269 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +663 -204
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +154 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +292 -0
- package/types/src/errors.d.ts.map +1 -0
- package/types/index.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,622 +1,209 @@
|
|
|
1
|
-
# @ix-xs/node-comfort
|
|
2
|
-
|
|
3
1
|
<div align="center">
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-

|
|
7
|
-

|
|
8
|
-

|
|
9
|
-
|
|
10
|
-
[](https://www.npmjs.com/package/@ix-xs/node-comfort)
|
|
11
|
-
[](https://www.npmjs.com/package/@ix-xs/node-comfort)
|
|
12
|
-
[](https://www.npmjs.com/package/@ix-xs/node-comfort)
|
|
13
|
-
|
|
14
|
-
</div>
|
|
3
|
+
# node-comfort
|
|
15
4
|
|
|
16
|
-
|
|
17
|
-
project, in one small, well-typed package:
|
|
18
|
-
|
|
19
|
-
- 🎨 **Logger** • colorful console logging with a tiny markup syntax and levels
|
|
20
|
-
- 📁 **FS** • safe filesystem operations, JSON I/O, watching, hashing
|
|
21
|
-
- 🧪 **Checker** • 35 runtime type guards (`isEmail`, `isEmpty`, `isPlainObject`, …)
|
|
22
|
-
- ⏳ **Utils** • `wait`, `when`, `dontCrash`, JSON helpers
|
|
23
|
-
- 🔤 **str** • case conversion, slugs, truncation, templating
|
|
24
|
-
- 🔢 **num** • clamp, round, ranges, byte/duration/number formatting
|
|
25
|
-
- 📚 **arr** • chunk, groupBy, unique, set operations, shuffle
|
|
26
|
-
- 🧩 **obj** • deep clone/merge/equal, dot-path get/set, pick/omit
|
|
27
|
-
- 🛠️ **func** • debounce, throttle, memoize, retry, timeout, pipe/compose
|
|
28
|
-
- 🕒 **time** • durations, relative time, date math • locale & time-zone aware
|
|
29
|
-
- 🆔 **id** • secure UUID / ULID / nanoid / tokens, hashing, constant-time compare
|
|
30
|
-
- 🗄️ **SQLite** • a small model + CRUD wrapper over Node's built-in `node:sqlite`
|
|
31
|
-
|
|
32
|
-
Everything is plain CommonJS with JSDoc, and the package ships full TypeScript
|
|
33
|
-
type definitions.
|
|
5
|
+
**The standard library Node.js deserves.**
|
|
34
6
|
|
|
35
|
-
|
|
7
|
+
Logging, config, validation, HTTP, SQLite, dates, crypto, CLI tools and 500+ helpers.<br>
|
|
8
|
+
One package, zero dependencies, documented right in your editor.
|
|
36
9
|
|
|
37
|
-
|
|
10
|
+
[](https://www.npmjs.com/package/@ix-xs/node-comfort)
|
|
11
|
+
[](https://www.npmjs.com/package/@ix-xs/node-comfort)
|
|
12
|
+
[](https://github.com/ix-xs/node-comfort/actions/workflows/ci.yml)
|
|
13
|
+
[](https://www.npmjs.com/package/@ix-xs/node-comfort?activeTab=dependencies)
|
|
14
|
+
[](https://nodejs.org)
|
|
15
|
+
[](./LICENSE)
|
|
38
16
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
pnpm add @ix-xs/node-comfort
|
|
45
|
-
```
|
|
17
|
+
[Documentation](https://ix-xs.github.io/node-comfort/) •
|
|
18
|
+
[Getting started](https://ix-xs.github.io/node-comfort/guide/getting-started.html) •
|
|
19
|
+
[API reference](https://ix-xs.github.io/node-comfort/api/index.html) •
|
|
20
|
+
[Recipes](https://ix-xs.github.io/node-comfort/guide/recipes.html) •
|
|
21
|
+
[Changelog](./CHANGELOG.md)
|
|
46
22
|
|
|
47
|
-
|
|
23
|
+
</div>
|
|
48
24
|
|
|
49
25
|
---
|
|
50
26
|
|
|
51
|
-
|
|
27
|
+
Most Node.js projects install the same twenty packages before writing a single line of their own: a logger, dotenv, a validation library, an HTTP client, a retry helper, a date library, uuid, bcrypt, a CLI parser, a spinner. Hundreds of transitive dependencies come with them.
|
|
52
28
|
|
|
53
|
-
|
|
54
|
-
Checker, Utils) and **namespaces** for the newer utility collections.
|
|
29
|
+
Node.js can do most of this on its own now. It ships `fetch`, `Intl`, `node:crypto` and even SQLite. node-comfort is the missing layer on top: one consistent, fully typed API, and nothing else to install.
|
|
55
30
|
|
|
56
31
|
```js
|
|
57
32
|
const nc = require("@ix-xs/node-comfort");
|
|
58
33
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// FS (flat)
|
|
65
|
-
nc.writeJSON("./data/config.json", { ready: true });
|
|
66
|
-
const cfg = nc.readJSON("./data/config.json", {});
|
|
67
|
-
|
|
68
|
-
// Checker (flat)
|
|
69
|
-
nc.isEmail("john@example.com"); // true
|
|
70
|
-
nc.isEmpty([]); // true
|
|
71
|
-
|
|
72
|
-
// Utils (flat)
|
|
73
|
-
await nc.wait(500);
|
|
74
|
-
|
|
75
|
-
// Namespaced collections
|
|
76
|
-
nc.str.slugify("Héllo World"); // "hello-world"
|
|
77
|
-
nc.num.formatBytes(1536); // "1.5 KB"
|
|
78
|
-
nc.arr.chunk([1, 2, 3, 4], 2); // [[1, 2], [3, 4]]
|
|
79
|
-
nc.obj.get(data, "a.b[0].c", "def");
|
|
80
|
-
nc.func.debounce(fn, 200);
|
|
81
|
-
nc.time.relative(Date.now() - 3600e3); // "1 hour ago"
|
|
82
|
-
nc.id.uuid();
|
|
83
|
-
|
|
84
|
-
// SQLite (class)
|
|
85
|
-
const db = new nc.SQLite("data/app.sqlite");
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
> The flat modules are also available as namespaces if you prefer explicit
|
|
89
|
-
> grouping: `nc.logger`, `nc.fs`, `nc.checker`, `nc.utils`.
|
|
90
|
-
|
|
91
|
-
---
|
|
92
|
-
|
|
93
|
-
## API at a glance
|
|
94
|
-
|
|
95
|
-
| Namespace | Highlights |
|
|
96
|
-
| --- | --- |
|
|
97
|
-
| flat / `logger` | `log` `info` `success` `warn` `error` `debug` `group` `groupEnd` `setTimestamp` `setDelimiter` |
|
|
98
|
-
| flat / `fs` | `readFile` `readJSON` `writeJSON` `appendFile` `createFile` `createFolder` `copy*` `move*` `delete*` `getFilesIn` `getFoldersIn` `exists` `stat` `fileSize` `hashFile` `emptyFolder` `watch` `getEnv` |
|
|
99
|
-
| flat / `checker` | `isArray` `isNumber` `isInteger` `isString` `isPlainObject` `isPromise` `isEmpty` `isEmail` `isURL` `isUUID` `isJSON` `isNumeric` … (35 total) |
|
|
100
|
-
| flat / `utils` | `wait` `when` `dontCrash` `JSONString` `JSONParse` |
|
|
101
|
-
| `str` | `capitalize` `camelCase` `snakeCase` `kebabCase` `slugify` `truncate` `template` `escapeHTML` `words` … |
|
|
102
|
-
| `num` | `clamp` `round` `range` `sum` `average` `median` `formatBytes` `abbreviate` `thousands` `ordinal` `percent` … |
|
|
103
|
-
| `arr` | `chunk` `unique` `groupBy` `keyBy` `partition` `sortBy` `difference` `intersection` `union` `zip` `shuffle` `sample` … |
|
|
104
|
-
| `obj` | `clone` `merge` `equal` `get` `set` `has` `pick` `omit` `mapValues` `flatten` `unflatten` `deepFreeze` … |
|
|
105
|
-
| `func` | `debounce` `throttle` `once` `memoize` `retry` `timeout` `pipe` `compose` `curry` `promisify` `attempt` … |
|
|
106
|
-
| `time` | `parseDuration` `formatDuration` `relative` `format` `add` `subtract` `diff` `startOf` `isSameDay` `stopwatch` `setLocale` `setTimezone` … (i18n + time-zone aware) |
|
|
107
|
-
| `id` | `uuid` `ulid` `nano` `token` `code` `hash` `hmac` `safeEqual` `seq` |
|
|
108
|
-
| `SQLite` | `createTable` `insert` `update` `upsert` `get` `getAll` `delete` `count` `transaction` `createIndex` … |
|
|
109
|
-
|
|
110
|
-
---
|
|
111
|
-
|
|
112
|
-
## Logger
|
|
113
|
-
|
|
114
|
-
Colorized console logger with a minimal markup syntax, optional timestamps,
|
|
115
|
-
groups, and level helpers.
|
|
116
|
-
|
|
117
|
-
Wrap styles in delimiters (default `<%` … `%>`):
|
|
118
|
-
|
|
119
|
-
- **styles**: `bold`, `italic`, `underline`, `overline`
|
|
120
|
-
- **colors**: `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `gray`, `black`
|
|
121
|
-
- **bright**: `redBright`, `greenBright`, … `whiteBright`
|
|
122
|
-
- **background**: `bgRed`, `bgBlue`, …
|
|
123
|
-
- **RGB**: `rgb(255, 0, 0)`
|
|
124
|
-
|
|
125
|
-
```js
|
|
126
|
-
nc.log("Plain log");
|
|
127
|
-
nc.log("<% green Success %>");
|
|
128
|
-
nc.log("<% red bold Error:%> something went wrong");
|
|
129
|
-
nc.log("<% bgBlue white INFO %> message");
|
|
130
|
-
nc.log("<% rgb(255,128,0) Orange text %>");
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### Level helpers
|
|
134
|
-
|
|
135
|
-
```js
|
|
136
|
-
nc.info("Loading configuration…"); // ℹ INFO
|
|
137
|
-
nc.success("Database connected"); // ✔ OK
|
|
138
|
-
nc.warn("Cache miss"); // ⚠ WARN
|
|
139
|
-
nc.error(new Error("Boom")); // ✖ ERROR (prints the stack)
|
|
140
|
-
nc.debug({ requestId: 42 }); // ● DEBUG (only when DEBUG/NODE_DEBUG is set)
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
### Timestamp, delimiters & groups
|
|
144
|
-
|
|
145
|
-
```js
|
|
146
|
-
nc.setTimestamp(false);
|
|
147
|
-
nc.setDelimiter({ open: "{{", close: "}}" });
|
|
148
|
-
nc.log("{{red Hello}} world");
|
|
149
|
-
|
|
150
|
-
nc.group("Startup");
|
|
151
|
-
nc.log("Loading config…");
|
|
152
|
-
nc.group("DB");
|
|
153
|
-
nc.log("Connecting…");
|
|
154
|
-
nc.groupEnd(); // end "DB"
|
|
155
|
-
nc.groupEnd(); // end "Startup"
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
---
|
|
159
|
-
|
|
160
|
-
## FS
|
|
161
|
-
|
|
162
|
-
File-system helper with safe path resolution, recursive walking, copy/move
|
|
163
|
-
helpers, JSON I/O, hashing and a small watcher. Relative paths (`./`, `../`) are
|
|
164
|
-
resolved from the **caller file**, which is handy inside libraries.
|
|
165
|
-
|
|
166
|
-
```js
|
|
167
|
-
// Paths
|
|
168
|
-
const p = nc.createPath("./config/app.json"); // absolute, even if missing
|
|
169
|
-
const folder = nc.getFolder("./src");
|
|
170
|
-
const file = nc.getFile("package.json");
|
|
171
|
-
|
|
172
|
-
// Create
|
|
173
|
-
nc.createFolder("./dist");
|
|
174
|
-
nc.createFile("./dist/info.txt", false, "hello");
|
|
175
|
-
|
|
176
|
-
// JSON I/O
|
|
177
|
-
nc.writeJSON("./dist/data.json", { ok: true }); // pretty, 2 spaces
|
|
178
|
-
const data = nc.readJSON("./dist/data.json", {}); // fallback on error
|
|
179
|
-
|
|
180
|
-
// Append / read
|
|
181
|
-
nc.appendFile("./logs/app.log", "started\n");
|
|
182
|
-
const content = nc.readFile("./README.md");
|
|
183
|
-
|
|
184
|
-
// Inspect
|
|
185
|
-
nc.exists("./dist"); // true
|
|
186
|
-
nc.fileSize("./dist/data.json"); // bytes
|
|
187
|
-
nc.stat("./dist/data.json"); // fs.Stats
|
|
188
|
-
nc.hashFile("./dist/data.json"); // sha256 hex
|
|
189
|
-
|
|
190
|
-
// List & delete
|
|
191
|
-
const files = nc.getFilesIn("./src", true);
|
|
192
|
-
nc.deleteFilesIn("./logs", false, (f) => f.endsWith(".log"));
|
|
193
|
-
nc.emptyFolder("./tmp"); // keep the folder, clear its contents
|
|
194
|
-
```
|
|
34
|
+
const config = nc.env.validate({
|
|
35
|
+
PORT: { type: "port", default: 3000 },
|
|
36
|
+
DATABASE_URL: { type: "url" },
|
|
37
|
+
});
|
|
195
38
|
|
|
196
|
-
|
|
39
|
+
const db = new nc.SQLite("./data/app.sqlite");
|
|
40
|
+
const github = nc.http.create({ baseURL: "https://api.github.com/", retry: { attempts: 3 } });
|
|
197
41
|
|
|
198
|
-
|
|
199
|
-
nc.
|
|
200
|
-
nc.copyFilesIn({ path: "./src", dest: "./dist", recursive: true, filter: (f) => f.endsWith(".js") });
|
|
201
|
-
nc.copyFile({ path: "./src/index.js", dest: "./dist", force: true });
|
|
42
|
+
const { data: repo } = await github.get("repos/nodejs/node");
|
|
43
|
+
nc.info(`Node.js has <% yellow ${nc.num.abbreviate(repo.stargazers_count)} %> stars`);
|
|
202
44
|
|
|
203
|
-
nc.
|
|
204
|
-
nc.moveFile({ path: "./logs/app.log", dest: "./logs/archive/app.log", force: true });
|
|
45
|
+
nc.sys.onShutdown(() => db.close());
|
|
205
46
|
```
|
|
206
47
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
```js
|
|
210
|
-
const watcher = nc.watch({ path: "./src", recursive: true, filter: (event, file) => file.endsWith(".js") });
|
|
211
|
-
|
|
212
|
-
watcher
|
|
213
|
-
.on("change", (file) => nc.log(`<% cyan changed %> ${file}`))
|
|
214
|
-
.on("rename", (file) => nc.log(`<% yellow renamed %> ${file}`));
|
|
215
|
-
|
|
216
|
-
watcher.pause();
|
|
217
|
-
watcher.resume();
|
|
218
|
-
watcher.stop();
|
|
219
|
-
```
|
|
48
|
+
## Install
|
|
220
49
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
## Checker
|
|
224
|
-
|
|
225
|
-
35 runtime type guards. Each is a boolean function that accepts any input.
|
|
226
|
-
|
|
227
|
-
```js
|
|
228
|
-
// Primitives & core
|
|
229
|
-
nc.isArray([1, 2]); nc.isNumber(42); nc.isInteger(4);
|
|
230
|
-
nc.isFloat(4.2); nc.isString("x"); nc.isBoolean(false);
|
|
231
|
-
nc.isBigInt(10n); nc.isSymbol(Symbol()); nc.isFunction(() => {});
|
|
232
|
-
nc.isAsyncFunction(async () => {});
|
|
233
|
-
|
|
234
|
-
// Nullish & primitiveness
|
|
235
|
-
nc.isNull(null); nc.isUndefined(undefined); nc.isNil(null); nc.isPrimitive(1);
|
|
236
|
-
|
|
237
|
-
// Objects & structures
|
|
238
|
-
nc.isObject({}); nc.isPlainObject({}); nc.isPromise(Promise.resolve());
|
|
239
|
-
nc.isRegExp(/a/); nc.isDate(new Date()); nc.isValidDate(new Date());
|
|
240
|
-
nc.isMap(new Map()); nc.isSet(new Set()); nc.isIterable([1]);
|
|
241
|
-
nc.isBuffer(Buffer.from("")); nc.isTypedArray(new Uint8Array()); nc.isError(new Error());
|
|
242
|
-
|
|
243
|
-
// Emptiness & formats
|
|
244
|
-
nc.isEmpty(""); nc.isEmpty([]); nc.isEmpty({}); nc.isEmpty(new Map());
|
|
245
|
-
nc.isEmail("john@example.com"); nc.isURL("https://x.com");
|
|
246
|
-
nc.isUUID("123e4567-e89b-12d3-a456-426614174000");
|
|
247
|
-
nc.isJSON('{"a":1}'); nc.isNumeric("42");
|
|
50
|
+
```bash
|
|
51
|
+
npm install @ix-xs/node-comfort
|
|
248
52
|
```
|
|
249
53
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
##
|
|
253
|
-
|
|
254
|
-
|
|
54
|
+
Node.js 22.13 or newer. That's the first version where `node:sqlite` works without a flag.
|
|
55
|
+
|
|
56
|
+
## Why node-comfort
|
|
57
|
+
|
|
58
|
+
- **Zero dependencies.** Built only on Node's own modules. No install scripts, nothing else to audit or keep up to date.
|
|
59
|
+
- **Your editor already knows it.** Every function, parameter and option is typed and documented, with examples. Type `nc.` and look around. It works in plain JavaScript too.
|
|
60
|
+
- **Fast to load.** Namespaces load the first time you use them. Requiring the package takes about 3 ms, and a script that only logs never loads the database or the HTTP client.
|
|
61
|
+
- **Safe by default.** Prototype-pollution-proof objects, scrypt passwords, AES-256-GCM, constant-time comparisons, parameterized SQL, atomic file writes.
|
|
62
|
+
- **One way of doing things.** Durations are always `"5m"` or milliseconds, options always come last, and every error has a stable `code`.
|
|
63
|
+
- **Tested everywhere.** Linux, macOS and Windows, Node.js 22 and 24, TypeScript 5.9, 6 and 7.
|
|
64
|
+
|
|
65
|
+
## What's inside
|
|
66
|
+
|
|
67
|
+
| Namespace | What it's for | Often replaces |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| [Logger](https://ix-xs.github.io/node-comfort/guide/logger.html) | Colorful logs, levels, JSON output, log files with rotation | `chalk`, `pino`, `winston` |
|
|
70
|
+
| [`fs`](https://ix-xs.github.io/node-comfort/guide/fs.html) | Files and folders without try/catch, atomic writes, glob, watch | `fs-extra`, `glob`, `rimraf` |
|
|
71
|
+
| [`checker`](https://ix-xs.github.io/node-comfort/guide/checker.html) | 60+ type guards and validators, `assert` | `is`, `validator` |
|
|
72
|
+
| [`env`](https://ix-xs.github.io/node-comfort/guide/env.html) | `.env` loading, typed and validated configuration | `dotenv`, `envalid` |
|
|
73
|
+
| [`schema`](https://ix-xs.github.io/node-comfort/guide/schema.html) | Data validation with TypeScript inference | `zod`, `yup`, `joi` |
|
|
74
|
+
| [`http`](https://ix-xs.github.io/node-comfort/guide/http.html) | `fetch` with JSON, retries, timeouts, hooks, downloads | `axios`, `got`, `ky` |
|
|
75
|
+
| [`SQLite`](https://ix-xs.github.io/node-comfort/guide/sqlite.html) | CRUD, filters, JSON columns, transactions, migrations | `better-sqlite3` and helpers |
|
|
76
|
+
| [`time`](https://ix-xs.github.io/node-comfort/guide/time.html) | Formatting in any language and time zone, durations, date math, cron | `dayjs`, `ms`, `node-cron` |
|
|
77
|
+
| [`str`](https://ix-xs.github.io/node-comfort/guide/str.html) | Case conversion, slugs, emoji-safe truncation, fuzzy matching, plurals | `lodash`, `slugify`, `leven` |
|
|
78
|
+
| [`num`](https://ix-xs.github.io/node-comfort/guide/num.html) | Exact rounding, statistics, currencies, bytes | `numeral`, `pretty-bytes` |
|
|
79
|
+
| [`arr`](https://ix-xs.github.io/node-comfort/guide/arr.html) | Chunk, group, multi-key sort, paginate, set operations | `lodash` |
|
|
80
|
+
| [`obj`](https://ix-xs.github.io/node-comfort/guide/obj.html) | Deep clone, merge, equal and diff, typed dot paths | `lodash`, `deepmerge`, `dequal` |
|
|
81
|
+
| [`func`](https://ix-xs.github.io/node-comfort/guide/func.html) | Debounce, throttle, memoize, retry with backoff, timeout | `p-retry`, `p-timeout` |
|
|
82
|
+
| [`async`](https://ix-xs.github.io/node-comfort/guide/async.html) | Concurrency limits, queues, mutex, polling | `p-limit`, `p-map`, `p-queue` |
|
|
83
|
+
| [`Cache`](https://ix-xs.github.io/node-comfort/guide/cache.html) | LRU cache with TTL, loaders and statistics | `lru-cache` |
|
|
84
|
+
| [`Emitter`](https://ix-xs.github.io/node-comfort/guide/emitter.html) | Typed events, `waitFor`, async iteration | `mitt`, `emittery` |
|
|
85
|
+
| [`id`](https://ix-xs.github.io/node-comfort/guide/id.html) | UUID v4 and v7, ULID, nanoid, tokens, Snowflakes | `uuid`, `ulid`, `nanoid` |
|
|
86
|
+
| [`crypto`](https://ix-xs.github.io/node-comfort/guide/crypto.html) | Passwords, encryption, JWT, TOTP two-factor codes | `bcrypt`, `jsonwebtoken`, `otplib` |
|
|
87
|
+
| [`cli`](https://ix-xs.github.io/node-comfort/guide/cli.html) | Typed arguments, prompts, menus, spinners, progress bars, tables | `commander`, `inquirer`, `ora` |
|
|
88
|
+
| [`color`](https://ix-xs.github.io/node-comfort/guide/color.html) | Chainable terminal colors, truecolor, gradients, links | `chalk`, `picocolors` |
|
|
89
|
+
| [`sys`](https://ix-xs.github.io/node-comfort/guide/sys.html) | Run commands, `which`, graceful shutdown, platform detection | `execa`, `which` |
|
|
90
|
+
| [`utils`](https://ix-xs.github.io/node-comfort/guide/utils.html) | `wait`, safe JSON, crash guard | |
|
|
91
|
+
| [`errors`](https://ix-xs.github.io/node-comfort/guide/errors.html) | Error classes with stable codes | |
|
|
92
|
+
|
|
93
|
+
The logger, `fs`, `checker` and `utils` helpers are also available at the top level: `nc.info()`, `nc.readJSON()`, `nc.isEmail()`, `nc.wait()`.
|
|
94
|
+
|
|
95
|
+
## Importing
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
// CommonJS
|
|
99
|
+
const nc = require("@ix-xs/node-comfort");
|
|
100
|
+
const { str, isEmail, info } = require("@ix-xs/node-comfort");
|
|
255
101
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
// when • poll a predicate and emit events
|
|
261
|
-
nc.when(() => Math.random() > 0.8, { label: "Lucky" }, { interval: 200, max: 3, timeout: 5000 })
|
|
262
|
-
.on("trigger", (p) => nc.log(`<% green ${p.label} %>`))
|
|
263
|
-
.on("timeout", () => nc.log("<% red Timeout %>"))
|
|
264
|
-
.start();
|
|
265
|
-
|
|
266
|
-
// dontCrash • process-level safety nets
|
|
267
|
-
nc.dontCrash()
|
|
268
|
-
.on("error", (err) => console.error("Global error:", err))
|
|
269
|
-
.on("sig", (signal) => { nc.warn(`Caught ${signal}`); process.exit(0); });
|
|
270
|
-
|
|
271
|
-
// JSON
|
|
272
|
-
const json = nc.JSONString({ hello: "world" }); // 4-space pretty print
|
|
273
|
-
const value = nc.JSONParse(json);
|
|
102
|
+
// ES modules
|
|
103
|
+
import nc from "@ix-xs/node-comfort";
|
|
104
|
+
import { slugify } from "@ix-xs/node-comfort/str"; // loads only what it needs
|
|
274
105
|
```
|
|
275
106
|
|
|
276
|
-
|
|
107
|
+
Every namespace has its own entry point (`@ix-xs/node-comfort/http`, `/sqlite`, `/schema`...). In an ES module, importing from these keeps startup lean. See [Performance](https://ix-xs.github.io/node-comfort/guide/performance.html).
|
|
277
108
|
|
|
278
|
-
|
|
109
|
+
TypeScript needs no setup: the types ship with the package.
|
|
279
110
|
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
nc.str.titleCase("the quick fox"); // "The Quick Fox"
|
|
283
|
-
nc.str.camelCase("foo-bar_baz"); // "fooBarBaz"
|
|
284
|
-
nc.str.pascalCase("foo bar"); // "FooBar"
|
|
285
|
-
nc.str.snakeCase("helloWorld"); // "hello_world"
|
|
286
|
-
nc.str.kebabCase("helloWorld"); // "hello-world"
|
|
287
|
-
nc.str.constantCase("helloWorld"); // "HELLO_WORLD"
|
|
288
|
-
|
|
289
|
-
nc.str.slugify("Héllo, World!"); // "hello-world"
|
|
290
|
-
nc.str.truncate("Hello world", 8); // "Hello w…"
|
|
291
|
-
nc.str.center("hi", 6, "*"); // "**hi**"
|
|
292
|
-
nc.str.reverse("abc"); // "cba" (emoji-safe)
|
|
293
|
-
nc.str.squish(" a b "); // "a b"
|
|
294
|
-
nc.str.stripTags("<b>Hi</b>"); // "Hi"
|
|
295
|
-
nc.str.escapeHTML('<a>&"'); // "<a>&""
|
|
296
|
-
nc.str.escapeRegExp("a.b"); // "a\\.b"
|
|
297
|
-
nc.str.count("banana", "a"); // 3
|
|
298
|
-
nc.str.ensurePrefix("a.com", "https://"); // "https://a.com"
|
|
299
|
-
nc.str.template("Hi {name} ({user.role})", { name: "Jo", user: { role: "admin" } });
|
|
300
|
-
// "Hi Jo (admin)"
|
|
301
|
-
nc.str.words("helloWorld-foo"); // ["hello", "World", "foo"]
|
|
302
|
-
nc.str.length("😀"); // 1
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
---
|
|
306
|
-
|
|
307
|
-
## num • number & math utilities
|
|
111
|
+
```ts
|
|
112
|
+
import { schema as s, type Infer } from "@ix-xs/node-comfort";
|
|
308
113
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
nc.num.round(1.005, 2); // 1.01
|
|
312
|
-
nc.num.inRange(5, 0, 10); // true
|
|
313
|
-
nc.num.lerp(0, 100, 0.5); // 50
|
|
314
|
-
nc.num.mapRange(5, 0, 10, 0, 100); // 50
|
|
315
|
-
nc.num.randomInt(1, 6); // dice roll
|
|
316
|
-
|
|
317
|
-
nc.num.range(4); // [0, 1, 2, 3]
|
|
318
|
-
nc.num.range(0, 10, 2); // [0, 2, 4, 6, 8]
|
|
319
|
-
nc.num.range(5, 0); // [5, 4, 3, 2, 1]
|
|
320
|
-
|
|
321
|
-
nc.num.sum([1, 2, 3]); // 6
|
|
322
|
-
nc.num.average([2, 4]); // 3
|
|
323
|
-
nc.num.median([3, 1, 2]); // 2
|
|
324
|
-
nc.num.percent(25, 200); // 12.5
|
|
325
|
-
|
|
326
|
-
nc.num.formatBytes(1536); // "1.5 KB"
|
|
327
|
-
nc.num.formatBytes(1024, { iec: true }); // "1 KiB"
|
|
328
|
-
nc.num.abbreviate(2_400_000); // "2.4M"
|
|
329
|
-
nc.num.thousands(1234567); // "1,234,567"
|
|
330
|
-
nc.num.ordinal(22); // "22nd"
|
|
331
|
-
nc.num.parse("42px"); // 42
|
|
114
|
+
const User = s.object({ email: s.string().email(), age: s.number().int().optional() });
|
|
115
|
+
type User = Infer<typeof User>; // { email: string; age?: number | undefined }
|
|
332
116
|
```
|
|
333
117
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
## arr • array utilities
|
|
118
|
+
## A quick tour
|
|
337
119
|
|
|
338
|
-
|
|
339
|
-
`iteratee`: a property key (string) or a function.
|
|
120
|
+
**Logs** that look good in a terminal and turn into JSON in production:
|
|
340
121
|
|
|
341
122
|
```js
|
|
342
|
-
nc.
|
|
343
|
-
nc.
|
|
344
|
-
nc.arr.unique(users, "id"); // de-dupe by id
|
|
345
|
-
nc.arr.groupBy([1, 2, 3, 4], (n) => (n % 2 ? "odd" : "even"));
|
|
346
|
-
nc.arr.keyBy(users, "id");
|
|
347
|
-
nc.arr.partition([1, 2, 3, 4], (n) => n % 2 === 0); // [[2, 4], [1, 3]]
|
|
348
|
-
nc.arr.countBy(["a", "b", "a"]); // { a: 2, b: 1 }
|
|
349
|
-
|
|
350
|
-
nc.arr.sortBy(users, "age");
|
|
351
|
-
nc.arr.sortBy(users, ["age", "name"], "desc");
|
|
352
|
-
nc.arr.sumBy(items, "price");
|
|
353
|
-
nc.arr.maxBy(items, "score");
|
|
354
|
-
|
|
355
|
-
nc.arr.difference([1, 2, 3, 4], [2, 4]); // [1, 3]
|
|
356
|
-
nc.arr.intersection([1, 2, 3], [2, 3, 4]); // [2, 3]
|
|
357
|
-
nc.arr.union([1, 2], [2, 3]); // [1, 2, 3]
|
|
358
|
-
nc.arr.zip(["a", "b"], [1, 2]); // [["a", 1], ["b", 2]]
|
|
359
|
-
|
|
360
|
-
nc.arr.compact([0, 1, false, 2, ""]); // [1, 2]
|
|
361
|
-
nc.arr.flatten([1, [2, [3]]], Infinity); // [1, 2, 3]
|
|
362
|
-
nc.arr.shuffle([1, 2, 3]); // random order
|
|
363
|
-
nc.arr.sample([1, 2, 3]); // random element
|
|
364
|
-
nc.arr.sampleSize([1, 2, 3, 4], 2); // 2 random elements
|
|
365
|
-
nc.arr.first([1, 2, 3], 2); // [1, 2]
|
|
366
|
-
nc.arr.last([1, 2, 3]); // 3
|
|
367
|
-
nc.arr.move([1, 2, 3], 0, 2); // [2, 3, 1]
|
|
368
|
-
nc.arr.times(3, (i) => i * 2); // [0, 2, 4]
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
---
|
|
372
|
-
|
|
373
|
-
## obj • object utilities
|
|
123
|
+
nc.info("Server listening on port 3000").success("Connected to the database");
|
|
124
|
+
nc.log("<% cyan bold Tip:%> press <% bgWhite black q %> to quit");
|
|
374
125
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
```js
|
|
378
|
-
nc.obj.clone({ a: { b: 1 } }); // deep clone (structuredClone + fallback)
|
|
379
|
-
nc.obj.merge({ a: { x: 1 } }, { a: { y: 2 } }); // { a: { x: 1, y: 2 } }
|
|
380
|
-
nc.obj.equal({ a: [1, 2] }, { a: [1, 2] }); // true (deep)
|
|
381
|
-
|
|
382
|
-
nc.obj.get(data, "a.b[0].c", "default");
|
|
383
|
-
nc.obj.set({}, "a.b.c", 1); // { a: { b: { c: 1 } } } (returns a clone)
|
|
384
|
-
nc.obj.has({ a: { b: 1 } }, "a.b"); // true
|
|
385
|
-
|
|
386
|
-
nc.obj.pick({ a: 1, b: 2, c: 3 }, ["a", "c"]); // { a: 1, c: 3 }
|
|
387
|
-
nc.obj.omit({ a: 1, b: 2 }, ["b"]); // { a: 1 }
|
|
388
|
-
nc.obj.filter({ a: 1, b: 2 }, (v) => v > 1); // { b: 2 }
|
|
389
|
-
nc.obj.mapValues({ a: 1 }, (v) => v * 10); // { a: 10 }
|
|
390
|
-
nc.obj.mapKeys({ a: 1 }, (k) => k.toUpperCase()); // { A: 1 }
|
|
391
|
-
nc.obj.invert({ a: "x" }); // { x: "a" }
|
|
392
|
-
nc.obj.compact({ a: 1, b: null }); // { a: 1 }
|
|
393
|
-
|
|
394
|
-
nc.obj.flatten({ a: { b: { c: 1 } } }); // { "a.b.c": 1 }
|
|
395
|
-
nc.obj.unflatten({ "a.b.c": 1 }); // { a: { b: { c: 1 } } }
|
|
396
|
-
nc.obj.deepFreeze(config); // recursively immutable
|
|
126
|
+
const log = nc.createLogger({ scope: "api", file: { path: "./logs/api.log", maxSize: "10MB", maxFiles: 5 } });
|
|
127
|
+
const reqLog = log.child("request", { fields: { requestId: "a1b2" } });
|
|
397
128
|
```
|
|
398
129
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
## func • function & control-flow utilities
|
|
130
|
+
**Validation** with the type inferred for you:
|
|
402
131
|
|
|
403
132
|
```js
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
// Caching & single-call
|
|
410
|
-
const init = nc.func.once(() => setup());
|
|
411
|
-
const fib = nc.func.memoize((n) => (n < 2 ? n : fib(n - 1) + fib(n - 2)));
|
|
412
|
-
|
|
413
|
-
// Resilience
|
|
414
|
-
const data = await nc.func.retry(() => fetchThing(), {
|
|
415
|
-
attempts: 5, delay: 200, backoff: 2, // 200, 400, 800…
|
|
133
|
+
const User = nc.schema.object({
|
|
134
|
+
email: nc.schema.string().trim().email(),
|
|
135
|
+
age: nc.schema.number().int().min(18).optional(),
|
|
136
|
+
role: nc.schema.enum(["admin", "user"]).default("user"),
|
|
416
137
|
});
|
|
417
|
-
const res = await nc.func.timeout(fetchThing(), 5000, "too slow");
|
|
418
138
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
// Composition
|
|
423
|
-
const clean = nc.func.pipe((s) => s.trim(), (s) => s.toUpperCase());
|
|
424
|
-
const add = nc.func.curry((a, b, c) => a + b + c);
|
|
425
|
-
add(1)(2)(3); // 6
|
|
426
|
-
|
|
427
|
-
// Interop
|
|
428
|
-
const readFile = nc.func.promisify(require("fs").readFile);
|
|
139
|
+
const result = User.safeParse(req.body);
|
|
140
|
+
if (!result.success) return res.status(400).json(result.error.flatten());
|
|
429
141
|
```
|
|
430
142
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
## time • date & time utilities
|
|
434
|
-
|
|
435
|
-
No heavyweight date library required.
|
|
143
|
+
**HTTP** on top of `fetch`, with the parts you always end up writing:
|
|
436
144
|
|
|
437
145
|
```js
|
|
438
|
-
nc.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
nc.time.relative(Date.now() + 86400e3); // "in 1 day"
|
|
445
|
-
|
|
446
|
-
nc.time.format(new Date(), "YYYY-MM-DD HH:mm:ss");
|
|
447
|
-
nc.time.add(new Date(), "2h");
|
|
448
|
-
nc.time.subtract(new Date(), 1, "d");
|
|
449
|
-
nc.time.diff("2024-01-03", "2024-01-01", "d"); // 2
|
|
450
|
-
nc.time.startOf(new Date(), "day");
|
|
451
|
-
nc.time.isSameDay(a, b);
|
|
452
|
-
nc.time.unix(); // seconds since epoch
|
|
453
|
-
|
|
454
|
-
const sw = nc.time.stopwatch();
|
|
455
|
-
doWork();
|
|
456
|
-
sw.stop(); // 12.34 (ms, sub-ms precise)
|
|
457
|
-
sw.stop(true); // "12.34ms"
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
### Language & time zone
|
|
461
|
-
|
|
462
|
-
`relative`, `formatDuration` and `format` are **locale-aware** and built on the
|
|
463
|
-
platform's native `Intl` (Node ships with full ICU), so **every language works
|
|
464
|
-
out of the box** • no locale files, no dependencies. The default locale is
|
|
465
|
-
`"en"` for deterministic output.
|
|
466
|
-
|
|
467
|
-
Set a global default (chainable), or override per call:
|
|
468
|
-
|
|
469
|
-
```js
|
|
470
|
-
// Global defaults
|
|
471
|
-
nc.time.setLocale("fr").setTimezone("Europe/Paris");
|
|
472
|
-
nc.time.getConfig(); // { locale: "fr", timeZone: "Europe/Paris" }
|
|
473
|
-
|
|
474
|
-
// …or per call via options
|
|
475
|
-
nc.time.relative(Date.now() - 3600e3, undefined, { locale: "fr" }); // "il y a 1 heure"
|
|
476
|
-
nc.time.relative(Date.now() - 86400e3, undefined, { numeric: "auto" }); // "yesterday"
|
|
477
|
-
nc.time.formatDuration(90_000, { long: true, locale: "de" }); // "1 Minute 30 Sekunden"
|
|
478
|
-
|
|
479
|
-
// Localized date formatting
|
|
480
|
-
nc.time.format(Date.now(), "dddd D MMMM YYYY", { locale: "fr" }); // "lundi 15 janvier 2024"
|
|
481
|
-
nc.time.format(Date.now(), "h:mm A"); // "2:30 PM"
|
|
146
|
+
const api = nc.http.create({
|
|
147
|
+
baseURL: "https://api.example.com/v1/",
|
|
148
|
+
auth: { bearer: process.env.API_TOKEN },
|
|
149
|
+
timeout: "10s",
|
|
150
|
+
retry: { attempts: 3 },
|
|
151
|
+
});
|
|
482
152
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
nc.time.isSameDay(a, b, { timeZone: "America/New_York" });
|
|
153
|
+
const { data: users } = await api.get("users", { query: { page: 2 } });
|
|
154
|
+
await api.post("users", { name: "Ada" });
|
|
486
155
|
```
|
|
487
156
|
|
|
488
|
-
|
|
489
|
-
`hh` `h` (12h) `mm` `m` `ss` `s` `SSS`; localized: `MMMM` `MMM` (month),
|
|
490
|
-
`dddd` `ddd` (weekday), `A` `a` (AM/PM). Wrap literal text in square brackets:
|
|
491
|
-
`format(d, "[Updated on] dddd")`.
|
|
492
|
-
|
|
493
|
-
---
|
|
494
|
-
|
|
495
|
-
## id • id, token & hashing utilities
|
|
496
|
-
|
|
497
|
-
Cryptographically secure, built on Node's native `node:crypto`.
|
|
157
|
+
**SQLite** without compiling anything, with objects in and objects out:
|
|
498
158
|
|
|
499
159
|
```js
|
|
500
|
-
nc.
|
|
501
|
-
nc.id.ulid(); // sortable, timestamp-prefixed id
|
|
502
|
-
nc.id.nano(); // URL-safe nanoid (21 chars)
|
|
503
|
-
nc.id.nano(10); // custom length
|
|
504
|
-
|
|
505
|
-
nc.id.token(); // 32-byte hex token
|
|
506
|
-
nc.id.token(16, "base64url"); // URL-safe base64
|
|
507
|
-
nc.id.token(16, "base58"); // Base58 (Bitcoin alphabet)
|
|
508
|
-
nc.id.code(); // "K7QF9X" (unambiguous, human-friendly)
|
|
509
|
-
|
|
510
|
-
nc.id.hash("hello"); // sha256 hex
|
|
511
|
-
nc.id.hash("hello", { algorithm: "md5" });
|
|
512
|
-
nc.id.hmac("payload", "secret"); // HMAC-SHA256
|
|
513
|
-
|
|
514
|
-
nc.id.safeEqual(provided, expected); // constant-time comparison
|
|
515
|
-
nc.id.seq("user"); // "user-1", "user-2", …
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
> **Tip:** ULIDs sort by creation time, making them a great primary-key
|
|
519
|
-
> alternative to UUIDs. Use `nc.id.safeEqual` whenever you compare secrets,
|
|
520
|
-
> tokens or signatures to avoid timing attacks.
|
|
521
|
-
|
|
522
|
-
---
|
|
160
|
+
const db = new nc.SQLite("./data/app.sqlite");
|
|
523
161
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
```js
|
|
531
|
-
const db = new nc.SQLite("data/app.sqlite"); // folder created if needed
|
|
532
|
-
// const db = new nc.SQLite(":memory:"); // transient in-memory DB
|
|
533
|
-
|
|
534
|
-
await db.createTable({
|
|
535
|
-
name: "users",
|
|
536
|
-
columns: {
|
|
537
|
-
id: { type: "INTEGER", primaryKey: true, autoincrement: true },
|
|
538
|
-
name: { type: "TEXT", notNull: true },
|
|
539
|
-
role: { type: "TEXT", values: ["admin", "user"], defaultValue: "user" },
|
|
540
|
-
createdAt: { type: "INTEGER", notNull: true },
|
|
541
|
-
},
|
|
542
|
-
indexes: [{ columns: ["role"] }],
|
|
162
|
+
db.insert("users", { email: "ada@example.com", settings: { theme: "dark" } });
|
|
163
|
+
const admins = db.getAll("users", { role: ["admin", "owner"], age: { gte: 18 } }, { orderBy: "email", limit: 20 });
|
|
164
|
+
db.transaction(() => {
|
|
165
|
+
db.update("accounts", { balance: 90 }, { id: 1 });
|
|
166
|
+
db.update("accounts", { balance: 110 }, { id: 2 });
|
|
543
167
|
});
|
|
544
|
-
|
|
545
|
-
await db.insert("users", { name: "John", role: "admin", createdAt: Date.now() });
|
|
546
|
-
await db.update("users", { role: "user" }, { name: "John" });
|
|
547
|
-
await db.upsert("users", { id: 1, role: "admin" }, ["id"]);
|
|
548
|
-
|
|
549
|
-
const user = await db.get("users", { name: "John" });
|
|
550
|
-
const admins = await db.getAll("users", { role: "admin" }, { limit: 100, orderBy: "createdAt", direction: "DESC" });
|
|
551
|
-
|
|
552
|
-
await db.delete("users", { role: "user" });
|
|
553
|
-
await db.count("users", { role: "admin" });
|
|
554
|
-
await db.clearTable("users");
|
|
555
168
|
```
|
|
556
169
|
|
|
557
|
-
|
|
170
|
+
**Dates** in any language and time zone:
|
|
558
171
|
|
|
559
172
|
```js
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
userId: { type: "INTEGER", notNull: true },
|
|
565
|
-
provider: { type: "TEXT", notNull: true },
|
|
566
|
-
providerUserId: { type: "TEXT", notNull: true },
|
|
567
|
-
},
|
|
568
|
-
constraints: [
|
|
569
|
-
{ type: "unique", columns: ["provider", "providerUserId"] },
|
|
570
|
-
{ type: "foreignKey", columns: ["userId"], references: { table: "users", columns: ["id"] }, onDelete: "CASCADE" },
|
|
571
|
-
],
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
await db.createIndex("UserAuthMethod", { columns: ["provider", "providerUserId"], unique: true });
|
|
173
|
+
nc.time.setLocale("fr").setTimezone("Europe/Paris");
|
|
174
|
+
nc.time.format(new Date(), "dddd D MMMM YYYY HH:mm"); // "vendredi 11 septembre 2026 14:30"
|
|
175
|
+
nc.time.relative(Date.now() - 3 * 3600e3); // "il y a 3 heures"
|
|
176
|
+
nc.time.cron("0 9 * * 1-5", sendReport); // weekdays at 9:00
|
|
575
177
|
```
|
|
576
178
|
|
|
577
|
-
|
|
179
|
+
**Command-line tools** with typed flags, prompts and spinners:
|
|
578
180
|
|
|
579
181
|
```js
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
return { ok: true }; // throw to roll back
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
await db.exec("CREATE INDEX IF NOT EXISTS idx_name ON users (name)");
|
|
587
|
-
const one = await db.queryOne("SELECT * FROM users WHERE id = ?", [1]);
|
|
588
|
-
const many = await db.queryAll("SELECT * FROM users WHERE role = ?", ["admin"]);
|
|
182
|
+
const { flags } = nc.cli.args({
|
|
183
|
+
port: { type: "number", short: "p", default: 3000, description: "Port to listen on" },
|
|
184
|
+
}, { version: "1.0.0" }); // --help and --version for free
|
|
589
185
|
|
|
590
|
-
|
|
186
|
+
const env = await nc.cli.select("Environment?", ["staging", "production"]);
|
|
187
|
+
const spinner = nc.cli.spinner(`Deploying to ${env}`).start();
|
|
188
|
+
spinner.succeed("Deployed");
|
|
591
189
|
```
|
|
592
190
|
|
|
593
|
-
|
|
191
|
+
That's a small part of it. The [guide](https://ix-xs.github.io/node-comfort/guide/introduction.html) walks through every namespace, and the [recipes](https://ix-xs.github.io/node-comfort/guide/recipes.html) show them working together: a small API server, a CLI, a scheduled job.
|
|
594
192
|
|
|
595
|
-
##
|
|
193
|
+
## Coming from 1.x
|
|
596
194
|
|
|
597
|
-
|
|
598
|
-
the box • no `@types` package needed.
|
|
195
|
+
2.0 keeps the 1.x API but changes a few behaviors. The main ones:
|
|
599
196
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
197
|
+
- Node.js 22.13 or newer is required.
|
|
198
|
+
- SQLite methods throw a `SQLiteError` instead of returning `{ error }`, and `getAll()` no longer adds a default limit and order.
|
|
199
|
+
- `warn` and `error` write to stderr.
|
|
200
|
+
- Deep imports (`@ix-xs/node-comfort/src/...`) are replaced by official entry points (`@ix-xs/node-comfort/str`).
|
|
603
201
|
|
|
604
|
-
|
|
605
|
-
const size: string = nc.num.formatBytes(2048);
|
|
606
|
-
```
|
|
607
|
-
|
|
608
|
-
---
|
|
202
|
+
The [migration guide](https://ix-xs.github.io/node-comfort/guide/migration.html) lists every change with before and after examples.
|
|
609
203
|
|
|
610
204
|
## Contributing
|
|
611
205
|
|
|
612
|
-
|
|
613
|
-
[github.com/ix-xs/node-comfort](https://github.com/ix-xs/node-comfort).
|
|
614
|
-
|
|
615
|
-
```bash
|
|
616
|
-
npm run build # regenerate the type definitions (tsc)
|
|
617
|
-
```
|
|
618
|
-
|
|
619
|
-
---
|
|
206
|
+
Bug reports, ideas and pull requests are welcome. [CONTRIBUTING.md](./CONTRIBUTING.md) explains how the project is organized. In short: `npm install`, then `npm run check` builds everything and runs the tests.
|
|
620
207
|
|
|
621
208
|
## License
|
|
622
209
|
|