@orieljs/routing 0.1.0-alpha.1 → 0.1.0-alpha.11

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/NOTICE CHANGED
@@ -7,3 +7,10 @@ This product includes software developed by the Oriel project
7
7
  Oriel is licensed under the Apache License, Version 2.0 (see LICENSE).
8
8
  The Oriel name and logo are not covered by that license; see TRADEMARKS.md
9
9
  (https://github.com/orieljs/framework/blob/main/TRADEMARKS.md).
10
+
11
+ @orieljs/support (and @orieljs/framework, which bundles it) includes transliteration data from
12
+ voku/portable-ascii (https://github.com/voku/portable-ascii), Copyright (C) 2019 Lars Moelleken, MIT License.
13
+
14
+ @orieljs/notifications (and @orieljs/framework, which bundles it) includes the HTML and CSS of
15
+ Laravel's default notification mail (laravel/framework, https://github.com/laravel/framework),
16
+ Copyright (c) Taylor Otwell, MIT License.
package/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # @orieljs/routing
2
+
3
+ The Oriel router: it maps a URL and an HTTP method to a closure or a controller action, as Laravel's routing. It is part of `@orieljs/framework`, which every Oriel app already installs, and is re-exported from it. In an app, import it from `@orieljs/framework`; `@orieljs/routing` is also published on its own.
4
+
5
+ ```bash
6
+ npm install @orieljs/framework
7
+ ```
8
+
9
+ ```ts title="routes/web.ts"
10
+ import { Route, view } from '@orieljs/framework'
11
+ import { PostController } from '../app/Http/Controllers/PostController'
12
+
13
+ Route.get('/', () => view('welcome')).name('home')
14
+
15
+ Route.middleware(['auth', 'verified']).group(() => {
16
+ Route.get('/posts', [PostController, 'index']).name('posts.index')
17
+ Route.get('/posts/{post}', [PostController, 'show']).name('posts.show')
18
+ Route.post('/posts', [PostController, 'store']).middleware('throttle:10,1')
19
+ })
20
+ ```
21
+
22
+ `Route` has a method for each HTTP verb, plus `match()`, `any()`, `redirect()`, `view()`, `inertia()`, `resource()` and `fallback()`. A route takes a `name()`, `middleware()` and parameter constraints with `where()`, and groups share `middleware()`, `prefix()`, `name()` or `controller()`. A parameter named after a model, like `{post}`, receives the model, and `route('posts.show', post)` builds its URL.
23
+
24
+ Documentation: https://orieljs.vercel.app/docs/routing
25
+
26
+ Status: early alpha. APIs will change.
27
+
28
+ ## License
29
+
30
+ Oriel is open-source software licensed under the Apache License 2.0 (Apache-2.0; see `LICENSE` and `NOTICE`). The Oriel name and logo are trademarks; see `TRADEMARKS.md`.
31
+
32
+ Website: https://orieljs.vercel.app
package/TRADEMARKS.md ADDED
@@ -0,0 +1,33 @@
1
+ # Oriel trademark policy
2
+
3
+ The Oriel code is open source under the Apache License 2.0. The **Oriel name and logo** are not: the license covers
4
+ the code only (section 6 of the license says so), and the name and logo stay with the project. This page explains
5
+ what you can and can't do with them. The goal is simple: people should always be able to tell what is the real Oriel
6
+ and what isn't.
7
+
8
+ ## You can, without asking
9
+
10
+ - Use, change, ship and sell the code, as the license allows.
11
+ - Say that your app or package is "built with Oriel", "for Oriel" or "compatible with Oriel".
12
+ - Name a package `oriel-<something>` or `<something>-for-oriel` when it works with Oriel, as long as it is clear that
13
+ it is not official (for example, don't publish it under the `@orieljs` npm scope).
14
+ - Use the name and logo to write about Oriel, teach it, or link to it, in articles, talks, videos and courses.
15
+ - Use the unmodified logo to point to the project, as it is (no recolouring, cropping or
16
+ distortion).
17
+
18
+ ## You can't, without written permission
19
+
20
+ - Call a fork, a modified copy or a competing product "Oriel", or anything likely to be confused with it.
21
+ - Use the name or logo as the name, logo or domain of your own product, company or service.
22
+ - Suggest that the Oriel project made, endorses or supports your work when it doesn't.
23
+ - Sell merchandise with the name or logo.
24
+
25
+ ## Forks
26
+
27
+ Forks are welcome. Please give yours a different name and logo, keep the `LICENSE` and `NOTICE` files, and say that
28
+ it is based on Oriel.
29
+
30
+ ## Questions
31
+
32
+ Open an issue at https://github.com/orieljs/framework or email meeeet.dev@gmail.com. If you're unsure whether a use
33
+ is fine, ask; the answer is usually yes.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Route as Route$1, RouteAction, Class, MiddlewareReference, RouteRegistrar, ResourceOptions, ControllerAction, RouteClosure, Router, Container, MiddlewareClass, HttpRequest, UrlGenerator, OrielError, Middleware, ModelNotFoundError, Application } from '@orieljs/contracts';
1
+ import { Route as Route$1, RouteAction, Class, MiddlewareReference, RouteRegistrar, ResourceOptions, ControllerAction, RouteClosure, InvokableClass, Router, Container, MiddlewareClass, HttpRequest, UrlGenerator, OrielError, Middleware, ModelNotFoundError, Application, Next } from '@orieljs/contracts';
2
2
  import * as _orieljs_support from '@orieljs/support';
3
3
  import { ServiceProvider } from '@orieljs/support';
4
4
 
@@ -38,9 +38,22 @@ declare class RouteDefinition implements Route$1 {
38
38
  getName(): string | undefined;
39
39
  /** Appends, like Laravel's $action['as'] .= $name. */
40
40
  name(name: string): this;
41
- middleware(...middleware: MiddlewareReference[]): this;
41
+ /** middleware('auth', 'verified') or middleware(['auth', 'verified']). */
42
+ middleware(...middleware: Array<MiddlewareReference | readonly MiddlewareReference[]>): this;
42
43
  withoutMiddleware(...middleware: MiddlewareReference[]): this;
43
44
  where(parameter: string | Record<string, string>, pattern?: string): this;
45
+ /** Laravel's whereAlpha(): ASCII letters only. One parameter name or several. */
46
+ whereAlpha(parameters: string | readonly string[]): this;
47
+ /** Laravel's whereAlphaNumeric(): ASCII letters and digits. */
48
+ whereAlphaNumeric(parameters: string | readonly string[]): this;
49
+ /** Laravel's whereNumber(): digits only. */
50
+ whereNumber(parameters: string | readonly string[]): this;
51
+ /** Laravel's whereUlid(): 26 Crockford base-32 characters, the first at most 7. */
52
+ whereUlid(parameters: string | readonly string[]): this;
53
+ /** Laravel's whereUuid(): five groups of hex digits (8-4-4-4-12), any version. */
54
+ whereUuid(parameters: string | readonly string[]): this;
55
+ /** Laravel's whereIn(): one of the values. Unlike Laravel, each value is matched literally (`c++` is not a pattern). */
56
+ whereIn(parameters: string | readonly string[], values: ReadonlyArray<string | number>): this;
44
57
  gatherMiddleware(): MiddlewareReference[];
45
58
  excludedMiddleware(): MiddlewareReference[];
46
59
  wheres(): Record<string, string>;
@@ -78,17 +91,17 @@ declare class GroupRegistrar implements RouteRegistrar {
78
91
  constructor(router: GroupHost);
79
92
  prefix(prefix: string): this;
80
93
  name(prefix: string): this;
81
- middleware(...middleware: MiddlewareReference[]): this;
94
+ middleware(...middleware: Array<MiddlewareReference | readonly MiddlewareReference[]>): this;
82
95
  controller(controller: Class<unknown>): this;
83
96
  group(routes: () => void | Promise<void>): Promise<void>;
84
- get<C>(uri: string, action: ControllerAction<C> | RouteClosure | string): RouteDefinition;
85
- post<C>(uri: string, action: ControllerAction<C> | RouteClosure | string): RouteDefinition;
86
- put<C>(uri: string, action: ControllerAction<C> | RouteClosure | string): RouteDefinition;
87
- patch<C>(uri: string, action: ControllerAction<C> | RouteClosure | string): RouteDefinition;
88
- delete<C>(uri: string, action: ControllerAction<C> | RouteClosure | string): RouteDefinition;
89
- options<C>(uri: string, action: ControllerAction<C> | RouteClosure | string): RouteDefinition;
90
- any<C>(uri: string, action: ControllerAction<C> | RouteClosure | string): RouteDefinition;
91
- match<C>(methods: string[], uri: string, action: ControllerAction<C> | RouteClosure | string): RouteDefinition;
97
+ get<C>(uri: string, action: ControllerAction<C> | RouteClosure | InvokableClass | string): RouteDefinition;
98
+ post<C>(uri: string, action: ControllerAction<C> | RouteClosure | InvokableClass | string): RouteDefinition;
99
+ put<C>(uri: string, action: ControllerAction<C> | RouteClosure | InvokableClass | string): RouteDefinition;
100
+ patch<C>(uri: string, action: ControllerAction<C> | RouteClosure | InvokableClass | string): RouteDefinition;
101
+ delete<C>(uri: string, action: ControllerAction<C> | RouteClosure | InvokableClass | string): RouteDefinition;
102
+ options<C>(uri: string, action: ControllerAction<C> | RouteClosure | InvokableClass | string): RouteDefinition;
103
+ any<C>(uri: string, action: ControllerAction<C> | RouteClosure | InvokableClass | string): RouteDefinition;
104
+ match<C>(methods: string[], uri: string, action: ControllerAction<C> | RouteClosure | InvokableClass | string): RouteDefinition;
92
105
  resource(name: string, controller: Class<unknown>, options?: ResourceOptions): void;
93
106
  apiResource(name: string, controller: Class<unknown>, options?: ResourceOptions): void;
94
107
  }
@@ -100,14 +113,14 @@ declare class BaseRouter implements Router {
100
113
  protected readonly container?: Container | undefined;
101
114
  /** `container` is the application; request-time lookups use the current (request) container first. */
102
115
  constructor(container?: Container | undefined);
103
- get<C>(uri: string, action: ControllerAction<C> | RouteClosure | string): RouteDefinition;
104
- post<C>(uri: string, action: ControllerAction<C> | RouteClosure | string): RouteDefinition;
105
- put<C>(uri: string, action: ControllerAction<C> | RouteClosure | string): RouteDefinition;
106
- patch<C>(uri: string, action: ControllerAction<C> | RouteClosure | string): RouteDefinition;
107
- delete<C>(uri: string, action: ControllerAction<C> | RouteClosure | string): RouteDefinition;
108
- options<C>(uri: string, action: ControllerAction<C> | RouteClosure | string): RouteDefinition;
109
- any<C>(uri: string, action: ControllerAction<C> | RouteClosure | string): RouteDefinition;
110
- match<C>(methods: string[], uri: string, action: ControllerAction<C> | RouteClosure | string): RouteDefinition;
116
+ get<C>(uri: string, action: ControllerAction<C> | RouteClosure | InvokableClass | string): RouteDefinition;
117
+ post<C>(uri: string, action: ControllerAction<C> | RouteClosure | InvokableClass | string): RouteDefinition;
118
+ put<C>(uri: string, action: ControllerAction<C> | RouteClosure | InvokableClass | string): RouteDefinition;
119
+ patch<C>(uri: string, action: ControllerAction<C> | RouteClosure | InvokableClass | string): RouteDefinition;
120
+ delete<C>(uri: string, action: ControllerAction<C> | RouteClosure | InvokableClass | string): RouteDefinition;
121
+ options<C>(uri: string, action: ControllerAction<C> | RouteClosure | InvokableClass | string): RouteDefinition;
122
+ any<C>(uri: string, action: ControllerAction<C> | RouteClosure | InvokableClass | string): RouteDefinition;
123
+ match<C>(methods: string[], uri: string, action: ControllerAction<C> | RouteClosure | InvokableClass | string): RouteDefinition;
111
124
  /** Registers a route with the current group's prefix, name prefix, middleware and controller. GET implies HEAD. */
112
125
  addRoute(methods: readonly string[], uri: string, action: RouteHandler, options?: {
113
126
  fallback?: boolean;
@@ -117,13 +130,18 @@ declare class BaseRouter implements Router {
117
130
  permanentRedirect(from: string, to: string): RouteDefinition;
118
131
  /** Route.view('/about', 'pages.about', data): GET/HEAD; route parameters are merged into the view data. */
119
132
  view(uri: string, view: string, data?: Record<string, unknown>): RouteDefinition;
133
+ /**
134
+ * Route.inertia('/about', 'About', props): GET/HEAD; renders the page through the InertiaFactory (resolved when
135
+ * the route runs, so Routing needs no Inertia import). Route parameters are merged into the props.
136
+ */
137
+ inertia(uri: string, component: string, props?: Record<string, unknown>): RouteDefinition;
120
138
  resource(name: string, controller: Class<unknown>, options?: ResourceOptions): void;
121
139
  apiResource(name: string, controller: Class<unknown>, options?: ResourceOptions): void;
122
140
  /** Matches any GET path no other route matches. Always tried last. */
123
141
  fallback(action: RouteAction): RouteDefinition;
124
142
  prefix(prefix: string): GroupRegistrar;
125
143
  name(prefix: string): GroupRegistrar;
126
- middleware(...middleware: MiddlewareReference[]): GroupRegistrar;
144
+ middleware(...middleware: Array<MiddlewareReference | readonly MiddlewareReference[]>): GroupRegistrar;
127
145
  controller(controller: Class<unknown>): GroupRegistrar;
128
146
  /**
129
147
  * Laravel's Route::group(attributes, callback). A synchronous callback's attributes are popped before this
@@ -139,6 +157,12 @@ declare class BaseRouter implements Router {
139
157
  getPatternVersion(): number;
140
158
  getBinder(parameter: string): RouteBinder | undefined;
141
159
  getModel(parameter: string): Class<unknown> | undefined;
160
+ /**
161
+ * Binds a matched route's raw parameter values as a request to it would: Route.bind() callbacks, Route.model(),
162
+ * then implicit binding through the RouteModelResolver of `container`. A missing model throws
163
+ * RouteModelNotFoundError (404). Live uses it to check a page route's `can:` middleware again on each update.
164
+ */
165
+ substituteBindings(route: RouteDefinition, values: Readonly<Record<string, string | undefined>>, container: Container): Promise<Record<string, unknown>>;
142
166
  aliasMiddleware(name: string, middleware: MiddlewareClass): this;
143
167
  /** The middleware aliases (Laravel's Router::getMiddleware()). */
144
168
  getMiddleware(): Record<string, MiddlewareClass>;
@@ -186,6 +210,20 @@ declare class BaseUrlGenerator implements UrlGenerator {
186
210
  /** The Referer header, then the session's previous URL, then `fallback`, then the root (Laravel's order). */
187
211
  previous(fallback?: string): string;
188
212
  hasRoute(name: string): boolean;
213
+ /**
214
+ * Laravel's URL::signedRoute(): the route URL with its parameters sorted by name, then a `signature` query (the
215
+ * HMAC-SHA256 of that URL, keyed by APP_KEY). `expiration` (a Date, or seconds from now) adds `expires`.
216
+ * `absolute: false` signs the path only (checked by `signed:relative`).
217
+ */
218
+ signedRoute(name: string, parameters?: unknown, expiration?: Date | number, absolute?: boolean): string;
219
+ /** Laravel's URL::temporarySignedRoute(): a signed URL that expires at `expiration` (a Date, or seconds from now). */
220
+ temporarySignedRoute(name: string, expiration: Date | number, parameters?: unknown, absolute?: boolean): string;
221
+ /** The request URL carries a correct signature (current or previous APP_KEY) that hasn't expired. */
222
+ hasValidSignature(request: HttpRequest, absolute?: boolean): boolean;
223
+ /** The signature matches the URL without its `signature` query, in a constant-time comparison. */
224
+ hasCorrectSignature(request: HttpRequest, absolute?: boolean): boolean;
225
+ /** No `expires` query, or one that is still in the future. */
226
+ signatureHasNotExpired(request: HttpRequest): boolean;
189
227
  }
190
228
 
191
229
  /** A middleware reference after alias and group expansion: 'throttle:60,1' → { cls: Throttle, parameters: ['60', '1'] }. */
@@ -300,6 +338,21 @@ declare class RoutingError extends Error implements OrielError {
300
338
  readonly status = 500;
301
339
  readonly name = "RoutingError";
302
340
  }
341
+ /** A signed URL whose signature is missing, wrong or expired (Laravel's InvalidSignatureException). */
342
+ declare class InvalidUrlSignatureError extends Error implements OrielError {
343
+ readonly kind: "Http";
344
+ readonly status = 403;
345
+ readonly name = "InvalidUrlSignatureError";
346
+ constructor();
347
+ }
348
+ /** Too many requests in the window (Laravel's ThrottleRequestsException): 429 with Retry-After and X-RateLimit-*. */
349
+ declare class ThrottleRequestsError extends Error implements OrielError {
350
+ readonly headers: Record<string, string>;
351
+ readonly kind: "Http";
352
+ readonly status = 429;
353
+ readonly name = "ThrottleRequestsError";
354
+ constructor(headers?: Record<string, string>);
355
+ }
303
356
 
304
357
  type ResourceMethod = 'index' | 'create' | 'store' | 'show' | 'edit' | 'update' | 'destroy';
305
358
  interface ResourceRoute {
@@ -323,6 +376,29 @@ declare function toResponse(request: unknown, value: unknown): Promise<Response>
323
376
  */
324
377
  declare const Route: _orieljs_support.Facade<BaseRouter, object>;
325
378
 
379
+ /**
380
+ * Dispatched by the health route (`/up`) on every request (Laravel's DiagnosingHealth event). A listener checks
381
+ * something the application needs, such as the database or the cache, and throws when it fails: `/up` then answers
382
+ * 500 with "Application experiencing problems", and the exception is reported.
383
+ *
384
+ * Event.listen(DiagnosingHealth, async () => { await DB.select('select 1') })
385
+ */
386
+ declare class DiagnosingHealth {
387
+ }
388
+ interface HealthPage {
389
+ /** False when a check failed: the page says so and the status is 500 (default true). */
390
+ healthy?: boolean;
391
+ /** How long the request took, shown when given. */
392
+ milliseconds?: number;
393
+ /** The page title (default "Oriel"). */
394
+ appName?: string;
395
+ }
396
+ /**
397
+ * The health route's page (Laravel's health-up view): "Application up", or "Application experiencing problems" with
398
+ * status 500. Self-contained HTML with inline CSS: it loads nothing, so it answers even when the assets don't.
399
+ */
400
+ declare function healthResponse(page?: HealthPage): Response;
401
+
326
402
  /** A route file may export a function instead of (or as well as) calling the Route facade at the top level. */
327
403
  type RouteFileCallback = (router: Router) => void | Promise<void>;
328
404
  /**
@@ -331,15 +407,15 @@ type RouteFileCallback = (router: Router) => void | Promise<void>;
331
407
  * A default export that is a function is called with the router.
332
408
  */
333
409
  declare function loadRoutesFrom(app: Application, router: Router, file: string): Promise<void>;
334
- /** The health route's page (Laravel's `/up`): 200 as long as the application boots and handles requests. */
335
- declare function healthResponse(): Response;
336
410
 
337
411
  /**
338
- * Binds the Router and the UrlGenerator, and once the app has booted loads the files given to withRouting(): the health URI,
339
- * then routes/web.ts in the `web` middleware group, then routes/api.ts under the api prefix in the `api` group
340
- * (Laravel's ApplicationBuilder::withRouting).
412
+ * Binds the Router and the UrlGenerator, registers the `signed` and `throttle` middleware, and once the app has booted
413
+ * loads the files given to withRouting(): the health URI (which dispatches DiagnosingHealth), then the web files in the
414
+ * `web` middleware group, then the api files under the api prefix in the `api` group (Laravel's
415
+ * ApplicationBuilder::withRouting).
341
416
  */
342
417
  declare class RoutingServiceProvider extends ServiceProvider {
418
+ #private;
343
419
  register(): void;
344
420
  /**
345
421
  * The files load in a booted() callback, after every provider's boot() (Laravel's withRouting does the same), so
@@ -349,4 +425,33 @@ declare class RoutingServiceProvider extends ServiceProvider {
349
425
  boot(): void;
350
426
  }
351
427
 
352
- export { BaseRouter, BaseUrlGenerator, ControllerMethodNotFoundError, type GroupAttributes, type GroupHost, GroupRegistrar, MethodNotAllowedHttpError, MiddlewareNotFoundError, NotFoundHttpError, type ResolvedMiddleware, type ResourceMethod, type ResourceRoute, Route, type RouteAttributes, type RouteBinder, RouteDefinition, type RouteFileCallback, type RouteHandler, RouteModelNotFoundError, RouteNotDefinedError, type RouteStage, RoutingError, RoutingServiceProvider, type UriParameter, UrlGenerationError, buildQuery, compareRoutes, dispatchRoute, healthResponse, loadRoutesFrom, mergeGroup, normalizePath, normalizeUri, orderForBindings, parseParameters, renderError, resolveMiddleware, resourceRoutes, runMiddleware, substituteBindings, toResponse };
428
+ /**
429
+ * Laravel's ThrottleRequests (`throttle:<maxAttempts>,<decayMinutes>[,<prefix>]`): counts the client's requests
430
+ * (the signed-in user, else the IP) in a window and answers 429 once there are too many. Responses carry
431
+ * X-RateLimit-Limit and X-RateLimit-Remaining; the 429 adds Retry-After and X-RateLimit-Reset.
432
+ */
433
+ declare class ThrottleRequests implements Middleware {
434
+ handle(request: HttpRequest, next: Next, maxAttempts?: string, decayMinutes?: string, prefix?: string): Promise<Response>;
435
+ /** The signed-in user's identifier, else the client IP, hashed (Laravel's resolveRequestSignature()). */
436
+ protected resolveRequestSignature(request: HttpRequest): Promise<string>;
437
+ }
438
+
439
+ /**
440
+ * Laravel's SetCacheHeaders (`cache.headers:public;max_age=3600;s_maxage=86400;etag`): a GET or HEAD response gets
441
+ * the Cache-Control directives, and with `etag` an ETag from the body's MD5, answering 304 to a matching
442
+ * If-None-Match. A response that sets a cookie, or already says `no-store`, is left alone, so a CDN never stores a
443
+ * page made for one visitor.
444
+ */
445
+ declare class SetCacheHeaders implements Middleware {
446
+ handle(request: HttpRequest, next: Next, ...parameters: string[]): Promise<Response>;
447
+ }
448
+
449
+ /**
450
+ * Laravel's ValidateSignature (`signed`, `signed:relative`): the request URL must carry a valid, unexpired
451
+ * signature from URL.signedRoute()/temporarySignedRoute(), else 403. `relative` checks a path-only signature.
452
+ */
453
+ declare class ValidateSignature implements Middleware {
454
+ handle(request: HttpRequest, next: Next, ...parameters: string[]): Promise<Response>;
455
+ }
456
+
457
+ export { BaseRouter, BaseUrlGenerator, ControllerMethodNotFoundError, DiagnosingHealth, type GroupAttributes, type GroupHost, GroupRegistrar, type HealthPage, InvalidUrlSignatureError, MethodNotAllowedHttpError, MiddlewareNotFoundError, NotFoundHttpError, type ResolvedMiddleware, type ResourceMethod, type ResourceRoute, Route, type RouteAttributes, type RouteBinder, RouteDefinition, type RouteFileCallback, type RouteHandler, RouteModelNotFoundError, RouteNotDefinedError, type RouteStage, RoutingError, RoutingServiceProvider, SetCacheHeaders, ThrottleRequests, ThrottleRequestsError, type UriParameter, UrlGenerationError, ValidateSignature, buildQuery, compareRoutes, dispatchRoute, healthResponse, loadRoutesFrom, mergeGroup, normalizePath, normalizeUri, orderForBindings, parseParameters, renderError, resolveMiddleware, resourceRoutes, runMiddleware, substituteBindings, toResponse };