@webda/graphql 4.0.0-beta.6 → 4.0.0-beta.7
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 +5 -4
- package/lib/graphql.service.d.ts +91 -3
- package/lib/graphql.service.d.ts.map +1 -1
- package/lib/graphql.service.js +299 -31
- package/lib/graphql.service.js.map +1 -1
- package/lib/mutations.d.ts +8 -0
- package/lib/mutations.d.ts.map +1 -1
- package/lib/mutations.js +27 -0
- package/lib/mutations.js.map +1 -1
- package/lib/operations.d.ts +110 -0
- package/lib/operations.d.ts.map +1 -0
- package/lib/operations.js +221 -0
- package/lib/operations.js.map +1 -0
- package/lib/types/any.d.ts.map +1 -1
- package/lib/types/any.js +27 -19
- package/lib/types/any.js.map +1 -1
- package/lib/types/date.d.ts.map +1 -1
- package/lib/types/date.js +4 -2
- package/lib/types/date.js.map +1 -1
- package/package.json +11 -6
- package/webda.module.json +10 -0
package/lib/graphql.service.js
CHANGED
|
@@ -32,10 +32,10 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
|
|
|
32
32
|
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
33
|
done = true;
|
|
34
34
|
};
|
|
35
|
-
import { DomainService, DomainServiceParameters, Route, WebdaError, useApplication, useCore, useCoreEvents, useModelMetadata, useRepository, isModelActionAllowed, queryModelWithPermissions, getClientWritableAttributes, assertNoPrivateFields, assertFilterOnly } from "@webda/core";
|
|
35
|
+
import { DomainService, DomainServiceParameters, Route, WebdaError, useApplication, useCore, useCoreEvents, useModelMetadata, useRepository, isModelActionAllowed, queryModelWithPermissions, getClientWritableAttributes, assertNoPrivateFields, assertFilterOnly, callOperation, canCallOperation, createOperationFilter, useInstanceStorage, runWithInstanceStorage, emitCoreEvent, runWithContext, useRouter } from "@webda/core";
|
|
36
36
|
import * as WebdaQL from "@webda/ql";
|
|
37
37
|
import { EventIterator, MergedIterator } from "@webda/runtime";
|
|
38
|
-
import { GraphQLBoolean, GraphQLError, GraphQLInputObjectType, GraphQLList, GraphQLObjectType, GraphQLSchema, GraphQLString, printSchema } from "graphql";
|
|
38
|
+
import { GraphQLBoolean, GraphQLError, GraphQLInputObjectType, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLSchema, GraphQLString, printSchema } from "graphql";
|
|
39
39
|
import { createHandler } from "graphql-http";
|
|
40
40
|
import { CloseCode, makeServer } from "graphql-ws";
|
|
41
41
|
import { nextTick } from "process";
|
|
@@ -43,7 +43,8 @@ import { WebSocketServer } from "ws";
|
|
|
43
43
|
import { AnyScalarType } from "./types/any.js";
|
|
44
44
|
import { DateScalar } from "./types/date.js";
|
|
45
45
|
import { GraphQLLong } from "./types/long.js";
|
|
46
|
-
import { createFromInput, isInputAttribute, loadForAction, updateFromInput } from "./mutations.js";
|
|
46
|
+
import { createFromInput, isInputAttribute, loadForAction, operationError, updateFromInput } from "./mutations.js";
|
|
47
|
+
import { GRAPHQL_NAME, GraphQLOperationContext, graphqlPlacement, operationTypeName, publicCopy } from "./operations.js";
|
|
47
48
|
const GraphIQL = `
|
|
48
49
|
<!doctype html>
|
|
49
50
|
<html lang="en">
|
|
@@ -140,6 +141,7 @@ export class GraphQLParameters extends DomainServiceParameters {
|
|
|
140
141
|
this.userModel ?? (this.userModel = "User");
|
|
141
142
|
this.exposeMe ?? (this.exposeMe = true);
|
|
142
143
|
this.globalSubscription ?? (this.globalSubscription = true);
|
|
144
|
+
this.exposeOperations ?? (this.exposeOperations = ["*"]);
|
|
143
145
|
return this;
|
|
144
146
|
}
|
|
145
147
|
}
|
|
@@ -161,6 +163,10 @@ let GraphQLService = (() => {
|
|
|
161
163
|
constructor() {
|
|
162
164
|
super(...arguments);
|
|
163
165
|
this.schema = __runInitializers(this, _instanceExtraInitializers);
|
|
166
|
+
/**
|
|
167
|
+
* Generated named types, by name: a name is generated once per shape (reset by generateSchema)
|
|
168
|
+
*/
|
|
169
|
+
this.namedTypes = new Map();
|
|
164
170
|
}
|
|
165
171
|
/**
|
|
166
172
|
* Load and initialize GraphQL service parameters
|
|
@@ -170,6 +176,14 @@ let GraphQLService = (() => {
|
|
|
170
176
|
loadParameters(params) {
|
|
171
177
|
return new GraphQLParameters().load(params);
|
|
172
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Parameters with their defaults (the core builds a service configuration through this, not loadParameters)
|
|
181
|
+
* @param params - raw partial configuration
|
|
182
|
+
* @returns initialized GraphQLParameters
|
|
183
|
+
*/
|
|
184
|
+
static createConfiguration(params = {}) {
|
|
185
|
+
return new GraphQLParameters().load(params);
|
|
186
|
+
}
|
|
173
187
|
/**
|
|
174
188
|
* Resolve a JSON Schema property by following $ref pointers
|
|
175
189
|
* @param prop - JSON Schema property that may contain $ref
|
|
@@ -188,6 +202,41 @@ let GraphQLService = (() => {
|
|
|
188
202
|
}
|
|
189
203
|
return prop || { type: "null" };
|
|
190
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* One GraphQL type per name: reuse the type already generated for this name and shape
|
|
207
|
+
* @param name - type name
|
|
208
|
+
* @param shape - what makes two types of this name the same (kind and field names)
|
|
209
|
+
* @param create - builds the type the first time
|
|
210
|
+
* @returns the type
|
|
211
|
+
* @throws Error when the name was generated for another shape
|
|
212
|
+
*/
|
|
213
|
+
namedType(name, shape, create) {
|
|
214
|
+
this.namedTypes ?? (this.namedTypes = new Map());
|
|
215
|
+
const known = this.namedTypes.get(name);
|
|
216
|
+
if (known) {
|
|
217
|
+
if (known.shape !== shape) {
|
|
218
|
+
throw new Error(`GraphQL type ${name} is generated for two different schemas (${known.shape} / ${shape}): give one of them another title`);
|
|
219
|
+
}
|
|
220
|
+
return known.type;
|
|
221
|
+
}
|
|
222
|
+
const type = create();
|
|
223
|
+
this.namedTypes.set(name, { type, shape });
|
|
224
|
+
return type;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Reserve a type name for a type built later (its entry gets the type once built)
|
|
228
|
+
* @param name - type name
|
|
229
|
+
* @param shape - identity of the reserving type
|
|
230
|
+
* @throws Error when the name is already taken by another shape
|
|
231
|
+
*/
|
|
232
|
+
reserveName(name, shape) {
|
|
233
|
+
this.namedTypes ?? (this.namedTypes = new Map());
|
|
234
|
+
const known = this.namedTypes.get(name);
|
|
235
|
+
if (known && known.shape !== shape) {
|
|
236
|
+
throw new Error(`GraphQL type ${name} is generated for two different schemas (${known.shape} / ${shape}): give one of them another title`);
|
|
237
|
+
}
|
|
238
|
+
this.namedTypes.set(name, known || { type: undefined, shape });
|
|
239
|
+
}
|
|
191
240
|
/**
|
|
192
241
|
* Convert a JSON Schema to a GraphQL type definition
|
|
193
242
|
* @param schema - JSON Schema to convert
|
|
@@ -196,6 +245,17 @@ let GraphQLService = (() => {
|
|
|
196
245
|
* @returns object with the GraphQL type and optional description, or undefined for null types
|
|
197
246
|
*/
|
|
198
247
|
getGraphQLSchemaFromSchema(schema, defaultName, input) {
|
|
248
|
+
// `type: ["string", "null"]`: the nullable form of one type (GraphQL fields are nullable already)
|
|
249
|
+
if (Array.isArray(schema?.type)) {
|
|
250
|
+
const types = schema.type.filter(t => t !== "null");
|
|
251
|
+
if (types.length === 0) {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
if (types.length > 1) {
|
|
255
|
+
return { type: AnyScalarType, description: schema.description };
|
|
256
|
+
}
|
|
257
|
+
return this.getGraphQLSchemaFromSchema({ ...schema, type: types[0] }, defaultName, input);
|
|
258
|
+
}
|
|
199
259
|
let type;
|
|
200
260
|
if (!schema || !schema.type) {
|
|
201
261
|
type = AnyScalarType;
|
|
@@ -242,10 +302,12 @@ let GraphQLService = (() => {
|
|
|
242
302
|
this.log("DEBUG", "Return map for", defaultName, "because no fields");
|
|
243
303
|
return { type: AnyScalarType, description: "Map" };
|
|
244
304
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
305
|
+
// A titled input type gets its own name: the output type of the same title already uses the title
|
|
306
|
+
const name = schema.title ? (input ? `${schema.title}Input` : schema.title) : defaultName;
|
|
307
|
+
type = this.namedType(name, `${input ? "input" : "output"}:${Object.keys(fields)
|
|
308
|
+
.sort()
|
|
309
|
+
.map(k => `${k}:${fields[k].type}`)
|
|
310
|
+
.join(",")}`, () => (input ? new GraphQLInputObjectType({ fields, name }) : new GraphQLObjectType({ fields, name })));
|
|
249
311
|
}
|
|
250
312
|
return { type, description: schema.description };
|
|
251
313
|
}
|
|
@@ -490,17 +552,40 @@ let GraphQLService = (() => {
|
|
|
490
552
|
super.resolve();
|
|
491
553
|
// Set-up ws server
|
|
492
554
|
this.wss = new WebSocketServer({ noServer: true });
|
|
555
|
+
// The HTTP server events fire outside the instance storage: re-enter it
|
|
556
|
+
const storage = useInstanceStorage();
|
|
493
557
|
useCoreEvents("Webda.Init.Http", (http) => {
|
|
494
558
|
http.on("upgrade", (req, socket, head) => {
|
|
495
|
-
|
|
559
|
+
runWithInstanceStorage(storage, () => {
|
|
560
|
+
if (req.url !== this.parameters.url)
|
|
561
|
+
return;
|
|
496
562
|
(async () => {
|
|
497
|
-
|
|
498
|
-
|
|
563
|
+
const httpServer = Object.values(useCore().getServices()).find(s => s.server === http && typeof s.getContextFromRequest === "function");
|
|
564
|
+
req.webdaContext ?? (req.webdaContext = await (httpServer ?? useCore()).getContextFromRequest(req));
|
|
565
|
+
const context = req.webdaContext;
|
|
566
|
+
await context.init();
|
|
567
|
+
// Same checks as a normal request: Webda.Request listeners and the router request filters
|
|
568
|
+
const allowed = await runWithContext(context, async () => {
|
|
569
|
+
try {
|
|
570
|
+
emitCoreEvent("Webda.Request", { context });
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
// listener error
|
|
574
|
+
}
|
|
575
|
+
return useRouter().checkRequest(context);
|
|
576
|
+
});
|
|
577
|
+
if (!allowed) {
|
|
578
|
+
socket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
499
581
|
this.wss.handleUpgrade(req, socket, head, ws => {
|
|
500
582
|
this.wss.emit("connection", ws, req);
|
|
501
583
|
});
|
|
502
|
-
})()
|
|
503
|
-
|
|
584
|
+
})().catch(err => {
|
|
585
|
+
this.log("ERROR", "GraphQL WebSocket upgrade failed", err);
|
|
586
|
+
socket.destroy();
|
|
587
|
+
});
|
|
588
|
+
});
|
|
504
589
|
});
|
|
505
590
|
});
|
|
506
591
|
return this;
|
|
@@ -514,6 +599,10 @@ let GraphQLService = (() => {
|
|
|
514
599
|
const subscriptions = {};
|
|
515
600
|
const models = this.app.getModels();
|
|
516
601
|
this.modelsMap = {};
|
|
602
|
+
this.namedTypes = new Map();
|
|
603
|
+
// First pass: reserve every exposed model name (and its Input) before any type is built, so a nested
|
|
604
|
+
// titled type of the same name fails whatever the model order
|
|
605
|
+
const exposed = [];
|
|
517
606
|
for (const i in models) {
|
|
518
607
|
const model = models[i];
|
|
519
608
|
const metadata = useModelMetadata(model);
|
|
@@ -534,6 +623,11 @@ let GraphQLService = (() => {
|
|
|
534
623
|
if (!schema)
|
|
535
624
|
continue;
|
|
536
625
|
const name = (metadata.ShortName || i.split("/").pop()).replace("/", "_");
|
|
626
|
+
this.reserveName(name, `model ${i}`);
|
|
627
|
+
this.reserveName(`${name}Input`, `model input ${i}`);
|
|
628
|
+
exposed.push({ i, model, metadata, schema, name });
|
|
629
|
+
}
|
|
630
|
+
for (const { i, model, metadata, schema, name } of exposed) {
|
|
537
631
|
this.log("INFO", "Add GraphQL type", name);
|
|
538
632
|
const modelGraph = metadata.Relations;
|
|
539
633
|
this.modelsMap[i] = new GraphQLObjectType({
|
|
@@ -544,6 +638,8 @@ let GraphQLService = (() => {
|
|
|
544
638
|
fields: this.getGraphQLFieldsFromSchema(schema, name + "Input", modelGraph, true, getClientWritableAttributes(model)),
|
|
545
639
|
name: name + "Input"
|
|
546
640
|
});
|
|
641
|
+
this.namedTypes.get(name).type = this.modelsMap[i];
|
|
642
|
+
this.namedTypes.get(`${name}Input`).type = input;
|
|
547
643
|
const actionsName = Object.keys(metadata.Actions);
|
|
548
644
|
if (!actionsName.includes("create")) {
|
|
549
645
|
mutations[`create${name}`] = {
|
|
@@ -619,7 +715,8 @@ let GraphQLService = (() => {
|
|
|
619
715
|
},
|
|
620
716
|
subscribe: async (_source, args, context) => {
|
|
621
717
|
this.log("DEBUG", "Subscription called on", args);
|
|
622
|
-
|
|
718
|
+
// The payload is keyed by the field name: the default resolver reads it from there
|
|
719
|
+
return this.registerAsyncIterator(model, args[uuidField], context, this.transformName(i.split("/").pop()));
|
|
623
720
|
}
|
|
624
721
|
};
|
|
625
722
|
const events = metadata.Events || [];
|
|
@@ -776,6 +873,9 @@ let GraphQLService = (() => {
|
|
|
776
873
|
for (const i in rootFields) {
|
|
777
874
|
subscriptions[i] ?? (subscriptions[i] = { ...rootFields[i], resolve: undefined });
|
|
778
875
|
}
|
|
876
|
+
// Operations (service operations, model and behaviour actions) next to the model schema; added after the copy
|
|
877
|
+
// above on purpose: a query operation is not a subscription
|
|
878
|
+
this.addOperationFields(rootFields, mutations, subscriptions);
|
|
779
879
|
if (this.parameters.globalSubscription) {
|
|
780
880
|
// Create global type based on all previous subscriptions
|
|
781
881
|
subscriptions["Aggregate"] = {
|
|
@@ -811,6 +911,149 @@ let GraphQLService = (() => {
|
|
|
811
911
|
}
|
|
812
912
|
this.schema = this.getGraphQLSchema(rootFields, mutations, subscriptions);
|
|
813
913
|
}
|
|
914
|
+
/**
|
|
915
|
+
* Expose the registered operations, except the model CRUD the model schema already serves: read-only ones as
|
|
916
|
+
* queries, the others as mutations, server-streaming ones as subscriptions
|
|
917
|
+
* @param rootFields - query fields
|
|
918
|
+
* @param mutations - mutation fields
|
|
919
|
+
* @param subscriptions - subscription fields
|
|
920
|
+
* @throws Error when a field name is already defined
|
|
921
|
+
*/
|
|
922
|
+
addOperationFields(rootFields, mutations, subscriptions) {
|
|
923
|
+
const exposed = createOperationFilter(this.parameters.exposeOperations);
|
|
924
|
+
const targets = { query: rootFields, mutation: mutations, subscription: subscriptions };
|
|
925
|
+
const owners = new Map();
|
|
926
|
+
const operations = useInstanceStorage().operations;
|
|
927
|
+
for (const id of Object.keys(operations).sort()) {
|
|
928
|
+
const op = operations[id];
|
|
929
|
+
if (!exposed(id))
|
|
930
|
+
continue;
|
|
931
|
+
const placement = graphqlPlacement(id, op);
|
|
932
|
+
if (!placement)
|
|
933
|
+
continue;
|
|
934
|
+
const target = targets[placement.kind];
|
|
935
|
+
const key = `${placement.kind}.${placement.name}`;
|
|
936
|
+
if (placement.kind === "subscription" && placement.name === "Aggregate" && this.parameters.globalSubscription) {
|
|
937
|
+
throw new Error(`GraphQL subscription field Aggregate of operation ${id} is already defined by the global Aggregate subscription (globalSubscription): rename it with @Operation({ graphql: { subscription: "…" } })`);
|
|
938
|
+
}
|
|
939
|
+
if (target[placement.name]) {
|
|
940
|
+
throw new Error(`GraphQL ${placement.kind} field ${placement.name} of operation ${id} is already defined by ${owners.get(key) ?? "the model schema"}: rename it with @Operation({ graphql: { ${placement.kind}: "…" } })`);
|
|
941
|
+
}
|
|
942
|
+
owners.set(key, `operation ${id}`);
|
|
943
|
+
const base = operationTypeName(id);
|
|
944
|
+
const { args, toInput } = this.operationArgs(op, base);
|
|
945
|
+
const type = this.operationOutputType(op, base);
|
|
946
|
+
const description = op.description || op.summary;
|
|
947
|
+
target[placement.name] =
|
|
948
|
+
placement.kind === "subscription"
|
|
949
|
+
? {
|
|
950
|
+
type,
|
|
951
|
+
args,
|
|
952
|
+
description,
|
|
953
|
+
subscribe: async (_source, fieldArgs, context) => this.subscribeOperation(id, op, toInput(fieldArgs), context),
|
|
954
|
+
resolve: (chunk) => chunk
|
|
955
|
+
}
|
|
956
|
+
: {
|
|
957
|
+
type,
|
|
958
|
+
args,
|
|
959
|
+
description,
|
|
960
|
+
resolve: async (_source, fieldArgs, context) => this.runOperation(id, op, toInput(fieldArgs), context)
|
|
961
|
+
};
|
|
962
|
+
this.log("INFO", `Add GraphQL ${placement.kind}`, placement.name, "for operation", id);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* GraphQL arguments of an operation: one per input schema property, or a single `input` argument
|
|
967
|
+
* @param op - operation
|
|
968
|
+
* @param base - type name prefix
|
|
969
|
+
* @returns the arguments and how to turn them back into the operation input
|
|
970
|
+
*/
|
|
971
|
+
operationArgs(op, base) {
|
|
972
|
+
const schema = op.input && op.input !== "void" ? this.app.getSchema(op.input) : undefined;
|
|
973
|
+
if (!schema) {
|
|
974
|
+
return { args: {}, toInput: () => undefined };
|
|
975
|
+
}
|
|
976
|
+
const resolved = this.getJsonSchemaDefinition(schema, schema.definitions);
|
|
977
|
+
const names = Object.keys(resolved.properties ?? {}).filter(name => !name.startsWith("__"));
|
|
978
|
+
if (resolved.type === "object" && names.length > 0 && names.every(name => GRAPHQL_NAME.test(name))) {
|
|
979
|
+
const required = new Set(resolved.required ?? []);
|
|
980
|
+
const args = {};
|
|
981
|
+
for (const name of names) {
|
|
982
|
+
const converted = this.getGraphQLSchemaFromSchema(this.getJsonSchemaDefinition(resolved.properties[name], resolved.definitions ?? schema.definitions), `${base}Input_${name}`, true);
|
|
983
|
+
// A property the converter cannot map still is an argument: the Object scalar takes anything
|
|
984
|
+
const type = (converted?.type ?? AnyScalarType);
|
|
985
|
+
args[name] = {
|
|
986
|
+
type: required.has(name) ? new GraphQLNonNull(type) : type,
|
|
987
|
+
description: converted?.description
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
return { args, toInput: fieldArgs => ({ ...fieldArgs }) };
|
|
991
|
+
}
|
|
992
|
+
const converted = this.getGraphQLSchemaFromSchema(resolved, `${base}Input`, true);
|
|
993
|
+
return {
|
|
994
|
+
args: { input: { type: (converted?.type ?? AnyScalarType) } },
|
|
995
|
+
toInput: fieldArgs => fieldArgs.input
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* GraphQL type of an operation result
|
|
1000
|
+
* @param op - operation
|
|
1001
|
+
* @param base - type name prefix
|
|
1002
|
+
* @returns the model type for a model output, Boolean for void, else the converted output schema
|
|
1003
|
+
*/
|
|
1004
|
+
operationOutputType(op, base) {
|
|
1005
|
+
if (!op.output || op.output === "void") {
|
|
1006
|
+
return GraphQLBoolean;
|
|
1007
|
+
}
|
|
1008
|
+
if (this.modelsMap[op.output]) {
|
|
1009
|
+
return this.modelsMap[op.output];
|
|
1010
|
+
}
|
|
1011
|
+
const schema = this.app.getSchema(op.output);
|
|
1012
|
+
const converted = schema &&
|
|
1013
|
+
this.getGraphQLSchemaFromSchema(this.getJsonSchemaDefinition(schema, schema.definitions), `${base}Output`);
|
|
1014
|
+
return (converted?.type ?? AnyScalarType);
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Run an operation for a query or mutation field
|
|
1018
|
+
* @param id - operation id
|
|
1019
|
+
* @param op - operation
|
|
1020
|
+
* @param input - operation input
|
|
1021
|
+
* @param context - the GraphQL request context
|
|
1022
|
+
* @returns the field value
|
|
1023
|
+
*/
|
|
1024
|
+
async runOperation(id, op, input, context) {
|
|
1025
|
+
this.countOperation(context);
|
|
1026
|
+
const ctx = new GraphQLOperationContext(context, input);
|
|
1027
|
+
await ctx.init();
|
|
1028
|
+
try {
|
|
1029
|
+
await callOperation(ctx, id);
|
|
1030
|
+
}
|
|
1031
|
+
catch (err) {
|
|
1032
|
+
throw operationError(err);
|
|
1033
|
+
}
|
|
1034
|
+
if (!op.output || op.output === "void") {
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
return this.modelsMap[op.output] ? ctx.result : publicCopy(ctx.result);
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Start a server-streaming operation for a subscription field
|
|
1041
|
+
* @param id - operation id
|
|
1042
|
+
* @param op - operation
|
|
1043
|
+
* @param input - operation input
|
|
1044
|
+
* @param context - the GraphQL request context
|
|
1045
|
+
* @returns the chunks; leaving the subscription cancels the operation
|
|
1046
|
+
*/
|
|
1047
|
+
async subscribeOperation(id, op, input, context) {
|
|
1048
|
+
this.countOperation(context);
|
|
1049
|
+
const ctx = new GraphQLOperationContext(context, input, this.modelsMap[op.output] ? chunk => chunk : publicCopy);
|
|
1050
|
+
await ctx.init();
|
|
1051
|
+
if (!(await canCallOperation(ctx, id, { input }))) {
|
|
1052
|
+
throw operationError(new WebdaError.Forbidden(`${id} PermissionDenied`));
|
|
1053
|
+
}
|
|
1054
|
+
callOperation(ctx, id).then(() => ctx.finish(), err => ctx.fail(err));
|
|
1055
|
+
return ctx.chunks();
|
|
1056
|
+
}
|
|
814
1057
|
/**
|
|
815
1058
|
* Extract the list of event names from the GraphQL selection set (excluding latestEventTime)
|
|
816
1059
|
* @param info - GraphQL resolve info containing field selections
|
|
@@ -980,11 +1223,24 @@ let GraphQLService = (() => {
|
|
|
980
1223
|
*/
|
|
981
1224
|
async registerAsyncEventIterator(model, uuid, events, context, identifier) {
|
|
982
1225
|
const updatedCallback = eventName => async (evt) => {
|
|
983
|
-
|
|
1226
|
+
// A class-wide subscription (uuid null) receives the events of every object of the model
|
|
1227
|
+
if (uuid !== null && evt.object_id !== uuid)
|
|
984
1228
|
return;
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1229
|
+
if (uuid === null) {
|
|
1230
|
+
// Only the events of objects the subscriber can read: an object that is gone or unreadable is skipped
|
|
1231
|
+
let object = evt.object;
|
|
1232
|
+
if (!object) {
|
|
1233
|
+
try {
|
|
1234
|
+
object = await model.ref(evt.object_id).get();
|
|
1235
|
+
}
|
|
1236
|
+
catch {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
if (!object || !(await isModelActionAllowed(object, context, "get", model)))
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
return { latestEventTime: Date.now(), [eventName]: publicCopy(evt) };
|
|
988
1244
|
};
|
|
989
1245
|
const eventsMap = {};
|
|
990
1246
|
if (uuid !== null) {
|
|
@@ -1008,6 +1264,30 @@ let GraphQLService = (() => {
|
|
|
1008
1264
|
logout: { evt: "nok?" }
|
|
1009
1265
|
}).iterate();
|
|
1010
1266
|
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Hand a WebSocket message to graphql-ws; a failure is logged and closes the socket without leaking details
|
|
1269
|
+
* @param socket - the WebSocket
|
|
1270
|
+
* @param cb - the graphql-ws message handler
|
|
1271
|
+
* @param data - the message
|
|
1272
|
+
* @param storage - the instance storage to re-enter for the resolvers
|
|
1273
|
+
* @returns a promise settled once the message is handled
|
|
1274
|
+
*/
|
|
1275
|
+
async handleSocketMessage(socket, cb, data, storage) {
|
|
1276
|
+
try {
|
|
1277
|
+
// wait for the the operation to complete
|
|
1278
|
+
// - if init message, waits for connect
|
|
1279
|
+
// - if query/mutation, waits for result
|
|
1280
|
+
// - if subscription, waits for complete
|
|
1281
|
+
// The socket events fire outside the instance storage: re-enter it for the resolvers
|
|
1282
|
+
await runWithInstanceStorage(storage, () => cb(data));
|
|
1283
|
+
}
|
|
1284
|
+
catch (err) {
|
|
1285
|
+
// all errors that could be thrown during the execution of operations will be caught here
|
|
1286
|
+
this.log("ERROR", "GraphQL websocket message failed", err);
|
|
1287
|
+
// A close reason is limited to 123 bytes and must not leak internal details
|
|
1288
|
+
socket.close(CloseCode.InternalServerError, "Internal server error");
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1011
1291
|
/**
|
|
1012
1292
|
* Assemble the final GraphQLSchema from root query fields, mutations, and subscriptions
|
|
1013
1293
|
* @param rootFields - top-level query fields
|
|
@@ -1059,6 +1339,7 @@ let GraphQLService = (() => {
|
|
|
1059
1339
|
return req.extra?.context || {};
|
|
1060
1340
|
}
|
|
1061
1341
|
});
|
|
1342
|
+
const storage = useInstanceStorage();
|
|
1062
1343
|
this.wss.on("connection", async (socket, request) => {
|
|
1063
1344
|
// a new socket opened, let graphql-ws take over
|
|
1064
1345
|
const closed = this.wsHandler.opened({
|
|
@@ -1067,20 +1348,7 @@ let GraphQLService = (() => {
|
|
|
1067
1348
|
socket.send(data, err => (err ? reject(err) : resolve()));
|
|
1068
1349
|
}), // control your data flow by timing the promise resolve
|
|
1069
1350
|
close: (code, reason) => socket.close(code, reason), // there are protocol standard closures
|
|
1070
|
-
onMessage: cb => socket.on("message",
|
|
1071
|
-
try {
|
|
1072
|
-
// wait for the the operation to complete
|
|
1073
|
-
// - if init message, waits for connect
|
|
1074
|
-
// - if query/mutation, waits for result
|
|
1075
|
-
// - if subscription, waits for complete
|
|
1076
|
-
await cb(event.toString());
|
|
1077
|
-
}
|
|
1078
|
-
catch (err) {
|
|
1079
|
-
// all errors that could be thrown during the
|
|
1080
|
-
// execution of operations will be caught here
|
|
1081
|
-
socket.close(CloseCode.InternalServerError, err.message);
|
|
1082
|
-
}
|
|
1083
|
-
})
|
|
1351
|
+
onMessage: cb => socket.on("message", event => this.handleSocketMessage(socket, cb, event.toString(), storage))
|
|
1084
1352
|
},
|
|
1085
1353
|
// pass values to the `extra` field in the context
|
|
1086
1354
|
{ socket, request, context: request.webdaContext });
|