mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-04 13:39:01 +01:00 
			
		
		
		
	Merge pull request #28 from TriliumNext/feature/typescript_backend_4
Convert backend to TypeScript (50% -> 64%)
This commit is contained in:
		
						commit
						6ac3c172b1
					
				
							
								
								
									
										120
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										120
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -88,16 +88,22 @@
 | 
				
			|||||||
        "trilium": "src/www.js"
 | 
					        "trilium": "src/www.js"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "devDependencies": {
 | 
					      "devDependencies": {
 | 
				
			||||||
 | 
					        "@types/archiver": "^6.0.2",
 | 
				
			||||||
        "@types/better-sqlite3": "^7.6.9",
 | 
					        "@types/better-sqlite3": "^7.6.9",
 | 
				
			||||||
        "@types/cls-hooked": "^4.3.8",
 | 
					        "@types/cls-hooked": "^4.3.8",
 | 
				
			||||||
        "@types/escape-html": "^1.0.4",
 | 
					        "@types/escape-html": "^1.0.4",
 | 
				
			||||||
        "@types/express": "^4.17.21",
 | 
					        "@types/express": "^4.17.21",
 | 
				
			||||||
 | 
					        "@types/html": "^1.0.4",
 | 
				
			||||||
        "@types/ini": "^4.1.0",
 | 
					        "@types/ini": "^4.1.0",
 | 
				
			||||||
        "@types/jsdom": "^21.1.6",
 | 
					        "@types/jsdom": "^21.1.6",
 | 
				
			||||||
        "@types/mime-types": "^2.1.4",
 | 
					        "@types/mime-types": "^2.1.4",
 | 
				
			||||||
        "@types/node": "^20.11.19",
 | 
					        "@types/node": "^20.11.19",
 | 
				
			||||||
        "@types/sanitize-html": "^2.11.0",
 | 
					        "@types/sanitize-html": "^2.11.0",
 | 
				
			||||||
 | 
					        "@types/sax": "^1.2.7",
 | 
				
			||||||
 | 
					        "@types/stream-throttle": "^0.1.4",
 | 
				
			||||||
 | 
					        "@types/turndown": "^5.0.4",
 | 
				
			||||||
        "@types/ws": "^8.5.10",
 | 
					        "@types/ws": "^8.5.10",
 | 
				
			||||||
 | 
					        "@types/xml2js": "^0.4.14",
 | 
				
			||||||
        "cross-env": "7.0.3",
 | 
					        "cross-env": "7.0.3",
 | 
				
			||||||
        "electron": "25.9.8",
 | 
					        "electron": "25.9.8",
 | 
				
			||||||
        "electron-builder": "24.13.3",
 | 
					        "electron-builder": "24.13.3",
 | 
				
			||||||
@ -1169,6 +1175,15 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-21.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-21.0.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-qVfOiFh0U8ZSkLgA6tf7kj2MciqRbSCWaJZRwftVO7UbtVDNsZAXpWXqvCDtIefvjC83UJB+vHTDOGm5ibXjEA=="
 | 
					      "integrity": "sha512-qVfOiFh0U8ZSkLgA6tf7kj2MciqRbSCWaJZRwftVO7UbtVDNsZAXpWXqvCDtIefvjC83UJB+vHTDOGm5ibXjEA=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@types/archiver": {
 | 
				
			||||||
 | 
					      "version": "6.0.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-KmROQqbQzKGuaAbmK+ZcytkJ51+YqDa7NmbXjmtC5YBLSyQYo21YaUnQ3HbaPFKL1ooo6RQ6OPYPIDyxfpDDXw==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@types/readdir-glob": "*"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/better-sqlite3": {
 | 
					    "node_modules/@types/better-sqlite3": {
 | 
				
			||||||
      "version": "7.6.9",
 | 
					      "version": "7.6.9",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.9.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.9.tgz",
 | 
				
			||||||
@ -1318,6 +1333,12 @@
 | 
				
			|||||||
        "@types/node": "*"
 | 
					        "@types/node": "*"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@types/html": {
 | 
				
			||||||
 | 
					      "version": "1.0.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/html/-/html-1.0.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-Wb1ymSAftCLxhc3D6vS0Ike/0xg7W6c+DQxAkerU6pD7C8CMzTYwvrwnlcrTfsVO/nMelB9KOKIT7+N5lOeQUg==",
 | 
				
			||||||
 | 
					      "dev": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/http-cache-semantics": {
 | 
					    "node_modules/@types/http-cache-semantics": {
 | 
				
			||||||
      "version": "4.0.1",
 | 
					      "version": "4.0.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
 | 
				
			||||||
@ -1468,6 +1489,15 @@
 | 
				
			|||||||
      "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
 | 
					      "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
 | 
				
			||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@types/readdir-glob": {
 | 
				
			||||||
 | 
					      "version": "1.1.5",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@types/node": "*"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/responselike": {
 | 
					    "node_modules/@types/responselike": {
 | 
				
			||||||
      "version": "1.0.0",
 | 
					      "version": "1.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
 | 
				
			||||||
@ -1559,6 +1589,15 @@
 | 
				
			|||||||
        "entities": "^4.4.0"
 | 
					        "entities": "^4.4.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@types/sax": {
 | 
				
			||||||
 | 
					      "version": "1.2.7",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@types/node": "*"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/send": {
 | 
					    "node_modules/@types/send": {
 | 
				
			||||||
      "version": "0.17.4",
 | 
					      "version": "0.17.4",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
 | 
				
			||||||
@ -1580,12 +1619,27 @@
 | 
				
			|||||||
        "@types/node": "*"
 | 
					        "@types/node": "*"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@types/stream-throttle": {
 | 
				
			||||||
 | 
					      "version": "0.1.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/stream-throttle/-/stream-throttle-0.1.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-VxXIHGjVuK8tYsVm60rIQMmF/0xguCeen5OmK5S4Y6K64A+z+y4/GI6anRnVzaUZaJB9Ah9IfbDcO0o1gZCc/w==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@types/node": "*"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/tough-cookie": {
 | 
					    "node_modules/@types/tough-cookie": {
 | 
				
			||||||
      "version": "4.0.5",
 | 
					      "version": "4.0.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
 | 
					      "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
 | 
				
			||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@types/turndown": {
 | 
				
			||||||
 | 
					      "version": "5.0.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-28GI33lCCkU4SGH1GvjDhFgOVr+Tym4PXGBIU1buJUa6xQolniPArtUT+kv42RR2N9MsMLInkr904Aq+ESHBJg==",
 | 
				
			||||||
 | 
					      "dev": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/unist": {
 | 
					    "node_modules/@types/unist": {
 | 
				
			||||||
      "version": "2.0.10",
 | 
					      "version": "2.0.10",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
 | 
				
			||||||
@ -1607,6 +1661,15 @@
 | 
				
			|||||||
        "@types/node": "*"
 | 
					        "@types/node": "*"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@types/xml2js": {
 | 
				
			||||||
 | 
					      "version": "0.4.14",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@types/node": "*"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/yauzl": {
 | 
					    "node_modules/@types/yauzl": {
 | 
				
			||||||
      "version": "2.9.2",
 | 
					      "version": "2.9.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz",
 | 
				
			||||||
@ -14084,6 +14147,15 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-21.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-21.0.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-qVfOiFh0U8ZSkLgA6tf7kj2MciqRbSCWaJZRwftVO7UbtVDNsZAXpWXqvCDtIefvjC83UJB+vHTDOGm5ibXjEA=="
 | 
					      "integrity": "sha512-qVfOiFh0U8ZSkLgA6tf7kj2MciqRbSCWaJZRwftVO7UbtVDNsZAXpWXqvCDtIefvjC83UJB+vHTDOGm5ibXjEA=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "@types/archiver": {
 | 
				
			||||||
 | 
					      "version": "6.0.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-KmROQqbQzKGuaAbmK+ZcytkJ51+YqDa7NmbXjmtC5YBLSyQYo21YaUnQ3HbaPFKL1ooo6RQ6OPYPIDyxfpDDXw==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "@types/readdir-glob": "*"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "@types/better-sqlite3": {
 | 
					    "@types/better-sqlite3": {
 | 
				
			||||||
      "version": "7.6.9",
 | 
					      "version": "7.6.9",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.9.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.9.tgz",
 | 
				
			||||||
@ -14233,6 +14305,12 @@
 | 
				
			|||||||
        "@types/node": "*"
 | 
					        "@types/node": "*"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "@types/html": {
 | 
				
			||||||
 | 
					      "version": "1.0.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/html/-/html-1.0.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-Wb1ymSAftCLxhc3D6vS0Ike/0xg7W6c+DQxAkerU6pD7C8CMzTYwvrwnlcrTfsVO/nMelB9KOKIT7+N5lOeQUg==",
 | 
				
			||||||
 | 
					      "dev": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "@types/http-cache-semantics": {
 | 
					    "@types/http-cache-semantics": {
 | 
				
			||||||
      "version": "4.0.1",
 | 
					      "version": "4.0.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
 | 
				
			||||||
@ -14376,6 +14454,15 @@
 | 
				
			|||||||
      "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
 | 
					      "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
 | 
				
			||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "@types/readdir-glob": {
 | 
				
			||||||
 | 
					      "version": "1.1.5",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "@types/node": "*"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "@types/responselike": {
 | 
					    "@types/responselike": {
 | 
				
			||||||
      "version": "1.0.0",
 | 
					      "version": "1.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
 | 
				
			||||||
@ -14444,6 +14531,15 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "@types/sax": {
 | 
				
			||||||
 | 
					      "version": "1.2.7",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "@types/node": "*"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "@types/send": {
 | 
					    "@types/send": {
 | 
				
			||||||
      "version": "0.17.4",
 | 
					      "version": "0.17.4",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
 | 
				
			||||||
@ -14465,12 +14561,27 @@
 | 
				
			|||||||
        "@types/node": "*"
 | 
					        "@types/node": "*"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "@types/stream-throttle": {
 | 
				
			||||||
 | 
					      "version": "0.1.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/stream-throttle/-/stream-throttle-0.1.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-VxXIHGjVuK8tYsVm60rIQMmF/0xguCeen5OmK5S4Y6K64A+z+y4/GI6anRnVzaUZaJB9Ah9IfbDcO0o1gZCc/w==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "@types/node": "*"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "@types/tough-cookie": {
 | 
					    "@types/tough-cookie": {
 | 
				
			||||||
      "version": "4.0.5",
 | 
					      "version": "4.0.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
 | 
					      "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
 | 
				
			||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "@types/turndown": {
 | 
				
			||||||
 | 
					      "version": "5.0.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-28GI33lCCkU4SGH1GvjDhFgOVr+Tym4PXGBIU1buJUa6xQolniPArtUT+kv42RR2N9MsMLInkr904Aq+ESHBJg==",
 | 
				
			||||||
 | 
					      "dev": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "@types/unist": {
 | 
					    "@types/unist": {
 | 
				
			||||||
      "version": "2.0.10",
 | 
					      "version": "2.0.10",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
 | 
				
			||||||
@ -14492,6 +14603,15 @@
 | 
				
			|||||||
        "@types/node": "*"
 | 
					        "@types/node": "*"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "@types/xml2js": {
 | 
				
			||||||
 | 
					      "version": "0.4.14",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "@types/node": "*"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "@types/yauzl": {
 | 
					    "@types/yauzl": {
 | 
				
			||||||
      "version": "2.9.2",
 | 
					      "version": "2.9.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz",
 | 
				
			||||||
 | 
				
			|||||||
@ -109,16 +109,22 @@
 | 
				
			|||||||
    "yauzl": "3.1.2"
 | 
					    "yauzl": "3.1.2"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
 | 
					    "@types/archiver": "^6.0.2",
 | 
				
			||||||
    "@types/better-sqlite3": "^7.6.9",
 | 
					    "@types/better-sqlite3": "^7.6.9",
 | 
				
			||||||
    "@types/cls-hooked": "^4.3.8",
 | 
					    "@types/cls-hooked": "^4.3.8",
 | 
				
			||||||
    "@types/escape-html": "^1.0.4",
 | 
					    "@types/escape-html": "^1.0.4",
 | 
				
			||||||
    "@types/express": "^4.17.21",
 | 
					    "@types/express": "^4.17.21",
 | 
				
			||||||
 | 
					    "@types/html": "^1.0.4",
 | 
				
			||||||
    "@types/ini": "^4.1.0",
 | 
					    "@types/ini": "^4.1.0",
 | 
				
			||||||
    "@types/jsdom": "^21.1.6",
 | 
					    "@types/jsdom": "^21.1.6",
 | 
				
			||||||
    "@types/mime-types": "^2.1.4",
 | 
					    "@types/mime-types": "^2.1.4",
 | 
				
			||||||
    "@types/node": "^20.11.19",
 | 
					    "@types/node": "^20.11.19",
 | 
				
			||||||
    "@types/sanitize-html": "^2.11.0",
 | 
					    "@types/sanitize-html": "^2.11.0",
 | 
				
			||||||
 | 
					    "@types/sax": "^1.2.7",
 | 
				
			||||||
 | 
					    "@types/stream-throttle": "^0.1.4",
 | 
				
			||||||
 | 
					    "@types/turndown": "^5.0.4",
 | 
				
			||||||
    "@types/ws": "^8.5.10",
 | 
					    "@types/ws": "^8.5.10",
 | 
				
			||||||
 | 
					    "@types/xml2js": "^0.4.14",
 | 
				
			||||||
    "cross-env": "7.0.3",
 | 
					    "cross-env": "7.0.3",
 | 
				
			||||||
    "electron": "25.9.8",
 | 
					    "electron": "25.9.8",
 | 
				
			||||||
    "electron-builder": "24.13.3",
 | 
					    "electron-builder": "24.13.3",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
const anonymizationService = require('./services/anonymization');
 | 
					import anonymizationService = require('./services/anonymization');
 | 
				
			||||||
const sqlInit = require('./services/sql_init');
 | 
					import sqlInit = require('./services/sql_init');
 | 
				
			||||||
require('./becca/entity_constructor');
 | 
					require('./becca/entity_constructor');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
sqlInit.dbReady.then(async () => {
 | 
					sqlInit.dbReady.then(async () => {
 | 
				
			||||||
@ -16,7 +16,7 @@ sqlInit.dbReady.then(async () => {
 | 
				
			|||||||
            console.log("Anonymization failed.");
 | 
					            console.log("Anonymization failed.");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    catch (e) {
 | 
					    catch (e: any) {
 | 
				
			||||||
        console.error(e.message, e.stack);
 | 
					        console.error(e.message, e.stack);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										10
									
								
								src/app.js
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								src/app.js
									
									
									
									
									
								
							@ -26,10 +26,10 @@ app.use(helmet({
 | 
				
			|||||||
    crossOriginEmbedderPolicy: false
 | 
					    crossOriginEmbedderPolicy: false
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app.use(express.text({limit: '500mb'}));
 | 
					app.use(express.text({ limit: '500mb' }));
 | 
				
			||||||
app.use(express.json({limit: '500mb'}));
 | 
					app.use(express.json({ limit: '500mb' }));
 | 
				
			||||||
app.use(express.raw({limit: '500mb'}));
 | 
					app.use(express.raw({ limit: '500mb' }));
 | 
				
			||||||
app.use(express.urlencoded({extended: false}));
 | 
					app.use(express.urlencoded({ extended: false }));
 | 
				
			||||||
app.use(cookieParser());
 | 
					app.use(cookieParser());
 | 
				
			||||||
app.use(express.static(path.join(__dirname, 'public/root')));
 | 
					app.use(express.static(path.join(__dirname, 'public/root')));
 | 
				
			||||||
app.use(`/manifest.webmanifest`, express.static(path.join(__dirname, 'public/manifest.webmanifest')));
 | 
					app.use(`/manifest.webmanifest`, express.static(path.join(__dirname, 'public/manifest.webmanifest')));
 | 
				
			||||||
@ -49,7 +49,7 @@ require('./services/sync');
 | 
				
			|||||||
require('./services/backup');
 | 
					require('./services/backup');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// trigger consistency checks timer
 | 
					// trigger consistency checks timer
 | 
				
			||||||
require('./services/consistency_checks.js');
 | 
					require('./services/consistency_checks');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require('./services/scheduler.js');
 | 
					require('./services/scheduler.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -25,7 +25,7 @@ interface ContentOpts {
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
 | 
					abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected utcDateModified?: string;
 | 
					    utcDateModified?: string;
 | 
				
			||||||
    protected dateCreated?: string;
 | 
					    protected dateCreated?: string;
 | 
				
			||||||
    protected dateModified?: string;
 | 
					    protected dateModified?: string;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
				
			|||||||
@ -62,7 +62,7 @@ export interface BlobRow {
 | 
				
			|||||||
    utcDateModified: string;
 | 
					    utcDateModified: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type AttributeType = "label" | "relation";
 | 
					export type AttributeType = "label" | "relation" | "label-definition" | "relation-definition";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface AttributeRow {
 | 
					export interface AttributeRow {
 | 
				
			||||||
    attributeId?: string;
 | 
					    attributeId?: string;
 | 
				
			||||||
 | 
				
			|||||||
@ -7,8 +7,8 @@ const TaskContext = require('../services/task_context');
 | 
				
			|||||||
const v = require('./validators.js');
 | 
					const v = require('./validators.js');
 | 
				
			||||||
const searchService = require('../services/search/services/search');
 | 
					const searchService = require('../services/search/services/search');
 | 
				
			||||||
const SearchContext = require('../services/search/search_context');
 | 
					const SearchContext = require('../services/search/search_context');
 | 
				
			||||||
const zipExportService = require('../services/export/zip.js');
 | 
					const zipExportService = require('../services/export/zip');
 | 
				
			||||||
const zipImportService = require('../services/import/zip.js');
 | 
					const zipImportService = require('../services/import/zip');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function register(router) {
 | 
					function register(router) {
 | 
				
			||||||
    eu.route(router, 'get', '/etapi/notes', (req, res, next) => {
 | 
					    eu.route(router, 'get', '/etapi/notes', (req, res, next) => {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
const specialNotesService = require('../services/special_notes.js');
 | 
					const specialNotesService = require('../services/special_notes');
 | 
				
			||||||
const dateNotesService = require('../services/date_notes');
 | 
					const dateNotesService = require('../services/date_notes');
 | 
				
			||||||
const eu = require('./etapi_utils');
 | 
					const eu = require('./etapi_utils');
 | 
				
			||||||
const mappers = require('./mappers.js');
 | 
					const mappers = require('./mappers.js');
 | 
				
			||||||
@ -17,7 +17,7 @@ function isValidDate(date) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function register(router) {
 | 
					function register(router) {
 | 
				
			||||||
    eu.route(router, 'get', '/etapi/inbox/:date', (req, res, next) => {
 | 
					    eu.route(router, 'get', '/etapi/inbox/:date', (req, res, next) => {
 | 
				
			||||||
        const {date} = req.params;
 | 
					        const { date } = req.params;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!isValidDate(date)) {
 | 
					        if (!isValidDate(date)) {
 | 
				
			||||||
            throw getDateInvalidError(date);
 | 
					            throw getDateInvalidError(date);
 | 
				
			||||||
@ -28,7 +28,7 @@ function register(router) {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    eu.route(router, 'get', '/etapi/calendar/days/:date', (req, res, next) => {
 | 
					    eu.route(router, 'get', '/etapi/calendar/days/:date', (req, res, next) => {
 | 
				
			||||||
        const {date} = req.params;
 | 
					        const { date } = req.params;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!isValidDate(date)) {
 | 
					        if (!isValidDate(date)) {
 | 
				
			||||||
            throw getDateInvalidError(date);
 | 
					            throw getDateInvalidError(date);
 | 
				
			||||||
@ -39,7 +39,7 @@ function register(router) {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    eu.route(router, 'get', '/etapi/calendar/weeks/:date', (req, res, next) => {
 | 
					    eu.route(router, 'get', '/etapi/calendar/weeks/:date', (req, res, next) => {
 | 
				
			||||||
        const {date} = req.params;
 | 
					        const { date } = req.params;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!isValidDate(date)) {
 | 
					        if (!isValidDate(date)) {
 | 
				
			||||||
            throw getDateInvalidError(date);
 | 
					            throw getDateInvalidError(date);
 | 
				
			||||||
@ -50,7 +50,7 @@ function register(router) {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    eu.route(router, 'get', '/etapi/calendar/months/:month', (req, res, next) => {
 | 
					    eu.route(router, 'get', '/etapi/calendar/months/:month', (req, res, next) => {
 | 
				
			||||||
        const {month} = req.params;
 | 
					        const { month } = req.params;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!/[0-9]{4}-[0-9]{2}/.test(month)) {
 | 
					        if (!/[0-9]{4}-[0-9]{2}/.test(month)) {
 | 
				
			||||||
            throw getMonthInvalidError(month);
 | 
					            throw getMonthInvalidError(month);
 | 
				
			||||||
@ -61,7 +61,7 @@ function register(router) {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    eu.route(router, 'get', '/etapi/calendar/years/:year', (req, res, next) => {
 | 
					    eu.route(router, 'get', '/etapi/calendar/years/:year', (req, res, next) => {
 | 
				
			||||||
        const {year} = req.params;
 | 
					        const { year } = req.params;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!/[0-9]{4}/.test(year)) {
 | 
					        if (!/[0-9]{4}/.test(year)) {
 | 
				
			||||||
            throw getYearInvalidError(year);
 | 
					            throw getYearInvalidError(year);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
const becca = require('../../becca/becca');
 | 
					const becca = require('../../becca/becca');
 | 
				
			||||||
const blobService = require('../../services/blob');
 | 
					const blobService = require('../../services/blob');
 | 
				
			||||||
const ValidationError = require('../../errors/validation_error');
 | 
					const ValidationError = require('../../errors/validation_error');
 | 
				
			||||||
const imageService = require("../../services/image.js");
 | 
					const imageService = require("../../services/image");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getAttachmentBlob(req) {
 | 
					function getAttachmentBlob(req) {
 | 
				
			||||||
    const preview = req.query.preview === 'true';
 | 
					    const preview = req.query.preview === 'true';
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,7 @@ const cloneService = require('../../services/cloning');
 | 
				
			|||||||
const noteService = require('../../services/notes');
 | 
					const noteService = require('../../services/notes');
 | 
				
			||||||
const dateNoteService = require('../../services/date_notes');
 | 
					const dateNoteService = require('../../services/date_notes');
 | 
				
			||||||
const dateUtils = require('../../services/date_utils');
 | 
					const dateUtils = require('../../services/date_utils');
 | 
				
			||||||
const imageService = require('../../services/image.js');
 | 
					const imageService = require('../../services/image');
 | 
				
			||||||
const appInfo = require('../../services/app_info');
 | 
					const appInfo = require('../../services/app_info');
 | 
				
			||||||
const ws = require('../../services/ws');
 | 
					const ws = require('../../services/ws');
 | 
				
			||||||
const log = require('../../services/log');
 | 
					const log = require('../../services/log');
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ const sql = require('../../services/sql');
 | 
				
			|||||||
const log = require('../../services/log');
 | 
					const log = require('../../services/log');
 | 
				
			||||||
const backupService = require('../../services/backup');
 | 
					const backupService = require('../../services/backup');
 | 
				
			||||||
const anonymizationService = require('../../services/anonymization');
 | 
					const anonymizationService = require('../../services/anonymization');
 | 
				
			||||||
const consistencyChecksService = require('../../services/consistency_checks.js');
 | 
					const consistencyChecksService = require('../../services/consistency_checks');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getExistingBackups() {
 | 
					function getExistingBackups() {
 | 
				
			||||||
    return backupService.getExistingBackups();
 | 
					    return backupService.getExistingBackups();
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,8 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const zipExportService = require('../../services/export/zip.js');
 | 
					const zipExportService = require('../../services/export/zip');
 | 
				
			||||||
const singleExportService = require('../../services/export/single.js');
 | 
					const singleExportService = require('../../services/export/single');
 | 
				
			||||||
const opmlExportService = require('../../services/export/opml.js');
 | 
					const opmlExportService = require('../../services/export/opml');
 | 
				
			||||||
const becca = require('../../becca/becca');
 | 
					const becca = require('../../becca/becca');
 | 
				
			||||||
const TaskContext = require('../../services/task_context');
 | 
					const TaskContext = require('../../services/task_context');
 | 
				
			||||||
const log = require('../../services/log');
 | 
					const log = require('../../services/log');
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const imageService = require('../../services/image.js');
 | 
					const imageService = require('../../services/image');
 | 
				
			||||||
const becca = require('../../becca/becca');
 | 
					const becca = require('../../becca/becca');
 | 
				
			||||||
const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
 | 
					const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
 | 
				
			||||||
const fs = require('fs');
 | 
					const fs = require('fs');
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,9 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const enexImportService = require('../../services/import/enex.js');
 | 
					const enexImportService = require('../../services/import/enex');
 | 
				
			||||||
const opmlImportService = require('../../services/import/opml.js');
 | 
					const opmlImportService = require('../../services/import/opml');
 | 
				
			||||||
const zipImportService = require('../../services/import/zip.js');
 | 
					const zipImportService = require('../../services/import/zip');
 | 
				
			||||||
const singleImportService = require('../../services/import/single.js');
 | 
					const singleImportService = require('../../services/import/single');
 | 
				
			||||||
const cls = require('../../services/cls');
 | 
					const cls = require('../../services/cls');
 | 
				
			||||||
const path = require('path');
 | 
					const path = require('path');
 | 
				
			||||||
const becca = require('../../becca/becca');
 | 
					const becca = require('../../becca/becca');
 | 
				
			||||||
@ -13,8 +13,8 @@ const TaskContext = require('../../services/task_context');
 | 
				
			|||||||
const ValidationError = require('../../errors/validation_error');
 | 
					const ValidationError = require('../../errors/validation_error');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function importNotesToBranch(req) {
 | 
					async function importNotesToBranch(req) {
 | 
				
			||||||
    const {parentNoteId} = req.params;
 | 
					    const { parentNoteId } = req.params;
 | 
				
			||||||
    const {taskId, last} = req.body;
 | 
					    const { taskId, last } = req.body;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const options = {
 | 
					    const options = {
 | 
				
			||||||
        safeImport: req.body.safeImport !== 'false',
 | 
					        safeImport: req.body.safeImport !== 'false',
 | 
				
			||||||
@ -81,8 +81,8 @@ async function importNotesToBranch(req) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function importAttachmentsToNote(req) {
 | 
					async function importAttachmentsToNote(req) {
 | 
				
			||||||
    const {parentNoteId} = req.params;
 | 
					    const { parentNoteId } = req.params;
 | 
				
			||||||
    const {taskId, last} = req.body;
 | 
					    const { taskId, last } = req.body;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const options = {
 | 
					    const options = {
 | 
				
			||||||
        shrinkImages: req.body.shrinkImages !== 'false',
 | 
					        shrinkImages: req.body.shrinkImages !== 'false',
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
const becca = require('../../becca/becca');
 | 
					const becca = require('../../becca/becca');
 | 
				
			||||||
const markdownService = require('../../services/import/markdown.js');
 | 
					const markdownService = require('../../services/import/markdown');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getIconUsage() {
 | 
					function getIconUsage() {
 | 
				
			||||||
    const iconClassToCountMap = {};
 | 
					    const iconClassToCountMap = {};
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,10 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const imageType = require('image-type');
 | 
					const imageType = require('image-type');
 | 
				
			||||||
const imageService = require('../../services/image.js');
 | 
					const imageService = require('../../services/image');
 | 
				
			||||||
const noteService = require('../../services/notes');
 | 
					const noteService = require('../../services/notes');
 | 
				
			||||||
const {sanitizeAttributeName} = require('../../services/sanitize_attribute_name');
 | 
					const { sanitizeAttributeName } = require('../../services/sanitize_attribute_name');
 | 
				
			||||||
const specialNotesService = require('../../services/special_notes.js');
 | 
					const specialNotesService = require('../../services/special_notes');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function uploadImage(req) {
 | 
					function uploadImage(req) {
 | 
				
			||||||
    const file = req.file;
 | 
					    const file = req.file;
 | 
				
			||||||
@ -17,14 +17,14 @@ function uploadImage(req) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const parentNote = specialNotesService.getInboxNote(req.headers['x-local-date']);
 | 
					    const parentNote = specialNotesService.getInboxNote(req.headers['x-local-date']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const {note, noteId} = imageService.saveImage(parentNote.noteId, file.buffer, originalName, true);
 | 
					    const { note, noteId } = imageService.saveImage(parentNote.noteId, file.buffer, originalName, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const labelsStr = req.headers['x-labels'];
 | 
					    const labelsStr = req.headers['x-labels'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (labelsStr?.trim()) {
 | 
					    if (labelsStr?.trim()) {
 | 
				
			||||||
        const labels = JSON.parse(labelsStr);
 | 
					        const labels = JSON.parse(labelsStr);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const {name, value} of labels) {
 | 
					        for (const { name, value } of labels) {
 | 
				
			||||||
            note.setLabel(sanitizeAttributeName(name), value);
 | 
					            note.setLabel(sanitizeAttributeName(name), value);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -39,7 +39,7 @@ function uploadImage(req) {
 | 
				
			|||||||
function saveNote(req) {
 | 
					function saveNote(req) {
 | 
				
			||||||
    const parentNote = specialNotesService.getInboxNote(req.headers['x-local-date']);
 | 
					    const parentNote = specialNotesService.getInboxNote(req.headers['x-local-date']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const {note, branch} = noteService.createNewNote({
 | 
					    const { note, branch } = noteService.createNewNote({
 | 
				
			||||||
        parentNoteId: parentNote.noteId,
 | 
					        parentNoteId: parentNote.noteId,
 | 
				
			||||||
        title: req.body.title,
 | 
					        title: req.body.title,
 | 
				
			||||||
        content: req.body.content,
 | 
					        content: req.body.content,
 | 
				
			||||||
@ -49,7 +49,7 @@ function saveNote(req) {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (req.body.labels) {
 | 
					    if (req.body.labels) {
 | 
				
			||||||
        for (const {name, value} of req.body.labels) {
 | 
					        for (const { name, value } of req.body.labels) {
 | 
				
			||||||
            note.setLabel(sanitizeAttributeName(name), value);
 | 
					            note.setLabel(sanitizeAttributeName(name), value);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const sqlInit = require('../../services/sql_init');
 | 
					const sqlInit = require('../../services/sql_init');
 | 
				
			||||||
const setupService = require('../../services/setup.js');
 | 
					const setupService = require('../../services/setup');
 | 
				
			||||||
const log = require('../../services/log');
 | 
					const log = require('../../services/log');
 | 
				
			||||||
const appInfo = require('../../services/app_info');
 | 
					const appInfo = require('../../services/app_info');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -24,7 +24,7 @@ function setupSyncFromServer(req) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function saveSyncSeed(req) {
 | 
					function saveSyncSeed(req) {
 | 
				
			||||||
    const {options, syncVersion} = req.body;
 | 
					    const { options, syncVersion } = req.body;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (appInfo.syncVersion !== syncVersion) {
 | 
					    if (appInfo.syncVersion !== syncVersion) {
 | 
				
			||||||
        const message = `Could not setup sync since local sync protocol version is ${appInfo.syncVersion} while remote is ${syncVersion}. To fix this issue, use same Trilium version on all instances.`;
 | 
					        const message = `Could not setup sync since local sync protocol version is ${appInfo.syncVersion} while remote is ${syncVersion}. To fix this issue, use same Trilium version on all instances.`;
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@
 | 
				
			|||||||
const dateNoteService = require('../../services/date_notes');
 | 
					const dateNoteService = require('../../services/date_notes');
 | 
				
			||||||
const sql = require('../../services/sql');
 | 
					const sql = require('../../services/sql');
 | 
				
			||||||
const cls = require('../../services/cls');
 | 
					const cls = require('../../services/cls');
 | 
				
			||||||
const specialNotesService = require('../../services/special_notes.js');
 | 
					const specialNotesService = require('../../services/special_notes');
 | 
				
			||||||
const becca = require('../../becca/becca');
 | 
					const becca = require('../../becca/becca');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getInboxNote(req) {
 | 
					function getInboxNote(req) {
 | 
				
			||||||
 | 
				
			|||||||
@ -132,7 +132,7 @@ function getChanged(req) {
 | 
				
			|||||||
const partialRequests = {};
 | 
					const partialRequests = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function update(req) {
 | 
					function update(req) {
 | 
				
			||||||
    let {body} = req;
 | 
					    let { body } = req;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const pageCount = parseInt(req.get('pageCount'));
 | 
					    const pageCount = parseInt(req.get('pageCount'));
 | 
				
			||||||
    const pageIndex = parseInt(req.get('pageIndex'));
 | 
					    const pageIndex = parseInt(req.get('pageIndex'));
 | 
				
			||||||
@ -164,7 +164,7 @@ function update(req) {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const {entities, instanceId} = body;
 | 
					    const { entities, instanceId } = body;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sql.transactional(() => syncUpdateService.updateEntities(entities, instanceId));
 | 
					    sql.transactional(() => syncUpdateService.updateEntities(entities, instanceId));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -193,7 +193,7 @@ function queueSector(req) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function checkEntityChanges() {
 | 
					function checkEntityChanges() {
 | 
				
			||||||
    require('../../services/consistency_checks.js').runEntityChangesChecks();
 | 
					    require('../../services/consistency_checks').runEntityChangesChecks();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
 | 
				
			|||||||
@ -41,14 +41,14 @@ const importRoute = require('./api/import.js');
 | 
				
			|||||||
const setupApiRoute = require('./api/setup.js');
 | 
					const setupApiRoute = require('./api/setup.js');
 | 
				
			||||||
const sqlRoute = require('./api/sql');
 | 
					const sqlRoute = require('./api/sql');
 | 
				
			||||||
const databaseRoute = require('./api/database.js');
 | 
					const databaseRoute = require('./api/database.js');
 | 
				
			||||||
const imageRoute = require('./api/image.js');
 | 
					const imageRoute = require('./api/image');
 | 
				
			||||||
const attributesRoute = require('./api/attributes');
 | 
					const attributesRoute = require('./api/attributes');
 | 
				
			||||||
const scriptRoute = require('./api/script.js');
 | 
					const scriptRoute = require('./api/script.js');
 | 
				
			||||||
const senderRoute = require('./api/sender.js');
 | 
					const senderRoute = require('./api/sender.js');
 | 
				
			||||||
const filesRoute = require('./api/files.js');
 | 
					const filesRoute = require('./api/files.js');
 | 
				
			||||||
const searchRoute = require('./api/search');
 | 
					const searchRoute = require('./api/search');
 | 
				
			||||||
const bulkActionRoute = require('./api/bulk_action.js');
 | 
					const bulkActionRoute = require('./api/bulk_action.js');
 | 
				
			||||||
const specialNotesRoute = require('./api/special_notes.js');
 | 
					const specialNotesRoute = require('./api/special_notes');
 | 
				
			||||||
const noteMapRoute = require('./api/note_map.js');
 | 
					const noteMapRoute = require('./api/note_map.js');
 | 
				
			||||||
const clipperRoute = require('./api/clipper.js');
 | 
					const clipperRoute = require('./api/clipper.js');
 | 
				
			||||||
const similarNotesRoute = require('./api/similar_notes.js');
 | 
					const similarNotesRoute = require('./api/similar_notes.js');
 | 
				
			||||||
@ -67,7 +67,7 @@ const etapiAttachmentRoutes = require('../etapi/attachments.js');
 | 
				
			|||||||
const etapiAttributeRoutes = require('../etapi/attributes');
 | 
					const etapiAttributeRoutes = require('../etapi/attributes');
 | 
				
			||||||
const etapiBranchRoutes = require('../etapi/branches.js');
 | 
					const etapiBranchRoutes = require('../etapi/branches.js');
 | 
				
			||||||
const etapiNoteRoutes = require('../etapi/notes.js');
 | 
					const etapiNoteRoutes = require('../etapi/notes.js');
 | 
				
			||||||
const etapiSpecialNoteRoutes = require('../etapi/special_notes.js');
 | 
					const etapiSpecialNoteRoutes = require('../etapi/special_notes');
 | 
				
			||||||
const etapiSpecRoute = require('../etapi/spec.js');
 | 
					const etapiSpecRoute = require('../etapi/spec.js');
 | 
				
			||||||
const etapiBackupRoute = require('../etapi/backup');
 | 
					const etapiBackupRoute = require('../etapi/backup');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -230,7 +230,7 @@ function register(app) {
 | 
				
			|||||||
    apiRoute(GET, '/api/app-info', appInfoRoute.getAppInfo);
 | 
					    apiRoute(GET, '/api/app-info', appInfoRoute.getAppInfo);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // docker health check
 | 
					    // docker health check
 | 
				
			||||||
    route(GET, '/api/health-check', [], () => ({"status": "ok"}), apiResultHandler);
 | 
					    route(GET, '/api/health-check', [], () => ({ "status": "ok" }), apiResultHandler);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // group of the services below are meant to be executed from the outside
 | 
					    // group of the services below are meant to be executed from the outside
 | 
				
			||||||
    route(GET, '/api/setup/status', [], setupApiRoute.getStatus, apiResultHandler);
 | 
					    route(GET, '/api/setup/status', [], setupApiRoute.getStatus, apiResultHandler);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const sqlInit = require('../services/sql_init');
 | 
					const sqlInit = require('../services/sql_init');
 | 
				
			||||||
const setupService = require('../services/setup.js');
 | 
					const setupService = require('../services/setup');
 | 
				
			||||||
const utils = require('../services/utils');
 | 
					const utils = require('../services/utils');
 | 
				
			||||||
const assetPath = require('../services/asset_path');
 | 
					const assetPath = require('../services/asset_path');
 | 
				
			||||||
const appPath = require('../services/app_path');
 | 
					const appPath = require('../services/app_path');
 | 
				
			||||||
@ -10,7 +10,7 @@ function setupPage(req, res) {
 | 
				
			|||||||
    if (sqlInit.isDbInitialized()) {
 | 
					    if (sqlInit.isDbInitialized()) {
 | 
				
			||||||
        if (utils.isElectron()) {
 | 
					        if (utils.isElectron()) {
 | 
				
			||||||
            const windowService = require('../services/window');
 | 
					            const windowService = require('../services/window');
 | 
				
			||||||
            const {app} = require('electron');
 | 
					            const { app } = require('electron');
 | 
				
			||||||
            windowService.createMainWindow(app);
 | 
					            windowService.createMainWindow(app);
 | 
				
			||||||
            windowService.closeSetupWindow();
 | 
					            windowService.closeSetupWindow();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -90,7 +90,7 @@ function getExistingAnonymizedDatabases() {
 | 
				
			|||||||
        }));
 | 
					        }));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					export = {
 | 
				
			||||||
    getFullAnonymizationScript,
 | 
					    getFullAnonymizationScript,
 | 
				
			||||||
    createAnonymizedCopy,
 | 
					    createAnonymizedCopy,
 | 
				
			||||||
    getExistingAnonymizedDatabases
 | 
					    getExistingAnonymizedDatabases
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										17
									
								
								src/services/api-interface.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/services/api-interface.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					import { OptionRow } from "../becca/entities/rows";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Response for /api/setup/status.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface SetupStatusResponse {
 | 
				
			||||||
 | 
					    syncVersion: number;
 | 
				
			||||||
 | 
					    schemaExists: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Response for /api/setup/sync-seed.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface SetupSyncSeedResponse {
 | 
				
			||||||
 | 
					    syncVersion: number;
 | 
				
			||||||
 | 
					    options: OptionRow[]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -15,10 +15,10 @@ const searchService = require('./search/services/search');
 | 
				
			|||||||
const SearchContext = require('./search/search_context');
 | 
					const SearchContext = require('./search/search_context');
 | 
				
			||||||
const becca = require('../becca/becca');
 | 
					const becca = require('../becca/becca');
 | 
				
			||||||
const ws = require('./ws');
 | 
					const ws = require('./ws');
 | 
				
			||||||
const SpacedUpdate = require('./spaced_update.js');
 | 
					const SpacedUpdate = require('./spaced_update');
 | 
				
			||||||
const specialNotesService = require('./special_notes.js');
 | 
					const specialNotesService = require('./special_notes');
 | 
				
			||||||
const branchService = require('./branches');
 | 
					const branchService = require('./branches');
 | 
				
			||||||
const exportService = require('./export/zip.js');
 | 
					const exportService = require('./export/zip');
 | 
				
			||||||
const syncMutex = require('./sync_mutex');
 | 
					const syncMutex = require('./sync_mutex');
 | 
				
			||||||
const backupService = require('./backup');
 | 
					const backupService = require('./backup');
 | 
				
			||||||
const optionsService = require('./options');
 | 
					const optionsService = require('./options');
 | 
				
			||||||
@ -320,7 +320,7 @@ function BackendScriptApi(currentNote, apiParams) {
 | 
				
			|||||||
     * @param {string} [extraOptions.attributes.value] - attribute value
 | 
					     * @param {string} [extraOptions.attributes.value] - attribute value
 | 
				
			||||||
     * @returns {{note: BNote, branch: BBranch}} object contains newly created entities note and branch
 | 
					     * @returns {{note: BNote, branch: BBranch}} object contains newly created entities note and branch
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    this.createNote = (parentNoteId, title, content = "", extraOptions= {}) => {
 | 
					    this.createNote = (parentNoteId, title, content = "", extraOptions = {}) => {
 | 
				
			||||||
        extraOptions.parentNoteId = parentNoteId;
 | 
					        extraOptions.parentNoteId = parentNoteId;
 | 
				
			||||||
        extraOptions.title = title;
 | 
					        extraOptions.title = title;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -340,7 +340,7 @@ function BackendScriptApi(currentNote, apiParams) {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return sql.transactional(() => {
 | 
					        return sql.transactional(() => {
 | 
				
			||||||
            const {note, branch} = noteService.createNewNote(extraOptions);
 | 
					            const { note, branch } = noteService.createNewNote(extraOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            for (const attr of extraOptions.attributes || []) {
 | 
					            for (const attr of extraOptions.attributes || []) {
 | 
				
			||||||
                attributeService.createAttribute({
 | 
					                attributeService.createAttribute({
 | 
				
			||||||
@ -352,7 +352,7 @@ function BackendScriptApi(currentNote, apiParams) {
 | 
				
			|||||||
                });
 | 
					                });
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return {note, branch};
 | 
					            return { note, branch };
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -369,7 +369,7 @@ function BackendScriptApi(currentNote, apiParams) {
 | 
				
			|||||||
    this.log = message => {
 | 
					    this.log = message => {
 | 
				
			||||||
        log.info(message);
 | 
					        log.info(message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const {noteId} = this.startNote;
 | 
					        const { noteId } = this.startNote;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.logMessages[noteId] = this.logMessages[noteId] || [];
 | 
					        this.logMessages[noteId] = this.logMessages[noteId] || [];
 | 
				
			||||||
        this.logSpacedUpdates[noteId] = this.logSpacedUpdates[noteId] || new SpacedUpdate(() => {
 | 
					        this.logSpacedUpdates[noteId] = this.logSpacedUpdates[noteId] || new SpacedUpdate(() => {
 | 
				
			||||||
@ -600,7 +600,7 @@ function BackendScriptApi(currentNote, apiParams) {
 | 
				
			|||||||
            launcherNote.removeLabel('iconClass');
 | 
					            launcherNote.removeLabel('iconClass');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return {note: launcherNote};
 | 
					        return { note: launcherNote };
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
 | 
				
			|||||||
@ -1,33 +1,41 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const sql = require('./sql');
 | 
					import sql = require('./sql');
 | 
				
			||||||
const sqlInit = require('./sql_init');
 | 
					import sqlInit = require('./sql_init');
 | 
				
			||||||
const log = require('./log');
 | 
					import log = require('./log');
 | 
				
			||||||
const ws = require('./ws');
 | 
					import ws = require('./ws');
 | 
				
			||||||
const syncMutexService = require('./sync_mutex');
 | 
					import syncMutexService = require('./sync_mutex');
 | 
				
			||||||
const cls = require('./cls');
 | 
					import cls = require('./cls');
 | 
				
			||||||
const entityChangesService = require('./entity_changes');
 | 
					import entityChangesService = require('./entity_changes');
 | 
				
			||||||
const optionsService = require('./options');
 | 
					import optionsService = require('./options');
 | 
				
			||||||
const BBranch = require('../becca/entities/bbranch');
 | 
					import BBranch = require('../becca/entities/bbranch');
 | 
				
			||||||
const revisionService = require('./revisions');
 | 
					import becca = require('../becca/becca');
 | 
				
			||||||
const becca = require('../becca/becca');
 | 
					import utils = require('../services/utils');
 | 
				
			||||||
const utils = require('../services/utils');
 | 
					import eraseService = require('../services/erase');
 | 
				
			||||||
const eraseService = require('../services/erase');
 | 
					import sanitizeAttributeName = require('./sanitize_attribute_name');
 | 
				
			||||||
const {sanitizeAttributeName} = require('./sanitize_attribute_name');
 | 
					import noteTypesService = require('../services/note_types');
 | 
				
			||||||
const noteTypes = require('../services/note_types').getNoteTypeNames();
 | 
					import { BranchRow } from '../becca/entities/rows';
 | 
				
			||||||
 | 
					import { EntityChange } from './entity_changes_interface';
 | 
				
			||||||
 | 
					const noteTypes = noteTypesService.getNoteTypeNames();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ConsistencyChecks {
 | 
					class ConsistencyChecks {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private autoFix: boolean;
 | 
				
			||||||
 | 
					    private unrecoveredConsistencyErrors: boolean;
 | 
				
			||||||
 | 
					    private fixedIssues: boolean;    
 | 
				
			||||||
 | 
					    private reloadNeeded: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * @param autoFix - automatically fix all encountered problems. False is only for debugging during development (fail fast)
 | 
					     * @param autoFix - automatically fix all encountered problems. False is only for debugging during development (fail fast)
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(autoFix) {
 | 
					    constructor(autoFix: boolean) {
 | 
				
			||||||
        this.autoFix = autoFix;
 | 
					        this.autoFix = autoFix;
 | 
				
			||||||
        this.unrecoveredConsistencyErrors = false;
 | 
					        this.unrecoveredConsistencyErrors = false;
 | 
				
			||||||
        this.fixedIssues = false;
 | 
					        this.fixedIssues = false;
 | 
				
			||||||
        this.reloadNeeded = false;
 | 
					        this.reloadNeeded = false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    findAndFixIssues(query, fixerCb) {
 | 
					    findAndFixIssues(query: string, fixerCb: (res: any) => void) {
 | 
				
			||||||
        const results = sql.getRows(query);
 | 
					        const results = sql.getRows(query);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const res of results) {
 | 
					        for (const res of results) {
 | 
				
			||||||
@ -39,7 +47,7 @@ class ConsistencyChecks {
 | 
				
			|||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    this.unrecoveredConsistencyErrors = true;
 | 
					                    this.unrecoveredConsistencyErrors = true;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            } catch (e) {
 | 
					            } catch (e: any) {
 | 
				
			||||||
                logError(`Fixer failed with ${e.message} ${e.stack}`);
 | 
					                logError(`Fixer failed with ${e.message} ${e.stack}`);
 | 
				
			||||||
                this.unrecoveredConsistencyErrors = true;
 | 
					                this.unrecoveredConsistencyErrors = true;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -49,8 +57,8 @@ class ConsistencyChecks {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    checkTreeCycles() {
 | 
					    checkTreeCycles() {
 | 
				
			||||||
        const childToParents = {};
 | 
					        const childToParents: Record<string, string[]> = {};
 | 
				
			||||||
        const rows = sql.getRows("SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0");
 | 
					        const rows = sql.getRows<BranchRow>("SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const row of rows) {
 | 
					        for (const row of rows) {
 | 
				
			||||||
            const childNoteId = row.noteId;
 | 
					            const childNoteId = row.noteId;
 | 
				
			||||||
@ -61,7 +69,7 @@ class ConsistencyChecks {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /** @returns {boolean} true if cycle was found and we should try again */
 | 
					        /** @returns {boolean} true if cycle was found and we should try again */
 | 
				
			||||||
        const checkTreeCycle = (noteId, path) => {
 | 
					        const checkTreeCycle = (noteId: string, path: string[]) => {
 | 
				
			||||||
            if (noteId === 'root') {
 | 
					            if (noteId === 'root') {
 | 
				
			||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -70,8 +78,10 @@ class ConsistencyChecks {
 | 
				
			|||||||
                if (path.includes(parentNoteId)) {
 | 
					                if (path.includes(parentNoteId)) {
 | 
				
			||||||
                    if (this.autoFix) {
 | 
					                    if (this.autoFix) {
 | 
				
			||||||
                        const branch = becca.getBranchFromChildAndParent(noteId, parentNoteId);
 | 
					                        const branch = becca.getBranchFromChildAndParent(noteId, parentNoteId);
 | 
				
			||||||
                        branch.markAsDeleted('cycle-autofix');
 | 
					                        if (branch) {
 | 
				
			||||||
                        logFix(`Branch '${branch.branchId}' between child '${noteId}' and parent '${parentNoteId}' has been deleted since it was causing a tree cycle.`);
 | 
					                            branch.markAsDeleted('cycle-autofix');
 | 
				
			||||||
 | 
					                            logFix(`Branch '${branch.branchId}' between child '${noteId}' and parent '${parentNoteId}' has been deleted since it was causing a tree cycle.`);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        return true;
 | 
					                        return true;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
@ -133,6 +143,9 @@ class ConsistencyChecks {
 | 
				
			|||||||
            ({branchId, noteId}) => {
 | 
					            ({branchId, noteId}) => {
 | 
				
			||||||
                if (this.autoFix) {
 | 
					                if (this.autoFix) {
 | 
				
			||||||
                    const branch = becca.getBranch(branchId);
 | 
					                    const branch = becca.getBranch(branchId);
 | 
				
			||||||
 | 
					                    if (!branch) {
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                    branch.markAsDeleted();
 | 
					                    branch.markAsDeleted();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    this.reloadNeeded = true;
 | 
					                    this.reloadNeeded = true;
 | 
				
			||||||
@ -154,12 +167,21 @@ class ConsistencyChecks {
 | 
				
			|||||||
                if (this.autoFix) {
 | 
					                if (this.autoFix) {
 | 
				
			||||||
                    // Delete the old branch and recreate it with root as parent.
 | 
					                    // Delete the old branch and recreate it with root as parent.
 | 
				
			||||||
                    const oldBranch = becca.getBranch(branchId);
 | 
					                    const oldBranch = becca.getBranch(branchId);
 | 
				
			||||||
 | 
					                    if (!oldBranch) {
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    const noteId = oldBranch.noteId;
 | 
					                    const noteId = oldBranch.noteId;
 | 
				
			||||||
                    oldBranch.markAsDeleted("missing-parent");
 | 
					                    oldBranch.markAsDeleted("missing-parent");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    let message = `Branch '${branchId}' was missing parent note '${parentNoteId}', so it was deleted. `;
 | 
					                    let message = `Branch '${branchId}' was missing parent note '${parentNoteId}', so it was deleted. `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (becca.getNote(noteId).getParentBranches().length === 0) {
 | 
					                    const note = becca.getNote(noteId);
 | 
				
			||||||
 | 
					                    if (!note) {
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (note.getParentBranches().length === 0) {
 | 
				
			||||||
                        const newBranch = new BBranch({
 | 
					                        const newBranch = new BBranch({
 | 
				
			||||||
                            parentNoteId: 'root',
 | 
					                            parentNoteId: 'root',
 | 
				
			||||||
                            noteId: noteId,
 | 
					                            noteId: noteId,
 | 
				
			||||||
@ -188,6 +210,9 @@ class ConsistencyChecks {
 | 
				
			|||||||
            ({attributeId, noteId}) => {
 | 
					            ({attributeId, noteId}) => {
 | 
				
			||||||
                if (this.autoFix) {
 | 
					                if (this.autoFix) {
 | 
				
			||||||
                    const attribute = becca.getAttribute(attributeId);
 | 
					                    const attribute = becca.getAttribute(attributeId);
 | 
				
			||||||
 | 
					                    if (!attribute) {
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                    attribute.markAsDeleted();
 | 
					                    attribute.markAsDeleted();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    this.reloadNeeded = true;
 | 
					                    this.reloadNeeded = true;
 | 
				
			||||||
@ -208,6 +233,9 @@ class ConsistencyChecks {
 | 
				
			|||||||
            ({attributeId, noteId}) => {
 | 
					            ({attributeId, noteId}) => {
 | 
				
			||||||
                if (this.autoFix) {
 | 
					                if (this.autoFix) {
 | 
				
			||||||
                    const attribute = becca.getAttribute(attributeId);
 | 
					                    const attribute = becca.getAttribute(attributeId);
 | 
				
			||||||
 | 
					                    if (!attribute) {
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                    attribute.markAsDeleted();
 | 
					                    attribute.markAsDeleted();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    this.reloadNeeded = true;
 | 
					                    this.reloadNeeded = true;
 | 
				
			||||||
@ -230,6 +258,9 @@ class ConsistencyChecks {
 | 
				
			|||||||
            ({attachmentId, ownerId}) => {
 | 
					            ({attachmentId, ownerId}) => {
 | 
				
			||||||
                if (this.autoFix) {
 | 
					                if (this.autoFix) {
 | 
				
			||||||
                    const attachment = becca.getAttachment(attachmentId);
 | 
					                    const attachment = becca.getAttachment(attachmentId);
 | 
				
			||||||
 | 
					                    if (!attachment) {
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                    attachment.markAsDeleted();
 | 
					                    attachment.markAsDeleted();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    this.reloadNeeded = false;
 | 
					                    this.reloadNeeded = false;
 | 
				
			||||||
@ -258,6 +289,7 @@ class ConsistencyChecks {
 | 
				
			|||||||
            ({branchId, noteId}) => {
 | 
					            ({branchId, noteId}) => {
 | 
				
			||||||
                if (this.autoFix) {
 | 
					                if (this.autoFix) {
 | 
				
			||||||
                    const branch = becca.getBranch(branchId);
 | 
					                    const branch = becca.getBranch(branchId);
 | 
				
			||||||
 | 
					                    if (!branch) return;
 | 
				
			||||||
                    branch.markAsDeleted();
 | 
					                    branch.markAsDeleted();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    this.reloadNeeded = true;
 | 
					                    this.reloadNeeded = true;
 | 
				
			||||||
@ -278,6 +310,9 @@ class ConsistencyChecks {
 | 
				
			|||||||
        `, ({branchId, parentNoteId}) => {
 | 
					        `, ({branchId, parentNoteId}) => {
 | 
				
			||||||
            if (this.autoFix) {
 | 
					            if (this.autoFix) {
 | 
				
			||||||
                const branch = becca.getBranch(branchId);
 | 
					                const branch = becca.getBranch(branchId);
 | 
				
			||||||
 | 
					                if (!branch) {
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
                branch.markAsDeleted();
 | 
					                branch.markAsDeleted();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                this.reloadNeeded = true;
 | 
					                this.reloadNeeded = true;
 | 
				
			||||||
@ -321,7 +356,7 @@ class ConsistencyChecks {
 | 
				
			|||||||
                    HAVING COUNT(1) > 1`,
 | 
					                    HAVING COUNT(1) > 1`,
 | 
				
			||||||
            ({noteId, parentNoteId}) => {
 | 
					            ({noteId, parentNoteId}) => {
 | 
				
			||||||
                if (this.autoFix) {
 | 
					                if (this.autoFix) {
 | 
				
			||||||
                    const branchIds = sql.getColumn(
 | 
					                    const branchIds = sql.getColumn<string>(
 | 
				
			||||||
                            `SELECT branchId
 | 
					                            `SELECT branchId
 | 
				
			||||||
                             FROM branches
 | 
					                             FROM branches
 | 
				
			||||||
                             WHERE noteId = ?
 | 
					                             WHERE noteId = ?
 | 
				
			||||||
@ -333,9 +368,17 @@ class ConsistencyChecks {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                    // it's not necessarily "original" branch, it's just the only one which will survive
 | 
					                    // it's not necessarily "original" branch, it's just the only one which will survive
 | 
				
			||||||
                    const origBranch = branches[0];
 | 
					                    const origBranch = branches[0];
 | 
				
			||||||
 | 
					                    if (!origBranch) {
 | 
				
			||||||
 | 
					                        logError(`Unable to find original branch.`);
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // delete all but the first branch
 | 
					                    // delete all but the first branch
 | 
				
			||||||
                    for (const branch of branches.slice(1)) {
 | 
					                    for (const branch of branches.slice(1)) {
 | 
				
			||||||
 | 
					                        if (!branch) {
 | 
				
			||||||
 | 
					                            continue;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        branch.markAsDeleted();
 | 
					                        branch.markAsDeleted();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        logFix(`Removing branch '${branch.branchId}' since it's a parent-child duplicate of branch '${origBranch.branchId}'`);
 | 
					                        logFix(`Removing branch '${branch.branchId}' since it's a parent-child duplicate of branch '${origBranch.branchId}'`);
 | 
				
			||||||
@ -357,6 +400,7 @@ class ConsistencyChecks {
 | 
				
			|||||||
            ({attachmentId, noteId}) => {
 | 
					            ({attachmentId, noteId}) => {
 | 
				
			||||||
                if (this.autoFix) {
 | 
					                if (this.autoFix) {
 | 
				
			||||||
                    const attachment = becca.getAttachment(attachmentId);
 | 
					                    const attachment = becca.getAttachment(attachmentId);
 | 
				
			||||||
 | 
					                    if (!attachment) return;
 | 
				
			||||||
                    attachment.markAsDeleted();
 | 
					                    attachment.markAsDeleted();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    this.reloadNeeded = false;
 | 
					                    this.reloadNeeded = false;
 | 
				
			||||||
@ -379,6 +423,7 @@ class ConsistencyChecks {
 | 
				
			|||||||
            ({noteId, type}) => {
 | 
					            ({noteId, type}) => {
 | 
				
			||||||
                if (this.autoFix) {
 | 
					                if (this.autoFix) {
 | 
				
			||||||
                    const note = becca.getNote(noteId);
 | 
					                    const note = becca.getNote(noteId);
 | 
				
			||||||
 | 
					                    if (!note) return;
 | 
				
			||||||
                    note.type = 'file'; // file is a safe option to recover notes if the type is not known
 | 
					                    note.type = 'file'; // file is a safe option to recover notes if the type is not known
 | 
				
			||||||
                    note.save();
 | 
					                    note.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -404,6 +449,10 @@ class ConsistencyChecks {
 | 
				
			|||||||
                    const fakeDate = "2000-01-01 00:00:00Z";
 | 
					                    const fakeDate = "2000-01-01 00:00:00Z";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    const blankContent = getBlankContent(isProtected, type, mime);
 | 
					                    const blankContent = getBlankContent(isProtected, type, mime);
 | 
				
			||||||
 | 
					                    if (!blankContent) {
 | 
				
			||||||
 | 
					                        logError(`Unable to recover note ${noteId} since it's content could not be retrieved (might be protected note).`);
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                    const blobId = utils.hashedBlobId(blankContent);
 | 
					                    const blobId = utils.hashedBlobId(blankContent);
 | 
				
			||||||
                    const blobAlreadyExists = !!sql.getValue("SELECT 1 FROM blobs WHERE blobId = ?", [blobId]);
 | 
					                    const blobAlreadyExists = !!sql.getValue("SELECT 1 FROM blobs WHERE blobId = ?", [blobId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -452,7 +501,11 @@ class ConsistencyChecks {
 | 
				
			|||||||
                    if (this.autoFix) {
 | 
					                    if (this.autoFix) {
 | 
				
			||||||
                        const note = becca.getNote(noteId);
 | 
					                        const note = becca.getNote(noteId);
 | 
				
			||||||
                        const blankContent = getBlankContent(false, type, mime);
 | 
					                        const blankContent = getBlankContent(false, type, mime);
 | 
				
			||||||
                        note.setContent(blankContent);
 | 
					                        if (!note) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if (blankContent) {
 | 
				
			||||||
 | 
					                            note.setContent(blankContent);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        this.reloadNeeded = true;
 | 
					                        this.reloadNeeded = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -506,7 +559,7 @@ class ConsistencyChecks {
 | 
				
			|||||||
                      AND branches.isDeleted = 0`,
 | 
					                      AND branches.isDeleted = 0`,
 | 
				
			||||||
            ({parentNoteId}) => {
 | 
					            ({parentNoteId}) => {
 | 
				
			||||||
                if (this.autoFix) {
 | 
					                if (this.autoFix) {
 | 
				
			||||||
                    const branchIds = sql.getColumn(`
 | 
					                    const branchIds = sql.getColumn<string>(`
 | 
				
			||||||
                        SELECT branchId
 | 
					                        SELECT branchId
 | 
				
			||||||
                        FROM branches
 | 
					                        FROM branches
 | 
				
			||||||
                        WHERE isDeleted = 0
 | 
					                        WHERE isDeleted = 0
 | 
				
			||||||
@ -515,6 +568,8 @@ class ConsistencyChecks {
 | 
				
			|||||||
                    const branches = branchIds.map(branchId => becca.getBranch(branchId));
 | 
					                    const branches = branchIds.map(branchId => becca.getBranch(branchId));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    for (const branch of branches) {
 | 
					                    for (const branch of branches) {
 | 
				
			||||||
 | 
					                        if (!branch) continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        // delete the old wrong branch
 | 
					                        // delete the old wrong branch
 | 
				
			||||||
                        branch.markAsDeleted("parent-is-search");
 | 
					                        branch.markAsDeleted("parent-is-search");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -543,6 +598,7 @@ class ConsistencyChecks {
 | 
				
			|||||||
            ({attributeId}) => {
 | 
					            ({attributeId}) => {
 | 
				
			||||||
                if (this.autoFix) {
 | 
					                if (this.autoFix) {
 | 
				
			||||||
                    const relation = becca.getAttribute(attributeId);
 | 
					                    const relation = becca.getAttribute(attributeId);
 | 
				
			||||||
 | 
					                    if (!relation) return;
 | 
				
			||||||
                    relation.markAsDeleted();
 | 
					                    relation.markAsDeleted();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    this.reloadNeeded = true;
 | 
					                    this.reloadNeeded = true;
 | 
				
			||||||
@ -563,6 +619,7 @@ class ConsistencyChecks {
 | 
				
			|||||||
            ({attributeId, type}) => {
 | 
					            ({attributeId, type}) => {
 | 
				
			||||||
                if (this.autoFix) {
 | 
					                if (this.autoFix) {
 | 
				
			||||||
                    const attribute = becca.getAttribute(attributeId);
 | 
					                    const attribute = becca.getAttribute(attributeId);
 | 
				
			||||||
 | 
					                    if (!attribute) return;
 | 
				
			||||||
                    attribute.type = 'label';
 | 
					                    attribute.type = 'label';
 | 
				
			||||||
                    attribute.save();
 | 
					                    attribute.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -584,6 +641,7 @@ class ConsistencyChecks {
 | 
				
			|||||||
            ({attributeId, noteId}) => {
 | 
					            ({attributeId, noteId}) => {
 | 
				
			||||||
                if (this.autoFix) {
 | 
					                if (this.autoFix) {
 | 
				
			||||||
                    const attribute = becca.getAttribute(attributeId);
 | 
					                    const attribute = becca.getAttribute(attributeId);
 | 
				
			||||||
 | 
					                    if (!attribute) return;
 | 
				
			||||||
                    attribute.markAsDeleted();
 | 
					                    attribute.markAsDeleted();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    this.reloadNeeded = true;
 | 
					                    this.reloadNeeded = true;
 | 
				
			||||||
@ -605,6 +663,7 @@ class ConsistencyChecks {
 | 
				
			|||||||
            ({attributeId, targetNoteId}) => {
 | 
					            ({attributeId, targetNoteId}) => {
 | 
				
			||||||
                if (this.autoFix) {
 | 
					                if (this.autoFix) {
 | 
				
			||||||
                    const attribute = becca.getAttribute(attributeId);
 | 
					                    const attribute = becca.getAttribute(attributeId);
 | 
				
			||||||
 | 
					                    if (!attribute) return;
 | 
				
			||||||
                    attribute.markAsDeleted();
 | 
					                    attribute.markAsDeleted();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    this.reloadNeeded = true;
 | 
					                    this.reloadNeeded = true;
 | 
				
			||||||
@ -616,14 +675,14 @@ class ConsistencyChecks {
 | 
				
			|||||||
            });
 | 
					            });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    runEntityChangeChecks(entityName, key) {
 | 
					    runEntityChangeChecks(entityName: string, key: string) {
 | 
				
			||||||
        this.findAndFixIssues(`
 | 
					        this.findAndFixIssues(`
 | 
				
			||||||
            SELECT ${key} as entityId
 | 
					            SELECT ${key} as entityId
 | 
				
			||||||
            FROM ${entityName}
 | 
					            FROM ${entityName}
 | 
				
			||||||
            LEFT JOIN entity_changes ec ON ec.entityName = '${entityName}' AND ec.entityId = ${entityName}.${key} 
 | 
					            LEFT JOIN entity_changes ec ON ec.entityName = '${entityName}' AND ec.entityId = ${entityName}.${key} 
 | 
				
			||||||
            WHERE ec.id IS NULL`,
 | 
					            WHERE ec.id IS NULL`,
 | 
				
			||||||
            ({entityId}) => {
 | 
					            ({entityId}) => {
 | 
				
			||||||
                const entityRow = sql.getRow(`SELECT * FROM ${entityName} WHERE ${key} = ?`, [entityId]);
 | 
					                const entityRow = sql.getRow<EntityChange>(`SELECT * FROM ${entityName} WHERE ${key} = ?`, [entityId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (this.autoFix) {
 | 
					                if (this.autoFix) {
 | 
				
			||||||
                    entityChangesService.putEntityChange({
 | 
					                    entityChangesService.putEntityChange({
 | 
				
			||||||
@ -691,10 +750,10 @@ class ConsistencyChecks {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    findWronglyNamedAttributes() {
 | 
					    findWronglyNamedAttributes() {
 | 
				
			||||||
        const attrNames = sql.getColumn(`SELECT DISTINCT name FROM attributes`);
 | 
					        const attrNames = sql.getColumn<string>(`SELECT DISTINCT name FROM attributes`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const origName of attrNames) {
 | 
					        for (const origName of attrNames) {
 | 
				
			||||||
            const fixedName = sanitizeAttributeName(origName);
 | 
					            const fixedName = sanitizeAttributeName.sanitizeAttributeName(origName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (fixedName !== origName) {
 | 
					            if (fixedName !== origName) {
 | 
				
			||||||
                if (this.autoFix) {
 | 
					                if (this.autoFix) {
 | 
				
			||||||
@ -721,7 +780,7 @@ class ConsistencyChecks {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    findSyncIssues() {
 | 
					    findSyncIssues() {
 | 
				
			||||||
        const lastSyncedPush = parseInt(sql.getValue("SELECT value FROM options WHERE name = 'lastSyncedPush'"));
 | 
					        const lastSyncedPush = parseInt(sql.getValue("SELECT value FROM options WHERE name = 'lastSyncedPush'"));
 | 
				
			||||||
        const maxEntityChangeId = sql.getValue("SELECT MAX(id) FROM entity_changes");
 | 
					        const maxEntityChangeId = sql.getValue<number>("SELECT MAX(id) FROM entity_changes");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (lastSyncedPush > maxEntityChangeId) {
 | 
					        if (lastSyncedPush > maxEntityChangeId) {
 | 
				
			||||||
            if (this.autoFix) {
 | 
					            if (this.autoFix) {
 | 
				
			||||||
@ -773,8 +832,8 @@ class ConsistencyChecks {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    runDbDiagnostics() {
 | 
					    runDbDiagnostics() {
 | 
				
			||||||
        function getTableRowCount(tableName) {
 | 
					        function getTableRowCount(tableName: string) {
 | 
				
			||||||
            const count = sql.getValue(`SELECT COUNT(1) FROM ${tableName}`);
 | 
					            const count = sql.getValue<number>(`SELECT COUNT(1) FROM ${tableName}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return `${tableName}: ${count}`;
 | 
					            return `${tableName}: ${count}`;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -810,7 +869,7 @@ class ConsistencyChecks {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getBlankContent(isProtected, type, mime) {
 | 
					function getBlankContent(isProtected: boolean, type: string, mime: string) {
 | 
				
			||||||
    if (isProtected) {
 | 
					    if (isProtected) {
 | 
				
			||||||
        return null; // this is wrong for protected non-erased notes, but we cannot create a valid value without a password
 | 
					        return null; // this is wrong for protected non-erased notes, but we cannot create a valid value without a password
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -822,11 +881,11 @@ function getBlankContent(isProtected, type, mime) {
 | 
				
			|||||||
    return ''; // empty string might be a wrong choice for some note types, but it's the best guess
 | 
					    return ''; // empty string might be a wrong choice for some note types, but it's the best guess
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function logFix(message) {
 | 
					function logFix(message: string) {
 | 
				
			||||||
    log.info(`Consistency issue fixed: ${message}`);
 | 
					    log.info(`Consistency issue fixed: ${message}`);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function logError(message) {
 | 
					function logError(message: string) {
 | 
				
			||||||
    log.info(`Consistency error: ${message}`);
 | 
					    log.info(`Consistency error: ${message}`);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -837,7 +896,7 @@ function runPeriodicChecks() {
 | 
				
			|||||||
    consistencyChecks.runChecks();
 | 
					    consistencyChecks.runChecks();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function runOnDemandChecks(autoFix) {
 | 
					async function runOnDemandChecks(autoFix: boolean) {
 | 
				
			||||||
    const consistencyChecks = new ConsistencyChecks(autoFix);
 | 
					    const consistencyChecks = new ConsistencyChecks(autoFix);
 | 
				
			||||||
    await consistencyChecks.runChecks();
 | 
					    await consistencyChecks.runChecks();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -7,6 +7,8 @@ export interface EntityChange {
 | 
				
			|||||||
	positions?: Record<string, number>;
 | 
						positions?: Record<string, number>;
 | 
				
			||||||
	hash: string;
 | 
						hash: string;
 | 
				
			||||||
	utcDateChanged?: string;
 | 
						utcDateChanged?: string;
 | 
				
			||||||
 | 
						utcDateModified?: string;
 | 
				
			||||||
 | 
						utcDateCreated?: string;
 | 
				
			||||||
	isSynced: boolean | 1 | 0;
 | 
						isSynced: boolean | 1 | 0;
 | 
				
			||||||
	isErased: boolean | 1 | 0;
 | 
						isErased: boolean | 1 | 0;
 | 
				
			||||||
	componentId?: string | null;
 | 
						componentId?: string | null;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,11 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TurndownService = require('turndown');
 | 
					import TurndownService = require('turndown');
 | 
				
			||||||
const turndownPluginGfm = require('joplin-turndown-plugin-gfm');
 | 
					import turndownPluginGfm = require('joplin-turndown-plugin-gfm');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let instance = null;
 | 
					let instance: TurndownService | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function toMarkdown(content) {
 | 
					function toMarkdown(content: string) {
 | 
				
			||||||
    if (instance === null) {
 | 
					    if (instance === null) {
 | 
				
			||||||
        instance = new TurndownService({ codeBlockStyle: 'fenced' });
 | 
					        instance = new TurndownService({ codeBlockStyle: 'fenced' });
 | 
				
			||||||
        instance.use(turndownPluginGfm.gfm);
 | 
					        instance.use(turndownPluginGfm.gfm);
 | 
				
			||||||
@ -14,6 +14,6 @@ function toMarkdown(content) {
 | 
				
			|||||||
    return instance.turndown(content);
 | 
					    return instance.turndown(content);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					export = {
 | 
				
			||||||
    toMarkdown
 | 
					    toMarkdown
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -1,9 +1,12 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const utils = require('../utils');
 | 
					import utils = require('../utils');
 | 
				
			||||||
const becca = require('../../becca/becca');
 | 
					import becca = require('../../becca/becca');
 | 
				
			||||||
 | 
					import TaskContext = require('../task_context');
 | 
				
			||||||
 | 
					import BBranch = require('../../becca/entities/bbranch');
 | 
				
			||||||
 | 
					import { Response } from 'express';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function exportToOpml(taskContext, branch, version, res) {
 | 
					function exportToOpml(taskContext: TaskContext, branch: BBranch, version: string, res: Response) {
 | 
				
			||||||
    if (!['1.0', '2.0'].includes(version)) {
 | 
					    if (!['1.0', '2.0'].includes(version)) {
 | 
				
			||||||
        throw new Error(`Unrecognized OPML version ${version}`);
 | 
					        throw new Error(`Unrecognized OPML version ${version}`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -12,9 +15,12 @@ function exportToOpml(taskContext, branch, version, res) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const note = branch.getNote();
 | 
					    const note = branch.getNote();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function exportNoteInner(branchId) {
 | 
					    function exportNoteInner(branchId: string) {
 | 
				
			||||||
        const branch = becca.getBranch(branchId);
 | 
					        const branch = becca.getBranch(branchId);
 | 
				
			||||||
 | 
					        if (!branch) { throw new Error("Unable to find branch."); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const note = branch.getNote();
 | 
					        const note = branch.getNote();
 | 
				
			||||||
 | 
					        if (!note) { throw new Error("Unable to find note."); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (note.hasOwnedLabel('excludeFromExport')) {
 | 
					        if (note.hasOwnedLabel('excludeFromExport')) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
@ -24,13 +30,13 @@ function exportToOpml(taskContext, branch, version, res) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if (opmlVersion === 1) {
 | 
					        if (opmlVersion === 1) {
 | 
				
			||||||
            const preparedTitle = escapeXmlAttribute(title);
 | 
					            const preparedTitle = escapeXmlAttribute(title);
 | 
				
			||||||
            const preparedContent = note.hasStringContent() ? prepareText(note.getContent()) : '';
 | 
					            const preparedContent = note.hasStringContent() ? prepareText(note.getContent() as string) : '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            res.write(`<outline title="${preparedTitle}" text="${preparedContent}">\n`);
 | 
					            res.write(`<outline title="${preparedTitle}" text="${preparedContent}">\n`);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else if (opmlVersion === 2) {
 | 
					        else if (opmlVersion === 2) {
 | 
				
			||||||
            const preparedTitle = escapeXmlAttribute(title);
 | 
					            const preparedTitle = escapeXmlAttribute(title);
 | 
				
			||||||
            const preparedContent = note.hasStringContent() ? escapeXmlAttribute(note.getContent()) : '';
 | 
					            const preparedContent = note.hasStringContent() ? escapeXmlAttribute(note.getContent() as string) : '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            res.write(`<outline text="${preparedTitle}" _note="${preparedContent}">\n`);
 | 
					            res.write(`<outline text="${preparedTitle}" _note="${preparedContent}">\n`);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -41,7 +47,9 @@ function exportToOpml(taskContext, branch, version, res) {
 | 
				
			|||||||
        taskContext.increaseProgressCount();
 | 
					        taskContext.increaseProgressCount();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const child of note.getChildBranches()) {
 | 
					        for (const child of note.getChildBranches()) {
 | 
				
			||||||
            exportNoteInner(child.branchId);
 | 
					            if (child?.branchId) {
 | 
				
			||||||
 | 
					                exportNoteInner(child.branchId);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        res.write('</outline>');
 | 
					        res.write('</outline>');
 | 
				
			||||||
@ -60,7 +68,9 @@ function exportToOpml(taskContext, branch, version, res) {
 | 
				
			|||||||
</head>
 | 
					</head>
 | 
				
			||||||
<body>`);
 | 
					<body>`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    exportNoteInner(branch.branchId);
 | 
					    if (branch.branchId) {
 | 
				
			||||||
 | 
					        exportNoteInner(branch.branchId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.write(`</body>
 | 
					    res.write(`</body>
 | 
				
			||||||
</opml>`);
 | 
					</opml>`);
 | 
				
			||||||
@ -69,7 +79,7 @@ function exportToOpml(taskContext, branch, version, res) {
 | 
				
			|||||||
    taskContext.taskSucceeded();
 | 
					    taskContext.taskSucceeded();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function prepareText(text) {
 | 
					function prepareText(text: string) {
 | 
				
			||||||
    const newLines = text.replace(/(<p[^>]*>|<br\s*\/?>)/g, '\n')
 | 
					    const newLines = text.replace(/(<p[^>]*>|<br\s*\/?>)/g, '\n')
 | 
				
			||||||
        .replace(/ /g, ' '); // nbsp isn't in XML standard (only HTML)
 | 
					        .replace(/ /g, ' '); // nbsp isn't in XML standard (only HTML)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -80,7 +90,7 @@ function prepareText(text) {
 | 
				
			|||||||
    return escaped.replace(/\n/g, '
');
 | 
					    return escaped.replace(/\n/g, '
');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function escapeXmlAttribute(text) {
 | 
					function escapeXmlAttribute(text: string) {
 | 
				
			||||||
    return text.replace(/&/g, '&')
 | 
					    return text.replace(/&/g, '&')
 | 
				
			||||||
        .replace(/</g, '<')
 | 
					        .replace(/</g, '<')
 | 
				
			||||||
        .replace(/>/g, '>')
 | 
					        .replace(/>/g, '>')
 | 
				
			||||||
@ -88,6 +98,6 @@ function escapeXmlAttribute(text) {
 | 
				
			|||||||
        .replace(/'/g, ''');
 | 
					        .replace(/'/g, ''');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					export = {
 | 
				
			||||||
    exportToOpml
 | 
					    exportToOpml
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -1,12 +1,15 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mimeTypes = require('mime-types');
 | 
					import mimeTypes = require('mime-types');
 | 
				
			||||||
const html = require('html');
 | 
					import html = require('html');
 | 
				
			||||||
const utils = require('../utils');
 | 
					import utils = require('../utils');
 | 
				
			||||||
const mdService = require('./md.js');
 | 
					import mdService = require('./md');
 | 
				
			||||||
const becca = require('../../becca/becca');
 | 
					import becca = require('../../becca/becca');
 | 
				
			||||||
 | 
					import TaskContext = require('../task_context');
 | 
				
			||||||
 | 
					import BBranch = require('../../becca/entities/bbranch');
 | 
				
			||||||
 | 
					import { Response } from 'express';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function exportSingleNote(taskContext, branch, format, res) {
 | 
					function exportSingleNote(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown", res: Response) {
 | 
				
			||||||
    const note = branch.getNote();
 | 
					    const note = branch.getNote();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (note.type === 'image' || note.type === 'file') {
 | 
					    if (note.type === 'image' || note.type === 'file') {
 | 
				
			||||||
@ -20,6 +23,9 @@ function exportSingleNote(taskContext, branch, format, res) {
 | 
				
			|||||||
    let payload, extension, mime;
 | 
					    let payload, extension, mime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let content = note.getContent();
 | 
					    let content = note.getContent();
 | 
				
			||||||
 | 
					    if (typeof content !== "string") {
 | 
				
			||||||
 | 
					        throw new Error("Unsupported content type for export.");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (note.type === 'text') {
 | 
					    if (note.type === 'text') {
 | 
				
			||||||
        if (format === 'html') {
 | 
					        if (format === 'html') {
 | 
				
			||||||
@ -64,7 +70,7 @@ function exportSingleNote(taskContext, branch, format, res) {
 | 
				
			|||||||
    taskContext.taskSucceeded();
 | 
					    taskContext.taskSucceeded();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function inlineAttachments(content) {
 | 
					function inlineAttachments(content: string) {
 | 
				
			||||||
    content = content.replace(/src="[^"]*api\/images\/([a-zA-Z0-9_]+)\/?[^"]+"/g, (match, noteId) => {
 | 
					    content = content.replace(/src="[^"]*api\/images\/([a-zA-Z0-9_]+)\/?[^"]+"/g, (match, noteId) => {
 | 
				
			||||||
        const note = becca.getNote(noteId);
 | 
					        const note = becca.getNote(noteId);
 | 
				
			||||||
        if (!note || !note.mime.startsWith('image/')) {
 | 
					        if (!note || !note.mime.startsWith('image/')) {
 | 
				
			||||||
@ -119,6 +125,6 @@ function inlineAttachments(content) {
 | 
				
			|||||||
    return content;
 | 
					    return content;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					export = {
 | 
				
			||||||
    exportSingleNote
 | 
					    exportSingleNote
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -1,33 +1,28 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const html = require('html');
 | 
					import html = require('html');
 | 
				
			||||||
const dateUtils = require('../date_utils');
 | 
					import dateUtils = require('../date_utils');
 | 
				
			||||||
const path = require('path');
 | 
					import path = require('path');
 | 
				
			||||||
const mimeTypes = require('mime-types');
 | 
					import mimeTypes = require('mime-types');
 | 
				
			||||||
const mdService = require('./md.js');
 | 
					import mdService = require('./md');
 | 
				
			||||||
const packageInfo = require('../../../package.json');
 | 
					import packageInfo = require('../../../package.json');
 | 
				
			||||||
const utils = require('../utils');
 | 
					import utils = require('../utils');
 | 
				
			||||||
const protectedSessionService = require('../protected_session');
 | 
					import protectedSessionService = require('../protected_session');
 | 
				
			||||||
const sanitize = require("sanitize-filename");
 | 
					import sanitize = require("sanitize-filename");
 | 
				
			||||||
const fs = require("fs");
 | 
					import fs = require("fs");
 | 
				
			||||||
const becca = require('../../becca/becca');
 | 
					import becca = require('../../becca/becca');
 | 
				
			||||||
const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
 | 
					const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
 | 
				
			||||||
const archiver = require('archiver');
 | 
					import archiver = require('archiver');
 | 
				
			||||||
const log = require('../log');
 | 
					import log = require('../log');
 | 
				
			||||||
const TaskContext = require('../task_context');
 | 
					import TaskContext = require('../task_context');
 | 
				
			||||||
const ValidationError = require('../../errors/validation_error');
 | 
					import ValidationError = require('../../errors/validation_error');
 | 
				
			||||||
const NoteMeta = require('../meta/note_meta');
 | 
					import NoteMeta = require('../meta/note_meta');
 | 
				
			||||||
const AttachmentMeta = require('../meta/attachment_meta');
 | 
					import AttachmentMeta = require('../meta/attachment_meta');
 | 
				
			||||||
const AttributeMeta = require('../meta/attribute_meta');
 | 
					import AttributeMeta = require('../meta/attribute_meta');
 | 
				
			||||||
 | 
					import BBranch = require('../../becca/entities/bbranch');
 | 
				
			||||||
 | 
					import { Response } from 'express';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown", res: Response | fs.WriteStream, setHeaders = true) {
 | 
				
			||||||
 * @param {TaskContext} taskContext
 | 
					 | 
				
			||||||
 * @param {BBranch} branch
 | 
					 | 
				
			||||||
 * @param {string} format - 'html' or 'markdown'
 | 
					 | 
				
			||||||
 * @param {object} res - express response
 | 
					 | 
				
			||||||
 * @param {boolean} setHeaders
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
async function exportToZip(taskContext, branch, format, res, setHeaders = true) {
 | 
					 | 
				
			||||||
    if (!['html', 'markdown'].includes(format)) {
 | 
					    if (!['html', 'markdown'].includes(format)) {
 | 
				
			||||||
        throw new ValidationError(`Only 'html' and 'markdown' allowed as export format, '${format}' given`);
 | 
					        throw new ValidationError(`Only 'html' and 'markdown' allowed as export format, '${format}' given`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -36,15 +31,9 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
 | 
				
			|||||||
        zlib: { level: 9 } // Sets the compression level.
 | 
					        zlib: { level: 9 } // Sets the compression level.
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** @type {Object.<string, NoteMeta>} */
 | 
					    const noteIdToMeta: Record<string, NoteMeta> = {};
 | 
				
			||||||
    const noteIdToMeta = {};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    function getUniqueFilename(existingFileNames: Record<string, number>, fileName: string) {
 | 
				
			||||||
     * @param {Object.<string, int>} existingFileNames
 | 
					 | 
				
			||||||
     * @param {string} fileName
 | 
					 | 
				
			||||||
     * @returns {string}
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    function getUniqueFilename(existingFileNames, fileName) {
 | 
					 | 
				
			||||||
        const lcFileName = fileName.toLowerCase();
 | 
					        const lcFileName = fileName.toLowerCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (lcFileName in existingFileNames) {
 | 
					        if (lcFileName in existingFileNames) {
 | 
				
			||||||
@ -67,14 +56,7 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    function getDataFileName(type: string | null, mime: string, baseFileName: string, existingFileNames: Record<string, number>): string {
 | 
				
			||||||
     * @param {string|null} type
 | 
					 | 
				
			||||||
     * @param {string} mime
 | 
					 | 
				
			||||||
     * @param {string} baseFileName
 | 
					 | 
				
			||||||
     * @param {Object.<string, int>} existingFileNames
 | 
					 | 
				
			||||||
     * @return {string}
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    function getDataFileName(type, mime, baseFileName, existingFileNames) {
 | 
					 | 
				
			||||||
        let fileName = baseFileName.trim();
 | 
					        let fileName = baseFileName.trim();
 | 
				
			||||||
        if (fileName.length > 30) {
 | 
					        if (fileName.length > 30) {
 | 
				
			||||||
            fileName = fileName.substr(0, 30).trim();
 | 
					            fileName = fileName.substr(0, 30).trim();
 | 
				
			||||||
@ -115,13 +97,7 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
 | 
				
			|||||||
        return getUniqueFilename(existingFileNames, fileName);
 | 
					        return getUniqueFilename(existingFileNames, fileName);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    function createNoteMeta(branch: BBranch, parentMeta: Partial<NoteMeta>, existingFileNames: Record<string, number>): NoteMeta | null {
 | 
				
			||||||
     * @param {BBranch} branch
 | 
					 | 
				
			||||||
     * @param {NoteMeta} parentMeta
 | 
					 | 
				
			||||||
     * @param {Object.<string, int>} existingFileNames
 | 
					 | 
				
			||||||
     * @returns {NoteMeta|null}
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    function createNoteMeta(branch, parentMeta, existingFileNames) {
 | 
					 | 
				
			||||||
        const note = branch.getNote();
 | 
					        const note = branch.getNote();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (note.hasOwnedLabel('excludeFromExport')) {
 | 
					        if (note.hasOwnedLabel('excludeFromExport')) {
 | 
				
			||||||
@ -136,24 +112,26 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
 | 
				
			|||||||
            baseFileName = baseFileName.substr(0, 200);
 | 
					            baseFileName = baseFileName.substr(0, 200);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!parentMeta.notePath) { throw new Error("Missing parent note path."); }
 | 
				
			||||||
        const notePath = parentMeta.notePath.concat([note.noteId]);
 | 
					        const notePath = parentMeta.notePath.concat([note.noteId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (note.noteId in noteIdToMeta) {
 | 
					        if (note.noteId in noteIdToMeta) {
 | 
				
			||||||
            const fileName = getUniqueFilename(existingFileNames, `${baseFileName}.clone.${format === 'html' ? 'html' : 'md'}`);
 | 
					            const fileName = getUniqueFilename(existingFileNames, `${baseFileName}.clone.${format === 'html' ? 'html' : 'md'}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const meta = new NoteMeta();
 | 
					            const meta: NoteMeta = {
 | 
				
			||||||
            meta.isClone = true;
 | 
					                isClone: true,
 | 
				
			||||||
            meta.noteId = note.noteId;
 | 
					                noteId: note.noteId,
 | 
				
			||||||
            meta.notePath = notePath;
 | 
					                notePath: notePath,
 | 
				
			||||||
            meta.title = note.getTitleOrProtected();
 | 
					                title: note.getTitleOrProtected(),
 | 
				
			||||||
            meta.prefix = branch.prefix;
 | 
					                prefix: branch.prefix,
 | 
				
			||||||
            meta.dataFileName = fileName;
 | 
					                dataFileName: fileName,
 | 
				
			||||||
            meta.type = 'text'; // export will have text description
 | 
					                type: 'text', // export will have text description
 | 
				
			||||||
            meta.format = format;
 | 
					                format: format
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
            return meta;
 | 
					            return meta;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const meta = new NoteMeta();
 | 
					        const meta: Partial<NoteMeta> = {};
 | 
				
			||||||
        meta.isClone = false;
 | 
					        meta.isClone = false;
 | 
				
			||||||
        meta.noteId = note.noteId;
 | 
					        meta.noteId = note.noteId;
 | 
				
			||||||
        meta.notePath = notePath;
 | 
					        meta.notePath = notePath;
 | 
				
			||||||
@ -164,12 +142,14 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
 | 
				
			|||||||
        meta.type = note.type;
 | 
					        meta.type = note.type;
 | 
				
			||||||
        meta.mime = note.mime;
 | 
					        meta.mime = note.mime;
 | 
				
			||||||
        meta.attributes = note.getOwnedAttributes().map(attribute => {
 | 
					        meta.attributes = note.getOwnedAttributes().map(attribute => {
 | 
				
			||||||
            const attrMeta = new AttributeMeta();
 | 
					            const attrMeta: AttributeMeta = {
 | 
				
			||||||
            attrMeta.type = attribute.type;
 | 
					                type: attribute.type,
 | 
				
			||||||
            attrMeta.name = attribute.name;
 | 
					                name: attribute.name,
 | 
				
			||||||
            attrMeta.value = attribute.value;
 | 
					                value: attribute.value,
 | 
				
			||||||
            attrMeta.isInheritable = attribute.isInheritable;
 | 
					                isInheritable: attribute.isInheritable,
 | 
				
			||||||
            attrMeta.position = attribute.position;
 | 
					                position: attribute.position
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return attrMeta;
 | 
					            return attrMeta;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -179,12 +159,12 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
 | 
				
			|||||||
            meta.format = format;
 | 
					            meta.format = format;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        noteIdToMeta[note.noteId] = meta;
 | 
					        noteIdToMeta[note.noteId] = meta as NoteMeta;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // sort children for having a stable / reproducible export format
 | 
					        // sort children for having a stable / reproducible export format
 | 
				
			||||||
        note.sortChildren();
 | 
					        note.sortChildren();
 | 
				
			||||||
        const childBranches = note.getChildBranches()
 | 
					        const childBranches = note.getChildBranches()
 | 
				
			||||||
            .filter(branch => branch.noteId !== '_hidden');
 | 
					            .filter(branch => branch?.noteId !== '_hidden');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const available = !note.isProtected || protectedSessionService.isProtectedSessionAvailable();
 | 
					        const available = !note.isProtected || protectedSessionService.isProtectedSessionAvailable();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -196,18 +176,19 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
 | 
				
			|||||||
        const attachments = note.getAttachments();
 | 
					        const attachments = note.getAttachments();
 | 
				
			||||||
        meta.attachments = attachments
 | 
					        meta.attachments = attachments
 | 
				
			||||||
            .map(attachment => {
 | 
					            .map(attachment => {
 | 
				
			||||||
                const attMeta = new AttachmentMeta();
 | 
					                const attMeta: AttachmentMeta = {
 | 
				
			||||||
                attMeta.attachmentId = attachment.attachmentId;
 | 
					                    attachmentId: attachment.attachmentId,
 | 
				
			||||||
                attMeta.title = attachment.title;
 | 
					                    title: attachment.title,
 | 
				
			||||||
                attMeta.role = attachment.role;
 | 
					                    role: attachment.role,
 | 
				
			||||||
                attMeta.mime = attachment.mime;
 | 
					                    mime: attachment.mime,
 | 
				
			||||||
                attMeta.position = attachment.position;
 | 
					                    position: attachment.position,
 | 
				
			||||||
                attMeta.dataFileName = getDataFileName(
 | 
					                    dataFileName: getDataFileName(
 | 
				
			||||||
                    null,
 | 
					                        null,
 | 
				
			||||||
                    attachment.mime,
 | 
					                        attachment.mime,
 | 
				
			||||||
                    baseFileName + "_" + attachment.title,
 | 
					                        baseFileName + "_" + attachment.title,
 | 
				
			||||||
                    existingFileNames
 | 
					                        existingFileNames
 | 
				
			||||||
                );
 | 
					                    )
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
                return attMeta;
 | 
					                return attMeta;
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -219,7 +200,9 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
 | 
				
			|||||||
            const childExistingNames = {};
 | 
					            const childExistingNames = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            for (const childBranch of childBranches) {
 | 
					            for (const childBranch of childBranches) {
 | 
				
			||||||
                const note = createNoteMeta(childBranch, meta, childExistingNames);
 | 
					                if (!childBranch) { continue; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const note = createNoteMeta(childBranch, meta as NoteMeta, childExistingNames);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // can be undefined if export is disabled for this note
 | 
					                // can be undefined if export is disabled for this note
 | 
				
			||||||
                if (note) {
 | 
					                if (note) {
 | 
				
			||||||
@ -228,18 +211,13 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return meta;
 | 
					        return meta as NoteMeta;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    function getNoteTargetUrl(targetNoteId: string, sourceMeta: NoteMeta): string | null {
 | 
				
			||||||
     * @param {string} targetNoteId
 | 
					 | 
				
			||||||
     * @param {NoteMeta} sourceMeta
 | 
					 | 
				
			||||||
     * @return {string|null}
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    function getNoteTargetUrl(targetNoteId, sourceMeta) {
 | 
					 | 
				
			||||||
        const targetMeta = noteIdToMeta[targetNoteId];
 | 
					        const targetMeta = noteIdToMeta[targetNoteId];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!targetMeta) {
 | 
					        if (!targetMeta || !targetMeta.notePath || !sourceMeta.notePath) {
 | 
				
			||||||
            return null;
 | 
					            return null;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -256,24 +234,20 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        for (let i = 0; i < targetPath.length - 1; i++) {
 | 
					        for (let i = 0; i < targetPath.length - 1; i++) {
 | 
				
			||||||
            const meta = noteIdToMeta[targetPath[i]];
 | 
					            const meta = noteIdToMeta[targetPath[i]];
 | 
				
			||||||
 | 
					            if (meta.dirFileName) {
 | 
				
			||||||
            url += `${encodeURIComponent(meta.dirFileName)}/`;
 | 
					                url += `${encodeURIComponent(meta.dirFileName)}/`;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const meta = noteIdToMeta[targetPath[targetPath.length - 1]];
 | 
					        const meta = noteIdToMeta[targetPath[targetPath.length - 1]];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // link can target note which is only "folder-note" and as such, will not have a file in an export
 | 
					        // link can target note which is only "folder-note" and as such, will not have a file in an export
 | 
				
			||||||
        url += encodeURIComponent(meta.dataFileName || meta.dirFileName);
 | 
					        url += encodeURIComponent(meta.dataFileName || meta.dirFileName || "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return url;
 | 
					        return url;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    function rewriteLinks(content: string, noteMeta: NoteMeta): string {
 | 
				
			||||||
     * @param {string} content
 | 
					 | 
				
			||||||
     * @param {NoteMeta} noteMeta
 | 
					 | 
				
			||||||
     * @return {string}
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    function rewriteLinks(content, noteMeta) {
 | 
					 | 
				
			||||||
        content = content.replace(/src="[^"]*api\/images\/([a-zA-Z0-9_]+)\/[^"]*"/g, (match, targetNoteId) => {
 | 
					        content = content.replace(/src="[^"]*api\/images\/([a-zA-Z0-9_]+)\/[^"]*"/g, (match, targetNoteId) => {
 | 
				
			||||||
            const url = getNoteTargetUrl(targetNoteId, noteMeta);
 | 
					            const url = getNoteTargetUrl(targetNoteId, noteMeta);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -300,10 +274,10 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return content;
 | 
					        return content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        function findAttachment(targetAttachmentId) {
 | 
					        function findAttachment(targetAttachmentId: string) {
 | 
				
			||||||
            let url;
 | 
					            let url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const attachmentMeta = noteMeta.attachments.find(attMeta => attMeta.attachmentId === targetAttachmentId);
 | 
					            const attachmentMeta = (noteMeta.attachments || []).find(attMeta => attMeta.attachmentId === targetAttachmentId);
 | 
				
			||||||
            if (attachmentMeta) {
 | 
					            if (attachmentMeta) {
 | 
				
			||||||
                // easy job here, because attachment will be in the same directory as the note's data file.
 | 
					                // easy job here, because attachment will be in the same directory as the note's data file.
 | 
				
			||||||
                url = attachmentMeta.dataFileName;
 | 
					                url = attachmentMeta.dataFileName;
 | 
				
			||||||
@ -314,21 +288,17 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    function prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer {
 | 
				
			||||||
     * @param {string} title
 | 
					        if (['html', 'markdown'].includes(noteMeta?.format || "")) {
 | 
				
			||||||
     * @param {string|Buffer} content
 | 
					 | 
				
			||||||
     * @param {NoteMeta} noteMeta
 | 
					 | 
				
			||||||
     * @return {string|Buffer}
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    function prepareContent(title, content, noteMeta) {
 | 
					 | 
				
			||||||
        if (['html', 'markdown'].includes(noteMeta.format)) {
 | 
					 | 
				
			||||||
            content = content.toString();
 | 
					            content = content.toString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            content = rewriteLinks(content, noteMeta);
 | 
					            content = rewriteLinks(content, noteMeta);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (noteMeta.format === 'html') {
 | 
					        if (noteMeta.format === 'html' && typeof content === "string") {
 | 
				
			||||||
            if (!content.substr(0, 100).toLowerCase().includes("<html")) {
 | 
					            if (!content.substr(0, 100).toLowerCase().includes("<html")) {
 | 
				
			||||||
 | 
					                if (!noteMeta?.notePath?.length) { throw new Error("Missing note path."); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                const cssUrl = `${"../".repeat(noteMeta.notePath.length - 1)}style.css`;
 | 
					                const cssUrl = `${"../".repeat(noteMeta.notePath.length - 1)}style.css`;
 | 
				
			||||||
                const htmlTitle = utils.escapeHtml(title);
 | 
					                const htmlTitle = utils.escapeHtml(title);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -354,7 +324,7 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
 | 
				
			|||||||
            return content.length < 100_000
 | 
					            return content.length < 100_000
 | 
				
			||||||
                ? html.prettyPrint(content, {indent_size: 2})
 | 
					                ? html.prettyPrint(content, {indent_size: 2})
 | 
				
			||||||
                : content;
 | 
					                : content;
 | 
				
			||||||
        } else if (noteMeta.format === 'markdown') {
 | 
					        } else if (noteMeta.format === 'markdown' && typeof content === "string") {
 | 
				
			||||||
            let markdownContent = mdService.toMarkdown(content);
 | 
					            let markdownContent = mdService.toMarkdown(content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (markdownContent.trim().length > 0 && !markdownContent.startsWith("# ")) {
 | 
					            if (markdownContent.trim().length > 0 && !markdownContent.startsWith("# ")) {
 | 
				
			||||||
@ -368,17 +338,17 @@ ${markdownContent}`;
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    function saveNote(noteMeta: NoteMeta, filePathPrefix: string) {
 | 
				
			||||||
     * @param {NoteMeta} noteMeta
 | 
					 | 
				
			||||||
     * @param {string} filePathPrefix
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    function saveNote(noteMeta, filePathPrefix) {
 | 
					 | 
				
			||||||
        log.info(`Exporting note '${noteMeta.noteId}'`);
 | 
					        log.info(`Exporting note '${noteMeta.noteId}'`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!noteMeta.noteId || !noteMeta.title) {
 | 
				
			||||||
 | 
					            throw new Error("Missing note meta.");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (noteMeta.isClone) {
 | 
					        if (noteMeta.isClone) {
 | 
				
			||||||
            const targetUrl = getNoteTargetUrl(noteMeta.noteId, noteMeta);
 | 
					            const targetUrl = getNoteTargetUrl(noteMeta.noteId, noteMeta);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            let content = `<p>This is a clone of a note. Go to its <a href="${targetUrl}">primary location</a>.</p>`;
 | 
					            let content: string | Buffer = `<p>This is a clone of a note. Go to its <a href="${targetUrl}">primary location</a>.</p>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            content = prepareContent(noteMeta.title, content, noteMeta);
 | 
					            content = prepareContent(noteMeta.title, content, noteMeta);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -388,6 +358,8 @@ ${markdownContent}`;
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const note = becca.getNote(noteMeta.noteId);
 | 
					        const note = becca.getNote(noteMeta.noteId);
 | 
				
			||||||
 | 
					        if (!note) { throw new Error("Unable to find note."); }
 | 
				
			||||||
 | 
					        if (!note.utcDateModified) { throw new Error("Unable to find modification date."); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (noteMeta.dataFileName) {
 | 
					        if (noteMeta.dataFileName) {
 | 
				
			||||||
            const content = prepareContent(noteMeta.title, note.getContent(), noteMeta);
 | 
					            const content = prepareContent(noteMeta.title, note.getContent(), noteMeta);
 | 
				
			||||||
@ -400,7 +372,9 @@ ${markdownContent}`;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        taskContext.increaseProgressCount();
 | 
					        taskContext.increaseProgressCount();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const attachmentMeta of noteMeta.attachments) {
 | 
					        for (const attachmentMeta of noteMeta.attachments || []) {
 | 
				
			||||||
 | 
					            if (!attachmentMeta.attachmentId) { continue; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const attachment = note.getAttachmentById(attachmentMeta.attachmentId);
 | 
					            const attachment = note.getAttachmentById(attachmentMeta.attachmentId);
 | 
				
			||||||
            const content = attachment.getContent();
 | 
					            const content = attachment.getContent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -410,29 +384,25 @@ ${markdownContent}`;
 | 
				
			|||||||
            });
 | 
					            });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (noteMeta.children?.length > 0) {
 | 
					        if (noteMeta.children?.length || 0 > 0) {
 | 
				
			||||||
            const directoryPath = filePathPrefix + noteMeta.dirFileName;
 | 
					            const directoryPath = filePathPrefix + noteMeta.dirFileName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // create directory
 | 
					            // create directory
 | 
				
			||||||
            archive.append('', { name: `${directoryPath}/`, date: dateUtils.parseDateTime(note.utcDateModified) });
 | 
					            archive.append('', { name: `${directoryPath}/`, date: dateUtils.parseDateTime(note.utcDateModified) });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            for (const childMeta of noteMeta.children) {
 | 
					            for (const childMeta of noteMeta.children || []) {
 | 
				
			||||||
                saveNote(childMeta, `${directoryPath}/`);
 | 
					                saveNote(childMeta, `${directoryPath}/`);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    function saveNavigation(rootMeta: NoteMeta, navigationMeta: NoteMeta) {
 | 
				
			||||||
     * @param {NoteMeta} rootMeta
 | 
					        function saveNavigationInner(meta: NoteMeta) {
 | 
				
			||||||
     * @param {NoteMeta} navigationMeta
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    function saveNavigation(rootMeta, navigationMeta) {
 | 
					 | 
				
			||||||
        function saveNavigationInner(meta) {
 | 
					 | 
				
			||||||
            let html = '<li>';
 | 
					            let html = '<li>';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const escapedTitle = utils.escapeHtml(`${meta.prefix ? `${meta.prefix} - ` : ''}${meta.title}`);
 | 
					            const escapedTitle = utils.escapeHtml(`${meta.prefix ? `${meta.prefix} - ` : ''}${meta.title}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (meta.dataFileName) {
 | 
					            if (meta.dataFileName && meta.noteId) {
 | 
				
			||||||
                const targetUrl = getNoteTargetUrl(meta.noteId, rootMeta);
 | 
					                const targetUrl = getNoteTargetUrl(meta.noteId, rootMeta);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                html += `<a href="${targetUrl}" target="detail">${escapedTitle}</a>`;
 | 
					                html += `<a href="${targetUrl}" target="detail">${escapedTitle}</a>`;
 | 
				
			||||||
@ -470,16 +440,12 @@ ${markdownContent}`;
 | 
				
			|||||||
        archive.append(prettyHtml, { name: navigationMeta.dataFileName });
 | 
					        archive.append(prettyHtml, { name: navigationMeta.dataFileName });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    function saveIndex(rootMeta: NoteMeta, indexMeta: NoteMeta) {
 | 
				
			||||||
     * @param {NoteMeta} rootMeta
 | 
					 | 
				
			||||||
     * @param {NoteMeta} indexMeta
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    function saveIndex(rootMeta, indexMeta) {
 | 
					 | 
				
			||||||
        let firstNonEmptyNote;
 | 
					        let firstNonEmptyNote;
 | 
				
			||||||
        let curMeta = rootMeta;
 | 
					        let curMeta = rootMeta;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        while (!firstNonEmptyNote) {
 | 
					        while (!firstNonEmptyNote) {
 | 
				
			||||||
            if (curMeta.dataFileName) {
 | 
					            if (curMeta.dataFileName && curMeta.noteId) {
 | 
				
			||||||
                firstNonEmptyNote = getNoteTargetUrl(curMeta.noteId, rootMeta);
 | 
					                firstNonEmptyNote = getNoteTargetUrl(curMeta.noteId, rootMeta);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -506,17 +472,13 @@ ${markdownContent}`;
 | 
				
			|||||||
        archive.append(fullHtml, { name: indexMeta.dataFileName });
 | 
					        archive.append(fullHtml, { name: indexMeta.dataFileName });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    function saveCss(rootMeta: NoteMeta, cssMeta: NoteMeta) {
 | 
				
			||||||
     * @param {NoteMeta} rootMeta
 | 
					 | 
				
			||||||
     * @param {NoteMeta} cssMeta
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    function saveCss(rootMeta, cssMeta) {
 | 
					 | 
				
			||||||
        const cssContent = fs.readFileSync(`${RESOURCE_DIR}/libraries/ckeditor/ckeditor-content.css`);
 | 
					        const cssContent = fs.readFileSync(`${RESOURCE_DIR}/libraries/ckeditor/ckeditor-content.css`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        archive.append(cssContent, { name: cssMeta.dataFileName });
 | 
					        archive.append(cssContent, { name: cssMeta.dataFileName });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const existingFileNames = format === 'html' ? ['navigation', 'index'] : [];
 | 
					    const existingFileNames: Record<string, number> = format === 'html' ? {'navigation': 0, 'index': 1} : {};
 | 
				
			||||||
    const rootMeta = createNoteMeta(branch, { notePath: [] }, existingFileNames);
 | 
					    const rootMeta = createNoteMeta(branch, { notePath: [] }, existingFileNames);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const metaFile = {
 | 
					    const metaFile = {
 | 
				
			||||||
@ -525,7 +487,9 @@ ${markdownContent}`;
 | 
				
			|||||||
        files: [ rootMeta ]
 | 
					        files: [ rootMeta ]
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let navigationMeta, indexMeta, cssMeta;
 | 
					    let navigationMeta: NoteMeta | null = null;
 | 
				
			||||||
 | 
					    let indexMeta: NoteMeta | null = null;
 | 
				
			||||||
 | 
					    let cssMeta: NoteMeta | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (format === 'html') {
 | 
					    if (format === 'html') {
 | 
				
			||||||
        navigationMeta = {
 | 
					        navigationMeta = {
 | 
				
			||||||
@ -552,7 +516,7 @@ ${markdownContent}`;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    for (const noteMeta of Object.values(noteIdToMeta)) {
 | 
					    for (const noteMeta of Object.values(noteIdToMeta)) {
 | 
				
			||||||
        // filter out relations which are not inside this export
 | 
					        // filter out relations which are not inside this export
 | 
				
			||||||
        noteMeta.attributes = noteMeta.attributes.filter(attr => {
 | 
					        noteMeta.attributes = (noteMeta.attributes || []).filter(attr => {
 | 
				
			||||||
            if (attr.type !== 'relation') {
 | 
					            if (attr.type !== 'relation') {
 | 
				
			||||||
                return true;
 | 
					                return true;
 | 
				
			||||||
            } else if (attr.value in noteIdToMeta) {
 | 
					            } else if (attr.value in noteIdToMeta) {
 | 
				
			||||||
@ -567,7 +531,9 @@ ${markdownContent}`;
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!rootMeta) { // corner case of disabled export for exported note
 | 
					    if (!rootMeta) { // corner case of disabled export for exported note
 | 
				
			||||||
        res.sendStatus(400);
 | 
					        if ("sendStatus" in res) {
 | 
				
			||||||
 | 
					            res.sendStatus(400);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -578,6 +544,10 @@ ${markdownContent}`;
 | 
				
			|||||||
    saveNote(rootMeta, '');
 | 
					    saveNote(rootMeta, '');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (format === 'html') {
 | 
					    if (format === 'html') {
 | 
				
			||||||
 | 
					        if (!navigationMeta || !indexMeta || !cssMeta) {
 | 
				
			||||||
 | 
					            throw new Error("Missing meta.");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        saveNavigation(rootMeta, navigationMeta);
 | 
					        saveNavigation(rootMeta, navigationMeta);
 | 
				
			||||||
        saveIndex(rootMeta, indexMeta);
 | 
					        saveIndex(rootMeta, indexMeta);
 | 
				
			||||||
        saveCss(rootMeta, cssMeta);
 | 
					        saveCss(rootMeta, cssMeta);
 | 
				
			||||||
@ -586,7 +556,7 @@ ${markdownContent}`;
 | 
				
			|||||||
    const note = branch.getNote();
 | 
					    const note = branch.getNote();
 | 
				
			||||||
    const zipFileName = `${branch.prefix ? `${branch.prefix} - ` : ""}${note.getTitleOrProtected()}.zip`;
 | 
					    const zipFileName = `${branch.prefix ? `${branch.prefix} - ` : ""}${note.getTitleOrProtected()}.zip`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (setHeaders) {
 | 
					    if (setHeaders && "setHeader" in res) {
 | 
				
			||||||
        res.setHeader('Content-Disposition', utils.getContentDisposition(zipFileName));
 | 
					        res.setHeader('Content-Disposition', utils.getContentDisposition(zipFileName));
 | 
				
			||||||
        res.setHeader('Content-Type', 'application/zip');
 | 
					        res.setHeader('Content-Type', 'application/zip');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -597,7 +567,7 @@ ${markdownContent}`;
 | 
				
			|||||||
    taskContext.taskSucceeded();
 | 
					    taskContext.taskSucceeded();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function exportToZipFile(noteId, format, zipFilePath) {
 | 
					async function exportToZipFile(noteId: string, format: "markdown" | "html", zipFilePath: string) {
 | 
				
			||||||
    const fileOutputStream = fs.createWriteStream(zipFilePath);
 | 
					    const fileOutputStream = fs.createWriteStream(zipFilePath);
 | 
				
			||||||
    const taskContext = new TaskContext('no-progress-reporting');
 | 
					    const taskContext = new TaskContext('no-progress-reporting');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -612,7 +582,7 @@ async function exportToZipFile(noteId, format, zipFilePath) {
 | 
				
			|||||||
    log.info(`Exported '${noteId}' with format '${format}' to '${zipFilePath}'`);
 | 
					    log.info(`Exported '${noteId}' with format '${format}' to '${zipFilePath}'`);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					export = {
 | 
				
			||||||
    exportToZip,
 | 
					    exportToZip,
 | 
				
			||||||
    exportToZipFile
 | 
					    exportToZipFile
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -1,19 +1,19 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const becca = require('../becca/becca');
 | 
					import becca = require('../becca/becca');
 | 
				
			||||||
const log = require('./log');
 | 
					import log = require('./log');
 | 
				
			||||||
const protectedSessionService = require('./protected_session');
 | 
					import protectedSessionService = require('./protected_session');
 | 
				
			||||||
const noteService = require('./notes');
 | 
					import noteService = require('./notes');
 | 
				
			||||||
const optionService = require('./options');
 | 
					import optionService = require('./options');
 | 
				
			||||||
const sql = require('./sql');
 | 
					import sql = require('./sql');
 | 
				
			||||||
const jimp = require('jimp');
 | 
					import jimp = require('jimp');
 | 
				
			||||||
const imageType = require('image-type');
 | 
					import imageType = require('image-type');
 | 
				
			||||||
const sanitizeFilename = require('sanitize-filename');
 | 
					import sanitizeFilename = require('sanitize-filename');
 | 
				
			||||||
const isSvg = require('is-svg');
 | 
					import isSvg = require('is-svg');
 | 
				
			||||||
const isAnimated = require('is-animated');
 | 
					import isAnimated = require('is-animated');
 | 
				
			||||||
const htmlSanitizer = require('./html_sanitizer');
 | 
					import htmlSanitizer = require('./html_sanitizer');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function processImage(uploadBuffer, originalName, shrinkImageSwitch) {
 | 
					async function processImage(uploadBuffer: Buffer, originalName: string, shrinkImageSwitch: boolean) {
 | 
				
			||||||
    const compressImages = optionService.getOptionBool("compressImages");
 | 
					    const compressImages = optionService.getOptionBool("compressImages");
 | 
				
			||||||
    const origImageFormat = getImageType(uploadBuffer);
 | 
					    const origImageFormat = getImageType(uploadBuffer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -44,7 +44,7 @@ async function processImage(uploadBuffer, originalName, shrinkImageSwitch) {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getImageType(buffer) {
 | 
					function getImageType(buffer: Buffer) {
 | 
				
			||||||
    if (isSvg(buffer)) {
 | 
					    if (isSvg(buffer)) {
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
            ext: 'svg'
 | 
					            ext: 'svg'
 | 
				
			||||||
@ -57,18 +57,19 @@ function getImageType(buffer) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getImageMimeFromExtension(ext) {
 | 
					function getImageMimeFromExtension(ext: string) {
 | 
				
			||||||
    ext = ext.toLowerCase();
 | 
					    ext = ext.toLowerCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return `image/${ext === 'svg' ? 'svg+xml' : ext}`;
 | 
					    return `image/${ext === 'svg' ? 'svg+xml' : ext}`;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function updateImage(noteId, uploadBuffer, originalName) {
 | 
					function updateImage(noteId: string, uploadBuffer: Buffer, originalName: string) {
 | 
				
			||||||
    log.info(`Updating image ${noteId}: ${originalName}`);
 | 
					    log.info(`Updating image ${noteId}: ${originalName}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    originalName = htmlSanitizer.sanitize(originalName);
 | 
					    originalName = htmlSanitizer.sanitize(originalName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const note = becca.getNote(noteId);
 | 
					    const note = becca.getNote(noteId);
 | 
				
			||||||
 | 
					    if (!note) { throw new Error("Unable to find note."); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    note.saveRevision();
 | 
					    note.saveRevision();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -85,7 +86,7 @@ function updateImage(noteId, uploadBuffer, originalName) {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function saveImage(parentNoteId, uploadBuffer, originalName, shrinkImageSwitch, trimFilename = false) {
 | 
					function saveImage(parentNoteId: string, uploadBuffer: Buffer, originalName: string, shrinkImageSwitch: boolean, trimFilename = false) {
 | 
				
			||||||
    log.info(`Saving image ${originalName} into parent ${parentNoteId}`);
 | 
					    log.info(`Saving image ${originalName} into parent ${parentNoteId}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (trimFilename && originalName.length > 40) {
 | 
					    if (trimFilename && originalName.length > 40) {
 | 
				
			||||||
@ -95,6 +96,7 @@ function saveImage(parentNoteId, uploadBuffer, originalName, shrinkImageSwitch,
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const fileName = sanitizeFilename(originalName);
 | 
					    const fileName = sanitizeFilename(originalName);
 | 
				
			||||||
    const parentNote = becca.getNote(parentNoteId);
 | 
					    const parentNote = becca.getNote(parentNoteId);
 | 
				
			||||||
 | 
					    if (!parentNote) { throw new Error("Unable to find parent note."); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const {note} = noteService.createNewNote({
 | 
					    const {note} = noteService.createNewNote({
 | 
				
			||||||
        parentNoteId,
 | 
					        parentNoteId,
 | 
				
			||||||
@ -131,7 +133,7 @@ function saveImage(parentNoteId, uploadBuffer, originalName, shrinkImageSwitch,
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function saveImageToAttachment(noteId, uploadBuffer, originalName, shrinkImageSwitch, trimFilename = false) {
 | 
					function saveImageToAttachment(noteId: string, uploadBuffer: Buffer, originalName: string, shrinkImageSwitch?: boolean, trimFilename = false) {
 | 
				
			||||||
    log.info(`Saving image '${originalName}' as attachment into note '${noteId}'`);
 | 
					    log.info(`Saving image '${originalName}' as attachment into note '${noteId}'`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (trimFilename && originalName.length > 40) {
 | 
					    if (trimFilename && originalName.length > 40) {
 | 
				
			||||||
@ -160,9 +162,10 @@ function saveImageToAttachment(noteId, uploadBuffer, originalName, shrinkImageSw
 | 
				
			|||||||
    }, 5000);
 | 
					    }, 5000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // resizing images asynchronously since JIMP does not support sync operation
 | 
					    // resizing images asynchronously since JIMP does not support sync operation
 | 
				
			||||||
    processImage(uploadBuffer, originalName, shrinkImageSwitch).then(({buffer, imageFormat}) => {
 | 
					    processImage(uploadBuffer, originalName, !!shrinkImageSwitch).then(({buffer, imageFormat}) => {
 | 
				
			||||||
        sql.transactional(() => {
 | 
					        sql.transactional(() => {
 | 
				
			||||||
            // re-read, might be changed in the meantime
 | 
					            // re-read, might be changed in the meantime
 | 
				
			||||||
 | 
					            if (!attachment.attachmentId) { throw new Error("Missing attachment ID."); }
 | 
				
			||||||
            attachment = becca.getAttachmentOrThrow(attachment.attachmentId);
 | 
					            attachment = becca.getAttachmentOrThrow(attachment.attachmentId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            attachment.mime = getImageMimeFromExtension(imageFormat.ext);
 | 
					            attachment.mime = getImageMimeFromExtension(imageFormat.ext);
 | 
				
			||||||
@ -179,7 +182,7 @@ function saveImageToAttachment(noteId, uploadBuffer, originalName, shrinkImageSw
 | 
				
			|||||||
    return attachment;
 | 
					    return attachment;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function shrinkImage(buffer, originalName) {
 | 
					async function shrinkImage(buffer: Buffer, originalName: string) {
 | 
				
			||||||
    let jpegQuality = optionService.getOptionInt('imageJpegQuality', 0);
 | 
					    let jpegQuality = optionService.getOptionInt('imageJpegQuality', 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (jpegQuality < 10 || jpegQuality > 100) {
 | 
					    if (jpegQuality < 10 || jpegQuality > 100) {
 | 
				
			||||||
@ -190,7 +193,7 @@ async function shrinkImage(buffer, originalName) {
 | 
				
			|||||||
    try {
 | 
					    try {
 | 
				
			||||||
        finalImageBuffer = await resize(buffer, jpegQuality);
 | 
					        finalImageBuffer = await resize(buffer, jpegQuality);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    catch (e) {
 | 
					    catch (e: any) {
 | 
				
			||||||
        log.error(`Failed to resize image '${originalName}', stack: ${e.stack}`);
 | 
					        log.error(`Failed to resize image '${originalName}', stack: ${e.stack}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        finalImageBuffer = buffer;
 | 
					        finalImageBuffer = buffer;
 | 
				
			||||||
@ -205,7 +208,7 @@ async function shrinkImage(buffer, originalName) {
 | 
				
			|||||||
    return finalImageBuffer;
 | 
					    return finalImageBuffer;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function resize(buffer, quality) {
 | 
					async function resize(buffer: Buffer, quality: number) {
 | 
				
			||||||
    const imageMaxWidthHeight = optionService.getOptionInt('imageMaxWidthHeight');
 | 
					    const imageMaxWidthHeight = optionService.getOptionInt('imageMaxWidthHeight');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const start = Date.now();
 | 
					    const start = Date.now();
 | 
				
			||||||
@ -231,7 +234,7 @@ async function resize(buffer, quality) {
 | 
				
			|||||||
    return resultBuffer;
 | 
					    return resultBuffer;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					export = {
 | 
				
			||||||
    saveImage,
 | 
					    saveImage,
 | 
				
			||||||
    saveImageToAttachment,
 | 
					    saveImageToAttachment,
 | 
				
			||||||
    updateImage
 | 
					    updateImage
 | 
				
			||||||
							
								
								
									
										5
									
								
								src/services/import/common.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/services/import/common.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					export interface File {
 | 
				
			||||||
 | 
					    originalname: string;
 | 
				
			||||||
 | 
					    mimetype: string;
 | 
				
			||||||
 | 
					    buffer: string | Buffer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,20 +1,23 @@
 | 
				
			|||||||
const sax = require("sax");
 | 
					import sax = require("sax");
 | 
				
			||||||
const stream = require('stream');
 | 
					import stream = require('stream');
 | 
				
			||||||
const {Throttle} = require('stream-throttle');
 | 
					import { Throttle } from 'stream-throttle';
 | 
				
			||||||
const log = require('../log');
 | 
					import log = require('../log');
 | 
				
			||||||
const utils = require('../utils');
 | 
					import utils = require('../utils');
 | 
				
			||||||
const sql = require('../sql');
 | 
					import sql = require('../sql');
 | 
				
			||||||
const noteService = require('../notes');
 | 
					import noteService = require('../notes');
 | 
				
			||||||
const imageService = require('../image.js');
 | 
					import imageService = require('../image');
 | 
				
			||||||
const protectedSessionService = require('../protected_session');
 | 
					import protectedSessionService = require('../protected_session');
 | 
				
			||||||
const htmlSanitizer = require('../html_sanitizer');
 | 
					import htmlSanitizer = require('../html_sanitizer');
 | 
				
			||||||
const {sanitizeAttributeName} = require('../sanitize_attribute_name');
 | 
					import sanitizeAttributeName = require('../sanitize_attribute_name');
 | 
				
			||||||
 | 
					import TaskContext = require("../task_context");
 | 
				
			||||||
 | 
					import BNote = require("../../becca/entities/bnote");
 | 
				
			||||||
 | 
					import { File } from "./common";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * date format is e.g. 20181121T193703Z or 2013-04-14T16:19:00.000Z (Mac evernote, see #3496)
 | 
					 * date format is e.g. 20181121T193703Z or 2013-04-14T16:19:00.000Z (Mac evernote, see #3496)
 | 
				
			||||||
 * @returns trilium date format, e.g. 2013-04-14 16:19:00.000Z
 | 
					 * @returns trilium date format, e.g. 2013-04-14 16:19:00.000Z
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function parseDate(text) {
 | 
					function parseDate(text: string) {
 | 
				
			||||||
    // convert ISO format to the "20181121T193703Z" format
 | 
					    // convert ISO format to the "20181121T193703Z" format
 | 
				
			||||||
    text = text.replace(/[-:]/g, "");
 | 
					    text = text.replace(/[-:]/g, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -25,10 +28,34 @@ function parseDate(text) {
 | 
				
			|||||||
    return text;
 | 
					    return text;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let note = {};
 | 
					interface Attribute {
 | 
				
			||||||
let resource;
 | 
					    type: string;
 | 
				
			||||||
 | 
					    name: string;
 | 
				
			||||||
 | 
					    value: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function importEnex(taskContext, file, parentNote) {
 | 
					interface Resource {
 | 
				
			||||||
 | 
					    title: string;
 | 
				
			||||||
 | 
					    content?: Buffer | string;
 | 
				
			||||||
 | 
					    mime?: string;
 | 
				
			||||||
 | 
					    attributes: Attribute[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Note {
 | 
				
			||||||
 | 
					    title: string;
 | 
				
			||||||
 | 
					    attributes: Attribute[];
 | 
				
			||||||
 | 
					    utcDateCreated: string;
 | 
				
			||||||
 | 
					    utcDateModified: string;
 | 
				
			||||||
 | 
					    noteId: string;
 | 
				
			||||||
 | 
					    blobId: string;
 | 
				
			||||||
 | 
					    content: string;
 | 
				
			||||||
 | 
					    resources: Resource[]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let note: Partial<Note> = {};
 | 
				
			||||||
 | 
					let resource: Resource;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function importEnex(taskContext: TaskContext, file: File, parentNote: BNote) {
 | 
				
			||||||
    const saxStream = sax.createStream(true);
 | 
					    const saxStream = sax.createStream(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const rootNoteTitle = file.originalname.toLowerCase().endsWith(".enex")
 | 
					    const rootNoteTitle = file.originalname.toLowerCase().endsWith(".enex")
 | 
				
			||||||
@ -45,7 +72,7 @@ function importEnex(taskContext, file, parentNote) {
 | 
				
			|||||||
        isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
 | 
					        isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
 | 
				
			||||||
    }).note;
 | 
					    }).note;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function extractContent(content) {
 | 
					    function extractContent(content: string) {
 | 
				
			||||||
        const openingNoteIndex = content.indexOf('<en-note>');
 | 
					        const openingNoteIndex = content.indexOf('<en-note>');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (openingNoteIndex !== -1) {
 | 
					        if (openingNoteIndex !== -1) {
 | 
				
			||||||
@ -90,7 +117,7 @@ function importEnex(taskContext, file, parentNote) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const path = [];
 | 
					    const path: string[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function getCurrentTag() {
 | 
					    function getCurrentTag() {
 | 
				
			||||||
        if (path.length >= 1) {
 | 
					        if (path.length >= 1) {
 | 
				
			||||||
@ -108,8 +135,8 @@ function importEnex(taskContext, file, parentNote) {
 | 
				
			|||||||
        // unhandled errors will throw, since this is a proper node event emitter.
 | 
					        // unhandled errors will throw, since this is a proper node event emitter.
 | 
				
			||||||
        log.error(`error when parsing ENEX file: ${e}`);
 | 
					        log.error(`error when parsing ENEX file: ${e}`);
 | 
				
			||||||
        // clear the error
 | 
					        // clear the error
 | 
				
			||||||
        this._parser.error = null;
 | 
					        (saxStream._parser as any).error = null;
 | 
				
			||||||
        this._parser.resume();
 | 
					        saxStream._parser.resume();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    saxStream.on("text", text => {
 | 
					    saxStream.on("text", text => {
 | 
				
			||||||
@ -123,13 +150,15 @@ function importEnex(taskContext, file, parentNote) {
 | 
				
			|||||||
                labelName = 'pageUrl';
 | 
					                labelName = 'pageUrl';
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            labelName = sanitizeAttributeName(labelName);
 | 
					            labelName = sanitizeAttributeName.sanitizeAttributeName(labelName || "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            note.attributes.push({
 | 
					            if (note.attributes) {
 | 
				
			||||||
                type: 'label',
 | 
					                note.attributes.push({
 | 
				
			||||||
                name: labelName,
 | 
					                    type: 'label',
 | 
				
			||||||
                value: text
 | 
					                    name: labelName,
 | 
				
			||||||
            });
 | 
					                    value: text
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else if (previousTag === 'resource-attributes') {
 | 
					        else if (previousTag === 'resource-attributes') {
 | 
				
			||||||
            if (currentTag === 'file-name') {
 | 
					            if (currentTag === 'file-name') {
 | 
				
			||||||
@ -169,10 +198,10 @@ function importEnex(taskContext, file, parentNote) {
 | 
				
			|||||||
                note.utcDateCreated = parseDate(text);
 | 
					                note.utcDateCreated = parseDate(text);
 | 
				
			||||||
            } else if (currentTag === 'updated') {
 | 
					            } else if (currentTag === 'updated') {
 | 
				
			||||||
                note.utcDateModified = parseDate(text);
 | 
					                note.utcDateModified = parseDate(text);
 | 
				
			||||||
            } else if (currentTag === 'tag') {
 | 
					            } else if (currentTag === 'tag' && note.attributes) {
 | 
				
			||||||
                note.attributes.push({
 | 
					                note.attributes.push({
 | 
				
			||||||
                    type: 'label',
 | 
					                    type: 'label',
 | 
				
			||||||
                    name: sanitizeAttributeName(text),
 | 
					                    name: sanitizeAttributeName.sanitizeAttributeName(text),
 | 
				
			||||||
                    value: ''
 | 
					                    value: ''
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -201,11 +230,13 @@ function importEnex(taskContext, file, parentNote) {
 | 
				
			|||||||
                attributes: []
 | 
					                attributes: []
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            note.resources.push(resource);
 | 
					            if (note.resources) {
 | 
				
			||||||
 | 
					                note.resources.push(resource);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function updateDates(note, utcDateCreated, utcDateModified) {
 | 
					    function updateDates(note: BNote, utcDateCreated?: string, utcDateModified?: string) {
 | 
				
			||||||
        // it's difficult to force custom dateCreated and dateModified to Note entity, so we do it post-creation with SQL
 | 
					        // it's difficult to force custom dateCreated and dateModified to Note entity, so we do it post-creation with SQL
 | 
				
			||||||
        sql.execute(`
 | 
					        sql.execute(`
 | 
				
			||||||
                UPDATE notes 
 | 
					                UPDATE notes 
 | 
				
			||||||
@ -227,6 +258,10 @@ function importEnex(taskContext, file, parentNote) {
 | 
				
			|||||||
        // make a copy because stream continues with the next call and note gets overwritten
 | 
					        // make a copy because stream continues with the next call and note gets overwritten
 | 
				
			||||||
        let {title, content, attributes, resources, utcDateCreated, utcDateModified} = note;
 | 
					        let {title, content, attributes, resources, utcDateCreated, utcDateModified} = note;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!title || !content) {
 | 
				
			||||||
 | 
					            throw new Error("Missing title or content for note.");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        content = extractContent(content);
 | 
					        content = extractContent(content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const noteEntity = noteService.createNewNote({
 | 
					        const noteEntity = noteService.createNewNote({
 | 
				
			||||||
@ -239,7 +274,7 @@ function importEnex(taskContext, file, parentNote) {
 | 
				
			|||||||
            isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
 | 
					            isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
 | 
				
			||||||
        }).note;
 | 
					        }).note;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const attr of attributes) {
 | 
					        for (const attr of attributes || []) {
 | 
				
			||||||
            noteEntity.addAttribute(attr.type, attr.name, attr.value);
 | 
					            noteEntity.addAttribute(attr.type, attr.name, attr.value);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -249,12 +284,14 @@ function importEnex(taskContext, file, parentNote) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        taskContext.increaseProgressCount();
 | 
					        taskContext.increaseProgressCount();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const resource of resources) {
 | 
					        for (const resource of resources || []) {
 | 
				
			||||||
            if (!resource.content) {
 | 
					            if (!resource.content) {
 | 
				
			||||||
                continue;
 | 
					                continue;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            resource.content = utils.fromBase64(resource.content);
 | 
					            if (typeof resource.content === "string") {
 | 
				
			||||||
 | 
					                resource.content = utils.fromBase64(resource.content);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const hash = utils.md5(resource.content);
 | 
					            const hash = utils.md5(resource.content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -273,6 +310,10 @@ function importEnex(taskContext, file, parentNote) {
 | 
				
			|||||||
            resource.mime = resource.mime || "application/octet-stream";
 | 
					            resource.mime = resource.mime || "application/octet-stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const createFileNote = () => {
 | 
					            const createFileNote = () => {
 | 
				
			||||||
 | 
					                if (typeof resource.content !== "string") {
 | 
				
			||||||
 | 
					                    throw new Error("Missing or wrong content type for resource.");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
                const resourceNote = noteService.createNewNote({
 | 
					                const resourceNote = noteService.createNewNote({
 | 
				
			||||||
                    parentNoteId: noteEntity.noteId,
 | 
					                    parentNoteId: noteEntity.noteId,
 | 
				
			||||||
                    title: resource.title,
 | 
					                    title: resource.title,
 | 
				
			||||||
@ -292,7 +333,7 @@ function importEnex(taskContext, file, parentNote) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                const resourceLink = `<a href="#root/${resourceNote.noteId}">${utils.escapeHtml(resource.title)}</a>`;
 | 
					                const resourceLink = `<a href="#root/${resourceNote.noteId}">${utils.escapeHtml(resource.title)}</a>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                content = content.replace(mediaRegex, resourceLink);
 | 
					                content = (content || "").replace(mediaRegex, resourceLink);
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (resource.mime && resource.mime.startsWith('image/')) {
 | 
					            if (resource.mime && resource.mime.startsWith('image/')) {
 | 
				
			||||||
@ -301,7 +342,7 @@ function importEnex(taskContext, file, parentNote) {
 | 
				
			|||||||
                        ? resource.title
 | 
					                        ? resource.title
 | 
				
			||||||
                        : `image.${resource.mime.substr(6)}`; // default if real name is not present
 | 
					                        : `image.${resource.mime.substr(6)}`; // default if real name is not present
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    const attachment = imageService.saveImageToAttachment(noteEntity.noteId, resource.content, originalName, taskContext.data.shrinkImages);
 | 
					                    const attachment = imageService.saveImageToAttachment(noteEntity.noteId, resource.content, originalName, !!taskContext.data?.shrinkImages);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    const encodedTitle = encodeURIComponent(attachment.title);
 | 
					                    const encodedTitle = encodeURIComponent(attachment.title);
 | 
				
			||||||
                    const url = `api/attachments/${attachment.attachmentId}/image/${encodedTitle}`;
 | 
					                    const url = `api/attachments/${attachment.attachmentId}/image/${encodedTitle}`;
 | 
				
			||||||
@ -314,7 +355,7 @@ function importEnex(taskContext, file, parentNote) {
 | 
				
			|||||||
                        // otherwise the image would be removed since no note would include it
 | 
					                        // otherwise the image would be removed since no note would include it
 | 
				
			||||||
                        content += imageLink;
 | 
					                        content += imageLink;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                } catch (e) {
 | 
					                } catch (e: any) {
 | 
				
			||||||
                    log.error(`error when saving image from ENEX file: ${e.message}`);
 | 
					                    log.error(`error when saving image from ENEX file: ${e.message}`);
 | 
				
			||||||
                    createFileNote();
 | 
					                    createFileNote();
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
@ -368,4 +409,4 @@ function importEnex(taskContext, file, parentNote) {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = { importEnex };
 | 
					export = { importEnex };
 | 
				
			||||||
@ -1,18 +0,0 @@
 | 
				
			|||||||
"use strict";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const marked = require("marked");
 | 
					 | 
				
			||||||
const htmlSanitizer = require('../html_sanitizer');
 | 
					 | 
				
			||||||
const importUtils = require('./utils');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function renderToHtml(content, title) {
 | 
					 | 
				
			||||||
    const html = marked.parse(content, {
 | 
					 | 
				
			||||||
        mangle: false,
 | 
					 | 
				
			||||||
        headerIds: false
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    const h1Handled = importUtils.handleH1(html, title); // h1 handling needs to come before sanitization
 | 
					 | 
				
			||||||
    return htmlSanitizer.sanitize(h1Handled);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
module.exports = {
 | 
					 | 
				
			||||||
    renderToHtml
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
							
								
								
									
										17
									
								
								src/services/import/markdown.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/services/import/markdown.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import marked = require("marked");
 | 
				
			||||||
 | 
					import htmlSanitizer = require('../html_sanitizer');
 | 
				
			||||||
 | 
					import importUtils = require('./utils');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function renderToHtml(content: string, title: string) {
 | 
				
			||||||
 | 
					    const html = marked.parse(content, {
 | 
				
			||||||
 | 
					        async: false
 | 
				
			||||||
 | 
					    }) as string;
 | 
				
			||||||
 | 
					    const h1Handled = importUtils.handleH1(html, title); // h1 handling needs to come before sanitization
 | 
				
			||||||
 | 
					    return htmlSanitizer.sanitize(h1Handled);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export = {
 | 
				
			||||||
 | 
					    renderToHtml
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -1,9 +1,10 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mimeTypes = require('mime-types');
 | 
					import mimeTypes = require('mime-types');
 | 
				
			||||||
const path = require('path');
 | 
					import path = require('path');
 | 
				
			||||||
 | 
					import { TaskData } from '../task_context_interface';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const CODE_MIME_TYPES = {
 | 
					const CODE_MIME_TYPES: Record<string, boolean | string> = {
 | 
				
			||||||
    'text/plain': true,
 | 
					    'text/plain': true,
 | 
				
			||||||
    'text/x-csrc': true,
 | 
					    'text/x-csrc': true,
 | 
				
			||||||
    'text/x-c++src': true,
 | 
					    'text/x-c++src': true,
 | 
				
			||||||
@ -44,7 +45,7 @@ const CODE_MIME_TYPES = {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// extensions missing in mime-db
 | 
					// extensions missing in mime-db
 | 
				
			||||||
const EXTENSION_TO_MIME = {
 | 
					const EXTENSION_TO_MIME: Record<string, string> = {
 | 
				
			||||||
    ".c": "text/x-csrc",
 | 
					    ".c": "text/x-csrc",
 | 
				
			||||||
    ".cs": "text/x-csharp",
 | 
					    ".cs": "text/x-csharp",
 | 
				
			||||||
    ".clj": "text/x-clojure",
 | 
					    ".clj": "text/x-clojure",
 | 
				
			||||||
@ -65,7 +66,7 @@ const EXTENSION_TO_MIME = {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** @returns false if MIME is not detected */
 | 
					/** @returns false if MIME is not detected */
 | 
				
			||||||
function getMime(fileName) {
 | 
					function getMime(fileName: string) {
 | 
				
			||||||
    if (fileName.toLowerCase() === 'dockerfile') {
 | 
					    if (fileName.toLowerCase() === 'dockerfile') {
 | 
				
			||||||
        return "text/x-dockerfile";
 | 
					        return "text/x-dockerfile";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -79,7 +80,7 @@ function getMime(fileName) {
 | 
				
			|||||||
    return mimeTypes.lookup(fileName);
 | 
					    return mimeTypes.lookup(fileName);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getType(options, mime) {
 | 
					function getType(options: TaskData, mime: string) {
 | 
				
			||||||
    mime = mime ? mime.toLowerCase() : '';
 | 
					    mime = mime ? mime.toLowerCase() : '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (options.textImportedAsText && (mime === 'text/html' || ['text/markdown', 'text/x-markdown'].includes(mime))) {
 | 
					    if (options.textImportedAsText && (mime === 'text/html' || ['text/markdown', 'text/x-markdown'].includes(mime))) {
 | 
				
			||||||
@ -96,18 +97,20 @@ function getType(options, mime) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function normalizeMimeType(mime) {
 | 
					function normalizeMimeType(mime: string) {
 | 
				
			||||||
    mime = mime ? mime.toLowerCase() : '';
 | 
					    mime = mime ? mime.toLowerCase() : '';
 | 
				
			||||||
 | 
					    const mappedMime = CODE_MIME_TYPES[mime];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!(mime in CODE_MIME_TYPES) || CODE_MIME_TYPES[mime] === true) {
 | 
					    if (mappedMime === true) {
 | 
				
			||||||
        return mime;
 | 
					        return mime;
 | 
				
			||||||
 | 
					    } else if (typeof mappedMime === "string") {
 | 
				
			||||||
 | 
					        return mappedMime;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else {
 | 
					
 | 
				
			||||||
        return CODE_MIME_TYPES[mime];
 | 
					    return undefined;
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					export = {
 | 
				
			||||||
    getMime,
 | 
					    getMime,
 | 
				
			||||||
    getType,
 | 
					    getType,
 | 
				
			||||||
    normalizeMimeType
 | 
					    normalizeMimeType
 | 
				
			||||||
@ -1,20 +1,37 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const noteService = require('../../services/notes');
 | 
					import noteService = require('../../services/notes');
 | 
				
			||||||
const parseString = require('xml2js').parseString;
 | 
					import xml2js = require("xml2js");
 | 
				
			||||||
const protectedSessionService = require('../protected_session');
 | 
					import protectedSessionService = require('../protected_session');
 | 
				
			||||||
const htmlSanitizer = require('../html_sanitizer');
 | 
					import htmlSanitizer = require('../html_sanitizer');
 | 
				
			||||||
 | 
					import TaskContext = require('../task_context');
 | 
				
			||||||
 | 
					import BNote = require('../../becca/entities/bnote');
 | 
				
			||||||
 | 
					const parseString = xml2js.parseString;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					interface OpmlXml {
 | 
				
			||||||
 * @param {TaskContext} taskContext
 | 
					    opml: OpmlBody;
 | 
				
			||||||
 * @param {Buffer} fileBuffer
 | 
					}
 | 
				
			||||||
 * @param {BNote} parentNote
 | 
					
 | 
				
			||||||
 * @returns {Promise<*[]|*>}
 | 
					interface OpmlBody {
 | 
				
			||||||
 */
 | 
					    $: {
 | 
				
			||||||
async function importOpml(taskContext, fileBuffer, parentNote) {
 | 
					        version: string
 | 
				
			||||||
    const xml = await new Promise(function(resolve, reject)
 | 
					    }
 | 
				
			||||||
 | 
					    body: OpmlOutline[]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface OpmlOutline {
 | 
				
			||||||
 | 
					    $: {
 | 
				
			||||||
 | 
					        title: string;
 | 
				
			||||||
 | 
					        text: string;
 | 
				
			||||||
 | 
					        _note: string;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    outline: OpmlOutline[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function importOpml(taskContext: TaskContext, fileBuffer: Buffer, parentNote: BNote) {
 | 
				
			||||||
 | 
					    const xml = await new Promise<OpmlXml>(function(resolve, reject)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        parseString(fileBuffer, function (err, result) {
 | 
					        parseString(fileBuffer, function (err: any, result: OpmlXml) {
 | 
				
			||||||
            if (err) {
 | 
					            if (err) {
 | 
				
			||||||
                reject(err);
 | 
					                reject(err);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -30,7 +47,7 @@ async function importOpml(taskContext, fileBuffer, parentNote) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const opmlVersion = parseInt(xml.opml.$.version);
 | 
					    const opmlVersion = parseInt(xml.opml.$.version);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function importOutline(outline, parentNoteId) {
 | 
					    function importOutline(outline: OpmlOutline, parentNoteId: string) {
 | 
				
			||||||
        let title, content;
 | 
					        let title, content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (opmlVersion === 1) {
 | 
					        if (opmlVersion === 1) {
 | 
				
			||||||
@ -83,7 +100,7 @@ async function importOpml(taskContext, fileBuffer, parentNote) {
 | 
				
			|||||||
    return returnNote;
 | 
					    return returnNote;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function toHtml(text) {
 | 
					function toHtml(text: string) {
 | 
				
			||||||
    if (!text) {
 | 
					    if (!text) {
 | 
				
			||||||
        return '';
 | 
					        return '';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -91,6 +108,6 @@ function toHtml(text) {
 | 
				
			|||||||
    return `<p>${text.replace(/(?:\r\n|\r|\n)/g, '</p><p>')}</p>`;
 | 
					    return `<p>${text.replace(/(?:\r\n|\r|\n)/g, '</p><p>')}</p>`;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					export = {
 | 
				
			||||||
    importOpml
 | 
					    importOpml
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -1,18 +1,22 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const noteService = require('../../services/notes');
 | 
					import BNote = require("../../becca/entities/bnote");
 | 
				
			||||||
const imageService = require('../../services/image.js');
 | 
					import TaskContext = require("../task_context");
 | 
				
			||||||
const protectedSessionService = require('../protected_session');
 | 
					 | 
				
			||||||
const markdownService = require('./markdown.js');
 | 
					 | 
				
			||||||
const mimeService = require('./mime.js');
 | 
					 | 
				
			||||||
const utils = require('../../services/utils');
 | 
					 | 
				
			||||||
const importUtils = require('./utils');
 | 
					 | 
				
			||||||
const htmlSanitizer = require('../html_sanitizer');
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
function importSingleFile(taskContext, file, parentNote) {
 | 
					import noteService = require('../../services/notes');
 | 
				
			||||||
 | 
					import imageService = require('../../services/image');
 | 
				
			||||||
 | 
					import protectedSessionService = require('../protected_session');
 | 
				
			||||||
 | 
					import markdownService = require('./markdown');
 | 
				
			||||||
 | 
					import mimeService = require('./mime');
 | 
				
			||||||
 | 
					import utils = require('../../services/utils');
 | 
				
			||||||
 | 
					import importUtils = require('./utils');
 | 
				
			||||||
 | 
					import htmlSanitizer = require('../html_sanitizer');
 | 
				
			||||||
 | 
					import { File } from "./common";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function importSingleFile(taskContext: TaskContext, file: File, parentNote: BNote) {
 | 
				
			||||||
    const mime = mimeService.getMime(file.originalname) || file.mimetype;
 | 
					    const mime = mimeService.getMime(file.originalname) || file.mimetype;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (taskContext.data.textImportedAsText) {
 | 
					    if (taskContext?.data?.textImportedAsText) {
 | 
				
			||||||
        if (mime === 'text/html') {
 | 
					        if (mime === 'text/html') {
 | 
				
			||||||
            return importHtml(taskContext, file, parentNote);
 | 
					            return importHtml(taskContext, file, parentNote);
 | 
				
			||||||
        } else if (['text/markdown', 'text/x-markdown'].includes(mime)) {
 | 
					        } else if (['text/markdown', 'text/x-markdown'].includes(mime)) {
 | 
				
			||||||
@ -22,7 +26,7 @@ function importSingleFile(taskContext, file, parentNote) {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (taskContext.data.codeImportedAsCode && mimeService.getType(taskContext.data, mime) === 'code') {
 | 
					    if (taskContext?.data?.codeImportedAsCode && mimeService.getType(taskContext.data, mime) === 'code') {
 | 
				
			||||||
        return importCodeNote(taskContext, file, parentNote);
 | 
					        return importCodeNote(taskContext, file, parentNote);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -33,15 +37,21 @@ function importSingleFile(taskContext, file, parentNote) {
 | 
				
			|||||||
    return importFile(taskContext, file, parentNote);
 | 
					    return importFile(taskContext, file, parentNote);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function importImage(file, parentNote, taskContext) {
 | 
					function importImage(file: File, parentNote: BNote, taskContext: TaskContext) {
 | 
				
			||||||
    const {note} = imageService.saveImage(parentNote.noteId, file.buffer, file.originalname, taskContext.data.shrinkImages);
 | 
					    if (typeof file.buffer === "string") {
 | 
				
			||||||
 | 
					        throw new Error("Invalid file content for image.");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const {note} = imageService.saveImage(parentNote.noteId, file.buffer, file.originalname, !!taskContext.data?.shrinkImages);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    taskContext.increaseProgressCount();
 | 
					    taskContext.increaseProgressCount();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return note;
 | 
					    return note;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function importFile(taskContext, file, parentNote) {
 | 
					function importFile(taskContext: TaskContext, file: File, parentNote: BNote) {
 | 
				
			||||||
 | 
					    if (typeof file.buffer !== "string") {
 | 
				
			||||||
 | 
					        throw new Error("Invalid file content for text.");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    const originalName = file.originalname;
 | 
					    const originalName = file.originalname;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const {note} = noteService.createNewNote({
 | 
					    const {note} = noteService.createNewNote({
 | 
				
			||||||
@ -60,8 +70,8 @@ function importFile(taskContext, file, parentNote) {
 | 
				
			|||||||
    return note;
 | 
					    return note;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function importCodeNote(taskContext, file, parentNote) {
 | 
					function importCodeNote(taskContext: TaskContext, file: File, parentNote: BNote) {
 | 
				
			||||||
    const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces);
 | 
					    const title = utils.getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces);
 | 
				
			||||||
    const content = file.buffer.toString("utf-8");
 | 
					    const content = file.buffer.toString("utf-8");
 | 
				
			||||||
    const detectedMime = mimeService.getMime(file.originalname) || file.mimetype;
 | 
					    const detectedMime = mimeService.getMime(file.originalname) || file.mimetype;
 | 
				
			||||||
    const mime = mimeService.normalizeMimeType(detectedMime);
 | 
					    const mime = mimeService.normalizeMimeType(detectedMime);
 | 
				
			||||||
@ -80,8 +90,8 @@ function importCodeNote(taskContext, file, parentNote) {
 | 
				
			|||||||
    return note;
 | 
					    return note;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function importPlainText(taskContext, file, parentNote) {
 | 
					function importPlainText(taskContext: TaskContext, file: File, parentNote: BNote) {
 | 
				
			||||||
    const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces);
 | 
					    const title = utils.getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces);
 | 
				
			||||||
    const plainTextContent = file.buffer.toString("utf-8");
 | 
					    const plainTextContent = file.buffer.toString("utf-8");
 | 
				
			||||||
    const htmlContent = convertTextToHtml(plainTextContent);
 | 
					    const htmlContent = convertTextToHtml(plainTextContent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -99,7 +109,7 @@ function importPlainText(taskContext, file, parentNote) {
 | 
				
			|||||||
    return note;
 | 
					    return note;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function convertTextToHtml(text) {
 | 
					function convertTextToHtml(text: string) {
 | 
				
			||||||
    // 1: Plain Text Search
 | 
					    // 1: Plain Text Search
 | 
				
			||||||
    text = text.replace(/&/g, "&").
 | 
					    text = text.replace(/&/g, "&").
 | 
				
			||||||
    replace(/</g, "<").
 | 
					    replace(/</g, "<").
 | 
				
			||||||
@ -117,13 +127,13 @@ function convertTextToHtml(text) {
 | 
				
			|||||||
    return text;
 | 
					    return text;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function importMarkdown(taskContext, file, parentNote) {
 | 
					function importMarkdown(taskContext: TaskContext, file: File, parentNote: BNote) {
 | 
				
			||||||
    const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces);
 | 
					    const title = utils.getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const markdownContent = file.buffer.toString("utf-8");
 | 
					    const markdownContent = file.buffer.toString("utf-8");
 | 
				
			||||||
    let htmlContent = markdownService.renderToHtml(markdownContent, title);
 | 
					    let htmlContent = markdownService.renderToHtml(markdownContent, title);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (taskContext.data.safeImport) {
 | 
					    if (taskContext.data?.safeImport) {
 | 
				
			||||||
        htmlContent = htmlSanitizer.sanitize(htmlContent);
 | 
					        htmlContent = htmlSanitizer.sanitize(htmlContent);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -141,11 +151,11 @@ function importMarkdown(taskContext, file, parentNote) {
 | 
				
			|||||||
    return note;
 | 
					    return note;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function importHtml(taskContext, file, parentNote) {
 | 
					function importHtml(taskContext: TaskContext, file: File, parentNote: BNote) {
 | 
				
			||||||
    const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces);
 | 
					    const title = utils.getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces);
 | 
				
			||||||
    let content = file.buffer.toString("utf-8");
 | 
					    let content = file.buffer.toString("utf-8");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (taskContext.data.safeImport) {
 | 
					    if (taskContext?.data?.safeImport) {
 | 
				
			||||||
        content = htmlSanitizer.sanitize(content);
 | 
					        content = htmlSanitizer.sanitize(content);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -165,17 +175,11 @@ function importHtml(taskContext, file, parentNote) {
 | 
				
			|||||||
    return note;
 | 
					    return note;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					function importAttachment(taskContext: TaskContext, file: File, parentNote: BNote) {
 | 
				
			||||||
 * @param {TaskContext} taskContext
 | 
					 | 
				
			||||||
 * @param file
 | 
					 | 
				
			||||||
 * @param {BNote} parentNote
 | 
					 | 
				
			||||||
 * @returns {BNote}
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
function importAttachment(taskContext, file, parentNote) {
 | 
					 | 
				
			||||||
    const mime = mimeService.getMime(file.originalname) || file.mimetype;
 | 
					    const mime = mimeService.getMime(file.originalname) || file.mimetype;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (mime.startsWith("image/")) {
 | 
					    if (mime.startsWith("image/") && typeof file.buffer !== "string") {
 | 
				
			||||||
        imageService.saveImageToAttachment(parentNote.noteId, file.buffer, file.originalname, taskContext.data.shrinkImages);
 | 
					        imageService.saveImageToAttachment(parentNote.noteId, file.buffer, file.originalname, taskContext.data?.shrinkImages);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        taskContext.increaseProgressCount();
 | 
					        taskContext.increaseProgressCount();
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
@ -190,7 +194,7 @@ function importAttachment(taskContext, file, parentNote) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					export = {
 | 
				
			||||||
    importSingleFile,
 | 
					    importSingleFile,
 | 
				
			||||||
    importAttachment
 | 
					    importAttachment
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function handleH1(content, title) {
 | 
					function handleH1(content: string, title: string) {
 | 
				
			||||||
    content = content.replace(/<h1>([^<]*)<\/h1>/gi, (match, text) => {
 | 
					    content = content.replace(/<h1>([^<]*)<\/h1>/gi, (match, text) => {
 | 
				
			||||||
        if (title.trim() === text.trim()) {
 | 
					        if (title.trim() === text.trim()) {
 | 
				
			||||||
            return ""; // remove whole H1 tag
 | 
					            return ""; // remove whole H1 tag
 | 
				
			||||||
@ -11,6 +11,6 @@ function handleH1(content, title) {
 | 
				
			|||||||
    return content;
 | 
					    return content;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					export = {
 | 
				
			||||||
    handleH1
 | 
					    handleH1
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -1,43 +1,45 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const BAttribute = require('../../becca/entities/battribute');
 | 
					import BAttribute = require('../../becca/entities/battribute');
 | 
				
			||||||
const utils = require('../../services/utils');
 | 
					import utils = require('../../services/utils');
 | 
				
			||||||
const log = require('../../services/log');
 | 
					import log = require('../../services/log');
 | 
				
			||||||
const noteService = require('../../services/notes');
 | 
					import noteService = require('../../services/notes');
 | 
				
			||||||
const attributeService = require('../../services/attributes');
 | 
					import attributeService = require('../../services/attributes');
 | 
				
			||||||
const BBranch = require('../../becca/entities/bbranch');
 | 
					import BBranch = require('../../becca/entities/bbranch');
 | 
				
			||||||
const path = require('path');
 | 
					import path = require('path');
 | 
				
			||||||
const protectedSessionService = require('../protected_session');
 | 
					import protectedSessionService = require('../protected_session');
 | 
				
			||||||
const mimeService = require('./mime.js');
 | 
					import mimeService = require('./mime');
 | 
				
			||||||
const treeService = require('../tree');
 | 
					import treeService = require('../tree');
 | 
				
			||||||
const yauzl = require("yauzl");
 | 
					import yauzl = require("yauzl");
 | 
				
			||||||
const htmlSanitizer = require('../html_sanitizer');
 | 
					import htmlSanitizer = require('../html_sanitizer');
 | 
				
			||||||
const becca = require('../../becca/becca');
 | 
					import becca = require('../../becca/becca');
 | 
				
			||||||
const BAttachment = require('../../becca/entities/battachment');
 | 
					import BAttachment = require('../../becca/entities/battachment');
 | 
				
			||||||
const markdownService = require('./markdown.js');
 | 
					import markdownService = require('./markdown');
 | 
				
			||||||
 | 
					import TaskContext = require('../task_context');
 | 
				
			||||||
 | 
					import BNote = require('../../becca/entities/bnote');
 | 
				
			||||||
 | 
					import NoteMeta = require('../meta/note_meta');
 | 
				
			||||||
 | 
					import AttributeMeta = require('../meta/attribute_meta');
 | 
				
			||||||
 | 
					import { Stream } from 'stream';
 | 
				
			||||||
 | 
					import { NoteType } from '../../becca/entities/rows';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					interface MetaFile {
 | 
				
			||||||
 * @param {TaskContext} taskContext
 | 
					    files: NoteMeta[]
 | 
				
			||||||
 * @param {Buffer} fileBuffer
 | 
					}
 | 
				
			||||||
 * @param {BNote} importRootNote
 | 
					
 | 
				
			||||||
 * @returns {Promise<BNote>}
 | 
					async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRootNote: BNote): Promise<BNote> {
 | 
				
			||||||
 */
 | 
					    /** maps from original noteId (in ZIP file) to newly generated noteId */
 | 
				
			||||||
async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
					    const noteIdMap: Record<string, string> = {};
 | 
				
			||||||
    /** @type {Object.<string, string>} maps from original noteId (in ZIP file) to newly generated noteId */
 | 
					    /** type maps from original attachmentId (in ZIP file) to newly generated attachmentId */
 | 
				
			||||||
    const noteIdMap = {};
 | 
					    const attachmentIdMap: Record<string, string> = {};
 | 
				
			||||||
    /** @type {Object.<string, string>} maps from original attachmentId (in ZIP file) to newly generated attachmentId */
 | 
					    const attributes: AttributeMeta[] = [];
 | 
				
			||||||
    const attachmentIdMap = {};
 | 
					 | 
				
			||||||
    const attributes = [];
 | 
					 | 
				
			||||||
    // path => noteId, used only when meta file is not available
 | 
					    // path => noteId, used only when meta file is not available
 | 
				
			||||||
    /** @type {Object.<string, string>} path => noteId | attachmentId */
 | 
					    /** path => noteId | attachmentId */
 | 
				
			||||||
    const createdPaths = { '/': importRootNote.noteId, '\\': importRootNote.noteId };
 | 
					    const createdPaths: Record<string, string> = { '/': importRootNote.noteId, '\\': importRootNote.noteId };
 | 
				
			||||||
    let metaFile = null;
 | 
					    let metaFile: MetaFile | null = null;
 | 
				
			||||||
    /** @type {BNote} */
 | 
					    let firstNote: BNote | null = null;
 | 
				
			||||||
    let firstNote = null;
 | 
					    const createdNoteIds = new Set<string>();
 | 
				
			||||||
    /** @type {Set.<string>} */
 | 
					 | 
				
			||||||
    const createdNoteIds = new Set();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function getNewNoteId(origNoteId) {
 | 
					    function getNewNoteId(origNoteId: string) {
 | 
				
			||||||
        if (!origNoteId.trim()) {
 | 
					        if (!origNoteId.trim()) {
 | 
				
			||||||
            // this probably shouldn't happen, but still good to have this precaution
 | 
					            // this probably shouldn't happen, but still good to have this precaution
 | 
				
			||||||
            return "empty_note_id";
 | 
					            return "empty_note_id";
 | 
				
			||||||
@ -55,7 +57,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
        return noteIdMap[origNoteId];
 | 
					        return noteIdMap[origNoteId];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function getNewAttachmentId(origAttachmentId) {
 | 
					    function getNewAttachmentId(origAttachmentId: string) {
 | 
				
			||||||
        if (!origAttachmentId.trim()) {
 | 
					        if (!origAttachmentId.trim()) {
 | 
				
			||||||
            // this probably shouldn't happen, but still good to have this precaution
 | 
					            // this probably shouldn't happen, but still good to have this precaution
 | 
				
			||||||
            return "empty_attachment_id";
 | 
					            return "empty_attachment_id";
 | 
				
			||||||
@ -68,12 +70,8 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
        return attachmentIdMap[origAttachmentId];
 | 
					        return attachmentIdMap[origAttachmentId];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    function getAttachmentMeta(parentNoteMeta: NoteMeta, dataFileName: string) {
 | 
				
			||||||
     * @param {NoteMeta} parentNoteMeta
 | 
					        for (const noteMeta of parentNoteMeta.children || []) {
 | 
				
			||||||
     * @param {string} dataFileName
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    function getAttachmentMeta(parentNoteMeta, dataFileName) {
 | 
					 | 
				
			||||||
        for (const noteMeta of parentNoteMeta.children) {
 | 
					 | 
				
			||||||
            for (const attachmentMeta of noteMeta.attachments || []) {
 | 
					            for (const attachmentMeta of noteMeta.attachments || []) {
 | 
				
			||||||
                if (attachmentMeta.dataFileName === dataFileName) {
 | 
					                if (attachmentMeta.dataFileName === dataFileName) {
 | 
				
			||||||
                    return {
 | 
					                    return {
 | 
				
			||||||
@ -88,22 +86,20 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
        return {};
 | 
					        return {};
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** @returns {{noteMeta: NoteMeta|undefined, parentNoteMeta: NoteMeta|undefined, attachmentMeta: AttachmentMeta|undefined}} */
 | 
					    function getMeta(filePath: string) {
 | 
				
			||||||
    function getMeta(filePath) {
 | 
					 | 
				
			||||||
        if (!metaFile) {
 | 
					        if (!metaFile) {
 | 
				
			||||||
            return {};
 | 
					            return {};
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const pathSegments = filePath.split(/[\/\\]/g);
 | 
					        const pathSegments = filePath.split(/[\/\\]/g);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /** @type {NoteMeta} */
 | 
					        let cursor: NoteMeta | undefined = {
 | 
				
			||||||
        let cursor = {
 | 
					 | 
				
			||||||
            isImportRoot: true,
 | 
					            isImportRoot: true,
 | 
				
			||||||
            children: metaFile.files
 | 
					            children: metaFile.files,
 | 
				
			||||||
 | 
					            dataFileName: ""
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /** @type {NoteMeta} */
 | 
					        let parent: NoteMeta | undefined = undefined;
 | 
				
			||||||
        let parent;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const segment of pathSegments) {
 | 
					        for (const segment of pathSegments) {
 | 
				
			||||||
            if (!cursor?.children?.length) {
 | 
					            if (!cursor?.children?.length) {
 | 
				
			||||||
@ -111,7 +107,9 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            parent = cursor;
 | 
					            parent = cursor;
 | 
				
			||||||
            cursor = parent.children.find(file => file.dataFileName === segment || file.dirFileName === segment);
 | 
					            if (parent.children) {
 | 
				
			||||||
 | 
					                cursor = parent.children.find(file => file.dataFileName === segment || file.dirFileName === segment);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!cursor) {
 | 
					            if (!cursor) {
 | 
				
			||||||
                return getAttachmentMeta(parent, segment);
 | 
					                return getAttachmentMeta(parent, segment);
 | 
				
			||||||
@ -120,19 +118,15 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
            parentNoteMeta: parent,
 | 
					            parentNoteMeta: parent,
 | 
				
			||||||
            noteMeta: cursor
 | 
					            noteMeta: cursor,
 | 
				
			||||||
 | 
					            attachmentMeta: null
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    function getParentNoteId(filePath: string, parentNoteMeta?: NoteMeta) {
 | 
				
			||||||
     * @param {string} filePath
 | 
					 | 
				
			||||||
     * @param {NoteMeta} parentNoteMeta
 | 
					 | 
				
			||||||
     * @return {string}
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    function getParentNoteId(filePath, parentNoteMeta) {
 | 
					 | 
				
			||||||
        let parentNoteId;
 | 
					        let parentNoteId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (parentNoteMeta) {
 | 
					        if (parentNoteMeta?.noteId) {
 | 
				
			||||||
            parentNoteId = parentNoteMeta.isImportRoot ? importRootNote.noteId : getNewNoteId(parentNoteMeta.noteId);
 | 
					            parentNoteId = parentNoteMeta.isImportRoot ? importRootNote.noteId : getNewNoteId(parentNoteMeta.noteId);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else {
 | 
					        else {
 | 
				
			||||||
@ -151,13 +145,8 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
        return parentNoteId;
 | 
					        return parentNoteId;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    function getNoteId(noteMeta: NoteMeta | undefined, filePath: string): string {
 | 
				
			||||||
     * @param {NoteMeta} noteMeta
 | 
					        if (noteMeta?.noteId) {
 | 
				
			||||||
     * @param {string} filePath
 | 
					 | 
				
			||||||
     * @return {string}
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    function getNoteId(noteMeta, filePath) {
 | 
					 | 
				
			||||||
        if (noteMeta) {
 | 
					 | 
				
			||||||
            return getNewNoteId(noteMeta.noteId);
 | 
					            return getNewNoteId(noteMeta.noteId);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -176,23 +165,19 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
        return noteId;
 | 
					        return noteId;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function detectFileTypeAndMime(taskContext, filePath) {
 | 
					    function detectFileTypeAndMime(taskContext: TaskContext, filePath: string) {
 | 
				
			||||||
        const mime = mimeService.getMime(filePath) || "application/octet-stream";
 | 
					        const mime = mimeService.getMime(filePath) || "application/octet-stream";
 | 
				
			||||||
        const type = mimeService.getType(taskContext.data, mime);
 | 
					        const type = mimeService.getType(taskContext.data || {}, mime);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return { mime, type };
 | 
					        return { mime, type };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    function saveAttributes(note: BNote, noteMeta: NoteMeta | undefined) {
 | 
				
			||||||
     * @param {BNote} note
 | 
					 | 
				
			||||||
     * @param {NoteMeta} noteMeta
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    function saveAttributes(note, noteMeta) {
 | 
					 | 
				
			||||||
        if (!noteMeta) {
 | 
					        if (!noteMeta) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const attr of noteMeta.attributes) {
 | 
					        for (const attr of noteMeta.attributes || []) {
 | 
				
			||||||
            attr.noteId = note.noteId;
 | 
					            attr.noteId = note.noteId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (attr.type === 'label-definition') {
 | 
					            if (attr.type === 'label-definition') {
 | 
				
			||||||
@ -218,11 +203,11 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
                attr.value = getNewNoteId(attr.value);
 | 
					                attr.value = getNewNoteId(attr.value);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (taskContext.data.safeImport && attributeService.isAttributeDangerous(attr.type, attr.name)) {
 | 
					            if (taskContext.data?.safeImport && attributeService.isAttributeDangerous(attr.type, attr.name)) {
 | 
				
			||||||
                attr.name = `disabled:${attr.name}`;
 | 
					                attr.name = `disabled:${attr.name}`;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (taskContext.data.safeImport) {
 | 
					            if (taskContext.data?.safeImport) {
 | 
				
			||||||
                attr.name = htmlSanitizer.sanitize(attr.name);
 | 
					                attr.name = htmlSanitizer.sanitize(attr.name);
 | 
				
			||||||
                attr.value = htmlSanitizer.sanitize(attr.value);
 | 
					                attr.value = htmlSanitizer.sanitize(attr.value);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -231,7 +216,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function saveDirectory(filePath) {
 | 
					    function saveDirectory(filePath: string) {
 | 
				
			||||||
        const { parentNoteMeta, noteMeta } = getMeta(filePath);
 | 
					        const { parentNoteMeta, noteMeta } = getMeta(filePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const noteId = getNoteId(noteMeta, filePath);
 | 
					        const noteId = getNoteId(noteMeta, filePath);
 | 
				
			||||||
@ -240,12 +225,16 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const noteTitle = utils.getNoteTitle(filePath, taskContext.data.replaceUnderscoresWithSpaces, noteMeta);
 | 
					        const noteTitle = utils.getNoteTitle(filePath, !!taskContext.data?.replaceUnderscoresWithSpaces, noteMeta);
 | 
				
			||||||
        const parentNoteId = getParentNoteId(filePath, parentNoteMeta);
 | 
					        const parentNoteId = getParentNoteId(filePath, parentNoteMeta);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!parentNoteId) {
 | 
				
			||||||
 | 
					            throw new Error("Missing parent note ID.");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const {note} = noteService.createNewNote({
 | 
					        const {note} = noteService.createNewNote({
 | 
				
			||||||
            parentNoteId: parentNoteId,
 | 
					            parentNoteId: parentNoteId,
 | 
				
			||||||
            title: noteTitle,
 | 
					            title: noteTitle || "",
 | 
				
			||||||
            content: '',
 | 
					            content: '',
 | 
				
			||||||
            noteId: noteId,
 | 
					            noteId: noteId,
 | 
				
			||||||
            type: resolveNoteType(noteMeta?.type),
 | 
					            type: resolveNoteType(noteMeta?.type),
 | 
				
			||||||
@ -265,8 +254,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
        return noteId;
 | 
					        return noteId;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** @returns {{attachmentId: string}|{noteId: string}} */
 | 
					    function getEntityIdFromRelativeUrl(url: string, filePath: string) {
 | 
				
			||||||
    function getEntityIdFromRelativeUrl(url, filePath) {
 | 
					 | 
				
			||||||
        while (url.startsWith("./")) {
 | 
					        while (url.startsWith("./")) {
 | 
				
			||||||
            url = url.substr(2);
 | 
					            url = url.substr(2);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -287,7 +275,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        const { noteMeta, attachmentMeta } = getMeta(absUrl);
 | 
					        const { noteMeta, attachmentMeta } = getMeta(absUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (attachmentMeta) {
 | 
					        if (attachmentMeta && attachmentMeta.attachmentId && noteMeta.noteId) {
 | 
				
			||||||
            return {
 | 
					            return {
 | 
				
			||||||
                attachmentId: getNewAttachmentId(attachmentMeta.attachmentId),
 | 
					                attachmentId: getNewAttachmentId(attachmentMeta.attachmentId),
 | 
				
			||||||
                noteId: getNewNoteId(noteMeta.noteId)
 | 
					                noteId: getNewNoteId(noteMeta.noteId)
 | 
				
			||||||
@ -299,15 +287,8 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    function processTextNoteContent(content: string, noteTitle: string, filePath: string, noteMeta?: NoteMeta) {
 | 
				
			||||||
     * @param {string} content
 | 
					        function isUrlAbsolute(url: string) {
 | 
				
			||||||
     * @param {string} noteTitle
 | 
					 | 
				
			||||||
     * @param {string} filePath
 | 
					 | 
				
			||||||
     * @param {NoteMeta} noteMeta
 | 
					 | 
				
			||||||
     * @return {string}
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    function processTextNoteContent(content, noteTitle, filePath, noteMeta) {
 | 
					 | 
				
			||||||
        function isUrlAbsolute(url) {
 | 
					 | 
				
			||||||
            return /^(?:[a-z]+:)?\/\//i.test(url);
 | 
					            return /^(?:[a-z]+:)?\/\//i.test(url);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -321,7 +302,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (taskContext.data.safeImport) {
 | 
					        if (taskContext.data?.safeImport) {
 | 
				
			||||||
            content = htmlSanitizer.sanitize(content);
 | 
					            content = htmlSanitizer.sanitize(content);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -336,7 +317,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
                url = decodeURIComponent(url).trim();
 | 
					                url = decodeURIComponent(url).trim();
 | 
				
			||||||
            } catch (e) {
 | 
					            } catch (e: any) {
 | 
				
			||||||
                log.error(`Cannot parse image URL '${url}', keeping original. Error: ${e.message}.`);
 | 
					                log.error(`Cannot parse image URL '${url}', keeping original. Error: ${e.message}.`);
 | 
				
			||||||
                return `src="${url}"`;
 | 
					                return `src="${url}"`;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -359,7 +340,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
        content = content.replace(/href="([^"]*)"/g, (match, url) => {
 | 
					        content = content.replace(/href="([^"]*)"/g, (match, url) => {
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
                url = decodeURIComponent(url).trim();
 | 
					                url = decodeURIComponent(url).trim();
 | 
				
			||||||
            } catch (e) {
 | 
					            } catch (e: any) {
 | 
				
			||||||
                log.error(`Cannot parse link URL '${url}', keeping original. Error: ${e.message}.`);
 | 
					                log.error(`Cannot parse link URL '${url}', keeping original. Error: ${e.message}.`);
 | 
				
			||||||
                return `href="${url}"`;
 | 
					                return `href="${url}"`;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -395,7 +376,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
        return content;
 | 
					        return content;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function removeTriliumTags(content) {
 | 
					    function removeTriliumTags(content: string) {
 | 
				
			||||||
        const tagsToRemove = [
 | 
					        const tagsToRemove = [
 | 
				
			||||||
            '<h1 data-trilium-h1>([^<]*)<\/h1>',
 | 
					            '<h1 data-trilium-h1>([^<]*)<\/h1>',
 | 
				
			||||||
            '<title data-trilium-title>([^<]*)<\/title>'
 | 
					            '<title data-trilium-title>([^<]*)<\/title>'
 | 
				
			||||||
@ -407,26 +388,18 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
        return content;
 | 
					        return content;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    function processNoteContent(noteMeta: NoteMeta | undefined, type: string, mime: string, content: string | Buffer, noteTitle: string, filePath: string) {
 | 
				
			||||||
     * @param {NoteMeta} noteMeta
 | 
					        if ((noteMeta?.format === 'markdown'
 | 
				
			||||||
     * @param {string} type
 | 
					            || (!noteMeta && taskContext.data?.textImportedAsText && ['text/markdown', 'text/x-markdown'].includes(mime)))
 | 
				
			||||||
     * @param {string} mime
 | 
					            && typeof content === "string") {
 | 
				
			||||||
     * @param {string|Buffer} content
 | 
					 | 
				
			||||||
     * @param {string} noteTitle
 | 
					 | 
				
			||||||
     * @param {string} filePath
 | 
					 | 
				
			||||||
     * @return {string}
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    function processNoteContent(noteMeta, type, mime, content, noteTitle, filePath) {
 | 
					 | 
				
			||||||
        if (noteMeta?.format === 'markdown'
 | 
					 | 
				
			||||||
            || (!noteMeta && taskContext.data.textImportedAsText && ['text/markdown', 'text/x-markdown'].includes(mime))) {
 | 
					 | 
				
			||||||
            content = markdownService.renderToHtml(content, noteTitle);
 | 
					            content = markdownService.renderToHtml(content, noteTitle);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (type === 'text') {
 | 
					        if (type === 'text' && typeof content === "string") {
 | 
				
			||||||
            content = processTextNoteContent(content, noteTitle, filePath, noteMeta);
 | 
					            content = processTextNoteContent(content, noteTitle, filePath, noteMeta);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (type === 'relationMap' && noteMeta) {
 | 
					        if (type === 'relationMap' && noteMeta && typeof content === "string") {
 | 
				
			||||||
            const relationMapLinks = (noteMeta.attributes || [])
 | 
					            const relationMapLinks = (noteMeta.attributes || [])
 | 
				
			||||||
                .filter(attr => attr.type === 'relation' && attr.name === 'relationMapLink');
 | 
					                .filter(attr => attr.type === 'relation' && attr.name === 'relationMapLink');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -440,11 +413,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
        return content;
 | 
					        return content;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    function saveNote(filePath: string, content: string | Buffer) {
 | 
				
			||||||
     * @param {string} filePath
 | 
					 | 
				
			||||||
     * @param {Buffer} content
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    function saveNote(filePath, content) {
 | 
					 | 
				
			||||||
        const { parentNoteMeta, noteMeta, attachmentMeta } = getMeta(filePath);
 | 
					        const { parentNoteMeta, noteMeta, attachmentMeta } = getMeta(filePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (noteMeta?.noImport) {
 | 
					        if (noteMeta?.noImport) {
 | 
				
			||||||
@ -453,7 +422,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        const noteId = getNoteId(noteMeta, filePath);
 | 
					        const noteId = getNoteId(noteMeta, filePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (attachmentMeta) {
 | 
					        if (attachmentMeta && attachmentMeta.attachmentId) {
 | 
				
			||||||
            const attachment = new BAttachment({
 | 
					            const attachment = new BAttachment({
 | 
				
			||||||
                attachmentId: getNewAttachmentId(attachmentMeta.attachmentId),
 | 
					                attachmentId: getNewAttachmentId(attachmentMeta.attachmentId),
 | 
				
			||||||
                ownerId: noteId,
 | 
					                ownerId: noteId,
 | 
				
			||||||
@ -487,16 +456,20 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let { type, mime } = noteMeta ? noteMeta : detectFileTypeAndMime(taskContext, filePath);
 | 
					        let { mime } = noteMeta ? noteMeta : detectFileTypeAndMime(taskContext, filePath);
 | 
				
			||||||
        type = resolveNoteType(type);
 | 
					        if (!mime) {
 | 
				
			||||||
 | 
					            throw new Error("Unable to resolve mime type.");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let type = resolveNoteType(noteMeta?.type);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (type !== 'file' && type !== 'image') {
 | 
					        if (type !== 'file' && type !== 'image') {
 | 
				
			||||||
            content = content.toString("utf-8");
 | 
					            content = content.toString("utf-8");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const noteTitle = utils.getNoteTitle(filePath, taskContext.data.replaceUnderscoresWithSpaces, noteMeta);
 | 
					        const noteTitle = utils.getNoteTitle(filePath, taskContext.data?.replaceUnderscoresWithSpaces || false, noteMeta);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        content = processNoteContent(noteMeta, type, mime, content, noteTitle, filePath);
 | 
					        content = processNoteContent(noteMeta, type, mime, content, noteTitle || "", filePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let note = becca.getNote(noteId);
 | 
					        let note = becca.getNote(noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -508,7 +481,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
            if (note.type === undefined) {
 | 
					            if (note.type === undefined) {
 | 
				
			||||||
                note.type = type;
 | 
					                note.type = type;
 | 
				
			||||||
                note.mime = mime;
 | 
					                note.mime = mime;
 | 
				
			||||||
                note.title = noteTitle;
 | 
					                note.title = noteTitle || "";
 | 
				
			||||||
                note.isProtected = isProtected;
 | 
					                note.isProtected = isProtected;
 | 
				
			||||||
                note.save();
 | 
					                note.save();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -519,16 +492,20 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
                new BBranch({
 | 
					                new BBranch({
 | 
				
			||||||
                    noteId,
 | 
					                    noteId,
 | 
				
			||||||
                    parentNoteId,
 | 
					                    parentNoteId,
 | 
				
			||||||
                    isExpanded: noteMeta.isExpanded,
 | 
					                    isExpanded: noteMeta?.isExpanded,
 | 
				
			||||||
                    prefix: noteMeta.prefix,
 | 
					                    prefix: noteMeta?.prefix,
 | 
				
			||||||
                    notePosition: noteMeta.notePosition
 | 
					                    notePosition: noteMeta?.notePosition
 | 
				
			||||||
                }).save();
 | 
					                }).save();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else {
 | 
					        else {
 | 
				
			||||||
 | 
					            if (typeof content !== "string") {
 | 
				
			||||||
 | 
					                throw new Error("Incorrect content type.");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            ({note} = noteService.createNewNote({
 | 
					            ({note} = noteService.createNewNote({
 | 
				
			||||||
                parentNoteId: parentNoteId,
 | 
					                parentNoteId: parentNoteId,
 | 
				
			||||||
                title: noteTitle,
 | 
					                title: noteTitle || "",
 | 
				
			||||||
                content: content,
 | 
					                content: content,
 | 
				
			||||||
                noteId,
 | 
					                noteId,
 | 
				
			||||||
                type,
 | 
					                type,
 | 
				
			||||||
@ -560,7 +537,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // we're running two passes to make sure that the meta file is loaded before the rest of the files is processed.
 | 
					    // we're running two passes to make sure that the meta file is loaded before the rest of the files is processed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await readZipFile(fileBuffer, async (zipfile, entry) => {
 | 
					    await readZipFile(fileBuffer, async (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => {
 | 
				
			||||||
        const filePath = normalizeFilePath(entry.fileName);
 | 
					        const filePath = normalizeFilePath(entry.fileName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (filePath === '!!!meta.json') {
 | 
					        if (filePath === '!!!meta.json') {
 | 
				
			||||||
@ -572,7 +549,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
        zipfile.readEntry();
 | 
					        zipfile.readEntry();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await readZipFile(fileBuffer, async (zipfile, entry) => {
 | 
					    await readZipFile(fileBuffer, async (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => {
 | 
				
			||||||
        const filePath = normalizeFilePath(entry.fileName);
 | 
					        const filePath = normalizeFilePath(entry.fileName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (/\/$/.test(entry.fileName)) {
 | 
					        if (/\/$/.test(entry.fileName)) {
 | 
				
			||||||
@ -590,6 +567,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    for (const noteId of createdNoteIds) {
 | 
					    for (const noteId of createdNoteIds) {
 | 
				
			||||||
        const note = becca.getNote(noteId);
 | 
					        const note = becca.getNote(noteId);
 | 
				
			||||||
 | 
					        if (!note) continue;
 | 
				
			||||||
        await noteService.asyncPostProcessContent(note, note.getContent());
 | 
					        await noteService.asyncPostProcessContent(note, note.getContent());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!metaFile) {
 | 
					        if (!metaFile) {
 | 
				
			||||||
@ -612,11 +590,15 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!firstNote) {
 | 
				
			||||||
 | 
					        throw new Error("Unable to determine first note.");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return firstNote;
 | 
					    return firstNote;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** @returns {string} path without leading or trailing slash and backslashes converted to forward ones */
 | 
					/** @returns path without leading or trailing slash and backslashes converted to forward ones */
 | 
				
			||||||
function normalizeFilePath(filePath) {
 | 
					function normalizeFilePath(filePath: string): string {
 | 
				
			||||||
    filePath = filePath.replace(/\\/g, "/");
 | 
					    filePath = filePath.replace(/\\/g, "/");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (filePath.startsWith("/")) {
 | 
					    if (filePath.startsWith("/")) {
 | 
				
			||||||
@ -630,29 +612,30 @@ function normalizeFilePath(filePath) {
 | 
				
			|||||||
    return filePath;
 | 
					    return filePath;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** @returns {Promise<Buffer>} */
 | 
					function streamToBuffer(stream: Stream): Promise<Buffer> {
 | 
				
			||||||
function streamToBuffer(stream) {
 | 
					    const chunks: Uint8Array[] = [];
 | 
				
			||||||
    const chunks = [];
 | 
					 | 
				
			||||||
    stream.on('data', chunk => chunks.push(chunk));
 | 
					    stream.on('data', chunk => chunks.push(chunk));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return new Promise((res, rej) => stream.on('end', () => res(Buffer.concat(chunks))));
 | 
					    return new Promise((res, rej) => stream.on('end', () => res(Buffer.concat(chunks))));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** @returns {Promise<Buffer>} */
 | 
					function readContent(zipfile: yauzl.ZipFile, entry: yauzl.Entry): Promise<Buffer> {
 | 
				
			||||||
function readContent(zipfile, entry) {
 | 
					 | 
				
			||||||
    return new Promise((res, rej) => {
 | 
					    return new Promise((res, rej) => {
 | 
				
			||||||
        zipfile.openReadStream(entry, function(err, readStream) {
 | 
					        zipfile.openReadStream(entry, function(err, readStream) {
 | 
				
			||||||
            if (err) rej(err);
 | 
					            if (err) rej(err);
 | 
				
			||||||
 | 
					            if (!readStream) throw new Error("Unable to read content.");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            streamToBuffer(readStream).then(res);
 | 
					            streamToBuffer(readStream).then(res);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function readZipFile(buffer, processEntryCallback) {
 | 
					function readZipFile(buffer: Buffer, processEntryCallback: (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => void) {
 | 
				
			||||||
    return new Promise((res, rej) => {
 | 
					    return new Promise((res, rej) => {
 | 
				
			||||||
        yauzl.fromBuffer(buffer, {lazyEntries: true, validateEntrySizes: false}, function(err, zipfile) {
 | 
					        yauzl.fromBuffer(buffer, {lazyEntries: true, validateEntrySizes: false}, function(err, zipfile) {
 | 
				
			||||||
            if (err) throw err;
 | 
					            if (err) throw err;
 | 
				
			||||||
 | 
					            if (!zipfile) throw new Error("Unable to read zip file.");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            zipfile.readEntry();
 | 
					            zipfile.readEntry();
 | 
				
			||||||
            zipfile.on("entry", entry => processEntryCallback(zipfile, entry));
 | 
					            zipfile.on("entry", entry => processEntryCallback(zipfile, entry));
 | 
				
			||||||
            zipfile.on("end", res);
 | 
					            zipfile.on("end", res);
 | 
				
			||||||
@ -660,20 +643,19 @@ function readZipFile(buffer, processEntryCallback) {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function resolveNoteType(type) {
 | 
					function resolveNoteType(type: string | undefined): NoteType {
 | 
				
			||||||
    // BC for ZIPs created in Triliun 0.57 and older
 | 
					    // BC for ZIPs created in Triliun 0.57 and older
 | 
				
			||||||
    if (type === 'relation-map') {
 | 
					    if (type === 'relation-map') {
 | 
				
			||||||
        type = 'relationMap';
 | 
					        return 'relationMap';
 | 
				
			||||||
    } else if (type === 'note-map') {
 | 
					    } else if (type === 'note-map') {
 | 
				
			||||||
        type = 'noteMap';
 | 
					        return 'noteMap';
 | 
				
			||||||
    } else if (type === 'web-view') {
 | 
					    } else if (type === 'web-view') {
 | 
				
			||||||
        type = 'webView';
 | 
					        return 'webView';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return type || "text";
 | 
					    return "text";
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export = {
 | 
				
			||||||
module.exports = {
 | 
					 | 
				
			||||||
    importZip
 | 
					    importZip
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -1,9 +1,9 @@
 | 
				
			|||||||
interface AttachmentMeta {
 | 
					interface AttachmentMeta {
 | 
				
			||||||
    attachmentId: string;
 | 
					    attachmentId?: string;
 | 
				
			||||||
    title: string;
 | 
					    title: string;
 | 
				
			||||||
    role: string;
 | 
					    role: string;
 | 
				
			||||||
    mime: string;
 | 
					    mime: string;
 | 
				
			||||||
    position: number;
 | 
					    position?: number;
 | 
				
			||||||
    dataFileName: string;
 | 
					    dataFileName: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,12 @@
 | 
				
			|||||||
 | 
					import { AttributeType } from "../../becca/entities/rows";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface AttributeMeta {
 | 
					interface AttributeMeta {
 | 
				
			||||||
    type: string;
 | 
					    noteId?: string;
 | 
				
			||||||
 | 
					    type: AttributeType;
 | 
				
			||||||
    name: string;
 | 
					    name: string;
 | 
				
			||||||
    value: string;
 | 
					    value: string;
 | 
				
			||||||
    isInheritable: boolean;
 | 
					    isInheritable?: boolean;
 | 
				
			||||||
    position: number;
 | 
					    position?: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export = AttributeMeta;
 | 
					export = AttributeMeta;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,23 +1,25 @@
 | 
				
			|||||||
 | 
					import AttachmentMeta = require("./attachment_meta");
 | 
				
			||||||
import AttributeMeta = require("./attribute_meta");
 | 
					import AttributeMeta = require("./attribute_meta");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface NoteMeta {
 | 
					interface NoteMeta {
 | 
				
			||||||
    noteId: string;
 | 
					    noteId?: string;
 | 
				
			||||||
    notePath: string;
 | 
					    notePath?: string[];
 | 
				
			||||||
    isClone: boolean;
 | 
					    isClone?: boolean;
 | 
				
			||||||
    title: string;
 | 
					    title?: string;
 | 
				
			||||||
    notePosition: number;
 | 
					    notePosition?: number;
 | 
				
			||||||
    prefix: string;
 | 
					    prefix?: string | null;
 | 
				
			||||||
    isExpanded: boolean;
 | 
					    isExpanded?: boolean;
 | 
				
			||||||
    type: string;
 | 
					    type?: string;
 | 
				
			||||||
    mime: string;
 | 
					    mime?: string;
 | 
				
			||||||
    /** 'html' or 'markdown', applicable to text notes only */
 | 
					    /** 'html' or 'markdown', applicable to text notes only */
 | 
				
			||||||
    format: "html" | "markdown";
 | 
					    format?: "html" | "markdown";
 | 
				
			||||||
    dataFileName: string;
 | 
					    dataFileName: string;
 | 
				
			||||||
    dirFileName: string;
 | 
					    dirFileName?: string;
 | 
				
			||||||
    /** this file should not be imported (e.g., HTML navigation) */
 | 
					    /** this file should not be imported (e.g., HTML navigation) */
 | 
				
			||||||
    noImport: boolean;
 | 
					    noImport?: boolean;
 | 
				
			||||||
    attributes: AttributeMeta[];
 | 
					    isImportRoot?: boolean;
 | 
				
			||||||
    attachments: AttributeMeta[];
 | 
					    attributes?: AttributeMeta[];
 | 
				
			||||||
 | 
					    attachments?: AttachmentMeta[];
 | 
				
			||||||
    children?: NoteMeta[];
 | 
					    children?: NoteMeta[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										25
									
								
								src/services/note-interface.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/services/note-interface.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { NoteType } from "../becca/entities/rows";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface NoteParams {
 | 
				
			||||||
 | 
					    /** optionally can force specific noteId */
 | 
				
			||||||
 | 
					    noteId?: string;
 | 
				
			||||||
 | 
					    parentNoteId: string;
 | 
				
			||||||
 | 
					    templateNoteId?: string;
 | 
				
			||||||
 | 
					    title: string;
 | 
				
			||||||
 | 
					    content: string;
 | 
				
			||||||
 | 
					    type: NoteType;
 | 
				
			||||||
 | 
					    /** default value is derived from default mimes for type */
 | 
				
			||||||
 | 
					    mime?: string;
 | 
				
			||||||
 | 
					    /** default is false */
 | 
				
			||||||
 | 
					    isProtected?: boolean;
 | 
				
			||||||
 | 
					    /** default is false */
 | 
				
			||||||
 | 
					    isExpanded?: boolean;
 | 
				
			||||||
 | 
					    /** default is empty string */
 | 
				
			||||||
 | 
					    prefix?: string;
 | 
				
			||||||
 | 
					    /** default is the last existing notePosition in a parent + 10 */
 | 
				
			||||||
 | 
					    notePosition?: number;
 | 
				
			||||||
 | 
					    dateCreated?: string;
 | 
				
			||||||
 | 
					    utcDateCreated?: string;
 | 
				
			||||||
 | 
					    ignoreForbiddenParents?: boolean;
 | 
				
			||||||
 | 
					    target?: "into";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -155,6 +155,7 @@ function getAndValidateParent(params: GetValidateParams) {
 | 
				
			|||||||
interface NoteParams {
 | 
					interface NoteParams {
 | 
				
			||||||
    /** optionally can force specific noteId */
 | 
					    /** optionally can force specific noteId */
 | 
				
			||||||
    noteId?: string;
 | 
					    noteId?: string;
 | 
				
			||||||
 | 
					    branchId?: string;
 | 
				
			||||||
    parentNoteId: string;
 | 
					    parentNoteId: string;
 | 
				
			||||||
    templateNoteId?: string;
 | 
					    templateNoteId?: string;
 | 
				
			||||||
    title: string;
 | 
					    title: string;
 | 
				
			||||||
@ -167,7 +168,7 @@ interface NoteParams {
 | 
				
			|||||||
    /** default is false */
 | 
					    /** default is false */
 | 
				
			||||||
    isExpanded?: boolean;
 | 
					    isExpanded?: boolean;
 | 
				
			||||||
    /** default is empty string */
 | 
					    /** default is empty string */
 | 
				
			||||||
    prefix?: string;
 | 
					    prefix?: string | null;
 | 
				
			||||||
    /** default is the last existing notePosition in a parent + 10 */
 | 
					    /** default is the last existing notePosition in a parent + 10 */
 | 
				
			||||||
    notePosition?: number;
 | 
					    notePosition?: number;
 | 
				
			||||||
    dateCreated?: string;
 | 
					    dateCreated?: string;
 | 
				
			||||||
@ -506,7 +507,7 @@ async function downloadImage(noteId: string, imageUrl: string) {
 | 
				
			|||||||
        const parsedUrl = url.parse(unescapedUrl);
 | 
					        const parsedUrl = url.parse(unescapedUrl);
 | 
				
			||||||
        const title = path.basename(parsedUrl.pathname || "");
 | 
					        const title = path.basename(parsedUrl.pathname || "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const imageService = require('../services/image.js');
 | 
					        const imageService = require('../services/image');
 | 
				
			||||||
        const attachment = imageService.saveImageToAttachment(noteId, imageBuffer, title, true, true);
 | 
					        const attachment = imageService.saveImageToAttachment(noteId, imageBuffer, title, true, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        imageUrlToAttachmentIdMapping[imageUrl] = attachment.attachmentId;
 | 
					        imageUrlToAttachmentIdMapping[imageUrl] = attachment.attachmentId;
 | 
				
			||||||
@ -539,7 +540,7 @@ function downloadImages(noteId: string, content: string) {
 | 
				
			|||||||
            const imageBase64 = url.substr(inlineImageMatch[0].length);
 | 
					            const imageBase64 = url.substr(inlineImageMatch[0].length);
 | 
				
			||||||
            const imageBuffer = Buffer.from(imageBase64, 'base64');
 | 
					            const imageBuffer = Buffer.from(imageBase64, 'base64');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const imageService = require('../services/image.js');
 | 
					            const imageService = require('../services/image');
 | 
				
			||||||
            const attachment = imageService.saveImageToAttachment(noteId, imageBuffer, "inline image", true, true);
 | 
					            const attachment = imageService.saveImageToAttachment(noteId, imageBuffer, "inline image", true, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const encodedTitle = encodeURIComponent(attachment.title);
 | 
					            const encodedTitle = encodeURIComponent(attachment.title);
 | 
				
			||||||
@ -657,7 +658,7 @@ function saveAttachments(note: BNote, content: string) {
 | 
				
			|||||||
    return content;
 | 
					    return content;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function saveLinks(note: BNote, content: string) {
 | 
					function saveLinks(note: BNote, content: string | Buffer) {
 | 
				
			||||||
    if ((note.type !== 'text' && note.type !== 'relationMap')
 | 
					    if ((note.type !== 'text' && note.type !== 'relationMap')
 | 
				
			||||||
        || (note.isProtected && !protectedSessionService.isProtectedSessionAvailable())) {
 | 
					        || (note.isProtected && !protectedSessionService.isProtectedSessionAvailable())) {
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
@ -669,7 +670,7 @@ function saveLinks(note: BNote, content: string) {
 | 
				
			|||||||
    const foundLinks: FoundLink[] = [];
 | 
					    const foundLinks: FoundLink[] = [];
 | 
				
			||||||
    let forceFrontendReload = false;
 | 
					    let forceFrontendReload = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (note.type === 'text') {
 | 
					    if (note.type === 'text' && typeof content === "string") {
 | 
				
			||||||
        content = downloadImages(note.noteId, content);
 | 
					        content = downloadImages(note.noteId, content);
 | 
				
			||||||
        content = saveAttachments(note, content);
 | 
					        content = saveAttachments(note, content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -679,7 +680,7 @@ function saveLinks(note: BNote, content: string) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        ({forceFrontendReload, content} = checkImageAttachments(note, content));
 | 
					        ({forceFrontendReload, content} = checkImageAttachments(note, content));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else if (note.type === 'relationMap') {
 | 
					    else if (note.type === 'relationMap' && typeof content === "string") {
 | 
				
			||||||
        findRelationMapLinks(content, foundLinks);
 | 
					        findRelationMapLinks(content, foundLinks);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else {
 | 
					    else {
 | 
				
			||||||
@ -874,7 +875,7 @@ function getUndeletedParentBranchIds(noteId: string, deleteId: string) {
 | 
				
			|||||||
                      AND parentNote.isDeleted = 0`, [noteId, deleteId]);
 | 
					                      AND parentNote.isDeleted = 0`, [noteId, deleteId]);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function scanForLinks(note: BNote, content: string) {
 | 
					function scanForLinks(note: BNote, content: string | Buffer) {
 | 
				
			||||||
    if (!note || !['text', 'relationMap'].includes(note.type)) {
 | 
					    if (!note || !['text', 'relationMap'].includes(note.type)) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -896,7 +897,7 @@ function scanForLinks(note: BNote, content: string) {
 | 
				
			|||||||
/**
 | 
					/**
 | 
				
			||||||
 * Things which have to be executed after updating content, but asynchronously (separate transaction)
 | 
					 * Things which have to be executed after updating content, but asynchronously (separate transaction)
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
async function asyncPostProcessContent(note: BNote, content: string) {
 | 
					async function asyncPostProcessContent(note: BNote, content: string | Buffer) {
 | 
				
			||||||
    if (cls.isMigrationRunning()) {
 | 
					    if (cls.isMigrationRunning()) {
 | 
				
			||||||
        // this is rarely needed for migrations, but can cause trouble by e.g. triggering downloads
 | 
					        // this is rarely needed for migrations, but can cause trouble by e.g. triggering downloads
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@ function getOptionOrNull(name: string): string | null {
 | 
				
			|||||||
    return option ? option.value : null;
 | 
					    return option ? option.value : null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getOption(name: string): string {
 | 
					function getOption(name: string) {
 | 
				
			||||||
    const val = getOptionOrNull(name);
 | 
					    const val = getOptionOrNull(name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (val === null) {
 | 
					    if (val === null) {
 | 
				
			||||||
@ -44,15 +44,15 @@ function getOptionInt(name: string, defaultValue?: number): number {
 | 
				
			|||||||
function getOptionBool(name: string): boolean {
 | 
					function getOptionBool(name: string): boolean {
 | 
				
			||||||
    const val = getOption(name);
 | 
					    const val = getOption(name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!['true', 'false'].includes(val)) {
 | 
					    if (typeof val !== "string" || !['true', 'false'].includes(val)) {
 | 
				
			||||||
        throw new Error(`Could not parse '${val}' into boolean for option '${name}'`);
 | 
					        throw new Error(`Could not parse '${val}' into boolean for option '${name}'`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return val === 'true';
 | 
					    return val === 'true';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setOption(name: string, value: string | boolean) {
 | 
					function setOption(name: string, value: string | number | boolean) {
 | 
				
			||||||
    if (value === true || value === false) {
 | 
					    if (value === true || value === false || typeof value === "number") {
 | 
				
			||||||
        value = value.toString();
 | 
					        value = value.toString();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -68,7 +68,7 @@ function setOption(name: string, value: string | boolean) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function createOption(name: string, value: string, isSynced: boolean) {
 | 
					function createOption(name: string, value: string | number, isSynced: boolean) {
 | 
				
			||||||
    // to avoid circular dependency, need to find a better solution
 | 
					    // to avoid circular dependency, need to find a better solution
 | 
				
			||||||
    const BOption = require('../becca/entities/boption');
 | 
					    const BOption = require('../becca/entities/boption');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -84,7 +84,7 @@ function getOptions() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getOptionMap() {
 | 
					function getOptionMap() {
 | 
				
			||||||
    const map: Record<string, string> = {};
 | 
					    const map: Record<string | number, string> = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const option of Object.values(becca.options)) {
 | 
					    for (const option of Object.values(becca.options)) {
 | 
				
			||||||
        map[option.name] = option.value;
 | 
					        map[option.name] = option.value;
 | 
				
			||||||
 | 
				
			|||||||
@ -33,7 +33,7 @@ interface Client {
 | 
				
			|||||||
    request(opts: ClientOpts): Request;
 | 
					    request(opts: ClientOpts): Request;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function exec(opts: ExecOpts) {
 | 
					function exec<T>(opts: ExecOpts): Promise<T> {
 | 
				
			||||||
    const client = getClient(opts);
 | 
					    const client = getClient(opts);
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // hack for cases where electron.net does not work, but we don't want to set proxy
 | 
					    // hack for cases where electron.net does not work, but we don't want to set proxy
 | 
				
			||||||
@ -129,7 +129,7 @@ function exec(opts: ExecOpts) {
 | 
				
			|||||||
                    : opts.body;
 | 
					                    : opts.body;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            request.end(payload);
 | 
					            request.end(payload as string);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (e: any) {
 | 
					        catch (e: any) {
 | 
				
			||||||
            reject(generateError(opts, e.message));
 | 
					            reject(generateError(opts, e.message));
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@ export interface CookieJar {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ExecOpts {
 | 
					export interface ExecOpts {
 | 
				
			||||||
    proxy: "noproxy" | null;
 | 
					    proxy: string | null;
 | 
				
			||||||
    method: string;
 | 
					    method: string;
 | 
				
			||||||
    url: string;
 | 
					    url: string;
 | 
				
			||||||
    paging?: {
 | 
					    paging?: {
 | 
				
			||||||
@ -16,5 +16,5 @@ export interface ExecOpts {
 | 
				
			|||||||
        password?: string;
 | 
					        password?: string;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    timeout: number;
 | 
					    timeout: number;
 | 
				
			||||||
    body: string;
 | 
					    body?: string | {};
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -1,15 +1,16 @@
 | 
				
			|||||||
const syncService = require('./sync');
 | 
					import syncService = require('./sync');
 | 
				
			||||||
const log = require('./log');
 | 
					import log = require('./log');
 | 
				
			||||||
const sqlInit = require('./sql_init');
 | 
					import sqlInit = require('./sql_init');
 | 
				
			||||||
const optionService = require('./options');
 | 
					import optionService = require('./options');
 | 
				
			||||||
const syncOptions = require('./sync_options');
 | 
					import syncOptions = require('./sync_options');
 | 
				
			||||||
const request = require('./request');
 | 
					import request = require('./request');
 | 
				
			||||||
const appInfo = require('./app_info');
 | 
					import appInfo = require('./app_info');
 | 
				
			||||||
const utils = require('./utils');
 | 
					import utils = require('./utils');
 | 
				
			||||||
const becca = require('../becca/becca');
 | 
					import becca = require('../becca/becca');
 | 
				
			||||||
 | 
					import { SetupStatusResponse, SetupSyncSeedResponse } from './api-interface';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function hasSyncServerSchemaAndSeed() {
 | 
					async function hasSyncServerSchemaAndSeed() {
 | 
				
			||||||
    const response = await requestToSyncServer('GET', '/api/setup/status');
 | 
					    const response = await requestToSyncServer<SetupStatusResponse>('GET', '/api/setup/status');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (response.syncVersion !== appInfo.syncVersion) {
 | 
					    if (response.syncVersion !== appInfo.syncVersion) {
 | 
				
			||||||
        throw new Error(`Could not setup sync since local sync protocol version is ${appInfo.syncVersion} while remote is ${response.syncVersion}. To fix this issue, use same Trilium version on all instances.`);
 | 
					        throw new Error(`Could not setup sync since local sync protocol version is ${appInfo.syncVersion} while remote is ${response.syncVersion}. To fix this issue, use same Trilium version on all instances.`);
 | 
				
			||||||
@ -32,7 +33,7 @@ function triggerSync() {
 | 
				
			|||||||
async function sendSeedToSyncServer() {
 | 
					async function sendSeedToSyncServer() {
 | 
				
			||||||
    log.info("Initiating sync to server");
 | 
					    log.info("Initiating sync to server");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await requestToSyncServer('POST', '/api/setup/sync-seed', {
 | 
					    await requestToSyncServer<void>('POST', '/api/setup/sync-seed', {
 | 
				
			||||||
        options: getSyncSeedOptions(),
 | 
					        options: getSyncSeedOptions(),
 | 
				
			||||||
        syncVersion: appInfo.syncVersion
 | 
					        syncVersion: appInfo.syncVersion
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -43,7 +44,7 @@ async function sendSeedToSyncServer() {
 | 
				
			|||||||
    optionService.setOption('lastSyncedPull', 0);
 | 
					    optionService.setOption('lastSyncedPull', 0);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function requestToSyncServer(method, path, body = null) {
 | 
					async function requestToSyncServer<T>(method: string, path: string, body?: string | {}): Promise<T> {
 | 
				
			||||||
    const timeout = syncOptions.getSyncTimeout();
 | 
					    const timeout = syncOptions.getSyncTimeout();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return await utils.timeLimit(request.exec({
 | 
					    return await utils.timeLimit(request.exec({
 | 
				
			||||||
@ -52,10 +53,10 @@ async function requestToSyncServer(method, path, body = null) {
 | 
				
			|||||||
        body,
 | 
					        body,
 | 
				
			||||||
        proxy: syncOptions.getSyncProxy(),
 | 
					        proxy: syncOptions.getSyncProxy(),
 | 
				
			||||||
        timeout: timeout
 | 
					        timeout: timeout
 | 
				
			||||||
    }), timeout);
 | 
					    }), timeout) as T;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function setupSyncFromSyncServer(syncServerHost, syncProxy, password) {
 | 
					async function setupSyncFromSyncServer(syncServerHost: string, syncProxy: string, password: string) {
 | 
				
			||||||
    if (sqlInit.isDbInitialized()) {
 | 
					    if (sqlInit.isDbInitialized()) {
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
            result: 'failure',
 | 
					            result: 'failure',
 | 
				
			||||||
@ -67,7 +68,7 @@ async function setupSyncFromSyncServer(syncServerHost, syncProxy, password) {
 | 
				
			|||||||
        log.info("Getting document options FROM sync server.");
 | 
					        log.info("Getting document options FROM sync server.");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // the response is expected to contain documentId and documentSecret options
 | 
					        // the response is expected to contain documentId and documentSecret options
 | 
				
			||||||
        const resp = await request.exec({
 | 
					        const resp = await request.exec<SetupSyncSeedResponse>({
 | 
				
			||||||
            method: 'get',
 | 
					            method: 'get',
 | 
				
			||||||
            url: `${syncServerHost}/api/setup/sync-seed`,
 | 
					            url: `${syncServerHost}/api/setup/sync-seed`,
 | 
				
			||||||
            auth: { password },
 | 
					            auth: { password },
 | 
				
			||||||
@ -92,7 +93,7 @@ async function setupSyncFromSyncServer(syncServerHost, syncProxy, password) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return { result: 'success' };
 | 
					        return { result: 'success' };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    catch (e) {
 | 
					    catch (e: any) {
 | 
				
			||||||
        log.error(`Sync failed: '${e.message}', stack: ${e.stack}`);
 | 
					        log.error(`Sync failed: '${e.message}', stack: ${e.stack}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
@ -1,9 +1,19 @@
 | 
				
			|||||||
 | 
					type Updater = () => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SpacedUpdate {
 | 
					class SpacedUpdate {
 | 
				
			||||||
    constructor(updater, updateInterval = 1000) {
 | 
					
 | 
				
			||||||
 | 
					    private updater: Updater;
 | 
				
			||||||
 | 
					    private lastUpdated: number;
 | 
				
			||||||
 | 
					    private changed: boolean;
 | 
				
			||||||
 | 
					    private updateInterval: number;
 | 
				
			||||||
 | 
					    private changeForbidden: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor(updater: Updater, updateInterval = 1000) {
 | 
				
			||||||
        this.updater = updater;
 | 
					        this.updater = updater;
 | 
				
			||||||
        this.lastUpdated = Date.now();
 | 
					        this.lastUpdated = Date.now();
 | 
				
			||||||
        this.changed = false;
 | 
					        this.changed = false;
 | 
				
			||||||
        this.updateInterval = updateInterval;
 | 
					        this.updateInterval = updateInterval;
 | 
				
			||||||
 | 
					        this.changeForbidden = false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    scheduleUpdate() {
 | 
					    scheduleUpdate() {
 | 
				
			||||||
@ -52,7 +62,7 @@ class SpacedUpdate {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async allowUpdateWithoutChange(callback) {
 | 
					    async allowUpdateWithoutChange(callback: () => void) {
 | 
				
			||||||
        this.changeForbidden = true;
 | 
					        this.changeForbidden = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
@ -64,4 +74,4 @@ class SpacedUpdate {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = SpacedUpdate;
 | 
					export = SpacedUpdate;
 | 
				
			||||||
@ -1,16 +1,20 @@
 | 
				
			|||||||
const attributeService = require('./attributes');
 | 
					import attributeService = require('./attributes');
 | 
				
			||||||
const dateNoteService = require('./date_notes');
 | 
					import dateNoteService = require('./date_notes');
 | 
				
			||||||
const becca = require('../becca/becca');
 | 
					import becca = require('../becca/becca');
 | 
				
			||||||
const noteService = require('./notes');
 | 
					import noteService = require('./notes');
 | 
				
			||||||
const dateUtils = require('./date_utils');
 | 
					import dateUtils = require('./date_utils');
 | 
				
			||||||
const log = require('./log');
 | 
					import log = require('./log');
 | 
				
			||||||
const hoistedNoteService = require('./hoisted_note');
 | 
					import hoistedNoteService = require('./hoisted_note');
 | 
				
			||||||
const searchService = require('./search/services/search');
 | 
					import searchService = require('./search/services/search');
 | 
				
			||||||
const SearchContext = require('./search/search_context');
 | 
					import SearchContext = require('./search/search_context');
 | 
				
			||||||
const {LBTPL_NOTE_LAUNCHER, LBTPL_CUSTOM_WIDGET, LBTPL_SPACER, LBTPL_SCRIPT} = require('./hidden_subtree');
 | 
					import hiddenSubtree = require('./hidden_subtree');
 | 
				
			||||||
 | 
					const { LBTPL_NOTE_LAUNCHER, LBTPL_CUSTOM_WIDGET, LBTPL_SPACER, LBTPL_SCRIPT } = hiddenSubtree;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getInboxNote(date) {
 | 
					function getInboxNote(date: string) {
 | 
				
			||||||
    const workspaceNote = hoistedNoteService.getWorkspaceNote();
 | 
					    const workspaceNote = hoistedNoteService.getWorkspaceNote();
 | 
				
			||||||
 | 
					    if (!workspaceNote) {
 | 
				
			||||||
 | 
					        throw new Error("Unable to find workspace note");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let inbox;
 | 
					    let inbox;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -48,8 +52,9 @@ function createSqlConsole() {
 | 
				
			|||||||
    return note;
 | 
					    return note;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function saveSqlConsole(sqlConsoleNoteId) {
 | 
					function saveSqlConsole(sqlConsoleNoteId: string) {
 | 
				
			||||||
    const sqlConsoleNote = becca.getNote(sqlConsoleNoteId);
 | 
					    const sqlConsoleNote = becca.getNote(sqlConsoleNoteId);
 | 
				
			||||||
 | 
					    if (!sqlConsoleNote) throw new Error(`Unable to find SQL console note ID: ${sqlConsoleNoteId}`);
 | 
				
			||||||
    const today = dateUtils.localNowDate();
 | 
					    const today = dateUtils.localNowDate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const sqlConsoleHome =
 | 
					    const sqlConsoleHome =
 | 
				
			||||||
@ -59,7 +64,7 @@ function saveSqlConsole(sqlConsoleNoteId) {
 | 
				
			|||||||
    const result = sqlConsoleNote.cloneTo(sqlConsoleHome.noteId);
 | 
					    const result = sqlConsoleNote.cloneTo(sqlConsoleHome.noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const parentBranch of sqlConsoleNote.getParentBranches()) {
 | 
					    for (const parentBranch of sqlConsoleNote.getParentBranches()) {
 | 
				
			||||||
        if (parentBranch.parentNote.hasAncestor('_hidden')) {
 | 
					        if (parentBranch.parentNote?.hasAncestor('_hidden')) {
 | 
				
			||||||
            parentBranch.markAsDeleted();
 | 
					            parentBranch.markAsDeleted();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -67,7 +72,7 @@ function saveSqlConsole(sqlConsoleNoteId) {
 | 
				
			|||||||
    return result;
 | 
					    return result;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function createSearchNote(searchString, ancestorNoteId) {
 | 
					function createSearchNote(searchString: string, ancestorNoteId: string) {
 | 
				
			||||||
    const {note} = noteService.createNewNote({
 | 
					    const {note} = noteService.createNewNote({
 | 
				
			||||||
        parentNoteId: getMonthlyParentNoteId('_search', 'search'),
 | 
					        parentNoteId: getMonthlyParentNoteId('_search', 'search'),
 | 
				
			||||||
        title: `Search: ${searchString}`,
 | 
					        title: `Search: ${searchString}`,
 | 
				
			||||||
@ -88,6 +93,9 @@ function createSearchNote(searchString, ancestorNoteId) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function getSearchHome() {
 | 
					function getSearchHome() {
 | 
				
			||||||
    const workspaceNote = hoistedNoteService.getWorkspaceNote();
 | 
					    const workspaceNote = hoistedNoteService.getWorkspaceNote();
 | 
				
			||||||
 | 
					    if (!workspaceNote) {
 | 
				
			||||||
 | 
					        throw new Error("Unable to find workspace note");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!workspaceNote.isRoot()) {
 | 
					    if (!workspaceNote.isRoot()) {
 | 
				
			||||||
        return workspaceNote.searchNoteInSubtree('#workspaceSearchHome')
 | 
					        return workspaceNote.searchNoteInSubtree('#workspaceSearchHome')
 | 
				
			||||||
@ -101,14 +109,18 @@ function getSearchHome() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function saveSearchNote(searchNoteId) {
 | 
					function saveSearchNote(searchNoteId: string) {
 | 
				
			||||||
    const searchNote = becca.getNote(searchNoteId);
 | 
					    const searchNote = becca.getNote(searchNoteId);
 | 
				
			||||||
 | 
					    if (!searchNote) {
 | 
				
			||||||
 | 
					        throw new Error("Unable to find search note");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const searchHome = getSearchHome();
 | 
					    const searchHome = getSearchHome();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const result = searchNote.cloneTo(searchHome.noteId);
 | 
					    const result = searchNote.cloneTo(searchHome.noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const parentBranch of searchNote.getParentBranches()) {
 | 
					    for (const parentBranch of searchNote.getParentBranches()) {
 | 
				
			||||||
        if (parentBranch.parentNote.hasAncestor('_hidden')) {
 | 
					        if (parentBranch.parentNote?.hasAncestor('_hidden')) {
 | 
				
			||||||
            parentBranch.markAsDeleted();
 | 
					            parentBranch.markAsDeleted();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -116,7 +128,7 @@ function saveSearchNote(searchNoteId) {
 | 
				
			|||||||
    return result;
 | 
					    return result;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getMonthlyParentNoteId(rootNoteId, prefix) {
 | 
					function getMonthlyParentNoteId(rootNoteId: string, prefix: string) {
 | 
				
			||||||
    const month = dateUtils.localNowDate().substring(0, 7);
 | 
					    const month = dateUtils.localNowDate().substring(0, 7);
 | 
				
			||||||
    const labelName = `${prefix}MonthNote`;
 | 
					    const labelName = `${prefix}MonthNote`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -138,7 +150,7 @@ function getMonthlyParentNoteId(rootNoteId, prefix) {
 | 
				
			|||||||
    return monthNote.noteId;
 | 
					    return monthNote.noteId;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function createScriptLauncher(parentNoteId, forceNoteId = null) {
 | 
					function createScriptLauncher(parentNoteId: string, forceNoteId?: string) {
 | 
				
			||||||
    const note = noteService.createNewNote({
 | 
					    const note = noteService.createNewNote({
 | 
				
			||||||
        noteId: forceNoteId,
 | 
					        noteId: forceNoteId,
 | 
				
			||||||
        title: "Script Launcher",
 | 
					        title: "Script Launcher",
 | 
				
			||||||
@ -151,7 +163,13 @@ function createScriptLauncher(parentNoteId, forceNoteId = null) {
 | 
				
			|||||||
    return note;
 | 
					    return note;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function createLauncher({parentNoteId, launcherType, noteId}) {
 | 
					interface LauncherConfig {
 | 
				
			||||||
 | 
					    parentNoteId: string;
 | 
				
			||||||
 | 
					    launcherType: string;
 | 
				
			||||||
 | 
					    noteId: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function createLauncher({ parentNoteId, launcherType, noteId }: LauncherConfig) {
 | 
				
			||||||
    let note;
 | 
					    let note;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (launcherType === 'note') {
 | 
					    if (launcherType === 'note') {
 | 
				
			||||||
@ -197,10 +215,10 @@ function createLauncher({parentNoteId, launcherType, noteId}) {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function resetLauncher(noteId) {
 | 
					function resetLauncher(noteId: string) {
 | 
				
			||||||
    const note = becca.getNote(noteId);
 | 
					    const note = becca.getNote(noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (note.isLaunchBarConfig()) {
 | 
					    if (note?.isLaunchBarConfig()) {
 | 
				
			||||||
        if (note) {
 | 
					        if (note) {
 | 
				
			||||||
            if (noteId === '_lbRoot') {
 | 
					            if (noteId === '_lbRoot') {
 | 
				
			||||||
                // deleting hoisted notes are not allowed, so we just reset the children
 | 
					                // deleting hoisted notes are not allowed, so we just reset the children
 | 
				
			||||||
@ -228,7 +246,13 @@ function resetLauncher(noteId) {
 | 
				
			|||||||
 * Another use case was for script-packages (e.g. demo Task manager) which could this way register automatically/easily
 | 
					 * Another use case was for script-packages (e.g. demo Task manager) which could this way register automatically/easily
 | 
				
			||||||
 * into the launchbar - for this it's recommended to use backend API's createOrUpdateLauncher()
 | 
					 * into the launchbar - for this it's recommended to use backend API's createOrUpdateLauncher()
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function createOrUpdateScriptLauncherFromApi(opts) {
 | 
					function createOrUpdateScriptLauncherFromApi(opts: {
 | 
				
			||||||
 | 
					    id: string;
 | 
				
			||||||
 | 
					    title: string;
 | 
				
			||||||
 | 
					    action: string;
 | 
				
			||||||
 | 
					    icon?: string;
 | 
				
			||||||
 | 
					    shortcut?: string;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
    if (opts.id && !/^[a-z0-9]+$/i.test(opts.id)) {
 | 
					    if (opts.id && !/^[a-z0-9]+$/i.test(opts.id)) {
 | 
				
			||||||
        throw new Error(`Launcher ID can be alphanumeric only, '${opts.id}' given`);
 | 
					        throw new Error(`Launcher ID can be alphanumeric only, '${opts.id}' given`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -263,7 +287,7 @@ function createOrUpdateScriptLauncherFromApi(opts) {
 | 
				
			|||||||
    return launcherNote;
 | 
					    return launcherNote;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					export = {
 | 
				
			||||||
    getInboxNote,
 | 
					    getInboxNote,
 | 
				
			||||||
    createSqlConsole,
 | 
					    createSqlConsole,
 | 
				
			||||||
    saveSqlConsole,
 | 
					    saveSqlConsole,
 | 
				
			||||||
@ -96,7 +96,7 @@ async function createInitialDatabase() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const dummyTaskContext = new TaskContext("no-progress-reporting", 'import', false);
 | 
					    const dummyTaskContext = new TaskContext("no-progress-reporting", 'import', false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const zipImportService = require('./import/zip.js');
 | 
					    const zipImportService = require('./import/zip');
 | 
				
			||||||
    await zipImportService.importZip(dummyTaskContext, demoFile, rootNote);
 | 
					    await zipImportService.importZip(dummyTaskContext, demoFile, rootNote);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sql.transactional(() => {
 | 
					    sql.transactional(() => {
 | 
				
			||||||
@ -179,7 +179,7 @@ dbReady.then(() => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getDbSize() {
 | 
					function getDbSize() {
 | 
				
			||||||
    return sql.getValue("SELECT page_count * page_size / 1000 as size FROM pragma_page_count(), pragma_page_size()");
 | 
					    return sql.getValue<number>("SELECT page_count * page_size / 1000 as size FROM pragma_page_count(), pragma_page_size()");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
log.info(`DB size: ${getDbSize()} KB`);
 | 
					log.info(`DB size: ${getDbSize()} KB`);
 | 
				
			||||||
 | 
				
			|||||||
@ -107,7 +107,7 @@ async function sync() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function login() {
 | 
					async function login() {
 | 
				
			||||||
    const setupService = require('./setup.js'); // circular dependency issue
 | 
					    const setupService = require('./setup'); // circular dependency issue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!await setupService.hasSyncServerSchemaAndSeed()) {
 | 
					    if (!await setupService.hasSyncServerSchemaAndSeed()) {
 | 
				
			||||||
        await setupService.sendSeedToSyncServer();
 | 
					        await setupService.sendSeedToSyncServer();
 | 
				
			||||||
@ -282,7 +282,7 @@ async function checkContentHash(syncContext: SyncContext) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if (failedChecks.length > 0) {
 | 
					    if (failedChecks.length > 0) {
 | 
				
			||||||
        // before re-queuing sectors, make sure the entity changes are correct
 | 
					        // before re-queuing sectors, make sure the entity changes are correct
 | 
				
			||||||
        const consistencyChecks = require('./consistency_checks.js');
 | 
					        const consistencyChecks = require('./consistency_checks');
 | 
				
			||||||
        consistencyChecks.runEntityChangesChecks();
 | 
					        consistencyChecks.runEntityChangesChecks();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await syncRequest(syncContext, 'POST', `/api/sync/check-entity-changes`);
 | 
					        await syncRequest(syncContext, 'POST', `/api/sync/check-entity-changes`);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { TaskData } from './task_context_interface';
 | 
				
			||||||
import ws = require('./ws');
 | 
					import ws = require('./ws');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// taskId => TaskContext
 | 
					// taskId => TaskContext
 | 
				
			||||||
@ -9,9 +10,9 @@ class TaskContext {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    private taskId: string;
 | 
					    private taskId: string;
 | 
				
			||||||
    private taskType: string | null;
 | 
					    private taskType: string | null;
 | 
				
			||||||
    private data: {} | null;
 | 
					 | 
				
			||||||
    private progressCount: number;
 | 
					    private progressCount: number;
 | 
				
			||||||
    private lastSentCountTs: number;
 | 
					    private lastSentCountTs: number;
 | 
				
			||||||
 | 
					    data: TaskData | null;
 | 
				
			||||||
    noteDeletionHandlerTriggered: boolean;
 | 
					    noteDeletionHandlerTriggered: boolean;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    constructor(taskId: string, taskType: string | null = null, data: {} | null = {}) {
 | 
					    constructor(taskId: string, taskType: string | null = null, data: {} | null = {}) {
 | 
				
			||||||
@ -65,7 +66,7 @@ class TaskContext {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    taskSucceeded(result: string) {
 | 
					    taskSucceeded(result?: string) {
 | 
				
			||||||
        ws.sendMessageToAllClients({
 | 
					        ws.sendMessageToAllClients({
 | 
				
			||||||
            type: 'taskSucceeded',
 | 
					            type: 'taskSucceeded',
 | 
				
			||||||
            taskId: this.taskId,
 | 
					            taskId: this.taskId,
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										7
									
								
								src/services/task_context_interface.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/services/task_context_interface.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					export interface TaskData {
 | 
				
			||||||
 | 
					    safeImport?: boolean;
 | 
				
			||||||
 | 
					    textImportedAsText?: boolean;
 | 
				
			||||||
 | 
					    codeImportedAsCode?: boolean;
 | 
				
			||||||
 | 
					    shrinkImages?: boolean;
 | 
				
			||||||
 | 
					    replaceUnderscoresWithSpaces?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -226,8 +226,8 @@ function removeTextFileExtension(filePath: string) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getNoteTitle(filePath: string, replaceUnderscoresWithSpaces: boolean, noteMeta: { title: string }) {
 | 
					function getNoteTitle(filePath: string, replaceUnderscoresWithSpaces: boolean, noteMeta?: { title?: string }) {
 | 
				
			||||||
    if (noteMeta) {
 | 
					    if (noteMeta?.title) {
 | 
				
			||||||
        return noteMeta.title;
 | 
					        return noteMeta.title;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        const basename = path.basename(removeTextFileExtension(filePath));
 | 
					        const basename = path.basename(removeTextFileExtension(filePath));
 | 
				
			||||||
 | 
				
			|||||||
@ -12,6 +12,7 @@ import AbstractBeccaEntity = require('../becca/entities/abstract_becca_entity');
 | 
				
			|||||||
import env = require('./env');
 | 
					import env = require('./env');
 | 
				
			||||||
import { IncomingMessage, Server } from 'http';
 | 
					import { IncomingMessage, Server } from 'http';
 | 
				
			||||||
import { EntityChange } from './entity_changes_interface';
 | 
					import { EntityChange } from './entity_changes_interface';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (env.isDev()) {
 | 
					if (env.isDev()) {
 | 
				
			||||||
    const chokidar = require('chokidar');
 | 
					    const chokidar = require('chokidar');
 | 
				
			||||||
    const debounce = require('debounce');
 | 
					    const debounce = require('debounce');
 | 
				
			||||||
@ -30,7 +31,8 @@ interface Message {
 | 
				
			|||||||
    type: string;
 | 
					    type: string;
 | 
				
			||||||
    data?: {
 | 
					    data?: {
 | 
				
			||||||
        lastSyncedPush?: number | null,
 | 
					        lastSyncedPush?: number | null,
 | 
				
			||||||
        entityChanges?: any[]
 | 
					        entityChanges?: any[],
 | 
				
			||||||
 | 
					        shrinkImages?: boolean
 | 
				
			||||||
    } | null,
 | 
					    } | null,
 | 
				
			||||||
    lastSyncedPush?: number | null,
 | 
					    lastSyncedPush?: number | null,
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										13
									
								
								src/types.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								src/types.d.ts
									
									
									
									
										vendored
									
									
								
							@ -16,4 +16,17 @@ declare module 'html2plaintext' {
 | 
				
			|||||||
declare module 'normalize-strings' {
 | 
					declare module 'normalize-strings' {
 | 
				
			||||||
    function normalizeString(string: string): string;
 | 
					    function normalizeString(string: string): string;
 | 
				
			||||||
    export = normalizeString;
 | 
					    export = normalizeString;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare module 'joplin-turndown-plugin-gfm' {
 | 
				
			||||||
 | 
					    import TurndownService = require("turndown");
 | 
				
			||||||
 | 
					    namespace gfm {
 | 
				
			||||||
 | 
					        function gfm(service: TurndownService): void;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    export = gfm;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare module 'is-animated' {
 | 
				
			||||||
 | 
					    function isAnimated(buffer: Buffer): boolean;
 | 
				
			||||||
 | 
					    export = isAnimated;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -4,7 +4,7 @@ const assetPath = require('./src/services/asset_path');
 | 
				
			|||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    mode: 'production',
 | 
					    mode: 'production',
 | 
				
			||||||
    entry: {
 | 
					    entry: {
 | 
				
			||||||
        setup: './src/public/app/setup.js',
 | 
					        setup: './src/public/app/setup.ts',
 | 
				
			||||||
        mobile: './src/public/app/mobile.js',
 | 
					        mobile: './src/public/app/mobile.js',
 | 
				
			||||||
        desktop: './src/public/app/desktop.js',
 | 
					        desktop: './src/public/app/desktop.js',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user