Skip to content

Commit 773c684

Browse files
authored
feat!: drop streams support COMPASS-7124 (#208)
We don't really need streams usage anymore and can just drop it. That's an easy way to ensure that we don't have any dependencies that require Node.js-specific builtins or polyfills of those.
1 parent c7b07ea commit 773c684

File tree

6 files changed

+98
-150
lines changed

6 files changed

+98
-150
lines changed

.github/workflows/unit-tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ jobs:
2121
run: npm ci
2222
- name: Check
2323
run: npm run check
24+
- name: Build
25+
run: npm run build
2426
- name: Test
2527
run: npm test
2628
- name: Coverage

examples/parse-from-file.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import { pipeline as callbackPipeline, PassThrough, Transform } from 'stream';
44
import path from 'path';
55
import fs from 'fs';
66
import { promisify } from 'util';
7-
8-
import stream from '../src/stream';
7+
import { parseSchema } from '../src';
98

109
const schemaFileName = path.join(__dirname, './fanclub.json');
1110

@@ -44,12 +43,10 @@ async function parseFromFile(fileName: string) {
4443
});
4544

4645
const dest = new PassThrough({ objectMode: true });
46+
const resultPromise = parseSchema(dest);
4747
const pipeline = promisify(callbackPipeline);
48-
await pipeline(fileReadStream, createFileStreamLineParser(), stream(), dest);
49-
let res;
50-
for await (const result of dest) {
51-
res = result;
52-
}
48+
await pipeline(fileReadStream, createFileStreamLineParser(), dest);
49+
const res = await resultPromise;
5350

5451
const dur = Date.now() - startTime;
5552
console.log(res);

src/index.ts

Lines changed: 20 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
import type { AggregationCursor, Document, FindCursor } from 'mongodb';
2-
import { Readable, PassThrough } from 'stream';
3-
import { pipeline } from 'stream/promises';
4-
5-
import stream from './stream';
6-
import type { ParseStreamOptions } from './stream';
71
import { SchemaAnalyzer } from './schema-analyzer';
82
import type {
93
ArraySchemaType,
@@ -24,72 +18,55 @@ import type {
2418
} from './schema-analyzer';
2519
import * as schemaStats from './stats';
2620

27-
type MongoDBCursor = AggregationCursor | FindCursor;
21+
type AnyIterable<T = any> = Iterable<T> | AsyncIterable<T>;
2822

29-
function getStreamSource(
30-
source: Document[] | MongoDBCursor | Readable
31-
): Readable {
32-
let streamSource: Readable;
33-
if ('stream' in source) {
34-
// MongoDB Cursor.
35-
streamSource = source.stream();
36-
} else if ('pipe' in source) {
37-
// Document stream.
38-
streamSource = source;
39-
} else if (Array.isArray(source)) {
40-
// Array of documents.
41-
streamSource = Readable.from(source);
42-
} else {
23+
function verifyStreamSource(
24+
source: AnyIterable
25+
): AnyIterable {
26+
if (!(Symbol.iterator in source) && !(Symbol.asyncIterator in source)) {
4327
throw new Error(
4428
'Unknown input type for `docs`. Must be an array, ' +
4529
'stream or MongoDB Cursor.'
4630
);
4731
}
4832

49-
return streamSource;
33+
return source;
5034
}
5135

52-
async function schemaStream(
53-
source: Document[] | MongoDBCursor | Readable,
54-
options?: ParseStreamOptions
55-
) {
56-
const streamSource = getStreamSource(source);
57-
58-
const dest = new PassThrough({ objectMode: true });
59-
await pipeline(streamSource, stream(options), dest);
60-
for await (const result of dest) {
61-
return result;
36+
async function getCompletedSchemaAnalyzer(
37+
source: AnyIterable,
38+
options?: SchemaParseOptions
39+
): Promise<SchemaAnalyzer> {
40+
const analyzer = new SchemaAnalyzer(options);
41+
for await (const doc of verifyStreamSource(source)) {
42+
analyzer.analyzeDoc(doc);
6243
}
63-
throw new Error('unreachable'); // `dest` always emits exactly one doc.
44+
return analyzer;
6445
}
6546

6647
/**
6748
* Convenience shortcut for parsing schemas. Accepts an array, stream or
6849
* MongoDB cursor object to parse documents` from.
6950
*/
7051
async function parseSchema(
71-
source: Document[] | MongoDBCursor | Readable,
52+
source: AnyIterable,
7253
options?: SchemaParseOptions
7354
): Promise<Schema> {
74-
return await schemaStream(source, options);
55+
return (await getCompletedSchemaAnalyzer(source, options)).getResult();
7556
}
7657

7758
// Convenience shortcut for getting schema paths.
7859
async function getSchemaPaths(
79-
source: Document[] | MongoDBCursor | Readable
60+
source: AnyIterable
8061
): Promise<string[][]> {
81-
return await schemaStream(source, {
82-
schemaPaths: true
83-
});
62+
return (await getCompletedSchemaAnalyzer(source)).getSchemaPaths();
8463
}
8564

8665
// Convenience shortcut for getting the simplified schema.
8766
async function getSimplifiedSchema(
88-
source: Document[] | MongoDBCursor | Readable
67+
source: AnyIterable
8968
): Promise<SimplifiedSchema> {
90-
return await schemaStream(source, {
91-
simplifiedSchema: true
92-
});
69+
return (await getCompletedSchemaAnalyzer(source)).getSimplifiedSchema();
9370
}
9471

9572
export default parseSchema;
@@ -113,8 +90,6 @@ export type {
11390
};
11491

11592
export {
116-
stream,
117-
getStreamSource,
11893
parseSchema,
11994
getSchemaPaths,
12095
getSimplifiedSchema,

src/stream.ts

Lines changed: 0 additions & 57 deletions
This file was deleted.

test/no-node.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import assert from 'assert';
2+
import vm from 'vm';
3+
import fs from 'fs';
4+
import path from 'path';
5+
6+
function createMockModuleSystem() {
7+
const context = vm.createContext(Object.create(null));
8+
class Module {
9+
exports = {};
10+
}
11+
const modules = new Map<string, Module>();
12+
// Tiny (incomplete) CommonJS module system mock
13+
function makeRequire(basename: string) {
14+
return function require(identifier: string): any {
15+
if (!identifier.startsWith('./') && !identifier.startsWith('../') && !path.isAbsolute(identifier)) {
16+
let current = path.dirname(basename);
17+
let previous: string;
18+
do {
19+
const nodeModulesEntry = path.resolve(current, 'node_modules', identifier);
20+
previous = current;
21+
current = path.dirname(current);
22+
if (fs.existsSync(nodeModulesEntry)) {
23+
return require(nodeModulesEntry);
24+
}
25+
} while (previous !== current);
26+
throw new Error(`mock require() does not support Node.js built-ins (${identifier})`);
27+
}
28+
let file = path.resolve(path.dirname(basename), identifier);
29+
if (!fs.existsSync(file) && fs.existsSync(`${file}.js`)) {
30+
file = `${file}.js`;
31+
} else if (fs.statSync(file).isDirectory()) {
32+
if (fs.existsSync(`${file}/package.json`)) {
33+
const pjson = JSON.parse(fs.readFileSync(`${file}/package.json`, 'utf8'));
34+
file = path.resolve(file, pjson.main || 'index.js');
35+
} else if (fs.existsSync(`${file}/index.js`)) {
36+
file = path.resolve(file, 'index.js');
37+
}
38+
}
39+
const existing = modules.get(file);
40+
if (existing) {
41+
return existing.exports;
42+
}
43+
const module = new Module();
44+
const source = fs.readFileSync(file);
45+
vm.runInContext(`(function(require, module, exports, __filename, __dirname) {\n${source}\n})`, context)(
46+
makeRequire(file), module, module.exports, file, path.dirname(file)
47+
);
48+
modules.set(file, module);
49+
return module.exports;
50+
};
51+
}
52+
return makeRequire;
53+
}
54+
55+
describe('getSchema should work in plain JS environment without Node.js or browser dependencies', function() {
56+
const docs = [
57+
{ foo: 'bar' },
58+
{ country: 'Croatia' },
59+
{ country: 'Croatia' },
60+
{ country: 'England' }
61+
];
62+
63+
it('Check if return value is a promise', async function() {
64+
const makeRequire = createMockModuleSystem();
65+
66+
const { parseSchema } = makeRequire(__filename)('../lib/index.js') as typeof import('..');
67+
68+
const result = await parseSchema(docs);
69+
assert.strictEqual(result.count, 4);
70+
assert.strictEqual(result.fields.map(f => f.name).join(','), 'country,foo');
71+
});
72+
});

test/stream.test.ts

Lines changed: 0 additions & 41 deletions
This file was deleted.

0 commit comments

Comments
 (0)