@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
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
+ });