UNPKG

zdata-client

Version:

TypeScript client library for zdata backend API with authentication and full CRUD operations

868 lines (854 loc) 24.7 kB
import axios from 'axios'; import { z, ZodError } from 'zod'; // zdata-client - TypeScript client for zdata backend API // src/core/errors/http-error.ts var HTTP_STATUS = { CLIENT_ERROR_START: 400, SERVER_ERROR_START: 500 }; var HttpError = class _HttpError extends Error { constructor(message, statusCode, response) { super(message); this.name = "HttpError"; this.statusCode = statusCode; this.response = response; Object.setPrototypeOf(this, _HttpError.prototype); } isClientError() { return Boolean( this.statusCode && this.statusCode >= HTTP_STATUS.CLIENT_ERROR_START && this.statusCode < HTTP_STATUS.SERVER_ERROR_START ); } isServerError() { return Boolean( this.statusCode && this.statusCode >= HTTP_STATUS.SERVER_ERROR_START ); } isNetworkError() { return !this.statusCode; } }; // src/core/http/axios-http-client.ts var TIMEOUT = { DEFAULT_MS: 1e4 }; var AxiosHttpClient = class { constructor(baseURL, defaultTimeout = TIMEOUT.DEFAULT_MS) { this.instance = axios.create({ baseURL, timeout: defaultTimeout, headers: { "Content-Type": "application/json", Accept: "application/json" } }); } async request(request) { try { const config = { url: request.url, method: request.method, ...request.headers !== void 0 && { headers: request.headers }, ...request.data !== void 0 && { data: request.data }, ...request.params !== void 0 && { params: request.params }, ...request.timeout !== void 0 && { timeout: request.timeout } }; const response = await this.instance.request(config); return { data: response.data, status: response.status, headers: response.headers }; } catch (error) { throw this.handleError(error); } } handleError(error) { if (axios.isAxiosError(error)) { const axiosError = error; return new HttpError( axiosError.message, axiosError.response?.status, axiosError.response?.data ); } if (error instanceof Error) { return new HttpError(error.message); } return new HttpError("Unknown HTTP error occurred"); } }; // src/core/errors/api-errors.ts var InvalidCredentialsError = class _InvalidCredentialsError extends Error { constructor(message = "Invalid credentials") { super(message); this.name = "InvalidCredentialsError"; Object.setPrototypeOf(this, _InvalidCredentialsError.prototype); } }; var ValidationError = class _ValidationError extends Error { constructor(message = "Validation error", errors = []) { super(message); this.name = "ValidationError"; this.errors = errors; Object.setPrototypeOf(this, _ValidationError.prototype); } }; var ApiClientError = class _ApiClientError extends Error { constructor(message, statusCode) { super(message); this.name = "ApiClientError"; if (statusCode !== void 0) { this.statusCode = statusCode; } Object.setPrototypeOf(this, _ApiClientError.prototype); } }; function isInvalidCredentialsError(error) { return error instanceof InvalidCredentialsError; } function isValidationError(error) { return error instanceof ValidationError; } function isApiClientError(error) { return error instanceof ApiClientError; } // src/features/validation/validator.ts var Validator = class { static validate(schema, data) { try { return schema.parse(data); } catch (error) { if (error instanceof ZodError) { throw new ValidationError( "Validation failed", this.mapZodErrorsToValidationDetails(error) ); } throw error; } } static validateSafely(schema, data) { try { const validData = schema.parse(data); return { success: true, data: validData }; } catch (error) { if (error instanceof ZodError) { return { success: false, error: new ValidationError( "Validation failed", this.mapZodErrorsToValidationDetails(error) ) }; } return { success: false, error: new ValidationError("Unknown validation error") }; } } static mapZodErrorsToValidationDetails(error) { return error.issues.map((err) => ({ code: err.code, path: err.path.map(String), message: err.message })); } }; var VALIDATION_LIMITS = { MIN_PASSWORD_LENGTH: 6, MAX_LIMIT: 100 }; var ApiConfigSchema = z.object({ baseUrl: z.string().url("Base URL must be a valid URL"), workspaceId: z.string().min(1, "Workspace ID is required"), timeout: z.number().min(1).max(3e5).optional(), headers: z.record(z.string(), z.string()).optional() }); var LoginRequestSchema = z.object({ email: z.string().email("Invalid email address"), password: z.string().min(1, "Password is required") }); var RegisterRequestSchema = z.object({ name: z.string().min(1, "Name is required"), email: z.string().email("Invalid email address"), password: z.string().min( VALIDATION_LIMITS.MIN_PASSWORD_LENGTH, "Password must be at least 6 characters" ) }); var FindRecordsParamsSchema = z.object({ resourceName: z.string().min(1, "Resource name is required"), search: z.string().optional(), page: z.number().min(1).optional(), limit: z.number().min(1).max(VALIDATION_LIMITS.MAX_LIMIT).optional() }); var BaseEntitySchema = z.object({ id: z.string(), created_at: z.string(), updated_at: z.string() }); var PaginationMetaSchema = z.object({ activePageNumber: z.number(), limit: z.number(), totalRecords: z.number(), totalPages: z.number(), hasNext: z.boolean(), hasPrev: z.boolean() }); var PaginatedResponseSchema = (recordSchema) => z.object({ records: z.array(recordSchema), meta: PaginationMetaSchema }); var AuthResponseSchema = z.object({ access_token: z.string(), expires_in: z.number(), token_type: z.string(), user: z.object({ id: z.string(), email: z.string().email(), name: z.string() }) }); var UserSchema = z.object({ id: z.string(), email: z.string().email(), name: z.string() }); var ApiErrorSchema = z.object({ message: z.string(), errors: z.array( z.object({ field: z.string(), message: z.string() }) ).optional() }); var LoggedUserSchema = z.object({ id: z.string(), name: z.string(), email: z.string().email(), settings: z.object({ themeMode: z.enum(["light", "dark"]) }) }); var UpdateLoggedUserSchema = z.object({ name: z.string().nullish(), themeMode: z.enum(["light", "dark"]).nullish() }); var FieldMetricsParamsSchema = z.object({ resourceName: z.string().min(1, "Resource name is required"), fields: z.array(z.string()).optional() }); var DistributionItemSchema = z.object({ value: z.string(), count: z.number(), percentage: z.number() }); var YearDistributionItemSchema = z.object({ year: z.number(), count: z.number() }); var NumericFieldStatsSchema = z.object({ sum: z.number(), average: z.number(), minimum: z.number(), maximum: z.number(), count: z.number(), totalRecords: z.number(), standardDeviation: z.number(), nullCount: z.number() }); var EnumFieldStatsSchema = z.object({ distribution: z.array(DistributionItemSchema), totalRecords: z.number(), nonNullRecords: z.number(), nullCount: z.number(), uniqueValues: z.number(), mostCommon: z.string() }); var BooleanFieldStatsSchema = z.object({ distribution: z.array(DistributionItemSchema), trueCount: z.number(), falseCount: z.number(), nullCount: z.number(), totalRecords: z.number(), truePercentage: z.number(), falsePercentage: z.number(), nullPercentage: z.number() }); var DateFieldStatsSchema = z.object({ earliest: z.string(), latest: z.string(), count: z.number(), totalRecords: z.number(), nullCount: z.number(), yearDistribution: z.array(YearDistributionItemSchema), mostActiveYear: z.number() }); var GenericFieldStatsSchema = z.object({ message: z.string() }); var FieldMetricSchema = z.object({ type: z.string(), title: z.string().optional(), description: z.string().optional(), stats: z.union([ NumericFieldStatsSchema, EnumFieldStatsSchema, BooleanFieldStatsSchema, DateFieldStatsSchema, GenericFieldStatsSchema ]) }); var MetricsResponseSchema = z.object({ entity: z.string(), workspace: z.string(), metrics: z.record(z.string(), FieldMetricSchema) }); // src/auth/login.ts var HTTP_STATUS2 = { UNAUTHORIZED: 401 }; async function login(baseUrl, workspaceId, credentials, timeout) { const validatedCredentials = Validator.validate( LoginRequestSchema, credentials ); const apiUrl = `${baseUrl}/api/v1/${workspaceId}`; const httpClient = new AxiosHttpClient(apiUrl, timeout); try { const response = await httpClient.request({ method: "POST", url: "/login", data: validatedCredentials }); return Validator.validate(AuthResponseSchema, response.data); } catch (error) { if (error instanceof HttpError && error.statusCode === HTTP_STATUS2.UNAUTHORIZED) { throw new InvalidCredentialsError("Invalid email or password"); } if (error instanceof HttpError) { throw new ApiClientError(error.message, error.statusCode); } throw error; } } // src/auth/register.ts async function register(baseUrl, workspaceId, userData, timeout) { const validatedUserData = Validator.validate(RegisterRequestSchema, userData); const apiUrl = `${baseUrl}/api/v1/${workspaceId}`; const httpClient = new AxiosHttpClient(apiUrl, timeout); try { const response = await httpClient.request({ method: "POST", url: "/register", data: validatedUserData }); return Validator.validate(AuthResponseSchema, response.data); } catch (error) { if (error instanceof HttpError) { throw new ApiClientError(error.message, error.statusCode); } throw error; } } var _ResourceRepository = class _ResourceRepository { constructor(baseUrl, workspaceId, accessToken, timeout, cache) { this.cache = cache; const apiUrl = `${baseUrl}/api/v1/${workspaceId}`; this.httpClient = new AxiosHttpClient(apiUrl, timeout); this.accessToken = accessToken; } async createRecord(resourceName, data) { this.validateResourceName(resourceName); try { const response = await this.httpClient.request({ method: "POST", url: `/${resourceName}`, data, headers: this.getAuthHeader() }); this.invalidateCacheForResource(resourceName); return response.data; } catch (error) { throw this.handleError(error); } } async updateRecord(resourceName, id, data) { this.validateResourceName(resourceName); this.validateId(id); try { const response = await this.httpClient.request({ method: "PUT", url: `/${resourceName}/${id}`, data, headers: this.getAuthHeader() }); this.invalidateCacheForResource(resourceName); this.invalidateCacheForRecord(resourceName, id); return response.data; } catch (error) { throw this.handleError(error); } } async deleteRecord(resourceName, id) { this.validateResourceName(resourceName); this.validateId(id); try { await this.httpClient.request({ method: "DELETE", url: `/${resourceName}/${id}`, headers: this.getAuthHeader() }); this.invalidateCacheForResource(resourceName); this.invalidateCacheForRecord(resourceName, id); } catch (error) { throw this.handleError(error); } } async findRecordById(resourceName, id) { this.validateResourceName(resourceName); this.validateId(id); const cacheKey = this.getCacheKey(resourceName, id); if (this.cache?.has(cacheKey)) { const cached = this.cache.get(cacheKey); if (cached) { return cached; } } try { const response = await this.httpClient.request({ method: "GET", url: `/${resourceName}/${id}`, headers: this.getAuthHeader() }); const result = response.data; if (this.cache) { this.cache.set(cacheKey, result, _ResourceRepository.CACHE_TTL_MS); } return result; } catch (error) { throw this.handleError(error); } } async findRecords(params) { const validatedParams = Validator.validate(FindRecordsParamsSchema, params); const cacheKey = this.getCacheKey( validatedParams.resourceName, "list", JSON.stringify(validatedParams) ); if (this.cache?.has(cacheKey)) { const cached = this.cache.get(cacheKey); if (cached) { return cached; } } try { const searchParams = this.buildSearchParams(validatedParams); const response = await this.httpClient.request({ method: "GET", url: `/${validatedParams.resourceName}`, params: searchParams, headers: this.getAuthHeader() }); const schema = PaginatedResponseSchema(z.unknown()); const validatedResponse = Validator.validate(schema, response.data); const result = validatedResponse; if (this.cache) { this.cache.set(cacheKey, result, _ResourceRepository.CACHE_TTL_MS); } return result; } catch (error) { throw this.handleError(error); } } validateResourceName(resourceName) { if (!resourceName || typeof resourceName !== "string") { throw new Error( "Resource name is required and must be a non-empty string" ); } if (resourceName.trim() !== resourceName) { throw new Error( "Resource name cannot have leading or trailing whitespace" ); } } validateId(id) { if (!id || typeof id !== "string") { throw new Error("ID is required and must be a non-empty string"); } if (id.trim() !== id) { throw new Error("ID cannot have leading or trailing whitespace"); } } buildSearchParams(params) { const searchParams = { page: String(params.page ?? 1), limit: String(params.limit ?? _ResourceRepository.DEFAULT_PAGE_SIZE) }; if (params.search) { searchParams.search = params.search; } return searchParams; } handleError(error) { if (error instanceof HttpError) { return new ApiClientError(error.message, error.statusCode); } if (error instanceof Error) { return error; } return new Error("Unknown error occurred"); } getCacheKey(...parts) { return parts.join(":"); } invalidateCacheForResource(_resourceName) { if (!this.cache) { return; } this.cache.clear(); } invalidateCacheForRecord(resourceName, id) { if (!this.cache) { return; } const cacheKey = this.getCacheKey(resourceName, id); this.cache.delete(cacheKey); } async getMetrics(resourceName, fields) { this.validateResourceName(resourceName); const validatedParams = Validator.validate(FieldMetricsParamsSchema, { resourceName, fields }); const cacheKey = this.getCacheKey( validatedParams.resourceName, "metrics", fields ? fields.join(",") : "all" ); if (this.cache?.has(cacheKey)) { const cached = this.cache.get(cacheKey); if (cached) { return cached; } } try { const searchParams = {}; if (fields && fields.length > 0) { searchParams.fields = fields.join(","); } const requestConfig = { method: "GET", url: `/${validatedParams.resourceName}/metrics`, headers: this.getAuthHeader(), ...Object.keys(searchParams).length > 0 && { params: searchParams } }; const response = await this.httpClient.request(requestConfig); const validatedResponse = Validator.validate( MetricsResponseSchema, response.data ); if (this.cache) { this.cache.set( cacheKey, validatedResponse, _ResourceRepository.CACHE_TTL_MS ); } return validatedResponse; } catch (error) { throw this.handleError(error); } } getAuthHeader() { return { Authorization: `Bearer ${this.accessToken}` }; } }; _ResourceRepository.CACHE_TTL_MS = 3e5; // 5 minutes in milliseconds _ResourceRepository.DEFAULT_PAGE_SIZE = 10; var ResourceRepository = _ResourceRepository; // src/clients/logged-user-client.ts var LoggedUserClient = class { constructor(baseUrl, workspaceId, accessToken, timeout) { const apiUrl = `${baseUrl}/api/v1/${workspaceId}`; this.httpClient = new AxiosHttpClient(apiUrl, timeout); this.accessToken = accessToken; } async getProfile() { try { const response = await this.httpClient.request({ method: "GET", url: "/me", headers: this.getAuthHeader() }); return Validator.validate(LoggedUserSchema, response.data); } catch (error) { throw this.handleError(error); } } async updateProfile(data) { const validatedData = Validator.validate(UpdateLoggedUserSchema, data); try { const response = await this.httpClient.request({ method: "PUT", url: "/me", data: validatedData, headers: this.getAuthHeader() }); return Validator.validate(LoggedUserSchema, response.data); } catch (error) { throw this.handleError(error); } } handleError(error) { if (error instanceof HttpError) { return new ApiClientError(error.message, error.statusCode); } if (error instanceof Error) { return error; } return new Error("Unknown error occurred"); } getAuthHeader() { return { Authorization: `Bearer ${this.accessToken}` }; } }; // src/base/base-client.ts var BaseDataSourceClient = class { /** * Create a new data source client instance * * @param baseUrl - API base URL * @param workspaceId - Workspace ID * @param accessToken - Access token for authentication * @param resourceName - The name of the resource/endpoint (e.g., 'users', 'payments') * @param timeout - Request timeout in milliseconds */ constructor(baseUrl, workspaceId, accessToken, resourceName, timeout) { this.repository = new ResourceRepository( baseUrl, workspaceId, accessToken, timeout ); this.resourceName = resourceName; } /** * Create a new record of type T * * @param data - Record data without base fields * @returns Promise resolving to created record with base fields */ async create(data) { return this.repository.createRecord(this.resourceName, data); } /** * Find a record by its ID * * @param id - Record ID * @returns Promise resolving to the record or null if not found */ async findById(id) { try { return await this.repository.findRecordById( this.resourceName, id ); } catch { return null; } } /** * Find records with pagination and search * * @param params - Search and pagination parameters * @returns Promise resolving to paginated results */ async find(params = {}) { return this.repository.findRecords({ resourceName: this.resourceName, ...params }); } /** * Update a record by its ID * * @param id - Record ID * @param data - Partial data to update * @returns Promise resolving to updated record */ async update(id, data) { return this.repository.updateRecord(this.resourceName, id, data); } /** * Delete a record by its ID * * @param id - Record ID * @returns Promise resolving when record is deleted */ async delete(id) { return this.repository.deleteRecord(this.resourceName, id); } /** * Get metrics for this resource * * @param fields - Optional array of field names to analyze. If not provided, analyzes all fields * @returns Promise resolving to metrics data */ async getMetrics(fields) { const fieldNames = fields?.map((field) => String(field)); return this.repository.getMetrics(this.resourceName, fieldNames); } /** * Get the resource name for this client * * @returns The resource name */ getResourceName() { return this.resourceName; } }; var DataSourceClient = class extends BaseDataSourceClient { /** * Create a new data source client instance * * @param baseUrl - API base URL * @param workspaceId - Workspace ID * @param accessToken - Access token for authentication * @param resourceName - The name of the resource/endpoint * @param timeout - Request timeout in milliseconds */ constructor(baseUrl, workspaceId, accessToken, resourceName, timeout) { super(baseUrl, workspaceId, accessToken, resourceName, timeout); } }; // src/features/cache/memory-cache.ts var MemoryCache = class { constructor(config) { this.cache = /* @__PURE__ */ new Map(); this.defaultTtlMs = config.defaultTtlMs; if (config.maxSize !== void 0) { this.maxSize = config.maxSize; } } get(key) { const entry = this.cache.get(key); if (!entry) { return null; } if (Date.now() > entry.expiresAt) { this.cache.delete(key); return null; } return entry.value; } set(key, value, ttlMs) { if (this.maxSize && this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value; if (firstKey) { this.cache.delete(firstKey); } } const expiresAt = Date.now() + (ttlMs ?? this.defaultTtlMs); this.cache.set(key, { value, expiresAt }); } delete(key) { this.cache.delete(key); } clear() { this.cache.clear(); } has(key) { const entry = this.cache.get(key); if (!entry) { return false; } if (Date.now() > entry.expiresAt) { this.cache.delete(key); return false; } return true; } size() { return this.cache.size; } cleanup() { const now = Date.now(); for (const [key, entry] of this.cache.entries()) { if (now > entry.expiresAt) { this.cache.delete(key); } } } }; // src/features/retry/retry-policy.ts var RETRYABLE_STATUS_CODES = { REQUEST_TIMEOUT: 408, TOO_MANY_REQUESTS: 429, BAD_GATEWAY: 502, SERVICE_UNAVAILABLE: 503, GATEWAY_TIMEOUT: 504 }; var DEFAULT_RETRY_CONFIG = { maxAttempts: 3, baseDelayMs: 1e3, maxDelayMs: 1e4, backoffMultiplier: 2, jitterMs: 100 }; var RetryPolicy = class { constructor(config = DEFAULT_RETRY_CONFIG) { this.config = config; } async execute(operation) { let lastError = null; for (let attempt = 1; attempt <= this.config.maxAttempts; attempt++) { try { return await operation(); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (attempt === this.config.maxAttempts || !this.shouldRetry(error)) { break; } await this.delay(this.calculateDelay(attempt)); } } throw lastError ?? new Error("Unknown retry error"); } shouldRetry(error) { if (error instanceof Error) { return error.name === "HttpError" && this.isRetryableStatusCode(error); } return false; } isRetryableStatusCode(error) { if (!error.statusCode) { return true; } return Object.values(RETRYABLE_STATUS_CODES).includes(error.statusCode); } calculateDelay(attempt) { const exponentialDelay = this.config.baseDelayMs * Math.pow(this.config.backoffMultiplier, attempt - 1); const delayWithJitter = exponentialDelay + Math.random() * this.config.jitterMs; return Math.min(delayWithJitter, this.config.maxDelayMs); } async delay(ms) { return new Promise((resolve) => { globalThis.setTimeout(resolve, ms); }); } }; // src/index.ts function createRepository(baseUrl, workspaceId, accessToken, timeout) { return new ResourceRepository(baseUrl, workspaceId, accessToken, timeout); } var VERSION = "2.0.0"; export { ApiClientError, ApiConfigSchema, ApiErrorSchema, AuthResponseSchema, AxiosHttpClient, BaseDataSourceClient, BaseEntitySchema, DataSourceClient, FindRecordsParamsSchema, InvalidCredentialsError, LoggedUserClient, LoggedUserSchema, LoginRequestSchema, MemoryCache, PaginationMetaSchema, RegisterRequestSchema, ResourceRepository, RetryPolicy, UpdateLoggedUserSchema, UserSchema, VERSION, ValidationError, Validator, createRepository, ResourceRepository as default, isApiClientError, isInvalidCredentialsError, isValidationError, login, register }; //# sourceMappingURL=index.mjs.map //# sourceMappingURL=index.mjs.map