jax-hono 1.0.21 → 1.0.23

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.
@@ -33,6 +33,10 @@ type EnabledPlugin = {
33
33
  rootDir: string
34
34
  }
35
35
 
36
+ type PluginFile = EnabledPlugin & {
37
+ filePath: string
38
+ }
39
+
36
40
  const controllerGroups: ControllerGroup[] = [
37
41
  { name: 'api', dirs: ['api', 'controllers/api', 'controller/api', 'modules'], suffix: '.api' },
38
42
  { name: 'backend', dirs: ['backend', 'controllers/backend', 'controller/backend', 'modules'], suffix: '.backend' },
@@ -213,6 +217,10 @@ function getEnabledPluginFiles(suffix: string) {
213
217
  })
214
218
  }
215
219
 
220
+ function isPluginFile(item: { filePath: string } | PluginFile): item is PluginFile {
221
+ return 'name' in item && 'rootDir' in item
222
+ }
223
+
216
224
  function isFileInDir(filePath: string, dirName: string) {
217
225
  const rel = relative(join(srcDir, dirName), filePath)
218
226
 
@@ -510,7 +518,7 @@ function generateServicesRegistry() {
510
518
  .map((match) => match[1])
511
519
  .sort((left, right) => left.localeCompare(right))
512
520
  const defaultServiceClass = source.match(/\bexport\s+default\s+class(?:\s+([a-zA-Z_$][a-zA-Z0-9_$]*))?/)
513
- const fallbackServicePath = 'name' in item
521
+ const fallbackServicePath = isPluginFile(item)
514
522
  ? toPluginModulePath(item.name, item.rootDir, filePath, ['.service'])
515
523
  : toModulePath('services', filePath)
516
524
  const defaultServiceName = defaultServiceClass
@@ -827,24 +835,56 @@ function serializePluginOptions(options: unknown) {
827
835
  return options === undefined ? '' : `, options: ${JSON.stringify(options)}`
828
836
  }
829
837
 
838
+ function pluginExportsJaxPluginContext(rootDir: string) {
839
+ const entryFiles = ['index.ts', 'index.js', 'index.mjs', 'index.cjs']
840
+
841
+ for (const fileName of entryFiles) {
842
+ const filePath = join(rootDir, fileName)
843
+
844
+ if (!existsSync(filePath)) {
845
+ continue
846
+ }
847
+
848
+ const content = readFileSync(filePath, 'utf8')
849
+
850
+ if (/\bexport\s+(?:type\s+)?(?:interface|type)\s+JaxPluginContext\b/.test(content)) {
851
+ return true
852
+ }
853
+ }
854
+
855
+ return false
856
+ }
857
+
830
858
  function generatePluginContextRegistry() {
831
859
  const enabledPlugins = getEnabledPluginConfigItems()
832
860
  const imports = enabledPlugins.map(([name, item]) => {
833
861
  return `import ${toLowerCamelIdentifier([name], 'Plugin')} from ${JSON.stringify(item.package)}`
834
862
  })
863
+ const pluginContexts = enabledPlugins
864
+ .filter(([, item]) => pluginExportsJaxPluginContext(resolvePluginPackageDir(item.package)))
865
+ .map(([name, item]) => ({
866
+ importName: toLowerCamelIdentifier([name], 'PluginContext'),
867
+ packageName: item.package,
868
+ }))
869
+ const contextImports = pluginContexts.map(({ importName, packageName }) => {
870
+ return `import type { JaxPluginContext as ${importName} } from ${JSON.stringify(packageName)}`
871
+ })
872
+ const pluginContextTypes = pluginContexts.map(({ importName }) => importName)
835
873
  const exportNames = enabledPlugins.map(([name]) => toLowerCamelIdentifier([name], 'Plugin'))
836
874
  const moduleEntries = enabledPlugins.map(
837
875
  ([name, item]) =>
838
876
  ` { name: ${JSON.stringify(name)}, module: ${toLowerCamelIdentifier([name], 'Plugin')}${serializePluginOptions(item.options)} },`,
839
877
  )
840
878
  const exportBlock = formatExportBlock(exportNames)
879
+ const pluginsType = ['Record<string, unknown>', ...pluginContextTypes].join(' & ')
841
880
 
842
881
  const content = createGeneratedContent([
843
882
  imports.join('\n'),
883
+ contextImports.join('\n'),
844
884
  exportBlock,
845
- `export const plugins = {} as Record<string, unknown>
885
+ `export type Plugins = ${pluginsType}
846
886
 
847
- export type Plugins = typeof plugins
887
+ export const plugins = {} as Plugins
848
888
 
849
889
  declare module 'jax-hono/core/generated' {
850
890
  interface JaxGenerated {
@@ -1,11 +1,14 @@
1
1
  import { Controller } from "./controller";
2
+ import {
3
+ addDateFilter,
4
+ addDateRangeFilter,
5
+ buildMongooseAutoFilter,
6
+ } from "../plugins/mongoose/filter";
2
7
  import { arrayFrom } from "../utils/array";
3
8
  import { escapeRegex } from "../utils/regexp";
4
9
  import type { Context } from "hono";
5
10
  import { omit, pick, queue } from "jax-hono/utils";
6
11
 
7
- const bodyCache = new WeakMap<Context, unknown>();
8
-
9
12
  export abstract class ApiController extends Controller {
10
13
  declare protected omit?: string | string[];
11
14
  declare protected pick?: string | string[];
@@ -32,6 +35,11 @@ export abstract class ApiController extends Controller {
32
35
  return true;
33
36
  }
34
37
 
38
+ // 启用自动过滤器
39
+ protected get enableAutoFilter() {
40
+ return getLegacyIsAllSearch(this) ?? false;
41
+ }
42
+
35
43
  protected get formatConcurrency() {
36
44
  return 50;
37
45
  }
@@ -52,23 +60,7 @@ export abstract class ApiController extends Controller {
52
60
  return this.context.req.param();
53
61
  }
54
62
 
55
- protected async body<T = Record<string, any>>() {
56
- if (bodyCache.has(this.context)) {
57
- return bodyCache.get(this.context) as T;
58
- }
59
-
60
- try {
61
- const body = await this.context.req.json<T>();
62
- bodyCache.set(this.context, body);
63
63
 
64
- return body;
65
- } catch {
66
- const body = {} as T;
67
- bodyCache.set(this.context, body);
68
-
69
- return body;
70
- }
71
- }
72
64
 
73
65
  async index(c: Context) {
74
66
  const Model = this.requireModel();
@@ -183,13 +175,34 @@ export abstract class ApiController extends Controller {
183
175
 
184
176
  protected async getFilter(extraFilter?: Record<string, any>) {
185
177
  const query = this.context.req.query();
186
- const filter: Record<string, any> = { ...(extraFilter ?? {}) };
187
- const searchKeys = arrayFrom<string>(
188
- (this as any).searchKey ?? (this as any).searchKeys,
189
- );
190
- const likeKeys = arrayFrom<string>(
191
- (this as any).likeKey ?? (this as any).likeKeys,
178
+ const filter: Record<string, any> = {};
179
+ const searchKeys = collectKeys(
180
+ (this as any).searchKey,
181
+ (this as any).searchKeys,
192
182
  );
183
+ const likeKeys = collectKeys((this as any).likeKey, (this as any).likeKeys);
184
+ const keywordKeys = collectKeys((this as any).keywordKeys);
185
+
186
+ if (this.enableAutoFilter) {
187
+ Object.assign(filter, buildMongooseAutoFilter(this.Model, query));
188
+ } else {
189
+ addDateFilter(filter, query, "createdAt", "createdAt");
190
+ addDateRangeFilter(
191
+ filter,
192
+ query,
193
+ "createdAt",
194
+ "createdAtStart",
195
+ "createdAtEnd",
196
+ );
197
+ }
198
+
199
+ if (keywordKeys.length && query.keyword) {
200
+ const reg = new RegExp(escapeRegex(query.keyword), "i");
201
+ filter.$or = [
202
+ ...(Array.isArray(filter.$or) ? filter.$or : []),
203
+ ...keywordKeys.map((key) => ({ [key]: reg })),
204
+ ];
205
+ }
193
206
 
194
207
  for (const key of searchKeys) {
195
208
  if (query[key] !== undefined && query[key] !== "") {
@@ -215,6 +228,10 @@ export abstract class ApiController extends Controller {
215
228
  Object.assign(filter, (this as any).filter);
216
229
  }
217
230
 
231
+ if (extraFilter) {
232
+ Object.assign(filter, extraFilter);
233
+ }
234
+
218
235
  return filter;
219
236
  }
220
237
 
@@ -288,3 +305,38 @@ function getCustomMethod(target: object, name: string, baseMethod: Function) {
288
305
 
289
306
  return undefined;
290
307
  }
308
+
309
+ function getLegacyIsAllSearch(target: object) {
310
+ if (Object.prototype.hasOwnProperty.call(target, "isAllSearch")) {
311
+ return Boolean((target as any).isAllSearch);
312
+ }
313
+
314
+ let prototype = Object.getPrototypeOf(target);
315
+
316
+ while (prototype && prototype !== ApiController.prototype) {
317
+ const descriptor = Object.getOwnPropertyDescriptor(
318
+ prototype,
319
+ "isAllSearch",
320
+ );
321
+
322
+ if (descriptor?.get) {
323
+ return Boolean(descriptor.get.call(target));
324
+ }
325
+
326
+ if (
327
+ "value" in (descriptor ?? {}) &&
328
+ descriptor?.value !== undefined &&
329
+ typeof descriptor.value !== "function"
330
+ ) {
331
+ return Boolean(descriptor.value);
332
+ }
333
+
334
+ prototype = Object.getPrototypeOf(prototype);
335
+ }
336
+
337
+ return undefined;
338
+ }
339
+
340
+ function collectKeys(...values: unknown[]) {
341
+ return values.flatMap((value) => arrayFrom<string>(value as any));
342
+ }
package/core/app.ts CHANGED
@@ -1,13 +1,18 @@
1
- import { createHono, createApi } from './hono';
2
- import { registerCrudRoutes } from '../helpers/crud';
3
- import config from '../config';
4
- import type { Config } from '../config';
5
- import { requireGeneratedModule, type GeneratedModels, type GeneratedPlugins } from './generated';
6
- import { runWithJaxContext } from './controller';
7
- import { startCronJobs, stopCronJobs } from './cron';
8
- import { requestLog } from '../middleware/request-log';
9
- import type { Hono } from 'hono';
10
- import type { JaxHono } from './hono';
1
+ import { createHono, createApi } from "./hono";
2
+ import { registerCrudRoutes } from "../helpers/crud";
3
+ import config from "../config";
4
+ import type { Config } from "../config";
5
+ import {
6
+ requireGeneratedModule,
7
+ type GeneratedModels,
8
+ type GeneratedPlugins,
9
+ } from "./generated";
10
+ import { runWithJaxContext } from "./controller";
11
+ import { startCronJobs, stopCronJobs } from "./cron";
12
+ import { logger } from "../middleware/logger";
13
+ import { requestLog } from "../middleware/request-log";
14
+ import type { Hono } from "hono";
15
+ import type { JaxHono } from "./hono";
11
16
 
12
17
  type Models = GeneratedModels;
13
18
  type CreateServices = (ctx: AppContext) => Record<string, any>;
@@ -55,19 +60,19 @@ type ActivePlugin = {
55
60
 
56
61
  export function createApp(deps: AppRuntimeDeps): JaxHono {
57
62
  const app = createHono();
58
- const { pluginModules } = requireGeneratedModule<{ pluginModules: readonly ModuleItem<AppPlugin>[] }>(
59
- 'plugins.generated.ts'
60
- );
61
- const { cronModules } = requireGeneratedModule<{ cronModules: Parameters<typeof startCronJobs>[0] }>(
62
- 'crons.generated.ts'
63
- );
63
+ const { pluginModules } = requireGeneratedModule<{
64
+ pluginModules: readonly ModuleItem<AppPlugin>[];
65
+ }>("plugins.generated.ts");
66
+ const { cronModules } = requireGeneratedModule<{
67
+ cronModules: Parameters<typeof startCronJobs>[0];
68
+ }>("crons.generated.ts");
64
69
 
65
70
  const ctx: AppContext = {
66
71
  app,
67
72
  config,
68
73
  model: deps.models,
69
74
  service: undefined as unknown as Services,
70
- plugin: {} as Plugins
75
+ plugin: {} as Plugins,
71
76
  };
72
77
  const activePlugins: ActivePlugin[] = [];
73
78
  let activeCrons: ReturnType<typeof startCronJobs> = [];
@@ -75,30 +80,38 @@ export function createApp(deps: AppRuntimeDeps): JaxHono {
75
80
  ctx.service = deps.createServices(ctx);
76
81
  app.jax = ctx;
77
82
 
78
- app.use('*', requestLog);
79
-
80
- app.use('*', async (c, next) => {
83
+ app.use("*", async (c, next) => {
81
84
  const state = {};
82
85
 
83
86
  c.state = state;
84
87
  c.model = ctx.model;
85
88
  c.service = ctx.service;
89
+ c.plugin = ctx.plugin;
86
90
 
87
- c.set('state', state);
88
- c.set('model', ctx.model);
89
- c.set('service', ctx.service);
91
+ c.set("state", state);
92
+ c.set("model", ctx.model);
93
+ c.set("service", ctx.service);
94
+ c.set("plugin", ctx.plugin);
90
95
 
91
96
  await runWithJaxContext(c, next);
92
97
  });
93
98
 
99
+ app.use("*", logger);
100
+
94
101
  const ready = Promise.all(
95
- (pluginModules as readonly ModuleItem<AppPlugin>[]).map((item) => setupPluginSafely(item, ctx))
102
+ (pluginModules as readonly ModuleItem<AppPlugin>[]).map((item) =>
103
+ setupPluginSafely(item, ctx),
104
+ ),
96
105
  ).then(async (plugins) => {
97
- activePlugins.push(...plugins.filter((plugin): plugin is ActivePlugin => Boolean(plugin)));
106
+ activePlugins.push(
107
+ ...plugins.filter((plugin): plugin is ActivePlugin => Boolean(plugin)),
108
+ );
98
109
 
99
110
  printEnabledPlugins(activePlugins);
100
111
 
101
- await Promise.all(activePlugins.map((plugin) => runPluginReadySafely(plugin, ctx)));
112
+ await Promise.all(
113
+ activePlugins.map((plugin) => runPluginReadySafely(plugin, ctx)),
114
+ );
102
115
 
103
116
  activeCrons = startCronJobs(cronModules, ctx);
104
117
  });
@@ -115,7 +128,7 @@ export function createApp(deps: AppRuntimeDeps): JaxHono {
115
128
 
116
129
  async function setupPlugin(
117
130
  item: ModuleItem<AppPlugin>,
118
- ctx: AppContext
131
+ ctx: AppContext,
119
132
  ): Promise<ActivePlugin | undefined> {
120
133
  if (item.enable && !item.enable(ctx)) {
121
134
  return;
@@ -123,11 +136,13 @@ async function setupPlugin(
123
136
 
124
137
  const loadedPlugin = item.module ?? (await loadPlugin(item));
125
138
  const plugin =
126
- loadedPlugin && typeof loadedPlugin === 'object' && 'default' in loadedPlugin
139
+ loadedPlugin &&
140
+ typeof loadedPlugin === "object" &&
141
+ "default" in loadedPlugin
127
142
  ? loadedPlugin.default
128
143
  : loadedPlugin;
129
144
 
130
- if (typeof plugin === 'function') {
145
+ if (typeof plugin === "function") {
131
146
  await plugin(ctx, item.options);
132
147
  return;
133
148
  }
@@ -137,19 +152,21 @@ async function setupPlugin(
137
152
  return {
138
153
  name: item.name,
139
154
  plugin,
140
- options: item.options
155
+ options: item.options,
141
156
  };
142
157
  }
143
158
 
144
- throw new TypeError(`Plugin ${item.name} must export a function or lifecycle object.`);
159
+ throw new TypeError(
160
+ `Plugin ${item.name} must export a function or lifecycle object.`,
161
+ );
145
162
  }
146
163
 
147
164
  function isPluginObject(plugin: unknown): plugin is AppPluginObject {
148
- if (!plugin || typeof plugin !== 'object') {
165
+ if (!plugin || typeof plugin !== "object") {
149
166
  return false;
150
167
  }
151
168
 
152
- return ['setup', 'ready', 'close'].some((key) => key in plugin);
169
+ return ["setup", "ready", "close"].some((key) => key in plugin);
153
170
  }
154
171
 
155
172
  async function loadPlugin(item: ModuleItem<AppPlugin>) {
@@ -183,11 +200,13 @@ async function runPluginReadySafely(item: ActivePlugin, ctx: AppContext) {
183
200
 
184
201
  function printEnabledPlugins(plugins: ActivePlugin[]) {
185
202
  if (plugins.length === 0) {
186
- console.log('[plugins] enabled: none');
203
+ console.log("[plugins] enabled: none");
187
204
  return;
188
205
  }
189
206
 
190
- console.log(`[plugins] enabled: ${plugins.map((plugin) => plugin.name).join(', ')}`);
207
+ console.log(
208
+ `[plugins] enabled: ${plugins.map((plugin) => plugin.name).join(", ")}`,
209
+ );
191
210
  }
192
211
 
193
212
  async function closePluginsSafely(plugins: ActivePlugin[], ctx: AppContext) {
@@ -1,11 +1,13 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
2
  import type { Context } from 'hono';
3
- import type { GeneratedModels, GeneratedServices } from './generated';
3
+ import type { GeneratedModels, GeneratedPlugins, GeneratedServices } from './generated';
4
4
 
5
5
  const controllerContextStorage = new AsyncLocalStorage<Context>();
6
6
  type ControllerConstructor<T extends Controller> = new (...args: any[]) => T;
7
7
  type ControllerExport<T extends Controller> = T | ControllerConstructor<T>;
8
8
 
9
+ const bodyCache = new WeakMap<Context, unknown>();
10
+
9
11
  export function runWithJaxContext<T>(c: Context, callback: () => T) {
10
12
  return controllerContextStorage.run(c, callback);
11
13
  }
@@ -49,6 +51,10 @@ export abstract class Controller {
49
51
  return this.context.get('service');
50
52
  }
51
53
 
54
+ protected get plugin(): GeneratedPlugins {
55
+ return this.context.get('plugin');
56
+ }
57
+
52
58
  protected get session() {
53
59
  return this.context.get('session');
54
60
  }
@@ -82,4 +88,22 @@ export abstract class Controller {
82
88
  prototype = Object.getPrototypeOf(prototype);
83
89
  }
84
90
  }
91
+
92
+ protected async body<T = Record<string, any>>() {
93
+ if (bodyCache.has(this.context)) {
94
+ return bodyCache.get(this.context) as T;
95
+ }
96
+
97
+ try {
98
+ const body = await this.context.req.json<T>();
99
+ bodyCache.set(this.context, body);
100
+
101
+ return body;
102
+ } catch {
103
+ const body = {} as T;
104
+ bodyCache.set(this.context, body);
105
+
106
+ return body;
107
+ }
108
+ }
85
109
  }
package/core/hono.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Hono } from 'hono';
2
2
  import { apiResponse } from '../middleware/api-response';
3
3
  import { registerCrudRoutes, type JaxCrudController } from '../helpers/crud';
4
- import type { GeneratedModels, GeneratedServices } from './generated';
4
+ import type { GeneratedModels, GeneratedPlugins, GeneratedServices } from './generated';
5
5
  import type { JaxState } from '../types/context';
6
6
 
7
7
  declare module 'hono' {
@@ -9,11 +9,13 @@ declare module 'hono' {
9
9
  state: JaxState;
10
10
  model: GeneratedModels;
11
11
  service: GeneratedServices;
12
+ plugin: GeneratedPlugins;
12
13
  }
13
14
 
14
15
  interface ContextVariableMap {
15
16
  model: GeneratedModels;
16
17
  service: GeneratedServices;
18
+ plugin: GeneratedPlugins;
17
19
  state: JaxState;
18
20
  }
19
21
  }
@@ -7,7 +7,7 @@ export type ApiSuccessResponse<T = unknown> = {
7
7
  };
8
8
 
9
9
  export type ApiFailResponse = {
10
- code: 1;
10
+ code: number;
11
11
  msg: string;
12
12
  };
13
13
 
@@ -21,9 +21,9 @@ export function ok<T>(c: Context, data?: T, msg = 'success'): Response {
21
21
  });
22
22
  }
23
23
 
24
- export function fail(c: Context, msg: string): Response {
24
+ export function fail(c: Context, msg: string, code = 1): Response {
25
25
  return c.json<ApiFailResponse>({
26
- code: 1,
26
+ code,
27
27
  msg
28
28
  });
29
29
  }
@@ -31,14 +31,14 @@ export function fail(c: Context, msg: string): Response {
31
31
  declare module 'hono' {
32
32
  interface Context {
33
33
  ok: <T>(data?: T, msg?: string) => Response;
34
- fail: (msg: string) => Response;
34
+ fail: (msg: string, code?: number) => Response;
35
35
  }
36
36
  }
37
37
 
38
38
  export const apiResponse: MiddlewareHandler = async (c, next) => {
39
39
  // 把统一响应方法挂到 Hono Context 上,业务接口直接使用 c.ok / c.fail。
40
40
  c.ok = <T>(data?: T, msg = 'success') => ok(c, data, msg);
41
- c.fail = (msg: string) => fail(c, msg);
41
+ c.fail = (msg: string, code?: number) => fail(c, msg, code);
42
42
 
43
43
  await next();
44
44
  };
@@ -0,0 +1,3 @@
1
+ import { logger as honoLogger } from "hono/logger";
2
+
3
+ export const logger = honoLogger();
@@ -1,3 +1,114 @@
1
- import { logger } from 'hono/logger';
1
+ import type { Context, MiddlewareHandler } from 'hono';
2
2
 
3
- export const requestLog = logger();
3
+ type ContextWithRequestLog = Context & {
4
+ error?: unknown;
5
+ model?: {
6
+ RequestLog?: {
7
+ create(data: Record<string, unknown>): unknown | Promise<unknown>;
8
+ };
9
+ };
10
+ state?: unknown;
11
+ };
12
+
13
+ export const requestLog: MiddlewareHandler = async (c, next) => {
14
+ const start = Date.now();
15
+ const body = getRequestBody(c);
16
+ let thrownError: unknown;
17
+
18
+ try {
19
+ await next();
20
+ } catch (error) {
21
+ thrownError = error;
22
+ throw error;
23
+ } finally {
24
+ await createRequestLog(c as ContextWithRequestLog, start, body, thrownError);
25
+ }
26
+ };
27
+
28
+ async function createRequestLog(
29
+ c: ContextWithRequestLog,
30
+ start: number,
31
+ body: Promise<unknown>,
32
+ thrownError?: unknown
33
+ ) {
34
+ const RequestLog = c.model?.RequestLog;
35
+
36
+ if (!RequestLog) {
37
+ return;
38
+ }
39
+
40
+ try {
41
+ const data: Record<string, unknown> = {
42
+ ip: c.req.header('x-real-ip'),
43
+ url: c.req.url,
44
+ method: c.req.method,
45
+ body: await body,
46
+ query: c.req.query(),
47
+ headers: c.req.header(),
48
+ responseTime: Date.now() - start,
49
+ status: c.res.status,
50
+ state: c.state
51
+ };
52
+ const error = thrownError ?? c.error;
53
+
54
+ if (error) {
55
+ data.error = error instanceof Error
56
+ ? {
57
+ name: error.name,
58
+ message: error.message,
59
+ stack: error.stack,
60
+ cause: error.cause,
61
+ keys: Object.getOwnPropertyNames(error)
62
+ }
63
+ : {
64
+ message: String(error),
65
+ keys: []
66
+ };
67
+ }
68
+
69
+ if (c.res.status !== 200 && c.res.status !== 304) {
70
+ data.response = await c.res.clone().text().catch(() => undefined);
71
+ } else {
72
+ const response = await c.res.clone().json().catch(() => undefined);
73
+
74
+ if (response && typeof response === 'object' && 'code' in response) {
75
+ data.apiCode = response.code;
76
+ if (response.code !== 0) data.response = JSON.stringify(response);
77
+ }
78
+ }
79
+
80
+ await RequestLog.create(data);
81
+ } catch (error) {
82
+ console.error('Request log create failed:', error);
83
+ }
84
+ }
85
+
86
+ async function getRequestBody(c: Context) {
87
+ if (!hasRequestBody(c.req.method)) {
88
+ return undefined;
89
+ }
90
+
91
+ const contentType = c.req.header('content-type') ?? '';
92
+ const request = c.req.raw.clone();
93
+
94
+ try {
95
+ if (contentType.includes('application/json')) {
96
+ return await request.json();
97
+ }
98
+
99
+ if (
100
+ contentType.includes('application/x-www-form-urlencoded') ||
101
+ contentType.includes('multipart/form-data')
102
+ ) {
103
+ return Object.fromEntries(await request.formData());
104
+ }
105
+
106
+ return await request.text();
107
+ } catch {
108
+ return undefined;
109
+ }
110
+ }
111
+
112
+ function hasRequestBody(method: string) {
113
+ return !['GET', 'HEAD'].includes(method.toUpperCase());
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jax-hono",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "Lightweight framework layer on top of Hono, built for Bun",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -0,0 +1,132 @@
1
+ import { escapeRegex } from "../../utils/regexp";
2
+
3
+ export type MongooseQueryValue = string | number | boolean;
4
+ export type MongooseQuery = Record<string, string>;
5
+
6
+ export type MongooseAutoFilterOptions = {
7
+ excludeKeys?: string[];
8
+ };
9
+
10
+ const defaultExcludeKeys = ["id", "_v", "__v"];
11
+
12
+ export function buildMongooseAutoFilter(
13
+ modelOrSchema: any,
14
+ query: MongooseQuery,
15
+ options: MongooseAutoFilterOptions = {},
16
+ ) {
17
+ const filter: Record<string, any> = {};
18
+ const schema = modelOrSchema?.schema ?? modelOrSchema;
19
+ const tree = schema?.tree ?? schema?.paths ?? {};
20
+ const excludeKeys = new Set([
21
+ ...defaultExcludeKeys,
22
+ ...(options.excludeKeys ?? []),
23
+ ]);
24
+
25
+ for (const key in tree) {
26
+ if (excludeKeys.has(key)) {
27
+ continue;
28
+ }
29
+
30
+ const schemaType = getSchemaPath(schema, key);
31
+
32
+ if (schemaType?.instance === "Date") {
33
+ addDateFilter(filter, query, key);
34
+ addDateRangeFilter(filter, query, key, `${key}Start`, `${key}End`);
35
+ }
36
+ }
37
+
38
+ for (const [key, rawValue] of Object.entries(query)) {
39
+ if (rawValue === undefined || rawValue === "") {
40
+ continue;
41
+ }
42
+
43
+ const schemaType = getSchemaPath(schema, key);
44
+
45
+ if (!schemaType || schemaType.instance === "Date") {
46
+ continue;
47
+ }
48
+
49
+ filter[key] = normalizeSchemaFilterValue(rawValue, schemaType.instance);
50
+ }
51
+
52
+ return filter;
53
+ }
54
+
55
+ export function addDateFilter(
56
+ filter: Record<string, any>,
57
+ query: MongooseQuery,
58
+ targetField: string,
59
+ queryKey = targetField,
60
+ ) {
61
+ if (!query[queryKey]) {
62
+ return;
63
+ }
64
+
65
+ filter[targetField] = {
66
+ $gte: new Date(`${query[queryKey]} 00:00:00`),
67
+ $lte: new Date(`${query[queryKey]} 23:59:59`),
68
+ };
69
+ }
70
+
71
+ export function addDateRangeFilter(
72
+ filter: Record<string, any>,
73
+ query: MongooseQuery,
74
+ targetField = "createdAt",
75
+ startDateField = `${targetField}Start`,
76
+ endDateField = `${targetField}End`,
77
+ ) {
78
+ if (query[startDateField]) {
79
+ filter[targetField] = {
80
+ $gte: new Date(processDateRange(query[startDateField], true)),
81
+ };
82
+ }
83
+
84
+ if (query[endDateField]) {
85
+ filter[targetField] = {
86
+ ...filter[targetField],
87
+ $lte: new Date(processDateRange(query[endDateField], false)),
88
+ };
89
+ }
90
+ }
91
+
92
+ function getSchemaPath(schema: any, key: string) {
93
+ return typeof schema?.path === "function"
94
+ ? schema.path(key)
95
+ : schema?.paths?.[key];
96
+ }
97
+
98
+ function normalizeSchemaFilterValue(
99
+ value: MongooseQueryValue,
100
+ schemaInstance?: string,
101
+ ) {
102
+ switch (schemaInstance) {
103
+ case "String":
104
+ return new RegExp(escapeRegex(value), "i");
105
+ case "ObjectId":
106
+ return value;
107
+ case "Number":
108
+ return Number(value);
109
+ case "Boolean":
110
+ return normalizeBoolean(value);
111
+ default:
112
+ return value;
113
+ }
114
+ }
115
+
116
+ function normalizeBoolean(value: MongooseQueryValue) {
117
+ if (typeof value === "string") {
118
+ return value === "true";
119
+ }
120
+
121
+ if (typeof value === "number") {
122
+ return value === 1;
123
+ }
124
+
125
+ return value;
126
+ }
127
+
128
+ function processDateRange(value: string, isStart: boolean) {
129
+ return value.length === 10
130
+ ? `${value} ${isStart ? "00:00:00" : "23:59:59"}`
131
+ : value;
132
+ }
@@ -55,3 +55,4 @@ export default definePlugin({
55
55
  });
56
56
  export * from './model';
57
57
  export * from './schema';
58
+ export * from './filter';
package/types/context.ts CHANGED
@@ -1,9 +1,10 @@
1
- import type { GeneratedModels, GeneratedServices } from '../core/generated';
1
+ import type { GeneratedModels, GeneratedPlugins, GeneratedServices } from '../core/generated';
2
2
 
3
3
  export type JaxState = Record<string, unknown>;
4
4
 
5
5
  export type JaxContextVariables = {
6
6
  model: GeneratedModels;
7
7
  service: GeneratedServices;
8
+ plugin: GeneratedPlugins;
8
9
  state: JaxState;
9
10
  };