Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* `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);
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user