nucleus-core-ts 0.9.813 → 0.9.815
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 +2 -1
- package/dist/fe/components/IntegrationsPage/components/jsonArray.d.ts +16 -0
- package/dist/fe/components/IntegrationsPage/components/jsonArray.js +17 -0
- package/dist/fe/components/IntegrationsPage/components/mappingDraft.js +5 -4
- package/dist/fe/components/IntegrationsPage/components/mappingDraft.test.d.ts +1 -0
- package/dist/fe/components/IntegrationsPage/components/mappingDraft.test.js +59 -0
- package/dist/index.js +10 -10
- package/package.json +1 -1
- package/scripts/generate-schema.test.ts +28 -0
- package/scripts/generate-schema.ts +12 -5
|
@@ -5,6 +5,7 @@ import { useIntegrationsStore } from '../store';
|
|
|
5
5
|
import { useLabels } from '../text/context';
|
|
6
6
|
import { integrationsPageTheme } from '../theme';
|
|
7
7
|
import { EndpointFormFields } from './EndpointFormFields';
|
|
8
|
+
import { asArray } from './jsonArray';
|
|
8
9
|
import { formatJsonObject, parseJsonObject } from './jsonObjectField';
|
|
9
10
|
import { PaginationFields } from './PaginationFields';
|
|
10
11
|
import { ParamBindingRows } from './ParamBindingRows';
|
|
@@ -33,7 +34,7 @@ export function EndpointForm({ sourceId, endpoint, actions, onSaved, onDeleted,
|
|
|
33
34
|
pagination: endpoint?.pagination ?? {
|
|
34
35
|
kind: 'none'
|
|
35
36
|
},
|
|
36
|
-
paramBindings: endpoint?.paramBindings
|
|
37
|
+
paramBindings: asArray(endpoint?.paramBindings),
|
|
37
38
|
queryParams: formatJsonObject(endpoint?.queryParams),
|
|
38
39
|
requestBody: formatJsonObject(endpoint?.requestBody)
|
|
39
40
|
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a jsonb column that is supposed to hold a list.
|
|
3
|
+
*
|
|
4
|
+
* It is not always a list. The schema generator wrote every object-shaped
|
|
5
|
+
* default — including `[]` — into Postgres as `'{}'`, which is an empty OBJECT.
|
|
6
|
+
* So every row that never had a value reads back as `{}`, and the first
|
|
7
|
+
* `for…of` over it throws "object is not iterable". That is every existing row
|
|
8
|
+
* the moment a new column is added to a table already in use, which is exactly
|
|
9
|
+
* when nobody is looking: the column is new, nothing has filled it in, and the
|
|
10
|
+
* screen that reads it dies on the next click.
|
|
11
|
+
*
|
|
12
|
+
* The generator is fixed. This exists because the rows it already wrote are
|
|
13
|
+
* still there, and because a value crossing the wire from a database is not
|
|
14
|
+
* something a component should have to trust.
|
|
15
|
+
*/
|
|
16
|
+
export declare function asArray<T>(value: unknown): T[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a jsonb column that is supposed to hold a list.
|
|
3
|
+
*
|
|
4
|
+
* It is not always a list. The schema generator wrote every object-shaped
|
|
5
|
+
* default — including `[]` — into Postgres as `'{}'`, which is an empty OBJECT.
|
|
6
|
+
* So every row that never had a value reads back as `{}`, and the first
|
|
7
|
+
* `for…of` over it throws "object is not iterable". That is every existing row
|
|
8
|
+
* the moment a new column is added to a table already in use, which is exactly
|
|
9
|
+
* when nobody is looking: the column is new, nothing has filled it in, and the
|
|
10
|
+
* screen that reads it dies on the next click.
|
|
11
|
+
*
|
|
12
|
+
* The generator is fixed. This exists because the rows it already wrote are
|
|
13
|
+
* still there, and because a value crossing the wire from a database is not
|
|
14
|
+
* something a component should have to trust.
|
|
15
|
+
*/ export function asArray(value) {
|
|
16
|
+
return Array.isArray(value) ? value : [];
|
|
17
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { asArray } from './jsonArray';
|
|
1
2
|
import { missingRecordFieldsIncomplete } from './MappingWriteModeFields';
|
|
2
3
|
/** Upsert and ignore: the two choices that cannot lose a row by themselves. */ export function blankMappingDraft(endpointId) {
|
|
3
4
|
return {
|
|
@@ -16,14 +17,14 @@ import { missingRecordFieldsIncomplete } from './MappingWriteModeFields';
|
|
|
16
17
|
endpointId: mapping.endpointId,
|
|
17
18
|
name: mapping.name,
|
|
18
19
|
targetEntity: mapping.targetEntity,
|
|
19
|
-
fieldMappings: mapping.fieldMappings,
|
|
20
|
+
fieldMappings: asArray(mapping.fieldMappings),
|
|
20
21
|
dedupSourceField: mapping.dedupSourceField,
|
|
21
22
|
dedupTargetField: mapping.dedupTargetField,
|
|
22
23
|
writeMode: mapping.writeMode,
|
|
23
24
|
missingRecordPolicy: mapping.missingRecordPolicy,
|
|
24
25
|
missingRecordColumn: mapping.missingRecordColumn,
|
|
25
26
|
missingRecordValue: mapping.missingRecordValue,
|
|
26
|
-
referenceChecks: mapping.referenceChecks
|
|
27
|
+
referenceChecks: asArray(mapping.referenceChecks),
|
|
27
28
|
passwordTargetColumn: mapping.passwordTargetColumn ?? null,
|
|
28
29
|
enabled: mapping.enabled
|
|
29
30
|
};
|
|
@@ -38,9 +39,9 @@ import { missingRecordFieldsIncomplete } from './MappingWriteModeFields';
|
|
|
38
39
|
if (draft.name.trim() === '') issues.push('Give the mapping a name.');
|
|
39
40
|
if (draft.targetEntity.trim() === '') issues.push('Choose a target entity.');
|
|
40
41
|
if (draft.endpointId.trim() === '') issues.push('Select the endpoint this mapping reads from.');
|
|
41
|
-
const usable = draft.fieldMappings.some((row)=>row.target.trim() !== '' && (row.source.trim() !== '' || (row.constant ?? '').trim() !== ''));
|
|
42
|
+
const usable = asArray(draft.fieldMappings).some((row)=>row.target.trim() !== '' && (row.source.trim() !== '' || (row.constant ?? '').trim() !== ''));
|
|
42
43
|
if (!usable) issues.push('Map at least one field to a target column.');
|
|
43
|
-
for (const check of draft.referenceChecks){
|
|
44
|
+
for (const check of asArray(draft.referenceChecks)){
|
|
44
45
|
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.');
|
|
45
46
|
}
|
|
46
47
|
if (missingRecordFieldsIncomplete(draft.missingRecordPolicy, draft.missingRecordColumn ?? '', draft.missingRecordValue ?? '')) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { mappingDraftIssues, toMappingDraft } from './mappingDraft';
|
|
3
|
+
/**
|
|
4
|
+
* A jsonb column that is supposed to hold a list, and does not.
|
|
5
|
+
*
|
|
6
|
+
* The schema generator wrote every object-shaped default — `[]` included — into
|
|
7
|
+
* Postgres as `'{}'`, an empty OBJECT. So every row that never had a value reads
|
|
8
|
+
* back as `{}`. `for…of` over that throws "object is not iterable", which took
|
|
9
|
+
* the whole admin panel down to a blank error page the first time a mapping was
|
|
10
|
+
* opened after the column was added. The generator is fixed; these rows are
|
|
11
|
+
* still in the database.
|
|
12
|
+
*/ describe('a list column that came back as an object', ()=>{
|
|
13
|
+
const mapping = (over)=>({
|
|
14
|
+
id: 'm1',
|
|
15
|
+
endpointId: 'e1',
|
|
16
|
+
name: 'Çalışanlar',
|
|
17
|
+
targetEntity: 'employees',
|
|
18
|
+
fieldMappings: [],
|
|
19
|
+
writeMode: 'upsert',
|
|
20
|
+
missingRecordPolicy: 'ignore',
|
|
21
|
+
...over
|
|
22
|
+
});
|
|
23
|
+
it('reads an empty object as an empty list', ()=>{
|
|
24
|
+
const draft = toMappingDraft(mapping({
|
|
25
|
+
referenceChecks: {},
|
|
26
|
+
fieldMappings: {}
|
|
27
|
+
}));
|
|
28
|
+
expect(draft.referenceChecks).toEqual([]);
|
|
29
|
+
expect(draft.fieldMappings).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
it('keeps a real list', ()=>{
|
|
32
|
+
const checks = [
|
|
33
|
+
{
|
|
34
|
+
column: 'department',
|
|
35
|
+
entity: 'departments',
|
|
36
|
+
entityColumn: 'name',
|
|
37
|
+
onMissing: 'create'
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
const draft = toMappingDraft(mapping({
|
|
41
|
+
referenceChecks: checks
|
|
42
|
+
}));
|
|
43
|
+
expect(draft.referenceChecks).toEqual(checks);
|
|
44
|
+
});
|
|
45
|
+
it('validates a draft whose lists arrived as objects instead of throwing', ()=>{
|
|
46
|
+
// This is the call that ran during render, so what it threw was not an error
|
|
47
|
+
// message on the screen — it was the screen.
|
|
48
|
+
const issues = mappingDraftIssues({
|
|
49
|
+
endpointId: 'e1',
|
|
50
|
+
name: 'Çalışanlar',
|
|
51
|
+
targetEntity: 'employees',
|
|
52
|
+
fieldMappings: {},
|
|
53
|
+
referenceChecks: {},
|
|
54
|
+
writeMode: 'upsert',
|
|
55
|
+
missingRecordPolicy: 'ignore'
|
|
56
|
+
});
|
|
57
|
+
expect(issues).toContain('Map at least one field to a target column.');
|
|
58
|
+
});
|
|
59
|
+
});
|