@mandujs/core 0.17.0 → 0.18.1
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
- package/src/router/fs-scanner.ts +21 -6
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ export * from "./island";
|
|
|
22
22
|
export * from "./intent";
|
|
23
23
|
export * from "./devtools";
|
|
24
24
|
export * from "./paths";
|
|
25
|
+
export * from "./resource";
|
|
25
26
|
|
|
26
27
|
// Consolidated Mandu namespace
|
|
27
28
|
import { ManduFilling, ManduContext, ManduFillingFactory, createSSEConnection } from "./filling";
|
package/src/paths.ts
CHANGED
|
@@ -17,6 +17,12 @@ export interface GeneratedPaths {
|
|
|
17
17
|
manifestPath: string;
|
|
18
18
|
/** 생성된 lock 경로 */
|
|
19
19
|
lockPath: string;
|
|
20
|
+
/** Resource 관련 경로 */
|
|
21
|
+
resourceContractsDir: string;
|
|
22
|
+
resourceTypesDir: string;
|
|
23
|
+
resourceSlotsDir: string;
|
|
24
|
+
resourceClientDir: string;
|
|
25
|
+
resourceSchemasDir: string;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
/**
|
|
@@ -30,6 +36,11 @@ export function resolveGeneratedPaths(rootDir: string): GeneratedPaths {
|
|
|
30
36
|
mapDir: path.join(rootDir, ".mandu/generated"),
|
|
31
37
|
manifestPath: path.join(rootDir, ".mandu/routes.manifest.json"),
|
|
32
38
|
lockPath: path.join(rootDir, ".mandu/spec.lock.json"),
|
|
39
|
+
resourceContractsDir: path.join(rootDir, ".mandu/generated/server/contracts"),
|
|
40
|
+
resourceTypesDir: path.join(rootDir, ".mandu/generated/server/types"),
|
|
41
|
+
resourceSlotsDir: path.join(rootDir, "spec/slots"),
|
|
42
|
+
resourceClientDir: path.join(rootDir, ".mandu/generated/client"),
|
|
43
|
+
resourceSchemasDir: path.join(rootDir, "spec/resources"),
|
|
33
44
|
};
|
|
34
45
|
}
|
|
35
46
|
|
|
@@ -44,4 +55,9 @@ export const GENERATED_RELATIVE_PATHS = {
|
|
|
44
55
|
manifest: ".mandu/routes.manifest.json",
|
|
45
56
|
lock: ".mandu/spec.lock.json",
|
|
46
57
|
history: ".mandu/history",
|
|
58
|
+
contracts: ".mandu/generated/server/contracts",
|
|
59
|
+
resourceTypes: ".mandu/generated/server/types",
|
|
60
|
+
slots: "spec/slots",
|
|
61
|
+
client: ".mandu/generated/client",
|
|
62
|
+
resourceSchemas: "spec/resources",
|
|
47
63
|
} as const;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backward Compatibility Tests
|
|
3
|
+
*
|
|
4
|
+
* QA Engineer: Ensure resource architecture doesn't break existing functionality
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, test, expect } from "bun:test";
|
|
8
|
+
import path from "path";
|
|
9
|
+
|
|
10
|
+
describe("Backward Compatibility - Path Structure", () => {
|
|
11
|
+
test("should have properly defined resource paths", async () => {
|
|
12
|
+
const { resolveGeneratedPaths } = await import("../../paths");
|
|
13
|
+
const paths = resolveGeneratedPaths(process.cwd());
|
|
14
|
+
|
|
15
|
+
// Resource-specific paths should be defined
|
|
16
|
+
expect(paths.resourceContractsDir).toBeDefined();
|
|
17
|
+
expect(paths.resourceTypesDir).toBeDefined();
|
|
18
|
+
expect(paths.resourceSlotsDir).toBeDefined();
|
|
19
|
+
expect(paths.resourceClientDir).toBeDefined();
|
|
20
|
+
expect(paths.resourceSchemasDir).toBeDefined();
|
|
21
|
+
|
|
22
|
+
// Verify path structure (normalize path separators)
|
|
23
|
+
expect(path.normalize(paths.resourceContractsDir)).toContain(
|
|
24
|
+
path.normalize(".mandu/generated/server/contracts")
|
|
25
|
+
);
|
|
26
|
+
expect(path.normalize(paths.resourceTypesDir)).toContain(
|
|
27
|
+
path.normalize(".mandu/generated/server/types")
|
|
28
|
+
);
|
|
29
|
+
expect(path.normalize(paths.resourceSlotsDir)).toContain(path.normalize("spec/slots"));
|
|
30
|
+
expect(path.normalize(paths.resourceClientDir)).toContain(
|
|
31
|
+
path.normalize(".mandu/generated/client")
|
|
32
|
+
);
|
|
33
|
+
expect(path.normalize(paths.resourceSchemasDir)).toContain(path.normalize("spec/resources"));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("should maintain existing path structure", async () => {
|
|
37
|
+
const { resolveGeneratedPaths } = await import("../../paths");
|
|
38
|
+
const paths = resolveGeneratedPaths(process.cwd());
|
|
39
|
+
|
|
40
|
+
// Existing paths should still exist
|
|
41
|
+
expect(paths.serverRoutesDir).toBeDefined();
|
|
42
|
+
expect(paths.webRoutesDir).toBeDefined();
|
|
43
|
+
expect(paths.typesDir).toBeDefined();
|
|
44
|
+
expect(paths.mapDir).toBeDefined();
|
|
45
|
+
expect(paths.manifestPath).toBeDefined();
|
|
46
|
+
expect(paths.lockPath).toBeDefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("should not conflict with existing generated directories", async () => {
|
|
50
|
+
const { resolveGeneratedPaths } = await import("../../paths");
|
|
51
|
+
const paths = resolveGeneratedPaths(process.cwd());
|
|
52
|
+
|
|
53
|
+
// Resource contracts and types use shared directories
|
|
54
|
+
// This is intentional - they coexist in the same location
|
|
55
|
+
expect(path.normalize(paths.resourceContractsDir)).toContain(
|
|
56
|
+
path.normalize("server/contracts")
|
|
57
|
+
);
|
|
58
|
+
expect(path.normalize(paths.resourceTypesDir)).toContain(path.normalize("server/types"));
|
|
59
|
+
|
|
60
|
+
// Slots are in the same spec/slots directory (intentional sharing)
|
|
61
|
+
expect(paths.resourceSlotsDir).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("Backward Compatibility - Existing Systems", () => {
|
|
66
|
+
test("should maintain existing exports from core", async () => {
|
|
67
|
+
const core = await import("../../index");
|
|
68
|
+
|
|
69
|
+
// Core exports should still be available
|
|
70
|
+
expect(core.Mandu).toBeDefined();
|
|
71
|
+
expect(core.createContract).toBeDefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("should not affect existing contract system", async () => {
|
|
75
|
+
const { createContract } = await import("../../contract/index");
|
|
76
|
+
const { z } = await import("zod");
|
|
77
|
+
|
|
78
|
+
// Existing contract creation should still work
|
|
79
|
+
const contract = createContract({
|
|
80
|
+
description: "Test backward compatibility",
|
|
81
|
+
request: {
|
|
82
|
+
GET: {
|
|
83
|
+
query: z.object({
|
|
84
|
+
id: z.string(),
|
|
85
|
+
}),
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
response: {
|
|
89
|
+
200: z.object({
|
|
90
|
+
data: z.string(),
|
|
91
|
+
}),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(contract).toBeDefined();
|
|
96
|
+
expect(contract.description).toBe("Test backward compatibility");
|
|
97
|
+
expect(contract.request.GET).toBeDefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("should not affect existing guard system", async () => {
|
|
101
|
+
const { detectCategory } = await import("../../guard/negotiation");
|
|
102
|
+
|
|
103
|
+
// Existing guard functionality should work
|
|
104
|
+
const category = detectCategory("사용자 인증");
|
|
105
|
+
expect(category).toBeDefined();
|
|
106
|
+
expect(typeof category).toBe("string");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("Backward Compatibility - Type Safety", () => {
|
|
111
|
+
test("resource types should not conflict with existing types", async () => {
|
|
112
|
+
const resourceModule = await import("../index");
|
|
113
|
+
const contractModule = await import("../../contract/index");
|
|
114
|
+
|
|
115
|
+
// Both should export their own types without conflicts
|
|
116
|
+
expect(typeof resourceModule.defineResource).toBe("function");
|
|
117
|
+
expect(typeof resourceModule.generateResourceArtifacts).toBe("function");
|
|
118
|
+
expect(typeof contractModule.createContract).toBe("function");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("FieldTypes should be properly namespaced", async () => {
|
|
122
|
+
const { FieldTypes } = await import("../schema");
|
|
123
|
+
|
|
124
|
+
// Verify FieldTypes doesn't pollute global namespace
|
|
125
|
+
expect(Array.isArray(FieldTypes)).toBe(true);
|
|
126
|
+
expect(FieldTypes.length).toBe(10);
|
|
127
|
+
|
|
128
|
+
// Verify it contains expected types
|
|
129
|
+
expect(FieldTypes).toContain("string");
|
|
130
|
+
expect(FieldTypes).toContain("number");
|
|
131
|
+
expect(FieldTypes).toContain("uuid");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("Backward Compatibility - API Exports", () => {
|
|
136
|
+
test("all resource exports should be properly namespaced", async () => {
|
|
137
|
+
const resourceExports = await import("../index");
|
|
138
|
+
|
|
139
|
+
// Schema API
|
|
140
|
+
expect(resourceExports.defineResource).toBeDefined();
|
|
141
|
+
expect(resourceExports.validateResourceDefinition).toBeDefined();
|
|
142
|
+
expect(resourceExports.FieldTypes).toBeDefined();
|
|
143
|
+
|
|
144
|
+
// Parser API
|
|
145
|
+
expect(resourceExports.parseResourceSchema).toBeDefined();
|
|
146
|
+
expect(resourceExports.parseResourceSchemas).toBeDefined();
|
|
147
|
+
|
|
148
|
+
// Generator API
|
|
149
|
+
expect(resourceExports.generateResourceArtifacts).toBeDefined();
|
|
150
|
+
expect(resourceExports.generateResourcesArtifacts).toBeDefined();
|
|
151
|
+
|
|
152
|
+
// Individual generators
|
|
153
|
+
expect(resourceExports.generateResourceContract).toBeDefined();
|
|
154
|
+
expect(resourceExports.generateResourceTypes).toBeDefined();
|
|
155
|
+
expect(resourceExports.generateResourceSlot).toBeDefined();
|
|
156
|
+
expect(resourceExports.generateResourceClient).toBeDefined();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("resource types should be properly exported", async () => {
|
|
160
|
+
const module = await import("../index");
|
|
161
|
+
|
|
162
|
+
// Functions should be defined
|
|
163
|
+
expect(typeof module.defineResource).toBe("function");
|
|
164
|
+
expect(typeof module.parseResourceSchema).toBe("function");
|
|
165
|
+
expect(typeof module.generateResourceArtifacts).toBe("function");
|
|
166
|
+
|
|
167
|
+
// Verify function signatures work
|
|
168
|
+
const definition = module.defineResource({
|
|
169
|
+
name: "test",
|
|
170
|
+
fields: {
|
|
171
|
+
id: { type: "uuid", required: true },
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(definition.name).toBe("test");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("Backward Compatibility - No Breaking Changes", () => {
|
|
180
|
+
test("no pollution of global scope", () => {
|
|
181
|
+
const globalKeys = Object.keys(globalThis);
|
|
182
|
+
|
|
183
|
+
// These should NOT exist in global scope
|
|
184
|
+
expect(globalKeys).not.toContain("defineResource");
|
|
185
|
+
expect(globalKeys).not.toContain("ResourceDefinition");
|
|
186
|
+
expect(globalKeys).not.toContain("FieldTypes");
|
|
187
|
+
expect(globalKeys).not.toContain("generateResourceArtifacts");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("existing test infrastructure still works", () => {
|
|
191
|
+
// Meta-test: if this runs, the test framework is working
|
|
192
|
+
expect(true).toBe(true);
|
|
193
|
+
expect(1 + 1).toBe(2);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("Backward Compatibility - Coexistence", () => {
|
|
198
|
+
test("can use both manifest and resource systems together", async () => {
|
|
199
|
+
const { createContract } = await import("../../contract/index");
|
|
200
|
+
const { defineResource } = await import("../schema");
|
|
201
|
+
const { z } = await import("zod");
|
|
202
|
+
|
|
203
|
+
// Create a traditional manifest-based contract
|
|
204
|
+
const manifestContract = createContract({
|
|
205
|
+
description: "Manifest-based API",
|
|
206
|
+
request: {
|
|
207
|
+
GET: {
|
|
208
|
+
query: z.object({ id: z.string() }),
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
response: {
|
|
212
|
+
200: z.object({ data: z.string() }),
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Create a resource-based definition
|
|
217
|
+
const resourceDef = defineResource({
|
|
218
|
+
name: "user",
|
|
219
|
+
fields: {
|
|
220
|
+
id: { type: "uuid", required: true },
|
|
221
|
+
name: { type: "string", required: true },
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Both should coexist without conflicts
|
|
226
|
+
expect(manifestContract).toBeDefined();
|
|
227
|
+
expect(manifestContract.description).toBe("Manifest-based API");
|
|
228
|
+
|
|
229
|
+
expect(resourceDef).toBeDefined();
|
|
230
|
+
expect(resourceDef.name).toBe("user");
|
|
231
|
+
expect(resourceDef.fields.id.type).toBe("uuid");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("resource and contract validators can coexist", async () => {
|
|
235
|
+
const { ContractValidator } = await import("../../contract/validator");
|
|
236
|
+
const { validateResourceDefinition } = await import("../schema");
|
|
237
|
+
const { createContract } = await import("../../contract/index");
|
|
238
|
+
const { z } = await import("zod");
|
|
239
|
+
|
|
240
|
+
// Use contract validator
|
|
241
|
+
const contract = createContract({
|
|
242
|
+
request: { GET: { query: z.object({ id: z.string() }) } },
|
|
243
|
+
response: { 200: z.object({ data: z.string() }) },
|
|
244
|
+
});
|
|
245
|
+
const validator = new ContractValidator(contract);
|
|
246
|
+
expect(validator).toBeDefined();
|
|
247
|
+
|
|
248
|
+
// Use resource validator
|
|
249
|
+
const resourceDef = {
|
|
250
|
+
name: "test",
|
|
251
|
+
fields: { id: { type: "uuid" as const, required: true } },
|
|
252
|
+
};
|
|
253
|
+
expect(() => validateResourceDefinition(resourceDef)).not.toThrow();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("path constants are properly defined", () => {
|
|
257
|
+
const { GENERATED_RELATIVE_PATHS } = require("../../paths");
|
|
258
|
+
|
|
259
|
+
// Verify all expected paths exist
|
|
260
|
+
expect(GENERATED_RELATIVE_PATHS.contracts).toBeDefined();
|
|
261
|
+
expect(GENERATED_RELATIVE_PATHS.slots).toBeDefined();
|
|
262
|
+
expect(GENERATED_RELATIVE_PATHS.client).toBeDefined();
|
|
263
|
+
expect(GENERATED_RELATIVE_PATHS.resourceSchemas).toBeDefined();
|
|
264
|
+
|
|
265
|
+
// Verify structure (normalize separators)
|
|
266
|
+
expect(path.normalize(GENERATED_RELATIVE_PATHS.contracts)).toContain("contracts");
|
|
267
|
+
expect(path.normalize(GENERATED_RELATIVE_PATHS.slots)).toContain("slots");
|
|
268
|
+
expect(path.normalize(GENERATED_RELATIVE_PATHS.resourceSchemas)).toContain("resources");
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("Backward Compatibility - Error Handling", () => {
|
|
273
|
+
test("resource validation errors should be clear and helpful", async () => {
|
|
274
|
+
const { validateResourceDefinition } = await import("../schema");
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
validateResourceDefinition({
|
|
278
|
+
name: "",
|
|
279
|
+
fields: {},
|
|
280
|
+
} as any);
|
|
281
|
+
expect(false).toBe(true); // Should not reach here
|
|
282
|
+
} catch (error) {
|
|
283
|
+
expect(error).toBeDefined();
|
|
284
|
+
expect((error as Error).message).toContain("Resource name is required");
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("should handle missing fields gracefully", async () => {
|
|
289
|
+
const { validateResourceDefinition } = await import("../schema");
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
validateResourceDefinition({
|
|
293
|
+
name: "test",
|
|
294
|
+
fields: {},
|
|
295
|
+
} as any);
|
|
296
|
+
expect(false).toBe(true); // Should not reach here
|
|
297
|
+
} catch (error) {
|
|
298
|
+
expect(error).toBeDefined();
|
|
299
|
+
expect((error as Error).message).toContain("must have at least one field");
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
});
|