3460 lines
144 KiB
PHP
3460 lines
144 KiB
PHP
<?php
|
||
|
||
namespace Leantime\Domain\Install\Repositories;
|
||
|
||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||
use Illuminate\Database\ConnectionInterface;
|
||
use Illuminate\Database\DatabaseManager;
|
||
use Illuminate\Database\Schema\Blueprint;
|
||
use Illuminate\Support\Facades\Cache;
|
||
use Illuminate\Support\Facades\Log;
|
||
use Illuminate\Support\Facades\Schema;
|
||
use Illuminate\Support\Str;
|
||
use Leantime\Core\Configuration\AppSettings as AppSettingCore;
|
||
use Leantime\Core\Configuration\Environment;
|
||
use Leantime\Core\Events\DispatchesEvents;
|
||
use Leantime\Core\Support\EntityRelationshipEnum;
|
||
use Leantime\Domain\Install\Services\SchemaBuilder;
|
||
use Leantime\Domain\Menu\Repositories\Menu as MenuRepository;
|
||
use Leantime\Domain\Setting\Repositories\Setting;
|
||
use Leantime\Domain\Setting\Services\SettingCache;
|
||
use PDO;
|
||
use PDOException;
|
||
|
||
class Install
|
||
{
|
||
use DispatchesEvents;
|
||
|
||
public string $name;
|
||
|
||
public int $id;
|
||
|
||
/**
|
||
* Laravel database connection interface
|
||
*/
|
||
private ConnectionInterface $connection;
|
||
|
||
/**
|
||
* Laravel database manager for creating connections
|
||
*/
|
||
private DatabaseManager $dbManager;
|
||
|
||
/**
|
||
* db update scripts listed out by version number with leading zeros A.BB.CC => ABBCC
|
||
*/
|
||
private array $dbUpdates = [
|
||
20004,
|
||
20100,
|
||
20101,
|
||
20102,
|
||
20103,
|
||
20104,
|
||
20105,
|
||
20106,
|
||
20107,
|
||
20108,
|
||
20109,
|
||
20110,
|
||
20111,
|
||
20112,
|
||
20113,
|
||
20114,
|
||
20115,
|
||
20116,
|
||
20117,
|
||
20118,
|
||
20120,
|
||
20121,
|
||
20122,
|
||
20401,
|
||
20402,
|
||
20405,
|
||
20406,
|
||
20407,
|
||
30002,
|
||
30003,
|
||
30400,
|
||
30408,
|
||
30409,
|
||
30410,
|
||
30411,
|
||
30412,
|
||
30413,
|
||
30500,
|
||
30501,
|
||
30502,
|
||
// 30503 (zp_device_tokens) intentionally skipped — superseded by
|
||
// 30504 which puts push columns on zp_access_tokens instead.
|
||
30504,
|
||
// 30505–30517 (the native-permission-engine rollout: table creation + the per-domain
|
||
// re-seeds, added one PR at a time) were consolidated into the single 30518 migration.
|
||
// Pre-release, so no installed DB ran the intermediate versions; fresh installs are
|
||
// covered by SchemaBuilder + setupDB().
|
||
30518,
|
||
30519,
|
||
30520,
|
||
30521,
|
||
30522,
|
||
30523,
|
||
30524,
|
||
30525,
|
||
30526,
|
||
30527,
|
||
30528,
|
||
30529,
|
||
];
|
||
|
||
/**
|
||
* config object, passed into constructor
|
||
*/
|
||
private Environment|string $config;
|
||
|
||
/**
|
||
* appSettings object, passed into constructor
|
||
*/
|
||
private string|AppSettingCore $settings;
|
||
|
||
/**
|
||
* __construct - get database connection using Laravel's database manager
|
||
*/
|
||
public function __construct(
|
||
Environment $config,
|
||
AppSettingCore $settings,
|
||
DatabaseManager $dbManager
|
||
) {
|
||
// Some scripts might take a long time to execute. Set timeout to 5minutes
|
||
ini_set('max_execution_time', 300);
|
||
|
||
$this->config = $config;
|
||
$this->settings = $settings;
|
||
$this->dbManager = $dbManager;
|
||
|
||
// Use Laravel's database connection for consistency with the rest of the application
|
||
// Detect the configured database driver (defaults to 'mysql')
|
||
$defaultConnection = config('database.default', 'mysql');
|
||
|
||
try {
|
||
$this->connection = $this->dbManager->connection($defaultConnection);
|
||
} catch (\Exception $e) {
|
||
Log::error('Failed to establish database connection during installation: '.$e->getMessage());
|
||
// During installation, we may need to create a temporary connection without database selection
|
||
$this->createTemporaryConnection();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* returns current database object
|
||
*/
|
||
public function getDBObject(): ?PDO
|
||
{
|
||
return $this->connection->getPdo();
|
||
}
|
||
|
||
/**
|
||
* Create a temporary database connection for installation purposes
|
||
* This is used when the main database connection fails during installation
|
||
* Supports both MySQL and PostgreSQL
|
||
*/
|
||
private function createTemporaryConnection(): void
|
||
{
|
||
try {
|
||
$driver = config('database.default', 'mysql');
|
||
|
||
if ($driver === 'pgsql') {
|
||
// PostgreSQL temporary connection
|
||
$config = [
|
||
'driver' => 'pgsql',
|
||
'host' => $this->config->dbHost,
|
||
'port' => $this->config->dbPort ?? 5432,
|
||
'database' => 'postgres', // Connect to default postgres database
|
||
'username' => $this->config->dbUser,
|
||
'password' => $this->config->dbPassword,
|
||
'charset' => 'utf8',
|
||
'prefix' => '',
|
||
'search_path' => 'public',
|
||
'sslmode' => env('LEAN_DB_SSLMODE', 'prefer'),
|
||
'options' => [
|
||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||
],
|
||
];
|
||
} else {
|
||
// MySQL temporary connection (default)
|
||
$config = [
|
||
'driver' => 'mysql',
|
||
'host' => $this->config->dbHost,
|
||
'port' => $this->config->dbPort ?? 3306,
|
||
'username' => $this->config->dbUser,
|
||
'password' => $this->config->dbPassword,
|
||
'charset' => 'utf8mb4',
|
||
'collation' => 'utf8mb4_unicode_ci',
|
||
'options' => [
|
||
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4,sql_mode="NO_ENGINE_SUBSTITUTION"',
|
||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||
],
|
||
];
|
||
}
|
||
|
||
// Use Laravel's DB manager to create a temporary connection
|
||
config(['database.connections.install_temp' => $config]);
|
||
$this->connection = $this->dbManager->connection('install_temp');
|
||
} catch (\Exception $e) {
|
||
Log::error('Failed to create temporary database connection: '.$e->getMessage());
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* checkIfInstalled checks if zp user table exists (and assumes that leantime is installed)
|
||
*/
|
||
public function checkIfInstalled(): bool
|
||
{
|
||
|
||
if (Cache::has('isInstalled') && Cache::get('isInstalled') === true) {
|
||
return true;
|
||
}
|
||
|
||
try {
|
||
|
||
// Select the database - MySQL uses USE, PostgreSQL uses search_path
|
||
$this->selectDatabase($this->config->dbDatabase);
|
||
|
||
$count = $this->connection->table('zp_user')->count();
|
||
|
||
Cache::set('isInstalled', true);
|
||
|
||
return true;
|
||
} catch (PDOException $e) {
|
||
|
||
Cache::forget('isInstalled');
|
||
|
||
Log::error($e);
|
||
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Select/switch to a specific database
|
||
* Handles database-specific syntax for MySQL vs PostgreSQL
|
||
*/
|
||
private function selectDatabase(string $database): void
|
||
{
|
||
$driver = $this->connection->getDriverName();
|
||
|
||
if ($driver === 'pgsql') {
|
||
// PostgreSQL: reconnect with the correct database
|
||
// Since we can't "USE" a database in PostgreSQL like MySQL,
|
||
// the database is specified at connection time
|
||
$this->connection->getPdo()->exec('SET search_path TO public');
|
||
} else {
|
||
// MySQL: USE statement
|
||
$this->connection->statement('USE `'.$database.'`');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* setupDB installs database using database-agnostic Schema Builder
|
||
*
|
||
* @param array $values Form values for admin user and company information
|
||
* @param string $db
|
||
*/
|
||
public function setupDB(array $values, $db = ''): bool
|
||
{
|
||
try {
|
||
// Select the database
|
||
$dbName = $db ?: $this->config->dbDatabase;
|
||
$this->selectDatabase($dbName);
|
||
|
||
$pwReset = Str::random(32);
|
||
session()->put('pwReset', $pwReset);
|
||
|
||
// Use SchemaBuilder for database-agnostic table creation
|
||
$schemaBuilder = app()->make(SchemaBuilder::class);
|
||
$schemaBuilder->createAllTables();
|
||
$schemaBuilder->insertInitialData($values, $pwReset);
|
||
|
||
// Seed the permission engine for fresh installs: sync the domain-declared
|
||
// vocabulary into zp_permissions and grant the built-in roles their defaults.
|
||
// (update_sql_* seeds existing installs; this covers the fresh-install path.)
|
||
$permissionSeeder = app()->make(\Leantime\Core\Auth\Permissions\PermissionSeeder::class);
|
||
$permissionSeeder->syncDiscoveredPermissions();
|
||
$permissionSeeder->seedBuiltInRoles();
|
||
|
||
return true;
|
||
} catch (\Exception $e) {
|
||
Log::error($e);
|
||
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* updateDB main entry point to update the db based on version number. Executes all missing db update scripts
|
||
*
|
||
* @throws BindingResolutionException
|
||
*/
|
||
public function updateDB(): array|bool
|
||
{
|
||
|
||
// Forget all the versions we think we know and start fresh
|
||
session()->forget('db-version');
|
||
$settingsCacheService = app()->make(SettingCache::class);
|
||
$settingsCacheService->forget('db-version');
|
||
|
||
$errors = [];
|
||
|
||
$this->selectDatabase($this->config->dbDatabase);
|
||
|
||
$versionArray = explode('.', $this->settings->dbVersion);
|
||
if (is_array($versionArray) && count($versionArray) == 3) {
|
||
$major = $versionArray[0];
|
||
$minor = str_pad($versionArray[1], 2, '0', STR_PAD_LEFT);
|
||
$patch = str_pad($versionArray[2], 2, '0', STR_PAD_LEFT);
|
||
$newDBVersion = $major.$minor.$patch;
|
||
} else {
|
||
$errors[0] = 'Problem identifying the version number';
|
||
|
||
return $errors;
|
||
}
|
||
|
||
$setting = app()->make(Setting::class);
|
||
$dbVersion = $setting->getSetting('db-version');
|
||
$currentDBVersion = 0;
|
||
if ($dbVersion) {
|
||
$versionArray = explode('.', $dbVersion);
|
||
if (is_array($versionArray) && count($versionArray) == 3) {
|
||
$major = $versionArray[0];
|
||
$minor = str_pad($versionArray[1], 2, '0', STR_PAD_LEFT);
|
||
$patch = str_pad($versionArray[2], 2, '0', STR_PAD_LEFT);
|
||
$currentDBVersion = $major.$minor.$patch;
|
||
} else {
|
||
$errors[0] = 'Problem identifying the version number';
|
||
|
||
return $errors;
|
||
}
|
||
}
|
||
|
||
if ($currentDBVersion == $newDBVersion) {
|
||
|
||
session()->forget('isUpdated');
|
||
session()->forget('dbVersion');
|
||
|
||
return true;
|
||
}
|
||
|
||
// The version changed: cached routes, plugin lists etc. may reference classes or
|
||
// methods that no longer exist in the new codebase. Everything in the installation
|
||
// store is re-derivable, so drop it wholesale before running the updates.
|
||
Cache::store('installation')->clear();
|
||
|
||
// Find all update functions that need to be executed
|
||
foreach ($this->dbUpdates as $updateVersion) {
|
||
if ($currentDBVersion < $updateVersion) {
|
||
$functionName = 'update_sql_'.$updateVersion;
|
||
|
||
$result = $this->$functionName();
|
||
|
||
if ($result !== true) {
|
||
$errors = array_merge($errors, $result);
|
||
} else {
|
||
// Update version number in db
|
||
try {
|
||
|
||
$settingsService = app()->make(\Leantime\Domain\Setting\Services\Setting::class);
|
||
$settingsService->saveSetting('db-version', $this->convert_version($updateVersion));
|
||
|
||
// $stmn = $this->database->prepare("INSERT INTO zp_settings (`key`, `value`) VALUES ('db-version', '".$this->settings->dbVersion."') ON DUPLICATE KEY UPDATE `value` = '".$this->settings->dbVersion."'");
|
||
// $stmn->execute();
|
||
|
||
$currentDBVersion = $updateVersion;
|
||
} catch (PDOException $e) {
|
||
Log::error($e);
|
||
Log::error($e->getTraceAsString());
|
||
|
||
return ['There was a problem updating the database'];
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
}
|
||
}
|
||
}
|
||
|
||
session()->forget('isUpdated');
|
||
session()->forget('dbVersion');
|
||
|
||
return true;
|
||
}
|
||
|
||
private function convert_version($inputVersion): string
|
||
{
|
||
// $inputVersion is in the format of 10000 and needs to be converted to 1.0.0
|
||
$versionString = str_pad((string) $inputVersion, 5, '0', STR_PAD_LEFT);
|
||
|
||
$major = intval(substr($versionString, 0, -4));
|
||
$minor = intval(substr($versionString, -4, 2));
|
||
$patch = intval(substr($versionString, -2));
|
||
|
||
return $major.'.'.$minor.'.'.$patch;
|
||
}
|
||
|
||
/**
|
||
* sqlPrep - returns all the create table statements
|
||
*/
|
||
private function sqlPrep(): string
|
||
{
|
||
|
||
$sql = "
|
||
CREATE TABLE `zp_calendar` (
|
||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||
`userId` int(11) DEFAULT NULL,
|
||
`dateFrom` datetime DEFAULT NULL,
|
||
`dateTo` datetime DEFAULT NULL,
|
||
`description` text,
|
||
`kind` varchar(255) DEFAULT NULL,
|
||
`allDay` varchar(10) DEFAULT NULL,
|
||
PRIMARY KEY (`id`),
|
||
KEY `idx_calendar_userId_dateFrom_dateTo` (`userId`, `dateFrom`, `dateTo`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_canvas` (
|
||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||
`title` varchar(255) DEFAULT NULL,
|
||
`author` int(10) DEFAULT NULL,
|
||
`created` datetime DEFAULT NULL,
|
||
`projectId` INT NULL,
|
||
`type` VARCHAR(45) NULL,
|
||
`description` TEXT,
|
||
`color` VARCHAR(50) DEFAULT 'ocean',
|
||
`modified` datetime DEFAULT NULL,
|
||
PRIMARY KEY (`id`),
|
||
KEY `ProjectIdType` (`projectId` ASC, `type` ASC),
|
||
KEY `idx_canvas_type_id` (`type`, `id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_canvas_items` (
|
||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||
`description` MEDIUMTEXT,
|
||
`assumptions` text,
|
||
`data` MEDIUMTEXT,
|
||
`conclusion` text,
|
||
`why_this_matters` text,
|
||
`starting_picture` text,
|
||
`box` varchar(255) DEFAULT NULL,
|
||
`author` int(11) DEFAULT NULL,
|
||
`created` datetime DEFAULT NULL,
|
||
`modified` datetime DEFAULT NULL,
|
||
`canvasId` int(11) DEFAULT NULL,
|
||
`sortindex` int(11) DEFAULT NULL,
|
||
`status` varchar(255) DEFAULT NULL,
|
||
`relates` varchar(255) DEFAULT NULL,
|
||
`milestoneId` VARCHAR(255) NULL,
|
||
`title` varchar(255) NULL,
|
||
`parent` int NULL,
|
||
`featured` int NULL,
|
||
`tags` text NULL,
|
||
`kpi` INT NULL DEFAULT NULL,
|
||
`data1` MEDIUMTEXT NULL DEFAULT NULL,
|
||
`data2` MEDIUMTEXT NULL DEFAULT NULL,
|
||
`data3` MEDIUMTEXT NULL DEFAULT NULL,
|
||
`data4` MEDIUMTEXT NULL DEFAULT NULL,
|
||
`data5` MEDIUMTEXT NULL DEFAULT NULL,
|
||
`startDate` DATETIME NULL DEFAULT NULL,
|
||
`endDate` DATETIME NULL DEFAULT NULL,
|
||
`setting` TEXT NULL DEFAULT NULL,
|
||
`metricType` VARCHAR(45) DEFAULT NULL,
|
||
`startValue` double(10,2) NULL DEFAULT NULL,
|
||
`currentValue` double(10,2) NULL DEFAULT NULL,
|
||
`endValue` double(10,2) NULL DEFAULT NULL,
|
||
`impact` INT NULL DEFAULT NULL,
|
||
`effort` INT NULL DEFAULT NULL,
|
||
`probability` INT NULL DEFAULT NULL,
|
||
`action` TEXT NULL DEFAULT NULL,
|
||
`assignedTo` INT NULL DEFAULT NULL,
|
||
PRIMARY KEY (`id`),
|
||
KEY `CanvasLookUp` (`canvasId` ASC, `box` ASC),
|
||
KEY `idx_canvas_items_box_milestoneId` (`box`, `milestoneId`),
|
||
KEY `idx_canvas_items_box_status_author` (`box`, `status`, `author`),
|
||
KEY `idx_canvas_items_parent_title` (`parent`, `title`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_approvals` (
|
||
`id` int auto_increment,
|
||
`module` varchar(100) NULL,
|
||
`entityId` int NULL,
|
||
`requestorId` int NULL,
|
||
`approverId` int NULL,
|
||
`approvalStatus` int NULL,
|
||
`requestedOn` datetime NULL,
|
||
`lastStatusChange` datetime NULL,
|
||
PRIMARY KEY (`id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_clients` (
|
||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||
`name` varchar(200) DEFAULT NULL,
|
||
`street` varchar(200) DEFAULT NULL,
|
||
`zip` int(10) DEFAULT NULL,
|
||
`city` varchar(50) DEFAULT NULL,
|
||
`state` varchar(50) DEFAULT NULL,
|
||
`country` varchar(50) DEFAULT NULL,
|
||
`phone` varchar(50) DEFAULT NULL,
|
||
`internet` varchar(200) DEFAULT NULL,
|
||
`published` int(1) DEFAULT NULL,
|
||
`age` int(3) DEFAULT NULL,
|
||
`email` varchar(255) DEFAULT NULL,
|
||
`modified` datetime DEFAULT NULL,
|
||
PRIMARY KEY (`id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
INSERT INTO `zp_clients`(`id`,`name`,`street`,`zip`,`city`,`state`,`country`,`phone`,`internet`,`published`,`age`,`email`) VALUES (1,:company,'',0,'','','','','',NULL,NULL,'');
|
||
|
||
CREATE TABLE `zp_comment` (
|
||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||
`module` varchar(200) DEFAULT NULL,
|
||
`userId` int(11) DEFAULT NULL,
|
||
`commentParent` int(11) DEFAULT NULL,
|
||
`date` datetime DEFAULT NULL,
|
||
`moduleId` int(11) DEFAULT NULL,
|
||
`text` text,
|
||
`status` varchar(50) null,
|
||
PRIMARY KEY (`id`),
|
||
KEY `idx_comment_moduleId_module_commentParent` (`moduleId`, `module`, `commentParent`),
|
||
KEY `idx_comment_userId_module` (`userId`, `module`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_file` (
|
||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||
`module` enum('project','ticket','client','user','lead','export','private') DEFAULT NULL,
|
||
`moduleId` int(11) DEFAULT NULL,
|
||
`userId` int(11) DEFAULT NULL,
|
||
`extension` varchar(10) DEFAULT NULL,
|
||
`encName` varchar(255) DEFAULT NULL,
|
||
`realName` varchar(255) DEFAULT NULL,
|
||
`date` datetime DEFAULT NULL,
|
||
PRIMARY KEY (`id`),
|
||
KEY `idx_file_module_moduleId_userId` (`module`, `moduleId`, `userId`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_gcallinks` (
|
||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||
`userId` int(255) DEFAULT NULL,
|
||
`url` text,
|
||
`name` varchar(255) DEFAULT NULL,
|
||
`colorClass` varchar(100) DEFAULT NULL,
|
||
PRIMARY KEY (`id`),
|
||
KEY `idx_gcallinks_userId` (`userId`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_note` (
|
||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||
`userId` int(11) DEFAULT NULL,
|
||
`title` varchar(255) DEFAULT NULL,
|
||
`description` text,
|
||
PRIMARY KEY (`id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_projects` (
|
||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||
`name` varchar(100) DEFAULT NULL,
|
||
`clientId` int(100) DEFAULT NULL,
|
||
`details` text,
|
||
`state` int(2) DEFAULT NULL,
|
||
`hourBudget` varchar(255) NOT NULL,
|
||
`dollarBudget` int(11) DEFAULT NULL,
|
||
`active` int(11) DEFAULT NULL,
|
||
`menuType` MEDIUMTEXT DEFAULT NULL,
|
||
`psettings` MEDIUMTEXT NULL,
|
||
`parent` INT(11) NULL,
|
||
`type` VARCHAR(45) NULL,
|
||
`start` DATETIME NULL,
|
||
`end` DATETIME NULL,
|
||
`created` DATETIME NULL,
|
||
`modified` DATETIME NULL,
|
||
`avatar` MEDIUMTEXT NULL ,
|
||
`cover` MEDIUMTEXT NULL,
|
||
`sortIndex` INT(11) NULL,
|
||
PRIMARY KEY (`id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_punch_clock` (
|
||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||
`userId` int(11) NOT NULL,
|
||
`minutes` int(11) DEFAULT NULL,
|
||
`hours` int(11) DEFAULT NULL,
|
||
`punchIn` int(11) DEFAULT NULL,
|
||
PRIMARY KEY (`id`,`userId`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_read` (
|
||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||
`module` enum('ticket','message') DEFAULT NULL,
|
||
`moduleId` int(11) DEFAULT NULL,
|
||
`userId` int(11) DEFAULT NULL,
|
||
PRIMARY KEY (`id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_relationuserproject` (
|
||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||
`userId` int(11) DEFAULT NULL,
|
||
`projectId` int(11) DEFAULT NULL,
|
||
`wage` int(11) DEFAULT NULL,
|
||
`projectRole` varchar(20),
|
||
PRIMARY KEY (`id`),
|
||
KEY zp_relationuserproject_projectId_index (`projectId`),
|
||
KEY zp_relationuserproject_userId_index (`userId`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_tickethistory` (
|
||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||
`userId` int(11) DEFAULT NULL,
|
||
`ticketId` int(11) DEFAULT NULL,
|
||
`changeType` varchar(255) DEFAULT NULL,
|
||
`changeValue` varchar(150) DEFAULT NULL,
|
||
`dateModified` datetime DEFAULT NULL,
|
||
PRIMARY KEY (`id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_goal_history` (
|
||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||
`itemId` int(11) NOT NULL,
|
||
`value` double DEFAULT NULL,
|
||
`userId` int(11) DEFAULT NULL,
|
||
`dateRecorded` datetime DEFAULT NULL,
|
||
PRIMARY KEY (`id`),
|
||
KEY `idx_goal_history_item_date` (`itemId`,`dateRecorded`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_tickets` (
|
||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||
`projectId` int(11) DEFAULT NULL,
|
||
`headline` varchar(255) DEFAULT NULL,
|
||
`description` text,
|
||
`acceptanceCriteria` text,
|
||
`outcomeImpact` text,
|
||
`date` datetime DEFAULT NULL,
|
||
`dateToFinish` datetime DEFAULT NULL,
|
||
`priority` varchar(60) DEFAULT NULL,
|
||
`status` int(2) DEFAULT NULL,
|
||
`userId` int(11) DEFAULT NULL,
|
||
`os` varchar(30) DEFAULT NULL,
|
||
`browser` varchar(30) DEFAULT NULL,
|
||
`resolution` varchar(30) DEFAULT NULL,
|
||
`component` varchar(100) DEFAULT NULL,
|
||
`version` varchar(20) DEFAULT NULL,
|
||
`url` varchar(100) DEFAULT NULL,
|
||
`dependingTicketId` int(100) DEFAULT NULL,
|
||
`editFrom` datetime DEFAULT NULL,
|
||
`editTo` datetime DEFAULT NULL,
|
||
`editorId` varchar(75) DEFAULT NULL,
|
||
`planHours` float DEFAULT NULL,
|
||
`hourRemaining` float DEFAULT NULL,
|
||
`type` varchar(255) DEFAULT NULL,
|
||
`production` int(1) DEFAULT '0',
|
||
`staging` int(1) DEFAULT '0',
|
||
`storypoints` float DEFAULT NULL,
|
||
`sprint` int(100) DEFAULT NULL,
|
||
`sortindex` bigint(20) DEFAULT NULL,
|
||
`kanbanSortIndex` bigint(20) DEFAULT NULL,
|
||
`tags` varchar(255) DEFAULT NULL,
|
||
`milestoneid` INT NULL,
|
||
`leancanvasitemid` INT NULL,
|
||
`retrospectiveid` INT NULL,
|
||
`ideaid` INT NULL,
|
||
`zp_ticketscol` VARCHAR(45) NULL,
|
||
`modified` datetime DEFAULT NULL,
|
||
PRIMARY KEY (`id`),
|
||
KEY `ProjectUserId` (`projectId`,`userId`),
|
||
KEY `StatusSprint` (`status`,`sprint`),
|
||
KEY `Sorting` (`sortindex`),
|
||
KEY `idx_tickets_editorId` (`editorId`),
|
||
KEY `idx_tickets_milestoneid` (`milestoneid`),
|
||
KEY `idx_tickets_editFrom` (`editFrom`),
|
||
KEY `idx_tickets_editTo` (`editTo`),
|
||
KEY `idx_tickets_dateToFinish` (`dateToFinish`),
|
||
KEY `idx_tickets_modified` (`modified`),
|
||
KEY `idx_tickets_projectId_status` (`projectId`, `status`),
|
||
KEY `idx_tickets_projectId_type` (`projectId`, `type`),
|
||
KEY `idx_tickets_status_type` (`status`, `type`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_timesheets` (
|
||
`id` int(255) NOT NULL AUTO_INCREMENT,
|
||
`userId` int(11) DEFAULT NULL,
|
||
`ticketId` int(11) DEFAULT NULL,
|
||
`workDate` datetime DEFAULT NULL,
|
||
`hours` float DEFAULT NULL,
|
||
`description` text,
|
||
`kind` varchar(175) DEFAULT NULL,
|
||
`invoicedEmpl` int(2) DEFAULT NULL,
|
||
`invoicedComp` int(2) DEFAULT NULL,
|
||
`invoicedEmplDate` datetime DEFAULT NULL,
|
||
`invoicedCompDate` datetime DEFAULT NULL,
|
||
`rate` varchar(255) DEFAULT NULL,
|
||
`paid` int(2) DEFAULT NULL,
|
||
`paidDate` datetime DEFAULT NULL,
|
||
`modified` datetime DEFAULT NULL,
|
||
PRIMARY KEY (`id`),
|
||
UNIQUE KEY `Unique` (`userId`,`ticketId`,`workDate`,`kind`),
|
||
KEY `idx_timesheets_ticketId` (`ticketId`),
|
||
KEY `idx_timesheets_userId_workDate` (`userId`, `workDate`),
|
||
KEY `idx_timesheets_ticketId_workDate` (`ticketId`, `workDate`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_user` (
|
||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||
`username` varchar(175) NOT NULL,
|
||
`password` varchar(255) NOT NULL DEFAULT '',
|
||
`firstname` varchar(100) NOT NULL,
|
||
`lastname` varchar(100) NOT NULL,
|
||
`phone` varchar(25) NOT NULL,
|
||
`profileId` varchar(100) NOT NULL DEFAULT '',
|
||
`lastlogin` datetime DEFAULT NULL,
|
||
`status` varchar(1) NOT NULL DEFAULT 'A',
|
||
`expires` DATETIME DEFAULT NULL,
|
||
`role` varchar(200) NOT NULL,
|
||
`session` varchar(100) DEFAULT NULL,
|
||
`sessiontime` varchar(50) DEFAULT NULL,
|
||
`wage` int(11) DEFAULT NULL,
|
||
`hours` int(11) DEFAULT NULL,
|
||
`weekly_hours` int(11) DEFAULT NULL,
|
||
`employment_type` varchar(20) DEFAULT NULL,
|
||
`description` text,
|
||
`clientId` int(11) DEFAULT NULL,
|
||
`notifications` int(2) DEFAULT NULL,
|
||
`pwReset` varchar(100) DEFAULT NULL,
|
||
`pwResetExpiration` datetime DEFAULT NULL,
|
||
`pwResetCount` INT(5) DEFAULT NULL,
|
||
`forcePwReset` TINYINT DEFAULT NULL,
|
||
`lastpwd_change` DATETIME DEFAULT NULL,
|
||
`settings` TEXT NULL,
|
||
`twoFAEnabled` tinyint(1) DEFAULT '0',
|
||
`twoFASecret` varchar(200) DEFAULT NULL,
|
||
`createdOn` DATETIME DEFAULT NULL,
|
||
`source` varchar(200) DEFAULT NULL,
|
||
`jobTitle` VARCHAR(200) NULL,
|
||
`jobLevel` VARCHAR(50) NULL,
|
||
`department` VARCHAR(200) NULL,
|
||
`modified` DATETIME DEFAULT NULL,
|
||
PRIMARY KEY (`id`),
|
||
UNIQUE KEY `username` (`username`),
|
||
KEY `idx_user_clientId` (`clientId`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
INSERT INTO `zp_user`(`id`,`username`,`firstname`,`lastname`,`phone`,`profileId`,`lastlogin`,`lastpwd_change`,`status`,`expires`,`role`,`session`,`sessiontime`,`wage`,`hours`,`description`,`clientId`, `notifications`, `createdOn`, `pwReset`)
|
||
VALUES (1,:email,:firstname,:lastname,'','',NULL,0,'i',NULL,'50','','',0,0,NULL,0,1, :createdOn, :pwReset);
|
||
|
||
CREATE TABLE `zp_sprints` (
|
||
`id` INT NOT NULL AUTO_INCREMENT,
|
||
`projectId` INT NULL,
|
||
`name` VARCHAR(45) NULL,
|
||
`startDate` DATETIME NULL,
|
||
`endDate` DATETIME NULL,
|
||
`modified` datetime DEFAULT NULL,
|
||
PRIMARY KEY (`id`),
|
||
KEY `idx_sprints_projectId_startDate_endDate` (`projectId`, `startDate`, `endDate`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_stats` (
|
||
`sprintId` INT NULL,
|
||
`projectId` INT NULL,
|
||
`date` DATETIME NULL,
|
||
`sum_todos` INT NULL,
|
||
`sum_open_todos` INT NULL,
|
||
`sum_progres_todos` INT NULL,
|
||
`sum_closed_todos` INT NULL,
|
||
`sum_planned_hours` FLOAT NULL,
|
||
`sum_estremaining_hours` FLOAT NULL,
|
||
`sum_logged_hours` FLOAT NULL,
|
||
`sum_points` INT NULL,
|
||
`sum_points_done` INT NULL,
|
||
`sum_points_progress` INT NULL,
|
||
`sum_points_open` INT NULL,
|
||
`sum_todos_xs` INT NULL,
|
||
`sum_todos_s` INT NULL,
|
||
`sum_todos_m` INT NULL,
|
||
`sum_todos_l` INT NULL,
|
||
`sum_todos_xl` INT NULL,
|
||
`sum_todos_xxl` INT NULL,
|
||
`sum_todos_none` INT NULL,
|
||
`tickets` TEXT NULL,
|
||
`daily_avg_hours_booked_todo` FLOAT NULL,
|
||
`daily_avg_hours_booked_point` FLOAT NULL,
|
||
`daily_avg_hours_planned_todo` FLOAT NULL,
|
||
`daily_avg_hours_planned_point` FLOAT NULL,
|
||
`daily_avg_hours_remaining_point` FLOAT NULL,
|
||
`daily_avg_hours_remaining_todo` FLOAT NULL,
|
||
`sum_teammembers` INT NULL,
|
||
INDEX `projectId` (`projectId` ASC, `sprintId` ASC),
|
||
KEY `idx_stats_projectId_sprintId_date` (`projectId`, `sprintId`, `date`),
|
||
KEY `idx_stats_sprintId_date` (`sprintId`, `date`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_settings` (
|
||
`key` VARCHAR(175) NOT NULL,
|
||
`value` MEDIUMTEXT NULL,
|
||
PRIMARY KEY (`key`),
|
||
KEY `idx_settings_key` (`key`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
INSERT INTO zp_settings (`key`, `value`) VALUES ('db-version', :dbVersion);
|
||
INSERT INTO zp_settings (`key`, `value`) VALUES ('companysettings.telemetry.active', 'true');
|
||
|
||
CREATE TABLE `zp_audit` (
|
||
`id` INT NOT NULL AUTO_INCREMENT,
|
||
`userId` INT NULL,
|
||
`projectId` INT NULL,
|
||
`action` VARCHAR(45) NULL,
|
||
`entity` VARCHAR(45) NULL,
|
||
`entityId` INT NULL,
|
||
`values` TEXT NULL,
|
||
`date` DATETIME NULL,
|
||
PRIMARY KEY (`id`),
|
||
KEY `projectId` (`projectId` ASC),
|
||
KEY `projectAction` (`projectId` ASC, `action` ASC),
|
||
KEY `projectEntityEntityId` (`projectId` ASC, `entity` ASC, `entityId` ASC)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_queue` (
|
||
`msghash` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||
`channel` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||
`userId` int(11) NOT NULL,
|
||
`subject` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||
`message` text COLLATE utf8mb4_unicode_ci NOT NULL,
|
||
`thedate` datetime NOT NULL,
|
||
`projectId` int(11) NOT NULL,
|
||
PRIMARY KEY (`msghash`),
|
||
KEY `projectId` (`projectId`),
|
||
KEY `userId` (`userId`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_plugins` (
|
||
`id` INT NOT NULL AUTO_INCREMENT,
|
||
`name` VARCHAR(45) NULL,
|
||
`enabled` TINYINT NULL,
|
||
`description` VARCHAR(255) NULL,
|
||
`version` VARCHAR(45) NULL,
|
||
`installdate` DATETIME NULL,
|
||
`foldername` VARCHAR(45),
|
||
`homepage` VARCHAR(255) NULL,
|
||
`authors` VARCHAR(255) NULL,
|
||
`license` TEXT NULL DEFAULT NULL,
|
||
`format` VARCHAR(45) NULL DEFAULT NULL,
|
||
PRIMARY KEY (`id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_notifications` (
|
||
`id` INT NOT NULL AUTO_INCREMENT,
|
||
`userId` INT NOT NULL,
|
||
`read` INT NULL,
|
||
`type` VARCHAR(45) NULL,
|
||
`module` VARCHAR(45) NULL,
|
||
`moduleId` INT NULL,
|
||
`datetime` DATETIME NULL,
|
||
`url` VARCHAR(255) NULL,
|
||
`authorId` INT NULL,
|
||
`message` TEXT NULL,
|
||
PRIMARY KEY (`id`),
|
||
INDEX `userId` (`userId` ASC),
|
||
INDEX `userId,datetime` (`userId` ASC, `datetime` DESC),
|
||
INDEX `userId,read` (`userId` ASC, `read` DESC)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_entity_relationship` (
|
||
`id` INT NOT NULL AUTO_INCREMENT,
|
||
`entityA` INT NULL,
|
||
`entityAType` VARCHAR(45) NULL,
|
||
`entityB` INT NULL,
|
||
`entityBType` VARCHAR(45) NULL,
|
||
`relationship` VARCHAR(45) NULL,
|
||
`createdOn` DATETIME NULL,
|
||
`createdBy` INT NULL,
|
||
`meta` TEXT NULL,
|
||
PRIMARY KEY (`id`),
|
||
INDEX `entityA` (`entityA` ASC, `entityAType` ASC, `relationship` ASC),
|
||
INDEX `entityB` (`entityB` ASC, `entityBType` ASC, `relationship` ASC)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_integration` (
|
||
`id` INT NOT NULL AUTO_INCREMENT,
|
||
`providerId` VARCHAR(45) NULL,
|
||
`method` VARCHAR(45) NULL,
|
||
`entity` VARCHAR(45) NULL,
|
||
`fields` TEXT NULL,
|
||
`schedule` VARCHAR(45) NULL,
|
||
`notes` VARCHAR(45) NULL,
|
||
`auth` TEXT NULL,
|
||
`meta` VARCHAR(45) NULL,
|
||
`createdOn` DATETIME NULL,
|
||
`createdBy` INT NULL,
|
||
`lastSync` VARCHAR(45) NULL,
|
||
PRIMARY KEY (`id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_reactions` (
|
||
`id` INT NOT NULL AUTO_INCREMENT,
|
||
`userId` INT NULL,
|
||
`moduleId` INT NULL,
|
||
`module` VARCHAR(45) NULL,
|
||
`reaction` VARCHAR(45) NULL,
|
||
`date` DATETIME NULL,
|
||
PRIMARY KEY (`id`),
|
||
INDEX `entity` (`moduleId` ASC, `module` ASC, `reaction` ASC),
|
||
INDEX `user` (`userId` ASC, `moduleId` ASC, `module` ASC, `reaction` ASC)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_access_tokens` (
|
||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||
`tokenable_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||
`tokenable_id` bigint unsigned NOT NULL,
|
||
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||
`token` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||
`abilities` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||
`last_used_at` timestamp NULL DEFAULT NULL,
|
||
`expires_at` timestamp NULL DEFAULT NULL,
|
||
`created_at` timestamp NULL DEFAULT NULL,
|
||
`updated_at` timestamp NULL DEFAULT NULL,
|
||
PRIMARY KEY (`id`),
|
||
UNIQUE KEY `personal_access_tokens_token_unique` (`token`),
|
||
KEY `personal_access_tokens_tokenable_type_tokenable_id_index` (`tokenable_type`,`tokenable_id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE `zp_jobs` (
|
||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||
`queue` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||
`payload` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||
`attempts` tinyint unsigned NOT NULL,
|
||
`reserved_at` int unsigned DEFAULT NULL,
|
||
`available_at` int unsigned NOT NULL,
|
||
`created_at` int unsigned NOT NULL,
|
||
PRIMARY KEY (`id`),
|
||
KEY `zp_jobs_queue_index` (`queue`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
CREATE TABLE IF NOT EXISTS `zp_recurring_patterns` (
|
||
`id` INT NOT NULL AUTO_INCREMENT,
|
||
`entityId` INT NOT NULL,
|
||
`module` VARCHAR(50) NOT NULL,
|
||
`type` VARCHAR(50) NOT NULL,
|
||
`trigger` VARCHAR(50) NOT NULL,
|
||
`interval` INT NOT NULL DEFAULT 1,
|
||
`weekDays` TEXT NULL,
|
||
`monthDay` INT NULL,
|
||
`months` TEXT NULL,
|
||
`action` VARCHAR(20) NOT NULL DEFAULT 'reset',
|
||
`lastProcessed` DATETIME NULL,
|
||
`nextProcessingDate` DATETIME NULL,
|
||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||
PRIMARY KEY (`id`),
|
||
INDEX `entityId` (`entityId`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
";
|
||
|
||
return $sql;
|
||
}
|
||
|
||
/**
|
||
* update_sql_20004 - database update sql for V2.0.4
|
||
* - Updates all tables and db to utf8mb4
|
||
* - converts 255 index to be smaller
|
||
*
|
||
* @noinspection SqlResolve - A lot of tables don't exist anymore, so this will not resolve. Keeping the update script for backwards compatibility
|
||
*/
|
||
private function update_sql_20004(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'ALTER TABLE `zp_wiki_articles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_submodulerights` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_canvas` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_wiki_categories` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_tickethistory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_gcallinks` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_message` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_note` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_timesheets` MODIFY kind VARCHAR(175);',
|
||
'ALTER TABLE `zp_timesheets` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_roles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_projects` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_modulerights` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_wiki_comments` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_punch_clock` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_clients` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_account` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_sprints` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_lead` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_user` MODIFY username VARCHAR(175);',
|
||
'ALTER TABLE `zp_user` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_settings` MODIFY `key` VARCHAR(175);',
|
||
'ALTER TABLE `zp_settings` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_comment` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_stats` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_tickets` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_canvas_items` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_dashboard_widgets` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_file` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_action_tabs` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_relationuserproject` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_calendar` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_read` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_wiki` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
private function update_sql_20100(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'UPDATE `zp_user` SET role = 50 WHERE role = 2;',
|
||
'UPDATE `zp_user` SET role = 10 WHERE role = 3;',
|
||
'UPDATE `zp_user` SET role = 20 WHERE role = 4;',
|
||
'UPDATE `zp_user` SET role = 40 WHERE role = 5;',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
private function update_sql_20101(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'ALTER TABLE `zp_comment` CHANGE COLUMN `module` `module` VARCHAR(200) NULL DEFAULT NULL ;',
|
||
'ALTER TABLE `zp_stats`
|
||
ADD COLUMN `sum_teammembers` INT(11) NULL DEFAULT NULL AFTER `daily_avg_hours_remaining_todo`,
|
||
CHANGE COLUMN `sum_planned_hours` `sum_planned_hours` FLOAT NULL DEFAULT NULL ,
|
||
CHANGE COLUMN `sum_logged_hours` `sum_logged_hours` FLOAT NULL DEFAULT NULL ,
|
||
CHANGE COLUMN `sum_estremaining_hours` `sum_estremaining_hours` FLOAT NULL DEFAULT NULL ,
|
||
CHANGE COLUMN `daily_avg_hours_booked_todo` `daily_avg_hours_booked_todo` FLOAT NULL DEFAULT NULL ,
|
||
CHANGE COLUMN `daily_avg_hours_booked_point` `daily_avg_hours_booked_point` FLOAT NULL DEFAULT NULL ,
|
||
CHANGE COLUMN `daily_avg_hours_planned_todo` `daily_avg_hours_planned_todo` FLOAT NULL DEFAULT NULL ,
|
||
CHANGE COLUMN `daily_avg_hours_planned_point` `daily_avg_hours_planned_point` FLOAT NULL DEFAULT NULL ,
|
||
CHANGE COLUMN `daily_avg_hours_remaining_point` `daily_avg_hours_remaining_point` FLOAT NULL DEFAULT NULL ,
|
||
CHANGE COLUMN `daily_avg_hours_remaining_todo` `daily_avg_hours_remaining_todo` FLOAT NULL DEFAULT NULL ;',
|
||
'CREATE TABLE `zp_audit` (
|
||
`id` INT NOT NULL AUTO_INCREMENT,
|
||
`userId` INT NULL,
|
||
`projectId` INT NULL,
|
||
`action` VARCHAR(45) NULL,
|
||
`entity` VARCHAR(45) NULL,
|
||
`entityId` INT NULL,
|
||
`values` TEXT NULL,
|
||
`date` DATETIME NULL,
|
||
PRIMARY KEY (`id`),
|
||
KEY `projectId` (`projectId` ASC),
|
||
KEY `projectAction` (`projectId` ASC, `action` ASC),
|
||
KEY `projectEntityEntityId` (`projectId` ASC, `entity` ASC, `entityId` ASC)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
private function update_sql_20102(): bool|array
|
||
{
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
"ALTER TABLE `zp_user` add COLUMN `twoFAEnabled` tinyint(1) DEFAULT '0'",
|
||
'ALTER TABLE `zp_user` add COLUMN `twoFASecret` varchar(200) DEFAULT NULL',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
private function update_sql_20103(): bool|array
|
||
{
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'ALTER TABLE `zp_tickets` CHANGE COLUMN `planHours` `planHours` FLOAT NULL DEFAULT NULL',
|
||
'ALTER TABLE `zp_tickets` CHANGE COLUMN `hourRemaining` `hourRemaining` FLOAT NULL DEFAULT NULL',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
private function update_sql_20104(): bool|array
|
||
{
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'ALTER TABLE `zp_user` ADD COLUMN `pwResetCount` INT(5) NULL AFTER `pwResetExpiration`',
|
||
'ALTER TABLE `zp_user` ADD COLUMN `forcePwReset` TINYINT NULL AFTER `pwResetCount`',
|
||
'ALTER TABLE `zp_user` ADD COLUMN `createdOn` DATETIME NULL AFTER `twoFASecret`',
|
||
'ALTER TABLE `zp_user` CHANGE COLUMN `lastpwd_change` `lastpwd_change` DATETIME NULL DEFAULT NULL AFTER `forcePwReset`',
|
||
'ALTER TABLE `zp_user` CHANGE COLUMN `expires` `expires` DATETIME NULL DEFAULT NULL',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
private function update_sql_20105(): bool|array
|
||
{
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'ALTER TABLE `zp_projects` ADD COLUMN `psettings` MEDIUMTEXT NULL AFTER `active`',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
private function update_sql_20106(): bool|array
|
||
{
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'ALTER TABLE `zp_user` ADD COLUMN `source` varchar(200) DEFAULT NULL',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
private function update_sql_20107(): bool|array
|
||
{
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
"INSERT INTO zp_settings (`key`, `value`) VALUES ('companysettings.telemetry.active', 'true')",
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
private function update_sql_20108(): bool|array
|
||
{
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'alter table zp_relationuserproject add `projectRole` varchar(20) null',
|
||
'create index zp_relationuserproject_projectId_index on zp_relationuserproject (projectId)',
|
||
'create index zp_relationuserproject_userId_index on zp_relationuserproject (userId)',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
private function update_sql_20109(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'CREATE TABLE IF NOT EXISTS `zp_queue` (
|
||
`msghash` varchar(50) NOT NULL,
|
||
`channel` varchar(255),
|
||
`userId` int(11) NOT NULL,
|
||
`subject` varchar(255),
|
||
`message` text NOT NULL,
|
||
`thedate` datetime NOT NULL,
|
||
`projectId` int(11) NOT NULL,
|
||
PRIMARY KEY (`msghash`),
|
||
KEY `projectId` (`projectId`),
|
||
KEY `userId` (`userId`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
private function update_sql_20110(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'alter table zp_canvas_items add tags text null',
|
||
'alter table zp_canvas_items add title varchar(255) null',
|
||
'alter table zp_canvas_items add parent int null',
|
||
'alter table zp_canvas_items add featured int null',
|
||
'create table zp_approvals
|
||
(
|
||
id int auto_increment,
|
||
module varchar(100) null,
|
||
entityId int null,
|
||
requestorId int null,
|
||
approverId int null,
|
||
approvalStatus int null,
|
||
requestedOn datetime null,
|
||
lastStatusChange datetime null,
|
||
constraint zp_approvals_pk
|
||
primary key (id)
|
||
)',
|
||
'alter table zp_comment add status varchar(50) null',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/* * *
|
||
* update_sql_20111 - Update database for new Canvas
|
||
*
|
||
* @access private
|
||
* @return bool|array Success of database update or array of errors
|
||
*/
|
||
|
||
private function update_sql_20111(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'ALTER TABLE zp_projects ADD menuType MEDIUMTEXT null',
|
||
"UPDATE zp_projects SET menuType = '".MenuRepository::DEFAULT_MENU."'",
|
||
'ALTER TABLE zp_canvas_items ADD relates VARCHAR(255) null',
|
||
'UPDATE zp_canvas_items INNER JOIN zp_canvas ON zp_canvas.id = zp_canvas_items.id '.
|
||
"SET zp_canvas_items.status = 'draft' WHERE zp_canvas_items.status = 'danger' AND zp_canvas.type = 'leancanvas'",
|
||
'UPDATE zp_canvas_items INNER JOIN zp_canvas ON zp_canvas.id = zp_canvas_items.id '.
|
||
"SET zp_canvas_items.status = 'valid' WHERE zp_canvas_items.status = 'sucess' AND zp_canvas.type = 'leancanvas'",
|
||
'UPDATE zp_canvas_items INNER JOIN zp_canvas ON zp_canvas.id = zp_canvas_items.id '.
|
||
"SET zp_canvas_items.status = 'invalid' WHERE zp_canvas_items.status = 'info' AND zp_canvas.type = 'leancanvas'",
|
||
"UPDATE zp_canvas SET zp_canvas.type = 'retroscanvas' WHERE zp_canvas.type = 'retrospective'",
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/* * *
|
||
* update_sql_20112 - Update database for new Canvas
|
||
*
|
||
* @access private
|
||
* @return bool|array Success of database update or array of errors
|
||
*/
|
||
|
||
private function update_sql_20112(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'CREATE TABLE `zp_plugins` (
|
||
`id` INT NOT NULL AUTO_INCREMENT,
|
||
`name` VARCHAR(45) NULL,
|
||
`enabled` TINYINT NULL,
|
||
`description` VARCHAR(255) NULL,
|
||
`version` VARCHAR(45) NULL,
|
||
`installdate` DATETIME NULL,
|
||
`foldername` VARCHAR(45) NULL,
|
||
`homepage` VARCHAR(255) NULL,
|
||
`authors` VARCHAR(255) NULL,
|
||
PRIMARY KEY (`id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;',
|
||
'ALTER TABLE `zp_timesheets` ADD COLUMN `paid` SMALLINT NULL AFTER `rate`, ADD COLUMN `paidDate` DATETIME NULL AFTER `paid`;',
|
||
'DROP TABLE IF EXISTS zp_account, zp_action_tabs, zp_dashboard_widgets, zp_lead, zp_message, zp_modulerights, zp_roles, zp_submodulerights, zp_wiki, zp_wiki_articles, zp_wiki_categories, zp_wiki_comments;',
|
||
'CREATE TABLE `zp_notifications` (
|
||
`id` INT NOT NULL AUTO_INCREMENT,
|
||
`userId` INT NOT NULL,
|
||
`read` INT NULL,
|
||
`type` VARCHAR(45) NULL,
|
||
`module` VARCHAR(45) NULL,
|
||
`moduleId` INT NULL,
|
||
`datetime` DATETIME NULL,
|
||
`url` VARCHAR(255) NULL,
|
||
`authorId` INT NULL,
|
||
`message` TEXT NULL,
|
||
PRIMARY KEY (`id`),
|
||
INDEX `userId` (`userId` ASC),
|
||
INDEX `userId,datetime` (`userId` ASC, `datetime` DESC),
|
||
INDEX `userId,read` (`userId` ASC, `read` DESC)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/* * *
|
||
* update_sql_20113 - Create onboarding setting for first time installs
|
||
*
|
||
* @access private
|
||
* @return bool|array Success of database update or array of errors
|
||
*/
|
||
|
||
private function update_sql_20113(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
"INSERT INTO zp_settings (`key`, `value`) VALUES ('companysettings.completedOnboarding', 'true') ON DUPLICATE KEY UPDATE `value` = 'true'",
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public function update_sql_20114(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'ALTER TABLE `zp_projects`
|
||
ADD COLUMN `type` VARCHAR(45) NULL,
|
||
ADD COLUMN `start` DATETIME NULL,
|
||
ADD COLUMN `end` DATETIME NULL,
|
||
ADD COLUMN `created` DATETIME NULL,
|
||
ADD COLUMN `modified` DATETIME NULL',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public function update_sql_20115(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
' CREATE TABLE `zp_entity_relationships` (
|
||
`id` INT NOT NULL AUTO_INCREMENT,
|
||
`enitityA` INT NULL,
|
||
`entityAType` VARCHAR(45) NULL,
|
||
`entityB` INT NULL,
|
||
`entityBType` VARCHAR(45) NULL,
|
||
`relationship` VARCHAR(45) NULL,
|
||
`createdOn` DATETIME NULL,
|
||
`createdBy` INT NULL,
|
||
`meta` TEXT NULL,
|
||
PRIMARY KEY (`id`),
|
||
INDEX `entityA` (`enitityA` ASC, `entityAType` ASC, `relationship` ASC),
|
||
INDEX `entityB` (`entityB` ASC, `entityBType` ASC, `relationship` ASC)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||
"UPDATE `zp_tickets` SET milestoneid = dependingTicketId , dependingTicketId = '' WHERE type <> 'subtask' AND dependingTicketId > 0",
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public function update_sql_20116(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
' CREATE TABLE `zp_reactions` (
|
||
`id` INT NOT NULL AUTO_INCREMENT,
|
||
`userId` INT NULL,
|
||
`moduleId` INT NULL,
|
||
`module` VARCHAR(45) NULL,
|
||
`reaction` VARCHAR(45) NULL,
|
||
`date` DATETIME NULL,
|
||
PRIMARY KEY (`id`),
|
||
INDEX `entity` (`moduleId` ASC, `module` ASC, `reaction` ASC),
|
||
INDEX `user` (`userId` ASC, `moduleId` ASC, `module` ASC, `reaction` ASC)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||
'ALTER TABLE `zp_projects`
|
||
ADD COLUMN `avatar` MEDIUMTEXT NULL AFTER `modified`,
|
||
ADD COLUMN `cover` MEDIUMTEXT NULL AFTER `avatar`;',
|
||
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public function update_sql_20117(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'ALTER TABLE `zp_projects`
|
||
ADD COLUMN `parent` INT(11) NULL;',
|
||
|
||
'ALTER TABLE `zp_projects`
|
||
ADD COLUMN `sortIndex` INT(11) NULL;',
|
||
|
||
'ALTER TABLE `zp_user`
|
||
ADD COLUMN `jobTitle` VARCHAR(200) NULL ,
|
||
ADD COLUMN `jobLevel` VARCHAR(50) NULL ,
|
||
ADD COLUMN `department` VARCHAR(200) NULL ;',
|
||
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public function update_sql_20118(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
|
||
'UPDATE `zp_projects` SET parent = null;',
|
||
|
||
'UPDATE `zp_projects` SET start = null, end = null;',
|
||
|
||
'ALTER TABLE `zp_projects`
|
||
CHANGE COLUMN `parent` `parent` INT(11) NULL DEFAULT NULL,
|
||
CHANGE COLUMN `type` `type` VARCHAR(45) NULL DEFAULT NULL ;',
|
||
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public function update_sql_20120(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
|
||
'ALTER TABLE `zp_canvas_items`
|
||
ADD COLUMN `kpi` INT NULL DEFAULT NULL AFTER `tags`,
|
||
ADD COLUMN `data1` TEXT NULL DEFAULT NULL AFTER `kpi`,
|
||
ADD COLUMN `data2` TEXT NULL DEFAULT NULL AFTER `data1`,
|
||
ADD COLUMN `data3` TEXT NULL DEFAULT NULL AFTER `data2`,
|
||
ADD COLUMN `data4` TEXT NULL DEFAULT NULL AFTER `data3`,
|
||
ADD COLUMN `data5` TEXT NULL DEFAULT NULL AFTER `data4`,
|
||
ADD COLUMN `startDate` DATETIME NULL DEFAULT NULL AFTER `data5`,
|
||
ADD COLUMN `endDate` DATETIME NULL DEFAULT NULL AFTER `startDate`,
|
||
ADD COLUMN `setting` TEXT NULL DEFAULT NULL AFTER `endDate`,
|
||
ADD COLUMN `metricType` VARCHAR(45) NULL DEFAULT NULL AFTER `setting`,
|
||
ADD COLUMN `startValue` double(10,2) NULL DEFAULT NULL AFTER `metricType`,
|
||
ADD COLUMN `currentValue` double(10,2) NULL DEFAULT NULL AFTER `startValue`,
|
||
ADD COLUMN `endValue` double(10,2) NULL DEFAULT NULL AFTER `currentValue`,
|
||
ADD COLUMN `impact` INT NULL DEFAULT NULL AFTER `endValue`,
|
||
ADD COLUMN `effort` INT NULL DEFAULT NULL AFTER `impact`,
|
||
ADD COLUMN `probability` INT NULL DEFAULT NULL AFTER `effort`,
|
||
ADD COLUMN `action` TEXT NULL DEFAULT NULL AFTER `probability`,
|
||
ADD COLUMN `assignedTo` INT NULL DEFAULT NULL AFTER `action`;',
|
||
"UPDATE zp_canvas_items SET
|
||
currentValue = CAST(CASE WHEN `data` = '' THEN 0 ELSE `data` END AS DECIMAL(10,2)),
|
||
endValue = CAST(CASE WHEN `conclusion` = '' THEN 0 ELSE `conclusion` END AS DECIMAL(10,2)),
|
||
title = description,
|
||
description = assumptions
|
||
WHERE box = 'goal';",
|
||
'ALTER TABLE `zp_canvas_items`
|
||
ADD INDEX `CanvasLookUp` (`canvasId` ASC, `box` ASC);',
|
||
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public function update_sql_20121(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'ALTER TABLE `zp_canvas`
|
||
ADD COLUMN `description` TEXT NULL DEFAULT NULL AFTER `type`;',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public function update_sql_20122(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'ALTER TABLE `zp_settings`
|
||
CHANGE COLUMN `value` `value` MEDIUMTEXT NULL DEFAULT NULL ;',
|
||
'ALTER TABLE `zp_canvas_items`
|
||
CHANGE COLUMN `description` `description` MEDIUMTEXT NULL DEFAULT NULL ,
|
||
CHANGE COLUMN `data` `data` MEDIUMTEXT NULL DEFAULT NULL;',
|
||
'ALTER TABLE `zp_user`
|
||
ADD COLUMN `modified` DATETIME NULL DEFAULT NULL;',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public function update_sql_20401(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'ALTER TABLE `zp_plugins`
|
||
ADD COLUMN `license` TEXT NULL DEFAULT NULL,
|
||
ADD COLUMN `format` VARCHAR(45) NULL DEFAULT NULL',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public function update_sql_20402(): bool|array
|
||
{
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'ALTER TABLE `zp_plugins`
|
||
ADD COLUMN `format` VARCHAR(45) NULL DEFAULT NULL',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
$errors[] = "$statement Failed: {$e->getMessage()}";
|
||
}
|
||
}
|
||
|
||
return count($errors) ? $errors : true;
|
||
}
|
||
|
||
/**
|
||
* Install script did not include medium text updates. Run again
|
||
*/
|
||
public function update_sql_20405(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'ALTER TABLE `zp_settings`
|
||
CHANGE COLUMN `value` `value` MEDIUMTEXT NULL DEFAULT NULL ;',
|
||
'ALTER TABLE `zp_canvas_items`
|
||
CHANGE COLUMN `description` `description` MEDIUMTEXT NULL DEFAULT NULL ,
|
||
CHANGE COLUMN `data` `data` MEDIUMTEXT NULL DEFAULT NULL;',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Install script did not include medium text updates. Run again
|
||
*/
|
||
public function update_sql_20406(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'ALTER TABLE `zp_canvas_items`
|
||
CHANGE COLUMN `data1` `data1` MEDIUMTEXT NULL DEFAULT NULL,
|
||
CHANGE COLUMN `data2` `data2` MEDIUMTEXT NULL DEFAULT NULL,
|
||
CHANGE COLUMN `data3` `data3` MEDIUMTEXT NULL DEFAULT NULL,
|
||
CHANGE COLUMN `data4` `data4` MEDIUMTEXT NULL DEFAULT NULL,
|
||
CHANGE COLUMN `data5` `data5` MEDIUMTEXT NULL DEFAULT NULL;',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public function update_sql_20407(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'CREATE TABLE IF NOT EXISTS `zp_integration` (
|
||
`id` INT NOT NULL AUTO_INCREMENT,
|
||
`providerId` VARCHAR(45) NULL,
|
||
`method` VARCHAR(45) NULL,
|
||
`entity` VARCHAR(45) NULL,
|
||
`fields` TEXT NULL,
|
||
`schedule` VARCHAR(45) NULL,
|
||
`notes` VARCHAR(45) NULL,
|
||
`auth` TEXT NULL,
|
||
`meta` VARCHAR(45) NULL,
|
||
`createdOn` DATETIME NULL,
|
||
`createdBy` INT NULL,
|
||
PRIMARY KEY (`id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||
'ALTER TABLE `zp_integration`
|
||
ADD COLUMN `lastSync` DATETIME NULL DEFAULT NULL',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
if (count($errors) > 0) {
|
||
return $errors;
|
||
} else {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public function update_sql_30002(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'ALTER TABLE `zp_plugins` ADD COLUMN `license` TEXT NULL DEFAULT NULL',
|
||
'ALTER TABLE `zp_plugins` ADD COLUMN `format` VARCHAR(45) NULL DEFAULT NULL',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public function update_sql_30003(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'ALTER TABLE `zp_canvas` ADD COLUMN `modified` datetime NULL DEFAULT NULL',
|
||
'ALTER TABLE `zp_clients` ADD COLUMN `modified` datetime NULL DEFAULT NULL',
|
||
'ALTER TABLE `zp_sprints` ADD COLUMN `modified` datetime NULL DEFAULT NULL',
|
||
'ALTER TABLE `zp_projects` ADD COLUMN `modified` datetime NULL DEFAULT NULL',
|
||
'ALTER TABLE `zp_timesheets` ADD COLUMN `modified` datetime NULL DEFAULT NULL',
|
||
'ALTER TABLE `zp_tickets` ADD COLUMN `modified` datetime NULL DEFAULT NULL',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public function update_sql_30400(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'CREATE TABLE IF NOT EXISTS `zp_access_tokens` (
|
||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||
`tokenable_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||
`tokenable_id` bigint unsigned NOT NULL,
|
||
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||
`token` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||
`abilities` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||
`last_used_at` timestamp NULL DEFAULT NULL,
|
||
`expires_at` timestamp NULL DEFAULT NULL,
|
||
`created_at` timestamp NULL DEFAULT NULL,
|
||
`updated_at` timestamp NULL DEFAULT NULL,
|
||
PRIMARY KEY (`id`),
|
||
UNIQUE KEY `personal_access_tokens_token_unique` (`token`),
|
||
KEY `personal_access_tokens_tokenable_type_tokenable_id_index` (`tokenable_type`,`tokenable_id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;',
|
||
'CREATE TABLE IF NOT EXISTS `zp_jobs` (
|
||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||
`queue` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||
`payload` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||
`attempts` tinyint unsigned NOT NULL,
|
||
`reserved_at` int unsigned DEFAULT NULL,
|
||
`available_at` int unsigned NOT NULL,
|
||
`created_at` int unsigned NOT NULL,
|
||
PRIMARY KEY (`id`),
|
||
KEY `zp_jobs_queue_index` (`queue`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;',
|
||
'CREATE TABLE IF NOT EXISTS `zp_recurring_patterns` (
|
||
`id` INT NOT NULL AUTO_INCREMENT,
|
||
`entityId` INT NOT NULL,
|
||
`module` VARCHAR(50) NOT NULL,
|
||
`type` VARCHAR(50) NOT NULL,
|
||
`trigger` VARCHAR(50) NOT NULL,
|
||
`interval` INT NOT NULL DEFAULT 1,
|
||
`weekDays` TEXT NULL,
|
||
`monthDay` INT NULL,
|
||
`months` TEXT NULL,
|
||
`action` VARCHAR(20) NOT NULL DEFAULT \'reset\',
|
||
`lastProcessed` DATETIME NULL,
|
||
`nextProcessingDate` DATETIME NULL,
|
||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||
PRIMARY KEY (`id`),
|
||
INDEX `entityId` (`entityId`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
$errors[] = 'Migration 30400 failed: '.$e->getMessage();
|
||
}
|
||
}
|
||
|
||
// Report failures instead of returning true regardless. Swallowing them here is what
|
||
// let installs record 3.4.0 as applied while zp_access_tokens did not exist, which
|
||
// then broke every subsequent upgrade at 30504 (#3706). The creates above are
|
||
// IF NOT EXISTS, so a re-run on a healthy install is still a no-op.
|
||
if ($errors !== []) {
|
||
return $errors;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public function update_sql_30408(): bool|array
|
||
{
|
||
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
'INSERT INTO zp_settings (`key`, `value`)
|
||
SELECT
|
||
CONCAT("user.", `id`, ".firstLoginComplete") AS `key`,
|
||
1 AS `value`
|
||
FROM zp_user;',
|
||
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Database performance optimization - add indexes for frequently used queries
|
||
*/
|
||
public function update_sql_30409(): bool|array
|
||
{
|
||
$errors = [];
|
||
|
||
// High Priority Indexes - Critical Performance Impact
|
||
$sql = [
|
||
// Individual column indexes for tickets table
|
||
'CREATE INDEX idx_tickets_editorId ON zp_tickets (editorId)',
|
||
'CREATE INDEX idx_tickets_milestoneid ON zp_tickets (milestoneid)',
|
||
'CREATE INDEX idx_tickets_editFrom ON zp_tickets (editFrom)',
|
||
'CREATE INDEX idx_tickets_editTo ON zp_tickets (editTo)',
|
||
'CREATE INDEX idx_tickets_dateToFinish ON zp_tickets (dateToFinish)',
|
||
'CREATE INDEX idx_tickets_modified ON zp_tickets (modified)',
|
||
|
||
// Medium Priority - Combined indexes for common filter patterns
|
||
'CREATE INDEX idx_tickets_projectId_status ON zp_tickets (projectId, status)',
|
||
'CREATE INDEX idx_tickets_projectId_type ON zp_tickets (projectId, type)',
|
||
|
||
// User and project related indexes
|
||
'CREATE INDEX idx_user_clientId ON zp_user (clientId)',
|
||
'CREATE INDEX idx_projects_clientId ON zp_projects (clientId)',
|
||
'CREATE INDEX idx_projects_state ON zp_projects (state)',
|
||
|
||
// Timesheets performance
|
||
'CREATE INDEX idx_timesheets_ticketId ON zp_timesheets (ticketId)',
|
||
|
||
// Lower Priority - Additional combinations
|
||
'CREATE INDEX idx_tickets_status_type ON zp_tickets (status, type)',
|
||
'CREATE INDEX idx_tickethistory_ticketId_dateModified ON zp_tickethistory (ticketId, dateModified)',
|
||
|
||
// Phase 2: Canvas, Comments, Files, Notifications, Calendar, Sprints Performance
|
||
// Canvas and Canvas Items - High Priority for Ideas, Goals, Wiki
|
||
'CREATE INDEX idx_canvas_projectId_type ON zp_canvas (projectId, type)',
|
||
'CREATE INDEX idx_canvas_type_id ON zp_canvas (type, id)',
|
||
'CREATE INDEX idx_canvas_items_canvasId_box ON zp_canvas_items (canvasId, box)',
|
||
'CREATE INDEX idx_canvas_items_box_milestoneId ON zp_canvas_items (box, milestoneId)',
|
||
'CREATE INDEX idx_canvas_items_box_status_author ON zp_canvas_items (box, status, author)',
|
||
'CREATE INDEX idx_canvas_items_parent_title ON zp_canvas_items (parent, title)',
|
||
|
||
// Notifications - High Priority for user interactions
|
||
'CREATE INDEX idx_notifications_userId_read_datetime ON zp_notifications (userId, `read`, datetime)',
|
||
|
||
// Timesheets - Additional High Priority indexes
|
||
'CREATE INDEX idx_timesheets_userId_workDate ON zp_timesheets (userId, workDate)',
|
||
'CREATE INDEX idx_timesheets_ticketId_workDate ON zp_timesheets (ticketId, workDate)',
|
||
|
||
// Calendar - High Priority for scheduling
|
||
'CREATE INDEX idx_calendar_userId_dateFrom_dateTo ON zp_calendar (userId, dateFrom, dateTo)',
|
||
'CREATE INDEX idx_gcallinks_userId ON zp_gcallinks (userId)',
|
||
|
||
// Comments - Medium Priority for threaded discussions
|
||
'CREATE INDEX idx_comment_moduleId_module_commentParent ON zp_comment (moduleId, module, commentParent)',
|
||
'CREATE INDEX idx_comment_userId_module ON zp_comment (userId, module)',
|
||
|
||
// Files - Medium Priority for file access
|
||
'CREATE INDEX idx_file_module_moduleId_userId ON zp_file (module, moduleId, userId)',
|
||
|
||
// Sprints - Medium Priority for project management
|
||
'CREATE INDEX idx_sprints_projectId_startDate_endDate ON zp_sprints (projectId, startDate, endDate)',
|
||
|
||
// Settings - For project configuration lookups
|
||
'CREATE INDEX idx_settings_key ON zp_settings (`key`)',
|
||
|
||
// Stats table for reports (if exists) - Lower Priority
|
||
'CREATE INDEX idx_stats_projectId_sprintId_date ON zp_stats (projectId, sprintId, date)',
|
||
'CREATE INDEX idx_stats_sprintId_date ON zp_stats (sprintId, date)',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed:'.$e->getMessage());
|
||
Log::error($e);
|
||
// Don't fail the entire migration for duplicate indexes
|
||
if (! str_contains($e->getMessage(), 'Duplicate key name')) {
|
||
array_push($errors, $statement.' Failed:'.$e->getMessage());
|
||
}
|
||
}
|
||
}
|
||
|
||
return count($errors) ? $errors : true;
|
||
}
|
||
|
||
/**
|
||
* Migration 30410: Ensure zp_entity_relationship table exists with correct schema
|
||
*
|
||
* This migration must handle all possible database states from different upgrade paths:
|
||
* - State A: Only plural table (zp_entity_relationships) exists with typo column (enitityA)
|
||
* - State B: Only singular table (zp_entity_relationship) exists with correct column (entityA)
|
||
* - State C: Only singular table exists with typo column (enitityA)
|
||
* - State D: Both tables exist (from failed previous migrations)
|
||
* - State E: Neither table exists (fresh install edge case)
|
||
*
|
||
* @return bool Always returns true to prevent blocking subsequent migrations
|
||
*/
|
||
public function update_sql_30410(): bool|array
|
||
{
|
||
$pluralTable = 'zp_entity_relationships';
|
||
$singularTable = 'zp_entity_relationship';
|
||
|
||
try {
|
||
$pluralExists = $this->tableExistsForMigration($pluralTable);
|
||
$singularExists = $this->tableExistsForMigration($singularTable);
|
||
|
||
// Case D: Both tables exist - merge data from plural into singular, then drop plural
|
||
if ($pluralExists && $singularExists) {
|
||
$this->mergeEntityRelationshipTables($pluralTable, $singularTable);
|
||
}
|
||
// Case A: Only plural exists - rename to singular
|
||
elseif ($pluralExists) {
|
||
$this->connection->statement("RENAME TABLE `{$pluralTable}` TO `{$singularTable}`");
|
||
}
|
||
// Case E: Neither exists - create singular with correct schema (plural already ruled out above)
|
||
elseif (! $singularExists) {
|
||
$this->createEntityRelationshipTable();
|
||
|
||
return true;
|
||
}
|
||
// Case B/C: Only singular exists - continue to fix column/index if needed
|
||
|
||
// Fix column typo if present (handles Case A after rename, Case C, Case D after merge)
|
||
$this->fixEntityAColumnTypo($singularTable);
|
||
|
||
// Ensure correct index exists
|
||
$this->ensureEntityAIndex($singularTable);
|
||
|
||
} catch (\Exception $e) {
|
||
Log::error('Migration 30410: '.$e->getMessage());
|
||
// Don't fail the migration - log and continue
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Check if a table exists in the current database
|
||
*/
|
||
private function tableExistsForMigration(string $tableName): bool
|
||
{
|
||
$result = $this->connection->select(
|
||
'SELECT COUNT(*) as cnt FROM information_schema.tables
|
||
WHERE table_schema = DATABASE() AND table_name = ?',
|
||
[$tableName]
|
||
);
|
||
|
||
return $result[0]->cnt > 0;
|
||
}
|
||
|
||
/**
|
||
* Check if a column exists in a table
|
||
*/
|
||
private function columnExistsForMigration(string $tableName, string $columnName): bool
|
||
{
|
||
$result = $this->connection->select(
|
||
'SELECT COUNT(*) as cnt FROM information_schema.columns
|
||
WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?',
|
||
[$tableName, $columnName]
|
||
);
|
||
|
||
return $result[0]->cnt > 0;
|
||
}
|
||
|
||
/**
|
||
* Check if an index exists on a table
|
||
*/
|
||
private function indexExistsForMigration(string $tableName, string $indexName): bool
|
||
{
|
||
$result = $this->connection->select(
|
||
'SELECT COUNT(*) as cnt FROM information_schema.statistics
|
||
WHERE table_schema = DATABASE() AND table_name = ? AND index_name = ?',
|
||
[$tableName, $indexName]
|
||
);
|
||
|
||
return $result[0]->cnt > 0;
|
||
}
|
||
|
||
/**
|
||
* Merge data from plural table into singular table, then drop plural
|
||
*/
|
||
private function mergeEntityRelationshipTables(string $sourceTable, string $targetTable): void
|
||
{
|
||
try {
|
||
// Determine column names in source table (may have typo)
|
||
$sourceHasTypo = $this->columnExistsForMigration($sourceTable, 'enitityA');
|
||
$sourceEntityAColumn = $sourceHasTypo ? 'enitityA' : 'entityA';
|
||
|
||
// Determine column names in target table (may have typo)
|
||
$targetHasTypo = $this->columnExistsForMigration($targetTable, 'enitityA');
|
||
$targetEntityAColumn = $targetHasTypo ? 'enitityA' : 'entityA';
|
||
|
||
// Insert data from source into target, ignoring duplicates
|
||
$this->connection->statement("
|
||
INSERT IGNORE INTO `{$targetTable}` (
|
||
`{$targetEntityAColumn}`, `entityAType`, `entityB`, `entityBType`,
|
||
`relationship`, `createdOn`, `createdBy`, `meta`
|
||
)
|
||
SELECT
|
||
`{$sourceEntityAColumn}`, `entityAType`, `entityB`, `entityBType`,
|
||
`relationship`, `createdOn`, `createdBy`, `meta`
|
||
FROM `{$sourceTable}`
|
||
");
|
||
|
||
// Drop the source (plural) table
|
||
$this->connection->statement("DROP TABLE `{$sourceTable}`");
|
||
|
||
} catch (\Exception $e) {
|
||
Log::error('Migration 30410: Failed to merge tables: '.$e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create the entity_relationship table with correct schema
|
||
*/
|
||
private function createEntityRelationshipTable(): void
|
||
{
|
||
$this->connection->statement('
|
||
CREATE TABLE `zp_entity_relationship` (
|
||
`id` INT NOT NULL AUTO_INCREMENT,
|
||
`entityA` INT NULL,
|
||
`entityAType` VARCHAR(45) NULL,
|
||
`entityB` INT NULL,
|
||
`entityBType` VARCHAR(45) NULL,
|
||
`relationship` VARCHAR(45) NULL,
|
||
`createdOn` DATETIME NULL,
|
||
`createdBy` INT NULL,
|
||
`meta` TEXT NULL,
|
||
PRIMARY KEY (`id`),
|
||
INDEX `entityA` (`entityA` ASC, `entityAType` ASC, `relationship` ASC),
|
||
INDEX `entityB` (`entityB` ASC, `entityBType` ASC, `relationship` ASC)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||
');
|
||
}
|
||
|
||
/**
|
||
* Fix the entityA column typo if it exists
|
||
*/
|
||
private function fixEntityAColumnTypo(string $tableName): void
|
||
{
|
||
try {
|
||
if ($this->columnExistsForMigration($tableName, 'enitityA')) {
|
||
$this->connection->statement(
|
||
"ALTER TABLE `{$tableName}` CHANGE COLUMN `enitityA` `entityA` INT NULL"
|
||
);
|
||
}
|
||
} catch (\Exception $e) {
|
||
Log::error("Migration 30410: Failed to fix column typo in {$tableName}: ".$e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Ensure the entityA index exists with correct columns
|
||
*/
|
||
private function ensureEntityAIndex(string $tableName): void
|
||
{
|
||
try {
|
||
// Drop the old index if it exists (it may reference the wrong column)
|
||
if ($this->indexExistsForMigration($tableName, 'entityA')) {
|
||
$this->connection->statement("ALTER TABLE `{$tableName}` DROP INDEX `entityA`");
|
||
}
|
||
|
||
// Create the index with correct columns
|
||
$this->connection->statement(
|
||
"ALTER TABLE `{$tableName}` ADD INDEX `entityA` (`entityA` ASC, `entityAType` ASC, `relationship` ASC)"
|
||
);
|
||
} catch (\Exception $e) {
|
||
Log::error("Migration 30410: Failed to ensure entityA index on {$tableName}: ".$e->getMessage());
|
||
}
|
||
}
|
||
|
||
public function update_sql_30411(): bool|array
|
||
{
|
||
$errors = [];
|
||
|
||
$sql = [
|
||
// Add color column to zp_canvas for notebook color support
|
||
'ALTER TABLE `zp_canvas` ADD COLUMN `color` VARCHAR(50) DEFAULT \'ocean\' AFTER `description`;',
|
||
];
|
||
|
||
foreach ($sql as $statement) {
|
||
try {
|
||
$this->connection->statement($statement);
|
||
} catch (\Exception $e) {
|
||
Log::error($statement.' Failed: '.$e->getMessage());
|
||
Log::error($e);
|
||
// Don't fail for duplicate column
|
||
if (! str_contains($e->getMessage(), 'Duplicate column name')) {
|
||
array_push($errors, $statement.' Failed: '.$e->getMessage());
|
||
}
|
||
}
|
||
}
|
||
|
||
return count($errors) ? $errors : true;
|
||
}
|
||
|
||
/**
|
||
* Migration 30412: Safety net for zp_entity_relationship table
|
||
*
|
||
* This migration acts as a final safety net for users who may have had issues
|
||
* with migration 30410. It performs the same checks and fixes using the shared
|
||
* helper methods to ensure the table is in the correct state.
|
||
*
|
||
* Since 30410 is now robust and idempotent, this migration will typically
|
||
* find everything already correct and simply return true.
|
||
*
|
||
* @return bool Always returns true to prevent blocking subsequent migrations
|
||
*/
|
||
public function update_sql_30412(): bool|array
|
||
{
|
||
$pluralTable = 'zp_entity_relationships';
|
||
$singularTable = 'zp_entity_relationship';
|
||
|
||
try {
|
||
$pluralExists = $this->tableExistsForMigration($pluralTable);
|
||
$singularExists = $this->tableExistsForMigration($singularTable);
|
||
|
||
// Handle any remaining plural table issues
|
||
if ($pluralExists && $singularExists) {
|
||
$this->mergeEntityRelationshipTables($pluralTable, $singularTable);
|
||
} elseif ($pluralExists) {
|
||
$this->connection->statement("RENAME TABLE `{$pluralTable}` TO `{$singularTable}`");
|
||
} elseif (! $singularExists) {
|
||
$this->createEntityRelationshipTable();
|
||
|
||
return true;
|
||
}
|
||
|
||
// Ensure column and index are correct
|
||
$this->fixEntityAColumnTypo($singularTable);
|
||
$this->ensureEntityAIndex($singularTable);
|
||
|
||
} catch (\Exception $e) {
|
||
Log::error('Migration 30412: '.$e->getMessage());
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Migration 30413: Final fix for zp_entity_relationship table
|
||
*
|
||
* This migration ensures all users get the entity_relationship table fix,
|
||
* including those whose DB version was already recorded as 30412+ due to
|
||
* previous failed or partial migrations.
|
||
*
|
||
* Since all the helper methods are idempotent, this will simply verify
|
||
* the table is correct (doing nothing if already fixed) or apply the fix
|
||
* if still needed.
|
||
*
|
||
* @return bool Always returns true to prevent blocking subsequent migrations
|
||
*/
|
||
public function update_sql_30413(): bool|array
|
||
{
|
||
$pluralTable = 'zp_entity_relationships';
|
||
$singularTable = 'zp_entity_relationship';
|
||
|
||
try {
|
||
$pluralExists = $this->tableExistsForMigration($pluralTable);
|
||
$singularExists = $this->tableExistsForMigration($singularTable);
|
||
|
||
// Handle any remaining plural table issues
|
||
if ($pluralExists && $singularExists) {
|
||
$this->mergeEntityRelationshipTables($pluralTable, $singularTable);
|
||
} elseif ($pluralExists) {
|
||
$this->connection->statement("RENAME TABLE `{$pluralTable}` TO `{$singularTable}`");
|
||
} elseif (! $singularExists) {
|
||
$this->createEntityRelationshipTable();
|
||
|
||
return true;
|
||
}
|
||
|
||
// Ensure column and index are correct
|
||
$this->fixEntityAColumnTypo($singularTable);
|
||
$this->ensureEntityAIndex($singularTable);
|
||
|
||
} catch (\Exception $e) {
|
||
Log::error('Migration 30413: '.$e->getMessage());
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Add performance indexes for frequently-queried columns.
|
||
*
|
||
* - zp_tickets.dependingTicketId: Used in every subtask query (was full table scan)
|
||
* - zp_tickethistory.ticketId: Used in ticket history lookups
|
||
* - zp_read (userId, module, moduleId): Table had zero indexes
|
||
* - zp_relationuserproject (userId, projectId): Used in auth checks
|
||
* - zp_comment (moduleId, module, date): Used in "latest comment" queries
|
||
*/
|
||
public function update_sql_30500(): bool|array
|
||
{
|
||
$indexes = [
|
||
['table' => 'zp_tickets', 'columns' => ['dependingTicketId'], 'name' => 'idx_tickets_dependingTicketId'],
|
||
['table' => 'zp_tickethistory', 'columns' => ['ticketId'], 'name' => 'idx_tickethistory_ticketId'],
|
||
['table' => 'zp_read', 'columns' => ['userId', 'module', 'moduleId'], 'name' => 'idx_read_userId_module_moduleId'],
|
||
['table' => 'zp_relationuserproject', 'columns' => ['userId', 'projectId'], 'name' => 'idx_relationuserproject_userId_projectId'],
|
||
['table' => 'zp_comment', 'columns' => ['moduleId', 'module', 'date'], 'name' => 'idx_comment_moduleId_module_date'],
|
||
];
|
||
|
||
foreach ($indexes as $index) {
|
||
try {
|
||
if (! Schema::hasTable($index['table'])) {
|
||
continue;
|
||
}
|
||
|
||
// Check if index already exists before adding
|
||
$existingIndexes = collect(
|
||
$this->connection->select("SHOW INDEX FROM `{$index['table']}`")
|
||
)->pluck('Key_name')->unique()->toArray();
|
||
|
||
if (! in_array($index['name'], $existingIndexes)) {
|
||
Schema::table($index['table'], function (Blueprint $table) use ($index) {
|
||
$table->index($index['columns'], $index['name']);
|
||
});
|
||
}
|
||
} catch (\Exception $e) {
|
||
Log::error("Migration 30500: Failed to add index {$index['name']} on {$index['table']}: ".$e->getMessage());
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Migration 30501: Ensure zp_canvas.color column exists
|
||
*
|
||
* Migration 30411 added this column but could fail silently on MySQL servers
|
||
* with ANSI_QUOTES sql_mode due to DEFAULT "ocean" using double quotes
|
||
* (interpreted as identifier, not string literal). The SQL was fixed in #3284
|
||
* but users who already ran the broken migration have the DB version advanced
|
||
* past 30411 without the column actually existing.
|
||
*
|
||
* @return bool|array Returns true on success, array of errors on failure
|
||
*/
|
||
public function update_sql_30501(): bool|array
|
||
{
|
||
try {
|
||
if (Schema::hasTable('zp_canvas') && ! Schema::hasColumn('zp_canvas', 'color')) {
|
||
Schema::table('zp_canvas', function (Blueprint $table) {
|
||
$table->string('color', 50)->nullable()->default('ocean')->after('description');
|
||
});
|
||
}
|
||
} catch (\Exception $e) {
|
||
Log::error('Migration 30501: '.$e->getMessage());
|
||
|
||
return ['Migration 30501 failed: '.$e->getMessage()];
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Migration 30502: Create the WorkStructure tables (structures, elements,
|
||
* relationships, mappings) for the meta-model that orchestrates entities
|
||
* across domains. Mirrors SchemaBuilder::createWorkStructureTables().
|
||
*
|
||
* @return bool|array Returns true on success, array of errors on failure
|
||
*/
|
||
public function update_sql_30502(): bool|array
|
||
{
|
||
try {
|
||
if (! Schema::hasTable('zp_work_structures')) {
|
||
Schema::create('zp_work_structures', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->string('title', 255);
|
||
$table->text('description')->nullable();
|
||
$table->string('type', 50)->default('custom');
|
||
$table->integer('created_by')->nullable();
|
||
$table->json('meta')->nullable();
|
||
$table->dateTime('created_at')->nullable();
|
||
$table->dateTime('modified_at')->nullable();
|
||
|
||
$table->unique(['title'], 'idx_work_structures_title');
|
||
$table->index(['type'], 'idx_work_structures_type');
|
||
});
|
||
}
|
||
|
||
if (! Schema::hasTable('zp_work_structure_elements')) {
|
||
Schema::create('zp_work_structure_elements', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->unsignedBigInteger('structure_id');
|
||
$table->string('type_key', 50);
|
||
$table->string('label', 100);
|
||
$table->text('description')->nullable();
|
||
$table->string('domain_reference', 255)->nullable();
|
||
$table->integer('sort_order')->default(0);
|
||
$table->json('meta')->nullable();
|
||
$table->dateTime('created_at')->nullable();
|
||
|
||
$table->index(['structure_id'], 'idx_wse_structure_id');
|
||
$table->unique(['structure_id', 'type_key'], 'idx_wse_structure_type_key');
|
||
});
|
||
}
|
||
|
||
if (! Schema::hasTable('zp_work_structure_relationships')) {
|
||
Schema::create('zp_work_structure_relationships', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->unsignedBigInteger('structure_id');
|
||
$table->unsignedBigInteger('from_element_id');
|
||
$table->unsignedBigInteger('to_element_id');
|
||
$table->string('relationship_type', 50);
|
||
$table->text('description')->nullable();
|
||
$table->json('meta')->nullable();
|
||
|
||
$table->index(['structure_id'], 'idx_wsr_structure_id');
|
||
});
|
||
}
|
||
|
||
if (! Schema::hasTable('zp_work_structure_mappings')) {
|
||
Schema::create('zp_work_structure_mappings', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->unsignedBigInteger('source_structure_id');
|
||
$table->unsignedBigInteger('source_element_id');
|
||
$table->unsignedBigInteger('target_structure_id');
|
||
$table->unsignedBigInteger('target_element_id');
|
||
$table->string('mapping_type', 50)->default('generates');
|
||
$table->json('meta')->nullable();
|
||
|
||
$table->index(['source_structure_id', 'target_structure_id'], 'idx_wsm_source_target');
|
||
// Enforce the same idempotency key StructureRegistry::registerMappings()
|
||
// checks in application code, so a race can't insert a duplicate mapping
|
||
// for the same source element → target structure.
|
||
$table->unique(['source_structure_id', 'source_element_id', 'target_structure_id'], 'idx_wsm_unique_mapping');
|
||
});
|
||
}
|
||
} catch (\Exception $e) {
|
||
Log::error('Migration 30502: '.$e->getMessage());
|
||
|
||
return ['Migration 30502 failed: '.$e->getMessage()];
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Push notification fields on zp_access_tokens — refactor of the
|
||
* original (never-fired-in-production) zp_device_tokens table from
|
||
* the earlier #3401 design. Per discussion: lifecycle is naturally
|
||
* tied to the user's login session, so we piggyback the bearer-
|
||
* token row instead of maintaining a parallel table + prune cron.
|
||
* Logout invalidates the row → push registration dies with it.
|
||
*
|
||
* Columns added to zp_access_tokens:
|
||
* - push_token VARCHAR(255) NULL — opaque token (Expo
|
||
* or FCM, see provider)
|
||
* - push_platform VARCHAR(16) NULL — 'ios' or 'android'
|
||
* - push_provider VARCHAR(8) NULL — 'expo' or 'fcm'
|
||
* - push_token_updated_at TIMESTAMP NULL — refreshed each
|
||
* registerPushToken call
|
||
* - push_invalidated_at TIMESTAMP NULL — set by mobile
|
||
* unregister OR provider
|
||
* DeviceNotRegistered
|
||
*
|
||
* The zp_device_tokens table from #3401's update_sql_30503 is no
|
||
* longer used. Installs that ran 30503 will have an empty
|
||
* zp_device_tokens table sitting unused — safe to drop in a
|
||
* future cleanup PR; not dropped here to keep this migration
|
||
* additive-only.
|
||
*/
|
||
public function update_sql_30504(): bool|array
|
||
{
|
||
try {
|
||
// Self-heal a missing table rather than dying on it (#3706). zp_access_tokens is
|
||
// created by update_sql_30400, but that migration wrapped its statements in a
|
||
// swallow-everything try/catch and still returned success — so an install whose
|
||
// CREATE TABLE failed recorded 3.4.0 as applied with no table to show for it, and
|
||
// every later upgrade died here on "1146 Table 'zp_access_tokens' doesn't exist"
|
||
// with no way forward. Recreating it is safe: the table only holds API tokens, and
|
||
// an install that never had one has none to lose.
|
||
if (! Schema::hasTable('zp_access_tokens')) {
|
||
Log::warning('Migration 30504: zp_access_tokens missing, recreating it before adding push columns.');
|
||
app()->make(SchemaBuilder::class)->createAccessTokensTable();
|
||
}
|
||
|
||
Schema::table('zp_access_tokens', function (Blueprint $table) {
|
||
if (! Schema::hasColumn('zp_access_tokens', 'push_token')) {
|
||
$table->string('push_token', 255)->nullable()->after('expires_at');
|
||
}
|
||
if (! Schema::hasColumn('zp_access_tokens', 'push_platform')) {
|
||
$table->string('push_platform', 16)->nullable()->after('push_token');
|
||
}
|
||
if (! Schema::hasColumn('zp_access_tokens', 'push_provider')) {
|
||
$table->string('push_provider', 8)->nullable()->after('push_platform');
|
||
}
|
||
if (! Schema::hasColumn('zp_access_tokens', 'push_token_updated_at')) {
|
||
$table->timestamp('push_token_updated_at')->nullable()->after('push_provider');
|
||
}
|
||
if (! Schema::hasColumn('zp_access_tokens', 'push_invalidated_at')) {
|
||
$table->timestamp('push_invalidated_at')->nullable()->after('push_token_updated_at');
|
||
}
|
||
});
|
||
|
||
// Index for "active push tokens for user" — the dominant
|
||
// access pattern when dispatching push. Skipped if it
|
||
// already exists from a re-run.
|
||
$existingIndexes = collect(\Illuminate\Support\Facades\DB::select('SHOW INDEX FROM zp_access_tokens'))
|
||
->pluck('Key_name')
|
||
->toArray();
|
||
if (! in_array('idx_access_tokens_tokenable_push', $existingIndexes, true)) {
|
||
Schema::table('zp_access_tokens', function (Blueprint $table) {
|
||
$table->index(
|
||
['tokenable_id', 'push_invalidated_at'],
|
||
'idx_access_tokens_tokenable_push'
|
||
);
|
||
});
|
||
}
|
||
} catch (\Exception $e) {
|
||
Log::error('Migration 30504: '.$e->getMessage());
|
||
|
||
return ['Migration 30504 failed: '.$e->getMessage()];
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Migration 30518 — the single native-permission-engine migration for existing installs.
|
||
*
|
||
* Creates the three engine tables (zp_roles, zp_permissions, zp_role_permissions — mirroring
|
||
* {@see \Leantime\Domain\Install\Services\SchemaBuilder}, which covers fresh installs), then
|
||
* syncs the discovered `domain.action` vocabulary and seeds the six built-in roles with their
|
||
* default grants.
|
||
*
|
||
* This consolidates what was originally rolled out as 30505 (tables + first seed) plus the
|
||
* per-domain re-seeds 30506–30517 — each an idempotent `flush → sync → seed` that picked up
|
||
* one more domain's permission provider. syncDiscoveredPermissions() discovers ALL providers
|
||
* and seedBuiltInRoles() applies the FULL matrix, so one seed reproduces the entire sequence.
|
||
* Pre-release: no installed DB ran the intermediate versions, so collapsing them is safe and
|
||
* keeps the migration history honest.
|
||
*
|
||
* Idempotent + additive: re-running never drops an admin's customized zp_role_permissions
|
||
* rows. The registry cache is flushed first so a stale discovered-provider list can't cause a
|
||
* partial seed.
|
||
*
|
||
* @return bool|array True on success, array of error strings on failure.
|
||
*/
|
||
public function update_sql_30518(): bool|array
|
||
{
|
||
try {
|
||
// The legacy zp_roles rights table was dropped at update_sql_30002. If a stale copy
|
||
// survives on a very old install it lacks the 'level' column — replace it.
|
||
if (Schema::hasTable('zp_roles') && ! Schema::hasColumn('zp_roles', 'level')) {
|
||
Schema::drop('zp_roles');
|
||
}
|
||
|
||
if (! Schema::hasTable('zp_roles')) {
|
||
Schema::create('zp_roles', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->string('name', 50);
|
||
$table->string('displayName', 100)->nullable();
|
||
$table->integer('level');
|
||
$table->tinyInteger('isSystem')->default(0);
|
||
$table->text('description')->nullable();
|
||
$table->dateTime('createdOn')->nullable();
|
||
$table->dateTime('modified')->nullable();
|
||
|
||
$table->unique(['name'], 'idx_roles_name');
|
||
$table->index(['level'], 'idx_roles_level');
|
||
});
|
||
}
|
||
|
||
if (! Schema::hasTable('zp_permissions')) {
|
||
Schema::create('zp_permissions', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->string('permissionKey', 150);
|
||
$table->string('domain', 60);
|
||
$table->string('action', 100);
|
||
$table->string('label', 191)->nullable();
|
||
$table->tinyInteger('isProjectScoped')->default(1);
|
||
$table->dateTime('createdOn')->nullable();
|
||
$table->dateTime('modified')->nullable();
|
||
|
||
$table->unique(['permissionKey'], 'idx_permissions_key');
|
||
$table->index(['domain'], 'idx_permissions_domain');
|
||
});
|
||
}
|
||
|
||
if (! Schema::hasTable('zp_role_permissions')) {
|
||
Schema::create('zp_role_permissions', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->unsignedBigInteger('roleId');
|
||
$table->unsignedBigInteger('permissionId');
|
||
|
||
$table->unique(['roleId', 'permissionId'], 'idx_role_permissions_unique');
|
||
$table->index(['roleId'], 'idx_role_permissions_roleId');
|
||
$table->index(['permissionId'], 'idx_role_permissions_permissionId');
|
||
});
|
||
}
|
||
|
||
// Populate the vocabulary, then grant the built-in roles their defaults. Flush the
|
||
// discovered-provider cache first so every domain's permission provider is rediscovered.
|
||
app(\Leantime\Core\Auth\Permissions\PermissionRegistry::class)->flush();
|
||
|
||
$seeder = app(\Leantime\Core\Auth\Permissions\PermissionSeeder::class);
|
||
$seeder->syncDiscoveredPermissions();
|
||
$seeder->seedBuiltInRoles();
|
||
} catch (\Exception $e) {
|
||
Log::error('Migration 30518: '.$e->getMessage());
|
||
|
||
return ['Migration 30518 failed: '.$e->getMessage()];
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Migration 30519 — grant the new `plugins.manage` capability to existing installs.
|
||
*
|
||
* The plugin-management surface (marketplace/folder install, enable, disable, remove,
|
||
* discover) is now gated by `plugins.manage`. Fresh installs pick this up through the
|
||
* built-in seed, but installs already past 30518 have no row for the new key — which would
|
||
* deny everyone, including admins. Sync the vocabulary and grant the key to admin + owner
|
||
* only. This is a targeted grant, NOT a full reseed, so any custom role edits/revocations
|
||
* an operator has made are preserved.
|
||
*/
|
||
public function update_sql_30519(): bool|array
|
||
{
|
||
try {
|
||
app(\Leantime\Core\Auth\Permissions\PermissionRegistry::class)->flush();
|
||
|
||
$seeder = app(\Leantime\Core\Auth\Permissions\PermissionSeeder::class);
|
||
$seeder->syncDiscoveredPermissions();
|
||
|
||
$repo = app(\Leantime\Core\Auth\Permissions\PermissionRepository::class);
|
||
|
||
foreach (['admin', 'owner'] as $roleName) {
|
||
$role = $repo->getRoleByName($roleName);
|
||
|
||
if ($role !== null) {
|
||
$repo->grant((int) $role['id'], \Leantime\Domain\Plugins\Permissions\PluginsPermissions::MANAGE);
|
||
}
|
||
}
|
||
|
||
app(\Leantime\Core\Auth\Permissions\PermissionService::class)->flushCache();
|
||
} catch (\Exception $e) {
|
||
Log::error('Migration 30519: '.$e->getMessage());
|
||
|
||
return ['Migration 30519 failed: '.$e->getMessage()];
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Migration 30520 — grant the new api.manage and connector.manage capabilities to existing
|
||
* installs. The API-key management and Connector integration surfaces are now permission-gated;
|
||
* fresh installs pick these up through the built-in seed, but installs already on the permission
|
||
* engine have no row for the new keys — which would deny everyone, including admins. Sync the
|
||
* vocabulary and grant both keys to admin + owner only (targeted grant, not a full reseed, so
|
||
* any custom role edits/revocations are preserved).
|
||
*/
|
||
public function update_sql_30520(): bool|array
|
||
{
|
||
try {
|
||
app(\Leantime\Core\Auth\Permissions\PermissionRegistry::class)->flush();
|
||
|
||
$seeder = app(\Leantime\Core\Auth\Permissions\PermissionSeeder::class);
|
||
$seeder->syncDiscoveredPermissions();
|
||
|
||
$repo = app(\Leantime\Core\Auth\Permissions\PermissionRepository::class);
|
||
$keys = [
|
||
\Leantime\Domain\Api\Permissions\ApiPermissions::MANAGE,
|
||
\Leantime\Domain\Connector\Permissions\ConnectorPermissions::MANAGE,
|
||
];
|
||
|
||
foreach (['admin', 'owner'] as $roleName) {
|
||
$role = $repo->getRoleByName($roleName);
|
||
|
||
if ($role !== null) {
|
||
foreach ($keys as $key) {
|
||
$repo->grant((int) $role['id'], $key);
|
||
}
|
||
}
|
||
}
|
||
|
||
app(\Leantime\Core\Auth\Permissions\PermissionService::class)->flushCache();
|
||
} catch (\Exception $e) {
|
||
Log::error('Migration 30520: '.$e->getMessage());
|
||
|
||
return ['Migration 30520 failed: '.$e->getMessage()];
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Migration 30521 — reporting groundwork (#reporting-screens):
|
||
* - zp_tickets.outcomeImpact TEXT NULL: retrospective outcome & impact narrative on
|
||
* milestones ("what did completing this produce"), captured inline on the report
|
||
* screens and rolled up into plan/strategy board reports. Distinct from `description`,
|
||
* which is forward-looking.
|
||
* - zp_goal_history: append-only record of goal currentValue changes. Goal values were
|
||
* previously overwritten in place, making KPI trend reporting impossible; rows are
|
||
* written on every value change plus a daily snapshot, so trends become chartable
|
||
* once history accumulates.
|
||
*/
|
||
public function update_sql_30521(): bool|array
|
||
{
|
||
try {
|
||
if (Schema::hasTable('zp_tickets') && ! Schema::hasColumn('zp_tickets', 'outcomeImpact')) {
|
||
Schema::table('zp_tickets', function (Blueprint $table) {
|
||
$table->text('outcomeImpact')->nullable()->after('acceptanceCriteria');
|
||
});
|
||
}
|
||
|
||
if (! Schema::hasTable('zp_goal_history')) {
|
||
Schema::create('zp_goal_history', function (Blueprint $table) {
|
||
$table->increments('id');
|
||
$table->integer('itemId');
|
||
$table->double('value')->nullable();
|
||
$table->integer('userId')->nullable();
|
||
$table->dateTime('dateRecorded')->nullable();
|
||
|
||
$table->index(['itemId', 'dateRecorded'], 'idx_goal_history_item_date');
|
||
});
|
||
}
|
||
} catch (\Exception $e) {
|
||
Log::error('Migration 30521: '.$e->getMessage());
|
||
|
||
return ['Migration 30521 failed: '.$e->getMessage()];
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Migration 30522: adds two authored-meaning fields to canvas items.
|
||
*
|
||
* why_this_matters — the human change (Outcome and Impact items).
|
||
* starting_picture — the world today, before the work (Impact only).
|
||
*
|
||
* Both nullable, both additive. `conclusion` is untouched — going forward
|
||
* it narrows to "as measured by" methodology only. The report's Impact
|
||
* Journey page (Page 4) reads `why_this_matters` as the meaning lead
|
||
* where present, falling back to today's rendering when absent.
|
||
*/
|
||
public function update_sql_30522(): bool|array
|
||
{
|
||
try {
|
||
if (Schema::hasTable('zp_canvas_items')) {
|
||
if (! Schema::hasColumn('zp_canvas_items', 'why_this_matters')) {
|
||
Schema::table('zp_canvas_items', function (Blueprint $table) {
|
||
$table->text('why_this_matters')->nullable()->after('conclusion');
|
||
});
|
||
}
|
||
if (! Schema::hasColumn('zp_canvas_items', 'starting_picture')) {
|
||
Schema::table('zp_canvas_items', function (Blueprint $table) {
|
||
$table->text('starting_picture')->nullable()->after('why_this_matters');
|
||
});
|
||
}
|
||
}
|
||
} catch (\Exception $e) {
|
||
Log::error('Migration 30522: '.$e->getMessage());
|
||
|
||
return ['Migration 30522 failed: '.$e->getMessage()];
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* update_sql_30523 — database update for v3.5.23.
|
||
*
|
||
* Adds user-level capacity attributes so downstream resource-planning
|
||
* surfaces (starting with PgmPro's Resource Allocation tab) can
|
||
* distinguish an FTE's target from a PT's ceiling, a contractor's
|
||
* billable cap, and a volunteer's best-effort throughput.
|
||
*
|
||
* Both columns are nullable with no default and no backfill —
|
||
* per product decision: teach users to configure this explicitly
|
||
* rather than assume everyone is 40h/FTE. Downstream code treats
|
||
* NULL as "not configured" and prompts admins to set it before
|
||
* capacity warnings can fire.
|
||
*/
|
||
public function update_sql_30523(): bool|array
|
||
{
|
||
try {
|
||
if (! Schema::hasTable('zp_user')) {
|
||
return true;
|
||
}
|
||
|
||
if (! Schema::hasColumn('zp_user', 'weekly_hours')) {
|
||
Schema::table('zp_user', function (Blueprint $table) {
|
||
$table->integer('weekly_hours')->nullable()->after('hours');
|
||
});
|
||
}
|
||
if (! Schema::hasColumn('zp_user', 'employment_type')) {
|
||
Schema::table('zp_user', function (Blueprint $table) {
|
||
$table->string('employment_type', 20)->nullable()->after('weekly_hours');
|
||
});
|
||
}
|
||
} catch (\Exception $e) {
|
||
Log::error('Migration 30523: '.$e->getMessage());
|
||
|
||
return ['Migration 30523 failed: '.$e->getMessage()];
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* update_sql_30524 — database update for v3.5.24.
|
||
*
|
||
* Backfills the legacy single goal→milestone link (the varchar
|
||
* `zp_canvas_items.milestoneId` column, `box='goal'`) into many-to-many
|
||
* edges on the core `zp_entity_relationship` graph, so a goal can be
|
||
* tracked by any number of milestones. Each edge:
|
||
* entityA = goal canvas item (GoalItem), entityB = milestone (Ticket),
|
||
* relationship = 'tracked_by'.
|
||
*
|
||
* The `milestoneId` column is intentionally KEPT here — readers are cut
|
||
* over incrementally and a later migration drops it once nothing reads it.
|
||
*
|
||
* Written with the query builder (not raw REGEXP/CAST) so it is portable
|
||
* across MySQL/Postgres/MSSQL, and idempotent: junk values (empty, '0',
|
||
* non-numeric, deleted-milestone) are skipped and already-migrated pairs
|
||
* are not duplicated on re-run.
|
||
*/
|
||
public function update_sql_30524(): bool|array
|
||
{
|
||
try {
|
||
// Guard on the installer's own connection (not the global Schema
|
||
// facade, which checks the default connection) so the existence
|
||
// check matches the connection the migration queries run against
|
||
// (may be a temp/target install connection).
|
||
// DatabaseManager always resolves a concrete Connection here; the
|
||
// property is typed to the interface, which doesn't declare the
|
||
// schema-builder accessor, so narrow it for static analysis.
|
||
/** @var \Illuminate\Database\Connection $connection */
|
||
$connection = $this->connection;
|
||
$schema = $connection->getSchemaBuilder();
|
||
if (! $schema->hasTable('zp_canvas_items')
|
||
|| ! $schema->hasTable('zp_canvas')
|
||
|| ! $schema->hasTable('zp_entity_relationship')
|
||
|| ! $schema->hasTable('zp_tickets')
|
||
|| ! $schema->hasColumn('zp_canvas_items', 'milestoneId')) {
|
||
// Surface the skip in the update log — a partial install
|
||
// silently no-oping would be invisible otherwise.
|
||
Log::info('Migration 30524 skipped: required tables/columns missing (partial install?)');
|
||
|
||
return true;
|
||
}
|
||
|
||
// UTC — DB datetimes are stored in UTC; date() would use the server
|
||
// timezone and write a skewed createdOn.
|
||
$now = gmdate('Y-m-d H:i:s');
|
||
|
||
// Chunk the goal rows so a very large zp_canvas_items never loads
|
||
// into memory at once. Each chunk resolves its own live-milestone
|
||
// and existing-edge sets, scoped to the chunk's ids (no full-table
|
||
// scan), then batch-inserts.
|
||
$this->connection->table('zp_canvas_items')
|
||
->where('box', 'goal')
|
||
->whereNotNull('milestoneId')
|
||
->where('milestoneId', '<>', '')
|
||
->where('milestoneId', '<>', '0')
|
||
->orderBy('id')
|
||
->chunkById(500, function ($goals) use ($now): void {
|
||
// Numeric-only (goal, milestone) pairs. milestoneId is a
|
||
// varchar, so trim before the digit check.
|
||
$pairs = [];
|
||
$milestoneIds = [];
|
||
$canvasIds = [];
|
||
foreach ($goals as $g) {
|
||
$raw = trim((string) $g->milestoneId);
|
||
if (! ctype_digit($raw)) {
|
||
continue;
|
||
}
|
||
$mid = (int) $raw;
|
||
if ($mid <= 0) {
|
||
continue;
|
||
}
|
||
$pairs[] = ['goalId' => (int) $g->id, 'milestoneId' => $mid, 'author' => $g->author, 'canvasId' => (int) $g->canvasId];
|
||
$milestoneIds[$mid] = true;
|
||
$canvasIds[(int) $g->canvasId] = true;
|
||
}
|
||
|
||
if ($pairs === []) {
|
||
return;
|
||
}
|
||
|
||
$goalIds = array_values(array_unique(array_map(static fn ($p) => $p['goalId'], $pairs)));
|
||
|
||
// Drop edges to deleted milestones + dedup existing edges,
|
||
// both scoped to this chunk's ids — O(1) lookups, no N+1.
|
||
// The milestone lookup keeps projectId: links are
|
||
// same-project only (product rule), so a legacy
|
||
// cross-project row must NOT be promoted to an edge.
|
||
$liveTickets = [];
|
||
foreach (
|
||
$this->connection->table('zp_tickets')->whereIn('id', array_keys($milestoneIds))->where('type', 'milestone')->where('status', '<>', -1)->get(['id', 'projectId']) as $t
|
||
) {
|
||
$liveTickets[(int) $t->id] = (int) $t->projectId;
|
||
}
|
||
|
||
// Goal projects, resolved via each goal's canvas (chunk-scoped).
|
||
$projectByCanvas = [];
|
||
foreach (
|
||
$this->connection->table('zp_canvas')->whereIn('id', array_keys($canvasIds))->get(['id', 'projectId']) as $c
|
||
) {
|
||
$projectByCanvas[(int) $c->id] = (int) $c->projectId;
|
||
}
|
||
|
||
$existingEdges = [];
|
||
foreach (
|
||
$this->connection->table('zp_entity_relationship')
|
||
->where('relationship', EntityRelationshipEnum::TrackedBy->value)
|
||
->where('entityAType', 'GoalItem')
|
||
->where('entityBType', 'Ticket')
|
||
->whereIn('entityA', $goalIds)
|
||
->select('entityA', 'entityB')
|
||
->get() as $e
|
||
) {
|
||
$existingEdges[sprintf('%d:%d', (int) $e->entityA, (int) $e->entityB)] = true;
|
||
}
|
||
|
||
$rows = [];
|
||
foreach ($pairs as $p) {
|
||
if (! isset($liveTickets[$p['milestoneId']])) {
|
||
continue;
|
||
}
|
||
// Same-project only: skip legacy rows pointing at a
|
||
// milestone in a different project than the goal's.
|
||
if (! isset($projectByCanvas[$p['canvasId']])
|
||
|| $liveTickets[$p['milestoneId']] !== $projectByCanvas[$p['canvasId']]) {
|
||
continue;
|
||
}
|
||
if (isset($existingEdges[$p['goalId'].':'.$p['milestoneId']])) {
|
||
continue;
|
||
}
|
||
// Unknown author stays NULL ("unknown"), not 0 — 0 would
|
||
// read as a real user id in downstream joins/filters.
|
||
$author = (int) ($p['author'] ?? 0);
|
||
$rows[] = [
|
||
'entityA' => $p['goalId'],
|
||
'entityAType' => 'GoalItem',
|
||
'entityB' => $p['milestoneId'],
|
||
'entityBType' => 'Ticket',
|
||
'relationship' => EntityRelationshipEnum::TrackedBy->value,
|
||
'createdOn' => $now,
|
||
'createdBy' => $author > 0 ? $author : null,
|
||
'meta' => json_encode(['source' => 'milestoneId_migration']),
|
||
];
|
||
}
|
||
|
||
foreach (array_chunk($rows, 200) as $insertChunk) {
|
||
$this->connection->table('zp_entity_relationship')->insert($insertChunk);
|
||
}
|
||
});
|
||
} catch (\Exception $e) {
|
||
Log::error('Migration 30524: '.$e->getMessage());
|
||
|
||
return ['Migration 30524 failed: '.$e->getMessage()];
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* update_sql_30525 — database update for v3.5.25.
|
||
*
|
||
* Top-up backfill for the goal↔milestone edge migration. `update_sql_30524`
|
||
* ran when the edge model shipped, but goal↔milestone assignments made
|
||
* after that point and before dual-write went live were written to the
|
||
* legacy `milestoneId` column only. Re-running the (idempotent) 30524
|
||
* backfill captures those stragglers as `tracked_by` edges; goals already
|
||
* migrated are skipped.
|
||
*/
|
||
public function update_sql_30525(): bool|array
|
||
{
|
||
$result = $this->update_sql_30524();
|
||
|
||
// Re-label a delegated failure so upgrade logs/output point at the step
|
||
// that actually ran (30525), not the 30524 delegate.
|
||
if (is_array($result)) {
|
||
return ['Migration 30525 failed (delegated to 30524): '.implode('; ', array_map('strval', $result))];
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* update_sql_30526 — database update for v3.5.26.
|
||
*
|
||
* Hygiene pass over the goal↔milestone `tracked_by` edges:
|
||
* 1. Removes CROSS-PROJECT edges — links are same-project only (product
|
||
* rule), but the original 30524/30525 backfill promoted legacy
|
||
* cross-project `milestoneId` rows into edges before the guard existed.
|
||
* 2. Removes ORPHANED edges — a milestone deleted through the generic
|
||
* ticket-delete path (which historically skipped the goal-detach
|
||
* cascade) or a goal removed out-of-band leaves edges pointing at
|
||
* rows that no longer exist.
|
||
*
|
||
* Query-builder only (portable) and naturally idempotent: a clean graph
|
||
* yields zero deletions.
|
||
*/
|
||
public function update_sql_30526(): bool|array
|
||
{
|
||
try {
|
||
/** @var \Illuminate\Database\Connection $connection */
|
||
$connection = $this->connection;
|
||
$schema = $connection->getSchemaBuilder();
|
||
if (! $schema->hasTable('zp_entity_relationship')
|
||
|| ! $schema->hasTable('zp_canvas_items')
|
||
|| ! $schema->hasTable('zp_canvas')
|
||
|| ! $schema->hasTable('zp_tickets')) {
|
||
// Surface the skip in the update log — a partial install
|
||
// silently no-oping would be invisible otherwise.
|
||
Log::info('Migration 30526 skipped: required tables missing (partial install?)');
|
||
|
||
return true;
|
||
}
|
||
|
||
// Cross-project edges: goal's canvas project != milestone's project.
|
||
$crossProject = $this->connection->table('zp_entity_relationship as er')
|
||
->join('zp_canvas_items as ci', 'er.entityA', '=', 'ci.id')
|
||
->join('zp_canvas as cb', 'ci.canvasId', '=', 'cb.id')
|
||
->join('zp_tickets as t', 'er.entityB', '=', 't.id')
|
||
->where('er.relationship', EntityRelationshipEnum::TrackedBy->value)
|
||
->where('er.entityAType', 'GoalItem')
|
||
->where('er.entityBType', 'Ticket')
|
||
->whereColumn('t.projectId', '<>', 'cb.projectId')
|
||
->pluck('er.id')
|
||
->all();
|
||
|
||
// Orphans: entityA no longer a goal item, or entityB no longer a
|
||
// live milestone ticket.
|
||
$orphaned = $this->connection->table('zp_entity_relationship as er')
|
||
->leftJoin('zp_canvas_items as ci', function ($join): void {
|
||
$join->on('er.entityA', '=', 'ci.id')->where('ci.box', '=', 'goal');
|
||
})
|
||
->leftJoin('zp_tickets as t', function ($join): void {
|
||
// "Live" = milestone-type AND not soft-deleted — matches
|
||
// the addGoalMilestoneLink chokepoint's definition, so an
|
||
// edge to a deleted milestone counts as orphaned.
|
||
$join->on('er.entityB', '=', 't.id')
|
||
->where('t.type', '=', 'milestone')
|
||
->where('t.status', '<>', -1);
|
||
})
|
||
->where('er.relationship', EntityRelationshipEnum::TrackedBy->value)
|
||
->where('er.entityAType', 'GoalItem')
|
||
->where('er.entityBType', 'Ticket')
|
||
->where(function ($q): void {
|
||
$q->whereNull('ci.id')->orWhereNull('t.id');
|
||
})
|
||
->pluck('er.id')
|
||
->all();
|
||
|
||
$ids = array_values(array_unique(array_map('intval', array_merge($crossProject, $orphaned))));
|
||
foreach (array_chunk($ids, 500) as $chunk) {
|
||
$this->connection->table('zp_entity_relationship')->whereIn('id', $chunk)->delete();
|
||
}
|
||
} catch (\Exception $e) {
|
||
Log::error('Migration 30526: '.$e->getMessage());
|
||
|
||
return ['Migration 30526 failed: '.$e->getMessage()];
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* update_sql_30527 — BOM (bill-of-materials) module tables + permission grants.
|
||
*
|
||
* Creates the zp_bom / zp_bom_item / zp_bom_column / zp_bom_source tables and
|
||
* grants the `bom.*` capability vocabulary to the built-in roles (admin/owner get
|
||
* everything; editor/manager get the standard project-scoped verbs via the central
|
||
* role matrix on fresh installs, so here we only backfill admin/owner explicitly,
|
||
* mirroring 30519/30520).
|
||
*/
|
||
public function update_sql_30527(): bool|array
|
||
{
|
||
try {
|
||
if (! Schema::hasTable('zp_bom')) {
|
||
Schema::create('zp_bom', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->integer('projectId')->nullable();
|
||
$table->string('bomNo', 100)->nullable();
|
||
$table->string('productName', 255)->nullable();
|
||
$table->string('specification', 255)->nullable();
|
||
$table->string('drawingNo', 100)->nullable();
|
||
$table->string('version', 50)->nullable();
|
||
$table->string('status', 20)->nullable()->default('0');
|
||
$table->text('remark')->nullable();
|
||
$table->dateTime('createdOn')->nullable();
|
||
$table->dateTime('modifiedOn')->nullable();
|
||
|
||
$table->index(['projectId'], 'idx_bom_projectId');
|
||
});
|
||
}
|
||
|
||
if (! Schema::hasTable('zp_bom_item')) {
|
||
Schema::create('zp_bom_item', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->unsignedBigInteger('bomId');
|
||
$table->integer('seq')->nullable()->default(0);
|
||
$table->string('partNo', 100)->nullable();
|
||
$table->string('partName', 255)->nullable();
|
||
$table->string('partDrawingNo', 100)->nullable();
|
||
$table->string('material', 100)->nullable();
|
||
$table->string('spec', 255)->nullable();
|
||
$table->string('qtyPerUnit', 50)->nullable();
|
||
$table->string('unit', 50)->nullable();
|
||
$table->string('process', 100)->nullable();
|
||
$table->text('remark')->nullable();
|
||
$table->text('extra')->nullable();
|
||
|
||
$table->index(['bomId'], 'idx_bom_item_bomId');
|
||
});
|
||
}
|
||
|
||
if (! Schema::hasTable('zp_bom_column')) {
|
||
Schema::create('zp_bom_column', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->unsignedBigInteger('bomId');
|
||
$table->string('key', 191);
|
||
$table->string('label', 191)->nullable();
|
||
$table->integer('sortOrder')->nullable()->default(0);
|
||
|
||
$table->index(['bomId'], 'idx_bom_column_bomId');
|
||
$table->unique(['bomId', 'key'], 'idx_bom_column_unique');
|
||
});
|
||
}
|
||
|
||
if (! Schema::hasTable('zp_bom_source')) {
|
||
Schema::create('zp_bom_source', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->unsignedBigInteger('bomId');
|
||
$table->string('name', 255)->nullable();
|
||
$table->string('baseUrl', 1000)->nullable();
|
||
$table->string('tableId', 100)->nullable();
|
||
$table->text('token')->nullable();
|
||
$table->string('remark', 500)->nullable();
|
||
$table->dateTime('createdOn')->nullable();
|
||
|
||
$table->index(['bomId'], 'idx_bom_source_bomId');
|
||
});
|
||
}
|
||
|
||
// Backfill the bom.* vocabulary to admin + owner. Fresh installs get the
|
||
// editor/manager grants from the central role matrix via the seed below.
|
||
app(\Leantime\Core\Auth\Permissions\PermissionRegistry::class)->flush();
|
||
$seeder = app(\Leantime\Core\Auth\Permissions\PermissionSeeder::class);
|
||
$seeder->syncDiscoveredPermissions();
|
||
|
||
$repo = app(\Leantime\Core\Auth\Permissions\PermissionRepository::class);
|
||
$keys = [
|
||
\Leantime\Domain\Bom\Permissions\BomPermissions::VIEW,
|
||
\Leantime\Domain\Bom\Permissions\BomPermissions::CREATE,
|
||
\Leantime\Domain\Bom\Permissions\BomPermissions::EDIT,
|
||
\Leantime\Domain\Bom\Permissions\BomPermissions::DELETE,
|
||
];
|
||
|
||
foreach (['admin', 'owner'] as $roleName) {
|
||
$role = $repo->getRoleByName($roleName);
|
||
if ($role !== null) {
|
||
foreach ($keys as $key) {
|
||
$repo->grant((int) $role['id'], $key);
|
||
}
|
||
}
|
||
}
|
||
|
||
app(\Leantime\Core\Auth\Permissions\PermissionService::class)->flushCache();
|
||
} catch (\Exception $e) {
|
||
Log::error('Migration 30527: '.$e->getMessage());
|
||
|
||
return ['Migration 30527 failed: '.$e->getMessage()];
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* update_sql_30528 — BOM 列显隐支持:zp_bom 增加 hiddenColumns(隐藏列 key 的 JSON 数组)。
|
||
*/
|
||
public function update_sql_30528(): bool|array
|
||
{
|
||
try {
|
||
if (Schema::hasTable('zp_bom') && ! Schema::hasColumn('zp_bom', 'hiddenColumns')) {
|
||
Schema::table('zp_bom', function (Blueprint $table) {
|
||
$table->text('hiddenColumns')->nullable()->after('remark');
|
||
});
|
||
}
|
||
} catch (\Exception $e) {
|
||
Log::error('Migration 30528: '.$e->getMessage());
|
||
|
||
return ['Migration 30528 failed: '.$e->getMessage()];
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* update_sql_30529 — 全局主数据体系:
|
||
* 1. zp_bom 增加 type 列(bom/process/tooling 三类主数据复用同一套表);
|
||
* 2. 项目引用层三张表:项目↔主数据关联、项目级追加列、项目级追加值(单向,不写回全局)。
|
||
*/
|
||
public function update_sql_30529(): bool|array
|
||
{
|
||
try {
|
||
if (Schema::hasTable('zp_bom') && ! Schema::hasColumn('zp_bom', 'type')) {
|
||
Schema::table('zp_bom', function (Blueprint $table) {
|
||
$table->string('type', 20)->nullable()->default('bom')->after('id');
|
||
});
|
||
}
|
||
|
||
if (! Schema::hasTable('zp_master_ref')) {
|
||
Schema::create('zp_master_ref', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->unsignedBigInteger('projectId');
|
||
$table->unsignedBigInteger('masterId'); // zp_bom.id(任意 type)
|
||
$table->dateTime('createdOn')->nullable();
|
||
|
||
$table->index(['projectId'], 'idx_mref_project');
|
||
$table->index(['masterId'], 'idx_mref_master');
|
||
$table->unique(['projectId', 'masterId'], 'idx_mref_unique');
|
||
});
|
||
}
|
||
|
||
if (! Schema::hasTable('zp_master_ref_column')) {
|
||
Schema::create('zp_master_ref_column', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->unsignedBigInteger('refId');
|
||
$table->string('key', 191);
|
||
$table->string('label', 191)->nullable();
|
||
$table->integer('sortOrder')->nullable()->default(0);
|
||
|
||
$table->index(['refId'], 'idx_mrefcol_ref');
|
||
$table->unique(['refId', 'key'], 'idx_mrefcol_unique');
|
||
});
|
||
}
|
||
|
||
if (! Schema::hasTable('zp_master_ref_value')) {
|
||
Schema::create('zp_master_ref_value', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->unsignedBigInteger('refId');
|
||
$table->unsignedBigInteger('columnId');
|
||
$table->unsignedBigInteger('itemId'); // zp_bom_item.id(全局明细行)
|
||
$table->text('value')->nullable();
|
||
|
||
$table->index(['refId', 'itemId'], 'idx_mrefval_refitem');
|
||
$table->unique(['refId', 'columnId', 'itemId'], 'idx_mrefval_unique');
|
||
});
|
||
}
|
||
} catch (\Exception $e) {
|
||
Log::error('Migration 30529: '.$e->getMessage());
|
||
|
||
return ['Migration 30529 failed: '.$e->getMessage()];
|
||
}
|
||
|
||
return true;
|
||
}
|
||
}
|