jax-hono 1.0.0
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/README.md +17 -0
- package/bin/jax.ts +68 -0
- package/build/build.ts +19 -0
- package/build/generate-config.ts +115 -0
- package/build/generate-registry.ts +615 -0
- package/config.ts +76 -0
- package/core/api-controller.ts +244 -0
- package/core/app.ts +199 -0
- package/core/controller.ts +86 -0
- package/core/hono.ts +64 -0
- package/core/service.ts +46 -0
- package/docs/jax.md +272 -0
- package/helpers/config.ts +23 -0
- package/helpers/crud.ts +30 -0
- package/helpers/index.ts +4 -0
- package/helpers/route.ts +7 -0
- package/index.ts +114 -0
- package/middleware/api-response.ts +44 -0
- package/middleware/jwt.ts +76 -0
- package/middleware/request-log.ts +3 -0
- package/package.json +64 -0
- package/plugins/config.ts +8 -0
- package/plugins/define.ts +5 -0
- package/plugins/mongoose/index.ts +57 -0
- package/plugins/mongoose/model.ts +281 -0
- package/plugins/mongoose/schema.ts +27 -0
- package/plugins/session/index.ts +20 -0
- package/setup.ts +11 -0
- package/types/config.ts +13 -0
- package/types/context.ts +10 -0
- package/types/index.ts +2 -0
- package/utils/array.ts +7 -0
- package/utils/crypto.ts +9 -0
- package/utils/index.ts +3 -0
- package/utils/regexp.ts +17 -0
- package/utils/transform.ts +3 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { Controller } from './controller';
|
|
2
|
+
import { arrayFrom } from '../utils/array';
|
|
3
|
+
import { escapeRegex } from '../utils/regexp';
|
|
4
|
+
import type { Context } from 'hono';
|
|
5
|
+
|
|
6
|
+
const bodyCache = new WeakMap<Context, unknown>();
|
|
7
|
+
|
|
8
|
+
export abstract class ApiController extends Controller {
|
|
9
|
+
protected get Model(): any {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
protected get key() {
|
|
14
|
+
return '_id';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
protected get sort() {
|
|
18
|
+
return { [this.key]: -1 };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
protected get isPage() {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected get batchAction() {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
protected get admin(): any {
|
|
30
|
+
return this.state.admin;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected get user(): any {
|
|
34
|
+
return this.state.user;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
protected get query() {
|
|
38
|
+
return this.context.req.query();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
protected get params() {
|
|
42
|
+
return this.context.req.param();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
protected async body<T = Record<string, any>>() {
|
|
46
|
+
if (bodyCache.has(this.context)) {
|
|
47
|
+
return bodyCache.get(this.context) as T;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const body = await this.context.req.json<T>();
|
|
52
|
+
bodyCache.set(this.context, body);
|
|
53
|
+
|
|
54
|
+
return body;
|
|
55
|
+
} catch {
|
|
56
|
+
const body = {} as T;
|
|
57
|
+
bodyCache.set(this.context, body);
|
|
58
|
+
|
|
59
|
+
return body;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async index(c: Context) {
|
|
64
|
+
const Model = this.requireModel();
|
|
65
|
+
const query = c.req.query();
|
|
66
|
+
let options: Record<string, any> = { sort: this.sort };
|
|
67
|
+
|
|
68
|
+
if (typeof (this as any).beforeIndex === 'function') {
|
|
69
|
+
options = { ...options, ...(await (this as any).beforeIndex()) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const filter = await this.getFilter(options.filter);
|
|
73
|
+
let data: any;
|
|
74
|
+
|
|
75
|
+
if (this.isPage) {
|
|
76
|
+
data = await Model.findPage(filter, {
|
|
77
|
+
...options,
|
|
78
|
+
page: query.page,
|
|
79
|
+
pageSize: query.pageSize
|
|
80
|
+
});
|
|
81
|
+
data.list = await this.formatList(data.list);
|
|
82
|
+
} else {
|
|
83
|
+
data = await Model.find(filter).sort(options.sort ?? this.sort);
|
|
84
|
+
data = await this.formatList(data);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return c.ok(data);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async show(c: Context) {
|
|
91
|
+
const Model = this.requireModel();
|
|
92
|
+
const doc = await Model.findOne({ [this.key]: c.req.param('id') });
|
|
93
|
+
|
|
94
|
+
if (!doc) {
|
|
95
|
+
return c.fail('数据获取失败');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return c.ok(await this.formatItem(doc));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async create(c: Context) {
|
|
102
|
+
const Model = this.requireModel();
|
|
103
|
+
const body = await this.body();
|
|
104
|
+
|
|
105
|
+
(this as any).isNew = true;
|
|
106
|
+
|
|
107
|
+
if (typeof (this as any).beforeCreate === 'function') {
|
|
108
|
+
await (this as any).beforeCreate();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (typeof (this as any).beforeSave === 'function') {
|
|
112
|
+
await (this as any).beforeSave();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const doc = await Model.create(body);
|
|
116
|
+
|
|
117
|
+
if (typeof (this as any).afterSave === 'function') {
|
|
118
|
+
await (this as any).afterSave(doc);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return c.ok(undefined, '操作成功');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async update(c: Context) {
|
|
125
|
+
const Model = this.requireModel();
|
|
126
|
+
const body = await this.body();
|
|
127
|
+
const ids = (c.req.param('id') ?? '').split(',').filter(Boolean);
|
|
128
|
+
|
|
129
|
+
(this as any).isUpdate = true;
|
|
130
|
+
(this as any).isBatch = ids.length > 1;
|
|
131
|
+
|
|
132
|
+
if ((this as any).isBatch && !this.batchAction) {
|
|
133
|
+
return c.fail('不可批量操作');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (typeof (this as any).beforeUpdate === 'function') {
|
|
137
|
+
await (this as any).beforeUpdate();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (typeof (this as any).beforeSave === 'function') {
|
|
141
|
+
await (this as any).beforeSave();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let doc: any;
|
|
145
|
+
|
|
146
|
+
if ((this as any).isBatch) {
|
|
147
|
+
await Model.updateMany({ [this.key]: { $in: ids } }, body);
|
|
148
|
+
} else {
|
|
149
|
+
doc = await Model.findOneAndUpdate({ [this.key]: ids[0] }, body, { new: true });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (typeof (this as any).afterSave === 'function') {
|
|
153
|
+
await (this as any).afterSave(doc);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return c.ok(undefined, '操作成功');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async delete(c: Context) {
|
|
160
|
+
const Model = this.requireModel();
|
|
161
|
+
const ids = (c.req.param('id') ?? '').split(',').filter(Boolean);
|
|
162
|
+
|
|
163
|
+
if (ids.length > 1 && !this.batchAction) {
|
|
164
|
+
return c.fail('不可批量操作');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await Model.deleteMany({ [this.key]: { $in: ids } });
|
|
168
|
+
|
|
169
|
+
return c.ok(undefined, '操作成功');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async destroy(c: Context) {
|
|
173
|
+
return this.delete(c);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
protected async getFilter(extraFilter?: Record<string, any>) {
|
|
177
|
+
const query = this.context.req.query();
|
|
178
|
+
const filter: Record<string, any> = { ...(extraFilter ?? {}) };
|
|
179
|
+
const searchKeys = arrayFrom<string>((this as any).searchKey ?? (this as any).searchKeys);
|
|
180
|
+
const likeKeys = arrayFrom<string>((this as any).likeKey ?? (this as any).likeKeys);
|
|
181
|
+
|
|
182
|
+
for (const key of searchKeys) {
|
|
183
|
+
if (query[key] !== undefined && query[key] !== '') {
|
|
184
|
+
filter[key] = query[key];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const key of likeKeys) {
|
|
189
|
+
if (query[key] !== undefined && query[key] !== '') {
|
|
190
|
+
filter[key] = new RegExp(escapeRegex(query[key]), 'i');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const customGetFilter = getCustomMethod(this, 'getFilter', ApiController.prototype.getFilter);
|
|
195
|
+
|
|
196
|
+
if (customGetFilter) {
|
|
197
|
+
Object.assign(filter, await customGetFilter.call(this, filter));
|
|
198
|
+
} else if ((this as any).filter) {
|
|
199
|
+
Object.assign(filter, (this as any).filter);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return filter;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
protected async formatList(list: any[]) {
|
|
206
|
+
return Promise.all(list.map((item) => this.formatItem(item)));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
protected async formatItem(item: any) {
|
|
210
|
+
const data = item?.toJSON ? item.toJSON() : { ...item };
|
|
211
|
+
|
|
212
|
+
if (typeof (this as any).formatData === 'function') {
|
|
213
|
+
const formatted = await (this as any).formatData(data);
|
|
214
|
+
|
|
215
|
+
return formatted ?? data;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return data;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private requireModel() {
|
|
222
|
+
if (!this.Model) {
|
|
223
|
+
throw new Error(`${this.constructor.name} must define Model.`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return this.Model;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getCustomMethod(target: object, name: string, baseMethod: Function) {
|
|
231
|
+
let prototype = Object.getPrototypeOf(target);
|
|
232
|
+
|
|
233
|
+
while (prototype && prototype !== ApiController.prototype) {
|
|
234
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
|
|
235
|
+
|
|
236
|
+
if (typeof descriptor?.value === 'function' && descriptor.value !== baseMethod) {
|
|
237
|
+
return descriptor.value;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
package/core/app.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
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 { pluginModules } from '../generated/plugins.generated';
|
|
6
|
+
import type { models as generatedModels } from '../generated/models.generated';
|
|
7
|
+
import type { createServices as generatedCreateServices } from '../generated/services.generated';
|
|
8
|
+
import { runWithJaxContext } from './controller';
|
|
9
|
+
import { requestLog } from '../middleware/request-log';
|
|
10
|
+
import type { Hono } from 'hono';
|
|
11
|
+
import type { JaxHono } from './hono';
|
|
12
|
+
import type { plugins as generatedPlugins } from '../generated/plugins.generated';
|
|
13
|
+
|
|
14
|
+
type Models = typeof generatedModels;
|
|
15
|
+
type CreateServices = typeof generatedCreateServices;
|
|
16
|
+
type Services = ReturnType<CreateServices>;
|
|
17
|
+
type Plugins = typeof generatedPlugins;
|
|
18
|
+
|
|
19
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
20
|
+
|
|
21
|
+
export type AppContext = {
|
|
22
|
+
app: Hono;
|
|
23
|
+
config: Config;
|
|
24
|
+
model: Models;
|
|
25
|
+
service: Services;
|
|
26
|
+
plugin: Plugins & Record<string, any>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type AppRuntimeDeps = {
|
|
30
|
+
models: Models;
|
|
31
|
+
createServices: CreateServices;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type AppPluginObject = {
|
|
35
|
+
setup?: (ctx: AppContext, options?: unknown) => MaybePromise<void>;
|
|
36
|
+
ready?: (ctx: AppContext, options?: unknown) => MaybePromise<void>;
|
|
37
|
+
close?: (ctx: AppContext, options?: unknown) => MaybePromise<void>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type AppPlugin =
|
|
41
|
+
| ((ctx: AppContext, options?: unknown) => MaybePromise<void>)
|
|
42
|
+
| AppPluginObject;
|
|
43
|
+
|
|
44
|
+
type ModuleItem<T> = {
|
|
45
|
+
name: string;
|
|
46
|
+
module?: T;
|
|
47
|
+
load?: () => Promise<{ default?: T } | T>;
|
|
48
|
+
enable?: (ctx: AppContext) => boolean;
|
|
49
|
+
options?: unknown;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type ActivePlugin = {
|
|
53
|
+
name: string;
|
|
54
|
+
plugin: AppPluginObject;
|
|
55
|
+
options?: unknown;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function createApp(deps: AppRuntimeDeps): JaxHono {
|
|
59
|
+
const app = createHono();
|
|
60
|
+
|
|
61
|
+
const ctx: AppContext = {
|
|
62
|
+
app,
|
|
63
|
+
config,
|
|
64
|
+
model: deps.models,
|
|
65
|
+
service: undefined as unknown as Services,
|
|
66
|
+
plugin: {} as Plugins
|
|
67
|
+
};
|
|
68
|
+
const activePlugins: ActivePlugin[] = [];
|
|
69
|
+
|
|
70
|
+
ctx.service = deps.createServices(ctx);
|
|
71
|
+
app.jax = ctx;
|
|
72
|
+
|
|
73
|
+
app.use('*', requestLog);
|
|
74
|
+
|
|
75
|
+
app.use('*', async (c, next) => {
|
|
76
|
+
const state = {};
|
|
77
|
+
|
|
78
|
+
c.state = state;
|
|
79
|
+
c.model = ctx.model;
|
|
80
|
+
c.service = ctx.service;
|
|
81
|
+
|
|
82
|
+
c.set('state', state);
|
|
83
|
+
c.set('model', ctx.model);
|
|
84
|
+
c.set('service', ctx.service);
|
|
85
|
+
|
|
86
|
+
await runWithJaxContext(c, next);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const ready = Promise.all(
|
|
90
|
+
(pluginModules as readonly ModuleItem<AppPlugin>[]).map((item) => setupPluginSafely(item, ctx))
|
|
91
|
+
).then(async (plugins) => {
|
|
92
|
+
activePlugins.push(...plugins.filter((plugin): plugin is ActivePlugin => Boolean(plugin)));
|
|
93
|
+
|
|
94
|
+
printEnabledPlugins(activePlugins);
|
|
95
|
+
|
|
96
|
+
await Promise.all(activePlugins.map((plugin) => runPluginReadySafely(plugin, ctx)));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
app.ready = ready;
|
|
100
|
+
app.close = async () => {
|
|
101
|
+
await ready.catch(() => undefined);
|
|
102
|
+
await closePluginsSafely(activePlugins, ctx);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return app;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function setupPlugin(
|
|
109
|
+
item: ModuleItem<AppPlugin>,
|
|
110
|
+
ctx: AppContext
|
|
111
|
+
): Promise<ActivePlugin | undefined> {
|
|
112
|
+
if (item.enable && !item.enable(ctx)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const loadedPlugin = item.module ?? (await loadPlugin(item));
|
|
117
|
+
const plugin =
|
|
118
|
+
loadedPlugin && typeof loadedPlugin === 'object' && 'default' in loadedPlugin
|
|
119
|
+
? loadedPlugin.default
|
|
120
|
+
: loadedPlugin;
|
|
121
|
+
|
|
122
|
+
if (typeof plugin === 'function') {
|
|
123
|
+
await plugin(ctx, item.options);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (isPluginObject(plugin)) {
|
|
128
|
+
await plugin.setup?.(ctx, item.options);
|
|
129
|
+
return {
|
|
130
|
+
name: item.name,
|
|
131
|
+
plugin,
|
|
132
|
+
options: item.options
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
throw new TypeError(`Plugin ${item.name} must export a function or lifecycle object.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isPluginObject(plugin: unknown): plugin is AppPluginObject {
|
|
140
|
+
if (!plugin || typeof plugin !== 'object') {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return ['setup', 'ready', 'close'].some((key) => key in plugin);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function loadPlugin(item: ModuleItem<AppPlugin>) {
|
|
148
|
+
if (!item.load) {
|
|
149
|
+
throw new TypeError(`Plugin ${item.name} must define module or load.`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return item.load();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function setupPluginSafely(item: ModuleItem<AppPlugin>, ctx: AppContext) {
|
|
156
|
+
try {
|
|
157
|
+
return await setupPlugin(item, ctx);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error(`Jax plugin setup failed [${item.name}]:`, error);
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function runPluginReadySafely(item: ActivePlugin, ctx: AppContext) {
|
|
165
|
+
if (!item.plugin.ready) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
await item.plugin.ready(ctx, item.options);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error(`Jax plugin ready failed [${item.name}]:`, error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function printEnabledPlugins(plugins: ActivePlugin[]) {
|
|
177
|
+
if (plugins.length === 0) {
|
|
178
|
+
console.log('[plugins] enabled: none');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(`[plugins] enabled: ${plugins.map((plugin) => plugin.name).join(', ')}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function closePluginsSafely(plugins: ActivePlugin[], ctx: AppContext) {
|
|
186
|
+
for (const item of [...plugins].reverse()) {
|
|
187
|
+
if (!item.plugin.close) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
await item.plugin.close(ctx, item.options);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error(`Jax plugin close failed [${item.name}]:`, error);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export { createApi, createHono, registerCrudRoutes };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import type { Context } from 'hono';
|
|
3
|
+
import type { Models } from '../generated/models.generated';
|
|
4
|
+
import type { Services } from '../generated/services.generated';
|
|
5
|
+
|
|
6
|
+
const controllerContextStorage = new AsyncLocalStorage<Context>();
|
|
7
|
+
type ControllerConstructor<T extends Controller> = new (...args: any[]) => T;
|
|
8
|
+
type ControllerExport<T extends Controller> = T | ControllerConstructor<T>;
|
|
9
|
+
|
|
10
|
+
export function runWithJaxContext<T>(c: Context, callback: () => T) {
|
|
11
|
+
return controllerContextStorage.run(c, callback);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getJaxContext() {
|
|
15
|
+
const context = controllerContextStorage.getStore();
|
|
16
|
+
|
|
17
|
+
if (!context) {
|
|
18
|
+
throw new Error('Jax request context is not available.');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return context;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createController<T extends Controller>(controller: ControllerExport<T>) {
|
|
25
|
+
return typeof controller === 'function' ? new controller() : controller;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export abstract class Controller {
|
|
29
|
+
constructor() {
|
|
30
|
+
this.bindMethods();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected get context() {
|
|
34
|
+
return getJaxContext();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
protected get ctx() {
|
|
38
|
+
return this.context;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
protected get state() {
|
|
42
|
+
return this.context.get('state');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
protected get model(): Models {
|
|
46
|
+
return this.context.get('model');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
protected get service(): Services {
|
|
50
|
+
return this.context.get('service');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
protected get session() {
|
|
54
|
+
return this.context.get('session');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private bindMethods() {
|
|
58
|
+
let prototype = Object.getPrototypeOf(this);
|
|
59
|
+
|
|
60
|
+
while (prototype && prototype !== Controller.prototype) {
|
|
61
|
+
for (const name of Object.getOwnPropertyNames(prototype)) {
|
|
62
|
+
if (name === 'constructor') {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
|
|
67
|
+
|
|
68
|
+
if (typeof descriptor?.value !== 'function') {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (Object.prototype.hasOwnProperty.call(this, name)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
Object.defineProperty(this, name, {
|
|
77
|
+
configurable: true,
|
|
78
|
+
writable: true,
|
|
79
|
+
value: descriptor.value.bind(this),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
package/core/hono.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { apiResponse } from '../middleware/api-response';
|
|
3
|
+
import { registerCrudRoutes, type JaxCrudController } from '../helpers/crud';
|
|
4
|
+
import type { Models } from '../generated/models.generated';
|
|
5
|
+
import type { Services } from '../generated/services.generated';
|
|
6
|
+
import type { JaxState } from '../types/context';
|
|
7
|
+
|
|
8
|
+
declare module 'hono' {
|
|
9
|
+
interface Context {
|
|
10
|
+
state: JaxState;
|
|
11
|
+
model: Models;
|
|
12
|
+
service: Services;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ContextVariableMap {
|
|
16
|
+
model: Models;
|
|
17
|
+
service: Services;
|
|
18
|
+
state: JaxState;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type JaxHono = Hono & {
|
|
23
|
+
ready?: Promise<void>;
|
|
24
|
+
close?: () => Promise<void>;
|
|
25
|
+
jax?: unknown;
|
|
26
|
+
resources(path: string, controller: JaxCrudController): JaxHono;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function createHono(): JaxHono {
|
|
30
|
+
const app = new Hono();
|
|
31
|
+
|
|
32
|
+
return withJaxRoutes(app);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createApi() {
|
|
36
|
+
const api = createHono();
|
|
37
|
+
|
|
38
|
+
// API 路由默认使用 { code, msg, data } 响应结构,并统一兜底异常返回。
|
|
39
|
+
api.use('*', apiResponse);
|
|
40
|
+
api.notFound((c) => c.fail('接口不存在'));
|
|
41
|
+
api.onError((error, c) => {
|
|
42
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
43
|
+
|
|
44
|
+
console.error('API 请求失败:', error);
|
|
45
|
+
|
|
46
|
+
return c.fail(message);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return api;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function withJaxRoutes(app: Hono): JaxHono {
|
|
53
|
+
const jaxApp = app as JaxHono;
|
|
54
|
+
|
|
55
|
+
jaxApp.resources = (path, controller) => {
|
|
56
|
+
registerCrudRoutes(jaxApp, path, controller);
|
|
57
|
+
|
|
58
|
+
return jaxApp;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return jaxApp;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { Hono };
|
package/core/service.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Config } from '../config';
|
|
2
|
+
import type { Models } from '../generated/models.generated';
|
|
3
|
+
import type { Services } from '../generated/services.generated';
|
|
4
|
+
import type { plugins as generatedPlugins } from '../generated/plugins.generated';
|
|
5
|
+
import { getJaxContext } from './controller';
|
|
6
|
+
|
|
7
|
+
type Plugins = typeof generatedPlugins;
|
|
8
|
+
|
|
9
|
+
export type ServiceContext = {
|
|
10
|
+
config: Config;
|
|
11
|
+
model: Models;
|
|
12
|
+
service: Services;
|
|
13
|
+
plugin: Plugins;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export abstract class Service {
|
|
17
|
+
constructor(protected readonly appContext: ServiceContext) {}
|
|
18
|
+
|
|
19
|
+
protected get context() {
|
|
20
|
+
return getJaxContext();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
protected get ctx() {
|
|
24
|
+
return this.context;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
protected get state() {
|
|
28
|
+
return this.context.get('state');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
protected get config() {
|
|
32
|
+
return this.appContext.config;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
protected get model() {
|
|
36
|
+
return this.appContext.model;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
protected get service() {
|
|
40
|
+
return this.appContext.service;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
protected get plugin(): Plugins {
|
|
44
|
+
return this.appContext.plugin;
|
|
45
|
+
}
|
|
46
|
+
}
|