Skip to content

Commit 6a05ef0

Browse files
ymc9claude
andcommitted
fix: structure REST OpenAPI model schema with attributes/relationships
Restructure buildModelReadSchema to nest scalar fields under `attributes` and relation fields under `relationships`, matching JSON:API resource structure. Relation fields now use proper relationship refs (_toManyRelationshipWithLinks/_toOneRelationshipWithLinks). Also generate schemas for composite type definitions (typeDefs). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 873b4e9 commit 6a05ef0

3 files changed

Lines changed: 262 additions & 181 deletions

File tree

packages/server/src/api/rest/openapi.ts

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { lowerCaseFirst } from '@zenstackhq/common-helpers';
2-
import type { EnumDef, FieldDef, ModelDef, SchemaDef } from '@zenstackhq/orm/schema';
2+
import type { EnumDef, FieldDef, ModelDef, SchemaDef, TypeDefDef } from '@zenstackhq/orm/schema';
33
import type { OpenAPIV3_1 } from 'openapi-types';
44
import { PROCEDURE_ROUTE_PREFIXES } from '../common/procedures';
55
import {
@@ -524,6 +524,13 @@ export class RestApiSpecGenerator<Schema extends SchemaDef = SchemaDef> {
524524
}
525525
}
526526

527+
// Per-typeDef schemas
528+
if (this.schema.typeDefs) {
529+
for (const [typeName, typeDef] of Object.entries(this.schema.typeDefs)) {
530+
schemas[typeName] = this.buildTypeDefSchema(typeDef);
531+
}
532+
}
533+
527534
// Per-model schemas
528535
for (const modelName of getIncludedModels(this.schema, this.queryOptions)) {
529536
const modelDef = this.schema.models[modelName]!;
@@ -710,31 +717,66 @@ export class RestApiSpecGenerator<Schema extends SchemaDef = SchemaDef> {
710717
};
711718
}
712719

713-
private buildModelReadSchema(modelName: string, modelDef: ModelDef): SchemaObject {
720+
private buildTypeDefSchema(typeDef: TypeDefDef): SchemaObject {
714721
const properties: Record<string, SchemaObject | ReferenceObject> = {};
715722
const required: string[] = [];
716723

724+
for (const [fieldName, fieldDef] of Object.entries(typeDef.fields)) {
725+
properties[fieldName] = this.fieldToSchema(fieldDef);
726+
if (!fieldDef.optional && !fieldDef.array) {
727+
required.push(fieldName);
728+
}
729+
}
730+
731+
const result: SchemaObject = { type: 'object', properties };
732+
if (required.length > 0) {
733+
result.required = required;
734+
}
735+
return result;
736+
}
737+
738+
private buildModelReadSchema(modelName: string, modelDef: ModelDef): SchemaObject {
739+
const attrProperties: Record<string, SchemaObject | ReferenceObject> = {};
740+
const attrRequired: string[] = [];
741+
const relProperties: Record<string, SchemaObject | ReferenceObject> = {};
742+
717743
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
718744
if (fieldDef.omit) continue;
719745
if (isFieldOmitted(modelName, fieldName, this.queryOptions)) continue;
720746
if (fieldDef.relation && !isModelIncluded(fieldDef.type, this.queryOptions)) continue;
721747

722-
const schema = this.fieldToSchema(fieldDef);
723-
const fieldDescription = getMetaDescription(fieldDef.attributes);
724-
if (fieldDescription && !('$ref' in schema)) {
725-
schema.description = fieldDescription;
726-
}
727-
properties[fieldName] = schema;
748+
if (fieldDef.relation) {
749+
const relRef: SchemaObject | ReferenceObject = fieldDef.array
750+
? { $ref: '#/components/schemas/_toManyRelationshipWithLinks' }
751+
: { $ref: '#/components/schemas/_toOneRelationshipWithLinks' };
752+
relProperties[fieldName] = fieldDef.optional ? { oneOf: [{ type: 'null' }, relRef] } : relRef;
753+
} else {
754+
const schema = this.fieldToSchema(fieldDef);
755+
const fieldDescription = getMetaDescription(fieldDef.attributes);
756+
if (fieldDescription && !('$ref' in schema)) {
757+
schema.description = fieldDescription;
758+
}
759+
attrProperties[fieldName] = schema;
728760

729-
if (!fieldDef.optional && !fieldDef.array) {
730-
required.push(fieldName);
761+
if (!fieldDef.optional && !fieldDef.array) {
762+
attrRequired.push(fieldName);
763+
}
731764
}
732765
}
733766

734-
const result: SchemaObject = { type: 'object', properties };
735-
if (required.length > 0) {
736-
result.required = required;
767+
const properties: Record<string, SchemaObject | ReferenceObject> = {};
768+
769+
if (Object.keys(attrProperties).length > 0) {
770+
const attrSchema: SchemaObject = { type: 'object', properties: attrProperties };
771+
if (attrRequired.length > 0) attrSchema.required = attrRequired;
772+
properties['attributes'] = attrSchema;
737773
}
774+
775+
if (Object.keys(relProperties).length > 0) {
776+
properties['relationships'] = { type: 'object', properties: relProperties };
777+
}
778+
779+
const result: SchemaObject = { type: 'object', properties };
738780
const description = getMetaDescription(modelDef.attributes);
739781
if (description) {
740782
result.description = description;

0 commit comments

Comments
 (0)