Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -3350,6 +3350,138 @@ See the \`references/\` directory for detailed per-entity API documentation:
]
`;

exports[`multi-target cli generator generates config command 1`] = `
"/**
* Config key-value store commands
* @generated by @constructive-io/graphql-codegen
* DO NOT EDIT - changes will be overwritten
*/
import { CLIOptions, Inquirerer, extractFirst } from "inquirerer";
import { getStore } from "../executor";
const usage = "\\nmyapp config <command>\\n\\nCommands:\\n get <key> Get a config value\\n set <key> <value> Set a config value\\n list List all config values\\n delete <key> Delete a config value\\n\\n --help, -h Show this help message\\n";
export default async (argv: Partial<Record<string, unknown>>, prompter: Inquirerer, _options: CLIOptions) => {
if (argv.help || argv.h) {
console.log(usage);
process.exit(0);
}
const store = getStore();
const {
first: subcommand,
newArgv
} = extractFirst(argv);
if (!subcommand) {
const answer = await prompter.prompt(argv, [{
type: "autocomplete",
name: "subcommand",
message: "What do you want to do?",
options: ["get", "set", "list", "delete"]
}]);
return handleSubcommand(answer.subcommand, newArgv, prompter, store);
}
return handleSubcommand(subcommand, newArgv, prompter, store);
};
async function handleSubcommand(subcommand: string, argv: Partial<Record<string, unknown>>, prompter: Inquirerer, store: ReturnType<typeof getStore>) {
switch (subcommand) {
case "get":
return handleGet(argv, prompter, store);
case "set":
return handleSet(argv, prompter, store);
case "list":
return handleList(store);
case "delete":
return handleDelete(argv, prompter, store);
default:
console.log(usage);
process.exit(1);
}
}
async function handleGet(argv: Partial<Record<string, unknown>>, prompter: Inquirerer, store: ReturnType<typeof getStore>) {
const {
first: key
} = extractFirst(argv);
if (!key) {
const answers = await prompter.prompt(argv, [{
type: "text",
name: "key",
message: "Config key",
required: true
}]);
const value = store.getVar(answers.key);
if (value === undefined) {
console.error(\`Key "\${answers.key}" not found.\`);
} else {
console.log(value);
}
return;
}
const value = store.getVar(key);
if (value === undefined) {
console.error(\`Key "\${key}" not found.\`);
} else {
console.log(value);
}
}
async function handleSet(argv: Partial<Record<string, unknown>>, prompter: Inquirerer, store: ReturnType<typeof getStore>) {
const {
first: key,
newArgv: restArgv
} = extractFirst(argv);
const {
first: value
} = extractFirst(restArgv);
if (!key || !value) {
const answers = await prompter.prompt({
key,
value
}, [{
type: "text",
name: "key",
message: "Config key",
required: true
}, {
type: "text",
name: "value",
message: "Config value",
required: true
}]);
store.setVar(answers.key, String.call(undefined, answers.value));
console.log(\`Set \${answers.key} = \${answers.value}\`);
return;
}
store.setVar(key, String(value));
console.log(\`Set \${key} = \${value}\`);
}
function handleList(store: ReturnType<typeof getStore>) {
const vars = store.listVars();
const entries = Object.entries(vars);
if (entries.length === 0) {
console.log("No config values set.");
return;
}
for (const [key, value] of entries) {
console.log(\`\${key} = \${value}\`);
}
}
async function handleDelete(argv: Partial<Record<string, unknown>>, prompter: Inquirerer, store: ReturnType<typeof getStore>) {
const {
first: key
} = extractFirst(argv);
if (!key) {
const answers = await prompter.prompt(argv, [{
type: "text",
name: "key",
message: "Config key to delete",
required: true
}]);
store.deleteVar(answers.key);
console.log(\`Deleted key: \${answers.key}\`);
return;
}
store.deleteVar(key);
console.log(\`Deleted key: \${key}\`);
}"
`;

exports[`multi-target cli generator generates credentials command (renamed from auth) 1`] = `
"/**
* Authentication commands
Expand Down Expand Up @@ -3456,6 +3588,45 @@ async function handleLogout(argv: Partial<Record<string, unknown>>, prompter: In
}"
`;

exports[`multi-target cli generator generates helpers.ts with typed client factories 1`] = `
"/**
* SDK helpers — typed per-target client factories with 3-tier credential resolution
* @generated by @constructive-io/graphql-codegen
* DO NOT EDIT - changes will be overwritten
*/
import { createConfigStore } from "appstash";
import type { ClientConfig } from "appstash";
import { createClient as createAuthOrmClient } from "../../generated/auth/orm";
import { createClient as createMembersOrmClient } from "../../generated/members/orm";
import { createClient as createAppOrmClient } from "../../generated/app/orm";
const store = createConfigStore("myapp");
export const getStore = () => store;
export function getClientConfig(targetName: string, contextName?: string): ClientConfig {
return store.getClientConfig(targetName, contextName);
}
export function createAuthClient(contextName?: string) {
const config = store.getClientConfig("auth", contextName);
return createAuthOrmClient({
endpoint: config.endpoint,
headers: config.headers
});
}
export function createMembersClient(contextName?: string) {
const config = store.getClientConfig("members", contextName);
return createMembersOrmClient({
endpoint: config.endpoint,
headers: config.headers
});
}
export function createAppClient(contextName?: string) {
const config = store.getClientConfig("app", contextName);
return createAppOrmClient({
endpoint: config.endpoint,
headers: config.headers
});
}"
`;

exports[`multi-target cli generator generates multi-target command map 1`] = `
"/**
* Multi-target CLI command map and entry point
Expand All @@ -3465,6 +3636,7 @@ exports[`multi-target cli generator generates multi-target command map 1`] = `
import { CLIOptions, Inquirerer, extractFirst } from "inquirerer";
import contextCmd from "./commands/context";
import credentialsCmd from "./commands/credentials";
import configCmd from "./commands/config";
import authUserCmd from "./commands/auth/user";
import authCurrentUserCmd from "./commands/auth/current-user";
import authLoginCmd from "./commands/auth/login";
Expand All @@ -3473,13 +3645,14 @@ import appCarCmd from "./commands/app/car";
const createCommandMap = () => ({
"context": contextCmd,
"credentials": credentialsCmd,
"config": configCmd,
"auth:user": authUserCmd,
"auth:current-user": authCurrentUserCmd,
"auth:login": authLoginCmd,
"members:member": membersMemberCmd,
"app:car": appCarCmd
});
const usage = "\\nmyapp <command>\\n\\nCommands:\\n context Manage API contexts\\n credentials Manage authentication\\n\\n auth:\\n auth:user user CRUD operations\\n auth:current-user Get the currently authenticated user\\n auth:login Authenticate a user\\n\\n members:\\n members:member member CRUD operations\\n\\n app:\\n app:car car CRUD operations\\n\\n --help, -h Show this help message\\n --version, -v Show version\\n";
const usage = "\\nmyapp <command>\\n\\nCommands:\\n context Manage API contexts\\n credentials Manage authentication\\n config Manage config key-value store\\n\\n auth:\\n auth:user user CRUD operations\\n auth:current-user Get the currently authenticated user\\n auth:login Authenticate a user\\n\\n members:\\n members:member member CRUD operations\\n\\n app:\\n app:car car CRUD operations\\n\\n --help, -h Show this help message\\n --version, -v Show version\\n";
export const commands = async (argv: Partial<Record<string, unknown>>, prompter: Inquirerer, options: CLIOptions) => {
if (argv.help || argv.h) {
console.log(usage);
Expand Down
64 changes: 62 additions & 2 deletions graphql/codegen/src/__tests__/codegen/cli-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,27 +421,31 @@ describe('resolveBuiltinNames', () => {
expect(resolveBuiltinNames(['app', 'members'])).toEqual({
auth: 'auth',
context: 'context',
config: 'config',
});
});

it('renames auth to credentials on collision', () => {
expect(resolveBuiltinNames(['auth', 'members', 'app'])).toEqual({
auth: 'credentials',
context: 'context',
config: 'config',
});
});

it('renames context to env on collision', () => {
expect(resolveBuiltinNames(['context', 'app'])).toEqual({
auth: 'auth',
context: 'env',
config: 'config',
});
});

it('renames both on collision', () => {
expect(resolveBuiltinNames(['auth', 'context', 'app'])).toEqual({
auth: 'credentials',
context: 'env',
config: 'config',
});
});

Expand All @@ -451,6 +455,7 @@ describe('resolveBuiltinNames', () => {
).toEqual({
auth: 'auth',
context: 'context',
config: 'config',
});
});

Expand All @@ -460,6 +465,33 @@ describe('resolveBuiltinNames', () => {
).toEqual({
auth: 'creds',
context: 'profile',
config: 'config',
});
});

it('renames config to vars on collision', () => {
expect(resolveBuiltinNames(['config', 'app'])).toEqual({
auth: 'auth',
context: 'context',
config: 'vars',
});
});

it('renames all three on collision', () => {
expect(resolveBuiltinNames(['auth', 'context', 'config', 'app'])).toEqual({
auth: 'credentials',
context: 'env',
config: 'vars',
});
});

it('respects user config override even with collision', () => {
expect(
resolveBuiltinNames(['config', 'app'], { config: 'config' }),
).toEqual({
auth: 'auth',
context: 'context',
config: 'config',
});
});
});
Expand Down Expand Up @@ -538,8 +570,8 @@ describe('multi-target cli generator', () => {
tables: 3,
customQueries: 1,
customMutations: 1,
infraFiles: 4,
totalFiles: 10,
infraFiles: 6,
totalFiles: 12,
});
});

Expand All @@ -557,10 +589,12 @@ describe('multi-target cli generator', () => {
'commands/auth/current-user.ts',
'commands/auth/login.ts',
'commands/auth/user.ts',
'commands/config.ts',
'commands/context.ts',
'commands/credentials.ts',
'commands/members/member.ts',
'executor.ts',
'helpers.ts',
'utils.ts',
]);
});
Expand Down Expand Up @@ -625,6 +659,32 @@ describe('multi-target cli generator', () => {
expect(file!.content).toContain('app:car');
expect(file!.content).toContain('credentials');
expect(file!.content).toContain('context');
expect(file!.content).toContain('config');
expect(file!.content).toMatchSnapshot();
});

it('generates config command', () => {
const file = multiResult.files.find(
(f) => f.fileName === 'commands/config.ts',
);
expect(file).toBeDefined();
expect(file!.content).toContain('getStore');
expect(file!.content).toContain('getVar');
expect(file!.content).toContain('setVar');
expect(file!.content).toContain('deleteVar');
expect(file!.content).toContain('listVars');
expect(file!.content).toMatchSnapshot();
});

it('generates helpers.ts with typed client factories', () => {
const file = multiResult.files.find((f) => f.fileName === 'helpers.ts');
expect(file).toBeDefined();
expect(file!.content).toContain('createConfigStore');
expect(file!.content).toContain('getClientConfig');
expect(file!.content).toContain('createAuthClient');
expect(file!.content).toContain('createMembersClient');
expect(file!.content).toContain('createAppClient');
expect(file!.content).toContain('ClientConfig');
expect(file!.content).toMatchSnapshot();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ export function generateCommandMap(

export interface MultiTargetCommandMapInput {
toolName: string;
builtinNames: { auth: string; context: string };
builtinNames: { auth: string; context: string; config: string };
targets: Array<{
name: string;
tables: CleanTable[];
Expand Down Expand Up @@ -419,6 +419,12 @@ export function generateMultiTargetCommandMap(
createImportDeclaration(`./commands/${builtinNames.auth}`, authImportName),
);

const configImportName = `${builtinNames.config}Cmd`;
commandEntries.push({ kebab: builtinNames.config, importName: configImportName });
statements.push(
createImportDeclaration(`./commands/${builtinNames.config}`, configImportName),
);

for (const target of targets) {
for (const table of target.tables) {
const { singularName } = getTableNames(table);
Expand Down Expand Up @@ -467,6 +473,7 @@ export function generateMultiTargetCommandMap(
'Commands:',
` ${builtinNames.context.padEnd(20)} Manage API contexts`,
` ${builtinNames.auth.padEnd(20)} Manage authentication`,
` ${builtinNames.config.padEnd(20)} Manage config key-value store`,
];

for (const target of targets) {
Expand Down
Loading
Loading