@stacksjs/router 0.63.1 → 0.64.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/router",
3
3
  "type": "module",
4
- "version": "0.63.1",
4
+ "version": "0.64.0",
5
5
  "description": "The Stacks framework router.",
6
6
  "author": "Chris Breuer",
7
7
  "license": "MIT",
package/src/middleware.ts CHANGED
@@ -30,4 +30,6 @@ async function importMiddlewares(directory: string) {
30
30
  return [directory] // fix this: return array of middlewares
31
31
  }
32
32
 
33
- export const middlewares = await importMiddlewares(userMiddlewarePath())
33
+ export const middlewares = async () => {
34
+ return await importMiddlewares(userMiddlewarePath())
35
+ }
package/src/request.ts CHANGED
@@ -4,41 +4,27 @@ import type { VineType } from '@stacksjs/types'
4
4
  import { customValidate, validateField } from '@stacksjs/validation'
5
5
 
6
6
  interface RequestData {
7
- [key: string]: string
8
- }
9
-
10
- interface ValidationType {
11
- rule: VineType
12
- message: { [key: string]: string }
7
+ [key: string]: any
13
8
  }
14
9
 
15
10
  interface ValidationField {
16
- [key: string]: string | ValidationType
17
- validation: ValidationType
11
+ rule: VineType
12
+ message: Record<string, string>
18
13
  }
19
14
 
20
15
  interface CustomAttributes {
21
16
  [key: string]: ValidationField
22
17
  }
23
18
 
24
- type RouteParams = { [key: string]: string } | null
19
+ type RouteParams = { [key: string]: string | number } | null
25
20
 
26
- export class Request implements RequestInstance {
27
- private static instance: Request
28
- private query: RequestData = {}
29
- private params: RouteParams = null
30
- private headers: any = {}
31
-
32
- // An attempt to singleston instance, might be needed at some point
33
- public static getInstance(): Request {
34
- if (!Request.instance) {
35
- Request.instance = new Request()
36
- }
37
- return Request.instance
38
- }
21
+ export class Request<T extends RequestData = RequestData> implements RequestInstance {
22
+ public query: T = {} as T
23
+ public params: RouteParams = null
24
+ public headers: any = {}
39
25
 
40
26
  public addQuery(url: URL): void {
41
- this.query = Object.fromEntries(url.searchParams)
27
+ this.query = Object.fromEntries(url.searchParams) as unknown as T
42
28
  }
43
29
 
44
30
  public addBodies(params: any): void {
@@ -57,7 +43,7 @@ export class Request implements RequestInstance {
57
43
  return this.query[element]
58
44
  }
59
45
 
60
- public all(): RequestData {
46
+ public all(): T {
61
47
  return this.query
62
48
  }
63
49
 
@@ -115,8 +101,8 @@ export class Request implements RequestInstance {
115
101
  }
116
102
 
117
103
  public getParamAsInt(key: string): number | null {
118
- const value = this.params ? this.params[key] || null : null
119
- return value ? Number.parseInt(value) : null
104
+ const value = this.getParam(key)
105
+ return value ? Number.parseInt(value.toString()) : null
120
106
  }
121
107
  }
122
108
 
package/src/router.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import type { Action } from '@stacksjs/actions'
2
2
  import { log } from '@stacksjs/logging'
3
- import { path as p, projectStoragePath, routesPath } from '@stacksjs/path'
3
+ import { path as p } from '@stacksjs/path'
4
4
  import { kebabCase, pascalCase } from '@stacksjs/strings'
5
5
  import type { Job } from '@stacksjs/types'
6
6
  import type { RedirectCode, Route, RouteGroupOptions, RouterInterface, StatusCode } from '@stacksjs/types'
7
- import { extractDefaultRequest, extractModelRequest, findRequestInstance } from './utils'
7
+ import { customValidate, isObjectNotEmpty } from '@stacksjs/validation'
8
+ import { extractDefaultRequest, findRequestInstance } from './utils'
8
9
 
9
10
  type ActionPath = string // TODO: narrow this by automating its generation
10
11
 
@@ -66,7 +67,7 @@ export class Router implements RouterInterface {
66
67
  public async email(path: Route['url']): Promise<this> {
67
68
  path = pascalCase(path)
68
69
 
69
- const emailModule = (await import(p.userNotificationsPath(`${path}.ts`))).default as Action
70
+ const emailModule = (await import(p.userNotificationsPath(path))).default as Action
70
71
  const callback = emailModule.handle
71
72
  const uri = this.prepareUri(path)
72
73
  this.addRoute('GET', uri, callback, 200)
@@ -75,7 +76,7 @@ export class Router implements RouterInterface {
75
76
  }
76
77
 
77
78
  public async health(): Promise<this> {
78
- const healthModule = (await import(p.userActionsPath('HealthAction.ts'))).default as Action
79
+ const healthModule = (await import(p.userActionsPath('HealthAction'))).default as Action
79
80
  const callback = healthModule.handle
80
81
  const path = healthModule.path ?? `${this.apiPrefix}/health`
81
82
 
@@ -107,7 +108,7 @@ export class Router implements RouterInterface {
107
108
  path = pascalCase(path) // actions are PascalCase
108
109
 
109
110
  try {
110
- const action = (await import(p.userActionsPath(`${path}.ts`))).default as Action
111
+ const action = (await import(p.userActionsPath(path))).default as Action
111
112
 
112
113
  return this.addRoute(action.method ?? 'GET', this.prepareUri(path), action.handle, 200)
113
114
  } catch (error) {
@@ -274,8 +275,7 @@ export class Router implements RouterInterface {
274
275
  let importPathFunction = p.appPath // Default import path function
275
276
 
276
277
  if (callbackPath.startsWith('../')) importPathFunction = p.routesPath
277
-
278
- if (modulePath.includes('OrmAction')) importPathFunction = p.projectStoragePath
278
+ if (modulePath.includes('OrmAction')) importPathFunction = p.storagePath
279
279
 
280
280
  // Remove trailing .ts if present
281
281
  modulePath = modulePath.endsWith('.ts') ? modulePath.slice(0, -3) : modulePath
@@ -301,17 +301,16 @@ export class Router implements RouterInterface {
301
301
  // if fails, return validation error
302
302
  let requestInstance
303
303
 
304
- console.log('actionModule', actionModule)
305
-
306
304
  if (actionModule.default.requestFile) {
307
305
  requestInstance = await findRequestInstance(actionModule.default.requestFile)
308
306
  } else {
309
307
  requestInstance = await extractDefaultRequest(modulePath)
310
308
  }
311
309
 
312
- console.log(actionModule.default.requestFile)
313
-
314
310
  try {
311
+ if (isObjectNotEmpty(actionModule.default.validations))
312
+ await customValidate(actionModule.default.validations, requestInstance.all())
313
+
315
314
  return await actionModule.default.handle(requestInstance)
316
315
  } catch (error: any) {
317
316
  return { status: error.status, errors: error.errors }
package/src/server.ts CHANGED
@@ -40,23 +40,22 @@ export async function serve(options: ServeOptions = {}) {
40
40
  }
41
41
 
42
42
  export async function serverResponse(req: Request, body: string) {
43
- log.info(`Incoming Request: ${req.method} ${req.url}`)
44
- log.info(`Headers: ${JSON.stringify(req.headers)}`)
45
- log.info(`Body: ${JSON.stringify(req.body)}`)
46
- // log.info(`Query: ${JSON.stringify(req.query)}`)
47
- // log.info(`Params: ${JSON.stringify(req.params)}`)
48
- // log.info(`Cookies: ${JSON.stringify(req.cookies)}`)
43
+ log.debug(`Incoming Request: ${req.method} ${req.url}`)
44
+ log.debug(`Headers: ${JSON.stringify(req.headers)}`)
45
+ log.debug(`Body: ${JSON.stringify(req.body)}`)
46
+ // log.debug(`Query: ${JSON.stringify(req.query)}`)
47
+ // log.debug(`Params: ${JSON.stringify(req.params)}`)
48
+ // log.debug(`Cookies: ${JSON.stringify(req.cookies)}`)
49
49
 
50
50
  // Trim trailing slash from the URL if it's not the root '/'
51
51
  // This automatically allows for route definitions, like
52
52
  // '/about' and '/about/' to be treated as the same
53
53
  const trimmedUrl = req.url.endsWith('/') && req.url.length > 1 ? req.url.slice(0, -1) : req.url
54
-
55
54
  const url = new URL(trimmedUrl)
56
-
57
55
  const routesList: Route[] = await route.getRoutes()
58
- log.info(`Routes List: ${JSON.stringify(routesList)}`)
59
- log.info(`URL: ${JSON.stringify(url)}`)
56
+
57
+ log.info(`Routes List: ${JSON.stringify(routesList)}`, { styled: false })
58
+ log.info(`URL: ${JSON.stringify(url)}`, { styled: false })
60
59
 
61
60
  if (req.method === 'OPTIONS') {
62
61
  return handleOptions(req)
@@ -70,9 +69,10 @@ export async function serverResponse(req: Request, body: string) {
70
69
  })
71
70
  .find((route: Route) => route.method === req.method)
72
71
 
73
- log.info(`Found Route: ${JSON.stringify(foundRoute)}`)
72
+ log.info(`Found Route: ${JSON.stringify(foundRoute)}`, { styled: false })
74
73
 
75
74
  if (!foundRoute) {
75
+ // TODO: create a pretty 404 page
76
76
  return new Response('Pretty 404 page coming soon', {
77
77
  status: 404,
78
78
  headers: {
@@ -80,7 +80,7 @@ export async function serverResponse(req: Request, body: string) {
80
80
  'Access-Control-Allow-Headers': '*',
81
81
  'Content-Type': 'json',
82
82
  },
83
- }) // TODO: create a pretty 404 page
83
+ })
84
84
  }
85
85
 
86
86
  const routeParams = extractDynamicSegments(foundRoute.uri, url.pathname)
@@ -144,7 +144,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
144
144
 
145
145
  const { status, ...payloadWithoutStatus } = middlewarePayload
146
146
 
147
- return await new Response(JSON.stringify(payloadWithoutStatus), {
147
+ return new Response(JSON.stringify(payloadWithoutStatus), {
148
148
  headers: {
149
149
  'Content-Type': 'json',
150
150
  'Access-Control-Allow-Origin': '*',
@@ -160,7 +160,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
160
160
  const callback = String(foundCallback)
161
161
  const response = Response.redirect(callback, statusCode)
162
162
 
163
- return await noCache(response)
163
+ return noCache(response)
164
164
  }
165
165
 
166
166
  if (foundRoute?.method !== req.method) {
@@ -178,7 +178,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
178
178
  try {
179
179
  const fileContent = Bun.file(foundCallback)
180
180
 
181
- return await new Response(fileContent, {
181
+ return new Response(fileContent, {
182
182
  headers: {
183
183
  'Content-Type': 'text/html',
184
184
  'Access-Control-Allow-Origin': '*',
@@ -186,7 +186,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
186
186
  },
187
187
  })
188
188
  } catch (error) {
189
- return await new Response('Error reading the HTML file', {
189
+ return new Response('Error reading the HTML file', {
190
190
  status: 500,
191
191
  headers: {
192
192
  'Access-Control-Allow-Origin': '*',
@@ -197,7 +197,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
197
197
  }
198
198
 
199
199
  if (isString(foundCallback))
200
- return await new Response(foundCallback, {
200
+ return new Response(foundCallback, {
201
201
  headers: {
202
202
  'Content-Type': 'json',
203
203
  'Access-Control-Allow-Origin': '*',
@@ -209,7 +209,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
209
209
  if (isFunction(foundCallback)) {
210
210
  const result = foundCallback()
211
211
 
212
- return await new Response(JSON.stringify(result), {
212
+ return new Response(JSON.stringify(result), {
213
213
  status: 200,
214
214
  headers: {
215
215
  'Access-Control-Allow-Origin': '*',
@@ -222,7 +222,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
222
222
  if (foundCallback.status === 401) {
223
223
  const { status, ...rest } = foundCallback
224
224
 
225
- return await new Response(JSON.stringify(rest), {
225
+ return new Response(JSON.stringify(rest), {
226
226
  headers: {
227
227
  'Content-Type': 'json',
228
228
  'Access-Control-Allow-Origin': '*',
@@ -235,7 +235,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
235
235
  if (foundCallback.status === 403) {
236
236
  const { status, ...rest } = foundCallback
237
237
 
238
- return await new Response(JSON.stringify(rest), {
238
+ return new Response(JSON.stringify(rest), {
239
239
  headers: {
240
240
  'Content-Type': 'json',
241
241
  'Access-Control-Allow-Origin': '*',
@@ -248,7 +248,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
248
248
  if (foundCallback.status === 422) {
249
249
  const { status, ...rest } = foundCallback
250
250
 
251
- return await new Response(JSON.stringify(rest), {
251
+ return new Response(JSON.stringify(rest), {
252
252
  headers: {
253
253
  'Content-Type': 'json',
254
254
  'Access-Control-Allow-Origin': '*',
@@ -261,7 +261,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
261
261
  if (foundCallback.status === 500) {
262
262
  const { status, ...rest } = foundCallback
263
263
 
264
- return await new Response(JSON.stringify(rest), {
264
+ return new Response(JSON.stringify(rest), {
265
265
  headers: { 'Content-Type': 'json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*' },
266
266
  status: 500,
267
267
  })
@@ -269,7 +269,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
269
269
  }
270
270
 
271
271
  if (isObject(foundCallback)) {
272
- return await new Response(JSON.stringify(foundCallback), {
272
+ return new Response(JSON.stringify(foundCallback), {
273
273
  headers: {
274
274
  'Content-Type': 'json',
275
275
  'Access-Control-Allow-Origin': '*',
@@ -280,7 +280,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
280
280
  }
281
281
 
282
282
  // If no known type matched, return a generic error.
283
- return await new Response('Unknown callback type.', {
283
+ return new Response('Unknown callback type.', {
284
284
  headers: {
285
285
  'Content-Type': 'json',
286
286
  'Access-Control-Allow-Origin': '*',
package/src/utils.ts CHANGED
@@ -17,7 +17,6 @@ export function extractModelFromAction(action: string): string {
17
17
 
18
18
  if (action.includes('IndexOrmAction')) {
19
19
  const match = action.match(/\/([A-Z][a-z]+)IndexOrmAction/)
20
-
21
20
  const modelString = match ? match[1] : ''
22
21
 
23
22
  model = modelString as string
@@ -25,7 +24,6 @@ export function extractModelFromAction(action: string): string {
25
24
 
26
25
  if (action.includes('StoreOrmAction')) {
27
26
  const match = action.match(/\/([A-Z][a-z]+)StoreOrmAction/)
28
-
29
27
  const modelString = match ? match[1] : ''
30
28
 
31
29
  model = modelString as string
@@ -57,7 +55,6 @@ export function extractModelFromAction(action: string): string {
57
55
 
58
56
  export function extractDynamicAction(action: string): string | undefined {
59
57
  const regex = /Actions\/(.*?)Action/
60
-
61
58
  const match = action.match(regex)
62
59
 
63
60
  return match ? match[1] : ''
@@ -65,23 +62,17 @@ export function extractDynamicAction(action: string): string | undefined {
65
62
 
66
63
  export async function extractModelRequest(action: string) {
67
64
  const extractedModel = extractModelFromAction(action)
68
-
69
65
  const lowerCaseModel = camelCase(extractedModel)
70
-
71
66
  const requestPath = path.frameworkPath(`requests/${extractedModel}Request.ts`)
72
-
73
67
  const requestInstance = await import(requestPath)
74
-
75
68
  const requestIndex = `${lowerCaseModel}Request`
76
69
 
77
70
  return requestInstance[requestIndex]
78
71
  }
79
72
 
80
73
  export async function findRequestInstance(requestInstance: string) {
81
- const frameworkDirectory = path.projectStoragePath('framework/requests')
82
-
74
+ const frameworkDirectory = path.storagePath('framework/requests')
83
75
  const filePath = path.join(frameworkDirectory, `${requestInstance}.ts`)
84
-
85
76
  const pathExists = await existsSync(filePath)
86
77
 
87
78
  // Check if the directory exists
@@ -91,8 +82,7 @@ export async function findRequestInstance(requestInstance: string) {
91
82
  return requestInstance.request
92
83
  }
93
84
 
94
- const defaultRequestPath = path.projectStoragePath('framework/core/router/src/request.ts')
95
-
85
+ const defaultRequestPath = path.storagePath('framework/core/router/src/request.ts')
96
86
  const fileExists = await existsSync(defaultRequestPath)
97
87
 
98
88
  if (fileExists) {