Skip to content

Commit 03d31dc

Browse files
authored
convert a MongoDBJSONSchema to a TypeScript type snippet (#237)
1 parent 5ca185a commit 03d31dc

File tree

4 files changed

+435
-2
lines changed

4 files changed

+435
-2
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
".esm-wrapper.mjs"
3535
],
3636
"scripts": {
37-
"test": "nyc mocha --timeout 5000 --colors -r ts-node/register test/*.ts test/**/*.ts src/**/*.test.ts",
37+
"test": "nyc mocha --timeout 5000 --colors -r ts-node/register test/*.ts test/**/*.ts src/**/*.{test,spec}.ts",
3838
"test-example-parse-from-file": "ts-node examples/parse-from-file.ts",
3939
"test-example-parse-schema": "ts-node examples/parse-schema.ts",
4040
"test-time": "ts-node ./test/time-testing.ts",

src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { convertInternalToMongodb } from './schema-converters/internalToMongoDB'
2222
import { convertInternalToStandard } from './schema-converters/internalToStandard';
2323
import * as schemaStats from './stats';
2424
import { AnyIterable, StandardJSONSchema, MongoDBJSONSchema, ExpandedJSONSchema } from './types';
25+
import { toTypescriptTypeDefinition } from './to-typescript';
2526

2627
/**
2728
* Analyze documents - schema can be retrieved in different formats.
@@ -94,5 +95,6 @@ export {
9495
getSchemaPaths,
9596
getSimplifiedSchema,
9697
SchemaAnalyzer,
97-
schemaStats
98+
schemaStats,
99+
toTypescriptTypeDefinition
98100
};

src/to-typescript.spec.ts

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import type { MongoDBJSONSchema, StandardJSONSchema } from './types';
2+
import { analyzeDocuments } from './index';
3+
import { toTypescriptTypeDefinition } from './to-typescript';
4+
5+
import assert from 'assert/strict';
6+
7+
import {
8+
BSONRegExp,
9+
Binary,
10+
Code,
11+
DBRef,
12+
Decimal128,
13+
Double,
14+
Int32,
15+
Long,
16+
MaxKey,
17+
MinKey,
18+
ObjectId,
19+
Timestamp,
20+
UUID,
21+
BSONSymbol
22+
} from 'bson';
23+
24+
import { inspect } from 'util';
25+
26+
function convertAndCompare(schema: MongoDBJSONSchema, expected: string) {
27+
const code = toTypescriptTypeDefinition(schema);
28+
29+
try {
30+
assert.equal(code, expected);
31+
} catch (err: any) {
32+
// While the diff you get when this fails is handy, it is much easier to
33+
// just copy/paste the actual code to the expected result once you've
34+
// confirmed it is correct. Also it is nice to be able to see what schema
35+
// you're working with.
36+
37+
// eslint-disable-next-line no-console
38+
console.log(inspect(schema, { depth: null }));
39+
40+
// eslint-disable-next-line no-console
41+
console.log(code);
42+
43+
throw err;
44+
}
45+
}
46+
47+
describe('toTypescriptTypeDefinition', function() {
48+
it('converts a MongoDB JSON schema to TypeScript', async function() {
49+
const docs = [
50+
{
51+
_id: new ObjectId('642d766b7300158b1f22e972'),
52+
double: new Double(1.2), // Double, 1, double
53+
doubleThatIsAlsoAnInteger: new Double(1), // Double, 1, double
54+
string: 'Hello, world!', // String, 2, string
55+
object: { key: 'value' }, // Object, 3, object
56+
array: [1, 2, 3], // Array, 4, array
57+
binData: new Binary(Buffer.from([1, 2, 3])), // Binary data, 5, binData
58+
// Undefined, 6, undefined (deprecated)
59+
objectId: new ObjectId('642d766c7300158b1f22e975'), // ObjectId, 7, objectId
60+
boolean: true, // Boolean, 8, boolean
61+
date: new Date('2023-04-05T13:25:08.445Z'), // Date, 9, date
62+
null: null, // Null, 10, null
63+
regex: new BSONRegExp('pattern', 'i'), // Regular Expression, 11, regex
64+
// DBPointer, 12, dbPointer (deprecated)
65+
javascript: new Code('function() {}'), // JavaScript, 13, javascript
66+
symbol: new BSONSymbol('symbol'), // Symbol, 14, symbol (deprecated)
67+
javascriptWithScope: new Code('function() {}', { foo: 1, bar: 'a' }), // JavaScript code with scope 15 "javascriptWithScope" Deprecated in MongoDB 4.4.
68+
int: new Int32(12345), // 32-bit integer, 16, "int"
69+
timestamp: new Timestamp(new Long('7218556297505931265')), // Timestamp, 17, timestamp
70+
long: new Long('123456789123456789'), // 64-bit integer, 18, long
71+
decimal: new Decimal128(
72+
Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])
73+
), // Decimal128, 19, decimal
74+
minKey: new MinKey(), // Min key, -1, minKey
75+
maxKey: new MaxKey(), // Max key, 127, maxKey
76+
77+
binaries: {
78+
generic: new Binary(Buffer.from([1, 2, 3]), 0), // 0
79+
functionData: new Binary(Buffer.from('//8='), 1), // 1
80+
binaryOld: new Binary(Buffer.from('//8='), 2), // 2
81+
uuidOld: new Binary(Buffer.from('c//SZESzTGmQ6OfR38A11A=='), 3), // 3
82+
uuid: new UUID('AAAAAAAA-AAAA-4AAA-AAAA-AAAAAAAAAAAA'), // 4
83+
md5: new Binary(Buffer.from('c//SZESzTGmQ6OfR38A11A=='), 5), // 5
84+
encrypted: new Binary(Buffer.from('c//SZESzTGmQ6OfR38A11A=='), 6), // 6
85+
compressedTimeSeries: new Binary(
86+
Buffer.from(
87+
'CQCKW/8XjAEAAIfx//////////H/////////AQAAAAAAAABfAAAAAAAAAAEAAAAAAAAAAgAAAAAAAAAHAAAAAAAAAA4AAAAAAAAAAA==',
88+
'base64'
89+
),
90+
7
91+
), // 7
92+
custom: new Binary(Buffer.from('//8='), 128) // 128
93+
},
94+
95+
dbRef: new DBRef('namespace', new ObjectId('642d76b4b7ebfab15d3c4a78')) // not actually a separate type, just a convention
96+
97+
// TODO: what about arrays of objects or arrays of arrays or heterogynous types in general
98+
}
99+
];
100+
const analyzedDocuments = await analyzeDocuments(docs);
101+
const schema = await analyzedDocuments.getMongoDBJsonSchema();
102+
103+
convertAndCompare(
104+
schema,
105+
`{
106+
_id?: bson.ObjectId;
107+
array?: bson.Double | number)[];
108+
binaries?: {
109+
binaryOld?: bson.Binary;
110+
compressedTimeSeries?: bson.Binary;
111+
custom?: bson.Binary;
112+
encrypted?: bson.Binary;
113+
functionData?: bson.Binary;
114+
generic?: bson.Binary;
115+
md5?: bson.Binary;
116+
uuid?: bson.Binary;
117+
uuidOld?: bson.Binary;
118+
};
119+
binData?: bson.Binary;
120+
boolean?: boolean;
121+
date?: bson.Date;
122+
dbRef?: bson.DBPointer;
123+
decimal?: bson.Decimal128;
124+
double?: bson.Double | number;
125+
doubleThatIsAlsoAnInteger?: bson.Double | number;
126+
int?: bson.Int32 | number;
127+
javascript?: bson.Code;
128+
javascriptWithScope?: bson.Code;
129+
long?: bson.Long | number;
130+
maxKey?: bson.MaxKey;
131+
minKey?: bson.MinKey;
132+
null?: null;
133+
object?: {
134+
key?: string;
135+
};
136+
objectId?: bson.ObjectId;
137+
regex?: bson.BSONRegExp;
138+
string?: string;
139+
symbol?: bson.BSONSymbol;
140+
timestamp?: bson.Timestamp;
141+
}`
142+
);
143+
});
144+
145+
it('converts a standard JSON schema to TypeScript', function() {
146+
// from https://json-schema.org/learn/miscellaneous-examples#complex-object-with-nested-properties
147+
const schema: StandardJSONSchema = {
148+
$id: 'https://example.com/complex-object.schema.json',
149+
$schema: 'https://json-schema.org/draft/2020-12/schema',
150+
title: 'Complex Object',
151+
type: 'object',
152+
properties: {
153+
name: {
154+
type: 'string'
155+
},
156+
age: {
157+
type: 'integer',
158+
minimum: 0
159+
},
160+
address: {
161+
type: 'object',
162+
properties: {
163+
street: {
164+
type: 'string'
165+
},
166+
city: {
167+
type: 'string'
168+
},
169+
state: {
170+
type: 'string'
171+
},
172+
postalCode: {
173+
type: 'string',
174+
pattern: '\\d{5}'
175+
}
176+
},
177+
required: ['street', 'city', 'state', 'postalCode']
178+
},
179+
hobbies: {
180+
type: 'array',
181+
items: {
182+
type: 'string'
183+
}
184+
}
185+
},
186+
required: ['name', 'age']
187+
};
188+
189+
convertAndCompare(
190+
schema,
191+
`{
192+
name?: string;
193+
age?: number;
194+
address?: {
195+
street?: string;
196+
city?: string;
197+
state?: string;
198+
postalCode?: string;
199+
};
200+
hobbies?: string[];
201+
}`
202+
);
203+
});
204+
205+
it('deals with inconsistent types', async function() {
206+
const docs = [
207+
{
208+
a: 1
209+
},
210+
{
211+
a: 'foo'
212+
},
213+
{
214+
a: true
215+
},
216+
{
217+
a: null
218+
}
219+
];
220+
221+
const analyzedDocuments = await analyzeDocuments(docs);
222+
const schema = await analyzedDocuments.getMongoDBJsonSchema();
223+
224+
convertAndCompare(
225+
schema,
226+
`{
227+
a?: bson.Double | number | string | boolean | null;
228+
}`
229+
);
230+
});
231+
232+
it('deals with nested arrays', async function() {
233+
const docs = [
234+
{
235+
a: [['foo']],
236+
b: [[{ b: 'foo' }]]
237+
}
238+
];
239+
240+
const analyzedDocuments = await analyzeDocuments(docs);
241+
const schema = await analyzedDocuments.getMongoDBJsonSchema();
242+
243+
convertAndCompare(
244+
schema,
245+
`{
246+
a?: any[];
247+
b?: any[];
248+
}`
249+
);
250+
});
251+
252+
it('deals with nested objects', async function() {
253+
const docs = [
254+
{
255+
a: {
256+
foo: { bar: 'baz' }
257+
},
258+
b: [
259+
{
260+
foo: { bar: 'baz' }
261+
}
262+
]
263+
}
264+
];
265+
266+
const analyzedDocuments = await analyzeDocuments(docs);
267+
const schema = await analyzedDocuments.getMongoDBJsonSchema();
268+
269+
convertAndCompare(
270+
schema,
271+
`{
272+
a?: {
273+
foo?: {
274+
bar?: string;
275+
};
276+
};
277+
b?: any[];
278+
}`
279+
);
280+
});
281+
});

0 commit comments

Comments
 (0)