convex-paystack 0.0.1

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 (52) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +490 -0
  3. package/dist/client/_generated/_ignore.d.ts +1 -0
  4. package/dist/client/_generated/_ignore.d.ts.map +1 -0
  5. package/dist/client/_generated/_ignore.js +3 -0
  6. package/dist/client/_generated/_ignore.js.map +1 -0
  7. package/dist/client/index.d.ts +201 -0
  8. package/dist/client/index.d.ts.map +1 -0
  9. package/dist/client/index.js +383 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/component/_generated/api.d.ts +34 -0
  12. package/dist/component/_generated/api.d.ts.map +1 -0
  13. package/dist/component/_generated/api.js +31 -0
  14. package/dist/component/_generated/api.js.map +1 -0
  15. package/dist/component/_generated/component.d.ts +147 -0
  16. package/dist/component/_generated/component.d.ts.map +1 -0
  17. package/dist/component/_generated/component.js +11 -0
  18. package/dist/component/_generated/component.js.map +1 -0
  19. package/dist/component/_generated/dataModel.d.ts +46 -0
  20. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  21. package/dist/component/_generated/dataModel.js +11 -0
  22. package/dist/component/_generated/dataModel.js.map +1 -0
  23. package/dist/component/_generated/server.d.ts +133 -0
  24. package/dist/component/_generated/server.d.ts.map +1 -0
  25. package/dist/component/_generated/server.js +80 -0
  26. package/dist/component/_generated/server.js.map +1 -0
  27. package/dist/component/convex.config.d.ts +3 -0
  28. package/dist/component/convex.config.d.ts.map +1 -0
  29. package/dist/component/convex.config.js +4 -0
  30. package/dist/component/convex.config.js.map +1 -0
  31. package/dist/component/lib.d.ts +137 -0
  32. package/dist/component/lib.d.ts.map +1 -0
  33. package/dist/component/lib.js +241 -0
  34. package/dist/component/lib.js.map +1 -0
  35. package/dist/component/schema.d.ts +77 -0
  36. package/dist/component/schema.d.ts.map +1 -0
  37. package/dist/component/schema.js +45 -0
  38. package/dist/component/schema.js.map +1 -0
  39. package/package.json +106 -0
  40. package/src/client/_generated/_ignore.ts +1 -0
  41. package/src/client/index.ts +570 -0
  42. package/src/client/setup.test.ts +26 -0
  43. package/src/component/_generated/api.ts +50 -0
  44. package/src/component/_generated/component.ts +217 -0
  45. package/src/component/_generated/dataModel.ts +60 -0
  46. package/src/component/_generated/server.ts +169 -0
  47. package/src/component/convex.config.ts +5 -0
  48. package/src/component/lib.test.ts +110 -0
  49. package/src/component/lib.ts +278 -0
  50. package/src/component/schema.ts +58 -0
  51. package/src/component/setup.test.ts +11 -0
  52. package/src/test.ts +18 -0
@@ -0,0 +1,570 @@
1
+ import { httpActionGeneric } from "convex/server";
2
+ import type { GenericActionCtx, GenericDataModel } from "convex/server";
3
+ import type { ComponentApi } from "../component/_generated/component.js";
4
+
5
+ const PAYSTACK_API_BASE = "https://api.paystack.co";
6
+
7
+ export type PaystackOptions = {
8
+ secretKey: string;
9
+ };
10
+
11
+ export type PaystackChannel =
12
+ | "card"
13
+ | "bank"
14
+ | "apple_pay"
15
+ | "ussd"
16
+ | "qr"
17
+ | "mobile_money"
18
+ | "bank_transfer"
19
+ | "eft"
20
+ | "capitec_pay"
21
+ | "payattitude";
22
+
23
+ export type InitializeTransactionArgs = {
24
+ email: string;
25
+ amount: number;
26
+ currency?: string;
27
+ callbackUrl?: string;
28
+ reference?: string;
29
+ channels?: PaystackChannel[];
30
+ plan?: string;
31
+ metadata?: Record<string, unknown>;
32
+ };
33
+
34
+ export type InitializeTransactionResult = {
35
+ authorizationUrl: string;
36
+ accessCode: string;
37
+ reference: string;
38
+ };
39
+
40
+ export type VerifyTransactionResult = {
41
+ status: string;
42
+ reference: string;
43
+ amount: number;
44
+ currency: string;
45
+ channel?: string;
46
+ gatewayResponse?: string;
47
+ paidAt?: number;
48
+ authorizationCode?: string;
49
+ customerEmail: string;
50
+ };
51
+
52
+ export type PlanInterval =
53
+ | "daily"
54
+ | "weekly"
55
+ | "monthly"
56
+ | "quarterly"
57
+ | "biannually"
58
+ | "annually";
59
+
60
+ export type CreatePlanArgs = {
61
+ name: string;
62
+ amount: number;
63
+ interval: PlanInterval;
64
+ currency?: string;
65
+ description?: string;
66
+ };
67
+
68
+ export type PlanResult = {
69
+ planCode: string;
70
+ name: string;
71
+ amount: number;
72
+ interval: string;
73
+ currency: string;
74
+ description?: string;
75
+ };
76
+
77
+ export type Balance = {
78
+ currency: string;
79
+ balance: number;
80
+ };
81
+
82
+ function timingSafeEqual(a: string, b: string): boolean {
83
+ if (a.length !== b.length) return false;
84
+ let mismatch = 0;
85
+ for (let i = 0; i < a.length; i++) {
86
+ mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
87
+ }
88
+ return mismatch === 0;
89
+ }
90
+
91
+ async function hmacSha512Hex(secret: string, payload: string): Promise<string> {
92
+ const enc = new TextEncoder();
93
+ const key = await crypto.subtle.importKey(
94
+ "raw",
95
+ enc.encode(secret),
96
+ { name: "HMAC", hash: "SHA-512" },
97
+ false,
98
+ ["sign"],
99
+ );
100
+ const signature = await crypto.subtle.sign("HMAC", key, enc.encode(payload));
101
+ return Array.from(new Uint8Array(signature))
102
+ .map((byte) => byte.toString(16).padStart(2, "0"))
103
+ .join("");
104
+ }
105
+
106
+ function planFromPaystack(plan: Record<string, unknown>): PlanResult {
107
+ return {
108
+ planCode: plan.plan_code as string,
109
+ name: plan.name as string,
110
+ amount: plan.amount as number,
111
+ interval: plan.interval as string,
112
+ currency: (plan.currency as string) ?? "NGN",
113
+ description: (plan.description as string) ?? undefined,
114
+ };
115
+ }
116
+
117
+ export class Paystack {
118
+ webhookHandler: ReturnType<typeof httpActionGeneric>;
119
+
120
+ constructor(
121
+ private component: ComponentApi,
122
+ private options: PaystackOptions,
123
+ ) {
124
+ const component_ = component;
125
+ const secretKey = options.secretKey;
126
+
127
+ this.webhookHandler = httpActionGeneric(async (ctx, request) => {
128
+ const rawBody = await request.text();
129
+ const signature = request.headers.get("x-paystack-signature");
130
+
131
+ if (!signature) {
132
+ return new Response(JSON.stringify({ error: "Missing signature" }), {
133
+ status: 400,
134
+ headers: { "Content-Type": "application/json" },
135
+ });
136
+ }
137
+
138
+ const expected = await hmacSha512Hex(secretKey, rawBody);
139
+ if (!timingSafeEqual(expected, signature)) {
140
+ console.error("convex-paystack: webhook signature mismatch");
141
+ return new Response(JSON.stringify({ error: "Invalid signature" }), {
142
+ status: 401,
143
+ headers: { "Content-Type": "application/json" },
144
+ });
145
+ }
146
+
147
+ let event: { event: string; data: Record<string, unknown> };
148
+ try {
149
+ event = JSON.parse(rawBody);
150
+ } catch {
151
+ return new Response(JSON.stringify({ error: "Invalid JSON" }), {
152
+ status: 400,
153
+ headers: { "Content-Type": "application/json" },
154
+ });
155
+ }
156
+
157
+ const data = event.data ?? {};
158
+ const eventId =
159
+ data.id !== undefined
160
+ ? `${event.event}:${data.id}`
161
+ : `${event.event}:${(data.reference as string) ?? (data.subscription_code as string) ?? crypto.randomUUID()}`;
162
+
163
+ const { alreadyProcessed } = await ctx.runMutation(component_.lib.checkAndRecordEvent, {
164
+ eventId,
165
+ eventType: event.event,
166
+ reference: (data.reference as string) ?? undefined,
167
+ payload: rawBody,
168
+ });
169
+
170
+ if (alreadyProcessed) {
171
+ return new Response(JSON.stringify({ success: true, duplicate: true }), {
172
+ status: 200,
173
+ headers: { "Content-Type": "application/json" },
174
+ });
175
+ }
176
+
177
+ switch (event.event) {
178
+ case "charge.success": {
179
+ const customer = data.customer as Record<string, unknown> | undefined;
180
+ const authorization = data.authorization as Record<string, unknown> | undefined;
181
+ await ctx.runMutation(component_.lib.recordTransaction, {
182
+ reference: data.reference as string,
183
+ customerEmail: (customer?.email as string) ?? "",
184
+ amount: data.amount as number,
185
+ currency: (data.currency as string) ?? "NGN",
186
+ status: "success",
187
+ channel: (data.channel as string) ?? undefined,
188
+ gatewayResponse: (data.gateway_response as string) ?? undefined,
189
+ authorizationCode: (authorization?.authorization_code as string) ?? undefined,
190
+ paidAt: data.paid_at ? new Date(data.paid_at as string).getTime() : undefined,
191
+ metadata: data.metadata ? JSON.stringify(data.metadata) : undefined,
192
+ });
193
+ break;
194
+ }
195
+ case "subscription.create": {
196
+ const customer = data.customer as Record<string, unknown> | undefined;
197
+ const plan = data.plan as Record<string, unknown> | undefined;
198
+ await ctx.runMutation(component_.lib.recordSubscriptionEvent, {
199
+ subscriptionCode: data.subscription_code as string,
200
+ emailToken: (data.email_token as string) ?? undefined,
201
+ customerEmail: (customer?.email as string) ?? "",
202
+ customerCode: (customer?.customer_code as string) ?? undefined,
203
+ planCode: (plan?.plan_code as string) ?? "",
204
+ status: (data.status as string as
205
+ | "active"
206
+ | "non-renewing"
207
+ | "attention"
208
+ | "completed"
209
+ | "cancelled") ?? "active",
210
+ amount: (data.amount as number) ?? undefined,
211
+ nextPaymentDate: data.next_payment_date
212
+ ? new Date(data.next_payment_date as string).getTime()
213
+ : undefined,
214
+ });
215
+ break;
216
+ }
217
+ case "subscription.disable": {
218
+ await ctx.runMutation(component_.lib.updateSubscriptionStatus, {
219
+ subscriptionCode: data.subscription_code as string,
220
+ status: "cancelled",
221
+ });
222
+ break;
223
+ }
224
+ case "subscription.not_renew": {
225
+ await ctx.runMutation(component_.lib.updateSubscriptionStatus, {
226
+ subscriptionCode: data.subscription_code as string,
227
+ status: "non-renewing",
228
+ });
229
+ break;
230
+ }
231
+ case "invoice.update": {
232
+ const subscription = data.subscription as Record<string, unknown> | undefined;
233
+ const subscriptionCode = subscription?.subscription_code as string | undefined;
234
+ if (subscriptionCode) {
235
+ await ctx.runMutation(component_.lib.updateSubscriptionStatus, {
236
+ subscriptionCode,
237
+ status: data.status === "success" ? "active" : "attention",
238
+ });
239
+ }
240
+ break;
241
+ }
242
+ case "invoice.payment_failed": {
243
+ const subscription = data.subscription as Record<string, unknown> | undefined;
244
+ const subscriptionCode = subscription?.subscription_code as string | undefined;
245
+ if (subscriptionCode) {
246
+ await ctx.runMutation(component_.lib.updateSubscriptionStatus, {
247
+ subscriptionCode,
248
+ status: "attention",
249
+ });
250
+ }
251
+ break;
252
+ }
253
+ default:
254
+ break;
255
+ }
256
+
257
+ return new Response(JSON.stringify({ success: true }), {
258
+ status: 200,
259
+ headers: { "Content-Type": "application/json" },
260
+ });
261
+ });
262
+ }
263
+
264
+ async initializeTransaction(
265
+ ctx: GenericActionCtx<GenericDataModel>,
266
+ args: InitializeTransactionArgs,
267
+ ): Promise<InitializeTransactionResult> {
268
+ const res = await fetch(`${PAYSTACK_API_BASE}/transaction/initialize`, {
269
+ method: "POST",
270
+ headers: {
271
+ Authorization: `Bearer ${this.options.secretKey}`,
272
+ "Content-Type": "application/json",
273
+ },
274
+ body: JSON.stringify({
275
+ email: args.email,
276
+ amount: args.amount,
277
+ currency: args.currency,
278
+ callback_url: args.callbackUrl,
279
+ reference: args.reference,
280
+ channels: args.channels,
281
+ plan: args.plan,
282
+ metadata: args.metadata,
283
+ }),
284
+ });
285
+ const json = (await res.json()) as {
286
+ status: boolean;
287
+ message?: string;
288
+ data: { authorization_url: string; access_code: string; reference: string };
289
+ };
290
+ if (!json.status) {
291
+ throw new Error(json.message ?? "Failed to initialize Paystack transaction");
292
+ }
293
+
294
+ await ctx.runMutation(this.component.lib.recordTransaction, {
295
+ reference: json.data.reference,
296
+ customerEmail: args.email,
297
+ amount: args.amount,
298
+ currency: args.currency ?? "NGN",
299
+ status: "pending",
300
+ metadata: args.metadata ? JSON.stringify(args.metadata) : undefined,
301
+ });
302
+
303
+ return {
304
+ authorizationUrl: json.data.authorization_url,
305
+ accessCode: json.data.access_code,
306
+ reference: json.data.reference,
307
+ };
308
+ }
309
+
310
+ async verifyTransaction(
311
+ ctx: GenericActionCtx<GenericDataModel>,
312
+ args: { reference: string },
313
+ ): Promise<VerifyTransactionResult> {
314
+ const res = await fetch(
315
+ `${PAYSTACK_API_BASE}/transaction/verify/${encodeURIComponent(args.reference)}`,
316
+ { headers: { Authorization: `Bearer ${this.options.secretKey}` } },
317
+ );
318
+ const json = (await res.json()) as {
319
+ status: boolean;
320
+ message?: string;
321
+ data: {
322
+ status: string;
323
+ reference: string;
324
+ amount: number;
325
+ currency: string;
326
+ channel?: string;
327
+ gateway_response?: string;
328
+ paid_at?: string;
329
+ authorization?: { authorization_code?: string };
330
+ customer?: { email?: string };
331
+ };
332
+ };
333
+ if (!json.status) {
334
+ throw new Error(json.message ?? "Failed to verify Paystack transaction");
335
+ }
336
+ const data = json.data;
337
+
338
+ await ctx.runMutation(this.component.lib.recordTransaction, {
339
+ reference: data.reference,
340
+ customerEmail: data.customer?.email ?? "",
341
+ amount: data.amount,
342
+ currency: data.currency,
343
+ status: data.status as "pending" | "success" | "failed" | "abandoned",
344
+ channel: data.channel ?? undefined,
345
+ gatewayResponse: data.gateway_response ?? undefined,
346
+ authorizationCode: data.authorization?.authorization_code ?? undefined,
347
+ paidAt: data.paid_at ? new Date(data.paid_at).getTime() : undefined,
348
+ });
349
+
350
+ return {
351
+ status: data.status,
352
+ reference: data.reference,
353
+ amount: data.amount,
354
+ currency: data.currency,
355
+ channel: data.channel ?? undefined,
356
+ gatewayResponse: data.gateway_response ?? undefined,
357
+ paidAt: data.paid_at ? new Date(data.paid_at).getTime() : undefined,
358
+ authorizationCode: data.authorization?.authorization_code ?? undefined,
359
+ customerEmail: data.customer?.email ?? "",
360
+ };
361
+ }
362
+
363
+ async cancelSubscription(
364
+ ctx: GenericActionCtx<GenericDataModel>,
365
+ args: { code: string; token: string },
366
+ ): Promise<void> {
367
+ const res = await fetch(`${PAYSTACK_API_BASE}/subscription/disable`, {
368
+ method: "POST",
369
+ headers: {
370
+ Authorization: `Bearer ${this.options.secretKey}`,
371
+ "Content-Type": "application/json",
372
+ },
373
+ body: JSON.stringify({ code: args.code, token: args.token }),
374
+ });
375
+ const json = (await res.json()) as { status: boolean; message?: string };
376
+ if (!json.status) {
377
+ throw new Error(json.message ?? "Failed to cancel Paystack subscription");
378
+ }
379
+ await ctx.runMutation(this.component.lib.updateSubscriptionStatus, {
380
+ subscriptionCode: args.code,
381
+ status: "cancelled",
382
+ });
383
+ }
384
+
385
+ async enableSubscription(
386
+ ctx: GenericActionCtx<GenericDataModel>,
387
+ args: { code: string; token: string },
388
+ ): Promise<void> {
389
+ const res = await fetch(`${PAYSTACK_API_BASE}/subscription/enable`, {
390
+ method: "POST",
391
+ headers: {
392
+ Authorization: `Bearer ${this.options.secretKey}`,
393
+ "Content-Type": "application/json",
394
+ },
395
+ body: JSON.stringify({ code: args.code, token: args.token }),
396
+ });
397
+ const json = (await res.json()) as { status: boolean; message?: string };
398
+ if (!json.status) {
399
+ throw new Error(json.message ?? "Failed to enable Paystack subscription");
400
+ }
401
+ await ctx.runMutation(this.component.lib.updateSubscriptionStatus, {
402
+ subscriptionCode: args.code,
403
+ status: "active",
404
+ });
405
+ }
406
+
407
+ /**
408
+ * Create a Paystack billing plan. Plans are the foundation for
409
+ * subscriptions: pass the returned `planCode` as `plan` to
410
+ * `initializeTransaction` to start a subscription on first payment.
411
+ */
412
+ async createPlan(
413
+ ctx: GenericActionCtx<GenericDataModel>,
414
+ args: CreatePlanArgs,
415
+ ): Promise<PlanResult> {
416
+ const res = await fetch(`${PAYSTACK_API_BASE}/plan`, {
417
+ method: "POST",
418
+ headers: {
419
+ Authorization: `Bearer ${this.options.secretKey}`,
420
+ "Content-Type": "application/json",
421
+ },
422
+ body: JSON.stringify({
423
+ name: args.name,
424
+ amount: args.amount,
425
+ interval: args.interval,
426
+ currency: args.currency,
427
+ description: args.description,
428
+ }),
429
+ });
430
+ const json = (await res.json()) as {
431
+ status: boolean;
432
+ message?: string;
433
+ data: Record<string, unknown>;
434
+ };
435
+ if (!json.status) {
436
+ throw new Error(json.message ?? "Failed to create Paystack plan");
437
+ }
438
+ return planFromPaystack(json.data);
439
+ }
440
+
441
+ /**
442
+ * Lists the currencies this Paystack account actually has enabled, with
443
+ * their current balance. Use this to build a currency picker that only
444
+ * ever offers currencies that will really work — `initializeTransaction`
445
+ * throws "Currency not supported by merchant" for anything else.
446
+ * Requires the secret key to have balance-read access; if it doesn't,
447
+ * catch the error and fall back to your account's default currency.
448
+ */
449
+ async listBalances(_ctx: GenericActionCtx<GenericDataModel>): Promise<Balance[]> {
450
+ const res = await fetch(`${PAYSTACK_API_BASE}/balance`, {
451
+ headers: { Authorization: `Bearer ${this.options.secretKey}` },
452
+ });
453
+ const json = (await res.json()) as {
454
+ status: boolean;
455
+ message?: string;
456
+ data: Balance[];
457
+ };
458
+ if (!json.status) {
459
+ throw new Error(json.message ?? "Failed to fetch Paystack balance");
460
+ }
461
+ return json.data;
462
+ }
463
+
464
+ /**
465
+ * Fetches a customer's subscriptions directly from Paystack (via the
466
+ * Fetch Customer endpoint, which returns them inline) and upserts each
467
+ * one locally. A checkout initialized with `plan` starts a subscription
468
+ * on Paystack's side immediately, but this component only learns about
469
+ * it when the `subscription.create` webhook arrives — which, in local
470
+ * development, requires that webhook URL to actually be registered in
471
+ * the Paystack Dashboard. Call this right after a subscription checkout
472
+ * returns to reconcile state even if that webhook hasn't fired yet.
473
+ * Returns the number of subscriptions synced.
474
+ */
475
+ async syncCustomerSubscriptions(
476
+ ctx: GenericActionCtx<GenericDataModel>,
477
+ args: { email: string },
478
+ ): Promise<number> {
479
+ const res = await fetch(
480
+ `${PAYSTACK_API_BASE}/customer/${encodeURIComponent(args.email)}`,
481
+ { headers: { Authorization: `Bearer ${this.options.secretKey}` } },
482
+ );
483
+ const json = (await res.json()) as {
484
+ status: boolean;
485
+ message?: string;
486
+ data?: { subscriptions?: Record<string, unknown>[] };
487
+ };
488
+ if (!json.status) {
489
+ throw new Error(json.message ?? "Failed to fetch Paystack customer");
490
+ }
491
+
492
+ const subscriptions = json.data?.subscriptions ?? [];
493
+ for (const sub of subscriptions) {
494
+ const plan = sub.plan as Record<string, unknown> | undefined;
495
+ const customer = sub.customer as Record<string, unknown> | undefined;
496
+ await ctx.runMutation(this.component.lib.recordSubscriptionEvent, {
497
+ subscriptionCode: sub.subscription_code as string,
498
+ emailToken: (sub.email_token as string) ?? undefined,
499
+ customerEmail: args.email,
500
+ customerCode: (customer?.customer_code as string) ?? undefined,
501
+ planCode: (plan?.plan_code as string) ?? "",
502
+ status: (sub.status as string as
503
+ | "active"
504
+ | "non-renewing"
505
+ | "attention"
506
+ | "completed"
507
+ | "cancelled") ?? "active",
508
+ amount: (sub.amount as number) ?? (plan?.amount as number) ?? undefined,
509
+ nextPaymentDate: sub.next_payment_date
510
+ ? new Date(sub.next_payment_date as string).getTime()
511
+ : undefined,
512
+ });
513
+ }
514
+ return subscriptions.length;
515
+ }
516
+
517
+ /** List billing plans already created on this Paystack account. */
518
+ async listPlans(_ctx: GenericActionCtx<GenericDataModel>): Promise<PlanResult[]> {
519
+ const res = await fetch(`${PAYSTACK_API_BASE}/plan?perPage=100`, {
520
+ headers: { Authorization: `Bearer ${this.options.secretKey}` },
521
+ });
522
+ const json = (await res.json()) as {
523
+ status: boolean;
524
+ message?: string;
525
+ data: Record<string, unknown>[];
526
+ };
527
+ if (!json.status) {
528
+ throw new Error(json.message ?? "Failed to list Paystack plans");
529
+ }
530
+ return json.data.map(planFromPaystack);
531
+ }
532
+
533
+ async getTransaction(ctx: RunQueryCtx, args: { reference: string }) {
534
+ return await ctx.runQuery(this.component.lib.getTransaction, args);
535
+ }
536
+
537
+ async listTransactions(ctx: RunQueryCtx, args: { customerEmail: string; limit?: number }) {
538
+ return await ctx.runQuery(this.component.lib.listTransactions, args);
539
+ }
540
+
541
+ async getSubscription(ctx: RunQueryCtx, args: { subscriptionCode: string }) {
542
+ return await ctx.runQuery(this.component.lib.getSubscription, args);
543
+ }
544
+
545
+ async listSubscriptions(ctx: RunQueryCtx, args: { customerEmail: string }) {
546
+ return await ctx.runQuery(this.component.lib.listSubscriptions, args);
547
+ }
548
+
549
+ async hasActiveSubscription(ctx: RunQueryCtx, args: { customerEmail: string }): Promise<boolean> {
550
+ return await ctx.runQuery(this.component.lib.hasActiveSubscription, args);
551
+ }
552
+
553
+ /**
554
+ * Reads the raw webhook event log, newest first — every event Paystack
555
+ * has sent this component, whether or not it changed local state.
556
+ * Handy for an audit trail or a live "what just happened" console.
557
+ */
558
+ async listRecentEvents(ctx: RunQueryCtx, args?: { limit?: number }) {
559
+ return await ctx.runQuery(this.component.lib.listRecentEvents, args ?? {});
560
+ }
561
+
562
+ /** Aggregate row counts — see {@link ComponentApi}'s `lib.getStats`. */
563
+ async getStats(ctx: RunQueryCtx) {
564
+ return await ctx.runQuery(this.component.lib.getStats, {});
565
+ }
566
+ }
567
+
568
+ type RunQueryCtx = {
569
+ runQuery: GenericActionCtx<GenericDataModel>["runQuery"];
570
+ };
@@ -0,0 +1,26 @@
1
+ /// <reference types="vite/client" />
2
+ import { test } from "vitest";
3
+ import { convexTest } from "convex-test";
4
+ export const modules = import.meta.glob("./**/*.*s");
5
+
6
+ import {
7
+ defineSchema,
8
+ type GenericSchema,
9
+ type SchemaDefinition,
10
+ } from "convex/server";
11
+ import { type ComponentApi } from "../component/_generated/component.js";
12
+ import { componentsGeneric } from "convex/server";
13
+ import { register } from "../test.js";
14
+
15
+ export function initConvexTest<
16
+ Schema extends SchemaDefinition<GenericSchema, boolean>,
17
+ >(schema?: Schema) {
18
+ const t = convexTest(schema ?? defineSchema({}), modules);
19
+ register(t);
20
+ return t;
21
+ }
22
+ export const components = componentsGeneric() as unknown as {
23
+ convexPaystack: ComponentApi;
24
+ };
25
+
26
+ test("setup", () => {});
@@ -0,0 +1,50 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Generated `api` utility.
4
+ *
5
+ * THIS CODE IS AUTOMATICALLY GENERATED.
6
+ *
7
+ * To regenerate, run `npx convex dev`.
8
+ * @module
9
+ */
10
+
11
+ import type * as lib from "../lib.js";
12
+
13
+ import type {
14
+ ApiFromModules,
15
+ FilterApi,
16
+ FunctionReference,
17
+ } from "convex/server";
18
+ import { anyApi, componentsGeneric } from "convex/server";
19
+
20
+ const fullApi: ApiFromModules<{
21
+ lib: typeof lib;
22
+ }> = anyApi as any;
23
+
24
+ /**
25
+ * A utility for referencing Convex functions in your app's public API.
26
+ *
27
+ * Usage:
28
+ * ```js
29
+ * const myFunctionReference = api.myModule.myFunction;
30
+ * ```
31
+ */
32
+ export const api: FilterApi<
33
+ typeof fullApi,
34
+ FunctionReference<any, "public">
35
+ > = anyApi as any;
36
+
37
+ /**
38
+ * A utility for referencing Convex functions in your app's internal API.
39
+ *
40
+ * Usage:
41
+ * ```js
42
+ * const myFunctionReference = internal.myModule.myFunction;
43
+ * ```
44
+ */
45
+ export const internal: FilterApi<
46
+ typeof fullApi,
47
+ FunctionReference<any, "internal">
48
+ > = anyApi as any;
49
+
50
+ export const components = componentsGeneric() as unknown as {};