@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.
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/paths.ts +16 -0
- package/src/resource/__tests__/backward-compat.test.ts +302 -0
- package/src/resource/__tests__/edge-cases.test.ts +514 -0
- package/src/resource/__tests__/fixtures.ts +203 -0
- package/src/resource/__tests__/generator.test.ts +324 -0
- package/src/resource/__tests__/performance.test.ts +311 -0
- package/src/resource/__tests__/schema.test.ts +184 -0
- package/src/resource/generator.ts +277 -0
- package/src/resource/generators/client.ts +199 -0
- package/src/resource/generators/contract.ts +264 -0
- package/src/resource/generators/slot.ts +193 -0
- package/src/resource/generators/types.ts +83 -0
- package/src/resource/index.ts +42 -0
- package/src/resource/parser.ts +139 -0
- package/src/resource/schema.ts +252 -0
|
@@ -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
|
+
}
|