@lowdefy/api 5.3.0 → 5.4.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 (31) hide show
  1. package/dist/context/createApiContext.js +13 -1
  2. package/dist/context/createEvaluateOperators.js +5 -3
  3. package/dist/context/resolveLocale.js +47 -0
  4. package/dist/routes/agent/callAgent.js +8 -0
  5. package/dist/routes/agent/getAgentConfig.js +9 -2
  6. package/dist/routes/agent/getAgentResolver.js +9 -2
  7. package/dist/routes/auth/getNextAuthConfig.js +4 -2
  8. package/dist/routes/endpoints/callEndpoint.js +1 -0
  9. package/dist/routes/endpoints/control/controlFor.js +3 -2
  10. package/dist/routes/endpoints/control/controlIf.js +3 -2
  11. package/dist/routes/endpoints/control/controlLog.js +6 -4
  12. package/dist/routes/endpoints/control/controlParallelFor.js +3 -2
  13. package/dist/routes/endpoints/control/controlReject.js +11 -7
  14. package/dist/routes/endpoints/control/controlReturn.js +3 -2
  15. package/dist/routes/endpoints/control/controlSetState.js +4 -3
  16. package/dist/routes/endpoints/control/controlSwitch.js +3 -2
  17. package/dist/routes/endpoints/control/controlThrow.js +9 -6
  18. package/dist/routes/endpoints/handleEndpointCall.js +7 -29
  19. package/dist/routes/endpoints/handleRequest.js +2 -0
  20. package/dist/routes/endpoints/handleValidateSchema.js +73 -0
  21. package/dist/routes/endpoints/invokeEndpoint.js +41 -0
  22. package/dist/routes/endpoints/runRoutine.js +16 -1
  23. package/dist/routes/endpoints/test/runTest.js +3 -1
  24. package/dist/routes/request/callRequest.js +2 -0
  25. package/dist/routes/request/callRequestResolver.js +33 -4
  26. package/dist/routes/request/evaluateOperators.js +3 -1
  27. package/dist/routes/rootConfig/getLowdefyI18n.js +19 -0
  28. package/dist/routes/rootConfig/getRootConfig.js +5 -1
  29. package/dist/routes/rootConfig/menus/filterMenuList.js +12 -1
  30. package/dist/test/testContext.js +2 -2
  31. package/package.json +8 -8
@@ -14,9 +14,21 @@
14
14
  limitations under the License.
15
15
  */ import createAuthorize from './createAuthorize.js';
16
16
  import createReadConfigFile from './createReadConfigFile.js';
17
+ import resolveLocale from './resolveLocale.js';
17
18
  function createApiContext(context) {
18
- context.state = {};
19
19
  context.user = context?.session?.user;
20
+ if (context.i18n?.defaultLocale) {
21
+ const active = resolveLocale({
22
+ i18n: context.i18n,
23
+ headers: context.headers
24
+ });
25
+ context.i18n = {
26
+ ...context.i18n,
27
+ active
28
+ };
29
+ } else {
30
+ context.i18n = undefined;
31
+ }
20
32
  context.authorize = createAuthorize(context);
21
33
  context.readConfigFile = createReadConfigFile(context);
22
34
  }
@@ -14,20 +14,22 @@
14
14
  limitations under the License.
15
15
  */ import { ServerParser } from '@lowdefy/operators';
16
16
  function createEvaluateOperators(context) {
17
- const { jsMap, operators, secrets, state, user } = context;
17
+ const { appMeta, i18n, jsMap, operators, secrets, user } = context;
18
18
  const operatorsParser = new ServerParser({
19
+ i18n,
19
20
  jsMap,
21
+ lowdefyApp: appMeta,
20
22
  operators,
21
23
  secrets,
22
- state,
23
24
  user
24
25
  });
25
- function evaluateOperators({ input, items, location, payload, steps }) {
26
+ function evaluateOperators({ input, items, location, payload, state, steps }) {
26
27
  const { output, errors } = operatorsParser.parse({
27
28
  input,
28
29
  items,
29
30
  location,
30
31
  payload,
32
+ state,
31
33
  steps
32
34
  });
33
35
  if (errors.length > 0) {
@@ -0,0 +1,47 @@
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
+ function parseAcceptLanguage(header) {
17
+ if (!type.isString(header)) return [];
18
+ return header.split(',').map((entry)=>{
19
+ const [code, ...params] = entry.trim().split(';').map((s)=>s.trim());
20
+ const qParam = params.find((p)=>p.startsWith('q='));
21
+ const q = qParam ? parseFloat(qParam.slice(2)) : 1;
22
+ return {
23
+ code,
24
+ q: Number.isFinite(q) ? q : 0
25
+ };
26
+ }).filter(({ code })=>code.length > 0).sort((a, b)=>b.q - a.q).map(({ code })=>code);
27
+ }
28
+ function pickBest(candidates, supported) {
29
+ for (const candidate of candidates){
30
+ if (supported.includes(candidate)) return candidate;
31
+ const primary = candidate.split('-')[0].toLowerCase();
32
+ const match = supported.find((code)=>code.split('-')[0].toLowerCase() === primary);
33
+ if (match) return match;
34
+ }
35
+ return undefined;
36
+ }
37
+ function resolveLocale({ i18n, headers }) {
38
+ if (!i18n || !type.isString(i18n.defaultLocale) || !type.isArray(i18n.locales)) {
39
+ return undefined;
40
+ }
41
+ const supportedCodes = i18n.locales.map((l)=>l.code);
42
+ const acceptLanguage = headers?.['accept-language'];
43
+ const candidates = parseAcceptLanguage(acceptLanguage);
44
+ const matched = pickBest(candidates, supportedCodes);
45
+ return matched ?? i18n.defaultLocale;
46
+ }
47
+ export default resolveLocale;
@@ -45,6 +45,7 @@ async function callAgent(context, { agentId, pageId, messages, conversationId, u
45
45
  input: agentConfig.properties ?? {},
46
46
  location: agentConfig.agentId,
47
47
  payload: agentContext,
48
+ state: {},
48
49
  steps: {}
49
50
  });
50
51
  // Load connection config from build artifacts using agent's connectionId
@@ -61,6 +62,7 @@ async function callAgent(context, { agentId, pageId, messages, conversationId, u
61
62
  input: connectionConfig.properties || {},
62
63
  location: connectionConfig.connectionId,
63
64
  payload: {},
65
+ state: {},
64
66
  steps: {}
65
67
  });
66
68
  // Create connection instance (e.g., Anthropic provider)
@@ -74,10 +76,12 @@ async function callAgent(context, { agentId, pageId, messages, conversationId, u
74
76
  // Build resolver context with callEndpoint that allows InternalApi endpoints
75
77
  const resolverContext = {
76
78
  agentContext,
79
+ i18n: context.i18n,
77
80
  evaluateOperators: (input)=>context.evaluateOperators({
78
81
  input,
79
82
  location: agentConfig.agentId,
80
83
  payload: agentContext,
84
+ state: {},
81
85
  steps: {}
82
86
  }),
83
87
  callEndpoint: async (endpointId, { payload, abortSignal })=>{
@@ -92,6 +96,7 @@ async function callAgent(context, { agentId, pageId, messages, conversationId, u
92
96
  payload: payload ?? {},
93
97
  arrayIndices: [],
94
98
  items: {},
99
+ state: {},
95
100
  endpointDepth: 0
96
101
  };
97
102
  const { error, response, status } = await runRoutine(context, routineContext, {
@@ -130,6 +135,7 @@ async function callAgent(context, { agentId, pageId, messages, conversationId, u
130
135
  input: subConnectionConfig.properties || {},
131
136
  location: subConnectionConfig.connectionId,
132
137
  payload: {},
138
+ state: {},
133
139
  steps: {}
134
140
  });
135
141
  return subConnection.create({
@@ -151,6 +157,7 @@ async function callAgent(context, { agentId, pageId, messages, conversationId, u
151
157
  input: mcpConnConfig.properties || {},
152
158
  location: mcpConnConfig.connectionId,
153
159
  payload: {},
160
+ state: {},
154
161
  steps: {}
155
162
  });
156
163
  const mcpConfig = mcpConnection.create({
@@ -185,6 +192,7 @@ async function callAgent(context, { agentId, pageId, messages, conversationId, u
185
192
  input: mcpConnConfig.properties || {},
186
193
  location: mcpConnConfig.connectionId,
187
194
  payload: {},
195
+ state: {},
188
196
  steps: {}
189
197
  });
190
198
  const mcpConfig = mcpConnection.create({
@@ -13,10 +13,17 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { ConfigError } from '@lowdefy/errors';
16
- async function getAgentConfig({ logger, readConfigFile }, { agentId }) {
16
+ import { translate } from '@lowdefy/helpers';
17
+ async function getAgentConfig({ i18n, logger, readConfigFile }, { agentId }) {
17
18
  const agent = await readConfigFile(`agents/${agentId}.json`);
18
19
  if (!agent) {
19
- const err = new ConfigError(`Agent "${agentId}" does not exist.`);
20
+ const err = new ConfigError(translate({
21
+ key: 'agent.runtime.agentNotFound',
22
+ values: {
23
+ agentId
24
+ },
25
+ i18n
26
+ }));
20
27
  logger.debug({
21
28
  params: {
22
29
  agentId
@@ -13,10 +13,17 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { ConfigError } from '@lowdefy/errors';
16
- function getAgentResolver({ agents, logger }, { agentConfig }) {
16
+ import { translate } from '@lowdefy/helpers';
17
+ function getAgentResolver({ agents, i18n, logger }, { agentConfig }) {
17
18
  const agentType = agents[agentConfig.type];
18
19
  if (!agentType) {
19
- const err = new ConfigError(`Agent type "${agentConfig.type}" can not be found.`, {
20
+ const err = new ConfigError(translate({
21
+ key: 'agent.runtime.agentTypeNotFound',
22
+ values: {
23
+ type: agentConfig.type
24
+ },
25
+ i18n
26
+ }), {
20
27
  configKey: agentConfig['~k']
21
28
  });
22
29
  logger.debug({
@@ -13,7 +13,7 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { ServerParser } from '@lowdefy/operators';
16
- import { _secret } from '@lowdefy/operators-js/operators/server';
16
+ import { _app, _secret } from '@lowdefy/operators-js/operators/server';
17
17
  import createAdapter from './createAdapter.js';
18
18
  import createCallbacks from './callbacks/createCallbacks.js';
19
19
  import createEvents from './events/createEvents.js';
@@ -21,10 +21,12 @@ import createLogger from './createLogger.js';
21
21
  import createProviders from './createProviders.js';
22
22
  const nextAuthConfig = {};
23
23
  let initialized = false;
24
- function getNextAuthConfig({ authJson, logger, plugins, secrets }) {
24
+ function getNextAuthConfig({ appMeta, authJson, logger, plugins, secrets }) {
25
25
  if (initialized) return nextAuthConfig;
26
26
  const operatorsParser = new ServerParser({
27
+ lowdefyApp: appMeta,
27
28
  operators: {
29
+ _app,
28
30
  _secret
29
31
  },
30
32
  secrets,
@@ -53,6 +53,7 @@ async function callEndpoint(context, { blockId, endpointId, pageId, payload }) {
53
53
  payload: serializer.deserialize(payload),
54
54
  arrayIndices: [],
55
55
  items: {},
56
+ state: {},
56
57
  endpointDepth: 0
57
58
  };
58
59
  const { error, response, status } = await runRoutine(context, routineContext, {
@@ -25,8 +25,9 @@ async function controlFor(context, routineContext, { control }) {
25
25
  input: control[':in'],
26
26
  items,
27
27
  location: control['~k'] ?? ':for',
28
- steps: routineContext.steps,
29
- payload: routineContext.payload
28
+ payload: routineContext.payload,
29
+ state: routineContext.state,
30
+ steps: routineContext.steps
30
31
  });
31
32
  logger.debug({
32
33
  event: 'debug_control_for',
@@ -20,8 +20,9 @@ async function controlIf(context, routineContext, { control }) {
20
20
  input: control[':if'],
21
21
  items,
22
22
  location: control['~k'] ?? ':if',
23
- steps: routineContext.steps,
24
- payload: routineContext.payload
23
+ payload: routineContext.payload,
24
+ state: routineContext.state,
25
+ steps: routineContext.steps
25
26
  });
26
27
  logger.debug({
27
28
  event: 'debug_control_if',
@@ -25,15 +25,17 @@ async function controlLog(context, routineContext, { control }) {
25
25
  input: control[':log'],
26
26
  items,
27
27
  location,
28
- steps: routineContext.steps,
29
- payload: routineContext.payload
28
+ payload: routineContext.payload,
29
+ state: routineContext.state,
30
+ steps: routineContext.steps
30
31
  });
31
32
  const logLevel = evaluateOperators({
32
33
  input: control[':level'],
33
34
  items,
34
35
  location,
35
- steps: routineContext.steps,
36
- payload: routineContext.payload
36
+ payload: routineContext.payload,
37
+ state: routineContext.state,
38
+ steps: routineContext.steps
37
39
  }) ?? 'info';
38
40
  if (!type.isString(logLevel)) {
39
41
  throw new ConfigError(`Invalid :log in endpoint "${endpointId}" - :level must be a string.`, {
@@ -25,8 +25,9 @@ async function controlParallelFor(context, routineContext, { control }) {
25
25
  input: control[':in'],
26
26
  items,
27
27
  location: control['~k'] ?? ':parallel_for',
28
- steps: routineContext.steps,
29
- payload: routineContext.payload
28
+ payload: routineContext.payload,
29
+ state: routineContext.state,
30
+ steps: routineContext.steps
30
31
  });
31
32
  logger.debug({
32
33
  event: 'debug_control_parallel',
@@ -12,7 +12,8 @@
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 controlReject(context, routineContext, { control }) {
15
+ */ import { UserError } from '@lowdefy/errors';
16
+ async function controlReject(context, routineContext, { control }) {
16
17
  const { evaluateOperators } = context;
17
18
  const { items } = routineContext;
18
19
  const location = control['~k'] ?? ':reject';
@@ -20,18 +21,21 @@
20
21
  input: control[':reject'],
21
22
  items,
22
23
  location,
23
- steps: routineContext.steps,
24
- payload: routineContext.payload
24
+ payload: routineContext.payload,
25
+ state: routineContext.state,
26
+ steps: routineContext.steps
25
27
  });
26
28
  const cause = evaluateOperators({
27
29
  input: control[':cause'],
28
30
  items,
29
31
  location,
30
- steps: routineContext.steps,
31
- payload: routineContext.payload
32
+ payload: routineContext.payload,
33
+ state: routineContext.state,
34
+ steps: routineContext.steps
32
35
  });
33
- const error = new Error(message, {
34
- cause
36
+ const error = new UserError(message, {
37
+ cause,
38
+ isReject: true
35
39
  });
36
40
  context.logger.warn({
37
41
  event: 'warn_control_reject',
@@ -19,8 +19,9 @@
19
19
  input: control[':return'],
20
20
  items,
21
21
  location: control['~k'] ?? ':return',
22
- steps: routineContext.steps,
23
- payload: routineContext.payload
22
+ payload: routineContext.payload,
23
+ state: routineContext.state,
24
+ steps: routineContext.steps
24
25
  });
25
26
  context.logger.debug({
26
27
  event: 'debug_control_return',
@@ -20,8 +20,9 @@ function controlSetState(context, routineContext, { control }) {
20
20
  input: control[':set_state'],
21
21
  items,
22
22
  location: control['~k'] ?? ':set_state',
23
- steps: routineContext.steps,
24
- payload: routineContext.payload
23
+ payload: routineContext.payload,
24
+ state: routineContext.state,
25
+ steps: routineContext.steps
25
26
  });
26
27
  logger.debug({
27
28
  event: 'debug_control_set_state',
@@ -29,7 +30,7 @@ function controlSetState(context, routineContext, { control }) {
29
30
  evaluated: evaluatedSetState
30
31
  });
31
32
  Object.entries(evaluatedSetState).forEach(([key, value])=>{
32
- set(context.state, key, value);
33
+ set(routineContext.state, key, value);
33
34
  });
34
35
  return {
35
36
  status: 'continue'
@@ -25,8 +25,9 @@ async function controlSwitch(context, routineContext, { control }) {
25
25
  input: caseObj[':case'],
26
26
  items,
27
27
  location: caseObj['~k'] ?? control['~k'] ?? ':switch',
28
- steps: routineContext.steps,
29
- payload: routineContext.payload
28
+ payload: routineContext.payload,
29
+ state: routineContext.state,
30
+ steps: routineContext.steps
30
31
  });
31
32
  logger.debug({
32
33
  event: 'debug_control_switch_case',
@@ -12,7 +12,8 @@
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 controlThrow(context, routineContext, { control }) {
15
+ */ import { UserError } from '@lowdefy/errors';
16
+ async function controlThrow(context, routineContext, { control }) {
16
17
  const { evaluateOperators } = context;
17
18
  const { items } = routineContext;
18
19
  const location = control['~k'] ?? ':throw';
@@ -20,17 +21,19 @@
20
21
  input: control[':throw'],
21
22
  items,
22
23
  location,
23
- steps: routineContext.steps,
24
- payload: routineContext.payload
24
+ payload: routineContext.payload,
25
+ state: routineContext.state,
26
+ steps: routineContext.steps
25
27
  });
26
28
  const cause = evaluateOperators({
27
29
  input: control[':cause'],
28
30
  items,
29
31
  location,
30
- steps: routineContext.steps,
31
- payload: routineContext.payload
32
+ payload: routineContext.payload,
33
+ state: routineContext.state,
34
+ steps: routineContext.steps
32
35
  });
33
- const error = new Error(message, {
36
+ const error = new UserError(message, {
34
37
  cause
35
38
  });
36
39
  context.logger.error({
@@ -12,11 +12,8 @@
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
- */ import { ConfigError } from '@lowdefy/errors';
16
- import addStepResult from './addStepResult.js';
17
- import authorizeApiEndpoint from './authorizeApiEndpoint.js';
18
- import getEndpointConfig from './getEndpointConfig.js';
19
- import runRoutine from './runRoutine.js';
15
+ */ import addStepResult from './addStepResult.js';
16
+ import invokeEndpoint from './invokeEndpoint.js';
20
17
  async function handleEndpointCall(context, routineContext, { step }) {
21
18
  const { logger, evaluateOperators } = context;
22
19
  logger.debug({
@@ -29,32 +26,13 @@ async function handleEndpointCall(context, routineContext, { step }) {
29
26
  items: routineContext.items,
30
27
  location: step.stepId,
31
28
  payload: routineContext.payload,
29
+ state: routineContext.state,
32
30
  steps: routineContext.steps
33
31
  });
34
- // Check recursion depth
35
- const currentDepth = routineContext.endpointDepth ?? 0;
36
- if (currentDepth >= 10) {
37
- throw new ConfigError('Endpoint call depth exceeded maximum of 10. Check for recursive endpoint calls.');
38
- }
39
- // Load target endpoint config
40
- const endpointConfig = await getEndpointConfig(context, {
41
- endpointId: evaluatedProperties.endpointId
42
- });
43
- // Authorize current user against target endpoint
44
- authorizeApiEndpoint(context, {
45
- endpointConfig
46
- });
47
- // Create isolated routineContext for the called endpoint
48
- const childRoutineContext = {
49
- steps: {},
50
- payload: evaluatedProperties.payload ?? {},
51
- arrayIndices: [],
52
- items: {},
53
- endpointDepth: currentDepth + 1
54
- };
55
- // Run the target endpoint's routine
56
- const result = await runRoutine(context, childRoutineContext, {
57
- routine: endpointConfig.routine
32
+ const result = await invokeEndpoint(context, {
33
+ endpointId: evaluatedProperties.endpointId,
34
+ payload: evaluatedProperties.payload,
35
+ endpointDepth: routineContext.endpointDepth
58
36
  });
59
37
  // Store the return value in the caller's steps
60
38
  const response = result.status === 'return' ? result.response : null;
@@ -45,6 +45,7 @@ async function handleRequest(context, routineContext, { request }) {
45
45
  items,
46
46
  payload: routineContext.payload,
47
47
  requestConfig,
48
+ state: routineContext.state,
48
49
  steps: routineContext.steps
49
50
  });
50
51
  checkConnectionRead(context, {
@@ -68,6 +69,7 @@ async function handleRequest(context, routineContext, { request }) {
68
69
  });
69
70
  const result = await callRequestResolver(context, {
70
71
  connectionProperties,
72
+ endpointDepth: routineContext.endpointDepth,
71
73
  requestConfig,
72
74
  requestProperties,
73
75
  requestResolver
@@ -0,0 +1,73 @@
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 { validate } from '@lowdefy/ajv';
16
+ import addStepResult from './addStepResult.js';
17
+ function buildErrorMessage(errors, stepId) {
18
+ const first = errors?.[0];
19
+ if (!first) return `ValidateSchema step "${stepId}" failed.`;
20
+ const path = first.instancePath || '(root)';
21
+ return `ValidateSchema step "${stepId}" failed at ${path}: ${first.message}.`;
22
+ }
23
+ async function handleValidateSchema(context, routineContext, { step }) {
24
+ const { logger, evaluateOperators } = context;
25
+ logger.debug({
26
+ event: 'debug_start_validate_schema',
27
+ step
28
+ });
29
+ const evaluatedProperties = evaluateOperators({
30
+ input: step.properties,
31
+ items: routineContext.items,
32
+ location: step.stepId,
33
+ payload: routineContext.payload,
34
+ steps: routineContext.steps
35
+ });
36
+ const { schema, data, throwOnInvalid = true } = evaluatedProperties;
37
+ const { valid, errors } = validate({
38
+ schema,
39
+ data,
40
+ returnErrors: true
41
+ });
42
+ const result = {
43
+ valid,
44
+ errors: errors ?? []
45
+ };
46
+ addStepResult(context, routineContext, {
47
+ result,
48
+ stepId: step.stepId
49
+ });
50
+ if (!valid && throwOnInvalid) {
51
+ const error = new Error(buildErrorMessage(result.errors, step.stepId), {
52
+ cause: result.errors
53
+ });
54
+ logger.error({
55
+ event: 'error_validate_schema',
56
+ stepId: step.stepId,
57
+ error
58
+ });
59
+ return {
60
+ status: 'error',
61
+ error
62
+ };
63
+ }
64
+ logger.debug({
65
+ event: 'debug_end_validate_schema',
66
+ stepId: step.stepId,
67
+ valid
68
+ });
69
+ return {
70
+ status: 'continue'
71
+ };
72
+ }
73
+ export default handleValidateSchema;
@@ -0,0 +1,41 @@
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 { ConfigError } from '@lowdefy/errors';
16
+ import authorizeApiEndpoint from './authorizeApiEndpoint.js';
17
+ import getEndpointConfig from './getEndpointConfig.js';
18
+ import runRoutine from './runRoutine.js';
19
+ async function invokeEndpoint(context, { endpointId, payload, endpointDepth }) {
20
+ if (endpointDepth >= 10) {
21
+ throw new ConfigError('Endpoint call depth exceeded maximum of 10. Check for recursive endpoint calls.');
22
+ }
23
+ const endpointConfig = await getEndpointConfig(context, {
24
+ endpointId
25
+ });
26
+ authorizeApiEndpoint(context, {
27
+ endpointConfig
28
+ });
29
+ const childRoutineContext = {
30
+ steps: {},
31
+ payload: payload ?? {},
32
+ arrayIndices: [],
33
+ items: {},
34
+ state: {},
35
+ endpointDepth: endpointDepth + 1
36
+ };
37
+ return runRoutine(context, childRoutineContext, {
38
+ routine: endpointConfig.routine
39
+ });
40
+ }
41
+ export default invokeEndpoint;
@@ -16,6 +16,7 @@
16
16
  import handleControl from './control/handleControl.js';
17
17
  import handleEndpointCall from './handleEndpointCall.js';
18
18
  import handleRequest from './handleRequest.js';
19
+ import handleValidateSchema from './handleValidateSchema.js';
19
20
  async function runRoutine(context, routineContext, { routine }) {
20
21
  try {
21
22
  if (type.isObject(routine)) {
@@ -29,6 +30,11 @@ async function runRoutine(context, routineContext, { routine }) {
29
30
  step: routine
30
31
  });
31
32
  }
33
+ if (routine.id?.startsWith?.('validate:')) {
34
+ return await handleValidateSchema(context, routineContext, {
35
+ step: routine
36
+ });
37
+ }
32
38
  return await handleControl(context, routineContext, {
33
39
  control: routine
34
40
  });
@@ -56,7 +62,16 @@ async function runRoutine(context, routineContext, { routine }) {
56
62
  }
57
63
  });
58
64
  } catch (error) {
59
- await context.handleError(error);
65
+ if (error.isReject) {
66
+ return {
67
+ status: 'reject',
68
+ error
69
+ };
70
+ }
71
+ if (!error.handled) {
72
+ await context.handleError(error);
73
+ error.handled = true;
74
+ }
60
75
  return {
61
76
  status: 'error',
62
77
  error
@@ -90,7 +90,9 @@ async function runTest({ routine, payload = {} }) {
90
90
  steps: {},
91
91
  payload,
92
92
  items: {},
93
- arrayIndices: []
93
+ arrayIndices: [],
94
+ state: {},
95
+ endpointDepth: 0
94
96
  };
95
97
  const res = await runRoutine(context, routineContext, {
96
98
  routine
@@ -60,6 +60,7 @@ async function callRequest(context, { blockId, pageId, payload, requestId }) {
60
60
  connectionConfig,
61
61
  payload: requestPayload,
62
62
  requestConfig,
63
+ state: {},
63
64
  steps: {}
64
65
  });
65
66
  checkConnectionRead(context, {
@@ -83,6 +84,7 @@ async function callRequest(context, { blockId, pageId, payload, requestId }) {
83
84
  });
84
85
  const response = await callRequestResolver(context, {
85
86
  connectionProperties,
87
+ endpointDepth: 0,
86
88
  requestConfig,
87
89
  requestProperties,
88
90
  requestResolver
@@ -12,16 +12,43 @@
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
- */ import { ConfigError, RequestError, ServiceError } from '@lowdefy/errors';
16
- async function callRequestResolver({ blockId, endpointId, logger, pageId, payload }, { connectionProperties, requestConfig, requestProperties, requestResolver }) {
15
+ */ import { RequestError, ServiceError } from '@lowdefy/errors';
16
+ import invokeEndpoint from '../endpoints/invokeEndpoint.js';
17
+ async function callRequestResolver(context, { connectionProperties, endpointDepth, requestConfig, requestProperties, requestResolver }) {
18
+ const { blockId, endpointId, logger, pageId, payload } = context;
17
19
  // stepId for endpoint steps (after build), requestId for page requests
18
20
  const stepOrRequestId = requestConfig.stepId ?? requestConfig.requestId;
21
+ const callApi = async ({ endpointId: targetEndpointId, payload: targetPayload } = {})=>{
22
+ logger.debug({
23
+ event: 'debug_start_call_api',
24
+ connectionId: requestConfig.connectionId,
25
+ requestId: stepOrRequestId,
26
+ endpointId: targetEndpointId
27
+ });
28
+ const result = await invokeEndpoint(context, {
29
+ endpointId: targetEndpointId,
30
+ payload: targetPayload,
31
+ endpointDepth
32
+ });
33
+ if (result.status === 'error' || result.status === 'reject') {
34
+ throw result.error;
35
+ }
36
+ const response = result.status === 'return' ? result.response : null;
37
+ logger.debug({
38
+ event: 'debug_end_call_api',
39
+ connectionId: requestConfig.connectionId,
40
+ requestId: stepOrRequestId,
41
+ endpointId: targetEndpointId
42
+ });
43
+ return response;
44
+ };
19
45
  try {
20
46
  const response = await requestResolver({
21
47
  blockId,
22
- endpointId,
48
+ callApi,
23
49
  connection: connectionProperties,
24
50
  connectionId: requestConfig.connectionId,
51
+ endpointId,
25
52
  pageId,
26
53
  payload,
27
54
  request: requestProperties,
@@ -33,7 +60,9 @@ async function callRequestResolver({ blockId, endpointId, logger, pageId, payloa
33
60
  if (!error.configKey) {
34
61
  error.configKey = requestConfig['~k'];
35
62
  }
36
- if (error instanceof ConfigError) {
63
+ // Lowdefy errors pass through unchanged — re-wrapping every boundary would
64
+ // nest causes and truncate the deepest (most informative) frame.
65
+ if (error.isLowdefyError) {
37
66
  logger.debug({
38
67
  params: {
39
68
  id: stepOrRequestId,
@@ -12,11 +12,12 @@
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
- */ function evaluateOperators({ evaluateOperators }, { connectionConfig, items, payload, requestConfig, steps }) {
15
+ */ function evaluateOperators({ evaluateOperators }, { connectionConfig, items, payload, requestConfig, state, steps }) {
16
16
  const connectionProperties = evaluateOperators({
17
17
  input: connectionConfig.properties || {},
18
18
  location: connectionConfig.connectionId,
19
19
  payload,
20
+ state,
20
21
  steps
21
22
  });
22
23
  const requestProperties = evaluateOperators({
@@ -24,6 +25,7 @@
24
25
  items,
25
26
  location: requestConfig.stepId ?? requestConfig.requestId,
26
27
  payload,
28
+ state,
27
29
  steps
28
30
  });
29
31
  return {
@@ -0,0 +1,19 @@
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
+ */ async function getLowdefyI18n({ readConfigFile }) {
16
+ const i18n = await readConfigFile('i18n.json');
17
+ return i18n || {};
18
+ }
19
+ export default getLowdefyI18n;
@@ -14,15 +14,19 @@
14
14
  limitations under the License.
15
15
  */ import getHomeAndMenus from './getHomeAndMenus.js';
16
16
  import getLowdefyGlobal from './getLowdefyGlobal.js';
17
+ import getLowdefyI18n from './getLowdefyI18n.js';
17
18
  import getLowdefyTheme from './getLowdefyTheme.js';
18
19
  async function getRootConfig(context) {
19
- const [lowdefyGlobal, theme, { home, menus }] = await Promise.all([
20
+ const [lowdefyGlobal, theme, i18n, { home, menus }] = await Promise.all([
20
21
  getLowdefyGlobal(context),
21
22
  getLowdefyTheme(context),
23
+ getLowdefyI18n(context),
22
24
  getHomeAndMenus(context)
23
25
  ]);
24
26
  return {
25
27
  home,
28
+ i18n,
29
+ lowdefyApp: context.appMeta,
26
30
  lowdefyGlobal,
27
31
  menus,
28
32
  theme
@@ -15,7 +15,7 @@
15
15
  */ import { get } from '@lowdefy/helpers';
16
16
  function filterMenuList(context, { menuList }) {
17
17
  const { authorize } = context;
18
- return menuList.map((item)=>{
18
+ const filtered = menuList.map((item)=>{
19
19
  if (item.type === 'MenuLink') {
20
20
  if (authorize(item)) {
21
21
  return item;
@@ -34,8 +34,19 @@ function filterMenuList(context, { menuList }) {
34
34
  links: filteredSubItems
35
35
  };
36
36
  }
37
+ return null;
38
+ }
39
+ if (item.type === 'MenuDivider') {
40
+ return item;
37
41
  }
38
42
  return null;
39
43
  }).filter((item)=>item !== null);
44
+ return filtered.filter((item, i, arr)=>{
45
+ if (item.type !== 'MenuDivider') return true;
46
+ if (i === 0) return false;
47
+ if (i === arr.length - 1) return false;
48
+ if (arr[i - 1].type === 'MenuDivider') return false;
49
+ return true;
50
+ });
40
51
  }
41
52
  export default filterMenuList;
@@ -13,7 +13,7 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import createAuthorize from '../context/createAuthorize.js';
16
- function testContext({ config = {}, connections = {}, headers = {}, logger = {
16
+ function testContext({ appMeta = {}, config = {}, connections = {}, headers = {}, logger = {
17
17
  debug: ()=>{},
18
18
  error: ()=>{},
19
19
  info: ()=>{},
@@ -22,6 +22,7 @@ function testContext({ config = {}, connections = {}, headers = {}, logger = {
22
22
  _test: ()=>'test'
23
23
  }, readConfigFile, secrets = {}, session } = {}) {
24
24
  return {
25
+ appMeta,
25
26
  authorize: createAuthorize({
26
27
  session
27
28
  }),
@@ -36,7 +37,6 @@ function testContext({ config = {}, connections = {}, headers = {}, logger = {
36
37
  readConfigFile,
37
38
  secrets,
38
39
  session,
39
- state: {},
40
40
  steps: {},
41
41
  user: session?.user
42
42
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/api",
3
- "version": "5.3.0",
3
+ "version": "5.4.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "",
6
6
  "homepage": "https://lowdefy.com",
@@ -34,13 +34,13 @@
34
34
  "dist/*"
35
35
  ],
36
36
  "dependencies": {
37
- "@lowdefy/ajv": "5.3.0",
38
- "@lowdefy/errors": "5.3.0",
39
- "@lowdefy/helpers": "5.3.0",
40
- "@lowdefy/node-utils": "5.3.0",
41
- "@lowdefy/nunjucks": "5.3.0",
42
- "@lowdefy/operators": "5.3.0",
43
- "@lowdefy/operators-js": "5.3.0"
37
+ "@lowdefy/ajv": "5.4.0",
38
+ "@lowdefy/errors": "5.4.0",
39
+ "@lowdefy/helpers": "5.4.0",
40
+ "@lowdefy/node-utils": "5.4.0",
41
+ "@lowdefy/nunjucks": "5.4.0",
42
+ "@lowdefy/operators": "5.4.0",
43
+ "@lowdefy/operators-js": "5.4.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@jest/globals": "28.1.3",