@webda/graphql 4.0.0-beta.2 → 4.0.0-beta.4

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