add possibility to set a relation "private"

Signed-off-by: Pierre Noël <pierre.noel@strapi.io>
This commit is contained in:
Pierre Noël 2020-03-12 10:56:37 +01:00 committed by Alexandre BODIN
parent 65a3e83ee3
commit 1227bfeba4
7 changed files with 286 additions and 127 deletions

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

@ -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) {
@ -250,7 +243,7 @@ module.exports = {
const inputs = `
input ${inputName} {
${Object.keys(model.attributes)
.map(attributeName => {
return `${attributeName}: ${this.convertType({
@ -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

@ -44,6 +44,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,15 +94,17 @@ describe('Test Graphql Relations API End to End', () => {
modelsUtils = createModelsUtils({ rq });
await modelsUtils.createContentTypes([documentModel, labelModel]);
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 documentsPayload = [{ name: 'document 1' }, { name: 'document 2' }];
@ -127,49 +164,46 @@ describe('Test Graphql Relations API End to End', () => {
data.labels = 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
name
}
}
}
}
`,
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({
@ -229,9 +263,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)),
}))
),
},
@ -405,5 +437,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,
},
},
});
});
});
});