├── .babelrc
├── .eslintrc.yml
├── .gitignore
├── .travis.yml
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── bin
└── ms2tsi
├── package.json
├── src
├── generate-interface.ts
├── generate-module.ts
├── ms2tsi.ts
└── utilities.ts
├── test
├── generate-interface.spec.ts
├── generate-module.spec.ts
├── schemas
│ ├── basic.schema.js
│ ├── es2015.schema.js
│ └── wrapped.schema.js
└── utilities.spec.ts
├── tsconfig.json
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | { "presets": ["es2015"] }
2 |
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | root: true
2 | parser: 'typescript-eslint-parser'
3 | parserOptions:
4 | sourceType: 'module'
5 | rules:
6 | semi: ['error', 'always']
7 | quotes: ['error', 'single']
8 | comma-dangle: ['error', {
9 | arrays: 'always-multiline',
10 | objects: 'always-multiline',
11 | imports: 'always-multiline',
12 | exports: 'always-multiline',
13 | functions: 'always-multiline'
14 | }]
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # node-waf configuration
21 | .lock-wscript
22 |
23 | # Compiled binary addons (http://nodejs.org/api/addons.html)
24 | build/Release
25 |
26 | # Dependency directory
27 | node_modules
28 |
29 | # Optional npm cache directory
30 | .npm
31 |
32 | # Optional REPL history
33 | .node_repl_history
34 |
35 | .DS_Store
36 |
37 | # Generated dist directory containing the final library code
38 | dist
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | notifications:
3 | email: false
4 | language: node_js
5 | cache:
6 | yarn: true
7 | directories:
8 | - node_modules
9 | node_js:
10 | - '9'
11 | - '8'
12 | before_install: yarn global add greenkeeper-lockfile@1
13 | before_script: greenkeeper-lockfile-update
14 | script:
15 | - yarn test
16 | - yarn build
17 | after_script: greenkeeper-lockfile-upload
18 | after_success:
19 | - yarn semantic-release
20 | branches:
21 | only:
22 | - master
23 | - /^greenkeeper/.*$/
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "prettier.eslintIntegration": true
4 | }
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 James Henry
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Mongoose Schema to TypeScript Interface
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | **[Not yet ready for use]** Generates TypeScript interfaces from Mongoose Schemas
20 |
--------------------------------------------------------------------------------
/bin/ms2tsi:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | require('../dist/ms2tsi.js')
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mongoose-schema-to-typescript-interface",
3 | "version": "0.0.0-semantically-released",
4 | "description": "Generates TypeScript interfaces from Mongoose Schemas",
5 | "main": "./dist/generate-interface.js",
6 | "scripts": {
7 | "test": "npm run build && jest",
8 | "build": "rm -rf dist/ && tsc",
9 | "semantic-release":
10 | "semantic-release pre && npm publish && semantic-release post",
11 | "precommit": "npm test && lint-staged",
12 | "cz": "git-cz"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url":
17 | "https://github.com/JamesHenry/mongoose-schema-to-typescript-interface.git"
18 | },
19 | "keywords": ["mongoose", "typescript", "schema", "interface"],
20 | "author": "James Henry ",
21 | "license": "MIT",
22 | "bugs": {
23 | "url":
24 | "https://github.com/JamesHenry/mongoose-schema-to-typescript-interface/issues"
25 | },
26 | "homepage":
27 | "https://github.com/JamesHenry/mongoose-schema-to-typescript-interface#readme",
28 | "devDependencies": {
29 | "@types/jest": "21.1.8",
30 | "@types/mongoose": "4.7.29",
31 | "@types/node": "8.0.58",
32 | "cz-conventional-changelog": "2.1.0",
33 | "eslint": "4.13.1",
34 | "husky": "0.14.3",
35 | "jest": "21.2.1",
36 | "lint-staged": "6.0.0",
37 | "prettier-eslint-cli": "4.4.2",
38 | "semantic-release": "8.2.0",
39 | "ts-jest": "21.2.4",
40 | "typescript": "2.6.2",
41 | "typescript-eslint-parser": "10.0.0"
42 | },
43 | "lint-staged": {
44 | "src/**/*": ["prettier-eslint --write", "git add"],
45 | "test/**/*": ["prettier-eslint --write", "git add"]
46 | },
47 | "config": {
48 | "commitizen": {
49 | "path": "./node_modules/cz-conventional-changelog"
50 | }
51 | },
52 | "bin": {
53 | "ms2tsi": "./bin/ms2tsi"
54 | },
55 | "dependencies": {
56 | "babel-preset-es2015": "6.24.1",
57 | "babel-register": "6.26.0",
58 | "commander": "2.12.2",
59 | "lodash.camelcase": "4.3.0",
60 | "lodash.upperfirst": "4.3.1",
61 | "mongoose": "4.13.7"
62 | },
63 | "jest": {
64 | "transform": {
65 | "^.+\\.tsx?$": "ts-jest"
66 | },
67 | "testRegex": "(/test/.*|(\\.|/)(test|spec))\\.ts$",
68 | "moduleFileExtensions": ["ts", "js", "json"]
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/generate-interface.ts:
--------------------------------------------------------------------------------
1 | import { VirtualType, Schema } from 'mongoose';
2 |
3 | import {
4 | TYPESCRIPT_TYPES,
5 | MONGOOSE_SCHEMA_TYPES,
6 | INTERFACE_PREFIX,
7 | REF_PATH_DELIMITER,
8 | appendNewline,
9 | indent,
10 | typeArrayOf,
11 | } from './utilities';
12 |
13 | const camelCase = require('lodash.camelcase');
14 | const upperFirst = require('lodash.upperfirst');
15 |
16 | /**
17 | * Format a given nested interface name
18 | * @private
19 | */
20 | function formatNestedInterfaceName(name: string): string {
21 | return upperFirst(camelCase(name));
22 | }
23 |
24 | /**
25 | * Return true if the given mongoose field config is a nested schema object
26 | * @private
27 | */
28 | function isNestedSchemaType(fieldConfig: any): boolean {
29 | return typeof fieldConfig === 'object' && !fieldConfig.type;
30 | }
31 |
32 | /**
33 | * Return true if the given mongoose field config is an array of nested schema objects
34 | * @private
35 | */
36 | function isNestedSchemaArrayType(fieldConfig: any): boolean {
37 | return (
38 | Array.isArray(fieldConfig.type) &&
39 | fieldConfig.type.every((nestedConfig: any) =>
40 | isNestedSchemaType(nestedConfig),
41 | )
42 | );
43 | }
44 |
45 | /**
46 | * Return true if the given mongoose field config is an instance of VirtualType
47 | * @private
48 | */
49 | function isVirtualType(fieldConfig: any): boolean {
50 | // In some cases fieldConfig will not pass true for instanceof, do some additional duck typing
51 | const looksLikeVirtualType =
52 | fieldConfig &&
53 | fieldConfig.path &&
54 | Array.isArray(fieldConfig.getters) &&
55 | Array.isArray(fieldConfig.setters) &&
56 | typeof fieldConfig.options === 'object';
57 | return fieldConfig instanceof VirtualType || looksLikeVirtualType;
58 | }
59 |
60 | /**
61 | * Return true if the given mongoose field config has enum values
62 | * @private
63 | */
64 | function hasEnumValues(fieldConfig: any): boolean {
65 | return fieldConfig.enum && fieldConfig.enum.length;
66 | }
67 |
68 | /**
69 | * If the provided schema has already been instantiated with mongoose,
70 | * use the `tree` definition as the schema config
71 | * @private
72 | */
73 | function getSchemaConfig(rawSchema: any): any {
74 | // In some cases rawSchema will not pass true for instanceof, do some additional duck typing
75 | if (rawSchema instanceof Schema || rawSchema.tree) {
76 | return rawSchema.tree;
77 | }
78 | return rawSchema;
79 | }
80 |
81 | /**
82 | * Convert an array of strings into a stringified TypeScript string literal type
83 | * @private
84 | */
85 | function generateStringLiteralTypeFromEnum(enumOptions: string[]): string {
86 | let stringLiteralStr = '';
87 |
88 | enumOptions.forEach((option, index) => {
89 | stringLiteralStr += `'${option}'`;
90 |
91 | if (index !== enumOptions.length - 1) {
92 | stringLiteralStr += ' | ';
93 | }
94 | });
95 |
96 | return stringLiteralStr;
97 | }
98 |
99 | /**
100 | * Return a string representing the determined TypeScript type, if supported,
101 | * otherwise return the value of TYPESCRIPT_TYPES.UNSUPPORTED
102 | * @private
103 | */
104 | function determineSupportedType(mongooseType: any): string {
105 | if (!mongooseType) {
106 | return TYPESCRIPT_TYPES.UNSUPPORTED;
107 | }
108 |
109 | switch (true) {
110 | case mongooseType === String:
111 | return TYPESCRIPT_TYPES.STRING;
112 |
113 | case mongooseType.schemaName === MONGOOSE_SCHEMA_TYPES.OBJECT_ID:
114 | case mongooseType.name === MONGOOSE_SCHEMA_TYPES.OBJECT_ID:
115 | return 'OBJECT_ID';
116 |
117 | case mongooseType === Number:
118 | return TYPESCRIPT_TYPES.NUMBER;
119 |
120 | case mongooseType.schemaName === MONGOOSE_SCHEMA_TYPES.MIXED:
121 | return TYPESCRIPT_TYPES.OBJECT_LITERAL;
122 |
123 | case mongooseType === Date:
124 | return TYPESCRIPT_TYPES.DATE;
125 |
126 | case mongooseType === Boolean:
127 | return TYPESCRIPT_TYPES.BOOLEAN;
128 |
129 | case Array.isArray(mongooseType) === true:
130 | return TYPESCRIPT_TYPES.ARRAY;
131 |
132 | case typeof mongooseType === 'object' &&
133 | Object.keys(mongooseType).length > 0:
134 | return TYPESCRIPT_TYPES.SCHEMA;
135 |
136 | default:
137 | return TYPESCRIPT_TYPES.UNSUPPORTED;
138 | }
139 | }
140 |
141 | /**
142 | * Predicate function to filter out invalid fields from a schema object
143 | * @private
144 | */
145 | function filterOutInvalidFields(fieldName: string) {
146 | return fieldName !== '0' && fieldName !== '1';
147 | }
148 |
149 | /**
150 | * Generate a field value (type definition) for a particular TypeScript interface
151 | * @private
152 | */
153 | function generateInterfaceFieldValue(
154 | supportedType: string,
155 | fieldConfig: any,
156 | ): string {
157 | switch (supportedType) {
158 | /**
159 | * Single values
160 | */
161 | case TYPESCRIPT_TYPES.NUMBER:
162 | case TYPESCRIPT_TYPES.OBJECT_LITERAL:
163 | case TYPESCRIPT_TYPES.DATE:
164 | case TYPESCRIPT_TYPES.BOOLEAN:
165 | return supportedType;
166 |
167 | /**
168 | * Strings and string literals
169 | */
170 | case TYPESCRIPT_TYPES.STRING:
171 | if (hasEnumValues(fieldConfig)) {
172 | return generateStringLiteralTypeFromEnum(fieldConfig.enum);
173 | }
174 | return supportedType;
175 | }
176 | return '';
177 | }
178 |
179 | /**
180 | * For the `rawSchema`, generate a TypeScript interface under the given `interfaceName`,
181 | * and any requisite nested interfaces
182 | * @public
183 | */
184 | export default function typescriptInterfaceGenerator(
185 | interfaceName: string,
186 | rawSchema: any,
187 | refMapping: any = {},
188 | ): string {
189 | let generatedContent = '';
190 |
191 | function generateInterface(name: string, fromSchema: any) {
192 | const fields = Object.keys(fromSchema).filter(filterOutInvalidFields);
193 | let interfaceString = `interface ${INTERFACE_PREFIX}${name} {`;
194 |
195 | if (fields.length) {
196 | interfaceString = appendNewline(interfaceString);
197 | }
198 |
199 | fields.forEach(fieldName => {
200 | const fieldConfig = fromSchema[fieldName];
201 |
202 | // VirtualType fields are not supported yet
203 | if (isVirtualType(fieldConfig)) {
204 | return null;
205 | }
206 |
207 | interfaceString += indent(fieldName);
208 |
209 | let supportedType: string;
210 |
211 | if (isNestedSchemaType(fieldConfig)) {
212 | supportedType = TYPESCRIPT_TYPES.OBJECT_LITERAL;
213 | } else {
214 | supportedType = determineSupportedType(fieldConfig.type);
215 | }
216 |
217 | /**
218 | * Unsupported type
219 | */
220 | if (supportedType === TYPESCRIPT_TYPES.UNSUPPORTED) {
221 | throw new Error(
222 | `Mongoose type not recognised/supported: ${JSON.stringify(
223 | fieldConfig,
224 | )}`,
225 | );
226 | }
227 |
228 | let interfaceVal: string = '';
229 |
230 | /**
231 | * Nested schema type
232 | */
233 | if (supportedType === TYPESCRIPT_TYPES.OBJECT_LITERAL) {
234 | if (
235 | fieldConfig.type &&
236 | fieldConfig.type.schemaName === MONGOOSE_SCHEMA_TYPES.MIXED
237 | ) {
238 | interfaceVal = '{}';
239 | } else {
240 | const nestedInterfaceName =
241 | formatNestedInterfaceName(name) +
242 | INTERFACE_PREFIX +
243 | formatNestedInterfaceName(fieldName);
244 | const nestedSchemaConfig = getSchemaConfig(fieldConfig);
245 | const nestedInterface = generateInterface(
246 | nestedInterfaceName,
247 | nestedSchemaConfig,
248 | );
249 |
250 | generatedContent += appendNewline(nestedInterface);
251 |
252 | interfaceVal = INTERFACE_PREFIX + nestedInterfaceName;
253 | }
254 | } else if (supportedType === TYPESCRIPT_TYPES.ARRAY) {
255 | /**
256 | * Empty array
257 | */
258 | if (!fieldConfig.type.length) {
259 | interfaceVal = typeArrayOf(TYPESCRIPT_TYPES.ANY);
260 | } else if (isNestedSchemaArrayType(fieldConfig)) {
261 | const nestedSchemaConfig = getSchemaConfig(fieldConfig.type[0]);
262 | let nestedSupportedType = determineSupportedType(nestedSchemaConfig);
263 |
264 | if (nestedSupportedType === TYPESCRIPT_TYPES.UNSUPPORTED) {
265 | throw new Error(
266 | `Mongoose type not recognised/supported: ${JSON.stringify(
267 | fieldConfig,
268 | )}`,
269 | );
270 | }
271 |
272 | /**
273 | * Nested Mixed types
274 | */
275 | if (nestedSupportedType === TYPESCRIPT_TYPES.OBJECT_LITERAL) {
276 | interfaceVal = typeArrayOf(
277 | generateInterfaceFieldValue(nestedSupportedType, fieldConfig),
278 | );
279 | } else {
280 | /**
281 | * Array of nested schema types
282 | */
283 | const nestedInterfaceName =
284 | formatNestedInterfaceName(name) +
285 | INTERFACE_PREFIX +
286 | formatNestedInterfaceName(fieldName);
287 | const nestedInterface = generateInterface(
288 | nestedInterfaceName,
289 | nestedSchemaConfig,
290 | );
291 |
292 | generatedContent += appendNewline(nestedInterface);
293 |
294 | interfaceVal = typeArrayOf(INTERFACE_PREFIX + nestedInterfaceName);
295 | }
296 | } else {
297 | /**
298 | * Array of single value types
299 | */
300 | let nestedSupportedType = determineSupportedType(fieldConfig.type[0]);
301 | if (nestedSupportedType === TYPESCRIPT_TYPES.UNSUPPORTED) {
302 | throw new Error(
303 | `Mongoose type not recognised/supported: ${JSON.stringify(
304 | fieldConfig,
305 | )}`,
306 | );
307 | }
308 |
309 | if (nestedSupportedType === 'OBJECT_ID') {
310 | if (fieldConfig.ref) {
311 | refMapping[
312 | INTERFACE_PREFIX + name + REF_PATH_DELIMITER + fieldName
313 | ] =
314 | fieldConfig.ref;
315 | }
316 | nestedSupportedType = TYPESCRIPT_TYPES.STRING;
317 | }
318 |
319 | interfaceVal = typeArrayOf(
320 | generateInterfaceFieldValue(nestedSupportedType, fieldConfig),
321 | );
322 | }
323 | } else {
324 | if (supportedType === 'OBJECT_ID') {
325 | if (fieldConfig.ref) {
326 | refMapping[
327 | INTERFACE_PREFIX + name + REF_PATH_DELIMITER + fieldName
328 | ] =
329 | fieldConfig.ref;
330 | }
331 | supportedType = TYPESCRIPT_TYPES.STRING;
332 | }
333 |
334 | /**
335 | * Single value types
336 | */
337 | interfaceVal = generateInterfaceFieldValue(supportedType, fieldConfig);
338 | }
339 |
340 | if (
341 | !isNestedSchemaType(fieldConfig) &&
342 | !isNestedSchemaArrayType(fieldConfig) &&
343 | !fieldConfig.required
344 | ) {
345 | interfaceString += TYPESCRIPT_TYPES.OPTIONAL_PROP;
346 | }
347 |
348 | interfaceString += `: ${interfaceVal}`;
349 |
350 | interfaceString += ';';
351 |
352 | interfaceString = appendNewline(interfaceString);
353 | });
354 |
355 | interfaceString += appendNewline('}');
356 |
357 | return interfaceString;
358 | }
359 |
360 | const schemaConfig = getSchemaConfig(rawSchema);
361 | const mainInterface = generateInterface(interfaceName, schemaConfig);
362 |
363 | generatedContent += appendNewline(mainInterface);
364 |
365 | return generatedContent;
366 | }
367 |
--------------------------------------------------------------------------------
/src/generate-module.ts:
--------------------------------------------------------------------------------
1 | import { indentEachLine, appendNewline } from './utilities';
2 |
3 | /**
4 | * Wrap the given stringified contents in a stringified TypeScript module,
5 | * using the given module name
6 | * @public
7 | */
8 | export default function typescriptModuleGenerator(
9 | moduleName: string,
10 | moduleContents: string,
11 | ) {
12 | if (!moduleName) {
13 | throw new Error('"moduleName" is required to generate a TypeScript module');
14 | }
15 | let typescriptModule = appendNewline(`declare module ${moduleName} {`);
16 | typescriptModule += indentEachLine(moduleContents);
17 | typescriptModule += appendNewline('}');
18 | return typescriptModule;
19 | }
20 |
--------------------------------------------------------------------------------
/src/ms2tsi.ts:
--------------------------------------------------------------------------------
1 | import * as program from 'commander';
2 | import * as fs from 'fs';
3 | import * as path from 'path';
4 |
5 | import { generateOutput } from './utilities';
6 |
7 | /**
8 | * Use Babel to transpile schema files
9 | */
10 | import 'babel-register';
11 |
12 | program.on('--help', () => {
13 | console.log(' Examples:');
14 | console.log('');
15 | console.log(' $ ms2tsi -h');
16 | console.log(
17 | ' $ ms2tsi generate --module-name myModule --output-dir ./interfaces ./**/schema.js',
18 | );
19 | console.log('');
20 | });
21 |
22 | program
23 | .command('generate [schemas...]')
24 | .option(
25 | '-o, --output-dir ',
26 | 'Where should the generated .d.ts file be written to?',
27 | )
28 | .option(
29 | '-m, --module-name ',
30 | 'What should the TypeScript module and filename be?',
31 | )
32 | .option('-e, --extend-refs', 'Experimental: Should refs be extended?')
33 | .action((schemas: string[], cmd: any) => {
34 | const { outputDir, moduleName, extendRefs } = cmd;
35 |
36 | if (!outputDir) {
37 | console.error('An output directory is required. Use -o or --output-dir');
38 | return process.exit(1);
39 | }
40 |
41 | if (!moduleName) {
42 | console.error('A module name is required. Use -m or --module-name');
43 | return process.exit(1);
44 | }
45 |
46 | if (!schemas || !schemas.length) {
47 | console.error(
48 | 'No schema files could be found. Please check the required usage by running `ms2tsi -h`',
49 | );
50 | return process.exit(1);
51 | }
52 |
53 | const currentDir = process.env.PWD as string;
54 | const resolvedSchemaFiles = schemas.map((schemaPath: string) => {
55 | const schemaFile = require(path.resolve(currentDir, schemaPath));
56 | /**
57 | * Allow for vanilla objects or full mongoose.Schema instances to
58 | * have been exported by normalising the exported `schema` property
59 | */
60 | if (schemaFile.schema && schemaFile.schema.tree) {
61 | schemaFile.schema = schemaFile.schema.tree;
62 | return schemaFile;
63 | }
64 | return schemaFile;
65 | });
66 |
67 | const output = generateOutput(
68 | moduleName,
69 | currentDir,
70 | resolvedSchemaFiles,
71 | extendRefs,
72 | );
73 |
74 | fs.writeFileSync(
75 | path.resolve(currentDir, `${outputDir}/${moduleName}.d.ts`),
76 | output,
77 | );
78 | });
79 |
80 | program.parse(process.argv);
81 |
--------------------------------------------------------------------------------
/src/utilities.ts:
--------------------------------------------------------------------------------
1 | import generateModule from './generate-module';
2 | import generateInterface from './generate-interface';
3 |
4 | /**
5 | * Library constants
6 | */
7 | export const INDENT_CHAR = '\t';
8 | export const NEWLINE_CHAR = '\n';
9 | export const INTERFACE_PREFIX = 'I';
10 | export const REF_PATH_DELIMITER = '::';
11 | export const TYPESCRIPT_TYPES = {
12 | STRING: 'string',
13 | NUMBER: 'number',
14 | BOOLEAN: 'boolean',
15 | ARRAY: 'Array',
16 | DATE: 'Date',
17 | OBJECT_LITERAL: '{}',
18 | ANY: 'any',
19 | ARRAY_THEREOF: '[]',
20 | OPTIONAL_PROP: '?',
21 | UNSUPPORTED: 'Unsupported',
22 | SCHEMA: 'SCHEMA',
23 | };
24 | export const MONGOOSE_SCHEMA_TYPES = {
25 | OBJECT_ID: 'ObjectId',
26 | MIXED: 'Mixed',
27 | };
28 |
29 | export function typeArrayOf(str: string): string {
30 | return `Array<${str}>`;
31 | }
32 |
33 | /**
34 | * Append the newline character to a given string
35 | */
36 | export function appendNewline(str: string): string {
37 | return `${str}${NEWLINE_CHAR}`;
38 | }
39 |
40 | /**
41 | * Prepend a given string with the indentation character
42 | */
43 | export function indent(str: string): string {
44 | return `${INDENT_CHAR}${str}`;
45 | }
46 |
47 | /**
48 | * Split on the newline character and prepend each of the
49 | * resulting strings with the indentation character
50 | */
51 | export function indentEachLine(content: string): string {
52 | return content
53 | .split(NEWLINE_CHAR)
54 | .map(line => {
55 | /**
56 | * Do not indent a line which purely consists of
57 | * a newline character
58 | */
59 | if (line.length) {
60 | return indent(line);
61 | }
62 |
63 | return line;
64 | })
65 | .join(NEWLINE_CHAR);
66 | }
67 |
68 | export function generateOutput(
69 | moduleName: string,
70 | _currentDir: string,
71 | schemaFiles: any[],
72 | extendRefs: boolean = false,
73 | ): string {
74 | let output = '';
75 | const refMapping = {};
76 |
77 | for (const schemaFile of schemaFiles) {
78 | const interfaceName = schemaFile.name;
79 | const schemaTree = schemaFile.schema;
80 |
81 | if (!interfaceName) {
82 | throw new Error(`Schema file does not export a 'name': ${schemaFile}`);
83 | }
84 |
85 | output += generateInterface(interfaceName, schemaTree, refMapping);
86 | }
87 |
88 | if (extendRefs) {
89 | output = extendRefTypes(output, refMapping);
90 | }
91 |
92 | output = generateModule(moduleName, output);
93 |
94 | return output;
95 | }
96 |
97 | function stripInterface(str: string): string {
98 | return str.replace('interface ', '').replace(' {', '');
99 | }
100 |
101 | export function extendRefTypes(
102 | generatedOutput: string,
103 | refMapping: any = {},
104 | ): string {
105 | const refPaths = Object.keys(refMapping);
106 | if (!refPaths || !refPaths.length) {
107 | return generatedOutput;
108 | }
109 |
110 | let updatedOutput = generatedOutput;
111 |
112 | refPaths.forEach(refPath => {
113 | const [interfaceName, propertyName] = refPath.split(REF_PATH_DELIMITER);
114 | const refValue = refMapping[refPath];
115 | const targetInterfaceStartRegexp = new RegExp(
116 | `interface ${interfaceName} {`,
117 | );
118 |
119 | /**
120 | * Check the given output for an applicable interface for the refValue
121 | */
122 | const refInterfaceStartRegexp = new RegExp(
123 | `interface ${INTERFACE_PREFIX}${refValue} {`,
124 | );
125 | const foundInterface = updatedOutput.match(refInterfaceStartRegexp);
126 | if (!foundInterface) {
127 | return null;
128 | }
129 |
130 | const matchingReferencedInterfaceName: string = stripInterface(
131 | foundInterface[0],
132 | );
133 |
134 | /**
135 | * Split the given output by line
136 | */
137 | const outputLines = updatedOutput.split('\n');
138 |
139 | /**
140 | * Locate the start of the target interface
141 | */
142 | let startIndexOfTargetInterface: number;
143 | for (let i = 0; i < outputLines.length; i++) {
144 | if (outputLines[i].match(targetInterfaceStartRegexp)) {
145 | startIndexOfTargetInterface = i;
146 | break;
147 | }
148 | }
149 |
150 | // @ts-ignore
151 | if (typeof startIndexOfTargetInterface !== 'number') {
152 | return null;
153 | }
154 |
155 | let endIndexOfTargetInterface: number;
156 | for (let i = startIndexOfTargetInterface; i < outputLines.length; i++) {
157 | if (outputLines[i].indexOf('}') > -1) {
158 | endIndexOfTargetInterface = i;
159 | break;
160 | }
161 | }
162 |
163 | const refPropertyRegexp = new RegExp(`${propertyName}: string;`);
164 | const updatedLines = outputLines.map((line, index) => {
165 | if (
166 | index > startIndexOfTargetInterface &&
167 | index < endIndexOfTargetInterface
168 | ) {
169 | const targetFieldDefinition = line.match(refPropertyRegexp);
170 | if (targetFieldDefinition) {
171 | return line.replace(
172 | 'string;',
173 | `string | ${matchingReferencedInterfaceName};`,
174 | );
175 | }
176 | }
177 |
178 | return line;
179 | });
180 |
181 | updatedOutput = updatedLines.join('\n');
182 | });
183 |
184 | return updatedOutput;
185 | }
186 |
--------------------------------------------------------------------------------
/test/generate-interface.spec.ts:
--------------------------------------------------------------------------------
1 | import { Schema } from 'mongoose';
2 |
3 | import generateInterface from '../src/generate-interface';
4 |
5 | describe('generateInterface', () => {
6 | it('should return a stringified TypeScript interface', () => {
7 | const input = generateInterface('EmptyInterface', {});
8 | const output = `interface IEmptyInterface {}
9 |
10 | `;
11 |
12 | expect(input).toEqual(output);
13 | });
14 |
15 | it('should convert mongoose \'type: ObjectId\' to TypeScript type \'string\'', () => {
16 | const input = generateInterface('ObjectIdInterface', {
17 | id: {
18 | type: Schema.Types.ObjectId,
19 | required: true,
20 | },
21 | });
22 |
23 | const output = `interface IObjectIdInterface {
24 | id: string;
25 | }
26 |
27 | `;
28 |
29 | expect(input).toEqual(output);
30 | });
31 |
32 | it('should convert mongoose \'required: false\' to TypeScript optional property syntax', () => {
33 | const input = generateInterface('OptionalPropInterface', {
34 | id: {
35 | type: Schema.Types.ObjectId,
36 | required: false,
37 | },
38 | });
39 |
40 | const output = `interface IOptionalPropInterface {
41 | id?: string;
42 | }
43 |
44 | `;
45 |
46 | expect(input).toEqual(output);
47 | });
48 |
49 | it('should convert mongoose \'type: Mixed\' to TypeScript type \'{}\'', () => {
50 | const input = generateInterface('MixedInterface', {
51 | id: {
52 | type: Schema.Types.Mixed,
53 | required: true,
54 | },
55 | });
56 |
57 | const output = `interface IMixedInterface {
58 | id: {};
59 | }
60 |
61 | `;
62 |
63 | expect(input).toEqual(output);
64 | });
65 |
66 | it('should convert mongoose \'type: String\' to TypeScript type \'string\'', () => {
67 | const input = generateInterface('NameStringInterface', {
68 | name: {
69 | type: String,
70 | required: true,
71 | },
72 | });
73 |
74 | const output = `interface INameStringInterface {
75 | name: string;
76 | }
77 |
78 | `;
79 |
80 | expect(input).toEqual(output);
81 | });
82 |
83 | it('should convert mongoose \'type: String\' and \'enum: [...]\' to TypeScript \'string literal type\'', () => {
84 | const input = generateInterface('StringOptionsInterface', {
85 | chosen_letter: {
86 | type: String,
87 | enum: ['a', 'b', 'c'],
88 | required: true,
89 | },
90 | });
91 |
92 | const output = `interface IStringOptionsInterface {
93 | chosen_letter: 'a' | 'b' | 'c';
94 | }
95 |
96 | `;
97 |
98 | expect(input).toEqual(output);
99 | });
100 |
101 | it('should convert mongoose \'type: Number\' to TypeScript type \'number\'', () => {
102 | const input = generateInterface('AgeNumberInterface', {
103 | age: {
104 | type: Number,
105 | required: true,
106 | },
107 | });
108 |
109 | const output = `interface IAgeNumberInterface {
110 | age: number;
111 | }
112 |
113 | `;
114 |
115 | expect(input).toEqual(output);
116 | });
117 |
118 | it('should convert mongoose \'type: Number\' to TypeScript type \'number\'', () => {
119 | const input = generateInterface('AgeNumberInterface', {
120 | age: {
121 | type: Number,
122 | required: true,
123 | },
124 | });
125 |
126 | const output = `interface IAgeNumberInterface {
127 | age: number;
128 | }
129 |
130 | `;
131 |
132 | expect(input).toEqual(output);
133 | });
134 |
135 | it('should convert mongoose \'type: Boolean\' to TypeScript type \'boolean\'', () => {
136 | const input = generateInterface('EnabledBooleanInterface', {
137 | enabled: {
138 | type: Boolean,
139 | required: true,
140 | },
141 | });
142 |
143 | const output = `interface IEnabledBooleanInterface {
144 | enabled: boolean;
145 | }
146 |
147 | `;
148 |
149 | expect(input).toEqual(output);
150 | });
151 |
152 | it('should convert mongoose \'type: Date\' to TypeScript type \'Date\'', () => {
153 | const input = generateInterface('StartInterface', {
154 | start: {
155 | type: Date,
156 | required: true,
157 | },
158 | });
159 |
160 | const output = `interface IStartInterface {
161 | start: Date;
162 | }
163 |
164 | `;
165 |
166 | expect(input).toEqual(output);
167 | });
168 |
169 | it('should convert mongoose \'type: []\' to TypeScript type \'Array\'', () => {
170 | const input = generateInterface('AnyListInterface', {
171 | stuff: {
172 | type: [],
173 | required: true,
174 | },
175 | });
176 |
177 | const output = `interface IAnyListInterface {
178 | stuff: Array;
179 | }
180 |
181 | `;
182 |
183 | expect(input).toEqual(output);
184 | });
185 |
186 | it('should convert mongoose \'type: [Number]\' to TypeScript type \'Array\'', () => {
187 | const input = generateInterface('NumberListInterface', {
188 | list: {
189 | type: [Number],
190 | required: true,
191 | },
192 | });
193 |
194 | const output = `interface INumberListInterface {
195 | list: Array;
196 | }
197 |
198 | `;
199 |
200 | expect(input).toEqual(output);
201 | });
202 |
203 | it('should convert mongoose \'type: [String]\' to TypeScript type \'Array\'', () => {
204 | const input = generateInterface('NameListInterface', {
205 | names: {
206 | type: [String],
207 | required: true,
208 | },
209 | });
210 |
211 | const output = `interface INameListInterface {
212 | names: Array;
213 | }
214 |
215 | `;
216 |
217 | expect(input).toEqual(output);
218 | });
219 |
220 | it('should convert mongoose \'type: [Boolean]\' to TypeScript type \'Array\'', () => {
221 | const input = generateInterface('StatusListInterface', {
222 | statuses: {
223 | type: [Boolean],
224 | required: true,
225 | },
226 | });
227 |
228 | const output = `interface IStatusListInterface {
229 | statuses: Array;
230 | }
231 |
232 | `;
233 |
234 | expect(input).toEqual(output);
235 | });
236 |
237 | it('should convert mongoose \'type: [Date]\' to TypeScript type \'Array\'', () => {
238 | const input = generateInterface('DateListInterface', {
239 | dates: {
240 | type: [Date],
241 | required: true,
242 | },
243 | });
244 |
245 | const output = `interface IDateListInterface {
246 | dates: Array;
247 | }
248 |
249 | `;
250 |
251 | expect(input).toEqual(output);
252 | });
253 |
254 | it('should convert mongoose \'type: [ObjectId]\' to TypeScript type \'Array\'', () => {
255 | const input = generateInterface('ObjectIdListInterface', {
256 | ids: {
257 | type: [Schema.Types.ObjectId],
258 | required: true,
259 | },
260 | });
261 |
262 | const output = `interface IObjectIdListInterface {
263 | ids: Array;
264 | }
265 |
266 | `;
267 |
268 | expect(input).toEqual(output);
269 | });
270 |
271 | it('should convert mongoose \'type: [Mixed]\' to TypeScript type \'Array<{}>\'', () => {
272 | const input = generateInterface('MixedListInterface', {
273 | id: {
274 | type: [Schema.Types.Mixed],
275 | required: true,
276 | },
277 | });
278 |
279 | const output = `interface IMixedListInterface {
280 | id: Array<{}>;
281 | }
282 |
283 | `;
284 |
285 | expect(input).toEqual(output);
286 | });
287 |
288 | it('should dynamically create any nested mongoose schemas as TypeScript interfaces', () => {
289 | const input = generateInterface('MainInterface', {
290 | nested: {
291 | stuff: {
292 | type: String,
293 | required: false,
294 | },
295 | },
296 | });
297 |
298 | const output = `interface IMainInterfaceINested {
299 | stuff?: string;
300 | }
301 |
302 | interface IMainInterface {
303 | nested: IMainInterfaceINested;
304 | }
305 |
306 | `;
307 |
308 | expect(input).toEqual(output);
309 | });
310 |
311 | it('should format nested schema names as TitleCase', () => {
312 | const input = generateInterface('MainInterface', {
313 | snake_case: {
314 | stuff: {
315 | type: String,
316 | required: false,
317 | },
318 | },
319 | });
320 |
321 | const output = `interface IMainInterfaceISnakeCase {
322 | stuff?: string;
323 | }
324 |
325 | interface IMainInterface {
326 | snake_case: IMainInterfaceISnakeCase;
327 | }
328 |
329 | `;
330 |
331 | expect(input).toEqual(output);
332 | });
333 |
334 | it('should support multiple schema fields on newlines', () => {
335 | const input = generateInterface('MultipleFieldsInterface', {
336 | field1: {
337 | type: String,
338 | required: true,
339 | },
340 | field2: {
341 | type: String,
342 | required: true,
343 | },
344 | });
345 |
346 | const output = `interface IMultipleFieldsInterface {
347 | field1: string;
348 | field2: string;
349 | }
350 |
351 | `;
352 |
353 | expect(input).toEqual(output);
354 | });
355 |
356 | it('should support arrays of nested schemas as a field type', () => {
357 | const nested = {
358 | stuff: {
359 | type: String,
360 | required: false,
361 | },
362 | };
363 |
364 | const input = generateInterface('MainInterface', {
365 | multipleNested: {
366 | type: [nested],
367 | },
368 | });
369 |
370 | const output = `interface IMainInterfaceIMultipleNested {
371 | stuff?: string;
372 | }
373 |
374 | interface IMainInterface {
375 | multipleNested: Array;
376 | }
377 |
378 | `;
379 |
380 | expect(input).toEqual(output);
381 | });
382 |
383 | it('should process a complex example', () => {
384 | const nestedSchema = new Schema(
385 | {
386 | thing: {
387 | type: Schema.Types.ObjectId,
388 | ref: 'Thing',
389 | },
390 | },
391 | {},
392 | );
393 |
394 | const nestedItemSchema = new Schema(
395 | {
396 | user: {
397 | type: Schema.Types.ObjectId,
398 | ref: 'User',
399 | required: true,
400 | },
401 | priority: {
402 | type: Number,
403 | required: true,
404 | default: 0,
405 | },
406 | },
407 | {
408 | _id: false,
409 | id: false,
410 | },
411 | );
412 |
413 | const mainSchema: any = new Schema(
414 | {
415 | name: {
416 | type: String,
417 | required: true,
418 | index: true,
419 | placeholder: 'PLACEHOLDER_NAME',
420 | },
421 | referencedDocument: {
422 | type: Schema.Types.ObjectId,
423 | ref: 'RefDoc',
424 | immutable: true,
425 | required: true,
426 | },
427 | stringOptionsWithDefault: {
428 | type: String,
429 | required: true,
430 | default: 'defaultVal',
431 | enum: ['defaultVal', 'Option2'],
432 | },
433 | setting_type: {
434 | type: String,
435 | },
436 | setting_value: {
437 | type: Number,
438 | min: 0,
439 | validate: function validate(val: any) {
440 | if (this.setting_type === 'foo') {
441 | if (val > 1) {
442 | return false;
443 | }
444 | }
445 | return true;
446 | },
447 | },
448 | enabled: {
449 | type: Boolean,
450 | required: true,
451 | default: false,
452 | },
453 | nestedSchema: nestedSchema,
454 | nestedInline: {
455 | prop: {
456 | type: Number,
457 | required: true,
458 | },
459 | },
460 | nestedEmptyInline: {},
461 | nestedItems: {
462 | type: [nestedItemSchema],
463 | },
464 | },
465 | {
466 | strict: true,
467 | },
468 | );
469 |
470 | mainSchema.foo = 'bar';
471 | mainSchema.searchable = true;
472 | mainSchema.index({
473 | name: 'text',
474 | });
475 |
476 | const input = generateInterface('MainInterface', mainSchema);
477 |
478 | const output = `interface IMainInterfaceINestedSchema {
479 | thing?: string;
480 | _id?: string;
481 | }
482 |
483 | interface IMainInterfaceINestedInline {
484 | prop: number;
485 | }
486 |
487 | interface IMainInterfaceINestedEmptyInline {}
488 |
489 | interface IMainInterfaceINestedItems {
490 | user: string;
491 | priority: number;
492 | }
493 |
494 | interface IMainInterface {
495 | name: string;
496 | referencedDocument: string;
497 | stringOptionsWithDefault: 'defaultVal' | 'Option2';
498 | setting_type?: string;
499 | setting_value?: number;
500 | enabled: boolean;
501 | nestedSchema: IMainInterfaceINestedSchema;
502 | nestedInline: IMainInterfaceINestedInline;
503 | nestedEmptyInline: IMainInterfaceINestedEmptyInline;
504 | nestedItems: Array;
505 | _id?: string;
506 | }
507 |
508 | `;
509 |
510 | expect(input).toEqual(output);
511 | });
512 | });
513 |
--------------------------------------------------------------------------------
/test/generate-module.spec.ts:
--------------------------------------------------------------------------------
1 | import generateModule from '../src/generate-module';
2 |
3 | describe('generateModule', () => {
4 | it('should return a stringified TypeScript module', () => {
5 | const input = generateModule('ModuleName', '');
6 | const output = `declare module ModuleName {
7 | }
8 | `;
9 |
10 | expect(input).toEqual(output);
11 | });
12 |
13 | it('should name the TypeScript module based on the given name', () => {
14 | const input = generateModule('GivenModuleName', '');
15 | const output = `declare module GivenModuleName {
16 | }
17 | `;
18 |
19 | expect(input).toEqual(output);
20 | });
21 |
22 | it('should wrap the given stringified content in a stringified TypeScript module', () => {
23 | const input = generateModule(
24 | 'ModuleName',
25 | `interface IEmptyInterface {}
26 | `,
27 | );
28 | const output = `declare module ModuleName {
29 | interface IEmptyInterface {}
30 | }
31 | `;
32 |
33 | expect(input).toEqual(output);
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/test/schemas/basic.schema.js:
--------------------------------------------------------------------------------
1 | exports.name = 'basic';
2 |
3 | exports.schema = {
4 | name: {
5 | type: String,
6 | required: true,
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/test/schemas/es2015.schema.js:
--------------------------------------------------------------------------------
1 | let name = 'es2015';
2 |
3 | exports.name = name;
4 |
5 | exports.schema = {
6 | start: {
7 | type: Date,
8 | required: true,
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/test/schemas/wrapped.schema.js:
--------------------------------------------------------------------------------
1 | const Schema = require('mongoose').Schema;
2 |
3 | const wrappedSchema = new Schema(
4 | {
5 | age: {
6 | type: Number,
7 | },
8 | },
9 | {
10 | strict: true,
11 | },
12 | );
13 |
14 | exports.name = 'wrapped';
15 |
16 | exports.schema = wrappedSchema;
17 |
--------------------------------------------------------------------------------
/test/utilities.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | NEWLINE_CHAR,
3 | INDENT_CHAR,
4 | REF_PATH_DELIMITER,
5 | appendNewline,
6 | indent,
7 | indentEachLine,
8 | extendRefTypes,
9 | } from '../src/utilities';
10 |
11 | describe('utilities', () => {
12 | describe('appendNewline', () => {
13 | it('should append a newline character to the given string', () => {
14 | expect(appendNewline('test string')).toEqual(
15 | 'test string' + NEWLINE_CHAR,
16 | );
17 | });
18 | });
19 |
20 | describe('indent', () => {
21 | it('should prepend an indent character to the given string', () => {
22 | expect(indent('test string')).toEqual(INDENT_CHAR + 'test string');
23 | });
24 | });
25 |
26 | describe('indentEachLine', () => {
27 | it('should prepend an indent character to each line of the given string', () => {
28 | expect(
29 | indentEachLine(`
30 | test1
31 | test2
32 | test3
33 | `),
34 | ).toEqual(`
35 | ${INDENT_CHAR}test1
36 | ${INDENT_CHAR}test2
37 | ${INDENT_CHAR}test3
38 | `);
39 | });
40 | });
41 |
42 | describe('extendRefTypes', () => {
43 | it('should scan the generated output for matching refs and extend the type annotation', () => {
44 | const refMapping = {
45 | [`IMainInterface${REF_PATH_DELIMITER}propWithRef`]: 'OtherThing',
46 | };
47 |
48 | const generatedOutput = `interface IOtherThing {
49 | foo: string;
50 | }
51 |
52 | interface IMainInterface {
53 | propWithRef: string;
54 | bar: number;
55 | }
56 |
57 | `;
58 | const expected = `interface IOtherThing {
59 | foo: string;
60 | }
61 |
62 | interface IMainInterface {
63 | propWithRef: string | IOtherThing;
64 | bar: number;
65 | }
66 |
67 | `;
68 |
69 | expect(extendRefTypes(generatedOutput, refMapping)).toEqual(expected);
70 | });
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "removeComments": true,
7 | "emitDecoratorMetadata": true,
8 | "experimentalDecorators": true,
9 | "sourceMap": false,
10 | "pretty": true,
11 | "allowUnreachableCode": false,
12 | "noFallthroughCasesInSwitch": false,
13 | /**
14 | * TODO: Fix issues
15 | */
16 | // "noImplicitReturns": true,
17 | "noImplicitAny": true,
18 | "strictNullChecks": true,
19 | "noUnusedParameters": true,
20 | "noUnusedLocals": true,
21 | "skipLibCheck": true,
22 | "skipDefaultLibCheck": true,
23 | "outDir": "./dist",
24 | "types": ["node"],
25 | "lib": ["es2015"]
26 | },
27 | "include": ["src/**/*.ts"],
28 | "exclude": ["node_modules"]
29 | }
30 |
--------------------------------------------------------------------------------