From 9299f90b85a0c0f88ffcd2a0ac7e99254fa88d70 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 00:25:23 +0200 Subject: [PATCH] feat(docs): internal API docs --- bin/copy-dist.ts | 7 ++++++- bin/generate-openapi.ts | 9 +++++++-- src/routes/api/openapi.json | 1 + src/routes/api_docs.ts | 24 +++++++++++++++++------- 4 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 src/routes/api/openapi.json diff --git a/bin/copy-dist.ts b/bin/copy-dist.ts index 85013e992..83a31bbf7 100644 --- a/bin/copy-dist.ts +++ b/bin/copy-dist.ts @@ -29,7 +29,12 @@ const copy = async () => { fs.copySync(path.join("build", srcFile), destFile, { recursive: true }); } - const filesToCopy = ["config-sample.ini", "tsconfig.webpack.json", "./src/etapi/etapi.openapi.yaml"]; + const filesToCopy = [ + "config-sample.ini", + "tsconfig.webpack.json", + "./src/etapi/etapi.openapi.yaml", + "./src/routes/api/openapi.json" + ]; for (const file of filesToCopy) { log(`Copying ${file}`); await fs.copy(file, path.join(DEST_DIR, file)); diff --git a/bin/generate-openapi.ts b/bin/generate-openapi.ts index 3ec26b2c3..4bd97a76f 100644 --- a/bin/generate-openapi.ts +++ b/bin/generate-openapi.ts @@ -1,4 +1,7 @@ +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; import swaggerJsdoc from 'swagger-jsdoc'; +import fs from "fs"; /* * Usage: npm run generate-openapi | tail -n1 > x.json @@ -33,8 +36,10 @@ const options = { }; const openapiSpecification = swaggerJsdoc(options); - -console.log(JSON.stringify(openapiSpecification)); +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const outputPath = join(scriptDir, "..", "src", "routes", "api", "openapi.json"); +fs.writeFileSync(outputPath, JSON.stringify(openapiSpecification)); +console.log("Saved to ", outputPath); /** * @swagger diff --git a/src/routes/api/openapi.json b/src/routes/api/openapi.json new file mode 100644 index 000000000..3034c7e13 --- /dev/null +++ b/src/routes/api/openapi.json @@ -0,0 +1 @@ +{"openapi":"3.1.1","info":{"title":"Trilium Notes - Sync server API","version":"0.96.6","description":"This is the internal sync server API used by Trilium Notes / TriliumNext Notes.\n\n_If you're looking for the officially supported External Trilium API, see [here](https://triliumnext.github.io/Docs/Wiki/etapi.html)._\n\nThis page does not yet list all routes. For a full list, see the [route controller](https://github.com/TriliumNext/Notes/blob/v0.91.6/src/routes/routes.ts).","contact":{"name":"TriliumNext issue tracker","url":"https://github.com/TriliumNext/Notes/issues"},"license":{"name":"GNU Free Documentation License 1.3 (or later)","url":"https://www.gnu.org/licenses/fdl-1.3"}},"paths":{"/api/setup/sync-seed":{"get":{"tags":["auth"],"summary":"Sync documentSecret value","description":"First step to logging in.","operationId":"setup-sync-seed","responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"syncVersion":{"type":"integer","example":34},"options":{"type":"object","properties":{"documentSecret":{"type":"string"}}}}}}}}},"security":[{"user-password":[]}]}},"/api/app-info":{"get":{"summary":"Get installation info","operationId":"app-info","externalDocs":{"description":"Server implementation","url":"https://github.com/TriliumNext/Notes/blob/v0.91.6/src/services/app_info.ts"},"responses":{"200":{"description":"Installation info","content":{"application/json":{"schema":{"type":"object","properties":{"appVersion":{"type":"string","example":"0.91.6"},"dbVersion":{"type":"integer","example":228},"nodeVersion":{"type":"string","description":"value of process.version"},"syncVersion":{"type":"integer","example":34},"buildDate":{"type":"string","example":"2024-09-07T18:36:34Z"},"buildRevision":{"type":"string","example":"7c0d6930fa8f20d269dcfbcbc8f636a25f6bb9a7"},"dataDirectory":{"type":"string","example":"/var/lib/trilium"},"clipperProtocolVersion":{"type":"string","example":"1.0"},"utcDateTime":{"$ref":"#/components/schemas/UtcDateTime"}}}}}}},"security":[{"session":[]}]}},"/api/branches/{branchId}":{"delete":{"summary":"Delete branch (note clone)","operationId":"branches-delete","parameters":[{"name":"branchId","in":"path","required":true,"schema":{"$ref":"#/components/schemas/BranchId"}},{"name":"taskId","in":"query","required":true,"schema":{"type":"string"},"description":"Task group identifier"},{"name":"eraseNotes","in":"query","schema":{"type":"boolean"},"required":false,"description":"Whether to erase the note immediately"},{"name":"last","in":"query","schema":{"type":"boolean"},"required":true,"description":"Whether this is the last request of this task group"}],"responses":{"200":{"description":"Branch successfully deleted","content":{"application/json":{"schema":{"type":"object","properties":{"noteDeleted":{"type":"boolean","description":"Whether the last note clone was deleted"}}}}}}},"security":[{"session":[]}],"tags":["data"]}},"/api/login/sync":{"post":{"tags":["auth"],"summary":"Log in using documentSecret","description":"The `hash` parameter is computed using a HMAC of the `documentSecret` and `timestamp`.","operationId":"login-sync","externalDocs":{"description":"HMAC calculation","url":"https://github.com/TriliumNext/Notes/blob/v0.91.6/src/services/utils.ts#L62-L66"},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"timestamp":{"$ref":"#/components/schemas/UtcDateTime"},"hash":{"type":"string"},"syncVersion":{"type":"integer","example":34}}}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"syncVersion":{"type":"integer","example":34},"options":{"type":"object","properties":{"documentSecret":{"type":"string"}}}}}}}},"400":{"description":"Sync version / document secret mismatch","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Non-matching sync versions, local is version ${server syncVersion}, remote is ${requested syncVersion}. It is recommended to run same version of Trilium on both sides of sync"}}}}}},"401":{"description":"Timestamp mismatch","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Auth request time is out of sync, please check that both client and server have correct time. The difference between clocks has to be smaller than 5 minutes"}}}}}}}}},"/api/notes/{noteId}":{"get":{"summary":"Retrieve note metadata","operationId":"notes-get","parameters":[{"name":"noteId","in":"path","required":true,"schema":{"$ref":"#/components/schemas/NoteId"}}],"responses":{"200":{"description":"Note metadata","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Note"},{"$ref":"#/components/schemas/Timestamps"}]}}}}},"security":[{"session":[]}],"tags":["data"]},"delete":{"summary":"Delete note","operationId":"notes-delete","parameters":[{"name":"noteId","in":"path","required":true,"schema":{"$ref":"#/components/schemas/NoteId"}},{"name":"taskId","in":"query","required":true,"schema":{"type":"string"},"description":"Task group identifier"},{"name":"eraseNotes","in":"query","schema":{"type":"boolean"},"required":false,"description":"Whether to erase the note immediately"},{"name":"last","in":"query","schema":{"type":"boolean"},"required":true,"description":"Whether this is the last request of this task group"}],"responses":{"200":{"description":"Note successfully deleted"}},"security":[{"session":[]}],"tags":["data"]}},"/api/notes/{noteId}/blob":{"get":{"summary":"Retrieve note content","operationId":"notes-blob","parameters":[{"name":"noteId","in":"path","required":true,"schema":{"$ref":"#/components/schemas/NoteId"}}],"responses":{"304":{"description":"Note content","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Blob"}}}}},"security":[{"session":[]}],"tags":["data"]}},"/api/notes/{noteId}/metadata":{"get":{"summary":"Retrieve note metadata (limited to timestamps)","operationId":"notes-metadata","parameters":[{"name":"noteId","in":"path","required":true,"schema":{"$ref":"#/components/schemas/NoteId"}}],"responses":{"200":{"description":"Note metadata","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Timestamps"}}}}},"security":[{"session":[]}],"tags":["data"]}},"/api/sync/changed":{"get":{"summary":"Pull sync changes","operationId":"sync-changed","externalDocs":{"description":"Server implementation","url":"https://github.com/TriliumNext/Notes/blob/v0.91.6/src/routes/api/sync.ts"},"parameters":[{"in":"query","name":"instanceId","required":true,"schema":{"type":"string"},"description":"Local instance ID"},{"in":"query","name":"lastEntityChangeId","required":true,"schema":{"type":"integer"},"description":"Last locally present change ID"},{"in":"query","name":"logMarkerId","required":true,"schema":{"type":"string"},"description":"Marker to identify this request in server log"}],"responses":{"200":{"description":"Sync changes, limited to approximately one megabyte.","content":{"application/json":{"schema":{"type":"object","properties":{"entityChanges":{"type":"list","items":{"$ref":"#/components/schemas/EntityChange"}},"lastEntityChangeId":{"type":"integer","description":"If `outstandingPullCount > 0`, pass this as parameter in your next request to continue."},"outstandingPullCount":{"type":"int","example":42,"description":"Number of changes not yet returned by the remote."}}}}}}},"security":[{"session":[]}],"tags":["sync"]}},"/api/sync/update":{"put":{"summary":"Push sync changes","description":"Basic usage: set `pageCount = 1`, `pageIndex = 0`, and omit `requestId`. Supply your entity changes in the request body.","operationId":"sync-update","externalDocs":{"description":"Server implementation","url":"https://github.com/TriliumNext/Notes/blob/v0.91.6/src/routes/api/sync.ts"},"parameters":[{"in":"header","name":"pageCount","required":true,"schema":{"type":"integer"}},{"in":"header","name":"pageIndex","required":true,"schema":{"type":"integer"}},{"in":"header","name":"requestId","schema":{"type":"string","description":"ID to identify paginated requests"}},{"in":"query","name":"logMarkerId","required":true,"schema":{"type":"string"},"description":"Marker to identify this request in server log"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"instanceId":{"type":"string","description":"Local instance ID"},"entities":{"type":"list","items":{"$ref":"#/components/schemas/EntityChange"}}}}}}},"responses":{"200":{"description":"Changes processed successfully"}},"security":[{"session":[]}],"tags":["sync"]}},"/api/tree":{"get":{"summary":"Retrieve tree data","operationId":"tree","externalDocs":{"description":"Server implementation","url":"https://github.com/TriliumNext/Notes/blob/v0.91.6/src/routes/api/tree.ts"},"parameters":[{"in":"query","name":"subTreeNoteId","required":false,"schema":{"type":"string"},"description":"Limit tree data to this note and descendants"}],"responses":{"200":{"description":"Notes, branches and attributes","content":{"application/json":{"schema":{"type":"object","properties":{"branches":{"type":"list","items":{"$ref":"#/components/schemas/Branch"}},"notes":{"type":"list","items":{"$ref":"#/components/schemas/Note"}},"attributes":{"type":"list","items":{"$ref":"#/components/schemas/Attribute"}}}}}}}},"security":[{"session":[]}],"tags":["data"]}}},"components":{},"tags":[]} \ No newline at end of file diff --git a/src/routes/api_docs.ts b/src/routes/api_docs.ts index 1535265c3..3df230feb 100644 --- a/src/routes/api_docs.ts +++ b/src/routes/api_docs.ts @@ -1,4 +1,4 @@ -import type { Router } from "express"; +import type { Application, Router } from "express"; import swaggerUi from "swagger-ui-express"; import { readFile } from "fs/promises"; import { fileURLToPath } from "url"; @@ -7,19 +7,29 @@ import yaml from "js-yaml"; import type { JsonObject } from "swagger-ui-express"; const __dirname = dirname(fileURLToPath(import.meta.url)); -const swaggerDocument = yaml.load( +const etapiDocument = yaml.load( await readFile(join(__dirname, "../etapi/etapi.openapi.yaml"), "utf8") ) as JsonObject; +const apiDocument = JSON.parse(await readFile(join(__dirname, "api", "openapi.json"), "utf-8")); -function register(router: Router) { - router.use( - "/etapi", - swaggerUi.serve, - swaggerUi.setup(swaggerDocument, { +function register(app: Application) { + app.use( + "/etapi/docs/", + swaggerUi.serveFiles(etapiDocument), + swaggerUi.setup(etapiDocument, { explorer: true, customSiteTitle: "TriliumNext ETAPI Documentation" }) ); + + app.use( + "/api/docs/", + swaggerUi.serveFiles(apiDocument), + swaggerUi.setup(apiDocument, { + explorer: true, + customSiteTitle: "TriliumNext Internal API Documentation" + }) + ); } export default {