├── .editorconfig ├── .gitignore ├── .husky └── pre-commit ├── .nvmrc ├── .prettierignore ├── LICENSE ├── README.md ├── eslint.config.mjs ├── jest.config.js ├── package-lock.json ├── package.json ├── scripts └── generate-peggy.sh ├── src ├── access.test.ts ├── access.ts ├── ast.ts ├── attributes.test.ts ├── attributes.ts ├── format.test.ts ├── format.ts ├── grammar.pegjs ├── index.test.ts ├── index.ts ├── parse.test.ts ├── parse.ts ├── visit.test.ts └── visit.ts ├── test-data ├── ast.ts └── schema.prisma ├── tsconfig.build.json ├── tsconfig.eslint.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __generated__/ 2 | coverage/ 3 | dist/ 4 | node_modules/ 5 | *.tgz 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22.11.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | __generated__/ 2 | coverage/ 3 | dist/ 4 | node_modules/ 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2022, Loan Crate, Inc. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prisma Schema Parser and Formatter 2 | 3 | Typescript library for parsing, traversing, and formatting 4 | [Prisma schema](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference) files. 5 | Based on the [PEG](https://en.wikipedia.org/wiki/Parsing_expression_grammar) 6 | [grammar](https://github.com/prisma/prisma-engines/blob/main/libs/datamodel/schema-ast/src/parser/datamodel.pest) in the Prisma source 7 | (ported from [pest](https://pest.rs/) to [PEG.js](https://pegjs.org/)), 8 | its goal is to parse any valid Prisma schema. 9 | Unit tests ensure 100% coverage of the hand-written Typescript code and the rule code generated by PEG.js. 10 | (There is a small amount of error reporting and unused feature code in the generated parser that is either unreachable or infeasible to test.) 11 | 12 | ## Goals 13 | 14 | - Parse and format any valid Prisma schema 15 | - Provide a complete and statically typed abstract syntax tree (AST) 16 | - Support legacy features like type aliases and GraphQL-style required type and list syntax 17 | - Preserve all non-whitespace constructs in the source, including comments 18 | - Preserve the source location of all high-level constructs 19 | - Provide utility functions to traverse and analyze the AST 20 | 21 | ### Non-goals 22 | 23 | - Continued parsing of invalid schemas beyond the first syntax error 24 | - Validation of the schema, such as type resolution or database-specific features 25 | - Preservation of whitespace 26 | 27 | ## Installation 28 | 29 | ```sh 30 | npm add @loancrate/prisma-schema-parser 31 | ``` 32 | 33 | ## Usage 34 | 35 | ```ts 36 | import { readFileSync } from "fs"; 37 | import { formatAst, parsePrismaSchema } from "@loancrate/prisma-schema-parser"; 38 | 39 | const ast = parsePrismaSchema( 40 | readFileSync("test-data/schema.prisma", { encoding: "utf8" }), 41 | ); 42 | // ... manipulate the schema ... 43 | console.log(formatAst(ast)); 44 | ``` 45 | 46 | ## License 47 | 48 | This library is available under the [ISC license](LICENSE). 49 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import globals from "globals"; 3 | import tsParser from "@typescript-eslint/parser"; 4 | import path from "node:path"; 5 | import { fileURLToPath } from "node:url"; 6 | import js from "@eslint/js"; 7 | import { FlatCompat } from "@eslint/eslintrc"; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all, 15 | }); 16 | 17 | export default [ 18 | { 19 | ignores: ["**/dist", "**/__generated__"], 20 | }, 21 | ...compat.extends( 22 | "eslint:recommended", 23 | "plugin:@typescript-eslint/recommended", 24 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 25 | ), 26 | { 27 | plugins: { 28 | "@typescript-eslint": typescriptEslint, 29 | }, 30 | 31 | languageOptions: { 32 | globals: { 33 | ...globals.node, 34 | }, 35 | 36 | parser: tsParser, 37 | ecmaVersion: 5, 38 | sourceType: "commonjs", 39 | 40 | parserOptions: { 41 | tsconfigRootDir: "/Users/trevor/git/prisma-schema-parser", 42 | project: ["./tsconfig.eslint.json"], 43 | }, 44 | }, 45 | 46 | rules: { 47 | "no-console": "warn", 48 | }, 49 | }, 50 | ]; 51 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | collectCoverage: true, 6 | collectCoverageFrom: ["src/*.ts"], 7 | coverageThreshold: { 8 | global: { 9 | statements: 100, 10 | branches: 100, 11 | functions: 100, 12 | lines: 100, 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@loancrate/prisma-schema-parser", 3 | "version": "3.0.0", 4 | "description": "Prisma Schema Parser", 5 | "keywords": [ 6 | "parser", 7 | "prisma", 8 | "schema" 9 | ], 10 | "homepage": "https://github.com/loancrate/prisma-schema-parser#readme", 11 | "bugs": { 12 | "url": "https://github.com/loancrate/prisma-schema-parser/issues" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/loancrate/prisma-schema-parser.git" 17 | }, 18 | "license": "ISC", 19 | "author": "Trevor Robinson", 20 | "files": [ 21 | "dist" 22 | ], 23 | "main": "dist/index.js", 24 | "types": "dist/index.d.ts", 25 | "scripts": { 26 | "build": "rm -rf dist && tsc --project tsconfig.build.json", 27 | "lint": "eslint src", 28 | "generate-peggy": "./scripts/generate-peggy.sh", 29 | "prepare": "npm run generate-peggy && npm run build", 30 | "prepublishOnly": "npm run test && npm run lint", 31 | "prettier": "prettier . --write", 32 | "test": "jest" 33 | }, 34 | "dependencies": { 35 | "catch-unknown": "^2.0.0", 36 | "error-cause": "^1.0.8", 37 | "no-case": "^3.0.4", 38 | "type-fest": "^4.30.2" 39 | }, 40 | "devDependencies": { 41 | "@tsconfig/node22": "^22.0.0", 42 | "@types/error-cause": "^1.0.4", 43 | "@types/jest": "^29.5.14", 44 | "@types/node": "^22.10.2", 45 | "@typescript-eslint/eslint-plugin": "^8.18.1", 46 | "@typescript-eslint/parser": "^8.18.1", 47 | "eslint": "^9.17.0", 48 | "husky": "^9.1.7", 49 | "jest": "^29.7.0", 50 | "lint-staged": "^15.2.11", 51 | "peggy": "^4.2.0", 52 | "prettier": "^3.4.2", 53 | "ts-jest": "^29.2.5", 54 | "typescript": "^5.7.2" 55 | }, 56 | "lint-staged": { 57 | "*.{ts,md}": "prettier --list-different", 58 | "*.ts": "eslint" 59 | }, 60 | "publishConfig": { 61 | "access": "public" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /scripts/generate-peggy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | mkdir -p src/__generated__ 6 | 7 | peggy --allowed-start-rules schema,field_type,expression -o src/__generated__/parser.js src/grammar.pegjs 8 | -------------------------------------------------------------------------------- /src/access.test.ts: -------------------------------------------------------------------------------- 1 | import { JsonArray, JsonObject } from "type-fest"; 2 | import { 3 | findAllAttributes, 4 | findFirstAttribute, 5 | getArgument, 6 | getArgumentValues, 7 | getDeclarationAttributes, 8 | getDeclarationName, 9 | hasBlockAttributes, 10 | hasFieldAttributes, 11 | readBooleanArgument, 12 | readFieldReferenceArgument, 13 | readFieldReferencesArgument, 14 | readNumberArgument, 15 | readStringArgument, 16 | } from "./access"; 17 | import { SchemaAttribute } from "./ast"; 18 | 19 | describe("getDeclarationName", () => { 20 | test("it", () => { 21 | expect( 22 | getDeclarationName({ 23 | kind: "datasource", 24 | name: { kind: "name", value: "db" }, 25 | members: [], 26 | }), 27 | ).toBe("datasource db"); 28 | expect( 29 | getDeclarationName({ 30 | kind: "commentBlock", 31 | comments: [], 32 | }), 33 | ).toBe("comment block"); 34 | }); 35 | }); 36 | 37 | describe("hasBlockAttributes", () => { 38 | test("true", () => { 39 | expect( 40 | hasBlockAttributes({ 41 | kind: "enum", 42 | name: { kind: "name", value: "x" }, 43 | members: [], 44 | }), 45 | ).toBe(true); 46 | }); 47 | test("false", () => { 48 | expect( 49 | hasBlockAttributes({ 50 | kind: "commentBlock", 51 | comments: [], 52 | }), 53 | ).toBe(false); 54 | }); 55 | }); 56 | 57 | describe("hasFieldAttributes", () => { 58 | test("true", () => { 59 | expect( 60 | hasFieldAttributes({ 61 | kind: "enumValue", 62 | name: { kind: "name", value: "x" }, 63 | }), 64 | ).toBe(true); 65 | }); 66 | test("false", () => { 67 | expect( 68 | hasFieldAttributes({ 69 | kind: "commentBlock", 70 | comments: [], 71 | }), 72 | ).toBe(false); 73 | }); 74 | }); 75 | 76 | describe("getDeclarationAttributes", () => { 77 | test("enum", () => { 78 | expect( 79 | getDeclarationAttributes({ 80 | kind: "enum", 81 | name: { kind: "name", value: "x" }, 82 | members: [ 83 | { kind: "blockAttribute", path: { kind: "path", value: ["map"] } }, 84 | { kind: "enumValue", name: { kind: "name", value: "y" } }, 85 | ], 86 | }), 87 | ).toStrictEqual([ 88 | { kind: "blockAttribute", path: { kind: "path", value: ["map"] } }, 89 | ]); 90 | }); 91 | test("enumValue", () => { 92 | expect( 93 | getDeclarationAttributes({ 94 | kind: "enumValue", 95 | name: { kind: "name", value: "x" }, 96 | attributes: [ 97 | { kind: "fieldAttribute", path: { kind: "path", value: ["map"] } }, 98 | ], 99 | }), 100 | ).toStrictEqual([ 101 | { kind: "fieldAttribute", path: { kind: "path", value: ["map"] } }, 102 | ]); 103 | }); 104 | test("commentBlock", () => { 105 | expect( 106 | getDeclarationAttributes({ 107 | kind: "commentBlock", 108 | comments: [], 109 | }), 110 | ).toStrictEqual([]); 111 | }); 112 | }); 113 | 114 | describe("findFirstAttribute", () => { 115 | test("it", () => { 116 | expect( 117 | findFirstAttribute( 118 | [ 119 | { 120 | kind: "blockAttribute", 121 | path: { kind: "path", value: ["a"] }, 122 | args: [{ kind: "literal", value: 1 }], 123 | }, 124 | { 125 | kind: "blockAttribute", 126 | path: { kind: "path", value: ["b"] }, 127 | args: [{ kind: "literal", value: 2 }], 128 | }, 129 | { 130 | kind: "blockAttribute", 131 | path: { kind: "path", value: ["b"] }, 132 | args: [{ kind: "literal", value: 3 }], 133 | }, 134 | ], 135 | "b", 136 | ), 137 | ).toStrictEqual({ 138 | kind: "blockAttribute", 139 | path: { kind: "path", value: ["b"] }, 140 | args: [{ kind: "literal", value: 2 }], 141 | }); 142 | }); 143 | }); 144 | 145 | describe("findAllAttributes", () => { 146 | test("some", () => { 147 | expect( 148 | findAllAttributes( 149 | [ 150 | { 151 | kind: "blockAttribute", 152 | path: { kind: "path", value: ["a"] }, 153 | args: [{ kind: "literal", value: 1 }], 154 | }, 155 | { 156 | kind: "blockAttribute", 157 | path: { kind: "path", value: ["b"] }, 158 | args: [{ kind: "literal", value: 2 }], 159 | }, 160 | { 161 | kind: "blockAttribute", 162 | path: { kind: "path", value: ["b"] }, 163 | args: [{ kind: "literal", value: 3 }], 164 | }, 165 | ], 166 | "b", 167 | ), 168 | ).toStrictEqual([ 169 | { 170 | kind: "blockAttribute", 171 | path: { kind: "path", value: ["b"] }, 172 | args: [{ kind: "literal", value: 2 }], 173 | }, 174 | { 175 | kind: "blockAttribute", 176 | path: { kind: "path", value: ["b"] }, 177 | args: [{ kind: "literal", value: 3 }], 178 | }, 179 | ]); 180 | }); 181 | 182 | test("none", () => { 183 | expect(findAllAttributes(undefined, "x")).toStrictEqual([]); 184 | }); 185 | }); 186 | 187 | describe("getArgument", () => { 188 | test("not found", () => { 189 | expect(() => getArgument([], "name")).toThrow( 190 | 'Argument "name" is required', 191 | ); 192 | }); 193 | }); 194 | 195 | describe("readBooleanArgument", () => { 196 | test("boolean", () => { 197 | expect(readBooleanArgument({ kind: "literal", value: true })).toBe(true); 198 | }); 199 | 200 | test("not boolean", () => { 201 | expect(() => readBooleanArgument({ kind: "literal", value: 1 })).toThrow( 202 | "Boolean literal expected but got literal number", 203 | ); 204 | }); 205 | }); 206 | 207 | describe("readNumberArgument", () => { 208 | test("number", () => { 209 | expect(readNumberArgument({ kind: "literal", value: 1 })).toBe(1); 210 | }); 211 | 212 | test("not number", () => { 213 | expect(() => readNumberArgument({ kind: "literal", value: true })).toThrow( 214 | "Number literal expected but got literal boolean", 215 | ); 216 | }); 217 | }); 218 | 219 | describe("readStringArgument", () => { 220 | test("string", () => { 221 | expect(readStringArgument({ kind: "literal", value: "1" })).toBe("1"); 222 | }); 223 | 224 | test("not string", () => { 225 | expect(() => readStringArgument({ kind: "literal", value: 1 })).toThrow( 226 | "String literal expected but got literal number", 227 | ); 228 | }); 229 | }); 230 | 231 | describe("readFieldReferenceArgument", () => { 232 | test("field reference", () => { 233 | expect(readFieldReferenceArgument({ kind: "path", value: ["x"] })).toBe( 234 | "x", 235 | ); 236 | }); 237 | 238 | test("not field reference", () => { 239 | expect(() => 240 | readFieldReferenceArgument({ kind: "literal", value: "x" }), 241 | ).toThrow("Field reference expected but got literal string"); 242 | }); 243 | }); 244 | 245 | describe("readFieldReferencesArgument", () => { 246 | test("field references", () => { 247 | expect( 248 | readFieldReferencesArgument({ 249 | kind: "array", 250 | items: [ 251 | { kind: "path", value: ["x"] }, 252 | { kind: "path", value: ["y"] }, 253 | ], 254 | }), 255 | ).toStrictEqual(["x", "y"]); 256 | }); 257 | 258 | test("not field references", () => { 259 | expect(() => 260 | readFieldReferencesArgument({ kind: "literal", value: "x" }), 261 | ).toThrow("Field references expected but got literal string"); 262 | }); 263 | }); 264 | 265 | describe("getArgumentValues", () => { 266 | test("only named arguments", () => { 267 | expect( 268 | getArgumentValues([ 269 | { 270 | kind: "namedArgument", 271 | name: { kind: "name", value: "map" }, 272 | expression: { kind: "path", value: ["_id"] }, 273 | }, 274 | { 275 | kind: "namedArgument", 276 | name: { kind: "name", value: "sort" }, 277 | expression: { kind: "literal", value: "Desc" }, 278 | }, 279 | ]), 280 | ).toStrictEqual({ 281 | map: "_id", 282 | sort: "Desc", 283 | }); 284 | }); 285 | 286 | test("mixed arguments", () => { 287 | expect( 288 | getArgumentValues([ 289 | { 290 | kind: "array", 291 | items: [ 292 | { kind: "literal", value: 1 }, 293 | { kind: "literal", value: 2 }, 294 | ], 295 | }, 296 | { 297 | kind: "namedArgument", 298 | name: { kind: "name", value: "fn" }, 299 | expression: { 300 | kind: "functionCall", 301 | path: { kind: "path", value: ["now"] }, 302 | }, 303 | }, 304 | ]), 305 | ).toStrictEqual([[1, 2], "now()"]); 306 | }); 307 | }); 308 | -------------------------------------------------------------------------------- /src/access.ts: -------------------------------------------------------------------------------- 1 | import { noCase } from "no-case"; 2 | import { JsonArray, JsonObject, JsonValue } from "type-fest"; 3 | import { 4 | BlockAttribute, 5 | BlockAttributed, 6 | EnumDeclaration, 7 | FieldAttributed, 8 | ModelDeclaration, 9 | NamedArgument, 10 | PrismaDeclaration, 11 | SchemaArgument, 12 | SchemaAttribute, 13 | SchemaExpression, 14 | } from "./ast"; 15 | import { formatAst, joinPath } from "./format"; 16 | 17 | export function getDeclarationName(decl: PrismaDeclaration): string { 18 | switch (decl.kind) { 19 | case "datasource": 20 | case "enum": 21 | case "enumValue": 22 | case "field": 23 | case "generator": 24 | case "model": 25 | case "type": 26 | case "view": 27 | case "typeAlias": 28 | return `${noCase(decl.kind)} ${decl.name.value}`; 29 | case "commentBlock": 30 | return "comment block"; 31 | } 32 | } 33 | 34 | export function hasBlockAttributes( 35 | decl: PrismaDeclaration, 36 | ): decl is BlockAttributed { 37 | switch (decl.kind) { 38 | case "enum": 39 | case "model": 40 | case "type": 41 | case "view": 42 | return true; 43 | } 44 | return false; 45 | } 46 | 47 | export function hasFieldAttributes( 48 | decl: PrismaDeclaration, 49 | ): decl is FieldAttributed { 50 | switch (decl.kind) { 51 | case "enumValue": 52 | case "field": 53 | case "typeAlias": 54 | return true; 55 | } 56 | return false; 57 | } 58 | 59 | export function getDeclarationAttributes( 60 | decl: PrismaDeclaration, 61 | ): readonly SchemaAttribute[] { 62 | switch (decl.kind) { 63 | case "enum": 64 | return getEnumAttributes(decl); 65 | case "model": 66 | case "type": 67 | case "view": 68 | return getModelAttributes(decl); 69 | case "enumValue": 70 | case "field": 71 | case "typeAlias": 72 | return decl.attributes ?? []; 73 | default: 74 | return []; 75 | } 76 | } 77 | 78 | export function getModelAttributes(decl: ModelDeclaration): BlockAttribute[] { 79 | return decl.members.filter( 80 | (m): m is BlockAttribute => m.kind === "blockAttribute", 81 | ); 82 | } 83 | 84 | export function getEnumAttributes(decl: EnumDeclaration): BlockAttribute[] { 85 | return decl.members.filter( 86 | (m): m is BlockAttribute => m.kind === "blockAttribute", 87 | ); 88 | } 89 | 90 | export function findFirstAttribute( 91 | attributes: readonly SchemaAttribute[] | undefined, 92 | name: string, 93 | ): SchemaAttribute | undefined { 94 | return attributes?.find( 95 | (attribute) => joinPath(attribute.path.value) === name, 96 | ); 97 | } 98 | 99 | export function findAllAttributes( 100 | attributes: readonly SchemaAttribute[] | undefined, 101 | name: string, 102 | ): SchemaAttribute[] { 103 | return ( 104 | attributes?.filter( 105 | (attribute) => joinPath(attribute.path.value) === name, 106 | ) ?? [] 107 | ); 108 | } 109 | 110 | export function findArgument( 111 | args: readonly SchemaArgument[] | undefined, 112 | name: string, 113 | position?: number, 114 | ): NamedArgument | undefined { 115 | if (args) { 116 | const namedArg = args.find( 117 | (arg): arg is NamedArgument => 118 | arg.kind === "namedArgument" && arg.name.value === name, 119 | ); 120 | if (namedArg) { 121 | return namedArg; 122 | } 123 | const unnamedArgs = args.filter( 124 | (arg): arg is SchemaExpression => arg.kind !== "namedArgument", 125 | ); 126 | if (position != null && position < unnamedArgs.length) { 127 | const expression = unnamedArgs[position]; 128 | return { 129 | kind: "namedArgument", 130 | name: { kind: "name", value: name }, 131 | expression, 132 | }; 133 | } 134 | } 135 | } 136 | 137 | export function getArgument( 138 | args: readonly SchemaArgument[] | undefined, 139 | name: string, 140 | position?: number, 141 | ): NamedArgument { 142 | const arg = findArgument(args, name, position); 143 | if (!arg) { 144 | throw new Error(`Argument "${name}" is required`); 145 | } 146 | return arg; 147 | } 148 | 149 | export function getArgumentExpression(arg: SchemaArgument): SchemaExpression { 150 | return arg.kind === "namedArgument" ? arg.expression : arg; 151 | } 152 | 153 | export function asBooleanArgument( 154 | arg: SchemaArgument | undefined, 155 | ): boolean | undefined { 156 | if (arg) { 157 | const expr = getArgumentExpression(arg); 158 | if (expr.kind === "literal" && typeof expr.value === "boolean") { 159 | return expr.value; 160 | } 161 | } 162 | } 163 | 164 | export function readBooleanArgument(arg: SchemaArgument): boolean { 165 | const value = asBooleanArgument(arg); 166 | if (value === undefined) { 167 | throw getArgumentTypeError(arg, "Boolean literal"); 168 | } 169 | return value; 170 | } 171 | 172 | export function asNumberArgument( 173 | arg: SchemaArgument | undefined, 174 | ): number | undefined { 175 | if (arg) { 176 | const expr = getArgumentExpression(arg); 177 | if (expr.kind === "literal" && typeof expr.value === "number") { 178 | return expr.value; 179 | } 180 | } 181 | } 182 | 183 | export function readNumberArgument(arg: SchemaArgument): number { 184 | const value = asNumberArgument(arg); 185 | if (value === undefined) { 186 | throw getArgumentTypeError(arg, "Number literal"); 187 | } 188 | return value; 189 | } 190 | 191 | export function asStringArgument( 192 | arg: SchemaArgument | undefined, 193 | ): string | undefined { 194 | if (arg) { 195 | const expr = getArgumentExpression(arg); 196 | if (expr.kind === "literal" && typeof expr.value === "string") { 197 | return expr.value; 198 | } 199 | } 200 | } 201 | 202 | export function readStringArgument(arg: SchemaArgument): string { 203 | const value = asStringArgument(arg); 204 | if (value === undefined) { 205 | throw getArgumentTypeError(arg, "String literal"); 206 | } 207 | return value; 208 | } 209 | 210 | export function asFieldReferenceArgument( 211 | arg: SchemaArgument | undefined, 212 | ): string | undefined { 213 | if (arg) { 214 | const expr = getArgumentExpression(arg); 215 | if (expr.kind === "path") { 216 | return joinPath(expr.value); 217 | } 218 | } 219 | } 220 | 221 | export function readFieldReferenceArgument(arg: SchemaArgument): string { 222 | const value = asFieldReferenceArgument(arg); 223 | if (value === undefined) { 224 | throw getArgumentTypeError(arg, "Field reference"); 225 | } 226 | return value; 227 | } 228 | 229 | export function asFieldReferencesArgument( 230 | arg: SchemaArgument | undefined, 231 | ): string[] | undefined { 232 | if (arg) { 233 | const expr = getArgumentExpression(arg); 234 | if (expr.kind === "array") { 235 | const items = expr.items.map(asFieldReferenceArgument); 236 | if (items.every((item): item is string => typeof item === "string")) { 237 | return items; 238 | } 239 | } 240 | } 241 | } 242 | 243 | export function readFieldReferencesArgument(arg: SchemaArgument): string[] { 244 | const value = asFieldReferencesArgument(arg); 245 | if (value === undefined) { 246 | throw getArgumentTypeError(arg, "Field references"); 247 | } 248 | return value; 249 | } 250 | 251 | export function getArgumentTypeError( 252 | arg: SchemaArgument, 253 | expectedType: string, 254 | ): Error { 255 | let message = `${expectedType} expected`; 256 | let { kind } = arg; 257 | if (arg.kind === "namedArgument") { 258 | message += ` for argument ${arg.name.value}`; 259 | ({ kind } = arg.expression); 260 | } 261 | message += ` but got ${noCase(kind)}`; 262 | if (arg.kind === "literal") { 263 | message += ` ${typeof arg.value}`; 264 | } 265 | return new Error(message); 266 | } 267 | 268 | export function getExpressionValue(expr: SchemaExpression): JsonValue { 269 | switch (expr.kind) { 270 | case "literal": 271 | return expr.value; 272 | case "path": 273 | return joinPath(expr.value); 274 | case "array": 275 | return expr.items.map(getExpressionValue); 276 | case "functionCall": 277 | return formatAst(expr); 278 | } 279 | } 280 | 281 | export function getArgumentValues( 282 | args: SchemaArgument[], 283 | ): JsonObject | JsonArray { 284 | if (args.every((arg): arg is NamedArgument => arg.kind === "namedArgument")) { 285 | return getArgumentValuesObject(args); 286 | } 287 | return getArgumentValuesArray(args); 288 | } 289 | 290 | export function getArgumentValuesArray(args: SchemaArgument[]): JsonArray { 291 | return args.map((arg) => 292 | getExpressionValue(arg.kind === "namedArgument" ? arg.expression : arg), 293 | ); 294 | } 295 | 296 | export function getArgumentValuesObject(args: NamedArgument[]): JsonObject { 297 | return Object.fromEntries( 298 | args.map((arg) => { 299 | const name = arg.name.value; 300 | const value = getExpressionValue(arg.expression); 301 | return [name, value]; 302 | }), 303 | ); 304 | } 305 | -------------------------------------------------------------------------------- /src/ast.ts: -------------------------------------------------------------------------------- 1 | export interface PrismaSchema { 2 | kind: "schema"; 3 | declarations: SchemaDeclaration[]; 4 | } 5 | 6 | export type PrismaDeclaration = 7 | | SchemaDeclaration 8 | | FieldDeclaration 9 | | EnumValue; 10 | 11 | export type SchemaDeclaration = 12 | | ModelDeclaration 13 | | EnumDeclaration 14 | | TypeAlias 15 | | ConfigBlock 16 | | CommentBlock; 17 | 18 | export interface ModelDeclaration { 19 | kind: "model" | "type" | "view"; 20 | name: NameNode; 21 | members: ModelDeclarationMember[]; 22 | location?: SourceRange; 23 | } 24 | 25 | export type ModelDeclarationMember = 26 | | FieldDeclaration 27 | | BlockAttribute 28 | | CommentBlock; 29 | 30 | export interface FieldDeclaration { 31 | kind: "field"; 32 | name: NameNode; 33 | type: PrismaType; 34 | attributes?: FieldAttribute[]; 35 | comment?: TrailingComment | null; 36 | location?: SourceRange; 37 | } 38 | 39 | export type PrismaType = BaseType | ListType | OptionalType | RequiredType; 40 | 41 | export type BaseType = TypeId | UnsupportedType; 42 | 43 | export interface TypeId { 44 | kind: "typeId"; 45 | name: NameNode; 46 | } 47 | 48 | export interface UnsupportedType { 49 | kind: "unsupported"; 50 | type: ScalarLiteral; 51 | } 52 | 53 | export interface ListType { 54 | kind: "list"; 55 | type: BaseType; 56 | } 57 | 58 | export interface OptionalType { 59 | kind: "optional"; 60 | type: BaseType; 61 | } 62 | 63 | export interface RequiredType { 64 | kind: "required"; 65 | type: BaseType; 66 | } 67 | 68 | export interface EnumDeclaration { 69 | kind: "enum"; 70 | name: NameNode; 71 | members: EnumDeclarationMember[]; 72 | location?: SourceRange; 73 | } 74 | 75 | export type EnumDeclarationMember = EnumValue | BlockAttribute | CommentBlock; 76 | 77 | export interface EnumValue { 78 | kind: "enumValue"; 79 | name: NameNode; 80 | attributes?: FieldAttribute[]; 81 | comment?: TrailingComment | null; 82 | location?: SourceRange; 83 | } 84 | 85 | export interface TypeAlias { 86 | kind: "typeAlias"; 87 | name: NameNode; 88 | type: BaseType; 89 | attributes?: FieldAttribute[]; 90 | location?: SourceRange; 91 | } 92 | 93 | export interface ConfigBlock { 94 | kind: "datasource" | "generator"; 95 | name: NameNode; 96 | members: ConfigBlockMember[]; 97 | location?: SourceRange; 98 | } 99 | 100 | export type ConfigBlockMember = Config | CommentBlock; 101 | 102 | export interface Config { 103 | kind: "config"; 104 | name: NameNode; 105 | value: SchemaExpression; 106 | comment?: TrailingComment | null; 107 | location?: SourceRange; 108 | } 109 | 110 | export type SchemaAttribute = BlockAttribute | FieldAttribute; 111 | 112 | export type BlockAttributed = ModelDeclaration | EnumDeclaration; 113 | 114 | export interface BlockAttribute { 115 | kind: "blockAttribute"; 116 | path: PathExpression; 117 | args?: SchemaArgument[]; 118 | comment?: TrailingComment | null; 119 | location?: SourceRange; 120 | } 121 | 122 | export type FieldAttributed = FieldDeclaration | EnumValue | TypeAlias; 123 | 124 | export interface FieldAttribute { 125 | kind: "fieldAttribute"; 126 | path: PathExpression; 127 | args?: SchemaArgument[]; 128 | location?: SourceRange; 129 | } 130 | 131 | export type SchemaArgument = NamedArgument | SchemaExpression; 132 | 133 | export interface NamedArgument { 134 | kind: "namedArgument"; 135 | name: NameNode; 136 | expression: SchemaExpression; 137 | } 138 | 139 | export interface NameNode { 140 | kind: "name"; 141 | value: string; 142 | location?: SourceRange; 143 | } 144 | 145 | export type SchemaExpression = 146 | | ScalarLiteral 147 | | PathExpression 148 | | ArrayExpression 149 | | FunctionCall; 150 | 151 | export interface ScalarLiteral { 152 | kind: "literal"; 153 | value: T; 154 | } 155 | 156 | export interface PathExpression { 157 | kind: "path"; 158 | value: string[]; 159 | location?: SourceRange; 160 | } 161 | 162 | export interface ArrayExpression { 163 | kind: "array"; 164 | items: SchemaExpression[]; 165 | } 166 | 167 | export interface FunctionCall { 168 | kind: "functionCall"; 169 | path: PathExpression; 170 | args?: SchemaArgument[]; 171 | } 172 | 173 | export interface CommentBlock { 174 | kind: "commentBlock"; 175 | comments: TrailingComment[]; 176 | } 177 | 178 | export type TrailingComment = Comment | DocComment; 179 | 180 | export interface Comment { 181 | kind: "comment"; 182 | text: string; 183 | location?: SourceRange; 184 | } 185 | 186 | export interface DocComment { 187 | kind: "docComment"; 188 | text: string; 189 | location?: SourceRange; 190 | } 191 | 192 | export interface SourceRange { 193 | start: SourceLocation; 194 | end: SourceLocation; 195 | } 196 | 197 | export interface SourceLocation { 198 | offset: number; 199 | line: number; 200 | column: number; 201 | } 202 | 203 | export type PrismaAstNode = 204 | | PrismaSchema 205 | | PrismaDeclaration 206 | | PrismaType 207 | | SchemaAttribute 208 | | SchemaArgument 209 | | Config 210 | | NameNode 211 | | TrailingComment; 212 | -------------------------------------------------------------------------------- /src/attributes.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DefaultFieldAttribute, 3 | findDefaultFieldAttribute, 4 | findIdBlockAttribute, 5 | findIdFieldAttribute, 6 | findIndexBlockAttributes, 7 | findMapBlockAttribute, 8 | findMapFieldAttribute, 9 | findRelationFieldAttribute, 10 | findUniqueBlockAttributes, 11 | findUniqueFieldAttribute, 12 | IdBlockAttribute, 13 | IdFieldAttribute, 14 | IndexBlockAttribute, 15 | MapBlockAttribute, 16 | MapFieldAttribute, 17 | RelationFieldAttribute, 18 | UniqueBlockAttribute, 19 | UniqueFieldAttribute, 20 | } from "./attributes"; 21 | 22 | describe("findIdFieldAttribute", () => { 23 | test("it", () => { 24 | expect( 25 | findIdFieldAttribute({ 26 | kind: "field", 27 | name: { kind: "name", value: "id" }, 28 | type: { kind: "typeId", name: { kind: "name", value: "String" } }, 29 | attributes: [ 30 | { 31 | kind: "fieldAttribute", 32 | path: { kind: "path", value: ["id"] }, 33 | args: [ 34 | { 35 | kind: "namedArgument", 36 | name: { kind: "name", value: "map" }, 37 | expression: { kind: "literal", value: "_id" }, 38 | }, 39 | { 40 | kind: "namedArgument", 41 | name: { kind: "name", value: "length" }, 42 | expression: { kind: "literal", value: 10 }, 43 | }, 44 | { 45 | kind: "namedArgument", 46 | name: { kind: "name", value: "sort" }, 47 | expression: { kind: "path", value: ["Desc"] }, 48 | }, 49 | { 50 | kind: "namedArgument", 51 | name: { kind: "name", value: "clustered" }, 52 | expression: { kind: "literal", value: true }, 53 | }, 54 | ], 55 | }, 56 | ], 57 | }), 58 | ).toStrictEqual({ 59 | map: "_id", 60 | length: 10, 61 | sort: "Desc", 62 | clustered: true, 63 | }); 64 | }); 65 | 66 | test("invalid sort", () => { 67 | expect(() => 68 | findIdFieldAttribute({ 69 | kind: "field", 70 | name: { kind: "name", value: "id" }, 71 | type: { kind: "typeId", name: { kind: "name", value: "String" } }, 72 | attributes: [ 73 | { 74 | kind: "fieldAttribute", 75 | path: { kind: "path", value: ["id"] }, 76 | args: [ 77 | { 78 | kind: "namedArgument", 79 | name: { kind: "name", value: "sort" }, 80 | expression: { kind: "path", value: ["none"] }, 81 | }, 82 | ], 83 | }, 84 | ], 85 | }), 86 | ).toThrow("Invalid attribute @id for field id: Invalid sort order: none"); 87 | }); 88 | }); 89 | 90 | describe("findIdBlockAttribute", () => { 91 | test("it", () => { 92 | expect( 93 | findIdBlockAttribute({ 94 | kind: "model", 95 | name: { kind: "name", value: "M" }, 96 | members: [ 97 | { 98 | kind: "blockAttribute", 99 | path: { kind: "path", value: ["id"] }, 100 | args: [ 101 | { 102 | kind: "array", 103 | items: [ 104 | { kind: "path", value: ["a"] }, 105 | { kind: "path", value: ["b"] }, 106 | ], 107 | }, 108 | ], 109 | }, 110 | ], 111 | }), 112 | ).toStrictEqual({ 113 | fields: [{ name: "a" }, { name: "b" }], 114 | name: undefined, 115 | map: undefined, 116 | clustered: undefined, 117 | }); 118 | }); 119 | }); 120 | 121 | describe("findDefaultFieldAttribute", () => { 122 | test("it", () => { 123 | expect( 124 | findDefaultFieldAttribute({ 125 | kind: "field", 126 | name: { kind: "name", value: "id" }, 127 | type: { kind: "typeId", name: { kind: "name", value: "String" } }, 128 | attributes: [ 129 | { 130 | kind: "fieldAttribute", 131 | path: { kind: "path", value: ["default"] }, 132 | args: [ 133 | { 134 | kind: "functionCall", 135 | path: { kind: "path", value: ["uuid"] }, 136 | args: [], 137 | }, 138 | ], 139 | }, 140 | ], 141 | }), 142 | ).toStrictEqual({ 143 | expression: { 144 | kind: "functionCall", 145 | path: { kind: "path", value: ["uuid"] }, 146 | args: [], 147 | }, 148 | map: undefined, 149 | }); 150 | }); 151 | }); 152 | 153 | describe("findUniqueFieldAttribute", () => { 154 | test("it", () => { 155 | expect( 156 | findUniqueFieldAttribute({ 157 | kind: "field", 158 | name: { kind: "name", value: "uniqueKey" }, 159 | type: { kind: "typeId", name: { kind: "name", value: "String" } }, 160 | attributes: [ 161 | { 162 | kind: "fieldAttribute", 163 | path: { kind: "path", value: ["unique"] }, 164 | }, 165 | ], 166 | }), 167 | ).toStrictEqual({ 168 | map: undefined, 169 | length: undefined, 170 | sort: undefined, 171 | clustered: undefined, 172 | }); 173 | }); 174 | }); 175 | 176 | describe("findUniqueBlockAttributes", () => { 177 | test("it", () => { 178 | expect( 179 | findUniqueBlockAttributes({ 180 | kind: "model", 181 | name: { kind: "name", value: "M" }, 182 | members: [ 183 | { 184 | kind: "blockAttribute", 185 | path: { kind: "path", value: ["unique"] }, 186 | args: [ 187 | { 188 | kind: "array", 189 | items: [ 190 | { kind: "path", value: ["a"] }, 191 | { kind: "path", value: ["b"] }, 192 | ], 193 | }, 194 | ], 195 | }, 196 | ], 197 | }), 198 | ).toStrictEqual([ 199 | { 200 | fields: [{ name: "a" }, { name: "b" }], 201 | name: undefined, 202 | map: undefined, 203 | clustered: undefined, 204 | }, 205 | ]); 206 | }); 207 | }); 208 | 209 | describe("findIndexBlockAttributes", () => { 210 | test("it", () => { 211 | expect( 212 | findIndexBlockAttributes({ 213 | kind: "model", 214 | name: { kind: "name", value: "M" }, 215 | members: [ 216 | { 217 | kind: "blockAttribute", 218 | path: { kind: "path", value: ["index"] }, 219 | args: [ 220 | { 221 | kind: "array", 222 | items: [ 223 | { kind: "path", value: ["a"] }, 224 | { 225 | kind: "functionCall", 226 | path: { kind: "path", value: ["b"] }, 227 | args: [ 228 | { 229 | kind: "namedArgument", 230 | name: { kind: "name", value: "ops" }, 231 | expression: { kind: "path", value: ["op"] }, 232 | }, 233 | ], 234 | }, 235 | ], 236 | }, 237 | { 238 | kind: "namedArgument", 239 | name: { kind: "name", value: "type" }, 240 | expression: { kind: "path", value: ["BTree"] }, 241 | }, 242 | ], 243 | }, 244 | { 245 | kind: "blockAttribute", 246 | path: { kind: "path", value: ["index"] }, 247 | args: [ 248 | { 249 | kind: "array", 250 | items: [ 251 | { kind: "path", value: ["c"] }, 252 | { 253 | kind: "functionCall", 254 | path: { kind: "path", value: ["d"] }, 255 | args: [ 256 | { 257 | kind: "namedArgument", 258 | name: { kind: "name", value: "ops" }, 259 | expression: { 260 | kind: "functionCall", 261 | path: { kind: "path", value: ["op"] }, 262 | }, 263 | }, 264 | ], 265 | }, 266 | ], 267 | }, 268 | ], 269 | }, 270 | { 271 | kind: "blockAttribute", 272 | path: { kind: "path", value: ["index"] }, 273 | args: [{ kind: "path", value: ["e"] }], 274 | }, 275 | ], 276 | }), 277 | ).toStrictEqual([ 278 | { 279 | fields: [ 280 | { name: "a" }, 281 | { name: "b", length: undefined, sort: undefined, ops: "op" }, 282 | ], 283 | name: undefined, 284 | map: undefined, 285 | clustered: undefined, 286 | type: "BTree", 287 | }, 288 | { 289 | fields: [ 290 | { name: "c" }, 291 | { name: "d", length: undefined, sort: undefined, ops: "op()" }, 292 | ], 293 | name: undefined, 294 | map: undefined, 295 | clustered: undefined, 296 | type: undefined, 297 | }, 298 | { 299 | fields: [{ name: "e" }], 300 | name: undefined, 301 | map: undefined, 302 | clustered: undefined, 303 | type: undefined, 304 | }, 305 | ]); 306 | }); 307 | 308 | test("invalid field", () => { 309 | expect(() => 310 | findIndexBlockAttributes({ 311 | kind: "model", 312 | name: { kind: "name", value: "M" }, 313 | members: [ 314 | { 315 | kind: "blockAttribute", 316 | path: { kind: "path", value: ["index"] }, 317 | args: [ 318 | { 319 | kind: "array", 320 | items: [{ kind: "literal", value: "a" }], 321 | }, 322 | ], 323 | location: { 324 | start: { offset: 647, line: 31, column: 3 }, 325 | end: { offset: 667, line: 31, column: 23 }, 326 | }, 327 | }, 328 | ], 329 | }), 330 | ).toThrow( 331 | "Invalid attribute @@index at 31:3-23 for model M: Field reference or function call expected but got literal", 332 | ); 333 | }); 334 | 335 | test("invalid ops", () => { 336 | expect(() => 337 | findIndexBlockAttributes({ 338 | kind: "model", 339 | name: { kind: "name", value: "M" }, 340 | members: [ 341 | { 342 | kind: "blockAttribute", 343 | path: { kind: "path", value: ["index"] }, 344 | args: [ 345 | { 346 | kind: "array", 347 | items: [ 348 | { kind: "path", value: ["a"] }, 349 | { 350 | kind: "functionCall", 351 | path: { kind: "path", value: ["b"] }, 352 | args: [ 353 | { 354 | kind: "namedArgument", 355 | name: { kind: "name", value: "ops" }, 356 | expression: { kind: "literal", value: 42 }, 357 | }, 358 | ], 359 | }, 360 | ], 361 | }, 362 | ], 363 | location: { 364 | start: { offset: 647, line: 31, column: 3 }, 365 | end: { offset: 667, line: 31, column: 23 }, 366 | }, 367 | }, 368 | ], 369 | }), 370 | ).toThrow( 371 | "Invalid attribute @@index at 31:3-23 for model M: Identifier or function call expected for argument ops but got literal", 372 | ); 373 | }); 374 | }); 375 | 376 | describe("findRelationFieldAttribute", () => { 377 | test("it", () => { 378 | expect( 379 | findRelationFieldAttribute({ 380 | kind: "field", 381 | name: { kind: "name", value: "parent" }, 382 | type: { kind: "typeId", name: { kind: "name", value: "MyModel" } }, 383 | attributes: [ 384 | { 385 | kind: "fieldAttribute", 386 | path: { kind: "path", value: ["relation"] }, 387 | args: [ 388 | { 389 | kind: "namedArgument", 390 | name: { kind: "name", value: "fields" }, 391 | expression: { 392 | kind: "array", 393 | items: [{ kind: "path", value: ["parentId"] }], 394 | }, 395 | }, 396 | { 397 | kind: "namedArgument", 398 | name: { kind: "name", value: "references" }, 399 | expression: { 400 | kind: "array", 401 | items: [{ kind: "path", value: ["id"] }], 402 | }, 403 | }, 404 | { 405 | kind: "namedArgument", 406 | name: { kind: "name", value: "onDelete" }, 407 | expression: { 408 | kind: "path", 409 | value: ["Restrict"], 410 | }, 411 | }, 412 | { 413 | kind: "namedArgument", 414 | name: { kind: "name", value: "onUpdate" }, 415 | expression: { 416 | kind: "path", 417 | value: ["Cascade"], 418 | }, 419 | }, 420 | ], 421 | }, 422 | ], 423 | }), 424 | ).toStrictEqual({ 425 | name: undefined, 426 | fields: ["parentId"], 427 | references: ["id"], 428 | onDelete: "Restrict", 429 | onUpdate: "Cascade", 430 | }); 431 | }); 432 | 433 | test("invalid referential action", () => { 434 | expect(() => 435 | findRelationFieldAttribute({ 436 | kind: "field", 437 | name: { kind: "name", value: "parent" }, 438 | type: { kind: "typeId", name: { kind: "name", value: "MyModel" } }, 439 | attributes: [ 440 | { 441 | kind: "fieldAttribute", 442 | path: { kind: "path", value: ["relation"] }, 443 | args: [ 444 | { 445 | kind: "namedArgument", 446 | name: { kind: "name", value: "onDelete" }, 447 | expression: { 448 | kind: "path", 449 | value: ["Explode"], 450 | }, 451 | }, 452 | ], 453 | }, 454 | ], 455 | }), 456 | ).toThrow( 457 | "Invalid attribute @relation for field parent: Invalid referential action: Explode", 458 | ); 459 | }); 460 | 461 | test("invalid referential action kind", () => { 462 | expect(() => 463 | findRelationFieldAttribute({ 464 | kind: "field", 465 | name: { kind: "name", value: "parent" }, 466 | type: { kind: "typeId", name: { kind: "name", value: "MyModel" } }, 467 | attributes: [ 468 | { 469 | kind: "fieldAttribute", 470 | path: { kind: "path", value: ["relation"] }, 471 | args: [ 472 | { 473 | kind: "namedArgument", 474 | name: { kind: "name", value: "onDelete" }, 475 | expression: { 476 | kind: "literal", 477 | value: "Cascade", 478 | }, 479 | }, 480 | ], 481 | }, 482 | ], 483 | }), 484 | ).toThrow( 485 | "Invalid attribute @relation for field parent: Referential action expected for argument onDelete but got literal", 486 | ); 487 | }); 488 | }); 489 | 490 | describe("findMapFieldAttribute", () => { 491 | test("one", () => { 492 | expect( 493 | findMapFieldAttribute({ 494 | kind: "field", 495 | name: { kind: "name", value: "id" }, 496 | type: { kind: "typeId", name: { kind: "name", value: "String" } }, 497 | attributes: [ 498 | { 499 | kind: "fieldAttribute", 500 | path: { kind: "path", value: ["map"] }, 501 | args: [{ kind: "literal", value: "_id" }], 502 | }, 503 | ], 504 | }), 505 | ).toStrictEqual({ 506 | name: "_id", 507 | }); 508 | }); 509 | test("none", () => { 510 | expect( 511 | findMapFieldAttribute({ 512 | kind: "field", 513 | name: { kind: "name", value: "id" }, 514 | type: { kind: "typeId", name: { kind: "name", value: "String" } }, 515 | }), 516 | ).toBeUndefined(); 517 | }); 518 | test("multiple", () => { 519 | expect(() => 520 | findMapFieldAttribute({ 521 | kind: "field", 522 | name: { kind: "name", value: "id" }, 523 | type: { kind: "typeId", name: { kind: "name", value: "String" } }, 524 | attributes: [ 525 | { 526 | kind: "fieldAttribute", 527 | path: { kind: "path", value: ["map"] }, 528 | args: [{ kind: "literal", value: "id1" }], 529 | }, 530 | { 531 | kind: "fieldAttribute", 532 | path: { kind: "path", value: ["map"] }, 533 | args: [{ kind: "literal", value: "id2" }], 534 | }, 535 | ], 536 | }), 537 | ).toThrow("Multiple instances of @map attribute"); 538 | }); 539 | }); 540 | 541 | describe("findMapBlockAttribute", () => { 542 | test("one", () => { 543 | expect( 544 | findMapBlockAttribute({ 545 | kind: "model", 546 | name: { kind: "name", value: "M" }, 547 | members: [ 548 | { 549 | kind: "blockAttribute", 550 | path: { kind: "path", value: ["map"] }, 551 | args: [{ kind: "literal", value: "m" }], 552 | }, 553 | ], 554 | }), 555 | ).toStrictEqual({ 556 | name: "m", 557 | }); 558 | }); 559 | test("none", () => { 560 | expect( 561 | findMapBlockAttribute({ 562 | kind: "model", 563 | name: { kind: "name", value: "M" }, 564 | members: [], 565 | }), 566 | ).toBeUndefined(); 567 | }); 568 | test("multiple", () => { 569 | expect(() => 570 | findMapBlockAttribute({ 571 | kind: "model", 572 | name: { kind: "name", value: "M" }, 573 | members: [ 574 | { 575 | kind: "blockAttribute", 576 | path: { kind: "path", value: ["map"] }, 577 | args: [{ kind: "literal", value: "m1" }], 578 | }, 579 | { 580 | kind: "blockAttribute", 581 | path: { kind: "path", value: ["map"] }, 582 | args: [{ kind: "literal", value: "m2" }], 583 | }, 584 | ], 585 | }), 586 | ).toThrow("Multiple instances of @@map attribute"); 587 | }); 588 | }); 589 | -------------------------------------------------------------------------------- /src/attributes.ts: -------------------------------------------------------------------------------- 1 | import { asError } from "catch-unknown"; 2 | 3 | import "error-cause/auto"; 4 | import { 5 | findAllAttributes, 6 | findArgument, 7 | getArgument, 8 | getArgumentExpression, 9 | getArgumentTypeError, 10 | getDeclarationAttributes, 11 | getDeclarationName, 12 | getModelAttributes, 13 | hasBlockAttributes, 14 | readBooleanArgument, 15 | readFieldReferenceArgument, 16 | readFieldReferencesArgument, 17 | readNumberArgument, 18 | readStringArgument, 19 | } from "./access"; 20 | import { 21 | EnumDeclaration, 22 | EnumValue, 23 | FieldDeclaration, 24 | ModelDeclaration, 25 | SchemaArgument, 26 | SchemaAttribute, 27 | SchemaExpression, 28 | } from "./ast"; 29 | import { formatAst, formatSourceRange, joinPath } from "./format"; 30 | 31 | export type SortOrder = "Asc" | "Desc"; 32 | 33 | export function isSortOrder(v: unknown): v is SortOrder { 34 | return v === "Asc" || v === "Desc"; 35 | } 36 | 37 | export function asSortOrder(s: string): SortOrder { 38 | if (!isSortOrder(s)) { 39 | throw new Error(`Invalid sort order: ${s}`); 40 | } 41 | return s; 42 | } 43 | 44 | export interface IdFieldAttribute { 45 | map?: string; 46 | length?: number; 47 | sort?: SortOrder; 48 | clustered?: boolean; 49 | } 50 | 51 | export function findIdFieldAttribute( 52 | decl: FieldDeclaration, 53 | ): IdFieldAttribute | undefined { 54 | return parseUniqueAttributeWith(decl, "id", parseIdFieldAttribute); 55 | } 56 | 57 | function parseIdFieldAttribute(attr: SchemaAttribute): IdFieldAttribute { 58 | return { 59 | map: applyOptional(findArgument(attr.args, "map"), readStringArgument), 60 | length: applyOptional( 61 | findArgument(attr.args, "length"), 62 | readNumberArgument, 63 | ), 64 | sort: applyOptional( 65 | applyOptional( 66 | findArgument(attr.args, "sort"), 67 | readFieldReferenceArgument, 68 | ), 69 | asSortOrder, 70 | ), 71 | clustered: applyOptional( 72 | findArgument(attr.args, "clustered"), 73 | readBooleanArgument, 74 | ), 75 | }; 76 | } 77 | 78 | export interface IndexField { 79 | name: string; 80 | length?: number; 81 | sort?: SortOrder; 82 | ops?: string; 83 | } 84 | 85 | function readFieldArgument(arg: SchemaArgument): IndexField { 86 | const expr = getArgumentExpression(arg); 87 | if (expr.kind === "path") { 88 | return { name: joinPath(expr.value) }; 89 | } 90 | if (expr.kind === "functionCall") { 91 | return { 92 | name: joinPath(expr.path.value), 93 | length: applyOptional( 94 | findArgument(expr.args, "length"), 95 | readNumberArgument, 96 | ), 97 | sort: applyOptional( 98 | applyOptional( 99 | findArgument(expr.args, "sort"), 100 | readFieldReferenceArgument, 101 | ), 102 | asSortOrder, 103 | ), 104 | ops: applyOptional(findArgument(expr.args, "ops"), readOpsArgument), 105 | }; 106 | } 107 | throw getArgumentTypeError(arg, "Field reference or function call"); 108 | } 109 | 110 | function readFieldsArgument(arg: SchemaArgument): IndexField[] { 111 | const expr = getArgumentExpression(arg); 112 | if (expr.kind === "array") { 113 | return expr.items.map(readFieldArgument); 114 | } 115 | return [readFieldArgument(arg)]; 116 | } 117 | 118 | export interface IdBlockAttribute { 119 | fields: IndexField[]; 120 | name?: string; 121 | map?: string; 122 | clustered?: boolean; 123 | } 124 | 125 | export function findIdBlockAttribute( 126 | decl: ModelDeclaration, 127 | ): IdBlockAttribute | undefined { 128 | return parseUniqueAttributeWith(decl, "id", parseIdBlockAttribute); 129 | } 130 | 131 | function parseIdBlockAttribute(attr: SchemaAttribute): IdBlockAttribute { 132 | return { 133 | fields: readFieldsArgument(getArgument(attr.args, "fields", 0)), 134 | name: applyOptional(findArgument(attr.args, "name"), readStringArgument), 135 | map: applyOptional(findArgument(attr.args, "map"), readStringArgument), 136 | clustered: applyOptional( 137 | findArgument(attr.args, "clustered"), 138 | readBooleanArgument, 139 | ), 140 | }; 141 | } 142 | 143 | export interface DefaultFieldAttribute { 144 | expression: SchemaExpression; 145 | map?: string; 146 | } 147 | 148 | export function findDefaultFieldAttribute( 149 | decl: FieldDeclaration, 150 | ): DefaultFieldAttribute | undefined { 151 | return parseUniqueAttributeWith(decl, "default", (attr) => ({ 152 | expression: getArgument(attr.args, "expression", 0).expression, 153 | map: applyOptional(findArgument(attr.args, "map"), readStringArgument), 154 | })); 155 | } 156 | 157 | export interface UniqueFieldAttribute { 158 | map?: string; 159 | length?: number; 160 | sort?: SortOrder; 161 | clustered?: boolean; 162 | } 163 | 164 | export function findUniqueFieldAttribute( 165 | decl: FieldDeclaration, 166 | ): UniqueFieldAttribute | undefined { 167 | return parseUniqueAttributeWith(decl, "unique", parseIdFieldAttribute); 168 | } 169 | 170 | export interface UniqueBlockAttribute { 171 | fields: IndexField[]; 172 | name?: string; 173 | map?: string; 174 | clustered?: boolean; 175 | } 176 | 177 | export function findUniqueBlockAttributes( 178 | decl: ModelDeclaration, 179 | ): UniqueBlockAttribute[] { 180 | return parseAttributesWith(decl, "unique", parseIdBlockAttribute); 181 | } 182 | 183 | export interface IndexBlockAttribute { 184 | fields: IndexField[]; 185 | name?: string; 186 | map?: string; 187 | clustered?: boolean; 188 | type?: string; 189 | } 190 | 191 | export function findIndexBlockAttributes( 192 | decl: ModelDeclaration, 193 | ): IndexBlockAttribute[] { 194 | return parseAttributesWith(decl, "index", (attr) => ({ 195 | ...parseIdBlockAttribute(attr), 196 | type: applyOptional( 197 | findArgument(attr.args, "type"), 198 | readFieldReferenceArgument, 199 | ), 200 | })); 201 | } 202 | 203 | export function readOpsArgument(arg: SchemaArgument): string { 204 | const expr = getArgumentExpression(arg); 205 | if (expr.kind === "path") { 206 | return joinPath(expr.value); 207 | } 208 | if (expr.kind === "functionCall") { 209 | return formatAst(expr); 210 | } 211 | throw getArgumentTypeError(arg, "Identifier or function call"); 212 | } 213 | 214 | const referentialActions = [ 215 | "Cascade", 216 | "Restrict", 217 | "NoAction", 218 | "SetNull", 219 | "SetDefault", 220 | ] as const; 221 | 222 | export type ReferentialAction = (typeof referentialActions)[number]; 223 | 224 | export function isReferentialAction(v: unknown): v is ReferentialAction { 225 | return referentialActions.some((a) => a === v); 226 | } 227 | 228 | export function asReferentialAction(s: string): ReferentialAction { 229 | if (!isReferentialAction(s)) { 230 | throw new Error(`Invalid referential action: ${s}`); 231 | } 232 | return s; 233 | } 234 | 235 | export interface RelationFieldAttribute { 236 | name?: string; 237 | fields?: string[]; 238 | references?: string[]; 239 | onDelete?: ReferentialAction; 240 | onUpdate?: ReferentialAction; 241 | } 242 | 243 | export function findRelationFieldAttribute( 244 | decl: FieldDeclaration, 245 | ): RelationFieldAttribute | undefined { 246 | return parseUniqueAttributeWith(decl, "relation", (attr) => ({ 247 | name: applyOptional(findArgument(attr.args, "name", 0), readStringArgument), 248 | fields: applyOptional( 249 | findArgument(attr.args, "fields"), 250 | readFieldReferencesArgument, 251 | ), 252 | references: applyOptional( 253 | findArgument(attr.args, "references"), 254 | readFieldReferencesArgument, 255 | ), 256 | onDelete: applyOptional( 257 | findArgument(attr.args, "onDelete"), 258 | readReferentialActionArgument, 259 | ), 260 | onUpdate: applyOptional( 261 | findArgument(attr.args, "onUpdate"), 262 | readReferentialActionArgument, 263 | ), 264 | })); 265 | } 266 | 267 | export function readReferentialActionArgument( 268 | arg: SchemaArgument, 269 | ): ReferentialAction { 270 | const expr = getArgumentExpression(arg); 271 | if (expr.kind === "path") { 272 | return asReferentialAction(joinPath(expr.value)); 273 | } 274 | throw getArgumentTypeError(arg, "Referential action"); 275 | } 276 | 277 | export interface MapFieldAttribute { 278 | name: string; 279 | } 280 | 281 | export function findMapFieldAttribute( 282 | decl: FieldDeclaration | EnumValue, 283 | ): MapFieldAttribute | undefined { 284 | return parseUniqueAttributeWith(decl, "map", parseMapAttribute); 285 | } 286 | 287 | export interface MapBlockAttribute { 288 | name: string; 289 | } 290 | 291 | export function findMapBlockAttribute( 292 | decl: ModelDeclaration | EnumDeclaration, 293 | ): MapBlockAttribute | undefined { 294 | return parseUniqueAttributeWith(decl, "map", parseMapAttribute); 295 | } 296 | 297 | function parseMapAttribute(attr: SchemaAttribute): MapFieldAttribute { 298 | return { 299 | name: readStringArgument(getArgument(attr.args, "name", 0)), 300 | }; 301 | } 302 | 303 | function parseUniqueAttributeWith( 304 | decl: ModelDeclaration | EnumDeclaration | FieldDeclaration | EnumValue, 305 | name: string, 306 | parser: (attr: SchemaAttribute) => T, 307 | ): T | undefined { 308 | const attrs = findAllAttributes(getDeclarationAttributes(decl), name); 309 | switch (attrs.length) { 310 | case 0: 311 | return undefined; 312 | case 1: 313 | return parseAttributeWith(attrs[0], decl, name, parser); 314 | default: { 315 | const prefixedName = prefixAttributeName(decl, name); 316 | throw new Error(`Multiple instances of ${prefixedName} attribute`); 317 | } 318 | } 319 | } 320 | 321 | function parseAttributesWith( 322 | decl: ModelDeclaration, 323 | name: string, 324 | parser: (attr: SchemaAttribute) => T, 325 | ): T[] { 326 | const attrs = findAllAttributes(getModelAttributes(decl), name); 327 | return attrs.map((attr) => parseAttributeWith(attr, decl, name, parser)); 328 | } 329 | 330 | function parseAttributeWith( 331 | attr: SchemaAttribute, 332 | decl: ModelDeclaration | EnumDeclaration | FieldDeclaration | EnumValue, 333 | name: string, 334 | parser: (attr: SchemaAttribute) => T, 335 | ): T { 336 | try { 337 | return parser(attr); 338 | } catch (err) { 339 | const { message } = asError(err); 340 | const atName = prefixAttributeName(decl, name); 341 | const declName = getDeclarationName(decl); 342 | let msg = `Invalid attribute ${atName}`; 343 | if (attr.location) { 344 | msg += ` at ${formatSourceRange(attr.location)}`; 345 | } 346 | msg += ` for ${declName}: ${message}`; 347 | throw new Error(msg, { cause: err }); 348 | } 349 | } 350 | 351 | function prefixAttributeName( 352 | decl: ModelDeclaration | EnumDeclaration | FieldDeclaration | EnumValue, 353 | name: string, 354 | ): string { 355 | return (hasBlockAttributes(decl) ? "@@" : "@") + name; 356 | } 357 | 358 | export function applyOptional( 359 | value: T | undefined, 360 | fn: (arg: T) => U, 361 | ): U | undefined { 362 | return value !== undefined ? fn(value) : undefined; 363 | } 364 | -------------------------------------------------------------------------------- /src/format.test.ts: -------------------------------------------------------------------------------- 1 | import testAst from "../test-data/ast"; 2 | import { formatAst, formatSourceLocation, formatSourceRange } from "./format"; 3 | import { readFileSync } from "fs"; 4 | import { join, dirname } from "path"; 5 | 6 | const testData = join(dirname(__dirname), "test-data"); 7 | 8 | function normalizeWhitespace(s: string): string { 9 | // Collapse horizontal spaces and remove blank lines and trailing newlines 10 | return s.replace(/ +/g, " ").replace(/\n+/g, "\n").replace(/\n+$/, ""); 11 | } 12 | 13 | describe("formatAst", () => { 14 | test("complete schema", () => { 15 | const schema = readFileSync(join(testData, "schema.prisma"), { 16 | encoding: "utf8", 17 | }); 18 | expect(normalizeWhitespace(formatAst(testAst))).toBe( 19 | normalizeWhitespace(schema), 20 | ); 21 | }); 22 | 23 | test("literal", () => { 24 | expect(formatAst({ kind: "literal", value: 42 })).toBe("42"); 25 | }); 26 | 27 | test("path", () => { 28 | expect(formatAst({ kind: "path", value: ["foo", "bar"] })).toBe("foo.bar"); 29 | }); 30 | 31 | test("array", () => { 32 | expect( 33 | formatAst({ 34 | kind: "array", 35 | items: [ 36 | { kind: "literal", value: 1 }, 37 | { kind: "literal", value: true }, 38 | ], 39 | }), 40 | ).toBe("[1, true]"); 41 | }); 42 | 43 | test("function call", () => { 44 | expect( 45 | formatAst({ 46 | kind: "functionCall", 47 | path: { kind: "path", value: ["foo", "bar"] }, 48 | args: [ 49 | { kind: "literal", value: 1 }, 50 | { 51 | kind: "namedArgument", 52 | name: { kind: "name", value: "name" }, 53 | expression: { kind: "literal", value: "abc" }, 54 | }, 55 | ], 56 | }), 57 | ).toBe('foo.bar(1, name: "abc")'); 58 | }); 59 | 60 | test("no-argument function call", () => { 61 | expect( 62 | formatAst({ 63 | kind: "functionCall", 64 | path: { kind: "path", value: ["foo", "bar"] }, 65 | }), 66 | ).toBe("foo.bar()"); 67 | }); 68 | 69 | test("type alias", () => { 70 | expect( 71 | formatAst({ 72 | kind: "typeAlias", 73 | name: { kind: "name", value: "UUID" }, 74 | type: { 75 | kind: "typeId", 76 | name: { kind: "name", value: "String" }, 77 | }, 78 | attributes: [ 79 | { 80 | kind: "fieldAttribute", 81 | path: { kind: "path", value: ["id"] }, 82 | args: [], 83 | }, 84 | { 85 | kind: "fieldAttribute", 86 | path: { kind: "path", value: ["default"] }, 87 | args: [ 88 | { 89 | kind: "functionCall", 90 | path: { kind: "path", value: ["uuid"] }, 91 | args: [], 92 | }, 93 | ], 94 | }, 95 | ], 96 | }), 97 | ).toBe("type UUID = String @id @default(uuid())"); 98 | }); 99 | 100 | test("legacy required", () => { 101 | expect( 102 | formatAst({ 103 | kind: "model", 104 | name: { kind: "name", value: "M" }, 105 | members: [ 106 | { 107 | kind: "field", 108 | name: { kind: "name", value: "f" }, 109 | type: { 110 | kind: "required", 111 | type: { 112 | kind: "typeId", 113 | name: { kind: "name", value: "String" }, 114 | }, 115 | }, 116 | attributes: [], 117 | }, 118 | ], 119 | }), 120 | ).toBe("model M {\n f String!\n}"); 121 | }); 122 | }); 123 | 124 | describe("formatSourceLocation", () => { 125 | test("it", () => { 126 | expect(formatSourceLocation({ line: 1, column: 2, offset: 3 })).toBe("1:2"); 127 | }); 128 | }); 129 | 130 | describe("formatSourceRange", () => { 131 | test("same line", () => { 132 | expect( 133 | formatSourceRange({ 134 | start: { line: 1, column: 2, offset: 3 }, 135 | end: { line: 1, column: 5, offset: 6 }, 136 | }), 137 | ).toBe("1:2-5"); 138 | }); 139 | test("different lines", () => { 140 | expect( 141 | formatSourceRange({ 142 | start: { line: 1, column: 2, offset: 3 }, 143 | end: { line: 4, column: 5, offset: 6 }, 144 | }), 145 | ).toBe("1:2-4:5"); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /src/format.ts: -------------------------------------------------------------------------------- 1 | import { PrismaAstNode, SourceLocation, SourceRange } from "./ast"; 2 | import { PrismaAstReducer, reduceAst } from "./visit"; 3 | 4 | const formatReducer: PrismaAstReducer = { 5 | schema({ declarations }) { 6 | return declarations.join("\n\n"); 7 | }, 8 | model({ name, members }) { 9 | return `model ${name} {\n${members.join("\n")}\n}`; 10 | }, 11 | type({ name, members }) { 12 | return `type ${name} {\n${members.join("\n")}\n}`; 13 | }, 14 | view({ name, members }) { 15 | return `view ${name} {\n${members.join("\n")}\n}`; 16 | }, 17 | enum({ name, members }) { 18 | return `enum ${name} {\n${members.join("\n")}\n}`; 19 | }, 20 | datasource({ name, members }) { 21 | return `datasource ${name} {\n${members.join("\n")}\n}`; 22 | }, 23 | generator({ name, members }) { 24 | return `generator ${name} {\n${members.join("\n")}\n}`; 25 | }, 26 | field({ name, type, attributes, comment }) { 27 | let result = ` ${name} ${type}`; 28 | if (attributes?.length) result += ` ${attributes.join(" ")}`; 29 | if (comment) result += ` ${comment}`; 30 | return result; 31 | }, 32 | typeId({ name }) { 33 | return name; 34 | }, 35 | list({ type }) { 36 | return `${type}[]`; 37 | }, 38 | optional({ type }) { 39 | return `${type}?`; 40 | }, 41 | required({ type }) { 42 | return `${type}!`; 43 | }, 44 | unsupported({ type }) { 45 | return `Unsupported(${type})`; 46 | }, 47 | enumValue({ name, attributes, comment }) { 48 | let result = ` ${name}`; 49 | if (attributes?.length) result += ` ${attributes.join(" ")}`; 50 | if (comment) result += ` ${comment}`; 51 | return result; 52 | }, 53 | typeAlias({ name, type, attributes }) { 54 | let result = `type ${name} = ${type}`; 55 | if (attributes?.length) result += ` ${attributes.join(" ")}`; 56 | return result; 57 | }, 58 | config({ name, value, comment }) { 59 | let result = ` ${name} = ${value}`; 60 | if (comment) result += ` ${comment}`; 61 | return result; 62 | }, 63 | blockAttribute({ path, args, comment }) { 64 | let result = ` @@${path}`; 65 | if (args?.length) result += `(${args.join(", ")})`; 66 | if (comment) result += ` ${comment}`; 67 | return result; 68 | }, 69 | fieldAttribute({ path, args }) { 70 | let result = `@${path}`; 71 | if (args?.length) result += `(${args.join(", ")})`; 72 | return result; 73 | }, 74 | namedArgument({ name, expression }) { 75 | return `${name}: ${expression}`; 76 | }, 77 | name({ value }) { 78 | return value; 79 | }, 80 | literal({ value }) { 81 | return typeof value === "string" ? JSON.stringify(value) : String(value); 82 | }, 83 | path({ value }) { 84 | return value.join("."); 85 | }, 86 | array({ items }) { 87 | return `[${items.join(", ")}]`; 88 | }, 89 | functionCall({ path, args }) { 90 | return `${path}(${args?.join(", ") ?? ""})`; 91 | }, 92 | commentBlock({ comments }) { 93 | return comments.join("\n"); 94 | }, 95 | comment({ text }) { 96 | return `// ${text}`; 97 | }, 98 | docComment({ text }) { 99 | return `/// ${text}`; 100 | }, 101 | }; 102 | 103 | export function formatAst(node: PrismaAstNode): string { 104 | return reduceAst(node, formatReducer) || /* istanbul ignore next */ ""; 105 | } 106 | 107 | export function formatSourceLocation(loc: SourceLocation): string { 108 | return `${loc.line}:${loc.column}`; 109 | } 110 | 111 | export function formatSourceRange(loc: SourceRange): string { 112 | const { start, end } = loc; 113 | if (start.line === end.line) { 114 | return `${start.line}:${start.column}-${end.column}`; 115 | } 116 | return `${formatSourceLocation(start)}-${formatSourceLocation(end)}`; 117 | } 118 | 119 | export function joinPath(path: string[]): string { 120 | return path.join("."); 121 | } 122 | -------------------------------------------------------------------------------- /src/grammar.pegjs: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/prisma/prisma-engines/blob/main/libs/datamodel/schema-ast/src/parser/datamodel.pest 2 | 3 | { 4 | function buildList(head, tail, index) { 5 | return [head].concat(extractList(tail, index)); 6 | } 7 | 8 | function extractList(list, index) { 9 | return list.map(function(element) { return element[index]; }); 10 | } 11 | 12 | function optionalList(value) { 13 | return value !== null ? value : []; 14 | } 15 | } 16 | 17 | schema = WS body:declarations? WS 18 | { return { kind: "schema", declarations: optionalList(body) }; } 19 | 20 | declarations = head:declaration tail:(WS declaration)* 21 | { return buildList(head, tail, 1); } 22 | 23 | declaration = model_declaration / enum_declaration / config_block / type_alias / comment_block 24 | 25 | // ###################################### 26 | // Model, view and composite types 27 | // ###################################### 28 | 29 | // At the syntax level, models and composite types are the same. 30 | model_declaration = kind:("model" / "type" / "view") __ name:name __ "{" WS members:model_declaration_members? WS "}" 31 | { return { kind, name, members: optionalList(members), location: location() }; } 32 | 33 | model_declaration_members = head:model_declaration_member tail:(WS model_declaration_member)* 34 | { return buildList(head, tail, 1); } 35 | 36 | model_declaration_member = field_declaration / block_attribute / comment_block 37 | 38 | field_declaration = name:name __ ":"? __ type:field_type? __ attributes:field_attributes? __ comment:trailing_comment? 39 | { return { kind: "field", name, type, attributes: optionalList(attributes), comment, location: location() }; } 40 | 41 | // ###################################### 42 | // Field Type 43 | // ###################################### 44 | 45 | field_type = list_type / optional_type / legacy_required_type / legacy_list_type / base_type 46 | 47 | list_type = type:base_type __ "[]" 48 | { return { kind: "list", type }; } 49 | 50 | optional_type = type:base_type __ "?" 51 | { return { kind: "optional", type }; } 52 | 53 | legacy_required_type = type:base_type __ "!" 54 | { return { kind: "required", type }; } 55 | 56 | legacy_list_type = "[" __ type:base_type __ "]" 57 | { return { kind: "list", type }; } 58 | 59 | base_type = unsupported_type 60 | / name:name { return { kind: "typeId", name }; } 61 | 62 | unsupported_type = "Unsupported(" __ type:string_literal __ ")" 63 | { return { kind: "unsupported", type }; } 64 | 65 | // ###################################### 66 | // Type Alias 67 | // ###################################### 68 | 69 | type_alias = "type" __ name:name __ "=" __ type:base_type __ attributes:field_attributes? 70 | { return { kind: "typeAlias", name, type, attributes: optionalList(attributes), location: location() }; } 71 | 72 | // ###################################### 73 | // Configuration blocks 74 | // ###################################### 75 | 76 | config_block = kind:("datasource" / "generator") __ name:name __ "{" WS members:config_block_members? WS "}" 77 | { return { kind, name, members: optionalList(members), location: location() }; } 78 | 79 | config_block_members = head:config_block_member tail:(WS config_block_member)* 80 | { return buildList(head, tail, 1); } 81 | 82 | config_block_member = key_value / comment_block 83 | 84 | key_value = name:name __ "=" __ value:expression __ comment:trailing_comment? 85 | { return { kind: "config", name, value, comment, location: location() }; } 86 | 87 | // ###################################### 88 | // Enum 89 | // ###################################### 90 | 91 | enum_declaration = kind:"enum" __ name:name __ "{" WS members:enum_declaration_members? WS "}" 92 | { return { kind, name, members: optionalList(members), location: location() }; } 93 | 94 | enum_declaration_members = head:enum_declaration_member tail:(WS enum_declaration_member)* 95 | { return buildList(head, tail, 1); } 96 | 97 | enum_declaration_member = enum_value_declaration / block_attribute / comment_block 98 | 99 | enum_value_declaration = name:name __ attributes:field_attributes? __ comment:trailing_comment? 100 | { return { kind: "enumValue", name, attributes: optionalList(attributes), comment, location: location() }; } 101 | 102 | // ###################################### 103 | // Attributes 104 | // ###################################### 105 | 106 | block_attribute = "@@" __ path:path __ args:arguments_list? __ comment:trailing_comment? 107 | { return { kind: "blockAttribute", path, args: optionalList(args), comment, location: location() }; } 108 | 109 | field_attribute = "@" __ path:path __ args:arguments_list? 110 | { return { kind: "fieldAttribute", path, args: optionalList(args), location: location() }; } 111 | 112 | field_attributes = head:field_attribute tail:(__ field_attribute)* 113 | { return buildList(head, tail, 1); } 114 | 115 | // ###################################### 116 | // Arguments 117 | // ###################################### 118 | 119 | arguments_list = "(" __ args:arguments? __ ","? __ ")" 120 | { return optionalList(args); } 121 | 122 | arguments = head:argument __ tail:("," __ argument)* 123 | { return buildList(head, tail, 2); } 124 | 125 | argument = named_argument / expression 126 | 127 | named_argument = name:name __ ":" __ expression:expression 128 | { return { kind: "namedArgument", name, expression }; } 129 | 130 | // ###################################### 131 | // Comments and Documentation Comments 132 | // ###################################### 133 | 134 | comment_block = head:trailing_comment tail:(WS trailing_comment)* 135 | { return { kind: "commentBlock", comments: buildList(head, tail, 1) }; } 136 | 137 | trailing_comment = doc_comment / comment 138 | 139 | doc_comment = "///" __ text:doc_content 140 | { return { kind: "docComment", text, location: location() }; } 141 | 142 | comment = "//" __ text:doc_content 143 | { return { kind: "comment", text, location: location() }; } 144 | 145 | doc_content = (!EOL .)* 146 | { return text(); } 147 | 148 | // ###################################### 149 | // Shared building blocks 150 | // ###################################### 151 | 152 | name = id:identifier 153 | { return { kind: "name", value: id, location: location() }; } 154 | 155 | path = head:identifier __ tail:("." __ identifier)* 156 | { return { kind: "path", value: buildList(head, tail, 2), location: location() }; } 157 | 158 | identifier = head:[0-9a-z]i tail:[0-9a-z_-]i* 159 | { return head + tail.join(""); } 160 | 161 | // ###################################### 162 | // Expressions & Functions 163 | // ###################################### 164 | 165 | function_call = path:path __ args:arguments_list 166 | { return { kind: "functionCall", path, args }; } 167 | 168 | array_expression = "[" __ items:expression_list? __ "]" 169 | { return { kind: "array", items: optionalList(items) }; } 170 | 171 | expression_list = head:expression __ tail:("," __ expression)* 172 | { return buildList(head, tail, 2); } 173 | 174 | expression 175 | = function_call 176 | / array_expression 177 | / boolean_literal 178 | / numeric_literal 179 | / string_literal 180 | / path 181 | 182 | // ###################################### 183 | // Literals / Values 184 | // ###################################### 185 | 186 | boolean_literal = ("false" / "true") 187 | { return { kind: "literal", value: text() === "true" }; } 188 | 189 | numeric_literal = "-"? [0-9]+ ("." [0-9]+)? 190 | { return { kind: "literal", value: parseInt(text()) }; } 191 | 192 | string_literal = '"' value:string_content '"' 193 | { return { kind: "literal", value }; } 194 | 195 | string_content = ("\\" . / [^\0-\x1F"])* 196 | { return text(); } 197 | 198 | __ "horizontal whitespace" = [ \t]* 199 | 200 | WS "any whitespace" = [ \t\n\r]* 201 | 202 | EOL "end of line" = [\n\r] 203 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { readFileSync } from "fs"; 3 | import { formatAst, parsePrismaSchema } from "."; 4 | 5 | test("README", () => { 6 | console.log = jest.fn(); 7 | const ast = parsePrismaSchema( 8 | readFileSync("test-data/schema.prisma", { encoding: "utf8" }), 9 | ); 10 | // ... manipulate the schema ... 11 | console.log(formatAst(ast)); 12 | }); 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./access"; 2 | export * from "./ast"; 3 | export * from "./attributes"; 4 | export * from "./format"; 5 | export * from "./parse"; 6 | export * from "./visit"; 7 | -------------------------------------------------------------------------------- /src/parse.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | import { dirname, join } from "path"; 3 | import testAst from "../test-data/ast"; 4 | import { 5 | PrismaAstNode, 6 | PrismaSchema, 7 | PrismaType, 8 | SchemaExpression, 9 | } from "./ast"; 10 | import { 11 | parsePrismaExpression, 12 | parsePrismaSchema, 13 | parsePrismaType, 14 | } from "./parse"; 15 | 16 | const testData = join(dirname(__dirname), "test-data"); 17 | 18 | // Strip location and null/undefined from AST nodes for simpler testing 19 | function stripAst(node: T): T { 20 | return Object.entries(node).reduce>((obj, [k, v]) => { 21 | if (k !== "location" && v != null) { 22 | if (typeof v === "object") { 23 | if (!Array.isArray(v)) { 24 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 25 | v = stripAst(v); 26 | } else if (v.length > 0) { 27 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 28 | v = v.map((e) => (typeof e === "object" ? stripAst(e) : e)); 29 | } 30 | } 31 | obj[k] = v; 32 | } 33 | return obj; 34 | }, {}) as T; 35 | } 36 | 37 | describe("parsePrismaSchema", () => { 38 | test("empty schema", () => { 39 | expect(parsePrismaSchema("")).toStrictEqual({ 40 | kind: "schema", 41 | declarations: [], 42 | }); 43 | }); 44 | 45 | test("complete schema", () => { 46 | const ast = parsePrismaSchema( 47 | readFileSync(join(testData, "schema.prisma"), { encoding: "utf8" }), 48 | ); 49 | // Uncomment temporarily to fix test-data/ast.json 50 | // console.log(JSON.stringify(ast)); 51 | expect(JSON.parse(JSON.stringify(ast))).toStrictEqual(testAst); 52 | }); 53 | 54 | test("type alias", () => { 55 | const ast = parsePrismaSchema(`type UUID = String @id @default(uuid())`); 56 | expect(stripAst(ast)).toStrictEqual({ 57 | kind: "schema", 58 | declarations: [ 59 | { 60 | kind: "typeAlias", 61 | name: { kind: "name", value: "UUID" }, 62 | type: { 63 | kind: "typeId", 64 | name: { kind: "name", value: "String" }, 65 | }, 66 | attributes: [ 67 | { 68 | kind: "fieldAttribute", 69 | path: { kind: "path", value: ["id"] }, 70 | args: [], 71 | }, 72 | { 73 | kind: "fieldAttribute", 74 | path: { kind: "path", value: ["default"] }, 75 | args: [ 76 | { 77 | kind: "functionCall", 78 | path: { kind: "path", value: ["uuid"] }, 79 | args: [], 80 | }, 81 | ], 82 | }, 83 | ], 84 | }, 85 | ], 86 | }); 87 | }); 88 | 89 | test("legacy required", () => { 90 | const ast = parsePrismaSchema(` 91 | model M { 92 | f String! 93 | } 94 | `); 95 | expect(stripAst(ast)).toStrictEqual({ 96 | kind: "schema", 97 | declarations: [ 98 | { 99 | kind: "model", 100 | name: { kind: "name", value: "M" }, 101 | members: [ 102 | { 103 | kind: "field", 104 | name: { kind: "name", value: "f" }, 105 | type: { 106 | kind: "required", 107 | type: { 108 | kind: "typeId", 109 | name: { kind: "name", value: "String" }, 110 | }, 111 | }, 112 | attributes: [], 113 | }, 114 | ], 115 | }, 116 | ], 117 | }); 118 | }); 119 | 120 | test("legacy list", () => { 121 | const ast = parsePrismaSchema(` 122 | model M { 123 | f [String] 124 | } 125 | `); 126 | expect(stripAst(ast)).toStrictEqual({ 127 | kind: "schema", 128 | declarations: [ 129 | { 130 | kind: "model", 131 | name: { kind: "name", value: "M" }, 132 | members: [ 133 | { 134 | kind: "field", 135 | name: { kind: "name", value: "f" }, 136 | type: { 137 | kind: "list", 138 | type: { 139 | kind: "typeId", 140 | name: { kind: "name", value: "String" }, 141 | }, 142 | }, 143 | attributes: [], 144 | }, 145 | ], 146 | }, 147 | ], 148 | }); 149 | }); 150 | }); 151 | 152 | describe("parsePrismaType", () => { 153 | test("base type", () => { 154 | const id = "Identifier"; 155 | expect(stripAst(parsePrismaType(id))).toStrictEqual({ 156 | kind: "typeId", 157 | name: { kind: "name", value: id }, 158 | }); 159 | }); 160 | 161 | test("optional", () => { 162 | const id = "Identifier"; 163 | expect(stripAst(parsePrismaType(`${id}?`))).toStrictEqual({ 164 | kind: "optional", 165 | type: { 166 | kind: "typeId", 167 | name: { kind: "name", value: id }, 168 | }, 169 | }); 170 | }); 171 | 172 | test("list", () => { 173 | const id = "Identifier"; 174 | expect(stripAst(parsePrismaType(`${id}[]`))).toStrictEqual({ 175 | kind: "list", 176 | type: { 177 | kind: "typeId", 178 | name: { kind: "name", value: id }, 179 | }, 180 | }); 181 | }); 182 | }); 183 | 184 | describe("parsePrismaExpression", () => { 185 | test("path", () => { 186 | expect( 187 | stripAst(parsePrismaExpression("foo.bar")), 188 | ).toStrictEqual({ 189 | kind: "path", 190 | value: ["foo", "bar"], 191 | }); 192 | }); 193 | 194 | test("array", () => { 195 | expect( 196 | stripAst(parsePrismaExpression('["foo", "bar"]')), 197 | ).toStrictEqual({ 198 | kind: "array", 199 | items: [ 200 | { 201 | kind: "literal", 202 | value: "foo", 203 | }, 204 | { 205 | kind: "literal", 206 | value: "bar", 207 | }, 208 | ], 209 | }); 210 | }); 211 | 212 | test("literal", () => { 213 | expect( 214 | stripAst(parsePrismaExpression('"foo.bar"')), 215 | ).toStrictEqual({ 216 | kind: "literal", 217 | value: "foo.bar", 218 | }); 219 | }); 220 | 221 | test("function call", () => { 222 | expect( 223 | stripAst(parsePrismaExpression('foo("bar")')), 224 | ).toStrictEqual({ 225 | kind: "functionCall", 226 | path: { kind: "path", value: ["foo"] }, 227 | args: [{ kind: "literal", value: "bar" }], 228 | }); 229 | }); 230 | }); 231 | -------------------------------------------------------------------------------- /src/parse.ts: -------------------------------------------------------------------------------- 1 | import { PrismaSchema, PrismaType, SchemaExpression } from "./ast"; 2 | 3 | import { parse } from "./__generated__/parser"; 4 | 5 | export function parsePrismaSchema(source: string): PrismaSchema { 6 | return parse(source) as PrismaSchema; 7 | } 8 | 9 | export function parsePrismaType(source: string): PrismaType { 10 | return parse(source, { startRule: "field_type" }) as unknown as PrismaType; 11 | } 12 | 13 | export function parsePrismaExpression(source: string): SchemaExpression { 14 | return parse(source, { 15 | startRule: "expression", 16 | }) as unknown as SchemaExpression; 17 | } 18 | -------------------------------------------------------------------------------- /src/visit.test.ts: -------------------------------------------------------------------------------- 1 | import testAst from "../test-data/ast"; 2 | import { ModelDeclaration } from "./ast"; 3 | import { visitAst } from "./visit"; 4 | 5 | describe("visit", () => { 6 | test("basic", () => { 7 | const enterModel = jest.fn(); 8 | const leaveModel = jest.fn(); 9 | const enterBlockAttribute = jest.fn(); 10 | const leaveBlockAttribute = jest.fn(); 11 | visitAst( 12 | { 13 | kind: "model", 14 | name: { kind: "name", value: "M" }, 15 | members: [ 16 | { 17 | kind: "blockAttribute", 18 | path: { kind: "path", value: ["map"] }, 19 | args: [{ kind: "literal", value: "m" }], 20 | }, 21 | ], 22 | }, 23 | { 24 | model: { 25 | enter: enterModel, 26 | leave: leaveModel, 27 | }, 28 | blockAttribute: { 29 | enter: enterBlockAttribute, 30 | leave: leaveBlockAttribute, 31 | }, 32 | }, 33 | ); 34 | expect(enterModel).toHaveBeenCalled(); 35 | expect(leaveModel).toHaveBeenCalled(); 36 | expect(enterBlockAttribute).toHaveBeenCalled(); 37 | expect(leaveBlockAttribute).toHaveBeenCalled(); 38 | }); 39 | 40 | test("full schema", () => { 41 | visitAst(testAst, {}); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/visit.ts: -------------------------------------------------------------------------------- 1 | import { PrismaAstNode } from "./ast"; 2 | 3 | type MaybeArray = T | T[] | null | undefined; 4 | 5 | type MaybeNodes = MaybeArray; 6 | 7 | const nestedNodeMap: { 8 | [T in PrismaAstNode as T["kind"]]: ReadonlyArray; 9 | } = { 10 | schema: ["declarations"], 11 | model: ["name", "members"], 12 | type: ["name", "members"], 13 | view: ["name", "members"], 14 | enum: ["name", "members"], 15 | datasource: ["name", "members"], 16 | generator: ["name", "members"], 17 | field: ["name", "type", "attributes", "comment"], 18 | typeId: ["name"], 19 | list: ["type"], 20 | optional: ["type"], 21 | required: ["type"], 22 | unsupported: ["type"], 23 | enumValue: ["name", "attributes", "comment"], 24 | typeAlias: ["name", "type", "attributes"], 25 | config: ["name", "value", "comment"], 26 | blockAttribute: ["path", "args", "comment"], 27 | fieldAttribute: ["path", "args"], 28 | namedArgument: ["name", "expression"], 29 | name: ["value"], 30 | literal: ["value"], 31 | path: ["value"], 32 | array: ["items"], 33 | functionCall: ["path", "args"], 34 | commentBlock: ["comments"], 35 | comment: ["text"], 36 | docComment: ["text"], 37 | }; 38 | 39 | function isPrismaAstNode(v: unknown): v is PrismaAstNode { 40 | return isRecord(v) && typeof v.kind === "string" && v.kind in nestedNodeMap; 41 | } 42 | 43 | function isRecord(v: unknown): v is Record { 44 | return typeof v === "object" && v != null; 45 | } 46 | 47 | export interface PrismaAstNodeVisitor { 48 | enter?(node: T): void; 49 | leave?(node: T): void; 50 | } 51 | 52 | export type PrismaAstVisitor = { 53 | readonly [T in PrismaAstNode as T["kind"]]?: PrismaAstNodeVisitor; 54 | }; 55 | 56 | export function visitAst( 57 | node: T, 58 | visitor: PrismaAstVisitor, 59 | ): void { 60 | const nv = visitor[node.kind] as PrismaAstNodeVisitor | undefined; 61 | nv?.enter?.(node); 62 | const keys = nestedNodeMap[node.kind] as ReadonlyArray; 63 | for (const key of keys) { 64 | const value = node[key]; 65 | if (Array.isArray(value) && value.every(isPrismaAstNode)) { 66 | for (const member of value) { 67 | visitAst(member, visitor); 68 | } 69 | } else if (isPrismaAstNode(value)) { 70 | visitAst(value, visitor); 71 | } 72 | } 73 | nv?.leave?.(node); 74 | } 75 | 76 | type ReducedField = T extends null | undefined 77 | ? T 78 | : T extends PrismaAstNode 79 | ? R 80 | : T extends ReadonlyArray 81 | ? ReadonlyArray 82 | : T; 83 | 84 | type ReducedNode = { 85 | [K in keyof T]: ReducedField; 86 | }; 87 | 88 | export type PrismaAstNodeReducer = ( 89 | node: ReducedNode, 90 | ) => R; 91 | 92 | export type PrismaAstReducer = { 93 | readonly [T in PrismaAstNode as T["kind"]]?: PrismaAstNodeReducer; 94 | }; 95 | 96 | export function reduceAst( 97 | node: T, 98 | reducer: PrismaAstReducer, 99 | ): R | undefined { 100 | const keys = nestedNodeMap[node.kind] as ReadonlyArray; 101 | const reducedNode = keys.reduce( 102 | (rn, key) => { 103 | rn[key] = reduceField(node, key, reducer) as (typeof rn)[typeof key]; 104 | return rn; 105 | }, 106 | {} as ReducedNode, 107 | ); 108 | const nr = reducer[node.kind] as PrismaAstNodeReducer | undefined; 109 | return nr?.(reducedNode); 110 | } 111 | 112 | function reduceField( 113 | node: T, 114 | key: keyof T, 115 | reducer: PrismaAstReducer, 116 | ): MaybeArray { 117 | const value = node[key] as MaybeNodes; 118 | if (Array.isArray(value) && value.every(isPrismaAstNode)) { 119 | return value.reduce((arr, elem) => { 120 | const reduced = reduceAst(elem, reducer); 121 | if (reduced != null) { 122 | arr.push(reduced); 123 | } 124 | return arr; 125 | }, []); 126 | } 127 | if (isPrismaAstNode(value)) { 128 | return reduceAst(value, reducer); 129 | } 130 | return value; 131 | } 132 | -------------------------------------------------------------------------------- /test-data/ast.ts: -------------------------------------------------------------------------------- 1 | import { PrismaSchema } from "../src/ast"; 2 | 3 | const ast: PrismaSchema = { 4 | kind: "schema", 5 | declarations: [ 6 | { 7 | kind: "commentBlock", 8 | comments: [ 9 | { 10 | kind: "comment", 11 | text: "Schema comment", 12 | location: { 13 | start: { offset: 0, line: 1, column: 1 }, 14 | end: { offset: 17, line: 1, column: 18 }, 15 | }, 16 | }, 17 | { 18 | kind: "docComment", 19 | text: "Doc comment", 20 | location: { 21 | start: { offset: 19, line: 3, column: 1 }, 22 | end: { offset: 34, line: 3, column: 16 }, 23 | }, 24 | }, 25 | ], 26 | }, 27 | { 28 | kind: "generator", 29 | name: { 30 | kind: "name", 31 | value: "client", 32 | location: { 33 | start: { offset: 45, line: 4, column: 11 }, 34 | end: { offset: 51, line: 4, column: 17 }, 35 | }, 36 | }, 37 | members: [ 38 | { 39 | kind: "config", 40 | name: { 41 | kind: "name", 42 | value: "provider", 43 | location: { 44 | start: { offset: 56, line: 5, column: 3 }, 45 | end: { offset: 64, line: 5, column: 11 }, 46 | }, 47 | }, 48 | value: { kind: "literal", value: "prisma-client-js" }, 49 | comment: { 50 | kind: "comment", 51 | text: "Trailing comment", 52 | location: { 53 | start: { offset: 86, line: 5, column: 33 }, 54 | end: { offset: 105, line: 5, column: 52 }, 55 | }, 56 | }, 57 | location: { 58 | start: { offset: 56, line: 5, column: 3 }, 59 | end: { offset: 105, line: 5, column: 52 }, 60 | }, 61 | }, 62 | { 63 | kind: "config", 64 | name: { 65 | kind: "name", 66 | value: "output", 67 | location: { 68 | start: { offset: 108, line: 6, column: 3 }, 69 | end: { offset: 114, line: 6, column: 9 }, 70 | }, 71 | }, 72 | value: { kind: "literal", value: "src/__generated__/PrismaClient" }, 73 | comment: null, 74 | location: { 75 | start: { offset: 108, line: 6, column: 3 }, 76 | end: { offset: 151, line: 6, column: 46 }, 77 | }, 78 | }, 79 | { 80 | kind: "config", 81 | name: { 82 | kind: "name", 83 | value: "previewFeatures", 84 | location: { 85 | start: { offset: 154, line: 7, column: 3 }, 86 | end: { offset: 169, line: 7, column: 18 }, 87 | }, 88 | }, 89 | value: { 90 | kind: "array", 91 | items: [ 92 | { kind: "literal", value: "views" }, 93 | ], 94 | }, 95 | comment: null, 96 | location: { 97 | start: { offset: 154, line: 7, column: 3 }, 98 | end: { offset: 181, line: 7, column: 30 }, 99 | }, 100 | }, 101 | ], 102 | location: { 103 | start: { offset: 35, line: 4, column: 1 }, 104 | end: { offset: 183, line: 8, column: 2 }, 105 | }, 106 | }, 107 | { 108 | kind: "datasource", 109 | name: { 110 | kind: "name", 111 | value: "postgresql", 112 | location: { 113 | start: { offset: 196, line: 10, column: 12 }, 114 | end: { offset: 206, line: 10, column: 22 }, 115 | }, 116 | }, 117 | members: [ 118 | { 119 | kind: "config", 120 | name: { 121 | kind: "name", 122 | value: "provider", 123 | location: { 124 | start: { offset: 211, line: 11, column: 3 }, 125 | end: { offset: 219, line: 11, column: 11 }, 126 | }, 127 | }, 128 | value: { kind: "literal", value: "mongodb" }, 129 | comment: null, 130 | location: { 131 | start: { offset: 211, line: 11, column: 3 }, 132 | end: { offset: 231, line: 11, column: 23 }, 133 | }, 134 | }, 135 | { 136 | kind: "config", 137 | name: { 138 | kind: "name", 139 | value: "url", 140 | location: { 141 | start: { offset: 234, line: 12, column: 3 }, 142 | end: { offset: 237, line: 12, column: 6 }, 143 | }, 144 | }, 145 | value: { 146 | kind: "functionCall", 147 | path: { 148 | kind: "path", 149 | value: ["env"], 150 | location: { 151 | start: { offset: 245, line: 12, column: 14 }, 152 | end: { offset: 248, line: 12, column: 17 }, 153 | }, 154 | }, 155 | args: [{ kind: "literal", value: "DATABASE_URL" }], 156 | }, 157 | comment: null, 158 | location: { 159 | start: { offset: 234, line: 12, column: 3 }, 160 | end: { offset: 264, line: 12, column: 33 }, 161 | }, 162 | }, 163 | ], 164 | location: { 165 | start: { offset: 185, line: 10, column: 1 }, 166 | end: { offset: 266, line: 13, column: 2 }, 167 | }, 168 | }, 169 | { 170 | kind: "enum", 171 | name: { 172 | kind: "name", 173 | value: "MyEnum", 174 | location: { 175 | start: { offset: 273, line: 15, column: 6 }, 176 | end: { offset: 279, line: 15, column: 12 }, 177 | }, 178 | }, 179 | members: [ 180 | { 181 | kind: "enumValue", 182 | name: { 183 | kind: "name", 184 | value: "FirstValue", 185 | location: { 186 | start: { offset: 284, line: 16, column: 3 }, 187 | end: { offset: 294, line: 16, column: 13 }, 188 | }, 189 | }, 190 | attributes: [ 191 | { 192 | kind: "fieldAttribute", 193 | path: { 194 | kind: "path", 195 | value: ["map"], 196 | location: { 197 | start: { offset: 297, line: 16, column: 16 }, 198 | end: { offset: 300, line: 16, column: 19 }, 199 | }, 200 | }, 201 | args: [{ kind: "literal", value: "v1" }], 202 | location: { 203 | start: { offset: 296, line: 16, column: 15 }, 204 | end: { offset: 306, line: 16, column: 25 }, 205 | }, 206 | }, 207 | ], 208 | comment: { 209 | kind: "comment", 210 | text: "Enum value comment", 211 | location: { 212 | start: { offset: 307, line: 16, column: 26 }, 213 | end: { offset: 328, line: 16, column: 47 }, 214 | }, 215 | }, 216 | location: { 217 | start: { offset: 284, line: 16, column: 3 }, 218 | end: { offset: 328, line: 16, column: 47 }, 219 | }, 220 | }, 221 | { 222 | kind: "enumValue", 223 | name: { 224 | kind: "name", 225 | value: "SecondValue", 226 | location: { 227 | start: { offset: 331, line: 17, column: 3 }, 228 | end: { offset: 342, line: 17, column: 14 }, 229 | }, 230 | }, 231 | attributes: [], 232 | comment: null, 233 | location: { 234 | start: { offset: 331, line: 17, column: 3 }, 235 | end: { offset: 342, line: 17, column: 14 }, 236 | }, 237 | }, 238 | { 239 | kind: "enumValue", 240 | name: { 241 | kind: "name", 242 | value: "ThirdValue", 243 | location: { 244 | start: { offset: 345, line: 18, column: 3 }, 245 | end: { offset: 355, line: 18, column: 13 }, 246 | }, 247 | }, 248 | attributes: [], 249 | comment: null, 250 | location: { 251 | start: { offset: 345, line: 18, column: 3 }, 252 | end: { offset: 355, line: 18, column: 13 }, 253 | }, 254 | }, 255 | ], 256 | location: { 257 | start: { offset: 268, line: 15, column: 1 }, 258 | end: { offset: 357, line: 19, column: 2 }, 259 | }, 260 | }, 261 | { 262 | kind: "model", 263 | name: { 264 | kind: "name", 265 | value: "MyModel", 266 | location: { 267 | start: { offset: 365, line: 21, column: 7 }, 268 | end: { offset: 372, line: 21, column: 14 }, 269 | }, 270 | }, 271 | members: [ 272 | { 273 | kind: "field", 274 | name: { 275 | kind: "name", 276 | value: "id", 277 | location: { 278 | start: { offset: 377, line: 22, column: 3 }, 279 | end: { offset: 379, line: 22, column: 5 }, 280 | }, 281 | }, 282 | type: { 283 | kind: "typeId", 284 | name: { 285 | kind: "name", 286 | value: "String", 287 | location: { 288 | start: { offset: 387, line: 22, column: 13 }, 289 | end: { offset: 393, line: 22, column: 19 }, 290 | }, 291 | }, 292 | }, 293 | attributes: [ 294 | { 295 | kind: "fieldAttribute", 296 | path: { 297 | kind: "path", 298 | value: ["id"], 299 | location: { 300 | start: { offset: 397, line: 22, column: 23 }, 301 | end: { offset: 400, line: 22, column: 26 }, 302 | }, 303 | }, 304 | args: [], 305 | location: { 306 | start: { offset: 396, line: 22, column: 22 }, 307 | end: { offset: 400, line: 22, column: 26 }, 308 | }, 309 | }, 310 | { 311 | kind: "fieldAttribute", 312 | path: { 313 | kind: "path", 314 | value: ["default"], 315 | location: { 316 | start: { offset: 401, line: 22, column: 27 }, 317 | end: { offset: 408, line: 22, column: 34 }, 318 | }, 319 | }, 320 | args: [ 321 | { 322 | kind: "functionCall", 323 | path: { 324 | kind: "path", 325 | value: ["uuid"], 326 | location: { 327 | start: { offset: 409, line: 22, column: 35 }, 328 | end: { offset: 413, line: 22, column: 39 }, 329 | }, 330 | }, 331 | args: [], 332 | }, 333 | ], 334 | location: { 335 | start: { offset: 400, line: 22, column: 26 }, 336 | end: { offset: 416, line: 22, column: 42 }, 337 | }, 338 | }, 339 | { 340 | kind: "fieldAttribute", 341 | path: { 342 | kind: "path", 343 | value: ["map"], 344 | location: { 345 | start: { offset: 418, line: 22, column: 44 }, 346 | end: { offset: 421, line: 22, column: 47 }, 347 | }, 348 | }, 349 | args: [{ kind: "literal", value: "_id" }], 350 | location: { 351 | start: { offset: 417, line: 22, column: 43 }, 352 | end: { offset: 428, line: 22, column: 54 }, 353 | }, 354 | }, 355 | ], 356 | comment: { 357 | kind: "comment", 358 | text: "Field comment", 359 | location: { 360 | start: { offset: 429, line: 22, column: 55 }, 361 | end: { offset: 445, line: 22, column: 71 }, 362 | }, 363 | }, 364 | location: { 365 | start: { offset: 377, line: 22, column: 3 }, 366 | end: { offset: 445, line: 22, column: 71 }, 367 | }, 368 | }, 369 | { 370 | kind: "field", 371 | name: { 372 | kind: "name", 373 | value: "type", 374 | location: { 375 | start: { offset: 448, line: 23, column: 3 }, 376 | end: { offset: 452, line: 23, column: 7 }, 377 | }, 378 | }, 379 | type: { 380 | kind: "optional", 381 | type: { 382 | kind: "typeId", 383 | name: { 384 | kind: "name", 385 | value: "MyEnum", 386 | location: { 387 | start: { offset: 458, line: 23, column: 13 }, 388 | end: { offset: 464, line: 23, column: 19 }, 389 | }, 390 | }, 391 | }, 392 | }, 393 | attributes: [], 394 | comment: null, 395 | location: { 396 | start: { offset: 448, line: 23, column: 3 }, 397 | end: { offset: 465, line: 23, column: 20 }, 398 | }, 399 | }, 400 | { 401 | kind: "field", 402 | name: { 403 | kind: "name", 404 | value: "version", 405 | location: { 406 | start: { offset: 468, line: 24, column: 3 }, 407 | end: { offset: 475, line: 24, column: 10 }, 408 | }, 409 | }, 410 | type: { 411 | kind: "typeId", 412 | name: { 413 | kind: "name", 414 | value: "Int", 415 | location: { 416 | start: { offset: 478, line: 24, column: 13 }, 417 | end: { offset: 481, line: 24, column: 16 }, 418 | }, 419 | }, 420 | }, 421 | attributes: [ 422 | { 423 | kind: "fieldAttribute", 424 | path: { 425 | kind: "path", 426 | value: ["default"], 427 | location: { 428 | start: { offset: 488, line: 24, column: 23 }, 429 | end: { offset: 495, line: 24, column: 30 }, 430 | }, 431 | }, 432 | args: [{ kind: "literal", value: 0 }], 433 | location: { 434 | start: { offset: 487, line: 24, column: 22 }, 435 | end: { offset: 498, line: 24, column: 33 }, 436 | }, 437 | }, 438 | ], 439 | comment: null, 440 | location: { 441 | start: { offset: 468, line: 24, column: 3 }, 442 | end: { offset: 498, line: 24, column: 33 }, 443 | }, 444 | }, 445 | { 446 | kind: "field", 447 | name: { 448 | kind: "name", 449 | value: "uniqueKey", 450 | location: { 451 | start: { offset: 501, line: 25, column: 3 }, 452 | end: { offset: 510, line: 25, column: 12 }, 453 | }, 454 | }, 455 | type: { 456 | kind: "typeId", 457 | name: { 458 | kind: "name", 459 | value: "String", 460 | location: { 461 | start: { offset: 511, line: 25, column: 13 }, 462 | end: { offset: 517, line: 25, column: 19 }, 463 | }, 464 | }, 465 | }, 466 | attributes: [ 467 | { 468 | kind: "fieldAttribute", 469 | path: { 470 | kind: "path", 471 | value: ["unique"], 472 | location: { 473 | start: { offset: 521, line: 25, column: 23 }, 474 | end: { offset: 527, line: 25, column: 29 }, 475 | }, 476 | }, 477 | args: [ 478 | { 479 | kind: "namedArgument", 480 | name: { 481 | kind: "name", 482 | value: "sort", 483 | location: { 484 | start: { offset: 528, line: 25, column: 30 }, 485 | end: { offset: 532, line: 25, column: 34 }, 486 | }, 487 | }, 488 | expression: { 489 | kind: "path", 490 | value: ["Asc"], 491 | location: { 492 | start: { offset: 534, line: 25, column: 36 }, 493 | end: { offset: 537, line: 25, column: 39 }, 494 | }, 495 | }, 496 | }, 497 | ], 498 | location: { 499 | start: { offset: 520, line: 25, column: 22 }, 500 | end: { offset: 538, line: 25, column: 40 }, 501 | }, 502 | }, 503 | { 504 | kind: "fieldAttribute", 505 | path: { 506 | kind: "path", 507 | value: ["map"], 508 | location: { 509 | start: { offset: 540, line: 25, column: 42 }, 510 | end: { offset: 543, line: 25, column: 45 }, 511 | }, 512 | }, 513 | args: [{ kind: "literal", value: "unique_key" }], 514 | location: { 515 | start: { offset: 539, line: 25, column: 41 }, 516 | end: { offset: 557, line: 25, column: 59 }, 517 | }, 518 | }, 519 | ], 520 | comment: null, 521 | location: { 522 | start: { offset: 501, line: 25, column: 3 }, 523 | end: { offset: 557, line: 25, column: 59 }, 524 | }, 525 | }, 526 | { 527 | kind: "field", 528 | name: { 529 | kind: "name", 530 | value: "createdAt", 531 | location: { 532 | start: { offset: 560, line: 26, column: 3 }, 533 | end: { offset: 569, line: 26, column: 12 }, 534 | }, 535 | }, 536 | type: { 537 | kind: "typeId", 538 | name: { 539 | kind: "name", 540 | value: "DateTime", 541 | location: { 542 | start: { offset: 570, line: 26, column: 13 }, 543 | end: { offset: 578, line: 26, column: 21 }, 544 | }, 545 | }, 546 | }, 547 | attributes: [ 548 | { 549 | kind: "fieldAttribute", 550 | path: { 551 | kind: "path", 552 | value: ["default"], 553 | location: { 554 | start: { offset: 580, line: 26, column: 23 }, 555 | end: { offset: 587, line: 26, column: 30 }, 556 | }, 557 | }, 558 | args: [ 559 | { 560 | kind: "functionCall", 561 | path: { 562 | kind: "path", 563 | value: ["now"], 564 | location: { 565 | start: { offset: 588, line: 26, column: 31 }, 566 | end: { offset: 591, line: 26, column: 34 }, 567 | }, 568 | }, 569 | args: [], 570 | }, 571 | ], 572 | location: { 573 | start: { offset: 579, line: 26, column: 22 }, 574 | end: { offset: 594, line: 26, column: 37 }, 575 | }, 576 | }, 577 | { 578 | kind: "fieldAttribute", 579 | path: { 580 | kind: "path", 581 | value: ["map"], 582 | location: { 583 | start: { offset: 596, line: 26, column: 39 }, 584 | end: { offset: 599, line: 26, column: 42 }, 585 | }, 586 | }, 587 | args: [{ kind: "literal", value: "created_at" }], 588 | location: { 589 | start: { offset: 595, line: 26, column: 38 }, 590 | end: { offset: 613, line: 26, column: 56 }, 591 | }, 592 | }, 593 | ], 594 | comment: null, 595 | location: { 596 | start: { offset: 560, line: 26, column: 3 }, 597 | end: { offset: 613, line: 26, column: 56 }, 598 | }, 599 | }, 600 | { 601 | kind: "field", 602 | name: { 603 | kind: "name", 604 | value: "updatedAt", 605 | location: { 606 | start: { offset: 616, line: 27, column: 3 }, 607 | end: { offset: 625, line: 27, column: 12 }, 608 | }, 609 | }, 610 | type: { 611 | kind: "typeId", 612 | name: { 613 | kind: "name", 614 | value: "DateTime", 615 | location: { 616 | start: { offset: 626, line: 27, column: 13 }, 617 | end: { offset: 634, line: 27, column: 21 }, 618 | }, 619 | }, 620 | }, 621 | attributes: [ 622 | { 623 | kind: "fieldAttribute", 624 | path: { 625 | kind: "path", 626 | value: ["default"], 627 | location: { 628 | start: { offset: 636, line: 27, column: 23 }, 629 | end: { offset: 643, line: 27, column: 30 }, 630 | }, 631 | }, 632 | args: [ 633 | { 634 | kind: "functionCall", 635 | path: { 636 | kind: "path", 637 | value: ["now"], 638 | location: { 639 | start: { offset: 644, line: 27, column: 31 }, 640 | end: { offset: 647, line: 27, column: 34 }, 641 | }, 642 | }, 643 | args: [], 644 | }, 645 | ], 646 | location: { 647 | start: { offset: 635, line: 27, column: 22 }, 648 | end: { offset: 650, line: 27, column: 37 }, 649 | }, 650 | }, 651 | { 652 | kind: "fieldAttribute", 653 | path: { 654 | kind: "path", 655 | value: ["updatedAt"], 656 | location: { 657 | start: { offset: 652, line: 27, column: 39 }, 658 | end: { offset: 662, line: 27, column: 49 }, 659 | }, 660 | }, 661 | args: [], 662 | location: { 663 | start: { offset: 651, line: 27, column: 38 }, 664 | end: { offset: 662, line: 27, column: 49 }, 665 | }, 666 | }, 667 | { 668 | kind: "fieldAttribute", 669 | path: { 670 | kind: "path", 671 | value: ["map"], 672 | location: { 673 | start: { offset: 663, line: 27, column: 50 }, 674 | end: { offset: 666, line: 27, column: 53 }, 675 | }, 676 | }, 677 | args: [{ kind: "literal", value: "updated_at" }], 678 | location: { 679 | start: { offset: 662, line: 27, column: 49 }, 680 | end: { offset: 680, line: 27, column: 67 }, 681 | }, 682 | }, 683 | ], 684 | comment: null, 685 | location: { 686 | start: { offset: 616, line: 27, column: 3 }, 687 | end: { offset: 680, line: 27, column: 67 }, 688 | }, 689 | }, 690 | { 691 | kind: "field", 692 | name: { 693 | kind: "name", 694 | value: "children", 695 | location: { 696 | start: { offset: 684, line: 29, column: 3 }, 697 | end: { offset: 692, line: 29, column: 11 }, 698 | }, 699 | }, 700 | type: { 701 | kind: "list", 702 | type: { 703 | kind: "typeId", 704 | name: { 705 | kind: "name", 706 | value: "MyOtherModel", 707 | location: { 708 | start: { offset: 693, line: 29, column: 12 }, 709 | end: { offset: 705, line: 29, column: 24 }, 710 | }, 711 | }, 712 | }, 713 | }, 714 | attributes: [], 715 | comment: null, 716 | location: { 717 | start: { offset: 684, line: 29, column: 3 }, 718 | end: { offset: 707, line: 29, column: 26 }, 719 | }, 720 | }, 721 | { 722 | kind: "blockAttribute", 723 | path: { 724 | kind: "path", 725 | value: ["unique"], 726 | location: { 727 | start: { offset: 713, line: 31, column: 5 }, 728 | end: { offset: 719, line: 31, column: 11 }, 729 | }, 730 | }, 731 | args: [ 732 | { 733 | kind: "array", 734 | items: [ 735 | { 736 | kind: "path", 737 | value: ["type"], 738 | location: { 739 | start: { offset: 721, line: 31, column: 13 }, 740 | end: { offset: 725, line: 31, column: 17 }, 741 | }, 742 | }, 743 | { 744 | kind: "path", 745 | value: ["version"], 746 | location: { 747 | start: { offset: 727, line: 31, column: 19 }, 748 | end: { offset: 734, line: 31, column: 26 }, 749 | }, 750 | }, 751 | ], 752 | }, 753 | ], 754 | comment: { 755 | kind: "comment", 756 | text: "Block attribute comment", 757 | location: { 758 | start: { offset: 737, line: 31, column: 29 }, 759 | end: { offset: 763, line: 31, column: 55 }, 760 | }, 761 | }, 762 | location: { 763 | start: { offset: 711, line: 31, column: 3 }, 764 | end: { offset: 763, line: 31, column: 55 }, 765 | }, 766 | }, 767 | { 768 | kind: "blockAttribute", 769 | path: { 770 | kind: "path", 771 | value: ["index"], 772 | location: { 773 | start: { offset: 768, line: 32, column: 5 }, 774 | end: { offset: 773, line: 32, column: 10 }, 775 | }, 776 | }, 777 | args: [ 778 | { 779 | kind: "array", 780 | items: [ 781 | { 782 | kind: "functionCall", 783 | path: { 784 | kind: "path", 785 | value: ["createdAt"], 786 | location: { 787 | start: { offset: 775, line: 32, column: 12 }, 788 | end: { offset: 784, line: 32, column: 21 }, 789 | }, 790 | }, 791 | args: [ 792 | { 793 | kind: "namedArgument", 794 | name: { 795 | kind: "name", 796 | value: "sort", 797 | location: { 798 | start: { offset: 785, line: 32, column: 22 }, 799 | end: { offset: 789, line: 32, column: 26 }, 800 | }, 801 | }, 802 | expression: { 803 | kind: "path", 804 | value: ["Desc"], 805 | location: { 806 | start: { offset: 791, line: 32, column: 28 }, 807 | end: { offset: 795, line: 32, column: 32 }, 808 | }, 809 | }, 810 | }, 811 | { 812 | kind: "namedArgument", 813 | name: { 814 | kind: "name", 815 | value: "ops", 816 | location: { 817 | start: { offset: 797, line: 32, column: 34 }, 818 | end: { offset: 800, line: 32, column: 37 }, 819 | }, 820 | }, 821 | expression: { 822 | kind: "functionCall", 823 | path: { 824 | kind: "path", 825 | value: ["raw"], 826 | location: { 827 | start: { offset: 802, line: 32, column: 39 }, 828 | end: { offset: 805, line: 32, column: 42 }, 829 | }, 830 | }, 831 | args: [{ kind: "literal", value: "other" }], 832 | }, 833 | }, 834 | ], 835 | }, 836 | ], 837 | }, 838 | { 839 | kind: "namedArgument", 840 | name: { 841 | kind: "name", 842 | value: "type", 843 | location: { 844 | start: { offset: 818, line: 32, column: 55 }, 845 | end: { offset: 822, line: 32, column: 59 }, 846 | }, 847 | }, 848 | expression: { 849 | kind: "path", 850 | value: ["BTree"], 851 | location: { 852 | start: { offset: 824, line: 32, column: 61 }, 853 | end: { offset: 829, line: 32, column: 66 }, 854 | }, 855 | }, 856 | }, 857 | ], 858 | comment: null, 859 | location: { 860 | start: { offset: 766, line: 32, column: 3 }, 861 | end: { offset: 830, line: 32, column: 67 }, 862 | }, 863 | }, 864 | { 865 | kind: "blockAttribute", 866 | path: { 867 | kind: "path", 868 | value: ["map"], 869 | location: { 870 | start: { offset: 835, line: 33, column: 5 }, 871 | end: { offset: 838, line: 33, column: 8 }, 872 | }, 873 | }, 874 | args: [{ kind: "literal", value: "my_model" }], 875 | comment: null, 876 | location: { 877 | start: { offset: 833, line: 33, column: 3 }, 878 | end: { offset: 850, line: 33, column: 20 }, 879 | }, 880 | }, 881 | ], 882 | location: { 883 | start: { offset: 359, line: 21, column: 1 }, 884 | end: { offset: 852, line: 34, column: 2 }, 885 | }, 886 | }, 887 | { 888 | kind: "type", 889 | name: { 890 | kind: "name", 891 | value: "MyType", 892 | location: { 893 | start: { offset: 859, line: 36, column: 6 }, 894 | end: { offset: 865, line: 36, column: 12 }, 895 | }, 896 | }, 897 | members: [ 898 | { 899 | kind: "field", 900 | name: { 901 | kind: "name", 902 | value: "field1", 903 | location: { 904 | start: { offset: 870, line: 37, column: 3 }, 905 | end: { offset: 876, line: 37, column: 9 }, 906 | }, 907 | }, 908 | type: { 909 | kind: "typeId", 910 | name: { 911 | kind: "name", 912 | value: "Float", 913 | location: { 914 | start: { offset: 877, line: 37, column: 10 }, 915 | end: { offset: 882, line: 37, column: 15 }, 916 | }, 917 | }, 918 | }, 919 | attributes: [], 920 | comment: null, 921 | location: { 922 | start: { offset: 870, line: 37, column: 3 }, 923 | end: { offset: 882, line: 37, column: 15 }, 924 | }, 925 | }, 926 | { 927 | kind: "field", 928 | name: { 929 | kind: "name", 930 | value: "field2", 931 | location: { 932 | start: { offset: 885, line: 38, column: 3 }, 933 | end: { offset: 891, line: 38, column: 9 }, 934 | }, 935 | }, 936 | type: { 937 | kind: "typeId", 938 | name: { 939 | kind: "name", 940 | value: "Boolean", 941 | location: { 942 | start: { offset: 892, line: 38, column: 10 }, 943 | end: { offset: 899, line: 38, column: 17 }, 944 | }, 945 | }, 946 | }, 947 | attributes: [ 948 | { 949 | kind: "fieldAttribute", 950 | path: { 951 | kind: "path", 952 | value: ["default"], 953 | location: { 954 | start: { offset: 913, line: 38, column: 31 }, 955 | end: { offset: 920, line: 38, column: 38 }, 956 | }, 957 | }, 958 | args: [{ kind: "literal", value: true }], 959 | location: { 960 | start: { offset: 912, line: 38, column: 30 }, 961 | end: { offset: 926, line: 38, column: 44 }, 962 | }, 963 | }, 964 | ], 965 | comment: null, 966 | location: { 967 | start: { offset: 885, line: 38, column: 3 }, 968 | end: { offset: 926, line: 38, column: 44 }, 969 | }, 970 | }, 971 | { 972 | kind: "field", 973 | name: { 974 | kind: "name", 975 | value: "field3", 976 | location: { 977 | start: { offset: 929, line: 39, column: 3 }, 978 | end: { offset: 935, line: 39, column: 9 }, 979 | }, 980 | }, 981 | type: { 982 | kind: "typeId", 983 | name: { 984 | kind: "name", 985 | value: "Json", 986 | location: { 987 | start: { offset: 936, line: 39, column: 10 }, 988 | end: { offset: 940, line: 39, column: 14 }, 989 | }, 990 | }, 991 | }, 992 | attributes: [], 993 | comment: null, 994 | location: { 995 | start: { offset: 929, line: 39, column: 3 }, 996 | end: { offset: 940, line: 39, column: 14 }, 997 | }, 998 | }, 999 | { 1000 | kind: "field", 1001 | name: { 1002 | kind: "name", 1003 | value: "field4", 1004 | location: { 1005 | start: { offset: 943, line: 40, column: 3 }, 1006 | end: { offset: 949, line: 40, column: 9 }, 1007 | }, 1008 | }, 1009 | type: { 1010 | kind: "unsupported", 1011 | type: { kind: "literal", value: "type" }, 1012 | }, 1013 | attributes: [], 1014 | comment: null, 1015 | location: { 1016 | start: { offset: 943, line: 40, column: 3 }, 1017 | end: { offset: 969, line: 40, column: 29 }, 1018 | }, 1019 | }, 1020 | ], 1021 | location: { 1022 | start: { offset: 854, line: 36, column: 1 }, 1023 | end: { offset: 971, line: 41, column: 2 }, 1024 | }, 1025 | }, 1026 | { 1027 | kind: "model", 1028 | name: { 1029 | kind: "name", 1030 | value: "MyOtherModel", 1031 | location: { 1032 | start: { offset: 979, line: 43, column: 7 }, 1033 | end: { offset: 991, line: 43, column: 19 }, 1034 | }, 1035 | }, 1036 | members: [ 1037 | { 1038 | kind: "field", 1039 | name: { 1040 | kind: "name", 1041 | value: "id", 1042 | location: { 1043 | start: { offset: 996, line: 44, column: 3 }, 1044 | end: { offset: 998, line: 44, column: 5 }, 1045 | }, 1046 | }, 1047 | type: { 1048 | kind: "typeId", 1049 | name: { 1050 | kind: "name", 1051 | value: "String", 1052 | location: { 1053 | start: { offset: 1005, line: 44, column: 12 }, 1054 | end: { offset: 1011, line: 44, column: 18 }, 1055 | }, 1056 | }, 1057 | }, 1058 | attributes: [ 1059 | { 1060 | kind: "fieldAttribute", 1061 | path: { 1062 | kind: "path", 1063 | value: ["id"], 1064 | location: { 1065 | start: { offset: 1013, line: 44, column: 20 }, 1066 | end: { offset: 1016, line: 44, column: 23 }, 1067 | }, 1068 | }, 1069 | args: [], 1070 | location: { 1071 | start: { offset: 1012, line: 44, column: 19 }, 1072 | end: { offset: 1016, line: 44, column: 23 }, 1073 | }, 1074 | }, 1075 | { 1076 | kind: "fieldAttribute", 1077 | path: { 1078 | kind: "path", 1079 | value: ["default"], 1080 | location: { 1081 | start: { offset: 1017, line: 44, column: 24 }, 1082 | end: { offset: 1024, line: 44, column: 31 }, 1083 | }, 1084 | }, 1085 | args: [ 1086 | { 1087 | kind: "functionCall", 1088 | path: { 1089 | kind: "path", 1090 | value: ["uuid"], 1091 | location: { 1092 | start: { offset: 1025, line: 44, column: 32 }, 1093 | end: { offset: 1029, line: 44, column: 36 }, 1094 | }, 1095 | }, 1096 | args: [], 1097 | }, 1098 | ], 1099 | location: { 1100 | start: { offset: 1016, line: 44, column: 23 }, 1101 | end: { offset: 1032, line: 44, column: 39 }, 1102 | }, 1103 | }, 1104 | { 1105 | kind: "fieldAttribute", 1106 | path: { 1107 | kind: "path", 1108 | value: ["map"], 1109 | location: { 1110 | start: { offset: 1034, line: 44, column: 41 }, 1111 | end: { offset: 1037, line: 44, column: 44 }, 1112 | }, 1113 | }, 1114 | args: [{ kind: "literal", value: "_id" }], 1115 | location: { 1116 | start: { offset: 1033, line: 44, column: 40 }, 1117 | end: { offset: 1044, line: 44, column: 51 }, 1118 | }, 1119 | }, 1120 | ], 1121 | comment: null, 1122 | location: { 1123 | start: { offset: 996, line: 44, column: 3 }, 1124 | end: { offset: 1044, line: 44, column: 51 }, 1125 | }, 1126 | }, 1127 | { 1128 | kind: "field", 1129 | name: { 1130 | kind: "name", 1131 | value: "parentId", 1132 | location: { 1133 | start: { offset: 1047, line: 45, column: 3 }, 1134 | end: { offset: 1055, line: 45, column: 11 }, 1135 | }, 1136 | }, 1137 | type: { 1138 | kind: "typeId", 1139 | name: { 1140 | kind: "name", 1141 | value: "String", 1142 | location: { 1143 | start: { offset: 1056, line: 45, column: 12 }, 1144 | end: { offset: 1062, line: 45, column: 18 }, 1145 | }, 1146 | }, 1147 | }, 1148 | attributes: [], 1149 | comment: null, 1150 | location: { 1151 | start: { offset: 1047, line: 45, column: 3 }, 1152 | end: { offset: 1062, line: 45, column: 18 }, 1153 | }, 1154 | }, 1155 | { 1156 | kind: "field", 1157 | name: { 1158 | kind: "name", 1159 | value: "text", 1160 | location: { 1161 | start: { offset: 1065, line: 46, column: 3 }, 1162 | end: { offset: 1069, line: 46, column: 7 }, 1163 | }, 1164 | }, 1165 | type: { 1166 | kind: "typeId", 1167 | name: { 1168 | kind: "name", 1169 | value: "String", 1170 | location: { 1171 | start: { offset: 1074, line: 46, column: 12 }, 1172 | end: { offset: 1080, line: 46, column: 18 }, 1173 | }, 1174 | }, 1175 | }, 1176 | attributes: [], 1177 | comment: null, 1178 | location: { 1179 | start: { offset: 1065, line: 46, column: 3 }, 1180 | end: { offset: 1080, line: 46, column: 18 }, 1181 | }, 1182 | }, 1183 | { 1184 | kind: "field", 1185 | name: { 1186 | kind: "name", 1187 | value: "parent", 1188 | location: { 1189 | start: { offset: 1084, line: 48, column: 3 }, 1190 | end: { offset: 1090, line: 48, column: 9 }, 1191 | }, 1192 | }, 1193 | type: { 1194 | kind: "typeId", 1195 | name: { 1196 | kind: "name", 1197 | value: "MyModel", 1198 | location: { 1199 | start: { offset: 1091, line: 48, column: 10 }, 1200 | end: { offset: 1098, line: 48, column: 17 }, 1201 | }, 1202 | }, 1203 | }, 1204 | attributes: [ 1205 | { 1206 | kind: "fieldAttribute", 1207 | path: { 1208 | kind: "path", 1209 | value: ["relation"], 1210 | location: { 1211 | start: { offset: 1100, line: 48, column: 19 }, 1212 | end: { offset: 1108, line: 48, column: 27 }, 1213 | }, 1214 | }, 1215 | args: [ 1216 | { 1217 | kind: "namedArgument", 1218 | name: { 1219 | kind: "name", 1220 | value: "fields", 1221 | location: { 1222 | start: { offset: 1109, line: 48, column: 28 }, 1223 | end: { offset: 1115, line: 48, column: 34 }, 1224 | }, 1225 | }, 1226 | expression: { 1227 | kind: "array", 1228 | items: [ 1229 | { 1230 | kind: "path", 1231 | value: ["parentId"], 1232 | location: { 1233 | start: { offset: 1118, line: 48, column: 37 }, 1234 | end: { offset: 1126, line: 48, column: 45 }, 1235 | }, 1236 | }, 1237 | ], 1238 | }, 1239 | }, 1240 | { 1241 | kind: "namedArgument", 1242 | name: { 1243 | kind: "name", 1244 | value: "references", 1245 | location: { 1246 | start: { offset: 1129, line: 48, column: 48 }, 1247 | end: { offset: 1139, line: 48, column: 58 }, 1248 | }, 1249 | }, 1250 | expression: { 1251 | kind: "array", 1252 | items: [ 1253 | { 1254 | kind: "path", 1255 | value: ["id"], 1256 | location: { 1257 | start: { offset: 1142, line: 48, column: 61 }, 1258 | end: { offset: 1144, line: 48, column: 63 }, 1259 | }, 1260 | }, 1261 | ], 1262 | }, 1263 | }, 1264 | ], 1265 | location: { 1266 | start: { offset: 1099, line: 48, column: 18 }, 1267 | end: { offset: 1146, line: 48, column: 65 }, 1268 | }, 1269 | }, 1270 | ], 1271 | comment: null, 1272 | location: { 1273 | start: { offset: 1084, line: 48, column: 3 }, 1274 | end: { offset: 1146, line: 48, column: 65 }, 1275 | }, 1276 | }, 1277 | ], 1278 | location: { 1279 | start: { offset: 973, line: 43, column: 1 }, 1280 | end: { offset: 1148, line: 49, column: 2 }, 1281 | }, 1282 | }, 1283 | { 1284 | kind: "view", 1285 | name: { 1286 | kind: "name", 1287 | value: "MyView", 1288 | location: { 1289 | start: { offset: 1155, line: 51, column: 6 }, 1290 | end: { offset: 1161, line: 51, column: 12 }, 1291 | }, 1292 | }, 1293 | members: [ 1294 | { 1295 | kind: "field", 1296 | name: { 1297 | kind: "name", 1298 | value: "id", 1299 | location: { 1300 | start: { offset: 1166, line: 52, column: 3 }, 1301 | end: { offset: 1168, line: 52, column: 5 }, 1302 | }, 1303 | }, 1304 | type: { 1305 | kind: "typeId", 1306 | name: { 1307 | kind: "name", 1308 | value: "Int", 1309 | location: { 1310 | start: { offset: 1172, line: 52, column: 9 }, 1311 | end: { offset: 1175, line: 52, column: 12 }, 1312 | }, 1313 | }, 1314 | }, 1315 | attributes: [ 1316 | { 1317 | kind: "fieldAttribute", 1318 | path: { 1319 | kind: "path", 1320 | value: ["id"], 1321 | location: { 1322 | start: { offset: 1180, line: 52, column: 17 }, 1323 | end: { offset: 1183, line: 52, column: 20 }, 1324 | }, 1325 | }, 1326 | args: [], 1327 | location: { 1328 | start: { offset: 1179, line: 52, column: 16 }, 1329 | end: { offset: 1183, line: 52, column: 20 }, 1330 | }, 1331 | }, 1332 | { 1333 | kind: "fieldAttribute", 1334 | path: { 1335 | kind: "path", 1336 | value: ["map"], 1337 | location: { 1338 | start: { offset: 1184, line: 52, column: 21 }, 1339 | end: { offset: 1187, line: 52, column: 24 }, 1340 | }, 1341 | }, 1342 | args: [{ kind: "literal", value: "_id" }], 1343 | location: { 1344 | start: { offset: 1183, line: 52, column: 20 }, 1345 | end: { offset: 1194, line: 52, column: 31 }, 1346 | }, 1347 | }, 1348 | ], 1349 | comment: null, 1350 | location: { 1351 | start: { offset: 1166, line: 52, column: 3 }, 1352 | end: { offset: 1194, line: 52, column: 31 }, 1353 | }, 1354 | }, 1355 | { 1356 | kind: "field", 1357 | name: { 1358 | kind: "name", 1359 | value: "email", 1360 | location: { 1361 | start: { offset: 1197, line: 53, column: 3 }, 1362 | end: { offset: 1202, line: 53, column: 8 }, 1363 | }, 1364 | }, 1365 | type: { 1366 | kind: "typeId", 1367 | name: { 1368 | kind: "name", 1369 | value: "String", 1370 | location: { 1371 | start: { offset: 1203, line: 53, column: 9 }, 1372 | end: { offset: 1209, line: 53, column: 15 }, 1373 | }, 1374 | }, 1375 | }, 1376 | attributes: [], 1377 | comment: null, 1378 | location: { 1379 | start: { offset: 1197, line: 53, column: 3 }, 1380 | end: { offset: 1209, line: 53, column: 15 }, 1381 | }, 1382 | }, 1383 | { 1384 | kind: "field", 1385 | name: { 1386 | kind: "name", 1387 | value: "name", 1388 | location: { 1389 | start: { offset: 1212, line: 54, column: 3 }, 1390 | end: { offset: 1216, line: 54, column: 7 }, 1391 | }, 1392 | }, 1393 | type: { 1394 | kind: "typeId", 1395 | name: { 1396 | kind: "name", 1397 | value: "String", 1398 | location: { 1399 | start: { offset: 1218, line: 54, column: 9 }, 1400 | end: { offset: 1224, line: 54, column: 15 }, 1401 | }, 1402 | }, 1403 | }, 1404 | attributes: [], 1405 | comment: null, 1406 | location: { 1407 | start: { offset: 1212, line: 54, column: 3 }, 1408 | end: { offset: 1224, line: 54, column: 15 }, 1409 | }, 1410 | }, 1411 | { 1412 | kind: "field", 1413 | name: { 1414 | kind: "name", 1415 | value: "bio", 1416 | location: { 1417 | start: { offset: 1227, line: 55, column: 3 }, 1418 | end: { offset: 1230, line: 55, column: 6 }, 1419 | }, 1420 | }, 1421 | type: { 1422 | kind: "typeId", 1423 | name: { 1424 | kind: "name", 1425 | value: "String", 1426 | location: { 1427 | start: { offset: 1233, line: 55, column: 9 }, 1428 | end: { offset: 1239, line: 55, column: 15 }, 1429 | }, 1430 | }, 1431 | }, 1432 | attributes: [], 1433 | comment: null, 1434 | location: { 1435 | start: { offset: 1227, line: 55, column: 3 }, 1436 | end: { offset: 1239, line: 55, column: 15 }, 1437 | }, 1438 | }, 1439 | { 1440 | kind: "blockAttribute", 1441 | path: { 1442 | kind: "path", 1443 | value: ["map"], 1444 | location: { 1445 | start: { offset: 1245, line: 57, column: 5 }, 1446 | end: { offset: 1248, line: 57, column: 8 }, 1447 | }, 1448 | }, 1449 | args: [ 1450 | { kind: "literal", value: "my_view" }, 1451 | ], 1452 | comment: null, 1453 | location: { 1454 | start: { offset: 1243, line: 57, column: 3 }, 1455 | end: { offset: 1259, line: 57, column: 19 }, 1456 | }, 1457 | }, 1458 | ], 1459 | location: { 1460 | start: { offset: 1150, line: 51, column: 1 }, 1461 | end: { offset: 1261, line: 58, column: 2 }, 1462 | }, 1463 | }, 1464 | ], 1465 | }; 1466 | 1467 | export default ast; 1468 | -------------------------------------------------------------------------------- /test-data/schema.prisma: -------------------------------------------------------------------------------- 1 | // Schema comment 2 | 3 | /// Doc comment 4 | generator client { 5 | provider = "prisma-client-js" // Trailing comment 6 | output = "src/__generated__/PrismaClient" 7 | previewFeatures = ["views"] 8 | } 9 | 10 | datasource postgresql { 11 | provider = "mongodb" 12 | url = env("DATABASE_URL") 13 | } 14 | 15 | enum MyEnum { 16 | FirstValue @map("v1") // Enum value comment 17 | SecondValue 18 | ThirdValue 19 | } 20 | 21 | model MyModel { 22 | id String @id @default(uuid()) @map("_id") // Field comment 23 | type MyEnum? 24 | version Int @default(0) 25 | uniqueKey String @unique(sort: Asc) @map("unique_key") 26 | createdAt DateTime @default(now()) @map("created_at") 27 | updatedAt DateTime @default(now()) @updatedAt @map("updated_at") 28 | 29 | children MyOtherModel[] 30 | 31 | @@unique([type, version]) // Block attribute comment 32 | @@index([createdAt(sort: Desc, ops: raw("other"))], type: BTree) 33 | @@map("my_model") 34 | } 35 | 36 | type MyType { 37 | field1 Float 38 | field2 Boolean @default(true) 39 | field3 Json 40 | field4 Unsupported("type") 41 | } 42 | 43 | model MyOtherModel { 44 | id String @id @default(uuid()) @map("_id") 45 | parentId String 46 | text String 47 | 48 | parent MyModel @relation(fields: [parentId], references: [id]) 49 | } 50 | 51 | view MyView { 52 | id Int @id @map("_id") 53 | email String 54 | name String 55 | bio String 56 | 57 | @@map("my_view") 58 | } 59 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["src/**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "include": ["src"] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "declaration": true, 6 | "declarationMap": true, 7 | "resolveJsonModule": true, 8 | "sourceMap": true, 9 | "outDir": "dist" 10 | }, 11 | "include": ["src"] 12 | } 13 | --------------------------------------------------------------------------------