diff --git a/README.md b/README.md index 6bbf220..a0b986e 100644 --- a/README.md +++ b/README.md @@ -17,17 +17,24 @@ nix run # installs vendors on first run, starts MariaDB, serves b - site: http://127.0.0.1:8000 - CMS: http://127.0.0.1:8001 -The site's article pages need the production schema, which only exists in the prod DB: +### 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. + +To work with real content, load a production dump (drops and recreates the local DB): ```sh -mysqldump greaterc_db2024 > dump.sql # on the server -nix run . -- db import dump.sql +nix run . -- db import .dev/greaterc_db2024.sql # phpMyAdmin export, kept out of git +nix run . -- db reset # back to empty schema ``` `nix run . -- ` forwards to the `dev` script; `nix develop` drops you into a shell with `dev`, `php`, `composer`, `mariadb`, `node` on PATH (direnv: `.envrc` → `use flake`). -Other commands: `dev db start|stop|status|shell`, `dev serve frontend|backend`, +Other commands: `dev db start|stop|status|shell|reset|import`, `dev serve frontend|backend`, `dev artisan ...`, `dev down`. See `dev help`. The local DB listens on `127.0.0.1:3307` (override with `DEV_DB_PORT`); the `dev` script diff --git a/dev/dev b/dev/dev index 54a2cac..17b9985 100755 --- a/dev/dev +++ b/dev/dev @@ -29,9 +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) + db start|stop|status manage local MariaDB (data in .dev/mysql, port $DB_PORT); start applies dev/schema.sql db shell mysql client on the local DB - db import load a production dump (schema lives only in the prod 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) 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 @@ -81,9 +82,19 @@ 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 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" +} + +db_recreate() { + mariadb --socket="$MYSQL_SOCK" -uroot -e "DROP DATABASE IF EXISTS \`$DB_NAME\`; CREATE DATABASE \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" +} + db_stop() { db_running || { echo "db: not running"; return; } mariadb-admin --socket="$MYSQL_SOCK" -uroot shutdown @@ -99,14 +110,24 @@ db_status() { fi } +# phpMyAdmin dumps carry bare CREATE TABLEs, so start from an empty database. db_import() { local dump="${1:?usage: dev db import }" + [ -f "$dump" ] || { echo "db: no such file: $dump" >&2; exit 1; } db_running || db_start + db_recreate mariadb --socket="$MYSQL_SOCK" -uroot "$DB_NAME" <"$dump" echo "db: imported $dump" mariadb --socket="$MYSQL_SOCK" -uroot "$DB_NAME" -e "SHOW TABLES" } +db_reset() { + db_running || db_start + db_recreate + db_apply_schema + echo "db: reset to dev/schema.sql" +} + composer_install() { for app in "$FRONTEND" "$BACKEND"; do [ -f "$app/.env" ] || { echo "missing $app/.env" >&2; exit 1; } @@ -118,7 +139,7 @@ serve() { local dir port dir="$(app_dir "$1")" case "$1" in frontend) port="$FRONTEND_PORT" ;; backend) port="$BACKEND_PORT" ;; esac - cd "$dir" && exec php artisan serve --host=127.0.0.1 --port="$port" + cd "$dir" && exec php artisan serve --no-reload --host=127.0.0.1 --port="$port" } up() { @@ -135,15 +156,16 @@ up() { db_stop } trap cleanup EXIT INT TERM - (cd "$FRONTEND" && php artisan serve --host=127.0.0.1 --port="$FRONTEND_PORT") & pids+=($!) - (cd "$BACKEND" && php artisan serve --host=127.0.0.1 --port="$BACKEND_PORT") & pids+=($!) + # --no-reload: without it `artisan serve` drops our DB_*/APP_* overrides from the php -S child env. + (cd "$FRONTEND" && php artisan serve --no-reload --host=127.0.0.1 --port="$FRONTEND_PORT") & pids+=($!) + (cd "$BACKEND" && php artisan serve --no-reload --host=127.0.0.1 --port="$BACKEND_PORT") & pids+=($!) echo "frontend: http://127.0.0.1:$FRONTEND_PORT" echo "backend: http://127.0.0.1:$BACKEND_PORT" wait } down() { - pkill -f "artisan serve --host=127.0.0.1 --port=($FRONTEND_PORT|$BACKEND_PORT)" 2>/dev/null || true + pkill -f "artisan serve --no-reload --host=127.0.0.1 --port=($FRONTEND_PORT|$BACKEND_PORT)" 2>/dev/null || true db_stop } @@ -157,6 +179,7 @@ case "$cmd" in stop) db_stop ;; status) db_status ;; shell) db_running || db_start; exec mariadb --socket="$MYSQL_SOCK" -uroot "$DB_NAME" ;; + reset) db_reset ;; import) db_import "$@" ;; *) usage; exit 2 ;; esac ;; diff --git a/dev/schema.sql b/dev/schema.sql new file mode 100644 index 0000000..f1317dc --- /dev/null +++ b/dev/schema.sql @@ -0,0 +1,131 @@ +-- 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);