nucleus-core-ts 0.9.789 → 0.9.791
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/IntegrationsPage.js +20 -4
- package/dist/fe/components/IntegrationsPage/components/connectionDraft.js +10 -1
- package/dist/fe/components/IntegrationsPage/components/connectionDraft.test.d.ts +1 -0
- package/dist/fe/components/IntegrationsPage/components/connectionDraft.test.js +133 -0
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useGSAP } from '@gsap/react';
|
|
4
4
|
import gsap from 'gsap';
|
|
5
|
-
import { useEffect, useEffectEvent, useRef } from 'react';
|
|
5
|
+
import { useEffect, useEffectEvent, useRef, useState } from 'react';
|
|
6
6
|
import { cn } from '../../../utils/cn';
|
|
7
7
|
import { useIntegrationsStore } from '../store';
|
|
8
8
|
import { integrationsPageTheme } from '../theme';
|
|
@@ -26,6 +26,19 @@ export function IntegrationsPage({ title = 'Integrations', subtitle = 'Connect a
|
|
|
26
26
|
const store = useIntegrationsStore();
|
|
27
27
|
const containerRef = useRef(null);
|
|
28
28
|
const contentRef = useRef(null);
|
|
29
|
+
/**
|
|
30
|
+
* Bumped after a connection is written, to pull the rail again.
|
|
31
|
+
*
|
|
32
|
+
* `ConnectionList` and `ConnectionSettingsTab` were both built to be told
|
|
33
|
+
* about a save and neither was ever told: the shell mounted them with only
|
|
34
|
+
* `actions`. Creating the first connection therefore selected a record the
|
|
35
|
+
* rail had never fetched, so the rail kept saying "0 connections" while the
|
|
36
|
+
* detail treated the missing record as still loading and sat on skeletons
|
|
37
|
+
* until the page was reloaded by hand. The write itself had succeeded.
|
|
38
|
+
*/ const [sourcesReloadToken, setSourcesReloadToken] = useState(0);
|
|
39
|
+
const handleSourceSaved = ()=>{
|
|
40
|
+
setSourcesReloadToken((token)=>token + 1);
|
|
41
|
+
};
|
|
29
42
|
// One measured entrance. Anything longer makes an operator wait to read counts
|
|
30
43
|
// they came here to check.
|
|
31
44
|
useGSAP(()=>{
|
|
@@ -114,7 +127,8 @@ export function IntegrationsPage({ title = 'Integrations', subtitle = 'Connect a
|
|
|
114
127
|
"aria-label": "Connections",
|
|
115
128
|
className: theme.layout.aside,
|
|
116
129
|
children: /*#__PURE__*/ _jsx(ConnectionList, {
|
|
117
|
-
actions: actions
|
|
130
|
+
actions: actions,
|
|
131
|
+
reloadToken: sourcesReloadToken
|
|
118
132
|
})
|
|
119
133
|
}),
|
|
120
134
|
/*#__PURE__*/ _jsx("section", {
|
|
@@ -138,7 +152,8 @@ export function IntegrationsPage({ title = 'Integrations', subtitle = 'Connect a
|
|
|
138
152
|
role: "tabpanel",
|
|
139
153
|
children: [
|
|
140
154
|
tab === 'connection' ? /*#__PURE__*/ _jsx(ConnectionSettingsTab, {
|
|
141
|
-
actions: actions
|
|
155
|
+
actions: actions,
|
|
156
|
+
onSaved: handleSourceSaved
|
|
142
157
|
}) : null,
|
|
143
158
|
tab === 'endpoints' ? /*#__PURE__*/ _jsx(EndpointsTab, {
|
|
144
159
|
actions: actions
|
|
@@ -163,7 +178,8 @@ export function IntegrationsPage({ title = 'Integrations', subtitle = 'Connect a
|
|
|
163
178
|
children: "Select a connection on the left, or set up a new one here."
|
|
164
179
|
}),
|
|
165
180
|
/*#__PURE__*/ _jsx(ConnectionSettingsTab, {
|
|
166
|
-
actions: actions
|
|
181
|
+
actions: actions,
|
|
182
|
+
onSaved: handleSourceSaved
|
|
167
183
|
})
|
|
168
184
|
]
|
|
169
185
|
})
|
|
@@ -65,6 +65,7 @@ export function usesOauth(authType) {
|
|
|
65
65
|
*/ export function draftToSource(draft, id) {
|
|
66
66
|
const oauth = usesOauth(draft.authType);
|
|
67
67
|
const timeout = Number.parseInt(draft.timeoutMs.trim(), 10);
|
|
68
|
+
const hasTimeout = Number.isFinite(timeout) && timeout > 0;
|
|
68
69
|
return {
|
|
69
70
|
...id ? {
|
|
70
71
|
id
|
|
@@ -84,7 +85,15 @@ export function usesOauth(authType) {
|
|
|
84
85
|
oauthScope: oauth ? orNull(draft.oauthScope) : null,
|
|
85
86
|
oauthClientAuth: oauth ? draft.oauthClientAuth : null,
|
|
86
87
|
tlsVerify: draft.tlsVerify,
|
|
87
|
-
|
|
88
|
+
// OMITTED when blank, never sent as null. The column is NOT NULL with a
|
|
89
|
+
// default, and an explicit null overrides a column default rather than
|
|
90
|
+
// falling back to it — so "leave it empty for the server default", which is
|
|
91
|
+
// what the field says, was answered "Missing required field: timeout_ms".
|
|
92
|
+
// A saved source always has a value, so blank only ever means "creating,
|
|
93
|
+
// and I have no opinion".
|
|
94
|
+
...hasTimeout ? {
|
|
95
|
+
timeoutMs: timeout
|
|
96
|
+
} : {}
|
|
88
97
|
};
|
|
89
98
|
}
|
|
90
99
|
const SLUG_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { draftFromSource, draftToSource, emptyConnectionDraft, validateConnectionDraft } from './connectionDraft';
|
|
3
|
+
/**
|
|
4
|
+
* Found in production, creating the first connection: the form said "Empty
|
|
5
|
+
* means the server default" under the timeout, and saving with it empty
|
|
6
|
+
* answered "Missing required field: timeout_ms".
|
|
7
|
+
*
|
|
8
|
+
* `timeout_ms` is NOT NULL with a default of 30000. An explicit null does not
|
|
9
|
+
* fall back to a column default — it overrides it and violates the constraint.
|
|
10
|
+
* The distinction between "send null" and "do not send the key" is the whole
|
|
11
|
+
* bug, so it is what these assert.
|
|
12
|
+
*/ const filled = ()=>({
|
|
13
|
+
...emptyConnectionDraft,
|
|
14
|
+
name: 'TatMetal HR',
|
|
15
|
+
slug: 'tatmetal-hr',
|
|
16
|
+
baseUrl: 'https://online.tatmetal.com/hr-service'
|
|
17
|
+
});
|
|
18
|
+
describe('draftToSource — columns that are NOT NULL with a default', ()=>{
|
|
19
|
+
test('an empty timeout is omitted, not sent as null', ()=>{
|
|
20
|
+
const payload = draftToSource(filled());
|
|
21
|
+
expect('timeoutMs' in payload).toBe(false);
|
|
22
|
+
expect(payload.timeoutMs).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
test('a timeout that was typed is sent as a number', ()=>{
|
|
25
|
+
const payload = draftToSource({
|
|
26
|
+
...filled(),
|
|
27
|
+
timeoutMs: '45000'
|
|
28
|
+
});
|
|
29
|
+
expect(payload.timeoutMs).toBe(45000);
|
|
30
|
+
});
|
|
31
|
+
test('whitespace and nonsense are treated as empty rather than as null', ()=>{
|
|
32
|
+
for (const value of [
|
|
33
|
+
' ',
|
|
34
|
+
'abc',
|
|
35
|
+
'0',
|
|
36
|
+
'-1'
|
|
37
|
+
]){
|
|
38
|
+
const payload = draftToSource({
|
|
39
|
+
...filled(),
|
|
40
|
+
timeoutMs: value
|
|
41
|
+
});
|
|
42
|
+
expect(`${value} → ${'timeoutMs' in payload}`).toBe(`${value} → false`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
test('the other NOT NULL columns always carry a concrete value', ()=>{
|
|
46
|
+
const payload = draftToSource(filled());
|
|
47
|
+
expect(payload.authType).toBe('none');
|
|
48
|
+
expect(payload.tlsVerify).toBe(true);
|
|
49
|
+
expect(payload.enabled).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('draftToSource — fields the chosen auth type does not use', ()=>{
|
|
53
|
+
test('a bearer connection clears the OAuth settings rather than leaving them', ()=>{
|
|
54
|
+
const payload = draftToSource({
|
|
55
|
+
...filled(),
|
|
56
|
+
authType: 'bearer',
|
|
57
|
+
tokenUrl: 'https://example.com/token',
|
|
58
|
+
oauthClientId: 'left-behind'
|
|
59
|
+
});
|
|
60
|
+
expect(payload.tokenUrl).toBeNull();
|
|
61
|
+
expect(payload.oauthClientId).toBeNull();
|
|
62
|
+
expect(payload.oauthGrantType).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
test('a username is kept only for the password grant', ()=>{
|
|
65
|
+
const base = {
|
|
66
|
+
...filled(),
|
|
67
|
+
authType: 'oauth2',
|
|
68
|
+
oauthUsername: 'svc-account'
|
|
69
|
+
};
|
|
70
|
+
expect(draftToSource({
|
|
71
|
+
...base,
|
|
72
|
+
oauthGrantType: 'password'
|
|
73
|
+
}).oauthUsername).toBe('svc-account');
|
|
74
|
+
expect(draftToSource({
|
|
75
|
+
...base,
|
|
76
|
+
oauthGrantType: 'client_credentials'
|
|
77
|
+
}).oauthUsername).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('validateConnectionDraft', ()=>{
|
|
81
|
+
test('the three fields a connection cannot do without are required', ()=>{
|
|
82
|
+
const errors = validateConnectionDraft(emptyConnectionDraft);
|
|
83
|
+
expect(errors.name).toBeDefined();
|
|
84
|
+
expect(errors.slug).toBeDefined();
|
|
85
|
+
expect(errors.baseUrl).toBeDefined();
|
|
86
|
+
});
|
|
87
|
+
test('a filled draft passes', ()=>{
|
|
88
|
+
expect(validateConnectionDraft(filled())).toEqual({});
|
|
89
|
+
});
|
|
90
|
+
test('plain http is accepted — an in-cluster service is a normal target', ()=>{
|
|
91
|
+
const errors = validateConnectionDraft({
|
|
92
|
+
...filled(),
|
|
93
|
+
baseUrl: 'http://hr-service:8080'
|
|
94
|
+
});
|
|
95
|
+
expect(errors.baseUrl).toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
test('a non-numeric timeout is refused before it can reach the server', ()=>{
|
|
98
|
+
expect(validateConnectionDraft({
|
|
99
|
+
...filled(),
|
|
100
|
+
timeoutMs: 'soon'
|
|
101
|
+
}).timeoutMs).toBeDefined();
|
|
102
|
+
expect(validateConnectionDraft({
|
|
103
|
+
...filled(),
|
|
104
|
+
timeoutMs: ''
|
|
105
|
+
}).timeoutMs).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe('draftFromSource', ()=>{
|
|
109
|
+
test('a saved timeout comes back into the field', ()=>{
|
|
110
|
+
const draft = draftFromSource({
|
|
111
|
+
id: 'a',
|
|
112
|
+
name: 'n',
|
|
113
|
+
slug: 's',
|
|
114
|
+
baseUrl: 'https://x.test',
|
|
115
|
+
authType: 'none',
|
|
116
|
+
timeoutMs: 45000
|
|
117
|
+
});
|
|
118
|
+
expect(draft.timeoutMs).toBe('45000');
|
|
119
|
+
});
|
|
120
|
+
test('a round trip through the form does not invent a change', ()=>{
|
|
121
|
+
const source = {
|
|
122
|
+
id: 'a',
|
|
123
|
+
name: 'n',
|
|
124
|
+
slug: 's',
|
|
125
|
+
baseUrl: 'https://x.test',
|
|
126
|
+
authType: 'none',
|
|
127
|
+
tlsVerify: true,
|
|
128
|
+
enabled: true,
|
|
129
|
+
timeoutMs: 30000
|
|
130
|
+
};
|
|
131
|
+
expect(draftToSource(draftFromSource(source), 'a').timeoutMs).toBe(30000);
|
|
132
|
+
});
|
|
133
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nucleus-core-ts",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.791",
|
|
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",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
|
-
"test": "bun test src scripts",
|
|
53
|
+
"test": "bun test src scripts fe",
|
|
54
54
|
"build": "bun run scripts/build.ts",
|
|
55
55
|
"build:quick": "bun run build:js && bun run build:types",
|
|
56
56
|
"build:js": "bun build ./index.ts ./client.ts ./fe/index.ts ./src/Client/Proxy/index.ts --outdir=dist --target=bun --format=esm --splitting --minify --external react --external react-dom --external gsap --external @gsap/react --external h-state --external elysia --external drizzle-orm --external drizzle-kit --external ioredis --external googleapis --external @dapr/dapr --external pg --external @xyflow/react --external @xyflow/system --external @azure/communication-email --external @azure/identity --external stripe --external sharp",
|