@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 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 step.request.url;
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 step.request.url;
2985
+ return url;
2921
2986
  }
2922
- const separator = step.request.url.includes("?") ? "&" : "?";
2923
- return `${step.request.url}${separator}${queryString}`;
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
- 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, validateField, validateSchema, validation, validationRegistry };
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
- 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, validateField, validateSchema, validation, validationRegistry };
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 step.request.url;
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 step.request.url;
2960
+ return url;
2897
2961
  }
2898
- const separator = step.request.url.includes("?") ? "&" : "?";
2899
- return `${step.request.url}${separator}${queryString}`;
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.39.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.39.0"
22
+ "@bsm-form/schema": "0.40.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "vitest": "^4.1.10"