Skip to content

Commit b94fe00

Browse files
authored
Merge pull request #64 from constructive-io/devin/1771360954-appstash-multi-target
feat(appstash): add multi-target context support
2 parents de74097 + f662b92 commit b94fe00

3 files changed

Lines changed: 116 additions & 2 deletions

File tree

packages/appstash/__tests__/config-store.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,95 @@ describe('createConfigStore', () => {
255255
});
256256
});
257257

258+
describe('multi-target contexts', () => {
259+
it('should create a context with targets', () => {
260+
const store = createStore();
261+
const ctx = store.createContext('production', {
262+
endpoint: 'https://api.example.com/graphql',
263+
targets: {
264+
auth: { endpoint: 'https://auth.example.com/graphql' },
265+
members: { endpoint: 'https://members.example.com/graphql' },
266+
app: { endpoint: 'https://app.example.com/graphql' },
267+
},
268+
});
269+
expect(ctx.targets).toBeDefined();
270+
expect(ctx.targets!.auth.endpoint).toBe('https://auth.example.com/graphql');
271+
expect(ctx.targets!.members.endpoint).toBe('https://members.example.com/graphql');
272+
expect(ctx.targets!.app.endpoint).toBe('https://app.example.com/graphql');
273+
});
274+
275+
it('should load a context with targets', () => {
276+
const store = createStore();
277+
store.createContext('production', {
278+
endpoint: 'https://api.example.com/graphql',
279+
targets: {
280+
auth: { endpoint: 'https://auth.example.com/graphql' },
281+
},
282+
});
283+
const ctx = store.loadContext('production');
284+
expect(ctx!.targets!.auth.endpoint).toBe('https://auth.example.com/graphql');
285+
});
286+
287+
it('should create a context without targets (backward compatible)', () => {
288+
const store = createStore();
289+
const ctx = store.createContext('production', {
290+
endpoint: 'https://api.example.com/graphql',
291+
});
292+
expect(ctx.targets).toBeUndefined();
293+
});
294+
295+
it('should get target endpoint from multi-target context', () => {
296+
const store = createStore();
297+
store.createContext('production', {
298+
endpoint: 'https://api.example.com/graphql',
299+
targets: {
300+
auth: { endpoint: 'https://auth.example.com/graphql' },
301+
app: { endpoint: 'https://app.example.com/graphql' },
302+
},
303+
});
304+
store.setCurrentContext('production');
305+
expect(store.getTargetEndpoint('auth')).toBe('https://auth.example.com/graphql');
306+
expect(store.getTargetEndpoint('app')).toBe('https://app.example.com/graphql');
307+
});
308+
309+
it('should fall back to main endpoint for unknown target', () => {
310+
const store = createStore();
311+
store.createContext('production', {
312+
endpoint: 'https://api.example.com/graphql',
313+
targets: {
314+
auth: { endpoint: 'https://auth.example.com/graphql' },
315+
},
316+
});
317+
store.setCurrentContext('production');
318+
expect(store.getTargetEndpoint('unknown')).toBe('https://api.example.com/graphql');
319+
});
320+
321+
it('should fall back to main endpoint for single-target context', () => {
322+
const store = createStore();
323+
store.createContext('production', {
324+
endpoint: 'https://api.example.com/graphql',
325+
});
326+
store.setCurrentContext('production');
327+
expect(store.getTargetEndpoint('anything')).toBe('https://api.example.com/graphql');
328+
});
329+
330+
it('should return null when no current context for getTargetEndpoint', () => {
331+
const store = createStore();
332+
expect(store.getTargetEndpoint('auth')).toBeNull();
333+
});
334+
335+
it('should get target endpoint by explicit context name', () => {
336+
const store = createStore();
337+
store.createContext('staging', {
338+
endpoint: 'https://staging.example.com/graphql',
339+
targets: {
340+
auth: { endpoint: 'https://auth.staging.example.com/graphql' },
341+
},
342+
});
343+
expect(store.getTargetEndpoint('auth', 'staging')).toBe('https://auth.staging.example.com/graphql');
344+
});
345+
});
346+
258347
describe('full workflow', () => {
259348
it('should support the complete context + auth workflow', () => {
260349
const store = createStore();

packages/appstash/src/config-store.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@ import * as fs from 'fs';
22
import * as path from 'path';
33
import { appstash, resolve } from './index';
44

5+
export interface ContextTargetEndpoint {
6+
endpoint: string;
7+
}
8+
59
export interface ContextConfig {
610
name: string;
711
endpoint: string;
12+
targets?: Record<string, ContextTargetEndpoint>;
813
createdAt: string;
914
updatedAt: string;
1015
}
@@ -31,12 +36,13 @@ export interface ConfigStore {
3136
loadSettings(): GlobalSettings;
3237
saveSettings(settings: GlobalSettings): void;
3338

34-
createContext(name: string, options: { endpoint: string }): ContextConfig;
39+
createContext(name: string, options: { endpoint: string; targets?: Record<string, ContextTargetEndpoint> }): ContextConfig;
3540
loadContext(name: string): ContextConfig | null;
3641
listContexts(): ContextConfig[];
3742
deleteContext(name: string): boolean;
3843
getCurrentContext(): ContextConfig | null;
3944
setCurrentContext(name: string): boolean;
45+
getTargetEndpoint(targetName: string, contextName?: string): string | null;
4046

4147
setCredentials(contextName: string, creds: ContextCredentials): void;
4248
getCredentials(contextName: string): ContextCredentials | null;
@@ -97,14 +103,17 @@ export function createConfigStore(toolName: string, options?: ConfigStoreOptions
97103
return readJson<ContextConfig | null>(contextPath(name), null);
98104
}
99105

100-
function createContext(name: string, options: { endpoint: string }): ContextConfig {
106+
function createContext(name: string, options: { endpoint: string; targets?: Record<string, ContextTargetEndpoint> }): ContextConfig {
101107
const now = new Date().toISOString();
102108
const context: ContextConfig = {
103109
name,
104110
endpoint: options.endpoint,
105111
createdAt: now,
106112
updatedAt: now,
107113
};
114+
if (options.targets) {
115+
context.targets = options.targets;
116+
}
108117
writeJson(contextPath(name), context);
109118
return context;
110119
}
@@ -203,6 +212,20 @@ export function createConfigStore(toolName: string, options?: ConfigStoreOptions
203212
return true;
204213
}
205214

215+
function getTargetEndpoint(targetName: string, contextName?: string): string | null {
216+
let ctx: ContextConfig | null;
217+
if (contextName) {
218+
ctx = loadContext(contextName);
219+
} else {
220+
ctx = getCurrentContext();
221+
}
222+
if (!ctx) return null;
223+
if (ctx.targets && ctx.targets[targetName]) {
224+
return ctx.targets[targetName].endpoint;
225+
}
226+
return ctx.endpoint;
227+
}
228+
206229
return {
207230
loadSettings,
208231
saveSettings,
@@ -212,6 +235,7 @@ export function createConfigStore(toolName: string, options?: ConfigStoreOptions
212235
deleteContext,
213236
getCurrentContext,
214237
setCurrentContext,
238+
getTargetEndpoint,
215239
setCredentials,
216240
getCredentials,
217241
removeCredentials,

packages/appstash/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ export type {
279279
ConfigStoreOptions,
280280
ContextConfig,
281281
ContextCredentials,
282+
ContextTargetEndpoint,
282283
Credentials,
283284
GlobalSettings,
284285
} from './config-store';

0 commit comments

Comments
 (0)