UNPKG

@triplit/client

Version:
259 lines (235 loc) 7.61 kB
import { CollectionNameFromModels, createUpdateProxyAndTrackChanges, deserializeEntity, deserializeFetchResult, EntityNotFoundError, FetchResult, Models, queryBuilder, ReadModel, SchemaQuery, serializeEntity, TriplitError, Type, UpdatePayload, WriteModel, } from '@triplit/db'; function parseError(error: string) { try { const jsonError = JSON.parse(error); return TriplitError.fromJson(jsonError); } catch (e) { return new TriplitError(`Failed to parse remote error response: ${error}`); } } export type HttpClientOptions<M extends Models<M> = Models> = { serverUrl?: string; token?: string; schema?: M; schemaFactory?: () => M | Promise<M>; }; // Interact with remote via http api, totally separate from your local database export class HttpClient<M extends Models<M> = Models> { constructor(private options: HttpClientOptions<M> = {}) {} private _schemaInitialized: boolean = false; // Hack: use schemaFactory to get schema if it's not ready from provider private async schema(): Promise<M | undefined> { if (!this._schemaInitialized) { if (!this.options.schema) { this.options.schema = await (this.options.schemaFactory ? this.options.schemaFactory() : undefined); } this._schemaInitialized = true; } return this.options.schema; } updateOptions(options: HttpClientOptions<M>) { this.options = { ...this.options, ...options }; } private async sendRequest( uri: string, method: string, body: any, options: { isFile?: boolean } = { isFile: false } ) { const serverUrl = this.options.serverUrl; if (!serverUrl) throw new TriplitError('No server url provided'); if (!this.options.token) throw new TriplitError('No token provided'); const headers: HeadersInit = { Authorization: 'Bearer ' + this.options.token, 'Content-Type': 'application/json', }; const stringifiedBody = JSON.stringify(body); let form; if (options.isFile) { form = new FormData(); form.append('data', stringifiedBody); delete headers['Content-Type']; } const res = await fetch(serverUrl + uri, { method, headers, body: options.isFile ? form : stringifiedBody, }); if (!res.ok) return { data: undefined, error: parseError(await res.text()) }; return { data: await res.json(), error: undefined }; } async fetch<Q extends SchemaQuery<M>>( query: Q ): Promise<FetchResult<M, Q, 'many'>> { const { data, error } = await this.sendRequest('/fetch', 'POST', { query, }); if (error) throw error; return deserializeFetchResult( query, await this.schema(), data ) as FetchResult<M, Q, 'many'>; } async fetchOne<Q extends SchemaQuery<M>>( query: Q ): Promise<FetchResult<M, Q, 'one'>> { query = { ...query, limit: 1 }; const { data, error } = await this.sendRequest('/fetch', 'POST', { query, }); if (error) throw error; const deserialized = deserializeFetchResult( query, await this.schema(), data ); const entity = deserialized[0]; if (!entity) return null; return entity as NonNullable<FetchResult<M, Q, 'one'>>; } async fetchById<CN extends CollectionNameFromModels<M>>( collectionName: CN, id: string ): Promise<FetchResult<M, { collectionName: CN }, 'one'>> { const query = this.query(collectionName).Id(id); return this.fetchOne<{ collectionName: CN }>(query); } async insert<CN extends CollectionNameFromModels<M>>( collectionName: CN, object: WriteModel<M, CN> ): Promise<ReadModel<M, CN>> { // we need to convert Sets to arrays before sending to the server const schema = await this.schema(); const collectionSchema = schema?.[collectionName].schema; // TODO: we should just be able to use the internal changeset here, which is // already JSON compliant const jsonEntity = collectionSchema ? Type.serialize(collectionSchema, object, 'decoded') : object; const { data, error } = await this.sendRequest('/insert', 'POST', { collectionName, entity: jsonEntity, }); if (error) throw error; return deserializeEntity(collectionSchema, data); } async bulkInsert(bulk: BulkInsert<M>): Promise<BulkInsertResult<M>> { const schema = await this.schema(); let payload = bulk; if (schema) { const schemaPayload: BulkInsert<M> = {}; for (const key in bulk) { const collectionName = key as CollectionNameFromModels<M>; const data = bulk[collectionName]; const collectionSchema = schema?.[collectionName].schema; if (!data) continue; schemaPayload[collectionName] = data.map((entity: any) => serializeEntity(collectionSchema, entity) ); } payload = schemaPayload; } const { data, error } = await this.sendRequest( '/bulk-insert-file', 'POST', payload, { isFile: true } ); if (error) throw error; const result: BulkInsertResult<M> = {}; for (const key in data) { const collectionName = key as CollectionNameFromModels<M>; const collectionSchema = schema?.[collectionName].schema; result[collectionName] = data[key].map((entity: any) => deserializeEntity(collectionSchema, entity) ); } return result; } async update<CN extends CollectionNameFromModels<M>>( collectionName: CN, id: string, update: UpdatePayload<M, CN> ) { let changes = undefined; const schema = await this.schema(); const collectionSchema = schema?.[collectionName]?.schema; if (typeof update === 'function') { const existingEntity = await this.fetchById(collectionName, id); if (!existingEntity) { throw new EntityNotFoundError(id, collectionName); } changes = {}; // one of the key assumptions we're making here is that the update proxy // will take car of the conversion of Sets and Dates. This is mostly // to account for capturing changes to Sets because we need something // that can track deletes and sets to a Set, which a Set itself cannot do await update( createUpdateProxyAndTrackChanges( existingEntity, changes, collectionSchema ) ); } else { changes = update; } changes = collectionSchema ? Type.encode(collectionSchema, changes) : changes; const { data, error } = await this.sendRequest('/update', 'POST', { collectionName, entityId: id, changes, }); if (error) throw error; return data; } async delete<CN extends CollectionNameFromModels<M>>( collectionName: CN, entityId: string ) { const { data, error } = await this.sendRequest('/delete', 'POST', { collectionName, entityId, }); if (error) throw error; return data; } async deleteAll<CN extends CollectionNameFromModels<M>>(collectionName: CN) { const { data, error } = await this.sendRequest('/delete-all', 'POST', { collectionName, }); if (error) throw error; return data; } query<CN extends CollectionNameFromModels<M>>(collectionName: CN) { return queryBuilder<M, CN>(collectionName); } } export type BulkInsert<M extends Models<M> = Models> = { [CN in CollectionNameFromModels<M>]?: WriteModel<M, CN>[]; }; export type BulkInsertResult<M extends Models<M> = Models> = { [CN in CollectionNameFromModels<M>]?: ReadModel<M, CN>[]; };