hapta 1.0.11 → 1.0.13

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 (2) hide show
  1. package/index.ts +103 -83
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -10,7 +10,7 @@ import jwt from "jsonwebtoken";
10
10
  import crypto from "crypto";
11
11
  import * as z from "zod";
12
12
  import { watch } from "fs";
13
- import fs from "fs"
13
+ import fs from "fs"
14
14
  import { pathToFileURL } from "url";
15
15
  import { MemoryState } from "./src/core/MemoryState";
16
16
 
@@ -37,9 +37,9 @@ if (!config.Database?.url || !config.Database?.Admin_Password || !config.Databas
37
37
 
38
38
  // --- Pocketbase Connection ---
39
39
  export const pb = new Pocketbase(config.Database?.url || "");
40
+ pb.autoCancellation(false)
40
41
  try {
41
- await pb.collection("_superusers").authWithPassword(config.Database?.Admin_Email || "", config.Database?.Admin_Password || "");
42
- pb.autoCancellation(false)
42
+ await pb.collection("_superusers").authWithPassword(config.Database?.Admin_Email || "", config.Database?.Admin_Password || "");
43
43
  } catch (_) { }
44
44
 
45
45
  const cache = new Cache();
@@ -88,7 +88,7 @@ function isOriginAllowed(origin: string | null, corsConfig: CorsConfig): boolean
88
88
  // Build CORS headers
89
89
  function buildCorsHeaders(origin: string | null, corsConfig: CorsConfig): Record<string, string> {
90
90
  const headers: Record<string, string> = {};
91
-
91
+
92
92
  if (origin && isOriginAllowed(origin, corsConfig)) {
93
93
  headers["Access-Control-Allow-Origin"] = origin;
94
94
  } else if (corsConfig.origins === "*") {
@@ -97,20 +97,20 @@ function buildCorsHeaders(origin: string | null, corsConfig: CorsConfig): Record
97
97
  // Use first origin as fallback
98
98
  headers["Access-Control-Allow-Origin"] = corsConfig.origins[0];
99
99
  }
100
-
100
+
101
101
  if (corsConfig.credentials) {
102
102
  headers["Access-Control-Allow-Credentials"] = "true";
103
103
  }
104
-
104
+
105
105
  headers["Access-Control-Allow-Methods"] = corsConfig.methods.join(", ");
106
106
  headers["Access-Control-Allow-Headers"] = corsConfig.allowedHeaders.join(", ");
107
-
107
+
108
108
  if (corsConfig.exposedHeaders.length > 0) {
109
109
  headers["Access-Control-Expose-Headers"] = corsConfig.exposedHeaders.join(", ");
110
110
  }
111
-
111
+
112
112
  headers["Access-Control-Max-Age"] = corsConfig.maxAge.toString();
113
-
113
+
114
114
  return headers;
115
115
  }
116
116
 
@@ -125,7 +125,7 @@ async function createServeConfig(): Promise<Serve> {
125
125
  const schemas = new Map();
126
126
  const middlewares = new Map<string, (ctx: Context) => Promise<Response | boolean>>();
127
127
 
128
- const reimportModule = async (modulePath: string) => {
128
+ const reimportModule = async (modulePath: string) => {
129
129
  const resolvedPath = path.resolve(modulePath);
130
130
  if (require.cache[resolvedPath]) {
131
131
  delete require.cache[resolvedPath];
@@ -138,7 +138,7 @@ async function createServeConfig(): Promise<Serve> {
138
138
  for (const [pathname, routePath] of Object.entries(router.routes)) {
139
139
  try {
140
140
  const routeModule = await reimportModule(routePath as string);
141
-
141
+
142
142
  // Store the entire module object for method-based routing
143
143
  routeHandlers.set(pathname, routeModule.default || routeModule);
144
144
 
@@ -155,7 +155,7 @@ async function createServeConfig(): Promise<Serve> {
155
155
  const middleware = await reimportModule(middlewarePath);
156
156
  if (middleware.default) middlewares.set(pathname, middleware.default);
157
157
  }
158
- } catch (e) {
158
+ } catch (e) {
159
159
  console.error(`❌ Error loading module for route ${pathname}:`, e);
160
160
  }
161
161
  }
@@ -178,13 +178,13 @@ async function createServeConfig(): Promise<Serve> {
178
178
  });
179
179
  }
180
180
 
181
-
181
+
182
182
  const routeMatch = router.match(url.pathname)
183
183
 
184
184
  if (!routeMatch) {
185
- return new Response("404 Not Found", {
185
+ return new Response("404 Not Found", {
186
186
  status: 404,
187
- headers: corsHeaders
187
+ headers: corsHeaders
188
188
  });
189
189
  }
190
190
 
@@ -193,15 +193,15 @@ async function createServeConfig(): Promise<Serve> {
193
193
  const schema = schemas.get(routeMatch.name);
194
194
 
195
195
  if (!routeHandler) {
196
- return new Response(`Route handler for ${routeMatch.name} not found.`, {
196
+ return new Response(`Route handler for ${routeMatch.name} not found.`, {
197
197
  status: 404,
198
- headers: corsHeaders
198
+ headers: corsHeaders
199
199
  });
200
200
  }
201
201
 
202
202
  try {
203
203
  const context = await buildRequestContext(req, req.headers);
204
- context.metadata.params = routeMatch.params;
204
+ context.metadata.params = routeMatch.params;
205
205
  context.metadata.query = Object.fromEntries(url.searchParams.entries());
206
206
  context.metadata.headers = Object.fromEntries(req.headers.entries());
207
207
 
@@ -220,9 +220,9 @@ async function createServeConfig(): Promise<Serve> {
220
220
  headers: responseHeaders,
221
221
  });
222
222
  }
223
- if (result === false) return new Response("Forbidden", {
223
+ if (result === false) return new Response("Forbidden", {
224
224
  status: 403,
225
- headers: corsHeaders
225
+ headers: corsHeaders
226
226
  });
227
227
  }
228
228
 
@@ -233,7 +233,7 @@ async function createServeConfig(): Promise<Serve> {
233
233
  query: schema.query?.safeParse(context.metadata.query),
234
234
  headers: schema.headers?.safeParse(context.metadata.headers),
235
235
  };
236
-
236
+
237
237
  for (const key of ["body", "query", "headers"] as const) {
238
238
  if (validation[key] && !validation[key]?.success) {
239
239
  routeHandler.onError && routeHandler.onError(req, {
@@ -242,16 +242,16 @@ async function createServeConfig(): Promise<Serve> {
242
242
  details: validation[key]?.error.flatten(),
243
243
  });
244
244
  return new Response(
245
- JSON.stringify({
246
- success: false,
247
- error: validation[key]?.error.flatten()
248
- }),
249
- {
250
- status: 400,
245
+ JSON.stringify({
246
+ success: false,
247
+ data: {error: validation[key]?.error.flatten()}
248
+ }),
249
+ {
250
+ status: 400,
251
251
  headers: {
252
252
  "Content-Type": "application/json",
253
253
  ...corsHeaders
254
- }
254
+ }
255
255
  }
256
256
  );
257
257
  }
@@ -261,11 +261,11 @@ async function createServeConfig(): Promise<Serve> {
261
261
  // Get method-specific handler
262
262
  const methodHandler = routeHandler[method] || routeHandler.default;
263
263
  console.log(`➡️ ${method} ${url.pathname} -> ${routeMatch.name}`);
264
-
264
+
265
265
  if (!methodHandler) {
266
266
  return new Response(
267
- `Method ${method} not allowed for ${routeMatch.name}`,
268
- {
267
+ `Method ${method} not allowed for ${routeMatch.name}`,
268
+ {
269
269
  status: 405,
270
270
  headers: {
271
271
  "Allow": Object.keys(routeHandler)
@@ -279,7 +279,7 @@ async function createServeConfig(): Promise<Serve> {
279
279
 
280
280
  // Execute the route handler
281
281
  const response = await methodHandler(context, db);
282
-
282
+
283
283
  // Add CORS headers to the response
284
284
  if (response instanceof Response) {
285
285
  const responseHeaders = new Headers(response.headers);
@@ -292,11 +292,11 @@ async function createServeConfig(): Promise<Serve> {
292
292
  headers: responseHeaders,
293
293
  });
294
294
  }
295
-
295
+
296
296
  return response;
297
297
  } catch (err) {
298
298
  console.error(`❌ Error handling ${url.pathname}:`, err);
299
-
299
+
300
300
  // Check if route handler has custom error handler
301
301
  if (routeHandler.onError) {
302
302
  try {
@@ -317,10 +317,10 @@ async function createServeConfig(): Promise<Serve> {
317
317
  console.error(`❌ Error handler also failed:`, errorHandlerErr);
318
318
  }
319
319
  }
320
-
321
- return new Response("Internal Server Error", {
320
+
321
+ return new Response("Internal Server Error", {
322
322
  status: 500,
323
- headers: corsHeaders
323
+ headers: corsHeaders
324
324
  });
325
325
  }
326
326
  },
@@ -328,9 +328,9 @@ async function createServeConfig(): Promise<Serve> {
328
328
  const url = new URL(error.request.url);
329
329
  const origin = error.request.headers.get("origin");
330
330
  const corsHeaders = buildCorsHeaders(origin, defaultCorsConfig);
331
-
331
+
332
332
  const routeMatch = router.match(url.href);
333
-
333
+
334
334
  if (routeMatch) {
335
335
  const routeHandler = routeHandlers.get(routeMatch.name);
336
336
  if (routeHandler && typeof routeHandler.onError === "function") {
@@ -352,10 +352,10 @@ async function createServeConfig(): Promise<Serve> {
352
352
  }
353
353
  }
354
354
  }
355
-
356
- return new Response("Something went wrong!", {
355
+
356
+ return new Response("Something went wrong!", {
357
357
  status: 500,
358
- headers: corsHeaders
358
+ headers: corsHeaders
359
359
  });
360
360
  },
361
361
  };
@@ -402,49 +402,69 @@ function validateToken(token: string) {
402
402
  }
403
403
  }
404
404
  async function Register(data: { username: string; email: string; password: string }) {
405
- try {
406
- //@ts-ignore
407
- data.passwordConfirm = password
408
- const response = await pb.collection("users").create(data);
405
+ try {
406
+ data.passwordConfirm = data.password
407
+ const response = await pb.collection("users").create(data);
408
+ return { success: true, data: response, status: 200 };
409
+ } catch (err: any) {
410
+ const fieldErrors: Record<string, { code: string; message: string }> = {};
411
+
412
+ if (err?.data) {
413
+ for (const key in err.data) {
414
+ let value = err.data[key];
415
+
416
+ // If value is a stringified JSON, parse it
417
+ if (typeof value === "string" && value.startsWith("{")) {
418
+ try {
419
+ value = JSON.parse(value);
420
+ } catch (_) {
421
+ // fallback: leave as string
422
+ }
423
+ }
409
424
 
410
- // If successful, return structured success response
411
- return {
412
- success: true,
413
- data: response,
414
- status: 200,
415
- };
416
- } catch (error: any) {
417
- // Initialize structured error object
418
- const fieldErrors: Record<string, { code: string; message: string }> = {};
419
-
420
- // PocketBase returns error.data for field errors
421
- if (error?.data) {
422
- for (const key in error.data) {
423
- const fieldMsg = error.data[key];
424
- fieldErrors[key] = {
425
- code: "validation_not_unique", // or map from PocketBase error if available
426
- message: Array.isArray(fieldMsg) ? fieldMsg.join(" ") : String(fieldMsg),
425
+ // If value is now an object of field errors
426
+ if (typeof value === "object" && !Array.isArray(value)) {
427
+ for (const field in value) {
428
+ const msg = value[field];
429
+ fieldErrors[field] = {
430
+ code: msg.code || "validation_error",
431
+ message: msg.message || String(msg),
432
+ };
433
+ }
434
+ } else if (Array.isArray(value)) {
435
+ fieldErrors[key] = {
436
+ code: "validation_error",
437
+ message: value.join(" "),
438
+ };
439
+ } else {
440
+ fieldErrors[key] = {
441
+ code: "validation_error",
442
+ message: String(value),
443
+ };
444
+ }
445
+ }
446
+ }
447
+
448
+ return {
449
+ success: false,
450
+ data: {
451
+ error: {
452
+ fieldErrors,
453
+ formErrors: [err?.message || "Failed to create record."],
454
+ },
455
+ },
456
+ status: err?.status || 400,
427
457
  };
428
- }
429
458
  }
430
-
431
- return {
432
- success: false,
433
- error: {
434
- fieldErrors,
435
- formErrors: [error?.message || "Failed to create record."],
436
- },
437
- status: error?.status || 400,
438
- };
439
- }
440
459
  }
441
460
 
461
+
442
462
  async function buildRequestContext(req: Request, headers: Headers): Promise<Context> {
443
463
  const reqJSON = await req.json().catch(() => ({}));
444
464
 
445
465
  const authHeader = headers.get("Authorization");
446
466
  const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : null;
447
-
467
+
448
468
  let isAuthenticated = false;
449
469
  let principal: any = { isAuthenticated };
450
470
 
@@ -462,22 +482,22 @@ async function buildRequestContext(req: Request, headers: Headers): Promise<Cont
462
482
  principal: { ...principal, isAuthenticated },
463
483
  services: {
464
484
  async Authenticate(options) {
465
- const { type } = options;
485
+ const { type } = options;
466
486
  if (type === "passwordAuth") {
467
- const { emailOrUsername, password } = options
468
- const login = await pb.collection("users").authWithPassword(emailOrUsername, password) as unknown as Promise<{ record: any }>;
487
+ const { emailOrUsername, password } = options
488
+ const login = await pb.collection("users").authWithPassword(emailOrUsername, password) as unknown as Promise<{ record: any }>;
469
489
  // make a token
470
- const token = jwt.sign({ id: (await login).record.id, username: (await login).record.username } , config.Server.JWT_Secret, { expiresIn: '1h' });
471
-
490
+ const token = jwt.sign({ id: (await login).record.id, username: (await login).record.username }, config.Server.JWT_Secret, { expiresIn: '1h' });
491
+
472
492
  if (login) {
473
493
  return { isAuthenticated: true, token, ...(await login).record };
474
494
  } else {
475
495
  return { isAuthenticated: false };
476
496
  }
477
497
  }
478
- },
479
- Register,
480
-
498
+ },
499
+ Register,
500
+
481
501
  },
482
502
  metadata: {
483
503
  requestID: crypto.randomUUID(),
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "bin": {
4
4
  "hapta": "./index.ts"
5
5
  },
6
- "version": "1.0.11",
6
+ "version": "1.0.13",
7
7
  "description": "modular, scalable, and feature-rich backend framework designed to extend Pocketbase with authentication, schema validation, caching, and tenant-based service orchestration.",
8
8
  "dependencies": {
9
9
  "jsonwebtoken": "^9.0.3",