@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.
- package/README.md +1 -1
- package/package.json +9 -9
- package/src/firebase/auth/setCustomClaims.ts +19 -5
- package/src/firebase/baseFunction.ts +11 -3
- package/src/firebase/billing/changePlan.ts +5 -1
- package/src/firebase/billing/createCheckoutSession.ts +3 -1
- package/src/firebase/billing/createCustomerPortal.ts +6 -2
- package/src/firebase/billing/webhookHandler.ts +4 -1
- package/src/firebase/crud/aggregate.ts +5 -1
- package/src/firebase/crud/create.ts +17 -4
- package/src/firebase/crud/list.ts +37 -5
- package/src/firebase/crud/update.ts +17 -4
- package/src/firebase/oauth/exchangeToken.ts +17 -4
- package/src/shared/__tests__/validation.test.ts +5 -3
- package/src/shared/billing/__tests__/createCheckout.test.ts +123 -22
- package/src/shared/billing/__tests__/webhookHandler.test.ts +121 -30
- package/src/shared/errorHandling.ts +6 -6
- package/src/shared/firebase.ts +1 -1
- package/src/shared/index.ts +2 -1
- package/src/shared/logger.ts +9 -7
- package/src/shared/oauth/__tests__/exchangeToken.test.ts +6 -8
- package/src/shared/oauth/__tests__/grantAccess.test.ts +31 -14
- package/src/shared/utils/external/subscription.ts +2 -2
- package/src/shared/utils/internal/auth.ts +10 -3
- package/src/shared/utils/internal/rateLimiter.ts +8 -2
- package/src/shared/utils.ts +23 -30
- package/src/supabase/auth/deleteAccount.ts +4 -11
- package/src/supabase/auth/getCustomClaims.ts +5 -3
- package/src/supabase/auth/getUserAuthStatus.ts +5 -3
- package/src/supabase/auth/removeCustomClaims.ts +10 -5
- package/src/supabase/auth/setCustomClaims.ts +9 -4
- package/src/supabase/baseFunction.ts +80 -21
- package/src/supabase/billing/cancelSubscription.ts +9 -3
- package/src/supabase/billing/changePlan.ts +20 -5
- package/src/supabase/billing/createCheckoutSession.ts +20 -5
- package/src/supabase/billing/createCustomerPortal.ts +14 -4
- package/src/supabase/billing/refreshSubscriptionStatus.ts +29 -9
- package/src/supabase/crud/aggregate.ts +14 -4
- package/src/supabase/crud/create.ts +30 -11
- package/src/supabase/crud/delete.ts +11 -3
- package/src/supabase/crud/get.ts +25 -3
- package/src/supabase/crud/list.ts +106 -21
- package/src/supabase/crud/update.ts +32 -10
- package/src/supabase/helpers/authProvider.ts +5 -2
- package/src/supabase/index.ts +1 -4
- package/src/supabase/registerCrudFunctions.ts +11 -9
- package/src/supabase/utils/idempotency.ts +13 -15
- package/src/supabase/utils/monitoring.ts +5 -1
- package/src/supabase/utils/rateLimiter.ts +13 -3
- package/src/vercel/api/billing/webhook-handler.ts +6 -2
- package/src/vercel/api/crud/create.ts +7 -2
- package/src/vercel/api/crud/delete.ts +3 -1
- package/src/vercel/api/crud/get.ts +3 -1
- package/src/vercel/api/crud/list.ts +3 -1
- 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(
|
|
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 = {
|
|
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(
|
|
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(
|
|
172
|
+
const snakeMetadata = mapper.toBackendKeys(
|
|
173
|
+
metadata as Record<string, unknown>
|
|
174
|
+
);
|
|
163
175
|
|
|
164
|
-
const {
|
|
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(
|
|
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(
|
|
31
|
+
export function createSupabaseAuthProvider(
|
|
32
|
+
supabaseAdmin: SupabaseClient
|
|
33
|
+
): AuthProvider {
|
|
32
34
|
return {
|
|
33
35
|
async getUser(userId: string) {
|
|
34
|
-
const { data, error } =
|
|
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
|
},
|
package/src/supabase/index.ts
CHANGED
|
@@ -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>)
|
|
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 =
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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<{
|
|
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
106
|
+
return res
|
|
107
|
+
.status(status)
|
|
108
|
+
.json({ error: handledError.message, code: handledError.code });
|
|
104
109
|
}
|
|
105
110
|
}
|
|
106
111
|
}
|