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