├── .commitlintrc.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.yml ├── .github └── workflows │ ├── codeql-analysis.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmrc ├── .prettierrc ├── .releaserc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── plop ├── js │ ├── index.hbs │ └── models │ │ ├── Model.hbs │ │ └── index.hbs ├── plopfile.js └── ts │ ├── index.hbs │ ├── models │ ├── Model.hbs │ └── index.hbs │ └── utils │ ├── find.hbs │ └── index.hbs ├── prisma ├── schema.prisma └── sequelize │ ├── config.json │ ├── index.ts │ └── models │ ├── Post.ts │ ├── User.ts │ └── index.ts ├── src ├── cli.ts ├── generator │ ├── helpers.ts │ ├── properties.ts │ └── transformDMMF.ts └── index.ts ├── tests ├── generator │ └── transformDMMF.test.ts └── index.test.ts └── tsconfig.json /.commitlintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - "@commitlint/config-conventional" 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/* 2 | dist/* 3 | plop/* 4 | prisma/* 5 | jest.config.js 6 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | jest: true 4 | root: true 5 | plugins: 6 | - jest 7 | - '@typescript-eslint' 8 | parserOptions: 9 | ecmaVersion: 9 10 | sourceType: module 11 | extends: 12 | - eslint:recommended 13 | - plugin:@typescript-eslint/eslint-recommended 14 | - plugin:@typescript-eslint/recommended 15 | - plugin:jest/recommended 16 | - plugin:jest/style 17 | - plugin:eslint-comments/recommended 18 | - prettier 19 | rules: 20 | semi: 'off' 21 | quotes: 'off' 22 | no-use-before-define: 'off' 23 | '@typescript-eslint/no-empty-interface': 'off' 24 | '@typescript-eslint/explicit-function-return-type': 'off' 25 | '@typescript-eslint/explicit-module-boundary-types': 'off' 26 | '@typescript-eslint/semi': error 27 | '@typescript-eslint/quotes': 28 | - error 29 | - single 30 | '@typescript-eslint/no-use-before-define': 31 | - error 32 | - functions: false 33 | classes: false 34 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: 'CodeQL' 13 | 14 | on: 15 | push: 16 | branches: [master] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [master] 20 | schedule: 21 | - cron: '35 23 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ['javascript'] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - beta 7 | - alpha 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [12.x, 14.x, 16.x] 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm run build 22 | - run: npm test 23 | coverage: 24 | needs: test 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v2 28 | - uses: actions/setup-node@v1 29 | with: 30 | node-version: 14 31 | - run: npm install 32 | - run: npm run build 33 | - run: npm run test:coverage 34 | - uses: coverallsapp/github-action@master 35 | with: 36 | github-token: ${{ secrets.GITHUB_TOKEN }} 37 | publish: 38 | needs: test 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v2 42 | - uses: actions/setup-node@v1 43 | with: 44 | node-version: 14 45 | - run: npm install 46 | - run: npx semantic-release 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 50 | HUSKY: 0 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Diagnostic reports (https://nodejs.org/api/report.html) 9 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 10 | 11 | # Ignore IDE and build tools 12 | .vscode 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | *.lcov 17 | 18 | # Compiled binary addons (https://nodejs.org/api/addons.html) 19 | build/Release 20 | 21 | # Dependency directories 22 | node_modules/ 23 | 24 | # TypeScript v1 declaration files 25 | typings/ 26 | 27 | # TypeScript cache 28 | *.tsbuildinfo 29 | 30 | # Typescript output 31 | dist/ 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional eslint cache 37 | .eslintcache 38 | 39 | # Output of 'npm pack' 40 | *.tgz 41 | 42 | # Yarn Integrity file 43 | .yarn-integrity 44 | 45 | # dotenv environment variables file 46 | .env 47 | .env.test 48 | 49 | # Prisma related files 50 | dmmf.json 51 | 52 | # Test artifacts 53 | .testArtifacts 54 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit "" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | arrowParens: always 2 | printWidth: 120 3 | semi: true 4 | singleQuote: true 5 | tabWidth: 2 6 | trailingComma: es5 7 | useTabs: false 8 | bracketSpacing: true 9 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | preset: conventionalcommits 2 | plugins: 3 | - "@semantic-release/commit-analyzer" 4 | - "@semantic-release/release-notes-generator" 5 | - "@semantic-release/changelog" 6 | - "@semantic-release/npm" 7 | - "@semantic-release/github" 8 | - "@semantic-release/git" 9 | branches: 10 | - name: master 11 | - name: beta 12 | prerelease: true 13 | channel: false 14 | - name: alpha 15 | prerelease: true 16 | channel: false 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.0-beta.5](https://github.com/floydspace/prisma-sequelize-generator/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2021-09-17) 2 | 3 | 4 | ### Features 5 | 6 | * support js code models generation ([bb915d0](https://github.com/floydspace/prisma-sequelize-generator/commit/bb915d081241fcfafd918be65b7c8f114b6c75f7)) 7 | 8 | ## [1.0.0-beta.4](https://github.com/floydspace/prisma-sequelize-generator/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2021-09-14) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * do not generate unnecessary code ([e5fe2ac](https://github.com/floydspace/prisma-sequelize-generator/commit/e5fe2ac787973dc99d18756cb9a9b3ae5e687cda)) 14 | 15 | ## [1.0.0-beta.3](https://github.com/floydspace/prisma-sequelize-generator/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2021-09-11) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * broken .env resolving ([93ff453](https://github.com/floydspace/prisma-sequelize-generator/commit/93ff45328479d4e998371a82cdef3ee46ff9c91f)) 21 | 22 | ## [1.0.0-beta.2](https://github.com/floydspace/prisma-sequelize-generator/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2021-09-11) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * bug in models associations, make factory synchronous, fix readme ([b8f4b2d](https://github.com/floydspace/prisma-sequelize-generator/commit/b8f4b2df687486ddfa429ade20df35121aa780b1)) 28 | 29 | ## [1.0.0-beta.1](https://github.com/floydspace/prisma-sequelize-generator/compare/v1.0.0-alpha.3...v1.0.0-beta.1) (2021-09-11) 30 | 31 | 32 | ### Features 33 | 34 | * accept sequelize options on create instance ([d509e78](https://github.com/floydspace/prisma-sequelize-generator/commit/d509e7867c26d2fbf5ad7eacdd5c5c8c85d5d8e3)) 35 | * cleanup generated file structure, improve model initialization ([f1b5730](https://github.com/floydspace/prisma-sequelize-generator/commit/f1b5730dd5e09695c7b6bfb15b93625ac6479271)) 36 | * support enums ([69e1d96](https://github.com/floydspace/prisma-sequelize-generator/commit/69e1d969016ff037152e5f611ba621ba73023c10)) 37 | 38 | ## [1.0.0-alpha.3](https://github.com/floydspace/prisma-sequelize-generator/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2021-09-10) 39 | 40 | 41 | ### Features 42 | 43 | * generate relation associations ([637e367](https://github.com/floydspace/prisma-sequelize-generator/commit/637e3672f28e9526ca9feaec1e631ae4e05560e5)) 44 | * support hasOne and hasMany relations ([8d63d3f](https://github.com/floydspace/prisma-sequelize-generator/commit/8d63d3f0fe527254e949a256eb3c0a0f451730cf)) 45 | * **generator:** generate primary key and timestamps and other field props properly ([23b1ece](https://github.com/floydspace/prisma-sequelize-generator/commit/23b1ece203e25de04d6823682d4c661b3e109709)) 46 | 47 | 48 | ### Bug Fixes 49 | 50 | * **env:** load and parse database url in runtime ([efe8b97](https://github.com/floydspace/prisma-sequelize-generator/commit/efe8b97a50c2235f9dda5fbecabf31bfa710d7f6)) 51 | * **sequelize:** no need to parse db url, sequelize accepts it ([7ed9a04](https://github.com/floydspace/prisma-sequelize-generator/commit/7ed9a04b2f6e0f7f34c11b430b77506a24db0d7d)) 52 | 53 | ## [1.0.0-alpha.2](https://github.com/floydspace/prisma-sequelize-generator/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2021-08-17) 54 | 55 | 56 | ### Bug Fixes 57 | 58 | * **release:** add missed scripts preparing build ([98f5fef](https://github.com/floydspace/prisma-sequelize-generator/commit/98f5fefaff0aed3b7be2d55794e5342f12692581)) 59 | 60 | ## 1.0.0-alpha.1 (2021-08-17) 61 | 62 | 63 | ### Features 64 | 65 | * implement basic generator ([87165fa](https://github.com/floydspace/prisma-sequelize-generator/commit/87165fab3f14b1461569faf1fe1b66554b4f2d19)) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * **ci:** do not run husky in ci ([f69abef](https://github.com/floydspace/prisma-sequelize-generator/commit/f69abefa4d8f2d7d1ac48c686318fb2c9dd17793)) 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Victor Korzunin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🏳️‍🌈⃤ Prisma Sequelize Generator 2 | 3 | A generator, which takes a Prisma 2 `schema.prisma` and generates Sequelize Models. 4 | 5 | [![Generic badge](https://img.shields.io/badge/Generator%20for-◭%20Prisma-9F7AEA.svg)](https://www.prisma.io) 6 | [![npm version](https://img.shields.io/npm/v/prisma-sequelize-generator?label=npm%20package)](https://www.npmjs.com/package/prisma-sequelize-generator) 7 | [![npm downloads](https://img.shields.io/npm/dm/prisma-sequelize-generator)](https://www.npmjs.com/package/prisma-sequelize-generator) 8 | [![build status](https://img.shields.io/github/workflow/status/floydspace/prisma-sequelize-generator/release)](https://github.com/floydspace/prisma-sequelize-generator/actions/workflows/release.yml) 9 | [![Code QL](https://github.com/floydspace/prisma-sequelize-generator/workflows/CodeQL/badge.svg)](https://github.com/floydspace/prisma-sequelize-generator/actions/workflows/codeql-analysis.yml) 10 | [![Coverage Status](https://coveralls.io/repos/github/floydspace/prisma-sequelize-generator/badge.svg?branch=beta)](https://coveralls.io/github/floydspace/prisma-sequelize-generator?branch=beta) 11 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 12 | [![GitHub license](https://img.shields.io/github/license/Naereen/StrapDown.js.svg)](https://github.com/floydspace/prisma-sequelize-generator/blob/master/LICENSE) 13 | 14 | ## Getting Started 15 | 16 | ### 1. Install 17 | 18 | npm: 19 | 20 | ```shell 21 | npm install prisma-sequelize-generator --save-dev 22 | ``` 23 | 24 | yarn: 25 | 26 | ```shell 27 | yarn add -D prisma-sequelize-generator 28 | ``` 29 | 30 | ### 2. Add the generator to the schema 31 | 32 | ```prisma 33 | generator client { 34 | provider = "prisma-sequelize-generator" 35 | } 36 | ``` 37 | 38 | With a custom output path (`./sequelize` - default) 39 | 40 | ```prisma 41 | generator client { 42 | provider = "prisma-sequelize-generator" 43 | output = "custom-output-path" 44 | } 45 | ``` 46 | 47 | Additional options 48 | 49 | ```prisma 50 | generator client { 51 | provider = "prisma-sequelize-generator" 52 | outputFormat = "typescript" 53 | } 54 | ``` 55 | 56 | Supported output formats are `javascript` (alias `js` - default) and `typescript` (alias `ts`). 57 | 58 | ### 3. Run generation 59 | 60 | prisma: 61 | 62 | ```shell 63 | prisma generate 64 | ``` 65 | 66 | ### 3. Use Sequelize Models to interact with your database 67 | 68 | ```typescript 69 | import { createSequelizeInstance } from './prisma/sequelize'; 70 | 71 | const { sequelize, models } = createSequelizeInstance({ 72 | ssl: true, 73 | dialectOptions: { 74 | connectTimeout: 1000, 75 | }, 76 | }); 77 | // use `sequelize` instance and `models` in your application to read and write data in your DB 78 | ``` 79 | 80 | No need to set a connection string, it is set form the `datasource` configuration in your `schema.prisma` by default. 81 | 82 | ## Supported Node Versions 83 | 84 | | Node Version | Support | 85 | | -------------------: | :----------------- | 86 | | (Maintenance LTS) 12 | :heavy_check_mark: | 87 | | (Active LTS) 14 | :heavy_check_mark: | 88 | | (Current) 16 | :heavy_check_mark: | 89 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | transform: { 4 | '^.+\\.ts$': 'ts-jest', 5 | }, 6 | testRegex: './tests/.+\\.test\\.ts$', 7 | collectCoverage: false, 8 | collectCoverageFrom: ['src/**/*.{js,ts}'], 9 | moduleFileExtensions: ['ts', 'js', 'json'], 10 | coverageReporters: ['text-summary', 'lcov'], 11 | }; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prisma-sequelize-generator", 3 | "version": "1.0.0-beta.5", 4 | "main": "dist/cli.js", 5 | "license": "MIT", 6 | "files": [ 7 | "dist", 8 | "plop" 9 | ], 10 | "description": "Sequelize models generator for prisma schema", 11 | "author": { 12 | "name": "Victor Korzunin", 13 | "email": "ifloydrose@gmail.com" 14 | }, 15 | "keywords": [ 16 | "prisma2", 17 | "prisma", 18 | "prisma-generator", 19 | "prisma-schema", 20 | "code-generation", 21 | "sequelize" 22 | ], 23 | "homepage": "https://github.com/floydspace/prisma-sequelize-generator", 24 | "repository": { 25 | "url": "https://github.com/floydspace/prisma-sequelize-generator.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/floydspace/prisma-sequelize-generator/issues" 29 | }, 30 | "dependencies": { 31 | "@prisma/generator-helper": "^2.29.1", 32 | "@prisma/sdk": "^2.29.1", 33 | "morphism": "^1.12.3", 34 | "node-plop": "^0.26.2", 35 | "ramda": "^0.27.1" 36 | }, 37 | "devDependencies": { 38 | "@commitlint/cli": "^13.1.0", 39 | "@commitlint/config-conventional": "^13.1.0", 40 | "@semantic-release/changelog": "^5.0.1", 41 | "@semantic-release/commit-analyzer": "^8.0.1", 42 | "@semantic-release/git": "^9.0.0", 43 | "@semantic-release/github": "^7.2.3", 44 | "@semantic-release/npm": "^7.1.3", 45 | "@semantic-release/release-notes-generator": "^9.0.3", 46 | "@types/jest": "^27.0.1", 47 | "@types/node": "^14.17.9", 48 | "@types/ramda": "^0.27.44", 49 | "@types/temp": "^0.9.1", 50 | "@typescript-eslint/eslint-plugin": "^4.29.2", 51 | "@typescript-eslint/parser": "^4.29.2", 52 | "eslint": "^7.32.0", 53 | "eslint-config-prettier": "^8.3.0", 54 | "eslint-plugin-eslint-comments": "^3.2.0", 55 | "eslint-plugin-jest": "^24.4.0", 56 | "husky": "^7.0.0", 57 | "jest": "^27.0.6", 58 | "lint-staged": "^11.1.2", 59 | "prettier": "^2.3.2", 60 | "prisma": "^2.29.1", 61 | "semantic-release": "^17.4.5", 62 | "temp": "^0.9.4", 63 | "ts-jest": "^27.0.5", 64 | "typescript": "^4.3.5" 65 | }, 66 | "scripts": { 67 | "generate": "prisma generate", 68 | "clean": "rm -rf ./dist", 69 | "build": "npm run clean && tsc", 70 | "prepublishOnly": "npm run build", 71 | "pretest": "npm run lint", 72 | "test": "jest --passWithNoTests", 73 | "test:watch": "jest --watch", 74 | "test:coverage": "jest --coverage", 75 | "lint": "eslint .", 76 | "prepare": "husky install" 77 | }, 78 | "bin": { 79 | "prisma-sequelize-generator": "dist/cli.js" 80 | }, 81 | "publishConfig": { 82 | "access": "public" 83 | }, 84 | "lint-staged": { 85 | "*.ts": [ 86 | "eslint --cache --fix", 87 | "prettier --write", 88 | "jest --bail --findRelatedTests" 89 | ], 90 | "*.md": "prettier --write" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /plop/js/index.hbs: -------------------------------------------------------------------------------- 1 | const { Sequelize } = require('sequelize'); 2 | const { tryLoadEnvs } = require('@prisma/sdk'); 3 | const { mergeDeepRight } = require('ramda'); 4 | const path = require('path'); 5 | 6 | const config = require('./config.json'); 7 | const models = require('./models'); 8 | 9 | const loadedEnv = tryLoadEnvs({ 10 | rootEnvPath: config.relativeEnvPaths.rootEnvPath && path.resolve(__dirname, config.relativeEnvPaths.rootEnvPath), 11 | schemaEnvPath: config.relativeEnvPaths.schemaEnvPath && path.resolve(__dirname, config.relativeEnvPaths.schemaEnvPath), 12 | }); 13 | const env = { ...(loadedEnv ? loadedEnv.parsed : {}), ...process.env }; 14 | const databaseUrl = config.datasource.url.fromEnvVar 15 | ? env[config.datasource.url.fromEnvVar] 16 | : config.datasource.url.value; 17 | 18 | module.exports.createSequelizeInstance = (options) => { 19 | const withDefaults = mergeDeepRight({ 20 | define: { 21 | freezeTableName: true, 22 | }, 23 | }); 24 | 25 | const sequelize = new Sequelize(databaseUrl, withDefaults(options ?? {})); 26 | 27 | // First initialize all models 28 | Object.keys(models).forEach((model) => { 29 | models[model].initialize?.(sequelize); 30 | }); 31 | 32 | // Then apply associations 33 | Object.keys(models).forEach((model) => { 34 | models[model].associate?.(models); 35 | models[model].hooks?.(models); 36 | }); 37 | 38 | return { 39 | sequelize, 40 | models, 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /plop/js/models/Model.hbs: -------------------------------------------------------------------------------- 1 | const { Model, DataTypes } = require('sequelize'); 2 | 3 | module.exports.{{modelName}} = class extends Model { 4 | static initialize(sequelize) { 5 | this.init( 6 | { 7 | {{#each scalarFields}} 8 | {{fieldName}}: { 9 | type: {{#if isList}}DataTypes.ARRAY(DataTypes.{{type}}){{else}}DataTypes.{{{type}}}{{/if}},{{#if (eq allowNull false)}} 10 | allowNull: {{allowNull}},{{/if}}{{#if (and hasDefaultValue (eq isAutoincrement false))}} 11 | defaultValue: '{{default}}',{{/if}}{{#if isId}} 12 | primaryKey: {{isId}},{{/if}}{{#if isAutoincrement}} 13 | autoIncrement: {{isAutoincrement}},{{/if}}{{#if isUnique}} 14 | unique: {{isUnique}},{{/if}} 15 | }, 16 | {{/each}} 17 | }, 18 | { 19 | sequelize, 20 | modelName: '{{modelName}}', 21 | tableName: '{{#if dbName}}{{dbName}}{{else}}{{modelName}}{{/if}}', 22 | timestamps: {{or (or hasCreatedAt hasUpdatedAt) hasDeletedAt}},{{#if (or (or hasCreatedAt hasUpdatedAt) hasDeletedAt)}}{{#if (eq hasCreatedAt false)}} 23 | createdAt: false,{{/if}}{{#if (eq hasUpdatedAt false)}} 24 | updatedAt: false,{{/if}}{{!-- {{#if (eq hasDeletedAt false)}} 25 | deletedAt: false,{{/if}} --}}{{#if hasDeletedAt}} 26 | paranoid: true,{{/if}}{{/if}} 27 | } 28 | ); 29 | } 30 | 31 | {{#if (or belongsToFields (or hasManyFields hasOneFields))}} 32 | static associate(models) { 33 | {{#each belongsToFields}} 34 | this.belongsTo(models.{{name}}, { as: '{{as}}', targetKey: '{{targetKey}}', foreignKey: '{{foreignKey}}' }); 35 | {{/each}} 36 | {{#each hasManyFields}} 37 | this.hasMany(models.{{name}}, { as: '{{as}}' }); 38 | {{/each}} 39 | {{#each hasOneFields}} 40 | this.hasOne(models.{{name}}, { as: '{{as}}' }); 41 | {{/each}} 42 | } 43 | {{/if}} 44 | } 45 | -------------------------------------------------------------------------------- /plop/js/models/index.hbs: -------------------------------------------------------------------------------- 1 | {{#each models}} 2 | const { {{modelName}} } = require('./{{modelName}}'); 3 | {{/each}} 4 | 5 | module.exports = { 6 | {{#each models}} 7 | {{modelName}}, 8 | {{/each}} 9 | }; 10 | -------------------------------------------------------------------------------- /plop/plopfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = function (plop) { 4 | plop.setHelper('eq', (v1, v2) => v1 === v2); 5 | plop.setHelper('ne', (v1, v2) => v1 !== v2); 6 | plop.setHelper('lt', (v1, v2) => v1 < v2); 7 | plop.setHelper('gt', (v1, v2) => v1 > v2); 8 | plop.setHelper('lte', (v1, v2) => v1 <= v2); 9 | plop.setHelper('gte', (v1, v2) => v1 >= v2); 10 | plop.setHelper('and', function () { 11 | return Array.prototype.every.call(arguments, Boolean); 12 | }); 13 | plop.setHelper('or', function () { 14 | return Array.prototype.slice.call(arguments, 0, -1).some(Boolean); 15 | }); 16 | 17 | plop.setGenerator('utils', { 18 | actions: () => [ 19 | { 20 | type: 'add', 21 | path: 'utils/find.{{outputFormat}}', 22 | templateFile: path.join(__dirname, './{{outputFormat}}/utils/find.hbs'), 23 | }, 24 | { 25 | type: 'add', 26 | path: 'utils/index.{{outputFormat}}', 27 | templateFile: path.join(__dirname, './{{outputFormat}}/utils/index.hbs'), 28 | }, 29 | ], 30 | }); 31 | 32 | plop.setGenerator('index', { 33 | actions: () => [ 34 | { 35 | type: 'add', 36 | path: 'models/index.{{outputFormat}}', 37 | templateFile: path.join(__dirname, './{{outputFormat}}/models/index.hbs'), 38 | }, 39 | { 40 | type: 'add', 41 | path: 'index.{{outputFormat}}', 42 | templateFile: path.join(__dirname, './{{outputFormat}}/index.hbs'), 43 | }, 44 | { 45 | type: 'add', 46 | path: 'config.json', 47 | template: '{{{config}}}', 48 | }, 49 | ], 50 | }); 51 | 52 | plop.setGenerator('Model', { 53 | actions: () => [ 54 | { 55 | type: 'add', 56 | path: 'models/{{modelName}}.{{outputFormat}}', 57 | templateFile: path.join(__dirname, './{{outputFormat}}/models/Model.hbs'), 58 | }, 59 | ], 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /plop/ts/index.hbs: -------------------------------------------------------------------------------- 1 | import { Options, Sequelize } from 'sequelize'; 2 | import { tryLoadEnvs } from '@prisma/sdk'; 3 | import { mergeDeepRight } from 'ramda'; 4 | import path from 'path'; 5 | 6 | import config from './config.json'; 7 | import * as models from './models'; 8 | 9 | const loadedEnv = tryLoadEnvs({ 10 | rootEnvPath: config.relativeEnvPaths.rootEnvPath && path.resolve(__dirname, config.relativeEnvPaths.rootEnvPath), 11 | schemaEnvPath: config.relativeEnvPaths.schemaEnvPath && path.resolve(__dirname, config.relativeEnvPaths.schemaEnvPath), 12 | }); 13 | const env = { ...(loadedEnv ? loadedEnv.parsed : {}), ...process.env }; 14 | const databaseUrl = config.datasource.url.fromEnvVar 15 | ? env[config.datasource.url.fromEnvVar] 16 | : config.datasource.url.value; 17 | 18 | export const createSequelizeInstance = (options?: Options) => { 19 | const withDefaults = mergeDeepRight({ 20 | define: { 21 | freezeTableName: true, 22 | }, 23 | }); 24 | 25 | const sequelize = new Sequelize(databaseUrl, withDefaults(options ?? {})); 26 | 27 | // First initialize all models 28 | Object.keys(models).forEach((model) => { 29 | models[model].initialize?.(sequelize); 30 | }); 31 | 32 | // Then apply associations 33 | Object.keys(models).forEach((model) => { 34 | models[model].associate?.(models); 35 | models[model].hooks?.(models); 36 | }); 37 | 38 | return { 39 | sequelize, 40 | models, 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /plop/ts/models/Model.hbs: -------------------------------------------------------------------------------- 1 | import { Sequelize, Model, DataTypes, ModelCtor } from 'sequelize'; 2 | 3 | export class {{modelName}} extends Model { 4 | static initialize(sequelize: Sequelize) { 5 | this.init( 6 | { 7 | {{#each scalarFields}} 8 | {{fieldName}}: { 9 | type: {{#if isList}}DataTypes.ARRAY(DataTypes.{{type}}){{else}}DataTypes.{{{type}}}{{/if}},{{#if (eq allowNull false)}} 10 | allowNull: {{allowNull}},{{/if}}{{#if (and hasDefaultValue (eq isAutoincrement false))}} 11 | defaultValue: '{{default}}',{{/if}}{{#if isId}} 12 | primaryKey: {{isId}},{{/if}}{{#if isAutoincrement}} 13 | autoIncrement: {{isAutoincrement}},{{/if}}{{#if isUnique}} 14 | unique: {{isUnique}},{{/if}} 15 | }, 16 | {{/each}} 17 | }, 18 | { 19 | sequelize, 20 | modelName: '{{modelName}}', 21 | tableName: '{{#if dbName}}{{dbName}}{{else}}{{modelName}}{{/if}}', 22 | timestamps: {{or (or hasCreatedAt hasUpdatedAt) hasDeletedAt}},{{#if (or (or hasCreatedAt hasUpdatedAt) hasDeletedAt)}}{{#if (eq hasCreatedAt false)}} 23 | createdAt: false,{{/if}}{{#if (eq hasUpdatedAt false)}} 24 | updatedAt: false,{{/if}}{{!-- {{#if (eq hasDeletedAt false)}} 25 | deletedAt: false,{{/if}} --}}{{#if hasDeletedAt}} 26 | paranoid: true,{{/if}}{{/if}} 27 | } 28 | ); 29 | } 30 | 31 | {{#if (or belongsToFields (or hasManyFields hasOneFields))}} 32 | static associate(models: Record>) { 33 | {{#each belongsToFields}} 34 | this.belongsTo(models.{{name}}, { as: '{{as}}', targetKey: '{{targetKey}}', foreignKey: '{{foreignKey}}' }); 35 | {{/each}} 36 | {{#each hasManyFields}} 37 | this.hasMany(models.{{name}}, { as: '{{as}}' }); 38 | {{/each}} 39 | {{#each hasOneFields}} 40 | this.hasOne(models.{{name}}, { as: '{{as}}' }); 41 | {{/each}} 42 | } 43 | {{/if}} 44 | } 45 | -------------------------------------------------------------------------------- /plop/ts/models/index.hbs: -------------------------------------------------------------------------------- 1 | {{#each models}} 2 | export * from './{{modelName}}'; 3 | {{/each}} 4 | -------------------------------------------------------------------------------- /plop/ts/utils/find.hbs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | type ItemType = 'd' | 'f' | 'l'; 5 | type Handler = (base: string, item: string, type: ItemType) => boolean | string; 6 | 7 | /** 8 | * Transform a dirent to a file type 9 | * @param dirent 10 | * @returns 11 | */ 12 | function direntToType(dirent: fs.Dirent | fs.Stats) { 13 | return dirent.isFile() ? 'f' : dirent.isDirectory() ? 'd' : dirent.isSymbolicLink() ? 'l' : undefined; 14 | } 15 | 16 | /** 17 | * Is true if at least one matched 18 | * @param string to match aigainst 19 | * @param regexs to be matched with 20 | * @returns 21 | */ 22 | function isMatched(string: string, regexs: (RegExp | string)[]) { 23 | for (const regex of regexs) { 24 | if (typeof regex === 'string') { 25 | if (string.includes(regex)) { 26 | return true; 27 | } 28 | } else if (regex.exec(string)) { 29 | return true; 30 | } 31 | } 32 | 33 | return false; 34 | } 35 | 36 | /** 37 | * Find paths that match a set of regexes 38 | * @param root to start from 39 | * @param match to match against 40 | * @param types to select files, folders, links 41 | * @param deep to recurse in the directory tree 42 | * @param limit to limit the results 43 | * @param handler to further filter results 44 | * @param found to add to already found 45 | * @param seen to add to already seen 46 | * @returns found paths (symlinks preserved) 47 | */ 48 | export function findSync( 49 | root: string, 50 | match: (RegExp | string)[], 51 | types: ('f' | 'd' | 'l')[] = ['f', 'd', 'l'], 52 | deep: ('d' | 'l')[] = [], 53 | limit: number = Infinity, 54 | handler: Handler = () => true, 55 | found: string[] = [], 56 | seen: Record = {} 57 | ) { 58 | try { 59 | const realRoot = fs.realpathSync(root); 60 | 61 | // we make sure not to loop infinitely 62 | if (seen[realRoot]) { 63 | return found; 64 | } 65 | 66 | // we stop if we found enough results 67 | if (limit - found.length <= 0) { 68 | return found; 69 | } 70 | 71 | // we check that the root is a directory 72 | if (direntToType(fs.statSync(realRoot)) !== 'd') { 73 | return found; 74 | } 75 | 76 | // we list the items in the current root 77 | const items = fs.readdirSync(root, { withFileTypes: true }); 78 | 79 | //seen[realRoot] = true 80 | for (const item of items) { 81 | // we get the file info for each item 82 | const itemName = item.name; 83 | const itemType = direntToType(item); 84 | const itemPath = path.join(root, item.name); 85 | 86 | // if the item is one of the selected 87 | if (itemType && types.includes(itemType)) { 88 | // if the path of an item has matched 89 | if (isMatched(itemPath, match)) { 90 | const value = handler(root, itemName, itemType); 91 | 92 | // if we changed the path value 93 | if (typeof value === 'string') { 94 | found.push(value); 95 | } 96 | // if we kept the default path 97 | else if (value === true) { 98 | found.push(itemPath); 99 | } 100 | } 101 | } 102 | 103 | if (deep.includes(itemType as any)) { 104 | // dive within the directory tree 105 | // we recurse and continue mutating `found` 106 | findSync(itemPath, match, types, deep, limit, handler, found, seen); 107 | } 108 | } 109 | } catch {} 110 | 111 | return found; 112 | } 113 | -------------------------------------------------------------------------------- /plop/ts/utils/index.hbs: -------------------------------------------------------------------------------- 1 | export * from './find'; 2 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator models { 2 | provider = "node ./dist/cli.js" 3 | outputFormat = "typescript" 4 | } 5 | 6 | datasource db { 7 | provider = "postgresql" 8 | url = env("DATABASE_URL") 9 | } 10 | 11 | model User { 12 | id Int @id @default(autoincrement()) 13 | createdAt DateTime @default(now()) 14 | email String @unique 15 | weight Float? 16 | is18 Boolean? 17 | name String? @db.VarChar(255) 18 | successorId Int? 19 | successor User? @relation("BlogOwnerHistory", fields: [successorId], references: [id]) 20 | predecessor User? @relation("BlogOwnerHistory") 21 | role Role @default(USER) 22 | posts Post[] 23 | keywords String[] 24 | biography Json 25 | } 26 | 27 | model Post { 28 | id Int @id @default(autoincrement()) 29 | user User? @relation(fields: [userId], references: [id]) 30 | userId Int? 31 | 32 | @@map("Posts") 33 | } 34 | 35 | enum Role { 36 | USER 37 | ADMIN 38 | } 39 | -------------------------------------------------------------------------------- /prisma/sequelize/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator": { 3 | "name": "models", 4 | "provider": { 5 | "fromEnvVar": null, 6 | "value": "node ./dist/cli.js" 7 | }, 8 | "output": { 9 | "value": "/Users/victor/Projects/_own/prisma-sequelize-generator/prisma/sequelize", 10 | "fromEnvVar": "null" 11 | }, 12 | "config": { 13 | "outputFormat": "typescript" 14 | }, 15 | "binaryTargets": [], 16 | "previewFeatures": [] 17 | }, 18 | "relativeEnvPaths": { 19 | "rootEnvPath": "../../.env", 20 | "schemaEnvPath": "../../.env" 21 | }, 22 | "datasource": { 23 | "name": "db", 24 | "provider": "postgresql", 25 | "activeProvider": "postgresql", 26 | "url": { 27 | "fromEnvVar": "DATABASE_URL", 28 | "value": null 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /prisma/sequelize/index.ts: -------------------------------------------------------------------------------- 1 | import { Options, Sequelize } from 'sequelize'; 2 | import { tryLoadEnvs } from '@prisma/sdk'; 3 | import { mergeDeepRight } from 'ramda'; 4 | import path from 'path'; 5 | 6 | import config from './config.json'; 7 | import * as models from './models'; 8 | 9 | const loadedEnv = tryLoadEnvs({ 10 | rootEnvPath: config.relativeEnvPaths.rootEnvPath && path.resolve(__dirname, config.relativeEnvPaths.rootEnvPath), 11 | schemaEnvPath: config.relativeEnvPaths.schemaEnvPath && path.resolve(__dirname, config.relativeEnvPaths.schemaEnvPath), 12 | }); 13 | const env = { ...(loadedEnv ? loadedEnv.parsed : {}), ...process.env }; 14 | const databaseUrl = config.datasource.url.fromEnvVar 15 | ? env[config.datasource.url.fromEnvVar] 16 | : config.datasource.url.value; 17 | 18 | export const createSequelizeInstance = (options?: Options) => { 19 | const withDefaults = mergeDeepRight({ 20 | define: { 21 | freezeTableName: true, 22 | }, 23 | }); 24 | 25 | const sequelize = new Sequelize(databaseUrl, withDefaults(options ?? {})); 26 | 27 | // First initialize all models 28 | Object.keys(models).forEach((model) => { 29 | models[model].initialize?.(sequelize); 30 | }); 31 | 32 | // Then apply associations 33 | Object.keys(models).forEach((model) => { 34 | models[model].associate?.(models); 35 | models[model].hooks?.(models); 36 | }); 37 | 38 | return { 39 | sequelize, 40 | models, 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /prisma/sequelize/models/Post.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize, Model, DataTypes, ModelCtor } from 'sequelize'; 2 | 3 | export class Post extends Model { 4 | static initialize(sequelize: Sequelize) { 5 | this.init( 6 | { 7 | id: { 8 | type: DataTypes.INTEGER, 9 | allowNull: false, 10 | primaryKey: true, 11 | autoIncrement: true, 12 | }, 13 | userId: { 14 | type: DataTypes.INTEGER, 15 | }, 16 | }, 17 | { 18 | sequelize, 19 | modelName: 'Post', 20 | tableName: 'Posts', 21 | timestamps: false, 22 | } 23 | ); 24 | } 25 | 26 | static associate(models: Record>) { 27 | this.belongsTo(models.User, { as: 'user', targetKey: 'id', foreignKey: 'userId' }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /prisma/sequelize/models/User.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize, Model, DataTypes, ModelCtor } from 'sequelize'; 2 | 3 | export class User extends Model { 4 | static initialize(sequelize: Sequelize) { 5 | this.init( 6 | { 7 | id: { 8 | type: DataTypes.INTEGER, 9 | allowNull: false, 10 | primaryKey: true, 11 | autoIncrement: true, 12 | }, 13 | email: { 14 | type: DataTypes.STRING, 15 | allowNull: false, 16 | unique: true, 17 | }, 18 | weight: { 19 | type: DataTypes.FLOAT, 20 | }, 21 | is18: { 22 | type: DataTypes.BOOLEAN, 23 | }, 24 | name: { 25 | type: DataTypes.STRING, 26 | }, 27 | successorId: { 28 | type: DataTypes.INTEGER, 29 | }, 30 | role: { 31 | type: DataTypes.ENUM('USER', 'ADMIN'), 32 | allowNull: false, 33 | defaultValue: 'USER', 34 | }, 35 | keywords: { 36 | type: DataTypes.ARRAY(DataTypes.STRING), 37 | allowNull: false, 38 | }, 39 | biography: { 40 | type: DataTypes.JSONB, 41 | allowNull: false, 42 | }, 43 | }, 44 | { 45 | sequelize, 46 | modelName: 'User', 47 | tableName: 'User', 48 | timestamps: true, 49 | updatedAt: false, 50 | } 51 | ); 52 | } 53 | 54 | static associate(models: Record>) { 55 | this.belongsTo(models.User, { as: 'successor', targetKey: 'id', foreignKey: 'successorId' }); 56 | this.hasMany(models.Post, { as: 'posts' }); 57 | this.hasOne(models.User, { as: 'predecessor' }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /prisma/sequelize/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './User'; 2 | export * from './Post'; 3 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import './'; 4 | -------------------------------------------------------------------------------- /src/generator/helpers.ts: -------------------------------------------------------------------------------- 1 | import { complement, compose, flip, includes, isEmpty } from 'ramda'; 2 | 3 | export const included = flip(includes); 4 | export const notIncluded = compose(complement, flip(includes)); 5 | export const isNotEmpty = complement(isEmpty); 6 | -------------------------------------------------------------------------------- /src/generator/properties.ts: -------------------------------------------------------------------------------- 1 | export const PrismaTypeToSequelizeType: Record = { 2 | Int: 'INTEGER', 3 | Float: 'FLOAT', 4 | Decimal: 'DECIMAL', 5 | String: 'STRING', 6 | Boolean: 'BOOLEAN', 7 | DateTime: 'DATE', 8 | Json: 'JSONB', 9 | }; 10 | 11 | export interface ModelProperties { 12 | modelName: string; 13 | dbName: string; 14 | scalarFields: ScalarProperties[]; 15 | belongsToFields: RelationProperties[]; 16 | hasOneFields: RelationProperties[]; 17 | hasManyFields: RelationProperties[]; 18 | hasCreatedAt: boolean; 19 | hasUpdatedAt: boolean; 20 | hasDeletedAt: boolean; 21 | } 22 | 23 | export interface RelationProperties { 24 | as: string; 25 | name: string; 26 | targetKey: string; 27 | foreignKey: string; 28 | } 29 | 30 | export interface ScalarProperties { 31 | isList: boolean; 32 | hasDefaultValue: boolean; 33 | default: any; 34 | isId: boolean; 35 | isUnique: boolean; 36 | fieldName: string; 37 | type: string; 38 | allowNull: boolean; 39 | isAutoincrement: boolean; 40 | } 41 | -------------------------------------------------------------------------------- /src/generator/transformDMMF.ts: -------------------------------------------------------------------------------- 1 | import { morphism, Schema, createSchema } from 'morphism'; 2 | import R from 'ramda'; 3 | 4 | import { included, isNotEmpty, notIncluded } from './helpers'; 5 | import { ModelProperties, PrismaTypeToSequelizeType, RelationProperties, ScalarProperties } from './properties'; 6 | 7 | import type { DMMF } from '@prisma/generator-helper'; 8 | 9 | export function transformDMMF(dmmf: DMMF.Document) { 10 | const enumIndex = R.indexBy(R.prop('name'), dmmf.datamodel.enums ?? []); 11 | const enumValuesToString = (e: DMMF.DatamodelEnum) => 12 | `ENUM(${e.values 13 | .map(R.prop('name')) 14 | .map((n) => `'${n}'`) 15 | .join(', ')})`; 16 | 17 | const scalarSchema = createSchema( 18 | { 19 | isList: 'isList', 20 | hasDefaultValue: 'hasDefaultValue', 21 | default: 'default', 22 | isId: 'isId', 23 | isUnique: 'isUnique', 24 | fieldName: 'name', 25 | type: (field: DMMF.Field) => 26 | field.kind === 'scalar' 27 | ? R.prop(field.type, PrismaTypeToSequelizeType) 28 | : enumValuesToString(R.prop(field.type, enumIndex)), 29 | allowNull: { path: 'isRequired', fn: R.not }, 30 | isAutoincrement: R.allPass([R.prop('hasDefaultValue'), R.pathEq(['default', 'name'], 'autoincrement')]), 31 | }, 32 | { undefinedValues: { strip: true } } 33 | ); 34 | 35 | const relationMorphism = morphism>({ 36 | as: 'name', 37 | name: 'type', 38 | targetKey: 'relationToFields[0]', 39 | foreignKey: 'relationFromFields[0]', 40 | }); 41 | 42 | const modelMorphism = morphism>({ 43 | modelName: 'name', 44 | dbName: 'dbName', 45 | scalarFields: { 46 | path: 'fields', 47 | fn: (fields: DMMF.Field[]) => 48 | R.filter( 49 | R.allPass([ 50 | R.propSatisfies(included(['scalar', 'enum']), 'kind'), 51 | R.propSatisfies(notIncluded(['createdAt', 'updatedAt', 'deletedAt']), 'name'), 52 | ]), 53 | fields 54 | ).map(morphism(scalarSchema)), 55 | }, 56 | belongsToFields: { 57 | path: 'fields', 58 | fn: (fields: DMMF.Field[]) => 59 | R.filter( 60 | R.allPass([ 61 | R.propEq('kind', 'object'), 62 | R.propSatisfies(R.not, 'isList'), 63 | R.propSatisfies(isNotEmpty, 'relationToFields'), 64 | ]), 65 | fields 66 | ).map(relationMorphism), 67 | }, 68 | hasOneFields: { 69 | path: 'fields', 70 | fn: (fields: DMMF.Field[]) => 71 | R.filter( 72 | R.allPass([ 73 | R.propEq('kind', 'object'), 74 | R.propSatisfies(R.not, 'isList'), 75 | R.propSatisfies(R.isEmpty, 'relationToFields'), 76 | ]), 77 | fields 78 | ).map(relationMorphism), 79 | }, 80 | hasManyFields: { 81 | path: 'fields', 82 | fn: (fields: DMMF.Field[]) => 83 | R.filter(R.allPass([R.propEq('kind', 'object'), R.prop('isList')]), fields).map(relationMorphism), 84 | }, 85 | hasCreatedAt: { path: 'fields', fn: R.compose(R.includes('createdAt'), R.map(R.prop('name'))) }, 86 | hasUpdatedAt: { path: 'fields', fn: R.compose(R.includes('updatedAt'), R.map(R.prop('name'))) }, 87 | hasDeletedAt: { path: 'fields', fn: R.compose(R.includes('deletedAt'), R.map(R.prop('name'))) }, 88 | }); 89 | 90 | const transformMorphism = morphism>({ 91 | models: { path: 'models', fn: modelMorphism }, 92 | }); 93 | 94 | return transformMorphism(dmmf.datamodel); 95 | } 96 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { generatorHandler } from '@prisma/generator-helper'; 2 | import { getEnvPaths, parseEnvValue } from '@prisma/sdk'; 3 | import nodePlop from 'node-plop'; 4 | import * as path from 'path'; 5 | import { mergeRight, prop } from 'ramda'; 6 | 7 | import { transformDMMF } from './generator/transformDMMF'; 8 | 9 | // eslint-disable-next-line @typescript-eslint/no-var-requires 10 | // const pkg = require('../package.json'); 11 | 12 | const formatAliases = { 13 | javascript: 'js', 14 | typescript: 'ts', 15 | js: 'js', 16 | ts: 'ts', 17 | }; 18 | const withDefaults = mergeRight({ 19 | outputFormat: formatAliases.js, 20 | }); 21 | 22 | generatorHandler({ 23 | onManifest() { 24 | return { 25 | defaultOutput: './sequelize', 26 | prettyName: 'Sequelize Models', 27 | }; 28 | }, 29 | async onGenerate(options) { 30 | if (!options.generator.output) { 31 | throw new Error('No output was specified for Prisma Sequelize Generator'); 32 | } 33 | 34 | const generatorConfig = withDefaults(options.generator.config); 35 | 36 | if (!Object.keys(formatAliases).includes(generatorConfig.outputFormat)) { 37 | throw new Error(`Incorrect output format was specified. Allowed formats are: ${Object.keys(formatAliases)}`); 38 | } 39 | const outputFormat = prop(generatorConfig.outputFormat as keyof typeof formatAliases, formatAliases); 40 | 41 | const outputDir = 42 | // This ensures previous version of prisma are still supported 43 | typeof options.generator.output === 'string' 44 | ? (options.generator.output as unknown as string) 45 | : parseEnvValue(options.generator.output); 46 | 47 | try { 48 | const plop = nodePlop(path.join(__dirname, '../plop/plopfile.js'), { destBasePath: outputDir, force: true }); 49 | // const utilsGenerator = plop.getGenerator('utils'); 50 | const indexGenerator = plop.getGenerator('index'); 51 | const modelGenerator = plop.getGenerator('Model'); 52 | 53 | const schemaDir = options.schemaPath ? path.dirname(options.schemaPath) : process.cwd(); 54 | const schemaPath = path.join(schemaDir, 'prisma.schema'); 55 | const envPaths = getEnvPaths(schemaPath, { cwd: outputDir }); 56 | 57 | const config = { 58 | generator: options.generator, 59 | relativeEnvPaths: { 60 | rootEnvPath: envPaths.rootEnvPath && path.relative(outputDir, envPaths.rootEnvPath), 61 | schemaEnvPath: envPaths.schemaEnvPath && path.relative(outputDir, envPaths.schemaEnvPath), 62 | }, 63 | // relativePath: path.relative(outputDir, schemaDir), 64 | // clientVersion: pkg.version, 65 | // engineVersion: options.version, 66 | // datasourceNames: options.datasources.map((d) => d.name), 67 | datasource: options.datasources[0], 68 | }; 69 | // const relativeOutputDir = path.relative(process.cwd(), outputDir); 70 | // const slsRelativeOutputDir = path.relative(process.cwd(), outputDir).split(path.sep).slice(1).join(path.sep); 71 | 72 | const { models } = transformDMMF(options.dmmf); 73 | 74 | await Promise.all([ 75 | // utilsGenerator.runActions({}), 76 | indexGenerator.runActions({ models, config: JSON.stringify(config, null, 2), outputFormat }), 77 | ...models.map((model) => modelGenerator.runActions({ ...model, outputFormat })), 78 | ]); 79 | } catch (e) { 80 | console.error('Error: unable to write files for Prisma Sequelize Generator'); 81 | throw e; 82 | } 83 | }, 84 | }); 85 | -------------------------------------------------------------------------------- /tests/generator/transformDMMF.test.ts: -------------------------------------------------------------------------------- 1 | import { getDMMF } from '@prisma/sdk'; 2 | import { PrismaTypeToSequelizeType } from '../../src/generator/properties'; 3 | 4 | import { transformDMMF } from '../../src/generator/transformDMMF'; 5 | 6 | describe('given transformDMMF,', () => { 7 | it('should transform properly.', async () => { 8 | expect.assertions(1); 9 | 10 | const datamodel = ` 11 | model User { 12 | id String @id 13 | } 14 | `; 15 | const expectedProperties = { 16 | models: [ 17 | { 18 | modelName: 'User', 19 | dbName: null, 20 | scalarFields: [ 21 | { 22 | fieldName: 'id', 23 | allowNull: false, 24 | hasDefaultValue: false, 25 | isAutoincrement: false, 26 | isId: true, 27 | isList: false, 28 | isUnique: false, 29 | type: PrismaTypeToSequelizeType.String, 30 | }, 31 | ], 32 | belongsToFields: [], 33 | hasManyFields: [], 34 | hasOneFields: [], 35 | hasCreatedAt: false, 36 | hasUpdatedAt: false, 37 | hasDeletedAt: false, 38 | }, 39 | ], 40 | }; 41 | 42 | const dmmf = await getDMMF({ datamodel }); 43 | const result = transformDMMF(dmmf); 44 | 45 | expect(result).toStrictEqual(expectedProperties); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { getGenerator } from '@prisma/sdk'; 2 | import temp from 'temp'; 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | 6 | describe('given prisma-sequelize-generator,', () => { 7 | beforeAll(() => { 8 | temp.track(); 9 | }); 10 | 11 | afterAll(() => { 12 | temp.cleanupSync(); 13 | }); 14 | 15 | it('should generate models.', async () => { 16 | expect.hasAssertions(); 17 | 18 | const mockDir = temp.mkdirSync({ dir: path.join(__dirname, '../.testArtifacts') }); 19 | 20 | const generator = await getGenerator({ 21 | schemaPath: path.join(__dirname, '../prisma/schema.prisma'), 22 | baseDir: mockDir, 23 | printDownloadProgress: false, 24 | skipDownload: true, 25 | }); 26 | 27 | await generator.generate(); 28 | 29 | generator.stop(); 30 | 31 | const expectedDir = path.join(mockDir, 'sequelize'); 32 | expect(fs.existsSync(expectedDir)).toBe(true); 33 | expect(fs.existsSync(path.join(expectedDir, 'index.ts'))).toBe(true); 34 | expect(fs.existsSync(path.join(expectedDir, 'config.json'))).toBe(true); 35 | expect(fs.existsSync(path.join(expectedDir, 'models', 'index.ts'))).toBe(true); 36 | expect(fs.existsSync(path.join(expectedDir, 'models', 'Post.ts'))).toBe(true); 37 | expect(fs.existsSync(path.join(expectedDir, 'models', 'User.ts'))).toBe(true); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ES6", 5 | "skipLibCheck": true, 6 | "outDir": "dist", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true 15 | }, 16 | "include": [ 17 | "src/**/*.ts" 18 | ], 19 | "exclude": [ 20 | "node_modules" 21 | ] 22 | } 23 | --------------------------------------------------------------------------------