Skip to content

Commit

Permalink
feat: add schema id option
Browse files Browse the repository at this point in the history
Added option to add a schema id to the generated json schema.

Fixes #73
  • Loading branch information
valentinpalkovic committed Sep 26, 2021
1 parent 2bfb3db commit 1d9ac04
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 21 deletions.
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ A generator, which takes a Prisma 2 `schema.prisma` and generates a JSON Schema
**1. Install**

npm:

```shell
npm install prisma-json-schema-generator --save-dev
```

yarn:

```shell
yarn add -D prisma-json-schema-generator
```
Expand All @@ -32,6 +34,7 @@ generator jsonSchema {
```

With a custom output path (default=./json-schema)

```prisma
generator jsonSchema {
provider = "prisma-json-schema-generator"
Expand All @@ -45,6 +48,7 @@ Additional options
generator jsonSchema {
provider = "prisma-json-schema-generator"
keepRelationScalarFields = "true"
schemaId = "some-schema-id"
}
```

Expand All @@ -53,27 +57,34 @@ The generator currently supports a single option
| Key | Default Value | Description |
| ------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| keepRelationScalarFields | "false" | By default, the JSON Schema that's generated will output only objects for related model records. If set to "true", this will cause the generator to also output foreign key fields for related records |
| schemaId | undefined | Add an id to the generated schema. All references will include the schema id |

**3. Run generation**

prisma:

```shell
prisma generate
```

nexus with prisma plugin:
nexus with prisma plugin:

```shell
nexus build
```

## Supported Node Versions

| Node Version | Support |
| -------------------: | :----------------- |
| (Maintenance LTS) 12 | :heavy_check_mark: |
| (Active LTS) 14 | :heavy_check_mark: |
| (Current) 16 | :heavy_check_mark: |

## Examples

This generator converts a prisma schema like this:

```prisma
datasource db {
provider = "postgresql"
Expand Down Expand Up @@ -172,6 +183,7 @@ into:
```

So the following input will correctly be validated:

```javascript
{
post: {
Expand Down Expand Up @@ -209,8 +221,8 @@ So the following input will correctly be validated:
}
```


## License: MIT

Copyright (c) 2020 Valentin Palkovič

Permission is hereby granted, free of charge, to any person obtaining a copy
Expand Down
2 changes: 1 addition & 1 deletion src/generator/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function getJSONSchemaModel(
) {
return (model: DMMF.Model): DefinitionMap => {
const definitionPropsMap = model.fields.map(
getJSONSchemaProperty(modelMetaData),
getJSONSchemaProperty(modelMetaData, transformOptions),
)

const propertiesMap = definitionPropsMap.map(
Expand Down
31 changes: 23 additions & 8 deletions src/generator/properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import {
isScalarType,
PrismaPrimitive,
} from './helpers'
import { ModelMetaData, PropertyMap, PropertyMetaData } from './types'
import {
ModelMetaData,
PropertyMap,
PropertyMetaData,
TransformOptions,
} from './types'

import type {
JSONSchema7,
Expand Down Expand Up @@ -58,18 +63,25 @@ function getFormatByDMMFType(fieldType: string): string | undefined {
}
}

function getJSONSchemaForPropertyReference(field: DMMF.Field): JSONSchema7 {
function getJSONSchemaForPropertyReference(
field: DMMF.Field,
{ schemaId }: TransformOptions,
): JSONSchema7 {
const notNullable = field.isRequired || field.isList
const ref = { $ref: `${DEFINITIONS_ROOT}${field.type}` }
const typeRef = `${DEFINITIONS_ROOT}${field.type}`
const ref = { $ref: schemaId ? `${schemaId}${typeRef}` : typeRef }
return notNullable ? ref : { anyOf: [ref, { type: 'null' }] }
}

function getItemsByDMMFType(field: DMMF.Field): JSONSchema7['items'] {
function getItemsByDMMFType(
field: DMMF.Field,
transformOptions: TransformOptions,
): JSONSchema7['items'] {
return (isScalarType(field) && !field.isList) || isEnumType(field)
? undefined
: isScalarType(field) && field.isList
? { type: getJSONSchemaScalar(field.type) }
: getJSONSchemaForPropertyReference(field)
: getJSONSchemaForPropertyReference(field, transformOptions)
}

function isSingleReference(field: DMMF.Field) {
Expand All @@ -87,11 +99,14 @@ function getEnumListByDMMFType(modelMetaData: ModelMetaData) {
}
}

export function getJSONSchemaProperty(modelMetaData: ModelMetaData) {
export function getJSONSchemaProperty(
modelMetaData: ModelMetaData,
transformOptions: TransformOptions,
) {
return (field: DMMF.Field): PropertyMap => {
const type = getJSONSchemaType(field)
const format = getFormatByDMMFType(field.type)
const items = getItemsByDMMFType(field)
const items = getItemsByDMMFType(field, transformOptions)
const enumList = getEnumListByDMMFType(modelMetaData)(field)

const definition: JSONSchema7Definition = {
Expand All @@ -108,7 +123,7 @@ export function getJSONSchemaProperty(modelMetaData: ModelMetaData) {
return [
field.name,
isSingleReference(field)
? getJSONSchemaForPropertyReference(field)
? getJSONSchemaForPropertyReference(field, transformOptions)
: definition,
propertyMetaData,
]
Expand Down
27 changes: 17 additions & 10 deletions src/generator/transformDMMF.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ import { toCamelCase } from './helpers'
import { getInitialJSON } from './jsonSchema'
import { getJSONSchemaModel } from './model'

function getPropertyDefinition(
model: DMMF.Model,
): [name: string, reference: JSONSchema7Definition] {
return [
toCamelCase(model.name),
{
$ref: `${DEFINITIONS_ROOT}${model.name}`,
},
]
function getPropertyDefinition({ schemaId }: TransformOptions) {
return (
model: DMMF.Model,
): [name: string, reference: JSONSchema7Definition] => {
const ref = `${DEFINITIONS_ROOT}${model.name}`
return [
toCamelCase(model.name),
{
$ref: schemaId ? `${schemaId}${ref}` : ref,
},
]
}
}

export function transformDMMF(
Expand All @@ -24,15 +27,19 @@ export function transformDMMF(
): JSONSchema7 {
const { models, enums } = dmmf.datamodel
const initialJSON = getInitialJSON()
const { schemaId } = transformOptions

const modelDefinitionsMap = models.map(
getJSONSchemaModel({ enums }, transformOptions),
)
const modelPropertyDefinitionsMap = models.map(getPropertyDefinition)
const modelPropertyDefinitionsMap = models.map(
getPropertyDefinition(transformOptions),
)
const definitions = Object.fromEntries(modelDefinitionsMap)
const properties = Object.fromEntries(modelPropertyDefinitionsMap)

return {
...(schemaId ? { $id: schemaId } : null),
...initialJSON,
definitions,
properties,
Expand Down
1 change: 1 addition & 0 deletions src/generator/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export type PropertyMap = [...DefinitionMap, PropertyMetaData]

export interface TransformOptions {
keepRelationScalarFields?: 'true' | 'false'
schemaId?: string
}
74 changes: 74 additions & 0 deletions src/tests/generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,80 @@ describe('JSON Schema Generator', () => {
})
})

it('adds schema id', async () => {
const dmmf = await getDMMF({ datamodel })
expect(
transformDMMF(dmmf, {
keepRelationScalarFields: 'true',
schemaId: 'schemaId',
}),
).toEqual({
$id: 'schemaId',
$schema: 'http://json-schema.org/draft-07/schema#',
definitions: {
Post: {
properties: {
id: { type: 'integer' },
user: {
anyOf: [
{ $ref: 'schemaId#/definitions/User' },
{ type: 'null' },
],
},
userId: {
type: ['integer', 'null'],
},
},
type: 'object',
},
User: {
properties: {
biography: { type: 'object' },
createdAt: { format: 'date-time', type: 'string' },
email: { type: 'string' },
number: { type: 'integer' },
bytes: { type: 'string' },
favouriteDecimal: { type: 'number' },
id: { type: 'integer' },
is18: { type: ['boolean', 'null'] },
keywords: {
items: { type: 'string' },
type: 'array',
},
name: { type: ['string', 'null'] },
posts: {
items: { $ref: 'schemaId#/definitions/Post' },
type: 'array',
},
predecessor: {
anyOf: [
{ $ref: 'schemaId#/definitions/User' },
{ type: 'null' },
],
},
role: { enum: ['USER', 'ADMIN'], type: 'string' },
successor: {
anyOf: [
{ $ref: 'schemaId#/definitions/User' },
{ type: 'null' },
],
},
successorId: {
type: ['integer', 'null'],
},
weight: { type: ['number', 'null'] },
},
type: 'object',
},
},
properties: {
post: { $ref: 'schemaId#/definitions/Post' },
user: { $ref: 'schemaId#/definitions/User' },
},
type: 'object',
})
})

// eslint-disable-next-line jest/expect-expect
it('generated schema validates against input', async () => {
const dmmf = await getDMMF({ datamodel })
Expand Down

0 comments on commit 1d9ac04

Please sign in to comment.