@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
|
@@ -20,9 +20,9 @@ import operators from '@lowdefy/operators-js/operators/build';
|
|
|
20
20
|
import { resolve, WalkContext } from './buildRefs/walker.js';
|
|
21
21
|
import getRefContent from './buildRefs/getRefContent.js';
|
|
22
22
|
import makeRefDefinition from './buildRefs/makeRefDefinition.js';
|
|
23
|
-
import evaluateStaticOperators from './buildRefs/evaluateStaticOperators.js';
|
|
24
23
|
import collectDynamicIdentifiers from './collectDynamicIdentifiers.js';
|
|
25
24
|
import validateOperatorsDynamic from './validateOperatorsDynamic.js';
|
|
25
|
+
import { makeShouldStop } from './buildRefs/deferredRegions.js';
|
|
26
26
|
validateOperatorsDynamic({
|
|
27
27
|
operators
|
|
28
28
|
});
|
|
@@ -48,11 +48,69 @@ function validateRequiredVars(varDefs, consumerVars, entryId, source, prefix = '
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
function getRuntimeOperatorKey(value) {
|
|
52
|
+
if (!type.isObject(value)) return null;
|
|
53
|
+
const nonTildeKeys = Object.keys(value).filter((k)=>!k.startsWith('~'));
|
|
54
|
+
if (nonTildeKeys.length === 1 && nonTildeKeys[0].startsWith('_')) return nonTildeKeys[0];
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
function suggestBuildOperator(operatorKey) {
|
|
58
|
+
// '_string.concat' -> '_build.string.concat', '_sum' -> '_build.sum'
|
|
59
|
+
return `_build.${operatorKey.slice(1)}`;
|
|
60
|
+
}
|
|
61
|
+
// Component bodies are preserved by config path (components.<i>.component), so
|
|
62
|
+
// bodies under an operator object at `components` itself or at an array element
|
|
63
|
+
// escape preservation and resolve eagerly during the manifest walk. That is
|
|
64
|
+
// harmless for static bodies (operator-composed component lists are supported —
|
|
65
|
+
// pinned by fixture 81-cross-module-build-op-components), but a _var in such a
|
|
66
|
+
// body resolves against the manifest walk's scope instead of the consumer's
|
|
67
|
+
// per-ref vars — a silently wrong value. Error on that combination explicitly.
|
|
68
|
+
// A _ref at either position is fully safe: it resolves top-down, so paths
|
|
69
|
+
// inside the ref'd file still match the preserve regex. An operator at
|
|
70
|
+
// components.<i>.component itself is inside the preserved body and is fine.
|
|
71
|
+
function findVarNode(value) {
|
|
72
|
+
if (type.isArray(value)) {
|
|
73
|
+
for (const item of value){
|
|
74
|
+
const found = findVarNode(item);
|
|
75
|
+
if (found) return found;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (!type.isObject(value)) return null;
|
|
80
|
+
if (getRuntimeOperatorKey(value) === '_var') return value;
|
|
81
|
+
for (const key of Object.keys(value)){
|
|
82
|
+
const found = findVarNode(value[key]);
|
|
83
|
+
if (found) return found;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function assertStaticComponentsList({ components, entryId, filePath }) {
|
|
88
|
+
if (!type.isObject(components) && !type.isArray(components)) return;
|
|
89
|
+
const check = (value, position)=>{
|
|
90
|
+
const operatorKey = getRuntimeOperatorKey(value);
|
|
91
|
+
if (operatorKey && operatorKey !== '_ref' && findVarNode(value)) {
|
|
92
|
+
throw new ConfigError(`Module "${entryId}": _var inside an operator-generated components section ` + `cannot resolve per consumer. Found "${operatorKey}" at ${position} with a _var ` + `in its content. Define components whose bodies use _var as static ` + `{ id, component } list items so the bodies are preserved.`, {
|
|
93
|
+
filePath
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
check(components, '"components"');
|
|
98
|
+
if (type.isArray(components)) {
|
|
99
|
+
components.forEach((item, i)=>check(item, `"components.${i}"`));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
51
102
|
function validateVarTypes(varDefs, resolvedVarCache, entryId, source, prefix = '') {
|
|
52
103
|
for (const [varName, varDef] of Object.entries(varDefs)){
|
|
53
104
|
const fullName = prefix ? `${prefix}.${varName}` : varName;
|
|
54
105
|
const value = resolvedVarCache[fullName];
|
|
55
106
|
if (varDef.type && !type.isNone(value)) {
|
|
107
|
+
// A typed var must hold a concrete value — runtime operators are not allowed
|
|
108
|
+
// regardless of whether they are static-foldable or dynamic. Suggest the
|
|
109
|
+
// build-time equivalent so the user knows how to fix it.
|
|
110
|
+
const operatorKey = getRuntimeOperatorKey(value);
|
|
111
|
+
if (operatorKey) {
|
|
112
|
+
throw new ConfigError(`Module "${entryId}" (${source}) var "${fullName}" is typed "${varDef.type}" ` + `but received a runtime operator "${operatorKey}". ` + `Use the build-time equivalent "${suggestBuildOperator(operatorKey)}" instead.` + (varDef.description ? `\n - ${varDef.description}` : ''));
|
|
113
|
+
}
|
|
56
114
|
if (type.typeOf(value) !== varDef.type) {
|
|
57
115
|
throw new ConfigError(`Module "${entryId}" (${source}) var "${fullName}" must be type ` + `"${varDef.type}" but got "${type.typeOf(value)}".` + (varDef.description ? `\n - ${varDef.description}` : ''));
|
|
58
116
|
}
|
|
@@ -85,7 +143,17 @@ async function resolveLocalManifest({ entry, resolvedPaths, context }) {
|
|
|
85
143
|
refDef,
|
|
86
144
|
referencedFrom: null
|
|
87
145
|
});
|
|
88
|
-
|
|
146
|
+
if (type.isObject(content)) {
|
|
147
|
+
assertStaticComponentsList({
|
|
148
|
+
components: content.components,
|
|
149
|
+
entryId: entry.id,
|
|
150
|
+
filePath: moduleYamlPath
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// Phase A — header parse: walk only the static header keys (dependencies,
|
|
154
|
+
// plugins, secrets, vars definitions — defaults record-ified). Content
|
|
155
|
+
// sections stay raw parsed YAML; Phase C.5 record-ifies exportables and
|
|
156
|
+
// Phase D resolves the rest.
|
|
89
157
|
const ctx = new WalkContext({
|
|
90
158
|
buildContext: context,
|
|
91
159
|
refId: refDef.id,
|
|
@@ -93,6 +161,9 @@ async function resolveLocalManifest({ entry, resolvedPaths, context }) {
|
|
|
93
161
|
vars: {},
|
|
94
162
|
moduleRoot,
|
|
95
163
|
packageRoot,
|
|
164
|
+
// The entry object is created after this walk; deferred-record coordinates
|
|
165
|
+
// still need the owning entry id.
|
|
166
|
+
entryId: entry.id,
|
|
96
167
|
path: '',
|
|
97
168
|
currentFile: moduleYamlPath,
|
|
98
169
|
refChain: new Set(refDef.path ? [
|
|
@@ -100,16 +171,9 @@ async function resolveLocalManifest({ entry, resolvedPaths, context }) {
|
|
|
100
171
|
] : []),
|
|
101
172
|
operators,
|
|
102
173
|
env: process.env,
|
|
174
|
+
lowdefyApp: context.appMeta,
|
|
103
175
|
dynamicIdentifiers,
|
|
104
|
-
shouldStop: (
|
|
105
|
-
if (/^vars(\.[^.]+\.properties)*\.[^.]+\.default(\..*)?$/.test(childPath)) return 'preserve';
|
|
106
|
-
if (/^components\.\d+\.component$/.test(childPath)) return 'preserve';
|
|
107
|
-
if (/^pages(\..*)?$/.test(childPath)) return 'preserve';
|
|
108
|
-
if (/^api(\..*)?$/.test(childPath)) return 'preserve';
|
|
109
|
-
if (/^connections(\..*)?$/.test(childPath)) return 'preserve';
|
|
110
|
-
if (/^menus\.\d+\.links$/.test(childPath)) return 'preserve';
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
176
|
+
shouldStop: makeShouldStop('header')
|
|
113
177
|
});
|
|
114
178
|
const manifest = await resolve(content, ctx);
|
|
115
179
|
// Parse dependencies array from manifest
|
|
@@ -173,6 +237,49 @@ async function resolveLocalManifest({ entry, resolvedPaths, context }) {
|
|
|
173
237
|
throw error;
|
|
174
238
|
}
|
|
175
239
|
}
|
|
240
|
+
// Phase C.5 — record-ify exportables: walk only the components and menus
|
|
241
|
+
// sections in module-static scope (entry id known, but NO moduleEntry on the
|
|
242
|
+
// context — a _module.var at a structural position errors, because export ids
|
|
243
|
+
// must not vary per consumer). File refs are entered and static operators
|
|
244
|
+
// resolve; component bodies and menu links become records before any phase can
|
|
245
|
+
// consume them, so cross-entry consumption never races record creation.
|
|
246
|
+
async function recordifyExportables({ entryId, context }) {
|
|
247
|
+
const moduleEntry = context.modules[entryId];
|
|
248
|
+
const { manifest, packageRoot, moduleRoot, refDef } = moduleEntry;
|
|
249
|
+
const moduleYamlPath = path.join(moduleRoot, 'module.lowdefy.yaml');
|
|
250
|
+
if (type.isObject(manifest)) {
|
|
251
|
+
assertStaticComponentsList({
|
|
252
|
+
components: manifest.components,
|
|
253
|
+
entryId,
|
|
254
|
+
filePath: moduleYamlPath
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
const ctx = new WalkContext({
|
|
258
|
+
buildContext: context,
|
|
259
|
+
refId: refDef.id,
|
|
260
|
+
sourceRefId: null,
|
|
261
|
+
vars: {},
|
|
262
|
+
moduleRoot,
|
|
263
|
+
packageRoot,
|
|
264
|
+
entryId,
|
|
265
|
+
path: '',
|
|
266
|
+
currentFile: moduleYamlPath,
|
|
267
|
+
refChain: new Set(refDef.path ? [
|
|
268
|
+
refDef.path
|
|
269
|
+
] : []),
|
|
270
|
+
operators,
|
|
271
|
+
env: process.env,
|
|
272
|
+
lowdefyApp: context.appMeta,
|
|
273
|
+
dynamicIdentifiers,
|
|
274
|
+
shouldStop: makeShouldStop('exportables')
|
|
275
|
+
});
|
|
276
|
+
moduleEntry.manifest = await resolve(manifest, ctx);
|
|
277
|
+
}
|
|
278
|
+
// Phase D — manifest resolve: ONE full walk with the module entry in context.
|
|
279
|
+
// Pages, api, and connections resolve completely; component/menuLinks
|
|
280
|
+
// placeholders pass through the kind-aware dispatch untouched; varDefault
|
|
281
|
+
// placeholders keep their rule so the dispatch cannot force demand-only
|
|
282
|
+
// records.
|
|
176
283
|
async function resolveFullManifest({ entryId, context }) {
|
|
177
284
|
const moduleEntry = context.modules[entryId];
|
|
178
285
|
const { manifest, packageRoot, moduleRoot, moduleDependencies, refDef } = moduleEntry;
|
|
@@ -193,24 +300,18 @@ async function resolveFullManifest({ entryId, context }) {
|
|
|
193
300
|
] : []),
|
|
194
301
|
operators,
|
|
195
302
|
env: process.env,
|
|
303
|
+
lowdefyApp: context.appMeta,
|
|
196
304
|
dynamicIdentifiers,
|
|
197
|
-
shouldStop: (
|
|
198
|
-
if (/^vars(\.[^.]+\.properties)*\.[^.]+\.default(\..*)?$/.test(childPath)) return 'preserve';
|
|
199
|
-
if (/^components\.\d+\.component$/.test(childPath)) return 'preserve';
|
|
200
|
-
return false;
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
let resolved = await resolve(manifest, ctx);
|
|
204
|
-
resolved = evaluateStaticOperators({
|
|
205
|
-
context,
|
|
206
|
-
input: resolved,
|
|
207
|
-
refDef
|
|
305
|
+
shouldStop: makeShouldStop('manifest')
|
|
208
306
|
});
|
|
307
|
+
const resolved = await resolve(manifest, ctx);
|
|
209
308
|
// Filter null entries produced by _ref resolution failures
|
|
210
309
|
for (const key of [
|
|
211
310
|
'pages',
|
|
212
311
|
'connections',
|
|
213
|
-
'api'
|
|
312
|
+
'api',
|
|
313
|
+
'agents',
|
|
314
|
+
'notifications'
|
|
214
315
|
]){
|
|
215
316
|
if (type.isArray(resolved[key])) {
|
|
216
317
|
resolved[key] = resolved[key].filter((item)=>!type.isNone(item));
|
|
@@ -223,4 +324,4 @@ async function resolveFullManifest({ entryId, context }) {
|
|
|
223
324
|
validateVarTypes(varDefs, moduleEntry.resolvedVarCache, entryId, moduleEntry.source);
|
|
224
325
|
}
|
|
225
326
|
}
|
|
226
|
-
export { resolveLocalManifest, resolveFullManifest, validateRequiredVars };
|
|
327
|
+
export { resolveLocalManifest, recordifyExportables, resolveFullManifest, validateRequiredVars };
|
|
@@ -0,0 +1,58 @@
|
|
|
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, ConfigWarning } from '@lowdefy/errors';
|
|
17
|
+
function collectCallAgentSteps(routine, steps) {
|
|
18
|
+
if (type.isArray(routine)) {
|
|
19
|
+
routine.forEach((item)=>collectCallAgentSteps(item, steps));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (type.isObject(routine)) {
|
|
23
|
+
if (routine.type === 'CallAgent') {
|
|
24
|
+
steps.push(routine);
|
|
25
|
+
}
|
|
26
|
+
// Recurse into all values (handles control structures like :then, :else, :try, :catch)
|
|
27
|
+
Object.values(routine).forEach((value)=>collectCallAgentSteps(value, steps));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Runs after buildAgents (needs context.agentIds and normalized agent.tools).
|
|
31
|
+
// Validates that CallAgent steps with a static agentId reference an existing
|
|
32
|
+
// agent, and warns when that agent has confirm tools — headless runs have no
|
|
33
|
+
// client to resolve an approval, so those tools auto-execute. Operator
|
|
34
|
+
// (dynamic) agentIds are skipped; they resolve at runtime.
|
|
35
|
+
function validateCallAgentSteps({ components, context }) {
|
|
36
|
+
(components.api ?? []).forEach((endpoint)=>{
|
|
37
|
+
const steps = [];
|
|
38
|
+
collectCallAgentSteps(endpoint.routine, steps);
|
|
39
|
+
steps.forEach((step)=>{
|
|
40
|
+
const agentId = step.properties?.agentId;
|
|
41
|
+
if (!type.isString(agentId)) return;
|
|
42
|
+
if (!context.agentIds?.has(agentId)) {
|
|
43
|
+
throw new ConfigError(`CallAgent step "${step.stepId}" at endpoint "${endpoint.endpointId}" references agent "${agentId}" which does not exist.`, {
|
|
44
|
+
configKey: step['~k']
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const agent = (components.agents ?? []).find((a)=>a.agentId === agentId);
|
|
48
|
+
const hasConfirmTools = (agent?.tools ?? []).some((t)=>t.confirm);
|
|
49
|
+
if (hasConfirmTools) {
|
|
50
|
+
context.handleWarning(new ConfigWarning(`Agent "${agentId}" has tools with confirm: true, but tool approval is not supported when run from a CallAgent step. Tools will auto-execute.`, {
|
|
51
|
+
configKey: step['~k']
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
return components;
|
|
57
|
+
}
|
|
58
|
+
export default validateCallAgentSteps;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import validateCronConfig from './validateCronConfig.js';
|
|
16
17
|
function validateConfig({ components }) {
|
|
17
18
|
if (type.isNone(components.config)) {
|
|
18
19
|
components.config = {};
|
|
@@ -25,6 +26,9 @@ function validateConfig({ components }) {
|
|
|
25
26
|
throw new Error('Base path must start with "/".');
|
|
26
27
|
}
|
|
27
28
|
}
|
|
29
|
+
validateCronConfig({
|
|
30
|
+
components
|
|
31
|
+
});
|
|
28
32
|
return components;
|
|
29
33
|
}
|
|
30
34
|
export default validateConfig;
|
|
@@ -0,0 +1,110 @@
|
|
|
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 getCronEnvironmentNames from '../utils/getCronEnvironmentNames.js';
|
|
18
|
+
// Environment names become a path segment (/api/cron-forward/<environment>/<endpointId>).
|
|
19
|
+
const environmentNamePattern = /^[A-Za-z0-9\-_]+$/;
|
|
20
|
+
function isAbsoluteHttpUrl(value) {
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(value);
|
|
23
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Returns true when the environment is forwarded to (has a url), false for the host environment.
|
|
29
|
+
function validateCronEnvironment({ name, environment, configKey }) {
|
|
30
|
+
if (name === 'default') {
|
|
31
|
+
throw new ConfigError('Cron environment name "default" is reserved: it names the schedules every environment inherits.', {
|
|
32
|
+
configKey
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (!environmentNamePattern.test(name)) {
|
|
36
|
+
throw new ConfigError(`Cron environment name "${name}" is invalid. Environment names must only contain A-Z, a-z, 0-9, "-" and "_".`, {
|
|
37
|
+
configKey
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (!type.isObject(environment)) {
|
|
41
|
+
throw new ConfigError(`Cron environment "${name}" is not an object.`, {
|
|
42
|
+
received: environment,
|
|
43
|
+
configKey
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (!type.isUndefined(environment.enabled) && !type.isBoolean(environment.enabled)) {
|
|
47
|
+
throw new ConfigError(`Cron environment "${name}" enabled is not a boolean.`, {
|
|
48
|
+
received: environment.enabled,
|
|
49
|
+
configKey: environment['~k'] ?? configKey
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (type.isUndefined(environment.url)) {
|
|
53
|
+
if (!type.isUndefined(environment.secret)) {
|
|
54
|
+
throw new ConfigError(`Cron environment "${name}" has a secret but no url. The environment without a url is the deployment that runs the crons and needs no secret.`, {
|
|
55
|
+
configKey: environment['~k'] ?? configKey
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (!type.isString(environment.url) || !isAbsoluteHttpUrl(environment.url)) {
|
|
61
|
+
throw new ConfigError(`Cron environment "${name}" url is not an absolute http(s) URL, e.g. "https://staging.example.com".`, {
|
|
62
|
+
received: environment.url,
|
|
63
|
+
configKey: environment['~k'] ?? configKey
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (!type.isString(environment.secret) || environment.secret === '') {
|
|
67
|
+
throw new ConfigError(`Cron environment "${name}" has a url but no secret. Set secret to the Lowdefy secret name that holds the environment's CRON_SECRET.`, {
|
|
68
|
+
received: environment.secret,
|
|
69
|
+
configKey: environment['~k'] ?? configKey
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
// Validates config.cron: the deployment environments scheduled endpoints run in. Exactly one
|
|
75
|
+
// environment has no url — the deployment whose crons Vercel fires — and every other environment
|
|
76
|
+
// is forwarded to from there, so it needs a url and the secret that authorizes the forwarded call.
|
|
77
|
+
function validateCronConfig({ components }) {
|
|
78
|
+
const cron = components.config.cron;
|
|
79
|
+
if (type.isUndefined(cron)) return;
|
|
80
|
+
const configKey = cron['~k'] ?? components.config['~k'];
|
|
81
|
+
if (!type.isObject(cron)) {
|
|
82
|
+
throw new ConfigError('lowdefy.config.cron is not an object.', {
|
|
83
|
+
received: cron,
|
|
84
|
+
configKey
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (!type.isObject(cron.environments)) {
|
|
88
|
+
throw new ConfigError('lowdefy.config.cron.environments is not an object.', {
|
|
89
|
+
received: cron.environments,
|
|
90
|
+
configKey
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const names = getCronEnvironmentNames(cron.environments);
|
|
94
|
+
if (names.length === 0) {
|
|
95
|
+
throw new ConfigError('lowdefy.config.cron.environments declares no environments.', {
|
|
96
|
+
configKey
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const hosts = names.filter((name)=>!validateCronEnvironment({
|
|
100
|
+
name,
|
|
101
|
+
environment: cron.environments[name],
|
|
102
|
+
configKey
|
|
103
|
+
}));
|
|
104
|
+
if (hosts.length !== 1) {
|
|
105
|
+
throw new ConfigError(`Exactly one environment in lowdefy.config.cron.environments must have no url (the deployment that runs the crons). Received ${JSON.stringify(hosts)}.`, {
|
|
106
|
+
configKey
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export default validateCronConfig;
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
function collectRenderNotificationSteps(routine, steps) {
|
|
18
|
+
if (type.isArray(routine)) {
|
|
19
|
+
routine.forEach((item)=>collectRenderNotificationSteps(item, steps));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (type.isObject(routine)) {
|
|
23
|
+
if (routine.type === 'RenderNotification') {
|
|
24
|
+
steps.push(routine);
|
|
25
|
+
}
|
|
26
|
+
// Recurse into all values (handles control structures like :then, :else, :try, :catch)
|
|
27
|
+
Object.values(routine).forEach((value)=>collectRenderNotificationSteps(value, steps));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Runs after buildNotifications (needs context.notificationIds). Validates that
|
|
31
|
+
// RenderNotification steps with a static notificationId reference an existing
|
|
32
|
+
// notification. Operator (dynamic) notificationIds are skipped; they resolve at
|
|
33
|
+
// runtime.
|
|
34
|
+
function validateRenderNotificationSteps({ components, context }) {
|
|
35
|
+
(components.api ?? []).forEach((endpoint)=>{
|
|
36
|
+
const steps = [];
|
|
37
|
+
collectRenderNotificationSteps(endpoint.routine, steps);
|
|
38
|
+
steps.forEach((step)=>{
|
|
39
|
+
const notificationId = step.properties?.notificationId;
|
|
40
|
+
if (!type.isString(notificationId)) return;
|
|
41
|
+
if (!context.notificationIds?.has(notificationId)) {
|
|
42
|
+
throw new ConfigError(`RenderNotification step "${step.stepId}" at endpoint "${endpoint.endpointId}" references notification "${notificationId}" which does not exist.`, {
|
|
43
|
+
configKey: step['~k']
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
return components;
|
|
49
|
+
}
|
|
50
|
+
export default validateRenderNotificationSteps;
|
package/dist/build/writeApi.js
CHANGED
|
@@ -13,9 +13,62 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { type, serializer } from '@lowdefy/helpers';
|
|
16
|
+
import getCronEnvironmentNames from '../utils/getCronEnvironmentNames.js';
|
|
16
17
|
async function writeEndpoint({ endpoint, context }) {
|
|
17
18
|
await context.writeBuildArtifact(`api/${endpoint.endpointId}.json`, serializer.serializeToString(endpoint ?? {}));
|
|
18
19
|
}
|
|
20
|
+
// Flat manifest of every scheduled endpoint, consumed by the Vercel Build Output assembly to
|
|
21
|
+
// generate the `crons` array in config.json. The runtime reads schedules off the endpoint artifact
|
|
22
|
+
// directly, so it does not depend on this file.
|
|
23
|
+
//
|
|
24
|
+
// With config.cron.environments declared there is one entry per enabled environment and schedule:
|
|
25
|
+
// `environment` names it and `forward` is true for environments the production deployment forwards
|
|
26
|
+
// to (those with a url) — Vercel fires crons only on production, so every environment's schedules
|
|
27
|
+
// must be registered there.
|
|
28
|
+
function getSchedules({ endpoint, environment }) {
|
|
29
|
+
if (type.isArray(endpoint.schedules)) return endpoint.schedules;
|
|
30
|
+
return endpoint.schedules?.[environment] ?? [];
|
|
31
|
+
}
|
|
32
|
+
function pushSchedules({ endpoint, environment, forward }) {
|
|
33
|
+
return getSchedules({
|
|
34
|
+
endpoint,
|
|
35
|
+
environment
|
|
36
|
+
}).map((schedule)=>({
|
|
37
|
+
endpointId: endpoint.endpointId,
|
|
38
|
+
cron: schedule.cron,
|
|
39
|
+
payload: schedule.payload ?? {},
|
|
40
|
+
...type.isUndefined(environment) ? {} : {
|
|
41
|
+
environment,
|
|
42
|
+
forward
|
|
43
|
+
}
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
async function writeSchedulesManifest({ components, context }) {
|
|
47
|
+
const cronEnvironments = components.config?.cron?.environments;
|
|
48
|
+
const environmentNames = getCronEnvironmentNames(cronEnvironments);
|
|
49
|
+
const schedules = [];
|
|
50
|
+
(components.api ?? []).forEach((endpoint)=>{
|
|
51
|
+
if (environmentNames.length === 0) {
|
|
52
|
+
schedules.push(...pushSchedules({
|
|
53
|
+
endpoint
|
|
54
|
+
}));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
environmentNames.forEach((environment)=>{
|
|
58
|
+
const config = cronEnvironments[environment];
|
|
59
|
+
if (config.enabled === false) return;
|
|
60
|
+
schedules.push(...pushSchedules({
|
|
61
|
+
endpoint,
|
|
62
|
+
environment,
|
|
63
|
+
forward: !type.isUndefined(config.url)
|
|
64
|
+
}));
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
// Only emit the manifest when something is scheduled; the Vercel assembly treats a missing
|
|
68
|
+
// schedules.json as "no crons".
|
|
69
|
+
if (schedules.length === 0) return;
|
|
70
|
+
await context.writeBuildArtifact('schedules.json', serializer.serializeToString(schedules));
|
|
71
|
+
}
|
|
19
72
|
async function writeApi({ components, context }) {
|
|
20
73
|
if (type.isNone(components.api)) return;
|
|
21
74
|
if (!type.isArray(components.api)) {
|
|
@@ -25,6 +78,10 @@ async function writeApi({ components, context }) {
|
|
|
25
78
|
endpoint,
|
|
26
79
|
context
|
|
27
80
|
}));
|
|
28
|
-
|
|
81
|
+
await Promise.all(writePromises);
|
|
82
|
+
await writeSchedulesManifest({
|
|
83
|
+
components,
|
|
84
|
+
context
|
|
85
|
+
});
|
|
29
86
|
}
|
|
30
87
|
export default writeApi;
|
|
@@ -0,0 +1,21 @@
|
|
|
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 { serializer } from '@lowdefy/helpers';
|
|
16
|
+
// Always written, even unconfigured - the runtime reads mcp.configured and
|
|
17
|
+
// never needs an existence check on the artifact.
|
|
18
|
+
async function writeMcp({ components, context }) {
|
|
19
|
+
await context.writeBuildArtifact('mcp.json', serializer.serializeToString(components.mcp));
|
|
20
|
+
}
|
|
21
|
+
export default writeMcp;
|
|
@@ -0,0 +1,30 @@
|
|
|
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, serializer } from '@lowdefy/helpers';
|
|
16
|
+
async function writeNotification({ notification, context }) {
|
|
17
|
+
await context.writeBuildArtifact(`notifications/${notification.notificationId}.json`, serializer.serializeToString(notification ?? {}));
|
|
18
|
+
}
|
|
19
|
+
async function writeNotifications({ components, context }) {
|
|
20
|
+
if (type.isNone(components.notifications)) return;
|
|
21
|
+
if (!type.isArray(components.notifications)) {
|
|
22
|
+
throw new Error(`Notifications is not an array.`);
|
|
23
|
+
}
|
|
24
|
+
const writePromises = components.notifications.map((notification)=>writeNotification({
|
|
25
|
+
notification,
|
|
26
|
+
context
|
|
27
|
+
}));
|
|
28
|
+
return Promise.all(writePromises);
|
|
29
|
+
}
|
|
30
|
+
export default writeNotifications;
|
|
@@ -0,0 +1,39 @@
|
|
|
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 { createRequire } from 'node:module';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
// Import a plugin module (e.g. `${pkg}/schemas`, `${pkg}/connections`) for
|
|
18
|
+
// schema collection. Default packages resolve from the build package itself;
|
|
19
|
+
// custom plugins only exist in the server's node_modules, so fall back to
|
|
20
|
+
// resolving from the server directory. Returns undefined when unresolvable —
|
|
21
|
+
// callers degrade gracefully.
|
|
22
|
+
async function importPluginModule({ context, specifier }) {
|
|
23
|
+
try {
|
|
24
|
+
return await import(/* webpackIgnore: true */ /* @vite-ignore */ specifier);
|
|
25
|
+
} catch {
|
|
26
|
+
// Not resolvable from the build package — try the server's node_modules.
|
|
27
|
+
}
|
|
28
|
+
const serverDir = context.directories?.server;
|
|
29
|
+
if (!serverDir) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const require = createRequire(path.join(serverDir, 'package.json'));
|
|
34
|
+
return await import(/* webpackIgnore: true */ /* @vite-ignore */ require.resolve(specifier));
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export default importPluginModule;
|
|
@@ -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
|
-
*/
|
|
15
|
+
*/ import importPluginModule from './importPluginModule.js';
|
|
16
|
+
async function writeActionSchemaMap({ components, context }) {
|
|
16
17
|
const schemas = {};
|
|
17
18
|
const typesMapSchemas = context.typesMap.schemas?.actions ?? {};
|
|
18
19
|
const actionsByPackage = {};
|
|
@@ -23,12 +24,10 @@
|
|
|
23
24
|
actionsByPackage[action.package].push(action);
|
|
24
25
|
}
|
|
25
26
|
for (const [packageName, actions] of Object.entries(actionsByPackage)){
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
// Package not resolvable from build context (custom plugins) — skip
|
|
31
|
-
}
|
|
27
|
+
const packageSchemas = await importPluginModule({
|
|
28
|
+
context,
|
|
29
|
+
specifier: `${packageName}/schemas`
|
|
30
|
+
});
|
|
32
31
|
for (const action of actions){
|
|
33
32
|
if (typesMapSchemas[action.typeName]) {
|
|
34
33
|
schemas[action.typeName] = typesMapSchemas[action.typeName];
|
|
@@ -30,5 +30,9 @@ async function writeAuthImports({ components, context }) {
|
|
|
30
30
|
imports: components.imports.auth.providers,
|
|
31
31
|
importPath: 'auth/providers'
|
|
32
32
|
}));
|
|
33
|
+
await context.writeBuildArtifact('plugins/auth/strategies.js', generateImportFile({
|
|
34
|
+
imports: components.imports.auth.strategies,
|
|
35
|
+
importPath: 'auth/strategies'
|
|
36
|
+
}));
|
|
33
37
|
}
|
|
34
38
|
export default writeAuthImports;
|