magically-sdk
Version:
Official SDK for Magically - Build mobile apps with AI
411 lines (366 loc) • 11.9 kB
text/typescript
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;
}
}
}