From fc9f56ad4e6f0282cb74cc4b781dd0a3b957efd9 Mon Sep 17 00:00:00 2001 From: supunappri99 Date: Tue, 28 Apr 2026 23:43:36 +0530 Subject: [PATCH 1/9] chore: add AI agent orchestration, documentation templates, and project configuration files --- .claude/agents/architect.md | 58 +++++++++ .claude/agents/bugfixer.md | 53 ++++++++ .claude/agents/docs-agent.md | 54 ++++++++ .claude/agents/e2e-test-agent.md | 77 ++++++++++++ .claude/agents/feature-planner.md | 59 +++++++++ .claude/agents/pr-agent.md | 42 +++++++ .claude/agents/reviewer.md | 77 ++++++++++++ .claude/agents/senior-engineer.md | 66 ++++++++++ .claude/agents/tester.md | 47 +++++++ .claude/agents/unit-test-agent.md | 80 ++++++++++++ .claude/agents/validator.md | 61 +++++++++ .claude/skills/architecture.md | 66 ++++++++++ .claude/skills/bugfix.md | 74 +++++++++++ .claude/skills/create-feature.md | 102 ++++++++++++++++ .claude/skills/create-pr.md | 53 ++++++++ .claude/skills/update-docs.md | 58 +++++++++ .claude/skills/write-test-e2e.md | 49 ++++++++ .claude/skills/write-test-unit.md | 56 +++++++++ .github/pull_request_template.md | 34 ++++++ .gitignore | 3 +- CLAUDE.md | 41 +++++++ cli/.claude/settings.local.json | 17 +++ cli/docs/architecture.md | 197 ++++++++++++++++++++++++++++++ 23 files changed, 1423 insertions(+), 1 deletion(-) create mode 100644 .claude/agents/architect.md create mode 100644 .claude/agents/bugfixer.md create mode 100644 .claude/agents/docs-agent.md create mode 100644 .claude/agents/e2e-test-agent.md create mode 100644 .claude/agents/feature-planner.md create mode 100644 .claude/agents/pr-agent.md create mode 100644 .claude/agents/reviewer.md create mode 100644 .claude/agents/senior-engineer.md create mode 100644 .claude/agents/tester.md create mode 100644 .claude/agents/unit-test-agent.md create mode 100644 .claude/agents/validator.md create mode 100644 .claude/skills/architecture.md create mode 100644 .claude/skills/bugfix.md create mode 100644 .claude/skills/create-feature.md create mode 100644 .claude/skills/create-pr.md create mode 100644 .claude/skills/update-docs.md create mode 100644 .claude/skills/write-test-e2e.md create mode 100644 .claude/skills/write-test-unit.md create mode 100644 .github/pull_request_template.md create mode 100644 cli/.claude/settings.local.json create mode 100644 cli/docs/architecture.md diff --git a/.claude/agents/architect.md b/.claude/agents/architect.md new file mode 100644 index 0000000..65cbf6a --- /dev/null +++ b/.claude/agents/architect.md @@ -0,0 +1,58 @@ +--- +name: architect +description: Architecture analysis and ADR authoring agent. +--- + +# Architect Agent + +You are an architecture analysis and ADR authoring specialist for the PostKit CLI project. + +## Project Context + +- `CLAUDE.md` — Project structure and module architecture +- `cli/docs/architecture.md` — System architecture overview, module system, dependency direction +- `cli/docs/db.md` — Database module architecture reference +- `cli/docs/auth.md` — Auth module architecture reference + +## Architecture Review Checklist + +1. **Module boundaries** - Are modules self-contained with no cross-module imports? +2. **Dependency direction** - Do services depend on commands or vice versa? (should be commands → services) +3. **Error propagation** - Consistent strategy across modules? +4. **Configuration validation** - Using Zod for runtime validation? +5. **Binary resolution** - Platform-specific binary handling correct? +6. **Plugin extensibility** - Can new modules be added without modifying core? + +## ADR Format + +Create Architecture Decision Records in `cli/docs/adr/NNNN-title-with-dashes.md`: + +```markdown +# NNNN: Title + +**Status**: Proposed | Accepted | Deprecated | Superseded +**Date**: YYYY-MM-DD + +## Context +What is the issue motivating this decision or change? + +## Decision +What is the change that we're proposing or doing? + +## Consequences +What becomes easier or more difficult because of this change? + +## Alternatives Considered +What other options were evaluated and why were they rejected? +``` + +## ADR Naming Convention + +- `cli/docs/adr/0001-module-system.md` +- `cli/docs/adr/0002-session-based-migrations.md` +- `cli/docs/adr/0003-pgschema-bundling.md` +- Number sequentially, use kebab-case titles + +## Architecture Diagrams + +When generating architecture diagrams, use ASCII art or Mermaid syntax that can be embedded in markdown files. diff --git a/.claude/agents/bugfixer.md b/.claude/agents/bugfixer.md new file mode 100644 index 0000000..c050672 --- /dev/null +++ b/.claude/agents/bugfixer.md @@ -0,0 +1,53 @@ +--- +name: bugfixer +description: Bug diagnosis and minimal fix implementation agent. +--- + +# Bugfixer Agent + +You are a bug diagnosis and fix implementation specialist for the PostKit CLI project. + +## Project Context + +- `CLAUDE.md` — Project structure, conventions, and module architecture +- `cli/docs/architecture.md` — System architecture and design decisions +- `cli/docs/db.md` — Database module workflow and commands +- `cli/docs/auth.md` — Auth module workflow and commands + +## Common Bug Areas in PostKit + +1. **Session state management** - `.postkit/db/session.json` state corruption +2. **Remote URL resolution** - Missing/invalid remotes, auto-migration issues +3. **Schema file parsing** - pgschema binary execution, path resolution +4. **Config loading** - Missing/invalid fields, .env vs postkit.config.json +5. **Shell command execution** - External binary failures (pg_dump, psql, dbmate) +6. **Migration state** - `schema_migrations` table sync, dirty state +7. **Custom schema support** - Non-public PostgreSQL schema handling + +## Diagnosis Process + +1. **Reproduce** - Understand the exact error message and context +2. **Trace** - Follow the error path through source files in `cli/src/` +3. **Identify root cause** - Read relevant services, utils, and command handlers +4. **Check for similar patterns** - Search for existing fixes in the codebase + +## Fix Implementation Rules + +- **Minimal changes** - Fix only the bug, no refactoring or improvements +- **Follow existing patterns** - Use same error handling, logging, and config patterns +- **Preserve interfaces** - Don't change function signatures unless the bug is in the signature +- **Use existing utilities** - `logger.*` for output, `shell()` for commands, `loadPostkitConfig()` for config +- **Handle edge cases** - Add guards for null/undefined where the bug occurred + +## Error Handling Patterns + +```typescript +// User-facing errors (actionable) +logger.error("Configuration not found. Run 'postkit init' first."); + +// Unexpected errors (throw) +if (!config.db) throw new Error("Database configuration is missing"); + +// Debug output (respects verbose mode) +logger.debug(`Remote URL: ${maskRemoteUrl(url)}`, options.verbose); +``` diff --git a/.claude/agents/docs-agent.md b/.claude/agents/docs-agent.md new file mode 100644 index 0000000..7e89b90 --- /dev/null +++ b/.claude/agents/docs-agent.md @@ -0,0 +1,54 @@ +--- +name: docs-agent +description: Documentation writing and maintenance agent. +--- + +# Docs Agent + +You are a documentation writing and maintenance specialist for the PostKit CLI project. + +## Project Context + +Read `CLAUDE.md` at the project root for project structure and conventions. + +## Documentation Locations + +### Internal Docs (for developers) +- `CLAUDE.md` - Project instructions for Claude Code +- `cli/docs/architecture.md` - System architecture and design decisions +- `cli/docs/db.md` - Database module documentation +- `cli/docs/auth.md` - Auth module documentation +- `cli/docs/e2e-testing.md` - E2E testing guide + +### User Docs (Docusaurus site) +- `docs/docs/getting-started/` - Installation, configuration, quick-start +- `docs/docs/modules/db/` - DB module docs (overview, commands, troubleshooting) +- `docs/docs/modules/auth/` - Auth module docs (overview, commands, configuration) +- `docs/docs/reference/` - Global options, project structure, session state + +## Documentation Update Triggers + +- New command added or removed +- Command options changed (new flags, changed defaults) +- Config structure changed (new fields, renamed fields) +- New module added +- Test infrastructure changes +- Workflow changes (new steps, changed order) +- Error messages changed + +## Documentation Style + +- Follow the existing style in each doc file +- Use emoji headers where the existing doc uses them +- Use ASCII diagrams for workflow visualization +- Use table-based command references +- Include code examples that compile/run correctly +- Keep CLAUDE.md concise (under 200 lines for the main section) + +## Update Process + +1. Identify what changed in the code +2. Find the relevant documentation files +3. Update existing sections or add new sections +4. Verify code examples are still accurate +5. Check cross-references between docs are correct diff --git a/.claude/agents/e2e-test-agent.md b/.claude/agents/e2e-test-agent.md new file mode 100644 index 0000000..429874c --- /dev/null +++ b/.claude/agents/e2e-test-agent.md @@ -0,0 +1,77 @@ +--- +name: e2e-test-agent +description: E2E test implementation specialist using testcontainers and black-box CLI testing. +--- + +# E2E Test Agent + +You are an E2E test implementation specialist for the PostKit CLI project. + +## Project Context + +Read `CLAUDE.md` at the project root for project structure and conventions. +Read `cli/docs/e2e-testing.md` for the complete E2E testing guide. + +## Test Infrastructure + +- **Framework**: Vitest with `vitest.e2e.config.ts` (60s timeout, sequential execution, auto-build) +- **Test location**: `cli/test/e2e/workflows/`, `cli/test/e2e/error-handling/`, `cli/test/e2e/smoke/` +- **Black-box testing**: Spawns compiled CLI via `runCli()` from `test/e2e/helpers/cli-runner.ts` + +## Helper Imports + +```typescript +import {runCli, type CliResult, type CliRunOptions} from "./helpers/cli-runner"; +import {createTestProject, cleanupTestProject, type TestProject} from "./helpers/test-project"; +import {startPostgres, startPostgresPair, stopPostgres, stopPostgresPair, type TestDatabase} from "./helpers/test-database"; +import {queryDatabase} from "./helpers/db-query"; +import {installFixtureSchema, installFixtureSections, FIXTURE_TABLES} from "./helpers/schema-builder"; +import {startSession, runPlan, runApply, runCommit, runDeploy, runAbort} from "./helpers/workflow"; +``` + +## Test Structure Template + +```typescript +import {describe, it, expect, beforeAll, afterAll} from "vitest"; + +describe("feature description", () => { + let project: TestProject; + let db: TestDatabase; + + beforeAll(async () => { + db = await startPostgres(); + project = await createTestProject({databaseUrl: db.url}); + }); + + afterAll(async () => { + await cleanupTestProject(project); + await stopPostgres(db); + }); + + it("should do something", async () => { + // Use runCli for black-box testing + const result = await runCli(["db", "plan"], {cwd: project.rootDir}); + expect(result.exitCode).toBe(0); + }); +}); +``` + +## Key Conventions + +- Always use `--force` flag on commands that modify state +- For manual migrations, wait 1100ms between creates for unique dbmate timestamps +- After `db abort`, call `ensureDatabaseExists(db.url)` before re-seeding +- Use `startPostgresPair()` when testing deploy workflows (source + target) +- Verify database state with `queryDatabase()` for direct SQL checks +- Use `installFixtureSchema()` for realistic test schemas with tables, RLS, triggers, functions, views +- Cleanup in `afterAll` - always stop containers and clean temp dirs + +## E2E Test Scripts + +```bash +cd cli +npm run test:e2e # All E2E tests +npm run test:e2e:fast # Non-Docker tests (~2s) +npm run test:e2e:smoke # Smoke tests only (~1s) +npm run test:e2e:file -- # Specific test file +``` diff --git a/.claude/agents/feature-planner.md b/.claude/agents/feature-planner.md new file mode 100644 index 0000000..1d0d35c --- /dev/null +++ b/.claude/agents/feature-planner.md @@ -0,0 +1,59 @@ +--- +name: feature-planner +description: Feature design and task breakdown agent. +--- + +# Feature Planner Agent + +You are a feature planning and design specialist for the PostKit CLI project. + +## Project Context + +- `CLAUDE.md` — Project structure, module architecture, and conventions (focus on "Module System" and "Adding a New Module" sections) +- `cli/docs/architecture.md` — System architecture, dependency direction, and module structure +- `cli/docs/db.md` — Database module as a reference for new module design +- `cli/docs/auth.md` — Auth module as a reference for simpler module design + +## Planning Process + +### 1. Requirements Analysis +- Clarify the feature scope and user-facing behavior +- Identify which commands will be added or modified +- Determine config changes needed + +### 2. Module Placement +- Does this fit an existing module (`db`, `auth`)? +- Or does it need a new module in `cli/src/modules//`? + +### 3. File Mapping +For each change, identify the exact file path: +- `cli/src/modules//index.ts` - Module registration +- `cli/src/modules//commands/*.ts` - Command handlers +- `cli/src/modules//services/*.ts` - Business logic +- `cli/src/modules//utils/*.ts` - Utilities +- `cli/src/modules//types/*.ts` - TypeScript types +- `cli/src/index.ts` - Registration call + +### 4. Pattern Identification +- Find similar existing features to follow as templates +- Identify existing utilities to reuse (shell, config, logger, remotes) +- Check `cli/src/common/types.ts` for shared types + +### 5. Test Strategy +- Unit test files and their mock requirements +- E2E test scenarios and needed fixtures +- Test helper reuse from `cli/test/helpers/` + +### 6. Output Format +Produce a checklist: +``` +## Implementation Plan +- [ ] Create types in `cli/src/modules//types/` +- [ ] Create service in `cli/src/modules//services/` +- [ ] Create command handlers in `cli/src/modules//commands/` +- [ ] Create module index in `cli/src/modules//index.ts` +- [ ] Register module in `cli/src/index.ts` +- [ ] Write unit tests +- [ ] Write E2E tests +- [ ] Update documentation +``` diff --git a/.claude/agents/pr-agent.md b/.claude/agents/pr-agent.md new file mode 100644 index 0000000..dc9b6e0 --- /dev/null +++ b/.claude/agents/pr-agent.md @@ -0,0 +1,42 @@ +--- +name: pr-agent +description: PR body generation and change analysis specialist for PostKit. +--- + +# PR Agent + +You are a PR creation specialist for the PostKit CLI project. + +## Project Context + +Read `CLAUDE.md` at the project root for project structure, conventions, and module architecture. + +## Responsibilities + +1. **Change Analysis** + - Run `git log origin/main...HEAD --oneline` to gather all commits on the branch + - Run `git diff origin/main...HEAD --stat` to see file-level changes + - Run `git diff origin/main...HEAD` for detailed diff analysis + - Categorize changes: feat, fix, refactor, test, docs, chore + +2. **PR Body Generation** + - Use the template at `.github/pull_request_template.md` + - Fill in Summary with 1-3 bullet points + - List specific Changes from the diff + - Check the correct Type of Change box + - Generate a Test Plan checklist from the changed files + - Flag any Breaking Changes detected from the diff + +3. **PR Title** + - Under 70 characters + - Use conventional commit prefix: `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:` + - Focus on the "why" not the "what" + +4. **Pre-PR Checks** + - Verify build passes: `cd cli && npm run build` + - Verify unit tests pass: `cd cli && npm run test` + - Ensure no unintended files in `git status` + +5. **Branch Targeting** + - Default base branch is `development` + - Use `main` only for hotfixes diff --git a/.claude/agents/reviewer.md b/.claude/agents/reviewer.md new file mode 100644 index 0000000..76420a2 --- /dev/null +++ b/.claude/agents/reviewer.md @@ -0,0 +1,77 @@ +--- +name: reviewer +description: Code review agent for correctness, reuse, lint, and best practices. +--- + +# Reviewer Agent + +You are a code review agent for the PostKit CLI project. You review changes for **correctness, reuse, lint compliance, and best practices**. + +## Project Context + +- `CLAUDE.md` — Project structure, conventions, and common patterns +- `cli/docs/architecture.md` — System architecture and dependency direction +- `cli/docs/db.md` — Database module workflow +- `cli/docs/auth.md` — Auth module workflow + +## Review Checklist + +### 1. Code Reuse — Check Before Writing New Code +Search the codebase for existing utilities before accepting new implementations. These are the reusable functions already available: + +**Common layer** (`cli/src/common/`): +- `loadPostkitConfig()`, `checkInitialized()` — config loading +- `logger.info/success/warn/error/debug/step/heading/box/sql/diff/table` — output +- `runCommand()`, `runSpawnCommand()`, `commandExists()`, `runPipedCommands()` — shell execution + +**DB utils** (`cli/src/modules/db/utils/`): +- `getDbConfig()`, `getPostkitDbDir()`, `getSessionFilePath()`, etc. — path resolution +- `getRemotes()`, `resolveRemote()`, `maskRemoteUrl()`, `addRemote()`, `removeRemote()` — remote management +- `getSession()`, `createSession()`, `updateSession()`, `deleteSession()`, `hasActiveSession()` — session state +- `getCommittedState()`, `addCommittedMigration()`, `getPendingCommittedMigrations()` — committed state +- `loadSqlGroup()` — SQL file loading + +**DB services** (`cli/src/modules/db/services/`): +- `parseConnectionUrl()`, `testConnection()`, `cloneDatabase()`, `executeSQL()`, `dropDatabase()` — database ops +- `runPgschemaplan()`, `runPgschemaDiff()`, `sanitizePlanSQL()`, `deletePlanFile()` — pgschema +- `checkDbmateInstalled()`, `createMigrationFile()`, `runSessionMigrate()`, `runCommittedMigrate()` — dbmate +- `generateSchemaSQLAndFingerprint()`, `discoverSchemaSections()`, `getSchemaFiles()` — schema generation +- `loadGrants()`, `loadSeeds()`, `loadInfra()` — SQL loaders +- `runPgschemaDump()`, `normalizeDumpForPostkit()`, `generateBaselineDDL()`, `syncMigrationState()` — import + +**Auth services** (`cli/src/modules/auth/services/`): +- `getAdminToken()`, `exportRealm()`, `cleanRealmConfig()`, `importRealm()` — Keycloak operations + +**Rule**: If a utility already exists, the code MUST reuse it. Flag any duplicated logic. + +### 2. Correctness +- Empty inputs, null/undefined values handled +- Concurrent access to session files considered +- Missing or invalid configuration fields guarded +- Binary resolution failures across platforms handled + +### 3. Error Handling +- User-facing errors use `logger.error()` with actionable messages +- Unexpected errors throw with descriptive messages +- Debug output respects `options.verbose` +- Shell command failures are properly propagated + +### 4. TypeScript & Lint +- No `any` types — use proper TypeScript types +- No unused imports or variables +- Consistent naming conventions (camelCase functions, PascalCase types) +- `CommandOptions` interface used for global flags (`verbose`, `dryRun`, `json`) +- Module boundaries respected (no cross-module imports) +- `--dry-run` and `--json` flags handled where applicable + +### 5. Backward Compatibility +- Config format changes don't break existing `postkit.config.json` files +- CLI flags maintain their current behavior +- Session state format remains compatible across versions +- Auto-migration handles legacy config fields + +### 6. Security +- No SQL injection via unsanitized user input +- No credential exposure in log output (use `maskRemoteUrl()`) +- Shell commands don't construct from raw user input +- Config files with secrets are gitignored diff --git a/.claude/agents/senior-engineer.md b/.claude/agents/senior-engineer.md new file mode 100644 index 0000000..80285e0 --- /dev/null +++ b/.claude/agents/senior-engineer.md @@ -0,0 +1,66 @@ +--- +name: senior-engineer +description: Feature implementation agent following PostKit project patterns. +--- + +# Senior Engineer Agent + +You are a feature implementation specialist for the PostKit CLI project. + +## Project Context + +- `CLAUDE.md` — Project structure, module architecture, and conventions +- `cli/docs/architecture.md` — System architecture, dependency direction, module structure +- `cli/docs/db.md` — Database module reference implementation +- `cli/docs/auth.md` — Auth module reference implementation + +## Implementation Patterns + +### Module Registration (`index.ts`) +```typescript +import type {Command} from "commander"; + +export function registerModule(program: Command): void { + const module = program.command("").description("..."); + module.command("action").description("...").action(async (options) => { + const {someCommand} = await import("./commands/some"); + return someCommand(options); + }); +} +``` + +### Command Handlers (`commands/*.ts`) +```typescript +import type {CommandOptions} from "../../../common/types"; + +export async function someCommand(options: CommandOptions): Promise { + const config = loadPostkitConfig(); + // Business logic... +} +``` + +`CommandOptions` includes: `verbose`, `dryRun`, `json`, and module-specific flags. + +### Services (`services/*.ts`) +- Core business logic, separated from CLI concerns +- Use `shell()` from `common/shell.ts` for external commands +- Use `logger.*` from `common/logger.ts` for output +- Use `loadPostkitConfig()` from `common/config.ts` for configuration + +### Utilities (`utils/*.ts`) +- Module-specific helper functions +- Path resolution, config validation, state management + +### Types (`types/*.ts`) +- TypeScript interfaces for module-specific data structures +- Zod schemas for runtime validation + +## Key Rules + +- Register module in `cli/src/index.ts` after creating module directory +- Use `withInitCheck()` wrapper for commands requiring initialized project +- Handle `--force` flag for destructive operations +- Handle `--dry-run` flag for preview operations +- Handle `--json` flag for machine-readable output +- Use proper TypeScript types (no `any`) +- Follow existing error handling patterns diff --git a/.claude/agents/tester.md b/.claude/agents/tester.md new file mode 100644 index 0000000..d4b61b1 --- /dev/null +++ b/.claude/agents/tester.md @@ -0,0 +1,47 @@ +--- +name: tester +description: Test creation and regression verification agent. +--- + +# Tester Agent + +You are a test creation and regression verification specialist for the PostKit CLI project. + +## Project Context + +- `CLAUDE.md` — Project structure and conventions +- `cli/docs/architecture.md` — System architecture overview +- `cli/docs/db.md` — Database module workflow and commands +- `cli/docs/e2e-testing.md` — E2E testing infrastructure and patterns + +## Responsibilities + +1. **Write regression tests** that specifically reproduce the bug scenario +2. **Write unit tests** using the patterns in `cli/test/helpers/` (mock-config, mock-shell, mock-fs, mock-pg) +3. **Write E2E tests** if the bug affects a user-facing workflow (using `test/e2e/helpers/`) +4. **Run full test suite** to check for regressions: `cd cli && npm run test` +5. **Run relevant E2E tests**: `cd cli && npm run test:e2e:file -- ` +6. **Verify test isolation** - no test state leakage between tests + +## Unit Test Conventions + +- Mirror source file paths: `cli/test//.test.ts` +- Use `vi.mock()` at module level before imports +- Use helpers from `cli/test/helpers/`: mock-config, mock-shell, mock-fs, mock-pg +- Call `vi.clearAllMocks()` in `beforeEach` +- Group with nested `describe()` blocks + +## E2E Test Conventions + +- Use helpers from `cli/test/e2e/helpers/`: cli-runner, test-project, test-database, workflow +- Black-box testing via `runCli()` - test the compiled binary +- Proper Docker container lifecycle in `beforeAll`/`afterAll` +- Always use `--force` flag on state-modifying commands + +## Verification Checklist + +- [ ] Bug-specific test written and passes +- [ ] Test fails without the fix (confirms it catches the bug) +- [ ] All existing unit tests still pass +- [ ] Relevant E2E tests still pass +- [ ] No test state leakage diff --git a/.claude/agents/unit-test-agent.md b/.claude/agents/unit-test-agent.md new file mode 100644 index 0000000..cca82f1 --- /dev/null +++ b/.claude/agents/unit-test-agent.md @@ -0,0 +1,80 @@ +--- +name: unit-test-agent +description: Unit test implementation specialist using Vitest with mocking patterns. +--- + +# Unit Test Agent + +You are a unit test implementation specialist for the PostKit CLI project. + +## Project Context + +Read `CLAUDE.md` at the project root for project structure and conventions. + +## Test Framework + +- **Vitest** with globals enabled (`"types": ["vitest/globals"]`) +- **Config**: `cli/vitest.config.ts` (Node environment, excludes `test/e2e/`) +- **Test location**: Mirror source structure in `cli/test/` + - `cli/src/modules/db/services/pgschema.ts` → `cli/test/modules/db/services/pgschema.test.ts` + - `cli/src/common/config.ts` → `cli/test/common/config.test.ts` + +## Import Pattern + +```typescript +import {describe, it, expect, vi, beforeEach} from "vitest"; +``` + +## Test Helpers + +Use existing mock helpers from `cli/test/helpers/`: + +- **`mock-config.ts`**: `createMockConfig()`, `mockLoadPostkitConfig()` +- **`mock-shell.ts`**: `createMockShell()`, `mockShellSuccess()`, `mockShellFailure()` +- **`mock-fs.ts`**: File system mocking utilities +- **`mock-pg.ts`**: PostgreSQL client mocking + +## Mocking Patterns + +```typescript +// Mock at module level BEFORE importing the module under test +vi.mock("../../src/common/shell", () => ({ + runCommand: vi.fn(), +})); + +vi.mock("../../src/common/config", () => ({ + loadPostkitConfig: vi.fn(), +})); + +// Import after mocks +import {runCommand} from "../../src/common/shell"; +import {loadPostkitConfig} from "../../src/common/config"; + +describe("myService", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should handle success", async () => { + vi.mocked(runCommand).mockResolvedValue({stdout: "ok", stderr: "", exitCode: 0}); + // ... test logic + }); +}); +``` + +## Test Structure + +- Call `vi.clearAllMocks()` in `beforeEach` +- Use nested `describe()` blocks for logical grouping by function/method +- Test both success and error paths +- Verify function arguments passed to mocked dependencies +- Test edge cases: empty inputs, null/undefined, missing config fields + +## Test Scripts + +```bash +cd cli +npm run test # Run all unit tests +npm run test:watch # Watch mode +npm run test:coverage # With coverage reporting +``` diff --git a/.claude/agents/validator.md b/.claude/agents/validator.md new file mode 100644 index 0000000..1b9c0a6 --- /dev/null +++ b/.claude/agents/validator.md @@ -0,0 +1,61 @@ +--- +name: validator +description: Build, test, and quality validation agent. +--- + +# Validator Agent + +You are a build, test, and quality validation specialist for the PostKit CLI project. + +## Project Context + +- `CLAUDE.md` — Build and test commands +- `cli/docs/architecture.md` — System architecture overview +- `cli/docs/e2e-testing.md` — E2E test infrastructure + +## Validation Steps + +### 1. Build Check +```bash +cd cli && npm run build +``` +Verify no TypeScript errors, clean compilation. + +### 2. Unit Tests +```bash +cd cli && npm run test +``` +All unit tests must pass. Check for any new failures. + +### 3. E2E Tests (if applicable) +```bash +cd cli && npm run test:e2e:file -- +``` +Run relevant E2E tests for the changed area. + +### 4. TypeScript Check +```bash +cd cli && npx tsc --noEmit +``` +Verify no type errors in source files. + +### 5. File Structure Check +- New files are in correct directories +- Imports resolve correctly +- No circular dependencies introduced + +### 6. Coverage Check +```bash +cd cli && npm run test:coverage +``` +Confirm test coverage is maintained or improved. + +## Validation Checklist + +- [ ] Build passes (`npm run build`) +- [ ] Unit tests pass (`npm run test`) +- [ ] No TypeScript errors (`npx tsc --noEmit`) +- [ ] Files in correct locations +- [ ] Imports resolve correctly +- [ ] No circular dependencies +- [ ] Test coverage maintained or improved diff --git a/.claude/skills/architecture.md b/.claude/skills/architecture.md new file mode 100644 index 0000000..d0c60f2 --- /dev/null +++ b/.claude/skills/architecture.md @@ -0,0 +1,66 @@ +--- +name: architecture +description: Review and propose system architecture decisions, generate ADRs. +--- + +# Architecture Skill + +Review and propose architecture decisions for the PostKit CLI project. + +## Project Context + +- `CLAUDE.md` — Project structure and module architecture +- `cli/docs/architecture.md` — Current system architecture documentation +- `cli/docs/db.md` — Database module architecture +- `cli/docs/auth.md` — Auth module architecture + +## Workflow + +### Step 1: Scope the Review +Determine what needs architectural analysis: +- New module proposal +- Cross-module dependency concern +- Performance or scalability question +- Technology choice (e.g., binary bundling, config format) +- Breaking change impact + +### Step 2: Analyze Current Architecture +1. Read relevant source files in `cli/src/` +2. Check module boundaries in `cli/src/modules/*/index.ts` +3. Review dependency direction (commands → services → utils) +4. Identify coupling points and potential issues + +### Step 3: Generate ADR +Create an Architecture Decision Record in `cli/docs/adr/`: + +```markdown +# NNNN: Title + +**Status**: Proposed | Accepted | Deprecated | Superseded +**Date**: YYYY-MM-DD + +## Context +[What is the issue motivating this decision?] + +## Decision +[What is the change being proposed?] + +## Consequences +[What becomes easier or more difficult?] + +## Alternatives Considered +[What other options were evaluated?] +``` + +### Step 4: Review Checklist +- [ ] Module boundaries respected +- [ ] Dependency direction correct (commands → services) +- [ ] Error propagation consistent +- [ ] Configuration validation approach +- [ ] Binary resolution strategy +- [ ] Plugin extensibility maintained + +## ADR Naming +- Store in `cli/docs/adr/` +- Format: `NNNN-kebab-case-title.md` +- Number sequentially from existing ADRs diff --git a/.claude/skills/bugfix.md b/.claude/skills/bugfix.md new file mode 100644 index 0000000..4a787b7 --- /dev/null +++ b/.claude/skills/bugfix.md @@ -0,0 +1,74 @@ +--- +name: bugfix +description: Diagnose and fix bugs with a 4-stage workflow: gather info, diagnose, fix, test, validate & review. +--- + +# Bugfix Skill + +Diagnose and fix bugs in the PostKit CLI project using a 5-stage pipeline. + +## Project Context + +- `CLAUDE.md` — Project structure and conventions +- `cli/docs/architecture.md` — System architecture and dependency direction +- `cli/docs/db.md` — Database module workflow and commands +- `cli/docs/auth.md` — Auth module workflow and commands + +## Pre-Flight: Gather Missing Info + +Before starting, check if the user provided enough context. If any of these are missing, **ask the user** before proceeding: + +- What is the error message or unexpected behavior? +- Which command or workflow triggered the bug? (e.g., `postkit db import`, `postkit db deploy`) +- What are the reproduction steps? +- Any logs, stack traces, or screenshots? + +**Do not start work until you have enough information to reproduce the bug.** + +## Workflow + +### Stage 1: Diagnose (bugfixer agent) +1. Understand the error message and reproduction steps +2. Read relevant source files to trace the error path +3. Check common PostKit failure areas: + - Session state in `.postkit/db/session.json` + - Remote URL resolution and config validation + - Schema file parsing and pgschema binary execution + - Shell command execution and error handling + - Custom schema support (non-public PostgreSQL schemas) +4. Identify the root cause + +### Stage 2: Fix (bugfixer agent) +1. Implement the minimal fix +2. Follow existing code patterns: + - `CommandOptions` interface for command handlers + - `loadPostkitConfig()` for configuration + - `logger.*` for output, `shell()` for external commands +3. No refactoring or improvements beyond the fix +4. Preserve existing function signatures + +### Stage 3: Test (tester agent) +1. Write unit tests that reproduce the bug scenario +2. Write E2E test if the bug affects user-facing workflow +3. Verify the test fails without the fix (confirms it catches the bug) +4. Run full unit test suite: `cd cli && npm run test` + +### Stage 4: Validate & Review (validator + reviewer agents) +1. Build: `cd cli && npm run build` +2. Type check: `cd cli && npx tsc --noEmit` +3. All unit tests pass +4. Relevant E2E tests pass +5. **Code review** (reviewer agent): + - Check for code reuse — reuse existing utilities from `common/`, `modules/*/utils/`, `modules/*/services/` + - Check TypeScript quality (no `any`, proper types, no unused imports) + - Check error handling patterns (logger.error for user-facing, throw for unexpected) + - Check backward compatibility + - Check security (no SQL injection, no credential exposure) + +## Output +- Bug diagnosis summary +- Fix implementation +- New regression tests +- Validation results + +**Note:** Use `/create-pr` separately when ready to create a pull request. diff --git a/.claude/skills/create-feature.md b/.claude/skills/create-feature.md new file mode 100644 index 0000000..3e6cea5 --- /dev/null +++ b/.claude/skills/create-feature.md @@ -0,0 +1,102 @@ +--- +name: create-feature +description: Implement new features end-to-end: gather info, plan, implement, test, validate, review. +--- + +# Create Feature Skill + +Implement new features for the PostKit CLI project using a multi-stage pipeline. + +## Project Context + +- `CLAUDE.md` — Project structure and "Adding a New Module" section +- `cli/docs/architecture.md` — System architecture, module structure, dependency direction +- `cli/docs/db.md` — Database module as reference implementation +- `cli/docs/auth.md` — Auth module as reference implementation + +## Pre-Flight: Gather Missing Info + +Before starting, check if the user provided enough context. If any of these are missing, **ask the user** before proceeding: + +- What is the feature? (description or user story) +- Which module does it belong to? (`db`, `auth`, or new module?) +- What commands should be added or modified? +- Any specific CLI flags or options expected? +- Any config changes needed? +- Target branch? (default: `development`) + +**Do not start work until you have enough information to plan the feature.** + +## Workflow + +### Stage 1: Plan (feature-planner agent) +1. Understand the feature requirements +2. Determine if it fits an existing module or needs a new one +3. Map out files to create/modify: + - `cli/src/modules//index.ts` - Module registration + - `cli/src/modules//commands/` - Command handlers + - `cli/src/modules//services/` - Business logic + - `cli/src/modules//utils/` - Utilities + - `cli/src/modules//types/` - TypeScript types + - `cli/src/index.ts` - Module registration call +4. Identify existing patterns and utilities to reuse +5. Plan test coverage (unit + E2E) +6. Produce an implementation checklist + +### Stage 2: Implement (senior-engineer agent) +1. Create module directory structure +2. Implement types first, then services, then commands, then registration +3. Follow project patterns: + - `registerModule(program: Command)` for registration + - `CommandOptions` for command handler signatures + - `loadPostkitConfig()` for config, `logger.*` for output, `shell()` for commands +4. Handle global flags: `verbose`, `dryRun`, `json`, `force` +5. Register in `cli/src/index.ts` + +### Stage 2b: Write Tests (unit-test-agent + e2e-test-agent) +**This step is mandatory — every feature must include tests.** + +Unit tests: +1. Mirror source file paths: `cli/test//.test.ts` +2. Use `vi.mock()` for all external dependencies +3. Use helpers from `cli/test/helpers/`: mock-config, mock-shell, mock-fs, mock-pg +4. Test success paths, error paths, and edge cases +5. Run: `cd cli && npm run test` + +E2E tests (if feature adds/modifies user-facing commands): +1. Write workflow test in `cli/test/e2e/workflows/` +2. Use helpers from `cli/test/e2e/helpers/`: cli-runner, test-project, test-database, workflow +3. Test full command lifecycle with real PostgreSQL (testcontainers) +4. Run: `cd cli && npm run test:e2e:file -- ` + +### Stage 3: Validate (validator agent) +1. Build: `cd cli && npm run build` +2. Type check: `cd cli && npx tsc --noEmit` +3. Run existing unit tests: `cd cli && npm run test` +4. Run relevant E2E tests +5. Verify file structure and imports + +### Stage 4: Review (reviewer + architect agents) + +**Code review** (reviewer agent): +- Check for code reuse — reuse existing utilities, don't duplicate logic +- Check TypeScript quality (no `any`, proper types, no unused imports) +- Check error handling patterns (logger.error for user-facing, throw for unexpected) +- Check backward compatibility +- Check security (no SQL injection, no credential exposure) + +**Architecture review** (architect agent): +1. Does the feature fit the module system? +2. API design consistency with existing commands +3. Error handling completeness +4. Edge case coverage +5. Documentation needs identified + +## Output +- Implementation plan +- Source code for the feature +- Unit and E2E tests +- Validation results +- Architecture review notes + +**Note:** Use `/create-pr` separately when ready to create a pull request. diff --git a/.claude/skills/create-pr.md b/.claude/skills/create-pr.md new file mode 100644 index 0000000..0aadcfe --- /dev/null +++ b/.claude/skills/create-pr.md @@ -0,0 +1,53 @@ +--- +name: create-pr +description: Create a standardized GitHub pull request with build verification and change analysis. +--- + +# Create PR Skill + +Create a standardized GitHub PR for the PostKit project. + +## Project Context + +Read `CLAUDE.md` at the project root for project conventions. +Use the PR template at `.github/pull_request_template.md`. + +## Workflow + +### Step 1: Pre-PR Verification +Run build and unit tests to ensure the branch is in a good state: +```bash +cd cli && npm run build +cd cli && npm run test +``` +If either fails, report the failure and stop. + +### Step 2: Analyze Changes +Gather branch information: +```bash +git log origin/main...HEAD --oneline +git diff origin/main...HEAD --stat +git diff origin/main...HEAD +``` +Categorize changes into: features, fixes, refactors, tests, docs, chore. + +### Step 3: Generate PR +Using the `pr-agent` sub-agent approach: +- Generate a title under 70 characters with conventional commit prefix +- Populate the PR template sections +- Create the PR targeting `development` (or `main` for hotfixes): +```bash +gh pr create --base development --title "" --body "<body>" +``` + +### Step 4: Post-Creation +- Verify the PR was created successfully +- Report the PR URL to the user + +## PR Title Format +- `feat: <description>` for new features +- `fix: <description>` for bug fixes +- `refactor: <description>` for code refactoring +- `test: <description>` for test changes +- `docs: <description>` for documentation changes +- `chore: <description>` for build/tooling changes diff --git a/.claude/skills/update-docs.md b/.claude/skills/update-docs.md new file mode 100644 index 0000000..052bca4 --- /dev/null +++ b/.claude/skills/update-docs.md @@ -0,0 +1,58 @@ +--- +name: update-docs +description: Update project documentation when code changes occur. +--- + +# Update Docs Skill + +Update project documentation to reflect code changes in the PostKit CLI project. + +## Project Context + +Read `CLAUDE.md` at the project root for project structure. +Read existing docs for style reference. + +## Documentation Files + +### Developer Docs (`cli/docs/`) +- `cli/docs/architecture.md` - System architecture and design decisions +- `cli/docs/db.md` - Database module documentation +- `cli/docs/auth.md` - Auth module documentation +- `cli/docs/e2e-testing.md` - E2E testing guide + +### User Docs (Docusaurus) +- `docs/docs/getting-started/` - Installation, configuration +- `docs/docs/modules/db/` - DB module user docs +- `docs/docs/modules/auth/` - Auth module user docs +- `docs/docs/reference/` - CLI reference + +## Workflow + +### Step 1: Detect Changes +Identify what changed in the codebase: +- New or modified commands +- Changed command options +- Config structure changes +- New modules or services +- Workflow changes + +### Step 2: Find Affected Docs +Map code changes to documentation: +- Command changes → `cli/docs/db.md` or `cli/docs/auth.md` +- Config changes → CLAUDE.md and module docs +- New modules → CLAUDE.md and new doc page +- Test changes → `cli/docs/e2e-testing.md` +- User-facing changes → Docusaurus `docs/docs/` + +### Step 3: Update Documentation +- Follow the existing style in each doc file +- Update command reference tables +- Add new code examples +- Verify cross-references between docs +- Keep CLAUDE.md concise + +### Step 4: Verify +- Code examples are accurate and runnable +- Command names and flags match source code +- Config examples match current schema +- No stale or outdated information remains diff --git a/.claude/skills/write-test-e2e.md b/.claude/skills/write-test-e2e.md new file mode 100644 index 0000000..8bb1be0 --- /dev/null +++ b/.claude/skills/write-test-e2e.md @@ -0,0 +1,49 @@ +--- +name: write-test-e2e +description: Write end-to-end tests for PostKit CLI using testcontainers and black-box testing. +--- + +# Write E2E Tests Skill + +Write E2E tests for the PostKit CLI project. + +## Project Context + +Read `CLAUDE.md` at the project root for project structure. +Read `cli/docs/e2e-testing.md` for the complete E2E testing guide. + +## Workflow + +### Step 1: Understand What to Test +- Read the source code for the feature/command being tested +- Identify the user-facing workflow to verify +- Check for existing E2E tests that cover similar scenarios + +### Step 2: Write the Test +Follow the E2E test patterns: + +```typescript +import {describe, it, expect, beforeAll, afterAll} from "vitest"; +import {runCli} from "./helpers/cli-runner"; +import {createTestProject, cleanupTestProject, type TestProject} from "./helpers/test-project"; +import {startPostgres, stopPostgres, type TestDatabase} from "./helpers/test-database"; +import {startSession, runPlan, runApply, runCommit} from "./helpers/workflow"; +``` + +**Conventions:** +- File location: `cli/test/e2e/workflows/` for workflows, `cli/test/e2e/error-handling/` for errors +- Use `--force` flag on state-modifying commands +- Proper `beforeAll`/`afterAll` cleanup for Docker containers and temp dirs +- Use `installFixtureSchema()` for realistic schemas +- Verify with `queryDatabase()` for direct SQL checks + +### Step 3: Run and Verify +```bash +cd cli && npm run test:e2e:file -- <test-file-path> +``` +If the test fails, fix it and re-run. + +## Reference Files +- `cli/docs/e2e-testing.md` - Complete testing guide +- `cli/test/e2e/helpers/` - All helper modules +- Existing tests in `cli/test/e2e/workflows/` - Pattern examples diff --git a/.claude/skills/write-test-unit.md b/.claude/skills/write-test-unit.md new file mode 100644 index 0000000..4f65ebf --- /dev/null +++ b/.claude/skills/write-test-unit.md @@ -0,0 +1,56 @@ +--- +name: write-test-unit +description: Write unit tests for PostKit CLI using Vitest with proper mocking patterns. +--- + +# Write Unit Tests Skill + +Write unit tests for the PostKit CLI project. + +## Project Context + +Read `CLAUDE.md` at the project root for project structure. + +## Workflow + +### Step 1: Identify What to Test +- Read the source file that needs tests +- Identify exported functions and their dependencies +- Check for existing test coverage in `cli/test/` + +### Step 2: Write the Test +Follow the unit test patterns: + +```typescript +import {describe, it, expect, vi, beforeEach} from "vitest"; + +// Mock dependencies BEFORE importing the module under test +vi.mock("../../src/common/shell", () => ({ + runCommand: vi.fn(), +})); + +vi.mock("../../src/common/config", () => ({ + loadPostkitConfig: vi.fn(), +})); + +import {runCommand} from "../../src/common/shell"; +import {loadPostkitConfig} from "../../src/common/config"; +``` + +**Conventions:** +- Mirror source file path: `cli/test/<path>/<file>.test.ts` +- Use mock helpers from `cli/test/helpers/`: mock-config, mock-shell, mock-fs, mock-pg +- Call `vi.clearAllMocks()` in `beforeEach` +- Group with nested `describe()` blocks by function +- Test both success and error paths + +### Step 3: Run and Verify +```bash +cd cli && npm run test +``` +If tests fail, fix and re-run. + +## Reference Files +- `cli/test/helpers/` - Mock utility helpers +- `cli/vitest.config.ts` - Test configuration +- Existing tests in `cli/test/common/` - Pattern examples diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..621d163 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,34 @@ +## Summary +<!-- 1-3 bullet points describing what this PR does --> + + +## Changes +<!-- List of specific changes made --> + +- + + +## Type of Change +<!-- Check one --> + +- [ ] feat: New feature +- [ ] fix: Bug fix +- [ ] refactor: Code refactoring (no functional change) +- [ ] test: Adding or updating tests +- [ ] docs: Documentation changes +- [ ] chore: Build, tooling, or CI changes + + +## Test Plan +<!-- How to verify the changes work correctly --> + +- [ ] Unit tests pass (`npm run test`) +- [ ] E2E tests pass (`npm run test:e2e`) (if applicable) +- [ ] Build succeeds (`npm run build`) +- [ ] Manually tested: <!-- describe what was tested --> + + +## Breaking Changes +<!-- If this PR introduces breaking changes, describe them here --> + +- [ ] No breaking changes diff --git a/.gitignore b/.gitignore index 9bf0228..58317cd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ node_modules/ realm-config/ test-proj/ -.claude/ +.claude/settings.local.json issue.md analyze.md import-feature.md @@ -16,3 +16,4 @@ dump_schema.sh test-import/ cli_testing_strategy.md cli/analysis_results.md +temp/ diff --git a/CLAUDE.md b/CLAUDE.md index da9c955..07a1e2d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -240,3 +240,44 @@ logger.debug(`Remote URL: ${maskRemoteUrl(url)}`, options.verbose); - The `vendor/` directory contains platform-specific binaries that are bundled with the CLI - no separate installation required. - The `.gitignore` should include `.postkit/` to ignore all runtime files. - All migration-related files are in `.postkit/db/` - the only user-maintained DB files should be in `db/schema/`. + +## Claude Code Skills & Agents + +Skills are invoked via `/<skill-name>` in Claude Code. Agents are sub-processes spawned by skills. + +### Project Documentation (`cli/docs/`) + +| Doc | Content | +|-----|---------| +| `cli/docs/architecture.md` | System architecture, module system, dependency direction | +| `cli/docs/db.md` | Database module workflow and commands | +| `cli/docs/auth.md` | Auth module workflow and commands | +| `cli/docs/e2e-testing.md` | E2E testing guide and infrastructure | + +### Skills Registry + +| Skill | Invocation | Purpose | Sub-Agents | +|-------|-----------|---------|------------| +| create-pr | `/create-pr` | Create a standardized GitHub PR | pr-agent | +| write-test-e2e | `/write-test-e2e` | Write E2E tests using testcontainers | e2e-test-agent | +| write-test-unit | `/write-test-unit` | Write unit tests with Vitest mocks | unit-test-agent | +| bugfix | `/bugfix` | Diagnose and fix bugs (asks for info, 4 stages) | bugfixer, tester, reviewer, validator | +| create-feature | `/create-feature` | Implement features (asks for info, 4 stages + tests) | feature-planner, senior-engineer, reviewer, validator, architect | +| architecture | `/architecture` | Review architecture, generate ADRs | architect | +| update-docs | `/update-docs` | Update documentation for code changes | docs-agent | + +### Agents Registry + +| Agent | File | Specialty | Used By | +|-------|------|-----------|---------| +| pr-agent | `.claude/agents/pr-agent.md` | PR body generation, change analysis | create-pr | +| e2e-test-agent | `.claude/agents/e2e-test-agent.md` | E2E test implementation (testcontainers) | write-test-e2e | +| unit-test-agent | `.claude/agents/unit-test-agent.md` | Unit test implementation (Vitest mocks) | write-test-unit | +| bugfixer | `.claude/agents/bugfixer.md` | Bug diagnosis and minimal fix | bugfix | +| tester | `.claude/agents/tester.md` | Regression test creation | bugfix | +| reviewer | `.claude/agents/reviewer.md` | Code review (reuse, lint, best practices) | bugfix, create-feature | +| validator | `.claude/agents/validator.md` | Build/test/quality validation | bugfix, create-feature | +| feature-planner | `.claude/agents/feature-planner.md` | Feature design and task breakdown | create-feature | +| senior-engineer | `.claude/agents/senior-engineer.md` | Feature implementation | create-feature | +| architect | `.claude/agents/architect.md` | Architecture analysis, ADR authoring | architecture | +| docs-agent | `.claude/agents/docs-agent.md` | Documentation writing and maintenance | update-docs | diff --git a/cli/.claude/settings.local.json b/cli/.claude/settings.local.json new file mode 100644 index 0000000..f56755b --- /dev/null +++ b/cli/.claude/settings.local.json @@ -0,0 +1,17 @@ +{ + "permissions": { + "allow": [ + "Bash(npm install:*)", + "Bash(npx vitest:*)", + "Bash(npm run:*)", + "Bash(node:*)", + "Read(//Users/supunnilakshana/development/appri/postkit/Postkit/test-proj/schema/**)", + "Read(//Users/supunnilakshana/development/appri/postkit/Postkit/**)", + "Bash(for dir:*)", + "Bash(do echo:*)", + "Read(//Users/supunnilakshana/development/appri/postkit/Postkit/cli/**)", + "Bash(done)", + "Bash(docker run:*)" + ] + } +} diff --git a/cli/docs/architecture.md b/cli/docs/architecture.md new file mode 100644 index 0000000..6b73170 --- /dev/null +++ b/cli/docs/architecture.md @@ -0,0 +1,197 @@ +# PostKit CLI — Architecture + +System architecture and design decisions for the PostKit modular CLI toolkit. + +--- + +## Overview + +PostKit is a modular CLI toolkit built with **TypeScript** and **Node.js** that provides developer tools for database migrations and Keycloak auth management. It uses a **plugin module architecture** where each feature is self-contained. + +``` +┌─────────────────────────────────────────────────────────┐ +│ postkit (CLI) │ +│ cli/src/index.ts │ +├──────────┬──────────────────────────┬───────────────────┤ +│ init │ db module │ auth module │ +│ command │ (migrations, import) │ (Keycloak sync) │ +├──────────┴──────────────────────────┴───────────────────┤ +│ common layer │ +│ config · logger · shell · types · init-check │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## Module System + +Each module is registered via `register<Name>Module(program: Command)` in `cli/src/index.ts`. Modules are self-contained with a consistent internal structure: + +``` +modules/<name>/ +├── index.ts # Module registration (commands → commander) +├── commands/ # Command handlers (thin layer, call services) +├── services/ # Core business logic +├── utils/ # Module-specific utilities +└── types/ # TypeScript interfaces +``` + +**Dependency direction** (strict, no cycles): +``` +commands → services → utils + ↓ ↓ + common (config, logger, shell, types) +``` + +### Adding a New Module + +1. Create directory under `cli/src/modules/<name>/` +2. Export `register<Name>Module(program)` from `index.ts` +3. Import and call in `cli/src/index.ts` +4. Commands use `withInitCheck()` wrapper for initialized-project validation + +--- + +## Modules + +### Database Module (`postkit db`) + +**Registration**: `registerDbModule()` in `cli/src/modules/db/index.ts` +**Docs**: `cli/docs/db.md` + +Session-based migration workflow: `start → plan → apply → commit → deploy` + +``` +┌──────────┐ ┌──────┐ ┌───────┐ ┌────────┐ ┌────────┐ +│ start │───▶│ plan │───▶│ apply │───▶│ commit │───▶│ deploy │ +│ (clone) │ │(diff)│ │(local)│ │(stage) │ │(remote)│ +└──────────┘ └──────┘ └───────┘ └────────┘ └────────┘ +``` + +**Key components:** +- **pgschema** — Bundled binary for schema diffing (`vendor/pgschema/`) +- **dbmate** — npm-installed migration runner (`--migrations-table postkit.schema_migrations`) +- **Session state** — Tracked in `.postkit/db/session.json` +- **Named remotes** — Multiple remote DBs via `db.remotes` in config +- **Schema directory** — User-maintained SQL files (`db/schema/`) with sections: `infra/`, `extensions/`, `types/`, `enums/`, `tables/`, `views/`, `functions/`, `triggers/`, `grants/`, `seeds/` + +**Import sub-workflow** (`postkit db import`): +1. pg_dump source → pgschema `dump --multi-file` → normalize → generate baseline via pgschema plan → apply locally → sync migration state + +### Auth Module (`postkit auth`) + +**Registration**: `registerAuthModule()` in `cli/src/modules/auth/index.ts` +**Docs**: `cli/docs/auth.md` + +Keycloak realm configuration management: `export → clean → import` + +``` +┌──────────┐ ┌───────┐ ┌────────┐ +│ export │───▶│ clean │───▶│ import │ +│ (source) │ │(strip)│ │(target)│ +└──────────┘ └───────┘ └────────┘ +``` + +--- + +## Common Layer + +Shared utilities used by all modules, located in `cli/src/common/`: + +| File | Purpose | +|------|---------| +| `config.ts` | Config loader (`.env`, `postkit.config.json`), path resolution | +| `logger.ts` | Chalk-based console output (respects `--verbose`) | +| `shell.ts` | Shell command execution wrapper | +| `types.ts` | Shared TypeScript types (`CommandOptions`) | +| `init-check.ts` | Project initialization validation | + +--- + +## Configuration + +Loaded from `postkit.config.json` via `loadPostkitConfig()`: + +```json +{ + "db": { + "localDbUrl": "postgres://...", + "schemaPath": "db/schema", + "schema": "public", + "remotes": { + "dev": { "url": "postgres://...", "default": true }, + "staging": { "url": "postgres://..." } + } + }, + "auth": { + "sourceKeycloak": { "baseUrl": "...", "realm": "..." }, + "targetKeycloak": { "baseUrl": "...", "realm": "..." } + } +} +``` + +--- + +## Binary Resolution + +PostKit bundles platform-specific binaries — no separate installation required: + +| Binary | Location | Purpose | +|--------|----------|---------| +| pgschema | `vendor/pgschema/pgschema-{platform}-{arch}` | Schema diffing and multi-file dump | +| dbmate | npm package `dbmate` | SQL migration execution | + +Supported platforms: `darwin-arm64`, `darwin-amd64`, `linux-arm64`, `linux-amd64`, `windows-arm64`, `windows-amd64` + +--- + +## Build System + +- **tsup** — Bundles TypeScript to ESM (Node 18+ target) +- **tsx** — Direct TypeScript execution for development +- Output: `cli/dist/index.js` with shebang for CLI execution + +```bash +npm run build # Production build +npm run dev -- <module> <command> # Development mode +``` + +--- + +## Testing + +- **Unit tests**: Vitest with `vi.mock()` for dependency isolation +- **E2E tests**: Black-box testing of compiled binary against real PostgreSQL (testcontainers) +- See `cli/docs/e2e-testing.md` for full testing guide + +``` +cli/test/ +├── common/ # Unit tests for common utilities +├── modules/ # Unit tests for module services/utils +│ ├── db/ +│ └── auth/ +├── e2e/ # End-to-end tests +│ ├── smoke/ # Quick tests (no Docker) +│ ├── workflows/ # Full workflow tests +│ └── error-handling/ # Error scenario tests +└── helpers/ # Shared test utilities (mock-config, mock-shell, etc.) +``` + +--- + +## Runtime Directory Structure + +All PostKit runtime files in `.postkit/` (gitignored): + +``` +.postkit/ +├── db/ +│ ├── session.json # Current session state +│ ├── committed.json # Committed migration tracking +│ ├── plan.sql # Generated migration plan +│ ├── schema.sql # Generated schema from files +│ ├── session/ # Session migrations (temporary) +│ └── migrations/ # Committed migrations (for deploy) +└── auth/ + └── raw/ # Exported realm config (pre-clean) +``` From 1a797cb8400bea61cb1256293bf6776c29a6b2e6 Mon Sep 17 00:00:00 2001 From: supunappri99 <supun@appritechnologies.com> Date: Tue, 28 Apr 2026 23:59:01 +0530 Subject: [PATCH 2/9] refactor: simplify PR creation by removing pr-agent and directing output to local file --- .claude/agents/pr-agent.md | 42 ------------------------------------ .claude/skills/create-pr.md | 43 ++++++++++++++++--------------------- CLAUDE.md | 3 +-- 3 files changed, 20 insertions(+), 68 deletions(-) delete mode 100644 .claude/agents/pr-agent.md diff --git a/.claude/agents/pr-agent.md b/.claude/agents/pr-agent.md deleted file mode 100644 index dc9b6e0..0000000 --- a/.claude/agents/pr-agent.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: pr-agent -description: PR body generation and change analysis specialist for PostKit. ---- - -# PR Agent - -You are a PR creation specialist for the PostKit CLI project. - -## Project Context - -Read `CLAUDE.md` at the project root for project structure, conventions, and module architecture. - -## Responsibilities - -1. **Change Analysis** - - Run `git log origin/main...HEAD --oneline` to gather all commits on the branch - - Run `git diff origin/main...HEAD --stat` to see file-level changes - - Run `git diff origin/main...HEAD` for detailed diff analysis - - Categorize changes: feat, fix, refactor, test, docs, chore - -2. **PR Body Generation** - - Use the template at `.github/pull_request_template.md` - - Fill in Summary with 1-3 bullet points - - List specific Changes from the diff - - Check the correct Type of Change box - - Generate a Test Plan checklist from the changed files - - Flag any Breaking Changes detected from the diff - -3. **PR Title** - - Under 70 characters - - Use conventional commit prefix: `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:` - - Focus on the "why" not the "what" - -4. **Pre-PR Checks** - - Verify build passes: `cd cli && npm run build` - - Verify unit tests pass: `cd cli && npm run test` - - Ensure no unintended files in `git status` - -5. **Branch Targeting** - - Default base branch is `development` - - Use `main` only for hotfixes diff --git a/.claude/skills/create-pr.md b/.claude/skills/create-pr.md index 0aadcfe..db8ba1e 100644 --- a/.claude/skills/create-pr.md +++ b/.claude/skills/create-pr.md @@ -1,11 +1,11 @@ --- name: create-pr -description: Create a standardized GitHub pull request with build verification and change analysis. +description: Generate a PR description and save to temp/pr-description.md. --- # Create PR Skill -Create a standardized GitHub PR for the PostKit project. +Generate a standardized PR description for the PostKit project and save it to `temp/pr-description.md`. ## Project Context @@ -14,15 +14,7 @@ Use the PR template at `.github/pull_request_template.md`. ## Workflow -### Step 1: Pre-PR Verification -Run build and unit tests to ensure the branch is in a good state: -```bash -cd cli && npm run build -cd cli && npm run test -``` -If either fails, report the failure and stop. - -### Step 2: Analyze Changes +### Step 1: Analyze Changes Gather branch information: ```bash git log origin/main...HEAD --oneline @@ -31,23 +23,26 @@ git diff origin/main...HEAD ``` Categorize changes into: features, fixes, refactors, tests, docs, chore. -### Step 3: Generate PR -Using the `pr-agent` sub-agent approach: -- Generate a title under 70 characters with conventional commit prefix -- Populate the PR template sections -- Create the PR targeting `development` (or `main` for hotfixes): -```bash -gh pr create --base development --title "<title>" --body "<body>" -``` +### Step 2: Generate PR Description +Using the PR template structure, generate: -### Step 4: Post-Creation -- Verify the PR was created successfully -- Report the PR URL to the user - -## PR Title Format +**Title** — under 70 characters with conventional commit prefix: - `feat: <description>` for new features - `fix: <description>` for bug fixes - `refactor: <description>` for code refactoring - `test: <description>` for test changes - `docs: <description>` for documentation changes - `chore: <description>` for build/tooling changes + +**Body** — using the template sections: +- Summary (1-3 bullet points) +- Changes (specific list) +- Type of Change (check one) +- Test Plan (checklist) + +### Step 3: Save to File +Create `temp/` directory if needed and save to `temp/pr-description.md`. +Use the exact format from `.github/pull_request_template.md` — read that file and follow its structure. + +### Step 4: Show to User +Display the generated PR description and ask for confirmation or edits before saving. diff --git a/CLAUDE.md b/CLAUDE.md index 07a1e2d..4f8c472 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -258,7 +258,7 @@ Skills are invoked via `/<skill-name>` in Claude Code. Agents are sub-processes | Skill | Invocation | Purpose | Sub-Agents | |-------|-----------|---------|------------| -| create-pr | `/create-pr` | Create a standardized GitHub PR | pr-agent | +| create-pr | `/create-pr` | Generate PR description to `temp/pr-description.md` | — | | write-test-e2e | `/write-test-e2e` | Write E2E tests using testcontainers | e2e-test-agent | | write-test-unit | `/write-test-unit` | Write unit tests with Vitest mocks | unit-test-agent | | bugfix | `/bugfix` | Diagnose and fix bugs (asks for info, 4 stages) | bugfixer, tester, reviewer, validator | @@ -270,7 +270,6 @@ Skills are invoked via `/<skill-name>` in Claude Code. Agents are sub-processes | Agent | File | Specialty | Used By | |-------|------|-----------|---------| -| pr-agent | `.claude/agents/pr-agent.md` | PR body generation, change analysis | create-pr | | e2e-test-agent | `.claude/agents/e2e-test-agent.md` | E2E test implementation (testcontainers) | write-test-e2e | | unit-test-agent | `.claude/agents/unit-test-agent.md` | Unit test implementation (Vitest mocks) | write-test-unit | | bugfixer | `.claude/agents/bugfixer.md` | Bug diagnosis and minimal fix | bugfix | From 536dab9486a851fc0149fca3777bc13f49f64099 Mon Sep 17 00:00:00 2001 From: supunappri99 <supun@appritechnologies.com> Date: Wed, 29 Apr 2026 18:51:15 +0530 Subject: [PATCH 3/9] refactor: migrate skill documentation files to structured subdirectories for better organization --- .claude/skills/{architecture.md => architecture/SKILL.md} | 0 .claude/skills/{bugfix.md => bugfix/SKILL.md} | 0 .claude/skills/{create-feature.md => create-feature/SKILL.md} | 0 .claude/skills/{create-pr.md => create-pr/SKILL.md} | 0 .claude/skills/{update-docs.md => update-docs/SKILL.md} | 0 .claude/skills/{write-test-e2e.md => write-test-e2e/SKILL.md} | 0 .claude/skills/{write-test-unit.md => write-test-unit/SKILL.md} | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename .claude/skills/{architecture.md => architecture/SKILL.md} (100%) rename .claude/skills/{bugfix.md => bugfix/SKILL.md} (100%) rename .claude/skills/{create-feature.md => create-feature/SKILL.md} (100%) rename .claude/skills/{create-pr.md => create-pr/SKILL.md} (100%) rename .claude/skills/{update-docs.md => update-docs/SKILL.md} (100%) rename .claude/skills/{write-test-e2e.md => write-test-e2e/SKILL.md} (100%) rename .claude/skills/{write-test-unit.md => write-test-unit/SKILL.md} (100%) diff --git a/.claude/skills/architecture.md b/.claude/skills/architecture/SKILL.md similarity index 100% rename from .claude/skills/architecture.md rename to .claude/skills/architecture/SKILL.md diff --git a/.claude/skills/bugfix.md b/.claude/skills/bugfix/SKILL.md similarity index 100% rename from .claude/skills/bugfix.md rename to .claude/skills/bugfix/SKILL.md diff --git a/.claude/skills/create-feature.md b/.claude/skills/create-feature/SKILL.md similarity index 100% rename from .claude/skills/create-feature.md rename to .claude/skills/create-feature/SKILL.md diff --git a/.claude/skills/create-pr.md b/.claude/skills/create-pr/SKILL.md similarity index 100% rename from .claude/skills/create-pr.md rename to .claude/skills/create-pr/SKILL.md diff --git a/.claude/skills/update-docs.md b/.claude/skills/update-docs/SKILL.md similarity index 100% rename from .claude/skills/update-docs.md rename to .claude/skills/update-docs/SKILL.md diff --git a/.claude/skills/write-test-e2e.md b/.claude/skills/write-test-e2e/SKILL.md similarity index 100% rename from .claude/skills/write-test-e2e.md rename to .claude/skills/write-test-e2e/SKILL.md diff --git a/.claude/skills/write-test-unit.md b/.claude/skills/write-test-unit/SKILL.md similarity index 100% rename from .claude/skills/write-test-unit.md rename to .claude/skills/write-test-unit/SKILL.md From 36a070916518d838d18ef2b05de33c544170c96d Mon Sep 17 00:00:00 2001 From: supunappri99 <supun@appritechnologies.com> Date: Wed, 29 Apr 2026 21:25:19 +0530 Subject: [PATCH 4/9] refactor: remove grants command and associated logic from CLI and documentation --- CLAUDE.md | 2 - cli/README.md | 1 - cli/docs/db.md | 51 +++----- cli/docs/e2e-testing.md | 10 +- cli/src/modules/db/commands/apply.ts | 92 +++----------- cli/src/modules/db/commands/deploy.ts | 27 +---- cli/src/modules/db/commands/grants.ts | 112 ------------------ cli/src/modules/db/commands/plan.ts | 1 - cli/src/modules/db/index.ts | 13 -- .../modules/db/services/grant-generator.ts | 96 --------------- cli/src/modules/db/types/index.ts | 1 - cli/src/modules/db/types/schema.ts | 5 - cli/src/modules/db/types/session.ts | 1 - cli/src/modules/db/utils/session.ts | 1 - .../e2e/workflows/infra-grants-seeds.test.ts | 32 +---- .../db/services/grant-generator.test.ts | 83 ------------- docs/docs/modules/db/commands/apply.md | 5 +- docs/docs/modules/db/commands/deploy.md | 4 +- docs/docs/modules/db/commands/grants.md | 49 -------- docs/docs/modules/db/commands/import.md | 4 +- docs/docs/modules/db/commands/infra.md | 1 - docs/docs/modules/db/commands/plan.md | 2 +- docs/docs/modules/db/commands/seed.md | 1 - .../modules/db/migrating-existing-database.md | 2 +- docs/docs/modules/db/overview.md | 9 +- docs/docs/modules/db/troubleshooting.md | 2 +- docs/docs/reference/project-structure.md | 4 +- docs/docs/reference/session-state.md | 2 - docs/sidebars.ts | 1 - 29 files changed, 60 insertions(+), 554 deletions(-) delete mode 100644 cli/src/modules/db/commands/grants.ts delete mode 100644 cli/src/modules/db/services/grant-generator.ts delete mode 100644 cli/test/modules/db/services/grant-generator.test.ts delete mode 100644 docs/docs/modules/db/commands/grants.md diff --git a/CLAUDE.md b/CLAUDE.md index da9c955..90690b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,7 +89,6 @@ The `db` module implements a **session-based migration workflow**: 5. **Schema directory structure** (`db/schema/`): - `infra/` - Pre-migration (roles, schemas, extensions) - excluded from pgschema - `extensions/`, `types/`, `enums/`, `tables/`, etc. - pgschema-managed - - `grants/` - Post-migration grants - excluded from pgschema - `seeds/` - Post-migration seed data - excluded from pgschema ### PostKit Directory Structure @@ -188,7 +187,6 @@ Remotes are managed via utilities in `modules/db/utils/remotes.ts`: | `postkit db remote remove <name>` | Remove a remote | | `postkit db remote use <name>` | Set default remote | | `postkit db infra [--apply]` | Manage infra SQL (roles, schemas, extensions) | -| `postkit db grants [--apply]` | Regenerate and apply grants | | `postkit db seed [--apply]` | Apply seed data | ## Common Patterns diff --git a/cli/README.md b/cli/README.md index 9600cbe..7a97042 100644 --- a/cli/README.md +++ b/cli/README.md @@ -75,7 +75,6 @@ Full documentation: [DB Module](https://docs.postkitstack.com/docs/modules/db/ov | `postkit db remote` | Manage remote databases | | `postkit db migration` | Create manual SQL migration | | `postkit db infra` | Manage infrastructure SQL | -| `postkit db grants` | Manage grant statements | | `postkit db seed` | Manage seed data | ### Auth Module (`auth`) diff --git a/cli/docs/db.md b/cli/docs/db.md index f55e3bc..d41bc18 100644 --- a/cli/docs/db.md +++ b/cli/docs/db.md @@ -34,25 +34,24 @@ A session-based database migration workflow for safe schema changes. Clone your │ │ 8. Create dbmate │ │ migration │ │ │ │ migration │ │ 9. Run dbmate │ │ │ │ 9. Run dbmate │ │ on local DB │ │ -│ │ on local DB │ │ 10. Apply grants │ │ -│ │ 10. Apply grants │ │ 11. Apply seeds │ │ -│ │ 11. Apply seeds │ └────────┬─────────┘ │ +│ │ on local DB │ │ 10. Apply seeds │ │ +│ │ 10. Apply seeds │ └────────┬─────────┘ │ │ └────────┬─────────┘ │ │ │ │ ▼ │ │ $ postkit db commit │ │ ┌──────────────────┐ ┌──────────────────┐ │ -│ │ 12. Copy staging │ │ 13. Copy session │ │ +│ │ 11. Copy staging │ │ 12. Copy session │ │ │ │ migrations │ │ migrations │ │ -│ │ 13. Update state │ │ to .postkit │ │ -│ │ 14. Track for │ │ /db/migrations│ │ -│ │ deploy │ │ 15. Update state │ │ +│ │ 12. Update state │ │ to .postkit │ │ +│ │ 13. Track for │ │ /db/migrations│ │ +│ │ deploy │ │ 14. Update state │ │ │ └──────────────────┘ └──────────────────┘ │ │ │ │ $ postkit db deploy │ │ ┌──────────────────┐ ┌──────────────────┐ │ -│ │ 15. Dry run on │ │ 16. Deploy to │ │ +│ │ 14. Dry run on │ │ 15. Deploy to │ │ │ │ local clone │───────────►│ remote DB │ │ -│ │ │ │ 17. Mark as │ │ +│ │ │ │ 16. Mark as │ │ │ │ │ │ deployed │ │ │ └──────────────────┘ └──────────────────┘ │ │ │ @@ -162,15 +161,15 @@ db/schema/ │ └── updated_at.sql ├── indexes/ │ └── performance.sql -├── grants/ # Post-migration grant statements +├── grants/ # Grant statements (managed by pgschema) │ └── app_user.sql └── seeds/ # Post-migration seed data └── default_roles.sql ``` -**Execution ordering:** infra (pre-migration) → pgschema-managed schema (extensions → types → enums → domains → sequences → tables → views → materialized_views → functions → triggers → indexes → constraints → policies) → grants (post-migration) → seeds (post-migration) +**Execution ordering:** infra (pre-migration) → pgschema-managed schema (extensions → types → enums → domains → sequences → tables → views → materialized_views → functions → triggers → indexes → constraints → policies → grants) → seeds (post-migration) -**Note:** `infra/`, `grants/`, and `seeds/` directories are excluded from pgschema processing and handled as separate steps. +**Note:** `infra/` and `seeds/` directories are excluded from pgschema processing and handled as separate steps. `grants/` is managed by pgschema. ### PostKit Directory Structure @@ -222,7 +221,7 @@ postkit db plan ``` **What it does:** -1. Combines all schema files from `db/schema/` into a single SQL file (excluding `infra/`, `grants/`, `seeds/`) +1. Combines all schema files from `db/schema/` into a single SQL file (excluding `infra/`, `seeds/`) 2. Runs `pgschema plan` to compare against local database 3. Saves a schema fingerprint (SHA-256 hash of source files) for validation during apply 4. Displays the migration plan and saves to `.postkit/db/plan.sql` @@ -245,10 +244,9 @@ postkit db apply -f # Skip confirmation 4. Applies infrastructure SQL from `db/schema/infra/` 5. Wraps the plan SQL and creates a dbmate migration file (staged in `.postkit/db/session/`) 6. Runs `dbmate migrate` on the local database -7. Applies grant statements from `db/schema/grants/` -8. Applies seed data from `db/schema/seeds/` +7. Applies seed data from `db/schema/seeds/` -**Resume support:** If grants or seeds fail, re-running `postkit db apply` resumes from where it left off (the migration is not re-applied). +**Resume support:** If seeds fail, re-running `postkit db apply` resumes from where it left off (the migration is not re-applied). --- @@ -288,10 +286,10 @@ postkit db deploy --dry-run # Verify only, don't touch target 3. If an active session exists, removes it (with confirmation unless `-f`) 4. Tests the target database connection 5. Clones the target database to local (using `LOCAL_DATABASE_URL`) -6. Runs a full dry-run on the local clone: infra, dbmate migrate, grants, seeds +6. Runs a full dry-run on the local clone: infra, dbmate migrate, seeds 7. If `--dry-run` is set, stops here and reports results without touching the target 8. Reports dry-run results and confirms deployment (unless `-f`) -9. Applies to target: infra, dbmate migrate, grants, seeds +9. Applies to target: infra, dbmate migrate, seeds 10. Drops the local clone database If the dry run fails, deployment is aborted and no changes are made to the target database. @@ -373,7 +371,7 @@ postkit db import --schema myschema --name initial_baseline - Roles queried directly from `pg_roles` → `infra/001_roles.sql` (idempotent `DO $$ IF NOT EXISTS $$`) - Schemas queried directly from `pg_namespace` → `infra/002_schemas.sql` (`CREATE SCHEMA IF NOT EXISTS`) - Extensions parsed from `schema.sql` → `extensions/imported_extensions.sql` - - Privileges consolidated into `grants/<schema>.sql` + - Privileges consolidated into `grants/<schema>.sql` (managed by pgschema) - All SQL files are prefixed with numeric ordering (`001_filename.sql`) based on `schema.sql` `\i` directives 6. Clears existing migrations directory and generates baseline DDL via `pgschema plan` against an empty temp database 7. For non-public schemas, prepends `SET search_path TO "<schema>"` to the baseline migration @@ -402,18 +400,6 @@ postkit db infra --apply --target=remote # Apply to remote --- -### `postkit db grants [--apply] [--target <local|remote>]` - -Regenerate and display grant statements from `db/schema/grants/`. - -```bash -postkit db grants # Show grants -postkit db grants --apply # Apply to local -postkit db grants --apply --target=remote # Apply to remote -``` - ---- - ### `postkit db seed [--apply] [--target <local|remote>]` Manage seed data from `db/schema/seeds/`. @@ -480,7 +466,6 @@ Session state is stored in `.postkit/db/session.json`: "description": null, "schemaFingerprint": null, "migrationApplied": false, - "grantsApplied": false, "seedsApplied": false } } @@ -524,7 +509,7 @@ Session migrations are staged in `.postkit/db/session/` and committed migrations | `No active migration session` | Run `postkit db start` first | | `Plan file is empty` | Schema files match current DB — make changes first | | `Schema files have changed since the plan was generated` | Schema files were modified after running `plan`. Run `postkit db plan` again | -| `Grants/seeds failed during apply` | Re-run `postkit db apply` — it resumes from where it left off | +| `Seeds failed during apply` | Re-run `postkit db apply` — it resumes from where it left off | | `Deploy failed during dry run` | No changes were made to the target. Fix the issue and retry. | | `Import: pgschema plan produced no output` | Schema directory may be empty after normalization. Check that the source DB has objects in the target schema. | | `Import: Could not insert migration tracking record` | Non-fatal. The local DB migration succeeded but the source DB tracking record failed. Manually insert the version into `postkit.schema_migrations` on the source DB. | diff --git a/cli/docs/e2e-testing.md b/cli/docs/e2e-testing.md index 1415439..6eaf0a0 100644 --- a/cli/docs/e2e-testing.md +++ b/cli/docs/e2e-testing.md @@ -10,7 +10,7 @@ Test Runner (vitest) └─ PostgreSQL container (Docker) ``` -All workflow tests use a **predefined fixture schema** (`test/e2e/fixtures/schema/`) that mirrors real project structure — tables with UUID PKs, CHECK constraints, indexes, RLS policies, grants per role, idempotent seeds, triggers, functions, and views. +All workflow tests use a **predefined fixture schema** (`test/e2e/fixtures/schema/`) that mirrors real project structure — tables with UUID PKs, CHECK constraints, indexes, RLS policies, grant management via pgschema, idempotent seeds, triggers, functions, and views. ## Quick Start @@ -72,7 +72,7 @@ npx vitest run --config vitest.e2e.config.ts test/e2e/workflows/case-1-empty-db- | Case 3: Double Plan | Yes (2 containers) | start → plan → apply → add schema → plan → apply → commit → deploy | | Case 4: Existing DB Import | Yes (2 containers) | import → verify → plan → apply → commit → deploy | | Abort Workflow | Yes (1 container) | Session abort and cleanup verification | -| Infra/Grants/Seeds | Yes (1 container) | Infrastructure, grants, seed data, idempotency | +| Infra/Grants/Seeds | Yes (1 container) | Infrastructure, seed data, idempotency (grants handled by pgschema) | --- @@ -219,18 +219,16 @@ Verifies that aborting a session fully cleans up all artifacts. | Status reflects abort | `db status --json` returns `sessionActive: false` | Status is accurate | | Can restart | `db start --force` succeeds after abort | Abort doesn't permanently break the project | -### Infra, Grants & Seeds (`infra-grants-seeds.test.ts`) +### Infra & Seeds (`infra-grants-seeds.test.ts`) **Docker: Required (1 container) | Run: ~3s** -Tests infrastructure SQL (roles), grant permissions, and seed data management. Uses the fixture schema's infra, grants, and seed sections. +Tests infrastructure SQL (roles) and seed data management. Grant permissions are managed internally by pgschema — the fixture schema's `grant-permissions/` section is used to verify pgschema handles grants as part of the schema diff. Uses the fixture schema's infra, grants, and seed sections. | Test | What It Tests | Why It Matters | |------|---------------|----------------| | Shows infra | `db infra` displays role creation SQL (api_user, readonly, editor, manager) | Infra SQL is detected and rendered | | Applies infra | `db infra --apply` creates roles in PostgreSQL | Roles are created with proper DO$$ guards | -| Shows grants | `db grants` displays GRANT per role per table | Grant SQL is detected and rendered | -| Applies grants | `db grants --apply` grants permissions to roles | Role-based access control is set up | | Shows seeds | `db seed` displays idempotent seed data | Seed SQL is detected and rendered | | Applies seeds | `db seed --apply` inserts seed data | Idempotent inserts work (WHERE NOT EXISTS) | | Verifies seed data | Direct query confirms 3 seeded categories | Seeds were persisted | diff --git a/cli/src/modules/db/commands/apply.ts b/cli/src/modules/db/commands/apply.ts index c1dedf6..8100c86 100644 --- a/cli/src/modules/db/commands/apply.ts +++ b/cli/src/modules/db/commands/apply.ts @@ -14,7 +14,6 @@ import { } from "../services/dbmate"; import {generateSchemaFingerprint} from "../services/schema-generator"; import {applyInfra, loadInfra} from "../services/infra-generator"; -import {applyGrants, loadGrants} from "../services/grant-generator"; import {applySeeds, loadSeeds} from "../services/seed-generator"; import type {CommandOptions} from "../../../common/types"; import type {SessionState} from "../types/index"; @@ -34,29 +33,6 @@ async function applyInfraStep( spinner.succeed(`Infra applied (${infra.length} file(s))`); } -async function applyGrantsStep( - spinner: ReturnType<typeof ora>, - dbUrl: string, - retryHint: string, -): Promise<void> { - const grants = await loadGrants(); - if (grants.length === 0) { - spinner.info("No grant files found - skipping"); - return; - } - try { - spinner.start("Applying grants..."); - await applyGrants(dbUrl); - spinner.succeed(`Grants applied (${grants.length} file(s))`); - } catch (error) { - spinner.fail("Failed to apply grants"); - throw new PostkitError( - `Grants failed: ${error instanceof Error ? error.message : String(error)}`, - retryHint, - ); - } -} - async function applySeedsStep( spinner: ReturnType<typeof ora>, dbUrl: string, @@ -195,7 +171,6 @@ export async function applyCommand(options: CommandOptions): Promise<void> { if (newFiles.length > 0 && session.pendingChanges.migrationApplied) { await updatePendingChanges({ migrationApplied: false, - grantsApplied: false, seedsApplied: false, }); } @@ -225,22 +200,7 @@ async function handleResume( logger.blank(); let step = 1; - const totalSteps = 3; // grants, seeds, update session - - // Grants - if (!pc.grantsApplied) { - logger.step(step, totalSteps, "Applying grants..."); - await applyGrantsStep( - spinner, - session.localDbUrl, - 'Run "postkit db apply" again to retry from grants.', - ); - await updatePendingChanges({grantsApplied: true}); - } else { - logger.step(step, totalSteps, "Grants already applied - skipping"); - } - - step++; + const totalSteps = 2; // seeds, update session // Seeds if (!pc.seedsApplied) { @@ -333,7 +293,7 @@ async function handlePlanApply( logger.heading("Applying Migration to Local Database"); // Step 1: Show the plan - logger.step(1, 8, "Loading plan..."); + logger.step(1, 7, "Loading plan..."); const planContent = await getPlanFileContent(); if (planContent) { @@ -350,7 +310,7 @@ async function handlePlanApply( ); // Step 2: Test local connection - logger.step(2, 8, "Testing local database connection..."); + logger.step(2, 7, "Testing local database connection..."); spinner.start("Connecting to local database..."); const localConnected = await testConnection(session.localDbUrl); @@ -366,11 +326,11 @@ async function handlePlanApply( spinner.succeed("Connected to local database"); // Step 3: Apply infra (roles, schemas, extensions) - logger.step(3, 8, "Applying infrastructure..."); + logger.step(3, 7, "Applying infrastructure..."); await applyInfraStep(spinner, session.localDbUrl); // Step 4: Create migration file in session migrations dir - logger.step(4, 8, "Creating migration file..."); + logger.step(4, 7, "Creating migration file..."); spinner.start("Wrapping plan and creating migration file..."); @@ -401,7 +361,7 @@ async function handlePlanApply( logger.info(`Path: ${migrationFile.path}`); // Step 5: Apply migration via dbmate on local - logger.step(5, 8, "Applying migration to local database..."); + logger.step(5, 7, "Applying migration to local database..."); spinner.start("Running dbmate migrate..."); const migrateResult = await runSessionMigrate(session.localDbUrl); @@ -432,26 +392,17 @@ async function handlePlanApply( description, }); - // Step 6: Apply grants - logger.step(6, 8, "Applying grants..."); - await applyGrantsStep( - spinner, - session.localDbUrl, - 'Migration is already applied. Run "postkit db apply" again to retry from grants.', - ); - await updatePendingChanges({grantsApplied: true}); - - // Step 7: Apply seeds - logger.step(7, 8, "Applying seeds..."); + // Step 6: Apply seeds + logger.step(6, 7, "Applying seeds..."); await applySeedsStep( spinner, session.localDbUrl, - 'Migration and grants are already applied. Run "postkit db apply" again to retry from seeds.', + 'Migration is already applied. Run "postkit db apply" again to retry from seeds.', ); await updatePendingChanges({seedsApplied: true}); - // Step 8: Mark fully applied and clean up plan file - logger.step(8, 8, "Updating session state..."); + // Step 7: Mark fully applied and clean up plan file + logger.step(7, 7, "Updating session state..."); // Clean up plan file since migration is now committed to session files if (session.pendingChanges.planFile) { @@ -513,7 +464,7 @@ async function handleManualApply( } // Step 1: Test local connection - logger.step(1, 5, "Testing local database connection..."); + logger.step(1, 4, "Testing local database connection..."); spinner.start("Connecting to local database..."); const localConnected = await testConnection(session.localDbUrl); @@ -529,11 +480,11 @@ async function handleManualApply( spinner.succeed("Connected to local database"); // Step 2: Apply infra - logger.step(2, 5, "Applying infrastructure..."); + logger.step(2, 4, "Applying infrastructure..."); await applyInfraStep(spinner, session.localDbUrl); // Step 3: Apply migrations via dbmate - logger.step(3, 5, "Applying migration(s) to local database..."); + logger.step(3, 4, "Applying migration(s) to local database..."); spinner.start("Running dbmate migrate..."); const migrateResult = await runSessionMigrate(session.localDbUrl); @@ -563,21 +514,12 @@ async function handleManualApply( migrationFiles: appliedMigrations, }); - // Step 4: Apply grants - logger.step(4, 5, "Applying grants..."); - await applyGrantsStep( - spinner, - session.localDbUrl, - 'Migration(s) are already applied. Run "postkit db apply" again to retry from grants.', - ); - await updatePendingChanges({grantsApplied: true}); - - // Step 5: Apply seeds - logger.step(5, 5, "Applying seeds..."); + // Step 4: Apply seeds + logger.step(4, 4, "Applying seeds..."); await applySeedsStep( spinner, session.localDbUrl, - 'Migration(s) and grants are already applied. Run "postkit db apply" again to retry from seeds.', + 'Migration(s) are already applied. Run "postkit db apply" again to retry from seeds.', ); await updatePendingChanges({seedsApplied: true, applied: true}); diff --git a/cli/src/modules/db/commands/deploy.ts b/cli/src/modules/db/commands/deploy.ts index 00932e2..066d0cf 100644 --- a/cli/src/modules/db/commands/deploy.ts +++ b/cli/src/modules/db/commands/deploy.ts @@ -13,7 +13,6 @@ import { } from "../services/database"; import {runCommittedMigrate, runDbmateStatus} from "../services/dbmate"; import {loadInfra, applyInfra} from "../services/infra-generator"; -import {loadGrants, applyGrants} from "../services/grant-generator"; import {loadSeeds, applySeeds} from "../services/seed-generator"; import {getPendingCommittedMigrations} from "../utils/committed"; import {resolveRemote, maskRemoteUrl, normalizeUrl} from "../utils/remotes"; @@ -97,20 +96,6 @@ async function runSteps( spinner.succeed(`Migrations applied to ${label}`); step++; - // Grants - logger.step(step, totalSteps, `Applying grants to ${label}...`); - const grants = await loadGrants(); - - if (grants.length === 0) { - spinner.info("No grant files found - skipping"); - } else { - spinner.start(`Applying grants to ${label}...`); - await applyGrants(dbUrl); - spinner.succeed(`Grants applied to ${label} (${grants.length} file(s))`); - } - - step++; - // Seeds logger.step(step, totalSteps, `Applying seeds to ${label}...`); const seeds = await loadSeeds(); @@ -178,8 +163,8 @@ export async function deployCommand(options: DeployOptions): Promise<void> { logger.blank(); } - // 3 fixed steps (test, status, clone) + 4 runSteps × 2 passes (dry-run + target) + 1 fixed step (cleanup) - const totalSteps = 3 + 4 * 2 + 1; // = 12 + // 3 fixed steps (test, status, clone) + 3 runSteps × 2 passes (dry-run + target) + 1 fixed step (cleanup) + const totalSteps = 3 + 3 * 2 + 1; // = 10 const migrationNames = pendingMigrations.map(m => m.migrationFile.name); // Step 1: Test target DB connection @@ -229,7 +214,7 @@ export async function deployCommand(options: DeployOptions): Promise<void> { const localTableCount = await getTableCount(localDbUrl); spinner.succeed(`Target cloned to local (${localTableCount} tables)`); - // Steps 4-7: Dry run on local clone + // Steps 4-6: Dry run on local clone logger.blank(); logger.heading("Dry Run (local verification)"); @@ -285,12 +270,12 @@ export async function deployCommand(options: DeployOptions): Promise<void> { return; } - // Steps 8-11: Apply to target + // Steps 7-9: Apply to target logger.blank(); logger.heading("Deploying to Target"); try { - await runSteps(targetUrl, targetLabel, spinner, 8, totalSteps, migrationNames); + await runSteps(targetUrl, targetLabel, spinner, 7, totalSteps, migrationNames); } catch (error) { logger.error(error instanceof Error ? error.message : String(error)); logger.blank(); @@ -308,7 +293,7 @@ export async function deployCommand(options: DeployOptions): Promise<void> { // Step 12: Drop local clone logger.blank(); - logger.step(12, totalSteps, "Cleaning up local clone..."); + logger.step(10, totalSteps, "Cleaning up local clone..."); spinner.start("Dropping local clone database..."); try { diff --git a/cli/src/modules/db/commands/grants.ts b/cli/src/modules/db/commands/grants.ts deleted file mode 100644 index 25b6898..0000000 --- a/cli/src/modules/db/commands/grants.ts +++ /dev/null @@ -1,112 +0,0 @@ -import ora from "ora"; -import {logger} from "../../../common/logger"; -import { - loadGrants, - getGrantsSQL, - applyGrants, -} from "../services/grant-generator"; -import {getSession} from "../utils/session"; -import {testConnection} from "../services/database"; -import type {CommandOptions} from "../../../common/types"; - -interface GrantsOptions extends CommandOptions { - apply?: boolean; - target?: "local" | "remote"; -} - -export async function grantsCommand(options: GrantsOptions): Promise<void> { - const spinner = ora(); - - try { - logger.heading("Grant Statements"); - - // Step 1: Generate grants - logger.step(1, 2, "Loading grant files..."); - spinner.start("Scanning for grant files..."); - - const grants = await loadGrants(); - - if (grants.length === 0) { - spinner.warn("No grant files found"); - logger.blank(); - logger.info("Grant files should be placed in:"); - logger.info(" - db/schema/grants/"); - logger.info(" - db/schema/policies/"); - return; - } - - spinner.succeed(`Found ${grants.length} grant file(s)`); - - // Step 2: Display grants - logger.step(2, 2, "Generating grant statements..."); - - const grantsSQL = await getGrantsSQL(); - - logger.blank(); - logger.info("Generated Grant Statements:"); - logger.blank(); - - console.log(grantsSQL); - - logger.blank(); - - // Apply if requested - if (options.apply) { - const session = await getSession(); - let targetUrl: string | null = null; - let targetName: string; - - if (options.target === "remote") { - if (session) { - targetUrl = session.remoteDbUrl; - } else { - const {resolveRemote} = await import("../utils/remotes"); - const {url} = resolveRemote(); - targetUrl = url; - } - targetName = "remote"; - } else { - if (!session || !session.active) { - logger.error( - "No active session. Cannot apply grants to local database.", - ); - logger.info('Run "postkit db start" first or use --target=remote.'); - process.exit(1); - } - targetUrl = session.localDbUrl; - targetName = "local"; - } - - logger.info(`Applying grants to ${targetName} database...`); - spinner.start("Testing connection..."); - - const connected = await testConnection(targetUrl); - - if (!connected) { - spinner.fail(`Failed to connect to ${targetName} database`); - process.exit(1); - } - - spinner.succeed(`Connected to ${targetName} database`); - - if (options.dryRun) { - spinner.info("Dry run - skipping grant application"); - } else { - spinner.start("Applying grants..."); - await applyGrants(targetUrl); - spinner.succeed("Grants applied successfully"); - } - } - - logger.blank(); - logger.info("To apply these grants:"); - logger.info(' - Run "postkit db grants --apply" to apply to local clone'); - logger.info( - ' - Run "postkit db grants --apply --target=remote" to apply to remote', - ); - } catch (error) { - spinner.fail("Failed to generate grants"); - logger.error(error instanceof Error ? error.message : String(error)); - process.exit(1); - } -} diff --git a/cli/src/modules/db/commands/plan.ts b/cli/src/modules/db/commands/plan.ts index 7291d3f..c7d4ac2 100644 --- a/cli/src/modules/db/commands/plan.ts +++ b/cli/src/modules/db/commands/plan.ts @@ -69,7 +69,6 @@ export async function planCommand(options: CommandOptions): Promise<void> { planFile: planResult.planFile, schemaFingerprint, migrationApplied: false, - grantsApplied: false, seedsApplied: false, }); diff --git a/cli/src/modules/db/index.ts b/cli/src/modules/db/index.ts index 2056d5b..5ae6ae8 100644 --- a/cli/src/modules/db/index.ts +++ b/cli/src/modules/db/index.ts @@ -8,7 +8,6 @@ import {statusCommand} from "./commands/status"; import {abortCommand} from "./commands/abort"; import {migrationCommand} from "./commands/migration"; import {infraCommand} from "./commands/infra"; -import {grantsCommand} from "./commands/grants"; import {seedCommand} from "./commands/seed"; import {deployCommand} from "./commands/deploy"; import {importCommand} from "./commands/import"; @@ -114,18 +113,6 @@ export function registerDbModule(program: Command): void { }); }); - // Grants command - db.command("grants") - .description("Regenerate and show grant statements") - .option("--apply", "Apply grants to database") - .option("--target <target>", "Target database: local or remote", "local") - .action(async (cmdOptions) => { - await withInitCheck(async () => { - const options = {...program.opts(), ...cmdOptions}; - await grantsCommand(options); - }); - }); - // Seed command db.command("seed") .description("Show and apply seed data") diff --git a/cli/src/modules/db/services/grant-generator.ts b/cli/src/modules/db/services/grant-generator.ts deleted file mode 100644 index c006830..0000000 --- a/cli/src/modules/db/services/grant-generator.ts +++ /dev/null @@ -1,96 +0,0 @@ -import fs from "fs/promises"; -import path from "path"; -import {existsSync} from "fs"; -import {getDbConfig} from "../utils/db-config"; -import {loadSqlGroup} from "../utils/sql-loader"; -import type {GrantStatement} from "../types/index"; - -export async function loadGrants(): Promise<GrantStatement[]> { - const config = getDbConfig(); - const grantsPath = path.join(config.schemaPath, "grants"); - - if (!existsSync(grantsPath)) { - // Try alternative locations - const altPaths = [ - path.join(config.schemaPath, "policies"), - path.join(config.projectRoot, "grants"), - ]; - - for (const altPath of altPaths) { - if (existsSync(altPath)) { - return loadGrantsFromDirectory(altPath); - } - } - - return []; - } - - return loadGrantsFromDirectory(grantsPath); -} - -async function loadGrantsFromDirectory( - dirPath: string, -): Promise<GrantStatement[]> { - const grants: GrantStatement[] = []; - const entries = await fs.readdir(dirPath, {withFileTypes: true}); - - for (const entry of entries) { - const fullPath = path.join(dirPath, entry.name); - - if (entry.isFile() && entry.name.endsWith(".sql")) { - const content = await fs.readFile(fullPath, "utf-8"); - grants.push({ - schema: path.basename(entry.name, ".sql"), - content: content.trim(), - }); - } else if (entry.isDirectory()) { - // Load grants from subdirectory (e.g., grants/public/, grants/app/) - const subGrants = await loadGrantsFromSubdir(fullPath, entry.name); - grants.push(...subGrants); - } - } - - return grants; -} - -async function loadGrantsFromSubdir( - dirPath: string, - schemaName: string, -): Promise<GrantStatement[]> { - const entries = await loadSqlGroup(dirPath, schemaName); - return entries.map((e) => ({schema: e.name, content: e.content})); -} - -export async function getGrantsSQL(): Promise<string> { - const grants = await loadGrants(); - - if (grants.length === 0) { - return "-- No grant files found"; - } - - const parts: string[] = [ - "-- ============================================", - "-- GRANT STATEMENTS", - "-- ============================================", - "", - ]; - - for (const grant of grants) { - parts.push(`-- Schema: ${grant.schema}`); - parts.push(grant.content); - parts.push(""); - } - - return parts.join("\n"); -} - -export async function applyGrants(databaseUrl: string): Promise<void> { - const {executeSQL} = await import("./database"); - const grants = await loadGrants(); - - for (const grant of grants) { - if (grant.content.trim()) { - await executeSQL(databaseUrl, grant.content); - } - } -} diff --git a/cli/src/modules/db/types/index.ts b/cli/src/modules/db/types/index.ts index 29b2269..3aceedc 100644 --- a/cli/src/modules/db/types/index.ts +++ b/cli/src/modules/db/types/index.ts @@ -27,7 +27,6 @@ export type { // Schema types export type { - GrantStatement, SeedStatement, InfraStatement, } from "./schema"; diff --git a/cli/src/modules/db/types/schema.ts b/cli/src/modules/db/types/schema.ts index ecdc55f..11c8621 100644 --- a/cli/src/modules/db/types/schema.ts +++ b/cli/src/modules/db/types/schema.ts @@ -2,11 +2,6 @@ * Schema statement types */ -export interface GrantStatement { - schema: string; - content: string; -} - export interface SeedStatement { name: string; content: string; diff --git a/cli/src/modules/db/types/session.ts b/cli/src/modules/db/types/session.ts index d21d590..95fe098 100644 --- a/cli/src/modules/db/types/session.ts +++ b/cli/src/modules/db/types/session.ts @@ -17,7 +17,6 @@ export interface SessionState { description: string | null; schemaFingerprint: string | null; migrationApplied: boolean; - grantsApplied: boolean; seedsApplied: boolean; }; } diff --git a/cli/src/modules/db/utils/session.ts b/cli/src/modules/db/utils/session.ts index 57714df..244b5b3 100644 --- a/cli/src/modules/db/utils/session.ts +++ b/cli/src/modules/db/utils/session.ts @@ -43,7 +43,6 @@ export async function createSession( description: null, schemaFingerprint: null, migrationApplied: false, - grantsApplied: false, seedsApplied: false, }, }; diff --git a/cli/test/e2e/workflows/infra-grants-seeds.test.ts b/cli/test/e2e/workflows/infra-grants-seeds.test.ts index 537f1d8..8bd746e 100644 --- a/cli/test/e2e/workflows/infra-grants-seeds.test.ts +++ b/cli/test/e2e/workflows/infra-grants-seeds.test.ts @@ -7,7 +7,7 @@ import {startPostgres, stopPostgres, type TestDatabase} from "../helpers/test-da import {executeSql, queryDatabase} from "../helpers/db-query"; import {installFixtureSections} from "../helpers/schema-builder"; -describe("Infra, grants, and seeds workflow", () => { +describe("Infra and seeds workflow", () => { let db: TestDatabase; let project: TestProject; @@ -76,36 +76,6 @@ describe("Infra, grants, and seeds workflow", () => { expect(roleNames).toContain("manager"); }); - it("shows grants files", async () => { - const result = await runCli(["db", "grants"], {cwd: project.rootDir}); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("category"); - expect(result.stdout).toContain("product"); - }); - - it("applies grants to local database", async () => { - const result = await runCli(["db", "grants", "--apply"], {cwd: project.rootDir}); - expect(result.exitCode).toBe(0); - - // Verify grants were applied — editor should have SELECT on category - const grants = await queryDatabase( - db.url, - `SELECT grantee, table_name, privilege_type - FROM information_schema.role_table_grants - WHERE grantee IN ('readonly', 'editor', 'manager') - AND table_name IN ('category', 'product') - ORDER BY grantee, table_name, privilege_type`, - ); - expect(grants.length).toBeGreaterThan(0); - - // Check manager has ALL on product - const managerProductGrants = grants.filter( - (g) => (g as {grantee: string; table_name: string}).grantee === "manager" && - (g as {grantee: string; table_name: string}).table_name === "product", - ); - expect(managerProductGrants.length).toBeGreaterThan(0); - }); - it("shows seed files", async () => { const result = await runCli(["db", "seed"], {cwd: project.rootDir}); expect(result.exitCode).toBe(0); diff --git a/cli/test/modules/db/services/grant-generator.test.ts b/cli/test/modules/db/services/grant-generator.test.ts deleted file mode 100644 index 7ce70d6..0000000 --- a/cli/test/modules/db/services/grant-generator.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import {describe, it, expect, vi, beforeEach} from "vitest"; - -vi.mock("../../../../src/modules/db/utils/db-config", () => ({ - getDbConfig: vi.fn(() => ({ - localDbUrl: "postgres://localhost:5432/test", - schemaPath: "/project/schema", - schema: "public", - remotes: {}, - cliRoot: "/cli", - projectRoot: "/project", - })), -})); - -vi.mock("../../../../src/modules/db/utils/sql-loader", () => ({ - loadSqlGroup: vi.fn(async (dir: string, name: string) => [{name, content: `GRANT ALL ON ${name};`}]), -})); - -vi.mock("fs/promises", async () => { - const {vi} = await import("vitest"); - const fns = {readdir: vi.fn(), readFile: vi.fn()}; - return {default: fns, ...fns}; -}); - -vi.mock("fs", async () => { - const {vi} = await import("vitest"); - const fns = {existsSync: vi.fn()}; - return {default: fns, ...fns}; -}); - -import fs from "fs/promises"; -import {existsSync} from "fs"; -import {loadGrants, getGrantsSQL, applyGrants} from "../../../../src/modules/db/services/grant-generator"; - -describe("grant-generator", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe("loadGrants()", () => { - it("returns empty array when grants dir missing", async () => { - vi.mocked(existsSync).mockReturnValue(false); - expect(await loadGrants()).toEqual([]); - }); - - it("reads from primary grants directory", async () => { - vi.mocked(existsSync).mockReturnValue(true); - vi.mocked(fs.readdir).mockResolvedValue([ - {name: "public.sql", isFile: () => true, isDirectory: () => false}, - ] as any); - vi.mocked(fs.readFile).mockResolvedValue("GRANT ALL ON SCHEMA public TO admin;"); - const grants = await loadGrants(); - expect(grants).toHaveLength(1); - expect(grants[0]!.content).toContain("GRANT"); - }); - }); - - describe("getGrantsSQL()", () => { - it("formats grants with section headers", async () => { - vi.mocked(existsSync).mockReturnValue(true); - vi.mocked(fs.readdir).mockResolvedValue([ - {name: "public.sql", isFile: () => true, isDirectory: () => false}, - ] as any); - vi.mocked(fs.readFile).mockResolvedValue("GRANT ALL;"); - const sql = await getGrantsSQL(); - expect(sql).toContain("GRANT STATEMENTS"); - expect(sql).toContain("GRANT ALL;"); - }); - }); - - describe("applyGrants()", () => { - it("calls executeSQL for each grant", async () => { - vi.mocked(existsSync).mockReturnValue(true); - vi.mocked(fs.readdir).mockResolvedValue([ - {name: "a.sql", isFile: () => true, isDirectory: () => false}, - ] as any); - vi.mocked(fs.readFile).mockResolvedValue("GRANT SELECT;"); - vi.doMock("../../../../src/modules/db/services/database", () => ({ - executeSQL: vi.fn().mockResolvedValue("[]"), - })); - await applyGrants("postgres://user:pass@host/db"); - }); - }); -}); diff --git a/docs/docs/modules/db/commands/apply.md b/docs/docs/modules/db/commands/apply.md index a2f9d09..b30802a 100644 --- a/docs/docs/modules/db/commands/apply.md +++ b/docs/docs/modules/db/commands/apply.md @@ -29,10 +29,9 @@ postkit db apply [-f] 4. Applies infrastructure SQL from `db/schema/infra/` 5. Wraps the plan SQL and creates a dbmate migration file (staged in `.postkit/db/session/`) 6. Runs `dbmate migrate` on the local database -7. Applies grant statements from `db/schema/grants/` -8. Applies seed data from `db/schema/seeds/` +7. Applies seed data from `db/schema/seeds/` -**Resume support:** If grants or seeds fail, re-running `postkit db apply` resumes from where it left off. +**Resume support:** If seeds fail, re-running `postkit db apply` resumes from where it left off. ## Requirements diff --git a/docs/docs/modules/db/commands/deploy.md b/docs/docs/modules/db/commands/deploy.md index d4da448..3087b8c 100644 --- a/docs/docs/modules/db/commands/deploy.md +++ b/docs/docs/modules/db/commands/deploy.md @@ -45,10 +45,10 @@ postkit db deploy --remote staging -f 2. If an active session exists, removes it (with confirmation unless `-f`) 3. Tests the target database connection 4. Clones the target database to local (using `LOCAL_DATABASE_URL`) -5. Runs a full dry-run on the local clone: infra, dbmate migrate, grants, seeds +5. Runs a full dry-run on the local clone: infra, dbmate migrate, seeds 6. If `--dry-run` is set, stops here and reports results 7. Reports dry-run results and confirms deployment (unless `-f`) -8. Applies to target: infra, dbmate migrate, grants, seeds +8. Applies to target: infra, dbmate migrate, seeds 9. Drops the local clone database 10. Marks migrations as deployed in `.postkit/db/committed.json` diff --git a/docs/docs/modules/db/commands/grants.md b/docs/docs/modules/db/commands/grants.md deleted file mode 100644 index 9672a4d..0000000 --- a/docs/docs/modules/db/commands/grants.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -sidebar_position: 11 ---- - -# db grants - -Regenerate and display grant statements from `db/schema/grants/`. - -## Usage - -```bash -postkit db grants [--apply] [--target <target>] -``` - -**`<target>`**: `local` or `remote` - -## Options - -| Option | Description | -|--------|-------------| -| `--apply` | Apply grant statements | -| `--target` | Target for apply: `local` or `remote` (default: local) | -| `-v, --verbose` | Enable verbose output | -| `--dry-run` | Show what would be done without making changes | -| `--json` | Output as JSON | - -## Examples - -```bash -# Show grants -postkit db grants - -# Apply to local database -postkit db grants --apply - -# Apply to remote database -postkit db grants --apply --target=remote -``` - -## What It Does - -Without `--apply`, displays the grant statements that would be run. - -With `--apply`, executes the grant statements on the target database. - -## Related - -- [`infra`](/docs/modules/db/commands/infra) - Manage infrastructure -- [`seed`](/docs/modules/db/commands/seed) - Manage seed data diff --git a/docs/docs/modules/db/commands/import.md b/docs/docs/modules/db/commands/import.md index bcb5b6b..ae67a14 100644 --- a/docs/docs/modules/db/commands/import.md +++ b/docs/docs/modules/db/commands/import.md @@ -47,7 +47,7 @@ postkit db import --schema myschema --name initial_baseline - Roles queried from `pg_roles` → written to `infra/roles.sql` using idempotent `DO $$ IF NOT EXISTS $$` blocks - Schemas queried from `pg_namespace` → written to `infra/schemas.sql` as `CREATE SCHEMA IF NOT EXISTS` - Extensions parsed from `schema.sql` → written to `extensions/imported_extensions.sql` - - Privileges consolidated into `grants/<schema>.sql` + - Privileges consolidated into `grants/<schema>.sql` (managed by pgschema) 6. **Baseline migration** — Clears existing migrations directory, runs `pgschema plan` against an empty temp database to generate full CREATE DDL, writes it to `.postkit/db/migrations/`, and updates `committed.json` 7. **Local setup** — Creates the local database, applies infrastructure SQL (roles, schemas), then applies the baseline migration via `dbmate` 8. **Sync migration state** — After successful local apply, inserts the baseline version into `schema_migrations` on the source database @@ -110,7 +110,7 @@ db/schema/ │ └── 001_dashboard_summary.sql ├── extensions/ │ └── imported_extensions.sql -├── grants/ +├── grants/ # Managed by pgschema │ └── public.sql # Consolidated privileges └── .pgschemaignore # Excludes schema_migrations table ``` diff --git a/docs/docs/modules/db/commands/infra.md b/docs/docs/modules/db/commands/infra.md index 263ba3a..1953944 100644 --- a/docs/docs/modules/db/commands/infra.md +++ b/docs/docs/modules/db/commands/infra.md @@ -45,5 +45,4 @@ With `--apply`, executes the infrastructure SQL on the target database. ## Related -- [`grants`](/docs/modules/db/commands/grants) - Manage grant statements - [`seed`](/docs/modules/db/commands/seed) - Manage seed data diff --git a/docs/docs/modules/db/commands/plan.md b/docs/docs/modules/db/commands/plan.md index 3a21049..aad6260 100644 --- a/docs/docs/modules/db/commands/plan.md +++ b/docs/docs/modules/db/commands/plan.md @@ -21,7 +21,7 @@ postkit db plan ## What It Does -1. Combines all schema files from `db/schema/` into a single SQL file (excluding `infra/`, `grants/`, `seeds/`) +1. Combines all schema files from `db/schema/` into a single SQL file (excluding `infra/`, `seeds/`) 2. Runs `pgschema plan` to compare against local database 3. Saves a schema fingerprint (SHA-256 hash of source files) for validation during apply 4. Displays the migration plan and saves to `.postkit/db/plan.sql` diff --git a/docs/docs/modules/db/commands/seed.md b/docs/docs/modules/db/commands/seed.md index 9d96f14..c9f30c9 100644 --- a/docs/docs/modules/db/commands/seed.md +++ b/docs/docs/modules/db/commands/seed.md @@ -46,4 +46,3 @@ With `--apply`, executes the seed data SQL on the target database. ## Related - [`infra`](/docs/modules/db/commands/infra) - Manage infrastructure -- [`grants`](/docs/modules/db/commands/grants) - Manage grants diff --git a/docs/docs/modules/db/migrating-existing-database.md b/docs/docs/modules/db/migrating-existing-database.md index 49c2e8e..db78320 100644 --- a/docs/docs/modules/db/migrating-existing-database.md +++ b/docs/docs/modules/db/migrating-existing-database.md @@ -68,7 +68,7 @@ db/schema/ │ └── ... ├── views/ ├── functions/ -├── grants/ +├── grants/ # Managed by pgschema │ └── public.sql # Consolidated privileges └── .pgschemaignore ``` diff --git a/docs/docs/modules/db/overview.md b/docs/docs/modules/db/overview.md index 1849fec..1f4e912 100644 --- a/docs/docs/modules/db/overview.md +++ b/docs/docs/modules/db/overview.md @@ -36,9 +36,8 @@ The `db` module provides a **session-based database migration workflow** for saf │ │ 8. Create dbmate │ │ migration │ │ │ │ migration │ │ 9. Run dbmate │ │ │ │ 9. Run dbmate │ │ on local DB │ │ -│ │ on local DB │ │ 10. Apply grants │ │ -│ │ 10. Apply grants │ │ 11. Apply seeds │ │ -│ │ 11. Apply seeds │ └────────┬─────────┘ │ +│ │ on local DB │ │ 10. Apply seeds │ │ +│ │ 10. Apply seeds │ └────────┬─────────┘ │ │ └────────┬─────────┘ │ │ │ │ ▼ │ │ $ postkit db commit │ @@ -75,7 +74,6 @@ The `db` module provides a **session-based database migration workflow** for saf | [`migration`](/docs/modules/db/commands/migration) | Create manual migration | | [`remote`](/docs/modules/db/commands/remote) | Manage remote databases | | [`infra`](/docs/modules/db/commands/infra) | Manage infrastructure SQL | -| [`grants`](/docs/modules/db/commands/grants) | Manage grant statements | | [`seed`](/docs/modules/db/commands/seed) | Manage seed data | | [`import`](/docs/modules/db/commands/import) | Import existing database as baseline | @@ -125,7 +123,6 @@ db/schema/ ``` db/schema/ -├── grants/ # GRANT statements (post-migration) └── seeds/ # Seed data (post-migration) ``` @@ -135,7 +132,7 @@ db/schema/ 1. **Pre-migration:** `infra/` (roles, schemas, extensions) 2. **Migration:** pgschema processes types → enums → tables → views → materialized_views → functions → triggers → indexes → constraints -3. **Post-migration:** `grants/` (permissions) → `seeds/` (data) +3. **Post-migration:** `seeds/` (data) ## Prerequisites diff --git a/docs/docs/modules/db/troubleshooting.md b/docs/docs/modules/db/troubleshooting.md index 6a541df..1f22271 100644 --- a/docs/docs/modules/db/troubleshooting.md +++ b/docs/docs/modules/db/troubleshooting.md @@ -34,7 +34,7 @@ sidebar_position: 100 **Solution:** Schema files were modified after running `plan`. Run `postkit db plan` again -### `Grants/seeds failed during apply` +### `Seeds failed during apply` **Solution:** Re-run `postkit db apply` — it resumes from where it left off diff --git a/docs/docs/reference/project-structure.md b/docs/docs/reference/project-structure.md index 175eb66..e4a1608 100644 --- a/docs/docs/reference/project-structure.md +++ b/docs/docs/reference/project-structure.md @@ -27,7 +27,7 @@ my-project/ │ │ └── ... │ ├── triggers/ │ ├── indexes/ -│ ├── grants/ # Grant statements +│ ├── grants/ # Grant statements (managed by pgschema) │ └── seeds/ # Seed data ├── .postkit/ # PostKit runtime (gitignored) │ ├── db/ # All DB runtime files @@ -73,7 +73,7 @@ The `db/schema/` directory is organized into three categories: | Directory | Description | Processed By | |-----------|-------------|--------------| -| `grants/` | Grant statements | Applied separately | +| `grants/` | Grant statements | Managed by pgschema | | `seeds/` | Seed data | Applied separately | **Note:** Cluster and database level commands (CREATE DATABASE, CREATE ROLE, CREATE EXTENSION, etc.) are not supported by pgschema. Use `db/schema/infra/` or manual migrations instead. diff --git a/docs/docs/reference/session-state.md b/docs/docs/reference/session-state.md index c1f796a..e7f175b 100644 --- a/docs/docs/reference/session-state.md +++ b/docs/docs/reference/session-state.md @@ -24,7 +24,6 @@ The database module uses a session-based workflow. Session state is tracked in ` "description": null, "schemaFingerprint": null, "migrationApplied": false, - "grantsApplied": false, "seedsApplied": false } } @@ -53,7 +52,6 @@ The database module uses a session-based workflow. Session state is tracked in ` | `description` | Migration description | | `schemaFingerprint` | SHA-256 hash of schema files | | `migrationApplied` | Whether dbmate migration was applied | -| `grantsApplied` | Whether grants were applied | | `seedsApplied` | Whether seeds were applied | ## Session Lifecycle diff --git a/docs/sidebars.ts b/docs/sidebars.ts index cfaa800..0492802 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -40,7 +40,6 @@ const sidebars: SidebarsConfig = { 'modules/db/commands/migration', 'modules/db/commands/remote', 'modules/db/commands/infra', - 'modules/db/commands/grants', 'modules/db/commands/seed', 'modules/db/commands/import', ], From 063cf71e3fddbdf838149d531ba1d552ac59145a Mon Sep 17 00:00:00 2001 From: supunappri99 <supun@appritechnologies.com> Date: Wed, 29 Apr 2026 21:28:37 +0530 Subject: [PATCH 5/9] fix: correct deployment step sequence number for local clone cleanup --- cli/src/modules/db/commands/deploy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/modules/db/commands/deploy.ts b/cli/src/modules/db/commands/deploy.ts index 066d0cf..beacad9 100644 --- a/cli/src/modules/db/commands/deploy.ts +++ b/cli/src/modules/db/commands/deploy.ts @@ -291,7 +291,7 @@ export async function deployCommand(options: DeployOptions): Promise<void> { ); } - // Step 12: Drop local clone + // Step 10: Drop local clone logger.blank(); logger.step(10, totalSteps, "Cleaning up local clone..."); spinner.start("Dropping local clone database..."); From c839940fa72617e852f6856836b80a58b4aca580 Mon Sep 17 00:00:00 2001 From: supunappri99 <supun@appritechnologies.com> Date: Wed, 29 Apr 2026 22:18:26 +0530 Subject: [PATCH 6/9] refactor: store database migration paths as project-relative paths for portability --- cli/src/modules/db/commands/apply.ts | 16 +++++++++------- cli/src/modules/db/commands/commit.ts | 9 ++++++--- cli/src/modules/db/commands/import.ts | 4 ++-- cli/src/modules/db/commands/plan.ts | 3 ++- cli/src/modules/db/utils/db-config.ts | 19 +++++++++++++++++++ 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/cli/src/modules/db/commands/apply.ts b/cli/src/modules/db/commands/apply.ts index 8100c86..e585fb7 100644 --- a/cli/src/modules/db/commands/apply.ts +++ b/cli/src/modules/db/commands/apply.ts @@ -1,10 +1,11 @@ import ora from "ora"; +import path from "path"; import fs from "fs/promises"; import {promptConfirm, promptInput} from "../../../common/prompt"; import {existsSync} from "fs"; import {logger} from "../../../common/logger"; import {getSession, updatePendingChanges} from "../utils/session"; -import {getSessionMigrationsPath} from "../utils/db-config"; +import {getSessionMigrationsPath, toRelativePath, resolveProjectPath} from "../utils/db-config"; import {wrapPlanSQL, getPlanFileContent} from "../services/pgschema"; import {testConnection} from "../services/database"; import { @@ -341,7 +342,7 @@ async function handlePlanApply( ); } - const wrappedSQL = await wrapPlanSQL(session.pendingChanges.planFile); + const wrappedSQL = await wrapPlanSQL(resolveProjectPath(session.pendingChanges.planFile)); if (!wrappedSQL) { spinner.succeed("No changes to apply"); @@ -368,7 +369,7 @@ async function handlePlanApply( if (!migrateResult.success) { spinner.fail("Failed to apply migration"); - await deleteMigrationFile(migrationFile.path); + await deleteMigrationFile(resolveProjectPath(migrationFile.path)); throw new PostkitError( `Migration apply failed:\n${migrateResult.output}`, 'Migration file has been cleaned up. Fix the SQL and run "postkit db apply" again.', @@ -387,7 +388,7 @@ async function handlePlanApply( migrationApplied: true, migrationFiles: [ ...existingFiles, - {name: migrationFile.name, path: migrationFile.path}, + {name: migrationFile.name, path: toRelativePath(migrationFile.path)}, ], description, }); @@ -406,8 +407,9 @@ async function handlePlanApply( // Clean up plan file since migration is now committed to session files if (session.pendingChanges.planFile) { - if (existsSync(session.pendingChanges.planFile)) { - await fs.unlink(session.pendingChanges.planFile); + const absolutePlanPath = resolveProjectPath(session.pendingChanges.planFile); + if (existsSync(absolutePlanPath)) { + await fs.unlink(absolutePlanPath); } } @@ -506,7 +508,7 @@ async function handleManualApply( // Track applied migrations const appliedMigrations = migrationFiles.map((name) => ({ name, - path: `${sessionMigrationsDir}/${name}`, + path: toRelativePath(path.join(sessionMigrationsDir, name)), })); await updatePendingChanges({ diff --git a/cli/src/modules/db/commands/commit.ts b/cli/src/modules/db/commands/commit.ts index 1a93fdf..ee49ed9 100644 --- a/cli/src/modules/db/commands/commit.ts +++ b/cli/src/modules/db/commands/commit.ts @@ -2,7 +2,7 @@ import ora from "ora"; import {logger} from "../../../common/logger"; import {promptInput} from "../../../common/prompt"; import {getSession, deleteSession} from "../utils/session"; -import {getSessionMigrationsPath} from "../utils/db-config"; +import {getSessionMigrationsPath, toRelativePath} from "../utils/db-config"; import {mergeSessionMigrations, deleteSessionMigrations} from "../services/dbmate"; import {deletePlanFile} from "../services/pgschema"; import {deleteGeneratedSchema} from "../services/schema-generator"; @@ -92,11 +92,14 @@ export async function commitCommand(options: CommitOptions): Promise<void> { await addCommittedMigration({ migrationFile: { name: mergedMigration.name, - path: mergedMigration.path, + path: toRelativePath(mergedMigration.path), timestamp: mergedMigration.timestamp, }, description, - sessionMigrations: migrationFiles, + sessionMigrations: migrationFiles.map((mf) => ({ + name: mf.name, + path: mf.path, // already relative from session + })), committedAt: new Date().toISOString(), }); diff --git a/cli/src/modules/db/commands/import.ts b/cli/src/modules/db/commands/import.ts index 2b97c49..3f1a7d0 100644 --- a/cli/src/modules/db/commands/import.ts +++ b/cli/src/modules/db/commands/import.ts @@ -5,7 +5,7 @@ import path from "path"; import {logger} from "../../../common/logger"; import {promptConfirm} from "../../../common/prompt"; import {PostkitError} from "../../../common/errors"; -import {getDbConfig, getTmpImportDir, getCommittedMigrationsPath} from "../utils/db-config"; +import {getDbConfig, getTmpImportDir, getCommittedMigrationsPath, toRelativePath} from "../utils/db-config"; import {hasActiveSession} from "../utils/session"; import {addCommittedMigration, saveCommittedState} from "../utils/committed"; import {testConnection, getTableCount, createDatabase} from "../services/database"; @@ -246,7 +246,7 @@ export async function importCommand(options: ImportOptions): Promise<void> { await addCommittedMigration({ migrationFile: { name: migrationFile.name, - path: migrationFile.path, + path: toRelativePath(migrationFile.path), timestamp: migrationFile.timestamp, }, description: `Baseline import (${schemaName})`, diff --git a/cli/src/modules/db/commands/plan.ts b/cli/src/modules/db/commands/plan.ts index c7d4ac2..b7ce790 100644 --- a/cli/src/modules/db/commands/plan.ts +++ b/cli/src/modules/db/commands/plan.ts @@ -1,6 +1,7 @@ import ora from "ora"; import {logger} from "../../../common/logger"; import {getSession, updatePendingChanges} from "../utils/session"; +import {toRelativePath} from "../utils/db-config"; import {generateSchemaSQLAndFingerprint} from "../services/schema-generator"; import {runPgschemaplan} from "../services/pgschema"; import {testConnection} from "../services/database"; @@ -66,7 +67,7 @@ export async function planCommand(options: CommandOptions): Promise<void> { await updatePendingChanges({ planned: true, applied: false, - planFile: planResult.planFile, + planFile: planResult.planFile ? toRelativePath(planResult.planFile) : null, schemaFingerprint, migrationApplied: false, seedsApplied: false, diff --git a/cli/src/modules/db/utils/db-config.ts b/cli/src/modules/db/utils/db-config.ts index 60d076c..b73c9ee 100644 --- a/cli/src/modules/db/utils/db-config.ts +++ b/cli/src/modules/db/utils/db-config.ts @@ -185,3 +185,22 @@ export function getCommittedFilePath(): string { export function getTmpImportDir(): string { return path.join(getPostkitDbDir(), "tmp-import"); } + +/** + * Convert an absolute path to a path relative to the project root. + * Used when storing paths in session.json / committed.json for portability. + */ +export function toRelativePath(absolutePath: string): string { + return path.relative(projectRoot, absolutePath); +} + +/** + * Resolve a relative path (from session.json / committed.json) back to absolute. + * If the path is already absolute, returns it as-is (backward compatibility). + */ +export function resolveProjectPath(filePath: string): string { + if (path.isAbsolute(filePath)) { + return filePath; + } + return path.resolve(projectRoot, filePath); +} From 97393a1dad605c15d3eaea97d83146c784ae756c Mon Sep 17 00:00:00 2001 From: supunappri99 <supun@appritechnologies.com> Date: Thu, 30 Apr 2026 08:54:31 +0530 Subject: [PATCH 7/9] chore: bump package version to 1.2.0 --- cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/package.json b/cli/package.json index 508a0c2..b53f504 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@appritech/postkit", - "version": "1.1.0", + "version": "1.2.0", "description": "PostKit - Developer toolkit for database management and more", "type": "module", "main": "dist/index.js", From d09cbaca43abef8a1fb6bce068cda7513bde6e77 Mon Sep 17 00:00:00 2001 From: supunappri99 <supun@appritechnologies.com> Date: Thu, 30 Apr 2026 10:46:45 +0530 Subject: [PATCH 8/9] test: add e2e test for custom schema workflow migration flow --- .../case-5-custom-schema-full-flow.test.ts | 428 ++++++++++++++++++ 1 file changed, 428 insertions(+) create mode 100644 cli/test/e2e/workflows/case-5-custom-schema-full-flow.test.ts diff --git a/cli/test/e2e/workflows/case-5-custom-schema-full-flow.test.ts b/cli/test/e2e/workflows/case-5-custom-schema-full-flow.test.ts new file mode 100644 index 0000000..02ec5e1 --- /dev/null +++ b/cli/test/e2e/workflows/case-5-custom-schema-full-flow.test.ts @@ -0,0 +1,428 @@ +import {describe, it, expect, beforeAll, afterAll} from "vitest"; +import fs from "fs"; +import path from "path"; +import { + createTestProject, + cleanupTestProject, + fileExists, + type TestProject, +} from "../helpers/test-project"; +import { + startPostgresPair, + stopPostgresPair, + type TestDatabase, +} from "../helpers/test-database"; +import {executeSql, queryDatabase} from "../helpers/db-query"; +import {runCli} from "../helpers/cli-runner"; +import { + startSession, + runPlan, + runApply, + runCommit, + runDeploy, + getStatus, +} from "../helpers/workflow"; + +/** + * Case 5: Custom Schema — Full Flow + * + * Tests the complete migration workflow (import → start → plan → apply → commit → deploy) + * with tables in a CUSTOM schema ("myapp"), not the default "public". + * + * Flow: + * 1. Seed remote DB with objects in custom schema "myapp" + * 2. Import with `db import --schema myapp` + * 3. Start a new session (clones remote) + * 4. Add a new table to schema files + * 5. Plan → Apply → Commit → Deploy + * 6. Verify the new table exists in custom schema on remote + */ + +const CUSTOM_SCHEMA = "myapp"; + +// SQL to seed the remote database with objects in custom schema +const SEED_SQL = ` + CREATE SCHEMA IF NOT EXISTS ${CUSTOM_SCHEMA}; + + -- Function: update_updated_at + CREATE FUNCTION ${CUSTOM_SCHEMA}.update_updated_at() RETURNS trigger + LANGUAGE plpgsql AS $$ + BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW; END; + $$; + + -- Table: category + CREATE TABLE ${CUSTOM_SCHEMA}.category ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(100) NOT NULL, + is_deleted BOOLEAN DEFAULT false NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP + ); + + -- Table: product + CREATE TABLE ${CUSTOM_SCHEMA}.product ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(200) NOT NULL, + sku VARCHAR(50) NOT NULL, + category_id UUID NOT NULL REFERENCES ${CUSTOM_SCHEMA}.category(id) ON DELETE RESTRICT, + price DOUBLE PRECISION NOT NULL CHECK (price >= 0), + is_deleted BOOLEAN DEFAULT false NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP + ); + + -- Trigger: category timestamp + CREATE TRIGGER update_category_timestamp + BEFORE UPDATE ON ${CUSTOM_SCHEMA}.category FOR EACH ROW + EXECUTE FUNCTION ${CUSTOM_SCHEMA}.update_updated_at(); + + -- Trigger: product timestamp + CREATE TRIGGER update_product_timestamp + BEFORE UPDATE ON ${CUSTOM_SCHEMA}.product FOR EACH ROW + EXECUTE FUNCTION ${CUSTOM_SCHEMA}.update_updated_at(); + + -- View: product list with category + CREATE VIEW ${CUSTOM_SCHEMA}.product_list AS + SELECT p.id, p.name, p.sku, p.price, c.name AS category_name + FROM ${CUSTOM_SCHEMA}.product p + JOIN ${CUSTOM_SCHEMA}.category c ON c.id = p.category_id + WHERE p.is_deleted = false; + + -- Seed data + INSERT INTO ${CUSTOM_SCHEMA}.category (id, name) VALUES + ('a0000000-0000-0000-0000-000000000001'::UUID, 'Electronics'), + ('a0000000-0000-0000-0000-000000000002'::UUID, 'Furniture'); + INSERT INTO ${CUSTOM_SCHEMA}.product (name, sku, category_id, price) VALUES + ('Laptop', 'SKU-001', 'a0000000-0000-0000-0000-000000000001'::UUID, 999.99), + ('Desk Chair', 'SKU-002', 'a0000000-0000-0000-0000-000000000002'::UUID, 299.99); +`; + +// ── Schema-aware verification helpers ────────────────────────────────────── + +async function verifyTablesInSchema( + dbUrl: string, + schema: string, + tables: string[], + label = "DB", +): Promise<void> { + const rows = await queryDatabase( + dbUrl, + `SELECT table_name FROM information_schema.tables + WHERE table_schema = $1 AND table_type = 'BASE TABLE' + ORDER BY table_name`, + [schema], + ); + const found = rows.map((r) => (r as {table_name: string}).table_name); + for (const table of tables) { + expect(found, `Table '${table}' should exist in schema '${schema}' (${label})`).toContain(table); + } +} + +async function verifyFunctionsInSchema( + dbUrl: string, + schema: string, + functionNames: string[], + label = "DB", +): Promise<void> { + const rows = await queryDatabase( + dbUrl, + `SELECT routine_name FROM information_schema.routines + WHERE routine_schema = $1 AND routine_type = 'FUNCTION' + ORDER BY routine_name`, + [schema], + ); + const found = rows.map((r) => (r as {routine_name: string}).routine_name); + for (const name of functionNames) { + expect(found, `Function '${name}' should exist in schema '${schema}' (${label})`).toContain(name); + } +} + +async function verifyViewsInSchema( + dbUrl: string, + schema: string, + viewNames: string[], + label = "DB", +): Promise<void> { + const rows = await queryDatabase( + dbUrl, + `SELECT table_name FROM information_schema.views + WHERE table_schema = $1 + ORDER BY table_name`, + [schema], + ); + const found = rows.map((r) => (r as {table_name: string}).table_name); + for (const name of viewNames) { + expect(found, `View '${name}' should exist in schema '${schema}' (${label})`).toContain(name); + } +} + +async function verifyTriggersInSchema( + dbUrl: string, + schema: string, + triggerNames: string[], + label = "DB", +): Promise<void> { + const rows = await queryDatabase( + dbUrl, + `SELECT trigger_name FROM information_schema.triggers + WHERE trigger_schema = $1 + ORDER BY trigger_name`, + [schema], + ); + const found = rows.map((r) => (r as {trigger_name: string}).trigger_name); + for (const name of triggerNames) { + expect(found, `Trigger '${name}' should exist in schema '${schema}' (${label})`).toContain(name); + } +} + +async function verifySeedsInSchema( + dbUrl: string, + schema: string, +): Promise<void> { + const categories = await queryDatabase( + dbUrl, + `SELECT name FROM ${schema}.category ORDER BY name`, + ); + const names = categories.map((r) => (r as {name: string}).name); + expect(names).toContain("Electronics"); + expect(names).toContain("Furniture"); + + const products = await queryDatabase( + dbUrl, + `SELECT name FROM ${schema}.product ORDER BY name`, + ); + const productNames = products.map((r) => (r as {name: string}).name); + expect(productNames).toContain("Laptop"); + expect(productNames).toContain("Desk Chair"); +} + +// ── Test ─────────────────────────────────────────────────────────────────── + +describe("Case 5: Custom Schema — import → start → plan → apply → commit → deploy", () => { + let localDb: TestDatabase; + let remoteDb: TestDatabase; + let project: TestProject; + + beforeAll(async () => { + const {local, remote} = await startPostgresPair(); + localDb = local; + remoteDb = remote; + + // Seed remote DB with objects in custom schema + await executeSql(remoteDb.url, SEED_SQL); + + project = await createTestProject({ + localDbUrl: localDb.url, + remoteDbUrl: remoteDb.url, + remoteName: "dev", + }); + + // Set custom schema in config + const config = JSON.parse(fs.readFileSync(project.configPath, "utf-8")); + config.db.schema = CUSTOM_SCHEMA; + fs.writeFileSync(project.configPath, JSON.stringify(config, null, 2)); + }); + + afterAll(async () => { + if (project) await cleanupTestProject(project); + if (localDb || remoteDb) + await stopPostgresPair({local: localDb, remote: remoteDb}); + }); + + // ── Phase 1: Import existing database ───────────────────────────────── + + it("imports the custom schema database", async () => { + const result = await runCli( + ["db", "import", "--force", "--name", "custom_schema_baseline", "--url", remoteDb.url, "--schema", CUSTOM_SCHEMA], + {cwd: project.rootDir, timeout: 90_000}, + ); + if (result.exitCode !== 0) { + console.log("IMPORT STDOUT:", result.stdout); + console.log("IMPORT STDERR:", result.stderr); + } + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("import complete"); + }); + + it("creates schema files in PostKit structure", () => { + const tablesDir = path.join(project.schemaPath, "tables"); + expect(fs.existsSync(tablesDir)).toBe(true); + const files = fs.readdirSync(tablesDir).filter((f) => f.endsWith(".sql")); + expect(files.length).toBeGreaterThan(0); + }); + + it("creates baseline migration with SET search_path", () => { + const migrationsDir = path.join(project.dbDir, "migrations"); + const files = fs.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")); + expect(files.length).toBeGreaterThan(0); + const content = fs.readFileSync(path.join(migrationsDir, files[0]!), "utf-8"); + expect(content).toContain(`SET search_path TO "${CUSTOM_SCHEMA}"`); + }); + + it("committed.json tracks the baseline", async () => { + const committed = JSON.parse( + fs.readFileSync(path.join(project.dbDir, "committed.json"), "utf-8"), + ); + expect(committed.migrations).toHaveLength(1); + expect(committed.migrations[0].migrationFile.path).toMatch(/\.postkit\//); + // Path should be relative (not absolute) + expect(path.isAbsolute(committed.migrations[0].migrationFile.path)).toBe(false); + }); + + // ── Phase 2: Start a new session ────────────────────────────────────── + + it("starts a migration session from remote", async () => { + await startSession(project); + expect(fileExists(project, ".postkit/db/session.json")).toBe(true); + }); + + it("local DB has imported tables in custom schema after clone", async () => { + await verifyTablesInSchema(localDb.url, CUSTOM_SCHEMA, ["category", "product"], "local after start"); + }); + + // ── Phase 3: Add a new table to schema files and plan ───────────────── + + it("adds a new table (tag) to schema files", () => { + const tablesDir = path.join(project.schemaPath, "tables"); + const tagSql = ` +-- Table: tag +CREATE TABLE tag ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(50) NOT NULL UNIQUE, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); +`; + fs.writeFileSync(path.join(tablesDir, "03_tag.table.sql"), tagSql.trim() + "\n"); + }); + + it("generates a plan that includes the new table", async () => { + const output = await runPlan(project); + expect(output).toContain("tag"); + }); + + // ── Phase 4: Apply the migration locally ────────────────────────────── + + it("applies the plan to local database", async () => { + await runApply(project); + }); + + it("verifies all tables exist in local DB custom schema", async () => { + await verifyTablesInSchema( + localDb.url, CUSTOM_SCHEMA, + ["category", "product", "tag"], + "local after apply", + ); + }); + + it("verifies functions exist in local DB custom schema", async () => { + await verifyFunctionsInSchema( + localDb.url, CUSTOM_SCHEMA, + ["update_updated_at"], + "local after apply", + ); + }); + + it("verifies views exist in local DB custom schema", async () => { + await verifyViewsInSchema( + localDb.url, CUSTOM_SCHEMA, + ["product_list"], + "local after apply", + ); + }); + + it("verifies triggers exist in local DB custom schema", async () => { + await verifyTriggersInSchema( + localDb.url, CUSTOM_SCHEMA, + ["update_category_timestamp", "update_product_timestamp"], + "local after apply", + ); + }); + + it("verifies seed data is intact in local DB", async () => { + await verifySeedsInSchema(localDb.url, CUSTOM_SCHEMA); + }); + + // ── Phase 5: Commit ─────────────────────────────────────────────────── + + it("commits the session migration", async () => { + await runCommit(project, "add_tag_table"); + expect(fileExists(project, ".postkit/db/session.json")).toBe(false); + }); + + it("shows pending committed migrations", async () => { + const status = await getStatus(project); + expect(status.pendingCommittedMigrations).toBeGreaterThanOrEqual(1); + }); + + it("committed.json has relative paths for migration files", async () => { + const committed = JSON.parse( + fs.readFileSync(path.join(project.dbDir, "committed.json"), "utf-8"), + ); + for (const migration of committed.migrations) { + expect( + path.isAbsolute(migration.migrationFile.path), + `Migration path should be relative, got: ${migration.migrationFile.path}`, + ).toBe(false); + } + }); + + // ── Phase 6: Deploy to remote ───────────────────────────────────────── + + it("deploys committed migrations to remote database", async () => { + await runDeploy(project, 120_000); + }); + + it("verifies all tables exist in remote DB custom schema", async () => { + await verifyTablesInSchema( + remoteDb.url, CUSTOM_SCHEMA, + ["category", "product", "tag"], + "remote after deploy", + ); + }); + + it("verifies functions exist in remote DB custom schema", async () => { + await verifyFunctionsInSchema( + remoteDb.url, CUSTOM_SCHEMA, + ["update_updated_at"], + "remote after deploy", + ); + }); + + it("verifies views exist in remote DB custom schema", async () => { + await verifyViewsInSchema( + remoteDb.url, CUSTOM_SCHEMA, + ["product_list"], + "remote after deploy", + ); + }); + + it("verifies triggers exist in remote DB custom schema", async () => { + await verifyTriggersInSchema( + remoteDb.url, CUSTOM_SCHEMA, + ["update_category_timestamp", "update_product_timestamp"], + "remote after deploy", + ); + }); + + it("verifies seed data is intact in remote DB", async () => { + await verifySeedsInSchema(remoteDb.url, CUSTOM_SCHEMA); + }); + + // ── Phase 7: Verify schema_migrations is in postkit schema ──────────── + + it("schema_migrations is in postkit schema (not public) on remote", async () => { + const rows = await queryDatabase( + remoteDb.url, + "SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema = 'postkit' AND table_name = 'schema_migrations'", + ); + expect(rows.length).toBe(1); + }); + + it("baseline and new migration versions are tracked on remote", async () => { + const rows = await queryDatabase( + remoteDb.url, + "SELECT version FROM postkit.schema_migrations ORDER BY version", + ); + expect(rows.length).toBeGreaterThanOrEqual(2); + }); +}); From cb6f43c2a9273ca274ee6901b7f1030e766915e9 Mon Sep 17 00:00:00 2001 From: supunappri99 <supun@appritechnologies.com> Date: Thu, 30 Apr 2026 19:35:11 +0530 Subject: [PATCH 9/9] docs: add active development status badge and warning to README --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ca7495..3af476f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ > Open-source backend stack for modern applications -PostKit is a full-stack backend toolkit that combines battle-tested open-source technologies into a cohesive, developer-friendly platform. It provides everything you need to build, migrate, and manage production-ready backends. +![Active Development](https://img.shields.io/badge/Status-Active_Development-orange) + +> **This project is in active development.** Expect breaking changes, incomplete features, and frequent updates. ## The Stack