@parsrun/server 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/dist/rls.d.ts ADDED
@@ -0,0 +1,114 @@
1
+ import { DatabaseAdapter, Middleware } from './context.js';
2
+ import 'hono';
3
+ import '@parsrun/core';
4
+
5
+ /**
6
+ * @parsrun/server - Row Level Security (RLS) Manager
7
+ * PostgreSQL RLS integration for multi-tenant isolation
8
+ */
9
+
10
+ /**
11
+ * RLS configuration
12
+ */
13
+ interface RLSConfig {
14
+ /** Tenant ID column name (default: tenant_id) */
15
+ tenantIdColumn?: string;
16
+ /** PostgreSQL session variable name (default: app.current_tenant_id) */
17
+ sessionVariable?: string;
18
+ /** Enable RLS by default */
19
+ enabled?: boolean;
20
+ }
21
+ /**
22
+ * RLS Manager for tenant isolation
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const rls = new RLSManager(db);
27
+ *
28
+ * // Set tenant context
29
+ * await rls.setTenantId("tenant-123");
30
+ *
31
+ * // All queries now filtered by tenant
32
+ * const items = await db.select().from(items);
33
+ *
34
+ * // Clear when done
35
+ * await rls.clearTenantId();
36
+ *
37
+ * // Or use withTenant helper
38
+ * await rls.withTenant("tenant-123", async () => {
39
+ * return await db.select().from(items);
40
+ * });
41
+ * ```
42
+ */
43
+ declare class RLSManager {
44
+ private db;
45
+ private config;
46
+ constructor(db: DatabaseAdapter, config?: RLSConfig);
47
+ /**
48
+ * Set current tenant ID in database session
49
+ * This enables RLS policies to filter by tenant
50
+ */
51
+ setTenantId(tenantId: string): Promise<void>;
52
+ /**
53
+ * Clear current tenant ID from database session
54
+ */
55
+ clearTenantId(): Promise<void>;
56
+ /**
57
+ * Execute a function with tenant context
58
+ * Automatically sets and clears tenant ID
59
+ */
60
+ withTenant<T>(tenantId: string, operation: () => Promise<T>): Promise<T>;
61
+ /**
62
+ * Check if RLS is enabled
63
+ */
64
+ isEnabled(): boolean;
65
+ /**
66
+ * Get current configuration
67
+ */
68
+ getConfig(): Required<RLSConfig>;
69
+ }
70
+ /**
71
+ * RLS Error class
72
+ */
73
+ declare class RLSError extends Error {
74
+ readonly code: string;
75
+ readonly cause?: unknown | undefined;
76
+ constructor(message: string, code: string, cause?: unknown | undefined);
77
+ }
78
+ /**
79
+ * Create RLS manager
80
+ */
81
+ declare function createRLSManager(db: DatabaseAdapter, config?: RLSConfig): RLSManager;
82
+ /**
83
+ * RLS Middleware
84
+ * Automatically sets tenant context for authenticated requests
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * app.use('*', rlsMiddleware());
89
+ * ```
90
+ */
91
+ declare function rlsMiddleware(config?: RLSConfig): Middleware;
92
+ /**
93
+ * Create SQL for enabling RLS on a table
94
+ *
95
+ * @example
96
+ * ```sql
97
+ * -- Generated SQL:
98
+ * ALTER TABLE items ENABLE ROW LEVEL SECURITY;
99
+ * CREATE POLICY items_tenant_isolation ON items
100
+ * USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
101
+ * ```
102
+ */
103
+ declare function generateRLSPolicy(tableName: string, options?: {
104
+ tenantIdColumn?: string;
105
+ sessionVariable?: string;
106
+ policyName?: string;
107
+ castType?: string;
108
+ }): string;
109
+ /**
110
+ * Create SQL for disabling RLS on a table
111
+ */
112
+ declare function generateDisableRLS(tableName: string): string;
113
+
114
+ export { type RLSConfig, RLSError, RLSManager, createRLSManager, generateDisableRLS, generateRLSPolicy, rlsMiddleware };
package/dist/rls.js ADDED
@@ -0,0 +1,126 @@
1
+ // src/rls.ts
2
+ var DEFAULT_RLS_CONFIG = {
3
+ tenantIdColumn: "tenant_id",
4
+ sessionVariable: "app.current_tenant_id",
5
+ enabled: true
6
+ };
7
+ var RLSManager = class {
8
+ constructor(db, config = {}) {
9
+ this.db = db;
10
+ this.config = { ...DEFAULT_RLS_CONFIG, ...config };
11
+ }
12
+ config;
13
+ /**
14
+ * Set current tenant ID in database session
15
+ * This enables RLS policies to filter by tenant
16
+ */
17
+ async setTenantId(tenantId) {
18
+ if (!this.config.enabled) return;
19
+ try {
20
+ const escapedTenantId = tenantId.replace(/'/g, "''");
21
+ await this.db.execute(`SET ${this.config.sessionVariable} = '${escapedTenantId}'`);
22
+ } catch (error) {
23
+ console.error("Failed to set tenant ID for RLS:", error);
24
+ throw new RLSError("Failed to set tenant context", "RLS_SET_FAILED", error);
25
+ }
26
+ }
27
+ /**
28
+ * Clear current tenant ID from database session
29
+ */
30
+ async clearTenantId() {
31
+ if (!this.config.enabled) return;
32
+ try {
33
+ await this.db.execute(`RESET ${this.config.sessionVariable}`);
34
+ } catch (error) {
35
+ console.error("Failed to clear tenant ID for RLS:", error);
36
+ throw new RLSError("Failed to clear tenant context", "RLS_CLEAR_FAILED", error);
37
+ }
38
+ }
39
+ /**
40
+ * Execute a function with tenant context
41
+ * Automatically sets and clears tenant ID
42
+ */
43
+ async withTenant(tenantId, operation) {
44
+ await this.setTenantId(tenantId);
45
+ try {
46
+ return await operation();
47
+ } finally {
48
+ await this.clearTenantId();
49
+ }
50
+ }
51
+ /**
52
+ * Check if RLS is enabled
53
+ */
54
+ isEnabled() {
55
+ return this.config.enabled;
56
+ }
57
+ /**
58
+ * Get current configuration
59
+ */
60
+ getConfig() {
61
+ return { ...this.config };
62
+ }
63
+ };
64
+ var RLSError = class extends Error {
65
+ constructor(message, code, cause) {
66
+ super(message);
67
+ this.code = code;
68
+ this.cause = cause;
69
+ this.name = "RLSError";
70
+ }
71
+ };
72
+ function createRLSManager(db, config) {
73
+ return new RLSManager(db, config);
74
+ }
75
+ function rlsMiddleware(config) {
76
+ return async (c, next) => {
77
+ const user = c.get("user");
78
+ const db = c.get("db");
79
+ if (!user?.tenantId || !db) {
80
+ return next();
81
+ }
82
+ const rls = new RLSManager(db, config);
83
+ try {
84
+ await rls.setTenantId(user.tenantId);
85
+ await next();
86
+ } finally {
87
+ await rls.clearTenantId();
88
+ }
89
+ };
90
+ }
91
+ function generateRLSPolicy(tableName, options = {}) {
92
+ const {
93
+ tenantIdColumn = "tenant_id",
94
+ sessionVariable = "app.current_tenant_id",
95
+ policyName = `${tableName}_tenant_isolation`,
96
+ castType = "uuid"
97
+ } = options;
98
+ return `
99
+ -- Enable RLS on table
100
+ ALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;
101
+
102
+ -- Force RLS for table owner too
103
+ ALTER TABLE ${tableName} FORCE ROW LEVEL SECURITY;
104
+
105
+ -- Create tenant isolation policy
106
+ DROP POLICY IF EXISTS ${policyName} ON ${tableName};
107
+ CREATE POLICY ${policyName} ON ${tableName}
108
+ FOR ALL
109
+ USING (${tenantIdColumn} = current_setting('${sessionVariable}', true)::${castType});
110
+ `.trim();
111
+ }
112
+ function generateDisableRLS(tableName) {
113
+ return `
114
+ ALTER TABLE ${tableName} DISABLE ROW LEVEL SECURITY;
115
+ ALTER TABLE ${tableName} NO FORCE ROW LEVEL SECURITY;
116
+ `.trim();
117
+ }
118
+ export {
119
+ RLSError,
120
+ RLSManager,
121
+ createRLSManager,
122
+ generateDisableRLS,
123
+ generateRLSPolicy,
124
+ rlsMiddleware
125
+ };
126
+ //# sourceMappingURL=rls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/rls.ts"],"sourcesContent":["/**\n * @parsrun/server - Row Level Security (RLS) Manager\n * PostgreSQL RLS integration for multi-tenant isolation\n */\n\nimport type { DatabaseAdapter, HonoContext, HonoNext, Middleware } from \"./context.js\";\n\n/**\n * RLS configuration\n */\nexport interface RLSConfig {\n /** Tenant ID column name (default: tenant_id) */\n tenantIdColumn?: string;\n /** PostgreSQL session variable name (default: app.current_tenant_id) */\n sessionVariable?: string;\n /** Enable RLS by default */\n enabled?: boolean;\n}\n\n/**\n * Default RLS configuration\n */\nconst DEFAULT_RLS_CONFIG: Required<RLSConfig> = {\n tenantIdColumn: \"tenant_id\",\n sessionVariable: \"app.current_tenant_id\",\n enabled: true,\n};\n\n/**\n * RLS Manager for tenant isolation\n *\n * @example\n * ```typescript\n * const rls = new RLSManager(db);\n *\n * // Set tenant context\n * await rls.setTenantId(\"tenant-123\");\n *\n * // All queries now filtered by tenant\n * const items = await db.select().from(items);\n *\n * // Clear when done\n * await rls.clearTenantId();\n *\n * // Or use withTenant helper\n * await rls.withTenant(\"tenant-123\", async () => {\n * return await db.select().from(items);\n * });\n * ```\n */\nexport class RLSManager {\n private config: Required<RLSConfig>;\n\n constructor(\n private db: DatabaseAdapter,\n config: RLSConfig = {}\n ) {\n this.config = { ...DEFAULT_RLS_CONFIG, ...config };\n }\n\n /**\n * Set current tenant ID in database session\n * This enables RLS policies to filter by tenant\n */\n async setTenantId(tenantId: string): Promise<void> {\n if (!this.config.enabled) return;\n\n try {\n // Use parameterized query to prevent SQL injection\n // PostgreSQL SET command with escaped string\n const escapedTenantId = tenantId.replace(/'/g, \"''\");\n await this.db.execute(`SET ${this.config.sessionVariable} = '${escapedTenantId}'`);\n } catch (error) {\n console.error(\"Failed to set tenant ID for RLS:\", error);\n throw new RLSError(\"Failed to set tenant context\", \"RLS_SET_FAILED\", error);\n }\n }\n\n /**\n * Clear current tenant ID from database session\n */\n async clearTenantId(): Promise<void> {\n if (!this.config.enabled) return;\n\n try {\n await this.db.execute(`RESET ${this.config.sessionVariable}`);\n } catch (error) {\n console.error(\"Failed to clear tenant ID for RLS:\", error);\n throw new RLSError(\"Failed to clear tenant context\", \"RLS_CLEAR_FAILED\", error);\n }\n }\n\n /**\n * Execute a function with tenant context\n * Automatically sets and clears tenant ID\n */\n async withTenant<T>(tenantId: string, operation: () => Promise<T>): Promise<T> {\n await this.setTenantId(tenantId);\n try {\n return await operation();\n } finally {\n await this.clearTenantId();\n }\n }\n\n /**\n * Check if RLS is enabled\n */\n isEnabled(): boolean {\n return this.config.enabled;\n }\n\n /**\n * Get current configuration\n */\n getConfig(): Required<RLSConfig> {\n return { ...this.config };\n }\n}\n\n/**\n * RLS Error class\n */\nexport class RLSError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n public readonly cause?: unknown\n ) {\n super(message);\n this.name = \"RLSError\";\n }\n}\n\n/**\n * Create RLS manager\n */\nexport function createRLSManager(db: DatabaseAdapter, config?: RLSConfig): RLSManager {\n return new RLSManager(db, config);\n}\n\n/**\n * RLS Middleware\n * Automatically sets tenant context for authenticated requests\n *\n * @example\n * ```typescript\n * app.use('*', rlsMiddleware());\n * ```\n */\nexport function rlsMiddleware(config?: RLSConfig): Middleware {\n return async (c: HonoContext, next: HonoNext): Promise<Response | void> => {\n const user = c.get(\"user\");\n const db = c.get(\"db\");\n\n // Skip if no user or no tenant\n if (!user?.tenantId || !db) {\n return next();\n }\n\n const rls = new RLSManager(db, config);\n\n try {\n await rls.setTenantId(user.tenantId);\n await next();\n } finally {\n await rls.clearTenantId();\n }\n };\n}\n\n/**\n * Create SQL for enabling RLS on a table\n *\n * @example\n * ```sql\n * -- Generated SQL:\n * ALTER TABLE items ENABLE ROW LEVEL SECURITY;\n * CREATE POLICY items_tenant_isolation ON items\n * USING (tenant_id = current_setting('app.current_tenant_id')::uuid);\n * ```\n */\nexport function generateRLSPolicy(\n tableName: string,\n options: {\n tenantIdColumn?: string;\n sessionVariable?: string;\n policyName?: string;\n castType?: string;\n } = {}\n): string {\n const {\n tenantIdColumn = \"tenant_id\",\n sessionVariable = \"app.current_tenant_id\",\n policyName = `${tableName}_tenant_isolation`,\n castType = \"uuid\",\n } = options;\n\n return `\n-- Enable RLS on table\nALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;\n\n-- Force RLS for table owner too\nALTER TABLE ${tableName} FORCE ROW LEVEL SECURITY;\n\n-- Create tenant isolation policy\nDROP POLICY IF EXISTS ${policyName} ON ${tableName};\nCREATE POLICY ${policyName} ON ${tableName}\n FOR ALL\n USING (${tenantIdColumn} = current_setting('${sessionVariable}', true)::${castType});\n`.trim();\n}\n\n/**\n * Create SQL for disabling RLS on a table\n */\nexport function generateDisableRLS(tableName: string): string {\n return `\nALTER TABLE ${tableName} DISABLE ROW LEVEL SECURITY;\nALTER TABLE ${tableName} NO FORCE ROW LEVEL SECURITY;\n`.trim();\n}\n"],"mappings":";AAsBA,IAAM,qBAA0C;AAAA,EAC9C,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,SAAS;AACX;AAwBO,IAAM,aAAN,MAAiB;AAAA,EAGtB,YACU,IACR,SAAoB,CAAC,GACrB;AAFQ;AAGR,SAAK,SAAS,EAAE,GAAG,oBAAoB,GAAG,OAAO;AAAA,EACnD;AAAA,EAPQ;AAAA;AAAA;AAAA;AAAA;AAAA,EAaR,MAAM,YAAY,UAAiC;AACjD,QAAI,CAAC,KAAK,OAAO,QAAS;AAE1B,QAAI;AAGF,YAAM,kBAAkB,SAAS,QAAQ,MAAM,IAAI;AACnD,YAAM,KAAK,GAAG,QAAQ,OAAO,KAAK,OAAO,eAAe,OAAO,eAAe,GAAG;AAAA,IACnF,SAAS,OAAO;AACd,cAAQ,MAAM,oCAAoC,KAAK;AACvD,YAAM,IAAI,SAAS,gCAAgC,kBAAkB,KAAK;AAAA,IAC5E;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,gBAA+B;AACnC,QAAI,CAAC,KAAK,OAAO,QAAS;AAE1B,QAAI;AACF,YAAM,KAAK,GAAG,QAAQ,SAAS,KAAK,OAAO,eAAe,EAAE;AAAA,IAC9D,SAAS,OAAO;AACd,cAAQ,MAAM,sCAAsC,KAAK;AACzD,YAAM,IAAI,SAAS,kCAAkC,oBAAoB,KAAK;AAAA,IAChF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,WAAc,UAAkB,WAAyC;AAC7E,UAAM,KAAK,YAAY,QAAQ;AAC/B,QAAI;AACF,aAAO,MAAM,UAAU;AAAA,IACzB,UAAE;AACA,YAAM,KAAK,cAAc;AAAA,IAC3B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,YAAqB;AACnB,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA,EAKA,YAAiC;AAC/B,WAAO,EAAE,GAAG,KAAK,OAAO;AAAA,EAC1B;AACF;AAKO,IAAM,WAAN,cAAuB,MAAM;AAAA,EAClC,YACE,SACgB,MACA,OAChB;AACA,UAAM,OAAO;AAHG;AACA;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAKO,SAAS,iBAAiB,IAAqB,QAAgC;AACpF,SAAO,IAAI,WAAW,IAAI,MAAM;AAClC;AAWO,SAAS,cAAc,QAAgC;AAC5D,SAAO,OAAO,GAAgB,SAA6C;AACzE,UAAM,OAAO,EAAE,IAAI,MAAM;AACzB,UAAM,KAAK,EAAE,IAAI,IAAI;AAGrB,QAAI,CAAC,MAAM,YAAY,CAAC,IAAI;AAC1B,aAAO,KAAK;AAAA,IACd;AAEA,UAAM,MAAM,IAAI,WAAW,IAAI,MAAM;AAErC,QAAI;AACF,YAAM,IAAI,YAAY,KAAK,QAAQ;AACnC,YAAM,KAAK;AAAA,IACb,UAAE;AACA,YAAM,IAAI,cAAc;AAAA,IAC1B;AAAA,EACF;AACF;AAaO,SAAS,kBACd,WACA,UAKI,CAAC,GACG;AACR,QAAM;AAAA,IACJ,iBAAiB;AAAA,IACjB,kBAAkB;AAAA,IAClB,aAAa,GAAG,SAAS;AAAA,IACzB,WAAW;AAAA,EACb,IAAI;AAEJ,SAAO;AAAA;AAAA,cAEK,SAAS;AAAA;AAAA;AAAA,cAGT,SAAS;AAAA;AAAA;AAAA,wBAGC,UAAU,OAAO,SAAS;AAAA,gBAClC,UAAU,OAAO,SAAS;AAAA;AAAA,WAE/B,cAAc,uBAAuB,eAAe,aAAa,QAAQ;AAAA,EAClF,KAAK;AACP;AAKO,SAAS,mBAAmB,WAA2B;AAC5D,SAAO;AAAA,cACK,SAAS;AAAA,cACT,SAAS;AAAA,EACrB,KAAK;AACP;","names":[]}
@@ -0,0 +1,262 @@
1
+ import { HonoContext, ApiResponse } from '../context.js';
2
+ import 'hono';
3
+ import '@parsrun/core';
4
+
5
+ /**
6
+ * @parsrun/server - Pagination Utilities
7
+ * Helpers for paginated API responses
8
+ */
9
+
10
+ /**
11
+ * Pagination parameters
12
+ */
13
+ interface PaginationParams {
14
+ page: number;
15
+ limit: number;
16
+ offset: number;
17
+ }
18
+ /**
19
+ * Pagination metadata
20
+ */
21
+ interface PaginationMeta {
22
+ page: number;
23
+ limit: number;
24
+ total: number;
25
+ totalPages: number;
26
+ hasNext: boolean;
27
+ hasPrev: boolean;
28
+ }
29
+ /**
30
+ * Paginated response
31
+ */
32
+ interface PaginatedResponse<T> {
33
+ data: T[];
34
+ pagination: PaginationMeta;
35
+ }
36
+ /**
37
+ * Pagination options
38
+ */
39
+ interface PaginationOptions {
40
+ /** Default page size */
41
+ defaultLimit?: number;
42
+ /** Maximum page size */
43
+ maxLimit?: number;
44
+ /** Page query parameter name */
45
+ pageParam?: string;
46
+ /** Limit query parameter name */
47
+ limitParam?: string;
48
+ }
49
+ /**
50
+ * Parse pagination from request query
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * app.get('/users', async (c) => {
55
+ * const { page, limit, offset } = parsePagination(c);
56
+ *
57
+ * const users = await db.query.users.findMany({
58
+ * limit,
59
+ * offset,
60
+ * });
61
+ *
62
+ * const total = await db.query.users.count();
63
+ *
64
+ * return c.json(paginate(users, { page, limit, total }));
65
+ * });
66
+ * ```
67
+ */
68
+ declare function parsePagination(c: HonoContext, options?: PaginationOptions): PaginationParams;
69
+ /**
70
+ * Create pagination metadata
71
+ */
72
+ declare function createPaginationMeta(params: {
73
+ page: number;
74
+ limit: number;
75
+ total: number;
76
+ }): PaginationMeta;
77
+ /**
78
+ * Create paginated response
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * const users = await getUsers({ limit, offset });
83
+ * const total = await countUsers();
84
+ *
85
+ * return c.json(paginate(users, { page, limit, total }));
86
+ * ```
87
+ */
88
+ declare function paginate<T>(data: T[], params: {
89
+ page: number;
90
+ limit: number;
91
+ total: number;
92
+ }): PaginatedResponse<T>;
93
+ /**
94
+ * Cursor-based pagination params
95
+ */
96
+ interface CursorPaginationParams {
97
+ cursor?: string | undefined;
98
+ limit: number;
99
+ direction: "forward" | "backward";
100
+ }
101
+ /**
102
+ * Cursor pagination metadata
103
+ */
104
+ interface CursorPaginationMeta {
105
+ cursor?: string | undefined;
106
+ nextCursor?: string | undefined;
107
+ prevCursor?: string | undefined;
108
+ hasMore: boolean;
109
+ limit: number;
110
+ }
111
+ /**
112
+ * Cursor paginated response
113
+ */
114
+ interface CursorPaginatedResponse<T> {
115
+ data: T[];
116
+ pagination: CursorPaginationMeta;
117
+ }
118
+ /**
119
+ * Parse cursor pagination from request
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * app.get('/feed', async (c) => {
124
+ * const { cursor, limit, direction } = parseCursorPagination(c);
125
+ *
126
+ * const items = await db.query.posts.findMany({
127
+ * where: cursor ? { id: { gt: cursor } } : undefined,
128
+ * limit: limit + 1, // Fetch one extra to check hasMore
129
+ * orderBy: { createdAt: 'desc' },
130
+ * });
131
+ *
132
+ * return c.json(cursorPaginate(items, { cursor, limit }));
133
+ * });
134
+ * ```
135
+ */
136
+ declare function parseCursorPagination(c: HonoContext, options?: PaginationOptions): CursorPaginationParams;
137
+ /**
138
+ * Create cursor paginated response
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * // Fetch limit + 1 items
143
+ * const items = await fetchItems(limit + 1);
144
+ * return c.json(cursorPaginate(items, { cursor, limit }));
145
+ * ```
146
+ */
147
+ declare function cursorPaginate<T extends {
148
+ id: string;
149
+ }>(data: T[], params: {
150
+ cursor?: string;
151
+ limit: number;
152
+ }): CursorPaginatedResponse<T>;
153
+ /**
154
+ * Add pagination headers to response
155
+ */
156
+ declare function setPaginationHeaders(c: HonoContext, meta: PaginationMeta): void;
157
+
158
+ /**
159
+ * @parsrun/server - Response Utilities
160
+ * Helpers for API responses
161
+ */
162
+
163
+ /**
164
+ * Send JSON success response
165
+ *
166
+ * @example
167
+ * ```typescript
168
+ * app.get('/users/:id', async (c) => {
169
+ * const user = await getUser(c.req.param('id'));
170
+ * return json(c, user);
171
+ * });
172
+ * ```
173
+ */
174
+ declare function json<T>(c: HonoContext, data: T, status?: number): Response;
175
+ /**
176
+ * Send JSON success response with metadata
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * app.get('/users', async (c) => {
181
+ * const { users, total } = await getUsers();
182
+ * return jsonWithMeta(c, users, { total, page: 1, limit: 20 });
183
+ * });
184
+ * ```
185
+ */
186
+ declare function jsonWithMeta<T>(c: HonoContext, data: T, meta: ApiResponse["meta"], status?: number): Response;
187
+ /**
188
+ * Send JSON error response
189
+ *
190
+ * @example
191
+ * ```typescript
192
+ * app.get('/users/:id', async (c) => {
193
+ * const user = await getUser(c.req.param('id'));
194
+ * if (!user) {
195
+ * return jsonError(c, 'USER_NOT_FOUND', 'User not found', 404);
196
+ * }
197
+ * return json(c, user);
198
+ * });
199
+ * ```
200
+ */
201
+ declare function jsonError(c: HonoContext, code: string, message: string, status?: number, details?: Record<string, unknown>): Response;
202
+ /**
203
+ * Send created response (201)
204
+ */
205
+ declare function created<T>(c: HonoContext, data: T, location?: string): Response;
206
+ /**
207
+ * Send no content response (204)
208
+ */
209
+ declare function noContent(_c: HonoContext): Response;
210
+ /**
211
+ * Send accepted response (202) - for async operations
212
+ */
213
+ declare function accepted<T>(c: HonoContext, data?: T): Response;
214
+ /**
215
+ * Redirect response
216
+ */
217
+ declare function redirect(c: HonoContext, url: string, status?: 301 | 302 | 303 | 307 | 308): Response;
218
+ /**
219
+ * Stream response helper
220
+ *
221
+ * @example
222
+ * ```typescript
223
+ * app.get('/stream', (c) => {
224
+ * return stream(c, async (write) => {
225
+ * for (let i = 0; i < 10; i++) {
226
+ * await write(`data: ${i}\n\n`);
227
+ * await new Promise(r => setTimeout(r, 1000));
228
+ * }
229
+ * });
230
+ * });
231
+ * ```
232
+ */
233
+ declare function stream(_c: HonoContext, callback: (write: (chunk: string) => Promise<void>) => Promise<void>, options?: {
234
+ contentType?: string;
235
+ headers?: Record<string, string>;
236
+ }): Response;
237
+ /**
238
+ * Server-Sent Events helper
239
+ *
240
+ * @example
241
+ * ```typescript
242
+ * app.get('/events', (c) => {
243
+ * return sse(c, async (send) => {
244
+ * for await (const event of eventStream) {
245
+ * await send({ event: 'update', data: event });
246
+ * }
247
+ * });
248
+ * });
249
+ * ```
250
+ */
251
+ declare function sse(c: HonoContext, callback: (send: (event: {
252
+ event?: string;
253
+ data: unknown;
254
+ id?: string;
255
+ retry?: number;
256
+ }) => Promise<void>) => Promise<void>): Response;
257
+ /**
258
+ * File download response
259
+ */
260
+ declare function download(c: HonoContext, data: Uint8Array | ArrayBuffer | string, filename: string, contentType?: string): Response;
261
+
262
+ export { type CursorPaginatedResponse, type CursorPaginationMeta, type CursorPaginationParams, type PaginatedResponse, type PaginationMeta, type PaginationOptions, type PaginationParams, accepted, createPaginationMeta, created, cursorPaginate, download, json, jsonError, jsonWithMeta, noContent, paginate, parseCursorPagination, parsePagination, redirect, setPaginationHeaders, sse, stream };
@@ -0,0 +1,193 @@
1
+ // src/utils/pagination.ts
2
+ var defaultOptions = {
3
+ defaultLimit: 20,
4
+ maxLimit: 100,
5
+ pageParam: "page",
6
+ limitParam: "limit"
7
+ };
8
+ function parsePagination(c, options = {}) {
9
+ const opts = { ...defaultOptions, ...options };
10
+ const pageStr = c.req.query(opts.pageParam);
11
+ const limitStr = c.req.query(opts.limitParam);
12
+ let page = pageStr ? parseInt(pageStr, 10) : 1;
13
+ let limit = limitStr ? parseInt(limitStr, 10) : opts.defaultLimit;
14
+ if (isNaN(page) || page < 1) page = 1;
15
+ if (isNaN(limit) || limit < 1) limit = opts.defaultLimit;
16
+ if (limit > opts.maxLimit) limit = opts.maxLimit;
17
+ const offset = (page - 1) * limit;
18
+ return { page, limit, offset };
19
+ }
20
+ function createPaginationMeta(params) {
21
+ const { page, limit, total } = params;
22
+ const totalPages = Math.ceil(total / limit);
23
+ return {
24
+ page,
25
+ limit,
26
+ total,
27
+ totalPages,
28
+ hasNext: page < totalPages,
29
+ hasPrev: page > 1
30
+ };
31
+ }
32
+ function paginate(data, params) {
33
+ return {
34
+ data,
35
+ pagination: createPaginationMeta(params)
36
+ };
37
+ }
38
+ function parseCursorPagination(c, options = {}) {
39
+ const opts = { ...defaultOptions, ...options };
40
+ const cursor = c.req.query("cursor") ?? void 0;
41
+ const limitStr = c.req.query(opts.limitParam);
42
+ const direction = c.req.query("direction") === "backward" ? "backward" : "forward";
43
+ let limit = limitStr ? parseInt(limitStr, 10) : opts.defaultLimit;
44
+ if (isNaN(limit) || limit < 1) limit = opts.defaultLimit;
45
+ if (limit > opts.maxLimit) limit = opts.maxLimit;
46
+ return { cursor, limit, direction };
47
+ }
48
+ function cursorPaginate(data, params) {
49
+ const { cursor, limit } = params;
50
+ const hasMore = data.length > limit;
51
+ const items = hasMore ? data.slice(0, limit) : data;
52
+ const lastItem = items[items.length - 1];
53
+ const firstItem = items[0];
54
+ return {
55
+ data: items,
56
+ pagination: {
57
+ cursor,
58
+ nextCursor: hasMore && lastItem ? lastItem.id : void 0,
59
+ prevCursor: cursor && firstItem ? firstItem.id : void 0,
60
+ hasMore,
61
+ limit
62
+ }
63
+ };
64
+ }
65
+ function setPaginationHeaders(c, meta) {
66
+ c.header("X-Total-Count", String(meta.total));
67
+ c.header("X-Total-Pages", String(meta.totalPages));
68
+ c.header("X-Page", String(meta.page));
69
+ c.header("X-Per-Page", String(meta.limit));
70
+ c.header("X-Has-Next", String(meta.hasNext));
71
+ c.header("X-Has-Prev", String(meta.hasPrev));
72
+ }
73
+
74
+ // src/context.ts
75
+ function success(data, meta) {
76
+ return {
77
+ success: true,
78
+ data,
79
+ meta: meta ?? void 0
80
+ };
81
+ }
82
+ function error(code, message, details) {
83
+ return {
84
+ success: false,
85
+ error: { code, message, details: details ?? void 0 }
86
+ };
87
+ }
88
+
89
+ // src/utils/response.ts
90
+ function json(c, data, status = 200) {
91
+ return c.json(success(data), status);
92
+ }
93
+ function jsonWithMeta(c, data, meta, status = 200) {
94
+ return c.json(success(data, meta), status);
95
+ }
96
+ function jsonError(c, code, message, status = 400, details) {
97
+ return c.json(error(code, message, details), status);
98
+ }
99
+ function created(c, data, location) {
100
+ if (location) {
101
+ c.header("Location", location);
102
+ }
103
+ return c.json(success(data), 201);
104
+ }
105
+ function noContent(_c) {
106
+ return new Response(null, { status: 204 });
107
+ }
108
+ function accepted(c, data) {
109
+ if (data) {
110
+ return c.json(success(data), 202);
111
+ }
112
+ return new Response(null, { status: 202 });
113
+ }
114
+ function redirect(c, url, status = 302) {
115
+ return c.redirect(url, status);
116
+ }
117
+ function stream(_c, callback, options = {}) {
118
+ const encoder = new TextEncoder();
119
+ const readableStream = new ReadableStream({
120
+ async start(controller) {
121
+ const write = async (chunk) => {
122
+ controller.enqueue(encoder.encode(chunk));
123
+ };
124
+ try {
125
+ await callback(write);
126
+ } finally {
127
+ controller.close();
128
+ }
129
+ }
130
+ });
131
+ return new Response(readableStream, {
132
+ headers: {
133
+ "Content-Type": options.contentType ?? "text/event-stream",
134
+ "Cache-Control": "no-cache",
135
+ Connection: "keep-alive",
136
+ ...options.headers
137
+ }
138
+ });
139
+ }
140
+ function sse(c, callback) {
141
+ return stream(c, async (write) => {
142
+ const send = async (event) => {
143
+ let message = "";
144
+ if (event.id) {
145
+ message += `id: ${event.id}
146
+ `;
147
+ }
148
+ if (event.event) {
149
+ message += `event: ${event.event}
150
+ `;
151
+ }
152
+ if (event.retry) {
153
+ message += `retry: ${event.retry}
154
+ `;
155
+ }
156
+ const data = typeof event.data === "string" ? event.data : JSON.stringify(event.data);
157
+ message += `data: ${data}
158
+
159
+ `;
160
+ await write(message);
161
+ };
162
+ await callback(send);
163
+ });
164
+ }
165
+ function download(c, data, filename, contentType = "application/octet-stream") {
166
+ c.header("Content-Disposition", `attachment; filename="${filename}"`);
167
+ c.header("Content-Type", contentType);
168
+ if (typeof data === "string") {
169
+ return c.body(data);
170
+ }
171
+ return new Response(data, {
172
+ headers: c.res.headers
173
+ });
174
+ }
175
+ export {
176
+ accepted,
177
+ createPaginationMeta,
178
+ created,
179
+ cursorPaginate,
180
+ download,
181
+ json,
182
+ jsonError,
183
+ jsonWithMeta,
184
+ noContent,
185
+ paginate,
186
+ parseCursorPagination,
187
+ parsePagination,
188
+ redirect,
189
+ setPaginationHeaders,
190
+ sse,
191
+ stream
192
+ };
193
+ //# sourceMappingURL=index.js.map