nucleus-core-ts 0.9.796 → 0.9.798
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/fe/components/IntegrationsPage/components/EndpointForm.js +21 -2
- package/dist/fe/components/IntegrationsPage/components/EndpointFormFields.d.ts +10 -1
- package/dist/fe/components/IntegrationsPage/components/EndpointFormFields.js +38 -1
- package/dist/fe/components/IntegrationsPage/components/RunPlanIssues.js +5 -1
- package/dist/fe/components/IntegrationsPage/components/jsonObjectField.d.ts +27 -0
- package/dist/fe/components/IntegrationsPage/components/jsonObjectField.js +49 -0
- package/dist/fe/components/IntegrationsPage/components/jsonObjectField.test.d.ts +1 -0
- package/dist/fe/components/IntegrationsPage/components/jsonObjectField.test.js +86 -0
- package/dist/fe/components/IntegrationsPage/types/results.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/src/Services/Integrations/types.d.ts +9 -0
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import { useEffect, useEffectEvent, useState } from 'react';
|
|
|
4
4
|
import { useIntegrationsStore } from '../store';
|
|
5
5
|
import { integrationsPageTheme } from '../theme';
|
|
6
6
|
import { EndpointFormFields } from './EndpointFormFields';
|
|
7
|
+
import { formatJsonObject, parseJsonObject } from './jsonObjectField';
|
|
7
8
|
import { PaginationFields } from './PaginationFields';
|
|
8
9
|
import { ParamBindingRows } from './ParamBindingRows';
|
|
9
10
|
import { pathParamNames } from './pathParams';
|
|
@@ -30,7 +31,9 @@ export function EndpointForm({ sourceId, endpoint, actions, onSaved, onDeleted,
|
|
|
30
31
|
pagination: endpoint?.pagination ?? {
|
|
31
32
|
kind: 'none'
|
|
32
33
|
},
|
|
33
|
-
paramBindings: endpoint?.paramBindings ?? []
|
|
34
|
+
paramBindings: endpoint?.paramBindings ?? [],
|
|
35
|
+
queryParams: formatJsonObject(endpoint?.queryParams),
|
|
36
|
+
requestBody: formatJsonObject(endpoint?.requestBody)
|
|
34
37
|
});
|
|
35
38
|
// Which tables a binding may read a column from. Loaded once; the list is
|
|
36
39
|
// small and an operator picking a parameter source should not wait for it.
|
|
@@ -59,7 +62,18 @@ export function EndpointForm({ sourceId, endpoint, actions, onSaved, onDeleted,
|
|
|
59
62
|
const tag = draft.tag.trim();
|
|
60
63
|
const path = draft.path.trim();
|
|
61
64
|
const busy = saving || deleting;
|
|
62
|
-
|
|
65
|
+
// Parsed here rather than on save so the message appears under the field
|
|
66
|
+
// being typed in. A body stored as broken text fails at RUN time, against a
|
|
67
|
+
// live service, with an error that names neither this field nor this screen.
|
|
68
|
+
const parsedQuery = parseJsonObject(draft.queryParams);
|
|
69
|
+
const parsedBody = parseJsonObject(draft.requestBody);
|
|
70
|
+
const errors = {
|
|
71
|
+
queryParams: parsedQuery.ok ? undefined : parsedQuery.reason,
|
|
72
|
+
// A GET carries no body, so text left behind in that field cannot block a
|
|
73
|
+
// save it will never be part of.
|
|
74
|
+
requestBody: draft.method === 'GET' || parsedBody.ok ? undefined : parsedBody.reason
|
|
75
|
+
};
|
|
76
|
+
const canSave = tag.length > 0 && path.length > 0 && !errors.queryParams && !errors.requestBody;
|
|
63
77
|
const handleSubmit = (event)=>{
|
|
64
78
|
event.preventDefault();
|
|
65
79
|
if (!canSave || busy) return;
|
|
@@ -79,6 +93,10 @@ export function EndpointForm({ sourceId, endpoint, actions, onSaved, onDeleted,
|
|
|
79
93
|
responseRootPath: draft.responseRootPath.trim() || null,
|
|
80
94
|
pagination: draft.pagination,
|
|
81
95
|
paramBindings: draft.paramBindings,
|
|
96
|
+
queryParams: parsedQuery.ok ? parsedQuery.value : null,
|
|
97
|
+
// A GET's body is dropped rather than stored: the engine never sends one,
|
|
98
|
+
// and a value kept out of sight is a value that surprises someone later.
|
|
99
|
+
requestBody: draft.method === 'GET' ? null : parsedBody.ok ? parsedBody.value : null,
|
|
82
100
|
enabled: draft.enabled
|
|
83
101
|
}).then((saved)=>{
|
|
84
102
|
setSaving(false);
|
|
@@ -114,6 +132,7 @@ export function EndpointForm({ sourceId, endpoint, actions, onSaved, onDeleted,
|
|
|
114
132
|
/*#__PURE__*/ _jsx(EndpointFormFields, {
|
|
115
133
|
disabled: busy,
|
|
116
134
|
draft: draft,
|
|
135
|
+
errors: errors,
|
|
117
136
|
onChange: set
|
|
118
137
|
}),
|
|
119
138
|
/*#__PURE__*/ _jsx(PaginationFields, {
|
|
@@ -10,10 +10,19 @@ export type EndpointDraft = {
|
|
|
10
10
|
pagination: PaginationProfileValue;
|
|
11
11
|
/** Where each path/query parameter of this endpoint gets its value. */
|
|
12
12
|
paramBindings: ParamBindingValue[];
|
|
13
|
+
/** JSON as typed. Parsed on save so a broken object cannot be stored. */
|
|
14
|
+
queryParams: string;
|
|
15
|
+
requestBody: string;
|
|
16
|
+
};
|
|
17
|
+
/** What could not be parsed, keyed the way the draft names it. */
|
|
18
|
+
export type EndpointDraftErrors = {
|
|
19
|
+
queryParams?: string;
|
|
20
|
+
requestBody?: string;
|
|
13
21
|
};
|
|
14
22
|
export type EndpointFormFieldsProps = {
|
|
15
23
|
draft: EndpointDraft;
|
|
16
24
|
disabled?: boolean;
|
|
25
|
+
errors?: EndpointDraftErrors;
|
|
17
26
|
onChange: (patch: Partial<EndpointDraft>) => void;
|
|
18
27
|
};
|
|
19
|
-
export declare function EndpointFormFields({ draft, disabled, onChange }: EndpointFormFieldsProps): import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
export declare function EndpointFormFields({ draft, disabled, errors, onChange }: EndpointFormFieldsProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { cn } from '../../../utils/cn';
|
|
4
4
|
import { integrationsPageTheme } from '../theme';
|
|
5
|
+
import { describedBy } from './formSupport';
|
|
5
6
|
import { Field } from './Primitives';
|
|
6
7
|
/**
|
|
7
8
|
* The plain fields of an endpoint.
|
|
@@ -16,7 +17,7 @@ const METHODS = [
|
|
|
16
17
|
'PATCH',
|
|
17
18
|
'DELETE'
|
|
18
19
|
];
|
|
19
|
-
export function EndpointFormFields({ draft, disabled, onChange }) {
|
|
20
|
+
export function EndpointFormFields({ draft, disabled, errors, onChange }) {
|
|
20
21
|
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
21
22
|
children: [
|
|
22
23
|
/*#__PURE__*/ _jsxs("div", {
|
|
@@ -122,6 +123,42 @@ export function EndpointFormFields({ draft, disabled, onChange }) {
|
|
|
122
123
|
value: draft.responseRootPath
|
|
123
124
|
})
|
|
124
125
|
}),
|
|
126
|
+
/*#__PURE__*/ _jsx(Field, {
|
|
127
|
+
error: errors?.queryParams,
|
|
128
|
+
hint: "Sent with every call, as JSON. A parameter bound below overrides the same name here.",
|
|
129
|
+
htmlFor: "endpoint-query",
|
|
130
|
+
label: "Fixed query parameters",
|
|
131
|
+
children: /*#__PURE__*/ _jsx("textarea", {
|
|
132
|
+
"aria-describedby": describedBy('endpoint-query', true, Boolean(errors?.queryParams)),
|
|
133
|
+
className: cn(theme.field.input, 'h-auto min-h-16 py-2 font-mono text-xs'),
|
|
134
|
+
disabled: disabled,
|
|
135
|
+
id: "endpoint-query",
|
|
136
|
+
onChange: (event)=>onChange({
|
|
137
|
+
queryParams: event.target.value
|
|
138
|
+
}),
|
|
139
|
+
placeholder: '{"state": "ACTIVE"}',
|
|
140
|
+
rows: 3,
|
|
141
|
+
value: draft.queryParams
|
|
142
|
+
})
|
|
143
|
+
}),
|
|
144
|
+
draft.method === 'GET' ? null : /*#__PURE__*/ _jsx(Field, {
|
|
145
|
+
error: errors?.requestBody,
|
|
146
|
+
hint: "The JSON body sent with this call. Some list endpoints are a POST whose body decides what comes back.",
|
|
147
|
+
htmlFor: "endpoint-body",
|
|
148
|
+
label: "Request body",
|
|
149
|
+
children: /*#__PURE__*/ _jsx("textarea", {
|
|
150
|
+
"aria-describedby": describedBy('endpoint-body', true, Boolean(errors?.requestBody)),
|
|
151
|
+
className: cn(theme.field.input, 'h-auto min-h-20 py-2 font-mono text-xs'),
|
|
152
|
+
disabled: disabled,
|
|
153
|
+
id: "endpoint-body",
|
|
154
|
+
onChange: (event)=>onChange({
|
|
155
|
+
requestBody: event.target.value
|
|
156
|
+
}),
|
|
157
|
+
placeholder: '{"employeeTypes": ["A", "S"]}',
|
|
158
|
+
rows: 4,
|
|
159
|
+
value: draft.requestBody
|
|
160
|
+
})
|
|
161
|
+
}),
|
|
125
162
|
/*#__PURE__*/ _jsx(Field, {
|
|
126
163
|
htmlFor: "endpoint-description",
|
|
127
164
|
label: "Description",
|
|
@@ -43,9 +43,13 @@ export function RunPlanIssues({ issues }) {
|
|
|
43
43
|
className: theme.field.hint,
|
|
44
44
|
children: `Dedup key: ${issue.dedupKey}`
|
|
45
45
|
}) : null,
|
|
46
|
+
issue.keptRow ? /*#__PURE__*/ _jsx("span", {
|
|
47
|
+
className: theme.field.hint,
|
|
48
|
+
children: `Kept — ${describeRow(issue.keptRow)}`
|
|
49
|
+
}) : null,
|
|
46
50
|
issue.row ? /*#__PURE__*/ _jsx("span", {
|
|
47
51
|
className: theme.field.hint,
|
|
48
|
-
children: describeRow(issue.row)
|
|
52
|
+
children: issue.keptRow ? `Skipped — ${describeRow(issue.row)}` : describeRow(issue.row)
|
|
49
53
|
}) : null
|
|
50
54
|
]
|
|
51
55
|
}, `${issue.sourceIndex}-${issue.kind}-${index}`))
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A JSON object typed into a form field.
|
|
3
|
+
*
|
|
4
|
+
* Query parameters and a request body are `Record<string, unknown>` on the wire
|
|
5
|
+
* — an endpoint may need `{"employeeTypes": ["A", "S"]}`, which no key/value
|
|
6
|
+
* grid of strings can express. So the field takes JSON, and the only question
|
|
7
|
+
* is whether a form is allowed to save what it cannot parse. It is not: a body
|
|
8
|
+
* saved as broken text fails at RUN time, against a live service, with an error
|
|
9
|
+
* from somewhere else entirely.
|
|
10
|
+
*/
|
|
11
|
+
export type JsonObjectResult = {
|
|
12
|
+
ok: true;
|
|
13
|
+
value: Record<string, unknown> | null;
|
|
14
|
+
} | {
|
|
15
|
+
ok: false;
|
|
16
|
+
reason: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Parses the text of a JSON-object field.
|
|
20
|
+
*
|
|
21
|
+
* Empty is `null` rather than `{}` — "nothing configured" and "an empty object
|
|
22
|
+
* configured" read the same on screen but not in a request, and the column is
|
|
23
|
+
* nullable precisely so the difference survives.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseJsonObject(text: string): JsonObjectResult;
|
|
26
|
+
/** What a stored object looks like back in the field. */
|
|
27
|
+
export declare function formatJsonObject(value: Record<string, unknown> | null | undefined): string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A JSON object typed into a form field.
|
|
3
|
+
*
|
|
4
|
+
* Query parameters and a request body are `Record<string, unknown>` on the wire
|
|
5
|
+
* — an endpoint may need `{"employeeTypes": ["A", "S"]}`, which no key/value
|
|
6
|
+
* grid of strings can express. So the field takes JSON, and the only question
|
|
7
|
+
* is whether a form is allowed to save what it cannot parse. It is not: a body
|
|
8
|
+
* saved as broken text fails at RUN time, against a live service, with an error
|
|
9
|
+
* from somewhere else entirely.
|
|
10
|
+
*/ /**
|
|
11
|
+
* Parses the text of a JSON-object field.
|
|
12
|
+
*
|
|
13
|
+
* Empty is `null` rather than `{}` — "nothing configured" and "an empty object
|
|
14
|
+
* configured" read the same on screen but not in a request, and the column is
|
|
15
|
+
* nullable precisely so the difference survives.
|
|
16
|
+
*/ export function parseJsonObject(text) {
|
|
17
|
+
const trimmed = text.trim();
|
|
18
|
+
if (trimmed === '') return {
|
|
19
|
+
ok: true,
|
|
20
|
+
value: null
|
|
21
|
+
};
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = JSON.parse(trimmed);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return {
|
|
27
|
+
ok: false,
|
|
28
|
+
reason: error instanceof Error ? error.message : 'This is not valid JSON.'
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (parsed === null) return {
|
|
32
|
+
ok: true,
|
|
33
|
+
value: null
|
|
34
|
+
};
|
|
35
|
+
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
reason: 'Expected an object — {"key": "value"}.'
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
ok: true,
|
|
43
|
+
value: parsed
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** What a stored object looks like back in the field. */ export function formatJsonObject(value) {
|
|
47
|
+
if (!value || Object.keys(value).length === 0) return '';
|
|
48
|
+
return JSON.stringify(value, null, 2);
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { formatJsonObject, parseJsonObject } from './jsonObjectField';
|
|
3
|
+
/**
|
|
4
|
+
* The HR service's employee list is a POST whose body selects which employees
|
|
5
|
+
* come back — `{"employeeTypes": ["A", "S"]}`. The engine has always sent a
|
|
6
|
+
* stored body; the panel had no field for one, so that endpoint could be
|
|
7
|
+
* described everywhere except in the one place that decides what it returns.
|
|
8
|
+
*/ describe('parseJsonObject', ()=>{
|
|
9
|
+
test('an object comes back as an object', ()=>{
|
|
10
|
+
expect(parseJsonObject('{"employeeTypes": ["A", "S"]}')).toEqual({
|
|
11
|
+
ok: true,
|
|
12
|
+
value: {
|
|
13
|
+
employeeTypes: [
|
|
14
|
+
'A',
|
|
15
|
+
'S'
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
test('empty means nothing configured, not an empty object', ()=>{
|
|
21
|
+
expect(parseJsonObject('')).toEqual({
|
|
22
|
+
ok: true,
|
|
23
|
+
value: null
|
|
24
|
+
});
|
|
25
|
+
expect(parseJsonObject(' \n ')).toEqual({
|
|
26
|
+
ok: true,
|
|
27
|
+
value: null
|
|
28
|
+
});
|
|
29
|
+
expect(parseJsonObject('null')).toEqual({
|
|
30
|
+
ok: true,
|
|
31
|
+
value: null
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
test('broken JSON is refused here rather than at run time', ()=>{
|
|
35
|
+
const result = parseJsonObject('{"a": }');
|
|
36
|
+
expect(result.ok).toBe(false);
|
|
37
|
+
if (!result.ok) expect(result.reason.length).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
test('an array or a scalar is not a body — both are refused', ()=>{
|
|
40
|
+
expect(parseJsonObject('[1, 2]').ok).toBe(false);
|
|
41
|
+
expect(parseJsonObject('"text"').ok).toBe(false);
|
|
42
|
+
expect(parseJsonObject('42').ok).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
test('nested values survive, which is why this is JSON and not a key/value grid', ()=>{
|
|
45
|
+
const result = parseJsonObject('{"filter": {"state": ["ACTIVE"], "limit": 50}}');
|
|
46
|
+
expect(result).toEqual({
|
|
47
|
+
ok: true,
|
|
48
|
+
value: {
|
|
49
|
+
filter: {
|
|
50
|
+
state: [
|
|
51
|
+
'ACTIVE'
|
|
52
|
+
],
|
|
53
|
+
limit: 50
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('formatJsonObject', ()=>{
|
|
60
|
+
test('a stored object comes back readable', ()=>{
|
|
61
|
+
expect(formatJsonObject({
|
|
62
|
+
employeeTypes: [
|
|
63
|
+
'A'
|
|
64
|
+
]
|
|
65
|
+
})).toBe('{\n "employeeTypes": [\n "A"\n ]\n}');
|
|
66
|
+
});
|
|
67
|
+
test('nothing stored shows an empty field, not the word null or a bare {}', ()=>{
|
|
68
|
+
expect(formatJsonObject(null)).toBe('');
|
|
69
|
+
expect(formatJsonObject(undefined)).toBe('');
|
|
70
|
+
expect(formatJsonObject({})).toBe('');
|
|
71
|
+
});
|
|
72
|
+
test('a round trip through the form changes nothing', ()=>{
|
|
73
|
+
const original = {
|
|
74
|
+
employeeTypes: [
|
|
75
|
+
'A',
|
|
76
|
+
'S'
|
|
77
|
+
],
|
|
78
|
+
employeeNo: '101307'
|
|
79
|
+
};
|
|
80
|
+
const result = parseJsonObject(formatJsonObject(original));
|
|
81
|
+
expect(result).toEqual({
|
|
82
|
+
ok: true,
|
|
83
|
+
value: original
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -16,6 +16,8 @@ export type RunIssueValue = {
|
|
|
16
16
|
* narrow and rows are being lost. The values are the difference.
|
|
17
17
|
*/
|
|
18
18
|
row?: Record<string, unknown>;
|
|
19
|
+
/** The record that claimed this key first — what the skipped one lost to. */
|
|
20
|
+
keptRow?: Record<string, unknown>;
|
|
19
21
|
};
|
|
20
22
|
/** What a run would do, computed without writing. */
|
|
21
23
|
export type RunPlanValue = {
|