diff --git a/README.md b/README.md index a0b986e..46f09fe 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,14 @@ nix run # installs vendors on first run, starts MariaDB, serves b ### Database -`dev/schema.sql` is the schema (10 tables, no `category` table — `category_id` 1/2 is hardcoded). -It is derived from the production dump and applied automatically on every `db start` -(idempotent), so a fresh checkout serves empty pages with no 500s. There are no Laravel -migrations for these tables; edit `dev/schema.sql` when prod changes. +The schema is owned by the CMS: `think-backend.greaterchiangmai.com/database/migrations/` +(10 tables; no `category` table — `category_id` 1/2 is hardcoded). `dev db start` runs +`artisan migrate` there on every start, so a fresh checkout serves empty pages with no 500s. + +`2025_11_03_000000_create_content_tables` is a baseline that reproduces the production +tables byte-for-byte (verified by diffing `mariadb-dump --no-data` against the prod export); +its creates are guarded with `Schema::hasTable`, so running `migrate` on production only +records it. Add new migrations in the backend for any schema change; the site app has none. To work with real content, load a production dump (drops and recreates the local DB): diff --git a/dev/dev b/dev/dev index 17b9985..40d4193 100755 --- a/dev/dev +++ b/dev/dev @@ -29,10 +29,10 @@ usage() { Usage: dev setup composer install both apps + init local MariaDB - db start|stop|status manage local MariaDB (data in .dev/mysql, port $DB_PORT); start applies dev/schema.sql + db start|stop|status manage local MariaDB (data in .dev/mysql, port $DB_PORT); start runs backend migrations db shell mysql client on the local DB db import drop + recreate the DB from a production dump (schema + data) - db reset drop + recreate the DB with dev/schema.sql only (empty tables) + db reset drop + recreate the DB, then migrate (empty tables) serve frontend php artisan serve -> http://127.0.0.1:$FRONTEND_PORT serve backend php artisan serve -> http://127.0.0.1:$BACKEND_PORT up db + both apps; Ctrl-C stops everything @@ -64,6 +64,7 @@ db_start() { db_init if db_running; then echo "db: already running (pid $(cat "$MYSQL_PID"))" + db_migrate return fi # Ignore terminal Ctrl-C in the DB so `up` can shut it down cleanly after the app servers. @@ -82,13 +83,14 @@ CREATE USER IF NOT EXISTS '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASS'; GRANT ALL ON \`$DB_NAME\`.* TO '$DB_USER'@'127.0.0.1', '$DB_USER'@'localhost'; FLUSH PRIVILEGES; SQL - db_apply_schema + db_migrate echo "db: listening on 127.0.0.1:$DB_PORT (pid $(cat "$MYSQL_PID"))" } -# Idempotent (CREATE TABLE IF NOT EXISTS), so safe on every start; a no-op once tables exist. -db_apply_schema() { - mariadb --socket="$MYSQL_SOCK" -uroot "$DB_NAME" <"$ROOT/dev/schema.sql" +# The schema lives in think-backend/database/migrations; `migrate` is a no-op once applied. +db_migrate() { + [ -f "$BACKEND/vendor/autoload.php" ] || composer_install + (cd "$BACKEND" && php artisan migrate --force --no-interaction) } db_recreate() { @@ -124,8 +126,8 @@ db_import() { db_reset() { db_running || db_start db_recreate - db_apply_schema - echo "db: reset to dev/schema.sql" + db_migrate + echo "db: reset to empty schema" } composer_install() { diff --git a/dev/schema.sql b/dev/schema.sql deleted file mode 100644 index f1317dc..0000000 --- a/dev/schema.sql +++ /dev/null @@ -1,131 +0,0 @@ --- Schema for greaterc_db2024, derived from the production phpMyAdmin dump (MariaDB 10.6, 2025-11-03). --- Shared by think.greaterchiangmai.com (site) and think-backend.greaterchiangmai.com (CMS). --- No Laravel migrations exist for these tables; this file is the source of truth for local dev. --- Applied automatically by `dev db start` on an empty database. Idempotent. - -SET NAMES utf8mb4; -SET time_zone = "+00:00"; - --- category_id: 1 = Chiang Mai ("no-where-else"), 2 = Where Else. There is no `category` table in prod. -CREATE TABLE IF NOT EXISTS `sub_category` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `category_id` int(11) DEFAULT NULL, - `due_date` datetime DEFAULT NULL, - `name` text DEFAULT NULL, - `name_en` text DEFAULT NULL, - `description_en` longblob DEFAULT NULL, - `description` longblob DEFAULT NULL, - `active` int(11) DEFAULT 1 COMMENT '0 = inactive, 1 = active', - `created_at` datetime NOT NULL, - `updated_at` datetime NOT NULL, - `image_url` text DEFAULT NULL, - `image_name` text DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; - --- sub_category_ids: comma-separated sub_category.id list, queried with FIND_IN_SET. --- description / description_en: Editor.js JSON stored as binary. -CREATE TABLE IF NOT EXISTS `article` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `category_id` int(11) DEFAULT NULL, - `sub_category_ids` text DEFAULT NULL, - `due_date` datetime DEFAULT NULL, - `name` text DEFAULT NULL, - `name_en` text DEFAULT NULL, - `description_en` longblob DEFAULT NULL, - `description` longblob DEFAULT NULL, - `active` int(11) DEFAULT 1 COMMENT '0 = inactive, 1 = active', - `created_at` datetime NOT NULL, - `updated_at` datetime NOT NULL, - `image_url` text DEFAULT NULL, - `image_name` text DEFAULT NULL, - `count_view` int(11) DEFAULT 0, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; - -CREATE TABLE IF NOT EXISTS `folder` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `name` varchar(255) NOT NULL, - `folder_type` int(11) NOT NULL, - `created_at` datetime NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; - -CREATE TABLE IF NOT EXISTS `file_document` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `name` text DEFAULT NULL, - `user_id` int(11) DEFAULT NULL, - `folder_id` int(11) DEFAULT NULL, - `created_at` datetime DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; - -CREATE TABLE IF NOT EXISTS `file_image` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `name` text DEFAULT NULL, - `user_id` int(11) DEFAULT NULL, - `folder_id` int(11) DEFAULT NULL, - `created_at` datetime DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; - -CREATE TABLE IF NOT EXISTS `file_sound` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `name` text DEFAULT NULL, - `user_id` int(11) DEFAULT NULL, - `folder_id` int(11) DEFAULT NULL, - `created_at` datetime DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; - -CREATE TABLE IF NOT EXISTS `file_video` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `name` text DEFAULT NULL, - `user_id` int(11) DEFAULT NULL, - `folder_id` int(11) DEFAULT NULL, - `created_at` datetime DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; - -CREATE TABLE IF NOT EXISTS `users` ( - `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, - `first_name` varchar(255) DEFAULT NULL, - `last_name` varchar(255) DEFAULT NULL, - `phone` varchar(10) DEFAULT NULL, - `email` varchar(255) NOT NULL, - `password` varchar(255) NOT NULL, - `remember_token` varchar(100) DEFAULT NULL, - `role` int(11) DEFAULT NULL COMMENT '0 = Admin, 1 = Editor, 2 = Contributor', - `active` int(11) DEFAULT 1 COMMENT '0 = inactive, 1 = active', - `image_url` varchar(255) DEFAULT NULL, - `image_name` varchar(255) DEFAULT NULL, - `forgot_password_token` varchar(255) DEFAULT NULL, - `forgot_password_date` datetime DEFAULT NULL, - `created_at` timestamp NULL DEFAULT NULL, - `updated_at` timestamp NULL DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `users_email_unique` (`email`), - KEY `id` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; - --- Laravel database cache driver (CACHE_DRIVER=database); the only table with a migration record. -CREATE TABLE IF NOT EXISTS `cache` ( - `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, - `key` varchar(255) NOT NULL, - `value` text NOT NULL, - `expiration` int(10) UNSIGNED DEFAULT NULL, - `created_at` timestamp NULL DEFAULT NULL, - `updated_at` timestamp NULL DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `cache_key_unique` (`key`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -CREATE TABLE IF NOT EXISTS `migrations` ( - `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, - `migration` varchar(255) NOT NULL, - `batch` int(11) NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -INSERT IGNORE INTO `migrations` (`id`, `migration`, `batch`) VALUES -(1, '2024_08_25_154254_create_cache_table', 1); diff --git a/think-backend.greaterchiangmai.com/app/Providers/AppServiceProvider.php b/think-backend.greaterchiangmai.com/app/Providers/AppServiceProvider.php index 452e6b6..709de87 100644 --- a/think-backend.greaterchiangmai.com/app/Providers/AppServiceProvider.php +++ b/think-backend.greaterchiangmai.com/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; +use Laravel\Sanctum\Sanctum; class AppServiceProvider extends ServiceProvider { @@ -11,7 +12,8 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + // Sanctum tokens are unused (only the stock /api/user route); prod has no personal_access_tokens table. + Sanctum::ignoreMigrations(); } /** diff --git a/think-backend.greaterchiangmai.com/database/migrations/2014_10_12_000000_create_users_table.php b/think-backend.greaterchiangmai.com/database/migrations/2014_10_12_000000_create_users_table.php deleted file mode 100644 index 444fafb..0000000 --- a/think-backend.greaterchiangmai.com/database/migrations/2014_10_12_000000_create_users_table.php +++ /dev/null @@ -1,32 +0,0 @@ -id(); - $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); - $table->rememberToken(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('users'); - } -}; diff --git a/think-backend.greaterchiangmai.com/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php b/think-backend.greaterchiangmai.com/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php deleted file mode 100644 index 81a7229..0000000 --- a/think-backend.greaterchiangmai.com/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php +++ /dev/null @@ -1,28 +0,0 @@ -string('email')->primary(); - $table->string('token'); - $table->timestamp('created_at')->nullable(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('password_reset_tokens'); - } -}; diff --git a/think-backend.greaterchiangmai.com/database/migrations/2019_08_19_000000_create_failed_jobs_table.php b/think-backend.greaterchiangmai.com/database/migrations/2019_08_19_000000_create_failed_jobs_table.php deleted file mode 100644 index 249da81..0000000 --- a/think-backend.greaterchiangmai.com/database/migrations/2019_08_19_000000_create_failed_jobs_table.php +++ /dev/null @@ -1,32 +0,0 @@ -id(); - $table->string('uuid')->unique(); - $table->text('connection'); - $table->text('queue'); - $table->longText('payload'); - $table->longText('exception'); - $table->timestamp('failed_at')->useCurrent(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('failed_jobs'); - } -}; diff --git a/think-backend.greaterchiangmai.com/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php b/think-backend.greaterchiangmai.com/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php deleted file mode 100644 index e828ad8..0000000 --- a/think-backend.greaterchiangmai.com/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php +++ /dev/null @@ -1,33 +0,0 @@ -id(); - $table->morphs('tokenable'); - $table->string('name'); - $table->string('token', 64)->unique(); - $table->text('abilities')->nullable(); - $table->timestamp('last_used_at')->nullable(); - $table->timestamp('expires_at')->nullable(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('personal_access_tokens'); - } -}; diff --git a/think-backend.greaterchiangmai.com/database/migrations/2024_08_25_154254_create_cache_table.php b/think-backend.greaterchiangmai.com/database/migrations/2024_08_25_154254_create_cache_table.php new file mode 100644 index 0000000..b3c3ac7 --- /dev/null +++ b/think-backend.greaterchiangmai.com/database/migrations/2024_08_25_154254_create_cache_table.php @@ -0,0 +1,25 @@ +id(); + $table->string('key')->unique(); + $table->text('value'); + $table->unsignedInteger('expiration')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('cache'); + } +}; diff --git a/think-backend.greaterchiangmai.com/database/migrations/2025_11_03_000000_create_content_tables.php b/think-backend.greaterchiangmai.com/database/migrations/2025_11_03_000000_create_content_tables.php new file mode 100644 index 0000000..3195c84 --- /dev/null +++ b/think-backend.greaterchiangmai.com/database/migrations/2025_11_03_000000_create_content_tables.php @@ -0,0 +1,118 @@ +createUnlessExists('sub_category', function (Blueprint $table) { + $table->integer('id')->autoIncrement(); + $table->integer('category_id')->nullable(); + $table->dateTime('due_date')->nullable(); + $table->text('name')->nullable(); + $table->text('name_en')->nullable(); + $table->binary('description_en')->nullable(); + $table->binary('description')->nullable(); + $table->integer('active')->nullable()->default(1)->comment('0 = inactive, 1 = active'); + $table->dateTime('created_at'); + $table->dateTime('updated_at'); + $table->text('image_url')->nullable(); + $table->text('image_name')->nullable(); + }, longblob: ['description_en', 'description']); + + // sub_category_ids: comma-separated sub_category.id list, queried with FIND_IN_SET. + // description / description_en: Editor.js JSON stored as longblob. + $this->createUnlessExists('article', function (Blueprint $table) { + $table->integer('id')->autoIncrement(); + $table->integer('category_id')->nullable(); + $table->text('sub_category_ids')->nullable(); + $table->dateTime('due_date')->nullable(); + $table->text('name')->nullable(); + $table->text('name_en')->nullable(); + $table->binary('description_en')->nullable(); + $table->binary('description')->nullable(); + $table->integer('active')->nullable()->default(1)->comment('0 = inactive, 1 = active'); + $table->dateTime('created_at'); + $table->dateTime('updated_at'); + $table->text('image_url')->nullable(); + $table->text('image_name')->nullable(); + $table->integer('count_view')->nullable()->default(0); + }, longblob: ['description_en', 'description']); + + $this->createUnlessExists('folder', function (Blueprint $table) { + $table->engine = 'MyISAM'; + $table->collation = 'utf8mb3_unicode_ci'; + $table->integer('id')->autoIncrement(); + $table->string('name'); + $table->integer('folder_type'); + $table->dateTime('created_at'); + }); + + foreach (['file_document', 'file_image', 'file_sound', 'file_video'] as $name) { + $this->createUnlessExists($name, function (Blueprint $table) { + $table->integer('id')->autoIncrement(); + $table->text('name')->nullable(); + $table->integer('user_id')->nullable(); + $table->integer('folder_id')->nullable(); + $table->dateTime('created_at')->nullable(); + }); + } + + $this->createUnlessExists('users', function (Blueprint $table) { + $table->id(); + $table->string('first_name')->nullable(); + $table->string('last_name')->nullable(); + $table->string('phone', 10)->nullable(); + $table->string('email')->unique(); + $table->string('password'); + $table->rememberToken(); + $table->integer('role')->nullable()->comment('0 = Admin, 1 = Editor, 2 = Contributor'); + $table->integer('active')->nullable()->default(1)->comment('0 = inactive, 1 = active'); + $table->string('image_url')->nullable(); + $table->string('image_name')->nullable(); + $table->string('forgot_password_token')->nullable(); + $table->dateTime('forgot_password_date')->nullable(); + $table->timestamps(); + $table->index('id', 'id'); + }); + } + + public function down(): void + { + foreach (['users', 'file_video', 'file_sound', 'file_image', 'file_document', 'folder', 'article', 'sub_category'] as $name) { + Schema::dropIfExists($name); + } + } + + /** + * @param string[] $longblob Blueprint has no longblob type (binary() is BLOB); widen these after create. + */ + private function createUnlessExists(string $name, Closure $define, array $longblob = []): void + { + if (Schema::hasTable($name)) { + return; + } + + Schema::create($name, function (Blueprint $table) use ($define) { + $table->charset = 'utf8mb3'; + $table->collation = 'utf8mb3_general_ci'; + $define($table); + }); + + foreach ($longblob as $column) { + DB::statement("ALTER TABLE `$name` MODIFY `$column` LONGBLOB NULL DEFAULT NULL"); + } + } +}; diff --git a/think.greaterchiangmai.com/database/migrations/2014_10_12_000000_create_users_table.php b/think.greaterchiangmai.com/database/migrations/2014_10_12_000000_create_users_table.php deleted file mode 100644 index 444fafb..0000000 --- a/think.greaterchiangmai.com/database/migrations/2014_10_12_000000_create_users_table.php +++ /dev/null @@ -1,32 +0,0 @@ -id(); - $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); - $table->rememberToken(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('users'); - } -}; diff --git a/think.greaterchiangmai.com/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php b/think.greaterchiangmai.com/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php deleted file mode 100644 index 81a7229..0000000 --- a/think.greaterchiangmai.com/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php +++ /dev/null @@ -1,28 +0,0 @@ -string('email')->primary(); - $table->string('token'); - $table->timestamp('created_at')->nullable(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('password_reset_tokens'); - } -}; diff --git a/think.greaterchiangmai.com/database/migrations/2019_08_19_000000_create_failed_jobs_table.php b/think.greaterchiangmai.com/database/migrations/2019_08_19_000000_create_failed_jobs_table.php deleted file mode 100644 index 249da81..0000000 --- a/think.greaterchiangmai.com/database/migrations/2019_08_19_000000_create_failed_jobs_table.php +++ /dev/null @@ -1,32 +0,0 @@ -id(); - $table->string('uuid')->unique(); - $table->text('connection'); - $table->text('queue'); - $table->longText('payload'); - $table->longText('exception'); - $table->timestamp('failed_at')->useCurrent(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('failed_jobs'); - } -}; diff --git a/think.greaterchiangmai.com/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php b/think.greaterchiangmai.com/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php deleted file mode 100644 index e828ad8..0000000 --- a/think.greaterchiangmai.com/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php +++ /dev/null @@ -1,33 +0,0 @@ -id(); - $table->morphs('tokenable'); - $table->string('name'); - $table->string('token', 64)->unique(); - $table->text('abilities')->nullable(); - $table->timestamp('last_used_at')->nullable(); - $table->timestamp('expires_at')->nullable(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('personal_access_tokens'); - } -};