├── .bin └── test.js ├── .editorconfig ├── .github ├── COMMIT_CONVENTION.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── lock.yml └── stale.yml ├── .gitignore ├── .husky └── commit-msg ├── .prettierignore ├── LICENSE.md ├── README.md ├── adonis-typings ├── container.ts ├── index.ts └── prisma.ts ├── commands ├── PrismaSeederInit.ts ├── PrismaSeederMake.ts ├── PrismaSeederRun.ts └── index.ts ├── package-lock.json ├── package.json ├── providers └── PrismaProvider.ts ├── src ├── PrismaAuthProvider.ts ├── PrismaSeederBase.ts └── SeedsRunner.ts ├── templates ├── seed-index.txt └── seeder.txt └── tsconfig.json /.bin/test.js: -------------------------------------------------------------------------------- 1 | require('@adonisjs/require-ts/build/register') 2 | 3 | const { configure } = require('japa') 4 | 5 | configure({ 6 | files: ['test/**/*.spec.ts'], 7 | }) 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 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 | [*.json] 12 | insert_final_newline = ignore 13 | 14 | [**.min.js] 15 | indent_style = ignore 16 | insert_final_newline = ignore 17 | 18 | [MakeFile] 19 | indent_style = space 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.github/COMMIT_CONVENTION.md: -------------------------------------------------------------------------------- 1 | ## Git Commit Message Convention 2 | 3 | > This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular). 4 | 5 | Using conventional commit messages, we can automate the process of generating the CHANGELOG file. All commits messages will automatically be validated against the following regex. 6 | 7 | ``` js 8 | /^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|ci|chore|types|build|improvement)((.+))?: .{1,50}/ 9 | ``` 10 | 11 | ## Commit Message Format 12 | A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**: 13 | 14 | > The **scope** is optional 15 | 16 | ``` 17 | feat(router): add support for prefix 18 | 19 | Prefix makes it easier to append a path to a group of routes 20 | ``` 21 | 22 | 1. `feat` is type. 23 | 2. `router` is scope and is optional 24 | 3. `add support for prefix` is the subject 25 | 4. The **body** is followed by a blank line. 26 | 5. The optional **footer** can be added after the body, followed by a blank line. 27 | 28 | ## Types 29 | Only one type can be used at a time and only following types are allowed. 30 | 31 | - feat 32 | - fix 33 | - docs 34 | - style 35 | - refactor 36 | - perf 37 | - test 38 | - workflow 39 | - ci 40 | - chore 41 | - types 42 | - build 43 | 44 | If a type is `feat`, `fix` or `perf`, then the commit will appear in the CHANGELOG.md file. However if there is any BREAKING CHANGE, the commit will always appear in the changelog. 45 | 46 | ### Revert 47 | If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit `., where the hash is the SHA of the commit being reverted. 48 | 49 | ## Scope 50 | The scope could be anything specifying place of the commit change. For example: `router`, `view`, `querybuilder`, `database`, `model` and so on. 51 | 52 | ## Subject 53 | The subject contains succinct description of the change: 54 | 55 | - use the imperative, present tense: "change" not "changed" nor "changes". 56 | - don't capitalize first letter 57 | - no dot (.) at the end 58 | 59 | ## Body 60 | 61 | Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". 62 | The body should include the motivation for the change and contrast this with previous behavior. 63 | 64 | ## Footer 65 | 66 | The footer should contain any information about **Breaking Changes** and is also the place to 67 | reference GitHub issues that this commit **Closes**. 68 | 69 | **Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this. 70 | 71 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love pull requests. And following this guidelines will make your pull request easier to merge 4 | 5 | ## Prerequisites 6 | 7 | - Install [EditorConfig](http://editorconfig.org/) plugin for your code editor to make sure it uses correct settings. 8 | - Fork the repository and clone your fork. 9 | - Install dependencies: `npm install`. 10 | 11 | ## Coding style 12 | 13 | We make use of [standard](https://standardjs.com/) to lint our code. Standard does not need a config file and comes with set of non-configurable rules. 14 | 15 | ## Development work-flow 16 | 17 | Always make sure to lint and test your code before pushing it to the GitHub. 18 | 19 | ```bash 20 | npm test 21 | ``` 22 | 23 | Just lint the code 24 | 25 | ```bash 26 | npm run lint 27 | ``` 28 | 29 | **Make sure you add sufficient tests for the change**. 30 | 31 | ## Other notes 32 | 33 | - Do not change version number inside the `package.json` file. 34 | - Do not update `CHANGELOG.md` file. 35 | 36 | ## Need help? 37 | 38 | Feel free to ask. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Prerequisites 4 | 5 | We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. 6 | 7 | - Ensure the issue isn't already reported. 8 | - Ensure you are reporting the bug in the correct repo. 9 | 10 | *Delete the above section and the instructions in the sections below before submitting* 11 | 12 | ## Description 13 | 14 | If this is a feature request, explain why it should be added. Specific use-cases are best. 15 | 16 | For bug reports, please provide as much *relevant* info as possible. 17 | 18 | ## Package version 19 | 20 | 21 | ## Error Message & Stack Trace 22 | 23 | ## Relevant Information 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Proposed changes 4 | 5 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 6 | 7 | ## Types of changes 8 | 9 | What types of changes does your code introduce? 10 | 11 | _Put an `x` in the boxes that apply_ 12 | 13 | - [ ] Bugfix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 16 | 17 | ## Checklist 18 | 19 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 20 | 21 | - [ ] I have read the [CONTRIBUTING](https://github.com/wahyubucil/adonis-prisma/blob/main/.github/CONTRIBUTING.md) doc 22 | - [ ] Lint and unit tests pass locally with my changes 23 | - [ ] I have added tests that prove my fix is effective or that my feature works. 24 | - [ ] I have added necessary documentation (if appropriate) 25 | 26 | ## Further comments 27 | 28 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 29 | -------------------------------------------------------------------------------- /.github/lock.yml: -------------------------------------------------------------------------------- 1 | # Configuration for Lock Threads - https://github.com/dessant/lock-threads-app 2 | 3 | # Number of days of inactivity before a closed issue or pull request is locked 4 | daysUntilLock: 60 5 | 6 | # Skip issues and pull requests created before a given timestamp. Timestamp must 7 | # follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable 8 | skipCreatedBefore: false 9 | 10 | # Issues and pull requests with these labels will be ignored. Set to `[]` to disable 11 | exemptLabels: ['Type: Security'] 12 | 13 | # Label to add before locking, such as `outdated`. Set to `false` to disable 14 | lockLabel: false 15 | 16 | # Comment to post before locking. Set to `false` to disable 17 | lockComment: > 18 | This thread has been automatically locked since there has not been 19 | any recent activity after it was closed. Please open a new issue for 20 | related bugs. 21 | 22 | # Assign `resolved` as the reason for locking. Set to `false` to disable 23 | setLockReason: false 24 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | 4 | # Number of days of inactivity before a stale issue is closed 5 | daysUntilClose: 7 6 | 7 | # Issues with these labels will never be considered stale 8 | exemptLabels: 9 | - "Type: Security" 10 | 11 | # Label to use when marking an issue as stale 12 | staleLabel: "Status: Abandoned" 13 | 14 | # Comment to post when marking an issue as stale. Set to `false` to disable 15 | markComment: > 16 | This issue has been automatically marked as stale because it has not had 17 | recent activity. It will be closed if no further activity occurs. Thank you 18 | for your contributions. 19 | 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | test/__app 4 | .DS_STORE 5 | .nyc_output 6 | .idea 7 | .vscode/ 8 | *.sublime-project 9 | *.sublime-workspace 10 | *.log 11 | build 12 | dist 13 | shrinkwrap.yaml 14 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | HUSKY_GIT_PARAMS=$1 node ./node_modules/@adonisjs/mrm-preset/validate-commit/conventional/validate.js 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | docs 3 | config.json 4 | .eslintrc.json 5 | package.json 6 | *.html 7 | *.txt 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright 2022 Wahyu Bucil, contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adonis Prisma 2 | 3 | > Prisma Provider for AdonisJS 4 | 5 | [![npm-image]][npm-url] [![license-image]][license-url] [![typescript-image]][typescript-url] 6 | 7 | If you want to use [Prisma](https://prisma.io) on AdonisJS, this package provides you with Prisma Client Provider and Auth Provider. 8 | 9 | ## Getting Started 10 | 11 | ### Installation 12 | 13 | Make sure you've already installed Prisma related packages: 14 | 15 | ```sh 16 | npm i --save-dev prisma && npm i @prisma/client 17 | ``` 18 | 19 | Install this package: 20 | 21 | ```sh 22 | npm i @wahyubucil/adonis-prisma 23 | ``` 24 | 25 | ### Setup 26 | 27 | ```sh 28 | node ace configure @wahyubucil/adonis-prisma 29 | ``` 30 | 31 | It will install the provider, and add typings. 32 | 33 | ## Usage 34 | 35 | ### Prisma Client Provider 36 | 37 | Import the Prisma Client from `@ioc:Adonis/Addons/Prisma`. For example: 38 | 39 | ```ts 40 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' 41 | import { prisma } from '@ioc:Adonis/Addons/Prisma' 42 | 43 | export default class UsersController { 44 | public async index({}: HttpContextContract) { 45 | const users = await prisma.user.findMany() 46 | return users 47 | } 48 | } 49 | ``` 50 | 51 | ### Authentication (Prisma Auth Provider) 52 | 53 | Install and configure Adonis Auth first: 54 | 55 | ```sh 56 | npm i @adonisjs/auth 57 | node ace configure @adonisjs/auth 58 | ``` 59 | 60 | When configuring Adonis Auth, you'll be asked some questions related to the provider. Because we're not using the default provider, answer the following questions like these so we can complete the configuration: 61 | 62 | ``` 63 | ❯ Select provider for finding users · database 64 | ... 65 | ❯ Enter the database table name to look up users · users 66 | ❯ Create migration for the users table? (y/N) · false 67 | ... 68 | ``` 69 | 70 | Other questions like `guard`, `storing API tokens`, etc, are based on your preference. 71 | 72 | After configuring the Adonis Auth, you need to config Prisma Auth Provider. Here's the example. 73 | 74 | First, define the schema. Example `schema.prisma`: 75 | 76 | ```prisma 77 | // prisma/schema.prisma 78 | 79 | ... 80 | 81 | model User { 82 | id String @id @default(cuid()) 83 | email String @unique 84 | password String 85 | rememberMeToken String? 86 | name String 87 | } 88 | 89 | ... 90 | ``` 91 | 92 | **IMPORTANT**: You need to define `password` and `rememberMeToken` fields like that because those fields are required. 93 | 94 | After configuring the schema, you need to config Prisma Auth Provider on `contracts/auth.ts` and `config/auth.ts`. For example: 95 | 96 | ```ts 97 | // contracts/auth.ts 98 | import { PrismaAuthProviderContract, PrismaAuthProviderConfig } from '@ioc:Adonis/Addons/Prisma' 99 | import { User } from '@prisma/client' 100 | 101 | declare module '@ioc:Adonis/Addons/Auth' { 102 | interface ProvidersList { 103 | user: { 104 | implementation: PrismaAuthProviderContract 105 | config: PrismaAuthProviderConfig 106 | } 107 | } 108 | 109 | ...... 110 | } 111 | ``` 112 | 113 | ```ts 114 | // config/auth.ts 115 | import { AuthConfig } from '@ioc:Adonis/Addons/Auth' 116 | 117 | const authConfig: AuthConfig = { 118 | guard: 'api', 119 | 120 | guards: { 121 | api: { 122 | driver: 'oat', 123 | provider: { 124 | driver: 'prisma', 125 | identifierKey: 'id', 126 | uids: ['email'], 127 | model: 'user', 128 | }, 129 | }, 130 | }, 131 | } 132 | 133 | export default authConfig 134 | ``` 135 | 136 | Then, you're ready to go! 137 | 138 | The rest usage is the same as other providers. You can refer to the [AdonisJS Authentication guide](https://docs.adonisjs.com/guides/auth/introduction) about the implementation. 139 | 140 | #### Configuration options 141 | 142 | Following is the list of all the available configuration options. 143 | 144 | ```ts 145 | { 146 | provider: { 147 | driver: 'prisma', 148 | identifierKey: 'id', 149 | uids: ['email'], 150 | model: 'user', 151 | hashDriver: 'argon', 152 | } 153 | } 154 | ``` 155 | 156 | ##### driver 157 | 158 | The driver name must always be set to `prisma`. 159 | 160 | --- 161 | 162 | ##### identifierKey 163 | 164 | The `identifierKey` is usually the primary key on the configured model. The authentication package needs it to uniquely identify a user. 165 | 166 | --- 167 | 168 | ##### uids 169 | 170 | An array of model columns to use for the user lookup. The `auth.login` method uses the `uids` to find a user by the provided value. 171 | 172 | For example: If your application allows login with email and username both, then you must define them as `uids`. Also, you need to define the model column names and not the database column names. 173 | 174 | --- 175 | 176 | ##### model 177 | 178 | The model to use for user lookup. 179 | 180 | --- 181 | 182 | ##### hashDriver (optional) 183 | 184 | The driver to use for verifying the user password hash. It is used by the `auth.login` method. If not defined, we will use the default hash driver from the `config/hash.ts` file. 185 | 186 | --- 187 | 188 | ### Seeder (Prisma Seeder) 189 | 190 | Init Prisma Seeder first with the following Ace command: 191 | 192 | ```sh 193 | node ace prisma-seeder:init 194 | ``` 195 | 196 | It will create a file `prisma/seeders/index.ts` to define all of the seeders later. 197 | 198 | You can create a new seeder file by running the following Ace command: 199 | 200 | ```sh 201 | node ace prisma-seeder:make User 202 | ``` 203 | 204 | It will create a file `prisma/seeders/User.ts`. 205 | 206 | Every seeder file must extend the `PrismaSeederBase` class and implement the `run` method. Here's an example implementation: 207 | 208 | ```ts 209 | // prisma/seeders/User.ts 210 | 211 | import { prisma, PrismaSeederBase } from '@ioc:Adonis/Addons/Prisma' 212 | 213 | export default class UserSeeder extends PrismaSeederBase { 214 | public static developmentOnly = false 215 | 216 | public async run() { 217 | await prisma.user.upsert({ 218 | where: { 219 | email: 'viola@prisma.io', 220 | }, 221 | update: { 222 | name: 'Viola the Magnificent', 223 | }, 224 | create: { 225 | email: 'viola@prisma.io', 226 | name: 'Viola the Magnificent', 227 | }, 228 | }) 229 | 230 | await prisma.user.createMany({ 231 | data: [ 232 | { name: 'Bob', email: 'bob@prisma.io' }, 233 | { name: 'Bobo', email: 'bob@prisma.io' }, 234 | { name: 'Yewande', email: 'yewande@prisma.io' }, 235 | { name: 'Angelique', email: 'angelique@prisma.io' }, 236 | ], 237 | skipDuplicates: true, 238 | }) 239 | } 240 | } 241 | ``` 242 | 243 | After creating a seeder, add the file name to `prisma/seeders/index.ts`. That file is useful to arrange the execution order of all seeders. For example: 244 | 245 | ```ts 246 | // prisma/seeders/index.ts 247 | 248 | /** 249 | * Put all seeders filename here. It will be executed based on the order 250 | */ 251 | export default ['User', 'Category', 'Article'] 252 | ``` 253 | 254 | #### Running seeders 255 | 256 | Before running seeders, make sure you've already config `prisma/seeders/index.ts` because the execution order will be based on that file. 257 | 258 | To run seeders, just run the following ace command: 259 | 260 | ```sh 261 | node ace prisma-seeder:run 262 | ``` 263 | 264 | #### Development only seeders 265 | 266 | You can mark a seeder file as development only. This ensures that you don't seed your production database with dummy data by mistake. Seeders for development will only run when the `NODE_ENV` environment variable is set to `development`. 267 | 268 | You can create a development only seeder with `--dev` as the argument. For example: 269 | 270 | ```sh 271 | node ace prisma-seeder:make User --dev 272 | ``` 273 | 274 | Or, if you want to make an existing seeder to development only, just change `developmentOnly` property to `true` on the implementation. For example: 275 | 276 | ```ts 277 | import { prisma, PrismaSeederBase } from '@ioc:Adonis/Addons/Prisma' 278 | 279 | export default class UserSeeder extends PrismaSeederBase { 280 | public static developmentOnly = true // <-- change this 281 | 282 | public async run() { 283 | // Write your database queries inside the run method 284 | } 285 | } 286 | ``` 287 | 288 | --- 289 | 290 |
291 | Built with ❤︎ by Wahyu "The GOAT" Bucil 292 |
293 | 294 | [npm-image]: https://img.shields.io/npm/v/@wahyubucil/adonis-prisma.svg?style=for-the-badge&logo=npm 295 | [npm-url]: https://npmjs.org/package/@wahyubucil/adonis-prisma 'npm' 296 | [license-image]: https://img.shields.io/npm/l/@wahyubucil/adonis-prisma?color=blueviolet&style=for-the-badge 297 | [license-url]: LICENSE.md 'license' 298 | [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript 299 | [typescript-url]: "typescript" 300 | -------------------------------------------------------------------------------- /adonis-typings/container.ts: -------------------------------------------------------------------------------- 1 | declare module '@ioc:Adonis/Core/Application' { 2 | import adonisPrisma from '@ioc:Adonis/Addons/Prisma' 3 | 4 | export interface ContainerBindings { 5 | 'Adonis/Addons/Prisma': typeof adonisPrisma 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /adonis-typings/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /adonis-typings/prisma.ts: -------------------------------------------------------------------------------- 1 | declare module '@ioc:Adonis/Addons/Prisma' { 2 | import type { UserProviderContract } from '@ioc:Adonis/Addons/Auth' 3 | import type { HashersList } from '@ioc:Adonis/Core/Hash' 4 | 5 | // @ts-ignore `Prisma` need to generated first, so we ignore the error 6 | import type { PrismaClient, Prisma } from '@prisma/client' 7 | 8 | /** PRISMA AUTH **/ 9 | 10 | /** 11 | * Shape of the extended property of user object for PrismaProvider 12 | */ 13 | export type PrismaAuthBaseUser = { 14 | password: string 15 | rememberMeToken: string | null 16 | } 17 | 18 | /** 19 | * The shape of configuration accepted by the PrismaAuth. 20 | */ 21 | export type PrismaAuthProviderConfig = { 22 | driver: 'prisma' 23 | identifierKey: string 24 | uids: (keyof Omit)[] 25 | // @ts-ignore `Prisma` need to generated first, so we ignore the error 26 | model: Lowercase 27 | hashDriver?: keyof HashersList 28 | } 29 | 30 | /** 31 | * Prisma Auth Provider 32 | */ 33 | export interface PrismaAuthProviderContract 34 | extends UserProviderContract {} 35 | 36 | /** PRISMA SEEDER **/ 37 | 38 | /** 39 | * Prisma Seeder file node 40 | */ 41 | export interface PrismaSeederFile { 42 | absPath: string 43 | name: string 44 | getSource: () => unknown 45 | } 46 | 47 | /** 48 | * Shape of file node returned by the run method 49 | */ 50 | export type PrismaSeederStatus = { 51 | status: 'pending' | 'completed' | 'failed' | 'ignored' 52 | error?: any 53 | file: PrismaSeederFile 54 | } 55 | 56 | /** 57 | * Shape of seeder class 58 | */ 59 | export type PrismaSeederConstructorContract = { 60 | developmentOnly: boolean 61 | new (): { 62 | run(): Promise 63 | } 64 | } 65 | 66 | const PrismaSeederBase: PrismaSeederConstructorContract 67 | 68 | /** PRISMA CLIENT PROVIDER **/ 69 | const prisma: PrismaClient 70 | 71 | export { prisma, PrismaSeederBase } 72 | } 73 | -------------------------------------------------------------------------------- /commands/PrismaSeederInit.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { BaseCommand } from '@adonisjs/core/build/standalone' 3 | 4 | export default class PrismaSeederInit extends BaseCommand { 5 | public static commandName = 'prisma-seeder:init' 6 | public static description = 'Init Prisma Seeder' 7 | 8 | public async run() { 9 | const stub = join(__dirname, '..', 'templates', 'seed-index.txt') 10 | 11 | this.generator 12 | .addFile('index') 13 | .stub(stub) 14 | .destinationDir('prisma/seeders') 15 | .useMustache() 16 | .appRoot(this.application.cliCwd || this.application.appRoot) 17 | 18 | await this.generator.run() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /commands/PrismaSeederMake.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { args, BaseCommand, flags } from '@adonisjs/core/build/standalone' 3 | 4 | export default class PrismaSeederMake extends BaseCommand { 5 | public static commandName = 'prisma-seeder:make' 6 | public static description = 'Make a new Prisma Seeder file' 7 | 8 | @args.string({ description: 'Name of the seeder class' }) 9 | public name: string 10 | 11 | @flags.boolean({ description: 'Create seeder for development only' }) 12 | public dev: boolean 13 | 14 | public async run() { 15 | const stub = join(__dirname, '..', 'templates', 'seeder.txt') 16 | 17 | this.generator 18 | .addFile(this.name, { pattern: 'pascalcase', form: 'singular' }) 19 | .stub(stub) 20 | .destinationDir('prisma/seeders') 21 | .useMustache() 22 | .appRoot(this.application.cliCwd || this.application.appRoot) 23 | .apply({ developmentOnly: Boolean(this.dev) }) 24 | 25 | await this.generator.run() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /commands/PrismaSeederRun.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommand } from '@adonisjs/core/build/standalone' 2 | import type { PrismaSeederFile, PrismaSeederStatus } from '@ioc:Adonis/Addons/Prisma' 3 | 4 | export default class PrismaSeederRun extends BaseCommand { 5 | public static commandName = 'prisma-seeder:run' 6 | public static description = 'Execute Prisma seeders' 7 | public static settings = { 8 | loadApp: true, 9 | } 10 | 11 | /** 12 | * Print log message to the console 13 | */ 14 | private printLogMessage(file: PrismaSeederStatus) { 15 | const colors = this.colors 16 | 17 | let color: keyof typeof colors = 'gray' 18 | let message: string = '' 19 | let prefix: string = '' 20 | 21 | switch (file.status) { 22 | case 'pending': 23 | message = 'pending ' 24 | color = 'gray' 25 | break 26 | case 'failed': 27 | message = 'error ' 28 | prefix = file.error!.message 29 | color = 'red' 30 | break 31 | case 'ignored': 32 | message = 'ignored ' 33 | prefix = 'Enabled only in development environment' 34 | color = 'dim' 35 | break 36 | case 'completed': 37 | message = 'completed' 38 | color = 'green' 39 | break 40 | } 41 | 42 | console.log(`${colors[color]('❯')} ${colors[color](message)} ${file.file.name}`) 43 | if (prefix) { 44 | console.log(` ${colors[color](prefix)}`) 45 | } 46 | } 47 | 48 | public async run() { 49 | const { SeedsRunner } = await import('../src/SeedsRunner') 50 | const seeder = new SeedsRunner(this.application) 51 | 52 | let files: PrismaSeederFile[] 53 | try { 54 | files = seeder.getList() 55 | } catch (error) { 56 | this.logger.error(error) 57 | this.exitCode = 1 58 | return 59 | } 60 | 61 | let hasError = false 62 | 63 | for (let file of files) { 64 | const response = await seeder.run(file) 65 | if (response.status === 'failed') { 66 | hasError = true 67 | } 68 | this.printLogMessage(response) 69 | } 70 | 71 | this.exitCode = hasError ? 1 : 0 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /commands/index.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | '@wahyubucil/adonis-prisma/build/commands/PrismaSeederInit', 3 | '@wahyubucil/adonis-prisma/build/commands/PrismaSeederMake', 4 | '@wahyubucil/adonis-prisma/build/commands/PrismaSeederRun', 5 | ] 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wahyubucil/adonis-prisma", 3 | "version": "1.0.1", 4 | "description": "Prisma Provider for AdonisJS", 5 | "main": "build/providers/PrismaProvider.js", 6 | "files": [ 7 | "build/adonis-typings", 8 | "build/commands", 9 | "build/providers", 10 | "build/src", 11 | "build/templates" 12 | ], 13 | "types": "./build/adonis-typings/index.d.ts", 14 | "keywords": [ 15 | "AdonisJS", 16 | "Prisma", 17 | "Adonis Prisma" 18 | ], 19 | "author": "wahyubucil", 20 | "license": "MIT", 21 | "scripts": { 22 | "mrm": "mrm --preset=@adonisjs/mrm-preset", 23 | "pretest": "npm run lint", 24 | "test": "node .bin/test.js", 25 | "clean": "del build", 26 | "compile": "npm run lint && npm run clean && tsc && npm run copyfiles", 27 | "copyfiles": "copyfiles \"templates/**/*.txt\" build", 28 | "build": "npm run compile", 29 | "prepublishOnly": "npm run build", 30 | "lint": "eslint . --ext=.ts", 31 | "format": "prettier --write .", 32 | "commit": "git-cz", 33 | "release": "np --message=\"chore(release): %s\"", 34 | "version": "npm run build", 35 | "sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json wahyubucil/adonis-prisma" 36 | }, 37 | "devDependencies": { 38 | "@adonisjs/auth": "^8.2.3", 39 | "@adonisjs/core": "^5.9.0", 40 | "@adonisjs/mrm-preset": "^5.0.3", 41 | "@adonisjs/require-ts": "^2.0.13", 42 | "@prisma/client": "^5.5.2", 43 | "@types/node": "^20.8.9", 44 | "commitizen": "^4.3.0", 45 | "copyfiles": "^2.4.1", 46 | "cz-conventional-changelog": "^3.3.0", 47 | "del-cli": "^5.1.0", 48 | "eslint": "^8.52.0", 49 | "eslint-config-prettier": "^9.0.0", 50 | "eslint-plugin-adonis": "^2.1.1", 51 | "eslint-plugin-prettier": "^5.0.1", 52 | "github-label-sync": "^2.3.1", 53 | "husky": "^8.0.3", 54 | "japa": "^4.0.0", 55 | "mrm": "^4.1.22", 56 | "np": "^8.0.4", 57 | "prettier": "^3.0.3", 58 | "prisma": "^5.5.2", 59 | "typescript": "^5.2.2" 60 | }, 61 | "dependencies": { 62 | "@poppinss/utils": "^5.0.0" 63 | }, 64 | "peerDependencies": { 65 | "@adonisjs/auth": "^8.2.3", 66 | "@adonisjs/core": "^5.9.0", 67 | "@prisma/client": "^5.0.0", 68 | "prisma": "^5.0.0" 69 | }, 70 | "peerDependenciesMeta": { 71 | "@adonisjs/auth": { 72 | "optional": true 73 | } 74 | }, 75 | "config": { 76 | "commitizen": { 77 | "path": "cz-conventional-changelog" 78 | } 79 | }, 80 | "np": { 81 | "contents": ".", 82 | "anyBranch": false 83 | }, 84 | "repository": { 85 | "type": "git", 86 | "url": "git+https://github.com/wahyubucil/adonis-prisma.git" 87 | }, 88 | "bugs": { 89 | "url": "https://github.com/wahyubucil/adonis-prisma/issues" 90 | }, 91 | "homepage": "https://github.com/wahyubucil/adonis-prisma#readme", 92 | "publishConfig": { 93 | "access": "public", 94 | "tag": "latest" 95 | }, 96 | "mrmConfig": { 97 | "core": false, 98 | "license": "MIT", 99 | "services": [], 100 | "minNodeVersion": "18.17.0", 101 | "probotApps": [ 102 | "stale", 103 | "lock" 104 | ] 105 | }, 106 | "eslintConfig": { 107 | "extends": [ 108 | "plugin:adonis/typescriptPackage", 109 | "prettier" 110 | ], 111 | "plugins": [ 112 | "prettier" 113 | ], 114 | "rules": { 115 | "prettier/prettier": [ 116 | "error", 117 | { 118 | "endOfLine": "auto" 119 | } 120 | ] 121 | } 122 | }, 123 | "eslintIgnore": [ 124 | "build" 125 | ], 126 | "prettier": { 127 | "trailingComma": "es5", 128 | "semi": false, 129 | "singleQuote": true, 130 | "useTabs": false, 131 | "quoteProps": "consistent", 132 | "bracketSpacing": true, 133 | "arrowParens": "always", 134 | "printWidth": 100 135 | }, 136 | "adonisjs": { 137 | "types": "@wahyubucil/adonis-prisma", 138 | "providers": [ 139 | "@wahyubucil/adonis-prisma" 140 | ], 141 | "commands": [ 142 | "@wahyubucil/adonis-prisma/build/commands" 143 | ] 144 | }, 145 | "engines": { 146 | "node": ">=18", 147 | "npm": ">=9" 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /providers/PrismaProvider.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationContract } from '@ioc:Adonis/Core/Application' 2 | import { PrismaClient } from '@prisma/client' 3 | 4 | /** 5 | * Registers Prisma with the IoC container 6 | */ 7 | export default class PrismaProvider { 8 | constructor(protected app: ApplicationContract) {} 9 | 10 | /** 11 | * Registering binding to the container 12 | */ 13 | public register() { 14 | this.app.container.singleton('Adonis/Addons/Prisma', () => { 15 | const prisma = new PrismaClient() 16 | const { PrismaSeederBase } = require('../src/PrismaSeederBase') 17 | return { prisma, PrismaSeederBase } 18 | }) 19 | } 20 | 21 | /** 22 | * Extend Adonis Auth with Prisma Auth Provider 23 | */ 24 | public boot() { 25 | this.app.container.withBindings( 26 | ['Adonis/Addons/Auth', 'Adonis/Core/Hash', 'Adonis/Addons/Prisma'], 27 | async (Auth, Hash, { prisma }) => { 28 | const { PrismaAuthProvider } = await import('../src/PrismaAuthProvider') 29 | Auth.extend('provider', 'prisma', (_, __, config) => { 30 | return new PrismaAuthProvider(config, Hash, prisma) 31 | }) 32 | } 33 | ) 34 | } 35 | 36 | /** 37 | * Disconnect Prisma on app shutdown 38 | */ 39 | public async shutdown() { 40 | const { prisma } = this.app.container.resolveBinding('Adonis/Addons/Prisma') 41 | await prisma.$disconnect() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/PrismaAuthProvider.ts: -------------------------------------------------------------------------------- 1 | import type { HashContract } from '@ioc:Adonis/Core/Hash' 2 | import type { ProviderUserContract } from '@ioc:Adonis/Addons/Auth' 3 | import type { 4 | PrismaAuthBaseUser, 5 | PrismaAuthProviderConfig, 6 | PrismaAuthProviderContract, 7 | } from '@ioc:Adonis/Addons/Prisma' 8 | import type { PrismaClient } from '@prisma/client' 9 | 10 | /** 11 | * Provider user works as a bridge between your User provider and 12 | * the AdonisJS auth module. 13 | */ 14 | class ProviderUser implements ProviderUserContract { 15 | constructor( 16 | public user: User | null, 17 | private config: PrismaAuthProviderConfig, 18 | private hash: HashContract 19 | ) {} 20 | 21 | public getId() { 22 | return this.user ? this.user[this.config.identifierKey] : null 23 | } 24 | 25 | public getRememberMeToken() { 26 | return this.user ? this.user.rememberMeToken || null : null 27 | } 28 | 29 | public setRememberMeToken(token: string) { 30 | if (!this.user) { 31 | throw new Error('Cannot set "rememberMeToken" on non-existing user') 32 | } 33 | this.user.rememberMeToken = token 34 | } 35 | 36 | public async verifyPassword(plainPassword: string) { 37 | if (!this.user) { 38 | throw new Error('Cannot "verifyPassword" for non-existing user') 39 | } 40 | 41 | if (!this.user.password) { 42 | throw new Error('Auth user object must have a password in order to call "verifyPassword"') 43 | } 44 | 45 | const hasher = this.config.hashDriver ? this.hash.use(this.config.hashDriver) : this.hash 46 | return hasher.verify(this.user.password, plainPassword) 47 | } 48 | } 49 | 50 | /** 51 | * The User provider implementation to lookup a user for different operations 52 | */ 53 | export class PrismaAuthProvider 54 | implements PrismaAuthProviderContract 55 | { 56 | constructor( 57 | private config: PrismaAuthProviderConfig, 58 | private hash: HashContract, 59 | private prisma: PrismaClient 60 | ) {} 61 | 62 | public async getUserFor(user: User | null) { 63 | return new ProviderUser(user, this.config, this.hash) 64 | } 65 | 66 | public async updateRememberMeToken(user: ProviderUser) { 67 | await (this.prisma[this.config.model] as any).update({ 68 | where: { 69 | [this.config.identifierKey]: user.getId(), 70 | }, 71 | data: { 72 | rememberMeToken: user.getRememberMeToken(), 73 | }, 74 | }) 75 | } 76 | 77 | public async findById(id: string | number) { 78 | const user = await (this.prisma[this.config.model] as any).findUnique({ 79 | where: { [this.config.identifierKey]: id }, 80 | }) 81 | return this.getUserFor(user) 82 | } 83 | 84 | public async findByUid(uidValue: string) { 85 | const orStatements = this.config.uids.map((field) => ({ 86 | [field]: uidValue, 87 | })) 88 | const user = await (this.prisma[this.config.model] as any).findFirst({ 89 | where: { 90 | OR: orStatements, 91 | }, 92 | }) 93 | return this.getUserFor(user) 94 | } 95 | 96 | public async findByRememberMeToken(userId: string | number, token: string) { 97 | const user = await (this.prisma[this.config.model] as any).findFirst({ 98 | where: { 99 | [this.config.identifierKey]: userId, 100 | rememberMeToken: token, 101 | }, 102 | }) 103 | return this.getUserFor(user) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/PrismaSeederBase.ts: -------------------------------------------------------------------------------- 1 | export class PrismaSeederBase { 2 | public static developmentOnly: boolean 3 | 4 | constructor() {} 5 | 6 | public async run() {} 7 | } 8 | -------------------------------------------------------------------------------- /src/SeedsRunner.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import type { ApplicationContract } from '@ioc:Adonis/Core/Application' 3 | import type { 4 | PrismaSeederConstructorContract, 5 | PrismaSeederFile, 6 | PrismaSeederStatus, 7 | } from '@ioc:Adonis/Addons/Prisma' 8 | import { esmRequire } from '@poppinss/utils' 9 | 10 | export class SeedsRunner { 11 | constructor(private app: ApplicationContract) {} 12 | 13 | /** 14 | * Returns an array of seeders 15 | */ 16 | public getList() { 17 | const prismaSeedersDirPath = join(this.app.appRoot, 'prisma/seeders') 18 | 19 | let nameList: string[] 20 | try { 21 | const indexFilePath = join(prismaSeedersDirPath, 'index') 22 | nameList = esmRequire(indexFilePath) 23 | } catch (_) { 24 | throw new Error("The index seed file doesn't exist, please run `node ace prisma-seeder:init`") 25 | } 26 | 27 | if (!Array.isArray(nameList) || nameList.some((e) => typeof e !== 'string')) { 28 | throw new Error('The index seed file should export an array of string') 29 | } 30 | 31 | const files: PrismaSeederFile[] = nameList.map((name) => { 32 | const absPath = join(prismaSeedersDirPath, name) 33 | const getSource = () => esmRequire(absPath) 34 | 35 | return { 36 | absPath, 37 | name, 38 | getSource, 39 | } 40 | }) 41 | 42 | return files 43 | } 44 | 45 | /** 46 | * Returns the seeder source by ensuring value is a class constructor 47 | */ 48 | private getSeederSource(file: PrismaSeederFile) { 49 | const source = file.getSource() 50 | if (typeof source === 'function') { 51 | return source as PrismaSeederConstructorContract 52 | } 53 | 54 | throw new Error(`Invalid schema class exported by "${file.name}"`) 55 | } 56 | 57 | public async run(file: PrismaSeederFile) { 58 | const Source = this.getSeederSource(file) 59 | 60 | const seeder: PrismaSeederStatus = { 61 | status: 'pending', 62 | file: file, 63 | } 64 | 65 | /** 66 | * Ignore when running in non-development environment and seeder is development 67 | * only 68 | */ 69 | if (Source.developmentOnly && !this.app.inDev) { 70 | seeder.status = 'ignored' 71 | return seeder 72 | } 73 | 74 | try { 75 | const seederInstance = new Source() 76 | if (typeof seederInstance.run !== 'function') { 77 | throw new Error(`Missing method "run" on "${seeder.file.name}" seeder`) 78 | } 79 | 80 | await seederInstance.run() 81 | seeder.status = 'completed' 82 | } catch (error) { 83 | seeder.status = 'failed' 84 | seeder.error = error 85 | } 86 | 87 | return seeder 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /templates/seed-index.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * Put all seeders filename here. It will be executed based on the order 3 | */ 4 | export default [] 5 | -------------------------------------------------------------------------------- /templates/seeder.txt: -------------------------------------------------------------------------------- 1 | import { prisma, PrismaSeederBase } from '@ioc:Adonis/Addons/Prisma' 2 | 3 | export default class {{ filename }}Seeder extends PrismaSeederBase { 4 | public static developmentOnly = {{ developmentOnly }} 5 | 6 | public async run() { 7 | // Write your database queries inside the run method 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@adonisjs/mrm-preset/_tsconfig", 3 | "compilerOptions": { 4 | "skipLibCheck": true, 5 | "experimentalDecorators": true 6 | }, 7 | "files": [ 8 | "./node_modules/@adonisjs/application/build/adonis-typings/index.d.ts", 9 | "./node_modules/@adonisjs/hash/build/adonis-typings/index.d.ts", 10 | "./node_modules/@adonisjs/auth/build/adonis-typings/index.d.ts" 11 | ] 12 | } 13 | --------------------------------------------------------------------------------