diff --git a/packages/server/package.json b/packages/server/package.json index 1f2acd1ce..78e287f06 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -8,6 +8,7 @@ "watch": "tsup-node --watch", "lint": "eslint src --ext ts", "test": "vitest run", + "update-baseline": "UPDATE_BASELINE=1 vitest run test/openapi", "pack": "pnpm pack" }, "keywords": [ @@ -124,6 +125,7 @@ "@zenstackhq/common-helpers": "workspace:*", "@zenstackhq/orm": "workspace:*", "decimal.js": "catalog:", + "openapi-types": "^12.1.3", "superjson": "^2.2.3", "ts-japi": "^1.12.1", "ts-pattern": "catalog:", @@ -131,6 +133,7 @@ "zod-validation-error": "catalog:" }, "devDependencies": { + "@readme/openapi-parser": "^6.0.0", "@sveltejs/kit": "catalog:", "@types/body-parser": "^1.19.6", "@types/express": "^5.0.0", diff --git a/packages/server/src/api/common/schemas.ts b/packages/server/src/api/common/schemas.ts index 44e30665c..e11c885e6 100644 --- a/packages/server/src/api/common/schemas.ts +++ b/packages/server/src/api/common/schemas.ts @@ -1,3 +1,27 @@ import z from 'zod'; export const loggerSchema = z.union([z.enum(['debug', 'info', 'warn', 'error']).array(), z.function()]); + +const fieldSlicingSchema = z.looseObject({ + includedFilterKinds: z.string().array().optional(), + excludedFilterKinds: z.string().array().optional(), +}); + +const modelSlicingSchema = z.looseObject({ + includedOperations: z.array(z.string()).optional(), + excludedOperations: z.array(z.string()).optional(), + fields: z.record(z.string(), fieldSlicingSchema).optional(), +}); + +const slicingSchema = z.looseObject({ + includedModels: z.array(z.string()).optional(), + excludedModels: z.array(z.string()).optional(), + models: z.record(z.string(), modelSlicingSchema).optional(), + includedProcedures: z.array(z.string()).optional(), + excludedProcedures: z.array(z.string()).optional(), +}); + +export const queryOptionsSchema = z.looseObject({ + omit: z.record(z.string(), z.record(z.string(), z.boolean())).optional(), + slicing: slicingSchema.optional(), +}); diff --git a/packages/server/src/api/common/spec-utils.ts b/packages/server/src/api/common/spec-utils.ts new file mode 100644 index 000000000..f469f79aa --- /dev/null +++ b/packages/server/src/api/common/spec-utils.ts @@ -0,0 +1,115 @@ +import { lowerCaseFirst } from '@zenstackhq/common-helpers'; +import type { QueryOptions } from '@zenstackhq/orm'; +import { ExpressionUtils, type AttributeApplication, type SchemaDef } from '@zenstackhq/orm/schema'; + +/** + * Checks if a model is included based on slicing options. + */ +export function isModelIncluded(modelName: string, queryOptions?: QueryOptions): boolean { + const slicing = queryOptions?.slicing; + if (!slicing) return true; + + const excluded = slicing.excludedModels as readonly string[] | undefined; + if (excluded?.includes(modelName)) return false; + + const included = slicing.includedModels as readonly string[] | undefined; + if (included && !included.includes(modelName)) return false; + + return true; +} + +/** + * Checks if a CRUD operation is included for a model based on slicing options. + */ +export function isOperationIncluded(modelName: string, op: string, queryOptions?: QueryOptions): boolean { + const slicing = queryOptions?.slicing; + if (!slicing?.models) return true; + + const modelKey = lowerCaseFirst(modelName); + const modelSlicing = (slicing.models as Record)[modelKey] ?? (slicing.models as any).$all; + if (!modelSlicing) return true; + + const excluded = modelSlicing.excludedOperations as readonly string[] | undefined; + if (excluded?.includes(op)) return false; + + const included = modelSlicing.includedOperations as readonly string[] | undefined; + if (included && !included.includes(op)) return false; + + return true; +} + +/** + * Checks if a procedure is included based on slicing options. + */ +export function isProcedureIncluded(procName: string, queryOptions?: QueryOptions): boolean { + const slicing = queryOptions?.slicing; + if (!slicing) return true; + + const excluded = slicing.excludedProcedures as readonly string[] | undefined; + if (excluded?.includes(procName)) return false; + + const included = slicing.includedProcedures as readonly string[] | undefined; + if (included && !included.includes(procName)) return false; + + return true; +} + +/** + * Checks if a field should be omitted from the output schema based on queryOptions.omit. + */ +export function isFieldOmitted(modelName: string, fieldName: string, queryOptions?: QueryOptions): boolean { + const omit = queryOptions?.omit as Record> | undefined; + return omit?.[modelName]?.[fieldName] === true; +} + +/** + * Returns the list of model names from the schema that pass the slicing filter. + */ +export function getIncludedModels(schema: SchemaDef, queryOptions?: QueryOptions): string[] { + return Object.keys(schema.models).filter((name) => isModelIncluded(name, queryOptions)); +} + +/** + * Checks if a filter kind is allowed for a specific field based on slicing options. + */ +export function isFilterKindIncluded( + modelName: string, + fieldName: string, + filterKind: string, + queryOptions?: QueryOptions, +): boolean { + const slicing = queryOptions?.slicing; + if (!slicing?.models) return true; + + const modelKey = lowerCaseFirst(modelName); + const modelSlicing = (slicing.models as Record)[modelKey] ?? (slicing.models as any).$all; + if (!modelSlicing?.fields) return true; + + const fieldSlicing = modelSlicing.fields[fieldName] ?? modelSlicing.fields.$all; + if (!fieldSlicing) return true; + + const excluded = fieldSlicing.excludedFilterKinds as readonly string[] | undefined; + if (excluded?.includes(filterKind)) return false; + + const included = fieldSlicing.includedFilterKinds as readonly string[] | undefined; + if (included && !included.includes(filterKind)) return false; + + return true; +} + +/** + * Extracts a "description" from `@@meta("description", "...")` or `@meta("description", "...")` attributes. + */ +export function getMetaDescription(attributes: readonly AttributeApplication[] | undefined): string | undefined { + if (!attributes) return undefined; + for (const attr of attributes) { + if (attr.name !== '@meta' && attr.name !== '@@meta') continue; + const nameArg = attr.args?.find((a) => a.name === 'name'); + if (!nameArg || ExpressionUtils.getLiteralValue(nameArg.value) !== 'description') continue; + const valueArg = attr.args?.find((a) => a.name === 'value'); + if (valueArg) { + return ExpressionUtils.getLiteralValue(valueArg.value) as string | undefined; + } + } + return undefined; +} diff --git a/packages/server/src/api/common/types.ts b/packages/server/src/api/common/types.ts new file mode 100644 index 000000000..167ca17e5 --- /dev/null +++ b/packages/server/src/api/common/types.ts @@ -0,0 +1,32 @@ +import type { QueryOptions } from '@zenstackhq/orm'; +import type { SchemaDef } from '@zenstackhq/orm/schema'; +import type { OpenAPIV3_1 } from 'openapi-types'; + +export type CommonHandlerOptions = { + /** Query options that affect the behavior of the OpenAPI provider. */ + queryOptions?: QueryOptions; +}; + +export type OpenApiSpecOptions = { + /** Spec title. Defaults to 'ZenStack Generated API' */ + title?: string; + + /** Spec version. Defaults to '1.0.0' */ + version?: string; + + /** Spec description. */ + description?: string; + + /** Spec summary. */ + summary?: string; +}; + +/** + * Interface for generating OpenAPI specifications. + */ +export interface OpenApiSpecGenerator { + /** + * Generates an OpenAPI v3.1 specification document. + */ + generateSpec(options?: OpenApiSpecOptions): Promise; +} diff --git a/packages/server/src/api/index.ts b/packages/server/src/api/index.ts index 09d9700eb..03c5d8819 100644 --- a/packages/server/src/api/index.ts +++ b/packages/server/src/api/index.ts @@ -1,2 +1,3 @@ +export type { OpenApiSpecGenerator, OpenApiSpecOptions } from './common/types'; export { RestApiHandler, type RestApiHandlerOptions } from './rest'; export { RPCApiHandler, type RPCApiHandlerOptions } from './rpc'; diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index c2723f061..2e31fdb63 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -10,9 +10,11 @@ import z from 'zod'; import { fromError } from 'zod-validation-error/v4'; import type { ApiHandler, LogConfig, RequestContext, Response } from '../../types'; import { getProcedureDef, mapProcedureArgs } from '../common/procedures'; -import { loggerSchema } from '../common/schemas'; +import { loggerSchema, queryOptionsSchema } from '../common/schemas'; +import type { CommonHandlerOptions, OpenApiSpecGenerator, OpenApiSpecOptions } from '../common/types'; import { processSuperJsonRequestPayload } from '../common/utils'; import { getZodErrorMessage, log, registerCustomSerializers } from '../utils'; +import { RestApiSpecGenerator } from './openapi'; /** * Options for {@link RestApiHandler} @@ -64,7 +66,7 @@ export type RestApiHandlerOptions = { * Mapping from model names to unique field name to be used as resource's ID. */ externalIdMapping?: Record; -}; +} & CommonHandlerOptions; type RelationshipInfo = { type: string; @@ -127,7 +129,7 @@ registerCustomSerializers(); /** * RESTful-style API request handler (compliant with JSON:API) */ -export class RestApiHandler implements ApiHandler { +export class RestApiHandler implements ApiHandler, OpenApiSpecGenerator { // resource serializers private serializers = new Map(); @@ -298,6 +300,7 @@ export class RestApiHandler implements Api urlSegmentCharset: z.string().min(1).optional(), modelNameMapping: z.record(z.string(), z.string()).optional(), externalIdMapping: z.record(z.string(), z.string()).optional(), + queryOptions: queryOptionsSchema.optional(), }); const parseResult = schema.safeParse(options); if (!parseResult.success) { @@ -2060,9 +2063,7 @@ export class RestApiHandler implements Api } } else { if (op === 'between') { - const parts = value - .split(',') - .map((v) => this.coerce(fieldDef, v)); + const parts = value.split(',').map((v) => this.coerce(fieldDef, v)); if (parts.length !== 2) { throw new InvalidValueError(`"between" expects exactly 2 comma-separated values`); } @@ -2201,4 +2202,11 @@ export class RestApiHandler implements Api } //#endregion + + async generateSpec(options?: OpenApiSpecOptions) { + const generator = new RestApiSpecGenerator(this.options); + return generator.generateSpec(options); + } } + +export { RestApiSpecGenerator } from './openapi'; diff --git a/packages/server/src/api/rest/openapi.ts b/packages/server/src/api/rest/openapi.ts new file mode 100644 index 000000000..b3429d4dd --- /dev/null +++ b/packages/server/src/api/rest/openapi.ts @@ -0,0 +1,1019 @@ +import { lowerCaseFirst } from '@zenstackhq/common-helpers'; +import type { EnumDef, FieldDef, ModelDef, SchemaDef, TypeDefDef } from '@zenstackhq/orm/schema'; +import type { OpenAPIV3_1 } from 'openapi-types'; +import { PROCEDURE_ROUTE_PREFIXES } from '../common/procedures'; +import { + getIncludedModels, + getMetaDescription, + isFieldOmitted, + isFilterKindIncluded, + isModelIncluded, + isOperationIncluded, + isProcedureIncluded, +} from '../common/spec-utils'; +import type { OpenApiSpecOptions } from '../common/types'; +import type { RestApiHandlerOptions } from '.'; + +type SchemaObject = OpenAPIV3_1.SchemaObject; +type ReferenceObject = OpenAPIV3_1.ReferenceObject; +type ParameterObject = OpenAPIV3_1.ParameterObject; + +const ERROR_RESPONSE = { + description: 'Error', + content: { + 'application/vnd.api+json': { + schema: { $ref: '#/components/schemas/_errorResponse' }, + }, + }, +}; + +const SCALAR_STRING_OPS = ['$contains', '$icontains', '$search', '$startsWith', '$endsWith']; +const SCALAR_COMPARABLE_OPS = ['$lt', '$lte', '$gt', '$gte']; +const SCALAR_ARRAY_OPS = ['$has', '$hasEvery', '$hasSome', '$isEmpty']; + +export class RestApiSpecGenerator { + constructor(private readonly handlerOptions: RestApiHandlerOptions) {} + + private get schema(): SchemaDef { + return this.handlerOptions.schema; + } + + private get modelNameMapping(): Record { + const mapping: Record = {}; + if (this.handlerOptions.modelNameMapping) { + for (const [k, v] of Object.entries(this.handlerOptions.modelNameMapping)) { + mapping[lowerCaseFirst(k)] = v; + } + } + return mapping; + } + + private get queryOptions() { + return this.handlerOptions?.queryOptions; + } + + generateSpec(options?: OpenApiSpecOptions): OpenAPIV3_1.Document { + return { + openapi: '3.1.0', + info: { + title: options?.title ?? 'ZenStack Generated API', + version: options?.version ?? '1.0.0', + ...(options?.description && { description: options.description }), + ...(options?.summary && { summary: options.summary }), + }, + tags: this.generateTags(), + paths: this.generatePaths(), + components: { + schemas: this.generateSchemas(), + parameters: this.generateSharedParams(), + }, + } as OpenAPIV3_1.Document; + } + + private generateTags(): OpenAPIV3_1.TagObject[] { + return getIncludedModels(this.schema, this.queryOptions).map((modelName) => ({ + name: lowerCaseFirst(modelName), + description: `${modelName} operations`, + })); + } + + private getModelPath(modelName: string): string { + const lower = lowerCaseFirst(modelName); + return this.modelNameMapping[lower] ?? lower; + } + + private generatePaths(): OpenAPIV3_1.PathsObject { + const paths: OpenAPIV3_1.PathsObject = {}; + + for (const modelName of getIncludedModels(this.schema, this.queryOptions)) { + const modelDef = this.schema.models[modelName]!; + const idFields = this.getIdFields(modelDef); + if (idFields.length === 0) continue; + + const modelPath = this.getModelPath(modelName); + const tag = lowerCaseFirst(modelName); + + // Collection: GET (list) + POST (create) + const collectionPath = this.buildCollectionPath(modelName, modelDef, tag); + if (Object.keys(collectionPath).length > 0) { + paths[`/${modelPath}`] = collectionPath; + } + + // Single resource: GET + PATCH + DELETE + const singlePath = this.buildSinglePath(modelName, tag); + if (Object.keys(singlePath).length > 0) { + paths[`/${modelPath}/{id}`] = singlePath; + } + + // Relation paths + for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) { + if (!fieldDef.relation) continue; + if (!isModelIncluded(fieldDef.type, this.queryOptions)) continue; + const relModelDef = this.schema.models[fieldDef.type]; + if (!relModelDef) continue; + const relIdFields = this.getIdFields(relModelDef); + if (relIdFields.length === 0) continue; + + // GET /{model}/{id}/{field} — fetch related + paths[`/${modelPath}/{id}/${fieldName}`] = this.buildFetchRelatedPath( + modelName, + fieldName, + fieldDef, + tag, + ); + + // Relationship management path + paths[`/${modelPath}/{id}/relationships/${fieldName}`] = this.buildRelationshipPath( + modelName, + fieldName, + fieldDef, + tag, + ); + } + } + + // Procedure paths + if (this.schema.procedures) { + for (const [procName, procDef] of Object.entries(this.schema.procedures)) { + if (!isProcedureIncluded(procName, this.queryOptions)) continue; + const isMutation = !!procDef.mutation; + if (isMutation) { + paths[`/${PROCEDURE_ROUTE_PREFIXES}/${procName}`] = { + post: this.buildProcedureOperation(procName, 'post'), + } as OpenAPIV3_1.PathItemObject; + } else { + paths[`/${PROCEDURE_ROUTE_PREFIXES}/${procName}`] = { + get: this.buildProcedureOperation(procName, 'get'), + } as OpenAPIV3_1.PathItemObject; + } + } + } + + return paths; + } + + private buildCollectionPath(modelName: string, modelDef: ModelDef, tag: string): Record { + const filterParams = this.buildFilterParams(modelName, modelDef); + + const listOp = { + tags: [tag], + summary: `List ${modelName} resources`, + operationId: `list${modelName}`, + parameters: [ + { $ref: '#/components/parameters/include' }, + { $ref: '#/components/parameters/sort' }, + { $ref: '#/components/parameters/pageOffset' }, + { $ref: '#/components/parameters/pageLimit' }, + ...filterParams, + ], + responses: { + '200': { + description: `List of ${modelName} resources`, + content: { + 'application/vnd.api+json': { + schema: { $ref: `#/components/schemas/${modelName}ListResponse` }, + }, + }, + }, + '400': ERROR_RESPONSE, + }, + }; + + const createOp = { + tags: [tag], + summary: `Create a ${modelName} resource`, + operationId: `create${modelName}`, + requestBody: { + required: true, + content: { + 'application/vnd.api+json': { + schema: { $ref: `#/components/schemas/${modelName}CreateRequest` }, + }, + }, + }, + responses: { + '201': { + description: `Created ${modelName} resource`, + content: { + 'application/vnd.api+json': { + schema: { $ref: `#/components/schemas/${modelName}Response` }, + }, + }, + }, + '400': ERROR_RESPONSE, + }, + }; + + const result: Record = {}; + if (isOperationIncluded(modelName, 'findMany', this.queryOptions)) { + result['get'] = listOp; + } + if (isOperationIncluded(modelName, 'create', this.queryOptions)) { + result['post'] = createOp; + } + return result; + } + + private buildSinglePath(modelName: string, tag: string): Record { + const idParam = { $ref: '#/components/parameters/id' }; + const result: Record = {}; + + if (isOperationIncluded(modelName, 'findUnique', this.queryOptions)) { + result['get'] = { + tags: [tag], + summary: `Get a ${modelName} resource by ID`, + operationId: `get${modelName}`, + parameters: [idParam, { $ref: '#/components/parameters/include' }], + responses: { + '200': { + description: `${modelName} resource`, + content: { + 'application/vnd.api+json': { + schema: { $ref: `#/components/schemas/${modelName}Response` }, + }, + }, + }, + '404': ERROR_RESPONSE, + }, + }; + } + + if (isOperationIncluded(modelName, 'update', this.queryOptions)) { + result['patch'] = { + tags: [tag], + summary: `Update a ${modelName} resource`, + operationId: `update${modelName}`, + parameters: [idParam], + requestBody: { + required: true, + content: { + 'application/vnd.api+json': { + schema: { $ref: `#/components/schemas/${modelName}UpdateRequest` }, + }, + }, + }, + responses: { + '200': { + description: `Updated ${modelName} resource`, + content: { + 'application/vnd.api+json': { + schema: { $ref: `#/components/schemas/${modelName}Response` }, + }, + }, + }, + '400': ERROR_RESPONSE, + '404': ERROR_RESPONSE, + }, + }; + } + + if (isOperationIncluded(modelName, 'delete', this.queryOptions)) { + result['delete'] = { + tags: [tag], + summary: `Delete a ${modelName} resource`, + operationId: `delete${modelName}`, + parameters: [idParam], + responses: { + '200': { description: 'Deleted successfully' }, + '404': ERROR_RESPONSE, + }, + }; + } + + return result; + } + + private buildFetchRelatedPath( + modelName: string, + fieldName: string, + fieldDef: FieldDef, + tag: string, + ): Record { + const isCollection = !!fieldDef.array; + const params: any[] = [{ $ref: '#/components/parameters/id' }, { $ref: '#/components/parameters/include' }]; + + if (isCollection && this.schema.models[fieldDef.type]) { + const relModelDef = this.schema.models[fieldDef.type]!; + params.push( + { $ref: '#/components/parameters/sort' }, + { $ref: '#/components/parameters/pageOffset' }, + { $ref: '#/components/parameters/pageLimit' }, + ...this.buildFilterParams(fieldDef.type, relModelDef), + ); + } + + return { + get: { + tags: [tag], + summary: `Fetch related ${fieldDef.type} for ${modelName}`, + operationId: `get${modelName}_${fieldName}`, + parameters: params, + responses: { + '200': { + description: `Related ${fieldDef.type} resource(s)`, + content: { + 'application/vnd.api+json': { + schema: isCollection + ? { $ref: `#/components/schemas/${fieldDef.type}ListResponse` } + : { $ref: `#/components/schemas/${fieldDef.type}Response` }, + }, + }, + }, + '404': ERROR_RESPONSE, + }, + }, + }; + } + + private buildRelationshipPath( + _modelName: string, + fieldName: string, + fieldDef: FieldDef, + tag: string, + ): Record { + const isCollection = !!fieldDef.array; + const idParam = { $ref: '#/components/parameters/id' }; + const relSchemaRef = isCollection + ? { $ref: '#/components/schemas/_toManyRelationshipWithLinks' } + : { $ref: '#/components/schemas/_toOneRelationshipWithLinks' }; + + const relRequestRef = isCollection + ? { $ref: '#/components/schemas/_toManyRelationshipRequest' } + : { $ref: '#/components/schemas/_toOneRelationshipRequest' }; + + const pathItem: Record = { + get: { + tags: [tag], + summary: `Fetch ${fieldName} relationship`, + operationId: `get${_modelName}_relationships_${fieldName}`, + parameters: [idParam], + responses: { + '200': { + description: `${fieldName} relationship`, + content: { 'application/vnd.api+json': { schema: relSchemaRef } }, + }, + '404': ERROR_RESPONSE, + }, + }, + put: { + tags: [tag], + summary: `Replace ${fieldName} relationship`, + operationId: `put${_modelName}_relationships_${fieldName}`, + parameters: [idParam], + requestBody: { + required: true, + content: { 'application/vnd.api+json': { schema: relRequestRef } }, + }, + responses: { + '200': { description: 'Relationship updated' }, + '400': ERROR_RESPONSE, + }, + }, + patch: { + tags: [tag], + summary: `Update ${fieldName} relationship`, + operationId: `patch${_modelName}_relationships_${fieldName}`, + parameters: [idParam], + requestBody: { + required: true, + content: { 'application/vnd.api+json': { schema: relRequestRef } }, + }, + responses: { + '200': { description: 'Relationship updated' }, + '400': ERROR_RESPONSE, + }, + }, + }; + + if (isCollection) { + pathItem['post'] = { + tags: [tag], + summary: `Add to ${fieldName} collection relationship`, + operationId: `post${_modelName}_relationships_${fieldName}`, + parameters: [idParam], + requestBody: { + required: true, + content: { + 'application/vnd.api+json': { + schema: { $ref: '#/components/schemas/_toManyRelationshipRequest' }, + }, + }, + }, + responses: { + '200': { description: 'Added to relationship collection' }, + '400': ERROR_RESPONSE, + }, + }; + } + + return pathItem; + } + + private buildProcedureOperation(procName: string, method: 'get' | 'post'): Record { + const op: Record = { + tags: ['$procs'], + summary: `Execute procedure ${procName}`, + operationId: `proc_${procName}`, + responses: { + '200': { description: `Result of ${procName}` }, + '400': ERROR_RESPONSE, + }, + }; + + if (method === 'get') { + op['parameters'] = [ + { + name: 'q', + in: 'query', + description: 'Procedure arguments as JSON', + schema: { type: 'string' }, + }, + ]; + } else { + op['requestBody'] = { + content: { + 'application/json': { + schema: { type: 'object' }, + }, + }, + }; + } + + return op; + } + + private buildFilterParams(modelName: string, modelDef: ModelDef): ParameterObject[] { + const params: ParameterObject[] = []; + const idFieldNames = new Set(modelDef.idFields); + + // id filter (Equality kind) + if (isFilterKindIncluded(modelName, 'id', 'Equality', this.queryOptions)) { + params.push({ + name: 'filter[id]', + in: 'query', + schema: { type: 'string' }, + description: `Filter by ${modelName} ID`, + }); + } + + for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) { + if (fieldDef.relation) continue; + if (idFieldNames.has(fieldName)) continue; + + const type = fieldDef.type; + + // Equality filter + if (isFilterKindIncluded(modelName, fieldName, 'Equality', this.queryOptions)) { + params.push({ + name: `filter[${fieldName}]`, + in: 'query', + schema: { type: 'string' }, + description: `Filter by ${fieldName}`, + }); + } + + if (type === 'String' && isFilterKindIncluded(modelName, fieldName, 'Like', this.queryOptions)) { + for (const op of SCALAR_STRING_OPS) { + params.push({ + name: `filter[${fieldName}][${op}]`, + in: 'query', + schema: { type: 'string' }, + }); + } + } else if ( + (type === 'Int' || + type === 'Float' || + type === 'BigInt' || + type === 'Decimal' || + type === 'DateTime') && + isFilterKindIncluded(modelName, fieldName, 'Range', this.queryOptions) + ) { + for (const op of SCALAR_COMPARABLE_OPS) { + params.push({ + name: `filter[${fieldName}][${op}]`, + in: 'query', + schema: { type: 'string' }, + }); + } + } + + if (fieldDef.array && isFilterKindIncluded(modelName, fieldName, 'List', this.queryOptions)) { + for (const op of SCALAR_ARRAY_OPS) { + params.push({ + name: `filter[${fieldName}][${op}]`, + in: 'query', + schema: { type: 'string' }, + }); + } + } + } + + return params; + } + + private generateSchemas(): Record { + const schemas: Record = {}; + + // Shared JSON:API components + Object.assign(schemas, this.buildSharedSchemas()); + + // Per-enum schemas + if (this.schema.enums) { + for (const [_enumName, enumDef] of Object.entries(this.schema.enums)) { + schemas[_enumName] = this.buildEnumSchema(enumDef); + } + } + + // Per-typeDef schemas + if (this.schema.typeDefs) { + for (const [typeName, typeDef] of Object.entries(this.schema.typeDefs)) { + schemas[typeName] = this.buildTypeDefSchema(typeDef); + } + } + + // Per-model schemas + for (const modelName of getIncludedModels(this.schema, this.queryOptions)) { + const modelDef = this.schema.models[modelName]!; + const idFields = this.getIdFields(modelDef); + if (idFields.length === 0) continue; + + schemas[modelName] = this.buildModelReadSchema(modelName, modelDef); + schemas[`${modelName}CreateRequest`] = this.buildCreateRequestSchema(modelName, modelDef); + schemas[`${modelName}UpdateRequest`] = this.buildUpdateRequestSchema(modelDef); + schemas[`${modelName}Response`] = this.buildModelResponseSchema(modelName); + schemas[`${modelName}ListResponse`] = this.buildModelListResponseSchema(modelName); + } + + return schemas; + } + + private buildSharedSchemas(): Record { + const nullableString: SchemaObject = { oneOf: [{ type: 'string' }, { type: 'null' }] }; + return { + _jsonapi: { + type: 'object', + properties: { + version: { type: 'string' }, + meta: { type: 'object' }, + }, + }, + _meta: { + type: 'object', + additionalProperties: true, + }, + _links: { + type: 'object', + properties: { + self: { type: 'string' }, + related: { type: 'string' }, + }, + }, + _pagination: { + type: 'object', + properties: { + first: nullableString, + last: nullableString, + prev: nullableString, + next: nullableString, + }, + }, + _errors: { + type: 'array', + items: { + type: 'object', + properties: { + status: { type: 'integer' }, + code: { type: 'string' }, + title: { type: 'string' }, + detail: { type: 'string' }, + }, + required: ['status', 'title'], + }, + }, + _errorResponse: { + type: 'object', + properties: { + errors: { $ref: '#/components/schemas/_errors' }, + }, + required: ['errors'], + }, + _resourceIdentifier: { + type: 'object', + properties: { + type: { type: 'string' }, + id: { type: 'string' }, + }, + required: ['type', 'id'], + }, + _resource: { + type: 'object', + properties: { + type: { type: 'string' }, + id: { type: 'string' }, + attributes: { type: 'object' }, + relationships: { type: 'object' }, + links: { $ref: '#/components/schemas/_links' }, + meta: { $ref: '#/components/schemas/_meta' }, + }, + required: ['type', 'id'], + }, + _relationLinks: { + type: 'object', + properties: { + self: { type: 'string' }, + related: { type: 'string' }, + }, + }, + _pagedRelationLinks: { + type: 'object', + allOf: [{ $ref: '#/components/schemas/_relationLinks' }, { $ref: '#/components/schemas/_pagination' }], + }, + _toOneRelationship: { + type: 'object', + properties: { + data: { + oneOf: [{ $ref: '#/components/schemas/_resourceIdentifier' }, { type: 'null' }], + }, + }, + }, + _toManyRelationship: { + type: 'object', + properties: { + data: { + type: 'array', + items: { $ref: '#/components/schemas/_resourceIdentifier' }, + }, + }, + }, + _toOneRelationshipWithLinks: { + type: 'object', + allOf: [ + { $ref: '#/components/schemas/_toOneRelationship' }, + { + properties: { + links: { $ref: '#/components/schemas/_relationLinks' }, + }, + }, + ], + }, + _toManyRelationshipWithLinks: { + type: 'object', + allOf: [ + { $ref: '#/components/schemas/_toManyRelationship' }, + { + properties: { + links: { $ref: '#/components/schemas/_pagedRelationLinks' }, + }, + }, + ], + }, + _toManyRelationshipRequest: { + type: 'object', + properties: { + data: { + type: 'array', + items: { $ref: '#/components/schemas/_resourceIdentifier' }, + }, + }, + required: ['data'], + }, + _toOneRelationshipRequest: { + type: 'object', + properties: { + data: { + oneOf: [{ $ref: '#/components/schemas/_resourceIdentifier' }, { type: 'null' }], + }, + }, + required: ['data'], + }, + _toManyRelationshipResponse: { + type: 'object', + properties: { + data: { + type: 'array', + items: { $ref: '#/components/schemas/_resourceIdentifier' }, + }, + links: { $ref: '#/components/schemas/_pagedRelationLinks' }, + meta: { $ref: '#/components/schemas/_meta' }, + }, + }, + _toOneRelationshipResponse: { + type: 'object', + properties: { + data: { + oneOf: [{ $ref: '#/components/schemas/_resourceIdentifier' }, { type: 'null' }], + }, + links: { $ref: '#/components/schemas/_relationLinks' }, + meta: { $ref: '#/components/schemas/_meta' }, + }, + }, + }; + } + + private buildEnumSchema(enumDef: EnumDef): SchemaObject { + return { + type: 'string', + enum: Object.values(enumDef.values), + }; + } + + private buildTypeDefSchema(typeDef: TypeDefDef): SchemaObject { + const properties: Record = {}; + const required: string[] = []; + + for (const [fieldName, fieldDef] of Object.entries(typeDef.fields)) { + properties[fieldName] = this.fieldToSchema(fieldDef); + if (!fieldDef.optional && !fieldDef.array) { + required.push(fieldName); + } + } + + const result: SchemaObject = { type: 'object', properties }; + if (required.length > 0) { + result.required = required; + } + return result; + } + + private buildModelReadSchema(modelName: string, modelDef: ModelDef): SchemaObject { + const attrProperties: Record = {}; + const attrRequired: string[] = []; + const relProperties: Record = {}; + + for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) { + if (fieldDef.omit) continue; + if (isFieldOmitted(modelName, fieldName, this.queryOptions)) continue; + if (fieldDef.relation && !isModelIncluded(fieldDef.type, this.queryOptions)) continue; + + if (fieldDef.relation) { + const relRef: SchemaObject | ReferenceObject = fieldDef.array + ? { $ref: '#/components/schemas/_toManyRelationshipWithLinks' } + : { $ref: '#/components/schemas/_toOneRelationshipWithLinks' }; + relProperties[fieldName] = fieldDef.optional ? { oneOf: [{ type: 'null' }, relRef] } : relRef; + } else { + const schema = this.fieldToSchema(fieldDef); + const fieldDescription = getMetaDescription(fieldDef.attributes); + if (fieldDescription && !('$ref' in schema)) { + schema.description = fieldDescription; + } + attrProperties[fieldName] = schema; + + if (!fieldDef.optional && !fieldDef.array) { + attrRequired.push(fieldName); + } + } + } + + const properties: Record = {}; + + if (Object.keys(attrProperties).length > 0) { + const attrSchema: SchemaObject = { type: 'object', properties: attrProperties }; + if (attrRequired.length > 0) attrSchema.required = attrRequired; + properties['attributes'] = attrSchema; + } + + if (Object.keys(relProperties).length > 0) { + properties['relationships'] = { type: 'object', properties: relProperties }; + } + + const result: SchemaObject = { type: 'object', properties }; + const description = getMetaDescription(modelDef.attributes); + if (description) { + result.description = description; + } + return result; + } + + private buildCreateRequestSchema(_modelName: string, modelDef: ModelDef): SchemaObject { + const idFieldNames = new Set(modelDef.idFields); + const attributes: Record = {}; + const attrRequired: string[] = []; + const relationships: Record = {}; + + for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) { + if (fieldDef.updatedAt) continue; + if (fieldDef.foreignKeyFor) continue; + // Skip auto-generated id fields + if (idFieldNames.has(fieldName) && fieldDef.default !== undefined) continue; + if (fieldDef.relation && !isModelIncluded(fieldDef.type, this.queryOptions)) continue; + + if (fieldDef.relation) { + relationships[fieldName] = fieldDef.array + ? { + type: 'object', + properties: { + data: { + type: 'array', + items: { $ref: '#/components/schemas/_resourceIdentifier' }, + }, + }, + } + : { + type: 'object', + properties: { + data: { $ref: '#/components/schemas/_resourceIdentifier' }, + }, + }; + } else { + attributes[fieldName] = this.fieldToSchema(fieldDef); + if (!fieldDef.optional && fieldDef.default === undefined && !fieldDef.array) { + attrRequired.push(fieldName); + } + } + } + + const dataProperties: Record = { + type: { type: 'string' }, + }; + + if (Object.keys(attributes).length > 0) { + const attrSchema: SchemaObject = { type: 'object', properties: attributes }; + if (attrRequired.length > 0) attrSchema.required = attrRequired; + dataProperties['attributes'] = attrSchema; + } + + if (Object.keys(relationships).length > 0) { + dataProperties['relationships'] = { type: 'object', properties: relationships }; + } + + return { + type: 'object', + properties: { + data: { + type: 'object', + properties: dataProperties, + required: ['type'], + }, + }, + required: ['data'], + }; + } + + private buildUpdateRequestSchema(modelDef: ModelDef): SchemaObject { + const attributes: Record = {}; + const relationships: Record = {}; + + for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) { + if (fieldDef.updatedAt) continue; + if (fieldDef.foreignKeyFor) continue; + if (fieldDef.relation && !isModelIncluded(fieldDef.type, this.queryOptions)) continue; + + if (fieldDef.relation) { + relationships[fieldName] = fieldDef.array + ? { + type: 'object', + properties: { + data: { + type: 'array', + items: { $ref: '#/components/schemas/_resourceIdentifier' }, + }, + }, + } + : { + type: 'object', + properties: { + data: { $ref: '#/components/schemas/_resourceIdentifier' }, + }, + }; + } else { + attributes[fieldName] = this.fieldToSchema(fieldDef); + } + } + + const dataProperties: Record = { + type: { type: 'string' }, + id: { type: 'string' }, + }; + + if (Object.keys(attributes).length > 0) { + dataProperties['attributes'] = { type: 'object', properties: attributes }; + } + + if (Object.keys(relationships).length > 0) { + dataProperties['relationships'] = { type: 'object', properties: relationships }; + } + + return { + type: 'object', + properties: { + data: { + type: 'object', + properties: dataProperties, + required: ['type', 'id'], + }, + }, + required: ['data'], + }; + } + + private buildModelResponseSchema(modelName: string): SchemaObject { + return { + type: 'object', + properties: { + jsonapi: { $ref: '#/components/schemas/_jsonapi' }, + data: { + allOf: [{ $ref: `#/components/schemas/${modelName}` }, { $ref: '#/components/schemas/_resource' }], + }, + meta: { $ref: '#/components/schemas/_meta' }, + }, + }; + } + + private buildModelListResponseSchema(modelName: string): SchemaObject { + return { + type: 'object', + properties: { + jsonapi: { $ref: '#/components/schemas/_jsonapi' }, + data: { + type: 'array', + items: { + allOf: [ + { $ref: `#/components/schemas/${modelName}` }, + { $ref: '#/components/schemas/_resource' }, + ], + }, + }, + links: { + allOf: [{ $ref: '#/components/schemas/_pagination' }, { $ref: '#/components/schemas/_links' }], + }, + meta: { $ref: '#/components/schemas/_meta' }, + }, + }; + } + + private generateSharedParams(): Record { + return { + id: { + name: 'id', + in: 'path', + required: true, + schema: { type: 'string' }, + description: 'Resource ID', + }, + include: { + name: 'include', + in: 'query', + schema: { type: 'string' }, + description: 'Comma-separated list of relationships to include', + }, + sort: { + name: 'sort', + in: 'query', + schema: { type: 'string' }, + description: 'Comma-separated list of fields to sort by. Prefix with - for descending', + }, + pageOffset: { + name: 'page[offset]', + in: 'query', + schema: { type: 'integer', minimum: 0 }, + description: 'Page offset', + }, + pageLimit: { + name: 'page[limit]', + in: 'query', + schema: { type: 'integer', minimum: 1 }, + description: 'Page limit', + }, + }; + } + + private fieldToSchema(fieldDef: FieldDef): SchemaObject | ReferenceObject { + const baseSchema = this.typeToSchema(fieldDef.type); + if (fieldDef.array) { + return { type: 'array', items: baseSchema }; + } + if (fieldDef.optional) { + return { oneOf: [baseSchema, { type: 'null' }] }; + } + return baseSchema; + } + + private typeToSchema(type: string): SchemaObject | ReferenceObject { + switch (type) { + case 'String': + return { type: 'string' }; + case 'Int': + case 'BigInt': + return { type: 'integer' }; + case 'Float': + return { type: 'number' }; + case 'Decimal': + return { oneOf: [{ type: 'number' }, { type: 'string' }] }; + case 'Boolean': + return { type: 'boolean' }; + case 'DateTime': + return { type: 'string', format: 'date-time' }; + case 'Bytes': + return { type: 'string', format: 'byte' }; + case 'Json': + case 'Unsupported': + return {}; + default: + return { $ref: `#/components/schemas/${type}` }; + } + } + + private getIdFields(modelDef: ModelDef): FieldDef[] { + return modelDef.idFields.map((name) => modelDef.fields[name]).filter((f): f is FieldDef => f !== undefined); + } +} diff --git a/packages/server/src/api/rpc/index.ts b/packages/server/src/api/rpc/index.ts index 7b3cbcb39..712170f9f 100644 --- a/packages/server/src/api/rpc/index.ts +++ b/packages/server/src/api/rpc/index.ts @@ -7,7 +7,8 @@ import z from 'zod'; import { fromError } from 'zod-validation-error/v4'; import type { ApiHandler, LogConfig, RequestContext, Response } from '../../types'; import { getProcedureDef, mapProcedureArgs, PROCEDURE_ROUTE_PREFIXES } from '../common/procedures'; -import { loggerSchema } from '../common/schemas'; +import { loggerSchema, queryOptionsSchema } from '../common/schemas'; +import type { CommonHandlerOptions } from '../common/types'; import { processSuperJsonRequestPayload, unmarshalQ } from '../common/utils'; import { log, registerCustomSerializers } from '../utils'; @@ -29,7 +30,7 @@ export type RPCApiHandlerOptions = { * Logging configuration */ log?: LogConfig; -}; +} & CommonHandlerOptions; /** * RPC style API request handler that mirrors the ZenStackClient API @@ -40,7 +41,11 @@ export class RPCApiHandler implements ApiH } private validateOptions(options: RPCApiHandlerOptions) { - const schema = z.strictObject({ schema: z.object(), log: loggerSchema.optional() }); + const schema = z.strictObject({ + schema: z.object(), + log: loggerSchema.optional(), + queryOptions: queryOptionsSchema.optional(), + }); const parseResult = schema.safeParse(options); if (!parseResult.success) { throw new Error(`Invalid options: ${fromError(parseResult.error)}`); @@ -240,7 +245,11 @@ export class RPCApiHandler implements ApiH if (!this.isValidModel(client, lowerCaseFirst(itemModel))) { return this.makeBadInputErrorResponse(`operation at index ${i} has unknown model: ${itemModel}`); } - if (itemArgs !== undefined && itemArgs !== null && (typeof itemArgs !== 'object' || Array.isArray(itemArgs))) { + if ( + itemArgs !== undefined && + itemArgs !== null && + (typeof itemArgs !== 'object' || Array.isArray(itemArgs)) + ) { return this.makeBadInputErrorResponse(`operation at index ${i} has invalid "args" field`); } diff --git a/packages/server/test/openapi/baseline/rest.baseline.yaml b/packages/server/test/openapi/baseline/rest.baseline.yaml new file mode 100644 index 000000000..0bd8177b0 --- /dev/null +++ b/packages/server/test/openapi/baseline/rest.baseline.yaml @@ -0,0 +1,4087 @@ +openapi: 3.1.0 +info: + title: ZenStack Generated API + version: 1.0.0 +tags: + - name: user + description: User operations + - name: profile + description: Profile operations + - name: post + description: Post operations + - name: comment + description: Comment operations + - name: setting + description: Setting operations + - name: postLike + description: PostLike operations + - name: postLikeInfo + description: PostLikeInfo operations +paths: + /user: + get: + tags: + - user + summary: List User resources + operationId: listUser + parameters: + - $ref: "#/components/parameters/include" + - $ref: "#/components/parameters/sort" + - $ref: "#/components/parameters/pageOffset" + - $ref: "#/components/parameters/pageLimit" + - name: filter[id] + in: query + schema: + type: string + description: Filter by User ID + - name: filter[createdAt] + in: query + schema: + type: string + description: Filter by createdAt + - name: filter[createdAt][$lt] + in: query + schema: + type: string + - name: filter[createdAt][$lte] + in: query + schema: + type: string + - name: filter[createdAt][$gt] + in: query + schema: + type: string + - name: filter[createdAt][$gte] + in: query + schema: + type: string + - name: filter[updatedAt] + in: query + schema: + type: string + description: Filter by updatedAt + - name: filter[updatedAt][$lt] + in: query + schema: + type: string + - name: filter[updatedAt][$lte] + in: query + schema: + type: string + - name: filter[updatedAt][$gt] + in: query + schema: + type: string + - name: filter[updatedAt][$gte] + in: query + schema: + type: string + - name: filter[email] + in: query + schema: + type: string + description: Filter by email + - name: filter[email][$contains] + in: query + schema: + type: string + - name: filter[email][$icontains] + in: query + schema: + type: string + - name: filter[email][$search] + in: query + schema: + type: string + - name: filter[email][$startsWith] + in: query + schema: + type: string + - name: filter[email][$endsWith] + in: query + schema: + type: string + - name: filter[address] + in: query + schema: + type: string + description: Filter by address + - name: filter[someJson] + in: query + schema: + type: string + description: Filter by someJson + responses: + "200": + description: List of User resources + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/UserListResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + post: + tags: + - user + summary: Create a User resource + operationId: createUser + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/UserCreateRequest" + responses: + "201": + description: Created User resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/UserResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /user/{id}: + get: + tags: + - user + summary: Get a User resource by ID + operationId: getUser + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + responses: + "200": + description: User resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/UserResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - user + summary: Update a User resource + operationId: updateUser + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/UserUpdateRequest" + responses: + "200": + description: Updated User resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/UserResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + delete: + tags: + - user + summary: Delete a User resource + operationId: deleteUser + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: Deleted successfully + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /user/{id}/posts: + get: + tags: + - user + summary: Fetch related Post for User + operationId: getUser_posts + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + - $ref: "#/components/parameters/sort" + - $ref: "#/components/parameters/pageOffset" + - $ref: "#/components/parameters/pageLimit" + - name: filter[id] + in: query + schema: + type: string + description: Filter by Post ID + - name: filter[createdAt] + in: query + schema: + type: string + description: Filter by createdAt + - name: filter[createdAt][$lt] + in: query + schema: + type: string + - name: filter[createdAt][$lte] + in: query + schema: + type: string + - name: filter[createdAt][$gt] + in: query + schema: + type: string + - name: filter[createdAt][$gte] + in: query + schema: + type: string + - name: filter[updatedAt] + in: query + schema: + type: string + description: Filter by updatedAt + - name: filter[updatedAt][$lt] + in: query + schema: + type: string + - name: filter[updatedAt][$lte] + in: query + schema: + type: string + - name: filter[updatedAt][$gt] + in: query + schema: + type: string + - name: filter[updatedAt][$gte] + in: query + schema: + type: string + - name: filter[title] + in: query + schema: + type: string + description: Filter by title + - name: filter[title][$contains] + in: query + schema: + type: string + - name: filter[title][$icontains] + in: query + schema: + type: string + - name: filter[title][$search] + in: query + schema: + type: string + - name: filter[title][$startsWith] + in: query + schema: + type: string + - name: filter[title][$endsWith] + in: query + schema: + type: string + - name: filter[authorId] + in: query + schema: + type: string + description: Filter by authorId + - name: filter[authorId][$contains] + in: query + schema: + type: string + - name: filter[authorId][$icontains] + in: query + schema: + type: string + - name: filter[authorId][$search] + in: query + schema: + type: string + - name: filter[authorId][$startsWith] + in: query + schema: + type: string + - name: filter[authorId][$endsWith] + in: query + schema: + type: string + - name: filter[published] + in: query + schema: + type: string + description: Filter by published + - name: filter[publishedAt] + in: query + schema: + type: string + description: Filter by publishedAt + - name: filter[publishedAt][$lt] + in: query + schema: + type: string + - name: filter[publishedAt][$lte] + in: query + schema: + type: string + - name: filter[publishedAt][$gt] + in: query + schema: + type: string + - name: filter[publishedAt][$gte] + in: query + schema: + type: string + - name: filter[viewCount] + in: query + schema: + type: string + description: Filter by viewCount + - name: filter[viewCount][$lt] + in: query + schema: + type: string + - name: filter[viewCount][$lte] + in: query + schema: + type: string + - name: filter[viewCount][$gt] + in: query + schema: + type: string + - name: filter[viewCount][$gte] + in: query + schema: + type: string + responses: + "200": + description: Related Post resource(s) + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostListResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /user/{id}/relationships/posts: + get: + tags: + - user + summary: Fetch posts relationship + operationId: getUser_relationships_posts + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: posts relationship + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipWithLinks" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + put: + tags: + - user + summary: Replace posts relationship + operationId: putUser_relationships_posts + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - user + summary: Update posts relationship + operationId: patchUser_relationships_posts + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + post: + tags: + - user + summary: Add to posts collection relationship + operationId: postUser_relationships_posts + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipRequest" + responses: + "200": + description: Added to relationship collection + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /user/{id}/likes: + get: + tags: + - user + summary: Fetch related PostLike for User + operationId: getUser_likes + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + - $ref: "#/components/parameters/sort" + - $ref: "#/components/parameters/pageOffset" + - $ref: "#/components/parameters/pageLimit" + - name: filter[id] + in: query + schema: + type: string + description: Filter by PostLike ID + - name: filter[superLike] + in: query + schema: + type: string + description: Filter by superLike + responses: + "200": + description: Related PostLike resource(s) + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostLikeListResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /user/{id}/relationships/likes: + get: + tags: + - user + summary: Fetch likes relationship + operationId: getUser_relationships_likes + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: likes relationship + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipWithLinks" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + put: + tags: + - user + summary: Replace likes relationship + operationId: putUser_relationships_likes + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - user + summary: Update likes relationship + operationId: patchUser_relationships_likes + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + post: + tags: + - user + summary: Add to likes collection relationship + operationId: postUser_relationships_likes + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipRequest" + responses: + "200": + description: Added to relationship collection + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /user/{id}/profile: + get: + tags: + - user + summary: Fetch related Profile for User + operationId: getUser_profile + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + responses: + "200": + description: Related Profile resource(s) + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/ProfileResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /user/{id}/relationships/profile: + get: + tags: + - user + summary: Fetch profile relationship + operationId: getUser_relationships_profile + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: profile relationship + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipWithLinks" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + put: + tags: + - user + summary: Replace profile relationship + operationId: putUser_relationships_profile + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - user + summary: Update profile relationship + operationId: patchUser_relationships_profile + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /profile: + get: + tags: + - profile + summary: List Profile resources + operationId: listProfile + parameters: + - $ref: "#/components/parameters/include" + - $ref: "#/components/parameters/sort" + - $ref: "#/components/parameters/pageOffset" + - $ref: "#/components/parameters/pageLimit" + - name: filter[id] + in: query + schema: + type: string + description: Filter by Profile ID + - name: filter[gender] + in: query + schema: + type: string + description: Filter by gender + - name: filter[gender][$contains] + in: query + schema: + type: string + - name: filter[gender][$icontains] + in: query + schema: + type: string + - name: filter[gender][$search] + in: query + schema: + type: string + - name: filter[gender][$startsWith] + in: query + schema: + type: string + - name: filter[gender][$endsWith] + in: query + schema: + type: string + - name: filter[userId] + in: query + schema: + type: string + description: Filter by userId + - name: filter[userId][$contains] + in: query + schema: + type: string + - name: filter[userId][$icontains] + in: query + schema: + type: string + - name: filter[userId][$search] + in: query + schema: + type: string + - name: filter[userId][$startsWith] + in: query + schema: + type: string + - name: filter[userId][$endsWith] + in: query + schema: + type: string + responses: + "200": + description: List of Profile resources + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/ProfileListResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + post: + tags: + - profile + summary: Create a Profile resource + operationId: createProfile + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/ProfileCreateRequest" + responses: + "201": + description: Created Profile resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/ProfileResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /profile/{id}: + get: + tags: + - profile + summary: Get a Profile resource by ID + operationId: getProfile + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + responses: + "200": + description: Profile resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/ProfileResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - profile + summary: Update a Profile resource + operationId: updateProfile + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/ProfileUpdateRequest" + responses: + "200": + description: Updated Profile resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/ProfileResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + delete: + tags: + - profile + summary: Delete a Profile resource + operationId: deleteProfile + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: Deleted successfully + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /profile/{id}/user: + get: + tags: + - profile + summary: Fetch related User for Profile + operationId: getProfile_user + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + responses: + "200": + description: Related User resource(s) + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/UserResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /profile/{id}/relationships/user: + get: + tags: + - profile + summary: Fetch user relationship + operationId: getProfile_relationships_user + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: user relationship + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipWithLinks" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + put: + tags: + - profile + summary: Replace user relationship + operationId: putProfile_relationships_user + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - profile + summary: Update user relationship + operationId: patchProfile_relationships_user + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /post: + get: + tags: + - post + summary: List Post resources + operationId: listPost + parameters: + - $ref: "#/components/parameters/include" + - $ref: "#/components/parameters/sort" + - $ref: "#/components/parameters/pageOffset" + - $ref: "#/components/parameters/pageLimit" + - name: filter[id] + in: query + schema: + type: string + description: Filter by Post ID + - name: filter[createdAt] + in: query + schema: + type: string + description: Filter by createdAt + - name: filter[createdAt][$lt] + in: query + schema: + type: string + - name: filter[createdAt][$lte] + in: query + schema: + type: string + - name: filter[createdAt][$gt] + in: query + schema: + type: string + - name: filter[createdAt][$gte] + in: query + schema: + type: string + - name: filter[updatedAt] + in: query + schema: + type: string + description: Filter by updatedAt + - name: filter[updatedAt][$lt] + in: query + schema: + type: string + - name: filter[updatedAt][$lte] + in: query + schema: + type: string + - name: filter[updatedAt][$gt] + in: query + schema: + type: string + - name: filter[updatedAt][$gte] + in: query + schema: + type: string + - name: filter[title] + in: query + schema: + type: string + description: Filter by title + - name: filter[title][$contains] + in: query + schema: + type: string + - name: filter[title][$icontains] + in: query + schema: + type: string + - name: filter[title][$search] + in: query + schema: + type: string + - name: filter[title][$startsWith] + in: query + schema: + type: string + - name: filter[title][$endsWith] + in: query + schema: + type: string + - name: filter[authorId] + in: query + schema: + type: string + description: Filter by authorId + - name: filter[authorId][$contains] + in: query + schema: + type: string + - name: filter[authorId][$icontains] + in: query + schema: + type: string + - name: filter[authorId][$search] + in: query + schema: + type: string + - name: filter[authorId][$startsWith] + in: query + schema: + type: string + - name: filter[authorId][$endsWith] + in: query + schema: + type: string + - name: filter[published] + in: query + schema: + type: string + description: Filter by published + - name: filter[publishedAt] + in: query + schema: + type: string + description: Filter by publishedAt + - name: filter[publishedAt][$lt] + in: query + schema: + type: string + - name: filter[publishedAt][$lte] + in: query + schema: + type: string + - name: filter[publishedAt][$gt] + in: query + schema: + type: string + - name: filter[publishedAt][$gte] + in: query + schema: + type: string + - name: filter[viewCount] + in: query + schema: + type: string + description: Filter by viewCount + - name: filter[viewCount][$lt] + in: query + schema: + type: string + - name: filter[viewCount][$lte] + in: query + schema: + type: string + - name: filter[viewCount][$gt] + in: query + schema: + type: string + - name: filter[viewCount][$gte] + in: query + schema: + type: string + responses: + "200": + description: List of Post resources + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostListResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + post: + tags: + - post + summary: Create a Post resource + operationId: createPost + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostCreateRequest" + responses: + "201": + description: Created Post resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /post/{id}: + get: + tags: + - post + summary: Get a Post resource by ID + operationId: getPost + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + responses: + "200": + description: Post resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - post + summary: Update a Post resource + operationId: updatePost + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostUpdateRequest" + responses: + "200": + description: Updated Post resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + delete: + tags: + - post + summary: Delete a Post resource + operationId: deletePost + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: Deleted successfully + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /post/{id}/author: + get: + tags: + - post + summary: Fetch related User for Post + operationId: getPost_author + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + responses: + "200": + description: Related User resource(s) + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/UserResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /post/{id}/relationships/author: + get: + tags: + - post + summary: Fetch author relationship + operationId: getPost_relationships_author + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: author relationship + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipWithLinks" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + put: + tags: + - post + summary: Replace author relationship + operationId: putPost_relationships_author + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - post + summary: Update author relationship + operationId: patchPost_relationships_author + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /post/{id}/comments: + get: + tags: + - post + summary: Fetch related Comment for Post + operationId: getPost_comments + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + - $ref: "#/components/parameters/sort" + - $ref: "#/components/parameters/pageOffset" + - $ref: "#/components/parameters/pageLimit" + - name: filter[id] + in: query + schema: + type: string + description: Filter by Comment ID + - name: filter[postId] + in: query + schema: + type: string + description: Filter by postId + - name: filter[postId][$lt] + in: query + schema: + type: string + - name: filter[postId][$lte] + in: query + schema: + type: string + - name: filter[postId][$gt] + in: query + schema: + type: string + - name: filter[postId][$gte] + in: query + schema: + type: string + - name: filter[content] + in: query + schema: + type: string + description: Filter by content + - name: filter[content][$contains] + in: query + schema: + type: string + - name: filter[content][$icontains] + in: query + schema: + type: string + - name: filter[content][$search] + in: query + schema: + type: string + - name: filter[content][$startsWith] + in: query + schema: + type: string + - name: filter[content][$endsWith] + in: query + schema: + type: string + responses: + "200": + description: Related Comment resource(s) + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/CommentListResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /post/{id}/relationships/comments: + get: + tags: + - post + summary: Fetch comments relationship + operationId: getPost_relationships_comments + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: comments relationship + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipWithLinks" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + put: + tags: + - post + summary: Replace comments relationship + operationId: putPost_relationships_comments + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - post + summary: Update comments relationship + operationId: patchPost_relationships_comments + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + post: + tags: + - post + summary: Add to comments collection relationship + operationId: postPost_relationships_comments + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipRequest" + responses: + "200": + description: Added to relationship collection + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /post/{id}/likes: + get: + tags: + - post + summary: Fetch related PostLike for Post + operationId: getPost_likes + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + - $ref: "#/components/parameters/sort" + - $ref: "#/components/parameters/pageOffset" + - $ref: "#/components/parameters/pageLimit" + - name: filter[id] + in: query + schema: + type: string + description: Filter by PostLike ID + - name: filter[superLike] + in: query + schema: + type: string + description: Filter by superLike + responses: + "200": + description: Related PostLike resource(s) + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostLikeListResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /post/{id}/relationships/likes: + get: + tags: + - post + summary: Fetch likes relationship + operationId: getPost_relationships_likes + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: likes relationship + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipWithLinks" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + put: + tags: + - post + summary: Replace likes relationship + operationId: putPost_relationships_likes + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - post + summary: Update likes relationship + operationId: patchPost_relationships_likes + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + post: + tags: + - post + summary: Add to likes collection relationship + operationId: postPost_relationships_likes + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipRequest" + responses: + "200": + description: Added to relationship collection + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /post/{id}/setting: + get: + tags: + - post + summary: Fetch related Setting for Post + operationId: getPost_setting + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + responses: + "200": + description: Related Setting resource(s) + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/SettingResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /post/{id}/relationships/setting: + get: + tags: + - post + summary: Fetch setting relationship + operationId: getPost_relationships_setting + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: setting relationship + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipWithLinks" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + put: + tags: + - post + summary: Replace setting relationship + operationId: putPost_relationships_setting + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - post + summary: Update setting relationship + operationId: patchPost_relationships_setting + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /comment: + get: + tags: + - comment + summary: List Comment resources + operationId: listComment + parameters: + - $ref: "#/components/parameters/include" + - $ref: "#/components/parameters/sort" + - $ref: "#/components/parameters/pageOffset" + - $ref: "#/components/parameters/pageLimit" + - name: filter[id] + in: query + schema: + type: string + description: Filter by Comment ID + - name: filter[postId] + in: query + schema: + type: string + description: Filter by postId + - name: filter[postId][$lt] + in: query + schema: + type: string + - name: filter[postId][$lte] + in: query + schema: + type: string + - name: filter[postId][$gt] + in: query + schema: + type: string + - name: filter[postId][$gte] + in: query + schema: + type: string + - name: filter[content] + in: query + schema: + type: string + description: Filter by content + - name: filter[content][$contains] + in: query + schema: + type: string + - name: filter[content][$icontains] + in: query + schema: + type: string + - name: filter[content][$search] + in: query + schema: + type: string + - name: filter[content][$startsWith] + in: query + schema: + type: string + - name: filter[content][$endsWith] + in: query + schema: + type: string + responses: + "200": + description: List of Comment resources + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/CommentListResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + post: + tags: + - comment + summary: Create a Comment resource + operationId: createComment + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/CommentCreateRequest" + responses: + "201": + description: Created Comment resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/CommentResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /comment/{id}: + get: + tags: + - comment + summary: Get a Comment resource by ID + operationId: getComment + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + responses: + "200": + description: Comment resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/CommentResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - comment + summary: Update a Comment resource + operationId: updateComment + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/CommentUpdateRequest" + responses: + "200": + description: Updated Comment resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/CommentResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + delete: + tags: + - comment + summary: Delete a Comment resource + operationId: deleteComment + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: Deleted successfully + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /comment/{id}/post: + get: + tags: + - comment + summary: Fetch related Post for Comment + operationId: getComment_post + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + responses: + "200": + description: Related Post resource(s) + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /comment/{id}/relationships/post: + get: + tags: + - comment + summary: Fetch post relationship + operationId: getComment_relationships_post + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: post relationship + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipWithLinks" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + put: + tags: + - comment + summary: Replace post relationship + operationId: putComment_relationships_post + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - comment + summary: Update post relationship + operationId: patchComment_relationships_post + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /setting: + get: + tags: + - setting + summary: List Setting resources + operationId: listSetting + parameters: + - $ref: "#/components/parameters/include" + - $ref: "#/components/parameters/sort" + - $ref: "#/components/parameters/pageOffset" + - $ref: "#/components/parameters/pageLimit" + - name: filter[id] + in: query + schema: + type: string + description: Filter by Setting ID + - name: filter[boost] + in: query + schema: + type: string + description: Filter by boost + - name: filter[boost][$lt] + in: query + schema: + type: string + - name: filter[boost][$lte] + in: query + schema: + type: string + - name: filter[boost][$gt] + in: query + schema: + type: string + - name: filter[boost][$gte] + in: query + schema: + type: string + - name: filter[postId] + in: query + schema: + type: string + description: Filter by postId + - name: filter[postId][$lt] + in: query + schema: + type: string + - name: filter[postId][$lte] + in: query + schema: + type: string + - name: filter[postId][$gt] + in: query + schema: + type: string + - name: filter[postId][$gte] + in: query + schema: + type: string + responses: + "200": + description: List of Setting resources + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/SettingListResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + post: + tags: + - setting + summary: Create a Setting resource + operationId: createSetting + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/SettingCreateRequest" + responses: + "201": + description: Created Setting resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/SettingResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /setting/{id}: + get: + tags: + - setting + summary: Get a Setting resource by ID + operationId: getSetting + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + responses: + "200": + description: Setting resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/SettingResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - setting + summary: Update a Setting resource + operationId: updateSetting + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/SettingUpdateRequest" + responses: + "200": + description: Updated Setting resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/SettingResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + delete: + tags: + - setting + summary: Delete a Setting resource + operationId: deleteSetting + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: Deleted successfully + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /setting/{id}/post: + get: + tags: + - setting + summary: Fetch related Post for Setting + operationId: getSetting_post + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + responses: + "200": + description: Related Post resource(s) + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /setting/{id}/relationships/post: + get: + tags: + - setting + summary: Fetch post relationship + operationId: getSetting_relationships_post + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: post relationship + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipWithLinks" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + put: + tags: + - setting + summary: Replace post relationship + operationId: putSetting_relationships_post + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - setting + summary: Update post relationship + operationId: patchSetting_relationships_post + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /postLike: + get: + tags: + - postLike + summary: List PostLike resources + operationId: listPostLike + parameters: + - $ref: "#/components/parameters/include" + - $ref: "#/components/parameters/sort" + - $ref: "#/components/parameters/pageOffset" + - $ref: "#/components/parameters/pageLimit" + - name: filter[id] + in: query + schema: + type: string + description: Filter by PostLike ID + - name: filter[superLike] + in: query + schema: + type: string + description: Filter by superLike + responses: + "200": + description: List of PostLike resources + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostLikeListResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + post: + tags: + - postLike + summary: Create a PostLike resource + operationId: createPostLike + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostLikeCreateRequest" + responses: + "201": + description: Created PostLike resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostLikeResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /postLike/{id}: + get: + tags: + - postLike + summary: Get a PostLike resource by ID + operationId: getPostLike + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + responses: + "200": + description: PostLike resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostLikeResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - postLike + summary: Update a PostLike resource + operationId: updatePostLike + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostLikeUpdateRequest" + responses: + "200": + description: Updated PostLike resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostLikeResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + delete: + tags: + - postLike + summary: Delete a PostLike resource + operationId: deletePostLike + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: Deleted successfully + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /postLike/{id}/post: + get: + tags: + - postLike + summary: Fetch related Post for PostLike + operationId: getPostLike_post + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + responses: + "200": + description: Related Post resource(s) + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /postLike/{id}/relationships/post: + get: + tags: + - postLike + summary: Fetch post relationship + operationId: getPostLike_relationships_post + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: post relationship + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipWithLinks" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + put: + tags: + - postLike + summary: Replace post relationship + operationId: putPostLike_relationships_post + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - postLike + summary: Update post relationship + operationId: patchPostLike_relationships_post + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /postLike/{id}/user: + get: + tags: + - postLike + summary: Fetch related User for PostLike + operationId: getPostLike_user + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + responses: + "200": + description: Related User resource(s) + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/UserResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /postLike/{id}/relationships/user: + get: + tags: + - postLike + summary: Fetch user relationship + operationId: getPostLike_relationships_user + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: user relationship + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipWithLinks" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + put: + tags: + - postLike + summary: Replace user relationship + operationId: putPostLike_relationships_user + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - postLike + summary: Update user relationship + operationId: patchPostLike_relationships_user + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /postLike/{id}/likeInfos: + get: + tags: + - postLike + summary: Fetch related PostLikeInfo for PostLike + operationId: getPostLike_likeInfos + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + - $ref: "#/components/parameters/sort" + - $ref: "#/components/parameters/pageOffset" + - $ref: "#/components/parameters/pageLimit" + - name: filter[id] + in: query + schema: + type: string + description: Filter by PostLikeInfo ID + - name: filter[text] + in: query + schema: + type: string + description: Filter by text + - name: filter[text][$contains] + in: query + schema: + type: string + - name: filter[text][$icontains] + in: query + schema: + type: string + - name: filter[text][$search] + in: query + schema: + type: string + - name: filter[text][$startsWith] + in: query + schema: + type: string + - name: filter[text][$endsWith] + in: query + schema: + type: string + - name: filter[postId] + in: query + schema: + type: string + description: Filter by postId + - name: filter[postId][$lt] + in: query + schema: + type: string + - name: filter[postId][$lte] + in: query + schema: + type: string + - name: filter[postId][$gt] + in: query + schema: + type: string + - name: filter[postId][$gte] + in: query + schema: + type: string + - name: filter[userId] + in: query + schema: + type: string + description: Filter by userId + - name: filter[userId][$contains] + in: query + schema: + type: string + - name: filter[userId][$icontains] + in: query + schema: + type: string + - name: filter[userId][$search] + in: query + schema: + type: string + - name: filter[userId][$startsWith] + in: query + schema: + type: string + - name: filter[userId][$endsWith] + in: query + schema: + type: string + responses: + "200": + description: Related PostLikeInfo resource(s) + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostLikeInfoListResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /postLike/{id}/relationships/likeInfos: + get: + tags: + - postLike + summary: Fetch likeInfos relationship + operationId: getPostLike_relationships_likeInfos + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: likeInfos relationship + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipWithLinks" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + put: + tags: + - postLike + summary: Replace likeInfos relationship + operationId: putPostLike_relationships_likeInfos + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - postLike + summary: Update likeInfos relationship + operationId: patchPostLike_relationships_likeInfos + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + post: + tags: + - postLike + summary: Add to likeInfos collection relationship + operationId: postPostLike_relationships_likeInfos + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toManyRelationshipRequest" + responses: + "200": + description: Added to relationship collection + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /postLikeInfo: + get: + tags: + - postLikeInfo + summary: List PostLikeInfo resources + operationId: listPostLikeInfo + parameters: + - $ref: "#/components/parameters/include" + - $ref: "#/components/parameters/sort" + - $ref: "#/components/parameters/pageOffset" + - $ref: "#/components/parameters/pageLimit" + - name: filter[id] + in: query + schema: + type: string + description: Filter by PostLikeInfo ID + - name: filter[text] + in: query + schema: + type: string + description: Filter by text + - name: filter[text][$contains] + in: query + schema: + type: string + - name: filter[text][$icontains] + in: query + schema: + type: string + - name: filter[text][$search] + in: query + schema: + type: string + - name: filter[text][$startsWith] + in: query + schema: + type: string + - name: filter[text][$endsWith] + in: query + schema: + type: string + - name: filter[postId] + in: query + schema: + type: string + description: Filter by postId + - name: filter[postId][$lt] + in: query + schema: + type: string + - name: filter[postId][$lte] + in: query + schema: + type: string + - name: filter[postId][$gt] + in: query + schema: + type: string + - name: filter[postId][$gte] + in: query + schema: + type: string + - name: filter[userId] + in: query + schema: + type: string + description: Filter by userId + - name: filter[userId][$contains] + in: query + schema: + type: string + - name: filter[userId][$icontains] + in: query + schema: + type: string + - name: filter[userId][$search] + in: query + schema: + type: string + - name: filter[userId][$startsWith] + in: query + schema: + type: string + - name: filter[userId][$endsWith] + in: query + schema: + type: string + responses: + "200": + description: List of PostLikeInfo resources + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostLikeInfoListResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + post: + tags: + - postLikeInfo + summary: Create a PostLikeInfo resource + operationId: createPostLikeInfo + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostLikeInfoCreateRequest" + responses: + "201": + description: Created PostLikeInfo resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostLikeInfoResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /postLikeInfo/{id}: + get: + tags: + - postLikeInfo + summary: Get a PostLikeInfo resource by ID + operationId: getPostLikeInfo + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + responses: + "200": + description: PostLikeInfo resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostLikeInfoResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - postLikeInfo + summary: Update a PostLikeInfo resource + operationId: updatePostLikeInfo + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostLikeInfoUpdateRequest" + responses: + "200": + description: Updated PostLikeInfo resource + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostLikeInfoResponse" + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + delete: + tags: + - postLikeInfo + summary: Delete a PostLikeInfo resource + operationId: deletePostLikeInfo + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: Deleted successfully + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /postLikeInfo/{id}/postLike: + get: + tags: + - postLikeInfo + summary: Fetch related PostLike for PostLikeInfo + operationId: getPostLikeInfo_postLike + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/include" + responses: + "200": + description: Related PostLike resource(s) + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/PostLikeResponse" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + /postLikeInfo/{id}/relationships/postLike: + get: + tags: + - postLikeInfo + summary: Fetch postLike relationship + operationId: getPostLikeInfo_relationships_postLike + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: postLike relationship + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipWithLinks" + "404": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + put: + tags: + - postLikeInfo + summary: Replace postLike relationship + operationId: putPostLikeInfo_relationships_postLike + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" + patch: + tags: + - postLikeInfo + summary: Update postLike relationship + operationId: patchPostLikeInfo_relationships_postLike + parameters: + - $ref: "#/components/parameters/id" + requestBody: + required: true + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_toOneRelationshipRequest" + responses: + "200": + description: Relationship updated + "400": + description: Error + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/_errorResponse" +components: + schemas: + _jsonapi: + type: object + properties: + version: + type: string + meta: + type: object + _meta: + type: object + additionalProperties: true + _links: + type: object + properties: + self: + type: string + related: + type: string + _pagination: + type: object + 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 + items: + type: object + properties: + status: + type: integer + code: + type: string + title: + type: string + detail: + type: string + required: + - status + - title + _errorResponse: + type: object + properties: + errors: + $ref: "#/components/schemas/_errors" + required: + - errors + _resourceIdentifier: + type: object + properties: + type: + type: string + id: + type: string + required: + - type + - id + _resource: + type: object + properties: + type: + type: string + id: + type: string + attributes: + type: object + relationships: + type: object + links: + $ref: "#/components/schemas/_links" + meta: + $ref: "#/components/schemas/_meta" + required: + - type + - id + _relationLinks: + type: object + properties: + self: + type: string + related: + type: string + _pagedRelationLinks: + type: object + allOf: + - $ref: "#/components/schemas/_relationLinks" + - $ref: "#/components/schemas/_pagination" + _toOneRelationship: + type: object + properties: + data: + oneOf: + - $ref: "#/components/schemas/_resourceIdentifier" + - type: "null" + _toManyRelationship: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/_resourceIdentifier" + _toOneRelationshipWithLinks: + type: object + allOf: + - $ref: "#/components/schemas/_toOneRelationship" + - properties: + links: + $ref: "#/components/schemas/_relationLinks" + _toManyRelationshipWithLinks: + type: object + allOf: + - $ref: "#/components/schemas/_toManyRelationship" + - properties: + links: + $ref: "#/components/schemas/_pagedRelationLinks" + _toManyRelationshipRequest: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/_resourceIdentifier" + required: + - data + _toOneRelationshipRequest: + type: object + properties: + data: + oneOf: + - $ref: "#/components/schemas/_resourceIdentifier" + - type: "null" + required: + - data + _toManyRelationshipResponse: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/_resourceIdentifier" + links: + $ref: "#/components/schemas/_pagedRelationLinks" + meta: + $ref: "#/components/schemas/_meta" + _toOneRelationshipResponse: + type: object + properties: + data: + oneOf: + - $ref: "#/components/schemas/_resourceIdentifier" + - type: "null" + links: + $ref: "#/components/schemas/_relationLinks" + meta: + $ref: "#/components/schemas/_meta" + Address: + type: object + properties: + city: + type: string + required: + - city + User: + type: object + properties: + attributes: + type: object + properties: + myId: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + email: + type: string + address: + oneOf: + - $ref: "#/components/schemas/Address" + - type: "null" + someJson: + oneOf: + - {} + - type: "null" + required: + - myId + - createdAt + - updatedAt + - email + relationships: + type: object + properties: + posts: + $ref: "#/components/schemas/_toManyRelationshipWithLinks" + likes: + $ref: "#/components/schemas/_toManyRelationshipWithLinks" + profile: + oneOf: + - type: "null" + - $ref: "#/components/schemas/_toOneRelationshipWithLinks" + UserCreateRequest: + type: object + properties: + data: + type: object + properties: + type: + type: string + attributes: + type: object + properties: + createdAt: + type: string + format: date-time + email: + type: string + address: + oneOf: + - $ref: "#/components/schemas/Address" + - type: "null" + someJson: + oneOf: + - {} + - type: "null" + required: + - email + relationships: + type: object + properties: + posts: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/_resourceIdentifier" + likes: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/_resourceIdentifier" + profile: + type: object + properties: + data: + $ref: "#/components/schemas/_resourceIdentifier" + required: + - type + required: + - data + UserUpdateRequest: + type: object + properties: + data: + type: object + properties: + type: + type: string + id: + type: string + attributes: + type: object + properties: + myId: + type: string + createdAt: + type: string + format: date-time + email: + type: string + address: + oneOf: + - $ref: "#/components/schemas/Address" + - type: "null" + someJson: + oneOf: + - {} + - type: "null" + relationships: + type: object + properties: + posts: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/_resourceIdentifier" + likes: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/_resourceIdentifier" + profile: + type: object + properties: + data: + $ref: "#/components/schemas/_resourceIdentifier" + required: + - type + - id + required: + - data + UserResponse: + type: object + properties: + jsonapi: + $ref: "#/components/schemas/_jsonapi" + data: + allOf: + - $ref: "#/components/schemas/User" + - $ref: "#/components/schemas/_resource" + meta: + $ref: "#/components/schemas/_meta" + UserListResponse: + type: object + properties: + jsonapi: + $ref: "#/components/schemas/_jsonapi" + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/User" + - $ref: "#/components/schemas/_resource" + links: + allOf: + - $ref: "#/components/schemas/_pagination" + - $ref: "#/components/schemas/_links" + meta: + $ref: "#/components/schemas/_meta" + Profile: + type: object + properties: + attributes: + type: object + properties: + id: + type: integer + gender: + type: string + userId: + type: string + required: + - id + - gender + - userId + relationships: + type: object + properties: + user: + $ref: "#/components/schemas/_toOneRelationshipWithLinks" + ProfileCreateRequest: + type: object + properties: + data: + type: object + properties: + type: + type: string + attributes: + type: object + properties: + gender: + type: string + required: + - gender + relationships: + type: object + properties: + user: + type: object + properties: + data: + $ref: "#/components/schemas/_resourceIdentifier" + required: + - type + required: + - data + ProfileUpdateRequest: + type: object + properties: + data: + type: object + properties: + type: + type: string + id: + type: string + attributes: + type: object + properties: + id: + type: integer + gender: + type: string + relationships: + type: object + properties: + user: + type: object + properties: + data: + $ref: "#/components/schemas/_resourceIdentifier" + required: + - type + - id + required: + - data + ProfileResponse: + type: object + properties: + jsonapi: + $ref: "#/components/schemas/_jsonapi" + data: + allOf: + - $ref: "#/components/schemas/Profile" + - $ref: "#/components/schemas/_resource" + meta: + $ref: "#/components/schemas/_meta" + ProfileListResponse: + type: object + properties: + jsonapi: + $ref: "#/components/schemas/_jsonapi" + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/Profile" + - $ref: "#/components/schemas/_resource" + links: + allOf: + - $ref: "#/components/schemas/_pagination" + - $ref: "#/components/schemas/_links" + meta: + $ref: "#/components/schemas/_meta" + Post: + type: object + properties: + attributes: + type: object + properties: + id: + type: integer + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + title: + type: string + authorId: + oneOf: + - type: string + - type: "null" + published: + type: boolean + publishedAt: + oneOf: + - type: string + format: date-time + - type: "null" + viewCount: + type: integer + required: + - id + - createdAt + - updatedAt + - title + - published + - viewCount + relationships: + type: object + properties: + author: + oneOf: + - type: "null" + - $ref: "#/components/schemas/_toOneRelationshipWithLinks" + comments: + $ref: "#/components/schemas/_toManyRelationshipWithLinks" + likes: + $ref: "#/components/schemas/_toManyRelationshipWithLinks" + setting: + oneOf: + - type: "null" + - $ref: "#/components/schemas/_toOneRelationshipWithLinks" + PostCreateRequest: + type: object + properties: + data: + type: object + properties: + type: + type: string + attributes: + type: object + properties: + createdAt: + type: string + format: date-time + title: + type: string + published: + type: boolean + publishedAt: + oneOf: + - type: string + format: date-time + - type: "null" + viewCount: + type: integer + required: + - title + relationships: + type: object + properties: + author: + type: object + properties: + data: + $ref: "#/components/schemas/_resourceIdentifier" + comments: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/_resourceIdentifier" + likes: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/_resourceIdentifier" + setting: + type: object + properties: + data: + $ref: "#/components/schemas/_resourceIdentifier" + required: + - type + required: + - data + PostUpdateRequest: + type: object + properties: + data: + type: object + properties: + type: + type: string + id: + type: string + attributes: + type: object + properties: + id: + type: integer + createdAt: + type: string + format: date-time + title: + type: string + published: + type: boolean + publishedAt: + oneOf: + - type: string + format: date-time + - type: "null" + viewCount: + type: integer + relationships: + type: object + properties: + author: + type: object + properties: + data: + $ref: "#/components/schemas/_resourceIdentifier" + comments: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/_resourceIdentifier" + likes: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/_resourceIdentifier" + setting: + type: object + properties: + data: + $ref: "#/components/schemas/_resourceIdentifier" + required: + - type + - id + required: + - data + PostResponse: + type: object + properties: + jsonapi: + $ref: "#/components/schemas/_jsonapi" + data: + allOf: + - $ref: "#/components/schemas/Post" + - $ref: "#/components/schemas/_resource" + meta: + $ref: "#/components/schemas/_meta" + PostListResponse: + type: object + properties: + jsonapi: + $ref: "#/components/schemas/_jsonapi" + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/Post" + - $ref: "#/components/schemas/_resource" + links: + allOf: + - $ref: "#/components/schemas/_pagination" + - $ref: "#/components/schemas/_links" + meta: + $ref: "#/components/schemas/_meta" + Comment: + type: object + properties: + attributes: + type: object + properties: + id: + type: integer + postId: + type: integer + content: + type: string + required: + - id + - postId + - content + relationships: + type: object + properties: + post: + $ref: "#/components/schemas/_toOneRelationshipWithLinks" + CommentCreateRequest: + type: object + properties: + data: + type: object + properties: + type: + type: string + attributes: + type: object + properties: + content: + type: string + required: + - content + relationships: + type: object + properties: + post: + type: object + properties: + data: + $ref: "#/components/schemas/_resourceIdentifier" + required: + - type + required: + - data + CommentUpdateRequest: + type: object + properties: + data: + type: object + properties: + type: + type: string + id: + type: string + attributes: + type: object + properties: + id: + type: integer + content: + type: string + relationships: + type: object + properties: + post: + type: object + properties: + data: + $ref: "#/components/schemas/_resourceIdentifier" + required: + - type + - id + required: + - data + CommentResponse: + type: object + properties: + jsonapi: + $ref: "#/components/schemas/_jsonapi" + data: + allOf: + - $ref: "#/components/schemas/Comment" + - $ref: "#/components/schemas/_resource" + meta: + $ref: "#/components/schemas/_meta" + CommentListResponse: + type: object + properties: + jsonapi: + $ref: "#/components/schemas/_jsonapi" + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/Comment" + - $ref: "#/components/schemas/_resource" + links: + allOf: + - $ref: "#/components/schemas/_pagination" + - $ref: "#/components/schemas/_links" + meta: + $ref: "#/components/schemas/_meta" + Setting: + type: object + properties: + attributes: + type: object + properties: + id: + type: integer + boost: + type: integer + postId: + type: integer + required: + - id + - boost + - postId + relationships: + type: object + properties: + post: + $ref: "#/components/schemas/_toOneRelationshipWithLinks" + SettingCreateRequest: + type: object + properties: + data: + type: object + properties: + type: + type: string + attributes: + type: object + properties: + boost: + type: integer + required: + - boost + relationships: + type: object + properties: + post: + type: object + properties: + data: + $ref: "#/components/schemas/_resourceIdentifier" + required: + - type + required: + - data + SettingUpdateRequest: + type: object + properties: + data: + type: object + properties: + type: + type: string + id: + type: string + attributes: + type: object + properties: + id: + type: integer + boost: + type: integer + relationships: + type: object + properties: + post: + type: object + properties: + data: + $ref: "#/components/schemas/_resourceIdentifier" + required: + - type + - id + required: + - data + SettingResponse: + type: object + properties: + jsonapi: + $ref: "#/components/schemas/_jsonapi" + data: + allOf: + - $ref: "#/components/schemas/Setting" + - $ref: "#/components/schemas/_resource" + meta: + $ref: "#/components/schemas/_meta" + SettingListResponse: + type: object + properties: + jsonapi: + $ref: "#/components/schemas/_jsonapi" + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/Setting" + - $ref: "#/components/schemas/_resource" + links: + allOf: + - $ref: "#/components/schemas/_pagination" + - $ref: "#/components/schemas/_links" + meta: + $ref: "#/components/schemas/_meta" + PostLike: + type: object + properties: + attributes: + type: object + properties: + postId: + type: integer + userId: + type: string + superLike: + type: boolean + required: + - postId + - userId + - superLike + relationships: + type: object + properties: + post: + $ref: "#/components/schemas/_toOneRelationshipWithLinks" + user: + $ref: "#/components/schemas/_toOneRelationshipWithLinks" + likeInfos: + $ref: "#/components/schemas/_toManyRelationshipWithLinks" + PostLikeCreateRequest: + type: object + properties: + data: + type: object + properties: + type: + type: string + attributes: + type: object + properties: + superLike: + type: boolean + required: + - superLike + relationships: + type: object + properties: + post: + type: object + properties: + data: + $ref: "#/components/schemas/_resourceIdentifier" + user: + type: object + properties: + data: + $ref: "#/components/schemas/_resourceIdentifier" + likeInfos: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/_resourceIdentifier" + required: + - type + required: + - data + PostLikeUpdateRequest: + type: object + properties: + data: + type: object + properties: + type: + type: string + id: + type: string + attributes: + type: object + properties: + superLike: + type: boolean + relationships: + type: object + properties: + post: + type: object + properties: + data: + $ref: "#/components/schemas/_resourceIdentifier" + user: + type: object + properties: + data: + $ref: "#/components/schemas/_resourceIdentifier" + likeInfos: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/_resourceIdentifier" + required: + - type + - id + required: + - data + PostLikeResponse: + type: object + properties: + jsonapi: + $ref: "#/components/schemas/_jsonapi" + data: + allOf: + - $ref: "#/components/schemas/PostLike" + - $ref: "#/components/schemas/_resource" + meta: + $ref: "#/components/schemas/_meta" + PostLikeListResponse: + type: object + properties: + jsonapi: + $ref: "#/components/schemas/_jsonapi" + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/PostLike" + - $ref: "#/components/schemas/_resource" + links: + allOf: + - $ref: "#/components/schemas/_pagination" + - $ref: "#/components/schemas/_links" + meta: + $ref: "#/components/schemas/_meta" + PostLikeInfo: + type: object + properties: + attributes: + type: object + properties: + id: + type: integer + text: + type: string + postId: + type: integer + userId: + type: string + required: + - id + - text + - postId + - userId + relationships: + type: object + properties: + postLike: + $ref: "#/components/schemas/_toOneRelationshipWithLinks" + PostLikeInfoCreateRequest: + type: object + properties: + data: + type: object + properties: + type: + type: string + attributes: + type: object + properties: + text: + type: string + required: + - text + relationships: + type: object + properties: + postLike: + type: object + properties: + data: + $ref: "#/components/schemas/_resourceIdentifier" + required: + - type + required: + - data + PostLikeInfoUpdateRequest: + type: object + properties: + data: + type: object + properties: + type: + type: string + id: + type: string + attributes: + type: object + properties: + id: + type: integer + text: + type: string + relationships: + type: object + properties: + postLike: + type: object + properties: + data: + $ref: "#/components/schemas/_resourceIdentifier" + required: + - type + - id + required: + - data + PostLikeInfoResponse: + type: object + properties: + jsonapi: + $ref: "#/components/schemas/_jsonapi" + data: + allOf: + - $ref: "#/components/schemas/PostLikeInfo" + - $ref: "#/components/schemas/_resource" + meta: + $ref: "#/components/schemas/_meta" + PostLikeInfoListResponse: + type: object + properties: + jsonapi: + $ref: "#/components/schemas/_jsonapi" + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/PostLikeInfo" + - $ref: "#/components/schemas/_resource" + links: + allOf: + - $ref: "#/components/schemas/_pagination" + - $ref: "#/components/schemas/_links" + meta: + $ref: "#/components/schemas/_meta" + parameters: + id: + name: id + in: path + required: true + schema: + type: string + description: Resource ID + include: + name: include + in: query + schema: + type: string + description: Comma-separated list of relationships to include + sort: + name: sort + in: query + schema: + type: string + description: Comma-separated list of fields to sort by. Prefix with - for descending + pageOffset: + name: page[offset] + in: query + schema: + type: integer + minimum: 0 + description: Page offset + pageLimit: + name: page[limit] + in: query + schema: + type: integer + minimum: 1 + description: Page limit diff --git a/packages/server/test/openapi/rest-openapi.test.ts b/packages/server/test/openapi/rest-openapi.test.ts new file mode 100644 index 000000000..87e2c246b --- /dev/null +++ b/packages/server/test/openapi/rest-openapi.test.ts @@ -0,0 +1,665 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import fs from 'fs'; +import path from 'path'; +import { beforeAll, describe, expect, it } from 'vitest'; +import YAML from 'yaml'; +import { validate } from '@readme/openapi-parser'; +import { RestApiHandler } from '../../src/api/rest'; + +const UPDATE_BASELINE = process.env.UPDATE_BASELINE === '1'; + +function loadBaseline(name: string) { + return YAML.parse(fs.readFileSync(path.join(__dirname, 'baseline', name), 'utf-8'), { maxAliasCount: 10000 }); +} + +function saveBaseline(name: string, spec: any) { + fs.writeFileSync(path.join(__dirname, 'baseline', name), YAML.stringify(spec, { lineWidth: 0, indent: 4, aliasDuplicateObjects: false })); +} + +const schema = ` +type Address { + city String +} + +model User { + myId String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique @email + posts Post[] + likes PostLike[] + profile Profile? + address Address? @json + someJson Json? +} + +model Profile { + id Int @id @default(autoincrement()) + gender String + user User @relation(fields: [userId], references: [myId]) + userId String @unique +} + +model Post { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String @length(1, 10) + author User? @relation(fields: [authorId], references: [myId]) + authorId String? + published Boolean @default(false) + publishedAt DateTime? + viewCount Int @default(0) + comments Comment[] + likes PostLike[] + setting Setting? +} + +model Comment { + id Int @id @default(autoincrement()) + post Post @relation(fields: [postId], references: [id]) + postId Int + content String +} + +model Setting { + id Int @id @default(autoincrement()) + boost Int + post Post @relation(fields: [postId], references: [id]) + postId Int @unique +} + +model PostLike { + postId Int + userId String + superLike Boolean + post Post @relation(fields: [postId], references: [id]) + user User @relation(fields: [userId], references: [myId]) + likeInfos PostLikeInfo[] + @@id([postId, userId]) +} + +model PostLikeInfo { + id Int @id @default(autoincrement()) + text String + postId Int + userId String + postLike PostLike @relation(fields: [postId, userId], references: [postId, userId]) +} +`; + +describe('REST OpenAPI spec generation', () => { + let handler: RestApiHandler; + let spec: any; + + beforeAll(async () => { + const client = await createTestClient(schema); + handler = new RestApiHandler({ + schema: client.$schema, + endpoint: 'http://localhost/api', + }); + spec = await handler.generateSpec(); + }); + + it('document structure is valid', () => { + expect(spec.openapi).toBe('3.1.0'); + expect(spec.info).toBeDefined(); + expect(spec.info.title).toBe('ZenStack Generated API'); + expect(spec.info.version).toBe('1.0.0'); + expect(spec.paths).toBeDefined(); + expect(spec.components).toBeDefined(); + expect(spec.components.schemas).toBeDefined(); + }); + + it('model paths exist', () => { + expect(spec.paths['/user']).toBeDefined(); + expect(spec.paths['/user/{id}']).toBeDefined(); + expect(spec.paths['/post']).toBeDefined(); + expect(spec.paths['/post/{id}']).toBeDefined(); + expect(spec.paths['/comment']).toBeDefined(); + expect(spec.paths['/comment/{id}']).toBeDefined(); + }); + + it('HTTP methods on collection path', () => { + expect(spec.paths['/user'].get).toBeDefined(); + expect(spec.paths['/user'].post).toBeDefined(); + }); + + it('HTTP methods on single resource path', () => { + expect(spec.paths['/user/{id}'].get).toBeDefined(); + expect(spec.paths['/user/{id}'].patch).toBeDefined(); + expect(spec.paths['/user/{id}'].delete).toBeDefined(); + }); + + it('relation paths exist', () => { + expect(spec.paths['/user/{id}/posts']).toBeDefined(); + expect(spec.paths['/user/{id}/relationships/posts']).toBeDefined(); + expect(spec.paths['/post/{id}/comments']).toBeDefined(); + }); + + it('fetch related path has response schema', () => { + // Collection relation: should reference ListResponse + const collectionPath = spec.paths['/user/{id}/posts'] as any; + const collectionSchema = collectionPath.get.responses['200'].content['application/vnd.api+json'].schema; + expect(collectionSchema.$ref).toBe('#/components/schemas/PostListResponse'); + + // Singular relation: should reference Response + const singularPath = spec.paths['/post/{id}/setting'] as any; + const singularSchema = singularPath.get.responses['200'].content['application/vnd.api+json'].schema; + expect(singularSchema.$ref).toBe('#/components/schemas/SettingResponse'); + }); + + it('relationship path has correct methods', () => { + const relPath = spec.paths['/user/{id}/relationships/posts']; + expect(relPath.get).toBeDefined(); + expect(relPath.put).toBeDefined(); + expect(relPath.patch).toBeDefined(); + // posts is a collection, so should have POST + expect(relPath.post).toBeDefined(); + }); + + it('model schemas exist', () => { + expect(spec.components.schemas['User']).toBeDefined(); + expect(spec.components.schemas['UserCreateRequest']).toBeDefined(); + expect(spec.components.schemas['UserUpdateRequest']).toBeDefined(); + expect(spec.components.schemas['UserResponse']).toBeDefined(); + expect(spec.components.schemas['UserListResponse']).toBeDefined(); + expect(spec.components.schemas['Post']).toBeDefined(); + expect(spec.components.schemas['PostCreateRequest']).toBeDefined(); + }); + + it('field types in schemas', () => { + const userSchema = spec.components.schemas['User']; + const userAttrs = userSchema.properties.attributes.properties; + expect(userAttrs).toBeDefined(); + // email -> string + expect(userAttrs['email']).toMatchObject({ type: 'string' }); + // myId -> string + expect(userAttrs['myId']).toMatchObject({ type: 'string' }); + + const postSchema = spec.components.schemas['Post']; + const postAttrs = postSchema.properties.attributes.properties; + expect(postAttrs).toBeDefined(); + // viewCount -> integer + expect(postAttrs['viewCount']).toMatchObject({ type: 'integer' }); + // published -> boolean + expect(postAttrs['published']).toMatchObject({ type: 'boolean' }); + // createdAt -> date-time + expect(postAttrs['createdAt']).toMatchObject({ type: 'string', format: 'date-time' }); + }); + + it('required fields marked in create schema', () => { + const createReq = spec.components.schemas['CommentCreateRequest']; + expect(createReq).toBeDefined(); + const dataProps = createReq.properties?.data?.properties; + expect(dataProps).toBeDefined(); + // content is required, non-optional non-default + const attrRequired = createReq.properties?.data?.properties?.attributes?.required ?? []; + expect(attrRequired).toContain('content'); + }); + + it('shared component schemas exist', () => { + expect(spec.components.schemas['_jsonapi']).toBeDefined(); + expect(spec.components.schemas['_errors']).toBeDefined(); + expect(spec.components.schemas['_errorResponse']).toBeDefined(); + expect(spec.components.schemas['_resourceIdentifier']).toBeDefined(); + expect(spec.components.schemas['_toOneRelationship']).toBeDefined(); + expect(spec.components.schemas['_toManyRelationship']).toBeDefined(); + }); + + it('shared parameters exist', () => { + expect(spec.components.parameters['id']).toBeDefined(); + expect(spec.components.parameters['include']).toBeDefined(); + expect(spec.components.parameters['sort']).toBeDefined(); + expect(spec.components.parameters['pageOffset']).toBeDefined(); + expect(spec.components.parameters['pageLimit']).toBeDefined(); + }); + + it('filter parameters appear on list operations', () => { + const listOp = spec.paths['/post'].get; + expect(listOp).toBeDefined(); + const paramNames = listOp.parameters.map((p: any) => ('name' in p ? p.name : p.$ref)); + expect(paramNames).toContain('filter[viewCount]'); + expect(paramNames).toContain('filter[published]'); + expect(paramNames).toContain('filter[title]'); + // String ops + expect(paramNames).toContain('filter[title][$contains]'); + // Numeric ops + expect(paramNames).toContain('filter[viewCount][$lt]'); + expect(paramNames).toContain('filter[viewCount][$gt]'); + }); + + it('modelNameMapping is reflected in paths', async () => { + const client = await createTestClient(schema); + const mappedHandler = new RestApiHandler({ + schema: client.$schema, + endpoint: 'http://localhost/api', + modelNameMapping: { User: 'users', Post: 'posts' }, + }); + const mappedSpec = await mappedHandler.generateSpec(); + expect(mappedSpec.paths?.['/users']).toBeDefined(); + expect(mappedSpec.paths?.['/posts']).toBeDefined(); + // Original paths should not exist + expect(mappedSpec.paths?.['/user']).toBeUndefined(); + expect(mappedSpec.paths?.['/post']).toBeUndefined(); + }); + + it('compound ID model paths exist', () => { + // PostLike has @@id([postId, userId]) + expect(spec.paths['/postLike']).toBeDefined(); + expect(spec.paths['/postLike/{id}']).toBeDefined(); + }); + + it('custom openApiOptions are reflected in info', async () => { + const client = await createTestClient(schema); + const customHandler = new RestApiHandler({ + schema: client.$schema, + endpoint: 'http://localhost/api', + }); + const customSpec = await customHandler.generateSpec({ + title: 'My Custom API', + version: '2.0.0', + description: 'A custom description', + }); + expect(customSpec.info.title).toBe('My Custom API'); + expect(customSpec.info.version).toBe('2.0.0'); + expect((customSpec.info as any).description).toBe('A custom description'); + }); +}); + +describe('REST OpenAPI spec generation - queryOptions', () => { + it('omit excludes fields from read schema', async () => { + const client = await createTestClient(schema); + const handler = new RestApiHandler({ + schema: client.$schema, + endpoint: 'http://localhost/api', + queryOptions: { omit: { User: { email: true } } }, + }); + const s = await handler.generateSpec(); + const userSchema = s.components?.schemas?.['User'] as any; + const userAttrs = userSchema.properties.attributes.properties; + expect(userAttrs['myId']).toBeDefined(); + expect(userAttrs['email']).toBeUndefined(); + }); + + it('slicing excludedModels removes model from spec', async () => { + const client = await createTestClient(schema); + const handler = new RestApiHandler({ + schema: client.$schema, + endpoint: 'http://localhost/api', + queryOptions: { slicing: { excludedModels: ['Post'] as any } }, + }); + const s = await handler.generateSpec(); + expect(s.paths?.['/user']).toBeDefined(); + expect(s.paths?.['/post']).toBeUndefined(); + expect(s.components?.schemas?.['Post']).toBeUndefined(); + + // Relation paths to excluded model should not exist + expect(s.paths?.['/user/{id}/posts']).toBeUndefined(); + expect(s.paths?.['/user/{id}/relationships/posts']).toBeUndefined(); + + // Relation fields to excluded model should not appear in read schema + const userSchema = s.components?.schemas?.['User'] as any; + const userRels = userSchema.properties.relationships?.properties ?? {}; + expect(userRels['posts']).toBeUndefined(); + const userAttrs = userSchema.properties.attributes.properties; + expect(userAttrs['email']).toBeDefined(); + }); + + it('slicing includedModels limits models in spec', async () => { + const client = await createTestClient(schema); + const handler = new RestApiHandler({ + schema: client.$schema, + endpoint: 'http://localhost/api', + queryOptions: { slicing: { includedModels: ['User'] as any } }, + }); + const s = await handler.generateSpec(); + expect(s.paths?.['/user']).toBeDefined(); + expect(s.paths?.['/post']).toBeUndefined(); + }); + + it('slicing excludedOperations removes HTTP methods from paths', async () => { + const client = await createTestClient(schema); + const handler = new RestApiHandler({ + schema: client.$schema, + endpoint: 'http://localhost/api', + queryOptions: { + slicing: { + models: { + post: { excludedOperations: ['create', 'delete'] }, + }, + } as any, + }, + }); + const s = await handler.generateSpec(); + + // Collection path: GET (findMany) should exist, POST (create) should not + expect((s.paths as any)['/post'].get).toBeDefined(); + expect((s.paths as any)['/post'].post).toBeUndefined(); + + // Single path: GET (findUnique) and PATCH (update) should exist, DELETE should not + expect((s.paths as any)['/post/{id}'].get).toBeDefined(); + expect((s.paths as any)['/post/{id}'].patch).toBeDefined(); + expect((s.paths as any)['/post/{id}'].delete).toBeUndefined(); + }); + + it('slicing excludedOperations on all CRUD ops removes paths entirely', async () => { + const client = await createTestClient(schema); + const handler = new RestApiHandler({ + schema: client.$schema, + endpoint: 'http://localhost/api', + queryOptions: { + slicing: { + models: { + post: { excludedOperations: ['findMany', 'create', 'findUnique', 'update', 'delete'] }, + }, + } as any, + }, + }); + const s = await handler.generateSpec(); + + // Both collection and single paths should be absent (empty path objects are not emitted) + expect(s.paths?.['/post']).toBeUndefined(); + expect(s.paths?.['/post/{id}']).toBeUndefined(); + }); + + it('slicing includedOperations limits HTTP methods in paths', async () => { + const client = await createTestClient(schema); + const handler = new RestApiHandler({ + schema: client.$schema, + endpoint: 'http://localhost/api', + queryOptions: { + slicing: { + models: { + post: { includedOperations: ['findMany', 'findUnique'] }, + }, + } as any, + }, + }); + const s = await handler.generateSpec(); + + // Collection: only GET + expect((s.paths as any)['/post'].get).toBeDefined(); + expect((s.paths as any)['/post'].post).toBeUndefined(); + + // Single: only GET + expect((s.paths as any)['/post/{id}'].get).toBeDefined(); + expect((s.paths as any)['/post/{id}'].patch).toBeUndefined(); + expect((s.paths as any)['/post/{id}'].delete).toBeUndefined(); + }); + + it('slicing $all excludedOperations applies to all models', async () => { + const client = await createTestClient(schema); + const handler = new RestApiHandler({ + schema: client.$schema, + endpoint: 'http://localhost/api', + queryOptions: { + slicing: { + models: { + $all: { excludedOperations: ['delete'] }, + }, + } as any, + }, + }); + const s = await handler.generateSpec(); + + // DELETE should be absent on all models + expect((s.paths as any)['/user/{id}'].delete).toBeUndefined(); + expect((s.paths as any)['/post/{id}'].delete).toBeUndefined(); + expect((s.paths as any)['/comment/{id}'].delete).toBeUndefined(); + + // Other methods should still exist + expect((s.paths as any)['/user/{id}'].get).toBeDefined(); + expect((s.paths as any)['/user/{id}'].patch).toBeDefined(); + }); + + it('slicing excludedFilterKinds removes filter params for a field', async () => { + const client = await createTestClient(schema); + const handler = new RestApiHandler({ + schema: client.$schema, + endpoint: 'http://localhost/api', + queryOptions: { + slicing: { + models: { + post: { + fields: { + title: { excludedFilterKinds: ['Like'] }, + viewCount: { excludedFilterKinds: ['Range'] }, + }, + }, + }, + } as any, + }, + }); + const s = await handler.generateSpec(); + const listOp = (s.paths as any)['/post'].get; + const paramNames = listOp.parameters.map((p: any) => ('name' in p ? p.name : p.$ref)); + + // Equality filters should still exist + expect(paramNames).toContain('filter[title]'); + expect(paramNames).toContain('filter[viewCount]'); + + // Like filters for title should be excluded + expect(paramNames).not.toContain('filter[title][$contains]'); + expect(paramNames).not.toContain('filter[title][$startsWith]'); + + // Range filters for viewCount should be excluded + expect(paramNames).not.toContain('filter[viewCount][$lt]'); + expect(paramNames).not.toContain('filter[viewCount][$gt]'); + }); + + it('slicing includedFilterKinds limits filter params for a field', async () => { + const client = await createTestClient(schema); + const handler = new RestApiHandler({ + schema: client.$schema, + endpoint: 'http://localhost/api', + queryOptions: { + slicing: { + models: { + post: { + fields: { + title: { includedFilterKinds: ['Equality'] }, + }, + }, + }, + } as any, + }, + }); + const s = await handler.generateSpec(); + const listOp = (s.paths as any)['/post'].get; + const paramNames = listOp.parameters.map((p: any) => ('name' in p ? p.name : p.$ref)); + + // Equality filter should exist + expect(paramNames).toContain('filter[title]'); + + // Like filters should be excluded (not in includedFilterKinds) + expect(paramNames).not.toContain('filter[title][$contains]'); + expect(paramNames).not.toContain('filter[title][$startsWith]'); + }); + + it('slicing $all field applies filter kind restriction to all fields', async () => { + const client = await createTestClient(schema); + const handler = new RestApiHandler({ + schema: client.$schema, + endpoint: 'http://localhost/api', + queryOptions: { + slicing: { + models: { + post: { + fields: { + $all: { includedFilterKinds: ['Equality'] }, + }, + }, + }, + } as any, + }, + }); + const s = await handler.generateSpec(); + const listOp = (s.paths as any)['/post'].get; + const paramNames = listOp.parameters.map((p: any) => ('name' in p ? p.name : p.$ref)); + + // Equality filters should exist + expect(paramNames).toContain('filter[title]'); + expect(paramNames).toContain('filter[viewCount]'); + + // Like and Range filters should be excluded + expect(paramNames).not.toContain('filter[title][$contains]'); + expect(paramNames).not.toContain('filter[viewCount][$lt]'); + }); + + it('slicing field-specific overrides $all for filter kinds', async () => { + const client = await createTestClient(schema); + const handler = new RestApiHandler({ + schema: client.$schema, + endpoint: 'http://localhost/api', + queryOptions: { + slicing: { + models: { + post: { + fields: { + $all: { includedFilterKinds: ['Equality'] }, + title: { includedFilterKinds: ['Equality', 'Like'] }, + }, + }, + }, + } as any, + }, + }); + const s = await handler.generateSpec(); + const listOp = (s.paths as any)['/post'].get; + const paramNames = listOp.parameters.map((p: any) => ('name' in p ? p.name : p.$ref)); + + // title should have both Equality and Like + expect(paramNames).toContain('filter[title]'); + expect(paramNames).toContain('filter[title][$contains]'); + + // viewCount should only have Equality (from $all) + expect(paramNames).toContain('filter[viewCount]'); + expect(paramNames).not.toContain('filter[viewCount][$lt]'); + }); + + it('slicing excludedFilterKinds on Equality removes basic filter param', async () => { + const client = await createTestClient(schema); + const handler = new RestApiHandler({ + schema: client.$schema, + endpoint: 'http://localhost/api', + queryOptions: { + slicing: { + models: { + post: { + fields: { + title: { excludedFilterKinds: ['Equality'] }, + }, + }, + }, + } as any, + }, + }); + const s = await handler.generateSpec(); + const listOp = (s.paths as any)['/post'].get; + const paramNames = listOp.parameters.map((p: any) => ('name' in p ? p.name : p.$ref)); + + // Basic equality filter should be excluded + expect(paramNames).not.toContain('filter[title]'); + + // Like filters should still exist + expect(paramNames).toContain('filter[title][$contains]'); + }); +}); + +describe('REST OpenAPI spec generation - @meta description', () => { + const metaSchema = ` +model User { + id String @id @default(cuid()) + email String @unique @meta("description", "The user's email address") + @@meta("description", "A user of the system") +} + +model Post { + id Int @id @default(autoincrement()) + title String +} +`; + + it('model @@meta description is used as schema description', async () => { + const client = await createTestClient(metaSchema); + const handler = new RestApiHandler({ + schema: client.$schema, + endpoint: 'http://localhost/api', + }); + const s = await handler.generateSpec(); + + const userSchema = s.components?.schemas?.['User'] as any; + expect(userSchema.description).toBe('A user of the system'); + + // Post has no @@meta description + const postSchema = s.components?.schemas?.['Post'] as any; + expect(postSchema.description).toBeUndefined(); + }); + + it('field @meta description is used as field schema description', async () => { + const client = await createTestClient(metaSchema); + const handler = new RestApiHandler({ + schema: client.$schema, + endpoint: 'http://localhost/api', + }); + const s = await handler.generateSpec(); + + const userSchema = s.components?.schemas?.['User'] as any; + const userAttrs = userSchema.properties.attributes.properties; + expect(userAttrs['email'].description).toBe("The user's email address"); + + // id has no @meta description + expect(userAttrs['id'].description).toBeUndefined(); + }); +}); + +describe('REST OpenAPI spec generation - baseline', () => { + it('matches baseline', async () => { + const client = await createTestClient(schema); + const handler = new RestApiHandler({ + schema: client.$schema, + endpoint: 'http://localhost/api', + }); + const spec = await handler.generateSpec(); + const baselineFile = 'rest.baseline.yaml'; + + if (UPDATE_BASELINE) { + saveBaseline(baselineFile, spec); + return; + } + + const baseline = loadBaseline(baselineFile); + expect(spec).toEqual(baseline); + + await validate(spec); + }); +}); + +describe('REST OpenAPI spec generation - with enum schema', () => { + it('enum schemas exist in components', async () => { + const enumSchema = ` +model Post { + id Int @id @default(autoincrement()) + title String + status PostStatus @default(DRAFT) +} + +enum PostStatus { + DRAFT + PUBLISHED + ARCHIVED +} +`; + const client = await createTestClient(enumSchema); + const h = new RestApiHandler({ + schema: client.$schema, + endpoint: 'http://localhost/api', + }); + const s = await h.generateSpec(); + expect(s.components?.schemas?.['PostStatus']).toBeDefined(); + expect((s.components?.schemas?.['PostStatus'] as any).type).toBe('string'); + expect((s.components?.schemas?.['PostStatus'] as any).enum).toContain('DRAFT'); + expect((s.components?.schemas?.['PostStatus'] as any).enum).toContain('PUBLISHED'); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5ac83d9e..2a4c2023a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -702,6 +702,9 @@ importers: decimal.js: specifier: 'catalog:' version: 10.6.0 + openapi-types: + specifier: ^12.1.3 + version: 12.1.3 superjson: specifier: ^2.2.3 version: 2.2.3 @@ -718,6 +721,9 @@ importers: specifier: 'catalog:' version: 4.0.1(zod@4.1.12) devDependencies: + '@readme/openapi-parser': + specifier: ^6.0.0 + version: 6.0.0(openapi-types@12.1.3) '@sveltejs/kit': specifier: 'catalog:' version: 2.53.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.53.5)(vite@7.3.1(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) @@ -1266,6 +1272,12 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@apidevtools/json-schema-ref-parser@14.2.1': + resolution: {integrity: sha512-HmdFw9CDYqM6B25pqGBpNeLCKvGPlIx1EbLrVL0zPvj50CJQUHyBNBw45Muk0kEIkogo1VZvOKHajdMuAzSxRg==} + engines: {node: '>= 20'} + peerDependencies: + '@types/json-schema': ^7.0.15 + '@asamuzakjp/css-color@4.1.2': resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==} @@ -2191,6 +2203,10 @@ packages: resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} + '@humanwhocodes/momoa@2.0.4': + resolution: {integrity: sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==} + engines: {node: '>=10.10.0'} + '@humanwhocodes/retry@0.3.1': resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} engines: {node: '>=18.18'} @@ -3046,6 +3062,22 @@ packages: '@prisma/get-platform@6.19.0': resolution: {integrity: sha512-ym85WDO2yDhC3fIXHWYpG3kVMBA49cL1XD2GCsCF8xbwoy2OkDQY44gEbAt2X46IQ4Apq9H6g0Ex1iFfPqEkHA==} + '@readme/better-ajv-errors@2.4.0': + resolution: {integrity: sha512-9WODaOAKSl/mU+MYNZ2aHCrkoRSvmQ+1YkLj589OEqqjOAhbn8j7Z+ilYoiTu/he6X63/clsxxAB4qny9/dDzg==} + engines: {node: '>=18'} + peerDependencies: + ajv: 4.11.8 - 8 + + '@readme/openapi-parser@6.0.0': + resolution: {integrity: sha512-PaTnrKlKgEJZzjJ77AAhGe28NiyLBdiKMx95rJ9xlLZ8QLqYitMpPBQAKhsuEGOWQQbsIMfBZEPavbXghACQHA==} + engines: {node: '>=20'} + peerDependencies: + openapi-types: '>=7' + + '@readme/openapi-schemas@3.1.0': + resolution: {integrity: sha512-9FC/6ho8uFa8fV50+FPy/ngWN53jaUu4GRXlAjcxIRrzhltJnpKkBG2Tp0IDraFJeWrOpk84RJ9EMEEYzaI1Bw==} + engines: {node: '>=18'} + '@rolldown/pluginutils@1.0.0-rc.2': resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} @@ -4202,6 +4234,14 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -6263,6 +6303,10 @@ packages: jsonify@0.0.1: resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} + jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + jsonschema@1.4.1: resolution: {integrity: sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==} @@ -6324,6 +6368,10 @@ packages: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -7360,6 +7408,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prelude-ls@1.2.1: @@ -9032,6 +9081,11 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@apidevtools/json-schema-ref-parser@14.2.1(@types/json-schema@7.0.15)': + dependencies: + '@types/json-schema': 7.0.15 + js-yaml: 4.1.1 + '@asamuzakjp/css-color@4.1.2': dependencies: '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) @@ -9077,7 +9131,7 @@ snapshots: '@babel/core@7.28.5': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 '@babel/generator': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) @@ -9348,19 +9402,19 @@ snapshots: '@babel/template@7.27.2': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 '@babel/parser': 7.28.5 '@babel/types': 7.28.5 '@babel/template@7.28.6': dependencies: - '@babel/code-frame': 7.28.6 + '@babel/code-frame': 7.29.0 '@babel/parser': 7.28.6 '@babel/types': 7.28.6 '@babel/traverse@7.28.5': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 '@babel/generator': 7.28.5 '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.5 @@ -9372,7 +9426,7 @@ snapshots: '@babel/traverse@7.28.6': dependencies: - '@babel/code-frame': 7.28.6 + '@babel/code-frame': 7.29.0 '@babel/generator': 7.28.6 '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.6 @@ -9946,6 +10000,8 @@ snapshots: '@humanwhocodes/module-importer@1.0.1': {} + '@humanwhocodes/momoa@2.0.4': {} + '@humanwhocodes/retry@0.3.1': {} '@humanwhocodes/retry@0.4.3': {} @@ -10786,6 +10842,28 @@ snapshots: dependencies: '@prisma/debug': 6.19.0 + '@readme/better-ajv-errors@2.4.0(ajv@8.18.0)': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.28.4 + '@humanwhocodes/momoa': 2.0.4 + ajv: 8.18.0 + jsonpointer: 5.0.1 + leven: 3.1.0 + picocolors: 1.1.1 + + '@readme/openapi-parser@6.0.0(openapi-types@12.1.3)': + dependencies: + '@apidevtools/json-schema-ref-parser': 14.2.1(@types/json-schema@7.0.15) + '@readme/better-ajv-errors': 2.4.0(ajv@8.18.0) + '@readme/openapi-schemas': 3.1.0 + '@types/json-schema': 7.0.15 + ajv: 8.18.0 + ajv-draft-04: 1.0.0(ajv@8.18.0) + openapi-types: 12.1.3 + + '@readme/openapi-schemas@3.1.0': {} + '@rolldown/pluginutils@1.0.0-rc.2': {} '@rolldown/pluginutils@1.0.0-rc.5': {} @@ -11853,7 +11931,7 @@ snapshots: '@vue/babel-plugin-resolve-type@2.0.1(@babel/core@7.29.0)': dependencies: - '@babel/code-frame': 7.28.6 + '@babel/code-frame': 7.29.0 '@babel/core': 7.29.0 '@babel/helper-module-imports': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 @@ -12071,6 +12149,10 @@ snapshots: agent-base@7.1.4: {} + ajv-draft-04@1.0.0(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + ajv-formats@3.0.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 @@ -13234,7 +13316,7 @@ snapshots: eslint: 9.29.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.29.0(jiti@2.6.1)) @@ -13267,7 +13349,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -13282,7 +13364,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -14328,6 +14410,8 @@ snapshots: jsonify@0.0.1: {} + jsonpointer@5.0.1: {} + jsonschema@1.4.1: {} jsx-ast-utils@3.3.5: @@ -14394,6 +14478,8 @@ snapshots: dependencies: readable-stream: 2.3.8 + leven@3.1.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1