@openapi-flow/n8n 0.3.7 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  Zod `^4.6.5` is a required host peer for the public configuration schemas. It is not an independently pinned runtime dependency. The n8n SDK's internal Zod 3 remains separately owned; this does not pass a Zod 4 instance into an API requiring Zod 3.
4
4
 
5
- Version **0.3.5**, with matching `@openapi-flow/core@0.3.5`. There is no n8n implementation change in this patch. Known limitation: Ajv 8.20.0 ignores a declared `properties.__proto__`. Generated validation can reject valid data or omit that property's constraints; do not rely on it for contracts using that key. The previously accepted limitation and original failing regression remain unchanged. No field substitution or schema compatibility transform is included. See the [release record](https://github.com/carefreelife98/openapi-flow/blob/main/docs/releases/0.3.5.md) for publication and host status.
5
+ Version **0.3.8**, with matching `@openapi-flow/core@0.3.8`. This patch adds named composition of unchanged API responses through the registered `ComposeApiResponses` capability. Known limitation: Ajv 8.20.0 ignores a declared `properties.__proto__`. Generated validation can reject valid data or omit that property's constraints; do not rely on it for contracts using that key. The previously accepted limitation and original failing regression remain unchanged. No field substitution or schema compatibility transform is included. See the [release record](https://github.com/carefreelife98/openapi-flow/blob/main/docs/releases/0.3.8.md) for publication and host status.
6
+
7
+ `createApiResponseObjectCapability({ materials })` / `N8nNativeFeature.ComposeApiResponses` register `compose-api-responses`. The model selects only `fields: [{ name, sourceNodeId, pointer }]`. Code derives value schemas from original OAS contracts, validates full responses and derives join inputs from distinct sources in first-field occurrence order. The compiler reuses official SDK Merge Append and Edit Fields (Set); one source needs no Merge. Literal names, nulls, scalar types and nested values are preserved. Duplicate names, absent pointers, undeclared bodies and ambiguous source item counts fail without defaults or coercion. This is one response item per API invocation, not a native-output projection or per-item batch join. See [the source guide](https://github.com/carefreelife98/openapi-flow#named-api-response-objects-038).
6
8
 
7
9
  `describeN8nManualTrigger()` describes the Manual Trigger already supplied by this compiler. Pass that descriptor as `entrypoint` to native and connection planning so a model sees the actual compiler contract without selecting an extra trigger. The descriptor is not a data producer or a substitute for graph validation.
8
10
 
@@ -18,7 +20,7 @@ materializer check presence separately from the unchanged OAS, including missing
18
20
  optional descendants of a response-bound object. Strict HTTP node creation also
19
21
  rejects missing requested literals. See [requested inputs](../../docs/workflow-draft-api-binding-questions.md#값이-미정인-선택-입력의-요청-추적).
20
22
 
21
- `resolveN8nNativePlanningFeatures` separates selectable implementation definitions from material readiness. Four source-dependent families can be selected before producers exist, then bind the original contracts through existing snapshots and SDK compilation. No guessed source schema or runtime name is used. See [planning and host integration](../../docs/native-feature-planning.md).
23
+ `resolveN8nNativePlanningFeatures` separates selectable implementation definitions from material readiness. Five source-dependent families can be selected before producers exist, then bind the original contracts through existing snapshots and SDK compilation. No guessed source schema or runtime name is used. See [planning and host integration](../../docs/native-feature-planning.md).
22
24
 
23
25
  material-dependent array/collection/item-join compilation rebinds current data contracts and resolves runtime names from actual SDK producer exits. `createN8nNativeOutputSources` requires actual `apiResponseContracts` for referenced APIs; partial subsets can declare external native outputs explicitly. Template requests accept `itemMode: 'linked'` for ancestry-preserving item streams. No old-contract/name fallback is used; custom compiler extensions must retain a coherent material binder. See [material snapshots and runtime proof](../../docs/native-material-snapshots.md).
24
26
 
@@ -28,9 +30,9 @@ Standalone strict, reviewable and finite-batch compilation also accepts explicit
28
30
 
29
31
  `describeN8nManualTrigger()` exposes the compiler-owned start contract for requirement planning. Its description and actual SDK trigger creation share one module, so hosts can declare the existing start separately from selectable native instances. It is explicit planning metadata, not a registry default, deployment check or extra trigger node.
30
32
 
31
- `resolveN8nNativeFeatures` and `N8nNativeFeature` expose one registry for eleven existing feature families. Omission inventories every family with explicit `ready`/`requires-context` status; an explicit enum list is strict and `[]` selects none. Ready capabilities retain the existing schemas, source declarations and SDK compilers. Material-dependent families require actual OAS/native contracts, not invented placeholders. The published four-feature factory keeps its original selection. See the [registration and extension contract](../../docs/native-feature-registry.md).
33
+ `resolveN8nNativeFeatures` and `N8nNativeFeature` expose one registry for twelve existing feature families. Omission inventories every family with explicit `ready`/`requires-context` status; an explicit enum list is strict and `[]` selects none. Ready capabilities retain the existing schemas, source declarations and SDK compilers. Material-dependent families require actual OAS/native contracts, not invented placeholders. The published four-feature factory keeps its original selection. See the [registration and extension contract](../../docs/native-feature-registry.md).
32
34
 
33
- Deterministic OpenAPI request-node creation and explicit n8n DAG assembly with the official workflow SDK. Version `0.3.3` depends on matching core `0.3.3`.
35
+ Deterministic OpenAPI request-node creation and explicit n8n DAG assembly with the official workflow SDK. Version `0.3.8` depends on matching core `0.3.8`.
34
36
 
35
37
  ## Installed declaration dependencies
36
38
 
@@ -0,0 +1,2 @@
1
+ import type { NativeMaterialFeatureDefinition } from '../../../types/native-material-binding.js';
2
+ export declare const apiResponseObjectDefinition: NativeMaterialFeatureDefinition;
@@ -0,0 +1,28 @@
1
+ import { apiResponseObjectParametersSchema } from '../../../schemas/api-response-object-schema.js';
2
+ export const apiResponseObjectDefinition = {
3
+ name: 'compose-api-responses',
4
+ description: 'Wait for all referenced API branches and compose explicitly named properties into one JSON object from unchanged whole response bodies or RFC 6901 values. Code derives all value types and input ports from actual references, validates full OAS responses and compiles official Merge plus Edit Fields (Set). Input1..inputN correspond to distinct sourceNodeIds in first-field occurrence order. Connect each API branch to its matching input; do not add a separate branch join for this operation. Requires exactly one item per referenced API execution. No casting, flattening or inferred matching. Repeated fields may read the same source under different names without adding an input.',
5
+ materialRequirements: [{ field: 'apiMaterials', minimumItems: 1 }],
6
+ parametersSchema: apiResponseObjectParametersSchema,
7
+ parameterReferenceSources: ['response'],
8
+ referenceStructureSchema: apiResponseObjectParametersSchema,
9
+ parameterReferenceBindings: (value) => apiResponseObjectParametersSchema
10
+ .parse(value)
11
+ .fields.map((field, index) => ({
12
+ nodeIdPointer: `/fields/${index}/sourceNodeId`,
13
+ valuePointer: `/fields/${index}/pointer`,
14
+ reference: {
15
+ source: 'response',
16
+ nodeId: field.sourceNodeId,
17
+ pointer: field.pointer,
18
+ },
19
+ })),
20
+ inputPorts: (value) => [
21
+ ...new Set(apiResponseObjectParametersSchema
22
+ .parse(value)
23
+ .fields.map((field) => field.sourceNodeId)),
24
+ ].map((_, index) => `input${index + 1}`),
25
+ outputPorts: () => ['main'],
26
+ waitsForAllInputs: true,
27
+ exclusiveOutputPorts: false,
28
+ };
@@ -0,0 +1,3 @@
1
+ import type { CompileApiResponseObjectInput } from '../../../types/api-response-object.js';
2
+ import type { N8nNodeFragment } from '../../../types/node-fragment.js';
3
+ export declare function compileApiResponseObject(input: CompileApiResponseObjectInput): N8nNodeFragment;
@@ -0,0 +1,86 @@
1
+ import { node } from '@n8n/workflow-sdk';
2
+ import { readFileSync } from 'node:fs';
3
+ import { createOutputReaderCode } from '../../request/bindings/create-output-reader-code.js';
4
+ import { javascriptJsonLiteral } from '../../../utils/javascript-json-literal.js';
5
+ import { createMergeAppendCapability } from '../merge-append-capability.js';
6
+ import { apiResponseObjectDefinition } from './api-response-object-definition.js';
7
+ export function compileApiResponseObject(input) {
8
+ const { planned, parameters, position } = input;
9
+ const sources = input.materials.map((material) => {
10
+ const nodeName = input.apiNodeNames[material.callId];
11
+ if (!Object.hasOwn(input.apiNodeNames, material.callId) ||
12
+ !nodeName?.trim())
13
+ throw new Error(`apiNodeNames is missing ${material.callId}`);
14
+ return {
15
+ kind: 'api-response',
16
+ nodeId: material.callId,
17
+ nodeName,
18
+ operation: material.operation,
19
+ };
20
+ });
21
+ const runtime = readFileSync(new URL('../../request/runtime/request-runtime.bundle.js', import.meta.url), 'utf8');
22
+ const reader = createOutputReaderCode(sources);
23
+ const join = sources.length > 1
24
+ ? createMergeAppendCapability().compile({
25
+ ...input,
26
+ planned: {
27
+ id: `${planned.id}-join-responses`,
28
+ capability: 'merge-append',
29
+ parameters: { numberInputs: sources.length },
30
+ },
31
+ })
32
+ : undefined;
33
+ const readPosition = [
34
+ position[0] + (join === undefined ? 0 : 220),
35
+ position[1],
36
+ ];
37
+ const extract = node({
38
+ type: 'n8n-nodes-base.code',
39
+ version: 2,
40
+ config: {
41
+ id: `${planned.id}-read-object-values`,
42
+ name: `Read object values ${planned.id}`,
43
+ position: readPosition,
44
+ parameters: {
45
+ mode: 'runOnceForAllItems',
46
+ language: 'javaScript',
47
+ jsCode: `${runtime}\n${reader}\nconst parameters=${javascriptJsonLiteral(parameters)};\nconst entries=parameters.fields.map(field=>{const value=OpenApiFlowRequestRuntime.pointerValue(responses[field.sourceNodeId],field.pointer);if(value===undefined)throw new Error('Missing response pointer '+field.sourceNodeId+field.pointer);return [field.name,value];});\nreturn [{json:Object.fromEntries(entries),pairedItem:$input.all().map((_,item)=>({item}))}];`,
48
+ },
49
+ },
50
+ });
51
+ const output = node({
52
+ type: 'n8n-nodes-base.set',
53
+ version: 3.4,
54
+ config: {
55
+ id: planned.id,
56
+ name: planned.id,
57
+ position: [readPosition[0] + 220, readPosition[1]],
58
+ parameters: {
59
+ mode: 'raw',
60
+ jsonOutput: '={{ JSON.stringify($json) }}',
61
+ includeOtherFields: false,
62
+ options: { dotNotation: false },
63
+ },
64
+ },
65
+ });
66
+ return {
67
+ nodeId: planned.id,
68
+ nodes: [...(join === undefined ? [] : join.nodes), extract, output],
69
+ entry: join === undefined ? extract : join.entry,
70
+ exit: output,
71
+ inputPorts: Object.fromEntries(apiResponseObjectDefinition
72
+ .inputPorts(parameters)
73
+ .map((port, index) => [port, index])),
74
+ outputPorts: { main: 0 },
75
+ internalEdges: [
76
+ ...(join === undefined
77
+ ? []
78
+ : [
79
+ ...(join.internalEdges === undefined ? [] : join.internalEdges),
80
+ { from: join.exit.id, output: 0, to: extract.id, input: 0 },
81
+ ]),
82
+ { from: extract.id, output: 0, to: output.id, input: 0 },
83
+ ],
84
+ bindingSources: sources,
85
+ };
86
+ }
@@ -0,0 +1,3 @@
1
+ import type { N8nNativeCapability } from '../../../types/native-capability.js';
2
+ import type { CreateApiResponseObjectCapabilityInput } from '../../../types/api-response-object.js';
3
+ export declare function createApiResponseObjectCapability(input: CreateApiResponseObjectCapabilityInput): N8nNativeCapability;
@@ -0,0 +1,60 @@
1
+ import { createResponseValueSchema } from '@openapi-flow/core';
2
+ import { createApiResponseObjectParametersSchema } from '../../../schemas/api-response-object-schema.js';
3
+ import { defineMaterialBoundCapability } from '../common/define-material-bound-capability.js';
4
+ import { apiResponseObjectDefinition } from './api-response-object-definition.js';
5
+ import { compileApiResponseObject } from './compile-api-response-object.js';
6
+ export function createApiResponseObjectCapability(input) {
7
+ const supplied = structuredClone(input.materials);
8
+ const materials = new Map(supplied.map((material) => [material.callId, material]));
9
+ if (!materials.size ||
10
+ materials.size !== supplied.length ||
11
+ supplied.some((material) => !material.callId.trim()))
12
+ throw new Error('response object capability requires unique API materials');
13
+ const parametersSchema = createApiResponseObjectParametersSchema([
14
+ ...materials.keys(),
15
+ ]);
16
+ function describeFields(parameters) {
17
+ return parameters.fields.map((field) => {
18
+ const material = materials.get(field.sourceNodeId);
19
+ if (!material)
20
+ throw new Error(`response object: unknown sourceNodeId ${field.sourceNodeId}`);
21
+ return {
22
+ field,
23
+ material,
24
+ schema: createResponseValueSchema(material.operation, field.pointer),
25
+ };
26
+ });
27
+ }
28
+ return defineMaterialBoundCapability({
29
+ ...apiResponseObjectDefinition,
30
+ parametersSchema,
31
+ materialSources: { apiMaterials: supplied, nativeOutputs: [] },
32
+ bindMaterialContracts: (sources) => createApiResponseObjectCapability({ materials: sources.apiMaterials }),
33
+ responseReferences: (value) => describeFields(parametersSchema.parse(value)).map(({ field }) => ({
34
+ source: 'response',
35
+ nodeId: field.sourceNodeId,
36
+ pointer: field.pointer,
37
+ })),
38
+ outputSchema: (value) => {
39
+ const fields = describeFields(parametersSchema.parse(value));
40
+ return {
41
+ type: 'object',
42
+ properties: Object.fromEntries(fields.map(({ field, schema }) => [field.name, schema])),
43
+ required: fields.map(({ field }) => field.name),
44
+ additionalProperties: false,
45
+ };
46
+ },
47
+ compile: (source) => {
48
+ const parameters = parametersSchema.parse(source.planned.parameters);
49
+ const fields = describeFields(parameters);
50
+ const used = [
51
+ ...new Map(fields.map(({ material }) => [material.callId, material])).values(),
52
+ ];
53
+ return compileApiResponseObject({
54
+ ...source,
55
+ parameters,
56
+ materials: used,
57
+ });
58
+ },
59
+ });
60
+ }
@@ -14,6 +14,8 @@ import { createBoundItemJoinCapability } from '../item-join/create-bound-item-jo
14
14
  import { createMaterialPlanningCapability } from '../common/create-material-planning-capability.js';
15
15
  import { responseArrayDefinition } from '../response-array-definition.js';
16
16
  import { responseCollectionDefinition } from '../response-collection-definition.js';
17
+ import { apiResponseObjectDefinition } from '../objects/api-response-object-definition.js';
18
+ import { createApiResponseObjectCapability } from '../objects/create-api-response-object-capability.js';
17
19
  import { nativeArrayDefinition } from '../arrays/native-array-definition.js';
18
20
  import { itemJoinDefinition } from '../item-join/item-join-definition.js';
19
21
  import { nativeFeatureMaterialRequirements } from './native-feature-material-requirements.js';
@@ -66,6 +68,11 @@ export const nativeFeatureDefinitions = {
66
68
  })),
67
69
  create: (context) => createResponseCollectionCapability({ materials: context.apiMaterials }),
68
70
  },
71
+ [N8nNativeFeature.ComposeApiResponses]: {
72
+ requirements: nativeFeatureMaterialRequirements(apiResponseObjectDefinition),
73
+ createPlanning: () => createMaterialPlanningCapability(apiResponseObjectDefinition, (sources) => createApiResponseObjectCapability({ materials: sources.apiMaterials })),
74
+ create: (context) => createApiResponseObjectCapability({ materials: context.apiMaterials }),
75
+ },
69
76
  [N8nNativeFeature.SplitNativeOutputArray]: {
70
77
  requirements: nativeFeatureMaterialRequirements(nativeArrayDefinition),
71
78
  createPlanning: (context) => createMaterialPlanningCapability(nativeArrayDefinition, (sources) => createBoundNativeArrayCapability({
@@ -33,6 +33,8 @@ export { createNativeArrayCapability } from './nodes/native/arrays/create-native
33
33
  export type * from './types/native-array.js';
34
34
  export { createResponseCollectionCapability } from './nodes/native/create-response-collection-capability.js';
35
35
  export type * from './types/response-collection.js';
36
+ export { createApiResponseObjectCapability } from './nodes/native/objects/create-api-response-object-capability.js';
37
+ export type * from './types/api-response-object.js';
36
38
  export { createItemJoinCapability } from './nodes/native/item-join/create-item-join-capability.js';
37
39
  export type * from './types/item-join.js';
38
40
  export { compileBatchedN8nWorkflow } from './workflow/batch/compile-batched-n8n-workflow.js';
@@ -19,6 +19,7 @@ export { resolveHttpRequestAuthentication } from './nodes/request/authentication
19
19
  export { createResponseArrayCapability } from './nodes/native/create-response-array-capability.js';
20
20
  export { createNativeArrayCapability } from './nodes/native/arrays/create-native-array-capability.js';
21
21
  export { createResponseCollectionCapability } from './nodes/native/create-response-collection-capability.js';
22
+ export { createApiResponseObjectCapability } from './nodes/native/objects/create-api-response-object-capability.js';
22
23
  export { createItemJoinCapability } from './nodes/native/item-join/create-item-join-capability.js';
23
24
  export { compileBatchedN8nWorkflow } from './workflow/batch/compile-batched-n8n-workflow.js';
24
25
  export { describeN8nManualTrigger } from './workflow/entrypoint/manual-trigger.js';
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ export declare const apiResponseObjectParametersSchema: z.ZodObject<{
3
+ fields: z.ZodArray<z.ZodObject<{
4
+ name: z.ZodString;
5
+ sourceNodeId: z.ZodString;
6
+ pointer: z.ZodString;
7
+ }, z.core.$strict>>;
8
+ }, z.core.$strict>;
9
+ export declare function createApiResponseObjectParametersSchema(callIds: string[]): z.ZodObject<{
10
+ fields: z.ZodArray<z.ZodObject<{
11
+ name: z.ZodString;
12
+ pointer: z.ZodString;
13
+ sourceNodeId: z.ZodEnum<{
14
+ [x: string]: string;
15
+ }>;
16
+ }, z.core.$strict>>;
17
+ }, z.core.$strict>;
@@ -0,0 +1,46 @@
1
+ import { z } from 'zod';
2
+ const apiResponseObjectFieldSchema = z.strictObject({
3
+ name: z
4
+ .string()
5
+ .describe('Literal output property name; dots are not nesting instructions.'),
6
+ sourceNodeId: z
7
+ .string()
8
+ .min(1)
9
+ .describe('An existing selected API callId, never an operation name or invented producer.'),
10
+ pointer: z
11
+ .string()
12
+ .regex(/^(?:\/(?:[^~]|~[01])*)*$/u)
13
+ .describe('RFC 6901 pointer into the unchanged API response body; empty string selects the whole body.'),
14
+ });
15
+ export const apiResponseObjectParametersSchema = z
16
+ .strictObject({
17
+ fields: z
18
+ .array(apiResponseObjectFieldSchema)
19
+ .min(1)
20
+ .describe('Explicit named output fields. Each value is copied unchanged from the declared OAS response. No formatting, casting, merging, calculations or inferred matching.'),
21
+ })
22
+ .superRefine((value, context) => {
23
+ const names = new Set();
24
+ value.fields.forEach((field, index) => {
25
+ if (names.has(field.name))
26
+ context.addIssue({
27
+ code: 'custom',
28
+ path: ['fields', index, 'name'],
29
+ message: 'Output property names must be unique',
30
+ });
31
+ names.add(field.name);
32
+ });
33
+ });
34
+ export function createApiResponseObjectParametersSchema(callIds) {
35
+ if (!callIds.length)
36
+ throw new Error('response object parameters require API callIds');
37
+ return apiResponseObjectParametersSchema.safeExtend({
38
+ fields: z
39
+ .array(apiResponseObjectFieldSchema.extend({
40
+ sourceNodeId: z
41
+ .enum(callIds)
42
+ .describe('Only a supplied API callId. Code derives the output schema from this source contract.'),
43
+ }))
44
+ .min(1),
45
+ });
46
+ }
@@ -9,6 +9,7 @@ export declare const n8nNativeFeatureSelectionSchema: z.ZodArray<z.ZodEnum<Reado
9
9
  readonly JsonOutput: "json-output";
10
10
  readonly SplitApiResponseArray: "split-api-response-array";
11
11
  readonly CollectApiResponses: "collect-api-responses";
12
+ readonly ComposeApiResponses: "compose-api-responses";
12
13
  readonly SplitNativeOutputArray: "split-native-output-array";
13
14
  readonly JoinApiItems: "join-api-items";
14
15
  }>>>;
@@ -0,0 +1,12 @@
1
+ import type { z } from 'zod';
2
+ import type { ApiBindingMaterial } from '@openapi-flow/core';
3
+ import type { apiResponseObjectParametersSchema } from '../schemas/api-response-object-schema.js';
4
+ import type { CompileNativeNodeInput } from './native-capability.js';
5
+ export type ApiResponseObjectParameters = z.infer<typeof apiResponseObjectParametersSchema>;
6
+ export interface CreateApiResponseObjectCapabilityInput {
7
+ materials: ApiBindingMaterial[];
8
+ }
9
+ export interface CompileApiResponseObjectInput extends CompileNativeNodeInput {
10
+ parameters: ApiResponseObjectParameters;
11
+ materials: ApiBindingMaterial[];
12
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -12,6 +12,7 @@ export declare const N8nNativeFeature: Readonly<{
12
12
  readonly JsonOutput: "json-output";
13
13
  readonly SplitApiResponseArray: "split-api-response-array";
14
14
  readonly CollectApiResponses: "collect-api-responses";
15
+ readonly ComposeApiResponses: "compose-api-responses";
15
16
  readonly SplitNativeOutputArray: "split-native-output-array";
16
17
  readonly JoinApiItems: "join-api-items";
17
18
  }>;
@@ -9,6 +9,7 @@ export const N8nNativeFeature = Object.freeze({
9
9
  JsonOutput: 'json-output',
10
10
  SplitApiResponseArray: 'split-api-response-array',
11
11
  CollectApiResponses: 'collect-api-responses',
12
+ ComposeApiResponses: 'compose-api-responses',
12
13
  SplitNativeOutputArray: 'split-native-output-array',
13
14
  JoinApiItems: 'join-api-items',
14
15
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openapi-flow/n8n",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Deterministic OpenAPI request nodes and n8n workflow assembly",
@@ -30,7 +30,7 @@
30
30
  "dependencies": {
31
31
  "@n8n/config": "2.41.2",
32
32
  "@n8n/workflow-sdk": "0.36.4",
33
- "@openapi-flow/core": "0.3.7",
33
+ "@openapi-flow/core": "0.3.8",
34
34
  "@sentry/node": "10.70.0",
35
35
  "@types/estree": "1.0.8",
36
36
  "@types/express": "5.0.1",