@gencow/core 0.1.0 → 0.1.1
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 +14 -0
- package/dist/reactive.js +20 -8
- package/package.json +1 -1
- package/src/__tests__/reactive.test.ts +65 -0
- package/src/reactive.ts +38 -9
package/dist/reactive.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ export interface QueryDef<TSchema = any, TReturn = any> {
|
|
|
53
53
|
key: string;
|
|
54
54
|
handler: QueryHandler<InferArgs<TSchema>, TReturn>;
|
|
55
55
|
argsSchema?: TSchema;
|
|
56
|
+
/** true = 인증 없이 접근 가능, false(기본) = auth 필수 (Secure by Default) */
|
|
57
|
+
isPublic: boolean;
|
|
56
58
|
_args?: InferArgs<TSchema>;
|
|
57
59
|
_return?: TReturn;
|
|
58
60
|
}
|
|
@@ -60,16 +62,28 @@ export interface MutationDef<TSchema = any, TReturn = any> {
|
|
|
60
62
|
invalidates: string[];
|
|
61
63
|
handler: MutationHandler<InferArgs<TSchema>, TReturn>;
|
|
62
64
|
argsSchema?: TSchema;
|
|
65
|
+
/** true = 인증 없이 접근 가능, false(기본) = auth 필수 (Secure by Default) */
|
|
66
|
+
isPublic: boolean;
|
|
63
67
|
_args?: InferArgs<TSchema>;
|
|
64
68
|
_return?: TReturn;
|
|
65
69
|
}
|
|
70
|
+
declare global {
|
|
71
|
+
var __gencow_queryRegistry: Map<string, QueryDef<any, any>>;
|
|
72
|
+
var __gencow_mutationRegistry: (MutationDef<any, any> & {
|
|
73
|
+
name: string;
|
|
74
|
+
})[];
|
|
75
|
+
var __gencow_subscribers: Map<string, Set<WSContext>>;
|
|
76
|
+
var __gencow_connectedClients: Set<WSContext>;
|
|
77
|
+
}
|
|
66
78
|
export declare function query<TSchema = any, TReturn = any>(key: string, handlerOrDef: QueryHandler<InferArgs<TSchema>, TReturn> | {
|
|
67
79
|
args?: TSchema;
|
|
80
|
+
public?: boolean;
|
|
68
81
|
handler: QueryHandler<InferArgs<TSchema>, TReturn>;
|
|
69
82
|
}): QueryDef<TSchema, TReturn>;
|
|
70
83
|
export declare function mutation<TSchema = any, TReturn = any>(invalidatesOrDef: string[] | {
|
|
71
84
|
name?: string;
|
|
72
85
|
args?: TSchema;
|
|
86
|
+
public?: boolean;
|
|
73
87
|
invalidates: string[];
|
|
74
88
|
handler: MutationHandler<InferArgs<TSchema>, TReturn>;
|
|
75
89
|
}, handler?: MutationHandler<InferArgs<TSchema>, TReturn>, name?: string): MutationDef<TSchema, TReturn>;
|
package/dist/reactive.js
CHANGED
|
@@ -1,25 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
if (!globalThis.__gencow_queryRegistry)
|
|
2
|
+
globalThis.__gencow_queryRegistry = new Map();
|
|
3
|
+
if (!globalThis.__gencow_mutationRegistry)
|
|
4
|
+
globalThis.__gencow_mutationRegistry = [];
|
|
5
|
+
if (!globalThis.__gencow_subscribers)
|
|
6
|
+
globalThis.__gencow_subscribers = new Map();
|
|
7
|
+
if (!globalThis.__gencow_connectedClients)
|
|
8
|
+
globalThis.__gencow_connectedClients = new Set();
|
|
9
|
+
const queryRegistry = globalThis.__gencow_queryRegistry;
|
|
10
|
+
const mutationRegistry = globalThis.__gencow_mutationRegistry;
|
|
11
|
+
const subscribers = globalThis.__gencow_subscribers;
|
|
5
12
|
/**
|
|
6
13
|
* Every WebSocket client that ever establishes a connection is tracked here.
|
|
7
14
|
* This allows broadcasting invalidation events to clients that never subscribed
|
|
8
15
|
* to a specific query (e.g. the admin dashboard's raw WebSocket connection).
|
|
9
16
|
*/
|
|
10
|
-
const connectedClients =
|
|
17
|
+
const connectedClients = globalThis.__gencow_connectedClients;
|
|
11
18
|
// ─── Public API (Convex-style) ──────────────────────────
|
|
12
19
|
export function query(key, handlerOrDef) {
|
|
13
20
|
let handler;
|
|
14
21
|
let argsSchema;
|
|
22
|
+
let isPublic = false;
|
|
15
23
|
if (typeof handlerOrDef === "function") {
|
|
16
24
|
handler = handlerOrDef;
|
|
17
25
|
}
|
|
18
26
|
else {
|
|
19
27
|
handler = handlerOrDef.handler;
|
|
20
28
|
argsSchema = handlerOrDef.args;
|
|
29
|
+
isPublic = handlerOrDef.public === true;
|
|
21
30
|
}
|
|
22
|
-
const def = { key, handler, argsSchema };
|
|
31
|
+
const def = { key, handler, argsSchema, isPublic };
|
|
23
32
|
queryRegistry.set(key, def);
|
|
24
33
|
return def;
|
|
25
34
|
}
|
|
@@ -29,6 +38,7 @@ export function mutation(invalidatesOrDef, handler, name) {
|
|
|
29
38
|
let argsSchema;
|
|
30
39
|
let actualHandler;
|
|
31
40
|
let mutName;
|
|
41
|
+
let isPublic = false;
|
|
32
42
|
if (Array.isArray(invalidatesOrDef)) {
|
|
33
43
|
// Legacy style: mutation([...], handler, "name")
|
|
34
44
|
invalidates = invalidatesOrDef;
|
|
@@ -36,17 +46,19 @@ export function mutation(invalidatesOrDef, handler, name) {
|
|
|
36
46
|
mutName = name || `mutation_${++mutationCounter}`;
|
|
37
47
|
}
|
|
38
48
|
else {
|
|
39
|
-
// New object style: mutation({ name?, invalidates, args?, handler })
|
|
49
|
+
// New object style: mutation({ name?, invalidates, args?, public?, handler })
|
|
40
50
|
invalidates = invalidatesOrDef.invalidates;
|
|
41
51
|
actualHandler = invalidatesOrDef.handler;
|
|
42
52
|
argsSchema = invalidatesOrDef.args;
|
|
53
|
+
isPublic = invalidatesOrDef.public === true;
|
|
43
54
|
mutName = invalidatesOrDef.name || name || `mutation_${++mutationCounter}`;
|
|
44
55
|
}
|
|
45
56
|
const def = {
|
|
46
57
|
name: mutName,
|
|
47
58
|
invalidates,
|
|
48
59
|
handler: actualHandler,
|
|
49
|
-
argsSchema
|
|
60
|
+
argsSchema,
|
|
61
|
+
isPublic,
|
|
50
62
|
};
|
|
51
63
|
mutationRegistry.push(def);
|
|
52
64
|
return def;
|
package/package.json
CHANGED
|
@@ -170,3 +170,68 @@ describe("emit() 방식과 legacy invalidateQueries() 혼용", () => {
|
|
|
170
170
|
deregisterClient(wsConnected);
|
|
171
171
|
});
|
|
172
172
|
});
|
|
173
|
+
|
|
174
|
+
// ─── Secure by Default: public 플래그 테스트 ─────────────────────────────────
|
|
175
|
+
|
|
176
|
+
import { query, mutation, getQueryDef, getRegisteredMutations } from "../reactive";
|
|
177
|
+
|
|
178
|
+
describe("Secure by Default — public 플래그", () => {
|
|
179
|
+
it("query() 기본값은 isPublic === false (auth 필수)", () => {
|
|
180
|
+
const q = query("sectest.private", {
|
|
181
|
+
handler: async () => [],
|
|
182
|
+
});
|
|
183
|
+
expect(q.isPublic).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("query({ public: true }) 시 isPublic === true", () => {
|
|
187
|
+
const q = query("sectest.public", {
|
|
188
|
+
public: true,
|
|
189
|
+
handler: async () => [],
|
|
190
|
+
});
|
|
191
|
+
expect(q.isPublic).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("query() legacy handler 형식도 isPublic === false", () => {
|
|
195
|
+
const q = query("sectest.legacy", async () => []);
|
|
196
|
+
expect(q.isPublic).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("getQueryDef()로 조회해도 isPublic 정보가 유지된다", () => {
|
|
200
|
+
query("sectest.lookup", { public: true, handler: async () => "ok" });
|
|
201
|
+
const def = getQueryDef("sectest.lookup");
|
|
202
|
+
expect(def).toBeDefined();
|
|
203
|
+
expect(def!.isPublic).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("mutation() 기본값은 isPublic === false", () => {
|
|
207
|
+
const m = mutation({
|
|
208
|
+
name: "sectest.mut.private",
|
|
209
|
+
invalidates: [],
|
|
210
|
+
handler: async () => ({ ok: true }),
|
|
211
|
+
});
|
|
212
|
+
expect(m.isPublic).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("mutation({ public: true }) 시 isPublic === true", () => {
|
|
216
|
+
const m = mutation({
|
|
217
|
+
name: "sectest.mut.public",
|
|
218
|
+
invalidates: [],
|
|
219
|
+
public: true,
|
|
220
|
+
handler: async () => ({ ok: true }),
|
|
221
|
+
});
|
|
222
|
+
expect(m.isPublic).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("mutation() legacy array 형식도 isPublic === false", () => {
|
|
226
|
+
const m = mutation(["some.key"], async () => ({ ok: true }), "sectest.mut.legacy");
|
|
227
|
+
expect(m.isPublic).toBe(false);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("getRegisteredMutations()에서 isPublic 정보가 노출된다", () => {
|
|
231
|
+
const all = getRegisteredMutations();
|
|
232
|
+
const pub = all.find(m => m.name === "sectest.mut.public");
|
|
233
|
+
const priv = all.find(m => m.name === "sectest.mut.private");
|
|
234
|
+
expect(pub?.isPublic).toBe(true);
|
|
235
|
+
expect(priv?.isPublic).toBe(false);
|
|
236
|
+
});
|
|
237
|
+
});
|
package/src/reactive.ts
CHANGED
|
@@ -65,6 +65,8 @@ export interface QueryDef<TSchema = any, TReturn = any> {
|
|
|
65
65
|
key: string;
|
|
66
66
|
handler: QueryHandler<InferArgs<TSchema>, TReturn>;
|
|
67
67
|
argsSchema?: TSchema;
|
|
68
|
+
/** true = 인증 없이 접근 가능, false(기본) = auth 필수 (Secure by Default) */
|
|
69
|
+
isPublic: boolean;
|
|
68
70
|
_args?: InferArgs<TSchema>;
|
|
69
71
|
_return?: TReturn;
|
|
70
72
|
}
|
|
@@ -73,40 +75,64 @@ export interface MutationDef<TSchema = any, TReturn = any> {
|
|
|
73
75
|
invalidates: string[];
|
|
74
76
|
handler: MutationHandler<InferArgs<TSchema>, TReturn>;
|
|
75
77
|
argsSchema?: TSchema;
|
|
78
|
+
/** true = 인증 없이 접근 가능, false(기본) = auth 필수 (Secure by Default) */
|
|
79
|
+
isPublic: boolean;
|
|
76
80
|
_args?: InferArgs<TSchema>;
|
|
77
81
|
_return?: TReturn;
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
// ─── Registry ───────────────────────────────────────────
|
|
85
|
+
//
|
|
86
|
+
// globalThis 기반 — 서버 번들(인라인)과 node_modules/@gencow/core 양쪽에서
|
|
87
|
+
// 동일한 레지스트리 인스턴스를 공유. Dual-Module Registry 버그 방지.
|
|
88
|
+
// See: docs/analysis/analysis-dual-module-registry.md
|
|
89
|
+
|
|
90
|
+
declare global {
|
|
91
|
+
// eslint-disable-next-line no-var
|
|
92
|
+
var __gencow_queryRegistry: Map<string, QueryDef<any, any>>;
|
|
93
|
+
// eslint-disable-next-line no-var
|
|
94
|
+
var __gencow_mutationRegistry: (MutationDef<any, any> & { name: string })[];
|
|
95
|
+
// eslint-disable-next-line no-var
|
|
96
|
+
var __gencow_subscribers: Map<string, Set<WSContext>>;
|
|
97
|
+
// eslint-disable-next-line no-var
|
|
98
|
+
var __gencow_connectedClients: Set<WSContext>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!globalThis.__gencow_queryRegistry) globalThis.__gencow_queryRegistry = new Map();
|
|
102
|
+
if (!globalThis.__gencow_mutationRegistry) globalThis.__gencow_mutationRegistry = [];
|
|
103
|
+
if (!globalThis.__gencow_subscribers) globalThis.__gencow_subscribers = new Map();
|
|
104
|
+
if (!globalThis.__gencow_connectedClients) globalThis.__gencow_connectedClients = new Set();
|
|
81
105
|
|
|
82
|
-
const queryRegistry =
|
|
83
|
-
const mutationRegistry
|
|
84
|
-
const subscribers =
|
|
106
|
+
const queryRegistry = globalThis.__gencow_queryRegistry;
|
|
107
|
+
const mutationRegistry = globalThis.__gencow_mutationRegistry;
|
|
108
|
+
const subscribers = globalThis.__gencow_subscribers;
|
|
85
109
|
|
|
86
110
|
/**
|
|
87
111
|
* Every WebSocket client that ever establishes a connection is tracked here.
|
|
88
112
|
* This allows broadcasting invalidation events to clients that never subscribed
|
|
89
113
|
* to a specific query (e.g. the admin dashboard's raw WebSocket connection).
|
|
90
114
|
*/
|
|
91
|
-
const connectedClients =
|
|
115
|
+
const connectedClients = globalThis.__gencow_connectedClients;
|
|
92
116
|
|
|
93
117
|
// ─── Public API (Convex-style) ──────────────────────────
|
|
94
118
|
|
|
95
119
|
export function query<TSchema = any, TReturn = any>(
|
|
96
120
|
key: string,
|
|
97
|
-
handlerOrDef: QueryHandler<InferArgs<TSchema>, TReturn> | { args?: TSchema; handler: QueryHandler<InferArgs<TSchema>, TReturn> }
|
|
121
|
+
handlerOrDef: QueryHandler<InferArgs<TSchema>, TReturn> | { args?: TSchema; public?: boolean; handler: QueryHandler<InferArgs<TSchema>, TReturn> }
|
|
98
122
|
): QueryDef<TSchema, TReturn> {
|
|
99
123
|
let handler: QueryHandler<InferArgs<TSchema>, TReturn>;
|
|
100
124
|
let argsSchema: TSchema | undefined;
|
|
125
|
+
let isPublic = false;
|
|
101
126
|
|
|
102
127
|
if (typeof handlerOrDef === "function") {
|
|
103
128
|
handler = handlerOrDef;
|
|
104
129
|
} else {
|
|
105
130
|
handler = handlerOrDef.handler;
|
|
106
131
|
argsSchema = handlerOrDef.args;
|
|
132
|
+
isPublic = handlerOrDef.public === true;
|
|
107
133
|
}
|
|
108
134
|
|
|
109
|
-
const def: QueryDef<TSchema, TReturn> = { key, handler, argsSchema };
|
|
135
|
+
const def: QueryDef<TSchema, TReturn> = { key, handler, argsSchema, isPublic };
|
|
110
136
|
queryRegistry.set(key, def);
|
|
111
137
|
return def;
|
|
112
138
|
}
|
|
@@ -114,7 +140,7 @@ export function query<TSchema = any, TReturn = any>(
|
|
|
114
140
|
let mutationCounter = 0;
|
|
115
141
|
|
|
116
142
|
export function mutation<TSchema = any, TReturn = any>(
|
|
117
|
-
invalidatesOrDef: string[] | { name?: string; args?: TSchema; invalidates: string[]; handler: MutationHandler<InferArgs<TSchema>, TReturn> },
|
|
143
|
+
invalidatesOrDef: string[] | { name?: string; args?: TSchema; public?: boolean; invalidates: string[]; handler: MutationHandler<InferArgs<TSchema>, TReturn> },
|
|
118
144
|
handler?: MutationHandler<InferArgs<TSchema>, TReturn>,
|
|
119
145
|
name?: string
|
|
120
146
|
): MutationDef<TSchema, TReturn> {
|
|
@@ -122,6 +148,7 @@ export function mutation<TSchema = any, TReturn = any>(
|
|
|
122
148
|
let argsSchema: TSchema | undefined;
|
|
123
149
|
let actualHandler: MutationHandler<InferArgs<TSchema>, TReturn>;
|
|
124
150
|
let mutName: string;
|
|
151
|
+
let isPublic = false;
|
|
125
152
|
|
|
126
153
|
if (Array.isArray(invalidatesOrDef)) {
|
|
127
154
|
// Legacy style: mutation([...], handler, "name")
|
|
@@ -129,17 +156,19 @@ export function mutation<TSchema = any, TReturn = any>(
|
|
|
129
156
|
actualHandler = handler!;
|
|
130
157
|
mutName = name || `mutation_${++mutationCounter}`;
|
|
131
158
|
} else {
|
|
132
|
-
// New object style: mutation({ name?, invalidates, args?, handler })
|
|
159
|
+
// New object style: mutation({ name?, invalidates, args?, public?, handler })
|
|
133
160
|
invalidates = invalidatesOrDef.invalidates;
|
|
134
161
|
actualHandler = invalidatesOrDef.handler;
|
|
135
162
|
argsSchema = invalidatesOrDef.args;
|
|
163
|
+
isPublic = invalidatesOrDef.public === true;
|
|
136
164
|
mutName = invalidatesOrDef.name || name || `mutation_${++mutationCounter}`;
|
|
137
165
|
}
|
|
138
166
|
const def: MutationDef<TSchema, TReturn> & { name: string } = {
|
|
139
167
|
name: mutName,
|
|
140
168
|
invalidates,
|
|
141
169
|
handler: actualHandler,
|
|
142
|
-
argsSchema
|
|
170
|
+
argsSchema,
|
|
171
|
+
isPublic,
|
|
143
172
|
};
|
|
144
173
|
mutationRegistry.push(def);
|
|
145
174
|
return def;
|