diff --git a/docs/src/pages/reference/configuration/output.md b/docs/src/pages/reference/configuration/output.md index 49799b0ca..e30b8d8c1 100644 --- a/docs/src/pages/reference/configuration/output.md +++ b/docs/src/pages/reference/configuration/output.md @@ -2082,3 +2082,20 @@ module.exports = { }, }; ``` + +### propertySortOrder + +Type: `Alphabetical` | `Specification` + +Default Value: `Specification` +This enables you to specify how properties in the models are sorted, either alphabetically or in the order they appear in the specification. + +```js +module.exports = { + petstore: { + output: { + propertySortOrder: 'Alphabetical', + }, + }, +}; +``` diff --git a/packages/core/src/getters/object.ts b/packages/core/src/getters/object.ts index ccacb0d83..5c2ca6b0b 100644 --- a/packages/core/src/getters/object.ts +++ b/packages/core/src/getters/object.ts @@ -2,6 +2,7 @@ import { ReferenceObject, SchemaObject } from 'openapi3-ts/oas30'; import { resolveExampleRefs, resolveObject, resolveValue } from '../resolvers'; import { ContextSpecs, + PropertySortOrder, ScalarValue, SchemaType, SchemaWithConst, @@ -70,98 +71,100 @@ export const getObject = ({ } if (item.properties && Object.entries(item.properties).length > 0) { - return Object.entries(item.properties) - .sort((a, b) => { + const entries = Object.entries(item.properties); + if (context.output.propertySortOrder === PropertySortOrder.ALPHABETICAL) { + entries.sort((a, b) => { return a[0].localeCompare(b[0]); - }) - .reduce( - ( - acc, - [key, schema]: [string, ReferenceObject | SchemaObject], - index, - arr, - ) => { - const isRequired = ( - Array.isArray(item.required) ? item.required : [] - ).includes(key); - - let propName = ''; - - if (name) { - const isKeyStartWithUnderscore = key.startsWith('_'); - - propName += pascal( - `${isKeyStartWithUnderscore ? '_' : ''}${name}_${key}`, - ); - } - - const allSpecSchemas = - context.specs[context.target]?.components?.schemas ?? {}; - - const isNameAlreadyTaken = Object.keys(allSpecSchemas).some( - (schemaName) => pascal(schemaName) === propName, + }); + } + return entries.reduce( + ( + acc, + [key, schema]: [string, ReferenceObject | SchemaObject], + index, + arr, + ) => { + const isRequired = ( + Array.isArray(item.required) ? item.required : [] + ).includes(key); + + let propName = ''; + + if (name) { + const isKeyStartWithUnderscore = key.startsWith('_'); + + propName += pascal( + `${isKeyStartWithUnderscore ? '_' : ''}${name}_${key}`, ); - - if (isNameAlreadyTaken) { - propName = propName + 'Property'; - } - - const resolvedValue = resolveObject({ - schema, - propName, - context, - }); - - const isReadOnly = item.readOnly || (schema as SchemaObject).readOnly; - if (!index) { - acc.value += '{'; - } - - const doc = jsDoc(schema as SchemaObject, true); - - acc.hasReadonlyProps ||= isReadOnly || false; - acc.imports.push(...resolvedValue.imports); - acc.value += `\n ${doc ? `${doc} ` : ''}${ - isReadOnly && !context.output.override.suppressReadonlyModifier - ? 'readonly ' - : '' - }${getKey(key)}${isRequired ? '' : '?'}: ${resolvedValue.value};`; - acc.schemas.push(...resolvedValue.schemas); - - if (arr.length - 1 === index) { - if (item.additionalProperties) { - if (isBoolean(item.additionalProperties)) { - acc.value += `\n [key: string]: unknown;\n }`; - } else { - const resolvedValue = resolveValue({ - schema: item.additionalProperties, - name, - context, - }); - acc.value += `\n [key: string]: ${resolvedValue.value};\n}`; - } + } + + const allSpecSchemas = + context.specs[context.target]?.components?.schemas ?? {}; + + const isNameAlreadyTaken = Object.keys(allSpecSchemas).some( + (schemaName) => pascal(schemaName) === propName, + ); + + if (isNameAlreadyTaken) { + propName = propName + 'Property'; + } + + const resolvedValue = resolveObject({ + schema, + propName, + context, + }); + + const isReadOnly = item.readOnly || (schema as SchemaObject).readOnly; + if (!index) { + acc.value += '{'; + } + + const doc = jsDoc(schema as SchemaObject, true); + + acc.hasReadonlyProps ||= isReadOnly || false; + acc.imports.push(...resolvedValue.imports); + acc.value += `\n ${doc ? `${doc} ` : ''}${ + isReadOnly && !context.output.override.suppressReadonlyModifier + ? 'readonly ' + : '' + }${getKey(key)}${isRequired ? '' : '?'}: ${resolvedValue.value};`; + acc.schemas.push(...resolvedValue.schemas); + + if (arr.length - 1 === index) { + if (item.additionalProperties) { + if (isBoolean(item.additionalProperties)) { + acc.value += `\n [key: string]: unknown;\n }`; } else { - acc.value += '\n}'; + const resolvedValue = resolveValue({ + schema: item.additionalProperties, + name, + context, + }); + acc.value += `\n [key: string]: ${resolvedValue.value};\n}`; } - - acc.value += nullable; + } else { + acc.value += '\n}'; } - return acc; - }, - { - imports: [], - schemas: [], - value: '', - isEnum: false, - type: 'object' as SchemaType, - isRef: false, - schema: {}, - hasReadonlyProps: false, - example: item.example, - examples: resolveExampleRefs(item.examples, context), - } as ScalarValue, - ); + acc.value += nullable; + } + + return acc; + }, + { + imports: [], + schemas: [], + value: '', + isEnum: false, + type: 'object' as SchemaType, + isRef: false, + schema: {}, + hasReadonlyProps: false, + example: item.example, + examples: resolveExampleRefs(item.examples, context), + } as ScalarValue, + ); } if (item.additionalProperties) { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index daffc4cfc..4792e49bb 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -64,6 +64,7 @@ export type NormalizedOutputOptions = { urlEncodeParameters: boolean; unionAddMissingProperties: boolean; optionsParamRequired: boolean; + propertySortOrder: PropertySortOrder; }; export type NormalizedParamsSerializerOptions = { @@ -180,6 +181,14 @@ export type BaseUrlFromConstant = { baseUrl: string; }; +export const PropertySortOrder = { + ALPHABETICAL: 'Alphabetical', + SPECIFICATION: 'Specification', +} as const; + +export type PropertySortOrder = + (typeof PropertySortOrder)[keyof typeof PropertySortOrder]; + export type OutputOptions = { workspace?: string; target?: string; @@ -205,6 +214,7 @@ export type OutputOptions = { urlEncodeParameters?: boolean; unionAddMissingProperties?: boolean; optionsParamRequired?: boolean; + propertySortOrder?: PropertySortOrder; }; export type SwaggerParserOptions = Omit & { diff --git a/packages/mock/src/faker/getters/object.ts b/packages/mock/src/faker/getters/object.ts index 33b9ac3f7..9116eca27 100644 --- a/packages/mock/src/faker/getters/object.ts +++ b/packages/mock/src/faker/getters/object.ts @@ -6,6 +6,7 @@ import { isReference, MockOptions, pascal, + PropertySorting, } from '@orval/core'; import { ReferenceObject, SchemaObject } from 'openapi3-ts/oas30'; import { resolveMockValue } from '../resolvers/value'; @@ -105,10 +106,13 @@ export const getMockObject = ({ let imports: GeneratorImport[] = []; let includedProperties: string[] = []; - const properyScalars = Object.entries(item.properties) - .sort((a, b) => { + const entries = Object.entries(item.properties); + if (context.output.propertySortOrder === PropertySorting.Alphabetical) { + entries.sort((a, b) => { return a[0].localeCompare(b[0]); - }) + }); + } + const properyScalars = entries .map(([key, prop]: [string, ReferenceObject | SchemaObject]) => { if (combine?.includedProperties.includes(key)) { return undefined; diff --git a/packages/orval/src/utils/options.ts b/packages/orval/src/utils/options.ts index f2f003d9b..67cf17fb0 100644 --- a/packages/orval/src/utils/options.ts +++ b/packages/orval/src/utils/options.ts @@ -26,6 +26,7 @@ import { OutputClient, OutputHttpClient, OutputMode, + PropertySortOrder, QueryOptions, RefComponentSuffix, SwaggerParserOptions, @@ -320,6 +321,8 @@ export const normalizeOptions = async ( allParamsOptional: outputOptions.allParamsOptional ?? false, urlEncodeParameters: outputOptions.urlEncodeParameters ?? false, optionsParamRequired: outputOptions.optionsParamRequired ?? false, + propertySortOrder: + outputOptions.propertySortOrder ?? PropertySortOrder.SPECIFICATION, }, hooks: options.hooks ? normalizeHooks(options.hooks) : {}, };