@lowdefy/build 5.6.0 → 6.0.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 (100) hide show
  1. package/dist/build/buildAgents.js +61 -16
  2. package/dist/build/buildApi/buildApi.js +4 -1
  3. package/dist/build/buildApi/buildEndpoint.js +8 -2
  4. package/dist/build/buildApi/buildRoutine/countStepTypes.js +1 -1
  5. package/dist/build/buildApi/buildRoutine/setStepId.js +2 -0
  6. package/dist/build/buildApi/buildRoutine/validateStep.js +61 -0
  7. package/dist/build/buildApi/resolveEndpointSchedules.js +37 -0
  8. package/dist/build/buildApi/validateEndpoint.js +96 -1
  9. package/dist/build/buildApp.js +13 -20
  10. package/dist/build/buildAppMeta.js +130 -0
  11. package/dist/build/buildAuth/buildAgentAuth.js +14 -1
  12. package/dist/build/buildAuth/buildAuth.js +8 -3
  13. package/dist/build/buildAuth/buildAuthPlugins.js +6 -0
  14. package/dist/build/buildAuth/buildAuthStrategies.js +117 -0
  15. package/dist/build/buildAuth/buildWebsocketAuth.js +55 -0
  16. package/dist/build/buildAuth/getProtectedWebsockets.js +29 -0
  17. package/dist/build/buildAuth/getWebsocketRoles.js +39 -0
  18. package/dist/build/buildAuth/validateAuthConfig.js +15 -3
  19. package/dist/build/buildImports/buildImportsDev.js +15 -0
  20. package/dist/build/buildImports/buildImportsProd.js +4 -1
  21. package/dist/build/buildJs/generateClientJsModule.js +27 -0
  22. package/dist/build/buildJs/writeJs.js +3 -5
  23. package/dist/build/buildMcp.js +73 -0
  24. package/dist/build/buildModuleDefs.js +90 -48
  25. package/dist/build/buildModules.js +61 -14
  26. package/dist/build/buildNotifications.js +98 -0
  27. package/dist/build/buildPages/buildBlock/buildBlock.js +3 -1
  28. package/dist/build/buildPages/buildBlock/buildDynamicBlock.js +131 -0
  29. package/dist/build/buildPages/buildBlock/buildEvents.js +186 -21
  30. package/dist/build/buildPages/buildBlock/buildRequests.js +9 -0
  31. package/dist/build/buildPages/buildBlock/normalizeClassAndStyles.js +2 -11
  32. package/dist/build/buildPages/buildBlock/setBlockId.js +5 -2
  33. package/dist/build/buildPages/buildBlock/validateBlock.js +7 -0
  34. package/dist/build/buildPages/buildPage.js +27 -3
  35. package/dist/build/buildPages/buildSubscriptions.js +145 -0
  36. package/dist/build/buildPages/validateDynamicBlockRefs.js +30 -0
  37. package/dist/build/buildPages/validateStateReferences.js +4 -2
  38. package/dist/build/buildPages/validateWebsocketRefs.js +30 -0
  39. package/dist/build/buildRefs/buildRefs.js +21 -12
  40. package/dist/build/buildRefs/cloneWithMarkers.js +57 -0
  41. package/dist/build/buildRefs/deferredRegions.js +80 -0
  42. package/dist/build/buildRefs/deferredRegistry.js +292 -0
  43. package/dist/build/buildRefs/getModuleRefContent.js +21 -6
  44. package/dist/build/buildRefs/getUserJavascriptFunction.js +1 -1
  45. package/dist/build/buildRefs/parseRefContent.js +24 -0
  46. package/dist/build/buildRefs/{evaluateStaticOperators.js → precomputeRuntimeOperators.js} +7 -6
  47. package/dist/build/buildRefs/walker.js +471 -207
  48. package/dist/build/buildTypes.js +22 -1
  49. package/dist/build/buildWebsockets.js +99 -0
  50. package/dist/build/collectPageContent.js +7 -1
  51. package/dist/build/copyAgentFileSystems.js +2 -2
  52. package/dist/build/full/buildPages.js +16 -0
  53. package/dist/build/full/updateServerPackageJson.js +21 -0
  54. package/dist/build/jit/buildPageJit.js +46 -6
  55. package/dist/build/jit/collectSkeletonSourceFiles.js +14 -0
  56. package/dist/build/jit/pageContentKeys.js +1 -0
  57. package/dist/build/jit/shallowBuild.js +81 -1
  58. package/dist/build/jit/writePageJit.js +11 -8
  59. package/dist/build/registerModules.js +125 -24
  60. package/dist/build/validateCallAgentSteps.js +58 -0
  61. package/dist/build/validateConfig.js +4 -0
  62. package/dist/build/validateCronConfig.js +110 -0
  63. package/dist/build/validateRenderNotificationSteps.js +50 -0
  64. package/dist/build/writeApi.js +58 -1
  65. package/dist/build/writeMcp.js +21 -0
  66. package/dist/build/writeNotifications.js +30 -0
  67. package/dist/build/writePluginImports/importPluginModule.js +39 -0
  68. package/dist/build/writePluginImports/writeActionSchemaMap.js +6 -7
  69. package/dist/build/writePluginImports/writeAuthImports.js +4 -0
  70. package/dist/build/writePluginImports/writeAvailableTypes.js +99 -0
  71. package/dist/build/writePluginImports/writeBlockSchemaMap.js +10 -9
  72. package/dist/build/writePluginImports/writeConnectionSchemaMap.js +101 -0
  73. package/dist/build/writePluginImports/writeGlobalsCss.js +20 -10
  74. package/dist/build/writePluginImports/writeNotificationImports.js +31 -0
  75. package/dist/build/writePluginImports/writeOperatorSchemaMap.js +6 -7
  76. package/dist/build/writePluginImports/writePluginImports.js +18 -4
  77. package/dist/build/writePluginImports/writeWebsocketImports.js +22 -0
  78. package/dist/build/writeWebsockets.js +30 -0
  79. package/dist/createContext.js +6 -1
  80. package/dist/defaultPackages.js +7 -1
  81. package/dist/defaultTypesMap.js +666 -388
  82. package/dist/dynamic/buildDynamicBlocks.js +120 -0
  83. package/dist/index.js +78 -8
  84. package/dist/indexDev.js +4 -0
  85. package/dist/lowdefySchema.js +861 -6
  86. package/dist/scripts/generateDefaultTypes.js +5 -2
  87. package/dist/test-utils/expectTerminates.js +33 -0
  88. package/dist/test-utils/runBuild.js +12 -1
  89. package/dist/test-utils/runBuildForSnapshots.js +34 -4
  90. package/dist/test-utils/testContext.js +8 -2
  91. package/dist/utils/countOperators.js +5 -17
  92. package/dist/utils/createPluginTypesMap.js +21 -0
  93. package/dist/utils/getCronEnvironmentNames.js +25 -0
  94. package/dist/utils/logCollectedErrors.js +15 -9
  95. package/dist/utils/serializeBuildException.js +29 -0
  96. package/dist/utils/validateCronExpression.js +131 -0
  97. package/dist/utils/writeBuildArtifact.js +5 -2
  98. package/package.json +66 -59
  99. package/dist/build/writePluginImports/writeServerExternalPackages.js +0 -121
  100. /package/dist/build/{resolveModuleOperators.js → buildRefs/scopeMenuItemIds.js} +0 -0
@@ -13,22 +13,43 @@
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
- import { serializer, type } from '@lowdefy/helpers';
16
+ import { type } from '@lowdefy/helpers';
17
+ function checkSecretNodes({ value, declaredSecrets, entryId }) {
18
+ if (type.isArray(value)) {
19
+ for (const item of value){
20
+ checkSecretNodes({
21
+ value: item,
22
+ declaredSecrets,
23
+ entryId
24
+ });
25
+ }
26
+ return;
27
+ }
28
+ if (!type.isObject(value)) return;
29
+ // Only a single-non-tilde-key object is operator shape (matches
30
+ // getRuntimeOperatorKey) — { _secret: 'X', extra: 1 } is deliberately not
31
+ // treated as a secret reference.
32
+ const keys = Object.keys(value).filter((k)=>!k.startsWith('~'));
33
+ if (keys.length === 1 && !type.isUndefined(value['_secret'])) {
34
+ const secretName = value['_secret'];
35
+ if (type.isString(secretName) && !declaredSecrets.has(secretName)) {
36
+ throw new ConfigError(`Module "${entryId}" references secret "${secretName}" ` + `but does not declare it in module.lowdefy.yaml secrets. ` + `Add it to the module's secrets list or remove the reference.`);
37
+ }
38
+ }
39
+ for (const key of Object.keys(value)){
40
+ checkSecretNodes({
41
+ value: value[key],
42
+ declaredSecrets,
43
+ entryId
44
+ });
45
+ }
46
+ }
17
47
  function validateModuleSecrets({ content, manifest, entryId }) {
18
48
  const declaredSecrets = new Set((manifest.secrets ?? []).map((s)=>s.name));
19
- serializer.copy(content, {
20
- reviver: (_, value)=>{
21
- if (!type.isObject(value)) return value;
22
- const keys = Object.keys(value).filter((k)=>!k.startsWith('~'));
23
- if (keys.length !== 1) return value;
24
- if (!type.isUndefined(value['_secret'])) {
25
- const secretName = value['_secret'];
26
- if (type.isString(secretName) && !declaredSecrets.has(secretName)) {
27
- throw new ConfigError(`Module "${entryId}" references secret "${secretName}" ` + `but does not declare it in module.lowdefy.yaml secrets. ` + `Add it to the module's secrets list or remove the reference.`);
28
- }
29
- }
30
- return value;
31
- }
49
+ checkSecretNodes({
50
+ value: content,
51
+ declaredSecrets,
52
+ entryId
32
53
  });
33
54
  }
34
55
  function buildModules({ components, context }) {
@@ -71,6 +92,20 @@ function buildModules({ components, context }) {
71
92
  entryId: entry.id
72
93
  });
73
94
  }
95
+ for (const agent of manifest.agents ?? []){
96
+ validateModuleSecrets({
97
+ content: agent,
98
+ manifest,
99
+ entryId: entry.id
100
+ });
101
+ }
102
+ for (const notification of manifest.notifications ?? []){
103
+ validateModuleSecrets({
104
+ content: notification,
105
+ manifest,
106
+ entryId: entry.id
107
+ });
108
+ }
74
109
  // Process pages
75
110
  for (const page of manifest.pages ?? []){
76
111
  page.id = `${entry.id}/${page.id}`;
@@ -90,6 +125,18 @@ function buildModules({ components, context }) {
90
125
  components.api = components.api ?? [];
91
126
  components.api.push(endpoint);
92
127
  }
128
+ // Process agents
129
+ for (const agent of manifest.agents ?? []){
130
+ agent.id = `${entry.id}/${agent.id}`;
131
+ components.agents = components.agents ?? [];
132
+ components.agents.push(agent);
133
+ }
134
+ // Process notifications
135
+ for (const notification of manifest.notifications ?? []){
136
+ notification.id = `${entry.id}/${notification.id}`;
137
+ components.notifications = components.notifications ?? [];
138
+ components.notifications.push(notification);
139
+ }
93
140
  }
94
141
  return components;
95
142
  }
@@ -0,0 +1,98 @@
1
+ /* eslint-disable no-param-reassign */ /*
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 { ConfigError } from '@lowdefy/errors';
17
+ import createCheckDuplicateId from '../utils/createCheckDuplicateId.js';
18
+ import validateId from '../utils/validateId.js';
19
+ function buildNotifications({ components, context }) {
20
+ if (components.notifications && !type.isArray(components.notifications)) {
21
+ throw new ConfigError('Notifications is not an array.', {
22
+ received: components.notifications
23
+ });
24
+ }
25
+ const notifications = type.isArray(components.notifications) ? components.notifications : [];
26
+ context.notificationIds = new Set();
27
+ const checkDuplicateNotificationId = createCheckDuplicateId({
28
+ message: 'Duplicate notificationId "{{ id }}".'
29
+ });
30
+ notifications.forEach((notification, index)=>{
31
+ const configKey = notification['~k'];
32
+ if (type.isUndefined(notification.id)) {
33
+ throw new ConfigError(`Notification id missing at notification ${index}.`, {
34
+ configKey
35
+ });
36
+ }
37
+ if (!type.isString(notification.id)) {
38
+ throw new ConfigError(`Notification id is not a string at notification ${index}.`, {
39
+ received: notification.id,
40
+ configKey
41
+ });
42
+ }
43
+ validateId({
44
+ id: notification.id,
45
+ field: 'Notification id',
46
+ configKey
47
+ });
48
+ checkDuplicateNotificationId({
49
+ id: notification.id,
50
+ configKey
51
+ });
52
+ if (!type.isString(notification.type)) {
53
+ throw new ConfigError(`Notification type is not a string at notification "${notification.id}".`, {
54
+ received: notification.type,
55
+ configKey
56
+ });
57
+ }
58
+ // Track type usage for buildTypes validation
59
+ context.typeCounters.notifications.increment(notification.type, configKey);
60
+ if (type.isNone(notification.properties)) {
61
+ notification.properties = {};
62
+ }
63
+ if (!type.isObject(notification.properties)) {
64
+ throw new ConfigError(`Notification properties is not an object at notification "${notification.id}".`, {
65
+ received: notification.properties,
66
+ configKey
67
+ });
68
+ }
69
+ // subject is a framework contract, independent of the template type's own schema —
70
+ // the render step reads it for the mail header and the returned result.
71
+ if (!type.isString(notification.properties.subject) || notification.properties.subject === '') {
72
+ throw new ConfigError(`Notification "${notification.id}" requires "properties.subject" to be a non-empty string.`, {
73
+ received: notification.properties.subject,
74
+ configKey
75
+ });
76
+ }
77
+ if (!type.isNone(notification.theme) && !type.isObject(notification.theme)) {
78
+ throw new ConfigError(`Notification theme is not an object at notification "${notification.id}".`, {
79
+ received: notification.theme,
80
+ configKey
81
+ });
82
+ }
83
+ if (!type.isNone(notification.testData) && !type.isObject(notification.testData)) {
84
+ throw new ConfigError(`Notification testData is not an object at notification "${notification.id}".`, {
85
+ received: notification.testData,
86
+ configKey
87
+ });
88
+ }
89
+ // Rename id to internal format
90
+ notification.notificationId = notification.id;
91
+ context.notificationIds.add(notification.notificationId);
92
+ notification.id = `notification:${notification.notificationId}`;
93
+ // No countOperators here — notification properties are nunjucks data templates,
94
+ // not operator config; there is no runtime operator evaluation inside them.
95
+ });
96
+ return components;
97
+ }
98
+ export default buildNotifications;
@@ -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
- */ import buildEvents from './buildEvents.js';
15
+ */ import buildDynamicBlock from './buildDynamicBlock.js';
16
+ import buildEvents from './buildEvents.js';
16
17
  import buildRequests from './buildRequests.js';
17
18
  import buildSubBlocks from './buildSubBlocks.js';
18
19
  import countBlockOperators from './countBlockOperators.js';
@@ -28,6 +29,7 @@ import validateSlots from './validateSlots.js';
28
29
  function buildBlock(block, pageContext, parentConfigKey) {
29
30
  validateBlock(block, pageContext, parentConfigKey);
30
31
  setBlockId(block, pageContext);
32
+ buildDynamicBlock(block, pageContext);
31
33
  normalizeLayout(block, pageContext);
32
34
  moveAreasToSlots(block, pageContext);
33
35
  countBlockOperators(block, pageContext);
@@ -0,0 +1,131 @@
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 { ConfigError } from '@lowdefy/errors';
17
+ import countOperators from '../../../utils/countOperators.js';
18
+ const TYPES_CATEGORIES = new Set([
19
+ 'blocks',
20
+ 'actions',
21
+ 'operators'
22
+ ]);
23
+ function countDeclaredTypes(block, { pageId, typeCounters }) {
24
+ const configKey = block['~k'];
25
+ const types = block.properties.types;
26
+ if (!type.isObject(types)) {
27
+ throw new ConfigError(`Dynamic block "${block.blockId}" on page "${pageId}" properties.types should be an object.`, {
28
+ received: types,
29
+ configKey
30
+ });
31
+ }
32
+ Object.keys(types).forEach((category)=>{
33
+ if (category.startsWith('~')) return;
34
+ if (!TYPES_CATEGORIES.has(category)) {
35
+ throw new ConfigError(`Dynamic block "${block.blockId}" on page "${pageId}" properties.types has unknown category "${category}". Valid categories: blocks, actions, operators.`, {
36
+ configKey
37
+ });
38
+ }
39
+ if (!type.isArray(types[category])) {
40
+ throw new ConfigError(`Dynamic block "${block.blockId}" on page "${pageId}" properties.types.${category} should be an array.`, {
41
+ received: types[category],
42
+ configKey
43
+ });
44
+ }
45
+ types[category].forEach((typeName)=>{
46
+ if (!type.isString(typeName)) {
47
+ throw new ConfigError(`Dynamic block "${block.blockId}" on page "${pageId}" properties.types.${category} should only contain strings.`, {
48
+ received: typeName,
49
+ configKey
50
+ });
51
+ }
52
+ if (category === 'blocks') {
53
+ typeCounters.blocks.increment(typeName, configKey);
54
+ }
55
+ if (category === 'actions') {
56
+ typeCounters.actions.increment(typeName, configKey);
57
+ }
58
+ if (category === 'operators') {
59
+ // Normalize method form (_number.round) to the base operator (_number),
60
+ // matching how countOperators counts operator usage.
61
+ const operator = typeName.split('.')[0];
62
+ if (operator.length < 2 || operator[0] !== '_') {
63
+ throw new ConfigError(`Dynamic block "${block.blockId}" on page "${pageId}" properties.types.operators contains "${typeName}" which is not an operator name. Operator names start with "_".`, {
64
+ configKey
65
+ });
66
+ }
67
+ typeCounters.operators.client.increment(operator, configKey);
68
+ }
69
+ });
70
+ });
71
+ }
72
+ function buildDynamicBlock(block, pageContext) {
73
+ if (block.type !== 'Dynamic') {
74
+ return;
75
+ }
76
+ const { pageId } = pageContext;
77
+ const configKey = block['~k'];
78
+ if (!type.isObject(block.properties)) {
79
+ throw new ConfigError(`Dynamic block "${block.blockId}" on page "${pageId}" properties should be an object.`, {
80
+ received: block.properties,
81
+ configKey
82
+ });
83
+ }
84
+ if (type.isUndefined(block.properties.endpointId)) {
85
+ throw new ConfigError(`Dynamic block "${block.blockId}" on page "${pageId}" requires properties.endpointId.`, {
86
+ configKey
87
+ });
88
+ }
89
+ if (!type.isString(block.properties.endpointId)) {
90
+ throw new ConfigError(`Dynamic block "${block.blockId}" on page "${pageId}" properties.endpointId is not a string.`, {
91
+ received: block.properties.endpointId,
92
+ configKey
93
+ });
94
+ }
95
+ if (!type.isNone(block.properties.params)) {
96
+ if (!type.isObject(block.properties.params)) {
97
+ throw new ConfigError(`Dynamic block "${block.blockId}" on page "${pageId}" properties.params should be an object.`, {
98
+ received: block.properties.params,
99
+ configKey
100
+ });
101
+ }
102
+ // The page artifact is static — params are passed to the endpoint verbatim,
103
+ // so operators here would never evaluate. Runtime values belong in the
104
+ // endpoint routine (_payload, _user, _secret).
105
+ countOperators(block.properties.params, {
106
+ counter: {
107
+ increment: (operator)=>{
108
+ throw new ConfigError(`Dynamic block "${block.blockId}" on page "${pageId}" properties.params must not contain operators. Found "${operator}". Params are static — read runtime values in the endpoint routine with _payload, _user or _secret.`, {
109
+ configKey
110
+ });
111
+ }
112
+ }
113
+ });
114
+ }
115
+ if (!type.isNone(block.properties.required) && !type.isBoolean(block.properties.required)) {
116
+ throw new ConfigError(`Dynamic block "${block.blockId}" on page "${pageId}" properties.required is not a boolean.`, {
117
+ received: block.properties.required,
118
+ configKey
119
+ });
120
+ }
121
+ if (!type.isNone(block.properties.types)) {
122
+ countDeclaredTypes(block, pageContext);
123
+ }
124
+ pageContext.hasDynamicBlocks = true;
125
+ pageContext.dynamicBlockRefs.push({
126
+ endpointId: block.properties.endpointId,
127
+ block,
128
+ sourcePageId: pageId
129
+ });
130
+ }
131
+ export default buildDynamicBlock;
@@ -23,7 +23,43 @@ const BROWSER_DEFAULT_SHORTCUTS = new Set([
23
23
  'mod+q',
24
24
  'mod+l'
25
25
  ]);
26
- function checkAction(action, { blockId, callApiActionRefs, checkDuplicateActionId, eventId, linkActionRefs, pageId, requestActionRefs, typeCounters }) {
26
+ const CONTROL_KEYS = [
27
+ ':if',
28
+ ':switch',
29
+ ':return'
30
+ ];
31
+ const ACTION_KEYS_NOT_ALLOWED_ON_CONTROLS = [
32
+ 'id',
33
+ 'skip',
34
+ 'messages'
35
+ ];
36
+ const CONTROL_ALLOWED_KEYS = {
37
+ ':if': [
38
+ ':if',
39
+ ':then',
40
+ ':else'
41
+ ],
42
+ ':return': [
43
+ ':return'
44
+ ],
45
+ ':switch': [
46
+ ':switch',
47
+ ':default'
48
+ ]
49
+ };
50
+ const CASE_ALLOWED_KEYS = [
51
+ ':case',
52
+ ':then'
53
+ ];
54
+ // '~'-prefixed keys are build meta markers (~k, ~r, ~l, ~ignoreBuildChecks) and
55
+ // never user config - tolerate them by prefix, as the rest of the build does.
56
+ function isMetaKey(key) {
57
+ return key.startsWith('~');
58
+ }
59
+ function isControl(item) {
60
+ return type.isObject(item) && CONTROL_KEYS.some((key)=>key in item);
61
+ }
62
+ function checkAction(action, { blockId, callApiActionRefs, checkDuplicateActionId, eventId, linkActionRefs, pageId, requestActionRefs, typeCounters, websocketActionRefs }) {
27
63
  const configKey = action['~k'];
28
64
  if (type.isUndefined(action.id)) {
29
65
  throw new ConfigError(`Action id missing on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
@@ -108,6 +144,142 @@ function checkAction(action, { blockId, callApiActionRefs, checkDuplicateActionI
108
144
  });
109
145
  }
110
146
  }
147
+ // Collect static Subscribe/Unsubscribe/Publish action references for validation
148
+ if ([
149
+ 'Subscribe',
150
+ 'Unsubscribe'
151
+ ].includes(action.type) && type.isString(action.params)) {
152
+ (websocketActionRefs ?? []).push({
153
+ websocketId: action.params,
154
+ action,
155
+ actionType: action.type,
156
+ blockId,
157
+ eventId,
158
+ sourcePageId: pageId
159
+ });
160
+ }
161
+ if (action.type === 'Publish' && type.isObject(action.params) && type.isString(action.params.websocketId)) {
162
+ (websocketActionRefs ?? []).push({
163
+ websocketId: action.params.websocketId,
164
+ action,
165
+ actionType: action.type,
166
+ blockId,
167
+ eventId,
168
+ sourcePageId: pageId
169
+ });
170
+ }
171
+ }
172
+ function checkBranchList({ list, listName, configKey }, ctx) {
173
+ const { blockId, eventId, pageId } = ctx;
174
+ if (!type.isArray(list)) {
175
+ throw new ConfigError(`Control "${listName}" must be an array on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
176
+ received: list,
177
+ configKey
178
+ });
179
+ }
180
+ checkActionList(list, ctx);
181
+ }
182
+ function checkControl(control, ctx) {
183
+ const { blockId, eventId, pageId } = ctx;
184
+ const configKey = control['~k'];
185
+ const controlKeys = CONTROL_KEYS.filter((key)=>key in control);
186
+ if (controlKeys.length > 1) {
187
+ const keyList = controlKeys.map((key)=>`"${key}"`).join(', ');
188
+ throw new ConfigError(`Control has more than one control key (${keyList}) on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
189
+ received: controlKeys,
190
+ configKey
191
+ });
192
+ }
193
+ const [controlKey] = controlKeys;
194
+ const invalidKeys = Object.keys(control).filter((key)=>!CONTROL_ALLOWED_KEYS[controlKey].includes(key) && !isMetaKey(key));
195
+ if (invalidKeys.length > 0) {
196
+ // Action keys do nothing on a control - call out the likely mistake directly.
197
+ if (ACTION_KEYS_NOT_ALLOWED_ON_CONTROLS.includes(invalidKeys[0])) {
198
+ throw new ConfigError(`Control "${controlKey}" can not have action property "${invalidKeys[0]}" on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
199
+ received: invalidKeys,
200
+ configKey
201
+ });
202
+ }
203
+ throw new ConfigError(`Control "${controlKey}" has invalid key "${invalidKeys[0]}" on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
204
+ received: invalidKeys,
205
+ configKey
206
+ });
207
+ }
208
+ if (controlKey === ':if') {
209
+ if (type.isNone(control[':then'])) {
210
+ throw new ConfigError(`Control ":if" requires a ":then" list on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
211
+ configKey
212
+ });
213
+ }
214
+ checkBranchList({
215
+ list: control[':then'],
216
+ listName: ':then',
217
+ configKey
218
+ }, ctx);
219
+ if (!type.isNone(control[':else'])) {
220
+ checkBranchList({
221
+ list: control[':else'],
222
+ listName: ':else',
223
+ configKey
224
+ }, ctx);
225
+ }
226
+ }
227
+ if (controlKey === ':switch') {
228
+ if (!type.isArray(control[':switch'])) {
229
+ throw new ConfigError(`Control ":switch" must be an array of case objects on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
230
+ received: control[':switch'],
231
+ configKey
232
+ });
233
+ }
234
+ control[':switch'].forEach((caseObject)=>{
235
+ if (!type.isObject(caseObject)) {
236
+ throw new ConfigError(`Control ":switch" case must be an object on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
237
+ received: caseObject,
238
+ configKey
239
+ });
240
+ }
241
+ const caseConfigKey = caseObject['~k'] ?? configKey;
242
+ if (!(':case' in caseObject)) {
243
+ throw new ConfigError(`Control ":switch" case requires a ":case" condition on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
244
+ configKey: caseConfigKey
245
+ });
246
+ }
247
+ if (type.isNone(caseObject[':then'])) {
248
+ throw new ConfigError(`Control ":case" requires a ":then" list on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
249
+ configKey: caseConfigKey
250
+ });
251
+ }
252
+ const invalidCaseKeys = Object.keys(caseObject).filter((key)=>!CASE_ALLOWED_KEYS.includes(key) && !isMetaKey(key));
253
+ if (invalidCaseKeys.length > 0) {
254
+ throw new ConfigError(`Control ":switch" case has invalid key "${invalidCaseKeys[0]}" on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
255
+ received: invalidCaseKeys,
256
+ configKey: caseConfigKey
257
+ });
258
+ }
259
+ checkBranchList({
260
+ list: caseObject[':then'],
261
+ listName: ':then',
262
+ configKey: caseConfigKey
263
+ }, ctx);
264
+ });
265
+ if (!type.isNone(control[':default'])) {
266
+ checkBranchList({
267
+ list: control[':default'],
268
+ listName: ':default',
269
+ configKey
270
+ }, ctx);
271
+ }
272
+ }
273
+ // ':return' takes a value, not a list - nothing more to validate.
274
+ }
275
+ function checkActionList(list, ctx) {
276
+ list.forEach((item)=>{
277
+ if (isControl(item)) {
278
+ checkControl(item, ctx);
279
+ } else {
280
+ checkAction(item, ctx);
281
+ }
282
+ });
111
283
  }
112
284
  function buildEvents(block, pageContext) {
113
285
  if (block.events) {
@@ -143,26 +315,19 @@ function buildEvents(block, pageContext) {
143
315
  const checkDuplicateActionId = createCheckDuplicateId({
144
316
  message: 'Duplicate actionId "{{ id }}" on event "{{ eventId }}" on block "{{ blockId }}" on page "{{ pageId }}".'
145
317
  });
146
- block.events[key].try.map((action)=>checkAction(action, {
147
- eventId: key,
148
- blockId: block.blockId,
149
- callApiActionRefs: pageContext.callApiActionRefs,
150
- typeCounters: pageContext.typeCounters,
151
- pageId: pageContext.pageId,
152
- linkActionRefs: pageContext.linkActionRefs,
153
- requestActionRefs: pageContext.requestActionRefs,
154
- checkDuplicateActionId
155
- }));
156
- block.events[key].catch.map((action)=>checkAction(action, {
157
- eventId: key,
158
- blockId: block.blockId,
159
- callApiActionRefs: pageContext.callApiActionRefs,
160
- typeCounters: pageContext.typeCounters,
161
- pageId: pageContext.pageId,
162
- linkActionRefs: pageContext.linkActionRefs,
163
- requestActionRefs: pageContext.requestActionRefs,
164
- checkDuplicateActionId
165
- }));
318
+ const actionContext = {
319
+ eventId: key,
320
+ blockId: block.blockId,
321
+ callApiActionRefs: pageContext.callApiActionRefs,
322
+ typeCounters: pageContext.typeCounters,
323
+ pageId: pageContext.pageId,
324
+ linkActionRefs: pageContext.linkActionRefs,
325
+ requestActionRefs: pageContext.requestActionRefs,
326
+ websocketActionRefs: pageContext.websocketActionRefs,
327
+ checkDuplicateActionId
328
+ };
329
+ checkActionList(block.events[key].try, actionContext);
330
+ checkActionList(block.events[key].catch, actionContext);
166
331
  // Validate shortcut strings and collect refs for duplicate detection
167
332
  if (type.isObject(block.events[key]) && !type.isNone(block.events[key].shortcut)) {
168
333
  const shortcuts = type.isArray(block.events[key].shortcut) ? block.events[key].shortcut : [
@@ -75,6 +75,15 @@ function buildRequest(request, pageContext) {
75
75
  pageContext.requests.push(request);
76
76
  }
77
77
  function buildRequests(block, pageContext) {
78
+ // Runtime-resolved dynamic content cannot define requests — request
79
+ // artifacts are written at build time, so a runtime request would have no
80
+ // server-side properties to execute.
81
+ if (pageContext.forbidRequests === true && !type.isNone(block.requests)) {
82
+ throw new ConfigError(`Dynamic content must not define requests — found "requests" on block "${block.blockId}" on page "${pageContext.pageId}". Reference requests defined statically on the page instead.`, {
83
+ received: block.requests,
84
+ configKey: block['~k']
85
+ });
86
+ }
78
87
  (block.requests || []).forEach((request)=>{
79
88
  buildRequest(request, pageContext);
80
89
  });
@@ -12,7 +12,7 @@
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 { type } from '@lowdefy/helpers';
15
+ */ import { getOperatorType, type } from '@lowdefy/helpers';
16
16
  import { ConfigError } from '@lowdefy/errors';
17
17
  const breakpointKeys = new Set([
18
18
  'xs',
@@ -22,17 +22,8 @@ const breakpointKeys = new Set([
22
22
  'xl',
23
23
  '2xl'
24
24
  ]);
25
- // Keys that look like operators (single key starting with _) but are not.
26
- const KNOWN_NON_OPERATORS = new Set([
27
- '_id'
28
- ]);
29
25
  function isOperator(value) {
30
- const nonTildeKeys = Object.keys(value).filter((k)=>!k.startsWith('~'));
31
- if (nonTildeKeys.length !== 1) return false;
32
- const key = nonTildeKeys[0];
33
- const [op] = key.split('.');
34
- const operator = op.replace(/^(_+)/gm, '_');
35
- return operator.length > 1 && operator[0] === '_' && !KNOWN_NON_OPERATORS.has(operator);
26
+ return getOperatorType(value) !== null;
36
27
  }
37
28
  function stripDotPrefix(key) {
38
29
  return key.startsWith('.') ? key.slice(1) : key;
@@ -13,14 +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 setBlockId(block, { pageId, blockIdCounter }) {
16
+ function setBlockId(block, { pageId, blockIdCounter, blockIdPrefix }) {
17
17
  block.blockId = block.id;
18
18
  if (block.blockId === pageId && blockIdCounter.getCount(block.blockId) > 0) {
19
19
  throw new ConfigError(`Block id "${block.blockId}" on page "${pageId}" collides with the page id. A block cannot have the same id as its page.`, {
20
20
  configKey: block['~k']
21
21
  });
22
22
  }
23
- block.id = `block:${pageId}:${block.blockId}:${blockIdCounter.getCount(block.blockId)}`;
23
+ // blockIdPrefix namespaces runtime-built dynamic content ids under the
24
+ // resolving Dynamic block's id so they can never collide with static ids.
25
+ const prefix = blockIdPrefix ?? `block:${pageId}`;
26
+ block.id = `${prefix}:${block.blockId}:${blockIdCounter.getCount(block.blockId)}`;
24
27
  blockIdCounter.increment(block.blockId);
25
28
  }
26
29
  export default setBlockId;
@@ -63,5 +63,12 @@ function validateBlock(block, { pageId }, parentConfigKey) {
63
63
  });
64
64
  }
65
65
  }
66
+ // Page subscriptions are extracted before block building — any subscriptions
67
+ // key reaching here is defined on a nested block, where it has no effect.
68
+ if (!type.isNone(block.subscriptions)) {
69
+ throw new ConfigError(`Subscriptions are only allowed on the page, not on block "${block.id}" on page "${pageId}".`, {
70
+ configKey
71
+ });
72
+ }
66
73
  }
67
74
  export default validateBlock;