@gencow/core 0.1.7 → 0.1.8
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/dist/reactive.d.ts +32 -2
- package/dist/reactive.js +49 -10
- package/package.json +1 -1
- package/src/__tests__/reactive.test.ts +77 -0
- package/src/reactive.ts +53 -11
package/dist/reactive.d.ts
CHANGED
|
@@ -153,13 +153,43 @@ export declare function query<TSchema = any, TReturn = any>(key: string, handler
|
|
|
153
153
|
public?: boolean;
|
|
154
154
|
handler: QueryHandler<InferArgs<TSchema>, TReturn>;
|
|
155
155
|
}): QueryDef<TSchema, TReturn>;
|
|
156
|
-
|
|
156
|
+
/**
|
|
157
|
+
* mutation — 데이터 변경 함수를 선언적으로 등록합니다.
|
|
158
|
+
*
|
|
159
|
+
* 3가지 호출 방식 지원 (query와 동일한 패턴 우선):
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* // ✅ 권장: query와 동일한 (name, def) 패턴
|
|
164
|
+
* mutation("tasks.create", {
|
|
165
|
+
* invalidates: [],
|
|
166
|
+
* args: { title: v.string() },
|
|
167
|
+
* handler: async (ctx, args) => { ... },
|
|
168
|
+
* });
|
|
169
|
+
*
|
|
170
|
+
* // ✅ 객체 스타일 (하위 호환)
|
|
171
|
+
* mutation({
|
|
172
|
+
* name: "tasks.create",
|
|
173
|
+
* invalidates: [],
|
|
174
|
+
* handler: async (ctx) => { ... },
|
|
175
|
+
* });
|
|
176
|
+
*
|
|
177
|
+
* // ⚠️ Legacy 배열 스타일 (비권장)
|
|
178
|
+
* mutation(["tasks.list"], handler, "tasks.create");
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
export declare function mutation<TSchema = any, TReturn = any>(nameOrInvalidatesOrDef: string | string[] | {
|
|
157
182
|
name?: string;
|
|
158
183
|
args?: TSchema;
|
|
159
184
|
public?: boolean;
|
|
160
185
|
invalidates: string[];
|
|
161
186
|
handler: MutationHandler<InferArgs<TSchema>, TReturn>;
|
|
162
|
-
},
|
|
187
|
+
}, handlerOrDef?: MutationHandler<InferArgs<TSchema>, TReturn> | {
|
|
188
|
+
invalidates?: string[];
|
|
189
|
+
args?: TSchema;
|
|
190
|
+
public?: boolean;
|
|
191
|
+
handler: MutationHandler<InferArgs<TSchema>, TReturn>;
|
|
192
|
+
}, name?: string): MutationDef<TSchema, TReturn>;
|
|
163
193
|
/**
|
|
164
194
|
* 커스텀 HTTP 엔드포인트를 선언적으로 등록합니다.
|
|
165
195
|
* query/mutation은 RPC 패턴이지만, httpAction은 RESTful HTTP 라우트를 직접 정의합니다.
|
package/dist/reactive.js
CHANGED
|
@@ -36,25 +36,64 @@ export function query(key, handlerOrDef) {
|
|
|
36
36
|
return def;
|
|
37
37
|
}
|
|
38
38
|
let mutationCounter = 0;
|
|
39
|
-
|
|
39
|
+
/**
|
|
40
|
+
* mutation — 데이터 변경 함수를 선언적으로 등록합니다.
|
|
41
|
+
*
|
|
42
|
+
* 3가지 호출 방식 지원 (query와 동일한 패턴 우선):
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* // ✅ 권장: query와 동일한 (name, def) 패턴
|
|
47
|
+
* mutation("tasks.create", {
|
|
48
|
+
* invalidates: [],
|
|
49
|
+
* args: { title: v.string() },
|
|
50
|
+
* handler: async (ctx, args) => { ... },
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
53
|
+
* // ✅ 객체 스타일 (하위 호환)
|
|
54
|
+
* mutation({
|
|
55
|
+
* name: "tasks.create",
|
|
56
|
+
* invalidates: [],
|
|
57
|
+
* handler: async (ctx) => { ... },
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* // ⚠️ Legacy 배열 스타일 (비권장)
|
|
61
|
+
* mutation(["tasks.list"], handler, "tasks.create");
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function mutation(nameOrInvalidatesOrDef, handlerOrDef, name) {
|
|
40
65
|
let invalidates;
|
|
41
66
|
let argsSchema;
|
|
42
67
|
let actualHandler;
|
|
43
68
|
let mutName;
|
|
44
69
|
let isPublic = false;
|
|
45
|
-
if (
|
|
70
|
+
if (typeof nameOrInvalidatesOrDef === "string") {
|
|
71
|
+
// New primary style: mutation("name", { invalidates?, args?, public?, handler })
|
|
72
|
+
mutName = nameOrInvalidatesOrDef;
|
|
73
|
+
const def = handlerOrDef;
|
|
74
|
+
invalidates = def.invalidates || [];
|
|
75
|
+
actualHandler = def.handler;
|
|
76
|
+
argsSchema = def.args;
|
|
77
|
+
isPublic = def.public === true;
|
|
78
|
+
}
|
|
79
|
+
else if (Array.isArray(nameOrInvalidatesOrDef)) {
|
|
46
80
|
// Legacy style: mutation([...], handler, "name")
|
|
47
|
-
invalidates =
|
|
48
|
-
actualHandler =
|
|
81
|
+
invalidates = nameOrInvalidatesOrDef;
|
|
82
|
+
actualHandler = handlerOrDef;
|
|
49
83
|
mutName = name || `mutation_${++mutationCounter}`;
|
|
50
84
|
}
|
|
51
85
|
else {
|
|
52
|
-
//
|
|
53
|
-
invalidates =
|
|
54
|
-
actualHandler =
|
|
55
|
-
argsSchema =
|
|
56
|
-
isPublic =
|
|
57
|
-
mutName =
|
|
86
|
+
// Object style: mutation({ name?, invalidates, args?, public?, handler })
|
|
87
|
+
invalidates = nameOrInvalidatesOrDef.invalidates;
|
|
88
|
+
actualHandler = nameOrInvalidatesOrDef.handler;
|
|
89
|
+
argsSchema = nameOrInvalidatesOrDef.args;
|
|
90
|
+
isPublic = nameOrInvalidatesOrDef.public === true;
|
|
91
|
+
mutName = nameOrInvalidatesOrDef.name || (typeof name === "string" ? name : "") || `mutation_${++mutationCounter}`;
|
|
92
|
+
}
|
|
93
|
+
// 이름 미지정 시 경고 — 디버깅 지원
|
|
94
|
+
if (mutName.startsWith("mutation_")) {
|
|
95
|
+
console.warn(`[gencow] mutation registered without explicit name → "${mutName}". ` +
|
|
96
|
+
`Use mutation("myMutation", { handler }) for better debugging.`);
|
|
58
97
|
}
|
|
59
98
|
const def = {
|
|
60
99
|
name: mutName,
|
package/package.json
CHANGED
|
@@ -235,3 +235,80 @@ describe("Secure by Default — public 플래그", () => {
|
|
|
235
235
|
expect(priv?.isPublic).toBe(false);
|
|
236
236
|
});
|
|
237
237
|
});
|
|
238
|
+
|
|
239
|
+
// ─── mutation("name", def) 새 시그니처 테스트 ────────────────────────────────
|
|
240
|
+
|
|
241
|
+
describe("mutation(name, def) — query와 동일 패턴", () => {
|
|
242
|
+
it("mutation('name', { handler })로 등록하면 name이 올바르게 설정된다", () => {
|
|
243
|
+
const m = mutation("newsig.basic", {
|
|
244
|
+
handler: async () => ({ ok: true }),
|
|
245
|
+
});
|
|
246
|
+
expect((m as any).name || (getRegisteredMutations().find(x => x.invalidates.length === 0 && x.handler === (m as any).handler) as any)?.name).toBeDefined();
|
|
247
|
+
const all = getRegisteredMutations();
|
|
248
|
+
const found = all.find(x => x.name === "newsig.basic");
|
|
249
|
+
expect(found).toBeDefined();
|
|
250
|
+
expect(found!.isPublic).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("mutation('name', { public: true })로 등록하면 isPublic === true", () => {
|
|
254
|
+
const m = mutation("newsig.public", {
|
|
255
|
+
public: true,
|
|
256
|
+
handler: async () => ({ ok: true }),
|
|
257
|
+
});
|
|
258
|
+
expect(m.isPublic).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("invalidates 미지정 시 빈 배열이 기본값", () => {
|
|
262
|
+
const m = mutation("newsig.noInvalidates", {
|
|
263
|
+
handler: async () => ({ ok: true }),
|
|
264
|
+
});
|
|
265
|
+
const all = getRegisteredMutations();
|
|
266
|
+
const found = all.find(x => x.name === "newsig.noInvalidates");
|
|
267
|
+
expect(found!.invalidates).toEqual([]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("invalidates 지정 시 올바르게 전달된다", () => {
|
|
271
|
+
const m = mutation("newsig.withInvalidates", {
|
|
272
|
+
invalidates: ["tasks.list", "tasks.get"],
|
|
273
|
+
handler: async () => ({ ok: true }),
|
|
274
|
+
});
|
|
275
|
+
const all = getRegisteredMutations();
|
|
276
|
+
const found = all.find(x => x.name === "newsig.withInvalidates");
|
|
277
|
+
expect(found!.invalidates).toEqual(["tasks.list", "tasks.get"]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("기존 객체 스타일도 여전히 동작한다 (하위 호환)", () => {
|
|
281
|
+
const m = mutation({
|
|
282
|
+
name: "newsig.compat.object",
|
|
283
|
+
invalidates: ["a.list"],
|
|
284
|
+
handler: async () => ({ ok: true }),
|
|
285
|
+
});
|
|
286
|
+
const all = getRegisteredMutations();
|
|
287
|
+
const found = all.find(x => x.name === "newsig.compat.object");
|
|
288
|
+
expect(found).toBeDefined();
|
|
289
|
+
expect(found!.invalidates).toEqual(["a.list"]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("기존 배열 스타일도 여전히 동작한다 (하위 호환)", () => {
|
|
293
|
+
const m = mutation(["b.list"], async () => ({ ok: true }), "newsig.compat.array");
|
|
294
|
+
const all = getRegisteredMutations();
|
|
295
|
+
const found = all.find(x => x.name === "newsig.compat.array");
|
|
296
|
+
expect(found).toBeDefined();
|
|
297
|
+
expect(found!.invalidates).toEqual(["b.list"]);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("이름 미지정 시 console.warn이 호출된다", () => {
|
|
301
|
+
const warnSpy = mock(() => {});
|
|
302
|
+
const originalWarn = console.warn;
|
|
303
|
+
console.warn = warnSpy;
|
|
304
|
+
|
|
305
|
+
mutation(["c.list"], async () => ({ ok: true }));
|
|
306
|
+
|
|
307
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
308
|
+
const warnMsg = warnSpy.mock.calls[0][0] as string;
|
|
309
|
+
expect(warnMsg).toContain("[gencow]");
|
|
310
|
+
expect(warnMsg).toContain("without explicit name");
|
|
311
|
+
|
|
312
|
+
console.warn = originalWarn;
|
|
313
|
+
});
|
|
314
|
+
});
|
package/src/reactive.ts
CHANGED
|
@@ -220,9 +220,34 @@ export function query<TSchema = any, TReturn = any>(
|
|
|
220
220
|
|
|
221
221
|
let mutationCounter = 0;
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* mutation — 데이터 변경 함수를 선언적으로 등록합니다.
|
|
225
|
+
*
|
|
226
|
+
* 3가지 호출 방식 지원 (query와 동일한 패턴 우선):
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```typescript
|
|
230
|
+
* // ✅ 권장: query와 동일한 (name, def) 패턴
|
|
231
|
+
* mutation("tasks.create", {
|
|
232
|
+
* invalidates: [],
|
|
233
|
+
* args: { title: v.string() },
|
|
234
|
+
* handler: async (ctx, args) => { ... },
|
|
235
|
+
* });
|
|
236
|
+
*
|
|
237
|
+
* // ✅ 객체 스타일 (하위 호환)
|
|
238
|
+
* mutation({
|
|
239
|
+
* name: "tasks.create",
|
|
240
|
+
* invalidates: [],
|
|
241
|
+
* handler: async (ctx) => { ... },
|
|
242
|
+
* });
|
|
243
|
+
*
|
|
244
|
+
* // ⚠️ Legacy 배열 스타일 (비권장)
|
|
245
|
+
* mutation(["tasks.list"], handler, "tasks.create");
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
223
248
|
export function mutation<TSchema = any, TReturn = any>(
|
|
224
|
-
|
|
225
|
-
|
|
249
|
+
nameOrInvalidatesOrDef: string | string[] | { name?: string; args?: TSchema; public?: boolean; invalidates: string[]; handler: MutationHandler<InferArgs<TSchema>, TReturn> },
|
|
250
|
+
handlerOrDef?: MutationHandler<InferArgs<TSchema>, TReturn> | { invalidates?: string[]; args?: TSchema; public?: boolean; handler: MutationHandler<InferArgs<TSchema>, TReturn> },
|
|
226
251
|
name?: string
|
|
227
252
|
): MutationDef<TSchema, TReturn> {
|
|
228
253
|
let invalidates: string[];
|
|
@@ -231,19 +256,36 @@ export function mutation<TSchema = any, TReturn = any>(
|
|
|
231
256
|
let mutName: string;
|
|
232
257
|
let isPublic = false;
|
|
233
258
|
|
|
234
|
-
if (
|
|
259
|
+
if (typeof nameOrInvalidatesOrDef === "string") {
|
|
260
|
+
// New primary style: mutation("name", { invalidates?, args?, public?, handler })
|
|
261
|
+
mutName = nameOrInvalidatesOrDef;
|
|
262
|
+
const def = handlerOrDef as { invalidates?: string[]; args?: TSchema; public?: boolean; handler: MutationHandler<InferArgs<TSchema>, TReturn> };
|
|
263
|
+
invalidates = def.invalidates || [];
|
|
264
|
+
actualHandler = def.handler;
|
|
265
|
+
argsSchema = def.args;
|
|
266
|
+
isPublic = def.public === true;
|
|
267
|
+
} else if (Array.isArray(nameOrInvalidatesOrDef)) {
|
|
235
268
|
// Legacy style: mutation([...], handler, "name")
|
|
236
|
-
invalidates =
|
|
237
|
-
actualHandler =
|
|
269
|
+
invalidates = nameOrInvalidatesOrDef;
|
|
270
|
+
actualHandler = handlerOrDef as MutationHandler<InferArgs<TSchema>, TReturn>;
|
|
238
271
|
mutName = name || `mutation_${++mutationCounter}`;
|
|
239
272
|
} else {
|
|
240
|
-
//
|
|
241
|
-
invalidates =
|
|
242
|
-
actualHandler =
|
|
243
|
-
argsSchema =
|
|
244
|
-
isPublic =
|
|
245
|
-
mutName =
|
|
273
|
+
// Object style: mutation({ name?, invalidates, args?, public?, handler })
|
|
274
|
+
invalidates = nameOrInvalidatesOrDef.invalidates;
|
|
275
|
+
actualHandler = nameOrInvalidatesOrDef.handler;
|
|
276
|
+
argsSchema = nameOrInvalidatesOrDef.args;
|
|
277
|
+
isPublic = nameOrInvalidatesOrDef.public === true;
|
|
278
|
+
mutName = nameOrInvalidatesOrDef.name || (typeof name === "string" ? name : "") || `mutation_${++mutationCounter}`;
|
|
246
279
|
}
|
|
280
|
+
|
|
281
|
+
// 이름 미지정 시 경고 — 디버깅 지원
|
|
282
|
+
if (mutName.startsWith("mutation_")) {
|
|
283
|
+
console.warn(
|
|
284
|
+
`[gencow] mutation registered without explicit name → "${mutName}". ` +
|
|
285
|
+
`Use mutation("myMutation", { handler }) for better debugging.`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
247
289
|
const def: MutationDef<TSchema, TReturn> & { name: string } = {
|
|
248
290
|
name: mutName,
|
|
249
291
|
invalidates,
|