Skip to content

Commit c0eba70

Browse files
authored
feat(core): property sorting (#1763)
* feat(core): property sorting * fix: specification is default sorting
1 parent 7c36284 commit c0eba70

File tree

5 files changed

+126
-89
lines changed

5 files changed

+126
-89
lines changed

docs/src/pages/reference/configuration/output.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2082,3 +2082,20 @@ module.exports = {
20822082
},
20832083
};
20842084
```
2085+
2086+
### propertySortOrder
2087+
2088+
Type: `Alphabetical` | `Specification`
2089+
2090+
Default Value: `Specification`
2091+
This enables you to specify how properties in the models are sorted, either alphabetically or in the order they appear in the specification.
2092+
2093+
```js
2094+
module.exports = {
2095+
petstore: {
2096+
output: {
2097+
propertySortOrder: 'Alphabetical',
2098+
},
2099+
},
2100+
};
2101+
```

packages/core/src/getters/object.ts

Lines changed: 89 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ReferenceObject, SchemaObject } from 'openapi3-ts/oas30';
22
import { resolveExampleRefs, resolveObject, resolveValue } from '../resolvers';
33
import {
44
ContextSpecs,
5+
PropertySortOrder,
56
ScalarValue,
67
SchemaType,
78
SchemaWithConst,
@@ -70,98 +71,100 @@ export const getObject = ({
7071
}
7172

7273
if (item.properties && Object.entries(item.properties).length > 0) {
73-
return Object.entries(item.properties)
74-
.sort((a, b) => {
74+
const entries = Object.entries(item.properties);
75+
if (context.output.propertySortOrder === PropertySortOrder.ALPHABETICAL) {
76+
entries.sort((a, b) => {
7577
return a[0].localeCompare(b[0]);
76-
})
77-
.reduce(
78-
(
79-
acc,
80-
[key, schema]: [string, ReferenceObject | SchemaObject],
81-
index,
82-
arr,
83-
) => {
84-
const isRequired = (
85-
Array.isArray(item.required) ? item.required : []
86-
).includes(key);
87-
88-
let propName = '';
89-
90-
if (name) {
91-
const isKeyStartWithUnderscore = key.startsWith('_');
92-
93-
propName += pascal(
94-
`${isKeyStartWithUnderscore ? '_' : ''}${name}_${key}`,
95-
);
96-
}
97-
98-
const allSpecSchemas =
99-
context.specs[context.target]?.components?.schemas ?? {};
100-
101-
const isNameAlreadyTaken = Object.keys(allSpecSchemas).some(
102-
(schemaName) => pascal(schemaName) === propName,
78+
});
79+
}
80+
return entries.reduce(
81+
(
82+
acc,
83+
[key, schema]: [string, ReferenceObject | SchemaObject],
84+
index,
85+
arr,
86+
) => {
87+
const isRequired = (
88+
Array.isArray(item.required) ? item.required : []
89+
).includes(key);
90+
91+
let propName = '';
92+
93+
if (name) {
94+
const isKeyStartWithUnderscore = key.startsWith('_');
95+
96+
propName += pascal(
97+
`${isKeyStartWithUnderscore ? '_' : ''}${name}_${key}`,
10398
);
104-
105-
if (isNameAlreadyTaken) {
106-
propName = propName + 'Property';
107-
}
108-
109-
const resolvedValue = resolveObject({
110-
schema,
111-
propName,
112-
context,
113-
});
114-
115-
const isReadOnly = item.readOnly || (schema as SchemaObject).readOnly;
116-
if (!index) {
117-
acc.value += '{';
118-
}
119-
120-
const doc = jsDoc(schema as SchemaObject, true);
121-
122-
acc.hasReadonlyProps ||= isReadOnly || false;
123-
acc.imports.push(...resolvedValue.imports);
124-
acc.value += `\n ${doc ? `${doc} ` : ''}${
125-
isReadOnly && !context.output.override.suppressReadonlyModifier
126-
? 'readonly '
127-
: ''
128-
}${getKey(key)}${isRequired ? '' : '?'}: ${resolvedValue.value};`;
129-
acc.schemas.push(...resolvedValue.schemas);
130-
131-
if (arr.length - 1 === index) {
132-
if (item.additionalProperties) {
133-
if (isBoolean(item.additionalProperties)) {
134-
acc.value += `\n [key: string]: unknown;\n }`;
135-
} else {
136-
const resolvedValue = resolveValue({
137-
schema: item.additionalProperties,
138-
name,
139-
context,
140-
});
141-
acc.value += `\n [key: string]: ${resolvedValue.value};\n}`;
142-
}
99+
}
100+
101+
const allSpecSchemas =
102+
context.specs[context.target]?.components?.schemas ?? {};
103+
104+
const isNameAlreadyTaken = Object.keys(allSpecSchemas).some(
105+
(schemaName) => pascal(schemaName) === propName,
106+
);
107+
108+
if (isNameAlreadyTaken) {
109+
propName = propName + 'Property';
110+
}
111+
112+
const resolvedValue = resolveObject({
113+
schema,
114+
propName,
115+
context,
116+
});
117+
118+
const isReadOnly = item.readOnly || (schema as SchemaObject).readOnly;
119+
if (!index) {
120+
acc.value += '{';
121+
}
122+
123+
const doc = jsDoc(schema as SchemaObject, true);
124+
125+
acc.hasReadonlyProps ||= isReadOnly || false;
126+
acc.imports.push(...resolvedValue.imports);
127+
acc.value += `\n ${doc ? `${doc} ` : ''}${
128+
isReadOnly && !context.output.override.suppressReadonlyModifier
129+
? 'readonly '
130+
: ''
131+
}${getKey(key)}${isRequired ? '' : '?'}: ${resolvedValue.value};`;
132+
acc.schemas.push(...resolvedValue.schemas);
133+
134+
if (arr.length - 1 === index) {
135+
if (item.additionalProperties) {
136+
if (isBoolean(item.additionalProperties)) {
137+
acc.value += `\n [key: string]: unknown;\n }`;
143138
} else {
144-
acc.value += '\n}';
139+
const resolvedValue = resolveValue({
140+
schema: item.additionalProperties,
141+
name,
142+
context,
143+
});
144+
acc.value += `\n [key: string]: ${resolvedValue.value};\n}`;
145145
}
146-
147-
acc.value += nullable;
146+
} else {
147+
acc.value += '\n}';
148148
}
149149

150-
return acc;
151-
},
152-
{
153-
imports: [],
154-
schemas: [],
155-
value: '',
156-
isEnum: false,
157-
type: 'object' as SchemaType,
158-
isRef: false,
159-
schema: {},
160-
hasReadonlyProps: false,
161-
example: item.example,
162-
examples: resolveExampleRefs(item.examples, context),
163-
} as ScalarValue,
164-
);
150+
acc.value += nullable;
151+
}
152+
153+
return acc;
154+
},
155+
{
156+
imports: [],
157+
schemas: [],
158+
value: '',
159+
isEnum: false,
160+
type: 'object' as SchemaType,
161+
isRef: false,
162+
schema: {},
163+
hasReadonlyProps: false,
164+
example: item.example,
165+
examples: resolveExampleRefs(item.examples, context),
166+
} as ScalarValue,
167+
);
165168
}
166169

167170
if (item.additionalProperties) {

packages/core/src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export type NormalizedOutputOptions = {
6464
urlEncodeParameters: boolean;
6565
unionAddMissingProperties: boolean;
6666
optionsParamRequired: boolean;
67+
propertySortOrder: PropertySortOrder;
6768
};
6869

6970
export type NormalizedParamsSerializerOptions = {
@@ -180,6 +181,14 @@ export type BaseUrlFromConstant = {
180181
baseUrl: string;
181182
};
182183

184+
export const PropertySortOrder = {
185+
ALPHABETICAL: 'Alphabetical',
186+
SPECIFICATION: 'Specification',
187+
} as const;
188+
189+
export type PropertySortOrder =
190+
(typeof PropertySortOrder)[keyof typeof PropertySortOrder];
191+
183192
export type OutputOptions = {
184193
workspace?: string;
185194
target?: string;
@@ -205,6 +214,7 @@ export type OutputOptions = {
205214
urlEncodeParameters?: boolean;
206215
unionAddMissingProperties?: boolean;
207216
optionsParamRequired?: boolean;
217+
propertySortOrder?: PropertySortOrder;
208218
};
209219

210220
export type SwaggerParserOptions = Omit<SwaggerParser.Options, 'validate'> & {

packages/mock/src/faker/getters/object.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
isReference,
77
MockOptions,
88
pascal,
9+
PropertySorting,
910
} from '@orval/core';
1011
import { ReferenceObject, SchemaObject } from 'openapi3-ts/oas30';
1112
import { resolveMockValue } from '../resolvers/value';
@@ -105,10 +106,13 @@ export const getMockObject = ({
105106
let imports: GeneratorImport[] = [];
106107
let includedProperties: string[] = [];
107108

108-
const properyScalars = Object.entries(item.properties)
109-
.sort((a, b) => {
109+
const entries = Object.entries(item.properties);
110+
if (context.output.propertySortOrder === PropertySorting.Alphabetical) {
111+
entries.sort((a, b) => {
110112
return a[0].localeCompare(b[0]);
111-
})
113+
});
114+
}
115+
const properyScalars = entries
112116
.map(([key, prop]: [string, ReferenceObject | SchemaObject]) => {
113117
if (combine?.includedProperties.includes(key)) {
114118
return undefined;

packages/orval/src/utils/options.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
OutputClient,
2727
OutputHttpClient,
2828
OutputMode,
29+
PropertySortOrder,
2930
QueryOptions,
3031
RefComponentSuffix,
3132
SwaggerParserOptions,
@@ -320,6 +321,8 @@ export const normalizeOptions = async (
320321
allParamsOptional: outputOptions.allParamsOptional ?? false,
321322
urlEncodeParameters: outputOptions.urlEncodeParameters ?? false,
322323
optionsParamRequired: outputOptions.optionsParamRequired ?? false,
324+
propertySortOrder:
325+
outputOptions.propertySortOrder ?? PropertySortOrder.SPECIFICATION,
323326
},
324327
hooks: options.hooks ? normalizeHooks(options.hooks) : {},
325328
};

0 commit comments

Comments
 (0)