appflare 0.1.12 → 0.2.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.
Files changed (31) hide show
  1. package/Documentation.md +735 -0
  2. package/cli/generate.ts +1 -1
  3. package/cli/templates/auth/README.md +3 -3
  4. package/cli/templates/auth/route-config.ts +1 -18
  5. package/cli/templates/auth/route-handler.ts +1 -18
  6. package/cli/templates/auth/route-request-utils.ts +2 -52
  7. package/cli/templates/auth/route.config.ts +18 -0
  8. package/cli/templates/auth/route.handler.ts +18 -0
  9. package/cli/templates/auth/route.request-utils.ts +55 -0
  10. package/cli/templates/auth/route.ts +2 -2
  11. package/cli/templates/core/README.md +2 -2
  12. package/cli/templates/core/client/appflare.ts +78 -3
  13. package/cli/templates/core/client/handlers.ts +1 -0
  14. package/cli/templates/core/client/index.ts +1 -0
  15. package/cli/templates/core/client/storage.ts +85 -5
  16. package/cli/templates/core/client/types.ts +91 -0
  17. package/cli/templates/core/client-modules/appflare.ts +1 -112
  18. package/cli/templates/core/client-modules/handlers.ts +1 -1
  19. package/cli/templates/core/client-modules/index.ts +1 -7
  20. package/cli/templates/core/client-modules/storage.ts +1 -180
  21. package/cli/templates/core/client-modules/types.ts +1 -145
  22. package/cli/templates/core/client.artifacts.ts +39 -0
  23. package/cli/templates/core/client.ts +4 -39
  24. package/cli/templates/core/server.ts +1 -1
  25. package/cli/templates/handlers/generators/registration/modules/realtime/publisher.ts +2 -5
  26. package/cli/templates/handlers/generators/registration/modules/realtime/routes.ts +4 -11
  27. package/cli/templates/handlers/generators/types/query-definitions.ts +74 -18
  28. package/cli/templates/handlers/generators/types/query-runtime.ts +20 -4
  29. package/package.json +1 -1
  30. /package/cli/templates/core/{client-modules → client}/handlers/index.ts +0 -0
  31. /package/cli/templates/core/{handlers-route.ts → handlers.route.ts} +0 -0
package/cli/generate.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { mkdir } from "node:fs/promises";
2
2
  import { relative, resolve } from "node:path";
3
3
  import { generateAuthConfigSource } from "./templates/auth/config";
4
- import { generateClientArtifacts } from "./templates/core/client";
4
+ import { generateClientArtifacts } from "./templates/core/client.artifacts";
5
5
  import { generateDrizzleConfigSource } from "./templates/core/drizzle";
6
6
  import { generateHandlersArtifacts } from "./templates/core/handlers";
7
7
  import { generateServerSource } from "./templates/core/server";
@@ -53,7 +53,7 @@ Behavior details:
53
53
 
54
54
  ---
55
55
 
56
- ## `route-config.ts`
56
+ ## `route.config.ts`
57
57
 
58
58
  ### `generateKvField(kvBinding?: string): string`
59
59
 
@@ -73,7 +73,7 @@ This keeps route generation dynamic for projects that may or may not enable KV.
73
73
 
74
74
  ---
75
75
 
76
- ## `route-handler.ts`
76
+ ## `route.handler.ts`
77
77
 
78
78
  ### `generateAuthHandler(authBasePath: string, databaseBinding: string, kvBinding?: string): string`
79
79
 
@@ -89,7 +89,7 @@ This is the bridge between framework routing and Better Auth request handling.
89
89
 
90
90
  ---
91
91
 
92
- ## `route-request-utils.ts`
92
+ ## `route.request-utils.ts`
93
93
 
94
94
  Contains generated helper functions to sanitize incoming request headers.
95
95
 
@@ -1,18 +1 @@
1
- function generateKvField(kvBinding?: string): string {
2
- if (!kvBinding) {
3
- return "";
4
- }
5
-
6
- return `,\n\t\t\tKV: c.env["${kvBinding}"] as KVNamespace`;
7
- }
8
-
9
- function generateAuthConfig(
10
- databaseBinding: string,
11
- kvBinding?: string,
12
- ): string {
13
- return `{
14
- DATABASE: c.env["${databaseBinding}"] as D1Database${generateKvField(kvBinding)}
15
- }`;
16
- }
17
-
18
- export { generateKvField, generateAuthConfig };
1
+ export { generateAuthConfig, generateKvField } from "./route.config";
@@ -1,18 +1 @@
1
- import { generateAuthConfig } from "./route-config";
2
-
3
- function generateAuthHandler(
4
- authBasePath: string,
5
- databaseBinding: string,
6
- kvBinding?: string,
7
- ): string {
8
- return `app.on(["GET", "POST"], "${authBasePath}/*", async (c) => {
9
- const auth = createAuth(
10
- ${generateAuthConfig(databaseBinding, kvBinding)},
11
- c.req.raw.cf as IncomingRequestCfProperties | undefined,
12
- );
13
- return auth.handler(getSanitizedRequest(c.req.raw));
14
- });
15
- `;
16
- }
17
-
18
- export { generateAuthHandler };
1
+ export { generateAuthHandler } from "./route.handler";
@@ -1,55 +1,5 @@
1
- function generateGetHeadersFunction(): string {
2
- return `export const getHeaders = (headers: Headers) => {
3
- const newHeaders = Object.fromEntries(headers as any);
4
- const headerObject: Record<string, any> = {};
5
- let hasCookie = false;
6
-
7
- for (const key in newHeaders) {
8
- if (key.toLowerCase() === "cookie") {
9
- hasCookie = true;
10
- break;
11
- }
12
- }
13
-
14
- for (const key in newHeaders) {
15
- const isAuthorization =
16
- key.toLowerCase() === "authorization" &&
17
- newHeaders[key]?.includes("Bearer");
18
-
19
- if (hasCookie && key.toLowerCase() === "authorization") {
20
- continue;
21
- }
22
-
23
- if (key.toLowerCase() === "authorization" && !isAuthorization) {
24
- continue;
25
- }
26
-
27
- headerObject[key] = newHeaders[key];
28
- }
29
-
30
- return headerObject as any as Headers;
31
- };
32
- `;
33
- }
34
-
35
- function generateSanitizedRequestFunction(): string {
36
- return `export const getSanitizedRequest = (req: Request) => {
37
- const newRequest = new Request(req, {
38
- headers: getHeaders(req.headers),
39
- });
40
- return newRequest;
41
- };
42
- `;
43
- }
44
-
45
- function generateRequestUtilities(): string {
46
- return (
47
- generateGetHeadersFunction() + "\n" + generateSanitizedRequestFunction()
48
- );
49
- }
50
-
51
1
  export {
52
2
  generateGetHeadersFunction,
53
- generateSanitizedRequestFunction,
54
3
  generateRequestUtilities,
55
- };
4
+ generateSanitizedRequestFunction,
5
+ } from "./route.request-utils";
@@ -0,0 +1,18 @@
1
+ function generateKvField(kvBinding?: string): string {
2
+ if (!kvBinding) {
3
+ return "";
4
+ }
5
+
6
+ return `,\n\t\t\tKV: c.env["${kvBinding}"] as KVNamespace`;
7
+ }
8
+
9
+ function generateAuthConfig(
10
+ databaseBinding: string,
11
+ kvBinding?: string,
12
+ ): string {
13
+ return `{
14
+ DATABASE: c.env["${databaseBinding}"] as D1Database${generateKvField(kvBinding)}
15
+ }`;
16
+ }
17
+
18
+ export { generateKvField, generateAuthConfig };
@@ -0,0 +1,18 @@
1
+ import { generateAuthConfig } from "./route.config";
2
+
3
+ function generateAuthHandler(
4
+ authBasePath: string,
5
+ databaseBinding: string,
6
+ kvBinding?: string,
7
+ ): string {
8
+ return `app.on(["GET", "POST"], "${authBasePath}/*", async (c) => {
9
+ const auth = createAuth(
10
+ ${generateAuthConfig(databaseBinding, kvBinding)},
11
+ c.req.raw.cf as IncomingRequestCfProperties | undefined,
12
+ );
13
+ return auth.handler(getSanitizedRequest(c.req.raw));
14
+ });
15
+ `;
16
+ }
17
+
18
+ export { generateAuthHandler };
@@ -0,0 +1,55 @@
1
+ function generateGetHeadersFunction(): string {
2
+ return `export const getHeaders = (headers: Headers) => {
3
+ const newHeaders = Object.fromEntries(headers as any);
4
+ const headerObject: Record<string, any> = {};
5
+ let hasCookie = false;
6
+
7
+ for (const key in newHeaders) {
8
+ if (key.toLowerCase() === "cookie") {
9
+ hasCookie = true;
10
+ break;
11
+ }
12
+ }
13
+
14
+ for (const key in newHeaders) {
15
+ const isAuthorization =
16
+ key.toLowerCase() === "authorization" &&
17
+ newHeaders[key]?.includes("Bearer");
18
+
19
+ if (hasCookie && key.toLowerCase() === "authorization") {
20
+ continue;
21
+ }
22
+
23
+ if (key.toLowerCase() === "authorization" && !isAuthorization) {
24
+ continue;
25
+ }
26
+
27
+ headerObject[key] = newHeaders[key];
28
+ }
29
+
30
+ return headerObject as any as Headers;
31
+ };
32
+ `;
33
+ }
34
+
35
+ function generateSanitizedRequestFunction(): string {
36
+ return `export const getSanitizedRequest = (req: Request) => {
37
+ const newRequest = new Request(req, {
38
+ headers: getHeaders(req.headers),
39
+ });
40
+ return newRequest;
41
+ };
42
+ `;
43
+ }
44
+
45
+ function generateRequestUtilities(): string {
46
+ return (
47
+ generateGetHeadersFunction() + "\n" + generateSanitizedRequestFunction()
48
+ );
49
+ }
50
+
51
+ export {
52
+ generateGetHeadersFunction,
53
+ generateSanitizedRequestFunction,
54
+ generateRequestUtilities,
55
+ };
@@ -1,5 +1,5 @@
1
- import { generateAuthHandler } from "./route-handler";
2
- import { generateRequestUtilities } from "./route-request-utils";
1
+ import { generateAuthHandler } from "./route.handler";
2
+ import { generateRequestUtilities } from "./route.request-utils";
3
3
 
4
4
  export function generateAuthRoute(
5
5
  authBasePath: string,
@@ -66,7 +66,7 @@ CORS behavior details:
66
66
 
67
67
  ---
68
68
 
69
- ### `client.ts`
69
+ ### `client.artifacts.ts`
70
70
 
71
71
  Provides `generateClientArtifacts(configPathImport)`.
72
72
 
@@ -120,7 +120,7 @@ This is the standard Cloudflare Worker module format.
120
120
 
121
121
  ---
122
122
 
123
- ### `handlers-route.ts`
123
+ ### `handlers.route.ts`
124
124
 
125
125
  Provides `generateHandlersRoute(databaseBinding, kvBinding?)`.
126
126
 
@@ -2,29 +2,104 @@ export function generateClientAppflareSource(): string {
2
2
  return `import { createAuthClient, type BetterAuthClientOptions } from "better-auth/client";
3
3
  import type {
4
4
  AppflareAuth,
5
+ AppflareAuthTokenResolver,
5
6
  AppflareOptions,
6
7
  InferredAuthOptions,
7
8
  StorageClient,
8
9
  } from "./types";
10
+ import type { MutationsClient, QueriesClient } from "./handlers";
9
11
  import { createStorageClient } from "./storage";
12
+ import { createMutationsClient, createQueriesClient } from "./handlers";
10
13
 
11
14
  export class Appflare<Options extends BetterAuthClientOptions = InferredAuthOptions> {
12
15
  public readonly endpoint: string;
16
+ public readonly wsEndpoint: string;
13
17
  public readonly auth: AppflareAuth<Options>;
14
18
  public readonly storage: StorageClient;
19
+ public readonly queries: QueriesClient;
20
+ public readonly mutations: MutationsClient;
15
21
 
16
22
  public constructor(options: AppflareOptions<Options>) {
17
- this.endpoint = options.endpoint.replace(/\/$/, "");
23
+ this.endpoint = options.endpoint.replace(/\\/$/, "");
24
+ this.wsEndpoint = (options.wsEndpoint ?? this.endpoint).replace(/\\/$/, "");
18
25
  const authOptions = (options.authOptions ?? {}) as Options;
19
26
  const request = options.fetch ?? fetch;
27
+ const onSetAuthToken = options.onSetAuthToken;
28
+ const onGetAuthToken: AppflareAuthTokenResolver =
29
+ options.onGetAuthToken ?? (() => "");
30
+
31
+ const authFetchOptions =
32
+ (authOptions as { fetchOptions?: Record<string, unknown> }).fetchOptions ??
33
+ {};
34
+ const mergedAuthOptions = {
35
+ ...(authOptions as Record<string, unknown>),
36
+ fetchOptions: {
37
+ ...authFetchOptions,
38
+ onSuccess: async (ctx: {
39
+ response: { headers: { get: (name: string) => string | null } };
40
+ }) => {
41
+ if (
42
+ typeof (authFetchOptions as { onSuccess?: unknown }).onSuccess ===
43
+ "function"
44
+ ) {
45
+ await (
46
+ (authFetchOptions as { onSuccess: (ctx: unknown) => unknown }).onSuccess
47
+ )(ctx);
48
+ }
49
+
50
+ const authToken = ctx.response.headers.get("set-auth-token");
51
+ if (authToken && onSetAuthToken) {
52
+ await onSetAuthToken(authToken);
53
+ }
54
+ },
55
+ onRequest: async (ctx: {
56
+ headers: Headers;
57
+ }) => {
58
+ let nextCtx = ctx;
59
+ if (
60
+ typeof (authFetchOptions as { onRequest?: unknown }).onRequest ===
61
+ "function"
62
+ ) {
63
+ const maybeCtx = await (
64
+ (authFetchOptions as { onRequest: (ctx: unknown) => unknown }).onRequest
65
+ )(ctx);
66
+ if (
67
+ typeof maybeCtx === "object" &&
68
+ maybeCtx !== null &&
69
+ "headers" in maybeCtx
70
+ ) {
71
+ nextCtx = maybeCtx as typeof ctx;
72
+ }
73
+ }
74
+
75
+ const authToken = await onGetAuthToken();
76
+ if (typeof authToken === "string" && authToken.trim().length > 0) {
77
+ nextCtx.headers.set("Authorization", \`Bearer \${authToken.trim()}\`);
78
+ }
79
+
80
+ return nextCtx;
81
+ },
82
+ },
83
+ };
20
84
 
21
85
  this.auth = createAuthClient<Options>({
22
- ...authOptions,
86
+ ...(mergedAuthOptions as Options),
23
87
  baseURL: \`\${this.endpoint}\${options.authPath ?? "/api/auth"}\`,
24
88
  fetch: request,
25
89
  });
26
90
 
27
- this.storage = createStorageClient(this.endpoint, request);
91
+ this.storage = createStorageClient(this.endpoint, request, onGetAuthToken);
92
+ this.queries = createQueriesClient(
93
+ this.endpoint,
94
+ options.requestOptions,
95
+ this.wsEndpoint,
96
+ onGetAuthToken,
97
+ );
98
+ this.mutations = createMutationsClient(
99
+ this.endpoint,
100
+ options.requestOptions,
101
+ onGetAuthToken,
102
+ );
28
103
  }
29
104
  }
30
105
 
@@ -0,0 +1 @@
1
+ export { generateClientHandlersSource } from "./handlers/index";
@@ -2,5 +2,6 @@ export function generateClientIndexSource(): string {
2
2
  return `export * from "./types";
3
3
  export * from "./appflare";
4
4
  export * from "./storage";
5
+ export * from "./handlers";
5
6
  `;
6
7
  }
@@ -1,14 +1,82 @@
1
1
  export function generateClientStorageSource(): string {
2
2
  return `import type { StorageClient, StorageSignedUrlResponse, StorageListResponse } from "./types";
3
3
 
4
- export function createStorageClient(endpoint: string, request: typeof fetch = fetch): StorageClient {
4
+ type AuthTokenResolver = (() => string | Promise<string>) | undefined;
5
+
6
+ function toHeaderRecord(headers: HeadersInit | undefined): Record<string, string> {
7
+ const result: Record<string, string> = {};
8
+ if (!headers) {
9
+ return result;
10
+ }
11
+
12
+ if (Array.isArray(headers)) {
13
+ for (const entry of headers) {
14
+ if (!Array.isArray(entry) || entry.length < 2) {
15
+ continue;
16
+ }
17
+ result[String(entry[0])] = String(entry[1]);
18
+ }
19
+ return result;
20
+ }
21
+
22
+ if (typeof (headers as { forEach?: unknown }).forEach === "function") {
23
+ (headers as Headers).forEach((value, key) => {
24
+ result[key] = String(value);
25
+ });
26
+ return result;
27
+ }
28
+
29
+ for (const [key, value] of Object.entries(headers as Record<string, unknown>)) {
30
+ result[key] = String(value ?? "");
31
+ }
32
+
33
+ return result;
34
+ }
35
+
36
+ function hasAuthorizationHeader(headers: Record<string, string>): boolean {
37
+ for (const key of Object.keys(headers)) {
38
+ if (key.toLowerCase() === "authorization") {
39
+ return true;
40
+ }
41
+ }
42
+
43
+ return false;
44
+ }
45
+
46
+ async function createAuthorizedHeaders(
47
+ headers: HeadersInit | undefined,
48
+ onGetAuthToken: AuthTokenResolver,
49
+ ): Promise<HeadersInit | undefined> {
50
+ const resolvedHeaders = toHeaderRecord(headers);
51
+ if (onGetAuthToken) {
52
+ const authToken = await onGetAuthToken();
53
+ if (typeof authToken === "string" && authToken.trim().length > 0) {
54
+ if (!hasAuthorizationHeader(resolvedHeaders)) {
55
+ resolvedHeaders.authorization = \`Bearer \${authToken.trim()}\`;
56
+ }
57
+ }
58
+ }
59
+
60
+ return resolvedHeaders;
61
+ }
62
+
63
+ export function createStorageClient(
64
+ endpoint: string,
65
+ request: typeof fetch = fetch,
66
+ onGetAuthToken?: () => string | Promise<string>,
67
+ ): StorageClient {
5
68
  return {
6
69
  upload: async (args) => {
7
- const response = await request(\`\${endpoint}/storage/upload\`, {
8
- method: "POST",
9
- headers: {
70
+ const headers = await createAuthorizedHeaders(
71
+ {
10
72
  "content-type": "application/json",
11
73
  },
74
+ onGetAuthToken,
75
+ );
76
+
77
+ const response = await request(\`\${endpoint}/storage/upload\`, {
78
+ method: "POST",
79
+ headers,
12
80
  body: JSON.stringify(args),
13
81
  });
14
82
  if (!response.ok) {
@@ -27,6 +95,9 @@ export function createStorageClient(endpoint: string, request: typeof fetch = fe
27
95
  });
28
96
  const response = await request(
29
97
  \`\${endpoint}/storage/download?\${query.toString()}\`,
98
+ {
99
+ headers: await createAuthorizedHeaders(undefined, onGetAuthToken),
100
+ },
30
101
  );
31
102
  if (!response.ok) {
32
103
  throw new Error(await response.text());
@@ -43,6 +114,9 @@ export function createStorageClient(endpoint: string, request: typeof fetch = fe
43
114
  });
44
115
  const response = await request(
45
116
  \`\${endpoint}/storage/preview?\${query.toString()}\`,
117
+ {
118
+ headers: await createAuthorizedHeaders(undefined, onGetAuthToken),
119
+ },
46
120
  );
47
121
  if (!response.ok) {
48
122
  throw new Error(await response.text());
@@ -56,7 +130,10 @@ export function createStorageClient(endpoint: string, request: typeof fetch = fe
56
130
  });
57
131
  const response = await request(
58
132
  \`\${endpoint}/storage/object?\${query.toString()}\`,
59
- { method: "DELETE" },
133
+ {
134
+ method: "DELETE",
135
+ headers: await createAuthorizedHeaders(undefined, onGetAuthToken),
136
+ },
60
137
  );
61
138
  if (!response.ok) {
62
139
  throw new Error(await response.text());
@@ -87,6 +164,9 @@ export function createStorageClient(endpoint: string, request: typeof fetch = fe
87
164
  querySuffix.length > 0
88
165
  ? \`\${endpoint}/storage/list?\${querySuffix}\`
89
166
  : \`\${endpoint}/storage/list\`,
167
+ {
168
+ headers: await createAuthorizedHeaders(undefined, onGetAuthToken),
169
+ },
90
170
  );
91
171
  if (!response.ok) {
92
172
  throw new Error(await response.text());
@@ -9,11 +9,89 @@ export type AppflareAuth<Options extends BetterAuthClientOptions = InferredAuthO
9
9
  typeof createAuthClient<Options>
10
10
  >;
11
11
 
12
+ export type AppflareAuthTokenResolver = () => string | Promise<string>;
13
+
12
14
  export type AppflareOptions<Options extends BetterAuthClientOptions = InferredAuthOptions> = {
13
15
  endpoint: string;
16
+ wsEndpoint?: string;
14
17
  authPath?: string;
15
18
  authOptions?: Options;
16
19
  fetch?: typeof fetch;
20
+ requestOptions?: AppflareResultRouteCallOptions;
21
+ onSetAuthToken?: (token: string) => void | Promise<void>;
22
+ onGetAuthToken?: AppflareAuthTokenResolver;
23
+ };
24
+
25
+ export type AppflareErrorMode = "throw" | "return";
26
+
27
+ export type AppflareRequestError = {
28
+ route: string;
29
+ method: "GET" | "POST";
30
+ status: number;
31
+ message: string;
32
+ body?: unknown;
33
+ responseText?: string;
34
+ };
35
+
36
+ export type AppflareRequestResult<TData> = {
37
+ data: TData | null;
38
+ error: AppflareRequestError | null;
39
+ };
40
+
41
+ export type AppflareRouteCallOptions<
42
+ Mode extends AppflareErrorMode = "throw",
43
+ > = {
44
+ errorMode?: Mode;
45
+ headers?: HeadersInit;
46
+ signal?: AbortSignal;
47
+ onError?: (error: AppflareRequestError) => void;
48
+ };
49
+
50
+ export type AppflareResultRouteCallOptions = Omit<
51
+ AppflareRouteCallOptions<"return">,
52
+ "errorMode"
53
+ >;
54
+
55
+ export type AppflareRouteClient<
56
+ TSchema extends import("zod").ZodObject<import("zod").ZodRawShape>,
57
+ TOutput,
58
+ > = {
59
+ schema: TSchema;
60
+ run(
61
+ args: import("zod").input<TSchema>,
62
+ options?: AppflareResultRouteCallOptions,
63
+ ): Promise<AppflareRequestResult<TOutput>>;
64
+ };
65
+
66
+ export type AppflareRealtimeSubscription = {
67
+ remove: () => void;
68
+ };
69
+
70
+ export type AppflareRealtimeQueryUpdate<TOutput> = {
71
+ event: "query:update";
72
+ payload: {
73
+ queryName: string;
74
+ signature: string;
75
+ data: TOutput;
76
+ };
77
+ };
78
+
79
+ export type AppflareQuerySubscribeOptions<TInput, TOutput> = {
80
+ args?: TInput;
81
+ authToken?: string;
82
+ onChange: (data: TOutput, update: AppflareRealtimeQueryUpdate<TOutput>) => void;
83
+ onError?: (error: unknown) => void;
84
+ requestOptions?: AppflareRouteCallOptions;
85
+ signal?: AbortSignal;
86
+ };
87
+
88
+ export type AppflareQueryRouteClient<
89
+ TSchema extends import("zod").ZodObject<import("zod").ZodRawShape>,
90
+ TOutput,
91
+ > = AppflareRouteClient<TSchema, TOutput> & {
92
+ subscribe: (
93
+ options: AppflareQuerySubscribeOptions<import("zod").input<TSchema>, TOutput>,
94
+ ) => AppflareRealtimeSubscription;
17
95
  };
18
96
 
19
97
  export type StorageSignedUrlResponse = {
@@ -50,5 +128,18 @@ export type StorageClient = {
50
128
  method?: "download" | "get" | "delete" | "list" | "put" | "preview";
51
129
  }) => Promise<StorageListResponse>;
52
130
  };
131
+
132
+ export type RealtimeSubscriptionResponse = {
133
+ token: string;
134
+ signature: string;
135
+ websocket: {
136
+ url: string;
137
+ protocol: string;
138
+ params: {
139
+ tokenParam: "token";
140
+ authTokenParam: "authToken";
141
+ };
142
+ };
143
+ };
53
144
  `;
54
145
  }