├── .github └── workflows │ ├── main.yml │ └── size.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package.json ├── postbuild.js ├── src ├── attribute-fns │ └── index.ts ├── enum │ └── index.ts ├── env │ └── index.ts ├── field-modifiers │ ├── default.test.ts │ ├── default.ts │ ├── index.ts │ ├── list.test.ts │ ├── list.ts │ ├── optional.test.ts │ ├── optional.ts │ ├── primary.test.ts │ ├── primary.ts │ ├── relation.test.ts │ ├── relation.ts │ ├── unique.test.ts │ ├── unique.ts │ ├── updated-at.test.ts │ └── updated-at.ts ├── field-types │ ├── belongs-to.ts │ ├── big-int.ts │ ├── boolean.ts │ ├── bytes.ts │ ├── datetime.ts │ ├── decimal.ts │ ├── enum.ts │ ├── float.ts │ ├── has-many.ts │ ├── has-one.ts │ ├── id.ts │ ├── index.ts │ ├── int.ts │ ├── json.ts │ ├── raw.ts │ ├── relation.ts │ ├── string.ts │ └── timestamps.ts ├── index.ts ├── interfaces │ └── index.ts ├── model │ └── index.ts ├── schema-gen │ └── index.ts └── utils │ ├── enquoteString.ts │ ├── generateFieldSchema.ts │ ├── generateModifier.ts │ └── index.ts ├── tsconfig.json └── yarn.lock /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['10.x', '12.x', '14.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Lint 26 | run: yarn lint 27 | 28 | - name: Test 29 | run: yarn test --ci --coverage --maxWorkers=2 30 | 31 | - name: Build 32 | run: yarn build 33 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Chinedu Daniel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prisma Prime 2 | 3 | Schema definition for Prisma in JavaScript/TypeScript. Prisma Prime allows you to programmatically generate your Prisma schema using TypeScript or JavaScript. 4 | 5 | ## Status 6 | 7 | This project is still in active development and not recommended for use in a production environment. 8 | 9 | ## Usage 10 | 11 | Example: 12 | 13 | ```ts 14 | // db/models/Post.ts 15 | import { model } from 'prisma-prime'; 16 | 17 | const Post = model('Post', t => { 18 | t.id(); 19 | t.string('title', { unique: true }); 20 | t.boolean('draft'); 21 | t.string('categories', { list: true }); 22 | t.string('slug', { unique: true }); 23 | t.hasOne('author', { source: 'User' }); 24 | t.hasMany('comments', { source: 'Comment' }); 25 | t.timestamps(); 26 | t.uniqueScope('title', 'slug'); 27 | }); 28 | 29 | // db/models/User.ts 30 | import { model, enumType } from 'prisma-prime'; 31 | 32 | const Role = enumType('Role', ['USER', 'ADMIN']); 33 | 34 | const User = model('User', t => { 35 | t.id(); 36 | t.string('fullName'); 37 | t.string('email', { unique: true }); 38 | t.enum('role', { source: Role }); 39 | t.hasMany('posts', { source: 'Post' }); 40 | t.timestamps(); 41 | }); 42 | 43 | // db/models/Comment.ts 44 | import { model } from 'prisma-prime'; 45 | 46 | const Comment = model('Comment', t => { 47 | t.id(); 48 | t.string('content'); 49 | t.belongsTo('post', { source: 'Post' }); 50 | t.timestamps(); 51 | }); 52 | 53 | // db/models/schema.ts 54 | import { generateSchema } from 'prisma-prime'; 55 | 56 | const schema = generateSchema({ 57 | datasource: { 58 | provider: 'postgresql', 59 | url: 60 | process.env.DATABASE_URL || 61 | 'postgresql://user@localhost:5432/prisma-prime', 62 | }, 63 | generator: { 64 | provider: 'prisma-client-js', 65 | }, 66 | models: { 67 | User, 68 | Post, 69 | Comment, 70 | }, 71 | }); 72 | ``` 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prisma-prime", 3 | "version": "0.2.0", 4 | "author": "Chinedu Daniel", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/thechinedu/prisma-prime" 8 | }, 9 | "description": "Prisma schema definition for JavaScript/TypeScript", 10 | "license": "MIT", 11 | "main": "dist/index.js", 12 | "typings": "dist/index.d.ts", 13 | "files": [ 14 | "dist" 15 | ], 16 | "engines": { 17 | "node": ">=10" 18 | }, 19 | "scripts": { 20 | "start": "tsdx watch", 21 | "build": "tsdx build --target node --format cjs --name prime", 22 | "postbuild": "node ./postbuild", 23 | "test": "tsdx test", 24 | "lint": "tsdx lint", 25 | "prepare": "yarn build", 26 | "size": "size-limit", 27 | "analyze": "size-limit --why" 28 | }, 29 | "husky": { 30 | "hooks": { 31 | "pre-commit": "tsdx lint" 32 | } 33 | }, 34 | "prettier": { 35 | "printWidth": 80, 36 | "semi": true, 37 | "singleQuote": true, 38 | "trailingComma": "es5" 39 | }, 40 | "size-limit": [ 41 | { 42 | "path": "dist/index.js", 43 | "limit": "10 KB" 44 | } 45 | ], 46 | "dependencies": { 47 | "@prisma/sdk": "^2.23.0" 48 | }, 49 | "devDependencies": { 50 | "@size-limit/preset-small-lib": "^4.10.1", 51 | "husky": "^5.1.3", 52 | "size-limit": "^4.10.1", 53 | "tsdx": "^0.14.1", 54 | "tslib": "^2.1.0", 55 | "typescript": "^4.2.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /postbuild.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | 3 | const run = command => { 4 | return new Promise((resolve, reject) => { 5 | exec(command, err => { 6 | if (err) reject(err); 7 | 8 | resolve(); 9 | }); 10 | }); 11 | }; 12 | 13 | (async () => { 14 | await run('cp ./dist/prime.cjs.production.min.js ./dist/index.js'); 15 | await run('cp ./dist/prime.cjs.production.min.js.map ./dist/index.js.map'); 16 | await run('rm -rf ./dist/prime.*'); 17 | })(); 18 | -------------------------------------------------------------------------------- /src/attribute-fns/index.ts: -------------------------------------------------------------------------------- 1 | import { Defaults } from '../interfaces'; 2 | import { enquoteString } from '../utils'; 3 | 4 | const autoincrement = () => 'autoincrement()' as Defaults.autoincrement; 5 | const cuid = () => 'cuid()' as Defaults.cuid; 6 | const dbgenerated = (value: string = '') => 7 | `dbgenerated(${enquoteString(value)})` as Defaults.dbgenerated; 8 | const now = () => 'now()' as Defaults.now; 9 | const uuid = () => 'uuid()' as Defaults.uuid; 10 | 11 | export { autoincrement, cuid, dbgenerated, now, uuid }; 12 | -------------------------------------------------------------------------------- /src/enum/index.ts: -------------------------------------------------------------------------------- 1 | import { Enum } from '../interfaces'; 2 | 3 | export const enumType = (name: string, keys: string[]): Enum => { 4 | const fields: Record = {}; 5 | 6 | for (const key of keys) fields[key] = key; 7 | 8 | const res = { 9 | name, 10 | ...fields, 11 | toSchema: `enum ${name} {\n${keys.join('\n')}\n}`, 12 | }; 13 | 14 | return res; 15 | }; 16 | -------------------------------------------------------------------------------- /src/env/index.ts: -------------------------------------------------------------------------------- 1 | export const env = (key: string, defaultValue = '') => { 2 | const result = process.env[key]; 3 | return result ? `env("DATABASE_URL")` : defaultValue; 4 | }; 5 | -------------------------------------------------------------------------------- /src/field-modifiers/default.test.ts: -------------------------------------------------------------------------------- 1 | import { fieldDefault } from './default'; 2 | 3 | describe('Default field modifier', () => { 4 | it('defines a default value for a field for with string type', () => { 5 | expect(fieldDefault('name String', 'hello world')).toBe( 6 | 'name String @default("hello world")' 7 | ); 8 | }); 9 | 10 | it('defines a default value for a field for with number type', () => { 11 | expect(fieldDefault('age Int', 42)).toBe('age Int @default(42)'); 12 | }); 13 | 14 | it('defines a default value for a field for with boolean type', () => { 15 | expect(fieldDefault('isCool Boolean', true)).toBe( 16 | 'isCool Boolean @default(true)' 17 | ); 18 | expect(fieldDefault('isRowdy Boolean', false)).toBe( 19 | 'isRowdy Boolean @default(false)' 20 | ); 21 | }); 22 | 23 | it('defines a default value for a field for with datetime type', () => { 24 | expect(fieldDefault('createdAt DateTime', '2021-03-28T23:00:00.000Z')).toBe( 25 | 'createdAt DateTime @default("2021-03-28T23:00:00.000Z")' 26 | ); 27 | expect(fieldDefault('createdAt DateTime', 'now()')).toBe( 28 | 'createdAt DateTime @default(now())' 29 | ); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/field-modifiers/default.ts: -------------------------------------------------------------------------------- 1 | import { generateModifier, enquoteString } from '../utils'; 2 | 3 | export const fieldDefault = ( 4 | fieldSchema: string, 5 | value: boolean | number | string 6 | ) => generateModifier(fieldSchema, 'default', enquoteString(value)); 7 | -------------------------------------------------------------------------------- /src/field-modifiers/index.ts: -------------------------------------------------------------------------------- 1 | import { fieldDefault } from './default'; 2 | import { list } from './list'; 3 | import { optional } from './optional'; 4 | import { primary } from './primary'; 5 | import { relation } from './relation'; 6 | import { unique } from './unique'; 7 | import { updatedAt } from './updated-at'; 8 | 9 | export const fieldModifierFns = { 10 | default: fieldDefault, 11 | list, 12 | optional, 13 | primary, 14 | relation, 15 | unique, 16 | updatedAt, 17 | }; 18 | -------------------------------------------------------------------------------- /src/field-modifiers/list.test.ts: -------------------------------------------------------------------------------- 1 | import { list } from './list'; 2 | 3 | describe('List field modifier', () => { 4 | it('marks a field as a list in the field schema if the field is set to true', () => { 5 | expect(list('name String', true)).toBe('name String[]'); 6 | }); 7 | 8 | it('performs a no-op if the field is set to false', () => { 9 | expect(list('name String', false)).toBe('name String'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/field-modifiers/list.ts: -------------------------------------------------------------------------------- 1 | import { generateModifier } from '../utils'; 2 | 3 | export const list = (fieldSchema: string, value?: boolean) => { 4 | if (!value) return fieldSchema; 5 | return generateModifier(fieldSchema, 'list'); 6 | }; 7 | -------------------------------------------------------------------------------- /src/field-modifiers/optional.test.ts: -------------------------------------------------------------------------------- 1 | import { optional } from './optional'; 2 | 3 | describe('Optional field modifier', () => { 4 | it('marks a field as optional in the field schema if the field is set to true', () => { 5 | expect(optional('name String', true)).toBe('name String?'); 6 | }); 7 | 8 | it('performs a no-op if the field is set to false', () => { 9 | expect(optional('name String', false)).toBe('name String'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/field-modifiers/optional.ts: -------------------------------------------------------------------------------- 1 | import { generateModifier } from '../utils'; 2 | 3 | export const optional = (fieldSchema: string, value?: boolean) => { 4 | if (!value) return fieldSchema; 5 | return generateModifier(fieldSchema, 'optional'); 6 | }; 7 | -------------------------------------------------------------------------------- /src/field-modifiers/primary.test.ts: -------------------------------------------------------------------------------- 1 | import { primary } from './primary'; 2 | 3 | describe('Primary field modifier', () => { 4 | it('adds the @id attribute to the field schema if the field is set to true', () => { 5 | expect(primary('name String', true)).toBe('name String @id'); 6 | }); 7 | 8 | it('performs a no-op if the field is set to false', () => { 9 | expect(primary('name String', false)).toBe('name String'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/field-modifiers/primary.ts: -------------------------------------------------------------------------------- 1 | import { generateModifier } from '../utils'; 2 | 3 | export const primary = (fieldSchema: string, value?: boolean) => { 4 | if (!value) return fieldSchema; 5 | return generateModifier(fieldSchema, 'primary'); 6 | }; 7 | -------------------------------------------------------------------------------- /src/field-modifiers/relation.test.ts: -------------------------------------------------------------------------------- 1 | import { relation } from './relation'; 2 | 3 | describe('Relation field modifier', () => { 4 | it('sets the "name" argument for a relation', () => { 5 | expect(relation('user User', { name: 'UserFollows' })).toBe( 6 | 'user User @relation(name: "UserFollows")' 7 | ); 8 | }); 9 | 10 | it('sets the "fields" argument for a relation', () => { 11 | expect(relation('user User', { fields: ['authorId'] })).toBe( 12 | 'user User @relation(fields: [authorId])' 13 | ); 14 | }); 15 | 16 | it('sets the "references" argument for a relation', () => { 17 | expect(relation('user User', { references: ['id'] })).toBe( 18 | 'user User @relation(references: [id])' 19 | ); 20 | }); 21 | 22 | it('sets all arguments for a relation', () => { 23 | expect( 24 | relation('user User', { 25 | name: 'UserFollows', 26 | fields: ['authorId'], 27 | references: ['id'], 28 | }) 29 | ).toBe( 30 | 'user User @relation(name: "UserFollows", fields: [authorId], references: [id])' 31 | ); 32 | }); 33 | 34 | it('sets the specified arguments only', () => { 35 | expect( 36 | relation('user User', { name: 'UserFollows', references: ['id'] }) 37 | ).toBe('user User @relation(name: "UserFollows", references: [id])'); 38 | 39 | expect( 40 | relation('user User', { name: 'UserFollows', fields: ['authorId'] }) 41 | ).toBe('user User @relation(name: "UserFollows", fields: [authorId])'); 42 | 43 | expect( 44 | relation('user User', { fields: ['authorId'], references: ['id'] }) 45 | ).toBe('user User @relation(fields: [authorId], references: [id])'); 46 | }); 47 | 48 | it('sets the arguments in the required order regardless of object entry order', () => { 49 | expect( 50 | relation('user User', { 51 | references: ['id'], 52 | name: 'UserFollows', 53 | fields: ['authorId'], 54 | }) 55 | ).toBe( 56 | 'user User @relation(name: "UserFollows", fields: [authorId], references: [id])' 57 | ); 58 | }); 59 | 60 | it('omits the relation modifier when no arguments are specified', () => { 61 | expect(relation('user User', {})).toBe('user User'); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/field-modifiers/relation.ts: -------------------------------------------------------------------------------- 1 | import { RelationModifiers } from '../interfaces'; 2 | import { generateModifier } from '../utils'; 3 | 4 | export const relation = ( 5 | fieldSchema: string, 6 | value: Omit 7 | ) => generateModifier(fieldSchema, 'relation', value); 8 | -------------------------------------------------------------------------------- /src/field-modifiers/unique.test.ts: -------------------------------------------------------------------------------- 1 | import { unique } from './unique'; 2 | 3 | describe('Unique field modifier', () => { 4 | it('adds the @unique attribute to the field schema if the field is set to true', () => { 5 | expect(unique('name String', true)).toBe('name String @unique'); 6 | }); 7 | 8 | it('performs a no-op if the field is set to false', () => { 9 | expect(unique('name String', false)).toBe('name String'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/field-modifiers/unique.ts: -------------------------------------------------------------------------------- 1 | import { generateModifier } from '../utils'; 2 | 3 | export const unique = (fieldSchema: string, value?: boolean) => { 4 | if (!value) return fieldSchema; 5 | return generateModifier(fieldSchema, 'unique'); 6 | }; 7 | -------------------------------------------------------------------------------- /src/field-modifiers/updated-at.test.ts: -------------------------------------------------------------------------------- 1 | import { updatedAt } from './updated-at'; 2 | 3 | describe('UpdatedAt field modifier', () => { 4 | it('marks a field with the updatedAt attribute in the field schema if the field is set to true', () => { 5 | expect(updatedAt('name String', true)).toBe('name String @updatedAt'); 6 | }); 7 | 8 | it('performs a no-op if the field is set to false', () => { 9 | expect(updatedAt('name String', false)).toBe('name String'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/field-modifiers/updated-at.ts: -------------------------------------------------------------------------------- 1 | import { generateModifier } from '../utils'; 2 | 3 | export const updatedAt = (fieldSchema: string, value?: boolean) => { 4 | if (!value) return fieldSchema; 5 | return generateModifier(fieldSchema, 'updatedAt'); 6 | }; 7 | -------------------------------------------------------------------------------- /src/field-types/belongs-to.ts: -------------------------------------------------------------------------------- 1 | import { Fields, RelationModifiers } from '../interfaces'; 2 | import { relationImpl } from './relation'; 3 | import { intImpl } from './int'; 4 | 5 | export const belongsToImpl = ( 6 | fields: Fields, 7 | name: string, 8 | modifiers: Pick 9 | ) => { 10 | const foreignKey = `${name}Id`; 11 | 12 | relationImpl( 13 | fields, 14 | name, 15 | Object.assign(modifiers, { fields: [foreignKey], references: ['id'] }) 16 | ); 17 | intImpl(fields, foreignKey); 18 | }; 19 | -------------------------------------------------------------------------------- /src/field-types/big-int.ts: -------------------------------------------------------------------------------- 1 | import { Fields, NumberModifiers } from '../interfaces'; 2 | import { fieldModifierFns } from '../field-modifiers'; 3 | import { generateFieldSchema } from '../utils'; 4 | 5 | export const bigIntImpl = ( 6 | fields: Fields, 7 | name: string, 8 | modifiers?: NumberModifiers 9 | ) => { 10 | const fieldType = 'BigInt'; 11 | 12 | Object.assign(fields, { 13 | [name]: { 14 | type: fieldType, 15 | ...modifiers, 16 | fieldSchema: generateFieldSchema(fieldModifierFns)( 17 | name, 18 | fieldType, 19 | modifiers 20 | ), 21 | }, 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/field-types/boolean.ts: -------------------------------------------------------------------------------- 1 | import { Fields, Modifiers } from '../interfaces'; 2 | import { fieldModifierFns } from '../field-modifiers'; 3 | import { generateFieldSchema } from '../utils'; 4 | 5 | export const booleanImpl = ( 6 | fields: Fields, 7 | name: string, 8 | modifiers?: Modifiers 9 | ) => { 10 | const fieldType = 'Boolean'; 11 | 12 | Object.assign(fields, { 13 | [name]: { 14 | type: fieldType, 15 | ...modifiers, 16 | fieldSchema: generateFieldSchema(fieldModifierFns)( 17 | name, 18 | fieldType, 19 | modifiers 20 | ), 21 | }, 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/field-types/bytes.ts: -------------------------------------------------------------------------------- 1 | import { Fields, Modifiers } from '../interfaces'; 2 | import { fieldModifierFns } from '../field-modifiers'; 3 | import { generateFieldSchema } from '../utils'; 4 | 5 | export const bytesImpl = ( 6 | fields: Fields, 7 | name: string, 8 | modifiers?: Modifiers 9 | ) => { 10 | const fieldType = 'Bytes'; 11 | 12 | Object.assign(fields, { 13 | [name]: { 14 | type: fieldType, 15 | ...modifiers, 16 | fieldSchema: generateFieldSchema(fieldModifierFns)( 17 | name, 18 | fieldType, 19 | modifiers 20 | ), 21 | }, 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/field-types/datetime.ts: -------------------------------------------------------------------------------- 1 | import { Fields, DateTimeModifiers } from '../interfaces'; 2 | import { fieldModifierFns } from '../field-modifiers'; 3 | import { generateFieldSchema } from '../utils'; 4 | 5 | export const datetimeImpl = ( 6 | fields: Fields, 7 | name: string, 8 | modifiers?: DateTimeModifiers 9 | ) => { 10 | const fieldType = 'DateTime'; 11 | 12 | Object.assign(fields, { 13 | [name]: { 14 | type: fieldType, 15 | ...modifiers, 16 | fieldSchema: generateFieldSchema(fieldModifierFns)( 17 | name, 18 | fieldType, 19 | modifiers 20 | ), 21 | }, 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/field-types/decimal.ts: -------------------------------------------------------------------------------- 1 | import { Fields, NumberModifiers } from '../interfaces'; 2 | import { fieldModifierFns } from '../field-modifiers'; 3 | import { generateFieldSchema } from '../utils'; 4 | 5 | export const decimalImpl = ( 6 | fields: Fields, 7 | name: string, 8 | modifiers?: NumberModifiers 9 | ) => { 10 | const fieldType = 'Decimal'; 11 | 12 | Object.assign(fields, { 13 | [name]: { 14 | type: fieldType, 15 | ...modifiers, 16 | fieldSchema: generateFieldSchema(fieldModifierFns)( 17 | name, 18 | fieldType, 19 | modifiers 20 | ), 21 | }, 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/field-types/enum.ts: -------------------------------------------------------------------------------- 1 | import { Fields, EnumModifiers, Enum } from '../interfaces'; 2 | import { fieldModifierFns } from '../field-modifiers'; 3 | import { generateFieldSchema } from '../utils'; 4 | 5 | export const enumImpl = ( 6 | fields: Fields, 7 | name: string, 8 | modifiers: EnumModifiers 9 | ) => { 10 | const fieldType = 11 | (modifiers.source as Enum)?.name || (modifiers.source as string); 12 | const fieldSchema = generateFieldSchema(fieldModifierFns)( 13 | name, 14 | fieldType, 15 | Object.assign(modifiers, { source: null }) 16 | ).replace(/["']/g, ''); 17 | 18 | Object.assign(fields, { 19 | [name]: { 20 | type: fieldType, 21 | ...modifiers, 22 | fieldSchema, 23 | }, 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /src/field-types/float.ts: -------------------------------------------------------------------------------- 1 | import { Fields, NumberModifiers } from '../interfaces'; 2 | import { fieldModifierFns } from '../field-modifiers'; 3 | import { generateFieldSchema } from '../utils'; 4 | 5 | export const floatImpl = ( 6 | fields: Fields, 7 | name: string, 8 | modifiers?: NumberModifiers 9 | ) => { 10 | const fieldType = 'Float'; 11 | 12 | Object.assign(fields, { 13 | [name]: { 14 | type: fieldType, 15 | ...modifiers, 16 | fieldSchema: generateFieldSchema(fieldModifierFns)( 17 | name, 18 | fieldType, 19 | modifiers 20 | ), 21 | }, 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/field-types/has-many.ts: -------------------------------------------------------------------------------- 1 | import { Fields, RelationModifiers } from '../interfaces'; 2 | import { relationImpl } from './relation'; 3 | 4 | export const hasManyImpl = ( 5 | fields: Fields, 6 | name: string, 7 | modifiers: Pick 8 | ) => { 9 | relationImpl(fields, name, Object.assign(modifiers, { list: true })); 10 | }; 11 | -------------------------------------------------------------------------------- /src/field-types/has-one.ts: -------------------------------------------------------------------------------- 1 | import { Fields, RelationModifiers } from '../interfaces'; 2 | import { relationImpl } from './relation'; 3 | 4 | export const hasOneImpl = ( 5 | fields: Fields, 6 | name: string, 7 | modifiers: Pick 8 | ) => { 9 | relationImpl(fields, name, Object.assign(modifiers, { optional: true })); 10 | }; 11 | -------------------------------------------------------------------------------- /src/field-types/id.ts: -------------------------------------------------------------------------------- 1 | import { Fields } from '../interfaces'; 2 | import { autoincrement } from '../attribute-fns'; 3 | import { intImpl } from './int'; 4 | 5 | export const idImpl = (fields: Fields) => { 6 | intImpl(fields, 'id', { primary: true, default: autoincrement() }); 7 | }; 8 | -------------------------------------------------------------------------------- /src/field-types/index.ts: -------------------------------------------------------------------------------- 1 | import { Fields, FieldTypes } from '../interfaces'; 2 | import { belongsToImpl } from './belongs-to'; 3 | import { bigIntImpl } from './big-int'; 4 | import { booleanImpl } from './boolean'; 5 | import { bytesImpl } from './bytes'; 6 | import { datetimeImpl } from './datetime'; 7 | import { decimalImpl } from './decimal'; 8 | import { enumImpl } from './enum'; 9 | import { floatImpl } from './float'; 10 | import { hasManyImpl } from './has-many'; 11 | import { hasOneImpl } from './has-one'; 12 | import { idImpl } from './id'; 13 | import { intImpl } from './int'; 14 | import { jsonImpl } from './json'; 15 | import { rawImpl } from './raw'; 16 | import { relationImpl } from './relation'; 17 | import { stringImpl } from './string'; 18 | import { timestampsImpl } from './timestamps'; 19 | 20 | export const populateFields = (fields: Fields): FieldTypes => ({ 21 | belongsTo: (name, modifiers) => belongsToImpl(fields, name, modifiers), 22 | bigInt: (name, modifiers) => bigIntImpl(fields, name, modifiers), 23 | boolean: (name, modifiers) => booleanImpl(fields, name, modifiers), 24 | bytes: (name, modifiers) => bytesImpl(fields, name, modifiers), 25 | datetime: (name, modifiers) => datetimeImpl(fields, name, modifiers), 26 | decimal: (name, modifiers) => decimalImpl(fields, name, modifiers), 27 | enum: (name, modifiers) => enumImpl(fields, name, modifiers), 28 | float: (name, modifiers) => floatImpl(fields, name, modifiers), 29 | hasMany: (name, modifiers) => hasManyImpl(fields, name, modifiers), 30 | hasOne: (name, modifiers) => hasOneImpl(fields, name, modifiers), 31 | id: () => idImpl(fields), 32 | int: (name, modifiers) => intImpl(fields, name, modifiers), 33 | json: (name, modifiers) => jsonImpl(fields, name, modifiers), 34 | raw: fieldSchema => rawImpl(fields, fieldSchema), 35 | relation: (name, modifiers) => relationImpl(fields, name, modifiers), 36 | string: (name, modifiers) => stringImpl(fields, name, modifiers), 37 | timestamps: () => timestampsImpl(fields), 38 | }); 39 | -------------------------------------------------------------------------------- /src/field-types/int.ts: -------------------------------------------------------------------------------- 1 | import { Fields, NumberModifiers } from '../interfaces'; 2 | import { fieldModifierFns } from '../field-modifiers'; 3 | import { generateFieldSchema } from '../utils'; 4 | 5 | export const intImpl = ( 6 | fields: Fields, 7 | name: string, 8 | modifiers?: NumberModifiers 9 | ) => { 10 | const fieldType = 'Int'; 11 | 12 | Object.assign(fields, { 13 | [name]: { 14 | type: fieldType, 15 | ...modifiers, 16 | fieldSchema: generateFieldSchema(fieldModifierFns)( 17 | name, 18 | fieldType, 19 | modifiers 20 | ), 21 | }, 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/field-types/json.ts: -------------------------------------------------------------------------------- 1 | import { Fields, Modifiers } from '../interfaces'; 2 | import { fieldModifierFns } from '../field-modifiers'; 3 | import { generateFieldSchema } from '../utils'; 4 | 5 | export const jsonImpl = ( 6 | fields: Fields, 7 | name: string, 8 | modifiers?: Modifiers 9 | ) => { 10 | const fieldType = 'Json'; 11 | 12 | Object.assign(fields, { 13 | [name]: { 14 | type: fieldType, 15 | ...modifiers, 16 | fieldSchema: generateFieldSchema(fieldModifierFns)( 17 | name, 18 | fieldType, 19 | modifiers 20 | ), 21 | }, 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/field-types/raw.ts: -------------------------------------------------------------------------------- 1 | import { Fields } from '../interfaces'; 2 | 3 | export const rawImpl = (fields: Fields, fieldSchema: string) => { 4 | const fieldName = Symbol('raw'); 5 | 6 | Object.assign(fields, { 7 | [fieldName]: { 8 | fieldSchema: fieldSchema, 9 | }, 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /src/field-types/relation.ts: -------------------------------------------------------------------------------- 1 | import { Fields, RelationModifiers } from '../interfaces'; 2 | import { fieldModifierFns } from '../field-modifiers'; 3 | import { generateFieldSchema } from '../utils'; 4 | 5 | export const relationImpl = ( 6 | fields: Fields, 7 | name: string, 8 | modifiers: RelationModifiers 9 | ) => { 10 | const fieldType = modifiers.source; 11 | const relationModifiers = Object.assign(modifiers, { 12 | relation: {}, 13 | list: modifiers.list, 14 | optional: modifiers.optional, 15 | }); 16 | 17 | if (modifiers.name) { 18 | Object.assign(relationModifiers, { 19 | relation: { ...relationModifiers.relation, name: modifiers.name }, 20 | }); 21 | } 22 | 23 | if (modifiers.fields) { 24 | Object.assign(relationModifiers, { 25 | relation: { ...relationModifiers.relation, fields: modifiers.fields }, 26 | }); 27 | } 28 | 29 | if (modifiers.references) { 30 | Object.assign(relationModifiers, { 31 | relation: { 32 | ...relationModifiers.relation, 33 | references: modifiers.references, 34 | }, 35 | }); 36 | } 37 | 38 | Object.assign(fields, { 39 | [name]: { 40 | type: fieldType, 41 | ...modifiers, 42 | fieldSchema: generateFieldSchema(fieldModifierFns)( 43 | name, 44 | fieldType, 45 | Object.assign(relationModifiers, { 46 | source: null, 47 | name: null, 48 | fields: null, 49 | references: null, 50 | }) 51 | ), 52 | }, 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /src/field-types/string.ts: -------------------------------------------------------------------------------- 1 | import { Fields, StringModifiers } from '../interfaces'; 2 | import { fieldModifierFns } from '../field-modifiers'; 3 | import { generateFieldSchema } from '../utils'; 4 | 5 | export const stringImpl = ( 6 | fields: Fields, 7 | name: string, 8 | modifiers?: StringModifiers 9 | ) => { 10 | const fieldType = 'String'; 11 | 12 | Object.assign(fields, { 13 | [name]: { 14 | type: fieldType, 15 | ...modifiers, 16 | fieldSchema: generateFieldSchema(fieldModifierFns)( 17 | name, 18 | fieldType, 19 | modifiers 20 | ), 21 | }, 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/field-types/timestamps.ts: -------------------------------------------------------------------------------- 1 | import { Fields } from '../interfaces'; 2 | import { fieldModifierFns } from '../field-modifiers'; 3 | import { generateFieldSchema } from '../utils'; 4 | import { now } from '../attribute-fns'; 5 | 6 | export const timestampsImpl = (fields: Fields) => { 7 | const fieldType = 'DateTime'; 8 | 9 | Object.assign(fields, { 10 | createdAt: { 11 | type: fieldType, 12 | fieldSchema: generateFieldSchema(fieldModifierFns)( 13 | 'createdAt', 14 | fieldType, 15 | { 16 | default: now(), 17 | } 18 | ), 19 | }, 20 | 21 | updatedAt: { 22 | type: fieldType, 23 | fieldSchema: generateFieldSchema(fieldModifierFns)( 24 | 'updatedAt', 25 | fieldType, 26 | { 27 | updatedAt: true, 28 | } 29 | ), 30 | }, 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as Defaults from './attribute-fns'; 2 | 3 | export { Defaults }; 4 | export { enumType } from './enum'; 5 | export { model } from './model'; 6 | export { generateSchema } from './schema-gen'; 7 | export { env } from './env'; 8 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/krzkaczor/ts-essentials/blob/master/lib/types.ts#L346 2 | type Without = { [P in Exclude]?: never }; 3 | 4 | // https://github.com/krzkaczor/ts-essentials/blob/master/lib/types.ts#L349 5 | type XOR = T | U extends object 6 | ? (Without & U) | (Without & T) 7 | : T | U; 8 | 9 | export enum Defaults { 10 | autoincrement = 'autoincrement', 11 | dbgenerated = 'dbgenerated', 12 | cuid = 'cuid', 13 | uuid = 'uuid', 14 | now = 'now', 15 | } 16 | 17 | type OptionalModifier = { 18 | /** 19 | * When set to true, the primary modifier should be false or not defined 20 | */ 21 | optional?: true; 22 | /** 23 | * When set to true, the primary modifier should be false or not defined 24 | */ 25 | unique?: boolean; 26 | primary?: false; 27 | list?: false; 28 | }; 29 | 30 | type PrimaryModifier = { 31 | /** 32 | * When set to true, the optional and unique modifier should be false or not defined 33 | */ 34 | primary?: true; 35 | unique?: false; 36 | optional?: false; 37 | list?: false; 38 | }; 39 | 40 | type ListModifier = { 41 | /** 42 | * When set to true, the primary and optional modifier should be false or not defined 43 | */ 44 | list?: true; 45 | primary?: false; 46 | optional?: false; 47 | unique?: boolean; 48 | }; 49 | 50 | type PrimaryXOROptional = XOR; 51 | type PrimaryXORList = XOR; 52 | type OptionalXORList = XOR; 53 | type ExclusiveModifiers = OptionalXORList | PrimaryXORList | PrimaryXOROptional; 54 | 55 | export type Modifiers = ExclusiveModifiers & { 56 | default?: boolean | number | string; 57 | updatedAt?: boolean; 58 | }; 59 | 60 | export type NumberModifiers = Modifiers & { 61 | default?: number | Defaults.autoincrement | Defaults.dbgenerated; 62 | }; 63 | 64 | export type StringModifiers = Modifiers & { 65 | default?: string | Defaults.cuid | Defaults.dbgenerated | Defaults.uuid; 66 | }; 67 | 68 | export type DateTimeModifiers = Modifiers & { 69 | /** 70 | * Date strings should be formatted using the ISO-8601 standard. 71 | * They must include the time including the time offsets from UTC 72 | */ 73 | default?: string | Defaults.now; 74 | }; 75 | 76 | export type RelationModifiers = XOR< 77 | Omit, 78 | Omit 79 | > & { 80 | /** 81 | * String literal matching the name of the model 82 | */ 83 | source: string; 84 | fields?: string[]; 85 | references?: string[]; 86 | name?: string; 87 | }; 88 | 89 | export type EnumModifiers = Modifiers & { 90 | /** 91 | * String literal matching a value in the enum 92 | */ 93 | default?: string; 94 | /** 95 | * When specifying a string literal, ensure it matches the name of the enum 96 | */ 97 | source: Enum | string; 98 | }; 99 | 100 | export type ModifierKey = keyof Modifiers | 'relation'; 101 | 102 | type FieldTypeWithOptionalModifiers = ( 103 | name: string, 104 | modifiers?: M 105 | ) => void; 106 | type FieldTypeWithRequiredModifiers = ( 107 | name: string, 108 | modifiers: M 109 | ) => void; 110 | type RawType = (rawString: string) => void; 111 | type HelperType = () => void; 112 | 113 | export type FieldTypes = { 114 | belongsTo: FieldTypeWithRequiredModifiers>; 115 | bigInt: FieldTypeWithOptionalModifiers; 116 | boolean: FieldTypeWithOptionalModifiers; 117 | bytes: FieldTypeWithOptionalModifiers; 118 | datetime: FieldTypeWithOptionalModifiers; 119 | decimal: FieldTypeWithOptionalModifiers; 120 | enum: FieldTypeWithRequiredModifiers; 121 | float: FieldTypeWithOptionalModifiers; 122 | hasMany: FieldTypeWithRequiredModifiers>; 123 | hasOne: FieldTypeWithRequiredModifiers>; 124 | id: HelperType; 125 | int: FieldTypeWithOptionalModifiers; 126 | json: FieldTypeWithOptionalModifiers; 127 | /** The raw type allows directly specifying a schema field in the PSL format */ 128 | raw: RawType; 129 | relation: FieldTypeWithRequiredModifiers; 130 | string: FieldTypeWithOptionalModifiers; 131 | timestamps: HelperType; 132 | }; 133 | 134 | export type Fields = { 135 | [key: string]: ( 136 | | Modifiers 137 | | NumberModifiers 138 | | StringModifiers 139 | | DateTimeModifiers 140 | | RelationModifiers 141 | | EnumModifiers 142 | ) & { 143 | type: string; 144 | fieldSchema: string; 145 | }; 146 | }; 147 | 148 | export type ModelFn = ( 149 | modelName: string, 150 | fieldDefinition: ModelDefinition 151 | ) => Model; 152 | 153 | export type Model = { 154 | name: string; 155 | fields: Fields; 156 | toSchema: string; 157 | }; 158 | 159 | export type ModelDefinition = (fieldTypes: FieldTypes) => void; 160 | 161 | export type Enum = { 162 | name: string; 163 | toSchema: string; 164 | [key: string]: string; 165 | }; 166 | 167 | type BinaryTargets = 168 | | 'native' 169 | | 'darwin' 170 | | 'windows' 171 | | 'linux-musl' 172 | | 'debian-openssl-1.0.x' 173 | | 'debian-openssl-1.1.x' 174 | | 'rhel-openssl-1.0.x' 175 | | 'rhel-openssl-1.1.x' 176 | | 'linux-arm-openssl-1.0.x' 177 | | 'linux-arm-openssl-1.1.x'; 178 | 179 | export type SchemaConfig = { 180 | /** 181 | * Definitions for the datasource block in the prisma schema 182 | */ 183 | datasource: { 184 | /** 185 | * Define the data source connector to use 186 | */ 187 | provider: 'postgresql' | 'mysql' | 'sqlite' | 'sqlserver'; 188 | url: string; 189 | shadowDatabaseUrl?: string; 190 | }; 191 | /** 192 | * Definitions for the prisma client generator block in the prisma schema 193 | */ 194 | generator?: { 195 | /** 196 | * Describes which generator to use. 197 | * This can point to a file that implements a generator 198 | * or specify a built-in generator directly. 199 | * It is set to "prisma-client-js" by default. 200 | */ 201 | provider?: string; 202 | /** 203 | * File path indicating the location of the generated client. 204 | * It is set to "node_modules/@prisma/client" by default 205 | */ 206 | output?: string; 207 | /** 208 | * Specify the OS on which the prisma client will run. 209 | * It is set to ["native"] by default 210 | */ 211 | binaryTargets?: BinaryTargets[]; 212 | /** 213 | * Use intellisense to see list of currently available preview features 214 | */ 215 | previewFeatures?: string[]; 216 | }; 217 | /** 218 | * An object specifying all the models that should be added to the prisma schema. 219 | */ 220 | models: Record; 221 | /** 222 | * An object specifying all the enums that should be added to the prisma schema. 223 | */ 224 | enums?: Record; 225 | /** 226 | * File path indicating where the generated prisma schema should be saved. 227 | * If the prisma schema path is specified in package.json, it'll use the specified path by default. 228 | * Otherwise, It is saved to /prisma/prisma.schema by default. 229 | */ 230 | schemaOutput?: string; 231 | }; 232 | -------------------------------------------------------------------------------- /src/model/index.ts: -------------------------------------------------------------------------------- 1 | import { ModelDefinition, ModelFn, Fields } from '../interfaces'; 2 | import { populateFields } from '../field-types'; 3 | 4 | export const model: ModelFn = (name: string, definition: ModelDefinition) => { 5 | const fields: Fields = {}; 6 | 7 | definition(populateFields(fields)); 8 | let schema = ''; 9 | 10 | Object.values(fields).forEach((value, idx, arr) => { 11 | if (idx === arr.length - 1) schema += `${value.fieldSchema}`; 12 | else schema += `${value.fieldSchema}\n`; 13 | }); 14 | 15 | const toSchema = `model ${name} { 16 | ${schema} 17 | }`; 18 | 19 | // TODO: deep freeze the object to prevent external modification 20 | return { name, fields, toSchema }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/schema-gen/index.ts: -------------------------------------------------------------------------------- 1 | import { formatSchema, getDMMF as validateSchema } from '@prisma/sdk'; 2 | import { createReadStream, createWriteStream } from 'fs'; 3 | import { join } from 'path'; 4 | 5 | import { SchemaConfig } from '../interfaces'; 6 | import { enquoteString } from '../utils'; 7 | 8 | export const generateSchema = async ({ 9 | datasource: { provider: datasourceProvider, url, shadowDatabaseUrl = '' }, 10 | generator: { 11 | provider: generatorProvider = 'prisma-client-js', 12 | output = '', 13 | binaryTargets = [], 14 | previewFeatures = [], 15 | } = {}, 16 | models, 17 | enums = {}, 18 | schemaOutput, 19 | }: SchemaConfig) => { 20 | const datasource = `datasource db { 21 | provider = "${datasourceProvider}" 22 | url = ${enquoteString(url)}${shadowDatabaseUrl && 23 | `\nshadowDatabaseUrl = "${shadowDatabaseUrl}"`} 24 | }`; 25 | const generator = `generator client { 26 | provider = "${generatorProvider}"${output && `\noutput = "${output}"`}${ 27 | binaryTargets.length 28 | ? `\nbinaryTargets = ${JSON.stringify(binaryTargets)}` 29 | : '' 30 | }${ 31 | previewFeatures.length 32 | ? `\previewFeatures = ${JSON.stringify(previewFeatures)}` 33 | : '' 34 | } 35 | }`; 36 | let schema = `${datasource}\n${generator}\n`; 37 | let schemaPath = schemaOutput; 38 | 39 | schema = buildSchemaFromRecord(models, schema); 40 | schema = buildSchemaFromRecord(enums, schema); 41 | 42 | await validateSchema({ datamodel: schema }).catch(err => console.error(err)); 43 | schema = await formatSchema({ schema }); 44 | schema += '\n'; 45 | 46 | if (!schemaPath) { 47 | schemaPath = await getDefaultSchemaPath(); 48 | } 49 | 50 | writeToSchemaPath(schemaPath, schema); 51 | }; 52 | 53 | const buildSchemaFromRecord = ( 54 | record: Record, 55 | schema: string 56 | ) => { 57 | for (const [_, value] of Object.entries(record)) { 58 | schema += `${value.toSchema}\n`; 59 | } 60 | 61 | return schema; 62 | }; 63 | 64 | const getDefaultSchemaPath = async (): Promise => { 65 | const packageJSONPath = join(process.cwd(), 'package.json'); 66 | const readPackageJson = new Promise((resolve, reject) => { 67 | const stream = createReadStream(packageJSONPath, { encoding: 'utf-8' }); 68 | 69 | stream.on('readable', () => { 70 | const content = stream.read(); 71 | 72 | if (content) resolve(JSON.parse(content)); 73 | }); 74 | 75 | stream.on('error', err => { 76 | reject(err); 77 | }); 78 | }); 79 | const packageJsonContent: any = await readPackageJson; 80 | let schemaPath: string = 81 | packageJsonContent?.prisma?.schema || './prisma/schema.prisma'; 82 | 83 | return join(process.cwd(), schemaPath); 84 | }; 85 | 86 | const writeToSchemaPath = (schemaPath: string, schema: string) => { 87 | const writable = createWriteStream(schemaPath, { encoding: 'utf-8' }); 88 | writable.write(schema); 89 | 90 | writable.end(); 91 | }; 92 | -------------------------------------------------------------------------------- /src/utils/enquoteString.ts: -------------------------------------------------------------------------------- 1 | const excludeList = [ 2 | 'autoincrement()', 3 | 'cuid()', 4 | 'dbgenerated()', 5 | 'now()', 6 | 'uuid()', 7 | 'env()', 8 | ]; 9 | 10 | const isExcluded = (value: string) => 11 | excludeList.includes(value.replace(/\(.*\)/, '()')); 12 | 13 | export const enquoteString = (value: any) => { 14 | if (typeof value !== 'string' || isExcluded(value)) return value; 15 | 16 | return `"${value}"`; 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/generateFieldSchema.ts: -------------------------------------------------------------------------------- 1 | import { Modifiers, ModifierKey } from '../interfaces'; 2 | 3 | type FieldModifierFns = { 4 | [key in ModifierKey]: (fieldSchema: string, fieldValue: any) => string; 5 | }; 6 | 7 | export const generateFieldSchema = (fieldModifierFns: FieldModifierFns) => ( 8 | name: string, 9 | type: string, 10 | modifiers: Modifiers = {} 11 | ) => { 12 | let res = `${name} ${type}`; 13 | 14 | for (let [key, value] of Object.entries(modifiers)) { 15 | if (value == null) continue; 16 | 17 | let modifier = key as ModifierKey; 18 | 19 | res = fieldModifierFns[modifier](res, value); 20 | } 21 | 22 | return res; 23 | }; 24 | -------------------------------------------------------------------------------- /src/utils/generateModifier.ts: -------------------------------------------------------------------------------- 1 | import { ModifierKey, RelationModifiers } from '../interfaces'; 2 | 3 | type SchemaRecord = { 4 | fieldName: string; 5 | fieldType: string; 6 | existingModifiers: string[]; 7 | key?: ModifierKey; 8 | value?: unknown; 9 | }; 10 | 11 | type ModifierMap = { 12 | [key in ModifierKey]?: (value?: unknown) => string; 13 | }; 14 | 15 | const modifierKeyToSchemaAttribute: ModifierMap = { 16 | primary: () => ' @id', 17 | unique: () => ' @unique', 18 | default: value => ` @default(${value})`, 19 | updatedAt: () => ` @updatedAt`, 20 | relation: value => { 21 | const relationValue = (value as unknown) as Omit< 22 | RelationModifiers, 23 | 'source' 24 | >; 25 | const name = relationValue.name ? `name: "${relationValue.name}"` : ''; 26 | const fields = relationValue.fields 27 | ? `fields: [${relationValue.fields}]` 28 | : ''; 29 | const references = relationValue.references 30 | ? `references: [${relationValue.references}]` 31 | : ''; 32 | const relationArgs = [name, fields, references].filter(Boolean).join(', '); 33 | 34 | if (!relationArgs) return ''; 35 | 36 | return ` @relation(${relationArgs})`; 37 | }, 38 | }; 39 | 40 | const typeModifiers = { 41 | optional: (type: string) => `${type}?`, 42 | list: (type: string) => `${type}[]`, 43 | base: (type: string) => type, 44 | }; 45 | 46 | const modifier = ({ 47 | fieldName, 48 | fieldType: type, 49 | existingModifiers: additionalModifiers, 50 | key, 51 | value, 52 | }: SchemaRecord) => { 53 | const typeModifierKey = key as keyof typeof typeModifiers; 54 | const typeModifier = typeModifiers[typeModifierKey] 55 | ? typeModifiers[typeModifierKey] 56 | : typeModifiers.base; 57 | const fieldType = typeModifier(type); 58 | const existingModifiers = additionalModifiers.length 59 | ? ` ${additionalModifiers.join(' ')}` 60 | : ''; 61 | const modifierMapKey = key as keyof ModifierMap; 62 | const modifierMapFn = modifierKeyToSchemaAttribute[modifierMapKey]; 63 | const schemaAttribute = `${modifierMapFn?.(value) ?? ''}`; 64 | 65 | return `${fieldName} ${fieldType}${existingModifiers}${schemaAttribute}`; 66 | }; 67 | 68 | export const generateModifier = ( 69 | fieldSchema: string, 70 | key: ModifierKey, 71 | value?: unknown 72 | ) => { 73 | const [fieldName, fieldType, ...existingModifiers] = fieldSchema.split(' '); 74 | 75 | return modifier({ fieldName, fieldType, existingModifiers, key, value }); 76 | }; 77 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { generateFieldSchema } from './generateFieldSchema'; 2 | export { generateModifier } from './generateModifier'; 3 | export { enquoteString } from './enquoteString'; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true 34 | } 35 | } 36 | --------------------------------------------------------------------------------