zdata-client
Version:
TypeScript client library for zdata backend API with authentication and full CRUD operations
868 lines (854 loc) • 24.7 kB
JavaScript
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