Skip to content
49 changes: 44 additions & 5 deletions packages/server/src/middleware/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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') {
Expand Down Expand Up @@ -76,7 +115,7 @@ const getHardCodedSchemata = ({
.map((schemaName) => ({ schemaName }))
},
schemaNames: { nodes: [] as Array<{ schemaName: string }> },
apiModules: { nodes: [] as Array<any> }
apiModules: [] as Array<any>
}
}
};
Expand Down Expand Up @@ -106,7 +145,7 @@ const getMetaSchema = ({
nodes: schemata.map((schemaName: string) => ({ schemaName }))
},
schemaNames: { nodes: [] as Array<{ schemaName: string }> },
apiModules: { nodes: [] as Array<any> }
apiModules: [] as Array<any>
}
}
};
Expand Down Expand Up @@ -209,7 +248,7 @@ const validateSchemata = async (pool: Pool, schemata: string[]): Promise<string[
`SELECT schema_name FROM information_schema.schemata WHERE schema_name = ANY($1::text[])`,
[schemata]
);
return result.rows.map(row => row.schema_name);
return result.rows.map((row: { schema_name: string }) => row.schema_name);
};

export const getApiConfig = async (opts: LaunchQLOptions, req: Request): Promise<any> => {
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { LaunchQLOptions } from '@launchql/types';
export const createAuthenticateMiddleware = (opts: LaunchQLOptions): RequestHandler => {

return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
const api = req.apiInfo?.data?.api;
const api = req.api;
if (!api) {
res.status(500).send('Missing API info');
return;
Expand Down
70 changes: 6 additions & 64 deletions packages/server/src/middleware/cors.ts
Original file line number Diff line number Diff line change
@@ -1,80 +1,22 @@
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<string[]>((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<string[]>((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,
optionsSuccessStatus: 200
})(req, res, next);
}

const sites = api.database.sites;
const siteUrls = getSiteUrls(sites);
const siteUrls = api.domains;

const listOfDomains = corsModules.reduce<string[]>((m, mod) => {
return [...mod.data.urls, ...m];
Expand Down
14 changes: 5 additions & 9 deletions packages/server/src/middleware/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,8 @@ export const ApiQuery = gql`
}
} # for now keep this for patches
apiModules {
nodes {
name
data
}
name
data
}
}
}
Expand Down Expand Up @@ -96,10 +94,8 @@ export const ApiByNameQuery = gql`
}
} # for now keep this for patches
apiModules {
nodes {
name
data
}
name
data
}
}
}
Expand All @@ -125,4 +121,4 @@ export const ListOfAllDomainsOfDb = gql`
}
}
}
`;
`;
16 changes: 5 additions & 11 deletions packages/server/src/middleware/graphile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)!
Expand All @@ -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'
);

Expand Down Expand Up @@ -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,
Expand Down
125 changes: 125 additions & 0 deletions packages/server/src/types.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading