@webda/graphql 4.0.0-beta.2 → 4.0.0-beta.3
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/README.md +131 -9
- package/lib/graphql.service.d.ts +205 -0
- package/lib/graphql.service.d.ts.map +1 -0
- package/lib/graphql.service.js +1088 -0
- package/lib/graphql.service.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -0
- package/lib/types/any.d.ts.map +1 -1
- package/lib/types/any.js +5 -0
- package/lib/types/any.js.map +1 -1
- package/lib/types/long.d.ts.map +1 -1
- package/lib/types/long.js +10 -0
- package/lib/types/long.js.map +1 -1
- package/package.json +33 -46
- package/webda.module.json +124 -126
- package/lib/graphql.d.ts +0 -173
- package/lib/graphql.d.ts.map +0 -1
- package/lib/graphql.js +0 -1005
- package/lib/graphql.js.map +0 -1
- package/lib/graphql.spec.d.ts +0 -2
- package/lib/graphql.spec.d.ts.map +0 -1
- package/lib/graphql.spec.js +0 -669
- package/lib/graphql.spec.js.map +0 -1
|
@@ -0,0 +1,1088 @@
|
|
|
1
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
2
|
+
var useValue = arguments.length > 2;
|
|
3
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
4
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
5
|
+
}
|
|
6
|
+
return useValue ? value : void 0;
|
|
7
|
+
};
|
|
8
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
9
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
10
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
11
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
12
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
13
|
+
var _, done = false;
|
|
14
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
15
|
+
var context = {};
|
|
16
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
17
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
18
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
19
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
20
|
+
if (kind === "accessor") {
|
|
21
|
+
if (result === void 0) continue;
|
|
22
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
23
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
24
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
25
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
26
|
+
}
|
|
27
|
+
else if (_ = accept(result)) {
|
|
28
|
+
if (kind === "field") initializers.unshift(_);
|
|
29
|
+
else descriptor[key] = _;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
|
+
done = true;
|
|
34
|
+
};
|
|
35
|
+
import { DomainService, DomainServiceParameters, Route, WebdaError, useApplication, useCore, useCoreEvents, useModelMetadata, useRepository } from "@webda/core";
|
|
36
|
+
import * as WebdaQL from "@webda/ql";
|
|
37
|
+
import { EventIterator, MergedIterator } from "@webda/runtime";
|
|
38
|
+
import { GraphQLBoolean, GraphQLError, GraphQLInputObjectType, GraphQLList, GraphQLObjectType, GraphQLSchema, GraphQLString, printSchema } from "graphql";
|
|
39
|
+
import { createHandler } from "graphql-http";
|
|
40
|
+
import { CloseCode, makeServer } from "graphql-ws";
|
|
41
|
+
import { nextTick } from "process";
|
|
42
|
+
import { WebSocketServer } from "ws";
|
|
43
|
+
import { AnyScalarType } from "./types/any.js";
|
|
44
|
+
import { DateScalar } from "./types/date.js";
|
|
45
|
+
import { GraphQLLong } from "./types/long.js";
|
|
46
|
+
const GraphIQL = `
|
|
47
|
+
<!doctype html>
|
|
48
|
+
<html lang="en">
|
|
49
|
+
<head>
|
|
50
|
+
<title>GraphiQL</title>
|
|
51
|
+
<style>
|
|
52
|
+
body {
|
|
53
|
+
height: 100%;
|
|
54
|
+
margin: 0;
|
|
55
|
+
width: 100%;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#graphiql {
|
|
60
|
+
height: 100vh;
|
|
61
|
+
}
|
|
62
|
+
</style>
|
|
63
|
+
<!--
|
|
64
|
+
This GraphiQL example depends on Promise and fetch, which are available in
|
|
65
|
+
modern browsers, but can be "polyfilled" for older browsers.
|
|
66
|
+
GraphiQL itself depends on React DOM.
|
|
67
|
+
If you do not want to rely on a CDN, you can host these files locally or
|
|
68
|
+
include them directly in your favored resource bundler.
|
|
69
|
+
-->
|
|
70
|
+
<script
|
|
71
|
+
crossorigin
|
|
72
|
+
src="https://unpkg.com/react@18/umd/react.development.js"
|
|
73
|
+
></script>
|
|
74
|
+
<script
|
|
75
|
+
crossorigin
|
|
76
|
+
src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
|
|
77
|
+
></script>
|
|
78
|
+
<!--
|
|
79
|
+
These two files can be found in the npm module, however you may wish to
|
|
80
|
+
copy them directly into your environment, or perhaps include them in your
|
|
81
|
+
favored resource bundler.
|
|
82
|
+
-->
|
|
83
|
+
<script
|
|
84
|
+
src="https://unpkg.com/graphiql/graphiql.min.js"
|
|
85
|
+
type="application/javascript"
|
|
86
|
+
></script>
|
|
87
|
+
<link rel="stylesheet" href="https://unpkg.com/graphiql/graphiql.min.css" />
|
|
88
|
+
<!--
|
|
89
|
+
These are imports for the GraphIQL Explorer plugin.
|
|
90
|
+
-->
|
|
91
|
+
<script
|
|
92
|
+
src="https://unpkg.com/@graphiql/plugin-explorer/dist/index.umd.js"
|
|
93
|
+
crossorigin
|
|
94
|
+
></script>
|
|
95
|
+
<script
|
|
96
|
+
src="https://unpkg.com/graphql-ws/umd/graphql-ws.min.js"
|
|
97
|
+
type="application/javascript"
|
|
98
|
+
></script>
|
|
99
|
+
<link
|
|
100
|
+
rel="stylesheet"
|
|
101
|
+
href="https://unpkg.com/@graphiql/plugin-explorer/dist/style.css"
|
|
102
|
+
/>
|
|
103
|
+
</head>
|
|
104
|
+
|
|
105
|
+
<body>
|
|
106
|
+
<div id="graphiql">Loading...</div>
|
|
107
|
+
<script>
|
|
108
|
+
const root = ReactDOM.createRoot(document.getElementById('graphiql'));
|
|
109
|
+
const fetcher = GraphiQL.createFetcher({
|
|
110
|
+
url: '{{URL}}',
|
|
111
|
+
subscriptionUrl: '{{WSURL}}',
|
|
112
|
+
headers: { 'X-GraphiQL-Schema': 'true' },
|
|
113
|
+
});
|
|
114
|
+
const explorerPlugin = GraphiQLPluginExplorer.explorerPlugin();
|
|
115
|
+
root.render(
|
|
116
|
+
React.createElement(GraphiQL, {
|
|
117
|
+
fetcher,
|
|
118
|
+
defaultEditorToolsVisibility: true,
|
|
119
|
+
plugins: [explorerPlugin],
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
</script>
|
|
123
|
+
</body>
|
|
124
|
+
</html>
|
|
125
|
+
`;
|
|
126
|
+
/**
|
|
127
|
+
* Parameters for the GraphQL service
|
|
128
|
+
*/
|
|
129
|
+
export class GraphQLParameters extends DomainServiceParameters {
|
|
130
|
+
/**
|
|
131
|
+
* Load parameters with defaults
|
|
132
|
+
* @param params - raw configuration values
|
|
133
|
+
* @returns this for chaining
|
|
134
|
+
*/
|
|
135
|
+
load(params = {}) {
|
|
136
|
+
super.load({ ...params, nameTransfomer: params.nameTransfomer || "PascalCase" });
|
|
137
|
+
this.url ?? (this.url = "/graphql");
|
|
138
|
+
this.maxOperationsPerRequest ?? (this.maxOperationsPerRequest = 10);
|
|
139
|
+
this.userModel ?? (this.userModel = "User");
|
|
140
|
+
this.exposeMe ?? (this.exposeMe = true);
|
|
141
|
+
this.globalSubscription ?? (this.globalSubscription = true);
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Implement a GraphQL service
|
|
147
|
+
*
|
|
148
|
+
* It will expose all your exposed models as GraphQL types
|
|
149
|
+
* Dynamically generate the schema
|
|
150
|
+
*
|
|
151
|
+
* @WebdaModda
|
|
152
|
+
*/
|
|
153
|
+
let GraphQLService = (() => {
|
|
154
|
+
var _a;
|
|
155
|
+
let _classSuper = DomainService;
|
|
156
|
+
let _instanceExtraInitializers = [];
|
|
157
|
+
let _schemaRoute_decorators;
|
|
158
|
+
let _endpoint_decorators;
|
|
159
|
+
return _a = class GraphQLService extends _classSuper {
|
|
160
|
+
constructor() {
|
|
161
|
+
super(...arguments);
|
|
162
|
+
this.schema = __runInitializers(this, _instanceExtraInitializers);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Load and initialize GraphQL service parameters
|
|
166
|
+
* @param params - raw partial configuration
|
|
167
|
+
* @returns initialized GraphQLParameters
|
|
168
|
+
*/
|
|
169
|
+
loadParameters(params) {
|
|
170
|
+
return new GraphQLParameters().load(params);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Resolve a JSON Schema property by following $ref pointers
|
|
174
|
+
* @param prop - JSON Schema property that may contain $ref
|
|
175
|
+
* @param definitions - schema definitions map for resolving references
|
|
176
|
+
* @returns the resolved JSON Schema (or a null type if unresolvable)
|
|
177
|
+
*/
|
|
178
|
+
getJsonSchemaDefinition(prop, definitions = {}) {
|
|
179
|
+
if (prop?.type) {
|
|
180
|
+
if (prop.type === "array" && prop.items["$ref"]) {
|
|
181
|
+
prop.items = this.getJsonSchemaDefinition(prop.items, definitions);
|
|
182
|
+
}
|
|
183
|
+
return prop;
|
|
184
|
+
}
|
|
185
|
+
while (prop && prop.$ref) {
|
|
186
|
+
prop = definitions[decodeURIComponent(prop.$ref.replace("#/definitions/", ""))];
|
|
187
|
+
}
|
|
188
|
+
return prop || { type: "null" };
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Convert a JSON Schema to a GraphQL type definition
|
|
192
|
+
* @param schema - JSON Schema to convert
|
|
193
|
+
* @param defaultName - fallback name for anonymous types
|
|
194
|
+
* @param input - if true, generate GraphQLInputObjectType instead of GraphQLObjectType
|
|
195
|
+
* @returns object with the GraphQL type and optional description, or undefined for null types
|
|
196
|
+
*/
|
|
197
|
+
getGraphQLSchemaFromSchema(schema, defaultName, input) {
|
|
198
|
+
let type;
|
|
199
|
+
if (!schema || !schema.type) {
|
|
200
|
+
type = AnyScalarType;
|
|
201
|
+
}
|
|
202
|
+
else if (schema.type === "string") {
|
|
203
|
+
if (schema.format === "date-time") {
|
|
204
|
+
type = DateScalar;
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
type = GraphQLString;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else if (schema.type === "boolean") {
|
|
211
|
+
type = GraphQLBoolean;
|
|
212
|
+
}
|
|
213
|
+
else if (schema.type === "array") {
|
|
214
|
+
type = new GraphQLList(this.getGraphQLSchemaFromSchema(schema.items, schema.title || defaultName, input)?.type ||
|
|
215
|
+
GraphQLString);
|
|
216
|
+
}
|
|
217
|
+
else if (schema.type === "integer" || schema.type === "number") {
|
|
218
|
+
// Use long to handle 2^52
|
|
219
|
+
type = GraphQLLong;
|
|
220
|
+
}
|
|
221
|
+
else if (schema.type === "null") {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
else if (schema.type === "object") {
|
|
225
|
+
const fields = {};
|
|
226
|
+
// Map does not fit well with graphql
|
|
227
|
+
if (!schema.properties) {
|
|
228
|
+
return { type: AnyScalarType, description: "Map" };
|
|
229
|
+
}
|
|
230
|
+
for (const i in schema.properties) {
|
|
231
|
+
const res = this.getGraphQLSchemaFromSchema(this.getJsonSchemaDefinition(schema.properties[i], schema.definitions), `${schema.title || defaultName}_${i}`, input);
|
|
232
|
+
if (!res)
|
|
233
|
+
continue;
|
|
234
|
+
fields[i] = res;
|
|
235
|
+
}
|
|
236
|
+
if (Object.keys(fields).length === 0) {
|
|
237
|
+
this.log("DEBUG", "Return map for", defaultName, "because no fields");
|
|
238
|
+
return { type: AnyScalarType, description: "Map" };
|
|
239
|
+
}
|
|
240
|
+
type = new (input ? GraphQLInputObjectType : GraphQLObjectType)({
|
|
241
|
+
fields,
|
|
242
|
+
name: schema.title || defaultName
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return { type, description: schema.description };
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Get or create a GraphQL query result wrapper type with results array and continuationToken
|
|
249
|
+
* @param type - GraphQL object type to wrap in a query result
|
|
250
|
+
* @returns GraphQL object type with results and continuationToken fields
|
|
251
|
+
*/
|
|
252
|
+
getGraphQLQueryResult(type) {
|
|
253
|
+
var _b;
|
|
254
|
+
const name = `${type.name}QueryResult`;
|
|
255
|
+
(_b = this.modelsMap)[name] ?? (_b[name] = new GraphQLObjectType({
|
|
256
|
+
fields: {
|
|
257
|
+
results: {
|
|
258
|
+
type: new GraphQLList(type)
|
|
259
|
+
},
|
|
260
|
+
continuationToken: {
|
|
261
|
+
type: GraphQLString
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
name
|
|
265
|
+
}));
|
|
266
|
+
return this.modelsMap[name];
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Build GraphQL field definitions from a JSON Schema, enriching with relation resolvers from the model graph
|
|
270
|
+
* @param schema - JSON Schema describing the model properties
|
|
271
|
+
* @param defaultName - fallback name for generated types
|
|
272
|
+
* @param webdaGraph - model relation graph (links, maps, queries, parent)
|
|
273
|
+
* @param input - if true, generate input types (skip readonly fields, skip relation resolvers)
|
|
274
|
+
* @returns map of field names to GraphQL field configs
|
|
275
|
+
*/
|
|
276
|
+
getGraphQLFieldsFromSchema(schema, defaultName, webdaGraph, input) {
|
|
277
|
+
const fields = {};
|
|
278
|
+
const skipFields = [];
|
|
279
|
+
const attributeFilter = (skip = false) => info => {
|
|
280
|
+
if (!this.modelsMap[info.model] && skip) {
|
|
281
|
+
skipFields.push(info.attribute);
|
|
282
|
+
this.log("INFO", `Skipping field ${info.attribute} as model ${info.model} is not exposed`);
|
|
283
|
+
}
|
|
284
|
+
return this.modelsMap[info.model];
|
|
285
|
+
};
|
|
286
|
+
if (webdaGraph && !input) {
|
|
287
|
+
// In graphql parent should not be displayed
|
|
288
|
+
if (webdaGraph.parent) {
|
|
289
|
+
fields[webdaGraph.parent.attribute] = {
|
|
290
|
+
type: this.modelsMap[webdaGraph.parent.model],
|
|
291
|
+
resolve: async (source, _args, context, info) => {
|
|
292
|
+
this.countOperation(context);
|
|
293
|
+
// Check permission and model exists
|
|
294
|
+
return this.loadModelInstance(source[webdaGraph.parent.attribute] || "", this.app.getModel(webdaGraph.parent.model), context, info.fieldNodes?.find(node => node.name.value === webdaGraph.parent.attribute));
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
// Links should probably not be displayed
|
|
299
|
+
(webdaGraph.links || []).filter(attributeFilter()).forEach(link => {
|
|
300
|
+
if (link.type === "LINK") {
|
|
301
|
+
fields[link.attribute] = {
|
|
302
|
+
type: this.modelsMap[link.model],
|
|
303
|
+
resolve: async (source, _, context, info) => {
|
|
304
|
+
// Check permission and model exists
|
|
305
|
+
return this.loadModelInstance(source[link.attribute] || "", this.app.getModel(link.model), context, info.fieldNodes.find(node => node.name.value === link.attribute));
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
fields[link.attribute] = {
|
|
311
|
+
type: new GraphQLList(this.modelsMap[link.model]),
|
|
312
|
+
args: {
|
|
313
|
+
filter: {
|
|
314
|
+
type: GraphQLString
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
resolve: async (source, args, context, info) => {
|
|
318
|
+
const src = link.type === "LINKS_MAP" ? Object.values(source[link.attribute]) : source[link.attribute];
|
|
319
|
+
return (await Promise.all(src.map(i => this.loadModelInstance(i, this.app.getModel(link.model), context, info.fieldNodes.find(node => node.name.value === link.attribute), args.filter ? new WebdaQL.PartialValidator(WebdaQL.unsanitize(args.filter)) : undefined)))).filter(i => i !== null);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
(webdaGraph.maps || []).filter(attributeFilter()).forEach(map => {
|
|
325
|
+
fields[map.attribute] = {
|
|
326
|
+
type: new GraphQLList(this.modelsMap[map.model]),
|
|
327
|
+
args: {
|
|
328
|
+
filter: {
|
|
329
|
+
type: GraphQLString
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
resolve: async (source, args, context, info) => {
|
|
333
|
+
const modelDefinition = this.app.getModel(map.model);
|
|
334
|
+
return (await Promise.all(source[map.attribute].map(i => this.loadModelInstance(i, modelDefinition, context, info.fieldNodes.find(node => node.name.value === map.attribute), args.filter ? new WebdaQL.PartialValidator(WebdaQL.unsanitize(args.filter)) : undefined)))).filter(i => i !== null);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
});
|
|
338
|
+
(webdaGraph.queries || []).filter(attributeFilter(true)).forEach(query => {
|
|
339
|
+
fields[query.attribute] = {
|
|
340
|
+
type: this.getGraphQLQueryResult(this.modelsMap[query.model]),
|
|
341
|
+
args: {
|
|
342
|
+
query: {
|
|
343
|
+
type: GraphQLString
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
resolve: async (source, args, context, info) => {
|
|
347
|
+
const res = await source[query.attribute].query(WebdaQL.unsanitize(args.query || ""), context);
|
|
348
|
+
this.countOperation(context, res.results.length);
|
|
349
|
+
return res;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
for (const i in schema.properties) {
|
|
355
|
+
// Was initiated by the known graph
|
|
356
|
+
if (fields[i] || skipFields.includes(i) || (schema.properties[i].readOnly && input)) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const prop = this.getGraphQLSchemaFromSchema(this.getJsonSchemaDefinition(schema.properties[i], schema.definitions), `${defaultName}_${i}`, input);
|
|
360
|
+
if (!prop) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
fields[i] = prop;
|
|
364
|
+
}
|
|
365
|
+
return fields;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Load a model instance, optimizing to skip store reads when all requested fields are already known
|
|
369
|
+
* @param knownFieldsOrId - UUID string or object with known field values
|
|
370
|
+
* @param model - model class to load from
|
|
371
|
+
* @param context - web context for permission checks
|
|
372
|
+
* @param info - GraphQL field node describing the selection set
|
|
373
|
+
* @param filter - optional WebdaQL filter to apply to the result
|
|
374
|
+
* @returns the model instance, or null if filtered out
|
|
375
|
+
*/
|
|
376
|
+
async loadModelInstance(knownFieldsOrId, model, context, info, filter) {
|
|
377
|
+
const res = typeof knownFieldsOrId === "string" ? { uuid: knownFieldsOrId } : knownFieldsOrId;
|
|
378
|
+
if (filter && !filter.eval(res)) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
let operation =
|
|
382
|
+
// Check if we have a valid selection set
|
|
383
|
+
info === undefined ||
|
|
384
|
+
info.selectionSet === undefined ||
|
|
385
|
+
info.selectionSet.selections === undefined ||
|
|
386
|
+
info.selectionSet.selections.filter(node => node.kind === "Field").length === 0;
|
|
387
|
+
// Check if know all fields already
|
|
388
|
+
for (const field of (info?.selectionSet?.selections || []).filter(node => node.kind === "Field")) {
|
|
389
|
+
if (res[field.name.value] === undefined) {
|
|
390
|
+
operation = true;
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// We already know the whole answer so no more request needed
|
|
395
|
+
if (!operation && (!filter || !filter.wasPartialMatch())) {
|
|
396
|
+
return res;
|
|
397
|
+
}
|
|
398
|
+
// Count the operation then and retrieve the model
|
|
399
|
+
this.countOperation(context);
|
|
400
|
+
const modelInstance = await model.ref(res.uuid || "").get();
|
|
401
|
+
if (!modelInstance) {
|
|
402
|
+
throw new GraphQLError("Object not found", {
|
|
403
|
+
extensions: {
|
|
404
|
+
code: "NOT_FOUND"
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
if ((await modelInstance?.canAct?.(context, "get")) !== true) {
|
|
409
|
+
throw new GraphQLError("Permission denied", {
|
|
410
|
+
extensions: {
|
|
411
|
+
code: "PERMISSION_DENIED"
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
if (filter && !filter.eval(modelInstance, false)) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
return modelInstance;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Increment the operation counter and throw if the per-request limit is exceeded
|
|
422
|
+
* @param context - web context holding the graphql extension with the counter
|
|
423
|
+
* @param increment - number of operations to add (default 1)
|
|
424
|
+
*/
|
|
425
|
+
countOperation(context, increment = 1) {
|
|
426
|
+
const ext = context.getExtension("graphql");
|
|
427
|
+
if (ext?.count === this.parameters.maxOperationsPerRequest) {
|
|
428
|
+
throw new GraphQLError("Too many operations", {
|
|
429
|
+
extensions: {
|
|
430
|
+
code: "TOO_MANY_OPERATIONS"
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
if (ext) {
|
|
435
|
+
ext.count ?? (ext.count = 0);
|
|
436
|
+
ext.count += increment;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Set up WebSocket server for GraphQL subscriptions and register upgrade handler
|
|
441
|
+
* @returns this instance for chaining
|
|
442
|
+
*/
|
|
443
|
+
resolve() {
|
|
444
|
+
super.resolve();
|
|
445
|
+
// Set-up ws server
|
|
446
|
+
this.wss = new WebSocketServer({ noServer: true });
|
|
447
|
+
useCoreEvents("Webda.Init.Http", (http) => {
|
|
448
|
+
http.on("upgrade", (req, socket, head) => {
|
|
449
|
+
if (req.url === this.parameters.url) {
|
|
450
|
+
(async () => {
|
|
451
|
+
req.webdaContext ?? (req.webdaContext = await useCore().getContextFromRequest(req));
|
|
452
|
+
await req.webdaContext.init();
|
|
453
|
+
this.wss.handleUpgrade(req, socket, head, ws => {
|
|
454
|
+
this.wss.emit("connection", ws, req);
|
|
455
|
+
});
|
|
456
|
+
})();
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
return this;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Generate GraphQL schema
|
|
464
|
+
*/
|
|
465
|
+
generateSchema() {
|
|
466
|
+
const rootFields = {};
|
|
467
|
+
const mutations = {};
|
|
468
|
+
const subscriptions = {};
|
|
469
|
+
const models = this.app.getModels();
|
|
470
|
+
this.modelsMap = {};
|
|
471
|
+
for (const i in models) {
|
|
472
|
+
const model = models[i];
|
|
473
|
+
const metadata = useModelMetadata(model);
|
|
474
|
+
if (!metadata)
|
|
475
|
+
continue;
|
|
476
|
+
// Only expose the "final" version of each model — mirrors the REST
|
|
477
|
+
// transport's behavior so that an app-specific override (e.g.
|
|
478
|
+
// WebdaSample/User) shadows a framework-provided one (Webda/User)
|
|
479
|
+
// instead of both being registered under the same GraphQL type name.
|
|
480
|
+
if (!this.app.isFinalModel(metadata.Identifier))
|
|
481
|
+
continue;
|
|
482
|
+
if (!this.parameters.isIncluded(metadata.Identifier))
|
|
483
|
+
continue;
|
|
484
|
+
const schema = this.app.getSchema(i);
|
|
485
|
+
if (!schema)
|
|
486
|
+
continue;
|
|
487
|
+
const name = (metadata.ShortName || i.split("/").pop()).replace("/", "_");
|
|
488
|
+
this.log("INFO", "Add GraphQL type", name);
|
|
489
|
+
const modelGraph = metadata.Relations;
|
|
490
|
+
this.modelsMap[i] = new GraphQLObjectType({
|
|
491
|
+
fields: () => this.getGraphQLFieldsFromSchema(schema, name, modelGraph),
|
|
492
|
+
name
|
|
493
|
+
});
|
|
494
|
+
const input = new GraphQLInputObjectType({
|
|
495
|
+
fields: this.getGraphQLFieldsFromSchema(schema, name + "Input", modelGraph, true),
|
|
496
|
+
name: name + "Input"
|
|
497
|
+
});
|
|
498
|
+
const actionsName = Object.keys(metadata.Actions);
|
|
499
|
+
if (!actionsName.includes("create")) {
|
|
500
|
+
mutations[`create${name}`] = {
|
|
501
|
+
type: this.modelsMap[i],
|
|
502
|
+
args: {
|
|
503
|
+
[name]: { type: input }
|
|
504
|
+
},
|
|
505
|
+
resolve: async (_, args, context) => {
|
|
506
|
+
const object = new model();
|
|
507
|
+
object.load(args[name]);
|
|
508
|
+
this.log("INFO", "Create", object, context.getCurrentUserId());
|
|
509
|
+
if ((await object.canAct?.(context, "create")) !== true) {
|
|
510
|
+
throw new GraphQLError("Permission denied", {
|
|
511
|
+
extensions: {
|
|
512
|
+
code: "PERMISSION_DENIED"
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
await object.save();
|
|
517
|
+
return object;
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
if (!actionsName.includes("update")) {
|
|
522
|
+
mutations[`update${name}`] = {
|
|
523
|
+
type: this.modelsMap[i],
|
|
524
|
+
args: {
|
|
525
|
+
[name]: { type: input },
|
|
526
|
+
uuid: {
|
|
527
|
+
type: GraphQLString
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
resolve: async (_, args, context) => {
|
|
531
|
+
const object = await model.ref(args.uuid).get();
|
|
532
|
+
if ((await object?.canAct?.(context, "update")) !== true) {
|
|
533
|
+
throw new GraphQLError("Permission denied", {
|
|
534
|
+
extensions: {
|
|
535
|
+
code: "PERMISSION_DENIED"
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
return object.load(args[name]).save();
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
// check for actions
|
|
544
|
+
if (!actionsName.includes("delete")) {
|
|
545
|
+
mutations[`delete${name}`] = {
|
|
546
|
+
type: new GraphQLObjectType({ name: `delete${name}`, fields: { success: { type: GraphQLBoolean } } }),
|
|
547
|
+
args: {
|
|
548
|
+
uuid: {
|
|
549
|
+
type: GraphQLString
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
resolve: async (_, args, context) => {
|
|
553
|
+
const object = await model.ref(args.uuid).get();
|
|
554
|
+
if ((await object?.canAct?.(context, "delete")) !== true) {
|
|
555
|
+
throw new GraphQLError("Permission denied", {
|
|
556
|
+
extensions: {
|
|
557
|
+
code: "PERMISSION_DENIED"
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
await object.delete();
|
|
562
|
+
return {
|
|
563
|
+
success: true
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
// Retrieval
|
|
569
|
+
const plural = this.transformName(metadata.Plural.split("/").pop());
|
|
570
|
+
rootFields[plural] = {
|
|
571
|
+
type: this.getGraphQLQueryResult(this.modelsMap[i]),
|
|
572
|
+
args: {
|
|
573
|
+
query: {
|
|
574
|
+
type: GraphQLString,
|
|
575
|
+
defaultValue: ""
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
resolve: async (_, args, context) => {
|
|
579
|
+
return await model.query(WebdaQL.unsanitize(args.query || ""));
|
|
580
|
+
},
|
|
581
|
+
subscribe: async (_source, args, context) => {
|
|
582
|
+
this.log("DEBUG", "Subscription called on", args);
|
|
583
|
+
return this.registerAsyncIteratorQuery(model, plural, WebdaQL.unsanitize(args.query || ""), context);
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
const uuidField = (metadata.PrimaryKey && metadata.PrimaryKey[0]) || "uuid";
|
|
587
|
+
/**
|
|
588
|
+
* Subscription for a specific object
|
|
589
|
+
*/
|
|
590
|
+
subscriptions[this.transformName(i.split("/").pop())] = {
|
|
591
|
+
type: this.modelsMap[i],
|
|
592
|
+
args: {
|
|
593
|
+
[uuidField]: {
|
|
594
|
+
type: GraphQLString
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
subscribe: async (_source, args, context) => {
|
|
598
|
+
this.log("DEBUG", "Subscription called on", args);
|
|
599
|
+
return this.registerAsyncIterator(model, args[uuidField], context);
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
const events = metadata.Events || [];
|
|
603
|
+
const modelEvents = events;
|
|
604
|
+
const instanceEvents = events;
|
|
605
|
+
if (modelEvents.length) {
|
|
606
|
+
const eventTypes = {};
|
|
607
|
+
modelEvents.forEach(e => {
|
|
608
|
+
eventTypes[e] = {
|
|
609
|
+
type: AnyScalarType
|
|
610
|
+
};
|
|
611
|
+
});
|
|
612
|
+
eventTypes["latestEventTime"] = { type: GraphQLLong };
|
|
613
|
+
/**
|
|
614
|
+
* Subscription for all events on a class of objects
|
|
615
|
+
*/
|
|
616
|
+
subscriptions[plural + "Events"] = {
|
|
617
|
+
type: new GraphQLObjectType({ fields: eventTypes, name: plural + "Events" }),
|
|
618
|
+
subscribe: async (_source, args, context, info) => {
|
|
619
|
+
const subscribedEvents = this.getSubscribedEvents(info);
|
|
620
|
+
this.log("DEBUG", "Subscription called on", model, args[uuidField], subscribedEvents);
|
|
621
|
+
return this.registerAsyncEventIterator(model, null, subscribedEvents, context, plural + "Events");
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
if (instanceEvents.length) {
|
|
626
|
+
const eventTypes = {};
|
|
627
|
+
instanceEvents.forEach(e => {
|
|
628
|
+
eventTypes[e] = { type: AnyScalarType };
|
|
629
|
+
});
|
|
630
|
+
eventTypes["latestEventTime"] = { type: GraphQLLong };
|
|
631
|
+
/**
|
|
632
|
+
* Subscription for event on a specific object
|
|
633
|
+
*/
|
|
634
|
+
subscriptions[this.transformName(i.split("/").pop()) + "Events"] = {
|
|
635
|
+
type: new GraphQLObjectType({ fields: eventTypes, name: this.transformName(i.split("/").pop()) + "Events2" }),
|
|
636
|
+
args: {
|
|
637
|
+
[uuidField]: {
|
|
638
|
+
type: GraphQLString
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
subscribe: async (_source, args, context, info) => {
|
|
642
|
+
const subscribedEvents = this.getSubscribedEvents(info);
|
|
643
|
+
this.log("DEBUG", "Subscription called on", model, args[uuidField], subscribedEvents);
|
|
644
|
+
return this.registerAsyncEventIterator(model, args[uuidField], subscribedEvents, context, this.transformName(i.split("/").pop()) + "Events");
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Query for a specific object
|
|
650
|
+
*/
|
|
651
|
+
rootFields[this.transformName(i.split("/").pop())] = {
|
|
652
|
+
type: this.modelsMap[i],
|
|
653
|
+
args: {
|
|
654
|
+
[uuidField]: {
|
|
655
|
+
type: GraphQLString
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
resolve: async (_source, args, context, _info) => {
|
|
659
|
+
this.countOperation(context);
|
|
660
|
+
return this.loadModelInstance(args[uuidField] || "", model, context);
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
// Expose the current user under me
|
|
665
|
+
if (this.parameters.exposeMe) {
|
|
666
|
+
const userGraph = this.app.completeNamespace(this.parameters.userModel);
|
|
667
|
+
const model = this.app.getModel(this.parameters.userModel);
|
|
668
|
+
const userMetadata = useModelMetadata(model);
|
|
669
|
+
if (this.modelsMap[userGraph] && userMetadata) {
|
|
670
|
+
rootFields[this.transformName("Me")] = {
|
|
671
|
+
type: this.modelsMap[userGraph],
|
|
672
|
+
resolve: async (_, _args, context) => {
|
|
673
|
+
this.countOperation(context);
|
|
674
|
+
return context.getCurrentUser();
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
subscriptions["Me"] = {
|
|
678
|
+
type: this.modelsMap[userGraph],
|
|
679
|
+
subscribe: async (_source, _args, context) => {
|
|
680
|
+
if (!context.getCurrentUserId()) {
|
|
681
|
+
throw new GraphQLError("Permission denied", {
|
|
682
|
+
extensions: {
|
|
683
|
+
code: "PERMISSION_DENIED"
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
const user = await context.getCurrentUser();
|
|
688
|
+
const userModelDef = this.app.getModel(this.app.getModelId(user.constructor));
|
|
689
|
+
return this.registerAsyncIterator(userModelDef, context.getCurrentUserId(), context, "Me");
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
const userEvents = userMetadata.Events || [];
|
|
693
|
+
const instanceEvents = userEvents;
|
|
694
|
+
if (instanceEvents.length) {
|
|
695
|
+
const eventTypes = {};
|
|
696
|
+
instanceEvents.forEach(e => {
|
|
697
|
+
eventTypes[e] = { type: AnyScalarType };
|
|
698
|
+
});
|
|
699
|
+
subscriptions["MeEvents"] = {
|
|
700
|
+
type: new GraphQLObjectType({ fields: eventTypes, name: "MeEvents" }),
|
|
701
|
+
subscribe: async (_source, args, context, info) => {
|
|
702
|
+
if (!context.getCurrentUserId()) {
|
|
703
|
+
throw new GraphQLError("Permission denied", {
|
|
704
|
+
extensions: {
|
|
705
|
+
code: "PERMISSION_DENIED"
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
const subscribedEvents = (info.fieldNodes[0].selectionSet?.selections || [])
|
|
710
|
+
.filter(node => node.kind === "Field")
|
|
711
|
+
.map(n => n.name.value);
|
|
712
|
+
const user = await context.getCurrentUser();
|
|
713
|
+
const userModelDef = this.app.getModel(this.app.getModelId(user.constructor));
|
|
714
|
+
return this.registerAsyncEventIterator(userModelDef, context.getCurrentUserId(), subscribedEvents, context, "MeEvents");
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
this.log("WARN", "Cannot expose me, user model is not exposed or get is restricted or type is not unavailable");
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const services = useCore().getServices();
|
|
724
|
+
for (const i in services) {
|
|
725
|
+
if (services[i]?.getClientEvents === undefined) {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
const events = services[i]?.getClientEvents() || [];
|
|
729
|
+
if (events.length === 0) {
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
const fields = {};
|
|
733
|
+
events.forEach(e => {
|
|
734
|
+
fields[e] = { type: AnyScalarType };
|
|
735
|
+
});
|
|
736
|
+
const id = `${i}Events`;
|
|
737
|
+
subscriptions[id] = {
|
|
738
|
+
type: new GraphQLObjectType({
|
|
739
|
+
fields,
|
|
740
|
+
name: id
|
|
741
|
+
}),
|
|
742
|
+
subscribe: async (_source, args, context, info) => {
|
|
743
|
+
const fieldsMap = {};
|
|
744
|
+
(info.fieldNodes[0].selectionSet?.selections || [])
|
|
745
|
+
.filter(node => node.kind === "Field")
|
|
746
|
+
.map(n => n.name.value)
|
|
747
|
+
.forEach(n => (fieldsMap[n] = true));
|
|
748
|
+
return new EventIterator(services[i], fieldsMap, id, {}).iterate();
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
// Copy the rootFields without resolver
|
|
753
|
+
for (const i in rootFields) {
|
|
754
|
+
subscriptions[i] ?? (subscriptions[i] = { ...rootFields[i], resolve: undefined });
|
|
755
|
+
}
|
|
756
|
+
if (this.parameters.globalSubscription) {
|
|
757
|
+
// Create global type based on all previous subscriptions
|
|
758
|
+
subscriptions["Aggregate"] = {
|
|
759
|
+
type: new GraphQLObjectType({
|
|
760
|
+
fields: subscriptions,
|
|
761
|
+
name: "AggregateSubscriptions"
|
|
762
|
+
}),
|
|
763
|
+
subscribe: async (_source, args2, context, info) => {
|
|
764
|
+
const iterators = {};
|
|
765
|
+
const p = [];
|
|
766
|
+
// Check if know all fields already
|
|
767
|
+
for (const field of (info.fieldNodes[0].selectionSet?.selections || []).filter(node => node.kind === "Field")) {
|
|
768
|
+
const name = field.name.value;
|
|
769
|
+
const args = {};
|
|
770
|
+
field.arguments?.forEach(arg => {
|
|
771
|
+
// @ts-ignore
|
|
772
|
+
args[arg.name.value] = arg.value.value;
|
|
773
|
+
});
|
|
774
|
+
p.push((async () => {
|
|
775
|
+
iterators[name] = await subscriptions[name].subscribe(_source, args, context, {
|
|
776
|
+
fieldNodes: [field] // Abusing a bit the builder currently
|
|
777
|
+
});
|
|
778
|
+
})());
|
|
779
|
+
}
|
|
780
|
+
await Promise.all(p);
|
|
781
|
+
return MergedIterator.iterate({
|
|
782
|
+
Aggregate: MergedIterator.iterate(iterators, true, data => {
|
|
783
|
+
return Object.values(data).pop();
|
|
784
|
+
})
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
this.schema = this.getGraphQLSchema(rootFields, mutations, subscriptions);
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Extract the list of event names from the GraphQL selection set (excluding latestEventTime)
|
|
793
|
+
* @param info - GraphQL resolve info containing field selections
|
|
794
|
+
* @returns array of event name strings the client subscribed to
|
|
795
|
+
*/
|
|
796
|
+
getSubscribedEvents(info) {
|
|
797
|
+
return (info.fieldNodes[0].selectionSet?.selections || [])
|
|
798
|
+
.filter(node => node.kind === "Field" && node.name.value !== "latestEventTime")
|
|
799
|
+
.map(n => n.name.value);
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Create an async iterator that streams query results, updating when store events occur
|
|
803
|
+
* @param model - model class to query
|
|
804
|
+
* @param plural - plural name used as the iterator prefix key
|
|
805
|
+
* @param query - WebdaQL query string
|
|
806
|
+
* @param context - web context for permission checks
|
|
807
|
+
* @returns async iterator yielding updated query results on store changes
|
|
808
|
+
*/
|
|
809
|
+
async registerAsyncIteratorQuery(model, plural, query, context) {
|
|
810
|
+
let result = await model.query(query);
|
|
811
|
+
const queryInfo = new WebdaQL.QueryValidator(query);
|
|
812
|
+
const updatedCallback = async (evt) => {
|
|
813
|
+
this.log("TRACE", "Event from", evt.emitterId, evt.object_id);
|
|
814
|
+
if (!result.results.find(e => evt.object_id === e.getUUID()))
|
|
815
|
+
return;
|
|
816
|
+
// We rely on the cache of the store to get the full object
|
|
817
|
+
// We let the other listeners finish before returning the object
|
|
818
|
+
await new Promise(resolve => nextTick(resolve));
|
|
819
|
+
result.results = await Promise.all(result.results.map(r => (r.getUUID() === evt.object_id ? model.ref(evt.object_id).get() : r)));
|
|
820
|
+
return {
|
|
821
|
+
continuationToken: result.continuationToken,
|
|
822
|
+
results: result.results
|
|
823
|
+
};
|
|
824
|
+
};
|
|
825
|
+
const events = {
|
|
826
|
+
Updated: updatedCallback,
|
|
827
|
+
// Deleted is different as we need to return null
|
|
828
|
+
Deleted: async (evt) => {
|
|
829
|
+
if (!result.results.find(e => evt.object_id === e.getUUID()))
|
|
830
|
+
return;
|
|
831
|
+
result = await model.query(query);
|
|
832
|
+
return result;
|
|
833
|
+
},
|
|
834
|
+
Created: async (evt) => {
|
|
835
|
+
// If object match the query and is not in the result and can be read by the user
|
|
836
|
+
if (queryInfo.eval(evt.object) && !queryInfo.getOffset() && evt.object.canAct(context, "get")) {
|
|
837
|
+
// Should check with the order by of the query to see if we need to recompute
|
|
838
|
+
result = await model.query(query);
|
|
839
|
+
return result;
|
|
840
|
+
}
|
|
841
|
+
return;
|
|
842
|
+
},
|
|
843
|
+
Patched: updatedCallback,
|
|
844
|
+
PartialUpdated: updatedCallback
|
|
845
|
+
};
|
|
846
|
+
// Listen on the model's repository typed events (Created/Updated/...) instead
|
|
847
|
+
// of the legacy Store.* events.
|
|
848
|
+
return new EventIterator(useRepository(model), events, plural, result).iterate();
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Create an async iterator that streams a single model instance, updating on store events
|
|
852
|
+
* @param model - model class the instance belongs to
|
|
853
|
+
* @param uuid - UUID of the specific instance to watch
|
|
854
|
+
* @param context - web context for permission checks
|
|
855
|
+
* @param identifier - optional key used as the iterator prefix (defaults to model identifier)
|
|
856
|
+
* @returns async iterator yielding the updated model on each store change
|
|
857
|
+
*/
|
|
858
|
+
async registerAsyncIterator(model, uuid, context, identifier) {
|
|
859
|
+
const updatedCallback = async (evt) => {
|
|
860
|
+
this.log("INFO", "Event from", evt.emitterId, evt.object_id);
|
|
861
|
+
if (evt.object_id !== uuid)
|
|
862
|
+
return;
|
|
863
|
+
// We rely on the cache of the store to get the full object
|
|
864
|
+
// We let the other listeners finish before returning the object
|
|
865
|
+
await new Promise(resolve => nextTick(resolve));
|
|
866
|
+
return model.ref(evt.object_id).get();
|
|
867
|
+
};
|
|
868
|
+
const events = {
|
|
869
|
+
Updated: updatedCallback,
|
|
870
|
+
// Deleted is different as we need to return null
|
|
871
|
+
Deleted: evt => {
|
|
872
|
+
if (evt.object_id !== uuid)
|
|
873
|
+
return;
|
|
874
|
+
return null;
|
|
875
|
+
},
|
|
876
|
+
Patched: updatedCallback,
|
|
877
|
+
PartialUpdated: updatedCallback
|
|
878
|
+
};
|
|
879
|
+
const modelInstance = await model.ref(uuid).get();
|
|
880
|
+
// Ensure we have the permission to get the object
|
|
881
|
+
if ((await modelInstance?.canAct?.(context, "get")) !== true) {
|
|
882
|
+
throw new GraphQLError("Permission denied", {
|
|
883
|
+
extensions: {
|
|
884
|
+
code: "PERMISSION_DENIED"
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
// Listen on the model's repository typed events instead of legacy Store.* events.
|
|
889
|
+
return new EventIterator(useRepository(model), events, identifier || useModelMetadata(model)?.Identifier, modelInstance).iterate();
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Create an async iterator for raw model events (not full objects), filtered by event name and UUID
|
|
893
|
+
* @param model - model class to listen on
|
|
894
|
+
* @param uuid - UUID to filter events for, or null for all instances
|
|
895
|
+
* @param events - list of event names the client wants to receive
|
|
896
|
+
* @param context - web context for authorization checks
|
|
897
|
+
* @param identifier - key used as the iterator prefix
|
|
898
|
+
* @returns async iterator yielding event payloads with latestEventTime
|
|
899
|
+
*/
|
|
900
|
+
async registerAsyncEventIterator(model, uuid, events, context, identifier) {
|
|
901
|
+
const updatedCallback = eventName => async (evt) => {
|
|
902
|
+
if (uuid === null || evt.object_id !== uuid)
|
|
903
|
+
return;
|
|
904
|
+
// We rely on the cache of the store to get the full object
|
|
905
|
+
// We let the other listeners finish before returning the object
|
|
906
|
+
return { latestEventTime: Date.now(), [eventName]: evt };
|
|
907
|
+
};
|
|
908
|
+
const eventsMap = {};
|
|
909
|
+
const modelInstance = uuid !== null ? await model.ref(uuid).get() : undefined;
|
|
910
|
+
events
|
|
911
|
+
.filter(e => useCore().getModelStore(model)?.authorizeClientEvent?.(e, context) !== false)
|
|
912
|
+
.forEach(e => (eventsMap[e] = updatedCallback(e)));
|
|
913
|
+
// Ensure we have the permission to listen to the object
|
|
914
|
+
if (Object.keys(eventsMap).length === 0) {
|
|
915
|
+
throw new GraphQLError("Permission denied", {
|
|
916
|
+
extensions: {
|
|
917
|
+
code: "PERMISSION_DENIED"
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
// Source events from the model's repository; authorization above still
|
|
922
|
+
// consults the store via authorizeClientEvent.
|
|
923
|
+
return new EventIterator(useRepository(model), eventsMap, identifier, {
|
|
924
|
+
logout: { evt: "nok?" }
|
|
925
|
+
}).iterate();
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Assemble the final GraphQLSchema from root query fields, mutations, and subscriptions
|
|
929
|
+
* @param rootFields - top-level query fields
|
|
930
|
+
* @param mutations - mutation fields
|
|
931
|
+
* @param subscription - subscription fields
|
|
932
|
+
* @returns the constructed GraphQLSchema
|
|
933
|
+
*/
|
|
934
|
+
getGraphQLSchema(rootFields, mutations = {}, subscription = {}) {
|
|
935
|
+
// Emit Webda.GraphQL.Schema to allow other services to contribute
|
|
936
|
+
this.emit("Webda.GraphQL.Schema", {
|
|
937
|
+
rootFields,
|
|
938
|
+
mutations,
|
|
939
|
+
subscription
|
|
940
|
+
});
|
|
941
|
+
return new GraphQLSchema({
|
|
942
|
+
query: new GraphQLObjectType({
|
|
943
|
+
name: "Query",
|
|
944
|
+
fields: rootFields
|
|
945
|
+
}),
|
|
946
|
+
types: Object.values(this.modelsMap),
|
|
947
|
+
subscription: Object.keys({ ...subscription }).length > 0
|
|
948
|
+
? new GraphQLObjectType({ fields: { ...subscription }, name: "Subscription" })
|
|
949
|
+
: null,
|
|
950
|
+
mutation: Object.keys(mutations).length > 0 ? new GraphQLObjectType({ fields: mutations, name: "Mutations" }) : null
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* @override
|
|
955
|
+
*/
|
|
956
|
+
async init() {
|
|
957
|
+
var _b;
|
|
958
|
+
await super.init();
|
|
959
|
+
// If not define then fallback to debug mode
|
|
960
|
+
(_b = this.parameters).exposeGraphiQL ?? (_b.exposeGraphiQL = useCore().isDebug());
|
|
961
|
+
this.app = useApplication();
|
|
962
|
+
this.generateSchema();
|
|
963
|
+
// Generate GraphQL schema
|
|
964
|
+
this.handler = createHandler({
|
|
965
|
+
schema: this.schema,
|
|
966
|
+
context: (req, _) => {
|
|
967
|
+
return req.context;
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
// Define the mechanism to handle the websocket
|
|
971
|
+
this.wsHandler = makeServer({
|
|
972
|
+
schema: this.schema,
|
|
973
|
+
context: async (req) => {
|
|
974
|
+
// @ts-ignore
|
|
975
|
+
return req.extra?.context || {};
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
this.wss.on("connection", async (socket, request) => {
|
|
979
|
+
// a new socket opened, let graphql-ws take over
|
|
980
|
+
const closed = this.wsHandler.opened({
|
|
981
|
+
protocol: socket.protocol, // will be validated
|
|
982
|
+
send: data => new Promise((resolve, reject) => {
|
|
983
|
+
socket.send(data, err => (err ? reject(err) : resolve()));
|
|
984
|
+
}), // control your data flow by timing the promise resolve
|
|
985
|
+
close: (code, reason) => socket.close(code, reason), // there are protocol standard closures
|
|
986
|
+
onMessage: cb => socket.on("message", async (event) => {
|
|
987
|
+
try {
|
|
988
|
+
// wait for the the operation to complete
|
|
989
|
+
// - if init message, waits for connect
|
|
990
|
+
// - if query/mutation, waits for result
|
|
991
|
+
// - if subscription, waits for complete
|
|
992
|
+
await cb(event.toString());
|
|
993
|
+
}
|
|
994
|
+
catch (err) {
|
|
995
|
+
// all errors that could be thrown during the
|
|
996
|
+
// execution of operations will be caught here
|
|
997
|
+
socket.close(CloseCode.InternalServerError, err.message);
|
|
998
|
+
}
|
|
999
|
+
})
|
|
1000
|
+
},
|
|
1001
|
+
// pass values to the `extra` field in the context
|
|
1002
|
+
{ socket, request, context: request.webdaContext });
|
|
1003
|
+
this.log("INFO", "Socket once closed");
|
|
1004
|
+
// notify server that the socket closed
|
|
1005
|
+
socket.once("close", (code, reason) => {
|
|
1006
|
+
this.log("INFO", "Socket closed - clean up Iterators");
|
|
1007
|
+
closed(code, reason.toString());
|
|
1008
|
+
});
|
|
1009
|
+
});
|
|
1010
|
+
return this;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Check whether a model should be handled by this service (always returns true)
|
|
1014
|
+
* @param model - model class to check
|
|
1015
|
+
* @param name - display name of the model
|
|
1016
|
+
* @param context - request context
|
|
1017
|
+
* @returns true if the model should be processed
|
|
1018
|
+
*/
|
|
1019
|
+
handleModel(model, name, context) {
|
|
1020
|
+
//throw new Error("Method not implemented.");
|
|
1021
|
+
return true;
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Serve the GraphQL schema as text or the GraphiQL IDE as HTML
|
|
1025
|
+
* @param ctx - incoming web context
|
|
1026
|
+
*/
|
|
1027
|
+
async schemaRoute(ctx) {
|
|
1028
|
+
const httpContext = ctx.getHttpContext();
|
|
1029
|
+
if (!this.parameters.exposeGraphiQL) {
|
|
1030
|
+
throw new WebdaError.NotFound("GraphiQL not exposed");
|
|
1031
|
+
}
|
|
1032
|
+
if (httpContext.getHeader("X-GraphiQL-Schema") === "true") {
|
|
1033
|
+
ctx.writeHead(200, { "Content-Type": "application/graphql" });
|
|
1034
|
+
ctx.write(printSchema(this.schema));
|
|
1035
|
+
}
|
|
1036
|
+
else {
|
|
1037
|
+
ctx.writeHead(200, { "Content-Type": "text/html" });
|
|
1038
|
+
ctx.write(GraphIQL.replace("{{URL}}", httpContext.getAbsoluteUrl()).replace("{{WSURL}}", httpContext.getAbsoluteUrl().replace("http", "ws")));
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Handle POST requests to the GraphQL endpoint
|
|
1043
|
+
* @param ctx - incoming web context with the GraphQL query body
|
|
1044
|
+
*/
|
|
1045
|
+
async endpoint(ctx) {
|
|
1046
|
+
const httpContext = ctx.getHttpContext();
|
|
1047
|
+
ctx.setExtension("graphql", { count: 0 });
|
|
1048
|
+
const [body, init] = await this.handler({
|
|
1049
|
+
url: httpContext.getUrl(),
|
|
1050
|
+
method: httpContext.getMethod(),
|
|
1051
|
+
headers: httpContext.getHeaders(),
|
|
1052
|
+
body: await ctx.getInput(),
|
|
1053
|
+
raw: ctx,
|
|
1054
|
+
context: ctx
|
|
1055
|
+
});
|
|
1056
|
+
ctx.writeHead(init.status, init.headers);
|
|
1057
|
+
ctx.write(body);
|
|
1058
|
+
}
|
|
1059
|
+
},
|
|
1060
|
+
(() => {
|
|
1061
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
1062
|
+
_schemaRoute_decorators = [Route(".", ["GET"], { hidden: true })];
|
|
1063
|
+
_endpoint_decorators = [Route(".", ["POST"], {
|
|
1064
|
+
post: {
|
|
1065
|
+
summary: "GraphQL endpoint",
|
|
1066
|
+
requestBody: {
|
|
1067
|
+
content: {
|
|
1068
|
+
"application/json": {
|
|
1069
|
+
schema: {
|
|
1070
|
+
type: "object",
|
|
1071
|
+
properties: {
|
|
1072
|
+
query: { type: "string" },
|
|
1073
|
+
variables: { type: "object" }
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
})];
|
|
1081
|
+
__esDecorate(_a, null, _schemaRoute_decorators, { kind: "method", name: "schemaRoute", static: false, private: false, access: { has: obj => "schemaRoute" in obj, get: obj => obj.schemaRoute }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1082
|
+
__esDecorate(_a, null, _endpoint_decorators, { kind: "method", name: "endpoint", static: false, private: false, access: { has: obj => "endpoint" in obj, get: obj => obj.endpoint }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
1083
|
+
if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
1084
|
+
})(),
|
|
1085
|
+
_a;
|
|
1086
|
+
})();
|
|
1087
|
+
export { GraphQLService };
|
|
1088
|
+
//# sourceMappingURL=graphql.service.js.map
|