Skip to content

Commit 4bdf797

Browse files
committed
test: add unit and integration tests for deduplication
1 parent c00b552 commit 4bdf797

File tree

7 files changed

+692
-4
lines changed

7 files changed

+692
-4
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`SchemaTypeGenerator > deduplication > snapshot of dedup output for queries with shared inline objects 1`] = `
4+
{
5+
"query1": "Array<{
6+
title: string | null;
7+
mainImage: InlineImage | null;
8+
}>",
9+
"query2": "Array<{
10+
name: string | null;
11+
avatar: InlineImage | null;
12+
}>",
13+
}
14+
`;

src/typescript/__tests__/__snapshots__/typeGenerator.test.ts.snap

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,114 @@ export type STRANGE_QUERY_RESULT = Array<{
4747
4848
"
4949
`;
50+
51+
exports[`TypeGenerator > deduplicates repeated inline object types across queries 1`] = `
52+
"export type Post = {
53+
_id: string;
54+
_type: "post";
55+
mainImage?: {
56+
_type: "image";
57+
alt?: string;
58+
url: string;
59+
};
60+
title?: string;
61+
};
62+
63+
export type Author = {
64+
_id: string;
65+
_type: "author";
66+
avatar?: {
67+
_type: "image";
68+
alt?: string;
69+
url: string;
70+
};
71+
name?: string;
72+
};
73+
74+
export type AllSanitySchemaTypes = Post | Author;
75+
76+
export declare const internalGroqTypeReferenceTo: unique symbol;
77+
78+
type InlineImage = {
79+
_type: "image";
80+
alt?: string;
81+
url: string;
82+
};
83+
84+
// Source: posts.ts
85+
// Variable: allPostsQuery
86+
// Query: *[_type == "post"]{_id, title, mainImage}
87+
export type AllPostsQueryResult = Array<{
88+
_id: string;
89+
title: string | null;
90+
mainImage: InlineImage | null;
91+
}>;
92+
93+
// Source: authors.ts
94+
// Variable: allAuthorsQuery
95+
// Query: *[_type == "author"]{_id, name, avatar}
96+
export type AllAuthorsQueryResult = Array<{
97+
_id: string;
98+
name: string | null;
99+
avatar: InlineImage | null;
100+
}>;
101+
102+
"
103+
`;
104+
105+
exports[`TypeGenerator > deduplication with deeply nested shared objects 1`] = `
106+
"export type Page = {
107+
_id: string;
108+
_type: "page";
109+
footer?: {
110+
image?: {
111+
alt?: string;
112+
url: string;
113+
};
114+
text: string;
115+
};
116+
hero?: {
117+
image?: {
118+
alt?: string;
119+
url: string;
120+
};
121+
title: string;
122+
};
123+
};
124+
125+
export type AllSanitySchemaTypes = Page;
126+
127+
export declare const internalGroqTypeReferenceTo: unique symbol;
128+
129+
type InlineType = {
130+
hero: InlineHero | null;
131+
footer: InlineFooter | null;
132+
};
133+
134+
type InlineHero = {
135+
image?: InlineImage;
136+
title: string;
137+
};
138+
139+
type InlineImage = {
140+
alt?: string;
141+
url: string;
142+
};
143+
144+
type InlineFooter = {
145+
image?: InlineImage;
146+
text: string;
147+
};
148+
149+
// Source: pages.ts
150+
// Variable: pagesQuery
151+
// Query: *[_type == "page"]{hero, footer}
152+
export type PagesQueryResult = Array<InlineType>;
153+
154+
// Source: pages.ts
155+
// Variable: singlePageQuery
156+
// Query: *[_type == "page"][0]{hero, footer}
157+
export type SinglePageQueryResult = InlineType | null;
158+
159+
"
160+
`;

src/typescript/__tests__/schemaTypeGenerator.test.ts

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import {CodeGenerator} from '@babel/generator'
22
import * as t from '@babel/types'
3-
import {type TypeNode} from 'groq-js'
3+
import {type ObjectTypeNode, type TypeNode} from 'groq-js'
44
import {describe, expect, test} from 'vitest'
55

66
import {SchemaTypeGenerator, walkAndCountQueryTypeNodeStats} from '../schemaTypeGenerator.js'
7+
import {
8+
buildDeduplicationRegistry,
9+
collectObjectFingerprints,
10+
fingerprintTypeNode,
11+
} from '../typeNodeFingerprint.js'
712

813
function generateCode(node: t.Node | undefined) {
914
if (!node) throw new Error('Node is undefined')
@@ -574,6 +579,130 @@ describe(SchemaTypeGenerator.name, () => {
574579
})
575580
})
576581

582+
describe('deduplication', () => {
583+
// eslint-disable-next-line unicorn/consistent-function-scoping
584+
function buildSchemaWithDuplicateInlineObjects() {
585+
// The inline image object has 2+ meaningful attributes (url, alt) beyond _type,
586+
// which meets the deduplication extraction threshold
587+
const imageAttributes = {
588+
_type: {type: 'objectAttribute' as const, value: {type: 'string' as const, value: 'image'}},
589+
alt: {optional: true, type: 'objectAttribute' as const, value: {type: 'string' as const}},
590+
url: {type: 'objectAttribute' as const, value: {type: 'string' as const}},
591+
}
592+
593+
return new SchemaTypeGenerator([
594+
{
595+
attributes: {
596+
_id: {type: 'objectAttribute', value: {type: 'string'}},
597+
_type: {type: 'objectAttribute', value: {type: 'string', value: 'post'}},
598+
mainImage: {
599+
optional: true,
600+
type: 'objectAttribute',
601+
value: {attributes: imageAttributes, type: 'object'},
602+
},
603+
title: {optional: true, type: 'objectAttribute', value: {type: 'string'}},
604+
},
605+
name: 'post',
606+
type: 'document',
607+
},
608+
{
609+
attributes: {
610+
_id: {type: 'objectAttribute', value: {type: 'string'}},
611+
_type: {type: 'objectAttribute', value: {type: 'string', value: 'author'}},
612+
avatar: {
613+
optional: true,
614+
type: 'objectAttribute',
615+
value: {attributes: imageAttributes, type: 'object'},
616+
},
617+
name: {optional: true, type: 'objectAttribute', value: {type: 'string'}},
618+
},
619+
name: 'author',
620+
type: 'document',
621+
},
622+
])
623+
}
624+
625+
test('setDeduplicationRegistry enables type references in object generation', () => {
626+
const gen = buildSchemaWithDuplicateInlineObjects()
627+
628+
// Evaluate two queries that produce the same inline image object
629+
const {typeNode: tn1} = gen.evaluateQueryTypeNode({
630+
query: '*[_type == "post"]{title, mainImage}',
631+
})
632+
const {typeNode: tn2} = gen.evaluateQueryTypeNode({
633+
query: '*[_type == "author"]{name, avatar}',
634+
})
635+
636+
// Build registry from collected type nodes
637+
const fingerprints = collectObjectFingerprints([tn1, tn2])
638+
const registry = buildDeduplicationRegistry(fingerprints, new Set(['Author', 'Post']))
639+
gen.setDeduplicationRegistry(registry)
640+
641+
// After setting registry, generating TS type should reference the extracted type
642+
const tsType = gen.generateQueryTsType(tn1)
643+
const code = generateCode(tsType)
644+
645+
// We should have exactly one extracted type (the image type)
646+
expect(registry.extractedTypes.size).toBe(1)
647+
648+
// Verify the generated code contains a reference to an extracted type
649+
const extractedNames = [...registry.extractedTypes.values()].map((e) => e.id.name)
650+
const hasRef = extractedNames.some((name) => code.includes(name))
651+
expect(extractedNames).includes('InlineImage')
652+
expect(hasRef).toBe(true)
653+
})
654+
655+
test('generateExtractedTypeTsType expands the object without self-referencing', () => {
656+
const gen = buildSchemaWithDuplicateInlineObjects()
657+
658+
const imageObj: ObjectTypeNode = {
659+
attributes: {
660+
_type: {type: 'objectAttribute', value: {type: 'string', value: 'image'}},
661+
alt: {optional: true, type: 'objectAttribute', value: {type: 'string'}},
662+
url: {type: 'objectAttribute', value: {type: 'string'}},
663+
},
664+
type: 'object',
665+
}
666+
667+
const fp = fingerprintTypeNode(imageObj)
668+
const fingerprints = new Map([[fp, {candidateName: 'image', count: 2, typeNode: imageObj}]])
669+
const registry = buildDeduplicationRegistry(fingerprints, new Set())
670+
gen.setDeduplicationRegistry(registry)
671+
672+
// Generate the extracted type body — it should NOT produce a self-reference
673+
const imageTypeBody = gen.generateExtractedTypeTsType(imageObj, fp)
674+
675+
expect(generateCode(imageTypeBody)).toMatchInlineSnapshot(String.raw`
676+
"{
677+
_type: "image";
678+
alt?: string;
679+
url: string;
680+
}"
681+
`)
682+
})
683+
684+
test('snapshot of dedup output for queries with shared inline objects', () => {
685+
const gen = buildSchemaWithDuplicateInlineObjects()
686+
687+
const {typeNode: tn1} = gen.evaluateQueryTypeNode({
688+
query: '*[_type == "post"]{title, mainImage}',
689+
})
690+
const {typeNode: tn2} = gen.evaluateQueryTypeNode({
691+
query: '*[_type == "author"]{name, avatar}',
692+
})
693+
694+
const fingerprints = collectObjectFingerprints([tn1, tn2])
695+
const registry = buildDeduplicationRegistry(fingerprints, new Set(['Author', 'Post']))
696+
gen.setDeduplicationRegistry(registry)
697+
698+
const code1 = generateCode(gen.generateQueryTsType(tn1))
699+
const code2 = generateCode(gen.generateQueryTsType(tn2))
700+
701+
// Both queries should reference the same extracted type for image
702+
expect({query1: code1, query2: code2}).toMatchSnapshot()
703+
})
704+
})
705+
577706
describe('walkAndCountQueryTypeNodeStats', () => {
578707
test('counts unknown type', () => {
579708
const node: TypeNode = {type: 'unknown'}

0 commit comments

Comments
 (0)