From d31dc911d66c24ca77b575661c722f796c430b5a Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Sun, 5 May 2024 12:20:31 +0200 Subject: [PATCH 01/41] WIP: Add fetching of entites to UuidResolver --- .../schema/uuid/abstract-uuid/resolvers.ts | 118 +++++++++++++++++- 1 file changed, 116 insertions(+), 2 deletions(-) diff --git a/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts b/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts index c49d34fc4..40ebd6b74 100644 --- a/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts +++ b/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts @@ -1,6 +1,7 @@ import * as auth from '@serlo/authorization' import { option as O } from 'fp-ts' import * as t from 'io-ts' +import { PathReporter } from 'io-ts/lib/PathReporter' import { date } from 'io-ts-types/lib/date' import * as R from 'ramda' @@ -24,6 +25,7 @@ import { EntityRevisionDecoder, PageRevisionDecoder, NotificationEventType, + EntityType, } from '~/model/decoder' import { createEvent } from '~/schema/events/event' import { SubjectResolver } from '~/schema/subject/resolvers' @@ -159,10 +161,39 @@ const BaseUuid = t.type({ }) const WeightedNumberList = t.record( - t.union([t.literal('__no_key'), t.number]), + t.union([t.literal('__no_key'), t.string]), t.union([t.null, t.number]), ) +type DBEntityType = t.TypeOf +const DBEntityType = t.union([ + t.literal('applet'), + t.literal('article'), + t.literal('course'), + t.literal('course-page'), + t.literal('event'), + t.literal('text-exercise'), + t.literal('text-exercise-group'), + t.literal('video'), +]) + +const BaseEntity = t.intersection([ + BaseUuid, + t.type({ + discriminator: t.literal('entity'), + entityInstance: InstanceDecoder, + entityLicenseId: t.number, + entityCurrentRevisionId: t.union([t.null, t.number]), + entityDate: date, + entityType: DBEntityType, + entityRevisionIds: WeightedNumberList, + entityTaxonomyIds: WeightedNumberList, + entityTitle: t.union([t.string, t.null]), + entityChildrenIds: WeightedNumberList, + entityParentId: t.union([t.null, t.number]), + }), +]) + const BaseComment = t.intersection([ BaseUuid, t.type({ @@ -206,6 +237,26 @@ async function resolveUuidFromDatabase( uuid.trashed, uuid.discriminator, + entity_instance.subdomain as entityInstance, + entity.license_id as entityLicenseId, + entity.current_revision_id as entityCurrentRevisionId, + entity.date as entityDate, + entity_type.name as entityType, + JSON_OBJECTAGG( + COALESCE(entity_revision.id, "__no_key"), + entity_revision.id + ) as entityRevisionIds, + JSON_OBJECTAGG( + COALESCE(entity_taxonomy.term_taxonomy_id, "__no_key"), + entity_taxonomy.term_taxonomy_id + ) as entityTaxonomyIds, + current_revision.title as entityTitle, + JSON_OBJECTAGG( + COALESCE(entity_link_child.child_id, "__no_key"), + entity_link_child.order + ) as entityChildrenIds, + MIN(entity_link_parent.parent_id) as entityParentId, + comment.author_id as commentAuthorId, comment.title as commentTitle, comment.date as commentDate, @@ -236,6 +287,15 @@ async function resolveUuidFromDatabase( ) as taxonomyEntityChildrenIds from uuid + left join entity on entity.id = uuid.id + left join instance entity_instance on entity_instance.id = entity.instance_id + left join type entity_type on entity_type.id = entity.type_id + left join entity_revision on entity_revision.repository_id = entity.id + left join term_taxonomy_entity entity_taxonomy on entity_taxonomy.entity_id = entity.id + left join entity_revision current_revision on current_revision.id = entity.current_revision_id + left join entity_link entity_link_child on entity_link_child.parent_id = entity.id + left join entity_link entity_link_parent on entity_link_parent.child_id = entity.id + left join comment on comment.id = uuid.id left join comment comment_children on comment_children.parent_id = comment.id left join comment_status on comment_status.id = comment.id @@ -257,7 +317,61 @@ async function resolveUuidFromDatabase( if (BaseUuid.is(baseUuid)) { const base = { id: baseUuid.id, trashed: Boolean(baseUuid.trashed) } - if (BaseComment.is(baseUuid)) { + if (BaseEntity.is(baseUuid)) { + const taxonomyTermIds = getSortedList(baseUuid.entityTaxonomyIds) + + const subject = + taxonomyTermIds.length > 0 + ? await SubjectResolver.resolve( + { taxonomyId: taxonomyTermIds[0] }, + context, + ) + : null + const subjectName = subject != null ? '/' + toSlug(subject.name) : '' + const slugTitle = baseUuid.entityTitle + ? toSlug(baseUuid.entityTitle) + : baseUuid.id + + const entity = { + ...base, + instance: baseUuid.entityInstance, + date: baseUuid.entityDate.toISOString(), + licenseId: baseUuid.entityLicenseId, + currentRevisionId: baseUuid.entityCurrentRevisionId, + taxonomyTermIds, + alias: `${subjectName}/${baseUuid.id}/${slugTitle}`, + revisionIds: getSortedList(baseUuid.entityRevisionIds), + canonicalSubjectId: subject != null ? subject.id : null, + } + switch (baseUuid.entityType) { + case 'applet': + return { ...entity, __typename: EntityType.Applet } + case 'article': + return { ...entity, __typename: EntityType.Article } + case 'course': + return { + ...entity, + __typename: EntityType.Course, + pageIds: getSortedList(baseUuid.entityChildrenIds), + } + case 'course-page': + return baseUuid.entityParentId != null + ? { + ...entity, + __typename: EntityType.CoursePage, + parentId: baseUuid.entityParentId, + } + : null + case 'event': + return { ...entity, __typename: EntityType.Event } + case 'text-exercise': + return { ...entity, __typename: EntityType.Exercise } + case 'text-exercise-group': + return { ...entity, __typename: EntityType.ExerciseGroup } + default: + return { ...entity, __typename: EntityType.Video } + } + } else if (BaseComment.is(baseUuid)) { const parentId = baseUuid.commentParentUuid ?? baseUuid.commentParentCommentId ?? null From f089b62792d6793de5f95cb0c2968e6fa12795a3 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Sun, 5 May 2024 12:21:05 +0200 Subject: [PATCH 02/41] test: Update dates in fixtures --- __fixtures__/uuid/applet.ts | 2 +- __fixtures__/uuid/article.ts | 2 +- __fixtures__/uuid/course-page.ts | 2 +- __fixtures__/uuid/course.ts | 2 +- __fixtures__/uuid/event.ts | 2 +- __fixtures__/uuid/exercise-group.ts | 2 +- __fixtures__/uuid/exercise.ts | 2 +- __fixtures__/uuid/video.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/__fixtures__/uuid/applet.ts b/__fixtures__/uuid/applet.ts index 78f029068..b4c1404e1 100644 --- a/__fixtures__/uuid/applet.ts +++ b/__fixtures__/uuid/applet.ts @@ -14,7 +14,7 @@ export const applet: Model<'Applet'> = { trashed: false, instance: Instance.En, alias: '/math/35596/example-applet', - date: '2014-03-01T20:45:56Z', + date: '2020-01-29T17:47:19.000Z', currentRevisionId: 35597, revisionIds: [35597], licenseId, diff --git a/__fixtures__/uuid/article.ts b/__fixtures__/uuid/article.ts index 0774dfda2..5f6b022a3 100644 --- a/__fixtures__/uuid/article.ts +++ b/__fixtures__/uuid/article.ts @@ -16,7 +16,7 @@ export const article: ArticleWithAllFieldsDefined = { trashed: false, instance: Instance.De, alias: '/mathe/1855/parabel', - date: '2014-03-01T20:45:56Z', + date: '2014-03-01T20:45:56.000Z', currentRevisionId: 30674, licenseId, taxonomyTermIds: [5], diff --git a/__fixtures__/uuid/course-page.ts b/__fixtures__/uuid/course-page.ts index 82e80187e..fbe111a46 100644 --- a/__fixtures__/uuid/course-page.ts +++ b/__fixtures__/uuid/course-page.ts @@ -15,7 +15,7 @@ export const coursePage: Model<'CoursePage'> = { trashed: false, instance: Instance.De, alias: '/mathe/18521/startseite', - date: '2014-03-01T20:45:56Z', + date: '2014-03-17T12:24:54.000Z', currentRevisionId: 19277, revisionIds: [19277], licenseId, diff --git a/__fixtures__/uuid/course.ts b/__fixtures__/uuid/course.ts index c8a2358dc..37675b4e7 100644 --- a/__fixtures__/uuid/course.ts +++ b/__fixtures__/uuid/course.ts @@ -14,7 +14,7 @@ export const course: Model<'Course'> = { trashed: false, instance: Instance.De, alias: '/mathe/18514/überblick-zum-satz-des-pythagoras', - date: '2014-03-01T20:45:56Z', + date: '2014-03-17T12:22:17.000Z', currentRevisionId: 30713, revisionIds: [30713], licenseId, diff --git a/__fixtures__/uuid/event.ts b/__fixtures__/uuid/event.ts index 837f8876a..168763654 100644 --- a/__fixtures__/uuid/event.ts +++ b/__fixtures__/uuid/event.ts @@ -14,7 +14,7 @@ export const event: Model<'Event'> = { trashed: false, instance: Instance.De, alias: '/mathe/35554/beispielveranstaltung', - date: '2014-03-01T20:45:56Z', + date: '2019-12-02T22:40:41.000Z', currentRevisionId: 35555, revisionIds: [35555], licenseId, diff --git a/__fixtures__/uuid/exercise-group.ts b/__fixtures__/uuid/exercise-group.ts index e357507b2..20ead7ba1 100644 --- a/__fixtures__/uuid/exercise-group.ts +++ b/__fixtures__/uuid/exercise-group.ts @@ -28,7 +28,7 @@ export const exerciseGroup: Model<'ExerciseGroup'> = { trashed: false, instance: Instance.De, alias: '/mathe/2217/2217', - date: '2014-03-01T20:45:56Z', + date: '2014-03-01T20:54:51.000Z', currentRevisionId: 2218, revisionIds: [2218], licenseId, diff --git a/__fixtures__/uuid/exercise.ts b/__fixtures__/uuid/exercise.ts index 3a52a31d7..663f218da 100644 --- a/__fixtures__/uuid/exercise.ts +++ b/__fixtures__/uuid/exercise.ts @@ -14,7 +14,7 @@ export const exercise: Model<'Exercise'> = { trashed: false, instance: Instance.De, alias: '/mathe/29637/29637', - date: '2014-03-01T20:45:56Z', + date: '2014-09-08T10:42:33.000Z', currentRevisionId: 29638, revisionIds: [29638], licenseId, diff --git a/__fixtures__/uuid/video.ts b/__fixtures__/uuid/video.ts index c05b49202..9329d37a9 100644 --- a/__fixtures__/uuid/video.ts +++ b/__fixtures__/uuid/video.ts @@ -13,7 +13,7 @@ export const video: Model<'Video'> = { trashed: false, instance: Instance.De, alias: '/mathe/32321/schriftliche-addition', - date: '2014-10-15T12:49:12+02:00', + date: '2014-10-15T12:49:12.000Z', currentRevisionId: 32322, revisionIds: [32322], licenseId, From 0e665757ab7c9ec56f95bf06fdadf524a58396eb Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Sun, 5 May 2024 14:34:44 +0200 Subject: [PATCH 03/41] refactor(uuid): Add EntityRevision to UuidResolver --- .../schema/uuid/abstract-uuid/resolvers.ts | 83 ++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts b/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts index 40ebd6b74..7e892d4e1 100644 --- a/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts +++ b/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts @@ -1,7 +1,6 @@ import * as auth from '@serlo/authorization' import { option as O } from 'fp-ts' import * as t from 'io-ts' -import { PathReporter } from 'io-ts/lib/PathReporter' import { date } from 'io-ts-types/lib/date' import * as R from 'ramda' @@ -26,6 +25,8 @@ import { PageRevisionDecoder, NotificationEventType, EntityType, + EntityRevisionType, + castToNonEmptyString, } from '~/model/decoder' import { createEvent } from '~/schema/events/event' import { SubjectResolver } from '~/schema/subject/resolvers' @@ -194,6 +195,23 @@ const BaseEntity = t.intersection([ }), ]) +const BaseEntityRevision = t.intersection([ + BaseUuid, + t.type({ + discriminator: t.literal('entityRevision'), + revisionType: DBEntityType, + revisionContent: t.union([t.string, t.null]), + revisionDate: date, + revisionAuthorId: t.number, + revisionRepositoryId: t.number, + revisionChanges: t.union([t.string, t.null]), + revisionTitle: t.union([t.string, t.null]), + revisionMetaTitle: t.union([t.string, t.null]), + revisionMetaDescription: t.union([t.string, t.null]), + revisionUrl: t.union([t.string, t.null]), + }), +]) + const BaseComment = t.intersection([ BaseUuid, t.type({ @@ -257,6 +275,17 @@ async function resolveUuidFromDatabase( ) as entityChildrenIds, MIN(entity_link_parent.parent_id) as entityParentId, + revision_type.name as revisionType, + revision.content as revisionContent, + revision.date as revisionDate, + revision.author_id as revisionAuthorId, + revision.repository_id as revisionRepositoryId, + revision.changes as revisionChanges, + revision.title as revisionTitle, + revision.meta_title as revisionMetaTitle, + revision.meta_description as revisionMetaDescription, + revision.url as revisionUrl, + comment.author_id as commentAuthorId, comment.title as commentTitle, comment.date as commentDate, @@ -296,6 +325,10 @@ async function resolveUuidFromDatabase( left join entity_link entity_link_child on entity_link_child.parent_id = entity.id left join entity_link entity_link_parent on entity_link_parent.child_id = entity.id + left join entity_revision revision on revision.id = uuid.id + left join entity revision_entity on revision_entity.id = revision.repository_id + left join type revision_type on revision_entity.type_id = revision_type.id + left join comment on comment.id = uuid.id left join comment comment_children on comment_children.parent_id = comment.id left join comment_status on comment_status.id = comment.id @@ -371,6 +404,54 @@ async function resolveUuidFromDatabase( default: return { ...entity, __typename: EntityType.Video } } + } else if (BaseEntityRevision.is(baseUuid)) { + const defaultContent = JSON.stringify({ plugin: 'rows', state: [] }) + const content = + baseUuid.revisionContent != null && baseUuid.revisionContent.length > 0 + ? baseUuid.revisionContent + : defaultContent + const revision = { + ...base, + alias: `/entity/repository/compare/${baseUuid.revisionRepositoryId}/${baseUuid.id}`, + content: castToNonEmptyString(content), + date: baseUuid.revisionDate.toISOString(), + authorId: baseUuid.revisionAuthorId, + changes: baseUuid.revisionChanges ?? '', + title: + baseUuid.revisionTitle ?? `${baseUuid.revisionType} ${baseUuid.id}`, + metaTitle: baseUuid.revisionMetaTitle ?? '', + metaDescription: baseUuid.revisionMetaDescription ?? '', + repositoryId: baseUuid.revisionRepositoryId, + url: baseUuid.revisionUrl ?? '', + } + + switch (baseUuid.revisionType) { + case 'applet': + return { ...revision, __typename: EntityRevisionType.AppletRevision } + case 'article': + return { ...revision, __typename: EntityRevisionType.ArticleRevision } + case 'course': + return { ...revision, __typename: EntityRevisionType.CourseRevision } + case 'course-page': + return { + ...revision, + __typename: EntityRevisionType.CoursePageRevision, + } + case 'event': + return { ...revision, __typename: EntityRevisionType.EventRevision } + case 'text-exercise': + return { + ...revision, + __typename: EntityRevisionType.ExerciseRevision, + } + case 'text-exercise-group': + return { + ...revision, + __typename: EntityRevisionType.ExerciseGroupRevision, + } + default: + return { ...revision, __typename: EntityRevisionType.VideoRevision } + } } else if (BaseComment.is(baseUuid)) { const parentId = baseUuid.commentParentUuid ?? baseUuid.commentParentCommentId ?? null From ac8ffae652e8437c50867ec4fa8b0b911278f2c3 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Sun, 5 May 2024 14:35:18 +0200 Subject: [PATCH 04/41] test: Update article.ts --- __tests__/schema/uuid/article.ts | 171 ++++++++++++++++++++----------- 1 file changed, 109 insertions(+), 62 deletions(-) diff --git a/__tests__/schema/uuid/article.ts b/__tests__/schema/uuid/article.ts index a72e345e9..01a147c51 100644 --- a/__tests__/schema/uuid/article.ts +++ b/__tests__/schema/uuid/article.ts @@ -1,76 +1,123 @@ import gql from 'graphql-tag' import * as R from 'ramda' -import { article, articleRevision } from '../../../__fixtures__' -import { given, Client } from '../../__utils__' +import { Client } from '../../__utils__' -test('Article', async () => { - given('UuidQuery').for(article) - - await new Client() - .prepareQuery({ - query: gql` - query ($id: Int!) { - uuid(id: $id) { - ... on Article { - __typename +export const articleQuery = new Client().prepareQuery({ + query: gql` + query ($id: Int!) { + uuid(id: $id) { + ... on Article { + __typename + id + instance + alias + trashed + date + title + licenseId + currentRevision { + id + } + revisions { + nodes { + id + } + } + taxonomyTerms { + nodes { id - instance - alias - trashed - date } } } - `, - }) - .withVariables({ id: article.id }) - .shouldReturnData({ - uuid: R.pick( - ['id', '__typename', 'instance', 'alias', 'trashed', 'date'], - article, - ), - }) + } + } + `, + variables: { id: 27801 }, }) -test('ArticleRevision', async () => { - given('UuidQuery').for(articleRevision) - - await new Client() - .prepareQuery({ - query: gql` - query ($id: Int!) { - uuid(id: $id) { - ... on ArticleRevision { - __typename - id - trashed - alias - title - content - changes - metaTitle - metaDescription - } +const articleRevisionQuery = new Client().prepareQuery({ + query: gql` + query ($id: Int!) { + uuid(id: $id) { + ... on ArticleRevision { + __typename + id + author { + id + } + trashed + alias + date + repository { + id } + title + content + changes + metaTitle + metaDescription + url } - `, - }) - .withVariables({ id: articleRevision.id }) - .shouldReturnData({ - uuid: R.pick( - [ - 'id', - '__typename', - 'trashed', - 'alias', - 'title', - 'content', - 'changes', - 'metaTitle', - 'metaDescription', + } + } + `, + variables: { id: 35296 }, +}) + +test('Article', async () => { + await articleQuery.shouldReturnData({ + uuid: { + __typename: 'Article', + id: 27801, + instance: 'de', + alias: '/mathe/27801/addition-und-subtraktion-von-dezimalbrchen', + trashed: false, + date: '2014-08-26T08:29:35.000Z', + title: 'Addition und Subtraktion von Dezimalbrüchen', + licenseId: 1, + currentRevision: { id: 31072 }, + revisions: { + nodes: [ + { id: 31072 }, + { id: 30291 }, + { id: 29601 }, + { id: 28697 }, + { id: 28300 }, + { id: 28297 }, + { id: 28292 }, + { id: 28287 }, + { id: 28284 }, + { id: 28283 }, + { id: 28103 }, + { id: 28097 }, + { id: 28094 }, + { id: 27837 }, + { id: 27816 }, ], - articleRevision, - ), - }) + }, + taxonomyTerms: { nodes: [{ id: 1360 }] }, + }, + }) +}) + +test('ArticleRevision', async () => { + await articleRevisionQuery.shouldReturnData({ + uuid: { + __typename: 'ArticleRevision', + id: 35296, + author: { id: 26334 }, + trashed: false, + alias: '/entity/repository/compare/35295/35296', + date: '2015-02-22T20:29:03.000Z', + repository: { id: 35295 }, + title: '"falsche Freunde"', + content: + '{"plugin":"rows","state":[{"plugin":"text","state":[{"type":"p","children":[{"text":"wip"}]}]}]}', + changes: '', + metaTitle: '', + metaDescription: '', + url: '', + }, + }) }) From 56edcf673d82034c423e88da12e4d9048a08ad1c Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Sun, 5 May 2024 14:57:04 +0200 Subject: [PATCH 05/41] refactor(entity): Add SQL to license update --- __tests__/schema/entity/update-license.ts | 87 ++++++------------- packages/server/src/model/database-layer.ts | 9 -- packages/server/src/model/serlo.ts | 14 --- .../schema/uuid/abstract-entity/resolvers.ts | 40 +++++++-- 4 files changed, 60 insertions(+), 90 deletions(-) diff --git a/__tests__/schema/entity/update-license.ts b/__tests__/schema/entity/update-license.ts index 606880f28..70a4d8345 100644 --- a/__tests__/schema/entity/update-license.ts +++ b/__tests__/schema/entity/update-license.ts @@ -1,60 +1,41 @@ import gql from 'graphql-tag' -import { HttpResponse } from 'msw' -import { article, user } from '../../../__fixtures__' -import { given, Client } from '../../__utils__' - -const mutation = new Client({ userId: user.id }) - .prepareQuery({ - query: gql` - mutation ($input: EntityUpdateLicenseInput!) { - entity { - updateLicense(input: $input) { - success - } +import { user } from '../../../__fixtures__' +import { Client, expectEvent } from '../../__utils__' +import { articleQuery } from '../uuid/article' +import { NotificationEventType } from '~/model/decoder' + +const input = { entityId: 1855, licenseId: 4 } +const mutation = new Client({ userId: user.id }).prepareQuery({ + query: gql` + mutation ($input: EntityUpdateLicenseInput!) { + entity { + updateLicense(input: $input) { + success } } - `, - }) - .withInput({ entityId: article.id, licenseId: 4 }) - -const newLicenseId = 4 - -beforeEach(() => { - given('UuidQuery').for(user, article) - - given('EntitySetLicenseMutation') - .withPayload({ - userId: user.id, - entityId: article.id, - licenseId: 4, - }) - .isDefinedBy(() => { - given('UuidQuery').for({ ...article, licenseId: newLicenseId }) - - return HttpResponse.json({ success: true }) - }) + } + `, + variables: { input }, }) -test('returns "{ success: true }" when mutation could be successfully executed', async () => { +test('Updates license of an entity', async () => { + await articleQuery + .withVariables({ id: input.entityId }) + .shouldReturnData({ uuid: { licenseId: 1 } }) + await mutation.shouldReturnData({ entity: { updateLicense: { success: true } }, }) - await new Client() - .prepareQuery({ - query: gql` - query ($id: Int!) { - uuid(id: $id) { - ... on Article { - licenseId - } - } - } - `, - }) - .withVariables({ id: article.id }) - .shouldReturnData({ uuid: { licenseId: newLicenseId } }) + await articleQuery + .withVariables({ id: input.entityId }) + .shouldReturnData({ uuid: { licenseId: 4 } }) + + await expectEvent({ + __typename: NotificationEventType.SetLicense, + objectId: input.entityId, + }) }) test('fails when user is not authenticated', async () => { @@ -64,15 +45,3 @@ test('fails when user is not authenticated', async () => { test('fails when user does not have role "admin"', async () => { await mutation.forLoginUser('de_moderator').shouldFailWithError('FORBIDDEN') }) - -test('fails when database layer returns a 400er response', async () => { - given('EntitySetLicenseMutation').returnsBadRequest() - - await mutation.shouldFailWithError('BAD_USER_INPUT') -}) - -test('fails when database layer has an internal error', async () => { - given('EntitySetLicenseMutation').hasInternalServerError() - - await mutation.shouldFailWithError('INTERNAL_SERVER_ERROR') -}) diff --git a/packages/server/src/model/database-layer.ts b/packages/server/src/model/database-layer.ts index 67af023ea..20bed1304 100644 --- a/packages/server/src/model/database-layer.ts +++ b/packages/server/src/model/database-layer.ts @@ -174,15 +174,6 @@ export const spec = { }), canBeNull: false, }, - EntitySetLicenseMutation: { - payload: t.type({ - entityId: t.number, - licenseId: t.number, - userId: t.number, - }), - response: t.type({ success: t.literal(true) }), - canBeNull: false, - }, SubjectsQuery: { payload: t.type({}), response: t.strict({ diff --git a/packages/server/src/model/serlo.ts b/packages/server/src/model/serlo.ts index f3890bf36..13355112e 100644 --- a/packages/server/src/model/serlo.ts +++ b/packages/server/src/model/serlo.ts @@ -618,19 +618,6 @@ export function createSerloModel({ }, }) - const setEntityLicense = createMutation({ - type: 'EntitySetLicenseMutation', - decoder: DatabaseLayer.getDecoderFor('EntitySetLicenseMutation'), - mutate: (payload: DatabaseLayer.Payload<'EntitySetLicenseMutation'>) => { - return DatabaseLayer.makeRequest('EntitySetLicenseMutation', payload) - }, - async updateCache({ entityId }, { success }) { - if (success) { - await UuidResolver.removeCacheEntry({ id: entityId }, context) - } - }, - }) - const getPages = createRequest({ type: 'PagesQuery', decoder: DatabaseLayer.getDecoderFor('PagesQuery'), @@ -694,7 +681,6 @@ export function createSerloModel({ getPages, rejectEntityRevision, setEmail, - setEntityLicense, setSubscription, setThreadStatus, sortEntity, diff --git a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts index bdf7ad0c7..2cad5666f 100644 --- a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts +++ b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts @@ -10,11 +10,16 @@ import { assertUserIsAuthorized, createNamespace, } from '~/internals/graphql' -import { CourseDecoder, EntityDecoder } from '~/model/decoder' +import { + CourseDecoder, + EntityDecoder, + NotificationEventType, +} from '~/model/decoder' import { fetchScopeOfUuid } from '~/schema/authorization/utils' import { resolveConnection } from '~/schema/connection/utils' import { Resolvers } from '~/types' import { isDateString } from '~/utils' +import { createEvent } from '~/schema/events/event' export const resolvers: Resolvers = { Query: { @@ -108,7 +113,7 @@ export const resolvers: Resolvers = { }, async updateLicense(_parent, { input }, context) { - const { dataSources, userId } = context + const { userId, database } = context assertUserIsAuthenticated(userId) const { licenseId, entityId } = input @@ -124,13 +129,32 @@ export const resolvers: Resolvers = { context, }) - await dataSources.model.serlo.setEntityLicense({ - entityId, - licenseId, - userId, - }) + const transaction = await database.beginTransaction() - return { success: true, query: {} } + try { + await database.mutate('update entity set license_id = ? where id = ?', [ + licenseId, + entityId, + ]) + + await createEvent( + { + __typename: NotificationEventType.SetLicense, + actorId: userId, + repositoryId: entity.id, + instance: entity.instance, + }, + context, + ) + + await transaction.commit() + + await UuidResolver.removeCacheEntry({ id: entity.id }, context) + + return { success: true, query: {} } + } finally { + await transaction.rollback() + } }, async checkoutRevision(_parent, { input }, context) { From 05f8633f508bcf49d428cfc128548b7c41dc7fc0 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Sun, 5 May 2024 14:58:31 +0200 Subject: [PATCH 06/41] Fix lint errors --- __tests__/schema/uuid/article.ts | 1 - packages/server/src/schema/uuid/abstract-entity/resolvers.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/__tests__/schema/uuid/article.ts b/__tests__/schema/uuid/article.ts index 01a147c51..52135b3be 100644 --- a/__tests__/schema/uuid/article.ts +++ b/__tests__/schema/uuid/article.ts @@ -1,5 +1,4 @@ import gql from 'graphql-tag' -import * as R from 'ramda' import { Client } from '../../__utils__' diff --git a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts index 2cad5666f..d94d05ee3 100644 --- a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts +++ b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts @@ -17,9 +17,9 @@ import { } from '~/model/decoder' import { fetchScopeOfUuid } from '~/schema/authorization/utils' import { resolveConnection } from '~/schema/connection/utils' +import { createEvent } from '~/schema/events/event' import { Resolvers } from '~/types' import { isDateString } from '~/utils' -import { createEvent } from '~/schema/events/event' export const resolvers: Resolvers = { Query: { From 8e9babeeacc2b5589c961187f79eee3a3f0821b1 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Wed, 8 May 2024 21:31:11 +0200 Subject: [PATCH 07/41] chore: Use db with the migration --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8e89ff54f..18b94f1f4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: ports: - '6379:6379' mysql: - image: eu.gcr.io/serlo-shared/serlo-mysql-database:latest + image: eu.gcr.io/serlo-shared/serlo-mysql-database:prerelease-merge-course-pages-into-courses platform: linux/x86_64 pull_policy: always ports: From 64a746a8efdd2a5152bb78a7b0bf37c2b9160c54 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Wed, 8 May 2024 21:45:22 +0200 Subject: [PATCH 08/41] refactor(uuid): Format SQL --- packages/server/src/schema/uuid/abstract-uuid/resolvers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts b/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts index 51c2f343b..290efed8e 100644 --- a/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts +++ b/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts @@ -313,7 +313,7 @@ async function resolveUuidFromDatabase( WHEN comment_status.name = 'no_status' THEN 'noStatus' ELSE comment_status.name END AS commentStatus, - + taxonomy_type.name AS taxonomyType, taxonomy_instance.subdomain AS taxonomyInstance, term.name AS taxonomyName, @@ -335,7 +335,7 @@ async function resolveUuidFromDatabase( user.last_login AS userLastLogin, user.description AS userDescription, JSON_ARRAYAGG(role.name) AS userRoles - + FROM uuid LEFT JOIN comment ON comment.id = uuid.id @@ -366,7 +366,7 @@ async function resolveUuidFromDatabase( LEFT JOIN user ON user.id = uuid.id LEFT JOIN role_user ON user.id = role_user.user_id LEFT JOIN role ON role.id = role_user.role_id - + WHERE uuid.id = ? GROUP BY uuid.id `, From 126ac52a7e216f39cfcc5fd67150a720cc71cd48 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Wed, 8 May 2024 22:21:31 +0200 Subject: [PATCH 09/41] WIP: Move SQL in reject and checkout revisions --- packages/server/src/model/database-layer.ts | 18 --- packages/server/src/model/serlo.ts | 43 ------- .../schema/uuid/abstract-entity/resolvers.ts | 114 +++++++++++++++--- 3 files changed, 100 insertions(+), 75 deletions(-) diff --git a/packages/server/src/model/database-layer.ts b/packages/server/src/model/database-layer.ts index 7dc0877c5..b8b8b0eac 100644 --- a/packages/server/src/model/database-layer.ts +++ b/packages/server/src/model/database-layer.ts @@ -74,24 +74,6 @@ export const spec = { }), canBeNull: false, }, - EntityCheckoutRevisionMutation: { - payload: t.type({ - revisionId: t.number, - userId: t.number, - reason: t.string, - }), - response: t.type({ success: t.literal(true) }), - canBeNull: false, - }, - EntityRejectRevisionMutation: { - payload: t.type({ - revisionId: t.number, - userId: t.number, - reason: t.string, - }), - response: t.type({ success: t.literal(true) }), - canBeNull: false, - }, EntityCreateMutation: { payload: t.type({ userId: t.number, diff --git a/packages/server/src/model/serlo.ts b/packages/server/src/model/serlo.ts index 2728382a8..401e2603f 100644 --- a/packages/server/src/model/serlo.ts +++ b/packages/server/src/model/serlo.ts @@ -469,34 +469,6 @@ export function createSerloModel({ }, }) - const checkoutEntityRevision = createMutation({ - type: 'EntityCheckoutRevisionMutation', - decoder: DatabaseLayer.getDecoderFor('EntityCheckoutRevisionMutation'), - async mutate( - payload: DatabaseLayer.Payload<'EntityCheckoutRevisionMutation'>, - ) { - return DatabaseLayer.makeRequest( - 'EntityCheckoutRevisionMutation', - payload, - ) - }, - async updateCache({ revisionId }) { - const revision = await UuidResolver.resolveWithDecoder( - EntityRevisionDecoder, - { id: revisionId }, - context, - ) - - await UuidResolver.removeCacheEntry( - { id: revision.repositoryId }, - context, - ) - await UuidResolver.removeCacheEntry({ id: revisionId }, context) - - await getUnrevisedEntities._querySpec.removeCache({ payload: undefined }) - }, - }) - const createPage = createMutation({ type: 'PageCreateMutation', decoder: DatabaseLayer.getDecoderFor('PageCreateMutation'), @@ -539,19 +511,6 @@ export function createSerloModel({ }, }) - const rejectEntityRevision = createMutation({ - type: 'EntityRejectRevisionMutation', - decoder: DatabaseLayer.getDecoderFor('EntityRejectRevisionMutation'), - mutate(payload: DatabaseLayer.Payload<'EntityRejectRevisionMutation'>) { - return DatabaseLayer.makeRequest('EntityRejectRevisionMutation', payload) - }, - async updateCache({ revisionId }) { - await UuidResolver.removeCacheEntry({ id: revisionId }, context) - - await getUnrevisedEntities._querySpec.removeCache({ payload: undefined }) - }, - }) - const getDeletedEntities = createRequest({ type: 'DeletedEntitiesQuery', decoder: DatabaseLayer.getDecoderFor('DeletedEntitiesQuery'), @@ -615,7 +574,6 @@ export function createSerloModel({ addPageRevision, addRole, archiveThread, - checkoutEntityRevision, checkoutPageRevision, createComment, createEntity, @@ -636,7 +594,6 @@ export function createSerloModel({ getUnrevisedEntitiesPerSubject, getUsersByRole, getPages, - rejectEntityRevision, setSubscription, sortEntity, } diff --git a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts index d94d05ee3..3388b9e3d 100644 --- a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts +++ b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts @@ -13,6 +13,7 @@ import { import { CourseDecoder, EntityDecoder, + EntityRevisionDecoder, NotificationEventType, } from '~/model/decoder' import { fetchScopeOfUuid } from '~/schema/authorization/utils' @@ -158,38 +159,123 @@ export const resolvers: Resolvers = { }, async checkoutRevision(_parent, { input }, context) { - const { dataSources, userId } = context + const { database, userId } = context assertUserIsAuthenticated(userId) - const scope = await fetchScopeOfUuid({ id: input.revisionId }, context) + const revision = await UuidResolver.resolveWithDecoder( + EntityRevisionDecoder, + { id: input.revisionId }, + context, + ) + const entity = await UuidResolver.resolveWithDecoder( + EntityDecoder, + { id: revision.repositoryId }, + context, + ) + await assertUserIsAuthorized({ message: 'You are not allowed to check out the provided revision.', - guard: serloAuth.Entity.checkoutRevision(scope), + guard: serloAuth.Entity.checkoutRevision( + instanceToScope(entity.instance), + ), context, }) - await dataSources.model.serlo.checkoutEntityRevision({ - revisionId: input.revisionId, - reason: input.reason, - userId, - }) + const transaction = await database.beginTransaction() + + try { + await database.mutate( + `update entity set current_revision = ? where id = ?`, + [revision.id, entity.id], + ) + + await database.mutate(`update uuid set trashed = 0 where id = ?`, [ + revision.id, + ]) + + await createEvent( + { + __typename: NotificationEventType.CheckoutRevision, + actorId: userId, + instance: entity.instance, + repositoryId: entity.id, + revisionId: revision.id, + reason: input.reason, + }, + context, + ) - return { success: true, query: {} } + await transaction.commit() + + await UuidResolver.removeCacheEntry({ id: entity.id }, context) + await UuidResolver.removeCacheEntry({ id: revision.id }, context) + + // TODO: UnrevisedRevisions + + return { success: true, query: {} } + } finally { + await transaction.rollback() + } }, + async rejectRevision(_parent, { input }, context) { - const { dataSources, userId } = context + const { database, userId } = context assertUserIsAuthenticated(userId) - const scope = await fetchScopeOfUuid({ id: input.revisionId }, context) + const revision = await UuidResolver.resolveWithDecoder( + EntityRevisionDecoder, + { id: input.revisionId }, + context, + ) + const entity = await UuidResolver.resolveWithDecoder( + EntityDecoder, + { id: revision.repositoryId }, + context, + ) + await assertUserIsAuthorized({ context, message: 'You are not allowed to reject the provided revision.', - guard: serloAuth.Entity.rejectRevision(scope), + guard: serloAuth.Entity.rejectRevision( + instanceToScope(entity.instance), + ), }) - await dataSources.model.serlo.rejectEntityRevision({ ...input, userId }) + const transaction = await database.beginTransaction() + + try { + await database.mutate( + `update entity set current_revision = ? where id = ?`, + [revision.id, entity.id], + ) + + await database.mutate(`update uuid set trashed = 0 where id = ?`, [ + revision.id, + ]) + + await createEvent( + { + __typename: NotificationEventType.CheckoutRevision, + actorId: userId, + instance: entity.instance, + repositoryId: entity.id, + revisionId: revision.id, + reason: input.reason, + }, + context, + ) - return { success: true, query: {} } + await transaction.commit() + + await UuidResolver.removeCacheEntry({ id: entity.id }, context) + await UuidResolver.removeCacheEntry({ id: revision.id }, context) + + // TODO: UnrevisedRevisions + + return { success: true, query: {} } + } finally { + await transaction.rollback() + } }, }, } From 9c1bfc19243f38588c912245dddd9db5d6dc2a14 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Thu, 9 May 2024 19:52:31 +0200 Subject: [PATCH 10/41] refactor(entity): Add SQL for UnrevisedEntitiesQuery --- __tests__/__utils__/handlers.ts | 5 - __tests__/schema/subject.ts | 27 +++++- __tests__/schema/uuid/user.ts | 34 +------ packages/server/src/model/database-layer.ts | 5 - packages/server/src/model/serlo.ts | 97 +------------------ packages/server/src/model/types.ts | 6 +- .../server/src/schema/subject/resolvers.ts | 28 +++--- .../schema/uuid/abstract-entity/resolvers.ts | 34 ++++++- .../server/src/schema/uuid/user/resolvers.ts | 38 ++------ 9 files changed, 86 insertions(+), 188 deletions(-) diff --git a/__tests__/__utils__/handlers.ts b/__tests__/__utils__/handlers.ts index 61fc2b999..4ecd3eb90 100644 --- a/__tests__/__utils__/handlers.ts +++ b/__tests__/__utils__/handlers.ts @@ -26,11 +26,6 @@ const ForDefinitions = { given('EventQuery').withPayload({ id: event.id }).returns(event) }) }, - UnrevisedEntitiesQuery(entities: Model<'AbstractEntity'>[]) { - given('UnrevisedEntitiesQuery') - .withPayload({}) - .returns({ unrevisedEntityIds: entities.map((entity) => entity.id) }) - }, DeletedEntitiesQuery(entities: Model<'AbstractEntity'>[]) { given('UuidQuery').for( entities.map((entity) => { diff --git a/__tests__/schema/subject.ts b/__tests__/schema/subject.ts index 225e8cded..4e5e47268 100644 --- a/__tests__/schema/subject.ts +++ b/__tests__/schema/subject.ts @@ -1,6 +1,6 @@ import gql from 'graphql-tag' -import { article, emptySubjects, taxonomyTermSubject } from '../../__fixtures__' +import { article, taxonomyTermSubject } from '../../__fixtures__' import { Client, given } from '../__utils__' import { Instance } from '~/types' @@ -54,7 +54,6 @@ test('`Subject.id` returns encoded id of subject', async () => { test('`Subject.unrevisedEntities` returns list of unrevisedEntities', async () => { given('UuidQuery').for(taxonomyTermSubject, article) - given('UnrevisedEntitiesQuery').for(article) await new Client() .prepareQuery({ @@ -73,16 +72,34 @@ test('`Subject.unrevisedEntities` returns list of unrevisedEntities', async () = } `, }) - .withVariables({ instance: article.instance }) + .withVariables({ instance: 'de' }) .shouldReturnData({ subject: { subjects: [ { unrevisedEntities: { - nodes: [{ __typename: 'Article', id: article.id }], + nodes: [{ __typename: 'Article', id: 34741 }], }, }, - ...emptySubjects, + { unrevisedEntities: { nodes: [] } }, + { unrevisedEntities: { nodes: [] } }, + { unrevisedEntities: { nodes: [] } }, + { + unrevisedEntities: { + nodes: [ + { __typename: 'Article', id: 34907 }, + { __typename: 'Article', id: 35247 }, + ], + }, + }, + { + unrevisedEntities: { + nodes: [{ __typename: 'Article', id: 26892 }], + }, + }, + { unrevisedEntities: { nodes: [] } }, + { unrevisedEntities: { nodes: [] } }, + { unrevisedEntities: { nodes: [] } }, ], }, }) diff --git a/__tests__/schema/uuid/user.ts b/__tests__/schema/uuid/user.ts index eb9bddc2c..12dbe2bba 100644 --- a/__tests__/schema/uuid/user.ts +++ b/__tests__/schema/uuid/user.ts @@ -469,35 +469,6 @@ describe('User', () => { }) test('property unrevisedEntities', async () => { - const unrevisedRevisionByUser: Model<'ArticleRevision'> = { - ...articleRevision, - id: nextUuid(article.currentRevisionId), - authorId: user.id, - } - const unrevisedRevisionByAnotherUser: Model<'ArticleRevision'> = { - ...articleRevision, - id: nextUuid(unrevisedRevisionByUser.id), - authorId: user2.id, - } - const articleByUser: Model<'Article'> = { - ...article, - id: nextUuid(article.id), - revisionIds: [unrevisedRevisionByUser.id, ...article.revisionIds], - } - const articleByAnotherUser: Model<'Article'> = { - ...article, - id: nextUuid(articleByUser.id), - revisionIds: [unrevisedRevisionByAnotherUser.id, ...article.revisionIds], - } - - given('UuidQuery').for( - unrevisedRevisionByUser, - unrevisedRevisionByAnotherUser, - articleByAnotherUser, - articleByUser, - ) - given('UnrevisedEntitiesQuery').for(articleByUser, articleByAnotherUser) - await client .prepareQuery({ query: gql` @@ -507,18 +478,17 @@ describe('User', () => { unrevisedEntities { nodes { id - __typename } } } } } `, + variables: { id: 299 }, }) - .withVariables({ id: user.id }) .shouldReturnData({ uuid: { - unrevisedEntities: { nodes: [getTypenameAndId(articleByUser)] }, + unrevisedEntities: { nodes: [{ id: 26892 }] }, }, }) }) diff --git a/packages/server/src/model/database-layer.ts b/packages/server/src/model/database-layer.ts index b8b8b0eac..62d240c6c 100644 --- a/packages/server/src/model/database-layer.ts +++ b/packages/server/src/model/database-layer.ts @@ -208,11 +208,6 @@ export const spec = { response: t.type({ firstCommentIds: t.array(t.number) }), canBeNull: false, }, - UnrevisedEntitiesQuery: { - payload: t.type({}), - response: t.strict({ unrevisedEntityIds: t.array(t.number) }), - canBeNull: false, - }, UserCreateMutation: { payload: t.type({ username: t.string, diff --git a/packages/server/src/model/serlo.ts b/packages/server/src/model/serlo.ts index 401e2603f..de7d756c3 100644 --- a/packages/server/src/model/serlo.ts +++ b/packages/server/src/model/serlo.ts @@ -1,13 +1,8 @@ import { option as O } from 'fp-ts' -import * as t from 'io-ts' import { executePrompt } from './ai' import * as DatabaseLayer from './database-layer' -import { - EntityDecoder, - EntityRevisionDecoder, - PageRevisionDecoder, -} from './decoder' +import { PageRevisionDecoder } from './decoder' import { Context } from '~/context' import { createMutation, @@ -147,48 +142,6 @@ export function createSerloModel({ context, ) - const getUnrevisedEntities = createLegacyQuery( - { - type: 'UnrevisedEntitiesQuery', - decoder: DatabaseLayer.getDecoderFor('UnrevisedEntitiesQuery'), - getCurrentValue: () => { - return DatabaseLayer.makeRequest('UnrevisedEntitiesQuery', {}) - }, - enableSwr: true, - staleAfter: { minutes: 2 }, - maxAge: { hours: 1 }, - getKey: () => 'serlo.org/unrevised', - getPayload: (key) => { - return key === 'serlo.org/unrevised' ? O.some(undefined) : O.none - }, - examplePayload: undefined, - }, - context, - ) - - const getUnrevisedEntitiesPerSubject = createRequest({ - type: 'getUnrevisedEntitiesPerSubject', - decoder: t.record(t.string, t.union([t.array(t.number), t.null])), - async getCurrentValue(_payload: undefined) { - const { unrevisedEntityIds } = await getUnrevisedEntities() - const result: Record = {} - - for (const entityId of unrevisedEntityIds) { - const entity = await UuidResolver.resolveWithDecoder( - EntityDecoder, - { id: entityId }, - context, - ) - const key = entity.canonicalSubjectId?.toString() ?? '__no_subject' - - result[key] ??= [] - result[key]?.push(entity.id) - } - - return result - }, - }) - const getNotificationEvent = createLegacyQuery( { type: 'EventQuery', @@ -367,29 +320,6 @@ export function createSerloModel({ await UuidResolver.removeCacheEntry({ id: taxonomyTermId }, context) } - await getUnrevisedEntities._querySpec.setCache({ - payload: undefined, - getValue(current) { - if (!current) return - if ( - !input.needsReview && - current.unrevisedEntityIds.includes(newEntity.id) - ) { - current.unrevisedEntityIds = current.unrevisedEntityIds.filter( - (id) => id !== newEntity.id, - ) - } - if ( - input.needsReview && - !current.unrevisedEntityIds.includes(newEntity.id) - ) { - current.unrevisedEntityIds.push(newEntity.id) - } - - return current - }, - }) - if (input.subscribeThis) { await getSubscriptions._querySpec.setCache({ payload: { userId }, @@ -419,29 +349,6 @@ export function createSerloModel({ if (success) { await UuidResolver.removeCacheEntry({ id: input.entityId }, context) - await getUnrevisedEntities._querySpec.setCache({ - payload: undefined, - getValue(current) { - if (!current) return - if ( - !input.needsReview && - current.unrevisedEntityIds.includes(input.entityId) - ) { - current.unrevisedEntityIds = current.unrevisedEntityIds.filter( - (id) => id !== input.entityId, - ) - } - if ( - input.needsReview && - !current.unrevisedEntityIds.includes(input.entityId) - ) { - current.unrevisedEntityIds.push(input.entityId) - } - - return current - }, - }) - if (input.subscribeThis) { await getSubscriptions._querySpec.setCache({ payload: { userId }, @@ -590,8 +497,6 @@ export function createSerloModel({ getPotentialSpamUsers, getSubscriptions, getThreadIds, - getUnrevisedEntities, - getUnrevisedEntitiesPerSubject, getUsersByRole, getPages, setSubscription, diff --git a/packages/server/src/model/types.ts b/packages/server/src/model/types.ts index edce6f317..be51ee0af 100644 --- a/packages/server/src/model/types.ts +++ b/packages/server/src/model/types.ts @@ -22,6 +22,7 @@ import { CreateTaxonomyLinkNotificationEventDecoder, CreateTaxonomyTermNotificationEventDecoder, CreateThreadNotificationEventDecoder, + EntityDecoder, EventDecoder, EventRevisionDecoder, ExerciseDecoder, @@ -64,7 +65,10 @@ export interface Models { ExerciseRevision: t.TypeOf Page: t.TypeOf PageRevision: t.TypeOf - Subject: { taxonomyTermId: number } + Subject: { + taxonomyTermId: number + allUnrevisedEntities: t.TypeOf[] + } SubscriptionInfo: t.TypeOf< typeof SubscriptionsDecoder >['subscriptions'][number] diff --git a/packages/server/src/schema/subject/resolvers.ts b/packages/server/src/schema/subject/resolvers.ts index 971fc6a77..52638587c 100644 --- a/packages/server/src/schema/subject/resolvers.ts +++ b/packages/server/src/schema/subject/resolvers.ts @@ -12,6 +12,7 @@ import { } from '~/model/decoder' import { encodeSubjectId } from '~/schema/subject/utils' import { type Resolvers } from '~/types' +import { resolveUnrevisedEntityIds } from '../uuid/abstract-entity/resolvers' export const SubjectsResolver = createCachedResolver({ name: 'SubjectsResolver', @@ -60,7 +61,18 @@ export const resolvers: Resolvers = { const filteredSubjects = subjects.filter( (subject) => subject.instance === payload.instance, ) - return filteredSubjects + const unrevisedEntityIds = await resolveUnrevisedEntityIds({}, context) + const unreviseedEntities = await Promise.all( + unrevisedEntityIds.map((id) => + UuidResolver.resolveWithDecoder(EntityDecoder, { id }, context), + ), + ) + return filteredSubjects.map((subject) => ({ + ...subject, + allUnrevisedEntities: unreviseedEntities.filter( + (entity) => entity.canonicalSubjectId === subject.taxonomyTermId, + ), + })) }, }, Subject: { @@ -74,19 +86,9 @@ export const resolvers: Resolvers = { context, ) }, - async unrevisedEntities(subject, payload, context) { - const entitiesPerSubject = - await context.dataSources.model.serlo.getUnrevisedEntitiesPerSubject() - const entityIds = - entitiesPerSubject[subject.taxonomyTermId.toString()] ?? [] - const entities = await Promise.all( - entityIds.map((id) => - UuidResolver.resolveWithDecoder(EntityDecoder, { id }, context), - ), - ) - + unrevisedEntities({ allUnrevisedEntities }, payload, _) { return resolveConnection({ - nodes: entities, + nodes: allUnrevisedEntities, payload, createCursor: (node) => node.id.toString(), }) diff --git a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts index 3388b9e3d..89ab368e4 100644 --- a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts +++ b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts @@ -4,6 +4,7 @@ import * as t from 'io-ts' import { createSetEntityResolver } from './entity-set-handler' import { UuidResolver } from '../abstract-uuid/resolvers' +import { Context } from '~/context' import { UserInputError } from '~/errors' import { assertUserIsAuthenticated, @@ -16,7 +17,6 @@ import { EntityRevisionDecoder, NotificationEventType, } from '~/model/decoder' -import { fetchScopeOfUuid } from '~/schema/authorization/utils' import { resolveConnection } from '~/schema/connection/utils' import { createEvent } from '~/schema/events/event' import { Resolvers } from '~/types' @@ -280,6 +280,38 @@ export const resolvers: Resolvers = { }, } +export async function resolveUnrevisedEntityIds( + { userId = null }: { userId?: number | null }, + { database }: Pick, +) { + const rows = await database.fetchAll<{ id: number }>( + ` + select + entity.id + from entity + join uuid entity_uuid on entity_uuid.id = entity.id + join ( + select + revision.repository_id as entity_id, + max(revision.id) as max_revision_id + from entity_revision revision + join uuid revision_uuid on revision.id = revision_uuid.id + where + revision_uuid.trashed = 0 + and (? is null or revision.author_id = ?) + group by revision.repository_id + ) as revision on revision.entity_id = entity.id + where + entity_uuid.trashed = 0 + and (entity.current_revision_id is null or + entity.current_revision_id < revision.max_revision_id) + `, + [userId, userId], + ) + + return rows.map((row) => row.id) +} + function decodeDateOfDeletion(after: string) { const afterParsed = JSON.parse( Buffer.from(after, 'base64').toString(), diff --git a/packages/server/src/schema/uuid/user/resolvers.ts b/packages/server/src/schema/uuid/user/resolvers.ts index 0234f3bc4..d4f9274d4 100644 --- a/packages/server/src/schema/uuid/user/resolvers.ts +++ b/packages/server/src/schema/uuid/user/resolvers.ts @@ -34,6 +34,7 @@ import { resolveConnection } from '~/schema/connection/utils' import { createThreadResolvers } from '~/schema/thread/utils' import { createUuidResolvers } from '~/schema/uuid/abstract-uuid/utils' import { Instance, Resolvers } from '~/types' +import { resolveUnrevisedEntityIds } from '../abstract-entity/resolvers' export const activeUserIdsQuery = createCachedResolver< Record, @@ -222,41 +223,18 @@ export const resolvers: Resolvers = { return edits < 5 }, async unrevisedEntities(user, payload, context) { - const { dataSources } = context - const { unrevisedEntityIds } = - await dataSources.model.serlo.getUnrevisedEntities() - const unrevisedEntitiesAndRevisions = await Promise.all( + const unrevisedEntityIds = await resolveUnrevisedEntityIds( + { userId: user.id }, + context, + ) + const unrevisedEntities = await Promise.all( unrevisedEntityIds.map((id) => - UuidResolver.resolveWithDecoder(EntityDecoder, { id }, context).then( - async (unrevisedEntity) => { - const unrevisedRevisionIds = unrevisedEntity.revisionIds.filter( - (revisionId) => - unrevisedEntity.currentRevisionId === null || - revisionId > unrevisedEntity.currentRevisionId, - ) - const unrevisedRevisions = await Promise.all( - unrevisedRevisionIds.map((id) => - UuidResolver.resolveWithDecoder( - RevisionDecoder, - { id }, - context, - ), - ), - ) - - return [unrevisedEntity, unrevisedRevisions] as const - }, - ), + UuidResolver.resolveWithDecoder(EntityDecoder, { id }, context), ), ) - const unrevisedEntitiesByUser = unrevisedEntitiesAndRevisions - .filter(([_, unrevisedRevisions]) => - unrevisedRevisions.some((revision) => revision.authorId === user.id), - ) - .map(([unrevisedEntity, _]) => unrevisedEntity) return resolveConnection({ - nodes: unrevisedEntitiesByUser, + nodes: unrevisedEntities, payload, createCursor: (node) => node.id.toString(), }) From bf72838afdf04e4f13ed2be58d97a09b6d079b1e Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Thu, 9 May 2024 20:21:39 +0200 Subject: [PATCH 11/41] test: Remove given() of subject.ts --- __tests__/schema/subject.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/__tests__/schema/subject.ts b/__tests__/schema/subject.ts index bff23ed74..d0bef8102 100644 --- a/__tests__/schema/subject.ts +++ b/__tests__/schema/subject.ts @@ -1,7 +1,6 @@ import gql from 'graphql-tag' -import { article, taxonomyTermSubject } from '../../__fixtures__' -import { Client, given, subjectQuery } from '../__utils__' +import { Client, subjectQuery } from '../__utils__' test('endpoint "subjects" returns list of all subjects for an instance', async () => { await subjectQuery.withVariables({ instance: 'en' }).shouldReturnData({ @@ -16,8 +15,6 @@ test('`Subject.id` returns encoded id of subject', async () => { }) test('`Subject.unrevisedEntities` returns list of unrevisedEntities', async () => { - given('UuidQuery').for(taxonomyTermSubject, article) - await new Client() .prepareQuery({ query: gql` From dafc537a751e9c20e905813fdbcdad1bee40d79e Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Thu, 9 May 2024 20:34:56 +0200 Subject: [PATCH 12/41] test: Add `entityQuery` and `entityRevisionQuery` --- __tests__/__utils__/query.ts | 62 ++++++++++++++++++++ __tests__/schema/entity/update-license.ts | 7 +-- __tests__/schema/uuid/article.ts | 70 +---------------------- 3 files changed, 68 insertions(+), 71 deletions(-) diff --git a/__tests__/__utils__/query.ts b/__tests__/__utils__/query.ts index fc676c087..08c9e0320 100644 --- a/__tests__/__utils__/query.ts +++ b/__tests__/__utils__/query.ts @@ -2,6 +2,68 @@ import gql from 'graphql-tag' import { Client } from './assertions' +export const entityQuery = new Client().prepareQuery({ + query: gql` + query ($id: Int!) { + uuid(id: $id) { + ... on Article { + __typename + id + instance + alias + trashed + date + title + licenseId + currentRevision { + id + } + revisions { + nodes { + id + } + } + taxonomyTerms { + nodes { + id + } + } + } + } + } + `, + variables: { id: 27801 }, +}) + +export const entityRevisionQuery = new Client().prepareQuery({ + query: gql` + query ($id: Int!) { + uuid(id: $id) { + ... on ArticleRevision { + __typename + id + author { + id + } + trashed + alias + date + repository { + id + } + title + content + changes + metaTitle + metaDescription + url + } + } + } + `, + variables: { id: 35296 }, +}) + export const taxonomyTermQuery = new Client().prepareQuery({ query: gql` query ($id: Int!) { diff --git a/__tests__/schema/entity/update-license.ts b/__tests__/schema/entity/update-license.ts index 70a4d8345..4f3dd002f 100644 --- a/__tests__/schema/entity/update-license.ts +++ b/__tests__/schema/entity/update-license.ts @@ -1,8 +1,7 @@ import gql from 'graphql-tag' import { user } from '../../../__fixtures__' -import { Client, expectEvent } from '../../__utils__' -import { articleQuery } from '../uuid/article' +import { Client, expectEvent, entityQuery } from '../../__utils__' import { NotificationEventType } from '~/model/decoder' const input = { entityId: 1855, licenseId: 4 } @@ -20,7 +19,7 @@ const mutation = new Client({ userId: user.id }).prepareQuery({ }) test('Updates license of an entity', async () => { - await articleQuery + await entityQuery .withVariables({ id: input.entityId }) .shouldReturnData({ uuid: { licenseId: 1 } }) @@ -28,7 +27,7 @@ test('Updates license of an entity', async () => { entity: { updateLicense: { success: true } }, }) - await articleQuery + await entityQuery .withVariables({ id: input.entityId }) .shouldReturnData({ uuid: { licenseId: 4 } }) diff --git a/__tests__/schema/uuid/article.ts b/__tests__/schema/uuid/article.ts index 52135b3be..df673b4b9 100644 --- a/__tests__/schema/uuid/article.ts +++ b/__tests__/schema/uuid/article.ts @@ -1,71 +1,7 @@ -import gql from 'graphql-tag' - -import { Client } from '../../__utils__' - -export const articleQuery = new Client().prepareQuery({ - query: gql` - query ($id: Int!) { - uuid(id: $id) { - ... on Article { - __typename - id - instance - alias - trashed - date - title - licenseId - currentRevision { - id - } - revisions { - nodes { - id - } - } - taxonomyTerms { - nodes { - id - } - } - } - } - } - `, - variables: { id: 27801 }, -}) - -const articleRevisionQuery = new Client().prepareQuery({ - query: gql` - query ($id: Int!) { - uuid(id: $id) { - ... on ArticleRevision { - __typename - id - author { - id - } - trashed - alias - date - repository { - id - } - title - content - changes - metaTitle - metaDescription - url - } - } - } - `, - variables: { id: 35296 }, -}) +import { entityQuery, entityRevisionQuery } from '../../__utils__' test('Article', async () => { - await articleQuery.shouldReturnData({ + await entityQuery.shouldReturnData({ uuid: { __typename: 'Article', id: 27801, @@ -101,7 +37,7 @@ test('Article', async () => { }) test('ArticleRevision', async () => { - await articleRevisionQuery.shouldReturnData({ + await entityRevisionQuery.shouldReturnData({ uuid: { __typename: 'ArticleRevision', id: 35296, From 8298ba6ff733c91aefc2b97c91b6ec4d148fd81e Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Thu, 9 May 2024 20:43:34 +0200 Subject: [PATCH 13/41] test: Replace specific entity tests with one --- __tests__/schema/uuid/applet.ts | 74 ------ __tests__/schema/uuid/course-page.ts | 99 -------- __tests__/schema/uuid/course.ts | 237 ------------------ __tests__/schema/uuid/entity-revision.ts | 22 ++ .../schema/uuid/{article.ts => entity.ts} | 25 +- __tests__/schema/uuid/event.ts | 72 ------ __tests__/schema/uuid/exercise-group.ts | 66 ----- __tests__/schema/uuid/exercise.ts | 62 ----- __tests__/schema/uuid/video.ts | 70 ------ 9 files changed, 24 insertions(+), 703 deletions(-) delete mode 100644 __tests__/schema/uuid/applet.ts delete mode 100644 __tests__/schema/uuid/course-page.ts delete mode 100644 __tests__/schema/uuid/course.ts create mode 100644 __tests__/schema/uuid/entity-revision.ts rename __tests__/schema/uuid/{article.ts => entity.ts} (55%) delete mode 100644 __tests__/schema/uuid/event.ts delete mode 100644 __tests__/schema/uuid/exercise-group.ts delete mode 100644 __tests__/schema/uuid/exercise.ts delete mode 100644 __tests__/schema/uuid/video.ts diff --git a/__tests__/schema/uuid/applet.ts b/__tests__/schema/uuid/applet.ts deleted file mode 100644 index e39ea081a..000000000 --- a/__tests__/schema/uuid/applet.ts +++ /dev/null @@ -1,74 +0,0 @@ -import gql from 'graphql-tag' -import * as R from 'ramda' - -import { applet, appletRevision } from '../../../__fixtures__' -import { given, Client } from '../../__utils__' - -test('Applet', async () => { - given('UuidQuery').for(applet) - - await new Client() - .prepareQuery({ - query: gql` - query ($id: Int!) { - uuid(id: $id) { - __typename - ... on Applet { - id - trashed - instance - date - } - } - } - `, - }) - .withVariables({ id: applet.id }) - .shouldReturnData({ - uuid: R.pick(['__typename', 'id', 'trashed', 'instance', 'date'], applet), - }) -}) - -test('AppletRevision', async () => { - given('UuidQuery').for(appletRevision) - - await new Client() - .prepareQuery({ - query: gql` - query ($id: Int!) { - uuid(id: $id) { - __typename - ... on AppletRevision { - id - trashed - date - url - title - content - changes - metaTitle - metaDescription - } - } - } - `, - }) - .withVariables(appletRevision) - .shouldReturnData({ - uuid: R.pick( - [ - '__typename', - 'id', - 'trashed', - 'date', - 'url', - 'title', - 'content', - 'changes', - 'metaTitle', - 'metaDescription', - ], - appletRevision, - ), - }) -}) diff --git a/__tests__/schema/uuid/course-page.ts b/__tests__/schema/uuid/course-page.ts deleted file mode 100644 index acb1025ee..000000000 --- a/__tests__/schema/uuid/course-page.ts +++ /dev/null @@ -1,99 +0,0 @@ -import gql from 'graphql-tag' -import * as R from 'ramda' - -import { course, coursePage, coursePageRevision } from '../../../__fixtures__' -import { getTypenameAndId, given, Client } from '../../__utils__' - -describe('CoursePage', () => { - beforeEach(() => { - given('UuidQuery').for(coursePage) - }) - - test('by id', async () => { - given('UuidQuery').for(coursePage) - - await new Client() - .prepareQuery({ - query: gql` - query coursePage($id: Int!) { - uuid(id: $id) { - __typename - ... on CoursePage { - id - trashed - instance - date - } - } - } - `, - }) - .withVariables({ id: coursePage.id }) - .shouldReturnData({ - uuid: R.pick( - ['__typename', 'id', 'trashed', 'instance', 'date'], - coursePage, - ), - }) - }) - - test('by id (w/ course)', async () => { - given('UuidQuery').for(course) - - await new Client() - .prepareQuery({ - query: gql` - query coursePage($id: Int!) { - uuid(id: $id) { - ... on CoursePage { - course { - __typename - id - } - } - } - } - `, - }) - .withVariables({ id: coursePage.id }) - .shouldReturnData({ uuid: { course: getTypenameAndId(course) } }) - }) - - test('CoursePageRevision', async () => { - given('UuidQuery').for(coursePageRevision) - - await new Client() - .prepareQuery({ - query: gql` - query coursePageRevision($id: Int!) { - uuid(id: $id) { - __typename - ... on CoursePageRevision { - id - trashed - date - title - content - changes - } - } - } - `, - }) - .withVariables({ id: coursePageRevision.id }) - .shouldReturnData({ - uuid: R.pick( - [ - '__typename', - 'id', - 'trashed', - 'date', - 'title', - 'content', - 'changes', - ], - coursePageRevision, - ), - }) - }) -}) diff --git a/__tests__/schema/uuid/course.ts b/__tests__/schema/uuid/course.ts deleted file mode 100644 index 100ffeca2..000000000 --- a/__tests__/schema/uuid/course.ts +++ /dev/null @@ -1,237 +0,0 @@ -import gql from 'graphql-tag' -import * as R from 'ramda' - -import { course, coursePage, courseRevision } from '../../../__fixtures__' -import { getTypenameAndId, given, Client } from '../../__utils__' - -describe('Course', () => { - beforeEach(() => { - given('UuidQuery').for(course) - }) - - test('by id', async () => { - await new Client() - .prepareQuery({ - query: gql` - query course($id: Int!) { - uuid(id: $id) { - __typename - ... on Course { - id - trashed - instance - date - } - } - } - `, - }) - .withVariables(course) - .shouldReturnData({ - uuid: R.pick( - ['__typename', 'id', 'trashed', 'instance', 'date'], - course, - ), - }) - }) - - test('by id (w/ pages)', async () => { - given('UuidQuery').for(coursePage) - - await new Client() - .prepareQuery({ - query: gql` - query course($id: Int!) { - uuid(id: $id) { - ... on Course { - pages { - __typename - id - } - } - } - } - `, - }) - .withVariables(course) - .shouldReturnData({ uuid: { pages: [getTypenameAndId(coursePage)] } }) - }) - - describe('filter "trashed"', () => { - const pages = [ - { ...coursePage, id: 1, trashed: true }, - { ...coursePage, id: 2, trashed: false }, - ] - const courseWithTwoPages = { ...course, pageIds: [1, 2] } - - beforeEach(() => { - given('UuidQuery').for(pages, courseWithTwoPages) - }) - - test('when not set', async () => { - await new Client() - .prepareQuery({ - query: gql` - query ($id: Int!) { - uuid(id: $id) { - ... on Course { - pages { - id - } - } - } - } - `, - }) - .withVariables({ id: courseWithTwoPages.id }) - .shouldReturnData({ uuid: { pages: [{ id: 1 }, { id: 2 }] } }) - }) - - test('when set to true', async () => { - await new Client() - .prepareQuery({ - query: gql` - query ($id: Int!) { - uuid(id: $id) { - ... on Course { - pages(trashed: true) { - id - } - } - } - } - `, - }) - .withVariables({ id: courseWithTwoPages.id }) - .shouldReturnData({ uuid: { pages: [{ id: 1 }] } }) - }) - - test('when set to false', async () => { - await new Client() - .prepareQuery({ - query: gql` - query ($id: Int!) { - uuid(id: $id) { - ... on Course { - pages(trashed: false) { - id - } - } - } - } - `, - }) - .withVariables({ id: courseWithTwoPages.id }) - .shouldReturnData({ uuid: { pages: [{ id: 2 }] } }) - }) - }) - - describe('filter "hasCurrentRevision"', () => { - const pages = [ - { ...coursePage, id: 1 }, - { ...coursePage, id: 2, currentRevisionId: null }, - ] - const courseWithTwoPages = { ...course, pageIds: [1, 2] } - - beforeEach(() => { - given('UuidQuery').for(pages, courseWithTwoPages) - }) - - test('when not set', async () => { - await new Client() - .prepareQuery({ - query: gql` - query ($id: Int!) { - uuid(id: $id) { - ... on Course { - pages { - id - } - } - } - } - `, - }) - .withVariables({ id: course.id }) - .shouldReturnData({ uuid: { pages: [{ id: 1 }, { id: 2 }] } }) - }) - - test('when set to true', async () => { - await new Client() - .prepareQuery({ - query: gql` - query ($id: Int!) { - uuid(id: $id) { - ... on Course { - pages(hasCurrentRevision: true) { - id - } - } - } - } - `, - }) - .withVariables({ id: course.id }) - .shouldReturnData({ uuid: { pages: [{ id: 1 }] } }) - }) - - test('when set to false', async () => { - await new Client() - .prepareQuery({ - query: gql` - query ($id: Int!) { - uuid(id: $id) { - ... on Course { - pages(hasCurrentRevision: false) { - id - } - } - } - } - `, - }) - .withVariables({ id: course.id }) - .shouldReturnData({ uuid: { pages: [{ id: 2 }] } }) - }) - }) -}) - -test('CourseRevision', async () => { - given('UuidQuery').for(courseRevision) - - await new Client() - .prepareQuery({ - query: gql` - query courseRevision($id: Int!) { - uuid(id: $id) { - __typename - ... on CourseRevision { - id - trashed - date - title - content - changes - metaDescription - } - } - } - `, - }) - .withVariables(courseRevision) - .shouldReturnData({ - uuid: R.pick( - [ - '__typename', - 'id', - 'trashed', - 'date', - 'title', - 'content', - 'changes', - 'metaDescription', - ], - courseRevision, - ), - }) -}) diff --git a/__tests__/schema/uuid/entity-revision.ts b/__tests__/schema/uuid/entity-revision.ts new file mode 100644 index 000000000..76c7f77f8 --- /dev/null +++ b/__tests__/schema/uuid/entity-revision.ts @@ -0,0 +1,22 @@ +import { entityRevisionQuery } from '../../__utils__' + +test('Uuid query for an entity revision', async () => { + await entityRevisionQuery.shouldReturnData({ + uuid: { + __typename: 'ArticleRevision', + id: 35296, + author: { id: 26334 }, + trashed: false, + alias: '/entity/repository/compare/35295/35296', + date: '2015-02-22T20:29:03.000Z', + repository: { id: 35295 }, + title: '"falsche Freunde"', + content: + '{"plugin":"rows","state":[{"plugin":"text","state":[{"type":"p","children":[{"text":"wip"}]}]}]}', + changes: '', + metaTitle: '', + metaDescription: '', + url: '', + }, + }) +}) diff --git a/__tests__/schema/uuid/article.ts b/__tests__/schema/uuid/entity.ts similarity index 55% rename from __tests__/schema/uuid/article.ts rename to __tests__/schema/uuid/entity.ts index df673b4b9..177f7b0c6 100644 --- a/__tests__/schema/uuid/article.ts +++ b/__tests__/schema/uuid/entity.ts @@ -1,6 +1,6 @@ -import { entityQuery, entityRevisionQuery } from '../../__utils__' +import { entityQuery } from '../../__utils__' -test('Article', async () => { +test('UuidQuery for an entity', async () => { await entityQuery.shouldReturnData({ uuid: { __typename: 'Article', @@ -35,24 +35,3 @@ test('Article', async () => { }, }) }) - -test('ArticleRevision', async () => { - await entityRevisionQuery.shouldReturnData({ - uuid: { - __typename: 'ArticleRevision', - id: 35296, - author: { id: 26334 }, - trashed: false, - alias: '/entity/repository/compare/35295/35296', - date: '2015-02-22T20:29:03.000Z', - repository: { id: 35295 }, - title: '"falsche Freunde"', - content: - '{"plugin":"rows","state":[{"plugin":"text","state":[{"type":"p","children":[{"text":"wip"}]}]}]}', - changes: '', - metaTitle: '', - metaDescription: '', - url: '', - }, - }) -}) diff --git a/__tests__/schema/uuid/event.ts b/__tests__/schema/uuid/event.ts deleted file mode 100644 index df98df2ce..000000000 --- a/__tests__/schema/uuid/event.ts +++ /dev/null @@ -1,72 +0,0 @@ -import gql from 'graphql-tag' -import * as R from 'ramda' - -import { event, eventRevision } from '../../../__fixtures__' -import { given, Client } from '../../__utils__' - -test('Event', async () => { - given('UuidQuery').for(event) - - await new Client() - .prepareQuery({ - query: gql` - query event($id: Int!) { - uuid(id: $id) { - __typename - ... on Event { - id - trashed - instance - date - } - } - } - `, - }) - .withVariables({ id: event.id }) - .shouldReturnData({ - uuid: R.pick(['__typename', 'id', 'trashed', 'instance', 'date'], event), - }) -}) - -test('EventRevision', async () => { - given('UuidQuery').for(eventRevision) - - await new Client() - .prepareQuery({ - query: gql` - query eventRevision($id: Int!) { - uuid(id: $id) { - __typename - ... on EventRevision { - id - trashed - date - title - content - changes - metaTitle - metaDescription - } - } - } - `, - }) - .withVariables({ id: eventRevision.id }) - .shouldReturnData({ - uuid: R.pick( - [ - '__typename', - 'id', - 'trashed', - 'date', - 'title', - 'content', - 'changes', - 'metaTitle', - 'metaDescription', - ], - eventRevision, - ), - }) -}) diff --git a/__tests__/schema/uuid/exercise-group.ts b/__tests__/schema/uuid/exercise-group.ts deleted file mode 100644 index 3d5177e8d..000000000 --- a/__tests__/schema/uuid/exercise-group.ts +++ /dev/null @@ -1,66 +0,0 @@ -import gql from 'graphql-tag' -import * as R from 'ramda' - -import { exerciseGroup, exerciseGroupRevision } from '../../../__fixtures__' -import { Client, given } from '../../__utils__' - -describe('ExerciseGroup', () => { - beforeEach(() => { - given('UuidQuery').for(exerciseGroup) - }) - - test('by id', async () => { - await new Client() - .prepareQuery({ - query: gql` - query exerciseGroup($id: Int!) { - uuid(id: $id) { - __typename - ... on ExerciseGroup { - id - trashed - instance - date - } - } - } - `, - }) - .withVariables({ id: exerciseGroup.id }) - .shouldReturnData({ - uuid: R.pick( - ['__typename', 'id', 'trashed', 'instance', 'date'], - exerciseGroup, - ), - }) - }) - - test('ExerciseGroupRevision', async () => { - given('UuidQuery').for(exerciseGroupRevision) - - await new Client() - .prepareQuery({ - query: gql` - query exerciseGroupRevision($id: Int!) { - uuid(id: $id) { - __typename - ... on ExerciseGroupRevision { - id - trashed - date - content - changes - } - } - } - `, - }) - .withVariables({ id: exerciseGroupRevision.id }) - .shouldReturnData({ - uuid: R.pick( - ['__typename', 'id', 'trashed', 'date', 'content', 'changes'], - exerciseGroupRevision, - ), - }) - }) -}) diff --git a/__tests__/schema/uuid/exercise.ts b/__tests__/schema/uuid/exercise.ts deleted file mode 100644 index 2db71151f..000000000 --- a/__tests__/schema/uuid/exercise.ts +++ /dev/null @@ -1,62 +0,0 @@ -import gql from 'graphql-tag' -import * as R from 'ramda' - -import { exercise, exerciseRevision } from '../../../__fixtures__' -import { given, Client } from '../../__utils__' - -test('Exercise', async () => { - given('UuidQuery').for(exercise) - - await new Client() - .prepareQuery({ - query: gql` - query exercise($id: Int!) { - uuid(id: $id) { - __typename - ... on Exercise { - id - trashed - instance - date - } - } - } - `, - }) - .withVariables({ id: exercise.id }) - .shouldReturnData({ - uuid: R.pick( - ['__typename', 'id', 'trashed', 'instance', 'date'], - exercise, - ), - }) -}) - -test('ExerciseRevision', async () => { - given('UuidQuery').for(exerciseRevision) - - await new Client() - .prepareQuery({ - query: gql` - query exerciseRevision($id: Int!) { - uuid(id: $id) { - __typename - ... on ExerciseRevision { - id - trashed - date - content - changes - } - } - } - `, - }) - .withVariables({ id: exerciseRevision.id }) - .shouldReturnData({ - uuid: R.pick( - ['__typename', 'id', 'trashed', 'date', 'content', 'changes'], - exerciseRevision, - ), - }) -}) diff --git a/__tests__/schema/uuid/video.ts b/__tests__/schema/uuid/video.ts deleted file mode 100644 index 92bc7517c..000000000 --- a/__tests__/schema/uuid/video.ts +++ /dev/null @@ -1,70 +0,0 @@ -import gql from 'graphql-tag' -import * as R from 'ramda' - -import { video, videoRevision } from '../../../__fixtures__' -import { given, Client } from '../../__utils__' - -test('Video', async () => { - given('UuidQuery').for(video) - - await new Client() - .prepareQuery({ - query: gql` - query video($id: Int!) { - uuid(id: $id) { - __typename - ... on Video { - id - trashed - instance - date - } - } - } - `, - }) - .withVariables({ id: video.id }) - .shouldReturnData({ - uuid: R.pick(['__typename', 'id', 'trashed', 'instance', 'date'], video), - }) -}) - -test('VideoRevision', async () => { - given('UuidQuery').for(videoRevision) - - await new Client() - .prepareQuery({ - query: gql` - query videoRevision($id: Int!) { - uuid(id: $id) { - __typename - ... on VideoRevision { - id - trashed - date - title - content - url - changes - } - } - } - `, - }) - .withVariables({ id: videoRevision.id }) - .shouldReturnData({ - uuid: R.pick( - [ - '__typename', - 'id', - 'trashed', - 'date', - 'title', - 'content', - 'url', - 'changes', - ], - videoRevision, - ), - }) -}) From cb75ba08f3e777878dbcc7ddf5d047379cb68d22 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Thu, 9 May 2024 20:48:40 +0200 Subject: [PATCH 14/41] test: Remove test abstract-taxonomy-term-child.ts --- .../uuid/abstract-taxonomy-term-child.ts | 68 ------------------- 1 file changed, 68 deletions(-) delete mode 100644 __tests__/schema/uuid/abstract-taxonomy-term-child.ts diff --git a/__tests__/schema/uuid/abstract-taxonomy-term-child.ts b/__tests__/schema/uuid/abstract-taxonomy-term-child.ts deleted file mode 100644 index 45b6fd2a8..000000000 --- a/__tests__/schema/uuid/abstract-taxonomy-term-child.ts +++ /dev/null @@ -1,68 +0,0 @@ -import gql from 'graphql-tag' -import * as R from 'ramda' - -import { - applet, - article, - course, - event, - exercise, - exerciseGroup, - taxonomyTermSubject, - video, -} from '../../../__fixtures__' -import { getTypenameAndId, given, Client } from '../../__utils__' -import { Model } from '~/internals/graphql' -import { EntityType } from '~/model/decoder' - -const taxonomyTermChildFixtures: Record< - Model<'AbstractTaxonomyTermChild'>['__typename'], - Model<'AbstractTaxonomyTermChild'> -> = { - [EntityType.Applet]: applet, - [EntityType.Article]: article, - [EntityType.Course]: course, - [EntityType.Event]: event, - [EntityType.Exercise]: exercise, - [EntityType.ExerciseGroup]: exerciseGroup, - [EntityType.Video]: video, -} -const taxonomyTermChildCases = R.toPairs(taxonomyTermChildFixtures) - -test.each(taxonomyTermChildCases)( - '%s by id (w/ taxonomyTerms)', - async (_type, entity) => { - given('UuidQuery').for( - { ...entity, taxonomyTermIds: [taxonomyTermSubject.id] }, - taxonomyTermSubject, - ) - - await new Client() - .prepareQuery({ - query: gql` - query taxonomyTerms($id: Int!) { - uuid(id: $id) { - ... on AbstractTaxonomyTermChild { - taxonomyTerms { - nodes { - __typename - id - } - totalCount - } - } - } - } - `, - }) - .withVariables({ id: entity.id }) - .shouldReturnData({ - uuid: { - taxonomyTerms: { - nodes: [getTypenameAndId(taxonomyTermSubject)], - totalCount: 1, - }, - }, - }) - }, -) From 621dfb0ccfd856acdfbacfd3b22c8adf9fd1e3d6 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Thu, 9 May 2024 20:55:15 +0200 Subject: [PATCH 15/41] test: Remove test for abstract-repository.ts Those are already tested in entity.ts and entity-revision.ts --- __tests__/schema/uuid/abstract-repository.ts | 392 ------------------- 1 file changed, 392 deletions(-) delete mode 100644 __tests__/schema/uuid/abstract-repository.ts diff --git a/__tests__/schema/uuid/abstract-repository.ts b/__tests__/schema/uuid/abstract-repository.ts deleted file mode 100644 index 84ede72a8..000000000 --- a/__tests__/schema/uuid/abstract-repository.ts +++ /dev/null @@ -1,392 +0,0 @@ -import gql from 'graphql-tag' -import * as R from 'ramda' - -import { - applet, - appletRevision, - article, - articleRevision, - course, - coursePage, - coursePageRevision, - courseRevision, - event, - eventRevision, - exercise, - exerciseGroup, - exerciseGroupRevision, - exerciseRevision, - licenseId, - page, - pageRevision, - user, - video, - videoRevision, -} from '../../../__fixtures__' -import { nextUuid, getTypenameAndId, given, Client } from '../../__utils__' -import { Model } from '~/internals/graphql' -import { - EntityRevisionType, - EntityType, - RepositoryType, - RevisionType, - DiscriminatorType, -} from '~/model/decoder' - -const client = new Client() - -const repositoryFixtures: Record< - RepositoryType, - { - repository: Model<'AbstractRepository'> - revision: Model<'AbstractRevision'> - revisionType: RevisionType - } -> = { - [EntityType.Applet]: { - repository: applet, - revision: appletRevision, - revisionType: EntityRevisionType.AppletRevision, - }, - [EntityType.Article]: { - repository: article, - revision: articleRevision, - revisionType: EntityRevisionType.ArticleRevision, - }, - [EntityType.Course]: { - repository: course, - revision: courseRevision, - revisionType: EntityRevisionType.CourseRevision, - }, - [EntityType.CoursePage]: { - repository: coursePage, - revision: coursePageRevision, - revisionType: EntityRevisionType.CoursePageRevision, - }, - [EntityType.Event]: { - repository: event, - revision: eventRevision, - revisionType: EntityRevisionType.EventRevision, - }, - [EntityType.Exercise]: { - repository: exercise, - revision: exerciseRevision, - revisionType: EntityRevisionType.ExerciseRevision, - }, - [EntityType.ExerciseGroup]: { - repository: exerciseGroup, - revision: exerciseGroupRevision, - revisionType: EntityRevisionType.ExerciseGroupRevision, - }, - [EntityType.Video]: { - repository: video, - revision: videoRevision, - revisionType: EntityRevisionType.VideoRevision, - }, - [DiscriminatorType.Page]: { - repository: page, - revision: pageRevision, - revisionType: DiscriminatorType.PageRevision, - }, -} -const repositoryCases = R.toPairs(repositoryFixtures) - -describe('Repository', () => { - const aliasQuery = client.prepareQuery({ - query: gql` - query repository($alias: AliasInput!) { - uuid(alias: $alias) { - __typename - ... on AbstractRepository { - id - } - } - } - `, - }) - - test.each(repositoryCases)('%s by id', async (_type, { repository }) => { - given('UuidQuery').for(repository) - - await client - .prepareQuery({ - query: gql` - query repository($id: Int!) { - uuid(id: $id) { - __typename - ... on AbstractRepository { - id - trashed - date - } - } - } - `, - }) - .withVariables(repository) - .shouldReturnData({ - uuid: R.pick(['__typename', 'id', 'trashed', 'date'], repository), - }) - }) - - test.each(repositoryCases)( - '%s by alias (url-encoded)', - async (_type, { repository }) => { - given('UuidQuery').for(repository) - given('AliasQuery') - .withPayload({ instance: repository.instance, path: '/ü' }) - .returns({ - id: repository.id, - instance: repository.instance, - path: '/ü', - }) - - await aliasQuery - .withVariables({ - alias: { instance: repository.instance, path: '/%C3%BC' }, - }) - .shouldReturnData({ uuid: getTypenameAndId(repository) }) - }, - ) - - test.each(repositoryCases)( - '%s by alias (/:id)', - async (_type, { repository }) => { - given('UuidQuery').for(repository) - given('AliasQuery') - .withPayload({ instance: repository.instance, path: '/path' }) - .returns({ - id: repository.id, - instance: repository.instance, - path: '/path', - }) - - await aliasQuery - .withVariables({ - alias: { instance: repository.instance, path: `/${repository.id}` }, - }) - .shouldReturnData({ uuid: getTypenameAndId(repository) }) - }, - ) - - test.each(repositoryCases)( - '%s by id (w/ currentRevision)', - async (type, { repository, revision }) => { - given('UuidQuery').for(repository, revision) - - await client - .prepareQuery({ - query: gql` - query repository($id: Int!) { - uuid(id: $id) { - ... on ${type} { - currentRevision { - __typename - id - trashed - date - } - } - } - } - `, - }) - .withVariables(repository) - .shouldReturnData({ - uuid: { - currentRevision: R.pick( - ['__typename', 'id', 'trashed', 'date'], - revision, - ), - }, - }) - }, - ) - - test.each(repositoryCases)( - '%s by id (w/ license)', - async (_type, { repository }) => { - given('UuidQuery').for(repository) - - await client - .prepareQuery({ - query: gql` - query license($id: Int!) { - uuid(id: $id) { - ... on AbstractRepository { - licenseId - } - } - } - `, - }) - .withVariables({ id: repository.id }) - .shouldReturnData({ uuid: { licenseId } }) - }, - ) - - describe.each(repositoryCases)( - '%s by id (w/ revisions)', - (type, { repository, revision }) => { - const revisedRevision = { ...revision, id: revision.id - 10 } - const unrevisedRevision = { ...revision, id: nextUuid(revision.id) } - const revisionsQuery = client.prepareQuery({ - query: gql` - query unrevisedRevisionsOfRepository($id: Int!, $unrevised: Boolean) { - uuid(id: $id) { - ... on ${type} { - revisions (unrevised: $unrevised) { - totalCount - nodes { - __typename - id - } - } - } - } - } - `, - }) - - beforeEach(() => { - given('UuidQuery').for( - { - ...repository, - revisionIds: [ - unrevisedRevision.id, - revisedRevision.id, - revision.id, - ], - }, - unrevisedRevision, - revision, - revisedRevision, - ) - }) - - test('returns all revisions when no arguments are given', async () => { - await revisionsQuery - .withVariables({ id: repository.id }) - .shouldReturnData({ - uuid: { - revisions: { - nodes: [ - getTypenameAndId(unrevisedRevision), - getTypenameAndId(revision), - getTypenameAndId(revisedRevision), - ], - }, - }, - }) - }) - - test('returns all unrevised revisions when unrevised=true', async () => { - await revisionsQuery - .withVariables({ id: repository.id, unrevised: true }) - .shouldReturnData({ - uuid: { - revisions: { - nodes: [getTypenameAndId(unrevisedRevision)], - totalCount: 1, - }, - }, - }) - }) - - test('when unrevised=true trashed revisions are not included', async () => { - given('UuidQuery').for({ ...unrevisedRevision, trashed: true }) - - await revisionsQuery - .withVariables({ id: repository.id, unrevised: true }) - .shouldReturnData({ - uuid: { revisions: { nodes: [], totalCount: 0 } }, - }) - }) - - test('returns all revised revisions when unrevised=false', async () => { - await revisionsQuery - .withVariables({ id: repository.id, unrevised: false }) - .shouldReturnData({ - uuid: { - revisions: { - totalCount: 2, - nodes: [ - getTypenameAndId(revision), - getTypenameAndId(revisedRevision), - ], - }, - }, - }) - }) - - test('when unrevised=true trashed revisions are not included', async () => { - given('UuidQuery').for({ ...revisedRevision, trashed: true }) - - await revisionsQuery - .withVariables({ id: repository.id, unrevised: false }) - .shouldReturnData({ - uuid: { - revisions: { - nodes: [getTypenameAndId(revision)], - totalCount: 1, - }, - }, - }) - }) - }, - ) -}) - -describe('Revision', () => { - test.each(repositoryCases)( - '%s by id (w/ author)', - async (_type, { revision }) => { - given('UuidQuery').for({ ...revision, authorId: user.id }, user) - - await client - .prepareQuery({ - query: gql` - query revision($id: Int!) { - uuid(id: $id) { - ... on AbstractRevision { - author { - __typename - id - } - } - } - } - `, - }) - .withVariables(revision) - .shouldReturnData({ uuid: { author: getTypenameAndId(user) } }) - }, - ) - - test.each(repositoryCases)( - '%s by id (w/ repository)', - async (_type, { repository, revision, revisionType }) => { - given('UuidQuery').for(repository, revision) - - await client - .prepareQuery({ - query: gql` - query revision($id: Int!) { - uuid(id: $id) { - ... on ${revisionType} { - repository { - __typename - id - } - } - } - } - `, - }) - .withVariables(revision) - .shouldReturnData({ - uuid: { repository: getTypenameAndId(repository) }, - }) - }, - ) -}) From 5bb1176a234c09aa7a510671217f7a54cc67c018 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Thu, 9 May 2024 21:36:00 +0200 Subject: [PATCH 16/41] test(entity): Remove sort.ts After https://github.com/serlo/db-migrations/pull/352 we do not need this endpoint any more ans thus we can delete already the test for it to make the refactoring more easy --- __tests__/schema/entity/sort.ts | 91 --------------------------------- 1 file changed, 91 deletions(-) delete mode 100644 __tests__/schema/entity/sort.ts diff --git a/__tests__/schema/entity/sort.ts b/__tests__/schema/entity/sort.ts deleted file mode 100644 index 6c34f19e3..000000000 --- a/__tests__/schema/entity/sort.ts +++ /dev/null @@ -1,91 +0,0 @@ -import gql from 'graphql-tag' -import { HttpResponse } from 'msw' - -import { course as baseCouse, user, coursePage } from '../../../__fixtures__' -import { Client, given } from '../../__utils__' - -const course = { ...baseCouse, pageIds: [18521, 30713] } - -const mutation = new Client({ userId: user.id }) - .prepareQuery({ - query: gql` - mutation ($input: EntitySortInput!) { - entity { - sort(input: $input) { - success - } - } - } - `, - }) - .withInput({ childrenIds: [30713, 18521], entityId: course.id }) - -beforeEach(() => { - given('UuidQuery').for(user, course) -}) - -test('returns "{ success: true }" when mutation could be successfully executed', async () => { - given('EntitySortMutation').returns({ success: true }) - - await mutation.shouldReturnData({ entity: { sort: { success: true } } }) -}) - -test('updates the cache of Course', async () => { - given('EntitySortMutation').isDefinedBy(async ({ request }) => { - const body = await request.json() - const { childrenIds } = body.payload - - given('UuidQuery').for({ ...course, pageIds: childrenIds }) - - return HttpResponse.json({ success: true }) - }) - - given('UuidQuery').for( - { ...coursePage, id: 18521 }, - { ...coursePage, id: 30713 }, - ) - - const query = new Client({ userId: user.id }) - .prepareQuery({ - query: gql` - query ($id: Int!) { - uuid(id: $id) { - ... on Course { - pages { - id - } - } - } - } - `, - }) - .withVariables({ id: course.id }) - - await query.shouldReturnData({ - uuid: { pages: [{ id: 18521 }, { id: 30713 }] }, - }) - - await mutation - .withInput({ childrenIds: [30713, 18521], entityId: course.id }) - .execute() - - await query.shouldReturnData({ - uuid: { pages: [{ id: 30713 }, { id: 18521 }] }, - }) -}) - -test('fails when user is not authenticated', async () => { - await mutation.forUnauthenticatedUser().shouldFailWithError('UNAUTHENTICATED') -}) - -test('fails when database layer returns a 400er response', async () => { - given('EntitySortMutation').returnsBadRequest() - - await mutation.shouldFailWithError('BAD_USER_INPUT') -}) - -test('fails when database layer has an internal error', async () => { - given('EntitySortMutation').hasInternalServerError() - - await mutation.shouldFailWithError('INTERNAL_SERVER_ERROR') -}) From 51ce1c1c9e69e5d2dcbc923df3d4d4c737dcbeb3 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Thu, 9 May 2024 21:55:05 +0200 Subject: [PATCH 17/41] refactor(entity): Update metadata after DB change --- __tests__/schema/metadata.ts | 7 ++++--- packages/server/src/schema/metadata/resolvers.ts | 10 ++-------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/__tests__/schema/metadata.ts b/__tests__/schema/metadata.ts index 5044bbd02..56032d164 100644 --- a/__tests__/schema/metadata.ts +++ b/__tests__/schema/metadata.ts @@ -72,9 +72,10 @@ describe('endpoint "resources"', () => { test('shows description when it is set', async () => { await global.database.mutate(` - update entity_revision_field - set value = "description for entity 2153" - where id = 41509 and field = "meta_description";`) + update entity_revision + join entity on entity.current_revision_id = entity_revision.id + set entity_revision.meta_description = "description for entity 2153" + where entity.id = 2153`) const after = afterForId(2153) const data = await query.withVariables({ first: 1, after }).getData() diff --git a/packages/server/src/schema/metadata/resolvers.ts b/packages/server/src/schema/metadata/resolvers.ts index 10a80bec4..61cb5ad76 100644 --- a/packages/server/src/schema/metadata/resolvers.ts +++ b/packages/server/src/schema/metadata/resolvers.ts @@ -120,8 +120,8 @@ export const resolvers: Resolvers = { entity.id, JSON_ARRAYAGG(subject_mapping.subject_id) AS subjectIds, type.name AS resourceType, - MIN(field_title.value) AS title, - MIN(field_description.value) AS description, + entity_revision.title AS title, + entity_revision.meta_description AS description, entity.date AS dateCreated, entity_revision.date AS dateModified, entity.current_revision_id AS currentRevisionId, @@ -138,12 +138,6 @@ export const resolvers: Resolvers = { JOIN type on entity.type_id = type.id JOIN license on license.id = entity.license_id JOIN entity_revision ON entity.current_revision_id = entity_revision.id - LEFT JOIN entity_revision_field field_title on - field_title.entity_revision_id = entity_revision.id AND - field_title.field = "title" - LEFT JOIN entity_revision_field field_description on - field_description.entity_revision_id = entity_revision.id AND - field_description.field = "meta_description" JOIN term_taxonomy_entity on term_taxonomy_entity.entity_id = entity.id JOIN term_taxonomy on term_taxonomy_entity.term_taxonomy_id = term_taxonomy.id JOIN term on term_taxonomy.term_id = term.id From e760e615fc67e902eb5fdd644a63eafa6b38beef Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Thu, 9 May 2024 22:04:13 +0200 Subject: [PATCH 18/41] test: Update tests for "title" --- __tests__/schema/uuid/abstract-uuid.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/__tests__/schema/uuid/abstract-uuid.ts b/__tests__/schema/uuid/abstract-uuid.ts index 216904c0a..68433f821 100644 --- a/__tests__/schema/uuid/abstract-uuid.ts +++ b/__tests__/schema/uuid/abstract-uuid.ts @@ -289,8 +289,9 @@ describe('property "title"', () => { }, articleRevision, ], - articleRevision.title, + 'Parabel', ], + /* [ 'article without current revision', [ @@ -303,6 +304,7 @@ describe('property "title"', () => { ], articleRevision.title, ], + */ [ 'article without revisions', [ @@ -315,8 +317,8 @@ describe('property "title"', () => { ], '123', ], - ['exercise', [exercise, taxonomyTermSubject], 'Mathe'], - ['exercise group', [exerciseGroup, taxonomyTermSubject], 'Mathe'], + ['exercise', [exercise, taxonomyTermSubject], 'Aufgaben zum Baumdiagramm'], + ['exercise group', [exerciseGroup, taxonomyTermSubject], 'Sachaufgaben'], ['user', [user], user.username], ['taxonomy term', [taxonomyTermRoot], 'Root'], ] as [string, Model<'AbstractUuid'>[], string][] From a0895eab656d087006abaf8ef61de5ada2ac1ee1 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Thu, 9 May 2024 22:39:50 +0200 Subject: [PATCH 19/41] checkout-revision: Fix code and tests --- __tests__/__utils__/query.ts | 17 ++ __tests__/schema/entity/checkout-revision.ts | 196 +++++------------- __tests__/schema/entity/reject-revision.ts | 167 +++++---------- __tests__/schema/uuid/user.ts | 35 +--- .../schema/uuid/abstract-entity/resolvers.ts | 2 +- 5 files changed, 128 insertions(+), 289 deletions(-) diff --git a/__tests__/__utils__/query.ts b/__tests__/__utils__/query.ts index 08c9e0320..990c286c7 100644 --- a/__tests__/__utils__/query.ts +++ b/__tests__/__utils__/query.ts @@ -64,6 +64,23 @@ export const entityRevisionQuery = new Client().prepareQuery({ variables: { id: 35296 }, }) +export const userQuery = new Client().prepareQuery({ + query: gql` + query ($id: Int!) { + uuid(id: $id) { + ... on User { + unrevisedEntities { + nodes { + id + } + } + } + } + } + `, + variables: { id: 299 }, +}) + export const taxonomyTermQuery = new Client().prepareQuery({ query: gql` query ($id: Int!) { diff --git a/__tests__/schema/entity/checkout-revision.ts b/__tests__/schema/entity/checkout-revision.ts index d7a1808f4..d786e3a65 100644 --- a/__tests__/schema/entity/checkout-revision.ts +++ b/__tests__/schema/entity/checkout-revision.ts @@ -1,166 +1,78 @@ import gql from 'graphql-tag' -import { HttpResponse } from 'msw' +import { user } from '../../../__fixtures__' import { - article as baseArticle, - articleRevision, - user as baseUser, - taxonomyTermSubject, - emptySubjects, -} from '../../../__fixtures__' -import { getTypenameAndId, nextUuid, given, Client } from '../../__utils__' -import { Instance } from '~/types' - -const user = { ...baseUser, roles: ['de_reviewer'] } -const article = { - ...baseArticle, - instance: Instance.De, - currentRevision: articleRevision.id, -} -const unrevisedRevision = { - ...articleRevision, - id: nextUuid(articleRevision.id), - trashed: true, -} -const mutation = new Client({ userId: user.id }) - .prepareQuery({ - query: gql` - mutation ($input: CheckoutRevisionInput!) { - entity { - checkoutRevision(input: $input) { - success - } + Client, + userQuery, + expectEvent, + entityQuery, + entityRevisionQuery, +} from '../../__utils__' +import { NotificationEventType } from '~/model/decoder' + +const input = { revisionId: 35290, reason: 'reason' } +const mutation = new Client({ userId: user.id }).prepareQuery({ + query: gql` + mutation ($input: CheckoutRevisionInput!) { + entity { + checkoutRevision(input: $input) { + success } } - `, - }) - .withInput({ - revisionId: unrevisedRevision.id, - reason: 'reason', - }) - -beforeEach(() => { - given('UuidQuery').for(user, article, articleRevision, unrevisedRevision) - given('UnrevisedEntitiesQuery').for([article]) - - given('EntityCheckoutRevisionMutation') - .withPayload({ - userId: user.id, - reason: 'reason', - revisionId: unrevisedRevision.id, - }) - .isDefinedBy(() => { - given('UuidQuery').for({ ...unrevisedRevision, trashed: false }) - given('UuidQuery').for({ - ...article, - currentRevisionId: unrevisedRevision.id, - }) - given('UnrevisedEntitiesQuery').for([]) - - return HttpResponse.json({ success: true }) - }) + } + `, + variables: { input }, }) -test('returns "{ success: true }" when mutation could be successfully executed', async () => { - await mutation.shouldReturnData({ - entity: { checkoutRevision: { success: true } }, +test('checks out a revision', async () => { + await entityQuery.withVariables({ id: 35247 }).shouldReturnData({ + uuid: { currentRevision: { id: 35248 } }, }) -}) -test('following queries for entity point to checkout revision when entity is already in the cache', async () => { - const articleQuery = new Client() - .prepareQuery({ - query: gql` - query ($id: Int!) { - uuid(id: $id) { - ... on Article { - currentRevision { - id - } - } - } - } - `, - }) - .withVariables({ id: article.id }) - - await articleQuery.shouldReturnData({ - uuid: { currentRevision: { id: articleRevision.id } }, + await userQuery.withVariables({ id: 26334 }).shouldReturnData({ + uuid: { + unrevisedEntities: { nodes: [{ id: 34907 }, { id: 35247 }] }, + }, }) await mutation.shouldReturnData({ entity: { checkoutRevision: { success: true } }, }) - await articleQuery.shouldReturnData({ - uuid: { currentRevision: { id: unrevisedRevision.id } }, + await entityQuery.withVariables({ id: 35247 }).shouldReturnData({ + uuid: { currentRevision: { id: 35290 } }, }) -}) - -test('checkout revision has trashed == false for following queries', async () => { - const revisionQuery = new Client() - .prepareQuery({ - query: gql` - query ($id: Int!) { - uuid(id: $id) { - ... on ArticleRevision { - trashed - } - } - } - `, - }) - .withVariables({ id: unrevisedRevision.id }) - await revisionQuery.shouldReturnData({ uuid: { trashed: true } }) - - await mutation.shouldReturnData({ - entity: { checkoutRevision: { success: true } }, + await userQuery.withVariables({ id: 26334 }).shouldReturnData({ + uuid: { unrevisedEntities: { nodes: [{ id: 34907 }] } }, }) - await revisionQuery.shouldReturnData({ uuid: { trashed: false } }) + await expectEvent({ + __typename: NotificationEventType.CheckoutRevision, + objectId: input.revisionId, + }) }) -test('after the checkout mutation the cache is cleared for unrevisedEntities', async () => { - given('UuidQuery').for(article) - - const unrevisedEntitiesQuery = new Client() - .prepareQuery({ - query: gql` - query ($instance: Instance!) { - subject { - subjects(instance: $instance) { - unrevisedEntities { - nodes { - __typename - id - } - } - } - } - } - `, +test('checkout revision has trashed == false for following queries', async () => { + await database.mutate('update uuid set trashed = 1 where id = ?', [ + input.revisionId, + ]) + + await entityRevisionQuery + .withVariables({ id: input.revisionId }) + .shouldReturnData({ + uuid: { trashed: true }, }) - .withVariables({ instance: taxonomyTermSubject.instance }) - - await unrevisedEntitiesQuery.shouldReturnData({ - subject: { - subjects: [ - { unrevisedEntities: { nodes: [getTypenameAndId(article)] } }, - ...emptySubjects, - ], - }, - }) await mutation.shouldReturnData({ entity: { checkoutRevision: { success: true } }, }) - await unrevisedEntitiesQuery.shouldReturnData({ - subject: { - subjects: [{ unrevisedEntities: { nodes: [] } }, ...emptySubjects], - }, - }) + await entityRevisionQuery + .withVariables({ id: input.revisionId }) + .shouldReturnData({ + uuid: { trashed: false }, + }) }) test('fails when user is not authenticated', async () => { @@ -170,15 +82,3 @@ test('fails when user is not authenticated', async () => { test('fails when user does not have role "reviewer"', async () => { await mutation.forLoginUser('de_moderator').shouldFailWithError('FORBIDDEN') }) - -test('fails when database layer returns a 400er response', async () => { - given('EntityCheckoutRevisionMutation').returnsBadRequest() - - await mutation.shouldFailWithError('BAD_USER_INPUT') -}) - -test('fails when database layer has an internal error', async () => { - given('EntityCheckoutRevisionMutation').hasInternalServerError() - - await mutation.shouldFailWithError('INTERNAL_SERVER_ERROR') -}) diff --git a/__tests__/schema/entity/reject-revision.ts b/__tests__/schema/entity/reject-revision.ts index 0971cde68..8f71df0f0 100644 --- a/__tests__/schema/entity/reject-revision.ts +++ b/__tests__/schema/entity/reject-revision.ts @@ -1,126 +1,85 @@ import gql from 'graphql-tag' -import { HttpResponse } from 'msw' import { - article as baseArticle, articleRevision, + user, taxonomyTermSubject, - user as baseUser, emptySubjects, } from '../../../__fixtures__' -import { given, getTypenameAndId, nextUuid, Client } from '../../__utils__' -import { Instance } from '~/types' - -const user = { ...baseUser, roles: ['de_reviewer'] } -const article = { - ...baseArticle, - instance: Instance.De, - currentRevision: articleRevision.id, -} -const currentRevision = { - ...articleRevision, - id: nextUuid(articleRevision.id), - trashed: false, -} - -const mutation = new Client({ userId: user.id }) - .prepareQuery({ - query: gql` - mutation ($input: RejectRevisionInput!) { - entity { - rejectRevision(input: $input) { - success - } +import { + getTypenameAndId, + given, + Client, + userQuery, + expectEvent, + entityQuery, + entityRevisionQuery, +} from '../../__utils__' +import { NotificationEventType } from '~/model/decoder' + +const input = { revisionId: 35290, reason: 'reason' } +const mutation = new Client({ userId: user.id }).prepareQuery({ + query: gql` + mutation ($input: CheckoutRevisionInput!) { + entity { + checkoutRevision(input: $input) { + success } } - `, - }) - .withInput({ revisionId: currentRevision.id, reason: 'reason' }) - -beforeEach(() => { - given('UuidQuery').for(user, article, articleRevision, currentRevision) - given('UnrevisedEntitiesQuery').for([article]) + } + `, + variables: { input }, +}) - given('EntityRejectRevisionMutation') - .withPayload({ - userId: user.id, - reason: 'reason', - revisionId: currentRevision.id, - }) - .isDefinedBy(() => { - given('UuidQuery').for({ ...currentRevision, trashed: true }) - given('UnrevisedEntitiesQuery').for([]) +test('checks out a revision', async () => { + await entityQuery.withVariables({ id: 35247 }).shouldReturnData({ + uuid: { currentRevision: { id: 35248 } }, + }) - return HttpResponse.json({ success: true }) - }) -}) + await userQuery.withVariables({ id: 26334 }).shouldReturnData({ + uuid: { + unrevisedEntities: { nodes: [{ id: 34907 }, { id: 35247 }] }, + }, + }) -test('returns "{ success: true }" when mutation could be successfully executed', async () => { await mutation.shouldReturnData({ - entity: { rejectRevision: { success: true } }, + entity: { checkoutRevision: { success: true } }, }) -}) -test('following queries for entity point to checkout revision when entity is already in the cache', async () => { - const revisionQuery = new Client() - .prepareQuery({ - query: gql` - query ($id: Int!) { - uuid(id: $id) { - trashed - } - } - `, - }) - .withVariables({ id: currentRevision.id }) - - await revisionQuery.shouldReturnData({ uuid: { trashed: false } }) + await entityQuery.withVariables({ id: 35247 }).shouldReturnData({ + uuid: { currentRevision: { id: 35290 } }, + }) - await mutation.shouldReturnData({ - entity: { rejectRevision: { success: true } }, + await userQuery.withVariables({ id: 26334 }).shouldReturnData({ + uuid: { unrevisedEntities: { nodes: [{ id: 34907 }] } }, }) - await revisionQuery.shouldReturnData({ uuid: { trashed: true } }) + await expectEvent({ + __typename: NotificationEventType.CheckoutRevision, + objectId: input.revisionId, + }) }) -test('after the reject mutation the cache is cleared for unrevisedEntities', async () => { - const unrevisedEntitiesQuery = new Client() - .prepareQuery({ - query: gql` - query ($instance: Instance!) { - subject { - subjects(instance: $instance) { - unrevisedEntities { - nodes { - __typename - id - } - } - } - } - } - `, - }) - .withVariables({ instance: taxonomyTermSubject.instance }) +test('checkout revision has trashed == false for following queries', async () => { + await database.mutate('update uuid set trashed = 1 where id = ?', [ + input.revisionId, + ]) - await unrevisedEntitiesQuery.shouldReturnData({ - subject: { - subjects: [ - { unrevisedEntities: { nodes: [getTypenameAndId(article)] } }, - ...emptySubjects, - ], - }, - }) + await entityRevisionQuery + .withVariables({ id: input.revisionId }) + .shouldReturnData({ + uuid: { trashed: true }, + }) await mutation.shouldReturnData({ - entity: { rejectRevision: { success: true } }, + entity: { checkoutRevision: { success: true } }, }) - await unrevisedEntitiesQuery.shouldReturnData({ - subject: { - subjects: [{ unrevisedEntities: { nodes: [] } }, ...emptySubjects], - }, - }) + await entityRevisionQuery + .withVariables({ id: input.revisionId }) + .shouldReturnData({ + uuid: { trashed: false }, + }) }) test('fails when user is not authenticated', async () => { @@ -130,15 +89,3 @@ test('fails when user is not authenticated', async () => { test('fails when user does not have role "reviewer"', async () => { await mutation.forLoginUser('de_moderator').shouldFailWithError('FORBIDDEN') }) - -test('fails when database layer returns a 400er response', async () => { - given('EntityRejectRevisionMutation').returnsBadRequest() - - await mutation.shouldFailWithError('BAD_USER_INPUT') -}) - -test('fails when database layer has an internal error', async () => { - given('EntityRejectRevisionMutation').hasInternalServerError() - - await mutation.shouldFailWithError('INTERNAL_SERVER_ERROR') -}) diff --git a/__tests__/schema/uuid/user.ts b/__tests__/schema/uuid/user.ts index 12dbe2bba..84e17999c 100644 --- a/__tests__/schema/uuid/user.ts +++ b/__tests__/schema/uuid/user.ts @@ -2,13 +2,7 @@ import { Scope } from '@serlo/authorization' import gql from 'graphql-tag' import * as R from 'ramda' -import { - article, - user, - user2, - articleRevision, - activityByType, -} from '../../../__fixtures__' +import { article, user, user2, activityByType } from '../../../__fixtures__' import { assertErrorEvent, assertNoErrorEvents, @@ -17,9 +11,9 @@ import { givenSpreadheetApi, givenSpreadsheet, hasInternalServerError, - nextUuid, given, Client, + userQuery, } from '../../__utils__' import { Model } from '~/internals/graphql' import { MajorDimension } from '~/model' @@ -469,28 +463,9 @@ describe('User', () => { }) test('property unrevisedEntities', async () => { - await client - .prepareQuery({ - query: gql` - query user($id: Int!) { - uuid(id: $id) { - ... on User { - unrevisedEntities { - nodes { - id - } - } - } - } - } - `, - variables: { id: 299 }, - }) - .shouldReturnData({ - uuid: { - unrevisedEntities: { nodes: [{ id: 26892 }] }, - }, - }) + await userQuery.shouldReturnData({ + uuid: { unrevisedEntities: { nodes: [{ id: 26892 }] } }, + }) }) describe('property lastLogin', () => { diff --git a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts index 89ab368e4..93b30f692 100644 --- a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts +++ b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts @@ -185,7 +185,7 @@ export const resolvers: Resolvers = { try { await database.mutate( - `update entity set current_revision = ? where id = ?`, + `update entity set current_revision_id = ? where id = ?`, [revision.id, entity.id], ) From 9ced2c70b17fdbe3f503202ab03452f78a3ab70e Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Thu, 9 May 2024 22:50:23 +0200 Subject: [PATCH 20/41] Fix and update reject-revision.ts --- __tests__/schema/entity/reject-revision.ts | 49 ++++++------------- .../schema/uuid/abstract-entity/resolvers.ts | 13 +---- 2 files changed, 16 insertions(+), 46 deletions(-) diff --git a/__tests__/schema/entity/reject-revision.ts b/__tests__/schema/entity/reject-revision.ts index 8f71df0f0..d0405c72c 100644 --- a/__tests__/schema/entity/reject-revision.ts +++ b/__tests__/schema/entity/reject-revision.ts @@ -1,14 +1,7 @@ import gql from 'graphql-tag' +import { user } from '../../../__fixtures__' import { - articleRevision, - user, - taxonomyTermSubject, - emptySubjects, -} from '../../../__fixtures__' -import { - getTypenameAndId, - given, Client, userQuery, expectEvent, @@ -20,9 +13,9 @@ import { NotificationEventType } from '~/model/decoder' const input = { revisionId: 35290, reason: 'reason' } const mutation = new Client({ userId: user.id }).prepareQuery({ query: gql` - mutation ($input: CheckoutRevisionInput!) { + mutation ($input: RejectRevisionInput!) { entity { - checkoutRevision(input: $input) { + rejectRevision(input: $input) { success } } @@ -42,46 +35,32 @@ test('checks out a revision', async () => { }, }) + await entityRevisionQuery + .withVariables({ id: input.revisionId }) + .shouldReturnData({ uuid: { trashed: false } }) + await mutation.shouldReturnData({ - entity: { checkoutRevision: { success: true } }, + entity: { rejectRevision: { success: true } }, }) await entityQuery.withVariables({ id: 35247 }).shouldReturnData({ - uuid: { currentRevision: { id: 35290 } }, + uuid: { currentRevision: { id: 35248 } }, }) + await entityRevisionQuery + .withVariables({ id: input.revisionId }) + .shouldReturnData({ uuid: { trashed: true } }) + await userQuery.withVariables({ id: 26334 }).shouldReturnData({ uuid: { unrevisedEntities: { nodes: [{ id: 34907 }] } }, }) await expectEvent({ - __typename: NotificationEventType.CheckoutRevision, + __typename: NotificationEventType.RejectRevision, objectId: input.revisionId, }) }) -test('checkout revision has trashed == false for following queries', async () => { - await database.mutate('update uuid set trashed = 1 where id = ?', [ - input.revisionId, - ]) - - await entityRevisionQuery - .withVariables({ id: input.revisionId }) - .shouldReturnData({ - uuid: { trashed: true }, - }) - - await mutation.shouldReturnData({ - entity: { checkoutRevision: { success: true } }, - }) - - await entityRevisionQuery - .withVariables({ id: input.revisionId }) - .shouldReturnData({ - uuid: { trashed: false }, - }) -}) - test('fails when user is not authenticated', async () => { await mutation.forUnauthenticatedUser().shouldFailWithError('UNAUTHENTICATED') }) diff --git a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts index 93b30f692..2cf48a859 100644 --- a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts +++ b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts @@ -210,8 +210,6 @@ export const resolvers: Resolvers = { await UuidResolver.removeCacheEntry({ id: entity.id }, context) await UuidResolver.removeCacheEntry({ id: revision.id }, context) - // TODO: UnrevisedRevisions - return { success: true, query: {} } } finally { await transaction.rollback() @@ -244,18 +242,13 @@ export const resolvers: Resolvers = { const transaction = await database.beginTransaction() try { - await database.mutate( - `update entity set current_revision = ? where id = ?`, - [revision.id, entity.id], - ) - - await database.mutate(`update uuid set trashed = 0 where id = ?`, [ + await database.mutate(`update uuid set trashed = 1 where id = ?`, [ revision.id, ]) await createEvent( { - __typename: NotificationEventType.CheckoutRevision, + __typename: NotificationEventType.RejectRevision, actorId: userId, instance: entity.instance, repositoryId: entity.id, @@ -270,8 +263,6 @@ export const resolvers: Resolvers = { await UuidResolver.removeCacheEntry({ id: entity.id }, context) await UuidResolver.removeCacheEntry({ id: revision.id }, context) - // TODO: UnrevisedRevisions - return { success: true, query: {} } } finally { await transaction.rollback() From 2a591e94d93069040e87f24d502cfe8f2d571f57 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Sat, 11 May 2024 01:53:14 +0200 Subject: [PATCH 21/41] WIP: Migrate creating entities / revisions --- .../schema/entity/set-abstract-entity.ts | 172 +++++++++++++ .../server/src/internals/graphql/utils.ts | 2 +- packages/server/src/model/database-layer.ts | 45 ---- packages/server/src/model/serlo.ts | 74 ------ .../abstract-entity/entity-set-handler.ts | 235 ------------------ .../schema/uuid/abstract-entity/resolvers.ts | 227 ++++++++++++++++- .../schema/uuid/abstract-entity/types.graphql | 2 + .../schema/uuid/taxonomy-term/resolvers.ts | 43 ++-- packages/server/src/types.ts | 4 + 9 files changed, 429 insertions(+), 375 deletions(-) create mode 100644 __tests__/schema/entity/set-abstract-entity.ts delete mode 100644 packages/server/src/schema/uuid/abstract-entity/entity-set-handler.ts diff --git a/__tests__/schema/entity/set-abstract-entity.ts b/__tests__/schema/entity/set-abstract-entity.ts new file mode 100644 index 000000000..5fd06f04a --- /dev/null +++ b/__tests__/schema/entity/set-abstract-entity.ts @@ -0,0 +1,172 @@ +import gql from 'graphql-tag' + +import { user } from '../../../__fixtures__' +import { + Client, + entityQuery, + entityRevisionQuery, + expectEvent, +} from '../../__utils__' +import { NodeClient } from '@sentry/node' +import { NotificationEventType } from '~/model/decoder' + +const input = { + entityType: 'Article', + changes: 'my change', + subscribeThis: true, + subscribeThisByEmail: true, + needsReview: true, + parentId: 5, + entityId: null, + content: JSON.stringify({ plugin: 'rows', state: [] }), + metaDescription: 'metaDescription', + metaTitle: 'metaTitle', + title: 'a title', + url: 'url', +} + +const mutation = new Client({ userId: user.id }).prepareQuery({ + query: gql` + mutation ($input: SetAbstractEntityInput!) { + entity { + setAbstractEntity(input: $input) { + entity { + id + } + revision { + id + } + success + } + } + } + `, + variables: { input }, +}) + +// test autoreview1 + +test('creates a new entity when "parentId" is set', async () => { + const data = (await mutation.getData()) as { + entity: { + setAbstractEntity: { entity: { id: number }; revision: { id: number } } + } + } + + expect(data).toMatchObject({ + entity: { setAbstractEntity: { success: true } }, + }) + + const entityId = data.entity.setAbstractEntity.entity.id + const revisionId = data.entity.setAbstractEntity.revision.id + + await entityQuery.withVariables({ id: entityId }).shouldReturnData({ + uuid: { + __typename: 'Article', + instance: 'de', + licenseId: 1, + taxonomyTerms: { nodes: [{ id: input.parentId }] }, + }, + }) + + await entityRevisionQuery.withVariables({ id: revisionId }).shouldReturnData({ + uuid: { + content: input.content, + url: input.url, + metaDescription: input.metaDescription, + title: input.title, + metaTitle: input.metaTitle, + changes: input.changes, + }, + }) + + await expectEvent( + { + __typename: NotificationEventType.CreateEntity, + objectId: entityId, + }, + 3, + ) +}) + +test('creates a new revision when "entityId" is set', async () => { + const data = (await mutation + .changeInput({ parentId: null, entityId: 1855 }) + .getData()) as { + entity: { + setAbstractEntity: { entity: { id: number }; revision: { id: number } } + } + } + + expect(data).toMatchObject({ + entity: { setAbstractEntity: { success: true } }, + }) + + const entityId = data.entity.setAbstractEntity.entity.id + const revisionId = data.entity.setAbstractEntity.revision.id + + await entityQuery.withVariables({ id: entityId }).shouldReturnData({ + uuid: { + __typename: 'Article', + id: 1855, + }, + }) + + await entityRevisionQuery.withVariables({ id: revisionId }).shouldReturnData({ + uuid: { + content: input.content, + url: input.url, + metaDescription: input.metaDescription, + title: input.title, + metaTitle: input.metaTitle, + changes: input.changes, + }, + }) + + await expectEvent({ + __typename: NotificationEventType.CreateEntityRevision, + objectId: revisionId, + }) +}) + +test('fails when both "entityId" and "parentId" are defined', async () => { + await mutation + .changeInput({ entityId: null, parentId: null }) + .shouldFailWithError('BAD_USER_INPUT') +}) + +test('fails when both "entityId" and "parentId" is null', async () => { + await mutation + .changeInput({ entityId: null, parentId: null }) + .shouldFailWithError('BAD_USER_INPUT') +}) + +describe('fails when a mandatory field is missing', () => { + test('case "url" for an applet', async () => { + await mutation + .changeInput({ entityType: 'Applet', url: '' }) + .shouldFailWithError('BAD_USER_INPUT') + }) + + test('case "title" for an article', async () => { + await mutation + .changeInput({ entityType: 'Article', title: null }) + .shouldFailWithError('BAD_USER_INPUT') + }) +}) + +test('fails when entityType is not valid', async () => { + await mutation + .changeInput({ entityType: 'foo' }) + .shouldFailWithError('BAD_USER_INPUT') +}) + +test('fails when changes is empty', async () => { + await mutation + .changeInput({ change: '' }) + .shouldFailWithError('BAD_USER_INPUT') +}) + +test('fails when user is not authenticated', async () => { + await mutation.forUnauthenticatedUser().shouldFailWithError('UNAUTHENTICATED') +}) diff --git a/packages/server/src/internals/graphql/utils.ts b/packages/server/src/internals/graphql/utils.ts index 0596024c1..ea2973779 100644 --- a/packages/server/src/internals/graphql/utils.ts +++ b/packages/server/src/internals/graphql/utils.ts @@ -80,7 +80,7 @@ export function decodeFromBase64(text: string) { export function assertStringIsNotEmpty(args: { [key: string]: unknown }) { const emptyArgs: string[] = Object.entries(args) .filter( - ([_, value]) => typeof value === 'string' && value.trim().length === 0, + ([_, value]) => typeof value !== 'string' || value.trim().length === 0, ) .map(([key]) => key) diff --git a/packages/server/src/model/database-layer.ts b/packages/server/src/model/database-layer.ts index 62d240c6c..9ba922bc1 100644 --- a/packages/server/src/model/database-layer.ts +++ b/packages/server/src/model/database-layer.ts @@ -3,9 +3,6 @@ import * as t from 'io-ts' import { CommentDecoder, - EntityDecoder, - EntityRevisionTypeDecoder, - EntityTypeDecoder, InstanceDecoder, NotificationEventDecoder, PageDecoder, @@ -55,48 +52,6 @@ export const spec = { }), canBeNull: false, }, - EntityAddRevisionMutation: { - payload: t.type({ - userId: t.number, - revisionType: EntityRevisionTypeDecoder, - input: t.type({ - changes: t.string, - entityId: t.number, - needsReview: t.boolean, - subscribeThis: t.boolean, - subscribeThisByEmail: t.boolean, - fields: t.record(t.string, t.union([t.string, t.undefined])), - }), - }), - response: t.type({ - success: t.literal(true), - revisionId: t.number, - }), - canBeNull: false, - }, - EntityCreateMutation: { - payload: t.type({ - userId: t.number, - entityType: EntityTypeDecoder, - input: t.intersection([ - t.type({ - changes: t.string, - licenseId: t.number, - needsReview: t.boolean, - subscribeThis: t.boolean, - subscribeThisByEmail: t.boolean, - fields: t.record(t.string, t.string), - }), - // TODO: prefer union - t.partial({ - parentId: t.number, - taxonomyTermId: t.number, - }), - ]), - }), - response: EntityDecoder, - canBeNull: false, - }, EntitySortMutation: { payload: t.type({ childrenIds: t.array(t.number), entityId: t.number }), response: t.type({ success: t.boolean }), diff --git a/packages/server/src/model/serlo.ts b/packages/server/src/model/serlo.ts index de7d756c3..74017b140 100644 --- a/packages/server/src/model/serlo.ts +++ b/packages/server/src/model/serlo.ts @@ -304,78 +304,6 @@ export function createSerloModel({ }, }) - const createEntity = createMutation({ - type: 'EntityCreateMutation', - decoder: DatabaseLayer.getDecoderFor('EntityCreateMutation'), - mutate: (payload: DatabaseLayer.Payload<'EntityCreateMutation'>) => { - return DatabaseLayer.makeRequest('EntityCreateMutation', payload) - }, - async updateCache({ userId, input }, newEntity) { - if (newEntity) { - const { parentId, taxonomyTermId } = input - if (parentId) { - await UuidResolver.removeCacheEntry({ id: parentId }, context) - } - if (taxonomyTermId) { - await UuidResolver.removeCacheEntry({ id: taxonomyTermId }, context) - } - - if (input.subscribeThis) { - await getSubscriptions._querySpec.setCache({ - payload: { userId }, - getValue(current) { - if (!current) return - - const newEntry = { - objectId: newEntity.id, - sendEmail: input.subscribeThisByEmail, - } - - return { subscriptions: [...current.subscriptions, newEntry] } - }, - }) - } - } - }, - }) - - const addEntityRevision = createMutation({ - type: 'EntityAddRevisionMutation', - decoder: DatabaseLayer.getDecoderFor('EntityAddRevisionMutation'), - mutate: (payload: DatabaseLayer.Payload<'EntityAddRevisionMutation'>) => { - return DatabaseLayer.makeRequest('EntityAddRevisionMutation', payload) - }, - updateCache: async ({ input, userId }, { success }) => { - if (success) { - await UuidResolver.removeCacheEntry({ id: input.entityId }, context) - - if (input.subscribeThis) { - await getSubscriptions._querySpec.setCache({ - payload: { userId }, - getValue(current) { - if (!current) return - - const currentWithoutNew = current.subscriptions.filter( - ({ objectId }) => input.entityId !== objectId, - ) - - const newEntry = { - objectId: input.entityId, - sendEmail: input.subscribeThisByEmail, - } - - return { - subscriptions: [...currentWithoutNew, newEntry].sort( - (a, b) => a.objectId - b.objectId, - ), - } - }, - }) - } - } - }, - }) - const createPage = createMutation({ type: 'PageCreateMutation', decoder: DatabaseLayer.getDecoderFor('PageCreateMutation'), @@ -477,13 +405,11 @@ export function createSerloModel({ }) return { - addEntityRevision, addPageRevision, addRole, archiveThread, checkoutPageRevision, createComment, - createEntity, createPage, createThread, deleteBots, diff --git a/packages/server/src/schema/uuid/abstract-entity/entity-set-handler.ts b/packages/server/src/schema/uuid/abstract-entity/entity-set-handler.ts deleted file mode 100644 index 50ac83340..000000000 --- a/packages/server/src/schema/uuid/abstract-entity/entity-set-handler.ts +++ /dev/null @@ -1,235 +0,0 @@ -import * as serloAuth from '@serlo/authorization' -import * as t from 'io-ts' -import * as R from 'ramda' - -import { fromEntityTypeToEntityRevisionType } from './utils' -import { UuidResolver } from '../abstract-uuid/resolvers' -import { autoreviewTaxonomyIds, defaultLicenseIds } from '~/config' -import { Context } from '~/context' -import { UserInputError } from '~/errors' -import { - Model, - assertStringIsNotEmpty, - assertUserIsAuthenticated, - assertUserIsAuthorized, -} from '~/internals/graphql' -import { - EntityDecoder, - EntityType, - EntityTypeDecoder, - TaxonomyTermDecoder, -} from '~/model/decoder' -import { fetchScopeOfUuid } from '~/schema/authorization/utils' - -export interface SetAbstractEntityInput { - entityType: string - changes: string - subscribeThis: boolean - subscribeThisByEmail: boolean - needsReview: boolean - entityId?: number - parentId?: number - content?: string - description?: string - metaDescription?: string - metaTitle?: string - title?: string - url?: string -} - -type InputFields = keyof SetAbstractEntityInput - -const mandatoryFieldsLookup: Record = { - [EntityType.Applet]: ['content', 'title', 'url'], - [EntityType.Article]: ['content', 'title'], - [EntityType.Course]: ['title'], - [EntityType.CoursePage]: ['content', 'title'], - [EntityType.Event]: ['content', 'title'], - [EntityType.Exercise]: ['content'], - [EntityType.ExerciseGroup]: ['content'], - [EntityType.Video]: ['title', 'url'], -} - -export function createSetEntityResolver() { - return async ( - _parent: unknown, - { input }: { input: SetAbstractEntityInput }, - context: Context, - ): Promise> => { - const { dataSources, userId } = context - const { - entityType, - changes, - needsReview, - subscribeThis, - subscribeThisByEmail, - } = input - assertStringIsNotEmpty({ - changes, - entityType, - }) - - if (!EntityTypeDecoder.is(entityType)) { - throw new UserInputError( - `The provided entityType (${entityType}) is not supported`, - ) - } - - assertStringIsNotEmpty({ - ...mandatoryFieldsLookup[entityType], - }) - - // TODO: the logic of this and others special cases should go to DB Layer - if (entityType === EntityType.Course) { - input = { - ...input, - description: input.content, - content: undefined, - } - } - if (entityType === EntityType.Video) { - input = { - ...input, - description: input.content, - content: input.url, - url: undefined, - } - } - - const forwardArgs = { changes, subscribeThis, subscribeThisByEmail } - - const fieldKeys = [ - 'content', - 'description', - 'metaDescription', - 'metaTitle', - 'title', - 'url', - ] as const - const fields = R.mapObjIndexed( - (val: string | boolean) => - typeof val !== 'string' ? val.toString() : val, - R.filter((val) => val != null, R.pick(fieldKeys, input)), - ) - - if (!checkInput(input)) - throw new UserInputError('Either entityId or parentId must be provided') - - assertUserIsAuthenticated(userId) - - const scope = await fetchScopeOfUuid( - { id: input.entityId != null ? input.entityId : input.parentId }, - context, - ) - - await assertUserIsAuthorized({ - context, - message: `You are not allowed to create ${ - input.entityId == null ? 'entities' : 'revisions' - }`, - guard: serloAuth.Uuid.create( - input.entityId == null ? 'Entity' : 'EntityRevision', - )(scope), - }) - - const isAutoreview = await isAutoreviewEntity( - input.entityId != null ? input.entityId : input.parentId, - context, - ) - - if (!isAutoreview && !needsReview) { - await assertUserIsAuthorized({ - context, - message: 'You are not allowed to skip the reviewing process.', - guard: serloAuth.Entity.checkoutRevision(scope), - }) - } - - const needsReviewForDBLayer = isAutoreview ? false : needsReview - - if (input.entityId != null) { - const { success } = await dataSources.model.serlo.addEntityRevision({ - revisionType: fromEntityTypeToEntityRevisionType(entityType), - userId, - input: { - ...forwardArgs, - entityId: input.entityId, - needsReview: needsReviewForDBLayer, - fields, - }, - }) - - return { - record: success - ? await UuidResolver.resolveWithDecoder( - EntityDecoder, - { id: input.entityId }, - context, - ) - : null, - success, - query: {}, - } - } else { - const parent = await UuidResolver.resolve({ id: input.parentId }, context) - const isParentTaxonomyTerm = TaxonomyTermDecoder.is(parent) - const isParentEntity = EntityDecoder.is(parent) - - if (!isParentTaxonomyTerm && !isParentEntity) - throw new UserInputError( - `No entity or taxonomy term found for the provided id ${input.parentId}`, - ) - - const entity = await dataSources.model.serlo.createEntity({ - entityType, - userId, - input: { - ...forwardArgs, - licenseId: defaultLicenseIds[parent.instance], - needsReview: needsReviewForDBLayer, - ...(isParentTaxonomyTerm ? { taxonomyTermId: input.parentId } : {}), - ...(isParentEntity ? { parentId: input.parentId } : {}), - fields, - }, - }) - - return { record: entity, success: entity != null, query: {} } - } - } -} - -async function isAutoreviewEntity( - id: number, - context: Context, -): Promise { - if (autoreviewTaxonomyIds.includes(id)) return true - - const uuid = await UuidResolver.resolve({ id }, context) - - if (t.type({ parentId: t.number }).is(uuid)) { - return ( - uuid.parentId != null && - (await isAutoreviewEntity(uuid.parentId, context)) - ) - } else if (t.type({ taxonomyTermIds: t.array(t.number) }).is(uuid)) { - return ( - await Promise.all( - uuid.taxonomyTermIds.map((id) => isAutoreviewEntity(id, context)), - ) - ).every((x) => x) - } else { - return false - } -} - -function checkInput(input: { - parentId?: number - entityId?: number -}): input is - | { parentId: number; entityId: undefined } - | { parentId: undefined; entityId: number } { - return ( - (input.entityId != null && input.parentId == null) || - (input.entityId == null && input.parentId != null) - ) -} diff --git a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts index 2cf48a859..066d47da3 100644 --- a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts +++ b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts @@ -1,12 +1,16 @@ import * as serloAuth from '@serlo/authorization' import { instanceToScope } from '@serlo/authorization' import * as t from 'io-ts' +import * as R from 'ramda' -import { createSetEntityResolver } from './entity-set-handler' import { UuidResolver } from '../abstract-uuid/resolvers' +import { createTaxonomyTermLink } from '../taxonomy-term/resolvers' +import { autoreviewTaxonomyIds, defaultLicenseIds } from '~/config' import { Context } from '~/context' -import { UserInputError } from '~/errors' +import { InternalServerError, UserInputError } from '~/errors' import { + Model, + assertStringIsNotEmpty, assertUserIsAuthenticated, assertUserIsAuthorized, createNamespace, @@ -15,13 +19,29 @@ import { CourseDecoder, EntityDecoder, EntityRevisionDecoder, + EntityType, + EntityTypeDecoder, NotificationEventType, + TaxonomyTermDecoder, } from '~/model/decoder' import { resolveConnection } from '~/schema/connection/utils' import { createEvent } from '~/schema/events/event' -import { Resolvers } from '~/types' +import { Resolvers, SetAbstractEntityInput } from '~/types' import { isDateString } from '~/utils' +type InputFields = keyof SetAbstractEntityInput + +const mandatoryFieldsLookup: Record = { + [EntityType.Applet]: ['content', 'title', 'url'], + [EntityType.Article]: ['content', 'title'], + [EntityType.Course]: ['title'], + [EntityType.CoursePage]: ['content', 'title'], + [EntityType.Event]: ['content', 'title'], + [EntityType.Exercise]: ['content'], + [EntityType.ExerciseGroup]: ['content'], + [EntityType.Video]: ['title', 'url'], +} as const + export const resolvers: Resolvers = { Query: { entity: createNamespace(), @@ -80,7 +100,171 @@ export const resolvers: Resolvers = { }, }, EntityMutation: { - setAbstractEntity: createSetEntityResolver(), + async setAbstractEntity(_parent, { input }, context) { + const { database, userId } = context + + assertUserIsAuthenticated(userId) + + const { entityType, changes, entityId, parentId } = input + + assertStringIsNotEmpty({ changes }) + + if (!EntityTypeDecoder.is(entityType)) { + throw new UserInputError('entityType must be a valid entity type') + } + + assertStringIsNotEmpty(R.pick(mandatoryFieldsLookup[entityType], input)) + + if ( + (entityId == null && parentId == null) || + (entityId != null && parentId != null) + ) { + throw new UserInputError( + 'Exactely one of entityId and parentId must be defined', + ) + } + + let entity: Model<'AbstractEntity'> + const transaction = await database.beginTransaction() + + try { + if (parentId != null) { + const parent = await UuidResolver.resolveWithDecoder( + TaxonomyTermDecoder, + { id: parentId }, + context, + ) + + await assertUserIsAuthorized({ + context, + message: 'You are not allowed to create entities', + guard: serloAuth.Uuid.create('Entity')( + instanceToScope(parent.instance), + ), + }) + + const { insertId: newEntityId } = await database.mutate( + 'insert into uuid (trashed, discriminator) values (0, "entity")', + ) + + await database.mutate( + ` + insert into entity (id, type_id, instance_id, license_id) + select ?, type.id, instance.id, ? + from type, instance + where type.name = ? and instance.subdomain = ?`, + [ + newEntityId, + defaultLicenseIds[parent.instance], + toDatabaseType(entityType), + parent.instance, + ], + ) + + await createTaxonomyTermLink( + { entityId: newEntityId, taxonomyTermId: parent.id }, + context, + ) + + await createEvent( + { + __typename: NotificationEventType.CreateEntity, + actorId: userId, + instance: parent.instance, + entityId: newEntityId, + }, + context, + ) + + entity = await UuidResolver.resolveWithDecoder( + EntityDecoder, + { id: newEntityId }, + context, + ) + + await UuidResolver.removeCacheEntry({ id: parent.id }, context) + } else { + // This should not happen due to the check above + if (entityId == null) throw new InternalServerError() + + entity = await UuidResolver.resolveWithDecoder( + EntityDecoder, + { id: entityId }, + context, + ) + } + + await assertUserIsAuthorized({ + context, + message: 'You are not allowed to create entities', + guard: serloAuth.Uuid.create('EntityRevision')( + instanceToScope(entity.instance), + ), + }) + + const { insertId: revisionId } = await database.mutate( + 'insert into uuid (trashed, discriminator) values (0, "entityRevision")', + ) + + await database.mutate( + ` + insert into entity_revision + (id, author_id, repository_id, content, meta_title, meta_description, + title, url, changes) + values (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + revisionId, + userId, + entity.id, + input.content ?? null, + input.metaTitle ?? null, + input.metaDescription ?? null, + input.title ?? null, + input.url ?? null, + input.changes ?? null, + ], + ) + + await createEvent( + { + __typename: NotificationEventType.CreateEntityRevision, + actorId: userId, + instance: entity.instance, + entityId: entity.id, + entityRevisionId: revisionId, + }, + context, + ) + + await transaction.commit() + + await UuidResolver.removeCacheEntry({ id: entity.id }, context) + + const finalEntity = await UuidResolver.resolveWithDecoder( + EntityDecoder, + { id: entity.id }, + context, + ) + + const revision = await UuidResolver.resolveWithDecoder( + EntityRevisionDecoder, + { id: revisionId }, + context, + ) + + // TODO: Delete subscriptions for user + + return { + success: true, + record: finalEntity, + entity: finalEntity, + revision, + query: {}, + } + } finally { + await transaction.rollback() + } + }, async sort(_parent, { input }, context) { const { dataSources, userId } = context @@ -303,6 +487,17 @@ export async function resolveUnrevisedEntityIds( return rows.map((row) => row.id) } +function toDatabaseType(entityType: EntityType) { + switch (entityType) { + case EntityType.CoursePage: + return 'course-page' + case EntityType.ExerciseGroup: + return 'exercise-group' + default: + return entityType.toLowerCase() + } +} + function decodeDateOfDeletion(after: string) { const afterParsed = JSON.parse( Buffer.from(after, 'base64').toString(), @@ -324,3 +519,27 @@ function decodeDateOfDeletion(after: string) { return new Date(dateOfDeletion).toISOString() } + +async function isAutoreviewEntity( + id: number, + context: Context, +): Promise { + if (autoreviewTaxonomyIds.includes(id)) return true + + const uuid = await UuidResolver.resolve({ id }, context) + + if (t.type({ parentId: t.number }).is(uuid)) { + return ( + uuid.parentId != null && + (await isAutoreviewEntity(uuid.parentId, context)) + ) + } else if (t.type({ taxonomyTermIds: t.array(t.number) }).is(uuid)) { + return ( + await Promise.all( + uuid.taxonomyTermIds.map((id) => isAutoreviewEntity(id, context)), + ) + ).every((x) => x) + } else { + return false + } +} diff --git a/packages/server/src/schema/uuid/abstract-entity/types.graphql b/packages/server/src/schema/uuid/abstract-entity/types.graphql index babe09a16..457f6490c 100644 --- a/packages/server/src/schema/uuid/abstract-entity/types.graphql +++ b/packages/server/src/schema/uuid/abstract-entity/types.graphql @@ -105,6 +105,8 @@ input SetAbstractEntityInput { type SetEntityResponse { record: AbstractEntity + entity: AbstractEntity + revision: AbstractEntityRevision success: Boolean! query: Query! } diff --git a/packages/server/src/schema/uuid/taxonomy-term/resolvers.ts b/packages/server/src/schema/uuid/taxonomy-term/resolvers.ts index 756a38d52..c663dd16a 100644 --- a/packages/server/src/schema/uuid/taxonomy-term/resolvers.ts +++ b/packages/server/src/schema/uuid/taxonomy-term/resolvers.ts @@ -23,6 +23,7 @@ import { createThreadResolvers } from '~/schema/thread/utils' import { createUuidResolvers } from '~/schema/uuid/abstract-uuid/utils' import { TaxonomyTermType, TaxonomyTypeCreateOptions, Resolvers } from '~/types' import { isDefined } from '~/utils' +import { Context } from '@sentry/node/types/integrations' const typesMap = { root: TaxonomyTermType.Root, @@ -268,22 +269,9 @@ export const resolvers: Resolvers = { continue } - const { lastPosition } = await database.fetchOne<{ - lastPosition: number - }>( - ` - SELECT IFNULL(MAX(position), 0) as lastPosition - FROM term_taxonomy_entity - WHERE term_taxonomy_id = ?`, - [taxonomyTermId], - ) - - await database.mutate( - ` - insert into term_taxonomy_entity (entity_id, term_taxonomy_id, position) - values (?,?,?) - `, - [entity.id, taxonomyTermId, lastPosition + 1], + await createTaxonomyTermLink( + { entityId: entity.id, taxonomyTermId }, + context, ) } @@ -496,6 +484,29 @@ export const resolvers: Resolvers = { }, } +export async function createTaxonomyTermLink( + { entityId, taxonomyTermId }: { entityId: number; taxonomyTermId: number }, + { database }: Pick, +) { + const { lastPosition } = await database.fetchOne<{ + lastPosition: number + }>( + ` + SELECT IFNULL(MAX(position), 0) as lastPosition + FROM term_taxonomy_entity + WHERE term_taxonomy_id = ?`, + [taxonomyTermId], + ) + + await database.mutate( + ` + insert into term_taxonomy_entity (entity_id, term_taxonomy_id, position) + values (?,?,?) + `, + [entityId, taxonomyTermId, lastPosition + 1], + ) +} + async function getParentTerms( taxonomyTerm: Model<'TaxonomyTerm'>, parentTerms: Model<'TaxonomyTerm'>[], diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index a9af71b47..3eff8b495 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -1267,8 +1267,10 @@ export type SetAbstractEntityInput = { export type SetEntityResponse = { __typename?: 'SetEntityResponse'; + entity?: Maybe; query: Query; record?: Maybe; + revision?: Maybe; success: Scalars['Boolean']['output']; }; @@ -3023,8 +3025,10 @@ export type ScopedRoleConnectionResolvers = { + entity?: Resolver, ParentType, ContextType>; query?: Resolver; record?: Resolver, ParentType, ContextType>; + revision?: Resolver, ParentType, ContextType>; success?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; From bbff9780035ed5c4b79b1b65e5f6369dfa6c9ffc Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Sat, 11 May 2024 02:01:22 +0200 Subject: [PATCH 22/41] Fix lint errors --- __tests__/schema/entity/set-abstract-entity.ts | 1 - packages/server/src/schema/subject/resolvers.ts | 2 +- packages/server/src/schema/uuid/taxonomy-term/resolvers.ts | 1 - packages/server/src/schema/uuid/user/resolvers.ts | 4 ++-- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/__tests__/schema/entity/set-abstract-entity.ts b/__tests__/schema/entity/set-abstract-entity.ts index 5fd06f04a..0f03f31ec 100644 --- a/__tests__/schema/entity/set-abstract-entity.ts +++ b/__tests__/schema/entity/set-abstract-entity.ts @@ -7,7 +7,6 @@ import { entityRevisionQuery, expectEvent, } from '../../__utils__' -import { NodeClient } from '@sentry/node' import { NotificationEventType } from '~/model/decoder' const input = { diff --git a/packages/server/src/schema/subject/resolvers.ts b/packages/server/src/schema/subject/resolvers.ts index 52638587c..a34de9778 100644 --- a/packages/server/src/schema/subject/resolvers.ts +++ b/packages/server/src/schema/subject/resolvers.ts @@ -2,6 +2,7 @@ import { option as O } from 'fp-ts' import * as t from 'io-ts' import { resolveConnection } from '../connection/utils' +import { resolveUnrevisedEntityIds } from '../uuid/abstract-entity/resolvers' import { UuidResolver } from '../uuid/abstract-uuid/resolvers' import { createCachedResolver } from '~/cached-resolver' import { createNamespace } from '~/internals/graphql' @@ -12,7 +13,6 @@ import { } from '~/model/decoder' import { encodeSubjectId } from '~/schema/subject/utils' import { type Resolvers } from '~/types' -import { resolveUnrevisedEntityIds } from '../uuid/abstract-entity/resolvers' export const SubjectsResolver = createCachedResolver({ name: 'SubjectsResolver', diff --git a/packages/server/src/schema/uuid/taxonomy-term/resolvers.ts b/packages/server/src/schema/uuid/taxonomy-term/resolvers.ts index c663dd16a..586d76b0c 100644 --- a/packages/server/src/schema/uuid/taxonomy-term/resolvers.ts +++ b/packages/server/src/schema/uuid/taxonomy-term/resolvers.ts @@ -23,7 +23,6 @@ import { createThreadResolvers } from '~/schema/thread/utils' import { createUuidResolvers } from '~/schema/uuid/abstract-uuid/utils' import { TaxonomyTermType, TaxonomyTypeCreateOptions, Resolvers } from '~/types' import { isDefined } from '~/utils' -import { Context } from '@sentry/node/types/integrations' const typesMap = { root: TaxonomyTermType.Root, diff --git a/packages/server/src/schema/uuid/user/resolvers.ts b/packages/server/src/schema/uuid/user/resolvers.ts index d4f9274d4..73eb63d9b 100644 --- a/packages/server/src/schema/uuid/user/resolvers.ts +++ b/packages/server/src/schema/uuid/user/resolvers.ts @@ -5,6 +5,7 @@ import * as t from 'io-ts' import * as R from 'ramda' import * as DatabaseLayer from '../../../model/database-layer' +import { resolveUnrevisedEntityIds } from '../abstract-entity/resolvers' import { UuidResolver } from '../abstract-uuid/resolvers' import { createCachedResolver } from '~/cached-resolver' import { Context } from '~/context' @@ -24,7 +25,7 @@ import { isGlobalRole, } from '~/internals/graphql' import { CellValues, MajorDimension } from '~/model' -import { EntityDecoder, RevisionDecoder, UserDecoder } from '~/model/decoder' +import { EntityDecoder, UserDecoder } from '~/model/decoder' import { getPermissionsForRole, getRolesWithInheritance, @@ -34,7 +35,6 @@ import { resolveConnection } from '~/schema/connection/utils' import { createThreadResolvers } from '~/schema/thread/utils' import { createUuidResolvers } from '~/schema/uuid/abstract-uuid/utils' import { Instance, Resolvers } from '~/types' -import { resolveUnrevisedEntityIds } from '../abstract-entity/resolvers' export const activeUserIdsQuery = createCachedResolver< Record, From 0928691b165382fb7eb9d02dc2abd5823bcb0ca3 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Sat, 11 May 2024 02:06:38 +0200 Subject: [PATCH 23/41] test: Remove old set.ts tests --- .../schema/entity/set-abstract-entity.ts | 6 +- __tests__/schema/entity/set.ts | 637 ------------------ 2 files changed, 5 insertions(+), 638 deletions(-) delete mode 100644 __tests__/schema/entity/set.ts diff --git a/__tests__/schema/entity/set-abstract-entity.ts b/__tests__/schema/entity/set-abstract-entity.ts index 0f03f31ec..a1eec11b1 100644 --- a/__tests__/schema/entity/set-abstract-entity.ts +++ b/__tests__/schema/entity/set-abstract-entity.ts @@ -128,6 +128,10 @@ test('creates a new revision when "entityId" is set', async () => { }) }) +// TODO: needsReview = false => checkout +// TODO: autoreview with needsReview = false +// TODO: autoreview ignored when in multiple taxonomy terms + test('fails when both "entityId" and "parentId" are defined', async () => { await mutation .changeInput({ entityId: null, parentId: null }) @@ -162,7 +166,7 @@ test('fails when entityType is not valid', async () => { test('fails when changes is empty', async () => { await mutation - .changeInput({ change: '' }) + .changeInput({ change: ' ' }) .shouldFailWithError('BAD_USER_INPUT') }) diff --git a/__tests__/schema/entity/set.ts b/__tests__/schema/entity/set.ts deleted file mode 100644 index 87440cd81..000000000 --- a/__tests__/schema/entity/set.ts +++ /dev/null @@ -1,637 +0,0 @@ -import gql from 'graphql-tag' -import { HttpResponse } from 'msw' -import * as R from 'ramda' - -import { - applet, - article, - course, - coursePage, - event, - exercise, - exerciseGroup, - taxonomyTermSubject, - taxonomyTermRoot, - user, - video, - appletRevision, - articleRevision, - courseRevision, - coursePageRevision, - eventRevision, - exerciseRevision, - exerciseGroupRevision, - videoRevision, - licenseId, -} from '../../../__fixtures__' -import { given, Client, nextUuid, getTypenameAndId } from '../../__utils__' -import { autoreviewTaxonomyIds } from '~/config' -import { Model } from '~/internals/graphql' -import { DatabaseLayer } from '~/model' -import { DiscriminatorType, EntityType } from '~/model/decoder' -import { SetAbstractEntityInput } from '~/schema/uuid/abstract-entity/entity-set-handler' -import { fromEntityTypeToEntityRevisionType } from '~/schema/uuid/abstract-entity/utils' -import { Instance } from '~/types' - -interface EntityFields { - title: string - content: string - description: string - metaTitle: string - metaDescription: string - url: string -} - -const ALL_POSSIBLE_FIELDS: EntityFields = { - title: 'title', - content: 'content', - description: 'description', - metaTitle: 'metaTitle', - metaDescription: 'metaDescription', - url: 'https://url.org', -} - -const fieldKeys: Record< - EntityType, - [keyof EntityFields, ...(keyof EntityFields)[]] -> = { - [EntityType.Applet]: [ - 'title', - 'content', - 'metaTitle', - 'metaDescription', - 'url', - ], - [EntityType.Article]: ['title', 'content', 'metaTitle', 'metaDescription'], - [EntityType.Course]: ['title', 'content', 'metaDescription'], - [EntityType.CoursePage]: ['title', 'content'], - [EntityType.Event]: ['title', 'content', 'metaTitle', 'metaDescription'], - [EntityType.Exercise]: ['content'], - [EntityType.ExerciseGroup]: ['content'], - [EntityType.Video]: ['title', 'content', 'url'], -} -const entities = [ - applet, - article, - course, - coursePage, - event, - exercise, - exerciseGroup, - { ...video, taxonomyTermIds: [taxonomyTermSubject.id] }, -] - -class EntitySetTestCase { - public fields: Partial - - constructor(public entity: Model<'AbstractEntity'>) { - this.fields = R.pick(fieldKeys[this.entityType], ALL_POSSIBLE_FIELDS) - } - - get entityType() { - return this.entity.__typename - } - - get parent(): Model<'AbstractEntity' | 'TaxonomyTerm'> { - switch (this.entityType) { - case EntityType.CoursePage: - return course - default: - return taxonomyTermSubject - } - } - - get fieldsToDBLayer() { - if (this.entityType === EntityType.ExerciseGroup) { - return { - content: this.fields.content!, - } - } else if (this.entityType === EntityType.Course) { - return { - description: this.fields.content!, - title: this.fields.title!, - metaDescription: this.fields.metaDescription!, - } - } else if (this.entityType === EntityType.Video) { - return { - content: this.fields.url!, - description: this.fields.content!, - title: this.fields.title!, - } - } else { - return this.fields as Record - } - } - - get revision() { - switch (this.entityType) { - case EntityType.Applet: - return appletRevision - case EntityType.Article: - return articleRevision - case EntityType.Course: - return courseRevision - case EntityType.CoursePage: - return coursePageRevision - case EntityType.Event: - return eventRevision - case EntityType.Exercise: - return exerciseRevision - case EntityType.ExerciseGroup: - return exerciseGroupRevision - case EntityType.Video: - return videoRevision - } - } -} - -const testCases = entities.map((entity) => new EntitySetTestCase(entity)) - -beforeEach(() => { - given('UuidQuery').for(user, taxonomyTermRoot) -}) - -testCases.forEach((testCase) => { - describe('setAbstractEntity', () => { - const input: SetAbstractEntityInput = { - entityType: testCase.entityType, - changes: 'changes', - needsReview: true, - subscribeThis: false, - subscribeThisByEmail: false, - ...testCase.fields, - } - - const mutationWithParentId = new Client({ userId: user.id }) - .prepareQuery({ - query: gql` - mutation set($input: SetAbstractEntityInput!) { - entity { - setAbstractEntity(input: $input) { - success - record { - id - } - } - } - } - `, - }) - .withInput({ ...input, parentId: testCase.parent.id }) - - const inputWithEntityId = { ...input, entityId: testCase.entity.id } - - const mutationWithEntityId = new Client({ userId: user.id }) - .prepareQuery({ - query: gql` - mutation set($input: SetAbstractEntityInput!) { - entity { - setAbstractEntity(input: $input) { - success - record { - id - } - } - } - } - `, - }) - .withInput(inputWithEntityId) - - const { changes, needsReview, subscribeThis, subscribeThisByEmail } = input - - const entityCreatePayload: DatabaseLayer.Payload<'EntityCreateMutation'> = { - input: { - changes, - needsReview, - licenseId, - subscribeThis, - subscribeThisByEmail, - fields: testCase.fieldsToDBLayer, - ...(testCase.parent.__typename == DiscriminatorType.TaxonomyTerm - ? { taxonomyTermId: testCase.parent.id } - : {}), - ...(testCase.parent.__typename != DiscriminatorType.TaxonomyTerm - ? { parentId: testCase.parent.id } - : {}), - }, - userId: user.id, - entityType: testCase.entityType, - } - - const entityAddRevisionPayload: DatabaseLayer.Payload<'EntityAddRevisionMutation'> = - { - input: { - changes, - entityId: inputWithEntityId.entityId, - needsReview, - subscribeThis, - subscribeThisByEmail, - fields: testCase.fieldsToDBLayer, - }, - userId: user.id, - revisionType: fromEntityTypeToEntityRevisionType(testCase.entityType), - } - - beforeEach(() => { - given('UuidQuery').for(testCase.parent, taxonomyTermSubject) - }) - - test('creates an entity when parentId is provided', async () => { - given('EntityCreateMutation') - .withPayload(entityCreatePayload) - .returns(testCase.entity) - - await mutationWithParentId.shouldReturnData({ - entity: { - setAbstractEntity: { - success: true, - record: { id: testCase.entity.id }, - }, - }, - }) - }) - - test('adds new entity revision when entityId is provided', async () => { - given('UuidQuery').for(testCase.entity) - - given('EntityAddRevisionMutation') - .withPayload(entityAddRevisionPayload) - .returns({ success: true, revisionId: 123 }) - - await mutationWithEntityId.shouldReturnData({ - entity: { - setAbstractEntity: { - success: true, - record: { id: testCase.entity.id }, - }, - }, - }) - }) - - test('fails when user is not authenticated', async () => { - await mutationWithEntityId - .forUnauthenticatedUser() - .shouldFailWithError('UNAUTHENTICATED') - }) - - test('fails when user does not have role "login"', async () => { - given('UuidQuery').for(testCase.entity) - - const guestUser = { ...user, id: nextUuid(user.id), roles: ['guest'] } - - given('UuidQuery').for(guestUser) - - await mutationWithEntityId - .withContext({ userId: guestUser.id }) - .shouldFailWithError('FORBIDDEN') - }) - - describe('fails when a field is empty', () => { - test.each([ - ['empty string', ''], - ['string with just spaces', ' '], - ])('%s', async (_, changes) => { - await mutationWithEntityId - .changeInput({ changes }) - .shouldFailWithError('BAD_USER_INPUT') - - await mutationWithParentId - .changeInput({ changes }) - .shouldFailWithError('BAD_USER_INPUT') - }) - }) - - test('fails when database layer returns a 400er response', async () => { - given('EntityCreateMutation').returnsBadRequest() - given('EntityAddRevisionMutation').returnsBadRequest() - - await mutationWithParentId.shouldFailWithError('BAD_USER_INPUT') - - given('UuidQuery').for(testCase.entity) - await mutationWithEntityId.shouldFailWithError('BAD_USER_INPUT') - }) - - test('fails when database layer has an internal error', async () => { - given('EntityCreateMutation').hasInternalServerError() - given('EntityAddRevisionMutation').hasInternalServerError() - - await mutationWithParentId.shouldFailWithError('INTERNAL_SERVER_ERROR') - - given('UuidQuery').for(testCase.entity) - await mutationWithEntityId.shouldFailWithError('INTERNAL_SERVER_ERROR') - }) - - // TODO: Make it a proper test when doing the migration - test.skip('fails when parent does not exists', async () => { - await mutationWithParentId.shouldFailWithError('BAD_USER_INPUT') - }) - - describe(`Cache after setAbstractEntity call`, () => { - const newRevision = { ...testCase.revision, id: 123 } - const anotherEntity = { ...testCase.entity, id: 456 } - - beforeEach(() => { - given('UuidQuery').for( - testCase.entity, - testCase.revision, - anotherEntity, - taxonomyTermSubject, - ) - - given('EntityAddRevisionMutation') - .withPayload(entityAddRevisionPayload) - .returns({ success: true, revisionId: newRevision.id }) - - given('EntityAddRevisionMutation') - .withPayload({ - ...entityAddRevisionPayload, - input: { - ...entityAddRevisionPayload.input, - subscribeThis: true, - subscribeThisByEmail: true, - }, - }) - .returns({ success: true, revisionId: newRevision.id }) - - given('EntityAddRevisionMutation') - .withPayload({ - ...entityAddRevisionPayload, - input: { ...entityAddRevisionPayload.input, needsReview: false }, - }) - .isDefinedBy(() => { - given('UuidQuery').for( - { ...testCase.entity, currentRevisionId: newRevision.id }, - newRevision, - ) - - return HttpResponse.json({ - success: true, - revisionId: newRevision.id, - }) - }) - - given('SubscriptionsQuery') - .withPayload({ userId: user.id }) - .returns({ - subscriptions: [{ objectId: anotherEntity.id, sendEmail: true }], - }) - }) - - test('updates the checked out revision when needsReview=false', async () => { - const uuidQuery = new Client({ userId: user.id }) - .prepareQuery({ - query: gql` - query ($id: Int!) { - uuid(id: $id) { - id - __typename - ... on ${testCase.entityType} { - currentRevision { - id - } - } - } - } - `, - }) - .withVariables({ id: testCase.entity.id }) - - await uuidQuery.shouldReturnData({ - uuid: { - id: testCase.entity.id, - __typename: testCase.entity.__typename, - currentRevision: { id: testCase.entity.currentRevisionId }, - }, - }) - - await mutationWithEntityId.execute() - - await uuidQuery.shouldReturnData({ - uuid: { currentRevision: { id: testCase.entity.currentRevisionId } }, - }) - - await mutationWithEntityId - .withInput({ ...inputWithEntityId, needsReview: false }) - .execute() - - await uuidQuery.shouldReturnData({ - uuid: { currentRevision: { id: newRevision.id } }, - }) - }) - - test('updates the subscriptions', async () => { - const subscritionsQuery = new Client({ - userId: user.id, - }).prepareQuery({ - query: gql` - query { - subscription { - getSubscriptions { - nodes { - object { - __typename - id - } - sendEmail - } - } - } - } - `, - }) - - await subscritionsQuery.shouldReturnData({ - subscription: { - getSubscriptions: { - nodes: [ - { object: getTypenameAndId(anotherEntity), sendEmail: true }, - ], - }, - }, - }) - - await mutationWithEntityId - .withInput({ - ...inputWithEntityId, - subscribeThis: true, - subscribeThisByEmail: true, - }) - .execute() - - await subscritionsQuery.shouldReturnData({ - subscription: { - getSubscriptions: { - nodes: [ - { object: getTypenameAndId(anotherEntity), sendEmail: true }, - { - object: getTypenameAndId(testCase.entity), - sendEmail: true, - }, - ], - }, - }, - }) - }) - }) - }) -}) - -test('uses default license of the instance', async () => { - const exerciseEn = { ...exercise, instance: Instance.En } - - given('UuidQuery').for(exerciseEn, taxonomyTermSubject, taxonomyTermRoot) - given('EntityCreateMutation') - .withPayload({ - userId: 1, - entityType: EntityType.Exercise, - input: { - changes: 'changes', - licenseId: 9, - needsReview: true, - subscribeThis: true, - subscribeThisByEmail: true, - fields: { content: 'Hello World' }, - parentId: exerciseEn.id, - }, - }) - .returns(exercise) - - await new Client({ userId: user.id }) - .prepareQuery({ - query: gql` - mutation ($input: SetAbstractEntityInput!) { - entity { - setAbstractEntity(input: $input) { - success - } - } - } - `, - }) - .withInput({ - entityType: EntityType.Exercise, - changes: 'changes', - subscribeThis: true, - subscribeThisByEmail: true, - needsReview: true, - parentId: exerciseEn.id, - content: 'Hello World', - }) - .shouldReturnData({ entity: { setAbstractEntity: { success: true } } }) -}) - -describe('Autoreview entities', () => { - const input = { - entityType: EntityType.Exercise, - changes: 'changes', - needsReview: true, - subscribeThis: false, - subscribeThisByEmail: false, - content: 'content', - } - - const mutation = new Client({ userId: user.id }).prepareQuery({ - query: gql` - mutation ($input: SetAbstractEntityInput!) { - entity { - setAbstractEntity(input: $input) { - record { - ... on Exercise { - currentRevision { - id - } - } - } - } - } - } - `, - }) - - const oldRevisionId = exercise.currentRevisionId - const newRevisionId = 789 - - const taxonomy = { ...taxonomyTermSubject, id: 106082 } - const entity: typeof exercise = { - ...exercise, - currentRevisionId: oldRevisionId, - taxonomyTermIds: [taxonomy.id], - } - - const newRevision = { ...exerciseRevision, id: newRevisionId } - - beforeEach(() => { - given('UuidQuery').for(entity, exerciseRevision, article, taxonomy) - - given('EntityAddRevisionMutation').isDefinedBy(async ({ request }) => { - const body = await request.json() - - given('UuidQuery').for(newRevision) - - if (!body.payload.input.needsReview) - given('UuidQuery').for({ ...entity, currentRevisionId: newRevisionId }) - - return HttpResponse.json({ success: true, revisionId: newRevisionId }) - }) - - given('EntityCreateMutation').isDefinedBy(async ({ request }) => { - const body = await request.json() - - given('UuidQuery').for(newRevision) - - return HttpResponse.json({ - ...entity, - currentRevisionId: body.payload.input.needsReview - ? oldRevisionId - : newRevisionId, - }) - }) - }) - - describe('checks out revision without need of review, even if needsReview initially true', () => { - test('when a new revision is added', async () => { - await mutation - .withInput({ ...input, entityId: entity.id }) - .shouldReturnData({ - entity: { - setAbstractEntity: { - record: { currentRevision: { id: newRevisionId } }, - }, - }, - }) - }) - - test('when a new entity is created', async () => { - await mutation - .withInput({ ...input, parentId: taxonomy.id }) - .shouldReturnData({ - entity: { - setAbstractEntity: { - record: { currentRevision: { id: newRevisionId } }, - }, - }, - }) - }) - }) - - test('autoreview is ignored when entity is also in non-autoreview taxonomy term', async () => { - const taxonomyTermIds = [autoreviewTaxonomyIds[0], taxonomyTermRoot.id] - - given('UuidQuery').for( - { ...exercise, taxonomyTermIds }, - { ...taxonomyTermSubject, id: autoreviewTaxonomyIds[0] }, - taxonomyTermRoot, - ) - - await mutation - .withInput({ ...input, entityId: entity.id }) - .shouldReturnData({ - entity: { - setAbstractEntity: { - record: { currentRevision: { id: oldRevisionId } }, - }, - }, - }) - }) -}) From 6ee11019a66f138ce4cd525aff60cd63bb444c8e Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Sat, 11 May 2024 09:09:33 +0200 Subject: [PATCH 24/41] feat: Add support for "needsReview" --- .../schema/entity/set-abstract-entity.ts | 23 ++++++++++++++++++- .../schema/uuid/abstract-entity/resolvers.ts | 7 ++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/__tests__/schema/entity/set-abstract-entity.ts b/__tests__/schema/entity/set-abstract-entity.ts index a1eec11b1..b3a2cdd93 100644 --- a/__tests__/schema/entity/set-abstract-entity.ts +++ b/__tests__/schema/entity/set-abstract-entity.ts @@ -14,7 +14,7 @@ const input = { changes: 'my change', subscribeThis: true, subscribeThisByEmail: true, - needsReview: true, + needsReview: false, parentId: 5, entityId: null, content: JSON.stringify({ plugin: 'rows', state: [] }), @@ -132,6 +132,27 @@ test('creates a new revision when "entityId" is set', async () => { // TODO: autoreview with needsReview = false // TODO: autoreview ignored when in multiple taxonomy terms +test('check outs new revision when `needsReview` is false', async () => { + const data = (await mutation.getData()) as { + entity: { + setAbstractEntity: { entity: { id: number }; revision: { id: number } } + } + } + + expect(data).toMatchObject({ + entity: { setAbstractEntity: { success: true } }, + }) + + const entityId = data.entity.setAbstractEntity.entity.id + const revisionId = data.entity.setAbstractEntity.revision.id + + await entityQuery.withVariables({ id: entityId }).shouldReturnData({ + uuid: { + currentRevision: { id: revisionId }, + }, + }) +}) + test('fails when both "entityId" and "parentId" are defined', async () => { await mutation .changeInput({ entityId: null, parentId: null }) diff --git a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts index 066d47da3..bcbc9052d 100644 --- a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts +++ b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts @@ -225,6 +225,13 @@ export const resolvers: Resolvers = { ], ) + if (!input.needsReview) { + await database.mutate( + 'update entity set current_revision_id = ? where id = ?', + [revisionId, entity.id], + ) + } + await createEvent( { __typename: NotificationEventType.CreateEntityRevision, From 017cea900f8f58ba124f3a3c77acf0623824616f Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Sat, 11 May 2024 10:44:37 +0200 Subject: [PATCH 25/41] feat(set-abstract-entity): Add autoreview feature --- __tests__/__utils__/query.ts | 14 +++- .../schema/entity/set-abstract-entity.ts | 79 ++++++++++++++----- .../schema/uuid/abstract-entity/resolvers.ts | 28 ++++--- 3 files changed, 88 insertions(+), 33 deletions(-) diff --git a/__tests__/__utils__/query.ts b/__tests__/__utils__/query.ts index 990c286c7..172b0340c 100644 --- a/__tests__/__utils__/query.ts +++ b/__tests__/__utils__/query.ts @@ -6,11 +6,13 @@ export const entityQuery = new Client().prepareQuery({ query: gql` query ($id: Int!) { uuid(id: $id) { - ... on Article { + ... on AbstractUuid { + alias + } + ... on AbstractEntity { __typename id instance - alias trashed date title @@ -23,6 +25,8 @@ export const entityQuery = new Client().prepareQuery({ id } } + } + ... on AbstractTaxonomyTermChild { taxonomyTerms { nodes { id @@ -39,14 +43,16 @@ export const entityRevisionQuery = new Client().prepareQuery({ query: gql` query ($id: Int!) { uuid(id: $id) { - ... on ArticleRevision { + ... on AbstractUuid { + alias + } + ... on AbstractEntityRevision { __typename id author { id } trashed - alias date repository { id diff --git a/__tests__/schema/entity/set-abstract-entity.ts b/__tests__/schema/entity/set-abstract-entity.ts index b3a2cdd93..ea2fe9009 100644 --- a/__tests__/schema/entity/set-abstract-entity.ts +++ b/__tests__/schema/entity/set-abstract-entity.ts @@ -14,7 +14,7 @@ const input = { changes: 'my change', subscribeThis: true, subscribeThisByEmail: true, - needsReview: false, + needsReview: true, parentId: 5, entityId: null, content: JSON.stringify({ plugin: 'rows', state: [] }), @@ -43,7 +43,11 @@ const mutation = new Client({ userId: user.id }).prepareQuery({ variables: { input }, }) -// test autoreview1 +beforeEach(async () => { + // create taxonomy Term 106082 + await database.mutate('update uuid set id = 106082 where id = 35607') + await database.mutate('update term_taxonomy set id = 106082 where id = 35607') +}) test('creates a new entity when "parentId" is set', async () => { const data = (await mutation.getData()) as { @@ -80,10 +84,7 @@ test('creates a new entity when "parentId" is set', async () => { }) await expectEvent( - { - __typename: NotificationEventType.CreateEntity, - objectId: entityId, - }, + { __typename: NotificationEventType.CreateEntity, objectId: entityId }, 3, ) }) @@ -105,10 +106,7 @@ test('creates a new revision when "entityId" is set', async () => { const revisionId = data.entity.setAbstractEntity.revision.id await entityQuery.withVariables({ id: entityId }).shouldReturnData({ - uuid: { - __typename: 'Article', - id: 1855, - }, + uuid: { __typename: 'Article', id: 1855 }, }) await entityRevisionQuery.withVariables({ id: revisionId }).shouldReturnData({ @@ -128,12 +126,10 @@ test('creates a new revision when "entityId" is set', async () => { }) }) -// TODO: needsReview = false => checkout -// TODO: autoreview with needsReview = false -// TODO: autoreview ignored when in multiple taxonomy terms - test('check outs new revision when `needsReview` is false', async () => { - const data = (await mutation.getData()) as { + const data = (await mutation + .changeInput({ needsReview: false }) + .getData()) as { entity: { setAbstractEntity: { entity: { id: number }; revision: { id: number } } } @@ -147,9 +143,56 @@ test('check outs new revision when `needsReview` is false', async () => { const revisionId = data.entity.setAbstractEntity.revision.id await entityQuery.withVariables({ id: entityId }).shouldReturnData({ - uuid: { - currentRevision: { id: revisionId }, - }, + uuid: { currentRevision: { id: revisionId } }, + }) +}) + +test('check outs new revision for entities in autoreview taxonomies', async () => { + await database.mutate( + 'update term_taxonomy_entity set term_taxonomy_id = 106082 where entity_id = 35554', + ) + + const data = (await mutation + .changeInput({ entityId: 35554, parentId: null }) + .getData()) as { + entity: { + setAbstractEntity: { entity: { id: number }; revision: { id: number } } + } + } + + expect(data).toMatchObject({ + entity: { setAbstractEntity: { success: true } }, + }) + + const entityId = data.entity.setAbstractEntity.entity.id + const revisionId = data.entity.setAbstractEntity.revision.id + + await entityQuery.withVariables({ id: entityId }).shouldReturnData({ + uuid: { currentRevision: { id: revisionId } }, + }) +}) + +test('does not check out new revision for entities being in autoreview and non-autoreview taxonomies', async () => { + await database.mutate( + 'insert into term_taxonomy_entity (term_taxonomy_id, entity_id) values (106082, 1855)', + ) + + const data = (await mutation + .changeInput({ entityId: 1855, parentId: null }) + .getData()) as { + entity: { + setAbstractEntity: { entity: { id: number }; revision: { id: number } } + } + } + + expect(data).toMatchObject({ + entity: { setAbstractEntity: { success: true } }, + }) + + const entityId = data.entity.setAbstractEntity.entity.id + + await entityQuery.withVariables({ id: entityId }).shouldReturnData({ + uuid: { currentRevision: { id: 30674 } }, }) }) diff --git a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts index bcbc9052d..fc144130e 100644 --- a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts +++ b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts @@ -225,7 +225,9 @@ export const resolvers: Resolvers = { ], ) - if (!input.needsReview) { + const isAutoreview = await isAutoreviewEntity(entity, context) + + if (!input.needsReview || isAutoreview) { await database.mutate( 'update entity set current_revision_id = ? where id = ?', [revisionId, entity.id], @@ -259,7 +261,10 @@ export const resolvers: Resolvers = { context, ) - // TODO: Delete subscriptions for user + // Set subscriptions + await context.dataSources.model.serlo.getSubscriptions._querySpec.removeCache( + { payload: { userId } }, + ) return { success: true, @@ -528,22 +533,23 @@ function decodeDateOfDeletion(after: string) { } async function isAutoreviewEntity( - id: number, + uuid: { id: number }, context: Context, ): Promise { - if (autoreviewTaxonomyIds.includes(id)) return true - - const uuid = await UuidResolver.resolve({ id }, context) + if (autoreviewTaxonomyIds.includes(uuid.id)) return true if (t.type({ parentId: t.number }).is(uuid)) { - return ( - uuid.parentId != null && - (await isAutoreviewEntity(uuid.parentId, context)) - ) + if (uuid.parentId == null) return false + const parent = await UuidResolver.resolve({ id: uuid.parentId }, context) + + return parent != null && (await isAutoreviewEntity(parent, context)) } else if (t.type({ taxonomyTermIds: t.array(t.number) }).is(uuid)) { return ( await Promise.all( - uuid.taxonomyTermIds.map((id) => isAutoreviewEntity(id, context)), + uuid.taxonomyTermIds.map(async (id) => { + const parent = await UuidResolver.resolve({ id }, context) + return parent != null && (await isAutoreviewEntity(parent, context)) + }), ) ).every((x) => x) } else { From 88b095cda954f8bc1397c192b12df705b28cbff4 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Sat, 11 May 2024 10:49:22 +0200 Subject: [PATCH 26/41] feat(set-abstract-entity): Check review rights --- __tests__/schema/entity/set-abstract-entity.ts | 10 +++++++++- .../src/schema/uuid/abstract-entity/resolvers.ts | 10 ++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/__tests__/schema/entity/set-abstract-entity.ts b/__tests__/schema/entity/set-abstract-entity.ts index ea2fe9009..ed01ddf26 100644 --- a/__tests__/schema/entity/set-abstract-entity.ts +++ b/__tests__/schema/entity/set-abstract-entity.ts @@ -147,12 +147,20 @@ test('check outs new revision when `needsReview` is false', async () => { }) }) -test('check outs new revision for entities in autoreview taxonomies', async () => { +test('fails on review when author has no review roles', async () => { + await mutation + .forLoginUser() + .changeInput({ needsReview: false }) + .shouldFailWithError('FORBIDDEN') +}) + +test('check outs new revision for entities in autoreview taxonomies for login users', async () => { await database.mutate( 'update term_taxonomy_entity set term_taxonomy_id = 106082 where entity_id = 35554', ) const data = (await mutation + .forLoginUser() .changeInput({ entityId: 35554, parentId: null }) .getData()) as { entity: { diff --git a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts index fc144130e..42db9d268 100644 --- a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts +++ b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts @@ -228,6 +228,16 @@ export const resolvers: Resolvers = { const isAutoreview = await isAutoreviewEntity(entity, context) if (!input.needsReview || isAutoreview) { + if (!isAutoreview) { + await assertUserIsAuthorized({ + context, + message: 'For needsReview = false you need review rights', + guard: serloAuth.Entity.checkoutRevision( + instanceToScope(entity.instance), + ), + }) + } + await database.mutate( 'update entity set current_revision_id = ? where id = ?', [revisionId, entity.id], From b8e4c29928cff22b3f5178888147f02121d66743 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Mon, 20 May 2024 20:06:50 +0200 Subject: [PATCH 27/41] refactor(subscription): Add setSubscription() --- __tests__/__utils__/query.ts | 17 +++++ __tests__/schema/subscription.ts | 33 +++------- .../src/schema/subscription/resolvers.ts | 64 +++++++++++++------ 3 files changed, 68 insertions(+), 46 deletions(-) diff --git a/__tests__/__utils__/query.ts b/__tests__/__utils__/query.ts index 172b0340c..10c41db6b 100644 --- a/__tests__/__utils__/query.ts +++ b/__tests__/__utils__/query.ts @@ -135,3 +135,20 @@ export const subjectQuery = new Client().prepareQuery({ `, variables: { instance: 'de' }, }) + +export const subscriptionsQuery = new Client({ userId: 27393 }).prepareQuery({ + query: gql` + query { + subscription { + getSubscriptions { + nodes { + object { + id + } + sendEmail + } + } + } + } + `, +}) diff --git a/__tests__/schema/subscription.ts b/__tests__/schema/subscription.ts index 06ba86244..6ad005698 100644 --- a/__tests__/schema/subscription.ts +++ b/__tests__/schema/subscription.ts @@ -1,23 +1,6 @@ import gql from 'graphql-tag' -import { Client } from '../__utils__' - -const subscriptionQuery = new Client({ userId: 27393 }).prepareQuery({ - query: gql` - query { - subscription { - getSubscriptions { - nodes { - object { - id - } - sendEmail - } - } - } - } - `, -}) +import { Client, subscriptionsQuery } from '../__utils__' describe('currentUserHasSubscribed', () => { const query = new Client({ userId: 1 }).prepareQuery({ @@ -44,7 +27,7 @@ describe('currentUserHasSubscribed', () => { }) test('getSubscriptions', async () => { - await subscriptionQuery.shouldReturnData({ + await subscriptionsQuery.shouldReturnData({ subscription: { getSubscriptions: { nodes: [ @@ -71,7 +54,7 @@ describe('subscription mutation set', () => { }) test('when subscribe=true and sendEmail=true', async () => { - await subscriptionQuery.shouldReturnData({ + await subscriptionsQuery.shouldReturnData({ subscription: { getSubscriptions: { nodes: [ @@ -87,7 +70,7 @@ describe('subscription mutation set', () => { .withInput({ id: [27781, 1555], subscribe: true, sendEmail: true }) .shouldReturnData({ subscription: { set: { success: true } } }) - await subscriptionQuery.shouldReturnData({ + await subscriptionsQuery.shouldReturnData({ subscription: { getSubscriptions: { nodes: [ @@ -102,7 +85,7 @@ describe('subscription mutation set', () => { }) test('when subscribe=true and sendEmail=false', async () => { - await subscriptionQuery.shouldReturnData({ + await subscriptionsQuery.shouldReturnData({ subscription: { getSubscriptions: { nodes: [ @@ -118,7 +101,7 @@ describe('subscription mutation set', () => { .withInput({ id: [27393, 1555], subscribe: true, sendEmail: false }) .shouldReturnData({ subscription: { set: { success: true } } }) - await subscriptionQuery.shouldReturnData({ + await subscriptionsQuery.shouldReturnData({ subscription: { getSubscriptions: { nodes: [ @@ -133,7 +116,7 @@ describe('subscription mutation set', () => { }) test('when subscribe=false', async () => { - await subscriptionQuery.shouldReturnData({ + await subscriptionsQuery.shouldReturnData({ subscription: { getSubscriptions: { nodes: [ @@ -149,7 +132,7 @@ describe('subscription mutation set', () => { .withInput({ id: [27393, 1555], subscribe: false, sendEmail: false }) .shouldReturnData({ subscription: { set: { success: true } } }) - await subscriptionQuery.shouldReturnData({ + await subscriptionsQuery.shouldReturnData({ subscription: { getSubscriptions: { nodes: [ diff --git a/packages/server/src/schema/subscription/resolvers.ts b/packages/server/src/schema/subscription/resolvers.ts index e1ecbcbca..f9ce58afb 100644 --- a/packages/server/src/schema/subscription/resolvers.ts +++ b/packages/server/src/schema/subscription/resolvers.ts @@ -1,6 +1,7 @@ import * as auth from '@serlo/authorization' import { UuidResolver } from '../uuid/abstract-uuid/resolvers' +import { Context } from '~/context' import { assertUserIsAuthenticated, assertUserIsAuthorized, @@ -87,27 +88,11 @@ export const resolvers: Resolvers = { const transaction = await database.beginTransaction() try { - if (subscribe) { - for (const id of ids) { - const { affectedRows } = await database.mutate( - 'update subscription set notify_mailman = ? where user_id = ? and uuid_id = ?', - [sendEmail ? 1 : 0, userId, id], - ) - - if (affectedRows === 0) { - await database.mutate( - 'insert into subscription (uuid_id, user_id, notify_mailman) values (?, ?, ?)', - [id, userId, sendEmail ? 1 : 0], - ) - } - } - } else { - for (const id of ids) { - await database.mutate( - `delete from subscription where user_id = ? and uuid_id = ?`, - [userId, id], - ) - } + for (const id of ids) { + await setSubscription( + { subscribe, userId, objectId: id, sendEmail }, + context, + ) } await transaction.commit() @@ -119,3 +104,40 @@ export const resolvers: Resolvers = { }, }, } + +export async function setSubscription( + args: { + subscribe: boolean + userId: number + objectId: number + sendEmail: boolean + }, + { database }: Pick, +) { + const { subscribe, userId, objectId, sendEmail } = args + const transaction = await database.beginTransaction() + + try { + if (subscribe) { + const { affectedRows } = await database.mutate( + 'update subscription set notify_mailman = ? where user_id = ? and uuid_id = ?', + [sendEmail ? 1 : 0, userId, objectId], + ) + + if (affectedRows === 0) { + await database.mutate( + 'insert into subscription (uuid_id, user_id, notify_mailman) values (?, ?, ?)', + [objectId, userId, sendEmail ? 1 : 0], + ) + } + } else { + await database.mutate( + `delete from subscription where user_id = ? and uuid_id = ?`, + [userId, objectId], + ) + } + await transaction.commit() + } finally { + await transaction.rollback() + } +} From e54c5a6c6d96c3c96405f02db60caa9352f2cf2f Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Mon, 20 May 2024 21:45:23 +0200 Subject: [PATCH 28/41] fix(set-abstract-entity): Add setting of subscriptions again --- .../schema/entity/set-abstract-entity.ts | 33 +++++++++++++++++-- .../schema/uuid/abstract-entity/resolvers.ts | 11 +++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/__tests__/schema/entity/set-abstract-entity.ts b/__tests__/schema/entity/set-abstract-entity.ts index cef8d17a9..8085a1046 100644 --- a/__tests__/schema/entity/set-abstract-entity.ts +++ b/__tests__/schema/entity/set-abstract-entity.ts @@ -1,11 +1,11 @@ import gql from 'graphql-tag' -import { user } from '../../../__fixtures__' import { Client, entityQuery, entityRevisionQuery, expectEvent, + subscriptionsQuery, } from '../../__utils__' import { NotificationEventType } from '~/model/decoder' @@ -24,7 +24,7 @@ const input = { url: 'url', } -const mutation = new Client({ userId: user.id }).prepareQuery({ +const mutation = new Client({ userId: 1 }).prepareQuery({ query: gql` mutation ($input: SetAbstractEntityInput!) { entity { @@ -91,6 +91,35 @@ test('creates a new entity when "parentId" is set', async () => { ) }) +test('creates a subscription', async () => { + await subscriptionsQuery.withContext({ userId: 15491 }).shouldReturnData({ + subscription: { getSubscriptions: { nodes: [] } }, + }) + + const data = (await mutation + .withContext({ userId: 15491 }) + .changeInput({ needsReview: true }) + .getData()) as { + entity: { + setAbstractEntity: { entity: { id: number }; revision: { id: number } } + } + } + + expect(data).toMatchObject({ + entity: { setAbstractEntity: { success: true } }, + }) + + const entityId = data.entity.setAbstractEntity.entity.id + + await subscriptionsQuery.withContext({ userId: 15491 }).shouldReturnData({ + subscription: { + getSubscriptions: { + nodes: [{ object: { id: entityId }, sendEmail: true }], + }, + }, + }) +}) + test('creates a new revision when "entityId" is set', async () => { const data = (await mutation .changeInput({ parentId: null, entityId: 1855 }) diff --git a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts index 96babb181..7c62d1ece 100644 --- a/packages/server/src/schema/uuid/abstract-entity/resolvers.ts +++ b/packages/server/src/schema/uuid/abstract-entity/resolvers.ts @@ -26,6 +26,7 @@ import { } from '~/model/decoder' import { resolveConnection } from '~/schema/connection/utils' import { createEvent } from '~/schema/events/event' +import { setSubscription } from '~/schema/subscription/resolvers' import { Resolvers, SetAbstractEntityInput } from '~/types' import { isDateString } from '~/utils' @@ -244,6 +245,16 @@ export const resolvers: Resolvers = { ) } + await setSubscription( + { + subscribe: input.subscribeThis, + sendEmail: input.subscribeThisByEmail, + objectId: entity.id, + userId, + }, + context, + ) + await createEvent( { __typename: NotificationEventType.CreateEntityRevision, From 81b36eb21d8d363bdf5de00ea93f00925f09b722 Mon Sep 17 00:00:00 2001 From: Hugo Tiburtino Date: Thu, 6 Jun 2024 09:45:04 +0200 Subject: [PATCH 29/41] chore(mysql): update container to have merged course pages --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 18b94f1f4..f37ba2e8b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: ports: - '6379:6379' mysql: - image: eu.gcr.io/serlo-shared/serlo-mysql-database:prerelease-merge-course-pages-into-courses + image: eu.gcr.io/serlo-shared/serlo-mysql-database:prerelease-merge-course-pages-into-courses.2 platform: linux/x86_64 pull_policy: always ports: From fbd6195ecb7b058d2e9af0fcbec54a8a1ab20e06 Mon Sep 17 00:00:00 2001 From: Hugo Tiburtino Date: Thu, 6 Jun 2024 10:44:31 +0200 Subject: [PATCH 30/41] test(entity): fix test after #1534 --- __tests__/schema/uuid/entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/schema/uuid/entity.ts b/__tests__/schema/uuid/entity.ts index 177f7b0c6..3ab383709 100644 --- a/__tests__/schema/uuid/entity.ts +++ b/__tests__/schema/uuid/entity.ts @@ -6,7 +6,7 @@ test('UuidQuery for an entity', async () => { __typename: 'Article', id: 27801, instance: 'de', - alias: '/mathe/27801/addition-und-subtraktion-von-dezimalbrchen', + alias: '/mathe/27801/addition-und-subtraktion-von-dezimalbruechen', trashed: false, date: '2014-08-26T08:29:35.000Z', title: 'Addition und Subtraktion von Dezimalbrüchen', From 148fe8427e5f3db6040b8eb32bcea1782e6dc334 Mon Sep 17 00:00:00 2001 From: Hugo Tiburtino Date: Thu, 6 Jun 2024 10:48:50 +0200 Subject: [PATCH 31/41] test(entity): fix test after https://github.com/serlo/db-migrations/pull/339 --- __tests__/schema/uuid/entity-revision.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/schema/uuid/entity-revision.ts b/__tests__/schema/uuid/entity-revision.ts index 76c7f77f8..24d2ffa50 100644 --- a/__tests__/schema/uuid/entity-revision.ts +++ b/__tests__/schema/uuid/entity-revision.ts @@ -12,7 +12,7 @@ test('Uuid query for an entity revision', async () => { repository: { id: 35295 }, title: '"falsche Freunde"', content: - '{"plugin":"rows","state":[{"plugin":"text","state":[{"type":"p","children":[{"text":"wip"}]}]}]}', + '{"plugin":"rows","state":[{"plugin":"text","state":[{"type":"p","children":[{"text":"wip"}]}],"id":"8abdb955-fa42-442d-87a8-91bfae603101"}],"id":"e5dd1162-7e55-4b1c-aacd-acb267c290ee"}', changes: '', metaTitle: '', metaDescription: '', From 6593cfc2f99986eb0794049029c008d9d8ee8c55 Mon Sep 17 00:00:00 2001 From: Hugo Tiburtino Date: Thu, 6 Jun 2024 11:59:51 +0200 Subject: [PATCH 32/41] test(metadata): fix test after merging course pages, more authors are included --- __fixtures__/metadata.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/__fixtures__/metadata.ts b/__fixtures__/metadata.ts index e18154148..ce68c0b5d 100644 --- a/__fixtures__/metadata.ts +++ b/__fixtures__/metadata.ts @@ -265,6 +265,26 @@ const courseMetadata = { }, ], creator: [ + { + id: 'https://serlo.org/15473', + name: '125f3e12', + type: 'Person', + affiliation: { + id: 'https://serlo.org/organization', + name: 'Serlo Education e.V.', + type: 'Organization', + }, + }, + { + id: 'https://serlo.org/15491', + name: '125f4a84', + type: 'Person', + affiliation: { + id: 'https://serlo.org/organization', + name: 'Serlo Education e.V.', + type: 'Organization', + }, + }, { id: 'https://serlo.org/324', name: '122d486a', From ae4b4c1e66b7a9e2a0bf44e70901c892555cd771 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Sun, 9 Jun 2024 14:03:19 +0200 Subject: [PATCH 33/41] chore(docker): Force images to refetch I changed "yarn start:containers" so that images are refetched once the tag is changed or there is a new image. Should should avoid problems when one uses a lot of preleleases of the mysql image. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 666324032..31679fd0b 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "redis:list": "docker compose exec redis redis-cli KEYS '*'", "start": "run-s start:containers start:server", "start:enmeshed": "docker-compose -f enmeshed/docker-compose.yml up -d", - "start:containers": "docker compose up --detach", + "start:containers": "docker compose pull && docker compose up --detach --force-recreate", "start:kratos": "docker compose -f docker-compose.kratos.yml up --detach", "start:sso": "docker compose -f docker-compose.sso.yml up --detach", "start:server": "yarn _start packages/server/src/server.ts server.cjs", From 0810b4de494be6ee189d302aa70d0fbace81ccfd Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Sun, 9 Jun 2024 14:42:08 +0200 Subject: [PATCH 34/41] refactor(taxonomy): Update after simplify taxonomy tables See https://github.com/serlo/db-migrations/issues/346 --- .../schema/entity/set-abstract-entity.ts | 2 +- __tests__/schema/taxonomy-term/create.ts | 14 ----- docker-compose.yml | 2 +- .../server/src/schema/metadata/resolvers.ts | 23 ++++---- .../server/src/schema/subject/resolvers.ts | 56 +++++++++---------- .../server/src/schema/thread/resolvers.ts | 4 +- .../schema/uuid/abstract-uuid/resolvers.ts | 18 +++--- .../schema/uuid/taxonomy-term/resolvers.ts | 50 +++++------------ 8 files changed, 64 insertions(+), 105 deletions(-) diff --git a/__tests__/schema/entity/set-abstract-entity.ts b/__tests__/schema/entity/set-abstract-entity.ts index 8085a1046..ab2ccbc06 100644 --- a/__tests__/schema/entity/set-abstract-entity.ts +++ b/__tests__/schema/entity/set-abstract-entity.ts @@ -47,7 +47,7 @@ beforeEach(async () => { // create taxonomy Term 106082 await databaseForTests.mutate('update uuid set id = 106082 where id = 35607') await databaseForTests.mutate( - 'update term_taxonomy set id = 106082 where id = 35607', + 'update taxonomy set id = 106082 where id = 35607', ) }) diff --git a/__tests__/schema/taxonomy-term/create.ts b/__tests__/schema/taxonomy-term/create.ts index 418aba57a..f4a609daf 100644 --- a/__tests__/schema/taxonomy-term/create.ts +++ b/__tests__/schema/taxonomy-term/create.ts @@ -64,20 +64,6 @@ describe('creates a new taxonomy term', () => { }) }) -test('does not fail when there are duplicated taxonomy entries', async () => { - // In the database layer there was a bug to created multiple duplicated - // taxonomy entries (which is currently not in the DB container) - // This test can be deleted after https://github.com/serlo/db-migrations/issues/346 - - await databaseForTests.mutate( - `insert into taxonomy (type_id, instance_id) values (11, 1)`, - ) - - await mutation.shouldReturnData({ - taxonomyTerm: { create: { success: true } }, - }) -}) - test('cache of parent is updated', async () => { const query = new Client().prepareQuery({ query: gql` diff --git a/docker-compose.yml b/docker-compose.yml index f37ba2e8b..3a2e9acd3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: ports: - '6379:6379' mysql: - image: eu.gcr.io/serlo-shared/serlo-mysql-database:prerelease-merge-course-pages-into-courses.2 + image: eu.gcr.io/serlo-shared/serlo-mysql-database:prerelease-simplify-taxonomy-tables platform: linux/x86_64 pull_policy: always ports: diff --git a/packages/server/src/schema/metadata/resolvers.ts b/packages/server/src/schema/metadata/resolvers.ts index 61cb5ad76..110700b73 100644 --- a/packages/server/src/schema/metadata/resolvers.ts +++ b/packages/server/src/schema/metadata/resolvers.ts @@ -85,7 +85,7 @@ export const resolvers: Resolvers = { originalAuthorUrl: string | null instance: string taxonomyTermIds: number[] - termNames: Record + taxonomyNames: Record authors: Record authorEdits: Record } @@ -94,11 +94,11 @@ export const resolvers: Resolvers = { ` WITH RECURSIVE subject_mapping AS ( SELECT - subject.id AS term_taxonomy_id, + subject.id AS taxonomy_id, subject.id AS subject_id, root.id AS root_id - FROM term_taxonomy root - JOIN term_taxonomy subject ON subject.parent_id = root.id + FROM taxonomy root + JOIN taxonomy subject ON subject.parent_id = root.id WHERE root.parent_id IS NULL OR root.id IN (106081, 146728) @@ -108,8 +108,8 @@ export const resolvers: Resolvers = { child.id, subject_mapping.subject_id, subject_mapping.root_id - FROM term_taxonomy child - JOIN subject_mapping ON subject_mapping.term_taxonomy_id = child.parent_id + FROM taxonomy child + JOIN subject_mapping ON subject_mapping.taxonomy_id = child.parent_id -- "Fächer im Aufbau" taxonomy is on the level of normal Serlo subjects, therefore we need a level below it. -- "Partner" taxonomy is below the subject "Mathematik", but we only want the entities with the specific partner as the subject. WHERE child.parent_id NOT IN (87993, 106081, 146728) @@ -128,8 +128,8 @@ export const resolvers: Resolvers = { license.url AS licenseUrl, license.original_author_url as originalAuthorUrl, instance.subdomain AS instance, - JSON_ARRAYAGG(term_taxonomy.id) AS taxonomyTermIds, - JSON_OBJECTAGG(term_taxonomy.id, term.name) AS termNames, + JSON_ARRAYAGG(taxonomy.id) AS taxonomyTermIds, + JSON_OBJECTAGG(taxonomy.id, taxonomy.name) AS taxonomyNames, JSON_OBJECTAGG(user.id, user.username) AS authors, JSON_OBJECTAGG(all_revisions_of_entity.id, user.id) AS authorEdits FROM entity @@ -139,11 +139,10 @@ export const resolvers: Resolvers = { JOIN license on license.id = entity.license_id JOIN entity_revision ON entity.current_revision_id = entity_revision.id JOIN term_taxonomy_entity on term_taxonomy_entity.entity_id = entity.id - JOIN term_taxonomy on term_taxonomy_entity.term_taxonomy_id = term_taxonomy.id - JOIN term on term_taxonomy.term_id = term.id + JOIN taxonomy on term_taxonomy_entity.term_taxonomy_id = taxonomy.id JOIN entity_revision all_revisions_of_entity ON all_revisions_of_entity.repository_id = entity.id JOIN user ON all_revisions_of_entity.author_id = user.id - JOIN subject_mapping on subject_mapping.term_taxonomy_id = term_taxonomy_entity.term_taxonomy_id + JOIN subject_mapping on subject_mapping.taxonomy_id = term_taxonomy_entity.term_taxonomy_id WHERE entity.id > ? AND (? is NULL OR instance.subdomain = ?) AND (? is NULL OR entity_revision.date > ?) @@ -311,7 +310,7 @@ export const resolvers: Resolvers = { } } const termName = - R.sortBy(([id]) => parseInt(id), Object.entries(row.termNames)) + R.sortBy(([id]) => parseInt(id), Object.entries(row.taxonomyNames)) .map((x) => x[1]) .at(0) ?? '' const fromI18n: string = row.instance === 'de' ? 'aus' : 'from' diff --git a/packages/server/src/schema/subject/resolvers.ts b/packages/server/src/schema/subject/resolvers.ts index a34de9778..9463c3ac2 100644 --- a/packages/server/src/schema/subject/resolvers.ts +++ b/packages/server/src/schema/subject/resolvers.ts @@ -30,13 +30,11 @@ export const SubjectsResolver = createCachedResolver({ SELECT subject.id as taxonomyTermId, subject_instance.subdomain as instance - FROM term_taxonomy AS subject - JOIN term_taxonomy AS root ON root.id = subject.parent_id + FROM taxonomy AS subject + JOIN taxonomy AS root ON root.id = subject.parent_id JOIN uuid as subject_uuid ON subject_uuid.id = subject.id - JOIN taxonomy AS subject_taxonomy ON subject_taxonomy.id = subject.taxonomy_id - JOIN type AS subject_type ON subject_type.id = subject_taxonomy.type_id - JOIN term AS subject_term ON subject_term.id = subject.term_id - JOIN instance AS subject_instance ON subject_instance.id = subject_term.instance_id + JOIN type AS subject_type ON subject_type.id = subject.type_id + JOIN instance AS subject_instance ON subject_instance.id = subject.instance_id WHERE (root.parent_id IS NULL OR root.id = 106081 @@ -119,29 +117,29 @@ export const SubjectResolver = createCachedResolver({ return await database.fetchOptional( ` - SELECT t.name as name, t1.id as id - FROM term_taxonomy t0 - JOIN term_taxonomy t1 ON t1.parent_id = t0.id - LEFT JOIN term_taxonomy t2 ON t2.parent_id = t1.id - LEFT JOIN term_taxonomy t3 ON t3.parent_id = t2.id - LEFT JOIN term_taxonomy t4 ON t4.parent_id = t3.id - LEFT JOIN term_taxonomy t5 ON t5.parent_id = t4.id - LEFT JOIN term_taxonomy t6 ON t6.parent_id = t5.id - LEFT JOIN term_taxonomy t7 ON t7.parent_id = t6.id - LEFT JOIN term_taxonomy t8 ON t8.parent_id = t7.id - LEFT JOIN term_taxonomy t9 ON t9.parent_id = t8.id - LEFT JOIN term_taxonomy t10 ON t10.parent_id = t9.id - LEFT JOIN term_taxonomy t11 ON t11.parent_id = t10.id - LEFT JOIN term_taxonomy t12 ON t12.parent_id = t11.id - LEFT JOIN term_taxonomy t13 ON t13.parent_id = t12.id - LEFT JOIN term_taxonomy t14 ON t14.parent_id = t13.id - LEFT JOIN term_taxonomy t15 ON t15.parent_id = t14.id - LEFT JOIN term_taxonomy t16 ON t16.parent_id = t15.id - LEFT JOIN term_taxonomy t17 ON t17.parent_id = t16.id - LEFT JOIN term_taxonomy t18 ON t18.parent_id = t17.id - LEFT JOIN term_taxonomy t19 ON t19.parent_id = t18.id - LEFT JOIN term_taxonomy t20 ON t20.parent_id = t19.id - JOIN term t on t1.term_id = t.id + SELECT + t1.name as name, t1.id as id + FROM taxonomy t0 + JOIN taxonomy t1 ON t1.parent_id = t0.id + LEFT JOIN taxonomy t2 ON t2.parent_id = t1.id + LEFT JOIN taxonomy t3 ON t3.parent_id = t2.id + LEFT JOIN taxonomy t4 ON t4.parent_id = t3.id + LEFT JOIN taxonomy t5 ON t5.parent_id = t4.id + LEFT JOIN taxonomy t6 ON t6.parent_id = t5.id + LEFT JOIN taxonomy t7 ON t7.parent_id = t6.id + LEFT JOIN taxonomy t8 ON t8.parent_id = t7.id + LEFT JOIN taxonomy t9 ON t9.parent_id = t8.id + LEFT JOIN taxonomy t10 ON t10.parent_id = t9.id + LEFT JOIN taxonomy t11 ON t11.parent_id = t10.id + LEFT JOIN taxonomy t12 ON t12.parent_id = t11.id + LEFT JOIN taxonomy t13 ON t13.parent_id = t12.id + LEFT JOIN taxonomy t14 ON t14.parent_id = t13.id + LEFT JOIN taxonomy t15 ON t15.parent_id = t14.id + LEFT JOIN taxonomy t16 ON t16.parent_id = t15.id + LEFT JOIN taxonomy t17 ON t17.parent_id = t16.id + LEFT JOIN taxonomy t18 ON t18.parent_id = t17.id + LEFT JOIN taxonomy t19 ON t19.parent_id = t18.id + LEFT JOIN taxonomy t20 ON t20.parent_id = t19.id WHERE ( t0.id = 146728 OR diff --git a/packages/server/src/schema/thread/resolvers.ts b/packages/server/src/schema/thread/resolvers.ts index f46b8583a..32bea80cb 100644 --- a/packages/server/src/schema/thread/resolvers.ts +++ b/packages/server/src/schema/thread/resolvers.ts @@ -68,13 +68,13 @@ export const resolvers: Resolvers = { ` WITH RECURSIVE descendants AS ( SELECT id, parent_id - FROM term_taxonomy + FROM taxonomy WHERE (? is null OR id = ?) UNION SELECT tt.id, tt.parent_id - FROM term_taxonomy tt + FROM taxonomy tt JOIN descendants d ON tt.parent_id = d.id ), subject_entities AS ( SELECT id as entity_id FROM descendants diff --git a/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts b/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts index b22bf529e..b7ee82918 100644 --- a/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts +++ b/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts @@ -311,13 +311,13 @@ async function resolveUuidFromDatabase( ELSE comment_status.name END AS commentStatus, + taxonomy.id AS taxonomyId, taxonomy_type.name AS taxonomyType, taxonomy_instance.subdomain AS taxonomyInstance, - term.name AS taxonomyName, - term_taxonomy.description AS taxonomyDescription, - term_taxonomy.weight AS taxonomyWeight, - taxonomy.id AS taxonomyId, - term_taxonomy.parent_id AS taxonomyParentId, + taxonomy.name AS taxonomyName, + taxonomy.description AS taxonomyDescription, + taxonomy.weight AS taxonomyWeight, + taxonomy.parent_id AS taxonomyParentId, JSON_OBJECTAGG( COALESCE(taxonomy_child.id, "__no_key"), taxonomy_child.weight @@ -351,13 +351,11 @@ async function resolveUuidFromDatabase( left join entity revision_entity on revision_entity.id = revision.repository_id left join type revision_type on revision_entity.type_id = revision_type.id - LEFT JOIN term_taxonomy ON term_taxonomy.id = uuid.id - LEFT JOIN taxonomy ON taxonomy.id = term_taxonomy.taxonomy_id + LEFT JOIN taxonomy ON taxonomy.id = uuid.id LEFT JOIN type taxonomy_type ON taxonomy_type.id = taxonomy.type_id LEFT JOIN instance taxonomy_instance ON taxonomy_instance.id = taxonomy.instance_id - LEFT JOIN term ON term.id = term_taxonomy.term_id - LEFT JOIN term_taxonomy taxonomy_child ON taxonomy_child.parent_id = term_taxonomy.id - LEFT JOIN term_taxonomy_entity ON term_taxonomy_entity.term_taxonomy_id = term_taxonomy.id + LEFT JOIN taxonomy taxonomy_child ON taxonomy_child.parent_id = taxonomy.id + LEFT JOIN term_taxonomy_entity ON term_taxonomy_entity.term_taxonomy_id = taxonomy.id LEFT JOIN user ON user.id = uuid.id LEFT JOIN role_user ON user.id = role_user.user_id diff --git a/packages/server/src/schema/uuid/taxonomy-term/resolvers.ts b/packages/server/src/schema/uuid/taxonomy-term/resolvers.ts index 52931a8e5..a3cff3f66 100644 --- a/packages/server/src/schema/uuid/taxonomy-term/resolvers.ts +++ b/packages/server/src/schema/uuid/taxonomy-term/resolvers.ts @@ -126,51 +126,31 @@ export const resolvers: Resolvers = { throw new InternalServerError('no uuid entry could be created') } - const { insertId: termId } = await database.mutate( - ` - insert into term (instance_id, name) - select term_parent.instance_id, ? - from term term_parent - join term_taxonomy taxonomy_parent on taxonomy_parent.term_id = term_parent.id - where taxonomy_parent.id = ? - limit 1 - `, - [name, parentId], - ) - - if (termId <= 0) { - throw new UserInputError( - `parent taxonomy ${parentId} does not exists`, - ) - } - const { currentHeaviest } = await database.fetchOne<{ currentHeaviest: number }>( ` - SELECT IFNULL(MAX(tt.weight), 0) AS currentHeaviest - FROM term_taxonomy tt - WHERE tt.parent_id = ? - `, + SELECT IFNULL(MAX(taxonomy.weight), 0) AS currentHeaviest + FROM taxonomy + WHERE taxonomy.parent_id = ? + `, [parentId], ) await database.mutate( ` - insert into term_taxonomy (id, taxonomy_id, term_id, parent_id, description, weight) - select ?, taxonomy.id, ?, ?, ?, ? - from taxonomy - join type on taxonomy.type_id = type.id - join instance on taxonomy.instance_id = instance.id + insert into taxonomy + (id, type_id, instance_id, parent_id, description, weight, name) + select ?, type.id, instance.id, ?, ?, ?, ? + from type, instance where type.name = ? and instance.subdomain = ? - limit 1 - `, + `, [ taxonomyId, - termId, parentId, description, currentHeaviest + 1, + name, taxonomyType, parent.instance, ], @@ -402,7 +382,7 @@ export const resolvers: Resolvers = { // we do not need to distinguish between them await database.mutate( - 'update term_taxonomy set weight = ? where parent_id = ? and id = ?', + 'update taxonomy set weight = ? where parent_id = ? and id = ?', [position, taxonomyTermId, childId], ) @@ -459,11 +439,9 @@ export const resolvers: Resolvers = { await database.mutate( ` - UPDATE term - JOIN term_taxonomy ON term.id = term_taxonomy.term_id - SET term.name = ?, - term_taxonomy.description = ? - WHERE term_taxonomy.id = ?; + UPDATE taxonomy + SET name = ?, description = ? + WHERE taxonomy.id = ?; `, [input.name, input.description, input.id], ) From 102ff06fa7464a7880c434c96e54fb128afe154f Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Wed, 12 Jun 2024 17:34:39 +0200 Subject: [PATCH 35/41] refactor(taxonomy): Set taxonomyId to -1 --- packages/server/src/schema/uuid/abstract-uuid/resolvers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts b/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts index b7ee82918..fc08d1def 100644 --- a/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts +++ b/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts @@ -311,7 +311,6 @@ async function resolveUuidFromDatabase( ELSE comment_status.name END AS commentStatus, - taxonomy.id AS taxonomyId, taxonomy_type.name AS taxonomyType, taxonomy_instance.subdomain AS taxonomyInstance, taxonomy.name AS taxonomyName, @@ -513,7 +512,8 @@ async function resolveUuidFromDatabase( name: baseUuid.taxonomyName, description: baseUuid.taxonomyDescription, weight: baseUuid.taxonomyWeight ?? 0, - taxonomyId: baseUuid.taxonomyId, + // TODO: Remove this property when https://github.com/serlo/frontend/pull/3882 is merged + taxonomyId: -1, parentId: baseUuid.taxonomyParentId, childrenIds, } From 05715ed6f9901d829b0eb6ae423fd1a4326454bf Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Fri, 14 Jun 2024 14:52:07 +0200 Subject: [PATCH 36/41] fix(taxonomy-term): Remove `taxonomyId` from decoder --- packages/server/src/schema/uuid/abstract-uuid/resolvers.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts b/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts index fc08d1def..0ec362c9d 100644 --- a/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts +++ b/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts @@ -235,7 +235,6 @@ const BaseTaxonomy = t.intersection([ taxonomyName: t.string, taxonomyDescription: t.union([t.null, t.string]), taxonomyWeight: t.union([t.null, t.number]), - taxonomyId: t.number, taxonomyParentId: t.union([t.null, t.number]), taxonomyChildrenIds: WeightedNumberList, taxonomyEntityChildrenIds: WeightedNumberList, From 2c94cf4559757919349eb11bf7006a0c5374a0f6 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Fri, 14 Jun 2024 14:52:49 +0200 Subject: [PATCH 37/41] refactor(taxonomy-term): Remove taxonomyId from GraphQL --- packages/server/src/model/decoder.ts | 1 - packages/server/src/schema/uuid/abstract-uuid/resolvers.ts | 2 -- packages/server/src/schema/uuid/taxonomy-term/types.graphql | 1 - packages/server/src/types.ts | 2 -- 4 files changed, 6 deletions(-) diff --git a/packages/server/src/model/decoder.ts b/packages/server/src/model/decoder.ts index f953e837f..fad9b596f 100644 --- a/packages/server/src/model/decoder.ts +++ b/packages/server/src/model/decoder.ts @@ -187,7 +187,6 @@ export const TaxonomyTermDecoder = t.exact( weight: t.number, childrenIds: t.array(t.number), parentId: t.union([t.number, t.null]), - taxonomyId: t.number, }), t.partial({ description: t.union([t.string, t.null]), diff --git a/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts b/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts index 0ec362c9d..389164d95 100644 --- a/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts +++ b/packages/server/src/schema/uuid/abstract-uuid/resolvers.ts @@ -511,8 +511,6 @@ async function resolveUuidFromDatabase( name: baseUuid.taxonomyName, description: baseUuid.taxonomyDescription, weight: baseUuid.taxonomyWeight ?? 0, - // TODO: Remove this property when https://github.com/serlo/frontend/pull/3882 is merged - taxonomyId: -1, parentId: baseUuid.taxonomyParentId, childrenIds, } diff --git a/packages/server/src/schema/uuid/taxonomy-term/types.graphql b/packages/server/src/schema/uuid/taxonomy-term/types.graphql index 426e4bca8..d57c884fa 100644 --- a/packages/server/src/schema/uuid/taxonomy-term/types.graphql +++ b/packages/server/src/schema/uuid/taxonomy-term/types.graphql @@ -23,7 +23,6 @@ type TaxonomyTerm implements AbstractUuid & InstanceAware & ThreadAware { weight: Int! parent: TaxonomyTerm path: [TaxonomyTerm]! - taxonomyId: Int! children(after: String, first: Int): AbstractUuidConnection! } diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index d5cda859a..a92d54ec5 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -1406,7 +1406,6 @@ export type TaxonomyTerm = AbstractUuid & InstanceAware & ThreadAware & { name: Scalars['String']['output']; parent?: Maybe; path: Array>; - taxonomyId: Scalars['Int']['output']; threads: ThreadConnection; title: Scalars['String']['output']; trashed: Scalars['Boolean']['output']; @@ -3112,7 +3111,6 @@ export type TaxonomyTermResolvers; parent?: Resolver, ParentType, ContextType>; path?: Resolver>, ParentType, ContextType>; - taxonomyId?: Resolver; threads?: Resolver>; title?: Resolver; trashed?: Resolver; From 3740186cd73ca668db6da63b3dde26b9b0800c21 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Fri, 14 Jun 2024 15:19:51 +0200 Subject: [PATCH 38/41] test(taxonomy-term): Update `taxonomyId` to -1 --- __tests__/schema/uuid/taxonomy-term.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/__tests__/schema/uuid/taxonomy-term.ts b/__tests__/schema/uuid/taxonomy-term.ts index 5c9e680fc..160482e93 100644 --- a/__tests__/schema/uuid/taxonomy-term.ts +++ b/__tests__/schema/uuid/taxonomy-term.ts @@ -13,7 +13,7 @@ test('TaxonomyTerm root', async () => { name: 'Root', description: null, weight: 0, - taxonomyId: 1, + taxonomyId: -1, path: [], parent: null, children: { @@ -55,7 +55,7 @@ test('TaxonomyTerm subject', async () => { name: 'Chemie', description: '', weight: 17, - taxonomyId: 3, + taxonomyId: -1, path: [], parent: { id: 3 }, children: { @@ -87,7 +87,7 @@ test('TaxonomyTerm exerciseFolder', async () => { name: 'Example topic folder', description: '', weight: 1, - taxonomyId: 19, + taxonomyId: -1, path: [{ id: 23590 }, { id: 23593 }, { id: 35559 }, { id: 35560 }], parent: { id: 35560, From 7b9d8ab14ebe8d7765f09bcded96bb58df13b845 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Fri, 14 Jun 2024 15:21:18 +0200 Subject: [PATCH 39/41] test: Remove `taxonomyId` --- __tests__/schema/uuid/taxonomy-term.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/__tests__/schema/uuid/taxonomy-term.ts b/__tests__/schema/uuid/taxonomy-term.ts index 160482e93..aa87997c3 100644 --- a/__tests__/schema/uuid/taxonomy-term.ts +++ b/__tests__/schema/uuid/taxonomy-term.ts @@ -13,7 +13,6 @@ test('TaxonomyTerm root', async () => { name: 'Root', description: null, weight: 0, - taxonomyId: -1, path: [], parent: null, children: { @@ -55,7 +54,6 @@ test('TaxonomyTerm subject', async () => { name: 'Chemie', description: '', weight: 17, - taxonomyId: -1, path: [], parent: { id: 3 }, children: { @@ -87,7 +85,6 @@ test('TaxonomyTerm exerciseFolder', async () => { name: 'Example topic folder', description: '', weight: 1, - taxonomyId: -1, path: [{ id: 23590 }, { id: 23593 }, { id: 35559 }, { id: 35560 }], parent: { id: 35560, From 3cc641f7f107a92bf6927e28d2d3145e6eff562f Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Fri, 14 Jun 2024 15:25:23 +0200 Subject: [PATCH 40/41] test: Remove `taxonomyId` from fixtures --- __fixtures__/uuid/taxonomy-term.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/__fixtures__/uuid/taxonomy-term.ts b/__fixtures__/uuid/taxonomy-term.ts index 2a313d76f..fbef0e442 100644 --- a/__fixtures__/uuid/taxonomy-term.ts +++ b/__fixtures__/uuid/taxonomy-term.ts @@ -13,7 +13,6 @@ export const taxonomyTermRoot: Model<'TaxonomyTerm'> = { description: null, weight: 1, parentId: null, - taxonomyId: 1, childrenIds: [5], } @@ -28,7 +27,6 @@ export const taxonomyTermSubject: Model<'TaxonomyTerm'> = { description: null, weight: 2, parentId: taxonomyTermRoot.id, - taxonomyId: 3, childrenIds: [16048], } @@ -42,7 +40,6 @@ export const taxonomyTermCurriculumTopic: Model<'TaxonomyTerm'> = { name: 'name', description: 'description', weight: 3, - taxonomyId: 11, parentId: taxonomyTermSubject.id, childrenIds: [1855], } @@ -57,7 +54,6 @@ export const taxonomyTermTopic: Model<'TaxonomyTerm'> = { name: 'Geometrie', description: null, weight: 6, - taxonomyId: 4, parentId: 5, childrenIds: [ 23453, 1454, 1394, 24518, 1380, 24410, 24422, 1381, 1383, 1300, 1413, @@ -74,7 +70,6 @@ export const taxonomyTermTopicFolder: Model<'TaxonomyTerm'> = { name: 'Aufgaben zu einfachen Potenzen', description: '', weight: 1, - taxonomyId: 9, parentId: 1288, childrenIds: [10385, 6925, 6921, 6933, 6917, 7085], } From 6002530924bc9e3cf0017ed63429b816e3a69747 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Fri, 14 Jun 2024 16:27:52 +0200 Subject: [PATCH 41/41] test: Remove `taxonomyId` from query --- __tests__/__utils__/query.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/__tests__/__utils__/query.ts b/__tests__/__utils__/query.ts index 419c2b9fb..1b97ac6a4 100644 --- a/__tests__/__utils__/query.ts +++ b/__tests__/__utils__/query.ts @@ -102,7 +102,6 @@ export const taxonomyTermQuery = new Client().prepareQuery({ name description weight - taxonomyId path { id }