@gencow/core 0.1.27 → 0.1.28

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.
Files changed (83) hide show
  1. package/dist/document-types.d.ts +65 -0
  2. package/dist/document-types.js +15 -0
  3. package/dist/grounded-answer-types.d.ts +62 -0
  4. package/dist/grounded-answer-types.js +6 -0
  5. package/dist/index.d.ts +10 -1
  6. package/dist/index.js +4 -0
  7. package/dist/rag-ingest-types.d.ts +39 -0
  8. package/dist/rag-ingest-types.js +1 -0
  9. package/dist/rag-operations-types.d.ts +81 -0
  10. package/dist/rag-operations-types.js +1 -0
  11. package/dist/rag-schema.d.ts +1557 -0
  12. package/dist/rag-schema.js +87 -0
  13. package/dist/reactive.d.ts +13 -0
  14. package/dist/rls-db.d.ts +9 -2
  15. package/dist/runtime-env-policy.d.ts +5 -0
  16. package/dist/runtime-env-policy.js +56 -0
  17. package/dist/search-types.d.ts +83 -0
  18. package/dist/search-types.js +1 -0
  19. package/dist/server.d.ts +1 -2
  20. package/dist/server.js +0 -1
  21. package/dist/storage-shared.d.ts +36 -0
  22. package/dist/storage-shared.js +39 -0
  23. package/dist/storage.d.ts +2 -26
  24. package/dist/storage.js +19 -15
  25. package/dist/workflow-types.d.ts +3 -1
  26. package/package.json +8 -7
  27. package/src/document-types.ts +95 -0
  28. package/src/grounded-answer-types.ts +78 -0
  29. package/src/index.ts +66 -1
  30. package/src/rag-ingest-types.ts +52 -0
  31. package/src/rag-operations-types.ts +90 -0
  32. package/src/rag-schema.ts +94 -0
  33. package/src/reactive.ts +13 -0
  34. package/src/rls-db.ts +9 -4
  35. package/src/runtime-env-policy.ts +66 -0
  36. package/src/search-types.ts +91 -0
  37. package/src/server.ts +1 -2
  38. package/src/storage-shared.ts +74 -0
  39. package/src/storage.ts +29 -46
  40. package/src/workflow-types.ts +3 -1
  41. package/src/__tests__/auth.test.ts +0 -118
  42. package/src/__tests__/crons.test.ts +0 -83
  43. package/src/__tests__/crud-codegen-integration.test.ts +0 -246
  44. package/src/__tests__/crud-owner-rls.test.ts +0 -387
  45. package/src/__tests__/crud.test.ts +0 -930
  46. package/src/__tests__/dist-exports.test.ts +0 -176
  47. package/src/__tests__/fixtures/basic/auth.ts +0 -32
  48. package/src/__tests__/fixtures/basic/drizzle.config.ts +0 -12
  49. package/src/__tests__/fixtures/basic/index.ts +0 -6
  50. package/src/__tests__/fixtures/basic/migrations/0000_last_warstar.sql +0 -75
  51. package/src/__tests__/fixtures/basic/migrations/meta/0000_snapshot.json +0 -497
  52. package/src/__tests__/fixtures/basic/migrations/meta/_journal.json +0 -13
  53. package/src/__tests__/fixtures/basic/schema.ts +0 -51
  54. package/src/__tests__/fixtures/basic/tasks.ts +0 -15
  55. package/src/__tests__/fixtures/common/auth-schema.ts +0 -67
  56. package/src/__tests__/helpers/basic-rls-fixture.ts +0 -135
  57. package/src/__tests__/helpers/pglite-migrations.ts +0 -32
  58. package/src/__tests__/helpers/pglite-rls-session.ts +0 -51
  59. package/src/__tests__/helpers/seed-like-fill.ts +0 -202
  60. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +0 -50
  61. package/src/__tests__/httpaction.test.ts +0 -122
  62. package/src/__tests__/image-optimization.test.ts +0 -648
  63. package/src/__tests__/load.test.ts +0 -389
  64. package/src/__tests__/network-sim.test.ts +0 -319
  65. package/src/__tests__/reactive.test.ts +0 -479
  66. package/src/__tests__/retry.test.ts +0 -113
  67. package/src/__tests__/rls-crud-basic.test.ts +0 -317
  68. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +0 -117
  69. package/src/__tests__/rls-custom-mutation-handlers.test.ts +0 -142
  70. package/src/__tests__/rls-custom-query-handlers.test.ts +0 -128
  71. package/src/__tests__/rls-db-leased-connection.test.ts +0 -118
  72. package/src/__tests__/rls-session-and-policies.test.ts +0 -228
  73. package/src/__tests__/scheduler-durable-v2.test.ts +0 -288
  74. package/src/__tests__/scheduler-durable.test.ts +0 -173
  75. package/src/__tests__/scheduler-exec.test.ts +0 -328
  76. package/src/__tests__/scheduler.test.ts +0 -187
  77. package/src/__tests__/storage.test.ts +0 -334
  78. package/src/__tests__/tsconfig.json +0 -8
  79. package/src/__tests__/validator.test.ts +0 -323
  80. package/src/__tests__/workflow.test.ts +0 -606
  81. package/src/__tests__/ws-integration.test.ts +0 -309
  82. package/src/__tests__/ws-scale.test.ts +0 -241
  83. package/src/auth.ts +0 -155
@@ -0,0 +1,87 @@
1
+ import { bigint, boolean, doublePrecision, integer, jsonb, pgTable, text, timestamp, vector } from "drizzle-orm/pg-core";
2
+ function buildScopeColumns() {
3
+ return {
4
+ id: text("id").primaryKey(),
5
+ corpus: text("corpus").notNull(),
6
+ visibilityScope: text("visibility_scope").notNull(),
7
+ ownerUserId: text("owner_user_id"),
8
+ createdAt: timestamp("created_at").defaultNow().notNull(),
9
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
10
+ };
11
+ }
12
+ export const ragCorpora = pgTable("rag_corpora", {
13
+ ...buildScopeColumns(),
14
+ title: text("title").notNull(),
15
+ description: text("description"),
16
+ allowExternalProviders: boolean("allow_external_providers").default(false).notNull(),
17
+ retentionDays: integer("retention_days"),
18
+ metadata: jsonb("metadata").default({}).notNull(),
19
+ });
20
+ export const ragSources = pgTable("rag_sources", {
21
+ ...buildScopeColumns(),
22
+ storageId: text("storage_id").notNull(),
23
+ sourceKey: text("source_key").notNull(),
24
+ sourceTitle: text("source_title").notNull(),
25
+ mimeType: text("mime_type").notNull(),
26
+ byteSize: bigint("byte_size", { mode: "number" }).notNull(),
27
+ sourceChecksum: text("source_checksum").notNull(),
28
+ convertProvider: text("convert_provider").notNull(),
29
+ convertStatus: text("convert_status").notNull(),
30
+ markdown: text("markdown").notNull(),
31
+ text: text("text").notNull(),
32
+ warnings: jsonb("warnings").default([]).notNull(),
33
+ metadata: jsonb("metadata").default({}).notNull(),
34
+ });
35
+ export const ragSections = pgTable("rag_sections", {
36
+ ...buildScopeColumns(),
37
+ sourceId: text("source_id").notNull(),
38
+ sectionIndex: integer("section_index").notNull(),
39
+ sectionPath: jsonb("section_path").default([]).notNull(),
40
+ title: text("title"),
41
+ depth: integer("depth").notNull(),
42
+ pageStart: integer("page_start"),
43
+ pageEnd: integer("page_end"),
44
+ charStart: integer("char_start").notNull(),
45
+ charEnd: integer("char_end").notNull(),
46
+ });
47
+ export const ragChunks = pgTable("rag_chunks", {
48
+ ...buildScopeColumns(),
49
+ sourceId: text("source_id").notNull(),
50
+ sectionId: text("section_id"),
51
+ chunkIndex: integer("chunk_index").notNull(),
52
+ chunkText: text("chunk_text").notNull(),
53
+ lexicalText: text("lexical_text").notNull(),
54
+ embedding: vector("embedding", { dimensions: 1536 }),
55
+ pageStart: integer("page_start"),
56
+ pageEnd: integer("page_end"),
57
+ chunkChecksum: text("chunk_checksum").notNull(),
58
+ metadata: jsonb("metadata").default({}).notNull(),
59
+ });
60
+ export const ragIngestJobs = pgTable("rag_ingest_jobs", {
61
+ ...buildScopeColumns(),
62
+ workflowId: text("workflow_id").notNull(),
63
+ sourceId: text("source_id").notNull(),
64
+ status: text("status").notNull(),
65
+ stage: text("stage").notNull(),
66
+ providerTrace: jsonb("provider_trace").default({}).notNull(),
67
+ metrics: jsonb("metrics").default({}).notNull(),
68
+ error: text("error"),
69
+ startedAt: timestamp("started_at").defaultNow().notNull(),
70
+ completedAt: timestamp("completed_at"),
71
+ });
72
+ export const ragOperationMetrics = pgTable("rag_operation_metrics", {
73
+ id: text("id").primaryKey(),
74
+ appId: text("app_id").notNull(),
75
+ corpus: text("corpus").notNull(),
76
+ visibilityScope: text("visibility_scope").notNull(),
77
+ ownerUserId: text("owner_user_id"),
78
+ operation: text("operation").notNull(),
79
+ jobId: text("job_id"),
80
+ workflowId: text("workflow_id"),
81
+ sourceId: text("source_id"),
82
+ metricName: text("metric_name").notNull(),
83
+ metricValue: doublePrecision("metric_value").notNull(),
84
+ unit: text("unit"),
85
+ metadata: jsonb("metadata").default({}).notNull(),
86
+ recordedAt: timestamp("recorded_at").defaultNow().notNull(),
87
+ });
@@ -2,6 +2,9 @@ import type { WSContext } from "hono/ws";
2
2
  import type { Storage } from "./storage.js";
3
3
  import type { Scheduler } from "./scheduler.js";
4
4
  import { type InferArgs } from "./v.js";
5
+ import type { HybridSearchOptions, SearchOptions, SearchResponse, VectorSearchOptions } from "./search-types.js";
6
+ import type { GencowServicesCtx } from "./document-types.js";
7
+ import type { GroundingRuntime } from "./grounded-answer-types.js";
5
8
  export interface UserIdentity {
6
9
  id: string;
7
10
  email: string;
@@ -101,8 +104,18 @@ export interface GencowCtx {
101
104
  realtime: RealtimeCtx;
102
105
  /** 재시도 — ctx.retry(fn, opts) — exponential backoff + jitter */
103
106
  retry: <T>(fn: () => Promise<T>, options?: import("./retry.js").RetryOptions) => Promise<T>;
107
+ /** 프레임워크 서비스 헬퍼 — workflow 전용 service는 별도 ctx에서 노출 */
108
+ services: GencowServicesCtx;
104
109
  /** AI 헬퍼 */
105
110
  ai?: AIContext;
111
+ /** Full-text / hybrid search helper */
112
+ search: (table: string, query: string, options: SearchOptions) => Promise<SearchResponse>;
113
+ /** Vector / semantic search helper */
114
+ vectorSearch: (table: string, options: VectorSearchOptions) => Promise<SearchResponse>;
115
+ /** Hybrid search helper (lexical + vector) */
116
+ hybridSearch: (table: string, query: string, options: HybridSearchOptions) => Promise<SearchResponse>;
117
+ /** Grounded answer helper over canonical rag_* tables */
118
+ grounding?: GroundingRuntime;
106
119
  }
107
120
  type QueryHandler<TArgs = any, TReturn = any> = (ctx: GencowCtx, args: TArgs) => Promise<TReturn>;
108
121
  type MutationHandler<TArgs = any, TReturn = any> = (ctx: GencowCtx, args: TArgs) => Promise<TReturn>;
package/dist/rls-db.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import type { PgAsyncDatabase } from "drizzle-orm/pg-core";
2
1
  /**
3
2
  * RLS DB wrapper — execution paths for `withRlsConnection`:
4
3
  * 1. **Reuse outer Drizzle transaction** (`reuseOuterConnection`): same connection, apply GUCs then run `fn`.
@@ -24,6 +23,13 @@ export type RlsSessionContext = {
24
23
  */
25
24
  vars?: Record<string, string>;
26
25
  };
26
+ type RlsDrizzleDatabaseLike = {
27
+ session: unknown;
28
+ _: {
29
+ session?: unknown;
30
+ };
31
+ transaction: (callback: (tx: unknown) => unknown | Promise<unknown>, ...rest: unknown[]) => Promise<unknown>;
32
+ };
27
33
  /**
28
34
  * pg `Pool.connect()`-style client: BEGIN → apply RLS GUCs → `fn` → COMMIT on success,
29
35
  * or ROLLBACK if anything fails (including failed COMMIT). Always `release()` in `finally`.
@@ -44,4 +50,5 @@ export declare function withRlsLeasedConnection<T>(leased: {
44
50
  *
45
51
  * `db.transaction()` still injects the same variables at the start of the callback transaction.
46
52
  */
47
- export declare function createRlsDb(db: PgAsyncDatabase<any, any, any, any>, rls: RlsSessionContext): PgAsyncDatabase<any, any, any, any>;
53
+ export declare function createRlsDb<TDb extends RlsDrizzleDatabaseLike>(db: TDb, rls: RlsSessionContext): TDb;
54
+ export {};
@@ -0,0 +1,5 @@
1
+ export declare function isReservedTenantRuntimeEnvKey(key: string): boolean;
2
+ export declare function filterTenantRuntimeEnvVars(vars: Record<string, string>): {
3
+ allowed: Record<string, string>;
4
+ rejectedKeys: string[];
5
+ };
@@ -0,0 +1,56 @@
1
+ const RESERVED_TENANT_RUNTIME_ENV_KEYS = new Set([
2
+ "PORT",
3
+ "DATABASE_URL",
4
+ "GENCOW_DB_URL",
5
+ "BETTER_AUTH_SECRET",
6
+ "BETTER_AUTH_URL",
7
+ "IS_PLATFORM",
8
+ "GENCOW_PLATFORM_CONFIG_FILE",
9
+ "GENCOW_PLATFORM_URL",
10
+ "GENCOW_PLATFORM_DB",
11
+ "PLATFORM_OPENAI_KEY",
12
+ "PLATFORM_GOOGLE_KEY",
13
+ "PLATFORM_INTERNAL_SECRET",
14
+ "INVITE_ONLY",
15
+ "RUNNER_TYPE",
16
+ "PGBOUNCER_PORT",
17
+ "GENCOW_FUNCTIONS",
18
+ "GENCOW_STORAGE",
19
+ "GENCOW_MIGRATIONS",
20
+ "GENCOW_APP_NAME",
21
+ "GENCOW_APP_DATA_DIR",
22
+ "GENCOW_INTERNAL_TOKEN",
23
+ "GENCOW_CRON_TOKEN",
24
+ "GENCOW_AI_PROXY_URL",
25
+ "GENCOW_AI_PROXY_URL_ALT",
26
+ "GENCOW_AI_PROXY_TOKEN",
27
+ "GENCOW_METERING_URL",
28
+ "GENCOW_METERING_URL_ALT",
29
+ "GENCOW_START_REASON",
30
+ "GENCOW_RESTART_ID",
31
+ "GENCOW_SHUTDOWN_MARKER_PATH",
32
+ "GENCOW_SKIP_MIGRATION",
33
+ "GENCOW_DB_MAX_CONNECTIONS",
34
+ "GENCOW_MEMORY_MB",
35
+ "BUN_JSC_forceRAMSize",
36
+ "MIMALLOC_PURGE_DELAY",
37
+ "NODE_PATH",
38
+ ]);
39
+ const RESERVED_TENANT_RUNTIME_ENV_PREFIXES = ["__GENCOW_", "GENCOW_DOCUMENT_", "GENCOW_WARM_"];
40
+ export function isReservedTenantRuntimeEnvKey(key) {
41
+ const normalized = key.trim();
42
+ return (RESERVED_TENANT_RUNTIME_ENV_KEYS.has(normalized) ||
43
+ RESERVED_TENANT_RUNTIME_ENV_PREFIXES.some((prefix) => normalized.startsWith(prefix)));
44
+ }
45
+ export function filterTenantRuntimeEnvVars(vars) {
46
+ const allowed = {};
47
+ const rejectedKeys = [];
48
+ for (const [key, value] of Object.entries(vars)) {
49
+ if (isReservedTenantRuntimeEnvKey(key)) {
50
+ rejectedKeys.push(key);
51
+ continue;
52
+ }
53
+ allowed[key] = value;
54
+ }
55
+ return { allowed, rejectedKeys };
56
+ }
@@ -0,0 +1,83 @@
1
+ export type SearchPrimitive = string | number | boolean;
2
+ export type SearchScope = {
3
+ corpus: string;
4
+ visibility: "private" | "shared" | "public";
5
+ ownerUserId?: string;
6
+ };
7
+ export type SearchFilter = {
8
+ eq?: Record<string, SearchPrimitive>;
9
+ in?: Record<string, SearchPrimitive[]>;
10
+ range?: Record<string, {
11
+ gte?: string | number;
12
+ lte?: string | number;
13
+ }>;
14
+ };
15
+ export type SearchOptions = {
16
+ fields: [string, ...string[]];
17
+ limit?: number;
18
+ offset?: number;
19
+ filters?: SearchFilter;
20
+ scope: SearchScope;
21
+ };
22
+ export type VectorSearchTuning = {
23
+ minScore?: number;
24
+ };
25
+ export type VectorSearchOptions = Omit<SearchOptions, "fields"> & {
26
+ vector: number[];
27
+ vectorField?: string;
28
+ tuning?: VectorSearchTuning;
29
+ };
30
+ export type HybridSearchFusionTuning = {
31
+ mode?: "rrf";
32
+ rrfK?: number;
33
+ keywordWeight?: number;
34
+ vectorWeight?: number;
35
+ };
36
+ export type HybridSearchTuning = {
37
+ keywordCandidateLimit?: number;
38
+ vectorCandidateLimit?: number;
39
+ minFusedScore?: number;
40
+ fusion?: HybridSearchFusionTuning;
41
+ };
42
+ export type HybridSearchOptions = SearchOptions & {
43
+ vector: number[];
44
+ vectorField?: string;
45
+ fusion?: "rrf";
46
+ keywordCandidateLimit?: number;
47
+ vectorCandidateLimit?: number;
48
+ tuning?: HybridSearchTuning;
49
+ };
50
+ export type SearchHit = {
51
+ id: string | number;
52
+ row: Record<string, unknown>;
53
+ score: number;
54
+ scores?: {
55
+ keyword?: number;
56
+ vector?: number;
57
+ fused?: number;
58
+ };
59
+ matchedBy: Array<"keyword" | "vector">;
60
+ };
61
+ export type SearchResponse = {
62
+ items: SearchHit[];
63
+ meta: {
64
+ engine: "tsvector" | "pgroonga";
65
+ tier: "free" | "pro" | "scale";
66
+ limit: number;
67
+ offset: number;
68
+ nextOffset?: number;
69
+ fusion?: "rrf";
70
+ mode?: "keyword" | "vector" | "hybrid";
71
+ };
72
+ };
73
+ export type SearchTierConfig = {
74
+ plan: "free" | "pro" | "scale";
75
+ engine: "tsvector" | "pgroonga";
76
+ hybridSearch: boolean;
77
+ locale: "english" | "multilingual";
78
+ extensions: {
79
+ vector: boolean;
80
+ pgroonga: boolean;
81
+ };
82
+ degradedReason?: string;
83
+ };
@@ -0,0 +1 @@
1
+ export {};
package/dist/server.d.ts CHANGED
@@ -6,6 +6,5 @@
6
6
  * bundled into user functions which run in Firecracker.
7
7
  */
8
8
  export { createStorage, storageRoutes } from "./storage.js";
9
- export type { StorageImageTierConfig } from "./storage.js";
9
+ export type { StorageImageTierConfig, StoredFile } from "./storage.js";
10
10
  export { createScheduler, getSchedulerInfo } from "./scheduler.js";
11
- export { authMiddleware, authRoutes, getUsers } from "./auth.js";
package/dist/server.js CHANGED
@@ -7,4 +7,3 @@
7
7
  */
8
8
  export { createStorage, storageRoutes } from "./storage.js";
9
9
  export { createScheduler, getSchedulerInfo } from "./scheduler.js";
10
- export { authMiddleware, authRoutes, getUsers } from "./auth.js";
@@ -0,0 +1,36 @@
1
+ /** 파일 업로드 최대 크기: 50MB (하드코딩 — 사용자가 오버라이드 불가) */
2
+ export declare const MAX_FILE_SIZE: number;
3
+ /** 기본 스토리지 쿼터: 1GB */
4
+ export declare const DEFAULT_STORAGE_QUOTA: number;
5
+ export interface StorageFile {
6
+ id: string;
7
+ name: string;
8
+ size: number;
9
+ type: string;
10
+ path: string;
11
+ }
12
+ export interface StoredFile {
13
+ id: string;
14
+ name: string;
15
+ size: number;
16
+ type: string;
17
+ buffer: Buffer;
18
+ }
19
+ export interface StorageOptions {
20
+ rawSql?: (sql: string, params?: unknown[]) => Promise<unknown[]>;
21
+ storageQuota?: number;
22
+ }
23
+ export interface Storage {
24
+ store(file: File | Blob, filename?: string): Promise<string>;
25
+ storeBuffer(buffer: Buffer, filename: string, type?: string): Promise<string>;
26
+ get(storageId: string): Promise<StoredFile | null>;
27
+ getUrl(storageId: string): string;
28
+ getMeta(storageId: string): Promise<StorageFile | null>;
29
+ delete(storageId: string): Promise<void>;
30
+ }
31
+ export declare function formatBytes(bytes: number): string;
32
+ export declare function loadStorageMeta(params: {
33
+ storageId: string;
34
+ dir: string;
35
+ metaStore: Map<string, StorageFile>;
36
+ }): Promise<StorageFile | null>;
@@ -0,0 +1,39 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ /** 파일 업로드 최대 크기: 50MB (하드코딩 — 사용자가 오버라이드 불가) */
4
+ export const MAX_FILE_SIZE = 50 * 1024 * 1024;
5
+ /** 기본 스토리지 쿼터: 1GB */
6
+ export const DEFAULT_STORAGE_QUOTA = 1024 * 1024 * 1024;
7
+ export function formatBytes(bytes) {
8
+ if (bytes < 1024)
9
+ return `${bytes}B`;
10
+ if (bytes < 1024 * 1024)
11
+ return `${(bytes / 1024).toFixed(1)}KB`;
12
+ if (bytes < 1024 * 1024 * 1024)
13
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
14
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
15
+ }
16
+ export async function loadStorageMeta(params) {
17
+ const cached = params.metaStore.get(params.storageId);
18
+ if (cached) {
19
+ return cached;
20
+ }
21
+ const filePath = path.join(params.dir, params.storageId);
22
+ try {
23
+ const raw = await fs.readFile(`${filePath}.meta`, "utf-8");
24
+ const parsed = JSON.parse(raw);
25
+ const stat = await fs.stat(filePath);
26
+ const meta = {
27
+ id: params.storageId,
28
+ name: typeof parsed.name === "string" ? parsed.name : params.storageId,
29
+ size: typeof parsed.size === "number" ? parsed.size : stat.size,
30
+ type: typeof parsed.type === "string" ? parsed.type : "application/octet-stream",
31
+ path: filePath,
32
+ };
33
+ params.metaStore.set(params.storageId, meta);
34
+ return meta;
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
package/dist/storage.d.ts CHANGED
@@ -1,28 +1,5 @@
1
- interface StorageFile {
2
- id: string;
3
- name: string;
4
- size: number;
5
- type: string;
6
- path: string;
7
- }
8
- export interface StorageOptions {
9
- /** Raw SQL 실행 함수 — DB 자동 기록 + 쿼터 검증에 필요 */
10
- rawSql?: (sql: string, params?: unknown[]) => Promise<unknown[]>;
11
- /** 앱별 스토리지 쿼터 (bytes). 0 = 무제한. 기본: 1GB */
12
- storageQuota?: number;
13
- }
14
- export interface Storage {
15
- /** Store a file and return a storageId — Convex의 ctx.storage.store() */
16
- store(file: File | Blob, filename?: string): Promise<string>;
17
- /** Store from raw buffer */
18
- storeBuffer(buffer: Buffer, filename: string, type?: string): Promise<string>;
19
- /** Get a serving URL for the file — Convex의 ctx.storage.getUrl() */
20
- getUrl(storageId: string): string;
21
- /** Get file metadata */
22
- getMeta(storageId: string): Promise<StorageFile | null>;
23
- /** Delete a stored file — Convex의 ctx.storage.delete() */
24
- delete(storageId: string): Promise<void>;
25
- }
1
+ import type { Storage, StorageOptions } from "./storage-shared.js";
2
+ export type { Storage, StorageFile, StorageOptions, StoredFile } from "./storage-shared.js";
26
3
  /**
27
4
  * Create a storage instance — Convex storage 패턴 재현
28
5
  *
@@ -75,4 +52,3 @@ export declare function storageRoutes(storage: ReturnType<typeof createStorage>,
75
52
  json: (data: unknown, status?: number) => Response;
76
53
  body: (data: unknown, status: number, headers: Record<string, string>) => Response;
77
54
  }) => Promise<Response>;
78
- export {};
package/dist/storage.js CHANGED
@@ -2,20 +2,7 @@ import * as fs from "fs/promises";
2
2
  import * as fsSync from "fs";
3
3
  import * as path from "path";
4
4
  import * as crypto from "crypto";
5
- // ─── Constants ──────────────────────────────────────────
6
- /** 파일 업로드 최대 크기: 50MB (하드코딩 — 사용자가 오버라이드 불가) */
7
- const MAX_FILE_SIZE = 50 * 1024 * 1024;
8
- /** 기본 스토리지 쿼터: 1GB */
9
- const DEFAULT_STORAGE_QUOTA = 1024 * 1024 * 1024;
10
- function formatBytes(bytes) {
11
- if (bytes < 1024)
12
- return `${bytes}B`;
13
- if (bytes < 1024 * 1024)
14
- return `${(bytes / 1024).toFixed(1)}KB`;
15
- if (bytes < 1024 * 1024 * 1024)
16
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
17
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
18
- }
5
+ import { DEFAULT_STORAGE_QUOTA, MAX_FILE_SIZE, formatBytes, loadStorageMeta, } from "./storage-shared.js";
19
6
  // ─── Implementation ─────────────────────────────────────
20
7
  const metaStore = new Map();
21
8
  /**
@@ -166,8 +153,25 @@ export function createStorage(dir = "./uploads", options) {
166
153
  getUrl(storageId) {
167
154
  return `/api/storage/${storageId}`;
168
155
  },
156
+ async get(storageId) {
157
+ const meta = await loadStorageMeta({ storageId, dir, metaStore });
158
+ if (!meta) {
159
+ return null;
160
+ }
161
+ const buffer = await fs.readFile(meta.path).catch(() => null);
162
+ if (!buffer) {
163
+ return null;
164
+ }
165
+ return {
166
+ id: meta.id,
167
+ name: meta.name,
168
+ size: meta.size,
169
+ type: meta.type,
170
+ buffer,
171
+ };
172
+ },
169
173
  async getMeta(storageId) {
170
- return metaStore.get(storageId) || null;
174
+ return loadStorageMeta({ storageId, dir, metaStore });
171
175
  },
172
176
  async delete(storageId) {
173
177
  const meta = metaStore.get(storageId);
@@ -1,4 +1,5 @@
1
1
  import type { GencowCtx } from "./reactive.js";
2
+ import type { WorkflowDocumentServicesCtx } from "./document-types.js";
2
3
  import type { InferArgs } from "./v.js";
3
4
  export type WorkflowStatus = "pending" | "running" | "completed" | "failed";
4
5
  export type WorkflowDuration = number | string;
@@ -53,9 +54,10 @@ export interface WorkflowSignalResult {
53
54
  export interface WorkflowResumePayload {
54
55
  workflowId: string;
55
56
  }
56
- export interface WorkflowCtx extends GencowCtx {
57
+ export interface WorkflowCtx extends Omit<GencowCtx, "services"> {
57
58
  workflowId: string;
58
59
  workflowName: string;
60
+ services: GencowCtx["services"] & WorkflowDocumentServicesCtx;
59
61
  step<TResult>(name: string, run: () => Promise<TResult>): Promise<TResult>;
60
62
  sleep(duration: WorkflowDuration): Promise<void>;
61
63
  waitForEvent<TPayload = unknown>(name: string, timeout?: WorkflowDuration): Promise<TPayload>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gencow/core",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "description": "Gencow core library — defineQuery, defineMutation, reactive subscriptions",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -21,6 +21,12 @@
21
21
  "dist/",
22
22
  "src/"
23
23
  ],
24
+ "scripts": {
25
+ "db:generate:fixture-basic": "drizzle-kit generate --config ./src/__tests__/fixtures/basic/drizzle.config.ts",
26
+ "build": "tsc",
27
+ "typecheck": "tsc --noEmit",
28
+ "prepublishOnly": "npm run build"
29
+ },
24
30
  "dependencies": {
25
31
  "node-cron": "^4.2.1"
26
32
  },
@@ -39,10 +45,5 @@
39
45
  "hono": "^4.12.0",
40
46
  "typescript": "^5.9.3",
41
47
  "uuid": "^13.0.0"
42
- },
43
- "scripts": {
44
- "db:generate:fixture-basic": "drizzle-kit generate --config ./src/__tests__/fixtures/basic/drizzle.config.ts",
45
- "build": "tsc",
46
- "typecheck": "tsc --noEmit"
47
48
  }
48
- }
49
+ }
@@ -0,0 +1,95 @@
1
+ export type DocumentVisibility = "private" | "shared" | "public";
2
+ export type DocumentConvertMode =
3
+ | "auto"
4
+ | "text-only"
5
+ | "no-external-provider"
6
+ | "prefer-external"
7
+ | "force-external"
8
+ | "force-ocr";
9
+ export type DocumentConvertProvider = "auto" | "local_text" | "gemini" | "openai" | "ocr" | "custom_vlm";
10
+ export type DocumentResolvedProvider = Exclude<DocumentConvertProvider, "auto">;
11
+ export type DocumentProviderRoute = "local_text" | "vlm" | "ocr_fallback";
12
+ export type DocumentCacheArtifact = "convert" | "embedding" | "summary" | "rerank";
13
+
14
+ export type DocumentConvertInput = {
15
+ storageId: string;
16
+ filename?: string;
17
+ mimeType?: string;
18
+ corpus: string;
19
+ visibility: DocumentVisibility;
20
+ ownerUserId?: string;
21
+ mode?: DocumentConvertMode;
22
+ provider?: DocumentConvertProvider;
23
+ };
24
+
25
+ export type DocumentPage = {
26
+ page: number;
27
+ charStart: number;
28
+ charEnd: number;
29
+ previewText?: string;
30
+ };
31
+
32
+ export type DocumentSection = {
33
+ id: string;
34
+ title?: string;
35
+ depth: number;
36
+ path: string[];
37
+ pageStart?: number;
38
+ pageEnd?: number;
39
+ charStart: number;
40
+ charEnd: number;
41
+ };
42
+
43
+ export type DocumentProviderTrace = {
44
+ provider: DocumentResolvedProvider;
45
+ route: DocumentProviderRoute;
46
+ external: boolean;
47
+ requestId?: string;
48
+ durationMs: number;
49
+ bytes: number;
50
+ };
51
+
52
+ export type DocumentConvertResult = {
53
+ text: string;
54
+ markdown: string;
55
+ pages: DocumentPage[];
56
+ sections: DocumentSection[];
57
+ warnings: string[];
58
+ providerTrace: DocumentProviderTrace;
59
+ };
60
+
61
+ export interface GencowServicesCtx {}
62
+
63
+ export interface WorkflowDocumentServicesCtx {
64
+ document: {
65
+ convert(input: DocumentConvertInput): Promise<DocumentConvertResult>;
66
+ };
67
+ }
68
+
69
+ export type DocumentCacheKeyInput = {
70
+ appId: string;
71
+ ownerUserId?: string | null;
72
+ corpus: string;
73
+ sourceKey: string;
74
+ checksum: string;
75
+ provider: DocumentResolvedProvider;
76
+ artifact: DocumentCacheArtifact;
77
+ };
78
+
79
+ function normalizeCachePart(value: string): string {
80
+ return value.trim() || "unknown";
81
+ }
82
+
83
+ export function buildDocumentCacheKey(input: DocumentCacheKeyInput): string {
84
+ const owner = input.ownerUserId?.trim() ? input.ownerUserId.trim() : "shared";
85
+
86
+ return [
87
+ `app:${normalizeCachePart(input.appId)}`,
88
+ `user:${normalizeCachePart(owner)}`,
89
+ `corpus:${normalizeCachePart(input.corpus)}`,
90
+ `source:${normalizeCachePart(input.sourceKey)}`,
91
+ `checksum:${normalizeCachePart(input.checksum)}`,
92
+ `provider:${normalizeCachePart(input.provider)}`,
93
+ `artifact:${normalizeCachePart(input.artifact)}`,
94
+ ].join(":");
95
+ }