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
16 changes: 16 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,19 @@ jobs:
contents: write
id-token: write
packages: read

registry-database:
name: Public database command smoke test
needs: publish
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: "lts/*"
cache: npm
- run: npm ci
- run: npm run test:registry-database
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"typecheck": "tsc -p tsconfig.json --noEmit",
"test:changelog": "vp test run -c vitest.config.ts tests/changelog.test.ts",
"test:peer-floor": "vp test run -c vitest.integration.config.ts tests/integration/peer-floor.test.ts",
"test:registry-database": "vp test run -c vitest.integration.config.ts tests/integration/registry-database.test.ts",
"test:publint": "publint",
"pack:check": "npm pack --ignore-scripts --dry-run --json",
"test:templates": "vp test run -c vitest.integration.config.ts tests/integration/packed-templates.test.ts",
Expand Down
2 changes: 1 addition & 1 deletion src/bin/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export async function loadOrmTooling(cwd: string): Promise<OrmTooling> {
entry = path.resolve(path.dirname(manifestPath), relative);
} catch (error) {
throw new Error(
"This project does not have @askrjs/orm installed. Install it before running `askr database`.",
"This project does not have @askrjs/orm installed. Run `npm install @askrjs/orm` before `askr database`.",
{ cause: error },
);
}
Expand Down
6 changes: 4 additions & 2 deletions src/generate/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -862,8 +862,10 @@ export async function writeGenerated(
await rename(stage, output);
if (moved) await rm(backup, { recursive: true, force: true });
} catch (error) {
await rm(output, { recursive: true, force: true });
if (moved) await rename(backup, output);
if (moved) {
await rm(output, { recursive: true, force: true });
await rename(backup, output);
}
throw error;
} finally {
await rm(stage, { recursive: true, force: true });
Expand Down
16 changes: 16 additions & 0 deletions tests/database.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import {
loadOrmTooling,
Expand All @@ -22,6 +25,19 @@ function io() {
type Loader = typeof loadOrmTooling;

describe("database command routing", () => {
it("should name the exact public ORM install command when tooling is missing", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "askr-cli-missing-orm-"));
try {
await fs.writeFile(
path.join(root, "package.json"),
`${JSON.stringify({ name: "missing-orm", private: true, type: "module" })}\n`,
);
await expect(loadOrmTooling(root)).rejects.toThrow("npm install @askrjs/orm");
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});

it("should forward all semantics to the project-installed ORM tooling", async () => {
const output = io();
const runDatabaseCli = vi.fn(async () => 0);
Expand Down
18 changes: 18 additions & 0 deletions tests/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,24 @@ describe("askr generate", () => {
);
expect(await readFile(output, "utf8")).toBe("keep");
});
it("should preserve generated output when its backup rename fails", async () => {
const root = await mkdtemp(join(tmpdir(), "askr-stale-backup-"));
const output = join(root, "generated");
const backup = `${output}.backup-${process.pid}`;
await mkdir(output);
await writeFile(join(output, ".askr-generated.json"), "original manifest\n");
await writeFile(join(output, "schemas.ts"), "original schema\n");
await mkdir(backup);
await writeFile(join(backup, "stale.txt"), "stale backup\n");

await expect(writeGenerated(output, generateFiles(document), false)).rejects.toThrow();

expect(await readFile(join(output, ".askr-generated.json"), "utf8")).toBe(
"original manifest\n",
);
expect(await readFile(join(output, "schemas.ts"), "utf8")).toBe("original schema\n");
expect(await readFile(join(backup, "stale.txt"), "utf8")).toBe("stale backup\n");
});
it("should detect stale and extra files without writes given check mode when checking", async () => {
const root = await mkdtemp(join(tmpdir(), "askr-check-"));
const output = join(root, "client");
Expand Down
73 changes: 73 additions & 0 deletions tests/integration/registry-database.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { execFile } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { promisify } from "node:util";
import { expect, test } from "vitest";
import manifest from "../../package.json" with { type: "json" };

const execFileAsync = promisify(execFile);

async function run(command: string, args: string[], cwd: string): Promise<string> {
const { stdout, stderr } = await execFileAsync(command, args, {
cwd,
env: { ...process.env, NO_COLOR: "1" },
maxBuffer: 20 * 1024 * 1024,
});
if (stderr) process.stderr.write(stderr);
return stdout;
}

async function installRegistryArtifacts(root: string, versions: readonly string[]): Promise<void> {
let lastError: unknown;
for (let attempt = 1; attempt <= 30; attempt += 1) {
try {
await run(
"npm",
["install", "--ignore-scripts", "--no-audit", "--no-fund", ...versions],
root,
);
return;
} catch (error) {
lastError = error;
if (attempt < 30) await new Promise((resolve) => setTimeout(resolve, 10_000));
}
}
throw lastError;
}

test("should validate a database using only public registry artifacts", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "askr-cli-registry-database-"));
const cliVersion = process.env.ASKR_CLI_REGISTRY_VERSION ?? manifest.version;
const ormVersion = process.env.ASKR_ORM_REGISTRY_VERSION ?? "0.0.1";
try {
await fs.writeFile(
path.join(root, "package.json"),
`${JSON.stringify({ name: "askr-registry-database", private: true, type: "module" }, null, 2)}\n`,
);
await installRegistryArtifacts(root, [
`@askrjs/cli@${cliVersion}`,
`@askrjs/orm@${ormVersion}`,
]);
await fs.mkdir(path.join(root, "database"));
await fs.writeFile(
path.join(root, "database", "index.ts"),
`import { defineDatabase } from "@askrjs/orm";
import { sqlite } from "@askrjs/orm/sqlite";

export default defineDatabase({ driver: sqlite({ filename: "./database.sqlite" }), tables: {} });
`,
);

const askr = path.join(root, "node_modules", ".bin", "askr");
await run(askr, ["database", "generate"], root);
const output = await run(askr, ["database", "validate"], root);

expect(output).toContain("valid");
expect(await fs.readFile(path.join(root, "database", "generated.ts"), "utf8")).toContain(
"GeneratedDatabaseArtifact",
);
} finally {
await fs.rm(root, { recursive: true, force: true });
}
}, 360_000);