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