Skip to content

Commit dc9d58d

Browse files
authored
Merge pull request #517 from abraham/copilot/update-entity-attributes-nullability
Fix: Don't mark entity attributes as nullable when all added in same version
2 parents 934c0d0 + 2e80867 commit dc9d58d

File tree

8 files changed

+868
-370
lines changed

8 files changed

+868
-370
lines changed

dist/schema.json

Lines changed: 261 additions & 366 deletions
Large diffs are not rendered by default.
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { EntityParser } from '../../parsers/EntityParser';
2+
import { OpenAPIGenerator } from '../../generators/OpenAPIGenerator';
3+
4+
describe('Entity Same Version Nullable - Integration Test', () => {
5+
it('should not mark AsyncRefresh attributes as nullable since all were added in 4.4.0', () => {
6+
const parser = new EntityParser();
7+
const entities = parser.parseAllEntities();
8+
9+
const asyncRefreshEntity = entities.find((e) => e.name === 'AsyncRefresh');
10+
11+
expect(asyncRefreshEntity).toBeDefined();
12+
13+
if (asyncRefreshEntity) {
14+
// Check that attributes are marked correctly
15+
const idAttr = asyncRefreshEntity.attributes.find((a) => a.name === 'id');
16+
const statusAttr = asyncRefreshEntity.attributes.find(
17+
(a) => a.name === 'status'
18+
);
19+
const resultCountAttr = asyncRefreshEntity.attributes.find(
20+
(a) => a.name === 'result_count'
21+
);
22+
23+
// id and status should NOT be nullable (no explicit nullable marker)
24+
expect(idAttr).toBeDefined();
25+
expect(idAttr?.nullable).toBeUndefined();
26+
expect(idAttr?.versions).toEqual(['4.4.0']);
27+
28+
expect(statusAttr).toBeDefined();
29+
expect(statusAttr?.nullable).toBeUndefined();
30+
expect(statusAttr?.versions).toEqual(['4.4.0']);
31+
32+
// result_count should remain nullable (explicitly marked with {{<nullable>}})
33+
expect(resultCountAttr).toBeDefined();
34+
expect(resultCountAttr?.nullable).toBe(true);
35+
expect(resultCountAttr?.explicitlyNullable).toBe(true);
36+
expect(resultCountAttr?.optional).toBe(true);
37+
expect(resultCountAttr?.versions).toEqual(['4.4.0']);
38+
}
39+
});
40+
41+
it('should generate correct OpenAPI schema for AsyncRefresh without nullable types', () => {
42+
const parser = new EntityParser();
43+
const entities = parser.parseAllEntities();
44+
const generator = new OpenAPIGenerator();
45+
46+
const spec = generator.generateSchema(entities, []);
47+
48+
const asyncRefreshSchema = spec.components?.schemas?.AsyncRefresh;
49+
50+
expect(asyncRefreshSchema).toBeDefined();
51+
52+
if (asyncRefreshSchema && 'properties' in asyncRefreshSchema) {
53+
// Check id property - should be string, not ["string", "null"]
54+
const idProp = asyncRefreshSchema.properties?.id;
55+
expect(idProp).toBeDefined();
56+
if (idProp && 'type' in idProp) {
57+
expect(idProp.type).toBe('string');
58+
expect(idProp.type).not.toEqual(['string', 'null']);
59+
}
60+
61+
// Check status property - should be string, not ["string", "null"]
62+
const statusProp = asyncRefreshSchema.properties?.status;
63+
expect(statusProp).toBeDefined();
64+
if (statusProp && 'type' in statusProp) {
65+
expect(statusProp.type).toBe('string');
66+
expect(statusProp.type).not.toEqual(['string', 'null']);
67+
}
68+
69+
// Check result_count property - should be nullable (explicitly marked)
70+
const resultCountProp = asyncRefreshSchema.properties?.result_count;
71+
expect(resultCountProp).toBeDefined();
72+
if (resultCountProp && 'type' in resultCountProp) {
73+
// result_count is explicitly nullable, so it should be ["integer", "null"]
74+
expect(resultCountProp.type).toEqual(['integer', 'null']);
75+
}
76+
77+
// Check required array - should include id and status, but not result_count (optional)
78+
expect(asyncRefreshSchema.required).toContain('id');
79+
expect(asyncRefreshSchema.required).toContain('status');
80+
expect(asyncRefreshSchema.required).not.toContain('result_count');
81+
}
82+
});
83+
84+
it('should still mark attributes as nullable when added in different versions', () => {
85+
const parser = new EntityParser();
86+
const entities = parser.parseAllEntities();
87+
88+
// Account entity has attributes added in different versions
89+
const accountEntity = entities.find((e) => e.name === 'Account');
90+
91+
expect(accountEntity).toBeDefined();
92+
93+
if (accountEntity) {
94+
// Some newer attributes should still be nullable
95+
const hideCollectionsAttr = accountEntity.attributes.find(
96+
(a) => a.name === 'hide_collections'
97+
);
98+
99+
// This was added in 4.3.0, so it should be nullable for backwards compatibility
100+
if (hideCollectionsAttr) {
101+
expect(hideCollectionsAttr.versions).toContain('4.3.0');
102+
expect(hideCollectionsAttr.nullable).toBe(true);
103+
}
104+
}
105+
});
106+
107+
it('should handle Admin::Dimension correctly (all attributes added in 3.5.0)', () => {
108+
const parser = new EntityParser();
109+
const entities = parser.parseAllEntities();
110+
111+
const dimensionEntity = entities.find((e) => e.name === 'Admin::Dimension');
112+
113+
expect(dimensionEntity).toBeDefined();
114+
115+
if (dimensionEntity) {
116+
// All attributes were added in 3.5.0, so none should be nullable
117+
for (const attr of dimensionEntity.attributes) {
118+
if (attr.versions && attr.versions.includes('3.5.0')) {
119+
expect(attr.nullable).toBeUndefined();
120+
}
121+
}
122+
}
123+
});
124+
125+
it('should preserve optional flag for explicitly optional attributes', () => {
126+
const parser = new EntityParser();
127+
const entities = parser.parseAllEntities();
128+
129+
// Admin::DimensionData has some explicitly optional attributes
130+
const dimensionDataEntity = entities.find(
131+
(e) => e.name === 'Admin::DimensionData'
132+
);
133+
134+
expect(dimensionDataEntity).toBeDefined();
135+
136+
if (dimensionDataEntity) {
137+
const unitAttr = dimensionDataEntity.attributes.find(
138+
(a) => a.name === 'unit'
139+
);
140+
const humanValueAttr = dimensionDataEntity.attributes.find(
141+
(a) => a.name === 'human_value'
142+
);
143+
144+
// These were explicitly marked as optional in the docs
145+
expect(unitAttr?.optional).toBe(true);
146+
expect(unitAttr?.nullable).toBeUndefined();
147+
148+
expect(humanValueAttr?.optional).toBe(true);
149+
expect(humanValueAttr?.nullable).toBeUndefined();
150+
}
151+
});
152+
153+
it('should preserve explicitly nullable attributes (Conversation#last_status)', () => {
154+
const parser = new EntityParser();
155+
const entities = parser.parseAllEntities();
156+
157+
const conversationEntity = entities.find((e) => e.name === 'Conversation');
158+
159+
expect(conversationEntity).toBeDefined();
160+
161+
if (conversationEntity) {
162+
// All attributes were added in 2.6.0
163+
const idAttr = conversationEntity.attributes.find((a) => a.name === 'id');
164+
const unreadAttr = conversationEntity.attributes.find(
165+
(a) => a.name === 'unread'
166+
);
167+
const lastStatusAttr = conversationEntity.attributes.find(
168+
(a) => a.name === 'last_status'
169+
);
170+
171+
// id and unread should NOT be nullable (version-based nullable removed)
172+
expect(idAttr?.nullable).toBeUndefined();
173+
expect(idAttr?.versions).toEqual(['2.6.0']);
174+
175+
expect(unreadAttr?.nullable).toBeUndefined();
176+
expect(unreadAttr?.versions).toEqual(['2.6.0']);
177+
178+
// last_status should remain nullable because it's explicitly marked as nullable
179+
expect(lastStatusAttr?.nullable).toBe(true);
180+
expect(lastStatusAttr?.explicitlyNullable).toBe(true);
181+
expect(lastStatusAttr?.versions).toEqual(['2.6.0']);
182+
}
183+
});
184+
});

0 commit comments

Comments
 (0)