@alloy-framework/core 0.4.0 β†’ 0.14.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.
@@ -1,322 +0,0 @@
1
- import { performance } from 'perf_hooks';
2
-
3
- /**
4
- * πŸŽ›οΈ Alloy HTTP 컨트둀러 기반 클래슀
5
- * Rust Axum μ„œλ²„μ˜ HTTP μš”μ²­μ„ μ²˜λ¦¬ν•©λ‹ˆλ‹€.
6
- * μš”μ²­(req)κ³Ό μ™„λ£Œμž(completer)λ₯Ό λ°›μ•„ JSON/HTML/Redirect λ“± λ‹€μ–‘ν•œ 응닡을 μƒμ„±ν•©λ‹ˆλ‹€.
7
- *
8
- * @example
9
- * export const HomeController = createController(class HomeController extends AlloyController {
10
- * async index() {
11
- * this.render('index.html', { title: 'Home' });
12
- * }
13
- *
14
- * async api() {
15
- * this.success({ version: '1.0' });
16
- * }
17
- * });
18
- */
19
- export class AlloyController {
20
- /**
21
- * @param {object} req - Rust AlloyRequest λ„€μ΄ν‹°λΈŒ 객체
22
- * @param {object} completer - Rust AlloyCompleter λ„€μ΄ν‹°λΈŒ ν•Έλ“€
23
- */
24
- constructor(req, completer) {
25
- /** @type {object} λ„€μ΄ν‹°λΈŒ μš”μ²­ 객체 */
26
- this.req = req;
27
- /** @type {object} λ„€μ΄ν‹°λΈŒ 응닡 μ™„λ£Œμž */
28
- this.completer = completer;
29
- /** @type {object|null} AlloyApp μ°Έμ‘° (dispatch μ‹œ μ£Όμž…) */
30
- this.app = null;
31
-
32
- /** @type {number} HTTP 응닡 μƒνƒœ μ½”λ“œ */
33
- this._status = 200;
34
- /** @type {object} 응닡 헀더 */
35
- this._headers = {};
36
- /** @type {number} μš”μ²­ μ‹œμž‘ μ‹œκ°„ (μ„±λŠ₯ 좔적) */
37
- this._start = performance.now();
38
- }
39
-
40
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
41
- // πŸ“₯ μš”μ²­ μ ‘κ·Όμž
42
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
43
-
44
- /** @returns {string} μš”μ²­ URL */
45
- get url() { return this.req.url; }
46
-
47
- /** @returns {string} HTTP λ©”μ„œλ“œ */
48
- get method() { return this.req.method; }
49
-
50
- /** @returns {string} ν΄λΌμ΄μ–ΈνŠΈ IP */
51
- get ip() { return this.req.ip; }
52
-
53
- /** @returns {object} URL 경둜 νŒŒλΌλ―Έν„° (:id λ“±) */
54
- get params() { return this.req.params || {}; }
55
-
56
- /** @returns {object} 쿼리 νŒŒλΌλ―Έν„° */
57
- get queries() { return this.req.query || {}; }
58
-
59
- /**
60
- * μš”μ²­ λ³Έλ¬Έ (μžλ™ JSON / URL-encoded νŒŒμ‹±)
61
- * @returns {object|string}
62
- */
63
- get body() {
64
- if (this._parsedBody !== undefined) return this._parsedBody;
65
-
66
- // dispatchμ—μ„œ validator 처리 μ‹œ 이미 νŒŒμ‹±λœ κ²°κ³Ό μž¬μ‚¬μš© (이쀑 νŒŒμ‹± λ°©μ§€)
67
- if (this.req.parsedBody !== undefined) {
68
- this._parsedBody = this.req.parsedBody;
69
- return this._parsedBody;
70
- }
71
-
72
- const raw = this.req.body;
73
- if (!raw || typeof raw !== 'string') {
74
- this._parsedBody = raw || {};
75
- return this._parsedBody;
76
- }
77
-
78
- const trimmed = raw.trim();
79
-
80
- // JSON 감지
81
- if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
82
- try { this._parsedBody = JSON.parse(trimmed); return this._parsedBody; } catch { /* fallthrough */ }
83
- }
84
-
85
- // URL-encoded 감지
86
- if (trimmed.includes('=')) {
87
- try {
88
- const params = new URLSearchParams(trimmed);
89
- this._parsedBody = Object.fromEntries(params.entries());
90
- return this._parsedBody;
91
- } catch { /* fallthrough */ }
92
- }
93
-
94
- this._parsedBody = raw;
95
- return this._parsedBody;
96
- }
97
-
98
- /**
99
- * 헀더 κ°’ 쑰회 (λŒ€μ†Œλ¬Έμž 무관)
100
- * @param {string} key
101
- * @returns {string|undefined}
102
- */
103
- header(key) {
104
- return this.req.headers?.[key.toLowerCase()] || this.req.headers?.[key];
105
- }
106
-
107
- /**
108
- * 쿼리 νŒŒλΌλ―Έν„° 단일 쑰회
109
- * @param {string} key
110
- * @returns {string|null}
111
- */
112
- query(key) {
113
- return this.req.query ? this.req.query[key] : null;
114
- }
115
-
116
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
117
- // πŸ”’ μ„Έμ…˜ 헬퍼
118
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
119
-
120
- /**
121
- * μ„Έμ…˜ κ°’ μ„€μ •
122
- * @param {string} key
123
- * @param {any} value - κ°μ²΄λŠ” μžλ™ JSON 직렬화
124
- */
125
- setSession(key, value) {
126
- this.req.session = this.req.session || {};
127
- this.req.session[key] = typeof value === 'object' ? JSON.stringify(value) : value;
128
- this._sessionDirty = true; // μ„Έμ…˜ λ³€κ²½ μ‹œ write-back ν”Œλž˜κ·Έ
129
- }
130
-
131
- /**
132
- * μ„Έμ…˜ κ°’ 쑰회 (μžλ™ 역직렬화)
133
- * @param {string} key
134
- * @returns {any}
135
- */
136
- getSession(key) {
137
- if (!this.req.session) return null;
138
- const val = this.req.session[key];
139
- if (val == null) return null;
140
- if (typeof val === 'string') {
141
- try { return JSON.parse(val); } catch { return val; }
142
- }
143
- return val;
144
- }
145
-
146
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
147
- // πŸ“€ 응닡 헬퍼
148
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
149
-
150
- /**
151
- * HTTP μƒνƒœ μ½”λ“œ μ„€μ • (체이닝)
152
- * @param {number} code
153
- * @returns {this}
154
- */
155
- status(code) {
156
- this._status = code;
157
- return this;
158
- }
159
-
160
- /**
161
- * 응닡 헀더 μ„€μ • (체이닝)
162
- * @param {string} key
163
- * @param {string} value
164
- * @returns {this}
165
- */
166
- setHeader(key, value) {
167
- this._headers[key] = value;
168
- return this;
169
- }
170
-
171
- /**
172
- * JSON 응닡 전솑
173
- * @param {object} data
174
- */
175
- json(data) {
176
- this.setHeader('Content-Type', 'application/json');
177
- this.send(JSON.stringify(data));
178
- }
179
-
180
- /**
181
- * Raw Body 응닡 전솑
182
- * @param {string} body
183
- */
184
- send(body) {
185
- this._finalize();
186
- this.completer.done({
187
- status: this._status,
188
- headers: this._headers,
189
- body: typeof body === 'string' ? body : String(body),
190
- session: this._sessionDirty ? this.req.session : undefined,
191
- });
192
- }
193
-
194
- /**
195
- * HTML 응닡 전솑
196
- * @param {string} html - HTML λ¬Έμžμ—΄
197
- */
198
- html(html) {
199
- this.setHeader('Content-Type', 'text/html; charset=utf-8');
200
- this.send(html);
201
- }
202
-
203
- /**
204
- * Tera ν…œν”Œλ¦Ώ λ Œλ”λ§ (Rust μ—”μ§„ ν™œμš©)
205
- * @param {string} view - ν…œν”Œλ¦Ώ 이름 (예: 'index.html')
206
- * @param {object} [data={}] - μ»¨ν…μŠ€νŠΈ 데이터
207
- */
208
- render(view, data = {}) {
209
- // ν…Œλ§ˆ 미듀웨어 지원
210
- if (this.req.theme && !view.startsWith('/') && !view.startsWith(this.req.theme)) {
211
- view = `${this.req.theme}/${view}`;
212
- }
213
-
214
- this._finalize();
215
- this.completer.done({
216
- status: this._status,
217
- headers: this._headers,
218
- body: '',
219
- template: view,
220
- context: JSON.stringify(data),
221
- session: this._sessionDirty ? this.req.session : undefined,
222
- });
223
- }
224
-
225
- /**
226
- * λ¦¬λ‹€μ΄λ ‰νŠΈ
227
- * @param {string} url
228
- * @param {number} [statusCode=302]
229
- */
230
- redirect(url, statusCode = 302) {
231
- this.status(statusCode);
232
- this.setHeader('Location', url);
233
- this.send('');
234
- }
235
-
236
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
237
- // πŸ“Š ν‘œμ€€ 응닡 포맷
238
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
239
-
240
- /**
241
- * 성곡 응닡
242
- * @param {any} data
243
- * @param {string} [message='Ok']
244
- * @param {string} [code='SUCCESS']
245
- */
246
- success(data, message = 'Ok', code = 'SUCCESS') {
247
- this.json({
248
- status: this._status,
249
- code,
250
- message,
251
- timestamp: Date.now(),
252
- data,
253
- });
254
- }
255
-
256
- /**
257
- * μ—λŸ¬ 응닡
258
- * @param {string} message
259
- * @param {string} [code='ERR_GENERIC']
260
- * @param {number} [statusCode=400]
261
- * @param {any} [data=null]
262
- */
263
- error(message, code = 'ERR_GENERIC', statusCode = 400, data = null) {
264
- this.status(statusCode);
265
- this.json({
266
- status: this._status,
267
- code,
268
- message,
269
- timestamp: Date.now(),
270
- data,
271
- });
272
- }
273
-
274
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
275
- // πŸ”§ λ‚΄λΆ€ λ©”μ„œλ“œ
276
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
277
-
278
- /**
279
- * 응닡 직전 처리 (μ„±λŠ₯ 헀더 μΆ”κ°€ λ“±)
280
- * @private
281
- */
282
- _finalize() {
283
- const duration = performance.now() - this._start;
284
- this.setHeader('X-Alloy-Time', `${duration.toFixed(2)}ms`);
285
- }
286
- }
287
-
288
- /**
289
- * 🏭 컨트둀러 Proxy νŒ©ν† λ¦¬
290
- * `Controller.methodName` β†’ `{ controller, method }` 라우트 λ””μŠ€ν¬λ¦½ν„° μžλ™ λ³€ν™˜
291
- *
292
- * @example
293
- * export const Home = createController(class extends AlloyController {
294
- * async index() { this.html('<h1>Hello</h1>'); }
295
- * async about() { this.html('<h1>About</h1>'); }
296
- * });
297
- *
298
- * // 라우트 등둝
299
- * router.get('/', Home); // β†’ index() 호좜
300
- * router.get('/about', Home.about); // β†’ about() 호좜
301
- *
302
- * @param {typeof AlloyController} ControllerClass
303
- * @returns {Proxy}
304
- */
305
- export function createController(ControllerClass) {
306
- return new Proxy(ControllerClass, {
307
- get(target, prop, receiver) {
308
- // prototype λ©”μ„œλ“œ β†’ 라우트 λ””μŠ€ν¬λ¦½ν„° λ°˜ν™˜
309
- if (
310
- typeof prop === 'string' &&
311
- prop !== 'prototype' &&
312
- prop !== 'name' &&
313
- prop !== 'length' &&
314
- prop !== 'constructor' &&
315
- typeof target.prototype[prop] === 'function'
316
- ) {
317
- return { controller: target, method: prop };
318
- }
319
- return Reflect.get(target, prop, receiver);
320
- },
321
- });
322
- }
@@ -1,130 +0,0 @@
1
- import { isMainThread } from 'worker_threads';
2
-
3
- /**
4
- * URL 경둜 κ²°ν•© (path.join은 OS 파일 κ²½λ‘œμš©μ΄λ―€λ‘œ URL에 뢀적합)
5
- * @param {...string} segments - κ²°ν•©ν•  경둜 μ„Έκ·Έλ¨ΌνŠΈ
6
- * @returns {string} μ •κ·œν™”λœ URL 경둜
7
- * @private
8
- */
9
- function joinUrl(...segments) {
10
- return ('/' + segments.join('/')).replace(/\/+/g, '/');
11
- }
12
-
13
- /**
14
- * πŸ—ΊοΈ Alloy λΌμš°ν„°
15
- * HTTP/HTTPS 라우트λ₯Ό λ“±λ‘ν•˜κ³  Rust λ„€μ΄ν‹°λΈŒ λΌμš°ν„°μ— μ „λ‹¬ν•©λ‹ˆλ‹€.
16
- * `controllers/` λ””λ ‰ν† λ¦¬μ˜ Route νŒŒμΌμ—μ„œ μ‚¬μš©ν•©λ‹ˆλ‹€.
17
- *
18
- * @example
19
- * // controllers/HomeRoute.js
20
- * import { HomeController } from './HomeController.js';
21
- *
22
- * export default (router) => {
23
- * router.get('/', HomeController);
24
- * router.get('/about', HomeController.about);
25
- * router.get('/api/status', HomeController.status);
26
- * };
27
- */
28
- export class AlloyRouter {
29
- /**
30
- * @param {object} app - AlloyApp μΈμŠ€ν„΄μŠ€
31
- */
32
- constructor(app) {
33
- /** @type {object} AlloyApp μ°Έμ‘° */
34
- this.app = app;
35
- /** @type {string} ν˜„μž¬ URL ν”„λ¦¬ν”½μŠ€ (group λ‚΄λΆ€μ—μ„œ μ‚¬μš©) */
36
- this.prefix = '';
37
- /** @type {Map<number, object>} ν•Έλ“€λŸ¬ ID β†’ { handler, middlewares, validator } */
38
- this.handlers = new Map();
39
- /** @type {Array} κΈ€λ‘œλ²Œ 미듀웨어 μŠ€νƒ */
40
- this.middlewares = [];
41
- /** @type {number} ν•Έλ“€λŸ¬ ID μΉ΄μš΄ν„° */
42
- this.handlerIdCounter = 1000;
43
- }
44
-
45
- /**
46
- * κΈ€λ‘œλ²Œ 미듀웨어 등둝
47
- * @param {object|Function} middleware
48
- */
49
- use(middleware) {
50
- this.middlewares.push(middleware);
51
- }
52
-
53
- /**
54
- * 라우트 κ·Έλ£Ή (곡톡 ν”„λ¦¬ν”½μŠ€)
55
- * @param {string} prefix - URL ν”„λ¦¬ν”½μŠ€
56
- * @param {Function} callback - (router) => { ... }
57
- */
58
- group(prefix, callback) {
59
- const parentPrefix = this.prefix;
60
- this.prefix = joinUrl(parentPrefix, prefix);
61
- callback(this);
62
- this.prefix = parentPrefix;
63
- }
64
-
65
- /** GET 라우트 */
66
- get(path, ...handlers) { this.register('GET', path, ...handlers); }
67
- /** POST 라우트 */
68
- post(path, ...handlers) { this.register('POST', path, ...handlers); }
69
- /** PUT 라우트 */
70
- put(path, ...handlers) { this.register('PUT', path, ...handlers); }
71
- /** DELETE 라우트 */
72
- delete(path, ...handlers) { this.register('DELETE', path, ...handlers); }
73
-
74
- /**
75
- * 라우트 등둝 (λ„€μ΄ν‹°λΈŒ Rust λΌμš°ν„°μ— 전달)
76
- *
77
- * @param {string} method - HTTP λ©”μ„œλ“œ
78
- * @param {string} path - URL 경둜
79
- * @param {...any} args - [미듀웨어 λ°°μ—΄?, μ˜΅μ…˜?, ν•Έλ“€λŸ¬]
80
- */
81
- register(method, path, ...args) {
82
- // 경둜 μ •κ·œν™”
83
- let fullPath = joinUrl(this.prefix, path);
84
- if (!fullPath.startsWith('/')) fullPath = '/' + fullPath;
85
- // trailing slash 제거 (루트 '/' μ œμ™Έ) β€” path.join이 URL κ²½λ‘œμ— μŠ¬λž˜μ‹œλ₯Ό μΆ”κ°€ν•˜λŠ” 문제 λ°©μ§€
86
- if (fullPath.length > 1 && fullPath.endsWith('/')) fullPath = fullPath.slice(0, -1);
87
-
88
- // λ§ˆμ§€λ§‰ 인자 = ν•Έλ“€λŸ¬
89
- let handler = args.pop();
90
-
91
- // ν•˜μœ„ν˜Έν™˜: (Controller, 'methodName') νŒ¨ν„΄
92
- if (typeof handler === 'string') {
93
- const methodName = handler;
94
- const controllerClass = args.pop();
95
- handler = { controller: controllerClass, method: methodName };
96
- }
97
-
98
- // 미듀웨어 + validator 뢄리
99
- let validator = null;
100
- const middlewares = [];
101
-
102
- for (const arg of args) {
103
- if (Array.isArray(arg)) {
104
- middlewares.push(...arg);
105
- } else if (arg && typeof arg === 'object' && !Array.isArray(arg) && (arg.validator || arg.validate)) {
106
- validator = arg.validator || arg.validate;
107
- } else if (arg) {
108
- middlewares.push(arg);
109
- }
110
- }
111
-
112
- // ν•Έλ“€λŸ¬ ID λΆ€μ—¬ + λ§΅ μ €μž₯
113
- const id = this.handlerIdCounter++;
114
- this.handlers.set(id, { handler, middlewares, validator });
115
-
116
- // Rust λ„€μ΄ν‹°λΈŒ λΌμš°ν„°μ— 등둝 (메인 μŠ€λ ˆλ“œμ—μ„œλ§Œ)
117
- if (isMainThread) {
118
- this.app.native.registerRoute(method, fullPath, id);
119
- }
120
- }
121
-
122
- /**
123
- * ν•Έλ“€λŸ¬ ID둜 ν•Έλ“€λŸ¬ 데이터 쑰회
124
- * @param {number} id
125
- * @returns {object|undefined}
126
- */
127
- getHandler(id) {
128
- return this.handlers.get(id);
129
- }
130
- }
@@ -1,52 +0,0 @@
1
- import Joi from 'joi';
2
-
3
- /**
4
- * βœ… Alloy μš”μ²­ 검증기
5
- * Joi μŠ€ν‚€λ§ˆλ₯Ό ν™œμš©ν•œ 데이터 검증 μœ ν‹Έλ¦¬ν‹°μž…λ‹ˆλ‹€.
6
- *
7
- * @example
8
- * class Users extends AlloyModel {
9
- * static schema = Joi.object({
10
- * username: Joi.string().required(),
11
- * email: Joi.string().email().required(),
12
- * });
13
- * }
14
- */
15
- export class Validator {
16
- /**
17
- * Joi μŠ€ν‚€λ§ˆλ‘œ 데이터 검증 + μ •μ œ
18
- * @param {import('joi').ObjectSchema} schema - Joi μŠ€ν‚€λ§ˆ
19
- * @param {object} data - 검증할 데이터
20
- * @param {boolean} [isPartial=false] - trueλ©΄ λˆ„λ½ ν•„λ“œ ν—ˆμš© (UPDATE용)
21
- * @returns {object} 검증 + μ •μ œλœ 데이터
22
- * @throws {Error} 검증 μ‹€νŒ¨ μ‹œ
23
- */
24
- static validate(schema, data, isPartial = false) {
25
- const options = {
26
- abortEarly: false, // λͺ¨λ“  μ—λŸ¬ μˆ˜μ§‘
27
- stripUnknown: true, // μŠ€ν‚€λ§ˆμ— μ—†λŠ” ν•„λ“œ 제거
28
- allowUnknown: false,
29
- };
30
-
31
- // UPDATE μ‹œ partial 검증 β€” λˆ„λ½ ν•„λ“œ ν—ˆμš©
32
- const target = isPartial ? schema.fork(
33
- Object.keys(schema.describe().keys),
34
- (s) => s.optional(),
35
- ) : schema;
36
-
37
- const { error, value } = target.validate(data, options);
38
-
39
- if (error) {
40
- const details = error.details.map(d => d.message).join(', ');
41
- const err = new Error(`Validation Error: ${details}`);
42
- err.statusCode = 400; // Bad Request
43
- err.code = 'VALIDATION_ERROR';
44
- throw err;
45
- }
46
-
47
- return value;
48
- }
49
- }
50
-
51
- // Joiλ₯Ό re-exportν•˜μ—¬ SDK μ‚¬μš©μžκ°€ 별도 μ„€μΉ˜ 없이 μ‚¬μš© κ°€λŠ₯
52
- export { Joi };
package/src/http/index.js DELETED
@@ -1,6 +0,0 @@
1
- /**
2
- * 🌐 HTTP 계측 톡합 export
3
- */
4
- export { AlloyController, createController } from './AlloyController.js';
5
- export { AlloyRouter } from './AlloyRouter.js';
6
- export { Validator, Joi } from './Validator.js';