Skip to content

Commit 5466412

Browse files
committed
feat: project creation
1 parent 7af4835 commit 5466412

File tree

11 files changed

+962
-55
lines changed

11 files changed

+962
-55
lines changed

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/mcp-server-supabase/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"date-fns": "^4.1.0",
4343
"msw": "^2.7.3",
4444
"openapi-typescript": "^7.5.0",
45+
"openapi-typescript-helpers": "^0.0.15",
4546
"prettier": "^3.3.3",
4647
"tsup": "^8.3.5",
4748
"tsx": "^4.19.2",

packages/mcp-server-supabase/src/management-api/index.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import createClient, { type Client, type FetchResponse } from 'openapi-fetch';
1+
import createClient, {
2+
type Client,
3+
type FetchResponse,
4+
type ParseAsResponse,
5+
} from 'openapi-fetch';
6+
import type {
7+
MediaType,
8+
ResponseObjectMap,
9+
SuccessResponse,
10+
} from 'openapi-typescript-helpers';
211
import { z } from 'zod';
312
import type { paths } from './types.js';
413

@@ -19,15 +28,29 @@ export function createManagementApiClient(
1928

2029
export type ManagementApiClient = Client<paths>;
2130

31+
export type SuccessResponseType<
32+
T extends Record<string | number, any>,
33+
Options,
34+
Media extends MediaType,
35+
> = {
36+
data: ParseAsResponse<SuccessResponse<ResponseObjectMap<T>, Media>, Options>;
37+
error?: never;
38+
response: Response;
39+
};
40+
2241
const errorSchema = z.object({
2342
message: z.string(),
2443
});
2544

26-
export function assertManagementApiResponse(
27-
response: FetchResponse<any, any, any>,
45+
export function assertSuccess<
46+
T extends Record<string | number, any>,
47+
Options,
48+
Media extends MediaType,
49+
>(
50+
response: FetchResponse<T, Options, Media>,
2851
fallbackMessage: string
29-
) {
30-
if (!response.response.ok) {
52+
): asserts response is SuccessResponseType<T, Options, Media> {
53+
if ('error' in response) {
3154
if (response.response.status === 401) {
3255
throw new Error(
3356
'Unauthorized. Please provide a valid access token to the MCP server via the --access-token flag.'
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { generatePassword } from './password.js';
3+
4+
describe('generatePassword', () => {
5+
it('should generate a password with default options', () => {
6+
const password = generatePassword();
7+
expect(password.length).toBe(10);
8+
expect(/^[A-Za-z]+$/.test(password)).toBe(true);
9+
});
10+
11+
it('should generate a password with custom length', () => {
12+
const password = generatePassword({ length: 16 });
13+
expect(password.length).toBe(16);
14+
});
15+
16+
it('should generate a password with numbers', () => {
17+
const password = generatePassword({
18+
numbers: true,
19+
uppercase: false,
20+
lowercase: false,
21+
});
22+
expect(/[0-9]/.test(password)).toBe(true);
23+
});
24+
25+
it('should generate a password with symbols', () => {
26+
const password = generatePassword({ symbols: true });
27+
expect(/[!@#$%^&*()_+~`|}{[\]:;?><,./-=]/.test(password)).toBe(true);
28+
});
29+
30+
it('should generate a password with uppercase only', () => {
31+
const password = generatePassword({ uppercase: true, lowercase: false });
32+
expect(/^[A-Z]+$/.test(password)).toBe(true);
33+
});
34+
35+
it('should generate a password with lowercase only', () => {
36+
const password = generatePassword({ uppercase: false, lowercase: true });
37+
expect(/^[a-z]+$/.test(password)).toBe(true);
38+
});
39+
40+
it('should not generate the same password twice', () => {
41+
const password1 = generatePassword();
42+
const password2 = generatePassword();
43+
expect(password1).not.toBe(password2);
44+
});
45+
46+
it('should throw an error if no character sets are selected', () => {
47+
expect(() =>
48+
generatePassword({
49+
uppercase: false,
50+
lowercase: false,
51+
numbers: false,
52+
symbols: false,
53+
})
54+
).toThrow('at least one character set must be selected');
55+
});
56+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
const UPPERCASE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
2+
const LOWERCASE_CHARS = 'abcdefghijklmnopqrstuvwxyz';
3+
const NUMBER_CHARS = '0123456789';
4+
const SYMBOL_CHARS = '!@#$%^&*()_+~`|}{[]:;?><,./-=';
5+
6+
export type GeneratePasswordOptions = {
7+
length?: number;
8+
numbers?: boolean;
9+
uppercase?: boolean;
10+
lowercase?: boolean;
11+
symbols?: boolean;
12+
};
13+
14+
/**
15+
* Generates a cryptographically secure random password.
16+
*
17+
* @returns The generated password
18+
*/
19+
export const generatePassword = ({
20+
length = 10,
21+
numbers = false,
22+
symbols = false,
23+
uppercase = true,
24+
lowercase = true,
25+
} = {}) => {
26+
// Build the character set based on options
27+
let chars = '';
28+
if (uppercase) {
29+
chars += UPPERCASE_CHARS;
30+
}
31+
if (lowercase) {
32+
chars += LOWERCASE_CHARS;
33+
}
34+
if (numbers) {
35+
chars += NUMBER_CHARS;
36+
}
37+
if (symbols) {
38+
chars += SYMBOL_CHARS;
39+
}
40+
41+
if (chars.length === 0) {
42+
throw new Error('at least one character set must be selected');
43+
}
44+
45+
const randomValues = new Uint32Array(length);
46+
crypto.getRandomValues(randomValues);
47+
48+
// Map random values to our character set
49+
let password = '';
50+
for (let i = 0; i < length; i++) {
51+
const randomIndex = randomValues[i]! % chars.length;
52+
password += chars.charAt(randomIndex);
53+
}
54+
55+
return password;
56+
};
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { describe, expect, it } from 'vitest';
2+
import {
3+
EARTH_RADIUS,
4+
getClosestAwsRegion,
5+
getCountryCoordinates,
6+
getDistance,
7+
} from './regions.js';
8+
9+
describe('getDistance', () => {
10+
it('should return 0 for the same coordinates', () => {
11+
const point = { lat: 50, lng: 50 };
12+
expect(getDistance(point, point)).toBe(0);
13+
});
14+
15+
it('should calculate distance between two points correctly', () => {
16+
// New York City coordinates
17+
const nyc = { lat: 40.7128, lng: -74.006 };
18+
// Los Angeles coordinates
19+
const la = { lat: 34.0522, lng: -118.2437 };
20+
21+
// Approximate distance between NYC and LA is ~3940 km
22+
const distance = getDistance(nyc, la);
23+
expect(distance).toBeCloseTo(3940, -2); // Allow ~100km margin
24+
});
25+
26+
it('should handle coordinates at opposite sides of the Earth', () => {
27+
const point1 = { lat: 0, lng: 0 };
28+
const point2 = { lat: 0, lng: 180 };
29+
30+
// Half circumference of Earth
31+
const expectedDistance = Math.PI * EARTH_RADIUS;
32+
expect(getDistance(point1, point2)).toBeCloseTo(expectedDistance, 0);
33+
});
34+
35+
it('should handle negative coordinates', () => {
36+
const sydney = { lat: -33.8688, lng: 151.2093 };
37+
const buenosAires = { lat: -34.6037, lng: -58.3816 };
38+
39+
// Approximate distance between Sydney and Buenos Aires is ~11800 km
40+
const distance = getDistance(sydney, buenosAires);
41+
expect(distance).toBeCloseTo(11800, -2); // Allow ~100km margin
42+
});
43+
44+
it('should handle coordinates at the equator', () => {
45+
const point1 = { lat: 0, lng: 0 };
46+
const point2 = { lat: 0, lng: 180 };
47+
48+
const expectedDistance = Math.PI * EARTH_RADIUS; // Half circumference
49+
expect(getDistance(point1, point2)).toBeCloseTo(expectedDistance, 0);
50+
});
51+
52+
it('should be symmetrical (a to b equals b to a)', () => {
53+
const london = { lat: 51.5074, lng: -0.1278 };
54+
const tokyo = { lat: 35.6762, lng: 139.6503 };
55+
56+
const distanceAtoB = getDistance(london, tokyo);
57+
const distanceBtoA = getDistance(tokyo, london);
58+
59+
expect(distanceAtoB).toEqual(distanceBtoA);
60+
});
61+
});
62+
63+
describe('getClosestRegion', () => {
64+
it('should find the closest AWS region to a specific location', async () => {
65+
const tokyo = { lat: 35.6762, lng: 139.6503 };
66+
const result = await getClosestAwsRegion(tokyo);
67+
expect(result.code).toBe('ap-northeast-1'); // Tokyo region
68+
});
69+
70+
it('should find the correct AWS region for European location', async () => {
71+
const london = { lat: 51.5074, lng: -0.1278 };
72+
const result = await getClosestAwsRegion(london);
73+
expect(result.code).toBe('eu-west-2'); // London region
74+
});
75+
76+
it('should find the correct AWS region for US West Coast location', async () => {
77+
const sanFrancisco = { lat: 37.7749, lng: -122.4194 };
78+
const result = await getClosestAwsRegion(sanFrancisco);
79+
expect(result.code).toBe('us-west-1'); // North California region
80+
});
81+
82+
it('should find the correct AWS region for Sydney location', async () => {
83+
const sydney = { lat: -33.8688, lng: 151.2093 };
84+
const result = await getClosestAwsRegion(sydney);
85+
expect(result.code).toBe('ap-southeast-2'); // Sydney region
86+
});
87+
88+
it('should find the correct AWS region for a location in South America', async () => {
89+
const saoPaulo = { lat: -23.5505, lng: -46.6333 };
90+
const result = await getClosestAwsRegion(saoPaulo);
91+
expect(result.code).toBe('sa-east-1'); // São Paulo region
92+
});
93+
});
94+
95+
describe('getCountryCoordinates', () => {
96+
it('should throw an error for an invalid country code', async () => {
97+
const invalidCountryCode = 'INVALID_CODE';
98+
expect(() => getCountryCoordinates(invalidCountryCode)).toThrowError(
99+
/unknown location code/
100+
);
101+
});
102+
103+
it('should return coordinates for a valid country code', () => {
104+
const result = getCountryCoordinates('US');
105+
expect(result).toEqual({ lat: 38, lng: -97 });
106+
});
107+
});

0 commit comments

Comments
 (0)