@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.
Files changed (50) hide show
  1. package/package.json +4 -4
  2. package/src/firebase/auth/setCustomClaims.ts +19 -5
  3. package/src/firebase/baseFunction.ts +11 -3
  4. package/src/firebase/billing/changePlan.ts +5 -1
  5. package/src/firebase/billing/createCustomerPortal.ts +6 -2
  6. package/src/firebase/billing/webhookHandler.ts +4 -1
  7. package/src/firebase/crud/aggregate.ts +5 -1
  8. package/src/firebase/crud/create.ts +17 -4
  9. package/src/firebase/crud/list.ts +9 -4
  10. package/src/firebase/crud/update.ts +17 -4
  11. package/src/firebase/oauth/exchangeToken.ts +17 -4
  12. package/src/shared/__tests__/validation.test.ts +5 -3
  13. package/src/shared/billing/__tests__/createCheckout.test.ts +123 -22
  14. package/src/shared/billing/__tests__/webhookHandler.test.ts +121 -30
  15. package/src/shared/firebase.ts +1 -1
  16. package/src/shared/logger.ts +7 -1
  17. package/src/shared/oauth/__tests__/exchangeToken.test.ts +6 -8
  18. package/src/shared/oauth/__tests__/grantAccess.test.ts +31 -14
  19. package/src/shared/utils/internal/auth.ts +10 -3
  20. package/src/shared/utils/internal/rateLimiter.ts +8 -2
  21. package/src/shared/utils.ts +5 -1
  22. package/src/supabase/auth/deleteAccount.ts +1 -1
  23. package/src/supabase/auth/getCustomClaims.ts +5 -3
  24. package/src/supabase/auth/getUserAuthStatus.ts +5 -3
  25. package/src/supabase/auth/removeCustomClaims.ts +10 -5
  26. package/src/supabase/auth/setCustomClaims.ts +9 -4
  27. package/src/supabase/baseFunction.ts +77 -22
  28. package/src/supabase/billing/cancelSubscription.ts +9 -3
  29. package/src/supabase/billing/changePlan.ts +20 -5
  30. package/src/supabase/billing/createCheckoutSession.ts +20 -5
  31. package/src/supabase/billing/createCustomerPortal.ts +14 -4
  32. package/src/supabase/billing/refreshSubscriptionStatus.ts +29 -9
  33. package/src/supabase/crud/aggregate.ts +14 -4
  34. package/src/supabase/crud/create.ts +30 -11
  35. package/src/supabase/crud/delete.ts +11 -3
  36. package/src/supabase/crud/get.ts +25 -3
  37. package/src/supabase/crud/list.ts +76 -22
  38. package/src/supabase/crud/update.ts +32 -10
  39. package/src/supabase/helpers/authProvider.ts +5 -2
  40. package/src/supabase/index.ts +1 -4
  41. package/src/supabase/registerCrudFunctions.ts +11 -9
  42. package/src/supabase/utils/idempotency.ts +13 -15
  43. package/src/supabase/utils/monitoring.ts +5 -1
  44. package/src/supabase/utils/rateLimiter.ts +13 -3
  45. package/src/vercel/api/billing/webhook-handler.ts +6 -2
  46. package/src/vercel/api/crud/create.ts +7 -2
  47. package/src/vercel/api/crud/delete.ts +3 -1
  48. package/src/vercel/api/crud/get.ts +3 -1
  49. package/src/vercel/api/crud/list.ts +3 -1
  50. 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': 'authorization, content-type, x-client-info, apikey',
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({ error: 'Missing or invalid authorization header' }, 401);
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 = getEnv('SUPABASE_SECRET_KEY') || getEnv('SUPABASE_SERVICE_ROLE_KEY');
130
+ const secretKey =
131
+ getEnv('SUPABASE_SECRET_KEY') || getEnv('SUPABASE_SERVICE_ROLE_KEY');
127
132
  if (!secretKey) {
128
- throw new Error('Missing environment variable: SUPABASE_SECRET_KEY or SUPABASE_SERVICE_ROLE_KEY');
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 { data: { user }, error: authError } = await supabaseAdmin.auth.getUser(token);
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
- { error: `Access denied. Required role: ${requiredRole}, your role: ${userRole}` },
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>)[operationName] ||
168
- DEFAULT_RATE_LIMITS.api;
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 = handlerError instanceof Error ? handlerError : new Error(String(handlerError));
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 = error instanceof Error ? error.message : 'Internal server error';
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(data: unknown, status: number, allowedOrigin = '*'): Response {
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 = (typeof Deno !== 'undefined' ? Deno.env.get(key) : process.env[key]) as string | undefined;
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 (typeof Deno !== 'undefined' ? Deno.env.get(key) : process.env[key]) as string | undefined || defaultValue;
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.split(',').map((ip) => ip.trim()).filter(Boolean);
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')) return 'not-found';
291
- if (message.includes('permission') || message.includes('denied') || message.includes('Forbidden')) return 'permission-denied';
292
- if (message.includes('mismatch') || message.includes('Invalid') || message.includes('Missing')) return 'invalid-argument';
293
- if (message.includes('already-exists') || message.includes('Duplicate')) return 'already-exists';
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')) return 404;
300
- if (message.includes('permission') || message.includes('denied') || message.includes('Forbidden')) return 403;
301
- if (message.includes('mismatch') || message.includes('Invalid') || message.includes('Missing')) return 400;
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: { env: { get(key: string): string | undefined } } | undefined;
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 = (typeof Deno !== 'undefined' ? Deno.env.get('STRIPE_SECRET_KEY') : process.env.STRIPE_SECRET_KEY) as string | undefined;
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: { env: { get(key: string): string | undefined } } | undefined;
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(v.string(), v.minLength(1, 'Billing config key is required')),
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(data.userId, data.newPriceId, data.billingConfigKey, billingConfig, authProvider);
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 = (typeof Deno !== 'undefined' ? Deno.env.get('STRIPE_SECRET_KEY') : process.env.STRIPE_SECRET_KEY) as string | undefined;
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: { env: { get(key: string): string | undefined } } | undefined;
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 { initStripe, stripe, validateStripeEnvironment } from '../../shared/utils.js';
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(data, stripeProvider, authProvider, billingConfig);
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 = (typeof Deno !== 'undefined' ? Deno.env.get('STRIPE_SECRET_KEY') : process.env.STRIPE_SECRET_KEY) as string | undefined;
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: { env: { get(key: string): string | undefined } } | undefined;
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(data.userId, authProvider, data.returnUrl);
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 = (typeof Deno !== 'undefined' ? Deno.env.get('STRIPE_SECRET_KEY') : process.env.STRIPE_SECRET_KEY) as string | undefined;
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: { env: { get(key: string): string | undefined } } | undefined;
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 { initStripe, stripe, validateStripeEnvironment } from '../../shared/utils.js';
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 = (currentClaims.subscription as { subscriptionId?: string } | undefined)?.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(subscriptionId) as Stripe.Subscription;
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((subscription as any).current_period_start * 1000).toISOString(),
65
- currentPeriodEnd: new Date((subscription as any).current_period_end * 1000).toISOString(),
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((subscription as any).current_period_end * 1000).toISOString(),
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 = (typeof Deno !== 'undefined' ? Deno.env.get('STRIPE_SECRET_KEY') : process.env.STRIPE_SECRET_KEY) as string | undefined;
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: { env: { get(key: string): string | undefined } } | undefined;
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(query, mapper.toBackendField(field), operator, value);
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.map((f) => mapper.toBackendField(f)).join(', ');
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(`Failed to aggregate entities: ${error.message}`, 'internal');
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<{ found: false } | { found: true; existingDoc: Record<string, any>; findOrCreate: boolean }> {
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(mapper.toBackendField(field), normalizeValue(payload[field]));
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<string, any>;
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(metadata as Record<string, unknown>);
194
+ const snakeMetadata = mapper.toBackendKeys(
195
+ metadata as Record<string, unknown>
196
+ );
189
197
 
190
- const { createdAt, updatedAt, created_at, updated_at, ...payloadWithoutTimestamps } = normalizedPayload;
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(`Failed to create entity: ${error.message}`, 'internal');
219
+ throw new DoNotDevError(
220
+ `Failed to create entity: ${error.message}`,
221
+ 'internal'
222
+ );
206
223
  }
207
224
 
208
- const result = mapper.fromBackendRow(inserted as Record<string, unknown>) as Record<string, any>;
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<{ collection: string; field: string; count: number }> = [];
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) => r.sourceCollection === ref.collection && r.sourceField === ref.field
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(`Failed to delete entity: ${error.message}`, 'internal');
152
+ throw new DoNotDevError(
153
+ `Failed to delete entity: ${error.message}`,
154
+ 'internal'
155
+ );
148
156
  }
149
157
 
150
158
  return { success: true };
@@ -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 (!isAdmin && (HIDDEN_STATUSES as readonly string[]).includes(row.status)) {
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 ? { documentData: row, uid, ownership } : undefined;
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
- row,
97
+ normalized,
76
98
  documentSchema,
77
99
  userRole,
78
100
  visibilityOptions