nucleus-core-ts 0.9.791 → 0.9.793
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/MappingsTab.js +36 -2
- package/dist/fe/components/IntegrationsPage/components/ParamBindingRow.js +2 -1
- package/dist/fe/components/IntegrationsPage/components/formSupport.d.ts +17 -1
- package/dist/fe/components/IntegrationsPage/components/formSupport.js +52 -1
- package/dist/fe/components/IntegrationsPage/components/formSupport.test.d.ts +1 -0
- package/dist/fe/components/IntegrationsPage/components/formSupport.test.js +156 -0
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useEffectEvent, useState } from 'react';
|
|
4
4
|
import { useIntegrationsStore } from '../store';
|
|
5
5
|
import { integrationsPageTheme } from '../theme';
|
|
6
|
+
import { fieldsOfStoredSample } from './formSupport';
|
|
6
7
|
import { MappingEditor } from './MappingEditor';
|
|
7
8
|
import { MappingList } from './MappingList';
|
|
8
9
|
import { blankMappingDraft, toMappingDraft } from './mappingDraft';
|
|
@@ -42,6 +43,32 @@ export function MappingsTab({ actions }) {
|
|
|
42
43
|
useEffect(()=>{
|
|
43
44
|
loadTargets();
|
|
44
45
|
}, []);
|
|
46
|
+
/**
|
|
47
|
+
* The connection's endpoints, when nothing has fetched them yet.
|
|
48
|
+
*
|
|
49
|
+
* `store.endpoints` is filled by the Endpoints tab, so opening a connection
|
|
50
|
+
* and going straight to Mappings met "No endpoints on this connection yet"
|
|
51
|
+
* and a disabled New mapping — about a connection with three of them. The
|
|
52
|
+
* only way through was to visit the other tab first, which nothing said.
|
|
53
|
+
*
|
|
54
|
+
* One page is enough to offer a source and to name one; the Endpoints tab
|
|
55
|
+
* remains the place that pages through them all.
|
|
56
|
+
*/ const loadEndpointsIfMissing = useEffectEvent(()=>{
|
|
57
|
+
if (sourceId === '' || store.endpoints.length > 0) return;
|
|
58
|
+
actions.listEndpoints(sourceId, {
|
|
59
|
+
page: 1,
|
|
60
|
+
limit: 50
|
|
61
|
+
}).then((result)=>{
|
|
62
|
+
store.setEndpoints(result.items, result.meta?.totalItems ?? result.items.length);
|
|
63
|
+
}).catch((error)=>{
|
|
64
|
+
store.setError(messageOf(error));
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
useEffect(()=>{
|
|
68
|
+
loadEndpointsIfMissing();
|
|
69
|
+
}, [
|
|
70
|
+
sourceId
|
|
71
|
+
]);
|
|
45
72
|
const startNew = ()=>{
|
|
46
73
|
setSelected(null);
|
|
47
74
|
setCreating(true);
|
|
@@ -88,8 +115,15 @@ export function MappingsTab({ actions }) {
|
|
|
88
115
|
};
|
|
89
116
|
const draft = creating ? blankMappingDraft(newMappingEndpointId) : selected ? toMappingDraft(selected) : null;
|
|
90
117
|
// A sample describes ONE endpoint. Offering its field names against a mapping
|
|
91
|
-
// that reads from a different one would suggest columns that never arrive
|
|
92
|
-
|
|
118
|
+
// that reads from a different one would suggest columns that never arrive —
|
|
119
|
+
// so both the in-session sample and the stored one are matched to the
|
|
120
|
+
// mapping's OWN endpoint, never to whatever happens to be selected elsewhere.
|
|
121
|
+
//
|
|
122
|
+
// The stored sample is the fallback rather than an afterthought: it is why the
|
|
123
|
+
// sample is persisted at all. Reading only the in-session one meant a
|
|
124
|
+
// connection revisited tomorrow claimed it had never been sampled.
|
|
125
|
+
const draftEndpoint = draft ? store.endpoints.find((e)=>e.id === draft.endpointId) : undefined;
|
|
126
|
+
const sourceFields = draft && store.selectedEndpointId === draft.endpointId && store.sample?.fields?.length ? store.sample.fields : fieldsOfStoredSample(draftEndpoint?.sampleResponse);
|
|
93
127
|
return /*#__PURE__*/ _jsxs("section", {
|
|
94
128
|
className: "flex flex-col gap-4",
|
|
95
129
|
children: [
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useId } from 'react';
|
|
4
4
|
import { integrationsPageTheme as theme } from '../theme';
|
|
5
|
+
import { endpointLabel } from './formSupport';
|
|
5
6
|
import { Field } from './Primitives';
|
|
6
7
|
export function ParamBindingRow({ binding, onChange, onRemove, siblings, entities, disabled }) {
|
|
7
8
|
const rowId = useId();
|
|
@@ -107,7 +108,7 @@ export function ParamBindingRow({ binding, onChange, onRemove, siblings, entitie
|
|
|
107
108
|
value: binding.endpointId,
|
|
108
109
|
children: siblings.map((sibling)=>/*#__PURE__*/ _jsx("option", {
|
|
109
110
|
value: sibling.id,
|
|
110
|
-
children: sibling
|
|
111
|
+
children: endpointLabel(sibling)
|
|
111
112
|
}, sibling.id))
|
|
112
113
|
})
|
|
113
114
|
}),
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The small things every form in this panel needs and none of them deserves a file.
|
|
3
3
|
*/
|
|
4
|
+
import type { IntegrationEndpoint } from '../types';
|
|
5
|
+
/**
|
|
6
|
+
* The field names an endpoint's STORED sample reports.
|
|
7
|
+
*
|
|
8
|
+
* A sample is persisted on the endpoint precisely so the field list survives a
|
|
9
|
+
* reload, but the mapping editor was reading only the sample taken in this
|
|
10
|
+
* browser session. Come back to a connection tomorrow and it said "sample the
|
|
11
|
+
* endpoint first" about an endpoint it had already sampled and saved — so the
|
|
12
|
+
* source names had to be typed from memory, which is the one thing the sample
|
|
13
|
+
* exists to prevent.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors the engine's `flattenKeys` rather than calling it: that lives in the
|
|
16
|
+
* server bundle, which this kit cannot import.
|
|
17
|
+
*/
|
|
18
|
+
export declare function fieldsOfStoredSample(sample: IntegrationEndpoint['sampleResponse']): string[];
|
|
19
|
+
export declare function endpointLabel(endpoint: IntegrationEndpoint): string;
|
|
4
20
|
/**
|
|
5
21
|
* The ids a control should point `aria-describedby` at.
|
|
6
22
|
*
|
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The small things every form in this panel needs and none of them deserves a file.
|
|
3
3
|
*/ /**
|
|
4
|
+
* How an endpoint should read in a list someone has to choose from.
|
|
5
|
+
*
|
|
6
|
+
* The parameter-binding picker showed `tag`, which groups the endpoints of one
|
|
7
|
+
* API — so a source whose endpoints are all tagged `corporate-portal` offered
|
|
8
|
+
* three identical options and no way to tell which one supplied the value.
|
|
9
|
+
* The name is what distinguishes them; the path is what distinguishes them when
|
|
10
|
+
* nobody typed a name, since it is the one field that cannot be blank.
|
|
11
|
+
*/ /** Same caps as the engine's `flattenKeys`: this feeds a dropdown, not an analysis. */ const MAX_FIELDS_LISTED = 200;
|
|
12
|
+
const MAX_FIELD_DEPTH = 4;
|
|
13
|
+
function walkKeys(value, prefix, out, depth) {
|
|
14
|
+
if (out.length > MAX_FIELDS_LISTED || depth > MAX_FIELD_DEPTH) return out;
|
|
15
|
+
if (value == null || typeof value !== 'object') return out;
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
if (prefix) out.push(prefix);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
for (const [key, child] of Object.entries(value)){
|
|
21
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
22
|
+
if (child != null && typeof child === 'object' && !Array.isArray(child)) {
|
|
23
|
+
walkKeys(child, path, out, depth + 1);
|
|
24
|
+
} else {
|
|
25
|
+
out.push(path);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The field names an endpoint's STORED sample reports.
|
|
32
|
+
*
|
|
33
|
+
* A sample is persisted on the endpoint precisely so the field list survives a
|
|
34
|
+
* reload, but the mapping editor was reading only the sample taken in this
|
|
35
|
+
* browser session. Come back to a connection tomorrow and it said "sample the
|
|
36
|
+
* endpoint first" about an endpoint it had already sampled and saved — so the
|
|
37
|
+
* source names had to be typed from memory, which is the one thing the sample
|
|
38
|
+
* exists to prevent.
|
|
39
|
+
*
|
|
40
|
+
* Mirrors the engine's `flattenKeys` rather than calling it: that lives in the
|
|
41
|
+
* server bundle, which this kit cannot import.
|
|
42
|
+
*/ export function fieldsOfStoredSample(sample) {
|
|
43
|
+
const first = sample?.records?.[0];
|
|
44
|
+
if (!first || typeof first !== 'object') return [];
|
|
45
|
+
return walkKeys(first, '', [], 0);
|
|
46
|
+
}
|
|
47
|
+
export function endpointLabel(endpoint) {
|
|
48
|
+
const name = endpoint.name?.trim();
|
|
49
|
+
if (name) return name;
|
|
50
|
+
const path = endpoint.path?.trim();
|
|
51
|
+
if (path) return `${endpoint.method} ${path}`;
|
|
52
|
+
return endpoint.tag;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
4
55
|
* The ids a control should point `aria-describedby` at.
|
|
5
56
|
*
|
|
6
57
|
* {@link Field} renders its hint as `<id>-hint` and its error as `<id>-error`,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { describedBy, endpointLabel, errorText, fieldsOfStoredSample } from './formSupport';
|
|
3
|
+
const endpoint = (over)=>({
|
|
4
|
+
id: 'e1',
|
|
5
|
+
sourceId: 's1',
|
|
6
|
+
tag: 'corporate-portal',
|
|
7
|
+
method: 'GET',
|
|
8
|
+
path: '/api/v1/ext/corporate-portal/company',
|
|
9
|
+
...over
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Seen while wiring a chained import against a real API: every endpoint of one
|
|
13
|
+
* service shares a tag, and the binding picker labelled its options with that
|
|
14
|
+
* tag — three entries reading "corporate-portal" and no way to tell which one
|
|
15
|
+
* feeds the parameter.
|
|
16
|
+
*/ describe('endpointLabel', ()=>{
|
|
17
|
+
test('the name is what distinguishes one endpoint from its siblings', ()=>{
|
|
18
|
+
expect(endpointLabel(endpoint({
|
|
19
|
+
name: 'Şirketler'
|
|
20
|
+
}))).toBe('Şirketler');
|
|
21
|
+
});
|
|
22
|
+
test('without a name, the method and path do — never the shared tag', ()=>{
|
|
23
|
+
const label = endpointLabel(endpoint({
|
|
24
|
+
name: null
|
|
25
|
+
}));
|
|
26
|
+
expect(label).toBe('GET /api/v1/ext/corporate-portal/company');
|
|
27
|
+
expect(label).not.toBe('corporate-portal');
|
|
28
|
+
});
|
|
29
|
+
test('a name of only spaces is not a name', ()=>{
|
|
30
|
+
expect(endpointLabel(endpoint({
|
|
31
|
+
name: ' '
|
|
32
|
+
}))).toBe('GET /api/v1/ext/corporate-portal/company');
|
|
33
|
+
});
|
|
34
|
+
test('the tag is the last resort, not the first', ()=>{
|
|
35
|
+
expect(endpointLabel(endpoint({
|
|
36
|
+
name: null,
|
|
37
|
+
path: ''
|
|
38
|
+
}))).toBe('corporate-portal');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
/**
|
|
42
|
+
* The sample is persisted on the endpoint so the field list survives a reload.
|
|
43
|
+
* The mapping editor was reading only the sample taken in the current browser
|
|
44
|
+
* session, so a connection revisited later said "sample the endpoint first"
|
|
45
|
+
* about an endpoint it had already sampled and stored.
|
|
46
|
+
*/ describe('fieldsOfStoredSample', ()=>{
|
|
47
|
+
test('reads the field names off the stored first record', ()=>{
|
|
48
|
+
const fields = fieldsOfStoredSample({
|
|
49
|
+
records: [
|
|
50
|
+
{
|
|
51
|
+
companyCode: 'TAT',
|
|
52
|
+
companyName: 'TATMETAL ÇELİK SAN. VE TİC.A.Ş.',
|
|
53
|
+
organizationCode: '010',
|
|
54
|
+
organizationName: 'YÖNETİM KURULU',
|
|
55
|
+
parentOrganizationCode: '*'
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
});
|
|
59
|
+
expect(fields).toEqual([
|
|
60
|
+
'companyCode',
|
|
61
|
+
'companyName',
|
|
62
|
+
'organizationCode',
|
|
63
|
+
'organizationName',
|
|
64
|
+
'parentOrganizationCode'
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
test('nested objects become dot-paths, the way the engine reports them', ()=>{
|
|
68
|
+
const fields = fieldsOfStoredSample({
|
|
69
|
+
records: [
|
|
70
|
+
{
|
|
71
|
+
a: 1,
|
|
72
|
+
b: {
|
|
73
|
+
c: 2,
|
|
74
|
+
d: {
|
|
75
|
+
e: 3
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
});
|
|
81
|
+
expect(fields).toEqual([
|
|
82
|
+
'a',
|
|
83
|
+
'b.c',
|
|
84
|
+
'b.d.e'
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
test('an array is a leaf — its contents are a shape, not a field name', ()=>{
|
|
88
|
+
const fields = fieldsOfStoredSample({
|
|
89
|
+
records: [
|
|
90
|
+
{
|
|
91
|
+
organizationName: 'GENEL MÜDÜRLÜK',
|
|
92
|
+
subOrganization: [
|
|
93
|
+
{
|
|
94
|
+
x: 1
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
});
|
|
100
|
+
expect(fields).toEqual([
|
|
101
|
+
'organizationName',
|
|
102
|
+
'subOrganization'
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
test('nothing stored yields nothing, rather than throwing', ()=>{
|
|
106
|
+
expect(fieldsOfStoredSample(null)).toEqual([]);
|
|
107
|
+
expect(fieldsOfStoredSample(undefined)).toEqual([]);
|
|
108
|
+
expect(fieldsOfStoredSample({
|
|
109
|
+
records: []
|
|
110
|
+
})).toEqual([]);
|
|
111
|
+
expect(fieldsOfStoredSample({
|
|
112
|
+
records: [
|
|
113
|
+
'not an object'
|
|
114
|
+
]
|
|
115
|
+
})).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
test('depth is capped, so a deep document cannot flood the picker', ()=>{
|
|
118
|
+
const deep = {
|
|
119
|
+
l1: {
|
|
120
|
+
l2: {
|
|
121
|
+
l3: {
|
|
122
|
+
l4: {
|
|
123
|
+
l5: {
|
|
124
|
+
l6: 'too far'
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
expect(fieldsOfStoredSample({
|
|
132
|
+
records: [
|
|
133
|
+
deep
|
|
134
|
+
]
|
|
135
|
+
})).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('describedBy', ()=>{
|
|
139
|
+
test('names only the ids that were rendered', ()=>{
|
|
140
|
+
expect(describedBy('f', true, true)).toBe('f-hint f-error');
|
|
141
|
+
expect(describedBy('f', true, false)).toBe('f-hint');
|
|
142
|
+
expect(describedBy('f', false, true)).toBe('f-error');
|
|
143
|
+
expect(describedBy('f', false, false)).toBeUndefined();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe('errorText', ()=>{
|
|
147
|
+
test("the server's own wording is kept, because only it knows what was wrong", ()=>{
|
|
148
|
+
expect(errorText(new Error('Token request failed'), 'fallback')).toBe('Token request failed');
|
|
149
|
+
expect(errorText('plain reason', 'fallback')).toBe('plain reason');
|
|
150
|
+
});
|
|
151
|
+
test('an empty or unrecognised reason falls back rather than saying nothing', ()=>{
|
|
152
|
+
expect(errorText(new Error(''), 'fallback')).toBe('fallback');
|
|
153
|
+
expect(errorText(undefined, 'fallback')).toBe('fallback');
|
|
154
|
+
expect(errorText({}, 'fallback')).toBe('fallback');
|
|
155
|
+
});
|
|
156
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nucleus-core-ts",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.793",
|
|
4
4
|
"description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
|
|
5
5
|
"author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|