@donotdev/functions 0.0.12 → 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/package.json +4 -4
- 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/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 +9 -4
- 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/firebase.ts +1 -1
- package/src/shared/logger.ts +7 -1
- package/src/shared/oauth/__tests__/exchangeToken.test.ts +6 -8
- package/src/shared/oauth/__tests__/grantAccess.test.ts +31 -14
- package/src/shared/utils/internal/auth.ts +10 -3
- package/src/shared/utils/internal/rateLimiter.ts +8 -2
- package/src/shared/utils.ts +5 -1
- package/src/supabase/auth/deleteAccount.ts +1 -1
- 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 +77 -22
- 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 +76 -22
- 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
|
@@ -103,7 +103,8 @@ export function createSupabaseHandler<TReq, TRes>(
|
|
|
103
103
|
headers: {
|
|
104
104
|
'Access-Control-Allow-Origin': allowedOrigin,
|
|
105
105
|
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
106
|
-
'Access-Control-Allow-Headers':
|
|
106
|
+
'Access-Control-Allow-Headers':
|
|
107
|
+
'authorization, content-type, x-client-info, apikey',
|
|
107
108
|
},
|
|
108
109
|
});
|
|
109
110
|
}
|
|
@@ -116,23 +117,32 @@ export function createSupabaseHandler<TReq, TRes>(
|
|
|
116
117
|
// Extract and verify auth token
|
|
117
118
|
const authHeader = req.headers.get('authorization');
|
|
118
119
|
if (!authHeader?.startsWith('Bearer ')) {
|
|
119
|
-
return respond(
|
|
120
|
+
return respond(
|
|
121
|
+
{ error: 'Missing or invalid authorization header' },
|
|
122
|
+
401
|
|
123
|
+
);
|
|
120
124
|
}
|
|
121
125
|
const token = authHeader.slice(7);
|
|
122
126
|
|
|
123
127
|
// Create admin client
|
|
124
128
|
const supabaseUrl = getEnvOrThrow('SUPABASE_URL');
|
|
125
129
|
// Try new env var first, fall back to legacy name
|
|
126
|
-
const secretKey =
|
|
130
|
+
const secretKey =
|
|
131
|
+
getEnv('SUPABASE_SECRET_KEY') || getEnv('SUPABASE_SERVICE_ROLE_KEY');
|
|
127
132
|
if (!secretKey) {
|
|
128
|
-
throw new Error(
|
|
133
|
+
throw new Error(
|
|
134
|
+
'Missing environment variable: SUPABASE_SECRET_KEY or SUPABASE_SERVICE_ROLE_KEY'
|
|
135
|
+
);
|
|
129
136
|
}
|
|
130
137
|
const supabaseAdmin = createClient(supabaseUrl, secretKey, {
|
|
131
138
|
auth: { autoRefreshToken: false, persistSession: false },
|
|
132
139
|
});
|
|
133
140
|
|
|
134
141
|
// Verify JWT and extract user
|
|
135
|
-
const {
|
|
142
|
+
const {
|
|
143
|
+
data: { user },
|
|
144
|
+
error: authError,
|
|
145
|
+
} = await supabaseAdmin.auth.getUser(token);
|
|
136
146
|
if (authError || !user) {
|
|
137
147
|
return respond({ error: 'Invalid or expired token' }, 401);
|
|
138
148
|
}
|
|
@@ -148,7 +158,9 @@ export function createSupabaseHandler<TReq, TRes>(
|
|
|
148
158
|
// Non-guest access: verify user has required role level
|
|
149
159
|
if (!hasRoleAccess(userRole, requiredRole)) {
|
|
150
160
|
return respond(
|
|
151
|
-
{
|
|
161
|
+
{
|
|
162
|
+
error: `Access denied. Required role: ${requiredRole}, your role: ${userRole}`,
|
|
163
|
+
},
|
|
152
164
|
403
|
|
153
165
|
);
|
|
154
166
|
}
|
|
@@ -164,8 +176,9 @@ export function createSupabaseHandler<TReq, TRes>(
|
|
|
164
176
|
: `uid_${user.id}`;
|
|
165
177
|
const rateLimitKey = `${operationName}_${rateLimitIdentifier}`;
|
|
166
178
|
const rateLimitConfig: RateLimitConfig =
|
|
167
|
-
(DEFAULT_RATE_LIMITS as Record<string, RateLimitConfig>)[
|
|
168
|
-
|
|
179
|
+
(DEFAULT_RATE_LIMITS as Record<string, RateLimitConfig>)[
|
|
180
|
+
operationName
|
|
181
|
+
] || DEFAULT_RATE_LIMITS.api;
|
|
169
182
|
|
|
170
183
|
const rateLimitResult = await checkRateLimitWithPostgres(
|
|
171
184
|
supabaseAdmin,
|
|
@@ -210,7 +223,10 @@ export function createSupabaseHandler<TReq, TRes>(
|
|
|
210
223
|
supabaseAdmin,
|
|
211
224
|
});
|
|
212
225
|
} catch (handlerError) {
|
|
213
|
-
error =
|
|
226
|
+
error =
|
|
227
|
+
handlerError instanceof Error
|
|
228
|
+
? handlerError
|
|
229
|
+
: new Error(String(handlerError));
|
|
214
230
|
throw handlerError;
|
|
215
231
|
} finally {
|
|
216
232
|
// Record metrics if enabled
|
|
@@ -233,7 +249,8 @@ export function createSupabaseHandler<TReq, TRes>(
|
|
|
233
249
|
return respond(result, 200);
|
|
234
250
|
} catch (error) {
|
|
235
251
|
console.error(`[${operationName}] Error:`, error);
|
|
236
|
-
const message =
|
|
252
|
+
const message =
|
|
253
|
+
error instanceof Error ? error.message : 'Internal server error';
|
|
237
254
|
const status = getErrorStatus(message);
|
|
238
255
|
// allowedOrigin may not be set if error occurred before that line; fall back to '*'
|
|
239
256
|
const origin = getEnv('ALLOWED_ORIGIN', '*');
|
|
@@ -246,7 +263,11 @@ export function createSupabaseHandler<TReq, TRes>(
|
|
|
246
263
|
// Helpers
|
|
247
264
|
// =============================================================================
|
|
248
265
|
|
|
249
|
-
function jsonResponse(
|
|
266
|
+
function jsonResponse(
|
|
267
|
+
data: unknown,
|
|
268
|
+
status: number,
|
|
269
|
+
allowedOrigin = '*'
|
|
270
|
+
): Response {
|
|
250
271
|
return new Response(JSON.stringify(data), {
|
|
251
272
|
status,
|
|
252
273
|
headers: {
|
|
@@ -257,13 +278,19 @@ function jsonResponse(data: unknown, status: number, allowedOrigin = '*'): Respo
|
|
|
257
278
|
}
|
|
258
279
|
|
|
259
280
|
function getEnvOrThrow(key: string): string {
|
|
260
|
-
const value = (
|
|
281
|
+
const value = (
|
|
282
|
+
typeof Deno !== 'undefined' ? Deno.env.get(key) : process.env[key]
|
|
283
|
+
) as string | undefined;
|
|
261
284
|
if (!value) throw new Error(`Missing environment variable: ${key}`);
|
|
262
285
|
return value;
|
|
263
286
|
}
|
|
264
287
|
|
|
265
288
|
function getEnv(key: string, defaultValue: string = ''): string {
|
|
266
|
-
return (
|
|
289
|
+
return (
|
|
290
|
+
((typeof Deno !== 'undefined' ? Deno.env.get(key) : process.env[key]) as
|
|
291
|
+
| string
|
|
292
|
+
| undefined) || defaultValue
|
|
293
|
+
);
|
|
267
294
|
}
|
|
268
295
|
|
|
269
296
|
function getClientIp(req: Request): string {
|
|
@@ -273,7 +300,10 @@ function getClientIp(req: Request): string {
|
|
|
273
300
|
// W19: Take the rightmost (last) IP — the leftmost entry is client-supplied
|
|
274
301
|
// and trivially spoofable. The last entry is appended by the nearest trusted
|
|
275
302
|
// reverse proxy and is the most reliable.
|
|
276
|
-
const ips = forwardedFor
|
|
303
|
+
const ips = forwardedFor
|
|
304
|
+
.split(',')
|
|
305
|
+
.map((ip) => ip.trim())
|
|
306
|
+
.filter(Boolean);
|
|
277
307
|
const lastIp = ips[ips.length - 1];
|
|
278
308
|
if (lastIp) return lastIp;
|
|
279
309
|
}
|
|
@@ -287,20 +317,45 @@ function getClientIp(req: Request): string {
|
|
|
287
317
|
|
|
288
318
|
function getErrorCode(message: string): string {
|
|
289
319
|
if (message.includes('Rate limit')) return 'rate-limit-exceeded';
|
|
290
|
-
if (message.includes('not found') || message.includes('No active'))
|
|
291
|
-
|
|
292
|
-
if (
|
|
293
|
-
|
|
320
|
+
if (message.includes('not found') || message.includes('No active'))
|
|
321
|
+
return 'not-found';
|
|
322
|
+
if (
|
|
323
|
+
message.includes('permission') ||
|
|
324
|
+
message.includes('denied') ||
|
|
325
|
+
message.includes('Forbidden')
|
|
326
|
+
)
|
|
327
|
+
return 'permission-denied';
|
|
328
|
+
if (
|
|
329
|
+
message.includes('mismatch') ||
|
|
330
|
+
message.includes('Invalid') ||
|
|
331
|
+
message.includes('Missing')
|
|
332
|
+
)
|
|
333
|
+
return 'invalid-argument';
|
|
334
|
+
if (message.includes('already-exists') || message.includes('Duplicate'))
|
|
335
|
+
return 'already-exists';
|
|
294
336
|
return 'internal';
|
|
295
337
|
}
|
|
296
338
|
|
|
297
339
|
function getErrorStatus(message: string): number {
|
|
298
340
|
if (message.includes('Rate limit')) return 429;
|
|
299
|
-
if (message.includes('not found') || message.includes('No active'))
|
|
300
|
-
|
|
301
|
-
if (
|
|
341
|
+
if (message.includes('not found') || message.includes('No active'))
|
|
342
|
+
return 404;
|
|
343
|
+
if (
|
|
344
|
+
message.includes('permission') ||
|
|
345
|
+
message.includes('denied') ||
|
|
346
|
+
message.includes('Forbidden')
|
|
347
|
+
)
|
|
348
|
+
return 403;
|
|
349
|
+
if (
|
|
350
|
+
message.includes('mismatch') ||
|
|
351
|
+
message.includes('Invalid') ||
|
|
352
|
+
message.includes('Missing')
|
|
353
|
+
)
|
|
354
|
+
return 400;
|
|
302
355
|
return 500;
|
|
303
356
|
}
|
|
304
357
|
|
|
305
358
|
// Deno global type declaration for env access
|
|
306
|
-
declare const Deno:
|
|
359
|
+
declare const Deno:
|
|
360
|
+
| { env: { get(key: string): string | undefined } }
|
|
361
|
+
| undefined;
|
|
@@ -44,14 +44,20 @@ export function createCancelSubscription() {
|
|
|
44
44
|
initStripe(getStripeKey());
|
|
45
45
|
const authProvider = createSupabaseAuthProvider(ctx.supabaseAdmin);
|
|
46
46
|
return cancelUserSubscription(data.userId, authProvider);
|
|
47
|
-
}
|
|
47
|
+
}
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function getStripeKey(): string {
|
|
52
|
-
const key = (
|
|
52
|
+
const key = (
|
|
53
|
+
typeof Deno !== 'undefined'
|
|
54
|
+
? Deno.env.get('STRIPE_SECRET_KEY')
|
|
55
|
+
: process.env.STRIPE_SECRET_KEY
|
|
56
|
+
) as string | undefined;
|
|
53
57
|
if (!key) throw new Error('Missing STRIPE_SECRET_KEY environment variable');
|
|
54
58
|
return key;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
declare const Deno:
|
|
61
|
+
declare const Deno:
|
|
62
|
+
| { env: { get(key: string): string | undefined } }
|
|
63
|
+
| undefined;
|
|
@@ -25,7 +25,10 @@ import { createSupabaseAuthProvider } from '../helpers/authProvider.js';
|
|
|
25
25
|
const changePlanSchema = v.object({
|
|
26
26
|
userId: v.pipe(v.string(), v.minLength(1, 'User ID is required')),
|
|
27
27
|
newPriceId: v.pipe(v.string(), v.minLength(1, 'Price ID is required')),
|
|
28
|
-
billingConfigKey: v.pipe(
|
|
28
|
+
billingConfigKey: v.pipe(
|
|
29
|
+
v.string(),
|
|
30
|
+
v.minLength(1, 'Billing config key is required')
|
|
31
|
+
),
|
|
29
32
|
});
|
|
30
33
|
|
|
31
34
|
// =============================================================================
|
|
@@ -48,15 +51,27 @@ export function createChangePlan(billingConfig: StripeBackConfig) {
|
|
|
48
51
|
async (data, ctx) => {
|
|
49
52
|
initStripe(getStripeKey());
|
|
50
53
|
const authProvider = createSupabaseAuthProvider(ctx.supabaseAdmin);
|
|
51
|
-
return changeUserPlan(
|
|
52
|
-
|
|
54
|
+
return changeUserPlan(
|
|
55
|
+
data.userId,
|
|
56
|
+
data.newPriceId,
|
|
57
|
+
data.billingConfigKey,
|
|
58
|
+
billingConfig,
|
|
59
|
+
authProvider
|
|
60
|
+
);
|
|
61
|
+
}
|
|
53
62
|
);
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
function getStripeKey(): string {
|
|
57
|
-
const key = (
|
|
66
|
+
const key = (
|
|
67
|
+
typeof Deno !== 'undefined'
|
|
68
|
+
? Deno.env.get('STRIPE_SECRET_KEY')
|
|
69
|
+
: process.env.STRIPE_SECRET_KEY
|
|
70
|
+
) as string | undefined;
|
|
58
71
|
if (!key) throw new Error('Missing STRIPE_SECRET_KEY environment variable');
|
|
59
72
|
return key;
|
|
60
73
|
}
|
|
61
74
|
|
|
62
|
-
declare const Deno:
|
|
75
|
+
declare const Deno:
|
|
76
|
+
| { env: { get(key: string): string | undefined } }
|
|
77
|
+
| undefined;
|
|
@@ -13,7 +13,11 @@ import type { StripeBackConfig } from '@donotdev/core/server';
|
|
|
13
13
|
import { CreateCheckoutSessionRequestSchema } from '@donotdev/core/server';
|
|
14
14
|
|
|
15
15
|
import { createCheckoutAlgorithm } from '../../shared/billing/createCheckout.js';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
initStripe,
|
|
18
|
+
stripe,
|
|
19
|
+
validateStripeEnvironment,
|
|
20
|
+
} from '../../shared/utils.js';
|
|
17
21
|
import { createSupabaseHandler } from '../baseFunction.js';
|
|
18
22
|
import { createSupabaseAuthProvider } from '../helpers/authProvider.js';
|
|
19
23
|
|
|
@@ -68,15 +72,26 @@ export function createCheckoutSession(billingConfig: StripeBackConfig) {
|
|
|
68
72
|
},
|
|
69
73
|
};
|
|
70
74
|
|
|
71
|
-
return createCheckoutAlgorithm(
|
|
72
|
-
|
|
75
|
+
return createCheckoutAlgorithm(
|
|
76
|
+
data,
|
|
77
|
+
stripeProvider,
|
|
78
|
+
authProvider,
|
|
79
|
+
billingConfig
|
|
80
|
+
);
|
|
81
|
+
}
|
|
73
82
|
);
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
function getStripeKey(): string {
|
|
77
|
-
const key = (
|
|
86
|
+
const key = (
|
|
87
|
+
typeof Deno !== 'undefined'
|
|
88
|
+
? Deno.env.get('STRIPE_SECRET_KEY')
|
|
89
|
+
: process.env.STRIPE_SECRET_KEY
|
|
90
|
+
) as string | undefined;
|
|
78
91
|
if (!key) throw new Error('Missing STRIPE_SECRET_KEY environment variable');
|
|
79
92
|
return key;
|
|
80
93
|
}
|
|
81
94
|
|
|
82
|
-
declare const Deno:
|
|
95
|
+
declare const Deno:
|
|
96
|
+
| { env: { get(key: string): string | undefined } }
|
|
97
|
+
| undefined;
|
|
@@ -44,15 +44,25 @@ export function createCustomerPortal() {
|
|
|
44
44
|
async (data, ctx) => {
|
|
45
45
|
initStripe(getStripeKey());
|
|
46
46
|
const authProvider = createSupabaseAuthProvider(ctx.supabaseAdmin);
|
|
47
|
-
return createCustomerPortalSession(
|
|
48
|
-
|
|
47
|
+
return createCustomerPortalSession(
|
|
48
|
+
data.userId,
|
|
49
|
+
authProvider,
|
|
50
|
+
data.returnUrl
|
|
51
|
+
);
|
|
52
|
+
}
|
|
49
53
|
);
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
function getStripeKey(): string {
|
|
53
|
-
const key = (
|
|
57
|
+
const key = (
|
|
58
|
+
typeof Deno !== 'undefined'
|
|
59
|
+
? Deno.env.get('STRIPE_SECRET_KEY')
|
|
60
|
+
: process.env.STRIPE_SECRET_KEY
|
|
61
|
+
) as string | undefined;
|
|
54
62
|
if (!key) throw new Error('Missing STRIPE_SECRET_KEY environment variable');
|
|
55
63
|
return key;
|
|
56
64
|
}
|
|
57
65
|
|
|
58
|
-
declare const Deno:
|
|
66
|
+
declare const Deno:
|
|
67
|
+
| { env: { get(key: string): string | undefined } }
|
|
68
|
+
| undefined;
|
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
import Stripe from 'stripe';
|
|
13
13
|
import * as v from 'valibot';
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
initStripe,
|
|
17
|
+
stripe,
|
|
18
|
+
validateStripeEnvironment,
|
|
19
|
+
} from '../../shared/utils.js';
|
|
16
20
|
import { createSupabaseHandler } from '../baseFunction.js';
|
|
17
21
|
import { createSupabaseAuthProvider } from '../helpers/authProvider.js';
|
|
18
22
|
|
|
@@ -48,12 +52,16 @@ export function createRefreshSubscriptionStatus() {
|
|
|
48
52
|
const user = await authProvider.getUser(data.userId);
|
|
49
53
|
const currentClaims = user.customClaims || {};
|
|
50
54
|
|
|
51
|
-
const subscriptionId = (
|
|
55
|
+
const subscriptionId = (
|
|
56
|
+
currentClaims.subscription as { subscriptionId?: string } | undefined
|
|
57
|
+
)?.subscriptionId;
|
|
52
58
|
if (!subscriptionId) {
|
|
53
59
|
throw new Error('No active subscription found');
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
const subscription = await stripe.subscriptions.retrieve(
|
|
62
|
+
const subscription = (await stripe.subscriptions.retrieve(
|
|
63
|
+
subscriptionId
|
|
64
|
+
)) as Stripe.Subscription;
|
|
57
65
|
|
|
58
66
|
// Update claims via auth provider
|
|
59
67
|
const updatedClaims = {
|
|
@@ -61,8 +69,12 @@ export function createRefreshSubscriptionStatus() {
|
|
|
61
69
|
subscription: {
|
|
62
70
|
...(currentClaims.subscription as Record<string, unknown>),
|
|
63
71
|
status: subscription.status,
|
|
64
|
-
currentPeriodStart: new Date(
|
|
65
|
-
|
|
72
|
+
currentPeriodStart: new Date(
|
|
73
|
+
(subscription as any).current_period_start * 1000
|
|
74
|
+
).toISOString(),
|
|
75
|
+
currentPeriodEnd: new Date(
|
|
76
|
+
(subscription as any).current_period_end * 1000
|
|
77
|
+
).toISOString(),
|
|
66
78
|
cancelAtPeriodEnd: (subscription as any).cancel_at_period_end,
|
|
67
79
|
},
|
|
68
80
|
};
|
|
@@ -73,17 +85,25 @@ export function createRefreshSubscriptionStatus() {
|
|
|
73
85
|
success: true,
|
|
74
86
|
userId: data.userId,
|
|
75
87
|
status: subscription.status,
|
|
76
|
-
currentPeriodEnd: new Date(
|
|
88
|
+
currentPeriodEnd: new Date(
|
|
89
|
+
(subscription as any).current_period_end * 1000
|
|
90
|
+
).toISOString(),
|
|
77
91
|
cancelAtPeriodEnd: (subscription as any).cancel_at_period_end,
|
|
78
92
|
};
|
|
79
|
-
}
|
|
93
|
+
}
|
|
80
94
|
);
|
|
81
95
|
}
|
|
82
96
|
|
|
83
97
|
function getStripeKey(): string {
|
|
84
|
-
const key = (
|
|
98
|
+
const key = (
|
|
99
|
+
typeof Deno !== 'undefined'
|
|
100
|
+
? Deno.env.get('STRIPE_SECRET_KEY')
|
|
101
|
+
: process.env.STRIPE_SECRET_KEY
|
|
102
|
+
) as string | undefined;
|
|
85
103
|
if (!key) throw new Error('Missing STRIPE_SECRET_KEY environment variable');
|
|
86
104
|
return key;
|
|
87
105
|
}
|
|
88
106
|
|
|
89
|
-
declare const Deno:
|
|
107
|
+
declare const Deno:
|
|
108
|
+
| { env: { get(key: string): string | undefined } }
|
|
109
|
+
| undefined;
|
|
@@ -92,11 +92,18 @@ export function createSupabaseAggregateEntities(
|
|
|
92
92
|
let query = supabaseAdmin.from(collection);
|
|
93
93
|
|
|
94
94
|
for (const [field, operator, value] of where) {
|
|
95
|
-
query = applyOperator(
|
|
95
|
+
query = applyOperator(
|
|
96
|
+
query,
|
|
97
|
+
mapper.toBackendField(field),
|
|
98
|
+
operator,
|
|
99
|
+
value
|
|
100
|
+
);
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
if (groupBy && groupBy.length > 0) {
|
|
99
|
-
const groupByColumns = groupBy
|
|
104
|
+
const groupByColumns = groupBy
|
|
105
|
+
.map((f) => mapper.toBackendField(f))
|
|
106
|
+
.join(', ');
|
|
100
107
|
const selectFields = `${groupByColumns}, ${aggregate.operation}(${aggregateColumn})`;
|
|
101
108
|
query = query.select(selectFields);
|
|
102
109
|
for (const field of groupBy) {
|
|
@@ -114,7 +121,10 @@ export function createSupabaseAggregateEntities(
|
|
|
114
121
|
const { data: rows, error, count } = await query;
|
|
115
122
|
|
|
116
123
|
if (error) {
|
|
117
|
-
throw new DoNotDevError(
|
|
124
|
+
throw new DoNotDevError(
|
|
125
|
+
`Failed to aggregate entities: ${error.message}`,
|
|
126
|
+
'internal'
|
|
127
|
+
);
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
// Process results
|
|
@@ -133,7 +143,7 @@ export function createSupabaseAggregateEntities(
|
|
|
133
143
|
return { value: count ?? 0 };
|
|
134
144
|
} else {
|
|
135
145
|
// Calculate aggregate from rows
|
|
136
|
-
const values = (rows || [] as any[])
|
|
146
|
+
const values = (rows || ([] as any[]))
|
|
137
147
|
.map((row) => row[aggregateColumn])
|
|
138
148
|
.filter((v) => v != null && !isNaN(Number(v)))
|
|
139
149
|
.map((v) => Number(v));
|
|
@@ -19,10 +19,7 @@ import { defaultFieldMapper } from '@donotdev/supabase';
|
|
|
19
19
|
import { createMetadata } from '../../shared/index.js';
|
|
20
20
|
import { DoNotDevError, validateDocument } from '../../shared/utils.js';
|
|
21
21
|
import { createSupabaseHandler } from '../baseFunction.js';
|
|
22
|
-
import {
|
|
23
|
-
checkIdempotency,
|
|
24
|
-
storeIdempotency,
|
|
25
|
-
} from '../utils/idempotency.js';
|
|
22
|
+
import { checkIdempotency, storeIdempotency } from '../utils/idempotency.js';
|
|
26
23
|
|
|
27
24
|
const mapper = defaultFieldMapper;
|
|
28
25
|
|
|
@@ -68,7 +65,10 @@ async function checkUniqueKeys(
|
|
|
68
65
|
uniqueKeys: UniqueKeyDefinition[],
|
|
69
66
|
isDraft: boolean,
|
|
70
67
|
supabaseAdmin: any
|
|
71
|
-
): Promise<
|
|
68
|
+
): Promise<
|
|
69
|
+
| { found: false }
|
|
70
|
+
| { found: true; existingDoc: Record<string, any>; findOrCreate: boolean }
|
|
71
|
+
> {
|
|
72
72
|
for (const uniqueKey of uniqueKeys) {
|
|
73
73
|
// Skip validation for drafts only if explicitly opted in
|
|
74
74
|
if (isDraft && uniqueKey.skipForDrafts === true) continue;
|
|
@@ -81,13 +81,19 @@ async function checkUniqueKeys(
|
|
|
81
81
|
|
|
82
82
|
let query = supabaseAdmin.from(collection).select('*');
|
|
83
83
|
for (const field of uniqueKey.fields) {
|
|
84
|
-
query = query.eq(
|
|
84
|
+
query = query.eq(
|
|
85
|
+
mapper.toBackendField(field),
|
|
86
|
+
normalizeValue(payload[field])
|
|
87
|
+
);
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
const { data: existing, error } = await query.limit(1);
|
|
88
91
|
|
|
89
92
|
if (!error && existing && existing.length > 0) {
|
|
90
|
-
const existingDoc = mapper.fromBackendRow(existing[0]) as Record<
|
|
93
|
+
const existingDoc = mapper.fromBackendRow(existing[0]) as Record<
|
|
94
|
+
string,
|
|
95
|
+
any
|
|
96
|
+
>;
|
|
91
97
|
|
|
92
98
|
if (uniqueKey.findOrCreate) {
|
|
93
99
|
return { found: true, existingDoc, findOrCreate: true };
|
|
@@ -185,9 +191,17 @@ export function createSupabaseCreateEntity(
|
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
const metadata = createMetadata(uid);
|
|
188
|
-
const snakeMetadata = mapper.toBackendKeys(
|
|
194
|
+
const snakeMetadata = mapper.toBackendKeys(
|
|
195
|
+
metadata as Record<string, unknown>
|
|
196
|
+
);
|
|
189
197
|
|
|
190
|
-
const {
|
|
198
|
+
const {
|
|
199
|
+
createdAt,
|
|
200
|
+
updatedAt,
|
|
201
|
+
created_at,
|
|
202
|
+
updated_at,
|
|
203
|
+
...payloadWithoutTimestamps
|
|
204
|
+
} = normalizedPayload;
|
|
191
205
|
const snakePayload = mapper.toBackendKeys(payloadWithoutTimestamps);
|
|
192
206
|
|
|
193
207
|
// Insert document (DB sets created_at/updated_at via triggers)
|
|
@@ -202,10 +216,15 @@ export function createSupabaseCreateEntity(
|
|
|
202
216
|
.single();
|
|
203
217
|
|
|
204
218
|
if (error) {
|
|
205
|
-
throw new DoNotDevError(
|
|
219
|
+
throw new DoNotDevError(
|
|
220
|
+
`Failed to create entity: ${error.message}`,
|
|
221
|
+
'internal'
|
|
222
|
+
);
|
|
206
223
|
}
|
|
207
224
|
|
|
208
|
-
const result = mapper.fromBackendRow(
|
|
225
|
+
const result = mapper.fromBackendRow(
|
|
226
|
+
inserted as Record<string, unknown>
|
|
227
|
+
) as Record<string, any>;
|
|
209
228
|
|
|
210
229
|
// Store result for idempotency if key provided
|
|
211
230
|
if (idempotencyKey) {
|
|
@@ -51,7 +51,11 @@ async function findReferences(
|
|
|
51
51
|
docId: string,
|
|
52
52
|
referenceMetadata?: ReferenceMetadata
|
|
53
53
|
): Promise<Array<{ collection: string; field: string; count: number }>> {
|
|
54
|
-
const references: Array<{
|
|
54
|
+
const references: Array<{
|
|
55
|
+
collection: string;
|
|
56
|
+
field: string;
|
|
57
|
+
count: number;
|
|
58
|
+
}> = [];
|
|
55
59
|
|
|
56
60
|
if (!referenceMetadata) {
|
|
57
61
|
return references;
|
|
@@ -120,7 +124,8 @@ export function createSupabaseDeleteEntity(
|
|
|
120
124
|
// Prevent deletion if required references exist
|
|
121
125
|
const requiredReferences = references.filter((ref) => {
|
|
122
126
|
const metadata = referenceMetadata?.incoming?.find(
|
|
123
|
-
(r) =>
|
|
127
|
+
(r) =>
|
|
128
|
+
r.sourceCollection === ref.collection && r.sourceField === ref.field
|
|
124
129
|
);
|
|
125
130
|
return metadata?.required === true;
|
|
126
131
|
});
|
|
@@ -144,7 +149,10 @@ export function createSupabaseDeleteEntity(
|
|
|
144
149
|
.eq('id', id);
|
|
145
150
|
|
|
146
151
|
if (error) {
|
|
147
|
-
throw new DoNotDevError(
|
|
152
|
+
throw new DoNotDevError(
|
|
153
|
+
`Failed to delete entity: ${error.message}`,
|
|
154
|
+
'internal'
|
|
155
|
+
);
|
|
148
156
|
}
|
|
149
157
|
|
|
150
158
|
return { success: true };
|
package/src/supabase/crud/get.ts
CHANGED
|
@@ -18,6 +18,12 @@ import {
|
|
|
18
18
|
} from '@donotdev/core/server';
|
|
19
19
|
import type { EntityOwnershipConfig, UserRole } from '@donotdev/core/server';
|
|
20
20
|
|
|
21
|
+
import {
|
|
22
|
+
createEntityAwareMapper,
|
|
23
|
+
defaultFieldMapper,
|
|
24
|
+
getEntityFieldNames,
|
|
25
|
+
} from '@donotdev/supabase';
|
|
26
|
+
|
|
21
27
|
import { DoNotDevError } from '../../shared/utils.js';
|
|
22
28
|
import { createSupabaseHandler } from '../baseFunction.js';
|
|
23
29
|
|
|
@@ -63,16 +69,32 @@ export function createSupabaseGetEntity(
|
|
|
63
69
|
const isAdmin = hasRoleAccess(userRole, 'admin');
|
|
64
70
|
|
|
65
71
|
// Hide drafts/deleted from non-admin users (security: hidden statuses never reach public)
|
|
66
|
-
if (
|
|
72
|
+
if (
|
|
73
|
+
!isAdmin &&
|
|
74
|
+
(HIDDEN_STATUSES as readonly string[]).includes(row.status)
|
|
75
|
+
) {
|
|
67
76
|
throw new DoNotDevError('Entity not found', 'not-found');
|
|
68
77
|
}
|
|
69
78
|
|
|
79
|
+
// Normalize DB row to entity field names before filtering.
|
|
80
|
+
const entityFieldNames = getEntityFieldNames(documentSchema);
|
|
81
|
+
const entityMapper =
|
|
82
|
+
entityFieldNames.length > 0
|
|
83
|
+
? createEntityAwareMapper(entityFieldNames)
|
|
84
|
+
: defaultFieldMapper;
|
|
85
|
+
const normalized = entityMapper.fromBackendRow(row) as Record<
|
|
86
|
+
string,
|
|
87
|
+
any
|
|
88
|
+
>;
|
|
89
|
+
|
|
70
90
|
const visibilityOptions =
|
|
71
|
-
ownership && uid
|
|
91
|
+
ownership && uid
|
|
92
|
+
? { documentData: normalized, uid, ownership }
|
|
93
|
+
: undefined;
|
|
72
94
|
|
|
73
95
|
// Filter fields based on visibility and user role (and ownership for visibility: 'owner')
|
|
74
96
|
const filteredData = filterVisibleFields(
|
|
75
|
-
|
|
97
|
+
normalized,
|
|
76
98
|
documentSchema,
|
|
77
99
|
userRole,
|
|
78
100
|
visibilityOptions
|