-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
app.ts
188 lines (178 loc) · 7.04 KB
/
app.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
import Path from 'path';
import hashString from '@emotion/hash';
import {
executeSync,
GraphQLNonNull,
GraphQLScalarType,
GraphQLSchema,
GraphQLUnionType,
parse,
FragmentDefinitionNode,
SelectionNode,
} from 'graphql';
import { AdminMetaRootVal } from '../../types';
import { staticAdminMetaQuery, StaticAdminMetaQuery } from '../admin-meta-graphql';
import { serializePathForImport } from '../utils/serializePathForImport';
type AppTemplateOptions = { configFileExists: boolean; projectAdminPath: string };
export const appTemplate = (
adminMetaRootVal: AdminMetaRootVal,
graphQLSchema: GraphQLSchema,
{ configFileExists, projectAdminPath }: AppTemplateOptions,
apiPath: string,
isLiveReload: boolean
) => {
const result = executeSync({
document: staticAdminMetaQuery,
schema: graphQLSchema,
contextValue: { isAdminUIBuildProcess: true },
});
if (result.errors) {
throw result.errors[0];
}
const { adminMeta } = result.data!.keystone;
const adminMetaQueryResultHash = hashString(JSON.stringify(adminMeta));
const allViews = adminMetaRootVal.views.map(views => {
// webpack/next for some reason _sometimes_ adds a query parameter to the return of require.resolve
// because it does it _sometimes_, we have to remove it so that during live reloading
// we're not constantly doing builds because the query param is there and then it's not and then it is and so on
views = views.replace(/\?[A-Za-z0-9]+$/, '');
// during a live reload, we'll have paths from a webpack compilation which will make the paths
// that __dirname/__filename/require.resolve return relative to the webpack's "context" option
// which for Next, it's set to the directory of the Next project which is projectAdminPath here.
// so to get absolute paths, we need to resolve them relative to the projectAdminPath
// generally though, relative paths are problematic because
// we don't know where to resolve them from so we disallow them
// we're assuming that the relative paths we get
// of course, this isn't necessarily true but it's kinda the best we can do
// this means that if someone writes a relative path as a view during live reloading
// they'll get a more confusing error than they would get at startup
if (isLiveReload) {
views = Path.resolve(projectAdminPath, views);
} else if (!Path.isAbsolute(views)) {
throw new Error(
`Field views must be absolute paths, but ${JSON.stringify(
views
)} was provided. Use path.join(__dirname, './relative/path') or require.resolve('./relative/path') to get an absolute path.`
);
}
const viewPath = Path.relative(Path.join(projectAdminPath, 'pages'), views);
return serializePathForImport(viewPath);
});
// -- TEMPLATE START
return `import { getApp } from '@keystone-next/keystone/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App';
${allViews.map((views, i) => `import * as view${i} from ${views};`).join('\n')}
${
configFileExists
? `import * as adminConfig from "../../../admin/config";`
: 'var adminConfig = {};'
}
export default getApp({
lazyMetadataQuery: ${JSON.stringify(getLazyMetadataQuery(graphQLSchema, adminMeta))},
fieldViews: [${allViews.map((_, i) => `view${i}`)}],
adminMetaHash: "${adminMetaQueryResultHash}",
adminConfig: adminConfig,
apiPath: "${apiPath}",
});
`;
// -- TEMPLATE END
};
function getLazyMetadataQuery(
graphqlSchema: GraphQLSchema,
adminMeta: StaticAdminMetaQuery['keystone']['adminMeta']
) {
const selections = (
parse(`fragment x on y {
keystone {
adminMeta {
lists {
key
isHidden
fields {
path
createView {
fieldMode
}
}
}
}
}
}`).definitions[0] as FragmentDefinitionNode
).selectionSet.selections as SelectionNode[];
const queryType = graphqlSchema.getQueryType();
if (queryType) {
const getListByKey = (name: string) => adminMeta.lists.find(({ key }: any) => key === name);
const fields = queryType.getFields();
if (fields['authenticatedItem'] !== undefined) {
const authenticatedItemType = fields['authenticatedItem'].type;
if (
!(authenticatedItemType instanceof GraphQLUnionType) ||
authenticatedItemType.name !== 'AuthenticatedItem'
) {
throw new Error(
`The type of Query.authenticatedItem must be a type named AuthenticatedItem and be a union of types that refer to Keystone lists but it is "${authenticatedItemType.toString()}"`
);
}
for (const type of authenticatedItemType.getTypes()) {
const fields = type.getFields();
const list = getListByKey(type.name);
if (list === undefined) {
throw new Error(
`All members of the AuthenticatedItem union must refer to Keystone lists but "${type.name}" is in the AuthenticatedItem union but is not a Keystone list`
);
}
let labelGraphQLField = fields[list.labelField];
if (labelGraphQLField === undefined) {
throw new Error(
`The labelField for the list "${list.key}" is "${list.labelField}" but the GraphQL type does not have a field named "${list.labelField}"`
);
}
let labelGraphQLFieldType = labelGraphQLField.type;
if (labelGraphQLFieldType instanceof GraphQLNonNull) {
labelGraphQLFieldType = labelGraphQLFieldType.ofType;
}
if (!(labelGraphQLFieldType instanceof GraphQLScalarType)) {
throw new Error(
`Label fields must be scalar GraphQL types but the labelField "${list.labelField}" on the list "${list.key}" is not a scalar type`
);
}
const requiredArgs = labelGraphQLField.args.filter(
arg => arg.defaultValue === undefined && arg.type instanceof GraphQLNonNull
);
if (requiredArgs.length) {
throw new Error(
`Label fields must have no required arguments but the labelField "${list.labelField}" on the list "${list.key}" has a required argument "${requiredArgs[0].name}"`
);
}
}
selections.push({
kind: 'Field',
name: { kind: 'Name', value: 'authenticatedItem' },
selectionSet: {
kind: 'SelectionSet',
selections: authenticatedItemType.getTypes().map(({ name }) => ({
kind: 'InlineFragment',
typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: name } },
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: getListByKey(name)!.labelField } },
],
},
})),
},
});
}
}
// We're returning the complete query AST here for explicit-ness
return {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
selectionSet: { kind: 'SelectionSet', selections },
},
],
};
}