sofa-api 0.16.3 → 0.17.0

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/index.js CHANGED
@@ -10,948 +10,1024 @@ const utils = require('@graphql-tools/utils');
10
10
  const paramCase = require('param-case');
11
11
  const fetch = require('@whatwg-node/fetch');
12
12
  const colors = _interopDefault(require('ansi-colors'));
13
- const router = require('@whatwg-node/router');
13
+ const fets = require('fets');
14
14
  const titleCase = require('title-case');
15
15
 
16
- function getOperationInfo(doc) {
17
- const op = graphql.getOperationAST(doc, null);
18
- if (!op) {
19
- return;
20
- }
21
- return {
22
- operation: op,
23
- name: op.name.value,
24
- variables: op.variableDefinitions || [],
25
- };
16
+ function getOperationInfo(doc) {
17
+ const op = graphql.getOperationAST(doc, null);
18
+ if (!op) {
19
+ return;
20
+ }
21
+ return {
22
+ operation: op,
23
+ name: op.name.value,
24
+ variables: op.variableDefinitions || [],
25
+ };
26
26
  }
27
27
 
28
- function convertName(name) {
29
- return paramCase.paramCase(name);
30
- }
31
- function isNil(val) {
32
- return val == null;
28
+ function convertName(name) {
29
+ return paramCase.paramCase(name);
30
+ }
31
+ function isNil(val) {
32
+ return val == null;
33
33
  }
34
34
 
35
- function parseVariable({ value, variable, schema, }) {
36
- if (isNil(value)) {
37
- return;
38
- }
39
- return resolveVariable({
40
- value,
41
- type: variable.type,
42
- schema,
43
- });
44
- }
45
- function resolveVariable({ value, type, schema, }) {
46
- if (type.kind === graphql.Kind.NAMED_TYPE) {
47
- const namedType = schema.getType(type.name.value);
48
- if (graphql.isScalarType(namedType)) {
49
- // GraphQLBoolean.serialize expects a boolean or a number only
50
- if (graphql.isEqualType(graphql.GraphQLBoolean, namedType)) {
51
- value = (value === 'true' || value === true);
52
- }
53
- return namedType.serialize(value);
54
- }
55
- if (graphql.isInputObjectType(namedType)) {
56
- return value && typeof value === 'object' ? value : JSON.parse(value);
57
- }
58
- return value;
59
- }
60
- if (type.kind === graphql.Kind.LIST_TYPE) {
61
- return (Array.isArray(value) ? value : [value]).map((val) => resolveVariable({
62
- value: val,
63
- type: type.type,
64
- schema,
65
- }));
66
- }
67
- if (type.kind === graphql.Kind.NON_NULL_TYPE) {
68
- return resolveVariable({
69
- value: value,
70
- type: type.type,
71
- schema,
72
- });
73
- }
35
+ function parseVariable({ value, variable, schema, }) {
36
+ if (isNil(value)) {
37
+ return;
38
+ }
39
+ return resolveVariable({
40
+ value,
41
+ type: variable.type,
42
+ schema,
43
+ });
44
+ }
45
+ function resolveVariable({ value, type, schema, }) {
46
+ if (type.kind === graphql.Kind.NAMED_TYPE) {
47
+ const namedType = schema.getType(type.name.value);
48
+ if (graphql.isScalarType(namedType)) {
49
+ // GraphQLBoolean.serialize expects a boolean or a number only
50
+ if (graphql.isEqualType(graphql.GraphQLBoolean, namedType)) {
51
+ value = (value === 'true' || value === true);
52
+ }
53
+ return namedType.serialize(value);
54
+ }
55
+ if (graphql.isInputObjectType(namedType)) {
56
+ return value && typeof value === 'object' ? value : JSON.parse(value);
57
+ }
58
+ return value;
59
+ }
60
+ if (type.kind === graphql.Kind.LIST_TYPE) {
61
+ return (Array.isArray(value) ? value : [value]).map((val) => resolveVariable({
62
+ value: val,
63
+ type: type.type,
64
+ schema,
65
+ }));
66
+ }
67
+ if (type.kind === graphql.Kind.NON_NULL_TYPE) {
68
+ return resolveVariable({
69
+ value: value,
70
+ type: type.type,
71
+ schema,
72
+ });
73
+ }
74
74
  }
75
75
 
76
- var _a, _b, _c, _d, _e;
77
- const levels = ['error', 'warn', 'info', 'debug'];
78
- const toLevel = (string) => levels.includes(string) ? string : null;
79
- const currentLevel = ((_b = (_a = globalThis.process) === null || _a === void 0 ? void 0 : _a.env) === null || _b === void 0 ? void 0 : _b.SOFA_DEBUG)
80
- ? 'debug'
81
- : (_e = toLevel((_d = (_c = globalThis.process) === null || _c === void 0 ? void 0 : _c.env) === null || _d === void 0 ? void 0 : _d.SOFA_LOGGER_LEVEL)) !== null && _e !== void 0 ? _e : 'info';
82
- const log = (level, color, args) => {
83
- if (levels.indexOf(level) <= levels.indexOf(currentLevel)) {
84
- console.log(`${color(level)}:`, ...args);
85
- }
86
- };
87
- const logger = {
88
- error: (...args) => {
89
- log('error', colors.red, args);
90
- },
91
- warn: (...args) => {
92
- log('warn', colors.yellow, args);
93
- },
94
- info: (...args) => {
95
- log('info', colors.green, args);
96
- },
97
- debug: (...args) => {
98
- log('debug', colors.blue, args);
99
- },
76
+ var _a, _b, _c, _d, _e;
77
+ const levels = ['error', 'warn', 'info', 'debug'];
78
+ const toLevel = (string) => levels.includes(string) ? string : null;
79
+ const currentLevel = ((_b = (_a = globalThis.process) === null || _a === void 0 ? void 0 : _a.env) === null || _b === void 0 ? void 0 : _b.SOFA_DEBUG)
80
+ ? 'debug'
81
+ : (_e = toLevel((_d = (_c = globalThis.process) === null || _c === void 0 ? void 0 : _c.env) === null || _d === void 0 ? void 0 : _d.SOFA_LOGGER_LEVEL)) !== null && _e !== void 0 ? _e : 'info';
82
+ const log = (level, color, args) => {
83
+ if (levels.indexOf(level) <= levels.indexOf(currentLevel)) {
84
+ console.log(`${color(level)}:`, ...args);
85
+ }
86
+ };
87
+ const logger = {
88
+ error: (...args) => {
89
+ log('error', colors.red, args);
90
+ },
91
+ warn: (...args) => {
92
+ log('warn', colors.yellow, args);
93
+ },
94
+ info: (...args) => {
95
+ log('info', colors.green, args);
96
+ },
97
+ debug: (...args) => {
98
+ log('debug', colors.blue, args);
99
+ },
100
100
  };
101
101
 
102
- function isAsyncIterable(obj) {
103
- return typeof obj[Symbol.asyncIterator] === 'function';
104
- }
105
- class SubscriptionManager {
106
- constructor(sofa) {
107
- this.sofa = sofa;
108
- this.operations = new Map();
109
- this.clients = new Map();
110
- this.buildOperations();
111
- }
112
- start(event, contextValue) {
113
- return tslib.__awaiter(this, void 0, void 0, function* () {
114
- const id = fetch.crypto.randomUUID();
115
- const name = event.subscription;
116
- if (!this.operations.has(name)) {
117
- throw new Error(`Subscription '${name}' is not available`);
118
- }
119
- logger.info(`[Subscription] Start ${id}`, event);
120
- const result = yield this.execute({
121
- id,
122
- name,
123
- url: event.url,
124
- variables: event.variables,
125
- contextValue,
126
- });
127
- if (typeof result !== 'undefined') {
128
- return result;
129
- }
130
- return { id };
131
- });
132
- }
133
- stop(id) {
134
- return tslib.__awaiter(this, void 0, void 0, function* () {
135
- logger.info(`[Subscription] Stop ${id}`);
136
- if (!this.clients.has(id)) {
137
- throw new Error(`Subscription with ID '${id}' does not exist`);
138
- }
139
- const execution = this.clients.get(id);
140
- if (execution.iterator.return) {
141
- execution.iterator.return();
142
- }
143
- this.clients.delete(id);
144
- return { id };
145
- });
146
- }
147
- update(event, contextValue) {
148
- return tslib.__awaiter(this, void 0, void 0, function* () {
149
- const { variables, id } = event;
150
- logger.info(`[Subscription] Update ${id}`, event);
151
- if (!this.clients.has(id)) {
152
- throw new Error(`Subscription with ID '${id}' does not exist`);
153
- }
154
- const { name: subscription, url } = this.clients.get(id);
155
- this.stop(id);
156
- return this.start({
157
- url,
158
- subscription,
159
- variables,
160
- }, contextValue);
161
- });
162
- }
163
- execute({ id, name, url, variables, contextValue, }) {
164
- return tslib.__awaiter(this, void 0, void 0, function* () {
165
- const { document, operationName, variables: variableNodes } = this.operations.get(name);
166
- const variableValues = variableNodes.reduce((values, variable) => {
167
- const value = parseVariable({
168
- value: variables[variable.variable.name.value],
169
- variable,
170
- schema: this.sofa.schema,
171
- });
172
- if (typeof value === 'undefined') {
173
- return values;
174
- }
175
- return Object.assign(Object.assign({}, values), { [variable.variable.name.value]: value });
176
- }, {});
177
- const execution = yield this.sofa.subscribe({
178
- schema: this.sofa.schema,
179
- document,
180
- operationName,
181
- variableValues,
182
- contextValue,
183
- });
184
- if (isAsyncIterable(execution)) {
185
- // successful
186
- // add execution to clients
187
- this.clients.set(id, {
188
- name,
189
- url,
190
- iterator: execution,
191
- });
192
- // success
193
- (() => tslib.__awaiter(this, void 0, void 0, function* () {
194
- var _a, e_1, _b, _c;
195
- try {
196
- for (var _d = true, execution_1 = tslib.__asyncValues(execution), execution_1_1; execution_1_1 = yield execution_1.next(), _a = execution_1_1.done, !_a;) {
197
- _c = execution_1_1.value;
198
- _d = false;
199
- try {
200
- const result = _c;
201
- yield this.sendData({
202
- id,
203
- result,
204
- });
205
- }
206
- finally {
207
- _d = true;
208
- }
209
- }
210
- }
211
- catch (e_1_1) { e_1 = { error: e_1_1 }; }
212
- finally {
213
- try {
214
- if (!_d && !_a && (_b = execution_1.return)) yield _b.call(execution_1);
215
- }
216
- finally { if (e_1) throw e_1.error; }
217
- }
218
- }))().then(() => {
219
- // completes
220
- this.clients.delete(id);
221
- }, (e) => {
222
- logger.info(`Subscription #${id} closed`);
223
- logger.error(e);
224
- this.clients.delete(id);
225
- });
226
- }
227
- else {
228
- return execution;
229
- }
230
- });
231
- }
232
- sendData({ id, result }) {
233
- return tslib.__awaiter(this, void 0, void 0, function* () {
234
- if (!this.clients.has(id)) {
235
- throw new Error(`Subscription with ID '${id}' does not exist`);
236
- }
237
- const { url } = this.clients.get(id);
238
- logger.info(`[Subscription] Trigger ${id}`);
239
- const response = yield fetch.fetch(url, {
240
- method: 'POST',
241
- body: JSON.stringify(result),
242
- headers: {
243
- 'Content-Type': 'application/json',
244
- },
245
- });
246
- yield response.text();
247
- });
248
- }
249
- buildOperations() {
250
- const subscription = this.sofa.schema.getSubscriptionType();
251
- if (!subscription) {
252
- return;
253
- }
254
- const fieldMap = subscription.getFields();
255
- for (const field in fieldMap) {
256
- const operationNode = utils.buildOperationNodeForField({
257
- kind: 'subscription',
258
- field,
259
- schema: this.sofa.schema,
260
- models: this.sofa.models,
261
- ignore: this.sofa.ignore,
262
- circularReferenceDepth: this.sofa.depthLimit,
263
- });
264
- const document = {
265
- kind: graphql.Kind.DOCUMENT,
266
- definitions: [operationNode],
267
- };
268
- const { variables, name: operationName } = getOperationInfo(document);
269
- this.operations.set(field, {
270
- operationName,
271
- document,
272
- variables,
273
- });
274
- }
275
- }
102
+ function isAsyncIterable(obj) {
103
+ return typeof obj[Symbol.asyncIterator] === 'function';
104
+ }
105
+ class SubscriptionManager {
106
+ constructor(sofa) {
107
+ this.sofa = sofa;
108
+ this.operations = new Map();
109
+ this.clients = new Map();
110
+ this.buildOperations();
111
+ }
112
+ start(event, contextValue) {
113
+ return tslib.__awaiter(this, void 0, void 0, function* () {
114
+ const id = fetch.crypto.randomUUID();
115
+ const name = event.subscription;
116
+ if (!this.operations.has(name)) {
117
+ throw new Error(`Subscription '${name}' is not available`);
118
+ }
119
+ logger.info(`[Subscription] Start ${id}`, event);
120
+ const result = yield this.execute({
121
+ id,
122
+ name,
123
+ url: event.url,
124
+ variables: event.variables,
125
+ contextValue,
126
+ });
127
+ if (typeof result !== 'undefined') {
128
+ return result;
129
+ }
130
+ return { id };
131
+ });
132
+ }
133
+ stop(id) {
134
+ return tslib.__awaiter(this, void 0, void 0, function* () {
135
+ logger.info(`[Subscription] Stop ${id}`);
136
+ if (!this.clients.has(id)) {
137
+ throw new Error(`Subscription with ID '${id}' does not exist`);
138
+ }
139
+ const execution = this.clients.get(id);
140
+ if (execution.iterator.return) {
141
+ execution.iterator.return();
142
+ }
143
+ this.clients.delete(id);
144
+ return { id };
145
+ });
146
+ }
147
+ update(event, contextValue) {
148
+ return tslib.__awaiter(this, void 0, void 0, function* () {
149
+ const { variables, id } = event;
150
+ logger.info(`[Subscription] Update ${id}`, event);
151
+ if (!this.clients.has(id)) {
152
+ throw new Error(`Subscription with ID '${id}' does not exist`);
153
+ }
154
+ const { name: subscription, url } = this.clients.get(id);
155
+ this.stop(id);
156
+ return this.start({
157
+ url,
158
+ subscription,
159
+ variables,
160
+ }, contextValue);
161
+ });
162
+ }
163
+ execute({ id, name, url, variables, contextValue, }) {
164
+ return tslib.__awaiter(this, void 0, void 0, function* () {
165
+ const { document, operationName, variables: variableNodes } = this.operations.get(name);
166
+ const variableValues = variableNodes.reduce((values, variable) => {
167
+ const value = parseVariable({
168
+ value: variables[variable.variable.name.value],
169
+ variable,
170
+ schema: this.sofa.schema,
171
+ });
172
+ if (typeof value === 'undefined') {
173
+ return values;
174
+ }
175
+ return Object.assign(Object.assign({}, values), { [variable.variable.name.value]: value });
176
+ }, {});
177
+ const execution = yield this.sofa.subscribe({
178
+ schema: this.sofa.schema,
179
+ document,
180
+ operationName,
181
+ variableValues,
182
+ contextValue,
183
+ });
184
+ if (isAsyncIterable(execution)) {
185
+ // successful
186
+ // add execution to clients
187
+ this.clients.set(id, {
188
+ name,
189
+ url,
190
+ iterator: execution,
191
+ });
192
+ // success
193
+ (() => tslib.__awaiter(this, void 0, void 0, function* () {
194
+ var _a, e_1, _b, _c;
195
+ try {
196
+ for (var _d = true, execution_1 = tslib.__asyncValues(execution), execution_1_1; execution_1_1 = yield execution_1.next(), _a = execution_1_1.done, !_a;) {
197
+ _c = execution_1_1.value;
198
+ _d = false;
199
+ try {
200
+ const result = _c;
201
+ yield this.sendData({
202
+ id,
203
+ result,
204
+ });
205
+ }
206
+ finally {
207
+ _d = true;
208
+ }
209
+ }
210
+ }
211
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
212
+ finally {
213
+ try {
214
+ if (!_d && !_a && (_b = execution_1.return)) yield _b.call(execution_1);
215
+ }
216
+ finally { if (e_1) throw e_1.error; }
217
+ }
218
+ }))().then(() => {
219
+ // completes
220
+ this.clients.delete(id);
221
+ }, (e) => {
222
+ logger.info(`Subscription #${id} closed`);
223
+ logger.error(e);
224
+ this.clients.delete(id);
225
+ });
226
+ }
227
+ else {
228
+ return execution;
229
+ }
230
+ });
231
+ }
232
+ sendData({ id, result }) {
233
+ return tslib.__awaiter(this, void 0, void 0, function* () {
234
+ if (!this.clients.has(id)) {
235
+ throw new Error(`Subscription with ID '${id}' does not exist`);
236
+ }
237
+ const { url } = this.clients.get(id);
238
+ logger.info(`[Subscription] Trigger ${id}`);
239
+ const response = yield fetch.fetch(url, {
240
+ method: 'POST',
241
+ body: JSON.stringify(result),
242
+ headers: {
243
+ 'Content-Type': 'application/json',
244
+ },
245
+ });
246
+ yield response.text();
247
+ });
248
+ }
249
+ buildOperations() {
250
+ const subscription = this.sofa.schema.getSubscriptionType();
251
+ if (!subscription) {
252
+ return;
253
+ }
254
+ const fieldMap = subscription.getFields();
255
+ for (const field in fieldMap) {
256
+ const operationNode = utils.buildOperationNodeForField({
257
+ kind: 'subscription',
258
+ field,
259
+ schema: this.sofa.schema,
260
+ models: this.sofa.models,
261
+ ignore: this.sofa.ignore,
262
+ circularReferenceDepth: this.sofa.depthLimit,
263
+ });
264
+ const document = {
265
+ kind: graphql.Kind.DOCUMENT,
266
+ definitions: [operationNode],
267
+ };
268
+ const { variables, name: operationName } = getOperationInfo(document);
269
+ this.operations.set(field, {
270
+ operationName,
271
+ document,
272
+ variables,
273
+ });
274
+ }
275
+ }
276
276
  }
277
277
 
278
- const defaultErrorHandler = (errors) => {
279
- var _a;
280
- let status;
281
- const headers = {
282
- 'Content-Type': 'application/json; charset=utf-8',
283
- };
284
- for (const error of errors) {
285
- if (typeof error === 'object' &&
286
- error != null &&
287
- ((_a = error.extensions) === null || _a === void 0 ? void 0 : _a.http)) {
288
- if (error.extensions.http.status &&
289
- (!status || error.extensions.http.status > status)) {
290
- status = error.extensions.http.status;
291
- }
292
- if (error.extensions.http.headers) {
293
- Object.assign(headers, error.extensions.http.headers);
294
- }
295
- delete error.extensions.http;
296
- }
297
- }
298
- if (!status) {
299
- status = 500;
300
- }
301
- return new router.Response(JSON.stringify({ errors }), {
302
- status,
303
- headers,
304
- });
305
- };
306
- function createRouter(sofa) {
307
- logger.debug('[Sofa] Creating router');
308
- const router$1 = router.createRouter({
309
- base: sofa.basePath,
310
- });
311
- const queryType = sofa.schema.getQueryType();
312
- const mutationType = sofa.schema.getMutationType();
313
- const subscriptionManager = new SubscriptionManager(sofa);
314
- if (queryType) {
315
- Object.keys(queryType.getFields()).forEach((fieldName) => {
316
- const route = createQueryRoute({ sofa, router: router$1, fieldName });
317
- if (sofa.onRoute) {
318
- sofa.onRoute(route);
319
- }
320
- });
321
- }
322
- if (mutationType) {
323
- Object.keys(mutationType.getFields()).forEach((fieldName) => {
324
- const route = createMutationRoute({ sofa, router: router$1, fieldName });
325
- if (sofa.onRoute) {
326
- sofa.onRoute(route);
327
- }
328
- });
329
- }
330
- router$1.post('/webhook', (request, serverContext) => tslib.__awaiter(this, void 0, void 0, function* () {
331
- const { subscription, variables, url } = yield request.json();
332
- try {
333
- const sofaContext = Object.assign(serverContext, {
334
- request,
335
- });
336
- const result = yield subscriptionManager.start({
337
- subscription,
338
- variables,
339
- url,
340
- }, sofaContext);
341
- return new router.Response(JSON.stringify(result), {
342
- status: 200,
343
- statusText: 'OK',
344
- headers: {
345
- 'Content-Type': 'application/json',
346
- },
347
- });
348
- }
349
- catch (error) {
350
- return new router.Response(JSON.stringify(error), {
351
- status: 500,
352
- statusText: 'Subscription failed',
353
- });
354
- }
355
- }));
356
- router$1.post('/webhook/:id', (request, serverContext) => tslib.__awaiter(this, void 0, void 0, function* () {
357
- var _a;
358
- const id = (_a = request.params) === null || _a === void 0 ? void 0 : _a.id;
359
- const body = yield request.json();
360
- const variables = body.variables;
361
- try {
362
- const sofaContext = Object.assign(serverContext, {
363
- request,
364
- });
365
- const contextValue = yield sofa.contextFactory(sofaContext);
366
- const result = yield subscriptionManager.update({
367
- id,
368
- variables,
369
- }, contextValue);
370
- return new router.Response(JSON.stringify(result), {
371
- status: 200,
372
- statusText: 'OK',
373
- headers: {
374
- 'Content-Type': 'application/json',
375
- },
376
- });
377
- }
378
- catch (error) {
379
- return new router.Response(JSON.stringify(error), {
380
- status: 500,
381
- statusText: 'Subscription failed to update',
382
- });
383
- }
384
- }));
385
- router$1.delete('/webhook/:id', (request) => tslib.__awaiter(this, void 0, void 0, function* () {
386
- var _b;
387
- const id = (_b = request.params) === null || _b === void 0 ? void 0 : _b.id;
388
- try {
389
- const result = yield subscriptionManager.stop(id);
390
- return new router.Response(JSON.stringify(result), {
391
- status: 200,
392
- statusText: 'OK',
393
- headers: {
394
- 'Content-Type': 'application/json',
395
- },
396
- });
397
- }
398
- catch (error) {
399
- return new router.Response(JSON.stringify(error), {
400
- status: 500,
401
- statusText: 'Subscription failed to stop',
402
- });
403
- }
404
- }));
405
- return router$1;
406
- }
407
- function createQueryRoute({ sofa, router, fieldName, }) {
408
- var _a, _b, _c, _d, _e, _f, _g;
409
- logger.debug(`[Router] Creating ${fieldName} query`);
410
- const queryType = sofa.schema.getQueryType();
411
- const operationNode = utils.buildOperationNodeForField({
412
- kind: 'query',
413
- schema: sofa.schema,
414
- field: fieldName,
415
- models: sofa.models,
416
- ignore: sofa.ignore,
417
- circularReferenceDepth: sofa.depthLimit,
418
- });
419
- const operation = {
420
- kind: graphql.Kind.DOCUMENT,
421
- definitions: [operationNode],
422
- };
423
- const info = getOperationInfo(operation);
424
- const field = queryType.getFields()[fieldName];
425
- const fieldType = field.type;
426
- const isSingle = graphql.isObjectType(fieldType) ||
427
- (graphql.isNonNullType(fieldType) && graphql.isObjectType(fieldType.ofType));
428
- const hasIdArgument = field.args.some((arg) => arg.name === 'id');
429
- const graphqlPath = `${queryType.name}.${fieldName}`;
430
- const routeConfig = (_a = sofa.routes) === null || _a === void 0 ? void 0 : _a[graphqlPath];
431
- const route = {
432
- method: (_b = routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.method) !== null && _b !== void 0 ? _b : 'GET',
433
- path: (_c = routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.path) !== null && _c !== void 0 ? _c : getPath(fieldName, isSingle && hasIdArgument),
434
- responseStatus: (_d = routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.responseStatus) !== null && _d !== void 0 ? _d : 200,
435
- };
436
- const routerMethod = route.method.toLowerCase();
437
- router[routerMethod](route.path, useHandler({ info, route, fieldName, sofa, operation }));
438
- logger.debug(`[Router] ${fieldName} query available at ${route.method} ${route.path}`);
439
- return {
440
- document: operation,
441
- path: route.path,
442
- method: route.method.toUpperCase(),
443
- tags: (_e = routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.tags) !== null && _e !== void 0 ? _e : [],
444
- description: (_g = (_f = routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.description) !== null && _f !== void 0 ? _f : field.description) !== null && _g !== void 0 ? _g : '',
445
- };
446
- }
447
- function createMutationRoute({ sofa, router, fieldName, }) {
448
- var _a, _b, _c, _d, _e, _f;
449
- logger.debug(`[Router] Creating ${fieldName} mutation`);
450
- const mutationType = sofa.schema.getMutationType();
451
- const field = mutationType.getFields()[fieldName];
452
- const operationNode = utils.buildOperationNodeForField({
453
- kind: 'mutation',
454
- schema: sofa.schema,
455
- field: fieldName,
456
- models: sofa.models,
457
- ignore: sofa.ignore,
458
- circularReferenceDepth: sofa.depthLimit,
459
- });
460
- const operation = {
461
- kind: graphql.Kind.DOCUMENT,
462
- definitions: [operationNode],
463
- };
464
- const info = getOperationInfo(operation);
465
- const graphqlPath = `${mutationType.name}.${fieldName}`;
466
- const routeConfig = (_a = sofa.routes) === null || _a === void 0 ? void 0 : _a[graphqlPath];
467
- const route = {
468
- method: (_b = routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.method) !== null && _b !== void 0 ? _b : 'POST',
469
- path: (_c = routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.path) !== null && _c !== void 0 ? _c : getPath(fieldName),
470
- responseStatus: (_d = routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.responseStatus) !== null && _d !== void 0 ? _d : 200,
471
- };
472
- const { method, path } = route;
473
- const routerKey = method.toLowerCase();
474
- router[routerKey](path, useHandler({ info, route, fieldName, sofa, operation }));
475
- logger.debug(`[Router] ${fieldName} mutation available at ${method} ${path}`);
476
- return {
477
- document: operation,
478
- path,
479
- method,
480
- tags: (routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.tags) || [],
481
- description: (_f = (_e = routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.description) !== null && _e !== void 0 ? _e : field.description) !== null && _f !== void 0 ? _f : '',
482
- };
483
- }
484
- function useHandler(config) {
485
- const { sofa, operation, fieldName } = config;
486
- const info = config.info;
487
- const errorHandler = sofa.errorHandler || defaultErrorHandler;
488
- return (request, serverContext) => tslib.__awaiter(this, void 0, void 0, function* () {
489
- var _a, _b;
490
- try {
491
- let body = {};
492
- if (request.body != null) {
493
- const strBody = yield request.text();
494
- if (strBody) {
495
- try {
496
- body = JSON.parse(strBody);
497
- }
498
- catch (error) {
499
- throw utils.createGraphQLError('POST body sent invalid JSON.', {
500
- extensions: {
501
- http: {
502
- status: 400,
503
- }
504
- }
505
- });
506
- }
507
- }
508
- }
509
- let variableValues = {};
510
- try {
511
- variableValues = info.variables.reduce((variables, variable) => {
512
- const name = variable.variable.name.value;
513
- const value = parseVariable({
514
- value: pickParam({
515
- url: request.url,
516
- body,
517
- params: request.params || {},
518
- name,
519
- }),
520
- variable,
521
- schema: sofa.schema,
522
- });
523
- if (typeof value === 'undefined') {
524
- return variables;
525
- }
526
- return Object.assign(Object.assign({}, variables), { [name]: value });
527
- }, {});
528
- }
529
- catch (error) {
530
- throw utils.createGraphQLError(error.message || ((_a = error.toString) === null || _a === void 0 ? void 0 : _a.call(error)) || error, {
531
- extensions: {
532
- http: {
533
- status: 400,
534
- }
535
- }
536
- });
537
- }
538
- const sofaContext = Object.assign(serverContext, {
539
- request,
540
- });
541
- const contextValue = yield sofa.contextFactory(sofaContext);
542
- const result = yield sofa.execute({
543
- schema: sofa.schema,
544
- document: operation,
545
- contextValue,
546
- variableValues,
547
- operationName: info.operation.name && info.operation.name.value,
548
- });
549
- if (result.errors) {
550
- return errorHandler(result.errors);
551
- }
552
- return new router.Response(JSON.stringify((_b = result.data) === null || _b === void 0 ? void 0 : _b[fieldName]), {
553
- status: config.route.responseStatus,
554
- headers: {
555
- 'Content-Type': 'application/json',
556
- },
557
- });
558
- }
559
- catch (error) {
560
- return errorHandler([error]);
561
- }
562
- });
563
- }
564
- function getPath(fieldName, hasId = false) {
565
- return `/${convertName(fieldName)}${hasId ? '/:id' : ''}`;
566
- }
567
- function pickParam({ name, url, params, body, }) {
568
- if (name in params) {
569
- return params[name];
570
- }
571
- const searchParams = new URLSearchParams(url.split('?')[1]);
572
- if (searchParams.has(name)) {
573
- const values = searchParams.getAll(name);
574
- return values.length === 1 ? values[0] : values;
575
- }
576
- if (body && body.hasOwnProperty(name)) {
577
- return body[name];
578
- }
278
+ function mapToPrimitive(type) {
279
+ const formatMap = {
280
+ Int: {
281
+ type: 'integer',
282
+ format: 'int32',
283
+ },
284
+ Float: {
285
+ type: 'number',
286
+ format: 'float',
287
+ },
288
+ String: {
289
+ type: 'string',
290
+ },
291
+ Boolean: {
292
+ type: 'boolean',
293
+ },
294
+ ID: {
295
+ type: 'string',
296
+ },
297
+ };
298
+ if (formatMap[type]) {
299
+ return formatMap[type];
300
+ }
301
+ }
302
+ function mapToRef(type) {
303
+ return `#/components/schemas/${type}`;
304
+ }
305
+ function normalizePathParamForOpenAPI(path) {
306
+ const pathParts = path.split('/');
307
+ const normalizedPathParts = pathParts.map((part) => {
308
+ if (part.startsWith(':')) {
309
+ return `{${part.slice(1)}}`;
310
+ }
311
+ return part;
312
+ });
313
+ return normalizedPathParts.join('/');
579
314
  }
580
315
 
581
- function createSofa(config) {
582
- logger.debug('[Sofa] Created');
583
- const models = extractsModels(config.schema);
584
- const ignore = config.ignore || [];
585
- const depthLimit = config.depthLimit || 1;
586
- logger.debug(`[Sofa] models: ${models.join(', ')}`);
587
- logger.debug(`[Sofa] ignore: ${ignore.join(', ')}`);
588
- return Object.assign({ execute: graphql.execute,
589
- subscribe: graphql.subscribe,
590
- models,
591
- ignore,
592
- depthLimit,
593
- contextFactory(serverContext) {
594
- if (config.context != null) {
595
- if (isContextFn(config.context)) {
596
- return config.context(serverContext);
597
- }
598
- else {
599
- return config.context;
600
- }
601
- }
602
- return serverContext;
603
- } }, config);
604
- }
605
- function isContextFn(context) {
606
- return typeof context === 'function';
607
- }
608
- // Objects and Unions are the only things that are used to define return types
609
- // and both might contain an ID
610
- // We don't treat Unions as models because
611
- // they might represent an Object that is not a model
612
- // We check it later, when an operation is being built
613
- function extractsModels(schema) {
614
- var _a, _b;
615
- const modelMap = {};
616
- const query = schema.getQueryType();
617
- const fields = query.getFields();
618
- // if Query[type] (no args) and Query[type](just id as an argument)
619
- // loop through every field
620
- for (const fieldName in fields) {
621
- const field = fields[fieldName];
622
- const namedType = graphql.getNamedType(field.type);
623
- if (hasID(namedType)) {
624
- if (!modelMap[namedType.name]) {
625
- modelMap[namedType.name] = {};
626
- }
627
- if (isArrayOf(field.type, namedType)) {
628
- // check if type is a list
629
- // check if name of a field matches a name of a named type (in plural)
630
- // check if has no non-optional arguments
631
- // add to registry with `list: true`
632
- const sameName = isNameEqual(field.name, namedType.name + 's');
633
- const allOptionalArguments = !field.args.some((arg) => graphql.isNonNullType(arg.type));
634
- (_a = modelMap[namedType.name]).list || (_a.list = sameName && allOptionalArguments);
635
- }
636
- else if (graphql.isObjectType(field.type) ||
637
- (graphql.isNonNullType(field.type) && graphql.isObjectType(field.type.ofType))) {
638
- // check if type is a graphql object type
639
- // check if name of a field matches with name of an object type
640
- // check if has only one argument named `id`
641
- // add to registry with `single: true`
642
- const sameName = isNameEqual(field.name, namedType.name);
643
- const hasIdArgument = field.args.length === 1 && field.args[0].name === 'id';
644
- (_b = modelMap[namedType.name]).single || (_b.single = sameName && hasIdArgument);
645
- }
646
- }
647
- }
648
- return Object.keys(modelMap).filter((name) => modelMap[name].list && modelMap[name].single);
649
- }
650
- // it's dumb but let's leave it for now
651
- function isArrayOf(type, expected) {
652
- const typeNameInSdl = type.toString();
653
- return (typeNameInSdl.includes('[') && typeNameInSdl.includes(expected.toString()));
654
- }
655
- function hasID(type) {
656
- return graphql.isObjectType(type) && !!type.getFields().id;
657
- }
658
- function isNameEqual(a, b) {
659
- return convertName(a) === convertName(b);
316
+ function buildSchemaObjectFromType(type, opts) {
317
+ const required = [];
318
+ const properties = {};
319
+ const fields = type.getFields();
320
+ for (const fieldName in fields) {
321
+ const field = fields[fieldName];
322
+ if (graphql.isNonNullType(field.type)) {
323
+ required.push(field.name);
324
+ }
325
+ properties[fieldName] = resolveField(field, opts);
326
+ if (field.description) {
327
+ properties[fieldName].description = field.description;
328
+ }
329
+ }
330
+ return Object.assign(Object.assign(Object.assign({ type: 'object' }, (required.length ? { required } : {})), { properties }), (type.description ? { description: type.description } : {}));
331
+ }
332
+ function resolveField(field, opts) {
333
+ return resolveFieldType(field.type, opts);
334
+ }
335
+ // array -> [type]
336
+ // type -> $ref
337
+ // scalar -> swagger primitive
338
+ function resolveFieldType(type, opts) {
339
+ var _a;
340
+ if (graphql.isNonNullType(type)) {
341
+ return resolveFieldType(type.ofType, opts);
342
+ }
343
+ if (graphql.isListType(type)) {
344
+ return {
345
+ type: 'array',
346
+ items: resolveFieldType(type.ofType, opts),
347
+ };
348
+ }
349
+ if (graphql.isObjectType(type)) {
350
+ return {
351
+ $ref: mapToRef(type.name),
352
+ };
353
+ }
354
+ if (graphql.isScalarType(type)) {
355
+ const resolved = mapToPrimitive(type.name) ||
356
+ opts.customScalars[type.name] ||
357
+ ((_a = type.extensions) === null || _a === void 0 ? void 0 : _a.jsonSchema) || {
358
+ type: 'object',
359
+ };
360
+ return Object.assign({}, resolved);
361
+ }
362
+ if (graphql.isEnumType(type)) {
363
+ return {
364
+ type: 'string',
365
+ enum: type.getValues().map((value) => value.name),
366
+ };
367
+ }
368
+ return {
369
+ type: 'object',
370
+ };
660
371
  }
661
372
 
662
- function mapToPrimitive(type) {
663
- const formatMap = {
664
- Int: {
665
- type: 'integer',
666
- format: 'int32',
667
- },
668
- Float: {
669
- type: 'number',
670
- format: 'float',
671
- },
672
- String: {
673
- type: 'string',
674
- },
675
- Boolean: {
676
- type: 'boolean',
677
- },
678
- ID: {
679
- type: 'string',
680
- },
681
- };
682
- if (formatMap[type]) {
683
- return formatMap[type];
684
- }
685
- }
686
- function mapToRef(type) {
687
- return `#/components/schemas/${type}`;
688
- }
689
- function normalizePathParamForOpenAPI(path) {
690
- const pathParts = path.split('/');
691
- const normalizedPathParts = pathParts.map((part) => {
692
- if (part.startsWith(':')) {
693
- return `{${part.slice(1)}}`;
694
- }
695
- return part;
696
- });
697
- return normalizedPathParts.join('/');
373
+ function buildPathFromOperation({ url, schema, operation, useRequestBody, tags, description, customScalars, }) {
374
+ const info = getOperationInfo(operation);
375
+ const enumTypes = resolveEnumTypes(schema);
376
+ const summary = resolveDescription(schema, info.operation);
377
+ const variables = info.operation.variableDefinitions;
378
+ const pathParams = variables === null || variables === void 0 ? void 0 : variables.filter((variable) => isInPath(url, variable.variable.name.value));
379
+ const bodyParams = variables === null || variables === void 0 ? void 0 : variables.filter((variable) => !isInPath(url, variable.variable.name.value));
380
+ return Object.assign(Object.assign({ tags,
381
+ description,
382
+ summary, operationId: info.name }, (useRequestBody
383
+ ? {
384
+ parameters: resolveParameters(url, pathParams, schema, info.operation, { customScalars, enumTypes }),
385
+ requestBody: {
386
+ content: {
387
+ 'application/json': {
388
+ schema: resolveRequestBody(bodyParams, schema, info.operation, { customScalars, enumTypes }),
389
+ },
390
+ },
391
+ },
392
+ }
393
+ : {
394
+ parameters: resolveParameters(url, variables, schema, info.operation, { customScalars, enumTypes }),
395
+ })), { responses: {
396
+ 200: {
397
+ description: summary,
398
+ content: {
399
+ 'application/json': {
400
+ schema: resolveResponse({
401
+ schema,
402
+ operation: info.operation,
403
+ opts: { customScalars, enumTypes },
404
+ }),
405
+ },
406
+ },
407
+ },
408
+ } });
409
+ }
410
+ function resolveEnumTypes(schema) {
411
+ const enumTypes = Object.values(schema.getTypeMap())
412
+ .filter(graphql.isEnumType);
413
+ return Object.fromEntries(enumTypes.map((type) => [
414
+ type.name,
415
+ {
416
+ type: 'string',
417
+ enum: type.getValues().map((value) => value.name),
418
+ },
419
+ ]));
420
+ }
421
+ function resolveParameters(url, variables, schema, operation, opts) {
422
+ if (!variables) {
423
+ return [];
424
+ }
425
+ return variables.map((variable) => {
426
+ return {
427
+ in: isInPath(url, variable.variable.name.value) ? 'path' : 'query',
428
+ name: variable.variable.name.value,
429
+ required: variable.type.kind === graphql.Kind.NON_NULL_TYPE,
430
+ schema: resolveParamSchema(variable.type, opts),
431
+ description: resolveVariableDescription(schema, operation, variable),
432
+ };
433
+ });
434
+ }
435
+ function resolveRequestBody(variables, schema, operation, opts) {
436
+ if (!variables) {
437
+ return {};
438
+ }
439
+ const properties = {};
440
+ const required = [];
441
+ variables.forEach((variable) => {
442
+ if (variable.type.kind === graphql.Kind.NON_NULL_TYPE) {
443
+ required.push(variable.variable.name.value);
444
+ }
445
+ properties[variable.variable.name.value] = Object.assign(Object.assign({}, resolveParamSchema(variable.type, opts)), { description: resolveVariableDescription(schema, operation, variable) });
446
+ });
447
+ return Object.assign({ type: 'object', properties }, (required.length ? { required } : {}));
448
+ }
449
+ // array -> [type]
450
+ // type -> $ref
451
+ // scalar -> swagger primitive
452
+ function resolveParamSchema(type, opts) {
453
+ if (type.kind === graphql.Kind.NON_NULL_TYPE) {
454
+ return resolveParamSchema(type.type, opts);
455
+ }
456
+ if (type.kind === graphql.Kind.LIST_TYPE) {
457
+ return {
458
+ type: 'array',
459
+ items: resolveParamSchema(type.type, opts),
460
+ };
461
+ }
462
+ const primitive = mapToPrimitive(type.name.value);
463
+ return (primitive ||
464
+ opts.customScalars[type.name.value] ||
465
+ opts.enumTypes[type.name.value] || { $ref: mapToRef(type.name.value) });
466
+ }
467
+ function resolveResponse({ schema, operation, opts, }) {
468
+ const operationType = operation.operation;
469
+ const rootField = operation.selectionSet.selections[0];
470
+ if (rootField.kind === graphql.Kind.FIELD) {
471
+ if (operationType === 'query') {
472
+ const queryType = schema.getQueryType();
473
+ const field = queryType.getFields()[rootField.name.value];
474
+ return resolveFieldType(field.type, opts);
475
+ }
476
+ if (operationType === 'mutation') {
477
+ const mutationType = schema.getMutationType();
478
+ const field = mutationType.getFields()[rootField.name.value];
479
+ return resolveFieldType(field.type, opts);
480
+ }
481
+ }
482
+ }
483
+ function isInPath(url, param) {
484
+ return url.includes(`:${param}`);
485
+ }
486
+ function getOperationFieldNode(schema, operation) {
487
+ const selection = operation.selectionSet.selections[0];
488
+ const fieldName = selection.name.value;
489
+ const typeDefinition = schema.getType(titleCase.titleCase(operation.operation));
490
+ if (!typeDefinition) {
491
+ return undefined;
492
+ }
493
+ const definitionNode = typeDefinition.astNode || graphql.parse(graphql.printType(typeDefinition)).definitions[0];
494
+ if (!isObjectTypeDefinitionNode(definitionNode)) {
495
+ return undefined;
496
+ }
497
+ return definitionNode.fields.find((field) => field.name.value === fieldName);
498
+ }
499
+ function resolveDescription(schema, operation) {
500
+ var _a;
501
+ const fieldNode = getOperationFieldNode(schema, operation);
502
+ return ((_a = fieldNode === null || fieldNode === void 0 ? void 0 : fieldNode.description) === null || _a === void 0 ? void 0 : _a.value) || '';
503
+ }
504
+ function resolveVariableDescription(schema, operation, variable) {
505
+ var _a, _b;
506
+ const fieldNode = getOperationFieldNode(schema, operation);
507
+ const argument = (_a = fieldNode === null || fieldNode === void 0 ? void 0 : fieldNode.arguments) === null || _a === void 0 ? void 0 : _a.find((arg) => arg.name.value === variable.variable.name.value);
508
+ return (_b = argument === null || argument === void 0 ? void 0 : argument.description) === null || _b === void 0 ? void 0 : _b.value;
509
+ }
510
+ function isObjectTypeDefinitionNode(node) {
511
+ return node.kind === graphql.Kind.OBJECT_TYPE_DEFINITION;
698
512
  }
699
513
 
700
- function buildSchemaObjectFromType(type, opts) {
701
- const required = [];
702
- const properties = {};
703
- const fields = type.getFields();
704
- for (const fieldName in fields) {
705
- const field = fields[fieldName];
706
- if (graphql.isNonNullType(field.type)) {
707
- required.push(field.name);
708
- }
709
- properties[fieldName] = resolveField(field, opts);
710
- if (field.description) {
711
- properties[fieldName].description = field.description;
712
- }
713
- }
714
- return Object.assign(Object.assign(Object.assign({ type: 'object' }, (required.length ? { required } : {})), { properties }), (type.description ? { description: type.description } : {}));
715
- }
716
- function resolveField(field, opts) {
717
- return resolveFieldType(field.type, opts);
718
- }
719
- // array -> [type]
720
- // type -> $ref
721
- // scalar -> swagger primitive
722
- function resolveFieldType(type, opts) {
723
- var _a;
724
- if (graphql.isNonNullType(type)) {
725
- return resolveFieldType(type.ofType, opts);
726
- }
727
- if (graphql.isListType(type)) {
728
- return {
729
- type: 'array',
730
- items: resolveFieldType(type.ofType, opts),
731
- };
732
- }
733
- if (graphql.isObjectType(type)) {
734
- return {
735
- $ref: mapToRef(type.name),
736
- };
737
- }
738
- if (graphql.isScalarType(type)) {
739
- const resolved = mapToPrimitive(type.name) ||
740
- opts.customScalars[type.name] ||
741
- ((_a = type.extensions) === null || _a === void 0 ? void 0 : _a.jsonSchema) || {
742
- type: 'object',
743
- };
744
- return Object.assign({}, resolved);
745
- }
746
- if (graphql.isEnumType(type)) {
747
- return {
748
- type: 'string',
749
- enum: type.getValues().map((value) => value.name),
750
- };
751
- }
752
- return {
753
- type: 'object',
754
- };
514
+ const defaultErrorHandler = (errors) => {
515
+ var _a;
516
+ let status;
517
+ const headers = {
518
+ 'Content-Type': 'application/json; charset=utf-8',
519
+ };
520
+ for (const error of errors) {
521
+ if (typeof error === 'object' &&
522
+ error != null &&
523
+ ((_a = error.extensions) === null || _a === void 0 ? void 0 : _a.http)) {
524
+ if (error.extensions.http.status &&
525
+ (!status || error.extensions.http.status > status)) {
526
+ status = error.extensions.http.status;
527
+ }
528
+ if (error.extensions.http.headers) {
529
+ Object.assign(headers, error.extensions.http.headers);
530
+ }
531
+ delete error.extensions.http;
532
+ }
533
+ }
534
+ if (!status) {
535
+ status = 500;
536
+ }
537
+ return fets.Response.json({ errors }, {
538
+ status,
539
+ headers,
540
+ });
541
+ };
542
+ function useRequestBody(method) {
543
+ return method === 'POST' || method === 'PUT' || method === 'PATCH';
544
+ }
545
+ function createRouter(sofa) {
546
+ logger.debug('[Sofa] Creating router');
547
+ const components = {
548
+ schemas: {},
549
+ };
550
+ const types = sofa.schema.getTypeMap();
551
+ for (const typeName in types) {
552
+ const type = types[typeName];
553
+ if ((graphql.isObjectType(type) || graphql.isInputObjectType(type)) &&
554
+ !graphql.isIntrospectionType(type)) {
555
+ components.schemas[typeName] = buildSchemaObjectFromType(type, {
556
+ customScalars: sofa.customScalars,
557
+ });
558
+ }
559
+ }
560
+ const router = fets.createRouter({
561
+ base: sofa.basePath,
562
+ title: sofa.title || 'SOFA API',
563
+ description: sofa.description || 'Generated by SOFA',
564
+ version: sofa.version || '0.0.0',
565
+ components,
566
+ });
567
+ const queryType = sofa.schema.getQueryType();
568
+ const mutationType = sofa.schema.getMutationType();
569
+ const subscriptionManager = new SubscriptionManager(sofa);
570
+ if (queryType) {
571
+ Object.keys(queryType.getFields()).forEach((fieldName) => {
572
+ createQueryRoute({ sofa, router, fieldName });
573
+ });
574
+ }
575
+ if (mutationType) {
576
+ Object.keys(mutationType.getFields()).forEach((fieldName) => {
577
+ createMutationRoute({ sofa, router, fieldName });
578
+ });
579
+ }
580
+ router.route({
581
+ path: '/webhook',
582
+ method: 'POST',
583
+ handler(request, serverContext) {
584
+ return tslib.__awaiter(this, void 0, void 0, function* () {
585
+ const { subscription, variables, url } = yield request.json();
586
+ try {
587
+ const sofaContext = Object.assign(serverContext, {
588
+ request,
589
+ });
590
+ const result = yield subscriptionManager.start({
591
+ subscription,
592
+ variables,
593
+ url,
594
+ }, sofaContext);
595
+ return fets.Response.json(result);
596
+ }
597
+ catch (error) {
598
+ return fets.Response.json(error, {
599
+ status: 500,
600
+ statusText: 'Subscription failed',
601
+ });
602
+ }
603
+ });
604
+ }
605
+ });
606
+ router.route({
607
+ path: '/webhook/:id',
608
+ method: 'POST',
609
+ handler(request, serverContext) {
610
+ var _a;
611
+ return tslib.__awaiter(this, void 0, void 0, function* () {
612
+ const id = (_a = request.params) === null || _a === void 0 ? void 0 : _a.id;
613
+ const body = yield request.json();
614
+ const variables = body.variables;
615
+ try {
616
+ const sofaContext = Object.assign(serverContext, {
617
+ request,
618
+ });
619
+ const contextValue = yield sofa.contextFactory(sofaContext);
620
+ const result = yield subscriptionManager.update({
621
+ id,
622
+ variables,
623
+ }, contextValue);
624
+ return fets.Response.json(result);
625
+ }
626
+ catch (error) {
627
+ return fets.Response.json(error, {
628
+ status: 500,
629
+ statusText: 'Subscription failed to update',
630
+ });
631
+ }
632
+ });
633
+ }
634
+ });
635
+ router.route({
636
+ path: '/webhook/:id',
637
+ method: 'DELETE',
638
+ handler(request) {
639
+ var _a;
640
+ return tslib.__awaiter(this, void 0, void 0, function* () {
641
+ const id = (_a = request.params) === null || _a === void 0 ? void 0 : _a.id;
642
+ try {
643
+ const result = yield subscriptionManager.stop(id);
644
+ return fets.Response.json(result);
645
+ }
646
+ catch (error) {
647
+ return fets.Response.json(error, {
648
+ status: 500,
649
+ statusText: 'Subscription failed to stop',
650
+ });
651
+ }
652
+ });
653
+ }
654
+ });
655
+ return router;
656
+ }
657
+ function createQueryRoute({ sofa, router, fieldName, }) {
658
+ var _a, _b, _c, _d, _e, _f, _g;
659
+ logger.debug(`[Router] Creating ${fieldName} query`);
660
+ const queryType = sofa.schema.getQueryType();
661
+ const operationNode = utils.buildOperationNodeForField({
662
+ kind: 'query',
663
+ schema: sofa.schema,
664
+ field: fieldName,
665
+ models: sofa.models,
666
+ ignore: sofa.ignore,
667
+ circularReferenceDepth: sofa.depthLimit,
668
+ });
669
+ const operation = {
670
+ kind: graphql.Kind.DOCUMENT,
671
+ definitions: [operationNode],
672
+ };
673
+ const info = getOperationInfo(operation);
674
+ const field = queryType.getFields()[fieldName];
675
+ const fieldType = field.type;
676
+ const isSingle = graphql.isObjectType(fieldType) ||
677
+ (graphql.isNonNullType(fieldType) && graphql.isObjectType(fieldType.ofType));
678
+ const hasIdArgument = field.args.some((arg) => arg.name === 'id');
679
+ const graphqlPath = `${queryType.name}.${fieldName}`;
680
+ const routeConfig = (_a = sofa.routes) === null || _a === void 0 ? void 0 : _a[graphqlPath];
681
+ const route = {
682
+ method: (_b = routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.method) !== null && _b !== void 0 ? _b : 'GET',
683
+ path: (_c = routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.path) !== null && _c !== void 0 ? _c : getPath(fieldName, isSingle && hasIdArgument),
684
+ responseStatus: (_d = routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.responseStatus) !== null && _d !== void 0 ? _d : 200,
685
+ };
686
+ router.route({
687
+ path: route.path,
688
+ method: route.method,
689
+ schemas: getRouteSchemas({
690
+ method: route.method,
691
+ path: route.path,
692
+ info,
693
+ sofa,
694
+ responseStatus: route.responseStatus,
695
+ }),
696
+ handler: useHandler({ info, route, fieldName, sofa, operation }),
697
+ });
698
+ logger.debug(`[Router] ${fieldName} query available at ${route.method} ${route.path}`);
699
+ return {
700
+ document: operation,
701
+ path: route.path,
702
+ method: route.method.toUpperCase(),
703
+ tags: (_e = routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.tags) !== null && _e !== void 0 ? _e : [],
704
+ description: (_g = (_f = routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.description) !== null && _f !== void 0 ? _f : field.description) !== null && _g !== void 0 ? _g : '',
705
+ };
706
+ }
707
+ function getRouteSchemas({ method, path, info, sofa, responseStatus, }) {
708
+ const params = {
709
+ properties: {},
710
+ required: [],
711
+ };
712
+ const query = {
713
+ properties: {},
714
+ required: [],
715
+ };
716
+ for (const variable of info.variables) {
717
+ const varSchema = resolveParamSchema(variable.type, {
718
+ customScalars: sofa.customScalars,
719
+ enumTypes: sofa.enumTypes,
720
+ });
721
+ varSchema.description = resolveVariableDescription(sofa.schema, info.operation, variable);
722
+ const varName = variable.variable.name.value;
723
+ const varObj = isInPath(path, varName) ? params : query;
724
+ varObj.properties[varName] = varSchema;
725
+ if (variable.type.kind === graphql.Kind.NON_NULL_TYPE) {
726
+ varObj.required.push(varName);
727
+ }
728
+ }
729
+ return {
730
+ request: {
731
+ json: useRequestBody(method) ? resolveRequestBody(info.variables, sofa.schema, info.operation, {
732
+ customScalars: sofa.customScalars,
733
+ enumTypes: sofa.enumTypes,
734
+ }) : undefined,
735
+ params,
736
+ query,
737
+ },
738
+ responses: {
739
+ [responseStatus]: resolveResponse({
740
+ schema: sofa.schema,
741
+ operation: info.operation,
742
+ opts: {
743
+ customScalars: sofa.customScalars,
744
+ enumTypes: sofa.enumTypes,
745
+ }
746
+ })
747
+ }
748
+ };
749
+ }
750
+ function createMutationRoute({ sofa, router, fieldName, }) {
751
+ var _a, _b, _c, _d, _e, _f;
752
+ logger.debug(`[Router] Creating ${fieldName} mutation`);
753
+ const mutationType = sofa.schema.getMutationType();
754
+ const field = mutationType.getFields()[fieldName];
755
+ const operationNode = utils.buildOperationNodeForField({
756
+ kind: 'mutation',
757
+ schema: sofa.schema,
758
+ field: fieldName,
759
+ models: sofa.models,
760
+ ignore: sofa.ignore,
761
+ circularReferenceDepth: sofa.depthLimit,
762
+ });
763
+ const operation = {
764
+ kind: graphql.Kind.DOCUMENT,
765
+ definitions: [operationNode],
766
+ };
767
+ const info = getOperationInfo(operation);
768
+ const graphqlPath = `${mutationType.name}.${fieldName}`;
769
+ const routeConfig = (_a = sofa.routes) === null || _a === void 0 ? void 0 : _a[graphqlPath];
770
+ const method = (_b = routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.method) !== null && _b !== void 0 ? _b : 'POST';
771
+ const path = (_c = routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.path) !== null && _c !== void 0 ? _c : getPath(fieldName);
772
+ const responseStatus = (_d = routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.responseStatus) !== null && _d !== void 0 ? _d : 200;
773
+ const route = {
774
+ method,
775
+ path,
776
+ responseStatus,
777
+ };
778
+ router.route({
779
+ method,
780
+ path,
781
+ schemas: getRouteSchemas({
782
+ method,
783
+ path,
784
+ info,
785
+ responseStatus,
786
+ sofa,
787
+ }),
788
+ handler: useHandler({ info, route, fieldName, sofa, operation }),
789
+ });
790
+ logger.debug(`[Router] ${fieldName} mutation available at ${method} ${path}`);
791
+ return {
792
+ document: operation,
793
+ path,
794
+ method,
795
+ tags: (routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.tags) || [],
796
+ description: (_f = (_e = routeConfig === null || routeConfig === void 0 ? void 0 : routeConfig.description) !== null && _e !== void 0 ? _e : field.description) !== null && _f !== void 0 ? _f : '',
797
+ };
798
+ }
799
+ function useHandler(config) {
800
+ const { sofa, operation, fieldName } = config;
801
+ const info = config.info;
802
+ const errorHandler = sofa.errorHandler || defaultErrorHandler;
803
+ return (request, serverContext) => tslib.__awaiter(this, void 0, void 0, function* () {
804
+ var _a, _b;
805
+ try {
806
+ let body = {};
807
+ if (request.body != null) {
808
+ const strBody = yield request.text();
809
+ if (strBody) {
810
+ try {
811
+ body = JSON.parse(strBody);
812
+ }
813
+ catch (error) {
814
+ throw utils.createGraphQLError('POST body sent invalid JSON.', {
815
+ extensions: {
816
+ http: {
817
+ status: 400,
818
+ }
819
+ }
820
+ });
821
+ }
822
+ }
823
+ }
824
+ let variableValues = {};
825
+ try {
826
+ variableValues = info.variables.reduce((variables, variable) => {
827
+ const name = variable.variable.name.value;
828
+ const value = parseVariable({
829
+ value: pickParam({
830
+ url: request.url,
831
+ body,
832
+ params: request.params || {},
833
+ name,
834
+ }),
835
+ variable,
836
+ schema: sofa.schema,
837
+ });
838
+ if (typeof value === 'undefined') {
839
+ return variables;
840
+ }
841
+ return Object.assign(Object.assign({}, variables), { [name]: value });
842
+ }, {});
843
+ }
844
+ catch (error) {
845
+ throw utils.createGraphQLError(error.message || ((_a = error.toString) === null || _a === void 0 ? void 0 : _a.call(error)) || error, {
846
+ extensions: {
847
+ http: {
848
+ status: 400,
849
+ }
850
+ }
851
+ });
852
+ }
853
+ const sofaContext = Object.assign(serverContext, {
854
+ request,
855
+ });
856
+ const contextValue = yield sofa.contextFactory(sofaContext);
857
+ const result = yield sofa.execute({
858
+ schema: sofa.schema,
859
+ document: operation,
860
+ contextValue,
861
+ variableValues,
862
+ operationName: info.operation.name && info.operation.name.value,
863
+ });
864
+ if (result.errors) {
865
+ return errorHandler(result.errors);
866
+ }
867
+ return fets.Response.json((_b = result.data) === null || _b === void 0 ? void 0 : _b[fieldName], {
868
+ status: config.route.responseStatus,
869
+ });
870
+ }
871
+ catch (error) {
872
+ return errorHandler([error]);
873
+ }
874
+ });
875
+ }
876
+ function getPath(fieldName, hasId = false) {
877
+ return `/${convertName(fieldName)}${hasId ? '/:id' : ''}`;
878
+ }
879
+ function pickParam({ name, url, params, body, }) {
880
+ if (name in params) {
881
+ return params[name];
882
+ }
883
+ const searchParams = new URLSearchParams(url.split('?')[1]);
884
+ if (searchParams.has(name)) {
885
+ const values = searchParams.getAll(name);
886
+ return values.length === 1 ? values[0] : values;
887
+ }
888
+ if (body && body.hasOwnProperty(name)) {
889
+ return body[name];
890
+ }
755
891
  }
756
892
 
757
- function buildPathFromOperation({ url, schema, operation, useRequestBody, tags, description, customScalars, }) {
758
- const info = getOperationInfo(operation);
759
- const enumTypes = resolveEnumTypes(schema);
760
- const summary = resolveDescription(schema, info.operation);
761
- const variables = info.operation.variableDefinitions;
762
- const pathParams = variables === null || variables === void 0 ? void 0 : variables.filter((variable) => isInPath(url, variable.variable.name.value));
763
- const bodyParams = variables === null || variables === void 0 ? void 0 : variables.filter((variable) => !isInPath(url, variable.variable.name.value));
764
- return Object.assign(Object.assign({ tags,
765
- description,
766
- summary, operationId: info.name }, (useRequestBody
767
- ? {
768
- parameters: resolveParameters(url, pathParams, schema, info.operation, { customScalars, enumTypes }),
769
- requestBody: {
770
- content: {
771
- 'application/json': {
772
- schema: resolveRequestBody(bodyParams, schema, info.operation, { customScalars, enumTypes }),
773
- },
774
- },
775
- },
776
- }
777
- : {
778
- parameters: resolveParameters(url, variables, schema, info.operation, { customScalars, enumTypes }),
779
- })), { responses: {
780
- 200: {
781
- description: summary,
782
- content: {
783
- 'application/json': {
784
- schema: resolveResponse({
785
- schema,
786
- operation: info.operation,
787
- opts: { customScalars, enumTypes },
788
- }),
789
- },
790
- },
791
- },
792
- } });
793
- }
794
- function resolveEnumTypes(schema) {
795
- const enumTypes = Object.values(schema.getTypeMap())
796
- .filter(graphql.isEnumType);
797
- return Object.fromEntries(enumTypes.map((type) => [
798
- type.name,
799
- {
800
- type: 'string',
801
- enum: type.getValues().map((value) => value.name),
802
- },
803
- ]));
804
- }
805
- function resolveParameters(url, variables, schema, operation, opts) {
806
- if (!variables) {
807
- return [];
808
- }
809
- return variables.map((variable) => {
810
- return {
811
- in: isInPath(url, variable.variable.name.value) ? 'path' : 'query',
812
- name: variable.variable.name.value,
813
- required: variable.type.kind === graphql.Kind.NON_NULL_TYPE,
814
- schema: resolveParamSchema(variable.type, opts),
815
- description: resolveVariableDescription(schema, operation, variable),
816
- };
817
- });
818
- }
819
- function resolveRequestBody(variables, schema, operation, opts) {
820
- if (!variables) {
821
- return {};
822
- }
823
- const properties = {};
824
- const required = [];
825
- variables.forEach((variable) => {
826
- if (variable.type.kind === graphql.Kind.NON_NULL_TYPE) {
827
- required.push(variable.variable.name.value);
828
- }
829
- properties[variable.variable.name.value] = Object.assign(Object.assign({}, resolveParamSchema(variable.type, opts)), { description: resolveVariableDescription(schema, operation, variable) });
830
- });
831
- return Object.assign({ type: 'object', properties }, (required.length ? { required } : {}));
832
- }
833
- // array -> [type]
834
- // type -> $ref
835
- // scalar -> swagger primitive
836
- function resolveParamSchema(type, opts) {
837
- if (type.kind === graphql.Kind.NON_NULL_TYPE) {
838
- return resolveParamSchema(type.type, opts);
839
- }
840
- if (type.kind === graphql.Kind.LIST_TYPE) {
841
- return {
842
- type: 'array',
843
- items: resolveParamSchema(type.type, opts),
844
- };
845
- }
846
- const primitive = mapToPrimitive(type.name.value);
847
- return (primitive ||
848
- opts.customScalars[type.name.value] ||
849
- opts.enumTypes[type.name.value] || { $ref: mapToRef(type.name.value) });
850
- }
851
- function resolveResponse({ schema, operation, opts, }) {
852
- const operationType = operation.operation;
853
- const rootField = operation.selectionSet.selections[0];
854
- if (rootField.kind === graphql.Kind.FIELD) {
855
- if (operationType === 'query') {
856
- const queryType = schema.getQueryType();
857
- const field = queryType.getFields()[rootField.name.value];
858
- return resolveFieldType(field.type, opts);
859
- }
860
- if (operationType === 'mutation') {
861
- const mutationType = schema.getMutationType();
862
- const field = mutationType.getFields()[rootField.name.value];
863
- return resolveFieldType(field.type, opts);
864
- }
865
- }
866
- }
867
- function isInPath(url, param) {
868
- return url.indexOf(`{${param}}`) !== -1;
869
- }
870
- function getOperationFieldNode(schema, operation) {
871
- const selection = operation.selectionSet.selections[0];
872
- const fieldName = selection.name.value;
873
- const typeDefinition = schema.getType(titleCase.titleCase(operation.operation));
874
- if (!typeDefinition) {
875
- return undefined;
876
- }
877
- const definitionNode = typeDefinition.astNode || graphql.parse(graphql.printType(typeDefinition)).definitions[0];
878
- if (!isObjectTypeDefinitionNode(definitionNode)) {
879
- return undefined;
880
- }
881
- return definitionNode.fields.find((field) => field.name.value === fieldName);
882
- }
883
- function resolveDescription(schema, operation) {
884
- var _a;
885
- const fieldNode = getOperationFieldNode(schema, operation);
886
- return ((_a = fieldNode === null || fieldNode === void 0 ? void 0 : fieldNode.description) === null || _a === void 0 ? void 0 : _a.value) || '';
887
- }
888
- function resolveVariableDescription(schema, operation, variable) {
889
- var _a, _b;
890
- const fieldNode = getOperationFieldNode(schema, operation);
891
- const argument = (_a = fieldNode === null || fieldNode === void 0 ? void 0 : fieldNode.arguments) === null || _a === void 0 ? void 0 : _a.find((arg) => arg.name.value === variable.variable.name.value);
892
- return (_b = argument === null || argument === void 0 ? void 0 : argument.description) === null || _b === void 0 ? void 0 : _b.value;
893
- }
894
- function isObjectTypeDefinitionNode(node) {
895
- return node.kind === graphql.Kind.OBJECT_TYPE_DEFINITION;
893
+ function createSofa(config) {
894
+ logger.debug('[Sofa] Created');
895
+ const models = extractsModels(config.schema);
896
+ const ignore = config.ignore || [];
897
+ const depthLimit = config.depthLimit || 1;
898
+ logger.debug(`[Sofa] models: ${models.join(', ')}`);
899
+ logger.debug(`[Sofa] ignore: ${ignore.join(', ')}`);
900
+ return Object.assign({ execute: graphql.execute,
901
+ subscribe: graphql.subscribe,
902
+ models,
903
+ ignore,
904
+ depthLimit,
905
+ contextFactory(serverContext) {
906
+ if (config.context != null) {
907
+ if (isContextFn(config.context)) {
908
+ return config.context(serverContext);
909
+ }
910
+ else {
911
+ return config.context;
912
+ }
913
+ }
914
+ return serverContext;
915
+ }, customScalars: config.customScalars || {}, enumTypes: config.enumTypes || {} }, config);
916
+ }
917
+ function isContextFn(context) {
918
+ return typeof context === 'function';
919
+ }
920
+ // Objects and Unions are the only things that are used to define return types
921
+ // and both might contain an ID
922
+ // We don't treat Unions as models because
923
+ // they might represent an Object that is not a model
924
+ // We check it later, when an operation is being built
925
+ function extractsModels(schema) {
926
+ var _a, _b;
927
+ const modelMap = {};
928
+ const query = schema.getQueryType();
929
+ const fields = query.getFields();
930
+ // if Query[type] (no args) and Query[type](just id as an argument)
931
+ // loop through every field
932
+ for (const fieldName in fields) {
933
+ const field = fields[fieldName];
934
+ const namedType = graphql.getNamedType(field.type);
935
+ if (hasID(namedType)) {
936
+ if (!modelMap[namedType.name]) {
937
+ modelMap[namedType.name] = {};
938
+ }
939
+ if (isArrayOf(field.type, namedType)) {
940
+ // check if type is a list
941
+ // check if name of a field matches a name of a named type (in plural)
942
+ // check if has no non-optional arguments
943
+ // add to registry with `list: true`
944
+ const sameName = isNameEqual(field.name, namedType.name + 's');
945
+ const allOptionalArguments = !field.args.some((arg) => graphql.isNonNullType(arg.type));
946
+ (_a = modelMap[namedType.name]).list || (_a.list = sameName && allOptionalArguments);
947
+ }
948
+ else if (graphql.isObjectType(field.type) ||
949
+ (graphql.isNonNullType(field.type) && graphql.isObjectType(field.type.ofType))) {
950
+ // check if type is a graphql object type
951
+ // check if name of a field matches with name of an object type
952
+ // check if has only one argument named `id`
953
+ // add to registry with `single: true`
954
+ const sameName = isNameEqual(field.name, namedType.name);
955
+ const hasIdArgument = field.args.length === 1 && field.args[0].name === 'id';
956
+ (_b = modelMap[namedType.name]).single || (_b.single = sameName && hasIdArgument);
957
+ }
958
+ }
959
+ }
960
+ return Object.keys(modelMap).filter((name) => modelMap[name].list && modelMap[name].single);
961
+ }
962
+ // it's dumb but let's leave it for now
963
+ function isArrayOf(type, expected) {
964
+ const typeNameInSdl = type.toString();
965
+ return (typeNameInSdl.includes('[') && typeNameInSdl.includes(expected.toString()));
966
+ }
967
+ function hasID(type) {
968
+ return graphql.isObjectType(type) && !!type.getFields().id;
969
+ }
970
+ function isNameEqual(a, b) {
971
+ return convertName(a) === convertName(b);
896
972
  }
897
973
 
898
- function OpenAPI({ schema, info, servers, components, security, tags, customScalars = {}, }) {
899
- const types = schema.getTypeMap();
900
- const swagger = {
901
- openapi: '3.0.0',
902
- info,
903
- servers,
904
- tags: [],
905
- paths: {},
906
- components: {
907
- schemas: {},
908
- },
909
- };
910
- for (const typeName in types) {
911
- const type = types[typeName];
912
- if ((graphql.isObjectType(type) || graphql.isInputObjectType(type)) &&
913
- !graphql.isIntrospectionType(type)) {
914
- swagger.components.schemas[typeName] = buildSchemaObjectFromType(type, {
915
- customScalars,
916
- });
917
- }
918
- }
919
- if (components) {
920
- swagger.components = Object.assign(Object.assign({}, components), swagger.components);
921
- }
922
- if (security) {
923
- swagger.security = security;
924
- }
925
- if (tags) {
926
- swagger.tags = tags;
927
- }
928
- return {
929
- addRoute(info, config) {
930
- const basePath = (config === null || config === void 0 ? void 0 : config.basePath) || '';
931
- const path = basePath +
932
- normalizePathParamForOpenAPI(info.path);
933
- if (!swagger.paths[path]) {
934
- swagger.paths[path] = {};
935
- }
936
- const pathsObj = swagger.paths[path];
937
- pathsObj[info.method.toLowerCase()] = buildPathFromOperation({
938
- url: path,
939
- operation: info.document,
940
- schema,
941
- useRequestBody: ['POST', 'PUT', 'PATCH'].includes(info.method),
942
- tags: info.tags || [],
943
- description: info.description || '',
944
- customScalars,
945
- });
946
- },
947
- get() {
948
- return swagger;
949
- },
950
- };
974
+ function OpenAPI({ schema, info, servers, components, security, tags, customScalars = {}, }) {
975
+ const types = schema.getTypeMap();
976
+ const swagger = {
977
+ openapi: '3.0.0',
978
+ info,
979
+ servers,
980
+ tags: [],
981
+ paths: {},
982
+ components: {
983
+ schemas: {},
984
+ },
985
+ };
986
+ for (const typeName in types) {
987
+ const type = types[typeName];
988
+ if ((graphql.isObjectType(type) || graphql.isInputObjectType(type)) &&
989
+ !graphql.isIntrospectionType(type)) {
990
+ swagger.components.schemas[typeName] = buildSchemaObjectFromType(type, {
991
+ customScalars,
992
+ });
993
+ }
994
+ }
995
+ if (components) {
996
+ swagger.components = Object.assign(Object.assign({}, components), swagger.components);
997
+ }
998
+ if (security) {
999
+ swagger.security = security;
1000
+ }
1001
+ if (tags) {
1002
+ swagger.tags = tags;
1003
+ }
1004
+ return {
1005
+ addRoute(info, config) {
1006
+ const basePath = (config === null || config === void 0 ? void 0 : config.basePath) || '';
1007
+ const path = basePath +
1008
+ normalizePathParamForOpenAPI(info.path);
1009
+ if (!swagger.paths[path]) {
1010
+ swagger.paths[path] = {};
1011
+ }
1012
+ const pathsObj = swagger.paths[path];
1013
+ pathsObj[info.method.toLowerCase()] = buildPathFromOperation({
1014
+ url: path,
1015
+ operation: info.document,
1016
+ schema,
1017
+ useRequestBody: ['POST', 'PUT', 'PATCH'].includes(info.method),
1018
+ tags: info.tags || [],
1019
+ description: info.description || '',
1020
+ customScalars,
1021
+ });
1022
+ },
1023
+ get() {
1024
+ return swagger;
1025
+ },
1026
+ };
951
1027
  }
952
1028
 
953
- function useSofa(config) {
954
- return createRouter(createSofa(config));
1029
+ function useSofa(config) {
1030
+ return createRouter(createSofa(config));
955
1031
  }
956
1032
 
957
1033
  exports.OpenAPI = OpenAPI;