Skip to content

Commit 31977a0

Browse files
committed
feat: add admin api sdk
1 parent 85708e9 commit 31977a0

16 files changed

Lines changed: 1692 additions & 13 deletions

File tree

packages/admin/package.json

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"name": "@replanejs/admin",
3+
"version": "0.8.10",
4+
"description": "Admin API SDK for Replane - programmatic management of projects, configs, environments, and SDK keys",
5+
"type": "module",
6+
"main": "./dist/index.cjs",
7+
"module": "./dist/index.js",
8+
"types": "./dist/index.d.ts",
9+
"exports": {
10+
".": {
11+
"types": "./dist/index.d.ts",
12+
"import": "./dist/index.js",
13+
"require": "./dist/index.cjs"
14+
}
15+
},
16+
"files": [
17+
"dist"
18+
],
19+
"sideEffects": false,
20+
"scripts": {
21+
"prebuild": "node ../../scripts/update-version.js .",
22+
"build": "tsdown",
23+
"dev": "tsdown --watch",
24+
"test": "vitest run",
25+
"typecheck": "tsc --noEmit"
26+
},
27+
"keywords": [
28+
"replane",
29+
"admin",
30+
"api",
31+
"sdk",
32+
"feature-flags",
33+
"remote-config",
34+
"configuration",
35+
"management"
36+
],
37+
"author": "",
38+
"license": "MIT",
39+
"repository": {
40+
"type": "git",
41+
"url": "https://github.com/replane-dev/replane-javascript",
42+
"directory": "packages/admin"
43+
},
44+
"bugs": {
45+
"url": "https://github.com/replane-dev/replane-javascript/issues"
46+
},
47+
"homepage": "https://github.com/replane-dev/replane-javascript#readme",
48+
"devDependencies": {
49+
"tsdown": "^0.11.9",
50+
"typescript": "^5.4.0",
51+
"vitest": "^3.2.4"
52+
},
53+
"engines": {
54+
"node": ">=18.0.0"
55+
},
56+
"publishConfig": {
57+
"access": "public"
58+
}
59+
}

packages/admin/src/client.ts

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
import type {
2+
ReplaneAdminOptions,
3+
Project,
4+
ProjectListResponse,
5+
CreateProjectRequest,
6+
CreateProjectResponse,
7+
UpdateProjectRequest,
8+
UpdateProjectResponse,
9+
Config,
10+
ConfigListResponse,
11+
CreateConfigRequest,
12+
CreateConfigResponse,
13+
UpdateConfigRequest,
14+
UpdateConfigResponse,
15+
EnvironmentListResponse,
16+
SdkKeyListResponse,
17+
SdkKeyWithToken,
18+
CreateSdkKeyRequest,
19+
MemberListResponse,
20+
ApiError,
21+
} from "./types.js";
22+
import { DEFAULT_AGENT } from "./version.js";
23+
24+
/**
25+
* Error thrown by the Admin API client
26+
*/
27+
export class ReplaneAdminError extends Error {
28+
constructor(
29+
message: string,
30+
public readonly status: number,
31+
public readonly response?: ApiError
32+
) {
33+
super(message);
34+
this.name = "ReplaneAdminError";
35+
}
36+
}
37+
38+
/**
39+
* Admin API client for Replane
40+
*
41+
* Provides programmatic access to manage projects, configs, environments,
42+
* SDK keys, and members.
43+
*
44+
* @example
45+
* ```typescript
46+
* import { ReplaneAdmin } from "@replanejs/admin";
47+
*
48+
* const admin = new ReplaneAdmin({
49+
* baseUrl: "https://app.replane.dev",
50+
* apiKey: "rpa_...",
51+
* });
52+
*
53+
* // List all projects
54+
* const { projects } = await admin.projects.list();
55+
*
56+
* // Create a new config
57+
* const { id } = await admin.configs.create("project-id", {
58+
* name: "my-config",
59+
* description: "My config",
60+
* editors: [],
61+
* maintainers: [],
62+
* base: { value: true, schema: null, overrides: [] },
63+
* variants: [],
64+
* });
65+
* ```
66+
*/
67+
export class ReplaneAdmin {
68+
private readonly apiKey: string;
69+
private readonly baseUrl: string;
70+
private readonly agent: string;
71+
private readonly fetchFn: typeof fetch;
72+
73+
public readonly projects: ProjectsApi;
74+
public readonly configs: ConfigsApi;
75+
public readonly environments: EnvironmentsApi;
76+
public readonly sdkKeys: SdkKeysApi;
77+
public readonly members: MembersApi;
78+
79+
constructor(options: ReplaneAdminOptions) {
80+
this.apiKey = options.apiKey;
81+
this.baseUrl = `${options.baseUrl.replace(/\/$/, "")}/api/admin/v1`;
82+
this.agent = options.agent ?? DEFAULT_AGENT;
83+
this.fetchFn = options.fetchFn ?? globalThis.fetch;
84+
85+
this.projects = new ProjectsApi(this);
86+
this.configs = new ConfigsApi(this);
87+
this.environments = new EnvironmentsApi(this);
88+
this.sdkKeys = new SdkKeysApi(this);
89+
this.members = new MembersApi(this);
90+
}
91+
92+
/**
93+
* Make an authenticated request to the Admin API
94+
* @internal
95+
*/
96+
async request<T>(
97+
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
98+
path: string,
99+
body?: unknown
100+
): Promise<T> {
101+
const url = `${this.baseUrl}${path}`;
102+
103+
const headers: Record<string, string> = {
104+
Authorization: `Bearer ${this.apiKey}`,
105+
"User-Agent": this.agent,
106+
};
107+
108+
if (body !== undefined) {
109+
headers["Content-Type"] = "application/json";
110+
}
111+
112+
const response = await this.fetchFn(url, {
113+
method,
114+
headers,
115+
body: body !== undefined ? JSON.stringify(body) : undefined,
116+
});
117+
118+
if (!response.ok) {
119+
let errorResponse: ApiError | undefined;
120+
try {
121+
errorResponse = await response.json();
122+
} catch {
123+
// Ignore JSON parse errors
124+
}
125+
throw new ReplaneAdminError(
126+
errorResponse?.error ?? `Request failed with status ${response.status}`,
127+
response.status,
128+
errorResponse
129+
);
130+
}
131+
132+
// Handle 204 No Content
133+
if (response.status === 204) {
134+
return undefined as T;
135+
}
136+
137+
return response.json();
138+
}
139+
}
140+
141+
/**
142+
* Projects API
143+
*/
144+
class ProjectsApi {
145+
constructor(private readonly client: ReplaneAdmin) {}
146+
147+
/**
148+
* List all projects
149+
*/
150+
async list(): Promise<ProjectListResponse> {
151+
return this.client.request<ProjectListResponse>("GET", "/projects");
152+
}
153+
154+
/**
155+
* Get a project by ID
156+
*/
157+
async get(projectId: string): Promise<Project> {
158+
return this.client.request<Project>("GET", `/projects/${projectId}`);
159+
}
160+
161+
/**
162+
* Create a new project
163+
*/
164+
async create(data: CreateProjectRequest): Promise<CreateProjectResponse> {
165+
return this.client.request<CreateProjectResponse>("POST", "/projects", data);
166+
}
167+
168+
/**
169+
* Update a project
170+
*/
171+
async update(projectId: string, data: UpdateProjectRequest): Promise<UpdateProjectResponse> {
172+
return this.client.request<UpdateProjectResponse>("PATCH", `/projects/${projectId}`, data);
173+
}
174+
175+
/**
176+
* Delete a project
177+
*/
178+
async delete(projectId: string): Promise<void> {
179+
return this.client.request<void>("DELETE", `/projects/${projectId}`);
180+
}
181+
}
182+
183+
/**
184+
* Configs API
185+
*/
186+
class ConfigsApi {
187+
constructor(private readonly client: ReplaneAdmin) {}
188+
189+
/**
190+
* List all configs in a project
191+
*/
192+
async list(projectId: string): Promise<ConfigListResponse> {
193+
return this.client.request<ConfigListResponse>("GET", `/projects/${projectId}/configs`);
194+
}
195+
196+
/**
197+
* Get a config by name
198+
*/
199+
async get(projectId: string, configName: string): Promise<Config> {
200+
return this.client.request<Config>(
201+
"GET",
202+
`/projects/${projectId}/configs/${encodeURIComponent(configName)}`
203+
);
204+
}
205+
206+
/**
207+
* Create a new config
208+
*/
209+
async create(projectId: string, data: CreateConfigRequest): Promise<CreateConfigResponse> {
210+
return this.client.request<CreateConfigResponse>(
211+
"POST",
212+
`/projects/${projectId}/configs`,
213+
data
214+
);
215+
}
216+
217+
/**
218+
* Update a config
219+
*/
220+
async update(
221+
projectId: string,
222+
configName: string,
223+
data: UpdateConfigRequest
224+
): Promise<UpdateConfigResponse> {
225+
return this.client.request<UpdateConfigResponse>(
226+
"PUT",
227+
`/projects/${projectId}/configs/${encodeURIComponent(configName)}`,
228+
data
229+
);
230+
}
231+
232+
/**
233+
* Delete a config
234+
*/
235+
async delete(projectId: string, configName: string): Promise<void> {
236+
return this.client.request<void>(
237+
"DELETE",
238+
`/projects/${projectId}/configs/${encodeURIComponent(configName)}`
239+
);
240+
}
241+
}
242+
243+
/**
244+
* Environments API
245+
*/
246+
class EnvironmentsApi {
247+
constructor(private readonly client: ReplaneAdmin) {}
248+
249+
/**
250+
* List all environments in a project
251+
*/
252+
async list(projectId: string): Promise<EnvironmentListResponse> {
253+
return this.client.request<EnvironmentListResponse>(
254+
"GET",
255+
`/projects/${projectId}/environments`
256+
);
257+
}
258+
}
259+
260+
/**
261+
* SDK Keys API
262+
*/
263+
class SdkKeysApi {
264+
constructor(private readonly client: ReplaneAdmin) {}
265+
266+
/**
267+
* List all SDK keys in a project
268+
*/
269+
async list(projectId: string): Promise<SdkKeyListResponse> {
270+
return this.client.request<SdkKeyListResponse>("GET", `/projects/${projectId}/sdk-keys`);
271+
}
272+
273+
/**
274+
* Create a new SDK key
275+
* Note: The returned key is only shown once and cannot be retrieved again
276+
*/
277+
async create(projectId: string, data: CreateSdkKeyRequest): Promise<SdkKeyWithToken> {
278+
return this.client.request<SdkKeyWithToken>("POST", `/projects/${projectId}/sdk-keys`, data);
279+
}
280+
281+
/**
282+
* Delete an SDK key
283+
*/
284+
async delete(projectId: string, sdkKeyId: string): Promise<void> {
285+
return this.client.request<void>("DELETE", `/projects/${projectId}/sdk-keys/${sdkKeyId}`);
286+
}
287+
}
288+
289+
/**
290+
* Members API
291+
*/
292+
class MembersApi {
293+
constructor(private readonly client: ReplaneAdmin) {}
294+
295+
/**
296+
* List all members in a project
297+
*/
298+
async list(projectId: string): Promise<MemberListResponse> {
299+
return this.client.request<MemberListResponse>("GET", `/projects/${projectId}/members`);
300+
}
301+
}

0 commit comments

Comments
 (0)