@facetlayer/prism-framework 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +20 -0
- package/CHANGELOG +28 -0
- package/CLAUDE.md +44 -0
- package/README.md +47 -0
- package/build.mts +8 -0
- package/dist/call-command.d.ts +13 -0
- package/dist/call-command.d.ts.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +475 -0
- package/dist/config/ConfigFile.d.ts +7 -0
- package/dist/config/ConfigFile.d.ts.map +1 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/loadConfig.d.ts +11 -0
- package/dist/config/loadConfig.d.ts.map +1 -0
- package/dist/generate-api-clients.d.ts +6 -0
- package/dist/generate-api-clients.d.ts.map +1 -0
- package/dist/getPorts.d.ts +10 -0
- package/dist/getPorts.d.ts.map +1 -0
- package/dist/list-endpoints-command.d.ts +5 -0
- package/dist/list-endpoints-command.d.ts.map +1 -0
- package/dist/loadEnv.d.ts +12 -0
- package/dist/loadEnv.d.ts.map +1 -0
- package/docs/endpoint-tools.md +116 -0
- package/docs/env-files.md +64 -0
- package/docs/generate-api-clients-config.md +84 -0
- package/docs/getting-started.md +86 -0
- package/package.json +43 -0
- package/src/call-command.ts +147 -0
- package/src/cli.ts +163 -0
- package/src/config/ConfigFile.ts +7 -0
- package/src/config/index.ts +3 -0
- package/src/config/loadConfig.ts +58 -0
- package/src/generate-api-clients.ts +203 -0
- package/src/getPorts.ts +39 -0
- package/src/list-endpoints-command.ts +34 -0
- package/src/loadEnv.ts +59 -0
- package/test/call-command.test.ts +96 -0
- package/test/generate-api-clients.test.ts +33 -0
- package/test/generate-api-clients.test.ts.disabled +75 -0
- package/tsconfig.json +21 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import yargs from 'yargs';
|
|
4
|
+
import { hideBin } from 'yargs/helpers';
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { loadEnv } from './loadEnv.ts';
|
|
9
|
+
import { callEndpoint } from './call-command.ts';
|
|
10
|
+
import { listEndpoints } from './list-endpoints-command.ts';
|
|
11
|
+
import { generateApiClients } from './generate-api-clients.ts';
|
|
12
|
+
import { loadConfig } from './config/index.ts';
|
|
13
|
+
import { DocFilesHelper } from '@facetlayer/doc-files-helper';
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
const __packageRoot = join(__dirname, '..');
|
|
18
|
+
const packageJson = JSON.parse(
|
|
19
|
+
readFileSync(join(__packageRoot, 'package.json'), 'utf-8')
|
|
20
|
+
);
|
|
21
|
+
const docFiles = new DocFilesHelper({
|
|
22
|
+
dirs: [join( __packageRoot, 'docs')],
|
|
23
|
+
files: [join(__packageRoot, 'README.md')],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
async function main() {
|
|
27
|
+
|
|
28
|
+
const args = yargs(hideBin(process.argv))
|
|
29
|
+
.command(
|
|
30
|
+
'list-endpoints',
|
|
31
|
+
'List all available endpoints',
|
|
32
|
+
{},
|
|
33
|
+
async () => {
|
|
34
|
+
try {
|
|
35
|
+
const cwd = process.cwd();
|
|
36
|
+
const config = loadEnv(cwd);
|
|
37
|
+
console.log(`Using API server at: ${config.baseUrl}\n`);
|
|
38
|
+
await listEndpoints(config.baseUrl);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error instanceof Error && error.stack) {
|
|
41
|
+
console.error(error.stack);
|
|
42
|
+
} else {
|
|
43
|
+
console.error('Error:', error instanceof Error ? error.message : String(error));
|
|
44
|
+
}
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
.command(
|
|
50
|
+
'call <positionals...>',
|
|
51
|
+
'Call an endpoint. Named args starting with { } or [ ] are parsed as JSON.',
|
|
52
|
+
(yargs) => {
|
|
53
|
+
return yargs
|
|
54
|
+
},
|
|
55
|
+
async (argv) => {
|
|
56
|
+
try {
|
|
57
|
+
const cwd = process.cwd();
|
|
58
|
+
const config = loadEnv(cwd);
|
|
59
|
+
|
|
60
|
+
// Collect all other arguments as request body data
|
|
61
|
+
const requestData: Record<string, any> = {};
|
|
62
|
+
for (const [key, value] of Object.entries(argv)) {
|
|
63
|
+
|
|
64
|
+
// Skip known options
|
|
65
|
+
if (['positionals', '_', '$0'].includes(key)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
requestData[key] = value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const result = await callEndpoint({
|
|
73
|
+
baseUrl: config.baseUrl,
|
|
74
|
+
positionalArgs: argv.positionals as string[],
|
|
75
|
+
namedArgs: requestData,
|
|
76
|
+
});
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Error calling endpoint:');
|
|
79
|
+
if (error instanceof Error) {
|
|
80
|
+
console.error(error.message);
|
|
81
|
+
if (error.stack) {
|
|
82
|
+
console.error('\nStack trace:');
|
|
83
|
+
console.error(error.stack);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
console.error(String(error));
|
|
87
|
+
}
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error instanceof Error && error.stack) {
|
|
92
|
+
console.error(error.stack);
|
|
93
|
+
} else {
|
|
94
|
+
console.error('Error:', error instanceof Error ? error.message : String(error));
|
|
95
|
+
}
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
.command(
|
|
101
|
+
'generate-api-clients',
|
|
102
|
+
'Generate TypeScript API client types from OpenAPI schema',
|
|
103
|
+
(yargs) => {
|
|
104
|
+
return yargs.option('out', {
|
|
105
|
+
type: 'string',
|
|
106
|
+
array: true,
|
|
107
|
+
describe: 'Output file path(s) to write generated types to',
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
async (argv) => {
|
|
111
|
+
try {
|
|
112
|
+
const cwd = process.cwd();
|
|
113
|
+
const envConfig = loadEnv(cwd);
|
|
114
|
+
console.log(`Using API server at: ${envConfig.baseUrl}\n`);
|
|
115
|
+
|
|
116
|
+
let outputFiles: string[] = [];
|
|
117
|
+
|
|
118
|
+
if (argv.out && argv.out.length > 0) {
|
|
119
|
+
outputFiles = argv.out;
|
|
120
|
+
} else {
|
|
121
|
+
const result = loadConfig(cwd);
|
|
122
|
+
if (!result || result.config.generateApiClientTargets.length === 0) {
|
|
123
|
+
console.error('Error: No output files specified.');
|
|
124
|
+
console.error('Either use --out to specify output files, or configure targets in .prism.qc:');
|
|
125
|
+
console.error(' generate-api-client-target out=./src/api-types.ts');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
outputFiles = result.config.generateApiClientTargets.map(t => t.outputFile);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await generateApiClients(envConfig.baseUrl, outputFiles);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error instanceof Error && error.stack) {
|
|
134
|
+
console.error(error.stack);
|
|
135
|
+
} else {
|
|
136
|
+
console.error('Error:', error instanceof Error ? error.message : String(error));
|
|
137
|
+
}
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
docFiles.yargsSetup(args);
|
|
144
|
+
|
|
145
|
+
args
|
|
146
|
+
.strictCommands()
|
|
147
|
+
.demandCommand(1, 'You must specify a command')
|
|
148
|
+
.help()
|
|
149
|
+
.alias('help', 'h')
|
|
150
|
+
.version(packageJson.version)
|
|
151
|
+
.alias('version', 'v')
|
|
152
|
+
.example([
|
|
153
|
+
['$0 list-endpoints', 'List all available endpoints'],
|
|
154
|
+
['$0 call /api/users', 'Call GET /api/users'],
|
|
155
|
+
['$0 call POST /api/users --name "John" --email "john@example.com"', 'call POST with data'],
|
|
156
|
+
['$0 call POST /api/users --config \'{"timeout":30}\'', 'pass JSON objects as args'],
|
|
157
|
+
['$0 generate-api-clients --out ./api-types.ts', 'Generate API client types to a file'],
|
|
158
|
+
['$0 generate-api-clients --out ./types.ts --out ./backup/types.ts', 'Write to multiple files'],
|
|
159
|
+
])
|
|
160
|
+
.parse();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
main();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { dirname, join, resolve } from 'path';
|
|
3
|
+
import { parseFile } from '@facetlayer/qc';
|
|
4
|
+
import type { ConfigFile, GenerateApiClientTarget } from './ConfigFile.ts';
|
|
5
|
+
|
|
6
|
+
const CONFIG_FILENAME = '.prism.qc';
|
|
7
|
+
|
|
8
|
+
export interface LoadConfigResult {
|
|
9
|
+
config: ConfigFile;
|
|
10
|
+
configDir: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find and load the .prism.qc config file.
|
|
15
|
+
* Searches in the provided directory and parent directories until found or root is reached.
|
|
16
|
+
*/
|
|
17
|
+
export function loadConfig(cwd: string): LoadConfigResult | null {
|
|
18
|
+
let currentDir = resolve(cwd);
|
|
19
|
+
|
|
20
|
+
while (true) {
|
|
21
|
+
const configPath = join(currentDir, CONFIG_FILENAME);
|
|
22
|
+
|
|
23
|
+
if (existsSync(configPath)) {
|
|
24
|
+
const config = parseConfigFile(configPath);
|
|
25
|
+
return { config, configDir: currentDir };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const parentDir = dirname(currentDir);
|
|
29
|
+
if (parentDir === currentDir) {
|
|
30
|
+
// Reached root directory
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
currentDir = parentDir;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseConfigFile(configPath: string): ConfigFile {
|
|
38
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
39
|
+
const queries = parseFile(content);
|
|
40
|
+
|
|
41
|
+
const generateApiClientTargets: GenerateApiClientTarget[] = [];
|
|
42
|
+
|
|
43
|
+
for (const query of queries) {
|
|
44
|
+
switch (query.command) {
|
|
45
|
+
case 'generate-api-client': {
|
|
46
|
+
const outputFile = query.getStringValue('output-file');
|
|
47
|
+
generateApiClientTargets.push({ outputFile });
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
default:
|
|
51
|
+
throw new Error(`Unknown command "${query.command}" in ${CONFIG_FILENAME}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
generateApiClientTargets,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
interface OpenAPISchema {
|
|
2
|
+
paths: Record<string, Record<string, Operation>>;
|
|
3
|
+
components?: {
|
|
4
|
+
schemas?: Record<string, any>;
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface Operation {
|
|
9
|
+
operationId?: string;
|
|
10
|
+
requestBody?: {
|
|
11
|
+
content?: {
|
|
12
|
+
'application/json'?: {
|
|
13
|
+
schema?: any;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
responses?: {
|
|
18
|
+
'200'?: {
|
|
19
|
+
content?: {
|
|
20
|
+
'application/json'?: {
|
|
21
|
+
schema?: any;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface EndpointMapping {
|
|
29
|
+
method: string;
|
|
30
|
+
path: string;
|
|
31
|
+
operationId: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function capitalizeFirst(str: string): string {
|
|
35
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Convert OpenAPI path parameter format {param} to Express format :param
|
|
40
|
+
*/
|
|
41
|
+
export function convertToExpressPath(openApiPath: string): string {
|
|
42
|
+
return openApiPath.replace(/\{([^}]+)\}/g, ':$1');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function schemaToTypeScript(schema: any, components?: Record<string, any>, indent = 0): string {
|
|
46
|
+
if (!schema) {
|
|
47
|
+
return 'unknown';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const indentStr = ' '.repeat(indent);
|
|
51
|
+
|
|
52
|
+
// Handle $ref
|
|
53
|
+
if (schema.$ref) {
|
|
54
|
+
const refName = schema.$ref.split('/').pop();
|
|
55
|
+
return refName;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Handle array
|
|
59
|
+
if (schema.type === 'array') {
|
|
60
|
+
const itemType = schemaToTypeScript(schema.items, components, indent);
|
|
61
|
+
return `Array<${itemType}>`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Handle object
|
|
65
|
+
if (schema.type === 'object' || schema.properties) {
|
|
66
|
+
const properties = schema.properties || {};
|
|
67
|
+
const required = schema.required || [];
|
|
68
|
+
|
|
69
|
+
if (Object.keys(properties).length === 0) {
|
|
70
|
+
return 'Record<string, unknown>';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const props = Object.entries(properties).map(([key, value]: [string, any]) => {
|
|
74
|
+
const isRequired = required.includes(key);
|
|
75
|
+
const propType = schemaToTypeScript(value, components, indent + 1);
|
|
76
|
+
const optional = isRequired ? '' : '?';
|
|
77
|
+
return `${indentStr} ${key}${optional}: ${propType};`;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return `{\n${props.join('\n')}\n${indentStr}}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Handle primitives
|
|
84
|
+
switch (schema.type) {
|
|
85
|
+
case 'string':
|
|
86
|
+
return 'string';
|
|
87
|
+
case 'number':
|
|
88
|
+
case 'integer':
|
|
89
|
+
return 'number';
|
|
90
|
+
case 'boolean':
|
|
91
|
+
return 'boolean';
|
|
92
|
+
case 'null':
|
|
93
|
+
return 'null';
|
|
94
|
+
default:
|
|
95
|
+
return 'unknown';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
100
|
+
import { dirname, resolve } from 'path';
|
|
101
|
+
|
|
102
|
+
export async function generateApiClients(baseUrl: string, outputFiles: string[]): Promise<void> {
|
|
103
|
+
if (outputFiles.length === 0) {
|
|
104
|
+
throw new Error('At least one --out file must be specified');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
// Fetch the OpenAPI schema from the server
|
|
109
|
+
const response = await fetch(`${baseUrl}/openapi.json`);
|
|
110
|
+
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Failed to fetch OpenAPI schema: ${response.status} ${response.statusText}\n` +
|
|
114
|
+
`Make sure the Prism API server is running at ${baseUrl}`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const schema = await response.json() as OpenAPISchema;
|
|
119
|
+
|
|
120
|
+
const lines: string[] = [];
|
|
121
|
+
const endpointMap: EndpointMapping[] = [];
|
|
122
|
+
|
|
123
|
+
// Generate component schemas first
|
|
124
|
+
if (schema.components?.schemas) {
|
|
125
|
+
lines.push('// Component Schemas');
|
|
126
|
+
for (const [name, componentSchema] of Object.entries(schema.components.schemas)) {
|
|
127
|
+
const typeStr = schemaToTypeScript(componentSchema, schema.components.schemas);
|
|
128
|
+
lines.push(`export type ${name} = ${typeStr};\n`);
|
|
129
|
+
}
|
|
130
|
+
lines.push('');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Process each endpoint
|
|
134
|
+
lines.push('// Endpoint Types');
|
|
135
|
+
for (const [pathStr, pathItem] of Object.entries(schema.paths)) {
|
|
136
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
137
|
+
if (!operation.operationId) continue;
|
|
138
|
+
|
|
139
|
+
const typeName = capitalizeFirst(operation.operationId);
|
|
140
|
+
const expressPath = convertToExpressPath(pathStr);
|
|
141
|
+
endpointMap.push({ method: method.toLowerCase(), path: expressPath, operationId: operation.operationId });
|
|
142
|
+
|
|
143
|
+
// Generate Request type
|
|
144
|
+
const requestSchema = operation.requestBody?.content?.['application/json']?.schema;
|
|
145
|
+
const requestType = requestSchema
|
|
146
|
+
? schemaToTypeScript(requestSchema, schema.components?.schemas)
|
|
147
|
+
: 'void';
|
|
148
|
+
lines.push(`export type ${typeName}Request = ${requestType};\n`);
|
|
149
|
+
|
|
150
|
+
// Generate Response type
|
|
151
|
+
const responseSchema = operation.responses?.['200']?.content?.['application/json']?.schema;
|
|
152
|
+
const responseType = responseSchema
|
|
153
|
+
? schemaToTypeScript(responseSchema, schema.components?.schemas)
|
|
154
|
+
: 'void';
|
|
155
|
+
lines.push(`export type ${typeName}Response = ${responseType};\n`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Generate ApiEndpoint union type
|
|
160
|
+
lines.push('// Union type of all valid endpoints');
|
|
161
|
+
lines.push('export type ApiEndpoint =');
|
|
162
|
+
const endpointStrings = endpointMap.map(({ method, path }) => ` | "${method} ${path}"`);
|
|
163
|
+
lines.push(endpointStrings.join('\n'));
|
|
164
|
+
lines.push(';\n');
|
|
165
|
+
|
|
166
|
+
// Generate generic RequestType and ResponseType
|
|
167
|
+
lines.push('// Generic Request/Response Types by Endpoint');
|
|
168
|
+
lines.push('export type RequestType<T extends ApiEndpoint> =');
|
|
169
|
+
const requestCases = endpointMap.map(({ method, path, operationId }) =>
|
|
170
|
+
` T extends "${method} ${path}" ? ${capitalizeFirst(operationId)}Request :`
|
|
171
|
+
);
|
|
172
|
+
lines.push(requestCases.join('\n'));
|
|
173
|
+
lines.push(' never;\n');
|
|
174
|
+
|
|
175
|
+
lines.push('export type ResponseType<T extends ApiEndpoint> =');
|
|
176
|
+
const responseCases = endpointMap.map(({ method, path, operationId }) =>
|
|
177
|
+
` T extends "${method} ${path}" ? ${capitalizeFirst(operationId)}Response :`
|
|
178
|
+
);
|
|
179
|
+
lines.push(responseCases.join('\n'));
|
|
180
|
+
lines.push(' never;\n');
|
|
181
|
+
|
|
182
|
+
const output = lines.join('\n');
|
|
183
|
+
|
|
184
|
+
// Add header comment
|
|
185
|
+
const header = `// Generated API client types
|
|
186
|
+
// Auto-generated from OpenAPI schema - do not edit manually
|
|
187
|
+
|
|
188
|
+
`;
|
|
189
|
+
|
|
190
|
+
const content = header + output;
|
|
191
|
+
|
|
192
|
+
// Write to each output file
|
|
193
|
+
for (const outputFile of outputFiles) {
|
|
194
|
+
const resolvedPath = resolve(outputFile);
|
|
195
|
+
mkdirSync(dirname(resolvedPath), { recursive: true });
|
|
196
|
+
writeFileSync(resolvedPath, content, 'utf-8');
|
|
197
|
+
console.log(`Written: ${resolvedPath}`);
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error('❌ Error generating client:', error);
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
package/src/getPorts.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as dotenv from 'dotenv';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Finds and reads the .env file for a directory
|
|
7
|
+
* Searches in the directory itself, then checks parent directories
|
|
8
|
+
* @param dir - Directory to search from
|
|
9
|
+
* @returns Parsed environment variables
|
|
10
|
+
*/
|
|
11
|
+
function loadEnvFile(dir: string): Record<string, string> {
|
|
12
|
+
// First, check if there's a .env file directly in the provided directory
|
|
13
|
+
let envPath = path.join(dir, '.env');
|
|
14
|
+
|
|
15
|
+
if (fs.existsSync(envPath)) {
|
|
16
|
+
const envConfig = dotenv.parse(fs.readFileSync(envPath));
|
|
17
|
+
return envConfig;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
throw new Error(`Environment file not found. Searched: ${dir}/.env`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Gets the port from a directory's .env file
|
|
25
|
+
* @param options - Configuration options
|
|
26
|
+
* @param options.dir - Directory to search for .env file (defaults to current working directory)
|
|
27
|
+
* @returns The port number
|
|
28
|
+
*/
|
|
29
|
+
export function getPort(options?: { dir?: string }): number {
|
|
30
|
+
const dir = options?.dir || process.cwd();
|
|
31
|
+
const env = loadEnvFile(dir);
|
|
32
|
+
|
|
33
|
+
if (env.PORT) {
|
|
34
|
+
return parseInt(env.PORT, 10);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
throw new Error('Unable to determine port from .env file');
|
|
38
|
+
}
|
|
39
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { callEndpoint } from './call-command.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* List all available endpoints by calling /endpoints.json
|
|
5
|
+
*/
|
|
6
|
+
export async function listEndpoints(baseUrl: string): Promise<void> {
|
|
7
|
+
try {
|
|
8
|
+
const response = await callEndpoint({
|
|
9
|
+
baseUrl,
|
|
10
|
+
positionalArgs: ['GET', '/endpoints.json'],
|
|
11
|
+
namedArgs: {},
|
|
12
|
+
quiet: true,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const endpoints = response.endpoints;
|
|
16
|
+
|
|
17
|
+
console.log('Available endpoints:\n');
|
|
18
|
+
if (Array.isArray(endpoints)) {
|
|
19
|
+
for (const endpoint of endpoints) {
|
|
20
|
+
const fullPath = `${endpoint.method} ${endpoint.path}`;
|
|
21
|
+
console.log(` ${fullPath}`);
|
|
22
|
+
if (endpoint.description) {
|
|
23
|
+
console.log(` ${endpoint.description}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
console.log(JSON.stringify(endpoints, null, 2));
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('Could not list endpoints. The server may not support the /api/endpoints introspection endpoint.');
|
|
31
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/loadEnv.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import { config } from 'dotenv';
|
|
4
|
+
|
|
5
|
+
export interface EnvConfig {
|
|
6
|
+
port: number;
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Load and parse the .env file from the project directory
|
|
12
|
+
* @param cwd - Current working directory to search from
|
|
13
|
+
* @returns Configuration object with port and baseUrl
|
|
14
|
+
* @throws Error if .env file is missing or PRISM_API_PORT is not defined
|
|
15
|
+
*/
|
|
16
|
+
export function loadEnv(cwd: string): EnvConfig {
|
|
17
|
+
const envPath = path.resolve(cwd, '.env');
|
|
18
|
+
|
|
19
|
+
if (!fs.existsSync(envPath)) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`No .env file found at ${envPath}\n\n` +
|
|
22
|
+
'Please create a .env file with PRISM_API_PORT defined.\n' +
|
|
23
|
+
'Example:\n' +
|
|
24
|
+
' PRISM_API_PORT=3000'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Load the .env file
|
|
29
|
+
const result = config({ path: envPath });
|
|
30
|
+
|
|
31
|
+
if (result.error) {
|
|
32
|
+
throw new Error(`Failed to load .env file: ${result.error.message}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const port = process.env.PRISM_API_PORT;
|
|
36
|
+
|
|
37
|
+
if (!port) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
'PRISM_API_PORT is not defined in .env file\n\n' +
|
|
40
|
+
'Please add PRISM_API_PORT to your .env file.\n' +
|
|
41
|
+
'Example:\n' +
|
|
42
|
+
' PRISM_API_PORT=3000'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const portNumber = parseInt(port, 10);
|
|
47
|
+
|
|
48
|
+
if (isNaN(portNumber) || portNumber <= 0 || portNumber > 65535) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Invalid PRISM_API_PORT value: ${port}\n\n` +
|
|
51
|
+
'Port must be a number between 1 and 65535'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
port: portNumber,
|
|
57
|
+
baseUrl: `http://localhost:${portNumber}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseNamedArgs } from '../src/call-command';
|
|
3
|
+
|
|
4
|
+
describe('parseNamedArgs', () => {
|
|
5
|
+
describe('basic values', () => {
|
|
6
|
+
it('should pass through simple string values', () => {
|
|
7
|
+
const result = parseNamedArgs({ name: 'John', email: 'john@example.com' });
|
|
8
|
+
expect(result).toEqual({ name: 'John', email: 'john@example.com' });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should pass through non-string values', () => {
|
|
12
|
+
const result = parseNamedArgs({ count: 42, active: true });
|
|
13
|
+
expect(result).toEqual({ count: 42, active: true });
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('JSON parsing', () => {
|
|
18
|
+
it('should parse JSON object strings', () => {
|
|
19
|
+
const result = parseNamedArgs({ config: '{"timeout": 30}' });
|
|
20
|
+
expect(result).toEqual({ config: { timeout: 30 } });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should parse JSON array strings', () => {
|
|
24
|
+
const result = parseNamedArgs({ items: '["a", "b", "c"]' });
|
|
25
|
+
expect(result).toEqual({ items: ['a', 'b', 'c'] });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should handle whitespace around JSON', () => {
|
|
29
|
+
const result = parseNamedArgs({ data: ' {"key": "value"} ' });
|
|
30
|
+
expect(result).toEqual({ data: { key: 'value' } });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should keep invalid JSON as string', () => {
|
|
34
|
+
const result = parseNamedArgs({ bad: '{not valid json}' });
|
|
35
|
+
expect(result).toEqual({ bad: '{not valid json}' });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should not parse strings that only start with { or [', () => {
|
|
39
|
+
const result = parseNamedArgs({ text: '{hello world' });
|
|
40
|
+
expect(result).toEqual({ text: '{hello world' });
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('nested objects from yargs', () => {
|
|
45
|
+
it('should parse JSON strings inside nested objects', () => {
|
|
46
|
+
// Yargs creates nested objects from dot notation before we see them
|
|
47
|
+
const result = parseNamedArgs({
|
|
48
|
+
schema: {
|
|
49
|
+
name: 'test-schema',
|
|
50
|
+
statements: '["CREATE TABLE test (id INT)"]'
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
expect(result).toEqual({
|
|
54
|
+
schema: {
|
|
55
|
+
name: 'test-schema',
|
|
56
|
+
statements: ['CREATE TABLE test (id INT)']
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should handle deeply nested objects with JSON strings', () => {
|
|
62
|
+
const result = parseNamedArgs({
|
|
63
|
+
config: {
|
|
64
|
+
database: {
|
|
65
|
+
options: '{"timeout": 30, "retries": 3}'
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
expect(result).toEqual({
|
|
70
|
+
config: {
|
|
71
|
+
database: {
|
|
72
|
+
options: { timeout: 30, retries: 3 }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('real-world example from test.sh', () => {
|
|
80
|
+
it('should handle the migration command args', () => {
|
|
81
|
+
// Yargs parses --schema.name and --schema.statements into nested object
|
|
82
|
+
const result = parseNamedArgs({
|
|
83
|
+
schema: {
|
|
84
|
+
name: 'test-schema-v2',
|
|
85
|
+
statements: '["CREATE TABLE test_products (id INTEGER PRIMARY KEY, name TEXT, price REAL)"]'
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
expect(result).toEqual({
|
|
89
|
+
schema: {
|
|
90
|
+
name: 'test-schema-v2',
|
|
91
|
+
statements: ['CREATE TABLE test_products (id INTEGER PRIMARY KEY, name TEXT, price REAL)']
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|