@occultist/occultist 0.0.11 → 0.0.12
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/dist/actions/actionSets.d.ts +7 -2
- package/dist/actions/actionSets.js +67 -9
- package/dist/actions/actions.d.ts +9 -8
- package/dist/actions/actions.js +11 -11
- package/dist/actions/context.d.ts +5 -1
- package/dist/actions/context.js +4 -0
- package/dist/actions/core.d.ts +8 -5
- package/dist/actions/core.js +20 -9
- package/dist/actions/route.d.ts +39 -0
- package/dist/actions/route.js +166 -0
- package/dist/actions/types.d.ts +3 -3
- package/dist/actions/writer.d.ts +1 -1
- package/dist/actions/writer.js +1 -3
- package/dist/cache/cache.d.ts +4 -3
- package/dist/cache/cache.js +10 -8
- package/dist/mod.d.ts +1 -1
- package/dist/mod.js +1 -1
- package/dist/registry.d.ts +53 -18
- package/dist/registry.js +60 -38
- package/dist/request.js +1 -2
- package/dist/scopes.d.ts +5 -4
- package/dist/scopes.js +17 -14
- package/dist/scopes.test.js +1 -1
- package/dist/types.d.ts +24 -19
- package/dist/utils/getRequestBodyValues.js +2 -0
- package/dist/utils/getRequestIRIValues.js +6 -9
- package/lib/actions/actionSets.ts +95 -11
- package/lib/actions/actions.ts +17 -17
- package/lib/actions/context.ts +7 -1
- package/lib/actions/core.ts +31 -11
- package/lib/actions/route.ts +202 -0
- package/lib/actions/types.ts +4 -3
- package/lib/actions/writer.ts +2 -4
- package/lib/cache/cache.ts +10 -7
- package/lib/mod.ts +1 -1
- package/lib/registry.ts +117 -52
- package/lib/request.ts +1 -2
- package/lib/scopes.test.ts +1 -1
- package/lib/scopes.ts +32 -23
- package/lib/types.ts +29 -24
- package/lib/utils/getRequestBodyValues.ts +2 -0
- package/lib/utils/getRequestIRIValues.ts +11 -14
- package/package.json +1 -1
- package/lib/actions/path.test.ts +0 -15
- package/lib/actions/path.ts +0 -99
- package/lib/registry.test.ts +0 -174
- package/lib/utils/normalizeURL.ts +0 -15
|
@@ -9,7 +9,7 @@ export type ActionAcceptMatch = {
|
|
|
9
9
|
type: 'match';
|
|
10
10
|
action: ImplementedAction;
|
|
11
11
|
contentType?: string;
|
|
12
|
-
|
|
12
|
+
languageTag?: string;
|
|
13
13
|
encoding?: string;
|
|
14
14
|
};
|
|
15
15
|
export type ActionMatchResult = UnsupportedContentTypeMatch | ActionAcceptMatch;
|
|
@@ -18,6 +18,11 @@ export type ActionMatchResult = UnsupportedContentTypeMatch | ActionAcceptMatch;
|
|
|
18
18
|
*/
|
|
19
19
|
export declare class ActionSet {
|
|
20
20
|
#private;
|
|
21
|
-
constructor(rootIRI: string, method: string, path: string, meta: ActionCore[]);
|
|
21
|
+
constructor(rootIRI: string, method: string, path: string, meta: ActionCore[], reverseExtensions: Map<string, string>);
|
|
22
|
+
/**
|
|
23
|
+
* @param method HTTP method to match against.
|
|
24
|
+
* @param path The pathname of the request.
|
|
25
|
+
* @param accept The accept cache of th erequest.
|
|
26
|
+
*/
|
|
22
27
|
matches(method: string, path: string, accept: Accept): null | ActionMatchResult;
|
|
23
28
|
}
|
|
@@ -8,42 +8,100 @@ export class ActionSet {
|
|
|
8
8
|
#method;
|
|
9
9
|
#urlPattern;
|
|
10
10
|
#contentTypeActionMap;
|
|
11
|
+
#extensionMap = new Map();
|
|
11
12
|
#ctc;
|
|
12
|
-
|
|
13
|
+
#autoLanguageTags;
|
|
14
|
+
constructor(rootIRI, method, path, meta, reverseExtensions) {
|
|
13
15
|
this.#rootIRI = rootIRI;
|
|
14
16
|
this.#method = method;
|
|
17
|
+
[
|
|
18
|
+
this.#contentTypeActionMap,
|
|
19
|
+
this.#extensionMap,
|
|
20
|
+
this.#ctc,
|
|
21
|
+
this.#autoLanguageTags,
|
|
22
|
+
] = this.#process(meta, reverseExtensions);
|
|
23
|
+
if (this.#extensionMap.size > 0) {
|
|
24
|
+
path += ':auto1(\\.[\\w\\-]+)?';
|
|
25
|
+
// language tags are only enabled if file extensions are
|
|
26
|
+
if (this.#autoLanguageTags) {
|
|
27
|
+
path += ':auto2(\\.[a-zA-Z0-9\\-]+)?';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
15
30
|
this.#urlPattern = makeURLPattern(path, rootIRI);
|
|
16
|
-
[this.#contentTypeActionMap, this.#ctc] = this.#process(meta);
|
|
17
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* @param method HTTP method to match against.
|
|
34
|
+
* @param path The pathname of the request.
|
|
35
|
+
* @param accept The accept cache of th erequest.
|
|
36
|
+
*/
|
|
18
37
|
matches(method, path, accept) {
|
|
19
38
|
if (method !== this.#method) {
|
|
20
39
|
return null;
|
|
21
40
|
}
|
|
22
|
-
|
|
41
|
+
const res = this.#urlPattern.exec(path, this.#rootIRI);
|
|
42
|
+
if (res == null) {
|
|
23
43
|
return null;
|
|
24
44
|
}
|
|
25
|
-
|
|
45
|
+
let contentType;
|
|
46
|
+
let languageTag;
|
|
47
|
+
if (res.pathname.groups.auto1 != null && res.pathname.groups.auto2 != null) {
|
|
48
|
+
languageTag = res.pathname.groups.auto1.replace('.', '');
|
|
49
|
+
const fileExtension = res.pathname.groups.auto2.replace('.', '');
|
|
50
|
+
contentType = this.#extensionMap.get(fileExtension);
|
|
51
|
+
if (contentType == null)
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
else if (res.pathname.groups.auto1 != null || res.pathname.groups.auto2 != null) {
|
|
55
|
+
const autoParam = res.pathname.groups.auto1 ?? res.pathname.groups.auto2;
|
|
56
|
+
const fileExtension = autoParam.replace('.', '');
|
|
57
|
+
contentType = this.#extensionMap.get(fileExtension);
|
|
58
|
+
if (contentType == null)
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (contentType == null) {
|
|
62
|
+
contentType = accept.negotiate(this.#ctc);
|
|
63
|
+
}
|
|
64
|
+
if (contentType == null)
|
|
65
|
+
return null;
|
|
26
66
|
const action = this.#contentTypeActionMap.get(contentType);
|
|
27
|
-
if (
|
|
67
|
+
if (action != null) {
|
|
28
68
|
return {
|
|
29
69
|
type: 'match',
|
|
30
70
|
action,
|
|
31
71
|
contentType,
|
|
72
|
+
languageTag,
|
|
32
73
|
};
|
|
33
74
|
}
|
|
34
75
|
return null;
|
|
35
76
|
}
|
|
36
|
-
#process(meta) {
|
|
77
|
+
#process(meta, reverseExtensions) {
|
|
78
|
+
let autoLanguageTags = false;
|
|
37
79
|
const contentTypes = [];
|
|
38
80
|
const contentTypeActionMap = new Map();
|
|
39
|
-
|
|
81
|
+
const extensionMap = new Map();
|
|
82
|
+
let l1 = meta.length;
|
|
83
|
+
for (let i = 0; i < l1; i++) {
|
|
40
84
|
const action = meta[i].action;
|
|
41
|
-
|
|
85
|
+
let l2 = action.contentTypes.length;
|
|
86
|
+
for (let j = 0; j < l2; j++) {
|
|
42
87
|
const contentType = action.contentTypes[j];
|
|
43
88
|
contentTypes.push(contentType);
|
|
44
89
|
contentTypeActionMap.set(contentType, action);
|
|
90
|
+
if (!autoLanguageTags && meta[i].autoLanguageTags) {
|
|
91
|
+
autoLanguageTags = true;
|
|
92
|
+
}
|
|
93
|
+
if (meta[i].autoFileExtensions &&
|
|
94
|
+
reverseExtensions.has(contentType) &&
|
|
95
|
+
!extensionMap.has(contentType)) {
|
|
96
|
+
extensionMap.set(reverseExtensions.get(contentType), contentType);
|
|
97
|
+
}
|
|
45
98
|
}
|
|
46
99
|
}
|
|
47
|
-
return [
|
|
100
|
+
return [
|
|
101
|
+
contentTypeActionMap,
|
|
102
|
+
extensionMap,
|
|
103
|
+
new ContentTypeCache(contentTypes),
|
|
104
|
+
autoLanguageTags,
|
|
105
|
+
];
|
|
48
106
|
}
|
|
49
107
|
}
|
|
@@ -3,6 +3,7 @@ import type { JSONLDContext, JSONObject, TypeDef } from "../jsonld.ts";
|
|
|
3
3
|
import type { Registry } from '../registry.ts';
|
|
4
4
|
import type { Scope } from "../scopes.ts";
|
|
5
5
|
import { MiddlewareRefs, type ActionCore } from "./core.ts";
|
|
6
|
+
import { Route } from './route.ts';
|
|
6
7
|
import type { ActionSpec, ContextState } from "./spec.ts";
|
|
7
8
|
import type { AuthMiddleware, AuthState, HandlerFn, HandlerMeta, HandlerObj, HandlerValue, HintArgs, ImplementedAction } from './types.ts';
|
|
8
9
|
import { type ResponseTypes } from './writer.ts';
|
|
@@ -15,13 +16,13 @@ export type DefineArgs<Term extends string = string, Spec extends ActionSpec = A
|
|
|
15
16
|
* after an action is defined.
|
|
16
17
|
*/
|
|
17
18
|
export declare class HandlerDefinition<State extends ContextState = ContextState, Auth extends AuthState = AuthState, Spec extends ActionSpec = ActionSpec> {
|
|
18
|
-
name
|
|
19
|
+
name?: string;
|
|
19
20
|
contentType: string;
|
|
20
21
|
handler: HandlerFn | HandlerValue;
|
|
21
22
|
meta: HandlerMeta;
|
|
22
23
|
action: ImplementedAction<State, Auth, Spec>;
|
|
23
24
|
cache: ReadonlyArray<CacheInstanceArgs>;
|
|
24
|
-
constructor(name: string, contentType: string, handler: HandlerFn | HandlerValue, meta: HandlerMeta, action: ImplementedAction<State, Auth, Spec>, actionMeta: ActionCore);
|
|
25
|
+
constructor(name: string | undefined, contentType: string, handler: HandlerFn | HandlerValue, meta: HandlerMeta, action: ImplementedAction<State, Auth, Spec>, actionMeta: ActionCore);
|
|
25
26
|
get [Symbol.toStringTag](): string;
|
|
26
27
|
}
|
|
27
28
|
export interface Handleable<State extends ContextState = ContextState, Auth extends AuthState = AuthState, Spec extends ActionSpec = ActionSpec> {
|
|
@@ -45,9 +46,9 @@ export declare class FinalizedAction<State extends ContextState = ContextState,
|
|
|
45
46
|
get term(): string | undefined;
|
|
46
47
|
get type(): string | undefined;
|
|
47
48
|
get typeDef(): TypeDef | undefined;
|
|
48
|
-
get name(): string;
|
|
49
|
+
get name(): string | undefined;
|
|
49
50
|
get template(): string;
|
|
50
|
-
get
|
|
51
|
+
get route(): Route;
|
|
51
52
|
get spec(): Spec;
|
|
52
53
|
get scope(): Scope | undefined;
|
|
53
54
|
get registry(): Registry;
|
|
@@ -84,9 +85,9 @@ export declare class DefinedAction<State extends ContextState = ContextState, Au
|
|
|
84
85
|
get term(): string | undefined;
|
|
85
86
|
get type(): string | undefined;
|
|
86
87
|
get typeDef(): TypeDef | undefined;
|
|
87
|
-
get name(): string;
|
|
88
|
+
get name(): string | undefined;
|
|
88
89
|
get template(): string;
|
|
89
|
-
get
|
|
90
|
+
get route(): Route;
|
|
90
91
|
get path(): string;
|
|
91
92
|
get spec(): Spec;
|
|
92
93
|
get scope(): Scope | undefined;
|
|
@@ -131,9 +132,9 @@ export declare class Action<State extends ContextState = ContextState, Auth exte
|
|
|
131
132
|
get term(): string | undefined;
|
|
132
133
|
get type(): string | undefined;
|
|
133
134
|
get typeDef(): TypeDef | undefined;
|
|
134
|
-
get name(): string;
|
|
135
|
+
get name(): string | undefined;
|
|
135
136
|
get template(): string;
|
|
136
|
-
get
|
|
137
|
+
get route(): Route;
|
|
137
138
|
get path(): string;
|
|
138
139
|
get spec(): ActionSpec;
|
|
139
140
|
get scope(): Scope | undefined;
|
package/dist/actions/actions.js
CHANGED
|
@@ -31,7 +31,7 @@ export class HandlerDefinition {
|
|
|
31
31
|
Object.freeze(this);
|
|
32
32
|
}
|
|
33
33
|
get [Symbol.toStringTag]() {
|
|
34
|
-
return `name=${this.name} contentType=${this.contentType}`;
|
|
34
|
+
return `name=${this.name ?? 'anon'} contentType=${this.contentType}`;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
export class FinalizedAction {
|
|
@@ -65,7 +65,7 @@ export class FinalizedAction {
|
|
|
65
65
|
return new FinalizedAction(typeDef, spec, core, arg3);
|
|
66
66
|
}
|
|
67
67
|
static async toJSONLD(action, scope) {
|
|
68
|
-
if (scope == null || action.typeDef == null) {
|
|
68
|
+
if (scope == null || action.typeDef == null || action.name == null) {
|
|
69
69
|
return null;
|
|
70
70
|
}
|
|
71
71
|
const apiSpec = await getPropertyValueSpecifications(action.spec);
|
|
@@ -103,8 +103,8 @@ export class FinalizedAction {
|
|
|
103
103
|
get template() {
|
|
104
104
|
return this.#core.uriTemplate;
|
|
105
105
|
}
|
|
106
|
-
get
|
|
107
|
-
return this.#core.
|
|
106
|
+
get route() {
|
|
107
|
+
return this.#core.route;
|
|
108
108
|
}
|
|
109
109
|
get spec() {
|
|
110
110
|
return this.#spec;
|
|
@@ -129,7 +129,7 @@ export class FinalizedAction {
|
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
url() {
|
|
132
|
-
return joinPaths(this.#core.registry.rootIRI, this.#core.
|
|
132
|
+
return joinPaths(this.#core.registry.rootIRI, this.#core.route.normalized);
|
|
133
133
|
}
|
|
134
134
|
/**
|
|
135
135
|
* Retrives the handler configured for the given content type.
|
|
@@ -239,11 +239,11 @@ export class DefinedAction {
|
|
|
239
239
|
get template() {
|
|
240
240
|
return this.#core.uriTemplate;
|
|
241
241
|
}
|
|
242
|
-
get
|
|
243
|
-
return this.#core.
|
|
242
|
+
get route() {
|
|
243
|
+
return this.#core.route;
|
|
244
244
|
}
|
|
245
245
|
get path() {
|
|
246
|
-
return this.#core.
|
|
246
|
+
return this.#core.route.normalized;
|
|
247
247
|
}
|
|
248
248
|
get spec() {
|
|
249
249
|
return this.#spec;
|
|
@@ -364,11 +364,11 @@ export class Action {
|
|
|
364
364
|
get template() {
|
|
365
365
|
return this.#core.uriTemplate;
|
|
366
366
|
}
|
|
367
|
-
get
|
|
368
|
-
return this.#core.
|
|
367
|
+
get route() {
|
|
368
|
+
return this.#core.route;
|
|
369
369
|
}
|
|
370
370
|
get path() {
|
|
371
|
-
return this.#core.
|
|
371
|
+
return this.#core.route.normalized;
|
|
372
372
|
}
|
|
373
373
|
get spec() {
|
|
374
374
|
return this.#spec;
|
|
@@ -6,6 +6,7 @@ import type { ResponseBody } from "./writer.ts";
|
|
|
6
6
|
export type CacheContextArgs<Auth extends AuthState = AuthState> = {
|
|
7
7
|
req: Request;
|
|
8
8
|
contentType: string;
|
|
9
|
+
languageTag: string | undefined;
|
|
9
10
|
public: boolean;
|
|
10
11
|
authKey?: string;
|
|
11
12
|
auth: Auth;
|
|
@@ -23,7 +24,8 @@ export declare class CacheContext<Auth extends AuthState = AuthState> {
|
|
|
23
24
|
req: Request;
|
|
24
25
|
method: string;
|
|
25
26
|
url: string;
|
|
26
|
-
contentType
|
|
27
|
+
contentType?: string;
|
|
28
|
+
languageTag?: string;
|
|
27
29
|
public: boolean;
|
|
28
30
|
authKey: string | null;
|
|
29
31
|
auth: Auth;
|
|
@@ -48,6 +50,7 @@ export declare class CacheContext<Auth extends AuthState = AuthState> {
|
|
|
48
50
|
export type ContextArgs<State extends ContextState = ContextState, Auth extends AuthState = AuthState, Spec extends ActionSpec = ActionSpec> = {
|
|
49
51
|
req: Request;
|
|
50
52
|
contentType: string;
|
|
53
|
+
languageTag: string | undefined;
|
|
51
54
|
public: boolean;
|
|
52
55
|
authKey?: string;
|
|
53
56
|
auth: Auth;
|
|
@@ -67,6 +70,7 @@ export declare class Context<State extends ContextState = ContextState, Auth ext
|
|
|
67
70
|
method: string;
|
|
68
71
|
url: string;
|
|
69
72
|
contentType: string;
|
|
73
|
+
languageTag?: string;
|
|
70
74
|
public: boolean;
|
|
71
75
|
authKey?: string;
|
|
72
76
|
auth: Auth;
|
package/dist/actions/context.js
CHANGED
|
@@ -16,6 +16,7 @@ export class CacheContext {
|
|
|
16
16
|
method;
|
|
17
17
|
url;
|
|
18
18
|
contentType;
|
|
19
|
+
languageTag;
|
|
19
20
|
public;
|
|
20
21
|
authKey;
|
|
21
22
|
auth;
|
|
@@ -30,6 +31,7 @@ export class CacheContext {
|
|
|
30
31
|
this.req = args.req;
|
|
31
32
|
this.url = args.req.url;
|
|
32
33
|
this.contentType = args.contentType;
|
|
34
|
+
this.languageTag = args.languageTag;
|
|
33
35
|
this.public = args.public;
|
|
34
36
|
this.authKey = args.authKey;
|
|
35
37
|
this.auth = args.auth;
|
|
@@ -80,6 +82,7 @@ export class Context {
|
|
|
80
82
|
method;
|
|
81
83
|
url;
|
|
82
84
|
contentType;
|
|
85
|
+
languageTag;
|
|
83
86
|
public = false;
|
|
84
87
|
authKey;
|
|
85
88
|
auth;
|
|
@@ -96,6 +99,7 @@ export class Context {
|
|
|
96
99
|
this.req = args.req;
|
|
97
100
|
this.url = args.req.url;
|
|
98
101
|
this.contentType = args.contentType;
|
|
102
|
+
this.languageTag = args.languageTag;
|
|
99
103
|
this.public = args.public;
|
|
100
104
|
this.authKey = args.authKey;
|
|
101
105
|
this.auth = args.auth;
|
package/dist/actions/core.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Registry } from '../registry.ts';
|
|
|
5
5
|
import type { Scope } from "../scopes.ts";
|
|
6
6
|
import { HandlerDefinition } from './actions.ts';
|
|
7
7
|
import { CacheContext, Context } from './context.ts';
|
|
8
|
-
import {
|
|
8
|
+
import { Route } from "./route.ts";
|
|
9
9
|
import type { ActionSpec, ContextState, FileValue, NextFn, TransformerFn } from './spec.ts';
|
|
10
10
|
import type { AuthMiddleware, AuthState, CacheHitHeader, HintArgs, ImplementedAction } from './types.ts';
|
|
11
11
|
import { type HTTPWriter, type ResponseTypes } from "./writer.ts";
|
|
@@ -27,13 +27,14 @@ export declare class MiddlewareRefs<State extends ContextState, Auth extends Aut
|
|
|
27
27
|
headers: Headers;
|
|
28
28
|
handler?: HandlerDefinition<State, Auth, Spec>;
|
|
29
29
|
contentType: string | null;
|
|
30
|
+
languageTag: string | null;
|
|
30
31
|
writer: HTTPWriter;
|
|
31
32
|
req: Request;
|
|
32
33
|
recordServerTiming: boolean;
|
|
33
34
|
prevTime: number | null;
|
|
34
35
|
serverTimes: string[];
|
|
35
36
|
cacheHitHeader: CacheHitHeader;
|
|
36
|
-
constructor(req: Request, writer: HTTPWriter, contentType: string | null, prevTime: number | null);
|
|
37
|
+
constructor(req: Request, writer: HTTPWriter, contentType: string | null, languageTag: string | null, prevTime: number | null);
|
|
37
38
|
recordServerTime(name: string): void;
|
|
38
39
|
}
|
|
39
40
|
/**
|
|
@@ -45,11 +46,11 @@ export declare class ActionCore<State extends ContextState = ContextState, Auth
|
|
|
45
46
|
rootIRI: string;
|
|
46
47
|
method: string;
|
|
47
48
|
isSafe: boolean;
|
|
48
|
-
name
|
|
49
|
+
name?: string;
|
|
49
50
|
uriTemplate: string;
|
|
50
51
|
public: boolean;
|
|
51
52
|
authKey?: string;
|
|
52
|
-
|
|
53
|
+
route: Route;
|
|
53
54
|
hints: HintArgs[];
|
|
54
55
|
transformers: Map<string, TransformerFn<JSONValue | FileValue, State, Spec>>;
|
|
55
56
|
scope?: Scope;
|
|
@@ -61,8 +62,10 @@ export declare class ActionCore<State extends ContextState = ContextState, Auth
|
|
|
61
62
|
cacheOccurance: 0 | 1;
|
|
62
63
|
auth?: AuthMiddleware<Auth>;
|
|
63
64
|
cache: CacheInstanceArgs[];
|
|
65
|
+
autoLanguageTags: boolean;
|
|
66
|
+
autoFileExtensions: boolean;
|
|
64
67
|
recordServerTiming: boolean;
|
|
65
|
-
constructor(rootIRI: string, method: string, name: string, uriTemplate: string, registry: Registry, writer: HTTPWriter, scope
|
|
68
|
+
constructor(rootIRI: string, method: string, name: string | undefined, uriTemplate: string, registry: Registry, writer: HTTPWriter, scope: Scope | undefined, autoLanguageTags: boolean, autoFileExtensions: boolean, recordServerTiming: boolean | undefined);
|
|
66
69
|
/**
|
|
67
70
|
* Called when the API is defined to compute all uncomputed values.
|
|
68
71
|
*/
|
package/dist/actions/core.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ProblemDetailsError } from "../errors.js";
|
|
|
3
3
|
import { processAction } from "../processAction.js";
|
|
4
4
|
import { joinPaths } from "../utils/joinPaths.js";
|
|
5
5
|
import { CacheContext, Context } from "./context.js";
|
|
6
|
-
import {
|
|
6
|
+
import { Route } from "./route.js";
|
|
7
7
|
const safeMethods = new Set([
|
|
8
8
|
'OPTIONS',
|
|
9
9
|
'HEAD',
|
|
@@ -29,16 +29,18 @@ export class MiddlewareRefs {
|
|
|
29
29
|
headers;
|
|
30
30
|
handler;
|
|
31
31
|
contentType;
|
|
32
|
+
languageTag;
|
|
32
33
|
writer;
|
|
33
34
|
req;
|
|
34
35
|
recordServerTiming;
|
|
35
36
|
prevTime;
|
|
36
37
|
serverTimes = [];
|
|
37
38
|
cacheHitHeader;
|
|
38
|
-
constructor(req, writer, contentType, prevTime) {
|
|
39
|
+
constructor(req, writer, contentType, languageTag, prevTime) {
|
|
39
40
|
this.req = req;
|
|
40
41
|
this.writer = writer;
|
|
41
42
|
this.contentType = contentType;
|
|
43
|
+
this.languageTag = languageTag;
|
|
42
44
|
this.prevTime = prevTime;
|
|
43
45
|
this.headers = new Headers();
|
|
44
46
|
}
|
|
@@ -59,12 +61,12 @@ export class MiddlewareRefs {
|
|
|
59
61
|
export class ActionCore {
|
|
60
62
|
rootIRI;
|
|
61
63
|
method;
|
|
62
|
-
isSafe
|
|
64
|
+
isSafe;
|
|
63
65
|
name;
|
|
64
66
|
uriTemplate;
|
|
65
67
|
public = false;
|
|
66
68
|
authKey;
|
|
67
|
-
|
|
69
|
+
route;
|
|
68
70
|
hints = [];
|
|
69
71
|
transformers = new Map();
|
|
70
72
|
scope;
|
|
@@ -76,8 +78,10 @@ export class ActionCore {
|
|
|
76
78
|
cacheOccurance = BeforeDefinition;
|
|
77
79
|
auth;
|
|
78
80
|
cache = [];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
autoLanguageTags;
|
|
82
|
+
autoFileExtensions;
|
|
83
|
+
recordServerTiming;
|
|
84
|
+
constructor(rootIRI, method, name, uriTemplate, registry, writer, scope, autoLanguageTags, autoFileExtensions, recordServerTiming) {
|
|
81
85
|
this.rootIRI = rootIRI;
|
|
82
86
|
this.method = method.toUpperCase();
|
|
83
87
|
this.isSafe = safeMethods.has(this.method);
|
|
@@ -86,7 +90,10 @@ export class ActionCore {
|
|
|
86
90
|
this.registry = registry;
|
|
87
91
|
this.writer = writer;
|
|
88
92
|
this.scope = scope;
|
|
89
|
-
this.
|
|
93
|
+
this.route = new Route(uriTemplate, rootIRI, autoLanguageTags, autoFileExtensions);
|
|
94
|
+
this.autoLanguageTags = autoLanguageTags;
|
|
95
|
+
this.autoFileExtensions = autoFileExtensions;
|
|
96
|
+
this.recordServerTiming = recordServerTiming ?? false;
|
|
90
97
|
}
|
|
91
98
|
/**
|
|
92
99
|
* Called when the API is defined to compute all uncomputed values.
|
|
@@ -134,7 +141,7 @@ export class ActionCore {
|
|
|
134
141
|
found = when(cacheCtx);
|
|
135
142
|
}
|
|
136
143
|
if (found) {
|
|
137
|
-
return new CacheDescriptor(contentType, this.action, req, this.cache[i]);
|
|
144
|
+
return new CacheDescriptor(contentType, cacheCtx.languageTag, this.action, req, this.cache[i]);
|
|
138
145
|
}
|
|
139
146
|
}
|
|
140
147
|
return null;
|
|
@@ -276,6 +283,7 @@ export class ActionCore {
|
|
|
276
283
|
refs.handlerCtx = new Context({
|
|
277
284
|
req: refs.req,
|
|
278
285
|
contentType: refs.contentType,
|
|
286
|
+
languageTag: refs.languageTag,
|
|
279
287
|
public: this.public && refs.authKey == null,
|
|
280
288
|
auth: refs.auth,
|
|
281
289
|
authKey: refs.authKey,
|
|
@@ -307,6 +315,7 @@ export class ActionCore {
|
|
|
307
315
|
refs.cacheCtx = new CacheContext({
|
|
308
316
|
req: refs.req,
|
|
309
317
|
contentType: refs.contentType,
|
|
318
|
+
languageTag: refs.languageTag,
|
|
310
319
|
public: this.public && refs.authKey == null,
|
|
311
320
|
cacheOperation: refs.cacheOperation,
|
|
312
321
|
auth: refs.auth,
|
|
@@ -340,7 +349,9 @@ export class ActionCore {
|
|
|
340
349
|
if (this.hints.length !== 0) {
|
|
341
350
|
const downstream = refs.next;
|
|
342
351
|
refs.next = async () => {
|
|
343
|
-
|
|
352
|
+
for (let i = 0; i < this.hints.length; i++) {
|
|
353
|
+
refs.writer.writeEarlyHints(this.hints[i]);
|
|
354
|
+
}
|
|
344
355
|
await downstream();
|
|
345
356
|
};
|
|
346
357
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type RouteMatchResult = {
|
|
2
|
+
path: Record<string, string>;
|
|
3
|
+
query: Record<string, undefined | string | string[]>;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Util class for handling paths defined using URI templates.
|
|
7
|
+
* https://datatracker.ietf.org/doc/html/rfc6570.
|
|
8
|
+
*
|
|
9
|
+
* URI Templates are used instead of URLPattern format to conform to
|
|
10
|
+
* JSON-ld https://schema.org/entryPoint needs. It is possible to add
|
|
11
|
+
* regex syntax which would end up in the generated URLPattern making
|
|
12
|
+
* it in conflict with the URITemplate. To be fixed...
|
|
13
|
+
*/
|
|
14
|
+
export declare class Route {
|
|
15
|
+
#private;
|
|
16
|
+
constructor(template: string, rootURL: string, autoLanguageTag: boolean, autoFileExtension: boolean);
|
|
17
|
+
get template(): string;
|
|
18
|
+
get regexp(): RegExp;
|
|
19
|
+
get pattern(): URLPattern;
|
|
20
|
+
/**
|
|
21
|
+
* A normalized form of the url template where arguments are named
|
|
22
|
+
* in order of appearance instead of with the provided names. This
|
|
23
|
+
* allows two paths to be compared based of their ability to match
|
|
24
|
+
* to the same request.
|
|
25
|
+
*/
|
|
26
|
+
get normalized(): string;
|
|
27
|
+
/**
|
|
28
|
+
* Matches a URL against this route and returns the
|
|
29
|
+
* path and query values.
|
|
30
|
+
*
|
|
31
|
+
* @param url The URL to match.
|
|
32
|
+
*/
|
|
33
|
+
match(url: string | URL): undefined | RouteMatchResult;
|
|
34
|
+
/**
|
|
35
|
+
* Returns the location of the given key if it is present in
|
|
36
|
+
* the path.
|
|
37
|
+
*/
|
|
38
|
+
locationOf(key: string): 'path' | 'query' | 'fragment' | null;
|
|
39
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { makeURLPattern } from "../utils/makeURLPattern.js";
|
|
2
|
+
const paramsRe = /((?<s>[^\{\}]+)|({(?<t>[\?\#\.])?(?<v>[^}]+)}))/g;
|
|
3
|
+
const languageTagReStr = '(?:\\.(?<languageTag>[a-zA-Z0-9][a-zA-Z0-9\\-]+))';
|
|
4
|
+
const fileExtensionReStr = '(?:\\.(?<fileExtension>[a-z][a-zA-Z0-9\\-]+))';
|
|
5
|
+
/**
|
|
6
|
+
* Util class for handling paths defined using URI templates.
|
|
7
|
+
* https://datatracker.ietf.org/doc/html/rfc6570.
|
|
8
|
+
*
|
|
9
|
+
* URI Templates are used instead of URLPattern format to conform to
|
|
10
|
+
* JSON-ld https://schema.org/entryPoint needs. It is possible to add
|
|
11
|
+
* regex syntax which would end up in the generated URLPattern making
|
|
12
|
+
* it in conflict with the URITemplate. To be fixed...
|
|
13
|
+
*/
|
|
14
|
+
export class Route {
|
|
15
|
+
#rootURL;
|
|
16
|
+
#template;
|
|
17
|
+
#regexp;
|
|
18
|
+
#pattern;
|
|
19
|
+
#normalized;
|
|
20
|
+
#pathKeys = new Set();
|
|
21
|
+
#queryKeys = new Set();
|
|
22
|
+
#fragmentKeys = new Set();
|
|
23
|
+
#autoLanguageTag;
|
|
24
|
+
#autoFileExtension;
|
|
25
|
+
constructor(template, rootURL, autoLanguageTag, autoFileExtension) {
|
|
26
|
+
this.#template = template;
|
|
27
|
+
this.#rootURL = rootURL;
|
|
28
|
+
this.#autoLanguageTag = autoLanguageTag;
|
|
29
|
+
this.#autoFileExtension = autoFileExtension;
|
|
30
|
+
[this.#pattern, this.#normalized] = this.#makePatterns();
|
|
31
|
+
}
|
|
32
|
+
get template() {
|
|
33
|
+
return this.#template;
|
|
34
|
+
}
|
|
35
|
+
get regexp() {
|
|
36
|
+
return this.#regexp;
|
|
37
|
+
}
|
|
38
|
+
get pattern() {
|
|
39
|
+
return this.#pattern;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A normalized form of the url template where arguments are named
|
|
43
|
+
* in order of appearance instead of with the provided names. This
|
|
44
|
+
* allows two paths to be compared based of their ability to match
|
|
45
|
+
* to the same request.
|
|
46
|
+
*/
|
|
47
|
+
get normalized() {
|
|
48
|
+
return this.#normalized;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Matches a URL against this route and returns the
|
|
52
|
+
* path and query values.
|
|
53
|
+
*
|
|
54
|
+
* @param url The URL to match.
|
|
55
|
+
*/
|
|
56
|
+
match(url) {
|
|
57
|
+
if (!url.toString().startsWith(this.#rootURL)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const url2 = new URL(url);
|
|
61
|
+
const match = this.#regexp.exec(url2.pathname);
|
|
62
|
+
if (match?.groups == null) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const path = Object.create(null);
|
|
66
|
+
const query = Object.create(null);
|
|
67
|
+
for (const [key, value] of Object.entries(match.groups)) {
|
|
68
|
+
path[key] = value;
|
|
69
|
+
}
|
|
70
|
+
for (const key of url2.searchParams.keys()) {
|
|
71
|
+
const value = url2.searchParams.getAll(key);
|
|
72
|
+
if (value.length > 1) {
|
|
73
|
+
query[key] = value;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
query[key] = value[0];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { path, query };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Returns the location of the given key if it is present in
|
|
83
|
+
* the path.
|
|
84
|
+
*/
|
|
85
|
+
locationOf(key) {
|
|
86
|
+
if (this.#pathKeys.has(key)) {
|
|
87
|
+
return 'path';
|
|
88
|
+
}
|
|
89
|
+
else if (this.#queryKeys.has(key)) {
|
|
90
|
+
return 'query';
|
|
91
|
+
}
|
|
92
|
+
else if (this.#fragmentKeys.has(key)) {
|
|
93
|
+
return 'fragment';
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
#makePatterns() {
|
|
98
|
+
let pattern = '';
|
|
99
|
+
let normalized = '';
|
|
100
|
+
// assign values to key location sets for quick querying
|
|
101
|
+
let match;
|
|
102
|
+
let foundQueryOrFragment = false;
|
|
103
|
+
let index = 0;
|
|
104
|
+
let template = '';
|
|
105
|
+
let regexpStr = '^';
|
|
106
|
+
while ((match = paramsRe.exec(this.#template))) {
|
|
107
|
+
const segment = match.groups?.s;
|
|
108
|
+
const type = match.groups?.t;
|
|
109
|
+
const value = match.groups?.v;
|
|
110
|
+
if (type != null && type !== '.' && !foundQueryOrFragment) {
|
|
111
|
+
foundQueryOrFragment = true;
|
|
112
|
+
if (this.#autoLanguageTag && this.#autoFileExtension) {
|
|
113
|
+
regexpStr += `(${languageTagReStr}?${fileExtensionReStr})?`;
|
|
114
|
+
template += '{.languageTag,fileExtension}';
|
|
115
|
+
}
|
|
116
|
+
else if (this.#autoFileExtension) {
|
|
117
|
+
regexpStr += fileExtensionReStr + '?';
|
|
118
|
+
template += '{.fileExtension}';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
template += match[0];
|
|
122
|
+
if (!foundQueryOrFragment && segment != null && type == null) {
|
|
123
|
+
regexpStr += segment;
|
|
124
|
+
normalized += segment;
|
|
125
|
+
pattern += segment;
|
|
126
|
+
}
|
|
127
|
+
if (value == null) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (type === '.' && !foundQueryOrFragment && value != null) {
|
|
131
|
+
index++;
|
|
132
|
+
regexpStr += `(\\.(?<${value}>[^\\/\\.]+))`;
|
|
133
|
+
pattern += `.:${value}`;
|
|
134
|
+
normalized += `.:value${index}`;
|
|
135
|
+
this.#pathKeys.add(value);
|
|
136
|
+
}
|
|
137
|
+
else if (!foundQueryOrFragment && value != null) {
|
|
138
|
+
index++;
|
|
139
|
+
regexpStr += `(?<${value}>[^\\/\\.]+)`;
|
|
140
|
+
pattern += `:${value}`;
|
|
141
|
+
normalized += `:value${index}`;
|
|
142
|
+
this.#pathKeys.add(value);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!foundQueryOrFragment) {
|
|
146
|
+
if (this.#autoLanguageTag && this.#autoFileExtension) {
|
|
147
|
+
regexpStr += `(${languageTagReStr}?${fileExtensionReStr})`;
|
|
148
|
+
template += '{.languageTag,fileExtension}';
|
|
149
|
+
this.#pathKeys.add('languageTag');
|
|
150
|
+
this.#pathKeys.add('fileExtension');
|
|
151
|
+
}
|
|
152
|
+
else if (this.#autoFileExtension) {
|
|
153
|
+
regexpStr += fileExtensionReStr;
|
|
154
|
+
template += '{.fileExtension}';
|
|
155
|
+
this.#pathKeys.add('fileExtension');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
regexpStr += '$';
|
|
159
|
+
this.#template = template;
|
|
160
|
+
this.#regexp = new RegExp(regexpStr);
|
|
161
|
+
return [
|
|
162
|
+
makeURLPattern(pattern, this.#rootURL),
|
|
163
|
+
normalized,
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
}
|