@brianbuie/node-kit 0.7.0 → 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/README.md CHANGED
@@ -32,7 +32,6 @@ Links: [API](#api), [Classes](#classes), [Functions](#functions), [Types](#types
32
32
  | [FileTypeCsv](#class-filetypecsv) |
33
33
  | [FileTypeJson](#class-filetypejson) |
34
34
  | [FileTypeNdjson](#class-filetypendjson) |
35
- | [Jwt](#class-jwt) |
36
35
  | [Log](#class-log) |
37
36
  | [TempDir](#class-tempdir) |
38
37
  | [TypeWriter](#class-typewriter) |
@@ -43,16 +42,20 @@ Links: [API](#api), [Classes](#classes), [Functions](#functions), [Types](#types
43
42
 
44
43
  ## Class: Cache
45
44
 
46
- Save results of a function in a temporary file.
45
+ Save data to a local file with an expiration.
46
+ Fresh/stale data is returned with a flag for if it's fresh or not,
47
+ so stale data can still be used if needed.
47
48
 
48
49
  ```ts
49
50
  export class Cache<T> {
50
51
  file;
51
52
  ttl;
52
- getValue;
53
- constructor(key: string, ttl: number, getValue: () => T | Promise<T>)
54
- async read()
55
- async write()
53
+ constructor(key: string, ttl: number | Duration, initialData?: T)
54
+ write(data: T)
55
+ read(): [
56
+ T | undefined,
57
+ boolean
58
+ ]
56
59
  }
57
60
  ```
58
61
 
@@ -244,10 +247,10 @@ export class File {
244
247
  path;
245
248
  constructor(filepath: string)
246
249
  get exists()
247
- createWriteStream(options: Parameters<typeof fs.createWriteStream>[1] = {})
248
250
  delete()
249
251
  read()
250
252
  write(contents: string)
253
+ async streamFrom(...options: Parameters<(typeof Readable)["from"]>)
251
254
  append(lines: string | string[])
252
255
  lines()
253
256
  static get FileType()
@@ -349,25 +352,6 @@ See also: [FileType](#class-filetype)
349
352
 
350
353
  Links: [API](#api), [Classes](#classes), [Functions](#functions), [Types](#types), [Variables](#variables)
351
354
 
352
- ---
353
- ## Class: Jwt
354
-
355
- ```ts
356
- export class Jwt {
357
- config;
358
- #saved?: {
359
- exp: number;
360
- token: string;
361
- };
362
- constructor(config: JwtConfig)
363
- get now()
364
- #createToken()
365
- get token()
366
- }
367
- ```
368
-
369
- Links: [API](#api), [Classes](#classes), [Functions](#functions), [Types](#types), [Variables](#variables)
370
-
371
355
  ---
372
356
  ## Class: Log
373
357
 
package/dist/Cache.d.ts CHANGED
@@ -1,17 +1,16 @@
1
+ import { type Duration } from 'date-fns';
1
2
  /**
2
- * Save results of a function in a temporary file.
3
- * @param key A unique name for the file
4
- * @param ttl cache duration in ms
5
- * @param getValue the function to populate the cache (eg. fetch results, generate key, etc)
3
+ * Save data to a local file with an expiration.
4
+ * Fresh/stale data is returned with a flag for if it's fresh or not,
5
+ * so stale data can still be used if needed.
6
6
  */
7
7
  export declare class Cache<T> {
8
8
  file: import("./File.js").FileTypeJson<{
9
- createdAt: number;
10
- value: T;
9
+ savedAt: string;
10
+ data: T;
11
11
  }>;
12
- ttl: number;
13
- getValue: () => T | Promise<T>;
14
- constructor(key: string, ttl: number, getValue: () => T | Promise<T>);
15
- read(): Promise<T>;
16
- write(): Promise<T>;
12
+ ttl: Duration;
13
+ constructor(key: string, ttl: number | Duration, initialData?: T);
14
+ write(data: T): void;
15
+ read(): [T | undefined, boolean];
17
16
  }
package/dist/Cache.js CHANGED
@@ -1,29 +1,26 @@
1
+ import { isAfter, add } from 'date-fns';
1
2
  import { temp } from './Dir.js';
2
3
  const cacheDir = temp.dir('cache');
3
4
  /**
4
- * Save results of a function in a temporary file.
5
- * @param key A unique name for the file
6
- * @param ttl cache duration in ms
7
- * @param getValue the function to populate the cache (eg. fetch results, generate key, etc)
5
+ * Save data to a local file with an expiration.
6
+ * Fresh/stale data is returned with a flag for if it's fresh or not,
7
+ * so stale data can still be used if needed.
8
8
  */
9
9
  export class Cache {
10
10
  file;
11
11
  ttl;
12
- getValue;
13
- constructor(key, ttl, getValue) {
12
+ constructor(key, ttl, initialData) {
14
13
  this.file = cacheDir.file(key).json();
15
- this.ttl = ttl;
16
- this.getValue = getValue;
14
+ this.ttl = typeof ttl === 'number' ? { minutes: ttl } : ttl;
15
+ if (initialData)
16
+ this.write(initialData);
17
17
  }
18
- async read() {
19
- const { createdAt, value } = this.file.read() || {};
20
- if (value && createdAt && createdAt + this.ttl > Date.now())
21
- return value;
22
- return this.write();
18
+ write(data) {
19
+ this.file.write({ savedAt: new Date().toUTCString(), data });
23
20
  }
24
- async write() {
25
- const value = await this.getValue();
26
- this.file.write({ createdAt: Date.now(), value });
27
- return value;
21
+ read() {
22
+ const { savedAt, data } = this.file.read() || {};
23
+ const isFresh = Boolean(savedAt && isAfter(add(savedAt, this.ttl), new Date()));
24
+ return [data, isFresh];
28
25
  }
29
26
  }
package/dist/Dir.js CHANGED
@@ -30,7 +30,7 @@ export class Dir {
30
30
  return new Dir(path.resolve(this.path, subPath));
31
31
  }
32
32
  sanitize(name) {
33
- return sanitizeFilename(name.replace('https://', '').replace('www.', ''), { replacement: '_' });
33
+ return sanitizeFilename(name.replace('https://', '').replace('www.', ''), { replacement: '_' }).slice(-200);
34
34
  }
35
35
  /**
36
36
  * @param base - The file name with extension
package/dist/File.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as fs from 'node:fs';
1
+ import { Readable } from 'node:stream';
2
2
  /**
3
3
  * WARNING: API will change!
4
4
  */
@@ -6,10 +6,10 @@ export declare class File {
6
6
  path: string;
7
7
  constructor(filepath: string);
8
8
  get exists(): boolean;
9
- createWriteStream(options?: Parameters<typeof fs.createWriteStream>[1]): fs.WriteStream;
10
9
  delete(): void;
11
10
  read(): string | undefined;
12
11
  write(contents: string): void;
12
+ streamFrom(...options: Parameters<(typeof Readable)['from']>): Promise<void>;
13
13
  /**
14
14
  * creates file if it doesn't exist, appends string or array of strings as new lines.
15
15
  * File always ends with '\n', so contents don't need to be read before appending
package/dist/File.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
+ import { Readable } from 'node:stream';
4
+ import { finished } from 'node:stream/promises';
3
5
  import { writeToString, parseString } from 'fast-csv';
4
6
  import { snapshot } from './snapshot.js';
5
7
  /**
@@ -13,9 +15,6 @@ export class File {
13
15
  get exists() {
14
16
  return fs.existsSync(this.path);
15
17
  }
16
- createWriteStream(options = {}) {
17
- return fs.createWriteStream(this.path, options);
18
- }
19
18
  delete() {
20
19
  fs.rmSync(this.path, { force: true });
21
20
  }
@@ -26,6 +25,9 @@ export class File {
26
25
  fs.mkdirSync(path.parse(this.path).dir, { recursive: true });
27
26
  fs.writeFileSync(this.path, contents);
28
27
  }
28
+ async streamFrom(...options) {
29
+ return finished(Readable.from(...options).pipe(fs.createWriteStream(this.path)));
30
+ }
29
31
  /**
30
32
  * creates file if it doesn't exist, appends string or array of strings as new lines.
31
33
  * File always ends with '\n', so contents don't need to be read before appending
package/dist/index.d.ts CHANGED
@@ -2,7 +2,6 @@ export { Dir, TempDir, temp } from './Dir.js';
2
2
  export { Cache } from './Cache.js';
3
3
  export { Fetcher, type Route, type Query, type FetchOptions } from './Fetcher.js';
4
4
  export { File, FileType, FileTypeJson, FileTypeNdjson, FileTypeCsv } from './File.js';
5
- export { Jwt } from './Jwt.js';
6
5
  export { Log } from './Log.js';
7
6
  export { snapshot } from './snapshot.js';
8
7
  export { timeout } from './timeout.js';
package/dist/index.js CHANGED
@@ -2,7 +2,6 @@ export { Dir, TempDir, temp } from './Dir.js';
2
2
  export { Cache } from './Cache.js';
3
3
  export { Fetcher } from './Fetcher.js';
4
4
  export { File, FileType, FileTypeJson, FileTypeNdjson, FileTypeCsv } from './File.js';
5
- export { Jwt } from './Jwt.js';
6
5
  export { Log } from './Log.js';
7
6
  export { snapshot } from './snapshot.js';
8
7
  export { timeout } from './timeout.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brianbuie/node-kit",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "license": "ISC",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,15 +30,14 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "chalk": "^5.6.2",
33
+ "date-fns": "^4.1.0",
33
34
  "extract-domain": "^5.0.2",
34
35
  "fast-csv": "^5.0.5",
35
- "jsonwebtoken": "^9.0.2",
36
36
  "lodash-es": "^4.17.21",
37
37
  "quicktype-core": "^23.2.6",
38
38
  "sanitize-filename": "^1.6.3"
39
39
  },
40
40
  "devDependencies": {
41
- "@types/jsonwebtoken": "^9.0.10",
42
41
  "@types/lodash-es": "^4.17.12",
43
42
  "@types/node": "^24.9.1",
44
43
  "ts2md": "^0.2.8",
package/src/Cache.ts CHANGED
@@ -1,33 +1,30 @@
1
+ import { type Duration, isAfter, add } from 'date-fns';
1
2
  import { temp } from './Dir.js';
2
3
 
3
4
  const cacheDir = temp.dir('cache');
4
5
 
5
6
  /**
6
- * Save results of a function in a temporary file.
7
- * @param key A unique name for the file
8
- * @param ttl cache duration in ms
9
- * @param getValue the function to populate the cache (eg. fetch results, generate key, etc)
7
+ * Save data to a local file with an expiration.
8
+ * Fresh/stale data is returned with a flag for if it's fresh or not,
9
+ * so stale data can still be used if needed.
10
10
  */
11
11
  export class Cache<T> {
12
12
  file;
13
13
  ttl;
14
- getValue;
15
14
 
16
- constructor(key: string, ttl: number, getValue: () => T | Promise<T>) {
17
- this.file = cacheDir.file(key).json<{ createdAt: number; value: T }>();
18
- this.ttl = ttl;
19
- this.getValue = getValue;
15
+ constructor(key: string, ttl: number | Duration, initialData?: T) {
16
+ this.file = cacheDir.file(key).json<{ savedAt: string; data: T }>();
17
+ this.ttl = typeof ttl === 'number' ? { minutes: ttl } : ttl;
18
+ if (initialData) this.write(initialData);
20
19
  }
21
20
 
22
- async read() {
23
- const { createdAt, value } = this.file.read() || {};
24
- if (value && createdAt && createdAt + this.ttl > Date.now()) return value;
25
- return this.write();
21
+ write(data: T) {
22
+ this.file.write({ savedAt: new Date().toUTCString(), data });
26
23
  }
27
24
 
28
- async write() {
29
- const value = await this.getValue();
30
- this.file.write({ createdAt: Date.now(), value });
31
- return value;
25
+ read(): [T | undefined, boolean] {
26
+ const { savedAt, data } = this.file.read() || {};
27
+ const isFresh = Boolean(savedAt && isAfter(add(savedAt, this.ttl), new Date()));
28
+ return [data, isFresh];
32
29
  }
33
30
  }
package/src/Dir.ts CHANGED
@@ -35,7 +35,7 @@ export class Dir {
35
35
  }
36
36
 
37
37
  sanitize(name: string) {
38
- return sanitizeFilename(name.replace('https://', '').replace('www.', ''), { replacement: '_' });
38
+ return sanitizeFilename(name.replace('https://', '').replace('www.', ''), { replacement: '_' }).slice(-200);
39
39
  }
40
40
 
41
41
  /**
package/src/File.test.ts CHANGED
@@ -35,6 +35,19 @@ describe('FileType', () => {
35
35
  });
36
36
  });
37
37
 
38
+ describe('File', () => {
39
+ it('Handles request body as stream input', async () => {
40
+ const res = await fetch('https://testingbot.com/free-online-tools/random-avatar/300');
41
+ const img = testDir.file('image.jpg');
42
+ if (res.body) {
43
+ await img.streamFrom(res.body);
44
+ assert(img.exists);
45
+ } else {
46
+ assert(false, 'No response body');
47
+ }
48
+ });
49
+ });
50
+
38
51
  describe('File.ndjson', () => {
39
52
  it('Appends new lines correctly', () => {
40
53
  const file = testDir.file('appends-lines').ndjson();
package/src/File.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
+ import { Readable } from 'node:stream';
4
+ import { finished } from 'node:stream/promises';
3
5
  import { writeToString, parseString } from 'fast-csv';
4
6
  import { snapshot } from './snapshot.js';
5
7
 
@@ -17,10 +19,6 @@ export class File {
17
19
  return fs.existsSync(this.path);
18
20
  }
19
21
 
20
- createWriteStream(options: Parameters<typeof fs.createWriteStream>[1] = {}) {
21
- return fs.createWriteStream(this.path, options);
22
- }
23
-
24
22
  delete() {
25
23
  fs.rmSync(this.path, { force: true });
26
24
  }
@@ -34,6 +32,10 @@ export class File {
34
32
  fs.writeFileSync(this.path, contents);
35
33
  }
36
34
 
35
+ async streamFrom(...options: Parameters<(typeof Readable)['from']>) {
36
+ return finished(Readable.from(...options).pipe(fs.createWriteStream(this.path)));
37
+ }
38
+
37
39
  /**
38
40
  * creates file if it doesn't exist, appends string or array of strings as new lines.
39
41
  * File always ends with '\n', so contents don't need to be read before appending
@@ -133,7 +135,7 @@ export class FileTypeNdjson<T extends object> extends FileType {
133
135
 
134
136
  append(lines: T | T[]) {
135
137
  this.file.append(
136
- Array.isArray(lines) ? lines.map((l) => JSON.stringify(snapshot(l))) : JSON.stringify(snapshot(lines))
138
+ Array.isArray(lines) ? lines.map((l) => JSON.stringify(snapshot(l))) : JSON.stringify(snapshot(lines)),
137
139
  );
138
140
  }
139
141
 
@@ -186,8 +188,8 @@ export class FileTypeCsv<Row extends object> extends FileType {
186
188
  ...all,
187
189
  [key]: parseVal(val as string),
188
190
  }),
189
- {} as Row
190
- )
191
+ {} as Row,
192
+ ),
191
193
  );
192
194
  });
193
195
  });
package/src/index.ts CHANGED
@@ -2,7 +2,6 @@ export { Dir, TempDir, temp } from './Dir.js';
2
2
  export { Cache } from './Cache.js';
3
3
  export { Fetcher, type Route, type Query, type FetchOptions } from './Fetcher.js';
4
4
  export { File, FileType, FileTypeJson, FileTypeNdjson, FileTypeCsv } from './File.js';
5
- export { Jwt } from './Jwt.js';
6
5
  export { Log } from './Log.js';
7
6
  export { snapshot } from './snapshot.js';
8
7
  export { timeout } from './timeout.js';
package/dist/Jwt.d.ts DELETED
@@ -1,15 +0,0 @@
1
- import { type JwtPayload, type SignOptions } from 'jsonwebtoken';
2
- type JwtConfig = {
3
- payload: JwtPayload;
4
- options: SignOptions;
5
- seconds: number;
6
- key: string;
7
- };
8
- export declare class Jwt {
9
- #private;
10
- config: JwtConfig;
11
- constructor(config: JwtConfig);
12
- get now(): number;
13
- get token(): string;
14
- }
15
- export {};
package/dist/Jwt.js DELETED
@@ -1,29 +0,0 @@
1
- import { default as jsonwebtoken } from 'jsonwebtoken';
2
- import { merge } from 'lodash-es';
3
- export class Jwt {
4
- config;
5
- #saved;
6
- constructor(config) {
7
- this.config = config;
8
- this.#createToken();
9
- }
10
- get now() {
11
- return Math.floor(Date.now() / 1000);
12
- }
13
- #createToken() {
14
- const exp = this.now + this.config.seconds;
15
- const payload = merge({
16
- iat: this.now,
17
- exp,
18
- }, this.config.payload);
19
- const token = jsonwebtoken.sign(payload, this.config.key, this.config.options);
20
- this.#saved = { token, exp };
21
- return token;
22
- }
23
- get token() {
24
- if (this.#saved && this.#saved.exp > this.now) {
25
- return this.#saved.token;
26
- }
27
- return this.#createToken();
28
- }
29
- }
package/src/Jwt.test.ts DELETED
@@ -1,22 +0,0 @@
1
- import { describe, it } from 'node:test';
2
- import assert from 'node:assert';
3
- import jsonwebtoken from 'jsonwebtoken';
4
- import { Jwt } from './Jwt.js';
5
-
6
- describe('Jwt', () => {
7
- it('Creates a valid JWT', () => {
8
- const key = 'test';
9
- const jwt = new Jwt({
10
- payload: {
11
- example: 'value',
12
- },
13
- options: {
14
- algorithm: 'HS256',
15
- },
16
- seconds: 60,
17
- key,
18
- });
19
- const result = jsonwebtoken.verify(jwt.token, key);
20
- assert(typeof result !== 'string' && result.example === 'value');
21
- });
22
- });
package/src/Jwt.ts DELETED
@@ -1,47 +0,0 @@
1
- import { default as jsonwebtoken, type JwtPayload, type SignOptions } from 'jsonwebtoken';
2
- import { merge } from 'lodash-es';
3
-
4
- type JwtConfig = {
5
- payload: JwtPayload;
6
- options: SignOptions;
7
- seconds: number;
8
- key: string;
9
- };
10
-
11
- export class Jwt {
12
- config;
13
- #saved?: {
14
- exp: number;
15
- token: string;
16
- };
17
-
18
- constructor(config: JwtConfig) {
19
- this.config = config;
20
- this.#createToken();
21
- }
22
-
23
- get now() {
24
- return Math.floor(Date.now() / 1000);
25
- }
26
-
27
- #createToken() {
28
- const exp = this.now + this.config.seconds;
29
- const payload: JwtPayload = merge(
30
- {
31
- iat: this.now,
32
- exp,
33
- },
34
- this.config.payload
35
- );
36
- const token = jsonwebtoken.sign(payload, this.config.key, this.config.options);
37
- this.#saved = { token, exp };
38
- return token;
39
- }
40
-
41
- get token() {
42
- if (this.#saved && this.#saved.exp > this.now) {
43
- return this.#saved.token;
44
- }
45
- return this.#createToken();
46
- }
47
- }