@mandujs/core 0.17.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,139 @@
1
+ /**
2
+ * Resource Schema Parser
3
+ * Parse and validate resource schema files
4
+ */
5
+
6
+ import type { ResourceDefinition } from "./schema";
7
+ import { validateResourceDefinition } from "./schema";
8
+ import path from "path";
9
+
10
+ // ============================================
11
+ // Parser Result
12
+ // ============================================
13
+
14
+ export interface ParsedResource {
15
+ /** 원본 정의 */
16
+ definition: ResourceDefinition;
17
+ /** 파일 경로 */
18
+ filePath: string;
19
+ /** 파일명 (확장자 제외) */
20
+ fileName: string;
21
+ /** 리소스 이름 */
22
+ resourceName: string;
23
+ }
24
+
25
+ // ============================================
26
+ // Parse Resource Schema
27
+ // ============================================
28
+
29
+ /**
30
+ * Parse resource schema from file
31
+ *
32
+ * @param filePath - Absolute path to resource schema file
33
+ * @returns Parsed resource with metadata
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * const parsed = await parseResourceSchema("/path/to/spec/resources/user.resource.ts");
38
+ * console.log(parsed.resourceName); // "user"
39
+ * console.log(parsed.definition.fields); // { id: {...}, email: {...}, ... }
40
+ * ```
41
+ */
42
+ export async function parseResourceSchema(filePath: string): Promise<ParsedResource> {
43
+ // Validate file path
44
+ if (!filePath.endsWith(".resource.ts")) {
45
+ throw new Error(
46
+ `Invalid resource schema file: "${filePath}". Must end with ".resource.ts"`
47
+ );
48
+ }
49
+
50
+ // Extract file name
51
+ const fileName = path.basename(filePath, ".resource.ts");
52
+
53
+ // Import the resource definition
54
+ let definition: ResourceDefinition;
55
+ try {
56
+ const module = await import(filePath);
57
+ definition = module.default;
58
+
59
+ if (!definition) {
60
+ throw new Error(
61
+ `Resource schema file "${filePath}" must export a default ResourceDefinition`
62
+ );
63
+ }
64
+ } catch (error) {
65
+ throw new Error(
66
+ `Failed to import resource schema "${filePath}": ${
67
+ error instanceof Error ? error.message : String(error)
68
+ }`
69
+ );
70
+ }
71
+
72
+ // Validate definition
73
+ try {
74
+ validateResourceDefinition(definition);
75
+ } catch (error) {
76
+ throw new Error(
77
+ `Invalid resource schema in "${filePath}": ${
78
+ error instanceof Error ? error.message : String(error)
79
+ }`
80
+ );
81
+ }
82
+
83
+ return {
84
+ definition,
85
+ filePath,
86
+ fileName,
87
+ resourceName: definition.name,
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Parse multiple resource schemas
93
+ *
94
+ * @param filePaths - Array of absolute paths to resource schema files
95
+ * @returns Array of parsed resources
96
+ */
97
+ export async function parseResourceSchemas(filePaths: string[]): Promise<ParsedResource[]> {
98
+ const results = await Promise.allSettled(filePaths.map(parseResourceSchema));
99
+
100
+ const errors: string[] = [];
101
+ const parsed: ParsedResource[] = [];
102
+
103
+ for (let i = 0; i < results.length; i++) {
104
+ const result = results[i];
105
+ if (result.status === "fulfilled") {
106
+ parsed.push(result.value);
107
+ } else {
108
+ errors.push(`${filePaths[i]}: ${result.reason}`);
109
+ }
110
+ }
111
+
112
+ if (errors.length > 0) {
113
+ throw new Error(`Failed to parse resource schemas:\n${errors.join("\n")}`);
114
+ }
115
+
116
+ return parsed;
117
+ }
118
+
119
+ /**
120
+ * Validate resource name uniqueness
121
+ */
122
+ export function validateResourceUniqueness(resources: ParsedResource[]): void {
123
+ const names = new Set<string>();
124
+ const duplicates: string[] = [];
125
+
126
+ for (const resource of resources) {
127
+ const name = resource.resourceName;
128
+ if (names.has(name)) {
129
+ duplicates.push(name);
130
+ }
131
+ names.add(name);
132
+ }
133
+
134
+ if (duplicates.length > 0) {
135
+ throw new Error(
136
+ `Duplicate resource names found: ${duplicates.join(", ")}. Resource names must be unique.`
137
+ );
138
+ }
139
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Resource Schema Definition
3
+ * Resource-Centric Architecture의 핵심 스키마 정의
4
+ */
5
+
6
+ import { z } from "zod";
7
+
8
+ // ============================================
9
+ // Field Types
10
+ // ============================================
11
+
12
+ export const FieldTypes = [
13
+ "string",
14
+ "number",
15
+ "boolean",
16
+ "date",
17
+ "uuid",
18
+ "email",
19
+ "url",
20
+ "json",
21
+ "array",
22
+ "object",
23
+ ] as const;
24
+
25
+ export type FieldType = (typeof FieldTypes)[number];
26
+
27
+ // ============================================
28
+ // Field Definition
29
+ // ============================================
30
+
31
+ export interface ResourceField {
32
+ /** 필드 타입 */
33
+ type: FieldType;
34
+ /** 필수 여부 */
35
+ required?: boolean;
36
+ /** 기본값 */
37
+ default?: unknown;
38
+ /** 설명 */
39
+ description?: string;
40
+ /** 배열 타입인 경우 요소 타입 */
41
+ items?: FieldType;
42
+ /** 커스텀 Zod 스키마 (고급 사용) */
43
+ schema?: z.ZodType<any>;
44
+ }
45
+
46
+ // ============================================
47
+ // Resource Options
48
+ // ============================================
49
+
50
+ export interface ResourceOptions {
51
+ /** 리소스 설명 */
52
+ description?: string;
53
+ /** API 태그 */
54
+ tags?: string[];
55
+ /** 자동 복수형 사용 여부 (기본: true) */
56
+ autoPlural?: boolean;
57
+ /** 커스텀 복수형 이름 */
58
+ pluralName?: string;
59
+ /** 활성화할 엔드포인트 */
60
+ endpoints?: {
61
+ list?: boolean;
62
+ get?: boolean;
63
+ create?: boolean;
64
+ update?: boolean;
65
+ delete?: boolean;
66
+ };
67
+ /** 인증 필요 여부 */
68
+ auth?: boolean;
69
+ /** 페이지네이션 설정 */
70
+ pagination?: {
71
+ defaultLimit?: number;
72
+ maxLimit?: number;
73
+ };
74
+ }
75
+
76
+ // ============================================
77
+ // Resource Definition
78
+ // ============================================
79
+
80
+ export interface ResourceDefinition {
81
+ /** 리소스 이름 (단수형) */
82
+ name: string;
83
+ /** 필드 정의 */
84
+ fields: Record<string, ResourceField>;
85
+ /** 옵션 */
86
+ options?: ResourceOptions;
87
+ }
88
+
89
+ // ============================================
90
+ // defineResource API
91
+ // ============================================
92
+
93
+ /**
94
+ * Define a resource with fields and options
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * const UserResource = defineResource({
99
+ * name: "user",
100
+ * fields: {
101
+ * id: { type: "uuid", required: true },
102
+ * email: { type: "email", required: true },
103
+ * name: { type: "string", required: true },
104
+ * createdAt: { type: "date", required: true },
105
+ * },
106
+ * options: {
107
+ * description: "User management API",
108
+ * tags: ["users"],
109
+ * endpoints: {
110
+ * list: true,
111
+ * get: true,
112
+ * create: true,
113
+ * update: true,
114
+ * delete: true,
115
+ * },
116
+ * },
117
+ * });
118
+ * ```
119
+ */
120
+ export function defineResource(definition: ResourceDefinition): ResourceDefinition {
121
+ // Validation
122
+ validateResourceDefinition(definition);
123
+
124
+ // Default options
125
+ const options: ResourceOptions = {
126
+ autoPlural: true,
127
+ endpoints: {
128
+ list: true,
129
+ get: true,
130
+ create: true,
131
+ update: true,
132
+ delete: true,
133
+ },
134
+ pagination: {
135
+ defaultLimit: 10,
136
+ maxLimit: 100,
137
+ },
138
+ ...definition.options,
139
+ };
140
+
141
+ return {
142
+ ...definition,
143
+ options,
144
+ };
145
+ }
146
+
147
+ // ============================================
148
+ // Validation
149
+ // ============================================
150
+
151
+ /**
152
+ * Validate resource definition
153
+ */
154
+ export function validateResourceDefinition(definition: ResourceDefinition): void {
155
+ // Name validation
156
+ if (!definition.name) {
157
+ throw new Error("Resource name is required");
158
+ }
159
+
160
+ if (!/^[a-z][a-z0-9_]*$/i.test(definition.name)) {
161
+ throw new Error(
162
+ `Invalid resource name: "${definition.name}". Must start with a letter and contain only letters, numbers, and underscores.`
163
+ );
164
+ }
165
+
166
+ // Fields validation
167
+ if (!definition.fields || Object.keys(definition.fields).length === 0) {
168
+ throw new Error(`Resource "${definition.name}" must have at least one field`);
169
+ }
170
+
171
+ for (const [fieldName, field] of Object.entries(definition.fields)) {
172
+ validateField(definition.name, fieldName, field);
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Validate individual field
178
+ */
179
+ function validateField(resourceName: string, fieldName: string, field: ResourceField): void {
180
+ // Field name validation
181
+ if (!/^[a-z][a-z0-9_]*$/i.test(fieldName)) {
182
+ throw new Error(
183
+ `Invalid field name: "${fieldName}" in resource "${resourceName}". Must start with a letter and contain only letters, numbers, and underscores.`
184
+ );
185
+ }
186
+
187
+ // Type validation
188
+ if (!FieldTypes.includes(field.type)) {
189
+ throw new Error(
190
+ `Invalid field type: "${field.type}" for field "${fieldName}" in resource "${resourceName}". Must be one of: ${FieldTypes.join(", ")}`
191
+ );
192
+ }
193
+
194
+ // Array type requires items
195
+ if (field.type === "array" && !field.items && !field.schema) {
196
+ throw new Error(
197
+ `Field "${fieldName}" in resource "${resourceName}" is array type but missing "items" property`
198
+ );
199
+ }
200
+ }
201
+
202
+ // ============================================
203
+ // Helper Functions
204
+ // ============================================
205
+
206
+ /**
207
+ * Get plural name for resource
208
+ */
209
+ export function getPluralName(definition: ResourceDefinition): string {
210
+ if (definition.options?.pluralName) {
211
+ return definition.options.pluralName;
212
+ }
213
+
214
+ if (definition.options?.autoPlural === false) {
215
+ return definition.name;
216
+ }
217
+
218
+ // Simple pluralization: add 's'
219
+ // TODO: Add more sophisticated pluralization rules if needed
220
+ return `${definition.name}s`;
221
+ }
222
+
223
+ /**
224
+ * Get enabled endpoints
225
+ */
226
+ export function getEnabledEndpoints(definition: ResourceDefinition): string[] {
227
+ const endpoints = definition.options?.endpoints ?? {
228
+ list: true,
229
+ get: true,
230
+ create: true,
231
+ update: true,
232
+ delete: true,
233
+ };
234
+
235
+ return Object.entries(endpoints)
236
+ .filter(([_, enabled]) => enabled)
237
+ .map(([name]) => name);
238
+ }
239
+
240
+ /**
241
+ * Check if field is required
242
+ */
243
+ export function isFieldRequired(field: ResourceField): boolean {
244
+ return field.required ?? false;
245
+ }
246
+
247
+ /**
248
+ * Get field default value
249
+ */
250
+ export function getFieldDefault(field: ResourceField): unknown | undefined {
251
+ return field.default;
252
+ }