Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions graphql/env/__tests__/__snapshots__/merge.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ exports[`getEnvOptions merges pgpm defaults, graphql defaults, config, env, and
"strictAuth": false,
"trustProxy": false,
},
"sms": {
"devsms": {
"baseUrl": "http://env-devsms:4000",
},
"dryRun": true,
"provider": "devsms",
"requestTimeoutMs": 9000,
"senderId": "OverrideSender",
},
"smtp": {
"debug": false,
"logger": false,
Expand Down
124 changes: 123 additions & 1 deletion graphql/env/__tests__/merge.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getEnvOptions } from '../src/merge';
import { getGraphQLEnvVars } from '../src/env';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
Expand Down Expand Up @@ -37,6 +38,15 @@ describe('getEnvOptions', () => {
enableServicesApi: false,
isPublic: false,
metaSchemas: ['config_meta']
},
sms: {
provider: 'devsms',
senderId: 'ConfigSender',
requestTimeoutMs: 3000,
dryRun: false,
devsms: {
baseUrl: 'http://config-devsms:4000'
}
}
});

Expand All @@ -52,7 +62,12 @@ describe('getEnvOptions', () => {
API_META_SCHEMAS: 'env_meta1,env_meta2',
API_ANON_ROLE: 'env_anon',
API_ROLE_NAME: 'env_role',
API_DEFAULT_DATABASE_ID: 'env_db'
API_DEFAULT_DATABASE_ID: 'env_db',
SMS_PROVIDER: 'devsms',
SMS_SENDER_ID: 'EnvSender',
SMS_REQUEST_TIMEOUT_MS: '4000',
SEND_SMS_DRY_RUN: 'true',
DEVSMS_BASE_URL: 'http://env-devsms:4000'
};

const result = getEnvOptions(
Expand All @@ -75,6 +90,10 @@ describe('getEnvOptions', () => {
api: {
enableServicesApi: false,
defaultDatabaseId: 'override_db'
},
sms: {
senderId: 'OverrideSender',
requestTimeoutMs: 9000
}
},
tempDir,
Expand Down Expand Up @@ -121,4 +140,107 @@ describe('getEnvOptions', () => {
expect(result.api?.exposedSchemas).toEqual(['public', 'override_schema']);
expect(result.api?.metaSchemas).toEqual(['env_meta', 'override_meta']);
});

it('parses SMS environment variables into typed options', () => {
const result = getGraphQLEnvVars({
SMS_PROVIDER: 'devsms',
SMS_SENDER_ID: 'LocalSender',
SMS_REQUEST_TIMEOUT_MS: '2500',
SEND_SMS_DRY_RUN: 'true',
DEVSMS_BASE_URL: 'http://localhost:4000'
});

expect(result.sms).toEqual({
provider: 'devsms',
senderId: 'LocalSender',
requestTimeoutMs: 2500,
dryRun: true,
devsms: {
baseUrl: 'http://localhost:4000'
}
});
});

it('accepts custom SMS provider names', () => {
const result = getGraphQLEnvVars({
SMS_PROVIDER: 'custom-sms-gateway'
});

expect(result.sms?.provider).toBe('custom-sms-gateway');
});

it('honors defaults, config, env, and runtime override priority for SMS', () => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'graphql-env-sms-'));
writeConfig(tempDir, {
sms: {
provider: 'devsms',
senderId: 'ConfigSender',
requestTimeoutMs: 3000,
dryRun: false,
devsms: {
baseUrl: 'http://config-devsms:4000'
}
}
});

const result = getEnvOptions(
{
sms: {
requestTimeoutMs: 9000
}
},
tempDir,
{
SMS_SENDER_ID: 'EnvSender',
SEND_SMS_DRY_RUN: 'true',
DEVSMS_BASE_URL: 'http://env-devsms:4000'
}
);

expect(result.sms).toEqual({
provider: 'devsms',
senderId: 'EnvSender',
requestTimeoutMs: 9000,
dryRun: true,
devsms: {
baseUrl: 'http://env-devsms:4000'
}
});
});

it('uses the injected env object instead of global process.env for SMS', () => {
const previousSmsProvider = process.env.SMS_PROVIDER;
process.env.SMS_PROVIDER = 'twilio';

try {
const result = getEnvOptions({}, process.cwd(), {
SMS_PROVIDER: 'devsms'
});

expect(result.sms?.provider).toBe('devsms');
} finally {
if (previousSmsProvider === undefined) {
delete process.env.SMS_PROVIDER;
} else {
process.env.SMS_PROVIDER = previousSmsProvider;
}
}
});

it('keeps SMS provider and DevSms base URL optional while applying defaults', () => {
const result = getEnvOptions({}, process.cwd(), {});

expect(result.sms).toEqual({
requestTimeoutMs: 5000,
dryRun: false
});
});

it('uses shared number parsing behavior for invalid SMS_REQUEST_TIMEOUT_MS values', () => {
const result = getGraphQLEnvVars({
SMS_REQUEST_TIMEOUT_MS: '5s'
});

expect(result.sms?.requestTimeoutMs).toBeUndefined();
});
});
24 changes: 23 additions & 1 deletion graphql/env/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ConstructiveOptions } from '@constructive-io/graphql-types';
import type { ConstructiveOptions } from '@constructive-io/graphql-types';
import { parseEnvNumber } from '@pgpmjs/env';
import { parseEnvBoolean } from '12factor-env';

/**
Expand Down Expand Up @@ -26,6 +27,12 @@ export const getGraphQLEnvVars = (env: NodeJS.ProcessEnv = process.env): Partial
CHAT_PROVIDER,
CHAT_MODEL,
CHAT_BASE_URL,

SMS_PROVIDER,
SMS_SENDER_ID,
SMS_REQUEST_TIMEOUT_MS,
SEND_SMS_DRY_RUN,
DEVSMS_BASE_URL,
} = env;

return {
Expand Down Expand Up @@ -68,5 +75,20 @@ export const getGraphQLEnvVars = (env: NodeJS.ProcessEnv = process.env): Partial
}),
},
}),
...((SMS_PROVIDER || SMS_SENDER_ID || SMS_REQUEST_TIMEOUT_MS || SEND_SMS_DRY_RUN || DEVSMS_BASE_URL) && {
sms: {
...(SMS_PROVIDER && { provider: SMS_PROVIDER }),
...(SMS_SENDER_ID && { senderId: SMS_SENDER_ID }),
...(SMS_REQUEST_TIMEOUT_MS && {
requestTimeoutMs: parseEnvNumber(SMS_REQUEST_TIMEOUT_MS)
}),
...(SEND_SMS_DRY_RUN && { dryRun: parseEnvBoolean(SEND_SMS_DRY_RUN) }),
...(DEVSMS_BASE_URL && {
devsms: {
baseUrl: DEVSMS_BASE_URL
}
}),
},
}),
};
};
1 change: 1 addition & 0 deletions graphql/env/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Export Constructive-specific env functions
export { getEnvOptions, getConstructiveEnvOptions } from './merge';
export { getGraphQLEnvVars } from './env';
export type { DevSmsOptions, SmsOptions } from '@constructive-io/graphql-types';
3 changes: 2 additions & 1 deletion graphql/env/src/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const getEnvOptions = (
const graphqlEnvOptions = getGraphQLEnvVars(env);

// Load config again to get any GraphQL-specific config
// Config files can contain Constructive options (graphile, features, api)
// Config files can contain Constructive options (graphile, features, api, sms)
// even though loadConfigSync returns PgpmOptions type
const configOptions = loadConfigSync(cwd) as Partial<ConstructiveOptions>;

Expand All @@ -43,6 +43,7 @@ export const getEnvOptions = (
...(configOptions.graphile && { graphile: configOptions.graphile }),
...(configOptions.features && { features: configOptions.features }),
...(configOptions.api && { api: configOptions.api }),
...(configOptions.sms && { sms: configOptions.sms }),
},
graphqlEnvOptions,
overrides
Expand Down
8 changes: 7 additions & 1 deletion graphql/types/src/constructive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
apiDefaults
} from './graphile';
import { LlmOptions } from './llm';
import { SmsOptions, smsDefaults } from './sms';

/**
* GraphQL-specific options for Constructive
Expand All @@ -30,6 +31,8 @@ export interface ConstructiveGraphQLOptions {
features?: GraphileFeatureOptions;
/** API configuration options */
api?: ApiOptions;
/** SMS provider configuration */
sms?: SmsOptions;
}

/**
Expand Down Expand Up @@ -59,6 +62,8 @@ export interface ConstructiveOptions extends PgpmOptions, ConstructiveGraphQLOpt
jobs?: JobsConfig;
/** LLM provider configuration (embeddings, chat, RAG) */
llm?: LlmOptions;
/** SMS provider configuration */
sms?: SmsOptions;
}

/**
Expand All @@ -67,7 +72,8 @@ export interface ConstructiveOptions extends PgpmOptions, ConstructiveGraphQLOpt
export const constructiveGraphqlDefaults: ConstructiveGraphQLOptions = {
graphile: graphileDefaults,
features: graphileFeatureDefaults,
api: apiDefaults
api: apiDefaults,
sms: smsDefaults
};

/**
Expand Down
7 changes: 7 additions & 0 deletions graphql/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,10 @@ export {
LlmEmbedderOptions,
LlmChatOptions
} from './llm';

// Export SMS types
export {
SmsOptions,
DevSmsOptions,
smsDefaults
} from './sms';
29 changes: 29 additions & 0 deletions graphql/types/src/sms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* SMS provider configuration options for Constructive runtimes.
*
* Production providers are intentionally configuration-only here. Runtime
* packages decide which providers they implement and validate that required
* provider-specific values are present before sending.
*/
export interface DevSmsOptions {
/** Base URL for the local DevSms API, e.g. http://localhost:4000 */
baseUrl?: string;
}

export interface SmsOptions {
/** SMS provider implementation to use; runtimes may register custom names. */
provider?: string;
/** Optional sender ID/default source address for providers that support it. */
senderId?: string;
/** Outbound provider HTTP timeout in milliseconds. */
requestTimeoutMs?: number;
/** Validate/render messages without sending them to the provider. */
dryRun?: boolean;
/** DevSms local provider options. */
devsms?: DevSmsOptions;
}

export const smsDefaults: SmsOptions = {
requestTimeoutMs: 5000,
dryRun: false
};
Loading