UNPKG

magically-sdk

Version:

Official SDK for Magically - Build mobile apps with AI

411 lines (366 loc) 11.9 kB
import { DataQueryOptions, DataInsertOptions, SDKConfig, StandardFields } from './types'; import { MagicallyAuth } from './MagicallyAuth'; import { APIClient } from './APIClient'; import { getAuthToken } from './utils'; export class MagicallyData { private config: SDKConfig; private auth: MagicallyAuth; private apiClient: APIClient; constructor(config: SDKConfig, auth: MagicallyAuth) { this.config = config; this.auth = auth; this.apiClient = new APIClient(config, 'MagicallyData'); } /** * Check if a query is public (doesn't require authentication) */ private isPublicQuery(filter?: Record<string, any>): boolean { return filter?.isPublic === true; } /** * Check if an aggregate pipeline is public (doesn't require authentication) */ private isPublicAggregate(pipeline: any[]): boolean { return pipeline?.[0]?.$match?.isPublic === true; } /** * Check if a raw operation is public (read-only with isPublic filter) */ private isPublicRawOperation(operation: string, query?: Record<string, any>): boolean { const readOperations = ['find', 'findOne', 'count', 'countDocuments', 'distinct']; return readOperations.includes(operation) && query?.isPublic === true; } /** * Query data from a collection with type safety * @param collection - Collection name * @param filter - MongoDB filter object (optional) * @param options - Query options (sort, limit, skip) */ async query<T extends StandardFields>( collection: string, filter?: Record<string, any>, options?: { sort?: Record<string, number>; limit?: number; skip?: number; populate?: string[]; } ): Promise<{ data: T[]; total: number }> { try { const token = await getAuthToken(this.apiClient, this.auth); const result = await this.apiClient.request<{ data: T[]; total: number }>( `/api/project/${this.config.projectId}/data/query`, { method: 'POST', body: { collection, filter: filter || {}, sort: options?.sort || {}, limit: options?.limit || 100, skip: options?.skip || 0, populate: options?.populate || [], }, operation: `query:${collection}` }, token ); return { data: result.data || [], total: result.total || 0 }; } catch (error) { throw error; } } /** * Insert data into a collection with type safety */ async insert<T extends StandardFields>( collection: string, data: Omit<T, keyof StandardFields>, options: { upsert?: boolean } = {} ): Promise<T> { try { const token = await getAuthToken(this.apiClient, this.auth); const result = await this.apiClient.request<{ data: T }>( `/api/project/${this.config.projectId}/data/insert`, { method: 'POST', body: { collection, data, upsert: options.upsert || false, }, operation: `insert:${collection}` }, token ); return result.data; } catch (error) { throw error; } } /** * Update existing data in a collection (fails if not found) */ async update<T extends StandardFields>( collection: string, filter: Record<string, any>, update: any ): Promise<T> { try { const token = await getAuthToken(this.apiClient, this.auth); // Check if it's already a MongoDB update object const isMongoUpdate = update.$set || update.$inc || update.$push || update.$pull || update.$unset || update.$addToSet || update.$rename || update.$min || update.$max || update.$mul || update.$currentDate; // If plain data, wrap in $set. Otherwise use as-is const updateObj = isMongoUpdate ? update : { $set: update }; const result = await this.apiClient.request<{ result: { value: T } }>( `/api/project/${this.config.projectId}/data/update`, { method: 'POST', body: { collection, filter, update: updateObj }, operation: `update:${collection}` }, token ); if (!result.result?.value) { throw new Error('Document not found for update'); } return result.result.value; } catch (error) { throw error; } } /** * Upsert data in a collection (update if exists, insert if not) * @param collection - Collection name * @param filter - Filter to find existing document * @param data - Data to insert or update * @returns The upserted document and whether it was inserted */ async upsert<T extends StandardFields>( collection: string, filter: Record<string, any>, data: Omit<T, keyof StandardFields> ): Promise<{ data: T; upserted: boolean }> { try { const token = await getAuthToken(this.apiClient, this.auth); const result = await this.apiClient.request<{ data: T; upserted: boolean }>( `/api/project/${this.config.projectId}/data/upsert`, { method: 'POST', body: { collection, filter, data }, operation: `upsert:${collection}` }, token ); return { data: result.data, upserted: result.upserted || false }; } catch (error) { throw error; } } /** * Update multiple documents in a collection * @param collection - Collection name * @param filter - MongoDB filter to find documents to update * @param update - Update data or MongoDB update object * @returns Number of matched and modified documents */ async updateMany<T extends StandardFields>( collection: string, filter: Record<string, any>, update: any ): Promise<{ matchedCount: number; modifiedCount: number }> { try { const token = await getAuthToken(this.apiClient, this.auth); // Check if it's already a MongoDB update object const isMongoUpdate = update.$set || update.$inc || update.$push || update.$pull || update.$unset || update.$addToSet || update.$rename || update.$min || update.$max || update.$mul || update.$currentDate; // If plain data, wrap in $set. Otherwise use as-is const updateObj = isMongoUpdate ? update : { $set: update }; const result = await this.apiClient.request<{ matchedCount: number; modifiedCount: number }>( `/api/project/${this.config.projectId}/data/update`, { method: 'POST', body: { collection, filter, update: updateObj, options: { multi: true } }, operation: `updateMany:${collection}` }, token ); return { matchedCount: result.matchedCount || 0, modifiedCount: result.modifiedCount || 0 }; } catch (error) { throw error; } } /** * Count documents in a collection * @param collection - Collection name * @param filter - Optional MongoDB filter * @returns Count of matching documents */ async count( collection: string, filter?: Record<string, any> ): Promise<{ count: number }> { try { const token = await getAuthToken(this.apiClient, this.auth); const result = await this.apiClient.request<{ result: number }>( `/api/project/${this.config.projectId}/data/raw`, { method: 'POST', body: { collection, operation: 'countDocuments', query: filter || {} }, operation: `count:${collection}` }, token ); return { count: result.result || 0 }; } catch (error) { throw error; } } /** * Delete data from a collection (deletes multiple documents) */ async delete(collection: string, filter: Record<string, any>): Promise<{ deletedCount: number }> { try { const token = await getAuthToken(this.apiClient, this.auth); const result = await this.apiClient.request<{ result: { deletedCount: number } }>( `/api/project/${this.config.projectId}/data/delete`, { method: 'POST', body: { collection, filter, options: { deleteMany: true } }, operation: `delete:${collection}` }, token ); return { deletedCount: result.result.deletedCount || 0 }; } catch (error) { throw error; } } /** * Delete a single document from a collection * @param collection - Collection name * @param filter - MongoDB filter to find the document to delete * @returns Number of deleted documents (0 or 1) */ async deleteOne(collection: string, filter: Record<string, any>): Promise<{ deletedCount: number }> { try { const token = await getAuthToken(this.apiClient, this.auth); const result = await this.apiClient.request<{ result: { deletedCount: number } }>( `/api/project/${this.config.projectId}/data/delete`, { method: 'POST', body: { collection, filter, options: { deleteMany: false } // deleteOne }, operation: `deleteOne:${collection}` }, token ); return { deletedCount: result.result.deletedCount || 0 }; } catch (error) { throw error; } } /** * Delete multiple documents from a collection (alias for delete) * @param collection - Collection name * @param filter - MongoDB filter to find documents to delete * @returns Number of deleted documents */ async deleteMany(collection: string, filter: Record<string, any>): Promise<{ deletedCount: number }> { return this.delete(collection, filter); } /** * Run aggregation query with type safety */ async aggregate<T = any>(collection: string, pipeline: any[]): Promise<T[]> { try { const token = await getAuthToken(this.apiClient, this.auth); const result = await this.apiClient.request<{ data: T[] }>( `/api/project/${this.config.projectId}/data/aggregate`, { method: 'POST', body: { collection, pipeline, allowDiskUse: false }, operation: `aggregate:${collection}` }, token ); return result.data; } catch (error) { throw error; } } /** * Run raw MongoDB operations with type safety and validation */ async raw<T = any>(collection: string, operation: string, options: Record<string, any> = {}): Promise<T> { // Validate allowed operations for security const allowedOperations = [ 'find', 'findOne', 'count', 'countDocuments', 'distinct', 'aggregate', 'findOneAndUpdate', 'updateOne', 'updateMany', 'deleteOne', 'deleteMany', 'insertOne', 'insertMany' ]; if (!allowedOperations.includes(operation)) { throw new Error(`Operation '${operation}' is not allowed. Allowed operations: ${allowedOperations.join(', ')}`); } try { const token = await getAuthToken(this.apiClient, this.auth); const result = await this.apiClient.request<{ result: T }>( `/api/project/${this.config.projectId}/data/raw`, { method: 'POST', body: { collection, operation, ...options }, operation: `raw:${operation}:${collection}` }, token ); return result.result; } catch (error) { throw error; } } }