@stacksjs/router 0.61.24 → 0.62.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.61.24",
4
+ "version": "0.62.0",
5
5
  "description": "The Stacks framework router.",
6
6
  "author": "Chris Breuer",
7
7
  "license": "MIT",
@@ -49,14 +49,14 @@
49
49
  },
50
50
  "peerDependencies": {
51
51
  "@stacksjs/config": "latest",
52
- "unplugin-vue-router": "^0.9.0",
53
- "vue-router": "^4.3.2"
52
+ "unplugin-vue-router": "^0.10.0",
53
+ "vue-router": "^4.4.0"
54
54
  },
55
55
  "dependencies": {
56
56
  "@stacksjs/config": "latest",
57
57
  "@stacksjs/logging": "latest",
58
- "unplugin-vue-router": "^0.9.0",
59
- "vue-router": "^4.3.2"
58
+ "unplugin-vue-router": "^0.10.0",
59
+ "vue-router": "^4.4.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@stacksjs/development": "latest"
package/src/request.ts CHANGED
@@ -1,15 +1,33 @@
1
- import type { RequestInstance, RouteParam } from "@stacksjs/types"
1
+ import type { RequestInstance, RouteParam } from '@stacksjs/types'
2
+
3
+ import type { VineType } from '@stacksjs/types'
4
+ import { customValidate, validateField } from '@stacksjs/validation'
2
5
 
3
6
  interface RequestData {
4
7
  [key: string]: string
5
8
  }
6
9
 
10
+ interface ValidationType {
11
+ rule: VineType
12
+ message: { [key: string]: string }
13
+ }
14
+
15
+ interface ValidationField {
16
+ [key: string]: string | ValidationType
17
+ validation: ValidationType
18
+ }
19
+
20
+ interface CustomAttributes {
21
+ [key: string]: ValidationField
22
+ }
23
+
7
24
  type RouteParams = { [key: string]: string } | null
8
25
 
9
- export class Request implements RequestInstance{
26
+ export class Request implements RequestInstance {
10
27
  private static instance: Request
11
28
  private query: RequestData = {}
12
29
  private params: RouteParams = null
30
+ private headers: any = {}
13
31
 
14
32
  // An attempt to singleston instance, might be needed at some point
15
33
  public static getInstance(): Request {
@@ -23,10 +41,18 @@ export class Request implements RequestInstance{
23
41
  this.query = Object.fromEntries(url.searchParams)
24
42
  }
25
43
 
44
+ public addBodies(params: any): void {
45
+ this.query = params
46
+ }
47
+
26
48
  public addParam(param: RouteParam): void {
27
49
  this.params = param
28
50
  }
29
51
 
52
+ public addHeaders(headerParams: Headers): void {
53
+ this.headers = headerParams
54
+ }
55
+
30
56
  public get(element: string): string | number | undefined {
31
57
  return this.query[element]
32
58
  }
@@ -35,6 +61,14 @@ export class Request implements RequestInstance{
35
61
  return this.query
36
62
  }
37
63
 
64
+ public async validate(attributes?: CustomAttributes): Promise<void> {
65
+ if (attributes === undefined || attributes === null) {
66
+ await validateField('Release', this.all())
67
+ } else {
68
+ await customValidate(attributes, this.all())
69
+ }
70
+ }
71
+
38
72
  public has(element: string): boolean {
39
73
  return element in this.query
40
74
  }
@@ -50,10 +84,32 @@ export class Request implements RequestInstance{
50
84
  if (match?.groups) this.params = match.groups
51
85
  }
52
86
 
87
+ public header(headerParam: string): string | number | boolean | null {
88
+ return this.headers.get(headerParam)
89
+ }
90
+
91
+ public getHeaders() {
92
+ return this.headers
93
+ }
94
+
95
+ public Header(headerParam: string): string | number | boolean | null {
96
+ return this.headers.get(headerParam)
97
+ }
98
+
53
99
  public getParam(key: string): number | string | null {
54
100
  return this.params ? this.params[key] || null : null
55
101
  }
56
102
 
103
+ public bearerToken(): string | null {
104
+ const authorizationHeader = this.headers.get('authorization')
105
+
106
+ if (authorizationHeader && authorizationHeader.startsWith('Bearer ')) {
107
+ return authorizationHeader.substring(7)
108
+ }
109
+
110
+ return null
111
+ }
112
+
57
113
  public getParams(): RouteParams {
58
114
  return this.params
59
115
  }
@@ -62,6 +118,6 @@ export class Request implements RequestInstance{
62
118
  const value = this.params ? this.params[key] || null : null
63
119
  return value ? Number.parseInt(value) : null
64
120
  }
65
- }
121
+ }
66
122
 
67
123
  export const request = new Request()
package/src/router.ts CHANGED
@@ -3,10 +3,9 @@ import { log } from '@stacksjs/logging'
3
3
  import { path as p, projectStoragePath, routesPath } from '@stacksjs/path'
4
4
  import { kebabCase, pascalCase } from '@stacksjs/strings'
5
5
  import type { Job } from '@stacksjs/types'
6
- import { request } from '@stacksjs/router'
7
6
  import type { RedirectCode, Route, RouteGroupOptions, RouterInterface, StatusCode } from '@stacksjs/types'
8
7
 
9
- import { extractModelRequest } from './utils'
8
+ import { extractDynamicRequest, extractModelRequest } from './utils'
10
9
 
11
10
  type ActionPath = string // TODO: narrow this by automating its generation
12
11
 
@@ -284,11 +283,9 @@ export class Router implements RouterInterface {
284
283
 
285
284
  let actionModule = null
286
285
 
287
- if (modulePath.includes('OrmAction')) {
286
+ if (modulePath.includes('OrmAction'))
288
287
  actionModule = await import(importPathFunction(`/framework/orm/${modulePath}.ts`))
289
- } else {
290
- actionModule = await import(importPathFunction(`${modulePath}.ts`))
291
- }
288
+ else actionModule = await import(importPathFunction(`${modulePath}.ts`))
292
289
 
293
290
  // Use custom path from action module if available
294
291
  const newPath = actionModule.default.path ?? originalPath
@@ -301,10 +298,16 @@ export class Router implements RouterInterface {
301
298
  // then validate
302
299
  // if succeeds, run the handle
303
300
  // if fails, return validation error
301
+ let requestInstance
302
+
303
+ if (modulePath.includes('OrmAction')) requestInstance = await extractModelRequest(modulePath)
304
+ else requestInstance = await extractDynamicRequest(modulePath)
304
305
 
305
- const requestInstance = await extractModelRequest(modulePath)
306
-
307
- return await actionModule.default.handle(requestInstance)
306
+ try {
307
+ return await actionModule.default.handle(requestInstance)
308
+ } catch (error: any) {
309
+ return { status: error.status, errors: error.errors }
310
+ }
308
311
  }
309
312
 
310
313
  private normalizePath(path: string): string {
package/src/server.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import process from 'node:process'
2
2
  import { log } from '@stacksjs/logging'
3
- import { extname, path } from '@stacksjs/path'
3
+ import { getModelName } from '@stacksjs/orm'
4
+ import { path, extname } from '@stacksjs/path'
4
5
  import { glob } from '@stacksjs/storage'
6
+ import { camelCase, lowercase } from '@stacksjs/strings'
5
7
  import type { Model, Route, RouteParam, StatusCode } from '@stacksjs/types'
6
8
  import { route } from '.'
7
9
  import { middlewares } from './middleware'
8
10
  import { request as RequestParam } from './request'
9
- import { getModelName } from '@stacksjs/orm'
10
- import { lowercase } from '@stacksjs/strings'
11
11
 
12
12
  interface ServeOptions {
13
13
  host?: string
@@ -33,12 +33,14 @@ export async function serve(options: ServeOptions = {}) {
33
33
  development,
34
34
 
35
35
  async fetch(req: Request) {
36
- return await serverResponse(req)
36
+ const reqBody = await req.text()
37
+
38
+ return await serverResponse(req, reqBody)
37
39
  },
38
40
  })
39
41
  }
40
42
 
41
- export async function serverResponse(req: Request) {
43
+ export async function serverResponse(req: Request, body: string) {
42
44
  log.info(`Incoming Request: ${req.method} ${req.url}`)
43
45
  log.info(`Headers: ${JSON.stringify(req.headers)}`)
44
46
  log.info(`Body: ${JSON.stringify(req.body)}`)
@@ -58,6 +60,10 @@ export async function serverResponse(req: Request) {
58
60
 
59
61
  log.info(`URL: ${JSON.stringify(url)}`)
60
62
 
63
+ if (req.method === 'OPTIONS') {
64
+ return handleOptions(req)
65
+ }
66
+
61
67
  const foundRoute: Route | undefined = routesList
62
68
  .filter((route: Route) => {
63
69
  const pattern = new RegExp(`^${route.uri.replace(/\{(\w+)\}/g, '(\\w+)')}$`)
@@ -67,21 +73,45 @@ export async function serverResponse(req: Request) {
67
73
  .find((route: Route) => route.method === req.method)
68
74
 
69
75
  log.info(`Found Route: ${JSON.stringify(foundRoute)}`)
70
- // if (url.pathname === '/favicon.ico')
71
- // return new Response('')
72
76
 
73
- if (!foundRoute) return new Response('Pretty 404 page coming soon', { status: 404 }) // TODO: create a pretty 404 page
77
+ if (!foundRoute) {
78
+ return new Response('Pretty 404 page coming soon', {
79
+ status: 404,
80
+ headers: {
81
+ 'Access-Control-Allow-Origin': '*',
82
+ 'Access-Control-Allow-Headers': '*',
83
+ 'Content-Type': 'json',
84
+ },
85
+ }) // TODO: create a pretty 404 page
86
+ }
74
87
 
75
88
  const routeParams = extractDynamicSegments(foundRoute.uri, url.pathname)
76
89
 
77
- await addRouteQuery(url)
78
- await addRouteParam(routeParams)
90
+ if (!body) {
91
+ await addRouteQuery(url)
92
+ } else {
93
+ await addBody(body)
94
+ }
79
95
 
80
- await executeMiddleware(foundRoute)
96
+ await addRouteParam(routeParams)
97
+ await addHeaders(req.headers)
81
98
 
82
99
  return await execute(foundRoute, req, { statusCode: foundRoute?.statusCode })
83
100
  }
84
101
 
102
+ function handleOptions(req: Request) {
103
+ return new Response(null, {
104
+ status: 204,
105
+ headers: {
106
+ 'Access-Control-Allow-Origin': '*',
107
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
108
+ 'Access-Control-Allow-Headers':
109
+ 'Content-Type, Authorization, Access-Control-Allow-Headers, Access-Control-Allow-Origin, Accept',
110
+ 'Access-Control-Max-Age': '86400', // Cache the preflight response for a day
111
+ },
112
+ })
113
+ }
114
+
85
115
  function extractDynamicSegments(routePattern: string, path: string): RouteParam {
86
116
  const regexPattern = new RegExp(`^${routePattern.replace(/\{(\w+)\}/g, '(\\w+)')}$`)
87
117
  const match = path.match(regexPattern)
@@ -103,6 +133,27 @@ function extractDynamicSegments(routePattern: string, path: string): RouteParam
103
133
  async function execute(foundRoute: Route, req: Request, { statusCode }: Options) {
104
134
  const foundCallback = await route.resolveCallback(foundRoute.callback)
105
135
 
136
+ const middlewarePayload = await executeMiddleware(foundRoute)
137
+
138
+ if (
139
+ middlewarePayload !== null &&
140
+ typeof middlewarePayload === 'object' &&
141
+ Object.keys(middlewarePayload).length > 0
142
+ ) {
143
+ const middlewareStatus = middlewarePayload.status
144
+
145
+ delete middlewarePayload.status
146
+
147
+ return await new Response(JSON.stringify(middlewarePayload), {
148
+ headers: {
149
+ 'Content-Type': 'json',
150
+ 'Access-Control-Allow-Origin': '*',
151
+ 'Access-Control-Allow-Headers': '*',
152
+ },
153
+ status: middlewareStatus || 401,
154
+ })
155
+ }
156
+
106
157
  if (!statusCode) statusCode = 200
107
158
 
108
159
  if (foundRoute?.method === 'GET' && (statusCode === 301 || statusCode === 302)) {
@@ -112,7 +163,14 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
112
163
  return await noCache(response)
113
164
  }
114
165
 
115
- if (foundRoute?.method !== req.method) return new Response('Method not allowed', { status: 405 })
166
+ if (foundRoute?.method !== req.method)
167
+ return new Response('Method not allowed', {
168
+ status: 405,
169
+ headers: {
170
+ 'Access-Control-Allow-Origin': '*',
171
+ 'Access-Control-Allow-Headers': '*',
172
+ },
173
+ })
116
174
 
117
175
  // Check if it's a path to an HTML file
118
176
  if (isString(foundCallback) && extname(foundCallback) === '.html') {
@@ -120,25 +178,117 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
120
178
  const fileContent = Bun.file(foundCallback)
121
179
 
122
180
  return await new Response(fileContent, {
123
- headers: { 'Content-Type': 'text/html' },
181
+ headers: {
182
+ 'Content-Type': 'text/html',
183
+ 'Access-Control-Allow-Origin': '*',
184
+ 'Access-Control-Allow-Headers': '*',
185
+ },
124
186
  })
125
187
  } catch (error) {
126
- return await new Response('Error reading the HTML file', { status: 500 })
188
+ return await new Response('Error reading the HTML file', {
189
+ status: 500,
190
+ headers: {
191
+ 'Access-Control-Allow-Origin': '*',
192
+ 'Access-Control-Allow-Headers': '*',
193
+ },
194
+ })
127
195
  }
128
196
  }
129
197
 
130
- if (isString(foundCallback)) return await new Response(foundCallback)
198
+ if (isString(foundCallback))
199
+ return await new Response(foundCallback, {
200
+ headers: {
201
+ 'Content-Type': 'json',
202
+ 'Access-Control-Allow-Origin': '*',
203
+ 'Access-Control-Allow-Headers': '*',
204
+ },
205
+ status: 200,
206
+ })
131
207
 
132
208
  if (isFunction(foundCallback)) {
133
209
  const result = foundCallback()
134
210
 
135
- return await new Response(JSON.stringify(result))
211
+ return await new Response(JSON.stringify(result), {
212
+ status: 200,
213
+ headers: {
214
+ 'Access-Control-Allow-Origin': '*',
215
+ 'Access-Control-Allow-Headers': '*',
216
+ },
217
+ })
136
218
  }
137
219
 
138
- if (isObject(foundCallback)) return await new Response(JSON.stringify(foundCallback))
220
+ if (isObject(foundCallback) && foundCallback.status) {
221
+ if (foundCallback.status === 401) {
222
+ delete foundCallback.status
223
+
224
+ return await new Response(JSON.stringify(foundCallback), {
225
+ headers: {
226
+ 'Content-Type': 'json',
227
+ 'Access-Control-Allow-Origin': '*',
228
+ 'Access-Control-Allow-Headers': '*',
229
+ },
230
+ status: 401,
231
+ })
232
+ }
233
+
234
+ if (foundCallback.status === 403) {
235
+ delete foundCallback.status
236
+ return await new Response(JSON.stringify(foundCallback), {
237
+ headers: {
238
+ 'Content-Type': 'json',
239
+ 'Access-Control-Allow-Origin': '*',
240
+ 'Access-Control-Allow-Headers': '*',
241
+ },
242
+ status: 403,
243
+ })
244
+ }
245
+
246
+ if (foundCallback.status === 422) {
247
+ // biome-ignore lint/performance/noDelete: <explanation>
248
+ delete foundCallback.status
249
+
250
+ return await new Response(JSON.stringify(foundCallback), {
251
+ headers: {
252
+ 'Content-Type': 'json',
253
+ 'Access-Control-Allow-Origin': '*',
254
+ 'Access-Control-Allow-Headers': '*',
255
+ },
256
+ status: 422,
257
+ })
258
+ }
259
+
260
+ if (foundCallback.status === 500) {
261
+ delete foundCallback.status
262
+ return await new Response(JSON.stringify(foundCallback), {
263
+ headers: {
264
+ 'Content-Type': 'json',
265
+ 'Access-Control-Allow-Origin': '*',
266
+ 'Access-Control-Allow-Headers': '*',
267
+ },
268
+ status: 500,
269
+ })
270
+ }
271
+ }
272
+
273
+ if (isObject(foundCallback))
274
+ return await new Response(JSON.stringify(foundCallback), {
275
+ headers: {
276
+ 'Content-Type': 'json',
277
+ 'Access-Control-Allow-Origin': '*',
278
+ 'Access-Control-Allow-Headers': '*',
279
+ },
280
+ status: 200,
281
+ })
139
282
 
140
283
  // If no known type matched, return a generic error.
141
- return await new Response('Unknown callback type.', { status: 500 })
284
+ return await new Response('Unknown callback type.', {
285
+ headers: {
286
+ 'Content-Type': 'json',
287
+ 'Access-Control-Allow-Origin': '*',
288
+ 'Access-Control-Allow-Headers': '*',
289
+ },
290
+ status: 500,
291
+ })
142
292
  }
143
293
 
144
294
  function noCache(response: Response) {
@@ -150,67 +300,108 @@ function noCache(response: Response) {
150
300
  }
151
301
 
152
302
  async function addRouteQuery(url: URL) {
153
- const modelFiles = glob.sync(path.userModelsPath('*.ts'));
303
+ const modelFiles = glob.sync(path.userModelsPath('*.ts'))
154
304
 
155
305
  for (const modelFile of modelFiles) {
156
- const model = (await import(modelFile)).default;
157
- const modelName = getModelName(model, modelFile);
158
- const modelNameLower = `${lowercase(modelName)}Request`;
159
- const requestPath = path.projectStoragePath(`framework/requests/${modelName}Request.ts`);
160
- const requestImport = await import(requestPath);
161
- const requestInstance = requestImport[modelNameLower];
162
-
163
- if (requestInstance && !isObjectNotEmpty(url.searchParams)) {
164
- requestInstance.addQuery(url);
306
+ const model = (await import(modelFile)).default
307
+ const modelName = getModelName(model, modelFile)
308
+ const modelNameLower = `${camelCase(modelName)}Request`
309
+ const requestPath = path.projectStoragePath(`framework/requests/${modelName}Request.ts`)
310
+ const requestImport = await import(requestPath)
311
+ const requestInstance = requestImport[modelNameLower]
312
+
313
+ if (requestInstance) {
314
+ requestInstance.addQuery(url)
165
315
  }
166
316
  }
167
317
 
168
- if (!isObjectNotEmpty(url.searchParams)) {
169
- RequestParam.addQuery(url);
318
+ RequestParam.addQuery(url)
319
+ }
320
+
321
+ async function addBody(params: any) {
322
+ const modelFiles = glob.sync(path.userModelsPath('*.ts'))
323
+
324
+ for (const modelFile of modelFiles) {
325
+ const model = (await import(modelFile)).default
326
+ const modelName = getModelName(model, modelFile)
327
+ const modelNameLower = `${camelCase(modelName)}Request`
328
+ const requestPath = path.projectStoragePath(`framework/requests/${modelName}Request.ts`)
329
+ const requestImport = await import(requestPath)
330
+ const requestInstance = requestImport[modelNameLower]
331
+
332
+ if (requestInstance) {
333
+ requestInstance.addBodies(JSON.parse(params))
334
+ }
170
335
  }
336
+
337
+ RequestParam.addBodies(JSON.parse(params))
171
338
  }
172
339
 
173
340
  async function addRouteParam(param: RouteParam): Promise<void> {
174
- const modelFiles = glob.sync(path.userModelsPath('*.ts'));
341
+ const modelFiles = glob.sync(path.userModelsPath('*.ts'))
175
342
 
176
343
  for (const modelFile of modelFiles) {
177
- const model = (await import(modelFile)).default as Model;
178
- const modelName = getModelName(model, modelFile);
179
- const modelNameLower = `${lowercase(modelName)}Request`;
180
- const requestPath = path.projectStoragePath(`framework/requests/${modelName}Request.ts`);
181
- const requestImport = await import(requestPath);
182
- const requestInstance = requestImport[modelNameLower];
344
+ const model = (await import(modelFile)).default as Model
345
+ const modelName = getModelName(model, modelFile)
346
+ const modelNameLower = `${lowercase(modelName)}Request`
347
+ const requestPath = path.projectStoragePath(`framework/requests/${modelName}Request.ts`)
348
+ const requestImport = await import(requestPath)
349
+ const requestInstance = requestImport[modelNameLower]
183
350
 
184
351
  if (requestInstance) {
185
- requestInstance.addParam(param);
352
+ requestInstance.addParam(param)
186
353
  }
187
354
  }
188
355
 
189
356
  RequestParam.addParam(param)
190
357
  }
191
358
 
192
- function executeMiddleware(route: Route): void {
359
+ async function addHeaders(headers: Headers): Promise<void> {
360
+ const modelFiles = glob.sync(path.userModelsPath('*.ts'))
361
+
362
+ for (const modelFile of modelFiles) {
363
+ const model = (await import(modelFile)).default as Model
364
+ const modelName = getModelName(model, modelFile)
365
+ const modelNameLower = `${lowercase(modelName)}Request`
366
+ const requestPath = path.projectStoragePath(`framework/requests/${modelName}Request.ts`)
367
+ const requestImport = await import(requestPath)
368
+ const requestInstance = requestImport[modelNameLower]
369
+
370
+ if (requestInstance) {
371
+ requestInstance.addHeaders(headers)
372
+ }
373
+ }
374
+
375
+ RequestParam.addHeaders(headers)
376
+ }
377
+
378
+ async function executeMiddleware(route: Route): Promise<any> {
193
379
  const { middleware = null } = route
194
380
 
195
381
  if (middleware && middlewares && isObjectNotEmpty(middlewares)) {
196
382
  // let middlewareItem: MiddlewareOptions
197
383
  if (isString(middleware)) {
198
- // TODO: fix and uncomment this
199
- // middlewareItem = middlewares.find((m) => {
200
- // return m.name === middleware
201
- // })
202
- // if (middlewareItem)
203
- // middlewareItem.handle() // Invoke only if it exists and is not undefined.
384
+ const middlewarePath = path.userMiddlewarePath(`${middleware}.ts`)
385
+
386
+ const middlewareInstance = (await import(middlewarePath)).default
387
+
388
+ try {
389
+ await middlewareInstance.handle()
390
+ } catch (error: any) {
391
+ return error
392
+ }
204
393
  } else {
205
- // middleware.forEach((m) => {
206
- middleware.forEach(() => {
207
- // TODO: fix and uncomment this
208
- // middlewareItem = middlewares.find((middlewareItem: MiddlewareOptions) => {
209
- // return middlewareItem.name === m
210
- // })
211
- // if (middlewareItem)
212
- // middlewareItem.handle() // Again, invoke only if it exists.
213
- })
394
+ for (const middlewareElement of middleware) {
395
+ const middlewarePath = path.userMiddlewarePath(`${middlewareElement}.ts`)
396
+
397
+ const middlewareInstance = (await import(middlewarePath)).default
398
+
399
+ try {
400
+ await middlewareInstance.handle()
401
+ } catch (error: any) {
402
+ return error
403
+ }
404
+ }
214
405
  }
215
406
  }
216
407
  }