rajt 0.0.17 → 0.0.18
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/package.json +8 -5
- package/src/action.ts +29 -11
- package/src/auth/ability.ts +51 -0
- package/src/auth/auth.ts +72 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/token.ts +80 -0
- package/src/auth/types.ts +2 -0
- package/src/create-app.ts +10 -4
- package/src/dev.ts +6 -1
- package/src/dynamodb/client.ts +13 -10
- package/src/dynamodb/compact.ts +3 -3
- package/src/dynamodb/model.ts +116 -87
- package/src/dynamodb/query-builder.ts +16 -16
- package/src/http.ts +46 -19
- package/src/middleware.ts +1 -1
- package/src/prod.ts +7 -3
- package/src/register.ts +6 -0
- package/src/response.ts +11 -11
- package/src/routes.ts +17 -12
- package/src/scripts/cache-routes.ts +8 -1
- package/src/utils/json-import.ts +16 -0
- package/src/utils/merge-middleware.ts +9 -0
- package/src/utils/resolve.ts +23 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rajt",
|
|
3
3
|
"description": "A serverless bundler layer, fully typed for AWS Lambda (Node.js and LLRT) and Cloudflare Workers.",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.18",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"local": "bun run --silent build && bun run --silent sam:local",
|
|
18
18
|
"build": "bun run --silent cache:routes && bun run --silent export && bun run --silent clean:temp",
|
|
19
19
|
"build:watch": "chokidar \"../../{actions,configs,models,utils}/**/*.ts\" -c \"bun run --silent build\" --initial",
|
|
20
|
-
"export": "esbuild --bundle --minify --outfile=../../dist/index.js --platform=node --target=node20 --format=esm --tree-shaking=true --legal-comments=none src/prod.ts",
|
|
20
|
+
"export": "esbuild --bundle --minify --outfile=../../dist/index.js --platform=node --target=node20 --format=esm --tree-shaking=true --legal-comments=none --external:@aws-sdk --external:@smithy src/prod.ts",
|
|
21
21
|
"package": "bun run --silent build && bun run --silent sam:package",
|
|
22
22
|
"deploy": "bun run --silent build && bun run --silent sam:package && bun run --silent sam:deploy",
|
|
23
23
|
"update": "bun run --silent build && bun run --silent zip && bun run --silent sam:update",
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
"start": "node ../../dist/index.js"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@aws-
|
|
38
|
-
"@aws-
|
|
39
|
-
"@aws-lite/dynamodb-types": "^0.3.11",
|
|
37
|
+
"@aws-sdk/client-dynamodb": "3.817.0",
|
|
38
|
+
"@aws-sdk/lib-dynamodb": "3.817.0",
|
|
40
39
|
"@hono/node-server": "^1.14.1",
|
|
41
40
|
"@hono/zod-validator": "^0.4.3",
|
|
42
41
|
"@types/node": "^20.11.0",
|
|
43
42
|
"chokidar-cli": "^3.0.0",
|
|
43
|
+
"cripta": "^0.1.6",
|
|
44
44
|
"dotenv": "^16.5.0",
|
|
45
45
|
"esbuild": "^0.25.2",
|
|
46
46
|
"hono": "^4.7.6",
|
|
@@ -49,6 +49,9 @@
|
|
|
49
49
|
"tsx": "^4.19.3",
|
|
50
50
|
"typescript": "^5.8.3"
|
|
51
51
|
},
|
|
52
|
+
"resolutions": {
|
|
53
|
+
"@smithy/types": "^4.3.0"
|
|
54
|
+
},
|
|
52
55
|
"publishConfig": {
|
|
53
56
|
"registry": "https://registry.npmjs.org"
|
|
54
57
|
},
|
package/src/action.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { Context, Handler, HonoRequest, MiddlewareHandler, Next, ValidationTarge
|
|
|
2
2
|
import { z, ZodObject, ZodRawShape } from 'zod'
|
|
3
3
|
import { zValidator } from '@hono/zod-validator'
|
|
4
4
|
// import { JSONValue } from 'hono/utils/types'
|
|
5
|
-
import json from './response'
|
|
6
5
|
import JsonResponse from './response'
|
|
7
6
|
import { bufferToFormData } from 'hono/utils/buffer'
|
|
8
7
|
import { HTTPException } from 'hono/http-exception'
|
|
@@ -26,7 +25,7 @@ export default abstract class Action {
|
|
|
26
25
|
rule<T extends keyof ValidationTargets>(target: T): { schema: (schema: ZodObject<any>) => RuleDefinition }
|
|
27
26
|
rule<T extends keyof ValidationTargets>(target: T, schema: ZodObject<any>): RuleDefinition
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
rule<T extends keyof ValidationTargets>(target: T, schema?: ZodObject<any>):
|
|
30
29
|
| { schema: (schema: ZodObject<any>) => RuleDefinition }
|
|
31
30
|
| RuleDefinition
|
|
32
31
|
{
|
|
@@ -47,15 +46,15 @@ export default abstract class Action {
|
|
|
47
46
|
}
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
|
|
49
|
+
param(key: string) {
|
|
51
50
|
return this.context.req.param(key)
|
|
52
51
|
}
|
|
53
52
|
|
|
54
|
-
|
|
53
|
+
query() {
|
|
55
54
|
return this.context.req.query()
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
|
|
57
|
+
async form(cType?: string) {
|
|
59
58
|
cType ??= this.context.req.header('Content-Type')
|
|
60
59
|
if (!cType) return {}
|
|
61
60
|
|
|
@@ -92,7 +91,7 @@ export default abstract class Action {
|
|
|
92
91
|
return form
|
|
93
92
|
}
|
|
94
93
|
|
|
95
|
-
|
|
94
|
+
async json<E>() {
|
|
96
95
|
try {
|
|
97
96
|
return await this.context.req.json<E>()
|
|
98
97
|
} catch {
|
|
@@ -100,7 +99,7 @@ export default abstract class Action {
|
|
|
100
99
|
}
|
|
101
100
|
}
|
|
102
101
|
|
|
103
|
-
|
|
102
|
+
async body<E>() {
|
|
104
103
|
const cType = this.context.req.header('Content-Type')
|
|
105
104
|
if (!cType) return {}
|
|
106
105
|
|
|
@@ -118,11 +117,11 @@ export default abstract class Action {
|
|
|
118
117
|
return {}
|
|
119
118
|
}
|
|
120
119
|
|
|
121
|
-
|
|
120
|
+
get response() {
|
|
122
121
|
return this.context ? JsonResponse.setContext(this.context) : JsonResponse
|
|
123
122
|
}
|
|
124
123
|
|
|
125
|
-
|
|
124
|
+
validate() {
|
|
126
125
|
const rules = this.rules()
|
|
127
126
|
const h = async (c: Context) => {
|
|
128
127
|
this.context = c
|
|
@@ -134,7 +133,7 @@ export default abstract class Action {
|
|
|
134
133
|
.map(rule => zValidator(rule.target, rule.schema, (result, c) => {
|
|
135
134
|
if (!result.success) {
|
|
136
135
|
// @ts-ignore
|
|
137
|
-
return
|
|
136
|
+
return JsonResponse.badRequest({ ...result.error.flatten()[rule.eTarget] })
|
|
138
137
|
}
|
|
139
138
|
}))
|
|
140
139
|
|
|
@@ -143,7 +142,26 @@ export default abstract class Action {
|
|
|
143
142
|
return rulesArray
|
|
144
143
|
}
|
|
145
144
|
|
|
146
|
-
|
|
145
|
+
get auth() {
|
|
146
|
+
const auth = this.context.get('#auth')
|
|
147
|
+
return auth ? auth?.data : null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
can(...abilities: string[]): boolean {
|
|
151
|
+
const auth = this.context.get('#auth')
|
|
152
|
+
return auth ? auth.can(...abilities) : false
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
cant(...abilities: string[]): boolean {
|
|
156
|
+
return !this.can(...abilities)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
hasRole(...roles: string[]): boolean {
|
|
160
|
+
const auth = this.context.get('#auth')
|
|
161
|
+
return auth ? auth.hasRole(...roles) : false
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
run() {
|
|
147
165
|
return this.validate()
|
|
148
166
|
}
|
|
149
167
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Routes } from '../types'
|
|
2
|
+
import { Roles, Abilities } from './types'
|
|
3
|
+
|
|
4
|
+
export class Ability {
|
|
5
|
+
static #roles: Roles = {}
|
|
6
|
+
static #abilities: Abilities = []
|
|
7
|
+
|
|
8
|
+
static empty() {
|
|
9
|
+
this.#roles = {}
|
|
10
|
+
this.#abilities = []
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static fromRoutes(actions: Routes) {
|
|
14
|
+
if (!actions?.length) return
|
|
15
|
+
|
|
16
|
+
const paths = actions?.map(a => Array.isArray(a) ? a[1] : a.path) ?? []
|
|
17
|
+
const items = new Set(paths)
|
|
18
|
+
|
|
19
|
+
if (items.size !== actions.length)
|
|
20
|
+
throw new Error(`Duplicate routes detected: "${paths.filter((path, index) => paths.indexOf(path) !== index).join('", "')}"`)
|
|
21
|
+
|
|
22
|
+
this.#abilities = Array.from(items).map(a => this.format(a)).filter(Boolean)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static fromAction(target: any): string | null {
|
|
26
|
+
return !target || !target?.p ? null : this.format(target.p)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static format(path: string) {
|
|
30
|
+
return path.normalize('NFD')
|
|
31
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
32
|
+
.replace(/^\/*/, '')
|
|
33
|
+
.replace(/[^a-zA-Z0-9/]|[\s_.]/g, '-')
|
|
34
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
35
|
+
.replace(/\//g, '.')
|
|
36
|
+
.replace(/-+/g, '-')
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static get abilities() {
|
|
41
|
+
return this.#abilities
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static get roles() {
|
|
45
|
+
return this.#roles
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static set roles(roles: Roles) {
|
|
49
|
+
this.#roles = roles
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/auth/auth.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Ability } from './ability'
|
|
2
|
+
|
|
3
|
+
type Authenticatable = {
|
|
4
|
+
role?: string,
|
|
5
|
+
roles?: string[],
|
|
6
|
+
perms?: string[],
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class Authnz<T extends object> {
|
|
10
|
+
#abilities: string[]
|
|
11
|
+
#roles: string[]
|
|
12
|
+
#data: T
|
|
13
|
+
|
|
14
|
+
constructor(data: T, abilities: string[], roles: string[]) {
|
|
15
|
+
this.#abilities = abilities
|
|
16
|
+
this.#roles = roles
|
|
17
|
+
this.#data = data
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
can(...abilities: string[]): boolean {
|
|
21
|
+
if (this.#abilities.includes('*')) return true
|
|
22
|
+
|
|
23
|
+
return abilities.flat().every(ability => {
|
|
24
|
+
if (this.#roles.includes(ability)) return true
|
|
25
|
+
return this.#abilities.some(rule => this.#match(rule, ability))
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
cant(...abilities: string[]): boolean {
|
|
29
|
+
return !this.can(...abilities)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
hasRole(...roles: string[]): boolean {
|
|
33
|
+
return roles.flat().every(role => this.#roles.includes(role))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static fromToken<T extends object>(user: any): Authnz<T> | null {
|
|
37
|
+
if (!user || user?.isInvalid()) return null
|
|
38
|
+
user = user.get()
|
|
39
|
+
const roles = [...(user?.role ? [user.role] : []), ...(user?.roles ?? [])]
|
|
40
|
+
|
|
41
|
+
const combined = [...(user?.perms ?? []), ...roles.flatMap(role => {
|
|
42
|
+
const perms = Ability.roles[role]
|
|
43
|
+
if (!perms) return []
|
|
44
|
+
return perms === '*' ? ['*'] : perms;
|
|
45
|
+
})]
|
|
46
|
+
|
|
47
|
+
const abilities = combined.includes('*') ? ['*'] : Array.from(new Set(combined))
|
|
48
|
+
|
|
49
|
+
return new Authnz(user as T, abilities, roles)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#match(rule: string, ability: string): boolean {
|
|
53
|
+
if (rule === ability) return true
|
|
54
|
+
if (rule.endsWith('.*')) {
|
|
55
|
+
const prefix = rule.slice(0, -2)
|
|
56
|
+
return ability.startsWith(`${prefix}.`) || ability === prefix
|
|
57
|
+
}
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get abilities() {
|
|
62
|
+
return this.#abilities
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get roles() {
|
|
66
|
+
return this.#roles
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get data() {
|
|
70
|
+
return this.#data
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Envir } from 't0n'
|
|
2
|
+
import { Token as Factory } from 'cripta'
|
|
3
|
+
import type { Context, HonoRequest, Next } from 'hono'
|
|
4
|
+
|
|
5
|
+
export class Token {
|
|
6
|
+
static #name: string = 'Authorization'
|
|
7
|
+
static #prefix: string = 'bearer'
|
|
8
|
+
|
|
9
|
+
static fromRequest(c: Context) {
|
|
10
|
+
// const token = this.fromHeader(c.req)
|
|
11
|
+
// Mock user
|
|
12
|
+
const token = this.create(c.req, {
|
|
13
|
+
name: 'Nicolau',
|
|
14
|
+
email: 'nicolau@zunq.com',
|
|
15
|
+
role: 'user',
|
|
16
|
+
}).get()
|
|
17
|
+
|
|
18
|
+
return token ? this.parse(c.req, token) : null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static fromHeader(req: HonoRequest): string | null {
|
|
22
|
+
const header = req.header(this.#name) || req.header('HTTP_AUTHORIZATION') || req.header('REDIRECT_HTTP_AUTHORIZATION') || null
|
|
23
|
+
|
|
24
|
+
if (header) {
|
|
25
|
+
const position = header.toLowerCase().indexOf(this.#prefix.toLowerCase())
|
|
26
|
+
if (position !== -1) {
|
|
27
|
+
let token = header.slice(position + this.#prefix.length).trim()
|
|
28
|
+
const commaPos = token.indexOf(',')
|
|
29
|
+
if (commaPos !== -1) token = token.slice(0, commaPos).trim()
|
|
30
|
+
|
|
31
|
+
return token
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static parse(req: HonoRequest, token: string) {
|
|
39
|
+
const host = this.host(Envir.get('FLOW_SERVER') || req.header('host') || '')
|
|
40
|
+
|
|
41
|
+
return Factory.parse(token)
|
|
42
|
+
.issuedBy(host)
|
|
43
|
+
.permittedFor(host)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static create(req: HonoRequest, user: any, exp: number = 7200) {
|
|
47
|
+
const time = Math.floor(Date.now() / 1000)
|
|
48
|
+
const host = this.host(req.header('host') || '')
|
|
49
|
+
|
|
50
|
+
return Factory.create()
|
|
51
|
+
.issuedBy(host)
|
|
52
|
+
.permittedFor(host)
|
|
53
|
+
.issuedAt(time)
|
|
54
|
+
.expiresAt(time + exp)
|
|
55
|
+
.body(user)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static setPrefix(prefix: string): void {
|
|
59
|
+
this.#prefix = prefix
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static setName(name: string): void {
|
|
63
|
+
this.#name = name
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static host(url: string | null | undefined): string {
|
|
67
|
+
if (!url) return ''
|
|
68
|
+
|
|
69
|
+
let formattedUrl = String(url)
|
|
70
|
+
if (!formattedUrl.startsWith('http'))
|
|
71
|
+
formattedUrl = 'http://' + formattedUrl
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const parsedUrl = new URL(formattedUrl)
|
|
75
|
+
return parsedUrl.host
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return ''
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/create-app.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { Hono } from 'hono'
|
|
3
3
|
import type { Env, Context, ErrorHandler, NotFoundHandler, Next } from 'hono'
|
|
4
|
-
// import type {
|
|
4
|
+
// import type { MiddlewareHandler } from 'hono'
|
|
5
5
|
// import { createMiddleware } from 'hono/factory'
|
|
6
6
|
// import type { H, Handler, HandlerResponse } from 'hono/types'
|
|
7
7
|
import { HTTPResponseError } from 'hono/types'
|
|
8
8
|
import { Routes } from './types'
|
|
9
9
|
import { BadRequest, Unauthorized } from './exceptions'
|
|
10
10
|
import response from './response'
|
|
11
|
-
import resolve from './utils/resolve'
|
|
12
|
-
import { getHandler } from './register'
|
|
11
|
+
import { resolve, resolveMiddleware } from './utils/resolve'
|
|
12
|
+
import { getGlobalMiddlewares, getHandler } from './register'
|
|
13
13
|
import env from './utils/environment'
|
|
14
14
|
|
|
15
15
|
type InitFunction<E extends Env = Env> = (app: Hono<E>) => void
|
|
@@ -90,8 +90,14 @@ export const createApp = <E extends Env>(options?: ServerOptions<E>) => {
|
|
|
90
90
|
response.setContext(c)
|
|
91
91
|
await next()
|
|
92
92
|
},
|
|
93
|
+
...getGlobalMiddlewares()
|
|
93
94
|
]
|
|
94
|
-
middlewares.forEach(
|
|
95
|
+
middlewares.forEach(mw => {
|
|
96
|
+
// @ts-ignore
|
|
97
|
+
const h = resolveMiddleware(mw)
|
|
98
|
+
// @ts-ignore
|
|
99
|
+
mw?.p ? app.use(String(mw.p), h) : app.use(h)
|
|
100
|
+
})
|
|
95
101
|
|
|
96
102
|
app.onError(options?.onError || EHandler)
|
|
97
103
|
app.notFound(options?.notFound || NFHandler)
|
package/src/dev.ts
CHANGED
|
@@ -2,18 +2,23 @@ import { config } from 'dotenv'
|
|
|
2
2
|
import { serve } from '@hono/node-server'
|
|
3
3
|
import createApp from './create-app'
|
|
4
4
|
import getRoutes from './routes'
|
|
5
|
+
import { Ability } from './auth'
|
|
5
6
|
import { getAvailablePort } from './utils/port'
|
|
7
|
+
import jsonImport from './utils/json-import'
|
|
6
8
|
|
|
7
9
|
config({ path: '../../.env.dev' })
|
|
8
10
|
|
|
9
11
|
const routes = await getRoutes()
|
|
12
|
+
Ability.fromRoutes(routes)
|
|
13
|
+
Ability.roles = jsonImport('../../../../.rolefile')
|
|
14
|
+
|
|
10
15
|
const fetch = createApp({ routes }).fetch
|
|
11
16
|
|
|
12
17
|
const desiredPort = process.env?.PORT ? Number(process.env.PORT) : 3000
|
|
13
18
|
getAvailablePort(desiredPort)
|
|
14
19
|
.then(port => {
|
|
15
20
|
if (port != desiredPort)
|
|
16
|
-
console.
|
|
21
|
+
console.warn(`🟨 Port ${desiredPort} was in use, using ${port} as a fallback`)
|
|
17
22
|
|
|
18
23
|
console.log(`🚀 API running on http://localhost:${port}`)
|
|
19
24
|
serve({ fetch, port })
|
package/src/dynamodb/client.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
|
|
2
|
+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'
|
|
3
3
|
import AbstractModel from './model'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
5
|
+
const client = new DynamoDBClient(process.env?.AWS_SAM_LOCAL ? {
|
|
6
|
+
region: process.env.AWS_REGION || "us-east-1",
|
|
7
|
+
endpoint: process.env.AWS_ENDPOINT_URL || undefined,
|
|
8
|
+
credentials: {
|
|
9
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID || "DUMMYIDEXAMPLE",
|
|
10
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "DUMMYEXAMPLEKEY",
|
|
11
|
+
},
|
|
12
|
+
} : {})
|
|
13
|
+
|
|
14
|
+
export const ddb = DynamoDBDocumentClient.from(client)
|
|
12
15
|
|
|
13
16
|
export class Dynamodb {
|
|
14
17
|
static model<T extends object>(cls: new (...args: any[]) => T) {
|
|
15
|
-
return new AbstractModel<T>(cls,
|
|
18
|
+
return new AbstractModel<T>(cls, ddb)
|
|
16
19
|
}
|
|
17
20
|
}
|
package/src/dynamodb/compact.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { SchemaStructure } from './types'
|
|
|
2
2
|
import getLength from '../utils/lenght'
|
|
3
3
|
|
|
4
4
|
export default class Compact {
|
|
5
|
-
|
|
5
|
+
static #typeMap: Record<string, string> = {
|
|
6
6
|
// Null
|
|
7
7
|
'null,': 'N,',
|
|
8
8
|
',null': ',N',
|
|
@@ -33,7 +33,7 @@ export default class Compact {
|
|
|
33
33
|
.replace(/'/g, '"')
|
|
34
34
|
.replace(/~TDQ~/g, "'")
|
|
35
35
|
.replace(/\\'/g, "^'"),
|
|
36
|
-
this
|
|
36
|
+
this.#typeMap
|
|
37
37
|
)
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -49,7 +49,7 @@ export default class Compact {
|
|
|
49
49
|
static decode<T = any>(val: string, schema: SchemaStructure): T {
|
|
50
50
|
if (!val) return val as T
|
|
51
51
|
|
|
52
|
-
val = this.replaceTypes(val, this.reverseMap(this
|
|
52
|
+
val = this.replaceTypes(val, this.reverseMap(this.#typeMap))
|
|
53
53
|
.replace(/"/g, '~TSQ~')
|
|
54
54
|
.replace(/'/g, '"')
|
|
55
55
|
.replace(/~TSQ~/g, "'")
|
package/src/dynamodb/model.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
DynamoDBDocumentClient,
|
|
3
|
+
BatchGetCommand,
|
|
4
|
+
BatchWriteCommand,
|
|
5
|
+
DeleteCommand,
|
|
6
|
+
GetCommand,
|
|
7
|
+
PutCommand,
|
|
8
|
+
QueryCommand,
|
|
9
|
+
ScanCommand,
|
|
10
|
+
UpdateCommand,
|
|
11
|
+
} from '@aws-sdk/lib-dynamodb'
|
|
2
12
|
import type { ModelMetadata, Keys, Model, Filter } from './types'
|
|
3
13
|
import { getModelMetadata } from './decorators'
|
|
4
14
|
import QueryBuilder from './query-builder'
|
|
@@ -6,18 +16,25 @@ import Compact from './compact'
|
|
|
6
16
|
import getLength from '../utils/lenght'
|
|
7
17
|
|
|
8
18
|
export default class AbstractModel<T extends object> {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
19
|
+
#meta: ModelMetadata
|
|
20
|
+
#cls!: Model<T>
|
|
21
|
+
lastKey?: Record<string, any>
|
|
22
|
+
#db: DynamoDBDocumentClient
|
|
23
|
+
#queryBuilder?: QueryBuilder
|
|
24
|
+
#model?: AbstractModel<T>
|
|
12
25
|
|
|
13
26
|
constructor(
|
|
14
27
|
cls: Model<T> | ModelMetadata,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
28
|
+
db: DynamoDBDocumentClient,
|
|
29
|
+
queryBuilder?: QueryBuilder,
|
|
30
|
+
model?: AbstractModel<T>
|
|
18
31
|
) {
|
|
32
|
+
this.#db = db
|
|
33
|
+
this.#queryBuilder = queryBuilder
|
|
34
|
+
this.#model = model
|
|
35
|
+
|
|
19
36
|
if (typeof (cls as ModelMetadata).table === 'string') {
|
|
20
|
-
this
|
|
37
|
+
this.#meta = cls as ModelMetadata
|
|
21
38
|
return
|
|
22
39
|
}
|
|
23
40
|
|
|
@@ -25,21 +42,21 @@ export default class AbstractModel<T extends object> {
|
|
|
25
42
|
if (!meta)
|
|
26
43
|
throw new Error('Missing model metadata')
|
|
27
44
|
|
|
28
|
-
this
|
|
29
|
-
this
|
|
45
|
+
this.#meta = meta
|
|
46
|
+
this.#cls = cls as Model<T>
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
get table(): string {
|
|
33
|
-
return this
|
|
50
|
+
return this.#meta.table
|
|
34
51
|
}
|
|
35
52
|
|
|
36
53
|
get keySchema() {
|
|
37
|
-
return this
|
|
54
|
+
return this.#meta.keys
|
|
38
55
|
}
|
|
39
56
|
|
|
40
57
|
set lastEvaluatedKey(val: Record<string, any> | undefined) {
|
|
41
|
-
if (this
|
|
42
|
-
this
|
|
58
|
+
if (this.#model) {
|
|
59
|
+
this.#model.lastKey = val
|
|
43
60
|
} else {
|
|
44
61
|
this.lastKey = val
|
|
45
62
|
}
|
|
@@ -51,52 +68,61 @@ export default class AbstractModel<T extends object> {
|
|
|
51
68
|
where(builderFn: (q: QueryBuilder) => void) {
|
|
52
69
|
const qb = new QueryBuilder()
|
|
53
70
|
builderFn(qb)
|
|
54
|
-
return new AbstractModel<T>(this
|
|
71
|
+
return new AbstractModel<T>(this.#meta, this.#db, qb, this)
|
|
55
72
|
}
|
|
56
73
|
|
|
57
74
|
async scan(filterFn?: Filter<T>) {
|
|
58
|
-
const result = await this
|
|
75
|
+
const result = await this.#db.send(new ScanCommand({
|
|
76
|
+
TableName: this.table,
|
|
77
|
+
...this.#queryBuilder?.filters,
|
|
78
|
+
}))
|
|
59
79
|
|
|
60
80
|
this.lastEvaluatedKey = result.LastEvaluatedKey
|
|
61
|
-
return this
|
|
81
|
+
return this.#processItems(result.Items, filterFn)
|
|
62
82
|
}
|
|
63
83
|
|
|
64
84
|
async query(filterFn?: Filter<T>) {
|
|
65
|
-
const result = await this
|
|
85
|
+
const result = await this.#db.send(new QueryCommand({
|
|
86
|
+
TableName: this.table,
|
|
87
|
+
...this.#queryBuilder?.conditions,
|
|
88
|
+
}))
|
|
66
89
|
|
|
67
90
|
this.lastEvaluatedKey = result.LastEvaluatedKey
|
|
68
|
-
return this
|
|
91
|
+
return this.#processItems(result.Items, filterFn)
|
|
69
92
|
}
|
|
70
93
|
|
|
71
94
|
async get(key: Keys, sk?: string) {
|
|
72
|
-
const result = await this
|
|
73
|
-
|
|
95
|
+
const result = await this.#db.send(new GetCommand({
|
|
96
|
+
TableName: this.table,
|
|
97
|
+
Key: this.#key(key, sk),
|
|
98
|
+
}))
|
|
99
|
+
return result.Item ? this.#processItem(result.Item) : undefined
|
|
74
100
|
}
|
|
75
101
|
|
|
76
102
|
async put(item: Partial<T>, key: Keys) {
|
|
77
103
|
let keys
|
|
78
|
-
if (this
|
|
79
|
-
keys = this
|
|
80
|
-
this
|
|
104
|
+
if (this.#meta.zip) {
|
|
105
|
+
keys = this.#getItemKey(item, key)
|
|
106
|
+
this.#validateKeys(keys)
|
|
81
107
|
// @ts-ignore
|
|
82
|
-
item = { ...keys, V: Compact.encode(this.getItemWithoutKeys(item), this.meta.fields)}
|
|
108
|
+
item = { ...keys, V: Compact.encode(this.getItemWithoutKeys(item), this.meta.fields) }
|
|
83
109
|
} else {
|
|
84
|
-
this
|
|
110
|
+
this.#validateKeys(item)
|
|
85
111
|
}
|
|
86
112
|
|
|
87
|
-
await this
|
|
88
|
-
return this
|
|
113
|
+
await this.#db.send(new PutCommand({ TableName: this.table, Item: item }))
|
|
114
|
+
return this.#processItem(item, keys)
|
|
89
115
|
}
|
|
90
116
|
|
|
91
117
|
async update(attrs: Partial<T>, key: Keys) {
|
|
92
118
|
let keys
|
|
93
|
-
if (this
|
|
94
|
-
keys = this
|
|
95
|
-
this
|
|
119
|
+
if (this.#meta.zip) {
|
|
120
|
+
keys = this.#getItemKey(attrs, key)
|
|
121
|
+
this.#validateKeys(keys)
|
|
96
122
|
// @ts-ignore
|
|
97
|
-
attrs = { V: Compact.encode(this.getItemWithoutKeys(attrs), this.meta.fields)}
|
|
123
|
+
attrs = { V: Compact.encode(this.getItemWithoutKeys(attrs), this.meta.fields) }
|
|
98
124
|
} else {
|
|
99
|
-
this
|
|
125
|
+
this.#validateKeys(attrs)
|
|
100
126
|
}
|
|
101
127
|
|
|
102
128
|
const UpdateExpressionParts: string[] = []
|
|
@@ -108,26 +134,29 @@ export default class AbstractModel<T extends object> {
|
|
|
108
134
|
const UpdateExpression = 'SET ' + UpdateExpressionParts.join(', ')
|
|
109
135
|
const ExpressionAttributeNames = Object.fromEntries(Object.keys(attrs).map(k => [`#${k}`, k]))
|
|
110
136
|
|
|
111
|
-
await this
|
|
137
|
+
await this.#db.send(new UpdateCommand({
|
|
112
138
|
TableName: this.table,
|
|
113
|
-
Key: this
|
|
139
|
+
Key: this.#key(key),
|
|
114
140
|
UpdateExpression,
|
|
115
141
|
ExpressionAttributeValues,
|
|
116
|
-
ExpressionAttributeNames
|
|
117
|
-
})
|
|
142
|
+
ExpressionAttributeNames,
|
|
143
|
+
}))
|
|
118
144
|
|
|
119
|
-
return this
|
|
145
|
+
return this.#processItem(attrs, keys)
|
|
120
146
|
}
|
|
121
147
|
|
|
122
148
|
async delete(key: Keys, sk?: string) {
|
|
123
|
-
return this
|
|
149
|
+
return this.#db.send(new DeleteCommand({
|
|
150
|
+
TableName: this.table,
|
|
151
|
+
Key: this.#key(key, sk),
|
|
152
|
+
}))
|
|
124
153
|
}
|
|
125
154
|
|
|
126
155
|
async batchGet(keys: Array<Keys>) {
|
|
127
|
-
const result = await this
|
|
128
|
-
[this.table]: { Keys: keys.map(key => this
|
|
129
|
-
}
|
|
130
|
-
return (result.Responses?.[this.table] as T[] || []).map(item => this
|
|
156
|
+
const result = await this.#db.send(new BatchGetCommand({
|
|
157
|
+
RequestItems: { [this.table]: { Keys: keys.map(key => this.#key(key)) } },
|
|
158
|
+
}))
|
|
159
|
+
return (result.Responses?.[this.table] as T[] || []).map(item => this.#processItem(item))
|
|
131
160
|
}
|
|
132
161
|
|
|
133
162
|
async batchWrite(items: Array<{ put?: Partial<T>, delete?: Keys }>) {
|
|
@@ -135,12 +164,12 @@ export default class AbstractModel<T extends object> {
|
|
|
135
164
|
if (i.put) {
|
|
136
165
|
return { PutRequest: { Item: i.put } }
|
|
137
166
|
} else if (i.delete) {
|
|
138
|
-
return { DeleteRequest: { Key: this
|
|
167
|
+
return { DeleteRequest: { Key: this.#key(i.delete) } }
|
|
139
168
|
}
|
|
140
169
|
return null
|
|
141
|
-
}).filter(Boolean)
|
|
170
|
+
}).filter(Boolean) as any[]
|
|
142
171
|
|
|
143
|
-
return this
|
|
172
|
+
return this.#db.send(new BatchWriteCommand({ RequestItems: { [this.table]: WriteRequests } }))
|
|
144
173
|
}
|
|
145
174
|
|
|
146
175
|
async deleteMany(keys: Array<Keys>) {
|
|
@@ -151,8 +180,8 @@ export default class AbstractModel<T extends object> {
|
|
|
151
180
|
return this.batchWrite(items.map(item => ({ put: item })))
|
|
152
181
|
}
|
|
153
182
|
|
|
154
|
-
|
|
155
|
-
if (!this
|
|
183
|
+
#key(key: Keys, sk?: string) {
|
|
184
|
+
if (!this.#meta.keys) return {}
|
|
156
185
|
|
|
157
186
|
let pk: string
|
|
158
187
|
let skValue: string | undefined
|
|
@@ -164,100 +193,100 @@ export default class AbstractModel<T extends object> {
|
|
|
164
193
|
skValue = sk
|
|
165
194
|
}
|
|
166
195
|
|
|
167
|
-
const keys = { [this
|
|
196
|
+
const keys = { [this.#meta.keys.PK]: pk }
|
|
168
197
|
|
|
169
|
-
if (this
|
|
198
|
+
if (this.#meta.keys?.SK) {
|
|
170
199
|
if (skValue) {
|
|
171
|
-
keys[this
|
|
172
|
-
} else if (this
|
|
173
|
-
keys[this
|
|
200
|
+
keys[this.#meta.keys.SK] = skValue
|
|
201
|
+
} else if (this.#meta.defaultSK) {
|
|
202
|
+
keys[this.#meta.keys.SK] = this.#meta.defaultSK
|
|
174
203
|
}
|
|
175
204
|
}
|
|
176
205
|
|
|
177
206
|
return keys
|
|
178
207
|
}
|
|
179
208
|
|
|
180
|
-
|
|
181
|
-
if (!this
|
|
209
|
+
#getItemKey(item: Partial<T>, key?: Keys): Record<string, string> {
|
|
210
|
+
if (!this.#meta.keys) return {}
|
|
182
211
|
|
|
183
212
|
const keys: Record<string, string> = {}
|
|
184
213
|
if (key)
|
|
185
|
-
this
|
|
214
|
+
this.#processExplicitKey(keys, key)
|
|
186
215
|
else if (getLength(item) > 0)
|
|
187
|
-
this
|
|
216
|
+
this.#processItemKeys(keys, item)
|
|
188
217
|
|
|
189
218
|
return keys
|
|
190
219
|
}
|
|
191
220
|
|
|
192
|
-
|
|
193
|
-
if (!this
|
|
221
|
+
#processExplicitKey(keys: Record<string, string>, key: Keys): void {
|
|
222
|
+
if (!this.#meta.keys) return
|
|
194
223
|
if (Array.isArray(key)) {
|
|
195
|
-
keys[this
|
|
224
|
+
keys[this.#meta.keys.PK] = key[0]
|
|
196
225
|
|
|
197
|
-
if (this
|
|
226
|
+
if (this.#meta.keys?.SK) {
|
|
198
227
|
if (key.length > 1)
|
|
199
228
|
// @ts-ignore
|
|
200
229
|
keys[this.meta.keys.SK] = key[1]
|
|
201
|
-
else if (this
|
|
202
|
-
keys[this
|
|
230
|
+
else if (this.#meta.defaultSK)
|
|
231
|
+
keys[this.#meta.keys.SK] = this.#meta.defaultSK
|
|
203
232
|
}
|
|
204
233
|
} else {
|
|
205
|
-
keys[this
|
|
234
|
+
keys[this.#meta.keys.PK] = String(key)
|
|
206
235
|
}
|
|
207
236
|
}
|
|
208
237
|
|
|
209
|
-
|
|
210
|
-
if (!this
|
|
238
|
+
#processItemKeys(keys: Record<string, string>, item: Partial<T>): void {
|
|
239
|
+
if (!this.#meta.keys) return
|
|
211
240
|
|
|
212
|
-
const pkValue = item[this
|
|
241
|
+
const pkValue = item[this.#meta.keys.PK as keyof Partial<T>]
|
|
213
242
|
if (pkValue !== undefined)
|
|
214
|
-
keys[this
|
|
243
|
+
keys[this.#meta.keys.PK] = String(pkValue)
|
|
215
244
|
|
|
216
|
-
if (this
|
|
217
|
-
const skValue = item[this
|
|
245
|
+
if (this.#meta.keys?.SK) {
|
|
246
|
+
const skValue = item[this.#meta.keys.SK as keyof Partial<T>]
|
|
218
247
|
if (skValue !== undefined)
|
|
219
|
-
keys[this
|
|
220
|
-
else if (this
|
|
221
|
-
keys[this
|
|
248
|
+
keys[this.#meta.keys.SK] = String(skValue)
|
|
249
|
+
else if (this.#meta.defaultSK)
|
|
250
|
+
keys[this.#meta.keys.SK] = this.#meta.defaultSK
|
|
222
251
|
}
|
|
223
252
|
}
|
|
224
253
|
|
|
225
|
-
|
|
226
|
-
if (!this
|
|
254
|
+
#validateKeys(keys: Record<string, any>) {
|
|
255
|
+
if (!this.#meta.keys)
|
|
227
256
|
throw new Error(`Missing keys of table "${this.table}"`)
|
|
228
257
|
|
|
229
|
-
if (!(this
|
|
258
|
+
if (!(this.#meta.keys.PK in keys))
|
|
230
259
|
throw new Error(`Missing partition key of table "${this.table}" `)
|
|
231
260
|
|
|
232
|
-
if (this
|
|
261
|
+
if (this.#meta.keys?.SK && !(this.#meta.keys.SK in keys))
|
|
233
262
|
throw new Error(`Missing sort key of table "${this.table}"`)
|
|
234
263
|
}
|
|
235
264
|
|
|
236
|
-
|
|
237
|
-
if (!this
|
|
265
|
+
#getItemWithoutKeys(item: Partial<T>): Partial<T> {
|
|
266
|
+
if (!this.#meta.keys || !item) return { ...item }
|
|
238
267
|
|
|
239
|
-
const { PK, SK } = this
|
|
268
|
+
const { PK, SK } = this.#meta.keys
|
|
240
269
|
const { [PK as keyof T]: _, [SK as keyof T]: __, ...rest } = item
|
|
241
270
|
|
|
242
271
|
return rest as Partial<T>
|
|
243
272
|
}
|
|
244
273
|
|
|
245
|
-
|
|
274
|
+
#processItems(items: any[] | undefined, filterFn?: Filter<T>): T[] {
|
|
246
275
|
if (!items) return []
|
|
247
276
|
|
|
248
|
-
items = this
|
|
277
|
+
items = this.#meta.zip ? Compact.smartDecode<T[]>(items, this.#meta.fields) : items as T[]
|
|
249
278
|
return filterFn ? items.filter(filterFn) : items
|
|
250
279
|
}
|
|
251
280
|
|
|
252
|
-
|
|
253
|
-
if (this
|
|
254
|
-
const model = new this
|
|
255
|
-
if (!keys) keys = this
|
|
281
|
+
#processItem(item: any, keys?: Record<string, string>): T {
|
|
282
|
+
if (this.#meta.zip && item?.V) {
|
|
283
|
+
const model = new this.#cls(Compact.decode(item.V, this.#meta.fields))
|
|
284
|
+
if (!keys) keys = this.#getItemKey(item)
|
|
256
285
|
|
|
257
286
|
// @ts-ignore
|
|
258
287
|
return model.withKey(keys[this.meta.keys.PK], keys[this.meta.keys.SK] || undefined)
|
|
259
288
|
}
|
|
260
289
|
|
|
261
|
-
return new this
|
|
290
|
+
return new this.#cls(item)
|
|
262
291
|
}
|
|
263
292
|
}
|
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
import type { Condition, Operator } from './types'
|
|
2
2
|
|
|
3
3
|
export default class QueryBuilder {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
#conditions: Condition[] = []
|
|
5
|
+
#limit?: number
|
|
6
|
+
#startKey?: Record<string, any>
|
|
7
|
+
#index?: string
|
|
8
8
|
|
|
9
9
|
filter(field: string, operator: Operator, value: any = null) {
|
|
10
|
-
this.
|
|
10
|
+
this.#conditions.push({ type: 'filter', field, operator, value })
|
|
11
11
|
return this
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
keyCondition(field: string, operator: Operator | any, value?: any) {
|
|
15
15
|
const noVal = value === undefined
|
|
16
|
-
this.
|
|
16
|
+
this.#conditions.push({ type: 'keyCondition', field, operator: noVal ? '=' : operator, value: noVal ? operator : value })
|
|
17
17
|
return this
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
limit(n: number) {
|
|
21
|
-
this
|
|
21
|
+
this.#limit = n
|
|
22
22
|
return this
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
exclusiveStartKey(key: Record<string, any>) {
|
|
26
|
-
this
|
|
26
|
+
this.#startKey = key
|
|
27
27
|
return this
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
index(name: string) {
|
|
31
|
-
this
|
|
31
|
+
this.#index = name
|
|
32
32
|
return this
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -38,7 +38,7 @@ export default class QueryBuilder {
|
|
|
38
38
|
const names: Record<string, string> = {}
|
|
39
39
|
|
|
40
40
|
let i = 0
|
|
41
|
-
for (const cond of this.
|
|
41
|
+
for (const cond of this.#conditions.filter(c => c.type === type)) {
|
|
42
42
|
const attr = `#attr${i}`
|
|
43
43
|
const val = `:val${i}`
|
|
44
44
|
names[attr] = cond.field
|
|
@@ -107,11 +107,11 @@ export default class QueryBuilder {
|
|
|
107
107
|
const filter = this.buildExpression('filter')
|
|
108
108
|
const params: any = {}
|
|
109
109
|
|
|
110
|
-
if (this
|
|
111
|
-
params.Limit = this
|
|
110
|
+
if (this.#limit)
|
|
111
|
+
params.Limit = this.#limit
|
|
112
112
|
|
|
113
|
-
if (this
|
|
114
|
-
params.ExclusiveStartKey = this
|
|
113
|
+
if (this.#startKey)
|
|
114
|
+
params.ExclusiveStartKey = this.#startKey
|
|
115
115
|
|
|
116
116
|
if (filter.expression)
|
|
117
117
|
params.FilterExpression = filter.expression
|
|
@@ -131,8 +131,8 @@ export default class QueryBuilder {
|
|
|
131
131
|
|
|
132
132
|
const params: any = { ...filters }
|
|
133
133
|
|
|
134
|
-
if (this
|
|
135
|
-
params.IndexName = this
|
|
134
|
+
if (this.#index)
|
|
135
|
+
params.IndexName = this.#index
|
|
136
136
|
|
|
137
137
|
if (keys.expression)
|
|
138
138
|
params.KeyConditionExpression = keys.expression
|
package/src/http.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import
|
|
1
|
+
import type { Context, Next } from 'hono'
|
|
2
|
+
import { MiddlewareType } from './middleware'
|
|
3
|
+
import JsonResponse from './response'
|
|
4
|
+
import { Ability, Authnz, Token } from './auth'
|
|
5
|
+
import { registerGlobalMiddleware } from './register'
|
|
6
|
+
import mergeMiddleware from './utils/merge-middleware'
|
|
3
7
|
|
|
4
8
|
function method(method: string, path = '/') {
|
|
5
9
|
return function (target: any) {
|
|
@@ -31,27 +35,50 @@ export function Delete(path = '/') {
|
|
|
31
35
|
|
|
32
36
|
export function Middleware(...handlers: MiddlewareType[]) {
|
|
33
37
|
return function (target: any) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
mergeMiddleware(target, ...handlers)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function Middlewares(...handlers: MiddlewareType[]) {
|
|
42
|
+
return Middleware(...handlers)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type MiddlewareOpt = string | RegExp
|
|
46
|
+
export function GlobalMiddleware(): ClassDecorator
|
|
47
|
+
export function GlobalMiddleware(target: Function): void
|
|
48
|
+
export function GlobalMiddleware(opt?: MiddlewareOpt): ClassDecorator
|
|
49
|
+
export function GlobalMiddleware(...args: any[]): void | ClassDecorator {
|
|
50
|
+
if (typeof args[0] === 'function')
|
|
51
|
+
return _globalmw(args[0])
|
|
52
|
+
|
|
53
|
+
return (target: any) => _globalmw(target, ...args)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function Auth(target: Function): void
|
|
57
|
+
export function Auth(): ClassDecorator
|
|
58
|
+
export function Auth(...args: any[]): void | ClassDecorator {
|
|
59
|
+
if (args.length === 1 && typeof args[0] === 'function')
|
|
60
|
+
return _auth(args[0])
|
|
37
61
|
|
|
38
|
-
|
|
39
|
-
|
|
62
|
+
return (target: any) => _auth(target)
|
|
63
|
+
}
|
|
40
64
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
65
|
+
function _auth(target: Function | any) {
|
|
66
|
+
mergeMiddleware(target, async (c: Context, next: Next) => {
|
|
67
|
+
const unauthorized = JsonResponse.unauthorized()
|
|
45
68
|
|
|
46
|
-
|
|
47
|
-
|
|
69
|
+
const auth = Authnz.fromToken(Token.fromRequest(c))
|
|
70
|
+
const ability = Ability.fromAction(target)
|
|
48
71
|
|
|
49
|
-
|
|
50
|
-
|
|
72
|
+
if (!auth || !ability || auth.cant(ability))
|
|
73
|
+
return unauthorized
|
|
51
74
|
|
|
52
|
-
|
|
53
|
-
|
|
75
|
+
c.set('#auth', auth)
|
|
76
|
+
await next()
|
|
77
|
+
})
|
|
54
78
|
}
|
|
55
|
-
|
|
56
|
-
|
|
79
|
+
|
|
80
|
+
function _globalmw(target: Function | any, path?: string) {
|
|
81
|
+
target.gmw = true
|
|
82
|
+
target.p = path
|
|
83
|
+
registerGlobalMiddleware(target)
|
|
57
84
|
}
|
package/src/middleware.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type IMiddleware = {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export default abstract class Middleware implements IMiddleware {
|
|
8
|
-
|
|
8
|
+
abstract handle(c: Context, next: Next): Promise<void> | void
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
// export type MiddlewareHandler = (c: Context, next: Next) => Promise<void> | void
|
package/src/prod.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import { config } from 'dotenv'
|
|
2
1
|
import { handle } from 'hono/aws-lambda'
|
|
3
2
|
import createApp from './create-app'
|
|
4
|
-
|
|
5
|
-
config({ path: '../../.env.prod' })
|
|
3
|
+
import { Ability } from './auth'
|
|
6
4
|
|
|
7
5
|
// @ts-ignore
|
|
8
6
|
await import('../../../tmp/import-routes.mjs')
|
|
9
7
|
|
|
10
8
|
// @ts-ignore
|
|
11
9
|
const routes = (await import('../../../tmp/routes.json')).default
|
|
10
|
+
|
|
11
|
+
// @ts-ignore
|
|
12
|
+
Ability.roles = (await import('../../../roles.json')).default
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
Ability.fromRoutes(routes)
|
|
15
|
+
|
|
12
16
|
// @ts-ignore
|
|
13
17
|
const app = createApp({ routes })
|
|
14
18
|
|
package/src/register.ts
CHANGED
|
@@ -23,3 +23,9 @@ export function getHandler(id: string): Function {
|
|
|
23
23
|
if (!handler) throw new Error(`Handler ${id} not registered`)
|
|
24
24
|
return handler
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
export const _mw: Function[] = []
|
|
28
|
+
export const getGlobalMiddlewares = () => _mw
|
|
29
|
+
export function registerGlobalMiddleware(handler: any) {
|
|
30
|
+
_mw.push(handler)
|
|
31
|
+
}
|
package/src/response.ts
CHANGED
|
@@ -25,19 +25,19 @@ class NullContext {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export default class JsonResponse {
|
|
28
|
-
|
|
28
|
+
static #c?: Context
|
|
29
29
|
|
|
30
30
|
static setContext(c: Context) {
|
|
31
|
-
this
|
|
31
|
+
this.#c = c
|
|
32
32
|
return this
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
return this
|
|
35
|
+
static #context(): Context | NullContext {
|
|
36
|
+
return this.#c ?? new NullContext()
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
static raw(status?: StatusCode, body?: string) {
|
|
40
|
-
return this
|
|
40
|
+
return this.#context().newResponse(body ? body : null, { status })
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
static ok(): Response
|
|
@@ -46,7 +46,7 @@ export default class JsonResponse {
|
|
|
46
46
|
if (data === undefined)
|
|
47
47
|
return this.raw(200)
|
|
48
48
|
|
|
49
|
-
return this
|
|
49
|
+
return this.#context().json(data, 200)
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
static created(): Response
|
|
@@ -55,7 +55,7 @@ export default class JsonResponse {
|
|
|
55
55
|
if (data === undefined)
|
|
56
56
|
return this.raw(201)
|
|
57
57
|
|
|
58
|
-
return this
|
|
58
|
+
return this.#context().json(data, 201)
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
static accepted(): Response
|
|
@@ -64,7 +64,7 @@ export default class JsonResponse {
|
|
|
64
64
|
if (data === undefined)
|
|
65
65
|
return this.raw(202)
|
|
66
66
|
|
|
67
|
-
return this
|
|
67
|
+
return this.#context().json(data, 202)
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
static deleted(): Response
|
|
@@ -88,7 +88,7 @@ export default class JsonResponse {
|
|
|
88
88
|
if (data === undefined)
|
|
89
89
|
return this.raw(401)
|
|
90
90
|
|
|
91
|
-
return this
|
|
91
|
+
return this.#context().json(data, 401)
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
static forbidden(): Response
|
|
@@ -97,7 +97,7 @@ export default class JsonResponse {
|
|
|
97
97
|
if (data === undefined)
|
|
98
98
|
return this.raw(403)
|
|
99
99
|
|
|
100
|
-
return this
|
|
100
|
+
return this.#context().json(data, 403)
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
static notFound(): Response
|
|
@@ -122,7 +122,7 @@ export default class JsonResponse {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
static error(errors?: Errors, msg?: string, status?: ContentfulStatusCode) {
|
|
125
|
-
const context = this
|
|
125
|
+
const context = this.#context()
|
|
126
126
|
status ??= 500
|
|
127
127
|
|
|
128
128
|
if (!errors && !msg)
|
package/src/routes.ts
CHANGED
|
@@ -1,27 +1,31 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
2
|
+
import { dirname, join, resolve } from 'node:path'
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import { Route } from './types'
|
|
5
5
|
import { registerHandler } from './register'
|
|
6
6
|
import { isAnonFn } from './utils/func'
|
|
7
7
|
|
|
8
8
|
const __filename = fileURLToPath(import.meta.url)
|
|
9
|
-
const __dirname =
|
|
9
|
+
const __dirname = dirname(__filename)
|
|
10
10
|
|
|
11
|
-
export default async function getRoutes(
|
|
11
|
+
export default async function getRoutes(
|
|
12
|
+
all: boolean = false,
|
|
13
|
+
dirs: string[] = ['actions', 'features', 'errors', 'middlewares']
|
|
14
|
+
): Promise<Route[]> {
|
|
12
15
|
const routes: Route[] = []
|
|
13
16
|
|
|
14
|
-
const walk = async (dir: string, middlewares: Function[] = []): Promise<void> => {
|
|
15
|
-
|
|
17
|
+
const walk = async (dir: string, baseDir: string, middlewares: Function[] = []): Promise<void> => {
|
|
18
|
+
if (!existsSync(dir)) return
|
|
19
|
+
const files = readdirSync(dir)
|
|
16
20
|
|
|
17
21
|
for (const file of files) {
|
|
18
|
-
const fullPath =
|
|
19
|
-
const stat =
|
|
22
|
+
const fullPath = join(dir, file)
|
|
23
|
+
const stat = statSync(fullPath)
|
|
20
24
|
|
|
21
25
|
if (stat.isDirectory()) {
|
|
22
|
-
const indexFile =
|
|
26
|
+
const indexFile = join(fullPath, 'index.ts')
|
|
23
27
|
|
|
24
|
-
if (
|
|
28
|
+
if (existsSync(indexFile)) {
|
|
25
29
|
const mod = await import(indexFile)
|
|
26
30
|
const group = mod.default
|
|
27
31
|
registerHandler(group.name, group)
|
|
@@ -37,11 +41,12 @@ export default async function getRoutes(all: boolean = false, baseDir: string =
|
|
|
37
41
|
})
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
await walk(fullPath, middlewares)
|
|
44
|
+
await walk(fullPath, baseDir, middlewares)
|
|
41
45
|
} else if (file.endsWith('.ts')) {
|
|
42
46
|
const mod = await import(fullPath)
|
|
43
47
|
const handle = mod.default
|
|
44
48
|
|
|
49
|
+
if (handle?.gmw) return
|
|
45
50
|
if (handle?.m) {
|
|
46
51
|
registerHandler(handle.name, handle)
|
|
47
52
|
|
|
@@ -58,6 +63,6 @@ export default async function getRoutes(all: boolean = false, baseDir: string =
|
|
|
58
63
|
}
|
|
59
64
|
}
|
|
60
65
|
|
|
61
|
-
await walk(
|
|
66
|
+
await Promise.all(dirs.map(dir => walk(resolve(__dirname, '../../..', dir), dir)))
|
|
62
67
|
return routes
|
|
63
68
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { writeFileSync } from 'node:fs'
|
|
1
|
+
import { existsSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { config } from 'dotenv'
|
|
2
3
|
import getRoutes from '../routes'
|
|
3
4
|
import ensureDir from '../utils/ensuredir'
|
|
4
5
|
|
|
6
|
+
config({ path: '../../.env.dev' })
|
|
7
|
+
|
|
5
8
|
async function cacheRoutes() {
|
|
9
|
+
const rolePath = '../../roles.json'
|
|
10
|
+
if (!existsSync(rolePath))
|
|
11
|
+
writeFileSync(rolePath, '{}')
|
|
12
|
+
|
|
6
13
|
const routes = await getRoutes(true)
|
|
7
14
|
|
|
8
15
|
const iPath = '../../tmp/import-routes.mjs'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
export default function jsonImport<T = any>(filePath: string, defaultValue: T = {} as T): T {
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
7
|
+
const __dirname = dirname(__filename)
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const fullPath = join(__dirname, filePath)
|
|
11
|
+
const fileContent = readFileSync(fullPath, 'utf-8')
|
|
12
|
+
return JSON.parse(fileContent) as T
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return defaultValue
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono'
|
|
2
|
+
import { MiddlewareType } from '../middleware'
|
|
3
|
+
import { resolveMiddleware } from './resolve'
|
|
4
|
+
|
|
5
|
+
export default function mergeMiddleware(target: Function | any, ...handlers: MiddlewareType[]) {
|
|
6
|
+
const existingMiddlewares: MiddlewareHandler[] = target?.mw || []
|
|
7
|
+
const allMiddlewares = [...existingMiddlewares, ...handlers.flat().map(handler => resolveMiddleware(handler))]
|
|
8
|
+
target.mw = allMiddlewares
|
|
9
|
+
}
|
package/src/utils/resolve.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Action, { ActionType } from '../action'
|
|
2
|
+
import { MiddlewareType } from '../middleware'
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
+
export function resolve(obj: ActionType) {
|
|
4
5
|
if (typeof obj === 'function' && obj?.length === 2)
|
|
5
6
|
return [obj]
|
|
6
7
|
|
|
@@ -16,3 +17,24 @@ export default function resolve(obj: ActionType) {
|
|
|
16
17
|
|
|
17
18
|
throw new Error('Invalid action')
|
|
18
19
|
}
|
|
20
|
+
|
|
21
|
+
export function resolveMiddleware(obj: MiddlewareType) {
|
|
22
|
+
if (typeof obj === 'function' && obj.length === 2)
|
|
23
|
+
return obj
|
|
24
|
+
|
|
25
|
+
if (obj?.handle)
|
|
26
|
+
return obj.handle
|
|
27
|
+
|
|
28
|
+
if (obj.prototype?.handle)
|
|
29
|
+
return (new obj()).handle
|
|
30
|
+
|
|
31
|
+
// if (obj instanceof BaseMiddleware)
|
|
32
|
+
// return obj.handle
|
|
33
|
+
|
|
34
|
+
// if (BaseMiddleware.isPrototypeOf(obj)) {
|
|
35
|
+
// const instance = new (obj as new () => BaseMiddleware)()
|
|
36
|
+
// return instance.handle
|
|
37
|
+
// }
|
|
38
|
+
|
|
39
|
+
throw new Error('Invalid middleware provided. Must be a Hono middleware function or MiddlewareClass instance/constructor')
|
|
40
|
+
}
|