unbody-io / ts-client

Typescript client for Unbody's API
https://unbody.io/docs/libraries/typescript-client
6 stars 1 forks source link

[Proposal] Push API Typescript Client #8

Open akib001 opened 1 week ago

akib001 commented 1 week ago

Proposal: Implement Unbody Push API

Brief Description

Develop a Push API for Unbody to enable seamless record and file management operations in custom collections.

Description and Goal

Create a developer-friendly API that allows developers to easily perform CRUD operations on records and files within the Unbody ecosystem. The goal is to simplify record management and file handling in custom collection, abstract complex API details from developers, and provide an intuitive api for common operations.

Use Cases

  1. Seamlessly Perform CRUD operations (Create, Read, Update, Delete) on records within a collection.
  2. Create custom collection.
  3. Upload, delete, and manage various file types (Image, Audio, Video, TextDocument)
  4. Handle cross references in custom collections.

Implementation Steps

  1. Create a base class to handle REST API operations with Axios interceptors, ensuring consistent communication with the Unbody backend apis.
  2. Design the API interface and class structure, creating a PushApi instance that manages interactions with collections and records using APIKey, projectId, and sourceId.
  3. Implement record CRUD functionality.
  4. Develop a record save method utilizing PATCH requests.
  5. Develop file-handling capabilities supporting various input types (fileBuffer, stream, formData) and handle file validations.
  6. Implement updating records with file references, enabling multiple file references to be updated simultaneously.
  7. Process and structure API responses in a developer-friendly format.
  8. Implement robust error handling and validation.
  9. Conduct thorough testing across various use cases.
  10. Create comprehensive documentation and usage examples.

API Interface Design

1. Create a PushApi Instance

const pushApiInstance = new PushApi({
  apiKey: "your-api-key",
  projectId: "your-project-id",
  sourceId: "your-source-id",
});

Type Definition for PushApi with Generics

interface PushApiInstance {
  collection: <T extends Record>(name: string) => CollectionInstance<T>;
  records: {
    delete: (collectionName: string, recordId: string) => Promise<void>;
    patch: <T extends Record>(
      collectionName: string,
      recordId: string,
      patchObject: Partial<T>
    ) => Promise<void>;
    update: <T extends Record>(
      collectionName: string,
      recordId: string,
      recordObject: T
    ) => Promise<void>;
  };
  files: {
    create: (
      fileRecordId: string,
      File: Buffer | Stream | FormData,
      metaData?: { fileName: string; mimeType: string }
    ) => Promise<File>;
    delete: (fileId: string) => Promise<void>;
  };
}

Type Definition for CollectionInstance

interface CollectionInstance<T> {
  create: (recordId: string, payload: T) => Promise<T>;
  update: (recordId: string, payload: T) => Promise<T>;
  patch: (recordId: string, patchObject: Partial<T>) => Promise<T>;
  delete: (recordId: string, payload: T) => Promise<void>;
  getRecords: () => Promise<T[]>;
  getOneRecord: (recordId: string) => Promise<T>;
}

2. Using Generics with Collections

To customize the type of the records in a collection, you can define a type for your collection.

Defining a Custom Type for ProfileCollection

import { IVideoBlock, IImageBlock, StringField } from "@unbody-io/ts-client";

// Define the structure for ProfileCollection
type ProfileCollection = {
  firstName: StringField;
  lastName: StringField;
  photos: CrossReferenceField<IImageBlock>;
  videos: CrossReferenceField<IVideoBlock>;
};

// Create a collection instance with the custom type
const profileCollectionInstance =
  await pushApiInstance.collection<ProfileCollection>("ProfileCollection");

Retrieving Records from collectionInstance

// Use the collection instance to retrieve records
const profileRecords: ProfileCollection[] =
  await profileCollectionInstance.getRecords();

3. CRUD Operations on Collection Instance

Create

// Create a new profile record
const profileCollectionInstance = await pushApi.collection("ProfileCollection");

// create new record from `profileCollectionInstance`
const profileRecordInstance = await profileCollectionInstance.create(
  "recordId", // custom recordId
  {
    firstName: "John",
    lastName: "Doe",
    birthday: "2000-09-18T18:39:38.026Z",
  }
);

Update, Patch, and Delete Record

await profileCollection.update("recordId", recordPayloadObject);

await profileCollection.patch("recordId", recordPayloadObject); // Partial update

await profileCollection.delete("recordId");

Type Definition for RecordInstance

interface RecordInstance<T extends Record> extends T {
  save: () => Promise<void>;
  delete: () => Promise<void>;
  patch: (data: Partial<T>) => Promise<void>;
  update: (data: T) => Promise<void>;
}
const profileRecords: Record[] = await profileCollectionInstance.getRecords();

// Find a specific record using JS array `find` method
const specificRecord = profileRecords.find(
  (record) => record.firstName === "Jane"
);

// Or find record by recordId from collectionInstance
const specificRecord = await profileCollectionInstance.getOneRecord("recordId");

Use record instance to delete, patch, and update the record

await specificRecord.delete(); // Delete the record
await specificRecord.patch({ firstName: "Janet" }); // Partial update
await specificRecord.update({
  firstName: "Jane",
  lastName: "Smith",
  ...otherFields,
}); // Full update

Fetch Records with Method Chaining

const records: Partial<ProfileCollection[]> = await pushApiInstance
  .collection<ProfileCollection>("ProfileCollection")
  .getRecords("ProfileCollection")
  .limit(10)
  .offset(1)
  .sort("firstName", "asc")
  .exec();

4. Record Operations Without a Reference Variable or instance

// Delete a record by explicitly specifying collection name and record ID
await pushApiInstance.records.delete("ProfileCollection", "customProfileId");

// Patch (partial update) a record by collection name and record ID
await pushApiInstance.records.patch<ProfileCollection>(
  "ProfileCollection", // CollectionName
  "customProfileRecordId",
  {
    firstName: "Janet",
  }
);

// Update (full object replacement) a record by collection name and record ID
await pushApiInstance.records.update<ProfileCollection>(
  "ProfileCollection",
  "customProfileRecordId",
  {
    firstName: "Jane",
    lastName: "Smith",
  }
);

5. File Operations

Type Definition for FileInstance

interface FileInstance {
  create: (
    fileRecordId: string,
    File: Buffer | Stream | FormData,
    metaData?: { fileName: string; mimeType: string }
  ) => Promise<File>;
  delete: (fileId: string) => Promise<void>;
}

Uploading and Deleting Files

const uploadedFile = await pushApiInstance.files.create(
  "customFileRecordId",
  "Buffer | Stream | FormData",
  {
    fileName: "example.txt",
    mimeType: "text/plain",
  }
);

// Delete from uploaded file instance
await uploadedFile.delete();

// Delete a file using its fileId (same as recordId)
await pushApiInstance.files.delete("exampleFileRecordId");

// Alternatively, delete a file from a record. Since files are treated as records:
await pushApiInstance.records.delete("ImageBlock", "exampleFileRecordId");

// Fetch all files
await pushApiInstance.files.getAll();

File Record Operations without Reference Variables

// Fetch all files and delete the first one
const files: File[] = await pushApiInstance.files.getAll(); // method chaining (limit, offset, sorting) can be used here also
await files[0].delete(); // Delete the first file

6. Cross-Reference Operations

Type Definition for CrossReferenceField

interface CrossReferenceField<T> {
  add(item: { id: string; collection: string } | File | File[]): Promise<void>;
  set(item: { id: string; collection: string } | File | File[]): Promise<void>;
  remove(id: string): Promise<void>;
}

Add and Remove Cross-References from profileRecordInstance

// Add files in `photos` cross reference field
await profileRecordInstance.photos.add(uploadedFile);

// Removing a cross-reference
await profileRecordInstance.photos.remove("recordId");

// Add files in bulk `photos` cross reference field
await profileRecordInstance.videos.add([video1, video2, video3]);

// Setting cross-reference
await profileRecordInstance.videos.set([video1, video2, video3]);
await profileRecordInstance.photos.set(uploadedFile);

// Access `photos` cross reference field from profileRecordInstance
const allPhotos = profileRecordInstance.photos;

// Add by crossReferenceRecordId and collectionName
await profileCollectionInstance.photos.add({
  id: "imageRecordId",
  collection: "ImageBlock",
});

Add cross reference using recordId and collectionName

profileCollectionInstance.photos = [
  // `photos` cross reference field
  { id: "imageRecordId", collection: "ImageBlock" },
];

// save
await profileCollectionInstance.save();

Patch or update a record with cross-reference fields using pushApiInstance

await pushApiInstance.records.patch<ProfileCollection>(
  "ProfileCollection",
  "recordId",
  {
    photos: [
      // `photos` cross reference field
      {
        id: "imageRecordId",
        collection: "ImageBlock",
      },
    ],
  }
);

Alternatively, directly patch the record by placing the uploaded file instance

await pushApiInstance.records.patch("customProfileId", "ProfileCollection", {
  profilePhoto: [uploadedFile],
});

Full update with cross-reference fields

await pushApiInstance.records.update("customProfileId", "ProfileCollection", {
  firstName: "Jane",
  lastName: "Doe",
  profilePhoto: [uploadedFile],
});