crelte 0.5.10 → 0.5.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/dist/init/client.d.ts +1 -8
- package/dist/init/client.d.ts.map +1 -1
- package/dist/init/client.js +3 -10
- package/dist/init/server.js +1 -1
- package/dist/plugins/Events.d.ts +6 -6
- package/dist/plugins/Events.d.ts.map +1 -1
- package/dist/queries/Queries.d.ts +30 -5
- package/dist/queries/Queries.d.ts.map +1 -1
- package/dist/queries/Queries.js +19 -2
- package/dist/queries/gql.d.ts +2 -2
- package/dist/queries/gql.d.ts.map +1 -1
- package/dist/queries/index.d.ts +47 -2
- package/dist/queries/index.d.ts.map +1 -1
- package/dist/queries/index.js +2 -2
- package/dist/queries/vars.d.ts +2 -0
- package/dist/queries/vars.d.ts.map +1 -1
- package/dist/queries/vars.js +10 -0
- package/dist/routing/router/BaseRouter.d.ts +2 -1
- package/dist/routing/router/BaseRouter.d.ts.map +1 -1
- package/dist/routing/router/BaseRouter.js +4 -2
- package/dist/routing/router/ClientRouter.d.ts.map +1 -1
- package/dist/routing/router/ClientRouter.js +3 -4
- package/dist/routing/router/Router.d.ts +2 -1
- package/dist/routing/router/Router.d.ts.map +1 -1
- package/dist/routing/router/Router.js +2 -1
- package/dist/routing/utils.d.ts +1 -0
- package/dist/routing/utils.d.ts.map +1 -1
- package/dist/routing/utils.js +1 -1
- package/dist/server/ServerRouter.d.ts.map +1 -1
- package/dist/server/ServerRouter.js +17 -7
- package/dist/server/queries/QueryGqlRoute.d.ts +28 -0
- package/dist/server/queries/QueryGqlRoute.d.ts.map +1 -0
- package/dist/server/queries/QueryGqlRoute.js +194 -0
- package/dist/server/queries/QueryHandleRoute.d.ts +12 -0
- package/dist/server/queries/QueryHandleRoute.d.ts.map +1 -0
- package/dist/server/queries/QueryHandleRoute.js +24 -0
- package/dist/server/queries/queries.d.ts.map +1 -1
- package/dist/server/queries/queries.js +42 -19
- package/dist/server/queries/routes.d.ts +7 -30
- package/dist/server/queries/routes.d.ts.map +1 -1
- package/dist/server/queries/routes.js +13 -199
- package/package.json +1 -1
- package/src/init/client.ts +3 -10
- package/src/init/server.ts +1 -1
- package/src/plugins/Events.ts +10 -6
- package/src/queries/Queries.ts +47 -14
- package/src/queries/gql.ts +2 -2
- package/src/queries/index.ts +71 -0
- package/src/queries/vars.ts +13 -0
- package/src/routing/router/BaseRouter.ts +4 -2
- package/src/routing/router/ClientRouter.ts +3 -4
- package/src/routing/router/Router.ts +2 -1
- package/src/routing/utils.ts +1 -1
- package/src/server/ServerRouter.ts +18 -7
- package/src/server/queries/QueryGqlRoute.ts +224 -0
- package/src/server/queries/QueryHandleRoute.ts +37 -0
- package/src/server/queries/queries.ts +57 -21
- package/src/server/queries/routes.ts +25 -229
package/src/plugins/Events.ts
CHANGED
|
@@ -40,7 +40,7 @@ export default class Events {
|
|
|
40
40
|
): () => void;
|
|
41
41
|
on(
|
|
42
42
|
ev: 'loadGlobalData',
|
|
43
|
-
fn: (cr: CrelteRequest) => Promise<any
|
|
43
|
+
fn: (cr: CrelteRequest) => Promise<any> | any,
|
|
44
44
|
): () => void;
|
|
45
45
|
on(
|
|
46
46
|
ev: 'loadEntry',
|
|
@@ -52,11 +52,11 @@ export default class Events {
|
|
|
52
52
|
): () => void;
|
|
53
53
|
on(
|
|
54
54
|
ev: 'afterLoadEntry',
|
|
55
|
-
fn: (cr: CrelteRequest) => Promise<any
|
|
55
|
+
fn: (cr: CrelteRequest) => Promise<any> | any,
|
|
56
56
|
): () => void;
|
|
57
57
|
on(
|
|
58
58
|
ev: 'loadData',
|
|
59
|
-
fn: (cr: CrelteRequest, entry: Entry) => Promise<any
|
|
59
|
+
fn: (cr: CrelteRequest, entry: Entry) => Promise<any> | any,
|
|
60
60
|
): () => void;
|
|
61
61
|
on(ev: 'beforeRender', fn: (cr: CrelteRequest) => void): () => void;
|
|
62
62
|
on(ev: string, fn: (...args: any[]) => any): () => void {
|
|
@@ -95,14 +95,18 @@ export default class Events {
|
|
|
95
95
|
* Trigger an event
|
|
96
96
|
*/
|
|
97
97
|
trigger(ev: 'beforeRequest', cr: CrelteRequest): (Promise<void> | void)[];
|
|
98
|
-
trigger(ev: 'loadGlobalData', cr: CrelteRequest): Promise<any>[];
|
|
98
|
+
trigger(ev: 'loadGlobalData', cr: CrelteRequest): (Promise<any> | any)[];
|
|
99
99
|
trigger(
|
|
100
100
|
ev: 'beforeQueryEntry',
|
|
101
101
|
cr: CrelteRequest,
|
|
102
102
|
vars: EntryQueryVars,
|
|
103
103
|
): (Promise<void> | void)[];
|
|
104
|
-
trigger(ev: 'afterLoadEntry', cr: CrelteRequest): Promise<any>[];
|
|
105
|
-
trigger(
|
|
104
|
+
trigger(ev: 'afterLoadEntry', cr: CrelteRequest): (Promise<any> | any)[];
|
|
105
|
+
trigger(
|
|
106
|
+
ev: 'loadData',
|
|
107
|
+
cr: CrelteRequest,
|
|
108
|
+
entry: Entry,
|
|
109
|
+
): (Promise<any> | any)[];
|
|
106
110
|
trigger(ev: 'beforeRender', cr: CrelteRequest, route: Route): void[];
|
|
107
111
|
trigger(ev: string, ...args: any[]): any[] {
|
|
108
112
|
const set = this.inner.get(ev);
|
package/src/queries/Queries.ts
CHANGED
|
@@ -17,17 +17,35 @@ export type QueriesOptions = {
|
|
|
17
17
|
/**
|
|
18
18
|
* A GraphQL query
|
|
19
19
|
*
|
|
20
|
-
* You should almost never
|
|
21
|
-
*
|
|
20
|
+
* **You should almost never**
|
|
21
|
+
*
|
|
22
|
+
* When importing a graphql file you will get a {@link NamedQuery}
|
|
23
|
+
* or use the {@link gql} template function to create
|
|
24
|
+
* an {@link InlineQuery}.
|
|
25
|
+
* Alternatively you can use the {@link namedQuery} function to create
|
|
26
|
+
* a {@link NamedQuery}.
|
|
27
|
+
*/
|
|
28
|
+
export type Query = InlineQuery | NamedQuery;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* You should never create this type directly. It is returned from
|
|
32
|
+
* the {@link gql} template function.
|
|
33
|
+
*/
|
|
34
|
+
export type InlineQuery = { path?: string; query: string };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Always create this object via the {@link namedQuery} function
|
|
38
|
+
*/
|
|
39
|
+
export type NamedQuery = { queryName: string };
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create a NamedQuery for the given server query name.
|
|
43
|
+
*
|
|
44
|
+
* Prefer importing a graphql file instead.
|
|
22
45
|
*/
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
query: string;
|
|
27
|
-
}
|
|
28
|
-
| {
|
|
29
|
-
queryName: string;
|
|
30
|
-
};
|
|
46
|
+
export function namedQuery(name: string): NamedQuery {
|
|
47
|
+
return { queryName: name };
|
|
48
|
+
}
|
|
31
49
|
|
|
32
50
|
/** Returns true if the passed object is a GraphQlQuery */
|
|
33
51
|
export function isQuery(obj: any): obj is Query {
|
|
@@ -102,8 +120,8 @@ export default class Queries {
|
|
|
102
120
|
*/
|
|
103
121
|
static new(
|
|
104
122
|
endpoint: string,
|
|
105
|
-
frontend: string,
|
|
106
|
-
ssrCache: SsrCache,
|
|
123
|
+
frontend: string | null = null,
|
|
124
|
+
ssrCache: SsrCache = new SsrCache(),
|
|
107
125
|
opts: QueriesOptions = {},
|
|
108
126
|
): Queries {
|
|
109
127
|
return new Queries(
|
|
@@ -113,6 +131,18 @@ export default class Queries {
|
|
|
113
131
|
);
|
|
114
132
|
}
|
|
115
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Create a new Queries instance that always intented to call an
|
|
136
|
+
* external GraphQl endpoint.
|
|
137
|
+
*/
|
|
138
|
+
static newExternal(endpoint: string, bearerToken?: string): Queries {
|
|
139
|
+
return new Queries(
|
|
140
|
+
new Inner(endpoint, null, new SsrCache(), { bearerToken }),
|
|
141
|
+
null,
|
|
142
|
+
null,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
116
146
|
/**
|
|
117
147
|
* Run a GraphQl Query
|
|
118
148
|
*
|
|
@@ -170,7 +200,7 @@ type InnerQueryOptions = {
|
|
|
170
200
|
|
|
171
201
|
class Inner {
|
|
172
202
|
endpoint: string;
|
|
173
|
-
frontend: string;
|
|
203
|
+
frontend: string | null;
|
|
174
204
|
ssrCache: SsrCache;
|
|
175
205
|
private listeners: Map<
|
|
176
206
|
string,
|
|
@@ -184,7 +214,7 @@ class Inner {
|
|
|
184
214
|
|
|
185
215
|
constructor(
|
|
186
216
|
endpoint: string,
|
|
187
|
-
frontend: string,
|
|
217
|
+
frontend: string | null,
|
|
188
218
|
ssrCache: SsrCache,
|
|
189
219
|
opts: QueriesOptions = {},
|
|
190
220
|
) {
|
|
@@ -264,6 +294,9 @@ class Inner {
|
|
|
264
294
|
let logName: string, url: URL;
|
|
265
295
|
|
|
266
296
|
if ('queryName' in query) {
|
|
297
|
+
if (!this.frontend)
|
|
298
|
+
throw new Error('only inline queries supported');
|
|
299
|
+
|
|
267
300
|
logName = `query (server: ${query.queryName})`;
|
|
268
301
|
url = new URL(this.frontend);
|
|
269
302
|
url.pathname = '/queries/' + query.queryName;
|
package/src/queries/gql.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { InlineQuery, isQuery } from './Queries.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Create a GraphQL query string with variables.
|
|
@@ -13,7 +13,7 @@ import { isQuery, Query } from './Queries.js';
|
|
|
13
13
|
export function gql(
|
|
14
14
|
strings: TemplateStringsArray | string[] | string,
|
|
15
15
|
...keys: unknown[]
|
|
16
|
-
):
|
|
16
|
+
): InlineQuery {
|
|
17
17
|
if (typeof strings === 'string') strings = [strings];
|
|
18
18
|
|
|
19
19
|
let query = '';
|
package/src/queries/index.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import Queries, {
|
|
2
|
+
InlineQuery,
|
|
2
3
|
isQuery,
|
|
4
|
+
namedQuery,
|
|
5
|
+
NamedQuery,
|
|
3
6
|
QueriesOptions,
|
|
4
7
|
Query,
|
|
5
8
|
QueryOptions,
|
|
6
9
|
} from '../queries/Queries.js';
|
|
10
|
+
import type CrelteServerRequest from '../server/CrelteServer.js';
|
|
7
11
|
import { gql } from './gql.js';
|
|
8
12
|
import QueryError, { QueryErrorResponse } from './QueryError.js';
|
|
9
13
|
import { QueryVar, ValidIf, vars, varsIdsEqual } from './vars.js';
|
|
@@ -13,6 +17,9 @@ export {
|
|
|
13
17
|
type QueriesOptions,
|
|
14
18
|
type QueryOptions,
|
|
15
19
|
type Query,
|
|
20
|
+
type InlineQuery as GqlQuery,
|
|
21
|
+
type NamedQuery,
|
|
22
|
+
namedQuery,
|
|
16
23
|
isQuery,
|
|
17
24
|
QueryError,
|
|
18
25
|
type QueryErrorResponse,
|
|
@@ -32,6 +39,8 @@ export type InferVariableTypes<T> = {
|
|
|
32
39
|
};
|
|
33
40
|
|
|
34
41
|
/**
|
|
42
|
+
* Defines when a query can safely be cached.
|
|
43
|
+
*
|
|
35
44
|
* #### Example
|
|
36
45
|
* ```ts
|
|
37
46
|
* import { vars, Caching } from 'crelte/queries';
|
|
@@ -48,3 +57,65 @@ export type InferVariableTypes<T> = {
|
|
|
48
57
|
export type Caching<
|
|
49
58
|
T extends Record<string, QueryVar<any>> = Record<string, QueryVar<any>>,
|
|
50
59
|
> = boolean | ((response: any, vars: InferVariableTypes<T>) => boolean);
|
|
60
|
+
|
|
61
|
+
/** use {@link Transfrom} */
|
|
62
|
+
export type TransformFn<
|
|
63
|
+
T extends Record<string, QueryVar<any>> = Record<string, QueryVar<any>>,
|
|
64
|
+
> = (
|
|
65
|
+
response: any,
|
|
66
|
+
vars: InferVariableTypes<T>,
|
|
67
|
+
) => void | any | Promise<void | any>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Transforms the query response before it is returned or cached.
|
|
71
|
+
*
|
|
72
|
+
* #### Example
|
|
73
|
+
* ```ts
|
|
74
|
+
* export const transform: Transform<typeof variables> = (response, vars) => {
|
|
75
|
+
* for (const entry of response.entries) {
|
|
76
|
+
* entry.title = entry.title.toUpperCase();
|
|
77
|
+
* }
|
|
78
|
+
* };
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export type Transform<
|
|
82
|
+
T extends Record<string, QueryVar<any>> = Record<string, QueryVar<any>>,
|
|
83
|
+
F extends TransformFn<T> = TransformFn<T>,
|
|
84
|
+
> = (response: any, vars: InferVariableTypes<T>) => Awaited<ReturnType<F>>;
|
|
85
|
+
|
|
86
|
+
/** use {@link Handle} */
|
|
87
|
+
export type HandleFn<
|
|
88
|
+
T extends Record<string, QueryVar<any>> = Record<string, QueryVar<any>>,
|
|
89
|
+
> = (csr: CrelteServerRequest, vars: InferVariableTypes<T>) => any;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Handles a query request.
|
|
93
|
+
*
|
|
94
|
+
* #### Example
|
|
95
|
+
* ```ts
|
|
96
|
+
* // queries/custom.ts
|
|
97
|
+
* import { vars, type Handle, gql, namedQuery } from 'crelte/queries';
|
|
98
|
+
*
|
|
99
|
+
* // It is good practice to have the query name inside the file
|
|
100
|
+
* export const customQuery = namedQuery('custom');
|
|
101
|
+
*
|
|
102
|
+
* export const variables = {
|
|
103
|
+
* name: vars.string(),
|
|
104
|
+
* };
|
|
105
|
+
*
|
|
106
|
+
* export const handle: Handle<typeof variables> = async (csr, vars) => {
|
|
107
|
+
* if (vars.name === 'demo') {
|
|
108
|
+
* throw new Response('not allowed', { status: 400 });
|
|
109
|
+
* }
|
|
110
|
+
*
|
|
111
|
+
* return { name: vars.name };
|
|
112
|
+
* };
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export type Handle<
|
|
116
|
+
T extends Record<string, QueryVar<any>> = Record<string, QueryVar<any>>,
|
|
117
|
+
F extends HandleFn<T> = HandleFn<T>,
|
|
118
|
+
> = (
|
|
119
|
+
csr: CrelteServerRequest,
|
|
120
|
+
vars: InferVariableTypes<T>,
|
|
121
|
+
) => Awaited<ReturnType<F>>;
|
package/src/queries/vars.ts
CHANGED
|
@@ -53,12 +53,14 @@ export class QueryVar<T = any> {
|
|
|
53
53
|
private name: string | null;
|
|
54
54
|
private type: 'any' | 'string' | 'number' | 'id' | 'ids';
|
|
55
55
|
private flagNullable: boolean;
|
|
56
|
+
private defaultValue: T | undefined;
|
|
56
57
|
private validIfFn: ValidIf<T>;
|
|
57
58
|
|
|
58
59
|
constructor() {
|
|
59
60
|
this.name = null;
|
|
60
61
|
this.type = 'any';
|
|
61
62
|
this.flagNullable = false;
|
|
63
|
+
this.defaultValue = undefined;
|
|
62
64
|
this.validIfFn = () => true;
|
|
63
65
|
}
|
|
64
66
|
|
|
@@ -87,6 +89,11 @@ export class QueryVar<T = any> {
|
|
|
87
89
|
return this as QueryVar<T | null>;
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
default(value: T): QueryVar<T> {
|
|
93
|
+
this.defaultValue = value;
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
90
97
|
/**
|
|
91
98
|
* Set a validation function for this variable
|
|
92
99
|
*
|
|
@@ -103,6 +110,8 @@ export class QueryVar<T = any> {
|
|
|
103
110
|
if (typeof v === 'undefined') v = null;
|
|
104
111
|
|
|
105
112
|
if (v === null) {
|
|
113
|
+
if (this.defaultValue !== undefined) return this.defaultValue;
|
|
114
|
+
|
|
106
115
|
if (!this.flagNullable)
|
|
107
116
|
throw new Error(`variable ${this.name} cannot be null`);
|
|
108
117
|
|
|
@@ -139,7 +148,11 @@ export class QueryVar<T = any> {
|
|
|
139
148
|
);
|
|
140
149
|
|
|
141
150
|
if (v.length <= 0) {
|
|
151
|
+
if (this.defaultValue !== undefined)
|
|
152
|
+
return this.defaultValue;
|
|
153
|
+
|
|
142
154
|
if (this.flagNullable) return null;
|
|
155
|
+
|
|
143
156
|
throw new Error(
|
|
144
157
|
`variable ${this.name} is not allowed to be empty`,
|
|
145
158
|
);
|
|
@@ -123,8 +123,10 @@ export default class BaseRouter {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
*
|
|
126
|
+
* Returns the default site tries to use a site which matches the preferred language
|
|
127
|
+
* else returns the primary site
|
|
127
128
|
*/
|
|
129
|
+
// todo check that the router uses the correct sites for each function
|
|
128
130
|
defaultSite(): Site {
|
|
129
131
|
return this.preferredSite() ?? this.primarySite();
|
|
130
132
|
}
|
|
@@ -233,7 +235,7 @@ export default class BaseRouter {
|
|
|
233
235
|
const req = this.targetToRequest(target, { origin: 'preload' });
|
|
234
236
|
const current = this.route.get();
|
|
235
237
|
|
|
236
|
-
// if the origin matches, the route will be able to be
|
|
238
|
+
// if the origin matches, the route will be able to be loaded
|
|
237
239
|
// so let's preload it
|
|
238
240
|
if (current && current.url.origin === req.url.origin) {
|
|
239
241
|
// todo i don't wan't to send a CrelteRequest?
|
|
@@ -202,11 +202,10 @@ export default class ClientRouter extends BaseRouter {
|
|
|
202
202
|
window.addEventListener('scroll', () => this.onScroll());
|
|
203
203
|
|
|
204
204
|
window.addEventListener('popstate', async e => {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
205
|
+
const req = this.targetToRequest(window.location.href, {
|
|
206
|
+
origin: 'pop',
|
|
207
|
+
});
|
|
208
208
|
req.z_fillFromState(e.state);
|
|
209
|
-
req.origin = 'pop';
|
|
210
209
|
|
|
211
210
|
// todo handle errors
|
|
212
211
|
this.handleRequest(req, () => {});
|
package/src/routing/utils.ts
CHANGED
|
@@ -5,7 +5,7 @@ export function trimSlashEnd(str: string) {
|
|
|
5
5
|
return str.endsWith('/') ? str.substring(0, str.length - 1) : str;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/** same as ?? but only for undefined */
|
|
9
9
|
export function orDef<T>(a: T | undefined, def: T): T {
|
|
10
10
|
return a === undefined ? def : a;
|
|
11
11
|
}
|
|
@@ -139,9 +139,7 @@ export default class ServerRouter {
|
|
|
139
139
|
this.endpointUrl,
|
|
140
140
|
this.frontendUrl,
|
|
141
141
|
new SsrCache(),
|
|
142
|
-
{
|
|
143
|
-
bearerToken: this.endpointToken,
|
|
144
|
-
},
|
|
142
|
+
{ bearerToken: this.endpointToken },
|
|
145
143
|
);
|
|
146
144
|
|
|
147
145
|
const csr = new CrelteServerRequest(nReq, {
|
|
@@ -152,12 +150,25 @@ export default class ServerRouter {
|
|
|
152
150
|
queries,
|
|
153
151
|
});
|
|
154
152
|
|
|
153
|
+
let resp: Response | null = null;
|
|
155
154
|
for (const handler of handlers) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
try {
|
|
156
|
+
const res = await handler(csr);
|
|
157
|
+
if (!res) continue;
|
|
158
|
+
|
|
159
|
+
resp = res;
|
|
160
|
+
} catch (e) {
|
|
161
|
+
if (!(e instanceof Response)) throw e;
|
|
162
|
+
|
|
163
|
+
resp = e;
|
|
160
164
|
}
|
|
165
|
+
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (resp) {
|
|
170
|
+
csr.z_finishResponse(resp);
|
|
171
|
+
return resp;
|
|
161
172
|
}
|
|
162
173
|
|
|
163
174
|
return null;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import CrelteServerRequest from '../CrelteServer.js';
|
|
2
|
+
import QueriesCaching from './QueriesCaching.js';
|
|
3
|
+
import { QueryVar, vars } from '../../queries/vars.js';
|
|
4
|
+
import { extractEntry } from '../../loadData/index.js';
|
|
5
|
+
import { calcKey } from '../../ssr/index.js';
|
|
6
|
+
import { CacheIfFn, newError, TransformFn, validateVars } from './routes.js';
|
|
7
|
+
|
|
8
|
+
export type QueryGqlArgs = {
|
|
9
|
+
vars: Record<string, QueryVar> | null;
|
|
10
|
+
cacheIfFn: CacheIfFn | null;
|
|
11
|
+
preventCaching: boolean;
|
|
12
|
+
transformFn: TransformFn | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// only internal
|
|
16
|
+
export default class QueryGqlRoute {
|
|
17
|
+
name: string;
|
|
18
|
+
query: string;
|
|
19
|
+
vars: Record<string, QueryVar> | null;
|
|
20
|
+
cacheIfFn: CacheIfFn | null;
|
|
21
|
+
transformFn: TransformFn | null;
|
|
22
|
+
|
|
23
|
+
constructor(name: string, query: string, args: QueryGqlArgs) {
|
|
24
|
+
if (args.cacheIfFn && !vars)
|
|
25
|
+
throw new Error(
|
|
26
|
+
'queryRoute: ' +
|
|
27
|
+
name +
|
|
28
|
+
' cannot have caching function if there are no ' +
|
|
29
|
+
'variables defined',
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
this.name = name;
|
|
33
|
+
this.query = query;
|
|
34
|
+
this.vars = args.vars;
|
|
35
|
+
this.cacheIfFn = args.cacheIfFn;
|
|
36
|
+
this.transformFn = args.transformFn;
|
|
37
|
+
|
|
38
|
+
if (args.preventCaching) {
|
|
39
|
+
if (this.cacheIfFn) throw new Error('unreachable');
|
|
40
|
+
// prevent filling defaults
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// add default vars and cacheIfFn if we know the route
|
|
45
|
+
if (this.name === 'entry') this.fillEntryDefaults();
|
|
46
|
+
else if (this.name === 'global') this.fillGlobalDefaults();
|
|
47
|
+
else this.fillBasicDefaults();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private fillEntryDefaults() {
|
|
51
|
+
if (this.vars) return;
|
|
52
|
+
|
|
53
|
+
// the _setName step happens in parseVars which happens before setting
|
|
54
|
+
// the defaults, so since we're adding vars here we need to set the name
|
|
55
|
+
// manually
|
|
56
|
+
this.vars = {
|
|
57
|
+
siteId: vars.siteId().z_setName('siteId'),
|
|
58
|
+
uri: vars.string().z_setName('uri'),
|
|
59
|
+
};
|
|
60
|
+
this.cacheIfFn = res => !!extractEntry(res);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private fillGlobalDefaults() {
|
|
64
|
+
if (this.vars) return;
|
|
65
|
+
|
|
66
|
+
this.vars = { siteId: vars.siteId().z_setName('siteId') };
|
|
67
|
+
this.cacheIfFn = () => true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* This adds caching to queries containing `query {` or
|
|
72
|
+
* `query ($siteId: [QueryArgument) {` without any additional vars
|
|
73
|
+
*/
|
|
74
|
+
private fillBasicDefaults() {
|
|
75
|
+
if (this.vars) return;
|
|
76
|
+
|
|
77
|
+
const NO_VAR_TEST = /(^|\s)query\s*{/;
|
|
78
|
+
const SITE_ID_VAR_TEST =
|
|
79
|
+
/(^|\s)query\s*\(\s*\$siteId\s*:\s*\[\s*QueryArgument\s*\]\s*\)\s*{/;
|
|
80
|
+
|
|
81
|
+
if (NO_VAR_TEST.test(this.query)) {
|
|
82
|
+
this.vars = {};
|
|
83
|
+
this.cacheIfFn = () => true;
|
|
84
|
+
} else if (SITE_ID_VAR_TEST.test(this.query)) {
|
|
85
|
+
this.vars = { siteId: vars.siteId().z_setName('siteId') };
|
|
86
|
+
this.cacheIfFn = () => true;
|
|
87
|
+
} else if (!this.query.includes('query')) {
|
|
88
|
+
// this warning might be shown to mutation queries or subscriptions
|
|
89
|
+
// in that case, the user should explicitly set caching to false
|
|
90
|
+
console.warn(
|
|
91
|
+
`cannot determine if query (${this.name}) is cacheable, see` +
|
|
92
|
+
' https://github.com/crelte/crelte/issues/114 for infos',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private async transform(
|
|
98
|
+
jsonResp: Record<string, any>,
|
|
99
|
+
vars: Record<string, any>,
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
if (!this.transformFn || !jsonResp.data) return;
|
|
102
|
+
|
|
103
|
+
const transformed = await this.transformFn(jsonResp.data, vars);
|
|
104
|
+
if (typeof transformed !== 'undefined') jsonResp.data = transformed;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async handle(
|
|
108
|
+
caching: QueriesCaching,
|
|
109
|
+
csr: CrelteServerRequest,
|
|
110
|
+
): Promise<Response> {
|
|
111
|
+
let vars: Record<string, any>;
|
|
112
|
+
try {
|
|
113
|
+
const reqVars = await csr.req.json();
|
|
114
|
+
vars = validateVars(this.vars, reqVars, caching.router);
|
|
115
|
+
if ('qName' in vars || 'xCraftSite' in vars)
|
|
116
|
+
throw new Error(
|
|
117
|
+
'qName and xCraftSite are reserved variable names',
|
|
118
|
+
);
|
|
119
|
+
} catch (e) {
|
|
120
|
+
return newError(e, 400);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let logInfo: string | null = null;
|
|
124
|
+
if (caching.debug) {
|
|
125
|
+
logInfo = `[queries: ${this.name}] vars: ${JSON.stringify(vars)}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let previewToken: string | null = null;
|
|
129
|
+
let siteToken: string | null = null;
|
|
130
|
+
|
|
131
|
+
const reqSearch = new URL(csr.req.url).searchParams;
|
|
132
|
+
|
|
133
|
+
if (reqSearch.has('token')) {
|
|
134
|
+
previewToken = reqSearch.get('token');
|
|
135
|
+
} else if (reqSearch.has('siteToken')) {
|
|
136
|
+
siteToken = reqSearch.get('siteToken');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// check for x-craft-site header and pass it on
|
|
140
|
+
const xCraftSite = csr.req.headers.get('X-Craft-Site');
|
|
141
|
+
|
|
142
|
+
let cacheKey: string | null = null;
|
|
143
|
+
const useCache = !previewToken && caching.isEnabled();
|
|
144
|
+
if (useCache) {
|
|
145
|
+
cacheKey = await calcKey({ ...vars, qName: this.name, xCraftSite });
|
|
146
|
+
const cached = await caching.getCache(cacheKey);
|
|
147
|
+
|
|
148
|
+
if (logInfo) console.log(`${logInfo} ${cached ? 'hit' : 'miss'}`);
|
|
149
|
+
|
|
150
|
+
// we found something in the cache
|
|
151
|
+
if (cached) return Response.json(cached);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const headers: Record<string, string> = {
|
|
155
|
+
'Content-Type': 'application/json',
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const auth = csr.getEnv('ENDPOINT_TOKEN');
|
|
159
|
+
if (auth) headers['Authorization'] = 'Bearer ' + auth;
|
|
160
|
+
|
|
161
|
+
const url = new URL(csr.getEnv('ENDPOINT_URL'));
|
|
162
|
+
if (previewToken) url.searchParams.set('token', previewToken);
|
|
163
|
+
if (siteToken) url.searchParams.set('siteToken', siteToken);
|
|
164
|
+
|
|
165
|
+
const xDebug = csr.req.headers.get('X-Debug');
|
|
166
|
+
if (xDebug) headers['X-Debug'] = xDebug;
|
|
167
|
+
|
|
168
|
+
if (xCraftSite) headers['X-Craft-Site'] = xCraftSite;
|
|
169
|
+
|
|
170
|
+
// now execute the gql request
|
|
171
|
+
let resp: Response;
|
|
172
|
+
try {
|
|
173
|
+
resp = await fetch(url, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers,
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
query: this.query,
|
|
178
|
+
variables: vars,
|
|
179
|
+
}),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// if the response is not ok we don't cache anything
|
|
183
|
+
// and just return the response
|
|
184
|
+
if (!resp.ok) return resp;
|
|
185
|
+
} catch (e) {
|
|
186
|
+
return newError(e, 500);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const respHeaders: Record<string, string> = {};
|
|
190
|
+
const xDebugLink = resp.headers.get('X-Debug-Link');
|
|
191
|
+
if (xDebugLink) respHeaders['X-Debug'] = xDebugLink;
|
|
192
|
+
|
|
193
|
+
let jsonResp: Record<string, any>;
|
|
194
|
+
try {
|
|
195
|
+
jsonResp = await resp.json();
|
|
196
|
+
if (!jsonResp || typeof jsonResp !== 'object')
|
|
197
|
+
throw new Error('invalid json response');
|
|
198
|
+
await this.transform(jsonResp, vars);
|
|
199
|
+
} catch (e) {
|
|
200
|
+
return newError(e, 500);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// also no caching for errors
|
|
204
|
+
if (jsonResp.errors) {
|
|
205
|
+
return Response.json(jsonResp, { headers: respHeaders });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// now we have a valid json resp.
|
|
209
|
+
// should we cache it?
|
|
210
|
+
if (cacheKey && this.cacheIfFn?.(jsonResp.data, vars)) {
|
|
211
|
+
try {
|
|
212
|
+
await caching.setCache(cacheKey, jsonResp);
|
|
213
|
+
if (logInfo) console.log(logInfo + ' set cache');
|
|
214
|
+
} catch (e) {
|
|
215
|
+
console.error('could not cache gql response', e);
|
|
216
|
+
}
|
|
217
|
+
// if caching is enabled but not used we warn
|
|
218
|
+
} else if (cacheKey && logInfo) {
|
|
219
|
+
console.warn('!! ' + logInfo + ' caching not allowed');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return Response.json(jsonResp, { headers: respHeaders });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { QueryVar } from '../../queries/vars.js';
|
|
2
|
+
import CrelteServerRequest from '../CrelteServer.js';
|
|
3
|
+
import ServerRouter from '../ServerRouter.js';
|
|
4
|
+
import { HandleFn, newError, validateVars } from './routes.js';
|
|
5
|
+
|
|
6
|
+
// only internal
|
|
7
|
+
export default class QueryHandleRoute {
|
|
8
|
+
name: string;
|
|
9
|
+
handleFn: HandleFn;
|
|
10
|
+
vars: Record<string, QueryVar> | null;
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
name: string,
|
|
14
|
+
handleFn: HandleFn,
|
|
15
|
+
vars: Record<string, QueryVar> | null,
|
|
16
|
+
) {
|
|
17
|
+
this.name = name;
|
|
18
|
+
this.handleFn = handleFn;
|
|
19
|
+
this.vars = vars;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async handle(
|
|
23
|
+
cs: ServerRouter,
|
|
24
|
+
csr: CrelteServerRequest,
|
|
25
|
+
): Promise<Response> {
|
|
26
|
+
let vars: Record<string, any>;
|
|
27
|
+
try {
|
|
28
|
+
const reqVars = await csr.req.json();
|
|
29
|
+
vars = validateVars(this.vars, reqVars, cs);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return newError(e, 400);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const res = await this.handleFn(csr, vars);
|
|
35
|
+
return Response.json({ data: res });
|
|
36
|
+
}
|
|
37
|
+
}
|