@gencow/core 0.1.17 → 0.1.18

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/crud.js CHANGED
@@ -280,7 +280,6 @@ export function crud(table, options) {
280
280
  // ── create ────────────────────────────────────
281
281
  const createDef = !enabledMethods.has('create') ? undefined : mutation(`${prefix}.create`, {
282
282
  public: isPublic,
283
- invalidates: [],
284
283
  handler: async (ctx, args) => {
285
284
  const user = isPublic ? null : ctx.auth.requireAuth();
286
285
  let insertData = { ...args };
@@ -304,7 +303,6 @@ export function crud(table, options) {
304
303
  // ── update ────────────────────────────────────
305
304
  const updateDef = !enabledMethods.has('update') ? undefined : mutation(`${prefix}.update`, {
306
305
  public: isPublic,
307
- invalidates: [],
308
306
  handler: async (ctx, args) => {
309
307
  if (!isPublic)
310
308
  ctx.auth.requireAuth();
@@ -338,7 +336,6 @@ export function crud(table, options) {
338
336
  // ── remove ────────────────────────────────────
339
337
  const removeDef = !enabledMethods.has('remove') ? undefined : mutation(`${prefix}.remove`, {
340
338
  public: isPublic,
341
- invalidates: [],
342
339
  handler: async (ctx, args) => {
343
340
  if (!isPublic)
344
341
  ctx.auth.requireAuth();
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * All with Convex-compatible DX patterns.
6
6
  */
7
7
  export type { GencowCtx, AuthCtx, UserIdentity, QueryDef, MutationDef, RealtimeCtx, HttpActionDef, HttpActionRequest, HttpActionResponse, HttpActionHandler, AIContext, AIMessage, AIResult } from "./reactive";
8
- export { query, mutation, httpAction, invalidateQueries, buildRealtimeCtx, subscribe, unsubscribe, registerClient, deregisterClient, handleWsMessage, getQueryHandler, getQueryDef, getRegisteredQueries, getRegisteredMutations, getRegisteredHttpActions } from "./reactive";
8
+ export { query, mutation, httpAction, buildRealtimeCtx, subscribe, unsubscribe, registerClient, deregisterClient, handleWsMessage, getQueryHandler, getQueryDef, getRegisteredQueries, getRegisteredMutations, getRegisteredHttpActions } from "./reactive";
9
9
  export type { Storage } from "./storage";
10
10
  export { createScheduler, getSchedulerInfo } from "./scheduler";
11
11
  export type { Scheduler, ScheduleOptions, FailedJob } from "./scheduler";
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Provides: query, mutation, storage, scheduler, auth
5
5
  * All with Convex-compatible DX patterns.
6
6
  */
7
- export { query, mutation, httpAction, invalidateQueries, buildRealtimeCtx, subscribe, unsubscribe, registerClient, deregisterClient, handleWsMessage, getQueryHandler, getQueryDef, getRegisteredQueries, getRegisteredMutations, getRegisteredHttpActions } from "./reactive";
7
+ export { query, mutation, httpAction, buildRealtimeCtx, subscribe, unsubscribe, registerClient, deregisterClient, handleWsMessage, getQueryHandler, getQueryDef, getRegisteredQueries, getRegisteredMutations, getRegisteredHttpActions } from "./reactive";
8
8
  export { createScheduler, getSchedulerInfo } from "./scheduler";
9
9
  export { v, parseArgs, GencowValidationError } from "./v";
10
10
  export { withRetry } from "./retry";
@@ -19,6 +19,9 @@ export interface AuthCtx {
19
19
  */
20
20
  export interface RealtimeCtx {
21
21
  /**
22
+ * 특정 queryKey를 구독 중인 클라이언트에 데이터를 즉시 push합니다.
23
+ * 초고빈도 mutation (채팅 등)에서 query re-run 비용을 회피할 때 사용.
24
+ *
22
25
  * @param queryKey - 업데이트할 쿼리 키 (예: "tasks.list")
23
26
  * @param data - push할 데이터 (해당 query 결과와 동일한 타입)
24
27
  *
@@ -27,6 +30,23 @@ export interface RealtimeCtx {
27
30
  * ctx.realtime.emit("tasks.list", freshList);
28
31
  */
29
32
  emit(queryKey: string, data: unknown): void;
33
+ /**
34
+ * 수동 mutation에서 리얼타임 업데이트의 기본 권장 방식.
35
+ * mutation handler 완료 후 서버가 해당 query를 re-run하여 결과를 push합니다.
36
+ * 복잡한 JOIN query도 queryKey만 지정하면 됩니다.
37
+ *
38
+ * @param queryKey - re-run할 쿼리 키 (예: "dashboard.revenue")
39
+ *
40
+ * @example
41
+ * mutation("orders.place", {
42
+ * handler: async (ctx, args) => {
43
+ * await ctx.db.insert(orders).values(args);
44
+ * ctx.realtime.refresh("orders.list");
45
+ * ctx.realtime.refresh("dashboard.revenue"); // JOIN query도 OK
46
+ * }
47
+ * });
48
+ */
49
+ refresh(queryKey: string): void;
30
50
  }
31
51
  /**
32
52
  * 사용자 함수(query/mutation)에 주입되는 컨텍스트.
@@ -128,7 +148,6 @@ export interface QueryDef<TSchema = any, TReturn = any> {
128
148
  _return?: TReturn;
129
149
  }
130
150
  export interface MutationDef<TSchema = any, TReturn = any> {
131
- invalidates: string[];
132
151
  handler: MutationHandler<InferArgs<TSchema>, TReturn>;
133
152
  argsSchema?: TSchema;
134
153
  /** true = 인증 없이 접근 가능, false(기본) = auth 필수 (Secure by Default) */
@@ -170,7 +189,6 @@ export declare function query<TSchema = any, TReturn = any>(key: string, handler
170
189
  * ```typescript
171
190
  * // ✅ 권장: query와 동일한 (name, def) 패턴
172
191
  * mutation("tasks.create", {
173
- * invalidates: [],
174
192
  * args: { title: v.string() },
175
193
  * handler: async (ctx, args) => { ... },
176
194
  * });
@@ -178,19 +196,21 @@ export declare function query<TSchema = any, TReturn = any>(key: string, handler
178
196
  * // ✅ 객체 스타일 (하위 호환)
179
197
  * mutation({
180
198
  * name: "tasks.create",
181
- * invalidates: [],
182
199
  * handler: async (ctx) => { ... },
183
200
  * });
184
201
  *
185
202
  * // ⚠️ Legacy 배열 스타일 (비권장)
186
203
  * mutation(["tasks.list"], handler, "tasks.create");
187
204
  * ```
205
+ *
206
+ * @note invalidates 필드는 deprecated — 전달해도 무시됩니다.
207
+ * 리얼타임 UI 갱신에는 ctx.realtime.emit() 또는 ctx.realtime.refresh()를 사용하세요.
188
208
  */
189
209
  export declare function mutation<TSchema = any, TReturn = any>(nameOrInvalidatesOrDef: string | string[] | {
190
210
  name?: string;
191
211
  args?: TSchema;
192
212
  public?: boolean;
193
- invalidates: string[];
213
+ invalidates?: string[];
194
214
  handler: MutationHandler<InferArgs<TSchema>, TReturn>;
195
215
  }, handlerOrDef?: MutationHandler<InferArgs<TSchema>, TReturn> | {
196
216
  invalidates?: string[];
@@ -235,14 +255,10 @@ export declare function unsubscribe(ws: WSContext): void;
235
255
  /** Register a raw WS connection without any query subscription (e.g. admin dashboard) */
236
256
  export declare function registerClient(ws: WSContext): void;
237
257
  export declare function deregisterClient(ws: WSContext): void;
238
- /**
239
- * After a mutation, re-run invalidated queries and push results
240
- * to all subscribers — Convex의 자동 reactive 업데이트 재현
241
- */
242
258
  /**
243
259
  * mutation 실행 시점에 생성되는 RealtimeCtx.
244
- * emit() 호출 해당 queryKey를 구독 중인 WebSocket 클라이언트들에게
245
- * data즉시 push합니다.
260
+ * emit(): 데이터를 직접 push (초고빈도 mutation용).
261
+ * refresh(): queryKeypending 큐에 추가, mutation 완료 후 서버가 query re-run하여 push.
246
262
  *
247
263
  * 💡 Batching: 같은 queryKey에 대한 emit이 50ms 내에 여러 번 호출되면
248
264
  * 마지막 데이터만 push하여 불필요한 전송을 방지합니다.
@@ -252,6 +268,8 @@ export declare function deregisterClient(ws: WSContext): void;
252
268
  * @param options.httpCallback BaaS 모드: Platform WS Gateway에 HTTP로 emit 전달.
253
269
  * 설정되면 WS 직접 push 대신 이 콜백을 호출.
254
270
  * 로컬 dev에서는 미설정 → 기존 WS 직접 push 유지.
271
+ * @param options.queryMap query 레지스트리 — refresh()에서 query handler를 찾아 re-run.
272
+ * @param options.buildCtxForRefresh refresh 시 query handler에 전달할 ctx 생성 함수.
255
273
  */
256
274
  export declare function buildRealtimeCtx(options?: {
257
275
  httpCallback?: (event: {
@@ -259,18 +277,13 @@ export declare function buildRealtimeCtx(options?: {
259
277
  queryKey: string;
260
278
  data: unknown;
261
279
  }) => void;
262
- }): RealtimeCtx;
263
- /**
264
- * mutation이 끝난 후 호출되는 legacy fallback.
265
- * `ctx.realtime.emit()`을 사용하는 새 mutation에서는 빈 배열([])을 전달하면 됩니다.
266
- *
267
- * invalidate 신호만 broadcast하여 클라이언트가 re-fetch 여부를 결정하게 합니다.
268
- * (서버에서 쿼리를 재실행하지 않으므로 DB 부하 없음)
269
- *
270
- * @deprecated ctx.realtime.emit() 사용 권장
271
- * @param httpInvalidateCallback BaaS 모드: Platform WS Gateway에 HTTP로 invalidation 전달.
272
- */
273
- export declare function invalidateQueries(queryKeys: string[], ctx: GencowCtx, httpInvalidateCallback?: (queryKeys: string[]) => void): Promise<void>;
280
+ queryMap?: Map<string, QueryDef<any, any>>;
281
+ buildCtxForRefresh?: () => GencowCtx;
282
+ }): RealtimeCtx & {
283
+ _hasEmitted: boolean;
284
+ _pendingRefresh: string[];
285
+ _flushRefresh: () => Promise<void>;
286
+ };
274
287
  export declare function handleWsMessage(ws: WSContext, raw: string | ArrayBuffer): void;
275
288
  export declare function getQueryHandler(key: string): QueryHandler | undefined;
276
289
  export declare function getQueryDef(key: string): QueryDef | undefined;
package/dist/reactive.js CHANGED
@@ -45,7 +45,6 @@ let mutationCounter = 0;
45
45
  * ```typescript
46
46
  * // ✅ 권장: query와 동일한 (name, def) 패턴
47
47
  * mutation("tasks.create", {
48
- * invalidates: [],
49
48
  * args: { title: v.string() },
50
49
  * handler: async (ctx, args) => { ... },
51
50
  * });
@@ -53,38 +52,36 @@ let mutationCounter = 0;
53
52
  * // ✅ 객체 스타일 (하위 호환)
54
53
  * mutation({
55
54
  * name: "tasks.create",
56
- * invalidates: [],
57
55
  * handler: async (ctx) => { ... },
58
56
  * });
59
57
  *
60
58
  * // ⚠️ Legacy 배열 스타일 (비권장)
61
59
  * mutation(["tasks.list"], handler, "tasks.create");
62
60
  * ```
61
+ *
62
+ * @note invalidates 필드는 deprecated — 전달해도 무시됩니다.
63
+ * 리얼타임 UI 갱신에는 ctx.realtime.emit() 또는 ctx.realtime.refresh()를 사용하세요.
63
64
  */
64
65
  export function mutation(nameOrInvalidatesOrDef, handlerOrDef, name) {
65
- let invalidates;
66
66
  let argsSchema;
67
67
  let actualHandler;
68
68
  let mutName;
69
69
  let isPublic = false;
70
70
  if (typeof nameOrInvalidatesOrDef === "string") {
71
- // New primary style: mutation("name", { invalidates?, args?, public?, handler })
71
+ // New primary style: mutation("name", { args?, public?, handler })
72
72
  mutName = nameOrInvalidatesOrDef;
73
73
  const def = handlerOrDef;
74
- invalidates = def.invalidates || [];
75
74
  actualHandler = def.handler;
76
75
  argsSchema = def.args;
77
76
  isPublic = def.public === true;
78
77
  }
79
78
  else if (Array.isArray(nameOrInvalidatesOrDef)) {
80
- // Legacy style: mutation([...], handler, "name")
81
- invalidates = nameOrInvalidatesOrDef;
79
+ // Legacy style: mutation([...], handler, "name") — invalidates ignored
82
80
  actualHandler = handlerOrDef;
83
81
  mutName = name || `mutation_${++mutationCounter}`;
84
82
  }
85
83
  else {
86
- // Object style: mutation({ name?, invalidates, args?, public?, handler })
87
- invalidates = nameOrInvalidatesOrDef.invalidates;
84
+ // Object style: mutation({ name?, args?, public?, handler })
88
85
  actualHandler = nameOrInvalidatesOrDef.handler;
89
86
  argsSchema = nameOrInvalidatesOrDef.args;
90
87
  isPublic = nameOrInvalidatesOrDef.public === true;
@@ -97,7 +94,6 @@ export function mutation(nameOrInvalidatesOrDef, handlerOrDef, name) {
97
94
  }
98
95
  const def = {
99
96
  name: mutName,
100
- invalidates,
101
97
  handler: actualHandler,
102
98
  argsSchema,
103
99
  isPublic,
@@ -168,14 +164,10 @@ export function deregisterClient(ws) {
168
164
  clients.delete(ws);
169
165
  }
170
166
  }
171
- /**
172
- * After a mutation, re-run invalidated queries and push results
173
- * to all subscribers — Convex의 자동 reactive 업데이트 재현
174
- */
175
167
  /**
176
168
  * mutation 실행 시점에 생성되는 RealtimeCtx.
177
- * emit() 호출 해당 queryKey를 구독 중인 WebSocket 클라이언트들에게
178
- * data즉시 push합니다.
169
+ * emit(): 데이터를 직접 push (초고빈도 mutation용).
170
+ * refresh(): queryKeypending 큐에 추가, mutation 완료 후 서버가 query re-run하여 push.
179
171
  *
180
172
  * 💡 Batching: 같은 queryKey에 대한 emit이 50ms 내에 여러 번 호출되면
181
173
  * 마지막 데이터만 push하여 불필요한 전송을 방지합니다.
@@ -185,11 +177,16 @@ export function deregisterClient(ws) {
185
177
  * @param options.httpCallback BaaS 모드: Platform WS Gateway에 HTTP로 emit 전달.
186
178
  * 설정되면 WS 직접 push 대신 이 콜백을 호출.
187
179
  * 로컬 dev에서는 미설정 → 기존 WS 직접 push 유지.
180
+ * @param options.queryMap query 레지스트리 — refresh()에서 query handler를 찾아 re-run.
181
+ * @param options.buildCtxForRefresh refresh 시 query handler에 전달할 ctx 생성 함수.
188
182
  */
189
183
  export function buildRealtimeCtx(options) {
190
184
  const pendingEmits = new Map();
185
+ const _pendingRefresh = [];
186
+ let _hasEmitted = false;
191
187
  return {
192
188
  emit(queryKey, data) {
189
+ _hasEmitted = true;
193
190
  // 기존 pending timer가 있으면 취소 (debounce)
194
191
  const existing = pendingEmits.get(queryKey);
195
192
  if (existing)
@@ -220,38 +217,61 @@ export function buildRealtimeCtx(options) {
220
217
  }
221
218
  }, 50); // 50ms batch window
222
219
  pendingEmits.set(queryKey, { data, timer });
223
- }
220
+ },
221
+ refresh(queryKey) {
222
+ _hasEmitted = true; // 경고 억제
223
+ if (!_pendingRefresh.includes(queryKey)) {
224
+ _pendingRefresh.push(queryKey);
225
+ }
226
+ },
227
+ get _hasEmitted() { return _hasEmitted; },
228
+ get _pendingRefresh() { return [..._pendingRefresh]; },
229
+ async _flushRefresh() {
230
+ if (_pendingRefresh.length === 0)
231
+ return;
232
+ // queryMap이 없으면 refresh 동작 불가 (로그 경고)
233
+ const qMap = options?.queryMap ?? queryRegistry;
234
+ for (const key of _pendingRefresh) {
235
+ const queryDef = qMap.get(key);
236
+ if (!queryDef) {
237
+ console.warn(`[gencow] refresh("${key}"): query not found in registry. Skipping.`);
238
+ continue;
239
+ }
240
+ try {
241
+ // refresh용 ctx 생성 (mutation ctx와 동일한 DB/auth 스코프)
242
+ const refreshCtx = options?.buildCtxForRefresh?.() ?? {};
243
+ const result = await queryDef.handler(refreshCtx, {});
244
+ // emit과 동일한 경로로 push
245
+ if (options?.httpCallback) {
246
+ options.httpCallback({ type: "emit", queryKey: key, data: result });
247
+ }
248
+ else {
249
+ const clients = subscribers.get(key);
250
+ if (clients && clients.size > 0) {
251
+ const message = JSON.stringify({
252
+ type: "query:updated",
253
+ query: key,
254
+ data: result,
255
+ });
256
+ for (const ws of clients) {
257
+ try {
258
+ ws.send(message);
259
+ }
260
+ catch {
261
+ clients.delete(ws);
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }
267
+ catch (e) {
268
+ console.warn(`[gencow] refresh("${key}") failed:`, e instanceof Error ? e.message : e);
269
+ }
270
+ }
271
+ _pendingRefresh.length = 0;
272
+ },
224
273
  };
225
274
  }
226
- /**
227
- * mutation이 끝난 후 호출되는 legacy fallback.
228
- * `ctx.realtime.emit()`을 사용하는 새 mutation에서는 빈 배열([])을 전달하면 됩니다.
229
- *
230
- * invalidate 신호만 broadcast하여 클라이언트가 re-fetch 여부를 결정하게 합니다.
231
- * (서버에서 쿼리를 재실행하지 않으므로 DB 부하 없음)
232
- *
233
- * @deprecated ctx.realtime.emit() 사용 권장
234
- * @param httpInvalidateCallback BaaS 모드: Platform WS Gateway에 HTTP로 invalidation 전달.
235
- */
236
- export async function invalidateQueries(queryKeys, ctx, httpInvalidateCallback) {
237
- if (queryKeys.length === 0)
238
- return; // emit() 방식에서는 no-op
239
- // BaaS 모드: Platform WS Gateway에 HTTP callback
240
- if (httpInvalidateCallback) {
241
- httpInvalidateCallback(queryKeys);
242
- return;
243
- }
244
- // 로컬 dev: WS 직접 broadcast (기존 동작)
245
- const invalidateMsg = JSON.stringify({ type: "invalidate", queries: queryKeys });
246
- for (const ws of connectedClients) {
247
- try {
248
- ws.send(invalidateMsg);
249
- }
250
- catch {
251
- connectedClients.delete(ws);
252
- }
253
- }
254
- }
255
275
  // ─── WebSocket message handler ──────────────────────────
256
276
  export function handleWsMessage(ws, raw) {
257
277
  try {
@@ -0,0 +1,34 @@
1
+ /**
2
+ * packages/core/src/scoped-db.ts
3
+ *
4
+ * Creates a scoped (Proxy-wrapped) Drizzle DB instance that auto-injects
5
+ * schema-level access control filters from gencowTable() metadata.
6
+ *
7
+ * Key behaviors:
8
+ * - .select().from(gencowTable) → auto-inject filter into WHERE
9
+ * - .insert(table) / .update(table) / .delete(table) → inject filter for writes
10
+ * - .leftJoin(table) / .innerJoin(table) → detect and inject filter
11
+ * - .execute() → blocked (throws Error)
12
+ * - .query.tableName.findMany() → inject filter into relational queries
13
+ *
14
+ * Run tests: bun test packages/core/src/__tests__/scoped-db.test.ts
15
+ */
16
+ import type { GencowCtx } from "./reactive";
17
+ /**
18
+ * Wrap a Drizzle DB instance with access control Proxy.
19
+ *
20
+ * @param db - Raw Drizzle DB instance
21
+ * @param ctx - GencowCtx (provides auth for filter evaluation)
22
+ * @returns Proxy-wrapped DB with auto-filter injection
23
+ */
24
+ export declare function createScopedDb(db: any, ctx: GencowCtx): any;
25
+ /**
26
+ * Apply field-level access control to query results.
27
+ * Nullifies fields that the current user is not authorized to read.
28
+ *
29
+ * @param result - Query result (array or single object)
30
+ * @param table - The gencowTable used in the query
31
+ * @param ctx - GencowCtx for auth checks
32
+ * @returns Filtered result with unauthorized fields set to null
33
+ */
34
+ export declare function applyFieldAccess(result: any, table: any, ctx: GencowCtx): any;