Files
HiveGrid-Wireframes/hivegrid-cloud/packages/cli/src/commands/create/entity.ts
T

180 lines
5.4 KiB
TypeScript

/**
* `toolctrl create entity <name>` — emit a Prisma model snippet plus a
* timestamped migration directory.
*
* The generator does NOT modify `prisma/schema.prisma` automatically —
* we drop the model snippet under a sibling `prisma/_generated/<name>.prisma`
* file plus a manual-include note in the README, because patching
* `schema.prisma` reliably from a CLI requires parsing prisma's own
* grammar (no off-the-shelf TS lib does that cleanly). The migration
* directory follows prisma's standard layout so `prisma migrate dev`
* picks it up.
*
* Refs: docs/improvement-plan/08-mobile-cli-storybook-sdk/cli-scaffolding.md
*/
import { defineCommand } from "citty";
import consola from "consola";
import path from "node:path";
import {
createScaffoldResult,
kebabCase,
pascalCase,
resolveRepoRoot,
snakeCase,
validateIdentifier,
writeScaffoldFile,
type ScaffoldResult,
} from "../_lib/scaffold.js";
export type GenerateEntityOptions = {
name: string;
repoRoot?: string;
dryRun?: boolean;
/** Override the migration timestamp prefix (test seam). */
timestamp?: string;
};
export async function generateEntity(options: GenerateEntityOptions): Promise<ScaffoldResult> {
const baseName = validateIdentifier(options.name, "entity name");
const dryRun = options.dryRun ?? false;
const repoRoot = resolveRepoRoot(options.repoRoot);
const pascal = pascalCase(baseName);
const snake = snakeCase(baseName);
const kebab = kebabCase(baseName);
const timestamp = options.timestamp ?? defaultTimestamp();
const migrationDir = `${timestamp}_create_entity_${snake}`;
const result = createScaffoldResult(dryRun);
await writeScaffoldFile({
targetPath: path.join(repoRoot, "prisma/_generated", `${kebab}.prisma`),
contents: renderPrismaModel({ pascal, snake }),
dryRun,
result,
});
await writeScaffoldFile({
targetPath: path.join(repoRoot, "prisma/migrations", migrationDir, "migration.sql"),
contents: renderMigrationSql({ pascal, snake }),
dryRun,
result,
});
await writeScaffoldFile({
targetPath: path.join(repoRoot, "prisma/migrations", migrationDir, "README.md"),
contents: renderMigrationReadme({ pascal, snake, kebab }),
dryRun,
result,
});
result.notes.push(
`Next steps:`,
` 1. Copy the model from prisma/_generated/${kebab}.prisma into prisma/schema.prisma`,
` 2. Run \`pnpm prisma migrate dev --name create_entity_${snake}\``,
` 3. Regenerate the prisma client: \`pnpm prisma generate\``,
);
return result;
}
function defaultTimestamp(): string {
// Prisma migration timestamp format: YYYYMMDDHHMMSS.
const now = new Date();
const pad = (n: number) => n.toString().padStart(2, "0");
return (
now.getUTCFullYear().toString() +
pad(now.getUTCMonth() + 1) +
pad(now.getUTCDate()) +
pad(now.getUTCHours()) +
pad(now.getUTCMinutes()) +
pad(now.getUTCSeconds())
);
}
function renderPrismaModel({ pascal, snake }: { pascal: string; snake: string }): string {
return `// Generated by toolctrl-cli — paste this block into prisma/schema.prisma.
model ${pascal} {
id String @id @default(cuid())
tenantId String
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([tenantId])
@@map("${snake}")
}
`;
}
function renderMigrationSql({ pascal, snake }: { pascal: string; snake: string }): string {
return `-- Generated by toolctrl-cli for entity ${pascal}.
-- Replace columns with actual schema before running \`prisma migrate dev\`.
CREATE TABLE "${snake}" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "${snake}_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "${snake}_tenantId_idx" ON "${snake}"("tenantId");
`;
}
function renderMigrationReadme({
pascal,
snake,
kebab,
}: {
pascal: string;
snake: string;
kebab: string;
}): string {
return `# Migration: create entity ${pascal}
Generated by \`toolctrl create entity ${kebab}\`.
## How to apply
1. Confirm the prisma model in \`prisma/_generated/${kebab}.prisma\` matches
your intended schema. Adjust columns / indexes / relations as needed.
2. Append the model to \`prisma/schema.prisma\`.
3. Run:
\`\`\`bash
pnpm prisma migrate dev --name create_entity_${snake}
\`\`\`
4. Prisma will rewrite the SQL in \`migration.sql\` to match the schema
diff — this file is only a placeholder so the directory exists.
`;
}
export const createEntityCommand = defineCommand({
meta: {
name: "entity",
description: "Scaffold a Prisma model + migration directory for a new entity.",
},
args: {
name: { type: "positional", required: true, description: "Entity name (will be PascalCased for the model)." },
"dry-run": {
type: "boolean",
required: false,
description: "Print the would-be writes without touching the filesystem.",
},
},
async run({ args }) {
const result = await generateEntity({ name: args.name, dryRun: args["dry-run"] === true });
consola.success(
`${result.dryRun ? "[dry-run] " : ""}Generated entity "${args.name}" — ${result.files.length} file(s).`,
);
for (const file of result.files) {
consola.info(` ${result.dryRun ? "would write" : "wrote"} ${file.path}`);
}
for (const note of result.notes) {
consola.info(note);
}
},
});