@stacksjs/router 0.66.0 → 0.68.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/src/index.ts DELETED
@@ -1,5 +0,0 @@
1
- export * from './middleware'
2
- export * from './request'
3
- export * from './router'
4
- export * from './server'
5
- export * from './utils'
package/src/middleware.ts DELETED
@@ -1,35 +0,0 @@
1
- import type { MiddlewareOptions } from '@stacksjs/types'
2
- import { userMiddlewarePath } from '@stacksjs/path'
3
-
4
- export class Middleware implements MiddlewareOptions {
5
- name: string
6
- priority: number
7
- handle: () => Promise<void>
8
-
9
- constructor(data: MiddlewareOptions) {
10
- this.name = data.name
11
- this.priority = data.priority
12
- this.handle = data.handle
13
- }
14
- }
15
-
16
- // const readdir = promisify(fs.readdir)
17
-
18
- async function importMiddlewares(directory: string): Promise<string[]> {
19
- // const middlewares = []
20
- // TODO: somehow this breaks ./buddy dev
21
- // const files = await readdir(directory)
22
-
23
- // for (const file of files) {
24
- // // Dynamically import the middleware
25
- // const imported = await import(path.join(directory, file))
26
- // middlewares.push(imported.default)
27
- // }
28
-
29
- // return middlewares
30
- return [directory] // fix this: return array of middlewares
31
- }
32
-
33
- export async function middlewares(): Promise<string[]> {
34
- return await importMiddlewares(userMiddlewarePath())
35
- }
package/src/request.ts DELETED
@@ -1,112 +0,0 @@
1
- import type { RequestInstance, RouteParam, VineType } from '@stacksjs/types'
2
-
3
- import { customValidate, validateField } from '@stacksjs/validation'
4
-
5
- interface RequestData {
6
- [key: string]: any
7
- }
8
-
9
- interface ValidationField {
10
- rule: VineType
11
- message: Record<string, string>
12
- }
13
-
14
- type AuthToken = `${number}:${number}:${string}`
15
-
16
- interface CustomAttributes {
17
- [key: string]: ValidationField
18
- }
19
-
20
- type RouteParams = { [key: string]: string | number } | null
21
-
22
- export class Request<T extends RequestData = RequestData> implements RequestInstance {
23
- public query: T = {} as T
24
- public params: RouteParams = null
25
- public headers: any = {}
26
-
27
- public addQuery(url: URL): void {
28
- this.query = Object.fromEntries(url.searchParams) as unknown as T
29
- }
30
-
31
- public addBodies(params: any): void {
32
- this.query = params
33
- }
34
-
35
- public addParam(param: RouteParam): void {
36
- this.params = param
37
- }
38
-
39
- public addHeaders(headerParams: Headers): void {
40
- this.headers = headerParams
41
- }
42
-
43
- public get(element: string): any {
44
- return this.query[element]
45
- }
46
-
47
- public all(): T {
48
- return this.query
49
- }
50
-
51
- public async validate(attributes?: CustomAttributes): Promise<void> {
52
- if (attributes === undefined || attributes === null) {
53
- await validateField('Release', this.all())
54
- }
55
- else {
56
- await customValidate(attributes, this.all())
57
- }
58
- }
59
-
60
- public has(element: string): boolean {
61
- return element in this.query
62
- }
63
-
64
- public isEmpty(): boolean {
65
- return Object.keys(this.query).length === 0
66
- }
67
-
68
- public extractParamsFromRoute(routePattern: string, pathname: string): void {
69
- const pattern = new RegExp(`^${routePattern.replace(/:(\w+)/g, (match, paramName) => `(?<${paramName}>\\w+)`)}$`)
70
- const match = pattern.exec(pathname)
71
-
72
- if (match?.groups)
73
- this.params = match.groups
74
- }
75
-
76
- public header(headerParam: string): string | number | boolean | null {
77
- return this.headers.get(headerParam)
78
- }
79
-
80
- public getHeaders(): any {
81
- return this.headers
82
- }
83
-
84
- public Header(headerParam: string): string | number | boolean | null {
85
- return this.headers.get(headerParam)
86
- }
87
-
88
- public getParam(key: string): number | string | null {
89
- return this.params ? this.params[key] || null : null
90
- }
91
-
92
- public bearerToken(): string | null | AuthToken {
93
- const authorizationHeader = this.headers.get('authorization')
94
-
95
- if (authorizationHeader?.startsWith('Bearer ')) {
96
- return authorizationHeader.substring(7)
97
- }
98
-
99
- return null
100
- }
101
-
102
- public getParams(): RouteParams {
103
- return this.params
104
- }
105
-
106
- public getParamAsInt(key: string): number | null {
107
- const value = this.getParam(key)
108
- return value ? Number.parseInt(value.toString()) : null
109
- }
110
- }
111
-
112
- export const request: Request = new Request()
package/src/router.ts DELETED
@@ -1,358 +0,0 @@
1
- import type { Action } from '@stacksjs/actions'
2
- import type { Job, RedirectCode, RequestInstance, Route, RouteGroupOptions, RouterInterface, StatusCode } from '@stacksjs/types'
3
- import { handleError } from '@stacksjs/error-handling'
4
- import { log } from '@stacksjs/logging'
5
- import { path as p } from '@stacksjs/path'
6
- import { kebabCase, pascalCase } from '@stacksjs/strings'
7
- import { customValidate, isObjectNotEmpty } from '@stacksjs/validation'
8
- import { extractDefaultRequest, findRequestInstance } from './utils'
9
-
10
- type ActionPath = string // TODO: narrow this by automating its generation
11
-
12
- export class Router implements RouterInterface {
13
- private routes: Route[] = []
14
- private groupPrefix = ''
15
- private path = ''
16
-
17
- private addRoute(
18
- method: Route['method'],
19
- uri: string,
20
- callback: Route['callback'] | string | object,
21
- statusCode: StatusCode,
22
- ): this {
23
- const name = uri.replace(/\//g, '.').replace(/:/g, '') // we can improve this
24
- const pattern = new RegExp(
25
- `^${uri.replace(/:[a-z]+/gi, (_match) => {
26
- return '([a-zA-Z0-9-]+)'
27
- })}$`,
28
- )
29
-
30
- // let routeCallback: Route['callback']
31
-
32
- // if (typeof callback === 'string' || typeof callback === 'object') {
33
- // // Convert string or object to RouteCallback
34
- // routeCallback = () => callback
35
- // } else {
36
- // routeCallback = callback
37
- // }
38
-
39
- log.debug(`Adding route: ${method} ${uri} with name ${name}`)
40
-
41
- this.routes.push({
42
- name,
43
- method,
44
- url: uri,
45
- uri,
46
- callback,
47
- pattern,
48
- statusCode,
49
- paramNames: [],
50
- // middleware: [],
51
- })
52
-
53
- return this
54
- }
55
-
56
- public get(path: Route['url'], callback: Route['callback']): this {
57
- this.path = this.normalizePath(path)
58
- log.debug(`Normalized Path: ${this.path}`)
59
-
60
- const uri = this.prepareUri(this.path)
61
- log.debug(`Prepared URI: ${uri}`)
62
-
63
- return this.addRoute('GET', uri, callback, 200)
64
- }
65
-
66
- public async email(path: Route['url']): Promise<this> {
67
- path = pascalCase(path)
68
-
69
- const emailModule = (await import(p.userNotificationsPath(path))).default as Action
70
- const callback = emailModule.handle
71
- const uri = this.prepareUri(path)
72
- this.addRoute('GET', uri, callback, 200)
73
-
74
- return this
75
- }
76
-
77
- public async health(): Promise<this> {
78
- const healthModule = (await import(p.userActionsPath('HealthAction'))).default as Action
79
- const callback = healthModule.handle
80
- const path = healthModule.path ?? `/health`
81
-
82
- this.addRoute('GET', path, callback, 200)
83
-
84
- return this
85
- }
86
-
87
- public async job(path: Route['url']): Promise<this> {
88
- path = pascalCase(path)
89
-
90
- // removes the potential `JobJob` suffix in case the user does not choose to use the Job suffix in their file name
91
- const job = (await import(p.userJobsPath(`${path}.ts`))).default as Job
92
-
93
- return this.addRoute('GET', this.prepareUri(path), job.handle, 200)
94
- }
95
-
96
- public async action(path: ActionPath | Route['path']): Promise<this> {
97
- if (!path)
98
- return this
99
-
100
- // check if action is a file anywhere in ./app/Actions/**/*.ts
101
- if (path?.endsWith('.ts')) {
102
- // given it ends with .ts, we treat it as an Actions path
103
- const action = (await import(p.userActionsPath(path))).default as Action
104
- path = action.path ?? kebabCase(path as string)
105
- return this.addRoute(action.method ?? 'GET', path, action.handle, 200)
106
- }
107
-
108
- path = pascalCase(path) // actions are PascalCase
109
-
110
- try {
111
- const action = (await import(p.userActionsPath(path))).default as Action
112
-
113
- return this.addRoute(action.method ?? 'GET', this.prepareUri(path), action.handle, 200)
114
- }
115
- catch (error) {
116
- handleError(`Could not find Action for path: ${path}`, error)
117
-
118
- return this
119
- }
120
- }
121
-
122
- public post(path: Route['url'], callback: Route['callback']): this {
123
- this.path = this.normalizePath(path)
124
-
125
- const uri = this.prepareUri(this.path)
126
-
127
- return this.addRoute('POST', uri, callback, 201)
128
- }
129
-
130
- public view(path: Route['url'], callback: Route['callback']): this {
131
- this.path = this.normalizePath(path)
132
-
133
- const uri = this.prepareUri(this.path)
134
-
135
- return this.addRoute('GET', uri, callback, 200)
136
- }
137
-
138
- public redirect(path: Route['url'], callback: Route['callback'], _status?: RedirectCode): this {
139
- return this.addRoute('GET', path, callback, 302)
140
- }
141
-
142
- public delete(path: Route['url'], callback: Route['callback']): this {
143
- return this.addRoute('DELETE', this.prepareUri(path), callback, 204)
144
- }
145
-
146
- public patch(path: Route['url'], callback: Route['callback']): this {
147
- this.path = this.normalizePath(path)
148
- log.debug(`Normalized Path: ${this.path}`)
149
-
150
- const uri = this.prepareUri(this.path)
151
- log.debug(`Prepared URI: ${uri}`)
152
-
153
- return this.addRoute('PATCH', uri, callback, 202)
154
- }
155
-
156
- public put(path: Route['url'], callback: Route['callback']): this {
157
- this.path = this.normalizePath(path)
158
-
159
- const uri = this.prepareUri(this.path)
160
-
161
- return this.addRoute('PUT', uri, callback, 202)
162
- }
163
-
164
- public group(options: string | RouteGroupOptions, callback?: () => void): this {
165
- if (typeof options === 'string')
166
- options = options.startsWith('/') ? options.slice(1) : options
167
-
168
- let cb: () => void
169
-
170
- this.prepareGroupPrefix(options)
171
-
172
- if (typeof options === 'function') {
173
- cb = options
174
- options = {}
175
- }
176
-
177
- if (!callback)
178
- throw new Error('Missing callback function for your route group.')
179
-
180
- cb = callback
181
-
182
- const { prefix, middleware = [] } = options as RouteGroupOptions
183
-
184
- // Save a reference to the original routes array
185
- const originalRoutes = this.routes
186
-
187
- // Create a new routes array for the duration of the callback
188
- this.routes = []
189
-
190
- // Execute the callback. This will add routes to the new this.routes array
191
- cb()
192
-
193
- // For each route added by the callback, adjust the URI and add to the original routes array
194
- this.routes.forEach((r) => {
195
- r.uri = `${prefix}${r.uri}`
196
-
197
- if (middleware.length)
198
- r.middleware = middleware
199
-
200
- originalRoutes.push(r)
201
- return this
202
- })
203
-
204
- // Restore the original routes array.
205
- this.routes = originalRoutes
206
-
207
- return this
208
- }
209
-
210
- public name(name: string): this {
211
- this.routes[this.routes.length - 1].name = name
212
-
213
- return this
214
- }
215
-
216
- public middleware(middleware: Route['middleware']): this {
217
- this.routes[this.routes.length - 1].middleware = middleware
218
-
219
- return this
220
- }
221
-
222
- public prefix(prefix: string): this {
223
- this.routes[this.routes.length - 1].prefix = prefix
224
-
225
- return this
226
- }
227
-
228
- public async getRoutes(): Promise<Route[]> {
229
- await import('../../../../../routes/api') // user routes
230
- await import('../../../orm/routes') // auto-generated routes
231
-
232
- return this.routes
233
- }
234
-
235
- private setGroupPrefix(prefix: string, options: RouteGroupOptions = {}) {
236
- if (prefix !== '') {
237
- prefix = `/${this.groupPrefix}/${prefix}`.replace(/\/\//g, '/') // remove double slashes in case there are any
238
- this.groupPrefix = prefix
239
- return
240
- }
241
-
242
- // Ensure options is always treated as an object, even if it's undefined or a function
243
- const effectiveOptions = typeof options === 'object' ? options : {}
244
-
245
- this.groupPrefix = effectiveOptions.prefix ?? prefix ?? ''
246
- }
247
-
248
- private prepareGroupPrefix(options: string | RouteGroupOptions): void {
249
- if (this.groupPrefix !== '' && typeof options !== 'string') {
250
- this.setGroupPrefix(this.groupPrefix, options)
251
- return
252
- }
253
-
254
- if (typeof options === 'string') {
255
- this.setGroupPrefix(options)
256
- return
257
- }
258
-
259
- this.setGroupPrefix('', options)
260
- }
261
-
262
- public async resolveCallback(callback: Route['callback']): Promise<Route['callback']> {
263
- if (callback instanceof Promise) {
264
- const actionModule = await callback
265
- return actionModule.default
266
- }
267
-
268
- if (typeof callback === 'string')
269
- return await this.importCallbackFromPath(callback, this.path)
270
-
271
- // in this case, the callback ends up being a function
272
- return callback
273
- }
274
-
275
- public async importCallbackFromPath(callbackPath: string, originalPath: string): Promise<Route['callback']> {
276
- let modulePath = callbackPath
277
- let importPathFunction = p.appPath // Default import path function
278
-
279
- if (callbackPath.startsWith('../'))
280
- importPathFunction = p.routesPath
281
- if (modulePath.includes('OrmAction'))
282
- importPathFunction = p.storagePath
283
-
284
- // Remove trailing .ts if present
285
- modulePath = modulePath.endsWith('.ts') ? modulePath.slice(0, -3) : modulePath
286
-
287
- let actionModule = null
288
-
289
- if (modulePath.includes('storage/framework/orm'))
290
- actionModule = await import(modulePath)
291
- else if (modulePath.includes('app/Actions'))
292
- actionModule = await import(modulePath)
293
- else if (modulePath.includes('OrmAction'))
294
- actionModule = await import(p.storagePath(`/framework/actions/src/${modulePath}.ts`))
295
- else actionModule = await import(importPathFunction(modulePath))
296
-
297
- // Use custom path from action module if available
298
- const newPath = actionModule.default.path ?? originalPath
299
- this.updatePathIfNeeded(newPath, originalPath)
300
-
301
- // we need to make sure the validation happens here
302
- // to do so, we need to:
303
- // find the ./app/Models/* file
304
- // then check via a regex which model attributes validations to utilize by checking what's in between t
305
- // then validate
306
- // if succeeds, run the handle
307
- // if fails, return validation error
308
- let requestInstance: RequestInstance
309
-
310
- if (actionModule.default.requestFile) {
311
- requestInstance = await findRequestInstance(actionModule.default.requestFile)
312
- }
313
- else {
314
- requestInstance = await extractDefaultRequest()
315
- }
316
-
317
- try {
318
- if (isObjectNotEmpty(actionModule.default.validations) && requestInstance)
319
- await customValidate(actionModule.default.validations, requestInstance.all())
320
-
321
- return await actionModule.default.handle(requestInstance)
322
- }
323
- catch (error: any) {
324
- if (error.status === 422)
325
- return { status: 422, errors: JSON.parse(error.message) }
326
-
327
- if (!error.status)
328
- return { status: 500, errors: error.message }
329
-
330
- return { status: error.status, errors: error.message }
331
- }
332
- }
333
-
334
- private normalizePath(path: string): string {
335
- return path.endsWith('/') ? path.slice(0, -1) : path
336
- }
337
-
338
- public prepareUri(path: string): string {
339
- // if string starts with / then remove it because we are adding it back in the next line
340
- if (path.startsWith('/'))
341
- path = path.slice(1)
342
-
343
- path = `${this.groupPrefix}/${path}`
344
-
345
- // if path ends in "/", then remove it
346
- // e.g. triggered when route is "/"
347
- return path.endsWith('/') ? path.slice(0, -1) : path
348
- }
349
-
350
- private updatePathIfNeeded(newPath: string, originalPath: string): void {
351
- if (newPath !== originalPath) {
352
- // Logic to update the path if needed, based on the action module's custom path
353
- this.path = newPath
354
- }
355
- }
356
- }
357
-
358
- export const route: Router = new Router()