Skip to content

Commit 3340340

Browse files
committed
Generate TypeScript from MongoDBJSONSchema or JSON Schemas
1 parent 7f25c5e commit 3340340

File tree

4 files changed

+281
-2
lines changed

4 files changed

+281
-2
lines changed

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: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { analyzeDocuments, StandardJSONSchema, toTypescriptTypeDefinition } from '.';
2+
3+
import assert from 'assert/strict';
4+
5+
import {
6+
BSONRegExp,
7+
Binary,
8+
Code,
9+
DBRef,
10+
Decimal128,
11+
Double,
12+
Int32,
13+
Long,
14+
MaxKey,
15+
MinKey,
16+
ObjectId,
17+
Timestamp,
18+
UUID,
19+
BSONSymbol
20+
} from 'bson';
21+
22+
import { inspect } from 'util';
23+
24+
const bsonDocuments = [
25+
{
26+
_id: new ObjectId('642d766b7300158b1f22e972'),
27+
double: new Double(1.2), // Double, 1, double
28+
doubleThatIsAlsoAnInteger: new Double(1), // Double, 1, double
29+
string: 'Hello, world!', // String, 2, string
30+
object: { key: 'value' }, // Object, 3, object
31+
array: [1, 2, 3], // Array, 4, array
32+
binData: new Binary(Buffer.from([1, 2, 3])), // Binary data, 5, binData
33+
// Undefined, 6, undefined (deprecated)
34+
objectId: new ObjectId('642d766c7300158b1f22e975'), // ObjectId, 7, objectId
35+
boolean: true, // Boolean, 8, boolean
36+
date: new Date('2023-04-05T13:25:08.445Z'), // Date, 9, date
37+
null: null, // Null, 10, null
38+
regex: new BSONRegExp('pattern', 'i'), // Regular Expression, 11, regex
39+
// DBPointer, 12, dbPointer (deprecated)
40+
javascript: new Code('function() {}'), // JavaScript, 13, javascript
41+
symbol: new BSONSymbol('symbol'), // Symbol, 14, symbol (deprecated)
42+
javascriptWithScope: new Code('function() {}', { foo: 1, bar: 'a' }), // JavaScript code with scope 15 "javascriptWithScope" Deprecated in MongoDB 4.4.
43+
int: new Int32(12345), // 32-bit integer, 16, "int"
44+
timestamp: new Timestamp(new Long('7218556297505931265')), // Timestamp, 17, timestamp
45+
long: new Long('123456789123456789'), // 64-bit integer, 18, long
46+
decimal: new Decimal128(
47+
Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])
48+
), // Decimal128, 19, decimal
49+
minKey: new MinKey(), // Min key, -1, minKey
50+
maxKey: new MaxKey(), // Max key, 127, maxKey
51+
52+
binaries: {
53+
generic: new Binary(Buffer.from([1, 2, 3]), 0), // 0
54+
functionData: new Binary(Buffer.from('//8='), 1), // 1
55+
binaryOld: new Binary(Buffer.from('//8='), 2), // 2
56+
uuidOld: new Binary(Buffer.from('c//SZESzTGmQ6OfR38A11A=='), 3), // 3
57+
uuid: new UUID('AAAAAAAA-AAAA-4AAA-AAAA-AAAAAAAAAAAA'), // 4
58+
md5: new Binary(Buffer.from('c//SZESzTGmQ6OfR38A11A=='), 5), // 5
59+
encrypted: new Binary(Buffer.from('c//SZESzTGmQ6OfR38A11A=='), 6), // 6
60+
compressedTimeSeries: new Binary(
61+
Buffer.from(
62+
'CQCKW/8XjAEAAIfx//////////H/////////AQAAAAAAAABfAAAAAAAAAAEAAAAAAAAAAgAAAAAAAAAHAAAAAAAAAA4AAAAAAAAAAA==',
63+
'base64'
64+
),
65+
7
66+
), // 7
67+
custom: new Binary(Buffer.from('//8='), 128) // 128
68+
},
69+
70+
dbRef: new DBRef('namespace', new ObjectId('642d76b4b7ebfab15d3c4a78')) // not actually a separate type, just a convention
71+
72+
// TODO: what about arrays of objects or arrays of arrays or heterogynous types in general
73+
}
74+
];
75+
76+
// from https://json-schema.org/learn/miscellaneous-examples#complex-object-with-nested-properties
77+
const jsonSchema: StandardJSONSchema = {
78+
$id: 'https://example.com/complex-object.schema.json',
79+
$schema: 'https://json-schema.org/draft/2020-12/schema',
80+
title: 'Complex Object',
81+
type: 'object',
82+
properties: {
83+
name: {
84+
type: 'string'
85+
},
86+
age: {
87+
type: 'integer',
88+
minimum: 0
89+
},
90+
address: {
91+
type: 'object',
92+
properties: {
93+
street: {
94+
type: 'string'
95+
},
96+
city: {
97+
type: 'string'
98+
},
99+
state: {
100+
type: 'string'
101+
},
102+
postalCode: {
103+
type: 'string',
104+
pattern: '\\d{5}'
105+
}
106+
},
107+
required: ['street', 'city', 'state', 'postalCode']
108+
},
109+
hobbies: {
110+
type: 'array',
111+
items: {
112+
type: 'string'
113+
}
114+
}
115+
},
116+
required: ['name', 'age']
117+
};
118+
119+
describe.only('toTypescriptTypeDefinition', function() {
120+
it('converts a MongoDB JSON schema to TypeScript', async function() {
121+
const databaseName = 'myDb';
122+
const collectionName = 'myCollection';
123+
const analyzedDocuments = await analyzeDocuments(bsonDocuments);
124+
const schema = await analyzedDocuments.getMongoDBJsonSchema();
125+
126+
console.log(inspect(schema, { depth: null }));
127+
128+
assert.equal(toTypescriptTypeDefinition(databaseName, collectionName, schema), '');
129+
});
130+
131+
it('converts a standard JSON schema to TypeScript', function() {
132+
const databaseName = 'myDb';
133+
const collectionName = 'myCollection';
134+
135+
console.log(inspect(jsonSchema, { depth: null }));
136+
137+
assert.equal(toTypescriptTypeDefinition(databaseName, collectionName, jsonSchema), '');
138+
});
139+
});

src/to-typescript.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import assert from 'assert';
2+
import type { MongoDBJSONSchema } from './types';
3+
4+
function getBSONType(property: MongoDBJSONSchema): string | string[] | undefined {
5+
return property.bsonType || property.type;
6+
}
7+
8+
function isBSONObjectProperty(property: MongoDBJSONSchema): boolean {
9+
return getBSONType(property) === 'object';
10+
}
11+
12+
function isBSONArrayProperty(property: MongoDBJSONSchema): boolean {
13+
return getBSONType(property) === 'array';
14+
}
15+
16+
function isBSONPrimitive(property: MongoDBJSONSchema): boolean {
17+
return !(isBSONArrayProperty(property) || isBSONObjectProperty(property));
18+
}
19+
20+
function toTypeName(type: string): string {
21+
// JSON Schema types
22+
if (type === 'string') {
23+
return 'string';
24+
}
25+
if (type === 'number' || type === 'integer') {
26+
return 'number';
27+
}
28+
if (type === 'boolean') {
29+
return 'boolean';
30+
}
31+
if (type === 'null') {
32+
return 'null';
33+
}
34+
35+
// BSON types
36+
// see InternalTypeToBsonTypeMap
37+
if (type === 'double') {
38+
return 'bson.Double';
39+
}
40+
if (type === 'binData') {
41+
return 'bson.Binary';
42+
}
43+
if (type === 'objectId') {
44+
return 'bson.ObjectId';
45+
}
46+
if (type === 'bool') {
47+
return 'boolean';
48+
}
49+
if (type === 'date') {
50+
return 'bson.Date';
51+
}
52+
if (type === 'regex') {
53+
return 'bson.BSONRegExp';
54+
}
55+
if (type === 'symbol') {
56+
return 'bson.BSONSymbol';
57+
}
58+
if (type === 'javascript' || type === 'javascriptWithScope') {
59+
return 'bson.Code';
60+
}
61+
if (type === 'int') {
62+
return 'bson.Int32';
63+
}
64+
if (type === 'timestamp') {
65+
return 'bson.Timestamp';
66+
}
67+
if (type === 'long') {
68+
return 'bson.Long';
69+
}
70+
if (type === 'decimal') {
71+
return 'bson.Decimal128';
72+
}
73+
if (type === 'minKey') {
74+
return 'bson.MinKey';
75+
}
76+
if (type === 'maxKey') {
77+
return 'bson.MaxKey';
78+
}
79+
if (type === 'dbPointer') {
80+
return 'bson.DBPointer';
81+
}
82+
if (type === 'undefined') {
83+
return 'undefined';
84+
}
85+
86+
return 'any';
87+
}
88+
89+
function uniqueTypes(property: MongoDBJSONSchema): Set<string> {
90+
const type = getBSONType(property);
91+
return new Set(Array.isArray(type) ? type.map((t) => toTypeName(t)) : [toTypeName(type ?? 'any')]);
92+
}
93+
94+
function indentSpaces(indent: number) {
95+
const spaces = [];
96+
for (let i = 0; i < indent; i++) {
97+
spaces.push(' ');
98+
}
99+
return spaces.join('');
100+
}
101+
102+
function arrayType(types: string[]) {
103+
assert(types.length, 'expected types');
104+
105+
if (types.length === 1) {
106+
return `${types[0]}[]`;
107+
}
108+
return `${types.join(' | ')})[]`;
109+
}
110+
111+
function toTypescriptType(properties: Record<string, MongoDBJSONSchema>, indent: number): string {
112+
const eachFieldDefinition = Object.entries(properties).map(([propertyName, schema]) => {
113+
if (isBSONPrimitive(schema)) {
114+
return `${indentSpaces(indent)}${propertyName}?: ${[...uniqueTypes(schema)].join(' | ')}`;
115+
}
116+
117+
if (isBSONArrayProperty(schema)) {
118+
assert(schema.items, 'expected schema.items');
119+
return `${indentSpaces(indent)}${propertyName}?: ${arrayType([...uniqueTypes(schema.items)])}`;
120+
}
121+
122+
if (isBSONObjectProperty(schema)) {
123+
assert(schema.properties, 'expected schema.properties');
124+
return `${indentSpaces(indent)}${propertyName}?: ${toTypescriptType(schema.properties, indent + 1)}`;
125+
}
126+
127+
assert(false, 'this should not be possible');
128+
});
129+
130+
return `{\n${eachFieldDefinition.join(';\n')}\n${indentSpaces(indent - 1)}}`;
131+
}
132+
export function toTypescriptTypeDefinition(databaseName: string, collectionName: string, schema: MongoDBJSONSchema): string {
133+
assert(schema.properties, 'expected schama.properties');
134+
135+
return `module ${databaseName} {
136+
type ${collectionName} = ${toTypescriptType(schema.properties, 2)};
137+
};`;
138+
}

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { InternalSchema } from '.';
33

44
export type StandardJSONSchema = JSONSchema4;
55

6-
export type MongoDBJSONSchema = Pick<StandardJSONSchema, 'title' | 'required' | 'description'> & {
6+
export type MongoDBJSONSchema = Partial<StandardJSONSchema> & Pick<StandardJSONSchema, 'title' | 'required' | 'description'> & {
77
bsonType?: string | string[];
88
properties?: Record<string, MongoDBJSONSchema>;
99
items?: MongoDBJSONSchema | MongoDBJSONSchema[];

0 commit comments

Comments
 (0)