fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #376 from hey-api/feat/services-to-compiler-2

feat: convert service request option templates to use compiler api

authored by

Jordan Shatford and committed by
GitHub
030bab75 d82f3110

+198 -164
+1 -2
packages/openapi-ts/rollup.config.ts
··· 25 25 knownHelpers: { 26 26 camelCase: true, 27 27 dataDestructure: true, 28 - dataParameters: true, 29 28 equals: true, 30 - escapeDescription: true, 31 29 ifdef: true, 32 30 nameOperationDataType: true, 33 31 notEquals: true, 34 32 toOperationComment: true, 33 + toRequestOptions: true, 35 34 useDateType: true, 36 35 }, 37 36 knownHelpersOnly: true,
+45 -11
packages/openapi-ts/src/compiler/types.ts
··· 5 5 /** 6 6 * Convert an unknown value to an expression. 7 7 * @param value - the unknown value. 8 + * @param unescape - if string should be unescaped. 9 + * @param shorthand - if shorthand syntax is allowed. 10 + * @param indentifier - list of keys that are treated as indentifiers. 8 11 * @returns ts.Expression 9 12 */ 10 - export const toExpression = (value: unknown, unescape = false): ts.Expression | undefined => { 13 + const toExpression = <T = unknown>({ 14 + value, 15 + unescape = false, 16 + shorthand = false, 17 + identifiers = [], 18 + }: { 19 + value: T; 20 + unescape?: boolean; 21 + shorthand?: boolean; 22 + identifiers?: string[]; 23 + }): ts.Expression | undefined => { 11 24 if (Array.isArray(value)) { 12 25 return createArrayType({ arr: value }); 13 26 } 14 27 15 28 if (typeof value === 'object' && value !== null) { 16 - return createObjectType({ obj: value }); 29 + return createObjectType({ identifiers, obj: value, shorthand }); 17 30 } 18 31 19 32 if (typeof value === 'number') { ··· 47 60 multiLine?: boolean; 48 61 }): ts.ArrayLiteralExpression => 49 62 ts.factory.createArrayLiteralExpression( 50 - arr.map(v => toExpression(v)).filter(isType<ts.Expression>), 63 + arr.map(value => toExpression({ value })).filter(isType<ts.Expression>), 51 64 // Multiline if the array contains objects, or if specified by the user. 52 65 (!Array.isArray(arr[0]) && typeof arr[0] === 'object') || multiLine 53 66 ); 54 67 55 68 /** 56 69 * Create Object type expression. 57 - * @param options - options to use when creating type. 70 + * @param comments - comments to add to each property. 71 + * @param identifier - keys that should be treated as identifiers. 72 + * @param multiLine - if the object should be multiline. 73 + * @param obj - the object to create expression with. 74 + * @param shorthand - if shorthand syntax should be used. 75 + * @param unescape - if properties strings should be unescaped. 58 76 * @returns ts.ObjectLiteralExpression 59 77 */ 60 78 export const createObjectType = <T extends object>({ 61 79 comments = {}, 80 + identifiers = [], 62 81 multiLine = true, 63 82 obj, 83 + shorthand = false, 64 84 unescape = false, 65 85 }: { 66 86 obj: T; 87 + comments?: Record<string | number, Comments>; 88 + identifiers?: string[]; 67 89 multiLine?: boolean; 90 + shorthand?: boolean; 68 91 unescape?: boolean; 69 - comments?: Record<string | number, Comments>; 70 92 }): ts.ObjectLiteralExpression => { 71 93 const properties = Object.entries(obj) 72 94 .map(([key, value]) => { 73 - const initializer = toExpression(value, unescape); 95 + // Pass all object properties as identifiers if the whole object is a indentifier 96 + let initializer: ts.Expression | undefined = toExpression({ 97 + identifiers: identifiers.includes(key) ? Object.keys(value) : [], 98 + shorthand, 99 + unescape, 100 + value, 101 + }); 74 102 if (!initializer) { 75 103 return undefined; 76 104 } 105 + // Create a identifier if the current key is one and it is not an object 106 + if (identifiers.includes(key) && !ts.isObjectLiteralExpression(initializer)) { 107 + initializer = ts.factory.createIdentifier(value as string); 108 + } 77 109 if (key.match(/\W/g) && !key.startsWith("'") && !key.endsWith("'")) { 78 110 key = `'${key}'`; 79 111 } 80 - const assignment = ts.factory.createPropertyAssignment(key, initializer); 112 + const assignment = 113 + shorthand && key === value 114 + ? ts.factory.createShorthandPropertyAssignment(key) 115 + : ts.factory.createPropertyAssignment(key, initializer); 81 116 const c = comments?.[key]; 82 117 if (c?.length) { 83 118 addLeadingComment(assignment, c); 84 119 } 85 120 return assignment; 86 121 }) 87 - .filter(isType<ts.PropertyAssignment>); 88 - const expression = ts.factory.createObjectLiteralExpression(properties, multiLine); 89 - return expression; 122 + .filter(isType<ts.ShorthandPropertyAssignment | ts.PropertyAssignment>); 123 + return ts.factory.createObjectLiteralExpression(properties as any[], multiLine); 90 124 }; 91 125 92 126 /** ··· 112 146 [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 113 147 ts.factory.createIdentifier(name), 114 148 Object.entries(obj).map(([key, value]) => { 115 - const initializer = toExpression(value, true); 149 + const initializer = toExpression({ unescape: true, value }); 116 150 const assignment = ts.factory.createEnumMember(key, initializer); 117 151 const c = comments?.[key]; 118 152 if (c) {
+8 -54
packages/openapi-ts/src/templates/exportService.hbs
··· 19 19 {{#equals @root.$config.client 'angular'}} 20 20 public {{{name}}}({{{nameOperationDataType 'req' this}}}): Observable<{{{nameOperationDataType 'res' this}}}> { 21 21 {{{dataDestructure this}}} 22 - return this.httpRequest.request({ 22 + return this.httpRequest.request({{{toRequestOptions this}}}); 23 + } 23 24 {{else}} 24 25 public {{{name}}}({{{nameOperationDataType 'req' this}}}): CancelablePromise<{{{nameOperationDataType 'res' this}}}> { 25 26 {{{dataDestructure this}}} 26 - return this.httpRequest.request({ 27 + return this.httpRequest.request({{{toRequestOptions this}}}); 28 + } 27 29 {{/equals}} 28 30 {{else}} 29 31 {{#equals @root.$config.client 'angular'}} 30 32 public {{{name}}}({{{nameOperationDataType 'req' this}}}): Observable<{{{nameOperationDataType 'res' this}}}> { 31 33 {{{dataDestructure this}}} 32 - return __request(OpenAPI, this.http, { 34 + return __request(OpenAPI, this.http, {{{toRequestOptions this}}}); 35 + } 33 36 {{else}} 34 37 public static {{{name}}}({{{nameOperationDataType 'req' this}}}): CancelablePromise<{{{nameOperationDataType 'res' this}}}> { 35 38 {{{dataDestructure this}}} 36 - return __request(OpenAPI, { 39 + return __request(OpenAPI, {{{toRequestOptions this}}}); 40 + } 37 41 {{/equals}} 38 42 {{/if}} 39 - method: '{{{method}}}', 40 - url: '{{{path}}}', 41 - {{#if parametersPath}} 42 - path: { 43 - {{{dataParameters parametersPath}}} 44 - }, 45 - {{/if}} 46 - {{#if parametersCookie}} 47 - cookies: { 48 - {{{dataParameters parametersCookie}}} 49 - }, 50 - {{/if}} 51 - {{#if parametersHeader}} 52 - headers: { 53 - {{{dataParameters parametersHeader}}} 54 - }, 55 - {{/if}} 56 - {{#if parametersQuery}} 57 - query: { 58 - {{{dataParameters parametersQuery}}} 59 - }, 60 - {{/if}} 61 - {{#if parametersForm}} 62 - formData: { 63 - {{{dataParameters parametersForm}}} 64 - }, 65 - {{/if}} 66 - {{#if parametersBody}} 67 - {{#equals parametersBody.in 'formData'}} 68 - formData: {{{parametersBody.name}}}, 69 - {{/equals}} 70 - {{#equals parametersBody.in 'body'}} 71 - body: {{{parametersBody.name}}}, 72 - {{/equals}} 73 - {{#if parametersBody.mediaType}} 74 - mediaType: '{{{parametersBody.mediaType}}}', 75 - {{/if}} 76 - {{/if}} 77 - {{#if responseHeader}} 78 - responseHeader: '{{{responseHeader}}}', 79 - {{/if}} 80 - {{#if errors}} 81 - errors: { 82 - {{#each errors}} 83 - {{{code}}}: `{{{escapeDescription description}}}`, 84 - {{/each}} 85 - }, 86 - {{/if}} 87 - }); 88 - } 89 43 90 44 {{/each}} 91 45 }
-2
packages/openapi-ts/src/utils/__tests__/handlebars.spec.ts
··· 29 29 const helpers = Object.keys(Handlebars.helpers); 30 30 expect(helpers).toContain('camelCase'); 31 31 expect(helpers).toContain('dataDestructure'); 32 - expect(helpers).toContain('dataParameters'); 33 32 expect(helpers).toContain('equals'); 34 - expect(helpers).toContain('escapeDescription'); 35 33 expect(helpers).toContain('ifdef'); 36 34 expect(helpers).toContain('nameOperationDataType'); 37 35 expect(helpers).toContain('notEquals');
+66 -17
packages/openapi-ts/src/utils/handlebars.ts
··· 77 77 return ''; 78 78 }; 79 79 80 - const dataParameters = (parameters: OperationParameter[]) => { 81 - const output = parameters.map(parameter => { 82 - const key = parameter.prop; 83 - const value = parameter.name; 84 - if (key === value) { 85 - return key; 86 - } 87 - if (escapeName(key) === key) { 88 - return `${key}: ${value}`; 89 - } 90 - return `'${key}': ${value}`; 91 - }); 92 - return output.join(', '); 93 - }; 94 - 95 80 export const serviceExportedNamespace = () => '$OpenApiTs'; 96 81 97 82 export const nameOperationDataType = (namespace: 'req' | 'res', operation: Service['operations'][number]) => { ··· 135 120 export const registerHandlebarHelpers = (): void => { 136 121 Handlebars.registerHelper('camelCase', camelCase); 137 122 Handlebars.registerHelper('dataDestructure', dataDestructure); 138 - Handlebars.registerHelper('dataParameters', dataParameters); 139 123 140 124 Handlebars.registerHelper( 141 125 'equals', ··· 144 128 } 145 129 ); 146 130 147 - Handlebars.registerHelper('escapeDescription', escapeDescription); 131 + Handlebars.registerHelper('toRequestOptions', (operation: Operation) => { 132 + const toObj = (parameters: OperationParameter[]) => 133 + parameters.reduce( 134 + (prev, curr) => { 135 + const key = curr.prop; 136 + const value = curr.name; 137 + if (key === value) { 138 + prev[key] = key; 139 + } else if (escapeName(key) === key) { 140 + prev[key] = value; 141 + } else { 142 + prev[`'${key}'`] = value; 143 + } 144 + return prev; 145 + }, 146 + {} as Record<string, unknown> 147 + ); 148 + 149 + const obj: Record<string, any> = { 150 + method: operation.method, 151 + url: operation.path, 152 + }; 153 + if (operation.parametersPath.length) { 154 + obj.path = toObj(operation.parametersPath); 155 + } 156 + if (operation.parametersCookie.length) { 157 + obj.cookies = toObj(operation.parametersCookie); 158 + } 159 + if (operation.parametersHeader.length) { 160 + obj.headers = toObj(operation.parametersHeader); 161 + } 162 + if (operation.parametersQuery.length) { 163 + obj.query = toObj(operation.parametersQuery); 164 + } 165 + if (operation.parametersForm.length) { 166 + obj.formData = toObj(operation.parametersForm); 167 + } 168 + if (operation.parametersBody) { 169 + if (operation.parametersBody.in === 'formData') { 170 + obj.formData = operation.parametersBody.name; 171 + } 172 + if (operation.parametersBody.in === 'body') { 173 + obj.body = operation.parametersBody.name; 174 + } 175 + } 176 + if (operation.parametersBody?.mediaType) { 177 + obj.mediaType = operation.parametersBody?.mediaType; 178 + } 179 + if (operation.responseHeader) { 180 + obj.responseHeader = operation.responseHeader; 181 + } 182 + if (operation.errors.length) { 183 + const errors: Record<number, string> = {}; 184 + operation.errors.forEach(err => { 185 + errors[err.code] = escapeDescription(err.description); 186 + }); 187 + obj.errors = errors; 188 + } 189 + return compiler.utils.toString( 190 + compiler.types.object({ 191 + identifiers: ['body', 'headers', 'formData', 'cookies', 'path', 'query'], 192 + obj, 193 + shorthand: true, 194 + }) 195 + ); 196 + }); 148 197 149 198 Handlebars.registerHelper('toOperationComment', (operation: Operation) => { 150 199 const config = getConfig();
+14 -14
packages/openapi-ts/test/__snapshots__/test/generated/v2/services.gen.ts.snap
··· 369 369 method: 'POST', 370 370 url: '/api/v{api-version}/response', 371 371 errors: { 372 - 500: `Message for 500 error`, 373 - 501: `Message for 501 error`, 374 - 502: `Message for 502 error`, 372 + 500: 'Message for 500 error', 373 + 501: 'Message for 501 error', 374 + 502: 'Message for 502 error', 375 375 }, 376 376 }); 377 377 } ··· 393 393 method: 'PUT', 394 394 url: '/api/v{api-version}/response', 395 395 errors: { 396 - 500: `Message for 500 error`, 397 - 501: `Message for 501 error`, 398 - 502: `Message for 502 error`, 396 + 500: 'Message for 500 error', 397 + 501: 'Message for 501 error', 398 + 502: 'Message for 502 error', 399 399 }, 400 400 }); 401 401 } ··· 547 547 parameterReference, 548 548 }, 549 549 errors: { 550 - 400: `400 server error`, 551 - 500: `500 server error`, 550 + 400: '400 server error', 551 + 500: '500 server error', 552 552 }, 553 553 }); 554 554 } ··· 567 567 url: '/api/v{api-version}/header', 568 568 responseHeader: 'operation-location', 569 569 errors: { 570 - 400: `400 server error`, 571 - 500: `500 server error`, 570 + 400: '400 server error', 571 + 500: '500 server error', 572 572 }, 573 573 }); 574 574 } ··· 590 590 status, 591 591 }, 592 592 errors: { 593 - 500: `Custom message: Internal Server Error`, 594 - 501: `Custom message: Not Implemented`, 595 - 502: `Custom message: Bad Gateway`, 596 - 503: `Custom message: Service Unavailable`, 593 + 500: 'Custom message: Internal Server Error', 594 + 501: 'Custom message: Not Implemented', 595 + 502: 'Custom message: Bad Gateway', 596 + 503: 'Custom message: Service Unavailable', 597 597 }, 598 598 }); 599 599 }
+16 -16
packages/openapi-ts/test/__snapshots__/test/generated/v3/services.gen.ts.snap
··· 340 340 query: { 341 341 parameter, 342 342 }, 343 - formData: formData, 343 + formData, 344 344 mediaType: 'multipart/form-data', 345 345 }); 346 346 } ··· 530 530 method: 'POST', 531 531 url: '/api/v{api-version}/response', 532 532 errors: { 533 - 500: `Message for 500 error`, 534 - 501: `Message for 501 error`, 535 - 502: `Message for 502 error`, 533 + 500: 'Message for 500 error', 534 + 501: 'Message for 501 error', 535 + 502: 'Message for 502 error', 536 536 }, 537 537 }); 538 538 } ··· 554 554 method: 'PUT', 555 555 url: '/api/v{api-version}/response', 556 556 errors: { 557 - 500: `Message for 500 error`, 558 - 501: `Message for 501 error`, 559 - 502: `Message for 502 error`, 557 + 500: 'Message for 500 error', 558 + 501: 'Message for 501 error', 559 + 502: 'Message for 502 error', 560 560 }, 561 561 }); 562 562 } ··· 746 746 parameterReference, 747 747 }, 748 748 errors: { 749 - 400: `400 server error`, 750 - 500: `500 server error`, 749 + 400: '400 server error', 750 + 500: '500 server error', 751 751 }, 752 752 }); 753 753 } ··· 783 783 return __request(OpenAPI, { 784 784 method: 'POST', 785 785 url: '/api/v{api-version}/multipart', 786 - formData: formData, 786 + formData, 787 787 mediaType: 'multipart/form-data', 788 788 }); 789 789 } ··· 815 815 url: '/api/v{api-version}/header', 816 816 responseHeader: 'operation-location', 817 817 errors: { 818 - 400: `400 server error`, 819 - 500: `500 server error`, 818 + 400: '400 server error', 819 + 500: '500 server error', 820 820 }, 821 821 }); 822 822 } ··· 838 838 status, 839 839 }, 840 840 errors: { 841 - 500: `Custom message: Internal Server Error`, 842 - 501: `Custom message: Not Implemented`, 843 - 502: `Custom message: Bad Gateway`, 844 - 503: `Custom message: Service Unavailable`, 841 + 500: 'Custom message: Internal Server Error', 842 + 501: 'Custom message: Not Implemented', 843 + 502: 'Custom message: Bad Gateway', 844 + 503: 'Custom message: Service Unavailable', 845 845 }, 846 846 }); 847 847 }
+16 -16
packages/openapi-ts/test/__snapshots__/test/generated/v3_angular/services.gen.ts.snap
··· 373 373 query: { 374 374 parameter, 375 375 }, 376 - formData: formData, 376 + formData, 377 377 mediaType: 'multipart/form-data', 378 378 }); 379 379 } ··· 577 577 method: 'POST', 578 578 url: '/api/v{api-version}/response', 579 579 errors: { 580 - 500: `Message for 500 error`, 581 - 501: `Message for 501 error`, 582 - 502: `Message for 502 error`, 580 + 500: 'Message for 500 error', 581 + 501: 'Message for 501 error', 582 + 502: 'Message for 502 error', 583 583 }, 584 584 }); 585 585 } ··· 601 601 method: 'PUT', 602 602 url: '/api/v{api-version}/response', 603 603 errors: { 604 - 500: `Message for 500 error`, 605 - 501: `Message for 501 error`, 606 - 502: `Message for 502 error`, 604 + 500: 'Message for 500 error', 605 + 501: 'Message for 501 error', 606 + 502: 'Message for 502 error', 607 607 }, 608 608 }); 609 609 } ··· 831 831 parameterReference, 832 832 }, 833 833 errors: { 834 - 400: `400 server error`, 835 - 500: `500 server error`, 834 + 400: '400 server error', 835 + 500: '500 server error', 836 836 }, 837 837 }); 838 838 } ··· 871 871 return __request(OpenAPI, this.http, { 872 872 method: 'POST', 873 873 url: '/api/v{api-version}/multipart', 874 - formData: formData, 874 + formData, 875 875 mediaType: 'multipart/form-data', 876 876 }); 877 877 } ··· 904 904 url: '/api/v{api-version}/header', 905 905 responseHeader: 'operation-location', 906 906 errors: { 907 - 400: `400 server error`, 908 - 500: `500 server error`, 907 + 400: '400 server error', 908 + 500: '500 server error', 909 909 }, 910 910 }); 911 911 } ··· 932 932 status, 933 933 }, 934 934 errors: { 935 - 500: `Custom message: Internal Server Error`, 936 - 501: `Custom message: Not Implemented`, 937 - 502: `Custom message: Bad Gateway`, 938 - 503: `Custom message: Service Unavailable`, 935 + 500: 'Custom message: Internal Server Error', 936 + 501: 'Custom message: Not Implemented', 937 + 502: 'Custom message: Bad Gateway', 938 + 503: 'Custom message: Service Unavailable', 939 939 }, 940 940 }); 941 941 }
+16 -16
packages/openapi-ts/test/__snapshots__/test/generated/v3_client/services.gen.ts.snap
··· 353 353 query: { 354 354 parameter, 355 355 }, 356 - formData: formData, 356 + formData, 357 357 mediaType: 'multipart/form-data', 358 358 }); 359 359 } ··· 551 551 method: 'POST', 552 552 url: '/api/v{api-version}/response', 553 553 errors: { 554 - 500: `Message for 500 error`, 555 - 501: `Message for 501 error`, 556 - 502: `Message for 502 error`, 554 + 500: 'Message for 500 error', 555 + 501: 'Message for 501 error', 556 + 502: 'Message for 502 error', 557 557 }, 558 558 }); 559 559 } ··· 575 575 method: 'PUT', 576 576 url: '/api/v{api-version}/response', 577 577 errors: { 578 - 500: `Message for 500 error`, 579 - 501: `Message for 501 error`, 580 - 502: `Message for 502 error`, 578 + 500: 'Message for 500 error', 579 + 501: 'Message for 501 error', 580 + 502: 'Message for 502 error', 581 581 }, 582 582 }); 583 583 } ··· 783 783 parameterReference, 784 784 }, 785 785 errors: { 786 - 400: `400 server error`, 787 - 500: `500 server error`, 786 + 400: '400 server error', 787 + 500: '500 server error', 788 788 }, 789 789 }); 790 790 } ··· 822 822 return this.httpRequest.request({ 823 823 method: 'POST', 824 824 url: '/api/v{api-version}/multipart', 825 - formData: formData, 825 + formData, 826 826 mediaType: 'multipart/form-data', 827 827 }); 828 828 } ··· 852 852 url: '/api/v{api-version}/header', 853 853 responseHeader: 'operation-location', 854 854 errors: { 855 - 400: `400 server error`, 856 - 500: `500 server error`, 855 + 400: '400 server error', 856 + 500: '500 server error', 857 857 }, 858 858 }); 859 859 } ··· 877 877 status, 878 878 }, 879 879 errors: { 880 - 500: `Custom message: Internal Server Error`, 881 - 501: `Custom message: Not Implemented`, 882 - 502: `Custom message: Bad Gateway`, 883 - 503: `Custom message: Service Unavailable`, 880 + 500: 'Custom message: Internal Server Error', 881 + 501: 'Custom message: Not Implemented', 882 + 502: 'Custom message: Bad Gateway', 883 + 503: 'Custom message: Service Unavailable', 884 884 }, 885 885 }); 886 886 }
+16 -16
packages/openapi-ts/test/__snapshots__/test/generated/v3_enums_typescript/services.gen.ts.snap
··· 340 340 query: { 341 341 parameter, 342 342 }, 343 - formData: formData, 343 + formData, 344 344 mediaType: 'multipart/form-data', 345 345 }); 346 346 } ··· 530 530 method: 'POST', 531 531 url: '/api/v{api-version}/response', 532 532 errors: { 533 - 500: `Message for 500 error`, 534 - 501: `Message for 501 error`, 535 - 502: `Message for 502 error`, 533 + 500: 'Message for 500 error', 534 + 501: 'Message for 501 error', 535 + 502: 'Message for 502 error', 536 536 }, 537 537 }); 538 538 } ··· 554 554 method: 'PUT', 555 555 url: '/api/v{api-version}/response', 556 556 errors: { 557 - 500: `Message for 500 error`, 558 - 501: `Message for 501 error`, 559 - 502: `Message for 502 error`, 557 + 500: 'Message for 500 error', 558 + 501: 'Message for 501 error', 559 + 502: 'Message for 502 error', 560 560 }, 561 561 }); 562 562 } ··· 746 746 parameterReference, 747 747 }, 748 748 errors: { 749 - 400: `400 server error`, 750 - 500: `500 server error`, 749 + 400: '400 server error', 750 + 500: '500 server error', 751 751 }, 752 752 }); 753 753 } ··· 783 783 return __request(OpenAPI, { 784 784 method: 'POST', 785 785 url: '/api/v{api-version}/multipart', 786 - formData: formData, 786 + formData, 787 787 mediaType: 'multipart/form-data', 788 788 }); 789 789 } ··· 815 815 url: '/api/v{api-version}/header', 816 816 responseHeader: 'operation-location', 817 817 errors: { 818 - 400: `400 server error`, 819 - 500: `500 server error`, 818 + 400: '400 server error', 819 + 500: '500 server error', 820 820 }, 821 821 }); 822 822 } ··· 838 838 status, 839 839 }, 840 840 errors: { 841 - 500: `Custom message: Internal Server Error`, 842 - 501: `Custom message: Not Implemented`, 843 - 502: `Custom message: Bad Gateway`, 844 - 503: `Custom message: Service Unavailable`, 841 + 500: 'Custom message: Internal Server Error', 842 + 501: 'Custom message: Not Implemented', 843 + 502: 'Custom message: Bad Gateway', 844 + 503: 'Custom message: Service Unavailable', 845 845 }, 846 846 }); 847 847 }