-
Notifications
You must be signed in to change notification settings - Fork 824
/
process-connections.ts
237 lines (224 loc) · 8.94 KB
/
process-connections.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
import { CodeGenModel, CodeGenModelMap, CodeGenField, CodeGenDirective } from '../visitors/appsync-visitor';
import { camelCase } from 'change-case';
export enum CodeGenConnectionType {
HAS_ONE = 'HAS_ONE',
BELONGS_TO = 'BELONGS_TO',
HAS_MANY = 'HAS_MANY',
}
export const DEFAULT_HASH_KEY_FIELD = 'id';
export type CodeGenConnectionTypeBase = {
kind: CodeGenConnectionType;
connectedModel: CodeGenModel;
isConnectingFieldAutoCreated: boolean;
};
export type CodeGenFieldConnectionBelongsTo = CodeGenConnectionTypeBase & {
kind: CodeGenConnectionType.BELONGS_TO;
targetName: string;
};
export type CodeGenFieldConnectionHasOne = CodeGenConnectionTypeBase & {
kind: CodeGenConnectionType.HAS_ONE;
associatedWith: CodeGenField;
};
export type CodeGenFieldConnectionHasMany = CodeGenConnectionTypeBase & {
kind: CodeGenConnectionType.HAS_MANY;
associatedWith: CodeGenField;
};
export type CodeGenFieldConnection = CodeGenFieldConnectionBelongsTo | CodeGenFieldConnectionHasOne | CodeGenFieldConnectionHasMany;
function getDirective(fieldOrModel: CodeGenField | CodeGenModel) {
return (directiveName: string): CodeGenDirective | undefined => {
return fieldOrModel.directives.find(d => d.name === directiveName);
};
}
export function makeConnectionAttributeName(type: string, field?: string) {
// The same logic is used graphql-connection-transformer package to generate association field
// Make sure the logic gets update in that package
return field ? camelCase([type, field, 'id'].join('_')) : camelCase([type, 'id'].join('_'));
}
export function getConnectedField(field: CodeGenField, model: CodeGenModel, connectedModel: CodeGenModel): CodeGenField {
const connectionInfo = getDirective(field)('connection');
if (!connectionInfo) {
throw new Error(`The ${field.name} on model ${model.name} is not connected`);
}
const connectionName = connectionInfo.arguments.name;
const keyName = connectionInfo.arguments.keyName;
const connectionFields = connectionInfo.arguments.fields;
if (connectionFields) {
let keyDirective;
if (keyName) {
throw new Error('DataStore does not support connection directive with keyName');
// When the library starts supporting remove the exception
// keyDirective = connectedModel.directives.find(dir => {
// return dir.name === 'key' && dir.arguments.name === keyName;
// });
// if (!keyDirective) {
// throw new Error(
// `Error processing @connection directive on ${model.name}.${field.name}, @key directive with name ${keyName} was not found in connected model ${connectedModel.name}`,
// );
// }
} else {
keyDirective = connectedModel.directives.find(dir => {
return dir.name === 'key' && typeof dir.arguments.name === 'undefined';
});
}
// when there is a fields argument in the connection
const connectedFieldName = keyDirective ? keyDirective.arguments.fields[0] : DEFAULT_HASH_KEY_FIELD;
// Find a field on the other side which connected by a @connection and has the same fields[0] as keyName field
const otherSideConnectedField = connectedModel.fields.find(f => {
return f.directives.find(d => {
return d.name === 'connection' && d.arguments.fields && d.arguments.fields[0] === connectedFieldName;
});
});
if (otherSideConnectedField) {
return otherSideConnectedField;
}
// If there are no field with @connection with keyName then try to find a field that has same name as connection name
const connectedField = connectedModel.fields.find(f => f.name === connectedFieldName);
if (!connectedField) {
throw new Error(`Can not find key field ${connectedFieldName} in ${connectedModel}`);
}
return connectedField;
} else if (connectionName) {
// when the connection is named
const connectedField = connectedModel.fields.find(f =>
f.directives.find(d => d.name === 'connection' && d.arguments.name === connectionName && f !== field),
);
if (!connectedField) {
throw new Error(`Can not find key field with connection name ${connectionName} in ${connectedModel}`);
}
return connectedField;
}
// un-named connection. Use an existing field or generate a new field
const connectedFieldName = makeConnectionAttributeName(model.name, field.name);
const connectedField = connectedModel.fields.find(f => f.name === connectedFieldName);
return connectedField
? connectedField
: {
name: connectedFieldName,
directives: [],
type: 'ID',
isList: false,
isNullable: true,
};
}
export function processConnections(
field: CodeGenField,
model: CodeGenModel,
modelMap: CodeGenModelMap,
): CodeGenFieldConnection | undefined {
const connectionDirective = field.directives.find(d => d.name === 'connection');
if (connectionDirective) {
const otherSide = modelMap[field.type];
const connectionFields = connectionDirective.arguments.fields || [];
const otherSideField = getConnectedField(field, model, otherSide);
const isNewField = !otherSide.fields.includes(otherSideField);
// if a type is connected using name, then graphql-connection-transformer adds a field to
// track the connection and that field is not part of the selection set
// but if the field are connected using fields argument in connection directive
// we are reusing the field and it should be preserved in selection set
const isConnectingFieldAutoCreated = connectionFields.length === 0;
if (!isNewField) {
// 2 way connection
if (field.isList && !otherSideField.isList) {
// Many to One
return {
kind: CodeGenConnectionType.HAS_MANY,
associatedWith: otherSideField,
isConnectingFieldAutoCreated,
connectedModel: otherSide,
};
} else if (!field.isList && otherSideField.isList) {
// One to Many
if (connectionFields.length > 1) {
// Todo: Move to a common function and update the error message
throw new Error('DataStore only support one key in field');
}
return {
kind: CodeGenConnectionType.BELONGS_TO,
connectedModel: otherSide,
isConnectingFieldAutoCreated,
targetName: connectionFields[0] || makeConnectionAttributeName(model.name, field.name),
};
} else if (!field.isList && !otherSideField.isList) {
// One to One
// Data store can only support models where 1:1 connection, one of the connection side should be
// Non null able to support the foreign key constrain.
if (!field.isNullable && otherSideField.isNullable) {
/*
# model
type Person { # hasOne
license: License;
}
# otherSide
type License { # belongsTo
person: Person!
}
*/
return {
kind: CodeGenConnectionType.BELONGS_TO,
connectedModel: otherSide,
isConnectingFieldAutoCreated,
targetName: connectionFields[0] || makeConnectionAttributeName(model.name, field.name),
};
} else if (field.isNullable && !otherSideField.isNullable) {
/*
# model
type License { # belongsTo
person: Person!
}
# otherSide
type Person { # hasOne
license: License;
}
*/
return {
kind: CodeGenConnectionType.HAS_ONE,
associatedWith: otherSideField,
connectedModel: otherSide,
isConnectingFieldAutoCreated,
};
} else {
/*
# model
type License { # belongsTo
person: Person!
}
# otherSide
type Person { # hasOne
license: License;
}
*/
throw new Error('DataStore does not support 1 to 1 connection with both sides of connection as optional field');
}
}
} else {
// one way connection
if (field.isList) {
const connectionFieldName = makeConnectionAttributeName(model.name, field.name);
const existingConnectionField = otherSide.fields.find(f => f.name === connectionFieldName);
return {
kind: CodeGenConnectionType.HAS_MANY,
connectedModel: otherSide,
isConnectingFieldAutoCreated,
associatedWith: existingConnectionField || {
name: connectionFieldName,
type: 'ID',
isList: false,
isNullable: true,
directives: [],
},
};
} else {
if (connectionFields.length > 1) {
// Todo: Update the message
throw new Error('DataStore only support one key in field');
}
return {
kind: CodeGenConnectionType.BELONGS_TO,
connectedModel: otherSide,
isConnectingFieldAutoCreated,
targetName: connectionFields[0] || makeConnectionAttributeName(model.name, field.name),
};
}
}
}
}