@shwfed/config 2.12.13 → 2.12.15
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/module.json +1 -1
- package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/config.d.vue.ts +8 -0
- package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/config.vue +118 -25
- package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/config.vue.d.ts +8 -0
- package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/runtime.vue +48 -21
- package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/schema.d.ts +5 -0
- package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/schema.js +24 -1
- package/dist/runtime/components/config/use-editor.js +7 -0
- package/dist/runtime/components/form/fields/2026-05-23/com.shwfed.form.field.tree.multi/config.d.vue.ts +18 -18
- package/dist/runtime/components/form/fields/2026-05-23/com.shwfed.form.field.tree.multi/config.vue.d.ts +18 -18
- package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.multi/config.d.vue.ts +18 -18
- package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.multi/config.vue.d.ts +18 -18
- package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.single/config.d.vue.ts +18 -18
- package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.single/config.vue.d.ts +18 -18
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.multi/config.d.vue.ts +18 -18
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.multi/config.vue.d.ts +18 -18
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.single/config.d.vue.ts +18 -18
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.single/config.vue.d.ts +18 -18
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.multi/config.d.vue.ts +18 -18
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.multi/config.vue.d.ts +18 -18
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.single/config.d.vue.ts +18 -18
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.single/config.vue.d.ts +18 -18
- package/dist/runtime/components/sidebar/use-editor.js +7 -0
- package/dist/runtime/components/table/config.vue +10 -4
- package/dist/runtime/components/table/index.vue +6 -1
- package/dist/runtime/components/ui/expression-editor/ExpressionEditor.vue +21 -3
- package/dist/runtime/components/ui/expression-editor/index.d.ts +1 -0
- package/dist/runtime/components/ui/expression-editor/index.js +5 -0
- package/dist/runtime/components/ui/expression-editor/validity.d.ts +12 -0
- package/dist/runtime/components/ui/expression-editor/validity.js +27 -0
- package/dist/runtime/utils/cel-context.d.ts +15 -0
- package/dist/runtime/utils/cel-context.js +18 -1
- package/dist/runtime/vendor/cel-js/CLAUDE.md +1 -1
- package/dist/runtime/vendor/cel-js/PROMPT.md +15 -0
- package/dist/runtime/vendor/cel-js/lib/functions.js +28 -0
- package/package.json +1 -1
|
@@ -55,15 +55,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
|
|
|
55
55
|
readonly locale: "en" | "ja" | "ko";
|
|
56
56
|
readonly message: string;
|
|
57
57
|
}[]];
|
|
58
|
-
readonly nodeTooltip?: readonly [{
|
|
59
|
-
readonly locale: "zh";
|
|
60
|
-
readonly message: string;
|
|
61
|
-
}, ...{
|
|
62
|
-
readonly locale: "en" | "ja" | "ko";
|
|
63
|
-
readonly message: string;
|
|
64
|
-
}[]] | undefined;
|
|
65
|
-
readonly nodeSelectable?: string | undefined;
|
|
66
|
-
readonly expandAll: boolean;
|
|
67
58
|
readonly validations?: readonly {
|
|
68
59
|
readonly message: readonly [{
|
|
69
60
|
readonly locale: "zh";
|
|
@@ -75,6 +66,15 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
|
|
|
75
66
|
readonly warning?: boolean | undefined;
|
|
76
67
|
readonly when: string;
|
|
77
68
|
}[] | undefined;
|
|
69
|
+
readonly nodeTooltip?: readonly [{
|
|
70
|
+
readonly locale: "zh";
|
|
71
|
+
readonly message: string;
|
|
72
|
+
}, ...{
|
|
73
|
+
readonly locale: "en" | "ja" | "ko";
|
|
74
|
+
readonly message: string;
|
|
75
|
+
}[]] | undefined;
|
|
76
|
+
readonly nodeSelectable?: string | undefined;
|
|
77
|
+
readonly expandAll: boolean;
|
|
78
78
|
}) => any;
|
|
79
79
|
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
80
80
|
"onUpdate:modelValue"?: ((value: {
|
|
@@ -129,15 +129,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
|
|
|
129
129
|
readonly locale: "en" | "ja" | "ko";
|
|
130
130
|
readonly message: string;
|
|
131
131
|
}[]];
|
|
132
|
-
readonly nodeTooltip?: readonly [{
|
|
133
|
-
readonly locale: "zh";
|
|
134
|
-
readonly message: string;
|
|
135
|
-
}, ...{
|
|
136
|
-
readonly locale: "en" | "ja" | "ko";
|
|
137
|
-
readonly message: string;
|
|
138
|
-
}[]] | undefined;
|
|
139
|
-
readonly nodeSelectable?: string | undefined;
|
|
140
|
-
readonly expandAll: boolean;
|
|
141
132
|
readonly validations?: readonly {
|
|
142
133
|
readonly message: readonly [{
|
|
143
134
|
readonly locale: "zh";
|
|
@@ -149,6 +140,15 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
|
|
|
149
140
|
readonly warning?: boolean | undefined;
|
|
150
141
|
readonly when: string;
|
|
151
142
|
}[] | undefined;
|
|
143
|
+
readonly nodeTooltip?: readonly [{
|
|
144
|
+
readonly locale: "zh";
|
|
145
|
+
readonly message: string;
|
|
146
|
+
}, ...{
|
|
147
|
+
readonly locale: "en" | "ja" | "ko";
|
|
148
|
+
readonly message: string;
|
|
149
|
+
}[]] | undefined;
|
|
150
|
+
readonly nodeSelectable?: string | undefined;
|
|
151
|
+
readonly expandAll: boolean;
|
|
152
152
|
}) => any) | undefined;
|
|
153
153
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
154
154
|
declare const _default: typeof __VLS_export;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
2
|
import { computed, ref, watch } from "vue";
|
|
3
3
|
import { formatValidationError } from "../config/utils/validation-error.js";
|
|
4
|
+
import { createExpressionValidityRegistry, provideExpressionValidity } from "../ui/expression-editor/index.js";
|
|
4
5
|
import { SidebarConfig } from "./schema.js";
|
|
5
6
|
function deepClone(value) {
|
|
6
7
|
return JSON.parse(JSON.stringify(value));
|
|
@@ -14,10 +15,16 @@ export function useSidebarEditor(model, options = {}) {
|
|
|
14
15
|
() => JSON.stringify(draft.value) !== JSON.stringify(baseline.value)
|
|
15
16
|
);
|
|
16
17
|
const validationError = ref(null);
|
|
18
|
+
const validityRegistry = createExpressionValidityRegistry();
|
|
19
|
+
provideExpressionValidity(validityRegistry);
|
|
17
20
|
function confirm() {
|
|
18
21
|
const schema = SidebarConfig(configure);
|
|
19
22
|
try {
|
|
20
23
|
const decoded = Schema.decodeUnknownSync(schema)(draft.value);
|
|
24
|
+
if (!validityRegistry.allValid()) {
|
|
25
|
+
validationError.value = "\u5B58\u5728\u65E0\u6548\u7684\u8DE8\u5C42\u4F5C\u7528\u57DF\u5F15\u7528\uFF0C\u8BF7\u4FEE\u6B63\u6807\u7EA2\u7684\u8868\u8FBE\u5F0F\u540E\u518D\u4FDD\u5B58";
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
21
28
|
validationError.value = null;
|
|
22
29
|
model.value = decoded;
|
|
23
30
|
baseline.value = deepClone(decoded);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, defineComponent, inject, onBeforeUnmount, provide, ref, watch } from "vue";
|
|
3
|
-
import { celBindings, injectCELContext, provideCELContext, provideScopeAncestor, provideSelectionRoster, SELECTIONS_VAR } from "../../utils/cel-context";
|
|
3
|
+
import { celBindings, injectCELContext, provideCELContext, provideRowUnreachableScope, provideScopeAncestor, provideSelectionRoster, SELECTIONS_VAR } from "../../utils/cel-context";
|
|
4
4
|
import { BREADCRUMB_EXTENSION_KEY } from "../config/breadcrumb-extension";
|
|
5
5
|
import { TABLE_COLUMN_LAYOUT_KEY } from "./column-layout";
|
|
6
6
|
import { Icon } from "@iconify/vue";
|
|
@@ -152,6 +152,7 @@ const TableActionsCELContext = defineComponent({
|
|
|
152
152
|
value: []
|
|
153
153
|
}
|
|
154
154
|
});
|
|
155
|
+
provideRowUnreachableScope();
|
|
155
156
|
return () => slots.default?.();
|
|
156
157
|
}
|
|
157
158
|
});
|
|
@@ -305,14 +306,19 @@ const tableScopeAncestor = computed(() => {
|
|
|
305
306
|
typeIcon: tableMetadata.icon,
|
|
306
307
|
members: [
|
|
307
308
|
{ key: "row", label: "\u5F53\u524D\u884C", type: "dyn", description: "\u8BE5\u8868\u683C\u5F53\u524D\u884C\u7684\u6574\u884C\u6570\u636E" },
|
|
308
|
-
{ key: "index", label: "\u884C\u5E8F\u53F7", type: "number", description: "\u8BE5\u8868\u683C\u5F53\u524D\u884C\u7684\u5E8F\u53F7\uFF0C\u4ECE `0` \u5F00\u59CB" }
|
|
309
|
+
{ key: "index", label: "\u884C\u5E8F\u53F7", type: "number", description: "\u8BE5\u8868\u683C\u5F53\u524D\u884C\u7684\u5E8F\u53F7\uFF0C\u4ECE `0` \u5F00\u59CB" },
|
|
310
|
+
// Registered at the table HOST frame (index.vue), so it is reachable from
|
|
311
|
+
// table-level actions (top toolbar / bottom bar) — where the relative
|
|
312
|
+
// `selected` is shadowed by an inner table — as well as from cells.
|
|
313
|
+
{ key: "selected", label: "\u5DF2\u9009\u884C", type: "list<dyn>", description: "\u8BE5\u8868\u683C\u5F53\u524D\u52FE\u9009\u7684\u6240\u6709\u884C" }
|
|
309
314
|
],
|
|
310
315
|
// Each row-provider rebinds the relative `selections` to its own per-row
|
|
311
316
|
// registry (see row-provider.vue), so a table row shadows an enclosing
|
|
312
317
|
// form's selection options even though the table exposes no `selections`
|
|
313
318
|
// member of its own — list it here so that form's options stay offerable to
|
|
314
|
-
// a cell expression inside this table.
|
|
315
|
-
|
|
319
|
+
// a cell expression inside this table. `selected` likewise: the table host
|
|
320
|
+
// rebinds it, shadowing an enclosing table's selection for everything below.
|
|
321
|
+
shadows: ["row", "index", "selected", SELECTIONS_VAR]
|
|
316
322
|
};
|
|
317
323
|
});
|
|
318
324
|
provideScopeAncestor(tableScopeAncestor);
|
|
@@ -17,7 +17,7 @@ import { Pagination, ScrollArea } from "reka-ui/namespaced";
|
|
|
17
17
|
import { computed, h, onMounted, ref, toRaw, watch } from "vue";
|
|
18
18
|
import { asRequest, fetchJsonOption } from "../../share/request";
|
|
19
19
|
import { useI18n } from "vue-i18n";
|
|
20
|
-
import { celBindings, celScope, provideCELContext, injectCELContext } from "../../utils/cel-context";
|
|
20
|
+
import { celBindings, celScope, provideCELContext, injectCELContext, provideScopeAddress } from "../../utils/cel-context";
|
|
21
21
|
import TableRowProvider from "./row-provider.vue";
|
|
22
22
|
import * as addRowOp from "../operations/2026-07-06/com.shwfed.operation.table.add-row/schema";
|
|
23
23
|
import { getLocalizedText } from "../../share/locale";
|
|
@@ -353,6 +353,11 @@ provideCELContext({
|
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
});
|
|
356
|
+
if (config.value?.id) {
|
|
357
|
+
provideScopeAddress(config.value.id, {
|
|
358
|
+
selected: () => tableApi.getSelectedRowModel().rows.map((r) => r.original)
|
|
359
|
+
});
|
|
360
|
+
}
|
|
356
361
|
provideSharedFetchLayer();
|
|
357
362
|
const hasDragHandleColumn = computed(() => (config.value?.columns ?? []).some((c) => c.type === DRAG_HANDLE_COLUMN_TYPE));
|
|
358
363
|
const reorderDisabledReason = computed(() => {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { Icon } from "@iconify/vue";
|
|
3
|
-
import { computed, ref, useSlots, useTemplateRef, watch } from "vue";
|
|
3
|
+
import { computed, onScopeDispose, ref, useSlots, useTemplateRef, watch } from "vue";
|
|
4
4
|
import { buildCheckEnvironment, evaluateExpression } from "../../../share/expression";
|
|
5
5
|
import { OPTIONS_VAR, SELECTIONS_VAR, injectCELContext, useScopeAncestry, useSelectionRoster, useStepRoster } from "../../../utils/cel-context";
|
|
6
6
|
import { Markdown } from "../markdown";
|
|
7
7
|
import CodeMirrorInput from "./CodeMirrorInput.vue";
|
|
8
|
-
import { buildScopeLookup } from "./scope-refs";
|
|
8
|
+
import { buildScopeLookup, scanScopeRefs } from "./scope-refs";
|
|
9
|
+
import { useExpressionValidity } from "./validity";
|
|
9
10
|
import { buildSelectionLookup } from "./selection-refs";
|
|
10
11
|
import { buildStepLookup } from "./step-refs";
|
|
11
12
|
import { buildOptionEntries, buildScopeEntries, buildSelectionEntries, buildStepEntries, buildVarEntries, selectShadowedScopes } from "./picker-entries";
|
|
@@ -62,11 +63,28 @@ const checkEnvironment = computed(() => {
|
|
|
62
63
|
{ unlistedVariablesAreDyn: props.unlistedVarsAreDyn }
|
|
63
64
|
);
|
|
64
65
|
});
|
|
66
|
+
const danglingScopeError = computed(() => {
|
|
67
|
+
const value = props.modelValue ?? props.defaultValue ?? "";
|
|
68
|
+
if (value.trim() === "") return null;
|
|
69
|
+
const lookup = scopeLookup.value;
|
|
70
|
+
for (const ref2 of scanScopeRefs(value)) {
|
|
71
|
+
const info = lookup.get(ref2.id);
|
|
72
|
+
if (!info || !info.members.has(ref2.member)) {
|
|
73
|
+
return "\u8868\u8FBE\u5F0F\u5F15\u7528\u4E86\u65E0\u6548\u7684\u8DE8\u5C42\u4F5C\u7528\u57DF\uFF08\u6765\u6E90\u5DF2\u4E0D\u5728\u53EF\u5BFB\u5740\u7684\u4E0A\u5C42\u4F5C\u7528\u57DF\u4E2D\uFF09";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
});
|
|
65
78
|
const validationError = computed(() => {
|
|
66
79
|
const value = props.modelValue ?? props.defaultValue ?? "";
|
|
67
80
|
if (value.trim() === "") return null;
|
|
68
|
-
return evaluateExpression(checkEnvironment.value, value, props.resultType);
|
|
81
|
+
return danglingScopeError.value ?? evaluateExpression(checkEnvironment.value, value, props.resultType);
|
|
69
82
|
});
|
|
83
|
+
const validityRegistry = useExpressionValidity();
|
|
84
|
+
if (validityRegistry) {
|
|
85
|
+
const unregister = validityRegistry.register(() => danglingScopeError.value === null);
|
|
86
|
+
onScopeDispose(unregister);
|
|
87
|
+
}
|
|
70
88
|
const open = ref(false);
|
|
71
89
|
const entries = ref([]);
|
|
72
90
|
const shownVars = computed(() => entries.value.filter((e) => e.group === "var"));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type ExpressionValidityRegistry = {
|
|
2
|
+
/**
|
|
3
|
+
* Register a mounted editor's validity thunk (`true` ⇒ no ancestry error).
|
|
4
|
+
* Returns an unregister fn to call from `onScopeDispose`.
|
|
5
|
+
*/
|
|
6
|
+
register: (isValid: () => boolean) => () => void;
|
|
7
|
+
/** `true` when every currently-registered editor reports valid. */
|
|
8
|
+
allValid: () => boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function createExpressionValidityRegistry(): ExpressionValidityRegistry;
|
|
11
|
+
export declare function provideExpressionValidity(registry: ExpressionValidityRegistry): void;
|
|
12
|
+
export declare function useExpressionValidity(): ExpressionValidityRegistry | undefined;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getCurrentInstance, inject, provide } from "vue";
|
|
2
|
+
const KEY = Symbol("shwfed-expression-validity");
|
|
3
|
+
export function createExpressionValidityRegistry() {
|
|
4
|
+
const entries = /* @__PURE__ */ new Map();
|
|
5
|
+
return {
|
|
6
|
+
register(isValid) {
|
|
7
|
+
const token = Symbol("expr");
|
|
8
|
+
entries.set(token, isValid);
|
|
9
|
+
return () => {
|
|
10
|
+
entries.delete(token);
|
|
11
|
+
};
|
|
12
|
+
},
|
|
13
|
+
allValid() {
|
|
14
|
+
for (const isValid of entries.values()) {
|
|
15
|
+
if (!isValid()) return false;
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function provideExpressionValidity(registry) {
|
|
22
|
+
if (!getCurrentInstance()) return;
|
|
23
|
+
provide(KEY, registry);
|
|
24
|
+
}
|
|
25
|
+
export function useExpressionValidity() {
|
|
26
|
+
return inject(KEY, void 0);
|
|
27
|
+
}
|
|
@@ -67,6 +67,21 @@ export declare function provideScopeAncestor(ancestor: Ref<ScopeAncestor | null>
|
|
|
67
67
|
* first. Empty when nothing up the config tree exposes an addressable scope.
|
|
68
68
|
*/
|
|
69
69
|
export declare function useScopeAncestry(): Ref<ReadonlyArray<ScopeAncestor>>;
|
|
70
|
+
/**
|
|
71
|
+
* Re-provide the inherited scope ancestry with the NEAREST frame's per-row
|
|
72
|
+
* members (`row` / `index`) dropped — its `selected` (and any other member)
|
|
73
|
+
* survives. Called by a table-LEVEL action editor (top toolbar / bottom bar):
|
|
74
|
+
* such an action renders OUTSIDE any row, so at runtime the owning table's
|
|
75
|
+
* `__scopes__[id]` there carries only `selected` (registered at the table host
|
|
76
|
+
* frame), never `row`/`index` (registered per row). Everything the action opens
|
|
77
|
+
* — nested modals and their tables — inherits this downgraded chain, so a
|
|
78
|
+
* cross-layer picker below never offers the owning table's 当前行 / 行序号, which
|
|
79
|
+
* would throw `No such key: row` at evaluation. Outer frames are untouched: each
|
|
80
|
+
* table masks only its own here, and an outer table reached via a genuine row
|
|
81
|
+
* action keeps its row/index reachable. `shadows` is left intact — this frame
|
|
82
|
+
* still masks the relative `row`/`index` for descendants at runtime.
|
|
83
|
+
*/
|
|
84
|
+
export declare function provideRowUnreachableScope(): void;
|
|
70
85
|
/** One selection-like field in the editing form, as the picker/chip surfaces it. */
|
|
71
86
|
export type SelectionFieldRef = Readonly<{
|
|
72
87
|
/** Field uuid keyed into `selections["<id>"]` — the registry/publish key. */
|
|
@@ -30,7 +30,8 @@ export function provideScopeAddress(uuid, accessor) {
|
|
|
30
30
|
// bracket access throws a located `No such key` at the offending uuid.
|
|
31
31
|
value: () => {
|
|
32
32
|
const base = parentScopes ? typeof parentScopes.value === "function" ? parentScopes.value() : parentScopes.value : {};
|
|
33
|
-
|
|
33
|
+
const prior = base[uuid];
|
|
34
|
+
return { ...base, [uuid]: { ...prior, ...resolveScopeAccessor(accessor) } };
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
});
|
|
@@ -49,6 +50,22 @@ export function provideScopeAncestor(ancestor) {
|
|
|
49
50
|
export function useScopeAncestry() {
|
|
50
51
|
return inject(SCOPE_ANCESTRY_KEY, void 0) ?? shallowRef([]);
|
|
51
52
|
}
|
|
53
|
+
export function provideRowUnreachableScope() {
|
|
54
|
+
const parent = inject(SCOPE_ANCESTRY_KEY, void 0);
|
|
55
|
+
provide(
|
|
56
|
+
SCOPE_ANCESTRY_KEY,
|
|
57
|
+
computed(() => {
|
|
58
|
+
const chain = parent?.value ?? [];
|
|
59
|
+
if (chain.length === 0) return chain;
|
|
60
|
+
const [nearest, ...rest] = chain;
|
|
61
|
+
const members = nearest.members.filter((m) => {
|
|
62
|
+
const shortcut = m.shortcut ?? m.key;
|
|
63
|
+
return shortcut !== "row" && shortcut !== "index";
|
|
64
|
+
});
|
|
65
|
+
return [{ ...nearest, members }, ...rest];
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
}
|
|
52
69
|
const SELECTION_ROSTER_KEY = Symbol("cel-selection-roster");
|
|
53
70
|
export function provideSelectionRoster(roster) {
|
|
54
71
|
provide(SELECTION_ROSTER_KEY, roster);
|
|
@@ -85,7 +85,7 @@ Internal-only (not re-exported from `index.js`): `parse`, standalone `evaluate`/
|
|
|
85
85
|
|
|
86
86
|
Primitives: `string`, `bool`, `number`, `bytes`, `null_type`, `type`
|
|
87
87
|
Composites: `list<T>`, `map<K, V>`, `optional<T>`, `dyn`, `message`
|
|
88
|
-
Custom: `Date` (backed by `TZDate` from `@date-fns/tz`), `URL` (backed by the native `URL`;
|
|
88
|
+
Custom: `Date` (backed by `TZDate` from `@date-fns/tz`), `URL` (backed by the native `URL`; host-supplied only — no CEL constructor. Reader `URL.query(string): optional<string>` returns the *first* value of a search param — `None` when absent, distinguishing it from a present-but-empty value. Writers return a **copy**, never mutating the receiver (a `URL` is host-injected shared state, unlike a fresh-per-expression `HttpRequestBuilder`): the set/replace `URL.query(string, string | number | optional<string> | optional<number>): URL` collapses repeated entries and on `None` *deletes* the key (the result mirrors the optional's state — deliberately unlike `HttpRequest.query`, whose `None` is a drop/no-op), while `URL.appendQuery(...)` with the same four value overloads appends an entry and no-ops on `None`. Number values render Decimal-exact, like `string(number)`. `string(URL)` is the full `href`.), `File` (native), `FormData` (native — see `form-builtins.ts`), `http` / `HttpRequest` (local addition — see `http-builtins.ts`)
|
|
89
89
|
|
|
90
90
|
## Function Signature Format
|
|
91
91
|
|
|
@@ -105,6 +105,7 @@ number(d) // Date → epoch-millisecond timestamp
|
|
|
105
105
|
string(42) // "42"
|
|
106
106
|
string(true) // "true"
|
|
107
107
|
string(d) // Date → ISO 8601 string
|
|
108
|
+
string(location) // URL → the full href string
|
|
108
109
|
bool("true") // true (also accepts "t", "1", "TRUE", "True")
|
|
109
110
|
bool("false") // false (also accepts "f", "0", "FALSE", "False")
|
|
110
111
|
bytes("text") // UTF-8 encode to bytes
|
|
@@ -310,6 +311,20 @@ location.query("missing").hasValue() // false — absent params are None
|
|
|
310
311
|
location.query("id").orValue("") // first value when a param repeats
|
|
311
312
|
```
|
|
312
313
|
|
|
314
|
+
`query(name, value)` **sets** a search param and returns a new `URL` — the
|
|
315
|
+
original is never modified. The value may be a `string`, a `number`, or an
|
|
316
|
+
optional of either: `Some(v)` sets the param, `None` **removes** it, so the
|
|
317
|
+
result always mirrors the optional's state. `appendQuery(name, value)` takes
|
|
318
|
+
the same value types but **adds** an entry alongside existing ones (`None`
|
|
319
|
+
appends nothing). Convert the result with `string(...)` when the host expects
|
|
320
|
+
a URL string.
|
|
321
|
+
```cel
|
|
322
|
+
location.query("page", 2) // ?page=1 → ?page=2 (replaced)
|
|
323
|
+
location.query("page", q.?page) // sets ?page= from the field, or removes it when absent
|
|
324
|
+
location.appendQuery("tag", "a") // ?tag=x → ?tag=x&tag=a (kept both)
|
|
325
|
+
string(location.query("page", 2)) // "https://…?page=2" — e.g. a navigation target
|
|
326
|
+
```
|
|
327
|
+
|
|
313
328
|
## HTTP
|
|
314
329
|
|
|
315
330
|
Build HTTP requests using the `http` builder. A CEL expression only *describes* a request — it always evaluates to an `HttpRequest`, never issues it. The host decides when and how to send it (downloading a file, reading a JSON response, etc.). There is no way to perform the request or read its response from CEL.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { evaluationError } from "./errors.js";
|
|
2
2
|
import { Decimal } from "./decimal.js";
|
|
3
3
|
import { TYPES, Type } from "./registry.js";
|
|
4
|
+
import { Option } from "effect";
|
|
4
5
|
import { register as registerOptional, optionalOf } from "./optional.js";
|
|
5
6
|
import { objKeys, arrayFrom } from "./globals.js";
|
|
6
7
|
import { TZDate } from "@date-fns/tz";
|
|
@@ -410,6 +411,33 @@ export function registerFunctions(registry) {
|
|
|
410
411
|
registry.registerType("File", File);
|
|
411
412
|
registry.registerType("URL", URL);
|
|
412
413
|
functionOverload("URL.query(string): optional<string>", (url, key) => optionalOf(url.searchParams.get(key) ?? void 0));
|
|
414
|
+
const withSearchParams = (u, write) => {
|
|
415
|
+
const url = new URL(u.href);
|
|
416
|
+
write(url.searchParams);
|
|
417
|
+
return url;
|
|
418
|
+
};
|
|
419
|
+
const numToParam = (v) => Decimal.from(v).toString();
|
|
420
|
+
functionOverload("URL.query(string, string): URL", (u, k, v) => withSearchParams(u, (p) => p.set(k, v)));
|
|
421
|
+
functionOverload("URL.query(string, number): URL", (u, k, v) => withSearchParams(u, (p) => p.set(k, numToParam(v))));
|
|
422
|
+
functionOverload("URL.query(string, optional<string>): URL", (u, k, v) => {
|
|
423
|
+
const value = v;
|
|
424
|
+
return withSearchParams(u, (p) => Option.isSome(value) ? p.set(k, value.value) : p.delete(k));
|
|
425
|
+
});
|
|
426
|
+
functionOverload("URL.query(string, optional<number>): URL", (u, k, v) => {
|
|
427
|
+
const value = v;
|
|
428
|
+
return withSearchParams(u, (p) => Option.isSome(value) ? p.set(k, numToParam(value.value)) : p.delete(k));
|
|
429
|
+
});
|
|
430
|
+
functionOverload("URL.appendQuery(string, string): URL", (u, k, v) => withSearchParams(u, (p) => p.append(k, v)));
|
|
431
|
+
functionOverload("URL.appendQuery(string, number): URL", (u, k, v) => withSearchParams(u, (p) => p.append(k, numToParam(v))));
|
|
432
|
+
functionOverload("URL.appendQuery(string, optional<string>): URL", (u, k, v) => {
|
|
433
|
+
const value = v;
|
|
434
|
+
return Option.isSome(value) ? withSearchParams(u, (p) => p.append(k, value.value)) : u;
|
|
435
|
+
});
|
|
436
|
+
functionOverload("URL.appendQuery(string, optional<number>): URL", (u, k, v) => {
|
|
437
|
+
const value = v;
|
|
438
|
+
return Option.isSome(value) ? withSearchParams(u, (p) => p.append(k, numToParam(value.value))) : u;
|
|
439
|
+
});
|
|
440
|
+
functionOverload("string(URL): string", (v) => v.href);
|
|
413
441
|
registerOptional(registry);
|
|
414
442
|
}
|
|
415
443
|
function stringSize(str) {
|