> ## Documentation Index
> Fetch the complete documentation index at: https://docs.upshiftcloud.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 2. Génération des documents

> Une fonction logique, exposée comme un outil AI et une action de workflow.

Maintenant le cœur : une [fonction logique](https://docs.upshiftcloud.com/developers/extend/apps/logic/logic-functions.md)
qui charge un modèle et un enregistrement, remplit les marqueurs et enregistre un nouveau document
.

Nous allons écrire la logique commerciale une fois en tant que **gestionnaire**, puis l'exposer à travers
plusieurs déclencheurs. Ce chapitre connecte deux d’entre eux — un **outil d’IA** et une
**action de flux de travail**.

## L'assistant de rendu

Gardez une logique pure dans son propre fichier donc il est facile de le tester. Cela aplanit un record
en jetons `{{dot.path}}` et les substitue.

```ts filename="src/logic-functions/utils/render-template.ts" theme={null}
const PLACEHOLDER_PATTERN = /\{\{\s*([\w.]+)\s*\}\}/g;

export const renderTemplate = (body: string, values: Record<string, string>) => {
  const missingTokens = new Set<string>();
  const content = body.replace(PLACEHOLDER_PATTERN, (_m, token: string) => {
    const value = values[token];
    if (value === undefined || value === '') { missingTokens.add(token); return ''; }
    return value;
  });
  return { content, missingTokens: [...missingTokens] };
};
```

> [!TIP]
>
> Comme ce fichier n'a pas d'effets secondaires, vous pouvez le couvrir avec des tests unitaires rapides
> (`yarn test:unit`). Voir [Testing](https://docs.upshiftcloud.com/developers/extend/apps/operations/testing.md).

## Le gestionnaire

Le gestionnaire utilise le [`CoreApiClient`](https://docs.upshiftcloud.com/developers/extend/apps/logic/logic-functions)
généré pour lire et écrire les données. Il charge le modèle, charge l'enregistrement cible, remplit
le corps, et crée un `document`.

```ts filename="src/logic-functions/handlers/generate-document-handler.ts" theme={null}
import { CoreApiClient } from 'twenty-client-sdk/core';
import { loadRecordValues } from 'src/logic-functions/utils/load-record-values';
import { renderTemplate } from 'src/logic-functions/utils/render-template';

export const generateDocumentHandler = async (
  input: { templateId: string; recordId: string },
) => {
  const client = new CoreApiClient();

  // Use a filtered list query, not the singular lookup: the singular query
  // throws when nothing matches, which would become a 500 instead of a 404.
  const { documentTemplates } = await client.query({
    documentTemplates: {
      __args: { filter: { id: { eq: input.templateId } }, first: 1 },
      edges: { node: { id: true, name: true, body: true, target: true } },
    },
  });
  const documentTemplate = documentTemplates?.edges?.[0]?.node;
  if (!documentTemplate?.id) return { success: false, status: 404, message: 'Template not found.' };

  const record = await loadRecordValues(client, documentTemplate.target, input.recordId);
  if (!record.found) return { success: false, status: 404, message: 'Record not found.' };

  const { content, missingTokens } = renderTemplate(documentTemplate.body ?? '', record.values);

  const { createDocument } = await client.mutation({
    createDocument: {
      __args: { data: {
        name: `${documentTemplate.name} — ${record.displayName}`,
        content, status: 'GENERATED', templateId: documentTemplate.id,
      } },
      id: true, name: true,
    },
  });

  return { success: true, documentId: createDocument.id, content, missingTokens };
};
```

`loadRecordValues` exécute une requête différente pour une personne par rapport à une entreprise et aplatit
le résultat — voir
[`load-record-values.ts`](https://github.com/exceev-technology/upshift/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/load-record-values.ts).

## Exposer comme un outil et une action de workflow

Une seule `defineLogicFunction` peut contenir plusieurs déclencheurs. Ici, `toolTriggerSettings`
le rend appelable par les agents AI, et `workflowActionTriggerSettings` le transforme en une étape
dans le constructeur de workflow visuel. Les deux décrivent leur entrée avec un schéma JSON.

```ts filename="src/logic-functions/generate-document.ts" theme={null}
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema } from 'twenty-sdk/logic-function';
import { GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
import { generateDocumentInputSchema } from 'src/logic-functions/schemas/generate-document-input.schema';

export default defineLogicFunction({
  universalIdentifier: GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
  name: 'generate-document',
  description: 'Generate a document from a template and a CRM record.',
  timeoutSeconds: 30,
  toolTriggerSettings: {
    inputSchema: generateDocumentInputSchema,
  },
  workflowActionTriggerSettings: {
    label: 'Generate Document',
    icon: 'IconFileText',
    inputSchema: jsonSchemaToInputSchema(generateDocumentInputSchema),
    outputSchema: [{ type: 'object', properties: {
      success: { type: 'boolean' }, documentId: { type: 'string' },
    } }],
  },
  handler: generateDocumentHandler,
});
```

Le schéma d'entrée est un schéma JSON simple décrivant `templateId` et `recordId` —
voir [`generate-document-input.schema.ts`](https://github.com/exceev-technology/upshift/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/schemas/generate-document-input.schema.ts).

## Accorder l'accès

Les fonctions logiques s'exécutent en tant que rôle de l'application. Il a besoin de lire les modèles et les enregistrements
et de créer des documents, donc permettez cela dans `src/roles/default-role.ts`:

```ts theme={null}
export default defineApplicationRole({
  universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
  label: 'Document Generator default role',
  canReadAllObjectRecords: true,
  canUpdateAllObjectRecords: true,
  canAccessAllTools: true,
  canBeAssignedToAgents: true,
  permissionFlagUniversalIdentifiers: [SystemPermissionFlag.UPLOAD_FILE],
});
```

`UPLOAD_FILE` permet à la fonction d'envoyer le PDF généré dans la section suivante.
Voir [Roles](https://docs.upshiftcloud.com/developers/extend/apps/config/roles.md) pour des autorisations plus fines.

## Joindre un vrai fichier PDF

Un champ de texte rendu est utile, mais les utilisateurs veulent un vrai document. Nous allons générer un
**PDF** et le stocker dans l'enregistrement en tant que fichier téléchargeable.

Premièrement, donnez à l'objet `document` un champ `FILES` pour contenir le PDF. Les applications téléchargent
dans leurs champs **propres** de fichiers, donc ce champ permet de router le téléchargement:

```ts filename="src/objects/document.object.ts" theme={null}
{
  universalIdentifier: DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
  type: FieldType.FILES,
  name: 'file',
  label: 'File',
  icon: 'IconFileTypePdf',
  universalSettings: { maxNumberOfValues: 1 },
}
```

Maintenant, renvoie ce PDF. Une application est un vrai projet Node, vous pouvez donc ajouter n'importe quel package npm
dont vous avez besoin et l'importer n'importe où ailleurs. Nous utilisons **[pdf-lib](https://pdf-lib.js.org/)**
pour dessiner le PDF et **[marked](https://marked.js.org/)** pour analyser le corps du Markdown
— le CLI les installe dans le runtime de la fonction pour vous :

```bash filename="Terminal" theme={null}
yarn add pdf-lib marked
```

L'aide complète est
[`generate-document-pdf.ts`](https://github.com/exceev-technology/upshift/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/generate-document-pdf.ts).
Il analyse le Markdown en jetons avec `marked. exer`, puis les pose avec
pdf-lib: de vraies rubriques, **gras**/*italique* exécute, puces et listes numérotées,
blockquotes et règles — un rendu A4 polyvalent et polyvalent du modèle
lui-même, plutôt qu'un mur de texte.

![Un PDF brossé et commercialisable généré](https://mintcdn.com/exceev-group/1cFKDdENVawzNm26/images/docs/developers/extends/apps/document-generator/07b-generated-pdf.png?fit=max\&auto=format\&n=1cFKDdENVawzNm26\&q=85\&s=4a23827a32bd3c3a507441535be29418)

Le PDF généré : typographie réelle et mise en forme Markdown, rendant le corps du modèle.

> [!NOTE]
>
> Les polices intégrées de pdf-lib's utilisent l'encodage WinAnsi, de sorte que les accents WesternEuropean rendent
> hors de la boite; l'aide mappe les guillemets intelligents et les tirets et drops les caractères qu'elle
> ne peut pas encoder. Le rendu de scripts non latins (chinois, arabe, cyrillique) impliquerait
> d’intégrer une police Unicode.

Ensuite téléchargez-le et stockez la référence sur l'enregistrement. `uploadFile` achemine les octets
vers votre champ de fichiers appartenant à l'application; le `id` retourné est ce que vous sauvegardez:

```ts filename="src/logic-functions/handlers/generate-document-handler.ts" theme={null}
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { generateDocumentPdf } from 'src/logic-functions/utils/generate-document-pdf';

const documentName = `${documentTemplate.name} — ${record.displayName}`;
const bytes = await generateDocumentPdf(documentName, content);
const fileName = 'proposal.pdf';

const uploaded = await new MetadataApiClient().uploadFile(
  Buffer.from(bytes),
  fileName,
  'application/pdf',
  DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
);

await client.mutation({
  updateDocument: {
    __args: {
      id: documentId,
      data: { file: [{ fileId: uploaded.id, label: fileName }] },
    },
    id: true,
  },
});
```

Le document généré possède maintenant un PDF téléchargeable:

![Un enregistrement de document avec un fichier PDF généré](https://mintcdn.com/exceev-group/1cFKDdENVawzNm26/images/docs/developers/extends/apps/document-generator/08-document-with-pdf.png?fit=max\&auto=format\&n=1cFKDdENVawzNm26\&q=85\&s=34361cb98322a2a75b3cdbbdddd4d282)

Le PDF généré, stocké dans le champ Fichier du document.

> [!NOTE]
>
> `uploadFile` ne cible que les champs de fichiers **app-owned** (donc les téléchargements nécessitent toujours une application
> qui possède le champ, plus le paramètre `UPLOAD_FILE`). C'est pourquoi le PDF
> se trouve sur le propre champ `file` de l'enregistrement — le même motif que l'application
> [call-recorder app](https://github.com/exceev-technology/upshift/tree/main/packages/twenty-apps/public/call-recorder)
> utilise pour les enregistrements.

**Après cette étape:** chaque document généré a un PDF réel, téléchargeable. Mais
rien ne peut *appeler* le générateur de l'interface utilisateur — pour cela nous avons besoin d'une route HTTP.

> **[Suivant : Routes HTTP →](https://docs.upshiftcloud.com/developers/extend/apps/tutorials/document-generator/http-routes.md)**
>
> Servez la fonction via HTTP et rendez les documents en tant que pages Web.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.