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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ apso dev # Start local dev server via Docker Compose

# Schema
apso migrate # Test schema migrations locally (PGlite sandbox)
apso migrate generate # Migration from a deployed .apsorc to this project's entities (TypeScript, PGlite)
apso migrate verify # Run a migration on the deployed schema, then check for drift
apso test smoke # Start the service on PGlite; call /health and every list endpoint
apso schema validate # Validate local .apsorc (includes lint)
apso schema lint # Lint .apsorc with @apso/schema-tools (--fix, --json)
apso schema diff # Diff local vs remote schema
Expand Down
14 changes: 14 additions & 0 deletions scripts/scaffold-smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,20 @@ case "$LANGUAGE" in
# setting rootDir to src, which trips TS6059 under a bare tsc run. rootDir
# only shapes emit layout and this is --noEmit, so widening it is safe.
npx tsc --noEmit --rootDir .
echo "==> Smoke testing the endpoints on PGlite"
"$CLI_ROOT/bin/run" test smoke
echo "==> Generating and verifying a migration for an added field"
cp .apsorc "$WORKDIR/deployed.apsorc"
node -e '
const fs = require("fs");
const rc = JSON.parse(fs.readFileSync(".apsorc", "utf8"));
rc.entities[0].fields.push({ name: "note", type: "text", nullable: true });
fs.writeFileSync(".apsorc", JSON.stringify(rc, null, 2) + "\n");
'
"$CLI_ROOT/bin/run" generate
"$CLI_ROOT/bin/run" migrate generate --baseline "$WORKDIR/deployed.apsorc"
MIGRATION="$(ls src/migrations/*-SchemaUpdate-migration.ts)"
"$CLI_ROOT/bin/run" migrate verify --baseline "$WORKDIR/deployed.apsorc" --migration "$MIGRATION"
;;
go)
echo "==> Resolving scaffolded project dependencies"
Expand Down
46 changes: 46 additions & 0 deletions src/commands/migrate/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as path from "path";
import { Flags } from "@oclif/core";
import BaseCommand from "../../lib/base-command";
import { cliBinPath, generateMigration, report } from "../../lib/verify";

export default class MigrateGenerate extends BaseCommand {
static description =
"Generate a TypeORM migration from the deployed schema to this project's entities, using a local PGlite database (TypeScript services)";

static examples = [`$ apso migrate generate --baseline deployed.apsorc`];

static flags = {
baseline: Flags.string({
description: "The deployed .apsorc to migrate from",
required: true,
}),
name: Flags.string({
description: "Migration name",
default: "SchemaUpdate",
}),
json: Flags.boolean({
description: "Print the result as JSON",
default: false,
}),
};

async run(): Promise<void> {
const { flags } = await this.parse(MigrateGenerate);
const result = await generateMigration(
process.cwd(),
path.resolve(flags.baseline),
flags.name,
cliBinPath(this.config.root)
);
const summary = result.needed
? `Created ${result.file}`
: "The database already matches; no migration needed.";
report(
(s) => this.log(s),
result,
flags.json,
result.ok ? summary : undefined
);
if (!result.ok) this.exit(1);
}
}
45 changes: 45 additions & 0 deletions src/commands/migrate/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as path from "path";
import { Flags } from "@oclif/core";
import BaseCommand from "../../lib/base-command";
import { cliBinPath, report, verifyMigration } from "../../lib/verify";

export default class MigrateVerify extends BaseCommand {
static description =
"Run a migration on a local PGlite copy of the deployed schema, then check the database matches this project's entities (TypeScript services)";

static examples = [
`$ apso migrate verify --baseline deployed.apsorc --migration src/migrations/1700000000000-SchemaUpdate-migration.ts`,
];

static flags = {
baseline: Flags.string({
description: "The deployed .apsorc the migration starts from",
required: true,
}),
migration: Flags.string({
description: "The migration file to run",
required: true,
}),
json: Flags.boolean({
description: "Print the result as JSON",
default: false,
}),
};

async run(): Promise<void> {
const { flags } = await this.parse(MigrateVerify);
const result = await verifyMigration(
process.cwd(),
path.resolve(flags.baseline),
flags.migration,
cliBinPath(this.config.root)
);
report(
(s) => this.log(s),
result,
flags.json,
result.ok ? "Migration verified." : undefined
);
if (!result.ok) this.exit(1);
}
}
33 changes: 33 additions & 0 deletions src/commands/test/smoke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Flags } from "@oclif/core";
import BaseCommand from "../../lib/base-command";
import { parseApsorc } from "../../lib/apsorc-parser";
import { report, smokeTest } from "../../lib/verify";

export default class TestSmoke extends BaseCommand {
static description =
"Start the service on a local PGlite database and call /health and every entity's list endpoint (TypeScript services)";

static examples = [`$ apso test smoke`];

static flags = {
json: Flags.boolean({
description: "Print the result as JSON",
default: false,
}),
};

async run(): Promise<void> {
const { flags } = await this.parse(TestSmoke);
const { entities, auth } = parseApsorc();
// With auth configured every entity route needs a session, so only /health is checked.
const names = auth ? [] : entities.map((e) => e.name);
const result = await smokeTest(process.cwd(), names);
report(
(s) => this.log(s),
result,
flags.json,
result.ok ? "Smoke test passed." : undefined
);
if (!result.ok) this.exit(1);
}
}
Loading
Loading