Skip to content
This repository has been archived by the owner on Nov 2, 2024. It is now read-only.

Commit

Permalink
✨ Add separate authors table, add support for "custom authors" (#1248)
Browse files Browse the repository at this point in the history
* Rebuild how authors are handled for news

* Fix how database articles are seeded by quering database

* Fix various issues for author

* Fix resolvers for author

* Frontend changes required for authors

* Fix and comment migration

* Remove temporary disabling of meilisearch

* Fix lint

* Drop enum type on migration rollback

* Use getAuthorImage on nolla article

* Refactor links without href

* Remove commented out code

* Add check constraint to enforce type column on authors

* gen grapghl

* Change authors to not have duplicate rows

* Change fromMember to fromAuthor on notifications

* Fix frontend for new notification author system

* Fix migration

* Fix lint

* Change datetime > check to >= check
  • Loading branch information
Macludde authored Sep 7, 2023
1 parent c5e7676 commit f78c7af
Show file tree
Hide file tree
Showing 57 changed files with 2,636 additions and 1,717 deletions.
445 changes: 279 additions & 166 deletions backend/services/core/graphql.schema.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
// Add custom authors table
await knex.schema.createTable('custom_authors', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.string('name').notNullable().comment('Swedish name of custom author');
table.string('name_en').comment('English name of custom author, possibly null');
table.string('image_url').comment('Image url of custom author, possibly null');
table.timestamps(true, true);
});
await knex.schema.createTable('custom_author_roles', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.uuid('custom_author_id').unsigned().notNullable().references('custom_authors.id')
.onDelete('CASCADE');
table.string('role').unsigned().notNullable().comment('should use role syntax, such as dsek.styr or dsek.stab.noll');
table.timestamps(true, true);
});
await knex.schema;
// Add new "authors" table
await knex.schema.createTable('authors', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.uuid('member_id').notNullable().references('members.id').onDelete('CASCADE');
table.uuid('mandate_id').unsigned().nullable().references('mandates.id')
.onDelete('SET NULL');
table.uuid('custom_id').unsigned().nullable().references('custom_authors.id')
.onDelete('SET NULL');
table.timestamps(true, true);

table.check(`mandate_id IS NULL AND custom_id IS NULL
OR (mandate_id IS NOT NULL AND custom_id IS NULL)
OR (mandate_id IS NULL AND custom_id IS NOT NULL)`, undefined, 'enforce_author_type');
table.unique(['member_id', 'mandate_id', 'custom_id']).comment('Only one author per member, mandate or custom author');
});
// add "type column" which is generated based on the other ones
await knex.raw(`
ALTER TABLE authors ADD COLUMN type VARCHAR GENERATED ALWAYS AS (
CASE
WHEN mandate_id IS NULL AND custom_id IS NULL THEN 'Member'
WHEN mandate_id IS NOT NULL AND custom_id IS NULL THEN 'Mandate'
WHEN mandate_id IS NULL AND custom_id IS NOT NULL THEN 'Custom'
END
) STORED`);
// Get all articles posted by a member, not as a mandate
const memberAuthors = await knex('articles').distinct('author_id')
.where({ author_type: 'Member' })
.distinct('author_id');
// Get all articles posted as a mandate
const mandateAuthors = await knex('articles').distinct('author_id as mandate_id', 'mandates.member_id')
.join('mandates', 'mandates.id', '=', 'articles.author_id')
.where({ author_type: 'Mandate' });
// Create rows in authors table for each article posted as a member
if (memberAuthors.length > 0) {
await knex('authors')
.insert(memberAuthors.map((author) => ({
member_id: author.author_id,
})));
}
// Create rows in authors table for each article posted as a mandate
if (mandateAuthors.length > 0) {
await knex('authors')
.insert(mandateAuthors.map((author) => ({
member_id: author.member_id,
mandate_id: author.mandate_id,
})));
}
// Update all articles to point to the newly created author rows,
// using the temporary article_id column
await knex.raw(`
UPDATE articles
SET author_id = authors.id
FROM authors
WHERE articles.author_id IS NOT NULL
AND articles.author_type = 'Member'
AND authors.member_id = articles.author_id
AND authors.type = 'Member'
`);
await knex.raw(`
UPDATE articles
SET author_id = authors.id
FROM authors
WHERE articles.author_id IS NOT NULL
AND articles.author_type = 'Mandate'
AND authors.mandate_id = articles.author_id
AND authors.type = 'Mandate'
`);

// remove old author columns from articles
await knex.schema.alterTable('articles', (table) => {
table.foreign('author_id').references('authors.id').onDelete('SET NULL');
table.dropColumn('author_type');
});
}

export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('articles', (table) => {
table.dropForeign('author_id');
table.string('author_type').notNullable().defaultTo('Member').comment('What type the author is (e.g. Member and Mandate)');
});
await knex.raw(`
UPDATE articles
SET author_id = authors.member_id,
author_type = 'Member'
FROM authors
WHERE authors.id = articles.author_id
AND authors.type = 'Member'
`);
await knex.raw(`
UPDATE articles
SET author_id = authors.mandate_id,
author_type = 'Mandate'
FROM authors
WHERE authors.id = articles.author_id
AND authors.type = 'Mandate'
`);
await knex.schema.dropTable('authors');
await knex.schema.dropTable('custom_author_roles');
await knex.schema.dropTable('custom_authors');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('notifications', (table) => {
table.uuid('from_author_id')
.unsigned()
.references('authors.id')
.onDelete('SET NULL')
.comment('The author which took the action that initiated the notification. Null if not relevant.');
});
const membersWithoutAuthor = await knex('notifications')
.distinct('notifications.from_member_id')
.leftJoin('authors', 'authors.member_id', '=', 'notifications.from_member_id')
.whereNotNull('notifications.from_member_id')
.andWhere('authors.type', 'Member')
.whereNull('authors.id');
if (membersWithoutAuthor.length > 0) {
await knex.insert(membersWithoutAuthor.map((member) => ({
member_id: member.from_member_id,
type: 'Member',
}))).into('authors');
}
await knex.raw(`
UPDATE notifications
SET from_author_id = authors.id
FROM authors
WHERE notifications.from_member_id = authors.member_id
AND authors.type = 'Member'
`);
await knex.schema.alterTable('notifications', (table) => {
table.dropColumn('from_member_id');
});
}

export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('notifications', (table) => {
table.uuid('from_member_id')
.unsigned()
.references('members.id')
.onDelete('SET NULL')
.comment('The author which took the action that initiated the notification. Null if not relevant.');
});
await knex.raw(`
UPDATE notifications
SET from_member_id = authors.member_id
FROM authors
WHERE notifications.from_author_id = authors.id
AND authors.type = 'Member'
`);
await knex.schema.alterTable('notifications', (table) => {
table.dropColumn('from_author_id');
});
}
41 changes: 23 additions & 18 deletions backend/services/core/seeds/data.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
import type { Knex } from 'knex';
import insertMarkdowns from './helpers/insertMarkdowns';
import insertExpoTokens from './helpers/insertExpoTokens';
import insertMembers from './helpers/insertMembers';
import insertCommittees from './helpers/insertCommittees';
import insertPositions from './helpers/insertPositions';
import insertMandates from './helpers/insertMandates';
import insertArticles from './helpers/insertArticles';
import
{
Alert, ArticleTag,
} from '~/src/types/news';
import deleteExistingEntries from './helpers/deleteExistingEntries';
import insertApiAccessPolicies from './helpers/insertApiAccessPolicies';
import insertArticleCommentsAndLikes from './helpers/insertArticleComments';
import insertKeycloakRelations from './helpers/insertKeycloakRelations';
import insertEvents from './helpers/insertEvents';
import insertEventComments from './helpers/insertEventComments';
import insertTags from './helpers/insertTags';
import insertArticles from './helpers/insertArticles';
import insertBookableCategories from './helpers/insertBookableCategories';
import insertBookables from './helpers/insertBookables';
import insertBookingRequests from './helpers/insertBookings';
import insertDoors from './helpers/insertDoors';
import insertCommittees from './helpers/insertCommittees';
import insertCustomAuthors from './helpers/insertCustomAuthors';
import insertDoorAccessPolicies from './helpers/insertDoorAccessPolicies';
import insertDoors from './helpers/insertDoors';
import insertEventComments from './helpers/insertEventComments';
import insertEvents from './helpers/insertEvents';
import insertExpoTokens from './helpers/insertExpoTokens';
import insertGoverningDocuments from './helpers/insertGoverningDocuments';
import insertKeycloakRelations from './helpers/insertKeycloakRelations';
import insertMailAlias from './helpers/insertMailAlias';
import insertMandates from './helpers/insertMandates';
import insertMarkdowns from './helpers/insertMarkdowns';
import insertMembers from './helpers/insertMembers';
import insertPositions from './helpers/insertPositions';
import insertProducts from './helpers/insertProducts';
import { ArticleTag, Alert } from '../src/types/news';
import insertApiAccessPolicies from './helpers/insertApiAccessPolicies';
import insertGoverningDocuments from './helpers/insertGoverningDocuments';
import { insertSubscriptionSettings, insertNotifications } from './helpers/notifications';
import insertTags from './helpers/insertTags';
import { insertNotifications, insertSubscriptionSettings } from './helpers/insertNotifications';

// eslint-disable-next-line import/prefer-default-export
export const seed = async (knex: Knex) => {
Expand All @@ -42,11 +46,12 @@ export const seed = async (knex: Knex) => {

const positionIds = await insertPositions(knex, committeesIds);

const mandateIds = await insertMandates(knex, memberIds, positionIds);
await insertMandates(knex, memberIds, positionIds);

const tagIds = await insertTags(knex);

const articleIds = await insertArticles(knex, memberIds, mandateIds);
await insertCustomAuthors(knex);
const articleIds = await insertArticles(knex);
await knex<ArticleTag>('article_tags').insert([{
article_id: articleIds[1],
tag_id: tagIds[0],
Expand Down
3 changes: 3 additions & 0 deletions backend/services/core/seeds/helpers/deleteExistingEntries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,7 @@ export default async function deleteExistingEntries(knex: Knex) {
await knex(TABLE.PRODUCT_DISCOUNT).del();
await knex('alerts').del();
await knex('governing_documents').del();
await knex('custom_authors').del();
await knex('custom_author_roles').del();
await knex('authors').del();
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export default async function insertApiAccessPolicies(knex: Knex) {
{ api_name: 'news:article:like', role: '_' },
{ api_name: 'news:article:comment', role: '_' },
{ api_name: 'news:article:comment:delete', role: 'dsek.infu' },
// { api_name: 'news:article:update', role: 'dsek.infu' },
// { api_name: 'news:article:delete', role: 'dsek.infu' },
{ api_name: 'news:article:update', role: 'dsek.infu' },
{ api_name: 'news:article:delete', role: 'dsek.infu' },
{ api_name: 'fileHandler:news:create', role: 'dsek.infu' },
{ api_name: 'fileHandler:news:read', role: '*' },
{ api_name: 'fileHandler:news:update', role: 'dsek.infu' },
Expand Down Expand Up @@ -87,6 +87,7 @@ export default async function insertApiAccessPolicies(knex: Knex) {
{ api_name: 'core:mail:alias:update', role: '*' },
{ api_name: 'governing_document:read', role: '*' },
{ api_name: 'governing_document:write', role: '_' },
{ api_name: 'nolla', role: '*' },
{ api_name: 'nolla:news', role: 'dsek.infu.dwww' },
{ api_name: 'nolla:news', role: 'dsek.noll' },
{ api_name: 'nolla:events', role: 'dsek.noll' },
Expand Down
Loading

0 comments on commit f78c7af

Please sign in to comment.