@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.
- package/bin/index.js +101 -58
- 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(
|
|
59
|
-
return Handlebars.compile(
|
|
59
|
+
function getTemplate(input) {
|
|
60
|
+
return Handlebars.compile(input);
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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(
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
318
|
-
var schema_default = 'import { relations, sql } from "drizzle-orm";\nimport { bigint, boolean, char, index, integer, pgTable,
|
|
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
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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.
|
|
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",
|