gencow 0.1.109 → 0.1.111
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/core/index.js +15 -10
- package/lib/__tests__/readme-codegen.test.ts +18 -2
- package/lib/readme-codegen.mjs +28 -10
- package/package.json +1 -1
- package/server/index.js +18 -13
- package/server/index.js.map +2 -2
- package/templates/ai-chat/ai.ts +129 -62
- package/templates/ai.ts +129 -62
- package/templates/fullstack/ai.ts +129 -62
package/core/index.js
CHANGED
|
@@ -2027,7 +2027,8 @@ function crud(table, options) {
|
|
|
2027
2027
|
const countResult = await db.select({ count: drizzleCount() }).from(anyTable).where(whereClause);
|
|
2028
2028
|
return { data, total: Number(countResult[0]?.count ?? 0) };
|
|
2029
2029
|
}
|
|
2030
|
-
const
|
|
2030
|
+
const enabledMethods = new Set(options?.methods ?? ["list", "get", "create", "update", "remove"]);
|
|
2031
|
+
const listDef = !enabledMethods.has("list") ? void 0 : query(`${prefix}.list`, {
|
|
2031
2032
|
public: isPublic,
|
|
2032
2033
|
args: {
|
|
2033
2034
|
page: v.optional(v.number()),
|
|
@@ -2058,7 +2059,7 @@ function crud(table, options) {
|
|
|
2058
2059
|
};
|
|
2059
2060
|
}
|
|
2060
2061
|
});
|
|
2061
|
-
const getDef = query(`${prefix}.get`, {
|
|
2062
|
+
const getDef = !enabledMethods.has("get") ? void 0 : query(`${prefix}.get`, {
|
|
2062
2063
|
public: isPublic,
|
|
2063
2064
|
args: { id: idValidator },
|
|
2064
2065
|
handler: async (ctx, args) => {
|
|
@@ -2072,7 +2073,7 @@ function crud(table, options) {
|
|
|
2072
2073
|
return result ?? null;
|
|
2073
2074
|
}
|
|
2074
2075
|
});
|
|
2075
|
-
const createDef = mutation(`${prefix}.create`, {
|
|
2076
|
+
const createDef = !enabledMethods.has("create") ? void 0 : mutation(`${prefix}.create`, {
|
|
2076
2077
|
public: isPublic,
|
|
2077
2078
|
invalidates: [],
|
|
2078
2079
|
handler: async (ctx, args) => {
|
|
@@ -2085,14 +2086,14 @@ function crud(table, options) {
|
|
|
2085
2086
|
insertData = await options.hooks.beforeCreate(insertData);
|
|
2086
2087
|
}
|
|
2087
2088
|
const [result] = await ctx.db.insert(anyTable).values(insertData).returning();
|
|
2088
|
-
if (useRealtime) {
|
|
2089
|
+
if (useRealtime && enabledMethods.has("list")) {
|
|
2089
2090
|
const listResult = await fetchListWithTotal(ctx.db);
|
|
2090
2091
|
ctx.realtime.emit(`${prefix}.list`, listResult);
|
|
2091
2092
|
}
|
|
2092
2093
|
return result;
|
|
2093
2094
|
}
|
|
2094
2095
|
});
|
|
2095
|
-
const updateDef = mutation(`${prefix}.update`, {
|
|
2096
|
+
const updateDef = !enabledMethods.has("update") ? void 0 : mutation(`${prefix}.update`, {
|
|
2096
2097
|
public: isPublic,
|
|
2097
2098
|
invalidates: [],
|
|
2098
2099
|
handler: async (ctx, args) => {
|
|
@@ -2107,14 +2108,18 @@ function crud(table, options) {
|
|
|
2107
2108
|
}
|
|
2108
2109
|
const [result] = await ctx.db.update(anyTable).set(updateData).where(eq(pk, id)).returning();
|
|
2109
2110
|
if (useRealtime) {
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2111
|
+
if (enabledMethods.has("list")) {
|
|
2112
|
+
const listResult = await fetchListWithTotal(ctx.db);
|
|
2113
|
+
ctx.realtime.emit(`${prefix}.list`, listResult);
|
|
2114
|
+
}
|
|
2115
|
+
if (enabledMethods.has("get")) {
|
|
2116
|
+
ctx.realtime.emit(`${prefix}.get`, result);
|
|
2117
|
+
}
|
|
2113
2118
|
}
|
|
2114
2119
|
return result;
|
|
2115
2120
|
}
|
|
2116
2121
|
});
|
|
2117
|
-
const removeDef = mutation(`${prefix}.remove`, {
|
|
2122
|
+
const removeDef = !enabledMethods.has("remove") ? void 0 : mutation(`${prefix}.remove`, {
|
|
2118
2123
|
public: isPublic,
|
|
2119
2124
|
invalidates: [],
|
|
2120
2125
|
handler: async (ctx, args) => {
|
|
@@ -2125,7 +2130,7 @@ function crud(table, options) {
|
|
|
2125
2130
|
} else {
|
|
2126
2131
|
await ctx.db.delete(anyTable).where(eq(pk, args.id));
|
|
2127
2132
|
}
|
|
2128
|
-
if (useRealtime) {
|
|
2133
|
+
if (useRealtime && enabledMethods.has("list")) {
|
|
2129
2134
|
const listResult = await fetchListWithTotal(ctx.db);
|
|
2130
2135
|
ctx.realtime.emit(`${prefix}.list`, listResult);
|
|
2131
2136
|
}
|
|
@@ -223,8 +223,24 @@ describe("buildAiPrompt — AI Vibe-Coding 프롬프트", () => {
|
|
|
223
223
|
|
|
224
224
|
it("mutation 제한 경고 포함", () => {
|
|
225
225
|
const md = buildAiPrompt(SIMPLE_API, ["tasks"]);
|
|
226
|
-
expect(md).toContain("mutation
|
|
227
|
-
expect(md).toContain("
|
|
226
|
+
expect(md).toContain("mutation: 30초");
|
|
227
|
+
expect(md).toContain("FUNCTION_TIMEOUT");
|
|
228
|
+
expect(md).toContain("별도 mutation으로 분리");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("mutation 간 공유 로직 패턴 포함", () => {
|
|
232
|
+
const md = buildAiPrompt(SIMPLE_API, ["tasks"]);
|
|
233
|
+
expect(md).toContain("mutation 간 로직 공유");
|
|
234
|
+
expect(md).toContain("일반 async 함수로 분리");
|
|
235
|
+
expect(md).toContain("mutation은 래핑만");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("AI 우회 금지 목록 포함", () => {
|
|
239
|
+
const md = buildAiPrompt(SIMPLE_API, ["tasks"]);
|
|
240
|
+
expect(md).toContain("절대 금지");
|
|
241
|
+
expect(md).toContain("openai-direct.ts");
|
|
242
|
+
expect(md).toContain("ai.ts 수정 또는 우회 금지");
|
|
243
|
+
expect(md).toContain("SDK 직접 설치/사용 금지");
|
|
228
244
|
});
|
|
229
245
|
|
|
230
246
|
it("크론 잡 사용법 포함", () => {
|
package/lib/readme-codegen.mjs
CHANGED
|
@@ -296,16 +296,28 @@ export function buildAiPrompt(apiObj, namespaces) {
|
|
|
296
296
|
md += `- 반드시 useQuery와 useMutation을 사용해서 데이터와 연결해줘.\n`;
|
|
297
297
|
md += `- fetch()를 직접 호출하거나 apiPost() 같은 래퍼를 만들지 마.\n`;
|
|
298
298
|
md += `- gencow/api.ts는 자동 생성된 파일이야. 수동으로 만들지 마.\n`;
|
|
299
|
+
md += `- ⚠️ gencow/ai.ts는 자동 생성 파일이야. 절대 수정하지 마. 에러 발생 시 환경변수(.env)를 확인하거나 \`npx gencow add AI\`로 재설치해.\n`;
|
|
299
300
|
md += `- gencow/index.ts의 re-export는 export * as moduleName from "./moduleName" 패턴을 써.\n`;
|
|
300
301
|
md += ` Module, Mod 같은 접미사를 붙이지 마.\n`;
|
|
301
302
|
md += `\n`;
|
|
302
|
-
md += `⚠️ mutation
|
|
303
|
-
md += `-
|
|
304
|
-
md += `-
|
|
305
|
-
md +=
|
|
306
|
-
md +=
|
|
307
|
-
md +=
|
|
308
|
-
md +=
|
|
303
|
+
md += `⚠️ mutation 제한 (타임아웃):\n`;
|
|
304
|
+
md += `- query: 30초, mutation: 30초, cron: 10분. 이 시간을 초과하면 FUNCTION_TIMEOUT 에러.\n`;
|
|
305
|
+
md += `- ✅ 1순위: 각 단계를 별도 mutation으로 분리하고, 프론트엔드에서 순차 호출해.\n`;
|
|
306
|
+
md += ` 예: 크롤링(Step1) → 필터링(Step2) → 요약(Step3) 각각 별도 mutation으로 분리.\n`;
|
|
307
|
+
md += ` 프론트엔드: await crawl(); await filter(); await summarize();\n`;
|
|
308
|
+
md += `- ⚠️ 보조: ctx.scheduler.runAfter(0, "module.nextStep", { sessionId }) — 로컬 dev에서만 안정적. 클라우드에서는 앱이 idle 상태로 전환 시 타이머가 소멸돼.\n`;
|
|
309
|
+
md += `\n`;
|
|
310
|
+
md += `⚠️ mutation 간 로직 공유:\n`;
|
|
311
|
+
md += `- mutation().handler()를 다른 mutation에서 직접 호출하면 안 돼.\n`;
|
|
312
|
+
md += `- ✅ 공유 로직은 일반 async 함수로 분리하고, 각 mutation에서 래핑해:\n`;
|
|
313
|
+
md += ` // 공유 함수 (일반 async 함수)\n`;
|
|
314
|
+
md += ` async function filterBatch(ctx, ids) { /* 실제 로직 */ }\n`;
|
|
315
|
+
md += ` // mutation은 래핑만\n`;
|
|
316
|
+
md += ` export const filter = mutation("pipeline.filter", {\n`;
|
|
317
|
+
md += ` handler: async (ctx, args) => filterBatch(ctx, args.ids),\n`;
|
|
318
|
+
md += ` });\n`;
|
|
319
|
+
md += `- ❌ fetch("/api/mutation") self-fetch 패턴은 사용하지 마.\n`;
|
|
320
|
+
|
|
309
321
|
md += `\n`;
|
|
310
322
|
md += `크론 잡 (예약 작업):\n`;
|
|
311
323
|
md += ` gencow/crons.ts에서 cronJobs()로 선언\n`;
|
|
@@ -369,12 +381,18 @@ export function buildAiPrompt(apiObj, namespaces) {
|
|
|
369
381
|
md += ` const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));\n`;
|
|
370
382
|
md += ` const hex = [...new Uint8Array(hash)].map(b => b.toString(16).padStart(2, "0")).join("");\n`;
|
|
371
383
|
md += `- 직접 구현한 simpleHash 같은 32비트 해시는 충돌 위험이 있으니 SHA-256을 사용해.\n`;
|
|
372
|
-
md +=
|
|
384
|
+
md += `⚠️ AI 호출 규칙 (반드시 준수):\n`;
|
|
385
|
+
md += `- ✅ AI 호출은 반드시 import { ai } from "./ai"를 사용해:\n`;
|
|
373
386
|
md += ` import { ai } from "./ai";\n`;
|
|
374
387
|
md += ` const result = await ai.chat({ messages: [{ role: "user", content: "안녕" }] });\n`;
|
|
375
|
-
md += ` ❌ import OpenAI from "openai"; → 시크릿 관리, 비용 추적, 보안 문제 발생\n`;
|
|
376
|
-
md += ` ❌ OpenAI/Anthropic SDK 직접 설치 금지\n`;
|
|
377
388
|
md += ` ✅ ai.chat()은 로컬(OPENAI_API_KEY)과 클라우드(Gencow 프록시) 자동 전환\n`;
|
|
389
|
+
md += `- ❌ 절대 금지:\n`;
|
|
390
|
+
md += ` - import OpenAI from "openai" — SDK 직접 설치/사용 금지\n`;
|
|
391
|
+
md += ` - fetch("https://api.openai.com/...") — API 직접 호출 금지\n`;
|
|
392
|
+
md += ` - openai-direct.ts, ai-wrapper.ts 같은 별도 래퍼 파일 생성 금지\n`;
|
|
393
|
+
md += ` - ai.ts 수정 또는 우회 금지 (프록시 우회 시 크레딧 차감 불가 + 장애 위험)\n`;
|
|
394
|
+
md += `- AI 에러 발생 시: .env의 OPENAI_API_KEY 확인 또는 npx gencow add AI로 재설치\n`;
|
|
395
|
+
md += `- JSON 응답 필요 시: ai.generateObject({ schema: z.object({...}), prompt: "..." })\n`;
|
|
378
396
|
md += `- AI 컴포넌트가 없으면: gencow add AI 로 설치해.\n`;
|
|
379
397
|
md += `- RAG가 필요하면: gencow add RAG (AI 자동 포함).\n`;
|
|
380
398
|
md += `- 메모리가 필요하면: gencow add Memory.\n`;
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -2076,7 +2076,8 @@ function crud(table, options) {
|
|
|
2076
2076
|
const countResult = await db.select({ count: drizzleCount() }).from(anyTable).where(whereClause);
|
|
2077
2077
|
return { data, total: Number(countResult[0]?.count ?? 0) };
|
|
2078
2078
|
}
|
|
2079
|
-
const
|
|
2079
|
+
const enabledMethods = new Set(options?.methods ?? ["list", "get", "create", "update", "remove"]);
|
|
2080
|
+
const listDef = !enabledMethods.has("list") ? void 0 : query(`${prefix}.list`, {
|
|
2080
2081
|
public: isPublic,
|
|
2081
2082
|
args: {
|
|
2082
2083
|
page: v.optional(v.number()),
|
|
@@ -2107,7 +2108,7 @@ function crud(table, options) {
|
|
|
2107
2108
|
};
|
|
2108
2109
|
}
|
|
2109
2110
|
});
|
|
2110
|
-
const getDef = query(`${prefix}.get`, {
|
|
2111
|
+
const getDef = !enabledMethods.has("get") ? void 0 : query(`${prefix}.get`, {
|
|
2111
2112
|
public: isPublic,
|
|
2112
2113
|
args: { id: idValidator },
|
|
2113
2114
|
handler: async (ctx, args) => {
|
|
@@ -2121,7 +2122,7 @@ function crud(table, options) {
|
|
|
2121
2122
|
return result ?? null;
|
|
2122
2123
|
}
|
|
2123
2124
|
});
|
|
2124
|
-
const createDef = mutation(`${prefix}.create`, {
|
|
2125
|
+
const createDef = !enabledMethods.has("create") ? void 0 : mutation(`${prefix}.create`, {
|
|
2125
2126
|
public: isPublic,
|
|
2126
2127
|
invalidates: [],
|
|
2127
2128
|
handler: async (ctx, args) => {
|
|
@@ -2134,14 +2135,14 @@ function crud(table, options) {
|
|
|
2134
2135
|
insertData = await options.hooks.beforeCreate(insertData);
|
|
2135
2136
|
}
|
|
2136
2137
|
const [result] = await ctx.db.insert(anyTable).values(insertData).returning();
|
|
2137
|
-
if (useRealtime) {
|
|
2138
|
+
if (useRealtime && enabledMethods.has("list")) {
|
|
2138
2139
|
const listResult = await fetchListWithTotal(ctx.db);
|
|
2139
2140
|
ctx.realtime.emit(`${prefix}.list`, listResult);
|
|
2140
2141
|
}
|
|
2141
2142
|
return result;
|
|
2142
2143
|
}
|
|
2143
2144
|
});
|
|
2144
|
-
const updateDef = mutation(`${prefix}.update`, {
|
|
2145
|
+
const updateDef = !enabledMethods.has("update") ? void 0 : mutation(`${prefix}.update`, {
|
|
2145
2146
|
public: isPublic,
|
|
2146
2147
|
invalidates: [],
|
|
2147
2148
|
handler: async (ctx, args) => {
|
|
@@ -2156,14 +2157,18 @@ function crud(table, options) {
|
|
|
2156
2157
|
}
|
|
2157
2158
|
const [result] = await ctx.db.update(anyTable).set(updateData).where(eq(pk, id)).returning();
|
|
2158
2159
|
if (useRealtime) {
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2160
|
+
if (enabledMethods.has("list")) {
|
|
2161
|
+
const listResult = await fetchListWithTotal(ctx.db);
|
|
2162
|
+
ctx.realtime.emit(`${prefix}.list`, listResult);
|
|
2163
|
+
}
|
|
2164
|
+
if (enabledMethods.has("get")) {
|
|
2165
|
+
ctx.realtime.emit(`${prefix}.get`, result);
|
|
2166
|
+
}
|
|
2162
2167
|
}
|
|
2163
2168
|
return result;
|
|
2164
2169
|
}
|
|
2165
2170
|
});
|
|
2166
|
-
const removeDef = mutation(`${prefix}.remove`, {
|
|
2171
|
+
const removeDef = !enabledMethods.has("remove") ? void 0 : mutation(`${prefix}.remove`, {
|
|
2167
2172
|
public: isPublic,
|
|
2168
2173
|
invalidates: [],
|
|
2169
2174
|
handler: async (ctx, args) => {
|
|
@@ -2174,7 +2179,7 @@ function crud(table, options) {
|
|
|
2174
2179
|
} else {
|
|
2175
2180
|
await ctx.db.delete(anyTable).where(eq(pk, args.id));
|
|
2176
2181
|
}
|
|
2177
|
-
if (useRealtime) {
|
|
2182
|
+
if (useRealtime && enabledMethods.has("list")) {
|
|
2178
2183
|
const listResult = await fetchListWithTotal(ctx.db);
|
|
2179
2184
|
ctx.realtime.emit(`${prefix}.list`, listResult);
|
|
2180
2185
|
}
|
|
@@ -82244,7 +82249,7 @@ async function executeWithTimeout(fn, type, functionName) {
|
|
|
82244
82249
|
new Promise((_, reject) => {
|
|
82245
82250
|
controller.signal.addEventListener("abort", () => {
|
|
82246
82251
|
const err = new Error(
|
|
82247
|
-
`Function "${functionName}" exceeded ${timeoutMs / 1e3}s time limit.
|
|
82252
|
+
`Function "${functionName}" exceeded ${timeoutMs / 1e3}s time limit. Split into smaller mutations and call them sequentially from the frontend. Limits: query 30s, mutation 30s, httpAction 5min, cron 10min.`
|
|
82248
82253
|
);
|
|
82249
82254
|
err.code = "FUNCTION_TIMEOUT";
|
|
82250
82255
|
reject(err);
|
|
@@ -83373,10 +83378,10 @@ async function main() {
|
|
|
83373
83378
|
} : void 0),
|
|
83374
83379
|
scheduler: scheduler ?? {
|
|
83375
83380
|
runAfter: () => {
|
|
83376
|
-
throw new Error("Scheduler not initialized");
|
|
83381
|
+
throw new Error("Scheduler not initialized \u2014 add gencow/crons.ts to enable. Note: In BaaS mode, scheduler.runAfter() is sleep-unsafe (timers are lost when app goes idle). Recommended: split into separate mutations and call sequentially from frontend.");
|
|
83377
83382
|
},
|
|
83378
83383
|
runAt: () => {
|
|
83379
|
-
throw new Error("Scheduler not initialized");
|
|
83384
|
+
throw new Error("Scheduler not initialized \u2014 add gencow/crons.ts to enable. Note: In BaaS mode, scheduler.runAt() is sleep-unsafe.");
|
|
83380
83385
|
},
|
|
83381
83386
|
cancel: () => false,
|
|
83382
83387
|
cron: () => {
|