Skip to content

Commit ba4a712

Browse files
authored
chore: add explainer for model introspection schema relationships (#806)
1 parent 04919af commit ba4a712

File tree

2 files changed

+263
-1
lines changed

2 files changed

+263
-1
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"eslint.packageManager": "yarn",
1515
"eslint.quiet": true,
1616
"editor.codeActionsOnSave": {
17-
"source.fixAll.eslint": true
17+
"source.fixAll.eslint": "explicit"
1818
},
1919
"jest.enableInlineErrorMessages": true,
2020
"jest.showCoverageOnLoad": true,

README-relationships.md

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
# Modeling relationships in the introspection schema
2+
3+
## Background
4+
5+
The Model Introspection Schema (MIS) is an intermediate representation of the GraphQL model that includes Amplify annotations. It is different
6+
from the standard [GraphQL introspection schema](https://graphql.org/learn/introspection/) in that it includes relationship information, not
7+
just type information.
8+
9+
> **NOTE:** The MIS is an internal implementation detail of the Amplify API plugin. It should not be used in a customer application.
10+
11+
## Sample
12+
13+
Given a schema like
14+
15+
```graphql
16+
type Primary @model @auth(rules: [{ allow: public, operations: [read] }, { allow: owner }]) {
17+
id: ID! @primaryKey
18+
relatedMany: [RelatedMany] @hasMany(references: "primaryId")
19+
relatedOne: RelatedOne @hasOne(references: "primaryId")
20+
}
21+
22+
type RelatedMany @model @auth(rules: [{ allow: public, operations: [read] }, { allow: owner }]) {
23+
id: ID! @primaryKey
24+
primaryId: ID!
25+
primary: Primary @belongsTo(references: "primaryId")
26+
}
27+
28+
type RelatedOne @model @auth(rules: [{ allow: public, operations: [read] }, { allow: owner }]) {
29+
id: ID! @primaryKey
30+
primaryId: ID!
31+
primary: Primary @belongsTo(references: "primaryId")
32+
}
33+
```
34+
35+
the MIS (abridged to show relationship information only) looks like:
36+
37+
```json
38+
{
39+
"version": 1,
40+
"models": {
41+
"Primary": {
42+
"name": "Primary",
43+
"fields": {
44+
"relatedMany": {
45+
"name": "relatedMany",
46+
"isArray": true,
47+
"type": {
48+
"model": "RelatedMany"
49+
},
50+
"isRequired": false,
51+
"attributes": [],
52+
"isArrayNullable": true,
53+
"association": {
54+
"connectionType": "HAS_MANY",
55+
"associatedWith": ["primaryId"]
56+
}
57+
},
58+
"relatedOne": {
59+
"name": "relatedOne",
60+
"isArray": false,
61+
"type": {
62+
"model": "RelatedOne"
63+
},
64+
"isRequired": false,
65+
"attributes": [],
66+
"association": {
67+
"connectionType": "HAS_ONE",
68+
"associatedWith": ["primaryId"],
69+
}
70+
}
71+
}
72+
},
73+
"RelatedMany": {
74+
"name": "RelatedMany",
75+
"fields": {
76+
"primary": {
77+
"name": "primary",
78+
"isArray": false,
79+
"type": {
80+
"model": "Primary"
81+
},
82+
"isRequired": false,
83+
"attributes": [],
84+
"association": {
85+
"connectionType": "BELONGS_TO",
86+
"targetNames": ["primaryId"]
87+
}
88+
},
89+
"primaryId": {
90+
"name": "primaryId",
91+
"isArray": false,
92+
"type": "ID",
93+
"isRequired": false,
94+
"attributes": []
95+
}
96+
}
97+
},
98+
"RelatedOne": {
99+
"name": "RelatedOne",
100+
"fields": {
101+
"primary": {
102+
"name": "primary",
103+
"isArray": false,
104+
"type": {
105+
"model": "Primary"
106+
},
107+
"isRequired": false,
108+
"attributes": [],
109+
"association": {
110+
"connectionType": "BELONGS_TO",
111+
"targetNames": ["primaryId"]
112+
}
113+
},
114+
"primaryId": {
115+
"name": "primaryId",
116+
"isArray": false,
117+
"type": "ID",
118+
"isRequired": false,
119+
"attributes": []
120+
}
121+
}
122+
}
123+
}
124+
}
125+
```
126+
127+
## Glossary
128+
129+
* **Associated type** - In a field decorated with a `@hasMany`, `@hasOne`, or `@belongsTo` directive, the model “pointed to” by the directive. In the sample schema:
130+
* `Related` is the **associated type** for the `@hasMany` directive on `Primary.related`
131+
* `Primary` is the **associated type** for the `@belongsTo` directive on `Related.primary`
132+
* **Association field** - See **Connection field**
133+
* **Connection field** - In any model type, the field that is decorated with a `@hasMany`, `@hasOne`, or `@belongsTo` directive. In the sample schema:
134+
* `Primary.related` is the **connection field** in the `Primary` model, for the relationship `Primary -> Related` defined by the `@hasMany` on `Primary.related` and the `@belongsTo` on `Related.primary`
135+
* `Related.primary` is the **connection field** in the `Related` model, for the relationship `Primary -> Related` defined by the `@hasMany` on `Primary.related` and the `@belongsTo` on `Related.primary`
136+
* **Source type** - In a field decorated with a `@hasMany`, `@hasOne`, or `@belongsTo` directive, the model containing the directive. In the sample schema:
137+
* `Primary` is the **source type** for the `@hasMany` directive on `Primary.related`
138+
* `Related` is the **source type** for the `@belongsTo` directive on `Related.primary`
139+
140+
## Structure
141+
142+
Relationships are modeled in an `association` structure in the MIS. The `association` attribute must belong to a `@model` field, not a field of non-model type, enum, input, or custom query/mutation.
143+
144+
Here are the relevant types to define the association structure. Note that this is a simplified rendition of the JSON/JavaScript version of the MIS. Other platforms may represent the MIS differently. The full definition is in [source code](./appsync-modelgen-plugin/src/utils/process-connections.ts);
145+
146+
```ts
147+
enum CodeGenConnectionType {
148+
HAS_ONE = 'HAS_ONE',
149+
BELONGS_TO = 'BELONGS_TO',
150+
HAS_MANY = 'HAS_MANY',
151+
}
152+
153+
type CodeGenConnectionTypeBase = {
154+
kind: CodeGenConnectionType;
155+
connectedModel: CodeGenModel;
156+
// ^-- Type not shown
157+
};
158+
159+
type CodeGenFieldConnectionBelongsTo = CodeGenConnectionTypeBase & {
160+
kind: CodeGenConnectionType.BELONGS_TO;
161+
targetNames: string[];
162+
}
163+
164+
type CodeGenFieldConnectionHasOne = CodeGenConnectionTypeBase & {
165+
kind: CodeGenConnectionType.HAS_ONE;
166+
associatedWith: CodeGenField[];
167+
// ^-- Type not shown -- rendered in MIS as a string array
168+
targetNames: string[];
169+
}
170+
171+
export type CodeGenFieldConnectionHasMany = CodeGenConnectionTypeBase & {
172+
kind: CodeGenConnectionType.HAS_MANY;
173+
associatedWith: CodeGenField[];
174+
// ^-- Type not shown -- rendered in MIS as a string array
175+
}
176+
```
177+
178+
Considering a snippet of the above sample:
179+
180+
```json
181+
"models": {
182+
"Primary": {
183+
"name": "Primary",
184+
"fields": {
185+
"relatedMany": {
186+
"name": "relatedMany",
187+
"isArray": true,
188+
"type": {
189+
"model": "RelatedMany"
190+
},
191+
"isRequired": false,
192+
"attributes": [],
193+
"isArrayNullable": true,
194+
"association": {
195+
"connectionType": "HAS_MANY",
196+
"associatedWith": ["primaryId"]
197+
}
198+
},
199+
...
200+
"RelatedMany": {
201+
"name": "RelatedMany",
202+
"fields": {
203+
"primary": {
204+
"name": "primary",
205+
"isArray": false,
206+
"type": {
207+
"model": "Primary"
208+
},
209+
"isRequired": false,
210+
"attributes": [],
211+
"association": {
212+
"connectionType": "BELONGS_TO",
213+
"targetNames": ["primaryId"]
214+
}
215+
},
216+
"primaryId": {
217+
"name": "primaryId",
218+
"isArray": false,
219+
"type": "ID",
220+
"isRequired": false,
221+
"attributes": []
222+
}
223+
```
224+
225+
- `models.Primary` - A type definition. The **source type** for any `association`s defined in this model.
226+
- `models.Primary.fields.relatedMany` - The **association field**/**connection field**
227+
- `models.Primary.fields.relatedMany.type` - The **associated type** for this relationship. This must be a `@model`.
228+
- `models.Primary.fields.relatedMany.association` - The structure containing the data needed to navigate the relationship with the associated type
229+
- `models.Primary.fields.relatedMany.association.connectionType` - The kind of relationship (has one, has many, belongs to) this **source type** has with the associated type
230+
- `models.Primary.fields.relatedMany.association.associatedWith` - A list of fields on the **associated type** that hold the primary key of the **source** record. This is an array so we can support composite primary keys.
231+
- `models.RelatedMany` - A type definition. The **source type** for any `association`s defined in this model.
232+
- `models.RelatedMany.fields.primary.association.targetNames` - A list of fields on the **source type** (that is, the current type) that hold the primary key of the **associated** record. This is an array so we can support composite primary keys.
233+
- `models.RelatedMany.fields.primaryId` - The field pointed to by `targetNames` above, containing the primary key of the **associated** record for the `RelatedOne.primary` relationship.
234+
235+
236+
## Navigating relationships
237+
238+
We will describe the steps to resolve the record in pseudo-sql
239+
240+
### From source record to associated record
241+
242+
* If the source model has an `associatedWith` but no `targetNames`:
243+
```
244+
SELECT *
245+
FROM <associated type>
246+
WHERE <associatedWith fields> = <source type>.primaryKey
247+
```
248+
* If the source model has an `associatedWith` AND `targetNames`:
249+
```
250+
SELECT *
251+
FROM <associated type>
252+
WHERE <associatedWith fields> = <source type>.<targetNames fields>
253+
```
254+
* If the source model has a `targetNames` but no `associatedWith`:
255+
```
256+
SELECT *
257+
FROM <associated type>
258+
WHERE <source type>.<targetNames fields> = <associated type>.primaryKey
259+
```
260+
261+
262+

0 commit comments

Comments
 (0)