From bb0317db3d51489e15caaa0a65ef6d756e7895fb Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 29 May 2025 20:35:22 -0700 Subject: [PATCH] cleaner API --- packages/cli/src/commands/server.ts | 67 +++++++++++++++++++++++++-- packages/server/src/middleware/api.ts | 39 +++++++++++++--- packages/types/src/env.ts | 16 +++++++ packages/types/src/launchql.ts | 27 ++++++++--- 4 files changed, 133 insertions(+), 16 deletions(-) diff --git a/packages/cli/src/commands/server.ts b/packages/cli/src/commands/server.ts index 0b992d29f6..8b201545cf 100644 --- a/packages/cli/src/commands/server.ts +++ b/packages/cli/src/commands/server.ts @@ -1,5 +1,5 @@ import { LaunchQLServer as server } from '@launchql/server'; -import { CLIOptions, Inquirerer, Question } from 'inquirerer'; +import { CLIOptions, Inquirerer, Question, OptionValue } from 'inquirerer'; import { getEnvOptions, LaunchQLOptions } from '@launchql/types'; import { getRootPgPool, Logger } from '@launchql/server-utils'; @@ -30,6 +30,14 @@ const questions: Question[] = [ default: true, useDefault: true }, + { + name: 'metaApi', + message: 'Enable Meta API?', + type: 'confirm', + required: false, + default: true, + useDefault: true + }, { name: 'port', message: 'Development server port', @@ -77,9 +85,58 @@ export default async ( oppositeBaseNames, port, postgis, - simpleInflection + simpleInflection, + metaApi } = await prompter.prompt(argv, questions); + let selectedSchemas: string[] = []; + let authRole: string | undefined; + let roleName: string | undefined; + if (!metaApi) { + const db = await getRootPgPool({ database: selectedDb }); + const result = await db.query(` + SELECT nspname + FROM pg_namespace + WHERE nspname NOT IN ('pg_catalog', 'information_schema') + ORDER BY nspname; + `); + + const schemaChoices = result.rows.map(row => ({ + name: row.nspname, + value: row.nspname, + selected: true + })); + const { schemas } = await prompter.prompt(argv, [ + { + type: 'checkbox', + name: 'schemas', + message: 'Select schemas to expose', + options: schemaChoices, + required: true + } + ]); + + selectedSchemas = (schemas as OptionValue[]).filter(s => s.selected).map(s => s.value); + const { authRole: selectedAuthRole, roleName: selectedRoleName } = await prompter.prompt(argv, [ + { + type: 'autocomplete', + name: 'authRole', + message: 'Select the authentication role', + options: ['postgres', 'authenticated', 'anonymous'], + required: true + }, + { + type: 'autocomplete', + name: 'roleName', + message: 'Enter the default role name:', + options: ['postgres', 'authenticated', 'anonymous'], + required: true + } + ]); + authRole = selectedAuthRole; + roleName = selectedRoleName; + } + const options: LaunchQLOptions = getEnvOptions({ pg: { database: selectedDb }, features: { @@ -87,10 +144,14 @@ export default async ( simpleInflection, postgis }, + api: { + enableMetaApi: metaApi, + ...(metaApi === false && { exposedSchemas: selectedSchemas, authRole, roleName }) + }, server: { port } - }); + } as LaunchQLOptions); log.success('✅ Selected Configuration:'); for (const [key, value] of Object.entries(options)) { diff --git a/packages/server/src/middleware/api.ts b/packages/server/src/middleware/api.ts index 2b9af7c0c8..3eb37ed559 100644 --- a/packages/server/src/middleware/api.ts +++ b/packages/server/src/middleware/api.ts @@ -63,8 +63,27 @@ export const getSubdomain = (reqDomains: string[]): string | null => { return !names.length ? null : names.join('.'); }; -export const createApiMiddleware = (opts: LaunchQLOptions) => { +export const createApiMiddleware = (opts: any) => { return async (req: any, res: Response, next: NextFunction): Promise => { + if (opts.api?.enableMetaApi === false) { + const schemas = opts.api.exposedSchemas; + const anonRole = opts.api.anonRole; + const roleName = opts.api.roleName; + const databaseId = opts.api.defaultDatabaseId; + const api: ApiStructure = { + dbname: opts.pg?.database ?? '', + anonRole, + roleName, + schema: schemas, + apiModules: [], + domains: [], + databaseId, + isPublic: false + }; + req.api = api; + req.databaseId = databaseId; + return next(); + } try { const svc = await getApiConfig(opts, req); @@ -135,7 +154,8 @@ const getMetaSchema = ({ key: string; databaseId: string; }): any => { - const schemata = opts.graphile.metaSchemas; + const apiOpts = (opts as any).api || {}; + const schemata = apiOpts.metaSchemas || []; const svc = { data: { api: { @@ -183,7 +203,8 @@ const queryServiceByDomainAndSubdomain = async ({ const nodes = result?.data?.domains?.nodes; if (nodes?.length) { const data = nodes[0]; - if (!data.api || data.api.isPublic !== opts.graphile.isPublic) return null; + const apiPublic = (opts as any).api?.isPublic; + if (!data.api || data.api.isPublic !== apiPublic) return null; const svc = { data }; svcCache.set(key, svc); return svc; @@ -217,7 +238,8 @@ const queryServiceByApiName = async ({ } const data = result?.data; - if (data?.api && data.api.isPublic === opts.graphile.isPublic) { + const apiPublic = (opts as any).api?.isPublic; + if (data?.api && data.api.isPublic === apiPublic) { const svc = { data }; svcCache.set(key, svc); return svc; @@ -232,7 +254,8 @@ const getSvcKey = (opts: LaunchQLOptions, req: any): string => { .concat(domain) .join('.'); - if (!opts.graphile.isPublic) { + const apiPublic = (opts as any).api?.isPublic; + if (apiPublic === false) { if (req.get('X-Api-Name')) { return 'api:' + req.get('X-Database-Id') + ':' + req.get('X-Api-Name'); } @@ -267,7 +290,8 @@ export const getApiConfig = async (opts: LaunchQLOptions, req: Request): Promise if (svcCache.has(key)) { svc = svcCache.get(key); } else { - const allSchemata = opts.graphile.metaSchemas || []; + const apiOpts = (opts as any).api || {}; + const allSchemata = apiOpts.metaSchemas || []; const validatedSchemata = await validateSchemata(rootPgPool, allSchemata); if (validatedSchemata.length === 0) { @@ -288,7 +312,8 @@ export const getApiConfig = async (opts: LaunchQLOptions, req: Request): Promise // @ts-ignore const client = new GraphileQuery({ schema, pool: rootPgPool, settings }); - if (!opts.graphile.isPublic) { + const apiPublic = (opts as any).api?.isPublic; + if (apiPublic === false) { if (req.get('X-Schemata')) { svc = getHardCodedSchemata({ opts, diff --git a/packages/types/src/env.ts b/packages/types/src/env.ts index 07db7467be..4c17120af7 100644 --- a/packages/types/src/env.ts +++ b/packages/types/src/env.ts @@ -53,6 +53,13 @@ const getEnvVars = (): LaunchQLOptions => { FEATURES_SIMPLE_INFLECTION, FEATURES_OPPOSITE_BASE_NAMES, FEATURES_POSTGIS, + API_ENABLE_META, + API_IS_PUBLIC, + API_EXPOSED_SCHEMAS, + API_META_SCHEMAS, + API_ANON_ROLE, + API_ROLE_NAME, + API_DEFAULT_DATABASE_ID, BUCKET_NAME, AWS_REGION, @@ -84,6 +91,15 @@ const getEnvVars = (): LaunchQLOptions => { ...(FEATURES_OPPOSITE_BASE_NAMES && { oppositeBaseNames: parseEnvBoolean(FEATURES_OPPOSITE_BASE_NAMES) }), ...(FEATURES_POSTGIS && { postgis: parseEnvBoolean(FEATURES_POSTGIS) }), }, + api: { + ...(API_ENABLE_META && { enableMetaApi: parseEnvBoolean(API_ENABLE_META) }), + ...(API_IS_PUBLIC && { isPublic: parseEnvBoolean(API_IS_PUBLIC) }), + ...(API_EXPOSED_SCHEMAS && { exposedSchemas: API_EXPOSED_SCHEMAS.split(',') }), + ...(API_META_SCHEMAS && { metaSchemas: API_META_SCHEMAS.split(',') }), + ...(API_ANON_ROLE && { anonRole: API_ANON_ROLE }), + ...(API_ROLE_NAME && { roleName: API_ROLE_NAME }), + ...(API_DEFAULT_DATABASE_ID && { defaultDatabaseId: API_DEFAULT_DATABASE_ID }), + }, cdn: { ...(BUCKET_NAME && { bucketName: BUCKET_NAME }), ...(AWS_REGION && { awsRegion: AWS_REGION }), diff --git a/packages/types/src/launchql.ts b/packages/types/src/launchql.ts index d8d076419b..c385c458a2 100644 --- a/packages/types/src/launchql.ts +++ b/packages/types/src/launchql.ts @@ -23,13 +23,21 @@ export interface PgTestConnectionOptions { } } +export interface ApiOptions { + enableMetaApi?: boolean; + exposedSchemas?: string[]; + anonRole?: string; + roleName?: string; + defaultDatabaseId?: string; + isPublic?: boolean; + metaSchemas?: string[]; +} + export interface LaunchQLOptions { db?: Partial; pg?: Partial; graphile?: { - isPublic?: boolean; schema?: string | string[]; - metaSchemas?: string[]; appendPlugins?: Plugin[]; graphileBuildOptions?: PostGraphileOptions['graphileBuildOptions']; overrideSettings?: Partial; @@ -46,6 +54,7 @@ export interface LaunchQLOptions { oppositeBaseNames?: boolean; postgis?: boolean; }; + api?: ApiOptions; cdn?: { bucketName?: string; awsRegion?: string; @@ -55,7 +64,6 @@ export interface LaunchQLOptions { }; } - export const launchqlDefaults: LaunchQLOptions = { db: { rootDb: 'postgres', @@ -76,9 +84,7 @@ export const launchqlDefaults: LaunchQLOptions = { database: 'postgres', }, graphile: { - isPublic: true, schema: [], - metaSchemas: ['collections_public', 'meta_public'], appendPlugins: [], overrideSettings: {}, graphileBuildOptions: {}, @@ -92,7 +98,16 @@ export const launchqlDefaults: LaunchQLOptions = { features: { simpleInflection: true, oppositeBaseNames: true, - postgis: true, + postgis: true + }, + api: { + enableMetaApi: true, + exposedSchemas: [], + anonRole: 'administrator', + roleName: 'administrator', + defaultDatabaseId: 'hard-coded', + isPublic: true, + metaSchemas: ['collections_public', 'meta_public'] }, cdn: { bucketName: 'test-bucket',