x-openapi-flow 1.4.3 → 1.5.0

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.
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+
3
+ const { createRuntimeFlowGuard, toErrorPayload } = require("./core");
4
+
5
+ function resolveExpressOperationId(req) {
6
+ if (req && req.openapi && req.openapi.operationId) {
7
+ return req.openapi.operationId;
8
+ }
9
+
10
+ if (req && req.operation && req.operation.operationId) {
11
+ return req.operation.operationId;
12
+ }
13
+
14
+ return null;
15
+ }
16
+
17
+ function defaultExpressPath(req) {
18
+ if (!req) {
19
+ return "/";
20
+ }
21
+
22
+ if (typeof req.path === "string") {
23
+ return req.path;
24
+ }
25
+
26
+ if (typeof req.originalUrl === "string") {
27
+ return req.originalUrl.split("?")[0];
28
+ }
29
+
30
+ if (typeof req.url === "string") {
31
+ return req.url.split("?")[0];
32
+ }
33
+
34
+ return "/";
35
+ }
36
+
37
+ function createExpressFlowGuard(options = {}) {
38
+ const guard = createRuntimeFlowGuard({
39
+ ...options,
40
+ resolveOperationId: options.resolveOperationId || ((context) => resolveExpressOperationId(context.req)),
41
+ });
42
+
43
+ return async function xOpenApiFlowExpressGuard(req, res, next) {
44
+ try {
45
+ await guard.enforce({
46
+ req,
47
+ res,
48
+ method: req && req.method,
49
+ path: defaultExpressPath(req),
50
+ params: (req && req.params) || {},
51
+ });
52
+ return next();
53
+ } catch (error) {
54
+ const payload = {
55
+ error: toErrorPayload(error),
56
+ };
57
+
58
+ const statusCode = (error && error.statusCode) || 500;
59
+ if (res && typeof res.status === "function" && typeof res.json === "function") {
60
+ return res.status(statusCode).json(payload);
61
+ }
62
+
63
+ return next(error);
64
+ }
65
+ };
66
+ }
67
+
68
+ module.exports = {
69
+ createExpressFlowGuard,
70
+ };
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+
3
+ const { createRuntimeFlowGuard, toErrorPayload } = require("./core");
4
+
5
+ function resolveFastifyOperationId(request) {
6
+ const routeConfig = request
7
+ && request.routeOptions
8
+ && request.routeOptions.config;
9
+
10
+ if (routeConfig && typeof routeConfig.operationId === "string") {
11
+ return routeConfig.operationId;
12
+ }
13
+
14
+ return null;
15
+ }
16
+
17
+ function defaultFastifyPath(request) {
18
+ if (!request) {
19
+ return "/";
20
+ }
21
+
22
+ if (request.routeOptions && request.routeOptions.url) {
23
+ return request.routeOptions.url;
24
+ }
25
+
26
+ if (typeof request.routerPath === "string") {
27
+ return request.routerPath;
28
+ }
29
+
30
+ if (typeof request.url === "string") {
31
+ return request.url.split("?")[0];
32
+ }
33
+
34
+ return "/";
35
+ }
36
+
37
+ function createFastifyFlowGuard(options = {}) {
38
+ const guard = createRuntimeFlowGuard({
39
+ ...options,
40
+ resolveOperationId: options.resolveOperationId || ((context) => resolveFastifyOperationId(context.req)),
41
+ });
42
+
43
+ return async function xOpenApiFlowFastifyGuard(request, reply) {
44
+ try {
45
+ await guard.enforce({
46
+ req: request,
47
+ reply,
48
+ method: request && request.method,
49
+ path: defaultFastifyPath(request),
50
+ params: (request && request.params) || {},
51
+ });
52
+ } catch (error) {
53
+ const statusCode = (error && error.statusCode) || 500;
54
+ return reply.code(statusCode).send({
55
+ error: toErrorPayload(error),
56
+ });
57
+ }
58
+
59
+ return undefined;
60
+ };
61
+ }
62
+
63
+ module.exports = {
64
+ createFastifyFlowGuard,
65
+ };
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+
3
+ const { createRuntimeFlowGuard, RuntimeFlowGuard, toErrorPayload } = require("./core");
4
+ const { createExpressFlowGuard } = require("./express");
5
+ const { createFastifyFlowGuard } = require("./fastify");
6
+ const { FlowGuardError } = require("./errors");
7
+
8
+ module.exports = {
9
+ createRuntimeFlowGuard,
10
+ RuntimeFlowGuard,
11
+ createExpressFlowGuard,
12
+ createFastifyFlowGuard,
13
+ FlowGuardError,
14
+ toErrorPayload,
15
+ };
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+
3
+ const { createStateMachineAdapterModel, normalizePathTemplate, routeKey } = require("../openapi-state-machine-adapter");
4
+ const { createStateMachineEngine } = require("../state-machine-engine");
5
+
6
+ function buildPathRegex(pathTemplate) {
7
+ const escaped = normalizePathTemplate(pathTemplate)
8
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
9
+ .replace(/\\\{[^}]+\\\}/g, "[^/]+");
10
+
11
+ return new RegExp(`^${escaped}$`);
12
+ }
13
+
14
+ function indexOperations(operations) {
15
+ const byOperationId = new Map();
16
+ const byRouteKey = new Map();
17
+
18
+ for (const operation of operations) {
19
+ const indexed = {
20
+ ...operation,
21
+ incomingFromStates: new Set(),
22
+ };
23
+
24
+ byOperationId.set(indexed.operationId, indexed);
25
+ byRouteKey.set(indexed.routeKey, indexed);
26
+ }
27
+
28
+ // Build incoming transition map. Priority for explicit next_operation_id.
29
+ const machineTransitions = [];
30
+
31
+ for (const source of operations) {
32
+ for (const transition of source.transitions) {
33
+ if (transition.next_operation_id && byOperationId.has(transition.next_operation_id)) {
34
+ const targetByOperation = byOperationId.get(transition.next_operation_id);
35
+ targetByOperation.incomingFromStates.add(source.currentState);
36
+
37
+ machineTransitions.push({
38
+ from: source.currentState,
39
+ action: transition.next_operation_id,
40
+ to: targetByOperation.currentState,
41
+ });
42
+ continue;
43
+ }
44
+
45
+ // Fallback for flows without next_operation_id: map by target_state -> current_state.
46
+ if (!transition.target_state) {
47
+ continue;
48
+ }
49
+
50
+ for (const target of byOperationId.values()) {
51
+ if (target.currentState === transition.target_state) {
52
+ target.incomingFromStates.add(source.currentState);
53
+
54
+ machineTransitions.push({
55
+ from: source.currentState,
56
+ action: target.operationId,
57
+ to: target.currentState,
58
+ });
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ const stateMachine = createStateMachineEngine({
65
+ transitions: machineTransitions,
66
+ });
67
+
68
+ return {
69
+ operations: [...byOperationId.values()],
70
+ byOperationId,
71
+ byRouteKey,
72
+ stateMachine,
73
+ };
74
+ }
75
+
76
+ function loadRuntimeModel(options) {
77
+ const model = createStateMachineAdapterModel(options || {});
78
+ return indexOperations(model.operations);
79
+ }
80
+
81
+ module.exports = {
82
+ loadRuntimeModel,
83
+ normalizePathTemplate,
84
+ routeKey,
85
+ };
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+
3
+ module.exports = require("./runtime-guard/index.js");
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+
3
+ function normalizeState(value) {
4
+ return value == null ? null : String(value);
5
+ }
6
+
7
+ function normalizeAction(value) {
8
+ return value == null ? null : String(value);
9
+ }
10
+
11
+ function validateDefinition(definition) {
12
+ const errors = [];
13
+
14
+ if (!definition || typeof definition !== "object") {
15
+ return {
16
+ ok: false,
17
+ errors: ["State machine definition must be an object."],
18
+ };
19
+ }
20
+
21
+ if (!Array.isArray(definition.transitions)) {
22
+ return {
23
+ ok: false,
24
+ errors: ["State machine definition requires a transitions array."],
25
+ };
26
+ }
27
+
28
+ for (let index = 0; index < definition.transitions.length; index += 1) {
29
+ const transition = definition.transitions[index];
30
+
31
+ if (!transition || typeof transition !== "object") {
32
+ errors.push(`Transition at index ${index} must be an object.`);
33
+ continue;
34
+ }
35
+
36
+ const from = normalizeState(transition.from);
37
+ const action = normalizeAction(transition.action);
38
+ const to = normalizeState(transition.to);
39
+
40
+ if (!from) {
41
+ errors.push(`Transition at index ${index} has invalid 'from' state.`);
42
+ }
43
+ if (!action) {
44
+ errors.push(`Transition at index ${index} has invalid 'action'.`);
45
+ }
46
+ if (!to) {
47
+ errors.push(`Transition at index ${index} has invalid 'to' state.`);
48
+ }
49
+ }
50
+
51
+ return {
52
+ ok: errors.length === 0,
53
+ errors,
54
+ };
55
+ }
56
+
57
+ class StateMachineEngine {
58
+ constructor(definition) {
59
+ const validation = validateDefinition(definition);
60
+ if (!validation.ok) {
61
+ throw new Error(`Invalid state machine definition: ${validation.errors.join(" ")}`);
62
+ }
63
+
64
+ this._transitions = [];
65
+ this._states = new Set();
66
+ this._actionsByState = new Map();
67
+ this._nextStateByStateAndAction = new Map();
68
+
69
+ for (const transition of definition.transitions) {
70
+ const from = normalizeState(transition.from);
71
+ const action = normalizeAction(transition.action);
72
+ const to = normalizeState(transition.to);
73
+
74
+ const key = `${from}::${action}`;
75
+ const existing = this._nextStateByStateAndAction.get(key);
76
+ if (existing && existing !== to) {
77
+ throw new Error(
78
+ `Non-deterministic transition detected for state '${from}' and action '${action}': '${existing}' vs '${to}'.`
79
+ );
80
+ }
81
+
82
+ if (!existing) {
83
+ this._nextStateByStateAndAction.set(key, to);
84
+ this._transitions.push({ from, action, to });
85
+ }
86
+
87
+ if (!this._actionsByState.has(from)) {
88
+ this._actionsByState.set(from, new Set());
89
+ }
90
+ this._actionsByState.get(from).add(action);
91
+
92
+ this._states.add(from);
93
+ this._states.add(to);
94
+ }
95
+
96
+ this._transitions.sort((a, b) => {
97
+ if (a.from !== b.from) return a.from.localeCompare(b.from);
98
+ if (a.action !== b.action) return a.action.localeCompare(b.action);
99
+ return a.to.localeCompare(b.to);
100
+ });
101
+ }
102
+
103
+ canTransition(currentState, action) {
104
+ const from = normalizeState(currentState);
105
+ const op = normalizeAction(action);
106
+ if (!from || !op) {
107
+ return false;
108
+ }
109
+
110
+ return this._nextStateByStateAndAction.has(`${from}::${op}`);
111
+ }
112
+
113
+ getNextState(currentState, action) {
114
+ const from = normalizeState(currentState);
115
+ const op = normalizeAction(action);
116
+ if (!from || !op) {
117
+ return null;
118
+ }
119
+
120
+ return this._nextStateByStateAndAction.get(`${from}::${op}`) || null;
121
+ }
122
+
123
+ getAvailableActions(currentState) {
124
+ const from = normalizeState(currentState);
125
+ if (!from) {
126
+ return [];
127
+ }
128
+
129
+ const actions = this._actionsByState.get(from);
130
+ if (!actions) {
131
+ return [];
132
+ }
133
+
134
+ return [...actions].sort((a, b) => a.localeCompare(b));
135
+ }
136
+
137
+ validateFlow(options = {}) {
138
+ const actions = Array.isArray(options.actions) ? options.actions : [];
139
+ const startState = normalizeState(options.startState);
140
+
141
+ if (!startState) {
142
+ return {
143
+ ok: false,
144
+ error: {
145
+ code: "INVALID_START_STATE",
146
+ message: "startState is required.",
147
+ index: -1,
148
+ },
149
+ };
150
+ }
151
+
152
+ let currentState = startState;
153
+
154
+ for (let index = 0; index < actions.length; index += 1) {
155
+ const action = normalizeAction(actions[index]);
156
+ if (!action) {
157
+ return {
158
+ ok: false,
159
+ error: {
160
+ code: "INVALID_ACTION",
161
+ message: `Action at index ${index} is invalid.`,
162
+ index,
163
+ state: currentState,
164
+ },
165
+ };
166
+ }
167
+
168
+ if (!this.canTransition(currentState, action)) {
169
+ return {
170
+ ok: false,
171
+ error: {
172
+ code: "INVALID_TRANSITION",
173
+ message: `Cannot apply action '${action}' from state '${currentState}'.`,
174
+ index,
175
+ state: currentState,
176
+ action,
177
+ availableActions: this.getAvailableActions(currentState),
178
+ },
179
+ };
180
+ }
181
+
182
+ currentState = this.getNextState(currentState, action);
183
+ }
184
+
185
+ return {
186
+ ok: true,
187
+ finalState: currentState,
188
+ };
189
+ }
190
+
191
+ getTransitions() {
192
+ return this._transitions.map((transition) => ({ ...transition }));
193
+ }
194
+
195
+ getStates() {
196
+ return [...this._states].sort((a, b) => a.localeCompare(b));
197
+ }
198
+ }
199
+
200
+ function createStateMachineEngine(definition) {
201
+ return new StateMachineEngine(definition);
202
+ }
203
+
204
+ module.exports = {
205
+ StateMachineEngine,
206
+ createStateMachineEngine,
207
+ validateDefinition,
208
+ };