@gencow/core 0.1.0

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/src/server.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Platform Core — Server Implementations
3
+ *
4
+ * Includes Node.js specific modules (e.g. fs, path, crypto) needed for the
5
+ * executing server. Excluded from client-side core (`index.ts`) so they aren't
6
+ * bundled into user functions which run in Firecracker.
7
+ */
8
+ export { createDb } from "./db";
9
+ export { createStorage, storageRoutes } from "./storage";
10
+ export { createScheduler, getSchedulerInfo } from "./scheduler";
11
+ export { authMiddleware, authRoutes, getUsers } from "./auth";
package/src/storage.ts ADDED
@@ -0,0 +1,149 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ import * as crypto from "crypto";
4
+
5
+ // ─── Types ──────────────────────────────────────────────
6
+
7
+ interface StorageFile {
8
+ id: string;
9
+ name: string;
10
+ size: number;
11
+ type: string;
12
+ path: string;
13
+ }
14
+
15
+ export interface Storage {
16
+ /** Store a file and return a storageId — Convex의 ctx.storage.store() */
17
+ store(file: File | Blob, filename?: string): Promise<string>;
18
+ /** Store from raw buffer */
19
+ storeBuffer(buffer: Buffer, filename: string, type?: string): Promise<string>;
20
+ /** Get a serving URL for the file — Convex의 ctx.storage.getUrl() */
21
+ getUrl(storageId: string): string;
22
+ /** Get file metadata */
23
+ getMeta(storageId: string): Promise<StorageFile | null>;
24
+ /** Delete a stored file — Convex의 ctx.storage.delete() */
25
+ delete(storageId: string): Promise<void>;
26
+ }
27
+
28
+ // ─── Implementation ─────────────────────────────────────
29
+
30
+ const metaStore = new Map<string, StorageFile>();
31
+
32
+ /**
33
+ * Create a storage instance — Convex storage 패턴 재현
34
+ *
35
+ * @example
36
+ * const storage = createStorage('./uploads');
37
+ * const id = await storage.store(file);
38
+ * const url = storage.getUrl(id);
39
+ */
40
+ export function createStorage(dir: string = "./uploads"): Storage {
41
+ // Ensure directory exists
42
+ fs.mkdir(dir, { recursive: true }).catch(() => { });
43
+
44
+ return {
45
+ async store(file: File | Blob, filename?: string): Promise<string> {
46
+ const id = crypto.randomUUID();
47
+ const name = filename || (file instanceof File ? file.name : `${id}.bin`);
48
+ const filePath = path.join(dir, id);
49
+
50
+ const arrayBuffer = await file.arrayBuffer();
51
+ await fs.writeFile(filePath, Buffer.from(arrayBuffer));
52
+
53
+ metaStore.set(id, {
54
+ id,
55
+ name,
56
+ size: file.size,
57
+ type: file.type || "application/octet-stream",
58
+ path: filePath,
59
+ });
60
+
61
+ return id;
62
+ },
63
+
64
+ async storeBuffer(
65
+ buffer: Buffer,
66
+ filename: string,
67
+ type: string = "application/octet-stream"
68
+ ): Promise<string> {
69
+ const id = crypto.randomUUID();
70
+ const filePath = path.join(dir, id);
71
+
72
+ await fs.writeFile(filePath, buffer);
73
+
74
+ metaStore.set(id, {
75
+ id,
76
+ name: filename,
77
+ size: buffer.length,
78
+ type,
79
+ path: filePath,
80
+ });
81
+
82
+ return id;
83
+ },
84
+
85
+ getUrl(storageId: string): string {
86
+ return `/api/storage/${storageId}`;
87
+ },
88
+
89
+ async getMeta(storageId: string): Promise<StorageFile | null> {
90
+ return metaStore.get(storageId) || null;
91
+ },
92
+
93
+ async delete(storageId: string): Promise<void> {
94
+ const meta = metaStore.get(storageId);
95
+ if (meta) {
96
+ await fs.unlink(meta.path).catch(() => { });
97
+ metaStore.delete(storageId);
98
+ }
99
+ },
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Hono routes for serving stored files
105
+ */
106
+ export function storageRoutes(
107
+ storage: ReturnType<typeof createStorage>,
108
+ rawSql?: (sql: string, params?: any[]) => Promise<any[]>,
109
+ storageDir?: string,
110
+ ) {
111
+ return async (c: any) => {
112
+ const id = c.req.param("id");
113
+ let meta = await storage.getMeta(id);
114
+
115
+ // Fallback: DB lookup when in-memory meta is missing (e.g. after server restart)
116
+ if (!meta && rawSql) {
117
+ try {
118
+ const rows = await rawSql(
119
+ `SELECT storage_id, name, size, type FROM files WHERE storage_id = $1 LIMIT 1`,
120
+ [id]
121
+ );
122
+ if (rows.length > 0) {
123
+ const row = rows[0];
124
+ const dir = storageDir || "./uploads";
125
+ meta = {
126
+ id: row.storage_id,
127
+ name: row.name,
128
+ size: Number(row.size),
129
+ type: row.type || "application/octet-stream",
130
+ path: path.join(dir, row.storage_id),
131
+ };
132
+ }
133
+ } catch { /* fallthrough to 404 */ }
134
+ }
135
+
136
+ if (!meta) {
137
+ return c.json({ error: "Not found" }, 404);
138
+ }
139
+
140
+ const file = await fs.readFile(meta.path);
141
+ return new Response(file, {
142
+ headers: {
143
+ "Content-Type": meta.type,
144
+ "Content-Disposition": `attachment; filename="${encodeURIComponent(meta.name)}"; filename*=UTF-8''${encodeURIComponent(meta.name)}`,
145
+ "Content-Length": String(meta.size),
146
+ },
147
+ });
148
+ };
149
+ }
package/src/v.ts ADDED
@@ -0,0 +1,158 @@
1
+ /**
2
+ * packages/core/src/v.ts
3
+ *
4
+ * Convex-style schema validator for API arguments.
5
+ * Provides both runtime validation and TypeScript type inference.
6
+ */
7
+
8
+ /**
9
+ * Validation-specific error — thrown by parseArgs on invalid input.
10
+ * Allows the server dispatcher to safely distinguish 400 from 500
11
+ * without fragile string-matching on the error message.
12
+ */
13
+ export class GencowValidationError extends Error {
14
+ readonly statusCode = 400;
15
+ constructor(message: string) {
16
+ super(message);
17
+ this.name = "GencowValidationError";
18
+ }
19
+ }
20
+
21
+ export type Validator<T = any> = {
22
+ parse(val: any): T;
23
+ _type: T; // Ghost property for TypeScript inference
24
+ };
25
+
26
+ export type Infer<T> = T extends Validator<infer U> ? U : never;
27
+
28
+ export const v = {
29
+ string(): Validator<string> {
30
+ return {
31
+ parse: (val) => {
32
+ if (typeof val !== "string") throw new Error(`Expected string, got ${typeof val}`);
33
+ return val;
34
+ },
35
+ _type: "" as string,
36
+ };
37
+ },
38
+ number(): Validator<number> {
39
+ return {
40
+ parse: (val) => {
41
+ // Accept numeric strings (e.g. from URL params / form data)
42
+ const n = typeof val === "string" ? Number(val) : val;
43
+ if (typeof n !== "number" || isNaN(n)) throw new Error(`Expected number, got ${typeof val}`);
44
+ return n;
45
+ },
46
+ _type: 0 as number,
47
+ };
48
+ },
49
+ boolean(): Validator<boolean> {
50
+ return {
51
+ parse: (val) => {
52
+ if (typeof val !== "boolean") throw new Error(`Expected boolean, got ${typeof val}`);
53
+ return val;
54
+ },
55
+ _type: false as boolean,
56
+ };
57
+ },
58
+ any(): Validator<any> {
59
+ return {
60
+ parse: (val) => val,
61
+ _type: null as any,
62
+ };
63
+ },
64
+ optional<T>(inner: Validator<T>): Validator<T | undefined> {
65
+ return {
66
+ parse: (val) => {
67
+ if (val === undefined || val === null) return undefined;
68
+ return inner.parse(val);
69
+ },
70
+ _type: undefined as T | undefined,
71
+ };
72
+ },
73
+ object<T extends Record<string, Validator>>(fields: T): Validator<{ [K in keyof T]: Infer<T[K]> }> {
74
+ return {
75
+ parse: (val) => {
76
+ if (typeof val !== "object" || val === null) throw new Error("Expected object");
77
+ const result: any = {};
78
+ for (const key in fields) {
79
+ result[key] = fields[key].parse((val as any)[key]);
80
+ }
81
+ return result;
82
+ },
83
+ _type: {} as any,
84
+ };
85
+ },
86
+ array<T>(inner: Validator<T>): Validator<T[]> {
87
+ return {
88
+ parse: (val) => {
89
+ if (!Array.isArray(val)) throw new Error("Expected array");
90
+ return val.map((item) => inner.parse(item));
91
+ },
92
+ _type: [] as T[],
93
+ };
94
+ },
95
+ };
96
+
97
+ /**
98
+ * Infer TypeScript type from a validator schema or a shorthand record of validators.
99
+ * Correctly handles optional properties (Validator<T | undefined> → optional key).
100
+ */
101
+ export type InferArgs<T> = T extends Validator<infer U>
102
+ ? U
103
+ : T extends Record<string, Validator>
104
+ ? {
105
+ [K in keyof T as T[K] extends Validator<infer U>
106
+ ? (undefined extends U ? never : K)
107
+ : K]: T[K] extends Validator<infer U> ? U : any;
108
+ } & {
109
+ [K in keyof T as T[K] extends Validator<infer U>
110
+ ? (undefined extends U ? K : never)
111
+ : never]?: T[K] extends Validator<infer U> ? U : any;
112
+ }
113
+ : T;
114
+
115
+ /**
116
+ * Validates and parses arguments against a schema at runtime.
117
+ *
118
+ * Supports:
119
+ * - Full validators: `v.object({ id: v.number() })`, `v.string()`, `v.any()`, etc.
120
+ * - Shorthand record: `{ id: v.number(), title: v.string() }` (Convex style)
121
+ *
122
+ * Throws `GencowValidationError` (HTTP 400) on validation failure.
123
+ */
124
+ export function parseArgs(schema: any, args: any): any {
125
+ if (!schema) return args;
126
+
127
+ // Direct Validator (has a .parse method)
128
+ if (typeof schema.parse === "function") {
129
+ try {
130
+ return schema.parse(args);
131
+ } catch (e: any) {
132
+ throw new GencowValidationError(e.message);
133
+ }
134
+ }
135
+
136
+ // Shorthand object — e.g. { id: v.number(), title: v.optional(v.string()) }
137
+ if (typeof schema === "object" && schema !== null) {
138
+ if (typeof args !== "object" || args === null) {
139
+ throw new GencowValidationError("Expected an object for arguments");
140
+ }
141
+ const result: any = {};
142
+ for (const key in schema) {
143
+ const validator = schema[key];
144
+ if (validator && typeof validator.parse === "function") {
145
+ try {
146
+ result[key] = validator.parse(args[key]);
147
+ } catch (e: any) {
148
+ throw new GencowValidationError(`Argument "${key}": ${e.message}`);
149
+ }
150
+ } else {
151
+ result[key] = args[key];
152
+ }
153
+ }
154
+ return result;
155
+ }
156
+
157
+ return args;
158
+ }