@lowdefy/api 6.0.0 → 6.1.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.
Files changed (46) hide show
  1. package/dist/context/createEvaluateOperators.js +3 -1
  2. package/dist/context/getCurrentEnvironment.js +27 -0
  3. package/dist/index.js +2 -1
  4. package/dist/response/buildEndpointResult.js +3 -1
  5. package/dist/response/createWireProjection.js +88 -0
  6. package/dist/response/normalizeErrorSources.js +10 -13
  7. package/dist/response/redactErrorResponse.js +42 -8
  8. package/dist/response/redactResponse.js +10 -10
  9. package/dist/routes/agent/prepareAgent.js +6 -0
  10. package/dist/routes/auth/defaultAuthUrl.js +31 -0
  11. package/dist/routes/auth/getAuthConfig.js +5 -1
  12. package/dist/routes/endpoints/callEndpoint.js +1 -0
  13. package/dist/routes/endpoints/control/controlFor.js +4 -7
  14. package/dist/routes/endpoints/control/controlIf.js +4 -8
  15. package/dist/routes/endpoints/control/controlLog.js +6 -14
  16. package/dist/routes/endpoints/control/controlParallelFor.js +4 -7
  17. package/dist/routes/endpoints/control/controlReject.js +13 -15
  18. package/dist/routes/endpoints/control/controlReturn.js +4 -9
  19. package/dist/routes/endpoints/control/controlSetState.js +4 -8
  20. package/dist/routes/endpoints/control/controlSwitch.js +4 -8
  21. package/dist/routes/endpoints/control/controlThrow.js +17 -19
  22. package/dist/routes/endpoints/control/controlTry.js +20 -10
  23. package/dist/routes/endpoints/evaluateRoutineOperators.js +26 -0
  24. package/dist/routes/endpoints/forwardScheduledEndpoint.js +19 -14
  25. package/dist/routes/endpoints/getEnvironmentSchedules.js +5 -7
  26. package/dist/routes/endpoints/handleAgentCall.js +4 -7
  27. package/dist/routes/endpoints/handleEndpointCall.js +4 -7
  28. package/dist/routes/endpoints/handleRenderNotification.js +15 -8
  29. package/dist/routes/endpoints/handleRequest.js +1 -5
  30. package/dist/routes/endpoints/handleValidateSchema.js +12 -10
  31. package/dist/routes/endpoints/invokeEndpoint.js +1 -0
  32. package/dist/routes/endpoints/operatorScope.js +27 -0
  33. package/dist/routes/endpoints/resolveCronEnvironment.js +7 -6
  34. package/dist/routes/endpoints/runDetachedEndpoint.js +1 -0
  35. package/dist/routes/endpoints/runScheduledEndpoint.js +1 -0
  36. package/dist/routes/endpoints/runWebhookEndpoint.js +1 -0
  37. package/dist/routes/endpoints/test/runTest.js +17 -2
  38. package/dist/routes/mcp/createMcpServer.js +20 -2
  39. package/dist/routes/request/callRequest.js +9 -3
  40. package/dist/routes/request/callRequestResolver.js +6 -0
  41. package/dist/routes/request/evaluateOperators.js +8 -12
  42. package/dist/routes/websocket/createChannelRegistry.js +19 -5
  43. package/dist/routes/websocket/createWebSocketConnection.js +15 -3
  44. package/dist/test/testContext.js +4 -2
  45. package/package.json +9 -9
  46. package/dist/response/omitErrorProps.js +0 -51
@@ -23,8 +23,10 @@ function createEvaluateOperators(context) {
23
23
  secrets,
24
24
  user
25
25
  });
26
- function evaluateOperators({ input, items, location, payload, state, steps }) {
26
+ function evaluateOperators({ arrayIndices, error, input, items, location, payload, state, steps }) {
27
27
  const { output, errors } = operatorsParser.parse({
28
+ arrayIndices,
29
+ error,
28
30
  input,
29
31
  items,
30
32
  location,
@@ -0,0 +1,27 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ // The environment this deployment is, resolved by the build from LOWDEFY_ENVIRONMENT into
16
+ // config.environment and config.environments. Null when no current environment is set or when it
17
+ // is not declared, so callers fall back to their own defaults.
18
+ function getCurrentEnvironment({ config }) {
19
+ const name = config?.environment;
20
+ const settings = config?.environments?.[name];
21
+ if (settings === undefined) return null;
22
+ return {
23
+ ...settings,
24
+ name
25
+ };
26
+ }
27
+ export default getCurrentEnvironment;
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ import createApiContext from './context/createApiContext.js';
21
21
  import createChannelRegistry from './routes/websocket/createChannelRegistry.js';
22
22
  import createMcpServer from './routes/mcp/createMcpServer.js';
23
23
  import createWebSocketConnection from './routes/websocket/createWebSocketConnection.js';
24
+ import createWireProjection from './response/createWireProjection.js';
24
25
  import createSessionCallback from './routes/auth/callbacks/createSessionCallback.js';
25
26
  import getAuthConfig from './routes/auth/getAuthConfig.js';
26
27
  import getAuthStrategies from './routes/auth/strategies/getAuthStrategies.js';
@@ -35,4 +36,4 @@ import forwardScheduledEndpoint from './routes/endpoints/forwardScheduledEndpoin
35
36
  import runDetachedEndpoint from './routes/endpoints/runDetachedEndpoint.js';
36
37
  import runWebhookEndpoint from './routes/endpoints/runWebhookEndpoint.js';
37
38
  import runScheduledEndpoint from './routes/endpoints/runScheduledEndpoint.js';
38
- export { buildEndpointResult, callAgent, callEndpoint, getEndpointConfig, callRequest, createApiContext, createChannelRegistry, createMcpServer, createSessionCallback, createWebSocketConnection, getAuthConfig, getAuthStrategies, getHomeAndMenus, getPageConfig, getRootConfig, logClientError, redactErrorResponse, redactResponse, resolveStrategyCaller, forwardScheduledEndpoint, runDetachedEndpoint, runWebhookEndpoint, runScheduledEndpoint };
39
+ export { buildEndpointResult, callAgent, callEndpoint, getEndpointConfig, callRequest, createApiContext, createChannelRegistry, createMcpServer, createSessionCallback, createWebSocketConnection, createWireProjection, getAuthConfig, getAuthStrategies, getHomeAndMenus, getPageConfig, getRootConfig, logClientError, redactErrorResponse, redactResponse, resolveStrategyCaller, forwardScheduledEndpoint, runDetachedEndpoint, runWebhookEndpoint, runScheduledEndpoint };
@@ -17,7 +17,9 @@ import redactResponse from './redactResponse.js';
17
17
  // The wire object every endpoint route returns after running its routine. One
18
18
  // function rather than a copy of the same return statement per route, so the
19
19
  // `response` field cannot end up policed differently from the `error` field beside
20
- // it - see redactResponse for why the response needs the policy at all.
20
+ // it - see redactResponse for why the response needs the policy at all. `error` is
21
+ // redactErrorResponse's error payload, so in dev it carries `devError`; `response`
22
+ // never does.
21
23
  function buildEndpointResult(context, { error, response, status }) {
22
24
  const success = ![
23
25
  'error',
@@ -0,0 +1,88 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { lowdefyErrorNames, readErrorCodes } from '@lowdefy/errors';
16
+ import { translate } from '@lowdefy/helpers';
17
+ // The framework writes these messages to reveal nothing, and clients branch on
18
+ // them, so they keep their text where every other non-author error does not.
19
+ const AUTH_REFUSAL_NAMES = new Set([
20
+ 'AuthenticationError',
21
+ 'AuthorizationError',
22
+ 'TwoFactorEnrolmentRequiredError'
23
+ ]);
24
+ // The client-bound error policy, as an allowlist. Passed to serializer.serialize
25
+ // as `projectError`, so it runs at every error node the walk emits - the cause
26
+ // chain, an Error-valued own property, an Error nested in a response value - and
27
+ // cannot become depth-limited.
28
+ //
29
+ // Only the author's message crosses. A library's message embeds whatever the
30
+ // library saw - URLs with credentials, SQL, hostnames - so every other error
31
+ // becomes one translated generic message plus the fields client config branches
32
+ // on. The full error stays in the server log, and in dev reaches the developer
33
+ // through `devError`, which config never reads.
34
+ //
35
+ // The context is optional: a websocket broadcast has no single request, so it
36
+ // passes no `rid`, and the server error handler can run before any request
37
+ // context exists.
38
+ function createWireProjection(context) {
39
+ const requestId = context?.rid;
40
+ const genericMessage = translate({
41
+ key: 'server.genericError',
42
+ i18n: context?.i18n
43
+ });
44
+ return function project(err) {
45
+ // A PluginError defaults configKey to null; the walk only drops undefined.
46
+ const configKey = err.configKey ?? undefined;
47
+ // Keyed on the name rather than instanceof: the error may come from another
48
+ // copy of @lowdefy/errors or from a serializer round trip, where the class
49
+ // identity is gone but the name survives. The cause is returned raw so the
50
+ // walk projects it too - an Error cause takes the generic shape, and a
51
+ // non-Error cause is author data cleaned as a value.
52
+ if (err.name === 'UserError') {
53
+ return {
54
+ name: err.name,
55
+ message: err.message,
56
+ cause: err.cause,
57
+ metaData: err.metaData,
58
+ blockId: err.blockId,
59
+ pageId: err.pageId,
60
+ isReject: err.isReject,
61
+ configKey,
62
+ // Not read from the node: a caught error rebuilt for `_error` keeps only
63
+ // its class, and without the flag the client wraps a rethrown UserError
64
+ // in an ActionError and reports it back to the server a second time.
65
+ isLowdefyError: true,
66
+ handled: err.handled,
67
+ requestId
68
+ };
69
+ }
70
+ const { code, statusCode } = readErrorCodes(err);
71
+ return {
72
+ // A foreign class name can itself say which library or driver failed.
73
+ name: lowdefyErrorNames.has(err.name) ? err.name : 'Error',
74
+ message: AUTH_REFUSAL_NAMES.has(err.name) ? err.message : genericMessage,
75
+ code,
76
+ statusCode,
77
+ configKey,
78
+ requestId,
79
+ // The client revives errors without running a constructor, so this flag
80
+ // exists there only if the wire carries it. Without it the action runner
81
+ // wraps the error in a new ActionError without `handled`, POSTs it back to
82
+ // /api/client-error, and the server logs and reports it a second time.
83
+ isLowdefyError: true,
84
+ handled: err.handled
85
+ };
86
+ };
87
+ }
88
+ export default createWireProjection;
@@ -16,13 +16,12 @@
16
16
  import { type } from '@lowdefy/helpers';
17
17
  // extractErrorProps emits a nested error as a plain props object, not as a second
18
18
  // '~e' wrapper, so an error node is recognised by its shape. Keying on the shape
19
- // rather than on a map of where errors can appear is deliberate: re-deriving those
20
- // positions is the mistake the walk-level omit exists to avoid, and this predicate
21
- // still visits every position.
19
+ // rather than on a map of where errors can appear means every position is visited
20
+ // without re-deriving where the walk puts errors.
22
21
  //
23
- // The check matters because the payload also carries author-written data the policy
24
- // deliberately preserves - a UserError's non-Error cause, its metaData - and a
25
- // `source` key inside those belongs to the app, not to us.
22
+ // The check matters because the payload also carries author-written data - a
23
+ // UserError's non-Error cause, its metaData - and a `source` key inside those
24
+ // belongs to the app, not to us.
26
25
  function isErrorNode(value) {
27
26
  return type.isString(value.name) && type.isString(value.message);
28
27
  }
@@ -43,17 +42,15 @@ function stripConfigDirectory(value, prefix) {
43
42
  }
44
43
  Object.values(value).forEach((child)=>stripConfigDirectory(child, prefix));
45
44
  }
46
- // Guarantees `source` reaches a client config-relative, never as an absolute server
47
- // path. resolveConfigLocation makes it absolute whenever the context carries a
48
- // configDirectory - server-dev and server-e2e do, and production happens not to
49
- // today by omission rather than by invariant, which is the drift this closes.
45
+ // Makes `source` in the dev-only `devError` config-relative, never an absolute
46
+ // server path, so the dev tools show the path the developer knows. The wire error
47
+ // carries no `source` at all. resolveConfigLocation makes it absolute whenever the
48
+ // context carries a configDirectory, which server-dev does.
50
49
  //
51
50
  // A rewrite rather than an omission, and it needs the context, so it runs as a pass
52
51
  // over the serialized payload instead of inside the walk.
53
52
  function normalizeErrorSources(context, payload) {
54
- // errorHandler has an else branch for requests with no lowdefyContext, so a
55
- // missing context is a supported call - it only means no source to normalise.
56
- const configDirectory = context?.configDirectory;
53
+ const configDirectory = context.configDirectory;
57
54
  if (type.isNone(configDirectory)) return payload;
58
55
  // configDirectory is `LOWDEFY_DIRECTORY_CONFIG || process.cwd()` at every site
59
56
  // that sets it, so it may be relative or carry a trailing separator, while
@@ -13,18 +13,52 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { serializer, type } from '@lowdefy/helpers';
16
+ import createWireProjection from './createWireProjection.js';
16
17
  import normalizeErrorSources from './normalizeErrorSources.js';
17
- import omitErrorProps from './omitErrorProps.js';
18
- // Owns the serialization as well as the policy, so the policy cannot be forgotten:
19
- // no bare serializer.serialize(error) is left in response position to wrap. Every
20
- // route returning an error to a caller - any status, any transport - goes through
21
- // here or through buildEndpointResult.
18
+ // The one error payload every route and transport sends for a thrown error, at
19
+ // any status. Owning the serialization as well as the policy leaves no bare
20
+ // serializer.serialize(error) in response position for a caller to forget.
21
+ //
22
+ // `~e` is the wire error - identical in dev and prod, because it is what app
23
+ // config reads. In dev the payload also carries `devError`, the error as the dev
24
+ // terminal logs it, for the dev tools. It sits beside `~e` rather than inside it:
25
+ // the serializer's reviver replaces any object holding `~e` with its error, so a
26
+ // plain deserialize of the payload drops `devError` and config never sees it.
27
+ //
28
+ // `received` is left out of devError at every node: on the request path it is
29
+ // the evaluated request properties, resolved _secret values included, and the
30
+ // dev server can be reached over a LAN or tunnel. The terminal still logs it.
31
+ function projectDevError(err) {
32
+ const props = {
33
+ message: err.message,
34
+ name: err.name,
35
+ stack: err.stack,
36
+ cause: err.cause,
37
+ ...err
38
+ };
39
+ delete props.received;
40
+ return props;
41
+ }
22
42
  function redactErrorResponse(context, error) {
23
43
  // Endpoint routes serialize the error field on success too, where it is null.
24
44
  // Passing that through unchanged keeps them from emitting an empty {'~e'}.
25
45
  if (type.isNone(error)) return error;
26
- return normalizeErrorSources(context, serializer.serialize(error, {
27
- omitErrorProps
28
- }));
46
+ const payload = serializer.serialize(error, {
47
+ projectError: createWireProjection(context)
48
+ });
49
+ // The server error handler has no request context when it fails before the
50
+ // context middleware runs, so there is no mode to read and no devError.
51
+ if (context?.mode === 'dev') {
52
+ const full = serializer.serialize(error, {
53
+ projectError: projectDevError
54
+ });
55
+ payload.devError = normalizeErrorSources(context, {
56
+ '~e': {
57
+ ...full['~e'],
58
+ requestId: context.rid
59
+ }
60
+ });
61
+ }
62
+ return payload;
29
63
  }
30
64
  export default redactErrorResponse;
@@ -13,19 +13,19 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { serializer } from '@lowdefy/helpers';
16
- import normalizeErrorSources from './normalizeErrorSources.js';
17
- import omitErrorProps from './omitErrorProps.js';
16
+ import createWireProjection from './createWireProjection.js';
18
17
  // The response-value call shape, beside redactErrorResponse's error-only one.
19
18
  // A response is not an error, but makeReplacer wraps any Error it meets anywhere
20
- // in a value, so a response holding one is an error-serialization site too - the
21
- // grep that enumerated those sites could not see them, which is why this exists
22
- // as a function rather than as a rule to remember.
19
+ // in a value, so a response holding one is an error-serialization site too - a
20
+ // grep for serialize(error) cannot see those sites, which is why this exists as
21
+ // a function rather than as a rule to remember.
23
22
  //
24
- // Same policy as the error field, because it reaches the same audience: a browser
25
- // for a request or endpoint body, a third party for cron and detached.
23
+ // Same wire projection as the error field, because it reaches the same audience.
24
+ // No devError, in any mode: an Error in a response value is data the routine
25
+ // returned, not a failure of this request.
26
26
  function redactResponse(context, response) {
27
- return normalizeErrorSources(context, serializer.serialize(response, {
28
- omitErrorProps
29
- }));
27
+ return serializer.serialize(response, {
28
+ projectError: createWireProjection(context)
29
+ });
30
30
  }
31
31
  export default redactResponse;
@@ -14,6 +14,7 @@
14
14
  limitations under the License.
15
15
  */ import { type } from '@lowdefy/helpers';
16
16
  import buildEndpointResult from '../../response/buildEndpointResult.js';
17
+ import createWireProjection from '../../response/createWireProjection.js';
17
18
  import getEndpointConfig from '../endpoints/getEndpointConfig.js';
18
19
  import invokeEndpoint from '../endpoints/invokeEndpoint.js';
19
20
  import authorizeAgent from './authorizeAgent.js';
@@ -72,6 +73,11 @@ async function prepareAgent(context, { agentId, agentContext, endpointDepth = 0,
72
73
  agentContext,
73
74
  i18n: context.i18n,
74
75
  mode,
76
+ // The agent stream's error text reaches the end user and AgentChat config, so it takes
77
+ // the wire policy. Built here because the agent plugins cannot import @lowdefy/api.
78
+ // The AI SDK types onError's argument as unknown: a tool can reject with null or a
79
+ // string, which takes the generic message like any other non-author error.
80
+ wireErrorMessage: (error)=>createWireProjection(context)(type.isError(error) ? error : new Error(String(error))).message,
75
81
  evaluateOperators: (input)=>context.evaluateOperators({
76
82
  input,
77
83
  location: agentConfig.agentId,
@@ -0,0 +1,31 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { type } from '@lowdefy/helpers';
16
+ import getCurrentEnvironment from '../../context/getCurrentEnvironment.js';
17
+ // Auth.js reads its base URL from AUTH_URL (NEXTAUTH_URL is aliased to it at startup). Default it to
18
+ // the current environment's url so a deployment declared in config.environments needs no separate
19
+ // AUTH_URL / NEXTAUTH_URL. The dev server build drops the current environment's url, so the dev
20
+ // server keeps the request origin.
21
+ function defaultAuthUrl({ config }) {
22
+ // Same "is it set" test as resolveCookies, so both agree on whether an auth URL is configured.
23
+ if (!type.isNone(process.env.AUTH_URL ?? process.env.NEXTAUTH_URL)) return;
24
+ const url = getCurrentEnvironment({
25
+ config
26
+ })?.url;
27
+ if (!type.isNone(url)) {
28
+ process.env.AUTH_URL = url;
29
+ }
30
+ }
31
+ export default defaultAuthUrl;
@@ -19,11 +19,15 @@ import createCallbacks from './callbacks/createCallbacks.js';
19
19
  import createEvents from './events/createEvents.js';
20
20
  import createLogger from './createLogger.js';
21
21
  import createProviders from './createProviders.js';
22
+ import defaultAuthUrl from './defaultAuthUrl.js';
22
23
  import resolveCookies from './resolveCookies.js';
23
24
  const authConfigCache = {};
24
25
  let initialized = false;
25
- function getAuthConfig({ appMeta, authJson, dev, logger, plugins, secrets }) {
26
+ function getAuthConfig({ appMeta, authJson, config, dev, logger, plugins, secrets }) {
26
27
  if (initialized) return authConfigCache;
28
+ defaultAuthUrl({
29
+ config
30
+ });
27
31
  const operatorsParser = new ServerParser({
28
32
  lowdefyApp: appMeta,
29
33
  operators: {
@@ -57,6 +57,7 @@ async function callEndpoint(context, { blockId, endpointId, pageId, payload }) {
57
57
  steps: {},
58
58
  payload: serializer.deserialize(payload),
59
59
  arrayIndices: [],
60
+ error: null,
60
61
  items: {},
61
62
  state: {},
62
63
  endpointDepth: 0
@@ -14,20 +14,17 @@
14
14
  limitations under the License.
15
15
  */ import { ConfigError } from '@lowdefy/errors';
16
16
  import runRoutine from '../runRoutine.js';
17
+ import evaluateRoutineOperators from '../evaluateRoutineOperators.js';
17
18
  async function controlFor(context, routineContext, { control }) {
18
- const { endpointId, logger, evaluateOperators } = context;
19
+ const { endpointId, logger } = context;
19
20
  const { items } = routineContext;
20
21
  const itemName = control[':for'];
21
22
  if (!itemName) {
22
23
  throw new Error(`Invalid :for in endpoint "${endpointId}" - missing variable name in :for.`);
23
24
  }
24
- const array = evaluateOperators({
25
+ const array = evaluateRoutineOperators(context, routineContext, {
25
26
  input: control[':in'],
26
- items,
27
- location: control['~k'] ?? ':for',
28
- payload: routineContext.payload,
29
- state: routineContext.state,
30
- steps: routineContext.steps
27
+ location: control['~k'] ?? ':for'
31
28
  });
32
29
  logger.debug({
33
30
  event: 'debug_control_for',
@@ -13,16 +13,12 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import runRoutine from '../runRoutine.js';
16
+ import evaluateRoutineOperators from '../evaluateRoutineOperators.js';
16
17
  async function controlIf(context, routineContext, { control }) {
17
- const { endpointId, logger, evaluateOperators } = context;
18
- const { items } = routineContext;
19
- const evaluatedIf = evaluateOperators({
18
+ const { endpointId, logger } = context;
19
+ const evaluatedIf = evaluateRoutineOperators(context, routineContext, {
20
20
  input: control[':if'],
21
- items,
22
- location: control['~k'] ?? ':if',
23
- payload: routineContext.payload,
24
- state: routineContext.state,
25
- steps: routineContext.steps
21
+ location: control['~k'] ?? ':if'
26
22
  });
27
23
  logger.debug({
28
24
  event: 'debug_control_if',
@@ -14,28 +14,20 @@
14
14
  limitations under the License.
15
15
  */ import { type } from '@lowdefy/helpers';
16
16
  import { ConfigError } from '@lowdefy/errors';
17
+ import evaluateRoutineOperators from '../evaluateRoutineOperators.js';
17
18
  async function controlLog(context, routineContext, { control }) {
18
- const { endpointId, logger, evaluateOperators } = context;
19
- const { items } = routineContext;
19
+ const { endpointId, logger } = context;
20
20
  const location = control['~k'] ?? ':log';
21
21
  logger.debug({
22
22
  event: 'debug_control_log'
23
23
  });
24
- const log = evaluateOperators({
24
+ const log = evaluateRoutineOperators(context, routineContext, {
25
25
  input: control[':log'],
26
- items,
27
- location,
28
- payload: routineContext.payload,
29
- state: routineContext.state,
30
- steps: routineContext.steps
26
+ location
31
27
  });
32
- const logLevel = evaluateOperators({
28
+ const logLevel = evaluateRoutineOperators(context, routineContext, {
33
29
  input: control[':level'],
34
- items,
35
- location,
36
- payload: routineContext.payload,
37
- state: routineContext.state,
38
- steps: routineContext.steps
30
+ location
39
31
  }) ?? 'info';
40
32
  if (!type.isString(logLevel)) {
41
33
  throw new ConfigError(`Invalid :log in endpoint "${endpointId}" - :level must be a string.`, {
@@ -14,20 +14,17 @@
14
14
  limitations under the License.
15
15
  */ import { ConfigError } from '@lowdefy/errors';
16
16
  import runRoutine from '../runRoutine.js';
17
+ import evaluateRoutineOperators from '../evaluateRoutineOperators.js';
17
18
  async function controlParallelFor(context, routineContext, { control }) {
18
- const { endpointId, logger, evaluateOperators } = context;
19
+ const { endpointId, logger } = context;
19
20
  const { items } = routineContext;
20
21
  const itemName = control[':parallel_for'];
21
22
  if (!itemName) {
22
23
  throw new Error(`Invalid :parallel_for in endpoint "${endpointId}" - missing variable name in :parallel_for.`);
23
24
  }
24
- const array = evaluateOperators({
25
+ const array = evaluateRoutineOperators(context, routineContext, {
25
26
  input: control[':in'],
26
- items,
27
- location: control['~k'] ?? ':parallel_for',
28
- payload: routineContext.payload,
29
- state: routineContext.state,
30
- steps: routineContext.steps
27
+ location: control['~k'] ?? ':parallel_for'
31
28
  });
32
29
  logger.debug({
33
30
  event: 'debug_control_parallel',
@@ -13,27 +13,25 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { UserError } from '@lowdefy/errors';
16
+ import { type } from '@lowdefy/helpers';
17
+ import createWireProjection from '../../../response/createWireProjection.js';
18
+ import evaluateRoutineOperators from '../evaluateRoutineOperators.js';
16
19
  async function controlReject(context, routineContext, { control }) {
17
- const { evaluateOperators } = context;
18
- const { items } = routineContext;
19
20
  const location = control['~k'] ?? ':reject';
20
- const message = evaluateOperators({
21
+ const message = evaluateRoutineOperators(context, routineContext, {
21
22
  input: control[':reject'],
22
- items,
23
- location,
24
- payload: routineContext.payload,
25
- state: routineContext.state,
26
- steps: routineContext.steps
23
+ location
27
24
  });
28
- const cause = evaluateOperators({
25
+ const cause = evaluateRoutineOperators(context, routineContext, {
29
26
  input: control[':cause'],
30
- items,
31
- location,
32
- payload: routineContext.payload,
33
- state: routineContext.state,
34
- steps: routineContext.steps
27
+ location
35
28
  });
36
- const error = new UserError(message, {
29
+ // A reject is the author telling the user something, so it stays a UserError, whose message the
30
+ // wire passes through as written. An Error given as the message is reduced to what the wire
31
+ // would show for it - the author's text for a UserError, the generic message for a plugin error
32
+ // - rather than stringified with its library text; `{ _error: message }` opts into the real text.
33
+ const text = type.isError(message) ? createWireProjection(context)(message).message : message;
34
+ const error = new UserError(text, {
37
35
  cause,
38
36
  isReject: true
39
37
  });
@@ -12,16 +12,11 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ async function controlReturn(context, routineContext, { control }) {
16
- const { evaluateOperators } = context;
17
- const { items } = routineContext;
18
- const response = evaluateOperators({
15
+ */ import evaluateRoutineOperators from '../evaluateRoutineOperators.js';
16
+ async function controlReturn(context, routineContext, { control }) {
17
+ const response = evaluateRoutineOperators(context, routineContext, {
19
18
  input: control[':return'],
20
- items,
21
- location: control['~k'] ?? ':return',
22
- payload: routineContext.payload,
23
- state: routineContext.state,
24
- steps: routineContext.steps
19
+ location: control['~k'] ?? ':return'
25
20
  });
26
21
  context.logger.debug({
27
22
  event: 'debug_control_return',
@@ -14,16 +14,12 @@
14
14
  limitations under the License.
15
15
  */ import { ConfigError } from '@lowdefy/errors';
16
16
  import { ReservedKeyError, set } from '@lowdefy/helpers';
17
+ import evaluateRoutineOperators from '../evaluateRoutineOperators.js';
17
18
  function controlSetState(context, routineContext, { control }) {
18
- const { logger, evaluateOperators } = context;
19
- const { items } = routineContext;
20
- const evaluatedSetState = evaluateOperators({
19
+ const { logger } = context;
20
+ const evaluatedSetState = evaluateRoutineOperators(context, routineContext, {
21
21
  input: control[':set_state'],
22
- items,
23
- location: control['~k'] ?? ':set_state',
24
- payload: routineContext.payload,
25
- state: routineContext.state,
26
- steps: routineContext.steps
22
+ location: control['~k'] ?? ':set_state'
27
23
  });
28
24
  logger.debug({
29
25
  event: 'debug_control_set_state',
@@ -13,21 +13,17 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import runRoutine from '../runRoutine.js';
16
+ import evaluateRoutineOperators from '../evaluateRoutineOperators.js';
16
17
  async function controlSwitch(context, routineContext, { control }) {
17
- const { endpointId, logger, evaluateOperators } = context;
18
- const { items } = routineContext;
18
+ const { endpointId, logger } = context;
19
19
  const cases = control[':switch'];
20
20
  logger.debug({
21
21
  event: 'debug_control_switch'
22
22
  });
23
23
  for (const caseObj of cases){
24
- const evaluatedCase = evaluateOperators({
24
+ const evaluatedCase = evaluateRoutineOperators(context, routineContext, {
25
25
  input: caseObj[':case'],
26
- items,
27
- location: caseObj['~k'] ?? control['~k'] ?? ':switch',
28
- payload: routineContext.payload,
29
- state: routineContext.state,
30
- steps: routineContext.steps
26
+ location: caseObj['~k'] ?? control['~k'] ?? ':switch'
31
27
  });
32
28
  logger.debug({
33
29
  event: 'debug_control_switch_case',
@@ -13,29 +13,27 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { UserError } from '@lowdefy/errors';
16
+ import { type } from '@lowdefy/helpers';
17
+ import evaluateRoutineOperators from '../evaluateRoutineOperators.js';
16
18
  async function controlThrow(context, routineContext, { control }) {
17
- const { evaluateOperators } = context;
18
- const { items } = routineContext;
19
19
  const location = control['~k'] ?? ':throw';
20
- const message = evaluateOperators({
20
+ const message = evaluateRoutineOperators(context, routineContext, {
21
21
  input: control[':throw'],
22
- items,
23
- location,
24
- payload: routineContext.payload,
25
- state: routineContext.state,
26
- steps: routineContext.steps
27
- });
28
- const cause = evaluateOperators({
29
- input: control[':cause'],
30
- items,
31
- location,
32
- payload: routineContext.payload,
33
- state: routineContext.state,
34
- steps: routineContext.steps
35
- });
36
- const error = new UserError(message, {
37
- cause
22
+ location
38
23
  });
24
+ // An Error, such as a rethrown `_error`, goes on unchanged. Wrapped in a UserError it would be
25
+ // stringified into the message, which the wire sends as author text; under its own class the
26
+ // wire makes it generic unless it was a UserError. It carries its own cause, so :cause is unused.
27
+ let error = message;
28
+ if (!type.isError(message)) {
29
+ const cause = evaluateRoutineOperators(context, routineContext, {
30
+ input: control[':cause'],
31
+ location
32
+ });
33
+ error = new UserError(message, {
34
+ cause
35
+ });
36
+ }
39
37
  // Log under `err` — the pino error serializer (createNodeLogger) is registered
40
38
  // for the `err` key only; an Error passed as `error` is JSON-dumped without its
41
39
  // non-enumerable `message`/`stack`, producing a log line with no message.