Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,16 @@ LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
# 開発は compose.yaml の PostgreSQL。`podman compose up -d` または
# `docker compose up -d` で起動する。receiptbuilder と同時起動できるよう host port は 55433 を使う。
# 運用サーバでは接続先と認証情報を差し替え、DB_SSLMODE=require を検討する。
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=55433
DB_DATABASE=quotebuilder
DB_USERNAME=quotebuilder
DB_PASSWORD=quotebuilder
# DB_SSLMODE=require

SESSION_DRIVER=database
SESSION_LIFETIME=120
Expand Down
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Frontend checks

on:
pull_request:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: frontend-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
frontend:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6

- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '22'
cache: npm

- name: Install dependencies
run: npm ci --ignore-scripts

- name: Type check
run: npm run type-check

- name: Build
run: npm run build
63 changes: 63 additions & 0 deletions .github/workflows/php-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: PHP tests

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
feature-tests:
runs-on: ubuntu-latest
env:
APP_ENV: testing
DB_CONNECTION: pgsql
DB_HOST: 127.0.0.1
DB_PORT: 55433
DB_DATABASE: quotebuilder_test
DB_USERNAME: quotebuilder
DB_PASSWORD: quotebuilder
DB_URL: ''
services:
postgres:
image: postgres:17
env:
POSTGRES_DB: quotebuilder_test
POSTGRES_USER: quotebuilder
POSTGRES_PASSWORD: quotebuilder
ports:
- 55433:5432
options: >-
--health-cmd "pg_isready -U quotebuilder -d quotebuilder_test"
--health-interval 5s
--health-timeout 3s
--health-retries 10

steps:
- uses: actions/checkout@v6

- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.5'
Comment thread
strnh marked this conversation as resolved.
extensions: pdo_pgsql, pdo_sqlite, sqlite3
coverage: none

- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y poppler-utils sqlite3

- name: Install dependencies
run: composer install --no-interaction --prefer-dist

- name: Prepare application
run: |
cp .env.example .env
php artisan key:generate

- name: Verify PostgreSQL migrations
run: php artisan migrate:fresh --env=testing --force

- name: Run tests
run: php artisan test
49 changes: 40 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@

## コンセプト

- 個人〜少人数、PC1台での運用を前提とした最小構成(SQLite・単一サーバー)
- 個人〜少人数での運用を前提とした最小構成(PostgreSQL・単一サーバー)
- 過去の見積ファイル(xlsx/ods、quotebuilder 出力PDF)を取り込んで移行できる
- データは利用者自身の手元に置き、バックアップ・復元も自己完結できる
- データは利用者が管理するPostgreSQLに置き、JSON形式で移行・復元できる

## 技術スタック

| 層 | 技術 |
|---|---|
| Backend | PHP 8.3 / Laravel 13 |
| Frontend | React 19 + TypeScript / Vite / Tailwind CSS 4(SPA、同一オリジン配信) |
| DB | SQLite(PostgreSQL対応は検討中: [#27](https://github.com/strnh/quotebuilder/issues/27)) |
| DB | PostgreSQL 17(SQLiteは旧環境からの移行・SQLite専用フルバックアップの互換用途) |
| 取込 | PhpSpreadsheet(xlsx / ods) |
| 集計表示 | Recharts |
| テスト | PHPUnit(Feature)/ Playwright(E2E)/ tsc |
Expand Down Expand Up @@ -52,7 +52,7 @@
**バックアップ / リストア**

- JSONエクスポート・リストア(データ移行向け。旧形式 version 1 の取込にも対応)
- SQLiteフルSQL dump(障害復旧向け。一貫スナップショット + メタデータ生成)
- SQLiteフルSQL dump(旧SQLite環境の障害復旧向け。一貫スナップショット + メタデータ生成)
- データ管理画面からの作成・一覧・ダウンロード、保持ルールによる世代管理
- 操作の監査ログ記録(`backup_audit_logs`)、backup系APIの admin 認可

Expand All @@ -70,7 +70,7 @@
| バックアップ管理画面の拡充 | [#22](https://github.com/strnh/quotebuilder/issues/22) | 画面からの復元操作 |
| 差分バックアップ | [#23](https://github.com/strnh/quotebuilder/issues/23) | 方式設計とMVP範囲の確定 |
| 見積一覧のTAB化 | [#17](https://github.com/strnh/quotebuilder/issues/17) | ステータス別TABでの一覧表示 |
| PostgreSQL対応 | [#27](https://github.com/strnh/quotebuilder/issues/27) | DB pluginの検討 |
| SQLite legacy DB連携 | [#27](https://github.com/strnh/quotebuilder/issues/27) | PostgreSQLからsqlite_fdwで旧DBを参照する方式の検討 |
| Windows簡易アプリ化 | [#2](https://github.com/strnh/quotebuilder/issues/2) | NativePHPによる非技術者向け配布 |

さらに先の構想として、OAuth2 / MFA(メール)による認証強化があります(#5 に記載)。
Expand Down Expand Up @@ -104,7 +104,16 @@ cd quotebuilder
./scripts/init.sh # 依存導入・.env生成・migrate・ビルドまで一括
```

または `composer run setup` でも同等の初期化ができます。開発サーバーの起動は:
初期化には PHP の `pdo_pgsql` 拡張と、Podman Compose または Docker Compose が必要です。
開発用PostgreSQLは `compose.yaml` で起動し、receiptbuilder (`55432`) と同時に使えるよう
`127.0.0.1:55433` で待ち受けます。手動で起動・停止する場合は次のコマンドを使います。

```bash
podman compose up -d # Dockerの場合は docker compose up -d
podman compose down # volumeを残して停止
```

PostgreSQLを手動で起動済みなら、`composer run setup` で依存導入・migration・ビルドを実行できます。開発サーバーの起動は:

```bash
composer run dev # serve + queue + logs + vite を並行起動
Expand All @@ -113,17 +122,39 @@ composer run dev # serve + queue + logs + vite を並行起動
テスト:

```bash
php artisan test # Feature テスト
php artisan test # PostgreSQLのテストDBでFeatureテスト
npm run type-check # TypeScript 型検査
npm run test:e2e # Playwright E2E
```

### 既存SQLite環境からの移行

移行前にSQLiteファイル自体を退避し、業務データをJSONへエクスポートします。

```bash
cp database/database.sqlite database/database.sqlite.pre-postgresql
php artisan backup:export --output=storage/app/quotebuilder-migration.json
```

その後 `.env` のDB設定を `.env.example` のPostgreSQL設定へ変更し、PostgreSQLを起動して取り込みます。

```bash
podman compose up -d # Dockerの場合は docker compose up -d
php artisan migrate --force
php artisan backup:import storage/app/quotebuilder-migration.json
php artisan admin:create
```

JSONにはユーザー、監査ログ、ジョブなどは含まれないため、管理者アカウントは移行先で再作成します。履歴を含む完全移行が必要な場合は、退避したSQLiteファイルを保持してください。

## バックアップ

用途に応じて2種類のバックアップを使い分けます。

- `php artisan backup:export`: 基本情報・顧客・取引先識別子・見積データをJSONで出力します。内容を確認しやすく、別環境へのデータ移行や競合を制御した取込み向けです。schema、ユーザー、ジョブなどは含みません。
- `php artisan backup:sqlite-dump`: SQLite DB全体をschemaとdataを含むSQLで出力します。同時書き込みに対して一貫したスナップショットを作り、障害復旧用のフルバックアップとして使用します。SQLite以外では実行できません。
- `php artisan backup:export`: 基本情報・顧客・取引先識別子・見積データをJSONで出力します。PostgreSQL環境間や旧SQLite環境からのデータ移行、競合を制御した取込み向けです。schema、ユーザー、ジョブなどは含みません。
- `php artisan backup:sqlite-dump`: 旧SQLite環境のDB全体をschemaとdataを含むSQLで出力します。SQLite接続でのみ実行でき、PostgreSQLの障害復旧用途には使用できません。

PostgreSQLの障害復旧バックアップは、運用基盤側で `pg_dump` またはマネージドDBのスナップショットを構成してください。アプリの「保存済みフルバックアップ」作成・復元は現時点ではSQLite専用で、PostgreSQL対応は [#63](https://github.com/strnh/quotebuilder/issues/63) で扱います。JSONバックアップとリストアはPostgreSQLでも利用できます。

フルバックアップは、既定では `storage/app/private/backups/` にSQLと `<SQLファイル>.meta.json` を生成します。出力先は次のように指定できます。

Expand Down
31 changes: 31 additions & 0 deletions app/Services/BackupRestorer.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,42 @@ public function restore(array $data, string $mode): array
}
}
$this->restoreRows('settlement_schedules', $scheduleRows, $mode, $summary, $encodeItems);

$this->resetPostgresSequences([
'sender_profiles',
'customers',
'customer_signatures',
'quotes',
'settlements',
'settlement_schedules',
]);
});

return $summary;
}

/**
* バックアップ側の id を明示挿入した後、PostgreSQL の採番位置を現在の最大値へ進める。
* SQLite は INTEGER PRIMARY KEY が最大値から自動採番するため追加処理は不要。
*
* @param list<string> $tables
*/
private function resetPostgresSequences(array $tables): void
{
$connection = DB::connection();
if ($connection->getDriverName() !== 'pgsql') {
return;
}

foreach ($tables as $table) {
$quotedTable = $connection->getQueryGrammar()->wrapTable($table);
$connection->statement(
"SELECT setval(pg_get_serial_sequence(?, 'id'), COALESCE(MAX(id), 1), MAX(id) IS NOT NULL) FROM {$quotedTable}",
[$table],
);
}
}

/**
* テーブルダンプとして最低限必要な行形式・主キー・NOT NULL カラムを検証する。
* ここで弾かないと DB の制約違反(500)になるカラムのみを対象とする。
Expand Down
24 changes: 24 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 開発用 PostgreSQL。podman compose / docker compose のどちらでも動く。
# receiptbuilder (55432) と同時起動できるよう host port は 55433 を使う。

services:
db:
image: docker.io/library/postgres:17
restart: unless-stopped
Comment thread
Copilot marked this conversation as resolved.
environment:
POSTGRES_DB: quotebuilder
POSTGRES_USER: quotebuilder
POSTGRES_PASSWORD: quotebuilder
ports:
- "127.0.0.1:55433:5432"
volumes:
- quotebuilder-pgdata:/var/lib/postgresql/data
- ./docker/postgres/init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U quotebuilder -d quotebuilder"]
interval: 5s
timeout: 3s
retries: 10

volumes:
quotebuilder-pgdata:
1 change: 0 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi"
],
"pre-package-uninstall": [
Expand Down
2 changes: 1 addition & 1 deletion config/database.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
|
*/

'default' => env('DB_CONNECTION', 'sqlite'),
'default' => env('DB_CONNECTION', 'pgsql'),

/*
|--------------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions config/queue.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
*/

'batching' => [
'database' => env('DB_CONNECTION', 'sqlite'),
'database' => env('DB_CONNECTION', 'pgsql'),
'table' => 'job_batches',
],

Expand All @@ -122,7 +122,7 @@

'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
'database' => env('DB_CONNECTION', 'sqlite'),
'database' => env('DB_CONNECTION', 'pgsql'),
'table' => 'failed_jobs',
],

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public function up(): void
->whereNotNull('customer_signature')
->where('customer_signature', '!=', '')
->groupBy('sig')
->having('n', '>', 1)
// PostgreSQL は HAVING 句から SELECT の別名を参照できないため、集約式を直接指定する。
->havingRaw('count(*) > 1')
->pluck('sig');

if ($duplicates->isNotEmpty()) {
Expand Down
3 changes: 3 additions & 0 deletions docker/postgres/init/01-create-test-database.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- phpunit 用の DB。RefreshDatabase が各テストの状態を初期化するため、開発用 DB とは分ける。
-- このスクリプトは volume が空の初回起動時のみ実行される。
CREATE DATABASE quotebuilder_test OWNER quotebuilder;
12 changes: 9 additions & 3 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,15 @@
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="BROADCAST_CONNECTION" value="null"/>
<env name="CACHE_STORE" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="DB_URL" value=""/>
<!-- 本番と同じ PostgreSQL driver で migration と Feature test を検証する。
quotebuilder_test は compose.yaml の初期化SQLが作成する。 -->
<env name="DB_CONNECTION" value="pgsql" force="true"/>
<env name="DB_HOST" value="127.0.0.1" force="true"/>
<env name="DB_PORT" value="55433" force="true"/>
<env name="DB_DATABASE" value="quotebuilder_test" force="true"/>
<env name="DB_USERNAME" value="quotebuilder" force="true"/>
<env name="DB_PASSWORD" value="quotebuilder" force="true"/>
<env name="DB_URL" value="" force="true"/>
Comment thread
strnh marked this conversation as resolved.
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
Expand Down
2 changes: 1 addition & 1 deletion resources/js/data/adapters/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// REST API アダプター(Laravel + SQLite バックエンド)。
// REST API アダプター(Laravel + PostgreSQL バックエンド)。
// /api/{customers,sender-profiles,quotes} の apiResource を呼ぶ。
// 認証は Sanctum SPA(同一オリジン・session cookie)。全リクエストで cookie を送り、
// 変更系は CSRF トークン(XSRF-TOKEN cookie → X-XSRF-TOKEN ヘッダ)を付ける。
Expand Down
2 changes: 1 addition & 1 deletion resources/js/data/store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// データアクセス層。アダプターを切り替えて使う。
// - 'api' : Laravel + SQLite バックエンド(既定)
// - 'api' : Laravel + PostgreSQL バックエンド(既定)
// - 'local' : localStorage 完結(オフライン/デモ/e2e テスト用)
// 切替: localStorage.setItem('zensales:adapter', 'local')
import * as apiAdapter from './adapters/api';
Expand Down
Loading