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.
Files changed (32) hide show
  1. package/dist/client.js +1 -1
  2. package/dist/fe/components/IntegrationsPage/components/ConnectionList.js +5 -0
  3. package/dist/fe/components/IntegrationsPage/components/ConnectionTransfer.d.ts +7 -0
  4. package/dist/fe/components/IntegrationsPage/components/ConnectionTransfer.js +157 -0
  5. package/dist/fe/components/IntegrationsPage/components/MappingAccountFields.d.ts +12 -0
  6. package/dist/fe/components/IntegrationsPage/components/MappingAccountFields.js +214 -0
  7. package/dist/fe/components/IntegrationsPage/components/MappingEditor.js +10 -0
  8. package/dist/fe/components/IntegrationsPage/components/RunPanel.js +14 -3
  9. package/dist/fe/components/IntegrationsPage/components/RunPlanSuggestions.d.ts +15 -0
  10. package/dist/fe/components/IntegrationsPage/components/RunPlanSuggestions.js +120 -0
  11. package/dist/fe/components/IntegrationsPage/components/RunPlanSuggestions.test.d.ts +1 -0
  12. package/dist/fe/components/IntegrationsPage/components/RunPlanSuggestions.test.js +103 -0
  13. package/dist/fe/components/IntegrationsPage/components/RunPlanSummary.js +5 -0
  14. package/dist/fe/components/IntegrationsPage/components/fieldMappingOptions.js +4 -2
  15. package/dist/fe/components/IntegrationsPage/components/mappingDraft.d.ts +3 -1
  16. package/dist/fe/components/IntegrationsPage/components/mappingDraft.js +14 -0
  17. package/dist/fe/components/IntegrationsPage/index.d.ts +3 -0
  18. package/dist/fe/components/IntegrationsPage/index.js +3 -0
  19. package/dist/fe/components/IntegrationsPage/types/actions.d.ts +18 -0
  20. package/dist/fe/components/IntegrationsPage/types/records.d.ts +26 -1
  21. package/dist/fe/components/IntegrationsPage/types/results.d.ts +2 -0
  22. package/dist/index.js +5 -5
  23. package/dist/src/ElysiaPlugin/routes/integrations/inspect.d.ts +1 -0
  24. package/dist/src/ElysiaPlugin/routes/integrations/repository.d.ts +36 -0
  25. package/dist/src/Services/Integrations/plan.d.ts +18 -1
  26. package/dist/src/Services/Integrations/runner.d.ts +13 -1
  27. package/dist/src/Services/Integrations/transfer.d.ts +123 -0
  28. package/dist/src/Services/Integrations/transfer.test.d.ts +1 -0
  29. package/dist/src/Services/Integrations/types.d.ts +47 -2
  30. package/dist/src/Services/Integrations/writer.d.ts +24 -0
  31. package/package.json +1 -1
  32. 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 = {