1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-08-02 20:15:27 +02:00

feat: Version 2

Closes #627, closes #1047
This commit is contained in:
Maksim Eltyshev 2025-05-10 02:09:06 +02:00
parent ad7fb51cfa
commit 2ee1166747
1557 changed files with 76832 additions and 47042 deletions

View file

@ -1,24 +0,0 @@
module.exports.up = (knex) =>
knex.raw(`
CREATE SEQUENCE next_id_seq;
CREATE FUNCTION next_id(OUT id BIGINT) AS $$
DECLARE
shard INT := 1;
epoch BIGINT := 1567191600000;
sequence BIGINT;
milliseconds BIGINT;
BEGIN
SELECT nextval('next_id_seq') % 1024 INTO sequence;
SELECT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000) INTO milliseconds;
id := (milliseconds - epoch) << 23;
id := id | (shard << 10);
id := id | (sequence);
END;
$$ LANGUAGE PLPGSQL;
`);
module.exports.down = (knex) =>
knex.raw(`
DROP SEQUENCE next_id_seq;
DROP FUNCTION next_id(OUT id BIGINT);
`);

View file

@ -1,19 +0,0 @@
module.exports.up = (knex) =>
knex.schema.createTable('archive', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.text('from_model').notNullable();
table.bigInteger('original_record_id').notNullable();
table.json('original_record').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.unique(['from_model', 'original_record_id']);
});
module.exports.down = (knex) => knex.schema.dropTable('archive');

View file

@ -1,29 +0,0 @@
module.exports.up = (knex) =>
knex.schema
.createTable('user_account', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.text('email').notNullable();
table.text('password').notNullable();
table.boolean('is_admin').notNullable();
table.text('name').notNullable();
table.text('username');
table.text('avatar_dirname');
table.text('phone');
table.text('organization');
table.boolean('subscribe_to_own_cards').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
table.timestamp('deleted_at', true);
})
.raw(
'ALTER TABLE "user_account" ADD CONSTRAINT "user_email_unique" EXCLUDE ("email" WITH =) WHERE ("deleted_at" IS NULL)',
)
.raw(
'ALTER TABLE "user_account" ADD CONSTRAINT "user_username_unique" EXCLUDE ("username" WITH =) WHERE ("username" IS NOT NULL AND "deleted_at" IS NULL)',
);
module.exports.down = (knex) => knex.schema.dropTable('user_account');

View file

@ -1,15 +0,0 @@
module.exports.up = (knex) =>
knex.schema.createTable('project', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.text('name').notNullable();
table.jsonb('background');
table.text('background_image_dirname');
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
});
module.exports.down = (knex) => knex.schema.dropTable('project');

View file

@ -1,19 +0,0 @@
module.exports.up = (knex) =>
knex.schema.createTable('project_manager', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('project_id').notNullable();
table.bigInteger('user_id').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.unique(['project_id', 'user_id']);
table.index('user_id');
});
module.exports.down = (knex) => knex.schema.dropTable('project_manager');

View file

@ -1,22 +0,0 @@
module.exports.up = (knex) =>
knex.schema.createTable('board', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('project_id').notNullable();
table.text('type').notNullable();
table.specificType('position', 'double precision').notNullable();
table.text('name').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('project_id');
table.index('position');
});
module.exports.down = (knex) => knex.schema.dropTable('board');

View file

@ -1,19 +0,0 @@
module.exports.up = (knex) =>
knex.schema.createTable('board_membership', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('board_id').notNullable();
table.bigInteger('user_id').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.unique(['board_id', 'user_id']);
table.index('user_id');
});
module.exports.down = (knex) => knex.schema.dropTable('board_membership');

View file

@ -1,20 +0,0 @@
module.exports.up = (knex) =>
knex.schema.createTable('label', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('board_id').notNullable();
table.text('name');
table.text('color').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('board_id');
});
module.exports.down = (knex) => knex.schema.dropTable('label');

View file

@ -1,21 +0,0 @@
module.exports.up = (knex) =>
knex.schema.createTable('list', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('board_id').notNullable();
table.specificType('position', 'double precision').notNullable();
table.text('name').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('board_id');
table.index('position');
});
module.exports.down = (knex) => knex.schema.dropTable('list');

View file

@ -1,28 +0,0 @@
module.exports.up = (knex) =>
knex.schema.createTable('card', async (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('board_id').notNullable();
table.bigInteger('list_id');
table.bigInteger('creator_user_id').notNullable();
table.bigInteger('cover_attachment_id');
table.specificType('position', 'double precision');
table.text('name').notNullable();
table.text('description');
table.timestamp('due_date', true);
table.jsonb('timer');
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('board_id');
table.index('list_id');
table.index('position');
});
module.exports.down = (knex) => knex.schema.dropTable('card');

View file

@ -1,21 +0,0 @@
module.exports.up = (knex) =>
knex.schema.createTable('card_subscription', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('card_id').notNullable();
table.bigInteger('user_id').notNullable();
table.boolean('is_permanent').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.unique(['card_id', 'user_id']);
table.index('user_id');
});
module.exports.down = (knex) => knex.schema.dropTable('card_subscription');

View file

@ -1,19 +0,0 @@
module.exports.up = (knex) =>
knex.schema.createTable('card_membership', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('card_id').notNullable();
table.bigInteger('user_id').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.unique(['card_id', 'user_id']);
table.index('user_id');
});
module.exports.down = (knex) => knex.schema.dropTable('card_membership');

View file

@ -1,19 +0,0 @@
module.exports.up = (knex) =>
knex.schema.createTable('card_label', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('card_id').notNullable();
table.bigInteger('label_id').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.unique(['card_id', 'label_id']);
table.index('label_id');
});
module.exports.down = (knex) => knex.schema.dropTable('card_label');

View file

@ -1,20 +0,0 @@
module.exports.up = (knex) =>
knex.schema.createTable('task', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('card_id').notNullable();
table.text('name').notNullable();
table.boolean('is_completed').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('card_id');
});
module.exports.down = (knex) => knex.schema.dropTable('task');

View file

@ -1,23 +0,0 @@
module.exports.up = (knex) =>
knex.schema.createTable('attachment', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('card_id').notNullable();
table.bigInteger('creator_user_id').notNullable();
table.text('dirname').notNullable();
table.text('filename').notNullable();
table.boolean('is_image').notNullable();
table.text('name').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('card_id');
});
module.exports.down = (knex) => knex.schema.dropTable('attachment');

View file

@ -1,21 +0,0 @@
module.exports.up = (knex) =>
knex.schema.createTable('action', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('card_id').notNullable();
table.bigInteger('user_id').notNullable();
table.text('type').notNullable();
table.jsonb('data').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('card_id');
});
module.exports.down = (knex) => knex.schema.dropTable('action');

View file

@ -1,24 +0,0 @@
module.exports.up = (knex) =>
knex.schema.createTable('notification', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('user_id').notNullable();
table.bigInteger('action_id').notNullable();
table.bigInteger('card_id').notNullable();
table.boolean('is_read').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('user_id');
table.index('action_id');
table.index('card_id');
table.index('is_read');
});
module.exports.down = (knex) => knex.schema.dropTable('notification');

View file

@ -1,71 +0,0 @@
const path = require('path');
const sharp = require('sharp');
const _ = require('lodash');
const getConfig = require('../../get-config');
module.exports.up = async (knex) => {
await knex.schema.table('attachment', (table) => {
/* Columns */
table.jsonb('image');
});
const config = await getConfig();
const attachments = await knex('attachment');
// eslint-disable-next-line no-restricted-syntax
for (const attachment of attachments) {
if (attachment.is_image) {
const image = sharp(
path.join(config.custom.attachmentsPath, attachment.dirname, attachment.filename),
);
let metadata;
try {
metadata = await image.metadata(); // eslint-disable-line no-await-in-loop
} catch (error) {
continue; // eslint-disable-line no-continue
}
if (!['svg', 'pdf'].includes(metadata.format)) {
// eslint-disable-next-line no-await-in-loop
await knex('attachment')
.update({
image: _.pick(metadata, ['width', 'height']),
})
.where('id', attachment.id);
}
}
}
return knex.schema.table('attachment', (table) => {
table.dropColumn('is_image');
});
};
module.exports.down = async (knex) => {
await knex.schema.table('attachment', (table) => {
/* Columns */
table.boolean('is_image');
});
const attachments = await knex('attachment');
// eslint-disable-next-line no-restricted-syntax
for (const attachment of attachments) {
// eslint-disable-next-line no-await-in-loop
await knex('attachment')
.update({
is_image: !!attachment.image,
})
.where('id', attachment.id);
}
return knex.schema.table('attachment', (table) => {
table.dropColumn('image');
table.dropNullable('is_image');
});
};

View file

@ -1,5 +0,0 @@
const { addPosition, removePosition } = require('../../utils/migrations');
module.exports.up = (knex) => addPosition(knex, 'task', 'card_id');
module.exports.down = (knex) => removePosition(knex, 'task');

View file

@ -1,11 +0,0 @@
module.exports.up = (knex) =>
knex.schema.table('user_account', (table) => {
/* Columns */
table.text('language');
});
module.exports.down = (knex) =>
knex.schema.table('user_account', (table) => {
table.dropColumn('language');
});

View file

@ -1,11 +0,0 @@
module.exports.up = (knex) =>
knex.schema.table('action', (table) => {
/* Indexes */
table.index('type');
});
module.exports.down = (knex) =>
knex.schema.table('action', (table) => {
table.dropIndex('type');
});

View file

@ -1,11 +0,0 @@
module.exports.up = (knex) =>
knex.schema.table('user_account', (table) => {
/* Columns */
table.timestamp('password_changed_at', true);
});
module.exports.down = (knex) =>
knex.schema.table('user_account', (table) => {
table.dropColumn('password_changed_at');
});

View file

@ -1,18 +0,0 @@
module.exports.up = async (knex) => {
await knex.schema.table('board_membership', (table) => {
/* Columns */
table.text('role').notNullable().defaultTo('editor');
table.boolean('can_comment');
});
return knex.schema.alterTable('board_membership', (table) => {
table.text('role').notNullable().alter();
});
};
module.exports.down = (knex) =>
knex.schema.table('board_membership', (table) => {
table.dropColumn('role');
table.dropColumn('can_comment');
});

View file

@ -1,24 +0,0 @@
module.exports.up = (knex) =>
knex.schema.createTable('session', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('user_id').notNullable();
table.text('access_token').notNullable();
table.text('remote_address').notNullable();
table.text('user_agent');
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
table.timestamp('deleted_at', true);
/* Indexes */
table.index('user_id');
table.unique('access_token');
table.index('remote_address');
});
module.exports.down = (knex) => knex.schema.dropTable('session');

View file

@ -1,4 +0,0 @@
/* Move to new naming by feature */
module.exports.up = () => Promise.resolve();
module.exports.down = () => Promise.resolve();

View file

@ -1,118 +0,0 @@
const path = require('path');
const rimraf = require('rimraf');
const sharp = require('sharp');
const getConfig = require('../../get-config');
const migrateImage = async (knex, tableName, fieldName, prevFieldName) => {
await knex.schema.table(tableName, (table) => {
/* Columns */
table.jsonb(fieldName);
});
await knex(tableName)
.update({
[fieldName]: knex.raw('format(\'{"dirname":"%s","extension":"jpg"}\', ??)::jsonb', [
prevFieldName,
]),
})
.whereNotNull(prevFieldName);
await knex.schema.table(tableName, (table) => {
table.dropColumn(prevFieldName);
});
};
const rollbackImage = async (knex, tableName, fieldName, prevFieldName) => {
await knex.schema.table(tableName, (table) => {
/* Columns */
table.text(prevFieldName);
});
await knex(tableName)
.update({
[prevFieldName]: knex.raw("??->>'dirname'", [fieldName]),
})
.whereNotNull(fieldName);
await knex.schema.table(tableName, (table) => {
table.dropColumn(fieldName);
});
};
const processAttachmentImage = async (attachment, attachmentsPath) => {
const rootPath = path.join(attachmentsPath, attachment.dirname);
const thumbnailsPath = path.join(rootPath, 'thumbnails');
const image = sharp(path.join(rootPath, attachment.filename), {
animated: true,
});
let metadata;
try {
metadata = await image.metadata();
} catch (error) {
return null;
}
const { width, pageHeight: height = metadata.height } = metadata;
const thumbnailsExtension = metadata.format === 'jpeg' ? 'jpg' : metadata.format;
try {
await image
.resize(256, height > width ? 320 : undefined, {
kernel: sharp.kernel.nearest,
})
.toFile(path.join(thumbnailsPath, `cover-256.${thumbnailsExtension}`));
} catch (error) {
return null;
}
if (thumbnailsExtension !== 'jpg') {
try {
rimraf.sync(path.join(thumbnailsPath, 'cover-256.jpg'));
} catch (error) {
console.warn(error.stack); // eslint-disable-line no-console
}
}
return {
width,
height,
thumbnailsExtension,
};
};
module.exports.up = async (knex) => {
await migrateImage(knex, 'user_account', 'avatar', 'avatar_dirname');
await migrateImage(knex, 'project', 'background_image', 'background_image_dirname');
const config = await getConfig();
const attachments = await knex('attachment').whereNotNull('image');
// eslint-disable-next-line no-restricted-syntax
for (const attachment of attachments) {
// eslint-disable-next-line no-await-in-loop
const image = await processAttachmentImage(attachment, config.custom.attachmentsPath);
// eslint-disable-next-line no-await-in-loop
await knex('attachment')
.update({
image: image || knex.raw('?? || \'{"thumbnailsExtension":"jpg"}\'', ['image']),
})
.where('id', attachment.id);
}
};
module.exports.down = async (knex) => {
await rollbackImage(knex, 'user_account', 'avatar', 'avatar_dirname');
await rollbackImage(knex, 'project', 'background_image', 'background_image_dirname');
return knex('attachment')
.update({
image: knex.raw("?? - 'thumbnailsExtension'", ['image']),
})
.whereNotNull('image');
};

View file

@ -1,21 +0,0 @@
module.exports.up = async (knex) => {
await knex.schema.table('board', (table) => {
table.dropColumn('type');
});
return knex.schema.table('card', (table) => {
table.dropNullable('list_id');
});
};
module.exports.down = async (knex) => {
await knex.schema.table('board', (table) => {
/* Columns */
table.text('type').notNullable().defaultTo('kanban'); // FIXME: drop default
});
return knex.schema.table('card', (table) => {
table.setNullable('list_id');
});
};

View file

@ -1,138 +0,0 @@
const path = require('path');
const sharp = require('sharp');
const getConfig = require('../../get-config');
const processUserAvatar = async (user, userAvatarsPath) => {
const rootPath = path.join(userAvatarsPath, user.avatar.dirname);
let image = sharp(path.join(rootPath, `original.${user.avatar.extension}`), {
animated: true,
});
let metadata;
try {
metadata = await image.metadata();
} catch (error) {
return;
}
let { width, pageHeight: height = metadata.height } = metadata;
if (metadata.orientation && metadata.orientation > 4) {
[image, width, height] = [image.rotate(), height, width];
}
try {
await image
.resize(
100,
100,
width < 100 || height < 100
? {
kernel: sharp.kernel.nearest,
}
: undefined,
)
.toFile(path.join(rootPath, `square-100.${user.avatar.extension}`));
} catch (error) {} // eslint-disable-line no-empty
};
const processProjectBackgroundImage = async (project, projectBackgroundImagesPath) => {
const rootPath = path.join(projectBackgroundImagesPath, project.background_image.dirname);
let image = sharp(path.join(rootPath, `original.${project.background_image.extension}`), {
animated: true,
});
let metadata;
try {
metadata = await image.metadata();
} catch (error) {
return;
}
let { width, pageHeight: height = metadata.height } = metadata;
if (metadata.orientation && metadata.orientation > 4) {
[image, width, height] = [image.rotate(), height, width];
}
try {
await image
.resize(
336,
200,
width < 336 || height < 200
? {
kernel: sharp.kernel.nearest,
}
: undefined,
)
.toFile(path.join(rootPath, `cover-336.${project.background_image.extension}`));
} catch (error) {} // eslint-disable-line no-empty
};
const processAttachmentImage = async (attachment, attachmentsPath) => {
const rootPath = path.join(attachmentsPath, attachment.dirname);
const thumbnailsPath = path.join(rootPath, 'thumbnails');
let image = sharp(path.join(rootPath, attachment.filename), {
animated: true,
});
let metadata;
try {
metadata = await image.metadata();
} catch (error) {
return;
}
let { width, pageHeight: height = metadata.height } = metadata;
if (metadata.orientation && metadata.orientation > 4) {
[image, width, height] = [image.rotate(), height, width];
}
const isPortrait = height > width;
try {
await image
.resize(
256,
isPortrait ? 320 : undefined,
width < 256 || (isPortrait && height < 320)
? {
kernel: sharp.kernel.nearest,
}
: undefined,
)
.toFile(path.join(thumbnailsPath, `cover-256.${attachment.image.thumbnailsExtension}`));
} catch (error) {} // eslint-disable-line no-empty
};
module.exports.up = async (knex) => {
const config = await getConfig();
const users = await knex('user_account').whereNotNull('avatar');
// eslint-disable-next-line no-restricted-syntax
for (const user of users) {
// eslint-disable-next-line no-await-in-loop
await processUserAvatar(user, config.custom.userAvatarsPath);
}
const projects = await knex('project').whereNotNull('background_image');
// eslint-disable-next-line no-restricted-syntax
for (const project of projects) {
// eslint-disable-next-line no-await-in-loop
await processProjectBackgroundImage(project, config.custom.projectBackgroundImagesPath);
}
const attachments = await knex('attachment').whereNotNull('image');
// eslint-disable-next-line no-restricted-syntax
for (const attachment of attachments) {
// eslint-disable-next-line no-await-in-loop
await processAttachmentImage(attachment, config.custom.attachmentsPath);
}
};
module.exports.down = () => Promise.resolve();

View file

@ -1,5 +0,0 @@
const { addPosition, removePosition } = require('../../utils/migrations');
module.exports.up = (knex) => addPosition(knex, 'label', 'board_id');
module.exports.down = (knex) => removePosition(knex, 'label');

View file

@ -1,9 +0,0 @@
module.exports.up = (knex) =>
knex.schema.table('card', (table) => {
table.renameColumn('timer', 'stopwatch');
});
module.exports.down = (knex) =>
knex.schema.table('card', (table) => {
table.renameColumn('stopwatch', 'timer');
});

View file

@ -1,44 +0,0 @@
module.exports.up = async (knex) => {
await knex.schema.createTable('identity_provider_user', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('user_id').notNullable();
table.text('issuer').notNullable();
table.text('sub').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.unique(['issuer', 'sub']);
table.index('user_id');
});
await knex.schema.table('user_account', (table) => {
/* Columns */
table.boolean('is_sso').notNullable().default(false);
/* Modifications */
table.setNullable('password');
});
return knex.schema.alterTable('user_account', (table) => {
table.boolean('is_sso').notNullable().alter();
});
};
module.exports.down = async (knex) => {
await knex.schema.dropTable('identity_provider_user');
return knex.schema.table('user_account', (table) => {
table.dropColumn('is_sso');
table.dropNullable('password');
});
};

View file

@ -1,68 +0,0 @@
const _ = require('lodash');
const LANGUAGES = [
'bg-BG',
'cs-CZ',
'da-DK',
'de-DE',
'en-US',
'es-ES',
'fa-IR',
'fr-FR',
'hu-HU',
'id-ID',
'it-IT',
'ja-JP',
'ko-KR',
'nl-NL',
'pl-PL',
'pt-BR',
'ro-RO',
'ru-RU',
'sk-SK',
'sv-SE',
'tr-TR',
'uz-UZ',
'zh-CN',
];
const LANGUAGE_BY_PREV_LANGUAGE = LANGUAGES.reduce(
(result, language) => ({
...result,
[language.split('-')[0]]: language,
}),
{},
);
LANGUAGE_BY_PREV_LANGUAGE.ua = 'uk-UA';
const PREV_LANGUAGE_BY_LANGUAGE = _.invert(LANGUAGE_BY_PREV_LANGUAGE);
module.exports.up = async (knex) => {
const users = await knex('user_account').whereNotNull('language');
const prevLanguages = [...new Set(users.map((user) => user.language))];
// eslint-disable-next-line no-restricted-syntax
for (const prevLanguage of prevLanguages) {
// eslint-disable-next-line no-await-in-loop
await knex('user_account')
.update({
language: LANGUAGE_BY_PREV_LANGUAGE[prevLanguage],
})
.where('language', prevLanguage);
}
};
module.exports.down = async (knex) => {
const users = await knex('user_account').whereNotNull('language');
const languages = [...new Set(users.map((user) => user.language))];
// eslint-disable-next-line no-restricted-syntax
for (const language of languages) {
// eslint-disable-next-line no-await-in-loop
await knex('user_account')
.update({
language: PREV_LANGUAGE_BY_LANGUAGE[language],
})
.where('language', language);
}
};

View file

@ -1,18 +0,0 @@
module.exports.up = async (knex) => {
await knex.schema.table('card', (table) => {
/* Columns */
table.boolean('is_due_date_completed');
});
return knex('card')
.update({
isDueDateCompleted: false,
})
.whereNotNull('due_date');
};
module.exports.down = (knex) =>
knex.schema.table('card', (table) => {
table.dropColumn('is_due_date_completed');
});

View file

@ -1,11 +0,0 @@
module.exports.up = async (knex) =>
knex.schema.table('session', (table) => {
/* Columns */
table.text('http_only_token');
});
module.exports.down = (knex) =>
knex.schema.table('session', (table) => {
table.dropColumn('http_only_token');
});

View file

@ -1,17 +0,0 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = (knex) =>
knex.schema.alterTable('list', (table) => {
table.text('color');
});
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = (knex) =>
knex.schema.alterTable('list', (table) => {
table.dropColumn('color');
});

View file

@ -0,0 +1,661 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
module.exports.up = async (knex) => {
await knex.raw(`
CREATE EXTENSION pg_trgm;
CREATE SEQUENCE next_id_seq;
CREATE FUNCTION next_id(OUT id BIGINT) AS $$
DECLARE
shard INT := 1;
epoch BIGINT := 1567191600000;
sequence BIGINT;
milliseconds BIGINT;
BEGIN
SELECT nextval('next_id_seq') % 1024 INTO sequence;
SELECT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000) INTO milliseconds;
id := (milliseconds - epoch) << 23;
id := id | (shard << 10);
id := id | (sequence);
END;
$$ LANGUAGE PLPGSQL;
`);
await knex.schema.createTable('file_reference', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.integer('total');
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('total');
});
await knex.schema
.createTable('user_account', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.text('email').notNullable();
table.text('password');
table.text('role').notNullable();
table.text('name').notNullable();
table.text('username');
table.jsonb('avatar');
table.text('phone');
table.text('organization');
table.text('language');
table.boolean('subscribe_to_own_cards').notNullable();
table.boolean('subscribe_to_card_when_commenting').notNullable();
table.boolean('turn_off_recent_card_highlighting').notNullable();
table.boolean('enable_favorites_by_default').notNullable();
table.text('default_editor_mode').notNullable();
table.text('default_home_view').notNullable();
table.text('default_projects_order').notNullable();
table.boolean('is_sso_user').notNullable();
table.boolean('is_deactivated').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
table.timestamp('password_changed_at', true);
/* Indexes */
table.unique('email');
table.index('role');
table.index('username');
table.index('is_deactivated');
})
.raw(
'ALTER TABLE user_account ADD CONSTRAINT user_account_username_unique EXCLUDE (username WITH =) WHERE (username IS NOT NULL);',
);
await knex.schema.createTable('identity_provider_user', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('user_id').notNullable();
table.text('issuer').notNullable();
table.text('sub').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.unique(['issuer', 'sub']);
table.index('user_id');
});
await knex.schema.createTable('session', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('user_id').notNullable();
table.text('access_token').notNullable();
table.text('http_only_token');
table.text('remote_address').notNullable();
table.text('user_agent');
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
table.timestamp('deleted_at', true);
/* Indexes */
table.index('user_id');
table.unique('access_token');
table.index('remote_address');
});
await knex.schema.createTable('project', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('owner_project_manager_id');
table.bigInteger('background_image_id');
table.text('name').notNullable();
table.text('description');
table.text('background_type');
table.text('background_gradient');
table.boolean('is_hidden').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('owner_project_manager_id');
});
await knex.schema.createTable('project_favorite', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('project_id').notNullable();
table.bigInteger('user_id').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.unique(['project_id', 'user_id']);
table.index('user_id');
});
await knex.schema.createTable('project_manager', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('project_id').notNullable();
table.bigInteger('user_id').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.unique(['project_id', 'user_id']);
table.index('user_id');
});
await knex.schema.createTable('background_image', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('project_id').notNullable();
table.text('dirname').notNullable();
table.text('extension').notNullable();
table.bigInteger('size_in_bytes').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('project_id');
});
await knex.schema.createTable('base_custom_field_group', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('project_id').notNullable();
table.text('name').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('project_id');
});
await knex.schema.createTable('board', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('project_id').notNullable();
table.specificType('position', 'double precision').notNullable();
table.text('name').notNullable();
table.text('default_view').notNullable();
table.text('default_card_type').notNullable();
table.boolean('limit_card_types_to_default_one').notNullable();
table.boolean('always_display_card_creator').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('project_id');
table.index('position');
});
await knex.schema.createTable('board_subscription', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('board_id').notNullable();
table.bigInteger('user_id').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.unique(['board_id', 'user_id']);
table.index('user_id');
});
await knex.schema.createTable('board_membership', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('project_id').notNullable();
table.bigInteger('board_id').notNullable();
table.bigInteger('user_id').notNullable();
table.text('role').notNullable();
table.boolean('can_comment');
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('project_id');
table.unique(['board_id', 'user_id']);
table.index('user_id');
});
await knex.schema.createTable('label', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('board_id').notNullable();
table.specificType('position', 'double precision').notNullable();
table.text('name');
table.text('color').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('board_id');
table.index('position');
});
await knex.schema.createTable('list', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('board_id').notNullable();
table.text('type').notNullable();
table.specificType('position', 'double precision');
table.text('name');
table.text('color');
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('board_id');
table.index('type');
table.index('position');
});
await knex.schema.createTable('card', async (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('board_id').notNullable();
table.bigInteger('list_id').notNullable();
table.bigInteger('creator_user_id');
table.bigInteger('prev_list_id');
table.bigInteger('cover_attachment_id');
table.text('type').notNullable();
table.specificType('position', 'double precision');
table.text('name').notNullable();
table.text('description');
table.timestamp('due_date', true);
table.jsonb('stopwatch');
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
table.timestamp('list_changed_at', true);
/* Indexes */
table.index('board_id');
table.index('list_id');
table.index('creator_user_id');
table.index('position');
table.index('list_changed_at');
}).raw(`
CREATE INDEX card_name_index ON card USING GIN (name gin_trgm_ops);
CREATE INDEX card_description_index ON card USING GIN (description gin_trgm_ops);
`);
await knex.schema.createTable('card_subscription', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('card_id').notNullable();
table.bigInteger('user_id').notNullable();
table.boolean('is_permanent').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.unique(['card_id', 'user_id']);
table.index('user_id');
});
await knex.schema.createTable('card_membership', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('card_id').notNullable();
table.bigInteger('user_id').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.unique(['card_id', 'user_id']);
table.index('user_id');
});
await knex.schema.createTable('card_label', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('card_id').notNullable();
table.bigInteger('label_id').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.unique(['card_id', 'label_id']);
table.index('label_id');
});
await knex.schema.createTable('task_list', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('card_id').notNullable();
table.specificType('position', 'double precision').notNullable();
table.text('name').notNullable();
table.boolean('show_on_front_of_card').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('card_id');
table.index('position');
});
await knex.schema.createTable('task', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('task_list_id').notNullable();
table.bigInteger('assignee_user_id');
table.specificType('position', 'double precision').notNullable();
table.text('name').notNullable();
table.boolean('is_completed').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('task_list_id');
table.index('assignee_user_id');
table.index('position');
});
await knex.schema.createTable('attachment', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('card_id').notNullable();
table.bigInteger('creator_user_id');
table.text('type').notNullable();
table.jsonb('data').notNullable();
table.text('name').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('card_id');
table.index('creator_user_id');
});
await knex.schema.createTable('custom_field_group', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('board_id');
table.bigInteger('card_id');
table.bigInteger('base_custom_field_group_id');
table.specificType('position', 'double precision').notNullable();
table.text('name');
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('board_id');
table.index('card_id');
table.index('base_custom_field_group_id');
table.index('position');
});
await knex.schema.createTable('custom_field', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('base_custom_field_group_id');
table.bigInteger('custom_field_group_id');
table.specificType('position', 'double precision').notNullable();
table.text('name').notNullable();
table.boolean('show_on_front_of_card').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('base_custom_field_group_id');
table.index('custom_field_group_id');
table.index('position');
});
await knex.schema.createTable('custom_field_value', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('card_id').notNullable();
table.bigInteger('custom_field_group_id').notNullable();
table.bigInteger('custom_field_id').notNullable();
table.text('content').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.unique(['card_id', 'custom_field_group_id', 'custom_field_id']);
table.index('custom_field_group_id');
table.index('custom_field_id');
});
await knex.schema.createTable('comment', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('card_id').notNullable();
table.bigInteger('user_id');
table.text('text').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('card_id');
table.index('user_id');
});
await knex.schema.createTable('action', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('card_id').notNullable();
table.bigInteger('user_id');
table.text('type').notNullable();
table.jsonb('data').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('card_id');
table.index('user_id');
});
await knex.schema.createTable('notification', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('user_id').notNullable();
table.bigInteger('creator_user_id');
table.bigInteger('board_id').notNullable();
table.bigInteger('card_id').notNullable();
table.bigInteger('comment_id');
table.bigInteger('action_id');
table.text('type').notNullable();
table.jsonb('data').notNullable();
table.boolean('is_read').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('user_id');
table.index('creator_user_id');
table.index('card_id');
table.index('comment_id');
table.index('action_id');
table.index('is_read');
});
return knex.schema.createTable('notification_service', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('user_id');
table.bigInteger('board_id');
table.text('url').notNullable();
table.text('format').notNullable();
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
/* Indexes */
table.index('user_id');
table.index('board_id');
});
};
module.exports.down = async (knex) => {
await knex.schema.dropTable('file_reference');
await knex.schema.dropTable('user_account');
await knex.schema.dropTable('identity_provider_user');
await knex.schema.dropTable('session');
await knex.schema.dropTable('project');
await knex.schema.dropTable('project_favorite');
await knex.schema.dropTable('project_manager');
await knex.schema.dropTable('background_image');
await knex.schema.dropTable('base_custom_field_group');
await knex.schema.dropTable('board');
await knex.schema.dropTable('board_subscription');
await knex.schema.dropTable('board_membership');
await knex.schema.dropTable('label');
await knex.schema.dropTable('list');
await knex.schema.dropTable('card');
await knex.schema.dropTable('card_subscription');
await knex.schema.dropTable('card_membership');
await knex.schema.dropTable('card_label');
await knex.schema.dropTable('task_list');
await knex.schema.dropTable('task');
await knex.schema.dropTable('attachment');
await knex.schema.dropTable('custom_field_group');
await knex.schema.dropTable('custom_field');
await knex.schema.dropTable('custom_field_value');
await knex.schema.dropTable('comment');
await knex.schema.dropTable('action');
await knex.schema.dropTable('notification');
await knex.schema.dropTable('notification_service');
return knex.raw(`
DROP EXTENSION pg_trgm;
DROP SEQUENCE next_id_seq;
DROP FUNCTION next_id(OUT id BIGINT);
`);
};