UNPKG

@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
/** * 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 } ); } }