Skip to content

Commit b13ebbb

Browse files
[sync] fix(sdk): normalize v2 rollup field docs (#1403) (#2737)
Synced from teableio/teable-ee@67908ee Co-authored-by: nichenqin <nichenqin@hotmail.com>
1 parent a1477d5 commit b13ebbb

2 files changed

Lines changed: 200 additions & 37 deletions

File tree

‎packages/sdk/src/model/field/factory.spec.ts‎

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,19 @@ const createBaseField = (): IFieldVo => ({
1414
dbFieldType: DbFieldType.Text,
1515
});
1616

17+
const foreignTableId = 'tblForeign00000001';
18+
const lookupFieldId = 'fldLookup000000001';
19+
const linkFieldId = 'fldLink000000000001';
20+
const sumExpression = 'sum({values})';
21+
1722
describe('createFieldInstance lookup normalization', () => {
1823
it('normalizes v2 conditionalLookup payload shape', () => {
1924
const field = {
2025
...createBaseField(),
2126
type: 'conditionalLookup',
2227
options: {
23-
foreignTableId: 'tblForeign00000001',
24-
lookupFieldId: 'fldLookup000000001',
28+
foreignTableId,
29+
lookupFieldId,
2530
condition: {
2631
filter: {
2732
conjunction: 'and',
@@ -43,8 +48,8 @@ describe('createFieldInstance lookup normalization', () => {
4348
expect(instance.isLookup).toBe(true);
4449
expect(instance.isConditionalLookup).toBe(true);
4550
expect(instance.lookupOptions).toMatchObject({
46-
foreignTableId: 'tblForeign00000001',
47-
lookupFieldId: 'fldLookup000000001',
51+
foreignTableId,
52+
lookupFieldId,
4853
sort: { fieldId: 'fldScore0000000001', order: 'asc' },
4954
limit: 5,
5055
});
@@ -58,9 +63,9 @@ describe('createFieldInstance lookup normalization', () => {
5863
cellValueType: CellValueType.Number,
5964
dbFieldType: DbFieldType.Real,
6065
options: {
61-
linkFieldId: 'fldLink000000000001',
62-
lookupFieldId: 'fldLookup000000001',
63-
foreignTableId: 'tblForeign00000001',
66+
linkFieldId,
67+
lookupFieldId,
68+
foreignTableId,
6469
},
6570
} as unknown as IFieldVo;
6671

@@ -70,9 +75,82 @@ describe('createFieldInstance lookup normalization', () => {
7075
expect(instance.isLookup).toBe(true);
7176
expect(instance.isConditionalLookup).toBeUndefined();
7277
expect(instance.lookupOptions).toMatchObject({
73-
linkFieldId: 'fldLink000000000001',
74-
lookupFieldId: 'fldLookup000000001',
75-
foreignTableId: 'tblForeign00000001',
78+
linkFieldId,
79+
lookupFieldId,
80+
foreignTableId,
81+
});
82+
});
83+
84+
it('normalizes v2 rollup payload shape', () => {
85+
const field = {
86+
...createBaseField(),
87+
type: FieldType.Rollup,
88+
options: {
89+
expression: sumExpression,
90+
formatting: {
91+
type: 'decimal',
92+
precision: 2,
93+
},
94+
},
95+
config: {
96+
linkFieldId,
97+
foreignTableId,
98+
lookupFieldId,
99+
},
100+
cellValueType: CellValueType.Number,
101+
dbFieldType: DbFieldType.Real,
102+
} as unknown as IFieldVo;
103+
104+
const instance = createFieldInstance(field);
105+
106+
expect(instance.type).toBe(FieldType.Rollup);
107+
expect((instance.options as { expression?: string }).expression).toBe(sumExpression);
108+
expect(instance.lookupOptions).toMatchObject({
109+
linkFieldId,
110+
foreignTableId,
111+
lookupFieldId,
112+
});
113+
});
114+
115+
it('normalizes v2 conditionalRollup payload shape', () => {
116+
const field = {
117+
...createBaseField(),
118+
type: FieldType.ConditionalRollup,
119+
options: {
120+
expression: sumExpression,
121+
formatting: {
122+
type: 'decimal',
123+
precision: 2,
124+
},
125+
},
126+
config: {
127+
foreignTableId,
128+
lookupFieldId,
129+
condition: {
130+
filter: {
131+
conjunction: 'and',
132+
filterSet: [{ fieldId: 'fldStatus000000001', operator: 'is', value: 'Active' }],
133+
},
134+
sort: { fieldId: 'fldScore0000000001', order: 'desc' },
135+
limit: 10,
136+
},
137+
},
138+
cellValueType: CellValueType.Number,
139+
dbFieldType: DbFieldType.Real,
140+
} as unknown as IFieldVo;
141+
142+
const instance = createFieldInstance(field);
143+
144+
expect(instance.type).toBe(FieldType.ConditionalRollup);
145+
expect(instance.options).toMatchObject({
146+
expression: sumExpression,
147+
foreignTableId,
148+
lookupFieldId,
149+
sort: { fieldId: 'fldScore0000000001', order: 'desc' },
150+
limit: 10,
76151
});
152+
expect(
153+
(instance.options as { filter?: { filterSet?: unknown[] } }).filter?.filterSet
154+
).toHaveLength(1);
77155
});
78156
});

‎packages/sdk/src/model/field/factory.ts‎

Lines changed: 112 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { SingleLineTextField } from './single-line-text.field';
2323
import { SingleSelectField } from './single-select.field';
2424
import { UserField } from './user.field';
2525

26-
const FIELD_TYPES = new Set<string>(Object.values(FieldType));
26+
const fieldTypes = new Set<string>(Object.values(FieldType));
2727

2828
const isRecordObject = (value: unknown): value is Record<string, unknown> =>
2929
typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -32,7 +32,7 @@ const asRecordObject = (value: unknown): Record<string, unknown> | undefined =>
3232
isRecordObject(value) ? value : undefined;
3333

3434
const isKnownFieldType = (value: unknown): value is FieldType =>
35-
typeof value === 'string' && FIELD_TYPES.has(value);
35+
typeof value === 'string' && fieldTypes.has(value);
3636

3737
const inferLookupInnerType = (field: IFieldVo): FieldType => {
3838
switch (field.cellValueType) {
@@ -47,63 +47,148 @@ const inferLookupInnerType = (field: IFieldVo): FieldType => {
4747
}
4848
};
4949

50-
/**
51-
* Translate v2 lookup DTO shape (type: lookup/conditionalLookup) to the v1-compatible shape
52-
* consumed by the existing SDK field model.
53-
*/
54-
const normalizeV2LookupField = (field: IFieldVo): IFieldVo => {
55-
const rawType = (field as { type: string }).type;
56-
if (rawType !== 'lookup' && rawType !== 'conditionalLookup') {
50+
const normalizeV2RollupField = (field: IFieldVo): IFieldVo => {
51+
if (field.type !== FieldType.Rollup) {
5752
return field;
5853
}
5954

6055
const raw = field as IFieldVo & {
61-
type: string;
62-
innerType?: unknown;
63-
innerOptions?: unknown;
56+
config?: unknown;
57+
lookupOptions?: unknown;
58+
};
59+
const config = asRecordObject(raw.config);
60+
if (!config) {
61+
return field;
62+
}
63+
64+
const lookupOptions = {
65+
...(asRecordObject(raw.lookupOptions) ?? {}),
66+
} as Record<string, unknown>;
67+
68+
if (config.linkFieldId != null) lookupOptions.linkFieldId = config.linkFieldId;
69+
if (config.foreignTableId != null) lookupOptions.foreignTableId = config.foreignTableId;
70+
if (config.lookupFieldId != null) lookupOptions.lookupFieldId = config.lookupFieldId;
71+
72+
return {
73+
...field,
74+
lookupOptions: lookupOptions as IFieldVo['lookupOptions'],
75+
};
76+
};
77+
78+
const normalizeV2ConditionalRollupField = (field: IFieldVo): IFieldVo => {
79+
if (field.type !== FieldType.ConditionalRollup) {
80+
return field;
81+
}
82+
83+
const raw = field as IFieldVo & {
84+
config?: unknown;
6485
options?: unknown;
6586
};
87+
const config = asRecordObject(raw.config);
88+
if (!config) {
89+
return field;
90+
}
6691

67-
const rawOptions = asRecordObject(raw.options as unknown);
92+
const condition = asRecordObject(config.condition);
93+
const options = {
94+
...(asRecordObject(raw.options) ?? {}),
95+
} as Record<string, unknown>;
96+
97+
if (config.foreignTableId != null) options.foreignTableId = config.foreignTableId;
98+
if (config.lookupFieldId != null) options.lookupFieldId = config.lookupFieldId;
99+
if (condition?.filter != null) options.filter = condition.filter;
100+
if (condition?.sort != null) options.sort = condition.sort;
101+
if (condition?.limit != null) options.limit = condition.limit;
102+
103+
return {
104+
...field,
105+
options: options as IFieldVo['options'],
106+
};
107+
};
108+
109+
const resolveLookupInnerType = (
110+
field: IFieldVo,
111+
raw: IFieldVo & {
112+
innerType?: unknown;
113+
options?: unknown;
114+
},
115+
rawOptions: Record<string, unknown> | undefined
116+
): FieldType => {
68117
const innerTypeCandidate =
69118
(typeof raw.innerType === 'string' ? raw.innerType : undefined) ||
70119
(typeof rawOptions?.innerType === 'string' ? rawOptions.innerType : undefined);
71-
const innerType = isKnownFieldType(innerTypeCandidate)
72-
? innerTypeCandidate
73-
: inferLookupInnerType(field);
120+
return isKnownFieldType(innerTypeCandidate) ? innerTypeCandidate : inferLookupInnerType(field);
121+
};
74122

75-
const innerOptions = (raw.innerOptions ?? rawOptions?.innerOptions) as IFieldVo['options'];
76-
const condition = asRecordObject(rawOptions?.condition);
123+
const mergeLookupCondition = (
124+
lookupOptions: Record<string, unknown>,
125+
condition: Record<string, unknown> | undefined
126+
) => {
127+
if (!condition) {
128+
return;
129+
}
130+
131+
if (condition.filter !== undefined) lookupOptions.filter = condition.filter;
132+
if (condition.sort !== undefined) lookupOptions.sort = condition.sort;
133+
if (condition.limit !== undefined) lookupOptions.limit = condition.limit;
134+
};
77135

136+
const buildLookupOptions = (
137+
field: IFieldVo,
138+
rawOptions: Record<string, unknown> | undefined
139+
): IFieldVo['lookupOptions'] => {
78140
const lookupOptions: Record<string, unknown> = {};
141+
79142
if (rawOptions?.foreignTableId != null) lookupOptions.foreignTableId = rawOptions.foreignTableId;
80143
if (rawOptions?.lookupFieldId != null) lookupOptions.lookupFieldId = rawOptions.lookupFieldId;
81144
if (rawOptions?.linkFieldId != null) lookupOptions.linkFieldId = rawOptions.linkFieldId;
82145
if (rawOptions?.filter != null) lookupOptions.filter = rawOptions.filter;
83146
if (rawOptions?.sort != null) lookupOptions.sort = rawOptions.sort;
84147
if (rawOptions?.limit != null) lookupOptions.limit = rawOptions.limit;
85-
if (condition) {
86-
if (condition.filter !== undefined) lookupOptions.filter = condition.filter;
87-
if (condition.sort !== undefined) lookupOptions.sort = condition.sort;
88-
if (condition.limit !== undefined) lookupOptions.limit = condition.limit;
148+
149+
mergeLookupCondition(lookupOptions, asRecordObject(rawOptions?.condition));
150+
151+
return Object.keys(lookupOptions).length > 0
152+
? (lookupOptions as IFieldVo['lookupOptions'])
153+
: field.lookupOptions;
154+
};
155+
156+
/**
157+
* Translate v2 lookup DTO shape (type: lookup/conditionalLookup) to the v1-compatible shape
158+
* consumed by the existing SDK field model.
159+
*/
160+
const normalizeV2LookupField = (field: IFieldVo): IFieldVo => {
161+
const rawType = (field as { type: string }).type;
162+
if (rawType !== 'lookup' && rawType !== 'conditionalLookup') {
163+
return field;
89164
}
90165

166+
const raw = field as IFieldVo & {
167+
type: string;
168+
innerType?: unknown;
169+
innerOptions?: unknown;
170+
options?: unknown;
171+
};
172+
173+
const rawOptions = asRecordObject(raw.options as unknown);
174+
const innerType = resolveLookupInnerType(field, raw, rawOptions);
175+
const innerOptions = (raw.innerOptions ?? rawOptions?.innerOptions) as IFieldVo['options'];
176+
91177
return {
92178
...field,
93179
type: innerType,
94180
isLookup: true,
95181
isConditionalLookup:
96182
rawType === 'conditionalLookup' || field.isConditionalLookup ? true : undefined,
97183
options: innerOptions ?? {},
98-
lookupOptions:
99-
Object.keys(lookupOptions).length > 0
100-
? (lookupOptions as IFieldVo['lookupOptions'])
101-
: field.lookupOptions,
184+
lookupOptions: buildLookupOptions(field, rawOptions),
102185
};
103186
};
104187

105188
export function createFieldInstance(field: IFieldVo, doc?: Doc<IFieldVo>) {
106-
const normalizedField = normalizeV2LookupField(field);
189+
const normalizedField = normalizeV2ConditionalRollupField(
190+
normalizeV2RollupField(normalizeV2LookupField(field))
191+
);
107192
const instance = (() => {
108193
switch (normalizedField.type) {
109194
case FieldType.SingleLineText:

0 commit comments

Comments
 (0)