@donotdev/functions 0.0.10 → 0.0.12
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 +32 -8
- package/src/firebase/auth/setCustomClaims.ts +26 -4
- package/src/firebase/baseFunction.ts +43 -20
- package/src/firebase/billing/cancelSubscription.ts +9 -1
- package/src/firebase/billing/changePlan.ts +8 -2
- package/src/firebase/billing/createCheckoutSession.ts +3 -1
- package/src/firebase/billing/createCustomerPortal.ts +16 -2
- package/src/firebase/billing/refreshSubscriptionStatus.ts +3 -1
- package/src/firebase/billing/webhookHandler.ts +13 -1
- package/src/firebase/crud/aggregate.ts +20 -5
- package/src/firebase/crud/create.ts +31 -7
- package/src/firebase/crud/list.ts +36 -24
- package/src/firebase/crud/update.ts +29 -7
- package/src/firebase/oauth/exchangeToken.ts +30 -4
- package/src/firebase/oauth/githubAccess.ts +8 -3
- package/src/firebase/registerCrudFunctions.ts +2 -2
- package/src/firebase/scheduled/checkExpiredSubscriptions.ts +20 -3
- package/src/shared/__tests__/detectFirestore.test.ts +52 -0
- package/src/shared/__tests__/errorHandling.test.ts +144 -0
- package/src/shared/__tests__/idempotency.test.ts +95 -0
- package/src/shared/__tests__/rateLimiter.test.ts +142 -0
- package/src/shared/__tests__/validation.test.ts +172 -0
- package/src/shared/billing/__tests__/createCheckout.test.ts +393 -0
- package/src/shared/billing/__tests__/webhookHandler.test.ts +1091 -0
- package/src/shared/billing/webhookHandler.ts +16 -7
- package/src/shared/errorHandling.ts +22 -60
- package/src/shared/firebase.ts +1 -25
- package/src/shared/index.ts +2 -1
- package/src/shared/logger.ts +3 -7
- package/src/shared/oauth/__tests__/exchangeToken.test.ts +116 -0
- package/src/shared/oauth/__tests__/grantAccess.test.ts +237 -0
- package/src/shared/utils/__tests__/functionWrapper.test.ts +122 -0
- package/src/shared/utils/external/subscription.ts +12 -2
- package/src/shared/utils/internal/auth.ts +140 -16
- package/src/shared/utils/internal/rateLimiter.ts +101 -90
- package/src/shared/utils/internal/validation.ts +47 -3
- package/src/shared/utils.ts +170 -66
- package/src/supabase/auth/deleteAccount.ts +52 -0
- package/src/supabase/auth/getCustomClaims.ts +56 -0
- package/src/supabase/auth/getUserAuthStatus.ts +64 -0
- package/src/supabase/auth/removeCustomClaims.ts +75 -0
- package/src/supabase/auth/setCustomClaims.ts +73 -0
- package/src/supabase/baseFunction.ts +306 -0
- package/src/supabase/billing/cancelSubscription.ts +57 -0
- package/src/supabase/billing/changePlan.ts +62 -0
- package/src/supabase/billing/createCheckoutSession.ts +82 -0
- package/src/supabase/billing/createCustomerPortal.ts +58 -0
- package/src/supabase/billing/refreshSubscriptionStatus.ts +89 -0
- package/src/supabase/crud/aggregate.ts +169 -0
- package/src/supabase/crud/create.ts +225 -0
- package/src/supabase/crud/delete.ts +154 -0
- package/src/supabase/crud/get.ts +89 -0
- package/src/supabase/crud/index.ts +24 -0
- package/src/supabase/crud/list.ts +388 -0
- package/src/supabase/crud/update.ts +199 -0
- package/src/supabase/helpers/authProvider.ts +45 -0
- package/src/supabase/index.ts +73 -0
- package/src/supabase/registerCrudFunctions.ts +180 -0
- package/src/supabase/utils/idempotency.ts +141 -0
- package/src/supabase/utils/monitoring.ts +187 -0
- package/src/supabase/utils/rateLimiter.ts +216 -0
- package/src/vercel/api/auth/get-custom-claims.ts +3 -2
- package/src/vercel/api/auth/get-user-auth-status.ts +3 -2
- package/src/vercel/api/auth/remove-custom-claims.ts +3 -2
- package/src/vercel/api/auth/set-custom-claims.ts +5 -2
- package/src/vercel/api/billing/cancel.ts +2 -1
- package/src/vercel/api/billing/change-plan.ts +3 -1
- package/src/vercel/api/billing/customer-portal.ts +4 -1
- package/src/vercel/api/billing/refresh-subscription-status.ts +3 -1
- package/src/vercel/api/billing/webhook-handler.ts +24 -4
- package/src/vercel/api/crud/create.ts +14 -8
- package/src/vercel/api/crud/delete.ts +15 -6
- package/src/vercel/api/crud/get.ts +16 -8
- package/src/vercel/api/crud/list.ts +22 -10
- package/src/vercel/api/crud/update.ts +16 -10
- package/src/vercel/api/oauth/check-github-access.ts +2 -5
- package/src/vercel/api/oauth/grant-github-access.ts +1 -5
- package/src/vercel/api/oauth/revoke-github-access.ts +7 -8
- package/src/vercel/api/utils/cors.ts +13 -2
- package/src/vercel/baseFunction.ts +40 -25
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// packages/functions/src/supabase/registerCrudFunctions.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Auto-register CRUD handlers from entities for Supabase Edge Functions
|
|
5
|
+
* @description Utility to automatically generate CRUD Edge Function handlers for all entities
|
|
6
|
+
*
|
|
7
|
+
* @version 0.0.1
|
|
8
|
+
* @since 0.5.0
|
|
9
|
+
* @author AMBROISE PARK Consulting
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createSchemas, getListCardFieldNames } from '@donotdev/core/server';
|
|
13
|
+
import type { Entity } from '@donotdev/core/server';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
createSupabaseGetEntity,
|
|
17
|
+
createSupabaseCreateEntity,
|
|
18
|
+
createSupabaseUpdateEntity,
|
|
19
|
+
createSupabaseDeleteEntity,
|
|
20
|
+
createSupabaseListEntities,
|
|
21
|
+
createSupabaseAggregateEntities,
|
|
22
|
+
} from './crud/index.js';
|
|
23
|
+
|
|
24
|
+
type SupabaseHandler = (req: Request) => Promise<Response>;
|
|
25
|
+
|
|
26
|
+
interface CrudHandlers {
|
|
27
|
+
handlers: Record<string, SupabaseHandler>;
|
|
28
|
+
serve: (req: Request) => Promise<Response>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create CRUD handlers for all entities (Supabase Edge Functions)
|
|
33
|
+
* Returns handlers object + serve dispatcher function
|
|
34
|
+
*
|
|
35
|
+
* @param entities - Object of { key: Entity } (from `import * as entities from 'entities'`)
|
|
36
|
+
* @returns Object with handlers and serve dispatcher
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* import * as entities from '../_shared/entities.ts';
|
|
41
|
+
* import { createSupabaseCrudFunctions } from '@donotdev/functions/supabase';
|
|
42
|
+
*
|
|
43
|
+
* const { serve } = createSupabaseCrudFunctions(entities);
|
|
44
|
+
* Deno.serve(serve);
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function createSupabaseCrudFunctions(
|
|
48
|
+
entities: Record<string, Entity | unknown>
|
|
49
|
+
): CrudHandlers {
|
|
50
|
+
const handlers: Record<string, SupabaseHandler> = {};
|
|
51
|
+
|
|
52
|
+
for (const [key, value] of Object.entries(entities)) {
|
|
53
|
+
if (!isEntity(value)) continue;
|
|
54
|
+
|
|
55
|
+
const entity = value as Entity;
|
|
56
|
+
const col = entity.collection;
|
|
57
|
+
const schemas = createSchemas(entity);
|
|
58
|
+
const access = entity.access;
|
|
59
|
+
|
|
60
|
+
handlers[`create_${col}`] = createSupabaseCreateEntity(
|
|
61
|
+
col,
|
|
62
|
+
schemas.create,
|
|
63
|
+
access.create
|
|
64
|
+
);
|
|
65
|
+
handlers[`get_${col}`] = createSupabaseGetEntity(
|
|
66
|
+
col,
|
|
67
|
+
schemas.get,
|
|
68
|
+
access.read,
|
|
69
|
+
entity.ownership
|
|
70
|
+
);
|
|
71
|
+
handlers[`list_${col}`] = createSupabaseListEntities(
|
|
72
|
+
col,
|
|
73
|
+
schemas.get,
|
|
74
|
+
access.read,
|
|
75
|
+
entity.listFields,
|
|
76
|
+
entity.ownership,
|
|
77
|
+
false
|
|
78
|
+
);
|
|
79
|
+
handlers[`listCard_${col}`] = createSupabaseListEntities(
|
|
80
|
+
col,
|
|
81
|
+
schemas.get,
|
|
82
|
+
access.read,
|
|
83
|
+
getListCardFieldNames(entity),
|
|
84
|
+
entity.ownership,
|
|
85
|
+
true
|
|
86
|
+
);
|
|
87
|
+
handlers[`update_${col}`] = createSupabaseUpdateEntity(
|
|
88
|
+
col,
|
|
89
|
+
schemas.update,
|
|
90
|
+
access.update
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Extract reference metadata from entity if available
|
|
94
|
+
const schemaWithMeta = schemas.get as {
|
|
95
|
+
metadata?: {
|
|
96
|
+
references?: {
|
|
97
|
+
outgoing?: Array<{
|
|
98
|
+
field: string;
|
|
99
|
+
targetCollection: string;
|
|
100
|
+
required?: boolean;
|
|
101
|
+
}>;
|
|
102
|
+
incoming?: Array<{
|
|
103
|
+
sourceCollection: string;
|
|
104
|
+
sourceField: string;
|
|
105
|
+
required?: boolean;
|
|
106
|
+
}>;
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
const referenceMetadata = schemaWithMeta.metadata?.references;
|
|
111
|
+
|
|
112
|
+
handlers[`delete_${col}`] = createSupabaseDeleteEntity(
|
|
113
|
+
col,
|
|
114
|
+
access.delete,
|
|
115
|
+
referenceMetadata
|
|
116
|
+
);
|
|
117
|
+
handlers[`aggregate_${col}`] = createSupabaseAggregateEntities(
|
|
118
|
+
col,
|
|
119
|
+
schemas.get,
|
|
120
|
+
access.read
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Serve dispatcher: reads _functionName from request body, routes to correct handler
|
|
126
|
+
*/
|
|
127
|
+
const serve = async (req: Request): Promise<Response> => {
|
|
128
|
+
try {
|
|
129
|
+
const body = await req.json().catch(() => ({}));
|
|
130
|
+
const functionName = (body as Record<string, unknown>)._functionName as string;
|
|
131
|
+
|
|
132
|
+
if (!functionName) {
|
|
133
|
+
return new Response(
|
|
134
|
+
JSON.stringify({ error: 'Missing _functionName in request body' }),
|
|
135
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const handler = handlers[functionName];
|
|
140
|
+
if (!handler) {
|
|
141
|
+
return new Response(
|
|
142
|
+
JSON.stringify({ error: `Unknown function: ${functionName}` }),
|
|
143
|
+
{ status: 404, headers: { 'Content-Type': 'application/json' } }
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Remove _functionName from body before passing to handler
|
|
148
|
+
const { _functionName, ...handlerData } = body as Record<string, unknown>;
|
|
149
|
+
|
|
150
|
+
// Create new request with cleaned body
|
|
151
|
+
const handlerReq = new Request(req.url, {
|
|
152
|
+
method: req.method,
|
|
153
|
+
headers: req.headers,
|
|
154
|
+
body: JSON.stringify(handlerData),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return handler(handlerReq);
|
|
158
|
+
} 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
|
+
);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return { handlers, serve };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Type guard to check if a value is an Entity
|
|
172
|
+
*/
|
|
173
|
+
function isEntity(value: unknown): value is Entity {
|
|
174
|
+
return (
|
|
175
|
+
typeof value === 'object' &&
|
|
176
|
+
value !== null &&
|
|
177
|
+
'collection' in value &&
|
|
178
|
+
'fields' in value
|
|
179
|
+
);
|
|
180
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// packages/functions/src/supabase/utils/idempotency.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Idempotency utilities for Supabase Edge Functions
|
|
5
|
+
* @description Provides idempotency checking and storage using Postgres table
|
|
6
|
+
*
|
|
7
|
+
* @version 0.0.1
|
|
8
|
+
* @since 0.5.0
|
|
9
|
+
* @author AMBROISE PARK Consulting
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Default TTL for idempotency records (24 hours)
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_TTL_HOURS = 24;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* TTL per operation type (hours)
|
|
21
|
+
*/
|
|
22
|
+
const OPERATION_TTL: Record<string, number> = {
|
|
23
|
+
create: 24,
|
|
24
|
+
update: 12,
|
|
25
|
+
delete: 1,
|
|
26
|
+
default: 24,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if an operation has already been processed (idempotency check)
|
|
31
|
+
*
|
|
32
|
+
* @param supabaseAdmin - Supabase admin client
|
|
33
|
+
* @param idempotencyKey - Client-provided idempotency key
|
|
34
|
+
* @param operation - Operation name (e.g., 'create', 'update')
|
|
35
|
+
* @returns Cached result if found, null otherwise
|
|
36
|
+
*/
|
|
37
|
+
export async function checkIdempotency<T>(
|
|
38
|
+
supabaseAdmin: SupabaseClient,
|
|
39
|
+
idempotencyKey: string,
|
|
40
|
+
operation: string
|
|
41
|
+
): Promise<T | null> {
|
|
42
|
+
try {
|
|
43
|
+
const id = `${operation}_${idempotencyKey}`;
|
|
44
|
+
|
|
45
|
+
const { data, error } = await supabaseAdmin
|
|
46
|
+
.from('idempotency')
|
|
47
|
+
.select('result')
|
|
48
|
+
.eq('idempotency_key', idempotencyKey)
|
|
49
|
+
.eq('operation', operation)
|
|
50
|
+
.gt('expires_at', new Date().toISOString())
|
|
51
|
+
.single();
|
|
52
|
+
|
|
53
|
+
if (error || !data) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return data.result as T;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// Fail open - allow the operation if idempotency check fails
|
|
60
|
+
console.error('[idempotency] Check failed:', error);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Store the result of an operation for idempotency
|
|
67
|
+
*
|
|
68
|
+
* @param supabaseAdmin - Supabase admin client
|
|
69
|
+
* @param idempotencyKey - Client-provided idempotency key
|
|
70
|
+
* @param operation - Operation name (e.g., 'create', 'update')
|
|
71
|
+
* @param result - Operation result to cache
|
|
72
|
+
* @param uid - User ID who processed the operation
|
|
73
|
+
* @param ttlHours - Optional TTL override (defaults to operation-specific TTL)
|
|
74
|
+
*/
|
|
75
|
+
export async function storeIdempotency<T>(
|
|
76
|
+
supabaseAdmin: SupabaseClient,
|
|
77
|
+
idempotencyKey: string,
|
|
78
|
+
operation: string,
|
|
79
|
+
result: T,
|
|
80
|
+
uid: string,
|
|
81
|
+
ttlHours?: number
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
try {
|
|
84
|
+
const id = `${operation}_${idempotencyKey}`;
|
|
85
|
+
const ttl = ttlHours ?? OPERATION_TTL[operation] ?? OPERATION_TTL.default;
|
|
86
|
+
const expiresAt = new Date();
|
|
87
|
+
expiresAt.setHours(expiresAt.getHours() + ttl);
|
|
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
|
+
);
|
|
104
|
+
|
|
105
|
+
if (error) {
|
|
106
|
+
console.error('[idempotency] Store failed:', error);
|
|
107
|
+
// Don't throw - idempotency storage failure shouldn't break the operation
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('[idempotency] Store failed:', error);
|
|
111
|
+
// Don't throw - idempotency storage failure shouldn't break the operation
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Clean up expired idempotency records
|
|
117
|
+
*
|
|
118
|
+
* @param supabaseAdmin - Supabase admin client
|
|
119
|
+
* @returns Number of deleted records
|
|
120
|
+
*/
|
|
121
|
+
export async function cleanupExpiredIdempotency(
|
|
122
|
+
supabaseAdmin: SupabaseClient
|
|
123
|
+
): Promise<number> {
|
|
124
|
+
try {
|
|
125
|
+
const { data, error } = await supabaseAdmin
|
|
126
|
+
.from('idempotency')
|
|
127
|
+
.delete()
|
|
128
|
+
.lt('expires_at', new Date().toISOString())
|
|
129
|
+
.select('id');
|
|
130
|
+
|
|
131
|
+
if (error) {
|
|
132
|
+
console.error('[idempotency] Cleanup failed:', error);
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return data?.length ?? 0;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('[idempotency] Cleanup failed:', error);
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// packages/functions/src/supabase/utils/monitoring.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Monitoring and metrics utilities for Supabase Edge Functions
|
|
5
|
+
* @description Provides metrics collection and analytics queries using Postgres table
|
|
6
|
+
*
|
|
7
|
+
* @version 0.0.1
|
|
8
|
+
* @since 0.5.0
|
|
9
|
+
* @author AMBROISE PARK Consulting
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Operation metrics data structure
|
|
16
|
+
*/
|
|
17
|
+
export interface OperationMetrics {
|
|
18
|
+
operation: string;
|
|
19
|
+
userId?: string;
|
|
20
|
+
status: 'success' | 'failed' | 'pending';
|
|
21
|
+
durationMs?: number;
|
|
22
|
+
metadata?: Record<string, any>;
|
|
23
|
+
errorCode?: string;
|
|
24
|
+
errorMessage?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Record operation metrics for monitoring
|
|
29
|
+
*
|
|
30
|
+
* @param supabaseAdmin - Supabase admin client
|
|
31
|
+
* @param metrics - Operation metrics to record
|
|
32
|
+
*/
|
|
33
|
+
export async function recordOperationMetrics(
|
|
34
|
+
supabaseAdmin: SupabaseClient,
|
|
35
|
+
metrics: OperationMetrics
|
|
36
|
+
): Promise<void> {
|
|
37
|
+
try {
|
|
38
|
+
const { error } = await supabaseAdmin.from('operation_metrics').insert({
|
|
39
|
+
operation: metrics.operation,
|
|
40
|
+
user_id: metrics.userId || null,
|
|
41
|
+
status: metrics.status,
|
|
42
|
+
duration_ms: metrics.durationMs || null,
|
|
43
|
+
metadata: metrics.metadata || null,
|
|
44
|
+
error_code: metrics.errorCode || null,
|
|
45
|
+
error_message: metrics.errorMessage || null,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (error) {
|
|
49
|
+
console.error('[monitoring] Record failed:', error);
|
|
50
|
+
// Don't throw - metrics failure shouldn't break the operation
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('[monitoring] Record failed:', error);
|
|
54
|
+
// Don't throw - metrics failure shouldn't break the operation
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get failure rate for an operation
|
|
60
|
+
*
|
|
61
|
+
* @param supabaseAdmin - Supabase admin client
|
|
62
|
+
* @param operation - Operation name
|
|
63
|
+
* @param hours - Time window in hours (default: 24)
|
|
64
|
+
* @returns Failure rate (0-100)
|
|
65
|
+
*/
|
|
66
|
+
export async function getFailureRate(
|
|
67
|
+
supabaseAdmin: SupabaseClient,
|
|
68
|
+
operation: string,
|
|
69
|
+
hours: number = 24
|
|
70
|
+
): Promise<number> {
|
|
71
|
+
try {
|
|
72
|
+
const cutoffTime = new Date();
|
|
73
|
+
cutoffTime.setHours(cutoffTime.getHours() - hours);
|
|
74
|
+
|
|
75
|
+
const { data, error } = await supabaseAdmin
|
|
76
|
+
.from('operation_metrics')
|
|
77
|
+
.select('status')
|
|
78
|
+
.eq('operation', operation)
|
|
79
|
+
.gte('timestamp', cutoffTime.toISOString());
|
|
80
|
+
|
|
81
|
+
if (error || !data || data.length === 0) {
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const failed = data.filter((m) => m.status === 'failed').length;
|
|
86
|
+
return (failed / data.length) * 100;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('[monitoring] Get failure rate failed:', error);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get operation counts by user
|
|
95
|
+
*
|
|
96
|
+
* @param supabaseAdmin - Supabase admin client
|
|
97
|
+
* @param userId - User ID
|
|
98
|
+
* @param hours - Time window in hours (default: 24)
|
|
99
|
+
* @returns Record of operation names to counts
|
|
100
|
+
*/
|
|
101
|
+
export async function getOperationCounts(
|
|
102
|
+
supabaseAdmin: SupabaseClient,
|
|
103
|
+
userId: string,
|
|
104
|
+
hours: number = 24
|
|
105
|
+
): Promise<Record<string, number>> {
|
|
106
|
+
try {
|
|
107
|
+
const cutoffTime = new Date();
|
|
108
|
+
cutoffTime.setHours(cutoffTime.getHours() - hours);
|
|
109
|
+
|
|
110
|
+
const { data, error } = await supabaseAdmin
|
|
111
|
+
.from('operation_metrics')
|
|
112
|
+
.select('operation')
|
|
113
|
+
.eq('user_id', userId)
|
|
114
|
+
.gte('timestamp', cutoffTime.toISOString());
|
|
115
|
+
|
|
116
|
+
if (error || !data) {
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const counts: Record<string, number> = {};
|
|
121
|
+
for (const metric of data) {
|
|
122
|
+
counts[metric.operation] = (counts[metric.operation] || 0) + 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return counts;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error('[monitoring] Get operation counts failed:', error);
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get slow operations (above threshold)
|
|
134
|
+
*
|
|
135
|
+
* @param supabaseAdmin - Supabase admin client
|
|
136
|
+
* @param thresholdMs - Duration threshold in milliseconds
|
|
137
|
+
* @param hours - Time window in hours (default: 24)
|
|
138
|
+
* @returns Array of operations with average duration
|
|
139
|
+
*/
|
|
140
|
+
export async function getSlowOperations(
|
|
141
|
+
supabaseAdmin: SupabaseClient,
|
|
142
|
+
thresholdMs: number,
|
|
143
|
+
hours: number = 24
|
|
144
|
+
): Promise<Array<{ operation: string; avgDuration: number; count: number }>> {
|
|
145
|
+
try {
|
|
146
|
+
const cutoffTime = new Date();
|
|
147
|
+
cutoffTime.setHours(cutoffTime.getHours() - hours);
|
|
148
|
+
|
|
149
|
+
const { data, error } = await supabaseAdmin
|
|
150
|
+
.from('operation_metrics')
|
|
151
|
+
.select('operation, duration_ms')
|
|
152
|
+
.gte('timestamp', cutoffTime.toISOString())
|
|
153
|
+
.not('duration_ms', 'is', null);
|
|
154
|
+
|
|
155
|
+
if (error || !data) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Group by operation and calculate average
|
|
160
|
+
const grouped: Record<string, { sum: number; count: number }> = {};
|
|
161
|
+
for (const metric of data) {
|
|
162
|
+
if (!metric.duration_ms) continue;
|
|
163
|
+
if (!grouped[metric.operation]) {
|
|
164
|
+
grouped[metric.operation] = { sum: 0, count: 0 };
|
|
165
|
+
}
|
|
166
|
+
grouped[metric.operation].sum += metric.duration_ms;
|
|
167
|
+
grouped[metric.operation].count += 1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const results: Array<{ operation: string; avgDuration: number; count: number }> = [];
|
|
171
|
+
for (const [operation, stats] of Object.entries(grouped)) {
|
|
172
|
+
const avgDuration = stats.sum / stats.count;
|
|
173
|
+
if (avgDuration >= thresholdMs) {
|
|
174
|
+
results.push({
|
|
175
|
+
operation,
|
|
176
|
+
avgDuration: Math.round(avgDuration),
|
|
177
|
+
count: stats.count,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return results.sort((a, b) => b.avgDuration - a.avgDuration);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error('[monitoring] Get slow operations failed:', error);
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
}
|