@gencow/core 0.1.17 → 0.1.19

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.
@@ -9,7 +9,6 @@
9
9
  import { describe, it, expect } from "bun:test";
10
10
  import {
11
11
  buildRealtimeCtx,
12
- invalidateQueries,
13
12
  subscribe,
14
13
  registerClient,
15
14
  deregisterClient,
@@ -320,44 +319,40 @@ describe("[Load] 대용량 페이로드 emit 성능", () => {
320
319
  });
321
320
  });
322
321
 
323
- // ─── 6. invalidateQueries broadcast 확장성 ──────────────────────────────────
322
+ // ─── 6. emit broadcast 확장성 (컨넥튰드 클라이언트 기반) ─────────────────────────
324
323
 
325
- describe("[Load] invalidateQueries broadcast 확장성", () => {
326
- it("1,000개 connectedClients에 broadcast를 50ms 이내에 완료한다", async () => {
324
+ describe("[Load] emit broadcast 확장성", () => {
325
+ it("1,000개 구독자에게 emit이 50ms 이내에 완료된다", async () => {
327
326
  const N = 1_000;
327
+ const key = makeUniqueKey("broadcast1k");
328
328
  const clients = Array.from({ length: N }, (_, i) => makeMockWs(i));
329
- clients.forEach(ws => registerClient(ws));
329
+ clients.forEach(ws => subscribe(key, ws));
330
330
 
331
- const mockCtx = {} as GencowCtx;
332
331
  const start = performance.now();
333
- await invalidateQueries(["tasks.list", "tasks.get"], mockCtx);
332
+ buildRealtimeCtx().emit(key, [{ id: 1 }, { id: 2 }]);
333
+ await wait(60);
334
334
  const elapsed = performance.now() - start;
335
335
 
336
- console.log(`[broadcast] 1,000 connectedClients → ${elapsed.toFixed(1)}ms`);
336
+ console.log(`[broadcast] 1,000 subscribers → ${elapsed.toFixed(1)}ms`);
337
337
 
338
338
  const received = clients.filter(ws => ws.sendCount === 1).length;
339
339
  expect(received).toBe(N);
340
- expect(elapsed).toBeLessThan(50);
340
+ expect(elapsed).toBeLessThan(100); // emit has 50ms debounce
341
341
 
342
342
  clients.forEach(ws => deregisterClient(ws));
343
343
  });
344
344
  });
345
345
 
346
- // ─── 7. push vs legacy 비교 ─────────────────────────────────────────────────
346
+ // ─── 7. Push emit 포맷 검증 ───────────────────────────────────────────────
347
347
 
348
- describe("[Load] Push 방식 vs Legacy invalidateQueries 비교", () => {
349
- it("emit(push) query:updated+data를, legacy는 invalidate 신호만 전달한다", async () => {
348
+ describe("[Load] Push emit 포맷 검증", () => {
349
+ it("emit(push) query:updated+data 실제 페이로드와 함께 전달한다", async () => {
350
350
  const N_SUBS = 500;
351
351
  const keyPush = makeUniqueKey("compare.push");
352
- const keyLegacy = makeUniqueKey("compare.legacy");
353
352
 
354
353
  // push: 구독 클라이언트 (query:updated 수신 예상)
355
354
  const pushClients = Array.from({ length: N_SUBS }, (_, i) => makeTrackedWs(i));
356
- // legacy: connectedClients 전용 (invalidate broadcast 수신 예상)
357
- const legacyClients = Array.from({ length: N_SUBS }, (_, i) => makeTrackedWs(i + N_SUBS));
358
-
359
355
  pushClients.forEach(ws => subscribe(keyPush, ws));
360
- legacyClients.forEach(ws => registerClient(ws)); // subscribe 안 함 → invalidate만 수신
361
356
 
362
357
  const data = Array.from({ length: 100 }, (_, i) => ({ id: i, title: `T${i}` }));
363
358
 
@@ -367,28 +362,15 @@ describe("[Load] Push 방식 vs Legacy invalidateQueries 비교", () => {
367
362
  await wait(80);
368
363
  const pushElapsed = performance.now() - pushStart;
369
364
 
370
- // Legacy path: invalidate broadcast 즉시 전송
371
- const legacyStart = performance.now();
372
- await invalidateQueries([keyLegacy], {} as GencowCtx);
373
- const legacyElapsed = performance.now() - legacyStart;
374
-
375
365
  const pushWithData = pushClients.filter(ws =>
376
366
  ws.received.some((m: any) => m.type === "query:updated" && m.hasData)
377
367
  ).length;
378
368
 
379
- const legacyWithSignal = legacyClients.filter(ws =>
380
- ws.received.some((m: any) => m.type === "invalidate")
381
- ).length;
382
-
383
- console.log(`[compare] Push : ${pushElapsed.toFixed(1)}ms → ${pushWithData}/${N_SUBS} with data (query:updated)`);
384
- console.log(`[compare] Legacy: ${legacyElapsed.toFixed(1)}ms → ${legacyWithSignal}/${N_SUBS} signal only (invalidate)`);
369
+ console.log(`[push] ${pushElapsed.toFixed(1)}ms ${pushWithData}/${N_SUBS} with data (query:updated)`);
385
370
 
386
371
  // 핵심 검증
387
372
  expect(pushWithData).toBe(N_SUBS); // 500/500이 data 포함 메시지 수신
388
- expect(legacyWithSignal).toBe(N_SUBS); // 500/500이 신호 수신
389
- expect(legacyElapsed).toBeLessThan(pushElapsed); // legacy가 더 즉각적 (debounce 없음)
390
373
 
391
374
  pushClients.forEach(ws => deregisterClient(ws));
392
- legacyClients.forEach(ws => deregisterClient(ws));
393
375
  });
394
376
  });
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * packages/core/src/__tests__/reactive.test.ts
3
3
  *
4
- * Tests for ctx.realtime.emit() push model and invalidateQueries() simplification.
4
+ * Tests for ctx.realtime.emit() push model and refresh() API.
5
5
  *
6
6
  * Run: bun test packages/core/src/__tests__/reactive.test.ts
7
7
  */
8
8
 
9
9
  import { describe, it, expect, mock, beforeEach } from "bun:test";
10
- import { buildRealtimeCtx, invalidateQueries, subscribe, deregisterClient, registerClient } from "../reactive";
10
+ import { buildRealtimeCtx, subscribe, deregisterClient, registerClient } from "../reactive";
11
11
  import type { GencowCtx } from "../reactive";
12
12
 
13
13
  // ─── Mock WebSocket (Bun-style WSContext) ────────────────────────────────────
@@ -96,78 +96,63 @@ describe("buildRealtimeCtx()", () => {
96
96
  });
97
97
  });
98
98
 
99
- // ─── invalidateQueries (simplified) ─────────────────────────────────────────
99
+ // ─── refresh() API ──────────────────────────────────────────────────────────
100
100
 
101
- describe("invalidateQueries() — simplified broadcast-only", () => {
102
- it(" 배열이면 아무것도 전송하지 않는다 (emit() 방식 no-op)", async () => {
103
- const ws = makeMockWs();
104
- registerClient(ws);
105
-
106
- const mockCtx = {} as GencowCtx;
107
- await invalidateQueries([], mockCtx);
101
+ describe("refresh() — 서버 쿼리 재실행 요청 큐", () => {
102
+ it("refresh()로 등록된 queryKey가 _pendingRefresh에 큐잉된다", () => {
103
+ const rt = buildRealtimeCtx();
104
+ rt.refresh("tasks.list");
108
105
 
109
- expect(ws._sent).toHaveLength(0);
110
- deregisterClient(ws);
106
+ expect((rt as any)._pendingRefresh).toContain("tasks.list");
111
107
  });
112
108
 
113
- it("queryKeys가 있으면 connectedClients 전체에 invalidate broadcast를 보낸다", async () => {
114
- const ws = makeMockWs();
115
- registerClient(ws);
116
-
117
- const mockCtx = {} as GencowCtx;
118
- await invalidateQueries(["tasks.list"], mockCtx);
119
-
120
- expect(ws._sent).toHaveLength(1);
121
- const msg = JSON.parse(ws._sent[0]);
122
- expect(msg.type).toBe("invalidate");
123
- expect(msg.queries).toEqual(["tasks.list"]);
109
+ it("동일 queryKey 중복 refresh는 번만 큐잉된다", () => {
110
+ const rt = buildRealtimeCtx();
111
+ rt.refresh("tasks.list");
112
+ rt.refresh("tasks.list");
113
+ rt.refresh("tasks.list");
124
114
 
125
- deregisterClient(ws);
115
+ const count = (rt as any)._pendingRefresh.filter((k: string) => k === "tasks.list").length;
116
+ expect(count).toBe(1);
126
117
  });
127
118
 
128
- it("서버에서 쿼리를 재실행하지 않는다 (query:updated 미전송)", async () => {
129
- const ws = makeMockWs();
130
- registerClient(ws);
131
-
132
- const mockCtx = {} as GencowCtx;
133
- await invalidateQueries(["tasks.list", "tasks.get"], mockCtx);
134
-
135
- // query:updated 메시지가 없어야 함
136
- const types = ws._sent.map((s: string) => JSON.parse(s).type);
137
- expect(types.every((t: string) => t === "invalidate")).toBe(true);
119
+ it("서로 다른 queryKey는 각각 큐잉된다", () => {
120
+ const rt = buildRealtimeCtx();
121
+ rt.refresh("tasks.list");
122
+ rt.refresh("users.list");
138
123
 
139
- deregisterClient(ws);
124
+ expect((rt as any)._pendingRefresh).toContain("tasks.list");
125
+ expect((rt as any)._pendingRefresh).toContain("users.list");
140
126
  });
141
127
  });
142
128
 
143
- // ─── buildRealtimeCtx + invalidateQueries 공존 시나리오 ─────────────────────
144
-
145
- describe("emit() 방식과 legacy invalidateQueries() 혼용", () => {
146
- it("emit()은 query:updated, invalidateQueries()는 invalidate를 각각 전송한다", async () => {
147
- const wsSubscribed = makeMockWs(); // query 구독 클라이언트
148
- const wsConnected = makeMockWs(); // 연결만 된 클라이언트 (대시보드)
129
+ // ─── emit()과 refresh() 병행 시나리오 ───────────────────────────────────────
149
130
 
131
+ describe("emit()과 refresh() 병행", () => {
132
+ it("emit()은 query:updated를 구독자에게 즉시 push한다", async () => {
133
+ const wsSubscribed = makeMockWs();
150
134
  subscribe("items.list", wsSubscribed);
151
- registerClient(wsConnected);
152
135
 
153
- // 1. emit() — 구독자에게만 query:updated
154
136
  const rt = buildRealtimeCtx();
155
137
  rt.emit("items.list", [{ id: 1 }]);
156
138
  await new Promise(r => setTimeout(r, 80));
157
139
 
158
140
  expect(wsSubscribed._sent.some((s: string) => JSON.parse(s).type === "query:updated")).toBe(true);
159
- // connectedClients에만 등록된 ws는 query:updated 미수신
160
- expect(wsConnected._sent).toHaveLength(0);
161
141
 
162
- // 2. invalidateQueries() — ALL connectedClients에 invalidate broadcast
163
- const mockCtx = {} as GencowCtx;
164
- await invalidateQueries(["items.list"], mockCtx);
142
+ deregisterClient(wsSubscribed);
143
+ });
165
144
 
166
- expect(wsConnected._sent).toHaveLength(1);
167
- expect(JSON.parse(wsConnected._sent[0]).type).toBe("invalidate");
145
+ it("emit() 호출 후 _hasEmitted가 true로 설정된다", () => {
146
+ const rt = buildRealtimeCtx();
147
+ expect((rt as any)._hasEmitted).toBe(false);
168
148
 
169
- deregisterClient(wsSubscribed);
170
- deregisterClient(wsConnected);
149
+ const ws = makeMockWs();
150
+ subscribe("flag.test", ws);
151
+ rt.emit("flag.test", [{ id: 1 }]);
152
+
153
+ expect((rt as any)._hasEmitted).toBe(true);
154
+
155
+ deregisterClient(ws);
171
156
  });
172
157
  });
173
158
 
@@ -206,7 +191,6 @@ describe("Secure by Default — public 플래그", () => {
206
191
  it("mutation() 기본값은 isPublic === false", () => {
207
192
  const m = mutation({
208
193
  name: "sectest.mut.private",
209
- invalidates: [],
210
194
  handler: async () => ({ ok: true }),
211
195
  });
212
196
  expect(m.isPublic).toBe(false);
@@ -215,7 +199,6 @@ describe("Secure by Default — public 플래그", () => {
215
199
  it("mutation({ public: true }) 시 isPublic === true", () => {
216
200
  const m = mutation({
217
201
  name: "sectest.mut.public",
218
- invalidates: [],
219
202
  public: true,
220
203
  handler: async () => ({ ok: true }),
221
204
  });
@@ -243,7 +226,7 @@ describe("mutation(name, def) — query와 동일 패턴", () => {
243
226
  const m = mutation("newsig.basic", {
244
227
  handler: async () => ({ ok: true }),
245
228
  });
246
- expect((m as any).name || (getRegisteredMutations().find(x => x.invalidates.length === 0 && x.handler === (m as any).handler) as any)?.name).toBeDefined();
229
+ expect((m as any).name || (getRegisteredMutations().find(x => x.handler === (m as any).handler) as any)?.name).toBeDefined();
247
230
  const all = getRegisteredMutations();
248
231
  const found = all.find(x => x.name === "newsig.basic");
249
232
  expect(found).toBeDefined();
@@ -258,35 +241,35 @@ describe("mutation(name, def) — query와 동일 패턴", () => {
258
241
  expect(m.isPublic).toBe(true);
259
242
  });
260
243
 
261
- it("invalidates 미지정 시 빈 배열이 기본값", () => {
244
+ it("invalidates 미지정 시 빈 배열이 기본값 (하위호환)", () => {
262
245
  const m = mutation("newsig.noInvalidates", {
263
246
  handler: async () => ({ ok: true }),
264
247
  });
265
248
  const all = getRegisteredMutations();
266
249
  const found = all.find(x => x.name === "newsig.noInvalidates");
267
- expect(found!.invalidates).toEqual([]);
250
+ // invalidates는 deprecated이지만 빈 배열로 유지 (하위호환)
251
+ expect(found).toBeDefined();
268
252
  });
269
253
 
270
- it("invalidates 지정 올바르게 전달된다", () => {
254
+ it("invalidates 지정해도 무시된다 (deprecated)", () => {
271
255
  const m = mutation("newsig.withInvalidates", {
272
256
  invalidates: ["tasks.list", "tasks.get"],
273
257
  handler: async () => ({ ok: true }),
274
258
  });
275
259
  const all = getRegisteredMutations();
276
260
  const found = all.find(x => x.name === "newsig.withInvalidates");
277
- expect(found!.invalidates).toEqual(["tasks.list", "tasks.get"]);
261
+ // invalidates deprecated — 전달해도 런타임에서 무시됨
262
+ expect(found).toBeDefined();
278
263
  });
279
264
 
280
265
  it("기존 객체 스타일도 여전히 동작한다 (하위 호환)", () => {
281
266
  const m = mutation({
282
267
  name: "newsig.compat.object",
283
- invalidates: ["a.list"],
284
268
  handler: async () => ({ ok: true }),
285
269
  });
286
270
  const all = getRegisteredMutations();
287
271
  const found = all.find(x => x.name === "newsig.compat.object");
288
272
  expect(found).toBeDefined();
289
- expect(found!.invalidates).toEqual(["a.list"]);
290
273
  });
291
274
 
292
275
  it("기존 배열 스타일도 여전히 동작한다 (하위 호환)", () => {
@@ -294,7 +277,6 @@ describe("mutation(name, def) — query와 동일 패턴", () => {
294
277
  const all = getRegisteredMutations();
295
278
  const found = all.find(x => x.name === "newsig.compat.array");
296
279
  expect(found).toBeDefined();
297
- expect(found!.invalidates).toEqual(["b.list"]);
298
280
  });
299
281
 
300
282
  it("이름 미지정 시 console.warn이 호출된다", () => {
@@ -206,3 +206,116 @@ describe("createStorage()", () => {
206
206
  });
207
207
  });
208
208
  });
209
+
210
+ // ─── _system_files 테이블 리네이밍 관련 테스트 ────────────
211
+ // 2026-04-10 WSOD 사고 후 추가: files → _system_files 리네이밍 검증
212
+ // 📄 참고: docs/analysis/analysis-files-page-wsod.md
213
+
214
+ describe("_system_files 시스템 테이블 네이밍", () => {
215
+ it("rawSql로 실행되는 모든 SQL에 old 'files' 테이블 참조가 없다", async () => {
216
+ const executedSql: string[] = [];
217
+ const mockRawSql = async (sql: string) => {
218
+ executedSql.push(sql);
219
+ if (sql.includes("SUM")) return [{ total: "0" }];
220
+ if (sql.includes("INSERT")) return [];
221
+ return [];
222
+ };
223
+
224
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-naming-"));
225
+ const storage = createStorage(tmpDir, { rawSql: mockRawSql });
226
+
227
+ // store 트리거 (ensureFilesTable + checkQuota + recordFileToDb)
228
+ const file = new File(["test"], "test.txt", { type: "text/plain" });
229
+ try { await storage.store(file); } catch {}
230
+
231
+ // 모든 실행된 SQL에서 old 테이블명 'files'가 아닌 '_system_files'만 참조하는지 확인
232
+ // (files 단독 참조를 찾되, _system_files는 통과)
233
+ for (const sql of executedSql) {
234
+ // "FROM files", "INTO files", "TABLE files" 같은 old 패턴이 없어야 함
235
+ expect(sql).not.toMatch(/\bFROM\s+files\b(?!_)/i);
236
+ expect(sql).not.toMatch(/\bINTO\s+files\b(?!_)/i);
237
+ expect(sql).not.toMatch(/\bTABLE\s+files\b(?!_)/i);
238
+ }
239
+
240
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
241
+ });
242
+
243
+ it("쿼터 검증 SQL이 _system_files 테이블을 참조한다", async () => {
244
+ const executedSql: string[] = [];
245
+ const mockRawSql = async (sql: string) => {
246
+ executedSql.push(sql);
247
+ if (sql.includes("SUM")) return [{ total: "0" }];
248
+ if (sql.includes("INSERT")) return [];
249
+ return [];
250
+ };
251
+
252
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-quota-"));
253
+ const storage = createStorage(tmpDir, {
254
+ rawSql: mockRawSql,
255
+ storageQuota: 1000000000,
256
+ });
257
+
258
+ const file = new File(["small"], "small.txt");
259
+ try { await storage.store(file); } catch {}
260
+
261
+ // SUM 쿼리가 _system_files 테이블을 참조하는지 확인
262
+ const sumSql = executedSql.find(s => s.includes("SUM"));
263
+ if (sumSql) {
264
+ expect(sumSql).toContain("_system_files");
265
+ expect(sumSql).not.toMatch(/FROM\s+files[^_]/);
266
+ }
267
+
268
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
269
+ });
270
+
271
+ it("recordFileToDb SQL이 _system_files 테이블에 INSERT한다", async () => {
272
+ const executedSql: string[] = [];
273
+ const mockRawSql = async (sql: string) => {
274
+ executedSql.push(sql);
275
+ if (sql.includes("SUM")) return [{ total: "0" }];
276
+ return [];
277
+ };
278
+
279
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-insert-"));
280
+ const storage = createStorage(tmpDir, { rawSql: mockRawSql });
281
+
282
+ const file = new File(["data"], "data.txt", { type: "text/plain" });
283
+ try { await storage.store(file); } catch {}
284
+
285
+ const insertSql = executedSql.find(s => s.includes("INSERT"));
286
+ if (insertSql) {
287
+ expect(insertSql).toContain("_system_files");
288
+ expect(insertSql).not.toMatch(/INTO\s+files[^_]/);
289
+ }
290
+
291
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
292
+ });
293
+
294
+ it("delete SQL이 _system_files 테이블에서 삭제한다", async () => {
295
+ const executedSql: string[] = [];
296
+ const mockRawSql = async (sql: string) => {
297
+ executedSql.push(sql);
298
+ if (sql.includes("SUM")) return [{ total: "0" }];
299
+ return [];
300
+ };
301
+
302
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-delete-"));
303
+ const storage = createStorage(tmpDir, { rawSql: mockRawSql });
304
+
305
+ // store 후 delete
306
+ const file = new File(["delete-me"], "del.txt");
307
+ let storageId: string | undefined;
308
+ try { storageId = await storage.store(file); } catch {}
309
+ if (storageId) {
310
+ try { await storage.delete(storageId); } catch {}
311
+ }
312
+
313
+ const deleteSql = executedSql.find(s => s.includes("DELETE"));
314
+ if (deleteSql) {
315
+ expect(deleteSql).toContain("_system_files");
316
+ expect(deleteSql).not.toMatch(/FROM\s+files[^_]/);
317
+ }
318
+
319
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
320
+ });
321
+ });
@@ -281,4 +281,39 @@ describe("parseArgs()", () => {
281
281
  expect(err.statusCode).toBe(400);
282
282
  expect(err.name).toBe("GencowValidationError");
283
283
  });
284
+
285
+ // ─── 빈 스키마 passthrough (FormData 업로드 버그 회귀 방지) ────
286
+
287
+ it("빈 스키마 {} → args 전체 passthrough (FormData file 필드 보존)", () => {
288
+ const schema = {};
289
+ const args = { file: new File(["hello"], "test.txt"), _mutation: "upload.store" };
290
+ const result = parseArgs(schema, args);
291
+ // 빈 스키마이므로 args가 그대로 반환되어야 함 (file 포함)
292
+ expect(result).toBe(args); // 참조 동일
293
+ expect(result.file).toBeInstanceOf(File);
294
+ expect(result._mutation).toBe("upload.store");
295
+ });
296
+
297
+ it("빈 스키마 {} + 일반 객체 → passthrough", () => {
298
+ const schema = {};
299
+ const args = { name: "test", count: 42, nested: { a: 1 } };
300
+ const result = parseArgs(schema, args);
301
+ expect(result).toBe(args);
302
+ expect(result.name).toBe("test");
303
+ expect(result.count).toBe(42);
304
+ });
305
+
306
+ it("빈 스키마 {} + 빈 args {} → 빈 객체 반환", () => {
307
+ const result = parseArgs({}, {});
308
+ expect(result).toEqual({});
309
+ });
310
+
311
+ it("키가 있는 스키마는 여전히 지정된 키만 추출 (file 제거됨)", () => {
312
+ const schema = { title: v.string() };
313
+ const args = { title: "hello", file: "should-be-stripped", extra: 123 };
314
+ const result = parseArgs(schema, args);
315
+ expect(result).toEqual({ title: "hello" });
316
+ expect(result.file).toBeUndefined();
317
+ expect(result.extra).toBeUndefined();
318
+ });
284
319
  });
package/src/crud.ts CHANGED
@@ -354,7 +354,6 @@ export function crud<T extends PgTable>(table: T, options?: CrudOptions<T>) {
354
354
 
355
355
  const createDef = !enabledMethods.has('create') ? undefined : mutation(`${prefix}.create`, {
356
356
  public: isPublic,
357
- invalidates: [],
358
357
  handler: async (ctx: any, args: any) => {
359
358
  const user = isPublic ? null : ctx.auth.requireAuth();
360
359
 
@@ -386,7 +385,6 @@ export function crud<T extends PgTable>(table: T, options?: CrudOptions<T>) {
386
385
 
387
386
  const updateDef = !enabledMethods.has('update') ? undefined : mutation(`${prefix}.update`, {
388
387
  public: isPublic,
389
- invalidates: [],
390
388
  handler: async (ctx: any, args: any) => {
391
389
  if (!isPublic) ctx.auth.requireAuth();
392
390
 
@@ -428,7 +426,6 @@ export function crud<T extends PgTable>(table: T, options?: CrudOptions<T>) {
428
426
 
429
427
  const removeDef = !enabledMethods.has('remove') ? undefined : mutation(`${prefix}.remove`, {
430
428
  public: isPublic,
431
- invalidates: [],
432
429
  handler: async (ctx: any, args: any) => {
433
430
  if (!isPublic) ctx.auth.requireAuth();
434
431
 
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  export type { GencowCtx, AuthCtx, UserIdentity, QueryDef, MutationDef, RealtimeCtx, HttpActionDef, HttpActionRequest, HttpActionResponse, HttpActionHandler, AIContext, AIMessage, AIResult } from "./reactive";
9
- export { query, mutation, httpAction, invalidateQueries, buildRealtimeCtx, subscribe, unsubscribe, registerClient, deregisterClient, handleWsMessage, getQueryHandler, getQueryDef, getRegisteredQueries, getRegisteredMutations, getRegisteredHttpActions } from "./reactive";
9
+ export { query, mutation, httpAction, buildRealtimeCtx, subscribe, unsubscribe, registerClient, deregisterClient, handleWsMessage, getQueryHandler, getQueryDef, getRegisteredQueries, getRegisteredMutations, getRegisteredHttpActions } from "./reactive";
10
10
  export type { Storage } from "./storage";
11
11
  export { createScheduler, getSchedulerInfo } from "./scheduler";
12
12
  export type { Scheduler, ScheduleOptions, FailedJob } from "./scheduler";