diff --git a/packages/server/src/middleware/api.ts b/packages/server/src/middleware/api.ts index c315a472c9..68e53f7150 100644 --- a/packages/server/src/middleware/api.ts +++ b/packages/server/src/middleware/api.ts @@ -9,6 +9,44 @@ import { LaunchQLOptions } from '@launchql/types'; import { Response, Request, NextFunction } from 'express'; import { Pool } from 'pg'; +/** + * Transforms the old service structure to the new api structure + */ +import { Service, ApiStructure, SchemaNode, Domain, Site } from '../types'; + +const transformServiceToApi = (svc: Service): ApiStructure => { + const api = svc.data.api; + const schemaNames = api.schemaNamesFromExt?.nodes?.map((n: SchemaNode) => n.schemaName) || []; + const additionalSchemas = api.schemaNames?.nodes?.map((n: SchemaNode) => n.schemaName) || []; + + let domains: string[] = []; + if (api.database?.sites?.nodes) { + domains = api.database.sites.nodes.reduce((acc: string[], site: Site) => { + if (site.domains?.nodes && site.domains.nodes.length) { + const siteUrls = site.domains.nodes.map((domain: Domain) => { + const hostname = domain.subdomain ? `${domain.subdomain}.${domain.domain}` : domain.domain; + const protocol = domain.domain === 'localhost' ? 'http://' : 'https://'; + return protocol + hostname; + }); + return [...acc, ...siteUrls]; + } + return acc; + }, []); + } + + return { + dbname: api.dbname, + anonRole: api.anonRole, + roleName: api.roleName, + schema: [...schemaNames, ...additionalSchemas], + apiModules: api.apiModules || [], + rlsModule: api.rlsModule, + domains, + databaseId: api.databaseId, + isPublic: api.isPublic + }; +}; + const getPortFromRequest = (req: Request): string | null => { const host = req.headers.host; if (!host) return null; @@ -34,8 +72,9 @@ export const createApiMiddleware = (opts: LaunchQLOptions) => { res.status(404).send(errorPage404Message('API service not found for the given domain/subdomain.')); return; } - req.apiInfo = svc; - req.databaseId = svc.data.api.databaseId; + const api = transformServiceToApi(svc); + req.api = api; + req.databaseId = api.databaseId; next(); } catch (e: any) { if (e.code === 'NO_VALID_SCHEMAS') { @@ -76,7 +115,7 @@ const getHardCodedSchemata = ({ .map((schemaName) => ({ schemaName })) }, schemaNames: { nodes: [] as Array<{ schemaName: string }> }, - apiModules: { nodes: [] as Array } + apiModules: [] as Array } } }; @@ -106,7 +145,7 @@ const getMetaSchema = ({ nodes: schemata.map((schemaName: string) => ({ schemaName })) }, schemaNames: { nodes: [] as Array<{ schemaName: string }> }, - apiModules: { nodes: [] as Array } + apiModules: [] as Array } } }; @@ -209,7 +248,7 @@ const validateSchemata = async (pool: Pool, schemata: string[]): Promise row.schema_name); + return result.rows.map((row: { schema_name: string }) => row.schema_name); }; export const getApiConfig = async (opts: LaunchQLOptions, req: Request): Promise => { diff --git a/packages/server/src/middleware/auth.ts b/packages/server/src/middleware/auth.ts index ed33d5a099..bb3b2812b3 100644 --- a/packages/server/src/middleware/auth.ts +++ b/packages/server/src/middleware/auth.ts @@ -6,7 +6,7 @@ import { LaunchQLOptions } from '@launchql/types'; export const createAuthenticateMiddleware = (opts: LaunchQLOptions): RequestHandler => { return async (req: Request, res: Response, next: NextFunction): Promise => { - const api = req.apiInfo?.data?.api; + const api = req.api; if (!api) { res.status(500).send('Missing API info'); return; diff --git a/packages/server/src/middleware/cors.ts b/packages/server/src/middleware/cors.ts index 428d3fcc4b..0fce6126d1 100644 --- a/packages/server/src/middleware/cors.ts +++ b/packages/server/src/middleware/cors.ts @@ -1,71 +1,14 @@ import corsPlugin from 'cors'; import { parseUrl } from '@launchql/url-domains'; import { Request, Response, NextFunction } from 'express'; +import { CorsModuleData } from '../types'; -interface Domain { - subdomain?: string; - domain: string; -} - -interface SiteDomainNode { - nodes: Domain[]; -} - -interface Site { - domains: SiteDomainNode; -} - -interface Sites { - nodes?: Site[]; -} - -interface ApiModule { - name: string; - data: { - urls: string[]; - }; -} - -interface ApiInfo { - data: { - api: { - apiModules: { - nodes: ApiModule[]; - }; - database?: { - sites: Sites; - }; - }; - }; -} - -const getUrlsFromDomains = (domains: Domain[]): string[] => { - return domains.reduce((m, { subdomain, domain }) => { - const hostname = subdomain ? `${subdomain}.${domain}` : domain; - const protocol = domain === 'localhost' ? 'http://' : 'https://'; - return [...m, protocol + hostname]; - }, []); -}; - -const getSiteUrls = (sites: Sites): string[] => { - let siteUrls: string[] = []; - if (sites.nodes) { - siteUrls = sites.nodes.reduce((m, site) => { - if (site.domains.nodes && site.domains.nodes.length) { - return [...m, ...getUrlsFromDomains(site.domains.nodes)]; - } - return m; - }, []); - } - return siteUrls; -}; - -export const cors = async (req: Request & { apiInfo: ApiInfo }, res: Response, next: NextFunction) => { - const api = req.apiInfo.data.api; - const corsModules = api.apiModules.nodes.filter((mod) => mod.name === 'cors'); +export const cors = async (req: Request, res: Response, next: NextFunction) => { + const api = req.api; + const corsModules = api.apiModules.filter((mod) => mod.name === 'cors') as { name: 'cors'; data: CorsModuleData }[]; let corsOptions = { origin: false as boolean | string | RegExp | (string | RegExp)[] }; // default: disabled - if (!api.database?.sites) { + if (!api.domains || api.domains.length === 0) { return corsPlugin({ ...corsOptions, credentials: true, @@ -73,8 +16,7 @@ export const cors = async (req: Request & { apiInfo: ApiInfo }, res: Response, n })(req, res, next); } - const sites = api.database.sites; - const siteUrls = getSiteUrls(sites); + const siteUrls = api.domains; const listOfDomains = corsModules.reduce((m, mod) => { return [...mod.data.urls, ...m]; diff --git a/packages/server/src/middleware/gql.ts b/packages/server/src/middleware/gql.ts index 651778c18f..66fe68bd2b 100644 --- a/packages/server/src/middleware/gql.ts +++ b/packages/server/src/middleware/gql.ts @@ -45,10 +45,8 @@ export const ApiQuery = gql` } } # for now keep this for patches apiModules { - nodes { - name - data - } + name + data } } } @@ -96,10 +94,8 @@ export const ApiByNameQuery = gql` } } # for now keep this for patches apiModules { - nodes { - name - data - } + name + data } } } @@ -125,4 +121,4 @@ export const ListOfAllDomainsOfDb = gql` } } } -`; \ No newline at end of file +`; diff --git a/packages/server/src/middleware/graphile.ts b/packages/server/src/middleware/graphile.ts index 816516cfe9..d32ffd081b 100644 --- a/packages/server/src/middleware/graphile.ts +++ b/packages/server/src/middleware/graphile.ts @@ -10,15 +10,9 @@ export const graphile = (lOpts: LaunchQLOptions): RequestHandler => { // @ts-ignore return async (req: Request, res: Response, next: NextFunction) => { try { - const api = req.apiInfo.data.api; + const api = req.api; const key = req.svc_key; - const { dbname } = api; - const { anonRole, roleName } = api; - - const { schemaNamesFromExt, schemaNames } = api; - const schemas = [] - .concat(schemaNamesFromExt.nodes.map(({ schemaName }: any) => schemaName)) - .concat(schemaNames.nodes.map(({ schemaName }: any) => schemaName)); + const { dbname, anonRole, roleName, schema } = api; if (graphileCache.has(key)) { const { handler } = graphileCache.get(key)! @@ -29,11 +23,11 @@ export const graphile = (lOpts: LaunchQLOptions): RequestHandler => { ...lOpts, graphile: { ...lOpts.graphile, - schema: schemas + schema: schema } }); - const pubkey_challenge = api.apiModules.nodes.find( + const pubkey_challenge = api.apiModules.find( (mod: any) => mod.name === 'pubkey_challenge' ); @@ -87,7 +81,7 @@ export const graphile = (lOpts: LaunchQLOptions): RequestHandler => { ...lOpts.pg, database: dbname }); - const handler = postgraphile(pgPool, schemas, opts); + const handler = postgraphile(pgPool, schema, opts); graphileCache.set(key, { pgPool, diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts new file mode 100644 index 0000000000..85691e95b5 --- /dev/null +++ b/packages/server/src/types.ts @@ -0,0 +1,125 @@ +import { PostGraphileOptions } from 'postgraphile'; +import type { Plugin } from 'graphile-build'; + +export interface CorsModuleData { + urls: string[]; +} + +export interface PublicKeyChallengeData { + schema: string; + crypto_network: string; + sign_up_with_key: string; + sign_in_request_challenge: string; + sign_in_record_failure: string; + sign_in_with_challenge: string; +} + +export interface GenericModuleData { + [key: string]: any; +} + +export type ApiModule = + | { name: 'cors'; data: CorsModuleData } + | { name: 'pubkey_challenge'; data: PublicKeyChallengeData } + | { name: string; data?: GenericModuleData }; + +export interface RlsModule { + authenticate?: string; + authenticateStrict?: string; + privateSchema: { + schemaName: string; + }; +} + +declare module 'express-serve-static-core' { + interface Request { + api: { + dbname: string; + anonRole: string; + roleName: string; + schema: string[]; // Pre-processed schema names + apiModules: ApiModule[]; + rlsModule?: { + authenticate?: string; + authenticateStrict?: string; + privateSchema: { + schemaName: string; + }; + }; + domains?: string[]; // Simplified from database.sites.nodes + databaseId?: string; + isPublic?: boolean; + }; + svc_key: string; + clientIp?: string; + databaseId?: string; + token?: { + id: string; + user_id: string; + [key: string]: any; + }; + } +} + +export interface SchemaNode { + schemaName: string; +} + +export interface SchemaNodes { + nodes: SchemaNode[]; +} + +export interface Domain { + subdomain?: string; + domain: string; +} + +export interface DomainNodes { + nodes: Domain[]; +} + +export interface Site { + domains: DomainNodes; +} + +export interface SiteNodes { + nodes: Site[]; +} + +export interface Database { + sites: SiteNodes; +} + + +export interface OldApiStructure { + dbname: string; + anonRole: string; + roleName: string; + schemaNames: SchemaNodes; + schemaNamesFromExt: SchemaNodes; + apiModules: ApiModule[]; + rlsModule?: RlsModule; + database?: Database; + databaseId?: string; + isPublic?: boolean; +} + +export interface ServiceData { + api: OldApiStructure; +} + +export interface Service { + data: ServiceData; +} + +export interface ApiStructure { + dbname: string; + anonRole: string; + roleName: string; + schema: string[]; + apiModules: ApiModule[]; + rlsModule?: RlsModule; + domains?: string[]; + databaseId?: string; + isPublic?: boolean; +} diff --git a/packages/types/src/launchql.ts b/packages/types/src/launchql.ts index 1fae477fac..d8d076419b 100644 --- a/packages/types/src/launchql.ts +++ b/packages/types/src/launchql.ts @@ -1,46 +1,6 @@ import { PostGraphileOptions } from 'postgraphile'; import type { Plugin } from 'graphile-build'; import { execSync } from 'child_process'; -declare module 'express-serve-static-core' { - interface Request { - apiInfo: { - data: { - api: { - dbname: string; - anonRole: string; - roleName: string; - schemaNames: { - nodes: { schemaName: string }[]; - }; - schemaNamesFromExt: { - nodes: { schemaName: string }[]; - }; - apiModules: { - nodes: { - name: string; - data?: any; - }[]; - }; - rlsModule?: { - authenticate?: string; - authenticateStrict?: string; - privateSchema: { - schemaName: string; - }; - }; - }; - }; - }; - svc_key: string; - clientIp?: string; - databaseId?: string; - token?: { - id: string; - user_id: string; - [key: string]: any; - }; - } -} export interface PgConfig { host: string;