├── .eslintrc.js ├── .github └── workflows │ ├── main.yml │ └── size.yml ├── .gitignore ├── .npmignore ├── EXAMPLES.md ├── LICENSE ├── README.md ├── package.json ├── src ├── PrismaSchemaBuilder.ts ├── finder.ts ├── getConfig.ts ├── getSchema.ts ├── index.ts ├── lexer.ts ├── parser.ts ├── printSchema.ts ├── produceSchema.ts ├── schemaSorter.ts ├── schemaUtils.ts └── visitor.ts ├── test ├── PrismaSchemaBuilder.test.ts ├── __snapshots__ │ ├── PrismaSchemaBuilder.test.ts.snap │ ├── getSchema.test.ts.snap │ └── printSchema.test.ts.snap ├── finder.test.ts ├── fixtures │ ├── atena-server.prisma │ ├── composite-types.prisma │ ├── empty-comment.prisma │ ├── example.prisma │ ├── kebab-case.prisma │ ├── keystonejs-weird.prisma │ ├── keystonejs.prisma │ ├── links.prisma │ ├── redwood.prisma │ ├── star.prisma │ ├── test.prisma │ └── unsorted.prisma ├── getSchema.test.ts ├── index.test.ts ├── printSchema.test.ts ├── produceSchema.test.ts └── utils.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2020": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": 11, 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['16.x', '18.x', '20.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v3 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Lint 26 | run: yarn lint 27 | 28 | - name: Test 29 | run: yarn test --ci --coverage --maxWorkers=2 30 | 31 | - name: Build 32 | run: yarn build 33 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: 'size' 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | jobs: 7 | size: 8 | runs-on: ubuntu-latest 9 | env: 10 | CI_JOB_NUMBER: 1 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 16 16 | - uses: andresz1/size-limit-action@v1 17 | with: 18 | github_token: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .github 3 | src 4 | test 5 | *.ts 6 | !*.d.ts 7 | -------------------------------------------------------------------------------- /EXAMPLES.md: -------------------------------------------------------------------------------- 1 | # SchemaBuilder 2 | 3 | ## Additional examples 4 | 5 | ### Set a datasource 6 | 7 | Since a schema can only have one datasource, calling this command will override the existing datasource if the schema already has one, or create a datasource block if it doesn't. 8 | 9 | ```ts 10 | datasource(provider: string, url: string | { env: string }) 11 | ``` 12 | 13 | You can set a datasource by passing in the provider and url parameters. 14 | 15 | ```ts 16 | builder.datasource('postgresql', { env: 'DATABASE_URL' }); 17 | ``` 18 | 19 | ```prisma 20 | datasource db { 21 | provider = "postgresql" 22 | url = env("DATABASE_URL") 23 | } 24 | ``` 25 | 26 | ### Access a datasource programmatically 27 | 28 | If you want to perform a custom action that there isn't a Builder method for, you can access the underlying schema object programmatically. 29 | 30 | ```ts 31 | import { Datasource } from '@mrleebo/prisma-ast'; 32 | 33 | // rename the datasource programmatically 34 | builder 35 | .datasource('postgresql', { env: 'DATABASE_URL' }) 36 | .then((datasource) => { 37 | datasource.name = 'DS'; 38 | }); 39 | ``` 40 | 41 | ```prisma 42 | datasource DS { 43 | provider = "postgresql" 44 | url = env("DATABASE_URL") 45 | } 46 | ``` 47 | 48 | ### Add or update a generator 49 | 50 | ```ts 51 | generator(name: string, provider: string) 52 | ``` 53 | 54 | If the schema already has a generator with the given name, it will be updated. Otherwise, a new generator will be created. 55 | 56 | ```ts 57 | builder.generator('nexusPrisma', 'nexus-prisma'); 58 | ``` 59 | 60 | ```prisma 61 | generator nexusPrisma { 62 | provider = "nexus-prisma" 63 | } 64 | ``` 65 | 66 | ### Adding additional assignments to generators 67 | 68 | ```ts 69 | assignment(key: string, value: string) 70 | ``` 71 | 72 | If your generator accepts additional assignments, they can be added by chaining .assignment() calls to your generator. 73 | 74 | ```ts 75 | builder.generator('client', 'prisma-client-js').assignment('output', 'db.js'); 76 | ``` 77 | 78 | ```prisma 79 | generator client { 80 | provider = "prisma-client-js" 81 | output = "db.js" 82 | } 83 | ``` 84 | 85 | ### Access a generator programmatically 86 | 87 | If you want to perform a custom action that there isn't a Builder method for, you can access the underlying schema object programmatically. 88 | 89 | ```prisma 90 | generator client { 91 | provider = "prisma-client-js" 92 | output = "db.js" 93 | } 94 | ``` 95 | 96 | ```ts 97 | import { Generator } from '@mrleebo/prisma-ast'; 98 | 99 | // rename the generator programmatically 100 | builder.generator('client').then((generator) => { 101 | generator.name = 'foo'; 102 | }); 103 | ``` 104 | 105 | ```prisma 106 | generator foo { 107 | provider = "prisma-client-js" 108 | output = "db.js" 109 | } 110 | ``` 111 | 112 | ### Add or update a model 113 | 114 | If the model with that name already exists in the schema, it will be selected and any fields that follow will be appended to the model. Otherwise, the model will be created and added to the schema. 115 | 116 | ```ts 117 | builder.model('Project').field('name', 'String'); 118 | ``` 119 | 120 | ```prisma 121 | model Project { 122 | name String 123 | } 124 | ``` 125 | 126 | ### Add or update a view 127 | 128 | If the view with that name already exists in the schema, it will be selected and any fields that follow will be appended to the view. Otherwise, the view will be created and added to the schema. 129 | 130 | ```ts 131 | builder.view('Project').field('name', 'String'); 132 | ``` 133 | 134 | ```prisma 135 | model Project { 136 | name String 137 | } 138 | ``` 139 | 140 | ### Add or update a composite type 141 | 142 | If the composite type with that name already exists in the schema, it will be selected and any fields that follow will be appended to the type. Otherwise, the composite type will be created and added to the schema. 143 | 144 | ```ts 145 | builder.type('Photo').field('width', 'Int').field('height', 'Int').field('url', 'String'); 146 | ``` 147 | 148 | ```prisma 149 | type Photo { 150 | width Int 151 | height Int 152 | url String 153 | } 154 | ``` 155 | 156 | ### Access a model programmatically 157 | 158 | If you want to perform a custom action that there isn't a Builder method for, you can access the underlying schema object programmatically. 159 | 160 | ```prisma 161 | model Project { 162 | name String 163 | } 164 | ``` 165 | 166 | ```ts 167 | import { Model } from '@mrleebo/prisma-ast'; 168 | 169 | // rename the datasource programmatically 170 | builder.model('Project').then((model) => { 171 | model.name = 'Task'; 172 | }); 173 | ``` 174 | 175 | ```prisma 176 | model Task { 177 | name String 178 | } 179 | ``` 180 | 181 | ### Add a field with an attribute to a model 182 | 183 | If the entered model name already exists, that model will be used as the subject for any field and attribute calls that follow. 184 | 185 | ```prisma 186 | model Project { 187 | name String 188 | } 189 | ``` 190 | 191 | ```ts 192 | builder.model('Project').field('projectCode', 'String').attribute('unique'); 193 | ``` 194 | 195 | ```prisma 196 | model Project { 197 | name String 198 | projectCode String @unique 199 | } 200 | ``` 201 | 202 | ### Add an attribute to an existing field 203 | 204 | If the field already exists, you can add new attributes to it by making calls to `.attribute()`. 205 | 206 | ```prisma 207 | model Project { 208 | name String 209 | projectCode String @unique 210 | } 211 | ``` 212 | 213 | ```ts 214 | builder.model('Project').field('name').attribute('unique'); 215 | ``` 216 | 217 | ```prisma 218 | model Project { 219 | name String @unique 220 | projectCode String @unique 221 | } 222 | ``` 223 | 224 | ### Remove a field 225 | 226 | You can remove an existing field with `.removeField()`. 227 | 228 | ```prisma 229 | model Project { 230 | name String 231 | projectCode String @unique 232 | } 233 | ``` 234 | 235 | ```ts 236 | builder.model('Project').removeField('projectCode'); 237 | ``` 238 | 239 | ```prisma 240 | model Project { 241 | name String 242 | } 243 | ``` 244 | 245 | ### Remove an attribute from an existing field 246 | 247 | You can remove an attribute from a field with `.removeAttribute()`. 248 | 249 | ```prisma 250 | model Project { 251 | name String 252 | projectCode String @unique 253 | } 254 | ``` 255 | 256 | ```ts 257 | builder.model('Project').field('projectCode').removeAttribute('unique'); 258 | ``` 259 | 260 | ```prisma 261 | model Project { 262 | name String 263 | projectCode String 264 | } 265 | ``` 266 | 267 | ### Access a field programmatically 268 | 269 | If you want to perform a custom action that there isn't a Builder method for, you can access the underlying schema object programmatically. 270 | 271 | ```prisma 272 | model TaskMessage { 273 | createdAt DateTime? @db.Timestamptz(6) 274 | } 275 | ``` 276 | 277 | ```ts 278 | import { Field, Attribute } from '@mrleebo/prisma-ast'; 279 | 280 | // Replace the @db.Timestamptz(6) attribute with @default(now()) 281 | builder 282 | .model('TaskMessage') 283 | .field('createdAt') 284 | .then((field) => { 285 | const attribute: Attribute = { 286 | type: 'attribute', 287 | kind: 'field', 288 | name: 'default', 289 | args: [{ type: 'attributeArgument', value: 'now()' }], 290 | }; 291 | field.attributes = [attribute]; 292 | }); 293 | ``` 294 | 295 | ```prisma 296 | model TaskMessage { 297 | createdAt DateTime? @default(now()) 298 | } 299 | ``` 300 | 301 | ### Add an index to a model 302 | 303 | ```prisma 304 | model Project { 305 | name String 306 | projectCode String @unique 307 | } 308 | ``` 309 | 310 | ```ts 311 | builder.model('Project').blockAttribute('index', ['name']); 312 | ``` 313 | 314 | ```prisma 315 | model Project { 316 | name String 317 | projectCode String @unique 318 | @@index([name]) 319 | } 320 | ``` 321 | 322 | ### Add an enum 323 | 324 | ```ts 325 | builder.enum('Role', ['USER', 'ADMIN']); 326 | ``` 327 | 328 | ```prisma 329 | enum Role { 330 | USER 331 | ADMIN 332 | } 333 | ``` 334 | 335 | Additional enumerators can also be added to an existing Enum 336 | 337 | ```ts 338 | builder 339 | .enum('Role') 340 | .break() 341 | .comment('New role added for feature #12') 342 | .enumerator('ORGANIZATION'); 343 | ``` 344 | 345 | ```prisma 346 | enum Role { 347 | USER 348 | ADMIN 349 | 350 | // New role added for feature #12 351 | ORGANIZATION 352 | } 353 | ``` 354 | 355 | ### Comments and Line breaks are also parsed and can be added to the schema 356 | 357 | ```ts 358 | builder 359 | .model('Project') 360 | .break() 361 | .comment('I wish I could add a color to your rainbow'); 362 | ``` 363 | 364 | ```prisma 365 | model Project { 366 | name String 367 | projectCode String @unique 368 | @@index([name]) 369 | 370 | // I wish I could add a color to your rainbow 371 | } 372 | ``` 373 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jeremy Liberman 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Total Downloads 4 | 5 | 6 | npm package 7 | 8 | 9 | License 10 | 11 |

12 |

13 | 14 | 15 | 16 | 17 | Buy Me A Coffee 18 | 19 |

20 | 21 | # @mrleebo/prisma-ast 22 | 23 | This library uses an abstract syntax tree to parse schema.prisma files into an object in JavaScript. It also allows you to update your Prisma schema files using a Builder object pattern that is fully implemented in TypeScript. 24 | 25 | It is similar to [@prisma/sdk](https://github.com/prisma/prisma/tree/master/src/packages/sdk) except that it preserves comments and model attributes. It also doesn't attempt to validate the correctness of the schema at all; the focus is instead on the ability to parse the schema into an object, manipulate it using JavaScript, and re-print the schema back to a file without losing information that isn't captured by other parsers. 26 | 27 | > It is probable that a future version of @prisma/sdk will render this library obsolete. 28 | 29 | ## Install 30 | 31 | ```bash 32 | npm install @mrleebo/prisma-ast 33 | ``` 34 | 35 | ## Examples 36 | 37 | ### Produce a modified schema by building upon an existing schema 38 | 39 | ```ts 40 | produceSchema(source: string, (builder: PrismaSchemaBuilder) => void, printOptions?: PrintOptions): string 41 | ``` 42 | 43 | produceSchema is the simplest way to interact with prisma-ast; you input your schema source and a producer function to produce modifications to it, and it will output the schema source with your modifications applied. 44 | 45 | ```ts 46 | import { produceSchema } from '@mrleebo/prisma-ast'; 47 | 48 | const source = ` 49 | model User { 50 | id Int @id @default(autoincrement()) 51 | name String @unique 52 | } 53 | `; 54 | 55 | const output = produceSchema(source, (builder) => { 56 | builder 57 | .model('AppSetting') 58 | .field('key', 'String', [{ name: 'id' }]) 59 | .field('value', 'Json'); 60 | }); 61 | ``` 62 | 63 | ```prisma 64 | model User { 65 | id Int @id @default(autoincrement()) 66 | name String @unique 67 | } 68 | 69 | model AppSetting { 70 | key String @id 71 | value Json 72 | } 73 | ``` 74 | 75 | For more information about what the builder can do, check out the [PrismaSchemaBuilder](#prismaschemabuilder) class. 76 | 77 | ### PrismaSchemaBuilder 78 | 79 | The `produceSchema()` utility will construct a builder for you, but you can also create your own instance, which may be useful for more interactive use-cases. 80 | 81 | ```ts 82 | import { createPrismaSchemaBuilder } from '@mrleebo/prisma-ast'; 83 | 84 | const builder = createPrismaSchemaBuilder(); 85 | 86 | builder 87 | .model('User') 88 | .field('id', 'Int') 89 | .attribute('id') 90 | .attribute('default', [{ name: 'autoincrement' }]) 91 | .field('name', 'String') 92 | .attribute('unique') 93 | .break() 94 | .comment('this is a comment') 95 | .blockAttribute('index', ['name']); 96 | 97 | const output = builder.print(); 98 | ``` 99 | 100 | ```prisma 101 | model User { 102 | id Int @id @default(autoincrement()) 103 | name String @unique 104 | 105 | // this is a comment 106 | @@index([name]) 107 | } 108 | ``` 109 | 110 | ### Query the prisma schema for specific objects 111 | 112 | The builder can also help you find matching objects in the schema based on name (by string or RegExp) or parent context. You can use this to write tests against your schema, or find fields that don't match a naming convention, for example. 113 | 114 | ```ts 115 | const source = ` 116 | model Product { 117 | id String @id @default(auto()) @map("_id") @db.ObjectId 118 | name String 119 | photos Photo[] 120 | } 121 | ` 122 | 123 | const builder = createPrismaSchemaBuilder(source); 124 | 125 | const product = builder.findByType('model', { name: 'Product' }); 126 | expect(product).toHaveProperty('name', 'Product'); 127 | 128 | const id = builder.findByType('field', { 129 | name: 'id', 130 | within: product?.properties, 131 | }); 132 | expect(id).toHaveProperty('name', 'id'); 133 | 134 | const map = builder.findByType('attribute', { 135 | name: 'map', 136 | within: id?.attributes, 137 | }); 138 | expect(map).toHaveProperty('name', 'map'); 139 | ``` 140 | 141 | ### Re-sort the schema 142 | 143 | prisma-ast can sort the schema for you. The default sort order is `['generator', 'datasource', 'model', 'enum']` and will sort objects of the same type alphabetically. 144 | 145 | ```ts 146 | print(options?: { 147 | sort: boolean, 148 | locales?: string | string[], 149 | sortOrder?: Array<'generator' | 'datasource' | 'model' | 'enum'> 150 | }) 151 | ``` 152 | 153 | You can optionally set your own sort order, or change the locale used by the sort. 154 | 155 | ```ts 156 | // sort with default parameters 157 | builder.print({ sort: true }); 158 | 159 | // sort with options 160 | builder.print({ 161 | sort: true, 162 | locales: 'en-US', 163 | sortOrder: ['datasource', 'generator', 'model', 'enum'], 164 | }); 165 | ``` 166 | 167 | ### Need More SchemaBuilder Code snippets? 168 | 169 | There is a lot that you can do with the schema builder. There are [additional sample references available](./EXAMPLES.md) for you to explore. 170 | 171 | ## Configuration Options 172 | 173 | prisma-ast uses [lilconfig](https://github.com/antonk52/lilconfig) to read configuration options which 174 | can be located in any of the following files, and in several other variations (see [the complete list of search paths](https://www.npmjs.com/package/cosmiconfig)): 175 | 176 | - `"prisma-ast"` in `package.json` 177 | - `.prisma-astrc` 178 | - `.prisma-astrc.json` 179 | - `.prisma-astrc.js` 180 | - `.config/.prisma-astrc` 181 | 182 | Configuration options are: 183 | 184 | | Option | Description | Default Value | 185 | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | 186 | | `parser.nodeTrackingLocation` | Include the token locations of CST Nodes in the output schema.
Disabled by default because it can impact parsing performance.
Possible values are `"none"`, `"onlyOffset"`, and `"full"`. | `"none"` | 187 | 188 | ### Example Custom Configuration 189 | 190 | Here is an example of how you can customize your configuration options in `package.json`. 191 | 192 | ```json 193 | { 194 | "prisma-ast": { 195 | "parser": { 196 | "nodeTrackingLocation": "full" 197 | } 198 | } 199 | } 200 | ``` 201 | 202 | ## Underlying utility functions 203 | 204 | The `produceSchema` and `createPrismaSchemaBuilder` functions are intended to be your interface for interacting with the prisma schema, but you can also get direct access to the AST representation if you need to edit the schema for more advanced usages that aren't covered by the methods above. 205 | 206 | ### Parse a schema.prisma file into an AST object 207 | 208 | The shape of the AST is not fully documented, and it is more likely to change than the builder API. 209 | 210 | ```ts 211 | import { getSchema } from '@mrleebo/prisma-ast'; 212 | 213 | const source = ` 214 | model User { 215 | id Int @id @default(autoincrement()) 216 | name String @unique 217 | } 218 | `; 219 | 220 | const schema = getSchema(source); 221 | ``` 222 | 223 | ### Print a schema AST back out as a string 224 | 225 | This is what `builder.print()` calls internally, and is what you'd use to print if you called `getSchema()`. 226 | 227 | ```ts 228 | import { printSchema } from '@mrleebo/prisma-ast'; 229 | 230 | const source = printSchema(schema); 231 | ``` 232 | 233 | You can optionally re-sort the schema. The default sort order is `['generator', 'datasource', 'model', 'enum']`, and objects with the same type are sorted alphabetically, but the sort order can be overridden. 234 | 235 | ```ts 236 | const source = printSchema(schema, { 237 | sort: true, 238 | locales: 'en-US', 239 | sortOrder: ['datasource', 'generator', 'model', 'enum'], 240 | }); 241 | ``` 242 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.12.1", 3 | "license": "MIT", 4 | "main": "dist/index.js", 5 | "typings": "dist/index.d.ts", 6 | "files": [ 7 | "dist", 8 | "src" 9 | ], 10 | "engines": { 11 | "node": ">=16" 12 | }, 13 | "scripts": { 14 | "start": "dts watch", 15 | "build": "dts build", 16 | "test": "dts test", 17 | "test:watch": "dts test --watch", 18 | "lint": "eslint src", 19 | "prepare": "dts build", 20 | "size": "NODE_OPTIONS=--openssl-legacy-provider size-limit", 21 | "publish-better": "npx np" 22 | }, 23 | "peerDependencies": {}, 24 | "husky": { 25 | "hooks": { 26 | "pre-commit": "eslint src" 27 | } 28 | }, 29 | "prettier": { 30 | "printWidth": 80, 31 | "semi": true, 32 | "singleQuote": true, 33 | "trailingComma": "es5" 34 | }, 35 | "name": "@mrleebo/prisma-ast", 36 | "author": "Jeremy Liberman", 37 | "module": "dist/prisma-ast.esm.js", 38 | "size-limit": [ 39 | { 40 | "path": "dist/prisma-ast.cjs.production.min.js", 41 | "limit": "56 KB" 42 | }, 43 | { 44 | "path": "dist/prisma-ast.esm.js", 45 | "limit": "56 KB" 46 | } 47 | ], 48 | "devDependencies": { 49 | "@size-limit/preset-small-lib": "^8.2.6", 50 | "@typescript-eslint/eslint-plugin": "^4.22.0", 51 | "@typescript-eslint/parser": "^4.22.0", 52 | "dts-cli": "^2.0.3", 53 | "eslint": "^7.24.0", 54 | "husky": "^6.0.0", 55 | "jest": "^29.6.0", 56 | "prettier": "^2.8.8", 57 | "size-limit": "^8.2.6", 58 | "ts-jest": "^29.1.1", 59 | "tslib": "^2.4.0", 60 | "typescript": "^5.1.6" 61 | }, 62 | "dependencies": { 63 | "chevrotain": "^10.5.0", 64 | "lilconfig": "^2.1.0" 65 | }, 66 | "publishConfig": { 67 | "access": "public" 68 | }, 69 | "description": "This library uses an abstract syntax tree to parse schema.prisma files into an object in JavaScript. It is similar to [@prisma/sdk](https://github.com/prisma/prisma/tree/master/src/packages/sdk) except that it preserves comments and model attributes.", 70 | "directories": { 71 | "test": "test" 72 | }, 73 | "repository": { 74 | "type": "git", 75 | "url": "git+https://github.com/MrLeebo/prisma-ast.git" 76 | }, 77 | "bugs": { 78 | "url": "https://github.com/MrLeebo/prisma-ast/issues" 79 | }, 80 | "homepage": "https://github.com/MrLeebo/prisma-ast#readme" 81 | } 82 | -------------------------------------------------------------------------------- /src/PrismaSchemaBuilder.ts: -------------------------------------------------------------------------------- 1 | import * as schema from './getSchema'; 2 | import { 3 | isOneOfSchemaObjects, 4 | isSchemaField, 5 | isSchemaObject, 6 | } from './schemaUtils'; 7 | import { PrintOptions, printSchema } from './printSchema'; 8 | import * as finder from './finder'; 9 | 10 | /** Returns the function type Original with its return type changed to NewReturn. */ 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | type ReplaceReturnType any, NewReturn> = ( 13 | ...a: Parameters 14 | ) => NewReturn; 15 | 16 | /** 17 | * Methods with return values that do not propagate the builder should not have 18 | * their return value modified by the type replacement system below 19 | * */ 20 | type ExtractKeys = 'getSchema' | 'getSubject' | 'getParent' | 'print'; 21 | 22 | /** These keys preserve the return value context that they were given */ 23 | type NeutralKeys = 24 | | 'break' 25 | | 'comment' 26 | | 'attribute' 27 | | 'enumerator' 28 | | 'then' 29 | | 'findByType' 30 | | 'findAllByType'; 31 | 32 | /** Keys allowed when you call .datasource() or .generator() */ 33 | type DatasourceOrGeneratorKeys = 'assignment'; 34 | 35 | /** Keys allowed when you call .enum("name") */ 36 | type EnumKeys = 'enumerator'; 37 | 38 | /** Keys allowed when you call .field("name") */ 39 | type FieldKeys = 'attribute' | 'removeAttribute'; 40 | 41 | /** Keys allowed when you call .model("name") */ 42 | type BlockKeys = 'blockAttribute' | 'field' | 'removeField'; 43 | 44 | type PrismaSchemaFinderOptions = finder.ByTypeOptions & { 45 | within?: finder.ByTypeSourceObject[]; 46 | }; 47 | 48 | /** 49 | * Utility type for making the PrismaSchemaBuilder below readable: 50 | * Removes methods from the builder that are prohibited based on the context 51 | * the builder is in. For example, you can add fields to a model, but you can't 52 | * add fields to an enum or a datasource. 53 | */ 54 | type PrismaSchemaSubset< 55 | Universe extends keyof ConcretePrismaSchemaBuilder, 56 | Method 57 | > = ReplaceReturnType< 58 | ConcretePrismaSchemaBuilder[Universe], 59 | PrismaSchemaBuilder> 60 | >; 61 | 62 | /** 63 | * The brain of this whole operation: depending on the key of the method name 64 | * we receive, filter the available list of method calls the user can make to 65 | * prevent them from making invalid calls, such as builder.datasource().field() 66 | * */ 67 | type PrismaSchemaBuilder = { 68 | [U in K]: U extends ExtractKeys 69 | ? ConcretePrismaSchemaBuilder[U] 70 | : U extends NeutralKeys 71 | ? ConcretePrismaSchemaBuilder[U] //ReplaceReturnType> 72 | : U extends 'datasource' 73 | ? PrismaSchemaSubset 74 | : U extends 'generator' 75 | ? PrismaSchemaSubset 76 | : U extends 'model' 77 | ? PrismaSchemaSubset 78 | : U extends 'view' 79 | ? PrismaSchemaSubset 80 | : U extends 'type' 81 | ? PrismaSchemaSubset 82 | : U extends 'field' 83 | ? PrismaSchemaSubset 84 | : U extends 'removeField' 85 | ? PrismaSchemaSubset 86 | : U extends 'enum' 87 | ? PrismaSchemaSubset 88 | : U extends 'removeAttribute' 89 | ? PrismaSchemaSubset 90 | : PrismaSchemaSubset< 91 | U, 92 | DatasourceOrGeneratorKeys | EnumKeys | FieldKeys | BlockKeys | 'comment' 93 | >; 94 | }; 95 | 96 | type Arg = 97 | | string 98 | | { 99 | name: string; 100 | function?: Arg[]; 101 | }; 102 | type Parent = schema.Block | undefined; 103 | type Subject = schema.Block | schema.Field | schema.Enumerator | undefined; 104 | 105 | export class ConcretePrismaSchemaBuilder { 106 | private schema: schema.Schema; 107 | private _subject: Subject; 108 | private _parent: Parent; 109 | 110 | constructor(source = '') { 111 | this.schema = schema.getSchema(source); 112 | } 113 | 114 | /** Prints the schema out as a source string */ 115 | print(options: PrintOptions = {}): string { 116 | return printSchema(this.schema, options); 117 | } 118 | 119 | /** Returns the underlying schema object for more advanced use cases. */ 120 | getSchema(): schema.Schema { 121 | return this.schema; 122 | } 123 | 124 | /** Mutation Methods */ 125 | 126 | /** Adds or updates a generator block based on the name. */ 127 | generator(name: string, provider = 'prisma-client-js'): this { 128 | const generator: schema.Generator = 129 | this.schema.list.reduce( 130 | (memo, block) => 131 | block.type === 'generator' && block.name === name ? block : memo, 132 | { 133 | type: 'generator', 134 | name, 135 | assignments: [ 136 | { type: 'assignment', key: 'provider', value: `"${provider}"` }, 137 | ], 138 | } 139 | ); 140 | 141 | if (!this.schema.list.includes(generator)) this.schema.list.push(generator); 142 | this._subject = generator; 143 | return this; 144 | } 145 | 146 | /** Removes something from the schema with the given name. */ 147 | drop(name: string): this { 148 | const index = this.schema.list.findIndex( 149 | (block) => 'name' in block && block.name === name 150 | ); 151 | if (index !== -1) this.schema.list.splice(index, 1); 152 | return this; 153 | } 154 | 155 | /** Sets the datasource for the schema. */ 156 | datasource(provider: string, url: string | { env: string }): this { 157 | const datasource: schema.Datasource = { 158 | type: 'datasource', 159 | name: 'db', 160 | assignments: [ 161 | { 162 | type: 'assignment', 163 | key: 'url', 164 | value: 165 | typeof url === 'string' 166 | ? `"${url}"` 167 | : { type: 'function', name: 'env', params: [`"${url.env}"`] }, 168 | }, 169 | { type: 'assignment', key: 'provider', value: provider }, 170 | ], 171 | }; 172 | const existingIndex = this.schema.list.findIndex( 173 | (block) => block.type === 'datasource' 174 | ); 175 | this.schema.list.splice( 176 | existingIndex, 177 | existingIndex !== -1 ? 1 : 0, 178 | datasource 179 | ); 180 | this._subject = datasource; 181 | return this; 182 | } 183 | 184 | /** Adds or updates a model based on the name. Can be chained with .field() or .blockAttribute() to add to it. */ 185 | model(name: string): this { 186 | const model = this.schema.list.reduce( 187 | (memo, block) => 188 | block.type === 'model' && block.name === name ? block : memo, 189 | { type: 'model', name, properties: [] } 190 | ); 191 | if (!this.schema.list.includes(model)) this.schema.list.push(model); 192 | this._subject = model; 193 | return this; 194 | } 195 | 196 | /** Adds or updates a view based on the name. Can be chained with .field() or .blockAttribute() to add to it. */ 197 | view(name: string): this { 198 | const view = this.schema.list.reduce( 199 | (memo, block) => 200 | block.type === 'view' && block.name === name ? block : memo, 201 | { type: 'view', name, properties: [] } 202 | ); 203 | if (!this.schema.list.includes(view)) this.schema.list.push(view); 204 | this._subject = view; 205 | return this; 206 | } 207 | 208 | /** Adds or updates a type based on the name. Can be chained with .field() or .blockAttribute() to add to it. */ 209 | type(name: string): this { 210 | const type = this.schema.list.reduce( 211 | (memo, block) => 212 | block.type === 'type' && block.name === name ? block : memo, 213 | { type: 'type', name, properties: [] } 214 | ); 215 | if (!this.schema.list.includes(type)) this.schema.list.push(type); 216 | this._subject = type; 217 | return this; 218 | } 219 | 220 | /** Adds or updates an enum based on the name. Can be chained with .enumerator() to add a value to it. */ 221 | enum(name: string, enumeratorNames: string[] = []): this { 222 | const e = this.schema.list.reduce( 223 | (memo, block) => 224 | block.type === 'enum' && block.name === name ? block : memo, 225 | { 226 | type: 'enum', 227 | name, 228 | enumerators: enumeratorNames.map((name) => ({ 229 | type: 'enumerator', 230 | name, 231 | })), 232 | } satisfies schema.Enum 233 | ); 234 | if (!this.schema.list.includes(e)) this.schema.list.push(e); 235 | this._subject = e; 236 | return this; 237 | } 238 | 239 | /** Add an enum value to the current enum. */ 240 | enumerator(value: string): this { 241 | const subject = this.getSubject(); 242 | if (!subject || !('type' in subject) || subject.type !== 'enum') { 243 | throw new Error('Subject must be a prisma enum!'); 244 | } 245 | 246 | const enumerator = { 247 | type: 'enumerator', 248 | name: value, 249 | } satisfies schema.Enumerator; 250 | subject.enumerators.push(enumerator); 251 | this._parent = this._subject as Exclude< 252 | Subject, 253 | { type: 'field' | 'enumerator' } 254 | >; 255 | this._subject = enumerator; 256 | return this; 257 | } 258 | 259 | /** 260 | * Returns the current subject, such as a model, field, or enum. 261 | * @example 262 | * builder.getModel('User').field('firstName').getSubject() // the firstName field 263 | * */ 264 | private getSubject(): S { 265 | return this._subject as S; 266 | } 267 | 268 | /** Returns the parent of the current subject when in a nested context. The parent of a field is its model or view. */ 269 | private getParent(): S { 270 | return this._parent as S; 271 | } 272 | 273 | /** 274 | * Adds a block-level attribute to the current model. 275 | * @example 276 | * builder.model('Project') 277 | * .blockAttribute("map", "projects") 278 | * .blockAttribute("unique", ["firstName", "lastName"]) // @@unique([firstName, lastName]) 279 | * */ 280 | blockAttribute( 281 | name: string, 282 | args?: string | string[] | Record 283 | ): this { 284 | let subject = this.getSubject(); 285 | if (subject.type !== 'enum' && !isSchemaObject(subject)) { 286 | const parent = this.getParent(); 287 | if (!isOneOfSchemaObjects(parent, ['model', 'view', 'type', 'enum'])) 288 | throw new Error('Subject must be a prisma model, view, or type!'); 289 | 290 | subject = this._subject = parent; 291 | } 292 | 293 | const attributeArgs = ((): schema.AttributeArgument[] => { 294 | if (!args) return [] as schema.AttributeArgument[]; 295 | if (typeof args === 'string') 296 | return [{ type: 'attributeArgument', value: `"${args}"` }]; 297 | if (Array.isArray(args)) 298 | return [{ type: 'attributeArgument', value: { type: 'array', args } }]; 299 | return Object.entries(args).map(([key, value]) => ({ 300 | type: 'attributeArgument', 301 | value: { type: 'keyValue', key, value }, 302 | })); 303 | })(); 304 | 305 | const property: schema.BlockAttribute = { 306 | type: 'attribute', 307 | kind: 'object', 308 | name, 309 | args: attributeArgs, 310 | }; 311 | 312 | if (subject.type === 'enum') { 313 | subject.enumerators.push(property); 314 | } else { 315 | subject.properties.push(property); 316 | } 317 | return this; 318 | } 319 | 320 | /** Adds an attribute to the current field. */ 321 | attribute( 322 | name: string, 323 | args?: Arg[] | Record 324 | ): this { 325 | const parent = this.getParent(); 326 | const subject = this.getSubject(); 327 | if (!isOneOfSchemaObjects(parent, ['model', 'view', 'type', 'enum'])) { 328 | throw new Error('Parent must be a prisma model or view!'); 329 | } 330 | 331 | if (!isSchemaField(subject)) { 332 | throw new Error('Subject must be a prisma field or enumerator!'); 333 | } 334 | 335 | if (!subject.attributes) subject.attributes = []; 336 | const attribute = subject.attributes.reduce( 337 | (memo, attr) => 338 | attr.type === 'attribute' && 339 | `${attr.group ? `${attr.group}.` : ''}${attr.name}` === name 340 | ? attr 341 | : memo, 342 | { 343 | type: 'attribute', 344 | kind: 'field', 345 | name, 346 | } 347 | ); 348 | 349 | if (Array.isArray(args)) { 350 | const mapArg = (arg: Arg): schema.Value | schema.Func => { 351 | return typeof arg === 'string' 352 | ? arg 353 | : { 354 | type: 'function', 355 | name: arg.name, 356 | params: arg.function?.map(mapArg) ?? [], 357 | }; 358 | }; 359 | 360 | if (args.length > 0) 361 | attribute.args = args.map((arg) => ({ 362 | type: 'attributeArgument', 363 | value: mapArg(arg), 364 | })); 365 | } else if (typeof args === 'object') { 366 | attribute.args = Object.entries(args).map(([key, value]) => ({ 367 | type: 'attributeArgument', 368 | value: { type: 'keyValue', key, value: { type: 'array', args: value } }, 369 | })); 370 | } 371 | 372 | if (!subject.attributes.includes(attribute)) 373 | subject.attributes.push(attribute); 374 | 375 | return this; 376 | } 377 | 378 | /** Remove an attribute from the current field */ 379 | removeAttribute(name: string): this { 380 | const parent = this.getParent(); 381 | const subject = this.getSubject(); 382 | if (!isSchemaObject(parent)) { 383 | throw new Error('Parent must be a prisma model or view!'); 384 | } 385 | 386 | if (!isSchemaField(subject)) { 387 | throw new Error('Subject must be a prisma field!'); 388 | } 389 | 390 | if (!subject.attributes) subject.attributes = []; 391 | subject.attributes = subject.attributes.filter( 392 | (attr) => !(attr.type === 'attribute' && attr.name === name) 393 | ); 394 | 395 | return this; 396 | } 397 | 398 | /** Add an assignment to a generator or datasource */ 399 | assignment( 400 | key: string, 401 | value: string 402 | ): this { 403 | const subject = this.getSubject(); 404 | if ( 405 | !subject || 406 | !('type' in subject) || 407 | !['generator', 'datasource'].includes(subject.type) 408 | ) 409 | throw new Error('Subject must be a prisma generator or datasource!'); 410 | 411 | function tap(subject: T, callback: (s: T) => void) { 412 | callback(subject); 413 | return subject; 414 | } 415 | 416 | const assignment = subject.assignments.reduce( 417 | (memo, assignment) => 418 | assignment.type === 'assignment' && assignment.key === key 419 | ? tap(assignment, (a) => { 420 | a.value = `"${value}"`; 421 | }) 422 | : memo, 423 | { 424 | type: 'assignment', 425 | key, 426 | value: `"${value}"`, 427 | } 428 | ); 429 | 430 | if (!subject.assignments.includes(assignment)) 431 | subject.assignments.push(assignment); 432 | 433 | return this; 434 | } 435 | 436 | /** Finder Methods */ 437 | 438 | /** 439 | * Queries the block list for the given block type. Returns `null` if none 440 | * match. Throws an error if more than one match is found. 441 | * */ 442 | findByType( 443 | typeToMatch: Match, 444 | { within = this.schema.list, ...options }: PrismaSchemaFinderOptions 445 | ): finder.FindByBlock | null { 446 | return finder.findByType(within, typeToMatch, options); 447 | } 448 | 449 | /** 450 | * Queries the block list for the given block type. Returns an array of all 451 | * matching objects, and an empty array (`[]`) if none match. 452 | * */ 453 | findAllByType( 454 | typeToMatch: Match, 455 | { within = this.schema.list, ...options }: PrismaSchemaFinderOptions 456 | ): Array | null> { 457 | return finder.findAllByType(within, typeToMatch, options); 458 | } 459 | 460 | /** Internal Utilities */ 461 | 462 | private blockInsert(statement: schema.Break | schema.Comment): this { 463 | let subject = this.getSubject(); 464 | const allowed = [ 465 | 'datasource', 466 | 'enum', 467 | 'generator', 468 | 'model', 469 | 'view', 470 | 'type', 471 | ]; 472 | if (!subject || !('type' in subject) || !allowed.includes(subject.type)) { 473 | const parent = this.getParent(); 474 | if (!parent || !('type' in parent) || !allowed.includes(parent.type)) { 475 | throw new Error('Subject must be a prisma block!'); 476 | } 477 | 478 | subject = this._subject = parent; 479 | } 480 | 481 | switch (subject.type) { 482 | case 'datasource': { 483 | subject.assignments.push(statement); 484 | break; 485 | } 486 | case 'enum': { 487 | subject.enumerators.push(statement); 488 | break; 489 | } 490 | case 'generator': { 491 | subject.assignments.push(statement); 492 | break; 493 | } 494 | case 'model': { 495 | subject.properties.push(statement); 496 | break; 497 | } 498 | } 499 | return this; 500 | } 501 | 502 | /** Add a line break */ 503 | break(): this { 504 | const lineBreak: schema.Break = { type: 'break' }; 505 | return this.blockInsert(lineBreak); 506 | } 507 | 508 | /** 509 | * Add a comment. Regular comments start with // and do not appear in the 510 | * prisma AST. Node comments start with /// and will appear in the AST, 511 | * affixed to the node that follows the comment. 512 | * */ 513 | comment(text: string, node = false): this { 514 | const comment: schema.Comment = { 515 | type: 'comment', 516 | text: `//${node ? '/' : ''} ${text}`, 517 | }; 518 | return this.blockInsert(comment); 519 | } 520 | 521 | /** 522 | * Add a comment to the schema. Regular comments start with // and do not appear in the 523 | * prisma AST. Node comments start with /// and will appear in the AST, 524 | * affixed to the node that follows the comment. 525 | * */ 526 | schemaComment(text: string, node = false): this { 527 | const comment: schema.Comment = { 528 | type: 'comment', 529 | text: `//${node ? '/' : ''} ${text}`, 530 | }; 531 | this.schema.list.push(comment); 532 | return this; 533 | } 534 | 535 | /** 536 | * Adds or updates a field in the current model. The field can be customized 537 | * further with one or more .attribute() calls. 538 | * */ 539 | field(name: string, fieldType: string | schema.Func = 'String'): this { 540 | let subject = this.getSubject(); 541 | if (!isSchemaObject(subject)) { 542 | const parent = this.getParent(); 543 | if (!isSchemaObject(parent)) 544 | throw new Error( 545 | 'Subject must be a prisma model or view or composite type!' 546 | ); 547 | 548 | subject = this._subject = parent; 549 | } 550 | 551 | const field = subject.properties.reduce( 552 | (memo, block) => 553 | block.type === 'field' && block.name === name ? block : memo, 554 | { 555 | type: 'field', 556 | name, 557 | fieldType, 558 | } 559 | ); 560 | 561 | if (!subject.properties.includes(field)) subject.properties.push(field); 562 | this._parent = subject; 563 | this._subject = field; 564 | return this; 565 | } 566 | 567 | /** Drop a field from the current model or view or composite type. */ 568 | removeField(name: string): this { 569 | let subject = this.getSubject(); 570 | if (!isSchemaObject(subject)) { 571 | const parent = this.getParent(); 572 | if (!isSchemaObject(parent)) 573 | throw new Error( 574 | 'Subject must be a prisma model or view or composite type!' 575 | ); 576 | 577 | subject = this._subject = parent; 578 | } 579 | 580 | subject.properties = subject.properties.filter( 581 | (field) => !(field.type === 'field' && field.name === name) 582 | ); 583 | return this; 584 | } 585 | 586 | /** 587 | * Returns the current subject, allowing for more advanced ways of 588 | * manipulating the schema. 589 | * */ 590 | then>( 591 | callback: (subject: R) => unknown 592 | ): this { 593 | callback(this._subject as R); 594 | return this; 595 | } 596 | } 597 | 598 | export function createPrismaSchemaBuilder( 599 | source?: string 600 | ): PrismaSchemaBuilder< 601 | Exclude< 602 | keyof ConcretePrismaSchemaBuilder, 603 | DatasourceOrGeneratorKeys | EnumKeys | FieldKeys | BlockKeys 604 | > 605 | > { 606 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 607 | return new ConcretePrismaSchemaBuilder(source) as any; 608 | } 609 | -------------------------------------------------------------------------------- /src/finder.ts: -------------------------------------------------------------------------------- 1 | import type * as schema from './getSchema'; 2 | 3 | export type ByTypeSourceObject = 4 | | schema.Block 5 | | schema.Enumerator 6 | | schema.Field 7 | | schema.Property 8 | | schema.Attribute 9 | | schema.Assignment; 10 | 11 | export type ByTypeMatchObject = Exclude< 12 | ByTypeSourceObject, 13 | schema.Comment | schema.Break 14 | >; 15 | export type ByTypeMatch = ByTypeMatchObject['type']; 16 | export type ByTypeOptions = { name?: string | RegExp }; 17 | export type FindByBlock = Extract; 18 | 19 | export const findByType = ( 20 | list: ByTypeSourceObject[], 21 | typeToMatch: Match, 22 | options: ByTypeOptions = {} 23 | ): FindByBlock | null => { 24 | const [match, unexpected] = list.filter(findBy(typeToMatch, options)); 25 | 26 | if (!match) return null; 27 | 28 | if (unexpected) 29 | throw new Error(`Found multiple blocks with [type=${typeToMatch}]`); 30 | 31 | return match; 32 | }; 33 | 34 | export const findAllByType = ( 35 | list: ByTypeSourceObject[], 36 | typeToMatch: Match, 37 | options: ByTypeOptions = {} 38 | ): Array> => { 39 | return list.filter(findBy(typeToMatch, options)); 40 | }; 41 | 42 | type NameOf = Extract< 43 | Match, 44 | Match extends 'assignment' ? 'key' : 'name' 45 | >; 46 | 47 | const findBy = 48 | >( 49 | typeToMatch: Match, 50 | { name }: ByTypeOptions = {} 51 | ) => 52 | (block: ByTypeSourceObject): block is FindByBlock => { 53 | if (name != null) { 54 | const nameAttribute = ( 55 | typeToMatch === 'assignment' ? 'key' : 'name' 56 | ) as MatchName; 57 | if (!(nameAttribute in block)) return false; 58 | const nameMatches = 59 | typeof name === 'string' 60 | ? block[nameAttribute] === name 61 | : name.test(block[nameAttribute]); 62 | if (!nameMatches) return false; 63 | } 64 | 65 | return block.type === typeToMatch; 66 | }; 67 | -------------------------------------------------------------------------------- /src/getConfig.ts: -------------------------------------------------------------------------------- 1 | import type { IParserConfig } from 'chevrotain'; 2 | import { 3 | lilconfigSync as configSync, 4 | type LilconfigResult as ConfigResultRaw, 5 | } from 'lilconfig'; 6 | 7 | export type PrismaAstParserConfig = Pick; 8 | export interface PrismaAstConfig { 9 | parser: PrismaAstParserConfig; 10 | } 11 | 12 | type ConfigResult = Omit & { 13 | config: T; 14 | }; 15 | 16 | const defaultConfig: PrismaAstConfig = { 17 | parser: { nodeLocationTracking: 'none' }, 18 | }; 19 | 20 | let config: PrismaAstConfig; 21 | export default function getConfig(): PrismaAstConfig { 22 | if (config != null) return config; 23 | 24 | const result: ConfigResult | null = 25 | configSync('prisma-ast').search(); 26 | return (config = Object.assign(defaultConfig, result?.config)); 27 | } 28 | -------------------------------------------------------------------------------- /src/getSchema.ts: -------------------------------------------------------------------------------- 1 | import { PrismaLexer } from './lexer'; 2 | import { PrismaVisitor, defaultVisitor } from './visitor'; 3 | import type { CstNodeLocation } from 'chevrotain'; 4 | import { PrismaParser, defaultParser } from './parser'; 5 | 6 | /** 7 | * Parses a string containing a prisma schema's source code and returns an 8 | * object that represents the parsed data structure. You can make direct 9 | * modifications to the objects and arrays nested within, and then produce 10 | * a new prisma schema using printSchema(). 11 | * 12 | * @example 13 | * const schema = getSchema(source) 14 | * // ... make changes to schema object ... 15 | * const changedSource = printSchema(schema) 16 | * */ 17 | export function getSchema( 18 | source: string, 19 | options?: { 20 | parser: PrismaParser; 21 | visitor: PrismaVisitor; 22 | } 23 | ): Schema { 24 | const lexingResult = PrismaLexer.tokenize(source); 25 | 26 | const parser = options?.parser ?? defaultParser; 27 | parser.input = lexingResult.tokens; 28 | const cstNode = parser.schema(); 29 | if (parser.errors.length > 0) throw parser.errors[0]; 30 | 31 | const visitor = options?.visitor ?? defaultVisitor; 32 | return visitor.visit(cstNode); 33 | } 34 | 35 | export interface Schema { 36 | type: 'schema'; 37 | list: Block[]; 38 | } 39 | 40 | export type Block = 41 | | Model 42 | | View 43 | | Datasource 44 | | Generator 45 | | Enum 46 | | Comment 47 | | Break 48 | | Type; 49 | 50 | export interface Object { 51 | type: 'model' | 'view' | 'type'; 52 | name: string; 53 | properties: Array; 54 | } 55 | 56 | export interface Model extends Object { 57 | type: 'model'; 58 | location?: CstNodeLocation; 59 | } 60 | 61 | export interface View extends Object { 62 | type: 'view'; 63 | location?: CstNodeLocation; 64 | } 65 | 66 | export interface Type extends Object { 67 | type: 'type'; 68 | location?: CstNodeLocation; 69 | } 70 | 71 | export interface Datasource { 72 | type: 'datasource'; 73 | name: string; 74 | assignments: Array; 75 | location?: CstNodeLocation; 76 | } 77 | 78 | export interface Generator { 79 | type: 'generator'; 80 | name: string; 81 | assignments: Array; 82 | location?: CstNodeLocation; 83 | } 84 | 85 | export interface Enum { 86 | type: 'enum'; 87 | name: string; 88 | enumerators: Array< 89 | Enumerator | Comment | Break | BlockAttribute | GroupedAttribute 90 | >; 91 | location?: CstNodeLocation; 92 | } 93 | 94 | export interface Comment { 95 | type: 'comment'; 96 | text: string; 97 | } 98 | 99 | export interface Break { 100 | type: 'break'; 101 | } 102 | 103 | export type Property = GroupedBlockAttribute | BlockAttribute | Field; 104 | 105 | export interface Assignment { 106 | type: 'assignment'; 107 | key: string; 108 | value: Value; 109 | } 110 | 111 | export interface Enumerator { 112 | type: 'enumerator'; 113 | name: string; 114 | value?: Value; 115 | attributes?: Attribute[]; 116 | comment?: string; 117 | } 118 | 119 | export interface BlockAttribute { 120 | type: 'attribute'; 121 | kind: 'object' | 'view' | 'type'; 122 | group?: string; 123 | name: string; 124 | args: AttributeArgument[]; 125 | location?: CstNodeLocation; 126 | } 127 | 128 | export type GroupedBlockAttribute = BlockAttribute & { group: string }; 129 | 130 | export interface Field { 131 | type: 'field'; 132 | name: string; 133 | fieldType: string | Func; 134 | array?: boolean; 135 | optional?: boolean; 136 | attributes?: Attribute[]; 137 | comment?: string; 138 | location?: CstNodeLocation; 139 | } 140 | 141 | export type Attr = 142 | | Attribute 143 | | GroupedAttribute 144 | | BlockAttribute 145 | | GroupedBlockAttribute; 146 | 147 | export interface Attribute { 148 | type: 'attribute'; 149 | kind: 'field'; 150 | group?: string; 151 | name: string; 152 | args?: AttributeArgument[]; 153 | location?: CstNodeLocation; 154 | } 155 | 156 | export type GroupedAttribute = Attribute & { group: string }; 157 | 158 | export interface AttributeArgument { 159 | type: 'attributeArgument'; 160 | value: KeyValue | Value | Func; 161 | } 162 | 163 | export interface KeyValue { 164 | type: 'keyValue'; 165 | key: string; 166 | value: Value; 167 | } 168 | 169 | export interface Func { 170 | type: 'function'; 171 | name: string; 172 | params: Value[]; 173 | } 174 | 175 | export interface RelationArray { 176 | type: 'array'; 177 | args: string[]; 178 | } 179 | 180 | export type Value = 181 | | string 182 | | number 183 | | boolean 184 | | Func 185 | | RelationArray 186 | | Array; 187 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './produceSchema'; 2 | export * from './getSchema'; 3 | export * from './printSchema'; 4 | export * from './PrismaSchemaBuilder'; 5 | export type { PrismaAstConfig } from './getConfig'; 6 | export type { CstNodeLocation } from 'chevrotain'; 7 | export { VisitorClassFactory } from './visitor'; 8 | export { PrismaParser } from './parser'; 9 | -------------------------------------------------------------------------------- /src/lexer.ts: -------------------------------------------------------------------------------- 1 | import { createToken, Lexer, IMultiModeLexerDefinition } from 'chevrotain'; 2 | 3 | export const Identifier = createToken({ 4 | name: 'Identifier', 5 | pattern: /[a-zA-Z][\w-]*/, 6 | }); 7 | export const Datasource = createToken({ 8 | name: 'Datasource', 9 | pattern: /datasource/, 10 | push_mode: 'block', 11 | }); 12 | export const Generator = createToken({ 13 | name: 'Generator', 14 | pattern: /generator/, 15 | push_mode: 'block', 16 | }); 17 | export const Model = createToken({ 18 | name: 'Model', 19 | pattern: /model/, 20 | push_mode: 'block', 21 | }); 22 | export const View = createToken({ 23 | name: 'View', 24 | pattern: /view/, 25 | push_mode: 'block', 26 | }); 27 | export const Enum = createToken({ 28 | name: 'Enum', 29 | pattern: /enum/, 30 | push_mode: 'block', 31 | }); 32 | export const Type = createToken({ 33 | name: 'Type', 34 | pattern: /type/, 35 | push_mode: 'block', 36 | }); 37 | export const True = createToken({ 38 | name: 'True', 39 | pattern: /true/, 40 | longer_alt: Identifier, 41 | }); 42 | export const False = createToken({ 43 | name: 'False', 44 | pattern: /false/, 45 | longer_alt: Identifier, 46 | }); 47 | export const Null = createToken({ 48 | name: 'Null', 49 | pattern: /null/, 50 | longer_alt: Identifier, 51 | }); 52 | export const Comment = createToken({ 53 | name: 'Comment', 54 | pattern: Lexer.NA, 55 | }); 56 | 57 | export const DocComment = createToken({ 58 | name: 'DocComment', 59 | pattern: /\/\/\/[ \t]*(.*)/, 60 | categories: [Comment], 61 | }); 62 | export const LineComment = createToken({ 63 | name: 'LineComment', 64 | pattern: /\/\/[ \t]*(.*)/, 65 | categories: [Comment], 66 | }); 67 | export const Attribute = createToken({ 68 | name: 'Attribute', 69 | pattern: Lexer.NA, 70 | }); 71 | export const BlockAttribute = createToken({ 72 | name: 'BlockAttribute', 73 | pattern: /@@/, 74 | label: "'@@'", 75 | categories: [Attribute], 76 | }); 77 | export const FieldAttribute = createToken({ 78 | name: 'FieldAttribute', 79 | pattern: /@/, 80 | label: "'@'", 81 | categories: [Attribute], 82 | }); 83 | export const Dot = createToken({ 84 | name: 'Dot', 85 | pattern: /\./, 86 | label: "'.'", 87 | }); 88 | export const QuestionMark = createToken({ 89 | name: 'QuestionMark', 90 | pattern: /\?/, 91 | label: "'?'", 92 | }); 93 | export const LCurly = createToken({ 94 | name: 'LCurly', 95 | pattern: /{/, 96 | label: "'{'", 97 | }); 98 | export const RCurly = createToken({ 99 | name: 'RCurly', 100 | pattern: /}/, 101 | label: "'}'", 102 | pop_mode: true, 103 | }); 104 | export const LRound = createToken({ 105 | name: 'LRound', 106 | pattern: /\(/, 107 | label: "'('", 108 | }); 109 | export const RRound = createToken({ 110 | name: 'RRound', 111 | pattern: /\)/, 112 | label: "')'", 113 | }); 114 | export const LSquare = createToken({ 115 | name: 'LSquare', 116 | pattern: /\[/, 117 | label: "'['", 118 | }); 119 | export const RSquare = createToken({ 120 | name: 'RSquare', 121 | pattern: /\]/, 122 | label: "']'", 123 | }); 124 | export const Comma = createToken({ 125 | name: 'Comma', 126 | pattern: /,/, 127 | label: "','", 128 | }); 129 | export const Colon = createToken({ 130 | name: 'Colon', 131 | pattern: /:/, 132 | label: "':'", 133 | }); 134 | export const Equals = createToken({ 135 | name: 'Equals', 136 | pattern: /=/, 137 | label: "'='", 138 | }); 139 | export const StringLiteral = createToken({ 140 | name: 'StringLiteral', 141 | pattern: /"(:?[^\\"\n\r]|\\(:?[bfnrtv"\\/]|u[0-9a-fA-F]{4}))*"/, 142 | }); 143 | export const NumberLiteral = createToken({ 144 | name: 'NumberLiteral', 145 | pattern: /-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/, 146 | }); 147 | export const WhiteSpace = createToken({ 148 | name: 'WhiteSpace', 149 | pattern: /\s+/, 150 | group: Lexer.SKIPPED, 151 | }); 152 | export const LineBreak = createToken({ 153 | name: 'LineBreak', 154 | pattern: /\n|\r\n/, 155 | line_breaks: true, 156 | label: 'LineBreak', 157 | }); 158 | 159 | const naTokens = [Comment, DocComment, LineComment, LineBreak, WhiteSpace]; 160 | 161 | export const multiModeTokens: IMultiModeLexerDefinition = { 162 | modes: { 163 | global: [...naTokens, Datasource, Generator, Model, View, Enum, Type], 164 | block: [ 165 | ...naTokens, 166 | Attribute, 167 | BlockAttribute, 168 | FieldAttribute, 169 | Dot, 170 | QuestionMark, 171 | LCurly, 172 | RCurly, 173 | LSquare, 174 | RSquare, 175 | LRound, 176 | RRound, 177 | Comma, 178 | Colon, 179 | Equals, 180 | True, 181 | False, 182 | Null, 183 | StringLiteral, 184 | NumberLiteral, 185 | Identifier, 186 | ], 187 | }, 188 | defaultMode: 'global', 189 | }; 190 | 191 | export const PrismaLexer = new Lexer(multiModeTokens); 192 | -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | import { CstParser } from 'chevrotain'; 2 | import getConfig, { PrismaAstParserConfig } from './getConfig'; 3 | import * as lexer from './lexer'; 4 | 5 | type ComponentType = 6 | | 'datasource' 7 | | 'generator' 8 | | 'model' 9 | | 'view' 10 | | 'enum' 11 | | 'type'; 12 | export class PrismaParser extends CstParser { 13 | readonly config: PrismaAstParserConfig; 14 | 15 | constructor(config: PrismaAstParserConfig) { 16 | super(lexer.multiModeTokens, config); 17 | this.performSelfAnalysis(); 18 | this.config = config; 19 | } 20 | 21 | private break = this.RULE('break', () => { 22 | this.CONSUME1(lexer.LineBreak); 23 | this.CONSUME2(lexer.LineBreak); 24 | }); 25 | 26 | private keyedArg = this.RULE('keyedArg', () => { 27 | this.CONSUME(lexer.Identifier, { LABEL: 'keyName' }); 28 | this.CONSUME(lexer.Colon); 29 | this.SUBRULE(this.value); 30 | }); 31 | 32 | private array = this.RULE('array', () => { 33 | this.CONSUME(lexer.LSquare); 34 | this.MANY_SEP({ 35 | SEP: lexer.Comma, 36 | DEF: () => { 37 | this.SUBRULE(this.value); 38 | }, 39 | }); 40 | this.CONSUME(lexer.RSquare); 41 | }); 42 | 43 | private func = this.RULE('func', () => { 44 | this.CONSUME(lexer.Identifier, { LABEL: 'funcName' }); 45 | this.CONSUME(lexer.LRound); 46 | this.MANY_SEP({ 47 | SEP: lexer.Comma, 48 | DEF: () => { 49 | this.OR([ 50 | { ALT: () => this.SUBRULE(this.keyedArg) }, 51 | { ALT: () => this.SUBRULE(this.value) }, 52 | ]); 53 | }, 54 | }); 55 | this.CONSUME(lexer.RRound); 56 | }); 57 | 58 | private value = this.RULE('value', () => { 59 | this.OR([ 60 | { ALT: () => this.CONSUME(lexer.StringLiteral, { LABEL: 'value' }) }, 61 | { ALT: () => this.CONSUME(lexer.NumberLiteral, { LABEL: 'value' }) }, 62 | { ALT: () => this.SUBRULE(this.array, { LABEL: 'value' }) }, 63 | { ALT: () => this.SUBRULE(this.func, { LABEL: 'value' }) }, 64 | { ALT: () => this.CONSUME(lexer.True, { LABEL: 'value' }) }, 65 | { ALT: () => this.CONSUME(lexer.False, { LABEL: 'value' }) }, 66 | { ALT: () => this.CONSUME(lexer.Null, { LABEL: 'value' }) }, 67 | { ALT: () => this.CONSUME(lexer.Identifier, { LABEL: 'value' }) }, 68 | ]); 69 | }); 70 | 71 | private property = this.RULE('property', () => { 72 | this.CONSUME(lexer.Identifier, { LABEL: 'propertyName' }); 73 | this.CONSUME(lexer.Equals); 74 | this.SUBRULE(this.value, { LABEL: 'propertyValue' }); 75 | }); 76 | 77 | private assignment = this.RULE('assignment', () => { 78 | this.CONSUME(lexer.Identifier, { LABEL: 'assignmentName' }); 79 | this.CONSUME(lexer.Equals); 80 | this.SUBRULE(this.value, { LABEL: 'assignmentValue' }); 81 | }); 82 | 83 | private field = this.RULE('field', () => { 84 | this.CONSUME(lexer.Identifier, { LABEL: 'fieldName' }); 85 | this.SUBRULE(this.value, { LABEL: 'fieldType' }); 86 | this.OPTION1(() => { 87 | this.OR([ 88 | { 89 | ALT: () => { 90 | this.CONSUME(lexer.LSquare, { LABEL: 'array' }); 91 | this.CONSUME(lexer.RSquare, { LABEL: 'array' }); 92 | }, 93 | }, 94 | { ALT: () => this.CONSUME(lexer.QuestionMark, { LABEL: 'optional' }) }, 95 | ]); 96 | }); 97 | this.MANY(() => { 98 | this.SUBRULE(this.fieldAttribute, { LABEL: 'attributeList' }); 99 | }); 100 | this.OPTION2(() => { 101 | this.CONSUME(lexer.Comment, { LABEL: 'comment' }); 102 | }); 103 | }); 104 | 105 | private block = this.RULE( 106 | 'block', 107 | ( 108 | options: { 109 | componentType?: ComponentType; 110 | } = {} 111 | ) => { 112 | const { componentType } = options; 113 | const isEnum = componentType === 'enum'; 114 | const isObject = 115 | componentType === 'model' || 116 | componentType === 'view' || 117 | componentType === 'type'; 118 | 119 | this.CONSUME(lexer.LCurly); 120 | this.CONSUME1(lexer.LineBreak); 121 | this.MANY(() => { 122 | this.OR([ 123 | { ALT: () => this.SUBRULE(this.comment, { LABEL: 'list' }) }, 124 | { 125 | GATE: () => isObject, 126 | ALT: () => this.SUBRULE(this.property, { LABEL: 'list' }), 127 | }, 128 | { ALT: () => this.SUBRULE(this.blockAttribute, { LABEL: 'list' }) }, 129 | { 130 | GATE: () => isObject, 131 | ALT: () => this.SUBRULE(this.field, { LABEL: 'list' }), 132 | }, 133 | { 134 | GATE: () => isEnum, 135 | ALT: () => this.SUBRULE(this.enum, { LABEL: 'list' }), 136 | }, 137 | { 138 | GATE: () => !isObject, 139 | ALT: () => this.SUBRULE(this.assignment, { LABEL: 'list' }), 140 | }, 141 | { ALT: () => this.SUBRULE(this.break, { LABEL: 'list' }) }, 142 | { ALT: () => this.CONSUME2(lexer.LineBreak) }, 143 | ]); 144 | }); 145 | this.CONSUME(lexer.RCurly); 146 | } 147 | ); 148 | 149 | private enum = this.RULE('enum', () => { 150 | this.CONSUME(lexer.Identifier, { LABEL: 'enumName' }); 151 | this.MANY(() => { 152 | this.SUBRULE(this.fieldAttribute, { LABEL: 'attributeList' }); 153 | }); 154 | this.OPTION(() => { 155 | this.CONSUME(lexer.Comment, { LABEL: 'comment' }); 156 | }); 157 | }); 158 | 159 | private fieldAttribute = this.RULE('fieldAttribute', () => { 160 | this.CONSUME(lexer.FieldAttribute, { LABEL: 'fieldAttribute' }); 161 | this.OR([ 162 | { 163 | ALT: () => { 164 | this.CONSUME1(lexer.Identifier, { LABEL: 'groupName' }); 165 | this.CONSUME(lexer.Dot); 166 | this.CONSUME2(lexer.Identifier, { LABEL: 'attributeName' }); 167 | }, 168 | }, 169 | { 170 | ALT: () => this.CONSUME(lexer.Identifier, { LABEL: 'attributeName' }), 171 | }, 172 | ]); 173 | 174 | this.OPTION(() => { 175 | this.CONSUME(lexer.LRound); 176 | this.MANY_SEP({ 177 | SEP: lexer.Comma, 178 | DEF: () => { 179 | this.SUBRULE(this.attributeArg); 180 | }, 181 | }); 182 | this.CONSUME(lexer.RRound); 183 | }); 184 | }); 185 | 186 | private blockAttribute = this.RULE('blockAttribute', () => { 187 | this.CONSUME(lexer.BlockAttribute, { LABEL: 'blockAttribute' }), 188 | this.OR([ 189 | { 190 | ALT: () => { 191 | this.CONSUME1(lexer.Identifier, { LABEL: 'groupName' }); 192 | this.CONSUME(lexer.Dot); 193 | this.CONSUME2(lexer.Identifier, { LABEL: 'attributeName' }); 194 | }, 195 | }, 196 | { 197 | ALT: () => this.CONSUME(lexer.Identifier, { LABEL: 'attributeName' }), 198 | }, 199 | ]); 200 | 201 | this.OPTION(() => { 202 | this.CONSUME(lexer.LRound); 203 | this.MANY_SEP({ 204 | SEP: lexer.Comma, 205 | DEF: () => { 206 | this.SUBRULE(this.attributeArg); 207 | }, 208 | }); 209 | this.CONSUME(lexer.RRound); 210 | }); 211 | }); 212 | 213 | private attributeArg = this.RULE('attributeArg', () => { 214 | this.OR([ 215 | { 216 | ALT: () => this.SUBRULE(this.keyedArg, { LABEL: 'value' }), 217 | }, 218 | { 219 | ALT: () => this.SUBRULE(this.value, { LABEL: 'value' }), 220 | }, 221 | ]); 222 | }); 223 | 224 | private component = this.RULE('component', () => { 225 | const type = this.OR1([ 226 | { ALT: () => this.CONSUME(lexer.Datasource, { LABEL: 'type' }) }, 227 | { ALT: () => this.CONSUME(lexer.Generator, { LABEL: 'type' }) }, 228 | { ALT: () => this.CONSUME(lexer.Model, { LABEL: 'type' }) }, 229 | { ALT: () => this.CONSUME(lexer.View, { LABEL: 'type' }) }, 230 | { ALT: () => this.CONSUME(lexer.Enum, { LABEL: 'type' }) }, 231 | { ALT: () => this.CONSUME(lexer.Type, { LABEL: 'type' }) }, 232 | ]); 233 | this.OR2([ 234 | { 235 | ALT: () => { 236 | this.CONSUME1(lexer.Identifier, { LABEL: 'groupName' }); 237 | this.CONSUME(lexer.Dot); 238 | this.CONSUME2(lexer.Identifier, { LABEL: 'componentName' }); 239 | }, 240 | }, 241 | { 242 | ALT: () => this.CONSUME(lexer.Identifier, { LABEL: 'componentName' }), 243 | }, 244 | ]); 245 | 246 | this.SUBRULE(this.block, { 247 | ARGS: [{ componentType: type.image as ComponentType }], 248 | }); 249 | }); 250 | 251 | private comment = this.RULE('comment', () => { 252 | this.CONSUME(lexer.Comment, { LABEL: 'text' }); 253 | }); 254 | 255 | public schema = this.RULE('schema', () => { 256 | this.MANY(() => { 257 | this.OR([ 258 | { ALT: () => this.SUBRULE(this.comment, { LABEL: 'list' }) }, 259 | { ALT: () => this.SUBRULE(this.component, { LABEL: 'list' }) }, 260 | { ALT: () => this.SUBRULE(this.break, { LABEL: 'list' }) }, 261 | { ALT: () => this.CONSUME(lexer.LineBreak) }, 262 | ]); 263 | }); 264 | }); 265 | } 266 | 267 | export const defaultParser = new PrismaParser(getConfig().parser); 268 | -------------------------------------------------------------------------------- /src/printSchema.ts: -------------------------------------------------------------------------------- 1 | import * as Types from './getSchema'; 2 | import { EOL } from 'os'; 3 | import { schemaSorter } from './schemaSorter'; 4 | 5 | type Block = 'generator' | 'datasource' | 'model' | 'view' | 'enum' | 'type'; 6 | 7 | export interface PrintOptions { 8 | sort?: boolean; 9 | locales?: string | string[]; 10 | sortOrder?: Block[]; 11 | } 12 | 13 | /** 14 | * Converts the given schema object into a string representing the prisma 15 | * schema's source code. Optionally can take options to change the sort order 16 | * of the schema parts. 17 | * */ 18 | export function printSchema( 19 | schema: Types.Schema, 20 | options: PrintOptions = {} 21 | ): string { 22 | const { sort = false, locales = undefined, sortOrder = undefined } = options; 23 | let blocks = schema.list; 24 | if (sort) { 25 | // no point in preserving line breaks when re-sorting 26 | blocks = schema.list = blocks.filter((block) => block.type !== 'break'); 27 | const sorter = schemaSorter(schema, locales, sortOrder); 28 | blocks.sort(sorter); 29 | } 30 | 31 | return ( 32 | blocks 33 | .map(printBlock) 34 | .filter(Boolean) 35 | .join(EOL) 36 | .replace(/(\r?\n\s*){3,}/g, EOL + EOL) + EOL 37 | ); 38 | } 39 | 40 | function printBlock(block: Types.Block): string { 41 | switch (block.type) { 42 | case 'comment': 43 | return printComment(block); 44 | case 'datasource': 45 | return printDatasource(block); 46 | case 'enum': 47 | return printEnum(block); 48 | case 'generator': 49 | return printGenerator(block); 50 | case 'model': 51 | case 'view': 52 | case 'type': 53 | return printObject(block); 54 | case 'break': 55 | return printBreak(); 56 | default: 57 | throw new Error(`Unrecognized block type`); 58 | } 59 | } 60 | 61 | function printComment(comment: Types.Comment) { 62 | return comment.text; 63 | } 64 | 65 | function printBreak() { 66 | return EOL; 67 | } 68 | 69 | function printDatasource(db: Types.Datasource) { 70 | const children = computeAssignmentFormatting(db.assignments); 71 | 72 | return ` 73 | datasource ${db.name} { 74 | ${children} 75 | }`; 76 | } 77 | 78 | function printEnum(enumerator: Types.Enum) { 79 | const list: Array< 80 | | Types.Comment 81 | | Types.Break 82 | | Types.Enumerator 83 | | Types.BlockAttribute 84 | | Types.GroupedBlockAttribute 85 | | Types.GroupedAttribute 86 | > = enumerator.enumerators; 87 | const children = list 88 | .filter(Boolean) 89 | .map(printEnumerator) 90 | .join(`${EOL} `) 91 | .replace(/(\r?\n\s*){3,}/g, `${EOL + EOL} `); 92 | 93 | return ` 94 | enum ${enumerator.name} { 95 | ${children} 96 | }`; 97 | } 98 | 99 | function printEnumerator( 100 | enumerator: 101 | | Types.Enumerator 102 | | Types.Attribute 103 | | Types.Comment 104 | | Types.Break 105 | | Types.BlockAttribute 106 | | Types.GroupedBlockAttribute 107 | | Types.GroupedAttribute 108 | ) { 109 | switch (enumerator.type) { 110 | case 'enumerator': { 111 | const attrs = enumerator.attributes 112 | ? enumerator.attributes.map(printAttribute) 113 | : []; 114 | return [enumerator.name, ...attrs, enumerator.comment] 115 | .filter(Boolean) 116 | .join(' '); 117 | } 118 | case 'attribute': 119 | return printAttribute(enumerator); 120 | case 'comment': 121 | return printComment(enumerator); 122 | case 'break': 123 | return printBreak(); 124 | default: 125 | throw new Error(`Unexpected enumerator type`); 126 | } 127 | } 128 | 129 | function printGenerator(generator: Types.Generator) { 130 | const children = computeAssignmentFormatting(generator.assignments); 131 | 132 | return ` 133 | generator ${generator.name} { 134 | ${children} 135 | }`; 136 | } 137 | 138 | function printObject(object: Types.Object) { 139 | const props = [...object.properties]; 140 | 141 | // If block attributes are declared in the middle of the block, move them to 142 | // the bottom of the list. 143 | let blockAttributeMoved = false; 144 | props.sort((a, b) => { 145 | if ( 146 | a.type === 'attribute' && 147 | a.kind === 'object' && 148 | (b.type !== 'attribute' || 149 | (b.type === 'attribute' && b.kind !== 'object')) 150 | ) { 151 | blockAttributeMoved = true; 152 | return 1; 153 | } 154 | 155 | if ( 156 | b.type === 'attribute' && 157 | b.kind === 'object' && 158 | (a.type !== 'attribute' || 159 | (a.type === 'attribute' && a.kind !== 'object')) 160 | ) { 161 | blockAttributeMoved = true; 162 | return -1; 163 | } 164 | 165 | return 0; 166 | }); 167 | 168 | // Insert a break between the block attributes and the file if the block 169 | // attributes are too close to the model's fields 170 | const attrIndex = props.findIndex( 171 | (item) => item.type === 'attribute' && item.kind === 'object' 172 | ); 173 | 174 | const needsSpace = !['break', 'comment'].includes(props[attrIndex - 1]?.type); 175 | if (blockAttributeMoved && needsSpace) { 176 | props.splice(attrIndex, 0, { type: 'break' }); 177 | } 178 | 179 | const children = computePropertyFormatting(props); 180 | 181 | return ` 182 | ${object.type} ${object.name} { 183 | ${children} 184 | }`; 185 | } 186 | 187 | function printAssignment( 188 | node: Types.Assignment | Types.Comment | Types.Break, 189 | keyLength = 0 190 | ) { 191 | switch (node.type) { 192 | case 'comment': 193 | return printComment(node); 194 | case 'break': 195 | return printBreak(); 196 | case 'assignment': 197 | return `${node.key.padEnd(keyLength)} = ${printValue(node.value)}`; 198 | default: 199 | throw new Error(`Unexpected assignment type`); 200 | } 201 | } 202 | 203 | function printProperty( 204 | node: Types.Property | Types.Comment | Types.Break, 205 | nameLength = 0, 206 | typeLength = 0 207 | ) { 208 | switch (node.type) { 209 | case 'attribute': 210 | return printAttribute(node); 211 | case 'field': 212 | return printField(node, nameLength, typeLength); 213 | case 'comment': 214 | return printComment(node); 215 | case 'break': 216 | return printBreak(); 217 | default: 218 | throw new Error(`Unrecognized property type`); 219 | } 220 | } 221 | 222 | function printAttribute(attribute: Types.Attribute | Types.BlockAttribute) { 223 | const args = 224 | attribute.args && attribute.args.length > 0 225 | ? `(${attribute.args.map(printAttributeArg).filter(Boolean).join(', ')})` 226 | : ''; 227 | 228 | const name = [attribute.name]; 229 | if (attribute.group) name.unshift(attribute.group); 230 | 231 | return `${attribute.kind === 'field' ? '@' : '@@'}${name.join('.')}${args}`; 232 | } 233 | 234 | function printAttributeArg(arg: Types.AttributeArgument) { 235 | return printValue(arg.value); 236 | } 237 | 238 | function printField(field: Types.Field, nameLength = 0, typeLength = 0) { 239 | const name = field.name.padEnd(nameLength); 240 | const fieldType = printFieldType(field).padEnd(typeLength); 241 | const attrs = field.attributes ? field.attributes.map(printAttribute) : []; 242 | const comment = field.comment; 243 | return ( 244 | [name, fieldType, ...attrs] 245 | .filter(Boolean) 246 | .join(' ') 247 | // comments ignore indents 248 | .trim() + (comment ? ` ${comment}` : '') 249 | ); 250 | } 251 | 252 | function printFieldType(field: Types.Field) { 253 | const suffix = field.array ? '[]' : field.optional ? '?' : ''; 254 | 255 | if (typeof field.fieldType === 'object') { 256 | switch (field.fieldType.type) { 257 | case 'function': { 258 | return `${printFunction(field.fieldType)}${suffix}`; 259 | } 260 | default: 261 | throw new Error(`Unexpected field type`); 262 | } 263 | } 264 | 265 | return `${field.fieldType}${suffix}`; 266 | } 267 | 268 | function printFunction(func: Types.Func) { 269 | const params = func.params ? func.params.map(printValue) : ''; 270 | return `${func.name}(${params})`; 271 | } 272 | 273 | function printValue(value: Types.KeyValue | Types.Value): string { 274 | switch (typeof value) { 275 | case 'object': { 276 | if ('type' in value) { 277 | switch (value.type) { 278 | case 'keyValue': 279 | return `${value.key}: ${printValue(value.value)}`; 280 | case 'function': 281 | return printFunction(value); 282 | case 'array': 283 | return `[${ 284 | value.args != null ? value.args.map(printValue).join(', ') : '' 285 | }]`; 286 | default: 287 | throw new Error(`Unexpected value type`); 288 | } 289 | } 290 | 291 | throw new Error(`Unexpected object value`); 292 | } 293 | default: 294 | return String(value); 295 | } 296 | } 297 | 298 | function computeAssignmentFormatting( 299 | list: Array 300 | ) { 301 | let pos = 0; 302 | const listBlocks = list.reduce>( 303 | (memo, current, index, arr) => { 304 | if (current.type === 'break') return memo; 305 | if (index > 0 && arr[index - 1].type === 'break') memo[++pos] = []; 306 | memo[pos].push(current); 307 | return memo; 308 | }, 309 | [[]] 310 | ); 311 | 312 | const keyLengths = listBlocks.map((lists) => 313 | lists.reduce( 314 | (max, current) => 315 | Math.max( 316 | max, 317 | // perhaps someone more typescript-savy than I am can fix this 318 | current.type === 'assignment' ? current.key.length : 0 319 | ), 320 | 0 321 | ) 322 | ); 323 | 324 | return list 325 | .map((item, index, arr) => { 326 | if (index > 0 && item.type !== 'break' && arr[index - 1].type === 'break') 327 | keyLengths.shift(); 328 | return printAssignment(item, keyLengths[0]); 329 | }) 330 | .filter(Boolean) 331 | .join(`${EOL} `) 332 | .replace(/(\r?\n\s*){3,}/g, `${EOL + EOL} `); 333 | } 334 | 335 | function computePropertyFormatting( 336 | list: Array 337 | ) { 338 | let pos = 0; 339 | const listBlocks = list.reduce>( 340 | (memo, current, index, arr) => { 341 | if (current.type === 'break') return memo; 342 | if (index > 0 && arr[index - 1].type === 'break') memo[++pos] = []; 343 | memo[pos].push(current); 344 | return memo; 345 | }, 346 | [[]] 347 | ); 348 | 349 | const nameLengths = listBlocks.map((lists) => 350 | lists.reduce( 351 | (max, current) => 352 | Math.max( 353 | max, 354 | // perhaps someone more typescript-savy than I am can fix this 355 | current.type === 'field' ? current.name.length : 0 356 | ), 357 | 0 358 | ) 359 | ); 360 | 361 | const typeLengths = listBlocks.map((lists) => 362 | lists.reduce( 363 | (max, current) => 364 | Math.max( 365 | max, 366 | // perhaps someone more typescript-savy than I am can fix this 367 | current.type === 'field' ? printFieldType(current).length : 0 368 | ), 369 | 0 370 | ) 371 | ); 372 | 373 | return list 374 | .map((prop, index, arr) => { 375 | if ( 376 | index > 0 && 377 | prop.type !== 'break' && 378 | arr[index - 1].type === 'break' 379 | ) { 380 | nameLengths.shift(); 381 | typeLengths.shift(); 382 | } 383 | 384 | return printProperty(prop, nameLengths[0], typeLengths[0]); 385 | }) 386 | .filter(Boolean) 387 | .join(`${EOL} `) 388 | .replace(/(\r?\n\s*){3,}/g, `${EOL + EOL} `); 389 | } 390 | -------------------------------------------------------------------------------- /src/produceSchema.ts: -------------------------------------------------------------------------------- 1 | import { PrintOptions } from './printSchema'; 2 | import { createPrismaSchemaBuilder } from './PrismaSchemaBuilder'; 3 | 4 | type Options = PrintOptions; 5 | 6 | /** 7 | * Receives a prisma schema in the form of a string containing source code, and 8 | * a callback builder function. Use the builder to modify your schema as 9 | * desired. Returns the schema as a string with the modifications applied. 10 | * */ 11 | export function produceSchema( 12 | source: string, 13 | producer: (builder: ReturnType) => void, 14 | options: Options = {} 15 | ): string { 16 | const builder = createPrismaSchemaBuilder(source); 17 | producer(builder); 18 | return builder.print(options); 19 | } 20 | -------------------------------------------------------------------------------- /src/schemaSorter.ts: -------------------------------------------------------------------------------- 1 | import { Block, Schema } from './getSchema'; 2 | 3 | const unsorted = ['break', 'comment']; 4 | const defaultSortOrder = [ 5 | 'generator', 6 | 'datasource', 7 | 'model', 8 | 'view', 9 | 'enum', 10 | 'break', 11 | 'comment', 12 | ]; 13 | 14 | /** Sorts the schema parts, in the given order, and alphabetically for parts of the same type. */ 15 | export const schemaSorter = 16 | ( 17 | schema: Schema, 18 | locales?: string | string[], 19 | sortOrder: string[] = defaultSortOrder 20 | ) => 21 | (a: Block, b: Block): number => { 22 | // Preserve the position of comments and line breaks relative to their 23 | // position in the file, since when a re-sort happens it wouldn't be 24 | // clear whether a comment should affix to the object above or below it. 25 | const aUnsorted = unsorted.indexOf(a.type) !== -1; 26 | const bUnsorted = unsorted.indexOf(b.type) !== -1; 27 | 28 | if (aUnsorted !== bUnsorted) { 29 | return schema.list.indexOf(a) - schema.list.indexOf(b); 30 | } 31 | 32 | if (sortOrder !== defaultSortOrder) 33 | sortOrder = sortOrder.concat(defaultSortOrder); 34 | const typeIndex = sortOrder.indexOf(a.type) - sortOrder.indexOf(b.type); 35 | if (typeIndex !== 0) return typeIndex; 36 | 37 | // Resolve ties using the name of object's name. 38 | if ('name' in a && 'name' in b) 39 | return a.name.localeCompare(b.name, locales); 40 | 41 | // If all else fails, leave objects in their original position. 42 | return 0; 43 | }; 44 | -------------------------------------------------------------------------------- /src/schemaUtils.ts: -------------------------------------------------------------------------------- 1 | import type { CstNode, IToken } from 'chevrotain'; 2 | import * as schema from './getSchema'; 3 | 4 | const schemaObjects = ['model', 'view', 'type'] as const; 5 | 6 | export function isOneOfSchemaObjects( 7 | obj: schema.Object, 8 | schemas: readonly T[] 9 | ): obj is Extract { 10 | return obj != null && 'type' in obj && schemas.includes(obj.type as T); 11 | } 12 | 13 | /** Returns true if the value is an Object, such as a model or view or composite type. */ 14 | export function isSchemaObject( 15 | obj: schema.Object 16 | ): obj is Extract { 17 | return isOneOfSchemaObjects(obj, schemaObjects); 18 | } 19 | 20 | const fieldObjects = ['field', 'enumerator'] as const; 21 | /** Returns true if the value is a Field or Enumerator. */ 22 | export function isSchemaField( 23 | field: schema.Field | schema.Enumerator 24 | ): field is Extract { 25 | return field != null && 'type' in field && fieldObjects.includes(field.type); 26 | } 27 | 28 | /** Returns true if the value of the CstNode is a Token. */ 29 | export function isToken(node: [IToken] | [CstNode]): node is [IToken] { 30 | return 'image' in node[0]; 31 | } 32 | 33 | /** 34 | * If parser.nodeLocationTracking is set, then read the location statistics 35 | * from the available tokens. If tracking is 'none' then just return the 36 | * existing data structure. 37 | * */ 38 | export function appendLocationData>( 39 | data: T, 40 | ...tokens: IToken[] 41 | ): T { 42 | const location = tokens.reduce((memo, token) => { 43 | if (!token) return memo; 44 | 45 | const { 46 | endColumn = -Infinity, 47 | endLine = -Infinity, 48 | endOffset = -Infinity, 49 | startColumn = Infinity, 50 | startLine = Infinity, 51 | startOffset = Infinity, 52 | } = memo; 53 | 54 | if (token.startLine != null && token.startLine < startLine) 55 | memo.startLine = token.startLine; 56 | if (token.startColumn != null && token.startColumn < startColumn) 57 | memo.startColumn = token.startColumn; 58 | if (token.startOffset != null && token.startOffset < startOffset) 59 | memo.startOffset = token.startOffset; 60 | 61 | if (token.endLine != null && token.endLine > endLine) 62 | memo.endLine = token.endLine; 63 | if (token.endColumn != null && token.endColumn > endColumn) 64 | memo.endColumn = token.endColumn; 65 | if (token.endOffset != null && token.endOffset > endOffset) 66 | memo.endOffset = token.endOffset; 67 | 68 | return memo; 69 | }, {} as IToken); 70 | 71 | return Object.assign(data, { location }); 72 | } 73 | -------------------------------------------------------------------------------- /src/visitor.ts: -------------------------------------------------------------------------------- 1 | import { CstNode, IToken } from '@chevrotain/types'; 2 | import * as Types from './getSchema'; 3 | 4 | import { appendLocationData, isToken } from './schemaUtils'; 5 | import { PrismaParser, defaultParser } from './parser'; 6 | import { ICstVisitor } from 'chevrotain'; 7 | 8 | /* eslint-disable @typescript-eslint/no-explicit-any */ 9 | type Class = new (...args: any[]) => T; 10 | export type PrismaVisitor = ICstVisitor; 11 | /* eslint-enable @typescript-eslint/no-explicit-any */ 12 | 13 | export const VisitorClassFactory = ( 14 | parser: PrismaParser 15 | ): Class => { 16 | const BasePrismaVisitor = parser.getBaseCstVisitorConstructorWithDefaults(); 17 | return class PrismaVisitor extends BasePrismaVisitor { 18 | constructor() { 19 | super(); 20 | this.validateVisitor(); 21 | } 22 | 23 | schema(ctx: CstNode & { list: CstNode[] }): Types.Schema { 24 | const list = ctx.list?.map((item) => this.visit([item])) || []; 25 | return { type: 'schema', list }; 26 | } 27 | 28 | component( 29 | ctx: CstNode & { 30 | type: [IToken]; 31 | componentName: [IToken]; 32 | block: [CstNode]; 33 | } 34 | ): Types.Block { 35 | const [type] = ctx.type; 36 | const [name] = ctx.componentName; 37 | const list = this.visit(ctx.block); 38 | 39 | const data = (() => { 40 | switch (type.image) { 41 | case 'datasource': 42 | return { 43 | type: 'datasource', 44 | name: name.image, 45 | assignments: list, 46 | } as const satisfies Types.Datasource; 47 | case 'generator': 48 | return { 49 | type: 'generator', 50 | name: name.image, 51 | assignments: list, 52 | } as const satisfies Types.Generator; 53 | case 'model': 54 | return { 55 | type: 'model', 56 | name: name.image, 57 | properties: list, 58 | } as const satisfies Types.Model; 59 | case 'view': 60 | return { 61 | type: 'view', 62 | name: name.image, 63 | properties: list, 64 | } as const satisfies Types.View; 65 | case 'enum': 66 | return { 67 | type: 'enum', 68 | name: name.image, 69 | enumerators: list, 70 | } as const satisfies Types.Enum; 71 | case 'type': 72 | return { 73 | type: 'type', 74 | name: name.image, 75 | properties: list, 76 | } as const satisfies Types.Type; 77 | default: 78 | throw new Error(`Unexpected block type: ${type}`); 79 | } 80 | })(); 81 | 82 | return this.maybeAppendLocationData(data, type, name); 83 | } 84 | 85 | break(): Types.Break { 86 | return { type: 'break' }; 87 | } 88 | 89 | comment(ctx: CstNode & { text: [IToken] }): Types.Comment { 90 | const [comment] = ctx.text; 91 | const data = { 92 | type: 'comment', 93 | text: comment.image, 94 | } as const satisfies Types.Comment; 95 | return this.maybeAppendLocationData(data, comment); 96 | } 97 | 98 | block(ctx: CstNode & { list: CstNode[] }): BlockList { 99 | return ctx.list?.map((item) => this.visit([item])); 100 | } 101 | 102 | assignment( 103 | ctx: CstNode & { assignmentName: [IToken]; assignmentValue: [CstNode] } 104 | ): Types.Assignment { 105 | const value = this.visit(ctx.assignmentValue); 106 | const [key] = ctx.assignmentName; 107 | const data = { 108 | type: 'assignment', 109 | key: key.image, 110 | value, 111 | } as const satisfies Types.Assignment; 112 | return this.maybeAppendLocationData(data, key); 113 | } 114 | 115 | field( 116 | ctx: CstNode & { 117 | fieldName: [IToken]; 118 | fieldType: [CstNode]; 119 | array: [IToken]; 120 | optional: [IToken]; 121 | attributeList: CstNode[]; 122 | comment: [IToken]; 123 | } 124 | ): Types.Field { 125 | const fieldType = this.visit(ctx.fieldType); 126 | const [name] = ctx.fieldName; 127 | const attributes = ctx.attributeList?.map((item) => this.visit([item])); 128 | const comment = ctx.comment?.[0]?.image; 129 | const data = { 130 | type: 'field', 131 | name: name.image, 132 | fieldType, 133 | array: ctx.array != null, 134 | optional: ctx.optional != null, 135 | attributes, 136 | comment, 137 | } as const satisfies Types.Field; 138 | 139 | return this.maybeAppendLocationData( 140 | data, 141 | name, 142 | ctx.optional?.[0], 143 | ctx.array?.[0] 144 | ); 145 | } 146 | 147 | fieldAttribute( 148 | ctx: CstNode & { 149 | fieldAttribute: [IToken]; 150 | groupName: [IToken]; 151 | attributeName: [IToken]; 152 | attributeArg: CstNode[]; 153 | } 154 | ): Types.Attr { 155 | const [name] = ctx.attributeName; 156 | const [group] = ctx.groupName || [{}]; 157 | const args = ctx.attributeArg?.map((attr) => this.visit(attr)); 158 | const data = { 159 | type: 'attribute', 160 | name: name.image, 161 | kind: 'field', 162 | group: group.image, 163 | args, 164 | } as const satisfies Types.Attr; 165 | return this.maybeAppendLocationData( 166 | data, 167 | name, 168 | ...ctx.fieldAttribute, 169 | group 170 | ); 171 | } 172 | 173 | blockAttribute( 174 | ctx: CstNode & { 175 | blockAttribute: [IToken]; 176 | groupName: [IToken]; 177 | attributeName: [IToken]; 178 | attributeArg: CstNode[]; 179 | } 180 | ): Types.Attr | null { 181 | const [name] = ctx.attributeName; 182 | const [group] = ctx.groupName || [{}]; 183 | const args = ctx.attributeArg?.map((attr) => this.visit(attr)); 184 | const data = { 185 | type: 'attribute', 186 | name: name.image, 187 | kind: 'object', 188 | group: group.image, 189 | args, 190 | } as const satisfies Types.Attr; 191 | 192 | return this.maybeAppendLocationData( 193 | data, 194 | name, 195 | ...ctx.blockAttribute, 196 | group 197 | ); 198 | } 199 | 200 | attributeArg(ctx: CstNode & { value: [CstNode] }): Types.AttributeArgument { 201 | const value = this.visit(ctx.value); 202 | return { type: 'attributeArgument', value }; 203 | } 204 | 205 | func( 206 | ctx: CstNode & { 207 | funcName: [IToken]; 208 | value: CstNode[]; 209 | keyedArg: CstNode[]; 210 | } 211 | ): Types.Func { 212 | const [name] = ctx.funcName; 213 | const params = ctx.value?.map((item) => this.visit([item])); 214 | const keyedParams = ctx.keyedArg?.map((item) => this.visit([item])); 215 | const pars = (params || keyedParams) && [ 216 | ...(params ?? []), 217 | ...(keyedParams ?? []), 218 | ]; 219 | const data = { 220 | type: 'function', 221 | name: name.image, 222 | params: pars, 223 | } as const satisfies Types.Func; 224 | return this.maybeAppendLocationData(data, name); 225 | } 226 | 227 | array(ctx: CstNode & { value: CstNode[] }): Types.RelationArray { 228 | const args = ctx.value?.map((item) => this.visit([item])); 229 | return { type: 'array', args }; 230 | } 231 | 232 | keyedArg( 233 | ctx: CstNode & { keyName: [IToken]; value: [CstNode] } 234 | ): Types.KeyValue { 235 | const [key] = ctx.keyName; 236 | const value = this.visit(ctx.value); 237 | const data = { 238 | type: 'keyValue', 239 | key: key.image, 240 | value, 241 | } as const satisfies Types.KeyValue; 242 | return this.maybeAppendLocationData(data, key); 243 | } 244 | 245 | value(ctx: CstNode & { value: [IToken] | [CstNode] }): Types.Value { 246 | if (isToken(ctx.value)) { 247 | const [{ image }] = ctx.value; 248 | return image; 249 | } 250 | return this.visit(ctx.value); 251 | } 252 | 253 | enum( 254 | ctx: CstNode & { 255 | enumName: [IToken]; 256 | attributeList: CstNode[]; 257 | comment: [IToken]; 258 | } 259 | ): Types.Enumerator { 260 | const [name] = ctx.enumName; 261 | const attributes = ctx.attributeList?.map((item) => this.visit([item])); 262 | const comment = ctx.comment?.[0]?.image; 263 | const data = { 264 | type: 'enumerator', 265 | name: name.image, 266 | attributes, 267 | comment, 268 | } as const satisfies Types.Enumerator; 269 | return this.maybeAppendLocationData(data, name); 270 | } 271 | 272 | maybeAppendLocationData>( 273 | data: T, 274 | ...tokens: IToken[] 275 | ): T { 276 | if (parser.config.nodeLocationTracking === 'none') return data; 277 | return appendLocationData(data, ...tokens); 278 | } 279 | }; 280 | }; 281 | 282 | type BlockList = Array< 283 | | Types.Comment 284 | | Types.Property 285 | | Types.Attribute 286 | | Types.Field 287 | | Types.Enum 288 | | Types.Assignment 289 | | Types.Break 290 | >; 291 | export const DefaultVisitorClass = VisitorClassFactory(defaultParser); 292 | export const defaultVisitor = new DefaultVisitorClass(); 293 | -------------------------------------------------------------------------------- /test/PrismaSchemaBuilder.test.ts: -------------------------------------------------------------------------------- 1 | import { createPrismaSchemaBuilder } from '../src/PrismaSchemaBuilder'; 2 | import * as schema from '../src/getSchema'; 3 | import { loadFixture } from './utils'; 4 | 5 | describe('PrismaSchemaBuilder', () => { 6 | it('adds a generator', () => { 7 | const builder = createPrismaSchemaBuilder(); 8 | builder 9 | .generator('client', 'prisma-client-js') 10 | .assignment('output', 'client.js'); 11 | expect(builder.print()).toMatchInlineSnapshot(` 12 | " 13 | generator client { 14 | provider = "prisma-client-js" 15 | output = "client.js" 16 | } 17 | " 18 | `); 19 | }); 20 | 21 | it('updates an existing generator', () => { 22 | const builder = createPrismaSchemaBuilder(); 23 | builder 24 | .generator('client', 'prisma-client-js') 25 | .assignment('output', 'client.js'); 26 | builder.generator('client').assignment('engineType', 'library'); 27 | expect(builder.print()).toMatchInlineSnapshot(` 28 | " 29 | generator client { 30 | provider = "prisma-client-js" 31 | output = "client.js" 32 | engineType = "library" 33 | } 34 | " 35 | `); 36 | }); 37 | 38 | it('accesses a generator', () => { 39 | const builder = createPrismaSchemaBuilder(); 40 | builder.generator('client').then((generator) => { 41 | const assignment = generator.assignments[0] as schema.Assignment; 42 | assignment.value = '"prisma-client-ts"'; 43 | }); 44 | expect(builder.print()).toMatchInlineSnapshot(` 45 | " 46 | generator client { 47 | provider = "prisma-client-ts" 48 | } 49 | " 50 | `); 51 | }); 52 | 53 | it('sets the datasource', () => { 54 | const builder = createPrismaSchemaBuilder(); 55 | builder.datasource('postgresql', { env: 'DATABASE_URL' }); 56 | expect(builder.print()).toMatchInlineSnapshot(` 57 | " 58 | datasource db { 59 | url = env("DATABASE_URL") 60 | provider = postgresql 61 | } 62 | " 63 | `); 64 | }); 65 | 66 | it('accesses the datasource', () => { 67 | const builder = createPrismaSchemaBuilder(); 68 | builder 69 | .datasource('postgresql', { env: 'DATABASE_URL' }) 70 | .then((datasource) => { 71 | datasource.name = 'my-database'; 72 | }); 73 | expect(builder.print()).toMatchInlineSnapshot(` 74 | " 75 | datasource my-database { 76 | url = env("DATABASE_URL") 77 | provider = postgresql 78 | } 79 | " 80 | `); 81 | }); 82 | 83 | it('can reset datasource url with assignments', () => { 84 | // not really sure why you'd do this, but *shrug* 85 | const builder = createPrismaSchemaBuilder(); 86 | builder 87 | .datasource('postgresql', { env: 'DATABASE_URL' }) 88 | .assignment('url', 'https://database.com'); 89 | expect(builder.print()).toMatchInlineSnapshot(` 90 | " 91 | datasource db { 92 | url = "https://database.com" 93 | provider = postgresql 94 | } 95 | " 96 | `); 97 | }); 98 | 99 | it('adds a model', () => { 100 | const builder = createPrismaSchemaBuilder(); 101 | builder.model('Project').field('name', 'String'); 102 | expect(builder.print()).toMatchInlineSnapshot(` 103 | " 104 | model Project { 105 | name String 106 | } 107 | " 108 | `); 109 | }); 110 | 111 | it('removes a model', () => { 112 | const builder = createPrismaSchemaBuilder(` 113 | datasource db { 114 | url = env("DATABASE_URL") 115 | } 116 | 117 | model Project { 118 | name String 119 | } 120 | `); 121 | builder.drop('Project'); 122 | expect(builder.print()).toMatchInlineSnapshot(` 123 | " 124 | datasource db { 125 | url = env("DATABASE_URL") 126 | } 127 | 128 | 129 | " 130 | `); 131 | }); 132 | 133 | it("removes nothing if the object being dropped doesn't exist", () => { 134 | const builder = createPrismaSchemaBuilder(` 135 | datasource db { 136 | url = env("DATABASE_URL") 137 | } 138 | 139 | model Project { 140 | name String 141 | } 142 | `); 143 | builder.drop('TheBeat'); 144 | expect(builder.print()).toMatchInlineSnapshot(` 145 | " 146 | datasource db { 147 | url = env("DATABASE_URL") 148 | } 149 | 150 | model Project { 151 | name String 152 | } 153 | " 154 | `); 155 | }); 156 | 157 | it('accesses a model', () => { 158 | const builder = createPrismaSchemaBuilder(` 159 | model Project { 160 | name String 161 | } 162 | `); 163 | builder.model('Project').then((project) => { 164 | project.name = 'Task'; 165 | }); 166 | expect(builder.print()).toMatchInlineSnapshot(` 167 | " 168 | model Task { 169 | name String 170 | } 171 | " 172 | `); 173 | }); 174 | 175 | it('renames a model attribute', () => { 176 | const builder = createPrismaSchemaBuilder(` 177 | model Project { 178 | name String 179 | 180 | @@id([name]) 181 | } 182 | `); 183 | builder.model('Project').then((project) => { 184 | for (const prop of project.properties) { 185 | if (prop.type === 'attribute' && prop.name === 'id') { 186 | prop.name = 'unique'; 187 | return; 188 | } 189 | } 190 | }); 191 | expect(builder.print()).toMatchInlineSnapshot(` 192 | " 193 | model Project { 194 | name String 195 | 196 | @@unique([name]) 197 | } 198 | " 199 | `); 200 | }); 201 | 202 | it('removes an enum', () => { 203 | const builder = createPrismaSchemaBuilder(` 204 | datasource db { 205 | url = env("DATABASE_URL") 206 | } 207 | 208 | enum Role { 209 | MEMBER 210 | ADMIN 211 | } 212 | `); 213 | 214 | builder.drop('Role'); 215 | 216 | expect(builder.print()).toMatchInlineSnapshot(` 217 | " 218 | datasource db { 219 | url = env("DATABASE_URL") 220 | } 221 | 222 | 223 | " 224 | `); 225 | }); 226 | 227 | it('adds an enum', () => { 228 | const builder = createPrismaSchemaBuilder(); 229 | builder 230 | .enum('Role', ['USER', 'ADMIN']) 231 | .break() 232 | .comment('test') 233 | .enumerator('OWNER'); 234 | 235 | expect(builder.print()).toMatchInlineSnapshot(` 236 | " 237 | enum Role { 238 | USER 239 | ADMIN 240 | 241 | // test 242 | OWNER 243 | } 244 | " 245 | `); 246 | }); 247 | 248 | it('updates an existing enum', () => { 249 | const builder = createPrismaSchemaBuilder(); 250 | builder.enum('Role', ['USER', 'ADMIN']); 251 | builder.enum('Role').enumerator('OWNER'); 252 | 253 | expect(builder.print()).toMatchInlineSnapshot(` 254 | " 255 | enum Role { 256 | USER 257 | ADMIN 258 | OWNER 259 | } 260 | " 261 | `); 262 | }); 263 | 264 | it('accesses an enum', () => { 265 | const builder = createPrismaSchemaBuilder(); 266 | builder.enum('Role', ['USER', 'ADMIN']).then((e) => { 267 | e.name = 'UserType'; 268 | }); 269 | 270 | expect(builder.print()).toMatchInlineSnapshot(` 271 | " 272 | enum UserType { 273 | USER 274 | ADMIN 275 | } 276 | " 277 | `); 278 | }); 279 | 280 | it('adds a field to an existing model', () => { 281 | const builder = createPrismaSchemaBuilder(` 282 | model Project { 283 | name String 284 | } 285 | `); 286 | builder.model('Project').field('description', 'String'); 287 | builder.model('Project').field('owner', 'String'); 288 | 289 | expect(builder.print()).toMatchInlineSnapshot(` 290 | " 291 | model Project { 292 | name String 293 | description String 294 | owner String 295 | } 296 | " 297 | `); 298 | }); 299 | 300 | it('updates an existing field', () => { 301 | const builder = createPrismaSchemaBuilder(` 302 | model TaskScript { 303 | name String 304 | createdAt DateTime 305 | updatedAt DateTime 306 | } 307 | `); 308 | builder 309 | .model('TaskScript') 310 | .field('createdAt', 'DateTime') 311 | .attribute('default', [{ name: 'now' }]); 312 | builder 313 | .model('TaskScript') 314 | .field('updatedAt', 'DateTime') 315 | .attribute('updatedAt'); 316 | expect(builder.print()).toMatchInlineSnapshot(` 317 | " 318 | model TaskScript { 319 | name String 320 | createdAt DateTime @default(now()) 321 | updatedAt DateTime @updatedAt 322 | } 323 | " 324 | `); 325 | }); 326 | 327 | it('removes a field', () => { 328 | const builder = createPrismaSchemaBuilder(` 329 | model TaskScript { 330 | name String 331 | createdAt DateTime 332 | updatedAt DateTime 333 | } 334 | `); 335 | builder 336 | .model('TaskScript') 337 | .removeField('createdAt') 338 | .removeField('updatedAt'); 339 | expect(builder.print()).toMatchInlineSnapshot(` 340 | " 341 | model TaskScript { 342 | name String 343 | } 344 | " 345 | `); 346 | }); 347 | 348 | it('adds an attribute', () => { 349 | const builder = createPrismaSchemaBuilder(); 350 | builder 351 | .model('TaskMessage') 352 | .field('createdAt', 'DateTime?') 353 | .attribute('db.Timestamptz', ['6']); 354 | expect(builder.print()).toMatchInlineSnapshot(` 355 | " 356 | model TaskMessage { 357 | createdAt DateTime? @db.Timestamptz(6) 358 | } 359 | " 360 | `); 361 | }); 362 | 363 | it('replaces an attribute', () => { 364 | const builder = createPrismaSchemaBuilder(); 365 | builder 366 | .model('TaskMessage') 367 | .field('createdAt', 'DateTime?') 368 | .attribute('db.Timestamptz', ['6']); 369 | 370 | // Replace the @db.Timestamptz(6) attribute by dropping and re-creating the field 371 | builder 372 | .model('TaskMessage') 373 | .field('createdAt', 'DateTime?') 374 | .removeAttribute('db.Timestamptz') 375 | .attribute('default', [{ name: 'now' }]); 376 | expect(builder.print()).toMatchInlineSnapshot(` 377 | " 378 | model TaskMessage { 379 | createdAt DateTime? @default(now()) 380 | } 381 | " 382 | `); 383 | }); 384 | 385 | it('replaces an attribute with access', () => { 386 | // Set up the schema with a model and a field with an attribute 387 | const builder = createPrismaSchemaBuilder(); 388 | builder 389 | .model('TaskMessage') 390 | .field('createdAt', 'DateTime?') 391 | .attribute('db.Timestamptz', ['6']); 392 | 393 | // Replace the @db.Timestamptz(6) attribute with @default(now()) 394 | builder 395 | .model('TaskMessage') 396 | .field('createdAt') 397 | .then((field) => { 398 | const attribute: schema.Attribute = { 399 | kind: 'field', 400 | name: 'default', 401 | type: 'attribute', 402 | args: [{ type: 'attributeArgument', value: 'now()' }], 403 | }; 404 | field.attributes = [attribute]; 405 | }); 406 | expect(builder.print()).toMatchInlineSnapshot(` 407 | " 408 | model TaskMessage { 409 | createdAt DateTime? @default(now()) 410 | } 411 | " 412 | `); 413 | }); 414 | 415 | it('adds a field relation', () => { 416 | const builder = createPrismaSchemaBuilder(); 417 | builder 418 | .model('Project') 419 | .field('user', 'User') 420 | .attribute('relation', { fields: ['clientId'], references: ['id'] }); 421 | expect(builder.print()).toMatchInlineSnapshot(` 422 | " 423 | model Project { 424 | user User @relation(fields: [clientId], references: [id]) 425 | } 426 | " 427 | `); 428 | }); 429 | 430 | it('adds a map attribute', () => { 431 | const builder = createPrismaSchemaBuilder(); 432 | builder.model('Project').blockAttribute('map', 'projects'); 433 | expect(builder.print()).toMatchInlineSnapshot(` 434 | " 435 | model Project { 436 | @@map("projects") 437 | } 438 | " 439 | `); 440 | }); 441 | 442 | it('adds an id attribute', () => { 443 | const builder = createPrismaSchemaBuilder(); 444 | builder 445 | .model('User') 446 | .field('firstName', 'String') 447 | .field('lastName', 'String') 448 | .blockAttribute('id', ['firstName', 'lastName']); 449 | expect(builder.print()).toMatchInlineSnapshot(` 450 | " 451 | model User { 452 | firstName String 453 | lastName String 454 | 455 | @@id([firstName, lastName]) 456 | } 457 | " 458 | `); 459 | }); 460 | 461 | it('adds a unique attribute', () => { 462 | const builder = createPrismaSchemaBuilder(); 463 | builder 464 | .model('Project') 465 | .field('code', 'String') 466 | .field('client', 'String') 467 | .blockAttribute('unique', ['code', 'client']); 468 | expect(builder.print()).toMatchInlineSnapshot(` 469 | " 470 | model Project { 471 | code String 472 | client String 473 | 474 | @@unique([code, client]) 475 | } 476 | " 477 | `); 478 | }); 479 | 480 | it('adds a comment', () => { 481 | const builder = createPrismaSchemaBuilder(); 482 | builder 483 | .model('User') 484 | .comment('this is the part of the name you say first', true) 485 | .field('firstName', 'String') 486 | .comment('this is the part of the name you say last', true) 487 | .field('lastName', 'String') 488 | .break() 489 | .comment('this is a random comment') 490 | .break(); 491 | 492 | expect(builder.print()).toMatchInlineSnapshot(` 493 | " 494 | model User { 495 | /// this is the part of the name you say first 496 | firstName String 497 | /// this is the part of the name you say last 498 | lastName String 499 | 500 | // this is a random comment 501 | 502 | } 503 | " 504 | `); 505 | }); 506 | 507 | it('adds a schema comment', () => { 508 | const builder = createPrismaSchemaBuilder(); 509 | builder 510 | .model('Project') 511 | .schemaComment('this is a comment') 512 | .schemaComment('this is a node comment', true) 513 | .model('Node'); 514 | expect(builder.print()).toMatchInlineSnapshot(` 515 | " 516 | model Project { 517 | 518 | } 519 | // this is a comment 520 | /// this is a node comment 521 | 522 | model Node { 523 | 524 | } 525 | " 526 | `); 527 | }); 528 | 529 | it('can reference the same attribute', () => { 530 | const builder = createPrismaSchemaBuilder(` 531 | model Test { 532 | id String @id @default(auto()) @map(\\"_id\\") @db.ObjectId 533 | } 534 | `); 535 | const result = builder 536 | .model('Test') 537 | .field('id', 'String') 538 | .attribute('id') 539 | .attribute('default', [{ name: 'auto' }]) 540 | .attribute('map', [`"_id"`]) 541 | .attribute('db.ObjectId') 542 | .print(); 543 | expect(result).toMatchInlineSnapshot(` 544 | " 545 | model Test { 546 | id String @id @default(auto()) @map("_id") @db.ObjectId 547 | } 548 | " 549 | `); 550 | }); 551 | 552 | it('can create a view', () => { 553 | const builder = createPrismaSchemaBuilder(` 554 | model Project { 555 | name String 556 | } 557 | `); 558 | const result = builder 559 | .view('TestView') 560 | .field('id', 'String') 561 | .attribute('id') 562 | .attribute('default', [{ name: 'auto' }]) 563 | .attribute('map', [`"_id"`]) 564 | .attribute('db.ObjectId') 565 | .print(); 566 | expect(result).toMatchInlineSnapshot(` 567 | " 568 | model Project { 569 | name String 570 | } 571 | 572 | view TestView { 573 | id String @id @default(auto()) @map("_id") @db.ObjectId 574 | } 575 | " 576 | `); 577 | }); 578 | 579 | it('edits an existing view', () => { 580 | const builder = createPrismaSchemaBuilder(` 581 | view TestView { 582 | id String 583 | } 584 | `); 585 | const result = builder.view('TestView').field('name', 'String').print(); 586 | expect(result).toMatchInlineSnapshot(` 587 | " 588 | view TestView { 589 | id String 590 | name String 591 | } 592 | " 593 | `); 594 | }); 595 | 596 | it('adds a composite type', async () => { 597 | const builder = createPrismaSchemaBuilder(` 598 | datasource db { 599 | provider = "mongodb" 600 | url = env("DATABASE_URL") 601 | } 602 | 603 | model Product { 604 | id String @id @default(auto()) @map("_id") @db.ObjectId 605 | name String 606 | photos Photo[] 607 | } 608 | `); 609 | 610 | const result = builder 611 | .type('Photo') 612 | .field('height', 'Int') 613 | .attribute('default', ['0']) 614 | .field('width', 'Int') 615 | .attribute('default', ['0']) 616 | .field('url', 'String') 617 | .print(); 618 | 619 | expect(result).toMatchSnapshot(); 620 | }); 621 | 622 | it('adds a mapped enum', async () => { 623 | const builder = createPrismaSchemaBuilder(` 624 | enum GradeLevel { 625 | KINDERGARTEN @map("kindergarten") 626 | FIRST @map("first") 627 | SECOND @map("second") 628 | THIRD @map("third") 629 | FOURTH @map("fourth") 630 | FIFTH @map("fifth") 631 | SIXTH @map("sixth") 632 | SEVENTH @map("seventh") 633 | EIGHTH @map("eighth") 634 | NINTH @map("ninth") 635 | TENTH @map("tenth") 636 | ELEVENTH @map("eleventh") 637 | TWELFTH @map("twelfth") 638 | THIRTEEN @map("thirteen") 639 | POST_SECONDARY @map("post_secondary") 640 | OTHER @map("other") 641 | } 642 | `); 643 | 644 | builder 645 | .enum('GradeLevel') 646 | .enumerator('FOO') 647 | .attribute('map', ['"foo"']) 648 | .break() 649 | .blockAttribute('map', 'grades'); 650 | expect(builder.print()).toMatchSnapshot(); 651 | }); 652 | 653 | it('prints the schema', async () => { 654 | const source = await loadFixture('example.prisma'); 655 | const result = createPrismaSchemaBuilder(source).print(); 656 | expect(result).toMatchInlineSnapshot(` 657 | "// https://www.prisma.io/docs/concepts/components/prisma-schema 658 | // added some fields to test keyword ambiguous 659 | 660 | datasource db { 661 | url = env("DATABASE_URL") 662 | provider = "postgresql" 663 | } 664 | 665 | generator client { 666 | provider = "prisma-client-js" 667 | } 668 | 669 | model User { 670 | id Int @id @default(autoincrement()) 671 | createdAt DateTime @default(now()) 672 | email String @unique 673 | name String? 674 | role Role @default(USER) 675 | posts Post[] 676 | projects Project[] 677 | } 678 | 679 | model Post { 680 | id Int @id @default(autoincrement()) 681 | createdAt DateTime @default(now()) 682 | updatedAt DateTime @updatedAt 683 | published Boolean @default(false) 684 | title String @db.VarChar(255) 685 | author User? @relation(fields: [authorId], references: [id]) 686 | authorId Int? 687 | // keyword test 688 | model String 689 | generator String 690 | datasource String 691 | enum String // inline comment 692 | 693 | @@map("posts") 694 | } 695 | 696 | model Project { 697 | client User @relation(fields: [clientId], references: [id]) 698 | clientId Int 699 | projectCode String 700 | dueDate DateTime 701 | 702 | @@id([projectCode]) 703 | @@unique([clientId, projectCode]) 704 | @@index([dueDate]) 705 | } 706 | 707 | model Model { 708 | id Int @id 709 | 710 | @@ignore 711 | } 712 | 713 | enum Role { 714 | USER @map("usr") // basic role 715 | ADMIN @map("adm") // more powerful role 716 | 717 | @@map("roles") 718 | } 719 | 720 | model Indexed { 721 | id String @id(map: "PK_indexed") @db.UniqueIdentifier 722 | foo String @db.UniqueIdentifier 723 | bar String @db.UniqueIdentifier 724 | 725 | @@index([foo, bar(sort: Desc)], map: "IX_indexed_indexedFoo") 726 | } 727 | " 728 | `); 729 | }); 730 | }); 731 | -------------------------------------------------------------------------------- /test/__snapshots__/PrismaSchemaBuilder.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`PrismaSchemaBuilder adds a composite type 1`] = ` 4 | " 5 | datasource db { 6 | provider = "mongodb" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model Product { 11 | id String @id @default(auto()) @map("_id") @db.ObjectId 12 | name String 13 | photos Photo[] 14 | } 15 | 16 | type Photo { 17 | height Int @default(0) 18 | width Int @default(0) 19 | url String 20 | } 21 | " 22 | `; 23 | 24 | exports[`PrismaSchemaBuilder adds a mapped enum 1`] = ` 25 | " 26 | enum GradeLevel { 27 | KINDERGARTEN @map("kindergarten") 28 | FIRST @map("first") 29 | SECOND @map("second") 30 | THIRD @map("third") 31 | FOURTH @map("fourth") 32 | FIFTH @map("fifth") 33 | SIXTH @map("sixth") 34 | SEVENTH @map("seventh") 35 | EIGHTH @map("eighth") 36 | NINTH @map("ninth") 37 | TENTH @map("tenth") 38 | ELEVENTH @map("eleventh") 39 | TWELFTH @map("twelfth") 40 | THIRTEEN @map("thirteen") 41 | POST_SECONDARY @map("post_secondary") 42 | OTHER @map("other") 43 | FOO @map("foo") 44 | 45 | @@map("grades") 46 | } 47 | " 48 | `; 49 | -------------------------------------------------------------------------------- /test/__snapshots__/printSchema.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`printSchema print atena-server.prisma 1`] = ` 4 | "// I found this on github by searching for large schema.prisma files in public repos. 5 | 6 | // This is your Prisma schema file, 7 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 8 | 9 | datasource db { 10 | provider = "postgresql" 11 | url = env("DATABASE_URL") 12 | } 13 | 14 | generator client { 15 | provider = "prisma-client-js" 16 | } 17 | 18 | model City { 19 | id String @id @default(uuid()) 20 | name String 21 | uf String 22 | ibge String @unique 23 | companies Company[] 24 | groups Group[] 25 | 26 | @@map(name: "cities") 27 | } 28 | 29 | model Company { 30 | id String @id @default(uuid()) 31 | cnpj String @unique 32 | name String 33 | city City @relation(fields: [cityId], references: [id]) 34 | cityId String 35 | sphere String 36 | agreements Agreement[] 37 | 38 | @@map(name: "companies") 39 | } 40 | 41 | model User { 42 | id String @id @default(uuid()) 43 | name String 44 | username String @unique 45 | email String @unique 46 | active Boolean @default(true) 47 | group Group? @relation(fields: [groupId], references: [id]) 48 | groupId String? 49 | 50 | @@map(name: "users") 51 | } 52 | 53 | model Group { 54 | id String @id @default(uuid()) 55 | name String 56 | access GroupAccess @default(ANY) 57 | cities City[] 58 | users User[] 59 | 60 | @@map(name: "groups") 61 | } 62 | 63 | enum GroupAccess { 64 | ANY 65 | MUNICIPAL_SPHERE 66 | STATE_SPHERE 67 | CITIES 68 | } 69 | 70 | model Agreement { 71 | id String @id @default(uuid()) 72 | agreementId String? 73 | company Company? @relation(fields: [companyId], references: [id]) 74 | companyId String? 75 | name String? 76 | status String? 77 | start DateTime? 78 | end DateTime? 79 | program String? 80 | proposalData ProposalData? 81 | workPlan WorkPlan? 82 | convenientExecution ConvenientExecution? 83 | accountability Accountability? 84 | createdAt DateTime @default(now()) 85 | updatedAt DateTime @default(now()) 86 | 87 | @@map(name: "agreements") 88 | } 89 | 90 | model ProposalData { 91 | id String @id @default(uuid()) 92 | agreement Agreement? @relation(fields: [agreementId], references: [id]) 93 | agreementId String? 94 | data Data? 95 | programs Program[] 96 | participants Participants? 97 | 98 | @@map(name: "proposals_data") 99 | } 100 | 101 | model Data { 102 | id String @id @default(uuid()) 103 | proposalData ProposalData? @relation(fields: [proposalDataId], references: [id]) 104 | proposalDataId String? 105 | modality String? 106 | contractingStatus String? 107 | status Status? 108 | proposalId String? 109 | organId String? 110 | processId String? 111 | proponent String? 112 | legalFoundation String? 113 | organ String? 114 | linkedOrgan String? 115 | description String? 116 | justification String? 117 | targetAudience String? 118 | problem String? 119 | result String? 120 | proposalAndObjectivesRelation String? 121 | categories String? 122 | object String? 123 | information String? 124 | proposalDate DateTime? 125 | biddingDate DateTime? 126 | homologationDate DateTime? 127 | 128 | @@map(name: "agreements_data") 129 | } 130 | 131 | model Status { 132 | id String @id @default(uuid()) 133 | data Data? @relation(fields: [dataId], references: [id]) 134 | dataId String? 135 | value String? 136 | committed String? 137 | publication String? 138 | 139 | @@map(name: "status") 140 | } 141 | 142 | model Program { 143 | id String @id @default(uuid()) 144 | proposalData ProposalData? @relation(fields: [proposalDataId], references: [id]) 145 | proposalDataId String? 146 | programId String? 147 | name String? 148 | value Float? @default(0) 149 | details ProgramDetails? 150 | 151 | @@map(name: "programs") 152 | } 153 | 154 | model ProgramDetails { 155 | id String @id @default(uuid()) 156 | program Program? @relation(fields: [programId], references: [id]) 157 | programId String? 158 | code String? 159 | name String? 160 | cps String? 161 | items String? 162 | couterpartRule String? 163 | totalValue Float? 164 | couterpartValues ProgramDetailsCounterpartValues? 165 | transferValues ProgramDetailsTransferValues? 166 | 167 | @@map(name: "programs_details") 168 | } 169 | 170 | model ProgramDetailsCounterpartValues { 171 | id String @id @default(uuid()) 172 | programDetails ProgramDetails? @relation(fields: [programDetailsId], references: [id]) 173 | programDetailsId String? 174 | total Float? 175 | financial Float? 176 | assetsAndServices Float? 177 | 178 | @@map(name: "programs_details_counterpart_values") 179 | } 180 | 181 | model ProgramDetailsTransferValues { 182 | id String @id @default(uuid()) 183 | programDetails ProgramDetails? @relation(fields: [programDetailsId], references: [id]) 184 | programDetailsId String? 185 | total Float? 186 | amendment String? 187 | 188 | @@map(name: "programs_details_transfer_values") 189 | } 190 | 191 | model Participants { 192 | id String @id @default(uuid()) 193 | proposalData ProposalData? @relation(fields: [proposalDataId], references: [id]) 194 | proposalDataId String? 195 | proponent String? 196 | respProponent String? 197 | grantor String? 198 | respGrantor String? 199 | 200 | @@map(name: "participants") 201 | } 202 | 203 | model WorkPlan { 204 | id String @id @default(uuid()) 205 | agreement Agreement? @relation(fields: [agreementId], references: [id]) 206 | agreementId String? 207 | physicalChrono PhysicalChrono? 208 | disbursementChrono DisbursementChrono? 209 | detailedApplicationPlan DetailedApplicationPlan? 210 | consolidatedApplicationPlan ConsolidatedApplicationPlan? 211 | attachments Attachments? 212 | notions Notions? 213 | 214 | @@map(name: "work_plans") 215 | } 216 | 217 | model PhysicalChrono { 218 | id String @id @default(uuid()) 219 | workPlan WorkPlan? @relation(fields: [workPlanId], references: [id]) 220 | workPlanId String? 221 | list PhysicalChronoItem[] 222 | values PhysicalChronoValues? 223 | 224 | @@map(name: "physical_chronos") 225 | } 226 | 227 | model PhysicalChronoItem { 228 | id String @id @default(uuid()) 229 | physicalChrono PhysicalChrono? @relation(fields: [physicalChronoId], references: [id]) 230 | physicalChronoId String? 231 | goalId Int? 232 | specification String? 233 | value Float? @default(0) 234 | startDate DateTime? 235 | endDate DateTime? 236 | income String? 237 | 238 | @@map(name: "physical_chrono_items") 239 | } 240 | 241 | model PhysicalChronoValues { 242 | id String @id @default(uuid()) 243 | physicalChrono PhysicalChrono? @relation(fields: [physicalChronoId], references: [id]) 244 | physicalChronoId String? 245 | registered Float? @default(0) 246 | register Float? @default(0) 247 | global Float? @default(0) 248 | 249 | @@map(name: "physical_chrono_values") 250 | } 251 | 252 | model DisbursementChrono { 253 | id String @id @default(uuid()) 254 | workPlan WorkPlan? @relation(fields: [workPlanId], references: [id]) 255 | workPlanId String? 256 | list DisbursementChronoItem[] 257 | values DisbursementChronoValues? 258 | 259 | @@map(name: "disbursement_chronos") 260 | } 261 | 262 | model DisbursementChronoItem { 263 | id String @id @default(uuid()) 264 | disbursementChrono DisbursementChrono? @relation(fields: [disbursementChronoId], references: [id]) 265 | disbursementChronoId String? 266 | portionId Int? 267 | type String? 268 | month String? 269 | year Int? 270 | value Float? @default(0) 271 | 272 | @@map(name: "disbursement_chrono_item") 273 | } 274 | 275 | model DisbursementChronoValues { 276 | id String @id @default(uuid()) 277 | disbursementChrono DisbursementChrono? @relation(fields: [disbursementChronoId], references: [id]) 278 | disbursementChronoId String? 279 | registered DisbursementChronoValue? @relation("RegisteredDisbursementChronoValue") 280 | register DisbursementChronoValue? @relation("RegisterDisbursementChronoValue") 281 | total DisbursementChronoValue? @relation("TotalDisbursementChronoValue") 282 | 283 | @@map(name: "disbursement_chrono_values") 284 | } 285 | 286 | model DisbursementChronoValue { 287 | id String @id @default(uuid()) 288 | registeredValue DisbursementChronoValues? @relation("RegisteredDisbursementChronoValue", fields: [registeredValueOf], references: [id]) 289 | registeredValueOf String? 290 | registerValue DisbursementChronoValues? @relation("RegisterDisbursementChronoValue", fields: [registerValueOf], references: [id]) 291 | registerValueOf String? 292 | totalValue DisbursementChronoValues? @relation("TotalDisbursementChronoValue", fields: [totalValueOf], references: [id]) 293 | totalValueOf String? 294 | granting Float? @default(0) 295 | convenient Float? @default(0) 296 | yield Float? @default(0) 297 | 298 | @@map(name: "disbursement_chrono_value") 299 | } 300 | 301 | model DetailedApplicationPlan { 302 | id String @id @default(uuid()) 303 | workPlan WorkPlan? @relation(fields: [workPlanId], references: [id]) 304 | workPlanId String? 305 | list DetailedApplicationPlanItem[] 306 | values DetailedApplicationPlanValues? 307 | 308 | @@map(name: "detailed_application_plans") 309 | } 310 | 311 | model DetailedApplicationPlanItem { 312 | id String @id @default(uuid()) 313 | detailedApplicationPlan DetailedApplicationPlan? @relation(fields: [detailedApplicationPlanId], references: [id]) 314 | detailedApplicationPlanId String? 315 | type String? 316 | description String? 317 | natureExpenseCode Int? 318 | natureAcquisition String? 319 | un String? 320 | amount Float? @default(0) 321 | unitValue Float? @default(0) 322 | totalValue Float? @default(0) 323 | status String? 324 | 325 | @@map(name: "detailed_application_plan_items") 326 | } 327 | 328 | model DetailedApplicationPlanValues { 329 | id String @id @default(uuid()) 330 | detailedApplicationPlan DetailedApplicationPlan? @relation(fields: [detailedApplicationPlanId], references: [id]) 331 | detailedApplicationPlanId String? 332 | assets DetailedApplicationPlanValue? @relation("AssetsDetailedApplicationPlanValue") 333 | tributes DetailedApplicationPlanValue? @relation("TributesDetailedApplicationPlanValue") 334 | construction DetailedApplicationPlanValue? @relation("ConstructionDetailedApplicationPlanValue") 335 | services DetailedApplicationPlanValue? @relation("ServicesDetailedApplicationPlanValue") 336 | others DetailedApplicationPlanValue? @relation("OthersDetailedApplicationPlanValue") 337 | administrative DetailedApplicationPlanValue? @relation("AdministrativeDetailedApplicationPlanValue") 338 | total DetailedApplicationPlanValue? @relation("TotalDetailedApplicationPlanValue") 339 | 340 | @@map(name: "detailed_application_plan_values") 341 | } 342 | 343 | model DetailedApplicationPlanValue { 344 | id String @id @default(uuid()) 345 | assetsValue DetailedApplicationPlanValues? @relation("AssetsDetailedApplicationPlanValue", fields: [assetsValueOf], references: [id]) 346 | assetsValueOf String? 347 | tributesValue DetailedApplicationPlanValues? @relation("TributesDetailedApplicationPlanValue", fields: [tributesValueOf], references: [id]) 348 | tributesValueOf String? 349 | constructionValue DetailedApplicationPlanValues? @relation("ConstructionDetailedApplicationPlanValue", fields: [constructionValueOf], references: [id]) 350 | constructionValueOf String? 351 | servicesValue DetailedApplicationPlanValues? @relation("ServicesDetailedApplicationPlanValue", fields: [servicesValueOf], references: [id]) 352 | servicesValueOf String? 353 | othersValue DetailedApplicationPlanValues? @relation("OthersDetailedApplicationPlanValue", fields: [othersValueOf], references: [id]) 354 | othersValueOf String? 355 | administrativeValue DetailedApplicationPlanValues? @relation("AdministrativeDetailedApplicationPlanValue", fields: [administrativeValueOf], references: [id]) 356 | administrativeValueOf String? 357 | totalValue DetailedApplicationPlanValues? @relation("TotalDetailedApplicationPlanValue", fields: [totalValueId], references: [id]) 358 | totalValueId String? 359 | total Float? @default(0) 360 | resource Float? @default(0) 361 | counterpart Float? @default(0) 362 | yield Float? @default(0) 363 | 364 | @@map(name: "detailed_application_plan_value") 365 | } 366 | 367 | model ConsolidatedApplicationPlan { 368 | id String @id @default(uuid()) 369 | workPlan WorkPlan? @relation(fields: [workPlanId], references: [id]) 370 | workPlanId String? 371 | list ConsolidatedApplicationPlanItem[] @relation("ListConsolidatedApplicationPlanItems") 372 | total ConsolidatedApplicationPlanItem? @relation("TotalConsolidatedApplicationPlanItems") 373 | 374 | @@map(name: "consolidated_application_plans") 375 | } 376 | 377 | model ConsolidatedApplicationPlanItem { 378 | id String @id @default(uuid()) 379 | listItem ConsolidatedApplicationPlan[] @relation("ListConsolidatedApplicationPlanItems", fields: [listItemOf], references: [id]) 380 | listItemOf String? 381 | totalItem ConsolidatedApplicationPlan? @relation("TotalConsolidatedApplicationPlanItems", fields: [totalItemOf], references: [id]) 382 | totalItemOf String? 383 | classification String? 384 | resources Float? @default(0) 385 | counterpart Float? @default(0) 386 | yield Float? @default(0) 387 | total Float? @default(0) 388 | 389 | @@map(name: "consolidated_application_plan_items") 390 | } 391 | 392 | model Attachments { 393 | id String @id @default(uuid()) 394 | workPlan WorkPlan? @relation(fields: [workPlanId], references: [id]) 395 | workPlanId String? 396 | proposalList Attachment[] @relation("ProposalAttachments") 397 | executionList Attachment[] @relation("ExecutionAttachments") 398 | 399 | @@map(name: "attachments") 400 | } 401 | 402 | model Attachment { 403 | id String @id @default(uuid()) 404 | proposalAttachment Attachments[] @relation("ProposalAttachments", fields: [proposalAttachmentOf], references: [id]) 405 | proposalAttachmentOf String? 406 | executionAttachment Attachments[] @relation("ExecutionAttachments", fields: [executionAttachmentOf], references: [id]) 407 | executionAttachmentOf String? 408 | name String? 409 | description String? 410 | date DateTime? 411 | 412 | @@map(name: "attachment") 413 | } 414 | 415 | model Notions { 416 | id String @id @default(uuid()) 417 | workPlan WorkPlan? @relation(fields: [workPlanId], references: [id]) 418 | workPlanId String? 419 | proposalList NotionItem[] @relation("ProposalNotionItems") 420 | workPlanList NotionItem[] @relation("WorkPlanNotionItems") 421 | 422 | @@map(name: "notions") 423 | } 424 | 425 | model NotionItem { 426 | id String @id @default(uuid()) 427 | proposalNotionItem Notions[] @relation("ProposalNotionItems", fields: [proposalNotionItemOf], references: [id]) 428 | proposalNotionItemOf String? 429 | executionNotionItem Notions[] @relation("WorkPlanNotionItems", fields: [executionNotionItemOf], references: [id]) 430 | executionNotionItemOf String? 431 | date DateTime? 432 | type String? 433 | responsible String? 434 | assignment String? 435 | occupation String? 436 | 437 | @@map(name: "notion_item") 438 | } 439 | 440 | model ConvenientExecution { 441 | id String @id @default(uuid()) 442 | agreement Agreement? @relation(fields: [agreementId], references: [id]) 443 | agreementId String? 444 | executionProcesses ExecutionProcess[] 445 | contracts Contract[] 446 | 447 | @@map(name: "convenient_execution") 448 | } 449 | 450 | model ExecutionProcess { 451 | id String @id @default(uuid()) 452 | convenientExecution ConvenientExecution? @relation(fields: [convenientExecutionId], references: [id]) 453 | convenientExecutionId String? 454 | executionId String? 455 | type String? 456 | date DateTime? 457 | processId String? 458 | status String? 459 | systemStatus String? 460 | system String? 461 | accepted String? 462 | details ExecutionProcessDetails? 463 | 464 | @@map(name: "execution_processes") 465 | } 466 | 467 | model ExecutionProcessDetails { 468 | id String @id @default(uuid()) 469 | executionProcessRel ExecutionProcess? @relation(fields: [executionProcessRelId], references: [id]) 470 | executionProcessRelId String? 471 | executionProcess String? 472 | buyType String? 473 | status String? 474 | origin String? 475 | financialResource String? 476 | modality String? 477 | biddingType String? 478 | processId String? 479 | biddingId String? 480 | object String? 481 | legalFoundation String? 482 | justification String? 483 | publishDate DateTime? 484 | beginDate DateTime? 485 | endDate DateTime? 486 | biddingValue Float? 487 | homologationDate DateTime? 488 | city String? 489 | analysisDate DateTime? 490 | accepted String? 491 | 492 | @@map(name: "execution_processes_details") 493 | } 494 | 495 | model Contract { 496 | id String @id @default(uuid()) 497 | convenientExecution ConvenientExecution? @relation(fields: [convenientExecutionId], references: [id]) 498 | convenientExecutionId String? 499 | contractId String? 500 | biddingId String? 501 | date DateTime? 502 | details ContractDetails? 503 | 504 | @@map(name: "contracts") 505 | } 506 | 507 | model ContractDetails { 508 | id String @id @default(uuid()) 509 | contract Contract? @relation(fields: [contractId], references: [id]) 510 | contractId String? 511 | hiredDocument String? 512 | hirerDocument String? 513 | type String? 514 | object String? 515 | totalValue Float? 516 | publishDate DateTime? 517 | beginDate DateTime? 518 | endDate DateTime? 519 | signDate DateTime? 520 | executionProcessId String? 521 | biddingModality String? 522 | processId String? 523 | 524 | @@map(name: "contracts_details") 525 | } 526 | 527 | model Accountability { 528 | id String @id @default(uuid()) 529 | agreement Agreement? @relation(fields: [agreementId], references: [id]) 530 | agreementId String? 531 | data AccountabilityData? 532 | 533 | @@map(name: "accountabilities") 534 | } 535 | 536 | model AccountabilityData { 537 | id String @id @default(uuid()) 538 | accountabilty Accountability? @relation(fields: [accountabiltyId], references: [id]) 539 | accountabiltyId String? 540 | organ String? 541 | convenient String? 542 | documentNumber String? 543 | modality String? 544 | status String? 545 | number String? 546 | validity String? 547 | limitDate DateTime? 548 | totalValue Float? 549 | transferValue Float? 550 | counterpartValue Float? 551 | yieldValue Float? 552 | 553 | @@map(name: "accountabilities_data") 554 | } 555 | " 556 | `; 557 | 558 | exports[`printSchema print composite-types.prisma 1`] = ` 559 | " 560 | datasource db { 561 | provider = "mongodb" 562 | url = env("DATABASE_URL") 563 | } 564 | 565 | model Product { 566 | id String @id @default(auto()) @map("_id") @db.ObjectId 567 | name String 568 | photos Photo[] 569 | } 570 | 571 | type Photo { 572 | height Int @default(0) 573 | width Int @default(0) 574 | url String 575 | } 576 | " 577 | `; 578 | 579 | exports[`printSchema print empty-comment.prisma 1`] = ` 580 | " 581 | datasource db { 582 | provider = "postgresql" 583 | url = env("DATABASE_URL") 584 | } 585 | 586 | model Product { 587 | id String @id @default(uuid()) @db.Uuid 588 | // 589 | productNumber String @unique 590 | price Decimal @db.Decimal(19, 4) // 591 | unit String @db.VarChar(3) 592 | unitOfMeasurement UnitOfMeasurement @relation(fields: [unit], references: [unit]) 593 | salesOrderItems SaleOrderItem[] 594 | productTexts ProductText[] 595 | createdAt DateTime @default(now()) 596 | version Int @default(0) 597 | } 598 | 599 | model UnitOfMeasurement { 600 | name String 601 | unit String @unique @db.VarChar(3) 602 | products Product[] 603 | } 604 | 605 | model SaleOrderItem { 606 | productId String @id @db.Uuid 607 | product Product @relation(fields: [productId], references: [id]) 608 | } 609 | 610 | model ProductText { 611 | description String 612 | productId String @id @db.Uuid 613 | textType TextType 614 | product Product @relation(fields: [productId], references: [id]) 615 | } 616 | 617 | enum TextType { 618 | // 619 | MARKDOWN 620 | RICHTEXT // 621 | } 622 | " 623 | `; 624 | 625 | exports[`printSchema print example.prisma 1`] = ` 626 | "// https://www.prisma.io/docs/concepts/components/prisma-schema 627 | // added some fields to test keyword ambiguous 628 | 629 | datasource db { 630 | url = env("DATABASE_URL") 631 | provider = "postgresql" 632 | } 633 | 634 | generator client { 635 | provider = "prisma-client-js" 636 | } 637 | 638 | model User { 639 | id Int @id @default(autoincrement()) 640 | createdAt DateTime @default(now()) 641 | email String @unique 642 | name String? 643 | role Role @default(USER) 644 | posts Post[] 645 | projects Project[] 646 | } 647 | 648 | model Post { 649 | id Int @id @default(autoincrement()) 650 | createdAt DateTime @default(now()) 651 | updatedAt DateTime @updatedAt 652 | published Boolean @default(false) 653 | title String @db.VarChar(255) 654 | author User? @relation(fields: [authorId], references: [id]) 655 | authorId Int? 656 | // keyword test 657 | model String 658 | generator String 659 | datasource String 660 | enum String // inline comment 661 | 662 | @@map("posts") 663 | } 664 | 665 | model Project { 666 | client User @relation(fields: [clientId], references: [id]) 667 | clientId Int 668 | projectCode String 669 | dueDate DateTime 670 | 671 | @@id([projectCode]) 672 | @@unique([clientId, projectCode]) 673 | @@index([dueDate]) 674 | } 675 | 676 | model Model { 677 | id Int @id 678 | 679 | @@ignore 680 | } 681 | 682 | enum Role { 683 | USER @map("usr") // basic role 684 | ADMIN @map("adm") // more powerful role 685 | 686 | @@map("roles") 687 | } 688 | 689 | model Indexed { 690 | id String @id(map: "PK_indexed") @db.UniqueIdentifier 691 | foo String @db.UniqueIdentifier 692 | bar String @db.UniqueIdentifier 693 | 694 | @@index([foo, bar(sort: Desc)], map: "IX_indexed_indexedFoo") 695 | } 696 | " 697 | `; 698 | 699 | exports[`printSchema print kebab-case.prisma 1`] = ` 700 | " 701 | generator prisma-model-generator { 702 | provider = "node ./dist/apps/prisma-model-generator/src/generator.js" 703 | fileNamingStyle = "kebab" 704 | classNamingStyle = "pascal" 705 | output = "./generated/" 706 | } 707 | " 708 | `; 709 | 710 | exports[`printSchema print keystonejs.prisma 1`] = ` 711 | " 712 | model Book { 713 | id String @default(cuid()) @id 714 | slug String @default("") @unique 715 | title String @default("") 716 | status String? @default("draft") 717 | authors User[] @relation("Book_authors") 718 | genres Genre[] @relation("Book_genres") 719 | language String @default("ua") 720 | chapters Chapter[] @relation("Chapter_book") 721 | tags Tag[] @relation("Book_tags") 722 | cover Image? @relation("Book_cover", fields: [coverId], references: [id]) 723 | coverId String? @map("cover") 724 | translationGroup TranslationGroup? @relation("Book_translationGroup", fields: [translationGroupId], references: [id]) 725 | translationGroupId String? @map("translationGroup") 726 | 727 | @@index([coverId]) 728 | @@index([translationGroupId]) 729 | } 730 | " 731 | `; 732 | 733 | exports[`printSchema print keystonejs-weird.prisma 1`] = ` 734 | " 735 | model Book { 736 | id String @default(cuid()) @id 737 | slug String @default("") @unique 738 | title String @default("") 739 | status String? @default("draft") 740 | authors User[] @relation("Book_authors") 741 | genres Genre[] @relation("Book_genres") 742 | language String @default("ua") 743 | chapters Chapter[] @relation("Chapter_book") 744 | tags Tag[] @relation("Book_tags") 745 | cover Image? @relation("Book_cover", fields: [coverId], references: [id]) 746 | coverId String? @map("cover") 747 | translationGroup TranslationGroup? @relation("Book_translationGroup", fields: [translationGroupId], references: [id]) 748 | translationGroupId String? @map("translationGroup") 749 | 750 | @@index([coverId]) 751 | @@index([translationGroupId]) 752 | } 753 | " 754 | `; 755 | 756 | exports[`printSchema print links.prisma 1`] = ` 757 | " 758 | datasource db { 759 | provider = "postgres" 760 | url = env("DATABASE_URL") 761 | } 762 | 763 | generator client { 764 | provider = "prisma-client-js" 765 | binaryTargets = "native" 766 | } 767 | 768 | model UserProfile { 769 | userID String @id @unique 770 | 771 | /// A one liner 772 | bio String? 773 | 774 | /// Hrefs which show under the user 775 | links String[] @default([]) 776 | } 777 | " 778 | `; 779 | 780 | exports[`printSchema print redwood.prisma 1`] = ` 781 | " 782 | datasource DS { 783 | provider = "sqlite" 784 | url = env("DATABASE_URL") 785 | } 786 | 787 | generator client { 788 | provider = "prisma-client-js" 789 | binaryTargets = "native" 790 | } 791 | 792 | /// Define your own datamodels here and run \`yarn redwood db save\` to create 793 | /// migrations for them. 794 | 795 | model Post { 796 | /// this is the post id 797 | id Int @id @default(autoincrement()) 798 | title String 799 | slug String @unique 800 | author String 801 | body String 802 | image String? 803 | tags Tag[] 804 | postedAt DateTime? 805 | } 806 | 807 | model Tag { 808 | id Int @id @default(autoincrement()) 809 | name String @unique 810 | posts Post[] 811 | } 812 | 813 | model User { 814 | id Int @id @default(autoincrement()) 815 | name String? 816 | email String @unique 817 | isAdmin Boolean @default(false) 818 | } 819 | " 820 | `; 821 | 822 | exports[`printSchema print star.prisma 1`] = ` 823 | "// an example from prisma with some atypical syntax to parse 824 | 825 | model Star { 826 | id Int @id @default(autoincrement()) 827 | position Unsupported("circle")? 828 | example1 Unsupported("circle") 829 | circle Unsupported("circle")? @default(dbgenerated("'<(10,4),11>'::circle")) 830 | } 831 | " 832 | `; 833 | 834 | exports[`printSchema print test.prisma 1`] = ` 835 | " 836 | datasource db { 837 | provider = "postgres" 838 | url = env("DATABASE_URL") 839 | } 840 | 841 | // this is foo 842 | 843 | generator foo { 844 | // it is a nexus 845 | provider = "nexus" 846 | previewFeatures = ["napi"] 847 | } 848 | 849 | model Bar { 850 | id Int @id @first(fields: [one, two]) @second(fields: ["three", "four"]) 851 | // this is not a break 852 | name String @unique 853 | 854 | supercalifragilisticexpialidocious String @default("it is something quite atrocious") 855 | owner String // test 856 | 857 | /// test 858 | 859 | alphabet String @db.VarChar(26) 860 | number Int 861 | } 862 | 863 | enum Role { 864 | USER 865 | ADMIN 866 | 867 | @@map("membership_role") 868 | @@db.special 869 | } 870 | " 871 | `; 872 | 873 | exports[`printSchema print unsorted.prisma 1`] = ` 874 | " 875 | enum Role { 876 | ADMIN 877 | OWNER // similar to ADMIN, but can delete the project 878 | MEMBER 879 | USER // deprecated 880 | } 881 | 882 | datasource db { 883 | url = env("DATABASE_URL") 884 | provider = "postgresql" 885 | } 886 | 887 | // this is a comment 888 | 889 | model User { 890 | id Int @id @default(autoincrement()) 891 | createdAt DateTime @default(now()) 892 | email String @unique 893 | name String? 894 | role Role @default(MEMBER) 895 | } 896 | 897 | generator client { 898 | provider = "prisma-client-js" 899 | } 900 | 901 | model AppSetting { 902 | key String @id 903 | value Json 904 | } 905 | " 906 | `; 907 | 908 | exports[`printSchema prints windows-style line breaks 1`] = ` 909 | " 910 | model Foo { 911 | one Int 912 | 913 | two String 914 | } 915 | " 916 | `; 917 | -------------------------------------------------------------------------------- /test/finder.test.ts: -------------------------------------------------------------------------------- 1 | import { createPrismaSchemaBuilder } from '../src/PrismaSchemaBuilder'; 2 | import { loadFixture } from './utils'; 3 | 4 | describe('finder', () => { 5 | it('finds a model', async () => { 6 | const source = await loadFixture('example.prisma'); 7 | const finder = createPrismaSchemaBuilder(source); 8 | 9 | const model = finder.findByType('model', { name: 'Indexed' }); 10 | expect(model).toHaveProperty('name', 'Indexed'); 11 | }); 12 | 13 | it('finds all matching models', async () => { 14 | const source = await loadFixture('atena-server.prisma'); 15 | const finder = createPrismaSchemaBuilder(source); 16 | 17 | const [proposalData, data, accountabilityData, unexpected] = 18 | finder.findAllByType('model', { 19 | name: /Data$/, 20 | }); 21 | expect(proposalData).toHaveProperty('name', 'ProposalData'); 22 | expect(data).toHaveProperty('name', 'Data'); 23 | expect(accountabilityData).toHaveProperty('name', 'AccountabilityData'); 24 | expect(unexpected).toBeUndefined(); 25 | }); 26 | 27 | it('finds an enumerator', async () => { 28 | const source = await loadFixture('atena-server.prisma'); 29 | const finder = createPrismaSchemaBuilder(source); 30 | 31 | const groupAccess = finder.findByType('enum', { name: 'GroupAccess' }); 32 | expect(groupAccess).toHaveProperty('name', 'GroupAccess'); 33 | 34 | const cities = finder.findByType('enumerator', { 35 | name: 'CITIES', 36 | within: groupAccess?.enumerators, 37 | }); 38 | 39 | expect(cities).toHaveProperty('name', 'CITIES'); 40 | }); 41 | 42 | it('finds all matching fields', async () => { 43 | const source = await loadFixture('empty-comment.prisma'); 44 | const finder = createPrismaSchemaBuilder(source); 45 | 46 | const product = finder.findByType('model', { name: 'Product' }); 47 | expect(product).toHaveProperty('name', 'Product'); 48 | 49 | const [unit, unitOfMeasurement, unexpected] = finder.findAllByType( 50 | 'field', 51 | { name: /unit/i, within: product?.properties } 52 | ); 53 | expect(unit).toHaveProperty('name', 'unit'); 54 | expect(unitOfMeasurement).toHaveProperty('name', 'unitOfMeasurement'); 55 | expect(unexpected).toBeUndefined(); 56 | }); 57 | 58 | it('finds an attribute', async () => { 59 | const source = await loadFixture('composite-types.prisma'); 60 | const finder = createPrismaSchemaBuilder(source); 61 | 62 | const product = finder.findByType('model', { name: 'Product' }); 63 | expect(product).toHaveProperty('name', 'Product'); 64 | 65 | const id = finder.findByType('field', { 66 | name: 'id', 67 | within: product?.properties, 68 | }); 69 | expect(id).toHaveProperty('name', 'id'); 70 | 71 | const map = finder.findByType('attribute', { 72 | name: 'map', 73 | within: id?.attributes, 74 | }); 75 | expect(map).toHaveProperty('name', 'map'); 76 | expect(map).toHaveProperty(['args', 0, 'value'], '"_id"'); 77 | }); 78 | 79 | it('finds an assignment', async () => { 80 | const source = await loadFixture('kebab-case.prisma'); 81 | const finder = createPrismaSchemaBuilder(source); 82 | 83 | const generator = finder.findByType('generator', { 84 | name: 'prisma-model-generator', 85 | }); 86 | expect(generator).toHaveProperty('name', 'prisma-model-generator'); 87 | 88 | const assignment = finder.findByType('assignment', { 89 | name: 'fileNamingStyle', 90 | within: generator?.assignments, 91 | }); 92 | expect(assignment).toHaveProperty('value', '"kebab"'); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /test/fixtures/atena-server.prisma: -------------------------------------------------------------------------------- 1 | // I found this on github by searching for large schema.prisma files in public repos. 2 | 3 | // This is your Prisma schema file, 4 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 5 | 6 | datasource db { 7 | provider = "postgresql" 8 | url = env("DATABASE_URL") 9 | } 10 | 11 | generator client { 12 | provider = "prisma-client-js" 13 | } 14 | 15 | model City { 16 | id String @id @default(uuid()) 17 | name String 18 | uf String 19 | ibge String @unique 20 | companies Company[] 21 | groups Group[] 22 | 23 | @@map(name: "cities") 24 | } 25 | 26 | model Company { 27 | id String @id @default(uuid()) 28 | cnpj String @unique 29 | name String 30 | city City @relation(fields: [cityId], references: [id]) 31 | cityId String 32 | sphere String 33 | agreements Agreement[] 34 | 35 | @@map(name: "companies") 36 | } 37 | 38 | model User { 39 | id String @id @default(uuid()) 40 | name String 41 | username String @unique 42 | email String @unique 43 | active Boolean @default(true) 44 | group Group? @relation(fields: [groupId], references: [id]) 45 | groupId String? 46 | 47 | @@map(name: "users") 48 | } 49 | 50 | model Group { 51 | id String @id @default(uuid()) 52 | name String 53 | access GroupAccess @default(ANY) 54 | cities City[] 55 | users User[] 56 | 57 | @@map(name: "groups") 58 | } 59 | 60 | enum GroupAccess { 61 | ANY 62 | MUNICIPAL_SPHERE 63 | STATE_SPHERE 64 | CITIES 65 | } 66 | 67 | model Agreement { 68 | id String @id @default(uuid()) 69 | agreementId String? 70 | company Company? @relation(fields: [companyId], references: [id]) 71 | companyId String? 72 | name String? 73 | status String? 74 | start DateTime? 75 | end DateTime? 76 | program String? 77 | proposalData ProposalData? 78 | workPlan WorkPlan? 79 | convenientExecution ConvenientExecution? 80 | accountability Accountability? 81 | createdAt DateTime @default(now()) 82 | updatedAt DateTime @default(now()) 83 | 84 | @@map(name: "agreements") 85 | } 86 | 87 | model ProposalData { 88 | id String @id @default(uuid()) 89 | agreement Agreement? @relation(fields: [agreementId], references: [id]) 90 | agreementId String? 91 | data Data? 92 | programs Program[] 93 | participants Participants? 94 | 95 | @@map(name: "proposals_data") 96 | } 97 | 98 | model Data { 99 | id String @id @default(uuid()) 100 | proposalData ProposalData? @relation(fields: [proposalDataId], references: [id]) 101 | proposalDataId String? 102 | modality String? 103 | contractingStatus String? 104 | status Status? 105 | proposalId String? 106 | organId String? 107 | processId String? 108 | proponent String? 109 | legalFoundation String? 110 | organ String? 111 | linkedOrgan String? 112 | description String? 113 | justification String? 114 | targetAudience String? 115 | problem String? 116 | result String? 117 | proposalAndObjectivesRelation String? 118 | categories String? 119 | object String? 120 | information String? 121 | proposalDate DateTime? 122 | biddingDate DateTime? 123 | homologationDate DateTime? 124 | 125 | @@map(name: "agreements_data") 126 | } 127 | 128 | model Status { 129 | id String @id @default(uuid()) 130 | data Data? @relation(fields: [dataId], references: [id]) 131 | dataId String? 132 | value String? 133 | committed String? 134 | publication String? 135 | 136 | @@map(name: "status") 137 | } 138 | 139 | model Program { 140 | id String @id @default(uuid()) 141 | proposalData ProposalData? @relation(fields: [proposalDataId], references: [id]) 142 | proposalDataId String? 143 | programId String? 144 | name String? 145 | value Float? @default(0) 146 | details ProgramDetails? 147 | 148 | @@map(name: "programs") 149 | } 150 | 151 | model ProgramDetails { 152 | id String @id @default(uuid()) 153 | program Program? @relation(fields: [programId], references: [id]) 154 | programId String? 155 | code String? 156 | name String? 157 | cps String? 158 | items String? 159 | couterpartRule String? 160 | totalValue Float? 161 | couterpartValues ProgramDetailsCounterpartValues? 162 | transferValues ProgramDetailsTransferValues? 163 | 164 | @@map(name: "programs_details") 165 | } 166 | 167 | model ProgramDetailsCounterpartValues { 168 | id String @id @default(uuid()) 169 | programDetails ProgramDetails? @relation(fields: [programDetailsId], references: [id]) 170 | programDetailsId String? 171 | total Float? 172 | financial Float? 173 | assetsAndServices Float? 174 | 175 | @@map(name: "programs_details_counterpart_values") 176 | } 177 | 178 | model ProgramDetailsTransferValues { 179 | id String @id @default(uuid()) 180 | programDetails ProgramDetails? @relation(fields: [programDetailsId], references: [id]) 181 | programDetailsId String? 182 | total Float? 183 | amendment String? 184 | 185 | @@map(name: "programs_details_transfer_values") 186 | } 187 | 188 | model Participants { 189 | id String @id @default(uuid()) 190 | proposalData ProposalData? @relation(fields: [proposalDataId], references: [id]) 191 | proposalDataId String? 192 | proponent String? 193 | respProponent String? 194 | grantor String? 195 | respGrantor String? 196 | 197 | @@map(name: "participants") 198 | } 199 | 200 | model WorkPlan { 201 | id String @id @default(uuid()) 202 | agreement Agreement? @relation(fields: [agreementId], references: [id]) 203 | agreementId String? 204 | physicalChrono PhysicalChrono? 205 | disbursementChrono DisbursementChrono? 206 | detailedApplicationPlan DetailedApplicationPlan? 207 | consolidatedApplicationPlan ConsolidatedApplicationPlan? 208 | attachments Attachments? 209 | notions Notions? 210 | 211 | @@map(name: "work_plans") 212 | } 213 | 214 | model PhysicalChrono { 215 | id String @id @default(uuid()) 216 | workPlan WorkPlan? @relation(fields: [workPlanId], references: [id]) 217 | workPlanId String? 218 | list PhysicalChronoItem[] 219 | values PhysicalChronoValues? 220 | 221 | @@map(name: "physical_chronos") 222 | } 223 | 224 | model PhysicalChronoItem { 225 | id String @id @default(uuid()) 226 | physicalChrono PhysicalChrono? @relation(fields: [physicalChronoId], references: [id]) 227 | physicalChronoId String? 228 | goalId Int? 229 | specification String? 230 | value Float? @default(0) 231 | startDate DateTime? 232 | endDate DateTime? 233 | income String? 234 | 235 | @@map(name: "physical_chrono_items") 236 | } 237 | 238 | model PhysicalChronoValues { 239 | id String @id @default(uuid()) 240 | physicalChrono PhysicalChrono? @relation(fields: [physicalChronoId], references: [id]) 241 | physicalChronoId String? 242 | registered Float? @default(0) 243 | register Float? @default(0) 244 | global Float? @default(0) 245 | 246 | @@map(name: "physical_chrono_values") 247 | } 248 | 249 | model DisbursementChrono { 250 | id String @id @default(uuid()) 251 | workPlan WorkPlan? @relation(fields: [workPlanId], references: [id]) 252 | workPlanId String? 253 | list DisbursementChronoItem[] 254 | values DisbursementChronoValues? 255 | 256 | @@map(name: "disbursement_chronos") 257 | } 258 | 259 | model DisbursementChronoItem { 260 | id String @id @default(uuid()) 261 | disbursementChrono DisbursementChrono? @relation(fields: [disbursementChronoId], references: [id]) 262 | disbursementChronoId String? 263 | portionId Int? 264 | type String? 265 | month String? 266 | year Int? 267 | value Float? @default(0) 268 | 269 | @@map(name: "disbursement_chrono_item") 270 | } 271 | 272 | model DisbursementChronoValues { 273 | id String @id @default(uuid()) 274 | disbursementChrono DisbursementChrono? @relation(fields: [disbursementChronoId], references: [id]) 275 | disbursementChronoId String? 276 | registered DisbursementChronoValue? @relation("RegisteredDisbursementChronoValue") 277 | register DisbursementChronoValue? @relation("RegisterDisbursementChronoValue") 278 | total DisbursementChronoValue? @relation("TotalDisbursementChronoValue") 279 | 280 | @@map(name: "disbursement_chrono_values") 281 | } 282 | 283 | model DisbursementChronoValue { 284 | id String @id @default(uuid()) 285 | registeredValue DisbursementChronoValues? @relation("RegisteredDisbursementChronoValue", fields: [registeredValueOf], references: [id]) 286 | registeredValueOf String? 287 | registerValue DisbursementChronoValues? @relation("RegisterDisbursementChronoValue", fields: [registerValueOf], references: [id]) 288 | registerValueOf String? 289 | totalValue DisbursementChronoValues? @relation("TotalDisbursementChronoValue", fields: [totalValueOf], references: [id]) 290 | totalValueOf String? 291 | granting Float? @default(0) 292 | convenient Float? @default(0) 293 | yield Float? @default(0) 294 | 295 | @@map(name: "disbursement_chrono_value") 296 | } 297 | 298 | model DetailedApplicationPlan { 299 | id String @id @default(uuid()) 300 | workPlan WorkPlan? @relation(fields: [workPlanId], references: [id]) 301 | workPlanId String? 302 | list DetailedApplicationPlanItem[] 303 | values DetailedApplicationPlanValues? 304 | 305 | @@map(name: "detailed_application_plans") 306 | } 307 | 308 | model DetailedApplicationPlanItem { 309 | id String @id @default(uuid()) 310 | detailedApplicationPlan DetailedApplicationPlan? @relation(fields: [detailedApplicationPlanId], references: [id]) 311 | detailedApplicationPlanId String? 312 | type String? 313 | description String? 314 | natureExpenseCode Int? 315 | natureAcquisition String? 316 | un String? 317 | amount Float? @default(0) 318 | unitValue Float? @default(0) 319 | totalValue Float? @default(0) 320 | status String? 321 | 322 | @@map(name: "detailed_application_plan_items") 323 | } 324 | 325 | model DetailedApplicationPlanValues { 326 | id String @id @default(uuid()) 327 | detailedApplicationPlan DetailedApplicationPlan? @relation(fields: [detailedApplicationPlanId], references: [id]) 328 | detailedApplicationPlanId String? 329 | assets DetailedApplicationPlanValue? @relation("AssetsDetailedApplicationPlanValue") 330 | tributes DetailedApplicationPlanValue? @relation("TributesDetailedApplicationPlanValue") 331 | construction DetailedApplicationPlanValue? @relation("ConstructionDetailedApplicationPlanValue") 332 | services DetailedApplicationPlanValue? @relation("ServicesDetailedApplicationPlanValue") 333 | others DetailedApplicationPlanValue? @relation("OthersDetailedApplicationPlanValue") 334 | administrative DetailedApplicationPlanValue? @relation("AdministrativeDetailedApplicationPlanValue") 335 | total DetailedApplicationPlanValue? @relation("TotalDetailedApplicationPlanValue") 336 | 337 | @@map(name: "detailed_application_plan_values") 338 | } 339 | 340 | model DetailedApplicationPlanValue { 341 | id String @id @default(uuid()) 342 | assetsValue DetailedApplicationPlanValues? @relation("AssetsDetailedApplicationPlanValue", fields: [assetsValueOf], references: [id]) 343 | assetsValueOf String? 344 | tributesValue DetailedApplicationPlanValues? @relation("TributesDetailedApplicationPlanValue", fields: [tributesValueOf], references: [id]) 345 | tributesValueOf String? 346 | constructionValue DetailedApplicationPlanValues? @relation("ConstructionDetailedApplicationPlanValue", fields: [constructionValueOf], references: [id]) 347 | constructionValueOf String? 348 | servicesValue DetailedApplicationPlanValues? @relation("ServicesDetailedApplicationPlanValue", fields: [servicesValueOf], references: [id]) 349 | servicesValueOf String? 350 | othersValue DetailedApplicationPlanValues? @relation("OthersDetailedApplicationPlanValue", fields: [othersValueOf], references: [id]) 351 | othersValueOf String? 352 | administrativeValue DetailedApplicationPlanValues? @relation("AdministrativeDetailedApplicationPlanValue", fields: [administrativeValueOf], references: [id]) 353 | administrativeValueOf String? 354 | totalValue DetailedApplicationPlanValues? @relation("TotalDetailedApplicationPlanValue", fields: [totalValueId], references: [id]) 355 | totalValueId String? 356 | total Float? @default(0) 357 | resource Float? @default(0) 358 | counterpart Float? @default(0) 359 | yield Float? @default(0) 360 | 361 | @@map(name: "detailed_application_plan_value") 362 | } 363 | 364 | model ConsolidatedApplicationPlan { 365 | id String @id @default(uuid()) 366 | workPlan WorkPlan? @relation(fields: [workPlanId], references: [id]) 367 | workPlanId String? 368 | list ConsolidatedApplicationPlanItem[] @relation("ListConsolidatedApplicationPlanItems") 369 | total ConsolidatedApplicationPlanItem? @relation("TotalConsolidatedApplicationPlanItems") 370 | 371 | @@map(name: "consolidated_application_plans") 372 | } 373 | 374 | model ConsolidatedApplicationPlanItem { 375 | id String @id @default(uuid()) 376 | listItem ConsolidatedApplicationPlan[] @relation("ListConsolidatedApplicationPlanItems", fields: [listItemOf], references: [id]) 377 | listItemOf String? 378 | totalItem ConsolidatedApplicationPlan? @relation("TotalConsolidatedApplicationPlanItems", fields: [totalItemOf], references: [id]) 379 | totalItemOf String? 380 | classification String? 381 | resources Float? @default(0) 382 | counterpart Float? @default(0) 383 | yield Float? @default(0) 384 | total Float? @default(0) 385 | 386 | @@map(name: "consolidated_application_plan_items") 387 | } 388 | 389 | model Attachments { 390 | id String @id @default(uuid()) 391 | workPlan WorkPlan? @relation(fields: [workPlanId], references: [id]) 392 | workPlanId String? 393 | proposalList Attachment[] @relation("ProposalAttachments") 394 | executionList Attachment[] @relation("ExecutionAttachments") 395 | 396 | @@map(name: "attachments") 397 | } 398 | 399 | model Attachment { 400 | id String @id @default(uuid()) 401 | proposalAttachment Attachments[] @relation("ProposalAttachments", fields: [proposalAttachmentOf], references: [id]) 402 | proposalAttachmentOf String? 403 | executionAttachment Attachments[] @relation("ExecutionAttachments", fields: [executionAttachmentOf], references: [id]) 404 | executionAttachmentOf String? 405 | name String? 406 | description String? 407 | date DateTime? 408 | 409 | @@map(name: "attachment") 410 | } 411 | 412 | model Notions { 413 | id String @id @default(uuid()) 414 | workPlan WorkPlan? @relation(fields: [workPlanId], references: [id]) 415 | workPlanId String? 416 | proposalList NotionItem[] @relation("ProposalNotionItems") 417 | workPlanList NotionItem[] @relation("WorkPlanNotionItems") 418 | 419 | @@map(name: "notions") 420 | } 421 | 422 | model NotionItem { 423 | id String @id @default(uuid()) 424 | proposalNotionItem Notions[] @relation("ProposalNotionItems", fields: [proposalNotionItemOf], references: [id]) 425 | proposalNotionItemOf String? 426 | executionNotionItem Notions[] @relation("WorkPlanNotionItems", fields: [executionNotionItemOf], references: [id]) 427 | executionNotionItemOf String? 428 | date DateTime? 429 | type String? 430 | responsible String? 431 | assignment String? 432 | occupation String? 433 | 434 | @@map(name: "notion_item") 435 | } 436 | 437 | model ConvenientExecution { 438 | id String @id @default(uuid()) 439 | agreement Agreement? @relation(fields: [agreementId], references: [id]) 440 | agreementId String? 441 | executionProcesses ExecutionProcess[] 442 | contracts Contract[] 443 | 444 | @@map(name: "convenient_execution") 445 | } 446 | 447 | model ExecutionProcess { 448 | id String @id @default(uuid()) 449 | convenientExecution ConvenientExecution? @relation(fields: [convenientExecutionId], references: [id]) 450 | convenientExecutionId String? 451 | executionId String? 452 | type String? 453 | date DateTime? 454 | processId String? 455 | status String? 456 | systemStatus String? 457 | system String? 458 | accepted String? 459 | details ExecutionProcessDetails? 460 | 461 | @@map(name: "execution_processes") 462 | } 463 | 464 | model ExecutionProcessDetails { 465 | id String @id @default(uuid()) 466 | executionProcessRel ExecutionProcess? @relation(fields: [executionProcessRelId], references: [id]) 467 | executionProcessRelId String? 468 | executionProcess String? 469 | buyType String? 470 | status String? 471 | origin String? 472 | financialResource String? 473 | modality String? 474 | biddingType String? 475 | processId String? 476 | biddingId String? 477 | object String? 478 | legalFoundation String? 479 | justification String? 480 | publishDate DateTime? 481 | beginDate DateTime? 482 | endDate DateTime? 483 | biddingValue Float? 484 | homologationDate DateTime? 485 | city String? 486 | analysisDate DateTime? 487 | accepted String? 488 | 489 | @@map(name: "execution_processes_details") 490 | } 491 | 492 | model Contract { 493 | id String @id @default(uuid()) 494 | convenientExecution ConvenientExecution? @relation(fields: [convenientExecutionId], references: [id]) 495 | convenientExecutionId String? 496 | contractId String? 497 | biddingId String? 498 | date DateTime? 499 | details ContractDetails? 500 | 501 | @@map(name: "contracts") 502 | } 503 | 504 | model ContractDetails { 505 | id String @id @default(uuid()) 506 | contract Contract? @relation(fields: [contractId], references: [id]) 507 | contractId String? 508 | hiredDocument String? 509 | hirerDocument String? 510 | type String? 511 | object String? 512 | totalValue Float? 513 | publishDate DateTime? 514 | beginDate DateTime? 515 | endDate DateTime? 516 | signDate DateTime? 517 | executionProcessId String? 518 | biddingModality String? 519 | processId String? 520 | 521 | @@map(name: "contracts_details") 522 | } 523 | 524 | model Accountability { 525 | id String @id @default(uuid()) 526 | agreement Agreement? @relation(fields: [agreementId], references: [id]) 527 | agreementId String? 528 | data AccountabilityData? 529 | 530 | @@map(name: "accountabilities") 531 | } 532 | 533 | model AccountabilityData { 534 | id String @id @default(uuid()) 535 | accountabilty Accountability? @relation(fields: [accountabiltyId], references: [id]) 536 | accountabiltyId String? 537 | organ String? 538 | convenient String? 539 | documentNumber String? 540 | modality String? 541 | status String? 542 | number String? 543 | validity String? 544 | limitDate DateTime? 545 | totalValue Float? 546 | transferValue Float? 547 | counterpartValue Float? 548 | yieldValue Float? 549 | 550 | @@map(name: "accountabilities_data") 551 | } 552 | -------------------------------------------------------------------------------- /test/fixtures/composite-types.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "mongodb" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | model Product { 7 | id String @id @default(auto()) @map("_id") @db.ObjectId 8 | name String 9 | photos Photo[] 10 | } 11 | 12 | type Photo { 13 | height Int @default(0) 14 | width Int @default(0) 15 | url String 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/empty-comment.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | model Product { 7 | id String @id() @default(uuid()) @db.Uuid 8 | // 9 | productNumber String @unique 10 | price Decimal @db.Decimal(19, 4) // 11 | unit String @db.VarChar(3) 12 | unitOfMeasurement UnitOfMeasurement @relation(fields: [unit], references: [unit]) 13 | salesOrderItems SaleOrderItem[] 14 | productTexts ProductText[] 15 | createdAt DateTime @default(now()) 16 | version Int @default(0) 17 | } 18 | 19 | model UnitOfMeasurement { 20 | name String 21 | unit String @unique @db.VarChar(3) 22 | products Product[] 23 | } 24 | 25 | model SaleOrderItem { 26 | productId String @id @db.Uuid 27 | product Product @relation(fields: [productId], references: [id]) 28 | } 29 | 30 | model ProductText { 31 | description String 32 | productId String @id @db.Uuid 33 | textType TextType 34 | product Product @relation(fields: [productId], references: [id]) 35 | } 36 | 37 | enum TextType { 38 | // 39 | MARKDOWN 40 | RICHTEXT // 41 | } 42 | -------------------------------------------------------------------------------- /test/fixtures/example.prisma: -------------------------------------------------------------------------------- 1 | // https://www.prisma.io/docs/concepts/components/prisma-schema 2 | // added some fields to test keyword ambiguous 3 | 4 | datasource db { 5 | url = env("DATABASE_URL") 6 | provider = "postgresql" 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | } 12 | 13 | model User { 14 | id Int @id @default(autoincrement()) 15 | createdAt DateTime @default(now()) 16 | email String @unique 17 | name String? 18 | role Role @default(USER) 19 | posts Post[] 20 | projects Project[] 21 | } 22 | 23 | model Post { 24 | id Int @id @default(autoincrement()) 25 | createdAt DateTime @default(now()) 26 | updatedAt DateTime @updatedAt 27 | published Boolean @default(false) 28 | title String @db.VarChar(255) 29 | author User? @relation(fields: [authorId], references: [id]) 30 | authorId Int? 31 | // keyword test 32 | model String 33 | generator String 34 | datasource String 35 | enum String // inline comment 36 | 37 | @@map("posts") 38 | } 39 | 40 | model Project { 41 | client User @relation(fields: [clientId], references: [id]) 42 | clientId Int 43 | projectCode String 44 | dueDate DateTime 45 | 46 | @@id([projectCode]) 47 | @@unique([clientId, projectCode]) 48 | @@index([dueDate]) 49 | } 50 | 51 | model Model { 52 | id Int @id 53 | 54 | @@ignore 55 | } 56 | 57 | enum Role { 58 | USER @map("usr") // basic role 59 | ADMIN @map("adm") // more powerful role 60 | 61 | @@map("roles") 62 | } 63 | 64 | model Indexed { 65 | id String @id(map: "PK_indexed") @db.UniqueIdentifier 66 | foo String @db.UniqueIdentifier 67 | bar String @db.UniqueIdentifier 68 | 69 | @@index([foo, bar(sort: Desc)], map: "IX_indexed_indexedFoo") 70 | } 71 | -------------------------------------------------------------------------------- /test/fixtures/kebab-case.prisma: -------------------------------------------------------------------------------- 1 | generator prisma-model-generator { 2 | provider = "node ./dist/apps/prisma-model-generator/src/generator.js" 3 | fileNamingStyle = "kebab" 4 | classNamingStyle = "pascal" 5 | output = "./generated/" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/keystonejs-weird.prisma: -------------------------------------------------------------------------------- 1 | model Book { 2 | id String @default(cuid()) @id 3 | slug String @default("")@unique 4 | title String @default("") 5 | status String? @default("draft") 6 | authors User[] @relation("Book_authors") 7 | genres Genre[] @relation("Book_genres") 8 | language String @default("ua") 9 | chapters Chapter[] @relation("Chapter_book") 10 | tags Tag[] @relation("Book_tags") 11 | cover Image? @relation("Book_cover", fields: [coverId], references: [id]) 12 | coverId String? @map("cover") 13 | @@index([coverId]) 14 | translationGroup TranslationGroup? @relation("Book_translationGroup", fields: [translationGroupId], references: [id]) 15 | translationGroupId String? @map("translationGroup") 16 | @@index([translationGroupId]) 17 | } -------------------------------------------------------------------------------- /test/fixtures/keystonejs.prisma: -------------------------------------------------------------------------------- 1 | 2 | model Book { 3 | id String @default(cuid()) @id 4 | slug String @default("") @unique 5 | title String @default("") 6 | status String? @default("draft") 7 | authors User[] @relation("Book_authors") 8 | genres Genre[] @relation("Book_genres") 9 | language String @default("ua") 10 | chapters Chapter[] @relation("Chapter_book") 11 | tags Tag[] @relation("Book_tags") 12 | cover Image? @relation("Book_cover", fields: [coverId], references: [id]) 13 | coverId String? @map("cover") 14 | translationGroup TranslationGroup? @relation("Book_translationGroup", fields: [translationGroupId], references: [id]) 15 | translationGroupId String? @map("translationGroup") 16 | 17 | @@index([coverId]) 18 | @@index([translationGroupId]) 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/links.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgres" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | binaryTargets = "native" 9 | } 10 | 11 | model UserProfile { 12 | userID String @id @unique 13 | 14 | /// A one liner 15 | bio String? 16 | 17 | /// Hrefs which show under the user 18 | links String[] @default([]) 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/redwood.prisma: -------------------------------------------------------------------------------- 1 | datasource DS { 2 | provider = "sqlite" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | binaryTargets = "native" 9 | } 10 | 11 | /// Define your own datamodels here and run `yarn redwood db save` to create 12 | /// migrations for them. 13 | model Post { 14 | /// this is the post id 15 | id Int @id @default(autoincrement()) 16 | title String 17 | slug String @unique 18 | author String 19 | body String 20 | image String? 21 | tags Tag[] 22 | postedAt DateTime? 23 | } 24 | 25 | model Tag { 26 | id Int @id @default(autoincrement()) 27 | name String @unique 28 | posts Post[] 29 | } 30 | 31 | model User { 32 | id Int @id @default(autoincrement()) 33 | name String? 34 | email String @unique 35 | isAdmin Boolean @default(false) 36 | } 37 | -------------------------------------------------------------------------------- /test/fixtures/star.prisma: -------------------------------------------------------------------------------- 1 | // an example from prisma with some atypical syntax to parse 2 | 3 | model Star { 4 | id Int @id @default(autoincrement()) 5 | position Unsupported("circle")? 6 | example1 Unsupported("circle") 7 | circle Unsupported("circle")? @default(dbgenerated("'<(10,4),11>'::circle")) 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/test.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgres" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | // this is foo 7 | generator foo { 8 | // it is a nexus 9 | provider = "nexus" 10 | previewFeatures = ["napi"] 11 | } 12 | 13 | model Bar { 14 | id Int @id @first(fields: [one, two]) @second(fields: ["three", "four"]) 15 | // this is not a break 16 | name String @unique 17 | 18 | supercalifragilisticexpialidocious String @default("it is something quite atrocious") 19 | owner String // test 20 | 21 | 22 | /// test 23 | 24 | 25 | alphabet String @db.VarChar(26) 26 | number Int 27 | } 28 | 29 | enum Role { 30 | USER 31 | ADMIN 32 | 33 | @@map("membership_role") 34 | @@db.special 35 | } 36 | -------------------------------------------------------------------------------- /test/fixtures/unsorted.prisma: -------------------------------------------------------------------------------- 1 | enum Role { 2 | ADMIN 3 | OWNER // similar to ADMIN, but can delete the project 4 | MEMBER 5 | USER // deprecated 6 | } 7 | 8 | datasource db { 9 | url = env("DATABASE_URL") 10 | provider = "postgresql" 11 | } 12 | 13 | // this is a comment 14 | model User { 15 | id Int @id @default(autoincrement()) 16 | createdAt DateTime @default(now()) 17 | email String @unique 18 | name String? 19 | role Role @default(MEMBER) 20 | } 21 | 22 | generator client { 23 | provider = "prisma-client-js" 24 | } 25 | 26 | model AppSetting { 27 | key String @id 28 | value Json 29 | } 30 | -------------------------------------------------------------------------------- /test/getSchema.test.ts: -------------------------------------------------------------------------------- 1 | import getConfig, { PrismaAstParserConfig } from '../src/getConfig'; 2 | import { type Field, getSchema, type BlockAttribute } from '../src/getSchema'; 3 | import { PrismaParser } from '../src/parser'; 4 | import { VisitorClassFactory } from '../src/visitor'; 5 | import { loadFixture, getFixtures } from './utils'; 6 | 7 | describe('getSchema', () => { 8 | for (const fixture of getFixtures()) { 9 | it(`parse ${fixture}`, async () => { 10 | const source = await loadFixture(fixture); 11 | const components = getSchema(source); 12 | 13 | expect(components).not.toBeUndefined(); 14 | expect(components).toMatchSnapshot(); 15 | }); 16 | } 17 | 18 | // https://github.com/MrLeebo/prisma-ast/issues/28 19 | describe('empty comments', () => { 20 | it('parses empty comments', async () => { 21 | const source = await loadFixture('empty-comment.prisma'); 22 | const schema = getSchema(source); 23 | 24 | for (const node of schema.list) { 25 | if (node.type !== 'model') continue; 26 | if (node.name !== 'Product') continue; 27 | 28 | for (const property of node.properties) { 29 | if (property.type === 'comment') { 30 | expect(property.text).toEqual('//'); 31 | return; 32 | } 33 | } 34 | } 35 | 36 | fail(); 37 | }); 38 | 39 | it('parses empty inline comments', async () => { 40 | const source = await loadFixture('empty-comment.prisma'); 41 | const schema = getSchema(source); 42 | 43 | for (const node of schema.list) { 44 | if (node.type !== 'model') continue; 45 | if (node.name !== 'Product') continue; 46 | 47 | for (const property of node.properties) { 48 | if (property.type === 'field' && property.comment != null) { 49 | expect(property.comment).toEqual('//'); 50 | return; 51 | } 52 | } 53 | } 54 | 55 | fail(); 56 | }); 57 | 58 | it('parses empty comments in enum', async () => { 59 | const source = await loadFixture('empty-comment.prisma'); 60 | const schema = getSchema(source); 61 | 62 | for (const node of schema.list) { 63 | if (node.type !== 'enum') continue; 64 | if (node.name !== 'TextType') continue; 65 | 66 | for (const property of node.enumerators) { 67 | if (property.type === 'comment') { 68 | expect(property.text).toEqual('//'); 69 | return; 70 | } 71 | } 72 | } 73 | 74 | fail(); 75 | }); 76 | 77 | it('parses empty inline enum comments', async () => { 78 | const source = await loadFixture('empty-comment.prisma'); 79 | const schema = getSchema(source); 80 | 81 | for (const node of schema.list) { 82 | if (node.type !== 'enum') continue; 83 | if (node.name !== 'TextType') continue; 84 | 85 | for (const property of node.enumerators) { 86 | if (property.type === 'enumerator' && property.comment != null) { 87 | expect(property.comment).toEqual('//'); 88 | return; 89 | } 90 | } 91 | } 92 | 93 | fail(); 94 | }); 95 | }); 96 | 97 | it('parses composite type', async () => { 98 | const source = await loadFixture('composite-types.prisma'); 99 | const schema = getSchema(source); 100 | 101 | for (const node of schema.list) { 102 | if (node.type === 'type' && node.name === 'Photo') { 103 | const [height, width, url] = node.properties; 104 | expect(height).toMatchObject({ 105 | type: 'field', 106 | name: 'height', 107 | fieldType: 'Int', 108 | }); 109 | expect(width).toMatchObject({ 110 | type: 'field', 111 | name: 'width', 112 | fieldType: 'Int', 113 | }); 114 | expect(url).toMatchObject({ 115 | type: 'field', 116 | name: 'url', 117 | fieldType: 'String', 118 | }); 119 | return; 120 | } 121 | } 122 | 123 | fail(); 124 | }); 125 | 126 | describe('with location tracking', () => { 127 | describe('passed-in parser and visitor', () => { 128 | it('contains field location info', async () => { 129 | const source = await loadFixture('example.prisma'); 130 | const config: PrismaAstParserConfig = { 131 | nodeLocationTracking: 'full', 132 | }; 133 | const parser = new PrismaParser(config); 134 | const VisitorClass = VisitorClassFactory(parser); 135 | const visitor = new VisitorClass(parser); 136 | const components = getSchema(source, { 137 | parser, 138 | visitor, 139 | }); 140 | const field = getField(); 141 | expect(field).toHaveProperty('location.startLine', 14); 142 | expect(field).toHaveProperty('location.startColumn', 3); 143 | // TODO: these offsets are OS-specific, due to the differing length of the line endings 144 | expect(field).toHaveProperty('location.startOffset'); 145 | expect(field).toHaveProperty('location.endLine', 14); 146 | expect(field).toHaveProperty('location.endColumn', 4); 147 | expect(field).toHaveProperty('location.endOffset'); 148 | 149 | function getField(): Field | null { 150 | for (const component of components.list) { 151 | if (component.type === 'model') { 152 | const field = component.properties.find( 153 | (field) => field.type === 'field' 154 | ) as Field; 155 | if (field) return field; 156 | } 157 | } 158 | return null; 159 | } 160 | }); 161 | }); 162 | 163 | describe('static config', () => { 164 | beforeAll(() => { 165 | getConfig().parser.nodeLocationTracking = 'full'; 166 | }); 167 | 168 | afterAll(() => { 169 | getConfig().parser.nodeLocationTracking = 'none'; 170 | }); 171 | 172 | it('contains block attribute location info', async () => { 173 | const source = await loadFixture('example.prisma'); 174 | const components = getSchema(source); 175 | const attr = getBlockAttribute(); 176 | expect(attr).toHaveProperty('location.startLine', 37); 177 | expect(attr).toHaveProperty('location.startColumn', 3); 178 | expect(attr).toHaveProperty('location.startOffset'); 179 | expect(attr).toHaveProperty('location.endLine', 37); 180 | expect(attr).toHaveProperty('location.endColumn', 7); 181 | expect(attr).toHaveProperty('location.endOffset'); 182 | 183 | function getBlockAttribute(): BlockAttribute | null { 184 | for (const component of components.list) { 185 | if (component.type === 'model' && component.name === 'Post') { 186 | const attr = component.properties.find( 187 | (attr) => attr.type === 'attribute' 188 | ); 189 | if (attr) return attr as BlockAttribute; 190 | } 191 | } 192 | return null; 193 | } 194 | }); 195 | }); 196 | }); 197 | 198 | describe('without location tracking', () => { 199 | it('does not contain location info', async () => { 200 | const source = await loadFixture('example.prisma'); 201 | const components = getSchema(source); 202 | expect(components).not.toHaveProperty('list[0].location.startLine'); 203 | expect(components).not.toHaveProperty('list[0].location.startColumn'); 204 | expect(components).not.toHaveProperty('list[0].location.startOffset'); 205 | expect(components).not.toHaveProperty('list[0].location.endLine'); 206 | expect(components).not.toHaveProperty('list[0].location.endColumn'); 207 | expect(components).not.toHaveProperty('list[0].location.endOffset'); 208 | }); 209 | }); 210 | }); 211 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as ast from '../src'; 2 | 3 | describe('@mrleebo/prisma-ast', () => { 4 | it.each([ 5 | [ast.produceSchema], 6 | [ast.getSchema], 7 | [ast.printSchema], 8 | [ast.createPrismaSchemaBuilder], 9 | ])('exports expected functions', (importFn) => { 10 | expect(typeof importFn).toBe('function'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/printSchema.test.ts: -------------------------------------------------------------------------------- 1 | import { getSchema, printSchema } from '../src'; 2 | import { loadFixture, getFixtures } from './utils'; 3 | 4 | describe('printSchema', () => { 5 | for (const fixture of getFixtures()) { 6 | it(`print ${fixture}`, async () => { 7 | const source = await loadFixture(fixture); 8 | const schema = getSchema(source); 9 | expect(schema).not.toBeUndefined(); 10 | expect(printSchema(schema)).toMatchSnapshot(); 11 | }); 12 | } 13 | 14 | it('prints windows-style line breaks', () => { 15 | const source = ` 16 | model Foo { 17 | one Int\r\n\r\n\r\ntwo String 18 | } 19 | `; 20 | const schema = getSchema(source); 21 | expect(schema).not.toBeUndefined(); 22 | expect(printSchema(schema)).toMatchSnapshot(); 23 | }); 24 | 25 | it('re-sorts the schema', async () => { 26 | const source = await loadFixture('unsorted.prisma'); 27 | const schema = getSchema(source); 28 | expect(schema).not.toBeUndefined(); 29 | expect(printSchema(schema, { sort: true, locales: 'en-US' })) 30 | .toMatchInlineSnapshot(` 31 | " 32 | generator client { 33 | provider = "prisma-client-js" 34 | } 35 | 36 | datasource db { 37 | url = env("DATABASE_URL") 38 | provider = "postgresql" 39 | } 40 | 41 | model AppSetting { 42 | key String @id 43 | value Json 44 | } 45 | 46 | model User { 47 | id Int @id @default(autoincrement()) 48 | createdAt DateTime @default(now()) 49 | email String @unique 50 | name String? 51 | role Role @default(MEMBER) 52 | } 53 | 54 | enum Role { 55 | ADMIN 56 | OWNER // similar to ADMIN, but can delete the project 57 | MEMBER 58 | USER // deprecated 59 | } 60 | // this is a comment 61 | " 62 | `); 63 | }); 64 | 65 | it('re-sorts the schema with a custom sort order', async () => { 66 | const source = await loadFixture('unsorted.prisma'); 67 | const schema = getSchema(source); 68 | expect(schema).not.toBeUndefined(); 69 | expect( 70 | printSchema(schema, { 71 | sort: true, 72 | locales: undefined, 73 | sortOrder: ['generator', 'datasource', 'model', 'enum'], 74 | }) 75 | ).toMatchInlineSnapshot(` 76 | " 77 | generator client { 78 | provider = "prisma-client-js" 79 | } 80 | 81 | datasource db { 82 | url = env("DATABASE_URL") 83 | provider = "postgresql" 84 | } 85 | 86 | model AppSetting { 87 | key String @id 88 | value Json 89 | } 90 | 91 | model User { 92 | id Int @id @default(autoincrement()) 93 | createdAt DateTime @default(now()) 94 | email String @unique 95 | name String? 96 | role Role @default(MEMBER) 97 | } 98 | 99 | enum Role { 100 | ADMIN 101 | OWNER // similar to ADMIN, but can delete the project 102 | MEMBER 103 | USER // deprecated 104 | } 105 | // this is a comment 106 | " 107 | `); 108 | }); 109 | 110 | it('prints block attributes at the end', async () => { 111 | const source = await loadFixture('keystonejs-weird.prisma'); 112 | const expected = await loadFixture('keystonejs.prisma'); 113 | const schema = getSchema(source); 114 | const result = printSchema(schema).replace(/\r\n/g, '\n'); 115 | expect(schema).not.toBeUndefined(); 116 | expect(result).toEqual(expected.replace(/\r\n/g, '\n')); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /test/produceSchema.test.ts: -------------------------------------------------------------------------------- 1 | import { produceSchema } from '../src/produceSchema'; 2 | 3 | describe('produceSchema', () => { 4 | it('prints the schema', () => { 5 | const result = produceSchema( 6 | '', 7 | (builder) => { 8 | builder 9 | .datasource('postgresql', { env: 'DATABASE_URL' }) 10 | .generator('client', 'prisma-client-js') 11 | .enum('Role', ['USER', 'ADMIN']) 12 | .model('User') 13 | .field('id', 'Int') 14 | .attribute('id') 15 | .attribute('default', [{ name: 'autoincrement' }]) 16 | .field('name', 'String') 17 | .attribute('unique') 18 | .model('AppSetting') 19 | .field('key', 'String') 20 | .attribute('id') 21 | .field('value', 'Json') 22 | .blockAttribute('index', ['key']); 23 | }, 24 | { sort: true } 25 | ); 26 | 27 | expect(result).toMatchInlineSnapshot(` 28 | " 29 | generator client { 30 | provider = "prisma-client-js" 31 | } 32 | 33 | datasource db { 34 | url = env("DATABASE_URL") 35 | provider = postgresql 36 | } 37 | 38 | model AppSetting { 39 | key String @id 40 | value Json 41 | 42 | @@index([key]) 43 | } 44 | 45 | model User { 46 | id Int @id @default(autoincrement()) 47 | name String @unique 48 | } 49 | 50 | enum Role { 51 | USER 52 | ADMIN 53 | } 54 | " 55 | `); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import { readFile, readdirSync } from 'fs'; 2 | import { join } from 'path'; 3 | import { promisify } from 'util'; 4 | 5 | const readFileAsync = promisify(readFile); 6 | 7 | export function loadFixture(name: string): Promise { 8 | const fixturePath = join(__dirname, 'fixtures', name); 9 | return readFileAsync(fixturePath, { encoding: 'utf-8' }); 10 | } 11 | 12 | export function getFixtures(): string[] { 13 | const fixtureDir = join(__dirname, 'fixtures'); 14 | return readdirSync(fixtureDir); 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `dts build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true, 34 | "removeComments": true 35 | } 36 | } 37 | --------------------------------------------------------------------------------