Skip to content

Commit a305c87

Browse files
author
ErikDakoda
committed
Aggregation
* This PR enables you to query data on the server with a schema that does not match a database collection - for example an aggregation or an array of objects * PR for the documentation is [here](VulcanJS/vulcan-docs#169) * Added new function `registerCustomQuery()`, to add a custom GraphQL type generated from a schema object and a resolver to query the data * Added new function `registerCustomDefaultFragment()`, to generate a default fragment from the same schema * Updated `multi2` to be compatible with custom queries - you can now specify a `typeName` instead of a `collection` * Updated `createSchema()` to support [SimpleSchema Shorthand Definitions](https://github.com/aldeed/simpl-schema#shorthand-definitions) * Added `defaultCanRead` parameter to `createSchema()`
1 parent c60f8cd commit a305c87

File tree

7 files changed

+268
-98
lines changed

7 files changed

+268
-98
lines changed

packages/vulcan-core/lib/modules/containers/create.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/*
22
33
Generic mutation wrapper to insert a new document in a collection and update
4-
a related query on the client with the new item and a new total item count.
4+
a related query on the client with the new item and a new total item count.
55
6-
Sample mutation:
6+
Sample mutation:
77
88
mutation createMovie($data: CreateMovieData) {
99
createMovie(data: $data) {
@@ -16,14 +16,14 @@ Sample mutation:
1616
}
1717
}
1818
19-
Arguments:
19+
Arguments:
2020
2121
- data: the document to insert
2222
2323
Child Props:
2424
2525
- createMovie({ data })
26-
26+
2727
*/
2828

2929
import React from 'react';
@@ -55,7 +55,7 @@ export const multiQueryUpdater = ({
5555
const multiResolverName = collection.options.multiResolverName;
5656
// update multi queries
5757
const multiQuery = buildMultiQuery({ typeName, fragmentName, fragment });
58-
const newDoc = data[resolverName].data;
58+
const newDoc = data[resolverName]?.data;
5959
// get all the resolvers that match
6060
const variablesList = getVariablesListFromCache(cache, multiResolverName);
6161
variablesList.forEach(async variables => {

packages/vulcan-core/lib/modules/containers/multi2.js

+21-9
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Options:
1717
- search
1818
- offset
1919
- limit
20-
20+
2121
*/
2222

2323
import React from 'react';
@@ -29,6 +29,7 @@ import {
2929
multiClientTemplate,
3030
extractCollectionInfo,
3131
extractFragmentInfo,
32+
multiQueryType,
3233
} from 'meteor/vulcan:lib';
3334
import merge from 'lodash/merge';
3435
import get from 'lodash/get';
@@ -195,13 +196,21 @@ export const useMulti = (options, props = {}) => {
195196
const initialPaginationInput = getInitialPaginationInput(options, props);
196197
const [paginationInput, setPaginationInput] = useState(initialPaginationInput);
197198

198-
let { extraQueries } = options;
199+
let { extraQueries, typeName, resolverName, collectionName, collection } = options;
199200

200-
const { collectionName, collection } = extractCollectionInfo(options);
201-
const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);
201+
if (!typeName) {
202+
const collectionInfo = extractCollectionInfo(options);
203+
collectionName = collectionInfo.collectionName;
204+
collection = collectionInfo.collection;
205+
typeName = collection.options.typeName;
206+
resolverName = collection.options.multiResolverName;
207+
}
208+
209+
if (!resolverName) {
210+
resolverName = multiQueryType(typeName);
211+
}
202212

203-
const typeName = collection.options.typeName;
204-
const resolverName = collection.options.multiResolverName;
213+
const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);
205214

206215
// build graphql query from options
207216
const query = buildMultiQuery({ typeName, fragmentName, extraQueries, fragment });
@@ -211,7 +220,7 @@ export const useMulti = (options, props = {}) => {
211220

212221
// workaround for https://github.com/apollographql/apollo-client/issues/2810
213222
queryRes.graphQLErrors = get(queryRes, 'error.networkError.result.errors');
214-
223+
215224
const result = buildResult(
216225
options,
217226
{ fragment, fragmentName, resolverName },
@@ -223,8 +232,11 @@ export const useMulti = (options, props = {}) => {
223232
};
224233

225234
export const withMulti = options => C => {
226-
const { collection } = extractCollectionInfo(options);
227-
const typeName = collection.options.typeName;
235+
let { typeName } = options;
236+
if (!typeName) {
237+
const { collection } = extractCollectionInfo(options);
238+
typeName = collection.options.typeName;
239+
}
228240
const Wrapped = props => {
229241
const res = useMulti(options, props);
230242
return <C {...props} {...res} />;

packages/vulcan-lib/lib/modules/graphql/defaultFragment.js

+102-69
Original file line numberDiff line numberDiff line change
@@ -2,93 +2,126 @@
22
* Generates the default fragment for a collection
33
* = a fragment containing all fields
44
*/
5-
import { getFragmentFieldNames } from '../schema_utils';
5+
import { getFragmentFieldNames, createSchema } from '../schema_utils';
66
import { isBlackbox } from '../simpleSchema_utils';
7+
import { registerFragment } from '../fragments.js';
8+
79

810
const intlSuffix = '_intl';
911

1012
// get fragment for a whole object (root schema or nested schema of an object or an array)
11-
const getObjectFragment = ({
12-
schema,
13-
fragmentName,
14-
options
13+
export const getObjectFragment = ({
14+
schema,
15+
fragmentName,
16+
options,
1517
}) => {
16-
const fieldNames = getFragmentFieldNames({ schema, options });
17-
const childFragments = fieldNames.length && fieldNames.map(fieldName => getFieldFragment({
18-
schema,
19-
fieldName,
20-
options,
21-
getObjectFragment: getObjectFragment
22-
}))
23-
// remove empty values
24-
.filter(f => !!f);
25-
if (childFragments.length) {
26-
return `${fragmentName} { ${childFragments.join('\n')} }`;
27-
}
28-
return null;
18+
const fieldNames = getFragmentFieldNames({ schema, options });
19+
const childFragments = fieldNames.length && fieldNames.map(fieldName => getFieldFragment({
20+
schema,
21+
fieldName,
22+
options,
23+
getObjectFragment: getObjectFragment,
24+
}))
25+
// remove empty values
26+
.filter(f => !!f);
27+
if (childFragments.length) {
28+
return `${fragmentName} { ${childFragments.join('\n')} }`;
29+
}
30+
return null;
2931
};
3032

3133
// get fragment for a specific field (either the field name or a nested fragment)
3234
export const getFieldFragment = ({
33-
schema,
34-
fieldName,
35-
options,
36-
getObjectFragment = getObjectFragment // a callback to call on nested schema
35+
schema,
36+
fieldName,
37+
options,
38+
getObjectFragment = getObjectFragment, // a callback to call on nested schema
3739
}) => {
38-
// intl
39-
if (fieldName.slice(-5) === intlSuffix) {
40-
return `${fieldName}{ locale value }`;
41-
}
42-
if (fieldName === '_id') return fieldName;
43-
const field = schema[fieldName];
44-
45-
const fieldType = field.type.singleType;
46-
const fieldTypeName =
47-
typeof fieldType === 'object' ? 'Object' : typeof fieldType === 'function' ? fieldType.name : fieldType;
48-
49-
switch (fieldTypeName) {
50-
case 'Object':
51-
if (!isBlackbox(field) && fieldType._schema) {
52-
return getObjectFragment({
53-
fragmentName: fieldName,
54-
schema: fieldType._schema,
55-
options
56-
}) || null;
57-
}
58-
return fieldName;
59-
case 'Array':
60-
const arrayItemFieldName = `${fieldName}.$`;
61-
const arrayItemField = schema[arrayItemFieldName];
62-
// note: make sure field has an associated array item field
63-
if (arrayItemField) {
64-
// child will either be native value or a an object (first case)
65-
const arrayItemFieldType = arrayItemField.type.singleType;
66-
if (!isBlackbox(field) && arrayItemFieldType._schema) {
67-
return getObjectFragment({
68-
fragmentName: fieldName,
69-
schema: arrayItemFieldType._schema,
70-
options
71-
}) || null;
72-
}
73-
}
74-
return fieldName;
75-
default:
76-
return fieldName; // fragment = fieldName
77-
}
40+
// intl
41+
if (fieldName.slice(-5) === intlSuffix) {
42+
return `${fieldName}{ locale value }`;
43+
}
44+
if (fieldName === '_id') return fieldName;
45+
const field = schema[fieldName];
46+
47+
const fieldType = field.type.singleType;
48+
const fieldTypeName =
49+
typeof fieldType === 'object' ? 'Object' : typeof fieldType === 'function' ? fieldType.name : fieldType;
50+
51+
switch (fieldTypeName) {
52+
case 'Object':
53+
if (!isBlackbox(field) && fieldType._schema) {
54+
return getObjectFragment({
55+
fragmentName: fieldName,
56+
schema: fieldType._schema,
57+
options,
58+
}) || null;
59+
}
60+
return fieldName;
61+
case 'Array':
62+
const arrayItemFieldName = `${fieldName}.$`;
63+
const arrayItemField = schema[arrayItemFieldName];
64+
// note: make sure field has an associated array item field
65+
if (arrayItemField) {
66+
// child will either be native value or a an object (first case)
67+
const arrayItemFieldType = arrayItemField.type.singleType;
68+
if (!isBlackbox(field) && arrayItemFieldType._schema) {
69+
return getObjectFragment({
70+
fragmentName: fieldName,
71+
schema: arrayItemFieldType._schema,
72+
options,
73+
}) || null;
74+
}
75+
}
76+
return fieldName;
77+
default:
78+
return fieldName; // fragment = fieldName
79+
}
7880
};
7981

82+
8083
/*
8184
8285
Create default "dumb" gql fragment object for a given collection
8386
8487
*/
8588
export const getDefaultFragmentText = (collection, options = { onlyViewable: true }) => {
86-
const schema = collection.simpleSchema()._schema;
87-
return getObjectFragment({
88-
schema,
89-
fragmentName: `fragment ${collection.options.collectionName}DefaultFragment on ${collection.typeName}`,
90-
options
91-
}) || null;
89+
const schema = collection.simpleSchema()._schema;
90+
return getObjectFragment({
91+
schema,
92+
fragmentName: `fragment ${collection.options.collectionName}DefaultFragment on ${collection.typeName}`,
93+
options,
94+
}) || null;
9295
};
9396

94-
export default getDefaultFragmentText;
97+
export default getDefaultFragmentText;
98+
99+
100+
/**
101+
* Generates and registers a default fragment for a typeName registered using `registerCustomQuery()`
102+
* @param {string} typeName The GraphQL Type registered using `registerCustomQuery()`
103+
* @param {Object|SimpleSchema} [schema] Schema definition object to convert to a fragment
104+
* @param {String} [fragmentName] The fragment's name; if omitted `${typeName}DefaultFragment` will be used
105+
* @param {[String]} [defaultCanRead] Fields in the schema without `canRead` will be assigned these read permissions
106+
* @param {Object} options Options sent to `getObjectFragment()`
107+
*/
108+
export const registerCustomDefaultFragment = function ({
109+
typeName,
110+
schema,
111+
fragmentName,
112+
defaultCanRead,
113+
options = { onlyViewable: true },
114+
}) {
115+
const simpleSchema = createSchema(schema, undefined, undefined, defaultCanRead);
116+
schema = simpleSchema._schema;
117+
118+
fragmentName = fragmentName || `${typeName}DefaultFragment`;
119+
120+
const defaultFragment = getObjectFragment({
121+
schema,
122+
fragmentName: `fragment ${fragmentName} on ${typeName}`,
123+
options,
124+
});
125+
126+
if (defaultFragment) registerFragment(defaultFragment);
127+
};

packages/vulcan-lib/lib/modules/graphql_templates/types.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ type Movie{
4646
*/
4747
export const mainTypeTemplate = ({ typeName, description, interfaces, fields }) =>
4848
`${description ? `# ${description}` : ''}
49-
type ${typeName} ${interfaces.length ? `implements ${interfaces.join(', ')} ` : ''}{
49+
type ${typeName} ${interfaces?.length ? `implements ${interfaces.join(', ')} ` : ''}{
5050
${convertToGraphQL(fields, ' ')}
5151
}
5252
`;

0 commit comments

Comments
 (0)