578 lines
17 KiB
TypeScript
578 lines
17 KiB
TypeScript
/**
|
|
* Tryton RPC Client for Node.js
|
|
* TypeScript implementation of sabatron-tryton-rpc-client
|
|
*/
|
|
|
|
import { ServerProxy, ServerPool } from "./jsonrpc";
|
|
|
|
import { ServerProxyOptions, ServerPoolOptions } from "./types";
|
|
|
|
// Constants
|
|
const CONNECT_TIMEOUT = 5000; // 5 seconds
|
|
const DEFAULT_TIMEOUT = 30000; // 30 seconds
|
|
import {
|
|
TrytonClientConfig,
|
|
TrytonClientOptions,
|
|
SearchDomain,
|
|
TrytonContext,
|
|
TrytonRecord,
|
|
TrytonUser,
|
|
UserPreferences,
|
|
LoginResult,
|
|
RecordId,
|
|
RecordIds,
|
|
FieldName,
|
|
ModelName,
|
|
ClientConfig,
|
|
TrytonMethodCall,
|
|
TypedModelOperations,
|
|
} from "./types";
|
|
|
|
/**
|
|
* Main client class for connecting to Tryton server via RPC
|
|
*/
|
|
export class TrytonClient {
|
|
private hostname: string;
|
|
private database: string;
|
|
private username: string;
|
|
private password: string;
|
|
private port: number;
|
|
private language: string;
|
|
private options: TrytonClientOptions;
|
|
private useHttps: boolean;
|
|
private connection: ServerPool | null;
|
|
private session: string | null;
|
|
|
|
/**
|
|
* Create a new Tryton client
|
|
*/
|
|
constructor(config: TrytonClientConfig) {
|
|
const {
|
|
hostname,
|
|
database,
|
|
username,
|
|
password,
|
|
port = 8000,
|
|
language = "en",
|
|
options = {},
|
|
} = config;
|
|
|
|
// Extract protocol from hostname if present
|
|
if (hostname.startsWith("https://")) {
|
|
this.hostname = hostname.replace("https://", "");
|
|
this.useHttps = true;
|
|
} else if (hostname.startsWith("http://")) {
|
|
this.hostname = hostname.replace("http://", "");
|
|
this.useHttps = false;
|
|
} else {
|
|
this.hostname = hostname;
|
|
this.useHttps = port === 443 || port === 8443;
|
|
}
|
|
|
|
this.database = database;
|
|
this.username = username;
|
|
this.password = password;
|
|
this.port = port;
|
|
this.language = language;
|
|
this.options = options;
|
|
this.connection = null;
|
|
this.session = null;
|
|
}
|
|
|
|
/**
|
|
* Alternative constructor for backward compatibility
|
|
*/
|
|
static create(
|
|
hostname: string,
|
|
database: string,
|
|
username: string,
|
|
password: string,
|
|
port: number = 8000,
|
|
language: string = "en"
|
|
): TrytonClient {
|
|
return new TrytonClient({
|
|
hostname,
|
|
database,
|
|
username,
|
|
password,
|
|
port,
|
|
language,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Connect to Tryton server and authenticate
|
|
* @returns {Promise<boolean>} - True if connection successful
|
|
* @throws {Error} - If connection or authentication fails
|
|
*/
|
|
async connect(): Promise<boolean> {
|
|
try {
|
|
const proxy = new ServerProxy(
|
|
this.hostname,
|
|
this.port,
|
|
this.database,
|
|
{
|
|
verbose: this.options.verbose || false,
|
|
connectTimeout:
|
|
this.options.connectTimeout || CONNECT_TIMEOUT,
|
|
timeout: this.options.timeout || DEFAULT_TIMEOUT,
|
|
useHttps: this.useHttps,
|
|
}
|
|
);
|
|
|
|
// Perform login
|
|
const parameters = {
|
|
password: this.password,
|
|
};
|
|
|
|
const result = await proxy.request<LoginResult>("common.db.login", [
|
|
this.username,
|
|
parameters,
|
|
this.language,
|
|
]);
|
|
|
|
proxy.close();
|
|
|
|
this.session = [this.username, ...result].join(":");
|
|
|
|
// Create connection pool with session
|
|
this.connection = new ServerPool(
|
|
this.hostname,
|
|
this.port,
|
|
this.database,
|
|
{
|
|
session: this.session,
|
|
cache: this.options.cache !== false ? [] : null, // Enable cache by default
|
|
verbose: this.options.verbose || false,
|
|
connectTimeout:
|
|
this.options.connectTimeout || CONNECT_TIMEOUT,
|
|
timeout: this.options.timeout || DEFAULT_TIMEOUT,
|
|
keepMax: this.options.keepMax || 4,
|
|
useHttps: this.useHttps,
|
|
}
|
|
);
|
|
|
|
return true;
|
|
} catch (error) {
|
|
throw new Error(`Connection failed: ${(error as Error).message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Call RPC method on server
|
|
* @param {string} methodName - RPC method name (e.g., 'model.party.party.read')
|
|
* @param {Array} args - Method arguments
|
|
* @returns {Promise<*>} - Method result
|
|
* @throws {Error} - If not connected or method call fails
|
|
*/
|
|
async call<T = any>(methodName: string, args: any[] = []): Promise<T> {
|
|
if (!this.connection) {
|
|
throw new Error("Not connected. Call connect() first.");
|
|
}
|
|
|
|
return this.connection.withConnection(async (conn) => {
|
|
return conn.request<T>(methodName, args);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Call multiple RPC methods in sequence
|
|
* @param {Array<{method: string, args: Array}>} calls - Array of method calls
|
|
* @returns {Promise<Array>} - Array of results
|
|
*/
|
|
async callMultiple(calls: TrytonMethodCall[]): Promise<any[]> {
|
|
const results: any[] = [];
|
|
for (const call of calls) {
|
|
const result = await this.call(call.method, call.args);
|
|
results.push(result);
|
|
}
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Call multiple RPC methods in parallel
|
|
* @param {Array<{method: string, args: Array}>} calls - Array of method calls
|
|
* @returns {Promise<Array>} - Array of results
|
|
*/
|
|
async callParallel(calls: TrytonMethodCall[]): Promise<any[]> {
|
|
const promises = calls.map((call) => this.call(call.method, call.args));
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
/**
|
|
* Helper method to read records from a model
|
|
* @param {string} model - Model name (e.g., 'party.party')
|
|
* @param {Array<number>} ids - Record IDs to read
|
|
* @param {Array<string>} fields - Fields to read
|
|
* @param {Object} [context={}] - Context dictionary
|
|
* @returns {Promise<Array>} - Array of records
|
|
*/
|
|
async read<T extends TrytonRecord = TrytonRecord>(
|
|
model: ModelName,
|
|
ids: RecordIds,
|
|
fields: FieldName[],
|
|
context: TrytonContext = {}
|
|
): Promise<T[]> {
|
|
return this.call<T[]>(`model.${model}.read`, [ids, fields, context]);
|
|
}
|
|
|
|
/**
|
|
* Helper method to create records in a model
|
|
* @param {string} model - Model name (e.g., 'party.party')
|
|
* @param {Array<Object>} records - Records to create
|
|
* @param {Object} [context={}] - Context dictionary
|
|
* @returns {Promise<Array<number>>} - Array of created record IDs
|
|
*/
|
|
async create(
|
|
model: ModelName,
|
|
records: Record<string, any>[],
|
|
context: TrytonContext = {}
|
|
): Promise<RecordIds> {
|
|
return this.call<RecordIds>(`model.${model}.create`, [
|
|
records,
|
|
context,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Helper method to write/update records in a model
|
|
* @param {string} model - Model name (e.g., 'party.party')
|
|
* @param {Array<number>} ids - Record IDs to update
|
|
* @param {Object} values - Values to update
|
|
* @param {Object} [context={}] - Context dictionary
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async write(
|
|
model: ModelName,
|
|
ids: RecordIds,
|
|
values: Record<string, any>,
|
|
context: TrytonContext = {}
|
|
): Promise<void> {
|
|
return this.call<void>(`model.${model}.write`, [ids, values, context]);
|
|
}
|
|
|
|
/**
|
|
* Helper method to delete records from a model
|
|
* @param {string} model - Model name (e.g., 'party.party')
|
|
* @param {Array<number>} ids - Record IDs to delete
|
|
* @param {Object} [context={}] - Context dictionary
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async delete(
|
|
model: ModelName,
|
|
ids: RecordIds,
|
|
context: TrytonContext = {}
|
|
): Promise<void> {
|
|
return this.call<void>(`model.${model}.delete`, [ids, context]);
|
|
}
|
|
|
|
/**
|
|
* Helper method to search for records
|
|
* @param {string} model - Model name (e.g., 'party.party')
|
|
* @param {Array} domain - Search domain
|
|
* @param {number} [offset=0] - Offset for pagination
|
|
* @param {number} [limit=null] - Limit for pagination
|
|
* @param {Array<string>} [order=null] - Order specification
|
|
* @param {Object} [context={}] - Context dictionary
|
|
* @returns {Promise<Array<number>>} - Array of record IDs
|
|
*/
|
|
async search(
|
|
model: ModelName,
|
|
domain: SearchDomain,
|
|
offset: number = 0,
|
|
limit: number | null = null,
|
|
order: string[] | null = null,
|
|
context: TrytonContext = {}
|
|
): Promise<RecordIds> {
|
|
return this.call<RecordIds>(`model.${model}.search`, [
|
|
domain,
|
|
offset,
|
|
limit,
|
|
order,
|
|
context,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Helper method to search and read records in one call
|
|
* @param {string} model - Model name (e.g., 'party.party')
|
|
* @param {Array} domain - Search domain
|
|
* @param {Array<string>} fields - Fields to read
|
|
* @param {number} [offset=0] - Offset for pagination
|
|
* @param {number} [limit=null] - Limit for pagination
|
|
* @param {Array<string>} [order=null] - Order specification
|
|
* @param {Object} [context={}] - Context dictionary
|
|
* @returns {Promise<Array>} - Array of records
|
|
*/
|
|
async searchRead<T extends TrytonRecord = TrytonRecord>(
|
|
model: ModelName,
|
|
domain: SearchDomain,
|
|
fields: FieldName[],
|
|
offset: number = 0,
|
|
limit: number | null = null,
|
|
order: string[] | null = null,
|
|
context: TrytonContext = {}
|
|
): Promise<T[]> {
|
|
return this.call<T[]>(`model.${model}.search_read`, [
|
|
domain,
|
|
offset,
|
|
limit,
|
|
order,
|
|
fields,
|
|
context,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Helper method to count records
|
|
* @param {string} model - Model name (e.g., 'party.party')
|
|
* @param {Array} domain - Search domain
|
|
* @param {Object} [context={}] - Context dictionary
|
|
* @returns {Promise<number>} - Number of records
|
|
*/
|
|
async searchCount(
|
|
model: ModelName,
|
|
domain: SearchDomain,
|
|
context: TrytonContext = {}
|
|
): Promise<number> {
|
|
return this.call<number>(`model.${model}.search_count`, [
|
|
domain,
|
|
context,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get database information
|
|
* @returns {Promise<Object>} - Database info
|
|
*/
|
|
async getDatabaseInfo(): Promise<any> {
|
|
return this.call("common.db.get_info", []);
|
|
}
|
|
|
|
/**
|
|
* List available databases
|
|
* @returns {Promise<Array<string>>} - Database names
|
|
*/
|
|
async listDatabases(): Promise<string[]> {
|
|
return this.call<string[]>("common.db.list", []);
|
|
}
|
|
|
|
/**
|
|
* Get server version
|
|
* @returns {Promise<string>} - Server version
|
|
*/
|
|
async getVersion(): Promise<string> {
|
|
return this.call<string>("common.version", []);
|
|
}
|
|
|
|
/**
|
|
* Get user preferences
|
|
*/
|
|
async getUserPreferences(): Promise<UserPreferences> {
|
|
return this.call<UserPreferences>("model.res.user.get_preferences", [
|
|
true,
|
|
{},
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get current user information
|
|
*/
|
|
async getCurrentUser(): Promise<TrytonUser> {
|
|
const preferences = await this.getUserPreferences();
|
|
const users = await this.read<TrytonUser>(
|
|
"res.user",
|
|
[preferences.id],
|
|
["id", "name", "login", "language", "company", "email"]
|
|
);
|
|
if (!users || users.length === 0) {
|
|
throw new Error("User not found");
|
|
}
|
|
return users[0]!;
|
|
}
|
|
|
|
/**
|
|
* Clear cache for specific prefix or all
|
|
* @param {string} [prefix] - Optional prefix to clear
|
|
*/
|
|
clearCache(prefix?: string): void {
|
|
if (this.connection) {
|
|
this.connection.clearCache(prefix);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get connection SSL status
|
|
* @returns {boolean|null} - SSL status or null if not connected
|
|
*/
|
|
get ssl(): boolean | null {
|
|
return this.connection ? this.connection.ssl : null;
|
|
}
|
|
|
|
/**
|
|
* Get connection URL
|
|
* @returns {string|null} - Connection URL or null if not connected
|
|
*/
|
|
get url(): string | null {
|
|
return this.connection ? this.connection.url : null;
|
|
}
|
|
|
|
/**
|
|
* Check if client is connected
|
|
* @returns {boolean} - True if connected
|
|
*/
|
|
get isConnected(): boolean {
|
|
return this.connection !== null;
|
|
}
|
|
|
|
/**
|
|
* Get current session
|
|
* @returns {string|null} - Session string or null if not connected
|
|
*/
|
|
getSession(): string | null {
|
|
return this.session;
|
|
}
|
|
|
|
/**
|
|
* Get current user ID from session
|
|
*/
|
|
getUserId(): number | null {
|
|
if (!this.session) return null;
|
|
const parts = this.session.split(":");
|
|
return parts.length >= 2 ? parseInt(parts[1], 10) : null;
|
|
}
|
|
|
|
/**
|
|
* Get session token from session
|
|
*/
|
|
getSessionToken(): string | null {
|
|
if (!this.session) return null;
|
|
const parts = this.session.split(":");
|
|
return parts.length >= 3 ? parts[2] : null;
|
|
}
|
|
|
|
/**
|
|
* Get username
|
|
*/
|
|
getUsername(): string {
|
|
return this.username;
|
|
}
|
|
|
|
/**
|
|
* Close connection and cleanup resources
|
|
*/
|
|
close(): void {
|
|
if (this.connection) {
|
|
this.connection.close();
|
|
this.connection = null;
|
|
this.session = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new client instance with the same configuration
|
|
* @returns {TrytonClient} - New client instance
|
|
*/
|
|
clone(): TrytonClient {
|
|
return new TrytonClient({
|
|
hostname: this.hostname,
|
|
database: this.database,
|
|
username: this.username,
|
|
password: this.password,
|
|
port: this.port,
|
|
language: this.language,
|
|
options: { ...this.options },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get client configuration (without sensitive data)
|
|
* @returns {Object} - Client configuration
|
|
*/
|
|
getConfig(): ClientConfig {
|
|
return {
|
|
hostname: this.hostname,
|
|
database: this.database,
|
|
username: this.username,
|
|
port: this.port,
|
|
language: this.language,
|
|
isConnected: this.isConnected,
|
|
ssl: this.ssl,
|
|
url: this.url,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Type-safe model operations factory
|
|
* Creates a typed interface for specific models
|
|
*/
|
|
model<T extends TrytonRecord = TrytonRecord>(
|
|
modelName: ModelName
|
|
): TypedModelOperations<T> {
|
|
return {
|
|
read: (
|
|
ids: RecordIds,
|
|
fields: FieldName[],
|
|
context?: TrytonContext
|
|
) => this.read<T>(modelName, ids, fields, context),
|
|
|
|
create: (
|
|
records: Partial<Omit<T, "id">>[],
|
|
context?: TrytonContext
|
|
) =>
|
|
this.create(
|
|
modelName,
|
|
records as Record<string, any>[],
|
|
context
|
|
),
|
|
|
|
write: (
|
|
ids: RecordIds,
|
|
values: Partial<Omit<T, "id">>,
|
|
context?: TrytonContext
|
|
) =>
|
|
this.write(
|
|
modelName,
|
|
ids,
|
|
values as Record<string, any>,
|
|
context
|
|
),
|
|
|
|
delete: (ids: RecordIds, context?: TrytonContext) =>
|
|
this.delete(modelName, ids, context),
|
|
|
|
search: (
|
|
domain: SearchDomain,
|
|
offset?: number,
|
|
limit?: number,
|
|
order?: string[],
|
|
context?: TrytonContext
|
|
) => this.search(modelName, domain, offset, limit, order, context),
|
|
|
|
searchRead: (
|
|
domain: SearchDomain,
|
|
fields: FieldName[],
|
|
offset?: number,
|
|
limit?: number,
|
|
order?: string[],
|
|
context?: TrytonContext
|
|
) =>
|
|
this.searchRead<T>(
|
|
modelName,
|
|
domain,
|
|
fields,
|
|
offset,
|
|
limit,
|
|
order,
|
|
context
|
|
),
|
|
|
|
searchCount: (domain: SearchDomain, context?: TrytonContext) =>
|
|
this.searchCount(modelName, domain, context),
|
|
};
|
|
}
|
|
}
|
|
|
|
// Export for CommonJS compatibility
|
|
export default TrytonClient;
|