Skip to content

Commit

Permalink
Better support for non-standard IDs (#263)
Browse files Browse the repository at this point in the history
* add config for custom type keys

* add multiple keys to selection set

* add refetch customization

* failing test

* configure node policy as a default value for the config

* paginate can fall on a custom type

* generate pagination query to match custom ids

* fixed tests

* embed refetch target type so we can figure out the right query field to pull values out of

* pagination handlers look at the custom field for responses with type configs

* compute pagination handler variables using type keys

* TypeConfig.refetch is now required - if `id` isnt a valid key, node(id: ID) wont work

* added changeset

* rename TypeConfig.refetch to TypeConfig.resolve

* Config cannot be imported into runtime

* duplicate id computation logic (will dry up in next PR)

* streamline id computation

* remove trailing __ in id
  • Loading branch information
AlecAivazis authored Apr 11, 2022
1 parent 3f34bff commit c5cce52
Show file tree
Hide file tree
Showing 26 changed files with 685 additions and 172 deletions.
6 changes: 6 additions & 0 deletions .changeset/honest-actors-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'houdini': patch
'houdini-common': patch
---

Added support for non-standard IDs and paginated fragment queries
4 changes: 2 additions & 2 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"@sveltejs/kit": "1.0.0-next.107",
"concurrently": "^6.2.1",
"graphql": "15.5.0",
"houdini": "^0.12.4",
"houdini-preprocess": "^0.12.4",
"houdini": "^0.13.9",
"houdini-preprocess": "^0.13.9",
"svelte": "^3.38.2",
"svelte-preprocess": "^4.0.0",
"tslib": "^2.2.0",
Expand Down
84 changes: 81 additions & 3 deletions packages/houdini-common/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ export type ConfigFile = {
cacheBufferSize?: number
defaultCachePolicy?: CachePolicy
defaultPartial?: boolean
defaultKeys?: string[]
types?: TypeConfig
}

export type TypeConfig = {
[typeName: string]: {
keys?: string[]
resolve: {
queryField: string
arguments?: (data: any) => { [key: string]: any }
}
}
}

export type ScalarSpec = {
Expand Down Expand Up @@ -54,6 +66,16 @@ export class Config {
defaultPartial: boolean
definitionsFile?: string
newSchema: string = ''
defaultKeys: string[] = ['id']
typeConfig: TypeConfig = {
Node: {
keys: ['id'],
resolve: {
queryField: 'node',
arguments: (node) => ({ id: node.id }),
},
},
}

constructor({
schema,
Expand All @@ -70,6 +92,8 @@ export class Config {
definitionsPath,
defaultCachePolicy = CachePolicy.NetworkOnly,
defaultPartial = false,
defaultKeys,
types = {},
}: ConfigFile & { filepath: string }) {
// make sure we got some kind of schema
if (!schema && !schemaPath) {
Expand Down Expand Up @@ -134,6 +158,17 @@ export class Config {
this.defaultPartial = defaultPartial
this.definitionsFile = definitionsPath

// hold onto the key config
if (defaultKeys) {
this.defaultKeys = defaultKeys
}
if (types) {
this.typeConfig = {
...this.typeConfig,
...types,
}
}

// if we are building a sapper project, we want to put the runtime in
// src/node_modules so that we can access @sapper/app and interact
// with the application stores directly
Expand Down Expand Up @@ -194,6 +229,16 @@ export class Config {
return `$houdini/${this.artifactDirectoryName}/${name}`
}

keyFieldsForType(type: string) {
return this.typeConfig[type]?.keys || this.defaultKeys
}

computeID(type: string, data: any): string {
return this.keyFieldsForType(type)
.map((key) => data[key])
.join('__')
}

// a string identifier for the document (must be unique)
documentName(document: graphql.DocumentNode): string {
// if there is an operation in the document
Expand Down Expand Up @@ -461,9 +506,8 @@ export async function getConfig(): Promise<Config> {
return _config
}

export function testConfig(config: Partial<ConfigFile> = {}) {
return new Config({
filepath: path.join(process.cwd(), 'config.cjs'),
export function testConfigFile(config: Partial<ConfigFile> = {}): ConfigFile {
return {
sourceGlob: '123',
schema: `
type User implements Node {
Expand All @@ -476,6 +520,7 @@ export function testConfig(config: Partial<ConfigFile> = {}) {
friendsByOffset(offset: Int, limit: Int, filter: String): [User!]!
friendsInterface: [Friend!]!
believesIn: [Ghost!]!
believesInConnection(first: Int, after: String, filter: String): GhostConnection!
cats: [Cat!]!
field(filter: String): String
}
Expand All @@ -485,6 +530,13 @@ export function testConfig(config: Partial<ConfigFile> = {}) {
aka: String!
believers: [User!]!
friends: [Ghost!]!
friendsConnection(first: Int, after: String): GhostConnection!
legends: [Legend!]!
}
type Legend {
name: String
believers(first: Int, after: String): GhostConnection
}
type Cat implements Friend & Node {
Expand All @@ -497,6 +549,7 @@ export function testConfig(config: Partial<ConfigFile> = {}) {
user: User!
version: Int!
ghost: Ghost!
ghosts: GhostConnection!
friends: [Friend!]!
users(boolValue: Boolean, intValue: Int, floatValue: Float, stringValue: String!): [User!]!
entities: [Entity!]!
Expand Down Expand Up @@ -524,6 +577,16 @@ export function testConfig(config: Partial<ConfigFile> = {}) {
edges: [UserEdge!]!
}
type GhostEdge {
cursor: String!
node: Ghost
}
type GhostConnection {
pageInfo: PageInfo!
edges: [GhostEdge!]!
}
interface Friend {
name: String!
}
Expand Down Expand Up @@ -573,7 +636,22 @@ export function testConfig(config: Partial<ConfigFile> = {}) {
`,
framework: 'sapper',
quiet: true,
types: {
Ghost: {
keys: ['name', 'aka'],
resolve: {
queryField: 'ghost',
},
},
},
...config,
}
}

export function testConfig(config: Partial<ConfigFile> = {}) {
return new Config({
filepath: path.join(process.cwd(), 'config.cjs'),
...testConfigFile(config),
})
}

Expand Down
3 changes: 2 additions & 1 deletion packages/houdini/cmd/generators/artifacts/artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2257,7 +2257,8 @@ describe('mutation artifacts', function () {
path: ["usersByCursor"],
method: "cursor",
pageSize: 10,
embedded: false
embedded: false,
targetType: "Query"
},
raw: \`query TestQuery($first: Int = 10, $after: String) {
Expand Down
9 changes: 6 additions & 3 deletions packages/houdini/cmd/generators/artifacts/pagination.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ test('pagination arguments stripped from key', async function () {
path: ["friendsByCursor"],
method: "cursor",
pageSize: 10,
embedded: true
embedded: true,
targetType: "Node"
},
raw: \`fragment PaginatedFragment on User {
Expand Down Expand Up @@ -165,7 +166,8 @@ test('offset based pagination marks appropriate field', async function () {
path: ["friendsByOffset"],
method: "offset",
pageSize: 10,
embedded: true
embedded: true,
targetType: "Node"
},
raw: \`fragment PaginatedFragment on User {
Expand Down Expand Up @@ -245,7 +247,8 @@ test("sibling aliases don't get marked", async function () {
path: ["friendsByCursor"],
method: "cursor",
pageSize: 10,
embedded: true
embedded: true,
targetType: "Node"
},
raw: \`fragment PaginatedFragment on User {
Expand Down
36 changes: 35 additions & 1 deletion packages/houdini/cmd/transforms/addID.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ test("doesn't add id if there isn't one", async function () {
`
query Friends {
ghost {
aka
legends {
name
}
}
}
`
Expand All @@ -54,6 +56,38 @@ test("doesn't add id if there isn't one", async function () {
expect(docs[0].document).toMatchInlineSnapshot(`
query Friends {
ghost {
legends {
name
}
name
aka
}
}
`)
})

test('adds custom id fields to selection sets of objects with them', async function () {
const docs = [
mockCollectedDoc(
`
query Friends {
ghost {
name
}
}
`
),
]

// run the pipeline
const config = testConfig()
await runPipeline(config, docs)

expect(docs[0].document).toMatchInlineSnapshot(`
query Friends {
ghost {
name
aka
}
}
Expand Down
50 changes: 29 additions & 21 deletions packages/houdini/cmd/transforms/addID.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,38 +35,46 @@ export default async function addID(
return
}

// look up the key fields for a given type
const keyFields = config.keyFieldsForType(fieldType.name)

// if there is no id field of the type
if (!fieldType.getFields()['id']) {
if (keyFields.find((key) => !fieldType.getFields()[key])) {
return
}

// if there is already a selection for id
if (
node.selectionSet.selections.find(
(selection) =>
selection.kind === 'Field' &&
!selection.alias &&
selection.name.value === 'id'
)
) {
return
// add the id fields for the given type
const selections = [...node.selectionSet.selections]

for (const keyField of keyFields) {
// if there is already a selection for id, ignore it
if (
node.selectionSet.selections.find(
(selection) =>
selection.kind === 'Field' &&
!selection.alias &&
selection.name.value === keyField
)
) {
continue
}

// add a selection for the field to the selection set
selections.push({
kind: 'Field',
name: {
kind: 'Name',
value: keyField,
},
})
}

// add the __typename selection to the field's selection set
return {
...node,
selectionSet: {
...node.selectionSet,
selections: [
...node.selectionSet.selections,
{
kind: 'Field',
name: {
kind: 'Name',
value: 'id',
},
},
],
selections,
},
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/houdini/cmd/transforms/lists.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ test('cannot use list directive if id is not a valid field', async function () {
query AllGhosts {
ghost {
friends {
friends @list(name: "Ghost_Friends"){
legends @list(name: "Ghost_Friends"){
name
}
}
Expand Down
Loading

0 comments on commit c5cce52

Please sign in to comment.