From 4ebaa1fa4aa8e762a11fb24700f5cb4e1bfbe688 Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 12 May 2023 10:48:56 -0700 Subject: [PATCH] feat: restful style openapi spec generation (#410) --- packages/plugins/openapi/package.json | 2 + .../plugins/openapi/src/generator-base.ts | 60 + packages/plugins/openapi/src/index.ts | 10 +- .../plugins/openapi/src/rest-generator.ts | 903 +++++ .../src/{generator.ts => rpc-generator.ts} | 65 +- .../openapi/tests/baseline/rest.baseline.yaml | 1727 +++++++++ .../openapi/tests/baseline/rpc.baseline.yaml | 3087 +++++++++++++++++ .../openapi/tests/openapi-restful.test.ts | 209 ++ .../{openapi.test.ts => openapi-rpc.test.ts} | 4 +- packages/schema/src/language-server/utils.ts | 22 +- .../validator/datamodel-validator.ts | 4 +- .../schema/src/plugins/model-meta/index.ts | 17 +- packages/sdk/src/utils.ts | 72 + pnpm-lock.yaml | 7 +- 14 files changed, 6089 insertions(+), 100 deletions(-) create mode 100644 packages/plugins/openapi/src/generator-base.ts create mode 100644 packages/plugins/openapi/src/rest-generator.ts rename packages/plugins/openapi/src/{generator.ts => rpc-generator.ts} (93%) create mode 100644 packages/plugins/openapi/tests/baseline/rest.baseline.yaml create mode 100644 packages/plugins/openapi/tests/baseline/rpc.baseline.yaml create mode 100644 packages/plugins/openapi/tests/openapi-restful.test.ts rename packages/plugins/openapi/tests/{openapi.test.ts => openapi-rpc.test.ts} (97%) diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 07afac7b6..fa9435a80 100644 --- a/packages/plugins/openapi/package.json +++ b/packages/plugins/openapi/package.json @@ -40,6 +40,7 @@ "@readme/openapi-parser": "^2.4.0", "@types/jest": "^29.5.0", "@types/lower-case-first": "^1.0.1", + "@types/pluralize": "^0.0.29", "@types/tmp": "^0.2.3", "@typescript-eslint/eslint-plugin": "^5.54.0", "@typescript-eslint/parser": "^5.54.0", @@ -47,6 +48,7 @@ "copyfiles": "^2.4.1", "eslint": "^8.35.0", "jest": "^29.5.0", + "pluralize": "^8.0.0", "rimraf": "^3.0.2", "tmp": "^0.2.1", "ts-jest": "^29.0.5", diff --git a/packages/plugins/openapi/src/generator-base.ts b/packages/plugins/openapi/src/generator-base.ts new file mode 100644 index 000000000..6d50a6a29 --- /dev/null +++ b/packages/plugins/openapi/src/generator-base.ts @@ -0,0 +1,60 @@ +import { DMMF } from '@prisma/generator-helper'; +import { PluginError, PluginOptions, getDataModels, hasAttribute } from '@zenstackhq/sdk'; +import { Model } from '@zenstackhq/sdk/ast'; +import type { OpenAPIV3_1 as OAPI } from 'openapi-types'; +import { SecuritySchemesSchema } from './schema'; +import { fromZodError } from 'zod-validation-error'; + +export abstract class OpenAPIGeneratorBase { + constructor(protected model: Model, protected options: PluginOptions, protected dmmf: DMMF.Document) {} + + abstract generate(): string[]; + + protected get includedModels() { + return getDataModels(this.model).filter((d) => !hasAttribute(d, '@@openapi.ignore')); + } + + protected wrapArray( + schema: OAPI.ReferenceObject | OAPI.SchemaObject, + isArray: boolean + ): OAPI.ReferenceObject | OAPI.SchemaObject { + if (isArray) { + return { type: 'array', items: schema }; + } else { + return schema; + } + } + + protected array(itemType: OAPI.SchemaObject | OAPI.ReferenceObject) { + return { type: 'array', items: itemType } as const; + } + + protected oneOf(...schemas: (OAPI.SchemaObject | OAPI.ReferenceObject)[]) { + return { oneOf: schemas }; + } + + protected allOf(...schemas: (OAPI.SchemaObject | OAPI.ReferenceObject)[]) { + return { allOf: schemas }; + } + + protected getOption(name: string): T | undefined; + protected getOption(name: string, defaultValue: D): T; + protected getOption(name: string, defaultValue?: T): T | undefined { + const value = this.options[name]; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + return value === undefined ? defaultValue : value; + } + + protected generateSecuritySchemes() { + const securitySchemes = this.getOption[]>('securitySchemes'); + if (securitySchemes) { + const parsed = SecuritySchemesSchema.safeParse(securitySchemes); + if (!parsed.success) { + throw new PluginError(`"securitySchemes" option is invalid: ${fromZodError(parsed.error)}`); + } + return parsed.data; + } + return undefined; + } +} diff --git a/packages/plugins/openapi/src/index.ts b/packages/plugins/openapi/src/index.ts index ec404c308..19758df67 100644 --- a/packages/plugins/openapi/src/index.ts +++ b/packages/plugins/openapi/src/index.ts @@ -1,10 +1,16 @@ import { DMMF } from '@prisma/generator-helper'; import { PluginOptions } from '@zenstackhq/sdk'; import { Model } from '@zenstackhq/sdk/ast'; -import { OpenAPIGenerator } from './generator'; +import { RESTfulOpenAPIGenerator } from './rest-generator'; +import { RPCOpenAPIGenerator } from './rpc-generator'; export const name = 'OpenAPI'; export default async function run(model: Model, options: PluginOptions, dmmf: DMMF.Document) { - return new OpenAPIGenerator(model, options, dmmf).generate(); + const flavor = options.flavor ? (options.flavor as string) : 'restful'; + if (flavor === 'restful') { + return new RESTfulOpenAPIGenerator(model, options, dmmf).generate(); + } else { + return new RPCOpenAPIGenerator(model, options, dmmf).generate(); + } } diff --git a/packages/plugins/openapi/src/rest-generator.ts b/packages/plugins/openapi/src/rest-generator.ts new file mode 100644 index 000000000..cc3a363d0 --- /dev/null +++ b/packages/plugins/openapi/src/rest-generator.ts @@ -0,0 +1,903 @@ +// Inspired by: https://github.com/omar-dulaimi/prisma-trpc-generator + +import { DMMF } from '@prisma/generator-helper'; +import { + AUXILIARY_FIELDS, + PluginError, + analyzePolicies, + getDataModels, + isForeignKeyField, + isIdField, + isRelationshipField, +} from '@zenstackhq/sdk'; +import { DataModel, DataModelField, DataModelFieldType, Enum, isDataModel, isEnum } from '@zenstackhq/sdk/ast'; +import * as fs from 'fs'; +import { lowerCaseFirst } from 'lower-case-first'; +import type { OpenAPIV3_1 as OAPI } from 'openapi-types'; +import * as path from 'path'; +import pluralize from 'pluralize'; +import invariant from 'tiny-invariant'; +import YAML from 'yaml'; +import { OpenAPIGeneratorBase } from './generator-base'; +import { getModelResourceMeta } from './meta'; + +type Policies = ReturnType; + +/** + * Generates RESTful style OpenAPI specification. + */ +export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { + private warnings: string[] = []; + + generate() { + const output = this.getOption('output', ''); + if (!output) { + throw new PluginError('"output" option is required'); + } + + const components = this.generateComponents(); + const paths = this.generatePaths(); + + // generate security schemes, and root-level security + components.securitySchemes = this.generateSecuritySchemes(); + let security: OAPI.Document['security'] | undefined = undefined; + if (components.securitySchemes && Object.keys(components.securitySchemes).length > 0) { + security = Object.keys(components.securitySchemes).map((scheme) => ({ [scheme]: [] })); + } + + const openapi: OAPI.Document = { + openapi: this.getOption('specVersion', '3.1.0'), + info: { + title: this.getOption('title', 'ZenStack Generated API'), + version: this.getOption('version', '1.0.0'), + description: this.getOption('description'), + summary: this.getOption('summary'), + }, + tags: this.includedModels.map((model) => { + const meta = getModelResourceMeta(model); + return { + name: lowerCaseFirst(model.name), + description: meta?.tagDescription ?? `${model.name} operations`, + }; + }), + paths, + components, + security, + }; + + const ext = path.extname(output); + if (ext && (ext.toLowerCase() === '.yaml' || ext.toLowerCase() === '.yml')) { + fs.writeFileSync(output, YAML.stringify(openapi)); + } else { + fs.writeFileSync(output, JSON.stringify(openapi, undefined, 2)); + } + + return this.warnings; + } + + private generatePaths(): OAPI.PathsObject { + let result: OAPI.PathsObject = {}; + + const includeModelNames = this.includedModels.map((d) => d.name); + + for (const model of this.dmmf.datamodel.models) { + if (includeModelNames.includes(model.name)) { + const zmodel = this.model.declarations.find( + (d) => isDataModel(d) && d.name === model.name + ) as DataModel; + if (zmodel) { + result = { + ...result, + ...this.generatePathsForModel(model, zmodel), + } as OAPI.PathsObject; + } else { + this.warnings.push(`Unable to load ZModel definition for: ${model.name}}`); + } + } + } + return result; + } + + private generatePathsForModel(model: DMMF.Model, zmodel: DataModel): OAPI.PathItemObject | undefined { + const result: Record = {}; + + // analyze access policies to determine default security + const policies = analyzePolicies(zmodel); + + let prefix = this.getOption('prefix', ''); + if (prefix.endsWith('/')) { + prefix = prefix.substring(0, prefix.length - 1); + } + + // GET /resource + // POST /resource + result[`${prefix}/${lowerCaseFirst(model.name)}`] = { + get: this.makeResourceList(zmodel, policies), + post: this.makeResourceCreate(zmodel, policies), + }; + + // GET /resource/{id} + // PATCH /resource/{id} + // DELETE /resource/{id} + result[`${prefix}/${lowerCaseFirst(model.name)}/{id}`] = { + get: this.makeResourceFetch(zmodel, policies), + patch: this.makeResourceUpdate(zmodel, policies), + delete: this.makeResourceDelete(zmodel, policies), + }; + + // paths for related resources and relationships + for (const field of zmodel.fields) { + const relationDecl = field.type.reference?.ref; + if (!isDataModel(relationDecl)) { + continue; + } + + // GET /resource/{id}/field + const relatedDataPath = `${prefix}/${lowerCaseFirst(model.name)}/{id}/${field.name}`; + let container = result[relatedDataPath]; + if (!container) { + container = result[relatedDataPath] = {}; + } + container.get = this.makeRelatedFetch(zmodel, field, relationDecl); + + const relationshipPath = `${prefix}/${lowerCaseFirst(model.name)}/{id}/relationships/${field.name}`; + container = result[relationshipPath]; + if (!container) { + container = result[relationshipPath] = {}; + } + // GET /resource/{id}/relationships/field + container.get = this.makeRelationshipFetch(zmodel, field, policies); + // PATCH /resource/{id}/relationships/field + container.patch = this.makeRelationshipUpdate(zmodel, field, policies); + if (field.type.array) { + // POST /resource/{id}/relationships/field + container.post = this.makeRelationshipCreate(zmodel, field, policies); + } + } + + return result; + } + + private makeResourceList(model: DataModel, policies: Policies) { + return { + operationId: `list-${model.name}`, + description: `List "${model.name}" resources`, + tags: [lowerCaseFirst(model.name)], + parameters: [ + this.parameter('include'), + this.parameter('sort'), + this.parameter('page-offset'), + this.parameter('page-limit'), + ...this.generateFilterParameters(model), + ], + responses: { + '200': this.success(`${model.name}ListResponse`), + '403': this.forbidden(), + }, + security: policies.read === true ? [] : undefined, + }; + } + + private makeResourceCreate(model: DataModel, policies: Policies) { + return { + operationId: `create-${model.name}`, + description: `Create a "${model.name}" resource`, + tags: [lowerCaseFirst(model.name)], + requestBody: { + content: { + 'application/vnd.api+json': { + schema: this.ref(`${model.name}CreateRequest`), + }, + }, + }, + responses: { + '201': this.success(`${model.name}Response`), + '403': this.forbidden(), + }, + security: policies.create === true ? [] : undefined, + }; + } + + private makeResourceFetch(model: DataModel, policies: Policies) { + return { + operationId: `fetch-${model.name}`, + description: `Fetch a "${model.name}" resource`, + tags: [lowerCaseFirst(model.name)], + parameters: [this.parameter('id'), this.parameter('include')], + responses: { + '200': this.success(`${model.name}Response`), + '403': this.forbidden(), + '404': this.notFound(), + }, + security: policies.read === true ? [] : undefined, + }; + } + + private makeRelatedFetch(model: DataModel, field: DataModelField, relationDecl: DataModel) { + const policies = analyzePolicies(relationDecl); + const parameters: OAPI.OperationObject['parameters'] = [this.parameter('id'), this.parameter('include')]; + if (field.type.array) { + parameters.push( + this.parameter('sort'), + this.parameter('page-offset'), + this.parameter('page-limit'), + ...this.generateFilterParameters(model) + ); + } + const result = { + operationId: `fetch-${model.name}-related-${field.name}`, + description: `Fetch the related "${field.name}" resource for "${model.name}"`, + tags: [lowerCaseFirst(model.name)], + parameters, + responses: { + '200': this.success( + field.type.array ? `${relationDecl.name}ListResponse` : `${relationDecl.name}Response` + ), + '403': this.forbidden(), + '404': this.notFound(), + }, + security: policies.read === true ? [] : undefined, + }; + return result; + } + + private makeResourceUpdate(model: DataModel, policies: Policies) { + return { + operationId: `update-${model.name}`, + description: `Update a "${model.name}" resource`, + tags: [lowerCaseFirst(model.name)], + parameters: [this.parameter('id')], + requestBody: { + content: { + 'application/vnd.api+json': { + schema: this.ref(`${model.name}UpdateRequest`), + }, + }, + }, + responses: { + '200': this.success(`${model.name}Response`), + '403': this.forbidden(), + '404': this.notFound(), + }, + security: policies.update === true ? [] : undefined, + }; + } + + private makeResourceDelete(model: DataModel, policies: Policies) { + return { + operationId: `delete-${model.name}`, + description: `Delete a "${model.name}" resource`, + tags: [lowerCaseFirst(model.name)], + parameters: [this.parameter('id')], + responses: { + '200': this.success(), + '403': this.forbidden(), + '404': this.notFound(), + }, + security: policies.delete === true ? [] : undefined, + }; + } + + private makeRelationshipFetch(model: DataModel, field: DataModelField, policies: Policies) { + const parameters: OAPI.OperationObject['parameters'] = [this.parameter('id')]; + if (field.type.array) { + parameters.push( + this.parameter('sort'), + this.parameter('page-offset'), + this.parameter('page-limit'), + ...this.generateFilterParameters(model) + ); + } + return { + operationId: `fetch-${model.name}-relationship-${field.name}`, + description: `Fetch the "${field.name}" relationships for a "${model.name}"`, + tags: [lowerCaseFirst(model.name)], + parameters, + responses: { + '200': field.type.array + ? this.success('_toManyRelationshipResponse') + : this.success('_toOneRelationshipResponse'), + '403': this.forbidden(), + '404': this.notFound(), + }, + security: policies.read === true ? [] : undefined, + }; + } + + private makeRelationshipCreate(model: DataModel, field: DataModelField, policies: Policies) { + return { + operationId: `create-${model.name}-relationship-${field.name}`, + description: `Create new "${field.name}" relationships for a "${model.name}"`, + tags: [lowerCaseFirst(model.name)], + parameters: [this.parameter('id')], + requestBody: { + content: { + 'application/vnd.api+json': { + schema: this.ref('_toManyRelationshipRequest'), + }, + }, + }, + responses: { + '200': this.success('_toManyRelationshipResponse'), + '403': this.forbidden(), + '404': this.notFound(), + }, + security: policies.update === true ? [] : undefined, + }; + } + + private makeRelationshipUpdate(model: DataModel, field: DataModelField, policies: Policies) { + return { + operationId: `update-${model.name}-relationship-${field.name}`, + description: `Update "${field.name}" ${pluralize('relationship', field.type.array ? 2 : 1)} for a "${ + model.name + }"`, + tags: [lowerCaseFirst(model.name)], + parameters: [this.parameter('id')], + requestBody: { + content: { + 'application/vnd.api+json': { + schema: field.type.array + ? this.ref('_toManyRelationshipRequest') + : this.ref('_toOneRelationshipRequest'), + }, + }, + }, + responses: { + '200': field.type.array + ? this.success('_toManyRelationshipResponse') + : this.success('_toOneRelationshipResponse'), + '403': this.forbidden(), + '404': this.notFound(), + }, + security: policies.update === true ? [] : undefined, + }; + } + + private generateFilterParameters(model: DataModel) { + const result: OAPI.ParameterObject[] = []; + + for (const field of model.fields) { + if (isForeignKeyField(field)) { + // no filtering with foreign keys because one can filter + // directly on the relationship + continue; + } + + if (isIdField(field)) { + // id filter + result.push(this.makeFilterParameter(field, 'id', 'Id filter')); + continue; + } + + // equality filter + result.push(this.makeFilterParameter(field, '', 'Equality filter', field.type.array)); + + if (isRelationshipField(field)) { + // TODO: how to express nested filters? + continue; + } + + if (field.type.array) { + // collection filters + result.push(this.makeFilterParameter(field, '$has', 'Collection contains filter')); + result.push(this.makeFilterParameter(field, '$hasEvery', 'Collection contains-all filter', true)); + result.push(this.makeFilterParameter(field, '$hasSome', 'Collection contains-any filter', true)); + result.push( + this.makeFilterParameter(field, '$isEmpty', 'Collection is empty filter', false, { + type: 'boolean', + }) + ); + } else { + if (field.type.type && ['Int', 'BigInt', 'Float', 'Decimal', 'DateTime'].includes(field.type.type)) { + // comparison filters + result.push(this.makeFilterParameter(field, '$lt', 'Less-than filter')); + result.push(this.makeFilterParameter(field, '$lte', 'Less-than or equal filter')); + result.push(this.makeFilterParameter(field, '$gt', 'Greater-than filter')); + result.push(this.makeFilterParameter(field, '$gte', 'Greater-than or equal filter')); + } + + if (field.type.type === 'String') { + result.push(this.makeFilterParameter(field, '$contains', 'String contains filter')); + result.push( + this.makeFilterParameter(field, '$icontains', 'String case-insensitive contains filter') + ); + result.push(this.makeFilterParameter(field, '$search', 'String full-text search filter')); + result.push(this.makeFilterParameter(field, '$startsWith', 'String startsWith filter')); + result.push(this.makeFilterParameter(field, '$endsWith', 'String endsWith filter')); + } + } + } + + return result; + } + + private makeFilterParameter( + field: DataModelField, + name: string, + description: string, + array = false, + schemaOverride?: OAPI.SchemaObject + ) { + let schema: OAPI.SchemaObject | OAPI.ReferenceObject; + + if (schemaOverride) { + schema = schemaOverride; + } else { + const fieldDecl = field.type.reference?.ref; + if (isEnum(fieldDecl)) { + schema = this.ref(fieldDecl.name); + } else if (isDataModel(fieldDecl)) { + schema = { type: 'string' }; + } else { + invariant(field.type.type); + schema = this.fieldTypeToOpenAPISchema(field.type); + } + } + if (array) { + schema = { type: 'array', items: schema }; + } + + return { + name: name === 'id' ? 'filter[id]' : `filter[${field.name}${name}]`, + required: false, + description: name === 'id' ? description : `${description} for "${field.name}"`, + in: 'query', + style: 'form', + explode: false, + schema, + } as OAPI.ParameterObject; + } + + private generateComponents() { + const schemas: Record = {}; + const parameters: Record = {}; + const components: OAPI.ComponentsObject = { + schemas, + parameters, + }; + + for (const [name, value] of Object.entries(this.generateSharedComponents())) { + schemas[name] = value; + } + + for (const [name, value] of Object.entries(this.generateParameters())) { + parameters[name] = value; + } + + for (const _enum of this.model.declarations.filter((d): d is Enum => isEnum(d))) { + schemas[_enum.name] = this.generateEnumComponent(_enum); + } + + // data models + for (const model of getDataModels(this.model)) { + if (!this.includedModels.includes(model)) { + continue; + } + for (const [name, value] of Object.entries(this.generateDataModelComponents(model))) { + schemas[name] = value; + } + } + + return components; + } + + private generateSharedComponents(): Record { + return { + _jsonapi: { + type: 'object', + description: 'An object describing the server’s implementation', + properties: { + version: { type: 'string' }, + meta: this.ref('_meta'), + }, + }, + _meta: { + type: 'object', + description: 'Meta information about the response', + additionalProperties: true, + }, + _resourceIdentifier: { + type: 'object', + description: 'Identifier for a resource', + required: ['type', 'id'], + properties: { + type: { type: 'string' }, + id: { type: 'string' }, + }, + }, + _resource: this.allOf(this.ref('_resourceIdentifier'), { + type: 'object', + description: 'A resource with attributes and relationships', + properties: { + attributes: { type: 'object' }, + relationships: { type: 'object' }, + }, + }), + _links: { + type: 'object', + required: ['self'], + description: 'Links related to the resource', + properties: { self: { type: 'string' } }, + }, + _pagination: { + type: 'object', + description: 'Pagination information', + properties: { + first: this.nullable({ type: 'string' }), + last: this.nullable({ type: 'string' }), + prev: this.nullable({ type: 'string' }), + next: this.nullable({ type: 'string' }), + }, + }, + _errors: { + type: 'array', + description: 'An array of error objects', + items: { + type: 'object', + properties: { + status: { type: 'string' }, + code: { type: 'string' }, + title: { type: 'string' }, + detail: { type: 'string' }, + }, + }, + }, + _errorResponse: { + type: 'object', + required: ['errors'], + description: 'An error response', + properties: { + jsonapi: this.ref('_jsonapi'), + errors: this.ref('_errors'), + }, + }, + _relationLinks: { + type: 'object', + required: ['self', 'related'], + description: 'Links related to a relationship', + properties: { + self: { type: 'string' }, + related: { type: 'string' }, + }, + }, + _toOneRelationship: { + type: 'object', + description: 'A to-one relationship', + properties: { + data: this.nullable(this.ref('_resourceIdentifier')), + }, + }, + _toOneRelationshipWithLinks: { + type: 'object', + required: ['links', 'data'], + description: 'A to-one relationship with links', + properties: { + links: this.ref('_relationLinks'), + data: this.nullable(this.ref('_resourceIdentifier')), + }, + }, + _toManyRelationship: { + type: 'object', + required: ['data'], + description: 'A to-many relationship', + properties: { + data: this.array(this.ref('_resourceIdentifier')), + }, + }, + _toManyRelationshipWithLinks: { + type: 'object', + required: ['links', 'data'], + description: 'A to-many relationship with links', + properties: { + links: this.ref('_pagedRelationLinks'), + data: this.array(this.ref('_resourceIdentifier')), + }, + }, + _pagedRelationLinks: { + description: 'Relationship links with pagination information', + ...this.allOf(this.ref('_pagination'), this.ref('_relationLinks')), + }, + _toManyRelationshipRequest: { + type: 'object', + required: ['data'], + description: 'Input for manipulating a to-many relationship', + properties: { + data: { + type: 'array', + items: this.ref('_resourceIdentifier'), + }, + }, + }, + _toOneRelationshipRequest: { + description: 'Input for manipulating a to-one relationship', + ...this.nullable({ + type: 'object', + required: ['data'], + properties: { + data: this.ref('_resourceIdentifier'), + }, + }), + }, + _toManyRelationshipResponse: { + description: 'Response for a to-many relationship', + ...this.allOf(this.ref('_toManyRelationshipWithLinks'), { + type: 'object', + properties: { + jsonapi: this.ref('_jsonapi'), + }, + }), + }, + _toOneRelationshipResponse: { + description: 'Response for a to-one relationship', + ...this.allOf(this.ref('_toOneRelationshipWithLinks'), { + type: 'object', + properties: { + jsonapi: this.ref('_jsonapi'), + }, + }), + }, + }; + } + + private generateParameters(): Record { + return { + id: { + name: 'id', + in: 'path', + description: 'The resource id', + required: true, + schema: { type: 'string' }, + }, + include: { + name: 'include', + in: 'query', + description: 'Relationships to include', + required: false, + style: 'form', + schema: { type: 'string' }, + }, + sort: { + name: 'sort', + in: 'query', + description: 'Fields to sort by', + required: false, + style: 'form', + schema: { type: 'string' }, + }, + 'page-offset': { + name: 'page[offset]', + in: 'query', + description: 'Offset for pagination', + required: false, + style: 'form', + schema: { type: 'integer' }, + }, + 'page-limit': { + name: 'page[limit]', + in: 'query', + description: 'Limit for pagination', + required: false, + style: 'form', + schema: { type: 'integer' }, + }, + }; + } + + private generateEnumComponent(_enum: Enum): OAPI.SchemaObject { + const schema: OAPI.SchemaObject = { + type: 'string', + description: `The "${_enum.name}" Enum`, + enum: _enum.fields.map((f) => f.name), + }; + return schema; + } + + private generateDataModelComponents(model: DataModel) { + const result: Record = {}; + result[`${model.name}`] = this.generateModelEntity(model, 'read'); + + result[`${model.name}CreateRequest`] = { + type: 'object', + description: `Input for creating a "${model.name}"`, + required: ['data'], + properties: { + data: this.generateModelEntity(model, 'create'), + }, + }; + + result[`${model.name}UpdateRequest`] = { + type: 'object', + description: `Input for updating a "${model.name}"`, + required: ['data'], + properties: { data: this.generateModelEntity(model, 'update') }, + }; + + const relationships: Record = {}; + for (const field of model.fields) { + if (isRelationshipField(field)) { + if (field.type.array) { + relationships[field.name] = this.ref('_toManyRelationship'); + } else { + relationships[field.name] = this.ref('_toOneRelationship'); + } + } + } + + result[`${model.name}Response`] = { + type: 'object', + description: `Response for a "${model.name}"`, + required: ['data'], + properties: { + jsonapi: this.ref('_jsonapi'), + data: this.allOf(this.ref(`${model.name}`), { + type: 'object', + properties: { relationships: { type: 'object', properties: relationships } }, + }), + + included: { + type: 'array', + items: this.ref('_resource'), + }, + links: this.ref('_links'), + }, + }; + + result[`${model.name}ListResponse`] = { + type: 'object', + description: `Response for a list of "${model.name}"`, + required: ['data', 'links'], + properties: { + jsonapi: this.ref('_jsonapi'), + data: this.array( + this.allOf(this.ref(`${model.name}`), { + type: 'object', + properties: { relationships: { type: 'object', properties: relationships } }, + }) + ), + included: { + type: 'array', + items: this.ref('_resource'), + }, + links: this.allOf(this.ref('_links'), this.ref('_pagination')), + }, + }; + + return result; + } + + private generateModelEntity(model: DataModel, mode: 'read' | 'create' | 'update'): OAPI.SchemaObject { + const fields = model.fields.filter((f) => !AUXILIARY_FIELDS.includes(f.name) && !isIdField(f)); + + const attributes: Record = {}; + const relationships: Record = {}; + + const required: string[] = []; + + for (const field of fields) { + if (isRelationshipField(field)) { + let relType: string; + if (mode === 'create' || mode === 'update') { + relType = field.type.array ? '_toManyRelationship' : '_toOneRelationship'; + } else { + relType = field.type.array ? '_toManyRelationshipWithLinks' : '_toOneRelationshipWithLinks'; + } + relationships[field.name] = this.ref(relType); + } else { + attributes[field.name] = this.generateField(field); + if ( + mode === 'create' && + !field.type.optional && + // collection relation fields are implicitly optional + !(isDataModel(field.$resolvedType?.decl) && field.type.array) + ) { + required.push(field.name); + } + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result: any = { + type: 'object', + description: `The "${model.name}" model`, + required: ['id', 'type', 'attributes'], + properties: { + type: { type: 'string' }, + id: { type: 'string' }, + attributes: { + type: 'object', + required: required.length > 0 ? required : undefined, + properties: attributes, + }, + }, + } satisfies OAPI.SchemaObject; + + if (Object.keys(relationships).length > 0) { + result.properties.relationships = { + type: 'object', + properties: relationships, + }; + } + + return result; + } + + private generateField(field: DataModelField) { + return this.wrapArray(this.fieldTypeToOpenAPISchema(field.type), field.type.array); + } + + private get specVersion() { + return this.getOption('specVersion', '3.0.0'); + } + + private fieldTypeToOpenAPISchema(type: DataModelFieldType): OAPI.ReferenceObject | OAPI.SchemaObject { + switch (type.type) { + case 'String': + return { type: 'string' }; + case 'Int': + case 'BigInt': + return { type: 'integer' }; + case 'Float': + case 'Decimal': + return { type: 'number' }; + case 'Boolean': + return { type: 'boolean' }; + case 'DateTime': + return { type: 'string', format: 'date-time' }; + case 'Json': + return { type: 'object' }; + default: { + const fieldDecl = type.reference?.ref; + invariant(fieldDecl); + return this.ref(fieldDecl?.name); + } + } + } + + private ref(type: string) { + return { $ref: `#/components/schemas/${type}` }; + } + + private nullable(schema: OAPI.SchemaObject | OAPI.ReferenceObject) { + return this.specVersion === '3.0.0' ? { ...schema, nullable: true } : this.oneOf(schema, { type: 'null' }); + } + + private parameter(type: string) { + return { $ref: `#/components/parameters/${type}` }; + } + + private forbidden() { + return { + description: 'Request is forbidden', + content: { + 'application/vnd.api+json': { + schema: this.ref('_errorResponse'), + }, + }, + }; + } + + private notFound() { + return { + description: 'Resource is not found', + content: { + 'application/vnd.api+json': { + schema: this.ref('_errorResponse'), + }, + }, + }; + } + + private success(responseComponent?: string) { + return { + description: 'Successful operation', + content: responseComponent + ? { + 'application/vnd.api+json': { + schema: this.ref(responseComponent), + }, + } + : undefined, + }; + } +} diff --git a/packages/plugins/openapi/src/generator.ts b/packages/plugins/openapi/src/rpc-generator.ts similarity index 93% rename from packages/plugins/openapi/src/generator.ts rename to packages/plugins/openapi/src/rpc-generator.ts index 8e3dd43f0..3372bd201 100644 --- a/packages/plugins/openapi/src/generator.ts +++ b/packages/plugins/openapi/src/rpc-generator.ts @@ -1,15 +1,8 @@ // Inspired by: https://github.com/omar-dulaimi/prisma-trpc-generator import { DMMF } from '@prisma/generator-helper'; -import { - analyzePolicies, - AUXILIARY_FIELDS, - getDataModels, - hasAttribute, - PluginError, - PluginOptions, -} from '@zenstackhq/sdk'; -import { DataModel, isDataModel, type Model } from '@zenstackhq/sdk/ast'; +import { analyzePolicies, AUXILIARY_FIELDS, PluginError } from '@zenstackhq/sdk'; +import { DataModel, isDataModel } from '@zenstackhq/sdk/ast'; import { addMissingInputObjectTypesForAggregate, addMissingInputObjectTypesForInclude, @@ -18,29 +11,25 @@ import { AggregateOperationSupport, resolveAggregateOperationSupport, } from '@zenstackhq/sdk/dmmf-helpers'; -import { lowerCaseFirst } from 'lower-case-first'; import * as fs from 'fs'; +import { lowerCaseFirst } from 'lower-case-first'; import type { OpenAPIV3_1 as OAPI } from 'openapi-types'; import * as path from 'path'; import invariant from 'tiny-invariant'; import YAML from 'yaml'; -import { fromZodError } from 'zod-validation-error'; +import { OpenAPIGeneratorBase } from './generator-base'; import { getModelResourceMeta } from './meta'; -import { SecuritySchemesSchema } from './schema'; /** * Generates OpenAPI specification. */ -export class OpenAPIGenerator { +export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { private inputObjectTypes: DMMF.InputType[] = []; private outputObjectTypes: DMMF.OutputType[] = []; private usedComponents: Set = new Set(); private aggregateOperationSupport: AggregateOperationSupport; - private includedModels: DataModel[]; private warnings: string[] = []; - constructor(private model: Model, private options: PluginOptions, private dmmf: DMMF.Document) {} - generate() { const output = this.getOption('output', ''); if (!output) { @@ -50,7 +39,6 @@ export class OpenAPIGenerator { // input types this.inputObjectTypes.push(...this.dmmf.schema.inputObjectTypes.prisma); this.outputObjectTypes.push(...this.dmmf.schema.outputObjectTypes.prisma); - this.includedModels = getDataModels(this.model).filter((d) => !hasAttribute(d, '@@openapi.ignore')); // add input object types that are missing from Prisma dmmf addMissingInputObjectTypesForModelArgs(this.inputObjectTypes, this.dmmf.datamodel.models); @@ -64,7 +52,7 @@ export class OpenAPIGenerator { const paths = this.generatePaths(components); // generate security schemes, and root-level security - this.generateSecuritySchemes(components); + components.securitySchemes = this.generateSecuritySchemes(); let security: OAPI.Document['security'] | undefined = undefined; if (components.securitySchemes && Object.keys(components.securitySchemes).length > 0) { security = Object.keys(components.securitySchemes).map((scheme) => ({ [scheme]: [] })); @@ -103,17 +91,6 @@ export class OpenAPIGenerator { return this.warnings; } - private generateSecuritySchemes(components: OAPI.ComponentsObject) { - const securitySchemes = this.getOption[]>('securitySchemes'); - if (securitySchemes) { - const parsed = SecuritySchemesSchema.safeParse(securitySchemes); - if (!parsed.success) { - throw new PluginError(`"securitySchemes" option is invalid: ${fromZodError(parsed.error)}`); - } - components.securitySchemes = parsed.data; - } - } - private pruneComponents(components: OAPI.ComponentsObject) { const schemas = components.schemas; if (schemas) { @@ -551,7 +528,7 @@ export class OpenAPIGenerator { description: 'Invalid request', }, '403': { - description: 'Forbidden', + description: 'Request is forbidden', }, }, }; @@ -617,15 +594,6 @@ export class OpenAPIGenerator { return this.ref(name); } - private getOption(name: string): T | undefined; - private getOption(name: string, defaultValue: D): T; - private getOption(name: string, defaultValue?: T): T | undefined { - const value = this.options[name]; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - return value === undefined ? defaultValue : value; - } - private generateComponents() { const schemas: Record = {}; const components: OAPI.ComponentsObject = { @@ -784,29 +752,10 @@ export class OpenAPIGenerator { } } - private wrapArray( - schema: OAPI.ReferenceObject | OAPI.SchemaObject, - isArray: boolean - ): OAPI.ReferenceObject | OAPI.SchemaObject { - if (isArray) { - return { type: 'array', items: schema }; - } else { - return schema; - } - } - private ref(type: string, rooted = true) { if (rooted) { this.usedComponents.add(type); } return { $ref: `#/components/schemas/${type}` }; } - - private array(itemType: unknown) { - return { type: 'array', items: itemType }; - } - - private oneOf(...schemas: unknown[]) { - return { oneOf: schemas }; - } } diff --git a/packages/plugins/openapi/tests/baseline/rest.baseline.yaml b/packages/plugins/openapi/tests/baseline/rest.baseline.yaml new file mode 100644 index 000000000..d3f1f7ee6 --- /dev/null +++ b/packages/plugins/openapi/tests/baseline/rest.baseline.yaml @@ -0,0 +1,1727 @@ +openapi: 3.1.0 +info: + title: ZenStack Generated API + version: 1.0.0 +tags: + - name: user + description: User operations + - name: post + description: Post-related operations +paths: + /user: + get: + operationId: list-User + description: List "User" resources + tags: + - user + parameters: + - $ref: '#/components/parameters/include' + - $ref: '#/components/parameters/sort' + - $ref: '#/components/parameters/page-offset' + - $ref: '#/components/parameters/page-limit' + - name: filter[id] + required: false + description: Id filter + in: query + style: form + explode: false + schema: + type: string + - name: filter[createdAt] + required: false + description: Equality filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[createdAt$lt] + required: false + description: Less-than filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[createdAt$lte] + required: false + description: Less-than or equal filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[createdAt$gt] + required: false + description: Greater-than filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[createdAt$gte] + required: false + description: Greater-than or equal filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt] + required: false + description: Equality filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt$lt] + required: false + description: Less-than filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt$lte] + required: false + description: Less-than or equal filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt$gt] + required: false + description: Greater-than filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt$gte] + required: false + description: Greater-than or equal filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[email] + required: false + description: Equality filter for "email" + in: query + style: form + explode: false + schema: + type: string + - name: filter[email$contains] + required: false + description: String contains filter for "email" + in: query + style: form + explode: false + schema: + type: string + - name: filter[email$icontains] + required: false + description: String case-insensitive contains filter for "email" + in: query + style: form + explode: false + schema: + type: string + - name: filter[email$search] + required: false + description: String full-text search filter for "email" + in: query + style: form + explode: false + schema: + type: string + - name: filter[email$startsWith] + required: false + description: String startsWith filter for "email" + in: query + style: form + explode: false + schema: + type: string + - name: filter[email$endsWith] + required: false + description: String endsWith filter for "email" + in: query + style: form + explode: false + schema: + type: string + - name: filter[role] + required: false + description: Equality filter for "role" + in: query + style: form + explode: false + schema: + $ref: '#/components/schemas/Role' + - name: filter[posts] + required: false + description: Equality filter for "posts" + in: query + style: form + explode: false + schema: + type: array + items: + type: string + responses: + '200': + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/UserListResponse' + '403': + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + post: + operationId: create-User + description: Create a "User" resource + tags: + - user + requestBody: + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/UserCreateRequest' + responses: + '201': + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/UserResponse' + '403': + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + '/user/{id}': + get: + operationId: fetch-User + description: Fetch a "User" resource + tags: + - user + parameters: + - $ref: '#/components/parameters/id' + - $ref: '#/components/parameters/include' + responses: + '200': + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/UserResponse' + '403': + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + '404': + description: Resource is not found + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + patch: + operationId: update-User + description: Update a "User" resource + tags: + - user + parameters: + - $ref: '#/components/parameters/id' + requestBody: + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/UserUpdateRequest' + responses: + '200': + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/UserResponse' + '403': + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + '404': + description: Resource is not found + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + delete: + operationId: delete-User + description: Delete a "User" resource + tags: + - user + parameters: + - $ref: '#/components/parameters/id' + responses: + '200': + description: Successful operation + '403': + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + '404': + description: Resource is not found + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + '/user/{id}/posts': + get: + operationId: fetch-User-related-posts + description: Fetch the related "posts" resource for "User" + tags: + - user + parameters: + - $ref: '#/components/parameters/id' + - $ref: '#/components/parameters/include' + - $ref: '#/components/parameters/sort' + - $ref: '#/components/parameters/page-offset' + - $ref: '#/components/parameters/page-limit' + - name: filter[id] + required: false + description: Id filter + in: query + style: form + explode: false + schema: + type: string + - name: filter[createdAt] + required: false + description: Equality filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[createdAt$lt] + required: false + description: Less-than filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[createdAt$lte] + required: false + description: Less-than or equal filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[createdAt$gt] + required: false + description: Greater-than filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[createdAt$gte] + required: false + description: Greater-than or equal filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt] + required: false + description: Equality filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt$lt] + required: false + description: Less-than filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt$lte] + required: false + description: Less-than or equal filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt$gt] + required: false + description: Greater-than filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt$gte] + required: false + description: Greater-than or equal filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[email] + required: false + description: Equality filter for "email" + in: query + style: form + explode: false + schema: + type: string + - name: filter[email$contains] + required: false + description: String contains filter for "email" + in: query + style: form + explode: false + schema: + type: string + - name: filter[email$icontains] + required: false + description: String case-insensitive contains filter for "email" + in: query + style: form + explode: false + schema: + type: string + - name: filter[email$search] + required: false + description: String full-text search filter for "email" + in: query + style: form + explode: false + schema: + type: string + - name: filter[email$startsWith] + required: false + description: String startsWith filter for "email" + in: query + style: form + explode: false + schema: + type: string + - name: filter[email$endsWith] + required: false + description: String endsWith filter for "email" + in: query + style: form + explode: false + schema: + type: string + - name: filter[role] + required: false + description: Equality filter for "role" + in: query + style: form + explode: false + schema: + $ref: '#/components/schemas/Role' + - name: filter[posts] + required: false + description: Equality filter for "posts" + in: query + style: form + explode: false + schema: + type: array + items: + type: string + responses: + '200': + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/PostListResponse' + '403': + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + '404': + description: Resource is not found + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + '/user/{id}/relationships/posts': + get: + operationId: fetch-User-relationship-posts + description: Fetch the "posts" relationships for a "User" + tags: + - user + parameters: + - $ref: '#/components/parameters/id' + - $ref: '#/components/parameters/sort' + - $ref: '#/components/parameters/page-offset' + - $ref: '#/components/parameters/page-limit' + - name: filter[id] + required: false + description: Id filter + in: query + style: form + explode: false + schema: + type: string + - name: filter[createdAt] + required: false + description: Equality filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[createdAt$lt] + required: false + description: Less-than filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[createdAt$lte] + required: false + description: Less-than or equal filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[createdAt$gt] + required: false + description: Greater-than filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[createdAt$gte] + required: false + description: Greater-than or equal filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt] + required: false + description: Equality filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt$lt] + required: false + description: Less-than filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt$lte] + required: false + description: Less-than or equal filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt$gt] + required: false + description: Greater-than filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt$gte] + required: false + description: Greater-than or equal filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[email] + required: false + description: Equality filter for "email" + in: query + style: form + explode: false + schema: + type: string + - name: filter[email$contains] + required: false + description: String contains filter for "email" + in: query + style: form + explode: false + schema: + type: string + - name: filter[email$icontains] + required: false + description: String case-insensitive contains filter for "email" + in: query + style: form + explode: false + schema: + type: string + - name: filter[email$search] + required: false + description: String full-text search filter for "email" + in: query + style: form + explode: false + schema: + type: string + - name: filter[email$startsWith] + required: false + description: String startsWith filter for "email" + in: query + style: form + explode: false + schema: + type: string + - name: filter[email$endsWith] + required: false + description: String endsWith filter for "email" + in: query + style: form + explode: false + schema: + type: string + - name: filter[role] + required: false + description: Equality filter for "role" + in: query + style: form + explode: false + schema: + $ref: '#/components/schemas/Role' + - name: filter[posts] + required: false + description: Equality filter for "posts" + in: query + style: form + explode: false + schema: + type: array + items: + type: string + responses: + '200': + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_toManyRelationshipResponse' + '403': + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + '404': + description: Resource is not found + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + patch: + operationId: update-User-relationship-posts + description: Update "posts" relationships for a "User" + tags: + - user + parameters: + - $ref: '#/components/parameters/id' + requestBody: + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_toManyRelationshipRequest' + responses: + '200': + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_toManyRelationshipResponse' + '403': + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + '404': + description: Resource is not found + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + post: + operationId: create-User-relationship-posts + description: Create new "posts" relationships for a "User" + tags: + - user + parameters: + - $ref: '#/components/parameters/id' + requestBody: + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_toManyRelationshipRequest' + responses: + '200': + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_toManyRelationshipResponse' + '403': + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + '404': + description: Resource is not found + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + /post: + get: + operationId: list-Post + description: List "Post" resources + tags: + - post + parameters: + - $ref: '#/components/parameters/include' + - $ref: '#/components/parameters/sort' + - $ref: '#/components/parameters/page-offset' + - $ref: '#/components/parameters/page-limit' + - name: filter[id] + required: false + description: Id filter + in: query + style: form + explode: false + schema: + type: string + - name: filter[createdAt] + required: false + description: Equality filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[createdAt$lt] + required: false + description: Less-than filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[createdAt$lte] + required: false + description: Less-than or equal filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[createdAt$gt] + required: false + description: Greater-than filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[createdAt$gte] + required: false + description: Greater-than or equal filter for "createdAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt] + required: false + description: Equality filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt$lt] + required: false + description: Less-than filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt$lte] + required: false + description: Less-than or equal filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt$gt] + required: false + description: Greater-than filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[updatedAt$gte] + required: false + description: Greater-than or equal filter for "updatedAt" + in: query + style: form + explode: false + schema: + type: string + format: date-time + - name: filter[title] + required: false + description: Equality filter for "title" + in: query + style: form + explode: false + schema: + type: string + - name: filter[title$contains] + required: false + description: String contains filter for "title" + in: query + style: form + explode: false + schema: + type: string + - name: filter[title$icontains] + required: false + description: String case-insensitive contains filter for "title" + in: query + style: form + explode: false + schema: + type: string + - name: filter[title$search] + required: false + description: String full-text search filter for "title" + in: query + style: form + explode: false + schema: + type: string + - name: filter[title$startsWith] + required: false + description: String startsWith filter for "title" + in: query + style: form + explode: false + schema: + type: string + - name: filter[title$endsWith] + required: false + description: String endsWith filter for "title" + in: query + style: form + explode: false + schema: + type: string + - name: filter[author] + required: false + description: Equality filter for "author" + in: query + style: form + explode: false + schema: + type: string + - name: filter[published] + required: false + description: Equality filter for "published" + in: query + style: form + explode: false + schema: + type: boolean + - name: filter[viewCount] + required: false + description: Equality filter for "viewCount" + in: query + style: form + explode: false + schema: + type: integer + - name: filter[viewCount$lt] + required: false + description: Less-than filter for "viewCount" + in: query + style: form + explode: false + schema: + type: integer + - name: filter[viewCount$lte] + required: false + description: Less-than or equal filter for "viewCount" + in: query + style: form + explode: false + schema: + type: integer + - name: filter[viewCount$gt] + required: false + description: Greater-than filter for "viewCount" + in: query + style: form + explode: false + schema: + type: integer + - name: filter[viewCount$gte] + required: false + description: Greater-than or equal filter for "viewCount" + in: query + style: form + explode: false + schema: + type: integer + responses: + '200': + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/PostListResponse' + '403': + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + post: + operationId: create-Post + description: Create a "Post" resource + tags: + - post + requestBody: + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/PostCreateRequest' + responses: + '201': + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/PostResponse' + '403': + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + '/post/{id}': + get: + operationId: fetch-Post + description: Fetch a "Post" resource + tags: + - post + parameters: + - $ref: '#/components/parameters/id' + - $ref: '#/components/parameters/include' + responses: + '200': + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/PostResponse' + '403': + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + '404': + description: Resource is not found + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + patch: + operationId: update-Post + description: Update a "Post" resource + tags: + - post + parameters: + - $ref: '#/components/parameters/id' + requestBody: + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/PostUpdateRequest' + responses: + '200': + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/PostResponse' + '403': + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + '404': + description: Resource is not found + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + delete: + operationId: delete-Post + description: Delete a "Post" resource + tags: + - post + parameters: + - $ref: '#/components/parameters/id' + responses: + '200': + description: Successful operation + '403': + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + '404': + description: Resource is not found + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + '/post/{id}/author': + get: + operationId: fetch-Post-related-author + description: Fetch the related "author" resource for "Post" + tags: + - post + parameters: + - $ref: '#/components/parameters/id' + - $ref: '#/components/parameters/include' + responses: + '200': + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/UserResponse' + '403': + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + '404': + description: Resource is not found + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + '/post/{id}/relationships/author': + get: + operationId: fetch-Post-relationship-author + description: Fetch the "author" relationships for a "Post" + tags: + - post + parameters: + - $ref: '#/components/parameters/id' + responses: + '200': + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_toOneRelationshipResponse' + '403': + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + '404': + description: Resource is not found + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + patch: + operationId: update-Post-relationship-author + description: Update "author" relationship for a "Post" + tags: + - post + parameters: + - $ref: '#/components/parameters/id' + requestBody: + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_toOneRelationshipRequest' + responses: + '200': + description: Successful operation + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_toOneRelationshipResponse' + '403': + description: Request is forbidden + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' + '404': + description: Resource is not found + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/_errorResponse' +components: + schemas: + _jsonapi: + type: object + description: An object describing the server’s implementation + properties: + version: + type: string + meta: + $ref: '#/components/schemas/_meta' + _meta: + type: object + description: Meta information about the response + additionalProperties: true + _resourceIdentifier: + type: object + description: Identifier for a resource + required: + - type + - id + properties: + type: + type: string + id: + type: string + _resource: + allOf: + - $ref: '#/components/schemas/_resourceIdentifier' + - type: object + description: A resource with attributes and relationships + properties: + attributes: + type: object + relationships: + type: object + _links: + type: object + required: + - self + description: Links related to the resource + properties: + self: + type: string + _pagination: + type: object + description: Pagination information + properties: + first: + oneOf: + - type: string + - type: 'null' + last: + oneOf: + - type: string + - type: 'null' + prev: + oneOf: + - type: string + - type: 'null' + next: + oneOf: + - type: string + - type: 'null' + _errors: + type: array + description: An array of error objects + items: + type: object + properties: + status: + type: string + code: + type: string + title: + type: string + detail: + type: string + _errorResponse: + type: object + required: + - errors + description: An error response + properties: + jsonapi: + $ref: '#/components/schemas/_jsonapi' + errors: + $ref: '#/components/schemas/_errors' + _relationLinks: + type: object + required: + - self + - related + description: Links related to a relationship + properties: + self: + type: string + related: + type: string + _toOneRelationship: + type: object + description: A to-one relationship + properties: + data: + oneOf: + - $ref: '#/components/schemas/_resourceIdentifier' + - type: 'null' + _toOneRelationshipWithLinks: + type: object + required: + - links + - data + description: A to-one relationship with links + properties: + links: + $ref: '#/components/schemas/_relationLinks' + data: + oneOf: + - $ref: '#/components/schemas/_resourceIdentifier' + - type: 'null' + _toManyRelationship: + type: object + required: + - data + description: A to-many relationship + properties: + data: + type: array + items: + $ref: '#/components/schemas/_resourceIdentifier' + _toManyRelationshipWithLinks: + type: object + required: + - links + - data + description: A to-many relationship with links + properties: + links: + $ref: '#/components/schemas/_pagedRelationLinks' + data: + type: array + items: + $ref: '#/components/schemas/_resourceIdentifier' + _pagedRelationLinks: + description: Relationship links with pagination information + allOf: + - $ref: '#/components/schemas/_pagination' + - $ref: '#/components/schemas/_relationLinks' + _toManyRelationshipRequest: + type: object + required: + - data + description: Input for manipulating a to-many relationship + properties: + data: + type: array + items: + $ref: '#/components/schemas/_resourceIdentifier' + _toOneRelationshipRequest: + description: Input for manipulating a to-one relationship + oneOf: + - type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/_resourceIdentifier' + - type: 'null' + _toManyRelationshipResponse: + description: Response for a to-many relationship + allOf: + - $ref: '#/components/schemas/_toManyRelationshipWithLinks' + - type: object + properties: + jsonapi: + $ref: '#/components/schemas/_jsonapi' + _toOneRelationshipResponse: + description: Response for a to-one relationship + allOf: + - $ref: '#/components/schemas/_toOneRelationshipWithLinks' + - type: object + properties: + jsonapi: + $ref: '#/components/schemas/_jsonapi' + Role: + type: string + description: The "Role" Enum + enum: + - USER + - ADMIN + User: + type: object + description: The "User" model + required: + - id + - type + - attributes + properties: + type: + type: string + id: + type: string + attributes: + type: object + properties: + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + email: + type: string + role: + $ref: '#/components/schemas/Role' + relationships: + type: object + properties: + posts: + $ref: '#/components/schemas/_toManyRelationshipWithLinks' + UserCreateRequest: + type: object + description: Input for creating a "User" + required: + - data + properties: + data: + type: object + description: The "User" model + required: + - id + - type + - attributes + properties: + type: + type: string + id: + type: string + attributes: + type: object + required: + - createdAt + - updatedAt + - email + - role + properties: + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + email: + type: string + role: + $ref: '#/components/schemas/Role' + relationships: + type: object + properties: + posts: + $ref: '#/components/schemas/_toManyRelationship' + UserUpdateRequest: + type: object + description: Input for updating a "User" + required: + - data + properties: + data: + type: object + description: The "User" model + required: + - id + - type + - attributes + properties: + type: + type: string + id: + type: string + attributes: + type: object + properties: + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + email: + type: string + role: + $ref: '#/components/schemas/Role' + relationships: + type: object + properties: + posts: + $ref: '#/components/schemas/_toManyRelationship' + UserResponse: + type: object + description: Response for a "User" + required: + - data + properties: + jsonapi: + $ref: '#/components/schemas/_jsonapi' + data: + allOf: + - $ref: '#/components/schemas/User' + - type: object + properties: + relationships: + type: object + properties: &a1 + posts: + $ref: '#/components/schemas/_toManyRelationship' + included: + type: array + items: + $ref: '#/components/schemas/_resource' + links: + $ref: '#/components/schemas/_links' + UserListResponse: + type: object + description: Response for a list of "User" + required: + - data + - links + properties: + jsonapi: + $ref: '#/components/schemas/_jsonapi' + data: + type: array + items: + allOf: + - $ref: '#/components/schemas/User' + - type: object + properties: + relationships: + type: object + properties: *a1 + included: + type: array + items: + $ref: '#/components/schemas/_resource' + links: + allOf: + - $ref: '#/components/schemas/_links' + - $ref: '#/components/schemas/_pagination' + Post: + type: object + description: The "Post" model + required: + - id + - type + - attributes + properties: + type: + type: string + id: + type: string + attributes: + type: object + properties: + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + title: + type: string + authorId: + type: string + published: + type: boolean + viewCount: + type: integer + relationships: + type: object + properties: + author: + $ref: '#/components/schemas/_toOneRelationshipWithLinks' + PostCreateRequest: + type: object + description: Input for creating a "Post" + required: + - data + properties: + data: + type: object + description: The "Post" model + required: + - id + - type + - attributes + properties: + type: + type: string + id: + type: string + attributes: + type: object + required: + - createdAt + - updatedAt + - title + - published + - viewCount + properties: + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + title: + type: string + authorId: + type: string + published: + type: boolean + viewCount: + type: integer + relationships: + type: object + properties: + author: + $ref: '#/components/schemas/_toOneRelationship' + PostUpdateRequest: + type: object + description: Input for updating a "Post" + required: + - data + properties: + data: + type: object + description: The "Post" model + required: + - id + - type + - attributes + properties: + type: + type: string + id: + type: string + attributes: + type: object + properties: + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + title: + type: string + authorId: + type: string + published: + type: boolean + viewCount: + type: integer + relationships: + type: object + properties: + author: + $ref: '#/components/schemas/_toOneRelationship' + PostResponse: + type: object + description: Response for a "Post" + required: + - data + properties: + jsonapi: + $ref: '#/components/schemas/_jsonapi' + data: + allOf: + - $ref: '#/components/schemas/Post' + - type: object + properties: + relationships: + type: object + properties: &a2 + author: + $ref: '#/components/schemas/_toOneRelationship' + included: + type: array + items: + $ref: '#/components/schemas/_resource' + links: + $ref: '#/components/schemas/_links' + PostListResponse: + type: object + description: Response for a list of "Post" + required: + - data + - links + properties: + jsonapi: + $ref: '#/components/schemas/_jsonapi' + data: + type: array + items: + allOf: + - $ref: '#/components/schemas/Post' + - type: object + properties: + relationships: + type: object + properties: *a2 + included: + type: array + items: + $ref: '#/components/schemas/_resource' + links: + allOf: + - $ref: '#/components/schemas/_links' + - $ref: '#/components/schemas/_pagination' + parameters: + id: + name: id + in: path + description: The resource id + required: true + schema: + type: string + include: + name: include + in: query + description: Relationships to include + required: false + style: form + schema: + type: string + sort: + name: sort + in: query + description: Fields to sort by + required: false + style: form + schema: + type: string + page-offset: + name: page[offset] + in: query + description: Offset for pagination + required: false + style: form + schema: + type: integer + page-limit: + name: page[limit] + in: query + description: Limit for pagination + required: false + style: form + schema: + type: integer diff --git a/packages/plugins/openapi/tests/baseline/rpc.baseline.yaml b/packages/plugins/openapi/tests/baseline/rpc.baseline.yaml new file mode 100644 index 000000000..17f8fa8d4 --- /dev/null +++ b/packages/plugins/openapi/tests/baseline/rpc.baseline.yaml @@ -0,0 +1,3087 @@ +openapi: 3.1.0 +info: + title: ZenStack Generated API + version: 1.0.0 +tags: + - name: user + description: User operations + - name: post + description: Post-related operations +components: + schemas: + Role: + type: string + enum: + - USER + - ADMIN + PostScalarFieldEnum: + type: string + enum: + - id + - createdAt + - updatedAt + - title + - authorId + - published + - viewCount + QueryMode: + type: string + enum: + - default + - insensitive + SortOrder: + type: string + enum: + - asc + - desc + UserScalarFieldEnum: + type: string + enum: + - id + - createdAt + - updatedAt + - email + - role + User: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + email: + type: string + role: + $ref: '#/components/schemas/Role' + posts: + type: array + items: + $ref: '#/components/schemas/Post' + required: + - id + - createdAt + - updatedAt + - email + - role + Post: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + title: + type: string + author: + $ref: '#/components/schemas/User' + authorId: + type: string + published: + type: boolean + viewCount: + type: integer + required: + - id + - createdAt + - updatedAt + - title + - published + - viewCount + UserWhereInput: + type: object + properties: + AND: + oneOf: + - $ref: '#/components/schemas/UserWhereInput' + - type: array + items: + $ref: '#/components/schemas/UserWhereInput' + OR: + type: array + items: + $ref: '#/components/schemas/UserWhereInput' + NOT: + oneOf: + - $ref: '#/components/schemas/UserWhereInput' + - type: array + items: + $ref: '#/components/schemas/UserWhereInput' + id: + oneOf: + - $ref: '#/components/schemas/StringFilter' + - type: string + createdAt: + oneOf: + - $ref: '#/components/schemas/DateTimeFilter' + - type: string + format: date-time + updatedAt: + oneOf: + - $ref: '#/components/schemas/DateTimeFilter' + - type: string + format: date-time + email: + oneOf: + - $ref: '#/components/schemas/StringFilter' + - type: string + role: + oneOf: + - $ref: '#/components/schemas/EnumRoleFilter' + - $ref: '#/components/schemas/Role' + posts: + $ref: '#/components/schemas/PostListRelationFilter' + UserOrderByWithRelationInput: + type: object + properties: + id: + $ref: '#/components/schemas/SortOrder' + createdAt: + $ref: '#/components/schemas/SortOrder' + updatedAt: + $ref: '#/components/schemas/SortOrder' + email: + $ref: '#/components/schemas/SortOrder' + role: + $ref: '#/components/schemas/SortOrder' + posts: + $ref: '#/components/schemas/PostOrderByRelationAggregateInput' + UserWhereUniqueInput: + type: object + properties: + id: + type: string + email: + type: string + UserScalarWhereWithAggregatesInput: + type: object + properties: + AND: + oneOf: + - $ref: '#/components/schemas/UserScalarWhereWithAggregatesInput' + - type: array + items: + $ref: '#/components/schemas/UserScalarWhereWithAggregatesInput' + OR: + type: array + items: + $ref: '#/components/schemas/UserScalarWhereWithAggregatesInput' + NOT: + oneOf: + - $ref: '#/components/schemas/UserScalarWhereWithAggregatesInput' + - type: array + items: + $ref: '#/components/schemas/UserScalarWhereWithAggregatesInput' + id: + oneOf: + - $ref: '#/components/schemas/StringWithAggregatesFilter' + - type: string + createdAt: + oneOf: + - $ref: '#/components/schemas/DateTimeWithAggregatesFilter' + - type: string + format: date-time + updatedAt: + oneOf: + - $ref: '#/components/schemas/DateTimeWithAggregatesFilter' + - type: string + format: date-time + email: + oneOf: + - $ref: '#/components/schemas/StringWithAggregatesFilter' + - type: string + role: + oneOf: + - $ref: '#/components/schemas/EnumRoleWithAggregatesFilter' + - $ref: '#/components/schemas/Role' + PostWhereInput: + type: object + properties: + AND: + oneOf: + - $ref: '#/components/schemas/PostWhereInput' + - type: array + items: + $ref: '#/components/schemas/PostWhereInput' + OR: + type: array + items: + $ref: '#/components/schemas/PostWhereInput' + NOT: + oneOf: + - $ref: '#/components/schemas/PostWhereInput' + - type: array + items: + $ref: '#/components/schemas/PostWhereInput' + id: + oneOf: + - $ref: '#/components/schemas/StringFilter' + - type: string + createdAt: + oneOf: + - $ref: '#/components/schemas/DateTimeFilter' + - type: string + format: date-time + updatedAt: + oneOf: + - $ref: '#/components/schemas/DateTimeFilter' + - type: string + format: date-time + title: + oneOf: + - $ref: '#/components/schemas/StringFilter' + - type: string + author: + oneOf: + - $ref: '#/components/schemas/UserRelationFilter' + - $ref: '#/components/schemas/UserWhereInput' + authorId: + oneOf: + - $ref: '#/components/schemas/StringNullableFilter' + - type: string + published: + oneOf: + - $ref: '#/components/schemas/BoolFilter' + - type: boolean + viewCount: + oneOf: + - $ref: '#/components/schemas/IntFilter' + - type: integer + PostOrderByWithRelationInput: + type: object + properties: + id: + $ref: '#/components/schemas/SortOrder' + createdAt: + $ref: '#/components/schemas/SortOrder' + updatedAt: + $ref: '#/components/schemas/SortOrder' + title: + $ref: '#/components/schemas/SortOrder' + author: + $ref: '#/components/schemas/UserOrderByWithRelationInput' + authorId: + $ref: '#/components/schemas/SortOrder' + published: + $ref: '#/components/schemas/SortOrder' + viewCount: + $ref: '#/components/schemas/SortOrder' + PostWhereUniqueInput: + type: object + properties: + id: + type: string + PostScalarWhereWithAggregatesInput: + type: object + properties: + AND: + oneOf: + - $ref: '#/components/schemas/PostScalarWhereWithAggregatesInput' + - type: array + items: + $ref: '#/components/schemas/PostScalarWhereWithAggregatesInput' + OR: + type: array + items: + $ref: '#/components/schemas/PostScalarWhereWithAggregatesInput' + NOT: + oneOf: + - $ref: '#/components/schemas/PostScalarWhereWithAggregatesInput' + - type: array + items: + $ref: '#/components/schemas/PostScalarWhereWithAggregatesInput' + id: + oneOf: + - $ref: '#/components/schemas/StringWithAggregatesFilter' + - type: string + createdAt: + oneOf: + - $ref: '#/components/schemas/DateTimeWithAggregatesFilter' + - type: string + format: date-time + updatedAt: + oneOf: + - $ref: '#/components/schemas/DateTimeWithAggregatesFilter' + - type: string + format: date-time + title: + oneOf: + - $ref: '#/components/schemas/StringWithAggregatesFilter' + - type: string + authorId: + oneOf: + - $ref: '#/components/schemas/StringNullableWithAggregatesFilter' + - type: string + published: + oneOf: + - $ref: '#/components/schemas/BoolWithAggregatesFilter' + - type: boolean + viewCount: + oneOf: + - $ref: '#/components/schemas/IntWithAggregatesFilter' + - type: integer + UserCreateInput: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + email: + type: string + role: + $ref: '#/components/schemas/Role' + posts: + $ref: '#/components/schemas/PostCreateNestedManyWithoutAuthorInput' + required: + - id + - email + UserUpdateInput: + type: object + properties: + id: + oneOf: + - type: string + - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' + createdAt: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' + updatedAt: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' + email: + oneOf: + - type: string + - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' + role: + oneOf: + - $ref: '#/components/schemas/Role' + - $ref: '#/components/schemas/EnumRoleFieldUpdateOperationsInput' + posts: + $ref: '#/components/schemas/PostUpdateManyWithoutAuthorNestedInput' + UserCreateManyInput: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + email: + type: string + role: + $ref: '#/components/schemas/Role' + required: + - id + - email + UserUpdateManyMutationInput: + type: object + properties: + id: + oneOf: + - type: string + - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' + createdAt: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' + updatedAt: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' + email: + oneOf: + - type: string + - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' + role: + oneOf: + - $ref: '#/components/schemas/Role' + - $ref: '#/components/schemas/EnumRoleFieldUpdateOperationsInput' + PostCreateInput: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + title: + type: string + author: + $ref: '#/components/schemas/UserCreateNestedOneWithoutPostsInput' + published: + type: boolean + viewCount: + type: integer + required: + - id + - title + PostUpdateInput: + type: object + properties: + id: + oneOf: + - type: string + - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' + createdAt: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' + updatedAt: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' + title: + oneOf: + - type: string + - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' + author: + $ref: '#/components/schemas/UserUpdateOneWithoutPostsNestedInput' + published: + oneOf: + - type: boolean + - $ref: '#/components/schemas/BoolFieldUpdateOperationsInput' + viewCount: + oneOf: + - type: integer + - $ref: '#/components/schemas/IntFieldUpdateOperationsInput' + PostCreateManyInput: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + title: + type: string + authorId: + type: string + published: + type: boolean + viewCount: + type: integer + required: + - id + - title + PostUpdateManyMutationInput: + type: object + properties: + id: + oneOf: + - type: string + - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' + createdAt: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' + updatedAt: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' + title: + oneOf: + - type: string + - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' + published: + oneOf: + - type: boolean + - $ref: '#/components/schemas/BoolFieldUpdateOperationsInput' + viewCount: + oneOf: + - type: integer + - $ref: '#/components/schemas/IntFieldUpdateOperationsInput' + StringFilter: + type: object + properties: + equals: + type: string + in: + type: array + items: + type: string + notIn: + type: array + items: + type: string + lt: + type: string + lte: + type: string + gt: + type: string + gte: + type: string + contains: + type: string + startsWith: + type: string + endsWith: + type: string + mode: + $ref: '#/components/schemas/QueryMode' + not: + oneOf: + - type: string + - $ref: '#/components/schemas/NestedStringFilter' + DateTimeFilter: + type: object + properties: + equals: + type: string + format: date-time + in: + type: array + items: + type: string + format: date-time + notIn: + type: array + items: + type: string + format: date-time + lt: + type: string + format: date-time + lte: + type: string + format: date-time + gt: + type: string + format: date-time + gte: + type: string + format: date-time + not: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/NestedDateTimeFilter' + EnumRoleFilter: + type: object + properties: + equals: + $ref: '#/components/schemas/Role' + in: + type: array + items: + $ref: '#/components/schemas/Role' + notIn: + type: array + items: + $ref: '#/components/schemas/Role' + not: + oneOf: + - $ref: '#/components/schemas/Role' + - $ref: '#/components/schemas/NestedEnumRoleFilter' + PostListRelationFilter: + type: object + properties: + every: + $ref: '#/components/schemas/PostWhereInput' + some: + $ref: '#/components/schemas/PostWhereInput' + none: + $ref: '#/components/schemas/PostWhereInput' + BoolFilter: + type: object + properties: + equals: + type: boolean + not: + oneOf: + - type: boolean + - $ref: '#/components/schemas/NestedBoolFilter' + PostOrderByRelationAggregateInput: + type: object + properties: + _count: + $ref: '#/components/schemas/SortOrder' + StringWithAggregatesFilter: + type: object + properties: + equals: + type: string + in: + type: array + items: + type: string + notIn: + type: array + items: + type: string + lt: + type: string + lte: + type: string + gt: + type: string + gte: + type: string + contains: + type: string + startsWith: + type: string + endsWith: + type: string + mode: + $ref: '#/components/schemas/QueryMode' + not: + oneOf: + - type: string + - $ref: '#/components/schemas/NestedStringWithAggregatesFilter' + _count: + $ref: '#/components/schemas/NestedIntFilter' + _min: + $ref: '#/components/schemas/NestedStringFilter' + _max: + $ref: '#/components/schemas/NestedStringFilter' + DateTimeWithAggregatesFilter: + type: object + properties: + equals: + type: string + format: date-time + in: + type: array + items: + type: string + format: date-time + notIn: + type: array + items: + type: string + format: date-time + lt: + type: string + format: date-time + lte: + type: string + format: date-time + gt: + type: string + format: date-time + gte: + type: string + format: date-time + not: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/NestedDateTimeWithAggregatesFilter' + _count: + $ref: '#/components/schemas/NestedIntFilter' + _min: + $ref: '#/components/schemas/NestedDateTimeFilter' + _max: + $ref: '#/components/schemas/NestedDateTimeFilter' + EnumRoleWithAggregatesFilter: + type: object + properties: + equals: + $ref: '#/components/schemas/Role' + in: + type: array + items: + $ref: '#/components/schemas/Role' + notIn: + type: array + items: + $ref: '#/components/schemas/Role' + not: + oneOf: + - $ref: '#/components/schemas/Role' + - $ref: '#/components/schemas/NestedEnumRoleWithAggregatesFilter' + _count: + $ref: '#/components/schemas/NestedIntFilter' + _min: + $ref: '#/components/schemas/NestedEnumRoleFilter' + _max: + $ref: '#/components/schemas/NestedEnumRoleFilter' + BoolWithAggregatesFilter: + type: object + properties: + equals: + type: boolean + not: + oneOf: + - type: boolean + - $ref: '#/components/schemas/NestedBoolWithAggregatesFilter' + _count: + $ref: '#/components/schemas/NestedIntFilter' + _min: + $ref: '#/components/schemas/NestedBoolFilter' + _max: + $ref: '#/components/schemas/NestedBoolFilter' + UserRelationFilter: + type: object + properties: + is: + $ref: '#/components/schemas/UserWhereInput' + isNot: + $ref: '#/components/schemas/UserWhereInput' + StringNullableFilter: + type: object + properties: + equals: + type: string + in: + type: array + items: + type: string + notIn: + type: array + items: + type: string + lt: + type: string + lte: + type: string + gt: + type: string + gte: + type: string + contains: + type: string + startsWith: + type: string + endsWith: + type: string + mode: + $ref: '#/components/schemas/QueryMode' + not: + oneOf: + - type: string + - $ref: '#/components/schemas/NestedStringNullableFilter' + IntFilter: + type: object + properties: + equals: + type: integer + in: + type: array + items: + type: integer + notIn: + type: array + items: + type: integer + lt: + type: integer + lte: + type: integer + gt: + type: integer + gte: + type: integer + not: + oneOf: + - type: integer + - $ref: '#/components/schemas/NestedIntFilter' + StringNullableWithAggregatesFilter: + type: object + properties: + equals: + type: string + in: + type: array + items: + type: string + notIn: + type: array + items: + type: string + lt: + type: string + lte: + type: string + gt: + type: string + gte: + type: string + contains: + type: string + startsWith: + type: string + endsWith: + type: string + mode: + $ref: '#/components/schemas/QueryMode' + not: + oneOf: + - type: string + - $ref: '#/components/schemas/NestedStringNullableWithAggregatesFilter' + _count: + $ref: '#/components/schemas/NestedIntNullableFilter' + _min: + $ref: '#/components/schemas/NestedStringNullableFilter' + _max: + $ref: '#/components/schemas/NestedStringNullableFilter' + IntWithAggregatesFilter: + type: object + properties: + equals: + type: integer + in: + type: array + items: + type: integer + notIn: + type: array + items: + type: integer + lt: + type: integer + lte: + type: integer + gt: + type: integer + gte: + type: integer + not: + oneOf: + - type: integer + - $ref: '#/components/schemas/NestedIntWithAggregatesFilter' + _count: + $ref: '#/components/schemas/NestedIntFilter' + _avg: + $ref: '#/components/schemas/NestedFloatFilter' + _sum: + $ref: '#/components/schemas/NestedIntFilter' + _min: + $ref: '#/components/schemas/NestedIntFilter' + _max: + $ref: '#/components/schemas/NestedIntFilter' + PostCreateNestedManyWithoutAuthorInput: + type: object + properties: + create: + oneOf: + - $ref: '#/components/schemas/PostCreateWithoutAuthorInput' + - type: array + items: + $ref: '#/components/schemas/PostCreateWithoutAuthorInput' + - $ref: '#/components/schemas/PostUncheckedCreateWithoutAuthorInput' + - type: array + items: + $ref: '#/components/schemas/PostUncheckedCreateWithoutAuthorInput' + connectOrCreate: + oneOf: + - $ref: '#/components/schemas/PostCreateOrConnectWithoutAuthorInput' + - type: array + items: + $ref: '#/components/schemas/PostCreateOrConnectWithoutAuthorInput' + createMany: + $ref: '#/components/schemas/PostCreateManyAuthorInputEnvelope' + connect: + oneOf: + - $ref: '#/components/schemas/PostWhereUniqueInput' + - type: array + items: + $ref: '#/components/schemas/PostWhereUniqueInput' + StringFieldUpdateOperationsInput: + type: object + properties: + set: + type: string + DateTimeFieldUpdateOperationsInput: + type: object + properties: + set: + type: string + format: date-time + EnumRoleFieldUpdateOperationsInput: + type: object + properties: + set: + $ref: '#/components/schemas/Role' + PostUpdateManyWithoutAuthorNestedInput: + type: object + properties: + create: + oneOf: + - $ref: '#/components/schemas/PostCreateWithoutAuthorInput' + - type: array + items: + $ref: '#/components/schemas/PostCreateWithoutAuthorInput' + - $ref: '#/components/schemas/PostUncheckedCreateWithoutAuthorInput' + - type: array + items: + $ref: '#/components/schemas/PostUncheckedCreateWithoutAuthorInput' + connectOrCreate: + oneOf: + - $ref: '#/components/schemas/PostCreateOrConnectWithoutAuthorInput' + - type: array + items: + $ref: '#/components/schemas/PostCreateOrConnectWithoutAuthorInput' + upsert: + oneOf: + - $ref: '#/components/schemas/PostUpsertWithWhereUniqueWithoutAuthorInput' + - type: array + items: + $ref: '#/components/schemas/PostUpsertWithWhereUniqueWithoutAuthorInput' + createMany: + $ref: '#/components/schemas/PostCreateManyAuthorInputEnvelope' + set: + oneOf: + - $ref: '#/components/schemas/PostWhereUniqueInput' + - type: array + items: + $ref: '#/components/schemas/PostWhereUniqueInput' + disconnect: + oneOf: + - $ref: '#/components/schemas/PostWhereUniqueInput' + - type: array + items: + $ref: '#/components/schemas/PostWhereUniqueInput' + delete: + oneOf: + - $ref: '#/components/schemas/PostWhereUniqueInput' + - type: array + items: + $ref: '#/components/schemas/PostWhereUniqueInput' + connect: + oneOf: + - $ref: '#/components/schemas/PostWhereUniqueInput' + - type: array + items: + $ref: '#/components/schemas/PostWhereUniqueInput' + update: + oneOf: + - $ref: '#/components/schemas/PostUpdateWithWhereUniqueWithoutAuthorInput' + - type: array + items: + $ref: '#/components/schemas/PostUpdateWithWhereUniqueWithoutAuthorInput' + updateMany: + oneOf: + - $ref: '#/components/schemas/PostUpdateManyWithWhereWithoutAuthorInput' + - type: array + items: + $ref: '#/components/schemas/PostUpdateManyWithWhereWithoutAuthorInput' + deleteMany: + oneOf: + - $ref: '#/components/schemas/PostScalarWhereInput' + - type: array + items: + $ref: '#/components/schemas/PostScalarWhereInput' + BoolFieldUpdateOperationsInput: + type: object + properties: + set: + type: boolean + UserCreateNestedOneWithoutPostsInput: + type: object + properties: + create: + oneOf: + - $ref: '#/components/schemas/UserCreateWithoutPostsInput' + - $ref: '#/components/schemas/UserUncheckedCreateWithoutPostsInput' + connectOrCreate: + $ref: '#/components/schemas/UserCreateOrConnectWithoutPostsInput' + connect: + $ref: '#/components/schemas/UserWhereUniqueInput' + UserUpdateOneWithoutPostsNestedInput: + type: object + properties: + create: + oneOf: + - $ref: '#/components/schemas/UserCreateWithoutPostsInput' + - $ref: '#/components/schemas/UserUncheckedCreateWithoutPostsInput' + connectOrCreate: + $ref: '#/components/schemas/UserCreateOrConnectWithoutPostsInput' + upsert: + $ref: '#/components/schemas/UserUpsertWithoutPostsInput' + disconnect: + type: boolean + delete: + type: boolean + connect: + $ref: '#/components/schemas/UserWhereUniqueInput' + update: + oneOf: + - $ref: '#/components/schemas/UserUpdateWithoutPostsInput' + - $ref: '#/components/schemas/UserUncheckedUpdateWithoutPostsInput' + IntFieldUpdateOperationsInput: + type: object + properties: + set: + type: integer + increment: + type: integer + decrement: + type: integer + multiply: + type: integer + divide: + type: integer + NestedStringFilter: + type: object + properties: + equals: + type: string + in: + type: array + items: + type: string + notIn: + type: array + items: + type: string + lt: + type: string + lte: + type: string + gt: + type: string + gte: + type: string + contains: + type: string + startsWith: + type: string + endsWith: + type: string + not: + oneOf: + - type: string + - $ref: '#/components/schemas/NestedStringFilter' + NestedDateTimeFilter: + type: object + properties: + equals: + type: string + format: date-time + in: + type: array + items: + type: string + format: date-time + notIn: + type: array + items: + type: string + format: date-time + lt: + type: string + format: date-time + lte: + type: string + format: date-time + gt: + type: string + format: date-time + gte: + type: string + format: date-time + not: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/NestedDateTimeFilter' + NestedEnumRoleFilter: + type: object + properties: + equals: + $ref: '#/components/schemas/Role' + in: + type: array + items: + $ref: '#/components/schemas/Role' + notIn: + type: array + items: + $ref: '#/components/schemas/Role' + not: + oneOf: + - $ref: '#/components/schemas/Role' + - $ref: '#/components/schemas/NestedEnumRoleFilter' + NestedBoolFilter: + type: object + properties: + equals: + type: boolean + not: + oneOf: + - type: boolean + - $ref: '#/components/schemas/NestedBoolFilter' + NestedStringWithAggregatesFilter: + type: object + properties: + equals: + type: string + in: + type: array + items: + type: string + notIn: + type: array + items: + type: string + lt: + type: string + lte: + type: string + gt: + type: string + gte: + type: string + contains: + type: string + startsWith: + type: string + endsWith: + type: string + not: + oneOf: + - type: string + - $ref: '#/components/schemas/NestedStringWithAggregatesFilter' + _count: + $ref: '#/components/schemas/NestedIntFilter' + _min: + $ref: '#/components/schemas/NestedStringFilter' + _max: + $ref: '#/components/schemas/NestedStringFilter' + NestedIntFilter: + type: object + properties: + equals: + type: integer + in: + type: array + items: + type: integer + notIn: + type: array + items: + type: integer + lt: + type: integer + lte: + type: integer + gt: + type: integer + gte: + type: integer + not: + oneOf: + - type: integer + - $ref: '#/components/schemas/NestedIntFilter' + NestedDateTimeWithAggregatesFilter: + type: object + properties: + equals: + type: string + format: date-time + in: + type: array + items: + type: string + format: date-time + notIn: + type: array + items: + type: string + format: date-time + lt: + type: string + format: date-time + lte: + type: string + format: date-time + gt: + type: string + format: date-time + gte: + type: string + format: date-time + not: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/NestedDateTimeWithAggregatesFilter' + _count: + $ref: '#/components/schemas/NestedIntFilter' + _min: + $ref: '#/components/schemas/NestedDateTimeFilter' + _max: + $ref: '#/components/schemas/NestedDateTimeFilter' + NestedEnumRoleWithAggregatesFilter: + type: object + properties: + equals: + $ref: '#/components/schemas/Role' + in: + type: array + items: + $ref: '#/components/schemas/Role' + notIn: + type: array + items: + $ref: '#/components/schemas/Role' + not: + oneOf: + - $ref: '#/components/schemas/Role' + - $ref: '#/components/schemas/NestedEnumRoleWithAggregatesFilter' + _count: + $ref: '#/components/schemas/NestedIntFilter' + _min: + $ref: '#/components/schemas/NestedEnumRoleFilter' + _max: + $ref: '#/components/schemas/NestedEnumRoleFilter' + NestedBoolWithAggregatesFilter: + type: object + properties: + equals: + type: boolean + not: + oneOf: + - type: boolean + - $ref: '#/components/schemas/NestedBoolWithAggregatesFilter' + _count: + $ref: '#/components/schemas/NestedIntFilter' + _min: + $ref: '#/components/schemas/NestedBoolFilter' + _max: + $ref: '#/components/schemas/NestedBoolFilter' + NestedStringNullableFilter: + type: object + properties: + equals: + type: string + in: + type: array + items: + type: string + notIn: + type: array + items: + type: string + lt: + type: string + lte: + type: string + gt: + type: string + gte: + type: string + contains: + type: string + startsWith: + type: string + endsWith: + type: string + not: + oneOf: + - type: string + - $ref: '#/components/schemas/NestedStringNullableFilter' + NestedStringNullableWithAggregatesFilter: + type: object + properties: + equals: + type: string + in: + type: array + items: + type: string + notIn: + type: array + items: + type: string + lt: + type: string + lte: + type: string + gt: + type: string + gte: + type: string + contains: + type: string + startsWith: + type: string + endsWith: + type: string + not: + oneOf: + - type: string + - $ref: '#/components/schemas/NestedStringNullableWithAggregatesFilter' + _count: + $ref: '#/components/schemas/NestedIntNullableFilter' + _min: + $ref: '#/components/schemas/NestedStringNullableFilter' + _max: + $ref: '#/components/schemas/NestedStringNullableFilter' + NestedIntNullableFilter: + type: object + properties: + equals: + type: integer + in: + type: array + items: + type: integer + notIn: + type: array + items: + type: integer + lt: + type: integer + lte: + type: integer + gt: + type: integer + gte: + type: integer + not: + oneOf: + - type: integer + - $ref: '#/components/schemas/NestedIntNullableFilter' + NestedIntWithAggregatesFilter: + type: object + properties: + equals: + type: integer + in: + type: array + items: + type: integer + notIn: + type: array + items: + type: integer + lt: + type: integer + lte: + type: integer + gt: + type: integer + gte: + type: integer + not: + oneOf: + - type: integer + - $ref: '#/components/schemas/NestedIntWithAggregatesFilter' + _count: + $ref: '#/components/schemas/NestedIntFilter' + _avg: + $ref: '#/components/schemas/NestedFloatFilter' + _sum: + $ref: '#/components/schemas/NestedIntFilter' + _min: + $ref: '#/components/schemas/NestedIntFilter' + _max: + $ref: '#/components/schemas/NestedIntFilter' + NestedFloatFilter: + type: object + properties: + equals: + type: number + in: + type: array + items: + type: number + notIn: + type: array + items: + type: number + lt: + type: number + lte: + type: number + gt: + type: number + gte: + type: number + not: + oneOf: + - type: number + - $ref: '#/components/schemas/NestedFloatFilter' + PostCreateWithoutAuthorInput: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + title: + type: string + published: + type: boolean + viewCount: + type: integer + required: + - id + - title + PostUncheckedCreateWithoutAuthorInput: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + title: + type: string + published: + type: boolean + viewCount: + type: integer + required: + - id + - title + PostCreateOrConnectWithoutAuthorInput: + type: object + properties: + where: + $ref: '#/components/schemas/PostWhereUniqueInput' + create: + oneOf: + - $ref: '#/components/schemas/PostCreateWithoutAuthorInput' + - $ref: '#/components/schemas/PostUncheckedCreateWithoutAuthorInput' + required: + - where + - create + PostCreateManyAuthorInputEnvelope: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/PostCreateManyAuthorInput' + skipDuplicates: + type: boolean + required: + - data + PostUpsertWithWhereUniqueWithoutAuthorInput: + type: object + properties: + where: + $ref: '#/components/schemas/PostWhereUniqueInput' + update: + oneOf: + - $ref: '#/components/schemas/PostUpdateWithoutAuthorInput' + - $ref: '#/components/schemas/PostUncheckedUpdateWithoutAuthorInput' + create: + oneOf: + - $ref: '#/components/schemas/PostCreateWithoutAuthorInput' + - $ref: '#/components/schemas/PostUncheckedCreateWithoutAuthorInput' + required: + - where + - update + - create + PostUpdateWithWhereUniqueWithoutAuthorInput: + type: object + properties: + where: + $ref: '#/components/schemas/PostWhereUniqueInput' + data: + oneOf: + - $ref: '#/components/schemas/PostUpdateWithoutAuthorInput' + - $ref: '#/components/schemas/PostUncheckedUpdateWithoutAuthorInput' + required: + - where + - data + PostUpdateManyWithWhereWithoutAuthorInput: + type: object + properties: + where: + $ref: '#/components/schemas/PostScalarWhereInput' + data: + oneOf: + - $ref: '#/components/schemas/PostUpdateManyMutationInput' + - $ref: '#/components/schemas/PostUncheckedUpdateManyWithoutPostsInput' + required: + - where + - data + PostScalarWhereInput: + type: object + properties: + AND: + oneOf: + - $ref: '#/components/schemas/PostScalarWhereInput' + - type: array + items: + $ref: '#/components/schemas/PostScalarWhereInput' + OR: + type: array + items: + $ref: '#/components/schemas/PostScalarWhereInput' + NOT: + oneOf: + - $ref: '#/components/schemas/PostScalarWhereInput' + - type: array + items: + $ref: '#/components/schemas/PostScalarWhereInput' + id: + oneOf: + - $ref: '#/components/schemas/StringFilter' + - type: string + createdAt: + oneOf: + - $ref: '#/components/schemas/DateTimeFilter' + - type: string + format: date-time + updatedAt: + oneOf: + - $ref: '#/components/schemas/DateTimeFilter' + - type: string + format: date-time + title: + oneOf: + - $ref: '#/components/schemas/StringFilter' + - type: string + authorId: + oneOf: + - $ref: '#/components/schemas/StringNullableFilter' + - type: string + published: + oneOf: + - $ref: '#/components/schemas/BoolFilter' + - type: boolean + viewCount: + oneOf: + - $ref: '#/components/schemas/IntFilter' + - type: integer + UserCreateWithoutPostsInput: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + email: + type: string + role: + $ref: '#/components/schemas/Role' + required: + - id + - email + UserUncheckedCreateWithoutPostsInput: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + email: + type: string + role: + $ref: '#/components/schemas/Role' + required: + - id + - email + UserCreateOrConnectWithoutPostsInput: + type: object + properties: + where: + $ref: '#/components/schemas/UserWhereUniqueInput' + create: + oneOf: + - $ref: '#/components/schemas/UserCreateWithoutPostsInput' + - $ref: '#/components/schemas/UserUncheckedCreateWithoutPostsInput' + required: + - where + - create + UserUpsertWithoutPostsInput: + type: object + properties: + update: + oneOf: + - $ref: '#/components/schemas/UserUpdateWithoutPostsInput' + - $ref: '#/components/schemas/UserUncheckedUpdateWithoutPostsInput' + create: + oneOf: + - $ref: '#/components/schemas/UserCreateWithoutPostsInput' + - $ref: '#/components/schemas/UserUncheckedCreateWithoutPostsInput' + required: + - update + - create + UserUpdateWithoutPostsInput: + type: object + properties: + id: + oneOf: + - type: string + - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' + createdAt: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' + updatedAt: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' + email: + oneOf: + - type: string + - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' + role: + oneOf: + - $ref: '#/components/schemas/Role' + - $ref: '#/components/schemas/EnumRoleFieldUpdateOperationsInput' + UserUncheckedUpdateWithoutPostsInput: + type: object + properties: + id: + oneOf: + - type: string + - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' + createdAt: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' + updatedAt: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' + email: + oneOf: + - type: string + - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' + role: + oneOf: + - $ref: '#/components/schemas/Role' + - $ref: '#/components/schemas/EnumRoleFieldUpdateOperationsInput' + PostCreateManyAuthorInput: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + title: + type: string + published: + type: boolean + viewCount: + type: integer + required: + - id + - title + PostUpdateWithoutAuthorInput: + type: object + properties: + id: + oneOf: + - type: string + - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' + createdAt: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' + updatedAt: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' + title: + oneOf: + - type: string + - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' + published: + oneOf: + - type: boolean + - $ref: '#/components/schemas/BoolFieldUpdateOperationsInput' + viewCount: + oneOf: + - type: integer + - $ref: '#/components/schemas/IntFieldUpdateOperationsInput' + PostUncheckedUpdateWithoutAuthorInput: + type: object + properties: + id: + oneOf: + - type: string + - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' + createdAt: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' + updatedAt: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' + title: + oneOf: + - type: string + - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' + published: + oneOf: + - type: boolean + - $ref: '#/components/schemas/BoolFieldUpdateOperationsInput' + viewCount: + oneOf: + - type: integer + - $ref: '#/components/schemas/IntFieldUpdateOperationsInput' + PostUncheckedUpdateManyWithoutPostsInput: + type: object + properties: + id: + oneOf: + - type: string + - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' + createdAt: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' + updatedAt: + oneOf: + - type: string + format: date-time + - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' + title: + oneOf: + - type: string + - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' + published: + oneOf: + - type: boolean + - $ref: '#/components/schemas/BoolFieldUpdateOperationsInput' + viewCount: + oneOf: + - type: integer + - $ref: '#/components/schemas/IntFieldUpdateOperationsInput' + UserArgs: + type: object + properties: + select: + $ref: '#/components/schemas/UserSelect' + include: + $ref: '#/components/schemas/UserInclude' + UserInclude: + type: object + properties: + posts: + oneOf: + - type: boolean + - $ref: '#/components/schemas/PostFindManyArgs' + _count: + oneOf: + - type: boolean + - $ref: '#/components/schemas/UserCountOutputTypeArgs' + PostInclude: + type: object + properties: + author: + oneOf: + - type: boolean + - $ref: '#/components/schemas/UserArgs' + UserCountOutputTypeSelect: + type: object + properties: + posts: + type: boolean + UserCountOutputTypeArgs: + type: object + properties: + select: + $ref: '#/components/schemas/UserCountOutputTypeSelect' + UserSelect: + type: object + properties: + id: + type: boolean + createdAt: + type: boolean + updatedAt: + type: boolean + email: + type: boolean + role: + type: boolean + posts: + oneOf: + - type: boolean + - $ref: '#/components/schemas/PostFindManyArgs' + _count: + oneOf: + - type: boolean + - $ref: '#/components/schemas/UserCountOutputTypeArgs' + PostSelect: + type: object + properties: + id: + type: boolean + createdAt: + type: boolean + updatedAt: + type: boolean + title: + type: boolean + author: + oneOf: + - type: boolean + - $ref: '#/components/schemas/UserArgs' + authorId: + type: boolean + published: + type: boolean + viewCount: + type: boolean + UserCountAggregateInput: + type: object + properties: + id: + type: boolean + createdAt: + type: boolean + updatedAt: + type: boolean + email: + type: boolean + role: + type: boolean + _all: + type: boolean + UserMinAggregateInput: + type: object + properties: + id: + type: boolean + createdAt: + type: boolean + updatedAt: + type: boolean + email: + type: boolean + role: + type: boolean + UserMaxAggregateInput: + type: object + properties: + id: + type: boolean + createdAt: + type: boolean + updatedAt: + type: boolean + email: + type: boolean + role: + type: boolean + PostCountAggregateInput: + type: object + properties: + id: + type: boolean + createdAt: + type: boolean + updatedAt: + type: boolean + title: + type: boolean + authorId: + type: boolean + published: + type: boolean + viewCount: + type: boolean + _all: + type: boolean + PostAvgAggregateInput: + type: object + properties: + viewCount: + type: boolean + PostSumAggregateInput: + type: object + properties: + viewCount: + type: boolean + PostMinAggregateInput: + type: object + properties: + id: + type: boolean + createdAt: + type: boolean + updatedAt: + type: boolean + title: + type: boolean + authorId: + type: boolean + published: + type: boolean + viewCount: + type: boolean + PostMaxAggregateInput: + type: object + properties: + id: + type: boolean + createdAt: + type: boolean + updatedAt: + type: boolean + title: + type: boolean + authorId: + type: boolean + published: + type: boolean + viewCount: + type: boolean + AggregateUser: + type: object + properties: + _count: + $ref: '#/components/schemas/UserCountAggregateOutputType' + _min: + $ref: '#/components/schemas/UserMinAggregateOutputType' + _max: + $ref: '#/components/schemas/UserMaxAggregateOutputType' + UserGroupByOutputType: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + email: + type: string + role: + $ref: '#/components/schemas/Role' + _count: + $ref: '#/components/schemas/UserCountAggregateOutputType' + _min: + $ref: '#/components/schemas/UserMinAggregateOutputType' + _max: + $ref: '#/components/schemas/UserMaxAggregateOutputType' + required: + - id + - createdAt + - updatedAt + - email + - role + AggregatePost: + type: object + properties: + _count: + $ref: '#/components/schemas/PostCountAggregateOutputType' + _avg: + $ref: '#/components/schemas/PostAvgAggregateOutputType' + _sum: + $ref: '#/components/schemas/PostSumAggregateOutputType' + _min: + $ref: '#/components/schemas/PostMinAggregateOutputType' + _max: + $ref: '#/components/schemas/PostMaxAggregateOutputType' + PostGroupByOutputType: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + title: + type: string + authorId: + type: string + published: + type: boolean + viewCount: + type: integer + _count: + $ref: '#/components/schemas/PostCountAggregateOutputType' + _avg: + $ref: '#/components/schemas/PostAvgAggregateOutputType' + _sum: + $ref: '#/components/schemas/PostSumAggregateOutputType' + _min: + $ref: '#/components/schemas/PostMinAggregateOutputType' + _max: + $ref: '#/components/schemas/PostMaxAggregateOutputType' + required: + - id + - createdAt + - updatedAt + - title + - published + - viewCount + UserCountAggregateOutputType: + type: object + properties: + id: + type: integer + createdAt: + type: integer + updatedAt: + type: integer + email: + type: integer + role: + type: integer + _all: + type: integer + required: + - id + - createdAt + - updatedAt + - email + - role + - _all + UserMinAggregateOutputType: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + email: + type: string + role: + $ref: '#/components/schemas/Role' + UserMaxAggregateOutputType: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + email: + type: string + role: + $ref: '#/components/schemas/Role' + PostCountAggregateOutputType: + type: object + properties: + id: + type: integer + createdAt: + type: integer + updatedAt: + type: integer + title: + type: integer + authorId: + type: integer + published: + type: integer + viewCount: + type: integer + _all: + type: integer + required: + - id + - createdAt + - updatedAt + - title + - authorId + - published + - viewCount + - _all + PostAvgAggregateOutputType: + type: object + properties: + viewCount: + type: number + PostSumAggregateOutputType: + type: object + properties: + viewCount: + type: integer + PostMinAggregateOutputType: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + title: + type: string + authorId: + type: string + published: + type: boolean + viewCount: + type: integer + PostMaxAggregateOutputType: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + title: + type: string + authorId: + type: string + published: + type: boolean + viewCount: + type: integer + BatchPayload: + type: object + properties: + count: + type: integer + UserCreateArgs: + type: object + properties: + select: + $ref: '#/components/schemas/UserSelect' + include: + $ref: '#/components/schemas/UserInclude' + data: + $ref: '#/components/schemas/UserCreateInput' + UserCreateManyArgs: + type: object + properties: + data: + $ref: '#/components/schemas/UserCreateManyInput' + UserFindUniqueArgs: + type: object + properties: + select: + $ref: '#/components/schemas/UserSelect' + include: + $ref: '#/components/schemas/UserInclude' + where: + $ref: '#/components/schemas/UserWhereUniqueInput' + UserFindFirstArgs: + type: object + properties: + select: + $ref: '#/components/schemas/UserSelect' + include: + $ref: '#/components/schemas/UserInclude' + where: + $ref: '#/components/schemas/UserWhereInput' + UserFindManyArgs: + type: object + properties: + select: + $ref: '#/components/schemas/UserSelect' + include: + $ref: '#/components/schemas/UserInclude' + where: + $ref: '#/components/schemas/UserWhereInput' + UserUpdateArgs: + type: object + properties: + select: + $ref: '#/components/schemas/UserSelect' + include: + $ref: '#/components/schemas/UserInclude' + where: + $ref: '#/components/schemas/UserWhereUniqueInput' + data: + $ref: '#/components/schemas/UserUpdateInput' + UserUpdateManyArgs: + type: object + properties: + where: + $ref: '#/components/schemas/UserWhereInput' + data: + $ref: '#/components/schemas/UserUpdateManyMutationInput' + UserUpsertArgs: + type: object + properties: + select: + $ref: '#/components/schemas/UserSelect' + include: + $ref: '#/components/schemas/UserInclude' + where: + $ref: '#/components/schemas/UserWhereUniqueInput' + create: + $ref: '#/components/schemas/UserCreateInput' + update: + $ref: '#/components/schemas/UserUpdateInput' + UserDeleteUniqueArgs: + type: object + properties: + select: + $ref: '#/components/schemas/UserSelect' + include: + $ref: '#/components/schemas/UserInclude' + where: + $ref: '#/components/schemas/UserWhereUniqueInput' + UserDeleteManyArgs: + type: object + properties: + where: + $ref: '#/components/schemas/UserWhereInput' + UserCountArgs: + type: object + properties: + select: + $ref: '#/components/schemas/UserSelect' + where: + $ref: '#/components/schemas/UserWhereInput' + UserAggregateArgs: + type: object + properties: + where: + $ref: '#/components/schemas/UserWhereInput' + orderBy: + $ref: '#/components/schemas/UserOrderByWithRelationInput' + cursor: + $ref: '#/components/schemas/UserWhereUniqueInput' + take: + type: integer + skip: + type: integer + _count: + oneOf: + - type: boolean + - $ref: '#/components/schemas/UserCountAggregateInput' + _min: + $ref: '#/components/schemas/UserMinAggregateInput' + _max: + $ref: '#/components/schemas/UserMaxAggregateInput' + UserGroupByArgs: + type: object + properties: + where: + $ref: '#/components/schemas/UserWhereInput' + orderBy: + $ref: '#/components/schemas/UserOrderByWithRelationInput' + by: + $ref: '#/components/schemas/UserScalarFieldEnum' + having: + $ref: '#/components/schemas/UserScalarWhereWithAggregatesInput' + take: + type: integer + skip: + type: integer + _count: + oneOf: + - type: boolean + - $ref: '#/components/schemas/UserCountAggregateInput' + _min: + $ref: '#/components/schemas/UserMinAggregateInput' + _max: + $ref: '#/components/schemas/UserMaxAggregateInput' + PostCreateArgs: + type: object + properties: + select: + $ref: '#/components/schemas/PostSelect' + include: + $ref: '#/components/schemas/PostInclude' + data: + $ref: '#/components/schemas/PostCreateInput' + PostCreateManyArgs: + type: object + properties: + data: + $ref: '#/components/schemas/PostCreateManyInput' + PostFindUniqueArgs: + type: object + properties: + select: + $ref: '#/components/schemas/PostSelect' + include: + $ref: '#/components/schemas/PostInclude' + where: + $ref: '#/components/schemas/PostWhereUniqueInput' + PostFindFirstArgs: + type: object + properties: + select: + $ref: '#/components/schemas/PostSelect' + include: + $ref: '#/components/schemas/PostInclude' + where: + $ref: '#/components/schemas/PostWhereInput' + PostFindManyArgs: + type: object + properties: + select: + $ref: '#/components/schemas/PostSelect' + include: + $ref: '#/components/schemas/PostInclude' + where: + $ref: '#/components/schemas/PostWhereInput' + PostUpdateArgs: + type: object + properties: + select: + $ref: '#/components/schemas/PostSelect' + include: + $ref: '#/components/schemas/PostInclude' + where: + $ref: '#/components/schemas/PostWhereUniqueInput' + data: + $ref: '#/components/schemas/PostUpdateInput' + PostUpdateManyArgs: + type: object + properties: + where: + $ref: '#/components/schemas/PostWhereInput' + data: + $ref: '#/components/schemas/PostUpdateManyMutationInput' + PostUpsertArgs: + type: object + properties: + select: + $ref: '#/components/schemas/PostSelect' + include: + $ref: '#/components/schemas/PostInclude' + where: + $ref: '#/components/schemas/PostWhereUniqueInput' + create: + $ref: '#/components/schemas/PostCreateInput' + update: + $ref: '#/components/schemas/PostUpdateInput' + PostDeleteUniqueArgs: + type: object + properties: + select: + $ref: '#/components/schemas/PostSelect' + include: + $ref: '#/components/schemas/PostInclude' + where: + $ref: '#/components/schemas/PostWhereUniqueInput' + PostDeleteManyArgs: + type: object + properties: + where: + $ref: '#/components/schemas/PostWhereInput' + PostCountArgs: + type: object + properties: + select: + $ref: '#/components/schemas/PostSelect' + where: + $ref: '#/components/schemas/PostWhereInput' + PostAggregateArgs: + type: object + properties: + where: + $ref: '#/components/schemas/PostWhereInput' + orderBy: + $ref: '#/components/schemas/PostOrderByWithRelationInput' + cursor: + $ref: '#/components/schemas/PostWhereUniqueInput' + take: + type: integer + skip: + type: integer + _count: + oneOf: + - type: boolean + - $ref: '#/components/schemas/PostCountAggregateInput' + _min: + $ref: '#/components/schemas/PostMinAggregateInput' + _max: + $ref: '#/components/schemas/PostMaxAggregateInput' + _sum: + $ref: '#/components/schemas/PostSumAggregateInput' + _avg: + $ref: '#/components/schemas/PostAvgAggregateInput' + PostGroupByArgs: + type: object + properties: + where: + $ref: '#/components/schemas/PostWhereInput' + orderBy: + $ref: '#/components/schemas/PostOrderByWithRelationInput' + by: + $ref: '#/components/schemas/PostScalarFieldEnum' + having: + $ref: '#/components/schemas/PostScalarWhereWithAggregatesInput' + take: + type: integer + skip: + type: integer + _count: + oneOf: + - type: boolean + - $ref: '#/components/schemas/PostCountAggregateInput' + _min: + $ref: '#/components/schemas/PostMinAggregateInput' + _max: + $ref: '#/components/schemas/PostMaxAggregateInput' + _sum: + $ref: '#/components/schemas/PostSumAggregateInput' + _avg: + $ref: '#/components/schemas/PostAvgAggregateInput' +paths: + /user/create: + post: + operationId: createUser + description: Create a new User + tags: + - user + responses: + '201': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid request + '403': + description: Request is forbidden + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserCreateArgs' + /user/createMany: + post: + operationId: createManyUser + description: Create several User + tags: + - user + responses: + '201': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/BatchPayload' + '400': + description: Invalid request + '403': + description: Request is forbidden + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserCreateManyArgs' + /user/findUnique: + get: + operationId: findUniqueUser + description: Find one unique User + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid request + '403': + description: Request is forbidden + parameters: + - name: q + in: query + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserFindUniqueArgs' + /user/findFirst: + get: + operationId: findFirstUser + description: Find the first User matching the given condition + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid request + '403': + description: Request is forbidden + parameters: + - name: q + in: query + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserFindFirstArgs' + /user/findMany: + get: + operationId: findManyUser + description: Find users matching the given conditions + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + '400': + description: Invalid request + '403': + description: Request is forbidden + parameters: + - name: q + in: query + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserFindManyArgs' + /user/update: + patch: + operationId: updateUser + description: Update a User + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid request + '403': + description: Request is forbidden + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserUpdateArgs' + /user/updateMany: + patch: + operationId: updateManyUser + description: Update Users matching the given condition + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/BatchPayload' + '400': + description: Invalid request + '403': + description: Request is forbidden + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserUpdateManyArgs' + /user/upsert: + post: + operationId: upsertUser + description: Upsert a User + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid request + '403': + description: Request is forbidden + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserUpsertArgs' + /user/dodelete: + put: + operationId: deleteUser + description: Delete a unique user + tags: + - delete + - user + summary: Delete a user yeah yeah + deprecated: true + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid request + '403': + description: Request is forbidden + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserDeleteUniqueArgs' + /user/deleteMany: + delete: + operationId: deleteManyUser + description: Delete Users matching the given condition + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/BatchPayload' + '400': + description: Invalid request + '403': + description: Request is forbidden + parameters: + - name: q + in: query + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserDeleteManyArgs' + /user/count: + get: + operationId: countUser + description: Find a list of User + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + oneOf: + - type: integer + - $ref: '#/components/schemas/UserCountAggregateOutputType' + '400': + description: Invalid request + '403': + description: Request is forbidden + parameters: + - name: q + in: query + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserCountArgs' + /user/aggregate: + get: + operationId: aggregateUser + description: Aggregate Users + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/AggregateUser' + '400': + description: Invalid request + '403': + description: Request is forbidden + parameters: + - name: q + in: query + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserAggregateArgs' + /user/groupBy: + get: + operationId: groupByUser + description: Group Users by fields + tags: + - user + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UserGroupByOutputType' + '400': + description: Invalid request + '403': + description: Request is forbidden + parameters: + - name: q + in: query + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserGroupByArgs' + /post/create: + post: + operationId: createPost + description: Create a new Post + tags: + - post + responses: + '201': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Post' + '400': + description: Invalid request + '403': + description: Request is forbidden + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PostCreateArgs' + /post/createMany: + post: + operationId: createManyPost + description: Create several Post + tags: + - post + responses: + '201': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/BatchPayload' + '400': + description: Invalid request + '403': + description: Request is forbidden + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PostCreateManyArgs' + /post/findUnique: + get: + operationId: findUniquePost + description: Find one unique Post + tags: + - post + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Post' + '400': + description: Invalid request + '403': + description: Request is forbidden + parameters: + - name: q + in: query + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PostFindUniqueArgs' + /post/findFirst: + get: + operationId: findFirstPost + description: Find the first Post matching the given condition + tags: + - post + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Post' + '400': + description: Invalid request + '403': + description: Request is forbidden + parameters: + - name: q + in: query + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PostFindFirstArgs' + /post/update: + patch: + operationId: updatePost + description: Update a Post + tags: + - post + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Post' + '400': + description: Invalid request + '403': + description: Request is forbidden + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PostUpdateArgs' + /post/updateMany: + patch: + operationId: updateManyPost + description: Update Posts matching the given condition + tags: + - post + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/BatchPayload' + '400': + description: Invalid request + '403': + description: Request is forbidden + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PostUpdateManyArgs' + /post/upsert: + post: + operationId: upsertPost + description: Upsert a Post + tags: + - post + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Post' + '400': + description: Invalid request + '403': + description: Request is forbidden + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PostUpsertArgs' + /post/delete: + delete: + operationId: deletePost + description: Delete one unique Post + tags: + - post + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Post' + '400': + description: Invalid request + '403': + description: Request is forbidden + parameters: + - name: q + in: query + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PostDeleteUniqueArgs' + /post/deleteMany: + delete: + operationId: deleteManyPost + description: Delete Posts matching the given condition + tags: + - post + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/BatchPayload' + '400': + description: Invalid request + '403': + description: Request is forbidden + parameters: + - name: q + in: query + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PostDeleteManyArgs' + /post/count: + get: + operationId: countPost + description: Find a list of Post + tags: + - post + responses: + '200': + description: Successful operation + content: + application/json: + schema: + oneOf: + - type: integer + - $ref: '#/components/schemas/PostCountAggregateOutputType' + '400': + description: Invalid request + '403': + description: Request is forbidden + parameters: + - name: q + in: query + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PostCountArgs' + /post/aggregate: + get: + operationId: aggregatePost + description: Aggregate Posts + tags: + - post + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/AggregatePost' + '400': + description: Invalid request + '403': + description: Request is forbidden + parameters: + - name: q + in: query + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PostAggregateArgs' + /post/groupBy: + get: + operationId: groupByPost + description: Group Posts by fields + tags: + - post + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PostGroupByOutputType' + '400': + description: Invalid request + '403': + description: Request is forbidden + parameters: + - name: q + in: query + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PostGroupByArgs' diff --git a/packages/plugins/openapi/tests/openapi-restful.test.ts b/packages/plugins/openapi/tests/openapi-restful.test.ts new file mode 100644 index 000000000..4a6a24185 --- /dev/null +++ b/packages/plugins/openapi/tests/openapi-restful.test.ts @@ -0,0 +1,209 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/// + +import OpenAPIParser from '@readme/openapi-parser'; +import { getLiteral, getObjectLiteral } from '@zenstackhq/sdk'; +import { isPlugin, Model, Plugin } from '@zenstackhq/sdk/ast'; +import { loadZModelAndDmmf } from '@zenstackhq/testtools'; +import * as fs from 'fs'; +import * as tmp from 'tmp'; +import YAML from 'yaml'; +import generate from '../src'; + +describe('Open API Plugin Tests', () => { + it('run plugin', async () => { + const { model, dmmf, modelFile } = await loadZModelAndDmmf(` +plugin openapi { + provider = '${process.cwd()}/dist' +} + +enum Role { + USER + ADMIN +} + +model User { + id String @id + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique + role Role @default(USER) + posts Post[] +} + +model Post { + id String @id + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String + author User? @relation(fields: [authorId], references: [id]) + authorId String? + published Boolean @default(false) + viewCount Int @default(0) + + @@openapi.meta({ + tagDescription: 'Post-related operations' + }) +} + +model Foo { + id String @id + @@openapi.ignore +} + +model Bar { + id String @id + @@ignore +} + `); + + const { name: output } = tmp.fileSync({ postfix: '.yaml' }); + + const options = buildOptions(model, modelFile, output, '3.1.0'); + await generate(model, options, dmmf); + + console.log('OpenAPI specification generated:', output); + + const api = await OpenAPIParser.validate(output); + + expect(api.tags).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'user', description: 'User operations' }), + expect.objectContaining({ name: 'post', description: 'Post-related operations' }), + ]) + ); + + expect(api.paths?.['/user']?.['get']).toBeTruthy(); + expect(api.paths?.['/user']?.['post']).toBeTruthy(); + expect(api.paths?.['/user']?.['put']).toBeFalsy(); + expect(api.paths?.['/user/{id}']?.['get']).toBeTruthy(); + expect(api.paths?.['/user/{id}']?.['patch']).toBeTruthy(); + expect(api.paths?.['/user/{id}']?.['delete']).toBeTruthy(); + expect(api.paths?.['/user/{id}/posts']?.['get']).toBeTruthy(); + expect(api.paths?.['/user/{id}/relationships/posts']?.['get']).toBeTruthy(); + expect(api.paths?.['/user/{id}/relationships/posts']?.['post']).toBeTruthy(); + expect(api.paths?.['/user/{id}/relationships/posts']?.['patch']).toBeTruthy(); + expect(api.paths?.['/post/{id}/relationships/author']?.['get']).toBeTruthy(); + expect(api.paths?.['/post/{id}/relationships/author']?.['post']).toBeUndefined(); + expect(api.paths?.['/post/{id}/relationships/author']?.['patch']).toBeTruthy(); + expect(api.paths?.['/foo']).toBeUndefined(); + expect(api.paths?.['/bar']).toBeUndefined(); + + const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); + expect(parsed.openapi).toBe('3.1.0'); + const baseline = YAML.parse(fs.readFileSync(`${__dirname}/baseline/rest.baseline.yaml`, 'utf-8')); + expect(parsed).toMatchObject(baseline); + }); + + it('options', async () => { + const { model, dmmf, modelFile } = await loadZModelAndDmmf(` +plugin openapi { + provider = '${process.cwd()}/dist' + specVersion = '3.0.0' + title = 'My Awesome API' + version = '1.0.0' + description = 'awesome api' + prefix = '/myapi' +} + +model User { + id String @id +} + `); + + const { name: output } = tmp.fileSync({ postfix: '.yaml' }); + const options = buildOptions(model, modelFile, output); + await generate(model, options, dmmf); + + console.log('OpenAPI specification generated:', output); + + const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); + expect(parsed.openapi).toBe('3.0.0'); + + const api = await OpenAPIParser.validate(output); + expect(api.info).toEqual( + expect.objectContaining({ + title: 'My Awesome API', + version: '1.0.0', + description: 'awesome api', + }) + ); + + expect(api.paths?.['/myapi/user']).toBeTruthy(); + }); + + it('security schemes valid', async () => { + const { model, dmmf, modelFile } = await loadZModelAndDmmf(` +plugin openapi { + provider = '${process.cwd()}/dist' + securitySchemes = { + myBasic: { type: 'http', scheme: 'basic' }, + myBearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, + myApiKey: { type: 'apiKey', in: 'header', name: 'X-API-KEY' } + } +} + +model User { + id String @id + posts Post[] +} + +model Post { + id String @id + author User @relation(fields: [authorId], references: [id]) + authorId String + @@allow('read', true) +} +`); + + const { name: output } = tmp.fileSync({ postfix: '.yaml' }); + const options = buildOptions(model, modelFile, output); + await generate(model, options, dmmf); + + console.log('OpenAPI specification generated:', output); + + const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); + expect(parsed.components.securitySchemes).toEqual( + expect.objectContaining({ + myBasic: { type: 'http', scheme: 'basic' }, + myBearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, + myApiKey: { type: 'apiKey', in: 'header', name: 'X-API-KEY' }, + }) + ); + expect(parsed.security).toEqual(expect.arrayContaining([{ myBasic: [] }, { myBearer: [] }])); + + const api = await OpenAPIParser.validate(output); + expect(api.paths?.['/user']?.['get']?.security).toBeUndefined(); + expect(api.paths?.['/user/{id}/posts']?.['get']?.security).toEqual([]); + expect(api.paths?.['/post']?.['get']?.security).toEqual([]); + expect(api.paths?.['/post']?.['post']?.security).toBeUndefined(); + }); + + it('security schemes invalid', async () => { + const { model, dmmf, modelFile } = await loadZModelAndDmmf(` +plugin openapi { + provider = '${process.cwd()}/dist' + securitySchemes = { + myBasic: { type: 'invalid', scheme: 'basic' } + } +} + +model User { + id String @id +} + `); + + const { name: output } = tmp.fileSync({ postfix: '.yaml' }); + const options = buildOptions(model, modelFile, output); + await expect(generate(model, options, dmmf)).rejects.toEqual( + expect.objectContaining({ message: expect.stringContaining('"securitySchemes" option is invalid') }) + ); + }); +}); + +function buildOptions(model: Model, modelFile: string, output: string, specVersion = '3.0.0') { + const optionFields = model.declarations.find((d): d is Plugin => isPlugin(d))?.fields || []; + const options: any = { schemaPath: modelFile, output, specVersion, flavor: 'restful' }; + optionFields.forEach((f) => (options[f.name] = getLiteral(f.value) ?? getObjectLiteral(f.value))); + return options; +} diff --git a/packages/plugins/openapi/tests/openapi.test.ts b/packages/plugins/openapi/tests/openapi-rpc.test.ts similarity index 97% rename from packages/plugins/openapi/tests/openapi.test.ts rename to packages/plugins/openapi/tests/openapi-rpc.test.ts index a0fb7d678..6215c258b 100644 --- a/packages/plugins/openapi/tests/openapi.test.ts +++ b/packages/plugins/openapi/tests/openapi-rpc.test.ts @@ -83,6 +83,8 @@ model Bar { const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); expect(parsed.openapi).toBe('3.1.0'); + const baseline = YAML.parse(fs.readFileSync(`${__dirname}/baseline/rpc.baseline.yaml`, 'utf-8')); + expect(parsed).toMatchObject(baseline); const api = await OpenAPIParser.validate(output); @@ -310,7 +312,7 @@ model User { function buildOptions(model: Model, modelFile: string, output: string) { const optionFields = model.declarations.find((d): d is Plugin => isPlugin(d))?.fields || []; - const options: any = { schemaPath: modelFile, output }; + const options: any = { schemaPath: modelFile, output, flavor: 'rpc' }; optionFields.forEach((f) => (options[f.name] = getLiteral(f.value) ?? getObjectLiteral(f.value))); return options; } diff --git a/packages/schema/src/language-server/utils.ts b/packages/schema/src/language-server/utils.ts index 775858fa7..3a26d112b 100644 --- a/packages/schema/src/language-server/utils.ts +++ b/packages/schema/src/language-server/utils.ts @@ -1,5 +1,3 @@ -import { AstNode } from 'langium'; -import { STD_LIB_MODULE_NAME } from './constants'; import { DataModel, DataModelField, @@ -10,6 +8,8 @@ import { ReferenceExpr, } from '@zenstackhq/language/ast'; import { resolved } from '@zenstackhq/sdk'; +import { AstNode } from 'langium'; +import { STD_LIB_MODULE_NAME } from './constants'; /** * Gets the toplevel Model containing the given node. @@ -29,24 +29,6 @@ export function isFromStdlib(node: AstNode) { return !!model && !!model.$document && model.$document.uri.path.endsWith(STD_LIB_MODULE_NAME); } -/** - * Gets id fields declared at the data model level - */ -export function getIdFields(model: DataModel) { - const idAttr = model.attributes.find((attr) => attr.decl.ref?.name === '@@id'); - if (!idAttr) { - return []; - } - const fieldsArg = idAttr.args.find((a) => a.$resolvedParam?.name === 'fields'); - if (!fieldsArg || !isArrayExpr(fieldsArg.value)) { - return []; - } - - return fieldsArg.value.items - .filter((item): item is ReferenceExpr => isReferenceExpr(item)) - .map((item) => resolved(item.target) as DataModelField); -} - /** * Gets lists of unique fields declared at the data model level */ diff --git a/packages/schema/src/language-server/validator/datamodel-validator.ts b/packages/schema/src/language-server/validator/datamodel-validator.ts index e8da86d22..65804a31c 100644 --- a/packages/schema/src/language-server/validator/datamodel-validator.ts +++ b/packages/schema/src/language-server/validator/datamodel-validator.ts @@ -6,11 +6,11 @@ import { isLiteralExpr, ReferenceExpr, } from '@zenstackhq/language/ast'; -import { analyzePolicies, getLiteral } from '@zenstackhq/sdk'; +import { analyzePolicies, getIdFields, getLiteral } from '@zenstackhq/sdk'; import { AstNode, DiagnosticInfo, getDocument, ValidationAcceptor } from 'langium'; import { IssueCodes, SCALAR_TYPES } from '../constants'; import { AstValidator } from '../types'; -import { getIdFields, getUniqueFields } from '../utils'; +import { getUniqueFields } from '../utils'; import { validateAttributeApplication, validateDuplicatedDeclarations } from './utils'; /** diff --git a/packages/schema/src/plugins/model-meta/index.ts b/packages/schema/src/plugins/model-meta/index.ts index 86245eb4a..25c373173 100644 --- a/packages/schema/src/plugins/model-meta/index.ts +++ b/packages/schema/src/plugins/model-meta/index.ts @@ -15,6 +15,7 @@ import { getDataModels, getLiteral, hasAttribute, + isIdField, PluginOptions, resolved, saveProject, @@ -22,7 +23,6 @@ import { import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; import { CodeBlockWriter, VariableDeclarationKind } from 'ts-morph'; -import { getIdFields } from '../../language-server/utils'; import { ensureNodeModuleFolder, getDefaultOutputFolder } from '../plugin-utils'; export const name = 'Model Metadata'; @@ -158,21 +158,6 @@ function getFieldAttributes(field: DataModelField): RuntimeAttribute[] { .filter((d): d is RuntimeAttribute => !!d); } -function isIdField(field: DataModelField) { - // field-level @id attribute - if (field.attributes.some((attr) => attr.decl.ref?.name === '@id')) { - return true; - } - - // model-level @@id attribute with a list of fields - const model = field.$container as DataModel; - const modelLevelIds = getIdFields(model); - if (modelLevelIds.includes(field)) { - return true; - } - return false; -} - function getUniqueConstraints(model: DataModel) { const constraints: Array<{ name: string; fields: string[] }> = []; for (const attr of model.attributes.filter( diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index 741e0fc99..974148b4d 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -11,8 +11,10 @@ import { isDataModel, isLiteralExpr, isObjectExpr, + isReferenceExpr, Model, Reference, + ReferenceExpr, } from '@zenstackhq/language/ast'; /** @@ -122,3 +124,73 @@ export function getAttributeArgLiteral( } return undefined; } + +/** + * Gets id fields declared at the data model level + */ +export function getIdFields(model: DataModel) { + const idAttr = model.attributes.find((attr) => attr.decl.ref?.name === '@@id'); + if (!idAttr) { + return []; + } + const fieldsArg = idAttr.args.find((a) => a.$resolvedParam?.name === 'fields'); + if (!fieldsArg || !isArrayExpr(fieldsArg.value)) { + return []; + } + + return fieldsArg.value.items + .filter((item): item is ReferenceExpr => isReferenceExpr(item)) + .map((item) => resolved(item.target) as DataModelField); +} + +/** + * Returns if the given field is declared as an id field. + */ +export function isIdField(field: DataModelField) { + // field-level @id attribute + if (field.attributes.some((attr) => attr.decl.ref?.name === '@id')) { + return true; + } + + // model-level @@id attribute with a list of fields + const model = field.$container as DataModel; + const modelLevelIds = getIdFields(model); + if (modelLevelIds.includes(field)) { + return true; + } + return false; +} + +/** + * Returns if the given field is a relation field. + */ +export function isRelationshipField(field: DataModelField) { + return isDataModel(field.type.reference?.ref); +} + +/** + * Returns if the given field is a relation foreign key field. + */ +export function isForeignKeyField(field: DataModelField) { + const model = field.$container as DataModel; + return model.fields.some((f) => { + // find @relation attribute + const relAttr = f.attributes.find((attr) => attr.decl.ref?.name === '@relation'); + if (relAttr) { + // find "fields" arg + const fieldsArg = relAttr.args.find((a) => a.$resolvedParam?.name === 'fields'); + + if (fieldsArg && isArrayExpr(fieldsArg.value)) { + // find a matching field reference + return fieldsArg.value.items.some((item): item is ReferenceExpr => { + if (isReferenceExpr(item)) { + return item.target.ref === field; + } else { + return false; + } + }); + } + } + return false; + }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c94599e44..61e7f7c3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,6 +126,9 @@ importers: '@types/lower-case-first': specifier: ^1.0.1 version: 1.0.1 + '@types/pluralize': + specifier: ^0.0.29 + version: 0.0.29 '@types/tmp': specifier: ^0.2.3 version: 0.2.3 @@ -147,6 +150,9 @@ importers: jest: specifier: ^29.5.0 version: 29.5.0 + pluralize: + specifier: ^8.0.0 + version: 8.0.0 rimraf: specifier: ^3.0.2 version: 3.0.2 @@ -7832,7 +7838,6 @@ packages: /pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} - dev: false /postcss@8.4.14: resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==}