UNPKG

@smartsamurai/krapi-sdk

Version:

KRAPI TypeScript SDK - Easy-to-use client SDK for connecting to self-hosted KRAPI servers (like Appwrite SDK)

445 lines (420 loc) 14.1 kB
/** * Documents Adapter * * Unifies CollectionsHttpClient and CollectionsService for document operations. */ import { CollectionsService } from "../../collections-service"; import { KrapiError } from "../../core/krapi-error"; import { CollectionsHttpClient } from "../../http-clients/collections-http-client"; import { Document } from "../../types"; import { createAdapterInitError } from "./error-handler"; type Mode = "client" | "server"; export class DocumentsAdapter { private mode: Mode; private httpClient: CollectionsHttpClient | undefined; private service: CollectionsService | undefined; constructor( mode: Mode, httpClient?: CollectionsHttpClient, service?: CollectionsService ) { this.mode = mode; this.httpClient = httpClient; this.service = service; } async create( projectId: string, collectionName: string, documentData: { data: Record<string, unknown>; created_by?: string; } ): Promise<Document> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const response = await this.httpClient.createDocument(projectId, collectionName, documentData); return response; } else { if (!this.service) { throw createAdapterInitError("Collections service", this.mode); } return this.service.createDocument(projectId, collectionName, documentData) as unknown as Document; } } async get(projectId: string, collectionName: string, documentId: string): Promise<Document> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const response = await this.httpClient.getDocument(projectId, collectionName, documentId); const document = response.data as unknown as Document; if (!document) { throw KrapiError.notFound(`Document '${documentId}' not found in collection '${collectionName}'`, { documentId, collectionName }); } return document; } else { if (!this.service) { throw createAdapterInitError("Collections service", this.mode); } const document = (await this.service.getDocumentById( projectId, collectionName, documentId )) as unknown as Document | null; if (!document) { throw KrapiError.notFound(`Document '${documentId}' not found in collection '${collectionName}'`, { documentId, collectionName }); } return document; } } async update( projectId: string, collectionName: string, documentId: string, updateData: { data: Record<string, unknown>; updated_by?: string; } ): Promise<Document> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const response = await this.httpClient.updateDocument( projectId, collectionName, documentId, updateData ); return (response.data as unknown as Document) || ({} as Document); } else { if (!this.service) { throw createAdapterInitError("Collections service", this.mode); } return this.service.updateDocument(projectId, collectionName, documentId, updateData) as unknown as Document; } } async delete( projectId: string, collectionName: string, documentId: string, deletedBy?: string ): Promise<{ success: boolean }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const response = await this.httpClient.deleteDocument( projectId, collectionName, documentId, deletedBy ? { deleted_by: deletedBy } : undefined ); return response.data || { success: false }; } else { if (!this.service) { throw createAdapterInitError("Collections service", this.mode); } const success = await this.service.deleteDocument(projectId, collectionName, documentId, deletedBy); return { success }; } } async getAll( projectId: string, collectionName: string, options?: { filter?: Record<string, unknown>; limit?: number; offset?: number; orderBy?: string; order?: "asc" | "desc"; search?: string; } ): Promise<Document[]> { if (this.mode === "client") { const httpOptions: { page?: number; limit?: number; orderBy?: string; order?: "asc" | "desc"; search?: string; filter?: Array<{ field: string; operator: string; value: unknown }>; } | undefined = options ? { ...(options.filter && { filter: Object.entries(options.filter).map(([field, value]) => ({ field, operator: "eq", value, })), }), } : undefined; if (httpOptions && options) { if (options.limit !== undefined) httpOptions.limit = options.limit; if (options.offset !== undefined) { httpOptions.page = Math.floor(options.offset / (options.limit || 10)) + 1; } if (options.orderBy !== undefined) httpOptions.orderBy = options.orderBy; if (options.order !== undefined) httpOptions.order = options.order; if (options.search !== undefined) httpOptions.search = options.search; } if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const response = await this.httpClient.getDocuments(projectId, collectionName, httpOptions); return response; } else { if (!this.service) { throw createAdapterInitError("Collections service", this.mode); } return this.service.getDocuments(projectId, collectionName, options?.filter, options) as unknown as Document[]; } } async search( projectId: string, collectionName: string, query: { text?: string; fields?: string[]; filters?: Record<string, unknown>; limit?: number; offset?: number; } ): Promise<Document[]> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const response = await this.httpClient.searchDocuments(projectId, collectionName, query); return (response.data as unknown as Document[]) || []; } else { if (!this.service) { throw createAdapterInitError("Collections service", this.mode); } // CollectionsService.searchDocuments expects searchQuery as string, options as second parameter const searchQuery = query.text || ""; const options: { limit?: number; offset?: number; sort_by?: string; sort_order?: "asc" | "desc"; select_fields?: string[]; } = {}; if (query.limit !== undefined) options.limit = query.limit; if (query.offset !== undefined) options.offset = query.offset; return this.service.searchDocuments(projectId, collectionName, searchQuery, options) as unknown as Document[]; } } async bulkCreate( projectId: string, collectionName: string, documents: Array<{ data: Record<string, unknown>; created_by?: string; }> ): Promise<{ created: Document[]; errors: Array<{ index: number; error: string }>; }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const response = await this.httpClient.bulkCreateDocuments(projectId, collectionName, documents); return response.data || { created: [], errors: [] }; } else { if (!this.service) { throw createAdapterInitError("Collections service", this.mode); } // The service returns Document[] directly, not an object with created/errors const createdDocs = await this.service.createDocuments( projectId, collectionName, documents.map((doc) => { const request: { data: Record<string, unknown>; created_by?: string } = { data: doc.data }; if (doc.created_by) request.created_by = doc.created_by; return request; }) ); return { created: createdDocs as unknown as Document[], errors: [], }; } } async bulkUpdate( projectId: string, collectionName: string, updates: Array<{ id: string; data: Record<string, unknown>; updated_by?: string; }> ): Promise<{ updated: Document[]; errors: Array<{ id: string; error: string }>; }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const response = await this.httpClient.bulkUpdateDocuments(projectId, collectionName, updates); return response.data || { updated: [], errors: [] }; } else { if (!this.service) { throw createAdapterInitError("Collections service", this.mode); } // The service returns Document[] directly const updatedDocs = await this.service.updateDocuments( projectId, collectionName, updates.map((u) => ({ id: u.id, data: u.data })) ); return { updated: updatedDocs as unknown as Document[], errors: [], }; } } async bulkDelete( projectId: string, collectionName: string, documentIds: string[], deletedBy?: string ): Promise<{ deleted_count: number; errors: Array<{ id: string; error: string }>; }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const response = await this.httpClient.bulkDeleteDocuments( projectId, collectionName, documentIds, deletedBy ? { deleted_by: deletedBy } : undefined ); return response.data || { deleted_count: 0, errors: [] }; } else { if (!this.service) { throw createAdapterInitError("Collections service", this.mode); } // The service returns boolean[] directly (true for each successful delete) const results = await this.service.deleteDocuments( projectId, collectionName, documentIds ); // Count successful deletes (true values) const deletedCount = results.filter(Boolean).length; return { deleted_count: deletedCount, errors: [], }; } } async count( projectId: string, collectionName: string, filter?: Record<string, unknown> ): Promise<{ count: number }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const response = await this.httpClient.countDocuments(projectId, collectionName, filter); return response.data || { count: 0 }; } else { if (!this.service) { throw createAdapterInitError("Collections service", this.mode); } const documents = await this.service.getDocuments(projectId, collectionName, filter); return { count: documents.length }; } } async aggregate( projectId: string, collectionName: string, aggregation: { group_by?: string[]; aggregations: Record< string, { type: "count" | "sum" | "avg" | "min" | "max"; field?: string; } >; filters?: Record<string, unknown>; } ): Promise<{ groups: Record<string, Record<string, number>>; total_groups: number; }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const response = await this.httpClient.aggregateDocuments(projectId, collectionName, aggregation); return response.data || { groups: {}, total_groups: 0 }; } else { if (!this.service) { throw createAdapterInitError("Collections service", this.mode); } // Convert aggregation format to pipeline format for the service const pipeline: Array<Record<string, unknown>> = []; // Add match stage for filters if (aggregation.filters) { pipeline.push({ $match: aggregation.filters }); } // Add group stage const groupStage: Record<string, unknown> = { _id: aggregation.group_by ? aggregation.group_by.reduce((acc, field) => { acc[field] = `$${field}`; return acc; }, {} as Record<string, string>) : null, }; // Add aggregation operations for (const [name, agg] of Object.entries(aggregation.aggregations)) { switch (agg.type) { case "count": groupStage[name] = { $sum: 1 }; break; case "sum": groupStage[name] = { $sum: `$${agg.field}` }; break; case "avg": groupStage[name] = { $avg: `$${agg.field}` }; break; case "min": groupStage[name] = { $min: `$${agg.field}` }; break; case "max": groupStage[name] = { $max: `$${agg.field}` }; break; } } pipeline.push({ $group: groupStage }); const result = await this.service.aggregateDocuments(projectId, collectionName, pipeline); // Transform result to expected format const groups: Record<string, Record<string, number>> = {}; let totalGroups = 0; if (Array.isArray(result)) { for (const row of result) { const groupKey = row._id ? JSON.stringify(row._id) : "all"; groups[groupKey] = {}; for (const [key, value] of Object.entries(row)) { if (key !== "_id" && typeof value === "number") { groups[groupKey][key] = value; } } totalGroups++; } } return { groups, total_groups: totalGroups }; } } }