@spfn/core 0.2.0-beta.1 → 0.2.0-beta.10

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 (64) hide show
  1. package/README.md +262 -1092
  2. package/dist/{boss-D-fGtVgM.d.ts → boss-DI1r4kTS.d.ts} +68 -11
  3. package/dist/codegen/index.d.ts +55 -8
  4. package/dist/codegen/index.js +159 -5
  5. package/dist/codegen/index.js.map +1 -1
  6. package/dist/config/index.d.ts +36 -0
  7. package/dist/config/index.js +15 -6
  8. package/dist/config/index.js.map +1 -1
  9. package/dist/db/index.d.ts +13 -0
  10. package/dist/db/index.js +40 -6
  11. package/dist/db/index.js.map +1 -1
  12. package/dist/env/index.d.ts +82 -3
  13. package/dist/env/index.js +81 -14
  14. package/dist/env/index.js.map +1 -1
  15. package/dist/env/loader.d.ts +87 -0
  16. package/dist/env/loader.js +70 -0
  17. package/dist/env/loader.js.map +1 -0
  18. package/dist/event/index.d.ts +3 -70
  19. package/dist/event/index.js +10 -1
  20. package/dist/event/index.js.map +1 -1
  21. package/dist/event/sse/client.d.ts +82 -0
  22. package/dist/event/sse/client.js +115 -0
  23. package/dist/event/sse/client.js.map +1 -0
  24. package/dist/event/sse/index.d.ts +40 -0
  25. package/dist/event/sse/index.js +92 -0
  26. package/dist/event/sse/index.js.map +1 -0
  27. package/dist/job/index.d.ts +54 -8
  28. package/dist/job/index.js +61 -12
  29. package/dist/job/index.js.map +1 -1
  30. package/dist/middleware/index.d.ts +102 -11
  31. package/dist/middleware/index.js +2 -2
  32. package/dist/middleware/index.js.map +1 -1
  33. package/dist/nextjs/index.d.ts +2 -2
  34. package/dist/nextjs/index.js +36 -4
  35. package/dist/nextjs/index.js.map +1 -1
  36. package/dist/nextjs/server.d.ts +62 -15
  37. package/dist/nextjs/server.js +102 -33
  38. package/dist/nextjs/server.js.map +1 -1
  39. package/dist/route/index.d.ts +227 -15
  40. package/dist/route/index.js +307 -31
  41. package/dist/route/index.js.map +1 -1
  42. package/dist/route/types.d.ts +2 -31
  43. package/dist/router-Di7ENoah.d.ts +151 -0
  44. package/dist/server/index.d.ts +153 -6
  45. package/dist/server/index.js +216 -14
  46. package/dist/server/index.js.map +1 -1
  47. package/dist/types-B-e_f2dQ.d.ts +121 -0
  48. package/dist/{types-DRG2XMTR.d.ts → types-BOPTApC2.d.ts} +91 -3
  49. package/docs/cache.md +133 -0
  50. package/docs/codegen.md +74 -0
  51. package/docs/database.md +346 -0
  52. package/docs/entity.md +539 -0
  53. package/docs/env.md +477 -0
  54. package/docs/errors.md +319 -0
  55. package/docs/event.md +116 -0
  56. package/docs/file-upload.md +717 -0
  57. package/docs/job.md +131 -0
  58. package/docs/logger.md +108 -0
  59. package/docs/middleware.md +337 -0
  60. package/docs/nextjs.md +241 -0
  61. package/docs/repository.md +496 -0
  62. package/docs/route.md +497 -0
  63. package/docs/server.md +307 -0
  64. package/package.json +18 -3
@@ -1,7 +1,7 @@
1
1
  import { logger } from '@spfn/core/logger';
2
+ import { FormatRegistry, Type, Kind } from '@sinclair/typebox';
2
3
  import { Value } from '@sinclair/typebox/value';
3
4
  import { ValidationError } from '@spfn/core/errors';
4
- import { Type } from '@sinclair/typebox';
5
5
 
6
6
  // src/route/route-builder.ts
7
7
  var RouteBuilder = class _RouteBuilder {
@@ -162,14 +162,46 @@ var RouteBuilder = class _RouteBuilder {
162
162
  /**
163
163
  * Define handler function
164
164
  *
165
+ * Response type is automatically inferred from the return value.
166
+ * Use helper methods like `c.created()`, `c.paginated()` for proper type inference.
167
+ *
165
168
  * @example
166
169
  * ```ts
170
+ * // Direct return - type inferred from data
167
171
  * route.get('/users/:id')
168
172
  * .input({ params: Type.Object({ id: Type.String() }) })
169
173
  * .handler(async (c) => {
170
174
  * const { params } = await c.data();
171
- * const user = await getUser(params.id);
172
- * return user; // Type inferred!
175
+ * return await getUser(params.id); // Type: User
176
+ * })
177
+ *
178
+ * // Using c.created() - returns data with 201 status, type preserved
179
+ * route.post('/users')
180
+ * .input({ body: Type.Object({ name: Type.String() }) })
181
+ * .handler(async (c) => {
182
+ * const { body } = await c.data();
183
+ * return c.created(await createUser(body)); // Type: User
184
+ * })
185
+ *
186
+ * // Using c.paginated() - returns PaginatedResult<T>
187
+ * route.get('/users')
188
+ * .handler(async (c) => {
189
+ * const users = await getUsers();
190
+ * return c.paginated(users, 1, 20, 100); // Type: PaginatedResult<User>
191
+ * })
192
+ *
193
+ * // Using c.noContent() - returns void
194
+ * route.delete('/users/:id')
195
+ * .handler(async (c) => {
196
+ * await deleteUser(params.id);
197
+ * return c.noContent(); // Type: void
198
+ * })
199
+ *
200
+ * // Using c.json() - returns Response (type inference lost)
201
+ * // Use only when you need custom status codes not covered by helpers
202
+ * route.get('/custom')
203
+ * .handler(async (c) => {
204
+ * return c.json({ data }, 418); // Type: Response
173
205
  * })
174
206
  * ```
175
207
  */
@@ -228,6 +260,108 @@ function createRouterInstance(routes, packageRouters = [], globalMiddlewares = [
228
260
  function defineRouter(routes) {
229
261
  return createRouterInstance(routes);
230
262
  }
263
+ function FileSchema(options) {
264
+ return Type.Unsafe({
265
+ [Kind]: "File",
266
+ type: "object",
267
+ fileOptions: options
268
+ });
269
+ }
270
+ function FileArraySchema(options) {
271
+ return Type.Unsafe({
272
+ [Kind]: "FileArray",
273
+ type: "array",
274
+ items: { [Kind]: "File", type: "object" },
275
+ fileOptions: options
276
+ });
277
+ }
278
+ function OptionalFileSchema(options) {
279
+ return Type.Optional(FileSchema(options));
280
+ }
281
+ function isFileSchema(schema) {
282
+ const kind = schema[Symbol.for("TypeBox.Kind")];
283
+ return kind === "File";
284
+ }
285
+ function isFileArraySchema(schema) {
286
+ const kind = schema[Symbol.for("TypeBox.Kind")];
287
+ return kind === "FileArray";
288
+ }
289
+ function getFileOptions(schema) {
290
+ return schema.fileOptions;
291
+ }
292
+ function formatFileSize(bytes) {
293
+ if (bytes >= 1024 * 1024 * 1024) {
294
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
295
+ }
296
+ if (bytes >= 1024 * 1024) {
297
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
298
+ }
299
+ if (bytes >= 1024) {
300
+ return `${(bytes / 1024).toFixed(1)}KB`;
301
+ }
302
+ return `${bytes}B`;
303
+ }
304
+
305
+ // src/route/validation.ts
306
+ FormatRegistry.Set(
307
+ "email",
308
+ (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
309
+ );
310
+ FormatRegistry.Set(
311
+ "uri",
312
+ (value) => /^https?:\/\/.+/.test(value)
313
+ );
314
+ FormatRegistry.Set(
315
+ "uuid",
316
+ (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)
317
+ );
318
+ FormatRegistry.Set(
319
+ "date",
320
+ (value) => /^\d{4}-\d{2}-\d{2}$/.test(value)
321
+ );
322
+ FormatRegistry.Set(
323
+ "date-time",
324
+ (value) => !isNaN(Date.parse(value))
325
+ );
326
+ function isFile(value) {
327
+ return value instanceof File || typeof value === "object" && value !== null && "name" in value && "size" in value && "type" in value && typeof value.arrayBuffer === "function";
328
+ }
329
+ function isFileSchemaDef(schema) {
330
+ const kind = schema[Symbol.for("TypeBox.Kind")];
331
+ return kind === "File";
332
+ }
333
+ function isFileArraySchemaDef(schema) {
334
+ const kind = schema[Symbol.for("TypeBox.Kind")];
335
+ return kind === "FileArray";
336
+ }
337
+ function getSchemaFileOptions(schema) {
338
+ return schema.fileOptions;
339
+ }
340
+ function validateSingleFile(file, fieldPath, options, errors) {
341
+ if (!options) return;
342
+ const { maxSize, minSize, allowedTypes } = options;
343
+ if (maxSize !== void 0 && file.size > maxSize) {
344
+ errors.push({
345
+ path: fieldPath,
346
+ message: `File size ${formatFileSize(file.size)} exceeds maximum ${formatFileSize(maxSize)}`,
347
+ value: file.size
348
+ });
349
+ }
350
+ if (minSize !== void 0 && file.size < minSize) {
351
+ errors.push({
352
+ path: fieldPath,
353
+ message: `File size ${formatFileSize(file.size)} is below minimum ${formatFileSize(minSize)}`,
354
+ value: file.size
355
+ });
356
+ }
357
+ if (allowedTypes && allowedTypes.length > 0 && !allowedTypes.includes(file.type)) {
358
+ errors.push({
359
+ path: fieldPath,
360
+ message: `File type "${file.type}" is not allowed. Allowed: ${allowedTypes.join(", ")}`,
361
+ value: file.type
362
+ });
363
+ }
364
+ }
231
365
  function validateField(schema, rawValue, fieldName) {
232
366
  if (!schema) {
233
367
  return {};
@@ -246,6 +380,87 @@ function validateField(schema, rawValue, fieldName) {
246
380
  }
247
381
  return converted;
248
382
  }
383
+ function validateFormData(schema, rawValue, fieldName) {
384
+ if (!schema) {
385
+ return {};
386
+ }
387
+ const schemaProps = schema.properties;
388
+ if (!schemaProps) {
389
+ return rawValue;
390
+ }
391
+ const result = {};
392
+ const nonFileData = {};
393
+ const nonFileSchema = {};
394
+ const fileErrors = [];
395
+ for (const [key, value] of Object.entries(rawValue)) {
396
+ const propSchema = schemaProps[key];
397
+ if (propSchema && isFileSchemaDef(propSchema)) {
398
+ result[key] = value;
399
+ if (isFile(value)) {
400
+ const fileOptions = getSchemaFileOptions(propSchema);
401
+ validateSingleFile(value, `/${key}`, fileOptions, fileErrors);
402
+ }
403
+ } else if (propSchema && isFileArraySchemaDef(propSchema)) {
404
+ result[key] = value;
405
+ const fileOptions = getSchemaFileOptions(propSchema);
406
+ const files = Array.isArray(value) ? value : [value];
407
+ const fileArray = files.filter(isFile);
408
+ if (fileOptions?.maxFiles !== void 0 && fileArray.length > fileOptions.maxFiles) {
409
+ fileErrors.push({
410
+ path: `/${key}`,
411
+ message: `Too many files. Maximum: ${fileOptions.maxFiles}, received: ${fileArray.length}`,
412
+ value: fileArray.length
413
+ });
414
+ }
415
+ if (fileOptions?.minFiles !== void 0 && fileArray.length < fileOptions.minFiles) {
416
+ fileErrors.push({
417
+ path: `/${key}`,
418
+ message: `Too few files. Minimum: ${fileOptions.minFiles}, received: ${fileArray.length}`,
419
+ value: fileArray.length
420
+ });
421
+ }
422
+ fileArray.forEach((file, index) => {
423
+ validateSingleFile(file, `/${key}/${index}`, fileOptions, fileErrors);
424
+ });
425
+ } else if (isFile(value) || Array.isArray(value) && value.some(isFile)) {
426
+ result[key] = value;
427
+ } else {
428
+ nonFileData[key] = value;
429
+ if (propSchema) {
430
+ nonFileSchema[key] = propSchema;
431
+ }
432
+ }
433
+ }
434
+ if (fileErrors.length > 0) {
435
+ throw new ValidationError({
436
+ message: `Invalid ${fieldName}`,
437
+ fields: fileErrors
438
+ });
439
+ }
440
+ if (Object.keys(nonFileSchema).length > 0) {
441
+ const tempSchema = {
442
+ ...schema,
443
+ properties: nonFileSchema,
444
+ required: schema.required?.filter((r) => r in nonFileSchema) ?? []
445
+ };
446
+ const converted = Value.Convert(tempSchema, nonFileData);
447
+ const errors = [...Value.Errors(tempSchema, converted)];
448
+ if (errors.length > 0) {
449
+ throw new ValidationError({
450
+ message: `Invalid ${fieldName}`,
451
+ fields: errors.map((e) => ({
452
+ path: e.path,
453
+ message: e.message,
454
+ value: e.value
455
+ }))
456
+ });
457
+ }
458
+ Object.assign(result, converted);
459
+ } else {
460
+ Object.assign(result, nonFileData);
461
+ }
462
+ return result;
463
+ }
249
464
  function extractQueryParams(c) {
250
465
  const url = new URL(c.req.url);
251
466
  const queryObj = {};
@@ -293,6 +508,34 @@ async function parseJsonBody(c) {
293
508
  });
294
509
  }
295
510
  }
511
+ async function parseFormData(c) {
512
+ try {
513
+ const formData = await c.req.formData();
514
+ const result = {};
515
+ formData.forEach((value, key) => {
516
+ const existing = result[key];
517
+ if (existing !== void 0) {
518
+ if (Array.isArray(existing)) {
519
+ existing.push(value);
520
+ } else {
521
+ result[key] = [existing, value];
522
+ }
523
+ } else {
524
+ result[key] = value;
525
+ }
526
+ });
527
+ return result;
528
+ } catch (error) {
529
+ throw new ValidationError({
530
+ message: "Invalid form data",
531
+ fields: [{
532
+ path: "/",
533
+ message: "Failed to parse form data",
534
+ value: error instanceof Error ? error.message : "Unknown error"
535
+ }]
536
+ });
537
+ }
538
+ }
296
539
 
297
540
  // src/route/register-routes.ts
298
541
  function isRouter(value) {
@@ -304,16 +547,20 @@ function isRouteDef(value) {
304
547
  function isNamedMiddleware(value) {
305
548
  return value !== null && typeof value === "object" && "name" in value && "handler" in value && "_name" in value;
306
549
  }
307
- function registerRoutes(app, router, namedMiddlewares) {
550
+ function registerRoutes(app, router, namedMiddlewares, collectedRoutes) {
551
+ const routes = collectedRoutes ?? [];
308
552
  const allNamedMiddlewares = [
309
553
  ...namedMiddlewares ?? [],
310
554
  ...router._globalMiddlewares.map((mw) => ({ name: mw.name, handler: mw.handler }))
311
555
  ];
312
556
  for (const [name, routeOrRouter] of Object.entries(router.routes)) {
313
557
  if (isRouter(routeOrRouter)) {
314
- registerRoutes(app, routeOrRouter, allNamedMiddlewares);
558
+ registerRoutes(app, routeOrRouter, allNamedMiddlewares, routes);
315
559
  } else if (isRouteDef(routeOrRouter)) {
316
- registerRoute(app, name, routeOrRouter, allNamedMiddlewares);
560
+ const registered = registerRoute(app, name, routeOrRouter, allNamedMiddlewares);
561
+ if (registered) {
562
+ routes.push(registered);
563
+ }
317
564
  } else {
318
565
  logger.warn(`Unknown route type for "${name}" - skipping`, {
319
566
  type: typeof routeOrRouter
@@ -322,9 +569,10 @@ function registerRoutes(app, router, namedMiddlewares) {
322
569
  }
323
570
  if (router._packageRouters && router._packageRouters.length > 0) {
324
571
  for (const pkgRouter of router._packageRouters) {
325
- registerRoutes(app, pkgRouter, allNamedMiddlewares);
572
+ registerRoutes(app, pkgRouter, allNamedMiddlewares, routes);
326
573
  }
327
574
  }
575
+ return routes;
328
576
  }
329
577
  function registerRoute(app, name, routeDef, namedMiddlewares) {
330
578
  const { method, path, input, middlewares = [], skipMiddlewares, handler } = routeDef;
@@ -333,15 +581,22 @@ function registerRoute(app, name, routeDef, namedMiddlewares) {
333
581
  method,
334
582
  path
335
583
  });
336
- return;
584
+ return null;
337
585
  }
338
586
  const wrappedHandler = async (c) => {
339
- const context = await createRouteBuilderContext(c, input || {});
587
+ const { context, responseMeta } = await createRouteBuilderContext(c, input || {});
340
588
  const result = await handler(context);
341
589
  if (result instanceof Response) {
342
590
  return result;
343
591
  }
344
- return c.json(result);
592
+ if (responseMeta.isEmpty) {
593
+ return c.body(null, responseMeta.status);
594
+ }
595
+ const hasCustomHeaders = Object.keys(responseMeta.headers).length > 0;
596
+ if (hasCustomHeaders) {
597
+ return c.json(result, responseMeta.status, responseMeta.headers);
598
+ }
599
+ return c.json(result, responseMeta.status);
345
600
  };
346
601
  const allMiddlewares = [];
347
602
  const registeredNames = /* @__PURE__ */ new Set();
@@ -387,6 +642,7 @@ function registerRoute(app, name, routeDef, namedMiddlewares) {
387
642
  app[methodLower](path, wrappedHandler);
388
643
  }
389
644
  logger.debug(`Registered route: ${method} ${path}`, { name });
645
+ return { method, path, name };
390
646
  }
391
647
  async function createRouteBuilderContext(c, input) {
392
648
  const params = validateField(input.params, c.req.param(), "path parameters");
@@ -394,42 +650,58 @@ async function createRouteBuilderContext(c, input) {
394
650
  const headers = validateField(input.headers, extractHeaders(c), "headers");
395
651
  const cookies = validateField(input.cookies, extractCookies(c), "cookies");
396
652
  let body = {};
397
- if (input.body) {
398
- const rawBody = await parseJsonBody(c);
399
- body = validateField(input.body, rawBody, "request body");
653
+ let formData = {};
654
+ if (input.body || input.formData) {
655
+ const contentType = c.req.header("content-type") || "";
656
+ if (contentType.includes("multipart/form-data") && input.formData) {
657
+ const rawFormData = await parseFormData(c);
658
+ formData = validateFormData(input.formData, rawFormData, "form data");
659
+ } else if (input.body) {
660
+ const rawBody = await parseJsonBody(c);
661
+ body = validateField(input.body, rawBody, "request body");
662
+ }
400
663
  }
401
- return {
402
- data: async () => ({
403
- params,
404
- query,
405
- body,
406
- headers,
407
- cookies
408
- }),
664
+ let cachedData = null;
665
+ const responseMeta = {
666
+ status: 200,
667
+ headers: {},
668
+ isEmpty: false
669
+ };
670
+ const context = {
671
+ data: async () => {
672
+ if (!cachedData) {
673
+ cachedData = { params, query, body, formData, headers, cookies };
674
+ }
675
+ return cachedData;
676
+ },
409
677
  json: (data, status, resHeaders) => {
410
678
  return c.json(data, status, resHeaders);
411
679
  },
412
680
  created: (data, location) => {
413
- const resHeaders = {};
681
+ responseMeta.status = 201;
414
682
  if (location) {
415
- resHeaders["Location"] = location;
683
+ responseMeta.headers["Location"] = location;
416
684
  }
417
- return c.json(data, 201, resHeaders);
685
+ return data;
418
686
  },
419
687
  accepted: (data) => {
688
+ responseMeta.status = 202;
420
689
  if (data === void 0) {
421
- return c.body(null, 202);
690
+ responseMeta.isEmpty = true;
691
+ return void 0;
422
692
  }
423
- return c.json(data, 202);
693
+ return data;
424
694
  },
425
695
  noContent: () => {
426
- return c.body(null, 204);
696
+ responseMeta.status = 204;
697
+ responseMeta.isEmpty = true;
427
698
  },
428
699
  notModified: () => {
429
- return c.body(null, 304);
700
+ responseMeta.status = 304;
701
+ responseMeta.isEmpty = true;
430
702
  },
431
703
  paginated: (data, page, limit, total) => {
432
- return c.json({
704
+ return {
433
705
  items: data,
434
706
  pagination: {
435
707
  page,
@@ -437,10 +709,14 @@ async function createRouteBuilderContext(c, input) {
437
709
  total,
438
710
  totalPages: Math.ceil(total / limit)
439
711
  }
440
- }, 200);
712
+ };
713
+ },
714
+ redirect: (url, status) => {
715
+ return c.redirect(url, status);
441
716
  },
442
717
  raw: c
443
718
  };
719
+ return { context, responseMeta };
444
720
  }
445
721
 
446
722
  // src/route/define-middleware.ts
@@ -499,6 +775,6 @@ function isHttpMethod(value) {
499
775
  var Nullable = (schema) => Type.Union([schema, Type.Null()]);
500
776
  var OptionalNullable = (schema) => Type.Optional(Type.Union([schema, Type.Null()]));
501
777
 
502
- export { Nullable, OptionalNullable, RouteBuilder, defineMiddleware, defineMiddlewareFactory, defineRouter, isHttpMethod, registerRoutes, route };
778
+ export { FileArraySchema, FileSchema, Nullable, OptionalFileSchema, OptionalNullable, defineMiddleware, defineMiddlewareFactory, defineRouter, formatFileSize, getFileOptions, isFileArraySchema, isFileSchema, isHttpMethod, registerRoutes, route };
503
779
  //# sourceMappingURL=index.js.map
504
780
  //# sourceMappingURL=index.js.map