Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/famous-coins-open.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"prisma-openapi": minor
---

Allow to exclude fields from prisma models
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
22.15.0
24.13.1
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ A Prisma generator that automatically creates OpenAPI specifications from your P
- [Custom Configuration](#custom-configuration)
- [JSDoc Integration](#jsdoc-integration)
- [Prisma Comments as Descriptions](#prisma-comments-as-descriptions)
- [Field Exclusion](#field-exclusion)
- [Configuration](#configuration)
- [License](#license)

Expand Down Expand Up @@ -281,6 +282,38 @@ User:
description: Optional display name
```

### Field Exclusion

You can exclude specific fields from the generated OpenAPI schema using two approaches:

**1. Using `@openapi.ignore` in field comments:**

Add `@openapi.ignore` to a field's triple-slash comment to exclude it from the generated schema:

```prisma
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
/// @openapi.ignore
password String
}
```

**2. Using `excludeFields` in generator config:**

Specify fields to exclude using `ModelName.fieldName` format:

```prisma
generator openapi {
provider = "prisma-openapi"
output = "./openapi"
excludeFields = "User.password, User.secretKey"
}
```

Both approaches can be used together. A field is excluded if it matches **either** condition. Excluded fields are removed from both `properties` and `required` in the generated schema.

## Configuration

| Option | Description | Default |
Expand All @@ -290,6 +323,7 @@ User:
| `description` | API description in OpenAPI spec | Empty string |
| `includeModels` | Comma-separated list of models to include | All models |
| `excludeModels` | Comma-separated list of models to exclude | None |
| `excludeFields` | Comma-separated list of fields to exclude (`ModelName.fieldName`) | None |
| `generateYaml` | Generate YAML format | `true` |
| `generateJson` | Generate JSON format | `false` |
| `generateJsDoc` | Include JSDoc comments in the schema | `false` |
Expand Down
2 changes: 1 addition & 1 deletion esbuild.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { build } from 'esbuild';
import {build} from 'esbuild';

await build({
entryPoints: ['src/lib/index.ts'],
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"author": "Nitzan Ohana <16689354+nitzano@users.noreply.github.com>",
"repository": "git@github.com:nitzano/prisma-openapi.git",
"version": "1.5.6",
"description": "",
"description": "Generate OpenAPI documentation from prisma schema",
"main": "dist/index.js",
"bin": {
"prisma-openapi": "./dist/index.js"
Expand Down
17 changes: 14 additions & 3 deletions src/on-generate/generate-js-doc-content.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {GeneratorOptions} from '@prisma/generator-helper';
import {isFieldIgnored} from './is-field-ignored.js';

/**
* Generate JSDoc OpenAPI comments for Prisma models
Expand All @@ -7,6 +8,7 @@ export function generateJsDocumentContent(
models: GeneratorOptions['dmmf']['datamodel']['models'],
filteredModels: GeneratorOptions['dmmf']['datamodel']['models'],
enums: GeneratorOptions['dmmf']['datamodel']['enums'],
excludeFields?: string[],
): string {
// Create JSDoc OpenAPI content with a single block
let jsDocumentContent = `/**
Expand All @@ -20,9 +22,9 @@ export function generateJsDocumentContent(
* ${model.name}:
* type: object
* properties:
${generateModelProperties(model).trimEnd()}
${generateModelProperties(model, excludeFields).trimEnd()}
* required:
${generateRequiredProperties(model)}`;
${generateRequiredProperties(model, excludeFields)}`;
}

// Add enum schemas
Expand All @@ -46,10 +48,15 @@ ${generateEnumValues(enumType)}`;
*/
function generateModelProperties(
model: GeneratorOptions['dmmf']['datamodel']['models'][0],
excludeFields?: string[],
): string {
let properties = '';

for (const field of model.fields) {
if (isFieldIgnored(model.name, field, excludeFields)) {
continue;
}

let propertyType = '';

// Handle different field types
Expand Down Expand Up @@ -165,9 +172,13 @@ function generateModelProperties(
*/
function generateRequiredProperties(
model: GeneratorOptions['dmmf']['datamodel']['models'][0],
excludeFields?: string[],
): string {
const requiredFields = model.fields
.filter((field) => field.isRequired)
.filter(
(field) =>
field.isRequired && !isFieldIgnored(model.name, field, excludeFields),
)
.map((field) => field.name);

return requiredFields.map((field) => ` * - ${field}`).join('\n');
Expand Down
21 changes: 18 additions & 3 deletions src/on-generate/generate-open-api-spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type {GeneratorOptions} from '@prisma/generator-helper';
import {OpenApiBuilder, type SchemaObject} from 'openapi3-ts/oas31';
import {generatePropertiesFromModel} from './generate-properties-from-model.js';
import {type PrismaOpenApiOptions} from './generator-options.js';
import {
type PrismaOpenApiOptions,
parseCommaSeparatedList,
} from './generator-options.js';
import {isFieldIgnored} from './is-field-ignored.js';

/**
* Generate an OpenAPI specification object from Prisma models
Expand All @@ -18,14 +22,25 @@ export function generateOpenApiSpec(
version: '1.0.0',
});

const excludeFieldsList = parseCommaSeparatedList(options.excludeFields);

// Create schemas for all filtered models
for (const model of filteredModels) {
const modelSchema: SchemaObject = {
type: 'object',
description: model.documentation,
properties: generatePropertiesFromModel(model, filteredModels, enums),
properties: generatePropertiesFromModel(
model,
filteredModels,
enums,
excludeFieldsList,
),
required: model.fields
.filter((field) => field.isRequired)
.filter(
(field) =>
field.isRequired &&
!isFieldIgnored(model.name, field, excludeFieldsList),
)
.map((field) => field.name),
};
builder.addSchema(model.name, modelSchema);
Expand Down
106 changes: 51 additions & 55 deletions src/on-generate/generate-properties-from-model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,50 @@
import type {GeneratorOptions} from '@prisma/generator-helper';
import {type ReferenceObject, type SchemaObject} from 'openapi3-ts/oas31';
import {isFieldIgnored} from './is-field-ignored.js';

/**
* Map a Prisma scalar type to an OpenAPI schema object
*/
function mapScalarType(type: string): SchemaObject {
switch (type) {
case 'String': {
return {type: 'string'};
}

case 'Int': {
return {type: 'integer', format: 'int32'};
}

case 'BigInt': {
return {type: 'integer', format: 'int64'};
}

case 'Float':
case 'Decimal': {
return {type: 'number', format: 'double'};
}

case 'Boolean': {
return {type: 'boolean'};
}

case 'DateTime': {
return {type: 'string', format: 'date-time'};
}

case 'Json': {
return {type: 'object'};
}

case 'unsupported': {
return {type: 'string', description: 'Unsupported type'};
}

default: {
return {type: 'string', description: 'Unknown type'};
}
}
}

/**
* Generate OpenAPI properties from a Prisma model
Expand All @@ -8,70 +53,21 @@ export function generatePropertiesFromModel(
model: GeneratorOptions['dmmf']['datamodel']['models'][0],
allModels: GeneratorOptions['dmmf']['datamodel']['models'],
enums: GeneratorOptions['dmmf']['datamodel']['enums'],
excludeFields?: string[],
): Record<string, SchemaObject | ReferenceObject> {
const properties: Record<string, SchemaObject | ReferenceObject> = {};

for (const field of model.fields) {
if (isFieldIgnored(model.name, field, excludeFields)) {
continue;
}

let property: SchemaObject | ReferenceObject;

// Handle different field types
switch (field.kind) {
case 'scalar': {
// Map Prisma scalar types to OpenAPI types
const scalarProperty: SchemaObject = {};
switch (field.type) {
case 'String': {
scalarProperty.type = 'string';
break;
}

case 'Int': {
scalarProperty.type = 'integer';
scalarProperty.format = 'int32';
break;
}

case 'BigInt': {
scalarProperty.type = 'integer';
scalarProperty.format = 'int64';
break;
}

case 'Float':
case 'Decimal': {
scalarProperty.type = 'number';
scalarProperty.format = 'double';
break;
}

case 'Boolean': {
scalarProperty.type = 'boolean';
break;
}

case 'DateTime': {
scalarProperty.type = 'string';
scalarProperty.format = 'date-time';
break;
}

case 'Json': {
scalarProperty.type = 'object';
break;
}

case 'unsupported': {
scalarProperty.type = 'string';
scalarProperty.description = 'Unsupported type';
break;
}

default: {
scalarProperty.type = 'string';
scalarProperty.description = 'Unknown type';
break;
}
}
const scalarProperty = mapScalarType(field.type);

if (field.isList) {
property = {
Expand Down
6 changes: 6 additions & 0 deletions src/on-generate/generator-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ export type PrismaOpenApiOptions = {
*/
excludeModels?: string;

/**
* Comma-separated list of fields to exclude (format: "ModelName.fieldName")
* @default undefined (none)
*/
excludeFields?: string;

/**
* Generate YAML format
* @default true
Expand Down
30 changes: 30 additions & 0 deletions src/on-generate/is-field-ignored.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const openapiIgnoreTag = '@openapi.ignore';

/**
* Check if a field should be ignored in OpenAPI generation.
* A field is ignored if:
* 1. Its documentation contains @openapi.ignore
* 2. It is listed in the excludeFields option (as "ModelName.fieldName")
*/
export function isFieldIgnored(
modelName: string,
field: {name: string; documentation?: string | undefined},
excludeFields?: string[],
): boolean {
if (field.documentation?.includes(openapiIgnoreTag)) {
return true;
}

if (excludeFields?.includes(`${modelName}.${field.name}`)) {
return true;
}

return false;
}

/**
* Strip the @openapi.ignore tag from documentation so it doesn't leak into descriptions
*/
export function cleanDocumentation(documentation: string): string {
return documentation.replace(openapiIgnoreTag, '').trim();
}
4 changes: 4 additions & 0 deletions src/on-generate/on-generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,14 @@ export async function onGenerate(options: GeneratorOptions) {
// Write JSDoc file if enabled
if (prismaOpenApiOptions.generateJsDoc) {
const jsDocumentPath = path.join(outputDirectory, 'openapi.js');
const excludeFieldsList = parseCommaSeparatedList(
prismaOpenApiOptions.excludeFields,
);
const jsDocumentContent = generateJsDocumentContent(
dmmf.datamodel.models,
filteredModels,
dmmf.datamodel.enums,
excludeFieldsList,
);
fs.writeFileSync(jsDocumentPath, jsDocumentContent);
logger.info(`OpenAPI JSDoc specification written to ${jsDocumentPath}`);
Expand Down
Loading