diff --git a/docs/3.0.0-beta.x/installation/digitalocean-one-click.md b/docs/3.0.0-beta.x/installation/digitalocean-one-click.md index c41cf57a44..01337aeb63 100644 --- a/docs/3.0.0-beta.x/installation/digitalocean-one-click.md +++ b/docs/3.0.0-beta.x/installation/digitalocean-one-click.md @@ -27,7 +27,7 @@ To create a project head over to the Strapi [listing on the marketplace](https:/ ### Step 3: Visit your app -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). +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). 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! @@ -101,7 +101,7 @@ 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). @@ -137,8 +137,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 diff --git a/packages/strapi-connector-bookshelf/lib/mount-models.js b/packages/strapi-connector-bookshelf/lib/mount-models.js index 5246a99889..06aa5c2e51 100644 --- a/packages/strapi-connector-bookshelf/lib/mount-models.js +++ b/packages/strapi-connector-bookshelf/lib/mount-models.js @@ -661,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); } }); diff --git a/packages/strapi-plugin-content-manager/test/dynamiczones/with-media.test.e2e.js b/packages/strapi-plugin-content-manager/test/dynamiczones/with-media.test.e2e.js index 50cc30de8a..90cad39125 100644 --- a/packages/strapi-plugin-content-manager/test/dynamiczones/with-media.test.e2e.js +++ b/packages/strapi-plugin-content-manager/test/dynamiczones/with-media.test.e2e.js @@ -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', () => { diff --git a/packages/strapi-plugin-content-type-builder/admin/src/components/ListRow/Wrapper.js b/packages/strapi-plugin-content-type-builder/admin/src/components/ListRow/Wrapper.js index 5ca247418e..19736cb12c 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/components/ListRow/Wrapper.js +++ b/packages/strapi-plugin-content-type-builder/admin/src/components/ListRow/Wrapper.js @@ -43,7 +43,6 @@ const Wrapper = styled.tr` }} p { font-weight: 500; - text-transform: capitalize; } } td:last-child { diff --git a/packages/strapi-plugin-content-type-builder/controllers/validation/relations.js b/packages/strapi-plugin-content-type-builder/controllers/validation/relations.js index 6221592cdc..4ac7885a91 100644 --- a/packages/strapi-plugin-content-type-builder/controllers/validation/relations.js +++ b/packages/strapi-plugin-content-type-builder/controllers/validation/relations.js @@ -35,5 +35,6 @@ module.exports = (obj, validNatures) => { .test(isValidName) .nullable(), targetColumnName: yup.string().nullable(), + private: yup.boolean().nullable(), }; }; diff --git a/packages/strapi-plugin-content-type-builder/services/schema-builder/index.js b/packages/strapi-plugin-content-type-builder/services/schema-builder/index.js index 1b8eedb306..8dc84312fe 100644 --- a/packages/strapi-plugin-content-type-builder/services/schema-builder/index.js +++ b/packages/strapi-plugin-content-type-builder/services/schema-builder/index.js @@ -115,12 +115,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, }; diff --git a/packages/strapi-plugin-graphql/services/type-builder.js b/packages/strapi-plugin-graphql/services/type-builder.js index 647faae0c5..45943e8d69 100644 --- a/packages/strapi-plugin-graphql/services/type-builder.js +++ b/packages/strapi-plugin-graphql/services/type-builder.js @@ -243,7 +243,7 @@ module.exports = { const inputs = ` input ${inputName} { - + ${Object.keys(model.attributes) .map(attributeName => { return `${attributeName}: ${this.convertType({ @@ -271,6 +271,7 @@ module.exports = { .join('\n')} } `; + return inputs; }, diff --git a/packages/strapi-plugin-graphql/services/type-definitions.js b/packages/strapi-plugin-graphql/services/type-definitions.js index 1ef8790d8f..0d2dd8cb86 100644 --- a/packages/strapi-plugin-graphql/services/type-definitions.js +++ b/packages/strapi-plugin-graphql/services/type-definitions.js @@ -55,6 +55,7 @@ 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]; diff --git a/packages/strapi-plugin-graphql/test/graphqlRelations.test.e2e.js b/packages/strapi-plugin-graphql/test/graphqlRelations.test.e2e.js index 35c6d346ec..3e10cc29fc 100644 --- a/packages/strapi-plugin-graphql/test/graphqlRelations.test.e2e.js +++ b/packages/strapi-plugin-graphql/test/graphqlRelations.test.e2e.js @@ -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, + }, + }, + }); + }); }); }); diff --git a/packages/strapi/lib/Strapi.js b/packages/strapi/lib/Strapi.js index bb899b1c22..aeec8893db 100644 --- a/packages/strapi/lib/Strapi.js +++ b/packages/strapi/lib/Strapi.js @@ -273,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(); }