@lowdefy/build 5.6.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +40 -0
- package/dist/build/buildApi/validateEndpoint.js +96 -1
- package/dist/build/buildApp.js +13 -20
- package/dist/build/buildAppMeta.js +138 -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/buildEnvironments.js +261 -0
- 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/buildLogger.js +22 -6
- 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 +84 -3
- 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 +6 -1
- package/dist/build/validateEnvironmentGuards.js +69 -0
- package/dist/build/validateRenderNotificationSteps.js +50 -0
- package/dist/build/writeApi.js +98 -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 +688 -388
- package/dist/dynamic/buildDynamicBlocks.js +120 -0
- package/dist/index.js +81 -10
- package/dist/indexDev.js +4 -0
- package/dist/lowdefySchema.js +969 -6
- package/dist/scripts/generateDefaultTypes.js +5 -2
- package/dist/test-utils/expectTerminates.js +33 -0
- package/dist/test-utils/runBuild.js +15 -1
- package/dist/test-utils/runBuildForSnapshots.js +37 -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/getEnvironmentNames.js +26 -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 +67 -60
- package/dist/build/writePluginImports/writeServerExternalPackages.js +0 -121
- /package/dist/build/{resolveModuleOperators.js → buildRefs/scopeMenuItemIds.js} +0 -0
|
@@ -25,17 +25,33 @@ const sentryDefaults = {
|
|
|
25
25
|
'_id'
|
|
26
26
|
]
|
|
27
27
|
};
|
|
28
|
+
// Runs after validateConfig, so config.environment and config.environments are resolved.
|
|
28
29
|
function buildLogger({ components }) {
|
|
29
30
|
if (type.isNone(components.logger)) {
|
|
30
31
|
components.logger = {};
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
};
|
|
33
|
+
const current = components.config.environment;
|
|
34
|
+
// Sentry is enabled by SENTRY_DSN, not by the config being present, so a current environment
|
|
35
|
+
// creates the Sentry config to carry its name. Without either, the runtime defaults apply.
|
|
36
|
+
if (type.isNone(components.logger.sentry) && type.isNone(current)) {
|
|
37
|
+
return components;
|
|
38
38
|
}
|
|
39
|
+
const sentry = {
|
|
40
|
+
...sentryDefaults,
|
|
41
|
+
...components.logger.sentry ?? {}
|
|
42
|
+
};
|
|
43
|
+
if (!type.isNone(current)) {
|
|
44
|
+
// Sentry reports under the environment name unless the app names one itself, and is off on
|
|
45
|
+
// both sides when the environment switches it off.
|
|
46
|
+
if (type.isNone(sentry.environment)) {
|
|
47
|
+
sentry.environment = current;
|
|
48
|
+
}
|
|
49
|
+
if (components.config.environments?.[current]?.sentry?.enabled === false) {
|
|
50
|
+
sentry.client = false;
|
|
51
|
+
sentry.server = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
components.logger.sentry = sentry;
|
|
39
55
|
return components;
|
|
40
56
|
}
|
|
41
57
|
export default buildLogger;
|
|
@@ -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
|
+
*/ /* eslint-disable no-param-reassign */ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import { ConfigError } from '@lowdefy/errors';
|
|
17
|
+
// Validates the mcp block and writes its defaults. Endpoints are matched via
|
|
18
|
+
// the same dual id/endpointId match buildApi uses. Referenced endpoints must
|
|
19
|
+
// carry the metadata an MCP client needs: a description and a payloadSchema
|
|
20
|
+
// (the tool inputSchema).
|
|
21
|
+
function buildMcp({ components }) {
|
|
22
|
+
if (type.isNone(components.mcp)) {
|
|
23
|
+
components.mcp = {};
|
|
24
|
+
}
|
|
25
|
+
const mcp = components.mcp;
|
|
26
|
+
const configKey = mcp['~k'] ?? components['~k'];
|
|
27
|
+
// Agent tools were removed from mcp; schema validation only warns on
|
|
28
|
+
// unknown properties, so a leftover "agents" key would otherwise build
|
|
29
|
+
// silently instead of surfacing the removal.
|
|
30
|
+
if (!type.isNone(mcp.agents)) {
|
|
31
|
+
throw new ConfigError('MCP agent tools are not supported. Remove "mcp.agents" from your config.', {
|
|
32
|
+
configKey
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
mcp.name = mcp.name ?? 'lowdefy';
|
|
36
|
+
mcp.version = mcp.version ?? '1.0.0';
|
|
37
|
+
mcp.endpoints = mcp.endpoints ?? [];
|
|
38
|
+
mcp.configured = mcp.endpoints.length > 0;
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
mcp.endpoints.forEach((endpointId)=>{
|
|
41
|
+
if (seen.has(endpointId)) {
|
|
42
|
+
throw new ConfigError(`Duplicate MCP tool "${endpointId}".`, {
|
|
43
|
+
configKey
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
seen.add(endpointId);
|
|
47
|
+
const endpoint = (components.api ?? []).find((e)=>e.id === endpointId || e.endpointId === endpointId);
|
|
48
|
+
if (type.isNone(endpoint)) {
|
|
49
|
+
throw new ConfigError(`MCP endpoint "${endpointId}" does not reference a defined api endpoint.`, {
|
|
50
|
+
configKey
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// MCP is an external transport - InternalApi endpoints are not addressable
|
|
54
|
+
// from outside the server, matching the HTTP endpoint route.
|
|
55
|
+
if (endpoint.type === 'InternalApi') {
|
|
56
|
+
throw new ConfigError(`MCP endpoint "${endpointId}" is an InternalApi endpoint. Only "Api" endpoints can be exposed as MCP tools.`, {
|
|
57
|
+
configKey
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (type.isNone(endpoint.description)) {
|
|
61
|
+
throw new ConfigError(`Endpoint "${endpointId}" is exposed as an MCP tool but does not have a "description".`, {
|
|
62
|
+
configKey: endpoint['~k'] ?? configKey
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (type.isNone(endpoint.payloadSchema)) {
|
|
66
|
+
throw new ConfigError(`Endpoint "${endpointId}" is exposed as an MCP tool but does not have a "payloadSchema".`, {
|
|
67
|
+
configKey: endpoint['~k'] ?? configKey
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return components;
|
|
72
|
+
}
|
|
73
|
+
export default buildMcp;
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import operators from '@lowdefy/operators-js/operators/build';
|
|
16
16
|
import { resolve, WalkContext } from './buildRefs/walker.js';
|
|
17
|
+
import { assertNoPlaceholderLeaks } from './buildRefs/deferredRegistry.js';
|
|
17
18
|
import getRefContent from './buildRefs/getRefContent.js';
|
|
18
19
|
import makeRefDefinition from './buildRefs/makeRefDefinition.js';
|
|
19
|
-
import evaluateStaticOperators from './buildRefs/evaluateStaticOperators.js';
|
|
20
20
|
import collectDynamicIdentifiers from './collectDynamicIdentifiers.js';
|
|
21
21
|
import validateOperatorsDynamic from './validateOperatorsDynamic.js';
|
|
22
22
|
import fetchModules from './fetchModules.js';
|
|
23
|
-
import { resolveLocalManifest, resolveFullManifest, validateRequiredVars } from './registerModules.js';
|
|
23
|
+
import { resolveLocalManifest, recordifyExportables, resolveFullManifest, validateRequiredVars } from './registerModules.js';
|
|
24
24
|
import resolveModuleDependencies from './resolveModuleDependencies.js';
|
|
25
25
|
validateOperatorsDynamic({
|
|
26
26
|
operators
|
|
@@ -50,64 +50,86 @@ async function parseLowdefyYaml({ context }) {
|
|
|
50
50
|
] : []),
|
|
51
51
|
operators,
|
|
52
52
|
env: process.env,
|
|
53
|
+
lowdefyApp: context.appMeta,
|
|
53
54
|
dynamicIdentifiers,
|
|
54
55
|
shouldStop: (path)=>{
|
|
55
56
|
// Defer entry vars and connections: they may contain cross-module
|
|
56
|
-
// refs that require modules to be registered first.
|
|
57
|
-
|
|
58
|
-
if (/^modules\.\d+\.
|
|
57
|
+
// refs that require modules to be registered first. Non-modules keys
|
|
58
|
+
// stay raw for the app buildRefs pass.
|
|
59
|
+
if (/^modules\.\d+\.vars$/.test(path)) return 'skip';
|
|
60
|
+
if (/^modules\.\d+\.connections$/.test(path)) return 'skip';
|
|
59
61
|
if (path.startsWith('modules')) return false;
|
|
60
|
-
return '
|
|
62
|
+
return 'skip';
|
|
61
63
|
}
|
|
62
64
|
});
|
|
63
|
-
|
|
64
|
-
config = evaluateStaticOperators({
|
|
65
|
-
context,
|
|
66
|
-
input: config,
|
|
67
|
-
refDef
|
|
68
|
-
});
|
|
65
|
+
const config = await resolve(content, ctx);
|
|
69
66
|
return config ?? {};
|
|
70
67
|
}
|
|
71
|
-
|
|
72
|
-
const moduleEntry = context.modules[entry.id];
|
|
68
|
+
function makeAppLevelCtx({ context, deferModuleRefs = false, entryId, entrySection }) {
|
|
73
69
|
const lowdefyYamlRefDef = context.lowdefyYamlRefDef;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
let resolvedVars = await resolve(moduleEntry.consumerVars, makeAppLevelCtx());
|
|
92
|
-
resolvedVars = evaluateStaticOperators({
|
|
93
|
-
context,
|
|
94
|
-
input: resolvedVars,
|
|
95
|
-
refDef
|
|
70
|
+
return new WalkContext({
|
|
71
|
+
buildContext: context,
|
|
72
|
+
refId: lowdefyYamlRefDef.id,
|
|
73
|
+
sourceRefId: null,
|
|
74
|
+
vars: {},
|
|
75
|
+
path: '',
|
|
76
|
+
currentFile: lowdefyYamlRefDef.path,
|
|
77
|
+
refChain: new Set(lowdefyYamlRefDef.path ? [
|
|
78
|
+
lowdefyYamlRefDef.path
|
|
79
|
+
] : []),
|
|
80
|
+
operators,
|
|
81
|
+
env: process.env,
|
|
82
|
+
lowdefyApp: context.appMeta,
|
|
83
|
+
dynamicIdentifiers,
|
|
84
|
+
deferModuleRefs,
|
|
85
|
+
entryId,
|
|
86
|
+
entrySection
|
|
96
87
|
});
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
88
|
+
}
|
|
89
|
+
// Stage 1 — prepare: resolve entry vars/connections in the enclosing (app)
|
|
90
|
+
// scope; cross-module refs become entryRef records with the prepared refDef as
|
|
91
|
+
// body (their dynamic parts already resolved), leaving placeholders in the
|
|
92
|
+
// blobs. Order-free: nothing is consumed here.
|
|
93
|
+
async function prepareEntryConfig({ moduleEntry, context }) {
|
|
94
|
+
const varsResult = await resolve(moduleEntry.consumerVars, makeAppLevelCtx({
|
|
100
95
|
context,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
96
|
+
deferModuleRefs: true,
|
|
97
|
+
entryId: moduleEntry.id,
|
|
98
|
+
entrySection: 'consumerVars'
|
|
99
|
+
}));
|
|
100
|
+
moduleEntry.consumerVars = varsResult ?? {};
|
|
101
|
+
const connectionsResult = await resolve(moduleEntry.connections, makeAppLevelCtx({
|
|
102
|
+
context,
|
|
103
|
+
deferModuleRefs: true,
|
|
104
|
+
entryId: moduleEntry.id,
|
|
105
|
+
entrySection: 'connections'
|
|
106
|
+
}));
|
|
107
|
+
moduleEntry.connections = connectionsResult ?? {};
|
|
108
|
+
}
|
|
109
|
+
// Final sweep: re-walk each entry's blobs with deferral off. Placeholders
|
|
110
|
+
// dispatch into resolveDeferred (value-granular demand, wait-graph cycle
|
|
111
|
+
// detection — order immaterial, memo skips already-demanded records), and the
|
|
112
|
+
// _module.connectionId object form (deferred during prepare) resolves against
|
|
113
|
+
// now-prepared targets. In-place walk mutation is the write-back, so resolved
|
|
114
|
+
// values land in consumerVars/connections with consumption provenance.
|
|
115
|
+
async function sweepEntryConfig({ moduleEntry, context }) {
|
|
116
|
+
const varsResult = await resolve(moduleEntry.consumerVars, makeAppLevelCtx({
|
|
117
|
+
context
|
|
118
|
+
}));
|
|
119
|
+
moduleEntry.consumerVars = varsResult ?? {};
|
|
120
|
+
const connectionsResult = await resolve(moduleEntry.connections, makeAppLevelCtx({
|
|
121
|
+
context
|
|
122
|
+
}));
|
|
123
|
+
moduleEntry.connections = connectionsResult ?? {};
|
|
124
|
+
validateRequiredVars(moduleEntry.varDefs, moduleEntry.consumerVars, moduleEntry.id, moduleEntry.source);
|
|
106
125
|
}
|
|
107
126
|
async function buildModuleDefs({ context }) {
|
|
108
127
|
const lowdefyConfig = await parseLowdefyYaml({
|
|
109
128
|
context
|
|
110
129
|
});
|
|
130
|
+
// Stash for index.js to compute appMeta before buildRefs, so the _build.app
|
|
131
|
+
// operator can resolve app metadata during ref resolution.
|
|
132
|
+
context.lowdefyConfig = lowdefyConfig;
|
|
111
133
|
context.plugins = lowdefyConfig.plugins ?? [];
|
|
112
134
|
const moduleEntries = lowdefyConfig.modules ?? [];
|
|
113
135
|
if (moduleEntries.length === 0) {
|
|
@@ -129,11 +151,28 @@ async function buildModuleDefs({ context }) {
|
|
|
129
151
|
resolveModuleDependencies({
|
|
130
152
|
context
|
|
131
153
|
});
|
|
132
|
-
// Step 2.
|
|
133
|
-
// then validate required vars against the resolved values.
|
|
154
|
+
// Step 2.5a — prepare all entries. No cross-entry work; order-free.
|
|
134
155
|
for (const entry of moduleEntries){
|
|
135
|
-
await
|
|
136
|
-
entry,
|
|
156
|
+
await prepareEntryConfig({
|
|
157
|
+
moduleEntry: context.modules[entry.id],
|
|
158
|
+
context
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// Phase C.5 — record-ify exportables (components, menu links) for every
|
|
162
|
+
// entry BEFORE anything can consume them: record creation must precede all
|
|
163
|
+
// consumption, or cross-entry refs would race manifest resolve order.
|
|
164
|
+
for (const entry of moduleEntries){
|
|
165
|
+
await recordifyExportables({
|
|
166
|
+
entryId: entry.id,
|
|
167
|
+
context
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// Step 2.5b — final sweep. Per-record demand through the wait-graph makes
|
|
171
|
+
// the order immaterial; required-var validation runs per entry after its
|
|
172
|
+
// blobs are concrete.
|
|
173
|
+
for (const entry of moduleEntries){
|
|
174
|
+
await sweepEntryConfig({
|
|
175
|
+
moduleEntry: context.modules[entry.id],
|
|
137
176
|
context
|
|
138
177
|
});
|
|
139
178
|
}
|
|
@@ -144,5 +183,8 @@ async function buildModuleDefs({ context }) {
|
|
|
144
183
|
context
|
|
145
184
|
});
|
|
146
185
|
}
|
|
186
|
+
// Post-sweep invariant: no deferred placeholder survives outside the
|
|
187
|
+
// per-consumer slots (manifest component/menu bodies, varDefs defaults).
|
|
188
|
+
assertNoPlaceholderLeaks(context);
|
|
147
189
|
}
|
|
148
190
|
export default buildModuleDefs;
|
|
@@ -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;
|