@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.
- package/dist/build/buildAgents.js +61 -16
- package/dist/build/buildApi/buildApi.js +4 -1
- package/dist/build/buildApi/buildEndpoint.js +8 -2
- package/dist/build/buildApi/buildRoutine/countStepTypes.js +1 -1
- package/dist/build/buildApi/buildRoutine/setStepId.js +2 -0
- package/dist/build/buildApi/buildRoutine/validateStep.js +61 -0
- package/dist/build/buildApi/resolveEndpointSchedules.js +37 -0
- package/dist/build/buildApi/validateEndpoint.js +96 -1
- package/dist/build/buildApp.js +13 -20
- package/dist/build/buildAppMeta.js +130 -0
- package/dist/build/buildAuth/buildAgentAuth.js +14 -1
- package/dist/build/buildAuth/buildAuth.js +8 -3
- package/dist/build/buildAuth/buildAuthPlugins.js +6 -0
- package/dist/build/buildAuth/buildAuthStrategies.js +117 -0
- package/dist/build/buildAuth/buildWebsocketAuth.js +55 -0
- package/dist/build/buildAuth/getProtectedWebsockets.js +29 -0
- package/dist/build/buildAuth/getWebsocketRoles.js +39 -0
- package/dist/build/buildAuth/validateAuthConfig.js +15 -3
- package/dist/build/buildImports/buildImportsDev.js +15 -0
- package/dist/build/buildImports/buildImportsProd.js +4 -1
- package/dist/build/buildJs/generateClientJsModule.js +27 -0
- package/dist/build/buildJs/writeJs.js +3 -5
- package/dist/build/buildMcp.js +73 -0
- package/dist/build/buildModuleDefs.js +90 -48
- package/dist/build/buildModules.js +61 -14
- package/dist/build/buildNotifications.js +98 -0
- package/dist/build/buildPages/buildBlock/buildBlock.js +3 -1
- package/dist/build/buildPages/buildBlock/buildDynamicBlock.js +131 -0
- package/dist/build/buildPages/buildBlock/buildEvents.js +186 -21
- package/dist/build/buildPages/buildBlock/buildRequests.js +9 -0
- package/dist/build/buildPages/buildBlock/normalizeClassAndStyles.js +2 -11
- package/dist/build/buildPages/buildBlock/setBlockId.js +5 -2
- package/dist/build/buildPages/buildBlock/validateBlock.js +7 -0
- package/dist/build/buildPages/buildPage.js +27 -3
- package/dist/build/buildPages/buildSubscriptions.js +145 -0
- package/dist/build/buildPages/validateDynamicBlockRefs.js +30 -0
- package/dist/build/buildPages/validateStateReferences.js +4 -2
- package/dist/build/buildPages/validateWebsocketRefs.js +30 -0
- package/dist/build/buildRefs/buildRefs.js +21 -12
- package/dist/build/buildRefs/cloneWithMarkers.js +57 -0
- package/dist/build/buildRefs/deferredRegions.js +80 -0
- package/dist/build/buildRefs/deferredRegistry.js +292 -0
- package/dist/build/buildRefs/getModuleRefContent.js +21 -6
- package/dist/build/buildRefs/getUserJavascriptFunction.js +1 -1
- package/dist/build/buildRefs/parseRefContent.js +24 -0
- package/dist/build/buildRefs/{evaluateStaticOperators.js → precomputeRuntimeOperators.js} +7 -6
- package/dist/build/buildRefs/walker.js +471 -207
- package/dist/build/buildTypes.js +22 -1
- package/dist/build/buildWebsockets.js +99 -0
- package/dist/build/collectPageContent.js +7 -1
- package/dist/build/copyAgentFileSystems.js +2 -2
- package/dist/build/full/buildPages.js +16 -0
- package/dist/build/full/updateServerPackageJson.js +21 -0
- package/dist/build/jit/buildPageJit.js +46 -6
- package/dist/build/jit/collectSkeletonSourceFiles.js +14 -0
- package/dist/build/jit/pageContentKeys.js +1 -0
- package/dist/build/jit/shallowBuild.js +81 -1
- package/dist/build/jit/writePageJit.js +11 -8
- package/dist/build/registerModules.js +125 -24
- package/dist/build/validateCallAgentSteps.js +58 -0
- package/dist/build/validateConfig.js +4 -0
- package/dist/build/validateCronConfig.js +110 -0
- package/dist/build/validateRenderNotificationSteps.js +50 -0
- package/dist/build/writeApi.js +58 -1
- package/dist/build/writeMcp.js +21 -0
- package/dist/build/writeNotifications.js +30 -0
- package/dist/build/writePluginImports/importPluginModule.js +39 -0
- package/dist/build/writePluginImports/writeActionSchemaMap.js +6 -7
- package/dist/build/writePluginImports/writeAuthImports.js +4 -0
- package/dist/build/writePluginImports/writeAvailableTypes.js +99 -0
- package/dist/build/writePluginImports/writeBlockSchemaMap.js +10 -9
- package/dist/build/writePluginImports/writeConnectionSchemaMap.js +101 -0
- package/dist/build/writePluginImports/writeGlobalsCss.js +20 -10
- package/dist/build/writePluginImports/writeNotificationImports.js +31 -0
- package/dist/build/writePluginImports/writeOperatorSchemaMap.js +6 -7
- package/dist/build/writePluginImports/writePluginImports.js +18 -4
- package/dist/build/writePluginImports/writeWebsocketImports.js +22 -0
- package/dist/build/writeWebsockets.js +30 -0
- package/dist/createContext.js +6 -1
- package/dist/defaultPackages.js +7 -1
- package/dist/defaultTypesMap.js +666 -388
- package/dist/dynamic/buildDynamicBlocks.js +120 -0
- package/dist/index.js +78 -8
- package/dist/indexDev.js +4 -0
- package/dist/lowdefySchema.js +861 -6
- package/dist/scripts/generateDefaultTypes.js +5 -2
- package/dist/test-utils/expectTerminates.js +33 -0
- package/dist/test-utils/runBuild.js +12 -1
- package/dist/test-utils/runBuildForSnapshots.js +34 -4
- package/dist/test-utils/testContext.js +8 -2
- package/dist/utils/countOperators.js +5 -17
- package/dist/utils/createPluginTypesMap.js +21 -0
- package/dist/utils/getCronEnvironmentNames.js +25 -0
- package/dist/utils/logCollectedErrors.js +15 -9
- package/dist/utils/serializeBuildException.js +29 -0
- package/dist/utils/validateCronExpression.js +131 -0
- package/dist/utils/writeBuildArtifact.js +5 -2
- package/package.json +66 -59
- package/dist/build/writePluginImports/writeServerExternalPackages.js +0 -121
- /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 {
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|