mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-12 02:19:43 +00:00
fix(API): Give a union of object schemas a root type: object in the generated spec (no-changelog) (#40409)
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6a41a5ed39
commit
77a692a4bb
@@ -129,6 +129,7 @@ responses:
|
||||
- kind
|
||||
- nodeType
|
||||
- filePath
|
||||
type: object
|
||||
settings:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
@@ -244,6 +245,7 @@ responses:
|
||||
- kind
|
||||
- nodeType
|
||||
- filePath
|
||||
type: object
|
||||
settings:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
@@ -362,6 +364,7 @@ responses:
|
||||
- kind
|
||||
- nodeType
|
||||
- filePath
|
||||
type: object
|
||||
settings:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
@@ -480,6 +483,7 @@ responses:
|
||||
- kind
|
||||
- nodeType
|
||||
- filePath
|
||||
type: object
|
||||
settings:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
@@ -595,6 +599,7 @@ responses:
|
||||
- kind
|
||||
- nodeType
|
||||
- filePath
|
||||
type: object
|
||||
settings:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
|
||||
+1
@@ -54,6 +54,7 @@ requestBody:
|
||||
required:
|
||||
- kind
|
||||
- value
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- action
|
||||
|
||||
+1
@@ -54,6 +54,7 @@ requestBody:
|
||||
required:
|
||||
- kind
|
||||
- value
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- action
|
||||
|
||||
+1
@@ -60,6 +60,7 @@ requestBody:
|
||||
required:
|
||||
- kind
|
||||
- value
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- action
|
||||
|
||||
+1
@@ -61,6 +61,7 @@ requestBody:
|
||||
required:
|
||||
- kind
|
||||
- value
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- action
|
||||
|
||||
+1
@@ -54,6 +54,7 @@ requestBody:
|
||||
required:
|
||||
- kind
|
||||
- value
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- action
|
||||
|
||||
+1
@@ -54,6 +54,7 @@ requestBody:
|
||||
required:
|
||||
- kind
|
||||
- value
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- action
|
||||
|
||||
+1
@@ -60,6 +60,7 @@ requestBody:
|
||||
required:
|
||||
- kind
|
||||
- value
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- action
|
||||
|
||||
+1
@@ -61,6 +61,7 @@ requestBody:
|
||||
required:
|
||||
- kind
|
||||
- value
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- action
|
||||
|
||||
+1
@@ -56,6 +56,7 @@ requestBody:
|
||||
- username
|
||||
- password
|
||||
additionalProperties: false
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- type
|
||||
|
||||
+1
@@ -58,6 +58,7 @@ requestBody:
|
||||
- username
|
||||
- password
|
||||
additionalProperties: false
|
||||
type: object
|
||||
additionalProperties: false
|
||||
minProperties: 1
|
||||
responses:
|
||||
|
||||
+1
@@ -366,6 +366,7 @@ requestBody:
|
||||
- type
|
||||
- dsn
|
||||
description: 'A log streaming destination. The `type` field selects the variant and its type-specific fields: `webhook`, `syslog`, or `sentry`.'
|
||||
type: object
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
|
||||
+1
@@ -372,6 +372,7 @@ responses:
|
||||
- dsn
|
||||
- id
|
||||
description: 'A log streaming destination. The `type` field selects the variant and its type-specific fields: `webhook`, `syslog`, or `sentry`.'
|
||||
type: object
|
||||
description: The configured log streaming destinations.
|
||||
required:
|
||||
- data
|
||||
|
||||
+1
@@ -358,6 +358,7 @@ requestBody:
|
||||
- type
|
||||
- dsn
|
||||
description: 'A log streaming destination. The `type` field selects the variant and its type-specific fields: `webhook`, `syslog`, or `sentry`.'
|
||||
type: object
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { addObjectTypeToObjectUnions } from '../object-union-type';
|
||||
|
||||
const webhook = { type: 'object', properties: { url: { type: 'string' } } };
|
||||
const syslog = { type: 'object', properties: { host: { type: 'string' } } };
|
||||
|
||||
describe('addObjectTypeToObjectUnions', () => {
|
||||
it('adds `type: object` to a oneOf whose members are all objects', () => {
|
||||
const schema = { oneOf: [webhook, syslog], description: 'A destination' };
|
||||
addObjectTypeToObjectUnions(schema);
|
||||
expect(schema).toEqual({
|
||||
type: 'object',
|
||||
oneOf: [webhook, syslog],
|
||||
description: 'A destination',
|
||||
});
|
||||
});
|
||||
|
||||
it('leaves a oneOf with a non-object member unchanged', () => {
|
||||
const schema = { oneOf: [webhook, { type: 'array', items: {} }] };
|
||||
addObjectTypeToObjectUnions(schema);
|
||||
expect(schema).toEqual({ oneOf: [webhook, { type: 'array', items: {} }] });
|
||||
});
|
||||
|
||||
it('leaves a oneOf with a $ref member unchanged', () => {
|
||||
const schema = { oneOf: [webhook, { $ref: '#/components/schemas/Syslog' }] };
|
||||
addObjectTypeToObjectUnions(schema);
|
||||
expect(schema).toEqual({ oneOf: [webhook, { $ref: '#/components/schemas/Syslog' }] });
|
||||
});
|
||||
|
||||
it('normalizes a nested union under items', () => {
|
||||
const schema = { type: 'array', items: { oneOf: [webhook, syslog] } };
|
||||
addObjectTypeToObjectUnions(schema);
|
||||
expect(schema).toEqual({ type: 'array', items: { type: 'object', oneOf: [webhook, syslog] } });
|
||||
});
|
||||
|
||||
it('normalizes a union under a property named like a data key', () => {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: { default: { oneOf: [webhook, syslog] }, enum: { oneOf: [webhook, syslog] } },
|
||||
};
|
||||
addObjectTypeToObjectUnions(schema);
|
||||
expect(schema).toEqual({
|
||||
type: 'object',
|
||||
properties: {
|
||||
default: { type: 'object', oneOf: [webhook, syslog] },
|
||||
enum: { type: 'object', oneOf: [webhook, syslog] },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not rewrite example data', () => {
|
||||
const schema = { type: 'object', example: { oneOf: [webhook, syslog] } };
|
||||
addObjectTypeToObjectUnions(schema);
|
||||
expect(schema).toEqual({ type: 'object', example: { oneOf: [webhook, syslog] } });
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
toOpenApiPathTemplate,
|
||||
} from '@/public-api/public-api-route-resolver';
|
||||
|
||||
import { stripUntypedNullable } from './untyped-nullable';
|
||||
import { applySchemaModifiers } from './schema-modifiers';
|
||||
|
||||
const REQUEST_BODY_COMPONENT = 'RequestBody';
|
||||
|
||||
@@ -194,9 +194,7 @@ export function buildRequestBodyJsonSchema(
|
||||
const { components } = new OpenApiGeneratorV3(registry.definitions).generateComponents();
|
||||
const schema = components?.schemas?.[REQUEST_BODY_COMPONENT];
|
||||
|
||||
// Keep in sync with `buildArtifactsFromRegistry`, both convert the same DTOs to OpenAPI
|
||||
// and `/discover` serves this schema straight from a route at runtime.
|
||||
stripUntypedNullable(schema);
|
||||
applySchemaModifiers(schema);
|
||||
|
||||
return isRecord(schema) ? schema : undefined;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
type NamedResponseDto,
|
||||
type SchemaResolver,
|
||||
} from './decorator-routes';
|
||||
import { stripUntypedNullable } from './untyped-nullable';
|
||||
import { applySchemaModifiers } from './schema-modifiers';
|
||||
|
||||
const COMPONENT_SCHEMA_REF = /^#\/components\/schemas\/(.+)$/;
|
||||
const SHARED_SCHEMA_DIR = 'shared/spec/schemas';
|
||||
@@ -90,9 +90,7 @@ export function buildArtifactsFromRegistry(
|
||||
info: { title: 'throwaway', version: '0.0.0' },
|
||||
});
|
||||
|
||||
// zod-to-openapi emits `z.unknown()`/`z.any()` as `{ nullable: true }` with no `type` which
|
||||
// express-openapi-validator rejects at schema-compile time once bundled.
|
||||
stripUntypedNullable(document);
|
||||
applySchemaModifiers(document);
|
||||
|
||||
const artifacts: GeneratedArtifact[] = [];
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { isRecord } from '@n8n/utils/is-record';
|
||||
|
||||
const DATA_VALUED_KEYS = new Set(['example', 'examples', 'default', 'enum']);
|
||||
|
||||
// The keys inside these objects are field names, not keywords. Check every value, so a field
|
||||
// named `example` or `default` is not skipped as sample data.
|
||||
const SCHEMA_MAP_KEYS = new Set(['properties', 'patternProperties', '$defs', 'definitions']);
|
||||
|
||||
/**
|
||||
* zod-to-openapi writes a discriminated union as a `oneOf` with no `type`. `.openapi()` cannot add
|
||||
* the `type` without replacing the union, so this function adds it to the generated document.
|
||||
*/
|
||||
export function addObjectTypeToObjectUnions(node: unknown): void {
|
||||
if (Array.isArray(node)) {
|
||||
node.forEach((item) => addObjectTypeToObjectUnions(item));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isRecord(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const members = node.oneOf;
|
||||
if (
|
||||
node.type === undefined &&
|
||||
Array.isArray(members) &&
|
||||
members.length > 0 &&
|
||||
members.every((member) => isRecord(member) && member.type === 'object')
|
||||
) {
|
||||
node.type = 'object';
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(node)) {
|
||||
if (SCHEMA_MAP_KEYS.has(key) && isRecord(value)) {
|
||||
Object.values(value).forEach((schema) => addObjectTypeToObjectUnions(schema));
|
||||
continue;
|
||||
}
|
||||
if (DATA_VALUED_KEYS.has(key)) {
|
||||
continue;
|
||||
}
|
||||
addObjectTypeToObjectUnions(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { addObjectTypeToObjectUnions } from './object-union-type';
|
||||
import { stripUntypedNullable } from './untyped-nullable';
|
||||
|
||||
const SCHEMA_MODIFIERS = [stripUntypedNullable, addObjectTypeToObjectUnions];
|
||||
|
||||
export function applySchemaModifiers(node: unknown): void {
|
||||
for (const modify of SCHEMA_MODIFIERS) {
|
||||
modify(node);
|
||||
}
|
||||
}
|
||||
@@ -454,6 +454,7 @@ oneOf:
|
||||
required:
|
||||
- kind
|
||||
- project
|
||||
type: object
|
||||
sourceValue:
|
||||
type: string
|
||||
maxLength: 1000
|
||||
@@ -496,6 +497,7 @@ oneOf:
|
||||
- name
|
||||
- scope
|
||||
- consumers
|
||||
type: object
|
||||
accessRequirements:
|
||||
type: array
|
||||
items:
|
||||
@@ -1253,6 +1255,7 @@ oneOf:
|
||||
- from
|
||||
- to
|
||||
- destructive
|
||||
type: object
|
||||
minItems: 1
|
||||
required:
|
||||
- kind
|
||||
@@ -1261,6 +1264,7 @@ oneOf:
|
||||
- name
|
||||
- consumers
|
||||
- changes
|
||||
type: object
|
||||
warnings:
|
||||
type: array
|
||||
items:
|
||||
@@ -1367,3 +1371,4 @@ oneOf:
|
||||
- configId
|
||||
- git
|
||||
- status
|
||||
type: object
|
||||
|
||||
@@ -443,6 +443,7 @@ oneOf:
|
||||
- to
|
||||
- destructive
|
||||
description: A single change `dataTableSchemaConflictPolicy=overwrite` would make (or made) to a matched target table's schema. `destructive` operations delete the data in a column.
|
||||
type: object
|
||||
description: 'For `schema-incompatible`: the changes `overwrite` would make to the target table.'
|
||||
missingScope:
|
||||
type: string
|
||||
@@ -642,3 +643,4 @@ oneOf:
|
||||
- name
|
||||
- violations
|
||||
description: 'A workflow the content-import policy refused. Takes down the whole package rather than skipping the workflow: the import rewrites cross-workflow references to the ids each workflow would get, so dropping one leaves the workflows that call it pointing at a row nothing ever wrote.'
|
||||
type: object
|
||||
|
||||
+1
@@ -352,3 +352,4 @@ oneOf:
|
||||
- dsn
|
||||
- id
|
||||
description: 'A log streaming destination. The `type` field selects the variant and its type-specific fields: `webhook`, `syslog`, or `sentry`.'
|
||||
type: object
|
||||
|
||||
+1
@@ -47,6 +47,7 @@ properties:
|
||||
required:
|
||||
- kind
|
||||
- value
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- action
|
||||
|
||||
+1
@@ -49,6 +49,7 @@ properties:
|
||||
required:
|
||||
- kind
|
||||
- value
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- action
|
||||
|
||||
+1
@@ -44,6 +44,7 @@ properties:
|
||||
required:
|
||||
- kind
|
||||
- value
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- action
|
||||
|
||||
+1
@@ -47,6 +47,7 @@ properties:
|
||||
required:
|
||||
- kind
|
||||
- value
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- action
|
||||
|
||||
+1
@@ -43,6 +43,7 @@ properties:
|
||||
required:
|
||||
- kind
|
||||
- value
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- action
|
||||
|
||||
+1
@@ -43,6 +43,7 @@ properties:
|
||||
required:
|
||||
- kind
|
||||
- value
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- action
|
||||
|
||||
Reference in New Issue
Block a user