@narrative.io/jsonforms-provider-protocols 1.0.3
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/LICENSE +21 -0
- package/README.md +200 -0
- package/dist/_virtual/_plugin-vue_export-helper.js +11 -0
- package/dist/_virtual/_plugin-vue_export-helper.js.map +1 -0
- package/dist/core/cache.d.ts +8 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/cache.js +27 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/jsonpath.d.ts +2 -0
- package/dist/core/jsonpath.d.ts.map +1 -0
- package/dist/core/jsonpath.js +40 -0
- package/dist/core/jsonpath.js.map +1 -0
- package/dist/core/registry.d.ts +8 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +17 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/templating.d.ts +3 -0
- package/dist/core/templating.d.ts.map +1 -0
- package/dist/core/templating.js +32 -0
- package/dist/core/templating.js.map +1 -0
- package/dist/core/types.d.ts +44 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/jsonforms-provider-protocols.css +4 -0
- package/dist/protocols/rest_api.d.ts +22 -0
- package/dist/protocols/rest_api.d.ts.map +1 -0
- package/dist/protocols/rest_api.js +92 -0
- package/dist/protocols/rest_api.js.map +1 -0
- package/dist/vue/components/ProviderAutocomplete.vue.d.ts +9 -0
- package/dist/vue/components/ProviderAutocomplete.vue.d.ts.map +1 -0
- package/dist/vue/components/ProviderAutocomplete.vue.js +68 -0
- package/dist/vue/components/ProviderAutocomplete.vue.js.map +1 -0
- package/dist/vue/components/ProviderAutocomplete.vue2.js +5 -0
- package/dist/vue/components/ProviderAutocomplete.vue2.js.map +1 -0
- package/dist/vue/components/ProviderSelect.vue.d.ts +9 -0
- package/dist/vue/components/ProviderSelect.vue.d.ts.map +1 -0
- package/dist/vue/components/ProviderSelect.vue.js +8 -0
- package/dist/vue/components/ProviderSelect.vue.js.map +1 -0
- package/dist/vue/components/ProviderSelect.vue2.js +74 -0
- package/dist/vue/components/ProviderSelect.vue2.js.map +1 -0
- package/dist/vue/composables/useProvider.d.ts +14 -0
- package/dist/vue/composables/useProvider.d.ts.map +1 -0
- package/dist/vue/composables/useProvider.js +76 -0
- package/dist/vue/composables/useProvider.js.map +1 -0
- package/dist/vue/index.d.ts +19 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +36 -0
- package/dist/vue/index.js.map +1 -0
- package/dist/vue/testers.d.ts +2 -0
- package/dist/vue/testers.d.ts.map +1 -0
- package/dist/vue/testers.js +12 -0
- package/dist/vue/testers.js.map +1 -0
- package/package.json +100 -0
- package/src/core/cache.ts +23 -0
- package/src/core/jsonpath.ts +41 -0
- package/src/core/registry.ts +15 -0
- package/src/core/templating.ts +27 -0
- package/src/core/types.ts +46 -0
- package/src/index.ts +36 -0
- package/src/protocols/rest_api.ts +136 -0
- package/src/vue/components/ProviderAutocomplete.vue +54 -0
- package/src/vue/components/ProviderSelect.vue +70 -0
- package/src/vue/composables/useProvider.ts +111 -0
- package/src/vue/index.ts +44 -0
- package/src/vue/testers.ts +19 -0
- package/src/vue-shim.d.ts +9 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function renderTpl(str: string, ctx: unknown): string {
|
|
2
|
+
return str.replace(/\{\{\s*([^}]+)\s*\}\}/g, (_, path) => {
|
|
3
|
+
const trimmedPath = path.trim();
|
|
4
|
+
return String(
|
|
5
|
+
trimmedPath.split(".").reduce((a: unknown, k: string) => {
|
|
6
|
+
if (a && typeof a === "object" && k in a) {
|
|
7
|
+
return (a as Record<string, unknown>)[k];
|
|
8
|
+
}
|
|
9
|
+
return undefined;
|
|
10
|
+
}, ctx) ?? "",
|
|
11
|
+
);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export function renderObj<T = unknown>(obj: unknown, ctx: unknown): T {
|
|
15
|
+
if (!obj || typeof obj !== "object") return obj as T;
|
|
16
|
+
const out: Record<string, unknown> | unknown[] = Array.isArray(obj) ? [] : {};
|
|
17
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
18
|
+
if (typeof v === "string") {
|
|
19
|
+
(out as Record<string, unknown>)[k] = renderTpl(v, ctx);
|
|
20
|
+
} else if (v && typeof v === "object") {
|
|
21
|
+
(out as Record<string, unknown>)[k] = renderObj(v, ctx);
|
|
22
|
+
} else {
|
|
23
|
+
(out as Record<string, unknown>)[k] = v;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out as T;
|
|
27
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type ProviderItem = {
|
|
2
|
+
label: string;
|
|
3
|
+
value: unknown;
|
|
4
|
+
meta?: Record<string, unknown>;
|
|
5
|
+
};
|
|
6
|
+
export type ProviderOutput = {
|
|
7
|
+
items: ProviderItem[];
|
|
8
|
+
ttl?: number;
|
|
9
|
+
etag?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ProviderContext = {
|
|
13
|
+
data: unknown;
|
|
14
|
+
path: string;
|
|
15
|
+
signal: AbortSignal;
|
|
16
|
+
ui?: { query?: string };
|
|
17
|
+
auth?: Record<string, unknown>; // Global auth registry, not AuthConfig
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface Protocol<Cfg = unknown> {
|
|
21
|
+
protocol: string; // 'rest_api' | 'table' | 'cookie' | ...
|
|
22
|
+
resolve(cfg: Cfg, ctx: ProviderContext): Promise<ProviderOutput>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ProviderBinding {
|
|
26
|
+
ref: string;
|
|
27
|
+
protocol: string;
|
|
28
|
+
config?: unknown;
|
|
29
|
+
auth?: AuthConfig;
|
|
30
|
+
cacheTTL?: number;
|
|
31
|
+
load?: "mount" | "onFocus" | "query";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type Mapping = {
|
|
35
|
+
label: string;
|
|
36
|
+
value: string;
|
|
37
|
+
meta?: Record<string, string>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export interface AuthConfig {
|
|
41
|
+
use?: string; // Reference global auth key
|
|
42
|
+
apiKey?: string | (() => string); // API key auth
|
|
43
|
+
bearer?: string | (() => string); // Bearer token auth
|
|
44
|
+
token?: string | (() => string); // Generic token auth
|
|
45
|
+
[key: string]: unknown; // Custom auth fields
|
|
46
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { App } from "vue";
|
|
2
|
+
import { cache as globalCache } from "./core/cache";
|
|
3
|
+
import { registry as globalRegistry } from "./core/registry";
|
|
4
|
+
import type { Protocol, AuthConfig } from "./core/types";
|
|
5
|
+
|
|
6
|
+
export { cache } from "./core/cache";
|
|
7
|
+
export * from "./core/jsonpath";
|
|
8
|
+
export { registry } from "./core/registry";
|
|
9
|
+
export * from "./core/templating";
|
|
10
|
+
// Core exports
|
|
11
|
+
export * from "./core/types";
|
|
12
|
+
|
|
13
|
+
// Protocol exports
|
|
14
|
+
export { RestApiProtocol } from "./protocols/rest_api";
|
|
15
|
+
|
|
16
|
+
// Vue exports
|
|
17
|
+
export * as vue from "./vue";
|
|
18
|
+
|
|
19
|
+
export interface ProviderConfig {
|
|
20
|
+
protocols?: Protocol[];
|
|
21
|
+
auth?: AuthConfig;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default {
|
|
25
|
+
install(app: App, opts?: ProviderConfig) {
|
|
26
|
+
const reg = globalRegistry;
|
|
27
|
+
if (opts?.protocols) {
|
|
28
|
+
for (const p of opts.protocols) {
|
|
29
|
+
reg.register(p);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
app.provide("providerRegistry", reg);
|
|
33
|
+
app.provide("providerCache", globalCache);
|
|
34
|
+
app.provide("providerAuth", opts?.auth ?? {});
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { jp } from "../core/jsonpath";
|
|
2
|
+
import { renderObj, renderTpl } from "../core/templating";
|
|
3
|
+
import type {
|
|
4
|
+
Protocol,
|
|
5
|
+
ProviderItem,
|
|
6
|
+
ProviderOutput,
|
|
7
|
+
AuthConfig,
|
|
8
|
+
} from "../core/types";
|
|
9
|
+
|
|
10
|
+
export type RestApiCfg = {
|
|
11
|
+
url: string;
|
|
12
|
+
method?: "GET" | "POST";
|
|
13
|
+
headers?: Record<string, string>;
|
|
14
|
+
query?: Record<string, unknown>;
|
|
15
|
+
body?: unknown;
|
|
16
|
+
items: string; // JSONPath to array; e.g. "$.items[*]"
|
|
17
|
+
map: { label: string; value: string; meta?: Record<string, string> }; // relative to each item
|
|
18
|
+
paginate?: { cursorPath: string; param: string; maxPages?: number };
|
|
19
|
+
auth?: AuthConfig;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function buildAuthHeaders(
|
|
23
|
+
auth?: AuthConfig,
|
|
24
|
+
globalAuth?: Record<string, unknown>,
|
|
25
|
+
): Record<string, string> {
|
|
26
|
+
const headers: Record<string, string> = {};
|
|
27
|
+
|
|
28
|
+
if (!auth) return headers;
|
|
29
|
+
|
|
30
|
+
// Handle "use" reference to global auth
|
|
31
|
+
if (auth.use && globalAuth?.[auth.use]) {
|
|
32
|
+
const globalValue = globalAuth[auth.use];
|
|
33
|
+
const value =
|
|
34
|
+
typeof globalValue === "function" ? globalValue() : globalValue;
|
|
35
|
+
|
|
36
|
+
if (auth.use === "apiKey") {
|
|
37
|
+
headers["X-API-Key"] = String(value);
|
|
38
|
+
} else if (auth.use === "bearer") {
|
|
39
|
+
headers["Authorization"] = `Bearer ${value}`;
|
|
40
|
+
} else if (auth.use === "token") {
|
|
41
|
+
headers["Authorization"] = `Token ${value}`;
|
|
42
|
+
}
|
|
43
|
+
return headers;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Handle direct auth values
|
|
47
|
+
if (auth.apiKey) {
|
|
48
|
+
const value =
|
|
49
|
+
typeof auth.apiKey === "function" ? auth.apiKey() : auth.apiKey;
|
|
50
|
+
headers["X-API-Key"] = String(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (auth.bearer) {
|
|
54
|
+
const value =
|
|
55
|
+
typeof auth.bearer === "function" ? auth.bearer() : auth.bearer;
|
|
56
|
+
headers["Authorization"] = `Bearer ${value}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (auth.token) {
|
|
60
|
+
const value = typeof auth.token === "function" ? auth.token() : auth.token;
|
|
61
|
+
headers["Authorization"] = `Token ${value}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Handle custom auth fields
|
|
65
|
+
for (const [key, value] of Object.entries(auth)) {
|
|
66
|
+
if (
|
|
67
|
+
!["use", "apiKey", "bearer", "token"].includes(key) &&
|
|
68
|
+
value !== undefined
|
|
69
|
+
) {
|
|
70
|
+
const authValue = typeof value === "function" ? value() : value;
|
|
71
|
+
headers[key] = String(authValue);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return headers;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const RestApiProtocol = (): Protocol<RestApiCfg> => ({
|
|
79
|
+
protocol: "rest_api",
|
|
80
|
+
async resolve(cfg, ctx): Promise<ProviderOutput> {
|
|
81
|
+
const ac = ctx.signal;
|
|
82
|
+
const out: ProviderItem[] = [];
|
|
83
|
+
let cursor: unknown = null;
|
|
84
|
+
let page = 0;
|
|
85
|
+
do {
|
|
86
|
+
const url = new URL(renderTpl(cfg.url, { data: ctx.data, ui: ctx.ui }));
|
|
87
|
+
const q = renderObj(cfg.query ?? {}, {
|
|
88
|
+
data: ctx.data,
|
|
89
|
+
ui: ctx.ui,
|
|
90
|
+
}) as Record<string, unknown>;
|
|
91
|
+
for (const [k, v] of Object.entries(q))
|
|
92
|
+
if (v !== undefined && v !== "") url.searchParams.set(k, String(v));
|
|
93
|
+
if (cursor && cfg.paginate)
|
|
94
|
+
url.searchParams.set(cfg.paginate.param, String(cursor));
|
|
95
|
+
|
|
96
|
+
// Build headers with auth
|
|
97
|
+
const baseHeaders = renderObj(cfg.headers ?? {}, {
|
|
98
|
+
data: ctx.data,
|
|
99
|
+
}) as Record<string, string>;
|
|
100
|
+
const authHeaders = buildAuthHeaders(cfg.auth, ctx.auth);
|
|
101
|
+
const headers = { ...baseHeaders, ...authHeaders };
|
|
102
|
+
|
|
103
|
+
const method = cfg.method ?? "GET";
|
|
104
|
+
const requestInit: RequestInit = {
|
|
105
|
+
method,
|
|
106
|
+
headers,
|
|
107
|
+
signal: ac,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Only include body for non-GET requests
|
|
111
|
+
if (method !== "GET" && cfg.body) {
|
|
112
|
+
requestInit.body = JSON.stringify(
|
|
113
|
+
renderObj(cfg.body, { data: ctx.data }),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const res = await fetch(url.toString(), requestInit);
|
|
118
|
+
if (!res.ok) throw new Error(`REST ${res.status}`);
|
|
119
|
+
const json = await res.json();
|
|
120
|
+
const items = jp(json, cfg.items);
|
|
121
|
+
for (const it of items) {
|
|
122
|
+
const label = jp(it, cfg.map.label)[0];
|
|
123
|
+
const value = jp(it, cfg.map.value)[0];
|
|
124
|
+
const meta = cfg.map.meta
|
|
125
|
+
? Object.fromEntries(
|
|
126
|
+
Object.entries(cfg.map.meta).map(([k, p]) => [k, jp(it, p)[0]]),
|
|
127
|
+
)
|
|
128
|
+
: undefined;
|
|
129
|
+
out.push({ label: String(label ?? ""), value, meta });
|
|
130
|
+
}
|
|
131
|
+
cursor = cfg.paginate ? jp(json, cfg.paginate.cursorPath)[0] : null;
|
|
132
|
+
page += 1;
|
|
133
|
+
} while (cfg.paginate && cursor && page < (cfg.paginate.maxPages ?? 5));
|
|
134
|
+
return { items: out, ttl: 300 };
|
|
135
|
+
},
|
|
136
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ControlElement, JsonSchema } from "@jsonforms/core";
|
|
3
|
+
import { useJsonFormsControl } from "@jsonforms/vue";
|
|
4
|
+
import { computed, ref, watch } from "vue";
|
|
5
|
+
import { useProvider } from "../composables/useProvider";
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
uischema: ControlElement;
|
|
9
|
+
schema: JsonSchema;
|
|
10
|
+
path: string;
|
|
11
|
+
}>();
|
|
12
|
+
const { control, handleChange } = useJsonFormsControl(props);
|
|
13
|
+
|
|
14
|
+
const binding = computed(() => {
|
|
15
|
+
const provider = control.value.uischema?.options?.provider;
|
|
16
|
+
// Ensure load property is set to 'query' by default for autocomplete
|
|
17
|
+
if (provider && typeof provider === "object" && !provider.load) {
|
|
18
|
+
return { ...provider, load: "query" };
|
|
19
|
+
}
|
|
20
|
+
return provider;
|
|
21
|
+
});
|
|
22
|
+
const query = ref("");
|
|
23
|
+
const { items, loading, error, reload } = useProvider(binding, {
|
|
24
|
+
data: control.value.data,
|
|
25
|
+
path: control.value.path,
|
|
26
|
+
uiQuery: query.value,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
watch(query, () => {
|
|
30
|
+
if (binding.value?.load === "query") reload();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const value = computed({
|
|
34
|
+
get: () => control.value.data,
|
|
35
|
+
set: (v) => handleChange(control.value.path, v),
|
|
36
|
+
});
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<div class="provider-autocomplete">
|
|
41
|
+
<label>{{ control.schema.title }}</label>
|
|
42
|
+
<input
|
|
43
|
+
v-model="query"
|
|
44
|
+
:placeholder="loading ? 'Loading…' : 'Type to search…'"
|
|
45
|
+
type="text"
|
|
46
|
+
/>
|
|
47
|
+
<ul v-if="items.length">
|
|
48
|
+
<li v-for="it in items" :key="String(it.value)" @click="value = it.value">
|
|
49
|
+
{{ it.label }}
|
|
50
|
+
</li>
|
|
51
|
+
</ul>
|
|
52
|
+
<small v-if="error" role="alert">Failed: {{ error }}</small>
|
|
53
|
+
</div>
|
|
54
|
+
</template>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ControlElement, JsonSchema } from "@jsonforms/core";
|
|
3
|
+
import { useJsonFormsControl } from "@jsonforms/vue";
|
|
4
|
+
import { computed, watch } from "vue";
|
|
5
|
+
import { useProvider } from "../composables/useProvider";
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
uischema: ControlElement;
|
|
9
|
+
schema: JsonSchema;
|
|
10
|
+
path: string;
|
|
11
|
+
}>();
|
|
12
|
+
const { control, handleChange } = useJsonFormsControl(props);
|
|
13
|
+
|
|
14
|
+
const binding = computed(() => {
|
|
15
|
+
const provider = control.value.uischema?.options?.provider;
|
|
16
|
+
// Ensure load property is set to 'mount' by default
|
|
17
|
+
if (provider && typeof provider === "object" && !provider.load) {
|
|
18
|
+
return { ...provider, load: "mount" };
|
|
19
|
+
}
|
|
20
|
+
return provider;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const deps = computed(
|
|
24
|
+
() =>
|
|
25
|
+
((
|
|
26
|
+
(control.value.schema as Record<string, unknown>)?.[
|
|
27
|
+
"x-provider"
|
|
28
|
+
] as Record<string, unknown>
|
|
29
|
+
)?.dependsOn as string[]) ?? [],
|
|
30
|
+
);
|
|
31
|
+
const depValues = computed(() => deps.value.map(() => null)); // you can resolve actual values via control.value.data & pointers
|
|
32
|
+
|
|
33
|
+
const { items, loading, error, reload } = useProvider(binding, {
|
|
34
|
+
data: control.value.data,
|
|
35
|
+
path: control.value.path,
|
|
36
|
+
dependsOnValues: depValues.value,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
watch(
|
|
40
|
+
() => control.value.data,
|
|
41
|
+
() => {
|
|
42
|
+
// if dependsOn changed → reload
|
|
43
|
+
reload();
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const value = computed({
|
|
48
|
+
get: () => control.value.data,
|
|
49
|
+
set: (v) => handleChange(control.value.path, v),
|
|
50
|
+
});
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<div class="provider-select">
|
|
55
|
+
<label>{{ control.schema.title }}</label>
|
|
56
|
+
<select v-model="value">
|
|
57
|
+
<option value="" disabled>{{ loading ? "Loading…" : "Select…" }}</option>
|
|
58
|
+
<option v-for="it in items" :key="String(it.value)" :value="it.value">
|
|
59
|
+
{{ it.label }}
|
|
60
|
+
</option>
|
|
61
|
+
</select>
|
|
62
|
+
<small v-if="error" role="alert">Failed to load: {{ error }}</small>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
65
|
+
|
|
66
|
+
<style scoped>
|
|
67
|
+
.provider-select select {
|
|
68
|
+
min-width: 16rem;
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ComputedRef,
|
|
3
|
+
computed,
|
|
4
|
+
inject,
|
|
5
|
+
onBeforeUnmount,
|
|
6
|
+
type Ref,
|
|
7
|
+
ref,
|
|
8
|
+
unref,
|
|
9
|
+
watch,
|
|
10
|
+
} from "vue";
|
|
11
|
+
import { cache as globalCache } from "../../core/cache";
|
|
12
|
+
import { registry as globalRegistry } from "../../core/registry";
|
|
13
|
+
import type {
|
|
14
|
+
AuthConfig,
|
|
15
|
+
ProviderBinding,
|
|
16
|
+
ProviderItem,
|
|
17
|
+
ProviderOutput,
|
|
18
|
+
} from "../../core/types";
|
|
19
|
+
|
|
20
|
+
export function useProvider(
|
|
21
|
+
binding:
|
|
22
|
+
| ProviderBinding
|
|
23
|
+
| Ref<ProviderBinding>
|
|
24
|
+
| ComputedRef<ProviderBinding>,
|
|
25
|
+
ctxBits: {
|
|
26
|
+
data: unknown;
|
|
27
|
+
path: string;
|
|
28
|
+
dependsOnValues?: unknown[];
|
|
29
|
+
uiQuery?: string;
|
|
30
|
+
},
|
|
31
|
+
) {
|
|
32
|
+
const registry = inject("providerRegistry", globalRegistry);
|
|
33
|
+
const cache = inject("providerCache", globalCache);
|
|
34
|
+
const auth = inject("providerAuth", {}) as Record<
|
|
35
|
+
string,
|
|
36
|
+
(() => string) | string
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
const items = ref<ProviderItem[]>([]);
|
|
40
|
+
const loading = ref(false);
|
|
41
|
+
const error = ref<string | undefined>(undefined);
|
|
42
|
+
const ac = new AbortController();
|
|
43
|
+
|
|
44
|
+
const cacheKey = computed(() =>
|
|
45
|
+
JSON.stringify({
|
|
46
|
+
b: unref(binding),
|
|
47
|
+
d: ctxBits.dependsOnValues ?? [],
|
|
48
|
+
q: ctxBits.uiQuery ?? "",
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
async function load() {
|
|
53
|
+
const bindingValue = unref(binding);
|
|
54
|
+
if (!bindingValue) return;
|
|
55
|
+
loading.value = true;
|
|
56
|
+
error.value = undefined;
|
|
57
|
+
const hit = cache.get(cacheKey.value) as ProviderOutput | undefined;
|
|
58
|
+
if (hit) {
|
|
59
|
+
items.value = hit.items;
|
|
60
|
+
loading.value = false;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const driver = registry.get(bindingValue.protocol);
|
|
65
|
+
if (!driver) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`No provider registered for protocol: ${bindingValue.protocol}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const out = await driver.resolve(bindingValue.config ?? {}, {
|
|
71
|
+
data: ctxBits.data,
|
|
72
|
+
path: ctxBits.path,
|
|
73
|
+
ui: { query: ctxBits.uiQuery },
|
|
74
|
+
signal: ac.signal,
|
|
75
|
+
auth: resolveAuth(bindingValue.auth, auth),
|
|
76
|
+
});
|
|
77
|
+
items.value = out.items;
|
|
78
|
+
cache.set(cacheKey.value, out, bindingValue.cacheTTL ?? out.ttl ?? 0);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
error.value = (e as Error)?.message ?? String(e);
|
|
81
|
+
items.value = [];
|
|
82
|
+
} finally {
|
|
83
|
+
loading.value = false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
onBeforeUnmount(() => ac.abort());
|
|
88
|
+
watch(
|
|
89
|
+
[cacheKey],
|
|
90
|
+
() => {
|
|
91
|
+
const bindingValue = unref(binding);
|
|
92
|
+
if (bindingValue?.load === "mount" || bindingValue?.load === "query")
|
|
93
|
+
load();
|
|
94
|
+
},
|
|
95
|
+
{ immediate: unref(binding)?.load === "mount" },
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return { items, loading, error, reload: load };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveAuth(
|
|
102
|
+
spec: AuthConfig | undefined,
|
|
103
|
+
authBag: Record<string, (() => string) | string>,
|
|
104
|
+
): AuthConfig {
|
|
105
|
+
if (!spec) return {};
|
|
106
|
+
if (spec.use && typeof authBag[spec.use] === "function") {
|
|
107
|
+
const authFunc = authBag[spec.use] as () => string;
|
|
108
|
+
return { [spec.use]: authFunc() };
|
|
109
|
+
}
|
|
110
|
+
return spec;
|
|
111
|
+
}
|
package/src/vue/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { UISchemaElement } from "@jsonforms/core";
|
|
2
|
+
import {
|
|
3
|
+
and,
|
|
4
|
+
isIntegerControl,
|
|
5
|
+
isNumberControl,
|
|
6
|
+
isStringControl,
|
|
7
|
+
or,
|
|
8
|
+
rankWith,
|
|
9
|
+
} from "@jsonforms/core";
|
|
10
|
+
import ProviderAutocomplete from "./components/ProviderAutocomplete.vue";
|
|
11
|
+
import ProviderSelect from "./components/ProviderSelect.vue";
|
|
12
|
+
|
|
13
|
+
// Custom tester that checks if provider option exists (as object or boolean)
|
|
14
|
+
const hasProvider = (uischema: UISchemaElement) => {
|
|
15
|
+
return uischema?.options?.provider !== undefined;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Create specific testers for each component type
|
|
19
|
+
const providerSelectTester = rankWith(
|
|
20
|
+
6,
|
|
21
|
+
and(
|
|
22
|
+
or(isStringControl, isNumberControl, isIntegerControl),
|
|
23
|
+
hasProvider,
|
|
24
|
+
(uischema) => !uischema?.options?.autocomplete,
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const providerAutocompleteTester = rankWith(
|
|
29
|
+
7,
|
|
30
|
+
and(
|
|
31
|
+
or(isStringControl, isNumberControl, isIntegerControl),
|
|
32
|
+
hasProvider,
|
|
33
|
+
(uischema) => uischema?.options?.autocomplete === true,
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export const providerRenderers = [
|
|
38
|
+
{ tester: providerSelectTester, renderer: ProviderSelect },
|
|
39
|
+
{ tester: providerAutocompleteTester, renderer: ProviderAutocomplete },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
export { ProviderAutocomplete, ProviderSelect };
|
|
43
|
+
export { useProvider } from "./composables/useProvider";
|
|
44
|
+
export * from "./testers";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
and,
|
|
3
|
+
isIntegerControl,
|
|
4
|
+
isNumberControl,
|
|
5
|
+
isStringControl,
|
|
6
|
+
or,
|
|
7
|
+
rankWith,
|
|
8
|
+
} from "@jsonforms/core";
|
|
9
|
+
|
|
10
|
+
// Tester that checks if provider option exists (as object or boolean)
|
|
11
|
+
export const providerTester = rankWith(
|
|
12
|
+
5,
|
|
13
|
+
and(
|
|
14
|
+
or(isStringControl, isNumberControl, isIntegerControl),
|
|
15
|
+
(uischema: import("@jsonforms/core").UISchemaElement) =>
|
|
16
|
+
uischema?.options?.provider !== undefined &&
|
|
17
|
+
uischema?.options?.provider !== null,
|
|
18
|
+
),
|
|
19
|
+
);
|