├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .github ├── .codecov.yaml ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md ├── dependabot.yml └── workflows │ ├── node.js.yml │ └── npm-publish.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierrc.cjs ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── .yarnrc.yml ├── LICENSE ├── README.md ├── SECURITY.md ├── examples ├── cjs │ ├── package.json │ ├── scripts │ │ └── types.ts │ ├── src │ │ ├── interfaces │ │ │ ├── One.ts │ │ │ ├── index.ts │ │ │ ├── subDir │ │ │ │ ├── Address.ts │ │ │ │ ├── Person.ts │ │ │ │ └── index.ts │ │ │ └── subDir2 │ │ │ │ ├── Employee.ts │ │ │ │ └── index.ts │ │ └── schemas │ │ │ ├── OneSchema.ts │ │ │ ├── subDir │ │ │ ├── AddressSchema.ts │ │ │ └── PersonSchema.ts │ │ │ └── subDir2 │ │ │ └── EmployeeSchema.ts │ ├── tsconfig.json │ └── yarn.lock └── esm │ ├── package.json │ ├── scripts │ └── types.ts │ ├── src │ ├── interfaces │ │ ├── One.ts │ │ ├── index.ts │ │ ├── subDir │ │ │ ├── Address.ts │ │ │ ├── Person.ts │ │ │ └── index.ts │ │ └── subDir2 │ │ │ ├── Employee.ts │ │ │ └── index.ts │ └── schemas │ │ ├── OneSchema.ts │ │ ├── subDir │ │ ├── AddressSchema.ts │ │ └── PersonSchema.ts │ │ └── subDir2 │ │ └── EmployeeSchema.ts │ ├── tsconfig.json │ └── yarn.lock ├── jest.config.cjs ├── package.json ├── src ├── __tests__ │ ├── _example │ │ ├── index.ts │ │ └── schemas │ │ │ └── OneSchema.ts │ ├── allowValid │ │ ├── index.ts │ │ └── schemas │ │ │ ├── Allow.ts │ │ │ ├── AllowOnly.ts │ │ │ ├── ParentSchema.ts │ │ │ └── Valid.ts │ ├── alternatives │ │ ├── alternatives.ts │ │ └── schemas │ │ │ └── OneSchema.ts │ ├── array │ │ ├── array.ts │ │ └── schemas │ │ │ ├── OneSchema.ts │ │ │ └── OneSparseSchema.ts │ ├── basic.ts │ ├── cast │ │ ├── cast.ts │ │ └── schemas │ │ │ └── OneSchema.ts │ ├── className │ │ ├── className.ts │ │ └── schemas │ │ │ ├── ClassName.ts │ │ │ ├── ClassNameProperty.ts │ │ │ ├── ClassNamePropertySpaced.ts │ │ │ ├── NoClassNameSchema.ts │ │ │ └── NoClassNameTest.ts │ ├── commentEverything.ts │ ├── concat │ │ ├── index.ts │ │ └── schemas │ │ │ └── FooBarSchema.ts │ ├── defaultInterfaceSuffix │ │ ├── defaultInterfaceSuffix.ts │ │ └── schemas │ │ │ └── OneSchema.ts │ ├── defaults │ │ └── defaults.ts │ ├── description │ │ ├── index.ts │ │ └── schemas │ │ │ └── OneSchema.ts │ ├── doublequotesEscape │ │ ├── index.ts │ │ └── schemas │ │ │ └── AllowSchema.ts │ ├── empty │ │ └── empty.ts │ ├── files │ │ ├── index.ts │ │ └── schemas │ │ │ ├── FooBarSchema.ts │ │ │ ├── OneSchema.ts │ │ │ ├── React.tsx │ │ │ ├── Readme.md │ │ │ ├── index.ts │ │ │ └── notASchema.ts │ ├── forbidden.ts │ ├── fromFile │ │ ├── fromFile.ts │ │ └── schemas │ │ │ ├── FooBarSchema.ts │ │ │ ├── OneSchema.ts │ │ │ └── notASchema.ts │ ├── headerFooter │ │ ├── index.ts │ │ └── schemas │ │ │ └── OneSchema.ts │ ├── ignoreFiles │ │ ├── ignoreFiles.ts │ │ └── schemas │ │ │ ├── OneSchema.ts │ │ │ ├── subDir │ │ │ ├── AddressSchema.ts │ │ │ └── PersonSchema.ts │ │ │ └── subDir2 │ │ │ └── EmployeeSchema.ts │ ├── indentation │ │ ├── indent.ts │ │ └── schemas │ │ │ └── NestedSchema.ts │ ├── interfaceFileSuffix │ │ ├── interfaceFileSuffix.ts │ │ └── schemas │ │ │ └── OneSchema.ts │ ├── joiExtensions │ │ └── joiExtensions.ts │ ├── joiTypes.ts │ ├── label │ │ ├── label.ts │ │ └── schemas │ │ │ ├── Label.ts │ │ │ ├── LabelProperty.ts │ │ │ ├── LabelPropertySpaced.ts │ │ │ ├── NoLabelSchema.ts │ │ │ └── NoLabelTest.ts │ ├── multipleFiles │ │ ├── multipleFiles.ts │ │ └── schemas │ │ │ ├── OneSchema.ts │ │ │ └── PersonSchema.ts │ ├── noIndex │ │ ├── noIndex.ts │ │ └── schemas │ │ │ ├── FooBarSchema.ts │ │ │ ├── InnerSchema.ts │ │ │ ├── OneSchema.ts │ │ │ └── notASchema.ts │ ├── none │ │ ├── none.ts │ │ └── schemas │ │ │ └── notASchema.ts │ ├── override.ts │ ├── patterns.ts │ ├── primitiveTypes │ │ ├── index.ts │ │ └── schemas │ │ │ ├── AllowSchema.ts │ │ │ ├── BooleanSchema.ts │ │ │ ├── CounterSchema.ts │ │ │ ├── DateFieldSchema.ts │ │ │ ├── EmailSchema.ts │ │ │ ├── ObjectSchema.ts │ │ │ ├── UnionSchema.ts │ │ │ └── UsingSchema.ts │ ├── readme │ │ ├── readme.ts │ │ └── schemas │ │ │ └── ReadmeSchema.ts │ ├── readonly.ts │ ├── subDirectories │ │ ├── AssertionCriteria.ts │ │ ├── schemas │ │ │ ├── OneSchema.ts │ │ │ ├── subDir │ │ │ │ ├── AddressSchema.ts │ │ │ │ └── PersonSchema.ts │ │ │ └── subDir2 │ │ │ │ └── EmployeeSchema.ts │ │ └── subDirectories.ts │ ├── tuple │ │ ├── schemas │ │ │ └── OneSchema.ts │ │ └── tuple.ts │ ├── unknown.ts │ └── utils.ts ├── analyseSchemaFile.ts ├── convertFilesInDirectory.ts ├── index.ts ├── joiDescribeTypes.ts ├── joiUtils.ts ├── parse.ts ├── types.ts ├── utils.ts ├── write.ts └── writeInterfaceFile.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | .github 4 | node_modules 5 | *.js 6 | interfaces 7 | *.md 8 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['@typescript-eslint'], 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | extends: [ 8 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 9 | 'prettier' 10 | ], 11 | parser: '@typescript-eslint/parser', 12 | parserOptions: { 13 | ecmaVersion: 2020, 14 | sourceType: 'module' // Allows for the use of imports 15 | }, 16 | rules: { 17 | 'lines-between-class-members': [ 18 | 'error', 19 | 'always', 20 | { 21 | exceptAfterSingleLine: true 22 | } 23 | ], 24 | '@typescript-eslint/no-use-before-define': [ 25 | 'error', 26 | { 27 | functions: false 28 | }, 29 | 30 | 31 | ], 32 | "no-console": "error", 33 | "eqeqeq": "error" 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /.github/.codecov.yaml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 95% 6 | patch: 7 | default: 8 | enabled: no 9 | range: "95...100" 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mrjono1 # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report an issue 3 | about: Create a report to help us improve 4 | title: 'What is your issue?' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | ```TypeScript 16 | import Joi from "joi"; 17 | export const JobSchema = Joi.object({ 18 | businessName: Joi.string().required(), 19 | jobTitle: Joi.string().required() 20 | }).meta({ className: 'Job' }); 21 | ``` 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | ```TypeScript 27 | export interface Job { 28 | businessName: string; 29 | jobTitle: string; 30 | } 31 | ``` 32 | 33 | **Actual behavior** 34 | A clear and concise description of what actually to happened. 35 | 36 | ```TypeScript 37 | export interface Job {} 38 | ``` 39 | 40 | **Additional context** 41 | Add any other context about the problem here. 42 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # Maintain dependencies for GitHub Actions 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | 14 | # Maintain dependencies for npm 15 | - package-ecosystem: "npm" 16 | directory: "/" # Location of package manifests 17 | schedule: 18 | interval: "monthly" 19 | ignore: 20 | - dependency-name: "@types/node" # Do this manually as currenly supporting node 12 which is not the latest version 21 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [18.x, 20.x, 21.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: yarn 27 | - run: yarn lint 28 | - run: yarn build 29 | - name: Run tests and Code coverage 30 | run: yarn coverage 31 | - name: Upload coverage to Codecov 32 | uses: codecov/codecov-action@v5.4.2 33 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: "20.x" 18 | - run: yarn 19 | - run: yarn lint 20 | - run: yarn build 21 | - run: yarn test 22 | 23 | publish-npm: 24 | needs: build 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: actions/setup-node@v4 29 | with: 30 | node-version: "20.x" 31 | registry-url: https://registry.npmjs.org/ 32 | - run: yarn 33 | - run: yarn pub 34 | env: 35 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Dont save test interfaces 107 | **/__tests__/**/interfaces 108 | 109 | 110 | 111 | # ignore nix files 112 | .envrc 113 | shell.nix 114 | 115 | package-lock.json 116 | 117 | .idea/ 118 | 119 | # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 120 | .yarn/* 121 | !.yarn/patches 122 | !.yarn/plugins 123 | !.yarn/releases 124 | !.yarn/sdks 125 | !.yarn/versions 126 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .vscode 3 | coverage 4 | examples 5 | src 6 | .editorconfig 7 | .eslintignore 8 | .eslintrc.cjs 9 | .gitignore 10 | .nvmrc 11 | .prettierrc.cjs 12 | jest.config.cjs 13 | tsconfig.json 14 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | printWidth: 120, 4 | trailingComma: "none", 5 | singleQuote: true, 6 | bracketSpacing: true, 7 | endOfLine: "auto", 8 | arrowParens: 'avoid', 9 | overrides: [ 10 | { 11 | files: ["*.yaml", "*.yml"], 12 | options: { 13 | singleQuote: false 14 | } 15 | } 16 | ] 17 | }; 18 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "editorconfig.editorconfig" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | // Configurations from https://github.com/microsoft/vscode-recipes/tree/master/debugging-jest-tests 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "type": "node", 10 | "request": "launch", 11 | "name": "Jest Current File", 12 | "program": "${workspaceFolder}/node_modules/.bin/jest", 13 | "args": [ 14 | "${fileBasenameNoExtension}", 15 | "--config", 16 | "jest.config.cjs" 17 | ], 18 | "console": "integratedTerminal", 19 | "internalConsoleOptions": "neverOpen", 20 | "windows": { 21 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 22 | } 23 | }, 24 | { 25 | "type": "node", 26 | "request": "launch", 27 | "name": "Jest All", 28 | "program": "${workspaceFolder}/node_modules/.bin/jest", 29 | "args": [ 30 | "--runInBand" 31 | ], 32 | "console": "integratedTerminal", 33 | "internalConsoleOptions": "neverOpen", 34 | "windows": { 35 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 36 | } 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "npm.packageManager": "yarn", 3 | "eslint.packageManager": "yarn", 4 | "editor.formatOnSave": true, 5 | "eslint.workingDirectories": ["./src"], 6 | "[javascript]": { 7 | "editor.formatOnSave": false 8 | }, 9 | "[typescript]": { 10 | "editor.formatOnSave": false, 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "editor.codeActionsOnSave": { 14 | "source.fixAll.eslint": "explicit" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jono 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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the latest version will get security updates 6 | 7 | ## Reporting a Vulnerability 8 | 9 | If a security vulnerability is found please raise an [Issue](https://github.com/mrjono1/joi-to-typescript/issues) to report or a [Pull Request](https://github.com/mrjono1/joi-to-typescript/pulls) to fix the issue 10 | -------------------------------------------------------------------------------- /examples/cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "license": "MIT", 4 | "scripts": { 5 | "types": "ts-node scripts/types.ts" 6 | }, 7 | "dependencies": { 8 | "joi": "^17.6.0" 9 | }, 10 | "devDependencies": { 11 | "@types/node": "^17.0.13", 12 | "joi-to-typescript": "^3.0.2", 13 | "ts-node": "^10.4.0", 14 | "typescript": "^4.5.5" 15 | }, 16 | "resolutions": { 17 | "joi-to-typescript": "file:../.." 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/cjs/scripts/types.ts: -------------------------------------------------------------------------------- 1 | import { convertFromDirectory } from 'joi-to-typescript'; 2 | 3 | async function types(): Promise { 4 | // eslint-disable-next-line no-console 5 | console.log('Running joi-to-typescript...'); 6 | 7 | // Configure your settings here 8 | const result = await convertFromDirectory({ 9 | schemaDirectory: './src/schemas', 10 | typeOutputDirectory: './src/interfaces', 11 | debug: true 12 | }); 13 | 14 | if (result) { 15 | // eslint-disable-next-line no-console 16 | console.log('Completed joi-to-typescript'); 17 | } else { 18 | // eslint-disable-next-line no-console 19 | console.log('Failed to run joi-to-typescript'); 20 | } 21 | } 22 | 23 | types(); 24 | -------------------------------------------------------------------------------- /examples/cjs/src/interfaces/One.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was automatically generated by joi-to-typescript 3 | * Do not modify this file manually 4 | */ 5 | 6 | import { Person } from './subDir'; 7 | 8 | export interface Item { 9 | /** 10 | * Female Zebra 11 | */ 12 | femaleZebra?: Zebra; 13 | /** 14 | * Male Zebra 15 | */ 16 | maleZebra?: Zebra; 17 | name: string; 18 | } 19 | 20 | /** 21 | * A list of People 22 | */ 23 | export type People = Person[]; 24 | 25 | /** 26 | * a test schema definition 27 | */ 28 | export interface Test { 29 | name?: string; 30 | /** 31 | * A list of People 32 | */ 33 | people?: People; 34 | propertyName1: boolean; 35 | } 36 | 37 | export interface Zebra { 38 | name?: string; 39 | } 40 | -------------------------------------------------------------------------------- /examples/cjs/src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was automatically generated by joi-to-typescript 3 | * Do not modify this file manually 4 | */ 5 | 6 | export * from './One'; 7 | -------------------------------------------------------------------------------- /examples/cjs/src/interfaces/subDir/Address.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was automatically generated by joi-to-typescript 3 | * Do not modify this file manually 4 | */ 5 | 6 | export interface Address { 7 | Suburb: string; 8 | addressLineNumber1: string; 9 | } 10 | -------------------------------------------------------------------------------- /examples/cjs/src/interfaces/subDir/Person.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was automatically generated by joi-to-typescript 3 | * Do not modify this file manually 4 | */ 5 | 6 | import { Address } from '.'; 7 | 8 | export interface Person { 9 | address: Address; 10 | firstName: string; 11 | lastName: string; 12 | } 13 | -------------------------------------------------------------------------------- /examples/cjs/src/interfaces/subDir/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was automatically generated by joi-to-typescript 3 | * Do not modify this file manually 4 | */ 5 | 6 | export * from './Address'; 7 | export * from './Person'; 8 | -------------------------------------------------------------------------------- /examples/cjs/src/interfaces/subDir2/Employee.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was automatically generated by joi-to-typescript 3 | * Do not modify this file manually 4 | */ 5 | 6 | import { Person } from '../subDir'; 7 | import { Item } from '..'; 8 | 9 | export interface Employee { 10 | personalDetails: Person; 11 | pet: Item; 12 | } 13 | -------------------------------------------------------------------------------- /examples/cjs/src/interfaces/subDir2/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was automatically generated by joi-to-typescript 3 | * Do not modify this file manually 4 | */ 5 | 6 | export * from './Employee'; 7 | -------------------------------------------------------------------------------- /examples/cjs/src/schemas/OneSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { PersonSchema } from './subDir/PersonSchema'; 3 | 4 | export const ZebraSchema = Joi.object({ 5 | name: Joi.string() 6 | }).meta({ className: 'Zebra' }); 7 | 8 | export const ItemSchema = Joi.object({ 9 | name: Joi.string().required(), 10 | maleZebra: ZebraSchema.description('Male Zebra'), 11 | femaleZebra: ZebraSchema.description('Female Zebra') 12 | }).meta({ className: 'Item' }); 13 | 14 | export const PeopleSchema = Joi.array() 15 | .items(PersonSchema) 16 | .required() 17 | .meta({ className: 'People' }) 18 | .description('A list of People'); 19 | 20 | export const TestSchema = Joi.object({ 21 | name: Joi.string().optional(), 22 | propertyName1: Joi.boolean().required(), 23 | people: PeopleSchema.optional() 24 | }) 25 | .meta({ className: 'Test' }) 26 | .description('a test schema definition'); 27 | -------------------------------------------------------------------------------- /examples/cjs/src/schemas/subDir/AddressSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const AddressSchema = Joi.object({ 4 | addressLineNumber1: Joi.string().required(), 5 | Suburb: Joi.string().required() 6 | }).meta({ className: 'Address' }); 7 | -------------------------------------------------------------------------------- /examples/cjs/src/schemas/subDir/PersonSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { AddressSchema } from './AddressSchema'; 3 | 4 | export const PersonSchema = Joi.object({ 5 | firstName: Joi.string().required(), 6 | lastName: Joi.string().required(), 7 | address: AddressSchema.required() 8 | }).meta({ className: 'Person' }); 9 | -------------------------------------------------------------------------------- /examples/cjs/src/schemas/subDir2/EmployeeSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { PersonSchema } from '../subDir/PersonSchema'; 3 | import { ItemSchema } from '../OneSchema'; 4 | 5 | export const EmployeeSchema = Joi.object({ 6 | personalDetails: PersonSchema.required(), 7 | pet: ItemSchema.required() 8 | }).meta({ className: 'Employee' }); 9 | -------------------------------------------------------------------------------- /examples/cjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true 4 | }, 5 | "include": ["src"] 6 | } 7 | -------------------------------------------------------------------------------- /examples/cjs/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@cspotcode/source-map-consumer@0.8.0": 6 | version "0.8.0" 7 | resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" 8 | integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== 9 | 10 | "@cspotcode/source-map-support@0.7.0": 11 | version "0.7.0" 12 | resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" 13 | integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== 14 | dependencies: 15 | "@cspotcode/source-map-consumer" "0.8.0" 16 | 17 | "@hapi/hoek@^9.0.0": 18 | version "9.2.0" 19 | resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131" 20 | integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug== 21 | 22 | "@hapi/topo@^5.0.0": 23 | version "5.0.0" 24 | resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7" 25 | integrity sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw== 26 | dependencies: 27 | "@hapi/hoek" "^9.0.0" 28 | 29 | "@sideway/address@^4.1.3": 30 | version "4.1.3" 31 | resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.3.tgz#d93cce5d45c5daec92ad76db492cc2ee3c64ab27" 32 | integrity sha512-8ncEUtmnTsMmL7z1YPB47kPUq7LpKWJNFPsRzHiIajGC5uXlWGn+AmkYPcHNl8S4tcEGx+cnORnNYaw2wvL+LQ== 33 | dependencies: 34 | "@hapi/hoek" "^9.0.0" 35 | 36 | "@sideway/formula@^3.0.0": 37 | version "3.0.1" 38 | resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" 39 | integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== 40 | 41 | "@sideway/pinpoint@^2.0.0": 42 | version "2.0.0" 43 | resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" 44 | integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== 45 | 46 | "@tsconfig/node10@^1.0.7": 47 | version "1.0.8" 48 | resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" 49 | integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== 50 | 51 | "@tsconfig/node12@^1.0.7": 52 | version "1.0.9" 53 | resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" 54 | integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== 55 | 56 | "@tsconfig/node14@^1.0.0": 57 | version "1.0.1" 58 | resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" 59 | integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== 60 | 61 | "@tsconfig/node16@^1.0.2": 62 | version "1.0.2" 63 | resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" 64 | integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== 65 | 66 | "@types/node@^17.0.13": 67 | version "17.0.13" 68 | resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.13.tgz#5ed7ed7c662948335fcad6c412bb42d99ea754e3" 69 | integrity sha512-Y86MAxASe25hNzlDbsviXl8jQHb0RDvKt4c40ZJQ1Don0AAL0STLZSs4N+6gLEO55pedy7r2cLwS+ZDxPm/2Bw== 70 | 71 | acorn-walk@^8.1.1: 72 | version "8.2.0" 73 | resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" 74 | integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== 75 | 76 | acorn@^8.4.1: 77 | version "8.7.0" 78 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" 79 | integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== 80 | 81 | arg@^4.1.0: 82 | version "4.1.3" 83 | resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" 84 | integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== 85 | 86 | create-require@^1.1.0: 87 | version "1.1.1" 88 | resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" 89 | integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== 90 | 91 | diff@^4.0.1: 92 | version "4.0.2" 93 | resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" 94 | integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== 95 | 96 | joi-to-typescript@^3.0.2: 97 | version "3.1.0" 98 | resolved "https://registry.yarnpkg.com/joi-to-typescript/-/joi-to-typescript-3.1.0.tgz#25a0ff6a7d0b6cdec295f2431febcb9ae65a0679" 99 | integrity sha512-bhNf0vqoXphFVXj1v3oDHedrF4fGRdTex7k8Uoji9QuqJ8UueonMvKw9kk62sU/jcyOsVNXbZykikMiH8HaXwQ== 100 | 101 | "joi-to-typescript@file:../..": 102 | version "4.0.7" 103 | 104 | joi@^17.6.0: 105 | version "17.6.0" 106 | resolved "https://registry.yarnpkg.com/joi/-/joi-17.6.0.tgz#0bb54f2f006c09a96e75ce687957bd04290054b2" 107 | integrity sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw== 108 | dependencies: 109 | "@hapi/hoek" "^9.0.0" 110 | "@hapi/topo" "^5.0.0" 111 | "@sideway/address" "^4.1.3" 112 | "@sideway/formula" "^3.0.0" 113 | "@sideway/pinpoint" "^2.0.0" 114 | 115 | make-error@^1.1.1: 116 | version "1.3.6" 117 | resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" 118 | integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== 119 | 120 | ts-node@^10.4.0: 121 | version "10.4.0" 122 | resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" 123 | integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A== 124 | dependencies: 125 | "@cspotcode/source-map-support" "0.7.0" 126 | "@tsconfig/node10" "^1.0.7" 127 | "@tsconfig/node12" "^1.0.7" 128 | "@tsconfig/node14" "^1.0.0" 129 | "@tsconfig/node16" "^1.0.2" 130 | acorn "^8.4.1" 131 | acorn-walk "^8.1.1" 132 | arg "^4.1.0" 133 | create-require "^1.1.0" 134 | diff "^4.0.1" 135 | make-error "^1.1.1" 136 | yn "3.1.1" 137 | 138 | typescript@^4.5.5: 139 | version "4.5.5" 140 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" 141 | integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== 142 | 143 | yn@3.1.1: 144 | version "3.1.1" 145 | resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" 146 | integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== 147 | -------------------------------------------------------------------------------- /examples/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "license": "MIT", 4 | "type": "module", 5 | "scripts": { 6 | "types": "node --experimental-modules --es-module-specifier-resolution=node --loader ts-node/esm scripts/types.ts" 7 | }, 8 | "dependencies": { 9 | "joi": "^17.6.0" 10 | }, 11 | "devDependencies": { 12 | "@types/node": "^17.0.13", 13 | "joi-to-typescript": "^3.0.2", 14 | "ts-node": "^10.4.0", 15 | "typescript": "^4.5.5" 16 | }, 17 | "resolutions": { 18 | "joi-to-typescript": "file:../.." 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/esm/scripts/types.ts: -------------------------------------------------------------------------------- 1 | import { convertFromDirectory } from 'joi-to-typescript'; 2 | 3 | async function types(): Promise { 4 | // eslint-disable-next-line no-console 5 | console.log('Running joi-to-typescript...'); 6 | 7 | // Configure your settings here 8 | const result = await convertFromDirectory({ 9 | schemaDirectory: './src/schemas', 10 | typeOutputDirectory: './src/interfaces', 11 | debug: true 12 | }); 13 | 14 | if (result) { 15 | // eslint-disable-next-line no-console 16 | console.log('Completed joi-to-typescript'); 17 | } else { 18 | // eslint-disable-next-line no-console 19 | console.log('Failed to run joi-to-typescript'); 20 | } 21 | } 22 | 23 | types(); 24 | -------------------------------------------------------------------------------- /examples/esm/src/interfaces/One.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was automatically generated by joi-to-typescript 3 | * Do not modify this file manually 4 | */ 5 | 6 | import { Person } from './subDir'; 7 | 8 | export interface Item { 9 | /** 10 | * Female Zebra 11 | */ 12 | femaleZebra?: Zebra; 13 | /** 14 | * Male Zebra 15 | */ 16 | maleZebra?: Zebra; 17 | name: string; 18 | } 19 | 20 | /** 21 | * A list of People 22 | */ 23 | export type People = Person[]; 24 | 25 | /** 26 | * a test schema definition 27 | */ 28 | export interface Test { 29 | name?: string; 30 | /** 31 | * A list of People 32 | */ 33 | people?: People; 34 | propertyName1: boolean; 35 | } 36 | 37 | export interface Zebra { 38 | name?: string; 39 | } 40 | -------------------------------------------------------------------------------- /examples/esm/src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was automatically generated by joi-to-typescript 3 | * Do not modify this file manually 4 | */ 5 | 6 | export * from './One'; 7 | -------------------------------------------------------------------------------- /examples/esm/src/interfaces/subDir/Address.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was automatically generated by joi-to-typescript 3 | * Do not modify this file manually 4 | */ 5 | 6 | export interface Address { 7 | Suburb: string; 8 | addressLineNumber1: string; 9 | } 10 | -------------------------------------------------------------------------------- /examples/esm/src/interfaces/subDir/Person.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was automatically generated by joi-to-typescript 3 | * Do not modify this file manually 4 | */ 5 | 6 | import { Address } from '.'; 7 | 8 | export interface Person { 9 | address: Address; 10 | firstName: string; 11 | lastName: string; 12 | } 13 | -------------------------------------------------------------------------------- /examples/esm/src/interfaces/subDir/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was automatically generated by joi-to-typescript 3 | * Do not modify this file manually 4 | */ 5 | 6 | export * from './Address'; 7 | export * from './Person'; 8 | -------------------------------------------------------------------------------- /examples/esm/src/interfaces/subDir2/Employee.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was automatically generated by joi-to-typescript 3 | * Do not modify this file manually 4 | */ 5 | 6 | import { Person } from '../subDir'; 7 | import { Item } from '..'; 8 | 9 | export interface Employee { 10 | personalDetails: Person; 11 | pet: Item; 12 | } 13 | -------------------------------------------------------------------------------- /examples/esm/src/interfaces/subDir2/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was automatically generated by joi-to-typescript 3 | * Do not modify this file manually 4 | */ 5 | 6 | export * from './Employee'; 7 | -------------------------------------------------------------------------------- /examples/esm/src/schemas/OneSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { PersonSchema } from './subDir/PersonSchema'; 3 | 4 | export const ZebraSchema = Joi.object({ 5 | name: Joi.string() 6 | }).meta({ className: 'Zebra' }); 7 | 8 | export const ItemSchema = Joi.object({ 9 | name: Joi.string().required(), 10 | maleZebra: ZebraSchema.description('Male Zebra'), 11 | femaleZebra: ZebraSchema.description('Female Zebra') 12 | }).meta({ className: 'Item' }); 13 | 14 | export const PeopleSchema = Joi.array() 15 | .items(PersonSchema) 16 | .required() 17 | .meta({ className: 'People' }) 18 | .description('A list of People'); 19 | 20 | export const TestSchema = Joi.object({ 21 | name: Joi.string().optional(), 22 | propertyName1: Joi.boolean().required(), 23 | people: PeopleSchema.optional() 24 | }) 25 | .meta({ className: 'Test' }) 26 | .description('a test schema definition'); 27 | -------------------------------------------------------------------------------- /examples/esm/src/schemas/subDir/AddressSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const AddressSchema = Joi.object({ 4 | addressLineNumber1: Joi.string().required(), 5 | Suburb: Joi.string().required() 6 | }).meta({ className: 'Address' }); 7 | -------------------------------------------------------------------------------- /examples/esm/src/schemas/subDir/PersonSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { AddressSchema } from './AddressSchema'; 3 | 4 | export const PersonSchema = Joi.object({ 5 | firstName: Joi.string().required(), 6 | lastName: Joi.string().required(), 7 | address: AddressSchema.required() 8 | }).meta({ className: 'Person' }); 9 | -------------------------------------------------------------------------------- /examples/esm/src/schemas/subDir2/EmployeeSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { PersonSchema } from '../subDir/PersonSchema'; 3 | import { ItemSchema } from '../OneSchema'; 4 | 5 | export const EmployeeSchema = Joi.object({ 6 | personalDetails: PersonSchema.required(), 7 | pet: ItemSchema.required() 8 | }).meta({ className: 'Employee' }); 9 | -------------------------------------------------------------------------------- /examples/esm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "esModuleInterop": true, 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/esm/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@cspotcode/source-map-consumer@0.8.0": 6 | version "0.8.0" 7 | resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" 8 | integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== 9 | 10 | "@cspotcode/source-map-support@0.7.0": 11 | version "0.7.0" 12 | resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" 13 | integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== 14 | dependencies: 15 | "@cspotcode/source-map-consumer" "0.8.0" 16 | 17 | "@hapi/hoek@^9.0.0": 18 | version "9.2.0" 19 | resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131" 20 | integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug== 21 | 22 | "@hapi/topo@^5.0.0": 23 | version "5.0.0" 24 | resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7" 25 | integrity sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw== 26 | dependencies: 27 | "@hapi/hoek" "^9.0.0" 28 | 29 | "@sideway/address@^4.1.3": 30 | version "4.1.3" 31 | resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.3.tgz#d93cce5d45c5daec92ad76db492cc2ee3c64ab27" 32 | integrity sha512-8ncEUtmnTsMmL7z1YPB47kPUq7LpKWJNFPsRzHiIajGC5uXlWGn+AmkYPcHNl8S4tcEGx+cnORnNYaw2wvL+LQ== 33 | dependencies: 34 | "@hapi/hoek" "^9.0.0" 35 | 36 | "@sideway/formula@^3.0.0": 37 | version "3.0.1" 38 | resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" 39 | integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== 40 | 41 | "@sideway/pinpoint@^2.0.0": 42 | version "2.0.0" 43 | resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" 44 | integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== 45 | 46 | "@tsconfig/node10@^1.0.7": 47 | version "1.0.8" 48 | resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" 49 | integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== 50 | 51 | "@tsconfig/node12@^1.0.7": 52 | version "1.0.9" 53 | resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" 54 | integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== 55 | 56 | "@tsconfig/node14@^1.0.0": 57 | version "1.0.1" 58 | resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" 59 | integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== 60 | 61 | "@tsconfig/node16@^1.0.2": 62 | version "1.0.2" 63 | resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" 64 | integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== 65 | 66 | "@types/node@^17.0.13": 67 | version "17.0.13" 68 | resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.13.tgz#5ed7ed7c662948335fcad6c412bb42d99ea754e3" 69 | integrity sha512-Y86MAxASe25hNzlDbsviXl8jQHb0RDvKt4c40ZJQ1Don0AAL0STLZSs4N+6gLEO55pedy7r2cLwS+ZDxPm/2Bw== 70 | 71 | acorn-walk@^8.1.1: 72 | version "8.2.0" 73 | resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" 74 | integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== 75 | 76 | acorn@^8.4.1: 77 | version "8.7.0" 78 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" 79 | integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== 80 | 81 | arg@^4.1.0: 82 | version "4.1.3" 83 | resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" 84 | integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== 85 | 86 | create-require@^1.1.0: 87 | version "1.1.1" 88 | resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" 89 | integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== 90 | 91 | diff@^4.0.1: 92 | version "4.0.2" 93 | resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" 94 | integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== 95 | 96 | joi-to-typescript@^3.0.2: 97 | version "3.1.0" 98 | resolved "https://registry.yarnpkg.com/joi-to-typescript/-/joi-to-typescript-3.1.0.tgz#25a0ff6a7d0b6cdec295f2431febcb9ae65a0679" 99 | integrity sha512-bhNf0vqoXphFVXj1v3oDHedrF4fGRdTex7k8Uoji9QuqJ8UueonMvKw9kk62sU/jcyOsVNXbZykikMiH8HaXwQ== 100 | 101 | "joi-to-typescript@file:../..": 102 | version "4.0.7" 103 | 104 | joi@^17.6.0: 105 | version "17.6.0" 106 | resolved "https://registry.yarnpkg.com/joi/-/joi-17.6.0.tgz#0bb54f2f006c09a96e75ce687957bd04290054b2" 107 | integrity sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw== 108 | dependencies: 109 | "@hapi/hoek" "^9.0.0" 110 | "@hapi/topo" "^5.0.0" 111 | "@sideway/address" "^4.1.3" 112 | "@sideway/formula" "^3.0.0" 113 | "@sideway/pinpoint" "^2.0.0" 114 | 115 | make-error@^1.1.1: 116 | version "1.3.6" 117 | resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" 118 | integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== 119 | 120 | ts-node@^10.4.0: 121 | version "10.4.0" 122 | resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" 123 | integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A== 124 | dependencies: 125 | "@cspotcode/source-map-support" "0.7.0" 126 | "@tsconfig/node10" "^1.0.7" 127 | "@tsconfig/node12" "^1.0.7" 128 | "@tsconfig/node14" "^1.0.0" 129 | "@tsconfig/node16" "^1.0.2" 130 | acorn "^8.4.1" 131 | acorn-walk "^8.1.1" 132 | arg "^4.1.0" 133 | create-require "^1.1.0" 134 | diff "^4.0.1" 135 | make-error "^1.1.1" 136 | yn "3.1.1" 137 | 138 | typescript@^4.5.5: 139 | version "4.5.5" 140 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" 141 | integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== 142 | 143 | yn@3.1.1: 144 | version "3.1.1" 145 | resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" 146 | integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== 147 | -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.ts$': 'ts-jest' 4 | }, 5 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.ts$', 6 | testPathIgnorePatterns: ['/schemas/', '/interfaces/', 'AssertionCriteria'], 7 | moduleFileExtensions: ['ts', 'js'], 8 | modulePaths: ['', '/src'], 9 | coveragePathIgnorePatterns: ['__tests__', 'examples'], 10 | testEnvironment: 'node' 11 | }; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joi-to-typescript", 3 | "description": "Convert Joi Schemas to TypeScript interfaces", 4 | "version": "4.15.0", 5 | "author": "Jono Clarnette", 6 | "keywords": [ 7 | "joi", 8 | "ts", 9 | "typescript", 10 | "hapi", 11 | "interface" 12 | ], 13 | "license": "MIT", 14 | "repository": { 15 | "url": "https://github.com/mrjono1/joi-to-typescript", 16 | "type": "git" 17 | }, 18 | "funding": "https://github.com/mrjono1/joi-to-typescript?sponsor=1", 19 | "bugs": { 20 | "url": "https://github.com/mrjono1/joi-to-typescript/issues" 21 | }, 22 | "main": "./dist/main/index.js", 23 | "module": "./dist/module/index.js", 24 | "typings": "./dist/types/index.d.ts", 25 | "exports": { 26 | "import": "./dist/module/index.js", 27 | "require": "./dist/main/index.js" 28 | }, 29 | "scripts": { 30 | "build:esm": "tsc --module es2020 --outDir ./dist/module", 31 | "build:cjs": "tsc --module commonjs --outDir ./dist/main", 32 | "build:dts": "tsc --declaration --emitDeclarationOnly --outDir ./dist/types", 33 | "build": "yarn build:esm && yarn build:cjs && yarn build:dts", 34 | "format": "prettier --write \"src/**/*.ts\"", 35 | "lint": "eslint 'src/**'", 36 | "test": "jest --config jest.config.cjs", 37 | "coverage": "yarn test --coverage --silent", 38 | "pub": "yarn build && yarn publish" 39 | }, 40 | "devDependencies": { 41 | "@types/jest": "29.5.14", 42 | "@types/node": "20.12.2", 43 | "@typescript-eslint/eslint-plugin": "7.18.0", 44 | "@typescript-eslint/parser": "7.18.0", 45 | "eslint": "8.57.0", 46 | "eslint-config-prettier": "10.1.5", 47 | "eslint-plugin-prettier": "5.4.1", 48 | "jest": "29.7.0", 49 | "joi": "17.13.3", 50 | "prettier": "3.5.3", 51 | "ts-jest": "29.3.4", 52 | "typescript": "5.8.3" 53 | }, 54 | "peerDependencies": { 55 | "joi": "17.x" 56 | }, 57 | "engines": { 58 | "node": ">=14.0.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/__tests__/_example/index.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, rmdirSync } from 'fs'; 2 | 3 | import { convertFromDirectory } from '../../index'; 4 | 5 | describe('example', () => { 6 | const typeOutputDirectory = './src/__tests__/_example/interfaces'; 7 | 8 | beforeAll(() => { 9 | if (existsSync(typeOutputDirectory)) { 10 | rmdirSync(typeOutputDirectory, { recursive: true }); 11 | } 12 | }); 13 | 14 | test('test example', async () => { 15 | const result = await convertFromDirectory({ 16 | schemaDirectory: './src/__tests__/_example/schemas', 17 | typeOutputDirectory, 18 | sortPropertiesByName: false 19 | }); 20 | 21 | expect(result).toBe(true); 22 | 23 | const oneContent = readFileSync(`${typeOutputDirectory}/One.ts`).toString(); 24 | expect(oneContent).toBe( 25 | `/** 26 | * This file was automatically generated by joi-to-typescript 27 | * Do not modify this file manually 28 | */ 29 | 30 | export interface Example { 31 | thing: string; 32 | } 33 | ` 34 | ); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/__tests__/_example/schemas/OneSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const exampleSchema = Joi.object({ 4 | thing: Joi.string().required() 5 | }).meta({ className: 'Example' }); 6 | -------------------------------------------------------------------------------- /src/__tests__/allowValid/schemas/Allow.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const roleSchema = Joi.string().allow('Admin', 'User').meta({ className: 'AllowRole' }); 4 | 5 | export const userSchema = Joi.object({ 6 | firstName: Joi.string().required(), 7 | lastName: Joi.string().required(), 8 | role: roleSchema.required() 9 | }).meta({ className: 'AllowUser' }); 10 | -------------------------------------------------------------------------------- /src/__tests__/allowValid/schemas/AllowOnly.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const roleSchema = Joi.string().allow('Admin', 'User').only().meta({ className: 'AllowOnlyRole' }); 4 | 5 | export const userSchema = Joi.object({ 6 | firstName: Joi.string().required(), 7 | lastName: Joi.string().required(), 8 | role: roleSchema.required() 9 | }).meta({ className: 'AllowOnlyUser' }); 10 | -------------------------------------------------------------------------------- /src/__tests__/allowValid/schemas/ParentSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const childSchema = Joi.object({ 4 | item: Joi.number().required().example(0) 5 | }).meta({ className: 'Child' }); 6 | 7 | export const parentSchema = Joi.object({ 8 | child: childSchema.allow(null).required() 9 | }).meta({ className: 'Parent' }); 10 | -------------------------------------------------------------------------------- /src/__tests__/allowValid/schemas/Valid.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const roleSchema = Joi.string().valid('Admin', 'User').only().meta({ className: 'ValidRole' }); 4 | 5 | export const userSchema = Joi.object({ 6 | firstName: Joi.string().required(), 7 | lastName: Joi.string().required(), 8 | role: roleSchema.required() 9 | }).meta({ className: 'ValidUser' }); 10 | -------------------------------------------------------------------------------- /src/__tests__/alternatives/alternatives.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, rmdirSync } from 'fs'; 2 | import Joi from 'joi'; 3 | 4 | import { convertFromDirectory, convertSchema } from '../../index'; 5 | 6 | describe('alternative types', () => { 7 | const typeOutputDirectory = './src/__tests__/alternatives/interfaces'; 8 | 9 | beforeAll(() => { 10 | if (existsSync(typeOutputDirectory)) { 11 | rmdirSync(typeOutputDirectory, { recursive: true }); 12 | } 13 | }); 14 | 15 | test('vaiations of alternatives from file', async () => { 16 | const result = await convertFromDirectory({ 17 | schemaDirectory: './src/__tests__/alternatives/schemas', 18 | typeOutputDirectory, 19 | sortPropertiesByName: false 20 | }); 21 | 22 | expect(result).toBe(true); 23 | 24 | const oneContent = readFileSync(`${typeOutputDirectory}/One.ts`).toString(); 25 | expect(oneContent).toBe( 26 | `/** 27 | * This file was automatically generated by joi-to-typescript 28 | * Do not modify this file manually 29 | */ 30 | 31 | export interface AlternativesArrayOptionalInterface { 32 | oneOrTheOtherMaybe: (number | string | undefined)[]; 33 | } 34 | 35 | export interface AlternativesObjectNoDesc { 36 | myVal?: number | string; 37 | } 38 | 39 | export type AlternativesRawNoDesc = number | string; 40 | 41 | export type AlternativesWithFunctionInterface = ((...args: any[]) => any) | { 42 | json: any; 43 | } | { 44 | raw: string; 45 | }; 46 | 47 | /** 48 | * a description for basic 49 | */ 50 | export type Basic = number | string; 51 | 52 | export interface Other { 53 | other?: string; 54 | } 55 | 56 | export interface SomeSchema { 57 | label?: string; 58 | someId?: any; 59 | } 60 | 61 | /** 62 | * a test schema definition 63 | */ 64 | export interface Test { 65 | name?: string; 66 | value?: Thing | Other; 67 | /** 68 | * a description for basic 69 | */ 70 | basic?: Basic; 71 | } 72 | 73 | /** 74 | * A list of Test object 75 | */ 76 | export type TestList = (boolean | string)[]; 77 | 78 | export interface Thing { 79 | thing: string; 80 | } 81 | ` 82 | ); 83 | }); 84 | 85 | test('blank alternative throws in joi', () => { 86 | expect(() => { 87 | Joi.alternatives().try().meta({ className: 'Basic' }).description('a description for basic'); 88 | }).toThrow(); 89 | }); 90 | 91 | test('allowed value in alternatives', () => { 92 | const schema = Joi.alternatives(Joi.string(), Joi.number()) 93 | .allow(null) 94 | .meta({ className: 'Test' }) 95 | .description('Test allowed values in alternatives'); 96 | 97 | const result = convertSchema({}, schema); 98 | expect(result).not.toBeUndefined(); 99 | expect(result?.content).toBe(`/** 100 | * Test allowed values in alternatives 101 | */ 102 | export type Test = string | number | null;`); 103 | }); 104 | 105 | test('union newlines', () => { 106 | const schema = Joi.object({ 107 | items: Joi.alternatives(Joi.string(), Joi.number().description('A number')).description( 108 | 'Test allowed values in alternatives' 109 | ), 110 | otherItems: Joi.alternatives(Joi.string(), Joi.number()).description('Test allowed values in alternatives') 111 | }) 112 | .description('An object') 113 | .meta({ className: 'Test' }); 114 | 115 | const result = convertSchema( 116 | { 117 | unionNewLine: true 118 | }, 119 | schema 120 | ); 121 | expect(result).not.toBeUndefined(); 122 | expect(result?.content).toBe(`/** 123 | * An object 124 | */ 125 | export interface Test { 126 | /** 127 | * Test allowed values in alternatives 128 | */ 129 | items?: 130 | | string 131 | /** 132 | * A number 133 | */ 134 | | number; 135 | /** 136 | * Test allowed values in alternatives 137 | */ 138 | otherItems?: 139 | | string 140 | | number; 141 | }`); 142 | }); 143 | 144 | test('union newlines 2', () => { 145 | const schema = Joi.object({ 146 | items: Joi.alternatives([Joi.string(), Joi.string().allow(null)]) 147 | }) 148 | .description('An object') 149 | .meta({ className: 'Test' }); 150 | 151 | const result = convertSchema( 152 | { 153 | unionNewLine: true 154 | }, 155 | schema 156 | ); 157 | expect(result).not.toBeUndefined(); 158 | expect(result?.content).toBe(`/** 159 | * An object 160 | */ 161 | export interface Test { 162 | items?: 163 | | string 164 | | ( 165 | | string 166 | | null); 167 | }`); 168 | }); 169 | 170 | test('union newlines one entry', () => { 171 | const schema = Joi.object({ 172 | items: Joi.alternatives([Joi.string()]) 173 | }) 174 | .description('An object') 175 | .meta({ className: 'Test' }); 176 | 177 | const result = convertSchema( 178 | { 179 | unionNewLine: true 180 | }, 181 | schema 182 | ); 183 | expect(result).not.toBeUndefined(); 184 | expect(result?.content).toBe(`/** 185 | * An object 186 | */ 187 | export interface Test { 188 | items?: string; 189 | }`); 190 | }); 191 | 192 | test('union newlines with defaults', () => { 193 | const schema = Joi.object({ 194 | items: Joi.alternatives([Joi.string(), Joi.number()]).default('hello') 195 | }) 196 | .description('An object') 197 | .meta({ className: 'Test' }); 198 | 199 | const result = convertSchema( 200 | { 201 | unionNewLine: true, 202 | supplyDefaultsInType: true 203 | }, 204 | schema 205 | ); 206 | expect(result).not.toBeUndefined(); 207 | expect(result?.content).toBe(`/** 208 | * An object 209 | */ 210 | export interface Test { 211 | items?: 212 | | "hello" 213 | | string 214 | | number; 215 | }`); 216 | }); 217 | 218 | test.skip('blank alternative thrown by joi but extra test if joi changes it', () => { 219 | expect(() => { 220 | const invalidSchema = Joi.alternatives() 221 | .try() 222 | .meta({ className: 'Basic' }) 223 | .description('a description for basic'); 224 | 225 | // the next code will not run as already thrown 226 | convertSchema({}, invalidSchema); 227 | }).toThrow(); 228 | }); 229 | }); 230 | -------------------------------------------------------------------------------- /src/__tests__/alternatives/schemas/OneSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const thingSchema = Joi.object({ 4 | thing: Joi.string().required() 5 | }).meta({ className: 'Thing' }); 6 | 7 | export const otherSchema = Joi.object({ 8 | other: Joi.string().optional() 9 | }).meta({ className: 'Other' }); 10 | 11 | export const basicSchema = Joi.alternatives() 12 | .try(Joi.number(), Joi.string()) 13 | .meta({ className: 'Basic' }) 14 | .description('a description for basic'); 15 | 16 | export const TestSchema = Joi.object({ 17 | name: Joi.string().optional(), 18 | value: Joi.alternatives().try(thingSchema, otherSchema), 19 | basic: basicSchema 20 | }) 21 | .meta({ className: 'Test' }) 22 | .description('a test schema definition'); 23 | 24 | export const TestListOfAltsSchema = Joi.array() 25 | .items(Joi.alt().try(Joi.bool(), Joi.string())) 26 | .required() 27 | .meta({ className: 'TestList' }) 28 | .description('A list of Test object'); 29 | 30 | export const AlternativesConditionalSchema = Joi.object({ 31 | label: Joi.string(), 32 | someId: Joi.alternatives().conditional('label', { 33 | is: 'abc', 34 | then: Joi.string().hex().required().length(24), 35 | otherwise: Joi.forbidden() 36 | }) 37 | }).meta({ className: 'SomeSchema' }); 38 | 39 | export const AlternativesWithFunctionSchema = Joi.alternatives([ 40 | Joi.function().minArity(2), 41 | Joi.object({ 42 | json: Joi.any().required() 43 | }), 44 | Joi.object({ 45 | raw: Joi.string().required() 46 | }) 47 | ]).meta({ className: 'AlternativesWithFunctionInterface' }); 48 | 49 | export const AlternativesArrayOptional = Joi.object({ 50 | oneOrTheOtherMaybe: Joi.array() 51 | .items(Joi.alternatives([Joi.number(), Joi.string(), Joi.alternatives()])) 52 | .required() 53 | }).meta({ className: 'AlternativesArrayOptionalInterface' }); 54 | 55 | export const alternativesRawNoDescSchema = Joi.alternatives([Joi.number(), Joi.string()]).meta({ 56 | className: 'AlternativesRawNoDesc', 57 | disableDescription: true 58 | }); 59 | 60 | export const alternativesObjectNoDescSchema = Joi.object({ 61 | myVal: Joi.alternatives([Joi.number(), Joi.string()]) 62 | }).meta({ className: 'AlternativesObjectNoDesc', disableDescription: true }); 63 | -------------------------------------------------------------------------------- /src/__tests__/array/array.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, rmdirSync } from 'fs'; 2 | import Joi from 'joi'; 3 | 4 | import { convertFromDirectory, convertSchema } from '../../index'; 5 | import { SparseTestListSchema } from './schemas/OneSparseSchema'; 6 | 7 | describe('Array types', () => { 8 | const typeOutputDirectory = './src/__tests__/array/interfaces'; 9 | 10 | beforeAll(() => { 11 | if (existsSync(typeOutputDirectory)) { 12 | rmdirSync(typeOutputDirectory, { recursive: true }); 13 | } 14 | }); 15 | 16 | test('array variations from file', async () => { 17 | const result = await convertFromDirectory({ 18 | schemaDirectory: './src/__tests__/array/schemas', 19 | typeOutputDirectory 20 | }); 21 | 22 | expect(result).toBe(true); 23 | 24 | const oneContent = readFileSync(`${typeOutputDirectory}/One.ts`).toString(); 25 | 26 | expect(oneContent).toBe( 27 | `/** 28 | * This file was automatically generated by joi-to-typescript 29 | * Do not modify this file manually 30 | */ 31 | 32 | export interface Item { 33 | name: string; 34 | } 35 | 36 | /** 37 | * a test schema definition 38 | */ 39 | export interface Test { 40 | items?: Item[]; 41 | name?: string; 42 | propertyName1: boolean; 43 | } 44 | 45 | /** 46 | * A list of Test object 47 | */ 48 | export type TestList = Test[]; 49 | ` 50 | ); 51 | }); 52 | 53 | test('test to ensure second items() is ignored', () => { 54 | // this tests this code 55 | //const childrenContent = children.map(child => typeContentToTsHelper(settings, child, indentLevel)); 56 | //if (childrenContent.length > 1) { 57 | // /* istanbul ignore next */ 58 | // throw new Error('Multiple array item types not supported'); 59 | //} 60 | const schema = Joi.array() 61 | .items(Joi.string().description('one')) 62 | .items(Joi.number().description('two')) 63 | .required() 64 | .meta({ className: 'TestList' }) 65 | .description('A list of Test object'); 66 | 67 | const result = convertSchema({ sortPropertiesByName: true }, schema); 68 | expect(result).not.toBeUndefined; 69 | expect(result?.content).toBe(`/** 70 | * A list of Test object 71 | */ 72 | export type TestList = string[];`); 73 | }); 74 | 75 | test('test to ensure sparse arrays have undefined as a possible type', () => { 76 | const result = convertSchema({ sortPropertiesByName: true }, SparseTestListSchema); 77 | expect(result).not.toBeUndefined; 78 | expect(result?.content).toBe(`/** 79 | * A sparse list of Item object 80 | */ 81 | export type SparseTestList = (Item | undefined)[];`); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/__tests__/array/schemas/OneSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const ItemSchema = Joi.object({ 4 | name: Joi.string().required() 5 | }).meta({ className: 'Item' }); 6 | 7 | export const TestSchema = Joi.object({ 8 | name: Joi.string().optional(), 9 | propertyName1: Joi.bool().required(), 10 | items: Joi.array().items(ItemSchema).optional() 11 | }) 12 | .meta({ className: 'Test' }) 13 | .description('a test schema definition'); 14 | 15 | export const TestListSchema = Joi.array() 16 | .items(TestSchema) 17 | .required() 18 | .meta({ className: 'TestList' }) 19 | .description('A list of Test object'); 20 | -------------------------------------------------------------------------------- /src/__tests__/array/schemas/OneSparseSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import {ItemSchema} from './OneSchema' 3 | 4 | export const SparseTestListSchema = Joi.array() 5 | .items(ItemSchema) 6 | .sparse() 7 | .meta({ className: 'SparseTestList' }) 8 | .description('A sparse list of Item object'); -------------------------------------------------------------------------------- /src/__tests__/basic.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | import { convertSchema } from '../index'; 4 | 5 | describe('some basic tests', () => { 6 | test('test the base types', () => { 7 | const schema = Joi.object({ 8 | // basic types 9 | name: Joi.string().optional().description('Test Schema Name'), 10 | propertyName1: Joi.boolean().required(), 11 | dateCreated: Joi.date(), 12 | count: Joi.number(), 13 | obj: Joi.object() 14 | }) 15 | .meta({ className: 'Test' }) 16 | .description('a test schema definition'); 17 | 18 | const result = convertSchema({}, schema); 19 | expect(result).not.toBeUndefined; 20 | expect(result?.content).toBe(`/** 21 | * a test schema definition 22 | */ 23 | export interface Test { 24 | count?: number; 25 | dateCreated?: Date; 26 | /** 27 | * Test Schema Name 28 | */ 29 | name?: string; 30 | obj?: object; 31 | propertyName1: boolean; 32 | }`); 33 | }); 34 | 35 | test('array tests', () => { 36 | const schemaArray = Joi.object({ 37 | // basic types 38 | name: Joi.array().items(Joi.string()).optional(), 39 | propertyName1: Joi.array().items(Joi.boolean()).required(), 40 | dateCreated: Joi.array().items(Joi.date()), 41 | count: Joi.array().items(Joi.number()), 42 | arr: Joi.array() 43 | }) 44 | .meta({ className: 'ArrayObject' }) 45 | .description('an Array test schema definition'); 46 | 47 | const arrayResult = convertSchema({ sortPropertiesByName: true }, schemaArray); 48 | expect(arrayResult).not.toBeUndefined; 49 | 50 | expect(arrayResult?.content).toBe(`/** 51 | * an Array test schema definition 52 | */ 53 | export interface ArrayObject { 54 | arr?: any[]; 55 | count?: number[]; 56 | dateCreated?: Date[]; 57 | name?: string[]; 58 | propertyName1: boolean[]; 59 | }`); 60 | }); 61 | 62 | test('nested types', () => { 63 | const schema = Joi.object({ 64 | nested: Joi.object({ a: Joi.object({ b: Joi.string() }) }), 65 | nestedComments: Joi.object({ a: Joi.object({ b: Joi.string().description('nested comment') }) }), 66 | nestedObject: Joi.object({ 67 | aType: Joi.object().meta({ className: 'Blue' }).description('A blue object property') 68 | }), 69 | 'x.y': Joi.string() 70 | }).meta({ className: 'Test' }); 71 | 72 | const result = convertSchema({ sortPropertiesByName: false }, schema); 73 | expect(result).not.toBeUndefined; 74 | expect(result?.content).toBe(`export interface Test { 75 | nested?: { 76 | a?: { 77 | b?: string; 78 | }; 79 | }; 80 | nestedComments?: { 81 | a?: { 82 | /** 83 | * nested comment 84 | */ 85 | b?: string; 86 | }; 87 | }; 88 | nestedObject?: { 89 | /** 90 | * A blue object property 91 | */ 92 | aType?: Blue; 93 | }; 94 | 'x.y'?: string; 95 | }`); 96 | }); 97 | 98 | test('Uppercase and lowercase property', () => { 99 | const schema = Joi.object({ 100 | a: Joi.string(), 101 | A: Joi.string() 102 | }).meta({ className: 'Test' }); 103 | 104 | const result = convertSchema({ sortPropertiesByName: false }, schema); 105 | expect(result).not.toBeUndefined; 106 | expect(result?.content).toBe(`export interface Test { 107 | a?: string; 108 | A?: string; 109 | }`); 110 | }); 111 | 112 | test('no properties on a schema', () => { 113 | const schema = Joi.object({}).meta({ className: 'Test' }); 114 | 115 | const result = convertSchema({}, schema); 116 | expect(result).not.toBeUndefined; 117 | expect(result?.content).toBe(`export interface Test {}`); 118 | }); 119 | 120 | describe('empty object', () => { 121 | test('empty object ts generation', () => { 122 | const schema = Joi.object({ 123 | field1: Joi.string(), 124 | // Raw empty object 125 | nothing1: {}, 126 | // Joi should allow any key/pair value here, as per docs: https://joi.dev/api/?v=17.9.1#object 127 | // "Defaults to allowing any child key." 128 | nothing2: Joi.object(), 129 | // In this case we forcefully communicate this is a EMPTY object. 130 | nothing3: Joi.object({}), 131 | nothingAppend: Joi.object().append({ hello: Joi.string() }), 132 | allowUnknown1: Joi.object().unknown(true), 133 | allowUnknown2: Joi.object({}).unknown(true), 134 | appended: Joi.object({}).append({ field1: Joi.string() }), 135 | nothingAlternative: Joi.alternatives([Joi.object({}), Joi.object()]) 136 | }).meta({ className: 'Test' }); 137 | 138 | const result = convertSchema({ sortPropertiesByName: false }, schema); 139 | expect(result).not.toBeUndefined; 140 | expect(result?.content).toBe(`export interface Test { 141 | field1?: string; 142 | nothing1?: Record; 143 | nothing2?: object; 144 | nothing3?: Record; 145 | nothingAppend?: { 146 | hello?: string; 147 | }; 148 | allowUnknown1?: { 149 | /** 150 | * Unknown Property 151 | */ 152 | [x: string]: unknown; 153 | }; 154 | allowUnknown2?: { 155 | /** 156 | * Unknown Property 157 | */ 158 | [x: string]: unknown; 159 | }; 160 | appended?: { 161 | field1?: string; 162 | }; 163 | nothingAlternative?: Record | object; 164 | }`); 165 | }); 166 | 167 | describe('empty object matching', () => { 168 | const tests: { 169 | schema: Joi.Schema; 170 | value: unknown; 171 | error?: boolean; 172 | }[] = [ 173 | { 174 | schema: Joi.object(), 175 | value: {}, 176 | error: false 177 | }, 178 | // When no args are passed, unknown values are allowed by default 179 | { 180 | schema: Joi.object(), 181 | value: { 182 | hello: 'world' 183 | }, 184 | error: false 185 | }, 186 | { 187 | schema: Joi.object({}), 188 | value: {}, 189 | error: false 190 | }, 191 | // When explicitly defining {}, no unknown values are allowed by default 192 | { 193 | schema: Joi.object({}), 194 | value: { 195 | hello: 'world' 196 | }, 197 | error: true 198 | }, 199 | { 200 | schema: Joi.object().unknown(false), 201 | value: {}, 202 | error: false 203 | }, 204 | { 205 | schema: Joi.object({}).unknown(false), 206 | value: {}, 207 | error: false 208 | }, 209 | { 210 | schema: Joi.object().unknown(false), 211 | value: { 212 | hello: 'world' 213 | }, 214 | // Maybe unexpected but this is how Joi works. 215 | error: false 216 | }, 217 | { 218 | schema: Joi.object({}).unknown(false), 219 | value: { 220 | hello: 'world' 221 | }, 222 | error: true 223 | }, 224 | { 225 | schema: Joi.object().unknown(true), 226 | value: {}, 227 | error: false 228 | }, 229 | { 230 | schema: Joi.object().unknown(true), 231 | value: { 232 | hello: 'world' 233 | }, 234 | error: false 235 | } 236 | ]; 237 | 238 | test.each(tests)('%#', t => { 239 | const result = t.schema.validate(t.value); 240 | if (t.error) { 241 | expect(result.error).not.toBeUndefined(); 242 | } else { 243 | expect(result.error).toBeUndefined(); 244 | } 245 | }); 246 | }); 247 | }); 248 | }); 249 | -------------------------------------------------------------------------------- /src/__tests__/cast/cast.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, rmdirSync } from 'fs'; 2 | 3 | import { convertFromDirectory } from '../../index'; 4 | 5 | describe('Cast primitive types', () => { 6 | const typeOutputDirectory = './src/__tests__/cast/interfaces'; 7 | 8 | beforeAll(() => { 9 | if (existsSync(typeOutputDirectory)) { 10 | rmdirSync(typeOutputDirectory, { recursive: true }); 11 | } 12 | }); 13 | 14 | test('casts all variations from file', async () => { 15 | const result = await convertFromDirectory({ 16 | schemaDirectory: './src/__tests__/cast/schemas', 17 | typeOutputDirectory 18 | }); 19 | 20 | expect(result).toBe(true); 21 | 22 | const oneContent = readFileSync(`${typeOutputDirectory}/One.ts`).toString(); 23 | 24 | expect(oneContent).toBe( 25 | `/** 26 | * This file was automatically generated by joi-to-typescript 27 | * Do not modify this file manually 28 | */ 29 | 30 | export type NumberType = number; 31 | 32 | export interface Numbers { 33 | bool: number; 34 | day: number; 35 | } 36 | 37 | export type StringType = string; 38 | 39 | export interface Strings { 40 | num: string; 41 | } 42 | ` 43 | ); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/__tests__/cast/schemas/OneSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const NumberSchema = Joi.object({ 4 | bool: Joi.boolean().cast('number').required(), 5 | day: Joi.date().cast('number').required() 6 | }).meta({ className: 'Numbers' }); 7 | 8 | export const StringSchema = Joi.object({ 9 | num: Joi.number().cast('string').required() 10 | }).meta({ className: 'Strings' }); 11 | 12 | export const StringType = Joi.number().cast('string'); 13 | 14 | export const NumberType = Joi.boolean().cast('number'); 15 | -------------------------------------------------------------------------------- /src/__tests__/className/className.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, rmdirSync } from 'fs'; 2 | import Joi from 'joi'; 3 | import { convertFromDirectory, convertSchema, Settings } from '../..'; 4 | import { Describe } from '../../joiDescribeTypes'; 5 | import { ensureInterfaceorTypeName } from '../../joiUtils'; 6 | 7 | describe('test the use of .meta({className: ""})', () => { 8 | const typeOutputDirectory = './src/__tests__/className/interfaces'; 9 | const schemaDirectory = './src/__tests__/className/schemas'; 10 | 11 | beforeAll(() => { 12 | if (existsSync(typeOutputDirectory)) { 13 | rmdirSync(typeOutputDirectory, { recursive: true }); 14 | } 15 | }); 16 | 17 | test('generate className interfaces', async () => { 18 | const consoleSpy = jest.spyOn(console, 'debug'); 19 | const result = await convertFromDirectory({ 20 | schemaDirectory, 21 | typeOutputDirectory, 22 | debug: true 23 | }); 24 | 25 | expect(result).toBe(true); 26 | 27 | expect(existsSync(`${typeOutputDirectory}/index.ts`)).toBeTruthy(); 28 | 29 | expect(consoleSpy).toHaveBeenCalledWith( 30 | "It is recommended you update the Joi Schema 'noClassNameSchema' similar to: noClassNameSchema = Joi.object().meta({className:'noClassName'})" 31 | ); 32 | }); 33 | 34 | test('no className', () => { 35 | const oneContent = readFileSync(`${typeOutputDirectory}/NoClassNameTest.ts`).toString(); 36 | 37 | expect(oneContent).toBe( 38 | `/** 39 | * This file was automatically generated by joi-to-typescript 40 | * Do not modify this file manually 41 | */ 42 | 43 | export interface noClassNametest { 44 | name?: string; 45 | } 46 | ` 47 | ); 48 | }); 49 | 50 | // it would be nice to auto remove this schema suffix but that could break the Joi, the safest is to warn the user about 51 | // how they could do it better 52 | test('no className with schema as suffix', () => { 53 | const oneContent = readFileSync(`${typeOutputDirectory}/NoClassName.ts`).toString(); 54 | 55 | expect(oneContent).toBe( 56 | `/** 57 | * This file was automatically generated by joi-to-typescript 58 | * Do not modify this file manually 59 | */ 60 | 61 | export interface noClassNameSchema { 62 | name?: string; 63 | } 64 | ` 65 | ); 66 | }); 67 | 68 | test('className', () => { 69 | const oneContent = readFileSync(`${typeOutputDirectory}/ClassName.ts`).toString(); 70 | 71 | expect(oneContent).toBe( 72 | `/** 73 | * This file was automatically generated by joi-to-typescript 74 | * Do not modify this file manually 75 | */ 76 | 77 | export interface Frank { 78 | name?: string; 79 | } 80 | ` 81 | ); 82 | }); 83 | 84 | test('className property names', () => { 85 | const oneContent = readFileSync(`${typeOutputDirectory}/ClassNameProperty.ts`).toString(); 86 | 87 | expect(oneContent).toBe( 88 | `/** 89 | * This file was automatically generated by joi-to-typescript 90 | * Do not modify this file manually 91 | */ 92 | 93 | export type Name = string; 94 | 95 | export interface className { 96 | name?: Name; 97 | } 98 | ` 99 | ); 100 | }); 101 | 102 | test('className property names with spaces', () => { 103 | const oneContent = readFileSync(`${typeOutputDirectory}/ClassNamePropertySpaced.ts`).toString(); 104 | 105 | expect(oneContent).toBe( 106 | `/** 107 | * This file was automatically generated by joi-to-typescript 108 | * Do not modify this file manually 109 | */ 110 | 111 | export type CustomerPhoneNumber = string; 112 | 113 | export type EmailAddress = string; 114 | 115 | export type Name = string; 116 | 117 | export interface spacedClassName { 118 | email?: EmailAddress; 119 | name?: Name; 120 | phone?: CustomerPhoneNumber; 121 | } 122 | ` 123 | ); 124 | }); 125 | 126 | test('no meta({className:""}) and no property name', () => { 127 | expect(() => { 128 | convertSchema( 129 | {}, 130 | Joi.object({ 131 | name: Joi.string().optional() 132 | }) 133 | ); 134 | }).toThrowError(); 135 | }); 136 | 137 | test('no meta({}) and no property name', () => { 138 | const details: Describe = { type: 'string', metas: [{ something: '' } as unknown] } as Describe; 139 | ensureInterfaceorTypeName({} as Settings, details, 'Force Me'); 140 | expect(details.metas).toMatchObject([{ something: '' }, { className: 'Force Me' }]); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /src/__tests__/className/schemas/ClassName.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const className = Joi.object({ 4 | name: Joi.string() 5 | }).meta({ className: 'Frank' }); 6 | -------------------------------------------------------------------------------- /src/__tests__/className/schemas/ClassNameProperty.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const Name = Joi.string().meta({ className: 'Name' }); 4 | 5 | export const className = Joi.object({ 6 | name: Name 7 | }); 8 | -------------------------------------------------------------------------------- /src/__tests__/className/schemas/ClassNamePropertySpaced.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const Name = Joi.string().meta({ className: 'Name' }); 4 | 5 | export const EmailAddress = Joi.string().email().meta({ className: 'Email Address' }); 6 | 7 | export const CustomerPhoneNumber = Joi.string().meta({ className: 'Customer Phone Number' }); 8 | 9 | export const spacedClassName = Joi.object({ 10 | name: Name, 11 | email: EmailAddress, 12 | phone: CustomerPhoneNumber 13 | }); 14 | -------------------------------------------------------------------------------- /src/__tests__/className/schemas/NoClassNameSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const noClassNameSchema = Joi.object({ 4 | name: Joi.string() 5 | }); 6 | -------------------------------------------------------------------------------- /src/__tests__/className/schemas/NoClassNameTest.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const noClassNametest = Joi.object({ 4 | name: Joi.string() 5 | }); 6 | -------------------------------------------------------------------------------- /src/__tests__/commentEverything.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | import { convertSchema } from '../index'; 4 | 5 | describe('test the `commentEverything` setting', () => { 6 | test('commentEverything = true', () => { 7 | const schema = Joi.object({ 8 | // basic types 9 | name: Joi.string().optional().description('Test Schema Name'), 10 | propertyName1: Joi.boolean().required(), 11 | dateCreated: Joi.date(), 12 | count: Joi.number() 13 | }) 14 | .meta({ className: 'TestSchema' }) 15 | .description('a test schema definition'); 16 | 17 | const result = convertSchema({ commentEverything: true, sortPropertiesByName: false }, schema); 18 | expect(result).not.toBeUndefined; 19 | expect(result?.content).toBe(`/** 20 | * a test schema definition 21 | */ 22 | export interface TestSchema { 23 | /** 24 | * Test Schema Name 25 | */ 26 | name?: string; 27 | /** 28 | * propertyName1 29 | */ 30 | propertyName1: boolean; 31 | /** 32 | * dateCreated 33 | */ 34 | dateCreated?: Date; 35 | /** 36 | * count 37 | */ 38 | count?: number; 39 | }`); 40 | }); 41 | 42 | test('commentEverything = false', () => { 43 | const schemaArray = Joi.object({ 44 | // basic types 45 | name: Joi.array().items(Joi.string()).optional(), 46 | propertyName1: Joi.array().items(Joi.boolean()).required(), 47 | dateCreated: Joi.array().items(Joi.date()), 48 | count: Joi.array().items(Joi.number()) 49 | }) 50 | .meta({ className: 'ArrayObject' }) 51 | .description('an Array test schema definition'); 52 | 53 | const arrayResult = convertSchema({ commentEverything: true, sortPropertiesByName: false }, schemaArray); 54 | expect(arrayResult).not.toBeUndefined; 55 | 56 | expect(arrayResult?.content).toBe(`/** 57 | * an Array test schema definition 58 | */ 59 | export interface ArrayObject { 60 | /** 61 | * name 62 | */ 63 | name?: string[]; 64 | /** 65 | * propertyName1 66 | */ 67 | propertyName1: boolean[]; 68 | /** 69 | * dateCreated 70 | */ 71 | dateCreated?: Date[]; 72 | /** 73 | * count 74 | */ 75 | count?: number[]; 76 | }`); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/__tests__/concat/index.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, rmdirSync } from 'fs'; 2 | import { convertFromDirectory } from '../..'; 3 | 4 | describe('test the use of joi.concat()', () => { 5 | const typeOutputDirectory = './src/__tests__/concat/interfaces'; 6 | const schemaDirectory = './src/__tests__/concat/schemas'; 7 | 8 | beforeAll(() => { 9 | if (existsSync(typeOutputDirectory)) { 10 | rmdirSync(typeOutputDirectory, { recursive: true }); 11 | } 12 | }); 13 | 14 | test('generate className interfaces', async () => { 15 | const result = await convertFromDirectory({ 16 | schemaDirectory, 17 | typeOutputDirectory, 18 | debug: true 19 | }); 20 | 21 | expect(result).toBe(true); 22 | 23 | expect(existsSync(`${typeOutputDirectory}/index.ts`)).toBeTruthy(); 24 | }); 25 | 26 | // it would be nice to auto remove this schema suffix but that could break the Joi, the safest is to warn the user about 27 | // how they could do it better 28 | test('no className with schema as suffix', () => { 29 | const oneContent = readFileSync(`${typeOutputDirectory}/FooBar.ts`).toString(); 30 | 31 | expect(oneContent).toBe( 32 | `/** 33 | * This file was automatically generated by joi-to-typescript 34 | * Do not modify this file manually 35 | */ 36 | 37 | export interface Bar { 38 | b?: string; 39 | } 40 | 41 | export interface Foo { 42 | a?: string; 43 | } 44 | 45 | export interface FooBar { 46 | a?: string; 47 | b?: string; 48 | } 49 | ` 50 | ); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/__tests__/concat/schemas/FooBarSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const FooSchema = Joi.object({ 4 | a: Joi.string() 5 | }).meta({ className: 'Foo' }); 6 | 7 | export const BarSchema = Joi.object({ 8 | b: Joi.string() 9 | }).meta({ className: 'Bar' }); 10 | 11 | export const FooBarSchema = FooSchema.concat(BarSchema).meta({ className: 'FooBar' }); 12 | -------------------------------------------------------------------------------- /src/__tests__/defaultInterfaceSuffix/defaultInterfaceSuffix.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, rmdirSync } from 'fs'; 2 | 3 | import { convertFromDirectory } from '../../index'; 4 | 5 | const typeOutputDirectory = './src/__tests__/defaultInterfaceSuffix/interfaces'; 6 | 7 | describe('Create interfaces from schema files and applies a default interface suffix', () => { 8 | beforeAll(() => { 9 | if (existsSync(typeOutputDirectory)) { 10 | rmdirSync(typeOutputDirectory, { recursive: true }); 11 | } 12 | }); 13 | 14 | test('generates interfaces', async () => { 15 | const result = await convertFromDirectory({ 16 | schemaDirectory: './src/__tests__/defaultInterfaceSuffix/schemas', 17 | typeOutputDirectory, 18 | defaultInterfaceSuffix: 'Interface' 19 | }); 20 | 21 | expect(result).toBe(true); 22 | 23 | const oneContent = readFileSync(`${typeOutputDirectory}/One.ts`).toString(); 24 | expect(oneContent).toBe( 25 | `/** 26 | * This file was automatically generated by joi-to-typescript 27 | * Do not modify this file manually 28 | */ 29 | 30 | /** 31 | * a test schema definition 32 | */ 33 | export interface TestInterface { 34 | name?: string; 35 | } 36 | 37 | export interface TestWithMetaInterface { 38 | name?: string; 39 | } 40 | ` 41 | ); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/__tests__/defaultInterfaceSuffix/schemas/OneSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const TestSchema = Joi.object({ 4 | name: Joi.string().optional() 5 | }).description('a test schema definition'); 6 | 7 | export const TestWithMetaSchema = Joi.object({ 8 | name: Joi.string().optional() 9 | }).meta({ 10 | myMeta: 'Hello' 11 | }); 12 | -------------------------------------------------------------------------------- /src/__tests__/description/index.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, rmdirSync } from 'fs'; 2 | 3 | import { convertFromDirectory, convertSchema } from '../../index'; 4 | import Joi from 'joi'; 5 | 6 | describe('description', () => { 7 | const typeOutputDirectory = './src/__tests__/description/interfaces'; 8 | 9 | beforeAll(() => { 10 | if (existsSync(typeOutputDirectory)) { 11 | rmdirSync(typeOutputDirectory, { recursive: true }); 12 | } 13 | }); 14 | 15 | test('generates proper descriptions', async () => { 16 | const result = await convertFromDirectory({ 17 | schemaDirectory: './src/__tests__/description/schemas', 18 | typeOutputDirectory, 19 | sortPropertiesByName: false, 20 | commentEverything: true 21 | }); 22 | 23 | expect(result).toBe(true); 24 | 25 | const oneContent = readFileSync(`${typeOutputDirectory}/One.ts`).toString(); 26 | expect(oneContent).toBe( 27 | `/** 28 | * This file was automatically generated by joi-to-typescript 29 | * Do not modify this file manually 30 | */ 31 | 32 | /** 33 | * A schema with an example 34 | * 35 | * @example 36 | * { 37 | * "more": "world" 38 | * } 39 | */ 40 | export interface DescriptionAndExample { 41 | /** 42 | * more 43 | */ 44 | more: string; 45 | } 46 | 47 | /** 48 | * A schema with two examples 49 | * 50 | * @example 51 | * { 52 | * "more": "world" 53 | * } 54 | * @example 55 | * { 56 | * "more": "coffee" 57 | * } 58 | */ 59 | export interface DescriptionAndExamples { 60 | /** 61 | * more 62 | */ 63 | more: string; 64 | } 65 | 66 | /** 67 | * A schema with a short example 68 | * 69 | * @example One liner 70 | */ 71 | export interface DescriptionAndShortExample { 72 | /** 73 | * more 74 | */ 75 | more: string; 76 | } 77 | 78 | export interface DisableDescription { 79 | /** 80 | * thing 81 | */ 82 | thing: string; 83 | } 84 | 85 | /** 86 | * DisableDescriptionObject 87 | */ 88 | export interface DisableDescriptionObject { 89 | /** 90 | * withDescription 91 | */ 92 | withDescription?: { 93 | /** 94 | * A simple description 95 | */ 96 | [pattern: string]: Example; 97 | }; 98 | /** 99 | * withoutDescription 100 | */ 101 | withoutDescription?: { 102 | /** 103 | * A simple description 104 | */ 105 | [pattern: string]: Example; 106 | }; 107 | } 108 | 109 | /** 110 | * A simple description 111 | */ 112 | export interface Example { 113 | /** 114 | * thing 115 | */ 116 | thing: string; 117 | } 118 | 119 | /** 120 | * ExampleAlternatives 121 | */ 122 | export interface ExampleAlternatives { 123 | /** 124 | * alt1 125 | */ 126 | alt1?: 127 | /** 128 | * A string 129 | */ 130 | | string 131 | /** 132 | * A number 133 | */ 134 | | number 135 | /** 136 | * An object 137 | */ 138 | | { 139 | /** 140 | * A value 141 | */ 142 | value?: string; 143 | } 144 | /** 145 | * An array 146 | */ 147 | | string[] 148 | /** 149 | * A tuple 150 | */ 151 | | ([number, 152 | /** 153 | * A string 154 | */ 155 | string, 156 | Item? 157 | ] | null) 158 | /** 159 | * Another tuple 160 | */ 161 | | [ 162 | /** 163 | * A tuple number 164 | */ 165 | number, 166 | string 167 | ] 168 | /** 169 | * A tuple with one item 170 | */ 171 | | [ 172 | /** 173 | * A tuple number 174 | */ 175 | number 176 | ]; 177 | } 178 | 179 | /** 180 | * ExampleAlternativesRaw 181 | */ 182 | export type ExampleAlternativesRaw = 183 | /** 184 | * A string 185 | */ 186 | | string 187 | /** 188 | * A number 189 | */ 190 | | number 191 | /** 192 | * An object 193 | */ 194 | | { 195 | /** 196 | * A value 197 | */ 198 | value?: string; 199 | } 200 | /** 201 | * An array 202 | */ 203 | | string[] 204 | /** 205 | * A tuple 206 | */ 207 | | ([number, 208 | /** 209 | * A string 210 | */ 211 | string, 212 | Item? 213 | ] | null) 214 | /** 215 | * Another tuple 216 | */ 217 | | [ 218 | /** 219 | * A tuple number 220 | */ 221 | number, 222 | string 223 | ] 224 | /** 225 | * A tuple with one item 226 | */ 227 | | [ 228 | /** 229 | * A tuple number 230 | */ 231 | number 232 | ]; 233 | 234 | /** 235 | * This is a long indented description. 236 | * There are many lines! 237 | * 238 | * And more here! 239 | */ 240 | export interface ExampleLong { 241 | /** 242 | * Another description 243 | */ 244 | another: string; 245 | /** 246 | * Not indented description 247 | */ 248 | noIndent?: string; 249 | /** 250 | * Badly indented description 251 | * What a line 252 | */ 253 | badIndent?: string; 254 | /** 255 | * Another badly indented description 256 | * What a line 257 | */ 258 | badIndent2?: string; 259 | } 260 | 261 | /** 262 | * ExampleNewLine 263 | * 264 | * @example 265 | * I have many 266 | * lines! 267 | */ 268 | export interface ExampleNewLine { 269 | /** 270 | * more 271 | */ 272 | more: string; 273 | } 274 | 275 | /** 276 | * Item 277 | */ 278 | export interface Item { 279 | /** 280 | * name 281 | */ 282 | name: string; 283 | } 284 | 285 | /** 286 | * NoComment 287 | */ 288 | export interface NoComment { 289 | /** 290 | * more 291 | */ 292 | more: string; 293 | } 294 | ` 295 | ); 296 | }); 297 | 298 | it('Test JsDoc example spacing', function () { 299 | { 300 | const converted = convertSchema( 301 | {}, 302 | Joi.object({ 303 | hello: Joi.string() 304 | }).example({ 305 | hello: 'world' 306 | }), 307 | 'HelloTest' 308 | ); 309 | expect(converted).toBeDefined(); 310 | expect(converted?.content).toEqual(`/** 311 | * @example 312 | * { 313 | * "hello": "world" 314 | * } 315 | */ 316 | export interface HelloTest { 317 | hello?: string; 318 | }`); 319 | } 320 | 321 | { 322 | const converted = convertSchema( 323 | {}, 324 | Joi.object({ 325 | hello: Joi.string() 326 | }) 327 | .description('A simple description') 328 | .example({ 329 | hello: 'world' 330 | }), 331 | 'HelloTest' 332 | ); 333 | expect(converted).toBeDefined(); 334 | expect(converted?.content).toEqual(`/** 335 | * A simple description 336 | * 337 | * @example 338 | * { 339 | * "hello": "world" 340 | * } 341 | */ 342 | export interface HelloTest { 343 | hello?: string; 344 | }`); 345 | } 346 | 347 | { 348 | const converted = convertSchema( 349 | {}, 350 | Joi.object({ 351 | template: Joi.alternatives([ 352 | Joi.alternatives([ 353 | Joi.string().description(`Alternative 1`), 354 | Joi.array().items(Joi.string()).description(`Alternative 2`) 355 | ]).required().description(` 356 | Some alternatives 357 | `), 358 | Joi.boolean().valid(true).description(` 359 | A boolean 360 | `) 361 | ]).description(` 362 | A multiline 363 | description 364 | `) 365 | }) 366 | .description('A simple description') 367 | .example({ 368 | hello: 'world' 369 | }), 370 | 'HelloTest' 371 | ); 372 | expect(converted).toBeDefined(); 373 | expect(converted?.content).toEqual(`/** 374 | * A simple description 375 | * 376 | * @example 377 | * { 378 | * "hello": "world" 379 | * } 380 | */ 381 | export interface HelloTest { 382 | /** 383 | * A multiline 384 | * description 385 | */ 386 | template?: 387 | /** 388 | * Some alternatives 389 | */ 390 | | ( 391 | /** 392 | * Alternative 1 393 | */ 394 | | string 395 | /** 396 | * Alternative 2 397 | */ 398 | | string[]) 399 | /** 400 | * A boolean 401 | */ 402 | | true; 403 | }`); 404 | } 405 | }); 406 | }); 407 | -------------------------------------------------------------------------------- /src/__tests__/description/schemas/OneSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const exampleSchema = Joi.object({ 4 | thing: Joi.string().required() 5 | }) 6 | .meta({ className: 'Example' }) 7 | .description('A simple description'); 8 | 9 | export const exampleLongSchema = Joi.object({ 10 | another: Joi.string().required().description(` 11 | Another description 12 | `), 13 | noIndent: Joi.string().description(` 14 | Not indented description 15 | `), 16 | badIndent: Joi.string().description(` 17 | Badly indented description 18 | What a line 19 | `), 20 | badIndent2: Joi.string().description(` 21 | Another badly indented description 22 | What a line 23 | `) 24 | }).meta({ className: 'ExampleLong' }).description(` 25 | This is a long indented description. 26 | There are many lines! 27 | 28 | And more here! 29 | `); 30 | 31 | export const noCommentSchema = Joi.object({ 32 | more: Joi.string().required() 33 | }).meta({ className: 'NoComment' }); 34 | 35 | export const disableDescriptionSchema = exampleSchema.meta({ 36 | className: 'DisableDescription', 37 | disableDescription: true 38 | }); 39 | export const disableDescriptionObjectSchema = Joi.object({ 40 | withDescription: Joi.object().pattern(Joi.string(), exampleSchema).meta({ unknownType: exampleSchema }), 41 | withoutDescription: Joi.object() 42 | .pattern(Joi.string(), exampleSchema) 43 | .meta({ unknownType: exampleSchema.meta({ disableDescription: true }) }) 44 | }).meta({ className: 'DisableDescriptionObject' }); 45 | 46 | export const descriptionAndShortExampleSchema = Joi.object({ 47 | more: Joi.string().required() 48 | }) 49 | .description(`A schema with a short example`) 50 | .example('One liner') 51 | .meta({ className: 'DescriptionAndShortExample' }); 52 | 53 | export const descriptionAndExampleSchema = Joi.object({ 54 | more: Joi.string().required() 55 | }) 56 | .description(`A schema with an example`) 57 | .example({ 58 | more: 'world' 59 | }) 60 | .meta({ className: 'DescriptionAndExample' }); 61 | 62 | export const exampleNewLineSchema = Joi.object({ 63 | more: Joi.string().required() 64 | }) 65 | .example( 66 | ` 67 | I have many 68 | lines! 69 | ` 70 | ) 71 | .meta({ className: 'ExampleNewLine' }); 72 | 73 | export const descriptionAndExamplesSchema = Joi.object({ 74 | more: Joi.string().required() 75 | }) 76 | .description(`A schema with two examples`) 77 | .example({ 78 | more: 'world' 79 | }) 80 | .example({ 81 | more: 'coffee' 82 | }) 83 | .meta({ className: 'DescriptionAndExamples' }); 84 | 85 | export const ItemSchema = Joi.object({ 86 | name: Joi.string().required() 87 | }).meta({ className: 'Item' }); 88 | 89 | export const exampleAlternativesSchema = Joi.object({ 90 | alt1: Joi.alternatives([ 91 | Joi.string().description('A string'), 92 | Joi.number().description('A number'), 93 | Joi.object({ 94 | value: Joi.string().description(`A value`) 95 | }).description('An object'), 96 | Joi.array().items(Joi.string()).description(`An array`), 97 | Joi.array() 98 | .ordered(Joi.number().required()) 99 | .ordered(Joi.string().required().description(`A string`)) 100 | .ordered(ItemSchema) 101 | .allow(null) 102 | .description(`A tuple`), 103 | Joi.array() 104 | .ordered(Joi.number().required().description('A tuple number')) 105 | .ordered(Joi.string().required()) 106 | .description(`Another tuple`), 107 | Joi.array().ordered(Joi.number().required().description('A tuple number')).description(`A tuple with one item`) 108 | ]) 109 | }).meta({ className: 'ExampleAlternatives' }); 110 | 111 | export const exampleAlternativesRawSchema = Joi.alternatives([ 112 | Joi.string().description('A string'), 113 | Joi.number().description('A number'), 114 | Joi.object({ 115 | value: Joi.string().description(`A value`) 116 | }).description('An object'), 117 | Joi.array().items(Joi.string()).description(`An array`), 118 | Joi.array() 119 | .ordered(Joi.number().required()) 120 | .ordered(Joi.string().required().description(`A string`)) 121 | .ordered(ItemSchema) 122 | .allow(null) 123 | .description(`A tuple`), 124 | Joi.array() 125 | .ordered(Joi.number().required().description('A tuple number')) 126 | .ordered(Joi.string().required()) 127 | .description(`Another tuple`), 128 | Joi.array().ordered(Joi.number().required().description('A tuple number')).description(`A tuple with one item`) 129 | ]).meta({ className: 'ExampleAlternativesRaw' }); 130 | -------------------------------------------------------------------------------- /src/__tests__/doublequotesEscape/index.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, rmdirSync } from 'fs'; 2 | 3 | import { convertFromDirectory } from '../../index'; 4 | 5 | describe('Use double quotes for string escapes', () => { 6 | const typeOutputDirectory = './src/__tests__/doublequotesEscape/interfaces'; 7 | 8 | test('default behavior / single quotes', async () => { 9 | if (existsSync(typeOutputDirectory)) { 10 | rmdirSync(typeOutputDirectory, { recursive: true }); 11 | } 12 | const result = await convertFromDirectory({ 13 | schemaDirectory: './src/__tests__/doublequotesEscape/schemas', 14 | typeOutputDirectory 15 | }); 16 | 17 | expect(result).toBe(true); 18 | 19 | const readmeContent = readFileSync(`${typeOutputDirectory}/Allow.ts`).toString(); 20 | 21 | expect(readmeContent).toBe(`/** 22 | * This file was automatically generated by joi-to-typescript 23 | * Do not modify this file manually 24 | */ 25 | 26 | export type Blank = string; 27 | 28 | export type BlankNull = string | null | ''; 29 | 30 | /** 31 | * This is date 32 | */ 33 | export type DateOptions = Date | null; 34 | 35 | /** 36 | * Test Schema Name 37 | */ 38 | export type Name = string; 39 | 40 | export type NormalList = 'red' | 'green' | 'blue'; 41 | 42 | export type NormalRequiredList = 'red' | 'green' | 'blue'; 43 | 44 | /** 45 | * nullable 46 | */ 47 | export type NullName = string | null; 48 | 49 | export type NullNumber = number | null; 50 | 51 | export type Numbers = 1 | 2 | 3 | 4 | 5; 52 | `); 53 | }); 54 | 55 | test('doublequoteEscape: false / single quotes', async () => { 56 | if (existsSync(typeOutputDirectory)) { 57 | rmdirSync(typeOutputDirectory, { recursive: true }); 58 | } 59 | const result = await convertFromDirectory({ 60 | schemaDirectory: './src/__tests__/doublequotesEscape/schemas', 61 | typeOutputDirectory, 62 | doublequoteEscape: false, 63 | }); 64 | 65 | expect(result).toBe(true); 66 | 67 | const readmeContent = readFileSync(`${typeOutputDirectory}/Allow.ts`).toString(); 68 | 69 | expect(readmeContent).toBe(`/** 70 | * This file was automatically generated by joi-to-typescript 71 | * Do not modify this file manually 72 | */ 73 | 74 | export type Blank = string; 75 | 76 | export type BlankNull = string | null | ''; 77 | 78 | /** 79 | * This is date 80 | */ 81 | export type DateOptions = Date | null; 82 | 83 | /** 84 | * Test Schema Name 85 | */ 86 | export type Name = string; 87 | 88 | export type NormalList = 'red' | 'green' | 'blue'; 89 | 90 | export type NormalRequiredList = 'red' | 'green' | 'blue'; 91 | 92 | /** 93 | * nullable 94 | */ 95 | export type NullName = string | null; 96 | 97 | export type NullNumber = number | null; 98 | 99 | export type Numbers = 1 | 2 | 3 | 4 | 5; 100 | `); 101 | }); 102 | 103 | test('doublequoteEscape: true / double quotes', async () => { 104 | if (existsSync(typeOutputDirectory)) { 105 | rmdirSync(typeOutputDirectory, { recursive: true }); 106 | } 107 | const result = await convertFromDirectory({ 108 | schemaDirectory: './src/__tests__/doublequotesEscape/schemas', 109 | typeOutputDirectory, 110 | doublequoteEscape: true, 111 | }); 112 | 113 | expect(result).toBe(true); 114 | 115 | const readmeContent = readFileSync(`${typeOutputDirectory}/Allow.ts`).toString(); 116 | 117 | expect(readmeContent).toBe(`/** 118 | * This file was automatically generated by joi-to-typescript 119 | * Do not modify this file manually 120 | */ 121 | 122 | export type Blank = string; 123 | 124 | export type BlankNull = string | null | ""; 125 | 126 | /** 127 | * This is date 128 | */ 129 | export type DateOptions = Date | null; 130 | 131 | /** 132 | * Test Schema Name 133 | */ 134 | export type Name = string; 135 | 136 | export type NormalList = "red" | "green" | "blue"; 137 | 138 | export type NormalRequiredList = "red" | "green" | "blue"; 139 | 140 | /** 141 | * nullable 142 | */ 143 | export type NullName = string | null; 144 | 145 | export type NullNumber = number | null; 146 | 147 | export type Numbers = 1 | 2 | 3 | 4 | 5; 148 | `); 149 | }); 150 | 151 | }); 152 | -------------------------------------------------------------------------------- /src/__tests__/doublequotesEscape/schemas/AllowSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const Name = Joi.string().optional().description('Test Schema Name').allow('').meta({ className: 'Name' }); 4 | export const NullName = Joi.string().optional().description('nullable').allow(null); 5 | export const BlankNull = Joi.string().optional().allow(null, ''); 6 | export const Blank = Joi.string().allow(''); 7 | export const NormalList = Joi.string().allow('red', 'green', 'blue'); 8 | export const NormalRequiredList = Joi.string().allow('red', 'green', 'blue').required(); 9 | export const Numbers = Joi.number().optional().allow(1, 2, 3, 4, 5); 10 | export const NullNumber = Joi.number().optional().allow(null); 11 | export const DateOptions = Joi.date().allow(null).description('This is date'); 12 | -------------------------------------------------------------------------------- /src/__tests__/empty/empty.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, rmdirSync } from 'fs'; 2 | import { convertFromDirectory } from '../../index'; 3 | 4 | describe('empty schema directory', () => { 5 | const typeOutputDirectory = './src/__tests__/empty/interfaces'; 6 | 7 | beforeEach(() => { 8 | if (existsSync(typeOutputDirectory)) { 9 | rmdirSync(typeOutputDirectory, { recursive: true }); 10 | } 11 | }); 12 | 13 | test('Throw and no index file', async () => { 14 | expect(async () => { 15 | await convertFromDirectory({ 16 | schemaDirectory: './src/__tests__/empty/schemas', 17 | typeOutputDirectory 18 | }); 19 | }).rejects.toThrowError(); 20 | 21 | expect(existsSync(`${typeOutputDirectory}/index.ts`)).toBeFalsy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/__tests__/files/index.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, rmdirSync } from 'fs'; 2 | import { convertFromDirectory } from '../..'; 3 | import { InputFileFilter } from '../../types'; 4 | 5 | describe('empty schema directory', () => { 6 | const typeOutputDirectory = './src/__tests__/files/interfaces'; 7 | 8 | beforeAll(() => { 9 | if (existsSync(typeOutputDirectory)) { 10 | rmdirSync(typeOutputDirectory, { recursive: true }); 11 | } 12 | }); 13 | 14 | test('does reading form files work', async () => { 15 | const result = await convertFromDirectory({ 16 | schemaDirectory: './src/__tests__/files/schemas', 17 | typeOutputDirectory, 18 | inputFileFilter: InputFileFilter.ExcludeIndex, 19 | debug: true 20 | }); 21 | 22 | expect(result).toBe(true); 23 | 24 | const baseInterfaceDir = './src/__tests__/files/interfaces/'; 25 | expect(existsSync(`${baseInterfaceDir}One.ts`)).toBe(true); 26 | expect(existsSync(`${baseInterfaceDir}React.tsx`)).toBe(false); 27 | }); 28 | 29 | test.concurrent.each([ 30 | 'index.ts', 31 | 'HelloSchema.ts', 32 | '/red/HelloSchema.ts', 33 | '/HelloSchema.ts', 34 | '/red/blue/BlackSchema.ts' 35 | ])('Default Regular Expression Valid: %s', async item => { 36 | expect(InputFileFilter.Default.test(item)).toBeTruthy(); 37 | }); 38 | 39 | test.concurrent.each(['index.tsx', 'index.t', 'frank', 'readme.md', 'foo.java', 'bar.js', '/bar.js'])( 40 | 'Default Regular Expression invalid: %s', 41 | async item => { 42 | expect(InputFileFilter.Default.test(item)).toBeFalsy(); 43 | } 44 | ); 45 | 46 | test.concurrent.each(['HelloSchema.ts', '/red/HelloSchema.ts', '/HelloSchema.ts', '/red/blue/BlackSchema.ts'])( 47 | 'Exclude Index Regular Expression Valid: %s', 48 | async item => { 49 | expect(InputFileFilter.ExcludeIndex.test(item)).toBeTruthy(); 50 | } 51 | ); 52 | 53 | test.concurrent.each(['index.tsx', 'index.ts', 'index.t', 'frank', 'readme.md', 'foo.java', 'bar.js', '/bar.js'])( 54 | 'Exclude Indext Regular Expression Invalid: %s', 55 | async item => { 56 | expect(InputFileFilter.ExcludeIndex.test(item)).toBeFalsy(); 57 | } 58 | ); 59 | 60 | test.concurrent.each([ 61 | 'index.ts', 62 | 'HelloSchema.ts', 63 | '/red/HelloSchema.ts', 64 | '/HelloSchema.ts', 65 | '/red/blue/BlackSchema.ts', 66 | 'index.js', 67 | 'HelloSchema.js', 68 | '/red/HelloSchema.js', 69 | '/HelloSchema.js', 70 | '/red/blue/BlackSchema.js' 71 | ])('Include JavaScript Regular Expression Valid: %s', async item => { 72 | expect(InputFileFilter.IncludeJavaScript.test(item)).toBeTruthy(); 73 | }); 74 | 75 | test.concurrent.each(['index.tsx', 'index.jsx', 'index.t', 'frank', 'readme.md', 'foo.java', 'bar.cjs', '/bar.jsm'])( 76 | 'Include JavaScript Regular Expression Invalid: %s', 77 | async item => { 78 | expect(InputFileFilter.IncludeJavaScript.test(item)).toBeFalsy(); 79 | } 80 | ); 81 | }); 82 | -------------------------------------------------------------------------------- /src/__tests__/files/schemas/FooBarSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const BarSchema = Joi.object({ 4 | id: Joi.number().required().description('Id').example(1) 5 | }).meta({ className: 'Bar' }); 6 | 7 | export const FooSchema = Joi.object({ 8 | id: Joi.number().required().description('Id').example(1), 9 | bar: BarSchema.required().description('Bar') 10 | }).meta({ className: 'Foo' }); 11 | -------------------------------------------------------------------------------- /src/__tests__/files/schemas/OneSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const TestSchema = Joi.object({ 4 | name: Joi.string().optional(), 5 | propertyName1: Joi.boolean().required(), 6 | 'yellow.flower': Joi.string() 7 | }) 8 | .meta({ className: 'TestSchema' }) 9 | .description('a test schema definition'); 10 | 11 | export const purple = (): void => { 12 | // eslint-disable-next-line no-console 13 | console.log('this is not a schema'); 14 | }; 15 | -------------------------------------------------------------------------------- /src/__tests__/files/schemas/React.tsx: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | export const ReactBarSchema = Joi.object({ 4 | id: Joi.number().required().description('Id').example(1) 5 | }).meta({ className: 'ReactBar' }); 6 | -------------------------------------------------------------------------------- /src/__tests__/files/schemas/Readme.md: -------------------------------------------------------------------------------- 1 | # skip this file 2 | 3 | this is not a schema 4 | -------------------------------------------------------------------------------- /src/__tests__/files/schemas/index.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const TestSchema = Joi.object({ 4 | name: Joi.string().optional(), 5 | propertyName1: Joi.boolean().required(), 6 | 'yellow.flower': Joi.string() 7 | }) 8 | .meta({ className: 'TestSchema' }) 9 | .description('a test schema definition'); 10 | 11 | export const purple = (): void => { 12 | // eslint-disable-next-line no-console 13 | console.log('this is not a schema'); 14 | }; 15 | -------------------------------------------------------------------------------- /src/__tests__/files/schemas/notASchema.ts: -------------------------------------------------------------------------------- 1 | export const red = (): void => { 2 | // eslint-disable-next-line no-console 3 | console.log('this is not a schema'); 4 | }; 5 | -------------------------------------------------------------------------------- /src/__tests__/forbidden.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | import { convertSchema } from '../index'; 4 | 5 | describe('forbidden tests', () => { 6 | test('enums using allow()', () => { 7 | const schema = Joi.object({ 8 | bit: Joi.boolean().forbidden(), 9 | customObject: Joi.object().meta({ className: 'CustomObject' }).forbidden() 10 | }) 11 | .meta({ className: 'TestSchema' }) 12 | .description('a test schema definition'); 13 | 14 | const result = convertSchema({ defaultToRequired: true }, schema); 15 | expect(result).not.toBeUndefined; 16 | expect(result?.content).toBe(`/** 17 | * a test schema definition 18 | */ 19 | export interface TestSchema { 20 | bit: undefined; 21 | customObject: undefined; 22 | }`); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/__tests__/fromFile/fromFile.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, rmdirSync } from 'fs'; 2 | 3 | import { convertFromDirectory } from '../../index'; 4 | 5 | const typeOutputDirectory = './src/__tests__/fromFile/interfaces'; 6 | 7 | describe('Create interfaces from schema files', () => { 8 | beforeAll(() => { 9 | if (existsSync(typeOutputDirectory)) { 10 | rmdirSync(typeOutputDirectory, { recursive: true }); 11 | } 12 | }); 13 | 14 | test('does reading form files work', async () => { 15 | const result = await convertFromDirectory({ 16 | schemaDirectory: './src/__tests__/fromFile/schemas', 17 | typeOutputDirectory 18 | }); 19 | 20 | expect(result).toBe(true); 21 | }); 22 | test('index.ts file contain correct content', () => { 23 | const indexContent = readFileSync(`${typeOutputDirectory}/index.ts`).toString(); 24 | 25 | expect(indexContent).toBe( 26 | `/** 27 | * This file was automatically generated by joi-to-typescript 28 | * Do not modify this file manually 29 | */ 30 | 31 | export * from './FooBar'; 32 | export * from './One'; 33 | ` 34 | ); 35 | }); 36 | test('One.ts file exists and its content', () => { 37 | const oneContent = readFileSync(`${typeOutputDirectory}/One.ts`).toString(); 38 | 39 | expect(oneContent).toBe( 40 | `/** 41 | * This file was automatically generated by joi-to-typescript 42 | * Do not modify this file manually 43 | */ 44 | 45 | /** 46 | * a test schema definition 47 | */ 48 | export interface TestSchema { 49 | 'yellow.flower'?: string; 50 | name?: string; 51 | propertyName1: boolean; 52 | } 53 | ` 54 | ); 55 | }); 56 | 57 | test('FooBar.ts file contain correct content', () => { 58 | const fooBarContent = readFileSync(`${typeOutputDirectory}/FooBar.ts`).toString(); 59 | 60 | expect(fooBarContent).toBe(`/** 61 | * This file was automatically generated by joi-to-typescript 62 | * Do not modify this file manually 63 | */ 64 | 65 | export interface Bar { 66 | /** 67 | * Id 68 | * 69 | * @example 1 70 | */ 71 | id: number; 72 | } 73 | 74 | export interface Foo { 75 | /** 76 | * Bar 77 | */ 78 | bar: Bar; 79 | /** 80 | * Id 81 | * 82 | * @example 1 83 | */ 84 | id: number; 85 | } 86 | `); 87 | }); 88 | }); 89 | 90 | describe('Create interfaces from schema files edge cases', () => { 91 | beforeEach(() => { 92 | if (existsSync(typeOutputDirectory)) { 93 | rmdirSync(typeOutputDirectory, { recursive: true }); 94 | } 95 | }); 96 | 97 | test('input directory that does not exits', async () => { 98 | await expect( 99 | convertFromDirectory({ 100 | schemaDirectory: './src/__tests__/doesnotexist', 101 | typeOutputDirectory 102 | }) 103 | ).rejects.toThrowError(); 104 | }); 105 | 106 | test('create deep output directory that does not exits', async () => { 107 | const deepDirectory = './src/__tests__/fromFile/interfaces/fake1/fake2'; 108 | const result = await convertFromDirectory({ 109 | schemaDirectory: './src/__tests__/fromFile/schemas', 110 | typeOutputDirectory: deepDirectory 111 | }); 112 | 113 | expect(result).toBe(true); 114 | expect(existsSync(deepDirectory)).toBe(true); 115 | }); 116 | 117 | test('debugging on', async () => { 118 | const consoleSpy = jest.spyOn(console, 'debug'); 119 | const result = await convertFromDirectory({ 120 | schemaDirectory: './src/__tests__/fromFile/schemas', 121 | typeOutputDirectory, 122 | debug: true 123 | }); 124 | 125 | expect(result).toBe(true); 126 | expect(consoleSpy).toHaveBeenCalledWith('FooBarSchema.ts - Processing'); 127 | expect(consoleSpy).toHaveBeenCalledWith('OneSchema.ts - Processing'); 128 | expect(consoleSpy).toHaveBeenCalledWith('notASchema.ts - Skipped - no Joi Schemas found'); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /src/__tests__/fromFile/schemas/FooBarSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const BarSchema = Joi.object({ 4 | id: Joi.number().required().description('Id').example(1) 5 | }).meta({ className: 'Bar' }); 6 | 7 | export const FooSchema = Joi.object({ 8 | id: Joi.number().required().description('Id').example(1), 9 | bar: BarSchema.required().description('Bar') 10 | }).meta({ className: 'Foo' }); 11 | -------------------------------------------------------------------------------- /src/__tests__/fromFile/schemas/OneSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const TestSchema = Joi.object({ 4 | name: Joi.string().optional(), 5 | propertyName1: Joi.boolean().required(), 6 | 'yellow.flower': Joi.string() 7 | }) 8 | .meta({ className: 'TestSchema' }) 9 | .description('a test schema definition'); 10 | 11 | export const purple = (): void => { 12 | // eslint-disable-next-line no-console 13 | console.log('this is not a schema'); 14 | }; 15 | -------------------------------------------------------------------------------- /src/__tests__/fromFile/schemas/notASchema.ts: -------------------------------------------------------------------------------- 1 | export const red = (): void => { 2 | // eslint-disable-next-line no-console 3 | console.log('this is not a schema'); 4 | }; 5 | -------------------------------------------------------------------------------- /src/__tests__/headerFooter/index.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, rmdirSync } from 'fs'; 2 | 3 | import { convertFromDirectory } from '../../index'; 4 | 5 | describe('header/footer content', () => { 6 | const typeOutputDirectory = './src/__tests__/headerFooter/interfaces'; 7 | 8 | beforeAll(() => { 9 | if (existsSync(typeOutputDirectory)) { 10 | rmdirSync(typeOutputDirectory, { recursive: true }); 11 | } 12 | }); 13 | 14 | test('headerFooter from file', async () => { 15 | const result = await convertFromDirectory({ 16 | schemaDirectory: './src/__tests__/headerFooter/schemas', 17 | typeOutputDirectory, 18 | sortPropertiesByName: false, 19 | tsContentHeader: type => `// [block "${type.interfaceOrTypeName}" begin]`, 20 | tsContentFooter: type => `// [block "${type.interfaceOrTypeName}" end]` 21 | }); 22 | 23 | expect(result).toBe(true); 24 | 25 | const oneContent = readFileSync(`${typeOutputDirectory}/One.ts`).toString(); 26 | expect(oneContent).toBe( 27 | `/** 28 | * This file was automatically generated by joi-to-typescript 29 | * Do not modify this file manually 30 | */ 31 | 32 | // [block "Basic" begin] 33 | /** 34 | * a description for basic 35 | */ 36 | export type Basic = number | string; 37 | // [block "Basic" end] 38 | 39 | // [block "Other" begin] 40 | export interface Other { 41 | other?: string; 42 | } 43 | // [block "Other" end] 44 | 45 | // [block "Thing" begin] 46 | export interface Thing { 47 | thing: string; 48 | } 49 | // [block "Thing" end] 50 | ` 51 | ); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/__tests__/headerFooter/schemas/OneSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const thingSchema = Joi.object({ 4 | thing: Joi.string().required() 5 | }).meta({ className: 'Thing' }); 6 | 7 | export const otherSchema = Joi.object({ 8 | other: Joi.string().optional() 9 | }).meta({ className: 'Other' }); 10 | 11 | export const basicSchema = Joi.alternatives() 12 | .try(Joi.number(), Joi.string()) 13 | .meta({ className: 'Basic' }) 14 | .description('a description for basic'); 15 | -------------------------------------------------------------------------------- /src/__tests__/ignoreFiles/ignoreFiles.ts: -------------------------------------------------------------------------------- 1 | import { rmdirSync, existsSync } from 'fs'; 2 | 3 | import { convertFromDirectory } from '../../index'; 4 | 5 | describe('ignore Files', () => { 6 | const typeOutputDirectory = './src/__tests__/ignoreFiles/interfaces'; 7 | const schemaDirectory = './src/__tests__/ignoreFiles/schemas'; 8 | 9 | beforeEach(() => { 10 | if (existsSync(typeOutputDirectory)) { 11 | rmdirSync(typeOutputDirectory, { recursive: true }); 12 | } 13 | }); 14 | 15 | test('Ignores file names in ignoreList', async () => { 16 | const ignoreFiles = ['AddressSchema.ts']; 17 | const result = await convertFromDirectory({ 18 | schemaDirectory, 19 | typeOutputDirectory, 20 | ignoreFiles 21 | }); 22 | 23 | expect(result).toBe(true); 24 | 25 | expect(existsSync(`${typeOutputDirectory}/index.ts`)).toBeTruthy(); 26 | expect(existsSync(`${typeOutputDirectory}/One.ts`)).toBeTruthy(); 27 | expect(existsSync(`${typeOutputDirectory}/subDir/index.ts`)).toBeTruthy(); 28 | expect(existsSync(`${typeOutputDirectory}/subDir/Person.ts`)).toBeTruthy(); 29 | expect(existsSync(`${typeOutputDirectory}/subDir/Address.ts`)).toBeFalsy(); 30 | expect(existsSync(`${typeOutputDirectory}/subDir2/index.ts`)).toBeTruthy(); 31 | expect(existsSync(`${typeOutputDirectory}/subDir2/Employee.ts`)).toBeTruthy(); 32 | }); 33 | 34 | test('Ignores folder names in ignoreList', async () => { 35 | const ignoreFiles = ['subDir/']; 36 | const result = await convertFromDirectory({ 37 | schemaDirectory, 38 | typeOutputDirectory, 39 | ignoreFiles 40 | }); 41 | 42 | expect(result).toBe(true); 43 | 44 | expect(existsSync(`${typeOutputDirectory}/index.ts`)).toBeTruthy(); 45 | expect(existsSync(`${typeOutputDirectory}/One.ts`)).toBeTruthy(); 46 | expect(existsSync(`${typeOutputDirectory}/subDir/index.ts`)).toBeFalsy(); 47 | expect(existsSync(`${typeOutputDirectory}/subDir/Person.ts`)).toBeFalsy(); 48 | expect(existsSync(`${typeOutputDirectory}/subDir/Address.ts`)).toBeFalsy(); 49 | expect(existsSync(`${typeOutputDirectory}/subDir2/index.ts`)).toBeTruthy(); 50 | expect(existsSync(`${typeOutputDirectory}/subDir2/Employee.ts`)).toBeTruthy(); 51 | }); 52 | 53 | test('Ignores a file and folder in an ignore list', async () => { 54 | const consoleSpy = jest.spyOn(console, 'debug'); 55 | const ignoreFiles = ['subDir2/', 'OneSchema.ts']; 56 | const result = await convertFromDirectory({ 57 | schemaDirectory, 58 | typeOutputDirectory, 59 | ignoreFiles, 60 | debug: true 61 | }); 62 | 63 | expect(result).toBe(true); 64 | 65 | expect(existsSync(`${typeOutputDirectory}/index.ts`)).toBeFalsy(); 66 | expect(existsSync(`${typeOutputDirectory}/One.ts`)).toBeFalsy(); 67 | expect(existsSync(`${typeOutputDirectory}/subDir/index.ts`)).toBeTruthy(); 68 | expect(existsSync(`${typeOutputDirectory}/subDir/Person.ts`)).toBeTruthy(); 69 | expect(existsSync(`${typeOutputDirectory}/subDir/Address.ts`)).toBeTruthy(); 70 | expect(existsSync(`${typeOutputDirectory}/subDir2/index.ts`)).toBeFalsy(); 71 | expect(existsSync(`${typeOutputDirectory}/subDir2/Employee.ts`)).toBeFalsy(); 72 | 73 | expect(consoleSpy).toHaveBeenCalledWith(expect.stringMatching(/subDir2 because it's in your ignore files list$/)); 74 | expect(consoleSpy).toHaveBeenCalledWith("Skipping OneSchema.ts because it's in your ignore files list"); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/__tests__/ignoreFiles/schemas/OneSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { PersonSchema } from './subDir/PersonSchema'; 3 | 4 | export const ZebraSchema = Joi.object({ 5 | name: Joi.string() 6 | }).meta({ className: 'Zebra' }); 7 | 8 | export const ItemSchema = Joi.object({ 9 | name: Joi.string().required(), 10 | maleZebra: ZebraSchema.description('Male Zebra'), 11 | femaleZebra: ZebraSchema.description('Female Zebra') 12 | }).meta({ className: 'Item' }); 13 | 14 | export const PeopleSchema = Joi.array() 15 | .items(PersonSchema) 16 | .required() 17 | .meta({ className: 'People' }) 18 | .description('A list of People'); 19 | 20 | export const TestSchema = Joi.object({ 21 | name: Joi.string().optional(), 22 | propertyName1: Joi.boolean().required(), 23 | people: PeopleSchema.optional() 24 | }) 25 | .meta({ className: 'Test' }) 26 | .description('a test schema definition'); 27 | -------------------------------------------------------------------------------- /src/__tests__/ignoreFiles/schemas/subDir/AddressSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const AddressSchema = Joi.object({ 4 | addressLineNumber1: Joi.string().required(), 5 | Suburb: Joi.string().required() 6 | }).meta({ className: 'Address' }); 7 | -------------------------------------------------------------------------------- /src/__tests__/ignoreFiles/schemas/subDir/PersonSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { AddressSchema } from './AddressSchema'; 3 | 4 | export const PersonSchema = Joi.object({ 5 | firstName: Joi.string().required(), 6 | lastName: Joi.string().required(), 7 | address: AddressSchema.required() 8 | }).meta({ className: 'Person' }); 9 | -------------------------------------------------------------------------------- /src/__tests__/ignoreFiles/schemas/subDir2/EmployeeSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { PersonSchema } from '../subDir/PersonSchema'; 3 | import { ItemSchema } from '../OneSchema'; 4 | 5 | export const EmployeeSchema = Joi.object({ 6 | personalDetails: PersonSchema.required(), 7 | pet: ItemSchema.required() 8 | }).meta({ className: 'Employee' }); 9 | -------------------------------------------------------------------------------- /src/__tests__/indentation/indent.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, rmdirSync } from 'fs'; 2 | import { convertFromDirectory } from '../..'; 3 | 4 | describe('indentation', () => { 5 | const typeOutputDirectory = './src/__tests__/indentation/interfaces'; 6 | beforeEach(() => { 7 | if (existsSync(typeOutputDirectory)) { 8 | rmdirSync(typeOutputDirectory, { recursive: true }); 9 | } 10 | }); 11 | 12 | test('default indentation', async () => { 13 | const result = await convertFromDirectory({ 14 | schemaDirectory: './src/__tests__/indentation/schemas', 15 | typeOutputDirectory 16 | }); 17 | 18 | expect(result).toBe(true); 19 | 20 | const content = readFileSync(`${typeOutputDirectory}/Nested.ts`).toString(); 21 | 22 | expect(content).toBe(`/** 23 | * This file was automatically generated by joi-to-typescript 24 | * Do not modify this file manually 25 | */ 26 | 27 | export interface Nested { 28 | address?: { 29 | line1: string; 30 | line2?: string; 31 | suburb: string; 32 | }; 33 | connections?: { 34 | frogs?: { 35 | colour: string; 36 | legs?: { 37 | toe: number; 38 | }; 39 | }[]; 40 | name?: string; 41 | type?: string[]; 42 | }[]; 43 | name?: string; 44 | } 45 | `); 46 | }); 47 | 48 | test('4 space indentation', async () => { 49 | const result = await convertFromDirectory({ 50 | schemaDirectory: './src/__tests__/indentation/schemas', 51 | typeOutputDirectory, 52 | indentationChacters: ' ' 53 | }); 54 | 55 | expect(result).toBe(true); 56 | 57 | const content = readFileSync(`${typeOutputDirectory}/Nested.ts`).toString(); 58 | 59 | expect(content).toBe(`/** 60 | * This file was automatically generated by joi-to-typescript 61 | * Do not modify this file manually 62 | */ 63 | 64 | export interface Nested { 65 | address?: { 66 | line1: string; 67 | line2?: string; 68 | suburb: string; 69 | }; 70 | connections?: { 71 | frogs?: { 72 | colour: string; 73 | legs?: { 74 | toe: number; 75 | }; 76 | }[]; 77 | name?: string; 78 | type?: string[]; 79 | }[]; 80 | name?: string; 81 | } 82 | `); 83 | }); 84 | 85 | test('tab indentation', async () => { 86 | const result = await convertFromDirectory({ 87 | schemaDirectory: './src/__tests__/indentation/schemas', 88 | typeOutputDirectory, 89 | indentationChacters: '\t' 90 | }); 91 | 92 | expect(result).toBe(true); 93 | 94 | const content = readFileSync(`${typeOutputDirectory}/Nested.ts`).toString(); 95 | 96 | // Had to use \t as my editor is setup to convert tabs to spaces 97 | expect(content).toBe(`/** 98 | * This file was automatically generated by joi-to-typescript 99 | * Do not modify this file manually 100 | */ 101 | 102 | export interface Nested { 103 | \taddress?: { 104 | \t\tline1: string; 105 | \t\tline2?: string; 106 | \t\tsuburb: string; 107 | \t}; 108 | \tconnections?: { 109 | \t\tfrogs?: { 110 | \t\t\tcolour: string; 111 | \t\t\tlegs?: { 112 | \t\t\t\ttoe: number; 113 | \t\t\t}; 114 | \t\t}[]; 115 | \t\tname?: string; 116 | \t\ttype?: string[]; 117 | \t}[]; 118 | \tname?: string; 119 | } 120 | `); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/__tests__/indentation/schemas/NestedSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const nestedSchema = Joi.object({ 4 | name: Joi.string().optional(), 5 | address: Joi.object({ 6 | line1: Joi.string().required(), 7 | line2: Joi.string().optional(), 8 | suburb: Joi.string().required() 9 | }), 10 | connections: Joi.array().items( 11 | Joi.object({ 12 | name: Joi.string(), 13 | type: Joi.array().items(Joi.string()), 14 | frogs: Joi.array().items( 15 | Joi.object({ colour: Joi.string().required(), legs: Joi.object({ toe: Joi.number().required() }) }) 16 | ) 17 | }) 18 | ) 19 | }).meta({ className: 'Nested' }); 20 | -------------------------------------------------------------------------------- /src/__tests__/interfaceFileSuffix/interfaceFileSuffix.ts: -------------------------------------------------------------------------------- 1 | import {existsSync, rmdirSync} from 'fs'; 2 | 3 | import {convertFromDirectory} from '../../index'; 4 | 5 | const typeOutputDirectory = './src/__tests__/interfaceFileSuffix/interfaces'; 6 | 7 | describe('Create interfaces from schema files with a suffix in the interface filename', () => { 8 | beforeAll(() => { 9 | if (existsSync(typeOutputDirectory)) { 10 | rmdirSync(typeOutputDirectory, {recursive: true}); 11 | } 12 | }); 13 | 14 | test('generates interfaces but no index files', async () => { 15 | const result = await convertFromDirectory({ 16 | schemaDirectory: './src/__tests__/interfaceFileSuffix/schemas', 17 | interfaceFileSuffix: '.generated', 18 | typeOutputDirectory 19 | }); 20 | 21 | expect(result) 22 | .toBe(true); 23 | 24 | expect(existsSync(`${typeOutputDirectory}/One.generated.ts`)) 25 | .toBe(true) 26 | 27 | // Index file should be untouched 28 | expect(existsSync(`${typeOutputDirectory}/index.ts`)) 29 | .toBe(true) 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/__tests__/interfaceFileSuffix/schemas/OneSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const TestSchema = Joi.object({ 4 | name: Joi.string().optional(), 5 | propertyName1: Joi.boolean().required(), 6 | 'yellow.flower': Joi.string() 7 | }) 8 | .meta({ className: 'TestSchema' }) 9 | .description('a test schema definition'); 10 | 11 | export const purple = (): void => { 12 | // eslint-disable-next-line no-console 13 | console.log('this is not a schema'); 14 | }; 15 | -------------------------------------------------------------------------------- /src/__tests__/joiExtensions/joiExtensions.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { convertSchema } from '../..'; 3 | 4 | // Add a couple of extensions to Joi, one without specifying the base type 5 | const ExtendedJoi = Joi.extend(joi => { 6 | const ext: Joi.Extension = { 7 | type: 'objectId', 8 | base: joi.string().meta({ baseType: 'string' }) 9 | }; 10 | return ext; 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | }).extend((joi: any) => { 13 | const ext: Joi.Extension = { 14 | type: 'dollars', 15 | base: joi.number() 16 | }; 17 | return ext; 18 | }); 19 | 20 | describe('Joi Extensions', () => { 21 | test('An extended type with baseType set in metadata', () => { 22 | const schema = Joi.object({ 23 | doStuff: ExtendedJoi.objectId() 24 | }).meta({ className: 'Test' }); 25 | 26 | const result = convertSchema({ debug: true }, schema); 27 | expect(result).not.toBeUndefined; 28 | // prettier-ignore 29 | expect(result?.content).toBe( 30 | [ 31 | 'export interface Test {', 32 | ' doStuff?: string;', 33 | '}' 34 | ].join('\n') 35 | ); 36 | }); 37 | 38 | test('An extended type with baseType not set in metadata', () => { 39 | const schema = Joi.object({ 40 | doStuff: ExtendedJoi.dollars() 41 | }).meta({ className: 'Test' }); 42 | 43 | const result = convertSchema({ debug: true }, schema); 44 | expect(result).not.toBeUndefined; 45 | // prettier-ignore 46 | expect(result?.content).toBe( 47 | [ 48 | 'export interface Test {', 49 | ' doStuff?: unknown;', 50 | '}' 51 | ].join('\n') 52 | ); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/__tests__/joiTypes.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { convertSchema } from '..'; 3 | 4 | // if I called this file just `types.ts` the vscode debugger ran all tests no just this file 5 | 6 | // If this test fails it could mean there is breaking changes in Joi 7 | describe('`Joi.types()`', () => { 8 | test('list of types', () => { 9 | const types = Joi.types(); 10 | 11 | const listOfTypes = [ 12 | 'alternatives', // Basic support 13 | 'any', // Supported 14 | 'array', // Supported 15 | 'boolean', // Supported 16 | 'date', // Supported 17 | 'function', // Basic Support 18 | 'link', // Not Supported - Might be possible 19 | 'number', // Supported 20 | 'object', // Supported 21 | 'string', // Supported 22 | 'symbol', // Not Supported - Might be possible 23 | 'binary', // Not Supported - Should be supported 24 | 'alt', // Supported 25 | 'bool', // Supported 26 | 'func' // Not Supported 27 | ]; 28 | 29 | // Joi.bool an Joi.boolean both output as 'boolean' 30 | // Joi.alt and Joi.alternatives both output as 'alternatives' 31 | expect(Object.keys(types)).toMatchObject(listOfTypes); 32 | }); 33 | 34 | test('Joi.function()', () => { 35 | const schema = Joi.object({ 36 | doStuff: Joi.function(), 37 | stuff: Joi.function().required(), 38 | moreThings: Joi.func() 39 | }).meta({ className: 'Test' }); 40 | 41 | const result = convertSchema({ debug: true }, schema); 42 | expect(result).not.toBeUndefined; 43 | expect(result?.content).toBe( 44 | [ 45 | 'export interface Test {', 46 | ' doStuff?: ((...args: any[]) => any);', 47 | ' moreThings?: ((...args: any[]) => any);', 48 | ' stuff: ((...args: any[]) => any);', 49 | '}' 50 | ].join('\n') 51 | ); 52 | }); 53 | 54 | test('Joi.function() bare value', () => { 55 | const schema = Joi.function().meta({ className: 'Test' }); 56 | 57 | const result = convertSchema({ debug: true }, schema); 58 | expect(result).not.toBeUndefined; 59 | expect(result?.content).toBe(['export type Test = ((...args: any[]) => any);'].join('\n')); 60 | }); 61 | 62 | // TODO: It might be possible to support link 63 | // I guess this would find the referenced schema and get its type 64 | test('Joi.link()', () => { 65 | const schema = Joi.object({ 66 | doStuff: Joi.link() 67 | }).meta({ className: 'Test' }); 68 | 69 | const result = convertSchema({ debug: true }, schema); 70 | expect(result).not.toBeUndefined; 71 | // prettier-ignore 72 | expect(result?.content).toBe( 73 | [ 74 | 'export interface Test {', 75 | ' doStuff?: unknown;', 76 | '}' 77 | ].join('\n') 78 | ); 79 | }); 80 | 81 | test('Joi.symbol()', () => { 82 | const schema = Joi.object({ 83 | doStuff: Joi.symbol() 84 | }).meta({ className: 'Test' }); 85 | 86 | const result = convertSchema({ debug: true }, schema); 87 | expect(result).not.toBeUndefined; 88 | // prettier-ignore 89 | expect(result?.content).toBe( 90 | [ 91 | 'export interface Test {', 92 | ' doStuff?: symbol;', 93 | '}' 94 | ].join('\n') 95 | ); 96 | }); 97 | 98 | // TODO: Support Binary 99 | test('Joi.binary()', () => { 100 | const schema = Joi.object({ 101 | doStuff: Joi.binary() 102 | }).meta({ className: 'Test' }); 103 | 104 | const result = convertSchema({ debug: true }, schema); 105 | expect(result).not.toBeUndefined; 106 | // prettier-ignore 107 | expect(result?.content).toBe( 108 | [ 109 | 'export interface Test {', 110 | ' doStuff?: Buffer;', 111 | '}' 112 | ].join('\n') 113 | ); 114 | }); 115 | 116 | test('Joi.any()', () => { 117 | const schema = Joi.object({ 118 | one: Joi.any(), 119 | // Force another type via meta 120 | two: Joi.any().meta({ baseType: 'number | string' }) 121 | }).meta({ className: 'Test' }); 122 | 123 | const result = convertSchema({ debug: true }, schema); 124 | expect(result).not.toBeUndefined; 125 | expect(result?.content).toBe( 126 | ` 127 | export interface Test { 128 | one?: any; 129 | two?: number | string; 130 | } 131 | `.trim() 132 | ); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /src/__tests__/label/label.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, rmdirSync } from 'fs'; 2 | import Joi from 'joi'; 3 | import { convertFromDirectory, convertSchema } from '../..'; 4 | 5 | describe('test the use of .label()', () => { 6 | const typeOutputDirectory = './src/__tests__/label/interfaces'; 7 | const schemaDirectory = './src/__tests__/label/schemas'; 8 | 9 | beforeAll(() => { 10 | if (existsSync(typeOutputDirectory)) { 11 | rmdirSync(typeOutputDirectory, { recursive: true }); 12 | } 13 | }); 14 | 15 | test('generate label interfaces', async () => { 16 | const consoleSpy = jest.spyOn(console, 'debug'); 17 | const result = await convertFromDirectory({ 18 | schemaDirectory, 19 | typeOutputDirectory, 20 | debug: true, 21 | useLabelAsInterfaceName: true 22 | }); 23 | 24 | expect(result).toBe(true); 25 | 26 | expect(existsSync(`${typeOutputDirectory}/index.ts`)).toBeTruthy(); 27 | 28 | expect(consoleSpy).toHaveBeenCalledWith( 29 | "It is recommended you update the Joi Schema 'nolabelSchema' similar to: nolabelSchema = Joi.object().label('nolabel')" 30 | ); 31 | }); 32 | 33 | test('no label', () => { 34 | const oneContent = readFileSync(`${typeOutputDirectory}/NoLabelTest.ts`).toString(); 35 | 36 | expect(oneContent).toBe( 37 | `/** 38 | * This file was automatically generated by joi-to-typescript 39 | * Do not modify this file manually 40 | */ 41 | 42 | export interface nolabeltest { 43 | name?: string; 44 | } 45 | ` 46 | ); 47 | }); 48 | 49 | // it would be nice to auto remove this schema suffix but that could break the Joi, the safest is to warn the user about 50 | // how they could do it better 51 | test('no label with schema as suffix', () => { 52 | const oneContent = readFileSync(`${typeOutputDirectory}/NoLabel.ts`).toString(); 53 | 54 | expect(oneContent).toBe( 55 | `/** 56 | * This file was automatically generated by joi-to-typescript 57 | * Do not modify this file manually 58 | */ 59 | 60 | export interface nolabelSchema { 61 | name?: string; 62 | } 63 | ` 64 | ); 65 | }); 66 | 67 | test('label', () => { 68 | const oneContent = readFileSync(`${typeOutputDirectory}/Label.ts`).toString(); 69 | 70 | expect(oneContent).toBe( 71 | `/** 72 | * This file was automatically generated by joi-to-typescript 73 | * Do not modify this file manually 74 | */ 75 | 76 | export interface Frank { 77 | name?: string; 78 | } 79 | ` 80 | ); 81 | }); 82 | 83 | test('labeled property names', () => { 84 | const oneContent = readFileSync(`${typeOutputDirectory}/LabelProperty.ts`).toString(); 85 | 86 | expect(oneContent).toBe( 87 | `/** 88 | * This file was automatically generated by joi-to-typescript 89 | * Do not modify this file manually 90 | */ 91 | 92 | export type Name = string; 93 | 94 | export interface label { 95 | name?: Name; 96 | } 97 | ` 98 | ); 99 | }); 100 | 101 | test('labeled property names with spaces', () => { 102 | const oneContent = readFileSync(`${typeOutputDirectory}/LabelPropertySpaced.ts`).toString(); 103 | 104 | expect(oneContent).toBe( 105 | `/** 106 | * This file was automatically generated by joi-to-typescript 107 | * Do not modify this file manually 108 | */ 109 | 110 | export type CustomerPhoneNumber = string; 111 | 112 | export type EmailAddress = string; 113 | 114 | export type Name = string; 115 | 116 | export interface spacedLabel { 117 | email?: EmailAddress; 118 | name?: Name; 119 | phone?: CustomerPhoneNumber; 120 | } 121 | ` 122 | ); 123 | }); 124 | 125 | test('no label() and no property name', () => { 126 | expect(() => { 127 | convertSchema( 128 | { useLabelAsInterfaceName: true }, 129 | Joi.object({ 130 | name: Joi.string().optional() 131 | }) 132 | ); 133 | }).toThrowError(); 134 | }); 135 | 136 | test('Joi.id() instead of Joi.label()', () => { 137 | const schema = Joi.object({ 138 | name: Joi.string() 139 | }).id('Test'); 140 | try { 141 | convertSchema({ debug: true, useLabelAsInterfaceName: true }, schema); 142 | expect(true).toBe(false); 143 | } catch (error) { 144 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 145 | expect(error && (error as any).message).toBe( 146 | 'At least one "object" does not have .label(\'\'). Details: {"type":"object","flags":{"id":"Test"},"keys":{"name":{"type":"string"}}}' 147 | ); 148 | } 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /src/__tests__/label/schemas/Label.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const label = Joi.object({ 4 | name: Joi.string() 5 | }).label('Frank'); 6 | -------------------------------------------------------------------------------- /src/__tests__/label/schemas/LabelProperty.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const Name = Joi.string().label('Name'); 4 | 5 | export const label = Joi.object({ 6 | name: Name 7 | }); 8 | -------------------------------------------------------------------------------- /src/__tests__/label/schemas/LabelPropertySpaced.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const Name = Joi.string().label('Name'); 4 | 5 | export const EmailAddress = Joi.string().email().label('Email Address'); 6 | 7 | export const CustomerPhoneNumber = Joi.string().label('Customer Phone Number'); 8 | 9 | export const spacedLabel = Joi.object({ 10 | name: Name, 11 | email: EmailAddress, 12 | phone: CustomerPhoneNumber 13 | }); 14 | -------------------------------------------------------------------------------- /src/__tests__/label/schemas/NoLabelSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const nolabelSchema = Joi.object({ 4 | name: Joi.string() 5 | }); 6 | -------------------------------------------------------------------------------- /src/__tests__/label/schemas/NoLabelTest.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const nolabeltest = Joi.object({ 4 | name: Joi.string() 5 | }); 6 | -------------------------------------------------------------------------------- /src/__tests__/multipleFiles/multipleFiles.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, rmdirSync } from 'fs'; 2 | 3 | import { convertFromDirectory } from '../../index'; 4 | 5 | const typeOutputDirectory = './src/__tests__/multipleFiles/interfaces'; 6 | 7 | describe('can files reference interfaces between schema files', () => { 8 | beforeEach(() => { 9 | if (existsSync(typeOutputDirectory)) { 10 | rmdirSync(typeOutputDirectory, { recursive: true }); 11 | } 12 | }); 13 | 14 | test('multipleFiles', async () => { 15 | const result = await convertFromDirectory({ 16 | schemaDirectory: './src/__tests__/multipleFiles/schemas', 17 | typeOutputDirectory 18 | }); 19 | 20 | expect(result).toBe(true); 21 | 22 | const oneContent = readFileSync(`${typeOutputDirectory}/One.ts`).toString(); 23 | 24 | expect(oneContent).toBe( 25 | `/** 26 | * This file was automatically generated by joi-to-typescript 27 | * Do not modify this file manually 28 | */ 29 | 30 | import { Person } from '.'; 31 | 32 | export interface Item { 33 | /** 34 | * Female Zebra 35 | */ 36 | femaleZebra?: Zebra; 37 | /** 38 | * Male Zebra 39 | */ 40 | maleZebra?: Zebra; 41 | name: string; 42 | } 43 | 44 | /** 45 | * A list of People 46 | */ 47 | export type People = Person[]; 48 | 49 | /** 50 | * a test schema definition 51 | */ 52 | export interface Test { 53 | name?: string; 54 | /** 55 | * A list of People 56 | */ 57 | people?: People; 58 | propertyName1: boolean; 59 | } 60 | 61 | export interface Zebra { 62 | name?: string; 63 | } 64 | ` 65 | ); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/__tests__/multipleFiles/schemas/OneSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { PersonSchema } from './PersonSchema'; 3 | 4 | export const ZebraSchema = Joi.object({ 5 | name: Joi.string() 6 | }).meta({ className: 'Zebra' }); 7 | 8 | export const ItemSchema = Joi.object({ 9 | name: Joi.string().required(), 10 | maleZebra: ZebraSchema.description('Male Zebra'), 11 | femaleZebra: ZebraSchema.description('Female Zebra') 12 | }).meta({ className: 'Item' }); 13 | 14 | export const PeopleSchema = Joi.array() 15 | .items(PersonSchema) 16 | .required() 17 | .meta({ className: 'People' }) 18 | .description('A list of People'); 19 | 20 | export const TestSchema = Joi.object({ 21 | name: Joi.string().optional(), 22 | propertyName1: Joi.boolean().required(), 23 | people: PeopleSchema.optional() 24 | }) 25 | .meta({ className: 'Test' }) 26 | .description('a test schema definition'); 27 | -------------------------------------------------------------------------------- /src/__tests__/multipleFiles/schemas/PersonSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const PersonSchema = Joi.object({ 4 | firstName: Joi.string().required(), 5 | lastName: Joi.string().required() 6 | }).meta({ className: 'Person' }); 7 | -------------------------------------------------------------------------------- /src/__tests__/noIndex/noIndex.ts: -------------------------------------------------------------------------------- 1 | import {existsSync, readFileSync, rmdirSync} from 'fs'; 2 | 3 | import {convertFromDirectory} from '../../index'; 4 | 5 | const typeOutputDirectory = './src/__tests__/noIndex/interfaces'; 6 | 7 | describe('Create interfaces from schema files but not index files', () => { 8 | beforeAll(() => { 9 | if (existsSync(typeOutputDirectory)) { 10 | rmdirSync(typeOutputDirectory, {recursive: true}); 11 | } 12 | }); 13 | 14 | test('generates interfaces but no index files', async () => { 15 | const result = await convertFromDirectory({ 16 | schemaDirectory: './src/__tests__/noIndex/schemas', 17 | omitIndexFiles: true, 18 | typeOutputDirectory 19 | }); 20 | 21 | expect(result) 22 | .toBe(true); 23 | 24 | expect(existsSync(`${typeOutputDirectory}/index.ts`)) 25 | .toBe(false) 26 | 27 | const fooBarContent = readFileSync(`${typeOutputDirectory}/FooBar.ts`).toString(); 28 | 29 | // The import should properly point to a file, and not to the root folder 30 | expect(fooBarContent).toContain(`import { InnerInterface } from './Inner';`) 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/__tests__/noIndex/schemas/FooBarSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { InnerSchema } from './InnerSchema'; 3 | 4 | export const BarSchema = Joi.object({ 5 | id: Joi.number().required().description('Id').example(1), 6 | inner: InnerSchema 7 | }).meta({ className: 'Bar' }); 8 | 9 | export const FooSchema = Joi.object({ 10 | id: Joi.number().required().description('Id').example(1), 11 | bar: BarSchema.required().description('Bar') 12 | }).meta({ className: 'Foo' }); 13 | -------------------------------------------------------------------------------- /src/__tests__/noIndex/schemas/InnerSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const InnerSchema = Joi.object({ 4 | hello: Joi.string() 5 | }).meta({ className: 'InnerInterface' }); 6 | -------------------------------------------------------------------------------- /src/__tests__/noIndex/schemas/OneSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const TestSchema = Joi.object({ 4 | name: Joi.string().optional(), 5 | propertyName1: Joi.boolean().required(), 6 | 'yellow.flower': Joi.string() 7 | }) 8 | .meta({ className: 'TestSchema' }) 9 | .description('a test schema definition'); 10 | 11 | export const purple = (): void => { 12 | // eslint-disable-next-line no-console 13 | console.log('this is not a schema'); 14 | }; 15 | -------------------------------------------------------------------------------- /src/__tests__/noIndex/schemas/notASchema.ts: -------------------------------------------------------------------------------- 1 | export const red = (): void => { 2 | // eslint-disable-next-line no-console 3 | console.log('this is not a schema'); 4 | }; 5 | -------------------------------------------------------------------------------- /src/__tests__/none/none.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, rmdirSync } from 'fs'; 2 | import { convertFromDirectory } from '../../index'; 3 | 4 | describe('no schemas in directory', () => { 5 | const typeOutputDirectory = './src/__tests__/none/interfaces'; 6 | 7 | beforeEach(() => { 8 | if (existsSync(typeOutputDirectory)) { 9 | rmdirSync(typeOutputDirectory, { recursive: true }); 10 | } 11 | }); 12 | 13 | test('Throw and no index file', async () => { 14 | expect(async () => { 15 | await convertFromDirectory({ 16 | schemaDirectory: './src/__tests__/none/schemas', 17 | typeOutputDirectory 18 | }); 19 | }).rejects.toThrowError(); 20 | 21 | expect(existsSync(`${typeOutputDirectory}/index.ts`)).toBeFalsy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/__tests__/none/schemas/notASchema.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-console 2 | export const log = (): void => console.log('hello world'); 3 | -------------------------------------------------------------------------------- /src/__tests__/override.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { convertSchema } from '../index'; 3 | 4 | describe('override tests', () => { 5 | test("figure out what joi's doing", () => { 6 | const val = Joi.override; 7 | expect(val).toBe(Joi.override); 8 | if (val !== Joi.override) throw 'not equal'; // double checking 9 | }); 10 | 11 | test('control: valid without Joi.override', () => { 12 | const schema = Joi.object({ 13 | foo: Joi.string().valid('val1', 'val2').valid('val3', 'val4') 14 | }) 15 | .meta({ className: 'OverrideSchema' }) 16 | .description('a test schema definition'); 17 | 18 | const result = convertSchema({ defaultToRequired: true }, schema); 19 | expect(result).not.toBeUndefined; 20 | 21 | expect(result?.content).toBe(`/** 22 | * a test schema definition 23 | */ 24 | export interface OverrideSchema { 25 | foo: 'val1' | 'val2' | 'val3' | 'val4'; 26 | }`); 27 | }); 28 | 29 | test('no error with valid() strings and Joi.override', () => { 30 | const schema = Joi.object({ 31 | foo: Joi.string().valid('val1', 'val2').valid(Joi.override, 'val3', 'val4') 32 | }) 33 | .meta({ className: 'OverrideSchema' }) 34 | .description('a test schema definition'); 35 | 36 | // shouldn't throw anything 37 | const result = convertSchema({ defaultToRequired: true }, schema); 38 | 39 | expect(result).not.toBeUndefined; 40 | expect(result?.content).toBe(`/** 41 | * a test schema definition 42 | */ 43 | export interface OverrideSchema { 44 | foo: 'val3' | 'val4'; 45 | }`); 46 | }); 47 | 48 | test('control: no error with valid() numbers without Joi.override', () => { 49 | const schema = Joi.object({ 50 | foo: Joi.number().valid(12, 34).valid(56, 78) 51 | }) 52 | .meta({ className: 'OverrideSchema' }) 53 | .description('a test schema definition'); 54 | 55 | const result = convertSchema({ defaultToRequired: true }, schema); 56 | 57 | expect(result).not.toBeUndefined; 58 | expect(result?.content).toBe(`/** 59 | * a test schema definition 60 | */ 61 | export interface OverrideSchema { 62 | foo: 12 | 34 | 56 | 78; 63 | }`); 64 | }); 65 | 66 | test('no error with valid() numbers and Joi.override', () => { 67 | const schema = Joi.object({ 68 | foo: Joi.number().valid(12, 34).valid(Joi.override, 56, 78) 69 | }) 70 | .meta({ className: 'OverrideSchema' }) 71 | .description('a test schema definition'); 72 | 73 | const result = convertSchema({ defaultToRequired: true }, schema); 74 | 75 | expect(result).not.toBeUndefined; 76 | expect(result?.content).toBe(`/** 77 | * a test schema definition 78 | */ 79 | export interface OverrideSchema { 80 | foo: 56 | 78; 81 | }`); 82 | }); 83 | 84 | // skipping -- these all fail: 85 | // original String sends allows (of non strings) to parseStringSchema 86 | // and then to toStringLiteral 87 | // details.allow parsing probably needs a more generic approach 88 | 89 | test.skip('[existing bug] control: no error with Joi.string().allow(null).allow(Joi.number())', () => { 90 | const schema = Joi.object({ 91 | foo: Joi.string().allow(null).allow(Joi.number()) 92 | // same as: .allow(null, Joi.number()) 93 | }) 94 | .meta({ className: 'OverrideSchema' }) 95 | .description('a test schema definition'); 96 | 97 | const result = convertSchema({ defaultToRequired: true }, schema); 98 | 99 | expect(result).not.toBeUndefined; 100 | expect(result?.content).toBe(`/** 101 | * a test schema definition 102 | */ 103 | export interface OverrideSchema { 104 | foo: string | null | number; 105 | }`); 106 | }); 107 | 108 | test.skip('[existing bug] no error with Joi.string().allow(null).allow(Joi.override, Joi.number())', () => { 109 | const schema = Joi.object({ 110 | foo: Joi.string().allow(null).allow(Joi.override, Joi.number()) 111 | }) 112 | .meta({ className: 'OverrideSchema' }) 113 | .description('a test schema definition'); 114 | 115 | const result = convertSchema({ defaultToRequired: true }, schema); 116 | 117 | expect(result).not.toBeUndefined; 118 | expect(result?.content).toBe(`/** 119 | * a test schema definition 120 | */ 121 | export interface OverrideSchema { 122 | foo: string | number; 123 | }`); 124 | }); 125 | 126 | test.skip('[not implemented] no error with any().invalid(Joi.string())', () => { 127 | const schema = Joi.object({ 128 | foo: Joi.any().invalid(Joi.string()) 129 | // .invalid( Joi.string() ).invalid( Joi.number() ) 130 | }) 131 | .meta({ className: 'OverrideSchema' }) 132 | .description('a test schema definition'); 133 | 134 | const result = convertSchema({ defaultToRequired: true }, schema); 135 | 136 | expect(result).not.toBeUndefined; 137 | expect(result?.content).toBe(`/** 138 | * a test schema definition 139 | */ 140 | export interface OverrideSchema { 141 | foo: Omit; 142 | }`); 143 | }); 144 | 145 | test.skip('[not implemented] no error with any().invalid(string).invalid(Joi.override, number)', () => { 146 | const schema = Joi.object({ 147 | foo: Joi.any().invalid(Joi.string()).invalid(Joi.override, Joi.number()) 148 | }) 149 | .meta({ className: 'OverrideSchema' }) 150 | .description('a test schema definition'); 151 | 152 | const result = convertSchema({ defaultToRequired: true }, schema); 153 | 154 | expect(result).not.toBeUndefined; 155 | expect(result?.content).toBe(`/** 156 | * a test schema definition 157 | */ 158 | export interface OverrideSchema { 159 | foo: Omit; 160 | }`); 161 | }); 162 | 163 | test.skip('[unfinished] try mixing .valid() .allow() .invalid() on one schema', () => { 164 | throw new Error('not implemented'); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /src/__tests__/patterns.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | import { convertSchema } from '../index'; 4 | 5 | describe('test `Joi.object().pattern()`', () => { 6 | test('`pattern(Joi.string(), Joi.AnySchema())`', () => { 7 | const schema = Joi.object() 8 | .pattern(Joi.string(), Joi.array().items(Joi.object({ 9 | id: Joi.string().required(), 10 | propertyName1: Joi.boolean().required() 11 | }))) 12 | .description('a test pattern schema definition'); 13 | 14 | const result = convertSchema({}, schema, 'TestSchema'); 15 | expect(result).not.toBeUndefined; 16 | expect(result?.content).toBe(`/** 17 | * a test pattern schema definition 18 | */ 19 | export interface TestSchema { 20 | [pattern: string]: { 21 | id: string; 22 | propertyName1: boolean; 23 | }[]; 24 | }`); 25 | }); 26 | 27 | test('`pattern(Joi.string(), Joi.number())`', () => { 28 | const schema = Joi.object() 29 | .description('a test deep pattern schema definition') 30 | .pattern(Joi.string(), Joi.number().description('Number Property')); 31 | 32 | const result = convertSchema({}, schema, 'TestSchema'); 33 | expect(result).not.toBeUndefined; 34 | expect(result?.content).toBe(`/** 35 | * a test deep pattern schema definition 36 | */ 37 | export interface TestSchema { 38 | /** 39 | * Number Property 40 | */ 41 | [pattern: string]: number; 42 | }`); 43 | }); 44 | 45 | test('`pattern(/^test$/, Joi.AnySchema())`', () => { 46 | const schema = Joi.object({ 47 | name: Joi.string(), 48 | }) 49 | .description('a test regex pattern schema definition') 50 | .pattern(Joi.string(), { 51 | name: Joi.string().optional(), 52 | propertyName1: Joi.object().pattern(/^test$/, Joi.object({ 53 | propertyName2: Joi.boolean() 54 | })).required() 55 | }); 56 | 57 | const result = convertSchema({ sortPropertiesByName: false }, schema, 'TestSchema'); 58 | expect(result).not.toBeUndefined; 59 | expect(result?.content).toBe(`/** 60 | * a test regex pattern schema definition 61 | */ 62 | export interface TestSchema { 63 | name?: string; 64 | [pattern: string]: { 65 | name?: string; 66 | propertyName1: { 67 | [pattern: string]: { 68 | propertyName2?: boolean; 69 | }; 70 | }; 71 | }; 72 | }`); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/__tests__/primitiveTypes/index.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, rmdirSync } from 'fs'; 2 | 3 | import { convertFromDirectory } from '../../index'; 4 | 5 | describe('Primitive Types', () => { 6 | const typeOutputDirectory = './src/__tests__/primitiveTypes/interfaces'; 7 | beforeAll(async () => { 8 | if (existsSync(typeOutputDirectory)) { 9 | rmdirSync(typeOutputDirectory, { recursive: true }); 10 | } 11 | const result = await convertFromDirectory({ 12 | schemaDirectory: './src/__tests__/primitiveTypes/schemas', 13 | typeOutputDirectory 14 | }); 15 | 16 | expect(result).toBe(true); 17 | }); 18 | 19 | test('String base schema', async () => { 20 | const readmeContent = readFileSync(`${typeOutputDirectory}/Email.ts`).toString(); 21 | 22 | expect(readmeContent).toBe(`/** 23 | * This file was automatically generated by joi-to-typescript 24 | * Do not modify this file manually 25 | */ 26 | 27 | export interface CompanySchema { 28 | email?: Email; 29 | } 30 | 31 | export type Email = string; 32 | 33 | export interface UserSchema { 34 | email: Email; 35 | } 36 | `); 37 | }); 38 | 39 | test('number base schema', async () => { 40 | const readmeContent = readFileSync(`${typeOutputDirectory}/Counter.ts`).toString(); 41 | 42 | expect(readmeContent).toBe(`/** 43 | * This file was automatically generated by joi-to-typescript 44 | * Do not modify this file manually 45 | */ 46 | 47 | export interface CompanySchema { 48 | counter?: Counter; 49 | } 50 | 51 | export type Counter = number; 52 | 53 | export interface UserSchema { 54 | counter: Counter; 55 | } 56 | `); 57 | }); 58 | 59 | test('Date base schema', async () => { 60 | const readmeContent = readFileSync(`${typeOutputDirectory}/DateField.ts`).toString(); 61 | 62 | expect(readmeContent).toBe(`/** 63 | * This file was automatically generated by joi-to-typescript 64 | * Do not modify this file manually 65 | */ 66 | 67 | export interface CompanySchema { 68 | counter?: DateField; 69 | } 70 | 71 | export type DateField = Date; 72 | 73 | export interface UserSchema { 74 | counter: DateField; 75 | } 76 | `); 77 | }); 78 | 79 | test('boolean base schema', async () => { 80 | const readmeContent = readFileSync(`${typeOutputDirectory}/Boolean.ts`).toString(); 81 | 82 | expect(readmeContent).toBe(`/** 83 | * This file was automatically generated by joi-to-typescript 84 | * Do not modify this file manually 85 | */ 86 | 87 | export type Boolean = boolean; 88 | 89 | export interface CompanySchema { 90 | counter?: Boolean; 91 | } 92 | 93 | export interface UserSchema { 94 | counter: Boolean; 95 | } 96 | `); 97 | }); 98 | 99 | test('allow on base schema', async () => { 100 | const readmeContent = readFileSync(`${typeOutputDirectory}/Allow.ts`).toString(); 101 | 102 | expect(readmeContent).toBe(`/** 103 | * This file was automatically generated by joi-to-typescript 104 | * Do not modify this file manually 105 | */ 106 | 107 | export type Blank = string; 108 | 109 | export type BlankNull = string | null | ''; 110 | 111 | /** 112 | * This is date 113 | */ 114 | export type DateOptions = Date | null; 115 | 116 | /** 117 | * Test Schema Name 118 | */ 119 | export type Name = string; 120 | 121 | export type NormalList = 'red' | 'green' | 'blue'; 122 | 123 | export type NormalRequiredList = 'red' | 'green' | 'blue'; 124 | 125 | /** 126 | * nullable 127 | */ 128 | export type NullName = string | null; 129 | 130 | export type NullNumber = number | null; 131 | 132 | export type Numbers = 1 | 2 | 3 | 4 | 5; 133 | `); 134 | }); 135 | 136 | test('ensure primitive types are exported/imported correctly', async () => { 137 | const readmeContent = readFileSync(`${typeOutputDirectory}/Using.ts`).toString(); 138 | 139 | expect(readmeContent).toBe(`/** 140 | * This file was automatically generated by joi-to-typescript 141 | * Do not modify this file manually 142 | */ 143 | 144 | export interface UsingOtherTypesSchema { 145 | property?: string | null | ''; 146 | } 147 | `); 148 | }); 149 | 150 | test('union/alternative primitive types', async () => { 151 | const readmeContent = readFileSync(`${typeOutputDirectory}/Union.ts`).toString(); 152 | 153 | expect(readmeContent).toBe(`/** 154 | * This file was automatically generated by joi-to-typescript 155 | * Do not modify this file manually 156 | */ 157 | 158 | export interface CompanySchema { 159 | counter?: Union; 160 | } 161 | 162 | export type Union = string | number; 163 | 164 | export interface UserSchema { 165 | counter: Union; 166 | } 167 | `); 168 | }); 169 | 170 | test('object types', async () => { 171 | const readmeContent = readFileSync(`${typeOutputDirectory}/Object.ts`).toString(); 172 | 173 | expect(readmeContent).toBe(`/** 174 | * This file was automatically generated by joi-to-typescript 175 | * Do not modify this file manually 176 | */ 177 | 178 | export interface ObjectEmpty {} 179 | 180 | export interface ObjectEmptyAny {} 181 | 182 | export interface ObjectWithObjectEmpty { 183 | nothing1?: Record; 184 | } 185 | `); 186 | }); 187 | }); 188 | -------------------------------------------------------------------------------- /src/__tests__/primitiveTypes/schemas/AllowSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const Name = Joi.string().optional().description('Test Schema Name').allow('').meta({ className: 'Name' }); 4 | export const NullName = Joi.string().optional().description('nullable').allow(null); 5 | export const BlankNull = Joi.string().optional().allow(null, ''); 6 | export const Blank = Joi.string().allow(''); 7 | export const NormalList = Joi.string().allow('red', 'green', 'blue'); 8 | export const NormalRequiredList = Joi.string().allow('red', 'green', 'blue').required(); 9 | export const Numbers = Joi.number().optional().allow(1, 2, 3, 4, 5); 10 | export const NullNumber = Joi.number().optional().allow(null); 11 | export const DateOptions = Joi.date().allow(null).description('This is date'); 12 | -------------------------------------------------------------------------------- /src/__tests__/primitiveTypes/schemas/BooleanSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const BooleanSchema = Joi.boolean().meta({ className: 'Boolean' }); 4 | 5 | export const CompanySchema = Joi.object({ 6 | counter: BooleanSchema 7 | }); 8 | 9 | export const UserSchema = Joi.object({ 10 | counter: BooleanSchema.required() 11 | }); 12 | -------------------------------------------------------------------------------- /src/__tests__/primitiveTypes/schemas/CounterSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const CounterSchema = Joi.number().meta({ className: 'Counter' }); 4 | 5 | export const CompanySchema = Joi.object({ 6 | counter: CounterSchema 7 | }); 8 | 9 | export const UserSchema = Joi.object({ 10 | counter: CounterSchema.required() 11 | }); 12 | -------------------------------------------------------------------------------- /src/__tests__/primitiveTypes/schemas/DateFieldSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const DateFieldSchema = Joi.date().meta({ className: 'DateField' }); 4 | 5 | export const CompanySchema = Joi.object({ 6 | counter: DateFieldSchema 7 | }); 8 | 9 | export const UserSchema = Joi.object({ 10 | counter: DateFieldSchema.required() 11 | }); 12 | -------------------------------------------------------------------------------- /src/__tests__/primitiveTypes/schemas/EmailSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const EmailSchema = Joi.string() 4 | .email({ tlds: { allow: false } }) 5 | .meta({ className: 'Email' }); 6 | 7 | export const CompanySchema = Joi.object({ 8 | email: EmailSchema 9 | }); 10 | 11 | export const UserSchema = Joi.object({ 12 | email: EmailSchema.required() 13 | }); 14 | -------------------------------------------------------------------------------- /src/__tests__/primitiveTypes/schemas/ObjectSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const ObjectEmptyAnySchema = Joi.object().meta({ className: 'ObjectEmptyAny' }); 4 | 5 | export const ObjectEmptySchema = Joi.object({}).meta({ className: 'ObjectEmpty' }); 6 | 7 | export const ObjectWithObjectEmptySchema = Joi.object({ 8 | nothing1: Joi.object({}) 9 | }).meta({ className: 'ObjectWithObjectEmpty' }); 10 | -------------------------------------------------------------------------------- /src/__tests__/primitiveTypes/schemas/UnionSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const UnionSchema = Joi.alternatives([Joi.string(), Joi.number()]).meta({ className: 'Union' }); 4 | 5 | export const CompanySchema = Joi.object({ 6 | counter: UnionSchema 7 | }); 8 | 9 | export const UserSchema = Joi.object({ 10 | counter: UnionSchema.required() 11 | }); 12 | -------------------------------------------------------------------------------- /src/__tests__/primitiveTypes/schemas/UsingSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { BlankNull } from './AllowSchema'; 3 | 4 | export const UsingOtherTypesSchema = Joi.object({ 5 | property: BlankNull 6 | }); 7 | -------------------------------------------------------------------------------- /src/__tests__/readme/readme.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, rmdirSync } from 'fs'; 2 | 3 | import { convertFromDirectory } from '../../index'; 4 | 5 | describe('this is the example on the readme', () => { 6 | const typeOutputDirectory = './src/__tests__/readme/interfaces'; 7 | beforeEach(() => { 8 | if (existsSync(typeOutputDirectory)) { 9 | rmdirSync(typeOutputDirectory, { recursive: true }); 10 | } 11 | }); 12 | 13 | test('a good example schema', async () => { 14 | const result = await convertFromDirectory({ 15 | schemaDirectory: './src/__tests__/readme/schemas', 16 | typeOutputDirectory 17 | }); 18 | 19 | expect(result).toBe(true); 20 | 21 | const readmeContent = readFileSync(`${typeOutputDirectory}/Readme.ts`).toString(); 22 | 23 | expect(readmeContent).toBe(`/** 24 | * This file was automatically generated by joi-to-typescript 25 | * Do not modify this file manually 26 | */ 27 | 28 | export interface Job { 29 | businessName: string; 30 | jobTitle: string; 31 | } 32 | 33 | /** 34 | * A list of People 35 | */ 36 | export type People = Person[]; 37 | 38 | export interface Person { 39 | firstName: string; 40 | job?: Job; 41 | /** 42 | * Last Name 43 | */ 44 | lastName: string; 45 | wallet?: Wallet; 46 | } 47 | 48 | export interface Wallet { 49 | /** 50 | * Number Property 51 | */ 52 | [x: string]: number; 53 | eur: number; 54 | usd: number; 55 | } 56 | `); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/__tests__/readme/schemas/ReadmeSchema.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-use-before-define */ 2 | import Joi from 'joi'; 3 | 4 | // Input 5 | export const JobSchema = Joi.object({ 6 | businessName: Joi.string().required(), 7 | jobTitle: Joi.string().required() 8 | }).meta({ className: 'Job' }); 9 | 10 | export const WalletSchema = Joi.object({ 11 | usd: Joi.number().required(), 12 | eur: Joi.number().required() 13 | }) 14 | .unknown() 15 | .meta({ className: 'Wallet', unknownType: 'number' }); 16 | 17 | export const PersonSchema = Joi.object({ 18 | firstName: Joi.string().required(), 19 | lastName: Joi.string().required().description('Last Name'), 20 | job: JobSchema, 21 | wallet: WalletSchema 22 | }).meta({ className: 'Person' }); 23 | 24 | export const PeopleSchema = Joi.array() 25 | .items(PersonSchema) 26 | .required() 27 | .meta({ className: 'People' }) 28 | .description('A list of People'); 29 | -------------------------------------------------------------------------------- /src/__tests__/readonly.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | import { convertSchema } from '../index'; 4 | 5 | describe('readonly tests', () => { 6 | test('readonly', () => { 7 | const schema = Joi.object({ 8 | bit: Joi.boolean().meta({ readonly: true }), 9 | customObject: Joi.boolean().meta({ readonly: true, className: 'CustomObject' }) 10 | // readonly on a top level object is ignored 11 | }).meta({ className: 'TestSchema', readonly: true }); 12 | 13 | const result = convertSchema({ defaultToRequired: true }, schema); 14 | expect(result).not.toBeUndefined; 15 | expect(result?.content).toBe(`export interface TestSchema { 16 | readonly bit: boolean; 17 | readonly customObject: CustomObject; 18 | }`); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/__tests__/subDirectories/AssertionCriteria.ts: -------------------------------------------------------------------------------- 1 | export class AssertionCriteria { 2 | private static autoGenHeader = `/** 3 | * This file was automatically generated by joi-to-typescript 4 | * Do not modify this file manually 5 | */ 6 | 7 | `; 8 | 9 | private static oneContentGeneratedModel = ` 10 | 11 | export interface Item { 12 | /** 13 | * Female Zebra 14 | */ 15 | femaleZebra?: Zebra; 16 | /** 17 | * Male Zebra 18 | */ 19 | maleZebra?: Zebra; 20 | name: string; 21 | } 22 | 23 | /** 24 | * A list of People 25 | */ 26 | export type People = Person[]; 27 | 28 | /** 29 | * a test schema definition 30 | */ 31 | export interface Test { 32 | name?: string; 33 | /** 34 | * A list of People 35 | */ 36 | people?: People; 37 | propertyName1: boolean; 38 | } 39 | 40 | export interface Zebra { 41 | name?: string; 42 | } 43 | `; 44 | 45 | public static oneContentFlatOrIndexAll = 46 | AssertionCriteria.autoGenHeader + `import { Person } from '.';` + AssertionCriteria.oneContentGeneratedModel; 47 | 48 | public static oneContentTree = 49 | AssertionCriteria.autoGenHeader + `import { Person } from './subDir';` + AssertionCriteria.oneContentGeneratedModel; 50 | 51 | public static personContentModel = ` 52 | 53 | export interface Person { 54 | address: Address; 55 | firstName: string; 56 | lastName: string; 57 | } 58 | `; 59 | 60 | public static personContentTree = 61 | AssertionCriteria.autoGenHeader + `import { Address } from '.';` + AssertionCriteria.personContentModel; 62 | 63 | public static personRootIndexContent = 64 | AssertionCriteria.autoGenHeader + `import { Address } from '..';` + AssertionCriteria.personContentModel; 65 | 66 | public static defaultRootIndexContent = 67 | AssertionCriteria.autoGenHeader + 68 | `export * from './One'; 69 | `; 70 | 71 | public static subDirIndexContent = 72 | AssertionCriteria.autoGenHeader + 73 | `export * from './Address'; 74 | export * from './Person'; 75 | `; 76 | 77 | public static flattenedRootIndexContent = 78 | AssertionCriteria.autoGenHeader + 79 | `export * from './One'; 80 | export * from './Address'; 81 | export * from './Person'; 82 | export * from './Employee'; 83 | `; 84 | 85 | public static indexAllToRootIndexContent = 86 | AssertionCriteria.autoGenHeader + 87 | `export * from './One'; 88 | export * from './subDir/Address'; 89 | export * from './subDir/Person'; 90 | export * from './subDir2/Employee'; 91 | `; 92 | 93 | public static addressContent = 94 | AssertionCriteria.autoGenHeader + 95 | `export interface Address { 96 | Suburb: string; 97 | addressLineNumber1: string; 98 | } 99 | `; 100 | 101 | public static employeeContentModel = ` 102 | 103 | export interface Employee { 104 | personalDetails: Person; 105 | pet: Item; 106 | } 107 | `; 108 | 109 | public static employeeContentTree = 110 | AssertionCriteria.autoGenHeader + 111 | `import { Person } from '../subDir';\nimport { Item } from '..';` + 112 | AssertionCriteria.employeeContentModel; 113 | 114 | public static employeeContentFlattened = 115 | AssertionCriteria.autoGenHeader + `import { Person, Item } from '.';` + AssertionCriteria.employeeContentModel; 116 | 117 | public static employeeRootIndexContent = 118 | AssertionCriteria.autoGenHeader + `import { Person, Item } from '..';` + AssertionCriteria.employeeContentModel; 119 | 120 | public static subDir2IndexContent = 121 | AssertionCriteria.autoGenHeader + 122 | `export * from './Employee'; 123 | `; 124 | } 125 | -------------------------------------------------------------------------------- /src/__tests__/subDirectories/schemas/OneSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { PersonSchema } from './subDir/PersonSchema'; 3 | 4 | export const ZebraSchema = Joi.object({ 5 | name: Joi.string() 6 | }).meta({ className: 'Zebra' }); 7 | 8 | export const ItemSchema = Joi.object({ 9 | name: Joi.string().required(), 10 | maleZebra: ZebraSchema.description('Male Zebra'), 11 | femaleZebra: ZebraSchema.description('Female Zebra') 12 | }).meta({ className: 'Item' }); 13 | 14 | export const PeopleSchema = Joi.array() 15 | .items(PersonSchema) 16 | .required() 17 | .meta({ className: 'People' }) 18 | .description('A list of People'); 19 | 20 | export const TestSchema = Joi.object({ 21 | name: Joi.string().optional(), 22 | propertyName1: Joi.boolean().required(), 23 | people: PeopleSchema.optional() 24 | }) 25 | .meta({ className: 'Test' }) 26 | .description('a test schema definition'); 27 | -------------------------------------------------------------------------------- /src/__tests__/subDirectories/schemas/subDir/AddressSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const AddressSchema = Joi.object({ 4 | addressLineNumber1: Joi.string().required(), 5 | Suburb: Joi.string().required() 6 | }).meta({ className: 'Address' }); 7 | -------------------------------------------------------------------------------- /src/__tests__/subDirectories/schemas/subDir/PersonSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { AddressSchema } from './AddressSchema'; 3 | 4 | export const PersonSchema = Joi.object({ 5 | firstName: Joi.string().required(), 6 | lastName: Joi.string().required(), 7 | address: AddressSchema.required() 8 | }).meta({ className: 'Person' }); 9 | -------------------------------------------------------------------------------- /src/__tests__/subDirectories/schemas/subDir2/EmployeeSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { PersonSchema } from '../subDir/PersonSchema'; 3 | import { ItemSchema } from '../OneSchema'; 4 | 5 | export const EmployeeSchema = Joi.object({ 6 | personalDetails: PersonSchema.required(), 7 | pet: ItemSchema.required() 8 | }).meta({ className: 'Employee' }); 9 | -------------------------------------------------------------------------------- /src/__tests__/subDirectories/subDirectories.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, rmdirSync } from 'fs'; 2 | 3 | import { convertFromDirectory } from '../../index'; 4 | import { AssertionCriteria } from './AssertionCriteria'; 5 | 6 | describe('subDirectories', () => { 7 | const typeOutputDirectory = './src/__tests__/subDirectories/interfaces'; 8 | const schemaDirectory = './src/__tests__/subDirectories/schemas'; 9 | 10 | beforeEach(() => { 11 | if (existsSync(typeOutputDirectory)) { 12 | rmdirSync(typeOutputDirectory, { recursive: true }); 13 | } 14 | }); 15 | 16 | test('Sub-Directory - Defaults [Tree]', async () => { 17 | const result = await convertFromDirectory({ 18 | schemaDirectory, 19 | typeOutputDirectory 20 | }); 21 | 22 | expect(result).toBe(true); 23 | 24 | const rootIndexContent = readFileSync(`${typeOutputDirectory}/index.ts`).toString(); 25 | const oneContent = readFileSync(`${typeOutputDirectory}/One.ts`).toString(); 26 | const subDirIndexContent = readFileSync(`${typeOutputDirectory}/subDir/index.ts`).toString(); 27 | const personContent = readFileSync(`${typeOutputDirectory}/subDir/Person.ts`).toString(); 28 | const addressContent = readFileSync(`${typeOutputDirectory}/subDir/Address.ts`).toString(); 29 | const subDir2IndexContent = readFileSync(`${typeOutputDirectory}/subDir2/index.ts`).toString(); 30 | const employeeContent = readFileSync(`${typeOutputDirectory}/subDir2/Employee.ts`).toString(); 31 | 32 | expect(rootIndexContent).toBe(AssertionCriteria.defaultRootIndexContent); 33 | expect(oneContent).toBe(AssertionCriteria.oneContentTree); 34 | expect(subDirIndexContent).toBe(AssertionCriteria.subDirIndexContent); 35 | expect(personContent).toBe(AssertionCriteria.personContentTree); 36 | expect(addressContent).toBe(AssertionCriteria.addressContent); 37 | expect(subDir2IndexContent).toBe(AssertionCriteria.subDir2IndexContent); 38 | expect(employeeContent).toBe(AssertionCriteria.employeeContentTree); 39 | }); 40 | 41 | test('Sub-Directory - Flatten', async () => { 42 | if (existsSync(typeOutputDirectory)) { 43 | rmdirSync(typeOutputDirectory, { recursive: true }); 44 | } 45 | const result = await convertFromDirectory({ 46 | schemaDirectory, 47 | typeOutputDirectory, 48 | flattenTree: true 49 | }); 50 | 51 | expect(result).toBe(true); 52 | 53 | const rootIndexContent = readFileSync(`${typeOutputDirectory}/index.ts`).toString(); 54 | const oneContent = readFileSync(`${typeOutputDirectory}/One.ts`).toString(); 55 | const personContent = readFileSync(`${typeOutputDirectory}/Person.ts`).toString(); 56 | const addressContent = readFileSync(`${typeOutputDirectory}/Address.ts`).toString(); 57 | const employeeContent = readFileSync(`${typeOutputDirectory}/Employee.ts`).toString(); 58 | 59 | expect(rootIndexContent).toBe(AssertionCriteria.flattenedRootIndexContent); 60 | expect(oneContent).toBe(AssertionCriteria.oneContentFlatOrIndexAll); 61 | expect(personContent).toBe(AssertionCriteria.personContentTree); 62 | expect(addressContent).toBe(AssertionCriteria.addressContent); 63 | expect(employeeContent).toBe(AssertionCriteria.employeeContentFlattened); 64 | }); 65 | 66 | test('Sub-Directory - Tree Index All to Root', async () => { 67 | const result = await convertFromDirectory({ 68 | schemaDirectory, 69 | typeOutputDirectory, 70 | indexAllToRoot: true 71 | }); 72 | 73 | expect(result).toBe(true); 74 | 75 | const rootIndexContent = readFileSync(`${typeOutputDirectory}/index.ts`).toString(); 76 | const oneContent = readFileSync(`${typeOutputDirectory}/One.ts`).toString(); 77 | const personContent = readFileSync(`${typeOutputDirectory}/subDir/Person.ts`).toString(); 78 | const addressContent = readFileSync(`${typeOutputDirectory}/subDir/Address.ts`).toString(); 79 | const employeeContent = readFileSync(`${typeOutputDirectory}/subDir2/Employee.ts`).toString(); 80 | 81 | expect(rootIndexContent).toBe(AssertionCriteria.indexAllToRootIndexContent); 82 | expect(oneContent).toBe(AssertionCriteria.oneContentFlatOrIndexAll); 83 | expect(personContent).toBe(AssertionCriteria.personRootIndexContent); 84 | expect(addressContent).toBe(AssertionCriteria.addressContent); 85 | expect(employeeContent).toBe(AssertionCriteria.employeeRootIndexContent); 86 | }); 87 | 88 | test('Sub-Directory - Root Directory Only', async () => { 89 | const result = await convertFromDirectory({ 90 | schemaDirectory: schemaDirectory + '/subDir', // Need to choose a directory with schemas that don't contain outer/sub dependencies. 91 | typeOutputDirectory, 92 | rootDirectoryOnly: true 93 | }); 94 | 95 | expect(result).toBe(true); 96 | 97 | const rootIndexContent = readFileSync(`${typeOutputDirectory}/index.ts`).toString(); 98 | const addressContent = readFileSync(`${typeOutputDirectory}/Address.ts`).toString(); 99 | 100 | expect(rootIndexContent).toBe(AssertionCriteria.subDirIndexContent); 101 | expect(addressContent).toBe(AssertionCriteria.addressContent); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /src/__tests__/tuple/schemas/OneSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const ItemSchema = Joi.object({ 4 | name: Joi.string().required() 5 | }).meta({ className: 'Item' }); 6 | 7 | export const TestSchema = Joi.object({ 8 | name: Joi.string().optional(), 9 | propertyName1: Joi.bool().required(), 10 | items: Joi.array().ordered(Joi.number().required()).ordered(Joi.string().required()).ordered(ItemSchema).allow(null) 11 | }) 12 | .meta({ className: 'Test' }) 13 | .description('a test schema definition'); 14 | 15 | export const TestTupleSchema = Joi.array() 16 | .ordered(TestSchema.required()) 17 | .ordered(Joi.alternatives(Joi.number(), Joi.string())) 18 | .meta({ className: 'TestTuple' }) 19 | .description('A tuple of Test object'); 20 | -------------------------------------------------------------------------------- /src/__tests__/tuple/tuple.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, rmdirSync } from 'fs'; 2 | import Joi from 'joi'; 3 | 4 | import { convertFromDirectory, convertSchema } from '../../index'; 5 | 6 | describe('Tuple types', () => { 7 | const typeOutputDirectory = './src/__tests__/tuple/interfaces'; 8 | 9 | beforeAll(() => { 10 | if (existsSync(typeOutputDirectory)) { 11 | rmdirSync(typeOutputDirectory, { recursive: true }); 12 | } 13 | }); 14 | 15 | test('tuple variations from file', async () => { 16 | const result = await convertFromDirectory({ 17 | schemaDirectory: './src/__tests__/tuple/schemas', 18 | typeOutputDirectory 19 | }); 20 | 21 | expect(result).toBe(true); 22 | 23 | const oneContent = readFileSync(`${typeOutputDirectory}/One.ts`).toString(); 24 | 25 | expect(oneContent).toBe( 26 | `/** 27 | * This file was automatically generated by joi-to-typescript 28 | * Do not modify this file manually 29 | */ 30 | 31 | export interface Item { 32 | name: string; 33 | } 34 | 35 | /** 36 | * a test schema definition 37 | */ 38 | export interface Test { 39 | items?: [number, string, Item?] | null; 40 | name?: string; 41 | propertyName1: boolean; 42 | } 43 | 44 | /** 45 | * A tuple of Test object 46 | */ 47 | export type TestTuple = [ 48 | /** 49 | * a test schema definition 50 | */ 51 | Test, 52 | (number | string)? 53 | ]; 54 | ` 55 | ); 56 | }); 57 | 58 | test("test to ensure can't use ordered and items both", () => { 59 | const schema = Joi.array() 60 | .ordered(Joi.string().description('one')) 61 | .items(Joi.number().description('two')) 62 | .required() 63 | .meta({ className: 'TestList' }) 64 | .description('A list of Test object'); 65 | 66 | const result = convertSchema({ sortPropertiesByName: true }, schema); 67 | expect(result).not.toBeUndefined; 68 | expect(result?.content).toBe(`/** 69 | * A list of Test object 70 | */ 71 | export type TestList = any[];`); 72 | }); 73 | 74 | test('tuple newline', () => { 75 | const schema = Joi.object({ 76 | name: Joi.string().optional(), 77 | propertyName1: Joi.bool().required(), 78 | items: Joi.array() 79 | .ordered(Joi.number().required()) 80 | .ordered(Joi.string().required()) 81 | .ordered( 82 | Joi.object({ 83 | another: Joi.string() 84 | }) 85 | ) 86 | .allow(null), 87 | simpleItems: Joi.array().ordered(Joi.number().required()).ordered(Joi.string().required()) 88 | }) 89 | .meta({ className: 'Test' }) 90 | .description('a test schema definition'); 91 | 92 | const result = convertSchema({ sortPropertiesByName: true, tupleNewLine: true }, schema); 93 | expect(result).not.toBeUndefined; 94 | expect(result?.content).toBe(`/** 95 | * a test schema definition 96 | */ 97 | export interface Test { 98 | items?: [ 99 | number, 100 | string, 101 | { 102 | another?: string; 103 | }? 104 | ] | null; 105 | name?: string; 106 | propertyName1: boolean; 107 | simpleItems?: [ 108 | number, 109 | string 110 | ]; 111 | }`); 112 | }); 113 | 114 | test('tuple and union newline', () => { 115 | const schema = Joi.object({ 116 | name: Joi.string().optional(), 117 | propertyName1: Joi.bool().required(), 118 | items: Joi.array() 119 | .ordered(Joi.number().required()) 120 | .ordered(Joi.string().required()) 121 | .ordered( 122 | Joi.object({ 123 | another: Joi.string() 124 | }) 125 | ) 126 | .allow(null) 127 | }) 128 | .meta({ className: 'Test' }) 129 | .description('a test schema definition'); 130 | 131 | const result = convertSchema({ sortPropertiesByName: true, tupleNewLine: true, unionNewLine: true }, schema); 132 | expect(result).not.toBeUndefined; 133 | expect(result?.content).toBe(`/** 134 | * a test schema definition 135 | */ 136 | export interface Test { 137 | items?: 138 | | [ 139 | number, 140 | string, 141 | { 142 | another?: string; 143 | }? 144 | ] 145 | | null; 146 | name?: string; 147 | propertyName1: boolean; 148 | }`); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /src/__tests__/unknown.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | import { convertSchema } from '../index'; 4 | 5 | describe('test `Joi.unknown()`', () => { 6 | test('`unknown(true)`', () => { 7 | const schema = Joi.object({ 8 | name: Joi.string() 9 | }) 10 | .meta({ className: 'TestSchema' }) 11 | .description('a test schema definition') 12 | .unknown(true); 13 | 14 | const result = convertSchema({ sortPropertiesByName: false }, schema); 15 | expect(result).not.toBeUndefined; 16 | expect(result?.content).toBe(`/** 17 | * a test schema definition 18 | */ 19 | export interface TestSchema { 20 | name?: string; 21 | /** 22 | * Unknown Property 23 | */ 24 | [x: string]: unknown; 25 | }`); 26 | }); 27 | 28 | test('`unknown(type)`', () => { 29 | const schema = Joi.object({ 30 | name: Joi.string() 31 | }) 32 | .meta({ className: 'TestSchema', unknownType: 'number' }) 33 | .description('a test schema definition') 34 | .unknown(true); 35 | 36 | const result = convertSchema({ sortPropertiesByName: false }, schema); 37 | expect(result).not.toBeUndefined; 38 | expect(result?.content).toBe(`/** 39 | * a test schema definition 40 | */ 41 | export interface TestSchema { 42 | name?: string; 43 | /** 44 | * Number Property 45 | */ 46 | [x: string]: number; 47 | }`); 48 | }); 49 | 50 | test('`unknown(false)`', () => { 51 | const schema2 = Joi.object({ 52 | name: Joi.string() 53 | }) 54 | .meta({ className: 'TestSchema' }) 55 | .description('a test schema definition') 56 | .unknown(false); 57 | 58 | const result2 = convertSchema({}, schema2); 59 | expect(result2).not.toBeUndefined; 60 | expect(result2?.content).toBe(`/** 61 | * a test schema definition 62 | */ 63 | export interface TestSchema { 64 | name?: string; 65 | }`); 66 | }); 67 | 68 | test('`unknown(true).meta({ unknownType: Joi.AnySchema() })`', () => { 69 | const schema = Joi.object({}) 70 | .unknown(true) 71 | .meta({ 72 | className: 'TestSchema', 73 | unknownType: Joi.array().items( 74 | Joi.object({ 75 | id: Joi.string().required() 76 | }) 77 | ) 78 | }) 79 | .description('a test schema definition'); 80 | 81 | const result = convertSchema({}, schema); 82 | expect(result).not.toBeUndefined; 83 | expect(result?.content).toBe(`/** 84 | * a test schema definition 85 | */ 86 | export interface TestSchema { 87 | [x: string]: { 88 | id: string; 89 | }[]; 90 | }`); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /src/__tests__/utils.ts: -------------------------------------------------------------------------------- 1 | import { filterMap, isDescribe } from '../utils'; 2 | 3 | describe('test the utils', () => { 4 | test('ensure undefined is removed', () => { 5 | const object = { 6 | a: 'blue', 7 | d: undefined 8 | }; 9 | 10 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 11 | const children = filterMap(Object.entries(object || {}), ([key, value]) => { 12 | return value; 13 | }); 14 | 15 | expect(children).toMatchObject(['blue']); 16 | }); 17 | 18 | test('flatten current object', () => { 19 | const object = { 20 | a: 'blue', 21 | b: { c: 'red', d: 'orange' }, 22 | e: 'purple' 23 | }; 24 | 25 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 26 | const children = filterMap(Object.entries(object || {}), ([key, value]) => { 27 | return value; 28 | }); 29 | 30 | expect(children).toMatchObject(['blue', { c: 'red', d: 'orange' }, 'purple']); 31 | }); 32 | 33 | test('isDescribe undefined', () => { 34 | expect(isDescribe(undefined)).toBe(false); 35 | }); 36 | 37 | test('isDescribe valid with type', () => { 38 | expect(isDescribe({ type: 'boo' })).toBe(true); 39 | }); 40 | 41 | test('isDescribe invalid', () => { 42 | expect(isDescribe({})).toBe(false); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/analyseSchemaFile.ts: -------------------------------------------------------------------------------- 1 | import Joi, { AnySchema } from 'joi'; 2 | import Path from 'path'; 3 | 4 | import { Settings, ConvertedType, GenerateTypeFile } from './types'; 5 | import { getTypeFileNameFromSchema } from './write'; 6 | import { getAllCustomTypes, parseSchema, typeContentToTs } from './parse'; 7 | import { Describe } from './joiDescribeTypes'; 8 | import { ensureInterfaceorTypeName, getInterfaceOrTypeName } from './joiUtils'; 9 | 10 | export function convertSchemaInternal( 11 | settings: Settings, 12 | joi: AnySchema, 13 | exportedName?: string, 14 | rootSchema?: boolean 15 | ): ConvertedType | undefined { 16 | const details = joi.describe() as Describe; 17 | 18 | const interfaceOrTypeName = getInterfaceOrTypeName(settings, details) || exportedName; 19 | 20 | if (!interfaceOrTypeName) { 21 | if (settings.useLabelAsInterfaceName) { 22 | throw new Error(`At least one "object" does not have .label(''). Details: ${JSON.stringify(details)}`); 23 | } else { 24 | throw new Error(`At least one "object" does not have .meta({className:''}). Details: ${JSON.stringify(details)}`); 25 | } 26 | } 27 | 28 | if (settings.debug && interfaceOrTypeName.toLowerCase().endsWith('schema')) { 29 | if (settings.useLabelAsInterfaceName) { 30 | // eslint-disable-next-line no-console 31 | console.debug( 32 | `It is recommended you update the Joi Schema '${interfaceOrTypeName}' similar to: ${interfaceOrTypeName} = Joi.object().label('${interfaceOrTypeName.replace( 33 | 'Schema', 34 | '' 35 | )}')` 36 | ); 37 | } else { 38 | // eslint-disable-next-line no-console 39 | console.debug( 40 | `It is recommended you update the Joi Schema '${interfaceOrTypeName}' similar to: ${interfaceOrTypeName} = Joi.object().meta({className:'${interfaceOrTypeName.replace( 41 | 'Schema', 42 | '' 43 | )}'})` 44 | ); 45 | } 46 | } 47 | 48 | ensureInterfaceorTypeName(settings, details, interfaceOrTypeName); 49 | 50 | const parsedSchema = parseSchema(details, settings, false, undefined, rootSchema); 51 | if (parsedSchema) { 52 | const customTypes = getAllCustomTypes(parsedSchema); 53 | const content = typeContentToTs(settings, parsedSchema, true); 54 | return { 55 | schema: joi, 56 | interfaceOrTypeName, 57 | customTypes, 58 | content 59 | }; 60 | } 61 | 62 | // The only type that could return this is alternatives 63 | // see parseAlternatives for why this is ignored 64 | /* istanbul ignore next */ 65 | return undefined; 66 | } 67 | /** 68 | * Analyse a schema file 69 | * 70 | * @param settings - Settings 71 | * @param schemaFileName - Schema File Name 72 | * @returns Schema analysis results 73 | */ 74 | export async function analyseSchemaFile( 75 | settings: Settings, 76 | schemaFileName: string 77 | ): Promise { 78 | const allConvertedTypes: ConvertedType[] = []; 79 | 80 | const fullFilePath = Path.resolve(Path.join(settings.schemaDirectory, schemaFileName)); 81 | const schemaFile = await import(fullFilePath); 82 | 83 | // Create Type File Name 84 | const typeFileName = getTypeFileNameFromSchema(schemaFileName, settings); 85 | const fullOutputFilePath = Path.join(settings.typeOutputDirectory, typeFileName); 86 | 87 | for (const exportedName in schemaFile) { 88 | const joiSchema = schemaFile[exportedName]; 89 | 90 | if (!Joi.isSchema(joiSchema)) { 91 | continue; 92 | } 93 | const convertedType = convertSchemaInternal(settings, joiSchema, exportedName, true); 94 | if (convertedType) { 95 | allConvertedTypes.push({ ...convertedType, location: fullOutputFilePath }); 96 | } 97 | } 98 | 99 | if (allConvertedTypes.length === 0) { 100 | if (settings.debug) { 101 | // eslint-disable-next-line no-console 102 | console.debug(`${schemaFileName} - Skipped - no Joi Schemas found`); 103 | } 104 | return; 105 | } 106 | 107 | if (settings.debug) { 108 | // eslint-disable-next-line no-console 109 | console.debug(`${schemaFileName} - Processing`); 110 | } 111 | 112 | // Clean up type list 113 | // Sort Types 114 | const typesToBeWritten = allConvertedTypes.sort( 115 | (interface1, interface2) => 0 - (interface1.interfaceOrTypeName > interface2.interfaceOrTypeName ? -1 : 1) 116 | ); 117 | 118 | // Write types 119 | const typeContent = typesToBeWritten.map(typeToBeWritten => { 120 | const content = typeToBeWritten.content; 121 | return [ 122 | ...(settings.tsContentHeader ? [settings.tsContentHeader(typeToBeWritten)] : []), 123 | content, 124 | ...(settings.tsContentFooter ? [settings.tsContentFooter(typeToBeWritten)] : []) 125 | ].join('\n'); 126 | }); 127 | 128 | // Get imports for the current file 129 | const allExternalTypes: ConvertedType[] = []; 130 | const allCurrentFileTypeNames = typesToBeWritten.map(typeToBeWritten => typeToBeWritten.interfaceOrTypeName); 131 | 132 | for (const typeToBeWritten of typesToBeWritten) { 133 | for (const customType of typeToBeWritten.customTypes) { 134 | if (!allCurrentFileTypeNames.includes(customType) && !allExternalTypes.includes(typeToBeWritten)) { 135 | allExternalTypes.push(typeToBeWritten); 136 | } 137 | } 138 | } 139 | 140 | const fileContent = `${typeContent.join('\n\n').concat('\n')}`; 141 | 142 | return { 143 | externalTypes: allExternalTypes, 144 | internalTypes: typesToBeWritten, 145 | fileContent, 146 | typeFileName, 147 | typeFileLocation: settings.typeOutputDirectory 148 | }; 149 | } 150 | -------------------------------------------------------------------------------- /src/convertFilesInDirectory.ts: -------------------------------------------------------------------------------- 1 | import Path from 'path'; 2 | import { existsSync, lstatSync, mkdirSync, readdirSync } from 'fs'; 3 | import { Settings, GenerateTypeFile, GenerateTypesDir } from './types'; 4 | import { writeIndexFile, getTypeFileNameFromSchema } from './write'; 5 | import { analyseSchemaFile } from './analyseSchemaFile'; 6 | 7 | /** 8 | * Create types from schemas from a directory 9 | * @param settings Settings 10 | */ 11 | export async function convertFilesInDirectory( 12 | appSettings: Settings, 13 | ogTypeOutputDir: string, 14 | fileTypesToExport: GenerateTypeFile[] = [] 15 | ): Promise { 16 | // Check and resolve directories 17 | const resolvedSchemaDirectory = Path.resolve(appSettings.schemaDirectory); 18 | if (!existsSync(resolvedSchemaDirectory)) { 19 | throw new Error(`schemaDirectory "${resolvedSchemaDirectory}" does not exist`); 20 | } 21 | const resolvedTypeOutputDirectory = Path.resolve(appSettings.typeOutputDirectory); 22 | if (!existsSync(resolvedTypeOutputDirectory)) { 23 | mkdirSync(resolvedTypeOutputDirectory, { recursive: true }); 24 | } 25 | 26 | let fileNamesToExport: string[] = []; 27 | const currentDirFileTypesToExport: GenerateTypeFile[] = fileTypesToExport; 28 | 29 | // Load files and get all types 30 | const files = readdirSync(resolvedSchemaDirectory); 31 | for (const schemaFileName of files) { 32 | const subDirectoryPath = Path.join(resolvedSchemaDirectory, schemaFileName); 33 | if (!appSettings.rootDirectoryOnly && lstatSync(subDirectoryPath).isDirectory()) { 34 | if (appSettings.ignoreFiles.includes(`${schemaFileName}/`)) { 35 | if (appSettings.debug) { 36 | // eslint-disable-next-line no-console 37 | console.debug(`Skipping ${subDirectoryPath} because it's in your ignore files list`); 38 | } 39 | continue; 40 | } 41 | const typeOutputDirectory = appSettings.flattenTree 42 | ? resolvedTypeOutputDirectory 43 | : Path.join(resolvedTypeOutputDirectory, schemaFileName); 44 | 45 | const thisDirsFileNamesToExport = await convertFilesInDirectory( 46 | { 47 | ...appSettings, 48 | schemaDirectory: subDirectoryPath, 49 | typeOutputDirectory 50 | }, 51 | ogTypeOutputDir, 52 | currentDirFileTypesToExport 53 | ); 54 | 55 | if (appSettings.indexAllToRoot || appSettings.flattenTree) { 56 | fileNamesToExport = fileNamesToExport.concat(thisDirsFileNamesToExport.typeFileNames); 57 | } 58 | } else { 59 | if (!appSettings.inputFileFilter.test(schemaFileName)) { 60 | if (appSettings.debug) { 61 | // eslint-disable-next-line no-console 62 | console.debug(`Skipping ${schemaFileName} because it's excluded via inputFileFilter`); 63 | } 64 | continue; 65 | } 66 | if (appSettings.ignoreFiles.includes(schemaFileName)) { 67 | if (appSettings.debug) { 68 | // eslint-disable-next-line no-console 69 | console.debug(`Skipping ${schemaFileName} because it's in your ignore files list`); 70 | } 71 | continue; 72 | } 73 | const exportType = await analyseSchemaFile(appSettings, schemaFileName); 74 | if (exportType) { 75 | let dirTypeFileName = exportType.typeFileName; 76 | if (appSettings.indexAllToRoot) { 77 | const findIndexEnd = resolvedTypeOutputDirectory.indexOf(ogTypeOutputDir) + ogTypeOutputDir.length + 1; 78 | dirTypeFileName = Path.join( 79 | resolvedTypeOutputDirectory.substring(findIndexEnd), 80 | getTypeFileNameFromSchema(schemaFileName, appSettings) 81 | ); 82 | } 83 | fileNamesToExport.push(dirTypeFileName); 84 | currentDirFileTypesToExport.push(exportType); 85 | } 86 | } 87 | } 88 | 89 | if (!appSettings.omitIndexFiles && (!appSettings.indexAllToRoot && !appSettings.flattenTree)) { 90 | // Write index.ts 91 | writeIndexFile(appSettings, fileNamesToExport); 92 | } 93 | 94 | return { typeFileNames: fileNamesToExport, types: currentDirFileTypesToExport }; 95 | } 96 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { AnySchema } from 'joi'; 2 | import Path from 'path'; 3 | 4 | import { Settings, ConvertedType, InputFileFilter } from './types'; 5 | import { convertFilesInDirectory } from './convertFilesInDirectory'; 6 | import { writeInterfaceFile } from './writeInterfaceFile'; 7 | import { convertSchemaInternal } from './analyseSchemaFile'; 8 | import { writeIndexFile } from './write'; 9 | 10 | export { Settings }; 11 | 12 | /** 13 | * Apply defaults to the Partial Settings parameter 14 | * 15 | * @param settings Partial Setting object 16 | * @returns Complete Settings object 17 | */ 18 | function defaultSettings(settings: Partial): Settings { 19 | const appSettings = Object.assign( 20 | { 21 | useLabelAsInterfaceName: false, 22 | defaultToRequired: false, 23 | schemaFileSuffix: 'Schema', 24 | interfaceFileSuffix: '', 25 | debug: false, 26 | fileHeader: `/** 27 | * This file was automatically generated by joi-to-typescript 28 | * Do not modify this file manually 29 | */`, 30 | sortPropertiesByName: true, 31 | commentEverything: false, 32 | ignoreFiles: [], 33 | indentationChacters: ' ', 34 | honorCastTo: [], 35 | treatDefaultedOptionalAsRequired: false, 36 | supplyDefaultsInType: false, 37 | inputFileFilter: InputFileFilter.Default, 38 | omitIndexFiles: false, 39 | }, 40 | settings 41 | ) as Settings; 42 | 43 | return appSettings; 44 | } 45 | 46 | export function convertSchema( 47 | settings: Partial, 48 | joi: AnySchema, 49 | exportedName?: string, 50 | root?: boolean 51 | ): ConvertedType | undefined { 52 | const appSettings = defaultSettings(settings); 53 | return convertSchemaInternal(appSettings, joi, exportedName, root); 54 | } 55 | 56 | /** 57 | * Create types from schemas from a directory 58 | * 59 | * @param settings - Configuration settings 60 | * @returns The success or failure of this operation 61 | */ 62 | export async function convertFromDirectory(settings: Partial): Promise { 63 | const appSettings = defaultSettings(settings); 64 | const filesInDirectory = await convertFilesInDirectory(appSettings, Path.resolve(appSettings.typeOutputDirectory)); 65 | 66 | if (!filesInDirectory.types || filesInDirectory.types.length === 0) { 67 | throw new Error('No schemas found, cannot generate interfaces'); 68 | } 69 | 70 | // TODO: remove fields from derived interfaces here 71 | 72 | for (const exportType of filesInDirectory.types) { 73 | writeInterfaceFile(appSettings, exportType.typeFileName, filesInDirectory.types); 74 | } 75 | 76 | if (appSettings.indexAllToRoot || appSettings.flattenTree) { 77 | // Write index.ts 78 | writeIndexFile(appSettings, filesInDirectory.typeFileNames); 79 | } 80 | 81 | return true; 82 | } 83 | -------------------------------------------------------------------------------- /src/joiDescribeTypes.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | /** 4 | * This file extends the TypeScript Types that are packaged as part of joi 5 | */ 6 | 7 | /** 8 | * Add extra typings that Joi Types does not include 9 | */ 10 | export interface BaseDescribe extends Joi.Description { 11 | flags?: { 12 | /** 13 | * https://joi.dev/api/#anyidid 14 | */ 15 | id?: string; 16 | /** 17 | * https://joi.dev/api/#anylabelname 18 | */ 19 | label?: string; 20 | /** 21 | * https://joi.dev/api/#anydescriptiondesc 22 | */ 23 | description?: string; 24 | /** 25 | * https://joi.dev/api/#anypresencemode 26 | */ 27 | presence?: 'optional' | 'required' | 'forbidden'; 28 | /** 29 | * Default object value 30 | */ 31 | default?: unknown; 32 | /** 33 | * Allow undefined values in array 34 | */ 35 | sparse?: boolean; 36 | /** 37 | * https://joi.dev/api/#objectunknownallow 38 | */ 39 | unknown?: boolean; 40 | /** 41 | * https://joi.dev/api/#anycastto 42 | */ 43 | cast?: 'string' | 'number' | 'map' | 'set'; 44 | /** 45 | * https://joi.dev/api/#anyonly 46 | */ 47 | only?: boolean; 48 | }; 49 | /** 50 | * https://joi.dev/api/#objectpatternpattern-schema-options 51 | */ 52 | patterns?: { 53 | schema?: Describe; 54 | regex?: string; 55 | rule: Describe; 56 | }[]; 57 | metas?: Meta[]; 58 | /** 59 | * The fist item in this array could be this instead of a value { override?: boolean} or contain { ref : {}}; 60 | */ 61 | allow?: unknown[]; 62 | } 63 | 64 | /** 65 | * Meta is a custom object provided by Joi 66 | * The values here are how this libarary uses it they are not standard Joi 67 | */ 68 | export interface Meta { 69 | className?: string; 70 | unknownType?: string; 71 | readonly?: boolean; 72 | } 73 | 74 | export interface ArrayDescribe extends BaseDescribe { 75 | type: 'array'; 76 | items?: Describe[]; 77 | ordered?: Describe[]; 78 | } 79 | 80 | export interface ObjectDescribe extends BaseDescribe { 81 | type: 'object'; 82 | keys: Record<'string', Describe>; 83 | } 84 | 85 | // We only properly support alternatives where matches have a schema 86 | // This interface represents that 87 | // When matches do not have a schema, like in conditionals, we return 'any' in code 88 | export interface AlternativesDescribe extends BaseDescribe { 89 | // Joi.alt and Joi.alternatives both output as 'alternatives' 90 | type: 'alternatives'; 91 | matches: { schema: Describe }[]; 92 | } 93 | 94 | export interface StringDescribe extends BaseDescribe { 95 | type: 'string'; 96 | } 97 | 98 | export interface BasicDescribe extends BaseDescribe { 99 | // Joi.bool an Joi.boolean both output as 'boolean' 100 | type: 'any' | 'boolean' | 'date' | 'number'; 101 | } 102 | 103 | export type Describe = ArrayDescribe | BasicDescribe | ObjectDescribe | AlternativesDescribe | StringDescribe; 104 | -------------------------------------------------------------------------------- /src/joiUtils.ts: -------------------------------------------------------------------------------- 1 | import { Settings } from '.'; 2 | import { Describe } from './joiDescribeTypes'; 3 | 4 | /** 5 | * Fetch the metadata values for a given field. Note that it is possible to have 6 | * more than one metadata record for a given field hence it is possible to get 7 | * back a list of values. 8 | * 9 | * @param field - the name of the metadata field to fetch 10 | * @param details - the schema details 11 | * @returns the values for the given field 12 | */ 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | export function getMetadataFromDetails(field: string, details: Describe): any[] { 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | const metas: any[] = details?.metas ?? []; 17 | return metas.filter(entry => entry[field]).map(entry => entry[field]); 18 | } 19 | 20 | export function getIsReadonly(details: Describe): boolean | undefined { 21 | const isReadonlyItems = getMetadataFromDetails('readonly', details); 22 | if (isReadonlyItems.length !== 0) { 23 | // If Joi.concat() or Joi.keys() has been used then there may be multiple 24 | // get the last one as this is the current value 25 | const isReadonly = isReadonlyItems.pop(); 26 | return Boolean(isReadonly); 27 | } 28 | 29 | return undefined; 30 | } 31 | 32 | export function getDisableDescription(details: Describe): boolean | undefined { 33 | const disableDescriptionItems = getMetadataFromDetails('disableDescription', details); 34 | if (disableDescriptionItems.length !== 0) { 35 | const disableDescription = disableDescriptionItems.pop(); 36 | return Boolean(disableDescription); 37 | } 38 | 39 | return undefined; 40 | } 41 | 42 | /** 43 | * Get the interface name from the Joi 44 | * @returns a string if it can find one 45 | */ 46 | export function getInterfaceOrTypeName(settings: Settings, details: Describe): string | undefined { 47 | if (details.flags?.presence === 'forbidden') { 48 | return 'undefined'; 49 | } 50 | if (settings.useLabelAsInterfaceName) { 51 | return details?.flags?.label?.replace(/\s/g, ''); 52 | } else { 53 | if (details?.metas && details.metas.length > 0) { 54 | const classNames: string[] = getMetadataFromDetails('className', details); 55 | if (classNames.length !== 0) { 56 | // If Joi.concat() or Joi.keys() has been used then there may be multiple 57 | // get the last one as this is the current className 58 | const className = classNames.pop(); 59 | return className?.replace(/\s/g, ''); 60 | } 61 | } 62 | return undefined; 63 | } 64 | } 65 | 66 | /** 67 | * Note: this is updating by reference 68 | */ 69 | export function ensureInterfaceorTypeName(settings: Settings, details: Describe, interfaceOrTypeName: string): void { 70 | if (settings.useLabelAsInterfaceName) { 71 | // Set the label from the exportedName if missing 72 | if (!details.flags) { 73 | details.flags = { label: interfaceOrTypeName }; 74 | } else if (!details.flags.label) { 75 | // Unable to build any test cases for this line but will keep it if joi.describe() changes 76 | /* istanbul ignore next */ 77 | details.flags.label = interfaceOrTypeName; 78 | } 79 | } else { 80 | if (!details.metas || details.metas.length === 0) { 81 | details.metas = []; 82 | } 83 | 84 | const className = details.metas.find(meta => meta.className)?.className; 85 | 86 | // Set the meta[].className from the exportedName if missing 87 | if (!className) { 88 | if (settings.defaultInterfaceSuffix && interfaceOrTypeName.toLowerCase().endsWith('schema')) { 89 | const nameWithNewSuffix = interfaceOrTypeName.slice(0, -6) + settings.defaultInterfaceSuffix; 90 | details.metas.push({ className: nameWithNewSuffix }); 91 | } else { 92 | details.metas.push({ className: interfaceOrTypeName }); 93 | } 94 | } 95 | } 96 | } 97 | 98 | /** 99 | * Ensure values is an array and remove any junk 100 | */ 101 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 102 | export function getAllowValues(allow: unknown[] | undefined): any[] { 103 | if (!allow || allow.length === 0) { 104 | return []; 105 | } 106 | 107 | // This may contain things like, so remove them 108 | // { override: true } 109 | // { ref: {...}} 110 | // If a user wants a complex custom type they need to use an interface 111 | const allowValues = allow.filter(item => item === null || !(typeof item === 'object')); 112 | 113 | return allowValues; 114 | } 115 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Describe } from 'joiDescribeTypes'; 2 | 3 | /** 4 | * Applies the mapper over each element in the list. 5 | * If the mapper returns undefined it will not show up in the result 6 | * 7 | * @param list - array to filter + map 8 | * @param mapper - mapper func to apply to map 9 | */ 10 | export function filterMap(list: T[], mapper: (t: T) => K | undefined): K[] { 11 | return list.reduce((res: K[], val: T) => { 12 | const mappedVal = mapper(val); 13 | if (mappedVal !== undefined) { 14 | res.push(mappedVal); 15 | } 16 | return res; 17 | }, []); 18 | } 19 | 20 | /** 21 | * Escape value so that it can be go into single quoted string literal. 22 | * @param value 23 | */ 24 | export function toStringLiteral(value: string, doublequoteEscape: boolean): string { 25 | const escapeChar = doublequoteEscape ? '"' : "'"; 26 | 27 | value = value.replace(/\\/g, '\\\\'); 28 | if (doublequoteEscape) { 29 | value = value.replace(/"/g, '\\"'); 30 | } else { 31 | value = value.replace(/'/g, "\\'"); 32 | } 33 | 34 | return `${escapeChar}${value}${escapeChar}`; 35 | } 36 | 37 | export function isDescribe(x: unknown): x is Describe { 38 | if (!x) { 39 | return false; 40 | } 41 | 42 | if ((x as Describe).type) { 43 | return true; 44 | } 45 | 46 | return false; 47 | } 48 | -------------------------------------------------------------------------------- /src/write.ts: -------------------------------------------------------------------------------- 1 | // Functions for converting properties to strings and to file system 2 | // TODO: Move all code here 3 | 4 | import { writeFileSync } from 'fs'; 5 | import Path from 'path'; 6 | 7 | import { JsDoc, Settings } from './types'; 8 | 9 | /** 10 | * Write index.ts file 11 | * 12 | * @param settings - Settings Object 13 | * @param fileNamesToExport - List of file names that will be added to the index.ts file 14 | */ 15 | export function writeIndexFile(settings: Settings, fileNamesToExport: string[]): void { 16 | if (fileNamesToExport.length === 0) { 17 | // Don't write an index file if its going to export nothing 18 | return; 19 | } 20 | const exportLines = fileNamesToExport.map(fileName => `export * from './${fileName.replace(/\\/g, '/')}';`); 21 | const fileContent = `${settings.fileHeader}\n\n${exportLines.join('\n').concat('\n')}`; 22 | writeFileSync(Path.join(settings.typeOutputDirectory, 'index.ts'), fileContent); 23 | } 24 | 25 | export function getTypeFileNameFromSchema(schemaFileName: string, settings: Settings): string { 26 | return ( 27 | (schemaFileName.endsWith(`${settings.schemaFileSuffix}.ts`) 28 | ? schemaFileName.substring(0, schemaFileName.length - `${settings.schemaFileSuffix}.ts`.length) 29 | : schemaFileName.replace('.ts', '')) + settings.interfaceFileSuffix 30 | ); 31 | } 32 | 33 | /** 34 | * Get all indent characters for this indent level 35 | * @param settings includes what the indent characters are 36 | * @param indentLevel how many indent levels 37 | */ 38 | export function getIndentStr(settings: Settings, indentLevel: number): string { 39 | return settings.indentationChacters.repeat(indentLevel); 40 | } 41 | 42 | /** 43 | * Get Interface jsDoc 44 | */ 45 | export function getJsDocString(settings: Settings, name: string, jsDoc?: JsDoc, indentLevel = 0): string { 46 | if (jsDoc?.disable === true) { 47 | return ''; 48 | } 49 | 50 | if (!settings.commentEverything && !jsDoc?.description && !jsDoc?.default && (jsDoc?.examples?.length ?? 0) === 0) { 51 | return ''; 52 | } 53 | 54 | const lines = []; 55 | 56 | if (settings.commentEverything || (jsDoc && jsDoc.description)) { 57 | let description = name; 58 | if (jsDoc?.description) { 59 | description = getStringIndentation(jsDoc.description).deIndentedString; 60 | } 61 | if (description) { 62 | lines.push(...description.split('\n').map(line => ` * ${line}`.trimEnd())); 63 | } 64 | } 65 | 66 | // Add a JsDoc divider if needed 67 | if (((jsDoc?.examples?.length ?? 0) > 0 || jsDoc?.default) && lines.length > 0) { 68 | lines.push(' *'); 69 | } 70 | 71 | if (jsDoc?.default) { 72 | const deIndented = getStringIndentation(jsDoc.default).deIndentedString; 73 | 74 | if (deIndented.includes('\n')) { 75 | lines.push(` * @default`); 76 | lines.push(...deIndented.split('\n').map(line => ` * ${line}`.trimEnd())); 77 | } else { 78 | lines.push(` * @default ${deIndented}`); 79 | } 80 | } 81 | 82 | for (const example of jsDoc?.examples ?? []) { 83 | const deIndented = getStringIndentation(example).deIndentedString; 84 | 85 | if (deIndented.includes('\n')) { 86 | lines.push(` * @example`); 87 | lines.push(...deIndented.split('\n').map(line => ` * ${line}`.trimEnd())); 88 | } else { 89 | lines.push(` * @example ${deIndented}`); 90 | } 91 | } 92 | 93 | if (lines.length === 0) { 94 | return ''; 95 | } 96 | 97 | // Add JsDoc boundaries 98 | lines.unshift('/**'); 99 | lines.push(' */'); 100 | 101 | return lines.map(line => `${getIndentStr(settings, indentLevel)}${line}`).join('\n') + '\n'; 102 | } 103 | 104 | interface GetStringIndentationResult { 105 | deIndentedString: string; 106 | indent: string; 107 | } 108 | 109 | /** 110 | * Given an indented string, uses the first line's indentation as base to de-indent 111 | * the rest of the string, and returns both the de-indented string and the 112 | * indentation found as prefix. 113 | */ 114 | function getStringIndentation(value: string): GetStringIndentationResult { 115 | const lines = value.split('\n'); 116 | let indent = ''; 117 | for (const line of lines) { 118 | // Skip initial newlines 119 | if (line.trim() === '') { 120 | continue; 121 | } 122 | const match = /^(\s+)\b/.exec(line); 123 | if (match) { 124 | indent = match[1]; 125 | } 126 | break; 127 | } 128 | 129 | const deIndentedString = lines 130 | .map(line => (line.startsWith(indent) ? line.substring(indent.length) : line)) 131 | .join('\n') 132 | .trim(); 133 | 134 | return { 135 | deIndentedString, 136 | indent 137 | }; 138 | } 139 | -------------------------------------------------------------------------------- /src/writeInterfaceFile.ts: -------------------------------------------------------------------------------- 1 | import Path from 'path'; 2 | import { writeFileSync } from 'fs'; 3 | 4 | import { Settings, GenerateTypeFile } from './types'; 5 | 6 | /** 7 | * Write interface file 8 | * 9 | * @returns The written file name 10 | */ 11 | export async function writeInterfaceFile( 12 | settings: Settings, 13 | typeFileName: string, 14 | generatedTypes: GenerateTypeFile[] 15 | ): Promise { 16 | const generatedFile = generatedTypes.find(x => x.typeFileName === typeFileName); 17 | if (generatedFile && generatedFile.fileContent && generatedFile.typeFileName) { 18 | let typeImports = ''; 19 | if (settings.flattenTree) { 20 | const externalTypeNames = generatedFile.externalTypes.map(typeToBeWritten => typeToBeWritten.customTypes).flat(); 21 | typeImports = externalTypeNames.length === 0 ? '' : `import { ${externalTypeNames.join(', ')} } from '.';\n\n`; 22 | } else { 23 | const customTypeLocationDict: { [id: string]: string[] } = {}; 24 | for (const externalCustomType of generatedFile.externalTypes 25 | .map(x => x.customTypes) 26 | .flat() 27 | .filter((value, index, self) => { 28 | // Remove Duplicates 29 | return self.indexOf(value) === index; 30 | })) { 31 | if (settings.indexAllToRoot) { 32 | if (!customTypeLocationDict[settings.typeOutputDirectory]) { 33 | customTypeLocationDict[settings.typeOutputDirectory] = []; 34 | } 35 | 36 | if (!customTypeLocationDict[settings.typeOutputDirectory].includes(externalCustomType)) { 37 | customTypeLocationDict[settings.typeOutputDirectory].push(externalCustomType); 38 | } 39 | } else { 40 | for (const generatedInternalType of generatedTypes 41 | .filter(f => f.typeFileName !== typeFileName) 42 | .map(x => x.internalTypes) 43 | .flat() 44 | .filter((value, index, self) => { 45 | return value.interfaceOrTypeName === externalCustomType && self.indexOf(value) === index; 46 | })) { 47 | if (generatedInternalType && generatedInternalType.location) { 48 | const generatedInternalTypeLocation = settings.omitIndexFiles 49 | ? // When we don't want to generate index files it means we need to directly refer 50 | // to each individually generated file 51 | generatedInternalType.location 52 | : // Otherwise it's ok to refer to the output directory's path 53 | Path.dirname(generatedInternalType.location); 54 | if (!customTypeLocationDict[generatedInternalTypeLocation]) { 55 | customTypeLocationDict[generatedInternalTypeLocation] = []; 56 | } 57 | 58 | if (!customTypeLocationDict[generatedInternalTypeLocation].includes(externalCustomType)) { 59 | customTypeLocationDict[generatedInternalTypeLocation].push(externalCustomType); 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | for (const customTypeLocation in customTypeLocationDict) { 67 | let relativePath = Path.relative(generatedFile.typeFileLocation, customTypeLocation); 68 | relativePath = relativePath ? `${relativePath}` : '.'; 69 | relativePath = relativePath.includes('..') || relativePath === '.' ? relativePath : `./${relativePath}`; 70 | typeImports += `import { ${customTypeLocationDict[customTypeLocation].join( 71 | ', ' 72 | )} } from '${relativePath.replace(/\\/g, '/')}';\n`; 73 | } 74 | 75 | if (typeImports) { 76 | typeImports += `\n`; 77 | } 78 | } 79 | 80 | const fileContent = `${settings.fileHeader}\n\n${typeImports}${generatedFile.fileContent}`; 81 | writeFileSync( 82 | `${Path.join( 83 | settings.flattenTree ? settings.typeOutputDirectory : generatedFile.typeFileLocation, 84 | typeFileName 85 | )}.ts`, 86 | fileContent 87 | ); 88 | return generatedFile.typeFileName; 89 | } 90 | 91 | // This function is intended to only be called by `convertFromDirectory` where the input 92 | // data is checked before calling this function 93 | /* istanbul ignore next */ 94 | return undefined; 95 | } 96 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "declarationMap": true, 8 | "noImplicitReturns": true, 9 | "noImplicitThis": true, 10 | "noUnusedLocals": true, 11 | "esModuleInterop": true, 12 | "strict": true, 13 | "baseUrl": "src", 14 | "moduleResolution": "node" 15 | }, 16 | "include": ["src"], 17 | "exclude": ["node_modules", "**/__tests__/*", "examples"] 18 | } 19 | --------------------------------------------------------------------------------