@gencow/core 0.1.27 → 0.1.29
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/auth-config.d.ts +92 -5
- package/dist/config.d.ts +107 -0
- package/dist/config.js +12 -0
- package/dist/context.d.ts +139 -0
- package/dist/context.js +3 -0
- package/dist/crud.d.ts +5 -5
- package/dist/crud.js +19 -35
- package/dist/document-types.d.ts +65 -0
- package/dist/document-types.js +15 -0
- package/dist/grounded-answer-types.d.ts +62 -0
- package/dist/grounded-answer-types.js +6 -0
- package/dist/http-action.d.ts +77 -0
- package/dist/http-action.js +41 -0
- package/dist/index.d.ts +30 -5
- package/dist/index.js +15 -2
- package/dist/platform-capacity-profile.d.ts +19 -0
- package/dist/platform-capacity-profile.js +94 -0
- package/dist/procedure.d.ts +58 -0
- package/dist/procedure.js +115 -0
- package/dist/rag-ingest-types.d.ts +39 -0
- package/dist/rag-ingest-types.js +1 -0
- package/dist/rag-operations-types.d.ts +81 -0
- package/dist/rag-operations-types.js +1 -0
- package/dist/rag-schema.d.ts +1466 -0
- package/dist/rag-schema.js +87 -0
- package/dist/reactive-mutation-types.d.ts +11 -0
- package/dist/reactive-mutation-types.js +1 -0
- package/dist/reactive-mutation.d.ts +51 -0
- package/dist/reactive-mutation.js +75 -0
- package/dist/reactive-query-types.d.ts +12 -0
- package/dist/reactive-query-types.js +1 -0
- package/dist/reactive-query.d.ts +14 -0
- package/dist/reactive-query.js +28 -0
- package/dist/reactive-realtime.d.ts +48 -0
- package/dist/reactive-realtime.js +236 -0
- package/dist/reactive.d.ts +29 -5
- package/dist/reactive.js +65 -0
- package/dist/rls-db.d.ts +9 -2
- package/dist/runtime-env-policy.d.ts +5 -0
- package/dist/runtime-env-policy.js +56 -0
- package/dist/search-types.d.ts +83 -0
- package/dist/search-types.js +1 -0
- package/dist/server.d.ts +1 -2
- package/dist/server.js +0 -1
- package/dist/storage-metering.d.ts +13 -0
- package/dist/storage-metering.js +18 -0
- package/dist/storage-shared.d.ts +36 -0
- package/dist/storage-shared.js +39 -0
- package/dist/storage.d.ts +5 -27
- package/dist/storage.js +30 -22
- package/dist/wake-app-result.d.ts +22 -0
- package/dist/wake-app-result.js +11 -0
- package/dist/workflow-types.d.ts +16 -2
- package/dist/workflow.d.ts +1 -1
- package/dist/workflow.js +136 -11
- package/dist/workflows-api.js +71 -3
- package/package.json +11 -7
- package/src/auth-config.ts +104 -3
- package/src/config.ts +119 -0
- package/src/context.ts +152 -0
- package/src/crud.ts +18 -35
- package/src/document-types.ts +102 -0
- package/src/grounded-answer-types.ts +78 -0
- package/src/http-action.ts +101 -0
- package/src/index.ts +142 -19
- package/src/platform-capacity-profile.ts +114 -0
- package/src/procedure.ts +283 -0
- package/src/rag-ingest-types.ts +52 -0
- package/src/rag-operations-types.ts +90 -0
- package/src/rag-schema.ts +94 -0
- package/src/reactive-mutation-types.ts +13 -0
- package/src/reactive-mutation.ts +115 -0
- package/src/reactive-query-types.ts +14 -0
- package/src/reactive-query.ts +48 -0
- package/src/reactive-realtime.ts +267 -0
- package/src/rls-db.ts +9 -4
- package/src/runtime-env-policy.ts +66 -0
- package/src/search-types.ts +91 -0
- package/src/server.ts +6 -2
- package/src/storage-metering.ts +35 -0
- package/src/storage-shared.ts +74 -0
- package/src/storage.ts +44 -53
- package/src/wake-app-result.ts +37 -0
- package/src/workflow-types.ts +16 -2
- package/src/workflow.ts +166 -12
- package/src/workflows-api.ts +82 -3
- package/src/__tests__/auth.test.ts +0 -118
- package/src/__tests__/crons.test.ts +0 -83
- package/src/__tests__/crud-codegen-integration.test.ts +0 -246
- package/src/__tests__/crud-owner-rls.test.ts +0 -387
- package/src/__tests__/crud.test.ts +0 -930
- package/src/__tests__/dist-exports.test.ts +0 -176
- package/src/__tests__/fixtures/basic/auth.ts +0 -32
- package/src/__tests__/fixtures/basic/drizzle.config.ts +0 -12
- package/src/__tests__/fixtures/basic/index.ts +0 -6
- package/src/__tests__/fixtures/basic/migrations/0000_last_warstar.sql +0 -75
- package/src/__tests__/fixtures/basic/migrations/meta/0000_snapshot.json +0 -497
- package/src/__tests__/fixtures/basic/migrations/meta/_journal.json +0 -13
- package/src/__tests__/fixtures/basic/schema.ts +0 -51
- package/src/__tests__/fixtures/basic/tasks.ts +0 -15
- package/src/__tests__/fixtures/common/auth-schema.ts +0 -67
- package/src/__tests__/helpers/basic-rls-fixture.ts +0 -135
- package/src/__tests__/helpers/pglite-migrations.ts +0 -32
- package/src/__tests__/helpers/pglite-rls-session.ts +0 -51
- package/src/__tests__/helpers/seed-like-fill.ts +0 -202
- package/src/__tests__/helpers/test-gencow-ctx-rls.ts +0 -50
- package/src/__tests__/httpaction.test.ts +0 -122
- package/src/__tests__/image-optimization.test.ts +0 -648
- package/src/__tests__/load.test.ts +0 -389
- package/src/__tests__/network-sim.test.ts +0 -319
- package/src/__tests__/reactive.test.ts +0 -479
- package/src/__tests__/retry.test.ts +0 -113
- package/src/__tests__/rls-crud-basic.test.ts +0 -317
- package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +0 -117
- package/src/__tests__/rls-custom-mutation-handlers.test.ts +0 -142
- package/src/__tests__/rls-custom-query-handlers.test.ts +0 -128
- package/src/__tests__/rls-db-leased-connection.test.ts +0 -118
- package/src/__tests__/rls-session-and-policies.test.ts +0 -228
- package/src/__tests__/scheduler-durable-v2.test.ts +0 -288
- package/src/__tests__/scheduler-durable.test.ts +0 -173
- package/src/__tests__/scheduler-exec.test.ts +0 -328
- package/src/__tests__/scheduler.test.ts +0 -187
- package/src/__tests__/storage.test.ts +0 -334
- package/src/__tests__/tsconfig.json +0 -8
- package/src/__tests__/validator.test.ts +0 -323
- package/src/__tests__/workflow.test.ts +0 -606
- package/src/__tests__/ws-integration.test.ts +0 -309
- package/src/__tests__/ws-scale.test.ts +0 -241
- package/src/auth.ts +0 -155
- package/src/reactive.ts +0 -580
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
|
|
40
|
+
const RESERVED_TENANT_RUNTIME_ENV_PREFIXES = ["__GENCOW_", "GENCOW_DOCUMENT_", "GENCOW_TEMPLATE_", "GENCOW_WARM_"];
|
|
41
|
+
|
|
42
|
+
export function isReservedTenantRuntimeEnvKey(key: string): boolean {
|
|
43
|
+
const normalized = key.trim();
|
|
44
|
+
return (
|
|
45
|
+
RESERVED_TENANT_RUNTIME_ENV_KEYS.has(normalized) ||
|
|
46
|
+
RESERVED_TENANT_RUNTIME_ENV_PREFIXES.some((prefix) => normalized.startsWith(prefix))
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function filterTenantRuntimeEnvVars(vars: Record<string, string>): {
|
|
51
|
+
allowed: Record<string, string>;
|
|
52
|
+
rejectedKeys: string[];
|
|
53
|
+
} {
|
|
54
|
+
const allowed: Record<string, string> = {};
|
|
55
|
+
const rejectedKeys: string[] = [];
|
|
56
|
+
|
|
57
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
58
|
+
if (isReservedTenantRuntimeEnvKey(key)) {
|
|
59
|
+
rejectedKeys.push(key);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
allowed[key] = value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { allowed, rejectedKeys };
|
|
66
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export type SearchPrimitive = string | number | boolean;
|
|
2
|
+
|
|
3
|
+
export type SearchScope = {
|
|
4
|
+
corpus: string;
|
|
5
|
+
visibility: "private" | "shared" | "public";
|
|
6
|
+
ownerUserId?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type SearchFilter = {
|
|
10
|
+
eq?: Record<string, SearchPrimitive>;
|
|
11
|
+
in?: Record<string, SearchPrimitive[]>;
|
|
12
|
+
range?: Record<string, { gte?: string | number; 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
|
+
|
|
23
|
+
export type VectorSearchTuning = {
|
|
24
|
+
minScore?: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type VectorSearchOptions = Omit<SearchOptions, "fields"> & {
|
|
28
|
+
vector: number[];
|
|
29
|
+
vectorField?: string;
|
|
30
|
+
tuning?: VectorSearchTuning;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type HybridSearchFusionTuning = {
|
|
34
|
+
mode?: "rrf";
|
|
35
|
+
rrfK?: number;
|
|
36
|
+
keywordWeight?: number;
|
|
37
|
+
vectorWeight?: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type HybridSearchTuning = {
|
|
41
|
+
keywordCandidateLimit?: number;
|
|
42
|
+
vectorCandidateLimit?: number;
|
|
43
|
+
minFusedScore?: number;
|
|
44
|
+
fusion?: HybridSearchFusionTuning;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type HybridSearchOptions = SearchOptions & {
|
|
48
|
+
vector: number[];
|
|
49
|
+
vectorField?: string;
|
|
50
|
+
fusion?: "rrf"; // legacy top-level compatibility
|
|
51
|
+
keywordCandidateLimit?: number; // legacy top-level compatibility
|
|
52
|
+
vectorCandidateLimit?: number; // legacy top-level compatibility
|
|
53
|
+
tuning?: HybridSearchTuning;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type SearchHit = {
|
|
57
|
+
id: string | number;
|
|
58
|
+
row: Record<string, unknown>;
|
|
59
|
+
score: number;
|
|
60
|
+
scores?: {
|
|
61
|
+
keyword?: number;
|
|
62
|
+
vector?: number;
|
|
63
|
+
fused?: number;
|
|
64
|
+
};
|
|
65
|
+
matchedBy: Array<"keyword" | "vector">;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type SearchResponse = {
|
|
69
|
+
items: SearchHit[];
|
|
70
|
+
meta: {
|
|
71
|
+
engine: "tsvector" | "pgroonga";
|
|
72
|
+
tier: "free" | "pro" | "scale";
|
|
73
|
+
limit: number;
|
|
74
|
+
offset: number;
|
|
75
|
+
nextOffset?: number;
|
|
76
|
+
fusion?: "rrf";
|
|
77
|
+
mode?: "keyword" | "vector" | "hybrid";
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type SearchTierConfig = {
|
|
82
|
+
plan: "free" | "pro" | "scale";
|
|
83
|
+
engine: "tsvector" | "pgroonga";
|
|
84
|
+
hybridSearch: boolean;
|
|
85
|
+
locale: "english" | "multilingual";
|
|
86
|
+
extensions: {
|
|
87
|
+
vector: boolean;
|
|
88
|
+
pgroonga: boolean;
|
|
89
|
+
};
|
|
90
|
+
degradedReason?: string;
|
|
91
|
+
};
|
package/src/server.ts
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* bundled into user functions which run in Firecracker.
|
|
7
7
|
*/
|
|
8
8
|
export { createStorage, storageRoutes } from "./storage.js";
|
|
9
|
-
export type {
|
|
9
|
+
export type {
|
|
10
|
+
StorageImageTierConfig,
|
|
11
|
+
StorageImageTransformMetric,
|
|
12
|
+
StorageMeteringOptions,
|
|
13
|
+
StoredFile,
|
|
14
|
+
} from "./storage.js";
|
|
10
15
|
export { createScheduler, getSchedulerInfo } from "./scheduler.js";
|
|
11
|
-
export { authMiddleware, authRoutes, getUsers } from "./auth.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
|
|
3
|
+
export interface StorageImageTransformMetric {
|
|
4
|
+
transformCount: number;
|
|
5
|
+
sourceBytes: number;
|
|
6
|
+
outputBytes: number;
|
|
7
|
+
format: string;
|
|
8
|
+
autoWebp: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface StorageMeteringOptions {
|
|
12
|
+
onImageTransform?: (metric: StorageImageTransformMetric) => void | Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function recordStorageImageTransform(
|
|
16
|
+
options: StorageMeteringOptions | undefined,
|
|
17
|
+
cachePath: string,
|
|
18
|
+
metric: Omit<StorageImageTransformMetric, "transformCount" | "outputBytes">,
|
|
19
|
+
): Promise<{ size: number }> {
|
|
20
|
+
const stats = await fs.stat(cachePath);
|
|
21
|
+
if (!options?.onImageTransform) return stats;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await options.onImageTransform({
|
|
25
|
+
...metric,
|
|
26
|
+
transformCount: 1,
|
|
27
|
+
outputBytes: stats.size,
|
|
28
|
+
});
|
|
29
|
+
} catch (error) {
|
|
30
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
31
|
+
console.warn(`[storage] image transform metering failed: ${msg.slice(0, 120)}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return stats;
|
|
35
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
|
|
4
|
+
/** 파일 업로드 최대 크기: 50MB (하드코딩 — 사용자가 오버라이드 불가) */
|
|
5
|
+
export const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
/** 기본 스토리지 쿼터: 1GB */
|
|
8
|
+
export const DEFAULT_STORAGE_QUOTA = 1024 * 1024 * 1024;
|
|
9
|
+
|
|
10
|
+
export interface StorageFile {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
size: number;
|
|
14
|
+
type: string;
|
|
15
|
+
path: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface StoredFile {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
size: number;
|
|
22
|
+
type: string;
|
|
23
|
+
buffer: Buffer;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface StorageOptions {
|
|
27
|
+
rawSql?: (sql: string, params?: unknown[]) => Promise<unknown[]>;
|
|
28
|
+
storageQuota?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Storage {
|
|
32
|
+
store(file: File | Blob, filename?: string): Promise<string>;
|
|
33
|
+
storeBuffer(buffer: Buffer, filename: string, type?: string): Promise<string>;
|
|
34
|
+
get(storageId: string): Promise<StoredFile | null>;
|
|
35
|
+
getUrl(storageId: string): string;
|
|
36
|
+
getMeta(storageId: string): Promise<StorageFile | null>;
|
|
37
|
+
delete(storageId: string): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function formatBytes(bytes: number): string {
|
|
41
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
42
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
43
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
44
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function loadStorageMeta(params: {
|
|
48
|
+
storageId: string;
|
|
49
|
+
dir: string;
|
|
50
|
+
metaStore: Map<string, StorageFile>;
|
|
51
|
+
}): Promise<StorageFile | null> {
|
|
52
|
+
const cached = params.metaStore.get(params.storageId);
|
|
53
|
+
if (cached) {
|
|
54
|
+
return cached;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const filePath = path.join(params.dir, params.storageId);
|
|
58
|
+
try {
|
|
59
|
+
const raw = await fs.readFile(`${filePath}.meta`, "utf-8");
|
|
60
|
+
const parsed = JSON.parse(raw) as { name?: unknown; type?: unknown; size?: unknown };
|
|
61
|
+
const stat = await fs.stat(filePath);
|
|
62
|
+
const meta: StorageFile = {
|
|
63
|
+
id: params.storageId,
|
|
64
|
+
name: typeof parsed.name === "string" ? parsed.name : params.storageId,
|
|
65
|
+
size: typeof parsed.size === "number" ? parsed.size : stat.size,
|
|
66
|
+
type: typeof parsed.type === "string" ? parsed.type : "application/octet-stream",
|
|
67
|
+
path: filePath,
|
|
68
|
+
};
|
|
69
|
+
params.metaStore.set(params.storageId, meta);
|
|
70
|
+
return meta;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/storage.ts
CHANGED
|
@@ -2,51 +2,18 @@ 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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
18
|
-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// ─── Types ──────────────────────────────────────────────
|
|
22
|
-
|
|
23
|
-
interface StorageFile {
|
|
24
|
-
id: string;
|
|
25
|
-
name: string;
|
|
26
|
-
size: number;
|
|
27
|
-
type: string;
|
|
28
|
-
path: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface StorageOptions {
|
|
32
|
-
/** Raw SQL 실행 함수 — DB 자동 기록 + 쿼터 검증에 필요 */
|
|
33
|
-
rawSql?: (sql: string, params?: unknown[]) => Promise<unknown[]>;
|
|
34
|
-
/** 앱별 스토리지 쿼터 (bytes). 0 = 무제한. 기본: 1GB */
|
|
35
|
-
storageQuota?: number;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface Storage {
|
|
39
|
-
/** Store a file and return a storageId — Convex의 ctx.storage.store() */
|
|
40
|
-
store(file: File | Blob, filename?: string): Promise<string>;
|
|
41
|
-
/** Store from raw buffer */
|
|
42
|
-
storeBuffer(buffer: Buffer, filename: string, type?: string): Promise<string>;
|
|
43
|
-
/** Get a serving URL for the file — Convex의 ctx.storage.getUrl() */
|
|
44
|
-
getUrl(storageId: string): string;
|
|
45
|
-
/** Get file metadata */
|
|
46
|
-
getMeta(storageId: string): Promise<StorageFile | null>;
|
|
47
|
-
/** Delete a stored file — Convex의 ctx.storage.delete() */
|
|
48
|
-
delete(storageId: string): Promise<void>;
|
|
49
|
-
}
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_STORAGE_QUOTA,
|
|
7
|
+
MAX_FILE_SIZE,
|
|
8
|
+
formatBytes,
|
|
9
|
+
loadStorageMeta,
|
|
10
|
+
} from "./storage-shared.js";
|
|
11
|
+
import type { Storage, StorageFile, StorageOptions, StoredFile } from "./storage-shared.js";
|
|
12
|
+
import { recordStorageImageTransform } from "./storage-metering.js";
|
|
13
|
+
import type { StorageMeteringOptions } from "./storage-metering.js";
|
|
14
|
+
|
|
15
|
+
export type { Storage, StorageFile, StorageOptions, StoredFile } from "./storage-shared.js";
|
|
16
|
+
export type { StorageImageTransformMetric, StorageMeteringOptions } from "./storage-metering.js";
|
|
50
17
|
|
|
51
18
|
// ─── Implementation ─────────────────────────────────────
|
|
52
19
|
|
|
@@ -96,6 +63,8 @@ async function checkStorageQuota(
|
|
|
96
63
|
): Promise<void> {
|
|
97
64
|
if (quota <= 0) return; // 무제한
|
|
98
65
|
|
|
66
|
+
await ensureFilesTable(rawSql);
|
|
67
|
+
|
|
99
68
|
const rows = await rawSql(`SELECT COALESCE(SUM(CAST(size AS BIGINT)), 0) AS total FROM _system_files`);
|
|
100
69
|
const currentUsage = Number((rows[0] as Record<string, string>)?.total || "0");
|
|
101
70
|
const projectedUsage = currentUsage + newFileSize;
|
|
@@ -147,7 +116,6 @@ export function createStorage(dir: string = "./uploads", options?: StorageOption
|
|
|
147
116
|
|
|
148
117
|
// Ensure directory exists
|
|
149
118
|
fs.mkdir(dir, { recursive: true }).catch(() => {});
|
|
150
|
-
|
|
151
119
|
return {
|
|
152
120
|
async store(file: File | Blob, filename?: string): Promise<string> {
|
|
153
121
|
// 크기 제한 검증 (단일 파일)
|
|
@@ -248,8 +216,28 @@ export function createStorage(dir: string = "./uploads", options?: StorageOption
|
|
|
248
216
|
return `/api/storage/${storageId}`;
|
|
249
217
|
},
|
|
250
218
|
|
|
219
|
+
async get(storageId: string): Promise<StoredFile | null> {
|
|
220
|
+
const meta = await loadStorageMeta({ storageId, dir, metaStore });
|
|
221
|
+
if (!meta) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const buffer = await fs.readFile(meta.path).catch(() => null);
|
|
226
|
+
if (!buffer) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
id: meta.id,
|
|
232
|
+
name: meta.name,
|
|
233
|
+
size: meta.size,
|
|
234
|
+
type: meta.type,
|
|
235
|
+
buffer,
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
|
|
251
239
|
async getMeta(storageId: string): Promise<StorageFile | null> {
|
|
252
|
-
return
|
|
240
|
+
return loadStorageMeta({ storageId, dir, metaStore });
|
|
253
241
|
},
|
|
254
242
|
|
|
255
243
|
async delete(storageId: string): Promise<void> {
|
|
@@ -482,6 +470,7 @@ export function storageRoutes(
|
|
|
482
470
|
rawSql?: (sql: string, params?: unknown[]) => Promise<unknown[]>,
|
|
483
471
|
storageDir?: string,
|
|
484
472
|
tierConfig?: StorageImageTierConfig,
|
|
473
|
+
meteringOptions?: StorageMeteringOptions,
|
|
485
474
|
) {
|
|
486
475
|
const baseTierConfig: Required<StorageImageTierConfig> = {
|
|
487
476
|
autoWebp: tierConfig?.autoWebp ?? true,
|
|
@@ -753,15 +742,17 @@ export function storageRoutes(
|
|
|
753
742
|
|
|
754
743
|
// 변환 실행 → 캐시에 저장
|
|
755
744
|
await pipeline.toFile(cachePath);
|
|
745
|
+
const cacheStats = await recordStorageImageTransform(meteringOptions, cachePath, {
|
|
746
|
+
sourceBytes: meta.size,
|
|
747
|
+
format: outputFormat || meta.type.replace(/^image\//, ""),
|
|
748
|
+
autoWebp: isAutoWebp,
|
|
749
|
+
});
|
|
756
750
|
|
|
757
751
|
// WebP/AVIF가 원본보다 큰 경우 → 캐시 삭제 + 원본 서빙
|
|
758
752
|
// (Static Deploy와 동일 전략 — apps.ts L840-847)
|
|
759
|
-
if (isAutoWebp) {
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
await fs.unlink(cachePath).catch(() => {});
|
|
763
|
-
return serveOriginal(c, meta);
|
|
764
|
-
}
|
|
753
|
+
if (isAutoWebp && cacheStats.size >= meta.size) {
|
|
754
|
+
await fs.unlink(cachePath).catch(() => {});
|
|
755
|
+
return serveOriginal(c, meta);
|
|
765
756
|
}
|
|
766
757
|
|
|
767
758
|
return serveCachedFile(c, cachePath, transformParams, isAutoWebp, meta);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type WakeAppSuccessStatus = "already_running" | "woke";
|
|
2
|
+
export type WakeAppDeferredStatus = "capacity_rejected" | "queue_timeout";
|
|
3
|
+
|
|
4
|
+
export type WakeAppSuccessResult = {
|
|
5
|
+
ok: true;
|
|
6
|
+
status: WakeAppSuccessStatus;
|
|
7
|
+
port: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type WakeAppDeferredResult = {
|
|
11
|
+
ok: false;
|
|
12
|
+
status: WakeAppDeferredStatus;
|
|
13
|
+
retryAfterSec: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type WakeAppBootFailedResult = {
|
|
17
|
+
ok: false;
|
|
18
|
+
status: "boot_failed";
|
|
19
|
+
error: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type WakeAppResult = WakeAppSuccessResult | WakeAppDeferredResult | WakeAppBootFailedResult;
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_WAKE_RETRY_AFTER_SEC = 30;
|
|
25
|
+
|
|
26
|
+
export function buildWakeAppSuccessResult(status: WakeAppSuccessStatus, port: number): WakeAppSuccessResult {
|
|
27
|
+
return { ok: true, status, port };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildWakeAppBootFailedResult(error: unknown): WakeAppBootFailedResult {
|
|
31
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
+
return { ok: false, status: "boot_failed", error: message };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isWakeAppDeferredResult(result: WakeAppResult): result is WakeAppDeferredResult {
|
|
36
|
+
return !result.ok && (result.status === "capacity_rejected" || result.status === "queue_timeout");
|
|
37
|
+
}
|
package/src/workflow-types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { GencowCtx } from "./
|
|
1
|
+
import type { GencowCtx } from "./context.js";
|
|
2
|
+
import type { WorkflowDocumentServicesCtx } from "./document-types.js";
|
|
2
3
|
import type { InferArgs } from "./v.js";
|
|
3
4
|
|
|
4
5
|
export type WorkflowStatus = "pending" | "running" | "completed" | "failed";
|
|
@@ -28,6 +29,7 @@ export interface WorkflowSummary {
|
|
|
28
29
|
derivedStatus: WorkflowDerivedStatus;
|
|
29
30
|
currentStep: string | null;
|
|
30
31
|
error: string | null;
|
|
32
|
+
errorCode: string | null;
|
|
31
33
|
retryCount: number;
|
|
32
34
|
maxRetries: number;
|
|
33
35
|
maxDurationMs: number;
|
|
@@ -77,9 +79,13 @@ export interface WorkflowResumePayload {
|
|
|
77
79
|
workflowId: string;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
export interface WorkflowCtx extends GencowCtx {
|
|
82
|
+
export interface WorkflowCtx extends Omit<GencowCtx, "services"> {
|
|
81
83
|
workflowId: string;
|
|
82
84
|
workflowName: string;
|
|
85
|
+
runId?: string;
|
|
86
|
+
attempt?: number;
|
|
87
|
+
stepAttempt?: number;
|
|
88
|
+
services: GencowCtx["services"] & WorkflowDocumentServicesCtx;
|
|
83
89
|
step<TResult>(name: string, run: () => Promise<TResult>): Promise<TResult>;
|
|
84
90
|
sleep(duration: WorkflowDuration): Promise<void>;
|
|
85
91
|
waitForEvent<TPayload = unknown>(name: string, timeout?: WorkflowDuration): Promise<TPayload>;
|
|
@@ -93,7 +99,11 @@ export type WorkflowHandler<TArgs, TReturn> = (wf: WorkflowCtx, args: TArgs) =>
|
|
|
93
99
|
export interface WorkflowOptions<TSchema = any, TReturn = any> {
|
|
94
100
|
args?: TSchema;
|
|
95
101
|
public?: boolean;
|
|
102
|
+
version?: string;
|
|
96
103
|
maxDuration?: WorkflowDuration;
|
|
104
|
+
maxActiveDuration?: WorkflowDuration;
|
|
105
|
+
lifecycleTimeout?: WorkflowDuration;
|
|
106
|
+
concurrency?: number;
|
|
97
107
|
retries?: number;
|
|
98
108
|
handler: WorkflowHandler<InferArgs<TSchema>, TReturn>;
|
|
99
109
|
}
|
|
@@ -102,7 +112,11 @@ export interface WorkflowDef<TSchema = any, TReturn = any> {
|
|
|
102
112
|
name: string;
|
|
103
113
|
argsSchema?: TSchema;
|
|
104
114
|
isPublic: boolean;
|
|
115
|
+
version?: string;
|
|
105
116
|
maxDurationMs: number;
|
|
117
|
+
maxActiveDurationMs: number;
|
|
118
|
+
lifecycleTimeoutMs: number | null;
|
|
119
|
+
concurrency: number | null;
|
|
106
120
|
maxRetries: number;
|
|
107
121
|
handler: WorkflowHandler<InferArgs<TSchema>, TReturn>;
|
|
108
122
|
}
|