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.
@@ -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,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
+ });