@smartsamurai/krapi-sdk
Version:
KRAPI TypeScript SDK - Easy-to-use client SDK for connecting to self-hosted KRAPI servers (like Appwrite SDK)
523 lines (490 loc) • 15 kB
text/typescript
/**
* Collections HTTP Client for KRAPI SDK
*
* HTTP-based collections and documents management for frontend applications.
* Provides collection CRUD operations and document management.
*
* @module http-clients/collections-http-client
* @example
* const client = new CollectionsHttpClient({ baseUrl: 'https://api.example.com' });
* const collections = await client.getCollectionsByProject('project-id');
*/
import {
Document,
DocumentFilter,
CreateDocumentRequest,
UpdateDocumentRequest,
} from "../collections-service";
import { ApiResponse, PaginatedResponse, QueryOptions } from "../core";
import { BaseHttpClient } from "./base-http-client";
/**
* Collection Interface
*
* @interface Collection
* @property {string} id - Collection ID
* @property {string} project_id - Project ID
* @property {string} name - Collection name
* @property {string} [description] - Collection description
* @property {Array} fields - Collection field definitions
* @property {Array} indexes - Collection index definitions
* @property {string} created_at - Creation timestamp
* @property {string} updated_at - Update timestamp
*/
export interface Collection {
id: string;
project_id: string;
name: string;
description?: string;
fields: Array<{
name: string;
type: string;
required: boolean;
unique: boolean;
indexed: boolean;
default?: unknown;
validation?: Record<string, unknown>;
}>;
indexes: Array<{
name: string;
fields: string[];
unique: boolean;
}>;
created_at: string;
updated_at: string;
}
/**
* Collections HTTP Client
*
* HTTP client for collections and documents management.
*
* @class CollectionsHttpClient
* @extends {BaseHttpClient}
* @example
* const client = new CollectionsHttpClient({ baseUrl: 'https://api.example.com' });
* const collection = await client.createCollection('project-id', { name: 'users', fields: [...] });
*/
export class CollectionsHttpClient extends BaseHttpClient {
// Constructor inherited from BaseHttpClient
/**
* Get all collections for a project
*
* @param {string} projectId - Project ID
* @returns {Promise<Collection[]>} Array of collections
*
* @example
* const collections = await client.getCollectionsByProject('project-id');
*/
async getCollectionsByProject(projectId: string): Promise<Collection[]> {
const response = await this.get<Collection[]>(
`/projects/${projectId}/collections`
);
return response.data || [];
}
/**
* Get all documents in a collection
*
* @param {string} projectId - Project ID
* @param {string} collectionName - Collection name (not ID)
* @param {Object} [options] - Query options
* @returns {Promise<Document[]>} Array of documents
*
* @example
* const documents = await client.getDocuments('project-id', 'collection-name', { limit: 10 });
*/
async getDocuments(
projectId: string,
collectionName: string,
options?: {
page?: number;
limit?: number;
orderBy?: string;
order?: "asc" | "desc";
search?: string;
filter?: Array<{
field: string;
operator: string;
value: unknown;
}>;
}
): Promise<Document[]> {
// Construct correct endpoint: /projects/{projectId}/collections/{collectionName}/documents
const response = await this.get<Document[] | { success: boolean; data?: { documents?: Document[]; [key: string]: unknown }; documents?: Document[] }>(
`/projects/${projectId}/collections/${collectionName}/documents`,
options
);
// Normalize response format - handle different backend response formats
// Backend can return: Document[], { success: true, data: { documents: [...] } }, or { success: true, documents: [...] }
if (Array.isArray(response)) {
return response;
}
if (response && typeof response === "object") {
// Check for wrapped format with 'documents' in data
if ("data" in response && response.data && typeof response.data === "object" && "documents" in response.data) {
const documents = (response.data as { documents?: Document[] }).documents;
if (Array.isArray(documents)) {
return documents;
}
}
// Check for wrapped format with 'documents' at root
if ("documents" in response && Array.isArray(response.documents)) {
return response.documents;
}
// Check for wrapped format with 'data' as array
if ("data" in response && Array.isArray(response.data)) {
return response.data;
}
}
return [];
}
/**
* Create a new document in a collection
*
* @param {string} projectId - Project ID
* @param {string} collectionName - Collection name (not ID)
* @param {Object} documentData - Document data
* @param {Record<string, unknown>} documentData.data - Document data object
* @param {string} [documentData.created_by] - User ID who created the document
* @returns {Promise<Document>} Created document
*
* @example
* const document = await client.createDocument('project-id', 'collection-name', {
* data: { title: 'Document Title', value: 42 },
* created_by: 'user-id'
* });
*/
async createDocument(
projectId: string,
collectionName: string,
documentData: {
data: Record<string, unknown>;
created_by?: string;
}
): Promise<Document> {
// Construct correct endpoint: /projects/{projectId}/collections/{collectionName}/documents
const response = await this.post<Document | { success: boolean; data?: Document; [key: string]: unknown }>(
`/projects/${projectId}/collections/${collectionName}/documents`,
documentData
);
// Normalize response format - handle different backend response formats
// Backend can return: Document directly, { success: true, data: {...} }, or { success: true, ... }
if (response && typeof response === "object") {
// Check if response itself is a Document (has id, collection_id, project_id, data)
if ("id" in response && "data" in response && ("collection_id" in response || "project_id" in response)) {
return response as unknown as Document;
}
// Check for wrapped format with 'data' key
if ("data" in response && response.data) {
const doc = response.data as unknown as Document;
if (doc && typeof doc === "object" && "id" in doc) {
return doc;
}
}
}
return (response as unknown as Document) || ({} as Document);
}
// Collection Management
async createCollection(
projectId: string,
collectionData: {
name: string;
description?: string;
fields: Array<{
name: string;
type: string;
required?: boolean;
unique?: boolean;
indexed?: boolean;
default?: unknown;
validation?: Record<string, unknown>;
}>;
indexes?: Array<{
name: string;
fields: string[];
unique?: boolean;
}>;
}
): Promise<ApiResponse<Collection>> {
const response = await this.post<Collection | { success: boolean; data?: Collection; collection?: Collection }>(
`/projects/${projectId}/collections`,
collectionData
);
// Normalize response format - handle different backend response formats
// Backend can return: direct object, { success: true, data: {...} }, or { success: true, collection: {...} }
let collection: Collection | undefined;
if (response && typeof response === "object") {
// Check if response itself is a Collection (has id and name)
if ("id" in response && "name" in response) {
collection = response as unknown as Collection;
}
// Check for wrapped format with 'collection' key
else if ("collection" in response && response.collection) {
collection = response.collection as unknown as Collection;
}
// Check for wrapped format with 'data' key (covers both { data: {...} } and { success: true, data: {...} })
else if ("data" in response && response.data) {
collection = response.data as unknown as Collection;
}
}
// Return normalized response
if (collection) {
return {
success: true,
data: collection,
} as ApiResponse<Collection>;
}
// If we can't extract collection, return the response as-is
return response as ApiResponse<Collection>;
}
async getCollection(
projectId: string,
collectionName: string
): Promise<ApiResponse<Collection>> {
return this.get<Collection>(
`/projects/${projectId}/collections/${collectionName}`
);
}
async updateCollection(
projectId: string,
collectionName: string,
updates: {
description?: string;
fields?: Array<{
name: string;
type: string;
required?: boolean;
unique?: boolean;
indexed?: boolean;
default?: unknown;
validation?: Record<string, unknown>;
}>;
indexes?: Array<{
name: string;
fields: string[];
unique?: boolean;
}>;
}
): Promise<ApiResponse<Collection>> {
return this.put<Collection>(
`/projects/${projectId}/collections/${collectionName}`,
updates
);
}
async deleteCollection(
projectId: string,
collectionName: string
): Promise<ApiResponse<{ success: boolean }>> {
return this.delete<{ success: boolean }>(
`/projects/${projectId}/collections/${collectionName}`
);
}
async getProjectCollections(
projectId: string,
options?: QueryOptions
): Promise<{ success: boolean; collections: Collection[] } | ApiResponse<Collection[]>> {
// Backend returns { success: true, collections: [...] }, not PaginatedResponse
return this.get<Collection[]>(
`/projects/${projectId}/collections`,
options
) as Promise<{ success: boolean; collections: Collection[] } | ApiResponse<Collection[]>>;
}
// Document Management
async getDocument(
projectId: string,
collectionName: string,
documentId: string
): Promise<ApiResponse<Document>> {
return this.get<Document>(
`/projects/${projectId}/collections/${collectionName}/documents/${documentId}`
);
}
async updateDocument(
projectId: string,
collectionName: string,
documentId: string,
updateData: UpdateDocumentRequest
): Promise<ApiResponse<Document>> {
return this.put<Document>(
`/projects/${projectId}/collections/${collectionName}/documents/${documentId}`,
updateData
);
}
async deleteDocument(
projectId: string,
collectionName: string,
documentId: string,
options?: { deleted_by?: string }
): Promise<ApiResponse<{ success: boolean }>> {
return this.delete<{ success: boolean }>(
`/projects/${projectId}/collections/${collectionName}/documents/${documentId}${
options?.deleted_by ? `?deleted_by=${options.deleted_by}` : ""
}`
);
}
// Collection Schema Operations
async validateCollectionSchema(
projectId: string,
collectionName: string
): Promise<
ApiResponse<{
valid: boolean;
issues: Array<{
type: string;
field?: string;
message: string;
severity: "error" | "warning" | "info";
}>;
}>
> {
return this.post(
`/projects/${projectId}/collections/${collectionName}/validate-schema`
);
}
async getCollectionStatistics(
projectId: string,
collectionName: string
): Promise<
ApiResponse<{
total_documents: number;
total_size_bytes: number;
average_document_size: number;
field_statistics: Record<
string,
{
null_count: number;
unique_values: number;
most_common_values?: Array<{ value: unknown; count: number }>;
}
>;
index_usage: Record<
string,
{
size_bytes: number;
scans: number;
last_used?: string;
}
>;
}>
> {
return this.get(
`/projects/${projectId}/collections/${collectionName}/statistics`
);
}
// Advanced Document Operations
async bulkCreateDocuments(
projectId: string,
collectionName: string,
documents: CreateDocumentRequest[]
): Promise<
ApiResponse<{
created: Document[];
errors: Array<{
index: number;
error: string;
}>;
}>
> {
return this.post(
`/projects/${projectId}/collections/${collectionName}/documents/bulk`,
{ documents }
);
}
async bulkUpdateDocuments(
projectId: string,
collectionName: string,
updates: Array<{
id: string;
data: Record<string, unknown>;
}>
): Promise<
ApiResponse<{
updated: Document[];
errors: Array<{
id: string;
error: string;
}>;
}>
> {
return this.put(
`/projects/${projectId}/collections/${collectionName}/documents/bulk`,
{ updates }
);
}
async bulkDeleteDocuments(
projectId: string,
collectionName: string,
documentIds: string[],
options?: { deleted_by?: string }
): Promise<
ApiResponse<{
deleted_count: number;
errors: Array<{
id: string;
error: string;
}>;
}>
> {
return this.post(
`/projects/${projectId}/collections/${collectionName}/documents/bulk-delete`,
{
document_ids: documentIds,
...(options?.deleted_by && { deleted_by: options.deleted_by }),
}
);
}
// Document Search and Filtering
async searchDocuments(
projectId: string,
collectionName: string,
query: {
text?: string;
fields?: string[];
filters?: DocumentFilter;
sort?: Array<{
field: string;
direction: "asc" | "desc";
}>;
limit?: number;
offset?: number;
}
): Promise<PaginatedResponse<Document & { _score?: number }>> {
return this.post(
`/projects/${projectId}/collections/${collectionName}/search`,
query
);
}
async aggregateDocuments(
projectId: string,
collectionName: string,
aggregation: {
group_by?: string[];
aggregations: Record<
string,
{
type: "count" | "sum" | "avg" | "min" | "max";
field?: string;
}
>;
filters?: DocumentFilter;
}
): Promise<
ApiResponse<{
groups: Record<string, Record<string, number>>;
total_groups: number;
}>
> {
return this.post(
`/projects/${projectId}/collections/${collectionName}/aggregate`,
aggregation
);
}
async countDocuments(
projectId: string,
collectionName: string,
filter?: DocumentFilter
): Promise<ApiResponse<{ count: number }>> {
return this.post(
`/projects/${projectId}/collections/${collectionName}/count`,
{ filter }
);
}
}