@superblocksteam/sdk-api 0.0.13 → 0.0.14

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/README.md +35 -610
  2. package/dist/errors.d.ts.map +1 -1
  3. package/dist/errors.js +2 -1
  4. package/dist/errors.js.map +1 -1
  5. package/dist/integrations/base/decode-worker-binary-response.d.ts.map +1 -1
  6. package/dist/integrations/base/decode-worker-binary-response.js +22 -1
  7. package/dist/integrations/base/decode-worker-binary-response.js.map +1 -1
  8. package/dist/integrations/base/decode-worker-binary-response.test.js +15 -2
  9. package/dist/integrations/base/decode-worker-binary-response.test.js.map +1 -1
  10. package/dist/integrations/base/index.d.ts +1 -1
  11. package/dist/integrations/base/index.d.ts.map +1 -1
  12. package/dist/integrations/base/rest-api-integration-client.d.ts +2 -2
  13. package/dist/integrations/base/rest-api-integration-client.d.ts.map +1 -1
  14. package/dist/integrations/base/rest-api-integration-client.js +3 -1
  15. package/dist/integrations/base/rest-api-integration-client.js.map +1 -1
  16. package/dist/integrations/base/types.d.ts +23 -6
  17. package/dist/integrations/base/types.d.ts.map +1 -1
  18. package/dist/integrations/restapiintegration/client.test.js +42 -32
  19. package/dist/integrations/restapiintegration/client.test.js.map +1 -1
  20. package/package.json +1 -1
  21. package/src/errors.ts +4 -1
  22. package/src/integrations/base/decode-worker-binary-response.test.ts +25 -5
  23. package/src/integrations/base/decode-worker-binary-response.ts +26 -1
  24. package/src/integrations/base/index.ts +1 -0
  25. package/src/integrations/base/rest-api-integration-client.ts +6 -3
  26. package/src/integrations/base/types.ts +25 -6
  27. package/src/integrations/lakebase/README.md +1 -3
  28. package/src/integrations/postgres/README.md +1 -3
  29. package/src/integrations/restapiintegration/client.test.ts +51 -38
  30. package/src/integrations/restapiintegration/overlays/response-types-binary.md +4 -8
  31. package/src/integrations/snowflakepostgres/README.md +1 -3
package/README.md CHANGED
@@ -1,13 +1,11 @@
1
1
  # @superblocksteam/sdk-api
2
2
 
3
- TypeScript SDK for defining Superblocks APIs as simple, type-safe functions.
4
-
5
3
  ## Overview
6
4
 
7
5
  This SDK enables writing Superblocks APIs as TypeScript functions with:
8
6
 
9
7
  - **Zod schemas** for input/output validation
10
- - **Typed integration clients** for Postgres, Slack, OpenAI, Anthropic, Stripe, GitHub, Notion, and more
8
+ - **Typed integration clients** for Postgres, Slack, OpenAI, Anthropic, Stripe, and more
11
9
  - **Full type inference** for inputs, outputs, and integrations
12
10
  - **Upfront integration declarations** for type-safe access and pre-authentication
13
11
  - **Runtime schema validation** with detailed error information for debugging
@@ -329,35 +327,6 @@ Defines a TypeScript-based API with input/output validation. Returns a complete
329
327
  | `run` | `(ctx, input) => Promise<Output>` | **Required.** The implementation function |
330
328
  | `integrations` | `Record<string, IntegrationRef>` | Optional. Integration declarations for the API |
331
329
 
332
- ```typescript
333
- import { api, z } from "@superblocksteam/sdk-api";
334
-
335
- const myApi = api({
336
- // REQUIRED: name is used for identification in logs and debugging
337
- name: "GreetUser",
338
-
339
- // REQUIRED: Zod schema for input validation
340
- input: z.object({
341
- name: z.string(),
342
- count: z.number().int().positive(),
343
- }),
344
-
345
- // REQUIRED: Zod schema for output validation
346
- output: z.object({
347
- greeting: z.string(),
348
- items: z.array(z.string()),
349
- }),
350
-
351
- // REQUIRED: API implementation
352
- async run(ctx, { name, count }) {
353
- return {
354
- greeting: `Hello, ${name}!`,
355
- items: Array(count).fill("item"),
356
- };
357
- },
358
- });
359
- ```
360
-
361
330
  ### Context (`ctx`) and Input
362
331
 
363
332
  The `run` function receives two arguments:
@@ -377,52 +346,24 @@ async run(ctx, { userId, options }) {
377
346
  Access typed integration clients via the **name (key)** you declared in the API config: `ctx.integrations.<name>`. For example, if you declared `integrations: { db: postgres(...), notifier: slack(...) }`, use `ctx.integrations.db` and `ctx.integrations.notifier` in your `run` function. Integrations must be declared upfront to enable type-safe access and pre-authentication.
378
347
 
379
348
  ```typescript
380
- import { api, z, postgres, slack, openai, anthropic, stripe, github, notion } from "@superblocksteam/sdk-api";
349
+ import { api, postgres, slack, z } from "@superblocksteam/sdk-api";
381
350
 
382
351
  // Integration IDs from the integrations panel - store in constants
383
352
  const PROD_POSTGRES = "a1b2c3d4-5678-90ab-cdef-ghijklmnopqr";
384
353
  const OPS_SLACK = "b2c3d4e5-6789-01ab-cdef-ghijklmnopqr";
385
- const PROD_OPENAI = "c3d4e5f6-7890-12ab-cdef-ghijklmnopqr";
386
- const PROD_ANTHROPIC = "d4e5f6g7-8901-23ab-cdef-ghijklmnopqr";
387
- const PROD_STRIPE = "e5f6g7h8-9012-34ab-cdef-ghijklmnopqr";
388
- const PROD_GITHUB = "f6g7h8i9-0123-45ab-cdef-ghijklmnopqr";
389
- const PROD_NOTION = "g7h8i9j0-1234-56ab-cdef-ghijklmnopqr";
390
354
 
391
355
  export default api({
392
356
  name: "ProcessUserWorkflow",
393
-
394
- // Declare all integrations you'll use
395
357
  integrations: {
396
- // Database
397
358
  db: postgres(PROD_POSTGRES),
398
-
399
- // Messaging
400
359
  notifier: slack(OPS_SLACK),
401
-
402
- // AI/LLM
403
- ai: openai(PROD_OPENAI),
404
- claude: anthropic(PROD_ANTHROPIC),
405
-
406
- // Payment Processing
407
- payments: stripe(PROD_STRIPE),
408
-
409
- // Developer Tools
410
- repo: github(PROD_GITHUB),
411
-
412
- // Productivity
413
- wiki: notion(PROD_NOTION),
414
360
  },
415
-
416
361
  input: z.object({ userId: z.string() }),
417
362
  output: z.object({ success: z.boolean() }),
418
-
419
363
  async run(ctx, { userId }) {
420
- // Each integration is fully typed based on its declaration
421
364
  const users = await ctx.integrations.db.query(...);
422
365
  await ctx.integrations.notifier.apiRequest(...);
423
- const completion = await ctx.integrations.ai.apiRequest(...);
424
-
425
- return { success: true };
366
+ return { success: users.length > 0 };
426
367
  },
427
368
  });
428
369
  ```
@@ -516,83 +457,14 @@ interface ApiUser {
516
457
  }
517
458
  ```
518
459
 
519
- ##### Basic Usage
520
-
521
- ```typescript
522
- import { api, z, postgres } from "@superblocksteam/sdk-api";
523
-
524
- const PROD_POSTGRES = "a1b2c3d4-5678-90ab-cdef-ghijklmnopqr";
525
-
526
- export default api({
527
- name: "GetMyProfile",
528
- integrations: { db: postgres(PROD_POSTGRES) },
529
- input: z.object({}),
530
- output: z.object({
531
- email: z.string(),
532
- name: z.string(),
533
- }),
534
-
535
- async run(ctx) {
536
- // Access user identity securely from the JWT — no frontend input needed
537
- ctx.log.info("Fetching profile", { userId: ctx.user.userId });
538
-
539
- const rows = await ctx.integrations.db.query(
540
- "SELECT email, display_name FROM users WHERE id = $1",
541
- z.object({ email: z.string(), display_name: z.string() }),
542
- [ctx.user.userId],
543
- );
544
-
545
- if (rows.length === 0) {
546
- throw new Error("User not found");
547
- }
548
-
549
- return { email: rows[0].email, name: rows[0].display_name };
550
- },
551
- });
552
- ```
553
-
554
460
  ##### Role-Based Access Control with Groups
555
461
 
556
462
  Use `ctx.user.groups` to restrict API access based on the user's group memberships. Groups are populated from the user's identity provider (e.g., Okta, Azure AD) via the JWT.
557
463
 
558
464
  ```typescript
559
- import { api, z, postgres } from "@superblocksteam/sdk-api";
560
-
561
- const PROD_POSTGRES = "a1b2c3d4-5678-90ab-cdef-ghijklmnopqr";
562
-
563
- export default api({
564
- name: "GetAdminDashboard",
565
- integrations: { db: postgres(PROD_POSTGRES) },
566
- input: z.object({}),
567
- output: z.object({
568
- stats: z.object({
569
- totalUsers: z.number(),
570
- activeUsers: z.number(),
571
- }),
572
- }),
573
-
574
- async run(ctx) {
575
- // Check group membership for authorization
576
- if (!ctx.user.groups.includes("admin")) {
577
- throw new Error("Access denied: admin group membership required");
578
- }
579
-
580
- const stats = await ctx.integrations.db.query(
581
- `SELECT
582
- COUNT(*) as total_users,
583
- COUNT(*) FILTER (WHERE status = 'active') as active_users
584
- FROM users`,
585
- z.object({ total_users: z.number(), active_users: z.number() }),
586
- );
587
-
588
- return {
589
- stats: {
590
- totalUsers: stats[0].total_users,
591
- activeUsers: stats[0].active_users,
592
- },
593
- };
594
- },
595
- });
465
+ if (!ctx.user.groups.includes("admin")) {
466
+ throw new Error("Access denied: admin group membership required");
467
+ }
596
468
  ```
597
469
 
598
470
  ##### Custom Claims
@@ -606,120 +478,17 @@ export default api({
606
478
  - Always write defensive code that handles missing custom claims gracefully.
607
479
 
608
480
  ```typescript
609
- import { api, z, postgres } from "@superblocksteam/sdk-api";
610
-
611
- const PROD_POSTGRES = "a1b2c3d4-5678-90ab-cdef-ghijklmnopqr";
612
-
613
- export default api({
614
- name: "GetTenantData",
615
- integrations: { db: postgres(PROD_POSTGRES) },
616
- input: z.object({}),
617
- output: z.object({
618
- tenantName: z.string(),
619
- records: z.array(z.object({ id: z.string(), value: z.string() })),
620
- }),
621
-
622
- async run(ctx) {
623
- // Access custom claims from the JWT — handle missing claims defensively
624
- const tenantId = ctx.user.customClaims["tenant_id"];
625
- if (typeof tenantId !== "string") {
626
- throw new Error(
627
- "Missing tenant_id claim. Custom claims require enterprise SSO or embed tokens with SSO claims.",
628
- );
629
- }
630
-
631
- const department = ctx.user.customClaims["department"] as
632
- | string
633
- | undefined;
634
-
635
- ctx.log.info("Fetching tenant data", {
636
- userId: ctx.user.userId,
637
- tenantId,
638
- department: department ?? "unknown",
639
- });
640
-
641
- const records = await ctx.integrations.db.query(
642
- "SELECT id, value FROM tenant_data WHERE tenant_id = $1",
643
- z.object({ id: z.string(), value: z.string() }),
644
- [tenantId],
645
- );
646
-
647
- const tenantNames = await ctx.integrations.db.query(
648
- "SELECT name FROM tenants WHERE id = $1",
649
- z.object({ name: z.string() }),
650
- [tenantId],
651
- );
652
-
653
- return {
654
- tenantName: tenantNames[0]?.name ?? "Unknown Tenant",
655
- records,
656
- };
657
- },
658
- });
481
+ const tenantId = ctx.user.customClaims["tenant_id"];
482
+ if (typeof tenantId !== "string") {
483
+ throw new Error(
484
+ "Missing tenant_id claim. Custom claims require enterprise SSO or embed tokens with SSO claims.",
485
+ );
486
+ }
659
487
  ```
660
488
 
661
489
  ##### Combining User Context for Row-Level Security
662
490
 
663
- ```typescript
664
- import { api, z, postgres } from "@superblocksteam/sdk-api";
665
-
666
- const PROD_POSTGRES = "a1b2c3d4-5678-90ab-cdef-ghijklmnopqr";
667
-
668
- export default api({
669
- name: "GetMyOrders",
670
- integrations: { db: postgres(PROD_POSTGRES) },
671
- input: z.object({
672
- status: z.enum(["pending", "completed", "cancelled"]).optional(),
673
- }),
674
- output: z.object({
675
- orders: z.array(
676
- z.object({
677
- id: z.string(),
678
- total: z.number(),
679
- status: z.string(),
680
- createdAt: z.string(),
681
- }),
682
- ),
683
- }),
684
-
685
- async run(ctx, { status }) {
686
- // Use ctx.user for row-level security — users can only see their own orders
687
- if (!ctx.user.email) {
688
- throw new Error("User email is required to filter orders");
689
- }
690
- const params: unknown[] = [ctx.user.email];
691
- let query =
692
- "SELECT id, total, status, created_at FROM orders WHERE user_email = $1";
693
-
694
- if (status) {
695
- query += " AND status = $2";
696
- params.push(status);
697
- }
698
-
699
- query += " ORDER BY created_at DESC LIMIT 50";
700
-
701
- const orders = await ctx.integrations.db.query(
702
- query,
703
- z.object({
704
- id: z.string(),
705
- total: z.number(),
706
- status: z.string(),
707
- created_at: z.string(),
708
- }),
709
- params,
710
- );
711
-
712
- return {
713
- orders: orders.map((o) => ({
714
- id: o.id,
715
- total: o.total,
716
- status: o.status,
717
- createdAt: o.created_at,
718
- })),
719
- };
720
- },
721
- });
722
- ```
491
+ Use `ctx.user.userId` or `ctx.user.email` in parameterized queries to scope rows to the current user. Never accept user identity through API input.
723
492
 
724
493
  ## Integration Clients
725
494
 
@@ -775,67 +544,31 @@ const users = await ctx.integrations.db.query(
775
544
  );
776
545
  ```
777
546
 
778
- ### Streaming / SSE
779
-
780
- > **Note:** The SDK's `apiRequest()` method does **not** support streaming or Server-Sent Events (SSE) responses. All AI provider calls (OpenAI, Anthropic, Gemini, Groq, Mistral, Cohere, Perplexity, Fireworks) return complete responses — the SDK waits for the full response before returning. If you are building a chat interface that needs real-time token streaming, you will need to handle streaming at the frontend/UI layer rather than through the SDK.
781
-
782
547
  ## Generic API Requests
783
548
 
784
549
  All REST API-based integration clients support the `apiRequest()` method for making custom API calls. **Response schema validation is required** for type safety.
785
550
 
786
551
  ```typescript
787
- import { api, z, slack } from "@superblocksteam/sdk-api";
788
-
789
- // Integration ID from the integrations panel
790
- const OPS_SLACK = "b2c3d4e5-6789-01ab-cdef-ghijklmnopqr";
791
-
792
- export default api({
793
- name: "PostSlackMessage",
552
+ const CreateTicketBodySchema = z.object({
553
+ title: z.string(),
554
+ priority: z.enum(["low", "high"]),
555
+ });
556
+ const CreateTicketResponseSchema = z.object({
557
+ id: z.string(),
558
+ createdAt: z.string(),
559
+ });
794
560
 
795
- integrations: {
796
- notifier: slack(OPS_SLACK),
561
+ const ticket = await ctx.integrations.ticketing.apiRequest(
562
+ {
563
+ method: "POST",
564
+ path: "/tickets",
565
+ body: { title: "Deployment failed", priority: "high" },
797
566
  },
798
-
799
- input: z.object({}),
800
- output: z.object({ ts: z.string() }),
801
-
802
- async run(ctx) {
803
- // Define request and response schemas
804
- const PostMessageBodySchema = z.object({
805
- channel: z.string(),
806
- text: z.string(),
807
- blocks: z.array(z.any()).optional(),
808
- });
809
-
810
- const PostMessageResponseSchema = z.object({
811
- ts: z.string(),
812
- channel: z.string(),
813
- });
814
-
815
- // Response schema is REQUIRED
816
- const result = await ctx.integrations.notifier.apiRequest(
817
- {
818
- method: "POST",
819
- path: "/chat.postMessage",
820
- body: {
821
- channel: "#alerts",
822
- text: "Deployment completed!",
823
- },
824
- },
825
- {
826
- body: PostMessageBodySchema,
827
- response: PostMessageResponseSchema, // Required!
828
- },
829
- );
830
-
831
- if (!result.ok) {
832
- throw new Error(`Slack API error: ${result.error}`);
833
- }
834
-
835
- // result is fully typed as { ok: true } & PostMessageResponseSchema
836
- return { ts: result.ts };
567
+ {
568
+ body: CreateTicketBodySchema,
569
+ response: CreateTicketResponseSchema,
837
570
  },
838
- });
571
+ );
839
572
  ```
840
573
 
841
574
  ### ApiRequestOptions
@@ -891,33 +624,11 @@ await ctx.integrations.db.execute(
891
624
  [userId],
892
625
  { label: "Update last login" },
893
626
  );
894
-
895
- // REST API clients (apiRequest)
896
- const result = await ctx.integrations.slack.apiRequest(
897
- { method: "POST", path: "/chat.postMessage", body: { channel, text } },
898
- { body: BodySchema, response: ResponseSchema },
899
- { label: "Send alert to Slack" },
900
- );
901
-
902
- // Other clients (MongoDB, S3, etc.)
903
- const doc = await ctx.integrations.mongo.run(
904
- "users",
905
- "findOne",
906
- ResultSchema,
907
- { filter: { _id: userId } },
908
- { label: "Look up user by ID" },
909
- );
910
627
  ```
911
628
 
912
629
  ### When to Use
913
630
 
914
- Trace metadata is optional and purely for observability. Use it when:
915
-
916
- - You want labeled traces in the diagnostics panel for easier debugging
917
- - An API makes multiple calls to the same integration and you need to distinguish them
918
- - You want to document the intent of each integration call for your team
919
-
920
- Metadata has no effect on execution - it only appears in diagnostics when `includeDiagnostics` is enabled.
631
+ Trace metadata is optional and has no effect on execution. When `includeDiagnostics` is enabled, use labels and descriptions to distinguish repeated integration calls in the diagnostics panel.
921
632
 
922
633
  ## Error Handling
923
634
 
@@ -984,47 +695,16 @@ try {
984
695
 
985
696
  // The actual data that failed validation
986
697
  console.error('Failed data:', error.details.data);
987
-
988
- // Access Zod utility methods
989
- const formatted = error.details.zodError.format();
990
- const flattened = error.details.zodError.flatten();
991
698
  }
992
699
  }
993
700
  ```
994
701
 
995
- The `RestApiValidationError` provides complete context for debugging:
996
-
997
- - **Full Zod error object** (`zodError`) with all validation information
998
- - **Validation codes** (e.g., `"invalid_type"`, `"too_small"`, `"invalid_email"`)
999
- - **Expected vs received** values for type mismatches
1000
- - **Constraint information** (e.g., minimum/maximum values)
1001
- - **The actual data** that failed validation
1002
- - **Utility methods** like `format()` and `flatten()` for different error representations
1003
-
1004
702
  ## Best Practices
1005
703
 
1006
704
  ### Push Filtering Down to the Data Source
1007
705
 
1008
706
  Always filter data at the source (database, API) rather than fetching everything and filtering in code. This reduces network transfer, memory usage, and processing time.
1009
707
 
1010
- **Inefficient - Fetching all rows and filtering in code:**
1011
-
1012
- ```typescript
1013
- const AllUsersSchema = z.array(
1014
- z.object({ id: z.string(), status: z.string(), region: z.string() }),
1015
- );
1016
-
1017
- // Fetches ALL users, then filters - wasteful!
1018
- async run(ctx, { region }) {
1019
- const allUsers = await ctx.integrations.db.query("SELECT * FROM users", AllUsersSchema);
1020
- const activeUsersInRegion = allUsers.filter(
1021
- (u) => u.status === "active" && u.region === region,
1022
- );
1023
- }
1024
- ```
1025
-
1026
- **Efficient - Filtering in the database query:**
1027
-
1028
708
  ```typescript
1029
709
  const UserSchema = z.object({
1030
710
  id: z.string(),
@@ -1042,22 +722,7 @@ async run(ctx, { region }) {
1042
722
  }
1043
723
  ```
1044
724
 
1045
- The same principle applies to REST APIs - use query parameters to filter at the source:
1046
-
1047
- **Inefficient:**
1048
-
1049
- ```typescript
1050
- // Fetches all issues, then filters client-side
1051
- const allIssues = await ctx.integrations.repo.apiRequest(
1052
- { method: "GET", path: "/repos/owner/repo/issues" },
1053
- { response: IssuesSchema },
1054
- );
1055
- const openBugs = allIssues.filter(
1056
- (i) => i.state === "open" && i.labels.includes("bug"),
1057
- );
1058
- ```
1059
-
1060
- **Efficient:**
725
+ Use query parameters to filter REST API responses at the source:
1061
726
 
1062
727
  ```typescript
1063
728
  // API does the filtering - only matching issues returned
@@ -1075,20 +740,6 @@ const openBugs = await ctx.integrations.repo.apiRequest(
1075
740
 
1076
741
  Avoid `SELECT *` in database queries. Fetch only the columns you actually use to reduce data transfer and improve query performance.
1077
742
 
1078
- **Inefficient:**
1079
-
1080
- ```typescript
1081
- // Fetches all columns even though we only need id and email
1082
- const users = await ctx.integrations.db.query(
1083
- "SELECT * FROM users WHERE status = $1",
1084
- UserSchema,
1085
- ["active"],
1086
- );
1087
- const emails = users.map((u) => u.email);
1088
- ```
1089
-
1090
- **Efficient:**
1091
-
1092
743
  ```typescript
1093
744
  const UserEmailSchema = z.object({ id: z.string(), email: z.string() });
1094
745
 
@@ -1135,28 +786,7 @@ while (hasMore) {
1135
786
 
1136
787
  ### Avoid N+1 Query Patterns
1137
788
 
1138
- Don't fetch related data one item at a time in a loop. Use JOINs or batch queries instead.
1139
-
1140
- **N+1 Problem - One query per order:**
1141
-
1142
- ```typescript
1143
- const orders = await ctx.integrations.db.query(
1144
- "SELECT * FROM orders",
1145
- OrderSchema,
1146
- );
1147
-
1148
- // BAD: This makes N additional queries!
1149
- for (const order of orders) {
1150
- const items = await ctx.integrations.db.query(
1151
- "SELECT * FROM order_items WHERE order_id = $1",
1152
- ItemSchema,
1153
- [order.id],
1154
- );
1155
- order.items = items;
1156
- }
1157
- ```
1158
-
1159
- **Efficient - Single query with JOIN:**
789
+ Don't fetch related data one item at a time in a loop (the N+1 problem: one query for the parent rows, then one more per row). Use JOINs or batch queries instead.
1160
790
 
1161
791
  ```typescript
1162
792
  const OrderWithItemsSchema = z.object({
@@ -1177,66 +807,11 @@ const rows = await ctx.integrations.db.query(
1177
807
  );
1178
808
  ```
1179
809
 
1180
- **Alternative - Batch fetch with IN clause:**
1181
-
1182
- ```typescript
1183
- const orders = await ctx.integrations.db.query(
1184
- "SELECT * FROM orders",
1185
- OrderSchema,
1186
- );
1187
- const orderIds = orders.map((o) => o.id);
1188
-
1189
- // Single query for all items
1190
- const items = await ctx.integrations.db.query(
1191
- "SELECT * FROM order_items WHERE order_id = ANY($1)",
1192
- ItemSchema,
1193
- [orderIds],
1194
- );
1195
- ```
810
+ When a join does not fit, fetch related rows in one batch with `WHERE id = ANY($1)`.
1196
811
 
1197
812
  ### Batch API Requests When Possible
1198
813
 
1199
- Some APIs support batch operations. Use them instead of making individual requests.
1200
-
1201
- **Inefficient - Individual requests:**
1202
-
1203
- ```typescript
1204
- // Makes 100 separate API calls!
1205
- for (const userId of userIds) {
1206
- await ctx.integrations.notifier.apiRequest(
1207
- {
1208
- method: "POST",
1209
- path: "/chat.postMessage",
1210
- body: { channel: userId, text: "Hello!" },
1211
- },
1212
- { response: MessageSchema },
1213
- );
1214
- }
1215
- ```
1216
-
1217
- **Efficient - Use batch endpoints when available:**
1218
-
1219
- ```typescript
1220
- // Check if the API supports batch operations
1221
- // For Slack, consider using chat.postMessage with multiple channel webhooks
1222
- // or processing in controlled parallel batches
1223
- const BATCH_SIZE = 10;
1224
- for (let i = 0; i < userIds.length; i += BATCH_SIZE) {
1225
- const batch = userIds.slice(i, i + BATCH_SIZE);
1226
- await Promise.all(
1227
- batch.map((userId) =>
1228
- ctx.integrations.notifier.apiRequest(
1229
- {
1230
- method: "POST",
1231
- path: "/chat.postMessage",
1232
- body: { channel: userId, text: "Hello!" },
1233
- },
1234
- { response: MessageSchema },
1235
- ),
1236
- ),
1237
- );
1238
- }
1239
- ```
814
+ Prefer a documented batch endpoint. When none exists, cap concurrency with small batches; do not launch one unbounded request per row.
1240
815
 
1241
816
  ### Use Precise Schemas
1242
817
 
@@ -1330,156 +905,6 @@ const getUsers = useApi<typeof GetUsersApi>("GetUsers");
1330
905
  const listProfiles = useApi<typeof ListProfiles>("ListProfiles");
1331
906
  ```
1332
907
 
1333
- ## Complete Example
1334
-
1335
- ```typescript
1336
- import { api, z, postgres, slack, openai } from "@superblocksteam/sdk-api";
1337
-
1338
- // Integration IDs from the integrations panel
1339
- const PROD_POSTGRES = "a1b2c3d4-5678-90ab-cdef-ghijklmnopqr";
1340
- const OPS_SLACK = "b2c3d4e5-6789-01ab-cdef-ghijklmnopqr";
1341
- const PROD_OPENAI = "c3d4e5f6-7890-12ab-cdef-ghijklmnopqr";
1342
-
1343
- // Define reusable schemas
1344
- const OrderItemSchema = z.object({
1345
- productId: z.string(),
1346
- quantity: z.number().int().positive(),
1347
- });
1348
-
1349
- const CompletionRequestSchema = z.object({
1350
- model: z.string(),
1351
- messages: z.array(
1352
- z.object({
1353
- role: z.string(),
1354
- content: z.string(),
1355
- }),
1356
- ),
1357
- });
1358
-
1359
- const CompletionResponseSchema = z.object({
1360
- choices: z.array(
1361
- z.object({
1362
- message: z.object({
1363
- content: z.string(),
1364
- }),
1365
- }),
1366
- ),
1367
- });
1368
-
1369
- const CustomerSchema = z.object({
1370
- id: z.string(),
1371
- email: z.string().email(),
1372
- });
1373
-
1374
- const SlackPostMessageResponseSchema = z.object({
1375
- ts: z.string().optional(),
1376
- });
1377
-
1378
- export default api({
1379
- name: "CreateOrder",
1380
-
1381
- // Declare all integrations upfront
1382
- integrations: {
1383
- db: postgres(PROD_POSTGRES),
1384
- notifier: slack(OPS_SLACK),
1385
- ai: openai(PROD_OPENAI),
1386
- },
1387
-
1388
- input: z.object({
1389
- customerId: z.string().uuid(),
1390
- items: z.array(OrderItemSchema).min(1),
1391
- sendAiSummary: z.boolean().default(false),
1392
- }),
1393
-
1394
- output: z.object({
1395
- orderId: z.string(),
1396
- total: z.number(),
1397
- itemCount: z.number(),
1398
- aiSummary: z.string().optional(),
1399
- }),
1400
-
1401
- async run(ctx, { customerId, items, sendAiSummary }) {
1402
- ctx.log.info("Creating order", { customerId, itemCount: items.length });
1403
-
1404
- // Get customer from database
1405
- const customers = await ctx.integrations.db.query(
1406
- "SELECT id, email FROM customers WHERE id = $1",
1407
- CustomerSchema,
1408
- [customerId],
1409
- );
1410
- const customer = customers[0];
1411
-
1412
- if (!customer) {
1413
- throw new Error(`Customer not found: ${customerId}`);
1414
- }
1415
-
1416
- // Calculate total
1417
- const total = items.reduce((sum, item) => {
1418
- // In real code, you'd look up prices
1419
- return sum + item.quantity * 10;
1420
- }, 0);
1421
-
1422
- // Insert order
1423
- await ctx.integrations.db.execute(
1424
- "INSERT INTO orders (customer_id, total, item_count) VALUES ($1, $2, $3)",
1425
- [customerId, total, items.length],
1426
- );
1427
-
1428
- const orderId = `ord_${Date.now()}`;
1429
-
1430
- // Notify via Slack
1431
- const slackResult = await ctx.integrations.notifier.apiRequest(
1432
- {
1433
- method: "POST",
1434
- path: "/chat.postMessage",
1435
- body: {
1436
- channel: "#orders",
1437
- text: `New order ${orderId} from ${customer.email}: $${total.toFixed(2)}`,
1438
- },
1439
- },
1440
- { response: SlackPostMessageResponseSchema },
1441
- );
1442
-
1443
- if (!slackResult.ok) {
1444
- throw new Error(`Slack API error: ${slackResult.error}`);
1445
- }
1446
-
1447
- // Generate AI summary if requested
1448
- let aiSummary: string | undefined;
1449
- if (sendAiSummary) {
1450
- const completion = await ctx.integrations.ai.apiRequest(
1451
- {
1452
- method: "POST",
1453
- path: "/chat/completions",
1454
- body: {
1455
- model: "gpt-4",
1456
- messages: [
1457
- {
1458
- role: "user",
1459
- content: `Summarize this order: ${items.length} items, total $${total}`,
1460
- },
1461
- ],
1462
- },
1463
- },
1464
- {
1465
- body: CompletionRequestSchema,
1466
- response: CompletionResponseSchema,
1467
- },
1468
- );
1469
-
1470
- aiSummary = completion.choices[0]?.message.content;
1471
- }
1472
-
1473
- return {
1474
- orderId,
1475
- total,
1476
- itemCount: items.length,
1477
- aiSummary,
1478
- };
1479
- },
1480
- });
1481
- ```
1482
-
1483
908
  ## Development
1484
909
 
1485
910
  ```bash