@stacksjs/router 0.61.24 → 0.63.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.63.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.6",
53
+ "vue-router": "^4.4.3"
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.6",
59
+ "vue-router": "^4.4.3"
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?.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,8 @@ 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
-
9
- import { extractModelRequest } from './utils'
7
+ import { extractDefaultRequest, extractModelRequest, findRequestInstance } from './utils'
10
8
 
11
9
  type ActionPath = string // TODO: narrow this by automating its generation
12
10
 
@@ -284,11 +282,11 @@ export class Router implements RouterInterface {
284
282
 
285
283
  let actionModule = null
286
284
 
287
- if (modulePath.includes('OrmAction')) {
285
+ if (modulePath.includes('storage/framework/orm')) actionModule = await import(`${modulePath}.ts`)
286
+ else if (modulePath.includes('app/Actions')) actionModule = await import(`${modulePath}.ts`)
287
+ else if (modulePath.includes('OrmAction'))
288
288
  actionModule = await import(importPathFunction(`/framework/orm/${modulePath}.ts`))
289
- } else {
290
- actionModule = await import(importPathFunction(`${modulePath}.ts`))
291
- }
289
+ else actionModule = await import(importPathFunction(`${modulePath}.ts`))
292
290
 
293
291
  // Use custom path from action module if available
294
292
  const newPath = actionModule.default.path ?? originalPath
@@ -301,10 +299,23 @@ export class Router implements RouterInterface {
301
299
  // then validate
302
300
  // if succeeds, run the handle
303
301
  // if fails, return validation error
302
+ let requestInstance
303
+
304
+ console.log('actionModule', actionModule)
305
+
306
+ if (actionModule.default.requestFile) {
307
+ requestInstance = await findRequestInstance(actionModule.default.requestFile)
308
+ } else {
309
+ requestInstance = await extractDefaultRequest(modulePath)
310
+ }
311
+
312
+ console.log(actionModule.default.requestFile)
304
313
 
305
- const requestInstance = await extractModelRequest(modulePath)
306
-
307
- return await actionModule.default.handle(requestInstance)
314
+ try {
315
+ return await actionModule.default.handle(requestInstance)
316
+ } catch (error: any) {
317
+ return { status: error.status, errors: error.errors }
318
+ }
308
319
  }
309
320
 
310
321
  private normalizePath(path: string): string {
package/src/server.ts CHANGED
@@ -1,13 +1,12 @@
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'
5
6
  import type { Model, Route, RouteParam, StatusCode } from '@stacksjs/types'
6
7
  import { route } from '.'
7
8
  import { middlewares } from './middleware'
8
9
  import { request as RequestParam } from './request'
9
- import { getModelName } from '@stacksjs/orm'
10
- import { lowercase } from '@stacksjs/strings'
11
10
 
12
11
  interface ServeOptions {
13
12
  host?: string
@@ -33,12 +32,14 @@ export async function serve(options: ServeOptions = {}) {
33
32
  development,
34
33
 
35
34
  async fetch(req: Request) {
36
- return await serverResponse(req)
35
+ const reqBody = await req.text()
36
+
37
+ return await serverResponse(req, reqBody)
37
38
  },
38
39
  })
39
40
  }
40
41
 
41
- export async function serverResponse(req: Request) {
42
+ export async function serverResponse(req: Request, body: string) {
42
43
  log.info(`Incoming Request: ${req.method} ${req.url}`)
43
44
  log.info(`Headers: ${JSON.stringify(req.headers)}`)
44
45
  log.info(`Body: ${JSON.stringify(req.body)}`)
@@ -58,6 +59,10 @@ export async function serverResponse(req: Request) {
58
59
 
59
60
  log.info(`URL: ${JSON.stringify(url)}`)
60
61
 
62
+ if (req.method === 'OPTIONS') {
63
+ return handleOptions(req)
64
+ }
65
+
61
66
  const foundRoute: Route | undefined = routesList
62
67
  .filter((route: Route) => {
63
68
  const pattern = new RegExp(`^${route.uri.replace(/\{(\w+)\}/g, '(\\w+)')}$`)
@@ -67,21 +72,45 @@ export async function serverResponse(req: Request) {
67
72
  .find((route: Route) => route.method === req.method)
68
73
 
69
74
  log.info(`Found Route: ${JSON.stringify(foundRoute)}`)
70
- // if (url.pathname === '/favicon.ico')
71
- // return new Response('')
72
75
 
73
- if (!foundRoute) return new Response('Pretty 404 page coming soon', { status: 404 }) // TODO: create a pretty 404 page
76
+ if (!foundRoute) {
77
+ return new Response('Pretty 404 page coming soon', {
78
+ status: 404,
79
+ headers: {
80
+ 'Access-Control-Allow-Origin': '*',
81
+ 'Access-Control-Allow-Headers': '*',
82
+ 'Content-Type': 'json',
83
+ },
84
+ }) // TODO: create a pretty 404 page
85
+ }
74
86
 
75
87
  const routeParams = extractDynamicSegments(foundRoute.uri, url.pathname)
76
88
 
77
- await addRouteQuery(url)
78
- await addRouteParam(routeParams)
89
+ if (!body) {
90
+ await addRouteQuery(url)
91
+ } else {
92
+ await addBody(body)
93
+ }
79
94
 
80
- await executeMiddleware(foundRoute)
95
+ await addRouteParam(routeParams)
96
+ await addHeaders(req.headers)
81
97
 
82
98
  return await execute(foundRoute, req, { statusCode: foundRoute?.statusCode })
83
99
  }
84
100
 
101
+ function handleOptions(req: Request) {
102
+ return new Response(null, {
103
+ status: 204,
104
+ headers: {
105
+ 'Access-Control-Allow-Origin': '*',
106
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
107
+ 'Access-Control-Allow-Headers':
108
+ 'Content-Type, Authorization, Access-Control-Allow-Headers, Access-Control-Allow-Origin, Accept',
109
+ 'Access-Control-Max-Age': '86400', // Cache the preflight response for a day
110
+ },
111
+ })
112
+ }
113
+
85
114
  function extractDynamicSegments(routePattern: string, path: string): RouteParam {
86
115
  const regexPattern = new RegExp(`^${routePattern.replace(/\{(\w+)\}/g, '(\\w+)')}$`)
87
116
  const match = path.match(regexPattern)
@@ -100,8 +129,31 @@ function extractDynamicSegments(routePattern: string, path: string): RouteParam
100
129
  return dynamicSegments
101
130
  }
102
131
 
132
+ type CallbackWithStatus = Route['callback'] & { status: number }
133
+
103
134
  async function execute(foundRoute: Route, req: Request, { statusCode }: Options) {
104
- const foundCallback = await route.resolveCallback(foundRoute.callback)
135
+ const foundCallback: CallbackWithStatus = await route.resolveCallback(foundRoute.callback)
136
+
137
+ const middlewarePayload = await executeMiddleware(foundRoute)
138
+
139
+ if (
140
+ middlewarePayload !== null &&
141
+ typeof middlewarePayload === 'object' &&
142
+ Object.keys(middlewarePayload).length > 0
143
+ ) {
144
+ const middlewareStatus = middlewarePayload.status
145
+
146
+ const { status, ...payloadWithoutStatus } = middlewarePayload
147
+
148
+ return await new Response(JSON.stringify(payloadWithoutStatus), {
149
+ headers: {
150
+ 'Content-Type': 'json',
151
+ 'Access-Control-Allow-Origin': '*',
152
+ 'Access-Control-Allow-Headers': '*',
153
+ },
154
+ status: middlewareStatus || 401,
155
+ })
156
+ }
105
157
 
106
158
  if (!statusCode) statusCode = 200
107
159
 
@@ -112,7 +164,15 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
112
164
  return await noCache(response)
113
165
  }
114
166
 
115
- if (foundRoute?.method !== req.method) return new Response('Method not allowed', { status: 405 })
167
+ if (foundRoute?.method !== req.method) {
168
+ return new Response('Method not allowed', {
169
+ status: 405,
170
+ headers: {
171
+ 'Access-Control-Allow-Origin': '*',
172
+ 'Access-Control-Allow-Headers': '*',
173
+ },
174
+ })
175
+ }
116
176
 
117
177
  // Check if it's a path to an HTML file
118
178
  if (isString(foundCallback) && extname(foundCallback) === '.html') {
@@ -120,25 +180,115 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
120
180
  const fileContent = Bun.file(foundCallback)
121
181
 
122
182
  return await new Response(fileContent, {
123
- headers: { 'Content-Type': 'text/html' },
183
+ headers: {
184
+ 'Content-Type': 'text/html',
185
+ 'Access-Control-Allow-Origin': '*',
186
+ 'Access-Control-Allow-Headers': '*',
187
+ },
124
188
  })
125
189
  } catch (error) {
126
- return await new Response('Error reading the HTML file', { status: 500 })
190
+ return await new Response('Error reading the HTML file', {
191
+ status: 500,
192
+ headers: {
193
+ 'Access-Control-Allow-Origin': '*',
194
+ 'Access-Control-Allow-Headers': '*',
195
+ },
196
+ })
127
197
  }
128
198
  }
129
199
 
130
- if (isString(foundCallback)) return await new Response(foundCallback)
200
+ if (isString(foundCallback))
201
+ return await new Response(foundCallback, {
202
+ headers: {
203
+ 'Content-Type': 'json',
204
+ 'Access-Control-Allow-Origin': '*',
205
+ 'Access-Control-Allow-Headers': '*',
206
+ },
207
+ status: 200,
208
+ })
131
209
 
132
210
  if (isFunction(foundCallback)) {
133
211
  const result = foundCallback()
134
212
 
135
- return await new Response(JSON.stringify(result))
213
+ return await new Response(JSON.stringify(result), {
214
+ status: 200,
215
+ headers: {
216
+ 'Access-Control-Allow-Origin': '*',
217
+ 'Access-Control-Allow-Headers': '*',
218
+ },
219
+ })
136
220
  }
137
221
 
138
- if (isObject(foundCallback)) return await new Response(JSON.stringify(foundCallback))
222
+ if (isObject(foundCallback) && foundCallback.status) {
223
+ if (foundCallback.status === 401) {
224
+ const { status, ...rest } = foundCallback
225
+
226
+ return await new Response(JSON.stringify(rest), {
227
+ headers: {
228
+ 'Content-Type': 'json',
229
+ 'Access-Control-Allow-Origin': '*',
230
+ 'Access-Control-Allow-Headers': '*',
231
+ },
232
+ status: 401,
233
+ })
234
+ }
235
+
236
+ if (foundCallback.status === 403) {
237
+ const { status, ...rest } = foundCallback
238
+
239
+ return await new Response(JSON.stringify(rest), {
240
+ headers: {
241
+ 'Content-Type': 'json',
242
+ 'Access-Control-Allow-Origin': '*',
243
+ 'Access-Control-Allow-Headers': '*',
244
+ },
245
+ status: 403,
246
+ })
247
+ }
248
+
249
+ if (foundCallback.status === 422) {
250
+ const { status, ...rest } = foundCallback
251
+
252
+ return await new Response(JSON.stringify(rest), {
253
+ headers: {
254
+ 'Content-Type': 'json',
255
+ 'Access-Control-Allow-Origin': '*',
256
+ 'Access-Control-Allow-Headers': '*',
257
+ },
258
+ status: 422,
259
+ })
260
+ }
261
+
262
+ if (foundCallback.status === 500) {
263
+ const { status, ...rest } = foundCallback
264
+
265
+ return await new Response(JSON.stringify(rest), {
266
+ headers: { 'Content-Type': 'json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*' },
267
+ status: 500,
268
+ })
269
+ }
270
+ }
271
+
272
+ if (isObject(foundCallback)) {
273
+ return await new Response(JSON.stringify(foundCallback), {
274
+ headers: {
275
+ 'Content-Type': 'json',
276
+ 'Access-Control-Allow-Origin': '*',
277
+ 'Access-Control-Allow-Headers': '*',
278
+ },
279
+ status: 200,
280
+ })
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,104 @@ 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 requestPath = path.frameworkPath(`requests/${modelName}Request.ts`)
309
+ const requestImport = await import(requestPath)
310
+ const requestInstance = requestImport.request
311
+
312
+ if (requestInstance) {
313
+ requestInstance.addQuery(url)
165
314
  }
166
315
  }
167
316
 
168
- if (!isObjectNotEmpty(url.searchParams)) {
169
- RequestParam.addQuery(url);
317
+ RequestParam.addQuery(url)
318
+ }
319
+
320
+ async function addBody(params: any) {
321
+ const modelFiles = glob.sync(path.userModelsPath('*.ts'))
322
+
323
+ for (const modelFile of modelFiles) {
324
+ const model = (await import(modelFile)).default
325
+ const modelName = getModelName(model, modelFile)
326
+ const requestPath = path.frameworkPath(`requests/${modelName}Request.ts`)
327
+ const requestImport = await import(requestPath)
328
+ const requestInstance = requestImport.request
329
+
330
+ if (requestInstance) {
331
+ requestInstance.addBodies(JSON.parse(params))
332
+ }
170
333
  }
334
+
335
+ RequestParam.addBodies(JSON.parse(params))
171
336
  }
172
337
 
173
338
  async function addRouteParam(param: RouteParam): Promise<void> {
174
- const modelFiles = glob.sync(path.userModelsPath('*.ts'));
339
+ const modelFiles = glob.sync(path.userModelsPath('*.ts'))
175
340
 
176
341
  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];
342
+ const model = (await import(modelFile)).default as Model
343
+ const modelName = getModelName(model, modelFile)
344
+ const requestPath = path.frameworkPath(`requests/${modelName}Request.ts`)
345
+ const requestImport = await import(requestPath)
346
+ const requestInstance = requestImport.request
183
347
 
184
348
  if (requestInstance) {
185
- requestInstance.addParam(param);
349
+ requestInstance.addParam(param)
186
350
  }
187
351
  }
188
352
 
189
353
  RequestParam.addParam(param)
190
354
  }
191
355
 
192
- function executeMiddleware(route: Route): void {
356
+ async function addHeaders(headers: Headers): Promise<void> {
357
+ const modelFiles = glob.sync(path.userModelsPath('*.ts'))
358
+
359
+ for (const modelFile of modelFiles) {
360
+ const model = (await import(modelFile)).default as Model
361
+ const modelName = getModelName(model, modelFile)
362
+ const requestPath = path.frameworkPath(`requests/${modelName}Request.ts`)
363
+ const requestImport = await import(requestPath)
364
+ const requestInstance = requestImport.request
365
+
366
+ if (requestInstance) {
367
+ requestInstance.addHeaders(headers)
368
+ }
369
+ }
370
+
371
+ RequestParam.addHeaders(headers)
372
+ }
373
+
374
+ async function executeMiddleware(route: Route): Promise<any> {
193
375
  const { middleware = null } = route
194
376
 
195
377
  if (middleware && middlewares && isObjectNotEmpty(middlewares)) {
196
378
  // let middlewareItem: MiddlewareOptions
197
379
  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.
380
+ const middlewarePath = path.userMiddlewarePath(`${middleware}.ts`)
381
+
382
+ const middlewareInstance = (await import(middlewarePath)).default
383
+
384
+ try {
385
+ await middlewareInstance.handle()
386
+ } catch (error: any) {
387
+ return error
388
+ }
204
389
  } 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
- })
390
+ for (const middlewareElement of middleware) {
391
+ const middlewarePath = path.userMiddlewarePath(`${middlewareElement}.ts`)
392
+
393
+ const middlewareInstance = (await import(middlewarePath)).default
394
+
395
+ try {
396
+ await middlewareInstance.handle()
397
+ } catch (error: any) {
398
+ return error
399
+ }
400
+ }
214
401
  }
215
402
  }
216
403
  }