diff --git a/models/content.js b/models/content.js index 72904060a..c166ff939 100644 --- a/models/content.js +++ b/models/content.js @@ -54,6 +54,8 @@ async function findAll(values = {}, options = {}) { if (values.where) { Object.keys(values.where).forEach((key) => { + if (key === '$not_null') return; + if (key === '$or') { values.where[key].forEach(($orObject) => { query.values.push(Object.values($orObject)[0]); @@ -83,6 +85,7 @@ async function findAll(values = {}, options = {}) { where: 'optional', count: 'optional', $or: 'optional', + $not_null: 'optional', limit: 'optional', attributes: 'optional', }); @@ -167,6 +170,16 @@ async function findAll(values = {}, options = {}) { return `(${$orQuery})`; } + if (columnName === '$not_null') { + const $notNullQuery = columnValue + .map((notColumnName) => { + return `contents.${notColumnName} IS NOT NULL`; + }) + .join(' AND '); + + return `(${$notNullQuery})`; + } + globalIndex += 1; return `contents.${columnName} = $${globalIndex}`; } diff --git a/models/validator.js b/models/validator.js index edb5792ee..ca78fd098 100644 --- a/models/validator.js +++ b/models/validator.js @@ -521,6 +521,7 @@ const schemas = { 'username', 'owner_username', '$or', + '$not_null', 'attributes', ]) { const keyValidationFunction = schemas[key]; @@ -566,6 +567,15 @@ const schemas = { }); }, + $not_null: function () { + return Joi.object({ + $not_null: Joi.array().optional().items(Joi.string().valid('parent_id')).messages({ + 'array.base': `"#not_null" deve ser do tipo Array.`, + 'any.only': `"#not_null" deve conter um dos seguintes valores: "parent_id".`, + }), + }); + }, + attributes: function () { return Joi.object({ attributes: Joi.object({ @@ -635,6 +645,32 @@ const schemas = { return contentSchema; }, + with_children: function () { + return Joi.object({ + with_children: Joi.boolean() + .allow(false) + .when('$required.with_children', { is: 'required', then: Joi.required(), otherwise: Joi.optional() }) + .messages({ + 'any.required': `"with_children" é um campo obrigatório.`, + 'string.empty': `"with_children" não pode estar em branco.`, + 'boolean.base': `"with_children" deve ser do tipo Boolean.`, + }), + }); + }, + + with_root: function () { + return Joi.object({ + with_root: Joi.boolean() + .allow(false) + .when('$required.with_root', { is: 'required', then: Joi.required(), otherwise: Joi.optional() }) + .messages({ + 'any.required': `"with_root" é um campo obrigatório.`, + 'string.empty': `"with_root" não pode estar em branco.`, + 'boolean.base': `"with_root" deve ser do tipo Boolean.`, + }), + }); + }, + event: function () { return Joi.object({ type: Joi.string() diff --git a/pages/api/v1/contents/[username]/index.public.js b/pages/api/v1/contents/[username]/index.public.js index 01ab5d4eb..f7d097653 100644 --- a/pages/api/v1/contents/[username]/index.public.js +++ b/pages/api/v1/contents/[username]/index.public.js @@ -24,6 +24,8 @@ function getValidationHandler(request, response, next) { page: 'optional', per_page: 'optional', strategy: 'optional', + with_root: 'optional', + with_children: 'optional', }); request.query = cleanValues; @@ -37,8 +39,10 @@ async function getHandler(request, response) { const results = await content.findWithStrategy({ strategy: request.query.strategy, where: { + parent_id: request.query.with_children === false ? null : undefined, owner_username: request.query.username, status: 'published', + $not_null: request.query.with_root === false ? ['parent_id'] : undefined, }, page: request.query.page, per_page: request.query.per_page, diff --git a/tests/integration/api/v1/contents/[username]/get.test.js b/tests/integration/api/v1/contents/[username]/get.test.js index b14f8ac46..4b8b773b0 100644 --- a/tests/integration/api/v1/contents/[username]/get.test.js +++ b/tests/integration/api/v1/contents/[username]/get.test.js @@ -677,5 +677,196 @@ describe('GET /api/v1/contents/[username]', () => { expect(page2ResponseBody[28].title).toEqual('Conteúdo #2'); expect(page2ResponseBody[29].title).toEqual('Conteúdo #1'); }); + + test('"username" existent with 4 contents, but only 2 "root" "published" and with_children "false"', async () => { + const firstUser = await orchestrator.createUser(); + const secondUser = await orchestrator.createUser(); + + const firstRootContent = await orchestrator.createContent({ + owner_id: firstUser.id, + title: 'Primeiro conteúdo criado', + status: 'published', + }); + + const secondRootContent = await orchestrator.createContent({ + owner_id: firstUser.id, + title: 'Segundo conteúdo criado', + status: 'published', + }); + + await orchestrator.createContent({ + owner_id: firstUser.id, + title: 'Terceiro conteúdo criado', + body: `Este conteúdo não deverá aparecer na lista retornada pelo /contents/[username], + porque quando um conteúdo possui o "status" como "draft", ele não + esta pronto para ser listado publicamente.`, + status: 'draft', + }); + + await orchestrator.createContent({ + owner_id: firstUser.id, + parent_id: firstRootContent.id, + title: 'Quarto conteúdo criado', + body: `Este conteúdo não deverá aparecer na lista retornada pelo /contents/[username], + porque o parâmetro "with_children" foi passado como "false".`, + status: 'published', + }); + + await orchestrator.createContent({ + owner_id: secondUser.id, + title: 'Conteúdo de outro usuário', + status: 'published', + }); + + const response = await fetch( + `${orchestrator.webserverUrl}/api/v1/contents/${firstUser.username}?strategy=new&with_children=false` + ); + const responseBody = await response.json(); + + expect(response.status).toEqual(200); + + expect(responseBody).toStrictEqual([ + { + id: secondRootContent.id, + owner_id: firstUser.id, + parent_id: null, + slug: 'segundo-conteudo-criado', + title: 'Segundo conteúdo criado', + status: 'published', + source_url: null, + created_at: secondRootContent.created_at.toISOString(), + updated_at: secondRootContent.updated_at.toISOString(), + published_at: secondRootContent.published_at.toISOString(), + deleted_at: null, + tabcoins: 1, + owner_username: firstUser.username, + children_deep_count: 0, + }, + { + id: firstRootContent.id, + owner_id: firstUser.id, + parent_id: null, + slug: 'primeiro-conteudo-criado', + title: 'Primeiro conteúdo criado', + status: 'published', + source_url: null, + created_at: firstRootContent.created_at.toISOString(), + updated_at: firstRootContent.updated_at.toISOString(), + published_at: firstRootContent.published_at.toISOString(), + deleted_at: null, + tabcoins: 1, + owner_username: firstUser.username, + children_deep_count: 1, + }, + ]); + + expect(uuidVersion(responseBody[0].id)).toEqual(4); + expect(uuidVersion(responseBody[1].id)).toEqual(4); + expect(uuidVersion(responseBody[0].owner_id)).toEqual(4); + expect(uuidVersion(responseBody[1].owner_id)).toEqual(4); + expect(responseBody[0].published_at > responseBody[1].published_at).toEqual(true); + }); + + test('"username" existent with 5 contents, but only 2 "root" "published", and with_root "false"', async () => { + const firstUser = await orchestrator.createUser(); + const secondUser = await orchestrator.createUser(); + + const firstRootContent = await orchestrator.createContent({ + owner_id: firstUser.id, + title: 'Primeiro conteúdo criado', + body: `Este conteúdo não deverá aparecer na lista retornada pelo /contents/[username], + porque o parâmetro "with_root" foi passado como "false".`, + status: 'published', + }); + + await orchestrator.createContent({ + owner_id: firstUser.id, + title: 'Segundo conteúdo criado', + body: `Este conteúdo não deverá aparecer na lista retornada pelo /contents/[username], + porque o parâmetro "with_root" foi passado como "false".`, + status: 'published', + }); + + await orchestrator.createContent({ + owner_id: firstUser.id, + title: 'Terceiro conteúdo criado', + body: `Este conteúdo não deverá aparecer na lista retornada pelo /contents/[username], + porque quando um conteúdo possui o "status" como "draft", ele não + esta pronto para ser listado publicamente.`, + status: 'draft', + }); + + const firstChildContent = await orchestrator.createContent({ + owner_id: firstUser.id, + parent_id: firstRootContent.id, + title: 'Quarto conteúdo criado', + body: `Este conteúdo deverá aparecer na lista retornada pelo /contents/[username]`, + status: 'published', + }); + + const secondChildContent = await orchestrator.createContent({ + owner_id: firstUser.id, + parent_id: firstRootContent.id, + title: 'Quinto conteúdo criado', + body: `Este conteúdo deverá aparecer na lista retornada pelo /contents/[username]`, + status: 'published', + }); + + await orchestrator.createContent({ + owner_id: secondUser.id, + title: 'Conteúdo de outro usuário', + status: 'published', + }); + + const response = await fetch( + `${orchestrator.webserverUrl}/api/v1/contents/${firstUser.username}?strategy=new&with_root=false` + ); + const responseBody = await response.json(); + + expect(response.status).toEqual(200); + + expect(responseBody).toStrictEqual([ + { + id: secondChildContent.id, + owner_id: firstUser.id, + parent_id: firstRootContent.id, + slug: 'quinto-conteudo-criado', + title: 'Quinto conteúdo criado', + body: 'Este conteúdo deverá aparecer na lista retornada pelo /contents/[username]', + status: 'published', + source_url: null, + created_at: secondChildContent.created_at.toISOString(), + updated_at: secondChildContent.updated_at.toISOString(), + published_at: secondChildContent.published_at.toISOString(), + deleted_at: null, + owner_username: firstUser.username, + tabcoins: 0, + children_deep_count: 0, + }, + { + id: firstChildContent.id, + owner_id: firstUser.id, + parent_id: firstRootContent.id, + slug: 'quarto-conteudo-criado', + title: 'Quarto conteúdo criado', + body: 'Este conteúdo deverá aparecer na lista retornada pelo /contents/[username]', + status: 'published', + source_url: null, + created_at: firstChildContent.created_at.toISOString(), + updated_at: firstChildContent.updated_at.toISOString(), + published_at: firstChildContent.published_at.toISOString(), + deleted_at: null, + owner_username: firstUser.username, + tabcoins: 0, + children_deep_count: 0, + }, + ]); + + expect(uuidVersion(responseBody[0].id)).toEqual(4); + expect(uuidVersion(responseBody[1].id)).toEqual(4); + expect(uuidVersion(responseBody[0].owner_id)).toEqual(4); + expect(uuidVersion(responseBody[1].owner_id)).toEqual(4); + expect(responseBody[0].published_at > responseBody[1].published_at).toEqual(true); + }); }); });