DEV Community

Jorge
Jorge

Posted on • Originally published at jorge.aguilera.soy on

DocAsCode con Notion

"Notion es un software de gestión de proyectos y para tomar notas"

En realidad Notion es un poco más que lo que dice su descripción oficial y se ha vuelto muy popular entre las empresas (y a nivel personal) para mantener la documentación corporativa.

Puedes crear páginas, organizarlas en árbol, tiene muchos emojis (demasiados) disponibles, bases de datos por páginas donde puedes estructurar tus datos como quieras y la página asociada los renderiza, etc.

Sin embargo, lo mismo que pasaba con Confluence, la mayoría de nosotros lo usa a través de la interfaz web y acabamos teniendo mil páginas desactualizadas.

Siguiendo la propuesta de DocAsCode, donde la documentación deberíamos tratarla como el código (versionado, con revisiones y despliegues automatizados) y aprovechando que Notion tiene una API sencillo he desarrollado un pequeño script para mantener mi repositorio git, donde tengo los diagramas de arquitectura del proyecto, actualizado en Notion.

WARNING

Lo he desarrollado en javascript, porque sí, porque puedo

Idea

Lo típico en todos los proyectos (con suerte) es crear una página en Notion (antes se hacía en Confluence, y antes en un Wiki, y previamente en una carpeta compartida) donde describimos la arquitectura del proyecto, incluimos algún diagrama generado con alguna herramienta como Draw.io y con mucha suerte incluimos el enlace al código del diagrama en lugar de insertar el PNG generado, (pero esto es con mucha suerte. El 99.9999% de los casos incluimos el PNG porque claro, "la arquitectura no va a cambiar")

INFO

Por otra parte, no niego la facilidad de uso y potencia que ofrece Notion para otro tipo de comunicaciones

Así la idea es tener una página de Notion "Arquitectura" y un repositorio git donde tendré mis diagramas C4 junto con explicaciones a los mismos. Cada vez que actualize el repo (tal vez mergeando en master, tal vez cada commit) un pipeline ejecutará mi script y reconstruirá la página con la última versión extraída del repo.

Quiero poder tener una estructura de carpetas y sub carpetas en mi repo de tal forma que se replique en Notion colgando de la página "Arquitectura"

Preparación

En primer lugar, debemos tener un espacio ya creado en Notion (obvio) así como una página "master" de donde colgaremos las generadas. Esta página puede ser una principal o una hija. Así mismo, esta página puede contener el texto explicativo que queramos, pues el script lo que va a hacer es eliminar las hijas y recrearlas (así que no deberías crear hijas, pues el script las va a borrar)

En segundo lugar, necesitamos crear una "conection". Esto lo tiene que hacer el owner del espacio y básicamente es generar una aplicación privada y Notion nos devolverá un token a usar.

Una vez que tienes la "connection" hay que darle permisos para poder acceder a la página "master" (usando los tres puntitos arriba a la derecha)

Repositorio

Nuestro repositorio es un repositorio normal, donde puedes tener el código o lo que quieras, y simplemente vamos a usar una carpeta del mismo para tener nuestra documentación y diagramas (como código)

WARNING

Esta versión es muy simple y la documentación la va a subir como texto. No negritas, no italica, no titulos, , quién sabe, tal vez en otra version lo mejore a un parseador de markdown a notion

Por ejemplo:

|_ src
  |_ main
    |_ java
|_ docs
  |_ 1.- Context
    |_ content.txt
    |_ content.puml
  |_ 2.- Applications
    |_ 1.- API
        |_ content.txt
        |_ content.puml
    |_ 2.- Cron
        |_ content.txt
        |_ content.puml
Enter fullscreen mode Exit fullscreen mode

En principio el nombre de los ficheros da igual, solo importa que haya uno con una extensión .txt y otro con extension .puml, (pero puedes cambiarlo a tu gusto)

Los ficheros .puml contienen los diagramas en formato texto y casi todos los IDEs tienen un plugin para editarlos y renderizarlos. Por ejemplo Intellij o VsCode los tienen, por lo que puedes ir viendo el diagrama a la vez que los editas

Un ejemplo de un puml

@startuml
!include https://raw.githubusercontent.com/adrianvlupu/C4-PlantUML/latest/C4_Context.puml

LAYOUT_WITH_LEGEND()

Person(pbc, "Customer", "A customer of our application.")
System(api, "MyCompany", "Place orders.")
System_Ext(es, "Exchange system", "Exchange systems.")
System_Ext(nf, "Notifications system", "Notifications system")

Rel(pbc, api, "Uses", "REST")

Rel(api, es, "Uses")

Rel(api, nf, "Uses")

Rel(nf, pbc, "Sends notificaions")
@enduml
Enter fullscreen mode Exit fullscreen mode

Al ser texto plano es fácil versionarlo, ver las diferencias, crear PR, etc

Script

Por último añadiremos a nuestro repo el siguiente script (en javascript, toma ya)

const fs = require('fs');
const path = require('path');

const { Client } = require("@notionhq/client")

const pako = require("pako")

require('dotenv').config();

// Initializing a client
const notion = new Client({
    auth: process.env.NOTION_TOKEN,
})

async function removeSubPages( id ){
    const page = await notion.blocks.children.list({
        block_id: id
    })
    const blocks = page.results.filter( (c)=> c.type =='child_page' ).map( (c)=> c.id)
    for(b of blocks){
        console.log(`deleting ${b}`)
        const deleted = await notion.blocks.delete({
            block_id: b
        })
    }
}

async function processFolder(directory, parentId, title){
    console.log(`Processing ${directory}`)
    const blocks = []

    const files = fs.readdirSync(directory)
    const fcontent = files.filter( (f)=>path.extname(f) ==".txt")
    const fuml = files.filter( (f)=>path.extname(f)==".puml")

    if( fcontent.length == 1) {
        const content = fs.readFileSync(`${directory}/${fcontent[0]}`, {
            encoding: 'utf8',
            flag: 'r'
        });
        blocks.push({
            "type": "paragraph",
            "paragraph": {
                "rich_text": [{
                    "type": "text",
                    "text": {
                        "content": content,
                        "link": null
                    }
                }]
            }
        })
    }
    if( fuml.length == 1) {
        const diagramSource = fs.readFileSync(`${directory}/${fuml[0]}`, {
            encoding: 'utf8',
            flag: 'r'
        });
        const data = Buffer.from(diagramSource, 'utf8')
        const compressed = pako.deflate(data, {level: 9})
        const result = Buffer.from(compressed)
            .toString('base64')
            .replace(/\+/g, '-').replace(/\//g, '_')

        blocks.push({
            "type": "embed",
            "embed": {
                "url": "https://kroki.io/plantuml/png/" + result
            }
        })
    }
    blocks.push({
        "type": "paragraph",
        "paragraph": {
            "rich_text": [{
                "type": "text",
                "text": {
                    "content": `\n(No modify this page, it's generated automatically)\n`,
                    "link": null
                }
            }]
        }
    })
    const newpage = await notion.pages.create({
        parent: {
            page_id: parentId
        },
        properties: {
            title: [{
                "text": {
                    "content": title
                }
            }]
        },
        children: blocks
    })
    console.log(`Page ${newpage.id} created`)
    const folders = fs.readdirSync(directory);
    for( subdir of folders) {
        if( fs.statSync(`${directory}/${subdir}`).isDirectory() )
            await processFolder( `${directory}/${subdir}`, newpage.id, subdir)
    }
}

;(async () => {
    // clean page
    await removeSubPages(process.env.PAGE);

    const folders = fs.readdirSync('docs')
    const parentId = process.env.PAGE
    for( subdir of folders) {
        if( fs.statSync(`docs/${subdir}`).isDirectory() )
            await processFolder( `docs/${subdir}`, parentId, subdir)
    }
})()
Enter fullscreen mode Exit fullscreen mode

Para no incluir el token en el código y así poder versional el script junto con nuestro código, usaremos un fichero.env donde pondremos los siguientes valores:

.env

NOTION_TOKEN=secret_nN8Zxxxxxxxxxxxx
PAGE=2f188xxxxxxxxxxxxxxxxx
Enter fullscreen mode Exit fullscreen mode

Donde PAGE es la parte final de la url de notion de nuestra página master (los numeros despues del titulo)

Ejecución

Simplemente, necesitamos hacer un npm install una vez para que se bajen las dependencias y una vez bajadas ejecutaremos

node upload2notion.js

Si todo va bien verás como las páginas hijas de la master desaparecen (si había) y se van creando las nuevas.

Cada página hija contendrá un texto y/o una imagen (en realidad un enlace al servicio de kroki.io)

Disclaimer

NO tengo ni idea de JavaScript asi que el código seguramente sea un atentado a ojos de gente con más experiencia

Top comments (0)