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