nucleus-core-ts 0.9.821 → 0.9.823
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/client.js +1 -1
- package/dist/fe/components/IntegrationsPage/components/ConnectionList.js +5 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionTransfer.d.ts +7 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionTransfer.js +157 -0
- package/dist/fe/components/IntegrationsPage/components/MappingAccountFields.d.ts +12 -0
- package/dist/fe/components/IntegrationsPage/components/MappingAccountFields.js +214 -0
- package/dist/fe/components/IntegrationsPage/components/MappingEditor.js +10 -0
- package/dist/fe/components/IntegrationsPage/components/RunPanel.js +14 -3
- package/dist/fe/components/IntegrationsPage/components/RunPlanSuggestions.d.ts +15 -0
- package/dist/fe/components/IntegrationsPage/components/RunPlanSuggestions.js +120 -0
- package/dist/fe/components/IntegrationsPage/components/RunPlanSuggestions.test.d.ts +1 -0
- package/dist/fe/components/IntegrationsPage/components/RunPlanSuggestions.test.js +103 -0
- package/dist/fe/components/IntegrationsPage/components/RunPlanSummary.js +5 -0
- package/dist/fe/components/IntegrationsPage/components/fieldMappingOptions.js +4 -2
- package/dist/fe/components/IntegrationsPage/components/mappingDraft.d.ts +3 -1
- package/dist/fe/components/IntegrationsPage/components/mappingDraft.js +14 -0
- package/dist/fe/components/IntegrationsPage/index.d.ts +3 -0
- package/dist/fe/components/IntegrationsPage/index.js +3 -0
- package/dist/fe/components/IntegrationsPage/types/actions.d.ts +18 -0
- package/dist/fe/components/IntegrationsPage/types/records.d.ts +26 -1
- package/dist/fe/components/IntegrationsPage/types/results.d.ts +2 -0
- package/dist/index.js +5 -5
- package/dist/src/ElysiaPlugin/routes/integrations/inspect.d.ts +1 -0
- package/dist/src/ElysiaPlugin/routes/integrations/repository.d.ts +36 -0
- package/dist/src/Services/Integrations/plan.d.ts +18 -1
- package/dist/src/Services/Integrations/runner.d.ts +13 -1
- package/dist/src/Services/Integrations/transfer.d.ts +123 -0
- package/dist/src/Services/Integrations/transfer.test.d.ts +1 -0
- package/dist/src/Services/Integrations/types.d.ts +47 -2
- package/dist/src/Services/Integrations/writer.d.ts +24 -0
- package/package.json +1 -1
- package/src/system.tables.json +12 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { missingRequiredCounts, suggestEmailTemplate } from './RunPlanSuggestions';
|
|
3
|
+
/**
|
|
4
|
+
* A preview that says "561 records are missing e-mail" has told the truth and
|
|
5
|
+
* stopped one step short of being useful. The answer is nearly always to build
|
|
6
|
+
* the value from the fields that ARE there, and the panel knows which those are.
|
|
7
|
+
*/ const plan = (issues)=>({
|
|
8
|
+
issues,
|
|
9
|
+
referenceInserts: [],
|
|
10
|
+
warnings: []
|
|
11
|
+
});
|
|
12
|
+
const issue = (message, i)=>({
|
|
13
|
+
sourceIndex: i,
|
|
14
|
+
kind: 'missing_required',
|
|
15
|
+
message
|
|
16
|
+
});
|
|
17
|
+
describe('counting what each missing field costs', ()=>{
|
|
18
|
+
it('counts the records one field kept out', ()=>{
|
|
19
|
+
const counts = missingRequiredCounts(plan([
|
|
20
|
+
issue('Required field(s) empty: email', 0),
|
|
21
|
+
issue('Required field(s) empty: email', 1)
|
|
22
|
+
]));
|
|
23
|
+
expect(counts).toEqual([
|
|
24
|
+
{
|
|
25
|
+
field: 'email',
|
|
26
|
+
records: 2
|
|
27
|
+
}
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
it('counts each field of a record that is missing two', ()=>{
|
|
31
|
+
const counts = missingRequiredCounts(plan([
|
|
32
|
+
issue('Required field(s) empty: email, code', 0)
|
|
33
|
+
]));
|
|
34
|
+
expect(counts).toEqual([
|
|
35
|
+
{
|
|
36
|
+
field: 'email',
|
|
37
|
+
records: 1
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
field: 'code',
|
|
41
|
+
records: 1
|
|
42
|
+
}
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
45
|
+
it('puts the biggest problem first', ()=>{
|
|
46
|
+
const counts = missingRequiredCounts(plan([
|
|
47
|
+
issue('Required field(s) empty: code', 0),
|
|
48
|
+
issue('Required field(s) empty: email', 1),
|
|
49
|
+
issue('Required field(s) empty: email', 2)
|
|
50
|
+
]));
|
|
51
|
+
expect(counts[0]).toEqual({
|
|
52
|
+
field: 'email',
|
|
53
|
+
records: 2
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
it('ignores issues of every other kind', ()=>{
|
|
57
|
+
const counts = missingRequiredCounts(plan([
|
|
58
|
+
{
|
|
59
|
+
sourceIndex: 0,
|
|
60
|
+
kind: 'duplicate_in_batch',
|
|
61
|
+
message: 'twice'
|
|
62
|
+
}
|
|
63
|
+
]));
|
|
64
|
+
expect(counts).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe('the address it suggests building', ()=>{
|
|
68
|
+
const mapping = (sources)=>({
|
|
69
|
+
fieldMappings: sources.map((source)=>({
|
|
70
|
+
source,
|
|
71
|
+
target: source
|
|
72
|
+
}))
|
|
73
|
+
});
|
|
74
|
+
it('is built from the fields this source really sends', ()=>{
|
|
75
|
+
expect(suggestEmailTemplate(mapping([
|
|
76
|
+
'name',
|
|
77
|
+
'surname',
|
|
78
|
+
'employeeNo'
|
|
79
|
+
]))).toBe('{name}.{surname}@example.com');
|
|
80
|
+
});
|
|
81
|
+
it('recognises the other names those fields go by', ()=>{
|
|
82
|
+
expect(suggestEmailTemplate(mapping([
|
|
83
|
+
'firstName',
|
|
84
|
+
'lastName'
|
|
85
|
+
]))).toBe('{firstName}.{lastName}@example.com');
|
|
86
|
+
});
|
|
87
|
+
it('offers what it can when only one of the two is there', ()=>{
|
|
88
|
+
expect(suggestEmailTemplate(mapping([
|
|
89
|
+
'surname'
|
|
90
|
+
]))).toBe('{surname}@example.com');
|
|
91
|
+
});
|
|
92
|
+
it('suggests nothing rather than nonsense when there is no name at all', ()=>{
|
|
93
|
+
expect(suggestEmailTemplate(mapping([
|
|
94
|
+
'employeeNo',
|
|
95
|
+
'departmentCode'
|
|
96
|
+
]))).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
it('ignores a source that is already a template', ()=>{
|
|
99
|
+
expect(suggestEmailTemplate(mapping([
|
|
100
|
+
'{a}-{b}'
|
|
101
|
+
]))).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -82,6 +82,11 @@ export function RunPlanSummary({ plan }) {
|
|
|
82
82
|
})
|
|
83
83
|
]
|
|
84
84
|
}),
|
|
85
|
+
plan.accountsToCreate > 0 ? /*#__PURE__*/ _jsx(Notice, {
|
|
86
|
+
title: `${plan.accountsToCreate} login(s) will be created, with the password typed above`,
|
|
87
|
+
tone: "info",
|
|
88
|
+
children: "Each new record gets an account in the same import — one password for all of them, which people change themselves afterwards. Anyone whose value already has an account is linked to it instead."
|
|
89
|
+
}) : null,
|
|
85
90
|
plan.warnings.map((warning, index)=>/*#__PURE__*/ _jsx(Notice, {
|
|
86
91
|
title: warning,
|
|
87
92
|
tone: "warning"
|
|
@@ -26,7 +26,8 @@ export const TRANSFORMS = [
|
|
|
26
26
|
'toBoolean',
|
|
27
27
|
'dateOnly',
|
|
28
28
|
'emailSafe',
|
|
29
|
-
'pathLeaf'
|
|
29
|
+
'pathLeaf',
|
|
30
|
+
'toList'
|
|
30
31
|
];
|
|
31
32
|
export const TRANSFORM_LABEL = {
|
|
32
33
|
none: 'No transform',
|
|
@@ -38,7 +39,8 @@ export const TRANSFORM_LABEL = {
|
|
|
38
39
|
toBoolean: 'To boolean',
|
|
39
40
|
dateOnly: 'Date only',
|
|
40
41
|
emailSafe: 'E-mail safe (ascii, lowercase)',
|
|
41
|
-
pathLeaf: 'Last part of a path (A/B/C → C)'
|
|
42
|
+
pathLeaf: 'Last part of a path (A/B/C → C)',
|
|
43
|
+
toList: 'To a list (for a column that holds many)'
|
|
42
44
|
};
|
|
43
45
|
/** Narrows what a select reported back to a transform this panel knows. */ export function toTransform(value) {
|
|
44
46
|
return TRANSFORMS.find((option)=>option === value) ?? 'none';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IntegrationMapping, ReferenceCheckValue } from '../types';
|
|
1
|
+
import type { AccountRuleValue, IntegrationMapping, ReferenceCheckValue } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* The mapping while it is being edited.
|
|
4
4
|
*
|
|
@@ -29,6 +29,8 @@ export type MappingDraft = {
|
|
|
29
29
|
referenceChecks: ReferenceCheckValue[];
|
|
30
30
|
/** Column that receives the shared opening password typed at run time. */
|
|
31
31
|
passwordTargetColumn?: string | null;
|
|
32
|
+
/** Creates a login for each new record, in the same import. */
|
|
33
|
+
accountRule?: AccountRuleValue | null;
|
|
32
34
|
enabled?: boolean;
|
|
33
35
|
};
|
|
34
36
|
/**
|
|
@@ -31,6 +31,7 @@ import { missingRecordFieldsIncomplete } from './MappingWriteModeFields';
|
|
|
31
31
|
missingRecordValue: mapping.missingRecordValue,
|
|
32
32
|
referenceChecks: asArray(mapping.referenceChecks),
|
|
33
33
|
passwordTargetColumn: mapping.passwordTargetColumn ?? null,
|
|
34
|
+
accountRule: mapping.accountRule ?? null,
|
|
34
35
|
enabled: mapping.enabled
|
|
35
36
|
};
|
|
36
37
|
}
|
|
@@ -46,6 +47,19 @@ import { missingRecordFieldsIncomplete } from './MappingWriteModeFields';
|
|
|
46
47
|
if (draft.endpointId.trim() === '') issues.push('Select the endpoint this mapping reads from.');
|
|
47
48
|
const usable = asArray(draft.fieldMappings).some((row)=>row.target.trim() !== '' && (row.source.trim() !== '' || (row.constant ?? '').trim() !== ''));
|
|
48
49
|
if (!usable) issues.push('Map at least one field to a target column.');
|
|
50
|
+
const account = draft.accountRule;
|
|
51
|
+
if (account) {
|
|
52
|
+
const missing = [
|
|
53
|
+
account.entity ? '' : 'the table accounts live in',
|
|
54
|
+
account.matchColumn ? '' : 'the column that identifies an account',
|
|
55
|
+
account.matchFrom ? '' : "the record's matching column",
|
|
56
|
+
account.passwordColumn ? '' : 'the column the password goes into',
|
|
57
|
+
account.idColumn ? '' : "where the account's id is stored"
|
|
58
|
+
].filter(Boolean);
|
|
59
|
+
if (missing.length > 0) {
|
|
60
|
+
issues.push(`Creating a login needs ${missing.join(', ')}.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
49
63
|
for (const check of asArray(draft.referenceChecks)){
|
|
50
64
|
if (check.column.trim() === '' || check.entity.trim() === '' || check.entityColumn.trim() === '') issues.push('Every reference needs a column, an entity and the column to match against.');
|
|
51
65
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
export { ConnectionList } from './components/ConnectionList';
|
|
2
2
|
export type { ConnectionTabsProps } from './components/ConnectionTabs';
|
|
3
3
|
export { ConnectionTabs, connectionTabId, connectionTabPanelId } from './components/ConnectionTabs';
|
|
4
|
+
export { ConnectionTransfer } from './components/ConnectionTransfer';
|
|
4
5
|
export { EndpointsTab } from './components/EndpointsTab';
|
|
5
6
|
export { IntegrationsPage } from './components/IntegrationsPage';
|
|
7
|
+
export { MappingAccountFields } from './components/MappingAccountFields';
|
|
6
8
|
export { MappingsTab } from './components/MappingsTab';
|
|
7
9
|
export type { BadgeProps, BadgeTone, CodeBlockProps, FieldProps, NoticeProps, NoticeTone, StatProps, } from './components/Primitives';
|
|
8
10
|
export { Badge, CodeBlock, Field, Notice, Stat } from './components/Primitives';
|
|
11
|
+
export { RunPlanSuggestions } from './components/RunPlanSuggestions';
|
|
9
12
|
export { RunsTab } from './components/RunsTab';
|
|
10
13
|
export type { ListSkeletonProps } from './components/Skeletons';
|
|
11
14
|
export { CardSkeleton, ListSkeleton, StatGridSkeleton } from './components/Skeletons';
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
export { ConnectionList } from './components/ConnectionList';
|
|
2
2
|
export { ConnectionTabs, connectionTabId, connectionTabPanelId } from './components/ConnectionTabs';
|
|
3
|
+
export { ConnectionTransfer } from './components/ConnectionTransfer';
|
|
3
4
|
export { EndpointsTab } from './components/EndpointsTab';
|
|
4
5
|
export { IntegrationsPage } from './components/IntegrationsPage';
|
|
6
|
+
export { MappingAccountFields } from './components/MappingAccountFields';
|
|
5
7
|
export { MappingsTab } from './components/MappingsTab';
|
|
6
8
|
export { Badge, CodeBlock, Field, Notice, Stat } from './components/Primitives';
|
|
9
|
+
export { RunPlanSuggestions } from './components/RunPlanSuggestions';
|
|
7
10
|
export { RunsTab } from './components/RunsTab';
|
|
8
11
|
export { CardSkeleton, ListSkeleton, StatGridSkeleton } from './components/Skeletons';
|
|
9
12
|
export { useInfiniteList } from './components/useInfiniteList';
|
|
@@ -92,6 +92,24 @@ export type IntegrationsActions = {
|
|
|
92
92
|
label: string;
|
|
93
93
|
fields: string[];
|
|
94
94
|
}[]>;
|
|
95
|
+
/**
|
|
96
|
+
* The whole connection as one portable document — WITHOUT any credential.
|
|
97
|
+
*
|
|
98
|
+
* A connection is the connection, its calls, the parameters those calls take
|
|
99
|
+
* from each other and the mappings that turn the answers into rows. Moving
|
|
100
|
+
* that by hand between environments is hours of work and a fresh chance to
|
|
101
|
+
* get one field wrong in a way nobody notices until an import writes it.
|
|
102
|
+
*/
|
|
103
|
+
exportConnection: (sourceId: string) => Promise<unknown>;
|
|
104
|
+
/** Brings such a document in. Re-importing the same file updates rather than duplicates. */
|
|
105
|
+
importConnection: (document: unknown) => Promise<{
|
|
106
|
+
ok: boolean;
|
|
107
|
+
message: string;
|
|
108
|
+
problems?: {
|
|
109
|
+
path: string;
|
|
110
|
+
message: string;
|
|
111
|
+
}[];
|
|
112
|
+
}>;
|
|
95
113
|
};
|
|
96
114
|
export type IntegrationsPageProps = {
|
|
97
115
|
title?: string;
|
|
@@ -86,7 +86,7 @@ export type IntegrationEndpoint = {
|
|
|
86
86
|
} | null;
|
|
87
87
|
enabled?: boolean;
|
|
88
88
|
};
|
|
89
|
-
export type MappingTransformValue = 'none' | 'trim' | 'upper' | 'lower' | 'toString' | 'toNumber' | 'toBoolean' | 'dateOnly' | 'emailSafe' | 'pathLeaf';
|
|
89
|
+
export type MappingTransformValue = 'none' | 'trim' | 'upper' | 'lower' | 'toString' | 'toNumber' | 'toBoolean' | 'dateOnly' | 'emailSafe' | 'pathLeaf' | 'toList';
|
|
90
90
|
export type FieldMappingValue = {
|
|
91
91
|
source: string;
|
|
92
92
|
target: string;
|
|
@@ -112,6 +112,29 @@ export type ReferenceCheckValue = {
|
|
|
112
112
|
entityColumn: string;
|
|
113
113
|
onMissing: 'create' | 'skipRecord' | 'ignore';
|
|
114
114
|
};
|
|
115
|
+
/**
|
|
116
|
+
* A login created alongside each imported record.
|
|
117
|
+
*
|
|
118
|
+
* A directory carries people, not accounts. Creating the logins afterwards, by
|
|
119
|
+
* hand, matching them up by e-mail one at a time, is the job nobody gets to.
|
|
120
|
+
*/
|
|
121
|
+
export type AccountRuleValue = {
|
|
122
|
+
/** Entity the account row goes into. */
|
|
123
|
+
entity: string;
|
|
124
|
+
/** Column on the target entity that receives the account's id. */
|
|
125
|
+
idColumn: string;
|
|
126
|
+
/** Column on the account that identifies it. */
|
|
127
|
+
matchColumn: string;
|
|
128
|
+
/** Column of the imported record carrying that same value. */
|
|
129
|
+
matchFrom: string;
|
|
130
|
+
/** Column on the account that receives the run's shared password. */
|
|
131
|
+
passwordColumn: string;
|
|
132
|
+
/** Other columns to fill on a new account, from the imported record. */
|
|
133
|
+
values?: {
|
|
134
|
+
column: string;
|
|
135
|
+
from: string;
|
|
136
|
+
}[];
|
|
137
|
+
};
|
|
115
138
|
export type IntegrationMapping = {
|
|
116
139
|
id: string;
|
|
117
140
|
endpointId: string;
|
|
@@ -127,6 +150,8 @@ export type IntegrationMapping = {
|
|
|
127
150
|
referenceChecks?: ReferenceCheckValue[] | null;
|
|
128
151
|
/** Column that receives the shared opening password typed at run time. */
|
|
129
152
|
passwordTargetColumn?: string | null;
|
|
153
|
+
/** Creates a login for each new record, in the same import. */
|
|
154
|
+
accountRule?: AccountRuleValue | null;
|
|
130
155
|
lastRunAt?: string | null;
|
|
131
156
|
enabled?: boolean;
|
|
132
157
|
};
|
|
@@ -49,6 +49,8 @@ export type RunPlanValue = {
|
|
|
49
49
|
warnings: string[];
|
|
50
50
|
/** Rows this run would add to OTHER tables so its records have something to point at. */
|
|
51
51
|
referenceInserts: ReferenceInsertValue[];
|
|
52
|
+
/** Logins this run would create so its records can be signed in to. */
|
|
53
|
+
accountsToCreate: number;
|
|
52
54
|
sample: Record<string, unknown>[];
|
|
53
55
|
};
|
|
54
56
|
export type ConnectionTestValue = {
|