@donotdev/functions 0.0.11 → 0.0.13

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 (55) hide show
  1. package/README.md +1 -1
  2. package/package.json +9 -9
  3. package/src/firebase/auth/setCustomClaims.ts +19 -5
  4. package/src/firebase/baseFunction.ts +11 -3
  5. package/src/firebase/billing/changePlan.ts +5 -1
  6. package/src/firebase/billing/createCheckoutSession.ts +3 -1
  7. package/src/firebase/billing/createCustomerPortal.ts +6 -2
  8. package/src/firebase/billing/webhookHandler.ts +4 -1
  9. package/src/firebase/crud/aggregate.ts +5 -1
  10. package/src/firebase/crud/create.ts +17 -4
  11. package/src/firebase/crud/list.ts +37 -5
  12. package/src/firebase/crud/update.ts +17 -4
  13. package/src/firebase/oauth/exchangeToken.ts +17 -4
  14. package/src/shared/__tests__/validation.test.ts +5 -3
  15. package/src/shared/billing/__tests__/createCheckout.test.ts +123 -22
  16. package/src/shared/billing/__tests__/webhookHandler.test.ts +121 -30
  17. package/src/shared/errorHandling.ts +6 -6
  18. package/src/shared/firebase.ts +1 -1
  19. package/src/shared/index.ts +2 -1
  20. package/src/shared/logger.ts +9 -7
  21. package/src/shared/oauth/__tests__/exchangeToken.test.ts +6 -8
  22. package/src/shared/oauth/__tests__/grantAccess.test.ts +31 -14
  23. package/src/shared/utils/external/subscription.ts +2 -2
  24. package/src/shared/utils/internal/auth.ts +10 -3
  25. package/src/shared/utils/internal/rateLimiter.ts +8 -2
  26. package/src/shared/utils.ts +23 -30
  27. package/src/supabase/auth/deleteAccount.ts +4 -11
  28. package/src/supabase/auth/getCustomClaims.ts +5 -3
  29. package/src/supabase/auth/getUserAuthStatus.ts +5 -3
  30. package/src/supabase/auth/removeCustomClaims.ts +10 -5
  31. package/src/supabase/auth/setCustomClaims.ts +9 -4
  32. package/src/supabase/baseFunction.ts +80 -21
  33. package/src/supabase/billing/cancelSubscription.ts +9 -3
  34. package/src/supabase/billing/changePlan.ts +20 -5
  35. package/src/supabase/billing/createCheckoutSession.ts +20 -5
  36. package/src/supabase/billing/createCustomerPortal.ts +14 -4
  37. package/src/supabase/billing/refreshSubscriptionStatus.ts +29 -9
  38. package/src/supabase/crud/aggregate.ts +14 -4
  39. package/src/supabase/crud/create.ts +30 -11
  40. package/src/supabase/crud/delete.ts +11 -3
  41. package/src/supabase/crud/get.ts +25 -3
  42. package/src/supabase/crud/list.ts +106 -21
  43. package/src/supabase/crud/update.ts +32 -10
  44. package/src/supabase/helpers/authProvider.ts +5 -2
  45. package/src/supabase/index.ts +1 -4
  46. package/src/supabase/registerCrudFunctions.ts +11 -9
  47. package/src/supabase/utils/idempotency.ts +13 -15
  48. package/src/supabase/utils/monitoring.ts +5 -1
  49. package/src/supabase/utils/rateLimiter.ts +13 -3
  50. package/src/vercel/api/billing/webhook-handler.ts +6 -2
  51. package/src/vercel/api/crud/create.ts +7 -2
  52. package/src/vercel/api/crud/delete.ts +3 -1
  53. package/src/vercel/api/crud/get.ts +3 -1
  54. package/src/vercel/api/crud/list.ts +3 -1
  55. package/src/vercel/api/crud/update.ts +7 -2
@@ -18,10 +18,7 @@ import { defaultFieldMapper } from '@donotdev/supabase';
18
18
  import { updateMetadata } from '../../shared/index.js';
19
19
  import { DoNotDevError, validateDocument } from '../../shared/utils.js';
20
20
  import { createSupabaseHandler } from '../baseFunction.js';
21
- import {
22
- checkIdempotency,
23
- storeIdempotency,
24
- } from '../utils/idempotency.js';
21
+ import { checkIdempotency, storeIdempotency } from '../utils/idempotency.js';
25
22
 
26
23
  const mapper = defaultFieldMapper;
27
24
 
@@ -68,7 +65,10 @@ async function checkUniqueKeys(
68
65
  // Build query excluding current document
69
66
  let query = supabaseAdmin.from(collection).select('*');
70
67
  for (const field of uniqueKey.fields) {
71
- query = query.eq(mapper.toBackendField(field), normalizeValue(payload[field]));
68
+ query = query.eq(
69
+ mapper.toBackendField(field),
70
+ normalizeValue(payload[field])
71
+ );
72
72
  }
73
73
  query = query.neq('id', id);
74
74
 
@@ -139,7 +139,10 @@ export function createSupabaseUpdateEntity(
139
139
  throw new DoNotDevError('Entity not found', 'not-found');
140
140
  }
141
141
 
142
- const merged = { ...(mapper.fromBackendRow(existing) as Record<string, any>), ...payload };
142
+ const merged = {
143
+ ...(mapper.fromBackendRow(existing) as Record<string, any>),
144
+ ...payload,
145
+ };
143
146
  const status = merged.status ?? existing.status;
144
147
  const isDraft = status === 'draft';
145
148
 
@@ -150,7 +153,14 @@ export function createSupabaseUpdateEntity(
150
153
  const uniqueKeys = schemaWithMeta.metadata?.uniqueKeys;
151
154
 
152
155
  if (uniqueKeys && uniqueKeys.length > 0) {
153
- await checkUniqueKeys(collection, id, merged, uniqueKeys, isDraft, supabaseAdmin);
156
+ await checkUniqueKeys(
157
+ collection,
158
+ id,
159
+ merged,
160
+ uniqueKeys,
161
+ isDraft,
162
+ supabaseAdmin
163
+ );
154
164
  }
155
165
 
156
166
  // Validate merged document (skip for drafts)
@@ -159,9 +169,18 @@ export function createSupabaseUpdateEntity(
159
169
  }
160
170
 
161
171
  const metadata = updateMetadata(uid);
162
- const snakeMetadata = mapper.toBackendKeys(metadata as Record<string, unknown>);
172
+ const snakeMetadata = mapper.toBackendKeys(
173
+ metadata as Record<string, unknown>
174
+ );
163
175
 
164
- const { createdAt, updatedAt, created_at, updated_at, id: _id, ...payloadWithoutTimestamps } = payload;
176
+ const {
177
+ createdAt,
178
+ updatedAt,
179
+ created_at,
180
+ updated_at,
181
+ id: _id,
182
+ ...payloadWithoutTimestamps
183
+ } = payload;
165
184
  const snakePayload = mapper.toBackendKeys(payloadWithoutTimestamps);
166
185
 
167
186
  // Update document (DB sets updated_at via trigger)
@@ -176,7 +195,10 @@ export function createSupabaseUpdateEntity(
176
195
  .single();
177
196
 
178
197
  if (error) {
179
- throw new DoNotDevError(`Failed to update entity: ${error.message}`, 'internal');
198
+ throw new DoNotDevError(
199
+ `Failed to update entity: ${error.message}`,
200
+ 'internal'
201
+ );
180
202
  }
181
203
 
182
204
  const result = mapper.fromBackendRow(updated) as Record<string, any>;
@@ -28,10 +28,13 @@ import type { SupabaseClient } from '@supabase/supabase-js';
28
28
  * @version 0.0.1
29
29
  * @since 0.5.0
30
30
  */
31
- export function createSupabaseAuthProvider(supabaseAdmin: SupabaseClient): AuthProvider {
31
+ export function createSupabaseAuthProvider(
32
+ supabaseAdmin: SupabaseClient
33
+ ): AuthProvider {
32
34
  return {
33
35
  async getUser(userId: string) {
34
- const { data, error } = await supabaseAdmin.auth.admin.getUserById(userId);
36
+ const { data, error } =
37
+ await supabaseAdmin.auth.admin.getUserById(userId);
35
38
  if (error) throw error;
36
39
  return { customClaims: data.user?.app_metadata ?? {} };
37
40
  },
@@ -60,10 +60,7 @@ export {
60
60
  checkRateLimitWithPostgres,
61
61
  DEFAULT_RATE_LIMITS,
62
62
  } from './utils/rateLimiter.js';
63
- export type {
64
- RateLimitConfig,
65
- RateLimitResult,
66
- } from './utils/rateLimiter.js';
63
+ export type { RateLimitConfig, RateLimitResult } from './utils/rateLimiter.js';
67
64
  export {
68
65
  recordOperationMetrics,
69
66
  getFailureRate,
@@ -89,7 +89,7 @@ export function createSupabaseCrudFunctions(
89
89
  schemas.update,
90
90
  access.update
91
91
  );
92
-
92
+
93
93
  // Extract reference metadata from entity if available
94
94
  const schemaWithMeta = schemas.get as {
95
95
  metadata?: {
@@ -108,7 +108,7 @@ export function createSupabaseCrudFunctions(
108
108
  };
109
109
  };
110
110
  const referenceMetadata = schemaWithMeta.metadata?.references;
111
-
111
+
112
112
  handlers[`delete_${col}`] = createSupabaseDeleteEntity(
113
113
  col,
114
114
  access.delete,
@@ -127,7 +127,8 @@ export function createSupabaseCrudFunctions(
127
127
  const serve = async (req: Request): Promise<Response> => {
128
128
  try {
129
129
  const body = await req.json().catch(() => ({}));
130
- const functionName = (body as Record<string, unknown>)._functionName as string;
130
+ const functionName = (body as Record<string, unknown>)
131
+ ._functionName as string;
131
132
 
132
133
  if (!functionName) {
133
134
  return new Response(
@@ -146,7 +147,7 @@ export function createSupabaseCrudFunctions(
146
147
 
147
148
  // Remove _functionName from body before passing to handler
148
149
  const { _functionName, ...handlerData } = body as Record<string, unknown>;
149
-
150
+
150
151
  // Create new request with cleaned body
151
152
  const handlerReq = new Request(req.url, {
152
153
  method: req.method,
@@ -156,11 +157,12 @@ export function createSupabaseCrudFunctions(
156
157
 
157
158
  return handler(handlerReq);
158
159
  } catch (error) {
159
- const message = error instanceof Error ? error.message : 'Internal server error';
160
- return new Response(
161
- JSON.stringify({ error: message }),
162
- { status: 500, headers: { 'Content-Type': 'application/json' } }
163
- );
160
+ const message =
161
+ error instanceof Error ? error.message : 'Internal server error';
162
+ return new Response(JSON.stringify({ error: message }), {
163
+ status: 500,
164
+ headers: { 'Content-Type': 'application/json' },
165
+ });
164
166
  }
165
167
  };
166
168
 
@@ -86,21 +86,19 @@ export async function storeIdempotency<T>(
86
86
  const expiresAt = new Date();
87
87
  expiresAt.setHours(expiresAt.getHours() + ttl);
88
88
 
89
- const { error } = await supabaseAdmin
90
- .from('idempotency')
91
- .upsert(
92
- {
93
- id,
94
- operation,
95
- idempotency_key: idempotencyKey,
96
- result: result as any,
97
- processed_by: uid,
98
- expires_at: expiresAt.toISOString(),
99
- },
100
- {
101
- onConflict: 'idempotency_key',
102
- }
103
- );
89
+ const { error } = await supabaseAdmin.from('idempotency').upsert(
90
+ {
91
+ id,
92
+ operation,
93
+ idempotency_key: idempotencyKey,
94
+ result: result as any,
95
+ processed_by: uid,
96
+ expires_at: expiresAt.toISOString(),
97
+ },
98
+ {
99
+ onConflict: 'idempotency_key',
100
+ }
101
+ );
104
102
 
105
103
  if (error) {
106
104
  console.error('[idempotency] Store failed:', error);
@@ -167,7 +167,11 @@ export async function getSlowOperations(
167
167
  grouped[metric.operation].count += 1;
168
168
  }
169
169
 
170
- const results: Array<{ operation: string; avgDuration: number; count: number }> = [];
170
+ const results: Array<{
171
+ operation: string;
172
+ avgDuration: number;
173
+ count: number;
174
+ }> = [];
171
175
  for (const [operation, stats] of Object.entries(grouped)) {
172
176
  const avgDuration = stats.sum / stats.count;
173
177
  if (avgDuration >= thresholdMs) {
@@ -24,7 +24,10 @@
24
24
  */
25
25
 
26
26
  import type { SupabaseClient } from '@supabase/supabase-js';
27
- import type { ServerRateLimitConfig as RateLimitConfig, ServerRateLimitResult as RateLimitResult } from '@donotdev/core';
27
+ import type {
28
+ ServerRateLimitConfig as RateLimitConfig,
29
+ ServerRateLimitResult as RateLimitResult,
30
+ } from '@donotdev/core';
28
31
 
29
32
  export type { RateLimitConfig, RateLimitResult };
30
33
 
@@ -198,7 +201,11 @@ export async function checkRateLimitWithPostgres(
198
201
 
199
202
  if (error) {
200
203
  // Log for observability — infrastructure errors here need immediate attention.
201
- console.error('[rateLimit] rate_limit_check RPC failed:', error.message, error.code);
204
+ console.error(
205
+ '[rateLimit] rate_limit_check RPC failed:',
206
+ error.message,
207
+ error.code
208
+ );
202
209
  return failClosed();
203
210
  }
204
211
 
@@ -210,7 +217,10 @@ export async function checkRateLimitWithPostgres(
210
217
  blockRemainingSeconds: result.block_remaining_seconds,
211
218
  };
212
219
  } catch (error) {
213
- console.error('[rateLimit] Unexpected error in checkRateLimitWithPostgres:', error);
220
+ console.error(
221
+ '[rateLimit] Unexpected error in checkRateLimitWithPostgres:',
222
+ error
223
+ );
214
224
  return failClosed();
215
225
  }
216
226
  }
@@ -55,7 +55,10 @@ export function createStripeWebhook(billingConfig: StripeBackConfig) {
55
55
  async getUser(userId: string) {
56
56
  return getFirebaseAdminAuth().getUser(userId);
57
57
  },
58
- async setCustomUserClaims(userId: string, claims: Record<string, unknown>) {
58
+ async setCustomUserClaims(
59
+ userId: string,
60
+ claims: Record<string, unknown>
61
+ ) {
59
62
  await getFirebaseAdminAuth().setCustomUserClaims(userId, claims);
60
63
  },
61
64
  };
@@ -90,7 +93,8 @@ async function getRawBody(req: NextApiRequest): Promise<Buffer> {
90
93
  const chunks: Buffer[] = [];
91
94
  let totalBytes = 0;
92
95
  for await (const chunk of req) {
93
- const buf = typeof chunk === 'string' ? Buffer.from(chunk) : (chunk as Buffer);
96
+ const buf =
97
+ typeof chunk === 'string' ? Buffer.from(chunk) : (chunk as Buffer);
94
98
  totalBytes += buf.length;
95
99
  if (totalBytes > MAX_BODY_BYTES) {
96
100
  throw new Error('Request body too large');
@@ -19,7 +19,10 @@ import {
19
19
  transformFirestoreData,
20
20
  } from '../../../shared/index.js';
21
21
  import { createMetadata } from '../../../shared/index.js';
22
- import { validateCollectionName, validateDocument } from '../../../shared/utils.js';
22
+ import {
23
+ validateCollectionName,
24
+ validateDocument,
25
+ } from '../../../shared/utils.js';
23
26
  import { verifyAuthToken } from '../../../shared/utils/internal/auth.js';
24
27
 
25
28
  import type { NextApiRequest, NextApiResponse } from 'next';
@@ -84,7 +87,9 @@ export default async function handler(
84
87
  handleError(error);
85
88
  } catch (handledError: any) {
86
89
  const status = handledError.code === 'invalid-argument' ? 400 : 500;
87
- return res.status(status).json({ error: handledError.message, code: handledError.code });
90
+ return res
91
+ .status(status)
92
+ .json({ error: handledError.message, code: handledError.code });
88
93
  }
89
94
  }
90
95
  }
@@ -60,7 +60,9 @@ export default async function handler(
60
60
  handleError(error);
61
61
  } catch (handledError: any) {
62
62
  const status = handledError.code === 'invalid-argument' ? 400 : 500;
63
- return res.status(status).json({ error: handledError.message, code: handledError.code });
63
+ return res
64
+ .status(status)
65
+ .json({ error: handledError.message, code: handledError.code });
64
66
  }
65
67
  }
66
68
  }
@@ -74,7 +74,9 @@ export default async function handler(
74
74
  handleError(error);
75
75
  } catch (handledError: any) {
76
76
  const status = handledError.code === 'invalid-argument' ? 400 : 500;
77
- return res.status(status).json({ error: handledError.message, code: handledError.code });
77
+ return res
78
+ .status(status)
79
+ .json({ error: handledError.message, code: handledError.code });
78
80
  }
79
81
  }
80
82
  }
@@ -87,7 +87,9 @@ export default async function handler(
87
87
  handleError(error);
88
88
  } catch (handledError: any) {
89
89
  const status = handledError.code === 'invalid-argument' ? 400 : 500;
90
- return res.status(status).json({ error: handledError.message, code: handledError.code });
90
+ return res
91
+ .status(status)
92
+ .json({ error: handledError.message, code: handledError.code });
91
93
  }
92
94
  }
93
95
  }
@@ -19,7 +19,10 @@ import {
19
19
  transformFirestoreData,
20
20
  } from '../../../shared/index.js';
21
21
  import { updateMetadata } from '../../../shared/index.js';
22
- import { validateCollectionName, validateDocument } from '../../../shared/utils.js';
22
+ import {
23
+ validateCollectionName,
24
+ validateDocument,
25
+ } from '../../../shared/utils.js';
23
26
  import { verifyAuthToken } from '../../../shared/utils/internal/auth.js';
24
27
 
25
28
  import type { NextApiRequest, NextApiResponse } from 'next';
@@ -100,7 +103,9 @@ export default async function handler(
100
103
  handleError(error);
101
104
  } catch (handledError: any) {
102
105
  const status = handledError.code === 'invalid-argument' ? 400 : 500;
103
- return res.status(status).json({ error: handledError.message, code: handledError.code });
106
+ return res
107
+ .status(status)
108
+ .json({ error: handledError.message, code: handledError.code });
104
109
  }
105
110
  }
106
111
  }