@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.
@@ -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