From d0107bf7740bfe437ba469b49aade6d10d7eedf1 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Thu, 11 May 2023 18:25:00 -0700 Subject: [PATCH 1/4] feat: openapi generator for restful style --- .../plugins/openapi/src/generator-base.ts | 37 + packages/plugins/openapi/src/index.ts | 10 +- .../plugins/openapi/src/rest-generator.ts | 789 ++++++++++++++++++ .../src/{generator.ts => rpc-generator.ts} | 39 +- .../openapi/tests/openapi-restful.test.ts | 95 +++ .../plugins/openapi/tests/openapi.test.ts | 2 +- 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 | 38 + 10 files changed, 978 insertions(+), 75 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} (96%) create mode 100644 packages/plugins/openapi/tests/openapi-restful.test.ts diff --git a/packages/plugins/openapi/src/generator-base.ts b/packages/plugins/openapi/src/generator-base.ts new file mode 100644 index 000000000..07c8b4dd4 --- /dev/null +++ b/packages/plugins/openapi/src/generator-base.ts @@ -0,0 +1,37 @@ +import { DMMF } from '@prisma/generator-helper'; +import { PluginOptions, getDataModels, hasAttribute } from '@zenstackhq/sdk'; +import { Model } from '@zenstackhq/sdk/ast'; +import type { OpenAPIV3_1 as OAPI } from 'openapi-types'; + +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 }; + } +} 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..7337dcb91 --- /dev/null +++ b/packages/plugins/openapi/src/rest-generator.ts @@ -0,0 +1,789 @@ +// Inspired by: https://github.com/omar-dulaimi/prisma-trpc-generator + +import { DMMF } from '@prisma/generator-helper'; +import { AUXILIARY_FIELDS, PluginError, getDataModels, isIdField } from '@zenstackhq/sdk'; +import { BuiltinType, DataModel, DataModelField, 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 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 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 + this.generateSecuritySchemes(components); + 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 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 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 = {}; + + 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), + post: this.makeResourceCreate(zmodel), + }; + + // GET /resource/{id} + // PATCH /resource/{id} + // DELETE /resource/{id} + result[`${prefix}/${lowerCaseFirst(model.name)}/{id}`] = { + get: this.makeResourceFetch(zmodel), + patch: this.makeResourceUpdate(zmodel), + delete: this.makeResourceDelete(zmodel), + }; + + // 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); + // PATCH /resource/{id}/relationships/field + container.patch = this.makeRelationshipUpdate(zmodel, field); + if (field.type.array) { + // POST /resource/{id}/relationships/field + container.post = this.makeRelationshipCreate(zmodel, field); + } + } + + return result; + } + + private makeResourceList(model: DataModel) { + 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(), + }, + }; + } + + private makeResourceCreate(model: DataModel) { + 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(), + }, + }; + } + + private makeResourceFetch(model: DataModel) { + return { + operationId: `fetch-${model.name}`, + description: `Fetch one ${model.name} resource`, + tags: [lowerCaseFirst(model.name)], + parameters: [this.parameter('id'), this.parameter('include'), ...this.generateFilterParameters(model)], + responses: { + '200': this.success(`${model.name}Response`), + '403': this.forbidden(), + }, + }; + } + + private makeRelatedFetch(model: DataModel, field: DataModelField, relationDecl: DataModel) { + return { + operationId: `fetch-${model.name}-related-${field.name}`, + description: `Fetch the related ${field.name} resource for ${model.name}`, + tags: [lowerCaseFirst(model.name)], + parameters: [this.parameter('id'), this.parameter('include'), ...this.generateFilterParameters(model)], + responses: { + '200': this.success( + field.type.array ? `${relationDecl.name}ListResponse` : `${relationDecl.name}Response` + ), + '403': this.forbidden(), + }, + }; + } + + private makeResourceUpdate(model: DataModel) { + return { + operationId: `update-${model.name}`, + description: `Update one ${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(), + }, + }; + } + + private makeResourceDelete(model: DataModel) { + return { + operationId: `delete-${model.name}`, + description: `Delete one ${model.name} resource`, + tags: [lowerCaseFirst(model.name)], + parameters: [this.parameter('id')], + responses: { + '200': this.success(), + '403': this.forbidden(), + }, + }; + } + + private makeRelationshipFetch(model: DataModel, field: DataModelField) { + 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${field.name} relationships for ${model.name}`, + tags: [lowerCaseFirst(model.name)], + parameters, + responses: { + '200': field.type.array + ? this.success('toManyRelationshipResponse') + : this.success('toOneRelationshipResponse'), + '403': this.forbidden(), + }, + }; + } + + private makeRelationshipCreate(model: DataModel, field: DataModelField) { + return { + operationId: `create-${model.name}-relationship-${field.name}`, + description: `Create new ${field.name} relationships for ${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(), + }, + }; + } + + private makeRelationshipUpdate(model: DataModel, field: DataModelField) { + return { + operationId: `update-${model.name}-relationship-${field.name}`, + description: `Update ${field.name} relationships for ${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(), + }, + }; + } + + private generateFilterParameters(zmodel: DataModel) { + const result: OAPI.ParameterObject[] = []; + + for (const field of zmodel.fields) { + if (isIdField(field)) { + result.push(this.makeParameter('filter[id]')); + continue; + } + switch (field.type.type) { + case 'Int': + case 'BigInt': + case 'Float': + case 'Decimal': + case 'DateTime': { + result.push(this.makeParameter(`filter[${field.name}$lt]`)); + result.push(this.makeParameter(`filter[${field.name}$lte]`)); + result.push(this.makeParameter(`filter[${field.name}$gt]`)); + result.push(this.makeParameter(`filter[${field.name}$gte]`)); + break; + } + case 'String': { + result.push(this.makeParameter(`filter[${field.name}$contains]`)); + result.push(this.makeParameter(`filter[${field.name}$icontains]`)); + result.push(this.makeParameter(`filter[${field.name}$search]`)); + result.push(this.makeParameter(`filter[${field.name}$startsWith]`)); + result.push(this.makeParameter(`filter[${field.name}$endsWith]`)); + break; + } + } + + if (field.type.array) { + result.push(this.makeParameter(`filter[${field.name}$has]`)); + result.push(this.makeParameter(`filter[${field.name}$hasEvery]`)); + result.push(this.makeParameter(`filter[${field.name}$hasSome]`)); + result.push(this.makeParameter(`filter[${field.name}$isEmpty]`)); + } + } + + return result; + } + + private makeParameter(name: string): OAPI.ParameterObject { + return { + name, + required: false, + in: 'query', + schema: { type: 'string' }, + }; + } + + 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 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', + additionalProperties: true, + }, + resourceIdentifier: { + type: 'object', + required: ['type', 'id'], + properties: { + type: { type: 'string' }, + id: { type: 'string' }, + }, + }, + resource: this.allOf(this.ref('resourceIdentifier'), { + type: 'object', + properties: { + attributes: { type: 'object' }, + relationships: { type: 'object' }, + }, + }), + links: { + type: 'object', + properties: { self: { type: 'string' } }, + }, + pagination: { + type: 'object', + properties: { + first: this.nullable({ type: 'string' }), + last: this.nullable({ type: 'string' }), + prev: this.nullable({ type: 'string' }), + next: this.nullable({ type: 'string' }), + }, + }, + errors: { + type: 'array', + items: { + type: 'object', + properties: { + status: { type: 'string' }, + code: { type: 'string' }, + title: { type: 'string' }, + detail: { type: 'string' }, + }, + }, + }, + errorResponse: { + type: 'object', + properties: { + jsonapi: this.ref('jsonapi'), + errors: this.ref('errors'), + }, + }, + relationLinks: { + type: 'object', + properties: { + self: { type: 'string' }, + related: { type: 'string' }, + }, + }, + toOneRelationship: { + type: 'object', + properties: { + data: this.ref('resourceIdentifier'), + }, + }, + toOneRelationshipWithLinks: { + type: 'object', + properties: { + links: this.ref('relationLinks'), + data: this.ref('resourceIdentifier'), + }, + }, + toManyRelationship: { + type: 'object', + properties: { + data: this.array(this.ref('resourceIdentifier')), + }, + }, + toManyRelationshipWithLinks: { + type: 'object', + properties: { + links: this.ref('pagedRelationLinks'), + data: this.array(this.ref('resourceIdentifier')), + }, + }, + pagedRelationLinks: this.allOf(this.ref('pagination'), this.ref('relationLinks')), + toManyRelationshipRequest: { + type: 'object', + properties: { + data: { + type: 'array', + items: this.ref('resourceIdentifier'), + }, + }, + }, + toOneRelationshipRequest: this.nullable({ + type: 'object', + properties: { + data: this.ref('resourceIdentifier'), + }, + }), + toManyRelationshipResponse: this.allOf(this.ref('toManyRelationshipWithLinks'), { + type: 'object', + properties: { + jsonapi: this.ref('jsonapi'), + }, + }), + toOneRelationshipResponse: this.nullable( + this.allOf(this.ref('toOneRelationshipWithLinks'), { + type: 'object', + properties: { + jsonapi: this.ref('jsonapi'), + }, + }) + ), + }; + } + + private generateParameters(): Record { + return { + id: { + name: 'id', + in: 'path', + required: true, + schema: { type: 'string' }, + }, + include: { + name: 'include', + in: 'query', + required: false, + style: 'form', + schema: { type: 'string' }, + }, + sort: { + name: 'sort', + in: 'query', + required: false, + style: 'form', + schema: { type: 'string' }, + }, + 'page-offset': { + name: 'page[offset]', + in: 'query', + required: false, + style: 'form', + schema: { type: 'integer' }, + }, + 'page-limit': { + name: 'page[limit]', + in: 'query', + required: false, + style: 'form', + schema: { type: 'integer' }, + }, + }; + } + + private generateEnumComponent(_enum: Enum): OAPI.SchemaObject { + const schema: OAPI.SchemaObject = { + type: 'string', + enum: _enum.fields.map((f) => f.name), + }; + return schema; + } + + private generateDataModelComponents(model: DataModel) { + const result: Record = {}; + result[`${model.name}`] = this.generateModelEntity(model, 'output'); + + result[`${model.name}CreateRequest`] = { + type: 'object', + required: ['data'], + properties: { + data: this.generateModelEntity(model, 'input'), + }, + }; + + result[`${model.name}UpdateRequest`] = { + type: 'object', + required: ['data'], + properties: { data: this.generateModelEntity(model, 'input') }, + }; + + const relationships: Record = {}; + for (const field of model.fields) { + if (this.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', + 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', + required: ['data'], + 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: 'input' | 'output'): 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 (this.isRelationshipField(field)) { + let relType: string; + if (mode === 'input') { + 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 ( + !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', + 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 isRelationshipField(field: DataModelField) { + return isDataModel(field.type.reference?.ref); + } + + private generateField(field: DataModelField) { + const resolvedDecl = field.type.reference?.ref; + if (resolvedDecl && isEnum(resolvedDecl)) { + return this.wrapArray(this.ref(resolvedDecl.name), field.type.array); + } + invariant(field?.type?.type); + return this.wrapArray(this.modelTypeToOpenAPIType(field.type.type), field.type.array); + } + + private get specVersion() { + return this.getOption('specVersion', '3.0.0'); + } + + private modelTypeToOpenAPIType(type: BuiltinType): OAPI.ReferenceObject | OAPI.SchemaObject { + switch (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 {}; + default: + return { $ref: `#/components/schemas/${type}` }; + } + } + + 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: 'Forbidden', + 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 96% rename from packages/plugins/openapi/src/generator.ts rename to packages/plugins/openapi/src/rpc-generator.ts index 8e3dd43f0..90d43ac78 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,27 @@ 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 +41,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); @@ -784,29 +774,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/openapi-restful.test.ts b/packages/plugins/openapi/tests/openapi-restful.test.ts new file mode 100644 index 000000000..cbe213575 --- /dev/null +++ b/packages/plugins/openapi/tests/openapi-restful.test.ts @@ -0,0 +1,95 @@ +/* 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[] + + @@openapi.meta({ + findMany: { + description: 'Find users matching the given conditions' + }, + delete: { + method: 'put', + path: 'dodelete', + description: 'Delete a unique user', + summary: 'Delete a user yeah yeah', + tags: ['delete', 'user'], + deprecated: true + }, + }) +} + +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', + findMany: { + ignore: true + } + }) +} + +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); + await generate(model, options, dmmf); + + console.log('OpenAPI specification generated:', output); + + YAML.parse(fs.readFileSync(output, 'utf-8')); + + await OpenAPIParser.validate(output); + }); +}); + +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.test.ts index a0fb7d678..7258a3ae9 100644 --- a/packages/plugins/openapi/tests/openapi.test.ts +++ b/packages/plugins/openapi/tests/openapi.test.ts @@ -310,7 +310,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..1911df34e 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,39 @@ 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; +} From 7aa70c7f8406e3243479b91bca7e950ea281f9e8 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 12 May 2023 00:03:50 -0700 Subject: [PATCH 2/4] fixes --- packages/plugins/openapi/package.json | 2 + .../plugins/openapi/src/generator-base.ts | 25 +- .../plugins/openapi/src/rest-generator.ts | 288 +- packages/plugins/openapi/src/rpc-generator.ts | 26 +- .../openapi/tests/baseline/rest.baseline.yaml | 1419 ++++++++ .../openapi/tests/baseline/rpc.baseline.yaml | 3087 +++++++++++++++++ .../openapi/tests/openapi-restful.test.ts | 154 +- .../{openapi.test.ts => openapi-rpc.test.ts} | 2 + pnpm-lock.yaml | 7 +- 9 files changed, 4849 insertions(+), 161 deletions(-) create mode 100644 packages/plugins/openapi/tests/baseline/rest.baseline.yaml create mode 100644 packages/plugins/openapi/tests/baseline/rpc.baseline.yaml rename packages/plugins/openapi/tests/{openapi.test.ts => openapi-rpc.test.ts} (98%) 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 index 07c8b4dd4..6d50a6a29 100644 --- a/packages/plugins/openapi/src/generator-base.ts +++ b/packages/plugins/openapi/src/generator-base.ts @@ -1,7 +1,9 @@ import { DMMF } from '@prisma/generator-helper'; -import { PluginOptions, getDataModels, hasAttribute } from '@zenstackhq/sdk'; +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) {} @@ -34,4 +36,25 @@ export abstract class OpenAPIGeneratorBase { 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/rest-generator.ts b/packages/plugins/openapi/src/rest-generator.ts index 7337dcb91..624364366 100644 --- a/packages/plugins/openapi/src/rest-generator.ts +++ b/packages/plugins/openapi/src/rest-generator.ts @@ -1,18 +1,19 @@ // Inspired by: https://github.com/omar-dulaimi/prisma-trpc-generator import { DMMF } from '@prisma/generator-helper'; -import { AUXILIARY_FIELDS, PluginError, getDataModels, isIdField } from '@zenstackhq/sdk'; +import { AUXILIARY_FIELDS, PluginError, analyzePolicies, getDataModels, isIdField } from '@zenstackhq/sdk'; import { BuiltinType, DataModel, DataModelField, 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 { fromZodError } from 'zod-validation-error'; import { OpenAPIGeneratorBase } from './generator-base'; import { getModelResourceMeta } from './meta'; -import { SecuritySchemesSchema } from './schema'; + +type Policies = ReturnType; /** * Generates RESTful style OpenAPI specification. @@ -30,7 +31,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { const paths = this.generatePaths(); // 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]: [] })); @@ -66,17 +67,6 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { 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 generatePaths(): OAPI.PathsObject { let result: OAPI.PathsObject = {}; @@ -103,6 +93,9 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { 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); @@ -111,17 +104,17 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { // GET /resource // POST /resource result[`${prefix}/${lowerCaseFirst(model.name)}`] = { - get: this.makeResourceList(zmodel), - post: this.makeResourceCreate(zmodel), + 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), - patch: this.makeResourceUpdate(zmodel), - delete: this.makeResourceDelete(zmodel), + get: this.makeResourceFetch(zmodel, policies), + patch: this.makeResourceUpdate(zmodel, policies), + delete: this.makeResourceDelete(zmodel, policies), }; // paths for related resources and relationships @@ -145,22 +138,22 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { container = result[relationshipPath] = {}; } // GET /resource/{id}/relationships/field - container.get = this.makeRelationshipFetch(zmodel, field); + container.get = this.makeRelationshipFetch(zmodel, field, policies); // PATCH /resource/{id}/relationships/field - container.patch = this.makeRelationshipUpdate(zmodel, field); + container.patch = this.makeRelationshipUpdate(zmodel, field, policies); if (field.type.array) { // POST /resource/{id}/relationships/field - container.post = this.makeRelationshipCreate(zmodel, field); + container.post = this.makeRelationshipCreate(zmodel, field, policies); } } return result; } - private makeResourceList(model: DataModel) { + private makeResourceList(model: DataModel, policies: Policies) { return { operationId: `list-${model.name}`, - description: `List ${model.name} resources`, + description: `List "${model.name}" resources`, tags: [lowerCaseFirst(model.name)], parameters: [ this.parameter('include'), @@ -173,13 +166,14 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { '200': this.success(`${model.name}ListResponse`), '403': this.forbidden(), }, + security: policies.read === true ? [] : undefined, }; } - private makeResourceCreate(model: DataModel) { + private makeResourceCreate(model: DataModel, policies: Policies) { return { operationId: `create-${model.name}`, - description: `Create a ${model.name} resource`, + description: `Create a "${model.name}" resource`, tags: [lowerCaseFirst(model.name)], requestBody: { content: { @@ -192,41 +186,57 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { '201': this.success(`${model.name}Response`), '403': this.forbidden(), }, + security: policies.create === true ? [] : undefined, }; } - private makeResourceFetch(model: DataModel) { + private makeResourceFetch(model: DataModel, policies: Policies) { return { operationId: `fetch-${model.name}`, - description: `Fetch one ${model.name} resource`, + description: `Fetch a "${model.name}" resource`, tags: [lowerCaseFirst(model.name)], - parameters: [this.parameter('id'), this.parameter('include'), ...this.generateFilterParameters(model)], + 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) { - return { + 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}`, + description: `Fetch the related "${field.name}" resource for "${model.name}"`, tags: [lowerCaseFirst(model.name)], - parameters: [this.parameter('id'), this.parameter('include'), ...this.generateFilterParameters(model)], + 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) { + private makeResourceUpdate(model: DataModel, policies: Policies) { return { operationId: `update-${model.name}`, - description: `Update one ${model.name} resource`, + description: `Update a "${model.name}" resource`, tags: [lowerCaseFirst(model.name)], parameters: [this.parameter('id')], requestBody: { @@ -239,24 +249,28 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { responses: { '200': this.success(`${model.name}Response`), '403': this.forbidden(), + '404': this.notFound(), }, + security: policies.update === true ? [] : undefined, }; } - private makeResourceDelete(model: DataModel) { + private makeResourceDelete(model: DataModel, policies: Policies) { return { operationId: `delete-${model.name}`, - description: `Delete one ${model.name} resource`, + 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) { + private makeRelationshipFetch(model: DataModel, field: DataModelField, policies: Policies) { const parameters: OAPI.OperationObject['parameters'] = [this.parameter('id')]; if (field.type.array) { parameters.push( @@ -268,59 +282,67 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { } return { operationId: `fetch-${model.name}-relationship-${field.name}`, - description: `Fetch${field.name} relationships for ${model.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'), + ? this.success('_toManyRelationshipResponse') + : this.success('_toOneRelationshipResponse'), '403': this.forbidden(), + '404': this.notFound(), }, + security: policies.read === true ? [] : undefined, }; } - private makeRelationshipCreate(model: DataModel, field: DataModelField) { + private makeRelationshipCreate(model: DataModel, field: DataModelField, policies: Policies) { return { operationId: `create-${model.name}-relationship-${field.name}`, - description: `Create new ${field.name} relationships for ${model.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'), + schema: this.ref('_toManyRelationshipRequest'), }, }, }, responses: { - '200': this.success('toManyRelationshipResponse'), + '200': this.success('_toManyRelationshipResponse'), '403': this.forbidden(), + '404': this.notFound(), }, + security: policies.update === true ? [] : undefined, }; } - private makeRelationshipUpdate(model: DataModel, field: DataModelField) { + private makeRelationshipUpdate(model: DataModel, field: DataModelField, policies: Policies) { return { operationId: `update-${model.name}-relationship-${field.name}`, - description: `Update ${field.name} relationships for ${model.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'), + ? this.ref('_toManyRelationshipRequest') + : this.ref('_toOneRelationshipRequest'), }, }, }, responses: { '200': field.type.array - ? this.success('toManyRelationshipResponse') - : this.success('toOneRelationshipResponse'), + ? this.success('_toManyRelationshipResponse') + : this.success('_toOneRelationshipResponse'), '403': this.forbidden(), + '404': this.notFound(), }, + security: policies.update === true ? [] : undefined, }; } @@ -374,15 +396,6 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { }; } - 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 parameters: Record = {}; @@ -418,39 +431,44 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { private generateSharedComponents(): Record { return { - jsonapi: { + _jsonapi: { type: 'object', - description: 'an object describing the server’s implementation', + description: 'An object describing the server’s implementation', properties: { version: { type: 'string' }, - meta: this.ref('meta'), + meta: this.ref('_meta'), }, }, - meta: { + _meta: { type: 'object', + description: 'Meta information about the response', additionalProperties: true, }, - resourceIdentifier: { + _resourceIdentifier: { type: 'object', + description: 'Identifier for a resource', required: ['type', 'id'], properties: { type: { type: 'string' }, id: { type: 'string' }, }, }, - resource: this.allOf(this.ref('resourceIdentifier'), { + _resource: this.allOf(this.ref('_resourceIdentifier'), { type: 'object', + description: 'A resource with attributes and relationships', properties: { attributes: { type: 'object' }, relationships: { type: 'object' }, }, }), - links: { + _links: { type: 'object', + description: 'Links related to the resource', properties: { self: { type: 'string' } }, }, - pagination: { + _pagination: { type: 'object', + description: 'Pagination information', properties: { first: this.nullable({ type: 'string' }), last: this.nullable({ type: 'string' }), @@ -458,8 +476,9 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { next: this.nullable({ type: 'string' }), }, }, - errors: { + _errors: { type: 'array', + description: 'An array of error objects', items: { type: 'object', properties: { @@ -470,76 +489,93 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { }, }, }, - errorResponse: { + _errorResponse: { type: 'object', + description: 'An error response', properties: { - jsonapi: this.ref('jsonapi'), - errors: this.ref('errors'), + jsonapi: this.ref('_jsonapi'), + errors: this.ref('_errors'), }, }, - relationLinks: { + _relationLinks: { type: 'object', + description: 'Links related to a relationship', properties: { self: { type: 'string' }, related: { type: 'string' }, }, }, - toOneRelationship: { + _toOneRelationship: { type: 'object', + description: 'A to-one relationship', properties: { - data: this.ref('resourceIdentifier'), + data: this.nullable(this.ref('_resourceIdentifier')), }, }, - toOneRelationshipWithLinks: { + _toOneRelationshipWithLinks: { type: 'object', + description: 'A to-one relationship with links', properties: { - links: this.ref('relationLinks'), - data: this.ref('resourceIdentifier'), + links: this.ref('_relationLinks'), + data: this.nullable(this.ref('_resourceIdentifier')), }, }, - toManyRelationship: { + _toManyRelationship: { type: 'object', + description: 'A to-many relationship', properties: { - data: this.array(this.ref('resourceIdentifier')), + data: this.array(this.ref('_resourceIdentifier')), }, }, - toManyRelationshipWithLinks: { + _toManyRelationshipWithLinks: { type: 'object', + description: 'A to-many relationship with links', properties: { - links: this.ref('pagedRelationLinks'), - data: this.array(this.ref('resourceIdentifier')), + links: this.ref('_pagedRelationLinks'), + data: this.array(this.ref('_resourceIdentifier')), }, }, - pagedRelationLinks: this.allOf(this.ref('pagination'), this.ref('relationLinks')), - toManyRelationshipRequest: { + _pagedRelationLinks: { + description: 'Relationship links with pagination information', + ...this.allOf(this.ref('_pagination'), this.ref('_relationLinks')), + }, + _toManyRelationshipRequest: { type: 'object', + description: 'Input for manipulating a to-many relationship', properties: { data: { type: 'array', - items: this.ref('resourceIdentifier'), + items: this.ref('_resourceIdentifier'), }, }, }, - toOneRelationshipRequest: this.nullable({ - type: 'object', - properties: { - data: this.ref('resourceIdentifier'), - }, - }), - toManyRelationshipResponse: this.allOf(this.ref('toManyRelationshipWithLinks'), { - type: 'object', - properties: { - jsonapi: this.ref('jsonapi'), - }, - }), - toOneRelationshipResponse: this.nullable( - this.allOf(this.ref('toOneRelationshipWithLinks'), { + _toOneRelationshipRequest: { + description: 'Input for manipulating a to-one relationship', + ...this.nullable({ + type: 'object', + 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'), + jsonapi: this.ref('_jsonapi'), }, - }) - ), + }), + }, }; } @@ -548,12 +584,14 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { 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' }, @@ -561,6 +599,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { sort: { name: 'sort', in: 'query', + description: 'Fields to sort by', required: false, style: 'form', schema: { type: 'string' }, @@ -568,6 +607,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { 'page-offset': { name: 'page[offset]', in: 'query', + description: 'Offset for pagination', required: false, style: 'form', schema: { type: 'integer' }, @@ -575,6 +615,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { 'page-limit': { name: 'page[limit]', in: 'query', + description: 'Limit for pagination', required: false, style: 'form', schema: { type: 'integer' }, @@ -585,6 +626,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { 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; @@ -596,6 +638,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { result[`${model.name}CreateRequest`] = { type: 'object', + description: `Input for creating a "${model.name}"`, required: ['data'], properties: { data: this.generateModelEntity(model, 'input'), @@ -604,6 +647,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { result[`${model.name}UpdateRequest`] = { type: 'object', + description: `Input for updating a "${model.name}"`, required: ['data'], properties: { data: this.generateModelEntity(model, 'input') }, }; @@ -612,18 +656,19 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { for (const field of model.fields) { if (this.isRelationshipField(field)) { if (field.type.array) { - relationships[field.name] = this.ref('toManyRelationship'); + relationships[field.name] = this.ref('_toManyRelationship'); } else { - relationships[field.name] = this.ref('toOneRelationship'); + 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'), + jsonapi: this.ref('_jsonapi'), data: this.allOf(this.ref(`${model.name}`), { type: 'object', properties: { relationships: { type: 'object', properties: relationships } }, @@ -631,17 +676,18 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { included: { type: 'array', - items: this.ref('resource'), + items: this.ref('_resource'), }, - links: this.ref('links'), + links: this.ref('_links'), }, }; result[`${model.name}ListResponse`] = { type: 'object', + description: `Response for a list of "${model.name}"`, required: ['data'], properties: { - jsonapi: this.ref('jsonapi'), + jsonapi: this.ref('_jsonapi'), data: this.array( this.allOf(this.ref(`${model.name}`), { type: 'object', @@ -650,9 +696,9 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { ), included: { type: 'array', - items: this.ref('resource'), + items: this.ref('_resource'), }, - links: this.allOf(this.ref('links'), this.ref('pagination')), + links: this.allOf(this.ref('_links'), this.ref('_pagination')), }, }; @@ -671,9 +717,9 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { if (this.isRelationshipField(field)) { let relType: string; if (mode === 'input') { - relType = field.type.array ? 'toManyRelationship' : 'toOneRelationship'; + relType = field.type.array ? '_toManyRelationship' : '_toOneRelationship'; } else { - relType = field.type.array ? 'toManyRelationshipWithLinks' : 'toOneRelationshipWithLinks'; + relType = field.type.array ? '_toManyRelationshipWithLinks' : '_toOneRelationshipWithLinks'; } relationships[field.name] = this.ref(relType); } else { @@ -691,6 +737,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { // 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' }, @@ -765,10 +812,21 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { private forbidden() { return { - description: 'Forbidden', + 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'), + schema: this.ref('_errorResponse'), }, }, }; diff --git a/packages/plugins/openapi/src/rpc-generator.ts b/packages/plugins/openapi/src/rpc-generator.ts index 90d43ac78..3372bd201 100644 --- a/packages/plugins/openapi/src/rpc-generator.ts +++ b/packages/plugins/openapi/src/rpc-generator.ts @@ -17,10 +17,8 @@ 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. @@ -54,7 +52,7 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { 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]: [] })); @@ -93,17 +91,6 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { 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) { @@ -541,7 +528,7 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { description: 'Invalid request', }, '403': { - description: 'Forbidden', + description: 'Request is forbidden', }, }, }; @@ -607,15 +594,6 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { 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 = { 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..7edf2ae35 --- /dev/null +++ b/packages/plugins/openapi/tests/baseline/rest.baseline.yaml @@ -0,0 +1,1419 @@ +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 + in: query + schema: + type: string + - name: filter[createdAt$lt] + required: false + in: query + schema: + type: string + - name: filter[createdAt$lte] + required: false + in: query + schema: + type: string + - name: filter[createdAt$gt] + required: false + in: query + schema: + type: string + - name: filter[createdAt$gte] + required: false + in: query + schema: + type: string + - name: filter[updatedAt$lt] + required: false + in: query + schema: + type: string + - name: filter[updatedAt$lte] + required: false + in: query + schema: + type: string + - name: filter[updatedAt$gt] + required: false + in: query + schema: + type: string + - name: filter[updatedAt$gte] + required: false + in: query + schema: + type: string + - name: filter[email$contains] + required: false + in: query + schema: + type: string + - name: filter[email$icontains] + required: false + in: query + schema: + type: string + - name: filter[email$search] + required: false + in: query + schema: + type: string + - name: filter[email$startsWith] + required: false + in: query + schema: + type: string + - name: filter[email$endsWith] + required: false + in: query + schema: + type: string + - name: filter[posts$has] + required: false + in: query + schema: + type: string + - name: filter[posts$hasEvery] + required: false + in: query + schema: + type: string + - name: filter[posts$hasSome] + required: false + in: query + schema: + type: string + - name: filter[posts$isEmpty] + required: false + in: query + schema: + 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 + in: query + schema: + type: string + - name: filter[createdAt$lt] + required: false + in: query + schema: + type: string + - name: filter[createdAt$lte] + required: false + in: query + schema: + type: string + - name: filter[createdAt$gt] + required: false + in: query + schema: + type: string + - name: filter[createdAt$gte] + required: false + in: query + schema: + type: string + - name: filter[updatedAt$lt] + required: false + in: query + schema: + type: string + - name: filter[updatedAt$lte] + required: false + in: query + schema: + type: string + - name: filter[updatedAt$gt] + required: false + in: query + schema: + type: string + - name: filter[updatedAt$gte] + required: false + in: query + schema: + type: string + - name: filter[email$contains] + required: false + in: query + schema: + type: string + - name: filter[email$icontains] + required: false + in: query + schema: + type: string + - name: filter[email$search] + required: false + in: query + schema: + type: string + - name: filter[email$startsWith] + required: false + in: query + schema: + type: string + - name: filter[email$endsWith] + required: false + in: query + schema: + type: string + - name: filter[posts$has] + required: false + in: query + schema: + type: string + - name: filter[posts$hasEvery] + required: false + in: query + schema: + type: string + - name: filter[posts$hasSome] + required: false + in: query + schema: + type: string + - name: filter[posts$isEmpty] + required: false + in: query + schema: + 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 + in: query + schema: + type: string + - name: filter[createdAt$lt] + required: false + in: query + schema: + type: string + - name: filter[createdAt$lte] + required: false + in: query + schema: + type: string + - name: filter[createdAt$gt] + required: false + in: query + schema: + type: string + - name: filter[createdAt$gte] + required: false + in: query + schema: + type: string + - name: filter[updatedAt$lt] + required: false + in: query + schema: + type: string + - name: filter[updatedAt$lte] + required: false + in: query + schema: + type: string + - name: filter[updatedAt$gt] + required: false + in: query + schema: + type: string + - name: filter[updatedAt$gte] + required: false + in: query + schema: + type: string + - name: filter[email$contains] + required: false + in: query + schema: + type: string + - name: filter[email$icontains] + required: false + in: query + schema: + type: string + - name: filter[email$search] + required: false + in: query + schema: + type: string + - name: filter[email$startsWith] + required: false + in: query + schema: + type: string + - name: filter[email$endsWith] + required: false + in: query + schema: + type: string + - name: filter[posts$has] + required: false + in: query + schema: + type: string + - name: filter[posts$hasEvery] + required: false + in: query + schema: + type: string + - name: filter[posts$hasSome] + required: false + in: query + schema: + type: string + - name: filter[posts$isEmpty] + required: false + in: query + schema: + 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 + in: query + schema: + type: string + - name: filter[createdAt$lt] + required: false + in: query + schema: + type: string + - name: filter[createdAt$lte] + required: false + in: query + schema: + type: string + - name: filter[createdAt$gt] + required: false + in: query + schema: + type: string + - name: filter[createdAt$gte] + required: false + in: query + schema: + type: string + - name: filter[updatedAt$lt] + required: false + in: query + schema: + type: string + - name: filter[updatedAt$lte] + required: false + in: query + schema: + type: string + - name: filter[updatedAt$gt] + required: false + in: query + schema: + type: string + - name: filter[updatedAt$gte] + required: false + in: query + schema: + type: string + - name: filter[title$contains] + required: false + in: query + schema: + type: string + - name: filter[title$icontains] + required: false + in: query + schema: + type: string + - name: filter[title$search] + required: false + in: query + schema: + type: string + - name: filter[title$startsWith] + required: false + in: query + schema: + type: string + - name: filter[title$endsWith] + required: false + in: query + schema: + type: string + - name: filter[authorId$contains] + required: false + in: query + schema: + type: string + - name: filter[authorId$icontains] + required: false + in: query + schema: + type: string + - name: filter[authorId$search] + required: false + in: query + schema: + type: string + - name: filter[authorId$startsWith] + required: false + in: query + schema: + type: string + - name: filter[authorId$endsWith] + required: false + in: query + schema: + type: string + - name: filter[viewCount$lt] + required: false + in: query + schema: + type: string + - name: filter[viewCount$lte] + required: false + in: query + schema: + type: string + - name: filter[viewCount$gt] + required: false + in: query + schema: + type: string + - name: filter[viewCount$gte] + required: false + in: query + schema: + 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' + 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 + 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 + description: An error response + properties: + jsonapi: + $ref: '#/components/schemas/_jsonapi' + errors: + $ref: '#/components/schemas/_errors' + _relationLinks: + type: object + 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 + description: A to-one relationship with links + properties: + links: + $ref: '#/components/schemas/_relationLinks' + data: + oneOf: + - $ref: '#/components/schemas/_resourceIdentifier' + - type: 'null' + _toManyRelationship: + type: object + description: A to-many relationship + properties: + data: + type: array + items: + $ref: '#/components/schemas/_resourceIdentifier' + _toManyRelationshipWithLinks: + type: object + 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 + 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 + 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 + 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/_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 + 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' + 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 + 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 + 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/_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 + 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' + 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 + 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 index cbe213575..4a6a24185 100644 --- a/packages/plugins/openapi/tests/openapi-restful.test.ts +++ b/packages/plugins/openapi/tests/openapi-restful.test.ts @@ -29,20 +29,6 @@ model User { email String @unique role Role @default(USER) posts Post[] - - @@openapi.meta({ - findMany: { - description: 'Find users matching the given conditions' - }, - delete: { - method: 'put', - path: 'dodelete', - description: 'Delete a unique user', - summary: 'Delete a user yeah yeah', - tags: ['delete', 'user'], - deprecated: true - }, - }) } model Post { @@ -56,10 +42,7 @@ model Post { viewCount Int @default(0) @@openapi.meta({ - tagDescription: 'Post-related operations', - findMany: { - ignore: true - } + tagDescription: 'Post-related operations' }) } @@ -76,14 +59,145 @@ model Bar { 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); - YAML.parse(fs.readFileSync(output, 'utf-8')); + 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' } + } +} - await OpenAPIParser.validate(output); +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') }) + ); }); }); diff --git a/packages/plugins/openapi/tests/openapi.test.ts b/packages/plugins/openapi/tests/openapi-rpc.test.ts similarity index 98% rename from packages/plugins/openapi/tests/openapi.test.ts rename to packages/plugins/openapi/tests/openapi-rpc.test.ts index 7258a3ae9..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); 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==} From b17378be4dae22e87f772b432623c1a6d48d119b Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 12 May 2023 10:10:23 -0700 Subject: [PATCH 3/4] improve filter generation --- .../plugins/openapi/src/rest-generator.ts | 168 ++++--- .../openapi/tests/baseline/rest.baseline.yaml | 474 +++++++++++++++--- packages/sdk/src/utils.ts | 34 ++ 3 files changed, 537 insertions(+), 139 deletions(-) diff --git a/packages/plugins/openapi/src/rest-generator.ts b/packages/plugins/openapi/src/rest-generator.ts index 624364366..cc3a363d0 100644 --- a/packages/plugins/openapi/src/rest-generator.ts +++ b/packages/plugins/openapi/src/rest-generator.ts @@ -1,8 +1,16 @@ // Inspired by: https://github.com/omar-dulaimi/prisma-trpc-generator import { DMMF } from '@prisma/generator-helper'; -import { AUXILIARY_FIELDS, PluginError, analyzePolicies, getDataModels, isIdField } from '@zenstackhq/sdk'; -import { BuiltinType, DataModel, DataModelField, Enum, isDataModel, isEnum } from '@zenstackhq/sdk/ast'; +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'; @@ -346,54 +354,99 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { }; } - private generateFilterParameters(zmodel: DataModel) { + private generateFilterParameters(model: DataModel) { const result: OAPI.ParameterObject[] = []; - for (const field of zmodel.fields) { + 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)) { - result.push(this.makeParameter('filter[id]')); + // id filter + result.push(this.makeFilterParameter(field, 'id', 'Id filter')); continue; } - switch (field.type.type) { - case 'Int': - case 'BigInt': - case 'Float': - case 'Decimal': - case 'DateTime': { - result.push(this.makeParameter(`filter[${field.name}$lt]`)); - result.push(this.makeParameter(`filter[${field.name}$lte]`)); - result.push(this.makeParameter(`filter[${field.name}$gt]`)); - result.push(this.makeParameter(`filter[${field.name}$gte]`)); - break; - } - case 'String': { - result.push(this.makeParameter(`filter[${field.name}$contains]`)); - result.push(this.makeParameter(`filter[${field.name}$icontains]`)); - result.push(this.makeParameter(`filter[${field.name}$search]`)); - result.push(this.makeParameter(`filter[${field.name}$startsWith]`)); - result.push(this.makeParameter(`filter[${field.name}$endsWith]`)); - break; - } + + // 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) { - result.push(this.makeParameter(`filter[${field.name}$has]`)); - result.push(this.makeParameter(`filter[${field.name}$hasEvery]`)); - result.push(this.makeParameter(`filter[${field.name}$hasSome]`)); - result.push(this.makeParameter(`filter[${field.name}$isEmpty]`)); + // 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 makeParameter(name: string): OAPI.ParameterObject { + 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: name === 'id' ? 'filter[id]' : `filter[${field.name}${name}]`, required: false, + description: name === 'id' ? description : `${description} for "${field.name}"`, in: 'query', - schema: { type: 'string' }, - }; + style: 'form', + explode: false, + schema, + } as OAPI.ParameterObject; } private generateComponents() { @@ -463,6 +516,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { }), _links: { type: 'object', + required: ['self'], description: 'Links related to the resource', properties: { self: { type: 'string' } }, }, @@ -491,6 +545,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { }, _errorResponse: { type: 'object', + required: ['errors'], description: 'An error response', properties: { jsonapi: this.ref('_jsonapi'), @@ -499,6 +554,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { }, _relationLinks: { type: 'object', + required: ['self', 'related'], description: 'Links related to a relationship', properties: { self: { type: 'string' }, @@ -514,6 +570,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { }, _toOneRelationshipWithLinks: { type: 'object', + required: ['links', 'data'], description: 'A to-one relationship with links', properties: { links: this.ref('_relationLinks'), @@ -522,6 +579,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { }, _toManyRelationship: { type: 'object', + required: ['data'], description: 'A to-many relationship', properties: { data: this.array(this.ref('_resourceIdentifier')), @@ -529,6 +587,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { }, _toManyRelationshipWithLinks: { type: 'object', + required: ['links', 'data'], description: 'A to-many relationship with links', properties: { links: this.ref('_pagedRelationLinks'), @@ -541,6 +600,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { }, _toManyRelationshipRequest: { type: 'object', + required: ['data'], description: 'Input for manipulating a to-many relationship', properties: { data: { @@ -553,6 +613,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { description: 'Input for manipulating a to-one relationship', ...this.nullable({ type: 'object', + required: ['data'], properties: { data: this.ref('_resourceIdentifier'), }, @@ -634,14 +695,14 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { private generateDataModelComponents(model: DataModel) { const result: Record = {}; - result[`${model.name}`] = this.generateModelEntity(model, 'output'); + 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, 'input'), + data: this.generateModelEntity(model, 'create'), }, }; @@ -649,12 +710,12 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { type: 'object', description: `Input for updating a "${model.name}"`, required: ['data'], - properties: { data: this.generateModelEntity(model, 'input') }, + properties: { data: this.generateModelEntity(model, 'update') }, }; const relationships: Record = {}; for (const field of model.fields) { - if (this.isRelationshipField(field)) { + if (isRelationshipField(field)) { if (field.type.array) { relationships[field.name] = this.ref('_toManyRelationship'); } else { @@ -685,7 +746,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { result[`${model.name}ListResponse`] = { type: 'object', description: `Response for a list of "${model.name}"`, - required: ['data'], + required: ['data', 'links'], properties: { jsonapi: this.ref('_jsonapi'), data: this.array( @@ -705,7 +766,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { return result; } - private generateModelEntity(model: DataModel, mode: 'input' | 'output'): OAPI.SchemaObject { + 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 = {}; @@ -714,9 +775,9 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { const required: string[] = []; for (const field of fields) { - if (this.isRelationshipField(field)) { + if (isRelationshipField(field)) { let relType: string; - if (mode === 'input') { + if (mode === 'create' || mode === 'update') { relType = field.type.array ? '_toManyRelationship' : '_toOneRelationship'; } else { relType = field.type.array ? '_toManyRelationshipWithLinks' : '_toOneRelationshipWithLinks'; @@ -725,6 +786,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { } 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) @@ -760,25 +822,16 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { return result; } - private isRelationshipField(field: DataModelField) { - return isDataModel(field.type.reference?.ref); - } - private generateField(field: DataModelField) { - const resolvedDecl = field.type.reference?.ref; - if (resolvedDecl && isEnum(resolvedDecl)) { - return this.wrapArray(this.ref(resolvedDecl.name), field.type.array); - } - invariant(field?.type?.type); - return this.wrapArray(this.modelTypeToOpenAPIType(field.type.type), field.type.array); + return this.wrapArray(this.fieldTypeToOpenAPISchema(field.type), field.type.array); } private get specVersion() { return this.getOption('specVersion', '3.0.0'); } - private modelTypeToOpenAPIType(type: BuiltinType): OAPI.ReferenceObject | OAPI.SchemaObject { - switch (type) { + private fieldTypeToOpenAPISchema(type: DataModelFieldType): OAPI.ReferenceObject | OAPI.SchemaObject { + switch (type.type) { case 'String': return { type: 'string' }; case 'Int': @@ -792,9 +845,12 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { case 'DateTime': return { type: 'string', format: 'date-time' }; case 'Json': - return {}; - default: - return { $ref: `#/components/schemas/${type}` }; + return { type: 'object' }; + default: { + const fieldDecl = type.reference?.ref; + invariant(fieldDecl); + return this.ref(fieldDecl?.name); + } } } diff --git a/packages/plugins/openapi/tests/baseline/rest.baseline.yaml b/packages/plugins/openapi/tests/baseline/rest.baseline.yaml index 7edf2ae35..08d25023b 100644 --- a/packages/plugins/openapi/tests/baseline/rest.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rest.baseline.yaml @@ -21,94 +21,168 @@ paths: - $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: Equal 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: Equal 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: Equal 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[posts$has] + - name: filter[role] required: false + description: Equal filter for "role" in: query + style: form + explode: false schema: - type: string - - name: filter[posts$hasEvery] + $ref: '#/components/schemas/Role' + - name: filter[posts] required: false + description: Equal filter for "posts" in: query + style: form + explode: false schema: - type: string - - name: filter[posts$hasSome] - required: false - in: query - schema: - type: string - - name: filter[posts$isEmpty] - required: false - in: query - schema: - type: string + type: array + items: + type: string responses: '200': description: Successful operation @@ -240,94 +314,168 @@ paths: - $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: Equal 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: Equal 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: Equal 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[posts$has] + - name: filter[role] required: false + description: Equal filter for "role" in: query + style: form + explode: false schema: - type: string - - name: filter[posts$hasEvery] + $ref: '#/components/schemas/Role' + - name: filter[posts] required: false + description: Equal filter for "posts" in: query + style: form + explode: false schema: - type: string - - name: filter[posts$hasSome] - required: false - in: query - schema: - type: string - - name: filter[posts$isEmpty] - required: false - in: query - schema: - type: string + type: array + items: + type: string responses: '200': description: Successful operation @@ -360,94 +508,168 @@ paths: - $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: Equal 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: Equal 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: Equal 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[posts$has] - required: false - in: query - schema: - type: string - - name: filter[posts$hasEvery] - required: false - in: query - schema: - type: string - - name: filter[posts$hasSome] + - name: filter[role] required: false + description: Equal filter for "role" in: query + style: form + explode: false schema: - type: string - - name: filter[posts$isEmpty] + $ref: '#/components/schemas/Role' + - name: filter[posts] required: false + description: Equal filter for "posts" in: query + style: form + explode: false schema: - type: string + type: array + items: + type: string responses: '200': description: Successful operation @@ -542,119 +764,206 @@ paths: - $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: Equal 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: Equal 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: Equal 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[authorId$contains] - required: false - in: query - schema: - type: string - - name: filter[authorId$icontains] + - name: filter[author] required: false + description: Equal filter for "author" in: query + style: form + explode: false schema: type: string - - name: filter[authorId$search] + - name: filter[published] required: false + description: Equal filter for "published" in: query + style: form + explode: false schema: - type: string - - name: filter[authorId$startsWith] - required: false - in: query - schema: - type: string - - name: filter[authorId$endsWith] + type: boolean + - name: filter[viewCount] required: false + description: Equal filter for "viewCount" in: query + style: form + explode: false schema: - type: string + type: integer - name: filter[viewCount$lt] required: false + description: Less-than filter for "viewCount" in: query + style: form + explode: false schema: - type: string + type: integer - name: filter[viewCount$lte] required: false + description: Less-than or equal filter for "viewCount" in: query + style: form + explode: false schema: - type: string + type: integer - name: filter[viewCount$gt] required: false + description: Greater-than filter for "viewCount" in: query + style: form + explode: false schema: - type: string + type: integer - name: filter[viewCount$gte] required: false + description: Greater-than or equal filter for "viewCount" in: query + style: form + explode: false schema: - type: string + type: integer responses: '200': description: Successful operation @@ -895,6 +1204,8 @@ components: type: object _links: type: object + required: + - self description: Links related to the resource properties: self: @@ -935,6 +1246,8 @@ components: type: string _errorResponse: type: object + required: + - errors description: An error response properties: jsonapi: @@ -943,6 +1256,9 @@ components: $ref: '#/components/schemas/_errors' _relationLinks: type: object + required: + - self + - related description: Links related to a relationship properties: self: @@ -959,6 +1275,9 @@ components: - type: 'null' _toOneRelationshipWithLinks: type: object + required: + - links + - data description: A to-one relationship with links properties: links: @@ -969,6 +1288,8 @@ components: - type: 'null' _toManyRelationship: type: object + required: + - data description: A to-many relationship properties: data: @@ -977,6 +1298,9 @@ components: $ref: '#/components/schemas/_resourceIdentifier' _toManyRelationshipWithLinks: type: object + required: + - links + - data description: A to-many relationship with links properties: links: @@ -992,6 +1316,8 @@ components: - $ref: '#/components/schemas/_relationLinks' _toManyRelationshipRequest: type: object + required: + - data description: Input for manipulating a to-many relationship properties: data: @@ -1002,6 +1328,8 @@ components: description: Input for manipulating a to-one relationship oneOf: - type: object + required: + - data properties: data: $ref: '#/components/schemas/_resourceIdentifier' @@ -1042,11 +1370,6 @@ components: type: string attributes: type: object - required: - - createdAt - - updatedAt - - email - - role properties: createdAt: type: string @@ -1124,11 +1447,6 @@ components: type: string attributes: type: object - required: - - createdAt - - updatedAt - - email - - role properties: createdAt: type: string @@ -1174,6 +1492,7 @@ components: description: Response for a list of "User" required: - data + - links properties: jsonapi: $ref: '#/components/schemas/_jsonapi' @@ -1209,12 +1528,6 @@ components: type: string attributes: type: object - required: - - createdAt - - updatedAt - - title - - published - - viewCount properties: createdAt: type: string @@ -1301,12 +1614,6 @@ components: type: string attributes: type: object - required: - - createdAt - - updatedAt - - title - - published - - viewCount properties: createdAt: type: string @@ -1356,6 +1663,7 @@ components: description: Response for a list of "Post" required: - data + - links properties: jsonapi: $ref: '#/components/schemas/_jsonapi' diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index 1911df34e..974148b4d 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -160,3 +160,37 @@ export function isIdField(field: DataModelField) { } 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; + }); +} From 4ecffe0d021329514e81c40910c595a7f4670b46 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 12 May 2023 10:28:03 -0700 Subject: [PATCH 4/4] update baseline --- .../openapi/tests/baseline/rest.baseline.yaml | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/plugins/openapi/tests/baseline/rest.baseline.yaml b/packages/plugins/openapi/tests/baseline/rest.baseline.yaml index 08d25023b..d3f1f7ee6 100644 --- a/packages/plugins/openapi/tests/baseline/rest.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rest.baseline.yaml @@ -29,7 +29,7 @@ paths: type: string - name: filter[createdAt] required: false - description: Equal filter for "createdAt" + description: Equality filter for "createdAt" in: query style: form explode: false @@ -74,7 +74,7 @@ paths: format: date-time - name: filter[updatedAt] required: false - description: Equal filter for "updatedAt" + description: Equality filter for "updatedAt" in: query style: form explode: false @@ -119,7 +119,7 @@ paths: format: date-time - name: filter[email] required: false - description: Equal filter for "email" + description: Equality filter for "email" in: query style: form explode: false @@ -167,7 +167,7 @@ paths: type: string - name: filter[role] required: false - description: Equal filter for "role" + description: Equality filter for "role" in: query style: form explode: false @@ -175,7 +175,7 @@ paths: $ref: '#/components/schemas/Role' - name: filter[posts] required: false - description: Equal filter for "posts" + description: Equality filter for "posts" in: query style: form explode: false @@ -322,7 +322,7 @@ paths: type: string - name: filter[createdAt] required: false - description: Equal filter for "createdAt" + description: Equality filter for "createdAt" in: query style: form explode: false @@ -367,7 +367,7 @@ paths: format: date-time - name: filter[updatedAt] required: false - description: Equal filter for "updatedAt" + description: Equality filter for "updatedAt" in: query style: form explode: false @@ -412,7 +412,7 @@ paths: format: date-time - name: filter[email] required: false - description: Equal filter for "email" + description: Equality filter for "email" in: query style: form explode: false @@ -460,7 +460,7 @@ paths: type: string - name: filter[role] required: false - description: Equal filter for "role" + description: Equality filter for "role" in: query style: form explode: false @@ -468,7 +468,7 @@ paths: $ref: '#/components/schemas/Role' - name: filter[posts] required: false - description: Equal filter for "posts" + description: Equality filter for "posts" in: query style: form explode: false @@ -516,7 +516,7 @@ paths: type: string - name: filter[createdAt] required: false - description: Equal filter for "createdAt" + description: Equality filter for "createdAt" in: query style: form explode: false @@ -561,7 +561,7 @@ paths: format: date-time - name: filter[updatedAt] required: false - description: Equal filter for "updatedAt" + description: Equality filter for "updatedAt" in: query style: form explode: false @@ -606,7 +606,7 @@ paths: format: date-time - name: filter[email] required: false - description: Equal filter for "email" + description: Equality filter for "email" in: query style: form explode: false @@ -654,7 +654,7 @@ paths: type: string - name: filter[role] required: false - description: Equal filter for "role" + description: Equality filter for "role" in: query style: form explode: false @@ -662,7 +662,7 @@ paths: $ref: '#/components/schemas/Role' - name: filter[posts] required: false - description: Equal filter for "posts" + description: Equality filter for "posts" in: query style: form explode: false @@ -772,7 +772,7 @@ paths: type: string - name: filter[createdAt] required: false - description: Equal filter for "createdAt" + description: Equality filter for "createdAt" in: query style: form explode: false @@ -817,7 +817,7 @@ paths: format: date-time - name: filter[updatedAt] required: false - description: Equal filter for "updatedAt" + description: Equality filter for "updatedAt" in: query style: form explode: false @@ -862,7 +862,7 @@ paths: format: date-time - name: filter[title] required: false - description: Equal filter for "title" + description: Equality filter for "title" in: query style: form explode: false @@ -910,7 +910,7 @@ paths: type: string - name: filter[author] required: false - description: Equal filter for "author" + description: Equality filter for "author" in: query style: form explode: false @@ -918,7 +918,7 @@ paths: type: string - name: filter[published] required: false - description: Equal filter for "published" + description: Equality filter for "published" in: query style: form explode: false @@ -926,7 +926,7 @@ paths: type: boolean - name: filter[viewCount] required: false - description: Equal filter for "viewCount" + description: Equality filter for "viewCount" in: query style: form explode: false