@bsm-form/core 0.39.0 → 0.40.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/index.cjs +166 -97
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +165 -97
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -38,6 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
runConditionEngine: () => runConditionEngine,
|
|
39
39
|
tagMapKey: () => tagMapKey,
|
|
40
40
|
traverseNode: () => traverseNode,
|
|
41
|
+
validateApiUrlTemplate: () => validateApiUrlTemplate,
|
|
41
42
|
validateField: () => validateField,
|
|
42
43
|
validateSchema: () => validateSchema,
|
|
43
44
|
validation: () => validation,
|
|
@@ -45,6 +46,144 @@ __export(index_exports, {
|
|
|
45
46
|
});
|
|
46
47
|
module.exports = __toCommonJS(index_exports);
|
|
47
48
|
|
|
49
|
+
// src/utils/get-path.ts
|
|
50
|
+
function pathSegments(path) {
|
|
51
|
+
const segments = [];
|
|
52
|
+
let i = 0;
|
|
53
|
+
while (i < path.length) {
|
|
54
|
+
const char = path[i];
|
|
55
|
+
if (char === ".") {
|
|
56
|
+
i += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (char === "[") {
|
|
60
|
+
const close = path.indexOf("]", i);
|
|
61
|
+
if (close === -1) {
|
|
62
|
+
segments.push(path.slice(i));
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
segments.push(path.slice(i + 1, close));
|
|
66
|
+
i = close + 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
let end = i;
|
|
70
|
+
while (end < path.length && path[end] !== "." && path[end] !== "[") {
|
|
71
|
+
end += 1;
|
|
72
|
+
}
|
|
73
|
+
if (end > i) {
|
|
74
|
+
segments.push(path.slice(i, end));
|
|
75
|
+
}
|
|
76
|
+
i = end;
|
|
77
|
+
}
|
|
78
|
+
return segments;
|
|
79
|
+
}
|
|
80
|
+
function getPath(obj, path) {
|
|
81
|
+
if (!path) {
|
|
82
|
+
return obj;
|
|
83
|
+
}
|
|
84
|
+
let current = obj;
|
|
85
|
+
for (const key of pathSegments(path)) {
|
|
86
|
+
if (typeof current !== "object" || current === null) {
|
|
87
|
+
return void 0;
|
|
88
|
+
}
|
|
89
|
+
current = current[key];
|
|
90
|
+
}
|
|
91
|
+
return current;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/utils/scope-path.ts
|
|
95
|
+
function resolveScopedPath(ctx, path) {
|
|
96
|
+
if (path === "$values" || path === "values") {
|
|
97
|
+
return ctx.values;
|
|
98
|
+
}
|
|
99
|
+
if (path === "$resources" || path === "resources") {
|
|
100
|
+
return ctx.resources;
|
|
101
|
+
}
|
|
102
|
+
if (path === "$row" || path === "row") {
|
|
103
|
+
return ctx.row;
|
|
104
|
+
}
|
|
105
|
+
if (path === "$item" || path === "item") {
|
|
106
|
+
return ctx.item;
|
|
107
|
+
}
|
|
108
|
+
if (path.startsWith("values.")) {
|
|
109
|
+
return getPath(ctx.values, path.replace("values.", ""));
|
|
110
|
+
}
|
|
111
|
+
if (path.startsWith("resources.")) {
|
|
112
|
+
return getPath(ctx.resources, path.replace("resources.", ""));
|
|
113
|
+
}
|
|
114
|
+
if (path.startsWith("row.")) {
|
|
115
|
+
return ctx.row === void 0 ? void 0 : getPath(ctx.row, path.replace("row.", ""));
|
|
116
|
+
}
|
|
117
|
+
if (path.startsWith("item.")) {
|
|
118
|
+
return ctx.item === void 0 ? void 0 : getPath(ctx.item, path.replace("item.", ""));
|
|
119
|
+
}
|
|
120
|
+
return getPath(ctx.values, path);
|
|
121
|
+
}
|
|
122
|
+
function isExplicitScopedPath(path) {
|
|
123
|
+
return path === "$values" || path === "values" || path === "$resources" || path === "resources" || path === "$row" || path === "row" || path === "$item" || path === "item" || path.startsWith("values.") || path.startsWith("resources.") || path.startsWith("row.") || path.startsWith("item.");
|
|
124
|
+
}
|
|
125
|
+
function resolvePathStringsInData(value, ctx) {
|
|
126
|
+
if (typeof value === "string") {
|
|
127
|
+
return isExplicitScopedPath(value) ? resolveScopedPath(ctx, value) : value;
|
|
128
|
+
}
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
return value.map((item) => resolvePathStringsInData(item, ctx));
|
|
131
|
+
}
|
|
132
|
+
if (typeof value === "object" && value !== null) {
|
|
133
|
+
const result = {};
|
|
134
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
135
|
+
result[key] = resolvePathStringsInData(entry, ctx);
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/utils/url-template.ts
|
|
143
|
+
var pathPattern = /^(values|resources)\.[A-Za-z_$][\w$]*(?:(?:\.(?:[A-Za-z_$][\w$]*|\d+))|(?:\[\d+\]))*$/;
|
|
144
|
+
function placeholders(url) {
|
|
145
|
+
const result = [];
|
|
146
|
+
let cursor = 0;
|
|
147
|
+
while (true) {
|
|
148
|
+
const start = url.indexOf("${", cursor);
|
|
149
|
+
if (start < 0) break;
|
|
150
|
+
const close = url.indexOf("}", start + 2);
|
|
151
|
+
if (close < 0) throw new Error("API URL template has an unclosed placeholder");
|
|
152
|
+
const path = url.slice(start + 2, close).trim();
|
|
153
|
+
if (!pathPattern.test(path)) {
|
|
154
|
+
throw new Error(`Invalid API URL placeholder: ${path}. Use a values.* or resources.* data path`);
|
|
155
|
+
}
|
|
156
|
+
const origin = url.match(/^(?:[A-Za-z][A-Za-z0-9+.-]*:)?\/\/[^/?#]*/)?.[0];
|
|
157
|
+
if (start === 0 || origin && start < origin.length) {
|
|
158
|
+
throw new Error("API URL placeholders cannot replace the service origin; use a static base URL");
|
|
159
|
+
}
|
|
160
|
+
result.push({ start, end: close + 1, path });
|
|
161
|
+
cursor = close + 1;
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
function validateApiUrlTemplate(url) {
|
|
166
|
+
if (!url.trim()) throw new Error("API URL must not be empty");
|
|
167
|
+
placeholders(url);
|
|
168
|
+
}
|
|
169
|
+
function resolveApiUrlTemplate(url, context) {
|
|
170
|
+
validateApiUrlTemplate(url);
|
|
171
|
+
let result = "";
|
|
172
|
+
let cursor = 0;
|
|
173
|
+
for (const placeholder of placeholders(url)) {
|
|
174
|
+
const value = resolveScopedPath(context, placeholder.path);
|
|
175
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean" || typeof value === "number" && !Number.isFinite(value) || value === "") {
|
|
176
|
+
throw new Error(`API URL placeholder ${placeholder.path} must resolve to a non-empty string, finite number, or boolean`);
|
|
177
|
+
}
|
|
178
|
+
if (value === "." || value === "..") {
|
|
179
|
+
throw new Error(`API URL placeholder ${placeholder.path} cannot be a dot segment`);
|
|
180
|
+
}
|
|
181
|
+
result += url.slice(cursor, placeholder.start) + encodeURIComponent(String(value));
|
|
182
|
+
cursor = placeholder.end;
|
|
183
|
+
}
|
|
184
|
+
return result + url.slice(cursor);
|
|
185
|
+
}
|
|
186
|
+
|
|
48
187
|
// src/compiler/compile-schema.ts
|
|
49
188
|
var import_schema = require("@bsm-form/schema");
|
|
50
189
|
|
|
@@ -1347,7 +1486,25 @@ function collectDrawerSteps(steps, path, context, owningDrawerId) {
|
|
|
1347
1486
|
});
|
|
1348
1487
|
});
|
|
1349
1488
|
}
|
|
1489
|
+
function validateApiUrlsInSteps(steps, path) {
|
|
1490
|
+
steps.forEach((step, index) => {
|
|
1491
|
+
const stepPath = `${path}[${index}]`;
|
|
1492
|
+
if (step.type === "when") {
|
|
1493
|
+
validateApiUrlsInSteps(step.then, `${stepPath}.then`);
|
|
1494
|
+
if (step.else) validateApiUrlsInSteps(step.else, `${stepPath}.else`);
|
|
1495
|
+
}
|
|
1496
|
+
if (step.type === "api") {
|
|
1497
|
+
try {
|
|
1498
|
+
validateApiUrlTemplate(step.request.url);
|
|
1499
|
+
} catch (error) {
|
|
1500
|
+
throw new Error(`Invalid API URL at ${stepPath}.request.url: ${error instanceof Error ? error.message : String(error)}`);
|
|
1501
|
+
}
|
|
1502
|
+
if (step.onError) validateApiUrlsInSteps(step.onError, `${stepPath}.onError`);
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1350
1506
|
function collectDialogSteps(steps, path, context, owningDialogId) {
|
|
1507
|
+
validateApiUrlsInSteps(steps, path);
|
|
1351
1508
|
steps.forEach((step, index) => {
|
|
1352
1509
|
if (step.type !== "dialog") {
|
|
1353
1510
|
return;
|
|
@@ -1759,99 +1916,6 @@ function isResourcePathInFlight(sourceRelative, inFlight) {
|
|
|
1759
1916
|
);
|
|
1760
1917
|
}
|
|
1761
1918
|
|
|
1762
|
-
// src/utils/get-path.ts
|
|
1763
|
-
function pathSegments(path) {
|
|
1764
|
-
const segments = [];
|
|
1765
|
-
let i = 0;
|
|
1766
|
-
while (i < path.length) {
|
|
1767
|
-
const char = path[i];
|
|
1768
|
-
if (char === ".") {
|
|
1769
|
-
i += 1;
|
|
1770
|
-
continue;
|
|
1771
|
-
}
|
|
1772
|
-
if (char === "[") {
|
|
1773
|
-
const close = path.indexOf("]", i);
|
|
1774
|
-
if (close === -1) {
|
|
1775
|
-
segments.push(path.slice(i));
|
|
1776
|
-
break;
|
|
1777
|
-
}
|
|
1778
|
-
segments.push(path.slice(i + 1, close));
|
|
1779
|
-
i = close + 1;
|
|
1780
|
-
continue;
|
|
1781
|
-
}
|
|
1782
|
-
let end = i;
|
|
1783
|
-
while (end < path.length && path[end] !== "." && path[end] !== "[") {
|
|
1784
|
-
end += 1;
|
|
1785
|
-
}
|
|
1786
|
-
if (end > i) {
|
|
1787
|
-
segments.push(path.slice(i, end));
|
|
1788
|
-
}
|
|
1789
|
-
i = end;
|
|
1790
|
-
}
|
|
1791
|
-
return segments;
|
|
1792
|
-
}
|
|
1793
|
-
function getPath(obj, path) {
|
|
1794
|
-
if (!path) {
|
|
1795
|
-
return obj;
|
|
1796
|
-
}
|
|
1797
|
-
let current = obj;
|
|
1798
|
-
for (const key of pathSegments(path)) {
|
|
1799
|
-
if (typeof current !== "object" || current === null) {
|
|
1800
|
-
return void 0;
|
|
1801
|
-
}
|
|
1802
|
-
current = current[key];
|
|
1803
|
-
}
|
|
1804
|
-
return current;
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
// src/utils/scope-path.ts
|
|
1808
|
-
function resolveScopedPath(ctx, path) {
|
|
1809
|
-
if (path === "$values" || path === "values") {
|
|
1810
|
-
return ctx.values;
|
|
1811
|
-
}
|
|
1812
|
-
if (path === "$resources" || path === "resources") {
|
|
1813
|
-
return ctx.resources;
|
|
1814
|
-
}
|
|
1815
|
-
if (path === "$row" || path === "row") {
|
|
1816
|
-
return ctx.row;
|
|
1817
|
-
}
|
|
1818
|
-
if (path === "$item" || path === "item") {
|
|
1819
|
-
return ctx.item;
|
|
1820
|
-
}
|
|
1821
|
-
if (path.startsWith("values.")) {
|
|
1822
|
-
return getPath(ctx.values, path.replace("values.", ""));
|
|
1823
|
-
}
|
|
1824
|
-
if (path.startsWith("resources.")) {
|
|
1825
|
-
return getPath(ctx.resources, path.replace("resources.", ""));
|
|
1826
|
-
}
|
|
1827
|
-
if (path.startsWith("row.")) {
|
|
1828
|
-
return ctx.row === void 0 ? void 0 : getPath(ctx.row, path.replace("row.", ""));
|
|
1829
|
-
}
|
|
1830
|
-
if (path.startsWith("item.")) {
|
|
1831
|
-
return ctx.item === void 0 ? void 0 : getPath(ctx.item, path.replace("item.", ""));
|
|
1832
|
-
}
|
|
1833
|
-
return getPath(ctx.values, path);
|
|
1834
|
-
}
|
|
1835
|
-
function isExplicitScopedPath(path) {
|
|
1836
|
-
return path === "$values" || path === "values" || path === "$resources" || path === "resources" || path === "$row" || path === "row" || path === "$item" || path === "item" || path.startsWith("values.") || path.startsWith("resources.") || path.startsWith("row.") || path.startsWith("item.");
|
|
1837
|
-
}
|
|
1838
|
-
function resolvePathStringsInData(value, ctx) {
|
|
1839
|
-
if (typeof value === "string") {
|
|
1840
|
-
return isExplicitScopedPath(value) ? resolveScopedPath(ctx, value) : value;
|
|
1841
|
-
}
|
|
1842
|
-
if (Array.isArray(value)) {
|
|
1843
|
-
return value.map((item) => resolvePathStringsInData(item, ctx));
|
|
1844
|
-
}
|
|
1845
|
-
if (typeof value === "object" && value !== null) {
|
|
1846
|
-
const result = {};
|
|
1847
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
1848
|
-
result[key] = resolvePathStringsInData(entry, ctx);
|
|
1849
|
-
}
|
|
1850
|
-
return result;
|
|
1851
|
-
}
|
|
1852
|
-
return value;
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
1919
|
// src/conditions/evaluate.ts
|
|
1856
1920
|
function evaluateCondition(condition, values, state, options) {
|
|
1857
1921
|
if ((0, import_schema2.isInvokeCondition)(condition)) {
|
|
@@ -2888,6 +2952,7 @@ function resolveApiRequestUrl(step, engine, options) {
|
|
|
2888
2952
|
...options?.row !== void 0 ? { row: options.row } : {},
|
|
2889
2953
|
...options?.item !== void 0 ? { item: options.item } : {}
|
|
2890
2954
|
};
|
|
2955
|
+
const url = resolveApiUrlTemplate(step.request.url, ctx);
|
|
2891
2956
|
const hasQueryFrom = "queryFrom" in step.request && step.request.queryFrom !== void 0;
|
|
2892
2957
|
const hasQuery = "query" in step.request && step.request.query !== void 0;
|
|
2893
2958
|
if (hasQueryFrom && hasQuery) {
|
|
@@ -2903,7 +2968,7 @@ function resolveApiRequestUrl(step, engine, options) {
|
|
|
2903
2968
|
} else if (hasQuery) {
|
|
2904
2969
|
queryValue = resolvePathStringsInData(step.request.query, ctx);
|
|
2905
2970
|
} else {
|
|
2906
|
-
return
|
|
2971
|
+
return url;
|
|
2907
2972
|
}
|
|
2908
2973
|
if (queryValue === void 0 || queryValue === null || typeof queryValue !== "object" || Array.isArray(queryValue)) {
|
|
2909
2974
|
throw new TypeError("api step query must resolve to an object");
|
|
@@ -2917,10 +2982,13 @@ function resolveApiRequestUrl(step, engine, options) {
|
|
|
2917
2982
|
}
|
|
2918
2983
|
const queryString = params.toString();
|
|
2919
2984
|
if (!queryString) {
|
|
2920
|
-
return
|
|
2985
|
+
return url;
|
|
2921
2986
|
}
|
|
2922
|
-
const
|
|
2923
|
-
|
|
2987
|
+
const hashIndex = url.indexOf("#");
|
|
2988
|
+
const base = hashIndex < 0 ? url : url.slice(0, hashIndex);
|
|
2989
|
+
const hash = hashIndex < 0 ? "" : url.slice(hashIndex);
|
|
2990
|
+
const separator = base.includes("?") ? "&" : "?";
|
|
2991
|
+
return `${base}${separator}${queryString}${hash}`;
|
|
2924
2992
|
}
|
|
2925
2993
|
function toRequestBody(value, headers) {
|
|
2926
2994
|
if (isBodyInit(value)) {
|
|
@@ -5214,6 +5282,7 @@ function findNode(node, name) {
|
|
|
5214
5282
|
runConditionEngine,
|
|
5215
5283
|
tagMapKey,
|
|
5216
5284
|
traverseNode,
|
|
5285
|
+
validateApiUrlTemplate,
|
|
5217
5286
|
validateField,
|
|
5218
5287
|
validateSchema,
|
|
5219
5288
|
validation,
|
package/dist/index.d.cts
CHANGED
|
@@ -588,4 +588,7 @@ declare function resolveTagValueMapping(value: unknown, map: Record<string, Tabl
|
|
|
588
588
|
declare function tagMapKey(value: unknown): string | undefined;
|
|
589
589
|
declare function formatTagSourceValue(value: unknown): string;
|
|
590
590
|
|
|
591
|
-
|
|
591
|
+
/** Validates syntax without reading values or running author code. */
|
|
592
|
+
declare function validateApiUrlTemplate(url: string): void;
|
|
593
|
+
|
|
594
|
+
export { type AccordionChangeReason, type AccordionChangeResult, type AccordionState, type ActionAdapterInput, type ActionContext, type ActionHandler, type ComputerHandler, DependencyGraph, type DependencyMap, type DialogState, type DialogTransitionReason, type DialogTransitionResult, type DisplayAdapterInput, type DrawerState, type DrawerTransitionReason, type DrawerTransitionResult, EventBus, type FieldAdapterInput, FormEngine, type FormEngineConfig, type FormEngineEnvironment, type FormEngineNotification, type FormEngineState, type FormErrors, type FormEvent, type FormSnapshot, type FormState, type FormValues, type InitializationStatus, type InvokeContext, type InvokeHandler, type LayoutAdapterInput, type Listener, type OptionsConfig, type PaginationChangeReason, type PaginationChangeResult, type PaginationState, type PipelineContext, type PredicateHandler, type RepeatMutationReason, type RepeatMutationResult, type RuntimeNode, type RuntimeNodeState, type RuntimeTree, type StepRunOptions, type StepperNavigationReason, type StepperNavigationResult, type StepperState, type TabsChangeReason, type TabsChangeResult, type TabsState, type ValidateFieldOptions, type ValidationResult, type Validator, type ValidatorHandler, adaptAction, adaptDisplay, adaptField, adaptLayout, compileSchema, conditions, createFormEngineEnvironment, createRuntimeNode, formatTagSourceValue, isThenable, resolveLoadingProp, resolveTagValueMapping, runConditionEngine, tagMapKey, traverseNode, validateApiUrlTemplate, validateField, validateSchema, validation, validationRegistry };
|
package/dist/index.d.ts
CHANGED
|
@@ -588,4 +588,7 @@ declare function resolveTagValueMapping(value: unknown, map: Record<string, Tabl
|
|
|
588
588
|
declare function tagMapKey(value: unknown): string | undefined;
|
|
589
589
|
declare function formatTagSourceValue(value: unknown): string;
|
|
590
590
|
|
|
591
|
-
|
|
591
|
+
/** Validates syntax without reading values or running author code. */
|
|
592
|
+
declare function validateApiUrlTemplate(url: string): void;
|
|
593
|
+
|
|
594
|
+
export { type AccordionChangeReason, type AccordionChangeResult, type AccordionState, type ActionAdapterInput, type ActionContext, type ActionHandler, type ComputerHandler, DependencyGraph, type DependencyMap, type DialogState, type DialogTransitionReason, type DialogTransitionResult, type DisplayAdapterInput, type DrawerState, type DrawerTransitionReason, type DrawerTransitionResult, EventBus, type FieldAdapterInput, FormEngine, type FormEngineConfig, type FormEngineEnvironment, type FormEngineNotification, type FormEngineState, type FormErrors, type FormEvent, type FormSnapshot, type FormState, type FormValues, type InitializationStatus, type InvokeContext, type InvokeHandler, type LayoutAdapterInput, type Listener, type OptionsConfig, type PaginationChangeReason, type PaginationChangeResult, type PaginationState, type PipelineContext, type PredicateHandler, type RepeatMutationReason, type RepeatMutationResult, type RuntimeNode, type RuntimeNodeState, type RuntimeTree, type StepRunOptions, type StepperNavigationReason, type StepperNavigationResult, type StepperState, type TabsChangeReason, type TabsChangeResult, type TabsState, type ValidateFieldOptions, type ValidationResult, type Validator, type ValidatorHandler, adaptAction, adaptDisplay, adaptField, adaptLayout, compileSchema, conditions, createFormEngineEnvironment, createRuntimeNode, formatTagSourceValue, isThenable, resolveLoadingProp, resolveTagValueMapping, runConditionEngine, tagMapKey, traverseNode, validateApiUrlTemplate, validateField, validateSchema, validation, validationRegistry };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,141 @@
|
|
|
1
|
+
// src/utils/get-path.ts
|
|
2
|
+
function pathSegments(path) {
|
|
3
|
+
const segments = [];
|
|
4
|
+
let i = 0;
|
|
5
|
+
while (i < path.length) {
|
|
6
|
+
const char = path[i];
|
|
7
|
+
if (char === ".") {
|
|
8
|
+
i += 1;
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (char === "[") {
|
|
12
|
+
const close = path.indexOf("]", i);
|
|
13
|
+
if (close === -1) {
|
|
14
|
+
segments.push(path.slice(i));
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
segments.push(path.slice(i + 1, close));
|
|
18
|
+
i = close + 1;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
let end = i;
|
|
22
|
+
while (end < path.length && path[end] !== "." && path[end] !== "[") {
|
|
23
|
+
end += 1;
|
|
24
|
+
}
|
|
25
|
+
if (end > i) {
|
|
26
|
+
segments.push(path.slice(i, end));
|
|
27
|
+
}
|
|
28
|
+
i = end;
|
|
29
|
+
}
|
|
30
|
+
return segments;
|
|
31
|
+
}
|
|
32
|
+
function getPath(obj, path) {
|
|
33
|
+
if (!path) {
|
|
34
|
+
return obj;
|
|
35
|
+
}
|
|
36
|
+
let current = obj;
|
|
37
|
+
for (const key of pathSegments(path)) {
|
|
38
|
+
if (typeof current !== "object" || current === null) {
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
current = current[key];
|
|
42
|
+
}
|
|
43
|
+
return current;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/utils/scope-path.ts
|
|
47
|
+
function resolveScopedPath(ctx, path) {
|
|
48
|
+
if (path === "$values" || path === "values") {
|
|
49
|
+
return ctx.values;
|
|
50
|
+
}
|
|
51
|
+
if (path === "$resources" || path === "resources") {
|
|
52
|
+
return ctx.resources;
|
|
53
|
+
}
|
|
54
|
+
if (path === "$row" || path === "row") {
|
|
55
|
+
return ctx.row;
|
|
56
|
+
}
|
|
57
|
+
if (path === "$item" || path === "item") {
|
|
58
|
+
return ctx.item;
|
|
59
|
+
}
|
|
60
|
+
if (path.startsWith("values.")) {
|
|
61
|
+
return getPath(ctx.values, path.replace("values.", ""));
|
|
62
|
+
}
|
|
63
|
+
if (path.startsWith("resources.")) {
|
|
64
|
+
return getPath(ctx.resources, path.replace("resources.", ""));
|
|
65
|
+
}
|
|
66
|
+
if (path.startsWith("row.")) {
|
|
67
|
+
return ctx.row === void 0 ? void 0 : getPath(ctx.row, path.replace("row.", ""));
|
|
68
|
+
}
|
|
69
|
+
if (path.startsWith("item.")) {
|
|
70
|
+
return ctx.item === void 0 ? void 0 : getPath(ctx.item, path.replace("item.", ""));
|
|
71
|
+
}
|
|
72
|
+
return getPath(ctx.values, path);
|
|
73
|
+
}
|
|
74
|
+
function isExplicitScopedPath(path) {
|
|
75
|
+
return path === "$values" || path === "values" || path === "$resources" || path === "resources" || path === "$row" || path === "row" || path === "$item" || path === "item" || path.startsWith("values.") || path.startsWith("resources.") || path.startsWith("row.") || path.startsWith("item.");
|
|
76
|
+
}
|
|
77
|
+
function resolvePathStringsInData(value, ctx) {
|
|
78
|
+
if (typeof value === "string") {
|
|
79
|
+
return isExplicitScopedPath(value) ? resolveScopedPath(ctx, value) : value;
|
|
80
|
+
}
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
return value.map((item) => resolvePathStringsInData(item, ctx));
|
|
83
|
+
}
|
|
84
|
+
if (typeof value === "object" && value !== null) {
|
|
85
|
+
const result = {};
|
|
86
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
87
|
+
result[key] = resolvePathStringsInData(entry, ctx);
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/utils/url-template.ts
|
|
95
|
+
var pathPattern = /^(values|resources)\.[A-Za-z_$][\w$]*(?:(?:\.(?:[A-Za-z_$][\w$]*|\d+))|(?:\[\d+\]))*$/;
|
|
96
|
+
function placeholders(url) {
|
|
97
|
+
const result = [];
|
|
98
|
+
let cursor = 0;
|
|
99
|
+
while (true) {
|
|
100
|
+
const start = url.indexOf("${", cursor);
|
|
101
|
+
if (start < 0) break;
|
|
102
|
+
const close = url.indexOf("}", start + 2);
|
|
103
|
+
if (close < 0) throw new Error("API URL template has an unclosed placeholder");
|
|
104
|
+
const path = url.slice(start + 2, close).trim();
|
|
105
|
+
if (!pathPattern.test(path)) {
|
|
106
|
+
throw new Error(`Invalid API URL placeholder: ${path}. Use a values.* or resources.* data path`);
|
|
107
|
+
}
|
|
108
|
+
const origin = url.match(/^(?:[A-Za-z][A-Za-z0-9+.-]*:)?\/\/[^/?#]*/)?.[0];
|
|
109
|
+
if (start === 0 || origin && start < origin.length) {
|
|
110
|
+
throw new Error("API URL placeholders cannot replace the service origin; use a static base URL");
|
|
111
|
+
}
|
|
112
|
+
result.push({ start, end: close + 1, path });
|
|
113
|
+
cursor = close + 1;
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
function validateApiUrlTemplate(url) {
|
|
118
|
+
if (!url.trim()) throw new Error("API URL must not be empty");
|
|
119
|
+
placeholders(url);
|
|
120
|
+
}
|
|
121
|
+
function resolveApiUrlTemplate(url, context) {
|
|
122
|
+
validateApiUrlTemplate(url);
|
|
123
|
+
let result = "";
|
|
124
|
+
let cursor = 0;
|
|
125
|
+
for (const placeholder of placeholders(url)) {
|
|
126
|
+
const value = resolveScopedPath(context, placeholder.path);
|
|
127
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean" || typeof value === "number" && !Number.isFinite(value) || value === "") {
|
|
128
|
+
throw new Error(`API URL placeholder ${placeholder.path} must resolve to a non-empty string, finite number, or boolean`);
|
|
129
|
+
}
|
|
130
|
+
if (value === "." || value === "..") {
|
|
131
|
+
throw new Error(`API URL placeholder ${placeholder.path} cannot be a dot segment`);
|
|
132
|
+
}
|
|
133
|
+
result += url.slice(cursor, placeholder.start) + encodeURIComponent(String(value));
|
|
134
|
+
cursor = placeholder.end;
|
|
135
|
+
}
|
|
136
|
+
return result + url.slice(cursor);
|
|
137
|
+
}
|
|
138
|
+
|
|
1
139
|
// src/compiler/compile-schema.ts
|
|
2
140
|
import {
|
|
3
141
|
fieldAllowsEvent,
|
|
@@ -1308,7 +1446,25 @@ function collectDrawerSteps(steps, path, context, owningDrawerId) {
|
|
|
1308
1446
|
});
|
|
1309
1447
|
});
|
|
1310
1448
|
}
|
|
1449
|
+
function validateApiUrlsInSteps(steps, path) {
|
|
1450
|
+
steps.forEach((step, index) => {
|
|
1451
|
+
const stepPath = `${path}[${index}]`;
|
|
1452
|
+
if (step.type === "when") {
|
|
1453
|
+
validateApiUrlsInSteps(step.then, `${stepPath}.then`);
|
|
1454
|
+
if (step.else) validateApiUrlsInSteps(step.else, `${stepPath}.else`);
|
|
1455
|
+
}
|
|
1456
|
+
if (step.type === "api") {
|
|
1457
|
+
try {
|
|
1458
|
+
validateApiUrlTemplate(step.request.url);
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
throw new Error(`Invalid API URL at ${stepPath}.request.url: ${error instanceof Error ? error.message : String(error)}`);
|
|
1461
|
+
}
|
|
1462
|
+
if (step.onError) validateApiUrlsInSteps(step.onError, `${stepPath}.onError`);
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1311
1466
|
function collectDialogSteps(steps, path, context, owningDialogId) {
|
|
1467
|
+
validateApiUrlsInSteps(steps, path);
|
|
1312
1468
|
steps.forEach((step, index) => {
|
|
1313
1469
|
if (step.type !== "dialog") {
|
|
1314
1470
|
return;
|
|
@@ -1729,99 +1885,6 @@ function isResourcePathInFlight(sourceRelative, inFlight) {
|
|
|
1729
1885
|
);
|
|
1730
1886
|
}
|
|
1731
1887
|
|
|
1732
|
-
// src/utils/get-path.ts
|
|
1733
|
-
function pathSegments(path) {
|
|
1734
|
-
const segments = [];
|
|
1735
|
-
let i = 0;
|
|
1736
|
-
while (i < path.length) {
|
|
1737
|
-
const char = path[i];
|
|
1738
|
-
if (char === ".") {
|
|
1739
|
-
i += 1;
|
|
1740
|
-
continue;
|
|
1741
|
-
}
|
|
1742
|
-
if (char === "[") {
|
|
1743
|
-
const close = path.indexOf("]", i);
|
|
1744
|
-
if (close === -1) {
|
|
1745
|
-
segments.push(path.slice(i));
|
|
1746
|
-
break;
|
|
1747
|
-
}
|
|
1748
|
-
segments.push(path.slice(i + 1, close));
|
|
1749
|
-
i = close + 1;
|
|
1750
|
-
continue;
|
|
1751
|
-
}
|
|
1752
|
-
let end = i;
|
|
1753
|
-
while (end < path.length && path[end] !== "." && path[end] !== "[") {
|
|
1754
|
-
end += 1;
|
|
1755
|
-
}
|
|
1756
|
-
if (end > i) {
|
|
1757
|
-
segments.push(path.slice(i, end));
|
|
1758
|
-
}
|
|
1759
|
-
i = end;
|
|
1760
|
-
}
|
|
1761
|
-
return segments;
|
|
1762
|
-
}
|
|
1763
|
-
function getPath(obj, path) {
|
|
1764
|
-
if (!path) {
|
|
1765
|
-
return obj;
|
|
1766
|
-
}
|
|
1767
|
-
let current = obj;
|
|
1768
|
-
for (const key of pathSegments(path)) {
|
|
1769
|
-
if (typeof current !== "object" || current === null) {
|
|
1770
|
-
return void 0;
|
|
1771
|
-
}
|
|
1772
|
-
current = current[key];
|
|
1773
|
-
}
|
|
1774
|
-
return current;
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
// src/utils/scope-path.ts
|
|
1778
|
-
function resolveScopedPath(ctx, path) {
|
|
1779
|
-
if (path === "$values" || path === "values") {
|
|
1780
|
-
return ctx.values;
|
|
1781
|
-
}
|
|
1782
|
-
if (path === "$resources" || path === "resources") {
|
|
1783
|
-
return ctx.resources;
|
|
1784
|
-
}
|
|
1785
|
-
if (path === "$row" || path === "row") {
|
|
1786
|
-
return ctx.row;
|
|
1787
|
-
}
|
|
1788
|
-
if (path === "$item" || path === "item") {
|
|
1789
|
-
return ctx.item;
|
|
1790
|
-
}
|
|
1791
|
-
if (path.startsWith("values.")) {
|
|
1792
|
-
return getPath(ctx.values, path.replace("values.", ""));
|
|
1793
|
-
}
|
|
1794
|
-
if (path.startsWith("resources.")) {
|
|
1795
|
-
return getPath(ctx.resources, path.replace("resources.", ""));
|
|
1796
|
-
}
|
|
1797
|
-
if (path.startsWith("row.")) {
|
|
1798
|
-
return ctx.row === void 0 ? void 0 : getPath(ctx.row, path.replace("row.", ""));
|
|
1799
|
-
}
|
|
1800
|
-
if (path.startsWith("item.")) {
|
|
1801
|
-
return ctx.item === void 0 ? void 0 : getPath(ctx.item, path.replace("item.", ""));
|
|
1802
|
-
}
|
|
1803
|
-
return getPath(ctx.values, path);
|
|
1804
|
-
}
|
|
1805
|
-
function isExplicitScopedPath(path) {
|
|
1806
|
-
return path === "$values" || path === "values" || path === "$resources" || path === "resources" || path === "$row" || path === "row" || path === "$item" || path === "item" || path.startsWith("values.") || path.startsWith("resources.") || path.startsWith("row.") || path.startsWith("item.");
|
|
1807
|
-
}
|
|
1808
|
-
function resolvePathStringsInData(value, ctx) {
|
|
1809
|
-
if (typeof value === "string") {
|
|
1810
|
-
return isExplicitScopedPath(value) ? resolveScopedPath(ctx, value) : value;
|
|
1811
|
-
}
|
|
1812
|
-
if (Array.isArray(value)) {
|
|
1813
|
-
return value.map((item) => resolvePathStringsInData(item, ctx));
|
|
1814
|
-
}
|
|
1815
|
-
if (typeof value === "object" && value !== null) {
|
|
1816
|
-
const result = {};
|
|
1817
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
1818
|
-
result[key] = resolvePathStringsInData(entry, ctx);
|
|
1819
|
-
}
|
|
1820
|
-
return result;
|
|
1821
|
-
}
|
|
1822
|
-
return value;
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
1888
|
// src/conditions/evaluate.ts
|
|
1826
1889
|
function evaluateCondition(condition, values, state, options) {
|
|
1827
1890
|
if (isInvokeCondition(condition)) {
|
|
@@ -2864,6 +2927,7 @@ function resolveApiRequestUrl(step, engine, options) {
|
|
|
2864
2927
|
...options?.row !== void 0 ? { row: options.row } : {},
|
|
2865
2928
|
...options?.item !== void 0 ? { item: options.item } : {}
|
|
2866
2929
|
};
|
|
2930
|
+
const url = resolveApiUrlTemplate(step.request.url, ctx);
|
|
2867
2931
|
const hasQueryFrom = "queryFrom" in step.request && step.request.queryFrom !== void 0;
|
|
2868
2932
|
const hasQuery = "query" in step.request && step.request.query !== void 0;
|
|
2869
2933
|
if (hasQueryFrom && hasQuery) {
|
|
@@ -2879,7 +2943,7 @@ function resolveApiRequestUrl(step, engine, options) {
|
|
|
2879
2943
|
} else if (hasQuery) {
|
|
2880
2944
|
queryValue = resolvePathStringsInData(step.request.query, ctx);
|
|
2881
2945
|
} else {
|
|
2882
|
-
return
|
|
2946
|
+
return url;
|
|
2883
2947
|
}
|
|
2884
2948
|
if (queryValue === void 0 || queryValue === null || typeof queryValue !== "object" || Array.isArray(queryValue)) {
|
|
2885
2949
|
throw new TypeError("api step query must resolve to an object");
|
|
@@ -2893,10 +2957,13 @@ function resolveApiRequestUrl(step, engine, options) {
|
|
|
2893
2957
|
}
|
|
2894
2958
|
const queryString = params.toString();
|
|
2895
2959
|
if (!queryString) {
|
|
2896
|
-
return
|
|
2960
|
+
return url;
|
|
2897
2961
|
}
|
|
2898
|
-
const
|
|
2899
|
-
|
|
2962
|
+
const hashIndex = url.indexOf("#");
|
|
2963
|
+
const base = hashIndex < 0 ? url : url.slice(0, hashIndex);
|
|
2964
|
+
const hash = hashIndex < 0 ? "" : url.slice(hashIndex);
|
|
2965
|
+
const separator = base.includes("?") ? "&" : "?";
|
|
2966
|
+
return `${base}${separator}${queryString}${hash}`;
|
|
2900
2967
|
}
|
|
2901
2968
|
function toRequestBody(value, headers) {
|
|
2902
2969
|
if (isBodyInit(value)) {
|
|
@@ -5189,6 +5256,7 @@ export {
|
|
|
5189
5256
|
runConditionEngine,
|
|
5190
5257
|
tagMapKey,
|
|
5191
5258
|
traverseNode,
|
|
5259
|
+
validateApiUrlTemplate,
|
|
5192
5260
|
validateField,
|
|
5193
5261
|
validateSchema,
|
|
5194
5262
|
validation,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsm-form/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "Framework-independent form engine for BSM FormSchema",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
],
|
|
20
20
|
"sideEffects": false,
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@bsm-form/schema": "0.
|
|
22
|
+
"@bsm-form/schema": "0.40.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"vitest": "^4.1.10"
|