Skip to content

Commit 16cb169

Browse files
committed
feat: Implement Mixcore SDK with modular architecture and framework-agnostic API services
- Added ApiService for handling HTTP requests with hooks for request/response processing. - Introduced BaseService as an abstract class for shared service configuration. - Created ConfigurationServices for managing application configuration with support for file uploads. - Developed MixDatabaseDataRestPortalService for database operations including save, get, init, export, import, and migrate functionalities. - Implemented FileServices for file handling operations such as upload, download, and metadata management. - Updated documentation for all packages, including usage examples and API references. - Enhanced package.json and TypeScript configurations for better development experience.
1 parent 2a357f5 commit 16cb169

File tree

3 files changed

+1510
-0
lines changed

3 files changed

+1510
-0
lines changed

llms-code.txt

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
// packages/api/src/api-services.ts
2+
@mixcore/api implementation:
3+
<<<CONTENT>>>
4+
import type { ApiServiceConfig } from '@mixcore/shared';
5+
import type { ApiResult, RestApiResult } from '@mixcore/shared';
6+
7+
export type { ApiResult, RestApiResult };
8+
9+
/**
10+
* ApiService
11+
* Framework-agnostic, TypeScript-native API client for Mixcore
12+
*
13+
* @remarks
14+
* Refactored from legacy AngularJS service. All SPA dependencies removed.
15+
* Configuration is injected via constructor.
16+
*/
17+
18+
export type ApiServiceHook = {
19+
onRequest?: (req: RequestInit & { url: string }) => void | Promise<void>;
20+
onResponse?: (res: Response, req: RequestInit & { url: string }) => void | Promise<void>;
21+
};
22+
23+
export class ApiService implements ApiService {
24+
private config: ApiServiceConfig;
25+
private hooks: ApiServiceHook[] = [];
26+
27+
constructor(config: ApiServiceConfig) {
28+
this.config = config;
29+
}
30+
31+
/**
32+
* Register a request/response hook
33+
*/
34+
use(hook: ApiServiceHook) {
35+
this.hooks.push(hook);
36+
}
37+
38+
/**
39+
* Generic GET request (returns ApiResult)
40+
*/
41+
async get(endpoint: string, params?: Record<string, any>): Promise<ApiResult> {
42+
const url = new URL(endpoint, this.config.apiBaseUrl);
43+
if (params) {
44+
Object.entries(params).forEach(([k, v]) => url.searchParams.append(k, String(v)));
45+
}
46+
const req: RequestInit & { url: string } = {
47+
url: url.toString(),
48+
headers: this.config.apiKey ? { 'Authorization': `Bearer ${this.config.apiKey}` } : undefined,
49+
};
50+
for (const hook of this.hooks) if (hook.onRequest) await hook.onRequest(req);
51+
try {
52+
const res = await fetch(req.url, req);
53+
for (const hook of this.hooks) if (hook.onResponse) await hook.onResponse(res, req);
54+
const data = await res.json().catch(() => undefined);
55+
if (!res.ok) {
56+
return { isSucceed: false, data, errors: [res.statusText], status: res.status };
57+
}
58+
return { isSucceed: true, data, status: res.status };
59+
} catch (err) {
60+
return { isSucceed: false, errors: [(err as Error).message] };
61+
}
62+
}
63+
64+
/**
65+
* Generic POST request (returns ApiResult, supports JSON or FormData)
66+
*/
67+
async post(endpoint: string, data: any, options?: { isFormData?: boolean }): Promise<ApiResult> {
68+
const url = new URL(endpoint, this.config.apiBaseUrl);
69+
let body: any = data;
70+
let headers: Record<string, string> = {};
71+
if (options?.isFormData) {
72+
// Let browser set Content-Type for FormData
73+
body = data;
74+
} else {
75+
body = JSON.stringify(data);
76+
headers['Content-Type'] = 'application/json';
77+
}
78+
if (this.config.apiKey) headers['Authorization'] = `Bearer ${this.config.apiKey}`;
79+
const req: RequestInit & { url: string } = {
80+
url: url.toString(),
81+
method: 'POST',
82+
headers,
83+
body,
84+
};
85+
for (const hook of this.hooks) if (hook.onRequest) await hook.onRequest(req);
86+
const res = await fetch(req.url, req);
87+
for (const hook of this.hooks) if (hook.onResponse) await hook.onResponse(res, req);
88+
const respData = await res.json().catch(() => undefined);
89+
if (!res.ok) {
90+
return { isSucceed: false, data: respData, errors: [res.statusText], status: res.status };
91+
}
92+
return { isSucceed: true, data: respData, status: res.status };
93+
}
94+
95+
/**
96+
* Generic DELETE request (returns ApiResult)
97+
*/
98+
async delete(endpoint: string): Promise<ApiResult> {
99+
const url = new URL(endpoint, this.config.apiBaseUrl);
100+
const req: RequestInit & { url: string } = {
101+
url: url.toString(),
102+
method: 'DELETE',
103+
headers: this.config.apiKey ? { 'Authorization': `Bearer ${this.config.apiKey}` } : undefined,
104+
};
105+
for (const hook of this.hooks) if (hook.onRequest) await hook.onRequest(req);
106+
try {
107+
const res = await fetch(req.url, req);
108+
for (const hook of this.hooks) if (hook.onResponse) await hook.onResponse(res, req);
109+
const data = await res.json().catch(() => undefined);
110+
if (!res.ok) {
111+
return { isSucceed: false, data, errors: [res.statusText], status: res.status };
112+
}
113+
return { isSucceed: true, data, status: res.status };
114+
} catch (err) {
115+
return { isSucceed: false, errors: [(err as Error).message] };
116+
}
117+
}
118+
}
119+
<<<END>>>
120+
121+
// packages/base/src/base-service.ts
122+
@mixcore/base implementation:
123+
<<<CONTENT>>>
124+
/**
125+
* BaseService
126+
* Abstract base class for Mixcore SDK services
127+
*
128+
* @remarks
129+
* Refactored from legacy AngularJS service. All SPA dependencies removed.
130+
* Configuration is injected via constructor.
131+
*/
132+
export interface BaseServiceConfig {
133+
apiBaseUrl: string;
134+
apiKey?: string;
135+
[key: string]: any;
136+
}
137+
138+
export abstract class BaseService {
139+
protected config: BaseServiceConfig;
140+
141+
constructor(config: BaseServiceConfig) {
142+
this.config = config;
143+
}
144+
145+
/**
146+
* Abstract method for error handling
147+
* @param error - Error object
148+
*/
149+
abstract handleError(error: any): void;
150+
}
151+
<<<END>>>
152+
153+
// packages/config/src/configuration-services.ts
154+
@mixcore/config implementation:
155+
<<<CONTENT>>>
156+
import { ApiService, ApiServiceConfig } from '@mixcore/shared';
157+
158+
/**
159+
* ConfigurationServices
160+
* TypeScript-native, framework-agnostic service for configuration management.
161+
* Migrated and refactored from legacy AngularJS ConfigurationService.
162+
*/
163+
export interface ConfigurationUpload {
164+
file: File;
165+
folder?: string;
166+
title?: string;
167+
description?: string;
168+
}
169+
170+
export class ConfigurationServices {
171+
private api: ApiService;
172+
private readonly prefixUrl: string = '/configuration';
173+
174+
constructor(api: ApiService) {
175+
this.api = api;
176+
}
177+
178+
/**
179+
* Uploads a configuration file.
180+
* @param configurationFile - The configuration file and metadata
181+
* @returns API result
182+
*/
183+
async uploadConfiguration(configurationFile: ConfigurationUpload): Promise<any> {
184+
if (!configurationFile.file) {
185+
throw new Error('No file provided');
186+
}
187+
const formData = new FormData();
188+
formData.append(configurationFile.file.name, configurationFile.file);
189+
if (configurationFile.folder) formData.append('fileFolder', configurationFile.folder);
190+
if (configurationFile.title) formData.append('title', configurationFile.title);
191+
if (configurationFile.description) formData.append('description', configurationFile.description);
192+
193+
const req = {
194+
url: this.prefixUrl + '/upload',
195+
method: 'POST',
196+
body: formData,
197+
headers: {},
198+
};
199+
// Use fetch directly for FormData, bypassing ApiService's JSON logic
200+
const url = new URL(req.url, this.api['config'].apiBaseUrl).toString();
201+
const res = await fetch(url, {
202+
method: 'POST',
203+
body: formData,
204+
// Let browser set Content-Type for FormData
205+
headers: this.api['config'].apiKey ? { 'Authorization': `Bearer ${this.api['config'].apiKey}` } : undefined,
206+
});
207+
if (!res.ok) throw new Error(`Upload failed: ${res.status} ${res.statusText}`);
208+
return res.json();
209+
}
210+
}
211+
<<<END>>>
212+
213+
// packages/database/src/mix-database-data-rest-portal-service.ts
214+
@mixcore/database implementation:
215+
<<<CONTENT>>>
216+
import type { ApiService } from '@mixcore/api';
217+
import type { ApiResult } from '@mixcore/api';
218+
219+
/**
220+
* MixDatabaseDataRestPortalService
221+
* TypeScript-native, framework-agnostic service for Mixcore database data portal operations.
222+
* Migrated and refactored from legacy AngularJS RestMixDatabaseDataPortalService.
223+
*/
224+
export class MixDatabaseDataRestPortalService {
225+
private api: ApiService;
226+
private readonly prefixUrl: string = '/mix-database-data/portal';
227+
228+
constructor(api: ApiService) {
229+
this.api = api;
230+
}
231+
232+
/**
233+
* Saves additional data for a Mixcore database. Returns ApiResult.
234+
*/
235+
async saveAdditionalData(objData: any): Promise<ApiResult> {
236+
const endpoint = `${this.prefixUrl}/save-additional-data`;
237+
return this.api.post(endpoint, objData);
238+
}
239+
240+
/**
241+
* Gets additional data for a Mixcore database. Returns ApiResult.
242+
*/
243+
async getAdditionalData(data?: any): Promise<ApiResult> {
244+
let endpoint = `${this.prefixUrl}/additional-data`;
245+
if (data && typeof data === 'object' && Object.keys(data).length > 0) {
246+
const params = new URLSearchParams(data).toString();
247+
endpoint += `?${params}`;
248+
}
249+
return this.api.get(endpoint);
250+
}
251+
252+
/**
253+
* Initializes data for a Mixcore database by name. Returns ApiResult.
254+
*/
255+
async initData(mixDatabaseName: string): Promise<ApiResult> {
256+
if (!mixDatabaseName) {
257+
return { isSucceed: false, errors: ['Missing mixDatabaseName'] };
258+
}
259+
const endpoint = `${this.prefixUrl}/init/${mixDatabaseName}`;
260+
return this.api.get(endpoint);
261+
}
262+
263+
/**
264+
* Exports data for a Mixcore database. Returns ApiResult.
265+
*/
266+
async export(objData?: any): Promise<ApiResult> {
267+
let endpoint = `${this.prefixUrl}/export`;
268+
if (objData && typeof objData === 'object' && Object.keys(objData).length > 0) {
269+
const params = new URLSearchParams(objData).toString();
270+
endpoint += `?${params}`;
271+
}
272+
return this.api.get(endpoint);
273+
}
274+
275+
/**
276+
* Imports data for a Mixcore database. Returns ApiResult.
277+
*/
278+
async import(mixDatabaseName: string, file: File): Promise<ApiResult> {
279+
if (!mixDatabaseName) {
280+
return { isSucceed: false, errors: ['Missing mixDatabaseName'] };
281+
}
282+
if (!file) {
283+
return { isSucceed: false, errors: ['Missing file'] };
284+
}
285+
const endpoint = `${this.prefixUrl}/import-data/${mixDatabaseName}`;
286+
if (!mixDatabaseName) {
287+
return { isSucceed: false, errors: ['Missing mixDatabaseName'] };
288+
}
289+
if (!file) {
290+
return { isSucceed: false, errors: ['Missing file'] };
291+
}
292+
const formData = new FormData();
293+
formData.append('file', file);
294+
return this.api.post(endpoint, formData, { isFormData: true });
295+
}
296+
297+
/**
298+
* Migrates data for a Mixcore database. Returns ApiResult.
299+
*/
300+
async migrate(mixDatabaseId: string): Promise<ApiResult> {
301+
if (!mixDatabaseId) {
302+
return { isSucceed: false, errors: ['Missing mixDatabaseId'] };
303+
}
304+
const endpoint = `${this.prefixUrl}/migrate-data/${mixDatabaseId}`;
305+
return this.api.get(endpoint);
306+
}
307+
}
308+
<<<END>>>
309+
310+
// packages/file/src/file-services.ts
311+
@mixcore/file implementation:
312+
<<<CONTENT>>>
313+
import type { ApiService, FileServices } from '@mixcore/shared';
314+
315+
export class FileServicesPortal implements FileServices {
316+
private api: ApiService;
317+
private prefixUrl = '/file/';
318+
319+
constructor(api: ApiService) {
320+
this.api = api;
321+
}
322+
323+
async getFile(folder: string, filename: string) {
324+
const url = `${this.prefixUrl}details?folder=${encodeURIComponent(folder)}&filename=${encodeURIComponent(filename)}`;
325+
return this.api.get(url);
326+
}
327+
328+
async initFile(type: string) {
329+
return this.api.get(`${this.prefixUrl}init/${encodeURIComponent(type)}`);
330+
}
331+
332+
async getFiles(request: any) {
333+
return this.api.post(`${this.prefixUrl}list`, request);
334+
}
335+
336+
async removeFile(fullPath: string) {
337+
return this.api.get(`${this.prefixUrl}delete/?fullPath=${encodeURIComponent(fullPath)}`);
338+
}
339+
340+
async saveFile(file: any) {
341+
return this.api.post(`${this.prefixUrl}save`, file);
342+
}
343+
344+
async uploadFile(file: File, folder: string) {
345+
const url = `${this.prefixUrl}upload-file`;
346+
const formData = new FormData();
347+
formData.append('folder', folder);
348+
formData.append('file', file);
349+
// Use fetch directly for multipart/form-data
350+
const res = await fetch(url, {
351+
method: 'POST',
352+
body: formData,
353+
});
354+
if (!res.ok) throw new Error(`UPLOAD ${url}: ${res.status} ${res.statusText}`);
355+
return res.json();
356+
}
357+
}
358+
<<<END>>>

0 commit comments

Comments
 (0)