Merge branch 'front/ui-fixes' of github.com:strapi/strapi into front/ui-fixes

This commit is contained in:
Virginie Ky 2020-03-20 14:41:47 +01:00
commit df1e81d317
16 changed files with 465 additions and 279 deletions

View File

@ -162,6 +162,38 @@ Here is the list of endpoints generated for each of your **Content Types**.
:::
::: tab Contact
`Contact` **Content Type**
<div id="endpoint-table">
| Method | Path | Description |
| :----- | :--------- | :------------------------- |
| GET | `/contact` | Get the contact content |
| PUT | `/contact` | Update the contact content |
| DELETE | `/contact` | Delete the contact content |
</div>
:::
::: tab About
`About` **Content Type**
<div id="endpoint-table">
| Method | Path | Description |
| :----- | :------- | :----------------------- |
| GET | `/about` | Get the about content |
| PUT | `/about` | Update the about content |
| DELETE | `/about` | Delete the about content |
</div>
:::
::::
### Collection Types

View File

@ -15,7 +15,7 @@ It's the origin purpose of the project.
### Custom content structure
With the admin panel of Strapi, You can generate the admin panel in just a few clicks, and get your whole CMS setup in a few minutes.
You can generate the admin panel in a few clicks and get your whole CMS setup in a few minutes.
### Manage content

View File

@ -29,6 +29,10 @@ To create a project head over to the Strapi [listing on the marketplace](https:/
Please note that it may take anywhere from 30 seconds to a few minutes for the droplet to startup, when it does you should see it in your [droplets list](https://cloud.digitalocean.com/droplets).
::: warning
After the droplet has started, it will take a few more minutes to finish the Strapi installation.
:::
From here you will see the public ipv4 address that you can use to visit your Strapi application, just open that in a browser and it should ask you to create your first administrator!
You can also SSH into the virtual machine using `root` as the SSH user and your public ipv4 address, there is no password for SSH as DigitalOcean uses SSH keys by default with password authentication disabled.
@ -101,13 +105,13 @@ upstream strapi {
### Strapi
In the DigitalOcean one-click application a service user is used in which it's home directory is located at `/srv/strapi`. Likewise the actual Strapi application is located within this home directory at `/srv/strapi/strapi`.
In the DigitalOcean one-click application a service user is used in which it's home directory is located at `/srv/strapi`. Likewise the actual Strapi application is located within this home directory at `/srv/strapi/strapi-development`.
Please note that with this application it is intially created and ran in the `development` environment to allow for creating models. **You should not use this directly in production**, it is recommended that you configure a private git repository to commit changes into and create a new application directory within the service user's home (Example: `/srv/strapi/strapi-production`). To run the new `production` or `staging` environments you can refer to the [PM2 Documentation](https://pm2.keymetrics.io/docs/usage/quick-start/#managing-processes).
## Using the Service Account
By default the Strapi application will be running under a "service account", this is an account that is extremely limited into what it can do and access. The purpose of using a service account is to project your system from security threats.
By default the Strapi application will be running under a "service account", this is an account that is extremely limited into what it can do and access. The purpose of using a service account is to help protect your system from security threats.
### Accessing the service account
@ -137,8 +141,6 @@ Strapi will automatically start if the virtual machine is rebooted, you can also
## Changing the PostgreSQL Password
Because of how the virtual machine is created, your database is setup with a long and random password, however for security you should change this password before moving into a production-like setting.
Use the following steps to change the PostgreSQL password and update Strapi's config:
- Make sure you are logged into the `strapi` service user

View File

@ -46,7 +46,7 @@ By default, the [Shadow CRUD](#shadow-crud) feature is enabled and the GraphQL i
Security limits on maximum number of items in your response by default is limited to 100, however you can change this on the following config option `amountLimit`. This should only be changed after careful consideration of the drawbacks of a large query which can cause what would basically be a DDoS (Distributed Denial of Service). And may cause abnormal load on your Strapi server, as well as your database server.
You can also enable the Apollo server tracing feature, which is supported by the playground to track the response time of each part of your query. To enable this feature just change/add the `"tracing": true` option in the GraphQL settings file. You can read more about the tracing feature from Apollo [here](https://www.apollographql.com/docs/engine/features/query-tracing.html).
You can also enable the Apollo server tracing feature, which is supported by the playground to track the response time of each part of your query. To enable this feature just change/add the `"tracing": true` option in the GraphQL settings file. You can read more about the tracing feature from Apollo [here](https://www.apollographql.com/docs/apollo-server/federation/metrics/).
You can edit these configurations by creating following file.
@ -234,12 +234,7 @@ Return the second decade of users which have an email that contains `@strapi.io`
```graphql
query {
users(
limit: 10
start: 10
sort: "username:asc"
where: { email_contains: "@strapi.io" }
) {
users(limit: 10, start: 10, sort: "username:asc", where: { email_contains: "@strapi.io" }) {
username
email
}
@ -720,10 +715,7 @@ module.exports = {
Mutation: {
createRestaurant: {
description: 'Create a new restaurant',
policies: [
'plugins::users-permissions.isAuthenticated',
'global::logging',
],
policies: ['plugins::users-permissions.isAuthenticated', 'global::logging'],
},
},
},

View File

@ -88,10 +88,7 @@ module.exports = ({ models, target }, ctx) => {
idAttribute: _.get(definition, 'options.idAttribute', 'id'),
associations: [],
defaults: Object.keys(definition.attributes).reduce((acc, current) => {
if (
definition.attributes[current].type &&
definition.attributes[current].default
) {
if (definition.attributes[current].type && definition.attributes[current].default) {
acc[current] = definition.attributes[current].default;
}
@ -114,9 +111,7 @@ module.exports = ({ models, target }, ctx) => {
const pivot = this.pivot && !omitPivot && this.pivot.attributes;
// Remove pivot attributes with prefix.
_.keys(pivot).forEach(
key => delete attributes[`${PIVOT_PREFIX}${key}`]
);
_.keys(pivot).forEach(key => delete attributes[`${PIVOT_PREFIX}${key}`]);
// Add pivot attributes without prefix.
const pivotAttributes = _.mapKeys(
@ -147,16 +142,10 @@ module.exports = ({ models, target }, ctx) => {
}
const { nature, verbose } =
utilsModels.getNature(details, name, undefined, model.toLowerCase()) ||
{};
utilsModels.getNature(details, name, undefined, model.toLowerCase()) || {};
// Build associations key
utilsModels.defineAssociations(
model.toLowerCase(),
definition,
details,
name
);
utilsModels.defineAssociations(model.toLowerCase(), definition, details, name);
let globalId;
const globalName = details.model || details.collection || '';
@ -164,10 +153,7 @@ module.exports = ({ models, target }, ctx) => {
// Exclude polymorphic association.
if (globalName !== '*') {
globalId = details.plugin
? _.get(
strapi.plugins,
`${details.plugin}.models.${globalName.toLowerCase()}.globalId`
)
? _.get(strapi.plugins, `${details.plugin}.models.${globalName.toLowerCase()}.globalId`)
: _.get(strapi.models, `${globalName.toLowerCase()}.globalId`);
}
@ -225,10 +211,7 @@ module.exports = ({ models, target }, ctx) => {
}
case 'belongsTo': {
loadedModel[name] = function() {
return this.belongsTo(
GLOBALS[globalId],
_.get(details, 'columnName', name)
);
return this.belongsTo(GLOBALS[globalId], _.get(details, 'columnName', name));
};
break;
}
@ -245,13 +228,9 @@ module.exports = ({ models, target }, ctx) => {
details.isVirtual = true;
if (nature === 'manyWay') {
const joinTableName = `${definition.collectionName}__${_.snakeCase(
name
)}`;
const joinTableName = `${definition.collectionName}__${_.snakeCase(name)}`;
const foreignKey = `${singular(definition.collectionName)}_${
definition.primaryKey
}`;
const foreignKey = `${singular(definition.collectionName)}_${definition.primaryKey}`;
let otherKey = `${details.attribute}_${details.column}`;
@ -277,10 +256,7 @@ module.exports = ({ models, target }, ctx) => {
} else {
const joinTableName =
_.get(details, 'collectionName') ||
utilsModels.getCollectionName(
targetModel.attributes[details.via],
details
);
utilsModels.getCollectionName(targetModel.attributes[details.via], details);
const relationship = targetModel.attributes[details.via];
@ -333,10 +309,7 @@ module.exports = ({ models, target }, ctx) => {
details.via,
`${definition.collectionName}`
).query(qb => {
qb.where(
_.get(model, ['attributes', details.via, 'filter'], 'field'),
name
);
qb.where(_.get(model, ['attributes', details.via, 'filter'], 'field'), name);
});
};
break;
@ -354,10 +327,7 @@ module.exports = ({ models, target }, ctx) => {
details.via,
`${definition.collectionName}`
).query(qb => {
qb.where(
_.get(model, ['attributes', details.via, 'filter'], 'field'),
name
);
qb.where(_.get(model, ['attributes', details.via, 'filter'], 'field'), name);
});
};
break;
@ -369,21 +339,17 @@ module.exports = ({ models, target }, ctx) => {
);
const morphValues = association.related.map(id => {
let models = Object.values(strapi.models).filter(
model => model.globalId === id
);
let models = Object.values(strapi.models).filter(model => model.globalId === id);
if (models.length === 0) {
models = Object.values(strapi.components).filter(
model => model.globalId === id
);
models = Object.values(strapi.components).filter(model => model.globalId === id);
}
if (models.length === 0) {
models = Object.keys(strapi.plugins).reduce((acc, current) => {
const models = Object.values(
strapi.plugins[current].models
).filter(model => model.globalId === id);
const models = Object.values(strapi.plugins[current].models).filter(
model => model.globalId === id
);
if (acc.length === 0 && models.length > 0) {
acc = models;
@ -395,9 +361,7 @@ module.exports = ({ models, target }, ctx) => {
if (models.length === 0) {
strapi.log.error(`Impossible to register the '${model}' model.`);
strapi.log.error(
'The collection name cannot be found for the morphTo method.'
);
strapi.log.error('The collection name cannot be found for the morphTo method.');
strapi.stop();
}
@ -417,10 +381,7 @@ module.exports = ({ models, target }, ctx) => {
related: function() {
return this.morphTo(
name,
...association.related.map((id, index) => [
GLOBALS[id],
morphValues[index],
])
...association.related.map((id, index) => [GLOBALS[id], morphValues[index]])
);
},
};
@ -434,16 +395,10 @@ module.exports = ({ models, target }, ctx) => {
// Upload has many Upload_morph that morph to different model.
loadedModel[name] = function() {
if (verbose === 'belongsToMorph') {
return this.hasOne(
GLOBALS[options.tableName],
`${definition.collectionName}_id`
);
return this.hasOne(GLOBALS[options.tableName], `${definition.collectionName}_id`);
}
return this.hasMany(
GLOBALS[options.tableName],
`${definition.collectionName}_id`
);
return this.hasMany(GLOBALS[options.tableName], `${definition.collectionName}_id`);
};
break;
}
@ -469,9 +424,7 @@ module.exports = ({ models, target }, ctx) => {
return _.mapKeys(params, (value, key) => {
const attr = definition.attributes[key] || {};
return _.isPlainObject(attr) && _.isString(attr['columnName'])
? attr['columnName']
: key;
return _.isPlainObject(attr) && _.isString(attr['columnName']) ? attr['columnName'] : key;
});
};
@ -504,20 +457,16 @@ module.exports = ({ models, target }, ctx) => {
case 'component': {
const { repeatable } = attr;
const components = relations[key]
.toJSON()
.map(el => el.component);
const components = relations[key].toJSON().map(el => el.component);
attrs[key] =
repeatable === true ? components : _.first(components) || null;
attrs[key] = repeatable === true ? components : _.first(components) || null;
break;
}
case 'dynamiczone': {
attrs[key] = relations[key].toJSON().map(el => {
const componentKey = Object.keys(strapi.components).find(
key =>
strapi.components[key].collectionName === el.component_type
key => strapi.components[key].collectionName === el.component_type
);
return {
@ -540,9 +489,7 @@ module.exports = ({ models, target }, ctx) => {
if (relation) {
// Extract raw JSON data.
attrs[association.alias] = relation.toJSON
? relation.toJSON(options)
: relation;
attrs[association.alias] = relation.toJSON ? relation.toJSON(options) : relation;
// Retrieve opposite model.
const model = strapi.getModel(
@ -553,8 +500,7 @@ module.exports = ({ models, target }, ctx) => {
// Reformat data by bypassing the many-to-many relationship.
switch (association.nature) {
case 'oneToManyMorph':
attrs[association.alias] =
attrs[association.alias][model.collectionName] || null;
attrs[association.alias] = attrs[association.alias][model.collectionName] || null;
break;
case 'manyToManyMorph':
attrs[association.alias] = attrs[association.alias].map(
@ -602,9 +548,7 @@ module.exports = ({ models, target }, ctx) => {
if (relation) {
// Extract raw JSON data.
attrs[association.alias] = relation.toJSON
? relation.toJSON(options)
: relation;
attrs[association.alias] = relation.toJSON ? relation.toJSON(options) : relation;
}
});
@ -717,9 +661,10 @@ module.exports = ({ models, target }, ctx) => {
await createComponentJoinTables({ definition, ORM });
} catch (err) {
strapi.log.error(`Impossible to register the '${model}' model.`);
strapi.log.error(err);
strapi.stop();
if (err instanceof TypeError || err instanceof ReferenceError) {
strapi.stopWithError(err, `Impossible to register the '${model}' model.`);
}
strapi.stopWithError(err);
}
});

View File

@ -20,9 +20,8 @@ const populateFetch = (definition, options) => {
} else if (_.isEmpty(options.withRelated)) {
options.withRelated = populateComponents(definition);
} else {
options.withRelated = formatPopulateOptions(
definition,
options.withRelated
options.withRelated = formatPopulateOptions(definition, options.withRelated).concat(
populateComponents(definition)
);
}
};
@ -173,9 +172,7 @@ const formatPopulateOptions = (definition, withRelated) => {
continue;
}
const assoc = tmpModel.associations.find(
association => association.alias === part
);
const assoc = tmpModel.associations.find(association => association.alias === part);
if (!assoc) return acc;

View File

@ -18,9 +18,9 @@ const uploadImg = () => {
describe.each([
[
'CONTENT MANAGER',
'/content-manager/explorer/application::withdynamiczone.withdynamiczone',
'/content-manager/explorer/application::withdynamiczonemedia.withdynamiczonemedia',
],
['GENERATED API', '/withdynamiczones'],
['GENERATED API', '/withdynamiczonemedias'],
])('[%s] => Not required dynamiczone', (_, path) => {
beforeAll(async () => {
const token = await registerAndLogin();
@ -61,17 +61,9 @@ describe.each([
},
});
await modelsUtils.createContentTypeWithType(
'withdynamiczone',
'dynamiczone',
{
components: [
'default.single-media',
'default.multiple-media',
'default.with-nested',
],
}
);
await modelsUtils.createContentTypeWithType('withdynamiczonemedia', 'dynamiczone', {
components: ['default.single-media', 'default.multiple-media', 'default.with-nested'],
});
rq = authRq.defaults({
baseUrl: `http://localhost:1337${path}`,
@ -82,7 +74,7 @@ describe.each([
await modelsUtils.deleteComponent('default.with-nested');
await modelsUtils.deleteComponent('default.single-media');
await modelsUtils.deleteComponent('default.multiple-media');
await modelsUtils.deleteContentType('withdynamiczone');
await modelsUtils.deleteContentType('withdynamiczonemedia');
}, 60000);
describe('Contains components with medias', () => {

View File

@ -43,7 +43,6 @@ const Wrapper = styled.tr`
}}
p {
font-weight: 500;
text-transform: capitalize;
}
}
td:last-child {
@ -53,11 +52,7 @@ const Wrapper = styled.tr`
}
}
&.relation-row {
background: linear-gradient(
135deg,
rgba(28, 93, 231, 0.05),
rgba(239, 243, 253, 0)
);
background: linear-gradient(135deg, rgba(28, 93, 231, 0.05), rgba(239, 243, 253, 0));
}
&.clickable {
&:hover {

View File

@ -18,26 +18,20 @@ module.exports = {
return ctx.send({ error }, 400);
}
const contentTypeService =
strapi.plugins['content-type-builder'].services.contenttypes;
const contentTypeService = strapi.plugins['content-type-builder'].services.contenttypes;
const contentTypes = Object.keys(strapi.contentTypes)
.filter(uid => {
if (uid.startsWith('strapi::')) return false;
if (uid === 'plugins::upload.file') return false; // TODO: add a flag in the content type instead
if (
kind &&
_.get(strapi.contentTypes[uid], 'kind', 'collectionType') !== kind
) {
if (kind && _.get(strapi.contentTypes[uid], 'kind', 'collectionType') !== kind) {
return false;
}
return true;
})
.map(uid =>
contentTypeService.formatContentType(strapi.contentTypes[uid])
);
.map(uid => contentTypeService.formatContentType(strapi.contentTypes[uid]));
ctx.send({
data: contentTypes,
@ -53,8 +47,7 @@ module.exports = {
return ctx.send({ error: 'contentType.notFound' }, 404);
}
const contentTypeService =
strapi.plugins['content-type-builder'].services.contenttypes;
const contentTypeService = strapi.plugins['content-type-builder'].services.contenttypes;
ctx.send({ data: contentTypeService.formatContentType(contentType) });
},
@ -71,8 +64,7 @@ module.exports = {
try {
strapi.reload.isWatching = false;
const contentTypeService =
strapi.plugins['content-type-builder'].services.contenttypes;
const contentTypeService = strapi.plugins['content-type-builder'].services.contenttypes;
const component = await contentTypeService.createContentType({
contentType: body.contentType,
@ -112,8 +104,7 @@ module.exports = {
try {
strapi.reload.isWatching = false;
const contentTypeService =
strapi.plugins['content-type-builder'].services.contenttypes;
const contentTypeService = strapi.plugins['content-type-builder'].services.contenttypes;
const component = await contentTypeService.editContentType(uid, {
contentType: body.contentType,
@ -139,8 +130,7 @@ module.exports = {
try {
strapi.reload.isWatching = false;
const contentTypeService =
strapi.plugins['content-type-builder'].services.contenttypes;
const contentTypeService = strapi.plugins['content-type-builder'].services.contenttypes;
const component = await contentTypeService.deleteContentType(uid);

View File

@ -35,5 +35,6 @@ module.exports = (obj, validNatures) => {
.test(isValidName)
.nullable(),
targetColumnName: yup.string().nullable(),
private: yup.boolean().nullable(),
};
};

View File

@ -60,18 +60,12 @@ function createSchemaBuilder({ components, contentTypes }) {
// init temporary ContentTypes
Object.keys(contentTypes).forEach(key => {
tmpContentTypes.set(
contentTypes[key].uid,
createSchemaHandler(contentTypes[key])
);
tmpContentTypes.set(contentTypes[key].uid, createSchemaHandler(contentTypes[key]));
});
// init temporary components
Object.keys(components).forEach(key => {
tmpComponents.set(
components[key].uid,
createSchemaHandler(components[key])
);
tmpComponents.set(components[key].uid, createSchemaHandler(components[key]));
});
return {
@ -120,12 +114,14 @@ function createSchemaBuilder({ components, contentTypes }) {
columnName,
dominant,
autoPopulate,
private: isPrivate,
} = attribute;
const attr = {
unique: unique === true ? true : undefined,
columnName: columnName || undefined,
configurable: configurable === false ? false : undefined,
private: isPrivate === true ? true : undefined,
autoPopulate,
};

View File

@ -16,8 +16,7 @@ const GraphQLLong = require('graphql-type-long');
const Time = require('../types/time');
const { toSingular, toInputName } = require('./naming');
const isScalarAttribute = ({ type }) =>
type && !['component', 'dynamiczone'].includes(type);
const isScalarAttribute = ({ type }) => type && !['component', 'dynamiczone'].includes(type);
module.exports = {
/**
@ -90,9 +89,7 @@ module.exports = {
typeName =
action === 'update'
? `edit${_.upperFirst(toSingular(globalId))}Input`
: `${_.upperFirst(toSingular(globalId))}Input${
required ? '!' : ''
}`;
: `${_.upperFirst(toSingular(globalId))}Input${required ? '!' : ''}`;
}
if (repeatable === true) {
@ -104,9 +101,7 @@ module.exports = {
if (attribute.type === 'dynamiczone') {
const { required } = attribute;
const unionName = `${modelName}${_.upperFirst(
_.camelCase(attributeName)
)}DynamicZone`;
const unionName = `${modelName}${_.upperFirst(_.camelCase(attributeName))}DynamicZone`;
let typeName = unionName;
@ -202,9 +197,7 @@ module.exports = {
addPolymorphicUnionType(definition) {
const types = graphql
.parse(definition)
.definitions.filter(
def => def.kind === 'ObjectTypeDefinition' && def.name.value !== 'Query'
)
.definitions.filter(def => def.kind === 'ObjectTypeDefinition' && def.name.value !== 'Query')
.map(def => def.name.value);
if (types.length > 0) {
@ -278,6 +271,7 @@ module.exports = {
.join('\n')}
}
`;
return inputs;
},

View File

@ -12,12 +12,7 @@ const DynamicZoneScalar = require('../types/dynamiczoneScalar');
const { formatModelConnectionsGQL } = require('./build-aggregation');
const types = require('./type-builder');
const {
mergeSchemas,
convertToParams,
convertToQuery,
amountLimiting,
} = require('./utils');
const { mergeSchemas, convertToParams, convertToQuery, amountLimiting } = require('./utils');
const { toSDL, getTypeDescription } = require('./schema-definitions');
const { toSingular, toPlural } = require('./naming');
const { buildQuery, buildMutation } = require('./resolvers-builder');
@ -60,10 +55,10 @@ const buildTypeDefObj = model => {
// Change field definition for collection relations
associations
.filter(association => association.type === 'collection')
.filter(association => attributes[association.alias].private !== true)
.forEach(association => {
typeDef[
`${association.alias}(sort: String, limit: Int, start: Int, where: JSON)`
] = typeDef[association.alias];
typeDef[`${association.alias}(sort: String, limit: Int, start: Int, where: JSON)`] =
typeDef[association.alias];
delete typeDef[association.alias];
});
@ -90,9 +85,7 @@ const generateDynamicZoneDefinitions = (attributes, globalId, schema) => {
.forEach(attribute => {
const { components } = attributes[attribute];
const typeName = `${globalId}${_.upperFirst(
_.camelCase(attribute)
)}DynamicZone`;
const typeName = `${globalId}${_.upperFirst(_.camelCase(attribute))}DynamicZone`;
if (components.length === 0) {
// Create dummy type because graphql doesn't support empty ones
@ -111,9 +104,7 @@ const generateDynamicZoneDefinitions = (attributes, globalId, schema) => {
return compo.globalId;
});
const unionType = `union ${typeName} = ${componentsTypeNames.join(
' | '
)}`;
const unionType = `union ${typeName} = ${componentsTypeNames.join(' | ')}`;
schema.definition += `\n${unionType}\n`;
}
@ -137,8 +128,7 @@ const generateDynamicZoneDefinitions = (attributes, globalId, schema) => {
};
const buildAssocResolvers = model => {
const contentManager =
strapi.plugins['content-manager'].services['contentmanager'];
const contentManager = strapi.plugins['content-manager'].services['contentmanager'];
const { primaryKey, associations = [] } = model;
@ -194,8 +184,7 @@ const buildAssocResolvers = model => {
};
if (
((association.nature === 'manyToMany' &&
association.dominant) ||
((association.nature === 'manyToMany' && association.dominant) ||
association.nature === 'manyWay') &&
_.has(obj, association.alias) // if populated
) {
@ -203,31 +192,21 @@ const buildAssocResolvers = model => {
queryOpts,
['query', targetModel.primaryKey],
obj[association.alias]
? obj[association.alias]
.map(val => val[targetModel.primaryKey] || val)
.sort()
? obj[association.alias].map(val => val[targetModel.primaryKey] || val).sort()
: []
);
} else {
_.set(
queryOpts,
['query', association.via],
obj[targetModel.primaryKey]
);
_.set(queryOpts, ['query', association.via], obj[targetModel.primaryKey]);
}
}
return association.model
? strapi.plugins.graphql.services['data-loaders'].loaders[
targetModel.uid
].load({
? strapi.plugins.graphql.services['data-loaders'].loaders[targetModel.uid].load({
params,
options: queryOpts,
single: true,
})
: strapi.plugins.graphql.services['data-loaders'].loaders[
targetModel.uid
].load({
: strapi.plugins.graphql.services['data-loaders'].loaders[targetModel.uid].load({
options: queryOpts,
association,
});
@ -308,9 +287,7 @@ const buildSingleType = model => {
const singularName = toSingular(modelName);
const _schema = _.cloneDeep(
_.get(strapi.plugins, 'graphql.config._schema.graphql', {})
);
const _schema = _.cloneDeep(_.get(strapi.plugins, 'graphql.config._schema.graphql', {}));
const globalType = _.get(_schema, ['type', model.globalId], {});
@ -357,9 +334,7 @@ const buildCollectionType = model => {
const singularName = toSingular(modelName);
const pluralName = toPlural(modelName);
const _schema = _.cloneDeep(
_.get(strapi.plugins, 'graphql.config._schema.graphql', {})
);
const _schema = _.cloneDeep(_.get(strapi.plugins, 'graphql.config._schema.graphql', {}));
const globalType = _.get(_schema, ['type', model.globalId], {});

View File

@ -33,7 +33,11 @@ const diffResolvers = (object, base) => {
Object.keys(object).forEach(type => {
Object.keys(object[type]).forEach(resolver => {
if (!_.has(base, [type, resolver])) {
if(type === 'Query' || type === 'Mutation') {
if (!_.has(base, [type, resolver])) {
_.set(newObj, [type, resolver], _.get(object, [type, resolver]));
}
} else {
_.set(newObj, [type, resolver], _.get(object, [type, resolver]));
}
});

View File

@ -9,7 +9,25 @@ let graphqlQuery;
let modelsUtils;
// utils
const selectFields = doc => _.pick(doc, ['id', 'name']);
const selectFields = doc => _.pick(doc, ['id', 'name', 'color']);
const rgbColorComponent = {
attributes: {
name: {
type: 'text',
},
red: {
type: 'integer',
},
green: {
type: 'integer',
},
blue: {
type: 'integer',
},
},
name: 'rgbColor',
};
const documentModel = {
attributes: {
@ -37,6 +55,11 @@ const labelModel = {
target: 'application::document.document',
targetAttribute: 'labels',
},
color: {
type: 'component',
component: 'default.rgb-color',
repeatable: false,
},
},
connection: 'default',
name: 'label',
@ -44,6 +67,41 @@ const labelModel = {
collectionName: '',
};
const carModel = {
attributes: {
name: {
type: 'text',
},
},
connection: 'default',
name: 'car',
description: '',
collectionName: '',
};
const personModel = {
attributes: {
name: {
type: 'text',
},
privateName: {
type: 'text',
private: true,
},
privateCars: {
nature: 'oneToMany',
target: 'application::car.car',
dominant: false,
targetAttribute: 'person',
private: true,
},
},
connection: 'default',
name: 'person',
description: '',
collectionName: '',
};
describe('Test Graphql Relations API End to End', () => {
beforeAll(async () => {
const token = await registerAndLogin();
@ -59,17 +117,24 @@ describe('Test Graphql Relations API End to End', () => {
modelsUtils = createModelsUtils({ rq });
await modelsUtils.createContentTypes([documentModel, labelModel]);
await modelsUtils.createComponent(rgbColorComponent);
await modelsUtils.createContentTypes([documentModel, labelModel, carModel, personModel]);
}, 60000);
afterAll(() => modelsUtils.deleteContentTypes(['document', 'label']), 60000);
afterAll(() => modelsUtils.deleteContentTypes(['document', 'label', 'car', 'person']), 60000);
describe('Test relations features', () => {
let data = {
labels: [],
documents: [],
people: [],
cars: [],
};
const labelsPayload = [{ name: 'label 1' }, { name: 'label 2' }];
const labelsPayload = [
{ name: 'label 1', color: null },
{ name: 'label 2', color: null },
{ name: 'labelWithColor', color: { name: 'tomato', red: 255, green: 99, blue: 71 } },
];
const documentsPayload = [{ name: 'document 1' }, { name: 'document 2' }];
test.each(labelsPayload)('Create label %o', async label => {
@ -79,6 +144,12 @@ describe('Test Graphql Relations API End to End', () => {
createLabel(input: $input) {
label {
name
color {
name
red
green
blue
}
}
}
}
@ -90,10 +161,8 @@ describe('Test Graphql Relations API End to End', () => {
},
});
const { body } = res;
expect(res.statusCode).toBe(200);
expect(body).toEqual({
expect(res.body).toEqual({
data: {
createLabel: {
label,
@ -109,6 +178,12 @@ describe('Test Graphql Relations API End to End', () => {
labels {
id
name
color {
name
red
green
blue
}
}
}
`,
@ -124,52 +199,55 @@ describe('Test Graphql Relations API End to End', () => {
});
// assign for later use
data.labels = res.body.data.labels;
data.labels = data.labels.concat(res.body.data.labels);
});
test.each(documentsPayload)(
'Create document linked to every labels %o',
async document => {
const res = await graphqlQuery({
query: /* GraphQL */ `
mutation createDocument($input: createDocumentInput) {
createDocument(input: $input) {
document {
test.each(documentsPayload)('Create document linked to every labels %o', async document => {
const res = await graphqlQuery({
query: /* GraphQL */ `
mutation createDocument($input: createDocumentInput) {
createDocument(input: $input) {
document {
name
labels {
id
name
labels {
id
color {
name
red
green
blue
}
}
}
}
`,
variables: {
input: {
data: {
...document,
labels: data.labels.map(t => t.id),
},
}
`,
variables: {
input: {
data: {
...document,
labels: data.labels.map(t => t.id),
},
},
});
},
});
const { body } = res;
const { body } = res;
expect(res.statusCode).toBe(200);
expect(res.statusCode).toBe(200);
expect(body).toMatchObject({
data: {
createDocument: {
document: {
...selectFields(document),
labels: expect.arrayContaining(data.labels.map(selectFields)),
},
expect(body).toMatchObject({
data: {
createDocument: {
document: {
...selectFields(document),
labels: expect.arrayContaining(data.labels.map(selectFields)),
},
},
});
}
);
},
});
});
test('List documents with labels', async () => {
const res = await graphqlQuery({
@ -181,6 +259,12 @@ describe('Test Graphql Relations API End to End', () => {
labels {
id
name
color {
name
red
green
blue
}
}
}
}
@ -212,6 +296,12 @@ describe('Test Graphql Relations API End to End', () => {
labels {
id
name
color {
name
red
green
blue
}
documents {
id
name
@ -229,9 +319,7 @@ describe('Test Graphql Relations API End to End', () => {
labels: expect.arrayContaining(
data.labels.map(label => ({
...selectFields(label),
documents: expect.arrayContaining(
data.documents.map(selectFields)
),
documents: expect.arrayContaining(data.documents.map(selectFields)),
}))
),
},
@ -251,6 +339,12 @@ describe('Test Graphql Relations API End to End', () => {
labels {
id
name
color {
name
red
green
blue
}
}
}
}
@ -277,6 +371,12 @@ describe('Test Graphql Relations API End to End', () => {
labels {
id
name
color {
name
red
green
blue
}
}
}
}
@ -316,6 +416,12 @@ describe('Test Graphql Relations API End to End', () => {
label {
id
name
color {
name
red
green
blue
}
}
}
}
@ -350,6 +456,12 @@ describe('Test Graphql Relations API End to End', () => {
labels {
id
name
color {
name
red
green
blue
}
}
}
}
@ -405,5 +517,184 @@ describe('Test Graphql Relations API End to End', () => {
});
}
});
test('Create person', async () => {
const person = {
name: 'Chuck Norris',
privateName: 'Jean-Eude',
};
const res = await graphqlQuery({
query: /* GraphQL */ `
mutation createPerson($input: createPersonInput) {
createPerson(input: $input) {
person {
id
name
}
}
}
`,
variables: {
input: {
data: person,
},
},
});
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({
data: {
createPerson: {
person: {
id: expect.anything(),
name: person.name,
},
},
},
});
data.people.push(res.body.data.createPerson.person);
});
test("Can't list a private field", async () => {
const res = await graphqlQuery({
query: /* GraphQL */ `
{
people {
name
privateName
}
}
`,
});
expect(res.statusCode).toBe(400);
expect(res.body).toMatchObject({
errors: [
{
message: 'Cannot query field "privateName" on type "Person".',
},
],
});
});
test('Create a car linked to a person (oneToMany)', async () => {
const car = {
name: 'Peugeot 508',
person: data.people[0].id,
};
const res = await graphqlQuery({
query: /* GraphQL */ `
mutation createCar($input: createCarInput) {
createCar(input: $input) {
car {
id
name
person {
id
name
}
}
}
}
`,
variables: {
input: {
data: {
...car,
},
},
},
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
data: {
createCar: {
car: {
id: expect.anything(),
name: car.name,
person: data.people[0],
},
},
},
});
data.cars.push({ id: res.body.data.createCar.car.id });
});
test("Can't list a private oneToMany relation", async () => {
const res = await graphqlQuery({
query: /* GraphQL */ `
{
people {
name
privateCars
}
}
`,
});
expect(res.statusCode).toBe(400);
expect(res.body).toMatchObject({
errors: [
{
message: 'Cannot query field "privateCars" on type "Person".',
},
],
});
});
test('Edit person/cars relations removes correctly a car', async () => {
const newPerson = {
name: 'Check Norris Junior',
privateCars: [],
};
const mutationRes = await graphqlQuery({
query: /* GraphQL */ `
mutation updatePerson($input: updatePersonInput) {
updatePerson(input: $input) {
person {
id
}
}
}
`,
variables: {
input: {
where: {
id: data.people[0].id,
},
data: {
...newPerson,
},
},
},
});
expect(mutationRes.statusCode).toBe(200);
const queryRes = await graphqlQuery({
query: /* GraphQL */ `
query($id: ID!) {
car(id: $id) {
person {
id
}
}
}
`,
variables: {
id: data.cars[0].id,
},
});
expect(queryRes.statusCode).toBe(200);
expect(queryRes.body).toEqual({
data: {
car: {
person: null,
},
},
});
});
});
});

View File

@ -22,10 +22,7 @@ const getPrefixedDeps = require('./utils/get-prefixed-dependencies');
const createEventHub = require('./services/event-hub');
const createWebhookRunner = require('./services/webhook-runner');
const {
webhookModel,
createWebhookStore,
} = require('./services/webhook-store');
const { webhookModel, createWebhookStore } = require('./services/webhook-store');
const { createCoreStore, coreStoreModel } = require('./services/core-store');
const createEntityService = require('./services/entity-service');
const createEntityValidator = require('./services/entity-validator');
@ -133,10 +130,7 @@ class Strapi extends EventEmitter {
}
requireProjectBootstrap() {
const bootstrapPath = path.resolve(
this.dir,
'config/functions/bootstrap.js'
);
const bootstrapPath = path.resolve(this.dir, 'config/functions/bootstrap.js');
if (fse.existsSync(bootstrapPath)) {
require(bootstrapPath);
@ -159,10 +153,7 @@ class Strapi extends EventEmitter {
[chalk.blue('Launched in'), Date.now() - this.config.launchedAt + ' ms'],
[chalk.blue('Environment'), this.config.environment],
[chalk.blue('Process PID'), process.pid],
[
chalk.blue('Version'),
`${this.config.info.strapi} (node v${this.config.info.node})`,
]
[chalk.blue('Version'), `${this.config.info.strapi} (node v${this.config.info.node})`]
);
console.log(infoTable.toString());
@ -176,9 +167,7 @@ class Strapi extends EventEmitter {
console.log(chalk.bold('One more thing...'));
console.log(
chalk.grey(
'Create your first administrator 💻 by going to the administration panel at:'
)
chalk.grey('Create your first administrator 💻 by going to the administration panel at:')
);
console.log();
@ -194,11 +183,7 @@ class Strapi extends EventEmitter {
console.log(chalk.bold('Welcome back!'));
if (this.config.serveAdminPanel === true) {
console.log(
chalk.grey(
'To manage your project 🚀, go to the administration panel at:'
)
);
console.log(chalk.grey('To manage your project 🚀, go to the administration panel at:'));
console.log(chalk.bold(this.config.admin.url));
console.log();
}
@ -243,11 +228,7 @@ class Strapi extends EventEmitter {
if (
(this.config.environment === 'development' &&
_.get(
this.config.currentEnvironment,
'server.admin.autoOpen',
true
) !== false) ||
_.get(this.config.currentEnvironment, 'server.admin.autoOpen', true) !== false) ||
!isInitialised
) {
await utils.openBrowser.call(this);
@ -265,9 +246,7 @@ class Strapi extends EventEmitter {
// handle port in use cleanly
this.server.on('error', err => {
if (err.code === 'EADDRINUSE') {
return this.stopWithError(
`The port ${err.port} is already used by another application.`
);
return this.stopWithError(`The port ${err.port} is already used by another application.`);
}
this.log.error(err);
@ -294,8 +273,11 @@ class Strapi extends EventEmitter {
};
}
stopWithError(err) {
stopWithError(err, customMessage) {
this.log.debug(`⛔️ Server wasn't able to start properly.`);
if (customMessage) {
this.log.error(customMessage);
}
this.log.error(err);
return this.stop();
}
@ -445,9 +427,7 @@ class Strapi extends EventEmitter {
}
const pluginBoostraps = Object.keys(this.plugins).map(plugin => {
return execBootstrap(
_.get(this.plugins[plugin], 'config.functions.bootstrap')
).catch(err => {
return execBootstrap(_.get(this.plugins[plugin], 'config.functions.bootstrap')).catch(err => {
strapi.log.error(`Bootstrap function in plugin "${plugin}" failed`);
strapi.log.error(err);
strapi.stop();