@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.
@@ -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
- export declare function mutation<TSchema = any, TReturn = any>(invalidatesOrDef: string[] | {
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
- }, handler?: MutationHandler<InferArgs<TSchema>, TReturn>, name?: string): MutationDef<TSchema, TReturn>;
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
- export function mutation(invalidatesOrDef, handler, name) {
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 (Array.isArray(invalidatesOrDef)) {
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 = invalidatesOrDef;
48
- actualHandler = handler;
81
+ invalidates = nameOrInvalidatesOrDef;
82
+ actualHandler = handlerOrDef;
49
83
  mutName = name || `mutation_${++mutationCounter}`;
50
84
  }
51
85
  else {
52
- // New object style: mutation({ name?, invalidates, args?, public?, handler })
53
- invalidates = invalidatesOrDef.invalidates;
54
- actualHandler = invalidatesOrDef.handler;
55
- argsSchema = invalidatesOrDef.args;
56
- isPublic = invalidatesOrDef.public === true;
57
- mutName = invalidatesOrDef.name || name || `mutation_${++mutationCounter}`;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gencow/core",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Gencow core library — defineQuery, defineMutation, reactive subscriptions",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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
- invalidatesOrDef: string[] | { name?: string; args?: TSchema; public?: boolean; invalidates: string[]; handler: MutationHandler<InferArgs<TSchema>, TReturn> },
225
- handler?: MutationHandler<InferArgs<TSchema>, TReturn>,
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 (Array.isArray(invalidatesOrDef)) {
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 = invalidatesOrDef;
237
- actualHandler = handler!;
269
+ invalidates = nameOrInvalidatesOrDef;
270
+ actualHandler = handlerOrDef as MutationHandler<InferArgs<TSchema>, TReturn>;
238
271
  mutName = name || `mutation_${++mutationCounter}`;
239
272
  } else {
240
- // New object style: mutation({ name?, invalidates, args?, public?, handler })
241
- invalidates = invalidatesOrDef.invalidates;
242
- actualHandler = invalidatesOrDef.handler;
243
- argsSchema = invalidatesOrDef.args;
244
- isPublic = invalidatesOrDef.public === true;
245
- mutName = invalidatesOrDef.name || name || `mutation_${++mutationCounter}`;
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,