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,57 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
import { definePlugin } from '../define';
|
|
3
|
+
|
|
4
|
+
export type JaxPluginContext = {
|
|
5
|
+
mongoose?: typeof mongoose;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type MongoosePluginOptions = {
|
|
9
|
+
uri?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
async function connectMongoose(config: any, options: unknown) {
|
|
13
|
+
const uri = getMongooseUri(config, options);
|
|
14
|
+
|
|
15
|
+
if (!uri) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
'Mongoose plugin requires options.uri, config.mongoose.uri, config.mongo.uri, or config.mongodb.'
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// mongoose.set('strictQuery', true); // 严格查询模式
|
|
22
|
+
|
|
23
|
+
mongoose.set('toJSON', { virtuals: true, getters: true });
|
|
24
|
+
mongoose.set('debug', (collectionName, methodName, query, doc) => {
|
|
25
|
+
if (['request_log'].some((item) => collectionName.includes(item))) return;
|
|
26
|
+
console.log(`Mongoose: ${collectionName}.${methodName}`, JSON.stringify(query), doc);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await mongoose.connect(uri);
|
|
30
|
+
|
|
31
|
+
console.log('Mongoose connected');
|
|
32
|
+
|
|
33
|
+
return mongoose;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getMongooseUri(config: any, options: unknown) {
|
|
37
|
+
const pluginOptions = isMongoosePluginOptions(options) ? options : {};
|
|
38
|
+
|
|
39
|
+
return pluginOptions.uri ?? config.mongoose?.uri ?? config.mongo?.uri ?? config.mongodb;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isMongoosePluginOptions(options: unknown): options is MongoosePluginOptions {
|
|
43
|
+
return Boolean(options && typeof options === 'object' && !Array.isArray(options));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default definePlugin({
|
|
47
|
+
async setup(ctx, options) {
|
|
48
|
+
ctx.plugin.mongoose = await connectMongoose(ctx.config, options);
|
|
49
|
+
},
|
|
50
|
+
async close() {
|
|
51
|
+
if (mongoose.connection.readyState !== 0) {
|
|
52
|
+
await mongoose.disconnect();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
export * from './model';
|
|
57
|
+
export * from './schema';
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { basename, parse } from 'node:path';
|
|
2
|
+
import mongoose, {
|
|
3
|
+
Schema,
|
|
4
|
+
model as mongooseModel,
|
|
5
|
+
type InferSchemaType,
|
|
6
|
+
type Model,
|
|
7
|
+
type SchemaOptions
|
|
8
|
+
} from 'mongoose';
|
|
9
|
+
import config from '@/jax/config';
|
|
10
|
+
import { rankSetter } from './schema';
|
|
11
|
+
import dayjs from 'dayjs';
|
|
12
|
+
|
|
13
|
+
export { rankSetter };
|
|
14
|
+
|
|
15
|
+
type PageOptions = {
|
|
16
|
+
page?: number | string;
|
|
17
|
+
pageSize?: number | string;
|
|
18
|
+
sort?: Record<string, unknown>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ModelStatics = {
|
|
22
|
+
findPage(
|
|
23
|
+
filter?: Record<string, unknown>,
|
|
24
|
+
options?: PageOptions
|
|
25
|
+
): Promise<{
|
|
26
|
+
total: number;
|
|
27
|
+
list: unknown[];
|
|
28
|
+
maxPage: number;
|
|
29
|
+
page: number;
|
|
30
|
+
pageSize: number;
|
|
31
|
+
}>;
|
|
32
|
+
tree(options?: {
|
|
33
|
+
filter?: Record<string, unknown>;
|
|
34
|
+
sort?: Record<string, unknown>;
|
|
35
|
+
}): Promise<unknown[]>;
|
|
36
|
+
flatTree(options?: {
|
|
37
|
+
filter?: Record<string, unknown>;
|
|
38
|
+
sort?: Record<string, unknown>;
|
|
39
|
+
}): Promise<unknown[]>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function createLooseModel(filePathOrName: string, collection?: string) {
|
|
43
|
+
const name = getModelName(filePathOrName);
|
|
44
|
+
const schema = new Schema({}, { strict: false });
|
|
45
|
+
|
|
46
|
+
applyDefaultSchemaOptions(schema);
|
|
47
|
+
installCommonFields(schema);
|
|
48
|
+
installModelStatics(schema);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
mongoose.models[name] ?? mongooseModel(name, schema, collection ?? getCollectionName(name))
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createSchemaModel<TSchema extends Schema>(
|
|
56
|
+
filePathOrName: string,
|
|
57
|
+
schema: TSchema,
|
|
58
|
+
collection?: string
|
|
59
|
+
): Model<InferSchemaType<TSchema>> & ModelStatics {
|
|
60
|
+
const name = getModelName(filePathOrName);
|
|
61
|
+
|
|
62
|
+
applyDefaultSchemaOptions(schema);
|
|
63
|
+
installCommonFields(schema);
|
|
64
|
+
installModelStatics(schema);
|
|
65
|
+
|
|
66
|
+
return (mongoose.models[name] ??
|
|
67
|
+
mongooseModel<InferSchemaType<TSchema>>(
|
|
68
|
+
name,
|
|
69
|
+
schema as any,
|
|
70
|
+
collection ?? getCollectionName(name)
|
|
71
|
+
)) as Model<InferSchemaType<TSchema>> & ModelStatics;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const createModel = createSchemaModel;
|
|
75
|
+
|
|
76
|
+
export function getModelName(filePathOrName: string) {
|
|
77
|
+
const baseName =
|
|
78
|
+
filePathOrName.includes('/') || filePathOrName.includes('\\')
|
|
79
|
+
? parse(basename(filePathOrName)).name.replace(/\.model$/, '')
|
|
80
|
+
: filePathOrName;
|
|
81
|
+
|
|
82
|
+
return toPascalCase(baseName);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getCollectionName(modelName: string) {
|
|
86
|
+
const prefix = (config as any).mongoose?.prefix ?? (config as any).mongo?.prefix ?? '';
|
|
87
|
+
const name = toSnakeCase(modelName);
|
|
88
|
+
|
|
89
|
+
return prefix ? `${prefix}_${name}` : name;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function schemaOptions(): SchemaOptions {
|
|
93
|
+
return {
|
|
94
|
+
timestamps: true,
|
|
95
|
+
versionKey: false,
|
|
96
|
+
toJSON: {
|
|
97
|
+
virtuals: true,
|
|
98
|
+
getters: true,
|
|
99
|
+
transform(_doc: unknown, ret: any) {
|
|
100
|
+
// if (ret._id && !ret.id) {
|
|
101
|
+
// ret.id = ret._id.toString();
|
|
102
|
+
// }
|
|
103
|
+
|
|
104
|
+
if (ret.createdAt) {
|
|
105
|
+
ret.createdAt = dayjs(ret.createdAt).format('YYYY-MM-DD HH:mm:ss');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (ret.updatedAt) {
|
|
109
|
+
ret.updatedAt = dayjs(ret.updatedAt).format('YYYY-MM-DD HH:mm:ss');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
delete ret.__v;
|
|
113
|
+
delete ret._id;
|
|
114
|
+
|
|
115
|
+
return ret;
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
toObject: {
|
|
119
|
+
virtuals: true,
|
|
120
|
+
getters: true
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function applyDefaultSchemaOptions(schema: Schema) {
|
|
126
|
+
const defaults = schemaOptions();
|
|
127
|
+
|
|
128
|
+
if (schema.options.timestamps === undefined) {
|
|
129
|
+
schema.set('timestamps', defaults.timestamps);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (schema.options.versionKey === undefined || schema.options.versionKey === '__v') {
|
|
133
|
+
schema.set('versionKey', defaults.versionKey);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const toJSON = {
|
|
137
|
+
...defaults.toJSON,
|
|
138
|
+
...(schema.options.toJSON ?? {})
|
|
139
|
+
} as any;
|
|
140
|
+
|
|
141
|
+
schema.set('toJSON', toJSON);
|
|
142
|
+
schema.set('toObject', {
|
|
143
|
+
...defaults.toObject,
|
|
144
|
+
...(schema.options.toObject ?? {})
|
|
145
|
+
} as any);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function installRankHooks(schema: Schema) {
|
|
149
|
+
schema.pre(['updateOne', 'findOneAndUpdate', 'updateMany'], function () {
|
|
150
|
+
normalizeRank(this.getUpdate());
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
schema.pre('save', function () {
|
|
154
|
+
normalizeRank(this);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function installAutoNo(schema: Schema, field: string, start = 0) {
|
|
159
|
+
schema.pre('save', async function () {
|
|
160
|
+
if (!this.isNew || this.get(field)) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const model = this.constructor as any;
|
|
165
|
+
const doc = await model.findOne({}).sort({ [field]: -1 });
|
|
166
|
+
|
|
167
|
+
this.set(field, (doc?.[field] ?? start) + 1);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function installModelStatics(schema: Schema) {
|
|
172
|
+
schema.static('findPage', async function findPage(filter = {}, options: PageOptions = {}) {
|
|
173
|
+
const page = Number(options.page || 1);
|
|
174
|
+
const pageSize = Number(options.pageSize || 20);
|
|
175
|
+
const sort = (options.sort ?? { _id: -1 }) as any;
|
|
176
|
+
const total = await this.countDocuments(filter);
|
|
177
|
+
let query = this.find(filter).sort(sort);
|
|
178
|
+
|
|
179
|
+
if (pageSize !== -1) {
|
|
180
|
+
query = query.skip((page - 1) * pageSize).limit(pageSize);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const list = await query;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
total,
|
|
187
|
+
list,
|
|
188
|
+
maxPage: pageSize === -1 ? 1 : Math.ceil(total / pageSize),
|
|
189
|
+
page,
|
|
190
|
+
pageSize
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
schema.static('tree', async function tree(options: any = {}) {
|
|
195
|
+
const docs = await this.find(options.filter ?? {})
|
|
196
|
+
.sort(options.sort ?? {})
|
|
197
|
+
.lean({ virtuals: true });
|
|
198
|
+
const byId = new Map<string, any>();
|
|
199
|
+
const tree: any[] = [];
|
|
200
|
+
|
|
201
|
+
for (const doc of docs) {
|
|
202
|
+
const id = String(doc._id ?? doc.id);
|
|
203
|
+
byId.set(id, { ...doc, id, children: [] });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const item of byId.values()) {
|
|
207
|
+
const parentId = item.parentId ? String(item.parentId) : '';
|
|
208
|
+
const parent = parentId ? byId.get(parentId) : undefined;
|
|
209
|
+
|
|
210
|
+
if (parent) {
|
|
211
|
+
parent.children.push(item);
|
|
212
|
+
} else {
|
|
213
|
+
tree.push(item);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return tree;
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
schema.static('flatTree', async function flatTree(this: any, options: any = {}) {
|
|
221
|
+
const tree = await this.tree(options);
|
|
222
|
+
const list: any[] = [];
|
|
223
|
+
|
|
224
|
+
walkTree(tree, 1, list);
|
|
225
|
+
|
|
226
|
+
return list;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function installCommonFields(schema: Schema) {
|
|
231
|
+
const fields: Record<string, unknown> = {};
|
|
232
|
+
|
|
233
|
+
if (!schema.path('isDelete')) {
|
|
234
|
+
fields.isDelete = { type: Boolean, default: false };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!schema.path('isOpen')) {
|
|
238
|
+
fields.isOpen = { type: Boolean, default: false };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (Object.keys(fields).length > 0) {
|
|
242
|
+
schema.add(fields as any);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function toSnakeCase(value: string) {
|
|
247
|
+
return value
|
|
248
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
249
|
+
.replace(/[-\s]+/g, '_')
|
|
250
|
+
.toLowerCase();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function toPascalCase(value: string) {
|
|
254
|
+
const camel = value.replace(/[-_\s]+([a-zA-Z0-9])/g, (_match, char: string) =>
|
|
255
|
+
char.toUpperCase()
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function walkTree(items: any[], depth: number, list: any[]) {
|
|
262
|
+
for (const item of items) {
|
|
263
|
+
const { children = [], ...data } = item;
|
|
264
|
+
|
|
265
|
+
list.push({ ...data, children, index: depth });
|
|
266
|
+
walkTree(children, depth + 1, list);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function normalizeRank(data: any) {
|
|
271
|
+
if (!data) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const target = data.$set ?? data;
|
|
276
|
+
|
|
277
|
+
if (target.rank !== undefined) {
|
|
278
|
+
target.rank = rankSetter(target.rank);
|
|
279
|
+
target.isTop = target.rank !== null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Schema, type SchemaDefinitionProperty } from 'mongoose';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
|
|
4
|
+
export const date = (format = 'YYYY-MM-DD HH:mm:ss'): SchemaDefinitionProperty<Date> =>
|
|
5
|
+
({
|
|
6
|
+
type: Date,
|
|
7
|
+
get: (value?: Date) => (value ? dayjs(value).format(format) : value)
|
|
8
|
+
}) as SchemaDefinitionProperty<Date>;
|
|
9
|
+
|
|
10
|
+
export const dateWithDefault = (
|
|
11
|
+
defaultValue: DateConstructor | (() => Date | number | string),
|
|
12
|
+
format = 'YYYY-MM-DD HH:mm:ss'
|
|
13
|
+
): SchemaDefinitionProperty<Date> =>
|
|
14
|
+
({
|
|
15
|
+
type: Date,
|
|
16
|
+
get: (value?: Date) => (value ? dayjs(value).format(format) : value),
|
|
17
|
+
default: defaultValue
|
|
18
|
+
}) as SchemaDefinitionProperty<Date>;
|
|
19
|
+
|
|
20
|
+
export function rankSetter(value: unknown) {
|
|
21
|
+
return value !== '' && value !== undefined && value !== null ? Number(value) : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const rankedFields = {
|
|
25
|
+
rank: { type: Number, set: rankSetter },
|
|
26
|
+
isTop: { type: Boolean, default: false }
|
|
27
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useSession, useSessionStorage } from '@hono/session'
|
|
2
|
+
import type { SessionEnv } from '@hono/session'
|
|
3
|
+
import { Hono } from 'hono'
|
|
4
|
+
import { definePlugin } from '../define'
|
|
5
|
+
|
|
6
|
+
export default definePlugin({
|
|
7
|
+
setup(ctx) {
|
|
8
|
+
ctx.app.use(
|
|
9
|
+
useSession({
|
|
10
|
+
secret: ctx.config?.session?.secret || 'jax-session',
|
|
11
|
+
duration: {
|
|
12
|
+
// 会话的有效时间以秒为单位。超过该时间后,会话将失效,需要重新认证才能继续使用。
|
|
13
|
+
absolute: 60 * 60 * 24 * 7, // 7天
|
|
14
|
+
// 会话被视为活跃的时间长度,以秒为单位。在这一时间内,会话的最大年龄可以延长。
|
|
15
|
+
// inactivity: 60 * 5,
|
|
16
|
+
},
|
|
17
|
+
}),
|
|
18
|
+
)
|
|
19
|
+
},
|
|
20
|
+
})
|
package/setup.ts
ADDED
package/types/config.ts
ADDED
package/types/context.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Models } from '../generated/models.generated';
|
|
2
|
+
import type { Services } from '../generated/services.generated';
|
|
3
|
+
|
|
4
|
+
export type JaxState = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
export type JaxContextVariables = {
|
|
7
|
+
model: Models;
|
|
8
|
+
service: Services;
|
|
9
|
+
state: JaxState;
|
|
10
|
+
};
|
package/types/index.ts
ADDED
package/utils/array.ts
ADDED
package/utils/crypto.ts
ADDED
package/utils/index.ts
ADDED
package/utils/regexp.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 转义正则表达式中的特殊字符,将字符串安全地用于动态构建正则表达式。
|
|
3
|
+
*
|
|
4
|
+
* 会将 `. * + ? ^ $ { } ( ) | [ ] \` 等特殊字符前加上反斜杠进行转义,
|
|
5
|
+
* 确保它们被当作字面字符匹配,而非正则语法符号。
|
|
6
|
+
*
|
|
7
|
+
* @param value - 需要转义的值,可以是任意类型,内部会通过 `String()` 转换为字符串
|
|
8
|
+
* @returns 转义后的字符串,可直接嵌入 `new RegExp()` 中安全使用
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* escapeRegex('hello.world') // => 'hello\\.world'
|
|
12
|
+
* escapeRegex('price$') // => 'price\\$'
|
|
13
|
+
* escapeRegex(123) // => '123'
|
|
14
|
+
*/
|
|
15
|
+
export function escapeRegex(value: unknown) {
|
|
16
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
17
|
+
}
|