@mandujs/core 0.16.0 → 0.18.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.
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Resource Contract Generator
3
+ * Generate Zod contract from resource definition
4
+ */
5
+
6
+ import type { ResourceDefinition, ResourceField } from "../schema";
7
+ import { getPluralName, getEnabledEndpoints } from "../schema";
8
+
9
+ /**
10
+ * Generate contract file for resource
11
+ *
12
+ * @returns Contract file content
13
+ */
14
+ export function generateResourceContract(definition: ResourceDefinition): string {
15
+ const resourceName = definition.name;
16
+ const pascalName = toPascalCase(resourceName);
17
+ const pluralName = getPluralName(definition);
18
+ const endpoints = getEnabledEndpoints(definition);
19
+
20
+ // Generate schema definitions
21
+ const schemaDefinitions = generateSchemaDefinitions(definition);
22
+
23
+ // Generate request schemas
24
+ const requestSchemas = generateRequestSchemas(definition, endpoints);
25
+
26
+ // Generate response schemas
27
+ const responseSchemas = generateResponseSchemas(definition, pascalName);
28
+
29
+ return `// 📜 Mandu Resource Contract - ${resourceName}
30
+ // Auto-generated from resource definition
31
+ // DO NOT EDIT - Regenerated on every \`mandu generate\`
32
+
33
+ import { z } from "zod";
34
+ import { Mandu } from "@mandujs/core";
35
+
36
+ // ============================================
37
+ // 🥟 Schema Definitions
38
+ // ============================================
39
+
40
+ ${schemaDefinitions}
41
+
42
+ // ============================================
43
+ // 📜 Contract Definition
44
+ // ============================================
45
+
46
+ export default Mandu.contract({
47
+ description: "${definition.options?.description || `${pascalName} API`}",
48
+ tags: ${JSON.stringify(definition.options?.tags || [resourceName])},
49
+
50
+ request: {
51
+ ${requestSchemas}
52
+ },
53
+
54
+ response: {
55
+ ${responseSchemas}
56
+ },
57
+ });
58
+ `;
59
+ }
60
+
61
+ /**
62
+ * Generate schema definitions for fields
63
+ */
64
+ function generateSchemaDefinitions(definition: ResourceDefinition): string {
65
+ const pascalName = toPascalCase(definition.name);
66
+ const fields = Object.entries(definition.fields);
67
+
68
+ // Generate individual field schemas
69
+ const fieldSchemas = fields.map(([name, field]) => {
70
+ const zodSchema = generateZodSchema(name, field);
71
+ return ` ${name}: ${zodSchema},`;
72
+ });
73
+
74
+ return `/**
75
+ * ${pascalName} Schema
76
+ */
77
+ const ${pascalName}Schema = z.object({
78
+ ${fieldSchemas.join("\n")}
79
+ });
80
+
81
+ /**
82
+ * ${pascalName} Create Schema (exclude id, createdAt, updatedAt)
83
+ */
84
+ const ${pascalName}CreateSchema = ${pascalName}Schema.omit({
85
+ id: true,
86
+ createdAt: true,
87
+ updatedAt: true,
88
+ });
89
+
90
+ /**
91
+ * ${pascalName} Update Schema (all fields optional except id)
92
+ */
93
+ const ${pascalName}UpdateSchema = ${pascalName}Schema.partial().required({ id: true });`;
94
+ }
95
+
96
+ /**
97
+ * Generate Zod schema for a field
98
+ */
99
+ function generateZodSchema(fieldName: string, field: ResourceField): string {
100
+ // Use custom schema if provided
101
+ if (field.schema) {
102
+ return "z.unknown() /* Custom schema */";
103
+ }
104
+
105
+ let schema: string;
106
+
107
+ switch (field.type) {
108
+ case "string":
109
+ schema = "z.string()";
110
+ break;
111
+ case "number":
112
+ schema = "z.number()";
113
+ break;
114
+ case "boolean":
115
+ schema = "z.boolean()";
116
+ break;
117
+ case "date":
118
+ schema = "z.string().datetime()";
119
+ break;
120
+ case "uuid":
121
+ schema = "z.string().uuid()";
122
+ break;
123
+ case "email":
124
+ schema = "z.string().email()";
125
+ break;
126
+ case "url":
127
+ schema = "z.string().url()";
128
+ break;
129
+ case "json":
130
+ schema = "z.record(z.unknown())";
131
+ break;
132
+ case "array":
133
+ const itemType = field.items || "unknown";
134
+ schema = `z.array(${generateZodTypeFromFieldType(itemType)})`;
135
+ break;
136
+ case "object":
137
+ schema = "z.record(z.unknown())";
138
+ break;
139
+ default:
140
+ schema = "z.unknown()";
141
+ }
142
+
143
+ // Add optional/required
144
+ if (!field.required) {
145
+ schema += ".optional()";
146
+ }
147
+
148
+ // Add default
149
+ if (field.default !== undefined) {
150
+ const defaultValue = JSON.stringify(field.default);
151
+ schema += `.default(${defaultValue})`;
152
+ }
153
+
154
+ // Add description
155
+ if (field.description) {
156
+ schema += `.describe("${field.description}")`;
157
+ }
158
+
159
+ return schema;
160
+ }
161
+
162
+ /**
163
+ * Generate Zod type from FieldType
164
+ */
165
+ function generateZodTypeFromFieldType(type: string): string {
166
+ switch (type) {
167
+ case "string":
168
+ return "z.string()";
169
+ case "number":
170
+ return "z.number()";
171
+ case "boolean":
172
+ return "z.boolean()";
173
+ case "date":
174
+ return "z.string().datetime()";
175
+ case "uuid":
176
+ return "z.string().uuid()";
177
+ case "email":
178
+ return "z.string().email()";
179
+ case "url":
180
+ return "z.string().url()";
181
+ default:
182
+ return "z.unknown()";
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Generate request schemas based on enabled endpoints
188
+ */
189
+ function generateRequestSchemas(definition: ResourceDefinition, endpoints: string[]): string {
190
+ const pascalName = toPascalCase(definition.name);
191
+ const schemas: string[] = [];
192
+
193
+ if (endpoints.includes("list")) {
194
+ const defaultLimit = definition.options?.pagination?.defaultLimit || 10;
195
+ const maxLimit = definition.options?.pagination?.maxLimit || 100;
196
+
197
+ schemas.push(` GET: {
198
+ query: z.object({
199
+ page: z.coerce.number().int().min(1).default(1),
200
+ limit: z.coerce.number().int().min(1).max(${maxLimit}).default(${defaultLimit}),
201
+ }),
202
+ }`);
203
+ }
204
+
205
+ if (endpoints.includes("create")) {
206
+ schemas.push(` POST: {
207
+ body: ${pascalName}CreateSchema,
208
+ }`);
209
+ }
210
+
211
+ if (endpoints.includes("update")) {
212
+ schemas.push(` PUT: {
213
+ body: ${pascalName}UpdateSchema,
214
+ }`);
215
+ }
216
+
217
+ if (endpoints.includes("delete")) {
218
+ schemas.push(` DELETE: {
219
+ // No body for DELETE
220
+ }`);
221
+ }
222
+
223
+ return schemas.join(",\n\n");
224
+ }
225
+
226
+ /**
227
+ * Generate response schemas
228
+ */
229
+ function generateResponseSchemas(definition: ResourceDefinition, pascalName: string): string {
230
+ return ` 200: z.object({
231
+ data: z.union([${pascalName}Schema, z.array(${pascalName}Schema)]),
232
+ pagination: z.object({
233
+ page: z.number(),
234
+ limit: z.number(),
235
+ total: z.number(),
236
+ }).optional(),
237
+ }),
238
+ 201: z.object({
239
+ data: ${pascalName}Schema,
240
+ }),
241
+ 400: z.object({
242
+ error: z.string(),
243
+ details: z.array(z.object({
244
+ type: z.string(),
245
+ issues: z.array(z.object({
246
+ path: z.string(),
247
+ message: z.string(),
248
+ })),
249
+ })).optional(),
250
+ }),
251
+ 404: z.object({
252
+ error: z.string(),
253
+ })`;
254
+ }
255
+
256
+ /**
257
+ * Convert string to PascalCase
258
+ */
259
+ function toPascalCase(str: string): string {
260
+ return str
261
+ .split(/[-_]/)
262
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
263
+ .join("");
264
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Resource Slot Generator
3
+ * Generate slot templates for resource endpoints
4
+ */
5
+
6
+ import type { ResourceDefinition } from "../schema";
7
+ import { getPluralName, getEnabledEndpoints } from "../schema";
8
+
9
+ /**
10
+ * Generate slot file for resource
11
+ * IMPORTANT: This should only be generated ONCE - never overwrite existing slots!
12
+ *
13
+ * @returns Slot file content
14
+ */
15
+ export function generateResourceSlot(definition: ResourceDefinition): string {
16
+ const resourceName = definition.name;
17
+ const pascalName = toPascalCase(resourceName);
18
+ const pluralName = getPluralName(definition);
19
+ const endpoints = getEnabledEndpoints(definition);
20
+
21
+ // Generate endpoint handlers
22
+ const handlers = generateHandlers(definition, endpoints, pascalName);
23
+
24
+ return `// 🥟 Mandu Filling - ${resourceName} Resource
25
+ // Pattern: /api/${pluralName}
26
+ // 이 파일에서 비즈니스 로직을 구현하세요.
27
+
28
+ import { Mandu } from "@mandujs/core";
29
+ import contract from "../contracts/${resourceName}.contract";
30
+
31
+ export default Mandu.filling()
32
+ ${handlers}
33
+
34
+ // 💡 Contract 기반 사용법:
35
+ // ctx.input(contract, "GET") - Contract로 요청 검증 + 정규화
36
+ // ctx.output(contract, 200, data) - Contract로 응답 검증
37
+ // ctx.okContract(contract, data) - 200 OK (Contract 검증)
38
+ // ctx.createdContract(contract, data) - 201 Created (Contract 검증)
39
+ //
40
+ // 💡 데이터베이스 연동 예시:
41
+ // const { data } = await db.select().from(${pluralName}).where(eq(${pluralName}.id, id));
42
+ // return ctx.output(contract, 200, { data });
43
+ `;
44
+ }
45
+
46
+ /**
47
+ * Generate handlers for enabled endpoints
48
+ */
49
+ function generateHandlers(
50
+ definition: ResourceDefinition,
51
+ endpoints: string[],
52
+ pascalName: string
53
+ ): string {
54
+ const handlers: string[] = [];
55
+
56
+ if (endpoints.includes("list")) {
57
+ handlers.push(generateListHandler(definition, pascalName));
58
+ }
59
+
60
+ if (endpoints.includes("get")) {
61
+ handlers.push(generateGetHandler(definition, pascalName));
62
+ }
63
+
64
+ if (endpoints.includes("create")) {
65
+ handlers.push(generateCreateHandler(definition, pascalName));
66
+ }
67
+
68
+ if (endpoints.includes("update")) {
69
+ handlers.push(generateUpdateHandler(definition, pascalName));
70
+ }
71
+
72
+ if (endpoints.includes("delete")) {
73
+ handlers.push(generateDeleteHandler(definition, pascalName));
74
+ }
75
+
76
+ return handlers.join("\n\n");
77
+ }
78
+
79
+ /**
80
+ * Generate LIST handler (GET /api/resources)
81
+ */
82
+ function generateListHandler(definition: ResourceDefinition, pascalName: string): string {
83
+ return ` // 📋 List ${pascalName}s
84
+ .get(async (ctx) => {
85
+ const input = await ctx.input(contract, "GET", ctx.params);
86
+ const { page, limit } = input;
87
+
88
+ // TODO: Implement database query
89
+ // const offset = (page - 1) * limit;
90
+ // const items = await db.select().from(${definition.name}s).limit(limit).offset(offset);
91
+ // const total = await db.select({ count: count() }).from(${definition.name}s);
92
+
93
+ const mockData = {
94
+ data: [], // Replace with actual data
95
+ pagination: {
96
+ page,
97
+ limit,
98
+ total: 0,
99
+ },
100
+ };
101
+
102
+ return ctx.output(contract, 200, mockData);
103
+ })`;
104
+ }
105
+
106
+ /**
107
+ * Generate GET handler (GET /api/resources/:id)
108
+ */
109
+ function generateGetHandler(definition: ResourceDefinition, pascalName: string): string {
110
+ return ` // 📄 Get Single ${pascalName}
111
+ .get(async (ctx) => {
112
+ const { id } = ctx.params;
113
+
114
+ // TODO: Implement database query
115
+ // const item = await db.select().from(${definition.name}s).where(eq(${definition.name}s.id, id)).limit(1);
116
+ // if (!item) return ctx.notFound("${pascalName} not found");
117
+
118
+ const mockData = {
119
+ data: { id, message: "${pascalName} details" }, // Replace with actual data
120
+ };
121
+
122
+ return ctx.output(contract, 200, mockData);
123
+ })`;
124
+ }
125
+
126
+ /**
127
+ * Generate CREATE handler (POST /api/resources)
128
+ */
129
+ function generateCreateHandler(definition: ResourceDefinition, pascalName: string): string {
130
+ return ` // ➕ Create ${pascalName}
131
+ .post(async (ctx) => {
132
+ const input = await ctx.input(contract, "POST", ctx.params);
133
+
134
+ // TODO: Implement database insertion
135
+ // const [created] = await db.insert(${definition.name}s).values(input).returning();
136
+
137
+ const mockData = {
138
+ data: { id: "new-id", ...input }, // Replace with actual created data
139
+ };
140
+
141
+ return ctx.output(contract, 201, mockData);
142
+ })`;
143
+ }
144
+
145
+ /**
146
+ * Generate UPDATE handler (PUT /api/resources/:id)
147
+ */
148
+ function generateUpdateHandler(definition: ResourceDefinition, pascalName: string): string {
149
+ return ` // ✏️ Update ${pascalName}
150
+ .put(async (ctx) => {
151
+ const { id } = ctx.params;
152
+ const input = await ctx.input(contract, "PUT", ctx.params);
153
+
154
+ // TODO: Implement database update
155
+ // const [updated] = await db.update(${definition.name}s)
156
+ // .set(input)
157
+ // .where(eq(${definition.name}s.id, id))
158
+ // .returning();
159
+ // if (!updated) return ctx.notFound("${pascalName} not found");
160
+
161
+ const mockData = {
162
+ data: { id, ...input }, // Replace with actual updated data
163
+ };
164
+
165
+ return ctx.output(contract, 200, mockData);
166
+ })`;
167
+ }
168
+
169
+ /**
170
+ * Generate DELETE handler (DELETE /api/resources/:id)
171
+ */
172
+ function generateDeleteHandler(definition: ResourceDefinition, pascalName: string): string {
173
+ return ` // 🗑️ Delete ${pascalName}
174
+ .delete(async (ctx) => {
175
+ const { id } = ctx.params;
176
+
177
+ // TODO: Implement database deletion
178
+ // const deleted = await db.delete(${definition.name}s).where(eq(${definition.name}s.id, id));
179
+ // if (!deleted) return ctx.notFound("${pascalName} not found");
180
+
181
+ return ctx.output(contract, 200, { data: { message: "${pascalName} deleted" } });
182
+ })`;
183
+ }
184
+
185
+ /**
186
+ * Convert string to PascalCase
187
+ */
188
+ function toPascalCase(str: string): string {
189
+ return str
190
+ .split(/[-_]/)
191
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
192
+ .join("");
193
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Resource TypeScript Types Generator
3
+ * Generate TypeScript type definitions from resource
4
+ */
5
+
6
+ import type { ResourceDefinition } from "../schema";
7
+
8
+ /**
9
+ * Generate TypeScript types for resource
10
+ *
11
+ * @returns Types file content
12
+ */
13
+ export function generateResourceTypes(definition: ResourceDefinition): string {
14
+ const pascalName = toPascalCase(definition.name);
15
+ const contractPath = `../contracts/${definition.name}.contract`;
16
+
17
+ return `// 🎯 Mandu Resource Types - ${definition.name}
18
+ // Auto-generated from resource definition
19
+ // DO NOT EDIT - Regenerated on every \`mandu generate\`
20
+
21
+ import type { InferContract, InferQuery, InferBody, InferParams, InferResponse } from "@mandujs/core";
22
+ import contract from "${contractPath}";
23
+
24
+ /**
25
+ * Full contract type for ${definition.name}
26
+ */
27
+ export type ${pascalName}Contract = InferContract<typeof contract>;
28
+
29
+ // ============================================
30
+ // Request Types
31
+ // ============================================
32
+
33
+ /** GET query parameters */
34
+ export type ${pascalName}GetQuery = InferQuery<typeof contract, "GET">;
35
+
36
+ /** POST request body */
37
+ export type ${pascalName}PostBody = InferBody<typeof contract, "POST">;
38
+
39
+ /** PUT request body */
40
+ export type ${pascalName}PutBody = InferBody<typeof contract, "PUT">;
41
+
42
+ /** PATCH request body */
43
+ export type ${pascalName}PatchBody = InferBody<typeof contract, "PATCH">;
44
+
45
+ /** DELETE query parameters */
46
+ export type ${pascalName}DeleteQuery = InferQuery<typeof contract, "DELETE">;
47
+
48
+ /** Path parameters (if any) */
49
+ export type ${pascalName}Params = InferParams<typeof contract, "GET">;
50
+
51
+ // ============================================
52
+ // Response Types
53
+ // ============================================
54
+
55
+ /** 200 OK response */
56
+ export type ${pascalName}Response200 = InferResponse<typeof contract, 200>;
57
+
58
+ /** 201 Created response */
59
+ export type ${pascalName}Response201 = InferResponse<typeof contract, 201>;
60
+
61
+ /** 204 No Content response */
62
+ export type ${pascalName}Response204 = InferResponse<typeof contract, 204>;
63
+
64
+ /** 400 Bad Request response */
65
+ export type ${pascalName}Response400 = InferResponse<typeof contract, 400>;
66
+
67
+ /** 404 Not Found response */
68
+ export type ${pascalName}Response404 = InferResponse<typeof contract, 404>;
69
+
70
+ // Re-export contract for runtime use
71
+ export { contract };
72
+ `;
73
+ }
74
+
75
+ /**
76
+ * Convert string to PascalCase
77
+ */
78
+ function toPascalCase(str: string): string {
79
+ return str
80
+ .split(/[-_]/)
81
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
82
+ .join("");
83
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Resource-Centric Architecture
3
+ * Public API exports
4
+ */
5
+
6
+ // Schema API
7
+ export {
8
+ defineResource,
9
+ validateResourceDefinition,
10
+ getPluralName,
11
+ getEnabledEndpoints,
12
+ isFieldRequired,
13
+ getFieldDefault,
14
+ FieldTypes,
15
+ } from "./schema";
16
+
17
+ export type {
18
+ ResourceDefinition,
19
+ ResourceField,
20
+ ResourceOptions,
21
+ FieldType,
22
+ } from "./schema";
23
+
24
+ // Parser API
25
+ export { parseResourceSchema, parseResourceSchemas, validateResourceUniqueness } from "./parser";
26
+
27
+ export type { ParsedResource } from "./parser";
28
+
29
+ // Generator API
30
+ export {
31
+ generateResourceArtifacts,
32
+ generateResourcesArtifacts,
33
+ logGeneratorResult,
34
+ } from "./generator";
35
+
36
+ export type { GeneratorOptions, GeneratorResult } from "./generator";
37
+
38
+ // Individual Generators (for advanced use)
39
+ export { generateResourceContract } from "./generators/contract";
40
+ export { generateResourceTypes } from "./generators/types";
41
+ export { generateResourceSlot } from "./generators/slot";
42
+ export { generateResourceClient } from "./generators/client";