Files
Leantime/app/Domain/Install/Repositories/Install.php

3460 lines
144 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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,
// 3050530517 (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 3050630517 — 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;
}
}