@devwithbobby/loops 0.1.13 → 0.1.16

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.
@@ -1,7 +1,7 @@
1
1
  import { actionGeneric, queryGeneric } from "convex/server";
2
2
  import { v } from "convex/values";
3
- import type { Mounts } from "../component/_generated/api.js";
4
- import type { RunActionCtx, RunQueryCtx, UseApi } from "./types.js";
3
+ import type { Mounts } from "../component/_generated/api";
4
+ import type { RunActionCtx, RunQueryCtx, UseApi } from "../types";
5
5
 
6
6
  export type LoopsComponent = UseApi<Mounts>;
7
7
 
@@ -18,20 +18,20 @@ export interface ContactData {
18
18
  export interface TransactionalEmailOptions {
19
19
  transactionalId: string;
20
20
  email: string;
21
- dataVariables?: Record<string, any>;
21
+ dataVariables?: Record<string, unknown>;
22
22
  }
23
23
 
24
24
  export interface EventOptions {
25
25
  email: string;
26
26
  eventName: string;
27
- eventProperties?: Record<string, any>;
27
+ eventProperties?: Record<string, unknown>;
28
28
  }
29
29
 
30
30
  export class Loops {
31
- private readonly component: LoopsComponent;
32
31
  public readonly options?: {
33
32
  apiKey?: string;
34
33
  };
34
+ private readonly lib: NonNullable<LoopsComponent["lib"]>;
35
35
 
36
36
  constructor(
37
37
  component: LoopsComponent,
@@ -56,7 +56,7 @@ export class Loops {
56
56
  );
57
57
  }
58
58
 
59
- this.component = component;
59
+ this.lib = component.lib;
60
60
  this.options = options;
61
61
 
62
62
  const apiKey = options?.apiKey ?? process.env.LOOPS_API_KEY;
@@ -83,22 +83,7 @@ export class Loops {
83
83
  * Add or update a contact in Loops
84
84
  */
85
85
  async addContact(ctx: RunActionCtx, contact: ContactData) {
86
- if (!this.component) {
87
- throw new Error(
88
- "Loops component is not initialized. " +
89
- "Make sure to pass components.loops to the Loops constructor: " +
90
- "new Loops(components.loops)",
91
- );
92
- }
93
- if (!this.component.lib) {
94
- throw new Error(
95
- "Invalid component reference. " +
96
- "The component may not be properly mounted. " +
97
- "Ensure the component is correctly mounted in convex.config.ts: " +
98
- "app.use(loops);",
99
- );
100
- }
101
- return ctx.runAction((this.component.lib as any).addContact, {
86
+ return ctx.runAction(this.lib.addContact, {
102
87
  apiKey: this.apiKey,
103
88
  contact,
104
89
  });
@@ -111,10 +96,10 @@ export class Loops {
111
96
  ctx: RunActionCtx,
112
97
  email: string,
113
98
  updates: Partial<ContactData> & {
114
- dataVariables?: Record<string, any>;
99
+ dataVariables?: Record<string, unknown>;
115
100
  },
116
101
  ) {
117
- return ctx.runAction((this.component.lib as any).updateContact, {
102
+ return ctx.runAction(this.lib.updateContact, {
118
103
  apiKey: this.apiKey,
119
104
  email,
120
105
  ...updates,
@@ -128,7 +113,7 @@ export class Loops {
128
113
  ctx: RunActionCtx,
129
114
  options: TransactionalEmailOptions,
130
115
  ) {
131
- return ctx.runAction((this.component.lib as any).sendTransactional, {
116
+ return ctx.runAction(this.lib.sendTransactional, {
132
117
  apiKey: this.apiKey,
133
118
  ...options,
134
119
  });
@@ -138,7 +123,7 @@ export class Loops {
138
123
  * Send an event to Loops to trigger email workflows
139
124
  */
140
125
  async sendEvent(ctx: RunActionCtx, options: EventOptions) {
141
- return ctx.runAction((this.component.lib as any).sendEvent, {
126
+ return ctx.runAction(this.lib.sendEvent, {
142
127
  apiKey: this.apiKey,
143
128
  ...options,
144
129
  });
@@ -149,7 +134,7 @@ export class Loops {
149
134
  * Retrieves contact information from Loops
150
135
  */
151
136
  async findContact(ctx: RunActionCtx, email: string) {
152
- return ctx.runAction((this.component.lib as any).findContact, {
137
+ return ctx.runAction(this.lib.findContact, {
153
138
  apiKey: this.apiKey,
154
139
  email,
155
140
  });
@@ -160,7 +145,7 @@ export class Loops {
160
145
  * Create multiple contacts in a single API call
161
146
  */
162
147
  async batchCreateContacts(ctx: RunActionCtx, contacts: ContactData[]) {
163
- return ctx.runAction((this.component.lib as any).batchCreateContacts, {
148
+ return ctx.runAction(this.lib.batchCreateContacts, {
164
149
  apiKey: this.apiKey,
165
150
  contacts,
166
151
  });
@@ -171,7 +156,7 @@ export class Loops {
171
156
  * Unsubscribes a contact from receiving emails (they remain in the system)
172
157
  */
173
158
  async unsubscribeContact(ctx: RunActionCtx, email: string) {
174
- return ctx.runAction((this.component.lib as any).unsubscribeContact, {
159
+ return ctx.runAction(this.lib.unsubscribeContact, {
175
160
  apiKey: this.apiKey,
176
161
  email,
177
162
  });
@@ -182,7 +167,7 @@ export class Loops {
182
167
  * Resubscribes a previously unsubscribed contact
183
168
  */
184
169
  async resubscribeContact(ctx: RunActionCtx, email: string) {
185
- return ctx.runAction((this.component.lib as any).resubscribeContact, {
170
+ return ctx.runAction(this.lib.resubscribeContact, {
186
171
  apiKey: this.apiKey,
187
172
  email,
188
173
  });
@@ -201,10 +186,7 @@ export class Loops {
201
186
  subscribed?: boolean;
202
187
  },
203
188
  ) {
204
- return ctx.runQuery(
205
- (this.component.lib as any).countContacts,
206
- options ?? {},
207
- );
189
+ return ctx.runQuery(this.lib.countContacts, options ?? {});
208
190
  }
209
191
 
210
192
  /**
@@ -222,7 +204,7 @@ export class Loops {
222
204
  offset?: number;
223
205
  },
224
206
  ) {
225
- return ctx.runQuery((this.component.lib as any).listContacts, {
207
+ return ctx.runQuery(this.lib.listContacts, {
226
208
  userGroup: options?.userGroup,
227
209
  source: options?.source,
228
210
  subscribed: options?.subscribed,
@@ -241,7 +223,7 @@ export class Loops {
241
223
  maxEmailsPerRecipient?: number;
242
224
  },
243
225
  ) {
244
- return ctx.runQuery((this.component.lib as any).detectRecipientSpam, {
226
+ return ctx.runQuery(this.lib.detectRecipientSpam, {
245
227
  timeWindowMs: options?.timeWindowMs ?? 3600000,
246
228
  maxEmailsPerRecipient: options?.maxEmailsPerRecipient ?? 10,
247
229
  });
@@ -257,7 +239,7 @@ export class Loops {
257
239
  maxEmailsPerActor?: number;
258
240
  },
259
241
  ) {
260
- return ctx.runQuery((this.component.lib as any).detectActorSpam, {
242
+ return ctx.runQuery(this.lib.detectActorSpam, {
261
243
  timeWindowMs: options?.timeWindowMs ?? 3600000,
262
244
  maxEmailsPerActor: options?.maxEmailsPerActor ?? 100,
263
245
  });
@@ -272,7 +254,7 @@ export class Loops {
272
254
  timeWindowMs?: number;
273
255
  },
274
256
  ) {
275
- return ctx.runQuery((this.component.lib as any).getEmailStats, {
257
+ return ctx.runQuery(this.lib.getEmailStats, {
276
258
  timeWindowMs: options?.timeWindowMs ?? 86400000,
277
259
  });
278
260
  }
@@ -287,7 +269,7 @@ export class Loops {
287
269
  minEmailsInWindow?: number;
288
270
  },
289
271
  ) {
290
- return ctx.runQuery((this.component.lib as any).detectRapidFirePatterns, {
272
+ return ctx.runQuery(this.lib.detectRapidFirePatterns, {
291
273
  timeWindowMs: options?.timeWindowMs ?? 60000,
292
274
  minEmailsInWindow: options?.minEmailsInWindow ?? 5,
293
275
  });
@@ -304,10 +286,7 @@ export class Loops {
304
286
  maxEmails: number;
305
287
  },
306
288
  ) {
307
- return ctx.runQuery(
308
- (this.component.lib as any).checkRecipientRateLimit,
309
- options,
310
- );
289
+ return ctx.runQuery(this.lib.checkRecipientRateLimit, options);
311
290
  }
312
291
 
313
292
  /**
@@ -321,10 +300,7 @@ export class Loops {
321
300
  maxEmails: number;
322
301
  },
323
302
  ) {
324
- return ctx.runQuery(
325
- (this.component.lib as any).checkActorRateLimit,
326
- options,
327
- );
303
+ return ctx.runQuery(this.lib.checkActorRateLimit, options);
328
304
  }
329
305
 
330
306
  /**
@@ -337,17 +313,14 @@ export class Loops {
337
313
  maxEmails: number;
338
314
  },
339
315
  ) {
340
- return ctx.runQuery(
341
- (this.component.lib as any).checkGlobalRateLimit,
342
- options,
343
- );
316
+ return ctx.runQuery(this.lib.checkGlobalRateLimit, options);
344
317
  }
345
318
 
346
319
  /**
347
320
  * Delete a contact from Loops
348
321
  */
349
322
  async deleteContact(ctx: RunActionCtx, email: string) {
350
- return ctx.runAction((this.component.lib as any).deleteContact, {
323
+ return ctx.runAction(this.lib.deleteContact, {
351
324
  apiKey: this.apiKey,
352
325
  email,
353
326
  });
@@ -368,11 +341,11 @@ export class Loops {
368
341
  options: {
369
342
  loopId: string;
370
343
  email: string;
371
- dataVariables?: Record<string, any>;
344
+ dataVariables?: Record<string, unknown>;
372
345
  eventName?: string; // Event name that triggers the loop
373
346
  },
374
347
  ) {
375
- return ctx.runAction((this.component.lib as any).triggerLoop, {
348
+ return ctx.runAction(this.lib.triggerLoop, {
376
349
  apiKey: this.apiKey,
377
350
  ...options,
378
351
  });
@@ -27,17 +27,9 @@ import type {
27
27
  declare const fullApi: ApiFromModules<{
28
28
  lib: typeof lib;
29
29
  }>;
30
- export type Mounts = {
31
- lib: {
32
- add: FunctionReference<
33
- "mutation",
34
- "public",
35
- { count: number; name: string; shards?: number },
36
- null
37
- >;
38
- count: FunctionReference<"query", "public", { name: string }, number>;
39
- };
40
- };
30
+ export type Mounts = ApiFromModules<{
31
+ lib: typeof lib;
32
+ }>;
41
33
  // For now fullApiWithMounts is only fullApi which provides
42
34
  // jump-to-definition in component client code.
43
35
  // Use Mounts for the same type without the inference.
@@ -1,8 +1,13 @@
1
1
  import { defineComponent } from "convex/server";
2
- import { api } from "./_generated/api.js";
2
+ import { api } from "./_generated/api";
3
3
 
4
4
  const component = defineComponent("loops");
5
- (component as any).export(api, {
5
+
6
+ type ExportableComponent = typeof component & {
7
+ export: (apiRef: typeof api, functions: Record<string, unknown>) => void;
8
+ };
9
+
10
+ (component as ExportableComponent).export(api, {
6
11
  addContact: api.lib.addContact,
7
12
  updateContact: api.lib.updateContact,
8
13
  findContact: api.lib.findContact,
@@ -0,0 +1,130 @@
1
+ import { type HeadersInitParam } from "../types";
2
+
3
+ const allowedOrigin =
4
+ process.env.CONVEX_URL ??
5
+ process.env.NEXT_PUBLIC_CONVEX_URL ??
6
+ process.env.CONVEX_SITE_URL ??
7
+ process.env.NEXT_PUBLIC_CONVEX_SITE_URL ??
8
+ process.env.LOOPS_HTTP_ALLOWED_ORIGIN ??
9
+ process.env.CLIENT_ORIGIN ??
10
+ "*";
11
+
12
+ export const LOOPS_API_BASE_URL = "https://app.loops.so/api/v1";
13
+
14
+ export const sanitizeLoopsError = (
15
+ status: number,
16
+ _errorText: string,
17
+ ): Error => {
18
+ if (status === 401 || status === 403) {
19
+ return new Error("Authentication failed. Please check your API key.");
20
+ }
21
+ if (status === 404) {
22
+ return new Error("Resource not found.");
23
+ }
24
+ if (status === 429) {
25
+ return new Error("Rate limit exceeded. Please try again later.");
26
+ }
27
+ if (status >= 500) {
28
+ return new Error("Loops service error. Please try again later.");
29
+ }
30
+ return new Error(`Loops API error (${status}). Please try again.`);
31
+ };
32
+
33
+ export type LoopsRequestInit = Omit<RequestInit, "body"> & {
34
+ json?: unknown;
35
+ };
36
+
37
+ export const loopsFetch = async (
38
+ apiKey: string,
39
+ path: string,
40
+ init: LoopsRequestInit = {},
41
+ ) => {
42
+ const { json, ...rest } = init;
43
+ const headers = new Headers(rest.headers ?? {});
44
+ headers.set("Authorization", `Bearer ${apiKey}`);
45
+ if (json !== undefined && !headers.has("Content-Type")) {
46
+ headers.set("Content-Type", "application/json");
47
+ }
48
+
49
+ return fetch(`${LOOPS_API_BASE_URL}${path}`, {
50
+ ...rest,
51
+ headers,
52
+ // @ts-expect-error RequestInit in this build doesn't declare body
53
+ body: json !== undefined ? JSON.stringify(json) : rest.body,
54
+ });
55
+ };
56
+
57
+ export const buildCorsHeaders = (extra?: HeadersInitParam) => {
58
+ const headers = new Headers(extra ?? {});
59
+ headers.set("Access-Control-Allow-Origin", allowedOrigin);
60
+ headers.set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
61
+ headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
62
+ headers.set("Access-Control-Max-Age", "86400");
63
+ headers.set("Vary", "Origin");
64
+ return headers;
65
+ };
66
+
67
+ export const jsonResponse = (data: unknown, init?: ResponseInit) => {
68
+ const headers = buildCorsHeaders(
69
+ (init?.headers as HeadersInitParam | undefined) ?? undefined,
70
+ );
71
+ headers.set("Content-Type", "application/json");
72
+ return new Response(JSON.stringify(data), { ...init, headers });
73
+ };
74
+
75
+ export const emptyResponse = (init?: ResponseInit) => {
76
+ const headers = buildCorsHeaders(
77
+ (init?.headers as HeadersInitParam | undefined) ?? undefined,
78
+ );
79
+ return new Response(null, { ...init, headers });
80
+ };
81
+
82
+ export const readJsonBody = async <T>(request: Request): Promise<T> => {
83
+ try {
84
+ return (await request.json()) as T;
85
+ } catch (error) {
86
+ throw new Error("Invalid JSON body");
87
+ }
88
+ };
89
+
90
+ export const booleanFromQuery = (value: string | null) => {
91
+ if (value === null) {
92
+ return undefined;
93
+ }
94
+ if (value === "true") {
95
+ return true;
96
+ }
97
+ if (value === "false") {
98
+ return false;
99
+ }
100
+ return undefined;
101
+ };
102
+
103
+ export const numberFromQuery = (value: string | null, fallback: number) => {
104
+ if (!value) {
105
+ return fallback;
106
+ }
107
+ const parsed = Number.parseInt(value, 10);
108
+ return Number.isNaN(parsed) ? fallback : parsed;
109
+ };
110
+
111
+ export const requireLoopsApiKey = () => {
112
+ const apiKey = process.env.LOOPS_API_KEY;
113
+ if (!apiKey) {
114
+ throw new Error(
115
+ "LOOPS_API_KEY environment variable must be set to use the HTTP API.",
116
+ );
117
+ }
118
+ return apiKey;
119
+ };
120
+
121
+ export const respondError = (error: unknown) => {
122
+ console.error("[loops:http]", error);
123
+ const message = error instanceof Error ? error.message : "Unexpected error";
124
+ const status =
125
+ error instanceof Error &&
126
+ error.message.includes("LOOPS_API_KEY environment variable")
127
+ ? 500
128
+ : 400;
129
+ return jsonResponse({ error: message }, { status });
130
+ };
@@ -0,0 +1,239 @@
1
+ import { httpRouter } from "convex/server";
2
+ import {
3
+ type ContactPayload,
4
+ type DeleteContactPayload,
5
+ type EventPayload,
6
+ internalLib,
7
+ type TransactionalPayload,
8
+ type TriggerPayload,
9
+ type UpdateContactPayload,
10
+ } from "../types";
11
+ import {
12
+ buildCorsHeaders,
13
+ jsonResponse,
14
+ emptyResponse,
15
+ readJsonBody,
16
+ respondError,
17
+ booleanFromQuery,
18
+ numberFromQuery,
19
+ requireLoopsApiKey,
20
+ } from "./helpers";
21
+ import { httpAction } from "./_generated/server";
22
+
23
+ const http = httpRouter();
24
+
25
+ const allowedOrigin =
26
+ process.env.LOOPS_HTTP_ALLOWED_ORIGIN ?? process.env.CLIENT_ORIGIN ?? "*";
27
+
28
+
29
+ http.route({
30
+ pathPrefix: "/loops",
31
+ method: "OPTIONS",
32
+ handler: httpAction(async (_ctx, request) => {
33
+ const headers = buildCorsHeaders();
34
+ const requestedHeaders = request.headers.get(
35
+ "Access-Control-Request-Headers",
36
+ );
37
+ if (requestedHeaders) {
38
+ headers.set("Access-Control-Allow-Headers", requestedHeaders);
39
+ }
40
+ const requestedMethod = request.headers.get(
41
+ "Access-Control-Request-Method",
42
+ );
43
+ if (requestedMethod) {
44
+ headers.set("Access-Control-Allow-Methods", `${requestedMethod},OPTIONS`);
45
+ }
46
+ return new Response(null, { status: 204, headers });
47
+ }),
48
+ });
49
+
50
+ http.route({
51
+ path: "/loops/contacts",
52
+ method: "POST",
53
+ handler: httpAction(async (ctx, request) => {
54
+ try {
55
+ const contact = await readJsonBody<ContactPayload>(request);
56
+ const data = await ctx.runAction(internalLib.addContact, {
57
+ apiKey: requireLoopsApiKey(),
58
+ contact,
59
+ });
60
+ return jsonResponse(data, { status: 201 });
61
+ } catch (error) {
62
+ return respondError(error);
63
+ }
64
+ }),
65
+ });
66
+
67
+ http.route({
68
+ path: "/loops/contacts",
69
+ method: "PUT",
70
+ handler: httpAction(async (ctx, request) => {
71
+ try {
72
+ const payload = await readJsonBody<UpdateContactPayload>(request);
73
+ if (!payload.email) {
74
+ throw new Error("email is required");
75
+ }
76
+ const data = await ctx.runAction(internalLib.updateContact, {
77
+ apiKey: requireLoopsApiKey(),
78
+ email: payload.email,
79
+ dataVariables: payload.dataVariables,
80
+ firstName: payload.firstName,
81
+ lastName: payload.lastName,
82
+ userId: payload.userId,
83
+ source: payload.source,
84
+ subscribed: payload.subscribed,
85
+ userGroup: payload.userGroup,
86
+ });
87
+ return jsonResponse(data);
88
+ } catch (error) {
89
+ return respondError(error);
90
+ }
91
+ }),
92
+ });
93
+
94
+ http.route({
95
+ path: "/loops/contacts",
96
+ method: "GET",
97
+ handler: httpAction(async (ctx, request) => {
98
+ try {
99
+ const url = new URL(request.url);
100
+ const email = url.searchParams.get("email");
101
+ if (email) {
102
+ const data = await ctx.runAction(internalLib.findContact, {
103
+ apiKey: requireLoopsApiKey(),
104
+ email,
105
+ });
106
+ return jsonResponse(data);
107
+ }
108
+
109
+ const data = await ctx.runQuery(internalLib.listContacts, {
110
+ userGroup: url.searchParams.get("userGroup") ?? undefined,
111
+ source: url.searchParams.get("source") ?? undefined,
112
+ subscribed: booleanFromQuery(url.searchParams.get("subscribed")),
113
+ limit: numberFromQuery(url.searchParams.get("limit"), 100),
114
+ offset: numberFromQuery(url.searchParams.get("offset"), 0),
115
+ });
116
+ return jsonResponse(data);
117
+ } catch (error) {
118
+ return respondError(error);
119
+ }
120
+ }),
121
+ });
122
+
123
+ http.route({
124
+ path: "/loops/contacts",
125
+ method: "DELETE",
126
+ handler: httpAction(async (ctx, request) => {
127
+ try {
128
+ const payload = await readJsonBody<DeleteContactPayload>(request);
129
+ if (!payload.email) {
130
+ throw new Error("email is required");
131
+ }
132
+ await ctx.runAction(internalLib.deleteContact, {
133
+ apiKey: requireLoopsApiKey(),
134
+ email: payload.email,
135
+ });
136
+ return emptyResponse({ status: 204 });
137
+ } catch (error) {
138
+ return respondError(error);
139
+ }
140
+ }),
141
+ });
142
+
143
+ http.route({
144
+ path: "/loops/transactional",
145
+ method: "POST",
146
+ handler: httpAction(async (ctx, request) => {
147
+ try {
148
+ const payload = await readJsonBody<TransactionalPayload>(request);
149
+ if (!payload.transactionalId) {
150
+ throw new Error("transactionalId is required");
151
+ }
152
+ if (!payload.email) {
153
+ throw new Error("email is required");
154
+ }
155
+ const data = await ctx.runAction(internalLib.sendTransactional, {
156
+ apiKey: requireLoopsApiKey(),
157
+ transactionalId: payload.transactionalId,
158
+ email: payload.email,
159
+ dataVariables: payload.dataVariables,
160
+ });
161
+ return jsonResponse(data);
162
+ } catch (error) {
163
+ return respondError(error);
164
+ }
165
+ }),
166
+ });
167
+
168
+ http.route({
169
+ path: "/loops/events",
170
+ method: "POST",
171
+ handler: httpAction(async (ctx, request) => {
172
+ try {
173
+ const payload = await readJsonBody<EventPayload>(request);
174
+ if (!payload.email) {
175
+ throw new Error("email is required");
176
+ }
177
+ if (!payload.eventName) {
178
+ throw new Error("eventName is required");
179
+ }
180
+ const data = await ctx.runAction(internalLib.sendEvent, {
181
+ apiKey: requireLoopsApiKey(),
182
+ email: payload.email,
183
+ eventName: payload.eventName,
184
+ eventProperties: payload.eventProperties,
185
+ });
186
+ return jsonResponse(data);
187
+ } catch (error) {
188
+ return respondError(error);
189
+ }
190
+ }),
191
+ });
192
+
193
+ http.route({
194
+ path: "/loops/trigger",
195
+ method: "POST",
196
+ handler: httpAction(async (ctx, request) => {
197
+ try {
198
+ const payload = await readJsonBody<TriggerPayload>(request);
199
+ if (!payload.loopId) {
200
+ throw new Error("loopId is required");
201
+ }
202
+ if (!payload.email) {
203
+ throw new Error("email is required");
204
+ }
205
+ const data = await ctx.runAction(internalLib.triggerLoop, {
206
+ apiKey: requireLoopsApiKey(),
207
+ loopId: payload.loopId,
208
+ email: payload.email,
209
+ dataVariables: payload.dataVariables,
210
+ eventName: payload.eventName,
211
+ });
212
+ return jsonResponse(data);
213
+ } catch (error) {
214
+ return respondError(error);
215
+ }
216
+ }),
217
+ });
218
+
219
+ http.route({
220
+ path: "/loops/stats",
221
+ method: "GET",
222
+ handler: httpAction(async (ctx, request) => {
223
+ try {
224
+ const url = new URL(request.url);
225
+ const timeWindowMs = numberFromQuery(
226
+ url.searchParams.get("timeWindowMs"),
227
+ 86400000,
228
+ );
229
+ const data = await ctx.runQuery(internalLib.getEmailStats, {
230
+ timeWindowMs,
231
+ });
232
+ return jsonResponse(data);
233
+ } catch (error) {
234
+ return respondError(error);
235
+ }
236
+ }),
237
+ });
238
+
239
+ export default http;