@omer-x/next-openapi-scaffold-generator 0.2.0 → 0.3.0

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.
Files changed (2) hide show
  1. package/bin/index.js +101 -58
  2. package/package.json +4 -2
package/bin/index.js CHANGED
@@ -1,3 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import prompts from "prompts";
5
+
1
6
  // src/core/file.ts
2
7
  import fs from "node:fs/promises";
3
8
  import path from "node:path";
@@ -7,6 +12,12 @@ async function saveFile(directory, fileName, content) {
7
12
  const filePath = path.resolve(dirPath, fileName);
8
13
  await fs.writeFile(filePath, content, "utf8");
9
14
  }
15
+ async function appendFile(directory, fileName, content) {
16
+ const dirPath = path.resolve(process.cwd(), directory);
17
+ await fs.mkdir(dirPath, { recursive: true });
18
+ const filePath = path.resolve(dirPath, fileName);
19
+ await fs.appendFile(filePath, content + "\n", "utf8");
20
+ }
10
21
 
11
22
  // src/core/string.ts
12
23
  import * as changeCase from "change-case";
@@ -43,26 +54,16 @@ function capitalCase2(text, plural, onlyFirst) {
43
54
  return changeCase.capitalCase(properText);
44
55
  }
45
56
 
46
- // src/prompt.ts
47
- import { stdin as input, stdout as output } from "node:process";
48
- import * as readline from "node:readline/promises";
49
- var rl = readline.createInterface({ input, output });
50
- async function askQuestions(queries) {
51
- const answers = await Promise.all(queries.map((query) => rl.question(query)));
52
- rl.close();
53
- return answers;
54
- }
55
-
56
57
  // src/core/template.ts
57
58
  import Handlebars from "handlebars";
58
- function getTemplate(input2) {
59
- return Handlebars.compile(input2);
59
+ function getTemplate(input) {
60
+ return Handlebars.compile(input);
60
61
  }
61
62
 
62
- // _8rxnfpgcy:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/api-route.hbs
63
+ // _rnokx36w3:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/api-route.hbs
63
64
  var api_route_default = 'import defineRoute from "@omer-x/next-openapi-route-handler";\nimport z from "zod";\nimport db from "~/database";\nimport { New{{ pascalCaseSingular }}DTO, {{ pascalCaseSingular }}DTO } from "~/models/{{ kebabCaseSingular }}";\nimport create{{ pascalCaseSingular }} from "~/operations/create{{ pascalCaseSingular }}";\nimport get{{ pascalCasePlural }} from "~/operations/get{{ pascalCasePlural }}";\n\n{{{ readAllOperation }}}\n\n{{{ createOperation }}}\n';
64
65
 
65
- // _8rxnfpgcy:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/routes/create.hbs
66
+ // _rnokx36w3:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/routes/create.hbs
66
67
  var create_default = 'export const { POST } = defineRoute({\n operationId: "create{{ pascalCaseSingular }}",\n method: "POST",\n summary: "Create {{ noCaseSingular }}",\n description: "Create a new {{ noCaseSingular }}",\n tags: ["{{ capitalCasePlural }}"],\n requestBody: New{{ pascalCaseSingular }}DTO,\n action: async ({ body }) => {\n const {{ camelCaseSingular }} = await create{{ pascalCaseSingular }}(db, body);\n return Response.json({{ camelCaseSingular }}, { status: 201 });\n },\n responses: {\n 201: { description: "{{ onlyFirstCapitalCaseSingular }} created successfully", content: {{ pascalCaseSingular }}DTO },\n },\n});\n';
67
68
 
68
69
  // src/templates/routes/create.ts
@@ -77,7 +78,7 @@ function generateCreateOperationRoute(modelName2) {
77
78
  });
78
79
  }
79
80
 
80
- // _8rxnfpgcy:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/routes/read-all.hbs
81
+ // _rnokx36w3:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/routes/read-all.hbs
81
82
  var read_all_default = 'export const { GET } = defineRoute({\n operationId: "get{{ pascalCasePlural }}",\n method: "GET",\n summary: "Get all {{ noCasePlural }}",\n description: "Retrieve a list of {{ noCasePlural }}",\n tags: ["{{ capitalCasePlural }}"],\n queryParams: z.object({\n select: {{ pascalCaseSingular }}DTO.keyof().array().default([])\n .describe("List of the column names"),\n }),\n action: async ({ queryParams }) => {\n return Response.json(await get{{ pascalCasePlural }}(db, queryParams.select));\n },\n responses: {\n 200: { description: "Returns a list of {{ noCasePlural }}", content: {{ pascalCaseSingular }}DTO, isArray: true },\n },\n});\n';
82
83
 
83
84
  // src/templates/routes/read-all.ts
@@ -104,10 +105,10 @@ function generateApiRoute(modelName2) {
104
105
  });
105
106
  }
106
107
 
107
- // _8rxnfpgcy:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/api-route-with-id.hbs
108
+ // _rnokx36w3:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/api-route-with-id.hbs
108
109
  var api_route_with_id_default = 'import defineRoute from "@omer-x/next-openapi-route-handler";\nimport db from "~/database";\nimport { {{ pascalCaseSingular }}DTO, {{ pascalCaseSingular }}PatchDTO } from "~/models/{{ kebabCaseSingular }}";\nimport delete{{ pascalCaseSingular }} from "~/operations/delete{{ pascalCaseSingular }}";\nimport get{{ pascalCaseSingular }} from "~/operations/get{{ pascalCaseSingular }}";\nimport update{{ pascalCaseSingular }} from "~/operations/update{{ pascalCaseSingular }}";\n\n{{{ readOperation }}}\n\n{{{ updateOperation }}}\n\n{{{ deleteOperation }}}\n';
109
110
 
110
- // _8rxnfpgcy:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/routes/delete.hbs
111
+ // _rnokx36w3:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/routes/delete.hbs
111
112
  var delete_default = 'export const { DELETE } = defineRoute({\n operationId: "delete{{ pascalCaseSingular }}",\n method: "DELETE",\n summary: "Delete {{ noCaseSingular }}",\n description: "Delete a specific {{ noCaseSingular }} by ID",\n tags: ["{{ capitalCasePlural }}"],\n pathParams: {{ pascalCaseSingular }}DTO.pick({ id: true }),\n action: async ({ pathParams }) => {\n const {{ camelCaseSingular }} = await delete{{ pascalCaseSingular }}(db, pathParams.id);\n if ({{ camelCaseSingular }}?.id === pathParams.id) {\n return new Response(null, { status: 204 });\n }\n return new Response(null, { status: 404 });\n },\n responses: {\n 204: { description: "{{ onlyFirstCapitalCaseSingular }} deleted successfully" },\n 404: { description: "{{ onlyFirstCapitalCaseSingular }} not found" },\n },\n});\n';
112
113
 
113
114
  // src/templates/routes/delete.ts
@@ -122,7 +123,7 @@ function generateDeleteOperationRoute(modelName2) {
122
123
  });
123
124
  }
124
125
 
125
- // _8rxnfpgcy:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/routes/read.hbs
126
+ // _rnokx36w3:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/routes/read.hbs
126
127
  var read_default = 'export const { GET } = defineRoute({\n operationId: "get{{ pascalCaseSingular }}",\n method: "GET",\n summary: "Get {{ noCaseSingular }}",\n description: "Get a specific {{ noCaseSingular }} by ID",\n tags: ["{{ capitalCasePlural }}"],\n pathParams: {{ pascalCaseSingular }}DTO.pick({ id: true }),\n action: async ({ pathParams }) => {\n const {{ camelCaseSingular }} = await get{{ pascalCaseSingular }}(db, pathParams.id);\n if ({{ camelCaseSingular }}) {\n return Response.json({{ camelCaseSingular }});\n }\n return new Response(null, { status: 404 });\n },\n responses: {\n 200: { description: "{{ onlyFirstCapitalCaseSingular }} found", content: {{ pascalCaseSingular }}DTO },\n 404: { description: "{{ onlyFirstCapitalCaseSingular }} not found" },\n },\n});\n';
127
128
 
128
129
  // src/templates/routes/read.ts
@@ -137,7 +138,7 @@ function generateReadOperationRoute(modelName2) {
137
138
  });
138
139
  }
139
140
 
140
- // _8rxnfpgcy:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/routes/update.hbs
141
+ // _rnokx36w3:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/routes/update.hbs
141
142
  var update_default = 'export const { PATCH } = defineRoute({\n operationId: "update{{ pascalCaseSingular }}",\n method: "PATCH",\n summary: "Update {{ noCaseSingular }}",\n description: "Update a specific {{ noCaseSingular }} by ID",\n tags: ["{{ capitalCasePlural }}"],\n pathParams: {{ pascalCaseSingular }}DTO.pick({ id: true }),\n requestBody: {{ pascalCaseSingular }}PatchDTO,\n action: async ({ pathParams, body }) => {\n const {{ camelCaseSingular }} = await update{{ pascalCaseSingular }}(db, pathParams.id, body);\n if ({{ camelCaseSingular }}?.id === pathParams.id) {\n return Response.json({{ camelCaseSingular }});\n }\n return new Response(null, { status: 404 });\n },\n responses: {\n 200: { description: "{{ onlyFirstCapitalCaseSingular }} updated successfully", content: {{ pascalCaseSingular }}DTO },\n 404: { description: "{{ onlyFirstCapitalCaseSingular }} not found" },\n },\n});\n';
142
143
 
143
144
  // src/templates/routes/update.ts
@@ -164,20 +165,22 @@ function generateApiRouteWithId(modelName2) {
164
165
  });
165
166
  }
166
167
 
167
- // _8rxnfpgcy:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/model.hbs
168
- var model_default = 'import { createInsertSchema } from "drizzle-zod";\nimport { {{ camelCasePlural }} } from "~/database/schema";\nimport type z from "zod";\n\nconst baseSchema = createInsertSchema({{ camelCasePlural }}, {\n id: schema => schema.id.readonly().describe("Unique identifier of the {{ noCaseSingular }}"),\n // describe other fields here\n createdAt: schema => schema.createdAt.readonly().describe("Creation date of the {{ noCaseSingular }} as an ISO 8601 date string"),\n updatedAt: schema => schema.updatedAt.readonly().describe("Modification date of the {{ noCaseSingular }} as an ISO 8601 date string"),\n});\n\nexport const {{ pascalCaseSingular }}DTO = baseSchema.required()\n .describe("Represents a {{ noCaseSingular }} definition");\n\nexport const New{{ pascalCaseSingular }}DTO = baseSchema.omit({\n id: true,\n createdAt: true,\n updatedAt: true,\n}).describe("Data Transfer Object for creating a new {{ noCaseSingular }}");\n\nexport const {{ pascalCaseSingular }}PatchDTO = New{{ pascalCaseSingular }}DTO.partial().omit({\n}).describe("Data Transfer Object for updating an existing {{ noCaseSingular }}");\n\nexport function test{{ pascalCaseSingular }}Data() {\n return {\n // add the required fields here\n } satisfies z.infer<typeof New{{ pascalCaseSingular }}DTO>;\n}\n';
168
+ // _rnokx36w3:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/model.hbs
169
+ var model_default = 'import { createInsertSchema } from "drizzle-zod";\nimport { {{ camelCasePlural }} } from "~/database/schema/{{ kebabCasePlural }}";\nimport type z from "zod";\n\nconst baseSchema = createInsertSchema({{ camelCasePlural }}, {\n id: schema => schema.id.readonly().describe("Unique identifier of the {{ noCaseSingular }}"),\n {{ firstField }}: schema => schema.{{ firstField }}.describe("............"),\n // describe other fields here\n createdAt: schema => schema.createdAt.readonly().describe("Creation date of the {{ noCaseSingular }} as an ISO 8601 date string"),\n updatedAt: schema => schema.updatedAt.readonly().describe("Modification date of the {{ noCaseSingular }} as an ISO 8601 date string"),\n});\n\nexport const {{ pascalCaseSingular }}DTO = baseSchema.required()\n .describe("Represents a {{ noCaseSingular }} definition");\n\nexport const New{{ pascalCaseSingular }}DTO = baseSchema.omit({\n id: true,\n createdAt: true,\n updatedAt: true,\n}).describe("Data Transfer Object for creating a new {{ noCaseSingular }}");\n\nexport const {{ pascalCaseSingular }}PatchDTO = New{{ pascalCaseSingular }}DTO.partial().omit({\n}).describe("Data Transfer Object for updating an existing {{ noCaseSingular }}");\n\nexport function test{{ pascalCaseSingular }}Data() {\n return {\n {{ firstField }}: "unknown",\n // add the required fields here\n } satisfies z.infer<typeof New{{ pascalCaseSingular }}DTO>;\n}\n';
169
170
 
170
171
  // src/templates/model.ts
171
- function generateModel(modelName2) {
172
+ function generateModel(modelName2, firstField2) {
172
173
  const template = getTemplate(model_default);
173
174
  return template({
174
175
  camelCasePlural: camelCase2(modelName2, true),
176
+ kebabCasePlural: kebabCase2(modelName2, true),
175
177
  noCaseSingular: noCase2(modelName2, false),
176
- pascalCaseSingular: pascalCase2(modelName2, false)
178
+ pascalCaseSingular: pascalCase2(modelName2, false),
179
+ firstField: firstField2
177
180
  });
178
181
  }
179
182
 
180
- // _8rxnfpgcy:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operation-tests/create.hbs
183
+ // _rnokx36w3:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operation-tests/create.hbs
181
184
  var create_default2 = 'import { afterAll, describe, expect, it } from "@jest/globals";\nimport db, { evacuatePool } from "~/database";\nimport { {{ pascalCaseSingular }}DTO, test{{ pascalCaseSingular }}Data } from "~/models/{{ kebabCaseSingular }}";\nimport create{{ pascalCaseSingular }} from "./create{{ pascalCaseSingular }}";\nimport delete{{ pascalCaseSingular }} from "./delete{{ pascalCaseSingular }}";\n\ndescribe("create{{ pascalCaseSingular }}", () => {\n const schema = {{ pascalCaseSingular }}DTO.pick({ id: true });\n const data = test{{ pascalCaseSingular }}Data();\n\n it("should successfully create a {{ noCaseSingular }}", async () => {\n await db.transaction(async tx => {\n const {{ camelCaseSingular }} = await create{{ pascalCaseSingular }}(tx, data);\n const { success } = schema.safeParse({{ camelCaseSingular }});\n expect(success).toBe(true);\n await delete{{ pascalCaseSingular }}(tx, {{ camelCaseSingular }}.id);\n });\n });\n});\n\nafterAll(done => {\n evacuatePool().then(() => done());\n});\n';
182
185
 
183
186
  // src/templates/operation-tests/create.ts
@@ -191,7 +194,7 @@ function generateCreateTest(modelName2) {
191
194
  });
192
195
  }
193
196
 
194
- // _8rxnfpgcy:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operation-tests/delete.hbs
197
+ // _rnokx36w3:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operation-tests/delete.hbs
195
198
  var delete_default2 = 'import { afterAll, afterEach, beforeEach, describe, expect, it } from "@jest/globals";\nimport db, { evacuatePool } from "~/database";\nimport { {{ pascalCaseSingular }}DTO, test{{ pascalCaseSingular }}Data } from "~/models/{{ kebabCaseSingular }}";\nimport create{{ pascalCaseSingular }} from "./create{{ pascalCaseSingular }}";\nimport delete{{ pascalCaseSingular }} from "./delete{{ pascalCaseSingular }}";\n\nlet test{{ pascalCaseSingular }}ID = "";\n\ndescribe("delete{{ pascalCaseSingular }}", () => {\n const schema = {{ pascalCaseSingular }}DTO.pick({ id: true });\n\n it("should fail when trying to delete a nonexistent {{ noCaseSingular }}", async () => {\n const pseudo{{ pascalCaseSingular }}Id = crypto.randomUUID();\n const result = await delete{{ pascalCaseSingular }}(db, pseudo{{ pascalCaseSingular }}Id);\n const { success } = schema.safeParse(result);\n expect(success).toBe(false);\n });\n\n it("should successfully delete an existing {{ noCaseSingular }}", async () => {\n await db.transaction(async tx => {\n const result = await delete{{ pascalCaseSingular }}(tx, test{{ pascalCaseSingular }}ID);\n const { success } = schema.safeParse(result);\n expect(success).toBe(true);\n });\n });\n});\n\nbeforeEach(async () => {\n const {{ camelCaseSingular }} = await create{{ pascalCaseSingular }}(db, test{{ pascalCaseSingular }}Data());\n test{{ pascalCaseSingular }}ID = {{ camelCaseSingular }}.id;\n});\n\nafterEach(async () => {\n await delete{{ pascalCaseSingular }}(db, test{{ pascalCaseSingular }}ID);\n});\n\nafterAll(done => {\n evacuatePool().then(() => done());\n});\n';
196
199
 
197
200
  // src/templates/operation-tests/delete.ts
@@ -205,7 +208,7 @@ function generateDeleteTest(modelName2) {
205
208
  });
206
209
  }
207
210
 
208
- // _8rxnfpgcy:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operation-tests/read.hbs
211
+ // _rnokx36w3:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operation-tests/read.hbs
209
212
  var read_default2 = 'import { afterAll, afterEach, beforeEach, describe, expect, it } from "@jest/globals";\nimport db, { evacuatePool } from "~/database";\nimport { {{ pascalCaseSingular }}DTO, test{{ pascalCaseSingular }}Data } from "~/models/{{ kebabCaseSingular }}";\nimport create{{ pascalCaseSingular }} from "./create{{ pascalCaseSingular }}";\nimport delete{{ pascalCaseSingular }} from "./delete{{ pascalCaseSingular }}";\nimport get{{ pascalCaseSingular }} from "./get{{ pascalCaseSingular }}";\n\nlet test{{ pascalCaseSingular }}ID = "";\n\ndescribe("get{{ pascalCaseSingular }}", () => {\n const schema = {{ pascalCaseSingular }}DTO;\n\n it("should fail when trying to find a nonexistent {{ noCaseSingular }}", async () => {\n const pseudo{{ pascalCaseSingular }}Id = crypto.randomUUID();\n const result = await get{{ pascalCaseSingular }}(db, pseudo{{ pascalCaseSingular }}Id);\n const { success } = schema.safeParse(result);\n expect(success).toBe(false);\n });\n\n it("should successfully retrieve an existing {{ noCaseSingular }}", async () => {\n const result = await get{{ pascalCaseSingular }}(db, test{{ pascalCaseSingular }}ID);\n const { success } = schema.safeParse(result);\n expect(success).toBe(true);\n });\n});\n\nbeforeEach(async () => {\n const {{ camelCaseSingular }} = await create{{ pascalCaseSingular }}(db, test{{ pascalCaseSingular }}Data());\n test{{ pascalCaseSingular }}ID = {{ camelCaseSingular }}.id;\n});\n\nafterEach(async () => {\n await delete{{ pascalCaseSingular }}(db, test{{ pascalCaseSingular }}ID);\n});\n\nafterAll(done => {\n evacuatePool().then(() => done());\n});\n';
210
213
 
211
214
  // src/templates/operation-tests/read.ts
@@ -219,7 +222,7 @@ function generateReadTest(modelName2) {
219
222
  });
220
223
  }
221
224
 
222
- // _8rxnfpgcy:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operation-tests/read-all.hbs
225
+ // _rnokx36w3:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operation-tests/read-all.hbs
223
226
  var read_all_default2 = 'import { afterAll, expect, test } from "@jest/globals";\nimport db, { evacuatePool } from "~/database";\nimport { {{ pascalCaseSingular }}DTO } from "~/models/{{ kebabCaseSingular }}";\nimport get{{ pascalCasePlural }} from "./get{{ pascalCasePlural }}";\n\ntest("get{{ pascalCasePlural }}", async () => {\n const result = await get{{ pascalCasePlural }}(db, ["id"]);\n const schema = {{ pascalCaseSingular }}DTO.partial().array();\n const { success } = schema.safeParse(result);\n expect(success).toBe(true);\n});\n\nafterAll(done => {\n evacuatePool().then(() => done());\n});\n';
224
227
 
225
228
  // src/templates/operation-tests/read-all.ts
@@ -232,22 +235,23 @@ function generateReadAllTest(modelName2) {
232
235
  });
233
236
  }
234
237
 
235
- // _8rxnfpgcy:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operation-tests/update.hbs
236
- var update_default2 = 'import { afterAll, afterEach, beforeEach, describe, expect, it } from "@jest/globals";\nimport db, { evacuatePool } from "~/database";\nimport { {{ pascalCaseSingular }}DTO, test{{ pascalCaseSingular }}Data } from "~/models/{{ kebabCaseSingular }}";\nimport create{{ pascalCaseSingular }} from "./create{{ pascalCaseSingular }}";\nimport delete{{ pascalCaseSingular }} from "./delete{{ pascalCaseSingular }}";\nimport update{{ pascalCaseSingular }} from "./update{{ pascalCaseSingular }}";\n\nlet test{{ pascalCaseSingular }}ID = "";\n\ndescribe("update{{ pascalCaseSingular }}", () => {\n const schema = {{ pascalCaseSingular }}DTO;\n\n it("should fail when trying to update a nonexistent {{ noCaseSingular }}", async () => {\n const pseudo{{ pascalCaseSingular }}Id = crypto.randomUUID();\n const result = await update{{ pascalCaseSingular }}(db, pseudo{{ pascalCaseSingular }}Id, test{{ pascalCaseSingular }}Data("new.email@example.com"));\n const { success } = schema.safeParse(result);\n expect(success).toBe(false);\n });\n\n it("should successfully update an existing {{ noCaseSingular }}", async () => {\n await db.transaction(async tx => {\n const result = await update{{ pascalCaseSingular }}(tx, test{{ pascalCaseSingular }}ID, test{{ pascalCaseSingular }}Data("new.email@example.com"));\n const { success } = schema.safeParse(result);\n expect(success).toBe(true);\n expect(result?.email).toBe("new.email@example.com");\n });\n });\n});\n\nbeforeEach(async () => {\n const {{ camelCaseSingular }} = await create{{ pascalCaseSingular }}(db, test{{ pascalCaseSingular }}Data());\n test{{ pascalCaseSingular }}ID = {{ camelCaseSingular }}.id;\n});\n\nafterEach(async () => {\n await delete{{ pascalCaseSingular }}(db, test{{ pascalCaseSingular }}ID);\n});\n\nafterAll(done => {\n evacuatePool().then(() => done());\n});\n';
238
+ // _rnokx36w3:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operation-tests/update.hbs
239
+ var update_default2 = 'import { afterAll, afterEach, beforeEach, describe, expect, it } from "@jest/globals";\nimport db, { evacuatePool } from "~/database";\nimport { {{ pascalCaseSingular }}DTO, test{{ pascalCaseSingular }}Data } from "~/models/{{ kebabCaseSingular }}";\nimport create{{ pascalCaseSingular }} from "./create{{ pascalCaseSingular }}";\nimport delete{{ pascalCaseSingular }} from "./delete{{ pascalCaseSingular }}";\nimport update{{ pascalCaseSingular }} from "./update{{ pascalCaseSingular }}";\n\nlet test{{ pascalCaseSingular }}ID = "";\n\ndescribe("update{{ pascalCaseSingular }}", () => {\n const schema = {{ pascalCaseSingular }}DTO;\n\n it("should fail when trying to update a nonexistent {{ noCaseSingular }}", async () => {\n const pseudo{{ pascalCaseSingular }}Id = crypto.randomUUID();\n const result = await update{{ pascalCaseSingular }}(db, pseudo{{ pascalCaseSingular }}Id, test{{ pascalCaseSingular }}Data());\n const { success } = schema.safeParse(result);\n expect(success).toBe(false);\n });\n\n it("should successfully update an existing {{ noCaseSingular }}", async () => {\n await db.transaction(async tx => {\n const result = await update{{ pascalCaseSingular }}(tx, test{{ pascalCaseSingular }}ID, { {{ firstField }}: "testing" });\n const { success } = schema.safeParse(result);\n expect(success).toBe(true);\n expect(result?.{{ firstField }}).toBe("testing");\n });\n });\n});\n\nbeforeEach(async () => {\n const {{ camelCaseSingular }} = await create{{ pascalCaseSingular }}(db, test{{ pascalCaseSingular }}Data());\n test{{ pascalCaseSingular }}ID = {{ camelCaseSingular }}.id;\n});\n\nafterEach(async () => {\n await delete{{ pascalCaseSingular }}(db, test{{ pascalCaseSingular }}ID);\n});\n\nafterAll(done => {\n evacuatePool().then(() => done());\n});\n';
237
240
 
238
241
  // src/templates/operation-tests/update.ts
239
- function generateUpdateTest(modelName2) {
242
+ function generateUpdateTest(modelName2, firstField2) {
240
243
  const template = getTemplate(update_default2);
241
244
  return template({
242
245
  camelCaseSingular: camelCase2(modelName2, false),
243
246
  kebabCaseSingular: kebabCase2(modelName2, false),
244
247
  noCaseSingular: noCase2(modelName2, false),
245
- pascalCaseSingular: pascalCase2(modelName2, false)
248
+ pascalCaseSingular: pascalCase2(modelName2, false),
249
+ firstField: firstField2
246
250
  });
247
251
  }
248
252
 
249
- // _8rxnfpgcy:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operations/create.hbs
250
- var create_default3 = 'import type database from "~/database";\nimport { {{ camelCasePlural }} } from "~/database/schema";\nimport type { New{{ pascalCaseSingular }}DTO } from "~/models/{{ kebabCaseSingular }}";\nimport type z from "zod";\n\nexport default async function create{{ pascalCaseSingular }}(db: typeof database, data: z.infer<typeof New{{ pascalCaseSingular }}DTO>) {\n const [{{ camelCaseSingular }}] = await db.insert({{ camelCasePlural }}).values(data).returning({ id: {{ camelCasePlural }}.id });\n return {{ camelCaseSingular }};\n}\n';
253
+ // _rnokx36w3:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operations/create.hbs
254
+ var create_default3 = 'import type database from "~/database";\nimport { {{ camelCasePlural }} } from "~/database/schema/{{ kebabCasePlural }}";\nimport type { New{{ pascalCaseSingular }}DTO } from "~/models/{{ kebabCaseSingular }}";\nimport type z from "zod";\n\nexport default async function create{{ pascalCaseSingular }}(db: typeof database, data: z.infer<typeof New{{ pascalCaseSingular }}DTO>) {\n const [{{ camelCaseSingular }}] = await db.insert({{ camelCasePlural }}).values(data).returning({ id: {{ camelCasePlural }}.id });\n return {{ camelCaseSingular }};\n}\n';
251
255
 
252
256
  // src/templates/operations/create.ts
253
257
  function generateCreateOperation(modelName2) {
@@ -255,13 +259,14 @@ function generateCreateOperation(modelName2) {
255
259
  return template({
256
260
  camelCasePlural: camelCase2(modelName2, true),
257
261
  camelCaseSingular: camelCase2(modelName2, false),
262
+ kebabCasePlural: kebabCase2(modelName2, true),
258
263
  kebabCaseSingular: kebabCase2(modelName2, false),
259
264
  pascalCaseSingular: pascalCase2(modelName2, false)
260
265
  });
261
266
  }
262
267
 
263
- // _8rxnfpgcy:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operations/delete.hbs
264
- var delete_default3 = 'import { eq } from "drizzle-orm";\nimport type database from "~/database";\nimport { {{ camelCasePlural }} } from "~/database/schema";\n\nexport default async function delete{{ pascalCaseSingular }}(db: typeof database, {{ camelCaseSingular }}Id: string) {\n const results = await db.delete({{ camelCasePlural }}).where(eq({{ camelCasePlural }}.id, {{ camelCaseSingular }}Id)).returning({\n id: {{ camelCasePlural }}.id,\n });\n return results.shift();\n}\n';
268
+ // _rnokx36w3:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operations/delete.hbs
269
+ var delete_default3 = 'import { eq } from "drizzle-orm";\nimport type database from "~/database";\nimport { {{ camelCasePlural }} } from "~/database/schema/{{ kebabCasePlural }}";\n\nexport default async function delete{{ pascalCaseSingular }}(db: typeof database, {{ camelCaseSingular }}Id: string) {\n const results = await db.delete({{ camelCasePlural }}).where(eq({{ camelCasePlural }}.id, {{ camelCaseSingular }}Id)).returning({\n id: {{ camelCasePlural }}.id,\n });\n return results.shift();\n}\n';
265
270
 
266
271
  // src/templates/operations/delete.ts
267
272
  function generateDeleteOperation(modelName2) {
@@ -269,11 +274,12 @@ function generateDeleteOperation(modelName2) {
269
274
  return template({
270
275
  camelCasePlural: camelCase2(modelName2, true),
271
276
  camelCaseSingular: camelCase2(modelName2, false),
277
+ kebabCasePlural: kebabCase2(modelName2, true),
272
278
  pascalCaseSingular: pascalCase2(modelName2, false)
273
279
  });
274
280
  }
275
281
 
276
- // _8rxnfpgcy:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operations/read.hbs
282
+ // _rnokx36w3:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operations/read.hbs
277
283
  var read_default3 = 'import type database from "~/database";\n\nexport default function get{{ pascalCaseSingular }}(db: typeof database, {{ camelCaseSingular }}Id: string) {\n return db.query.{{ camelCasePlural }}.findFirst({\n where: (table, { eq }) => eq(table.id, {{ camelCaseSingular }}Id),\n });\n}\n';
278
284
 
279
285
  // src/templates/operations/read.ts
@@ -286,7 +292,7 @@ function generateReadOperation(modelName2) {
286
292
  });
287
293
  }
288
294
 
289
- // _8rxnfpgcy:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operations/read-all.hbs
295
+ // _rnokx36w3:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operations/read-all.hbs
290
296
  var read_all_default3 = 'import selectColumns from "~/core/column";\nimport type database from "~/database";\nimport { {{ pascalCaseSingular }}DTO } from "~/models/{{ kebabCaseSingular }}";\nimport type { z } from "zod";\n\nconst selectSchema = {{ pascalCaseSingular }}DTO.keyof().array().default([]);\n\nexport default function get{{ pascalCasePlural }}(db: typeof database, select: z.infer<typeof selectSchema>) {\n return db.query.{{ camelCasePlural }}.findMany({\n columns: selectColumns(select),\n orderBy: (u, { asc }) => [asc(u.createdAt)],\n });\n}\n';
291
297
 
292
298
  // src/templates/operations/read-all.ts
@@ -300,8 +306,8 @@ function generateReadAllOperation(modelName2) {
300
306
  });
301
307
  }
302
308
 
303
- // _8rxnfpgcy:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operations/update.hbs
304
- var update_default3 = 'import { eq } from "drizzle-orm";\nimport type database from "~/database";\nimport { {{ camelCasePlural }} } from "~/database/schema";\nimport type { {{ pascalCaseSingular }}PatchDTO } from "~/models/{{ kebabCaseSingular }}";\nimport type { z } from "zod";\n\nexport default async function update{{ pascalCaseSingular }}(db: typeof database, {{ camelCaseSingular }}Id: string, patch: z.infer<typeof {{ pascalCaseSingular }}PatchDTO>) {\n const results = await db.update({{ camelCasePlural }})\n .set(patch)\n .where(eq({{ camelCasePlural }}.id, {{ camelCaseSingular }}Id))\n .returning();\n return results.shift();\n}\n';
309
+ // _rnokx36w3:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/operations/update.hbs
310
+ var update_default3 = 'import { eq } from "drizzle-orm";\nimport type database from "~/database";\nimport { {{ camelCasePlural }} } from "~/database/schema/{{ kebabCasePlural }}";\nimport type { {{ pascalCaseSingular }}PatchDTO } from "~/models/{{ kebabCaseSingular }}";\nimport type { z } from "zod";\n\nexport default async function update{{ pascalCaseSingular }}(db: typeof database, {{ camelCaseSingular }}Id: string, patch: z.infer<typeof {{ pascalCaseSingular }}PatchDTO>) {\n const results = await db.update({{ camelCasePlural }})\n .set(patch)\n .where(eq({{ camelCasePlural }}.id, {{ camelCaseSingular }}Id))\n .returning();\n return results.shift();\n}\n';
305
311
 
306
312
  // src/templates/operations/update.ts
307
313
  function generateUpdateOperation(modelName2) {
@@ -309,37 +315,74 @@ function generateUpdateOperation(modelName2) {
309
315
  return template({
310
316
  camelCasePlural: camelCase2(modelName2, true),
311
317
  camelCaseSingular: camelCase2(modelName2, false),
318
+ kebabCasePlural: kebabCase2(modelName2, true),
312
319
  kebabCaseSingular: kebabCase2(modelName2, false),
313
320
  pascalCaseSingular: pascalCase2(modelName2, false)
314
321
  });
315
322
  }
316
323
 
317
- // _8rxnfpgcy:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/schema.hbs
318
- var schema_default = 'import { relations, sql } from "drizzle-orm";\nimport { bigint, boolean, char, index, integer, pgTable, primaryKey, smallint, text, timestamp, uuid, varchar } from "drizzle-orm/pg-core";\n\nexport const {{ camelCasePlural }} = pgTable("{{ snakeCasePlural }}", {\n id: uuid("id").defaultRandom().primaryKey(),\n // ...\n exampleInteger: integer("example_integer").notNull().default(2147483647),\n exampleSmallInt: smallint("example_small_int").notNull().default(32767),\n exampleBigInt: bigint("example_big_int", { mode: "number" }).notNull().default(Number.MAX_SAFE_INTEGER),\n exampleBiggerInt: bigint("example_bigger_int", { mode: "bigint" }).notNull().default(0),\n exampleBoolean: boolean("example_boolean").notNull().default(false),\n exampleText: text("example_text").notNull().default("Lorem ipsum dolor sit amet"),\n exampleVarchar: varchar("example_varchar", { length: 16 }).notNull().default("hello world"),\n examplechar: char("example_char", { length: 16 }).notNull().default("hello world "),\n // ...\n createdAt: timestamp("created_at", { withTimezone: true, precision: 3 }).default(sql`CURRENT_TIMESTAMP`).notNull(),\n updatedAt: timestamp("updated_at", { withTimezone: true, precision: 3 }).default(sql`CURRENT_TIMESTAMP`).notNull(),\n}, table => ({\n createdAtIdx: index().on(table.createdAt),\n}));\n\nexport const relationsOf{{ pascalCasePlural }} = relations({{ camelCasePlural }}, ({ many, one }) => ({\n master: one(masters, {\n fields: [{{ camelCasePlural }}.masterId],\n references: [masters.id],\n }),\n slaves: many(slaves),\n}));\n';
324
+ // _rnokx36w3:/home/runner/work/next-openapi-scaffold-generator/next-openapi-scaffold-generator/src/templates/schema.hbs
325
+ var schema_default = 'import { relations, sql } from "drizzle-orm";\nimport { bigint, boolean, char, index, integer, pgTable, smallint, text, timestamp, uuid, varchar } from "drizzle-orm/pg-core";\n\nexport const {{ camelCasePlural }} = pgTable("{{ snakeCasePlural }}", {\n id: uuid("id").defaultRandom().primaryKey(),\n {{ firstField }}: text("{{ firstField }}").notNull(),\n // ...\n exampleInteger: integer("example_integer").notNull().default(2147483647),\n exampleSmallInt: smallint("example_small_int").notNull().default(32767),\n exampleBigInt: bigint("example_big_int", { mode: "number" }).notNull().default(Number.MAX_SAFE_INTEGER),\n exampleBiggerInt: bigint("example_bigger_int", { mode: "bigint" }).notNull().default(0n),\n exampleBoolean: boolean("example_boolean").notNull().default(false),\n exampleText: text("example_text").notNull().default("Lorem ipsum dolor sit amet"),\n exampleVarchar: varchar("example_varchar", { length: 16 }).notNull().default("hello world"),\n examplechar: char("example_char", { length: 16 }).notNull().default("hello world "),\n // ...\n createdAt: timestamp("created_at", { withTimezone: true, precision: 3 }).default(sql`CURRENT_TIMESTAMP`).notNull(),\n updatedAt: timestamp("updated_at", { withTimezone: true, precision: 3 }).default(sql`CURRENT_TIMESTAMP`).notNull(),\n}, table => ({\n createdAtIdx: index().on(table.createdAt),\n}));\n\nexport const relationsOf{{ pascalCasePlural }} = relations({{ camelCasePlural }}, ({ many, one }) => ({\n master: one(masters, {\n fields: [{{ camelCasePlural }}.masterId],\n references: [masters.id],\n }),\n slaves: many(slaves),\n}));\n';
319
326
 
320
327
  // src/templates/schema.ts
321
- function generateSchema(modelName2) {
328
+ function generateSchema(modelName2, firstField2) {
322
329
  const template = getTemplate(schema_default);
323
330
  return template({
324
331
  camelCasePlural: camelCase2(modelName2, true),
325
332
  pascalCasePlural: pascalCase2(modelName2, true),
326
- snakeCasePlural: snakeCase2(modelName2, true)
333
+ snakeCasePlural: snakeCase2(modelName2, true),
334
+ firstField: firstField2
327
335
  });
328
336
  }
329
337
 
330
338
  // src/index.ts
331
- var [modelName] = await askQuestions(["Enter Model Name: "]);
332
- await saveFile(`src/app/${kebabCase2(modelName, true)}`, "route.ts", generateApiRoute(modelName));
333
- await saveFile(`src/app/${kebabCase2(modelName, true)}/[id]`, "route.ts", generateApiRouteWithId(modelName));
334
- await saveFile("src/database/schema", `${kebabCase2(modelName, true)}.ts`, generateSchema(modelName));
335
- await saveFile("src/models", `${kebabCase2(modelName, false)}.ts`, generateModel(modelName));
336
- await saveFile("src/operations", `get${pascalCase2(modelName, true)}.ts`, generateReadAllOperation(modelName));
337
- await saveFile("src/operations", `get${pascalCase2(modelName, true)}.test.ts`, generateReadAllTest(modelName));
338
- await saveFile("src/operations", `create${pascalCase2(modelName, false)}.ts`, generateCreateOperation(modelName));
339
- await saveFile("src/operations", `create${pascalCase2(modelName, false)}.test.ts`, generateCreateTest(modelName));
340
- await saveFile("src/operations", `get${pascalCase2(modelName, false)}.ts`, generateReadOperation(modelName));
341
- await saveFile("src/operations", `get${pascalCase2(modelName, false)}.test.ts`, generateReadTest(modelName));
342
- await saveFile("src/operations", `update${pascalCase2(modelName, false)}.ts`, generateUpdateOperation(modelName));
343
- await saveFile("src/operations", `update${pascalCase2(modelName, false)}.test.ts`, generateUpdateTest(modelName));
344
- await saveFile("src/operations", `delete${pascalCase2(modelName, false)}.ts`, generateDeleteOperation(modelName));
345
- await saveFile("src/operations", `delete${pascalCase2(modelName, false)}.test.ts`, generateDeleteTest(modelName));
339
+ var { modelName } = await prompts({
340
+ type: "text",
341
+ name: "modelName",
342
+ message: "Enter model name"
343
+ });
344
+ var { orm } = await prompts({
345
+ type: "select",
346
+ name: "orm",
347
+ message: "Pick an ORM",
348
+ choices: [
349
+ { title: "Drizzle", description: "a headless TypeScript ORM with a head. \u{1F432}", value: "drizzle" },
350
+ { title: "none", value: null, disabled: true }
351
+ ],
352
+ initial: 0
353
+ });
354
+ var { firstField } = await prompts({
355
+ type: "text",
356
+ name: "firstField",
357
+ message: "Enter name of the first field",
358
+ initial: "name"
359
+ });
360
+ var { operationTests } = await prompts({
361
+ type: "toggle",
362
+ name: "operationTests",
363
+ message: "Do you want to include operation tests?",
364
+ initial: true,
365
+ active: "yes",
366
+ inactive: "no"
367
+ });
368
+ if (modelName && firstField) {
369
+ await saveFile(`src/app/${kebabCase2(modelName, true)}`, "route.ts", generateApiRoute(modelName));
370
+ await saveFile(`src/app/${kebabCase2(modelName, true)}/[id]`, "route.ts", generateApiRouteWithId(modelName));
371
+ if (orm === "drizzle") {
372
+ await saveFile("src/database/schema", `${kebabCase2(modelName, true)}.ts`, generateSchema(modelName, firstField));
373
+ await appendFile("src/database/schema", "index.ts", `export * from "./${kebabCase2(modelName, true)}";`);
374
+ }
375
+ await saveFile("src/models", `${kebabCase2(modelName, false)}.ts`, generateModel(modelName, firstField));
376
+ await saveFile("src/operations", `get${pascalCase2(modelName, true)}.ts`, generateReadAllOperation(modelName));
377
+ await saveFile("src/operations", `create${pascalCase2(modelName, false)}.ts`, generateCreateOperation(modelName));
378
+ await saveFile("src/operations", `get${pascalCase2(modelName, false)}.ts`, generateReadOperation(modelName));
379
+ await saveFile("src/operations", `update${pascalCase2(modelName, false)}.ts`, generateUpdateOperation(modelName));
380
+ await saveFile("src/operations", `delete${pascalCase2(modelName, false)}.ts`, generateDeleteOperation(modelName));
381
+ if (operationTests) {
382
+ await saveFile("src/operations", `get${pascalCase2(modelName, true)}.test.ts`, generateReadAllTest(modelName));
383
+ await saveFile("src/operations", `create${pascalCase2(modelName, false)}.test.ts`, generateCreateTest(modelName));
384
+ await saveFile("src/operations", `get${pascalCase2(modelName, false)}.test.ts`, generateReadTest(modelName));
385
+ await saveFile("src/operations", `update${pascalCase2(modelName, false)}.test.ts`, generateUpdateTest(modelName, firstField));
386
+ await saveFile("src/operations", `delete${pascalCase2(modelName, false)}.test.ts`, generateDeleteTest(modelName));
387
+ }
388
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omer-x/next-openapi-scaffold-generator",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Generates model, schema and API routes for CRUD operations",
5
5
  "keywords": [
6
6
  "next",
@@ -54,12 +54,14 @@
54
54
  "dependencies": {
55
55
  "change-case": "^5.4.4",
56
56
  "handlebars": "^4.7.8",
57
- "pluralize": "^8.0.0"
57
+ "pluralize": "^8.0.0",
58
+ "prompts": "^2.4.2"
58
59
  },
59
60
  "devDependencies": {
60
61
  "@omer-x/eslint-config": "^1.0.7",
61
62
  "@types/node": "^22.5.0",
62
63
  "@types/pluralize": "^0.0.33",
64
+ "@types/prompts": "^2.4.9",
63
65
  "conventional-github-releaser": "^3.1.5",
64
66
  "esbuild-plugin-inline-import": "^1.1.0",
65
67
  "eslint": "^8.57.0",