@occultist/occultist 0.0.1
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/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/accept.d.ts +41 -0
- package/dist/accept.js +110 -0
- package/dist/accept.test.d.ts +1 -0
- package/dist/accept.test.js +44 -0
- package/dist/action.test.d.ts +1 -0
- package/dist/action.test.js +1 -0
- package/dist/actions/actionSets.d.ts +23 -0
- package/dist/actions/actionSets.js +49 -0
- package/dist/actions/actions.d.ts +163 -0
- package/dist/actions/actions.js +436 -0
- package/dist/actions/context.d.ts +78 -0
- package/dist/actions/context.js +112 -0
- package/dist/actions/meta.d.ts +49 -0
- package/dist/actions/meta.js +177 -0
- package/dist/actions/path.d.ts +21 -0
- package/dist/actions/path.js +83 -0
- package/dist/actions/path.test.d.ts +1 -0
- package/dist/actions/path.test.js +9 -0
- package/dist/actions/spec.d.ts +214 -0
- package/dist/actions/spec.js +1 -0
- package/dist/actions/types.d.ts +112 -0
- package/dist/actions/types.js +2 -0
- package/dist/actions/writer.d.ts +27 -0
- package/dist/actions/writer.js +140 -0
- package/dist/actions/writer.test.d.ts +1 -0
- package/dist/actions/writer.test.js +42 -0
- package/dist/auth/types.d.ts +14 -0
- package/dist/auth/types.js +1 -0
- package/dist/cache/cache.d.ts +30 -0
- package/dist/cache/cache.js +220 -0
- package/dist/cache/etag.d.ts +17 -0
- package/dist/cache/etag.js +83 -0
- package/dist/cache/etag.test.d.ts +1 -0
- package/dist/cache/etag.test.js +91 -0
- package/dist/cache/memory.d.ts +12 -0
- package/dist/cache/memory.js +36 -0
- package/dist/cache/types.d.ts +175 -0
- package/dist/cache/types.js +4 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +54 -0
- package/dist/jsonld.d.ts +43 -0
- package/dist/jsonld.js +1 -0
- package/dist/makeTypeDefs.d.ts +27 -0
- package/dist/makeTypeDefs.js +70 -0
- package/dist/merge.d.ts +61 -0
- package/dist/merge.js +1 -0
- package/dist/mod.d.ts +14 -0
- package/dist/mod.js +14 -0
- package/dist/processAction.d.ts +15 -0
- package/dist/processAction.js +512 -0
- package/dist/registry.d.ts +88 -0
- package/dist/registry.js +314 -0
- package/dist/registry.test.d.ts +1 -0
- package/dist/registry.test.js +133 -0
- package/dist/request.d.ts +29 -0
- package/dist/request.js +118 -0
- package/dist/scopes.d.ts +35 -0
- package/dist/scopes.js +121 -0
- package/dist/scopes.test.d.ts +1 -0
- package/dist/scopes.test.js +55 -0
- package/dist/transformers/fileTransformer.d.ts +1 -0
- package/dist/transformers/fileTransformer.js +8 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.js +1 -0
- package/dist/utils/alwaysArray.d.ts +1 -0
- package/dist/utils/alwaysArray.js +9 -0
- package/dist/utils/contextBuilder.d.ts +9 -0
- package/dist/utils/contextBuilder.js +82 -0
- package/dist/utils/getActionContext.d.ts +7 -0
- package/dist/utils/getActionContext.js +48 -0
- package/dist/utils/getInternalName.d.ts +6 -0
- package/dist/utils/getInternalName.js +7 -0
- package/dist/utils/getParamLocation.d.ts +2 -0
- package/dist/utils/getParamLocation.js +6 -0
- package/dist/utils/getPropertyValueSpecifications.d.ts +2 -0
- package/dist/utils/getPropertyValueSpecifications.js +49 -0
- package/dist/utils/getRequestBodyValues.d.ts +11 -0
- package/dist/utils/getRequestBodyValues.js +122 -0
- package/dist/utils/getRequestIRIValues.d.ts +14 -0
- package/dist/utils/getRequestIRIValues.js +133 -0
- package/dist/utils/isBodyInit.d.ts +1 -0
- package/dist/utils/isBodyInit.js +21 -0
- package/dist/utils/isNil.d.ts +1 -0
- package/dist/utils/isNil.js +4 -0
- package/dist/utils/isObject.d.ts +6 -0
- package/dist/utils/isObject.js +6 -0
- package/dist/utils/isPopulatedObject.d.ts +5 -0
- package/dist/utils/isPopulatedObject.js +8 -0
- package/dist/utils/isPopulatedString.d.ts +1 -0
- package/dist/utils/isPopulatedString.js +4 -0
- package/dist/utils/joinPaths.d.ts +1 -0
- package/dist/utils/joinPaths.js +31 -0
- package/dist/utils/makeAppendProblemDetails.d.ts +14 -0
- package/dist/utils/makeAppendProblemDetails.js +26 -0
- package/dist/utils/makeURLPattern.d.ts +5 -0
- package/dist/utils/makeURLPattern.js +12 -0
- package/dist/utils/normalizeURL.d.ts +4 -0
- package/dist/utils/normalizeURL.js +11 -0
- package/dist/utils/parseSearchParams.d.ts +3 -0
- package/dist/utils/parseSearchParams.js +24 -0
- package/dist/utils/preferredMediaTypes.d.ts +42 -0
- package/dist/utils/preferredMediaTypes.js +149 -0
- package/dist/utils/urlToIRI.d.ts +1 -0
- package/dist/utils/urlToIRI.js +8 -0
- package/dist/utils/validateSpecValue.d.ts +1 -0
- package/dist/utils/validateSpecValue.js +1 -0
- package/dist/validators.d.ts +16 -0
- package/dist/validators.js +134 -0
- package/lib/accept.test.ts +55 -0
- package/lib/accept.ts +147 -0
- package/lib/action.test.ts +2 -0
- package/lib/actions/actionSets.ts +88 -0
- package/lib/actions/actions.ts +795 -0
- package/lib/actions/context.ts +170 -0
- package/lib/actions/meta.ts +251 -0
- package/lib/actions/path.test.ts +15 -0
- package/lib/actions/path.ts +99 -0
- package/lib/actions/spec.ts +545 -0
- package/lib/actions/types.ts +146 -0
- package/lib/actions/writer.test.ts +57 -0
- package/lib/actions/writer.ts +176 -0
- package/lib/auth/types.ts +22 -0
- package/lib/cache/cache.ts +291 -0
- package/lib/cache/etag.test.ts +122 -0
- package/lib/cache/etag.ts +106 -0
- package/lib/cache/memory.ts +52 -0
- package/lib/cache/types.ts +240 -0
- package/lib/errors.ts +66 -0
- package/lib/jsonld.ts +67 -0
- package/lib/makeTypeDefs.ts +138 -0
- package/lib/merge.ts +86 -0
- package/lib/mod.ts +14 -0
- package/lib/processAction.ts +690 -0
- package/lib/registry.test.ts +174 -0
- package/lib/registry.ts +455 -0
- package/lib/request.ts +153 -0
- package/lib/scopes.test.ts +70 -0
- package/lib/scopes.ts +178 -0
- package/lib/transformers/fileTransformer.ts +10 -0
- package/lib/types.ts +13 -0
- package/lib/utils/alwaysArray.ts +10 -0
- package/lib/utils/contextBuilder.ts +111 -0
- package/lib/utils/getActionContext.ts +76 -0
- package/lib/utils/getInternalName.ts +15 -0
- package/lib/utils/getParamLocation.ts +14 -0
- package/lib/utils/getPropertyValueSpecifications.ts +76 -0
- package/lib/utils/getRequestBodyValues.ts +155 -0
- package/lib/utils/getRequestIRIValues.ts +201 -0
- package/lib/utils/isBodyInit.ts +22 -0
- package/lib/utils/isNil.ts +4 -0
- package/lib/utils/isObject.ts +8 -0
- package/lib/utils/isPopulatedObject.ts +9 -0
- package/lib/utils/isPopulatedString.ts +4 -0
- package/lib/utils/joinPaths.ts +36 -0
- package/lib/utils/makeAppendProblemDetails.ts +57 -0
- package/lib/utils/makeURLPattern.ts +18 -0
- package/lib/utils/normalizeURL.ts +15 -0
- package/lib/utils/parseSearchParams.ts +36 -0
- package/lib/utils/preferredMediaTypes.ts +220 -0
- package/lib/utils/urlToIRI.ts +11 -0
- package/lib/utils/validateSpecValue.ts +0 -0
- package/lib/validators.ts +186 -0
- package/package.json +41 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type {HandlerDefinition} from "../mod.js";
|
|
2
|
+
import type {Registry} from "../registry.js";
|
|
3
|
+
import type {ActionPayload, ActionSpec, ContextState, ParsedIRIValues} from "./spec.js";
|
|
4
|
+
import type {ImplementedAction} from "./types.js";
|
|
5
|
+
import type {ResponseBody} from "./writer.js";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EditableContext {
|
|
9
|
+
hit: boolean = false;
|
|
10
|
+
etag?: string;
|
|
11
|
+
status?: number;
|
|
12
|
+
body?: ResponseBody;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type CacheContextArgs = {
|
|
16
|
+
req: Request;
|
|
17
|
+
url: string;
|
|
18
|
+
contentType: string;
|
|
19
|
+
public: boolean;
|
|
20
|
+
authKey?: string;
|
|
21
|
+
handler: HandlerDefinition;
|
|
22
|
+
params: ParsedIRIValues;
|
|
23
|
+
query: ParsedIRIValues;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Request context object.
|
|
29
|
+
*/
|
|
30
|
+
export class CacheContext {
|
|
31
|
+
#editable = new EditableContext();
|
|
32
|
+
req: Request;
|
|
33
|
+
method: string;
|
|
34
|
+
url: string;
|
|
35
|
+
contentType: string;
|
|
36
|
+
public: boolean = false
|
|
37
|
+
authKey?: string;
|
|
38
|
+
action: ImplementedAction;
|
|
39
|
+
registry: Registry;
|
|
40
|
+
params: ParsedIRIValues;
|
|
41
|
+
query: ParsedIRIValues;
|
|
42
|
+
headers: Headers = new Headers();
|
|
43
|
+
|
|
44
|
+
constructor(args: CacheContextArgs) {
|
|
45
|
+
this.req = args.req;
|
|
46
|
+
this.url = args.url;
|
|
47
|
+
this.contentType = args.contentType;
|
|
48
|
+
this.public = args.public;
|
|
49
|
+
this.authKey = args.authKey;
|
|
50
|
+
this.action = args.handler.action;
|
|
51
|
+
this.method = args.handler.action.method;
|
|
52
|
+
this.registry = args.handler.action.registry;
|
|
53
|
+
this.params = args.params;
|
|
54
|
+
this.query = args.query;
|
|
55
|
+
|
|
56
|
+
Object.freeze(this);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get hit(): boolean {
|
|
60
|
+
return this.#editable.hit;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
set hit(hit: boolean) {
|
|
64
|
+
this.#editable.hit = hit;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get status(): undefined | number {
|
|
68
|
+
return this.#editable.status;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
set status(status: number) {
|
|
72
|
+
this.#editable.status = status;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get body(): undefined | ResponseBody {
|
|
76
|
+
return this.#editable.body;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
set body(body: ResponseBody) {
|
|
80
|
+
this.#editable.body = body;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get etag(): undefined | string {
|
|
84
|
+
return this.#editable.etag;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
set etag(etag: string) {
|
|
88
|
+
this.#editable.etag = etag;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get [Symbol.toStringTag]() {
|
|
92
|
+
return `action=${this.action.name} method=${this.method} contentType=${this.contentType}`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
export type ContextArgs<
|
|
98
|
+
State extends ContextState = ContextState,
|
|
99
|
+
Spec extends ActionSpec = ActionSpec,
|
|
100
|
+
> = {
|
|
101
|
+
req: Request;
|
|
102
|
+
url: string;
|
|
103
|
+
contentType: string;
|
|
104
|
+
public: boolean;
|
|
105
|
+
authKey?: string;
|
|
106
|
+
handler: HandlerDefinition<State, Spec>;
|
|
107
|
+
params: ParsedIRIValues;
|
|
108
|
+
query: ParsedIRIValues;
|
|
109
|
+
payload: ActionPayload<Spec>;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Request context object.
|
|
114
|
+
*/
|
|
115
|
+
export class Context<
|
|
116
|
+
State extends ContextState = ContextState,
|
|
117
|
+
Spec extends ActionSpec = ActionSpec,
|
|
118
|
+
> {
|
|
119
|
+
#editable = new EditableContext();
|
|
120
|
+
req: Request;
|
|
121
|
+
method: string;
|
|
122
|
+
url: string;
|
|
123
|
+
contentType: string;
|
|
124
|
+
public: boolean = false
|
|
125
|
+
authKey?: string;
|
|
126
|
+
state: State = {} as State;
|
|
127
|
+
action: ImplementedAction<State, Spec>;
|
|
128
|
+
registry: Registry;
|
|
129
|
+
params: ParsedIRIValues;
|
|
130
|
+
query: ParsedIRIValues;
|
|
131
|
+
payload: ActionPayload<Spec>;
|
|
132
|
+
headers: Headers = new Headers();
|
|
133
|
+
|
|
134
|
+
constructor(args: ContextArgs<State, Spec>) {
|
|
135
|
+
this.req = args.req;
|
|
136
|
+
this.url = args.url;
|
|
137
|
+
this.contentType = args.contentType;
|
|
138
|
+
this.public = args.public;
|
|
139
|
+
this.authKey = args.authKey;
|
|
140
|
+
this.action = args.handler.action;
|
|
141
|
+
this.method = args.handler.action.method;
|
|
142
|
+
this.registry = args.handler.action.registry;
|
|
143
|
+
this.params = args.params;
|
|
144
|
+
this.query = args.query;
|
|
145
|
+
this.payload = args.payload;
|
|
146
|
+
|
|
147
|
+
Object.freeze(this);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get status(): undefined | number {
|
|
151
|
+
return this.#editable.status;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
set status(status: number) {
|
|
155
|
+
this.#editable.status = status;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get body(): undefined | ResponseBody {
|
|
159
|
+
return this.#editable.body;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
set body(body: ResponseBody) {
|
|
163
|
+
this.#editable.body = body;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
get [Symbol.toStringTag]() {
|
|
167
|
+
return `action=${this.action.name} method=${this.method} contentType=${this.contentType}`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import {CacheMiddleware} from '../cache/cache.js';
|
|
2
|
+
import {CacheEntryDescriptor, CacheInstanceArgs} from '../cache/types.js';
|
|
3
|
+
import {JSONValue} from '../jsonld.js';
|
|
4
|
+
import {processAction} from '../processAction.js';
|
|
5
|
+
import type {Registry} from '../registry.js';
|
|
6
|
+
import type {Scope} from "../scopes.js";
|
|
7
|
+
import {joinPaths} from '../utils/joinPaths.js';
|
|
8
|
+
import {HandlerDefinition} from './actions.js';
|
|
9
|
+
import {CacheContext, Context} from './context.js';
|
|
10
|
+
import {Path} from "./path.js";
|
|
11
|
+
import type {ActionSpec, ContextState, FileValue, NextFn, TransformerFn} from './spec.js';
|
|
12
|
+
import type {HintArgs, ImplementedAction} from './types.js';
|
|
13
|
+
import type {HTTPWriter, ResponseTypes} from "./writer.js";
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export const BeforeDefinition = 0;
|
|
17
|
+
export const AfterDefinition = 1;
|
|
18
|
+
|
|
19
|
+
const cacheMiddleware = new CacheMiddleware();
|
|
20
|
+
|
|
21
|
+
export class ActionMeta<
|
|
22
|
+
State extends ContextState = ContextState,
|
|
23
|
+
Spec extends ActionSpec = ActionSpec,
|
|
24
|
+
> {
|
|
25
|
+
rootIRI: string;
|
|
26
|
+
method: string;
|
|
27
|
+
name: string;
|
|
28
|
+
uriTemplate: string;
|
|
29
|
+
public: boolean = false;
|
|
30
|
+
authKey?: string;
|
|
31
|
+
path: Path;
|
|
32
|
+
hints: HintArgs[] = [];
|
|
33
|
+
transformers: Map<string, TransformerFn<JSONValue | FileValue, State, Spec>> = new Map();
|
|
34
|
+
scope?: Scope;
|
|
35
|
+
registry: Registry;
|
|
36
|
+
writer: HTTPWriter;
|
|
37
|
+
action?: ImplementedAction<State, Spec>;
|
|
38
|
+
acceptCache = new Set<string>();
|
|
39
|
+
compressBeforeCache: boolean = false;
|
|
40
|
+
cacheOccurance: 0 | 1 = BeforeDefinition;
|
|
41
|
+
cache: CacheInstanceArgs[] = [];
|
|
42
|
+
serverTiming: boolean = false;
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
rootIRI: string,
|
|
46
|
+
method: string,
|
|
47
|
+
name: string,
|
|
48
|
+
uriTemplate: string,
|
|
49
|
+
registry: Registry,
|
|
50
|
+
writer: HTTPWriter,
|
|
51
|
+
scope?: Scope,
|
|
52
|
+
) {
|
|
53
|
+
this.rootIRI = rootIRI;
|
|
54
|
+
this.method = method.toUpperCase()
|
|
55
|
+
this.name = name;
|
|
56
|
+
this.uriTemplate = joinPaths(rootIRI, uriTemplate);
|
|
57
|
+
this.registry = registry;
|
|
58
|
+
this.writer = writer;
|
|
59
|
+
this.scope = scope;
|
|
60
|
+
this.path = new Path(uriTemplate, rootIRI);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Called when the API is defined to compute all uncomputed values.
|
|
65
|
+
*/
|
|
66
|
+
finalize() {
|
|
67
|
+
this.#setAcceptCache();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async handleRequest({
|
|
71
|
+
startTime,
|
|
72
|
+
contentType,
|
|
73
|
+
language: _language,
|
|
74
|
+
encoding: _encoding,
|
|
75
|
+
url,
|
|
76
|
+
req,
|
|
77
|
+
writer,
|
|
78
|
+
spec,
|
|
79
|
+
handler,
|
|
80
|
+
}: {
|
|
81
|
+
startTime: number;
|
|
82
|
+
contentType?: string;
|
|
83
|
+
language?: string;
|
|
84
|
+
encoding?: string;
|
|
85
|
+
url: string;
|
|
86
|
+
req: Request;
|
|
87
|
+
writer: HTTPWriter;
|
|
88
|
+
spec?: Spec;
|
|
89
|
+
handler?: HandlerDefinition<State, Spec>,
|
|
90
|
+
}): Promise<ResponseTypes> {
|
|
91
|
+
const state: State = {} as State;
|
|
92
|
+
const headers = new Headers();
|
|
93
|
+
let ctx: CacheContext | Context<State, Spec>;
|
|
94
|
+
let cacheCtx: CacheContext;
|
|
95
|
+
let prevTime = startTime;
|
|
96
|
+
|
|
97
|
+
const serverTiming = (name: string) => {
|
|
98
|
+
const nextTime = performance.now();
|
|
99
|
+
const duration = nextTime - prevTime;
|
|
100
|
+
|
|
101
|
+
headers.append('Server-Timing', `${name};dur=${duration.toPrecision(2)}`);
|
|
102
|
+
prevTime = nextTime;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (this.serverTiming) serverTiming('enter');
|
|
106
|
+
|
|
107
|
+
// add auth check
|
|
108
|
+
if (this.hints.length !== 0) {
|
|
109
|
+
await Promise.all(
|
|
110
|
+
this.hints.map((hint) => writer.writeEarlyHints(hint))
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let next: NextFn = async () => {
|
|
115
|
+
if (typeof handler.handler === 'function') {
|
|
116
|
+
await handler.handler(ctx as Context<State, Spec>);
|
|
117
|
+
} else {
|
|
118
|
+
ctx.status = 200;
|
|
119
|
+
ctx.body = handler.handler;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (this.serverTiming) serverTiming('handle');
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
{
|
|
126
|
+
const upstream: NextFn = next;
|
|
127
|
+
next = async () => {
|
|
128
|
+
const res = await processAction<State, Spec>({
|
|
129
|
+
iri: url,
|
|
130
|
+
req,
|
|
131
|
+
spec: spec ?? {} as Spec,
|
|
132
|
+
state,
|
|
133
|
+
action: this.action,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
ctx = new Context<State, Spec>({
|
|
137
|
+
req,
|
|
138
|
+
url,
|
|
139
|
+
contentType,
|
|
140
|
+
public: this.public,
|
|
141
|
+
handler,
|
|
142
|
+
params: res.params,
|
|
143
|
+
query: res.query,
|
|
144
|
+
payload: res.payload,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (contentType != null) {
|
|
148
|
+
ctx.headers.set('Content-Type', contentType)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (this.serverTiming) serverTiming('payload');
|
|
152
|
+
|
|
153
|
+
await upstream();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (this.cache.length > 0) {
|
|
158
|
+
cacheCtx = new CacheContext({
|
|
159
|
+
req,
|
|
160
|
+
url,
|
|
161
|
+
contentType,
|
|
162
|
+
public: this.public,
|
|
163
|
+
handler,
|
|
164
|
+
params: {},
|
|
165
|
+
query: {},
|
|
166
|
+
});
|
|
167
|
+
const descriptors: CacheEntryDescriptor[] = this.cache.map(args => {
|
|
168
|
+
return {
|
|
169
|
+
contentType,
|
|
170
|
+
action: this.action as ImplementedAction,
|
|
171
|
+
request: req,
|
|
172
|
+
args,
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const upstream = next;
|
|
177
|
+
next = async () => {
|
|
178
|
+
await cacheMiddleware.use(
|
|
179
|
+
descriptors,
|
|
180
|
+
cacheCtx,
|
|
181
|
+
async () => {
|
|
182
|
+
// cache was not hit if in this function
|
|
183
|
+
await upstream();
|
|
184
|
+
|
|
185
|
+
// the cache middleware requires these values are set
|
|
186
|
+
cacheCtx.status = ctx.status;
|
|
187
|
+
|
|
188
|
+
if (ctx.body instanceof ReadableStream) {
|
|
189
|
+
const [a, b] = ctx.body.tee();
|
|
190
|
+
|
|
191
|
+
ctx.body = a;
|
|
192
|
+
cacheCtx.body = b;
|
|
193
|
+
} else {
|
|
194
|
+
cacheCtx.body = ctx.body;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const [key, value] of ctx.headers.entries()) {
|
|
198
|
+
cacheCtx.headers.set(key, value);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
await next();
|
|
207
|
+
|
|
208
|
+
if (cacheCtx?.hit) {
|
|
209
|
+
if (this.serverTiming) serverTiming('hit');
|
|
210
|
+
|
|
211
|
+
// set the ctx so the writer has access to the cached values.
|
|
212
|
+
ctx = cacheCtx;
|
|
213
|
+
} else if (cacheCtx?.etag != null) {
|
|
214
|
+
ctx.headers.set('Etag', cacheCtx.etag);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
writer.mergeHeaders(headers);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
writer.mergeHeaders(headers);
|
|
220
|
+
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
writer.writeHead(ctx.status ?? 200, ctx.headers);
|
|
225
|
+
|
|
226
|
+
if (ctx.body != null) {
|
|
227
|
+
writer.writeBody(ctx.body);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return writer.response();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#setAcceptCache(): void {
|
|
234
|
+
const action = this.action;
|
|
235
|
+
|
|
236
|
+
if (action == null) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.acceptCache.add('*/*');
|
|
241
|
+
|
|
242
|
+
for (const contentType of action.contentTypes) {
|
|
243
|
+
this.acceptCache.add(contentType);
|
|
244
|
+
this.acceptCache.add(contentType.replace(/[^/]+$/, '*'));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
get [Symbol.toStringTag]() {
|
|
249
|
+
return `[Meta ${this.name} ${this.uriTemplate}]`;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { Path } from "./path.js";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
test('URI template style path creates valid URL Pattern', () => {
|
|
7
|
+
const path = new Path(
|
|
8
|
+
'/api/recipes/{recipeUUID}/ingredients/{ingredientUUID}/units'
|
|
9
|
+
+ '{?search,page,pageSize}'
|
|
10
|
+
+ '{#sliceStart,sliceEnd}',
|
|
11
|
+
'https://example.com',
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
assert(path.pattern.test('https://example.com/api/recipes/xxxx-xxxx-xxxx-xxxx/ingredients/xxxx-xxxx-xxxx-xxxx/units'));
|
|
15
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { makeURLPattern } from "../utils/makeURLPattern.js";
|
|
2
|
+
|
|
3
|
+
const paramsRe = /((?<s>[^\{\}]+)|({(?<t>[\?\#])?(?<v>[^}]+)}))/g
|
|
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 class Path {
|
|
15
|
+
#rootIRI: string;
|
|
16
|
+
#template: string;
|
|
17
|
+
#pattern: URLPattern;
|
|
18
|
+
#normalized: string;
|
|
19
|
+
#paramKeys: Set<string> = new Set();
|
|
20
|
+
#queryKeys: Set<string> = new Set();
|
|
21
|
+
#fragmentKeys: Set<string> = new Set();
|
|
22
|
+
|
|
23
|
+
constructor(template: string, rootIRI: string) {
|
|
24
|
+
this.#template = template;
|
|
25
|
+
this.#rootIRI = rootIRI;
|
|
26
|
+
|
|
27
|
+
[this.#pattern, this.#normalized] = this.#makePatterns()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get template(): string {
|
|
31
|
+
return this.#template;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get pattern(): URLPattern {
|
|
35
|
+
return this.#pattern;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get normalized(): string {
|
|
39
|
+
return this.#normalized;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns the location of the given key if it is present in
|
|
44
|
+
* the path.
|
|
45
|
+
*/
|
|
46
|
+
locationOf(key: string): 'params' | 'query' | 'fragment' | null {
|
|
47
|
+
if (this.#paramKeys.has(key)) {
|
|
48
|
+
return 'params';
|
|
49
|
+
} else if (this.#queryKeys.has(key)) {
|
|
50
|
+
return 'query';
|
|
51
|
+
} else if (this.#fragmentKeys.has(key)) {
|
|
52
|
+
return 'fragment';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#makePatterns(): [URLPattern, string] {
|
|
59
|
+
const paramsRe = /((?<s>[^\{\}]+)|({(?<t>[\?\#])?(?<v>[^}]+)}))/g
|
|
60
|
+
let pattern: string = '';
|
|
61
|
+
let normalized: string = '';
|
|
62
|
+
|
|
63
|
+
// assign values to key location sets for quick querying
|
|
64
|
+
let match: RegExpExecArray | null;
|
|
65
|
+
let foundQueryOrFragment = false;
|
|
66
|
+
let index = 0;
|
|
67
|
+
|
|
68
|
+
while ((match = paramsRe.exec(this.#template))) {
|
|
69
|
+
const segment = match.groups?.s;
|
|
70
|
+
const type = match.groups?.t;
|
|
71
|
+
const value = match.groups?.v;
|
|
72
|
+
|
|
73
|
+
if (type != null) {
|
|
74
|
+
foundQueryOrFragment = true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!foundQueryOrFragment && segment != null && type == null) {
|
|
78
|
+
normalized += segment;
|
|
79
|
+
pattern += segment;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (value == null) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!foundQueryOrFragment && value != null) {
|
|
87
|
+
index++;
|
|
88
|
+
pattern += `:${value}`;
|
|
89
|
+
normalized += `:value${index}`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return [
|
|
94
|
+
makeURLPattern(pattern, this.#rootIRI),
|
|
95
|
+
normalized,
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
}
|