-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor endpoint records with defineEndpoint
- Loading branch information
Showing
8 changed files
with
236 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,11 @@ | ||
import { endpoints } from '@trpfrog.net/constants' | ||
import { services } from '@trpfrog.net/constants' | ||
import { hc } from 'hono/client' | ||
|
||
import { NODE_ENV } from '@/env/client.ts' | ||
|
||
import type { AppType } from './[[...route]]/route.ts' | ||
|
||
const baseUrl = typeof window === 'undefined' ? endpoints(NODE_ENV).website : window.location.origin | ||
const baseUrl = | ||
typeof window === 'undefined' ? services.website.endpoint(NODE_ENV) : window.location.origin | ||
|
||
export const bffClient = hc<AppType>(baseUrl).api |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
import { describe, test, expect, expectTypeOf } from 'vitest' | ||
|
||
import { defineEndpoints } from './defineEndpoints' | ||
|
||
describe('defineEndpoints', () => { | ||
describe('should correctly parse and return endpoints with all fields provided', () => { | ||
const endpoints = defineEndpoints({ | ||
api: { | ||
port: 3000, | ||
development: 'http://dev.api.local', | ||
production: 'https://api.example.com', | ||
}, | ||
auth: { | ||
port: 4000, | ||
development: 'http://dev.auth.local', | ||
production: 'https://auth.example.com', | ||
}, | ||
}) | ||
|
||
test('endpoints.api', () => { | ||
expect(endpoints.api.port).toBe(3000) | ||
expect(endpoints.api.development).toBe('http://dev.api.local') | ||
expect(endpoints.api.production).toBe('https://api.example.com') | ||
expect(endpoints.api.endpoint('development')).toBe('http://dev.api.local') | ||
expect(endpoints.api.endpoint('production')).toBe('https://api.example.com') | ||
expect(endpoints.api.endpoint('test')).toBe('http://dev.api.local') | ||
}) | ||
|
||
test('endpoints.api (type)', () => { | ||
expectTypeOf(endpoints.api).toEqualTypeOf<{ | ||
port: 3000 | ||
development: 'http://dev.api.local' | ||
production: 'https://api.example.com' | ||
endpoint: (env: 'development' | 'production' | 'test') => string | null | ||
}>() | ||
}) | ||
|
||
test('endpoints.auth', () => { | ||
expect(endpoints.auth.port).toBe(4000) | ||
expect(endpoints.auth.development).toBe('http://dev.auth.local') | ||
expect(endpoints.auth.production).toBe('https://auth.example.com') | ||
expect(endpoints.auth.endpoint('development')).toBe('http://dev.auth.local') | ||
expect(endpoints.auth.endpoint('production')).toBe('https://auth.example.com') | ||
expect(endpoints.auth.endpoint('test')).toBe('http://dev.auth.local') | ||
}) | ||
|
||
test('endpoints.auth (type)', () => { | ||
expectTypeOf(endpoints.auth).toEqualTypeOf<{ | ||
port: 4000 | ||
development: 'http://dev.auth.local' | ||
production: 'https://auth.example.com' | ||
endpoint: (env: 'development' | 'production' | 'test') => string | null | ||
}>() | ||
}) | ||
}) | ||
|
||
describe('should default development to localhost URL when development is missing but port is provided', () => { | ||
const endpoints = defineEndpoints({ | ||
api: { | ||
port: 3000, | ||
production: 'https://api.example.com', | ||
}, | ||
}) | ||
|
||
test('endpoints.api', () => { | ||
expect(endpoints.api.development).toBe('http://localhost:3000') | ||
expect(endpoints.api.endpoint('development')).toBe('http://localhost:3000') | ||
expect(endpoints.api.endpoint('production')).toBe('https://api.example.com') | ||
expect(endpoints.api.endpoint('test')).toBe('http://localhost:3000') | ||
}) | ||
|
||
test('endpoints.api (type)', () => { | ||
expectTypeOf(endpoints.api).toEqualTypeOf<{ | ||
port: 3000 | ||
development: 'http://localhost:3000' | ||
production: 'https://api.example.com' | ||
endpoint: (env: 'development' | 'production' | 'test') => string | null | ||
}>() | ||
}) | ||
}) | ||
|
||
describe('should default development to production when both development and port are missing', () => { | ||
const endpoints = defineEndpoints({ | ||
metrics: { | ||
production: 'https://metrics.example.com', | ||
}, | ||
}) | ||
|
||
test('endpoints.metrics', () => { | ||
expect(endpoints.metrics.development).toBe('https://metrics.example.com') | ||
expect(endpoints.metrics.production).toBe('https://metrics.example.com') | ||
expect(endpoints.metrics.endpoint('development')).toBe('https://metrics.example.com') | ||
expect(endpoints.metrics.endpoint('production')).toBe('https://metrics.example.com') | ||
expect(endpoints.metrics.endpoint('test')).toBe('https://metrics.example.com') | ||
}) | ||
|
||
test('endpoints.metrics (type)', () => { | ||
expectTypeOf(endpoints.metrics).toEqualTypeOf<{ | ||
port: undefined | ||
development: 'https://metrics.example.com' | ||
production: 'https://metrics.example.com' | ||
endpoint: (env: 'development' | 'production' | 'test') => string | null | ||
}>() | ||
}) | ||
}) | ||
|
||
describe('should handle production being null gracefully', () => { | ||
const endpoints = defineEndpoints({ | ||
api: { | ||
port: 3000, | ||
production: null, | ||
}, | ||
}) | ||
|
||
test('endpoints.api', () => { | ||
expect(endpoints.api.development).toBe('http://localhost:3000') | ||
expect(endpoints.api.production).toBe(null) | ||
expect(endpoints.api.endpoint('development')).toBe('http://localhost:3000') | ||
expect(endpoints.api.endpoint('production')).toBe(null) | ||
expect(endpoints.api.endpoint('test')).toBe('http://localhost:3000') | ||
}) | ||
|
||
test('endpoints.api (type)', () => { | ||
expectTypeOf(endpoints.api).toEqualTypeOf<{ | ||
port: 3000 | ||
development: 'http://localhost:3000' | ||
production: null | ||
endpoint: (env: 'development' | 'production' | 'test') => string | null | ||
}>() | ||
}) | ||
}) | ||
|
||
describe('should throw an error when invalid data is provided', () => { | ||
test('should throw when port is not a number', () => { | ||
expect(() => | ||
defineEndpoints({ | ||
api: { | ||
port: 'not-a-number' as unknown as number, | ||
production: 'https://api.example.com', | ||
}, | ||
}), | ||
).toThrow() | ||
}) | ||
|
||
test('should throw when development is not a valid URL', () => { | ||
expect(() => | ||
defineEndpoints({ | ||
api: { | ||
port: 3000, | ||
development: 'invalid-url', | ||
production: 'https://api.example.com', | ||
}, | ||
}), | ||
).toThrow() | ||
}) | ||
|
||
test('should throw when production is missing', () => { | ||
expect(() => | ||
defineEndpoints({ | ||
api: { | ||
port: 3000, | ||
development: 'http://dev.api.local', | ||
}, | ||
}), | ||
).toThrow() | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { z } from 'zod' | ||
|
||
const EndpointRecordSchema = z.record( | ||
z.string(), | ||
z.object({ | ||
port: z.number().nullish(), | ||
development: z.string().url().nullish(), | ||
production: z.string().url().nullish(), | ||
}), | ||
) | ||
|
||
export type EndpointRecord = z.infer<typeof EndpointRecordSchema> | ||
|
||
export function defineEndpoints<const T extends EndpointRecord>(endpoints: T) { | ||
const parsedEndpoints = EndpointRecordSchema.parse(endpoints) | ||
|
||
for (const name in parsedEndpoints) { | ||
const currentEndpoint = parsedEndpoints[name] | ||
parsedEndpoints[name].development ??= currentEndpoint.port | ||
? `http://localhost:${currentEndpoint.port}` | ||
: currentEndpoint.production | ||
|
||
// @ts-expect-error - endpoint is not typed | ||
parsedEndpoints[name].endpoint = (env: 'development' | 'production' | 'test') => { | ||
return env === 'production' | ||
? parsedEndpoints[name].production | ||
: parsedEndpoints[name].development | ||
} | ||
} | ||
|
||
return parsedEndpoints as { | ||
[K in keyof T]: { | ||
port: T[K]['port'] extends number ? T[K]['port'] : undefined | ||
development: T[K]['development'] extends string | ||
? T[K]['development'] | ||
: T[K]['port'] extends number | ||
? `http://localhost:${T[K]['port']}` | ||
: T[K]['production'] | ||
production: T[K]['production'] | ||
endpoint: (env: 'development' | 'production' | 'test') => string | null | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,75 +1,16 @@ | ||
import { z } from 'zod' | ||
import { defineEndpoints } from './defineEndpoints' | ||
|
||
const EndpointRecordSchema = z.object({ | ||
name: z.string(), | ||
port: z.number().nullable(), | ||
development: z.string().url().nullable(), | ||
production: z.string().url().nullable(), | ||
}) | ||
|
||
type EndpointRecord = z.infer<typeof EndpointRecordSchema> | ||
|
||
function createEndpointRecord<const T extends Omit<EndpointRecord, 'development'>>(record: T) { | ||
const ret = { | ||
...record, | ||
development: record.port ? `http://localhost:${record.port}` : null, | ||
} as const | ||
return EndpointRecordSchema.parse(ret) as typeof ret | ||
} | ||
|
||
// ============================== ENDPOINTS ============================== // | ||
|
||
const internalEndpoints = [ | ||
createEndpointRecord({ | ||
name: 'website', | ||
export const services = defineEndpoints({ | ||
website: { | ||
port: 3000, | ||
production: 'https://trpfrog.net', | ||
}), | ||
createEndpointRecord({ | ||
name: 'imageGeneration', | ||
}, | ||
imageGeneration: { | ||
port: 8001, | ||
production: 'https://production.trpfrog-diffusion.trpfrog.workers.dev', | ||
}), | ||
createEndpointRecord({ | ||
name: 'mdServer', | ||
}, | ||
mdServer: { | ||
port: 8002, | ||
production: null, | ||
}), | ||
] as const satisfies EndpointRecord[] | ||
|
||
// ======================================================================= // | ||
|
||
/** | ||
* The endpoints of the application. | ||
*/ | ||
export const devEndpoints = Object.fromEntries( | ||
internalEndpoints | ||
.filter(endpoint => endpoint.production == null) | ||
.map(endpoint => [ | ||
endpoint.name, | ||
// eslint-disable-next-line n/no-process-env | ||
process?.env.NODE_ENV === 'development' && endpoint.development | ||
? endpoint.development | ||
: endpoint.production, | ||
]), | ||
) | ||
|
||
export function endpoints(env: 'development' | 'production' | 'test') { | ||
return Object.fromEntries( | ||
internalEndpoints | ||
.filter(endpoint => endpoint.production != null) | ||
.map(endpoint => [ | ||
endpoint.name, | ||
env === 'production' ? endpoint.production : `http://localhost:${endpoint.port}`, | ||
]), | ||
) | ||
} | ||
|
||
/** | ||
* The ports of the backend services. | ||
*/ | ||
export const ports = Object.fromEntries( | ||
internalEndpoints | ||
.filter(endpoint => typeof endpoint.port === 'number') | ||
.map(endpoint => [endpoint.name, endpoint.port]), | ||
) | ||
}, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
export { devEndpoints, endpoints, ports } from './endpoints' | ||
export { services } from './endpoints' |