core-nails 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/generate/index.d.ts +1 -0
- package/dist/cli/generate/index.js +23 -0
- package/dist/cli/generate/model.d.ts +1 -0
- package/dist/cli/generate/model.js +61 -0
- package/dist/cli/generate/scaffold.d.ts +1 -0
- package/dist/cli/generate/scaffold.js +105 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +18 -0
- package/dist/src/cli/generate/index.d.ts +1 -0
- package/dist/src/cli/generate/index.js +23 -0
- package/dist/src/cli/generate/model.d.ts +1 -0
- package/dist/src/cli/generate/model.js +61 -0
- package/dist/src/cli/generate/scaffold.d.ts +1 -0
- package/dist/src/cli/generate/scaffold.js +105 -0
- package/dist/src/cli/index.d.ts +3 -0
- package/dist/src/cli/index.js +18 -0
- package/package.json +18 -0
- package/src/cli/generate/index.ts +27 -0
- package/src/cli/generate/model.ts +73 -0
- package/src/cli/generate/scaffold.ts +134 -0
- package/src/cli/index.ts +21 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runGenerate(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export async function runGenerate(args) {
|
|
2
|
+
const [type, name] = args;
|
|
3
|
+
if (!type || !name) {
|
|
4
|
+
console.error("Usage: nails generate <type> <name>");
|
|
5
|
+
process.exit(1);
|
|
6
|
+
}
|
|
7
|
+
switch (type) {
|
|
8
|
+
case "model": {
|
|
9
|
+
const { generateModel } = await import("./model.js");
|
|
10
|
+
await generateModel(name);
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
case "scaffold": {
|
|
14
|
+
const { generateScaffold } = await import("./scaffold.js");
|
|
15
|
+
await generateScaffold(name);
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
default: {
|
|
19
|
+
console.error(`Unknown generator: ${type}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateModel(modelName: string): Promise<void>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import pluralize from "pluralize";
|
|
4
|
+
export async function generateModel(modelName) {
|
|
5
|
+
if (!modelName) {
|
|
6
|
+
console.error("Usage: yarn generate model <ModelName>");
|
|
7
|
+
console.error("Example: yarn generate model UserActivity");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
// Convert model name to match filename conventions
|
|
11
|
+
const dirName = path.join("app", "(model)", modelName);
|
|
12
|
+
const filePath = path.join(dirName, "index.ts");
|
|
13
|
+
// Pluralize collection name
|
|
14
|
+
const collectionName = pluralize(modelName.charAt(0).toLowerCase() + modelName.slice(1));
|
|
15
|
+
// Boilerplate content for model
|
|
16
|
+
const modelContent = `import CreateService from "../concerns/CreateService";
|
|
17
|
+
import { ${modelName}Schema } from "@schema";
|
|
18
|
+
|
|
19
|
+
const ${modelName} = CreateService({
|
|
20
|
+
collection: "${collectionName}",
|
|
21
|
+
schema: ${modelName}Schema,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export default ${modelName};
|
|
25
|
+
`;
|
|
26
|
+
// Create model directory and write index.ts
|
|
27
|
+
fs.mkdirSync(dirName, { recursive: true });
|
|
28
|
+
fs.writeFileSync(filePath, modelContent);
|
|
29
|
+
console.log(`create ${path.relative(process.cwd(), filePath)}`);
|
|
30
|
+
// --- Append schema stub to db/schema.ts ---
|
|
31
|
+
const dbDir = path.join("db");
|
|
32
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
33
|
+
const schemaFilePath = path.join(dbDir, "schema.ts");
|
|
34
|
+
const schemaBaseContent = `import FirebaseAdmin from "@lib/FirebaseAdmin";
|
|
35
|
+
import { z } from "zod";
|
|
36
|
+
|
|
37
|
+
// Helper Function
|
|
38
|
+
const FirebaseTimestamp = z.union([
|
|
39
|
+
z.instanceof(FirebaseAdmin.firestore.Timestamp),
|
|
40
|
+
z.date(),
|
|
41
|
+
z.custom((val) => val === FirebaseAdmin.firestore.FieldValue.serverTimestamp(), { message: "Expected serverTimestamp()" }),
|
|
42
|
+
]);
|
|
43
|
+
`;
|
|
44
|
+
// Ensure schema file exists
|
|
45
|
+
if (!fs.existsSync(schemaFilePath)) {
|
|
46
|
+
fs.writeFileSync(schemaFilePath, schemaBaseContent);
|
|
47
|
+
console.log(`create ${path.relative(process.cwd(), schemaFilePath)}`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log(`append ${path.relative(process.cwd(), schemaFilePath)}`);
|
|
51
|
+
}
|
|
52
|
+
const schemaStub = `
|
|
53
|
+
// ${modelName} Schema
|
|
54
|
+
export const ${modelName}Schema = z.object({
|
|
55
|
+
createdAt: FirebaseTimestamp.optional(),
|
|
56
|
+
updatedAt: FirebaseTimestamp.optional(),
|
|
57
|
+
});
|
|
58
|
+
export type ${modelName}Type = z.infer<typeof ${modelName}Schema>;
|
|
59
|
+
`;
|
|
60
|
+
fs.appendFileSync(schemaFilePath, schemaStub);
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateScaffold(scaffoldName: string): Promise<void>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import pluralize from "pluralize";
|
|
4
|
+
import { generateModel } from "./model.js";
|
|
5
|
+
export async function generateScaffold(scaffoldName) {
|
|
6
|
+
if (!scaffoldName) {
|
|
7
|
+
console.error("Usage: yarn generate scaffold <ScaffoldName>");
|
|
8
|
+
console.error("Example: yarn generate scaffold UserActivity");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
function toSnakeCase(name) {
|
|
12
|
+
const snake = name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
13
|
+
return pluralize(snake);
|
|
14
|
+
}
|
|
15
|
+
const camelName = scaffoldName.charAt(0).toUpperCase() + scaffoldName.slice(1);
|
|
16
|
+
const snakePluralName = toSnakeCase(scaffoldName);
|
|
17
|
+
// --- 1️⃣ Generate Model (if missing) ---
|
|
18
|
+
const modelDir = path.join("app", "(model)", camelName);
|
|
19
|
+
if (!fs.existsSync(modelDir)) {
|
|
20
|
+
console.log(`Model not found for ${camelName}, generating...`);
|
|
21
|
+
await generateModel(scaffoldName);
|
|
22
|
+
}
|
|
23
|
+
// --- 2️⃣ Paths ---
|
|
24
|
+
const controllerDir = path.join("app", "(controller)", camelName);
|
|
25
|
+
const apiDir = path.join("app", "api", snakePluralName);
|
|
26
|
+
const apiIdDir = path.join(apiDir, "[id]");
|
|
27
|
+
// --- 3️⃣ Action files ---
|
|
28
|
+
const actions = ["index", "show", "create", "update", "destroy"];
|
|
29
|
+
fs.mkdirSync(controllerDir, { recursive: true });
|
|
30
|
+
const actionsDir = path.join(controllerDir, "actions");
|
|
31
|
+
fs.mkdirSync(actionsDir, { recursive: true });
|
|
32
|
+
actions.forEach((action) => {
|
|
33
|
+
const filePath = path.join(actionsDir, `${action}_action.ts`);
|
|
34
|
+
const content = `export default async function ${action}_action(req: Request${["index", "create"].includes(action) ? "" : ", id: string"}) {
|
|
35
|
+
return new Response(JSON.stringify({ message: "${action} ${camelName}" }));
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
fs.writeFileSync(filePath, content);
|
|
39
|
+
console.log(`create ${path.relative(process.cwd(), filePath)}`);
|
|
40
|
+
});
|
|
41
|
+
// --- 4️⃣ controller index.ts ---
|
|
42
|
+
const indexContent = actions
|
|
43
|
+
.map((action) => `import ${action}_action from "./actions/${action}_action";`)
|
|
44
|
+
.join("\n") +
|
|
45
|
+
`
|
|
46
|
+
|
|
47
|
+
export default {
|
|
48
|
+
${actions.map((a) => ` ${a}_action,`).join("\n")}
|
|
49
|
+
};
|
|
50
|
+
`;
|
|
51
|
+
const controllerIndexPath = path.join(controllerDir, "index.ts");
|
|
52
|
+
fs.writeFileSync(controllerIndexPath, indexContent);
|
|
53
|
+
console.log(`create ${path.relative(process.cwd(), controllerIndexPath)}`);
|
|
54
|
+
// --- 5️⃣ API route.ts ---
|
|
55
|
+
fs.mkdirSync(apiDir, { recursive: true });
|
|
56
|
+
const apiRoutePath = path.join(apiDir, "route.ts");
|
|
57
|
+
const apiRouteContent = `/**
|
|
58
|
+
* Standard RESTful Routes
|
|
59
|
+
*
|
|
60
|
+
* | HTTP Verb | Controller#Action | Purpose | Path |
|
|
61
|
+
* |-----------|-------------------|------------------|--------------------------------------------|
|
|
62
|
+
* | GET | index | List all | /${snakePluralName}
|
|
63
|
+
* | GET | show | Get one | /${snakePluralName}/:id
|
|
64
|
+
* | POST | create | Create | /${snakePluralName}
|
|
65
|
+
* | PATCH | update | Update (partial) | /${snakePluralName}/:id
|
|
66
|
+
* | PUT | update | Update (full) | /${snakePluralName}/:id
|
|
67
|
+
* | DELETE | destroy | Delete | /${snakePluralName}/:id
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
import ${camelName}Controller from "@controller/${camelName}";
|
|
71
|
+
|
|
72
|
+
export async function GET(req: Request) {
|
|
73
|
+
return ${camelName}Controller.index_action(req);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function POST(req: Request) {
|
|
77
|
+
return ${camelName}Controller.create_action(req);
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
fs.writeFileSync(apiRoutePath, apiRouteContent);
|
|
81
|
+
console.log(`create ${path.relative(process.cwd(), apiRoutePath)}`);
|
|
82
|
+
// --- 6️⃣ API [id]/route.ts ---
|
|
83
|
+
fs.mkdirSync(apiIdDir, { recursive: true });
|
|
84
|
+
const apiIdRoutePath = path.join(apiIdDir, "route.ts");
|
|
85
|
+
const apiIdRouteContent = `import ${camelName}Controller from "@controller/${camelName}";
|
|
86
|
+
|
|
87
|
+
export async function GET(req: Request, { params }: { params: { id: string } }) {
|
|
88
|
+
return ${camelName}Controller.show_action(req, params.id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function PATCH(req: Request, { params }: { params: { id: string } }) {
|
|
92
|
+
return ${camelName}Controller.update_action(req, params.id);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function PUT(req: Request, { params }: { params: { id: string } }) {
|
|
96
|
+
return ${camelName}Controller.update_action(req, params.id);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function DELETE(req: Request, { params }: { params: { id: string } }) {
|
|
100
|
+
return ${camelName}Controller.destroy_action(req, params.id);
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
fs.writeFileSync(apiIdRoutePath, apiIdRouteContent);
|
|
104
|
+
console.log(`create ${path.relative(process.cwd(), apiIdRoutePath)}`);
|
|
105
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const [, , command, ...args] = process.argv;
|
|
4
|
+
async function run() {
|
|
5
|
+
switch (command) {
|
|
6
|
+
case "generate": {
|
|
7
|
+
const { runGenerate } = await import("./generate/index.js");
|
|
8
|
+
await runGenerate(args);
|
|
9
|
+
break;
|
|
10
|
+
}
|
|
11
|
+
default: {
|
|
12
|
+
console.log(`Unknown command: ${command}`);
|
|
13
|
+
console.log("Available commands: generate");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
run();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runGenerate(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export async function runGenerate(args) {
|
|
2
|
+
const [type, name] = args;
|
|
3
|
+
if (!type || !name) {
|
|
4
|
+
console.error("Usage: nails generate <type> <name>");
|
|
5
|
+
process.exit(1);
|
|
6
|
+
}
|
|
7
|
+
switch (type) {
|
|
8
|
+
case "model": {
|
|
9
|
+
const { generateModel } = await import("./model.js");
|
|
10
|
+
await generateModel(name);
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
case "scaffold": {
|
|
14
|
+
const { generateScaffold } = await import("./scaffold.js");
|
|
15
|
+
await generateScaffold(name);
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
default: {
|
|
19
|
+
console.error(`Unknown generator: ${type}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateModel(modelName: string): Promise<void>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import pluralize from "pluralize";
|
|
4
|
+
export async function generateModel(modelName) {
|
|
5
|
+
if (!modelName) {
|
|
6
|
+
console.error("Usage: yarn generate model <ModelName>");
|
|
7
|
+
console.error("Example: yarn generate model UserActivity");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
// Convert model name to match filename conventions
|
|
11
|
+
const dirName = path.join("app", "(model)", modelName);
|
|
12
|
+
const filePath = path.join(dirName, "index.ts");
|
|
13
|
+
// Pluralize collection name
|
|
14
|
+
const collectionName = pluralize(modelName.charAt(0).toLowerCase() + modelName.slice(1));
|
|
15
|
+
// Boilerplate content for model
|
|
16
|
+
const modelContent = `import CreateService from "../concerns/CreateService";
|
|
17
|
+
import { ${modelName}Schema } from "@schema";
|
|
18
|
+
|
|
19
|
+
const ${modelName} = CreateService({
|
|
20
|
+
collection: "${collectionName}",
|
|
21
|
+
schema: ${modelName}Schema,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export default ${modelName};
|
|
25
|
+
`;
|
|
26
|
+
// Create model directory and write index.ts
|
|
27
|
+
fs.mkdirSync(dirName, { recursive: true });
|
|
28
|
+
fs.writeFileSync(filePath, modelContent);
|
|
29
|
+
console.log(`create ${path.relative(process.cwd(), filePath)}`);
|
|
30
|
+
// --- Append schema stub to db/schema.ts ---
|
|
31
|
+
const dbDir = path.join("db");
|
|
32
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
33
|
+
const schemaFilePath = path.join(dbDir, "schema.ts");
|
|
34
|
+
const schemaBaseContent = `import FirebaseAdmin from "@lib/FirebaseAdmin";
|
|
35
|
+
import { z } from "zod";
|
|
36
|
+
|
|
37
|
+
// Helper Function
|
|
38
|
+
const FirebaseTimestamp = z.union([
|
|
39
|
+
z.instanceof(FirebaseAdmin.firestore.Timestamp),
|
|
40
|
+
z.date(),
|
|
41
|
+
z.custom((val) => val === FirebaseAdmin.firestore.FieldValue.serverTimestamp(), { message: "Expected serverTimestamp()" }),
|
|
42
|
+
]);
|
|
43
|
+
`;
|
|
44
|
+
// Ensure schema file exists
|
|
45
|
+
if (!fs.existsSync(schemaFilePath)) {
|
|
46
|
+
fs.writeFileSync(schemaFilePath, schemaBaseContent);
|
|
47
|
+
console.log(`create ${path.relative(process.cwd(), schemaFilePath)}`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log(`append ${path.relative(process.cwd(), schemaFilePath)}`);
|
|
51
|
+
}
|
|
52
|
+
const schemaStub = `
|
|
53
|
+
// ${modelName} Schema
|
|
54
|
+
export const ${modelName}Schema = z.object({
|
|
55
|
+
createdAt: FirebaseTimestamp.optional(),
|
|
56
|
+
updatedAt: FirebaseTimestamp.optional(),
|
|
57
|
+
});
|
|
58
|
+
export type ${modelName}Type = z.infer<typeof ${modelName}Schema>;
|
|
59
|
+
`;
|
|
60
|
+
fs.appendFileSync(schemaFilePath, schemaStub);
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateScaffold(scaffoldName: string): Promise<void>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import pluralize from "pluralize";
|
|
4
|
+
import { generateModel } from "./model.js";
|
|
5
|
+
export async function generateScaffold(scaffoldName) {
|
|
6
|
+
if (!scaffoldName) {
|
|
7
|
+
console.error("Usage: yarn generate scaffold <ScaffoldName>");
|
|
8
|
+
console.error("Example: yarn generate scaffold UserActivity");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
function toSnakeCase(name) {
|
|
12
|
+
const snake = name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
13
|
+
return pluralize(snake);
|
|
14
|
+
}
|
|
15
|
+
const camelName = scaffoldName.charAt(0).toUpperCase() + scaffoldName.slice(1);
|
|
16
|
+
const snakePluralName = toSnakeCase(scaffoldName);
|
|
17
|
+
// --- 1️⃣ Generate Model (if missing) ---
|
|
18
|
+
const modelDir = path.join("app", "(model)", camelName);
|
|
19
|
+
if (!fs.existsSync(modelDir)) {
|
|
20
|
+
console.log(`Model not found for ${camelName}, generating...`);
|
|
21
|
+
await generateModel(scaffoldName);
|
|
22
|
+
}
|
|
23
|
+
// --- 2️⃣ Paths ---
|
|
24
|
+
const controllerDir = path.join("app", "(controller)", camelName);
|
|
25
|
+
const apiDir = path.join("app", "api", snakePluralName);
|
|
26
|
+
const apiIdDir = path.join(apiDir, "[id]");
|
|
27
|
+
// --- 3️⃣ Action files ---
|
|
28
|
+
const actions = ["index", "show", "create", "update", "destroy"];
|
|
29
|
+
fs.mkdirSync(controllerDir, { recursive: true });
|
|
30
|
+
const actionsDir = path.join(controllerDir, "actions");
|
|
31
|
+
fs.mkdirSync(actionsDir, { recursive: true });
|
|
32
|
+
actions.forEach((action) => {
|
|
33
|
+
const filePath = path.join(actionsDir, `${action}_action.ts`);
|
|
34
|
+
const content = `export default async function ${action}_action(req: Request${["index", "create"].includes(action) ? "" : ", id: string"}) {
|
|
35
|
+
return new Response(JSON.stringify({ message: "${action} ${camelName}" }));
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
fs.writeFileSync(filePath, content);
|
|
39
|
+
console.log(`create ${path.relative(process.cwd(), filePath)}`);
|
|
40
|
+
});
|
|
41
|
+
// --- 4️⃣ controller index.ts ---
|
|
42
|
+
const indexContent = actions
|
|
43
|
+
.map((action) => `import ${action}_action from "./actions/${action}_action";`)
|
|
44
|
+
.join("\n") +
|
|
45
|
+
`
|
|
46
|
+
|
|
47
|
+
export default {
|
|
48
|
+
${actions.map((a) => ` ${a}_action,`).join("\n")}
|
|
49
|
+
};
|
|
50
|
+
`;
|
|
51
|
+
const controllerIndexPath = path.join(controllerDir, "index.ts");
|
|
52
|
+
fs.writeFileSync(controllerIndexPath, indexContent);
|
|
53
|
+
console.log(`create ${path.relative(process.cwd(), controllerIndexPath)}`);
|
|
54
|
+
// --- 5️⃣ API route.ts ---
|
|
55
|
+
fs.mkdirSync(apiDir, { recursive: true });
|
|
56
|
+
const apiRoutePath = path.join(apiDir, "route.ts");
|
|
57
|
+
const apiRouteContent = `/**
|
|
58
|
+
* Standard RESTful Routes
|
|
59
|
+
*
|
|
60
|
+
* | HTTP Verb | Controller#Action | Purpose | Path |
|
|
61
|
+
* |-----------|-------------------|------------------|--------------------------------------------|
|
|
62
|
+
* | GET | index | List all | /${snakePluralName}
|
|
63
|
+
* | GET | show | Get one | /${snakePluralName}/:id
|
|
64
|
+
* | POST | create | Create | /${snakePluralName}
|
|
65
|
+
* | PATCH | update | Update (partial) | /${snakePluralName}/:id
|
|
66
|
+
* | PUT | update | Update (full) | /${snakePluralName}/:id
|
|
67
|
+
* | DELETE | destroy | Delete | /${snakePluralName}/:id
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
import ${camelName}Controller from "@controller/${camelName}";
|
|
71
|
+
|
|
72
|
+
export async function GET(req: Request) {
|
|
73
|
+
return ${camelName}Controller.index_action(req);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function POST(req: Request) {
|
|
77
|
+
return ${camelName}Controller.create_action(req);
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
fs.writeFileSync(apiRoutePath, apiRouteContent);
|
|
81
|
+
console.log(`create ${path.relative(process.cwd(), apiRoutePath)}`);
|
|
82
|
+
// --- 6️⃣ API [id]/route.ts ---
|
|
83
|
+
fs.mkdirSync(apiIdDir, { recursive: true });
|
|
84
|
+
const apiIdRoutePath = path.join(apiIdDir, "route.ts");
|
|
85
|
+
const apiIdRouteContent = `import ${camelName}Controller from "@controller/${camelName}";
|
|
86
|
+
|
|
87
|
+
export async function GET(req: Request, { params }: { params: { id: string } }) {
|
|
88
|
+
return ${camelName}Controller.show_action(req, params.id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function PATCH(req: Request, { params }: { params: { id: string } }) {
|
|
92
|
+
return ${camelName}Controller.update_action(req, params.id);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function PUT(req: Request, { params }: { params: { id: string } }) {
|
|
96
|
+
return ${camelName}Controller.update_action(req, params.id);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function DELETE(req: Request, { params }: { params: { id: string } }) {
|
|
100
|
+
return ${camelName}Controller.destroy_action(req, params.id);
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
fs.writeFileSync(apiIdRoutePath, apiIdRouteContent);
|
|
104
|
+
console.log(`create ${path.relative(process.cwd(), apiIdRoutePath)}`);
|
|
105
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const [, , command, ...args] = process.argv;
|
|
4
|
+
async function run() {
|
|
5
|
+
switch (command) {
|
|
6
|
+
case "generate": {
|
|
7
|
+
const { runGenerate } = await import("./generate/index.js");
|
|
8
|
+
await runGenerate(args);
|
|
9
|
+
break;
|
|
10
|
+
}
|
|
11
|
+
default: {
|
|
12
|
+
console.log(`Unknown command: ${command}`);
|
|
13
|
+
console.log("Available commands: generate");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
run();
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "core-nails",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"build": "tsc"
|
|
6
|
+
},
|
|
7
|
+
"bin": {
|
|
8
|
+
"nails": "dist/cli/index.js"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/node": "^25.5.0",
|
|
12
|
+
"@types/pluralize": "^0.0.33",
|
|
13
|
+
"typescript": "^6.0.2"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"pluralize": "^8.0.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export async function runGenerate(args: string[]) {
|
|
2
|
+
const [type, name] = args;
|
|
3
|
+
|
|
4
|
+
if (!type || !name) {
|
|
5
|
+
console.error("Usage: nails generate <type> <name>");
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
switch (type) {
|
|
10
|
+
case "model": {
|
|
11
|
+
const { generateModel } = await import("./model.js");
|
|
12
|
+
await generateModel(name);
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
case "scaffold": {
|
|
17
|
+
const { generateScaffold } = await import("./scaffold.js");
|
|
18
|
+
await generateScaffold(name);
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
default: {
|
|
23
|
+
console.error(`Unknown generator: ${type}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import pluralize from "pluralize";
|
|
4
|
+
|
|
5
|
+
export async function generateModel(modelName: string) {
|
|
6
|
+
if (!modelName) {
|
|
7
|
+
console.error("Usage: yarn generate model <ModelName>");
|
|
8
|
+
console.error("Example: yarn generate model UserActivity");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Convert model name to match filename conventions
|
|
13
|
+
const dirName = path.join("app", "(model)", modelName);
|
|
14
|
+
const filePath = path.join(dirName, "index.ts");
|
|
15
|
+
|
|
16
|
+
// Pluralize collection name
|
|
17
|
+
const collectionName = pluralize(
|
|
18
|
+
modelName.charAt(0).toLowerCase() + modelName.slice(1)
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// Boilerplate content for model
|
|
22
|
+
const modelContent = `import CreateService from "../concerns/CreateService";
|
|
23
|
+
import { ${modelName}Schema } from "@schema";
|
|
24
|
+
|
|
25
|
+
const ${modelName} = CreateService({
|
|
26
|
+
collection: "${collectionName}",
|
|
27
|
+
schema: ${modelName}Schema,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export default ${modelName};
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
// Create model directory and write index.ts
|
|
34
|
+
fs.mkdirSync(dirName, { recursive: true });
|
|
35
|
+
fs.writeFileSync(filePath, modelContent);
|
|
36
|
+
|
|
37
|
+
console.log(`create ${path.relative(process.cwd(), filePath)}`);
|
|
38
|
+
|
|
39
|
+
// --- Append schema stub to db/schema.ts ---
|
|
40
|
+
const dbDir = path.join("db");
|
|
41
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
42
|
+
const schemaFilePath = path.join(dbDir, "schema.ts");
|
|
43
|
+
|
|
44
|
+
const schemaBaseContent = `import FirebaseAdmin from "@lib/FirebaseAdmin";
|
|
45
|
+
import { z } from "zod";
|
|
46
|
+
|
|
47
|
+
// Helper Function
|
|
48
|
+
const FirebaseTimestamp = z.union([
|
|
49
|
+
z.instanceof(FirebaseAdmin.firestore.Timestamp),
|
|
50
|
+
z.date(),
|
|
51
|
+
z.custom((val) => val === FirebaseAdmin.firestore.FieldValue.serverTimestamp(), { message: "Expected serverTimestamp()" }),
|
|
52
|
+
]);
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
// Ensure schema file exists
|
|
56
|
+
if (!fs.existsSync(schemaFilePath)) {
|
|
57
|
+
fs.writeFileSync(schemaFilePath, schemaBaseContent);
|
|
58
|
+
console.log(`create ${path.relative(process.cwd(), schemaFilePath)}`);
|
|
59
|
+
} else {
|
|
60
|
+
console.log(`append ${path.relative(process.cwd(), schemaFilePath)}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const schemaStub = `
|
|
64
|
+
// ${modelName} Schema
|
|
65
|
+
export const ${modelName}Schema = z.object({
|
|
66
|
+
createdAt: FirebaseTimestamp.optional(),
|
|
67
|
+
updatedAt: FirebaseTimestamp.optional(),
|
|
68
|
+
});
|
|
69
|
+
export type ${modelName}Type = z.infer<typeof ${modelName}Schema>;
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
fs.appendFileSync(schemaFilePath, schemaStub);
|
|
73
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import pluralize from "pluralize";
|
|
4
|
+
import { generateModel } from "./model.js";
|
|
5
|
+
|
|
6
|
+
export async function generateScaffold(scaffoldName: string) {
|
|
7
|
+
if (!scaffoldName) {
|
|
8
|
+
console.error("Usage: yarn generate scaffold <ScaffoldName>");
|
|
9
|
+
console.error("Example: yarn generate scaffold UserActivity");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toSnakeCase(name: string) {
|
|
14
|
+
const snake = name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
15
|
+
return pluralize(snake);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const camelName =
|
|
19
|
+
scaffoldName.charAt(0).toUpperCase() + scaffoldName.slice(1);
|
|
20
|
+
|
|
21
|
+
const snakePluralName = toSnakeCase(scaffoldName);
|
|
22
|
+
|
|
23
|
+
// --- 1️⃣ Generate Model (if missing) ---
|
|
24
|
+
const modelDir = path.join("app", "(model)", camelName);
|
|
25
|
+
|
|
26
|
+
if (!fs.existsSync(modelDir)) {
|
|
27
|
+
console.log(`Model not found for ${camelName}, generating...`);
|
|
28
|
+
await generateModel(scaffoldName);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// --- 2️⃣ Paths ---
|
|
32
|
+
const controllerDir = path.join("app", "(controller)", camelName);
|
|
33
|
+
const apiDir = path.join("app", "api", snakePluralName);
|
|
34
|
+
const apiIdDir = path.join(apiDir, "[id]");
|
|
35
|
+
|
|
36
|
+
// --- 3️⃣ Action files ---
|
|
37
|
+
const actions = ["index", "show", "create", "update", "destroy"];
|
|
38
|
+
|
|
39
|
+
fs.mkdirSync(controllerDir, { recursive: true });
|
|
40
|
+
const actionsDir = path.join(controllerDir, "actions");
|
|
41
|
+
fs.mkdirSync(actionsDir, { recursive: true });
|
|
42
|
+
|
|
43
|
+
actions.forEach((action) => {
|
|
44
|
+
const filePath = path.join(actionsDir, `${action}_action.ts`);
|
|
45
|
+
|
|
46
|
+
const content = `export default async function ${action}_action(req: Request${["index", "create"].includes(action) ? "" : ", id: string"}) {
|
|
47
|
+
return new Response(JSON.stringify({ message: "${action} ${camelName}" }));
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
fs.writeFileSync(filePath, content);
|
|
52
|
+
console.log(`create ${path.relative(process.cwd(), filePath)}`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// --- 4️⃣ controller index.ts ---
|
|
56
|
+
const indexContent =
|
|
57
|
+
actions
|
|
58
|
+
.map(
|
|
59
|
+
(action) =>
|
|
60
|
+
`import ${action}_action from "./actions/${action}_action";`
|
|
61
|
+
)
|
|
62
|
+
.join("\n") +
|
|
63
|
+
`
|
|
64
|
+
|
|
65
|
+
export default {
|
|
66
|
+
${actions.map((a) => ` ${a}_action,`).join("\n")}
|
|
67
|
+
};
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
const controllerIndexPath = path.join(controllerDir, "index.ts");
|
|
71
|
+
fs.writeFileSync(controllerIndexPath, indexContent);
|
|
72
|
+
console.log(
|
|
73
|
+
`create ${path.relative(process.cwd(), controllerIndexPath)}`
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// --- 5️⃣ API route.ts ---
|
|
77
|
+
fs.mkdirSync(apiDir, { recursive: true });
|
|
78
|
+
|
|
79
|
+
const apiRoutePath = path.join(apiDir, "route.ts");
|
|
80
|
+
|
|
81
|
+
const apiRouteContent = `/**
|
|
82
|
+
* Standard RESTful Routes
|
|
83
|
+
*
|
|
84
|
+
* | HTTP Verb | Controller#Action | Purpose | Path |
|
|
85
|
+
* |-----------|-------------------|------------------|--------------------------------------------|
|
|
86
|
+
* | GET | index | List all | /${snakePluralName}
|
|
87
|
+
* | GET | show | Get one | /${snakePluralName}/:id
|
|
88
|
+
* | POST | create | Create | /${snakePluralName}
|
|
89
|
+
* | PATCH | update | Update (partial) | /${snakePluralName}/:id
|
|
90
|
+
* | PUT | update | Update (full) | /${snakePluralName}/:id
|
|
91
|
+
* | DELETE | destroy | Delete | /${snakePluralName}/:id
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
import ${camelName}Controller from "@controller/${camelName}";
|
|
95
|
+
|
|
96
|
+
export async function GET(req: Request) {
|
|
97
|
+
return ${camelName}Controller.index_action(req);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function POST(req: Request) {
|
|
101
|
+
return ${camelName}Controller.create_action(req);
|
|
102
|
+
}
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
fs.writeFileSync(apiRoutePath, apiRouteContent);
|
|
106
|
+
console.log(`create ${path.relative(process.cwd(), apiRoutePath)}`);
|
|
107
|
+
|
|
108
|
+
// --- 6️⃣ API [id]/route.ts ---
|
|
109
|
+
fs.mkdirSync(apiIdDir, { recursive: true });
|
|
110
|
+
|
|
111
|
+
const apiIdRoutePath = path.join(apiIdDir, "route.ts");
|
|
112
|
+
|
|
113
|
+
const apiIdRouteContent = `import ${camelName}Controller from "@controller/${camelName}";
|
|
114
|
+
|
|
115
|
+
export async function GET(req: Request, { params }: { params: { id: string } }) {
|
|
116
|
+
return ${camelName}Controller.show_action(req, params.id);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function PATCH(req: Request, { params }: { params: { id: string } }) {
|
|
120
|
+
return ${camelName}Controller.update_action(req, params.id);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function PUT(req: Request, { params }: { params: { id: string } }) {
|
|
124
|
+
return ${camelName}Controller.update_action(req, params.id);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function DELETE(req: Request, { params }: { params: { id: string } }) {
|
|
128
|
+
return ${camelName}Controller.destroy_action(req, params.id);
|
|
129
|
+
}
|
|
130
|
+
`;
|
|
131
|
+
|
|
132
|
+
fs.writeFileSync(apiIdRoutePath, apiIdRouteContent);
|
|
133
|
+
console.log(`create ${path.relative(process.cwd(), apiIdRoutePath)}`);
|
|
134
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const [, , command, ...args] = process.argv;
|
|
4
|
+
|
|
5
|
+
async function run() {
|
|
6
|
+
switch (command) {
|
|
7
|
+
case "generate": {
|
|
8
|
+
const { runGenerate } = await import("./generate/index.js");
|
|
9
|
+
await runGenerate(args);
|
|
10
|
+
break;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
default: {
|
|
14
|
+
console.log(`Unknown command: ${command}`);
|
|
15
|
+
console.log("Available commands: generate");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
run();
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"outDir": "dist",
|
|
4
|
+
"rootDir": "src",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"target": "ES2020",
|
|
7
|
+
"moduleResolution": "Node",
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"ignoreDeprecations": "6.0",
|
|
12
|
+
"types": ["node"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src"]
|
|
15
|
+
}
|