@gravito/monolith 1.0.0-alpha.6 → 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/CHANGELOG.md +9 -0
- package/README.md +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/index.cjs +428 -0
- package/dist/index.d.cts +168 -0
- package/dist/index.d.ts +168 -0
- package/dist/index.js +309 -5521
- package/ion/src/index.js +2 -2
- package/package.json +29 -15
- package/src/ContentManager.ts +76 -6
- package/src/Controller.ts +82 -0
- package/src/FormRequest.ts +86 -0
- package/src/Router.ts +35 -0
- package/src/Sanitizer.ts +32 -0
- package/src/index.ts +6 -2
- package/src/middleware/TrimStrings.ts +39 -0
- package/tests/ecommerce_integration.test.ts +58 -0
- package/tests/extra.test.ts +135 -0
- package/tests/mvc.test.ts +150 -0
- package/tsconfig.json +3 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { rm } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { ContentManager } from '../src/ContentManager'
|
|
7
|
+
import { Controller } from '../src/Controller'
|
|
8
|
+
import { OrbitMonolith } from '../src/index'
|
|
9
|
+
import { Sanitizer } from '../src/Sanitizer'
|
|
10
|
+
|
|
11
|
+
class TestController extends Controller {
|
|
12
|
+
public run() {
|
|
13
|
+
return {
|
|
14
|
+
json: this.json({ ok: true }, 201),
|
|
15
|
+
text: this.text('hi', 202),
|
|
16
|
+
redirect: this.redirect('/next', 303),
|
|
17
|
+
value: this.get<string>('token'),
|
|
18
|
+
req: this.request,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public async runValidate() {
|
|
23
|
+
return this.validate<{ id: number }>({}, 'json')
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('Controller helpers', () => {
|
|
28
|
+
it('exposes response helpers and request access', async () => {
|
|
29
|
+
const context = {
|
|
30
|
+
json: (data: unknown, status: number) => ({ type: 'json', data, status }),
|
|
31
|
+
text: (text: string, status: number) => ({ type: 'text', text, status }),
|
|
32
|
+
redirect: (url: string, status: number) => ({ type: 'redirect', url, status }),
|
|
33
|
+
get: (key: string) => (key === 'token' ? 'abc' : undefined),
|
|
34
|
+
req: {
|
|
35
|
+
valid: () => ({ id: 42 }),
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const controller = new TestController().setContext(context as any)
|
|
40
|
+
const result = controller.run()
|
|
41
|
+
|
|
42
|
+
expect(result.json).toEqual({ type: 'json', data: { ok: true }, status: 201 })
|
|
43
|
+
expect(result.text).toEqual({ type: 'text', text: 'hi', status: 202 })
|
|
44
|
+
expect(result.redirect).toEqual({ type: 'redirect', url: '/next', status: 303 })
|
|
45
|
+
expect(result.value).toBe('abc')
|
|
46
|
+
expect(result.req).toBe(context.req)
|
|
47
|
+
|
|
48
|
+
const validated = await controller.runValidate()
|
|
49
|
+
expect(validated.id).toBe(42)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('Sanitizer', () => {
|
|
54
|
+
it('cleans strings and nested objects', () => {
|
|
55
|
+
expect(Sanitizer.clean('plain')).toBe('plain')
|
|
56
|
+
|
|
57
|
+
const data = {
|
|
58
|
+
name: ' Ada ',
|
|
59
|
+
empty: ' ',
|
|
60
|
+
nested: {
|
|
61
|
+
title: ' Test ',
|
|
62
|
+
},
|
|
63
|
+
items: [' one ', ''],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const cleaned = Sanitizer.clean(data)
|
|
67
|
+
expect(cleaned).toEqual({
|
|
68
|
+
name: 'Ada',
|
|
69
|
+
empty: null,
|
|
70
|
+
nested: { title: 'Test' },
|
|
71
|
+
items: ['one', null],
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('ContentManager security helpers', () => {
|
|
77
|
+
it('sanitizes path segments and escapes unsafe output', async () => {
|
|
78
|
+
const root = mkdtempSync(join(tmpdir(), 'monolith-content-'))
|
|
79
|
+
const base = join(root, 'content', 'docs', 'en')
|
|
80
|
+
mkdirSync(base, { recursive: true })
|
|
81
|
+
|
|
82
|
+
const markdown = `---\ntitle: Safe\n---\n\n<link>\n\n[bad](javascript:alert(1))\n\n[good](https://example.com "Title")`
|
|
83
|
+
writeFileSync(join(base, 'safe.md'), markdown)
|
|
84
|
+
|
|
85
|
+
const manager = new ContentManager(root)
|
|
86
|
+
manager.defineCollection('docs', { path: 'content/docs' })
|
|
87
|
+
|
|
88
|
+
expect(await manager.find('docs', '../hack', 'en')).toBeNull()
|
|
89
|
+
expect(await manager.list('docs', '../evil')).toEqual([])
|
|
90
|
+
|
|
91
|
+
const item = await manager.find('docs', 'safe', 'en')
|
|
92
|
+
expect(item).not.toBeNull()
|
|
93
|
+
expect(item?.body).toContain('<link>')
|
|
94
|
+
expect(item?.body).toContain('bad')
|
|
95
|
+
expect(item?.body).toContain('<a href="https://example.com" title="Title">good</a>')
|
|
96
|
+
|
|
97
|
+
await rm(root, { recursive: true, force: true })
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('OrbitMonolith', () => {
|
|
102
|
+
it('injects ContentManager into context', async () => {
|
|
103
|
+
const middlewares: Array<(c: any, next: () => Promise<void>) => Promise<void>> = []
|
|
104
|
+
|
|
105
|
+
const core = {
|
|
106
|
+
adapter: {
|
|
107
|
+
use: (_path: string, handler: any) => {
|
|
108
|
+
middlewares.push(handler)
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
logger: {
|
|
112
|
+
info: (_message: string) => {},
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const orbit = new OrbitMonolith({
|
|
117
|
+
root: '/tmp',
|
|
118
|
+
collections: { docs: { path: 'content/docs' } },
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
orbit.install(core as any)
|
|
122
|
+
|
|
123
|
+
const context = {
|
|
124
|
+
set: (key: string, value: unknown) => {
|
|
125
|
+
if (key === 'content') {
|
|
126
|
+
context.content = value
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
content: null as null | ContentManager,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await middlewares[0]?.(context, async () => {})
|
|
133
|
+
expect(context.content).toBeInstanceOf(ContentManager)
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'bun:test'
|
|
2
|
+
import { Photon } from '@gravito/photon'
|
|
3
|
+
import { Controller, FormRequest, Route, Schema } from '../src'
|
|
4
|
+
|
|
5
|
+
// --- Mock Classes ---
|
|
6
|
+
|
|
7
|
+
class MockController extends Controller {
|
|
8
|
+
async index() {
|
|
9
|
+
return this.json({ success: true, action: 'index' })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async store() {
|
|
13
|
+
return this.json({ success: true, action: 'store' }, 201)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async show() {
|
|
17
|
+
return this.text('Viewing record')
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class MockRequest extends FormRequest {
|
|
22
|
+
authorize() {
|
|
23
|
+
// Simulate authorization failure if header is present
|
|
24
|
+
return this.context.req.header('x-fail-auth') !== 'yes'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
schema() {
|
|
28
|
+
return Schema.Object({
|
|
29
|
+
name: Schema.String({ minLength: 3 }),
|
|
30
|
+
age: Schema.Number(),
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --- Tests ---
|
|
36
|
+
|
|
37
|
+
describe('@gravito/monolith MVC', () => {
|
|
38
|
+
let app: Photon
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
app = new Photon()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('Base Controller', () => {
|
|
45
|
+
it('should resolve call() and return json', async () => {
|
|
46
|
+
app.get('/test', MockController.call('index'))
|
|
47
|
+
|
|
48
|
+
const res = await app.request('/test')
|
|
49
|
+
expect(res.status).toBe(200)
|
|
50
|
+
expect(await res.json()).toEqual({ success: true, action: 'index' })
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should resolve call() and return text', async () => {
|
|
54
|
+
app.get('/show', MockController.call('show'))
|
|
55
|
+
|
|
56
|
+
const res = await app.request('/show')
|
|
57
|
+
expect(res.status).toBe(200)
|
|
58
|
+
expect(await res.text()).toBe('Viewing record')
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('Route Helper (Resource Routing)', () => {
|
|
63
|
+
it('should register multiple standard routes automatically', async () => {
|
|
64
|
+
Route.resource(app, 'users', MockController)
|
|
65
|
+
|
|
66
|
+
const resIndex = await app.request('/users')
|
|
67
|
+
expect(resIndex.status).toBe(200)
|
|
68
|
+
expect(await resIndex.json()).toEqual({ success: true, action: 'index' })
|
|
69
|
+
|
|
70
|
+
const resStore = await app.request('/users', { method: 'POST' })
|
|
71
|
+
expect(resStore.status).toBe(201)
|
|
72
|
+
expect(await resStore.json()).toEqual({ success: true, action: 'store' })
|
|
73
|
+
|
|
74
|
+
const resShow = await app.request('/users/123')
|
|
75
|
+
expect(resShow.status).toBe(200)
|
|
76
|
+
expect(await resShow.text()).toBe('Viewing record')
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('FormRequest', () => {
|
|
81
|
+
it('should pass valid data', async () => {
|
|
82
|
+
app.post('/validate', MockRequest.middleware(), (c) => {
|
|
83
|
+
return c.json({ ok: true })
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const res = await app.request('/validate', {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({ name: 'Carl', age: 25 }),
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
expect(res.status).toBe(200)
|
|
93
|
+
expect(await res.json()).toEqual({ ok: true })
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('should return 422 on validation failure with Laravel-style format', async () => {
|
|
97
|
+
app.post('/validate', MockRequest.middleware(), (c) => c.json({ ok: true }))
|
|
98
|
+
|
|
99
|
+
const res = await app.request('/validate', {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: { 'Content-Type': 'application/json' },
|
|
102
|
+
body: JSON.stringify({ name: 'Ca', age: 'invalid' }),
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
expect(res.status).toBe(422)
|
|
106
|
+
const data: any = await res.json()
|
|
107
|
+
console.log('DEBUG ERRORS:', JSON.stringify(data.errors))
|
|
108
|
+
expect(data.message).toBe('The given data was invalid.')
|
|
109
|
+
expect(data.errors).toBeDefined()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should return 403 on authorization failure', async () => {
|
|
113
|
+
app.post('/validate', MockRequest.middleware(), (c) => c.json({ ok: true }))
|
|
114
|
+
|
|
115
|
+
const res = await app.request('/validate', {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: {
|
|
118
|
+
'Content-Type': 'application/json',
|
|
119
|
+
'x-fail-auth': 'yes',
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify({ name: 'Carl', age: 25 }),
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
expect(res.status).toBe(403)
|
|
125
|
+
expect(await res.json()).toEqual({ message: 'This action is unauthorized.' })
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should trim strings and convert empty strings to null in validated data', async () => {
|
|
129
|
+
app.post('/clean', MockRequest.middleware(), async (c) => {
|
|
130
|
+
const req = new MockRequest()
|
|
131
|
+
req.setContext(c)
|
|
132
|
+
return c.json(req.validated())
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const res = await app.request('/clean', {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: { 'Content-Type': 'application/json' },
|
|
138
|
+
body: JSON.stringify({
|
|
139
|
+
name: ' Carl ', // Should be trimmed
|
|
140
|
+
age: 25,
|
|
141
|
+
extra: '', // Should become null if it were in schema, but MockRequest only has name and age
|
|
142
|
+
}),
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
expect(res.status).toBe(200)
|
|
146
|
+
const data: any = await res.json()
|
|
147
|
+
expect(data.name).toBe('Carl')
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
})
|
package/tsconfig.json
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
"compilerOptions": {
|
|
4
4
|
"outDir": "./dist",
|
|
5
5
|
"baseUrl": ".",
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"types": ["bun-types"],
|
|
6
8
|
"paths": {
|
|
7
|
-
"gravito
|
|
9
|
+
"@gravito/core": ["../../packages/core/src/index.ts"],
|
|
8
10
|
"@gravito/*": ["../../packages/*/src/index.ts"]
|
|
9
11
|
}
|
|
10
12
|
},
|