@naturalcycles/nodejs-lib 15.37.1 → 15.38.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/security/nanoid.js +1 -1
- package/dist/stream/index.d.ts +0 -1
- package/dist/stream/index.js +0 -1
- package/dist/stream/writable/writableChunk.d.ts +11 -13
- package/dist/stream/writable/writableChunk.js +58 -67
- package/dist/validation/ajv/getAjv.js +49 -11
- package/package.json +1 -2
- package/src/security/nanoid.ts +1 -1
- package/src/stream/index.ts +0 -1
- package/src/stream/writable/writableChunk.ts +69 -82
- package/src/validation/ajv/getAjv.ts +57 -12
- package/src/validation/joi/joi.extensions.ts +2 -2
- package/dist/stream/transform/transformMultiFork.d.ts +0 -14
- package/dist/stream/transform/transformMultiFork.js +0 -79
- package/src/stream/transform/transformMultiFork.ts +0 -97
package/dist/security/nanoid.js
CHANGED
|
@@ -4,7 +4,7 @@ This file is "vendored" from Nanoid, all credit is to Nanoid authors:
|
|
|
4
4
|
https://github.com/ai/nanoid/
|
|
5
5
|
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
// oxlint-disable no-bitwise -- NanoID implementation relies on bitwise operations
|
|
8
8
|
import { randomFillSync } from 'node:crypto';
|
|
9
9
|
export const ALPHABET_NONAMBIGUOUS = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
|
|
10
10
|
export const ALPHABET_NUMBER = '0123456789';
|
package/dist/stream/index.d.ts
CHANGED
|
@@ -18,7 +18,6 @@ export * from './transform/transformLogProgress.js';
|
|
|
18
18
|
export * from './transform/transformMap.js';
|
|
19
19
|
export * from './transform/transformMapSimple.js';
|
|
20
20
|
export * from './transform/transformMapSync.js';
|
|
21
|
-
export * from './transform/transformMultiFork.js';
|
|
22
21
|
export * from './transform/transformNoOp.js';
|
|
23
22
|
export * from './transform/transformOffset.js';
|
|
24
23
|
export * from './transform/transformSplit.js';
|
package/dist/stream/index.js
CHANGED
|
@@ -18,7 +18,6 @@ export * from './transform/transformLogProgress.js';
|
|
|
18
18
|
export * from './transform/transformMap.js';
|
|
19
19
|
export * from './transform/transformMapSimple.js';
|
|
20
20
|
export * from './transform/transformMapSync.js';
|
|
21
|
-
export * from './transform/transformMultiFork.js';
|
|
22
21
|
export * from './transform/transformNoOp.js';
|
|
23
22
|
export * from './transform/transformOffset.js';
|
|
24
23
|
export * from './transform/transformSplit.js';
|
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import type { Predicate } from '@naturalcycles/js-lib/types';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
splitPredicate: Predicate<T>;
|
|
5
|
-
transformFactories?: (() => TransformTyped<T, T>)[];
|
|
6
|
-
writableFactory: (splitIndex: number) => WritableTyped<T>;
|
|
7
|
-
}
|
|
1
|
+
import type { NonNegativeInteger, Predicate } from '@naturalcycles/js-lib/types';
|
|
2
|
+
import { Pipeline } from '../pipeline.js';
|
|
3
|
+
import type { TransformOptions, WritableTyped } from '../stream.model.js';
|
|
8
4
|
/**
|
|
9
|
-
* Allows to split the
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
5
|
+
* Allows to "split the stream" into chunks, and attach a new Pipeline to
|
|
6
|
+
* each of the chunks.
|
|
7
|
+
*
|
|
8
|
+
* Example use case: you want to write to Cloud Storage, 1000 rows per file,
|
|
9
|
+
* each file needs its own destination Pipeline.
|
|
10
|
+
*
|
|
11
|
+
* @experimental
|
|
14
12
|
*/
|
|
15
|
-
export declare function writableChunk<T>(
|
|
13
|
+
export declare function writableChunk<T>(splitPredicate: Predicate<T>, fn: (pipeline: Pipeline<T>, splitIndex: NonNegativeInteger) => Promise<void>, opt?: TransformOptions): WritableTyped<T>;
|
|
@@ -1,83 +1,74 @@
|
|
|
1
1
|
import { Writable } from 'node:stream';
|
|
2
|
-
import { _first, _last } from '@naturalcycles/js-lib/array';
|
|
3
2
|
import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { pDefer } from '@naturalcycles/js-lib/promise/pDefer.js';
|
|
4
|
+
import { Pipeline } from '../pipeline.js';
|
|
5
|
+
import { createReadable } from '../readable/createReadable.js';
|
|
6
6
|
/**
|
|
7
|
-
* Allows to split the
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* Allows to "split the stream" into chunks, and attach a new Pipeline to
|
|
8
|
+
* each of the chunks.
|
|
9
|
+
*
|
|
10
|
+
* Example use case: you want to write to Cloud Storage, 1000 rows per file,
|
|
11
|
+
* each file needs its own destination Pipeline.
|
|
12
|
+
*
|
|
13
|
+
* @experimental
|
|
12
14
|
*/
|
|
13
|
-
export function writableChunk(opt) {
|
|
14
|
-
const {
|
|
15
|
+
export function writableChunk(splitPredicate, fn, opt = {}) {
|
|
16
|
+
const { objectMode = true, highWaterMark } = opt;
|
|
15
17
|
const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel);
|
|
16
18
|
let indexWritten = 0;
|
|
17
|
-
let
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
transformFactories.push((transformNoOp));
|
|
21
|
-
}
|
|
22
|
-
// Create the transforms as well as the Writable, and pipe them together
|
|
23
|
-
let currentWritable = writableFactory(currentSplitIndex);
|
|
24
|
-
let transforms = transformFactories.map(f => f());
|
|
25
|
-
generateTuples(transforms).forEach(([t1, t2]) => t1.pipe(t2));
|
|
26
|
-
_last(transforms).pipe(currentWritable);
|
|
27
|
-
// We keep track of all the pending writables, so we can await them in the final method
|
|
28
|
-
const writablesFinish = [awaitFinish(currentWritable)];
|
|
19
|
+
let splitIndex = 0;
|
|
20
|
+
let lock;
|
|
21
|
+
let fork = createNewFork();
|
|
29
22
|
return new Writable({
|
|
30
|
-
objectMode
|
|
23
|
+
objectMode,
|
|
31
24
|
highWaterMark,
|
|
32
|
-
write(chunk, _, cb) {
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
async write(chunk, _, cb) {
|
|
26
|
+
if (lock) {
|
|
27
|
+
// Forked pipeline is locked - let's wait for it to call _read
|
|
28
|
+
await lock;
|
|
29
|
+
// lock is undefined at this point
|
|
30
|
+
}
|
|
31
|
+
// pass to the "forked" pipeline
|
|
32
|
+
const shouldContinue = fork.push(chunk);
|
|
33
|
+
if (!shouldContinue && !lock) {
|
|
34
|
+
// Forked pipeline indicates that we should Pause
|
|
35
|
+
lock = pDefer();
|
|
36
|
+
logger.debug(`WritableChunk(${splitIndex}): pause`);
|
|
37
|
+
}
|
|
35
38
|
if (splitPredicate(chunk, ++indexWritten)) {
|
|
36
|
-
logger.log(`
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
_last(transforms).pipe(currentWritable);
|
|
43
|
-
writablesFinish.push(awaitFinish(currentWritable));
|
|
39
|
+
logger.log(`WritableChunk(${splitIndex}): splitting to ${splitIndex + 1}`);
|
|
40
|
+
splitIndex++;
|
|
41
|
+
fork.push(null);
|
|
42
|
+
lock?.resolve();
|
|
43
|
+
lock = undefined;
|
|
44
|
+
fork = createNewFork();
|
|
44
45
|
}
|
|
46
|
+
// acknowledge that we've finished processing the input chunk
|
|
47
|
+
cb();
|
|
45
48
|
},
|
|
46
49
|
async final(cb) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
catch (err) {
|
|
54
|
-
cb(err);
|
|
55
|
-
}
|
|
50
|
+
logger.log(`WritableChunk: final`);
|
|
51
|
+
// Pushing null "closes"/ends the secondary pipeline correctly
|
|
52
|
+
fork.push(null);
|
|
53
|
+
// Acknowledge that we've received `null` and passed it through to the fork
|
|
54
|
+
cb();
|
|
56
55
|
},
|
|
57
56
|
});
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
* ```
|
|
75
|
-
*/
|
|
76
|
-
function generateTuples(arr) {
|
|
77
|
-
const tuples = [];
|
|
78
|
-
const arrCopy = _deepCopy(arr);
|
|
79
|
-
for (let i = 1; i < arrCopy.length; i++) {
|
|
80
|
-
tuples.push([arrCopy[i - 1], arrCopy[i]]);
|
|
57
|
+
function createNewFork() {
|
|
58
|
+
const currentSplitIndex = splitIndex;
|
|
59
|
+
const readable = createReadable([], {}, () => {
|
|
60
|
+
// `_read` is called
|
|
61
|
+
if (!lock)
|
|
62
|
+
return;
|
|
63
|
+
// We had a lock - let's Resume
|
|
64
|
+
logger.debug(`WritableChunk(${currentSplitIndex}): resume`);
|
|
65
|
+
const lockCopy = lock;
|
|
66
|
+
lock = undefined;
|
|
67
|
+
lockCopy.resolve();
|
|
68
|
+
});
|
|
69
|
+
void fn(Pipeline.from(readable), currentSplitIndex).then(() => {
|
|
70
|
+
logger.log(`WritableChunk(${currentSplitIndex}): done`);
|
|
71
|
+
});
|
|
72
|
+
return readable;
|
|
81
73
|
}
|
|
82
|
-
return tuples;
|
|
83
74
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/prefer-string-starts-ends-with */
|
|
2
|
-
/* eslint-disable unicorn/prefer-code-point */
|
|
3
1
|
import { _lazyValue } from '@naturalcycles/js-lib';
|
|
4
|
-
import { Ajv } from 'ajv';
|
|
2
|
+
import { _, Ajv } from 'ajv';
|
|
5
3
|
import ajvFormats from 'ajv-formats';
|
|
6
|
-
|
|
4
|
+
/* eslint-disable @typescript-eslint/prefer-string-starts-ends-with */
|
|
5
|
+
// oxlint-disable unicorn/prefer-code-point
|
|
7
6
|
const AJV_OPTIONS = {
|
|
8
7
|
removeAdditional: true,
|
|
9
8
|
allErrors: true,
|
|
@@ -51,17 +50,56 @@ export function createAjv(opt) {
|
|
|
51
50
|
// https://ajv.js.org/guide/formats.html
|
|
52
51
|
// @ts-expect-error types are wrong
|
|
53
52
|
ajvFormats(ajv);
|
|
54
|
-
// https://ajv.js.org/packages/ajv-keywords.html
|
|
55
|
-
// @ts-expect-error types are wrong
|
|
56
|
-
ajvKeywords(ajv, [
|
|
57
|
-
'transform', // trim, toLowerCase, etc.
|
|
58
|
-
'uniqueItemProperties',
|
|
59
|
-
'instanceof',
|
|
60
|
-
]);
|
|
61
53
|
// Adds $merge, $patch keywords
|
|
62
54
|
// https://github.com/ajv-validator/ajv-merge-patch
|
|
63
55
|
// Kirill: temporarily disabled, as it creates a noise of CVE warnings
|
|
64
56
|
// require('ajv-merge-patch')(ajv)
|
|
57
|
+
ajv.addKeyword({
|
|
58
|
+
keyword: 'transform',
|
|
59
|
+
type: 'string',
|
|
60
|
+
modifying: true,
|
|
61
|
+
schemaType: 'object',
|
|
62
|
+
code(cxt) {
|
|
63
|
+
const { gen, data, schema, it } = cxt;
|
|
64
|
+
const { parentData, parentDataProperty } = it;
|
|
65
|
+
if (schema.trim) {
|
|
66
|
+
gen.assign(_ `${data}`, _ `${data}.trim()`);
|
|
67
|
+
}
|
|
68
|
+
if (schema.toLowerCase) {
|
|
69
|
+
gen.assign(_ `${data}`, _ `${data}.toLowerCase()`);
|
|
70
|
+
}
|
|
71
|
+
if (schema.toUpperCase) {
|
|
72
|
+
gen.assign(_ `${data}`, _ `${data}.toUpperCase()`);
|
|
73
|
+
}
|
|
74
|
+
if (typeof schema.truncate === 'number' && schema.truncate >= 0) {
|
|
75
|
+
gen.assign(_ `${data}`, _ `${data}.slice(0, ${schema.truncate})`);
|
|
76
|
+
if (schema.trim) {
|
|
77
|
+
gen.assign(_ `${data}`, _ `${data}.trim()`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
gen.if(_ `${parentData} !== undefined`, () => {
|
|
81
|
+
gen.assign(_ `${parentData}[${parentDataProperty}]`, data);
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
ajv.addKeyword({
|
|
86
|
+
keyword: 'instanceof',
|
|
87
|
+
modifying: true,
|
|
88
|
+
schemaType: 'string',
|
|
89
|
+
validate(instanceOf, data, _schema, _ctx) {
|
|
90
|
+
if (typeof data !== 'object')
|
|
91
|
+
return false;
|
|
92
|
+
if (data === null)
|
|
93
|
+
return false;
|
|
94
|
+
let proto = Object.getPrototypeOf(data);
|
|
95
|
+
while (proto) {
|
|
96
|
+
if (proto.constructor?.name === instanceOf)
|
|
97
|
+
return true;
|
|
98
|
+
proto = Object.getPrototypeOf(proto);
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
65
103
|
return ajv;
|
|
66
104
|
}
|
|
67
105
|
const TS_2500 = 16725225600; // 2500-01-01
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/nodejs-lib",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.38.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@naturalcycles/js-lib": "^15",
|
|
7
7
|
"@types/js-yaml": "^4",
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"@types/yargs": "^16",
|
|
10
10
|
"ajv": "^8",
|
|
11
11
|
"ajv-formats": "^3",
|
|
12
|
-
"ajv-keywords": "^5",
|
|
13
12
|
"chalk": "^5",
|
|
14
13
|
"dotenv": "^17",
|
|
15
14
|
"joi": "^18",
|
package/src/security/nanoid.ts
CHANGED
package/src/stream/index.ts
CHANGED
|
@@ -18,7 +18,6 @@ export * from './transform/transformLogProgress.js'
|
|
|
18
18
|
export * from './transform/transformMap.js'
|
|
19
19
|
export * from './transform/transformMapSimple.js'
|
|
20
20
|
export * from './transform/transformMapSync.js'
|
|
21
|
-
export * from './transform/transformMultiFork.js'
|
|
22
21
|
export * from './transform/transformNoOp.js'
|
|
23
22
|
export * from './transform/transformOffset.js'
|
|
24
23
|
export * from './transform/transformSplit.js'
|
|
@@ -1,104 +1,91 @@
|
|
|
1
1
|
import { Writable } from 'node:stream'
|
|
2
|
-
import { _first, _last } from '@naturalcycles/js-lib/array'
|
|
3
2
|
import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log'
|
|
4
|
-
import {
|
|
5
|
-
import type { Predicate } from '@naturalcycles/js-lib/types'
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
type TransformTyped,
|
|
10
|
-
type WritableTyped,
|
|
11
|
-
} from '../index.js'
|
|
12
|
-
|
|
13
|
-
export interface WritableChunkOptions<T> extends TransformOptions {
|
|
14
|
-
splitPredicate: Predicate<T>
|
|
15
|
-
transformFactories?: (() => TransformTyped<T, T>)[]
|
|
16
|
-
writableFactory: (splitIndex: number) => WritableTyped<T>
|
|
17
|
-
}
|
|
3
|
+
import { type DeferredPromise, pDefer } from '@naturalcycles/js-lib/promise/pDefer.js'
|
|
4
|
+
import type { NonNegativeInteger, Predicate } from '@naturalcycles/js-lib/types'
|
|
5
|
+
import { Pipeline } from '../pipeline.js'
|
|
6
|
+
import { createReadable } from '../readable/createReadable.js'
|
|
7
|
+
import type { ReadableTyped, TransformOptions, WritableTyped } from '../stream.model.js'
|
|
18
8
|
|
|
19
9
|
/**
|
|
20
|
-
* Allows to split the
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
10
|
+
* Allows to "split the stream" into chunks, and attach a new Pipeline to
|
|
11
|
+
* each of the chunks.
|
|
12
|
+
*
|
|
13
|
+
* Example use case: you want to write to Cloud Storage, 1000 rows per file,
|
|
14
|
+
* each file needs its own destination Pipeline.
|
|
15
|
+
*
|
|
16
|
+
* @experimental
|
|
25
17
|
*/
|
|
26
|
-
export function writableChunk<T>(
|
|
27
|
-
|
|
18
|
+
export function writableChunk<T>(
|
|
19
|
+
splitPredicate: Predicate<T>,
|
|
20
|
+
fn: (pipeline: Pipeline<T>, splitIndex: NonNegativeInteger) => Promise<void>,
|
|
21
|
+
opt: TransformOptions = {},
|
|
22
|
+
): WritableTyped<T> {
|
|
23
|
+
const { objectMode = true, highWaterMark } = opt
|
|
28
24
|
const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel)
|
|
29
|
-
|
|
30
25
|
let indexWritten = 0
|
|
31
|
-
let
|
|
32
|
-
// We don't want to have an empty chain, so we add a no-op transform
|
|
33
|
-
if (transformFactories.length === 0) {
|
|
34
|
-
transformFactories.push(transformNoOp<T>)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Create the transforms as well as the Writable, and pipe them together
|
|
38
|
-
let currentWritable = writableFactory(currentSplitIndex)
|
|
39
|
-
let transforms = transformFactories.map(f => f())
|
|
40
|
-
generateTuples(transforms).forEach(([t1, t2]) => t1.pipe(t2))
|
|
41
|
-
_last(transforms).pipe(currentWritable)
|
|
26
|
+
let splitIndex = 0
|
|
42
27
|
|
|
43
|
-
|
|
44
|
-
|
|
28
|
+
let lock: DeferredPromise | undefined
|
|
29
|
+
let fork = createNewFork()
|
|
45
30
|
|
|
46
31
|
return new Writable({
|
|
47
|
-
objectMode
|
|
32
|
+
objectMode,
|
|
48
33
|
highWaterMark,
|
|
49
|
-
write(chunk: T, _, cb) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
currentSplitIndex++
|
|
56
|
-
transforms[0]!.end()
|
|
34
|
+
async write(chunk: T, _, cb) {
|
|
35
|
+
if (lock) {
|
|
36
|
+
// Forked pipeline is locked - let's wait for it to call _read
|
|
37
|
+
await lock
|
|
38
|
+
// lock is undefined at this point
|
|
39
|
+
}
|
|
57
40
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
41
|
+
// pass to the "forked" pipeline
|
|
42
|
+
const shouldContinue = fork.push(chunk)
|
|
43
|
+
if (!shouldContinue && !lock) {
|
|
44
|
+
// Forked pipeline indicates that we should Pause
|
|
45
|
+
lock = pDefer()
|
|
46
|
+
logger.debug(`WritableChunk(${splitIndex}): pause`)
|
|
47
|
+
}
|
|
62
48
|
|
|
63
|
-
|
|
49
|
+
if (splitPredicate(chunk, ++indexWritten)) {
|
|
50
|
+
logger.log(`WritableChunk(${splitIndex}): splitting to ${splitIndex + 1}`)
|
|
51
|
+
splitIndex++
|
|
52
|
+
fork.push(null)
|
|
53
|
+
lock?.resolve()
|
|
54
|
+
lock = undefined
|
|
55
|
+
fork = createNewFork()
|
|
64
56
|
}
|
|
57
|
+
|
|
58
|
+
// acknowledge that we've finished processing the input chunk
|
|
59
|
+
cb()
|
|
65
60
|
},
|
|
66
61
|
async final(cb) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
62
|
+
logger.log(`WritableChunk: final`)
|
|
63
|
+
|
|
64
|
+
// Pushing null "closes"/ends the secondary pipeline correctly
|
|
65
|
+
fork.push(null)
|
|
66
|
+
|
|
67
|
+
// Acknowledge that we've received `null` and passed it through to the fork
|
|
68
|
+
cb()
|
|
75
69
|
},
|
|
76
70
|
})
|
|
77
|
-
}
|
|
78
71
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
* event.
|
|
82
|
-
* This is used to await all the writables in the final method of the writableChunk
|
|
83
|
-
*/
|
|
84
|
-
async function awaitFinish(stream: Writable): Promise<void> {
|
|
85
|
-
return await new Promise(resolve => {
|
|
86
|
-
stream.on('finish', resolve)
|
|
87
|
-
})
|
|
88
|
-
}
|
|
72
|
+
function createNewFork(): ReadableTyped<T> {
|
|
73
|
+
const currentSplitIndex = splitIndex
|
|
89
74
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
75
|
+
const readable = createReadable<T>([], {}, () => {
|
|
76
|
+
// `_read` is called
|
|
77
|
+
if (!lock) return
|
|
78
|
+
// We had a lock - let's Resume
|
|
79
|
+
logger.debug(`WritableChunk(${currentSplitIndex}): resume`)
|
|
80
|
+
const lockCopy = lock
|
|
81
|
+
lock = undefined
|
|
82
|
+
lockCopy.resolve()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
void fn(Pipeline.from<T>(readable), currentSplitIndex).then(() => {
|
|
86
|
+
logger.log(`WritableChunk(${currentSplitIndex}): done`)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
return readable
|
|
102
90
|
}
|
|
103
|
-
return tuples
|
|
104
91
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/prefer-string-starts-ends-with */
|
|
2
|
-
/* eslint-disable unicorn/prefer-code-point */
|
|
3
1
|
import { _lazyValue } from '@naturalcycles/js-lib'
|
|
4
2
|
import type { Options } from 'ajv'
|
|
5
|
-
import { Ajv } from 'ajv'
|
|
3
|
+
import { _, Ajv } from 'ajv'
|
|
6
4
|
import ajvFormats from 'ajv-formats'
|
|
7
|
-
|
|
5
|
+
|
|
6
|
+
/* eslint-disable @typescript-eslint/prefer-string-starts-ends-with */
|
|
7
|
+
// oxlint-disable unicorn/prefer-code-point
|
|
8
8
|
|
|
9
9
|
const AJV_OPTIONS: Options = {
|
|
10
10
|
removeAdditional: true,
|
|
@@ -60,19 +60,64 @@ export function createAjv(opt?: Options): Ajv {
|
|
|
60
60
|
// @ts-expect-error types are wrong
|
|
61
61
|
ajvFormats(ajv)
|
|
62
62
|
|
|
63
|
-
// https://ajv.js.org/packages/ajv-keywords.html
|
|
64
|
-
// @ts-expect-error types are wrong
|
|
65
|
-
ajvKeywords(ajv, [
|
|
66
|
-
'transform', // trim, toLowerCase, etc.
|
|
67
|
-
'uniqueItemProperties',
|
|
68
|
-
'instanceof',
|
|
69
|
-
])
|
|
70
|
-
|
|
71
63
|
// Adds $merge, $patch keywords
|
|
72
64
|
// https://github.com/ajv-validator/ajv-merge-patch
|
|
73
65
|
// Kirill: temporarily disabled, as it creates a noise of CVE warnings
|
|
74
66
|
// require('ajv-merge-patch')(ajv)
|
|
75
67
|
|
|
68
|
+
ajv.addKeyword({
|
|
69
|
+
keyword: 'transform',
|
|
70
|
+
type: 'string',
|
|
71
|
+
modifying: true,
|
|
72
|
+
schemaType: 'object',
|
|
73
|
+
code(cxt) {
|
|
74
|
+
const { gen, data, schema, it } = cxt
|
|
75
|
+
const { parentData, parentDataProperty } = it
|
|
76
|
+
|
|
77
|
+
if (schema.trim) {
|
|
78
|
+
gen.assign(_`${data}`, _`${data}.trim()`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (schema.toLowerCase) {
|
|
82
|
+
gen.assign(_`${data}`, _`${data}.toLowerCase()`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (schema.toUpperCase) {
|
|
86
|
+
gen.assign(_`${data}`, _`${data}.toUpperCase()`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (typeof schema.truncate === 'number' && schema.truncate >= 0) {
|
|
90
|
+
gen.assign(_`${data}`, _`${data}.slice(0, ${schema.truncate})`)
|
|
91
|
+
|
|
92
|
+
if (schema.trim) {
|
|
93
|
+
gen.assign(_`${data}`, _`${data}.trim()`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
gen.if(_`${parentData} !== undefined`, () => {
|
|
98
|
+
gen.assign(_`${parentData}[${parentDataProperty}]`, data)
|
|
99
|
+
})
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
ajv.addKeyword({
|
|
104
|
+
keyword: 'instanceof',
|
|
105
|
+
modifying: true,
|
|
106
|
+
schemaType: 'string',
|
|
107
|
+
validate(instanceOf: string, data: unknown, _schema, _ctx) {
|
|
108
|
+
if (typeof data !== 'object') return false
|
|
109
|
+
if (data === null) return false
|
|
110
|
+
|
|
111
|
+
let proto = Object.getPrototypeOf(data)
|
|
112
|
+
while (proto) {
|
|
113
|
+
if (proto.constructor?.name === instanceOf) return true
|
|
114
|
+
proto = Object.getPrototypeOf(proto)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return false
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
|
|
76
121
|
return ajv
|
|
77
122
|
}
|
|
78
123
|
|
|
@@ -5,9 +5,9 @@ import type { StringSchema } from './string.extensions.js'
|
|
|
5
5
|
import { stringExtensions } from './string.extensions.js'
|
|
6
6
|
|
|
7
7
|
export interface ExtendedJoi extends JoiLib.Root {
|
|
8
|
-
// eslint-disable-next-line id-
|
|
8
|
+
// eslint-disable-next-line id-denylist
|
|
9
9
|
string: <TSchema = string>() => StringSchema<TSchema>
|
|
10
|
-
// eslint-disable-next-line id-
|
|
10
|
+
// eslint-disable-next-line id-denylist
|
|
11
11
|
number: <TSchema = number>() => NumberSchema<TSchema>
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { NonNegativeInteger, Predicate } from '@naturalcycles/js-lib/types';
|
|
2
|
-
import { Pipeline } from '../pipeline.js';
|
|
3
|
-
import type { TransformOptions, TransformTyped } from '../stream.model.js';
|
|
4
|
-
/**
|
|
5
|
-
* Like transformFork, but allows to fork multiple times,
|
|
6
|
-
* aka "split the stream" into chunks, and attach a Pipeline to
|
|
7
|
-
* each of the chunks.
|
|
8
|
-
*
|
|
9
|
-
* Example use case: you want to write to Cloud Storage, 1000 rows per file,
|
|
10
|
-
* each file needs its own destination Pipeline.
|
|
11
|
-
*
|
|
12
|
-
* @experimental
|
|
13
|
-
*/
|
|
14
|
-
export declare function transformMultiFork<T>(splitPredicate: Predicate<T>, fn: (pipeline: Pipeline<T>, splitIndex: NonNegativeInteger) => Promise<void>, opt?: TransformOptions): TransformTyped<T, T>;
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { Transform } from 'node:stream';
|
|
2
|
-
import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log';
|
|
3
|
-
import { pDefer } from '@naturalcycles/js-lib/promise/pDefer.js';
|
|
4
|
-
import { Pipeline } from '../pipeline.js';
|
|
5
|
-
import { createReadable } from '../readable/createReadable.js';
|
|
6
|
-
/**
|
|
7
|
-
* Like transformFork, but allows to fork multiple times,
|
|
8
|
-
* aka "split the stream" into chunks, and attach a Pipeline to
|
|
9
|
-
* each of the chunks.
|
|
10
|
-
*
|
|
11
|
-
* Example use case: you want to write to Cloud Storage, 1000 rows per file,
|
|
12
|
-
* each file needs its own destination Pipeline.
|
|
13
|
-
*
|
|
14
|
-
* @experimental
|
|
15
|
-
*/
|
|
16
|
-
export function transformMultiFork(splitPredicate, fn, opt = {}) {
|
|
17
|
-
const { objectMode = true, highWaterMark } = opt;
|
|
18
|
-
const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel);
|
|
19
|
-
let indexWritten = 0;
|
|
20
|
-
let splitIndex = 0;
|
|
21
|
-
let lock;
|
|
22
|
-
let fork = createNewFork();
|
|
23
|
-
return new Transform({
|
|
24
|
-
objectMode,
|
|
25
|
-
highWaterMark,
|
|
26
|
-
async transform(chunk, _, cb) {
|
|
27
|
-
// pass through to the "main" pipeline
|
|
28
|
-
// Main pipeline should handle backpressure "automatically",
|
|
29
|
-
// so, we're not maintaining a Lock for it
|
|
30
|
-
this.push(chunk);
|
|
31
|
-
if (lock) {
|
|
32
|
-
// Forked pipeline is locked - let's wait for it to call _read
|
|
33
|
-
await lock;
|
|
34
|
-
// lock is undefined at this point
|
|
35
|
-
}
|
|
36
|
-
// pass to the "forked" pipeline
|
|
37
|
-
const shouldContinue = fork.push(chunk);
|
|
38
|
-
if (!shouldContinue && !lock) {
|
|
39
|
-
// Forked pipeline indicates that we should Pause
|
|
40
|
-
lock = pDefer();
|
|
41
|
-
logger.debug(`TransformMultiFork(${splitIndex}): pause`);
|
|
42
|
-
}
|
|
43
|
-
if (splitPredicate(chunk, ++indexWritten)) {
|
|
44
|
-
logger.log(`TransformMultiFork(${splitIndex}): splitting to ${splitIndex + 1}`);
|
|
45
|
-
splitIndex++;
|
|
46
|
-
fork.push(null);
|
|
47
|
-
lock?.resolve();
|
|
48
|
-
lock = undefined;
|
|
49
|
-
fork = createNewFork();
|
|
50
|
-
}
|
|
51
|
-
// acknowledge that we've finished processing the input chunk
|
|
52
|
-
cb();
|
|
53
|
-
},
|
|
54
|
-
async final(cb) {
|
|
55
|
-
logger.log(`TransformMultiFork: final`);
|
|
56
|
-
// Pushing null "closes"/ends the secondary pipeline correctly
|
|
57
|
-
fork.push(null);
|
|
58
|
-
// Acknowledge that we've received `null` and passed it through to the fork
|
|
59
|
-
cb();
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
function createNewFork() {
|
|
63
|
-
const currentSplitIndex = splitIndex;
|
|
64
|
-
const readable = createReadable([], {}, () => {
|
|
65
|
-
// `_read` is called
|
|
66
|
-
if (!lock)
|
|
67
|
-
return;
|
|
68
|
-
// We had a lock - let's Resume
|
|
69
|
-
logger.debug(`TransformMultiFork(${currentSplitIndex}): resume`);
|
|
70
|
-
const lockCopy = lock;
|
|
71
|
-
lock = undefined;
|
|
72
|
-
lockCopy.resolve();
|
|
73
|
-
});
|
|
74
|
-
void fn(Pipeline.from(readable), currentSplitIndex).then(() => {
|
|
75
|
-
logger.log(`TransformMultiFork(${currentSplitIndex}): done`);
|
|
76
|
-
});
|
|
77
|
-
return readable;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { Transform } from 'node:stream'
|
|
2
|
-
import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log'
|
|
3
|
-
import { type DeferredPromise, pDefer } from '@naturalcycles/js-lib/promise/pDefer.js'
|
|
4
|
-
import type { NonNegativeInteger, Predicate } from '@naturalcycles/js-lib/types'
|
|
5
|
-
import { Pipeline } from '../pipeline.js'
|
|
6
|
-
import { createReadable } from '../readable/createReadable.js'
|
|
7
|
-
import type { ReadableTyped, TransformOptions, TransformTyped } from '../stream.model.js'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Like transformFork, but allows to fork multiple times,
|
|
11
|
-
* aka "split the stream" into chunks, and attach a Pipeline to
|
|
12
|
-
* each of the chunks.
|
|
13
|
-
*
|
|
14
|
-
* Example use case: you want to write to Cloud Storage, 1000 rows per file,
|
|
15
|
-
* each file needs its own destination Pipeline.
|
|
16
|
-
*
|
|
17
|
-
* @experimental
|
|
18
|
-
*/
|
|
19
|
-
export function transformMultiFork<T>(
|
|
20
|
-
splitPredicate: Predicate<T>,
|
|
21
|
-
fn: (pipeline: Pipeline<T>, splitIndex: NonNegativeInteger) => Promise<void>,
|
|
22
|
-
opt: TransformOptions = {},
|
|
23
|
-
): TransformTyped<T, T> {
|
|
24
|
-
const { objectMode = true, highWaterMark } = opt
|
|
25
|
-
const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel)
|
|
26
|
-
let indexWritten = 0
|
|
27
|
-
let splitIndex = 0
|
|
28
|
-
|
|
29
|
-
let lock: DeferredPromise | undefined
|
|
30
|
-
let fork = createNewFork()
|
|
31
|
-
|
|
32
|
-
return new Transform({
|
|
33
|
-
objectMode,
|
|
34
|
-
highWaterMark,
|
|
35
|
-
async transform(chunk: T, _, cb) {
|
|
36
|
-
// pass through to the "main" pipeline
|
|
37
|
-
// Main pipeline should handle backpressure "automatically",
|
|
38
|
-
// so, we're not maintaining a Lock for it
|
|
39
|
-
this.push(chunk)
|
|
40
|
-
|
|
41
|
-
if (lock) {
|
|
42
|
-
// Forked pipeline is locked - let's wait for it to call _read
|
|
43
|
-
await lock
|
|
44
|
-
// lock is undefined at this point
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// pass to the "forked" pipeline
|
|
48
|
-
const shouldContinue = fork.push(chunk)
|
|
49
|
-
if (!shouldContinue && !lock) {
|
|
50
|
-
// Forked pipeline indicates that we should Pause
|
|
51
|
-
lock = pDefer()
|
|
52
|
-
logger.debug(`TransformMultiFork(${splitIndex}): pause`)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (splitPredicate(chunk, ++indexWritten)) {
|
|
56
|
-
logger.log(`TransformMultiFork(${splitIndex}): splitting to ${splitIndex + 1}`)
|
|
57
|
-
splitIndex++
|
|
58
|
-
fork.push(null)
|
|
59
|
-
lock?.resolve()
|
|
60
|
-
lock = undefined
|
|
61
|
-
fork = createNewFork()
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// acknowledge that we've finished processing the input chunk
|
|
65
|
-
cb()
|
|
66
|
-
},
|
|
67
|
-
async final(cb) {
|
|
68
|
-
logger.log(`TransformMultiFork: final`)
|
|
69
|
-
|
|
70
|
-
// Pushing null "closes"/ends the secondary pipeline correctly
|
|
71
|
-
fork.push(null)
|
|
72
|
-
|
|
73
|
-
// Acknowledge that we've received `null` and passed it through to the fork
|
|
74
|
-
cb()
|
|
75
|
-
},
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
function createNewFork(): ReadableTyped<T> {
|
|
79
|
-
const currentSplitIndex = splitIndex
|
|
80
|
-
|
|
81
|
-
const readable = createReadable<T>([], {}, () => {
|
|
82
|
-
// `_read` is called
|
|
83
|
-
if (!lock) return
|
|
84
|
-
// We had a lock - let's Resume
|
|
85
|
-
logger.debug(`TransformMultiFork(${currentSplitIndex}): resume`)
|
|
86
|
-
const lockCopy = lock
|
|
87
|
-
lock = undefined
|
|
88
|
-
lockCopy.resolve()
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
void fn(Pipeline.from<T>(readable), currentSplitIndex).then(() => {
|
|
92
|
-
logger.log(`TransformMultiFork(${currentSplitIndex}): done`)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
return readable
|
|
96
|
-
}
|
|
97
|
-
}
|