Files
genesis-2/notebooks/carbone.ipynb
T
alexrg 357c244f2a
Supa-backup / run_db_backup (push) Successful in 38s
feat: add carbone-sdk dependency and create initial CarboneClient implementation
- Added carbone-sdk@^1.6.0 to package.json and deno.lock
- Created a new notebook (carbone.ipynb) with CarboneClient class for interacting with the Carbone API
- Implemented methods for listing templates, categories, tags, uploading templates, and rendering documents
2026-03-13 11:09:22 -06:00

17 KiB

import { load } from "jsr:@std/dotenv";

const env = await load({
    export: true,
});
type ListTemplatesParams = {
  id?: string;
  versionId?: string;
  category?: string;
  origin?: 0 | 1;
  includeVersions?: boolean;
  search?: string;
  limit?: number;
  cursor?: string;
};

class CarboneClient {
  listTemplates(params?: ListTemplatesParams) {}
  listCategories() {}
  listTags() {}
  uploadTemplate(input: {
    file?: Buffer;
    filename?: string;
    template?: string; // base64
    versioning?: boolean;
    id?: string;
    name?: string;
    comment?: string;
    tags?: string[];
    category?: string;
    sample?: any[];
    deployedAt?: number;
    expireAt?: number;
  }) {}
  patchTemplate(id: string, body: {
    name?: string;
    comment?: string;
    tags?: string[];
    category?: string;
    deployedAt?: number;
    expireAt?: number;
  }) {}
  deleteTemplate(id: string) {}
  downloadTemplate(id: string) {}
  render(templateId: string, body: any, opts?: { download?: boolean }) {}
  renderFromTemplate(body: any, opts?: { download?: boolean }) {}
  downloadRender(renderId: string) {}
}
const CARBONE_VERSION = "5";

type CarboneSuccess<T> = {
  success: true;
  data: T;
  hasMore?: boolean;
  nextCursor?: string;
};

type CarboneError = {
  success: false;
  error: string;
};

type CarboneResponse<T> = CarboneSuccess<T> | CarboneError;

export class CarboneClient {
  constructor(
    private baseUrl: string,
    private apiToken: string
  ) {}

  private buildHeaders(extra?: Record<string, string>) {
    return {
      Authorization: `Bearer ${this.apiToken}`,
      "carbone-version": CARBONE_VERSION,
      ...extra,
    };
  }

  private async parseResponse<T>(res: Response): Promise<T> {
    const contentType = res.headers.get("content-type") || "";

    if (!res.ok) {
      let message = `Carbone request failed: ${res.status}`;
      try {
        const err = await res.json();
        message = err?.error || err?.message || message;
      } catch {}
      throw new Error(message);
    }

    if (contentType.includes("application/json")) {
      return res.json() as Promise<T>;
    }

    return res as unknown as T;
  }

  private toQuery(params?: Record<string, unknown>) {
    const qs = new URLSearchParams();
    if (!params) return "";
    for (const [key, value] of Object.entries(params)) {
      if (value === undefined || value === null) continue;
      qs.set(key, String(value));
    }
    const s = qs.toString();
    return s ? `?${s}` : "";
  }

  async listTemplates(params?: {
    id?: string;
    versionId?: string;
    category?: string;
    origin?: 0 | 1;
    includeVersions?: boolean;
    search?: string;
    limit?: number;
    cursor?: string;
  }) {
    const res = await fetch(
      `${this.baseUrl}/templates${this.toQuery(params)}`,
      {
        method: "GET",
        headers: this.buildHeaders(),
      }
    );
    return this.parseResponse<
      CarboneResponse<
        Array<{
          id: string;
          versionId: string;
          deployedAt: number;
          createdAt: number;
          expireAt?: number;
          size: number;
          type: string;
          name?: string;
          category?: string;
          comment?: string;
          tags?: string[];
          origin?: number;
        }>
      >
    >(res);
  }

  async listCategories() {
    const res = await fetch(`${this.baseUrl}/templates/categories`, {
      method: "GET",
      headers: this.buildHeaders(),
    });
    return this.parseResponse<CarboneResponse<Array<{ name: string }>>>(res);
  }

  async listTags() {
    const res = await fetch(`${this.baseUrl}/templates/tags`, {
      method: "GET",
      headers: this.buildHeaders(),
    });
    return this.parseResponse<CarboneResponse<Array<{ name: string }>>>(res);
  }

  async uploadTemplateJson(body: {
    template: string;
    versioning?: boolean;
    id?: string;
    name?: string;
    comment?: string;
    tags?: string[];
    category?: string;
    sample?: any[];
    deployedAt?: number;
    expireAt?: number;
  }) {
    const res = await fetch(`${this.baseUrl}/template`, {
      method: "POST",
      headers: this.buildHeaders({
        "Content-Type": "application/json",
      }),
      body: JSON.stringify(body),
    });
    return this.parseResponse<
      CarboneResponse<{
        id?: string;
        versionId?: string;
        templateId?: string;
        type?: string;
        size?: number;
        createdAt?: number;
      }>
    >(res);
  }

  async uploadTemplateFile(input: {
    file: Blob;
    filename: string;
    versioning?: boolean;
    id?: string;
    name?: string;
    comment?: string;
    tags?: string[];
    category?: string;
    sample?: any[];
    deployedAt?: number;
    expireAt?: number;
  }) {
    const form = new FormData();

    if (input.versioning !== undefined) {
      form.append("versioning", String(input.versioning));
    }
    if (input.id) form.append("id", input.id);
    if (input.name) form.append("name", input.name);
    if (input.comment) form.append("comment", input.comment);
    if (input.category) form.append("category", input.category);
    if (input.tags) {
      for (const tag of input.tags) form.append("tags[]", tag);
    }
    if (input.sample) form.append("sample", JSON.stringify(input.sample));
    if (input.deployedAt !== undefined) {
      form.append("deployedAt", String(input.deployedAt));
    }
    if (input.expireAt !== undefined) {
      form.append("expireAt", String(input.expireAt));
    }

    // template should be appended last
    form.append("template", input.file, input.filename);

    const res = await fetch(`${this.baseUrl}/template`, {
      method: "POST",
      headers: this.buildHeaders(),
      body: form,
    });
    return this.parseResponse<
      CarboneResponse<{
        id?: string;
        versionId?: string;
        templateId?: string;
        type?: string;
        size?: number;
        createdAt?: number;
      }>
    >(res);
  }

  async patchTemplate(
    id: string,
    body: {
      name?: string;
      comment?: string;
      tags?: string[];
      category?: string;
      deployedAt?: number;
      expireAt?: number;
    }
  ) {
    const res = await fetch(`${this.baseUrl}/template/${id}`, {
      method: "PATCH",
      headers: this.buildHeaders({
        "Content-Type": "application/json",
      }),
      body: JSON.stringify(body),
    });
    return this.parseResponse<CarboneResponse<true>>(res);
  }

  async deleteTemplate(id: string) {
    const res = await fetch(`${this.baseUrl}/template/${id}`, {
      method: "DELETE",
      headers: this.buildHeaders(),
    });
    return this.parseResponse<CarboneResponse<true>>(res);
  }

  async downloadTemplate(id: string) {
    const res = await fetch(`${this.baseUrl}/template/${id}`, {
      method: "GET",
      headers: this.buildHeaders(),
    });

    if (!res.ok) {
      const err = await res.json().catch(() => null);
      throw new Error(err?.error || `Failed to download template: ${res.status}`);
    }

    return {
      buffer: Buffer.from(await res.arrayBuffer()),
      contentType: res.headers.get("content-type"),
      contentDisposition: res.headers.get("content-disposition"),
    };
  }

  async render(
    templateIdOrVersionId: string,
    body: Record<string, unknown>,
    opts?: { download?: boolean }
  ) {
    const query = opts?.download ? "?download=true" : "";
    const res = await fetch(
      `${this.baseUrl}/render/${templateIdOrVersionId}${query}`,
      {
        method: "POST",
        headers: this.buildHeaders({
          "Content-Type": "application/json",
        }),
        body: JSON.stringify(body),
      }
    );

    if (opts?.download) {
      if (!res.ok) {
        const err = await res.json().catch(() => null);
        throw new Error(err?.error || `Render failed: ${res.status}`);
      }
      return {
        buffer: Buffer.from(await res.arrayBuffer()),
        contentType: res.headers.get("content-type"),
        contentDisposition: res.headers.get("content-disposition"),
      };
    }

    return this.parseResponse<CarboneResponse<{ renderId: string }>>(res);
  }

  async renderFromTemplate(
    body: Record<string, unknown>,
    opts?: { download?: boolean }
  ) {
    const query = opts?.download ? "?download=true" : "";
    const res = await fetch(`${this.baseUrl}/render/template${query}`, {
      method: "POST",
      headers: this.buildHeaders({
        "Content-Type": "application/json",
      }),
      body: JSON.stringify(body),
    });

    if (opts?.download) {
      if (!res.ok) {
        const err = await res.json().catch(() => null);
        throw new Error(err?.error || `Render failed: ${res.status}`);
      }
      return {
        buffer: Buffer.from(await res.arrayBuffer()),
        contentType: res.headers.get("content-type"),
        contentDisposition: res.headers.get("content-disposition"),
      };
    }

    return this.parseResponse<CarboneResponse<{ renderId: string }>>(res);
  }

  async downloadRender(renderId: string) {
    const res = await fetch(`${this.baseUrl}/render/${renderId}`, {
      method: "GET",
    });

    if (!res.ok) {
      const err = await res.json().catch(() => null);
      throw new Error(err?.error || `Failed to download render: ${res.status}`);
    }

    return {
      buffer: Buffer.from(await res.arrayBuffer()),
      contentType: res.headers.get("content-type"),
      contentDisposition: res.headers.get("content-disposition"),
    };
  }
}
const carbone = new CarboneClient(
  "https://carbone.lci.ulsa.mx",
  Deno.env.get("CARBONE_API_TOKEN")!
);

const templates = await carbone.listTemplates({ limit: 20 });
console.log(templates);

const categories = await carbone.listCategories();
console.log(categories);

const tags = await carbone.listTags();
console.log(tags);
{
  success: true,
  data: [
    {
      versionId: "1990f3351b4313779ae3ef3caeaea14dea51c3a98a691fb90a347dec53f875e6",
      id: "1373945625988937236",
      deployedAt: 1773246290,
      createdAt: 1773246316,
      expireAt: 0,
      size: 29008,
      type: "docx",
      name: "Anexo 1",
      category: "Planes de estudio",
      comment: "Plantilla de anexo 1 SEP RVOE Acuerdo 17-11-17",
      tags: [],
      origin: 1
    },
    {
      versionId: "caee5a7804647382f6df62ef92cc8f04c822b6a297479c5068e22d08def685d7",
      id: "1373945108764607184",
      deployedAt: 1773246225,
      createdAt: 1773246251,
      expireAt: 0,
      size: 29642,
      type: "xlsx",
      name: "Anexo 2",
      category: "",
      comment: "",
      tags: [],
      origin: 1
    },
    {
      versionId: "b0ab3c6fe90c73151fcae64d8509e406803381f3966aa88d30d8d30e2b1ffe8f",
      id: "1373944894291796699",
      deployedAt: 1773246204,
      createdAt: 1773246230,
      expireAt: 0,
      size: 23806,
      type: "docx",
      name: "Anexo 3",
      category: "",
      comment: "",
      tags: [],
      origin: 1
    }
  ],
  hasMore: false
}
{ success: true, data: [ { name: "Planes de estudio" } ] }
{ success: true, data: [] }
const templateID = "1373945625988937236";
await carbone.patchTemplate("1373945625988937236", {
    category: "Planes de estudio",
    comment: "Plantilla de anexo 1 SEP RVOE Acuerdo 17-11-17",
});
{
  success: true,
  data: {
    category: "Planes de estudio",
    comment: "Plantilla de anexo 1 SEP RVOE Acuerdo 17-11-17",
    versionId: "1990f3351b4313779ae3ef3caeaea14dea51c3a98a691fb90a347dec53f875e6"
  }
}