@spfn/core 0.2.0-beta.3 → 0.2.0-beta.30

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 (65) hide show
  1. package/README.md +260 -1175
  2. package/dist/{boss-BO8ty33K.d.ts → boss-DI1r4kTS.d.ts} +24 -7
  3. package/dist/codegen/index.d.ts +55 -8
  4. package/dist/codegen/index.js +179 -5
  5. package/dist/codegen/index.js.map +1 -1
  6. package/dist/config/index.d.ts +204 -6
  7. package/dist/config/index.js +44 -11
  8. package/dist/config/index.js.map +1 -1
  9. package/dist/db/index.d.ts +24 -3
  10. package/dist/db/index.js +118 -45
  11. package/dist/db/index.js.map +1 -1
  12. package/dist/env/index.d.ts +83 -3
  13. package/dist/env/index.js +83 -15
  14. package/dist/env/index.js.map +1 -1
  15. package/dist/env/loader.d.ts +95 -0
  16. package/dist/env/loader.js +78 -0
  17. package/dist/env/loader.js.map +1 -0
  18. package/dist/event/index.d.ts +29 -70
  19. package/dist/event/index.js +15 -1
  20. package/dist/event/index.js.map +1 -1
  21. package/dist/event/sse/client.d.ts +157 -0
  22. package/dist/event/sse/client.js +169 -0
  23. package/dist/event/sse/client.js.map +1 -0
  24. package/dist/event/sse/index.d.ts +46 -0
  25. package/dist/event/sse/index.js +238 -0
  26. package/dist/event/sse/index.js.map +1 -0
  27. package/dist/job/index.d.ts +23 -8
  28. package/dist/job/index.js +108 -23
  29. package/dist/job/index.js.map +1 -1
  30. package/dist/logger/index.js +9 -0
  31. package/dist/logger/index.js.map +1 -1
  32. package/dist/middleware/index.d.ts +23 -1
  33. package/dist/middleware/index.js +58 -5
  34. package/dist/middleware/index.js.map +1 -1
  35. package/dist/nextjs/index.d.ts +2 -2
  36. package/dist/nextjs/index.js +37 -5
  37. package/dist/nextjs/index.js.map +1 -1
  38. package/dist/nextjs/server.d.ts +44 -23
  39. package/dist/nextjs/server.js +87 -66
  40. package/dist/nextjs/server.js.map +1 -1
  41. package/dist/route/index.d.ts +168 -5
  42. package/dist/route/index.js +262 -17
  43. package/dist/route/index.js.map +1 -1
  44. package/dist/router-Di7ENoah.d.ts +151 -0
  45. package/dist/server/index.d.ts +316 -5
  46. package/dist/server/index.js +892 -200
  47. package/dist/server/index.js.map +1 -1
  48. package/dist/{types-BVxUIkcU.d.ts → types-7Mhoxnnt.d.ts} +68 -2
  49. package/dist/types-DAVwA-_7.d.ts +339 -0
  50. package/docs/cache.md +133 -0
  51. package/docs/codegen.md +74 -0
  52. package/docs/database.md +346 -0
  53. package/docs/entity.md +539 -0
  54. package/docs/env.md +499 -0
  55. package/docs/errors.md +319 -0
  56. package/docs/event.md +443 -0
  57. package/docs/file-upload.md +717 -0
  58. package/docs/job.md +131 -0
  59. package/docs/logger.md +108 -0
  60. package/docs/middleware.md +337 -0
  61. package/docs/nextjs.md +247 -0
  62. package/docs/repository.md +496 -0
  63. package/docs/route.md +497 -0
  64. package/docs/server.md +429 -0
  65. package/package.json +18 -2
@@ -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 {
@@ -260,6 +260,108 @@ function createRouterInstance(routes, packageRouters = [], globalMiddlewares = [
260
260
  function defineRouter(routes) {
261
261
  return createRouterInstance(routes);
262
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
+ }
263
365
  function validateField(schema, rawValue, fieldName) {
264
366
  if (!schema) {
265
367
  return {};
@@ -278,6 +380,87 @@ function validateField(schema, rawValue, fieldName) {
278
380
  }
279
381
  return converted;
280
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
+ }
281
464
  function extractQueryParams(c) {
282
465
  const url = new URL(c.req.url);
283
466
  const queryObj = {};
@@ -325,6 +508,34 @@ async function parseJsonBody(c) {
325
508
  });
326
509
  }
327
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
+ }
328
539
 
329
540
  // src/route/register-routes.ts
330
541
  function isRouter(value) {
@@ -336,16 +547,20 @@ function isRouteDef(value) {
336
547
  function isNamedMiddleware(value) {
337
548
  return value !== null && typeof value === "object" && "name" in value && "handler" in value && "_name" in value;
338
549
  }
339
- function registerRoutes(app, router, namedMiddlewares) {
550
+ function registerRoutes(app, router, namedMiddlewares, collectedRoutes) {
551
+ const routes = collectedRoutes ?? [];
340
552
  const allNamedMiddlewares = [
341
553
  ...namedMiddlewares ?? [],
342
554
  ...router._globalMiddlewares.map((mw) => ({ name: mw.name, handler: mw.handler }))
343
555
  ];
344
556
  for (const [name, routeOrRouter] of Object.entries(router.routes)) {
345
557
  if (isRouter(routeOrRouter)) {
346
- registerRoutes(app, routeOrRouter, allNamedMiddlewares);
558
+ registerRoutes(app, routeOrRouter, allNamedMiddlewares, routes);
347
559
  } else if (isRouteDef(routeOrRouter)) {
348
- registerRoute(app, name, routeOrRouter, allNamedMiddlewares);
560
+ const registered = registerRoute(app, name, routeOrRouter, allNamedMiddlewares);
561
+ if (registered) {
562
+ routes.push(registered);
563
+ }
349
564
  } else {
350
565
  logger.warn(`Unknown route type for "${name}" - skipping`, {
351
566
  type: typeof routeOrRouter
@@ -354,9 +569,10 @@ function registerRoutes(app, router, namedMiddlewares) {
354
569
  }
355
570
  if (router._packageRouters && router._packageRouters.length > 0) {
356
571
  for (const pkgRouter of router._packageRouters) {
357
- registerRoutes(app, pkgRouter, allNamedMiddlewares);
572
+ registerRoutes(app, pkgRouter, allNamedMiddlewares, routes);
358
573
  }
359
574
  }
575
+ return routes;
360
576
  }
361
577
  function registerRoute(app, name, routeDef, namedMiddlewares) {
362
578
  const { method, path, input, middlewares = [], skipMiddlewares, handler } = routeDef;
@@ -365,7 +581,7 @@ function registerRoute(app, name, routeDef, namedMiddlewares) {
365
581
  method,
366
582
  path
367
583
  });
368
- return;
584
+ return null;
369
585
  }
370
586
  const wrappedHandler = async (c) => {
371
587
  const { context, responseMeta } = await createRouteBuilderContext(c, input || {});
@@ -386,18 +602,28 @@ function registerRoute(app, name, routeDef, namedMiddlewares) {
386
602
  const registeredNames = /* @__PURE__ */ new Set();
387
603
  const registeredHandlers = /* @__PURE__ */ new Set();
388
604
  const skipAll = skipMiddlewares === "*";
605
+ const autoSkips = /* @__PURE__ */ new Set();
606
+ for (const mw of middlewares) {
607
+ if (isNamedMiddleware(mw) && mw.skips) {
608
+ for (const skipName of mw.skips) {
609
+ autoSkips.add(skipName);
610
+ }
611
+ }
612
+ }
389
613
  if (namedMiddlewares && namedMiddlewares.length > 0) {
390
614
  if (skipAll) {
391
615
  logger.debug(`\u23ED\uFE0F Skipping all middlewares (*) for route: ${method} ${path}`, { name });
392
616
  } else {
393
617
  const skipSet = new Set(Array.isArray(skipMiddlewares) ? skipMiddlewares : []);
394
618
  for (const middleware of namedMiddlewares) {
395
- if (!skipSet.has(middleware.name)) {
619
+ if (skipSet.has(middleware.name)) {
620
+ logger.debug(`\u23ED\uFE0F Skipping middleware '${middleware.name}' for route: ${method} ${path}`, { name });
621
+ } else if (autoSkips.has(middleware.name)) {
622
+ logger.debug(`\u23ED\uFE0F Auto-skipping middleware '${middleware.name}' for route: ${method} ${path}`, { name });
623
+ } else {
396
624
  allMiddlewares.push(middleware.handler);
397
625
  registeredNames.add(middleware.name);
398
626
  registeredHandlers.add(middleware.handler);
399
- } else {
400
- logger.debug(`\u23ED\uFE0F Skipping middleware '${middleware.name}' for route: ${method} ${path}`, { name });
401
627
  }
402
628
  }
403
629
  }
@@ -426,6 +652,7 @@ function registerRoute(app, name, routeDef, namedMiddlewares) {
426
652
  app[methodLower](path, wrappedHandler);
427
653
  }
428
654
  logger.debug(`Registered route: ${method} ${path}`, { name });
655
+ return { method, path, name };
429
656
  }
430
657
  async function createRouteBuilderContext(c, input) {
431
658
  const params = validateField(input.params, c.req.param(), "path parameters");
@@ -433,9 +660,16 @@ async function createRouteBuilderContext(c, input) {
433
660
  const headers = validateField(input.headers, extractHeaders(c), "headers");
434
661
  const cookies = validateField(input.cookies, extractCookies(c), "cookies");
435
662
  let body = {};
436
- if (input.body) {
437
- const rawBody = await parseJsonBody(c);
438
- body = validateField(input.body, rawBody, "request body");
663
+ let formData = {};
664
+ if (input.body || input.formData) {
665
+ const contentType = c.req.header("content-type") || "";
666
+ if (contentType.includes("multipart/form-data") && input.formData) {
667
+ const rawFormData = await parseFormData(c);
668
+ formData = validateFormData(input.formData, rawFormData, "form data");
669
+ } else if (input.body) {
670
+ const rawBody = await parseJsonBody(c);
671
+ body = validateField(input.body, rawBody, "request body");
672
+ }
439
673
  }
440
674
  let cachedData = null;
441
675
  const responseMeta = {
@@ -446,7 +680,7 @@ async function createRouteBuilderContext(c, input) {
446
680
  const context = {
447
681
  data: async () => {
448
682
  if (!cachedData) {
449
- cachedData = { params, query, body, headers, cookies };
683
+ cachedData = { params, query, body, formData, headers, cookies };
450
684
  }
451
685
  return cachedData;
452
686
  },
@@ -496,14 +730,16 @@ async function createRouteBuilderContext(c, input) {
496
730
  }
497
731
 
498
732
  // src/route/define-middleware.ts
499
- function defineMiddleware(name, handlerOrFactory) {
733
+ function defineMiddleware(name, handlerOrFactory, options) {
734
+ const skips = options?.skips;
500
735
  if (typeof handlerOrFactory === "function") {
501
736
  const paramCount = handlerOrFactory.length;
502
737
  if (paramCount === 2) {
503
738
  return {
504
739
  name,
505
740
  handler: handlerOrFactory,
506
- _name: name
741
+ _name: name,
742
+ ...skips && { skips }
507
743
  };
508
744
  } else {
509
745
  const factory = handlerOrFactory;
@@ -520,13 +756,22 @@ function defineMiddleware(name, handlerOrFactory) {
520
756
  enumerable: false,
521
757
  configurable: true
522
758
  });
759
+ if (skips) {
760
+ Object.defineProperty(wrapper, "skips", {
761
+ value: skips,
762
+ writable: false,
763
+ enumerable: false,
764
+ configurable: true
765
+ });
766
+ }
523
767
  return wrapper;
524
768
  }
525
769
  }
526
770
  return {
527
771
  name,
528
772
  handler: handlerOrFactory,
529
- _name: name
773
+ _name: name,
774
+ ...skips && { skips }
530
775
  };
531
776
  }
532
777
  function defineMiddlewareFactory(name, factory) {
@@ -551,6 +796,6 @@ function isHttpMethod(value) {
551
796
  var Nullable = (schema) => Type.Union([schema, Type.Null()]);
552
797
  var OptionalNullable = (schema) => Type.Optional(Type.Union([schema, Type.Null()]));
553
798
 
554
- export { Nullable, OptionalNullable, defineMiddleware, defineMiddlewareFactory, defineRouter, isHttpMethod, registerRoutes, route };
799
+ export { FileArraySchema, FileSchema, Nullable, OptionalFileSchema, OptionalNullable, defineMiddleware, defineMiddlewareFactory, defineRouter, formatFileSize, getFileOptions, isFileArraySchema, isFileSchema, isHttpMethod, registerRoutes, route };
555
800
  //# sourceMappingURL=index.js.map
556
801
  //# sourceMappingURL=index.js.map