naystack 1.1.17 → 1.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.
package/README.md CHANGED
@@ -43,6 +43,8 @@ import {
43
43
 
44
44
  Setup email-based authentication with JWT tokens and optional Turnstile captcha verification.
45
45
 
46
+ **Basic Example:**
47
+
46
48
  ```typescript
47
49
  const emailAuth = getEmailAuthRoutes({
48
50
  getUser: async (email: string) => { /* fetch user by email */ },
@@ -59,6 +61,58 @@ const emailAuth = getEmailAuthRoutes({
59
61
  export const { GET, POST, PUT, DELETE, getUserIdFromRequest } = emailAuth;
60
62
  ```
61
63
 
64
+ **Real-World Example with Drizzle ORM:**
65
+
66
+ ```typescript
67
+ import { db } from "@/lib/db";
68
+ import { UserTable, WebPushSubscriptionTable } from "@/lib/db/schema";
69
+ import { eq } from "drizzle-orm";
70
+ import { getEmailAuthRoutes } from "naystack/auth";
71
+ import { waitUntil } from "@vercel/functions";
72
+
73
+ export const { GET, POST, PUT, DELETE, getUserIdFromRequest } =
74
+ getEmailAuthRoutes({
75
+ // Fetch user by email using Drizzle
76
+ getUser: (email: string) =>
77
+ db.query.UserTable.findFirst({
78
+ where: eq(UserTable.email, email)
79
+ }),
80
+
81
+ // Create new user
82
+ createUser: async (user) => {
83
+ const [newUser] = await db
84
+ .insert(UserTable)
85
+ .values(user)
86
+ .returning();
87
+ return newUser;
88
+ },
89
+
90
+ signingKey: process.env.SIGNING_KEY!,
91
+ refreshKey: process.env.REFRESH_KEY!,
92
+ turnstileKey: process.env.TURNSTILE_KEY!,
93
+
94
+ // Send welcome email on signup
95
+ onSignUp: ({ id, email }) =>
96
+ waitUntil(
97
+ (async () => {
98
+ const link = await getVerificationLink(id);
99
+ if (link && email) {
100
+ await sendTemplateEmail(email, "WelcomeUser", {
101
+ verifyLink: link,
102
+ });
103
+ }
104
+ })(),
105
+ ),
106
+
107
+ // Clean up push subscriptions on logout
108
+ onLogout: async (endpoint: string) => {
109
+ await db
110
+ .delete(WebPushSubscriptionTable)
111
+ .where(eq(WebPushSubscriptionTable.endpoint, endpoint));
112
+ },
113
+ });
114
+ ```
115
+
62
116
  #### Options
63
117
 
64
118
  | Option | Type | Required | Description |
@@ -93,20 +147,54 @@ interface UserOutput {
93
147
 
94
148
  ### Google Authentication
95
149
 
150
+ **Real-World Example:**
151
+
96
152
  ```typescript
97
- const googleAuth = initGoogleAuth({
153
+ import { db } from "@/lib/db";
154
+ import { UserTable } from "@/lib/db/schema";
155
+ import { eq } from "drizzle-orm";
156
+ import { initGoogleAuth } from "naystack/auth";
157
+
158
+ export const { GET } = initGoogleAuth({
98
159
  clientId: process.env.GOOGLE_CLIENT_ID!,
99
160
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
100
- authRoute: "/api/auth/google",
161
+ authRoute: `${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/google`,
101
162
  successRedirectURL: "/dashboard",
102
163
  errorRedirectURL: "/login?error=google",
103
- refreshKey: process.env.JWT_REFRESH_KEY!,
104
- getUserIdFromEmail: async (userInfo) => {
105
- /* return user ID or null */
164
+ refreshKey: process.env.REFRESH_KEY!,
165
+
166
+ // Find existing user or create new one
167
+ getUserIdFromEmail: async ({ email, name }) => {
168
+ if (!email) return null;
169
+
170
+ // Update existing user's email verification status
171
+ const [existingUser] = await db
172
+ .update(UserTable)
173
+ .set({ emailVerified: true })
174
+ .where(eq(UserTable.email, email))
175
+ .returning({ id: UserTable.id });
176
+
177
+ if (existingUser) {
178
+ return existingUser.id;
179
+ }
180
+
181
+ // Create new user if doesn't exist
182
+ if (name && email) {
183
+ const [newUser] = await db
184
+ .insert(UserTable)
185
+ .values({
186
+ email,
187
+ name,
188
+ emailVerified: true,
189
+ })
190
+ .returning({ id: UserTable.id });
191
+
192
+ return newUser?.id || null;
193
+ }
194
+
195
+ return null;
106
196
  },
107
197
  });
108
-
109
- export const { GET } = googleAuth;
110
198
  ```
111
199
 
112
200
  #### Options
@@ -125,20 +213,65 @@ export const { GET } = googleAuth;
125
213
 
126
214
  ### Instagram Authentication
127
215
 
216
+ **Real-World Example:**
217
+
128
218
  ```typescript
129
- const instagramAuth = initInstagramAuth({
130
- clientId: process.env.INSTAGRAM_CLIENT_ID!,
219
+ import { db } from "@/lib/db";
220
+ import { InstagramDetails, UserTable } from "@/lib/db/schema";
221
+ import { and, eq, ne } from "drizzle-orm";
222
+ import { initInstagramAuth } from "naystack/auth";
223
+
224
+ export const { GET, getRefreshedAccessToken } = initInstagramAuth({
225
+ clientId: process.env.NEXT_PUBLIC_INSTAGRAM_CLIENT_ID!,
131
226
  clientSecret: process.env.INSTAGRAM_CLIENT_SECRET!,
132
- authRoute: "/api/auth/instagram",
133
- successRedirectURL: "/dashboard",
134
- errorRedirectURL: "/login?error=instagram",
135
- refreshKey: process.env.JWT_REFRESH_KEY!,
136
- onUser: async (data, userId, accessToken) => {
137
- /* handle Instagram user */
227
+ authRoute: `${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/instagram`,
228
+ successRedirectURL: "/profile",
229
+ errorRedirectURL: "/signup",
230
+ refreshKey: process.env.REFRESH_KEY!,
231
+
232
+ // Verify and link Instagram account
233
+ onUser: async (instagramData, userId, accessToken) => {
234
+ if (!userId) return "You are not logged in";
235
+
236
+ const user = await db.query.UserTable.findFirst({
237
+ where: eq(UserTable.id, userId),
238
+ });
239
+
240
+ if (!user?.instagramDetails) {
241
+ return "Please connect Instagram first";
242
+ }
243
+
244
+ // Update Instagram verification status
245
+ const [updated] = await db
246
+ .update(InstagramDetails)
247
+ .set({
248
+ isVerified: true,
249
+ accessToken: accessToken
250
+ })
251
+ .where(
252
+ and(
253
+ eq(InstagramDetails.id, user.instagramDetails),
254
+ eq(InstagramDetails.username, instagramData.username),
255
+ ),
256
+ )
257
+ .returning({ username: InstagramDetails.username });
258
+
259
+ if (!updated) {
260
+ return "Please login with the same username as the connected account";
261
+ }
262
+
263
+ // Disconnect from other users if linked
264
+ await db
265
+ .update(UserTable)
266
+ .set({ instagramDetails: null })
267
+ .where(
268
+ and(
269
+ ne(UserTable.id, userId),
270
+ eq(UserTable.instagramDetails, user.instagramDetails),
271
+ ),
272
+ );
138
273
  },
139
274
  });
140
-
141
- export const { GET, getRefreshedAccessToken } = instagramAuth;
142
275
  ```
143
276
 
144
277
  #### Options
@@ -171,6 +304,8 @@ import type { Context, AuthorizedContext } from "naystack/graphql";
171
304
 
172
305
  ### Initialize GraphQL Server
173
306
 
307
+ **Basic Example:**
308
+
174
309
  ```typescript
175
310
  const { GET, POST } = await initGraphQLServer({
176
311
  resolvers: [UserResolver, PostResolver],
@@ -185,6 +320,43 @@ const { GET, POST } = await initGraphQLServer({
185
320
  export { GET, POST };
186
321
  ```
187
322
 
323
+ **Real-World Example with Advanced Context:**
324
+
325
+ ```typescript
326
+ import { initGraphQLServer } from "naystack/graphql";
327
+ import { getUserIdFromRequest } from "@/api/(auth)/email/setup";
328
+ import { authChecker } from "@/lib/auth/context";
329
+
330
+ export const { GET, POST } = await initGraphQLServer({
331
+ resolvers: [
332
+ UserResolver,
333
+ PostResolver,
334
+ ApplicationResolver,
335
+ ChatResolver,
336
+ // ... more resolvers
337
+ ],
338
+ authChecker,
339
+ context: async (req) => {
340
+ const res = getUserIdFromRequest(req);
341
+ if (!res) return { userId: null };
342
+
343
+ // Handle refresh token user ID
344
+ if (res.refreshUserID) {
345
+ const isMobile = req.headers.get("x-platform-is-mobile");
346
+ if (isMobile) return { userId: null };
347
+ return { userId: res.refreshUserID, onlyQuery: true };
348
+ }
349
+
350
+ // Handle access token user ID
351
+ if (res.accessUserId) {
352
+ return { userId: res.accessUserId };
353
+ }
354
+
355
+ return { userId: null };
356
+ },
357
+ });
358
+ ```
359
+
188
360
  #### Options
189
361
 
190
362
  | Option | Type | Required | Description |
@@ -196,6 +368,8 @@ export { GET, POST };
196
368
 
197
369
  ### Error Handling
198
370
 
371
+ **Basic Usage:**
372
+
199
373
  ```typescript
200
374
  import { GQLError } from "naystack/graphql";
201
375
 
@@ -206,10 +380,64 @@ throw GQLError(400); // "Please provide all required inputs"
206
380
  throw GQLError(); // "Server Error" (500)
207
381
  ```
208
382
 
383
+ **Real-World Example in Resolvers:**
384
+
385
+ ```typescript
386
+ import { GQLError } from "naystack/graphql";
387
+ import { db } from "@/lib/db";
388
+ import { UserTable, PostingTable } from "@/lib/db/schema";
389
+ import { eq } from "drizzle-orm";
390
+
391
+ export async function createPosting(
392
+ ctx: Context,
393
+ input: NewPostingInput,
394
+ ): Promise<number | null> {
395
+ // Authentication check
396
+ if (!ctx.userId) {
397
+ throw GQLError(400, "Please login to create posting");
398
+ }
399
+
400
+ const user = await db.query.UserTable.findFirst({
401
+ where: eq(UserTable.id, ctx.userId),
402
+ });
403
+
404
+ // Authorization check
405
+ if (!user || user.role === "CREATOR") {
406
+ throw GQLError(400, "Only onboarded users can create postings");
407
+ }
408
+
409
+ // Validation check
410
+ if (!user.emailVerified) {
411
+ throw GQLError(400, "Please verify email to create posting");
412
+ }
413
+
414
+ // Rate limiting check
415
+ const yesterday = new Date();
416
+ yesterday.setDate(yesterday.getDate() - 1);
417
+ const recentPostings = await db.query.PostingTable.findMany({
418
+ where: and(
419
+ eq(PostingTable.userId, ctx.userId),
420
+ gte(PostingTable.createdAt, yesterday),
421
+ ),
422
+ });
423
+
424
+ if (recentPostings.length >= MAXIMUM_POSTINGS_DAY) {
425
+ throw GQLError(
426
+ 400,
427
+ `Only ${MAXIMUM_POSTINGS_DAY} allowed in 24 hours. Try again later.`,
428
+ );
429
+ }
430
+
431
+ // Create posting...
432
+ }
433
+ ```
434
+
209
435
  ### Query & Field Helpers
210
436
 
211
437
  Build resolvers functionally using `query`, `field`, `QueryLibrary`, and `FieldLibrary`:
212
438
 
439
+ **Basic Example:**
440
+
213
441
  ```typescript
214
442
  import { query, QueryLibrary, field, FieldLibrary } from "naystack/graphql";
215
443
 
@@ -255,6 +483,62 @@ const fields = {
255
483
  const UserFieldResolver = FieldLibrary(User, fields);
256
484
  ```
257
485
 
486
+ **Real-World Example:**
487
+
488
+ ```typescript
489
+ import { query, QueryLibrary, field, FieldLibrary } from "naystack/graphql";
490
+ import { db } from "@/lib/db";
491
+ import { NotificationTable } from "@/lib/db/schema";
492
+ import { eq, lte } from "drizzle-orm";
493
+ import { waitUntil } from "@vercel/functions";
494
+
495
+ // Query with side effects
496
+ export const getNotifications = query(
497
+ async (ctx) => {
498
+ if (!ctx.userId) return [];
499
+
500
+ // Mark all as read
501
+ const notifications = await db
502
+ .update(NotificationTable)
503
+ .set({ read: true })
504
+ .where(eq(NotificationTable.user, ctx.userId))
505
+ .returning();
506
+
507
+ // Clean up old notifications (async)
508
+ const weekBefore = new Date();
509
+ weekBefore.setDate(weekBefore.getDate() - 7);
510
+ waitUntil(
511
+ db
512
+ .delete(NotificationTable)
513
+ .where(lte(NotificationTable.createdAt, weekBefore)),
514
+ );
515
+
516
+ return notifications.sort((a, b) => a.id - b.id);
517
+ },
518
+ {
519
+ output: [NotificationGQL!],
520
+ },
521
+ );
522
+
523
+ // Create resolver from queries
524
+ export const NotificationResolver = QueryLibrary({
525
+ getNotifications,
526
+ getUnreadNotifications,
527
+ });
528
+
529
+ // Field resolver example
530
+ export const UserFields = FieldLibrary(UserGQL, {
531
+ isOnboarded: field(
532
+ async (user) => {
533
+ return getIsOnboarded(user);
534
+ },
535
+ {
536
+ output: Boolean,
537
+ }
538
+ ),
539
+ });
540
+ ```
541
+
258
542
  ### Types
259
543
 
260
544
  ```typescript
@@ -285,7 +569,9 @@ import {
285
569
 
286
570
  #### `useVisibility`
287
571
 
288
- Observe element visibility using Intersection Observer.
572
+ Observe element visibility using Intersection Observer. Perfect for infinite scroll and lazy loading.
573
+
574
+ **Basic Example:**
289
575
 
290
576
  ```typescript
291
577
  function Component() {
@@ -297,9 +583,38 @@ function Component() {
297
583
  }
298
584
  ```
299
585
 
586
+ **Real-World Example - Infinite Scroll:**
587
+
588
+ ```typescript
589
+ "use client";
590
+
591
+ import { useVisibility } from "naystack/client";
592
+ import { useRef } from "react";
593
+
594
+ export default function PostingCard({
595
+ posting,
596
+ fetchMore
597
+ }: {
598
+ posting: Posting;
599
+ fetchMore?: () => void;
600
+ }) {
601
+ // Trigger fetchMore when card becomes visible
602
+ const mainRef = useVisibility(fetchMore);
603
+
604
+ return (
605
+ <div ref={mainRef} className="posting-card">
606
+ <h3>{posting.title}</h3>
607
+ <p>{posting.description}</p>
608
+ </div>
609
+ );
610
+ }
611
+ ```
612
+
300
613
  #### `useBreakpoint`
301
614
 
302
- React to media query changes.
615
+ React to media query changes. Useful for responsive layouts and conditional rendering.
616
+
617
+ **Basic Example:**
303
618
 
304
619
  ```typescript
305
620
  function Component() {
@@ -309,6 +624,35 @@ function Component() {
309
624
  }
310
625
  ```
311
626
 
627
+ **Real-World Example - Responsive Layout:**
628
+
629
+ ```typescript
630
+ "use client";
631
+
632
+ import { useBreakpoint } from "naystack/client";
633
+
634
+ export default function LayoutWrapper({
635
+ children
636
+ }: {
637
+ children: React.ReactNode;
638
+ }) {
639
+ const isLarge = useBreakpoint("(min-width: 1024px)");
640
+
641
+ return (
642
+ <div
643
+ style={{
644
+ height: isLarge
645
+ ? "calc(100svh - 80px)"
646
+ : "calc(100svh - 55px)",
647
+ }}
648
+ className="flex flex-col"
649
+ >
650
+ {children}
651
+ </div>
652
+ );
653
+ }
654
+ ```
655
+
312
656
  ### SEO Helper
313
657
 
314
658
  ```typescript
@@ -329,14 +673,28 @@ export const metadata = getSEO(
329
673
 
330
674
  ### Instagram Authorization URL
331
675
 
676
+ Generate Instagram OAuth authorization URLs for client-side redirects.
677
+
678
+ **Example:**
679
+
332
680
  ```typescript
333
- const getInstagramAuthURL = getInstagramAuthorizationURLSetup(
334
- process.env.INSTAGRAM_CLIENT_ID!,
335
- "https://myapp.com/api/auth/instagram"
681
+ import { getInstagramAuthorizationURLSetup } from "naystack/client";
682
+
683
+ // Setup once
684
+ const getInstagramAuthorizationURL = getInstagramAuthorizationURLSetup(
685
+ process.env.NEXT_PUBLIC_INSTAGRAM_CLIENT_ID!,
686
+ `${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/instagram`,
336
687
  );
337
688
 
338
- // Usage
339
- const authURL = getInstagramAuthURL(userToken);
689
+ // Usage in component
690
+ function ConnectInstagramButton() {
691
+ const handleConnect = () => {
692
+ const authURL = getInstagramAuthorizationURL(userToken);
693
+ window.location.href = authURL;
694
+ };
695
+
696
+ return <button onClick={handleConnect}>Connect Instagram</button>;
697
+ }
340
698
  ```
341
699
 
342
700
  ### Image Upload Client
@@ -363,6 +721,8 @@ import { setupFileUpload } from "naystack/file";
363
721
 
364
722
  ### Setup File Upload
365
723
 
724
+ **Basic Example:**
725
+
366
726
  ```typescript
367
727
  const fileUpload = setupFileUpload({
368
728
  refreshKey: process.env.JWT_REFRESH_KEY!,
@@ -388,6 +748,113 @@ const { getUploadFileURL, uploadImage, deleteImage, getFileURL, uploadFile } =
388
748
  fileUpload;
389
749
  ```
390
750
 
751
+ **Real-World Example with Multiple File Types:**
752
+
753
+ ```typescript
754
+ import { setupFileUpload } from "naystack/file";
755
+ import { db } from "@/lib/db";
756
+ import { PortfolioTable, UserTable } from "@/lib/db/schema";
757
+ import { and, eq } from "drizzle-orm";
758
+ import { waitUntil } from "@vercel/functions";
759
+
760
+ export const { deleteImage, getUploadFileURL, getFileURL, uploadImage, PUT } =
761
+ setupFileUpload({
762
+ region: process.env.SITE_AWS_REGION!,
763
+ refreshKey: process.env.REFRESH_KEY!,
764
+ awsSecret: process.env.SITE_AWS_SECRET_ACCESS_KEY!,
765
+ awsKey: process.env.SITE_AWS_ACCESS_KEY_ID!,
766
+ signingKey: process.env.SIGNING_KEY!,
767
+ bucket: process.env.SITE_AWS_BUCKET!,
768
+
769
+ processFile: async ({ url, userId, data, type }) => {
770
+ switch (type) {
771
+ case "PORTFOLIO":
772
+ // Handle portfolio image upload
773
+ waitUntil(
774
+ (async () => {
775
+ if (!url) return {};
776
+ const id = (data as { id?: number }).id;
777
+
778
+ if (id) {
779
+ // Update existing portfolio
780
+ const [existing] = await db
781
+ .select()
782
+ .from(PortfolioTable)
783
+ .where(
784
+ and(
785
+ eq(PortfolioTable.id, id),
786
+ eq(PortfolioTable.user, userId),
787
+ ),
788
+ );
789
+
790
+ if (!existing) {
791
+ return { deleteURL: url };
792
+ }
793
+
794
+ const oldURL = existing.imageURL;
795
+ await db
796
+ .update(PortfolioTable)
797
+ .set({ imageURL: url })
798
+ .where(
799
+ and(
800
+ eq(PortfolioTable.id, id),
801
+ eq(PortfolioTable.user, userId),
802
+ ),
803
+ );
804
+
805
+ return {
806
+ deleteURL: oldURL,
807
+ data: { id },
808
+ };
809
+ } else {
810
+ // Create new portfolio
811
+ const [portfolio] = await db
812
+ .insert(PortfolioTable)
813
+ .values({
814
+ user: userId,
815
+ imageURL: url,
816
+ caption: "",
817
+ link: "",
818
+ })
819
+ .returning({ id: PortfolioTable.id });
820
+
821
+ return { data: { id: portfolio?.id } };
822
+ }
823
+ })(),
824
+ );
825
+ break;
826
+
827
+ case "PROFILE_PICTURE":
828
+ // Handle profile picture upload
829
+ waitUntil(
830
+ (async () => {
831
+ const user = await db.query.UserTable.findFirst({
832
+ where: eq(UserTable.id, userId),
833
+ });
834
+
835
+ if (!user && url) {
836
+ return { deleteURL: url };
837
+ }
838
+
839
+ const oldPhoto = user?.photo;
840
+ await db
841
+ .update(UserTable)
842
+ .set({ photo: url })
843
+ .where(eq(UserTable.id, userId));
844
+
845
+ return {
846
+ deleteURL: oldPhoto || undefined,
847
+ };
848
+ })(),
849
+ );
850
+ break;
851
+ }
852
+
853
+ return {};
854
+ },
855
+ });
856
+ ```
857
+
391
858
  #### Options
392
859
 
393
860
  | Option | Type | Required | Description |
@@ -452,6 +919,8 @@ const user = await getInstagramUser(accessToken, "me", [
452
919
 
453
920
  #### Get Media
454
921
 
922
+ **Basic Usage:**
923
+
455
924
  ```typescript
456
925
  const media = await getInstagramMedia(accessToken);
457
926
  const media = await getInstagramMedia(
@@ -461,6 +930,60 @@ const media = await getInstagramMedia(
461
930
  );
462
931
  ```
463
932
 
933
+ **Real-World Example - Fetching Media with Custom Fields:**
934
+
935
+ ```typescript
936
+ import { getInstagramMedia } from "naystack/socials";
937
+
938
+ export async function fetchInstagramGraphMedia(
939
+ accessToken: string,
940
+ followers: number,
941
+ userId: number,
942
+ ) {
943
+ const fetchReq = await getInstagramMedia<{
944
+ thumbnail_url?: string;
945
+ id: string;
946
+ like_count?: number;
947
+ comments_count: number;
948
+ permalink: string;
949
+ caption: string;
950
+ media_url?: string;
951
+ media_type?: string;
952
+ timestamp: string;
953
+ }>(accessToken, [
954
+ "id",
955
+ "thumbnail_url",
956
+ "media_url",
957
+ "like_count",
958
+ "comments_count",
959
+ "media_type",
960
+ "permalink",
961
+ "caption",
962
+ "timestamp",
963
+ ]);
964
+
965
+ if (fetchReq?.data) {
966
+ return fetchReq.data.map((media) => ({
967
+ isVideo: media.media_type === "VIDEO",
968
+ comments: media.comments_count || -1,
969
+ likes: media.like_count || 0,
970
+ link: media.permalink,
971
+ thumbnail: media.thumbnail_url || media.media_url,
972
+ mediaURL: media.media_url,
973
+ timestamp: media.timestamp,
974
+ caption: media.caption,
975
+ appID: media.id,
976
+ user: userId,
977
+ er: calculateEngagementRate(
978
+ followers,
979
+ media.like_count || 0,
980
+ media.comments_count || -1,
981
+ ),
982
+ }));
983
+ }
984
+ }
985
+ ```
986
+
464
987
  #### Conversations
465
988
 
466
989
  ```typescript
@@ -495,6 +1018,8 @@ const result = await sendInstagramMessage(accessToken, recipientId, "Hello!");
495
1018
 
496
1019
  #### Webhook
497
1020
 
1021
+ **Basic Example:**
1022
+
498
1023
  ```typescript
499
1024
  const instagramWebhook = setupInstagramWebhook({
500
1025
  secret: process.env.INSTAGRAM_WEBHOOK_SECRET!,
@@ -506,6 +1031,59 @@ const instagramWebhook = setupInstagramWebhook({
506
1031
  export const { GET, POST } = instagramWebhook;
507
1032
  ```
508
1033
 
1034
+ **Real-World Example - Auto-Reply Bot:**
1035
+
1036
+ ```typescript
1037
+ import {
1038
+ getInstagramConversationByUser,
1039
+ sendInstagramMessage,
1040
+ setupInstagramWebhook,
1041
+ } from "naystack/socials";
1042
+
1043
+ export const { GET, POST } = setupInstagramWebhook({
1044
+ secret: process.env.REFRESH_KEY!,
1045
+ callback: async (
1046
+ type,
1047
+ value: {
1048
+ sender: { id: string };
1049
+ message: { text: string };
1050
+ recipient: { id: string };
1051
+ },
1052
+ ) => {
1053
+ if (
1054
+ type === "messaging" &&
1055
+ value.message.text &&
1056
+ value.sender.id !== "YOUR_PAGE_ID" &&
1057
+ value.recipient.id === "YOUR_PAGE_ID"
1058
+ ) {
1059
+ // Check if message is recent (within 24 hours)
1060
+ const conversation = await getInstagramConversationByUser(
1061
+ process.env.INSTAGRAM_ACCESS_TOKEN!,
1062
+ value.sender.id,
1063
+ );
1064
+
1065
+ const lastMessage = conversation?.messages?.data[1]?.created_time;
1066
+ if (lastMessage) {
1067
+ const lastMessageDate = new Date(lastMessage);
1068
+ if (lastMessageDate.getTime() > Date.now() - 1000 * 60 * 60 * 24) {
1069
+ return; // Already replied recently
1070
+ }
1071
+ }
1072
+
1073
+ // Generate reply (using your AI/LLM service)
1074
+ const reply = await generateReply(value.message.text);
1075
+ if (reply && reply !== '""') {
1076
+ await sendInstagramMessage(
1077
+ process.env.INSTAGRAM_ACCESS_TOKEN!,
1078
+ value.sender.id,
1079
+ reply,
1080
+ );
1081
+ }
1082
+ }
1083
+ },
1084
+ });
1085
+ ```
1086
+
509
1087
  ---
510
1088
 
511
1089
  ### Threads API
@@ -547,6 +1125,8 @@ const firstPostId = await createThread(accessToken, [
547
1125
 
548
1126
  #### Webhook
549
1127
 
1128
+ **Basic Example:**
1129
+
550
1130
  ```typescript
551
1131
  const threadsWebhook = setupThreadsWebhook({
552
1132
  secret: process.env.THREADS_WEBHOOK_SECRET!,
@@ -559,6 +1139,144 @@ const threadsWebhook = setupThreadsWebhook({
559
1139
  export const { GET, POST } = threadsWebhook;
560
1140
  ```
561
1141
 
1142
+ **Real-World Example - Auto-Reply to Threads:**
1143
+
1144
+ ```typescript
1145
+ import {
1146
+ createThreadsPost,
1147
+ getThreadsReplies,
1148
+ setupThreadsWebhook,
1149
+ } from "naystack/socials";
1150
+ import { db } from "@/lib/db";
1151
+ import { SocialPostsTable } from "@/lib/db/schema";
1152
+ import { eq } from "drizzle-orm";
1153
+
1154
+ const REPLIES = [
1155
+ "Thanks for your interest! Check out campaign ${ID}",
1156
+ "We'd love to work with you! See campaign ${ID}",
1157
+ ];
1158
+
1159
+ export const { GET, POST } = setupThreadsWebhook({
1160
+ secret: process.env.REFRESH_KEY!,
1161
+ callback: async (
1162
+ field,
1163
+ value: {
1164
+ id: string;
1165
+ username: string;
1166
+ text: string;
1167
+ replied_to: { id: string };
1168
+ root_post: { id: string; username: string };
1169
+ },
1170
+ ) => {
1171
+ // Skip if replying to own post
1172
+ if (value.root_post.username === value.username) return true;
1173
+
1174
+ // Only reply to direct replies to root post
1175
+ if (value.root_post.id !== value.replied_to.id) return true;
1176
+
1177
+ // Check if already replied
1178
+ const replies = await getThreadsReplies(
1179
+ process.env.THREADS_ACCESS_TOKEN!,
1180
+ value.id,
1181
+ );
1182
+ if (replies?.length ?? 0 > 0) return true;
1183
+
1184
+ // Find associated campaign
1185
+ const [post] = await db
1186
+ .select()
1187
+ .from(SocialPostsTable)
1188
+ .where(eq(SocialPostsTable.postID, value.root_post.id));
1189
+
1190
+ if (!post) return true;
1191
+
1192
+ // Generate reply message
1193
+ const message = REPLIES[
1194
+ Math.floor(Math.random() * REPLIES.length)
1195
+ ]?.replace("${ID}", post.campaignID.toString());
1196
+
1197
+ if (!message) return true;
1198
+
1199
+ // Send reply
1200
+ const res = await createThreadsPost(
1201
+ process.env.THREADS_ACCESS_TOKEN!,
1202
+ message,
1203
+ value.id,
1204
+ );
1205
+
1206
+ return !!res;
1207
+ },
1208
+ });
1209
+ ```
1210
+
1211
+ ---
1212
+
1213
+ ## Best Practices & Common Patterns
1214
+
1215
+ ### Authentication Flow
1216
+
1217
+ 1. **Email Auth with Database Integration:**
1218
+ - Use Drizzle ORM queries in `getUser` and `createUser`
1219
+ - Send verification emails in `onSignUp` callback
1220
+ - Clean up session data in `onLogout`
1221
+
1222
+ 2. **OAuth Integration:**
1223
+ - Always verify email/username matches existing accounts
1224
+ - Create new users only when necessary
1225
+ - Update verification status for existing users
1226
+
1227
+ ### GraphQL Resolver Patterns
1228
+
1229
+ 1. **Error Handling:**
1230
+ - Use `GQLError` for all error cases
1231
+ - Provide clear, user-friendly error messages
1232
+ - Use appropriate HTTP status codes (400, 403, 404, 500)
1233
+
1234
+ 2. **Authorization:**
1235
+ - Check authentication first (`if (!ctx.userId)`)
1236
+ - Verify permissions before operations
1237
+ - Use `authChecker` for role-based access control
1238
+
1239
+ 3. **Query Library Pattern:**
1240
+ - Use `query()` helper for simple queries/mutations
1241
+ - Use `QueryLibrary()` to combine multiple queries
1242
+ - Use `field()` and `FieldLibrary()` for computed fields
1243
+
1244
+ ### File Upload Patterns
1245
+
1246
+ 1. **Multiple File Types:**
1247
+ - Use `type` parameter to distinguish upload types
1248
+ - Handle each type in `processFile` callback
1249
+ - Return `deleteURL` for old files to clean up
1250
+
1251
+ 2. **Async Processing:**
1252
+ - Use `waitUntil()` for non-blocking operations
1253
+ - Process file metadata in background
1254
+ - Update database after successful upload
1255
+
1256
+ ### Webhook Patterns
1257
+
1258
+ 1. **Instagram Webhooks:**
1259
+ - Verify sender/recipient IDs
1260
+ - Check message timestamps to avoid duplicates
1261
+ - Use async operations for replies
1262
+
1263
+ 2. **Threads Webhooks:**
1264
+ - Filter by reply structure (root_post vs replied_to)
1265
+ - Check for existing replies before responding
1266
+ - Return `true`/`false` to indicate success/failure
1267
+
1268
+ ### Client-Side Patterns
1269
+
1270
+ 1. **Infinite Scroll:**
1271
+ - Use `useVisibility` hook with `fetchMore` callback
1272
+ - Attach ref to last item in list
1273
+ - Handle loading states
1274
+
1275
+ 2. **Responsive Design:**
1276
+ - Use `useBreakpoint` for conditional rendering
1277
+ - Adjust layout based on screen size
1278
+ - Combine with CSS for optimal UX
1279
+
562
1280
  ---
563
1281
 
564
1282
  ## Environment Variables