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.
- package/build/generate-registry.ts +43 -3
- package/core/api-controller.ts +76 -24
- package/core/app.ts +54 -35
- package/core/controller.ts +25 -1
- package/core/hono.ts +3 -1
- package/middleware/api-response.ts +5 -5
- package/middleware/logger.ts +3 -0
- package/middleware/request-log.ts +113 -2
- package/package.json +1 -1
- package/plugins/mongoose/filter.ts +132 -0
- package/plugins/mongoose/index.ts +1 -0
- package/types/context.ts +2 -1
|
@@ -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 =
|
|
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
|
|
885
|
+
`export type Plugins = ${pluginsType}
|
|
846
886
|
|
|
847
|
-
export
|
|
887
|
+
export const plugins = {} as Plugins
|
|
848
888
|
|
|
849
889
|
declare module 'jax-hono/core/generated' {
|
|
850
890
|
interface JaxGenerated {
|
package/core/api-controller.ts
CHANGED
|
@@ -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> = {
|
|
187
|
-
const searchKeys =
|
|
188
|
-
(this as any).searchKey
|
|
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
|
|
2
|
-
import { registerCrudRoutes } from
|
|
3
|
-
import config from
|
|
4
|
-
import type { Config } from
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import
|
|
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<{
|
|
59
|
-
|
|
60
|
-
);
|
|
61
|
-
const { cronModules } = requireGeneratedModule<{
|
|
62
|
-
|
|
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(
|
|
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(
|
|
88
|
-
c.set(
|
|
89
|
-
c.set(
|
|
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) =>
|
|
102
|
+
(pluginModules as readonly ModuleItem<AppPlugin>[]).map((item) =>
|
|
103
|
+
setupPluginSafely(item, ctx),
|
|
104
|
+
),
|
|
96
105
|
).then(async (plugins) => {
|
|
97
|
-
activePlugins.push(
|
|
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(
|
|
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 &&
|
|
139
|
+
loadedPlugin &&
|
|
140
|
+
typeof loadedPlugin === "object" &&
|
|
141
|
+
"default" in loadedPlugin
|
|
127
142
|
? loadedPlugin.default
|
|
128
143
|
: loadedPlugin;
|
|
129
144
|
|
|
130
|
-
if (typeof plugin ===
|
|
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(
|
|
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 !==
|
|
165
|
+
if (!plugin || typeof plugin !== "object") {
|
|
149
166
|
return false;
|
|
150
167
|
}
|
|
151
168
|
|
|
152
|
-
return [
|
|
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(
|
|
203
|
+
console.log("[plugins] enabled: none");
|
|
187
204
|
return;
|
|
188
205
|
}
|
|
189
206
|
|
|
190
|
-
console.log(
|
|
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) {
|
package/core/controller.ts
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
};
|
|
@@ -1,3 +1,114 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Context, MiddlewareHandler } from 'hono';
|
|
2
2
|
|
|
3
|
-
|
|
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
|
@@ -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
|
+
}
|
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
|
};
|